// 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 <x68k/dos.h>
#include <x68k/iocs.h>

#include "port.h"
#include <stddef.h>

#include "joey/debug.h"
#include <string.h>

#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;
}
