Crafter.Graphics/tests/UIFusedShader/main.cpp

187 lines
7.3 KiB
C++
Raw Normal View History

2026-07-22 18:09:06 +02:00
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
perf(ui): add additive DispatchFused to collapse consecutive UI passes (#47) Add a fifth UI compute path — DispatchFused — alongside the four per-element Dispatch* calls. It composites quads → circles → images → text (canonical back-to-front order) in ONE dispatch that loads the destination image once and stores once, eliminating the per-pass load+store and the inter-pass VkMemoryBarriers a run of consecutive Dispatch* calls would pay. The win materialises at >= 2 non-empty categories; an absent category is a zero-trip, push-constant-uniform loop (~free). The new uber-kernel (shaders/ui-fused.comp.glsl) reuses each category's exact cooperative shared-memory tile-cull + per-pixel accumulate, so a fused category is pixel-identical to its standalone pass. Categories run sequentially per thread and share one set of shared-memory scratch, so the VGPR/shared high-water mark stays at the per-category level, not the sum. Additive and non-breaking: the per-element Dispatch* calls and the frozen 48-byte UIDispatchHeader are untouched. DispatchFused gets its own 128-byte push-constant layout (UIFusedHeader: four uvec4 headers + four per-category clip vec4s). To interleave a custom ui.Dispatch() between standard passes, bracket it with two DispatchFused calls — the dispatch boundary is the explicit, app-declared flush point. WebGPU (Vulkan-second): DispatchFused falls back to the per-element Dispatch* calls in canonical order — one texture/sampler per dispatch there can't feed the bindless image phase — so the result matches; only the load/store fusion is Vulkan-only. HelloUI now composites its background quads, mouse circle, and label text in a single DispatchFused. Verified rendering identical on a live Vulkan/Wayland session. Resolves #47 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:03:24 +00:00
// Regression test for issue #47: the fused UI uber-kernel
// (shaders/ui-fused.comp.glsl) and its C++ push-constant mirror
// (Crafter::UIFusedHeader). The fused kernel is large and hand-written, and
// its push-constant block must stay byte-for-byte identical to UIFusedHeader
// or DispatchFused silently corrupts every fused frame. This test:
//
// 1. compiles the real ui-fused.comp.glsl with glslang (same target-env the
// engine builds it with), proving the uber-kernel still compiles and its
// GL_GOOGLE_include of ui-shared.glsl resolves;
// 2. runs spirv-val over the result under the engine's block-layout flags;
// 3. asserts exactly one push-constant variable survives (a well-formed
// single block, like every other UI shader);
// 4. parses the push-constant struct's member Offset decorations and asserts
// they are exactly {0,16,32,48,64,80,96,112} — the layout of
// UIFusedHeader (four uvec4 headers at 0/16/32/48 then four vec4 clip
// rects at 64/80/96/112, 128 bytes total). Any add/remove/reorder of a
// member shifts these offsets and fails here, catching a GLSL/C++ drift
// that the C++-side static_assert(sizeof==128) alone cannot.
//
// glslang and spirv-val are invoked at runtime, so the test declares them as
// required tools (see project.cpp). It needs no GPU device.
#include <cstdlib>
import Crafter.Graphics; // for the static_assert(sizeof(UIFusedHeader)==128)
import std;
using namespace Crafter;
namespace {
namespace fs = std::filesystem;
int RunCommand(const std::string& cmd) {
int status = std::system(cmd.c_str());
if (status == -1) return -1;
return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f);
}
std::vector<std::uint32_t> ReadSpirv(const fs::path& p) {
std::ifstream f(p, std::ios::binary | std::ios::ate);
if (!f) return {};
std::streamsize size = f.tellg();
f.seekg(0);
std::vector<std::uint32_t> words(static_cast<std::size_t>(size) / sizeof(std::uint32_t));
f.read(reinterpret_cast<char*>(words.data()), size);
return words;
}
// Count OpVariable instructions in the PushConstant storage class (SC == 9).
int CountPushConstantVariables(const std::vector<std::uint32_t>& words) {
constexpr std::uint32_t OpVariable = 59;
constexpr std::uint32_t StorageClassPushConstant = 9;
int count = 0;
for (std::size_t i = 5; i < words.size();) {
std::uint32_t len = words[i] >> 16;
if (len == 0 || i + len > words.size()) break;
if ((words[i] & 0xFFFFu) == OpVariable && len >= 4 && words[i + 3] == StorageClassPushConstant)
++count;
i += len;
}
return count;
}
// The single PushConstant struct's member byte offsets, sorted ascending.
// Finds the OpTypePointer in the PushConstant storage class (SC == 9), takes
// the struct type it points at, and collects every Offset (decoration 35)
// declared on that struct via OpMemberDecorate.
std::vector<std::uint32_t> PushConstantMemberOffsets(const std::vector<std::uint32_t>& words) {
constexpr std::uint32_t OpTypePointer = 32;
constexpr std::uint32_t OpMemberDecorate = 72;
constexpr std::uint32_t StorageClassPushConstant = 9;
constexpr std::uint32_t DecorationOffset = 35;
std::uint32_t structId = 0;
for (std::size_t i = 5; i < words.size();) {
std::uint32_t len = words[i] >> 16;
if (len == 0 || i + len > words.size()) break;
// OpTypePointer: [op][result id][storage class][pointee type id]
if ((words[i] & 0xFFFFu) == OpTypePointer && len >= 4 &&
words[i + 2] == StorageClassPushConstant) {
structId = words[i + 3];
break;
}
i += len;
}
if (structId == 0) return {};
std::vector<std::uint32_t> offsets;
for (std::size_t i = 5; i < words.size();) {
std::uint32_t len = words[i] >> 16;
if (len == 0 || i + len > words.size()) break;
// OpMemberDecorate: [op][struct id][member][decoration][operands...]
if ((words[i] & 0xFFFFu) == OpMemberDecorate && len >= 5 &&
words[i + 1] == structId && words[i + 3] == DecorationOffset) {
offsets.push_back(words[i + 4]);
}
i += len;
}
std::ranges::sort(offsets);
offsets.erase(std::ranges::unique(offsets).begin(), offsets.end());
return offsets;
}
// Locate shaders/ui-fused.comp.glsl whether the test runs from the build
// output dir or the project root (mirrors ShapeTextCache's font probing).
fs::path LocateShader() {
const std::array<fs::path, 4> candidates = {
"shaders/ui-fused.comp.glsl",
"../shaders/ui-fused.comp.glsl",
"../../shaders/ui-fused.comp.glsl",
"../../../shaders/ui-fused.comp.glsl",
};
for (const auto& c : candidates) {
std::error_code ec;
if (fs::exists(c, ec)) return c;
}
return {};
}
} // namespace
int main() {
// Sanity: the C++ mirror is the size the shader layout assumes.
static_assert(sizeof(UIFusedHeader) == 128);
const fs::path src = LocateShader();
if (src.empty()) {
std::println(std::cerr, "could not locate shaders/ui-fused.comp.glsl from {}",
fs::current_path().string());
return 1;
}
const fs::path dir = fs::temp_directory_path() / "crafter-uifused-test";
std::error_code ec;
fs::create_directories(dir, ec);
const fs::path spv = dir / "ui-fused.comp.spv";
// 1. Compile the real uber-kernel. GL_GOOGLE_include resolves
// ui-shared.glsl relative to the source directory.
std::string compile = "glslang --target-env vulkan1.4 -V -S comp \""
+ src.string() + "\" -o \"" + spv.string() + "\" > /dev/null";
if (RunCommand(compile) != 0) {
std::println(std::cerr, "glslang failed to compile ui-fused.comp.glsl");
return 1;
}
std::vector<std::uint32_t> words = ReadSpirv(spv);
if (words.size() < 5) {
std::println(std::cerr, "could not read compiled SPIR-V");
return 1;
}
// 2. spirv-val under the engine's relaxed block-layout flags.
std::string validate = "spirv-val \"" + spv.string()
+ "\" --relax-block-layout --scalar-block-layout --target-env vulkan1.4";
if (RunCommand(validate) != 0) {
std::println(std::cerr, "spirv-val rejected ui-fused.comp.spv");
return 1;
}
// 3. Exactly one push-constant block.
int pcVars = CountPushConstantVariables(words);
if (pcVars != 1) {
std::println(std::cerr, "expected exactly 1 push-constant variable, found {}", pcVars);
return 1;
}
// 4. Member offsets must match UIFusedHeader's layout exactly.
const std::vector<std::uint32_t> expected = {0, 16, 32, 48, 64, 80, 96, 112};
std::vector<std::uint32_t> offsets = PushConstantMemberOffsets(words);
if (offsets != expected) {
std::print(std::cerr, "push-constant member offsets mismatch — expected {{");
for (auto o : expected) std::print(std::cerr, "{} ", o);
std::print(std::cerr, "}}, got {{");
for (auto o : offsets) std::print(std::cerr, "{} ", o);
std::println(std::cerr, "}}");
return 1;
}
std::println(std::cout,
"ui-fused shader ok (push-constant vars: {}, member offsets pinned to UIFusedHeader, 128 bytes)",
pcVars);
return 0;
}