Skip to content

perf(presentation): specialize retained-pixel hashing - #1598

Draft
rlanday wants to merge 9 commits into
benletchford:masterfrom
rlanday:perf/pixel-index-hash
Draft

perf(presentation): specialize retained-pixel hashing#1598
rlanday wants to merge 9 commits into
benletchford:masterfrom
rlanday:perf/pixel-index-hash

Conversation

@rlanday

@rlanday rlanday commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Use a small private integer hasher for four retained-presentation tables
whose keys are exclusively usize pixel/sample offsets. SC2K's scripted
new-city phase uses 31.6–40.4% less process CPU time on the refreshed 0.39.1
stack
, on top of #1597; the earlier 0.39.0 experiment measured 30.25–32.10%.
Both are after #1524. Clock/load conditions account for part of the spread,
so this is not evidence that 0.39.1 increased the optimization's effect.

Draft: EV is not cleared. The unrestricted current EV pair used 2.369%
more CPU and 2.155% more cycles despite fewer instructions. A second pair was
severely throttled and its apparent CPU win is excluded. The current and
historical results, including unfavorable measurements, are detailed below.

Stacked on #1597.
The PR targets master, so its diff includes #1597 until that parent merges.
The incremental change from a7689d11 to this head f72155cc is only
src/memory/presentation.rs and its controls.rs test helper: 94 insertions,
13 deletions. No private benchmark code or bulk-fill prototype is included.
Standalone CI on f72155cc
passes all four jobs: Linux build/tests, headless/package, licenses and the
nonblocking formatting/clippy job. The draft status remains for the EV
performance check, not a failing correctness test. Local combined tests are
documented below.

No drawing, invalidation, memory observer, guest instruction, sound callback
or validation operation is removed. No JIT/admission threshold changes.

How this fits with the other performance work

The font/retained-text overhaul, Systemless #1524, merged in 0.39.0. The effect sizes below are SC2K process CPU time for the stated replay, not GUI Activity Monitor percentages or a cross-workload ranking.

Change Relationship SC2K evidence and rendering era
#1596: fixed-time headless scheduling Measurement prerequisite; independent of the optimizations No claimed windowed CPU saving. Corrects artificial retained-wait re-entry in older instruction-quota benchmarks.
#1597: defer sound-slice chrome Independent runtime PR; does not require #1421/#1423 or m68k #174 About 41.0% less CPU, after #1524, measured on 0.39.0 with #1596's scheduling overlay. Largest demonstrated post-overhaul saving among these open PRs. Not a new 0.39.1 rerun.
#1423: device-colour mirror Prerequisite for #1421 Historical SC2K CPU −0.9%, before #1524, on an earlier implementation/legacy driver. Not current-mirror acceptance.
#1421: theme-artwork cache Stacked on and includes #1423 Historical CPU −20.8%, including #1423, before #1524/with the legacy driver. Do not add the two PR percentages.
m68k #174: publication generations Separate opt-in CPU-core API; needs an audited Systemless integration that is not shipped in these PRs Historical CPU −4.64% on a local integration before #1524/with the legacy driver; not a current Systemless benefit.

This pixel-index-hash change previously measured about 31.2% additional CPU reduction on top of #1597, also after #1524 on 0.39.0. It is not included in any of the five other PRs above. A bulk retained-fill prototype is being tested on top of that; it has no measured effect size yet. Sequential reductions apply to remaining work, not by adding percentages; there is no directly measured all-PR total.

What the text-rendering overhaul changed

#1524 added CPU-side retained high-resolution glyph coverage alongside guest framebuffer bytes. Glyph masks are cached: the new hot work is repeatedly erasing/repainting their pixels and updating subpixel, palette and snapshot state, not necessarily rasterizing font outlines again. GPU presentation does not remove this bookkeeping.

That changes what each optimization can save. #1421 avoids rebuilding themed background artwork, but replay still performs observed pixel writes and actual title glyphs are drawn separately. #1423 makes colour resolution cheaper, not retained-pixel bookkeeping. #1597 removes whole redundant paint passes, so it also avoids their new text cost. This hash change and the separate unpublished fill work target that remaining per-pixel cost directly.

We have not run a matched before/after-#1524 experiment for the older PRs. Their percentage changes therefore cannot yet be quantified; the old numbers must not be ranked against the 41%/31% post-overhaul results as if they shared a baseline. More rendering CPU can also dilute a JIT optimization's percentage without making the JIT optimization itself slower. The mechanism explains why priorities changed, but is not proof of a particular slowdown or speedup caused by #1524 alone.

