diff --git a/.gitignore b/.gitignore index 571df62..c50f839 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ Thumbs.db # Crap I added stuff/* docs/* +tools/__pycache__/* # AGI game data (Sierra/fan-made AGI games used for testing; never committed) examples/agi/gamedata/ diff --git a/src/x68000/hal.c b/src/x68000/hal.c index e963cfa..0ea6e52 100644 --- a/src/x68000/hal.c +++ b/src/x68000/hal.c @@ -158,6 +158,11 @@ static void installTickIsr(void); static void uploadGfxPalette(const jlSurfaceT *src); +// serial.c: drain the tiny (~64 B) RSDRV receive ring into the port's soft ring. Called from the +// long-running paths below (present / VBL wait / input poll) so mid-frame serial bytes are never +// lost while the HAL is busy -- see the soft-ring note in serial.c. +extern void x68kSerialPump(void); + static void vdispHandler(void); static void vdispPoll(void); @@ -359,6 +364,7 @@ void jlpPresent(const jlSurfaceT *src) { uint16_t hi0, hi1, hi2, hi3; uint16_t lo0, lo1, lo2, lo3; uint8_t palBase; + uint16_t blitRows = 0; if (src == NULL) { return; @@ -378,10 +384,18 @@ void jlpPresent(const jlSurfaceT *src) { if (pd == NULL) { return; } + x68kSerialPump(); // the palette upload + spread-table build above are already line time for (y = 0; y < SURFACE_HEIGHT; y++) { if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) { continue; } + // A blitted row costs ~0.5-0.7 ms, so pump every second one: that keeps the gap near ~1 ms, + // inside the ~2.8 ms the 64-byte RSDRV ring covers at the port's fastest rate (230400). + // Clean rows are a flag test and skip the pump, so an idle present stays trap-free. + if ((blitRows & 1u) == 0u) { + x68kSerialPump(); + } + blitRows++; // Bands are 16-bit stage words = 4 pixels = 2 bytes of a 1bpp plane. firstByte = (uint16_t)(gStageMinWord[y] >> 1); rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u); @@ -502,6 +516,9 @@ void jlpInputPoll(void) { uint8_t scan; for (group = 0; group < X68K_KEYGROUP_COUNT; group++) { + // Per-group, not per-poll: the whole bitsns trap storm can pass the ~2.8 ms the 64-byte RSDRV + // ring covers at the port's fastest rate, but one group's trap never does. + x68kSerialPump(); groups[group] = (uint8_t)(_iocs_bitsns((int)group) & 0xFF); } // Write both states each poll: the bitmap is live key-DOWN data, @@ -613,6 +630,11 @@ void jlpWaitVBL(void) { guard = 0ul; while (gFrameCount == start && guard < X68K_VBL_SPIN_LIMIT) { vdispPoll(); + // Every 64 spins ~= a few hundred microseconds: comfortably inside the ~2.8 ms the 64-byte + // RSDRV ring covers at 230400. Trap cost is free here -- this is a blocking wait anyway. + if ((guard & 63ul) == 0ul) { + x68kSerialPump(); + } guard++; } } diff --git a/src/x68000/serial.c b/src/x68000/serial.c index ed772cd..d3fb711 100644 --- a/src/x68000/serial.c +++ b/src/x68000/serial.c @@ -46,17 +46,150 @@ // write instead of hanging the frame. Same guard the ST HAL uses. #define X68K_TX_SPIN_LIMIT 200000ul +// ----- Soft receive ring ----------------------------------------------------- +// +// RSDRV.SYS's own receive ring is tiny -- MEASURED at ~64 bytes on MAME: a live capture showed a +// 203-byte frame delivered as its first 63 bytes, a 139-byte hole, then its final byte, exactly the +// signature of a 64-entry ring filling while the client was busy and discarding until drained. Every +// other JoeyLib target rides a big interrupt-fed ring (DOS hardware IRQ ring, ST XBIOS IOREC, and the +// 8-bit clients' IRQ rings), so only this port could lose mid-frame bytes whenever the caller went +// deaf for longer than ~66 ms at 9600 baud -- and jlpPresent's full-stage blit, jlpWaitVBL's V-DISP +// spin, and jlpInputPoll's IOCS trap storm all can. The fix: drain the IOCS ring into this soft ring +// from INSIDE those long-running paths (x68kSerialPump below), so the 64-byte hardware-side window +// only ever has to cover the gap between pumps, never a whole present. No ISR is involved -- the +// pump is a plain call -- so none of the vdispst-style vector hazards apply. +// +// SIZED FOR THE FASTEST LINE, not just the 9600 default: this port inherits RSDRV.SYS's boot rate +// (the mode word is deliberately not written), and RSDRV's table reaches 230400, where the 64-byte +// IOCS ring covers only ~2.8 ms -- so every pump site keeps its interval near 1 ms (present: every +// 2 blitted rows; input poll: every bitsns group; VBL wait: every 64 spins). 4 KB here holds ~178 ms +// of full-rate 230400 traffic, longer than the worst deaf window (a >100 ms all-dirty present), so +// even an unpaced burst -- the server's flowWait timeout degrade streams a whole screen without acks +// -- fits while the loop is busy. It is plain RAM on a 4 MB machine; do not shrink it to save bytes. +#define X68K_RX_RING_CAP 4096u + +// Shared with x68kSccRxIsr.s (the SCC channel A RX interrupt handler): the ISR is the producer +// (writes gX68kRxTail), the mainline is the consumer (writes gX68kRxHead). The byte is stored +// before the tail is published and the 68000 is strictly in-order, so volatile indices are the +// whole interlock. When the ISR could not be installed, x68kSerialPump below produces instead. +uint8_t gX68kRxRing[X68K_RX_RING_CAP]; +volatile uint16_t gX68kRxHead = 0u; // pop side +volatile uint16_t gX68kRxTail = 0u; // push side + +// ----- SCC channel A RX interrupt takeover ----------------------------------- +// +// The proper fix for the tiny RSDRV ring: own the receive interrupt, the way every other JoeyLib +// target does (DOS UART IRQ ring, the 8-bit ACIA rings). serial.c reads the SCC's programmed +// vector base (RR2 on channel A returns it UNmodified) and steals the two channel A receive vectors +// -- base|0x0C "character available" and base|0x0E "special condition" (Z8530 status-affects-vector +// puts the source in bits 3-1) -- pointing them at the raw handlers in x68kSccRxIsr.s. RSDRV's own +// RX handler simply never runs again; its 64-byte ring starves and the polled pump becomes a no-op. +// If the base looks wrong (VIS off, unexpected layout) nothing is stolen and the polled pump keeps +// carrying the port exactly as before -- the fallback is automatic. +#define X68K_SCC_A_CTRL ((volatile uint8_t *)0xE98005L) +#define X68K_VECTOR(v) ((volatile uint32_t *)((uint32_t)(v) << 2)) + +extern void x68kSccRxIsr(void); +extern void x68kSccSpIsr(void); + +static uint16_t x68kIrqMask(void); +static void x68kIrqRestore(uint16_t sr); +static void sccIsrInstall(void); +static void sccIsrUninstall(void); + +static bool gSccIsrInstalled = false; +static uint8_t gSccRxVec = 0u; +static uint32_t gSccOldRx = 0ul; +static uint32_t gSccOldSp = 0ul; + + +static uint16_t x68kIrqMask(void) { + uint16_t sr; + + __asm__ volatile("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d"(sr) : : "cc"); + return sr; +} + + +static void x68kIrqRestore(uint16_t sr) { + __asm__ volatile("move.w %0,%%sr" : : "d"(sr) : "cc"); +} + + +static void sccIsrInstall(void) { + uint16_t sr; + uint8_t base; + + if (gSccIsrInstalled) { + return; + } + // RR2 read is a two-access pointer sequence on the shared SCC, so no interrupt (the channel B + // mouse) may interleave. We run supervisor (jlpInit's _dos_super), so the SR mask is legal. + sr = x68kIrqMask(); + *X68K_SCC_A_CTRL = 2u; + base = *X68K_SCC_A_CTRL; + x68kIrqRestore(sr); + // With status-affects-vector the base must have bits 3-1 clear; anything else means the layout + // is not what we expect, and the polled pump stays in charge. + if (base == 0u || (base & 0x0Eu) != 0u) { + return; + } + gSccRxVec = (uint8_t)(base | 0x0Cu); + sr = x68kIrqMask(); + gSccOldRx = *X68K_VECTOR(gSccRxVec); + gSccOldSp = *X68K_VECTOR(gSccRxVec + 2u); + *X68K_VECTOR(gSccRxVec) = (uint32_t)x68kSccRxIsr; + *X68K_VECTOR(gSccRxVec + 2u) = (uint32_t)x68kSccSpIsr; + x68kIrqRestore(sr); + gSccIsrInstalled = true; +} + + +static void sccIsrUninstall(void) { + uint16_t sr; + + if (!gSccIsrInstalled) { + return; + } + sr = x68kIrqMask(); + *X68K_VECTOR(gSccRxVec) = gSccOldRx; + *X68K_VECTOR(gSccRxVec + 2u) = gSccOldSp; + x68kIrqRestore(sr); + gSccIsrInstalled = false; +} + + +// Drain everything the IOCS ring holds into the soft ring. Called from the HAL's own long-running +// paths (present / VBL wait / input poll) and from every serial entry point. With the SCC ISR +// installed this is a cheap no-op guard (RSDRV's ring never fills); without it, it is the polled +// fallback that keeps the port working, with the caller deaf-tolerant up to the SOFT ring instead +// of RSDRV's ~64 bytes. +void x68kSerialPump(void) { + if (gSccIsrInstalled) { + return; + } + while (_iocs_isns232c() != 0) { + uint16_t next = (uint16_t)((gX68kRxTail + 1u) % X68K_RX_RING_CAP); + + if (next == gX68kRxHead) { + break; // soft ring full: leave the rest in the IOCS ring rather than drop it here + } + gX68kRxRing[gX68kRxTail] = (uint8_t)(_iocs_inp232c() & 0xFF); + gX68kRxTail = next; + } +} + // ----- HAL entry points (alphabetical) ----- uint16_t jlpSerialAvailable(void) { - // _iocs_isns232c reports presence, not a count: non-zero == at least one. - return (_iocs_isns232c() != 0) ? 1u : 0u; + x68kSerialPump(); + return (gX68kRxHead != gX68kRxTail) ? 1u : 0u; } void jlpSerialClose(void) { - // IOCS path hijacks no vector -- nothing to tear down. + sccIsrUninstall(); // hand channel A RX back to RSDRV before the vectors dangle } @@ -64,6 +197,7 @@ void jlpSerialFlush(void) { while (_iocs_isns232c() != 0) { (void)_iocs_inp232c(); } + gX68kRxHead = gX68kRxTail; // the ISR may be live: only the consumer side may move } @@ -72,26 +206,30 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { if (config == NULL) { return false; } - // config is accepted but NOT applied -- see the mode-word note above. The - // port keeps whatever RSDRV.SYS set at boot. + // Line config is accepted but NOT applied -- see the mode-word note above. The + // port keeps whatever RSDRV.SYS set at boot. (An RTS/CTS enable via an + // _iocs_set232c(-1) read-modify-write was tried and REVERTED: the -1-as-query + // idiom is a _CRTMOD contract, undocumented for _SET232C -- XEiJ reads the + // RSDRV work area instead -- so the write could not be trusted.) (void)config; + sccIsrInstall(); // own the RX interrupt; falls back to the polled pump if the vectors look wrong return true; } void jlpSerialPoll(void) { - // IOCS buffers RX itself; nothing to pump. + x68kSerialPump(); } uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) { uint16_t n; + x68kSerialPump(); n = 0u; - // Gate each read on the status call first: _iocs_inp232c BLOCKS when the - // buffer is empty, which would stall the frame. - while (n < max && _iocs_isns232c() != 0) { - buf[n] = (uint8_t)(_iocs_inp232c() & 0xFF); + while (n < max && gX68kRxHead != gX68kRxTail) { + buf[n] = gX68kRxRing[gX68kRxHead]; + gX68kRxHead = (uint16_t)((gX68kRxHead + 1u) % X68K_RX_RING_CAP); n++; } return n; diff --git a/src/x68000/x68kSccRxIsr.s b/src/x68000/x68kSccRxIsr.s new file mode 100644 index 0000000..ef5f452 --- /dev/null +++ b/src/x68000/x68kSccRxIsr.s @@ -0,0 +1,59 @@ +| Sharp X68000 RS-232C receive: own the Z8530 SCC channel A RX interrupt. +| +| RSDRV.SYS's receive ring is ~64 bytes -- far too small for a binary protocol +| whose client can be busy blitting for >100 ms -- so instead of draining it by +| polling, serial.c steals the SCC's channel A RX vectors and points them here: +| every received byte lands straight in the port's big soft ring with no RSDRV +| involvement at all. This is the same take-over-the-hardware model every other +| JoeyLib target uses (the DOS UART IRQ ring, the 8-bit ACIA rings). +| +| Entered DIRECTLY from the 68000 exception vector the SCC supplies at IACK +| (WR2 base | %110x for channel A RX -- serial.c reads RR2 and computes it), so +| these are raw interrupt handlers: every register touched is saved, and both +| return with RTE. Unlike the tick hook there is NO chaining -- the byte is +| consumed here, so running RSDRV's handler afterwards would be wrong. +| +| Ring protocol (single consumer, single producer): the mainline reads gX68kRxHead, +| this ISR writes gX68kRxTail; the byte is stored BEFORE the tail moves, and the +| 68000 is strictly in-order, so no further interlock is needed. A full ring +| drops the byte -- the frame CRC upstream turns that into a clean retransmit-less +| drop rather than corruption. +| +| GAS m68k syntax, ELF target -- symbols carry NO leading underscore. + + .equ SCC_A_CTRL, 0xE98005 | channel A command/status + .equ SCC_A_DATA, 0xE98007 | channel A data (direct, no pointer) + .equ RING_MASK, 4095 | X68K_RX_RING_CAP - 1 (power of two) + + .text + .even + .globl x68kSccRxIsr + .globl x68kSccSpIsr + +| Channel A "receive character available". +x68kSccRxIsr: + movem.l %d0-%d2/%a0,-(%sp) + move.b SCC_A_DATA,%d0 | the received byte + move.w gX68kRxTail,%d1 + move.w %d1,%d2 + addq.w #1,%d2 + andi.w #RING_MASK,%d2 | next tail + cmp.w gX68kRxHead,%d2 + beq.s rxFull | ring full: drop, CRC upstream recovers + lea gX68kRxRing,%a0 + move.b %d0,(%a0,%d1.w) | store BEFORE publishing the new tail + move.w %d2,gX68kRxTail +rxFull: move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS + movem.l (%sp)+,%d0-%d2/%a0 + rte + +| Channel A "special receive condition" (overrun / framing error). The byte is +| suspect, so unlatch and discard it, clear the error, and drop the IUS -- the +| frame CRC turns the loss into a clean drop. +x68kSccSpIsr: + move.l %d0,-(%sp) + move.b SCC_A_DATA,%d0 | unlatch the offending byte + move.b #0x30,SCC_A_CTRL | WR0: error reset + move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS + move.l (%sp)+,%d0 + rte