Files
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

385 lines
16 KiB
C++
Raw Permalink 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.
//
// Created by atdunbg on 2026/9/22.
//
// RenderGraph:声明式帧图内核。
//
// 设计原则(面向引擎,非 demo):
// 1. 声明与执行分离:图结构(pass + 虚拟资源)只声明一次并 Compile,
// 每帧 Execute 零图结构开销、零堆分配(barrier 列表全部编译期预算)。
// 2. 虚拟资源 + 生成号句柄:上层永远不持有裸 vulkan 句柄,
// 生命周期、layout、物理内存全部由图管理。
// 3. 物理资源池:真实 GpuImage 按 (format, extent, usage) 池化复用,
// 每个虚拟资源按帧槽位拥有 kMaxFramesInFlight 个物理实例,
// 从根上避免帧间读写竞争,同时杜绝每帧重建 GPU 资源。
// 4. 自动同步:layout/访问状态链在 Compile 期解算,产出批量
// vk::ImageMemoryBarrier2(Synchronization2),上层不再手写 barrier。
//
// 已知边界(后续演进点):
// - 暂不支持异步 compute queue / 跨队列信号量
// - 同一 pass 内对同一资源"读+写"(input attachment / RWM)未建模
//
#ifndef HELLOVULKAN_RENDERGRAPH_H
#define HELLOVULKAN_RENDERGRAPH_H
#include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h>
#include <Engine/Vulkan/MemoryAllocator.h>
#include <vulkan/vulkan.hpp>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
namespace Engine::RG {
// ---------- 句柄 ----------
// 带生成号的索引句柄:图永远不会删除条目(只追加),
// 生成号用于上层误持过期句柄时的校验,防止 use-after-free 式错误。
inline constexpr u32 kInvalidHandle = 0xFFFFFFFF;
template<typename Tag>
struct Handle {
u32 index = kInvalidHandle;
u32 generation = 0;
bool Valid() const { return index != kInvalidHandle; }
explicit operator bool() const { return Valid(); }
};
struct TextureTag;
using TextureHandle = Handle<TextureTag>;
// ---------- 资源使用方式 ----------
// 声明 pass 怎么用资源;layout/access/stage 映射和 image usage
// 都由这里推导,上层不需要(也不允许)手写。
enum class ResourceUse : u8 {
ColorAttachment,
DepthAttachment,
ShaderRead,
TransferSrc,
TransferDst,
Present, // 仅用于 swapchain 导入资源:transition 到 ePresentSrcKHR
};
// ---------- 虚拟纹理描述 ----------
struct TextureDesc {
vk::Format format = vk::Format::eUndefined;
u32 width = 0; // 0 = 跟随目标(swapchain)尺寸 × sizeScale
u32 height = 0;
f32 sizeScale = 1.0f; // 相对尺寸(后处理降采样用,如 bloom 0.5)
// 附加 usage(在 pass 读写推导之外;如调试截图需要 TRANSFER_SRC)
vk::ImageUsageFlags extraUsage{};
};
// ---------- 物理镜像 ----------
struct PhysicalImage {
vk::Image image{};
vk::ImageView view{};
vk::Extent2D extent{};
vk::Format format{};
vk::ImageUsageFlags usage{};
MemoryAllocator::Allocation allocation{};
bool valid = false;
};
// ---------- Pass 上下文(Execute 回调收到的) ----------
struct RenderGraphContext {
vk::CommandBuffer cmd;
FrameContext frame;
vk::Extent2D extent{};
// 按句柄取当前帧槽位的物理资源(image/view)
vk::ImageView View(TextureHandle h) const;
vk::Image Image(TextureHandle h) const;
// 物理尺寸(导入资源 = SetImportedExtent 设定值;创建资源 = 物理镜像)
vk::Extent2D Extent(TextureHandle h) const;
// 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。
// 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。
u32 HistorySlot(u32 frameSlot) const;
class RenderGraph* graph = nullptr;
};
using ExecuteFn = std::function<void(RenderGraphContext&)>;
// ---------- 图内部结构 ----------
struct TextureEntry {
std::string name;
u32 index = 0; // 自身在 m_Textures 中的下标
TextureDesc desc;
bool imported = false; // swapchain 导入资源无物理实例
u32 generation = 1;
// Compile 期推导
vk::ImageUsageFlags usage{};
vk::ImageAspectFlagBits aspect = vk::ImageAspectFlagBits::eColor;
i32 firstPass = -1;
i32 lastPass = -1;
// 物理实例:每个帧槽位一个,created 资源才拥有
std::vector<PhysicalImage*> physical;
// 导入资源每帧绑定
vk::Image importedImage{};
vk::ImageView importedView{};
vk::Extent2D importedExtent{}; // 物理尺寸(swapchain 重建后更新)
// 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体)
struct SlotState {
bool used = false;
};
std::vector<SlotState> slots;
// 是否存在帧历史读(Compile 期校验:历史资源必须有同帧正常使用,
// 否则没有物理镜像可读)
bool hasHistoryUse = false;
// Compile 期状态链解算用(执行序上的"当前"状态)
vk::ImageLayout chainLayout = vk::ImageLayout::eUndefined;
vk::PipelineStageFlags2 chainStage{};
vk::AccessFlags2 chainAccess{};
bool chainWasWrite = false;
// 稳态:上一轮执行结束时(lastPass 之后)的状态。
// 同一帧槽位再次进入图时,entry barrier 从这里出发。
vk::ImageLayout restingLayout = vk::ImageLayout::eUndefined;
vk::PipelineStageFlags2 restingStage{};
vk::AccessFlags2 restingAccess{};
};
struct PassNode {
std::string name;
struct Use {
u32 resIndex;
ResourceUse use;
vk::ShaderStageFlags readStage; // ShaderRead 时的消费 stage
bool history = false; // 帧历史读:读上一帧槽位的物理镜像
};
std::vector<Use> uses;
ExecuteFn exec;
bool sideEffect = false; // 无读写但必须执行(compute 写 buffer 等)
bool culled = false;
vk::Extent2D extent{}; // 本 pass 的渲染尺寸(写入附件的最大分辨率,Compile 期解析)
// Compile 期预算好的 barrier:每个资源两个变体
// steady = 该槽位已被使用过(从 resting 状态出发)
// first = 该槽位首次使用(从 Undefined 出发)
// imported 资源恒用 first(acquire 后 layout 语义上为 Undefined)
// history = 帧历史读(操作上一帧槽位的镜像,steady 变体带跨帧可见性同步)
struct BarrierPair {
u32 resIndex;
vk::ImageMemoryBarrier2 steady;
vk::ImageMemoryBarrier2 first;
bool history = false;
};
std::vector<BarrierPair> barriers;
// 历史读的出口 barrier(exec 之后执行):历史镜像被临时转进读
// layout 采样,读完必须恢复 resting layout,否则下一帧该槽位
// entry barrier 的 oldLayout 与实际 layout 脱节
std::vector<BarrierPair> postBarriers;
};
// ---------- RenderGraph ----------
class RenderGraph {
public:
RenderGraph(Device& device, MemoryAllocator& allocator, u32 framesInFlight);
~RenderGraph();
RenderGraph(const RenderGraph&) = delete;
RenderGraph& operator=(const RenderGraph&) = delete;
// ---- 声明阶段(AddPass 之后不得再声明资源;全部声明完成后 Compile)----
// 声明一张图拥有的瞬时纹理(此时不分配任何 GPU 内存)
TextureHandle CreateTexture(std::string name, const TextureDesc& desc);
// 导入 swapchain 颜色附件(每帧通过 SetImportedImage 绑定物理 image)
TextureHandle ImportSwapchain(std::string name, vk::Format format);
// 每帧 acquire 后调用,把当帧 swapchain image 绑到导入资源
void SetImportedImage(TextureHandle h, vk::Image image, vk::ImageView view);
// 导入资源的物理尺寸(swapchain 重建后同步更新)。pass 渲染区域
// 跟实际附件尺寸走:backbuffer = 窗口尺寸,与目标尺寸(场景渲染
// 分辨率)解耦——Editor 视口模式下二者不同
void SetImportedExtent(TextureHandle h, vk::Extent2D extent);
// 指定帧槽位的物理镜像 view(Compile 后有效)。
// 用途:把 RG 管理的资源绑定到外部描述符(如阴影图采样)。
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
vk::ImageView GetPhysicalView(TextureHandle h, u32 frameSlot) const;
// 帧历史槽位 = 上一帧使用的槽位:(slot - 1 + F) mod F
u32 HistorySlot(u32 frameSlot) const {
return (frameSlot + m_FramesInFlight - 1) % m_FramesInFlight;
}
// ---- Pass 声明 ----
class PassBuilder {
public:
PassBuilder& Write(TextureHandle h, ResourceUse use);
PassBuilder& Read(TextureHandle h, ResourceUse use,
vk::ShaderStageFlags stage = vk::ShaderStageFlagBits::eFragment);
// 帧历史读:读上一帧槽位的物理镜像(TAA/运动模糊/时域滤波用)。
// - 不产生图内边:跨帧依赖由同队列提交序 + 历史 barrier 保证
// - 资源必须有同帧正常使用(否则无物理镜像),Compile 期校验
// - 采样描述符在 pass 内用 GetPhysicalView(h, HistorySlot(slot)) 预绑
PassBuilder& ReadHistory(TextureHandle h, ResourceUse use,
vk::ShaderStageFlags stage = vk::ShaderStageFlagBits::eFragment);
PassBuilder& SideEffect();
private:
friend class RenderGraph;
PassBuilder(RenderGraph& graph, u32 passIndex)
: m_Graph(graph), m_PassIndex(passIndex) {}
RenderGraph& m_Graph;
u32 m_PassIndex;
};
[[nodiscard]] PassBuilder AddPass(std::string name, ExecuteFn exec);
// ---- 编译与执行 ----
// 编译:死 pass 剔除 → 拓扑排序 → 生命周期 → barrier 预算 → 物理分配。
// 目标尺寸变化时调用 SetTargetExtent 会自动重编译并轮换物理资源。
void Compile();
void SetTargetExtent(vk::Extent2D extent);
vk::Extent2D GetTargetExtent() const { return m_TargetExtent; }
// 每帧执行:按编译好的顺序录命令。要求 cmd 处于 recording 状态。
void Execute(const FrameContext& frame);
// GPU timestamp(可选):注入 pool 后,每个 pass 前后各写一枚
// 时间戳,索引 = frameSlot * queriesPerFrame + 执行序位置 * 2。
// pool 由调用方创建(容量 ≥ framesInFlight × queriesPerFrame),
// 读取由调用方在本槽位 fence 等待之后进行。
void SetQueryPool(vk::QueryPool pool, u32 queriesPerFrame);
// GPU profiler(可选):pass 边界的 GPU zone 钩子(Tracy 等工具的
// 接缝,RG 本体不感知任何 profiler 实现)。BeginPass 返回的 scope
// 析构 = 结束时间戳;注入者须保证 scope 生命周期覆盖 pass lambda。
// EndFrame 在全部 pass 录制完后调用(仍在当帧 cmd 内,提交前)。
class IGpuProfiler {
public:
virtual ~IGpuProfiler() = default;
class IScope {
public:
virtual ~IScope() = default;
};
[[nodiscard]] virtual Scope<IScope> BeginPass(
const char* name, vk::CommandBuffer cmd) = 0;
virtual void EndFrame(vk::CommandBuffer cmd) = 0;
};
void SetGpuProfiler(IGpuProfiler* profiler) { m_GpuProfiler = profiler; }
std::vector<std::string> GetPassNames() const;
// ---- 统计 ----
u32 GetPassCount() const;
u32 GetActivePassCount() const;
u32 GetTextureCount() const;
u32 GetBarrierCount() const;
u32 GetPooledImageCount() const;
// GPU timestamp:指定槽位上一轮实际写入的查询数(active pass × 2)。
// 剔除使 active 数 < 总 pass 数——读侧必须按此数取结果,按满额
// 阻塞读会等永远无人写入的查询(死锁)
u32 GetWrittenQueries(u32 frameSlot) const
{
return frameSlot < 4 ? m_WrittenQueries[frameSlot] : 0;
}
private:
friend struct RenderGraphContext;
// 编译子步骤
void ResolveUsage(TextureEntry& entry);
void CullPasses();
void Toposort();
void ComputeLifetimes();
void PrecomputeBarriers();
void AllocatePhysical();
// 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,
vk::ImageAspectFlagBits aspect);
TextureEntry& GetEntry(TextureHandle h);
const TextureEntry& GetEntry(TextureHandle h) const;
void CheckHandle(TextureHandle h) const;
void CheckMutable() const; // 声明期校验:Compile 后禁止再改图
static vk::ImageLayout UseLayout(ResourceUse use);
static vk::PipelineStageFlags2 UseDstStage(const PassNode::Use& use);
static vk::PipelineStageFlags2 UseSrcStage(ResourceUse use);
static vk::AccessFlags2 UseDstAccess(const PassNode::Use& use);
static vk::AccessFlags2 UseSrcAccess(ResourceUse use);
Device& m_Device;
MemoryAllocator& m_Allocator;
u32 m_FramesInFlight;
bool m_Compiled = false;
std::vector<TextureEntry> m_Textures;
std::vector<PassNode> m_Passes;
u32 m_ActivePassCount = 0;
vk::Extent2D m_TargetExtent{};
// 拓扑排序后的执行序(存 pass 下标)
std::vector<u32> m_ExecutionOrder;
// GPU timestamp(可选注入)
vk::QueryPool m_QueryPool{};
u32 m_QueriesPerFrame = 0;
u32 m_WrittenQueries[4] = {}; // 每槽位上一轮实际写入数(active×2)
IGpuProfiler* m_GpuProfiler = nullptr; // 非拥有(Renderer 持有)
// 物理资源池:key = (format, extent, usage),value = 空闲镜像列表
struct PoolKey {
u32 format;
u32 width;
u32 height;
u32 usage;
bool operator==(const PoolKey& o) const {
return format == o.format && width == o.width &&
height == o.height && usage == o.usage;
}
};
struct PoolKeyHash {
size_t operator()(const PoolKey& k) const {
size_t h = std::hash<u64>{}(
(static_cast<u64>(k.format) << 32) ^ k.usage);
h ^= std::hash<u64>{}((static_cast<u64>(k.width) << 32) ^ k.height) << 1;
return h;
}
};
std::unordered_map<PoolKey, std::vector<Scope<PhysicalImage>>, PoolKeyHash> m_Pool;
// Execute 用的 barrier 暂存(成员变量,避免每帧堆分配)
std::vector<vk::ImageMemoryBarrier2> m_ScratchBarriers;
};
} // namespace Engine::RG
#endif //HELLOVULKAN_RENDERGRAPH_H