fix(window): silence per-frame and setup-path Vulkan validation errors (#153)
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>
This commit is contained in:
parent
7720f0a9bc
commit
7316e51dca
5 changed files with 265 additions and 4 deletions
|
|
@ -482,12 +482,14 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
|||
.layerCount = VK_REMAINING_ARRAY_LAYERS,
|
||||
};
|
||||
|
||||
// Transition into VK_IMAGE_LAYOUT_GENERAL before recording passes. image
|
||||
// and oldLayout are patched per frame (oldLayout depends on first use).
|
||||
// Transition into VK_IMAGE_LAYOUT_GENERAL before recording passes. image,
|
||||
// oldLayout (depends on first use) and dstAccessMask (derived from the
|
||||
// per-pass stage union) are patched per frame in Render(); the dstAccessMask
|
||||
// here is just a well-formed default.
|
||||
acquireBarrier = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||
.srcAccessMask = 0,
|
||||
.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
|
||||
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
|
|
@ -496,7 +498,9 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
|||
.subresourceRange = range,
|
||||
};
|
||||
|
||||
// Transition back to PRESENT_SRC_KHR after the passes. Only image varies.
|
||||
// Transition back to PRESENT_SRC_KHR after the passes. image and
|
||||
// srcAccessMask (derived from the per-pass stage union) are patched per
|
||||
// frame in Render(); the srcAccessMask here is just a well-formed default.
|
||||
presentBarrier = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
|
||||
|
|
@ -835,6 +839,13 @@ void Window::Render() {
|
|||
// barrier below (passes don't change within a Render()).
|
||||
const VkPipelineStageFlags swapWriterStages = SwapchainStageUnion(passes);
|
||||
|
||||
// dstAccessMask must only contain access flags supported by the dst stage
|
||||
// mask (VUID-vkCmdPipelineBarrier-pImageMemoryBarriers-02820). Derive it
|
||||
// from the same per-pass stage union the barrier uses as its dst stage mask
|
||||
// instead of hardcoding SHADER_WRITE|TRANSFER_WRITE, which would carry
|
||||
// TRANSFER_WRITE into a compute-only frame's COMPUTE_SHADER dst stage.
|
||||
acquireBarrier.dstAccessMask = SwapchainWriterAccess(swapWriterStages);
|
||||
|
||||
// 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);
|
||||
|
|
@ -919,6 +930,12 @@ void Window::Render() {
|
|||
|
||||
presentBarrier.image = images[currentBuffer];
|
||||
|
||||
// Mirror of the acquire barrier: srcAccessMask must be supported by the src
|
||||
// stage mask (swapWriterStages). SHADER_WRITE alone is valid for COMPUTE/RT
|
||||
// and so never fires today, but a transfer-stage writer would need
|
||||
// TRANSFER_WRITE here too — derive both from the same union for symmetry.
|
||||
presentBarrier.srcAccessMask = SwapchainWriterAccess(swapWriterStages);
|
||||
|
||||
// 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.
|
||||
|
|
@ -1107,6 +1124,20 @@ void Window::CreateSwapchain()
|
|||
VkCommandBuffer Window::StartInit() {
|
||||
VkCommandBufferBeginInfo cmdBufInfo {};
|
||||
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
// StartInit reuses drawCmdBuffers[currentBuffer] — the same buffer the
|
||||
// steady-state Render() loop submits, guarded by waitFences[currentBuffer].
|
||||
// When setup runs mid-session (e.g. building map meshes / acceleration
|
||||
// structures on a scene transition) the loop's last submission of this
|
||||
// buffer may still be in flight: re-beginning it would trip
|
||||
// VUID-vkBeginCommandBuffer-commandBuffer-00049, re-submitting it
|
||||
// VUID-vkQueueSubmit-pCommandBuffers-00071, and any resource the caller
|
||||
// destroys between here and FinishInit could still be referenced by that
|
||||
// pending submission (vkDestroyBuffer-in-use). Drain the queue first so the
|
||||
// buffer is free to re-record and no in-flight frame still references the
|
||||
// resources this setup is about to replace (issue #153). Setup is rare
|
||||
// (not the per-frame path), so a full wait-idle here is fine — FinishInit
|
||||
// already wait-idles at the end.
|
||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo));
|
||||
|
||||
// The swapchain images are deliberately NOT transitioned here. They are
|
||||
|
|
@ -1132,6 +1163,11 @@ void Window::FinishInit() {
|
|||
VkCommandBuffer Window::GetCmd() {
|
||||
VkCommandBufferBeginInfo cmdBufInfo {};
|
||||
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
// Same lifecycle hazard as StartInit: GetCmd re-begins and EndCmd
|
||||
// re-submits the shared drawCmdBuffers[currentBuffer], so drain any
|
||||
// in-flight submission of it (and free the resources it references) before
|
||||
// re-recording (issue #153).
|
||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo));
|
||||
|
||||
VkImageSubresourceRange range{};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue