From 71bbd10ad0ba87e25e256b617e0b3446abd37fb6 Mon Sep 17 00:00:00 2001 From: libretroadmin Date: Sun, 14 Jun 2026 15:30:30 +0000 Subject: [PATCH] tengen: auto-reload + trigger-on-reach-0 for RAMBO-1 IRQ (#544) The Tengen 800032 (RAMBO-1) IRQ counter on real hardware auto-reloads from its latch when it would otherwise underflow, and the IRQ triggers when the counter REACHES zero (going from 1 to 0) rather than on the underflow step that follows. Upstream's earlier model triggered on underflow (count 0 -> 0xFF) and then continued decrementing through 0xFE, 0xFD, ... until the game wrote a reload register ($C000/$C001/$E000/$E001 with rmode==1). For titles that don't write the reload register on every IRQ - Skull & Crossbones in 2-player mode is the documented case (libretro docs Compatibility entry), but Hard Drivin', Klax, Rolling Thunder, Road Runner, Xenophobe etc. all use the same mapper - subsequent IRQs after the first underflow are silently swallowed, displacing the status-bar split point and producing the screen-shake / glitch reported in #544. Backport reference: negativeExponent's libretro-fceumm_next commit 7f277a8 ("Cleanup mapper 64/158") plus the Mesen RAMBO-1 model. This patch is the minimal algorithmic delta against upstream: - RAMBO1IRQHook (CPU-cycle mode, IRQmode==1): trigger on IRQCount reaching 0, auto-reload from IRQLatch on the next clock after the trigger. - RAMBO1HBHook (scanline / A12-approximation mode, IRQmode==0): same change. rmode is still set on IRQ fire so existing register-reload semantics on $C000/$C001/$E000/$E001 are preserved bit-for-bit - the writes do exactly what they did before, the only behaviour change is what the IRQ hook itself does between writes. - IRQLatch == 0 edge case handled (reload yields 0, which is the reach-0 trigger condition, so IRQs fire on every clock - the documented hardware behaviour at latch=0). Crucially, this does NOT change: - SFORMAT chunk layout (REGS/CMDR/MIRR/RMOD/IRQM/IRQC/IRQA/IRQL chunks all still present, same byte sizes, same order). Old savestates continue to load correctly. - The write-side dispatch (RAMBO1_Write) for $A000, $8000, $8001, $C000, $C001, $E000, $E001 - all behave exactly as before, including the rmode==1 conditional reload. - Mapper 158 (TENGEN 800037, Alien Syndrome Unl) wiring - it shares the RAMBO1 IRQ hook and gets the same fix for free. - PRG/CHR/mirroring sync logic. - Power-on and StateRestore behaviour. Build verified: tengen.c -O2 / -O3 / aarch64 -O2 clean. --- src/boards/tengen.c | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/boards/tengen.c b/src/boards/tengen.c index 6abf140..aaf49c5 100644 --- a/src/boards/tengen.c +++ b/src/boards/tengen.c @@ -48,9 +48,28 @@ static void FP_FASTAPASS(1) RAMBO1IRQHook(int a) { smallcount += a; while (smallcount >= 4) { smallcount -= 4; - IRQCount--; - if (IRQCount == 0xFF) - if (IRQa) X6502_IRQBegin(FCEU_IQEXT); + /* Mesen / _next reference: the Tengen RAMBO-1 IRQ counter + * auto-reloads from the latch when it would otherwise + * underflow, and the IRQ triggers when the counter + * REACHES zero (going from 1 to 0) rather than on the + * underflow step that follows. Upstream's earlier model + * triggered on underflow (count 0 -> 0xFF) and then + * continued decrementing through 0xFE, 0xFD, ... until + * the game wrote a reload register. Match the hardware + * model: trigger on reach-0, and on the following clock + * reload from latch automatically so the counter cycles + * continuously without needing per-IRQ register writes. */ + if (IRQCount == 0) { + IRQCount = IRQLatch; + if (IRQCount == 0 && IRQa) { + X6502_IRQBegin(FCEU_IQEXT); + } + } else { + IRQCount--; + if (IRQCount == 0 && IRQa) { + X6502_IRQBegin(FCEU_IQEXT); + } + } } } } @@ -58,9 +77,20 @@ static void FP_FASTAPASS(1) RAMBO1IRQHook(int a) { static void RAMBO1HBHook(void) { if ((!IRQmode) && (scanline != 240)) { rmode = 0; - IRQCount--; - if (IRQCount == 0xFF) { - if (IRQa) { + /* See comment in RAMBO1IRQHook above for the auto-reload / + * trigger-on-reach-0 model. Same change applies here for the + * scanline-clocked (A12 approximation) IRQ path. */ + if (IRQCount == 0) { + IRQCount = IRQLatch; + /* Latch == 0 means fire on every clock - reload yields + * 0 again, which is the reach-0 trigger condition. */ + if (IRQCount == 0 && IRQa) { + rmode = 1; + X6502_IRQBegin(FCEU_IQEXT); + } + } else { + IRQCount--; + if (IRQCount == 0 && IRQa) { rmode = 1; X6502_IRQBegin(FCEU_IQEXT); }