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>
This commit is contained in:
parent
c58b68ec40
commit
574242dd30
8 changed files with 720 additions and 8 deletions
203
tests/UIFusedShader/main.cpp
Normal file
203
tests/UIFusedShader/main.cpp
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
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
|
||||
*/
|
||||
|
||||
// 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue