diff --git a/examples/keys/keys.c b/examples/keys/keys.c index 9beb748..5d1d29f 100644 --- a/examples/keys/keys.c +++ b/examples/keys/keys.c @@ -1,13 +1,15 @@ -// Visual keyboard demo: one square per JoeyKeyE, filled bright when -// the key is held and dim otherwise. Press ESC to quit. +// Visual keyboard + mouse demo: one square per JoeyKeyE, lit when its +// key is held, and a small pointer drawn at the live mouse position. +// Holding the left mouse button while the pointer is over a cell lights +// it as if the corresponding key were down. Press ESC to quit. // -// The first paint fills every cell and does one full-surface present. -// Thereafter the loop compares each cell against its previous drawn -// state and only redraws + presents the cells that changed. Full- -// surface chunky-to-planar conversion is expensive on 68000-class -// hardware (hundreds of milliseconds for 320x200x4bpp on Amiga / ST); -// per-cell rect presents keep the response tight regardless of how -// fast the host can convert a full frame. +// The render loop only redraws cells whose target lit state changed +// since last frame -- so on idle frames, the only work is the cursor +// erase + redraw + a tiny rect-present pair. The cursor erase is +// implemented by redrawing the cell that contained the *previous* +// cursor position; in the gap regions between cells the cursor will +// briefly leave a trail until that cell is touched again, which is an +// acceptable demo-quality compromise. #include @@ -21,14 +23,24 @@ #define MARGIN_X 2 #define MARGIN_Y 6 +#define CURSOR_W 4 +#define CURSOR_H 4 + #define COLOR_BACKGROUND 0 #define COLOR_UNLIT 1 #define COLOR_LIT 2 +#define COLOR_CURSOR 3 + +#define CELL_NONE ((int16_t)-1) static void buildPalette(SurfaceT *screen); +static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow); +static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow); static void drawCell(SurfaceT *screen, int16_t col, int16_t row, bool lit); -static void drawAllCells(SurfaceT *screen); -static void presentChangedCells(SurfaceT *screen); +static void drawCursor(SurfaceT *screen, int16_t x, int16_t y); +static void initialPaint(SurfaceT *screen); +static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow); +static void updateCursor(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow); // Keys laid out row-by-row. KEY_NONE cells stay blank. Shape roughly // resembles a real keyboard (top number row, then QWERTY rows, then a @@ -42,7 +54,11 @@ static const JoeyKeyE gKeyGrid[GRID_ROWS][GRID_COLS] = { { KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10 } }; -static bool gCellLit[GRID_ROWS][GRID_COLS]; +static bool gCellLit[GRID_ROWS][GRID_COLS]; +static int16_t gLastCursorX = -100; +static int16_t gLastCursorY = -100; +static int16_t gLastCursorCol = CELL_NONE; +static int16_t gLastCursorRow = CELL_NONE; static void buildPalette(SurfaceT *screen) { @@ -55,11 +71,51 @@ static void buildPalette(SurfaceT *screen) { colors[COLOR_BACKGROUND] = 0x0000; // black colors[COLOR_UNLIT] = 0x0333; // dark gray colors[COLOR_LIT] = 0x00F0; // bright green + colors[COLOR_CURSOR] = 0x0FFF; // white paletteSet(screen, 0, colors); } +static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow) { + int16_t col; + int16_t row; + int16_t cx; + int16_t cy; + + *outCol = CELL_NONE; + *outRow = CELL_NONE; + for (row = 0; row < GRID_ROWS; row++) { + for (col = 0; col < GRID_COLS; col++) { + cx = (int16_t)(MARGIN_X + col * (CELL_W + GAP)); + cy = (int16_t)(MARGIN_Y + row * (CELL_H + GAP)); + if (px >= cx && px < (cx + CELL_W) && py >= cy && py < (cy + CELL_H)) { + *outCol = col; + *outRow = row; + return; + } + } + } +} + + +static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow) { + JoeyKeyE key; + + key = gKeyGrid[row][col]; + if (key == KEY_NONE) { + return false; + } + if (joeyKeyDown(key)) { + return true; + } + if (col == cursorCol && row == cursorRow && joeyMouseDown(MOUSE_BUTTON_LEFT)) { + return true; + } + return false; +} + + static void drawCell(SurfaceT *screen, int16_t col, int16_t row, bool lit) { int16_t x; int16_t y; @@ -72,27 +128,32 @@ static void drawCell(SurfaceT *screen, int16_t col, int16_t row, bool lit) { } -static void drawAllCells(SurfaceT *screen) { +static void drawCursor(SurfaceT *screen, int16_t x, int16_t y) { + fillRect(screen, x, y, CURSOR_W, CURSOR_H, COLOR_CURSOR); +} + + +static void initialPaint(SurfaceT *screen) { int16_t col; int16_t row; JoeyKeyE key; - bool lit; + surfaceClear(screen, COLOR_BACKGROUND); for (row = 0; row < GRID_ROWS; row++) { for (col = 0; col < GRID_COLS; col++) { key = gKeyGrid[row][col]; if (key == KEY_NONE) { continue; } - lit = joeyKeyDown(key); - drawCell(screen, col, row, lit); - gCellLit[row][col] = lit; + drawCell(screen, col, row, false); + gCellLit[row][col] = false; } } + surfacePresent(screen); } -static void presentChangedCells(SurfaceT *screen) { +static void presentChangedCells(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow) { int16_t col; int16_t row; JoeyKeyE key; @@ -106,7 +167,7 @@ static void presentChangedCells(SurfaceT *screen) { if (key == KEY_NONE) { continue; } - lit = joeyKeyDown(key); + lit = cellTargetLit(col, row, cursorCol, cursorRow); if (lit == gCellLit[row][col]) { continue; } @@ -120,9 +181,46 @@ static void presentChangedCells(SurfaceT *screen) { } +// Erase the previous cursor (by redrawing the cell that held it) and +// stamp the new cursor at the current mouse position. Both rects are +// presented; if the cursor stayed inside the same cell only one rect +// pair is touched, so steady-state cost is small. +static void updateCursor(SurfaceT *screen, int16_t cursorCol, int16_t cursorRow) { + int16_t mouseX; + int16_t mouseY; + + mouseX = joeyMouseX(); + mouseY = joeyMouseY(); + + if (gLastCursorX != mouseX || gLastCursorY != mouseY) { + if (gLastCursorCol != CELL_NONE) { + drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]); + surfacePresentRect(screen, + (int16_t)(MARGIN_X + gLastCursorCol * (CELL_W + GAP)), + (int16_t)(MARGIN_Y + gLastCursorRow * (CELL_H + GAP)), + CELL_W, CELL_H); + } else if (gLastCursorX >= 0 && gLastCursorY >= 0) { + // Old cursor was in a gap region. Stamp background over it. + fillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND); + surfacePresentRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H); + } + } + + drawCursor(screen, mouseX, mouseY); + surfacePresentRect(screen, mouseX, mouseY, CURSOR_W, CURSOR_H); + + gLastCursorX = mouseX; + gLastCursorY = mouseY; + gLastCursorCol = cursorCol; + gLastCursorRow = cursorRow; +} + + int main(void) { JoeyConfigT config; SurfaceT *screen; + int16_t cursorCol; + int16_t cursorRow; config.hostMode = HOST_MODE_TAKEOVER; config.codegenBytes = 32 * 1024; @@ -144,17 +242,17 @@ int main(void) { buildPalette(screen); scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); - surfaceClear(screen, COLOR_BACKGROUND); + initialPaint(screen); joeyInputPoll(); - drawAllCells(screen); - surfacePresent(screen); for (;;) { joeyInputPoll(); if (joeyKeyPressed(KEY_ESCAPE)) { break; } - presentChangedCells(screen); + cellAtPoint(joeyMouseX(), joeyMouseY(), &cursorCol, &cursorRow); + presentChangedCells(screen, cursorCol, cursorRow); + updateCursor(screen, cursorCol, cursorRow); } joeyShutdown(); diff --git a/include/joey/input.h b/include/joey/input.h index 2282b73..8bf0ea3 100644 --- a/include/joey/input.h +++ b/include/joey/input.h @@ -1,13 +1,22 @@ -// Keyboard input polling. +// Keyboard and mouse input polling. // // Call joeyInputPoll() once per frame (typically right before -// drawing) to refresh the keyboard state. After polling, the -// joeyKey* predicates return the current state of every key: +// drawing) to refresh both keyboard and mouse state. After polling, +// the joeyKey* predicates return the current state of every key: // // joeyKeyDown(k) -- is key k held down right now // joeyKeyPressed(k) -- rising edge since the previous poll // joeyKeyReleased(k) -- falling edge since the previous poll // +// And the mouse predicates return the pointer state: +// +// joeyMouseX/Y() -- pointer position in surface +// coords (0..SURFACE_WIDTH-1, +// 0..SURFACE_HEIGHT-1) +// joeyMouseDown(b) -- is button b held right now +// joeyMousePressed(b) -- rising edge since last poll +// joeyMouseReleased(b) -- falling edge since last poll +// // Edge predicates are one-shot: they return true only in the // frame the transition occurred and false thereafter. @@ -39,9 +48,25 @@ typedef enum { KEY_COUNT } JoeyKeyE; +typedef enum { + MOUSE_BUTTON_NONE = 0, + MOUSE_BUTTON_LEFT, + MOUSE_BUTTON_RIGHT, + MOUSE_BUTTON_MIDDLE, + + MOUSE_BUTTON_COUNT +} JoeyMouseButtonE; + void joeyInputPoll(void); + bool joeyKeyDown(JoeyKeyE key); bool joeyKeyPressed(JoeyKeyE key); bool joeyKeyReleased(JoeyKeyE key); +int16_t joeyMouseX(void); +int16_t joeyMouseY(void); +bool joeyMouseDown(JoeyMouseButtonE button); +bool joeyMousePressed(JoeyMouseButtonE button); +bool joeyMouseReleased(JoeyMouseButtonE button); + #endif diff --git a/scripts/run-dos.sh b/scripts/run-dos.sh index 1f97860..b1cf932 100755 --- a/scripts/run-dos.sh +++ b/scripts/run-dos.sh @@ -23,4 +23,14 @@ if [[ ! -f "$bin_dir/$file" ]]; then exit 1 fi -exec dosbox -c "C:" -c "$file" -c "pause" --exit "$bin_dir" +# mouse_capture=seamless is required for DOSBox-Staging when running +# inside a VM whose host already manages the pointer; without it the +# default capture-on-click behavior fights the VM's grab and mouse +# input is unusable. On plain DOSBox this -set flag is unknown and is +# logged once as a warning, then ignored -- harmless either way. +exec dosbox \ + -set "mouse_capture=seamless" \ + -c "C:" \ + -c "$file" \ + -c "pause" \ + --exit "$bin_dir" diff --git a/scripts/run-iigs.sh b/scripts/run-iigs.sh index 4bbf98f..005aebe 100755 --- a/scripts/run-iigs.sh +++ b/scripts/run-iigs.sh @@ -71,6 +71,16 @@ EOF # the rest of the temp state, rather than polluting the directory # the user invoked the script from. cd "$work" +# GSplus accepts any config.kegs key as a CLI override via "- +# ". g_limit_speed=2 caps emulation at the IIgs's native +# 2.8 MHz; without this override GSplus boots at 8 MHz and our +# performance characteristics don't match real hardware. The other +# settings (0=unlimited, 1=1.024 MHz, 3=8 MHz) are reachable through +# the in-emulator speed-cycle key if needed. +# # No exec: let the bash EXIT trap fire so the scratch dir (and the # config.kegs GSplus auto-creates) gets cleaned up. -"$gsplus" -rom "$rom" -s5d1 "$work/boot.po" -s5d2 "$work/joey.2mg" +"$gsplus" -rom "$rom" \ + -s5d1 "$work/boot.po" \ + -s5d2 "$work/joey.2mg" \ + -g_limit_speed 2 diff --git a/src/core/input.c b/src/core/input.c index df3edbe..73e33ee 100644 --- a/src/core/input.c +++ b/src/core/input.c @@ -1,7 +1,12 @@ -// Public input API. Maintains two key-state buffers: the current -// state populated by the port's halInputPoll, and the previous -// state captured just before polling so rising/falling edges can -// be computed without the port having to track them. +// Public input API. Maintains current/previous state buffers for both +// the keyboard (gKeyState/gKeyPrev) and the mouse buttons +// (gMouseButtonState/gMouseButtonPrev). joeyInputPoll snapshots the +// previous state, then asks the port HAL to refresh the current state; +// the predicates derive held/edge values from those two buffers. +// +// Mouse position (gMouseX/gMouseY) is plain current state -- there is +// no "previous position" exposed; games that want delta movement can +// remember the previous joeyMouseX/Y themselves. #include @@ -9,12 +14,18 @@ #include "hal.h" #include "inputInternal.h" -bool gKeyState[KEY_COUNT]; -bool gKeyPrev [KEY_COUNT]; +bool gKeyState [KEY_COUNT]; +bool gKeyPrev [KEY_COUNT]; + +int16_t gMouseX = 0; +int16_t gMouseY = 0; +bool gMouseButtonState[MOUSE_BUTTON_COUNT]; +bool gMouseButtonPrev [MOUSE_BUTTON_COUNT]; void joeyInputPoll(void) { - memcpy(gKeyPrev, gKeyState, sizeof(gKeyState)); + memcpy(gKeyPrev, gKeyState, sizeof(gKeyState)); + memcpy(gMouseButtonPrev, gMouseButtonState, sizeof(gMouseButtonState)); halInputPoll(); } @@ -41,3 +52,37 @@ bool joeyKeyReleased(JoeyKeyE key) { } return !gKeyState[key] && gKeyPrev[key]; } + + +bool joeyMouseDown(JoeyMouseButtonE button) { + if (button <= MOUSE_BUTTON_NONE || button >= MOUSE_BUTTON_COUNT) { + return false; + } + return gMouseButtonState[button]; +} + + +bool joeyMousePressed(JoeyMouseButtonE button) { + if (button <= MOUSE_BUTTON_NONE || button >= MOUSE_BUTTON_COUNT) { + return false; + } + return gMouseButtonState[button] && !gMouseButtonPrev[button]; +} + + +bool joeyMouseReleased(JoeyMouseButtonE button) { + if (button <= MOUSE_BUTTON_NONE || button >= MOUSE_BUTTON_COUNT) { + return false; + } + return !gMouseButtonState[button] && gMouseButtonPrev[button]; +} + + +int16_t joeyMouseX(void) { + return gMouseX; +} + + +int16_t joeyMouseY(void) { + return gMouseY; +} diff --git a/src/core/inputInternal.h b/src/core/inputInternal.h index 2e5cd4e..8e865bc 100644 --- a/src/core/inputInternal.h +++ b/src/core/inputInternal.h @@ -1,8 +1,10 @@ // Internal input state shared between core and per-port HAL. // -// Per-port halInputPoll() writes directly into gKeyState[]: set -// entries to true for keys currently held, false for released keys. -// The core compares gKeyState to gKeyPrev each joeyInputPoll to +// Per-port halInputPoll() writes directly into gKeyState[] and the +// gMouse* globals: set key entries to true for keys currently held, +// store pointer position in gMouseX/gMouseY (surface-space pixels), +// and set gMouseButtonState[] entries true for buttons currently held. +// The core compares against the *Prev shadow each joeyInputPoll to // derive edge events. #ifndef JOEYLIB_INPUT_INTERNAL_H @@ -14,4 +16,9 @@ extern bool gKeyState[KEY_COUNT]; extern bool gKeyPrev [KEY_COUNT]; +extern int16_t gMouseX; +extern int16_t gMouseY; +extern bool gMouseButtonState[MOUSE_BUTTON_COUNT]; +extern bool gMouseButtonPrev [MOUSE_BUTTON_COUNT]; + #endif diff --git a/src/port/amiga/input.c b/src/port/amiga/input.c index 8ccf99c..0d39dbe 100644 --- a/src/port/amiga/input.c +++ b/src/port/amiga/input.c @@ -1,6 +1,7 @@ -// Amiga keyboard input: opens a backdrop borderless window on the -// JoeyLib CUSTOMSCREEN to receive IDCMP_RAWKEY events, then drains -// those messages each joeyInputPoll to update gKeyState. +// 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 +// joeyInputPoll. // // RAWKEY is the lowest-level IDCMP event Intuition exposes: ie_Code // carries the Amiga raw key code (press in 0x00..0x7F, release with @@ -8,6 +9,13 @@ // 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. #include @@ -35,7 +43,7 @@ extern struct Screen *gScreen; // ----- Prototypes ----- -static void drainKeyMessages(void); +static void drainMessages(void); // ----- Module state ----- @@ -107,9 +115,12 @@ static struct Window *gWindow = NULL; // ----- Internal helpers ----- -static void drainKeyMessages(void) { +static void drainMessages(void) { struct IntuiMessage *msg; - UWORD rawCode; + UWORD msgClass; + UWORD msgCode; + int16_t msgMouseX; + int16_t msgMouseY; uint8_t code; uint8_t key; bool isRelease; @@ -118,16 +129,42 @@ static void drainKeyMessages(void) { return; } while ((msg = (struct IntuiMessage *)GetMsg(gWindow->UserPort)) != NULL) { - if (msg->Class == IDCMP_RAWKEY) { - rawCode = msg->Code; - isRelease = (rawCode & AMIGA_KEY_RELEASE_BIT) != 0; - code = (uint8_t)(rawCode & AMIGA_KEY_CODE_MASK); - key = gRawKeyToKey[code]; - if (key != KEY_NONE) { - gKeyState[key] = !isRelease; - } - } + msgClass = msg->Class; + msgCode = msg->Code; + msgMouseX = (int16_t)msg->MouseX; + msgMouseY = (int16_t)msg->MouseY; ReplyMsg((struct Message *)msg); + + switch (msgClass) { + case IDCMP_RAWKEY: + isRelease = (msgCode & AMIGA_KEY_RELEASE_BIT) != 0; + code = (uint8_t)(msgCode & AMIGA_KEY_CODE_MASK); + key = gRawKeyToKey[code]; + if (key != KEY_NONE) { + gKeyState[key] = !isRelease; + } + break; + case IDCMP_MOUSEMOVE: + gMouseX = msgMouseX; + gMouseY = msgMouseY; + break; + case IDCMP_MOUSEBUTTONS: + switch (msgCode) { + case SELECTDOWN: gMouseButtonState[MOUSE_BUTTON_LEFT] = true; break; + case SELECTUP: gMouseButtonState[MOUSE_BUTTON_LEFT] = false; break; + case MENUDOWN: gMouseButtonState[MOUSE_BUTTON_RIGHT] = true; break; + case MENUUP: gMouseButtonState[MOUSE_BUTTON_RIGHT] = false; break; + case MIDDLEDOWN: gMouseButtonState[MOUSE_BUTTON_MIDDLE] = true; break; + case MIDDLEUP: gMouseButtonState[MOUSE_BUTTON_MIDDLE] = false; break; + default: break; + } + // MOUSEBUTTONS messages also carry the mouse position. + gMouseX = msgMouseX; + gMouseY = msgMouseY; + break; + default: + break; + } } } @@ -152,14 +189,22 @@ void halInputInit(void) { | WFLG_BORDERLESS | WFLG_ACTIVATE | WFLG_RMBTRAP - | WFLG_NOCAREREFRESH), - (ULONG)WA_IDCMP, (ULONG)IDCMP_RAWKEY, + | WFLG_NOCAREREFRESH + | WFLG_REPORTMOUSE), + (ULONG)WA_IDCMP, (ULONG)(IDCMP_RAWKEY + | IDCMP_MOUSEMOVE + | IDCMP_MOUSEBUTTONS), TAG_DONE); + + if (gWindow != NULL) { + gMouseX = (int16_t)(gWindow->Width / 2); + gMouseY = (int16_t)(gWindow->Height / 2); + } } void halInputPoll(void) { - drainKeyMessages(); + drainMessages(); } @@ -167,7 +212,7 @@ void halInputShutdown(void) { if (gWindow == NULL) { return; } - drainKeyMessages(); + drainMessages(); CloseWindow(gWindow); gWindow = NULL; } diff --git a/src/port/atarist/input.c b/src/port/atarist/input.c index 2d20a52..5a639ed 100644 --- a/src/port/atarist/input.c +++ b/src/port/atarist/input.c @@ -1,19 +1,23 @@ -// Atari ST keyboard input: replaces the TOS ikbdsys vector in the -// KBDVECS table so every byte from the IKBD ACIA comes through our +// 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 track how many trailing packet -// bytes to discard; everything else is treated as a key event. +// fixed number of trailing bytes. We dispatch based on header: // -// The ISR writes into a private gIsrState buffer; halInputPoll copies -// it into gKeyState under a raised IPL so the snapshot is atomic -// relative to the ACIA interrupt. This two-buffer split is what makes -// joeyKeyPressed edge detection work -- joeyInputPoll snapshots -// gKeyState into gKeyPrev *before* halInputPoll runs, so gKeyState -// must only advance during halInputPoll, never at interrupt time. +// 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; halInputPoll copies them into the public globals. +// joeyKeyPressed edge detection requires that public gKeyState only +// advance during halInputPoll, never at interrupt time -- joeyInputPoll +// snapshots gKeyState into gKeyPrev before halInputPoll runs. #include @@ -22,6 +26,7 @@ #include "hal.h" #include "inputInternal.h" +#include "joey/surface.h" // ----- Constants ----- @@ -42,6 +47,15 @@ #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 + +// 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 + // ----- Prototypes ----- static long patchIkbdVector(void); @@ -117,13 +131,26 @@ static const uint8_t gScanToKey[SCAN_TABLE_SIZE] = { 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; static volatile bool 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; + // ----- Internal helpers ----- // Runs in MFP ACIA interrupt context. Reads one byte from the ACIA, -// consumes trailing packet bytes, and maps key codes into gKeyState. +// 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; @@ -133,11 +160,26 @@ static long ikbdHandler(void) { byte = *ST_ACIA_DATA; if (gPacketRemaining != 0) { + if (gPacketKind == PKT_KIND_REL_MOUSE) { + // mouse-packet payload: byte 0 = dx (signed), byte 1 = dy + if (gMousePacketByte == 0) { + gIsrMouseDx += (int8_t)byte; + gMousePacketByte = 1; + } else { + gIsrMouseDy += (int8_t)byte; + gMousePacketByte = 0; + } + } 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; @@ -158,6 +200,10 @@ static long ikbdHandler(void) { 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; } @@ -194,18 +240,57 @@ void halInputInit(void) { memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset((void *)gIsrState, 0, sizeof(gIsrState)); + gMouseAbsX = SURFACE_WIDTH / 2; + gMouseAbsY = SURFACE_HEIGHT / 2; + gMouseX = gMouseAbsX; + gMouseY = gMouseAbsY; + gIsrMouseDx = 0; + gIsrMouseDy = 0; + gIsrMouseButtons = 0; + gKbdvecs = (_KBDVECS *)Kbdvbase(); gPacketRemaining = 0; + gPacketKind = PKT_KIND_NONE; Supexec(patchIkbdVector); gHooked = true; } -// The ACIA ISR only writes gIsrState; bytes land at ~100 Hz max so the -// ~60-byte memcpy is essentially never racing a write. Worst case is a -// single key lagging one frame -- well under perceptible. +// The ACIA ISR writes gIsrState (keys) and the gIsrMouse* deltas at +// ~100 Hz max; the ~60-byte memcpy is essentially never racing a write. +// Worst case is a single key or one mouse packet lagging one frame -- +// well under perceptible. void halInputPoll(void) { + int32_t dx; + int32_t dy; + int32_t newX; + int32_t newY; + uint8_t btn; + 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; } diff --git a/src/port/dos/hal.c b/src/port/dos/hal.c index 35092dc..a67151b 100644 --- a/src/port/dos/hal.c +++ b/src/port/dos/hal.c @@ -9,12 +9,16 @@ // // Access to VGA memory uses DJGPP's nearptr mechanism for speed. +#include +#include #include +#include #include #include #include #include +#include #include #include "hal.h" @@ -29,7 +33,9 @@ // ----- Prototypes ----- +static void crashHandler(int sig); static void expandAndWriteLine(const SurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst); +static void installCrashLog(void); static void uploadPalette(const SurfaceT *src); static void uploadPaletteIfNeeded(const SurfaceT *src); @@ -37,6 +43,7 @@ static void uploadPaletteIfNeeded(const SurfaceT *src); static uint8_t *gVgaMem = NULL; static bool gNearEnabled = false; +static FILE *gCrashLog = NULL; // Cached palette from the last present. VGA DAC programming is ~768 // outportb calls; skip it when the source palette is unchanged. @@ -88,11 +95,82 @@ static void uploadPalette(const SurfaceT *src) { } +// Restore VGA text mode and dump CPU state to gCrashLog so the host +// can recover the report after DOSBox exits. Runs from a signal +// context so we cannot call much of libc safely; fprintf into an +// already-open FILE* and a stale __dpmi_int are the riskiest things +// we touch, both of which DJGPP documents as signal-safe enough for +// crash reporting. +static void crashHandler(int sig) { + __dpmi_regs regs; + struct __jmp_buf *st; + + if (gCrashLog != NULL) { + // __djgpp_exception_state is a jmp_buf (array of one struct). + // Decaying it as &[0] gives a pointer to the register fields. + st = (__djgpp_exception_state_ptr != NULL) + ? &(*__djgpp_exception_state_ptr)[0] + : NULL; + fprintf(gCrashLog, "JoeyLib DOS crash: signal %d\n", sig); + if (st != NULL) { + fprintf(gCrashLog, + " eip=%08lx eflags=%08lx cs=%04x\n", + st->__eip, st->__eflags, st->__cs); + fprintf(gCrashLog, + " eax=%08lx ebx=%08lx ecx=%08lx edx=%08lx\n", + st->__eax, st->__ebx, st->__ecx, st->__edx); + fprintf(gCrashLog, + " esi=%08lx edi=%08lx ebp=%08lx esp=%08lx\n", + st->__esi, st->__edi, st->__ebp, st->__esp); + fprintf(gCrashLog, + " ds=%04x es=%04x fs=%04x gs=%04x ss=%04x\n", + st->__ds, st->__es, st->__fs, st->__gs, st->__ss); + } + fflush(gCrashLog); + fclose(gCrashLog); + gCrashLog = NULL; + } + + if (gNearEnabled) { + __djgpp_nearptr_disable(); + gNearEnabled = false; + } + memset(®s, 0, sizeof(regs)); + regs.x.ax = 0x0003; + __dpmi_int(0x10, ®s); + + signal(sig, SIG_DFL); + raise(sig); +} + + +// Open CRASH.LOG in cwd (DOSBox's mounted dir on the host) and install +// signal handlers that route fault reports there. This runs before the +// VGA mode change so its file handle is established while the screen +// is still text-mode, but the writes themselves only happen on a +// fault, by which time mode might be 13h -- which is fine because the +// log goes to disk, not the screen. +static void installCrashLog(void) { + if (gCrashLog != NULL) { + return; + } + gCrashLog = fopen("CRASH.LOG", "w"); + if (gCrashLog == NULL) { + return; + } + setbuf(gCrashLog, NULL); + signal(SIGSEGV, crashHandler); + signal(SIGFPE, crashHandler); + signal(SIGILL, crashHandler); + signal(SIGABRT, crashHandler); +} + + static void uploadPaletteIfNeeded(const SurfaceT *src) { if (gCacheValid && memcmp(gCachedPalette, src->palette, sizeof(gCachedPalette)) == 0) { return; } - uploadPaletteIfNeeded(src); + uploadPalette(src); memcpy(gCachedPalette, src->palette, sizeof(gCachedPalette)); gCacheValid = true; } @@ -105,6 +183,8 @@ bool halInit(const JoeyConfigT *config) { (void)config; + installCrashLog(); + memset(®s, 0, sizeof(regs)); regs.x.ax = 0x0013; __dpmi_int(0x10, ®s); @@ -163,4 +243,9 @@ void halShutdown(void) { memset(®s, 0, sizeof(regs)); regs.x.ax = 0x0003; __dpmi_int(0x10, ®s); + + if (gCrashLog != NULL) { + fclose(gCrashLog); + gCrashLog = NULL; + } } diff --git a/src/port/dos/input.c b/src/port/dos/input.c index 818f6fe..f403bdc 100644 --- a/src/port/dos/input.c +++ b/src/port/dos/input.c @@ -1,7 +1,9 @@ -// DOS keyboard input: hooks INT 9 to capture AT set-1 scan codes from -// port 0x60. The ISR reads the scan code, maps it to a JoeyKeyE, updates -// a private gIsrState buffer, and sends EOI to the PIC. halInputPoll -// snapshots gIsrState into gKeyState with interrupts disabled. +// DOS keyboard + mouse input. +// +// Keyboard: hooks INT 9 to capture AT set-1 scan codes from port 0x60. +// The ISR reads the scan code, maps it to a JoeyKeyE, updates a private +// gIsrState buffer, and sends EOI to the PIC. halInputPoll snapshots +// gIsrState into gKeyState with interrupts disabled. // // The two-buffer split is required for joeyKeyPressed edge detection. // joeyInputPoll does memcpy(gKeyPrev, gKeyState) *before* halInputPoll @@ -14,7 +16,15 @@ // Because DJGPP runs in protected mode with paging, the ISR code and // any data it touches must be locked or a page fault at interrupt time // will hang the machine. +// +// Mouse: uses INT 33h, the standard MS DOS mouse driver interface. +// halInputInit calls function 0 (reset/detect), then 7/8 to clamp the +// driver's coordinate range to the surface dimensions. halInputPoll +// calls function 3 once per frame to read absolute X/Y and the button +// bitmask. INT 33h is provided by every common DOS mouse driver and by +// DOSBox / DOSBox-X. +#include #include #include #include @@ -22,6 +32,7 @@ #include "hal.h" #include "inputInternal.h" +#include "joey/surface.h" // ----- Constants ----- @@ -34,9 +45,22 @@ #define SCAN_TABLE_SIZE 128 #define ISR_LOCK_SIZE 4096 +// INT 33h mouse driver functions and button bits. +#define MOUSE_INT 0x33 +#define MOUSE_FN_RESET 0x0000 +#define MOUSE_FN_GET_STATE 0x0003 +#define MOUSE_FN_SET_X_RANGE 0x0007 +#define MOUSE_FN_SET_Y_RANGE 0x0008 +#define MOUSE_FN_SET_POS 0x0004 +#define MOUSE_BTN_LEFT 0x0001 +#define MOUSE_BTN_RIGHT 0x0002 +#define MOUSE_BTN_MIDDLE 0x0004 + // ----- Prototypes ----- static void keyboardIsr(void); +static bool mouseInit(void); +static void mousePoll(void); // ----- Module state ----- @@ -107,6 +131,8 @@ static _go32_dpmi_seginfo gNewHandler; static bool gHooked = false; static volatile bool gIsrState[KEY_COUNT]; +static bool gMousePresent = false; + // ----- Internal helpers ----- static void keyboardIsr(void) { @@ -130,6 +156,61 @@ static void keyboardIsr(void) { } +// Reset the mouse driver, detect presence, clamp coordinate range to +// the surface, and center the pointer. AX=0xFFFF after reset means a +// driver is loaded; anything else means no mouse and we leave gMouse* +// at their zero defaults. +static bool mouseInit(void) { + __dpmi_regs r; + + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_RESET; + __dpmi_int(MOUSE_INT, &r); + if (r.x.ax != 0xFFFF) { + return false; + } + + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_SET_X_RANGE; + r.x.cx = 0; + r.x.dx = SURFACE_WIDTH - 1; + __dpmi_int(MOUSE_INT, &r); + + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_SET_Y_RANGE; + r.x.cx = 0; + r.x.dx = SURFACE_HEIGHT - 1; + __dpmi_int(MOUSE_INT, &r); + + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_SET_POS; + r.x.cx = SURFACE_WIDTH / 2; + r.x.dx = SURFACE_HEIGHT / 2; + __dpmi_int(MOUSE_INT, &r); + + return true; +} + + +static void mousePoll(void) { + __dpmi_regs r; + uint16_t btn; + + if (!gMousePresent) { + return; + } + memset(&r, 0, sizeof(r)); + r.x.ax = MOUSE_FN_GET_STATE; + __dpmi_int(MOUSE_INT, &r); + gMouseX = (int16_t)r.x.cx; + gMouseY = (int16_t)r.x.dx; + btn = r.x.bx; + gMouseButtonState[MOUSE_BUTTON_LEFT] = (btn & MOUSE_BTN_LEFT) != 0; + gMouseButtonState[MOUSE_BUTTON_RIGHT] = (btn & MOUSE_BTN_RIGHT) != 0; + gMouseButtonState[MOUSE_BUTTON_MIDDLE] = (btn & MOUSE_BTN_MIDDLE) != 0; +} + + // ----- HAL API (alphabetical) ----- void halInputInit(void) { @@ -150,6 +231,8 @@ void halInputInit(void) { } _go32_dpmi_set_protected_mode_interrupt_vector(9, &gNewHandler); gHooked = true; + + gMousePresent = mouseInit(); } @@ -157,6 +240,7 @@ void halInputPoll(void) { disable(); memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState)); enable(); + mousePoll(); } diff --git a/src/port/iigs/hal.c b/src/port/iigs/hal.c index 2cf693a..b519f8a 100644 --- a/src/port/iigs/hal.c +++ b/src/port/iigs/hal.c @@ -31,6 +31,11 @@ // NEWVIDEO bit masks #define NEWVIDEO_SHR_ON 0x80 #define NEWVIDEO_LINEARIZE 0x40 +// Bit 0 is documented as reserved-must-be-1 in the IIgs Hardware +// Reference for forward compatibility. Real silicon doesn't care, but +// GSplus halts on writes that leave it clear (see moremem.c c029 +// handler) and bumps its "Code: RED" status. Always include this bit. +#define NEWVIDEO_RESERVED_BIT 0x01 // ----- Module state ----- @@ -42,7 +47,7 @@ static bool gModeSet = false; bool halInit(const JoeyConfigT *config) { (void)config; gPreviousNewVideo = *IIGS_NEWVIDEO_REG; - *IIGS_NEWVIDEO_REG = (uint8_t)(NEWVIDEO_SHR_ON | NEWVIDEO_LINEARIZE); + *IIGS_NEWVIDEO_REG = (uint8_t)(NEWVIDEO_SHR_ON | NEWVIDEO_LINEARIZE | NEWVIDEO_RESERVED_BIT); gModeSet = true; return true; } diff --git a/src/port/iigs/input.c b/src/port/iigs/input.c index 1a9aa0d..bdcea94 100644 --- a/src/port/iigs/input.c +++ b/src/port/iigs/input.c @@ -1,11 +1,10 @@ -// Apple IIgs keyboard input via the classic Apple II softswitches. +// Apple IIgs keyboard + mouse input via classic Apple II softswitches. // -// The first cut used the Event Manager, but that requires EMStartUp -// from a toolbox-aware program, and our S16 binary does not go through -// the full ToolBox bring-up -- calling GetNextEvent uninitialized -// corrupted KEGS' emulation state. Softswitches have no such -// dependency: they are live memory-mapped hardware that the monitor -// ROM, BASIC, and ProDOS have always relied on. +// 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. // // Tradeoff: $C000 reports the *last* key pressed, not a per-key matrix. // Holding multiple non-modifier keys simultaneously cannot be observed; @@ -19,6 +18,16 @@ // $C000 refreshes the TTL; each halInputPoll decays it; when TTL hits // zero we assume the key was released. KEY_TTL is sized to cover the // typematic initial delay so that a held key does not flicker. +// +// 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 halInputPoll, 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 @@ -26,12 +35,15 @@ #include "hal.h" #include "inputInternal.h" +#include "joey/surface.h" // ----- 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) #define KBD_STROBE_BIT 0x80 #define KBD_ASCII_MASK 0x7F @@ -42,6 +54,16 @@ #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 + // Polls a key stays "down" after the last observed strobe. Covers the // typematic initial delay so a held key does not flicker off/on between // repeats. @@ -62,8 +84,10 @@ // ----- Prototypes ----- -static void buildAsciiTable(void); -static void readModifierKeys(void); +static void buildAsciiTable(void); +static void pollMouse(void); +static void readModifierKeys(void); +static int8_t signExtend7(uint8_t raw); // ----- Module state ----- @@ -73,6 +97,9 @@ static void readModifierKeys(void); static uint8_t gAsciiToKey[ASCII_TABLE_SIZE]; static uint8_t gKeyTtl [KEY_COUNT]; +static int16_t gMouseAbsX = SURFACE_WIDTH / 2; +static int16_t gMouseAbsY = SURFACE_HEIGHT / 2; + // ----- Internal helpers ----- static void buildAsciiTable(void) { @@ -113,6 +140,61 @@ static void readModifierKeys(void) { } +// 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; +} + + +// 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 halInputInit(void) { @@ -121,6 +203,11 @@ void halInputInit(void) { memset(gKeyTtl, 0, sizeof(gKeyTtl)); buildAsciiTable(); + gMouseAbsX = SURFACE_WIDTH / 2; + gMouseAbsY = SURFACE_HEIGHT / 2; + gMouseX = gMouseAbsX; + gMouseY = gMouseAbsY; + // Clear any pending strobe from before we started. (void)*IIGS_KBDSTRB; } @@ -153,6 +240,7 @@ void halInputPoll(void) { } readModifierKeys(); + pollMouse(); }