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