Two distinct validation errors the native frame loop emitted, both originating in Crafter.Graphics with no consumer-side influence. Problem 1 — per-frame acquire-barrier access/stage mismatch. The acquire->GENERAL barrier hardcoded dstAccessMask = SHADER_WRITE|TRANSFER_WRITE but used the per-pass stage union as its dst stage mask. For an all-compute frame the union narrows to COMPUTE_SHADER, which does not support TRANSFER_WRITE, so VUID-02820 fired every frame. Derive the access mask from the same stage union via a new SwapchainWriterAccess() helper (mirroring SwapchainStageUnion), and apply it to both the acquire dst and present src masks for symmetry. Problem 2 — mid-session StartInit/FinishInit (and GetCmd/EndCmd) reuse the shared drawCmdBuffers[currentBuffer]. With no steady-state wait-idle the loop's last submission of that buffer may still be in flight when scene setup runs (building map meshes / acceleration structures), so the old code re-began (VUID-00049) and re-submitted (VUID-00071) a pending buffer, and resources freed in the StartInit..FinishInit bracket could still be referenced by it. Drain the queue at the start of StartInit/GetCmd before re-recording; setup is rare, so a wait-idle is fine (FinishInit/EndCmd already wait-idle at the end). Tests: extend SwapchainBarrierScope with SwapchainWriterAccess coverage (pure CPU), and add SetupCmdBufferReuse — a real-frame-loop regression test driving a compute pass plus interleaved mid-session StartInit rounds, asserting the validation layer stays silent. Verified both halves fail (reproducing the exact VUIDs) when their respective fix is reverted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
6 KiB
C++
120 lines
6 KiB
C++
/*
|
|
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
|
|
*/
|
|
module;
|
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
|
#include "vulkan/vulkan.h"
|
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
|
export module Crafter.Graphics:RenderPass;
|
|
import std;
|
|
import :GraphicsTypes;
|
|
|
|
export namespace Crafter {
|
|
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 {
|
|
virtual void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
|
|
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 access mask matching a swapchain-writer stage union, for the
|
|
// frame-edge barriers (acquire dst / present src). A barrier's access mask
|
|
// must only contain access flags supported by its accompanying stage mask
|
|
// (VUID-vkCmdPipelineBarrier-pImageMemoryBarriers-02820): TRANSFER_WRITE is
|
|
// not a valid access for COMPUTE/RAY_TRACING stages, so hardcoding both
|
|
// SHADER_WRITE and TRANSFER_WRITE fires the VUID every frame on an
|
|
// all-compute frame. The swapchain image is written as a storage image by
|
|
// compute/RT passes (SHADER_WRITE) and only via TRANSFER_WRITE when a
|
|
// transfer-stage pass is present, so derive the access bits from the same
|
|
// per-pass stage union the barrier's stage mask uses instead of hardcoding.
|
|
inline VkAccessFlags SwapchainWriterAccess(VkPipelineStageFlags stages) {
|
|
VkAccessFlags access = 0;
|
|
if (stages & (VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
|
|
| VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR))
|
|
access |= VK_ACCESS_SHADER_WRITE_BIT;
|
|
if (stages & VK_PIPELINE_STAGE_TRANSFER_BIT)
|
|
access |= VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
return access;
|
|
}
|
|
|
|
// 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
|
|
}
|