perf(ui): cooperative shared-memory tile culling in UI compute shaders
Each 8×8-tile workgroup previously had all 64 threads walk the entire item list, re-reading every item from the SSBO 64× and running the per-pixel reject for items that never touch the tile — O(tiles × N) item loads dominated by SSBO traffic. The workgroup now streams the item list in chunks of 64: each thread loads one item, tests its AABB against the whole tile, and the survivors are compacted — in original buffer order — into shared memory. Every thread then runs the unchanged per-pixel accumulate over only those survivors. This drops SSBO traffic ~64× (each item read once per workgroup instead of once per pixel) and shrinks the inner loop to the items that actually overlap the tile. Draw order is preserved exactly: chunks run in array order and the in-chunk compaction is a stable in-order scan, so later items still overdraw earlier ones (no atomic-append nondeterminism). The inner loop body is byte-for-byte the original per-pixel logic, so output is pixel-identical — the cull only decides which items reach it. Threads no longer early-return so every thread reaches the workgroup barriers. Applied to ui-quads, ui-circles, ui-images and ui-text; shared helpers (uiTileBounds / uiAabbOverlapsTile) live in ui-shared.glsl. Resolves #46 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
cea57f9af8
commit
6eb11fbf71
5 changed files with 337 additions and 107 deletions
|
|
@ -148,3 +148,38 @@ float uiSdRoundRect(vec2 p, vec2 halfSize, vec4 r) {
|
|||
vec2 q = abs(p) - halfSize + r.x;
|
||||
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x;
|
||||
}
|
||||
|
||||
// ─── shared-memory cooperative tile culling ─────────────────────────────
|
||||
// Each shader still dispatches one 8×8 workgroup per screen tile, but rather
|
||||
// than have all 64 threads walk the FULL item list — re-reading every item
|
||||
// from the SSBO 64× and running the per-pixel reject for items that never
|
||||
// touch the tile — the workgroup streams the list in chunks of 64. Each
|
||||
// thread loads ONE item, tests its AABB against the whole tile, and the
|
||||
// survivors are compacted (in original array order) into a shared list that
|
||||
// every thread then iterates for its own pixel.
|
||||
//
|
||||
// This preserves draw order exactly: chunks run in array order and the
|
||||
// in-chunk compaction is a stable in-order scan, so the per-pixel inner loop
|
||||
// still sees items in buffer order (later overdraws earlier). It also drops
|
||||
// SSBO traffic ~64× (each item read once per workgroup, not once per pixel)
|
||||
// and shrinks the inner loop to only items that actually overlap the tile.
|
||||
// The inner loop body is left byte-for-byte identical to the unculled path,
|
||||
// so output is pixel-identical — the cull only decides which items reach it.
|
||||
|
||||
const uint UI_TILE_SIZE = 8u; // matches local_size_x / local_size_y
|
||||
const uint UI_CHUNK = 64u; // = workgroup size (8×8); one item per thread
|
||||
|
||||
// This workgroup's tile bounds in pixels.
|
||||
void uiTileBounds(out vec2 tileMin, out vec2 tileMax) {
|
||||
tileMin = vec2(gl_WorkGroupID.xy * UI_TILE_SIZE);
|
||||
tileMax = tileMin + vec2(float(UI_TILE_SIZE));
|
||||
}
|
||||
|
||||
// Conservative AABB-vs-tile overlap. MUST be a superset of "some pixel in the
|
||||
// tile passes the in-loop per-pixel reject" so an item a pixel needs is never
|
||||
// dropped. Pixel centers live in (tileMin, tileMax), so testing the full tile
|
||||
// box only ever keeps more, never fewer.
|
||||
bool uiAabbOverlapsTile(vec2 lo, vec2 hi, vec2 tileMin, vec2 tileMax) {
|
||||
return lo.x < tileMax.x && hi.x > tileMin.x
|
||||
&& lo.y < tileMax.y && hi.y > tileMin.y;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue