perf(sync): scope frame-loop barriers to swapchain image + real per-pass stages (#115)
The inter-pass and acquire/present barriers in the frame loop set both stage masks to ALL_COMMANDS, and the inter-pass dependency used a queue-wide VkMemoryBarrier — fully serialising against every pipeline stage and flushing all caches every frame, when all the next pass needs is the swapchain image the previous one wrote. Replace the inter-pass global VkMemoryBarrier with an image memory barrier scoped to the swapchain image's single colour subresource (as the intra-pass UI barrier already does), and derive the barrier stage masks per pass: RenderPass::SwapchainStage() is overridden by UIRenderer (COMPUTE_SHADER) and RTPass (RAY_TRACING_SHADER), so a compute->compute edge only serialises COMPUTE while an RT pass pulls in RAY_TRACING — the acquire/present frame-edge masks use the real union of the frame's passes (SwapchainStageUnion). The base default and the empty-passes fallback are the conservative COMPUTE | RAY_TRACING | TRANSFER union, so a polymorphic or un-overridden pass can only be over- not under-synchronised. Adds SwapchainBarrierScope (pure CPU) pinning the per-pass derivation, the union narrowing, and the inter-pass barrier scope; FrameLoopSync already drives the real GPU frame loop with validation enabled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9d9f9d9d2c
commit
9414863cff
6 changed files with 316 additions and 8 deletions
|
|
@ -815,7 +815,16 @@ void Window::Render() {
|
||||||
: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||||
acquireBarrier.image = images[currentBuffer];
|
acquireBarrier.image = images[currentBuffer];
|
||||||
|
|
||||||
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &acquireBarrier);
|
// Real per-pass stage union for this frame's passes — the swapchain image
|
||||||
|
// is only ever written by compute/RT (and possibly transfer), never by the
|
||||||
|
// whole pipeline, so the frame-edge barriers wait on exactly those stages
|
||||||
|
// instead of ALL_COMMANDS. Derived once here and reused for the present
|
||||||
|
// barrier below (passes don't change within a Render()).
|
||||||
|
const VkPipelineStageFlags swapWriterStages = SwapchainStageUnion(passes);
|
||||||
|
|
||||||
|
// The acquire->GENERAL transition only needs to be visible before the first
|
||||||
|
// pass writes the image, i.e. at the writer stages — not ALL_COMMANDS.
|
||||||
|
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, swapWriterStages, 0, 0, nullptr, 0, nullptr, 1, &acquireBarrier);
|
||||||
|
|
||||||
// Synthesise key-repeat events before listeners run, so the focused
|
// Synthesise key-repeat events before listeners run, so the focused
|
||||||
// widget's OnTextInput / OnKeyDown sees them in the same frame.
|
// widget's OnTextInput / OnKeyDown sees them in the same frame.
|
||||||
|
|
@ -879,18 +888,28 @@ void Window::Render() {
|
||||||
passes[i]->Record(drawCmdBuffers[currentBuffer], currentBuffer, *this);
|
passes[i]->Record(drawCmdBuffers[currentBuffer], currentBuffer, *this);
|
||||||
|
|
||||||
if (i + 1 < passes.size()) {
|
if (i + 1 < passes.size()) {
|
||||||
VkMemoryBarrier mb {
|
// Make pass i's writes to the swapchain image visible to pass i+1.
|
||||||
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
|
// The only resource the inter-pass hazard touches is that one image
|
||||||
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
|
// (later passes overdraw earlier ones), so use an image memory
|
||||||
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
// barrier scoped to its subresource instead of a queue-wide
|
||||||
};
|
// VkMemoryBarrier — only this image's caches round-trip, exactly as
|
||||||
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &mb, 0, nullptr, 0, nullptr);
|
// the intra-pass UI barrier already does. The stage masks come from
|
||||||
|
// each pass's own SwapchainStage(), so a compute->compute edge only
|
||||||
|
// serialises COMPUTE_SHADER while an RT pass on either side pulls in
|
||||||
|
// RAY_TRACING_SHADER — never the ALL_COMMANDS full-pipeline stall.
|
||||||
|
VkImageMemoryBarrier ib = BuildSwapchainInterPassBarrier(images[currentBuffer]);
|
||||||
|
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer],
|
||||||
|
passes[i]->SwapchainStage(), passes[i + 1]->SwapchainStage(),
|
||||||
|
0, 0, nullptr, 0, nullptr, 1, &ib);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
presentBarrier.image = images[currentBuffer];
|
presentBarrier.image = images[currentBuffer];
|
||||||
|
|
||||||
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &presentBarrier);
|
// The ->PRESENT_SRC transition only needs to wait on the stages that
|
||||||
|
// actually wrote the image (the per-pass union), not ALL_COMMANDS; present
|
||||||
|
// itself is gated by the render-finished semaphore, so dst stays BOTTOM.
|
||||||
|
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], swapWriterStages, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &presentBarrier);
|
||||||
|
|
||||||
Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));
|
Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ export namespace Crafter {
|
||||||
|
|
||||||
RTPass(PipelineRTVulkan* p) : pipeline(p) {}
|
RTPass(PipelineRTVulkan* p) : pipeline(p) {}
|
||||||
|
|
||||||
|
// An RT pass writes the swapchain image from the ray-tracing pipeline,
|
||||||
|
// so the frame loop's barriers must wait on RAY_TRACING_SHADER — using
|
||||||
|
// the compute stage here would under-synchronise and corrupt the image.
|
||||||
|
VkPipelineStageFlags SwapchainStage() const override {
|
||||||
|
return VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR;
|
||||||
|
}
|
||||||
|
|
||||||
void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override {
|
void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override {
|
||||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, pipeline->pipeline);
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, pipeline->pipeline);
|
||||||
// NVIDIA descriptor-heap AS-read workaround (issue #15 / #7): feed
|
// NVIDIA descriptor-heap AS-read workaround (issue #15 / #7): feed
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ You should have received a copy of the GNU Lesser General Public
|
||||||
License along with this library; if not, write to the Free Software
|
License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
module;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
#include "vulkan/vulkan.h"
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
export module Crafter.Graphics:RenderPass;
|
export module Crafter.Graphics:RenderPass;
|
||||||
import std;
|
import std;
|
||||||
import :GraphicsTypes;
|
import :GraphicsTypes;
|
||||||
|
|
@ -23,8 +27,74 @@ import :GraphicsTypes;
|
||||||
export namespace Crafter {
|
export namespace Crafter {
|
||||||
struct Window;
|
struct Window;
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
// Conservative union of every pipeline stage that can write the swapchain
|
||||||
|
// storage image: a compute pass via COMPUTE_SHADER, a ray-tracing pass via
|
||||||
|
// RAY_TRACING_SHADER, plus TRANSFER for any blit/copy writer. Used as the
|
||||||
|
// RenderPass default (a pass that doesn't narrow its own stage) and as the
|
||||||
|
// fallback when there are no passes to derive a real union from. Far tighter
|
||||||
|
// than ALL_COMMANDS, but never under-synchronises a polymorphic pass.
|
||||||
|
inline constexpr VkPipelineStageFlags kSwapchainWriterStages =
|
||||||
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
|
||||||
|
| VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR
|
||||||
|
| VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
||||||
struct RenderPass {
|
struct RenderPass {
|
||||||
virtual void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
|
virtual void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
|
||||||
virtual ~RenderPass() = default;
|
virtual ~RenderPass() = default;
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
// Pipeline stage at which this pass reads and writes the swapchain
|
||||||
|
// storage image. The frame loop uses this to scope the inter-pass and
|
||||||
|
// frame-edge barriers to the stages that actually touch the image
|
||||||
|
// instead of ALL_COMMANDS. A subclass MUST narrow this to its real
|
||||||
|
// stage (RTPass -> RAY_TRACING_SHADER, a compute pass -> COMPUTE_SHADER):
|
||||||
|
// the default is the conservative writer union so an un-overridden pass
|
||||||
|
// can never be under-synchronised, only over-synchronised.
|
||||||
|
virtual VkPipelineStageFlags SwapchainStage() const { return kSwapchainWriterStages; }
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
// Union of the swapchain-access stages declared by `passes` — the "real"
|
||||||
|
// per-pass stage union the frame-edge barriers (acquire dst / present src)
|
||||||
|
// narrow to: an all-compute frame yields COMPUTE_SHADER only, a frame with
|
||||||
|
// any RT pass folds in RAY_TRACING_SHADER, and so on. Falls back to the
|
||||||
|
// conservative writer union when there are no passes (the image is still
|
||||||
|
// transitioned, just never written), so the barrier is always well-formed.
|
||||||
|
inline VkPipelineStageFlags SwapchainStageUnion(std::span<RenderPass* const> passes) {
|
||||||
|
if (passes.empty()) return kSwapchainWriterStages;
|
||||||
|
VkPipelineStageFlags stages = 0;
|
||||||
|
for (RenderPass* pass : passes) stages |= pass->SwapchainStage();
|
||||||
|
return stages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The inter-pass swapchain barrier (replacing the old queue-wide
|
||||||
|
// VkMemoryBarrier at the #115 location). Scoped to the swapchain image's
|
||||||
|
// single colour subresource so only that image's shader caches round-trip —
|
||||||
|
// unrelated buffers/images stay resident — exactly as the intra-pass UI
|
||||||
|
// barrier already does. The image is bound as a storage image and stays in
|
||||||
|
// VK_IMAGE_LAYOUT_GENERAL, so this is a pure memory dependency (no layout
|
||||||
|
// transition). The execution scope (stage masks) is supplied per pass pair
|
||||||
|
// by the caller via vkCmdPipelineBarrier.
|
||||||
|
inline VkImageMemoryBarrier BuildSwapchainInterPassBarrier(VkImage image) {
|
||||||
|
return {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||||
|
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
|
||||||
|
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||||
|
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||||
|
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||||
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||||
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||||
|
.image = image,
|
||||||
|
.subresourceRange = {
|
||||||
|
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||||
|
.baseMipLevel = 0,
|
||||||
|
.levelCount = 1,
|
||||||
|
.baseArrayLayer = 0,
|
||||||
|
.layerCount = 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,15 @@ export namespace Crafter {
|
||||||
|
|
||||||
void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override;
|
void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override;
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
// A UI pass draws into the swapchain image entirely from compute
|
||||||
|
// shaders (see Dispatch), so the frame loop's inter-pass / frame-edge
|
||||||
|
// barriers only need to synchronise the COMPUTE_SHADER stage for it.
|
||||||
|
VkPipelineStageFlags SwapchainStage() const override {
|
||||||
|
return VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||||
|
}
|
||||||
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
||||||
UIDispatchHeader FillHeader(std::uint32_t itemBufferSlot,
|
UIDispatchHeader FillHeader(std::uint32_t itemBufferSlot,
|
||||||
std::uint32_t itemCount,
|
std::uint32_t itemCount,
|
||||||
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f},
|
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f},
|
||||||
|
|
|
||||||
29
project.cpp
29
project.cpp
|
|
@ -473,6 +473,35 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
mbc.GetInterfacesAndImplementations(ifaces, mipBarrierImpls);
|
mbc.GetInterfacesAndImplementations(ifaces, mipBarrierImpls);
|
||||||
cfg.tests.push_back(std::move(mipBarrierTest));
|
cfg.tests.push_back(std::move(mipBarrierTest));
|
||||||
|
|
||||||
|
// Issue #115: the frame loop's inter-pass and acquire/present barriers
|
||||||
|
// no longer use ALL_COMMANDS / a queue-wide VkMemoryBarrier. The stage
|
||||||
|
// masks are derived per pass via RenderPass::SwapchainStage() (compute
|
||||||
|
// vs ray-tracing) and unioned across the frame by SwapchainStageUnion,
|
||||||
|
// and the inter-pass dependency is scoped to the swapchain image by
|
||||||
|
// BuildSwapchainInterPassBarrier — all pure CPU logic over the pass
|
||||||
|
// list, so this test drives them directly with no GPU at runtime.
|
||||||
|
Test swapBarrierTest;
|
||||||
|
Configuration& sbc = swapBarrierTest.config;
|
||||||
|
sbc.path = cfg.path;
|
||||||
|
sbc.name = "SwapchainBarrierScope";
|
||||||
|
sbc.outputName = "SwapchainBarrierScope";
|
||||||
|
sbc.type = ConfigurationType::Executable;
|
||||||
|
sbc.target = cfg.target;
|
||||||
|
sbc.march = cfg.march;
|
||||||
|
sbc.mtune = cfg.mtune;
|
||||||
|
sbc.debug = cfg.debug;
|
||||||
|
sbc.sysroot = cfg.sysroot;
|
||||||
|
sbc.dependencies = cfg.dependencies;
|
||||||
|
sbc.externalDependencies = cfg.externalDependencies;
|
||||||
|
sbc.compileFlags = cfg.compileFlags;
|
||||||
|
sbc.linkFlags = cfg.linkFlags;
|
||||||
|
sbc.defines = cfg.defines;
|
||||||
|
sbc.cFiles = cfg.cFiles;
|
||||||
|
std::vector<fs::path> swapBarrierImpls(impls.begin(), impls.end());
|
||||||
|
swapBarrierImpls.emplace_back("tests/SwapchainBarrierScope/main");
|
||||||
|
sbc.GetInterfacesAndImplementations(ifaces, swapBarrierImpls);
|
||||||
|
cfg.tests.push_back(std::move(swapBarrierTest));
|
||||||
|
|
||||||
// Issue #47: the fused UI uber-kernel (shaders/ui-fused.comp.glsl) and
|
// Issue #47: the fused UI uber-kernel (shaders/ui-fused.comp.glsl) and
|
||||||
// its C++ push-constant mirror UIFusedHeader. Compiles the real shader
|
// its C++ push-constant mirror UIFusedHeader. Compiles the real shader
|
||||||
// with glslang, validates with spirv-val, and pins the push-constant
|
// with glslang, validates with spirv-val, and pins the push-constant
|
||||||
|
|
|
||||||
174
tests/SwapchainBarrierScope/main.cpp
Normal file
174
tests/SwapchainBarrierScope/main.cpp
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
Crafter®.Graphics
|
||||||
|
Copyright (C) 2026 Catcrafts®
|
||||||
|
catcrafts.net
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License version 3.0 as published by the Free Software Foundation;
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Issue #115: the frame loop's inter-pass and frame-edge (acquire/present)
|
||||||
|
// barriers used to set BOTH stage masks to VK_PIPELINE_STAGE_ALL_COMMANDS_BIT
|
||||||
|
// and the inter-pass dependency to a queue-wide VkMemoryBarrier — flushing
|
||||||
|
// every cache and fully serialising against the whole pipeline every frame,
|
||||||
|
// when all the next pass needs is the swapchain image the previous one wrote.
|
||||||
|
//
|
||||||
|
// The fix scopes the inter-pass dependency to the swapchain image's single
|
||||||
|
// colour subresource (BuildSwapchainInterPassBarrier) and narrows the stage
|
||||||
|
// masks to the stages that actually touch that image, derived per pass via the
|
||||||
|
// polymorphic RenderPass::SwapchainStage() and unioned across the frame by
|
||||||
|
// SwapchainStageUnion. The load-bearing correctness constraint is that this
|
||||||
|
// derivation is per-pass: a compute pass writes via COMPUTE_SHADER, a ray-
|
||||||
|
// tracing pass via RAY_TRACING_SHADER, so a hardcoded compute mask would
|
||||||
|
// UNDER-synchronise an RT pass and corrupt the image. All three helpers are
|
||||||
|
// pure CPU logic over the pass list, so this test drives them directly with no
|
||||||
|
// GPU device at runtime — mirroring MipChainBarrierBatch / UploadStrategy.
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include "vulkan/vulkan.h"
|
||||||
|
|
||||||
|
import Crafter.Graphics;
|
||||||
|
import std;
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
void Check(bool ok, std::string_view what) {
|
||||||
|
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
||||||
|
if (!ok) ++failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pass that never overrides SwapchainStage(): stands in for any future pass
|
||||||
|
// that forgets to narrow its stage, and must fall back to the conservative
|
||||||
|
// writer union rather than silently picking one stage (which could under-sync).
|
||||||
|
struct DefaultPass : RenderPass {
|
||||||
|
void Record(GraphicsCommandBuffer, std::uint32_t, Window&) override {}
|
||||||
|
};
|
||||||
|
|
||||||
|
VkImage SentinelImage() {
|
||||||
|
return reinterpret_cast<VkImage>(static_cast<std::uintptr_t>(0xC0FFEE));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// ─── per-pass SwapchainStage() polymorphism ─────────────────────────────
|
||||||
|
// The whole reason the fix is "moderate, not trivial": these two stages
|
||||||
|
// MUST differ, or scoping an RT pass's barrier to the compute stage would
|
||||||
|
// under-synchronise it. Pin that they are distinct and each correct.
|
||||||
|
Check(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT != VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR,
|
||||||
|
"compute and ray-tracing write stages are distinct (the under-sync trap)");
|
||||||
|
|
||||||
|
UIRenderer ui; // real compute UI pass
|
||||||
|
RTPass rt(nullptr); // real RT pass (SwapchainStage doesn't touch the pipeline)
|
||||||
|
DefaultPass def; // un-overridden fallback
|
||||||
|
|
||||||
|
// Access through the base pointer — the frame loop only ever sees RenderPass*.
|
||||||
|
RenderPass* uiBase = &ui;
|
||||||
|
RenderPass* rtBase = &rt;
|
||||||
|
RenderPass* defBase = &def;
|
||||||
|
|
||||||
|
Check(uiBase->SwapchainStage() == VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||||
|
"UIRenderer reports the COMPUTE_SHADER stage");
|
||||||
|
Check(rtBase->SwapchainStage() == VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR,
|
||||||
|
"RTPass reports the RAY_TRACING_SHADER stage");
|
||||||
|
Check(defBase->SwapchainStage() == kSwapchainWriterStages,
|
||||||
|
"an un-overridden pass falls back to the conservative writer union");
|
||||||
|
|
||||||
|
// ─── the conservative writer union constant ─────────────────────────────
|
||||||
|
constexpr VkPipelineStageFlags expectedUnion =
|
||||||
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
|
||||||
|
| VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR
|
||||||
|
| VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||||
|
Check(kSwapchainWriterStages == expectedUnion,
|
||||||
|
"writer union = COMPUTE_SHADER | RAY_TRACING_SHADER | TRANSFER");
|
||||||
|
// The point of the whole change: even the conservative fallback is far
|
||||||
|
// tighter than the ALL_COMMANDS mask it replaces.
|
||||||
|
Check(kSwapchainWriterStages != VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||||
|
"writer union is narrower than ALL_COMMANDS");
|
||||||
|
|
||||||
|
// ─── SwapchainStageUnion over the frame's pass list ─────────────────────
|
||||||
|
// Empty frame: no writers to derive from, so fall back to the conservative
|
||||||
|
// union (the image is still transitioned, just never written).
|
||||||
|
Check(SwapchainStageUnion(std::span<RenderPass* const>{}) == kSwapchainWriterStages,
|
||||||
|
"empty pass list falls back to the conservative writer union");
|
||||||
|
|
||||||
|
// All-compute frame: the union narrows to COMPUTE_SHADER ONLY — this is the
|
||||||
|
// real perf win, no RAY_TRACING / TRANSFER / ALL_COMMANDS dragged in.
|
||||||
|
{
|
||||||
|
std::array<RenderPass*, 2> computeOnly = {uiBase, uiBase};
|
||||||
|
VkPipelineStageFlags u = SwapchainStageUnion(computeOnly);
|
||||||
|
Check(u == VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||||
|
"all-compute frame narrows the union to COMPUTE_SHADER only");
|
||||||
|
Check((u & VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) == 0,
|
||||||
|
"all-compute frame does NOT pull in RAY_TRACING_SHADER");
|
||||||
|
}
|
||||||
|
|
||||||
|
// RT-only frame: narrows to RAY_TRACING_SHADER only.
|
||||||
|
{
|
||||||
|
std::array<RenderPass*, 1> rtOnly = {rtBase};
|
||||||
|
Check(SwapchainStageUnion(rtOnly) == VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR,
|
||||||
|
"RT-only frame narrows the union to RAY_TRACING_SHADER only");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mixed frame: the union folds in BOTH stages — never under-syncs the RT
|
||||||
|
// pass, never over-syncs to ALL_COMMANDS.
|
||||||
|
{
|
||||||
|
std::array<RenderPass*, 2> mixed = {uiBase, rtBase};
|
||||||
|
VkPipelineStageFlags u = SwapchainStageUnion(mixed);
|
||||||
|
Check(u == (VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR),
|
||||||
|
"mixed compute+RT frame unions COMPUTE_SHADER | RAY_TRACING_SHADER");
|
||||||
|
Check((u & VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) != 0,
|
||||||
|
"mixed frame keeps RAY_TRACING_SHADER (no RT under-sync)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A frame containing an un-overridden pass conservatively widens to the
|
||||||
|
// full writer union — safe by construction.
|
||||||
|
{
|
||||||
|
std::array<RenderPass*, 2> withDefault = {uiBase, defBase};
|
||||||
|
Check(SwapchainStageUnion(withDefault) == kSwapchainWriterStages,
|
||||||
|
"a frame with an un-overridden pass widens to the conservative union");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── BuildSwapchainInterPassBarrier ─────────────────────────────────────
|
||||||
|
// Replaces the queue-wide VkMemoryBarrier: an image memory barrier scoped
|
||||||
|
// to the swapchain image's single colour subresource, GENERAL->GENERAL (no
|
||||||
|
// layout change — the storage image stays bound), matching the intra-pass
|
||||||
|
// UI barrier's access scope.
|
||||||
|
{
|
||||||
|
VkImageMemoryBarrier b = BuildSwapchainInterPassBarrier(SentinelImage());
|
||||||
|
Check(b.sType == VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, "inter-pass barrier sType is IMAGE_MEMORY_BARRIER");
|
||||||
|
Check(b.image == SentinelImage(), "inter-pass barrier carries the swapchain image handle");
|
||||||
|
Check(b.oldLayout == VK_IMAGE_LAYOUT_GENERAL && b.newLayout == VK_IMAGE_LAYOUT_GENERAL,
|
||||||
|
"inter-pass barrier is GENERAL->GENERAL (pure memory dependency, no transition)");
|
||||||
|
Check(b.srcAccessMask == VK_ACCESS_SHADER_WRITE_BIT,
|
||||||
|
"inter-pass barrier src access is shader write");
|
||||||
|
Check(b.dstAccessMask == (VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT),
|
||||||
|
"inter-pass barrier dst access is shader read|write");
|
||||||
|
Check(b.srcQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED && b.dstQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED,
|
||||||
|
"inter-pass barrier is not a queue-family transfer");
|
||||||
|
Check(b.subresourceRange.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT,
|
||||||
|
"inter-pass barrier covers the colour aspect");
|
||||||
|
Check(b.subresourceRange.levelCount == 1 && b.subresourceRange.layerCount == 1,
|
||||||
|
"inter-pass barrier is scoped to the single swapchain subresource");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures != 0) {
|
||||||
|
std::println("{} check(s) failed", failures);
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
std::println("all checks passed");
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue