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
131
tests/SetupCmdBufferReuse/main.cpp
Normal file
131
tests/SetupCmdBufferReuse/main.cpp
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
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 #153: two distinct validation errors the frame loop emitted, both
|
||||
// reproduced here by driving the *real* loop against a live compositor and
|
||||
// asserting the Vulkan validation layer stays silent — same harness idea as
|
||||
// FrameLoopSync (the silent layer is the load-bearing check).
|
||||
//
|
||||
// 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.
|
||||
// FrameLoopSync runs with NO passes, where the union falls back to the
|
||||
// conservative writer union (which DOES include TRANSFER) and the VUID never
|
||||
// fires — so it cannot catch this. The key here is a real compute pass that
|
||||
// narrows SwapchainStage() to COMPUTE_SHADER, exactly like UIRenderer.
|
||||
//
|
||||
// Problem 2 — mid-session StartInit/FinishInit reuse the shared
|
||||
// drawCmdBuffers[currentBuffer]. With no steady-state wait-idle the loop's
|
||||
// last submission of that buffer is still in flight when setup runs, so the
|
||||
// old code re-began it (VUID-vkBeginCommandBuffer-commandBuffer-00049) and
|
||||
// re-submitted it (VUID-vkQueueSubmit-pCommandBuffers-00071) while pending,
|
||||
// and any resource freed in the StartInit..FinishInit bracket could still be
|
||||
// referenced by that submission. StartInit/GetCmd now drain the queue first.
|
||||
//
|
||||
// Needs a live Wayland compositor + a Vulkan device at runtime (same as the
|
||||
// windowed examples / FrameLoopSync), so it shares the native build settings
|
||||
// and is Linux-only.
|
||||
|
||||
#include "vulkan/vulkan.h"
|
||||
#include <cstdlib>
|
||||
|
||||
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 minimal compute pass: records nothing, but narrows its swapchain write
|
||||
// stage to COMPUTE_SHADER like every real menu/game pass. That narrowing is
|
||||
// what shrinks the frame's stage union to COMPUTE_SHADER and so exposes the
|
||||
// problem-1 acquire-barrier access/stage mismatch. Record() needs no work —
|
||||
// the bug is in the frame-edge barriers the loop records around the passes,
|
||||
// not in the pass body.
|
||||
struct ComputePass : RenderPass {
|
||||
void Record(GraphicsCommandBuffer, std::uint32_t, Window&) override {}
|
||||
VkPipelineStageFlags SwapchainStage() const override {
|
||||
return VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
Device::Initialize();
|
||||
|
||||
Window window(640, 480, "SetupCmdBufferReuse test");
|
||||
|
||||
ComputePass pass;
|
||||
window.passes.push_back(&pass);
|
||||
|
||||
// ── Problem 1: all-compute frames ───────────────────────────────────────
|
||||
// Run enough frames that the acquire barrier is recorded many times with
|
||||
// swapWriterStages == COMPUTE_SHADER. Pre-fix this logged VUID-02820 once
|
||||
// per frame.
|
||||
constexpr int kWarmupFrames = 8;
|
||||
for (int i = 0; i < kWarmupFrames; ++i) window.Render();
|
||||
|
||||
Check(Device::validationErrorCount == 0,
|
||||
std::format("no validation errors over {} all-compute frames "
|
||||
"(acquire-barrier access/stage match) ({} seen)",
|
||||
kWarmupFrames, Device::validationErrorCount));
|
||||
|
||||
// ── Problem 2: mid-session setup while a frame is in flight ──────────────
|
||||
// After Render() returns there is no steady-state wait-idle, so the last
|
||||
// submission of drawCmdBuffers[currentBuffer] is still pending. Calling
|
||||
// StartInit()/FinishInit() now (as a scene transition would, to build map
|
||||
// meshes / acceleration structures) reuses that exact command buffer. The
|
||||
// setup buffer is left empty on purpose: it's the begin/submit lifecycle
|
||||
// around the in-flight buffer that the old code got wrong, not the
|
||||
// contents. Interleave more frames so each StartInit again races a fresh
|
||||
// in-flight submission.
|
||||
constexpr int kSetupRounds = 3;
|
||||
for (int r = 0; r < kSetupRounds; ++r) {
|
||||
VkCommandBuffer cmd = window.StartInit();
|
||||
Check(cmd != VK_NULL_HANDLE, "StartInit returns a usable command buffer");
|
||||
window.FinishInit();
|
||||
for (int f = 0; f < 4; ++f) window.Render();
|
||||
}
|
||||
|
||||
Check(Device::validationErrorCount == 0,
|
||||
std::format("no validation errors across {} mid-session StartInit/"
|
||||
"FinishInit rounds interleaved with rendering ({} seen)",
|
||||
kSetupRounds, Device::validationErrorCount));
|
||||
|
||||
// Drain before teardown — no steady-state wait-idle means GPU work may
|
||||
// still be in flight when the loop exits.
|
||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||
|
||||
if (failures != 0) {
|
||||
std::println("{} check(s) failed", failures);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::println("all checks passed");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
|
@ -165,6 +165,45 @@ int main() {
|
|||
"inter-pass barrier is scoped to the single swapchain subresource");
|
||||
}
|
||||
|
||||
// ─── SwapchainWriterAccess (issue #153) ─────────────────────────────────
|
||||
// The frame-edge (acquire dst / present src) barriers must set an access
|
||||
// mask supported by their accompanying stage mask
|
||||
// (VUID-vkCmdPipelineBarrier-pImageMemoryBarriers-02820). The old code
|
||||
// hardcoded SHADER_WRITE|TRANSFER_WRITE on a barrier whose stage mask is the
|
||||
// per-pass union, so an all-compute frame carried TRANSFER_WRITE into a
|
||||
// COMPUTE_SHADER dst stage and fired the VUID every frame. The fix derives
|
||||
// the access from the same stage union, so each stage only pulls in the
|
||||
// access flags it actually supports.
|
||||
{
|
||||
// All-compute frame: COMPUTE_SHADER supports SHADER_WRITE only — and
|
||||
// crucially NOT TRANSFER_WRITE (the bit that fired the VUID).
|
||||
VkAccessFlags compute = SwapchainWriterAccess(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||
Check(compute == VK_ACCESS_SHADER_WRITE_BIT,
|
||||
"compute-only writer access is SHADER_WRITE only");
|
||||
Check((compute & VK_ACCESS_TRANSFER_WRITE_BIT) == 0,
|
||||
"compute-only writer access does NOT include TRANSFER_WRITE (the VUID-02820 trap)");
|
||||
|
||||
// RT frame: RAY_TRACING_SHADER also writes via SHADER_WRITE, no transfer.
|
||||
Check(SwapchainWriterAccess(VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) == VK_ACCESS_SHADER_WRITE_BIT,
|
||||
"RT-only writer access is SHADER_WRITE only");
|
||||
|
||||
// A transfer-stage writer pulls in TRANSFER_WRITE.
|
||||
Check(SwapchainWriterAccess(VK_PIPELINE_STAGE_TRANSFER_BIT) == VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
"transfer-only writer access is TRANSFER_WRITE only");
|
||||
|
||||
// The conservative writer union (un-overridden / empty frame) folds in
|
||||
// both — and every bit it sets is supported by some stage in the union.
|
||||
Check(SwapchainWriterAccess(kSwapchainWriterStages)
|
||||
== (VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT),
|
||||
"conservative writer union access is SHADER_WRITE | TRANSFER_WRITE");
|
||||
|
||||
// Mixed compute+RT (no transfer): still SHADER_WRITE only, no transfer.
|
||||
VkAccessFlags mixed = SwapchainWriterAccess(
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR);
|
||||
Check(mixed == VK_ACCESS_SHADER_WRITE_BIT,
|
||||
"compute+RT writer access is SHADER_WRITE only (no spurious TRANSFER_WRITE)");
|
||||
}
|
||||
|
||||
if (failures != 0) {
|
||||
std::println("{} check(s) failed", failures);
|
||||
return EXIT_FAILURE;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue