// Atari ST keyboard + mouse input: replaces the TOS ikbdsys vector in // the KBDVECS table so every byte from the IKBD ACIA comes through our // packet-aware handler. // // The IKBD protocol mixes keyboard scan codes with mouse/joy packets. // Bytes 0x00-0x72 (with optional 0x80 break bit -> 0x80..0xF2) are // keyboard scan codes; 0xF6-0xFF are packet headers that announce a // fixed number of trailing bytes. We dispatch based on header: // // 0xF8..0xFB Relative mouse packet (3 bytes total). The header's // low two bits are the button state (bit 1 = left, // bit 0 = right). Next two bytes are signed dx, dy. // Other Discarded (joystick / clock / status etc.) -- we just // consume the documented byte count to stay in sync. // // The ISR writes into private gIsrState (keyboard) and gIsrMouse* // buffers; jlpInputPoll copies them into the public globals. // jlKeyPressed edge detection requires that public gKeyState only // advance during jlpInputPoll, never at interrupt time -- jlInputPoll // snapshots gKeyState into gKeyPrev before jlpInputPoll runs. #include #include #include #include "port.h" #include "inputInternal.h" #include "joey/surface.h" // ----- Constants ----- // IKBD ACIA data register; reading also clears the ACIA's interrupt // so the MFP ACIA line drops. #define ST_ACIA_DATA ((volatile uint8_t *)0xFFFFFC02L) #define SCAN_BREAK_BIT 0x80 #define SCAN_CODE_MASK 0x7F #define SCAN_TABLE_SIZE 128 #define PKT_STATUS 0xF6 // + 7 bytes #define PKT_ABS_MOUSE 0xF7 // + 5 bytes #define PKT_REL_MOUSE_MIN 0xF8 // 0xF8..0xFB + 2 bytes #define PKT_REL_MOUSE_MAX 0xFB #define PKT_TIME 0xFC // + 6 bytes #define PKT_JOY_BOTH 0xFD // + 2 bytes #define PKT_JOY0 0xFE // + 1 byte #define PKT_JOY1 0xFF // + 1 byte // Bit layout of the IKBD relative-mouse packet header: #define MOUSE_HDR_LEFT_BTN 0x02 #define MOUSE_HDR_RIGHT_BTN 0x01 // IKBD joystick byte: bit set = direction held / fire pressed. #define JOY_BIT_UP 0x01 #define JOY_BIT_DOWN 0x02 #define JOY_BIT_LEFT 0x04 #define JOY_BIT_RIGHT 0x08 #define JOY_BIT_FIRE 0x80 // Packet kinds for the ISR's small state machine. #define PKT_KIND_NONE 0 #define PKT_KIND_DISCARD 1 #define PKT_KIND_REL_MOUSE 2 #define PKT_KIND_JOY0 3 // 0xFE -- 1 byte for ST joy port 0 (mouse port) #define PKT_KIND_JOY1 4 // 0xFF -- 1 byte for ST joy port 1 (dedicated) #define PKT_KIND_JOY_BOTH 5 // 0xFD -- 2 bytes (joy0 then joy1) // Mapping from ST joystick port -> jlJoystickE. Port 1 is the // dedicated stick that retro games use; expose it as the primary // JOYSTICK_0. Port 0 (mouse port) becomes JOYSTICK_1. #define ST_PORT0_AS_JS JOYSTICK_1 #define ST_PORT1_AS_JS JOYSTICK_0 // ----- Prototypes ----- static long patchIkbdVector(void); static long restoreIkbdVector(void); static long ikbdHandler(void); // ----- Module state ----- // ST IKBD scan codes. Identical encoding to the PC AT set-1 for most // keys, which is why the table mirrors the DOS scan map. static const uint8_t gScanToKey[SCAN_TABLE_SIZE] = { [0x01] = KEY_ESCAPE, [0x02] = KEY_1, [0x03] = KEY_2, [0x04] = KEY_3, [0x05] = KEY_4, [0x06] = KEY_5, [0x07] = KEY_6, [0x08] = KEY_7, [0x09] = KEY_8, [0x0A] = KEY_9, [0x0B] = KEY_0, [0x0E] = KEY_BACKSPACE, [0x0F] = KEY_TAB, [0x10] = KEY_Q, [0x11] = KEY_W, [0x12] = KEY_E, [0x13] = KEY_R, [0x14] = KEY_T, [0x15] = KEY_Y, [0x16] = KEY_U, [0x17] = KEY_I, [0x18] = KEY_O, [0x19] = KEY_P, [0x1C] = KEY_RETURN, [0x1D] = KEY_LCTRL, [0x1E] = KEY_A, [0x1F] = KEY_S, [0x20] = KEY_D, [0x21] = KEY_F, [0x22] = KEY_G, [0x23] = KEY_H, [0x24] = KEY_J, [0x25] = KEY_K, [0x26] = KEY_L, [0x2A] = KEY_LSHIFT, [0x2C] = KEY_Z, [0x2D] = KEY_X, [0x2E] = KEY_C, [0x2F] = KEY_V, [0x30] = KEY_B, [0x31] = KEY_N, [0x32] = KEY_M, [0x36] = KEY_RSHIFT, [0x38] = KEY_LALT, [0x39] = KEY_SPACE, [0x3B] = KEY_F1, [0x3C] = KEY_F2, [0x3D] = KEY_F3, [0x3E] = KEY_F4, [0x3F] = KEY_F5, [0x40] = KEY_F6, [0x41] = KEY_F7, [0x42] = KEY_F8, [0x43] = KEY_F9, [0x44] = KEY_F10, [0x48] = KEY_UP, [0x4B] = KEY_LEFT, [0x4D] = KEY_RIGHT, [0x50] = KEY_DOWN, }; static _KBDVECS *gKbdvecs = NULL; static long (*gOldIkbdsys)(void) = NULL; static volatile uint8_t gPacketRemaining = 0; static volatile uint8_t gPacketKind = PKT_KIND_NONE; static volatile uint8_t gMousePacketByte = 0; // bytes consumed in current packet static bool gHooked = false; // uint8_t (not bool) so element size matches gKeyState's. See // src/core/inputInternal.h for the full rationale. static volatile uint8_t gIsrState[KEY_COUNT]; // Mouse delta accumulator. Each ACIA mouse packet adds dx/dy here; the // poll routine clamps the running absolute position into the surface // rectangle. Buttons are latched in the header byte and stay live in // gIsrMouseButtons until the next packet rewrites them. static volatile int32_t gIsrMouseDx = 0; static volatile int32_t gIsrMouseDy = 0; static volatile uint8_t gIsrMouseButtons = 0; static int16_t gMouseAbsX = SURFACE_WIDTH / 2; static int16_t gMouseAbsY = SURFACE_HEIGHT / 2; // Latched joystick state, written by the IKBD ISR. The byte stays // unchanged between packets while a direction or fire is held, so // jlpInputPoll can simply read the latest value. static volatile uint8_t gIsrJoyByte[JOYSTICK_COUNT]; // ----- Internal helpers ----- // Runs in MFP ACIA interrupt context. Reads one byte from the ACIA, // dispatches to either keyboard handling, mouse-packet capture, or // "discard remaining N bytes" for packets we do not yet care about. static long ikbdHandler(void) { uint8_t byte; uint8_t code; uint8_t key; bool isBreak; byte = *ST_ACIA_DATA; if (gPacketRemaining != 0) { switch (gPacketKind) { case PKT_KIND_REL_MOUSE: // mouse-packet payload: byte 0 = dx, byte 1 = dy if (gMousePacketByte == 0) { gIsrMouseDx += (int8_t)byte; gMousePacketByte = 1; } else { gIsrMouseDy += (int8_t)byte; gMousePacketByte = 0; } break; case PKT_KIND_JOY0: gIsrJoyByte[ST_PORT0_AS_JS] = byte; break; case PKT_KIND_JOY1: gIsrJoyByte[ST_PORT1_AS_JS] = byte; break; case PKT_KIND_JOY_BOTH: // 0xFD payload is joy0 then joy1. if (gMousePacketByte == 0) { gIsrJoyByte[ST_PORT0_AS_JS] = byte; gMousePacketByte = 1; } else { gIsrJoyByte[ST_PORT1_AS_JS] = byte; gMousePacketByte = 0; } break; default: break; } gPacketRemaining = (uint8_t)(gPacketRemaining - 1); if (gPacketRemaining == 0) { gPacketKind = PKT_KIND_NONE; } return 0; } if (byte >= PKT_STATUS) { gPacketKind = PKT_KIND_DISCARD; gMousePacketByte = 0; switch (byte) { case PKT_STATUS: gPacketRemaining = 7; break; case PKT_ABS_MOUSE: gPacketRemaining = 5; break; case PKT_TIME: gPacketRemaining = 6; break; case PKT_JOY_BOTH: gPacketRemaining = 2; gPacketKind = PKT_KIND_JOY_BOTH; break; case PKT_JOY0: gPacketRemaining = 1; gPacketKind = PKT_KIND_JOY0; break; case PKT_JOY1: gPacketRemaining = 1; gPacketKind = PKT_KIND_JOY1; break; default: if (byte >= PKT_REL_MOUSE_MIN && byte <= PKT_REL_MOUSE_MAX) { gPacketRemaining = 2; gPacketKind = PKT_KIND_REL_MOUSE; gIsrMouseButtons = (uint8_t)(byte & (MOUSE_HDR_LEFT_BTN | MOUSE_HDR_RIGHT_BTN)); } else { gPacketKind = PKT_KIND_NONE; } break; } return 0; } isBreak = (byte & SCAN_BREAK_BIT) != 0; code = (uint8_t)(byte & SCAN_CODE_MASK); key = gScanToKey[code]; if (key != KEY_NONE) { gIsrState[key] = !isBreak; } return 0; } static long patchIkbdVector(void) { gOldIkbdsys = gKbdvecs->ikbdsys; gKbdvecs->ikbdsys = ikbdHandler; return 0; } static long restoreIkbdVector(void) { gKbdvecs->ikbdsys = gOldIkbdsys; return 0; } // ----- HAL API (alphabetical) ----- void jlpJoystickReset(jlJoystickE js) { // Atari ST sticks are digital -- no calibration to do. (void)js; } void jlpInputInit(void) { memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset((void *)gIsrState, 0, sizeof(gIsrState)); memset((void *)gIsrJoyByte, 0, sizeof(gIsrJoyByte)); gMouseAbsX = SURFACE_WIDTH / 2; gMouseAbsY = SURFACE_HEIGHT / 2; gMouseX = gMouseAbsX; gMouseY = gMouseAbsY; gIsrMouseDx = 0; gIsrMouseDy = 0; gIsrMouseButtons = 0; // Both ports always announce themselves on the ST -- packets // arrive even when no stick is plugged in (state stays at zero). gJoyConnected[JOYSTICK_0] = true; gJoyConnected[JOYSTICK_1] = true; gKbdvecs = (_KBDVECS *)Kbdvbase(); gPacketRemaining = 0; gPacketKind = PKT_KIND_NONE; Supexec(patchIkbdVector); gHooked = true; } // The ACIA ISR writes gIsrState (keys), gIsrMouse* deltas, and // gIsrJoyByte[] at ~100 Hz max; the ~60-byte memcpy is essentially // never racing a write. Worst case is a single key or one packet // lagging one frame -- well under perceptible. void jlpInputPoll(void) { int32_t dx; int32_t dy; int32_t newX; int32_t newY; uint8_t btn; uint8_t joy; uint16_t i; memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState)); // Drain accumulated mouse deltas + latch button state. dx = gIsrMouseDx; dy = gIsrMouseDy; btn = gIsrMouseButtons; gIsrMouseDx = 0; gIsrMouseDy = 0; newX = (int32_t)gMouseAbsX + dx; newY = (int32_t)gMouseAbsY + dy; if (newX < 0) { newX = 0; } if (newX > SURFACE_WIDTH - 1) { newX = SURFACE_WIDTH - 1; } if (newY < 0) { newY = 0; } if (newY > SURFACE_HEIGHT - 1) { newY = SURFACE_HEIGHT - 1; } gMouseAbsX = (int16_t)newX; gMouseAbsY = (int16_t)newY; gMouseX = gMouseAbsX; gMouseY = gMouseAbsY; gMouseButtonState[MOUSE_BUTTON_LEFT] = (btn & MOUSE_HDR_LEFT_BTN) != 0; gMouseButtonState[MOUSE_BUTTON_RIGHT] = (btn & MOUSE_HDR_RIGHT_BTN) != 0; gMouseButtonState[MOUSE_BUTTON_MIDDLE] = false; // Decode latest joystick byte for each stick. Direction is digital // on the ST, so axes snap to the JOYSTICK_AXIS_* extremes. for (i = 0; i < JOYSTICK_COUNT; i++) { joy = gIsrJoyByte[i]; gJoyAxisX[i] = 0; gJoyAxisY[i] = 0; if (joy & JOY_BIT_LEFT) { gJoyAxisX[i] = JOYSTICK_AXIS_MIN; } if (joy & JOY_BIT_RIGHT) { gJoyAxisX[i] = JOYSTICK_AXIS_MAX; } if (joy & JOY_BIT_UP) { gJoyAxisY[i] = JOYSTICK_AXIS_MIN; } if (joy & JOY_BIT_DOWN) { gJoyAxisY[i] = JOYSTICK_AXIS_MAX; } gJoyButtonState[i][JOY_BUTTON_0] = (joy & JOY_BIT_FIRE) != 0; gJoyButtonState[i][JOY_BUTTON_1] = false; // ST has 1 fire button } } void jlpInputShutdown(void) { if (!gHooked) { return; } Supexec(restoreIkbdVector); gHooked = false; }