perf(ui): subgroup prefix-sum stream compaction in the UI compute shaders (#124) #145

Merged
catbot merged 1 commit from claude/issue-124 into master 2026-06-18 15:27:30 +02:00
Member

Summary

uiCompactChunk (and the same pattern inlined in the four standalone UI shaders) ran the per-chunk survivor scan entirely on gl_LocalInvocationIndex == 0 — ~64 serial iterations with 63/64 lanes idle, between two barriers, 4× per chunk in the fused kernel. It has to produce a stable in-order (buffer-order) exclusive prefix of the survivors into s_order[] so the per-pixel inner loop still sees items in draw order.

This replaces the lane-0 scan with a subgroup exclusive prefix-sum + cross-subgroup carry:

  • Each subgroup computes subgroupExclusiveAdd(keep) (rank within the subgroup) and subgroupAdd(keep) (its survivor total) at register speed.
  • One lane per subgroup publishes the total to shared s_subTotals[].
  • After one barrier, every lane sums the totals of all lower-id subgroups for its base offset.
  • A survivor's slot = base + subPrefix = the number of survivors with a smaller local index → buffer (draw) order preserved exactly. (An atomicAdd would scramble order — deliberately avoided, per the issue.)

The carry makes it correct for any subgroup width (2 subgroups on the 32-wide descriptor_heap target, up to 8/16 on narrower parts), relying only on the gl_LocalInvocationIndex(gl_SubgroupID, gl_SubgroupInvocationID) linear mapping every Vulkan compute implementation provides. The feature is free at the device baseline (apiVersion 1.4 + VK_EXT_descriptor_heap implies Vulkan 1.1 subgroup arithmetic), so no correctness fallback is needed. WebGPU is unaffected (it runs separate embedded WGSL).

Applied to ui-fused.comp.glsl and the inlined pattern in ui-quads/circles/images/text.

Verification

  • crafter-build test — all 23 pass, including UIFusedShader (recompiles ui-fused.comp.glsl with glslang, runs spirv-val, and pins the push-constant member offsets — unchanged).
  • 140k-trial CPU simulation of the exact algorithm matches the serial reference for subgroup sizes {1,2,4,8,16,32,64}.
  • Runtime on real hardware: HelloUI (which uses DispatchFused with quads + circles + text) renders correctly on an RTX 4090 — the 2-subgroups-per-workgroup carry path. Text composites on top of the button quad and the circle knob on top of the slider track, confirming draw order is intact.

Screenshots

HelloUI on RTX 4090 — DispatchFused quads+circles+text, draw order intact

Resolves #124

## Summary `uiCompactChunk` (and the same pattern inlined in the four standalone UI shaders) ran the per-chunk survivor scan entirely on `gl_LocalInvocationIndex == 0` — ~64 serial iterations with 63/64 lanes idle, between two barriers, **4× per chunk** in the fused kernel. It has to produce a **stable in-order** (buffer-order) exclusive prefix of the survivors into `s_order[]` so the per-pixel inner loop still sees items in draw order. This replaces the lane-0 scan with a **subgroup exclusive prefix-sum + cross-subgroup carry**: - Each subgroup computes `subgroupExclusiveAdd(keep)` (rank within the subgroup) and `subgroupAdd(keep)` (its survivor total) at register speed. - One lane per subgroup publishes the total to shared `s_subTotals[]`. - After one barrier, every lane sums the totals of all **lower-id** subgroups for its base offset. - A survivor's slot = `base + subPrefix` = the number of survivors with a smaller local index → **buffer (draw) order preserved exactly**. (An `atomicAdd` would scramble order — deliberately avoided, per the issue.) The carry makes it correct for any subgroup width (2 subgroups on the 32-wide `descriptor_heap` target, up to 8/16 on narrower parts), relying only on the `gl_LocalInvocationIndex` ↔ `(gl_SubgroupID, gl_SubgroupInvocationID)` linear mapping every Vulkan compute implementation provides. The feature is free at the device baseline (`apiVersion 1.4` + `VK_EXT_descriptor_heap` implies Vulkan 1.1 subgroup arithmetic), so no correctness fallback is needed. WebGPU is unaffected (it runs separate embedded WGSL). Applied to `ui-fused.comp.glsl` and the inlined pattern in `ui-quads`/`circles`/`images`/`text`. ## Verification - **`crafter-build test` — all 23 pass**, including `UIFusedShader` (recompiles `ui-fused.comp.glsl` with glslang, runs `spirv-val`, and pins the push-constant member offsets — unchanged). - **140k-trial CPU simulation** of the exact algorithm matches the serial reference for subgroup sizes {1,2,4,8,16,32,64}. - **Runtime on real hardware**: `HelloUI` (which uses `DispatchFused` with quads + circles + text) renders correctly on an RTX 4090 — the 2-subgroups-per-workgroup carry path. Text composites on top of the button quad and the circle knob on top of the slider track, confirming draw order is intact. ## Screenshots ![HelloUI on RTX 4090 — DispatchFused quads+circles+text, draw order intact](https://forgejo.catcrafts.net/attachments/3c40c5a7-54b8-473d-9cef-cb6dc9cddc0f) Resolves #124
uiCompactChunk ran the survivor scan entirely on gl_LocalInvocationIndex
== 0: ~64 serial iterations with 63/64 lanes idle, between two barriers,
4x per chunk in the fused kernel. It must emit a stable in-order
(buffer-order) exclusive prefix of the survivors so the per-pixel inner
loop still sees items in draw order.

Replace it with a per-subgroup subgroupExclusiveAdd of the keep bits plus
a carry across subgroups: each subgroup publishes its survivor total to
shared memory, then every lane sums the totals of all lower-id subgroups
for its base. A survivor's slot in s_order[] therefore equals the number
of survivors with a smaller local index — buffer (draw) order preserved
exactly, unlike an atomicAdd which would scramble it.

The carry makes the result correct for any subgroup width (2 subgroups on
the 32-wide descriptor_heap target, up to 8/16 on narrower parts), relying
only on the gl_LocalInvocationIndex <-> (gl_SubgroupID,
gl_SubgroupInvocationID) linear mapping every Vulkan compute
implementation provides. The feature is free at the device baseline
(apiVersion 1.4 + VK_EXT_descriptor_heap implies Vulkan 1.1 subgroup
arithmetic), so no correctness fallback is needed; WebGPU is unaffected
(separate embedded WGSL).

Applied to ui-fused.comp.glsl and the same pattern inlined in
ui-quads/circles/images/text.

Verified: all 23 tests pass (UIFusedShader recompiles + spirv-val +
pins push-constant offsets); a 140k-trial CPU simulation of the algorithm
matches the serial reference for subgroup sizes 1..64; HelloUI renders
correctly on an RTX 4090 (DispatchFused quads+circles+text) with draw
order intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot merged commit 9046562a90 into master 2026-06-18 15:27:30 +02:00
catbot deleted branch claude/issue-124 2026-06-18 15:27:30 +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!145
No description provided.