Theme is another comparison boundary: the 41% and 31% post-overhaul replays use classic-system7. #1421's non-classic theme-artwork cache is bypassed in that path, so it does not add its historical 20.8% saving to those runs. The old 5f21681 CLI default was systemless-default; the current CLI default is classic-system7. Changes in theme, scheduling and retained rendering must be separated before attributing a changed effect size specifically to #1524.

Why these tables are hot

High-resolution fonts retain additional sample coverage without changing
the guest's logical pixels or text metrics. Painting a character, clearing
its old background, saving/restoring a window or menu strip, and recoloring
a palette all consult this retained data. A same-value logical framebuffer
write can erase high-resolution glyph detail; simply skipping those writes
would not preserve behavior.

After PR1597 stopped painting chrome during individual sound slices, outer
window/menu composition remained a major SC2K cost. A diagnostic profile
put 1,773 of 2,277 main-thread samples in outer chrome. The three exclusive
hash_one, hash-map removal and SipHash-write buckets totalled 769 samples.
That profile identified an opportunity; it did not predict an exact saving.

Every lookup previously paid the default general-purpose hashing cost for
a machine-word numeric offset. The retained index is already available;
there is no arbitrary string to hash. This patch specializes that operation
without changing the collection's normal key equality, collision handling,
allocation, growth or removal semantics.

Implementation

Private collection Key and value Operation retained
SavedPixels.detail byte offset → shared detail cell Snapshot capture/restore
DetailCell.ink local sample index → ink Covered samples within one cell
Presentation.ink RGB byte offset → ink Visible retained coverage
Presentation.run_ink RGB byte offset set Preserve same-run glyph overhangs

The private PixelIndexMap<T> and PixelIndexSet aliases still use the
standard HashMap/HashSet, with BuildHasherDefault<PixelIndexHasher>.
For a single usize, the hasher multiplies the full word by an odd 64-bit
constant, then XOR-folds its upper half into its lower half. It supplies
mixed bucket/tag bits with a short sequence of integer operations. It does
not truncate a 64-bit key to 32 bits and uses ordinary portable Rust with
wrapping arithmetic. There is no architecture-specific assembly or unsafe
code. A byte-write implementation fulfills the hasher interface, but the
aliases only admit usize keys.

The mixing technique follows the existing write-journal AddressHasher;
this patch does not change that journal or share new mutable hash state.
The two private implementations differ in their typed input (usize versus
u32). Keeping this first experiment scoped to retained-presentation tables
avoids also changing the bus/journal types and their generated code. Sharing
one private helper is a reasonable follow-up/cleanup option, but has not been
measured and should not silently be substituted for the benchmarked patch.
The tuple-keyed offscreen text-run set and ordered offscreen map are left
unchanged. There is no new dependency, environment switch or public API.

Correctness and security boundaries

Hash iteration order changes. Palette recoloring writes disjoint pixel
ranges, snapshot remapping preserves each key, and the production
transform_detail callbacks are pure per-offset/value transformations.
None relies on the former randomized traversal order.

This is deliberately not a global replacement for randomized hashing.
Guest drawing can influence pixel positions: these are constrained numeric
offsets, not cryptographically trusted input. The new deterministic hasher
does not retain the default hasher's attack resistance against chosen
keys. Existing index bounds, collection equality/collision handling and
memory safety remain; hostile-key performance is a review consideration,
not something the game benchmarks prove safe in all circumstances. In
particular, Presentation.ink/run_ink are surface-bounded and each
DetailCell.ink is bounded by scale², but SavedPixels.detail can cover
offscreen/snapshot ranges too. A finite capacity bound is not by itself a
proof against expensive probing or quadratic total work from chosen keys.

The default hasher's stored state also disappears from these internal
types, changing structure layout. We have not isolated that contribution
with a padded-layout ablation. The measured win belongs to the complete
patch, not to a claim that hashing arithmetic alone explains every cycle.

Refreshed 0.39.1 measurements and remaining EV check

Both arms use upstream b5f4f083, current #1597 and identical #1596/private
phase-counter overlays, default release settings and the classic theme. Only
the two retained-presentation hash source files differ. This is separate from
the bulk-fill prototype, #1421/#1423 and m68k #174.

The scripted phase clocks, guest work, archive/input bytes, start/end logical
PNGs and captured mono audio match exactly in every pair. No compiler, sampler,
GUI or other emulator overlapped. Before/after process lists record remaining
macOS indexing/media/UI activity; this was not a completely quiescent machine.

