Crafter.Graphics/shaders/ui-images.comp.glsl
catbot da9f2a89e8 perf(ui): subgroup prefix-sum stream compaction, not lane-0 serial scan (#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>
2026-06-18 13:26:37 +00:00

114 lines
3.9 KiB
GLSL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#version 460
#extension GL_GOOGLE_include_directive : enable
#extension GL_KHR_shader_subgroup_basic : enable
#extension GL_KHR_shader_subgroup_arithmetic : enable
#include "ui-shared.glsl"
// One workgroup per 8×8 screen tile. The workgroup cooperatively streams the
// ImageItem list in chunks of 64 (see ui-shared.glsl), culling each chunk
// against the tile and compacting survivors — in buffer order — into shared
// memory; every thread then accumulates over only those survivors.
layout(push_constant) uniform PC {
UIDispatchHeader hdr;
} pc;
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
shared vec4 s_rect[UI_CHUNK];
shared vec4 s_uv[UI_CHUNK];
shared vec4 s_tint[UI_CHUNK];
shared uvec4 s_slots[UI_CHUNK];
shared uint s_keep[UI_CHUNK];
shared uint s_order[UI_CHUNK];
shared uint s_count;
shared uint s_subTotals[UI_CHUNK]; // per-subgroup survivor totals (carry step)
// Stable in-order compaction of one chunk's survivors via a per-subgroup
// exclusive prefix-sum plus a carry across subgroups, so a survivor's slot in
// s_order[] equals the number of survivors with a smaller local index — buffer
// (draw) order preserved. Replaces the old lane-0 serial scan. See
// ui-fused.comp.glsl for the full rationale; the body is identical.
void uiCompactChunk() {
uint lid = gl_LocalInvocationIndex;
uint keep = s_keep[lid];
uint subPrefix = subgroupExclusiveAdd(keep);
uint subTotal = subgroupAdd(keep);
if (subgroupElect())
s_subTotals[gl_SubgroupID] = subTotal;
barrier();
uint base = 0u;
uint total = 0u;
for (uint s = 0u; s < gl_NumSubgroups; ++s) {
uint t = s_subTotals[s];
if (s < gl_SubgroupID) base += t;
total += t;
}
if (lid == 0u) s_count = total;
if (keep != 0u)
s_order[base + subPrefix] = lid;
}
void main() {
ivec2 screenPx;
bool valid = uiResolveScreenPixel(pc.hdr, screenPx);
vec4 dst = vec4(0.0);
vec2 sp = vec2(0.0);
if (valid) {
dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
sp = vec2(screenPx) + 0.5;
}
vec2 tileMin, tileMax;
uiTileBounds(tileMin, tileMax);
uint lid = gl_LocalInvocationIndex;
for (uint base = 0u; base < pc.hdr.itemCount; base += UI_CHUNK) {
uint idx = base + lid;
bool keep = false;
if (idx < pc.hdr.itemCount) {
s_rect[lid] = uiImageHeap[pc.hdr.itemBuffer].items[idx].rect;
s_uv[lid] = uiImageHeap[pc.hdr.itemBuffer].items[idx].uv;
s_tint[lid] = uiImageHeap[pc.hdr.itemBuffer].items[idx].tint;
s_slots[lid] = uiImageHeap[pc.hdr.itemBuffer].items[idx].slots;
keep = uiAabbOverlapsTile(s_rect[lid].xy, s_rect[lid].xy + s_rect[lid].zw,
tileMin, tileMax);
}
s_keep[lid] = keep ? 1u : 0u;
barrier();
uiCompactChunk();
barrier();
if (valid) {
for (uint j = 0u; j < s_count; ++j) {
uint c = s_order[j];
vec2 lo = s_rect[c].xy;
vec2 hi = s_rect[c].xy + s_rect[c].zw;
if (sp.x < lo.x || sp.y < lo.y) continue;
if (sp.x >= hi.x || sp.y >= hi.y) continue;
vec2 t = (sp - s_rect[c].xy) / s_rect[c].zw;
vec2 uv = mix(s_uv[c].xy, s_uv[c].zw, t);
uint texSlot = s_slots[c].x;
uint sampSlot = s_slots[c].y;
vec4 sampled = texture(
sampler2D(uiTextures[nonuniformEXT(texSlot)],
uiSamplers[nonuniformEXT(sampSlot)]),
uv
);
vec4 src = sampled * s_tint[c];
if (src.a <= 0.0) continue;
dst = uiBlendOver(dst, src);
}
}
barrier();
}
if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
}