资产浏览器可视化改造 + 材质缩略图 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;
|
||||
};
|
||||
|
||||
@@ -28,6 +28,13 @@ namespace Engine::RG {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 固定尺寸资源:desc 显式给定 width/height,物理尺寸与目标渲染
|
||||
// 分辨率无关(如材质球预览 atlas)。这类镜像跨重编译沿用,内容存续
|
||||
bool IsResolutionIndependent(const TextureEntry& e)
|
||||
{
|
||||
return !e.imported && e.desc.width != 0 && e.desc.height != 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ---------- 构造 / 析构 ----------
|
||||
@@ -39,7 +46,8 @@ namespace Engine::RG {
|
||||
|
||||
RenderGraph::~RenderGraph()
|
||||
{
|
||||
ReleasePhysical();
|
||||
// 析构需全量归还(含固定尺寸资源),池随后统一销毁
|
||||
ReleasePhysical(false);
|
||||
|
||||
const auto& vkDevice = m_Device.GetDevice();
|
||||
for (auto &images: m_Pool | std::views::values) {
|
||||
@@ -172,7 +180,9 @@ namespace Engine::RG {
|
||||
ENGINE_FATAL("RenderGraph: Compile before SetTargetExtent");
|
||||
}
|
||||
|
||||
ReleasePhysical();
|
||||
// 固定尺寸镜像已在 entry 上沿用(SetTargetExtent 只回收尺寸随目标
|
||||
// 变化的镜像),此处再收一次只对非固定尺寸的残留生效
|
||||
ReleasePhysical(true);
|
||||
|
||||
// 清理上一次编译的中间结果
|
||||
for (auto& entry : m_Textures) {
|
||||
@@ -229,11 +239,15 @@ namespace Engine::RG {
|
||||
if (extent == m_TargetExtent) return;
|
||||
|
||||
// 尺寸变化:调用方必须保证 GPU 空闲(通常在 swapchain recreate 的 waitIdle 之后)
|
||||
ReleasePhysical();
|
||||
// 只回收尺寸随目标变化的镜像;固定尺寸资源(预览 atlas)保留在
|
||||
// entry 上,PurgePool 也看不到它们,内容不会被销毁
|
||||
ReleasePhysical(true);
|
||||
PurgePool(extent);
|
||||
|
||||
// 物理镜像全部换新,槽位状态清零
|
||||
// 只有被回收重建的镜像才回到"首次使用"(Undefined 出发);保留的
|
||||
// 镜像仍在上一轮 resting layout,槽位使用态必须跟着保留
|
||||
for (auto& entry : m_Textures) {
|
||||
if (IsResolutionIndependent(entry)) continue;
|
||||
for (auto& slot : entry.slots) slot.used = false;
|
||||
}
|
||||
|
||||
@@ -594,6 +608,19 @@ namespace Engine::RG {
|
||||
};
|
||||
|
||||
for (u32 slot = 0; slot < m_FramesInFlight; ++slot) {
|
||||
// 跨重编译保留的固定尺寸镜像:形态与本次一致则原样沿用
|
||||
//(同一 VkImage → 内容存续);不一致则归还池再重分配
|
||||
PhysicalImage* keep = entry.physical[slot];
|
||||
if (keep) {
|
||||
if (keep->format == entry.desc.format &&
|
||||
keep->extent == extent && keep->usage == entry.usage) {
|
||||
continue;
|
||||
}
|
||||
PoolKey key{static_cast<u32>(keep->format),
|
||||
keep->extent.width, keep->extent.height,
|
||||
static_cast<u32>(keep->usage)};
|
||||
m_Pool[key].push_back(Scope<PhysicalImage>(keep));
|
||||
}
|
||||
entry.physical[slot] = AcquirePhysical(
|
||||
entry.desc.format, extent, entry.usage, entry.aspect);
|
||||
}
|
||||
@@ -601,10 +628,11 @@ namespace Engine::RG {
|
||||
}
|
||||
}
|
||||
|
||||
void RenderGraph::ReleasePhysical()
|
||||
void RenderGraph::ReleasePhysical(bool keepFixedSize)
|
||||
{
|
||||
for (auto& entry : m_Textures) {
|
||||
if (entry.imported) continue;
|
||||
if (keepFixedSize && IsResolutionIndependent(entry)) continue;
|
||||
for (u32 slot = 0; slot < entry.physical.size(); ++slot) {
|
||||
auto* img = entry.physical[slot];
|
||||
if (!img) continue;
|
||||
|
||||
@@ -174,10 +174,8 @@ namespace Engine
|
||||
|
||||
MaterialAsset::MaterialAsset(
|
||||
MaterialAssetData&& data,
|
||||
std::array<Scope<Texture>, Material::kSlotCount>&& ownedTextures,
|
||||
Scope<Material>&& material)
|
||||
: m_Data(std::move(data))
|
||||
, m_OwnedTextures(std::move(ownedTextures))
|
||||
, m_Material(std::move(material))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
#include <Engine/Vulkan/UniformBuffer.h>
|
||||
#include <Engine/Vulkan/Buffer.h>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
@@ -103,7 +104,8 @@ namespace Engine::Passes
|
||||
DescriptorManager& descriptors, SceneGpu& gpu,
|
||||
FullscreenGpu& fullscreen, RG::TextureHandle hdrAtlas,
|
||||
RG::TextureHandle velocityAtlas, RG::TextureHandle sdrAtlas,
|
||||
const std::vector<PreviewDirtyTile>& dirtyTiles)
|
||||
const std::vector<PreviewDirtyTile>& dirtyTiles,
|
||||
const std::vector<PreviewBakeTile>& bakeTiles)
|
||||
: m_Device(device)
|
||||
, m_Allocator(allocator)
|
||||
, m_Descriptors(descriptors)
|
||||
@@ -113,6 +115,7 @@ namespace Engine::Passes
|
||||
, m_VelocityAtlas(velocityAtlas)
|
||||
, m_SDRAtlas(sdrAtlas)
|
||||
, m_DirtyTiles(dirtyTiles)
|
||||
, m_BakeTiles(bakeTiles)
|
||||
{
|
||||
// ---- set0:棚拍 UBO + GPUDriven 契约占位(内容恒定,创建期
|
||||
// 一次写入;in-flight 帧并发只读同一份,无需 per-slot ring)----
|
||||
@@ -190,8 +193,22 @@ namespace Engine::Passes
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_TonemapSets[slot] = fullscreen.AllocateSet();
|
||||
}
|
||||
|
||||
// ---- 烘焙回读暂存:每槽位一份 host-visible 持久映射缓冲 ----
|
||||
const vk::DeviceSize bakeBytes =
|
||||
static_cast<vk::DeviceSize>(PreviewTilePool::kPreviewRendersPerFrame) *
|
||||
kPreviewTileBytes;
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_BakeStaging[slot] = CreateScope<Buffer>(
|
||||
device, allocator, bakeBytes,
|
||||
vk::BufferUsageFlagBits::eTransferDst,
|
||||
vk::MemoryPropertyFlagBits::eHostVisible |
|
||||
vk::MemoryPropertyFlagBits::eHostCoherent);
|
||||
}
|
||||
}
|
||||
|
||||
MaterialPreviewPass::~MaterialPreviewPass() = default;
|
||||
|
||||
void MaterialPreviewPass::Register(RG::RenderGraph& rg)
|
||||
{
|
||||
// ---- 球体渲染:HDR atlas(loadOp=Load 保留缓存内容,逐脏 tile
|
||||
@@ -325,6 +342,42 @@ namespace Engine::Passes
|
||||
.Read(m_HDRAtlas, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
.Write(m_SDRAtlas, RG::ResourceUse::ColorAttachment);
|
||||
|
||||
// ---- 烘焙回读:当帧渲染过的 tile 从 SDR atlas 拷进 host 暂存 ----
|
||||
// TransferSrc use 让 RG 自动完成 eColorAttachmentOptimal →
|
||||
// eTransferSrcOptimal 迁移(ImGui 随后仍以 ShaderRead 采样同一图,
|
||||
// RG 按 read-read layout 变化再迁回)。SideEffect 保证空表帧不被
|
||||
// 剔除(barrier 照常插入,无拷贝则 GPU 零实际工作)
|
||||
rg.AddPass("MaterialPreviewBake",
|
||||
[this](RG::RenderGraphContext& ctx) {
|
||||
if (m_BakeTiles.empty()) return;
|
||||
Buffer* staging = m_BakeStaging[ctx.frame.frameSlot].get();
|
||||
if (!staging) return;
|
||||
const vk::Image src = ctx.Image(m_SDRAtlas);
|
||||
for (size_t i = 0; i < m_BakeTiles.size(); ++i) {
|
||||
const u32 tile = m_BakeTiles[i].tile;
|
||||
if (tile >= kPreviewTileCount) continue;
|
||||
const u32 col = tile % kPreviewTilesPerSide;
|
||||
const u32 row = tile / kPreviewTilesPerSide;
|
||||
vk::BufferImageCopy region;
|
||||
region.setBufferOffset(static_cast<vk::DeviceSize>(i) *
|
||||
kPreviewTileBytes)
|
||||
.setBufferRowLength(0)
|
||||
.setBufferImageHeight(0)
|
||||
.setImageSubresource(vk::ImageSubresourceLayers(
|
||||
vk::ImageAspectFlagBits::eColor, 0, 0, 1))
|
||||
.setImageOffset(vk::Offset3D{
|
||||
i32(col * kPreviewTileSize),
|
||||
i32(row * kPreviewTileSize), 0})
|
||||
.setImageExtent(vk::Extent3D{
|
||||
kPreviewTileSize, kPreviewTileSize, 1});
|
||||
ctx.cmd.copyImageToBuffer(
|
||||
src, vk::ImageLayout::eTransferSrcOptimal,
|
||||
staging->Get(), region);
|
||||
}
|
||||
})
|
||||
.Read(m_SDRAtlas, RG::ResourceUse::TransferSrc)
|
||||
.SideEffect();
|
||||
}
|
||||
|
||||
void MaterialPreviewPass::RecordBall(vk::CommandBuffer cmd, u32 tile,
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include <Engine/Vulkan/CommandManager.h>
|
||||
#include <Engine/Vulkan/SyncManager.h>
|
||||
#include <Engine/Vulkan/UniformBuffer.h>
|
||||
#include <Engine/Vulkan/Buffer.h>
|
||||
#include <Engine/Core/Window.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ImGui/ImGuiLayer.h>
|
||||
@@ -563,7 +564,7 @@ namespace Engine
|
||||
m_Passes.push_back(CreateScope<Passes::MaterialPreviewPass>(
|
||||
*m_Device, *m_Allocator, *m_DescriptorManager, *m_SceneGpu,
|
||||
*m_FullscreenGpu, m_PreviewHDR, m_PreviewVelocity, m_PreviewSDR,
|
||||
m_PreviewRenders));
|
||||
m_PreviewRenders, m_PreviewBakes));
|
||||
m_PreviewPass =
|
||||
static_cast<Passes::MaterialPreviewPass*>(m_Passes.back().get());
|
||||
#endif
|
||||
@@ -673,6 +674,11 @@ namespace Engine
|
||||
void Renderer::DropMaterialPreview(const Material* material)
|
||||
{
|
||||
m_PreviewPool.DropMaterial(material);
|
||||
if (material) {
|
||||
const u64 serial = material->GetSerial();
|
||||
std::erase_if(m_BakeReqs, [serial](const PreviewBakeReq& r)
|
||||
{ return r.serial == serial; });
|
||||
}
|
||||
}
|
||||
|
||||
void* Renderer::GetMaterialPreviewTexture() const
|
||||
@@ -684,6 +690,66 @@ namespace Engine
|
||||
}
|
||||
return tex;
|
||||
}
|
||||
|
||||
void Renderer::RequestMaterialPreviewBake(Material* material, u64 userKey)
|
||||
{
|
||||
if (!material) return;
|
||||
const u64 serial = material->GetSerial();
|
||||
// 同 serial 去重(逐帧 keepalive);同 key 换 serial(资产重载 →
|
||||
// 旧实例销毁)时替换,避免残留悬垂 Material*
|
||||
std::erase_if(m_BakeReqs, [&](const PreviewBakeReq& r)
|
||||
{ return r.key == userKey && r.serial != serial; });
|
||||
for (auto& r : m_BakeReqs) {
|
||||
if (r.serial == serial) {
|
||||
r.material = material; r.key = userKey;
|
||||
r.lastRequestFrame = m_FrameIndex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
PreviewBakeReq req;
|
||||
req.key = userKey;
|
||||
req.serial = serial;
|
||||
req.material = material;
|
||||
req.lastRequestFrame = m_FrameIndex;
|
||||
m_BakeReqs.push_back(req);
|
||||
}
|
||||
|
||||
bool Renderer::PollMaterialPreviewBake(u64& outKey, std::vector<u8>& outPixels,
|
||||
u32& outWidth, u32& outHeight)
|
||||
{
|
||||
if (m_BakeDone.empty()) return false;
|
||||
PreviewBakeResult res = std::move(m_BakeDone.front());
|
||||
m_BakeDone.erase(m_BakeDone.begin());
|
||||
outKey = res.key;
|
||||
outPixels = std::move(res.pixels);
|
||||
outWidth = res.width;
|
||||
outHeight = res.height;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 槽位 fence 已通过 → 帧 N-2 在该槽位录制的拷贝已完成,取走并出队
|
||||
void Renderer::CollectBakeResults(u32 frameSlot)
|
||||
{
|
||||
std::vector<u64> recorded = std::move(m_BakeRecorded[frameSlot]);
|
||||
m_BakeRecorded[frameSlot].clear();
|
||||
if (recorded.empty()) return;
|
||||
if (!m_PreviewPass) return;
|
||||
Buffer* staging = m_PreviewPass->BakeStaging(frameSlot);
|
||||
const auto* base =
|
||||
static_cast<const u8*>(staging ? staging->MappedPtr() : nullptr);
|
||||
if (!base) return;
|
||||
for (size_t i = 0; i < recorded.size(); ++i) {
|
||||
PreviewBakeResult res;
|
||||
res.key = recorded[i];
|
||||
res.width = Passes::kPreviewTileSize;
|
||||
res.height = Passes::kPreviewTileSize;
|
||||
const u8* src = base + i * Passes::kPreviewTileBytes;
|
||||
res.pixels.assign(src, src + Passes::kPreviewTileBytes);
|
||||
m_BakeDone.push_back(std::move(res));
|
||||
std::erase_if(m_BakeReqs, [&](const PreviewBakeReq& r)
|
||||
{ return r.key == recorded[i]; });
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// RG 物理镜像按帧槽位各一份:每槽位注册一个 ImGui 描述符。
|
||||
@@ -691,21 +757,27 @@ namespace Engine
|
||||
void Renderer::RefreshSceneTextures()
|
||||
{
|
||||
if (!m_ImGuiLayer || !m_PresentTarget.Valid()) return;
|
||||
bool previewRecreated = false;
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_ImGuiLayer->UpdateSceneTexture(
|
||||
slot, m_RenderGraph->GetPhysicalView(m_PresentTarget, slot));
|
||||
// 材质球 atlas 同槽位注册(语义与场景纹理一致)
|
||||
#ifdef HELLO_EDITOR
|
||||
if (m_PreviewSDR.Valid()) {
|
||||
m_ImGuiLayer->UpdatePreviewTexture(
|
||||
slot, m_RenderGraph->GetPhysicalView(m_PreviewSDR, slot));
|
||||
const vk::ImageView view =
|
||||
m_RenderGraph->GetPhysicalView(m_PreviewSDR, slot);
|
||||
// 固定尺寸的预览 atlas 跨视口 resize 沿用同一镜像(内容
|
||||
// 存续);只有镜像真的换新时才需要让池重画
|
||||
previewRecreated = previewRecreated || view != m_PreviewViews[slot];
|
||||
m_PreviewViews[slot] = view;
|
||||
m_ImGuiLayer->UpdatePreviewTexture(slot, view);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#ifdef HELLO_EDITOR
|
||||
// 重编译 = 物理镜像全部换新(三槽位 atlas 内容清零)——池的
|
||||
// paintedSlots 记录全部失效,可见行 keepalive 请求会重新补画
|
||||
if (m_PreviewSDR.Valid()) {
|
||||
// 物理镜像换新 = atlas 内容清零,池的 paintedSlots 记录全部失效,
|
||||
// 可见行 keepalive 请求会重新补画
|
||||
if (previewRecreated) {
|
||||
m_PreviewPool.InvalidateAll();
|
||||
}
|
||||
#endif
|
||||
@@ -756,6 +828,9 @@ namespace Engine
|
||||
|
||||
const u32 frameSlot = m_CurrentFrame;
|
||||
const auto& vkDevice = m_Device->GetDevice();
|
||||
#ifdef HELLO_EDITOR
|
||||
++m_FrameIndex; // 单调帧号(烘焙请求老化判据)
|
||||
#endif
|
||||
|
||||
// 1. 场景 → 当帧渲染数据(相机/灯光/双视图剔除;纯 CPU,不碰 GPU)
|
||||
if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent, dt)) {
|
||||
@@ -791,6 +866,14 @@ namespace Engine
|
||||
// 2. 等本槽位 fence → 写 UBO/SSBO → reset(写-读竞争关闭)
|
||||
m_SyncManager->WaitFence(frameSlot);
|
||||
ReadGpuTimes(frameSlot); // fence 已等:上一轮该槽位的查询已就绪
|
||||
#ifdef HELLO_EDITOR
|
||||
CollectBakeResults(frameSlot); // 该槽位上一轮拷贝已完成 → 取走
|
||||
// 老化丢弃:UI 已不再 keepalive 的未完成烘焙请求(离屏/换目录),
|
||||
// 避免其持续占用渲染预算
|
||||
std::erase_if(m_BakeReqs, [&](const PreviewBakeReq& r)
|
||||
{ return !r.copied &&
|
||||
m_FrameIndex - r.lastRequestFrame > 4; });
|
||||
#endif
|
||||
m_UniformBuffer->Update(frameSlot, &frame.ubo, sizeof(frame.ubo));
|
||||
m_SyncManager->ResetFence(frameSlot);
|
||||
|
||||
@@ -903,7 +986,31 @@ namespace Engine
|
||||
// 全部跳帧路径,标记 paintedSlots 的槽位镜像本帧必然被写入)。
|
||||
// 预算外的脏 tile 保持未取状态,下帧可见行 keepalive 请求重新入队
|
||||
#ifdef HELLO_EDITOR
|
||||
// 烘焙准备:UI keepalive 请求的材质逐个 RequestBake(强制标脏,
|
||||
// 使其进入本帧脏表;池满无槽者 -1 下帧重试)
|
||||
for (auto& r : m_BakeReqs) {
|
||||
if (r.copied) continue;
|
||||
r.frameTile = m_PreviewPool.RequestBake(r.material);
|
||||
}
|
||||
m_PreviewRenders = m_PreviewPool.TakeRenders(frameSlot);
|
||||
// 回读表只收"本帧确实渲染过"的 tile(条数 ≤ 渲染预算)——内容
|
||||
// 当帧新鲜,拷贝可靠;未渲染的请求留待下帧
|
||||
m_PreviewBakes.clear();
|
||||
u32 renderedTiles = 0;
|
||||
for (const auto& d : m_PreviewRenders) {
|
||||
renderedTiles |= (1u << d.tile);
|
||||
}
|
||||
for (auto& r : m_BakeReqs) {
|
||||
if (r.copied || r.frameTile < 0) continue;
|
||||
if (!(renderedTiles & (1u << r.frameTile))) continue;
|
||||
m_PreviewBakes.push_back(
|
||||
Passes::PreviewBakeTile{r.key, static_cast<u32>(r.frameTile)});
|
||||
r.copied = true;
|
||||
}
|
||||
// 每槽位记录当帧回读 key(帧 N 录 → N+2 同槽位 fence 后取走)
|
||||
auto& recorded = m_BakeRecorded[frameSlot];
|
||||
recorded.clear();
|
||||
for (const auto& b : m_PreviewBakes) recorded.push_back(b.key);
|
||||
#endif
|
||||
|
||||
m_RenderGraph->Execute(frameCtx);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
@@ -31,34 +32,84 @@ namespace Engine {
|
||||
InitSharedDefaultTextures();
|
||||
|
||||
// 每槽解析:路径(CWD 相对,反斜杠归一,与 Model::GetOrLoadTexture
|
||||
// 同语义)→ 独占纹理;空/缺失 → 共享默认纹理(非拥有)
|
||||
std::array<Scope<Texture>, Material::kSlotCount> owned;
|
||||
// 同语义)→ 路径缓存命中的共享纹理 / 本批新建;空/缺失 → 共享
|
||||
// 默认纹理(非拥有)。新建纹理的拷贝/barrier 全部录制进同一 cmd,
|
||||
// 末尾一次 submit + 一次 fence 等待服务整批——取代过去"每张贴图
|
||||
// 各自建命令池 + 提交 + 阻塞等待",这是大材质目录首开卡顿的根因
|
||||
std::array<Texture*, Material::kSlotCount> bound{};
|
||||
// 槽位 sRGB 语义:颜色图 true,数据图 false(与 Model 同表)
|
||||
const bool slotSrgb[Material::kSlotCount] = {true, false, false,
|
||||
true, false, false};
|
||||
|
||||
std::vector<Texture*> created; // 本批新建纹理(fence 后释放 staging)
|
||||
vk::CommandPool pool = nullptr;
|
||||
vk::CommandBuffer cmd = nullptr;
|
||||
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
std::string path = data.texPaths[slot];
|
||||
std::replace(path.begin(), path.end(), '\\', '/');
|
||||
if (!path.empty() && std::filesystem::exists(path)) {
|
||||
owned[slot] = CreateScope<Texture>(
|
||||
m_Device, m_Allocator, path, slotSrgb[slot]);
|
||||
bound[slot] = owned[slot].get();
|
||||
}
|
||||
else {
|
||||
if (path.empty() || !std::filesystem::exists(path)) {
|
||||
if (!path.empty()) {
|
||||
ENGINE_WARN("MaterialAsset: texture not found '{}', "
|
||||
"slot {} falls back to default", path, slot);
|
||||
}
|
||||
bound[slot] = m_DefaultTextures[slot].get();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 缓存键:路径 + sRGB 标记(同一文件按颜色/数据两种语义加载时
|
||||
// 格式不同,必须分开缓存)
|
||||
std::string key = path;
|
||||
key += slotSrgb[slot] ? "|s" : "|l";
|
||||
if (auto it = m_TextureCache.find(key); it != m_TextureCache.end()) {
|
||||
bound[slot] = it->second.get();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 未命中:惰性建批式命令缓冲,解码 + 录制上传(不提交)
|
||||
if (!cmd) {
|
||||
vk::CommandPoolCreateInfo poolInfo;
|
||||
poolInfo.setQueueFamilyIndex(m_Device.GetGraphicsFamily())
|
||||
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
|
||||
pool = m_Device.GetDevice().createCommandPool(poolInfo);
|
||||
|
||||
vk::CommandBufferAllocateInfo allocInfo;
|
||||
allocInfo.setCommandPool(pool)
|
||||
.setLevel(vk::CommandBufferLevel::ePrimary)
|
||||
.setCommandBufferCount(1);
|
||||
cmd = m_Device.GetDevice().allocateCommandBuffers(allocInfo)[0];
|
||||
|
||||
cmd.begin(vk::CommandBufferBeginInfo{
|
||||
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
|
||||
}
|
||||
|
||||
auto tex = Texture::CreateDeferred(m_Device, m_Allocator, path,
|
||||
slotSrgb[slot], cmd);
|
||||
Texture* raw = tex.get();
|
||||
bound[slot] = raw;
|
||||
created.push_back(raw);
|
||||
m_TextureCache.emplace(std::move(key), std::move(tex));
|
||||
}
|
||||
|
||||
// 整批一次提交 + 一次等待,随后释放各新建纹理的 staging
|
||||
if (cmd) {
|
||||
cmd.end();
|
||||
vk::Fence fence = m_Device.GetDevice().createFence({});
|
||||
m_Device.GetGraphicsQueue().submit(
|
||||
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
|
||||
if (m_Device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX)
|
||||
!= vk::Result::eSuccess) {
|
||||
ENGINE_ERROR("Material texture batch upload: waitForFences failed");
|
||||
}
|
||||
m_Device.GetDevice().destroyFence(fence);
|
||||
m_Device.GetDevice().destroyCommandPool(pool);
|
||||
for (Texture* t : created) t->ReleasePendingStaging();
|
||||
}
|
||||
|
||||
auto mat = CreateScope<Material>(
|
||||
m_Device, m_Allocator, m_Descriptors, m_TextureLayout,
|
||||
bound, data.params, data.transparent);
|
||||
return CreateScope<MaterialAsset>(std::move(data), std::move(owned),
|
||||
std::move(mat));
|
||||
return CreateScope<MaterialAsset>(std::move(data), std::move(mat));
|
||||
}
|
||||
|
||||
void VulkanAssetUploader::WaitDeviceIdle()
|
||||
|
||||
@@ -93,7 +93,11 @@ namespace Engine
|
||||
return tex;
|
||||
}
|
||||
|
||||
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb)
|
||||
// 公共上传路径核心:建 staging/image/view/sampler + 逐 mip
|
||||
// barrier/copy/blit 录制进 cmd(**不**提交)。staging 存 m_PendingStaging,
|
||||
// 由调用方在提交 fence 通过后 ReleasePendingStaging 释放
|
||||
void Texture::RecordPixelsUpload(const void* pixels, u32 width, u32 height,
|
||||
bool srgb, vk::CommandBuffer cmd)
|
||||
{
|
||||
// 重入安全:CreateFromEncodedMemory 先经 ProceduralTag ctor 落 1x1
|
||||
// 占位(只为确定 device/allocator 成员),再调本函数传真实像素。
|
||||
@@ -118,15 +122,17 @@ namespace Engine
|
||||
vk::DeviceSize imageSize = static_cast<vk::DeviceSize>(width) *
|
||||
static_cast<vk::DeviceSize>(height) * 4; // RGBA8
|
||||
|
||||
// 2. 创建 Staging Buffer(HOST_VISIBLE,VMA 持久映射)
|
||||
auto stagingAlloc = m_Allocator.CreateBuffer(
|
||||
// 2. 创建 Staging Buffer(HOST_VISIBLE,VMA 持久映射)。暂存对象内,
|
||||
// 由调用方在提交 fence 通过后 ReleasePendingStaging 释放
|
||||
m_PendingStaging = m_Allocator.CreateBuffer(
|
||||
imageSize,
|
||||
vk::BufferUsageFlagBits::eTransferSrc,
|
||||
vk::MemoryPropertyFlagBits::eHostVisible |
|
||||
vk::MemoryPropertyFlagBits::eHostCoherent);
|
||||
m_HasPendingStaging = true;
|
||||
|
||||
// 3. 拷贝像素到 Staging Buffer
|
||||
std::memcpy(stagingAlloc.MappedPtr, pixels, static_cast<size_t>(imageSize));
|
||||
std::memcpy(m_PendingStaging.MappedPtr, pixels, static_cast<size_t>(imageSize));
|
||||
|
||||
// 4. 创建 Device Local Image
|
||||
vk::ImageCreateInfo imageInfo;
|
||||
@@ -180,25 +186,9 @@ namespace Engine
|
||||
|
||||
m_Sampler = m_Device.GetDevice().createSampler(samplerInfo);
|
||||
|
||||
// 6. 一次性命令缓冲:Staging → Image
|
||||
vk::CommandPoolCreateInfo poolInfo;
|
||||
poolInfo.setQueueFamilyIndex(m_Device.GetGraphicsFamily())
|
||||
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
|
||||
|
||||
vk::CommandPool pool = m_Device.GetDevice().createCommandPool(poolInfo);
|
||||
|
||||
vk::CommandBufferAllocateInfo allocInfo;
|
||||
allocInfo.setCommandPool(pool)
|
||||
.setLevel(vk::CommandBufferLevel::ePrimary)
|
||||
.setCommandBufferCount(1);
|
||||
|
||||
vk::CommandBuffer cmd = m_Device.GetDevice().allocateCommandBuffers(allocInfo)[0];
|
||||
|
||||
vk::CommandBufferBeginInfo beginInfo;
|
||||
beginInfo.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
|
||||
cmd.begin(beginInfo);
|
||||
|
||||
// 6.1 布局:UNDEFINED → TRANSFER_DST
|
||||
// 6. 布局:UNDEFINED → TRANSFER_DST(整条 mip 链一次性转目标布局:
|
||||
// 后续 blit 的目标 mip 必须已在目标布局;仅转 mip0 会触发
|
||||
// validation 对每个 blit 目标的 UNDEFINED 布局告警)
|
||||
vk::ImageMemoryBarrier2 toTransfer;
|
||||
toTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
|
||||
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
|
||||
@@ -207,9 +197,6 @@ namespace Engine
|
||||
.setOldLayout(vk::ImageLayout::eUndefined)
|
||||
.setNewLayout(vk::ImageLayout::eTransferDstOptimal)
|
||||
.setImage(m_Image)
|
||||
// 整条 mip 链一次性 UNDEFINED → TRANSFER_DST:后续 blit
|
||||
// 的目标 mip 必须已在目标布局(仅转 mip0 会触发 validation
|
||||
// 对每个 blit 目标的 UNDEFINED 布局告警)
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, m_MipLevels, 0, 1});
|
||||
|
||||
@@ -226,7 +213,7 @@ namespace Engine
|
||||
.setImageOffset({0, 0, 0})
|
||||
.setImageExtent({m_Width, m_Height, 1});
|
||||
|
||||
cmd.copyBufferToImage(stagingAlloc.Buffer, m_Image,
|
||||
cmd.copyBufferToImage(m_PendingStaging.Buffer, m_Image,
|
||||
vk::ImageLayout::eTransferDstOptimal, region);
|
||||
|
||||
// 6.3 blit 生成 mip 链:逐级半分辨率缩小(硬件线性滤波)。
|
||||
@@ -291,27 +278,6 @@ namespace Engine
|
||||
dep2.setImageMemoryBarriers(toShader);
|
||||
cmd.pipelineBarrier2(dep2);
|
||||
|
||||
cmd.end();
|
||||
|
||||
// 7. 提交并等待
|
||||
vk::SubmitInfo submit;
|
||||
submit.setCommandBuffers(cmd);
|
||||
|
||||
vk::FenceCreateInfo fenceInfo;
|
||||
vk::Fence fence = m_Device.GetDevice().createFence(fenceInfo);
|
||||
|
||||
m_Device.GetGraphicsQueue().submit(submit, fence);
|
||||
auto waitResult = m_Device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX);
|
||||
if (waitResult != vk::Result::eSuccess)
|
||||
{
|
||||
ENGINE_ERROR("Texture upload: waitForFences failed");
|
||||
}
|
||||
|
||||
// 8. 清理临时对象(staging 资源与分配经 VMA 成对释放)
|
||||
m_Device.GetDevice().destroyFence(fence);
|
||||
m_Device.GetDevice().destroyCommandPool(pool);
|
||||
m_Allocator.Destroy(stagingAlloc);
|
||||
|
||||
// 显存占用按真实格式 + mip 链估算(与分配器台账同一算法)
|
||||
if (m_TextureBytes != 0) {
|
||||
STATS.RemoveTexture(m_TextureBytes); // 重建防双计
|
||||
@@ -321,6 +287,87 @@ namespace Engine
|
||||
STATS.AddTexture(m_TextureBytes);
|
||||
}
|
||||
|
||||
// 单张自提交路径(程序化/CreateFromEncodedMemory/非批式下载):录制
|
||||
// 完自行建一次性命令池 + 提交 + fence 等待 + 释放 staging
|
||||
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height,
|
||||
bool srgb)
|
||||
{
|
||||
vk::CommandPoolCreateInfo poolInfo;
|
||||
poolInfo.setQueueFamilyIndex(m_Device.GetGraphicsFamily())
|
||||
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
|
||||
vk::CommandPool pool = m_Device.GetDevice().createCommandPool(poolInfo);
|
||||
|
||||
vk::CommandBufferAllocateInfo allocInfo;
|
||||
allocInfo.setCommandPool(pool)
|
||||
.setLevel(vk::CommandBufferLevel::ePrimary)
|
||||
.setCommandBufferCount(1);
|
||||
vk::CommandBuffer cmd =
|
||||
m_Device.GetDevice().allocateCommandBuffers(allocInfo)[0];
|
||||
|
||||
cmd.begin(vk::CommandBufferBeginInfo{
|
||||
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
|
||||
RecordPixelsUpload(pixels, width, height, srgb, cmd);
|
||||
cmd.end();
|
||||
|
||||
vk::Fence fence = m_Device.GetDevice().createFence({});
|
||||
m_Device.GetGraphicsQueue().submit(
|
||||
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
|
||||
if (m_Device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) !=
|
||||
vk::Result::eSuccess) {
|
||||
ENGINE_ERROR("Texture upload: waitForFences failed");
|
||||
}
|
||||
|
||||
m_Device.GetDevice().destroyFence(fence);
|
||||
m_Device.GetDevice().destroyCommandPool(pool);
|
||||
ReleasePendingStaging();
|
||||
}
|
||||
|
||||
// 批式路径构造:复用 stbi 解码 + RecordPixelsUpload(录制进外部 cmd,
|
||||
// 不提交;staging 交由调用方在批量 fence 后释放)。DDS 稀有路径退回
|
||||
// 自带提交的 InitFromDds
|
||||
Texture::Texture(Device& device, MemoryAllocator& allocator,
|
||||
const std::string& path, bool srgb, vk::CommandBuffer cmd)
|
||||
: m_Device(device), m_Allocator(allocator)
|
||||
{
|
||||
const std::string ext = path.size() >= 4
|
||||
? path.substr(path.size() - 4) : "";
|
||||
std::string lower;
|
||||
for (char c : ext) lower += static_cast<char>(std::tolower(c));
|
||||
|
||||
if (lower == ".dds") {
|
||||
InitFromDds(path, srgb);
|
||||
return;
|
||||
}
|
||||
|
||||
int width = 0, height = 0, channels = 0;
|
||||
stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels,
|
||||
STBI_rgb_alpha);
|
||||
if (!pixels) {
|
||||
ENGINE_ERROR("Failed to load texture: {}", path);
|
||||
return;
|
||||
}
|
||||
RecordPixelsUpload(pixels, static_cast<u32>(width),
|
||||
static_cast<u32>(height), srgb, cmd);
|
||||
stbi_image_free(pixels);
|
||||
ENGINE_INFO("Texture loaded (batched): {} ({}x{})", path, m_Width,
|
||||
m_Height);
|
||||
}
|
||||
|
||||
Scope<Texture> Texture::CreateDeferred(Device& device,
|
||||
MemoryAllocator& allocator,
|
||||
const std::string& path, bool srgb,
|
||||
vk::CommandBuffer cmd)
|
||||
{
|
||||
return Scope<Texture>(new Texture(device, allocator, path, srgb, cmd));
|
||||
}
|
||||
|
||||
void Texture::ReleasePendingStaging()
|
||||
{
|
||||
if (!m_HasPendingStaging) return;
|
||||
m_Allocator.Destroy(m_PendingStaging);
|
||||
m_HasPendingStaging = false;
|
||||
}
|
||||
|
||||
// DDS → Vulkan 格式映射。sRGB 语义由贴图槽位决定(渲染侧职责):
|
||||
// 颜色图(BaseColor/Emissive)→ SRGB 变体
|
||||
// 数据图(Normal/Specular 等线性槽位)→ UNORM
|
||||
@@ -520,6 +567,10 @@ namespace Engine
|
||||
{
|
||||
const auto& vkDevice = m_Device.GetDevice();
|
||||
|
||||
// 批式上传若在 fence 通过前被析构(如加载中途失败):兜底释放
|
||||
// staging,避免 VMA 块非空断言崩溃
|
||||
ReleasePendingStaging();
|
||||
|
||||
vkDevice.destroySampler(m_Sampler);
|
||||
vkDevice.destroyImageView(m_View);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user