// // ThumbnailCache.cpp:缩略图解碼 + 降采样 + ImGui 用户纹理上传 + LRU 淘汰。 // // 生命周期(依赖 vendored ImGui 1.93.0-WIP 语义,与后端/主循环逐帧对齐): // 帧 N :Request 入队 → BeginFrame 解码 → ImTextureData::Create + // RegisterUserTexture(Status=WantCreate,TexID=Invalid) // 帧 N 末:UpdateTexturesEndFrame 把用户纹理并入 PlatformIO.Textures; // ImGui_ImplVulkan_RenderDrawData 内 UpdateTexture 建 VkImage + // 描述符,置 TexID、Status=OK // 帧 N+1:BeginFrame 见 TexID != Invalid → Ready(当帧注册、次帧出图) // 淘汰 :WantDestroyNextFrame=true → 后端 UnusedFrames>=ImageCount 时 // 销毁并清 BackendUserData → 下一帧见 Destroyed/无 BackendUserData // → UnregisterUserTexture + IM_DELETE // 关机 :本单例是进程级 function-local static,析构晚于 main 内的 // SandboxApp(含 ImGuiLayer)——ImGui_ImplVulkan_Shutdown 已对 // RefCount==1 的用户纹理统一释放 GPU;析构仅 IM_DELETE(只 // IM_FREE(Pixels),不触任何已销毁的 ImGui 上下文)。 // #include "ThumbnailCache.h" #include // RegisterUserTexture / UnregisterUserTexture #include #include #include namespace { constexpr int kThumbMaxDim = 128; // 缩略图最长边 constexpr size_t kMaxEntries = 256; // 缓存上限(≤ 描述符池) constexpr int kMaxDecodesPerFrame = 2; // 每帧最多解码张数 constexpr long long kMaxSourcePixels = 4096ll * 4096ll; // 超大源图跳过 constexpr int kReadyTimeoutFrames = 3; // 上传超时 → Failed // box filter 等比降采样(RGBA8):输出像素在源块内最多取 // kMaxSamplesPerAxis² 个采样点(按步长抽样)。大图降采样时避免对 // 整块(如 4096² → 128² 时每像素 1024 次读)全量累加——那是单张 // 百毫秒级的卡顿源 void ResampleBoxRGBA8(const unsigned char* src, int sw, int sh, unsigned char* dst, int dw, int dh) { constexpr int kMaxSamplesPerAxis = 8; for (int y = 0; y < dh; ++y) { const int sy0 = y * sh / dh; int sy1 = (y + 1) * sh / dh; if (sy1 <= sy0) sy1 = sy0 + 1; const int syStep = std::max( 1, (sy1 - sy0 + kMaxSamplesPerAxis - 1) / kMaxSamplesPerAxis); for (int x = 0; x < dw; ++x) { const int sx0 = x * sw / dw; int sx1 = (x + 1) * sw / dw; if (sx1 <= sx0) sx1 = sx0 + 1; const int sxStep = std::max( 1, (sx1 - sx0 + kMaxSamplesPerAxis - 1) / kMaxSamplesPerAxis); unsigned int r = 0, g = 0, b = 0, a = 0, n = 0; for (int sy = sy0; sy < sy1; sy += syStep) { const unsigned char* row = src + static_cast(sy) * sw * 4; for (int sx = sx0; sx < sx1; sx += sxStep) { const unsigned char* p = row + static_cast(sx) * 4; r += p[0]; g += p[1]; b += p[2]; a += p[3]; ++n; } } unsigned char* q = dst + (static_cast(y) * dw + x) * 4; q[0] = static_cast(r / n); q[1] = static_cast(g / n); q[2] = static_cast(b / n); q[3] = static_cast(a / n); } } } } // namespace EditorThumbnailCache& EditorThumbnailCache::Get() { static EditorThumbnailCache s_Cache; return s_Cache; } EditorThumbnailCache::~EditorThumbnailCache() { // 仅回收 CPU 侧(见文件头注释)。勿调 UnregisterUserTexture—— // 此刻 ImGui 上下文已随 ImGuiLayer 销毁 for (auto& [path, e] : m_Entries) { if (e.tex) IM_DELETE(e.tex); } for (ImTextureData* tex : m_Retire) IM_DELETE(tex); } void EditorThumbnailCache::BeginFrame() { ++m_Frame; DrainRetired(); // 就绪推进:注册后等后端上传(次帧可见) for (auto& [path, e] : m_Entries) { if (e.state != ThumbView::State::Pending || !e.tex) continue; ++e.pendingFrames; if (e.tex->TexID != ImTextureID_Invalid) { e.state = ThumbView::State::Ready; } else if (e.pendingFrames > kReadyTimeoutFrames && e.tex->Status == ImTextureStatus_Destroyed) { e.state = ThumbView::State::Failed; } } // 限量解码(未解码 = tex 为空) int decodes = 0; while (decodes < kMaxDecodesPerFrame && !m_Queue.empty()) { const std::string p = m_Queue.front(); m_Queue.pop_front(); auto it = m_Entries.find(p); if (it == m_Entries.end()) continue; // 已被淘汰 if (it->second.tex) continue; // 已解码(防御) DecodeOne(p, it->second); ++decodes; } // 超上限 → 淘汰最久未用 while (m_Entries.size() > kMaxEntries) { if (!RetireOne()) break; // 全在用:本帧放弃 } } ThumbView EditorThumbnailCache::Request(const std::string& assetPath) { ThumbView v; if (assetPath.empty()) { v.state = ThumbView::State::Failed; return v; } auto it = m_Entries.find(assetPath); if (it == m_Entries.end()) { Entry e; e.lastUseFrame = m_Frame; m_Entries.emplace(assetPath, e); m_Queue.push_back(assetPath); return v; // Pending } Entry& e = it->second; e.lastUseFrame = m_Frame; v.state = e.state; v.w = e.w; v.h = e.h; if (e.state == ThumbView::State::Ready && e.tex) { v.tex = e.tex->GetTexRef(); } return v; } void EditorThumbnailCache::UploadRGBA8(const unsigned char* src, int sw, int sh, Entry& e) { // 等比缩放到最长边 ≤ kThumbMaxDim int dw = sw, dh = sh; if (sw >= sh && sw > kThumbMaxDim) { dw = kThumbMaxDim; dh = std::max(1, sh * kThumbMaxDim / sw); } else if (sh > sw && sh > kThumbMaxDim) { dh = kThumbMaxDim; dw = std::max(1, sw * kThumbMaxDim / sh); } ImTextureData* tex = IM_NEW(ImTextureData)(); tex->Create(ImTextureFormat_RGBA32, dw, dh); if (dw == sw && dh == sh) { std::memcpy(tex->GetPixels(), src, static_cast(dw) * dh * 4); } else { ResampleBoxRGBA8(src, sw, sh, static_cast(tex->GetPixels()), dw, dh); } ImGui::RegisterUserTexture(tex); tex->SetStatus(ImTextureStatus_WantCreate); e.tex = tex; e.w = dw; e.h = dh; e.state = ThumbView::State::Pending; e.pendingFrames = 0; } void EditorThumbnailCache::DecodeOne(const std::string& path, Entry& e) { int sw = 0, sh = 0, ch = 0; if (!stbi_info(path.c_str(), &sw, &sh, &ch) || sw <= 0 || sh <= 0 || static_cast(sw) * sh > kMaxSourcePixels) { e.state = ThumbView::State::Failed; return; } unsigned char* src = stbi_load(path.c_str(), &sw, &sh, &ch, 4); if (!src) { e.state = ThumbView::State::Failed; return; } UploadRGBA8(src, sw, sh, e); stbi_image_free(src); e.lastUseFrame = m_Frame; } ThumbView EditorThumbnailCache::Peek(const std::string& assetPath) { ThumbView v; auto it = m_Entries.find(assetPath); if (it == m_Entries.end()) { v.state = ThumbView::State::Failed; // 缺省 = 未注入 return v; } Entry& e = it->second; e.lastUseFrame = m_Frame; v.state = e.state; v.w = e.w; v.h = e.h; if (e.state == ThumbView::State::Ready && e.tex) { v.tex = e.tex->GetTexRef(); } return v; } void EditorThumbnailCache::Invalidate(const std::string& assetPath) { auto it = m_Entries.find(assetPath); if (it == m_Entries.end()) return; if (it->second.tex) { // 交回回收流程(与 LRU 淘汰同一路径:WantDestroyNextFrame → // 后端释放 → DrainRetired 注销 + 回收) it->second.tex->WantDestroyNextFrame = true; m_Retire.push_back(it->second.tex); } m_Entries.erase(it); } unsigned long long EditorThumbnailCache::BakeKeyForPath(const std::string& path) { auto it = m_PathToBakeKey.find(path); if (it != m_PathToBakeKey.end()) return it->second; const unsigned long long key = ++m_BakeKeyCounter; m_PathToBakeKey.emplace(path, key); m_BakeKeyToPath.emplace(key, path); return key; } void EditorThumbnailCache::CompleteBake(unsigned long long key, const unsigned char* rgba, int w, int h) { auto it = m_BakeKeyToPath.find(key); if (it == m_BakeKeyToPath.end()) return; // 已注销(重复回读) const std::string path = it->second; m_BakeKeyToPath.erase(it); m_PathToBakeKey.erase(path); InjectRGBA8(path, rgba, w, h); } void EditorThumbnailCache::InjectRGBA8(const std::string& path, const unsigned char* rgba, int w, int h) { if (!rgba || w <= 0 || h <= 0) return; Entry& e = m_Entries[path]; // 材质路径不经解码队列,缺省即新建 if (e.tex) return; // 已有纹理(重复注入):忽略 UploadRGBA8(rgba, w, h, e); e.lastUseFrame = m_Frame; } bool EditorThumbnailCache::RetireOne() { auto worst = m_Entries.end(); for (auto it = m_Entries.begin(); it != m_Entries.end(); ++it) { if (it->second.lastUseFrame == m_Frame) continue; // 本帧在用 if (worst == m_Entries.end() || it->second.lastUseFrame < worst->second.lastUseFrame) { worst = it; } } if (worst == m_Entries.end()) return false; if (worst->second.tex) { worst->second.tex->WantDestroyNextFrame = true; m_Retire.push_back(worst->second.tex); } m_Entries.erase(worst); return true; } void EditorThumbnailCache::DrainRetired() { for (size_t i = 0; i < m_Retire.size();) { ImTextureData* tex = m_Retire[i]; // 后端销毁后清 BackendUserData 并置 Destroyed → 可安全回收 if (tex->BackendUserData == nullptr && tex->Status == ImTextureStatus_Destroyed) { ImGui::UnregisterUserTexture(tex); IM_DELETE(tex); m_Retire[i] = m_Retire.back(); m_Retire.pop_back(); } else { ++i; } } }