perf(ui): additive DispatchFused to collapse consecutive UI passes (#47) #93

Merged
catbot merged 3 commits from claude/issue-47 into master 2026-06-16 19:12:21 +02:00
Member

Summary

Adds a fifth UI compute path — DispatchFused — alongside the four per-element Dispatch* calls (issue #47). 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 (saves 1 load + 1 store + 1 barrier per extra type). An absent category is a zero-trip, push-constant-uniform loop (~free) — no per-combination kernels.
  • The new uber-kernel shaders/ui-fused.comp.glsl reuses each category's exact cooperative shared-memory tile-cull + per-pixel accumulate (co-designed with #46), so a fused category is pixel-identical to its standalone pass. Categories run sequentially per thread and share one set of shared scratch, so the VGPR/shared-memory high-water mark stays at the per-category level, not the sum.

Additive — frozen contract untouched

The per-element Dispatch* calls and the 48-byte UIDispatchHeader are unchanged. DispatchFused gets its own 128-byte push-constant layout (UIFusedHeader: four uvec4 headers + four per-category clip vec4s — exactly the guaranteed push-constant minimum, no padding holes). 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 (fusing across a custom dispatch is a memory round-trip, i.e. physics, not removed here).

WebGPU (Vulkan-second)

DispatchFused falls back to issuing 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.

Testing

  • New automated test UIFusedShader (crafter-build test): compiles the real ui-fused.comp.glsl with glslang, validates with spirv-val, asserts exactly one push-constant block, and pins the push-constant member offsets to {0,16,32,48,64,80,96,112} — guarding the GLSL↔C++ layout drift that the C++-side static_assert(sizeof(UIFusedHeader)==128) alone can't.
  • Full suite green: 8 passed (crafter-build test --jobs=1).
  • Live exercise: HelloUI now composites its background quads, mouse circle, and label text in a single DispatchFused. Rendered on a real Vulkan/Wayland session — identical to the per-element path.

Resolves #47

🤖 Generated with Claude Code

Screenshots

HelloUI compositing background quads, a mouse-following circle, and label text in a single DispatchFused — pixel-identical to the per-element path:

HelloUI rendered via DispatchFused

## Summary Adds a fifth UI compute path — `DispatchFused` — alongside the four per-element `Dispatch*` calls (issue #47). 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 `VkMemoryBarrier`s a run of consecutive `Dispatch*` calls would pay. - The win materialises at **≥ 2 non-empty categories** (saves 1 load + 1 store + 1 barrier per extra type). An absent category is a **zero-trip, push-constant-uniform loop** (~free) — no per-combination kernels. - The new uber-kernel `shaders/ui-fused.comp.glsl` reuses each category's **exact** cooperative shared-memory tile-cull + per-pixel accumulate (co-designed with #46), so a fused category is **pixel-identical** to its standalone pass. Categories run sequentially per thread and share one set of shared scratch, so the VGPR/shared-memory high-water mark stays at the per-category level, not the sum. ## Additive — frozen contract untouched The per-element `Dispatch*` calls and the 48-byte `UIDispatchHeader` are **unchanged**. `DispatchFused` gets its own **128-byte** push-constant layout (`UIFusedHeader`: four `uvec4` headers + four per-category clip `vec4`s — exactly the guaranteed push-constant minimum, no padding holes). 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 (fusing *across* a custom dispatch is a memory round-trip, i.e. physics, not removed here). ## WebGPU (Vulkan-second) `DispatchFused` falls back to issuing 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. ## Testing - **New automated test `UIFusedShader`** (`crafter-build test`): compiles the real `ui-fused.comp.glsl` with glslang, validates with `spirv-val`, asserts exactly one push-constant block, and **pins the push-constant member offsets to `{0,16,32,48,64,80,96,112}`** — guarding the GLSL↔C++ layout drift that the C++-side `static_assert(sizeof(UIFusedHeader)==128)` alone can't. - Full suite green: **8 passed** (`crafter-build test --jobs=1`). - **Live exercise:** `HelloUI` now composites its background quads, mouse circle, and label text in a single `DispatchFused`. Rendered on a real Vulkan/Wayland session — identical to the per-element path. Resolves #47 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Screenshots HelloUI compositing background quads, a mouse-following circle, and label text in a single `DispatchFused` — pixel-identical to the per-element path: ![HelloUI rendered via DispatchFused](https://forgejo.catcrafts.net/attachments/a6aa8273-86df-4378-a88a-c3d559694835)
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>
# Conflicts:
#	interfaces/Crafter.Graphics-UI.cppm
#	project.cpp
catbot merged commit a19c2934c8 into master 2026-06-16 19:12:21 +02:00
catbot deleted branch claude/issue-47 2026-06-16 19:12:21 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Catcrafts/Crafter.Graphics!93
No description provided.