/* 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 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; }