Skip to content

Compiled resume on region-paced code (generate --aot-resume) - #283

Open
r-bin wants to merge 2 commits into
RetroPortingToolKit:mainfrom
r-bin:aot-resume-main
Open

r-bin wants to merge 2 commits into
RetroPortingToolKit:mainfrom
r-bin:aot-resume-main

Conversation

@r-bin

@r-bin r-bin commented Nov 11, 2026

Copy link
Copy Markdown

--region-paced seems to have solved a lot of problems for me. This is an extension, so Evermore can use the native code as often as possible.

Summary

With --instruction-blocks --region-paced, compiled code checks the master-clock deadline before every instruction, so a CPU
slice can stop at any instruction. The scheduler then interprets the rest of the routine, and its callers after its return,
until the next call bounces. A host that slices often (DRAM refresh points, vblank edges, single steps around NMI enables)
ends up mostly interpreted: about 13 % of Secret of Evermore's instructions ran compiled.

--aot-resume gives each region-paced variant a resume switch, so code a slice stopped in goes on compiled:

 deadline reached in compiled code
   unwind to the scheduler
+  record each abandoned frame (S, entry S, return contract)
 scheduler at PC
-  interpret until the next call bounces
+  frame recorded at this S?  re-enter that variant at PC (cpu_dispatch_resume_paired)
+  no frame known?            enter as a frame one below S; its RTS/RTL hands back the popped PC

Off by default; the flag is part of the generation identity only when on (existing recipe digests do not change).
SNESRECOMP_AOT_RESUME=0 at run time turns it off, =2 keeps suspended frames only.

What keeps the tiers alike:

  • An instruction compiled code hands back in place (unmodeled, MMIO, a pointer it cannot preview) runs once in the
    interpreter before resuming there again.
  • The prologue's stack-window fallback hands back the resume point, not the variant's entry.
  • A frameless frame's normal return (a bound routine that ran its own RTS/RTL) goes on at the address it popped.
  • A compiled RTI goes on at the popped PC while the scheduler owns the run, so interrupt handlers resume too.
  • The JSL envelope restores PB after a normal return even with an unwind pending.
  • Each region-paced instruction that stays compiled reports itself to g_interp_bridge_pc_hook (cpu_aot_insn_begin),
    as the interpreter does.
  • The beam is synced before compiled code reads PPU, joypad and $42xx registers.

Related: #276 adds yields at H/V IRQ latches to region-paced code; each such stop would otherwise hand the rest of the
routine to the interpreter, so the two complement each other. The compiled RTI here also covers what #251's Secret of Mana
run needed as a test-only addition (resumable interrupt handlers).

Evidence

Secret of Evermore (USA), two MesenCE 2.2.1 movies from power-on (8,592 and 23,756 NMIs), WRAM, VRAM, CGRAM and OAM
compared at every NMI, nothing copied or masked, on main 80a41241:

  • Before (--instruction-blocks --region-paced): both replays identical; about 13 % of instructions compiled; the
    second replay runs in 93 s.
    After (--aot-resume added): both replays identical; 94.05 % / 92.43 % compiled; the second replay runs in 79 s
    (15 % faster end to end, PPU and APU included). Also identical with resume off at run time and all-interpreted.

pytest tests/v2 tests/test_snes_cycles.py: 638 passed. New: tests/v2/test_aot_resume.py (a resume entry per instruction,
off by default, needs region pacing, the fallback hands back the resume point, the resume table keeps the longest variant,
the flag check). The region-pacing harness gets stubs for the two resume helpers and cpu_aot_insn_begin.

Recipe: https://github.com/r-bin/SecretOfEvermoreRecomp/blob//docs/pr/README.md#reproduce

Merge Danger

Door: two-way. Opt-in at generation; off at run time with SNESRECOMP_AOT_RESUME=0.

Blast Radius: small for projects that do not pass --aot-resume. Two changes reach everyone using region pacing or the
bridge: region-paced instructions now call cpu_aot_insn_begin (a hook call, no timing change), and the beam is synced
before compiled register reads.

Who gains: projects that pass --region-paced and whose host stops the CPU often (per-line slices, single steps around
interrupts, IRQ-latch yields as in #276). With beam_frame_driver's few stops per field the gain is small; without region
pacing nothing changes.

Limits: measured on one game; the speed gain is one replay's wall time. Not yet run together with #276. Frameless entries
assume a frame one below S; a routine that returns from such an entry in an unusual way goes back to the interpreter at
the popped PC.

r-bin and others added 2 commits November 11, 2026 09:49
With --instruction-blocks --region-paced a CPU slice can stop at any
instruction. The scheduler then interprets the rest of the routine, and its
callers after its return, until the next call bounces. --aot-resume gives each
region-paced variant a switch in its prologue (g_aot_resume_table in
dispatch_v2.c), so code a slice stopped in goes on compiled: a frame suspended
at a deadline is re-entered with its own entry S and return contract, and code
the scheduler reached by a jump is entered as a frame one below S.
SNESRECOMP_AOT_RESUME=0 at run time turns it off, =2 keeps suspended frames
only. Off by default; the flag is part of the generation identity only when on.

What keeps the tiers alike:
- An instruction compiled code hands back in place (a region-paced fallback:
  unmodeled, MMIO, a pointer it cannot preview) runs once in the interpreter
  before resuming there again.
- The prologue's stack-window fallback hands back the resume point, not the
  variant's entry.
- A frameless frame's normal return (a bound routine that ran its own RTS/RTL)
  goes on at the address it popped.
- A compiled RTI goes on at the popped PC while the scheduler owns the run
  (interp_bridge_aot_rti), so interrupt handlers resume too.
- The JSL envelope restores PB after a normal return even with an unwind
  pending.
- Each region-paced instruction that stays compiled reports itself to
  g_interp_bridge_pc_hook (cpu_aot_insn_begin), as the interpreter does.
- The beam is synced before compiled code reads PPU, joypad and $42xx
  registers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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