SC2K pair Baseline CPU s Candidate CPU s CPU Host instructions Cycles
AB 7.673186 4.570319 −40.438% −35.130% −33.619%
BA 6.752183 4.620039 −31.577% −35.171% −28.718%

Both runs show a substantial CPU saving, consistent with the earlier 0.39.0
experiment. Speed-limit snapshots were 100 throughout, but cycles/CPU-ns were
3.6074→4.0204 and3.8133→3.9726; clock/load differences contribute to the wider
CPU spread. The raw paired CPU median is −36.007%. Do not interpret this as
proof that 0.39.1 increased the patch's benefit relative to the earlier31%.

EV pair Baseline CPU s Candidate CPU s CPU Host instructions Cycles
AB, limits 100/100 in both arms 25.539911 26.144852 +2.369% −1.888% +2.155%
BA, severely throttled; CPU result excluded 37.611322 32.743851 raw−12.942%, not an accepted win −1.866% +4.513%

In the second EV pair, candidate speed limits moved 100→60 and baseline 60→54.
The apparent CPU improvement is confounded and excluded; the raw two-pair
median must not be quoted as an EV benefit. The unrestricted pair is a small
CPU/cycle regression signal, not something to dismiss because instructions
fell. A clean rerun is needed; a layout effect is a hypothesis, not a diagnosed
cause. This unresolved EV result is why this PR is draft.

