diff --git a/shaders/ui-circles.comp.glsl b/shaders/ui-circles.comp.glsl index 9778ec9..eea0c93 100644 --- a/shaders/ui-circles.comp.glsl +++ b/shaders/ui-circles.comp.glsl @@ -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); } diff --git a/shaders/ui-images.comp.glsl b/shaders/ui-images.comp.glsl index 0b6bfb1..a4424a6 100644 --- a/shaders/ui-images.comp.glsl +++ b/shaders/ui-images.comp.glsl @@ -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); } diff --git a/shaders/ui-quads.comp.glsl b/shaders/ui-quads.comp.glsl index 26523f5..24e7daf 100644 --- a/shaders/ui-quads.comp.glsl +++ b/shaders/ui-quads.comp.glsl @@ -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); } diff --git a/shaders/ui-shared.glsl b/shaders/ui-shared.glsl index c71d991..52e29b3 100644 --- a/shaders/ui-shared.glsl +++ b/shaders/ui-shared.glsl @@ -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; +} diff --git a/shaders/ui-text.comp.glsl b/shaders/ui-text.comp.glsl index 88a0b9d..a64abbf 100644 --- a/shaders/ui-text.comp.glsl +++ b/shaders/ui-text.comp.glsl @@ -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); }