Files
HelloVulkan/App/editor/ThumbnailCache.cpp
T
atdunbg 84cf547704 资产浏览器可视化改造 + 材质缩略图 GPU 烘焙 + 纹理上传批式化
- 资产浏览器可视化与配色:面板重构(网格/列表/缩略图)、AssetKind 分类、
  EditorColors 统一配色、AssetItemView 抽出条目绘制;HierarchyPanel/
  MaterialEditorPanel 同步调整
- 材质缩略图改走 GPU 回读烘焙:MaterialPreviewPass 新增 bake pass
  (SDR atlas → host staging copyImageToBuffer)、Renderer 提供
  Request/PollMaterialPreviewBake(无 waitIdle,依赖 per-slot fence),
  Editor 每帧排空后注入 ThumbnailCache——脱离预览 tile 池,消除"滚出视野
  即失效/池耗尽"两问题
- 缩略图失效:BakedIdentity(serial, editVersion) 与当前材质身份比对,
  换贴图槽(重载新 serial)/调参数(editVersion++)不符即 Invalidate 重烘,
  修"修改材质后资产浏览器缩略图不更新"
- 材质加载闸门:每帧至多 1 个 + 按实测耗时冷却,替代固定 2/帧
- 纹理上传批式化:VulkanAssetUploader 新增路径纹理缓存(跨材质去重,
  同一贴图只解码/上传一次)+ 同批新纹理合并为单 cmd/单 submit/单 fence;
  Texture 抽出 RecordPixelsUpload/CreateDeferred/ReleasePendingStaging,
  析构兜底释放 staging;MaterialAsset 纹理所有权移交 uploader 缓存——
  修大材质目录首次打开卡顿
2026-10-18 20:05:29 +08:00

342 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 <imgui_internal.h> // RegisterUserTexture / UnregisterUserTexture
#include <stb/stb_image.h>
#include <algorithm>
#include <cstring>
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<size_t>(sy) * sw * 4;
for (int sx = sx0; sx < sx1; sx += sxStep)
{
const unsigned char* p =
row + static_cast<size_t>(sx) * 4;
r += p[0]; g += p[1]; b += p[2]; a += p[3]; ++n;
}
}
unsigned char* q = dst + (static_cast<size_t>(y) * dw + x) * 4;
q[0] = static_cast<unsigned char>(r / n);
q[1] = static_cast<unsigned char>(g / n);
q[2] = static_cast<unsigned char>(b / n);
q[3] = static_cast<unsigned char>(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<size_t>(dw) * dh * 4);
}
else
{
ResampleBoxRGBA8(src, sw, sh,
static_cast<unsigned char*>(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<long long>(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;
}
}
}