feat: RenderGraph 帧历史资源(ReadHistory)+ TemporalDiff 验证 pass

- PassBuilder::ReadHistory:读上一帧槽位 ((slot-1+F) mod F) 的物理镜像,
  TAA/运动模糊等时域效果的前置能力,RG 最后一个已知边界补齐
- 历史读不建拓扑边(跨帧依赖由同队列提交序保证)、不入本帧状态链、
  不改生命周期;剔除照常参与(生产者存活判定)
- 历史 barrier:首帧走 first(old=Undefined),之后走 steady 且
  src=ALL_COMMANDS|MEMORY_WRITE 显式闭合跨帧可用性+可见性;
  有历史读的资源,常规 entry steady 的 src 同样升级(历史读打破了
  同槽位 fence 全等保前提)
- TemporalDiff 调试 pass:|上一帧−当前帧|×增益,静止=黑/运动边缘发光
  即链路正确;Tonemap binding2 经 push 开关切调试视图
- 编译零错误、validation 零警告、8 秒冒烟通过(9 pass 全活跃)
This commit is contained in:
2026-09-23 13:05:10 +08:00
parent 0de8498350
commit c75832e73e
15 changed files with 294 additions and 17 deletions
+92 -6
View File
@@ -130,6 +130,23 @@ namespace Engine::RG {
return *this;
}
RenderGraph::PassBuilder& RenderGraph::PassBuilder::ReadHistory(
TextureHandle h, ResourceUse use, vk::ShaderStageFlags stage)
{
m_Graph.CheckMutable();
m_Graph.CheckHandle(h);
auto& entry = m_Graph.m_Textures[h.index];
if (entry.imported) {
// swapchain image 不按槽位持久存在,历史读无意义
throw std::runtime_error(
"RenderGraph: history read of imported resource '" + entry.name +
"' is not supported");
}
entry.hasHistoryUse = true;
m_Graph.m_Passes[m_PassIndex].uses.push_back({h.index, use, stage, true});
return *this;
}
RenderGraph::PassBuilder& RenderGraph::PassBuilder::SideEffect()
{
m_Graph.CheckMutable();
@@ -265,6 +282,7 @@ namespace Engine::RG {
bool keep = pass.sideEffect;
for (const auto& use : pass.uses) {
// 帧历史读照常参与:它消费了上一帧的产出,生产者必须存活
if (IsWriteUse(use.use) && readByKept[use.resIndex]) {
keep = true;
} else if (m_Textures[use.resIndex].imported) {
@@ -317,6 +335,11 @@ namespace Engine::RG {
auto& lw = lastWriter[use.resIndex];
auto& readers = pendingReaders[use.resIndex];
// 帧历史读不建边:它消费的是上一帧槽位的物理镜像(与本帧
// 任何写操作无资源竞争),跨帧依赖由同队列提交序 + 历史
// barrier(ALL_COMMANDS 可见性同步)保证
if (use.history) continue;
if (IsWriteUse(use.use)) {
for (u32 r : readers) addEdge(r, i); // WAR
readers.clear();
@@ -363,6 +386,7 @@ namespace Engine::RG {
for (u32 pos = 0; pos < m_ExecutionOrder.size(); ++pos) {
const u32 passIdx = m_ExecutionOrder[pos];
for (const auto& use : m_Passes[passIdx].uses) {
if (use.history) continue; // 帧历史读不改变本帧状态链与生命周期
auto& entry = m_Textures[use.resIndex];
if (entry.firstPass < 0) entry.firstPass = static_cast<i32>(pos);
entry.lastPass = static_cast<i32>(pos);
@@ -387,6 +411,7 @@ namespace Engine::RG {
// ---- Pass A:状态链推演 ----
for (const u32 passIdx : m_ExecutionOrder) {
for (const auto& use : m_Passes[passIdx].uses) {
if (use.history) continue; // 历史读操作另一槽位的镜像,不入本帧链
auto& entry = m_Textures[use.resIndex];
entry.chainLayout = UseLayout(use.use);
entry.chainStage = UseDstStage(use);
@@ -416,6 +441,36 @@ namespace Engine::RG {
for (const auto& use : pass.uses) {
auto& entry = m_Textures[use.resIndex];
// ---- 帧历史读:操作上一帧槽位的物理镜像,不进入本帧状态链。
// first = 该槽位镜像从未用过(图刚编译/重编译),old=Undefined
// steady = 跨帧可见性同步:src=ALL_COMMANDS|MEMORY_WRITE
// 覆盖上一帧提交对该镜像的全部写入(同队列提交序
// 保证 barrier 执行时上一帧命令已全部完成,
// ALL_COMMANDS 把可用性+可见性显式闭合),
// old/new = resting layout(历史镜像静止在读布局)
if (use.history) {
const vk::ImageLayout desiredLayout = UseLayout(use.use);
const vk::PipelineStageFlags2 dstStage = UseDstStage(use);
const vk::AccessFlags2 dstAccess = UseDstAccess(use);
vk::ImageMemoryBarrier2 first;
first.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
.setDstStageMask(dstStage)
.setDstAccessMask(dstAccess)
.setOldLayout(vk::ImageLayout::eUndefined)
.setNewLayout(desiredLayout)
.setSubresourceRange({entry.aspect, 0, 1, 0, 1});
vk::ImageMemoryBarrier2 steady = first;
steady.setSrcStageMask(vk::PipelineStageFlagBits2::eAllCommands)
.setSrcAccessMask(vk::AccessFlagBits2::eMemoryWrite)
.setOldLayout(entry.restingLayout);
pass.barriers.push_back({use.resIndex, steady, first, true});
continue; // 不推进 chain:本帧链只描述当前槽位镜像
}
const vk::ImageLayout desiredLayout = UseLayout(use.use);
const vk::PipelineStageFlags2 dstStage = UseDstStage(use);
const vk::AccessFlags2 dstAccess = UseDstAccess(use);
@@ -451,6 +506,17 @@ namespace Engine::RG {
steady = makeBarrier(entry.restingLayout,
entry.restingStage,
entry.restingAccess);
if (entry.hasHistoryUse) {
// 历史读打破了"同槽位两次进入之间有 fence 全等保"
// 的前提(上一帧对该槽位镜像的历史读不在本槽位
// fence 的保护范围内),src 升级为 ALL_COMMANDS
// 覆盖跨帧的一切触碰
steady.setSrcStageMask(
vk::PipelineStageFlagBits2::eAllCommands)
.setSrcAccessMask(
vk::AccessFlagBits2::eMemoryWrite |
vk::AccessFlagBits2::eMemoryRead);
}
}
pass.barriers.push_back({use.resIndex, steady, first});
@@ -481,7 +547,16 @@ namespace Engine::RG {
void RenderGraph::AllocatePhysical()
{
for (auto& entry : m_Textures) {
if (entry.imported || entry.firstPass < 0) continue;
if (entry.imported) continue;
// 帧历史校验:历史资源必须有同帧正常使用(否则既无产出也无镜像)
if (entry.hasHistoryUse && entry.firstPass < 0) {
throw std::runtime_error(
"RenderGraph: history-read resource '" + entry.name +
"' has no same-frame use (nothing to read history of)");
}
if (entry.firstPass < 0) continue;
// 显式尺寸优先;否则 = 目标尺寸 × sizeScale(后处理降采样)
vk::Extent2D extent{
@@ -649,21 +724,27 @@ namespace Engine::RG {
for (const auto& bp : pass.barriers) {
auto& entry = m_Textures[bp.resIndex];
u32 imageSlot = frame.frameSlot;
bool useFirst;
if (entry.imported) {
useFirst = true; // 恒为 Undefined 起点
} else if (bp.history) {
// 帧历史:操作上一帧槽位的物理镜像。
// used 标记持久(跨帧),首帧走 first,之后走 steady
imageSlot = HistorySlot(frame.frameSlot);
useFirst = !entry.slots[imageSlot].used;
entry.slots[imageSlot].used = true;
} else {
auto& slot = entry.slots[frame.frameSlot];
useFirst = !slot.used;
slot.used = true;
useFirst = !entry.slots[imageSlot].used;
entry.slots[imageSlot].used = true;
}
vk::ImageMemoryBarrier2 b = useFirst ? bp.first : bp.steady;
// image 句柄在执行期绑定:imported 用当帧 acquire 的 image,
// created 用当前帧槽位的物理镜像(每个槽位各有一份)
// created 用当前帧槽位(历史 barrier 用上一帧槽位)的物理镜像
b.setImage(entry.imported
? entry.importedImage
: entry.physical[frame.frameSlot]->image);
: entry.physical[imageSlot]->image);
m_ScratchBarriers.push_back(b);
}
@@ -707,6 +788,11 @@ namespace Engine::RG {
return entry.physical[frame.frameSlot]->image;
}
u32 RenderGraphContext::HistorySlot(u32 frameSlot) const
{
return graph->HistorySlot(frameSlot);
}
// ---------- 工具 / 统计 ----------
TextureEntry& RenderGraph::GetEntry(TextureHandle h)
+13 -2
View File
@@ -16,12 +16,16 @@ namespace Engine::Passes
ShaderManager& shaders)
: m_Device(device), m_Descriptors(descriptors)
{
// Set0 = {HDR 颜色图, Bloom 图},push = 16B 通用结构
// Set0 = {HDR 颜色图, Bloom 图, 调试输入},push = 16B 通用结构
//(binding2 只被 tonemap 的调试视图使用;bloom 系 shader 静态
// 使用的 binding 是布局的子集,合法)
std::vector<DescriptorBinding> bindings = {
{0, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment},
{2, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment},
};
setLayout = descriptors.CreateLayout(bindings);
@@ -59,13 +63,19 @@ namespace Engine::Passes
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{setLayout},
static_cast<u32>(sizeof(FullscreenPush)));
auto diffF = shaders.CreateFromFile(
shaderDir + "temporal_diff.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{setLayout},
static_cast<u32>(sizeof(FullscreenPush)));
vert = vertShader.Handle;
tonemapFrag = tonemapF.Handle;
extractFrag = extractF.Handle;
blurFrag = blurF.Handle;
diffFrag = diffF.Handle;
if (!vert || !tonemapFrag || !extractFrag || !blurFrag) {
if (!vert || !tonemapFrag || !extractFrag || !blurFrag || !diffFrag) {
ENGINE_FATAL("FullscreenGpu: failed to create shader objects");
}
}
@@ -78,6 +88,7 @@ namespace Engine::Passes
if (tonemapFrag) vkDevice.destroyShaderEXT(tonemapFrag);
if (extractFrag) vkDevice.destroyShaderEXT(extractFrag);
if (blurFrag) vkDevice.destroyShaderEXT(blurFrag);
if (diffFrag) vkDevice.destroyShaderEXT(diffFrag);
vkDevice.destroySampler(sampler);
vkDevice.destroyPipelineLayout(layout);
@@ -0,0 +1,49 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/TemporalDiffPass.h>
#include <Engine/Core/FrameContext.h>
namespace Engine::Passes
{
TemporalDiffPass::TemporalDiffPass(FullscreenGpu& gpu, f32 gain,
RG::TextureHandle hdrColor,
RG::TextureHandle output)
: m_Gpu(gpu), m_Gain(gain), m_HDR(hdrColor), m_Output(output)
{
m_Sets.resize(kMaxFramesInFlight);
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_Sets[slot] = m_Gpu.AllocateSet();
}
}
void TemporalDiffPass::Register(RG::RenderGraph& rg)
{
// 帧历史读 + 当前帧读:同一虚拟资源的两个槽位镜像在同一个
// pass 里采样(TAA 的标准形态)。历史读不建图内边,跨帧同步
// 由 RG 的历史 barrier(ALL_COMMANDS 可见性)保证。
rg.AddPass("TemporalDiff",
[this](RG::RenderGraphContext& ctx) {
m_Gpu.RecordFullscreen(ctx, ctx.View(m_Output), m_Gpu.diffFrag,
m_Sets[ctx.frame.frameSlot],
FullscreenPush{0.0f, m_Gain, 0.0f, 0.0f});
})
.ReadHistory(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Write(m_Output, RG::ResourceUse::ColorAttachment);
}
void TemporalDiffPass::OnTargetsRebuilt(RG::RenderGraph& rg)
{
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
// binding0 = 帧历史(上一帧槽位镜像),binding1 = 当前帧
m_Gpu.UpdateSetInput(m_Sets[slot], 0, rg, m_HDR,
rg.HistorySlot(slot));
m_Gpu.UpdateSetInput(m_Sets[slot], 1, rg, m_HDR, slot);
}
}
} // namespace Engine::Passes
+10 -2
View File
@@ -9,9 +9,11 @@ namespace Engine::Passes
{
TonemapPass::TonemapPass(FullscreenGpu& gpu, PostProcessParams& params,
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
RG::TextureHandle temporalDiff,
RG::TextureHandle backbuffer)
: m_Gpu(gpu), m_Params(params),
m_HDR(hdrColor), m_A(bloomA), m_Backbuffer(backbuffer)
m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff),
m_Backbuffer(backbuffer)
{
m_Sets.resize(kMaxFramesInFlight);
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
@@ -28,12 +30,17 @@ namespace Engine::Passes
m_Sets[ctx.frame.frameSlot],
FullscreenPush{m_Params.exposure,
m_Params.useACES ? 1.0f : 0.0f,
m_Params.bloomStrength, 0.0f});
m_Params.bloomStrength,
m_Params.showTemporalDiff ? 1.0f : 0.0f});
})
.Write(m_Backbuffer, RG::ResourceUse::ColorAttachment)
.Read(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_A, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
// shader 静态采样 binding2(调试视图开关在 push constant 里),
// 声明读否则本 pass 的产出无人消费会被剔除、镜像也不会分配
.Read(m_TemporalDiff, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment);
}
@@ -42,6 +49,7 @@ namespace Engine::Passes
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_Gpu.UpdateSetInput(m_Sets[slot], 0, rg, m_HDR, slot);
m_Gpu.UpdateSetInput(m_Sets[slot], 1, rg, m_A, slot);
m_Gpu.UpdateSetInput(m_Sets[slot], 2, rg, m_TemporalDiff, slot);
}
}
+13 -1
View File
@@ -16,6 +16,7 @@
#include <Engine/Renderer/Passes/ForwardPass.h>
#include <Engine/Renderer/Passes/BloomPass.h>
#include <Engine/Renderer/Passes/TonemapPass.h>
#include <Engine/Renderer/Passes/TemporalDiffPass.h>
#include <Engine/Renderer/Passes/ImGuiPass.h>
#include <Engine/Renderer/Passes/PresentPass.h>
#include <Engine/Vulkan/Device.h>
@@ -143,6 +144,8 @@ namespace Engine
void Renderer::SetBloomThreshold(f32 v) { m_PostParams->bloomThreshold = v; }
f32 Renderer::GetBloomStrength() const { return m_PostParams->bloomStrength; }
void Renderer::SetBloomStrength(f32 v) { m_PostParams->bloomStrength = v; }
bool Renderer::GetShowTemporalDiff() const { return m_PostParams->showTemporalDiff; }
void Renderer::SetShowTemporalDiff(bool b) { m_PostParams->showTemporalDiff = b; }
u32 Renderer::GetMainInstanceCount() const
{
@@ -194,6 +197,12 @@ namespace Engine
"BloomB",
RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat, 0, 0, 0.5f});
// 帧历史差异(调试视图):|上一帧 − 当前帧| × 增益。
// 帧历史机制(ReadHistory)的验证消费端,也是 TAA 的参考形态
m_TemporalDiff = m_RenderGraph->CreateTexture(
"TemporalDiff",
RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat, 0, 0, 0.5f});
// ---- pass 注册:执行序由拓扑排序决定,与注册顺序无关 ----
// 要加新 pass:new 一个 RenderPass 子类,在这里 push 一行。
FrameSceneData& frame = m_SceneRenderer->Frame();
@@ -201,10 +210,13 @@ namespace Engine
*m_SceneGpu, frame, m_ShadowMap));
m_Passes.push_back(CreateScope<Passes::ForwardPass>(
*m_SceneGpu, frame, m_HDRColor, m_Depth, m_ShadowMap));
m_Passes.push_back(CreateScope<Passes::TemporalDiffPass>(
*m_FullscreenGpu, /*gain*/ 8.0f, m_HDRColor, m_TemporalDiff));
m_Passes.push_back(CreateScope<Passes::BloomPass>(
*m_FullscreenGpu, *m_PostParams, m_HDRColor, m_BloomA, m_BloomB));
m_Passes.push_back(CreateScope<Passes::TonemapPass>(
*m_FullscreenGpu, *m_PostParams, m_HDRColor, m_BloomA, m_Backbuffer));
*m_FullscreenGpu, *m_PostParams, m_HDRColor, m_BloomA,
m_TemporalDiff, m_Backbuffer));
m_Passes.push_back(CreateScope<Passes::ImGuiPass>(
&m_ImGuiLayer, m_Backbuffer));
m_Passes.push_back(CreateScope<Passes::PresentPass>(m_Backbuffer));