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>
UIRenderer::Dispatch inserted a queue-wide VkMemoryBarrier
(SHADER_WRITE -> SHADER_READ|WRITE) before every dispatch after the
first, flushing/invalidating all shader caches even though the only
hazard is the read-modify-write of the single swapchain storage image
that every UI pass shares.
Narrow it to a VkImageMemoryBarrier scoped to that image's subresource
(COMPUTE -> COMPUTE, GENERAL -> GENERAL, no layout transition). Same
correctness — later passes still observe earlier passes' writes — but
only this image's caches are flushed; unrelated resources are no longer
invalidated.
This is a narrower cache flush only: the execution dependency stays
compute->compute queue-wide, so passes still do not overlap. Eliminating
the barriers entirely requires pass fusion (#47) and is intentionally
out of scope here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>