// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // Amiga keyboard + mouse input: opens a backdrop borderless window on // the JoeyLib CUSTOMSCREEN to receive IDCMP_RAWKEY, IDCMP_MOUSEMOVE, // and IDCMP_MOUSEBUTTONS events, then drains those messages each // jlInputPoll. // // RAWKEY is the lowest-level IDCMP event Intuition exposes: ie_Code // carries the Amiga raw key code (press in 0x00..0x7F, release with // bit 7 set), which we map straight to jlKeyE entries. The backdrop // borderless window covers the whole screen without drawing over our // planar bitmap (c2p writes to Screen->BitMap->Planes directly), and // WFLG_ACTIVATE ensures keyboard focus lands on us from the start. // // MOUSEMOVE messages carry MouseX/MouseY in window-relative pixels; // since the window is at (0,0) covering the whole screen, those are // also surface-space coordinates. WA_ReportMouse = TRUE is required // for MOUSEMOVE events to fire continuously rather than only on button // transitions. WFLG_RMBTRAP keeps the right mouse button from opening // Intuition's screen menu so right-clicks come to us as MENUDOWN. // // Typed characters: RAWKEY press events are also run through // keymap.library's MapRawKey with the message qualifier and dead-key // context, which applies the user's keymap (shift, caps, national // layouts, dead keys) and yields translated bytes for the core typed- // character queue. Intuition delivers key repeats as further RAWKEY // events, so OS auto-repeat produces repeated characters for free. #include #include #include #include #include #include #include #include #include #include #include #include "port.h" #include "inputInternal.h" // ----- Constants ----- #define AMIGA_KEY_TABLE_SIZE 128 #define AMIGA_KEY_RELEASE_BIT 0x80 #define AMIGA_KEY_CODE_MASK 0x7F // MapRawKey output buffer. A single keystroke maps to at most a few // bytes (dead-key compositions); 8 is comfortably above any keymap's // output. #define MAP_RAW_KEY_BUF_SIZE 8 // Keymaps encode arrows / F-keys / Help as ANSI control sequences // beginning with CSI (0x9B). The CSI byte itself would be filtered, // but the sequence's TAIL bytes are printable ('A' for cursor up, // "0~" for F1, ...) and would leak into the typed-character queue as // stray text -- so a mapping that starts with CSI is discarded whole. #define AMIGA_CSI 0x9B // keymap.library base referenced by the MapRawKey inline stub. Defined // here and opened explicitly (not left to libnix auto-open) so a // system without the library degrades to "no typed characters" instead // of failing at startup. struct Library *KeymapBase = NULL; // lowlevel.library base referenced by the ReadJoyPort and SetJoyPortAttrs // inline stubs, defined and opened here for the same reason: a boot drive // without Workbench's Libs/lowlevel.library degrades to "no joysticks". struct Library *LowLevelBase = NULL; // ----- External from hal.c ----- extern struct Screen *jlgScreen; // ----- Empty pointer image ----- // // Full-screen custom-screen games conventionally hide the system // pointer; we install an all-zero sprite via SetPointer so Workbench's // arrow doesn't drift over the playfield. Game code that needs a // cursor draws its own from jlMouseX/Y. // // Amiga hardware sprite data: 2 leading control words (VSTART/HSTART/ // VSTOP), one line of (planeA, planeB) data words, 2 trailing zero // words as terminator. All zeros yields a transparent pointer. Must // live in chip RAM since sprite DMA reads it. #define EMPTY_POINTER_BYTES (6 * sizeof(UWORD)) static UWORD *gEmptyPointer = NULL; // ----- Prototypes ----- static void drainMessages(void); static void joystickUnlock(int stick); static void mouseButtonEvent(jlMouseButtonE button, bool down); static void pollJoysticks(void); // ----- Module state ----- // Amiga raw-key code -> jlKeyE. Codes are layout-independent; the // OS has a separate keymap for producing characters. A second key for a // named key carries INPUT_KEY_SECOND. static const uint8_t gRawKeyToKey[AMIGA_KEY_TABLE_SIZE] = { [0x01] = JL_KEY_1, [0x02] = JL_KEY_2, [0x03] = JL_KEY_3, [0x04] = JL_KEY_4, [0x05] = JL_KEY_5, [0x06] = JL_KEY_6, [0x07] = JL_KEY_7, [0x08] = JL_KEY_8, [0x09] = JL_KEY_9, [0x0A] = JL_KEY_0, [0x10] = JL_KEY_Q, [0x11] = JL_KEY_W, [0x12] = JL_KEY_E, [0x13] = JL_KEY_R, [0x14] = JL_KEY_T, [0x15] = JL_KEY_Y, [0x16] = JL_KEY_U, [0x17] = JL_KEY_I, [0x18] = JL_KEY_O, [0x19] = JL_KEY_P, [0x1E] = JL_KEY_DOWN | INPUT_KEY_SECOND, // keypad 2, 4, 6 and 8: the arrows too, [0x20] = JL_KEY_A, [0x21] = JL_KEY_S, [0x22] = JL_KEY_D, [0x23] = JL_KEY_F, [0x24] = JL_KEY_G, [0x25] = JL_KEY_H, [0x26] = JL_KEY_J, [0x27] = JL_KEY_K, [0x28] = JL_KEY_L, [0x2D] = JL_KEY_LEFT | INPUT_KEY_SECOND, // as on every port with a keypad [0x2F] = JL_KEY_RIGHT | INPUT_KEY_SECOND, [0x31] = JL_KEY_Z, [0x32] = JL_KEY_X, [0x33] = JL_KEY_C, [0x34] = JL_KEY_V, [0x35] = JL_KEY_B, [0x36] = JL_KEY_N, [0x37] = JL_KEY_M, [0x3E] = JL_KEY_UP | INPUT_KEY_SECOND, [0x40] = JL_KEY_SPACE, [0x41] = JL_KEY_BACKSPACE, [0x42] = JL_KEY_TAB, [0x43] = JL_KEY_RETURN | INPUT_KEY_SECOND, // keypad Enter, as every port reads it [0x44] = JL_KEY_RETURN, [0x45] = JL_KEY_ESCAPE, [0x4C] = JL_KEY_UP, [0x4D] = JL_KEY_DOWN, [0x4E] = JL_KEY_RIGHT, [0x4F] = JL_KEY_LEFT, [0x50] = JL_KEY_F1, [0x51] = JL_KEY_F2, [0x52] = JL_KEY_F3, [0x53] = JL_KEY_F4, [0x54] = JL_KEY_F5, [0x55] = JL_KEY_F6, [0x56] = JL_KEY_F7, [0x57] = JL_KEY_F8, [0x58] = JL_KEY_F9, [0x59] = JL_KEY_F10, [0x60] = JL_KEY_LSHIFT, [0x61] = JL_KEY_RSHIFT, [0x63] = JL_KEY_LCTRL, [0x64] = JL_KEY_LALT, [0x65] = JL_KEY_LALT | INPUT_KEY_SECOND, // right Alt: either Alt, as on the PC ports }; static struct Window *gWindow = NULL; // AmigaOS exposes joystick port 1 (the dedicated stick) via lowlevel // ReadJoyPort(1); port 0 shares the mouse port. We map our public // JL_JOYSTICK_0 to Amiga port 1 so the primary stick is what retro games // expect. static const ULONG gAmigaPortForStick[JL_JOYSTICK_COUNT] = { 1, 0 }; // lowlevel.library senses a port's controller again now and then, and a // two-button stick held with both buttons down reads as no controller // then, until it moves (measured under FS-UAE). So a port that has shown // a stick is set to one (SetJoyPortAttrs) and stays one until // jlJoystickReset or shutdown hands it back to autosensing. static bool gJoyLocked[JL_JOYSTICK_COUNT]; // lowlevel.library reports a one-button stick and a two-button one alike // (a second button is pin 9, which reads as released until it is pressed), // and most Amiga owners use one-button Atari sticks. So a joystick counts as // one-button (jlgJoyOneButton: a hold of its button is button 1) until its // second button has been seen pressed, and again once its port has no // controller. A CD32 game controller always has both. static bool gJoySecondButton[JL_JOYSTICK_COUNT]; // Keys and mouse buttons as Intuition's messages leave them, and pressed // since the last poll: a tap between two polls still reads down for one // (jlInputLatch). The keys are latched only when a message changed them or // the last latch showed a tap that must now go up. static uint8_t gKeyHeld [JL_KEY_COUNT]; static uint8_t gKeyTapped [JL_KEY_COUNT]; static bool gKeysChanged; static bool gKeyTapShown; static uint8_t gMouseHeld [JL_MOUSE_BUTTON_COUNT]; static uint8_t gMouseTapped [JL_MOUSE_BUTTON_COUNT]; // ----- Internal helpers ----- static void drainMessages(void) { struct IntuiMessage *msg; struct InputEvent ie; UBYTE mapBuf[MAP_RAW_KEY_BUF_SIZE]; WORD mapCount; WORD mapIdx; bool mapChord; ULONG msgClass; UWORD msgCode; int16_t msgMouseX; int16_t msgMouseY; uint8_t code; uint8_t entry; uint8_t key; bool isRelease; if (gWindow == NULL) { return; } while ((msg = (struct IntuiMessage *)GetMsg(gWindow->UserPort)) != NULL) { msgClass = msg->Class; msgCode = msg->Code; msgMouseX = (int16_t)msg->MouseX; msgMouseY = (int16_t)msg->MouseY; // Typed-character translation must run BEFORE ReplyMsg: the // qualifier and the dead-key context behind IAddress belong // to the message and may be recycled once replied. Release // events (bit 7) type nothing. mapCount = 0; mapChord = (msg->Qualifier & IEQUALIFIER_CONTROL) != 0; if (msgClass == IDCMP_RAWKEY && (msgCode & AMIGA_KEY_RELEASE_BIT) == 0 && KeymapBase != NULL) { ie.ie_NextEvent = NULL; ie.ie_Class = IECLASS_RAWKEY; ie.ie_SubClass = 0; ie.ie_Code = msgCode; ie.ie_Qualifier = msg->Qualifier; // Dead-key context: IAddress points at the previous-code // storage MapRawKey wants in ie_EventAddress (RKM recipe). ie.ie_EventAddress = (APTR)*((ULONG *)msg->IAddress); mapCount = MapRawKey(&ie, (STRPTR)mapBuf, MAP_RAW_KEY_BUF_SIZE, NULL); if (mapCount > 0 && mapBuf[0] == AMIGA_CSI) { mapCount = 0; } } ReplyMsg((struct Message *)msg); switch (msgClass) { case IDCMP_RAWKEY: isRelease = (msgCode & AMIGA_KEY_RELEASE_BIT) != 0; code = (uint8_t)(msgCode & AMIGA_KEY_CODE_MASK); entry = gRawKeyToKey[code]; key = (uint8_t)(entry & INPUT_KEY_MASK); if (key != JL_KEY_NONE) { gKeyHeld[key] = inputKeyHeld(gKeyHeld[key], entry, !isRelease); if (!isRelease) { gKeyTapped[key] = 1u; } gKeysChanged = true; } // MapRawKey returns -1 on overflow (composition longer // than the buffer) -- nothing usable, skip. // A Ctrl chord types nothing (jlInputCharTyped); Alt is a // layer of the keymap's characters. for (mapIdx = 0; mapIdx < mapCount; mapIdx++) { jlInputCharTyped((uint8_t)mapBuf[mapIdx], mapChord); } break; case IDCMP_MOUSEMOVE: jlInputMouseMove(msgMouseX, msgMouseY); break; case IDCMP_INACTIVEWINDOW: // The keys and buttons it lets go elsewhere are never // reported here: let them all go now. memset(gKeyHeld, 0, sizeof(gKeyHeld)); memset(gMouseHeld, 0, sizeof(gMouseHeld)); gKeysChanged = true; break; case IDCMP_MOUSEBUTTONS: switch (msgCode) { case SELECTDOWN: mouseButtonEvent(JL_MOUSE_BUTTON_LEFT, true); break; case SELECTUP: mouseButtonEvent(JL_MOUSE_BUTTON_LEFT, false); break; case MENUDOWN: mouseButtonEvent(JL_MOUSE_BUTTON_RIGHT, true); break; case MENUUP: mouseButtonEvent(JL_MOUSE_BUTTON_RIGHT, false); break; case MIDDLEDOWN: mouseButtonEvent(JL_MOUSE_BUTTON_MIDDLE, true); break; case MIDDLEUP: mouseButtonEvent(JL_MOUSE_BUTTON_MIDDLE, false); break; default: break; } // MOUSEBUTTONS messages also carry the mouse position. jlInputMouseMove(msgMouseX, msgMouseY); break; default: break; } } } // Hand a locked port back to lowlevel.library's autosensing (which also // frees the potgo bits it took for the stick). static void joystickUnlock(int stick) { if (!gJoyLocked[stick]) { return; } SetJoyPortAttrs(gAmigaPortForStick[stick], SJA_Reinitialize, TRUE, TAG_DONE); gJoyLocked[stick] = false; } // A mouse button's message: held as it leaves it, and pressed since the // last poll. static void mouseButtonEvent(jlMouseButtonE button, bool down) { gMouseHeld[button] = down; if (down) { gMouseTapped[button] = 1u; } } // Read both joystick ports via lowlevel.library. Devices that report // JP_TYPE_JOYSTK or JP_TYPE_GAMECTLR populate gJoy*; anything else // (mouse on port 0, no device, unknown) leaves the stick disconnected // with zeroed state. static void pollJoysticks(void) { ULONG state; ULONG type; int stick; if (LowLevelBase == NULL) { return; } for (stick = 0; stick < JL_JOYSTICK_COUNT; stick++) { state = ReadJoyPort(gAmigaPortForStick[stick]); type = state & JP_TYPE_MASK; if (type == JP_TYPE_JOYSTK && !gJoyLocked[stick]) { gJoyLocked[stick] = SetJoyPortAttrs(gAmigaPortForStick[stick], SJA_Type, SJA_TYPE_JOYSTK, TAG_DONE) != FALSE; } if (type != JP_TYPE_JOYSTK && type != JP_TYPE_GAMECTLR) { jlgJoyConnected[stick] = false; jlgJoyAxisX[stick] = 0; jlgJoyAxisY[stick] = 0; jlgJoyOneButton[stick] = false; gJoySecondButton[stick] = false; jlgJoyButtonState[stick][JL_JOY_BUTTON_0] = false; jlgJoyButtonState[stick][JL_JOY_BUTTON_1] = false; continue; } jlgJoyConnected[stick] = true; jlgJoyAxisX[stick] = 0; if (state & JPF_JOY_LEFT) { jlgJoyAxisX[stick] = JL_JOYSTICK_AXIS_MIN; } if (state & JPF_JOY_RIGHT) { jlgJoyAxisX[stick] = JL_JOYSTICK_AXIS_MAX; } jlgJoyAxisY[stick] = 0; if (state & JPF_JOY_UP) { jlgJoyAxisY[stick] = JL_JOYSTICK_AXIS_MIN; } if (state & JPF_JOY_DOWN) { jlgJoyAxisY[stick] = JL_JOYSTICK_AXIS_MAX; } // Standard 1-button stick reports JPF_BUTTON_RED only. CD32 // game controllers also report BLUE for the second face button. jlgJoyButtonState[stick][JL_JOY_BUTTON_0] = (state & JPF_BUTTON_RED) != 0; jlgJoyButtonState[stick][JL_JOY_BUTTON_1] = (state & JPF_BUTTON_BLUE) != 0; if (type == JP_TYPE_GAMECTLR || (state & JPF_BUTTON_BLUE) != 0) { gJoySecondButton[stick] = true; } jlgJoyOneButton[stick] = !gJoySecondButton[stick]; } } // ----- HAL API (alphabetical) ----- // Sticks are digital, so nothing to calibrate: the port goes back to // autosensing, as a stick swapped for a mouse or a game controller needs. void jlpJoystickReset(jlJoystickE js) { joystickUnlock((int)js); } void jlpInputInit(void) { memset(jlgKeyState, 0, sizeof(jlgKeyState)); memset(jlgKeyPrev, 0, sizeof(jlgKeyPrev)); memset(gKeyHeld, 0, sizeof(gKeyHeld)); memset(gKeyTapped, 0, sizeof(gKeyTapped)); gKeysChanged = true; memset(gMouseHeld, 0, sizeof(gMouseHeld)); memset(gMouseTapped, 0, sizeof(gMouseTapped)); if (jlgScreen == NULL) { return; } gWindow = OpenWindowTags(NULL, (ULONG)WA_CustomScreen, (ULONG)jlgScreen, (ULONG)WA_Left, (ULONG)0, (ULONG)WA_Top, (ULONG)0, (ULONG)WA_Width, (ULONG)jlgScreen->Width, (ULONG)WA_Height, (ULONG)jlgScreen->Height, (ULONG)WA_Flags, (ULONG)(WFLG_BACKDROP | WFLG_BORDERLESS | WFLG_ACTIVATE | WFLG_RMBTRAP | WFLG_NOCAREREFRESH | WFLG_REPORTMOUSE), (ULONG)WA_IDCMP, (ULONG)(IDCMP_RAWKEY | IDCMP_MOUSEMOVE | IDCMP_MOUSEBUTTONS | IDCMP_INACTIVEWINDOW), TAG_DONE); if (gWindow != NULL) { jlgMouseAttached = true; // Intuition delivers IDCMP mouse events on this window gEmptyPointer = (UWORD *)AllocMem((ULONG)EMPTY_POINTER_BYTES, (ULONG)(MEMF_CHIP | MEMF_CLEAR)); if (gEmptyPointer != NULL) { SetPointer(gWindow, gEmptyPointer, 1, 16, 0, 0); } } // lowlevel.library (OS 3.1, V40) is a disk library: Kickstart has none // in ROM. If the boot drive has none either, the joystick API silently // reports disconnected sticks rather than failing init. memset(gJoyLocked, 0, sizeof(gJoyLocked)); LowLevelBase = OpenLibrary((CONST_STRPTR)"lowlevel.library", 0); // keymap.library (OS 2.0+) drives the typed-character path. If // absent, jlInputGetChar just stays empty; the predicates still // work off the raw codes. KeymapBase = OpenLibrary((CONST_STRPTR)"keymap.library", 0); } void jlpInputPoll(void) { drainMessages(); if (gKeysChanged || gKeyTapShown) { gKeysChanged = false; gKeyTapShown = jlInputLatch(jlgKeyState, gKeyHeld, gKeyTapped, JL_KEY_COUNT); } jlInputLatch(jlgMouseButtonState, gMouseHeld, gMouseTapped, JL_MOUSE_BUTTON_COUNT); pollJoysticks(); } void jlpInputShutdown(void) { if (KeymapBase != NULL) { CloseLibrary(KeymapBase); KeymapBase = NULL; } if (LowLevelBase != NULL) { // The ports' types are lowlevel.library's, shared with every // program: none may stay set to a stick after this one. joystickUnlock(JL_JOYSTICK_0); joystickUnlock(JL_JOYSTICK_1); CloseLibrary(LowLevelBase); LowLevelBase = NULL; } if (gWindow == NULL) { return; } drainMessages(); if (gEmptyPointer != NULL) { // Restore the system pointer before freeing our image so // Intuition isn't holding a stale chip-RAM reference. ClearPointer(gWindow); FreeMem(gEmptyPointer, (ULONG)EMPTY_POINTER_BYTES); gEmptyPointer = NULL; } CloseWindow(gWindow); gWindow = NULL; }