perf: optimize CPU, sprite, and color-math hot paths#66
Open
eduardovra wants to merge 4 commits into
Open
Conversation
The read/write/modify addressing modes resolved their index/target register via getattr(cpu, name) on every executed instruction, and LongRead/LongWrite/ IndirectLong* passed Reg(16, 0) as the getattr default — allocating a throwaway Reg object on every call (the default is evaluated unconditionally). The index register is only ever "" / "X" / "Y" and the modify target only "A" / "X" / "Y", so resolve them with a direct branch instead. Also drop the unused **kwargs from the M/X dispatch wrappers (the instruction table never passes keywords), avoiding a per-call empty-dict. Behaviour-preserving: verified against the Tom Harte 65816 SingleStepTests (6144 cases, all opcodes x 12, both M/X modes).
draw_objects runs once per priority level (0-3) for every scanline, so it re-scanned all 128 OAM entries up to 4x per line. Build the list of sprites intersecting the scanline once and reuse it across the priority passes, keyed on (frame, scanline) so it never goes stale. The cache attributes are transient render state, created lazily so Ppu.__init__ (a save-state-hashed module) is untouched. In draw_tiles, hoist the per-tile layer tag out of the per-pixel loop (it is constant for a tile) and skip transparent pixels before computing position and bounds. ~22% faster on a heavy sprite scene (128 sprites x 32x32: 2.34 -> 1.82 ms/frame). Rendered output (main_bgs + main_layer) is byte-for-byte identical to before.
apply_brightness_scanline did `v * brightness // 15` per channel per pixel; replace with a 256-entry lookup built once per brightness level and cached (a fade holds each level across many scanlines/frames). composite_scanline's per-pixel layer if/elif chain becomes a small participation-table lookup. Both run over the whole screen when active (color math nearly every frame; brightness during fades / light transitions), so this directly helps the "lighting changing" slowdown. composite ~36% faster (0.599 -> 0.385 ms/frame), brightness ~44% faster (0.220 -> 0.124 ms/frame). Output verified pixel-identical across all add/subtract/half/enable combinations and every brightness level.
Harnesses used to measure and verify the hot-path optimizations from the MMX save state (hold RIGHT): - profile_mmx.py compute-only per-frame timing / cProfile - bench_windowed.py end-to-end achieved FPS with a real SDL window - bench_sprites.py deterministic sprite-render micro-benchmark - diag_mmx.py correlate frame time with sprites / brightness / composite - jit_replay.py isolate JIT-warmup cost by replaying identical frames - jit_test.py A/B PyPy JIT params (pypyjit.set_param) They monkeypatch savestate._compat_hash so the MMX state loads regardless of local source edits; benchmark-only, no effect on the emulator.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Performance pass on the emulation hot paths, motivated by FPS dips in Mega Man X stage 1 (slowdowns when many sprites are on screen — e.g. trucks — and during the lighting/brightness transitions). Every change is behaviour-preserving and verified, and none touch save-state-hashed modules, so existing
.statefiles keep loading.Changes
CPU (
cpu/wdc65816/addressing_modes) — removed per-instructiongetattr(cpu, name)register lookups and throwawayReg(16, 0)allocations (the getattr default was evaluated on every call) from the read/write/modify addressing modes; dropped unused**kwargsfrom the M/X dispatch wrappers.Sprite renderer (
ppu/obj_renderer.py) —draw_objectsruns 4× per scanline (once per priority) and re-scanned all 128 OAM entries each time; now builds the intersecting-sprite list once per scanline and reuses it, keyed on(frame, scanline). Indraw_tiles, hoisted the per-tile layer tag out of the pixel loop and skip transparent pixels earlier.Color math (
ppu/color_math.py) —apply_brightness_scanlinenow uses a cached 256-entry lookup per brightness level instead of multiply/divide per channel per pixel;composite_scanlinereplaces its per-pixelif/eliflayer chain with a participation-table lookup.Benchmarks (
scripts/) — the harnesses used to measure and verify the above.Verification
test_ppu_screenshotcases fail identically onmain— pre-existing, unrelated).main.Notes / out of scope
Two slowdown sources were investigated and found not code-fixable, documented here so they aren't re-chased:
trace_limit,trace_eagerness,threshold,loop_longevity) was same-or-worse than defaults.chrt/nice), not code.