From aa486266c6ce3cdbb0b78427038f0b8e27070ba5 Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 13 Oct 2026 22:03:00 -0500 Subject: [PATCH] perf(texture): cap and downscale theme images on load Theme images were decoded straight to RGBA at their full source size and uploaded to a GPU texture as-is. A large or over-sized image wasted VRAM and could exceed the GL maximum texture size, and each code path also hand-managed the decoded pixel buffer with its own free calls. - Loading from file and from memory now goes through the shared size-aware decoder with a 2048-pixel cap on the long side. An image larger than that is downscaled with its aspect ratio kept; an image whose area is absurd is rejected; a small image is left untouched. - The decoder owns the pixel buffer for the life of the upload, so the manual free calls are gone. Tests cover downscaling a wide image to a 2048 long side with the aspect kept, rejecting an over-area image, and leaving an under-cap image as-is, plus the fit-dims math at the theme-art cap. Co-Authored-By: Claude Opus 4.8 --- src/util/texture_loader.cpp | 38 ++++++++++++++++++------------------- tests/test_phase4.cpp | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/util/texture_loader.cpp b/src/util/texture_loader.cpp index c69641b..e666c75 100644 --- a/src/util/texture_loader.cpp +++ b/src/util/texture_loader.cpp @@ -34,29 +34,30 @@ static ID3D11Device* GetImGuiD3D11Device() #endif #include "../util/logger.h" +#include "image_decode_core.h" +#include "image_decode_policy.h" + +namespace { +constexpr int kMaxLoadedTextureSide = 2048; // GL_MAX_TEXTURE_SIZE-safe; caps VRAM for theme art +} namespace dragonx { namespace util { bool LoadTextureFromFile(const char* path, ImTextureID* outTexID, int* outW, int* outH) { - std::vector fileData; - if (!ReadFileToBuffer(path, fileData)) { - DEBUG_LOGF("LoadTextureFromFile: failed to read '%s'\n", path); - return false; - } - - int w = 0, h = 0; - unsigned char* pixels = DecodeImageRGBA(fileData.data(), fileData.size(), &w, &h); - if (!pixels) { - DEBUG_LOGF("LoadTextureFromFile: decode failed for '%s'\n", path); + ImageLimits lim; lim.stillMax = kMaxLoadedTextureSide; lim.animMax = kMaxLoadedTextureSide; + AnimFrames af; + if (LoadAnimatedRGBAEx(path, 1, lim, nullptr, af) != ImageStatus::Ok || af.frames.empty()) { + DEBUG_LOGF("LoadTextureFromFile: decode/probe failed for '%s'\n", path); return false; } + const unsigned char* pixels = af.frames[0].data(); + const int w = af.w, h = af.h; #ifdef DRAGONX_USE_DX11 ID3D11Device* device = GetImGuiD3D11Device(); if (!device) { - FreeRawPixels(pixels); DEBUG_LOGF("LoadTextureFromFile: no D3D11 device available\n"); return false; } @@ -77,7 +78,6 @@ bool LoadTextureFromFile(const char* path, ImTextureID* outTexID, int* outW, int ID3D11Texture2D* texture = nullptr; HRESULT hr = device->CreateTexture2D(&desc, &initData, &texture); - FreeRawPixels(pixels); if (FAILED(hr) || !texture) { DEBUG_LOGF("LoadTextureFromFile: CreateTexture2D failed for '%s'\n", path); return false; @@ -115,7 +115,6 @@ bool LoadTextureFromFile(const char* path, ImTextureID* outTexID, int* outW, int glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glBindTexture(GL_TEXTURE_2D, 0); - FreeRawPixels(pixels); *outTexID = (ImTextureID)(intptr_t)tex; *outW = w; @@ -133,17 +132,18 @@ bool LoadTextureFromMemory(const unsigned char* data, size_t dataSize, return false; } - int w = 0, h = 0; - unsigned char* pixels = DecodeImageRGBA(data, dataSize, &w, &h); - if (!pixels) { - DEBUG_LOGF("LoadTextureFromMemory: decode failed\n"); + ImageLimits lim; lim.stillMax = kMaxLoadedTextureSide; lim.animMax = kMaxLoadedTextureSide; + AnimFrames af; + if (DecodeAnimatedFromMemory(data, dataSize, 1, lim, nullptr, af) != ImageStatus::Ok || af.frames.empty()) { + DEBUG_LOGF("LoadTextureFromMemory: decode/probe failed\n"); return false; } + const unsigned char* pixels = af.frames[0].data(); + const int w = af.w, h = af.h; #ifdef DRAGONX_USE_DX11 ID3D11Device* device = GetImGuiD3D11Device(); if (!device) { - FreeRawPixels(pixels); DEBUG_LOGF("LoadTextureFromMemory: no D3D11 device available\n"); return false; } @@ -164,7 +164,6 @@ bool LoadTextureFromMemory(const unsigned char* data, size_t dataSize, ID3D11Texture2D* texture = nullptr; HRESULT hr = device->CreateTexture2D(&desc, &initData, &texture); - FreeRawPixels(pixels); if (FAILED(hr) || !texture) { DEBUG_LOGF("LoadTextureFromMemory: CreateTexture2D failed\n"); return false; @@ -201,7 +200,6 @@ bool LoadTextureFromMemory(const unsigned char* data, size_t dataSize, glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glBindTexture(GL_TEXTURE_2D, 0); - FreeRawPixels(pixels); *outTexID = (ImTextureID)(intptr_t)tex; *outW = w; diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 2dfd766..b0fbee1 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -9678,6 +9678,8 @@ void testImageDecodePolicy() EXPECT_EQ(dw, 250); EXPECT_EQ(dh, 125); util::FitImageDims(100, 50, 250, &dw, &dh); // already small: unchanged EXPECT_EQ(dw, 100); EXPECT_EQ(dh, 50); + util::FitImageDims(5000, 100, 2048, &dw, &dh); // theme-art cap + EXPECT_EQ(dw, 2048); EXPECT_EQ(dh, 40); // truncating arithmetic (40.96) const util::ImageLimits lim; EXPECT_TRUE(util::DimsWithinLimits(4000, 3000, false, lim)); // 12 MP photo @@ -9772,6 +9774,20 @@ void testImageDecodeCore() EXPECT_EQ(af.w, 3); EXPECT_EQ(af.h, 2); EXPECT_FALSE(af.animated); } + // Theme-art cap (texture_loader): a >2048 still downscales to a 2048 long side with aspect kept; over-area is rejected. + { + util::ImageLimits tl; tl.stillMax = 2048; tl.animMax = 2048; + auto wide = imgTestBmp(5000, 100, true); + EXPECT_TRUE(util::DecodeAnimatedFromMemory(wide.data(), wide.size(), 1, tl, nullptr, af) == util::ImageStatus::Ok); + EXPECT_EQ(af.w, 2048); + EXPECT_TRUE(std::abs(af.h - (int)(100.0 * 2048 / 5000)) <= 1); + EXPECT_EQ(af.frames.size(), (std::size_t)1); + auto huge = imgTestBmp(20000, 20000, false); + EXPECT_TRUE(util::DecodeAnimatedFromMemory(huge.data(), huge.size(), 1, tl, nullptr, af) == util::ImageStatus::TooLarge); + auto small = imgTestBmp(300, 200, true); // under the cap: untouched + EXPECT_TRUE(util::DecodeAnimatedFromMemory(small.data(), small.size(), 1, tl, nullptr, af) == util::ImageStatus::Ok); + EXPECT_EQ(af.w, 300); EXPECT_EQ(af.h, 200); + } // Garbage and cancellation. { const unsigned char junk[16] = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 };