资产浏览器可视化改造 + 材质缩略图 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 缓存—— 修大材质目录首次打开卡顿
This commit is contained in:
@@ -314,7 +314,10 @@ namespace Engine::RG {
|
||||
void ComputeLifetimes();
|
||||
void PrecomputeBarriers();
|
||||
void AllocatePhysical();
|
||||
void ReleasePhysical();
|
||||
// keepFixedSize = true:固定尺寸资源(desc 显式 width/height,与目标
|
||||
// 渲染尺寸无关)的物理镜像不归还池、保留在 entry 上——重编译后原样
|
||||
// 沿用同一 VkImage,内容存续(材质球预览 atlas 不因视口 resize 清空)
|
||||
void ReleasePhysical(bool keepFixedSize);
|
||||
void PurgePool(vk::Extent2D target);
|
||||
PhysicalImage* AcquirePhysical(vk::Format format, vk::Extent2D extent,
|
||||
vk::ImageUsageFlags usage,
|
||||
|
||||
@@ -67,12 +67,10 @@ namespace Engine {
|
||||
static bool Save(const std::string& path, const MaterialAssetData& data);
|
||||
static bool Load(const std::string& path, MaterialAssetData& out);
|
||||
|
||||
// GPU 段注入(仅 VulkanAssetUploader 调用):ownedTextures =
|
||||
// 本资产加载的纹理(独占所有权),material = 组装好的材质
|
||||
//(空槽已由 uploader 绑共享默认纹理)
|
||||
MaterialAsset(MaterialAssetData&& data,
|
||||
std::array<Scope<Texture>, Material::kSlotCount>&& ownedTextures,
|
||||
Scope<Material>&& material);
|
||||
// GPU 段注入(仅 VulkanAssetUploader 调用):material = 组装好的
|
||||
// 材质(空槽已由 uploader 绑共享默认纹理)。纹理所有权归 uploader
|
||||
// 的路径缓存(跨材质去重),本资产只持有绑进 Material 的裸指针
|
||||
MaterialAsset(MaterialAssetData&& data, Scope<Material>&& material);
|
||||
// 析构外置(成员为重资源句柄,销毁序与 GPU 生命周期解耦遵循
|
||||
// Model/Material 既有模式)
|
||||
~MaterialAsset();
|
||||
@@ -87,8 +85,6 @@ namespace Engine {
|
||||
|
||||
private:
|
||||
MaterialAssetData m_Data;
|
||||
// 本资产独占的纹理(共享默认纹理不在其中,由 uploader 持有)
|
||||
std::array<Scope<Texture>, Material::kSlotCount> m_OwnedTextures;
|
||||
Scope<Material> m_Material;
|
||||
};
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ namespace Engine {
|
||||
inline constexpr u32 kPreviewTilesPerSide = 5;
|
||||
inline constexpr u32 kPreviewTileCount =
|
||||
kPreviewTilesPerSide * kPreviewTilesPerSide;
|
||||
// 单 tile 回读字节数(SDR atlas 为 RGBA8)
|
||||
inline constexpr u32 kPreviewTileBytes =
|
||||
kPreviewTileSize * kPreviewTileSize * 4;
|
||||
|
||||
// 本帧待渲染的脏 tile(Material* 来自本帧 UI 请求,活指针)
|
||||
struct PreviewDirtyTile {
|
||||
@@ -80,13 +83,22 @@ namespace Engine {
|
||||
u32 tile = 0;
|
||||
};
|
||||
|
||||
// 本帧待回读烘焙的 tile(key = 调用方令牌,Renderer 不解释)。
|
||||
// 只收录"本帧确实被渲染"的 tile,故条数 ≤ 渲染预算
|
||||
struct PreviewBakeTile {
|
||||
u64 key = 0;
|
||||
u32 tile = 0;
|
||||
};
|
||||
|
||||
// 编辑器 tile 池(Renderer 持有,UE FAssetThumbnailPool 语义)。
|
||||
// 缓存键 = Material serial(出生唯一号,防指针地址复用的 ABA)
|
||||
// + editVersion(参数代数)。游戏构建整体编译裁剪,无运行时成本
|
||||
class PreviewTilePool {
|
||||
public:
|
||||
// 每帧渲染预算:脏 tile 多时逐帧消化(滚动列表渐进出图)
|
||||
static constexpr u32 kPreviewRendersPerFrame = 2;
|
||||
// 每帧渲染预算:脏 tile 多时逐帧消化(浏览器缩略图烘焙 / 滚动
|
||||
// 列表渐进出图)。预览球是 128 三角形的 256² 直绘,8 个/帧的
|
||||
// GPU 成本可忽略;烘焙吞吐 = 预算(回读只收当帧渲染过的 tile)
|
||||
static constexpr u32 kPreviewRendersPerFrame = 8;
|
||||
|
||||
// UI 构建段逐可见行调用(幂等 keepalive,刷新 LRU + 脏检测)。
|
||||
// 返回 tile:>=0 = 三槽位镜像内容齐备可显示(含重渲期间的旧
|
||||
@@ -145,6 +157,44 @@ namespace Engine {
|
||||
return displayable ? static_cast<i32>(tileIdx) : -1;
|
||||
}
|
||||
|
||||
// 烘焙请求(浏览器缩略图回读):强制标脏——即使 tile 已
|
||||
// displayable 也重渲一次,保证"当帧渲染 → 当帧拷贝回读"闭环
|
||||
// (回读只收当帧渲染过的 tile)。返回槽位 tile 索引;-1 =
|
||||
// 池满无槽(调用方下帧重试)。不参与 paintedSlots 记账
|
||||
i32 RequestBake(Material* material)
|
||||
{
|
||||
if (!material) return -1;
|
||||
const u64 serial = material->GetSerial();
|
||||
Entry* slot = nullptr;
|
||||
for (auto& e : m_Entries) {
|
||||
if (e.serial == serial) { slot = &e; break; }
|
||||
}
|
||||
if (!slot) {
|
||||
u64 oldest = ~0ull;
|
||||
for (auto& e : m_Entries) {
|
||||
if (e.serial == 0) { slot = &e; break; }
|
||||
if (e.lastUseFrame < m_Frame &&
|
||||
e.lastUseFrame < oldest) {
|
||||
oldest = e.lastUseFrame;
|
||||
slot = &e;
|
||||
}
|
||||
}
|
||||
if (!slot) return -1;
|
||||
slot->serial = serial;
|
||||
slot->paintedSlots = 0;
|
||||
slot->trackedVersion = 0;
|
||||
}
|
||||
slot->lastUseFrame = m_Frame;
|
||||
const u32 tileIdx =
|
||||
static_cast<u32>(slot - m_Entries.data());
|
||||
bool queued = false;
|
||||
for (auto& d : m_FrameDirty) {
|
||||
if (d.tile == tileIdx) { d.material = material; queued = true; break; }
|
||||
}
|
||||
if (!queued) m_FrameDirty.push_back({material, tileIdx});
|
||||
return static_cast<i32>(tileIdx);
|
||||
}
|
||||
|
||||
// Render 段取本帧预算内的脏 tile(frameSlot = 本帧渲染槽位,
|
||||
// 该槽位镜像在本帧被写入)。渲染语义(清矩形+画球)由 pass
|
||||
// 执行后该槽位内容即有效。剩余脏 tile 保持未取——下帧可见行
|
||||
@@ -215,6 +265,7 @@ namespace Engine {
|
||||
class MaterialPreviewPass final : public RenderPass {
|
||||
public:
|
||||
// dirtyTiles = Renderer 持有的当帧脏表(Render 段换入,只读)
|
||||
// bakeTiles = Renderer 持有的当帧回读表(只读;条数 ≤ 渲染预算)
|
||||
MaterialPreviewPass(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptors,
|
||||
SceneGpu& gpu, FullscreenGpu& fullscreen,
|
||||
@@ -222,8 +273,19 @@ namespace Engine {
|
||||
RG::TextureHandle velocityAtlas,
|
||||
RG::TextureHandle sdrAtlas,
|
||||
const std::vector<PreviewDirtyTile>&
|
||||
dirtyTiles);
|
||||
~MaterialPreviewPass() override = default;
|
||||
dirtyTiles,
|
||||
const std::vector<PreviewBakeTile>&
|
||||
bakeTiles);
|
||||
~MaterialPreviewPass() override;
|
||||
|
||||
// 本槽位的回读暂存(host-visible,持久映射):Renderer 在该
|
||||
// 槽位 fence 通过后 memcpy 取走烘焙像素
|
||||
Buffer* BakeStaging(u32 frameSlot) const
|
||||
{
|
||||
return frameSlot < kMaxFramesInFlight
|
||||
? m_BakeStaging[frameSlot].get()
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
||||
@@ -246,6 +308,11 @@ namespace Engine {
|
||||
RG::TextureHandle m_VelocityAtlas;
|
||||
RG::TextureHandle m_SDRAtlas;
|
||||
const std::vector<PreviewDirtyTile>& m_DirtyTiles;
|
||||
const std::vector<PreviewBakeTile>& m_BakeTiles;
|
||||
|
||||
// 回读暂存:每帧槽位一份(容量 = 渲染预算 × 单 tile 字节)。
|
||||
// 拷贝录制在该槽位命令缓冲,Renderer 在该槽位 fence 通过后读
|
||||
std::array<Scope<Buffer>, kMaxFramesInFlight> m_BakeStaging;
|
||||
|
||||
// ---- 预览专用 set0(与 uboSetLayout 同布局,内容恒定)----
|
||||
// UBO = 固定棚拍相机/布光;SSBO = GPUDriven 契约的最小占位
|
||||
|
||||
@@ -111,6 +111,17 @@ namespace Engine {
|
||||
// 脏表引用与槽位——脏 tile 本帧不再解引用已销毁材质(UAF)
|
||||
void DropMaterialPreview(const Material* material);
|
||||
void* GetMaterialPreviewTexture() const;
|
||||
|
||||
// ---- 材质球缩略图烘焙(GPU→CPU 异步回读,无 waitIdle)----
|
||||
// UI 段对"需要静态缩略图"的材质逐帧幂等请求(userKey 由调用方
|
||||
// 定义,Renderer 不解释、原样回传;同 Material serial 去重)。渲染
|
||||
// 段把该材质渲染进预览 tile 后当帧拷进 host 暂存,待该帧槽位
|
||||
// fence 通过(两帧后同槽位再入图)再取走——不阻塞主线程
|
||||
void RequestMaterialPreviewBake(Material* material, u64 userKey);
|
||||
// UI 段轮询:有完成的烘焙则取走(一次性)。像素 = RGBA8,
|
||||
// kPreviewTileSize²(tonemap 后的 SDR,可直接当缩略图)
|
||||
bool PollMaterialPreviewBake(u64& outKey, std::vector<u8>& outPixels,
|
||||
u32& outWidth, u32& outHeight);
|
||||
#endif
|
||||
|
||||
// ---- 后处理参数(ImGui 可调,转发到 pass 共享的 PostProcessParams)----
|
||||
@@ -213,6 +224,9 @@ namespace Engine {
|
||||
void HandleResize(bool recreateSwapchain);
|
||||
void ReadGpuTimes(u32 frameSlot);
|
||||
void RefreshSceneTextures(); // 物理 view → ImGui 描述符(重建后刷新)
|
||||
#ifdef HELLO_EDITOR
|
||||
void CollectBakeResults(u32 frameSlot); // 槽位 fence 后取走回读结果
|
||||
#endif
|
||||
// 期望渲染分辨率:Editor 视口 = 面板尺寸(未上报时回退 swapchain);
|
||||
// Direct = swapchain 尺寸
|
||||
vk::Extent2D DesiredRenderExtent() const;
|
||||
@@ -300,6 +314,34 @@ namespace Engine {
|
||||
// TakeRenders 取预算内的脏 tile 换入 m_PreviewRenders(pass 只读)
|
||||
Passes::PreviewTilePool m_PreviewPool;
|
||||
std::vector<Passes::PreviewDirtyTile> m_PreviewRenders;
|
||||
// 上一轮注册到 ImGui 的预览 atlas 物理 view:重编译后若镜像真的
|
||||
// 换新(固定尺寸资源跨视口 resize 会保留同一镜像)才需让池重画,
|
||||
// 否则预览球内容存续、不闪烁
|
||||
vk::ImageView m_PreviewViews[kMaxFramesInFlight]{};
|
||||
|
||||
// ---- 材质球缩略图烘焙(GPU→CPU 异步回读)----
|
||||
struct PreviewBakeReq
|
||||
{
|
||||
u64 key = 0; // 调用方令牌(App 侧 path → key)
|
||||
u64 serial = 0; // Material serial(去重/失效判据)
|
||||
Material* material = nullptr; // 仅 !copied 期间被解引用
|
||||
i32 frameTile = -1; // 本帧 RequestBake 返回的槽位
|
||||
bool copied = false; // 拷贝已录制,等待回读
|
||||
u64 lastRequestFrame = 0; // UI keepalive 帧号(老化丢弃判据)
|
||||
};
|
||||
std::vector<PreviewBakeReq> m_BakeReqs; // 跨帧存活至回读完成
|
||||
u64 m_FrameIndex = 0; // 单调帧号(请求老化判据)
|
||||
std::vector<Passes::PreviewBakeTile> m_PreviewBakes; // 当帧(pass 只读)
|
||||
// 每槽位当帧录制的回读 key(帧 N 录,帧 N+2 同槽位 fence 后取走)
|
||||
std::array<std::vector<u64>, kMaxFramesInFlight> m_BakeRecorded;
|
||||
struct PreviewBakeResult
|
||||
{
|
||||
u64 key = 0;
|
||||
std::vector<u8> pixels;
|
||||
u32 width = 0;
|
||||
u32 height = 0;
|
||||
};
|
||||
std::vector<PreviewBakeResult> m_BakeDone;
|
||||
#endif
|
||||
// GPU timestamp(pass 级耗时,ImGui 可读)
|
||||
vk::QueryPool m_QueryPool;
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
|
||||
#include <Engine/Assets/IAssetUploader.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
@@ -49,7 +52,8 @@ namespace Engine {
|
||||
~VulkanAssetUploader() override;
|
||||
|
||||
// ---- 材质资产(.material 数据段 → GPU 段)----
|
||||
// 每槽路径建独占纹理(空/缺失回退共享默认纹理),再组 Material
|
||||
// 每槽经路径纹理缓存(跨材质去重)取纹理,同批新纹理合并为一次
|
||||
// submit;空/缺失回退共享默认纹理,再组 Material
|
||||
Scope<MaterialAsset> CreateMaterial(MaterialAssetData&& data) override;
|
||||
// 设备空闲屏障(编辑器资产重建路径:卸载旧 GPU 资源前全停飞)
|
||||
void WaitDeviceIdle() override;
|
||||
@@ -60,6 +64,12 @@ namespace Engine {
|
||||
void InitSharedDefaultTextures();
|
||||
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures{};
|
||||
|
||||
// 路径纹理缓存:键 = 归一化路径 + sRGB 标记 → 独占纹理。多个
|
||||
// 材质引用同一贴图(仓库 4K 贴图被多材质共享)只解码/上传一次。
|
||||
// 生命周期长于所有 MaterialAsset(uploader 在 assets 之后析构),
|
||||
// 故 Material 可安全持裸指针。DDS 与 stbi 同缓存
|
||||
std::unordered_map<std::string, Scope<Texture>> m_TextureCache;
|
||||
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
DescriptorManager& m_Descriptors;
|
||||
|
||||
@@ -42,6 +42,18 @@ namespace Engine {
|
||||
Device& device, MemoryAllocator& allocator,
|
||||
const void* data, u32 bytes, bool srgb);
|
||||
|
||||
// 批式上传(一次 submit + 一次 fence 等待服务同批多张贴图,省掉
|
||||
// 每张各自的命令池创建销毁与阻塞等待):解码 + 建 image/view/
|
||||
// sampler + 把拷贝/barrier 录制进调用方 cmd(**不**提交)。staging
|
||||
// 暂存对象内,调用方在批量 submit fence 通过后逐张
|
||||
// ReleasePendingStaging 释放;析构亦兜底释放。DDS(稀有)仍走
|
||||
// 自带提交,不参与批量
|
||||
static Scope<Texture> CreateDeferred(Device& device,
|
||||
MemoryAllocator& allocator,
|
||||
const std::string& path, bool srgb,
|
||||
vk::CommandBuffer cmd);
|
||||
void ReleasePendingStaging();
|
||||
|
||||
~Texture();
|
||||
|
||||
Texture(const Texture&) = delete;
|
||||
@@ -62,9 +74,19 @@ namespace Engine {
|
||||
const std::array<u8, 4>& rgba, bool srgb);
|
||||
|
||||
// 公共路径:staging → device local image → blit 生成完整 mip 链 →
|
||||
// view/sampler(RGBA8,各向异性过滤)
|
||||
// view/sampler(RGBA8,各向异性过滤)。录制进 cmd 后自行提交等待
|
||||
void InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb);
|
||||
|
||||
// 批式路径构造(cmd 由调用方提供,不在本构造内提交)
|
||||
Texture(Device& device, MemoryAllocator& allocator,
|
||||
const std::string& path, bool srgb, vk::CommandBuffer cmd);
|
||||
|
||||
// 公共上传路径核心:建 staging/image/view/sampler + 逐 mip
|
||||
// barrier/copy/blit 录制进 cmd(不提交、不销毁 staging,staging 存
|
||||
// m_PendingStaging)。InitFromPixels 与批式路径共用
|
||||
void RecordPixelsUpload(const void* pixels, u32 width, u32 height,
|
||||
bool srgb, vk::CommandBuffer cmd);
|
||||
|
||||
// DDS 路径:压缩数据直传(BC 硬件采样解码)+ 文件自带 mip 链
|
||||
void InitFromDds(const std::string& path, bool srgb);
|
||||
|
||||
@@ -83,6 +105,10 @@ namespace Engine {
|
||||
|
||||
u64 m_TextureBytes = 0;
|
||||
|
||||
// 批式上传暂存的 staging(submit fence 通过前不可释放)
|
||||
bool m_HasPendingStaging = false;
|
||||
MemoryAllocator::Allocation m_PendingStaging{};
|
||||
|
||||
MemoryAllocator::Allocation m_ImageAllocation;
|
||||
MemoryAllocator& m_Allocator;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user