diff --git a/App/main.cpp b/App/main.cpp index e6dbead..b221b4e 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -166,6 +166,13 @@ protected: renderer.SetBloomStrength(strength); } + // 调试视图:帧历史差异(静止=黑,运动边缘发光=链路正确) + bool temporalDiff = renderer.GetShowTemporalDiff(); + if (ImGui::Checkbox("Temporal Diff View", &temporalDiff)) + { + renderer.SetShowTemporalDiff(temporalDiff); + } + // Pass 级 GPU 耗时(RG timestamp) ImGui::Separator(); ImGui::Text("GPU Timings"); diff --git a/Engine/include/Engine/Core/Application.h b/Engine/include/Engine/Core/Application.h index d644870..0a2462e 100644 --- a/Engine/include/Engine/Core/Application.h +++ b/Engine/include/Engine/Core/Application.h @@ -124,7 +124,7 @@ namespace Engine { u64 m_FrameCount = 0; // 帧率控制 - bool m_VSync = false; // 垂直同步(present 自带节奏) + bool m_VSync = true; // 垂直同步(present 自带节奏) f32 m_TargetFps = 60.0f; // 限帧目标(≥24) std::chrono::steady_clock::time_point m_LastFrameTime; diff --git a/Engine/include/Engine/RenderGraph/RenderGraph.h b/Engine/include/Engine/RenderGraph/RenderGraph.h index 870b169..0432e06 100644 --- a/Engine/include/Engine/RenderGraph/RenderGraph.h +++ b/Engine/include/Engine/RenderGraph/RenderGraph.h @@ -16,7 +16,6 @@ // // 已知边界(后续演进点): // - 暂不支持异步 compute queue / 跨队列信号量 -// - 暂不支持内存别名(需 VMA)与帧历史资源(TAA 等) // - 同一 pass 内对同一资源"读+写"(input attachment / RWM)未建模 // @@ -98,6 +97,10 @@ namespace Engine::RG { vk::ImageView View(TextureHandle h) const; vk::Image Image(TextureHandle h) const; + // 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。 + // 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。 + u32 HistorySlot(u32 frameSlot) const; + class RenderGraph* graph = nullptr; }; @@ -131,6 +134,10 @@ namespace Engine::RG { }; std::vector slots; + // 是否存在帧历史读(Compile 期校验:历史资源必须有同帧正常使用, + // 否则没有物理镜像可读) + bool hasHistoryUse = false; + // Compile 期状态链解算用(执行序上的"当前"状态) vk::ImageLayout chainLayout = vk::ImageLayout::eUndefined; vk::PipelineStageFlags2 chainStage{}; @@ -150,6 +157,7 @@ namespace Engine::RG { u32 resIndex; ResourceUse use; vk::ShaderStageFlags readStage; // ShaderRead 时的消费 stage + bool history = false; // 帧历史读:读上一帧槽位的物理镜像 }; std::vector uses; ExecuteFn exec; @@ -161,10 +169,12 @@ namespace Engine::RG { // 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 barriers; }; @@ -195,6 +205,11 @@ namespace Engine::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 { @@ -202,6 +217,12 @@ namespace Engine::RG { 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: diff --git a/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h b/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h index 2c1c136..55073bb 100644 --- a/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h +++ b/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h @@ -37,6 +37,7 @@ namespace Engine { bool useACES = true; f32 bloomThreshold = 1.0f; // HDR 域亮度阈值 f32 bloomStrength = 0.8f; // 叠加强度 + bool showTemporalDiff = false; // 调试视图:显示帧历史差异 }; class FullscreenGpu { @@ -48,10 +49,11 @@ namespace Engine { FullscreenGpu(const FullscreenGpu&) = delete; FullscreenGpu& operator=(const FullscreenGpu&) = delete; - vk::DescriptorSetLayout setLayout; // Set0 = {输入0, 输入1} + vk::DescriptorSetLayout setLayout; // Set0 = {输入0, 输入1, 调试输入} vk::PipelineLayout layout; // push 16B(FullscreenPush) vk::Sampler sampler; vk::ShaderEXT vert{}, extractFrag{}, blurFrag{}, tonemapFrag{}; + vk::ShaderEXT diffFrag{}; // temporal_diff(帧历史调试) // 各 pass 自己持有 per-slot set;分配统一走这里 vk::DescriptorSet AllocateSet(); diff --git a/Engine/include/Engine/Renderer/Passes/TemporalDiffPass.h b/Engine/include/Engine/Renderer/Passes/TemporalDiffPass.h new file mode 100644 index 0000000..c20d565 --- /dev/null +++ b/Engine/include/Engine/Renderer/Passes/TemporalDiffPass.h @@ -0,0 +1,40 @@ +// +// Created by atdunbg on 2026/9/23. +// +// TemporalDiffPass:帧历史链路调试视图。 +// 读 HDRColor 的帧历史镜像(ReadHistory)与当前帧,输出 |prev − cur| × 增益。 +// 静止画面纯黑、运动边缘发光 = 帧历史机制正确;噪声/残影错位 = 有 bug。 +// 也是 RenderGraph::ReadHistory 的参考实现(TAA 照此模式接入)。 +// + +#ifndef HELLOVULKAN_TEMPORALDIFFPASS_H +#define HELLOVULKAN_TEMPORALDIFFPASS_H + +#include +#include +#include +#include + +namespace Engine::Passes { + + class TemporalDiffPass final : public RenderPass { + public: + TemporalDiffPass(FullscreenGpu& gpu, f32 gain, + RG::TextureHandle hdrColor, RG::TextureHandle output); + ~TemporalDiffPass() override = default; + + void Register(RG::RenderGraph& rg) override; + void OnTargetsRebuilt(RG::RenderGraph& rg) override; + + private: + FullscreenGpu& m_Gpu; + f32 m_Gain; + RG::TextureHandle m_HDR; + RG::TextureHandle m_Output; + + std::vector m_Sets; // per-slot + }; + +} // namespace Engine::Passes + +#endif //HELLOVULKAN_TEMPORALDIFFPASS_H diff --git a/Engine/include/Engine/Renderer/Passes/TonemapPass.h b/Engine/include/Engine/Renderer/Passes/TonemapPass.h index d6380c2..9a72d0d 100644 --- a/Engine/include/Engine/Renderer/Passes/TonemapPass.h +++ b/Engine/include/Engine/Renderer/Passes/TonemapPass.h @@ -20,7 +20,7 @@ namespace Engine::Passes { public: TonemapPass(FullscreenGpu& gpu, PostProcessParams& params, RG::TextureHandle hdrColor, RG::TextureHandle bloomA, - RG::TextureHandle backbuffer); + RG::TextureHandle temporalDiff, RG::TextureHandle backbuffer); ~TonemapPass() override = default; void Register(RG::RenderGraph& rg) override; @@ -31,6 +31,7 @@ namespace Engine::Passes { PostProcessParams& m_Params; RG::TextureHandle m_HDR; RG::TextureHandle m_A; + RG::TextureHandle m_TemporalDiff; // 调试视图输入(当前帧槽位) RG::TextureHandle m_Backbuffer; std::vector m_Sets; // per-slot diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index 08073d8..8a2932f 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -79,6 +79,9 @@ namespace Engine { void SetBloomThreshold(f32 v); f32 GetBloomStrength() const; void SetBloomStrength(f32 v); + // 调试视图:显示帧历史差异(TemporalDiff pass 产出) + bool GetShowTemporalDiff() const; + void SetShowTemporalDiff(bool b); // pass 级 GPU 耗时(最近完成帧,毫秒;与 GetPassGpuMs 下标对应) const std::vector& GetPassNames() const { return m_PassNames; } @@ -125,6 +128,7 @@ namespace Engine { RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),Tonemap 归一 RG::TextureHandle m_BloomA{}; // 半分辨率:提取 + 垂直模糊结果 RG::TextureHandle m_BloomB{}; // 半分辨率:水平模糊中间结果 + RG::TextureHandle m_TemporalDiff{}; // 半分辨率:帧历史差异(调试) // pass 共享的可调参数(Renderer 持有,App 层访问器转发) Scope m_PostParams; diff --git a/Engine/include/Engine/Vulkan/Swapchain.h b/Engine/include/Engine/Vulkan/Swapchain.h index 1b94137..daeb6e6 100644 --- a/Engine/include/Engine/Vulkan/Swapchain.h +++ b/Engine/include/Engine/Vulkan/Swapchain.h @@ -61,7 +61,7 @@ namespace Engine vk::Format m_Format; vk::Extent2D m_Extent; - bool m_VSync = false; + bool m_VSync = true; }; } // namespace Engine diff --git a/Engine/src/RenderGraph/RenderGraph.cpp b/Engine/src/RenderGraph/RenderGraph.cpp index bf0a9df..4203c4d 100644 --- a/Engine/src/RenderGraph/RenderGraph.cpp +++ b/Engine/src/RenderGraph/RenderGraph.cpp @@ -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(pos); entry.lastPass = static_cast(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) diff --git a/Engine/src/Renderer/Passes/FullscreenGpu.cpp b/Engine/src/Renderer/Passes/FullscreenGpu.cpp index 4bae26f..bdf8f11 100644 --- a/Engine/src/Renderer/Passes/FullscreenGpu.cpp +++ b/Engine/src/Renderer/Passes/FullscreenGpu.cpp @@ -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 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{setLayout}, static_cast(sizeof(FullscreenPush))); + auto diffF = shaders.CreateFromFile( + shaderDir + "temporal_diff.frag.spv", + vk::ShaderStageFlagBits::eFragment, + std::vector{setLayout}, + static_cast(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); diff --git a/Engine/src/Renderer/Passes/TemporalDiffPass.cpp b/Engine/src/Renderer/Passes/TemporalDiffPass.cpp new file mode 100644 index 0000000..d3750ac --- /dev/null +++ b/Engine/src/Renderer/Passes/TemporalDiffPass.cpp @@ -0,0 +1,49 @@ +// +// Created by atdunbg on 2026/9/23. +// + +#include +#include + +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 diff --git a/Engine/src/Renderer/Passes/TonemapPass.cpp b/Engine/src/Renderer/Passes/TonemapPass.cpp index dcccf43..8356064 100644 --- a/Engine/src/Renderer/Passes/TonemapPass.cpp +++ b/Engine/src/Renderer/Passes/TonemapPass.cpp @@ -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); } } diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 4cb12d1..82211c3 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -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( *m_SceneGpu, frame, m_HDRColor, m_Depth, m_ShadowMap)); + m_Passes.push_back(CreateScope( + *m_FullscreenGpu, /*gain*/ 8.0f, m_HDRColor, m_TemporalDiff)); m_Passes.push_back(CreateScope( *m_FullscreenGpu, *m_PostParams, m_HDRColor, m_BloomA, m_BloomB)); m_Passes.push_back(CreateScope( - *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( &m_ImGuiLayer, m_Backbuffer)); m_Passes.push_back(CreateScope(m_Backbuffer)); diff --git a/shaders/temporal_diff.frag b/shaders/temporal_diff.frag new file mode 100644 index 0000000..2433ccd --- /dev/null +++ b/shaders/temporal_diff.frag @@ -0,0 +1,28 @@ +#version 450 + +// TemporalDiff:帧历史链路调试视图。 +// 输出 |上一帧 HDR − 当前帧 HDR| × 增益: +// - 静止画面应接近纯黑(两帧内容一致) +// - 运动的实体边缘发光(时域差异可见) +// - 全屏噪声 / 错位残影 = 帧历史 barrier 或槽位镜像绑定错误 +// 这是 RenderGraph 帧历史资源(ReadHistory)的验证消费端。 + +layout(location = 0) in vec2 fragUV; +layout(location = 0) out vec4 outColor; + +layout(set = 0, binding = 0) uniform sampler2D prevImage; // 帧历史(上一帧槽位镜像) +layout(set = 0, binding = 1) uniform sampler2D currImage; // 当前帧 + +// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878) +layout(push_constant) uniform PushConstants { + float exposure; // 与其它全屏 pass 布局一致(本 pass 不用) + float gain; // 差异增益 + float param1; + float param2; +} pc; + +void main() { + vec3 prev = texture(prevImage, fragUV).rgb; + vec3 curr = texture(currImage, fragUV).rgb; + outColor = vec4(abs(prev - curr) * pc.gain, 1.0); +} diff --git a/shaders/tonemap.frag b/shaders/tonemap.frag index 8e98a3c..0b95f22 100644 --- a/shaders/tonemap.frag +++ b/shaders/tonemap.frag @@ -10,13 +10,14 @@ layout(location = 0) out vec4 outColor; layout(set = 0, binding = 0) uniform sampler2D hdrImage; layout(set = 0, binding = 1) uniform sampler2D bloomImage; +layout(set = 0, binding = 2) uniform sampler2D temporalDiffImage; // 帧历史差异(调试视图) // 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878) layout(push_constant) uniform PushConstants { float exposure; // 曝光系数 float aces; // >0.5 启用 ACES 曲线,否则纯 gamma float bloomStrength; // bloom 叠加强度(0 = 关闭) - float _pad0; + float debugView; // >0.5 显示帧历史差异视图(TemporalDiff) } pc; vec3 acesFilm(vec3 x) { @@ -24,6 +25,13 @@ vec3 acesFilm(vec3 x) { } void main() { + // 调试视图:帧历史差异(静止=黑,运动边缘发光) + if (pc.debugView > 0.5) { + vec3 d = clamp(texture(temporalDiffImage, fragUV).rgb, 0.0, 1.0); + outColor = vec4(pow(d, vec3(1.0 / 2.2)), 1.0); + return; + } + // bloom 在 HDR 域叠加(半分辨率模糊图线性采样上采样) vec4 hdrTex = texture(hdrImage, fragUV); vec3 hdr = hdrTex.rgb + texture(bloomImage, fragUV).rgb * pc.bloomStrength;