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:
catbot 2026-06-16 17:03:24 +00:00
commit 574242dd30
8 changed files with 720 additions and 8 deletions

View file

@ -204,6 +204,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.shaders.emplace_back(fs::path("shaders/ui-circles.comp.glsl"), std::string("main"), ShaderType::Compute);
cfg.shaders.emplace_back(fs::path("shaders/ui-images.comp.glsl"), std::string("main"), ShaderType::Compute);
cfg.shaders.emplace_back(fs::path("shaders/ui-text.comp.glsl"), std::string("main"), ShaderType::Compute);
cfg.shaders.emplace_back(fs::path("shaders/ui-fused.comp.glsl"), std::string("main"), ShaderType::Compute);
cfg.buildFiles.emplace_back(fs::path("shaders/ui-shared.glsl"));
// Regression test for issue #18: drive the NVIDIA descriptor-heap
@ -413,6 +414,35 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
memImpls.emplace_back("tests/MemoryTypeFallback/main");
mc.GetInterfacesAndImplementations(ifaces, memImpls);
cfg.tests.push_back(std::move(memTest));
// Issue #47: the fused UI uber-kernel (shaders/ui-fused.comp.glsl) and
// its C++ push-constant mirror UIFusedHeader. Compiles the real shader
// with glslang, validates with spirv-val, and pins the push-constant
// member offsets to UIFusedHeader's layout so a GLSL/C++ drift can't
// slip through (the C++ static_assert only guards the C++ side). No GPU
// device at runtime, but glslang + spirv-val are required tools.
Test fusedTest;
Configuration& fc = fusedTest.config;
fc.path = cfg.path;
fc.name = "UIFusedShader";
fc.outputName = "UIFusedShader";
fc.type = ConfigurationType::Executable;
fc.target = cfg.target;
fc.march = cfg.march;
fc.mtune = cfg.mtune;
fc.debug = cfg.debug;
fc.sysroot = cfg.sysroot;
fc.dependencies = cfg.dependencies;
fc.externalDependencies = cfg.externalDependencies;
fc.compileFlags = cfg.compileFlags;
fc.linkFlags = cfg.linkFlags;
fc.defines = cfg.defines;
fc.cFiles = cfg.cFiles;
std::vector<fs::path> fusedImpls(impls.begin(), impls.end());
fusedImpls.emplace_back("tests/UIFusedShader/main");
fc.GetInterfacesAndImplementations(ifaces, fusedImpls);
fusedTest.requires_ = { "tool:glslang", "tool:spirv-val" };
cfg.tests.push_back(std::move(fusedTest));
}
return cfg;