Skip to content

Yield region-paced code at the next H/V IRQ latch - #276

Open
pogoflow wants to merge 2 commits into
RetroPortingToolKit:mainfrom
pogoflow:feat/region-paced-irq-deadline
Open

pogoflow wants to merge 2 commits into
RetroPortingToolKit:mainfrom
pogoflow:feat/region-paced-irq-deadline

Conversation

@pogoflow

Copy link
Copy Markdown
Contributor

As I reported on #251, region-paced code only sees the host's frame-end deadline, so a raster IRQ that latches inside compiled code is taken at the next deadline yield, a later instruction boundary than the interpreter's. The interpreter syncs the beam after every opcode and yields at the first boundary after the comparator latches. I offered a fix there; this is it.

The beam frame driver now arms an IRQ deadline around its CPU slice: interp_bridge_set_irq_deadline(next_irq_latch), where the callback returns beamMasterLast + snes_master_clocks_until_irq() + 1 (the park step's rule). While armed and I is clear, interp_bridge_lle_master_deadline_reached is also true once inIrq is set or the master clock reaches that latch. The callback is asked at every such poll, so $4200 and $4207–$420A writes and the $4211 acknowledge take effect at the next boundary without any invalidation. (On #251 I described a cached prediction refreshed on those writes; asking live is simpler and needs no change to snes.c.)

Only a poll that directly follows cpu_aot_instruction_boundary() takes it. Only region-paced code emits that call, and only there is master_cycles the exact clock of an instruction boundary at the poll. Default emission keeps its behavior; its poll gains a single flag test. No emitter change, no new runtime flag, no new link dependencies for the bridge.

The equivalent change in my fork, on top of #251, removed all raster-IRQ divergences between native and interpreted runs in the game's mode-7 scenes (the numbers are in my #251 comment). I can't run that game on plain upstream yet, so this exact code is covered by the test below, not by the game.

Test: tests/interp816/bridge_test.c S8i. It covers:

  • yield at the latch
  • no yield with I set
  • a comparator reprogrammed inside the body moves the deadline either way
  • a pending IRQ yields; an acknowledged one does not
  • an aggregate poll, a stale boundary and a disarmed deadline do not yield

Results (MinGW gcc 16.2, Windows):

  • bridge_test 471/471, also with SNESRECOMP_INTERP_FUSED_ROM_READ 0 and 1
  • interp816_test 89/89, interrupt_tail_test 27/27
  • targeted pytest (region pacing, APU boundary, bridge contract, interrupt tail, beam events and state, timing identity) 28 passed
  • tests/v2 614 passed; the one failure needs the lib/recomp-net submodule, which isn't checked out here

Not addressed here:

  • Interpreted gaps nested inside paced code (interp_tier_dispatch_tail) poll neither the frame deadline nor this one.
  • With I clear, a paced build now makes one indirect call per instruction boundary. If that shows up in profiles, the prediction can be cached and invalidated on the same register writes.

The interpreter syncs the beam after every opcode and yields at the first
instruction boundary after the H/V comparator latches. Region-paced code
polls only the host's master deadline, which the beam frame driver sets to
the frame end, so a raster IRQ that latches inside compiled code is taken at
the next deadline yield, at a later instruction boundary than the
interpreter takes it.

The driver now arms an IRQ deadline around its CPU slice with a callback
that returns the master clock at which the beam walk latches the next match
(beam + snes_master_clocks_until_irq + 1, the park step's rule). While it is
armed and I is clear, interp_bridge_lle_master_deadline_reached is also true
once inIrq is set or the master clock reaches that latch. The callback is
asked at every such poll, so writes to $4200 and $4207-$420A and the $4211
acknowledge take effect at the next boundary without any invalidation.

Only a poll that directly follows cpu_aot_instruction_boundary() takes the
IRQ deadline. Region-paced emission is the only emitter that reports
instruction boundaries, and only there is master_cycles the exact clock of
an instruction boundary at the poll; an aggregate block polls before its
block-level charge, so a mid-block latch could not be placed on the
interpreter's boundary anyway. Gating on the reported boundary leaves
default emission's behavior unchanged and adds only a flag test to its
deadline poll, with no new runtime flag and no emitter change. The bridge's
own boundary call after a paired bounce is followed by interpreted opcodes
that charge clocks first, so its mark never matches a later compiled poll.

Test: tests/interp816/bridge_test.c S8i (yield at the latch; no yield with
I set; a comparator reprogrammed inside the body moves the deadline either
way; a pending IRQ yields; an acknowledged one does not; an aggregate poll,
a stale boundary and a disarmed deadline do not).
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