Image decoding lived in the GL texture file and had no limits: a file of any size was read whole, stb and libwebp allocated whatever the header claimed (a WebP alone up to 1 GB), and a GIF was decoded at full size, every frame, before the 300-frame cap. A 720p 150-frame GIF avatar cost 0.2-1.5 s and 140-370 MB on the UI thread. stb's GIF loader also read before the start of its buffer on disposal-3 frames, and when a long GIF ran out of memory it freed the frame delays that the caller then freed again (an abort). Decoding, and the stb implementation, now live in the GL-free util/image_decode_core, safe on any thread: - the header is probed first; stills above 256 MP, any GIF or animated WebP canvas above 32 Mi px, and files above 256 MiB are rejected before anything is allocated (real photos, 200 MP phone shots and wide panoramas still load); - a GIF decodes one composited frame at a time, downscaled as it goes, and stops at the frame cap: peak memory is stb's working buffers, two canvases and the small frames; a truncated GIF keeps the frames decoded so far; - a header stb cannot read means "unsupported", never a blind decode; - decodes above 16 MP run one at a time; a decode can be cancelled between frames. util::StartImageDecode runs one decode on a detached worker that owns its job and its lane's in-flight counter, so nothing waits at exit; the next commits move avatars and the image picker onto it. The limits apply now to avatars and the picker's hover previews; thumbnails and avatar imports get them when they move to workers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
289 lines
8.8 KiB
C++
289 lines
8.8 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "texture_loader.h"
|
|
|
|
// Image DECODING (the stb_image implementation, libwebp, limits, streaming GIF) lives in
|
|
// image_decode_core.cpp, which is GL-free and safe on worker threads. This file only creates and destroys
|
|
// GPU textures and must be called from the UI (GL/DX) thread.
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#ifdef DRAGONX_USE_DX11
|
|
#include <d3d11.h>
|
|
|
|
// Get D3D11 device from ImGui backend (same pattern as qr_code.cpp)
|
|
static ID3D11Device* GetImGuiD3D11Device()
|
|
{
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
if (!io.BackendRendererUserData) return nullptr;
|
|
return *reinterpret_cast<ID3D11Device**>(io.BackendRendererUserData);
|
|
}
|
|
#else
|
|
#ifdef DRAGONX_HAS_GLAD
|
|
#include <glad/gl.h>
|
|
#else
|
|
#include <SDL3/SDL_opengl.h>
|
|
#endif
|
|
#endif
|
|
|
|
#include "../util/logger.h"
|
|
|
|
namespace dragonx {
|
|
namespace util {
|
|
|
|
bool LoadTextureFromFile(const char* path, ImTextureID* outTexID, int* outW, int* outH)
|
|
{
|
|
std::vector<unsigned char> 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);
|
|
return false;
|
|
}
|
|
|
|
#ifdef DRAGONX_USE_DX11
|
|
ID3D11Device* device = GetImGuiD3D11Device();
|
|
if (!device) {
|
|
FreeRawPixels(pixels);
|
|
DEBUG_LOGF("LoadTextureFromFile: no D3D11 device available\n");
|
|
return false;
|
|
}
|
|
|
|
D3D11_TEXTURE2D_DESC desc = {};
|
|
desc.Width = w;
|
|
desc.Height = h;
|
|
desc.MipLevels = 1;
|
|
desc.ArraySize = 1;
|
|
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
desc.SampleDesc.Count = 1;
|
|
desc.Usage = D3D11_USAGE_DEFAULT;
|
|
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
|
|
|
D3D11_SUBRESOURCE_DATA initData = {};
|
|
initData.pSysMem = pixels;
|
|
initData.SysMemPitch = w * 4;
|
|
|
|
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;
|
|
}
|
|
|
|
ID3D11ShaderResourceView* srv = nullptr;
|
|
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
|
|
srvDesc.Format = desc.Format;
|
|
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
|
srvDesc.Texture2D.MipLevels = 1;
|
|
|
|
hr = device->CreateShaderResourceView(texture, &srvDesc, &srv);
|
|
texture->Release();
|
|
if (FAILED(hr) || !srv) {
|
|
DEBUG_LOGF("LoadTextureFromFile: CreateSRV failed for '%s'\n", path);
|
|
return false;
|
|
}
|
|
|
|
*outTexID = (ImTextureID)(intptr_t)srv;
|
|
*outW = w;
|
|
*outH = h;
|
|
DEBUG_LOGF("LoadTextureFromFile: loaded '%s' (%dx%d) -> DX11 SRV %p\n", path, w, h, (void*)srv);
|
|
return true;
|
|
#else
|
|
|
|
GLuint tex = 0;
|
|
glGenTextures(1, &tex);
|
|
glBindTexture(GL_TEXTURE_2D, tex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
|
glGenerateMipmap(GL_TEXTURE_2D);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
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;
|
|
*outH = h;
|
|
DEBUG_LOGF("LoadTextureFromFile: loaded '%s' (%dx%d) -> texture %u\n", path, w, h, tex);
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
bool LoadTextureFromMemory(const unsigned char* data, size_t dataSize,
|
|
ImTextureID* outTexID, int* outW, int* outH)
|
|
{
|
|
if (!data || dataSize == 0) {
|
|
DEBUG_LOGF("LoadTextureFromMemory: null/empty data\n");
|
|
return false;
|
|
}
|
|
|
|
int w = 0, h = 0;
|
|
unsigned char* pixels = DecodeImageRGBA(data, dataSize, &w, &h);
|
|
if (!pixels) {
|
|
DEBUG_LOGF("LoadTextureFromMemory: decode failed\n");
|
|
return false;
|
|
}
|
|
|
|
#ifdef DRAGONX_USE_DX11
|
|
ID3D11Device* device = GetImGuiD3D11Device();
|
|
if (!device) {
|
|
FreeRawPixels(pixels);
|
|
DEBUG_LOGF("LoadTextureFromMemory: no D3D11 device available\n");
|
|
return false;
|
|
}
|
|
|
|
D3D11_TEXTURE2D_DESC desc = {};
|
|
desc.Width = w;
|
|
desc.Height = h;
|
|
desc.MipLevels = 1;
|
|
desc.ArraySize = 1;
|
|
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
desc.SampleDesc.Count = 1;
|
|
desc.Usage = D3D11_USAGE_DEFAULT;
|
|
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
|
|
|
D3D11_SUBRESOURCE_DATA initData = {};
|
|
initData.pSysMem = pixels;
|
|
initData.SysMemPitch = w * 4;
|
|
|
|
ID3D11Texture2D* texture = nullptr;
|
|
HRESULT hr = device->CreateTexture2D(&desc, &initData, &texture);
|
|
FreeRawPixels(pixels);
|
|
if (FAILED(hr) || !texture) {
|
|
DEBUG_LOGF("LoadTextureFromMemory: CreateTexture2D failed\n");
|
|
return false;
|
|
}
|
|
|
|
ID3D11ShaderResourceView* srv = nullptr;
|
|
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
|
|
srvDesc.Format = desc.Format;
|
|
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
|
srvDesc.Texture2D.MipLevels = 1;
|
|
|
|
hr = device->CreateShaderResourceView(texture, &srvDesc, &srv);
|
|
texture->Release();
|
|
if (FAILED(hr) || !srv) {
|
|
DEBUG_LOGF("LoadTextureFromMemory: CreateSRV failed\n");
|
|
return false;
|
|
}
|
|
|
|
*outTexID = (ImTextureID)(intptr_t)srv;
|
|
*outW = w;
|
|
*outH = h;
|
|
DEBUG_LOGF("LoadTextureFromMemory: %dx%d -> DX11 SRV %p\n", w, h, (void*)srv);
|
|
return true;
|
|
#else
|
|
GLuint tex = 0;
|
|
glGenTextures(1, &tex);
|
|
glBindTexture(GL_TEXTURE_2D, tex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
|
glGenerateMipmap(GL_TEXTURE_2D);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
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;
|
|
*outH = h;
|
|
DEBUG_LOGF("LoadTextureFromMemory: %dx%d -> texture %u\n", w, h, tex);
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
bool CreateRawTexture(const unsigned char* pixels, int w, int h,
|
|
bool repeat, ImTextureID* outTexID)
|
|
{
|
|
if (!pixels || w <= 0 || h <= 0) return false;
|
|
|
|
#ifdef DRAGONX_USE_DX11
|
|
ID3D11Device* device = GetImGuiD3D11Device();
|
|
if (!device) return false;
|
|
|
|
D3D11_TEXTURE2D_DESC desc = {};
|
|
desc.Width = w;
|
|
desc.Height = h;
|
|
desc.MipLevels = 1;
|
|
desc.ArraySize = 1;
|
|
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
desc.SampleDesc.Count = 1;
|
|
desc.Usage = D3D11_USAGE_DEFAULT;
|
|
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
|
|
|
D3D11_SUBRESOURCE_DATA initData = {};
|
|
initData.pSysMem = pixels;
|
|
initData.SysMemPitch = w * 4;
|
|
|
|
ID3D11Texture2D* texture = nullptr;
|
|
HRESULT hr = device->CreateTexture2D(&desc, &initData, &texture);
|
|
if (FAILED(hr) || !texture) return false;
|
|
|
|
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
|
|
srvDesc.Format = desc.Format;
|
|
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
|
srvDesc.Texture2D.MipLevels = 1;
|
|
|
|
ID3D11ShaderResourceView* srv = nullptr;
|
|
hr = device->CreateShaderResourceView(texture, &srvDesc, &srv);
|
|
texture->Release();
|
|
if (FAILED(hr) || !srv) return false;
|
|
|
|
*outTexID = (ImTextureID)(intptr_t)srv;
|
|
return true;
|
|
#else
|
|
GLuint tex = 0;
|
|
glGenTextures(1, &tex);
|
|
glBindTexture(GL_TEXTURE_2D, tex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
if (repeat) {
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
|
} else {
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
}
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
*outTexID = (ImTextureID)(intptr_t)tex;
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
void DestroyTexture(ImTextureID texID)
|
|
{
|
|
if (!texID) return;
|
|
#ifdef DRAGONX_USE_DX11
|
|
auto* srv = (ID3D11ShaderResourceView*)(intptr_t)texID;
|
|
srv->Release();
|
|
#else
|
|
GLuint tex = (GLuint)(intptr_t)texID;
|
|
glDeleteTextures(1, &tex);
|
|
#endif
|
|
}
|
|
|
|
} // namespace util
|
|
} // namespace dragonx
|