perf(ui): cooperative shared-memory tile culling in UI compute shaders #85
5 changed files with 349 additions and 119 deletions
|
|
@ -2,47 +2,99 @@
|
|||
#extension GL_GOOGLE_include_directive : enable
|
||||
#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 {
|
||||
UIDispatchHeader hdr;
|
||||
} pc;
|
||||
|
||||
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() {
|
||||
ivec2 screenPx;
|
||||
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
||||
bool valid = uiResolveScreenPixel(pc.hdr, screenPx);
|
||||
|
||||
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||
vec2 sp = vec2(screenPx) + 0.5;
|
||||
|
||||
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
||||
CircleItem it = LoadCircleItem(pc.hdr.itemBuffer, i);
|
||||
|
||||
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);
|
||||
vec4 dst = vec4(0.0);
|
||||
vec2 sp = vec2(0.0);
|
||||
if (valid) {
|
||||
dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||
sp = vec2(screenPx) + 0.5;
|
||||
}
|
||||
|
||||
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
|
||||
#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;
|
||||
|
||||
void main() {
|
||||
ivec2 screenPx;
|
||||
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
||||
bool valid = uiResolveScreenPixel(pc.hdr, screenPx);
|
||||
|
||||
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||
vec2 sp = vec2(screenPx) + 0.5;
|
||||
|
||||
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
||||
ImageItem it = LoadImageItem(pc.hdr.itemBuffer, i);
|
||||
|
||||
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);
|
||||
vec4 dst = vec4(0.0);
|
||||
vec2 sp = vec2(0.0);
|
||||
if (valid) {
|
||||
dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||
sp = vec2(screenPx) + 0.5;
|
||||
}
|
||||
|
||||
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
|
||||
#include "ui-shared.glsl"
|
||||
|
||||
// One workgroup per 8×8 screen tile. Each thread owns one pixel and iterates
|
||||
// every QuadItem in order, accumulating into a local dst register, so item
|
||||
// order in the buffer == draw order on screen (later items overdraw earlier).
|
||||
// One workgroup per 8×8 screen tile. The workgroup cooperatively streams the
|
||||
// QuadItem 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 runs the per-pixel accumulate over only those
|
||||
// survivors. Item order in the buffer == draw order on screen.
|
||||
layout(push_constant) uniform PC {
|
||||
UIDispatchHeader hdr;
|
||||
} pc;
|
||||
|
||||
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() {
|
||||
// NOTE: do not early-return — every thread must reach the barriers below.
|
||||
ivec2 screenPx;
|
||||
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
||||
bool valid = uiResolveScreenPixel(pc.hdr, screenPx);
|
||||
|
||||
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||
vec2 sp = vec2(screenPx) + 0.5;
|
||||
|
||||
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
||||
QuadItem it = LoadQuadItem(pc.hdr.itemBuffer, i);
|
||||
|
||||
// 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);
|
||||
vec4 dst = vec4(0.0);
|
||||
vec2 sp = vec2(0.0);
|
||||
if (valid) {
|
||||
dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||
sp = vec2(screenPx) + 0.5;
|
||||
}
|
||||
|
||||
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;
|
||||
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
|
||||
#include "ui-shared.glsl"
|
||||
|
||||
// One workgroup per 8×8 screen tile. Iterates every glyph in order; each
|
||||
// pixel keeps a local accumulator so order in the buffer == draw order.
|
||||
// One workgroup per 8×8 screen tile. The workgroup cooperatively streams the
|
||||
// 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 {
|
||||
UIDispatchHeader hdr;
|
||||
uint fontTextureSlot;
|
||||
|
|
@ -18,51 +20,93 @@ layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
|||
const float ON_EDGE = 128.0 / 255.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() {
|
||||
ivec2 screenPx;
|
||||
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
||||
bool valid = uiResolveScreenPixel(pc.hdr, screenPx);
|
||||
|
||||
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||
vec2 sp = vec2(screenPx) + 0.5;
|
||||
|
||||
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
||||
GlyphItem it = LoadGlpyhtem(pc.hdr.itemBuffer, i);
|
||||
|
||||
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);
|
||||
|
||||
// Font slots are push constants — provably dynamically uniform, so no
|
||||
// nonuniformEXT: lets the compiler hoist the descriptor load out of the
|
||||
// per-pixel glyph loop and avoid the divergent-descriptor path.
|
||||
float sdf = texture(
|
||||
sampler2D(uiTextures[pc.fontTextureSlot],
|
||||
uiSamplers[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);
|
||||
vec4 dst = vec4(0.0);
|
||||
vec2 sp = vec2(0.0);
|
||||
if (valid) {
|
||||
dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||
sp = vec2(screenPx) + 0.5;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Font slots are push constants — provably dynamically uniform, so no
|
||||
// nonuniformEXT: lets the compiler hoist the descriptor load out of the
|
||||
// per-pixel glyph loop and avoid the divergent-descriptor path.
|
||||
float sdf = texture(
|
||||
sampler2D(uiTextures[pc.fontTextureSlot],
|
||||
uiSamplers[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