joeylib2/src/atarist/input.c
2026-06-30 18:07:12 -05:00

379 lines
12 KiB
C

// 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 <string.h>
#include <mint/osbind.h>
#include <mint/ostruct.h>
#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;
}