- 资产浏览器可视化与配色:面板重构(网格/列表/缩略图)、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 缓存—— 修大材质目录首次打开卡顿
342 lines
11 KiB
C++
342 lines
11 KiB
C++
//
|
||
// 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;
|
||
}
|
||
}
|
||
}
|