[perf][MEDIUM] Add DispatchFused (additive) to collapse consecutive UI passes to one load/store #47
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Catcrafts/Crafter.Graphics#47
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Subsystem: UI compute pipeline + GLSL shaders
Location:
shaders/ui-quads.comp.glsl:18-50(same in circles/images/text); new fused kernel +UIRenderer::DispatchFusedImpact: MEDIUM · Effort: Large
Related: #46 (tile culling — co-design), #48 (barrier scoping — non-breaking partial win)
Problem
Each of the four UI shaders does one
imageLoad+ oneimageStoreof the swapchain storage image. A frame that runs N consecutive standard passes loads+stores every covered pixel N times (2N image transactions/pixel) plus N-1 inter-passVkMemoryBarriers that drain compute and flush the whole image. For the common quads+text frame that is 4 image transactions/pixel + 1 full barrier where 2 transactions + 0 barriers would do.Why the original "fuse all four passes into one dispatch" was rejected
It removed the generic
ui.Dispatch(customShader, ...)interleave point and the per-pass read-modify-write semantics that a real downstream consumer (3DForts) depends on. Verified:Forts3D-MainMenu.cpp:744-906interleaves custom compute (blockRevealShader,circleWipeShader,textFadeShader) between standard passes, intooutImage, with load-bearing ordering ("drawn AFTER the dark-gray wipe shader so it sits on top");Forts3D-Bloom.cpp:560composites intoOutImageSlot()the same way. Both consume the frozen 48-byteUIDispatchHeader,FillHeader,OutImageSlot,FontAtlas*Slot. A blind single-pass merge breaks all of this.Proposed fix — additive
DispatchFusedAdd one new entry point alongside the existing per-element
Dispatch*(which stay, unchanged):backed by one uber-kernel that loads
dstonce, composites quads→circles→images→text in canonical order in a register, and stores once — eliminating the redundant load/store and the inter-pass barriers for that fused run.The interleave capability is preserved explicitly via
DispatchFused → ui.Dispatch(custom) → DispatchFused. No hidden flush state; the app declares its fusible batches. 3DForts MainMenu becomes:DispatchFused(early)→ custom reveal/wipe →DispatchFused(late).This is additive and does NOT touch the frozen contract: per-element
Dispatch*and the 48-byteUIDispatchHeaderare untouched.DispatchFusedgets its own new push-constant layout (4 sub-headers + font slots).Cost model (why one uber-kernel, NOT per-combination kernels)
DispatchFusedfor only quads+text is not wasted; the unused circle/image loops are ~free.DispatchQuads(dead checks) — so keep the per-element calls for single-type / fine-grained use.DispatchFusedQuadText, etc. = 15 kernels for 2^4-1 subsets). The categories composite sequentially, so per-loop temporaries don't overlap and the compiler reuses registers across phases — the uber-kernel's VGPR high-water mark is ~max(per-category), not sum. Specialization recovers <1% (dead checks + binary size) at the cost of 15 pipelines + API bloat + maintenance. Not worth it.Caveats to measure / watch
VkSpecializationInfobooleans (HAS_QUADS/...) and lazily create pipelines keyed by the present-mask, so only the ~3-4 combinations that actually occur compile. NOT hand-written variants. Measure-first only.DispatchFusedcarries one clip per category in its header; slightly larger PC.DispatchImagesbinds one texture per dispatch on WebGPU (Forts3D-OptionsMenu.cpp:574). Vulkan bindless reads texture slots from item data trivially; the WebGPU images path may need to fall back to per-element or be handled specially. Vulkan-first.Irreducible limit
Fusion across a custom
ui.Dispatch()is impossible (a dispatch boundary is a memory round-trip — registers don't survive it). MainMenu's interleaved passes still pay a store/load + barrier around each custom dispatch. That is physics, not API design, and is no longer something this issue tries to remove.[perf][MEDIUM] Redundant swapchain imageLoad/imageStore per UI dispatchto [perf][MEDIUM] Add DispatchFused (additive) to collapse consecutive UI passes to one load/store