Files
HelloVulkan/App/editor/AssetItemView.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

506 lines
19 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.
//
// AssetItemView.cpp:资产条目绘制单元实现。
//
#include "AssetItemView.h"
#include "AssetKind.h"
#include "EditorColors.h"
#include "ThumbnailCache.h"
#include "panels/AssetBrowserPanel.h" // kPrefabDragPayload / kModelDragPayload
#include "panels/ControllerEditorPanel.h" // kControllerDragPayload
#include "panels/MaterialEditorPanel.h" // kMaterialDragPayload / kTextureDragPayload
#include <Engine/Renderer/Model.h>
#include <Engine/Renderer/MaterialAsset.h>
#include <imgui.h>
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <ctime>
#include <set>
#include <string>
#include <unordered_map>
#include <utility>
namespace {
constexpr float kBoxRounding = 3.0f;
// 类别色压暗背板 + 描边(p0/p1 来自上一个 item rect)
void DrawBoxChrome(const ImVec2& p0, const ImVec2& p1,
const ImVec4& accent)
{
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(p0, p1,
ImGui::GetColorU32(ImVec4(accent.x * 0.25f,
accent.y * 0.25f,
accent.z * 0.25f, 1.0f)),
kBoxRounding);
dl->AddRect(p0, p1, ImGui::GetColorU32(accent), kBoxRounding);
}
// 名字按可用宽度截断(UTF-8 边界安全)
std::string TruncateToWidth(const std::string& s, float maxW)
{
if (ImGui::CalcTextSize(s.c_str()).x <= maxW) return s;
std::string out = s;
const char* ell = "..";
while (!out.empty() &&
ImGui::CalcTextSize((out + ell).c_str()).x > maxW)
{
out.pop_back();
while (!out.empty() && (static_cast<unsigned char>(out.back()) &
0xC0) == 0x80)
{
out.pop_back(); // 退到 UTF-8 起始字节
}
}
return out + ell;
}
// 已尝试按需加载的材质资产路径:加载失败不再每帧重试(资产缓存接管
// 成功路径,重复调用是缓存命中)
std::set<std::string>& MaterialLoadTried()
{
static std::set<std::string> s;
return s;
}
// 材质加载闸门:大目录(上百个材质)逐个同步加载(.material 解析 +
// GPU 纹理上传——Texture 构造内含 submit + fence 阻塞等待,单个材质
// 可含多张大贴图)会顶爆单帧 → "卡成 PPT"。闸门 = 每帧至多 1 个 +
// 按上次实测耗时冷却(1 帧≈16ms;重载 ≈ 冷却相应帧数),把加载占用
// 压到 ≤ ~50% 帧预算,剩余帧时间还给渲染
struct MaterialLoadGate
{
bool Allow()
{
const int frame = ImGui::GetFrameCount();
return frame != m_LastLoadFrame && frame >= m_CooldownUntil;
}
void Charge(double elapsedMs)
{
const int frame = ImGui::GetFrameCount();
m_LastLoadFrame = frame;
const int cool = static_cast<int>(elapsedMs / 16.0);
m_CooldownUntil = frame + 1 + std::min(cool, 30);
}
private:
int m_LastLoadFrame = -1;
int m_CooldownUntil = 0;
};
MaterialLoadGate& MaterialLoad()
{
static MaterialLoadGate s_Gate;
return s_Gate;
}
// 已烘焙材质缩略图对应的 (serial, editVersion)。资产重载(新 serial)
// 或参数编辑(editVersion++)后与当前不符 = 缩略图过期,须作废重烘
//(否则浏览器永远停在旧图)——记录时机 = 发起烘焙请求那一帧
std::unordered_map<std::string, std::pair<Engine::u64, Engine::u32>>&
BakedIdentity()
{
static std::unordered_map<std::string,
std::pair<Engine::u64, Engine::u32>> s;
return s;
}
// 材质球缩略图全部走编辑器缩略图缓存:引擎把预览球渲染一次后 GPU→CPU
// 回读,App 注入缓存(与贴图缩略图同一条出图路径),浏览器不再占用
// 引擎预览 tile 池——大目录滚动/材质编辑器预览互不饿死
void DrawCachedThumb(const ThumbView& v, float size, const ImVec2& p0)
{
float fw = size, fh = size;
if (v.w > 0 && v.h > 0)
{
if (v.w >= v.h) fh = size * static_cast<float>(v.h) / v.w;
else fw = size * static_cast<float>(v.w) / v.h;
}
const ImVec2 fp0(p0.x + (size - fw) * 0.5f, p0.y + (size - fh) * 0.5f);
ImGui::GetWindowDrawList()->AddImage(
v.tex, fp0, ImVec2(fp0.x + fw, fp0.y + fh));
}
// 缩略图/占位块:一个命中项 + 绘制层。
// interactive=true → InvisibleButton(带 ID,可承载拖拽源/点击;Dummy
// 无 ID,在其上 BeginDragDropSource 会断言);false → Dummy(行内小图
// 纯展示,不抢行 Selectable 的点击)。
// 出图时**不铺任何背板**——透明像素会被类别色染色;背板只在未出图时
// 作为占位(类别色压暗 + 短标签)。仅可见项才请求缩略图,离屏项不入
// 解码队列(大目录不再一次性排队,出图延迟可控)
void DrawThumbBox(const EditorContext& ctx,
const std::filesystem::path& path, float size,
EditorAsset::AssetKind kind, bool interactive)
{
const bool visible = ImGui::IsRectVisible(ImVec2(size, size));
if (interactive)
{
ImGui::InvisibleButton("##thumb", ImVec2(size, size));
}
else
{
ImGui::Dummy(ImVec2(size, size));
}
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
const ImVec4 accent = EditorColors::AssetColor(kind);
bool drawn = false;
if (visible && kind == EditorAsset::AssetKind::Material && ctx.assets)
{
// 材质资产缩略图 = 引擎烘焙的静态材质球(GPU→CPU 回读 → 缩略图
// 缓存;与贴图缩略图同一条出图路径)
auto& cache = EditorThumbnailCache::Get();
const std::string assetPath = path.generic_string();
// 当前材质身份:已加载才有;未加载时无法判新旧
Engine::Material* curMat = nullptr;
if (ctx.assets->IsMaterialLoaded(assetPath))
{
Engine::MaterialAsset* a =
ctx.assets->FindMaterialAsset(assetPath);
curMat = a ? a->GetMaterial() : nullptr;
}
const ThumbView v = cache.Peek(assetPath);
if (v.state == ThumbView::State::Ready)
{
// 失效检测:缓存缩略图对应的材质身份 vs 当前身份——资产
// 重载(新 serial,换贴图槽)/ 参数编辑(editVersion++)后
// 不符 = 过期,作废缓存让下一帧重烘(否则浏览器永远旧图)
bool stale = false;
if (curMat)
{
auto it = BakedIdentity().find(assetPath);
if (it != BakedIdentity().end() &&
(it->second.first != curMat->GetSerial() ||
it->second.second != curMat->GetEditVersion()))
{
stale = true;
}
}
if (!stale)
{
DrawCachedThumb(v, size, p0);
drawn = true;
}
else
{
cache.Invalidate(assetPath);
BakedIdentity().erase(assetPath);
}
}
else if (v.state != ThumbView::State::Pending && ctx.renderer)
{
if (curMat)
{
// 未烘焙/已失效:请求烘焙并登记当前身份(key 幂等
// keepalive,回读完成即进缓存)
ctx.renderer->RequestMaterialPreviewBake(
curMat, cache.BakeKeyForPath(assetPath));
BakedIdentity()[assetPath] = {
curMat->GetSerial(), curMat->GetEditVersion()};
}
else if (!MaterialLoadTried().count(assetPath) &&
MaterialLoad().Allow())
{
// 按需加载材质(闸门限流,避免大目录同帧卡顿)
MaterialLoadTried().insert(assetPath);
const auto t0 = std::chrono::steady_clock::now();
ctx.assets->LoadMaterialAsset(assetPath);
const double ms = std::chrono::duration<double,
std::milli>(std::chrono::steady_clock::now() - t0)
.count();
MaterialLoad().Charge(ms);
}
}
}
if (!drawn && visible && EditorAsset::IsThumbnailable(path))
{
const ThumbView v =
EditorThumbnailCache::Get().Request(path.generic_string());
if (v.state == ThumbView::State::Ready)
{
DrawCachedThumb(v, size, p0);
drawn = true;
}
}
if (!drawn)
{
// 占位:类别色压暗背板 + 描边;大框叠加短标签
DrawBoxChrome(p0, p1, accent);
if (size >= 28.0f)
{
const char* label = EditorAsset::KindShortLabel(kind);
const ImVec2 ts = ImGui::CalcTextSize(label);
ImGui::GetWindowDrawList()->AddText(
ImVec2(p0.x + (size - ts.x) * 0.5f,
p0.y + (size - ts.y) * 0.5f),
ImGui::GetColorU32(accent), label);
}
}
}
// 文件夹命中项(黄色)+ 短标签
void DrawFolderBox(float size, bool interactive)
{
if (interactive)
{
ImGui::InvisibleButton("##dir", ImVec2(size, size));
}
else
{
ImGui::Dummy(ImVec2(size, size));
}
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
DrawBoxChrome(p0, p1, EditorColors::kFolder);
if (size >= 28.0f)
{
const ImVec2 ts = ImGui::CalcTextSize("DIR");
ImGui::GetWindowDrawList()->AddText(
ImVec2(p0.x + (size - ts.x) * 0.5f,
p0.y + (size - ts.y) * 0.5f),
ImGui::GetColorU32(EditorColors::kFolder), "DIR");
}
}
} // namespace
namespace EditorAssetView {
void DrawDragSource(const std::filesystem::path& path,
const std::string& name)
{
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
const char* payload = nullptr;
switch (kind)
{
case EditorAsset::AssetKind::Material: payload = kMaterialDragPayload; break;
case EditorAsset::AssetKind::AnimClip: payload = kControllerDragPayload; break;
case EditorAsset::AssetKind::Prefab: payload = kPrefabDragPayload; break;
case EditorAsset::AssetKind::Model: payload = kModelDragPayload; break;
case EditorAsset::AssetKind::Texture: payload = kTextureDragPayload; break;
default: return;
}
if (ImGui::BeginDragDropSource())
{
const std::string assetPath = path.generic_string();
ImGui::SetDragDropPayload(payload, assetPath.c_str(),
assetPath.size() + 1);
ImGui::TextUnformatted(name.c_str());
ImGui::EndDragDropSource();
}
}
void DrawTooltip(const EditorContext& ctx,
const std::filesystem::path& path)
{
namespace fs = std::filesystem;
const std::string name = path.filename().string();
const std::string assetPath = path.generic_string();
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
ImGui::BeginTooltip();
ImGui::TextUnformatted(name.c_str());
ImGui::Separator();
ImGui::Text("type: %s", EditorAsset::KindLabel(kind));
std::error_code ec;
const auto sz = fs::file_size(path, ec);
if (!ec)
{
char szBuf[32];
if (sz >= 1024ull * 1024ull)
{
std::snprintf(szBuf, sizeof(szBuf), "%.1f MB",
static_cast<double>(sz) / (1024.0 * 1024.0));
}
else
{
std::snprintf(szBuf, sizeof(szBuf), "%.1f KB",
static_cast<double>(sz) / 1024.0);
}
ImGui::Text("size: %s", szBuf);
}
const auto ftime = fs::last_write_time(path, ec);
if (!ec)
{
// file_clock → system_clock 近似转换(C++17 无官方转换)
const auto sctp = std::chrono::time_point_cast<
std::chrono::system_clock::duration>(
ftime - fs::file_time_type::clock::now() +
std::chrono::system_clock::now());
const std::time_t tt = std::chrono::system_clock::to_time_t(sctp);
char tmBuf[32];
std::strftime(tmBuf, sizeof(tmBuf), "%Y-%m-%d %H:%M",
std::localtime(&tt));
ImGui::Text("modified: %s", tmBuf);
}
if (ctx.assets)
{
if (kind == EditorAsset::AssetKind::Model ||
kind == EditorAsset::AssetKind::Texture)
{
const bool loaded =
(kind == EditorAsset::AssetKind::Model)
? ctx.assets->IsModelLoaded(assetPath)
: ctx.assets->IsEnvironmentLoaded(assetPath);
ImGui::Text("state: %s", loaded ? "loaded" : "not loaded");
}
else if (kind == EditorAsset::AssetKind::Prefab)
{
ImGui::TextDisabled(
"template — drag into scene / double-click");
}
if (kind == EditorAsset::AssetKind::Model)
{
if (const Engine::Model* m =
ctx.assets->GetModelByPath(assetPath);
m != nullptr)
{
ImGui::Text("meshes: %u, bones: %u, clips: %u",
static_cast<unsigned>(m->GetMeshes().size()),
m->GetBoneCount(),
static_cast<unsigned>(m->GetClips().size()));
}
}
}
ImGui::TextDisabled("%s", assetPath.c_str());
ImGui::EndTooltip();
}
ItemResult DrawRow(const EditorContext& ctx,
const std::filesystem::path& path, bool selected)
{
ItemResult res;
const std::string name = path.filename().string();
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
ImGui::PushID(name.c_str());
// 内联小缩略图 / 类别色占位块(非交互:行点击归 Selectable)
DrawThumbBox(ctx, path, ImGui::GetTextLineHeight(), kind, false);
ImGui::SameLine();
// 名称(类别色提亮)+ 类别色 hover/选中高亮
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind));
ImGui::PushStyleColor(ImGuiCol_Header,
EditorColors::AssetHighlightColor(kind));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
EditorColors::AssetHighlightColor(kind));
ImGui::PushStyleColor(ImGuiCol_HeaderActive,
EditorColors::AssetHighlightColor(kind));
res.clicked = ImGui::Selectable(name.c_str(), selected,
ImGuiSelectableFlags_AllowDoubleClick);
ImGui::PopStyleColor(4);
if (res.clicked && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
{
res.doubleClicked = true;
}
DrawDragSource(path, name);
if (ImGui::IsItemHovered())
{
DrawTooltip(ctx, path);
}
ImGui::PopID();
return res;
}
ItemResult DrawTile(const EditorContext& ctx,
const std::filesystem::path& path,
float tileSize, bool selected)
{
ItemResult res;
const std::string name = path.filename().string();
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
const ImVec4 accent = EditorColors::AssetColor(kind);
ImGui::PushID(name.c_str());
ImGui::BeginGroup();
DrawThumbBox(ctx, path, tileSize, kind, true);
// 选中/hover 描边(叠在缩略图上)
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
const bool hovered = ImGui::IsItemHovered();
if (selected || hovered)
{
ImGui::GetWindowDrawList()->AddRect(
p0, p1, ImGui::GetColorU32(accent), kBoxRounding, 0,
selected ? 2.0f : 1.0f);
}
res.clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
{
res.doubleClicked = true;
}
DrawDragSource(path, name);
if (ImGui::IsItemHovered())
{
DrawTooltip(ctx, path);
}
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind));
ImGui::TextUnformatted(TruncateToWidth(name, tileSize).c_str());
ImGui::PopStyleColor();
ImGui::EndGroup();
ImGui::PopID();
return res;
}
bool DrawFolderRow(const std::filesystem::path& dir)
{
const std::string dname = dir.filename().string();
ImGui::PushID(dname.c_str());
DrawFolderBox(ImGui::GetTextLineHeight(), false);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder);
const bool clicked = ImGui::Selectable(dname.c_str());
ImGui::PopStyleColor();
ImGui::PopID();
return clicked;
}
bool DrawFolderTile(const std::filesystem::path& dir, float tileSize,
bool selected)
{
const std::string dname = dir.filename().string();
ImGui::PushID(dname.c_str());
ImGui::BeginGroup();
DrawFolderBox(tileSize, true);
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
if (selected || ImGui::IsItemHovered())
{
ImGui::GetWindowDrawList()->AddRect(
p0, p1, ImGui::GetColorU32(EditorColors::kFolder), kBoxRounding,
0, selected ? 2.0f : 1.0f);
}
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder);
ImGui::TextUnformatted(TruncateToWidth(dname, tileSize).c_str());
ImGui::PopStyleColor();
ImGui::EndGroup();
ImGui::PopID();
return clicked;
}
} // namespace EditorAssetView