115 lines
5.1 KiB
C++
115 lines
5.1 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// 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;
|
|
}
|