// Sharp X68000 HAL -- bring-up (Phase B: chunky stage, expand on present). // // The stage stays a chunky 4bpp surface in main RAM and jlpPresent expands it // into GVRAM. That is deliberately the slow-but-correct path: it makes all of // src/generic work unmodified, so the port renders correctly before any native // primitive exists. Nothing here claims a JL_HAS_* override yet. // // GVRAM lives at $C00000. In 16-colour mode a pixel occupies the low nibble of // its own 16-bit word (the other three nibbles belong to graphic pages 1-3), so // one 4bpp source byte becomes two GVRAM words. That is 2 bytes of address // space per displayed pixel -- the reason this path is a stepping stone rather // than the destination. See the storage-model note in joey/platform.h. // // VERIFIED UNDER EMULATION (MAME 0.264 + the patches in patches/): this path // renders, and UBER's captured hashes are byte-identical to the Apple IIgs // golden reference. Input, serial and audio are separately proven -- see // claudeDocs/x68000_port.md for the evidence for each. #include #include #include "port.h" #include #include "joey/debug.h" #include #include "surfaceInternal.h" #include "x68kPlanar.h" #include "inputInternal.h" // ----- Hardware addresses --------------------------------------------------- #define X68K_GVRAM ((volatile uint16_t *)0xC00000L) // GVRAM is addressed as a 512-word-per-line grid regardless of the visible // width, so the row stride is a constant, not a function of SURFACE_WIDTH. #define X68K_GVRAM_STRIDE 512u // _iocs_crtmod screen mode. 13 = 512x512, 256 colours, 31 kHz -- same timing as // mode 12 (512x512/16) one slot below it, but with the 256-entry graphics // palette this port needs for SCB per-band palettes. There is no 320x200 mode on // this machine, so the stage is centred and letterboxed. #define X68K_CRTMOD_512_256 13 #define X68K_CRTMOD_512_16 12 // 512x512, 16 colours, 31 kHz -- text plane // _iocs_bitsns key-group numbers that carry the keys jlKeyE cares about. #define X68K_KEYGROUP_COUNT 15 // Cap on the per-poll IOCS type-ahead drain for the typed-character // queue -- purely defensive, so a wedged _B_KEYSNS can't spin forever. #define X68K_KEY_DRAIN_GUARD 32u // MC68901 MFP general-purpose I/O. Bit 4 is the CRTC's V-DISP line. #define X68K_MFP_GPIP ((volatile uint8_t *)0xE88001L) #define X68K_GPIP_VDISP 0x10u // Which plane jlpPresent writes. 1 = the TEXT plane at $E00000, which is // PLANAR at 128 bytes per row -- the stage's own layout -- so a present is a // per-row copy per plane with no pixel conversion at all. 0 = the 256-colour // GRAPHICS plane, where a pixel owns a whole 16-bit GVRAM word and every eight // pixels cost sixteen table reads to assemble. Measured on the text plane: // 0.5 bytes of traffic per pixel against GVRAM's 2, and no gather. // // The graphics path is kept because it is the one that needs no raster // interrupt to honour the SCB: the 256-entry palette holds all sixteen // 16-colour palettes at once, so (scb << 4) | nibble picks the band by value. // The text plane has ONE 16-entry palette, so per-band palettes need the // beam-chasing interrupt below. #ifndef X68K_PRESENT_TEXT #define X68K_PRESENT_TEXT 1 #endif // CRTC register 9: the raster the compare interrupt fires on. It does not // reach the CPU directly -- it drives MFP GPIP6 (channel 14, vector $4E, // exception address $138), which is the vector _CRTCRAS installs into. #define X68K_CRTC_RASTER ((volatile uint16_t *)0xE80012L) #define X68K_MFP_IERA ((volatile uint8_t *)0xE88007L) // reg 3 #define X68K_MFP_IMRA ((volatile uint8_t *)0xE88013L) // reg 9 #define X68K_MFP_GPIP6 0x40u // channel 14 #define X68K_RASTER_VEC_ADDR 0x00000138ul // vector $4E #define X68K_VBL_SPIN_LIMIT 2000000ul #define X68K_TEE_SPIN_LIMIT 200000ul // ----- SCB per-band palette (256-colour graphics plane) ---------------------- // // JoeyLib surfaces carry an SCB: one palette index per scanline. This port does // it the way the DOS port does (src/dos/hal.c:7), not the way the Atari ST does. // DOS writes `pixel byte = (scb[y] << 4) | nibble` into a 256-entry DAC, so all // 16 palettes are resident at once and the PIXEL VALUE selects the band. No // interrupt, no timing window, correct by construction. // // The ST needs a raster interrupt only because the STF shifter really has 16 // hardware colours. This machine does not: the graphics plane in 256-colour mode // has a 256-entry palette at $E82000 -- exactly 16 palettes x 16 colours. The // TEXT plane cannot do it (four 1bpp planes cap it at 16, and MAME's // get_text_pixel resolves 0-15 only), which is why this port moved off it. // // GVRAM layout in 256-colour mode, from MAME x68k_v.cpp:339-340: // colour = gvram[lineoffset0 + x] & 0x000F; // low nibble // colour |= gvram[lineoffset1 + x] & 0x00F0; // high nibble // With both graphics scroll registers 0 the two line offsets are equal, so both // nibbles come from the SAME word: the palette index is that word's LOW BYTE. // One byte write per pixel, 512 words per line. #define X68K_GVRAM_BASE 0x00C00000UL #define X68K_GVRAM_WORDS_ROW 512u // 256-entry graphics palette (x68k.cpp:965). NOT $E82200, the 16-entry text/PCG // block the old text-plane path used. #define X68K_GFX_PALETTE ((volatile uint16_t *)0xE82000L) #define X68K_PALETTE_ENTRIES (SURFACE_PALETTE_COUNT * SURFACE_COLORS_PER_PALETTE) // Video Controller register 2 ($E82600) low byte = per-plane display enable (MAME x68k_v.cpp): bits 0-3 // graphic layers, bit 4 graphic, bit 5 (0x20) TEXT plane, bit 6 sprite. _iocs_crtmod leaves the text // plane ON, so Human68k's desktop (Drv0-3, the kana/romaji status column) bleeds through the graphics; // clearing bit 5 hides it while keeping the graphic layers we draw into. #define X68K_VIDCTRL2 ((volatile uint16_t *)0xE82600L) #define X68K_VIDCTRL2_TEXT 0x0020u // Video Controller register 0 ($E82400) low 2 bits select the graphic colour depth (MAME x68k_v.cpp // switch(reg[0] & 3)): 0 = 16-colour, 1 = 256-colour palette-indexed, 3 = 65536-colour DIRECT. // _iocs_crtmod(13) sets the 512x512 CRTC but leaves this at 3 (direct), whereas this port draws 8-bit // PALETTE INDICES into GVRAM and uploads a 256-entry palette at $E82000 -- so the mode MUST be forced to // 256-colour or every index is interpreted as a near-black direct colour (the "invisible/dim" bug). #define X68K_VIDCTRL0 ((volatile uint16_t *)0xE82400L) #define X68K_VIDCTRL0_DEPTH 0x0003u #define X68K_VIDCTRL0_256 0x0001u // ----- Millisecond clock ----------------------------------------------------- // // Without this the port fell back to jlpGenericMillisElapsed(), which derives // milliseconds from the FRAME COUNTER -- and this port's counter is polled // (vdispPoll), not interrupt-driven. An op spanning more than one frame lets // V-DISP edges pass unobserved, so the measured time shrank in proportion to how // slow the op was and the two errors cancelled: every UBER row landed near // 10,000 ops/sec regardless of workload. Games pacing on jlMillisElapsed were // equally wrong. // // Fix: chain Human68k's existing MFP Timer C tick (~100 Hz) and count it. MFP // channel 5, so vector ($40 | 5) = $45 at address $45 * 4 = $114. #define X68K_TICK_VEC_ADDR 0x00000114ul // Timer C's rate is NOT assumed -- it is derived at init from the MFP's own // registers, because Human68k's programming is not what you would guess. A // measured run showed ~178 Hz, not the 100 Hz a first guess suggested: // 4 MHz / (prescale 200 * TCDR 112) = 178.6 Hz. #define X68K_MFP_TCDCR ((volatile uint8_t *)0xE8801DL) // reg 14 #define X68K_MFP_TCDR ((volatile uint8_t *)0xE88023L) // reg 17 #define X68K_MFP_CLOCK_HZ 4000000ul #define X68K_TCDCR_C_SHIFT 4u #define X68K_TCDCR_C_MASK 0x07u // Display frames to count the tick against at init. Sixteen is ~290 ms, long // enough that one frame of slop is under a percent. #define X68K_TICK_CAL_FRAMES 16u // Spin bound for that calibration: comfortably more polls than sixteen frames // need, so only a machine that is not going to toggle V-DISP ever hits it. #define X68K_TICK_CAL_GUARD 4000000ul // ----- Module state --------------------------------------------------------- static int gPrevCrtMode = -1; static bool gModeSet = false; static uint16_t gFrameCount = 0; // Saved USP from the _dos_super(0) switch, or -1 if we were already in // supervisor mode and must not switch back. static int gPrevSsp = -1; // Last sampled V-DISP level, for the frame-counter edge detect. static uint8_t gLastVdisp = 0; // True once the V-DISP interrupt handler is live; the poll then stands down. static bool gVdispInstalled = false; // Bumped by x68kTickIsr.s on every Human68k Timer C interrupt. Written from // interrupt context, so volatile; read with a single long load, which is atomic // against the handler's addq.l on a 68000. volatile uint32_t gX68kTicks = 0ul; extern void x68kTickIsr(void); extern uint32_t x68kTickChainAddr; // the JMP operand inside the ISR static uint32_t gTickBase = 0ul; static uint32_t gTickHz = 0ul; static bool gTickBaseSet = false; static bool gTickHooked = false; // ----- Prototypes ----- static uint32_t tickHzFromMfp(void); uint32_t jlpMillisElapsed(void); uint16_t jlpFrameHz(void); static void calibrateTickHz(void); static void installTickIsr(void); #if X68K_PRESENT_TEXT static void clearTextPlane(void); static void disarmRaster(void); #endif #if !X68K_PRESENT_TEXT static void buildSpreadTables(void); static void spreadSetPalBase(uint8_t palBase); static void uploadGfxPalette(const jlSurfaceT *src); #endif // 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); extern void *const gX68kVdispHandlerRef; // ----- Lifecycle ------------------------------------------------------------ bool jlpInit(const jlConfigT *config) { (void)config; // Human68k starts .X programs in USER mode, so direct I/O reads (the MFP // GPIP poll in jlpWaitVBL, and any future register banging) do not see the // hardware. Same trap the Atari ST port hit, same fix: go supervisor here // and stay there. _dos_super(0) returns the old USP, or a negative value // if we were already supervisor -- in which case do not switch back. gPrevSsp = _dos_super(0); gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing #if X68K_PRESENT_TEXT // 16 colours, because the present writes the four 1bpp TEXT planes directly // and that is what they are. _G_CLR_ON clears the GRAPHICS plane so it // cannot show through underneath, and then the text plane is wiped to get // rid of Human68k's desktop -- which the graphics-plane path could only // leave bleeding over the frame (a reg[2] &= ~0x20 write to hide it did NOT // take on MAME). Presenting INTO the text plane makes that bleed go away by // construction: we own every pixel of it. // Mode 13, NOT the 16-colour mode 12 you might expect for a 4bpp text // plane. MEASURED: crtmod 12 costs about 45% of ALL CPU throughput -- // jlKeyDown fell 63728 -> 34624 ops/sec and jlMouseX 108679 -> 60071, and // those touch nothing but RAM, so it is the mode and not the drawing. Mode // 13 keeps full speed and displays the text plane anyway: the desktop // "bleeding over our frame" that this port used to document as a cosmetic // wart was the text plane being composited ON TOP, which is exactly what // we now want. _G_CLR_ON blacks the graphics plane underneath and the wipe // takes the desktop away. _iocs_crtmod(X68K_CRTMOD_512_256); _iocs_g_clr_on(); clearTextPlane(); #else _iocs_crtmod(X68K_CRTMOD_512_256); _iocs_g_clr_on(); // clear graphics + enable the plane // crtmod 13 leaves the video controller in 65536-colour DIRECT mode (reg[0] & 3 == 3). This port // draws 8-bit palette indices, so force 256-colour palette-indexed mode or every colour renders // near-black. Root-caused + proven live on MAME (reg0 0x0003 -> 0x0001 turned a black frame into a // correct red/yellow/blue test pattern). X68K_VIDCTRL0[0] = (uint16_t)((X68K_VIDCTRL0[0] & (uint16_t)~X68K_VIDCTRL0_DEPTH) | X68K_VIDCTRL0_256); // jlpPresent draws into the GRAPHIC plane (GVRAM $C00000), NOT the text plane -- so Human68k's text // plane (the Drv0-3 + kana/romaji desktop that _iocs_crtmod leaves on) bleeds over our frame. A // reg[2] &= ~0x20 write to hide it did NOT take on MAME (the desktop stayed visible), so that cosmetic // bleed is left as a follow-up; it does not affect the graphic layers we draw into. #endif // NOTE: installing vdispHandler via _iocs_vdispst HANGS the machine -- // tested, no serial output at all, so it wedges before main() gets going. // IOCS does NOT wrap the handler: _VDISPST writes the pointer straight into // exception vector $134 (iplrom.dat $FF9DC8), exactly as _CRTCRAS does for // $138. So the handler must be a raw interrupt routine -- preserve every // register, return with RTE -- and a C function (RTS, free to clobber // d0/d1/a0/a1) can never be valid. A raw asm handler would be required; // the polled fallback below owns the frame counter until then. // Until then the polled fallback below owns the counter. gVdispInstalled = false; installTickIsr(); gModeSet = true; return true; } void jlpShutdown(void) { if (!gModeSet) { return; } // Restore Human68k's Timer C vector BEFORE dropping supervisor mode: the chained x68kTickIsr // lives in this program's RAM, and leaving $114 pointing at it after exit sends the OS clock's // next tick into freed memory (the dangling-ISR trap this port already fixed for the SCC RX // vectors in jlpSerialClose). if (gTickHooked) { static volatile uintptr_t addr = X68K_TICK_VEC_ADDR; volatile uint32_t *vec = (volatile uint32_t *)addr; uint16_t sr; __asm__ volatile ("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d" (sr)); *vec = x68kTickChainAddr; __asm__ volatile ("move.w %0,%%sr" :: "d" (sr)); gTickHooked = false; } #if X68K_PRESENT_TEXT disarmRaster(); #endif if (gPrevCrtMode >= 0) { _iocs_crtmod(gPrevCrtMode); } // Hand the mouse back the way the lines above hand back crtmod, the Timer // C vector and user mode. jlpInputInit boxed the IOCS cursor tracker into // the 320x200 stage with _MS_LIMIT, and that box lives in the IOCS work // area, which outlives the program -- leaving it set would confine the // next Human68k program's pointer to a corner of the screen. _MS_INIT // re-establishes IOCS's own defaults, and it runs AFTER the crtmod restore // above so those defaults match the screen the user is going back to. _iocs_ms_init(); gModeSet = false; if (gPrevSsp >= 0) { (void)_dos_super(gPrevSsp); // back to user mode for Human68k gPrevSsp = -1; } } // Timer C's interrupt rate, read from the MFP rather than assumed: // MFP clock / (prescaler * data register). The prescaler is a 3-bit code in the // high nibble of TCDCR; code 0 means the timer is stopped. static uint32_t tickHzFromMfp(void) { static const uint16_t kPrescale[8] = { 0u, 4u, 10u, 16u, 50u, 64u, 100u, 200u }; uint16_t code; uint16_t divisor; uint16_t data; code = (uint16_t)((*X68K_MFP_TCDCR >> X68K_TCDCR_C_SHIFT) & X68K_TCDCR_C_MASK); divisor = kPrescale[code]; if (divisor == 0u) { return 0ul; } data = *X68K_MFP_TCDR; if (data == 0u) { data = 256u; // 0 in the data register means 256 } return X68K_MFP_CLOCK_HZ / ((uint32_t)divisor * (uint32_t)data); } // Chain our counter onto Human68k's Timer C interrupt. The previous handler is // patched into the ISR's JMP operand, so it still runs and still owns the RTE -- // the OS clock keeps working. Supervisor already (jlpInit's _dos_super), so the // vector write is legal. Refuses if the vector looks unset, because chaining to // nothing would fault on the first tick. static void installTickIsr(void) { // Laundered through a volatile: gcc treats a constant pointer this close to // zero as a null dereference and -Werror=array-bounds rejects it, though // $114 is a perfectly real supervisor-writable exception vector. static volatile uintptr_t addr = X68K_TICK_VEC_ADDR; volatile uint32_t *vec; vec = (volatile uint32_t *)addr; if (*vec == 0ul) { return; } gTickHz = tickHzFromMfp(); if (gTickHz == 0ul) { return; // timer stopped: no usable clock } x68kTickChainAddr = *vec; *vec = (uint32_t)x68kTickIsr; gTickHooked = true; calibrateTickHz(); } // Count real ticks across a known number of display frames and believe THAT // rather than the MFP registers. // // Reading the prescale and data registers looks exact and is not: two runs of // the same benchmark reported 50 s and 116 s of elapsed time for runs that // really took 118 s and 100 s, a 2.7x disagreement between them, because the // rate Human68k leaves Timer C at is not the rate those registers describe by // the time a program reads them. Everything derived from the clock was wrong // by that factor, which is how a performance change came back looking three // times slower than the code it replaced. // // The display frame is the one rate on this machine that cannot drift: it // comes from the CRT mode. Sixteen frames is about 290 ms of init. static void calibrateTickHz(void) { uint8_t last; uint16_t frames = 0u; uint32_t start; uint32_t ticks; uint32_t guard; // Every spin is bounded. A machine where V-DISP never toggles would // otherwise hang here in jlInit, which is far worse than a clock that is // merely wrong: the guard gives up and keeps the register-derived rate. last = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); guard = X68K_TICK_CAL_GUARD; while ((uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u) == last) { if (--guard == 0ul) { return; } } last = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); start = gX68kTicks; guard = X68K_TICK_CAL_GUARD; while (frames < X68K_TICK_CAL_FRAMES) { uint8_t now = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); if (now != last) { last = now; if (now != 0u) { frames++; } } if (--guard == 0ul) { return; } } ticks = gX68kTicks - start; if (ticks == 0ul) { return; // ISR never fired: keep the guess } gTickHz = (ticks * (uint32_t)jlpFrameHz()) / (uint32_t)X68K_TICK_CAL_FRAMES; if (gTickHz == 0ul) { gTickHz = tickHzFromMfp(); } } // ----- SCB per-band palette -------------------------------------------------- #if X68K_PRESENT_TEXT // The text plane has one 16-entry palette, so honouring the SCB means changing // it as the beam scans. A band is a RUN of scanlines sharing an SCB value, and // only transitions matter: one CRTC raster interrupt per transition, which for // a single-palette screen (Space Taxi reports one band) is none at all. // // SCB values are freely reusable, and reuse does NOT reduce the interrupt // count: an A-B-A-B screen uses two palettes but still needs a reload at every // change, because the hardware palette was overwritten in between. The cost is // the number of TRANSITIONS, worst case one per scanline. #define X68K_MAX_BANDS SURFACE_HEIGHT extern void x68kRasterIsr(void); // src/x68000/x68kRasterIsr.s // Every palette pre-converted to the hardware's word format, so the interrupt // service copies words and does no arithmetic. Rebuilt only when a palette // changes. static uint16_t gScbPal[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; static uint16_t gBandLine[X68K_MAX_BANDS]; // display line the band starts on static uint8_t gBandPal[X68K_MAX_BANDS]; // which palette it selects static uint16_t gBandCount; static volatile uint16_t gBandNext; // service cursor static bool gRasterArmed; static uint32_t gRasterOldVec; // $138 before we took it // Interrupt service, called from the raw shim in x68kRasterIsr.s with every // register already saved -- which is what lets this be ordinary C. Loads the // current band's palette and re-points the compare register at the next band, // wrapping to band 0 for the next frame. void x68kRasterService(void) { const uint16_t *pal; uint16_t band; uint16_t col; band = gBandNext; if (band >= gBandCount) { band = 0u; } pal = gScbPal[gBandPal[band]]; for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { X68K_TEXT_PALETTE[col] = pal[col]; } band = (uint16_t)((band + 1u < gBandCount) ? (band + 1u) : 0u); gBandNext = band; *X68K_CRTC_RASTER = gBandLine[band]; } // Quantise every palette once into the form the service copies. static void buildScbPalettes(const jlSurfaceT *src) { uint16_t pal; uint16_t col; for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) { for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { gScbPal[pal][col] = x68kColorFromRgb12(src->palette[pal][col]); } } } // One band per RUN of equal SCB values. Lines are stored already offset into // display space, because that is what the compare register wants. static void buildScbBands(const jlSurfaceT *src) { uint16_t line; uint8_t idx; uint8_t prev; gBandCount = 0u; prev = 0xFFu; for (line = 0u; line < SURFACE_HEIGHT; line++) { idx = src->scb[line]; if (idx >= SURFACE_PALETTE_COUNT) { idx = 0u; } if (idx != prev) { if (gBandCount < X68K_MAX_BANDS) { gBandLine[gBandCount] = (uint16_t)(line + X68K_ORIGIN_Y); gBandPal[gBandCount] = idx; gBandCount++; } prev = idx; } } if (gBandCount == 0u) { // no line claimed a palette gBandLine[0] = X68K_ORIGIN_Y; gBandPal[0] = 0u; gBandCount = 1u; } } // A single-palette screen needs no interrupt: load it once and leave the // hardware alone. Anything else arms the compare on band 0 and lets the // service walk the list. // Mask GPIP6 and hand $138 back. This MUST run before the program exits: the // handler lives in this program's RAM, so leaving the vector pointing at it // sends the next raster interrupt into freed memory -- the same dangling-ISR // trap this port already fixed for the Timer C and SCC RX vectors. static void disarmRaster(void) { uint16_t sr; if (!gRasterArmed) { return; } __asm__ volatile ("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d" (sr)); *X68K_MFP_IMRA = (uint8_t)(*X68K_MFP_IMRA & (uint8_t)~X68K_MFP_GPIP6); *X68K_MFP_IERA = (uint8_t)(*X68K_MFP_IERA & (uint8_t)~X68K_MFP_GPIP6); if (gRasterOldVec != 0ul) { static volatile uintptr_t addr = X68K_RASTER_VEC_ADDR; *(volatile uint32_t *)addr = gRasterOldVec; } __asm__ volatile ("move.w %0,%%sr" :: "d" (sr)); gRasterArmed = false; } static void applyScbBands(void) { uint16_t col; if (gBandCount <= 1u) { disarmRaster(); for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { X68K_TEXT_PALETTE[col] = gScbPal[gBandPal[0]][col]; } return; } gBandNext = 0u; if (!gRasterArmed) { // _CRTCRAS takes the handler and the line, and owns enabling GPIP6 in // the MFP. The handler must be the raw shim, never a C function. // Remember the old vector first so shutdown can put it back. { static volatile uintptr_t addr = X68K_RASTER_VEC_ADDR; gRasterOldVec = *(volatile uint32_t *)addr; } (void)_iocs_crtcras(x68kRasterIsr, (int)gBandLine[0]); gRasterArmed = true; } else { *X68K_CRTC_RASTER = gBandLine[0]; } } #endif // X68K_PRESENT_TEXT #if !X68K_PRESENT_TEXT // ----- GRAPHICS-plane present support (only when X68K_PRESENT_TEXT is 0) ----- // Load all 16 palettes into the 256-entry graphics palette: entry p*16+c is // palette p colour c, so a pixel byte of (p << 4) | c selects it directly. // Rebuilt only when a palette actually changes; the SCB flag is deliberately // NOT consumed here, because the per-line choice is applied in the pixel // expansion, which reads src->scb directly. Same contract as // src/dos/hal.c:232-234. static void uploadGfxPalette(const jlSurfaceT *src) { uint16_t pal; uint16_t col; for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) { for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { X68K_GFX_PALETTE[(pal << 4) | col] = x68kColorFromRgb12(src->palette[pal][col]); } } } // ----- Present expansion tables ---------------------------------------------- // // The naive loop extracted one nibble per pixel with four shifts and a mask, // then stored one byte: about 8 operations per pixel over 64,000 pixels, which // put the composite game frame at 0.10x of the IIgs. // // DOS's gExpandLut cannot be copied directly: mode 13h pixels are ADJACENT // bytes so DOS fuses two into one 16-bit store, whereas GVRAM here is one // 16-bit WORD per pixel (index in the low byte), so the output bytes are two // apart. The fusion that works on this machine is the other way up -- write // WHOLE WORDS, because one 32-bit store then covers exactly two pixels. The // high byte belongs to graphics pages 2/3, which the 256-colour index does not // read (x68k_v.cpp:339-340 masks $000F | $00F0), so writing 0 there is safe. // // So the tables are indexed by a PLANE and a NIBBLE (4 source pixels) and are // pre-interleaved into word positions: gSpreadHi holds the first two of those // pixels as (px0 << 16) | px1, gSpreadLo the second two. Four planes OR // together, palBase ORs in replicated, and the result is two long stores per // four pixels -- no shifting in the inner loop at all. #define X68K_SPREAD_NIBBLES 16u // One table entry, addressed as a byte displacement from a plane base. #define SPREAD_AT(base, off) (*(const uint32_t *)((base) + (off))) // One table, laid out so the expansion loop can reach it with the 68000's // (d8,An,Xn) mode and nothing else: [plane][nibble] holds the pair of output // longs (pixels 0-1, then 2-3) that plane contributes, so ONE index serves // two stores. A plane is 16 nibbles x 8 bytes = 128 bytes, which is exactly // the reach of the signed 8-bit displacement, so two base pointers parked on // planes 1 and 3 address all four planes at -128 and 0 with no arithmetic. // // Plane 0's entries carry the row's palette base as well. Folding it in there // costs 16 stores when the SCB changes and saves an OR on every output long: // the inner loop just ORs four table reads and stores. static uint32_t gSpreadPair[X68K_BITPLANES][X68K_SPREAD_NIBBLES][2]; static uint8_t gSpreadPalBase = 0u; static bool gSpreadBuilt = false; // Bit 3 of the nibble is the LEFTMOST of its four pixels, matching the plane // byte convention (bit 7 leftmost of eight). static void buildSpreadTables(void) { uint16_t plane; uint16_t nib; uint32_t bit; for (plane = 0u; plane < X68K_BITPLANES; plane++) { bit = (uint32_t)1ul << plane; for (nib = 0u; nib < X68K_SPREAD_NIBBLES; nib++) { gSpreadPair[plane][nib][0] = (((nib >> 3) & 1u) ? (bit << 16) : 0ul) | (((nib >> 2) & 1u) ? bit : 0ul); gSpreadPair[plane][nib][1] = (((nib >> 1) & 1u) ? (bit << 16) : 0ul) | (((nib >> 0) & 1u) ? bit : 0ul); } } gSpreadPalBase = 0u; gSpreadBuilt = true; } // Re-fold the row's palette base into plane 0's entries. Sixteen pairs, and // only when the SCB actually changes from one row to the next -- which for a // screen with one palette means once per present. static void spreadSetPalBase(uint8_t palBase) { uint32_t pal2; uint16_t nib; if (palBase == gSpreadPalBase) { return; } pal2 = ((uint32_t)palBase << 16) | (uint32_t)palBase; for (nib = 0u; nib < X68K_SPREAD_NIBBLES; nib++) { uint32_t bare0 = (((nib >> 3) & 1u) ? 0x10000ul : 0ul) | (((nib >> 2) & 1u) ? 1ul : 0ul); uint32_t bare1 = (((nib >> 1) & 1u) ? 0x10000ul : 0ul) | (((nib >> 0) & 1u) ? 1ul : 0ul); gSpreadPair[0][nib][0] = bare0 | pal2; gSpreadPair[0][nib][1] = bare1 | pal2; } gSpreadPalBase = palBase; } #endif // !X68K_PRESENT_TEXT // ----- Present -------------------------------------------------------------- // Copy the surface's four planes into the TEXT PLANE at $E00000. // // This is the payoff of native planar storage. The surface rows are already in // display format -- 40 bytes per row per plane -- so present is a straight copy // with a stride change (40 -> 128), not a per-pixel expansion. Traffic per full // frame is 4 planes x 200 rows x 40 bytes = 32,000 bytes, against the 128,000 // bytes of GVRAM word writes the chunky path moved. Bus traffic is what the // measured wait states punish, so that 4x cut is the win. // // Dirty rows only, and only the marked band within a row. #if X68K_PRESENT_TEXT // Wipe the whole visible text plane once at start-up. Human68k leaves its // desktop there, and we only ever write the centred stage rectangle, so // without this the surround keeps the desktop. Long stores; one-time cost. static void clearTextPlane(void) { uint16_t plane; uint16_t i; for (plane = 0u; plane < X68K_BITPLANES; plane++) { volatile uint32_t *p = (volatile uint32_t *)(X68K_TVRAM_PLANE0 + ((uint32_t)plane * X68K_TVRAM_PLANE_STEP)); for (i = 0u; i < (uint16_t)(X68K_DISPLAY_H * (X68K_TVRAM_STRIDE / 4u)); i++) { p[i] = 0ul; } } } // Copy one row of one plane into the text plane. Both ends sit at the same // offset from a 4-aligned base -- the stage plane's row stride is 40 and the // text plane's is 128, both multiples of 4, and the centring offset // X68K_ORIGIN_BYTE is 12 -- so once the leading bytes are stepped off, the rest // moves a long at a time with no shifting. Inline rather than memcpy because a // full present is 800 of these calls and the call alone was costing more than // the bytes. static inline __attribute__((always_inline)) void x68kCopyRowToText(volatile uint8_t *dst, const uint8_t *srcRow, uint16_t count) { while (count != 0u && (((uintptr_t)dst & 3u) != 0u)) { *dst++ = *srcRow++; count--; } while (count >= 4u) { *(volatile uint32_t *)dst = *(const uint32_t *)srcRow; dst += 4; srcRow += 4; count -= 4u; } while (count != 0u) { *dst++ = *srcRow++; count--; } } // Present to the TEXT plane. The stage and the text plane are both four 1bpp // planes, so this is a copy -- no pixel conversion, no table, 0.5 bytes of bus // traffic per pixel. The SCB is honoured by the raster interrupt above rather // than by a per-pixel palette index. void jlpPresent(const jlSurfaceT *src) { X68kPlanarT *pd; const uint8_t *sp; uint16_t g; uint16_t y; uint16_t yEnd; uint16_t plane; uint16_t firstByte; uint16_t rowBytes; uint16_t blitRows = 0; // Four rows of dirty band read as ONE long. A clean row's minWord is the // 0xFF CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX, so // four clean rows read exactly 0xFFFFFFFF and the whole group skips. This // is the trick src/dos/hal.c already used and this port was missing: // visiting all two hundred rows one at a time cost roughly 0.9 ms of EVERY // present, dirty or not. A failing group still runs the per-row clean test, // so the rows written are byte-identical to the plain walk. typedef uint32_t __attribute__((may_alias)) BandGroupT; const BandGroupT *minGroups = (const BandGroupT *)(const void *)gStageMinWord; if (src == NULL) { return; } vdispPoll(); // keep the frame counter honest: see the note on vdispPoll pd = x68kSurfacePlanar(src); if (pd == NULL) { return; } // gStagePaletteDirty and gStageScbDirty live in core/surface.c; consume // both here the way src/iigs/hal.c does. A palette change alters what the // bands load, so it forces the band apply too. if (gStagePaletteDirty) { buildScbPalettes(src); gStagePaletteDirty = false; gStageScbDirty = true; } if (gStageScbDirty) { buildScbBands(src); applyScbBands(); gStageScbDirty = false; } x68kSerialPump(); for (g = 0u; g < (uint16_t)(SURFACE_HEIGHT / 4u); g++) { if (minGroups[g] == 0xFFFFFFFFul) { continue; } yEnd = (uint16_t)(g * 4u + 4u); for (y = (uint16_t)(g * 4u); y < yEnd; y++) { if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) { continue; } // Same pump cadence as the graphics path: a blitted row is line // time, so keep the 64-byte RSDRV ring from overflowing. if ((blitRows & 1u) == 0u) { x68kSerialPump(); } blitRows++; // Bands are 16-bit stage words = 4 pixels = 2 bytes of a plane. firstByte = (uint16_t)(gStageMinWord[y] >> 1); rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u); if ((uint16_t)(firstByte + rowBytes) > X68K_BYTES_PER_ROW) { rowBytes = (uint16_t)(X68K_BYTES_PER_ROW - firstByte); } for (plane = 0u; plane < X68K_BITPLANES; plane++) { sp = pd->planes[plane] + ((uint16_t)y * X68K_BYTES_PER_ROW) + firstByte; x68kCopyRowToText((volatile uint8_t *)(X68K_TVRAM_PLANE0 + ((uint32_t)plane * X68K_TVRAM_PLANE_STEP) + x68kTvramOffset(0u, (uint16_t)(y + X68K_ORIGIN_Y)) + X68K_ORIGIN_BYTE + firstByte), sp, rowBytes); } } } } #else void jlpPresent(const jlSurfaceT *src) { X68kPlanarT *pd; volatile uint32_t *wordDst; const uint8_t *p0; const uint8_t *p1; const uint8_t *p2; const uint8_t *p3; uint32_t rowBase; uint16_t y; uint16_t firstByte; uint16_t rowBytes; const uint8_t *sp; uint16_t b; uint8_t palBase; uint16_t blitRows = 0; if (src == NULL) { return; } vdispPoll(); // keep the frame counter honest: see the note on vdispPoll // Reload the 256-entry graphics palette when a palette changed. The SCB // flag is NOT consumed: the per-line palette choice is applied below in the // pixel expansion, which reads src->scb directly (as DOS does). if (!gSpreadBuilt) { buildSpreadTables(); } if (gStagePaletteDirty) { uploadGfxPalette(src); gStagePaletteDirty = false; } pd = x68kSurfacePlanar(src); if (pd == NULL) { return; } sp = (const uint8_t *)&gSpreadPair[0][0][0]; 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); if ((uint16_t)(firstByte + rowBytes) > X68K_BYTES_PER_ROW) { rowBytes = (uint16_t)(X68K_BYTES_PER_ROW - firstByte); } // This row's band selects the high nibble of every pixel byte -- the // whole SCB mechanism, exactly src/dos/hal.c:83. It rides in plane 0's // table rather than in an OR per output long. palBase = (uint8_t)(src->scb[y] << 4); spreadSetPalBase(palBase); p0 = pd->planes[0] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte; p1 = pd->planes[1] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte; p2 = pd->planes[2] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte; p3 = pd->planes[3] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte; // Centred in the 512x512 screen. One 16-bit GVRAM word per pixel, and // the palette index is that word's LOW byte -- odd address on this // big-endian bus. rowBase = X68K_GVRAM_BASE + (((uint32_t)(y + X68K_ORIGIN_Y) * X68K_GVRAM_WORDS_ROW) + X68K_ORIGIN_X + ((uint32_t)firstByte * 8u)) * 2u; wordDst = (volatile uint32_t *)rowBase; // Four long stores per eight pixels; each long is two GVRAM words. // // The index is a BYTE offset into the table, never an array // subscript: (v >> 1) & 0x78 is the high nibble times the 8-byte // entry size and (v << 3) & 0x78 is the low one, so the compiler has // nothing left to scale. The plane is ORed into the same index -- // 0x78 leaves bit 7 upwards free -- which keeps ONE table base live // in an address register. Handing the compiler a base per plane // instead made it worse: it folded each into an absolute and then // reloaded it with an LEA before every single lookup. for (b = 0u; b < rowBytes; b++) { uint8_t v0 = *p0++; uint8_t v1 = *p1++; uint8_t v2 = *p2++; uint8_t v3 = *p3++; uint16_t o0 = (uint16_t)((v0 >> 1) & 0x78u); uint16_t o1 = (uint16_t)((v1 >> 1) & 0x78u); uint16_t o2 = (uint16_t)((v2 >> 1) & 0x78u); uint16_t o3 = (uint16_t)((v3 >> 1) & 0x78u); *wordDst++ = SPREAD_AT(sp, o0) | SPREAD_AT(sp, o1 | 128) | SPREAD_AT(sp, o2 | 256) | SPREAD_AT(sp, o3 | 384); *wordDst++ = SPREAD_AT(sp, o0 + 4) | SPREAD_AT(sp, (o1 | 128) + 4) | SPREAD_AT(sp, (o2 | 256) + 4) | SPREAD_AT(sp, (o3 | 384) + 4); o0 = (uint16_t)((v0 << 3) & 0x78u); o1 = (uint16_t)((v1 << 3) & 0x78u); o2 = (uint16_t)((v2 << 3) & 0x78u); o3 = (uint16_t)((v3 << 3) & 0x78u); *wordDst++ = SPREAD_AT(sp, o0) | SPREAD_AT(sp, o1 | 128) | SPREAD_AT(sp, o2 | 256) | SPREAD_AT(sp, o3 | 384); *wordDst++ = SPREAD_AT(sp, o0 + 4) | SPREAD_AT(sp, (o1 | 128) + 4) | SPREAD_AT(sp, (o2 | 256) + 4) | SPREAD_AT(sp, (o3 | 384) + 4); } } } #endif // X68K_PRESENT_TEXT // ----- Input ---------------------------------------------------------------- // _iocs_bitsns hands back a raw per-group key-DOWN bitmap, so there is no ISR // to install, no vector to take over and no packet state machine -- unlike the // ST, which has to replace the TOS ikbdsys vector and decode IKBD packets. void jlpInputInit(void) { uint16_t drain; // Empty the IOCS type-ahead buffer so keys typed at the Human68k // prompt (including the Enter that launched us) don't surface as // the app's first typed characters. for (drain = 0; drain < X68K_KEY_DRAIN_GUARD; drain++) { if (_iocs_b_keysns() == 0) { break; } (void)_iocs_b_keyinp(); } // The mouse plugs into the keyboard and reaches the machine over // SCC channel B, which IOCS owns end to end -- _MS_INIT (re)arms // that path and zeroes its work area so boot-time motion never // leaks into the first poll. This runs before jlSerialOpen can // steal channel A's RX vectors, so the two SCC clients never touch // the chip at the same time. // // Position comes from the IOCS cursor tracker (_MS_CURGT), not // from _MS_GETDT deltas: the IPL 1.0 ROM's _MS_GETDT work area // holds the LAST packet's displacement and is not cleared by // reading (verified against the real ROM under MAME by // verify-x68000-mouse.sh), so re-reading it between packets // re-counts stale motion. The tracker integrates each packet // exactly once at receive time and clamps to the _MS_LIMIT box, // which is set to stage coordinates here -- no cursor is ever // shown (_MS_CURON is never called), the tracker just does the // bookkeeping. _iocs_ms_init(); _iocs_ms_limit(0, 0, SURFACE_WIDTH - 1, SURFACE_HEIGHT - 1); _iocs_ms_curst(SURFACE_WIDTH / 2, SURFACE_HEIGHT / 2); gMouseAttached = true; gMouseX = SURFACE_WIDTH / 2; gMouseY = SURFACE_HEIGHT / 2; } // The _MS_LIMIT box is handed back in jlpShutdown instead of here: it has to // be re-established AFTER the CRT mode is restored, and core's jlShutdown // calls this first. void jlpInputShutdown(void) { gMouseAttached = false; } // jlKeyE -> X68000 scancode. Values taken from XEiJ's keyboard table // (Keyboard.java), which is the authoritative mapping, not guessed. // 0 means "this key has no equivalent on an X68000 keyboard". // // Notes on the keys that do not map one-to-one: // - The X68000 has a SINGLE shift key (0x70), so KEY_LSHIFT and KEY_RSHIFT // both read it. A game testing either sees the same physical key. // - There is no ALT; OPT.1 (0x72) is the closest equivalent and is what // KEY_LALT reads. OPT.2 (0x73) is left unmapped. static const uint8_t kScanForKey[KEY_COUNT] = { [KEY_NONE] = 0x00, [KEY_A] = 0x1e, [KEY_B] = 0x2e, [KEY_C] = 0x2c, [KEY_D] = 0x20, [KEY_E] = 0x13, [KEY_F] = 0x21, [KEY_G] = 0x22, [KEY_H] = 0x23, [KEY_I] = 0x18, [KEY_J] = 0x24, [KEY_K] = 0x25, [KEY_L] = 0x26, [KEY_M] = 0x30, [KEY_N] = 0x2f, [KEY_O] = 0x19, [KEY_P] = 0x1a, [KEY_Q] = 0x11, [KEY_R] = 0x14, [KEY_S] = 0x1f, [KEY_T] = 0x15, [KEY_U] = 0x17, [KEY_V] = 0x2d, [KEY_W] = 0x12, [KEY_X] = 0x2b, [KEY_Y] = 0x16, [KEY_Z] = 0x2a, [KEY_0] = 0x0b, [KEY_1] = 0x02, [KEY_2] = 0x03, [KEY_3] = 0x04, [KEY_4] = 0x05, [KEY_5] = 0x06, [KEY_6] = 0x07, [KEY_7] = 0x08, [KEY_8] = 0x09, [KEY_9] = 0x0a, [KEY_SPACE] = 0x35, [KEY_ESCAPE] = 0x01, [KEY_RETURN] = 0x1d, [KEY_TAB] = 0x10, [KEY_BACKSPACE] = 0x0f, [KEY_UP] = 0x3c, [KEY_DOWN] = 0x3e, [KEY_LEFT] = 0x3b, [KEY_RIGHT] = 0x3d, [KEY_LSHIFT] = 0x70, [KEY_RSHIFT] = 0x70, // one physical SHIFT [KEY_LCTRL] = 0x71, [KEY_LALT] = 0x72, // OPT.1 [KEY_F1] = 0x63, [KEY_F2] = 0x64, [KEY_F3] = 0x65, [KEY_F4] = 0x66, [KEY_F5] = 0x67, [KEY_F6] = 0x68, [KEY_F7] = 0x69, [KEY_F8] = 0x6a, [KEY_F9] = 0x6b, [KEY_F10] = 0x6c, }; // _iocs_bitsns(group) returns a bitmap of the eight keys in that group, bit n // set meaning "down": group = scancode >> 3, bit = scancode & 7. Every group // is fetched once per poll rather than per key, so the whole keyboard costs // X68K_KEYGROUP_COUNT IOCS calls regardless of how many keys are tested. // // No ISR, no vector takeover, no packet decoding -- unlike the ST, which has // to replace the TOS ikbdsys vector and run an IKBD packet state machine. void jlpInputPoll(void) { uint8_t groups[X68K_KEYGROUP_COUNT]; uint16_t group; uint16_t key; uint16_t drain; uint8_t scan; uint32_t mousePos; uint32_t mouseButtons; 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, // so a released key must drop back to 0 here (set-only left keys // latched down forever, hanging any second jlWaitForAnyKey). for (key = 1; key < KEY_COUNT; key++) { scan = kScanForKey[key]; if (scan == 0u) { continue; } group = (uint16_t)(scan >> 3); if (group < X68K_KEYGROUP_COUNT) { gKeyState[key] = (uint8_t)((groups[group] >> (scan & 7u)) & 1u); } } // Typed-character path: the IOCS keyboard interrupt queues // translated characters (shift/caps/layout applied) independently // of the bitmap above; _B_KEYINP pops (scancode << 8) | char. // Non-character keys carry 0 in the low byte and are dropped by // jlInputCharPush's filter. for (drain = 0; drain < X68K_KEY_DRAIN_GUARD; drain++) { if (_iocs_b_keysns() == 0) { break; } jlInputCharPush((uint8_t)(_iocs_b_keyinp() & 0xFFu)); } // Mouse position: the IOCS cursor tracker already integrated and // clamped every SCC channel B packet into the _MS_LIMIT box set at // init (stage coordinates), so _MS_CURGT's packed (x << 16) | y IS // the pointer position -- no delta math and no second clamp here. // Buttons ride _MS_GETDT's low word, which holds live levels: // right in bits 15-8, left in bits 7-0, 0xFF held / 0x00 up. That // byte order is the one the real IPL 1.0 ROM produces (verified // under MAME by verify-x68000-mouse.sh); published summaries that // list left first are wrong. Neither trap blocks on the 4800-baud // wire -- both just read the IOCS work area. mousePos = (uint32_t)_iocs_ms_curgt(); gMouseX = (int16_t)((mousePos >> 16) & 0xFFFFu); gMouseY = (int16_t)(mousePos & 0xFFFFu); mouseButtons = (uint32_t)_iocs_ms_getdt(); gMouseButtonState[MOUSE_BUTTON_LEFT] = (mouseButtons & 0xFFu) != 0u; gMouseButtonState[MOUSE_BUTTON_RIGHT] = ((mouseButtons >> 8) & 0xFFu) != 0u; gMouseButtonState[MOUSE_BUTTON_MIDDLE] = false; } void jlpJoystickReset(jlJoystickE js) { (void)js; // Digital sticks: nothing to calibrate. } // ----- Timing --------------------------------------------------------------- // There is no plain "wait for vblank" IOCS call -- _iocs_vdispst INSTALLS a // vertical-display handler rather than blocking -- so poll the MFP's V-DISP // input directly and edge-detect it. Both spins are bounded so a wedged or // mis-programmed CRTC degrades to a dropped frame instead of hanging the game. // // ASSUMES SUPERVISOR MODE for the $E88001 read. Human68k normally leaves user // programs in supervisor, unlike TOS on the ST (which needs Super(0L) -- see // the ST HAL). If that turns out not to hold, this becomes an _iocs_vdispst // handler bumping gFrameCount instead. // Vertical-display interrupt handler: the AUTHORITATIVE frame counter. // // A polled edge-detector loses ticks, and it loses them exactly when it hurts: // any frame doing real work between jlFrameCount() calls misses the V-DISP // transitions that happened meanwhile. Measured: 400 audio refills across ~19 // frames reported ZERO elapsed frames, because nothing polled in between. // Animation and music tempo would silently run slow under load. // // Registered with _iocs_vdispst, which calls this once per vertical display // period. Plain C (RTS) is the IOCS convention for a _VDISPST handler; IOCS // owns the interrupt frame and the RTE. static void vdispHandler(void) { gFrameCount++; } // Referenced only to keep the handler compiled and honest about its intent // until the asm thunk exists; see the note in jlpInit. void *const gX68kVdispHandlerRef = (void *)vdispHandler; // V-DISP edge detect. This is the PRIMARY frame clock (the interrupt route // hangs -- see jlpInit), so it is called from every per-frame entry point the // library owns -- jlpPresent, jlpInputPoll, jlpFrameCount, jlpWaitVBL -- not // just when the app asks the time. A game that presents or polls input once a // frame therefore keeps an accurate count even while doing heavy work. // // The residual limitation is real and worth knowing: a frame that does NONE of // those for longer than one V-DISP period still loses ticks. An asm thunk for // _iocs_vdispst removes it for good. // // CRITICAL CONTRACT: jlpFrameCount must be monotonic ON ITS OWN, without // jlpWaitVBL being called. UBER's timing model (and any game that paces by // polling rather than blocking) sits in a tight loop reading jlFrameCount and // never calls jlWaitVBL -- so a counter bumped only inside the wait never // advances and the caller spins forever. That is exactly what happened here: // UBER ran for 38,000 frames with a live, moving PC and a frozen screen, // because it was stuck on its first timed op waiting for a tick that could // not arrive. static void vdispPoll(void) { uint8_t now; if (gVdispInstalled) { return; // the interrupt owns the counter } now = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u); if (now != gLastVdisp) { gLastVdisp = now; if (now != 0u) { gFrameCount++; } } } void jlpWaitVBL(void) { uint16_t start; uint32_t guard; // Block until the counter actually moves, so the wait and the counter can // never disagree. Bounded, so a wedged CRTC drops a frame rather than // hanging the game. start = gFrameCount; 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++; } } // Frames since init, derived from the honest millisecond tick rather than from // counting V-DISP edges. // // The edge counter (vdispPoll) can only see a transition while the CPU happens // to be inside a JoeyLib call, so any op that spans more than one frame loses // the edges in between. That undercount is what made UBER's "16 frame" windows // run for 88 seconds and what made every op report the same throughput. The // tick is interrupt-driven and cannot miss, so time -> frames is monotonic and // correct regardless of what the caller is doing. // // vdispPoll still runs: jlpWaitVBL needs the real V-DISP edge to synchronise // to, and gFrameCount remains the fallback when the tick could not be hooked. uint16_t jlpFrameCount(void) { vdispPoll(); if (!gTickHooked) { return gFrameCount; } return (uint16_t)((jlpMillisElapsed() * (uint32_t)jlpFrameHz()) / 1000ul); } #ifdef JOEY_LOG_SERIAL_TEE // Mirror every log line out RS-232C. Polled and blocking: a diagnostic build // trades speed for never dropping the line that explains the failure. The // transmit-ready spin is bounded so an unwired or unopened port degrades to // slow-but-running instead of wedging the machine being diagnosed. void jlpLogTee(const char *text) { uint32_t guard; while (*text != '\0') { guard = 0ul; while (_iocs_osns232c() == 0 && guard < X68K_TEE_SPIN_LIMIT) { guard++; } if (guard >= X68K_TEE_SPIN_LIMIT) { return; // port not draining -- give up on this line } if (*text == '\n') { _iocs_out232c((int)'\r'); } _iocs_out232c((int)(unsigned char)*text); text++; } } #endif // Milliseconds since the first call. Latches its base explicitly rather than // treating 0 as a sentinel, because 0 is a legal tick value at power-on and at // every wrap. Falls back to the frame-derived generic clock if the tick could // not be hooked, which is wrong in the ways described above but better than // returning a constant. uint32_t jlpMillisElapsed(void) { uint32_t ticks; if (!gTickHooked) { return jlpGenericMillisElapsed(); } ticks = gX68kTicks; if (!gTickBaseSet) { gTickBase = ticks; gTickBaseSet = true; } // Scale by the measured rate. Multiply first (ticks fit comfortably in 32 // bits for any realistic session) so the divide does not quantize away the // sub-tick remainder. return (uint32_t)(((ticks - gTickBase) * 1000ul) / gTickHz); } uint16_t jlpFrameHz(void) { // 31 kHz modes on this machine run at ~55.5 Hz rather than 60. Reported // only -- the generic millisElapsed divides by it. return 55u; }