Completed matched CPU measurements (0.39.0, after #1524)

Intel macOS, Rust1.96.1, default production release: fat LTO, one codegen
unit, identical published dependencies and private benchmark lock. The
private replay overlay is identical between arms and is not for publication.
The baseline already contains PR1597. Runs alternate AB then BA, serially,
with no compiler, sampler, GUI or second emulator overlapping acceptance.

SC2K: frontend ticks1800→2400, exactly600simulated ticks and31,686,472guest
instructions per measured phase. CPU is process user+system time:

Pair Baseline CPU s Candidate CPU s CPU change Host instructions Host cycles
1 AB 7.054691 4.789997 -32.102% -35.111% -28.860%
2 BA 6.737045 4.698928 -30.252% -35.125% -28.024%

Median paired CPU change -31.177%. All before/after CPU speed limits
were 100%, but those snapshots do not prove constant clocks during a run.
Cycles per CPU-ns were3.7260→3.9040 and3.8379→3.9605: clock differences can
affect the size of the CPU delta. Both pairs reduce CPU time and cycles.

EV Override flight: ticks3300→6300, exactly3000ticks and202,572,307guest
instructions per phase:

Pair Baseline CPU s Candidate CPU s CPU change Host instructions Host cycles
1 AB 28.025892 28.069811 +0.157% -1.923% +1.385%
2 BA 28.767245 28.058984 -2.462% -1.921% -0.123%

No established EV CPU win; a small regression is not excluded. Pair1
limits were 100→97(base),97→100(candidate); pair2all100. The small positive
cycle result must not be silently dismissed as thermal noise. This EV
flight scene suppresses native Mac window/menu chrome and draws its own
HUD/playfield, so it has substantially less exposure to the targeted work.
That explanation does not apply to every EV dialog or scene.

All four runs per app match archive/input bytes, phase entry/exit guest
clocks/work, logical checkpoint PNGs and captured mono samples/hash exactly.
The emulator performs the same guest work; we do not normalize divergent
runs. These are not GUI Activity Monitor percentages, FPS, every transient
frame, high-resolution game-capture comparisons or stereo-device tests.
In particular, the previously reported spinning-newspaper >100%CPU peak
has not been measured by this city replay.

Validation

  • New tests exercise full-word/high-bit keys, regular pixel offsets,
    growth, replacement, entry updates, membership, removal/missing removal
    and clear against standard collections.
  • September8 refreshed combined library suite: 5,376 passed, 0 failed,
    3 ignored on 0.39.1 + current#1597 + this hash patch + the local bulk-fill
    prototype (906f7486). This is not relabeled as standalone hash-PR CI.
    Desktop on that same combined tree also completed: 88 passed, 0 failed, 1 ignored,
    with the independently reproduced upstream Metal minification test explicitly
    excluded (not passed). The earlier standalone attempt
    was interrupted by reboot, not passed. Standalone CI now passes; the clean EV regression check remains pending.
  • Existing retained glyph, same-value erase, palette, snapshot, copy,
    overlap, indexed/direct-color, control-corner and callback equivalence
    tests are required; logical PNG equality alone is insufficient.
  • Refreshed 0.39.1 checks also completed on both published-binary arms: Lemmings
    live level1 (2,400ticks), 3in3 prologue (1,800) and Twilight introduction
    (2,400). Archive/input bytes, guest clocks/work, captured mono and final logical
    PNGs match exactly. No timings from these correctness-only runs are scored.
    Twilight executes 264,750 guest instructions on each current arm versus 264,783
    on the old 0.39.0 stack; this is not a cross-upstream work-identity claim.
  • Lemmings live level1, 3in3 illustrated prologue and System's Twilight
    illustrated intro also match saved PR1597 archive/input bytes, exact
    guest progress, captured mono and final logical PNGs. These are not full
    puzzle playthroughs. Their timings are excluded because compilation
    overlapped these correctness-only replays.
  • A separate3-second SC2K diagnostic sample shows the old SipHash/hash-map
    removal symbols no longer appearing in sampled stacks. Outer chrome
    remains1,004/1,307main-thread samples. Inlining changes attribution; this
    does not mean all hashing cost vanished. No sampled-run CPU is scored.

Alternatives and next steps

This experiment has one measured runtime candidate. Do not relabel ideas
below as benchmarked failures:

  • A blanket replacement of all hashers would broaden security and workload
    risks; scope is deliberately the four numeric-offset collections.
  • Eliminating same-value framebuffer writes would lose meaningful glyph
    erases. That shortcut is not implemented.
  • Dense arrays or per-cell fixed storage might eliminate more hashing but
    need memory/sparsity and lifecycle measurements; not prototyped here.
  • Avoiding unchanged window/menu repaints is closer to how a native macOS
    app would work. It needs authoritative guest drawing, palette, window,
    menu, theme and retained-detail invalidation. This patch only makes the
    retained bookkeeping cheaper; it is compatible in principle with a later
    correct repaint-coalescing design, but their combined gain is unmeasured.
  • Removing unnecessary menu-strip snapshot/restore with proven clipping
    bounds is another preserved follow-up lead, not part of this patch.

Reproduction and provenance

Refreshed 0.39.1 private control: 86aad255, saved systemless-pr-sound-chrome-0391,
SHA256 4d9bbcebb95ddb405eff5bdaeca6ed21a71043e40b96409b0791703f3059a59a.
Refreshed private candidate: 6d3e580b, saved systemless-pr-pixel-index-hash-0391,
SHA256 5b2a295456726353cb6b7527e3109fca81c7a6d8048f704a2d55ad18dc57dc9e.
Identical private lock SHA256
ce727ab3e7fa78a2f135c431211fd759025374a7062ff123c8428056b1ad265f.
Reports: logs/pixel-index-hash-0391-{sc2k,ev}-ab.txt; host-process and thermal
snapshots accompany every raw run. Full details and the excluded EV CPU pair
are recorded in PIXEL-INDEX-HASH-EXPERIMENT-2026-09-08.md.

Earlier0.39.0 provenance:

Private baseline: 271d9410, saved systemless-pr-sound-chrome-v1, SHA256
614b0d26d789c9c826ef036775ce8b4c60bc888e684b79b020ab093d865f1c17.
Private candidate: 095424ce, saved systemless-pr-pixel-index-hash-v1, SHA256
52a104ca33a8960d076b121b545fe2e3f513426c93b3fa7254cff333eb23066d.
Private lock SHA256070e3942145e8ff5b097f49b60d59624587bb231f0b2af7c9fe1dd3250adc3b2.
Full commands, caveats and artifact paths:
.work/steady/PIXEL-INDEX-HASH-EXPERIMENT-2026-09-08.md;
reports logs/pixel-index-hash-{sc2k,ev}-ab.txt.

@benletchford

Copy link
Copy Markdown
Owner

Reviewed the retained-pixel-only delta and the stated validation. Leaving this draft unmerged: the unrestricted EV pair still reports +2.369% CPU / +2.155% cycles, while the reverse pair was excluded for throttling. A rebase alone does not resolve that acceptance gap. Please rerun clean matched AB/BA EV workloads on the current baseline, preserving equal guest progress, retained detail and audio, and document the deterministic-hasher worst-case/guest-controlled-offset risk (particularly SavedPixels.detail). Once #1597 lands, drop its already-merged changes from this stack and mark ready only when those outstanding checks are resolved.

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.

2 participants