Skip to content

Support PAL - #272

Open
LunarLaurus wants to merge 4 commits into
RetroPortingToolKit:mainfrom
LunarLaurus:pal-region-v2
Open

LunarLaurus wants to merge 4 commits into
RetroPortingToolKit:mainfrom
LunarLaurus:pal-region-v2

Conversation

@LunarLaurus

@LunarLaurus LunarLaurus commented Nov 10, 2026 •

Copy link
Copy Markdown

Why

snesrecomp has always modelled an NTSC console. PAL cartridges therefore ran at the NTSC field rate, the PPU reported the wrong region in $213F bit 4, and the APU, host pacing, beam timing and coprocessor timers all used NTSC periods. PAL games ran too fast and any game that checked the console region saw the wrong machine.

What changed

  • Read the cartridge header's $FFD9 destination code during snes_loadRom. PAL codes ($02-$0C and $11) select PAL timing; everything else stays NTSC.
  • Add an explicit override: --region=auto|ntsc|pal and SNESRECOMP_REGION=auto|ntsc|pal. auto remains the default and follows the header. The override is applied before cart_load, so coprocessors created during load see the selected region.
  • Model PAL as 312 lines per field at 21281370 Hz (50.0070 Hz) beside NTSC's 262 lines at 21477272 Hz (60.0988 Hz). Scanline length stays 1364 master clocks.
  • Pace the APU and host from the selected region: the APU ratio uses the 32040 Hz DSP rate, and wall-clock pacing uses the measured field rate.
  • Carry the region through the beam and generic frame drivers, the PPU raster line count, APU frame clock, SA-1, Cx4 and DSP1 timers, interpolation/emission timing, and host clock.
  • Report PAL in $213F bit 4 (0 = NTSC, 1 = PAL).
  • Keep palTiming out of the savestate tail padding. Existing savestates keep their layout; a zeroed Snes is still NTSC, so existing code and tests behave as before.

The PAL model deliberately keeps every scanline at 1364 clocks and keeps the progressive line count for interlaced fields. It does not model the one short NTSC line or the one long PAL line; this matches the existing NTSC approximation.

Tests

  • New tests/region/region_header_test.c covers Japan/USA NTSC headers, all PAL destination codes, the SNESRECOMP_REGION override, precedence, and that cart_load sees the selected region.
  • tests/host/host_args_test.c covers --region=auto|ntsc|pal parsing and rejection.
  • Updated timing tests: tests/dma/beam_events_test.c, tests/ppu/ppu_raster_journal_test.c, tests/runtime_dispatch/apu_port_guest_time_test.c, tests/sa1/timer_span_test.c, and tests/test_audio_producer_pacing.py.
  • tests/run_c_tests.sh runs the new region test.

Validation

  • PAL check, run 29756, job 49652: passed. It covers the original ROM, a PAL-header copy, the original forced to PAL with --region=pal, and the original forced to PAL with SNESRECOMP_REGION=pal, compared against snes9x.
  • Unit tests, base suite and PAL suite: run 29757, from the same head and base (80a41241): all passed. Jobs: unit 49657, suite (base) 49658, suite (pal) 49659.

The fork PR has no GitHub Actions checks of its own; the validation above runs on the self-hosted Trampoline workflow.

Review focus

  • $FFD9 mapping and the auto/override precedence.
  • Savestate compatibility, especially the tail padding.
  • PAL timing in the coprocessors and the APU/host split (model time versus wall-clock pacing).

LunarLaurus and others added 4 commits November 9, 2026 21:12
The runtime modelled only an NTSC console: 262 lines per field, $213F bit 4
clear and the NTSC master clock in every coprocessor conversion. snes_other.c
already decoded the $FFD9 destination code into a PAL flag that nothing read.

snes_loadRom now sets Snes.palTiming from that flag before cart_load, unless
snes_set_region_override or SNESRECOMP_REGION=ntsc|pal says otherwise.
snes_region.h holds the numbers both regions use. Every timing site reads
them through the flag:

- the beam walk, the IRQ and line schedulers and the field wrap use 312 lines
  on PAL; vblank still starts at line 225
- the SLHV latch position and the DMA start alignment use the field's lines
- $213F reports bit 4
- the raster journal keys and their validation span the PAL field
- the generic and beam frame drivers run one PAL field per frame
- Cx4 and DSP-1 convert master clocks at 21281370 Hz on PAL
- the SA-1 H/V timer counts 312 lines on PAL
- the interpreter's SPC ratio and its vblank fast-forward use the PAL field

kInterpApuPerMaster keeps its name as a macro over the region, because
tests/v2/test_region_apu_boundary.py supplies its own value for that name.

The flag sits in the padding before beamMasterLast, so no offset in the
savestate blob moves. A zeroed Snes, Sa1, Cx4 or Dsp1 stays NTSC. NTSC
behaviour and savestates are therefore unchanged.
The APU frame clock counted every frame as 357368 master clocks and 17088
SPC cycles. A PAL frame is one 312-line field of 425568 master clocks. At the
runtime's nominal 50 Hz it holds 20505.6 SPC cycles, which RtlApuFramePeriod
carries as a fraction. The NTSC period keeps the old arithmetic exactly,
since its denominator is 1. The opt-in master-clock APU timeline divides by
the PAL master clock (21281370 Hz) on PAL; NTSC keeps 21477270.

The desktop host paces simulation at 50.0070 Hz when the loaded ROM is PAL
and the port left simulation_hz at 0. --region=auto|ntsc|pal overrides the
header for one run.
@LunarLaurus
LunarLaurus marked this pull request as ready for review November 12, 2026 08:49
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