joeylib2/src/x68000/hal.c

788 lines
35 KiB
C

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