// 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 // docs/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 // _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 #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 // ----- 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 buildSpreadTables(void); 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); 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 _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. // 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 (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; } // ----- SCB per-band palette -------------------------------------------------- // 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 static uint32_t gSpreadHi[X68K_BITPLANES][X68K_SPREAD_NIBBLES]; static uint32_t gSpreadLo[X68K_BITPLANES][X68K_SPREAD_NIBBLES]; 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++) { gSpreadHi[plane][nib] = (((nib >> 3) & 1u) ? (bit << 16) : 0ul) | (((nib >> 2) & 1u) ? bit : 0ul); gSpreadLo[plane][nib] = (((nib >> 1) & 1u) ? (bit << 16) : 0ul) | (((nib >> 0) & 1u) ? bit : 0ul); } } gSpreadBuilt = true; } // ----- 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. 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; uint32_t pal2; uint16_t b; uint16_t hi0, hi1, hi2, hi3; uint16_t lo0, lo1, lo2, lo3; 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; } 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. palBase = (uint8_t)(src->scb[y] << 4); 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; // Two long stores per four pixels; each long is two 16-bit GVRAM words. pal2 = (uint32_t)(((uint32_t)palBase << 16) | (uint32_t)palBase); for (b = 0u; b < rowBytes; b++) { hi0 = (uint16_t)(p0[b] >> 4); hi1 = (uint16_t)(p1[b] >> 4); hi2 = (uint16_t)(p2[b] >> 4); hi3 = (uint16_t)(p3[b] >> 4); lo0 = (uint16_t)(p0[b] & 0x0Fu); lo1 = (uint16_t)(p1[b] & 0x0Fu); lo2 = (uint16_t)(p2[b] & 0x0Fu); lo3 = (uint16_t)(p3[b] & 0x0Fu); *wordDst++ = gSpreadHi[0][hi0] | gSpreadHi[1][hi1] | gSpreadHi[2][hi2] | gSpreadHi[3][hi3] | pal2; *wordDst++ = gSpreadLo[0][hi0] | gSpreadLo[1][hi1] | gSpreadLo[2][hi2] | gSpreadLo[3][hi3] | pal2; *wordDst++ = gSpreadHi[0][lo0] | gSpreadHi[1][lo1] | gSpreadHi[2][lo2] | gSpreadHi[3][lo3] | pal2; *wordDst++ = gSpreadLo[0][lo0] | gSpreadLo[1][lo1] | gSpreadLo[2][lo2] | gSpreadLo[3][lo3] | pal2; } } } // ----- 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; }