[perf] Hoist per-item/per-glyph loop-invariant math into shared memory at load (ui-images/text/fused) #125

Closed
opened 2026-06-17 21:18:54 +02:00 by jorijnvdgraaf · 0 comments

Found by an adversarially-verified performance audit (confirmed real after a skeptic pass).

This issue covers hoisting loop-invariant per-item / per-glyph math out of the per-pixel inner loop into shared memory at cooperative-load time — the same technique applied in the same cooperative-load sections, so it's one pass through the shaders. (Merged from the former #126.)

Severity: medium · Effort: moderate · Category: gpu

Problem

  1. Per-pixel reciprocal of item-constant rect sizeshaders/ui-images.comp.glsl:70-71 (also ui-text.comp.glsl:76, ui-fused.comp.glsl:252,302): (sp - rect.xy) / rect.zw recomputes a vec2 reciprocal per pixel per item. rect.zw is loaded once per item into shared memory and is loop-invariant, but the compiler can't hoist it (shared mem, varying index).
  2. Per-glyph-constant SDF AA scaleui-text.comp.glsl:93-98 (also ui-fused.comp.glsl:315-318): uvSpan / atlasPerScreen / scalePx / band depend only on the glyph yet are computed per pixel-glyph (a divide + two maxes).

Both recompute, per pixel, values that are constant for the whole item/glyph.

Fix

During the cooperative load, precompute into shared slots once per item/glyph:

  • invRectSize = 1.0/rect.zw, then t = (sp - rect.xy) * invRectSize;
  • band into a float[UI_CHUNK] shared slot.

Both edits land in the same cooperative-load section of ui-text/ui-fused (and ui-images for the rect part). Minor LDS pressure increase.

Note

The SDF-band part (2) is marginal on its own — the inner loop is dominated by the SDF texture() fetch — but it rides along for free with the rect-reciprocal hoist (1) since it's the same load-section edit.

Found by an adversarially-verified performance audit (confirmed real after a skeptic pass). This issue covers **hoisting loop-invariant per-item / per-glyph math out of the per-pixel inner loop into shared memory at cooperative-load time** — the same technique applied in the same cooperative-load sections, so it's one pass through the shaders. (Merged from the former #126.) **Severity:** medium · **Effort:** moderate · **Category:** gpu ### Problem 1. **Per-pixel reciprocal of item-constant rect size** — `shaders/ui-images.comp.glsl:70-71` (also `ui-text.comp.glsl:76`, `ui-fused.comp.glsl:252,302`): `(sp - rect.xy) / rect.zw` recomputes a vec2 reciprocal per pixel per item. `rect.zw` is loaded once per item into shared memory and is loop-invariant, but the compiler can't hoist it (shared mem, varying index). 2. **Per-glyph-constant SDF AA scale** — `ui-text.comp.glsl:93-98` (also `ui-fused.comp.glsl:315-318`): `uvSpan` / `atlasPerScreen` / `scalePx` / `band` depend only on the glyph yet are computed per pixel-glyph (a divide + two maxes). Both recompute, per pixel, values that are constant for the whole item/glyph. ### Fix During the cooperative load, precompute into shared slots once per item/glyph: - `invRectSize = 1.0/rect.zw`, then `t = (sp - rect.xy) * invRectSize`; - `band` into a `float[UI_CHUNK]` shared slot. Both edits land in the same cooperative-load section of `ui-text`/`ui-fused` (and `ui-images` for the rect part). Minor LDS pressure increase. ### Note The SDF-band part (2) is marginal on its own — the inner loop is dominated by the SDF `texture()` fetch — but it rides along for free with the rect-reciprocal hoist (1) since it's the same load-section edit.
jorijnvdgraaf changed title from [perf] Per-pixel divide by item-constant rect size in inner loop (ui-images.comp.glsl) to [perf] Hoist per-item/per-glyph loop-invariant math into shared memory at load (ui-images/text/fused) 2026-06-17 21:26:02 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Catcrafts/Crafter.Graphics#125
No description provided.