Skip to content

perf(core): skip transparent glyph pixels - #411

Draft
lfkdsk wants to merge 1 commit into
pocket-stack:mainfrom
lfkdsk:perf/core-glyph-sparse-spans
Draft

perf(core): skip transparent glyph pixels#411
lfkdsk wants to merge 1 commit into
pocket-stack:mainfrom
lfkdsk:perf/core-glyph-sparse-spans

Conversation

@lfkdsk

@lfkdsk lfkdsk commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Glyph cells are mostly empty. Rasterizing hero's text scans 10.7M glyph-cell
pixels of which 1.66M carry coverage — 84.6% of the work touches transparent
pixels. On a text-denser app (note) it is 86.8%.

This builds a sparse per-row span index when an atlas is parsed, and walks only
the covered samples during full-render glyph runs. Coverage values still live in
the bitmap; the index stores coordinates.

Measured on main

At the Vita configuration — density-2 baked atlases, scale-2 output — 180-frame
medians over 15 alternating independent-process pairs:

dense sparse
hero 325.255 ms 263.670 ms −18.9%, 15/15
note 210.322 ms 141.945 ms −32.5%, 15/15

The cost is parse-time storage: hero's d2 span indexes are 144,628 B across 10
allocations, note's 84,890 B across 8. The 180 render calls after that allocate
nothing. The feature adds 6,292 B of wasm over the dense opt-out build.

Parse happens once per guest boot — every loadFontAtlas call site is a mount
loop, a PSP/Vita pak feed, or a fresh sim/tape/web world. There is no runtime
on-demand font-slot loading, so the parse cost is never paid at frame cadence.

Where it applies

Sparse spans compile in on web/wasm, the Vita and Switch software rasterizers,
and normal hosts; they compile out on PSP, Nintendo 3DS and ESP-IDF. Real
PSP and 3DS target outputs plus a forced ESP-IDF configuration each contain zero
sparse helper symbols.

Verification

Four cargo matrices compare sparse against the dense oracle on buffers
prefilled with nonzero data, covering scales 1..4, density-2 sampling and
downsampling, four-edge and disjoint clips, negative cell origins, translucent
composition with destination reads, alpha 0, a full DrawList through the real
GLYPH_RUN dispatch, and RGBA/ARGB/RGB565 targets. Both 180-frame app replays
assert per-frame hashes.

Mutation checks: treating coverage below 64 as transparent fails 6 tests and
diverges at d2/s2 frame 0 (the threshold is chosen because baked atlases contain
no bytes 1..8, so a cov <= 1 mutant would be invisible end to end); dropping
the last source row or column at density ≠ 1 fails 2 tests each.

One documented gap: a floor-instead-of-ceil mutant in the destination-run
boundary math fails its unit test but still passes both 180-frame d2/s3 replays.
The non-integer downsample branch has no frame-level coverage today — shipped
tapes are d1/s1 and Vita is d2/s2 — so it rests on the density 1..255 × scale
1..4 unit matrix.

Suite behaviour matches clean main exactly: 574 pass, 1 skip, the same five
unit failures, and the same frame-0 divergence of the committed hero-main tape
hashes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant