perf(ui): cooperative shared-memory tile culling in UI compute shaders #85
5 changed files with 337 additions and 107 deletions
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>
commit
6eb11fbf71
|
|
@ -2,47 +2,99 @@
|
||||||
#extension GL_GOOGLE_include_directive : enable
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
#include "ui-shared.glsl"
|
#include "ui-shared.glsl"
|
||||||
|
|
||||||
|
// One workgroup per 8×8 screen tile. The workgroup cooperatively streams the
|
||||||
|
// CircleItem 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 {
|
layout(push_constant) uniform PC {
|
||||||
UIDispatchHeader hdr;
|
UIDispatchHeader hdr;
|
||||||
} pc;
|
} pc;
|
||||||
|
|
||||||
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||||
|
|
||||||
|
shared vec4 s_centerRadius[UI_CHUNK];
|
||||||
|
shared vec4 s_color[UI_CHUNK];
|
||||||
|
shared vec4 s_outline[UI_CHUNK];
|
||||||
|
shared uint s_keep[UI_CHUNK];
|
||||||
|
shared uint s_order[UI_CHUNK];
|
||||||
|
shared uint s_count;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
ivec2 screenPx;
|
ivec2 screenPx;
|
||||||
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
bool valid = uiResolveScreenPixel(pc.hdr, screenPx);
|
||||||
|
|
||||||
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
vec4 dst = vec4(0.0);
|
||||||
vec2 sp = vec2(screenPx) + 0.5;
|
vec2 sp = vec2(0.0);
|
||||||
|
if (valid) {
|
||||||
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||||
CircleItem it = LoadCircleItem(pc.hdr.itemBuffer, i);
|
sp = vec2(screenPx) + 0.5;
|
||||||
|
|
||||||
vec2 center = it.centerRadius.xy;
|
|
||||||
float radius = it.centerRadius.z;
|
|
||||||
if (radius <= 0.0) continue;
|
|
||||||
|
|
||||||
// Cheap bounding-box reject.
|
|
||||||
if (abs(sp.x - center.x) > radius + 1.0) continue;
|
|
||||||
if (abs(sp.y - center.y) > radius + 1.0) continue;
|
|
||||||
|
|
||||||
float d = length(sp - center) - radius;
|
|
||||||
|
|
||||||
float bodyA = clamp(0.5 - d, 0.0, 1.0);
|
|
||||||
if (bodyA <= 0.0 && it.outline.x <= 0.0) continue;
|
|
||||||
|
|
||||||
vec4 src = vec4(it.color.rgb, it.color.a * bodyA);
|
|
||||||
|
|
||||||
if (it.outline.x > 0.0) {
|
|
||||||
float t = abs(d + it.outline.x * 0.5) - it.outline.x * 0.5;
|
|
||||||
float outlineA = clamp(0.5 - t, 0.0, 1.0);
|
|
||||||
src.rgb = mix(src.rgb, it.outline.yzw, outlineA);
|
|
||||||
src.a = max(src.a, outlineA);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (src.a <= 0.0) continue;
|
|
||||||
dst = uiBlendOver(dst, src);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
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_centerRadius[lid] = uiCircleHeap[pc.hdr.itemBuffer].items[idx].centerRadius;
|
||||||
|
s_color[lid] = uiCircleHeap[pc.hdr.itemBuffer].items[idx].color;
|
||||||
|
s_outline[lid] = uiCircleHeap[pc.hdr.itemBuffer].items[idx].outline;
|
||||||
|
// Cull box matches the in-loop bounding-box reject (radius + 1px).
|
||||||
|
float radius = s_centerRadius[lid].z;
|
||||||
|
if (radius > 0.0) {
|
||||||
|
vec2 c = s_centerRadius[lid].xy;
|
||||||
|
vec2 r = vec2(radius + 1.0);
|
||||||
|
keep = uiAabbOverlapsTile(c - r, c + r, tileMin, tileMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s_keep[lid] = keep ? 1u : 0u;
|
||||||
|
barrier();
|
||||||
|
|
||||||
|
if (lid == 0u) {
|
||||||
|
uint n = 0u;
|
||||||
|
uint lim = min(UI_CHUNK, pc.hdr.itemCount - base);
|
||||||
|
for (uint k = 0u; k < lim; ++k)
|
||||||
|
if (s_keep[k] != 0u) s_order[n++] = k;
|
||||||
|
s_count = n;
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
for (uint j = 0u; j < s_count; ++j) {
|
||||||
|
uint c = s_order[j];
|
||||||
|
|
||||||
|
vec2 center = s_centerRadius[c].xy;
|
||||||
|
float radius = s_centerRadius[c].z;
|
||||||
|
if (radius <= 0.0) continue;
|
||||||
|
|
||||||
|
// Cheap bounding-box reject.
|
||||||
|
if (abs(sp.x - center.x) > radius + 1.0) continue;
|
||||||
|
if (abs(sp.y - center.y) > radius + 1.0) continue;
|
||||||
|
|
||||||
|
float d = length(sp - center) - radius;
|
||||||
|
|
||||||
|
vec4 outline = s_outline[c];
|
||||||
|
float bodyA = clamp(0.5 - d, 0.0, 1.0);
|
||||||
|
if (bodyA <= 0.0 && outline.x <= 0.0) continue;
|
||||||
|
|
||||||
|
vec4 col = s_color[c];
|
||||||
|
vec4 src = vec4(col.rgb, col.a * bodyA);
|
||||||
|
|
||||||
|
if (outline.x > 0.0) {
|
||||||
|
float t = abs(d + outline.x * 0.5) - outline.x * 0.5;
|
||||||
|
float outlineA = clamp(0.5 - t, 0.0, 1.0);
|
||||||
|
src.rgb = mix(src.rgb, outline.yzw, outlineA);
|
||||||
|
src.a = max(src.a, outlineA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.a <= 0.0) continue;
|
||||||
|
dst = uiBlendOver(dst, src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,42 +2,89 @@
|
||||||
#extension GL_GOOGLE_include_directive : enable
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
#include "ui-shared.glsl"
|
#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 {
|
layout(push_constant) uniform PC {
|
||||||
UIDispatchHeader hdr;
|
UIDispatchHeader hdr;
|
||||||
} pc;
|
} pc;
|
||||||
|
|
||||||
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
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;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
ivec2 screenPx;
|
ivec2 screenPx;
|
||||||
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
bool valid = uiResolveScreenPixel(pc.hdr, screenPx);
|
||||||
|
|
||||||
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
vec4 dst = vec4(0.0);
|
||||||
vec2 sp = vec2(screenPx) + 0.5;
|
vec2 sp = vec2(0.0);
|
||||||
|
if (valid) {
|
||||||
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||||
ImageItem it = LoadImageItem(pc.hdr.itemBuffer, i);
|
sp = vec2(screenPx) + 0.5;
|
||||||
|
|
||||||
vec2 lo = it.rect.xy;
|
|
||||||
vec2 hi = it.rect.xy + it.rect.zw;
|
|
||||||
if (sp.x < lo.x || sp.y < lo.y) continue;
|
|
||||||
if (sp.x >= hi.x || sp.y >= hi.y) continue;
|
|
||||||
|
|
||||||
vec2 t = (sp - it.rect.xy) / it.rect.zw;
|
|
||||||
vec2 uv = mix(it.uv.xy, it.uv.zw, t);
|
|
||||||
|
|
||||||
uint texSlot = it.slots.x;
|
|
||||||
uint sampSlot = it.slots.y;
|
|
||||||
|
|
||||||
vec4 sampled = texture(
|
|
||||||
sampler2D(uiTextures[nonuniformEXT(texSlot)],
|
|
||||||
uiSamplers[nonuniformEXT(sampSlot)]),
|
|
||||||
uv
|
|
||||||
);
|
|
||||||
vec4 src = sampled * it.tint;
|
|
||||||
if (src.a <= 0.0) continue;
|
|
||||||
dst = uiBlendOver(dst, src);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
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();
|
||||||
|
|
||||||
|
if (lid == 0u) {
|
||||||
|
uint n = 0u;
|
||||||
|
uint lim = min(UI_CHUNK, pc.hdr.itemCount - base);
|
||||||
|
for (uint k = 0u; k < lim; ++k)
|
||||||
|
if (s_keep[k] != 0u) s_order[n++] = k;
|
||||||
|
s_count = n;
|
||||||
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,50 +2,102 @@
|
||||||
#extension GL_GOOGLE_include_directive : enable
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
#include "ui-shared.glsl"
|
#include "ui-shared.glsl"
|
||||||
|
|
||||||
// One workgroup per 8×8 screen tile. Each thread owns one pixel and iterates
|
// One workgroup per 8×8 screen tile. The workgroup cooperatively streams the
|
||||||
// every QuadItem in order, accumulating into a local dst register, so item
|
// QuadItem list in chunks of 64 (see ui-shared.glsl), culling each chunk
|
||||||
// order in the buffer == draw order on screen (later items overdraw earlier).
|
// against the tile and compacting survivors — in buffer order — into shared
|
||||||
|
// memory; every thread then runs the per-pixel accumulate over only those
|
||||||
|
// survivors. Item order in the buffer == draw order on screen.
|
||||||
layout(push_constant) uniform PC {
|
layout(push_constant) uniform PC {
|
||||||
UIDispatchHeader hdr;
|
UIDispatchHeader hdr;
|
||||||
} pc;
|
} pc;
|
||||||
|
|
||||||
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||||
|
|
||||||
|
// Per-chunk cooperative-cull scratch (one slot per workgroup thread).
|
||||||
|
shared vec4 s_rect[UI_CHUNK];
|
||||||
|
shared vec4 s_color[UI_CHUNK];
|
||||||
|
shared vec4 s_corners[UI_CHUNK];
|
||||||
|
shared vec4 s_outline[UI_CHUNK];
|
||||||
|
shared uint s_keep[UI_CHUNK];
|
||||||
|
shared uint s_order[UI_CHUNK];
|
||||||
|
shared uint s_count;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
// NOTE: do not early-return — every thread must reach the barriers below.
|
||||||
ivec2 screenPx;
|
ivec2 screenPx;
|
||||||
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
bool valid = uiResolveScreenPixel(pc.hdr, screenPx);
|
||||||
|
|
||||||
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
vec4 dst = vec4(0.0);
|
||||||
vec2 sp = vec2(screenPx) + 0.5;
|
vec2 sp = vec2(0.0);
|
||||||
|
if (valid) {
|
||||||
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||||
QuadItem it = LoadQuadItem(pc.hdr.itemBuffer, i);
|
sp = vec2(screenPx) + 0.5;
|
||||||
|
|
||||||
// Cheap pre-test against the item's axis-aligned rect.
|
|
||||||
vec2 lo = it.rect.xy;
|
|
||||||
vec2 hi = it.rect.xy + it.rect.zw;
|
|
||||||
if (sp.x < lo.x || sp.y < lo.y) continue;
|
|
||||||
if (sp.x >= hi.x || sp.y >= hi.y) continue;
|
|
||||||
|
|
||||||
vec2 halfSize = it.rect.zw * 0.5;
|
|
||||||
vec2 p = sp - (it.rect.xy + halfSize);
|
|
||||||
float d = uiSdRoundRect(p, halfSize, it.corners);
|
|
||||||
|
|
||||||
float bodyA = clamp(0.5 - d, 0.0, 1.0);
|
|
||||||
if (bodyA <= 0.0 && it.outline.x <= 0.0) continue;
|
|
||||||
|
|
||||||
vec4 src = vec4(it.color.rgb, it.color.a * bodyA);
|
|
||||||
|
|
||||||
if (it.outline.x > 0.0) {
|
|
||||||
float t = abs(d + it.outline.x * 0.5) - it.outline.x * 0.5;
|
|
||||||
float outlineA = clamp(0.5 - t, 0.0, 1.0);
|
|
||||||
src.rgb = mix(src.rgb, it.outline.yzw, outlineA);
|
|
||||||
src.a = max(src.a, outlineA);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (src.a <= 0.0) continue;
|
|
||||||
dst = uiBlendOver(dst, src);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
vec2 tileMin, tileMax;
|
||||||
|
uiTileBounds(tileMin, tileMax);
|
||||||
|
|
||||||
|
uint lid = gl_LocalInvocationIndex;
|
||||||
|
for (uint base = 0u; base < pc.hdr.itemCount; base += UI_CHUNK) {
|
||||||
|
// Cooperative load: each thread reads one item member-by-member
|
||||||
|
// (the NVIDIA descriptor-heap workaround) into shared and culls it.
|
||||||
|
uint idx = base + lid;
|
||||||
|
bool keep = false;
|
||||||
|
if (idx < pc.hdr.itemCount) {
|
||||||
|
s_rect[lid] = uiQuadHeap[pc.hdr.itemBuffer].items[idx].rect;
|
||||||
|
s_color[lid] = uiQuadHeap[pc.hdr.itemBuffer].items[idx].color;
|
||||||
|
s_corners[lid] = uiQuadHeap[pc.hdr.itemBuffer].items[idx].corners;
|
||||||
|
s_outline[lid] = uiQuadHeap[pc.hdr.itemBuffer].items[idx].outline;
|
||||||
|
keep = uiAabbOverlapsTile(s_rect[lid].xy, s_rect[lid].xy + s_rect[lid].zw,
|
||||||
|
tileMin, tileMax);
|
||||||
|
}
|
||||||
|
s_keep[lid] = keep ? 1u : 0u;
|
||||||
|
barrier();
|
||||||
|
|
||||||
|
// Stable in-order compaction of this chunk's survivors.
|
||||||
|
if (lid == 0u) {
|
||||||
|
uint n = 0u;
|
||||||
|
uint lim = min(UI_CHUNK, pc.hdr.itemCount - base);
|
||||||
|
for (uint k = 0u; k < lim; ++k)
|
||||||
|
if (s_keep[k] != 0u) s_order[n++] = k;
|
||||||
|
s_count = n;
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
for (uint j = 0u; j < s_count; ++j) {
|
||||||
|
uint c = s_order[j];
|
||||||
|
|
||||||
|
// Cheap pre-test against the item's axis-aligned rect.
|
||||||
|
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 halfSize = s_rect[c].zw * 0.5;
|
||||||
|
vec2 p = sp - (s_rect[c].xy + halfSize);
|
||||||
|
float d = uiSdRoundRect(p, halfSize, s_corners[c]);
|
||||||
|
|
||||||
|
vec4 outline = s_outline[c];
|
||||||
|
float bodyA = clamp(0.5 - d, 0.0, 1.0);
|
||||||
|
if (bodyA <= 0.0 && outline.x <= 0.0) continue;
|
||||||
|
|
||||||
|
vec4 col = s_color[c];
|
||||||
|
vec4 src = vec4(col.rgb, col.a * bodyA);
|
||||||
|
|
||||||
|
if (outline.x > 0.0) {
|
||||||
|
float t = abs(d + outline.x * 0.5) - outline.x * 0.5;
|
||||||
|
float outlineA = clamp(0.5 - t, 0.0, 1.0);
|
||||||
|
src.rgb = mix(src.rgb, outline.yzw, outlineA);
|
||||||
|
src.a = max(src.a, outlineA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.a <= 0.0) continue;
|
||||||
|
dst = uiBlendOver(dst, src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
barrier(); // done reading shared for this chunk before it's overwritten
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,3 +148,38 @@ float uiSdRoundRect(vec2 p, vec2 halfSize, vec4 r) {
|
||||||
vec2 q = abs(p) - halfSize + r.x;
|
vec2 q = abs(p) - halfSize + r.x;
|
||||||
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - 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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@
|
||||||
#extension GL_GOOGLE_include_directive : enable
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
#include "ui-shared.glsl"
|
#include "ui-shared.glsl"
|
||||||
|
|
||||||
// One workgroup per 8×8 screen tile. Iterates every glyph in order; each
|
// One workgroup per 8×8 screen tile. The workgroup cooperatively streams the
|
||||||
// pixel keeps a local accumulator so order in the buffer == draw order.
|
// GlyphItem 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 {
|
layout(push_constant) uniform PC {
|
||||||
UIDispatchHeader hdr;
|
UIDispatchHeader hdr;
|
||||||
uint fontTextureSlot;
|
uint fontTextureSlot;
|
||||||
|
|
@ -18,48 +20,90 @@ layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||||
const float ON_EDGE = 128.0 / 255.0;
|
const float ON_EDGE = 128.0 / 255.0;
|
||||||
const float DIST_SCALE = 32.0;
|
const float DIST_SCALE = 32.0;
|
||||||
|
|
||||||
|
shared vec4 s_rect[UI_CHUNK];
|
||||||
|
shared vec4 s_uv[UI_CHUNK];
|
||||||
|
shared vec4 s_color[UI_CHUNK];
|
||||||
|
shared uint s_keep[UI_CHUNK];
|
||||||
|
shared uint s_order[UI_CHUNK];
|
||||||
|
shared uint s_count;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
ivec2 screenPx;
|
ivec2 screenPx;
|
||||||
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
bool valid = uiResolveScreenPixel(pc.hdr, screenPx);
|
||||||
|
|
||||||
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
vec4 dst = vec4(0.0);
|
||||||
vec2 sp = vec2(screenPx) + 0.5;
|
vec2 sp = vec2(0.0);
|
||||||
|
if (valid) {
|
||||||
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||||
GlyphItem it = LoadGlpyhtem(pc.hdr.itemBuffer, i);
|
sp = vec2(screenPx) + 0.5;
|
||||||
|
|
||||||
vec2 lo = it.rect.xy;
|
|
||||||
vec2 hi = it.rect.xy + it.rect.zw;
|
|
||||||
if (sp.x < lo.x || sp.y < lo.y) continue;
|
|
||||||
if (sp.x >= hi.x || sp.y >= hi.y) continue;
|
|
||||||
|
|
||||||
vec2 t = (sp - it.rect.xy) / it.rect.zw;
|
|
||||||
vec2 uv = mix(it.uv.xy, it.uv.zw, t);
|
|
||||||
|
|
||||||
float sdf = texture(
|
|
||||||
sampler2D(uiTextures[nonuniformEXT(pc.fontTextureSlot)],
|
|
||||||
uiSamplers[nonuniformEXT(pc.fontSamplerSlot)]),
|
|
||||||
uv
|
|
||||||
).r;
|
|
||||||
|
|
||||||
// Distance in atlas-pixels (negative inside the glyph).
|
|
||||||
float dAtlas = (ON_EDGE - sdf) * DIST_SCALE;
|
|
||||||
|
|
||||||
// Atlas-px per screen-px along this glyph's transform — keeps AA crisp
|
|
||||||
// at any rendering size. uvSpan * atlasSize / screenSpan.
|
|
||||||
vec2 uvSpan = it.uv.zw - it.uv.xy;
|
|
||||||
// FontAtlas::kAtlasSize = 1024.
|
|
||||||
vec2 atlasPerScreen = (uvSpan * 1024.0) / it.rect.zw;
|
|
||||||
float scalePx = max(atlasPerScreen.x, atlasPerScreen.y);
|
|
||||||
// 1-screen-px AA band, expressed in atlas-pixel units of dAtlas.
|
|
||||||
float band = max(scalePx, 0.0001);
|
|
||||||
|
|
||||||
float a = clamp(0.5 - dAtlas / band, 0.0, 1.0);
|
|
||||||
if (a <= 0.0) continue;
|
|
||||||
|
|
||||||
vec4 src = vec4(it.color.rgb, it.color.a * a);
|
|
||||||
dst = uiBlendOver(dst, src);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
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] = uiGlyphHeap[pc.hdr.itemBuffer].items[idx].rect;
|
||||||
|
s_uv[lid] = uiGlyphHeap[pc.hdr.itemBuffer].items[idx].uv;
|
||||||
|
s_color[lid] = uiGlyphHeap[pc.hdr.itemBuffer].items[idx].color;
|
||||||
|
keep = uiAabbOverlapsTile(s_rect[lid].xy, s_rect[lid].xy + s_rect[lid].zw,
|
||||||
|
tileMin, tileMax);
|
||||||
|
}
|
||||||
|
s_keep[lid] = keep ? 1u : 0u;
|
||||||
|
barrier();
|
||||||
|
|
||||||
|
if (lid == 0u) {
|
||||||
|
uint n = 0u;
|
||||||
|
uint lim = min(UI_CHUNK, pc.hdr.itemCount - base);
|
||||||
|
for (uint k = 0u; k < lim; ++k)
|
||||||
|
if (s_keep[k] != 0u) s_order[n++] = k;
|
||||||
|
s_count = n;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
|
||||||
|
float sdf = texture(
|
||||||
|
sampler2D(uiTextures[nonuniformEXT(pc.fontTextureSlot)],
|
||||||
|
uiSamplers[nonuniformEXT(pc.fontSamplerSlot)]),
|
||||||
|
uv
|
||||||
|
).r;
|
||||||
|
|
||||||
|
// Distance in atlas-pixels (negative inside the glyph).
|
||||||
|
float dAtlas = (ON_EDGE - sdf) * DIST_SCALE;
|
||||||
|
|
||||||
|
// Atlas-px per screen-px along this glyph's transform — keeps AA crisp
|
||||||
|
// at any rendering size. uvSpan * atlasSize / screenSpan.
|
||||||
|
vec2 uvSpan = s_uv[c].zw - s_uv[c].xy;
|
||||||
|
// FontAtlas::kAtlasSize = 1024.
|
||||||
|
vec2 atlasPerScreen = (uvSpan * 1024.0) / s_rect[c].zw;
|
||||||
|
float scalePx = max(atlasPerScreen.x, atlasPerScreen.y);
|
||||||
|
// 1-screen-px AA band, expressed in atlas-pixel units of dAtlas.
|
||||||
|
float band = max(scalePx, 0.0001);
|
||||||
|
|
||||||
|
float a = clamp(0.5 - dAtlas / band, 0.0, 1.0);
|
||||||
|
if (a <= 0.0) continue;
|
||||||
|
|
||||||
|
vec4 col = s_color[c];
|
||||||
|
vec4 src = vec4(col.rgb, col.a * a);
|
||||||
|
dst = uiBlendOver(dst, src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue