Skip to content

perf: defer native chrome repaint during sound callback slices - #1597

Merged
benletchford merged 1 commit into
benletchford:masterfrom
rlanday:perf/defer-sound-chrome-pr
Sep 11, 2026
Merged

perf: defer native chrome repaint during sound callback slices#1597
benletchford merged 1 commit into
benletchford:masterfrom
rlanday:perf/defer-sound-chrome-pr

Conversation

@rlanday

@rlanday rlanday commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

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.

Draft #1598: retained-pixel hashing is stacked on #1597, not included in any of the five PRs above. It measured about31.2% additional SC2K CPU reduction on0.39.0 and31.6–40.4% on the refreshed0.39.1 stack; both are after #1524. Clock/load variation means that wider range is not evidence that0.39.1 made the patch better. EV is not cleared: its unrestricted current pair used2.369% more CPU; a severely throttled reverse pair is excluded. The PR remains draft for that check. A separate local bulk retained-fill prototype 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. #1598 and the separate local bulk-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.

Summary

Keep Sound Manager callback servicing at its existing boundaries, but leave native window/menu composition to the desktop frontend's presentation pass. In two matched, fixed-simulated-time SC2K city comparisons this reduced process CPU time by 40.1% and 42.0%. EV Override did not show an established benefit; its results and caveats are included below.

This is a standalone two-file change, now integrated with upstream b5f4f083 / Systemless 0.39.1 at a7689d11. The performance measurements below were collected on the earlier b4bbe723 / 0.39.0 base and are retained with that provenance; they have not yet been repeated after upstream #1595. It does not include or require #1596, the earlier trap/CopyMask experiments, dependency changes, or benchmark instrumentation. The measurement builds used the same #1596 replay driver in both arms; that distinction matters and is explained below.

Why this work was being done

The desktop frontend already divides execution into small foreground CPU batches, audio mixing/callback work, and an outer presentation pass. run_gui_cpu_slice correctly defers frame completion. However, service_pending_sound_work called run_pending_sound_work, whose completion path performed:

bounded Sound Manager callback slice
  -> repaint native window/menu chrome
  -> service refilled buffers, sound queues and channel state

outer presentation
  -> repaint native window/menu chrome again

The existing public callback API is also usable by standalone callers, so full completion is a reasonable contract there. It is unnecessary repaint work when the caller already owns presentation. A sound callback slice is not a video frame; multiple such slices may occur before presentation.

After the TrueType/retained-text work in #1524, repainting can erase and recreate glyph coverage, hash per-pixel metadata, and capture/restore that detail around clipped window drawing. A diagnostic SC2K baseline sample placed 3,333 of 3,541 main-thread samples under native chrome redraw, roughly split between internal slice completion and outer presentation. These are inclusive sampled stacks, not an exact call census or an Activity Monitor CPU reading.

The useful native-macOS-app analogy is to service audio promptly and coalesce UI work at presentation. This change avoids redundant host drawing; it does not lower the guest execution rate, skip guest callbacks, introduce a game-specific idle heuristic, drop audio, or reduce glyph resolution.

Implementation and correctness boundary

The private finish_frame: bool becomes an explicit FrameFinalization policy:

Policy Sound completion servicing Native chrome repaint Previously enabled PPC completion synchronization
Deferred Deferred Deferred Same deferred behavior as before
AudioOnly Retained Deferred Retained
Complete Retained Retained Retained

All old callers map to their previous Deferred or Complete behavior. The existing public run_pending_sound_work remains a complete slice. A new run_gui_pending_sound_work selects AudioOnly, and the desktop sound helper uses that API. Its documentation requires a caller that owns outer presentation. Other frontends are not silently switched.

The pending-work/halt checks, callback instruction limit, and per-frame reserved sound budget remain unchanged. The classic normal completion path retains sound_interrupt_dispatched || sound_work_only; it still loads ready double buffers, drains guest-written sound queues, synchronizes channel fields, and preserves callback dispatch boundaries. Zero requested mixed samples does not make those services optional.

