// Apple IIgs keyboard + mouse input via classic Apple II softswitches. // // Keyboard: $C000 (data) and $C010 (clear strobe). The Event Manager // would be the "modern" approach, but it requires a full ToolBox // bring-up that our S16 binary does not perform; calling GetNextEvent // uninitialized corrupted KEGS' emulation state. Softswitches have no // such dependency: they are live memory-mapped hardware. // // The typed-character queue is fed from the same $C000 drain: the ADB // firmware translates shift/caps/layout before the byte reaches the // strobe register, so each drained event doubles as a typed character // (jlInputCharPush) with no extra keymap work. // // Tradeoff: $C000 reports the *last* key pressed, not a per-key matrix. // Holding multiple non-modifier keys simultaneously cannot be observed; // the demo and any game using this port sees one typable key at a time, // plus live shift/ctrl/option state from the modifier register at // $C025. This matches what every Apple II game ever shipped does, and // it is enough for feature parity with the other platforms on typical // "press a key, act on it" flows. // // Release detection uses the IIe-inherited "any key currently down" // live flag at $C010 bit 7 (set by the keyboard scanner independently // of the strobe). Each jlpInputPoll drains pending strobe events to // pick up presses, then samples $C010: bit 7 == 0 means no // non-modifier key is physically held, and we wholesale-clear // gKeyState. readModifierKeys then re-asserts the modifiers from // $C025's live state, so shift/ctrl/option stay accurate. Avoids // the inferred-release lag the old TTL-decay scheme had, and works // on every IIgs (real or stealth) without ToolBox / ADB Tool init. // // Mouse: $C024 (delta data) and $C027 (status). Each $C024 read // returns one signed 7-bit delta; $C027 bit 1 indicates whether the // next read will return X (0) or Y (1). On the Y read, $C024 bit 7 // also encodes inverted button state (0 = pressed). We do exactly two // $C024 reads per jlpInputPoll, accumulating the deltas onto an // absolute position which is clamped to the surface rectangle. The // IIgs ADB MCU autopolls the mouse and queues fifos behind these // softswitches, so the per-frame two-read cadence keeps up with // normal motion; bursts may lag by a frame. #include #include "port.h" #include "inputInternal.h" #include "joey/surface.h" #include "surfaceInternal.h" // iigsByteFill (hunt-3 rank 5) // ----- Hardware registers ----- #define IIGS_KBD ((volatile uint8_t *)0x00C000L) #define IIGS_KBDSTRB ((volatile uint8_t *)0x00C010L) #define IIGS_MOUSEDATA ((volatile uint8_t *)0x00C024L) #define IIGS_MODIFIERS ((volatile uint8_t *)0x00C025L) #define IIGS_KMSTATUS ((volatile uint8_t *)0x00C027L) // Joystick / paddle softswitches. #define IIGS_BTN0 ((volatile uint8_t *)0x00C061L) #define IIGS_BTN1 ((volatile uint8_t *)0x00C062L) #define IIGS_PADDLE0 ((volatile uint8_t *)0x00C064L) #define IIGS_PADDLE1 ((volatile uint8_t *)0x00C065L) #define IIGS_PTRIG ((volatile uint8_t *)0x00C070L) #define IIGS_BUTTON_BIT 0x80 #define IIGS_PADDLE_BUSY 0x80 #define PADDLE_TIMEOUT 256 #define PADDLE_LO_THRESHOLD 64 #define PADDLE_HI_THRESHOLD 192 #define KBD_STROBE_BIT 0x80 #define KBD_ASCII_MASK 0x7F // $C010 RDKBDSTRB: reading clears the keyboard strobe at $C000 and // returns the live "any key currently held" flag in bit 7 (set by // the keyboard scanner / ADB MCU independently of the strobe). Used // to drive immediate release detection without an inferred-release // TTL counter. #define KBD_ANY_KEY_DOWN_BIT 0x80 // Cap on the per-poll keyboard-FIFO drain. The IIgs ADB queue is // small in practice; this is purely a defensive bound so a stuck // strobe can't spin jlpInputPoll forever. #define KBD_DRAIN_GUARD 32u // $C025 layout (IIgs Hardware Reference): bit 0 = shift, bit 1 = ctrl, // bit 6 = option (Closed-Apple), bit 7 = command (Open-Apple). #define MOD_SHIFT 0x01 #define MOD_CONTROL 0x02 #define MOD_OPTION 0x40 // $C027 layout (IIgs Hardware Reference / ADB MCU): #define KMSTATUS_MOUSE_DATA 0x80 // mouse data available #define KMSTATUS_MOUSE_COORD 0x02 // 0 = next $C024 read is X, 1 = Y // $C024 mouse-data layout: bit 7 on Y reads encodes button (0=down). // Bit 6 carries the sign of the 7-bit delta; bits 5-0 the magnitude. #define MOUSE_DELTA_MASK 0x7F #define MOUSE_DELTA_SIGN_BIT 0x40 #define MOUSE_BUTTON_INV 0x80 #define ASCII_TABLE_SIZE 128 // Apple II arrow-key ASCII conventions. #define ASCII_LEFT 0x08 #define ASCII_RIGHT 0x15 #define ASCII_UP 0x0B #define ASCII_DOWN 0x0A #define ASCII_RETURN 0x0D #define ASCII_TAB 0x09 #define ASCII_ESCAPE 0x1B #define ASCII_DELETE 0x7F #define ASCII_SPACE 0x20 // ----- Prototypes ----- static void buildAsciiTable(void); static void pollJoystick(void); static void pollMouse(void); static void readModifierKeys(void); static int8_t signExtend7(uint8_t raw); static int8_t thresholdPaddle(uint8_t v); // ----- Module state ----- // ASCII -> jlKeyE, filled once at jlpInputInit. Runtime fill keeps // lookup O(1) instead of a 40-plus-case switch. static uint8_t gAsciiToKey[ASCII_TABLE_SIZE]; static int16_t gMouseAbsX = SURFACE_WIDTH / 2; static int16_t gMouseAbsY = SURFACE_HEIGHT / 2; // ----- Internal helpers ----- static void buildAsciiTable(void) { uint16_t i; memset(gAsciiToKey, 0, sizeof(gAsciiToKey)); for (i = 'A'; i <= 'Z'; i++) { gAsciiToKey[i] = (uint8_t)(KEY_A + (i - 'A')); gAsciiToKey[i - 'A' + 'a'] = (uint8_t)(KEY_A + (i - 'A')); } for (i = '0'; i <= '9'; i++) { gAsciiToKey[i] = (uint8_t)(KEY_0 + (i - '0')); } gAsciiToKey[ASCII_SPACE] = KEY_SPACE; gAsciiToKey[ASCII_ESCAPE] = KEY_ESCAPE; gAsciiToKey[ASCII_RETURN] = KEY_RETURN; gAsciiToKey[ASCII_TAB] = KEY_TAB; gAsciiToKey[ASCII_DELETE] = KEY_BACKSPACE; // The left-arrow key produces 0x08, which is also ASCII backspace // on a classic Apple II. Prefer the arrow interpretation since // there is a dedicated Delete key that reports 0x7F. gAsciiToKey[ASCII_LEFT] = KEY_LEFT; gAsciiToKey[ASCII_RIGHT] = KEY_RIGHT; gAsciiToKey[ASCII_UP] = KEY_UP; gAsciiToKey[ASCII_DOWN] = KEY_DOWN; } static void readModifierKeys(void) { uint8_t mods; mods = *IIGS_MODIFIERS; gKeyState[KEY_LSHIFT] = (mods & MOD_SHIFT) != 0; gKeyState[KEY_LCTRL] = (mods & MOD_CONTROL) != 0; gKeyState[KEY_LALT] = (mods & MOD_OPTION) != 0; } // Sign-extend a 7-bit two's-complement number stored in bits 0-6. static int8_t signExtend7(uint8_t raw) { uint8_t v; v = (uint8_t)(raw & MOUSE_DELTA_MASK); if (v & MOUSE_DELTA_SIGN_BIT) { return (int8_t)(v | 0x80); } return (int8_t)v; } // Map a raw 0..255 paddle reading to JOYSTICK_AXIS_MIN..MAX, using the // stick's calibrated center (captured by jlJoystickReset) and a // dead-zone band around it. Returns 0 if reading is within deadZone of // the center; otherwise the offset from center, clamped to int8_t. static int8_t analogPaddle(uint8_t v, uint8_t center, uint8_t deadZone) { int16_t delta; delta = (int16_t)v - (int16_t)center; if (delta < 0) { if ((-delta) <= (int16_t)deadZone) { return 0; } if (delta < (int16_t)JOYSTICK_AXIS_MIN) { return JOYSTICK_AXIS_MIN; } } else { if (delta <= (int16_t)deadZone) { return 0; } if (delta > (int16_t)JOYSTICK_AXIS_MAX) { return JOYSTICK_AXIS_MAX; } } return (int8_t)delta; } // Threshold a 0..255 paddle reading into a digital direction so the // IIgs analog stick presents the same axis semantics as the digital // sticks on ST/Amiga/DOS. Center range is treated as zero. Used // before jlJoystickReset has been called -- once the app calibrates, // we switch to analogPaddle for finer control. static int8_t thresholdPaddle(uint8_t v) { if (v < PADDLE_LO_THRESHOLD) { return JOYSTICK_AXIS_MIN; } if (v > PADDLE_HI_THRESHOLD) { return JOYSTICK_AXIS_MAX; } return 0; } // Read the Apple IIgs joystick (paddle 0/1 + buttons 0/1). Buttons at // $C061/$C062 are tied to the Open-Apple/Closed-Apple keys, so holding // either modifier key looks like a fire press -- intentional Apple // behavior, accept it. Only one stick is exposed; the IIgs second // "stick" wiring (paddles 2/3) is rarely used by retro games. // // Each paddle read triggers an RC scan via $C070 and then polls the // paddle softswitch until bit 7 clears; the iteration count // approximates the paddle's 0..255 position (the Apple firmware // PREAD routine works the same way). The paddle one-shot timer takes ~3 ms to // charge at full deflection; if NO joystick is wired up, the BUSY bit // stays set forever and the busy-wait runs the full PADDLE_TIMEOUT // every frame -- ~3 ms wasted per frame on a stick that isn't there. // // After JOY_DISCONNECT_THRESHOLD consecutive timeouts we latch the // stick as absent and stop polling entirely. The app calls // jlJoystickReset to clear the latch and resume polling. #define JOY_DISCONNECT_THRESHOLD 60u static uint16_t gJoyConsecutiveTimeouts = 0; static bool gJoyDisconnectLatched = false; // Analog calibration: gJoyCenterX/Y hold the raw paddle reading we // captured the last time the user called jlJoystickReset. Until // that's called, gJoyCenterValid is false and pollJoystick falls back // to the digital threshold mapping. gJoyRecalibrate is set by // jlpJoystickReset and cleared on the next successful poll, which // captures the new center. // uint8_t (not bool) so the per-element stride is a known 1 byte. // Storage is still 0 or 1 either way. static uint8_t gJoyCenterX [JOYSTICK_COUNT]; static uint8_t gJoyCenterY [JOYSTICK_COUNT]; static uint8_t gJoyCenterValid [JOYSTICK_COUNT]; static uint8_t gJoyRecalibrate [JOYSTICK_COUNT]; void jlpJoystickReset(jlJoystickE js) { if ((uint16_t)js >= (uint16_t)JOYSTICK_COUNT) { return; } // Re-enable polling and arm a fresh center capture for the next // poll. The dead-zone value lives in core's gJoyDeadZone[js]. gJoyConsecutiveTimeouts = 0; gJoyDisconnectLatched = false; gJoyRecalibrate[js] = true; } // Asm paddle reader (joeyDraw.s, John Brooks' 1 MHz GetJoyXY). Switches // the CPU to 1 MHz for the read so paddle counts match what every other // IIgs/Apple II joystick game produces (a busy-wait at 2.8 MHz inflates // counts). Returns the paddle read packed as a uint32_t in A:X (register // return -- asm writes to a global do not reach the C-read address in // this toolchain): // resolved = ret & 0xFF (bit0: JoyX valid, bit1: JoyY valid) // px = (ret >> 8) & 0xFF (JoyX 0..255) // py = (ret >> 16) & 0xFF (JoyY 0..255) extern uint32_t iigsPollJoystickInner(void); static void pollJoystick(void) { uint32_t result; uint8_t px; uint8_t py; uint8_t resolvedFlags; bool xResolved; bool yResolved; // Buttons are I/O reads -- always cheap, do them every frame. // Indexing through a (uint8_t *) cast collapses each // gJoyButtonState[i][j] write to a literal byte offset. ((uint8_t *)gJoyButtonState)[JOYSTICK_0 * JOY_BUTTON_COUNT + JOY_BUTTON_0] = (*IIGS_BTN0 & IIGS_BUTTON_BIT) != 0; ((uint8_t *)gJoyButtonState)[JOYSTICK_0 * JOY_BUTTON_COUNT + JOY_BUTTON_1] = (*IIGS_BTN1 & IIGS_BUTTON_BIT) != 0; gJoyConnected[JOYSTICK_1] = false; // Once the stick has been latched as disconnected, only buttons // get polled. The app must call jlJoystickReset to resume axis // polling (e.g., when the user has just plugged in a stick). if (gJoyDisconnectLatched) { gJoyAxisX[JOYSTICK_0] = 0; gJoyAxisY[JOYSTICK_0] = 0; gJoyConnected[JOYSTICK_0] = false; return; } // Asm read at 1 MHz -- result returned packed in registers (A:X). result = iigsPollJoystickInner(); resolvedFlags = (uint8_t)(result & 0xFFu); px = (uint8_t)((result >> 8) & 0xFFu); py = (uint8_t)((result >> 16) & 0xFFu); xResolved = (resolvedFlags & 0x01) != 0; yResolved = (resolvedFlags & 0x02) != 0; gJoyConnected[JOYSTICK_0] = xResolved || yResolved; // Update auto-disconnect counter. Both axes failing => probably no // stick. One resolves => stick is present, reset the counter. // // gJoyConsecutiveTimeouts uses a local-var read-modify-write rather // than `++` so the update is DBR-independent: an `inc abs` on the // static would depend on DBR pointing at this static's bank, and a // cross-segment JSL doesn't update DBR, so a caller in a different // load segment could silently mutate the wrong byte. Long-mode // lda+sta avoids that. if (!xResolved && !yResolved) { uint16_t timeouts; timeouts = gJoyConsecutiveTimeouts; if (timeouts < 0xFFFFu) { timeouts = (uint16_t)(timeouts + 1u); gJoyConsecutiveTimeouts = timeouts; } if (timeouts >= JOY_DISCONNECT_THRESHOLD) { gJoyDisconnectLatched = true; } gJoyAxisX[JOYSTICK_0] = 0; gJoyAxisY[JOYSTICK_0] = 0; return; } gJoyConsecutiveTimeouts = 0; // Capture the resting position on recalibrate (one-shot). if (gJoyRecalibrate[JOYSTICK_0]) { gJoyCenterX [JOYSTICK_0] = px; gJoyCenterY [JOYSTICK_0] = py; gJoyCenterValid[JOYSTICK_0] = true; gJoyRecalibrate[JOYSTICK_0] = false; } // Calibrated => analog axis report (offset from center, dead-zone // clamped). Uncalibrated => the legacy 3-state digital threshold, // matching how the stick behaved before jlJoystickReset existed. if (gJoyCenterValid[JOYSTICK_0]) { gJoyAxisX[JOYSTICK_0] = analogPaddle(px, gJoyCenterX[JOYSTICK_0], gJoyDeadZone[JOYSTICK_0]); gJoyAxisY[JOYSTICK_0] = analogPaddle(py, gJoyCenterY[JOYSTICK_0], gJoyDeadZone[JOYSTICK_0]); } else { gJoyAxisX[JOYSTICK_0] = thresholdPaddle(px); gJoyAxisY[JOYSTICK_0] = thresholdPaddle(py); } } // Drain one X+Y delta pair from the ADB mouse FIFO. $C027 bit 1 tells // us which coordinate the next $C024 read will return; we honor that // rather than assuming an order, so we stay in sync even if a stray // $C024 read happened between frames. The Y read also carries the // inverted button state in bit 7 (0 = pressed). static void pollMouse(void) { uint8_t status; uint8_t data; int8_t delta; int16_t newPos; bool isYRead; uint16_t i; for (i = 0; i < 2; i++) { status = *IIGS_KMSTATUS; isYRead = (status & KMSTATUS_MOUSE_COORD) != 0; data = *IIGS_MOUSEDATA; delta = signExtend7(data); if (isYRead) { newPos = (int16_t)(gMouseAbsY + delta); if (newPos < 0) { newPos = 0; } if (newPos > SURFACE_HEIGHT - 1) { newPos = SURFACE_HEIGHT - 1; } gMouseAbsY = newPos; // Button bit only meaningful on Y reads. 0 = pressed. gMouseButtonState[MOUSE_BUTTON_LEFT] = (data & MOUSE_BUTTON_INV) == 0; } else { newPos = (int16_t)(gMouseAbsX + delta); if (newPos < 0) { newPos = 0; } if (newPos > SURFACE_WIDTH - 1) { newPos = SURFACE_WIDTH - 1; } gMouseAbsX = newPos; } } gMouseX = gMouseAbsX; gMouseY = gMouseAbsY; // The ADB mouse only reports the single physical button; right // and middle stay false. gMouseButtonState[MOUSE_BUTTON_RIGHT] = false; gMouseButtonState[MOUSE_BUTTON_MIDDLE] = false; } // ----- HAL API (alphabetical) ----- void jlpInputInit(void) { memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyPrev, 0, sizeof(gKeyPrev)); buildAsciiTable(); gMouseAttached = true; // the ADB mouse is part of every IIgs gMouseAbsX = SURFACE_WIDTH / 2; gMouseAbsY = SURFACE_HEIGHT / 2; gMouseX = gMouseAbsX; gMouseY = gMouseAbsY; // Clear any pending strobe from before we started. (void)*IIGS_KBDSTRB; } void jlpInputPoll(void) { // Keep the serial soft ring fed across the input scan (multiple I/O-page reads). { extern void iigsSerialPump(void); iigsSerialPump(); } uint8_t kbd; uint8_t ascii; uint8_t key; uint8_t kbdStrb; uint16_t drainGuard; bool strobeObserved; // The gKeyState/gKeyPrev/gMouseButtonPrev/gJoyButtonPrev snapshots // all happen earlier in jlInputPoll's call to iigsInputSnapshot // (asm). We just read the live hardware state here. // Drain the keyboard FIFO, not just the head. The IIgs ADB MCU // queues press + autorepeat events; consuming only one per poll // would leave queued events waiting to refresh state on later // polls. KBD_DRAIN_GUARD bounds the loop in case a stuck strobe // ever fails to clear. strobeObserved = false; for (drainGuard = 0; drainGuard < KBD_DRAIN_GUARD; drainGuard++) { { extern void iigsSerialPump(void); // Per-iteration pump (the X68000 per-group lesson): the FIFO drain + decode below is a // multi-ms deaf window at 9600 and the Z8530 FIFO is 3 bytes - a pump only at poll ENTRY // measurably lost a 4-byte clump mid-frame. iigsSerialPump(); } kbd = *IIGS_KBD; if ((kbd & KBD_STROBE_BIT) == 0) { break; } strobeObserved = true; ascii = (uint8_t)(kbd & KBD_ASCII_MASK); key = gAsciiToKey[ascii]; if (key != KEY_NONE) { gKeyState[key] = true; } // Typed-character path: the ADB firmware already applies // shift/caps and the keyboard layout, so the strobe byte IS // the translated character. The Delete key (0x7F, the IIgs // backspace key) is normalized to JL_CHAR_BACKSPACE. A raw // 0x08 is the LEFT-ARROW key (Apple II heritage), and arrows // are not characters -- suppress it so cursor movement never // types a backspace; the other arrows' codes are stopped by // jlInputCharPush's filter anyway. if (ascii == ASCII_DELETE) { jlInputCharPush(JL_CHAR_BACKSPACE); } else if (ascii != ASCII_LEFT) { jlInputCharPush(ascii); } (void)*IIGS_KBDSTRB; } // $C010 bit 7 is the live "any non-modifier key currently held" // flag (IIe-inherited; updated by the keyboard scanner / ADB MCU // independently of the strobe). When 0 we know all non-modifier // keys are physically released, so wholesale-clear gKeyState and // let readModifierKeys re-assert the modifiers from $C025 below. // // strobeObserved guard: a press that arrived AND was released // between two polls would otherwise be set-then-cleared in a // single poll, losing the rising edge that jlKeyPressed needs. // Holding the press for one poll preserves it; the next poll's // bit-7 read will clear normally. kbdStrb = *IIGS_KBDSTRB; if (!strobeObserved && (kbdStrb & KBD_ANY_KEY_DOWN_BIT) == 0) { // iigsByteFill, not memset: llvm-mos lowers memset to a // far-call byte loop (finding #79), and this branch runs on // nearly EVERY real game frame (idle keyboard). Hunt-3 rank 5. iigsByteFill(gKeyState, 0u, (uint16_t)sizeof(gKeyState)); } readModifierKeys(); pollMouse(); pollJoystick(); } void jlpInputShutdown(void) { (void)*IIGS_KBDSTRB; }