From 225556532adf2b4598526ee962c354be5fe14209 Mon Sep 17 00:00:00 2001 From: catbot Date: Thu, 18 Jun 2026 13:26:32 +0000 Subject: [PATCH] perf(ui): skip the destination read-modify-write on empty tiles (#127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standalone per-category UI shaders (quads/circles/images/text) unconditionally imageLoad the destination pixel up front and imageStore it at the end, paying a full read-modify-write even on tiles no item touches — the common case for a sparse UI. The fused uber-kernel already amortizes a single load/store across all four categories; the standalone Dispatch* path has no such umbrella. Defer the imageLoad to just before the first surviving blend (`loaded` flag) and skip the imageStore unless something was actually blended. Output is bit-identical: before the first blend `dst` is unused, and a skipped store leaves the pixel exactly as a no-op load+store would have rewritten it. Co-Authored-By: Claude Opus 4.8 --- shaders/ui-circles.comp.glsl | 22 +++++++++++++++------- shaders/ui-images.comp.glsl | 22 +++++++++++++++------- shaders/ui-quads.comp.glsl | 22 +++++++++++++++------- shaders/ui-text.comp.glsl | 22 +++++++++++++++------- 4 files changed, 60 insertions(+), 28 deletions(-) diff --git a/shaders/ui-circles.comp.glsl b/shaders/ui-circles.comp.glsl index eea0c93..b5cb547 100644 --- a/shaders/ui-circles.comp.glsl +++ b/shaders/ui-circles.comp.glsl @@ -23,12 +23,16 @@ 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; - } + // Defer the destination read-modify-write: a sparse UI leaves most tiles + // untouched, so only load the pixel when the first surviving item blends + // over it (`loaded`), and only store when something actually touched it. + // The fused kernel amortizes a single load/store across all categories; the + // standalone Dispatch* path has no such umbrella and otherwise pays a full + // read-modify-write per empty tile. + vec4 dst = vec4(0.0); + vec2 sp = vec2(0.0); + bool loaded = false; + if (valid) sp = vec2(screenPx) + 0.5; vec2 tileMin, tileMax; uiTileBounds(tileMin, tileMax); @@ -90,11 +94,15 @@ void main() { } if (src.a <= 0.0) continue; + if (!loaded) { + dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); + loaded = true; + } dst = uiBlendOver(dst, src); } } barrier(); } - if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); + if (loaded) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); // loaded ⇒ valid } diff --git a/shaders/ui-images.comp.glsl b/shaders/ui-images.comp.glsl index a4424a6..bafb2f5 100644 --- a/shaders/ui-images.comp.glsl +++ b/shaders/ui-images.comp.glsl @@ -24,12 +24,16 @@ 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; - } + // Defer the destination read-modify-write: a sparse UI leaves most tiles + // untouched, so only load the pixel when the first surviving item blends + // over it (`loaded`), and only store when something actually touched it. + // The fused kernel amortizes a single load/store across all categories; the + // standalone Dispatch* path has no such umbrella and otherwise pays a full + // read-modify-write per empty tile. + vec4 dst = vec4(0.0); + vec2 sp = vec2(0.0); + bool loaded = false; + if (valid) sp = vec2(screenPx) + 0.5; vec2 tileMin, tileMax; uiTileBounds(tileMin, tileMax); @@ -80,11 +84,15 @@ void main() { ); vec4 src = sampled * s_tint[c]; if (src.a <= 0.0) continue; + if (!loaded) { + dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); + loaded = true; + } dst = uiBlendOver(dst, src); } } barrier(); } - if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); + if (loaded) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); // loaded ⇒ valid } diff --git a/shaders/ui-quads.comp.glsl b/shaders/ui-quads.comp.glsl index 24e7daf..6bba7c4 100644 --- a/shaders/ui-quads.comp.glsl +++ b/shaders/ui-quads.comp.glsl @@ -27,12 +27,16 @@ 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; - } + // Defer the destination read-modify-write: a sparse UI leaves most tiles + // untouched, so only load the pixel when the first surviving item blends + // over it (`loaded`), and only store when something actually touched it. + // The fused kernel amortizes a single load/store across all categories; the + // standalone Dispatch* path has no such umbrella and otherwise pays a full + // read-modify-write per empty tile. + vec4 dst = vec4(0.0); + vec2 sp = vec2(0.0); + bool loaded = false; + if (valid) sp = vec2(screenPx) + 0.5; vec2 tileMin, tileMax; uiTileBounds(tileMin, tileMax); @@ -93,11 +97,15 @@ void main() { } if (src.a <= 0.0) continue; + if (!loaded) { + dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); + loaded = true; + } dst = uiBlendOver(dst, src); } } barrier(); // done reading shared for this chunk before it's overwritten } - if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); + if (loaded) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); // loaded ⇒ valid } diff --git a/shaders/ui-text.comp.glsl b/shaders/ui-text.comp.glsl index a64abbf..b34a485 100644 --- a/shaders/ui-text.comp.glsl +++ b/shaders/ui-text.comp.glsl @@ -31,12 +31,16 @@ 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; - } + // Defer the destination read-modify-write: a sparse UI leaves most tiles + // untouched, so only load the pixel when the first surviving glyph blends + // over it (`loaded`), and only store when something actually touched it. + // The fused kernel amortizes a single load/store across all categories; the + // standalone Dispatch* path has no such umbrella and otherwise pays a full + // read-modify-write per empty tile. + vec4 dst = vec4(0.0); + vec2 sp = vec2(0.0); + bool loaded = false; + if (valid) sp = vec2(screenPx) + 0.5; vec2 tileMin, tileMax; uiTileBounds(tileMin, tileMax); @@ -102,11 +106,15 @@ void main() { vec4 col = s_color[c]; vec4 src = vec4(col.rgb, col.a * a); + if (!loaded) { + dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); + loaded = true; + } dst = uiBlendOver(dst, src); } } barrier(); } - if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); + if (loaded) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); // loaded ⇒ valid } -- 2.54.0