The policy is carried through UI-yield and native-engine handoff/early-return paths, not just the ordinary 68K tail. Existing PPC render/front-buffer/VFS synchronization is not accidentally disabled. ExitToShell paths that previously repainted without audio remain audio-free.

composite_frame still performs the native chrome work and its parked-idle write-journal validation. No raw-memory shortcut, unchecked framebuffer write, or disabled retained-text observer is introduced.

A concrete intra-frame difference: guest code that overwrites a chrome pixel and reads it back after a sound callback, but before outer composition, can now see its own write instead of a callback-triggered chrome restore. That state was already possible between deferred foreground CPU slices; this change removes the additional callback restore point.

There is an important scope limit: the current chrome routine also blits window contents and maintains fullscreen/visibility state and overlays. Deferring it changes intra-frame timing, so it is not enough to argue that “decoration cannot affect the guest.” Tests and game checkpoints cover the cases below; they do not prove every possible transient guest framebuffer read or overlay interaction.

Actual CPU measurements

Intel macOS, Rust 1.96.1, published m68k 0.13.0 / ppc 0.6.0, default release optimization (fat LTO, one codegen unit). Both arms used identical dependencies, the corrected #1596 time-replay driver, and identical private phase counters. No compiler, sampler, or second emulator overlapped the production A/B runs.

Each workload ran one baseline→candidate pair and one candidate→baseline pair with fresh archive/save copies, the same input script, startup clock, 415,628-instructions/tick machine profile, and 60.15 Hz frontend clock. The acceptance metric is process user + system CPU time inside a fixed simulated-time phase, excluding launch and checkpoint PNG encoding. Host instructions and cycles are supporting measurements, not substitutes for CPU time. Fixed --max-instructions totals are not used to score these workloads.

SC2K: new city, 600 frontend ticks

Phase 1,800→2,400: January→March 1900, an unobstructed new city, not a developed-city or budget-dialog benchmark. All four runs execute exactly 31,686,472 guest instructions within the phase.

Pair Baseline CPU seconds Candidate CPU seconds CPU change Host instructions Host cycles
Baseline then candidate 11.791937 7.068146 −40.060% −42.146% −40.821%
Candidate then baseline 12.152595 7.047725 −42.006% −42.140% −41.460%

Median paired CPU change: −41.033%. Before/after CPU-speed-limit snapshots were 100 throughout except baseline 2 ending at 97. Those snapshots cannot establish perfectly fixed clock/cache conditions, but the large actual CPU saving repeats in both orders. This is not a claim of 41% lower GUI Activity Monitor usage or a measured FPS improvement.

EV Override: flight, 3,000 frontend ticks

Phase 3,300→6,300: 202,572,307 guest instructions in every run.

Pair Baseline CPU seconds Candidate CPU seconds CPU change Host instructions Host cycles
Baseline then candidate 25.867444 26.588177 +2.786% −0.010% +1.606%
Candidate then baseline 26.528963 26.515351 −0.051% −0.013% +0.574%

No EV improvement is established, and a small regression is not excluded. The paired CPU median is +1.367%; neither pair is discarded. Speed-limit snapshots were 97→100 for baseline 1, 100→100 for candidate 1, and 97→97 for both pair-2 arms. The callback census below helps explain the near-zero instruction change. The small cycle increase in both pairs must not be dismissed as proven thermal noise; code-layout/cache effects also remain possible.

Why SC2K improves but EV flight does not

The saving depends on how often an extra completion repaint occurs × how much work that repaint does. It is not a general speedup of guest instructions or the sound mixer, and it is not selected by game name.

Separate diagnostic replays with the existing SYSTEMLESS_TRACE_SOUND flag counted callback events strictly between the same phase markers. Both replays retain the exact guest progress, final logical screenshot, and captured mono output of the untraced baseline. These runs overlapped compilation and printed tracing, so none of their CPU timings are used in the tables above.

