Mouse support working.

This commit is contained in:
Scott Duensing 2026-04-24 18:35:03 -05:00
parent 9bacd2c68f
commit b10a7802b9
12 changed files with 673 additions and 86 deletions

View file

@ -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 <stdio.h>
@ -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
@ -43,6 +55,10 @@ static const JoeyKeyE gKeyGrid[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();

View file

@ -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

View file

@ -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"

View file

@ -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 "-<key>
# <value>". 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

View file

@ -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 <string.h>
@ -9,12 +14,18 @@
#include "hal.h"
#include "inputInternal.h"
bool gKeyState[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(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;
}

View file

@ -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

View file

@ -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 <string.h>
@ -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);
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;
}
ReplyMsg((struct Message *)msg);
}
}
@ -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;
}

View file

@ -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 <string.h>
@ -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;
}

View file

@ -9,12 +9,16 @@
//
// Access to VGA memory uses DJGPP's nearptr mechanism for speed.
#include <setjmp.h>
#include <signal.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <dpmi.h>
#include <go32.h>
#include <pc.h>
#include <sys/exceptn.h>
#include <sys/nearptr.h>
#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(&regs, 0, sizeof(regs));
regs.x.ax = 0x0003;
__dpmi_int(0x10, &regs);
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(&regs, 0, sizeof(regs));
regs.x.ax = 0x0013;
__dpmi_int(0x10, &regs);
@ -163,4 +243,9 @@ void halShutdown(void) {
memset(&regs, 0, sizeof(regs));
regs.x.ax = 0x0003;
__dpmi_int(0x10, &regs);
if (gCrashLog != NULL) {
fclose(gCrashLog);
gCrashLog = NULL;
}
}

View file

@ -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 <dos.h>
#include <dpmi.h>
#include <go32.h>
#include <pc.h>
@ -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();
}

View file

@ -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;
}

View file

@ -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 <string.h>
@ -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.
@ -63,7 +85,9 @@
// ----- Prototypes -----
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();
}