diff --git a/src/port/amiga/hal.c b/src/port/amiga/hal.c index 054605d..b1d4b85 100644 --- a/src/port/amiga/hal.c +++ b/src/port/amiga/hal.c @@ -24,6 +24,8 @@ #include #include +#include +#include #include #include #include @@ -45,6 +47,13 @@ extern struct Custom custom; + +// Frame-counter VBL server lives at end of file; forward-declare so +// halInit / halShutdown can install / remove it without C inferring +// implicit non-static linkage at the call sites. +static void installVblServer(void); +static void removeVblServer(void); + // ----- Constants ----- #define AMIGA_BITPLANES 4 @@ -444,6 +453,7 @@ bool halInit(const JoeyConfigT *config) { // do nothing -- their palette[0] writes the same COLOR00 once the // first LoadRGB4 fires from uploadScbAndPalette. SetRGB4(&gScreen->ViewPort, 0, 0, 0, 0); + installVblServer(); return true; } @@ -509,26 +519,55 @@ void halWaitVBL(void) { } -// VPOSR ($DFF004) upper byte: low 3 bits = vertical scanline bits -// 8..10. The bit-8 transition from 1 -> 0 marks "vertical wrap" -- -// a fresh frame. Edge-detected per call so caller (UBER, etc.) -// just polls; no IRQ server needed. -#define AMIGA_VPOSR ((volatile uint16_t *)0xDFF004UL) +// Frame counter via a VBL interrupt server (AddIntServer on +// INTB_VERTB). Polling VPOSR/VHPOSR is unreliable across chipsets +// and OS versions -- the bit positions vary and the polling rate has +// to win against the high-bit window per frame, which it doesn't +// when the caller's loop body is long. The interrupt server fires +// exactly once per VBlank regardless of caller cadence. +// +// halFrameCount just reads the volatile counter -- no edge detection +// needed in the polling path. + +static volatile uint16_t gFrameCount = 0; +static struct Interrupt gVblIntServer; +static bool gVblInstalled = false; + +// Server protocol: called by the interrupt dispatcher with A1 = +// is_Data, A6 = ExecBase. __saveds gives us the libnix data base in +// A4 so we can reference gFrameCount through the small-data ABI. +// Return Z=0 (non-zero result) to keep the chain going so other +// VBL servers further down the priority list still fire. +static __saveds ULONG vblServer(void) { + gFrameCount = (uint16_t)(gFrameCount + 1u); + return 0; +} + + +static void installVblServer(void) { + if (gVblInstalled) { + return; + } + gVblIntServer.is_Node.ln_Type = NT_INTERRUPT; + gVblIntServer.is_Node.ln_Pri = -60; + gVblIntServer.is_Node.ln_Name = (char *)"joeyFrameCount"; + gVblIntServer.is_Data = NULL; + gVblIntServer.is_Code = (void (*)())vblServer; + AddIntServer(INTB_VERTB, &gVblIntServer); + gVblInstalled = true; +} + + +static void removeVblServer(void) { + if (!gVblInstalled) { + return; + } + RemIntServer(INTB_VERTB, &gVblIntServer); + gVblInstalled = false; +} -static uint16_t gFrameCount = 0; -static uint8_t gPrevVbHi = 0; uint16_t halFrameCount(void) { - uint8_t now; - - /* Bit 0 of the upper byte = scanline bit 8. PAL frame is ~313 - * lines, NTSC ~263 -- both wrap bit 8 once per frame, which is - * what we want as the "frame edge" signal. */ - now = (uint8_t)((*AMIGA_VPOSR >> 8) & 1u); - if (gPrevVbHi && !now) { - gFrameCount++; - } - gPrevVbHi = now; return gFrameCount; } @@ -541,6 +580,9 @@ uint16_t halFrameHz(void) { void halShutdown(void) { + // Tear down the VBL server before closing the screen so the + // interrupt chain is clean if anything else is watching. + removeVblServer(); if (gScreen != NULL) { // CloseScreen should free attached UCopList, but be explicit // to catch any case where the screen close path skips it. diff --git a/src/port/iigs/hal.c b/src/port/iigs/hal.c index 3435f5e..5cad5b7 100644 --- a/src/port/iigs/hal.c +++ b/src/port/iigs/hal.c @@ -323,15 +323,28 @@ void halWaitVBL(void) { // caller (UBER, animation loops) polls fast enough that we never // miss a VBL transition. No IRQ involvement; safe in the S16 takeover // context where ToolBox interrupt setup would be intrusive. +// +// gFrameCount uses an explicit lda+adc+sta read-modify-write rather +// than `gFrameCount++` because ORCA-C lowers the post-increment to +// `inc |gFrameCount` (the only INC abs form on 65816 -- there is no +// INC long-abs). With this file in the DRAWPRIMS load segment but +// halFrameCount called from CORESYS via JSL, DBR isn't pointing at +// DRAWPRIMS's data bank, so the abs INC silently mutates the wrong +// byte and the counter never advances. The explicit lda > / sta > +// pattern uses long-mode addressing throughout, which is +// DBR-independent. static uint16_t gFrameCount = 0; static uint8_t gPrevInVbl = 0; uint16_t halFrameCount(void) { - uint8_t now; + uint8_t now; + uint16_t cnt; now = (*IIGS_VBL_STATUS & VBL_BAR_BIT) == 0; if (now && !gPrevInVbl) { - gFrameCount++; + cnt = gFrameCount; + cnt = (uint16_t)(cnt + 1u); + gFrameCount = cnt; } gPrevInVbl = now; return gFrameCount; diff --git a/src/port/iigs/input.c b/src/port/iigs/input.c index 3cede63..09772f4 100644 --- a/src/port/iigs/input.c +++ b/src/port/iigs/input.c @@ -322,11 +322,22 @@ static void pollJoystick(void) { // Update auto-disconnect counter. Both axes failing => probably no // stick. One resolves => stick is present, reset the counter. + // + // gJoyConsecutiveTimeouts uses a local-var read-modify-write rather + // than `++`. ORCA-C lowers `++` to `inc abs` (the only INC abs form + // on 65816), which depends on DBR pointing at this static's bank. + // Cross-segment JSL doesn't update DBR, so a caller in a different + // load segment would silently mutate the wrong byte. Long-mode + // lda+sta is DBR-independent. if (!xResolved && !yResolved) { - if (gJoyConsecutiveTimeouts < 0xFFFFu) { - gJoyConsecutiveTimeouts++; + uint16_t timeouts; + + timeouts = gJoyConsecutiveTimeouts; + if (timeouts < 0xFFFFu) { + timeouts = (uint16_t)(timeouts + 1u); + gJoyConsecutiveTimeouts = timeouts; } - if (gJoyConsecutiveTimeouts >= JOY_DISCONNECT_THRESHOLD) { + if (timeouts >= JOY_DISCONNECT_THRESHOLD) { gJoyDisconnectLatched = true; } gJoyAxisX[JOYSTICK_0] = 0;