Observed sound activity SC2K city EV Override flight
Phase duration, frontend ticks 600 3,000
Double-buffer callbacks fired and completed 594 1,081
Completed callbacks per simulated second about 59.5 about 21.7
Stream buffer size 370 sample frames 1,024 sample frames
Stream format 22,050 Hz, mono, 8-bit about 22,254.5 Hz, stereo, 8-bit

EV absolutely does use guest sound callbacks. Saying it does not would be wrong. SC2K's smaller buffers require replenishment approximately every 16.8 ms, versus about 46 ms for EV's stream, so SC2K offers more callback-boundary repaint opportunities per simulated second. These counts are callback firings/completions, not an instrumented count of redraw_chrome calls: one callback can span more than one bounded CPU slice.

The more important difference is the cost of the chrome pass:

  • SC2K's measured city has visible Macintosh menu/window chrome. Its checkpoint shows the File/Speed/Options/Disasters/Windows/Newspaper menu strip and document title/frame. A full completion pass repaints that UI, including retained text and the menu-strip save/restore work around window drawing. The baseline sample directly confirms substantial chrome work below slice completion. This patch removes that extra expensive pass while retaining outer presentation.
  • EV's measured flight is a fullscreen, borderless game-drawn playfield. Setup logs show the full-screen (0, 0, 600, 800) procID=2 window and MBarHeight=0; the flight checkpoint has no Macintosh menu/title bar. redraw_chrome already gates menu drawing on visible-menu/non-fullscreen state and gates window chrome on a positive menu-bar height/non-fullscreen state. In this state those expensive native menu/window painting paths are absent. Removing extra completion calls therefore eliminates little work, even though the sound callbacks themselves are frequent. The ship, planet, radar and instrument-panel rendering are game drawing, not the redundant native chrome targeted by this patch.

This explains the contrast between about 42.14% fewer host instructions in SC2K and only 0.01% fewer in EV flight. It does not establish the cause of EV's small positive cycle/CPU deltas, nor does it imply EV's registration/dialog/menu workloads must behave like flight. It also does not predict uniformly low SC2K CPU during expensive game animations such as the spinning newspaper. Those are different workloads that still need targeted profiling.

Verification

September 8 upstream refresh: normal merge commit a7689d11 incorporates #1595's foreground dialog callback clock/preemption and nested-dialog fixes. The callback predicates introduced upstream are retained; the finalization policy does not replace them. Refreshed-head CI passes Linux build/tests, headless/package checks, licenses, and the nonblocking formatting/clippy job. The local test counts and CPU/GUI results below remain attributed to their explicitly named earlier heads; this CI run does not establish new CPU numbers or macOS visual acceptance.

  • Exact A/B acceptance: same fixture/input bytes, phase clocks and guest instructions, start/end logical PNGs, and captured mono sample count/hash; no stationary-frame or instruction-budget-exhaustion divergence. SC2K capture: 763,209 samples, hash b301ba58003db172; EV: 2,293,347, hash 96e0d7e286d6c874 (whole replay captures).
  • Other matching scripted checkpoints: Lemmings live level 1 (2,400 ticks), 3 in Three illustrated prologue (1,800), and System's Twilight illustrated introduction (2,400). The last two are not puzzle gameplay. Their smoke runs overlapped compilation, so their timings are excluded.
  • Four new library regressions cover zero/one/full callback budgets, two queued callbacks, unchanged guest clock and restored PC/SP, ready double-buffer loading, guest-written sound FIFO/state servicing, and parked-chrome repair/revocation at composition.
  • The composition oracle installs an actual guest menu through NewMenu/InsertMenu, then compares full guest RAM, logical pixels and retained subpixel metadata, and visible high-resolution RGB after outer composition. It separately asserts fewer cumulative glyph draws. Equal logical bytes alone are insufficient because a same-value write can erase retained glyph detail.
  • Completed library suite on the tested fix(headless): replay simulated time with GUI wait scheduling #1596 overlay: 5,368 passed, 0 failed, 3 ignored. Frontend: 97 passed, 0 failed, 1 ignored, 1 filtered. The only filtered test is the independently reproduced upstream metal_present::tests::minification_retains_thin_strokes_between_sample_centers; the ignored test is the opt-in actual Metal capture. Neither is counted as a pass.
  • Standalone branch local macOS reruns: library 5,367 passed, 0 failed, 3 ignored; frontend 88 passed, 0 failed, 1 ignored, 1 filtered. These use the same explicit Metal exclusion as above. The lower counts versus the overlay are fix(headless): replay simulated time with GUI wait scheduling #1596's separate headless regressions, which this PR does not include. Both full reruns completed on the published standalone head 6cc7e5f7, with ci-test, --locked --offline --features test-support, four codegen units, one build job, and two test threads.
  • CI run 34169538984 passed on the pre-refresh standalone head 6cc7e5f7: Linux build/library/desktop tests, classic 68K and PowerPC showcase assertions, headless build, documentation/package checks, dependency licenses, and the nonblocking formatting/clippy job. The known local macOS Metal exclusion above remains disclosed; green Linux CI does not erase that limitation or substitute for CI on a7689d11.
  • Interactive acceptance: the requester tested this candidate in a normal SC2K macOS window with host audio and the classic theme and reported that it works before approving publication. They also observed CPU usage exceeding 100% during parts of the test, including the spinning newspaper animation. This is a user-reported GUI peak, not a controlled baseline/candidate comparison; the PR does not claim uniformly low GUI CPU usage or that this spike is a regression. No measured GUI FPS result is available. This is useful interactive acceptance, not exhaustive visual or stereo-device equivalence.

The initial new glyph test exposed a fixture mistake: populating only the host menu cache did not install the canonical guest MenuList, so no title glyph was painted. The corrected fixture uses the actual traps and asserts visible detail before the comparison. Also, the fourth field of the RGB capture is a cumulative draw counter, not visible state; equal output with fewer draws is the intended result. These were test-only corrections, not removed correctness checks.

Alternatives considered and remaining work

  • Set finish_frame=false: rejected by source analysis, not a benchmark. That would also skip required sound queue/refill servicing and alter native synchronization.
  • Call mix_gui_audio_slice(0) afterward: not equivalent as-is; it supplies a different callback-dispatch flag and can change the callback boundary.
  • Disable all chrome repainting: not proposed. Guest writes can overwrite chrome, and the routine has the non-decorative responsibilities described above.
  • Reuse/cache unchanged chrome: still promising, but requires authoritative guest menu/window inputs, guest writes, palette/theme/font changes, ordering/clipping, and retained-detail invalidation. The earlier RGB-only cache is not assumed correct with retained glyphs.
  • Optimize hashes first: could reduce mandatory work, but does not eliminate the repeated erase/rebuild work at its source.

A separate post-change SC2K diagnostic sample found no chrome repaint under sound-slice completion, but 1,773 of 2,277 main-thread samples (77.87%) remain under outer chrome composition. This confirms the intended change in sampled stacks and leaves substantial performance work. Avoiding unnecessary menu-strip snapshot/restore or unchanged window/menu repaint is a separate next optimization, not a saving claimed by this PR. The sampled run is excluded from the CPU acceptance pairs. The user-observed spinning-newspaper CPU peak is also a specific follow-up workload: capture that transition under identical input and compare/profile baseline and candidate separately from steady-city performance.

@benletchford
benletchford force-pushed the perf/defer-sound-chrome-pr branch from a7689d1 to fec3dca Compare September 11, 2026 06:50
@benletchford

Copy link
Copy Markdown
Owner

Rebased onto current master, preserving the current callback scheduling changes, and flattened the branch to a single conventional commit. The two-file scope is unchanged. Fresh local full-library validation: 5,427 passed, 0 failed, 3 ignored. New-head CI is running; this does not claim refreshed performance measurements.

@benletchford
benletchford merged commit 11a5725 into benletchford:master Sep 11, 2026
4 checks passed
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