Full keyboard/typing support added. A few bugs fixed.

This commit is contained in:
Scott Duensing 2026-08-11 21:49:44 -05:00
parent b8b8c7dd93
commit 74bc0eca7a
21 changed files with 1792 additions and 61 deletions

View file

@ -406,6 +406,14 @@ Call `jlInputPoll` once per frame, then query the state predicates.
Edge predicates (`*Pressed`, `*Released`) fire only in the frame the Edge predicates (`*Pressed`, `*Released`) fire only in the frame the
transition happened. transition happened.
Text entry uses the separate typed-character queue: `jlInputGetChar`
pops the next typed character (printable ASCII 0x20..0x7E plus
`JL_CHAR_BACKSPACE`/`TAB`/`RETURN`/`ESCAPE`) or -1 when empty, with
shift, caps lock, and the machine's keyboard layout already applied by
the backend -- so punctuation like `.` and `:` arrives correctly on
every port. Refilled by the same `jlInputPoll`; see `docs/input.md`
for per-port sources and limitations.
```c ```c
typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE, typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE,
KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE, KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
@ -419,8 +427,14 @@ typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE;
#define JOYSTICK_AXIS_MAX 127 #define JOYSTICK_AXIS_MAX 127
#define JOYSTICK_AXIS_MIN (-127) #define JOYSTICK_AXIS_MIN (-127)
#define JL_CHAR_BACKSPACE 0x08
#define JL_CHAR_TAB 0x09
#define JL_CHAR_RETURN 0x0D
#define JL_CHAR_ESCAPE 0x1B
void jlInputPoll (void); void jlInputPoll (void);
void jlWaitForAnyKey (void); void jlWaitForAnyKey (void);
int jlInputGetChar (void);
bool jlKeyDown (jlKeyE key); bool jlKeyDown (jlKeyE key);
bool jlKeyPressed (jlKeyE key); bool jlKeyPressed (jlKeyE key);

View file

@ -3,6 +3,15 @@
// Holding the left mouse button while the pointer is over a cell lights // 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. // it as if the corresponding key were down. Press ESC to quit.
// //
// The strip under the grid is a typed-text line fed by jlInputGetChar:
// whatever you type -- letters, digits, shifted punctuation -- appears
// there, Backspace deletes, Return clears. It doubles as the
// acceptance test for the typed-character queue ("192.168.1.10:6510"
// must be typeable on every port). The bottom scanline additionally
// encodes every received character as raw pixel nibbles (2 pixels =
// 1 byte) behind an A5A5 sentinel so emulator harnesses can verify
// the exact bytes from a framebuffer dump.
//
// The render loop only redraws cells whose target lit state changed // The render loop only redraws cells whose target lit state changed
// since last frame -- so on idle frames, the only work is the cursor // since last frame -- so on idle frames, the only work is the cursor
// erase + redraw + a tiny rect-present pair. The cursor erase is // erase + redraw + a tiny rect-present pair. The cursor erase is
@ -14,14 +23,15 @@
#include <stdio.h> #include <stdio.h>
#include <joey/joey.h> #include <joey/joey.h>
#include <joey/debug.h>
#define GRID_COLS 10 #define GRID_COLS 10
#define GRID_ROWS 6 #define GRID_ROWS 6
#define CELL_W 28 #define CELL_W 28
#define CELL_H 28 #define CELL_H 26
#define GAP 4 #define GAP 4
#define MARGIN_X 2 #define MARGIN_X 2
#define MARGIN_Y 6 #define MARGIN_Y 2
#define CURSOR_W 4 #define CURSOR_W 4
#define CURSOR_H 4 #define CURSOR_H 4
@ -33,13 +43,40 @@
#define CELL_NONE ((int16_t)-1) #define CELL_NONE ((int16_t)-1)
// Typed-text line under the grid (the grid ends at y = 178 with the
// 26-pixel cells above).
#define FONT_W 5
#define FONT_H 7
#define TEXT_STRIP_Y 180
#define TEXT_X 4
#define TEXT_Y 182
#define TEXT_INPUT_X 68
#define MAX_LINE 40
// Verification scanline: sentinel nibbles, then a received-character
// count byte, then every received character as (high nibble, low
// nibble) pixel pairs. An emulator harness reads this row back from
// the framebuffer to prove each typed byte arrived.
#define VERIFY_Y 198
#define VERIFY_SENTINEL_0 0xA
#define VERIFY_SENTINEL_1 0x5
#define VERIFY_COUNT_X 4
#define VERIFY_CHARS_X 8
#define MAX_RECEIVED 150
static void buildPalette(jlSurfaceT *screen); static void buildPalette(jlSurfaceT *screen);
static void cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow); 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 bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow);
static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit); static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit);
static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y); static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y);
static void drawTextLine(jlSurfaceT *screen, int16_t x, int16_t y, const char *text, uint8_t color);
static void drawVerifyRow(jlSurfaceT *screen);
static int glyphIdx(char c);
static void initialPaint(jlSurfaceT *screen); static void initialPaint(jlSurfaceT *screen);
static void logTypedHistory(void);
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow); static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
static void processTypedChars(jlSurfaceT *screen);
static void redrawTextStrip(jlSurfaceT *screen);
static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow); static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
// Keys laid out row-by-row. KEY_NONE cells stay blank. Shape roughly // Keys laid out row-by-row. KEY_NONE cells stay blank. Shape roughly
@ -60,18 +97,111 @@ static int16_t gLastCursorY = -100;
static int16_t gLastCursorCol = CELL_NONE; static int16_t gLastCursorCol = CELL_NONE;
static int16_t gLastCursorRow = CELL_NONE; static int16_t gLastCursorRow = CELL_NONE;
// Typed-text line state plus the full received-character history for
// the verification row.
static char gLine[MAX_LINE + 1];
static int16_t gLineLen = 0;
static uint8_t gReceived[MAX_RECEIVED];
static int16_t gReceivedCount = 0;
// Tiny 5x7 font (high 5 bits of each row byte), borrowed from the
// adventure example and extended with a few typed-punctuation glyphs.
// Coverage: space, 0-9, A-Z (lowercase renders uppercase), and
// . , : ! ? - ' " / ; = + _ -- everything else renders as the hollow
// box at the end of the table.
//
// Row indices used by glyphIdx. The table below must keep this order;
// the trailing row comments carry the same numbers.
#define GLYPH_SPACE 0
#define GLYPH_DIGIT_BASE 1
#define GLYPH_ALPHA_BASE 11
#define GLYPH_PERIOD 37
#define GLYPH_COMMA 38
#define GLYPH_COLON 39
#define GLYPH_BANG 40
#define GLYPH_QUESTION 41
#define GLYPH_MINUS 42
#define GLYPH_APOSTROPHE 43
#define GLYPH_QUOTE 44
#define GLYPH_SLASH 45
#define GLYPH_SEMICOLON 46
#define GLYPH_EQUALS 47
#define GLYPH_PLUS 48
#define GLYPH_UNDERSCORE 49
#define GLYPH_UNKNOWN 50
static const uint8_t kFontGlyph[][FONT_H] = {
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0 space
{ 0x70, 0x88, 0x98, 0xA8, 0xC8, 0x88, 0x70 }, // 1 '0'
{ 0x20, 0x60, 0x20, 0x20, 0x20, 0x20, 0x70 }, // 2 '1'
{ 0x70, 0x88, 0x08, 0x10, 0x20, 0x40, 0xF8 }, // 3 '2'
{ 0x70, 0x88, 0x08, 0x30, 0x08, 0x88, 0x70 }, // 4 '3'
{ 0x10, 0x30, 0x50, 0x90, 0xF8, 0x10, 0x10 }, // 5 '4'
{ 0xF8, 0x80, 0xF0, 0x08, 0x08, 0x88, 0x70 }, // 6 '5'
{ 0x30, 0x40, 0x80, 0xF0, 0x88, 0x88, 0x70 }, // 7 '6'
{ 0xF8, 0x08, 0x10, 0x20, 0x40, 0x40, 0x40 }, // 8 '7'
{ 0x70, 0x88, 0x88, 0x70, 0x88, 0x88, 0x70 }, // 9 '8'
{ 0x70, 0x88, 0x88, 0x78, 0x08, 0x10, 0x60 }, // 10 '9'
{ 0x70, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, // 11 'A'
{ 0xF0, 0x88, 0x88, 0xF0, 0x88, 0x88, 0xF0 }, // 12 'B'
{ 0x70, 0x88, 0x80, 0x80, 0x80, 0x88, 0x70 }, // 13 'C'
{ 0xF0, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF0 }, // 14 'D'
{ 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0xF8 }, // 15 'E'
{ 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0x80 }, // 16 'F'
{ 0x70, 0x88, 0x80, 0xB8, 0x88, 0x88, 0x70 }, // 17 'G'
{ 0x88, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, // 18 'H'
{ 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70 }, // 19 'I'
{ 0x38, 0x10, 0x10, 0x10, 0x90, 0x90, 0x60 }, // 20 'J'
{ 0x88, 0x90, 0xA0, 0xC0, 0xA0, 0x90, 0x88 }, // 21 'K'
{ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xF8 }, // 22 'L'
{ 0x88, 0xD8, 0xA8, 0xA8, 0x88, 0x88, 0x88 }, // 23 'M'
{ 0x88, 0xC8, 0xA8, 0x98, 0x88, 0x88, 0x88 }, // 24 'N'
{ 0x70, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, // 25 'O'
{ 0xF0, 0x88, 0x88, 0xF0, 0x80, 0x80, 0x80 }, // 26 'P'
{ 0x70, 0x88, 0x88, 0x88, 0xA8, 0x90, 0x68 }, // 27 'Q'
{ 0xF0, 0x88, 0x88, 0xF0, 0xA0, 0x90, 0x88 }, // 28 'R'
{ 0x70, 0x88, 0x80, 0x70, 0x08, 0x88, 0x70 }, // 29 'S'
{ 0xF8, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, // 30 'T'
{ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, // 31 'U'
{ 0x88, 0x88, 0x88, 0x88, 0x88, 0x50, 0x20 }, // 32 'V'
{ 0x88, 0x88, 0x88, 0xA8, 0xA8, 0xD8, 0x88 }, // 33 'W'
{ 0x88, 0x88, 0x50, 0x20, 0x50, 0x88, 0x88 }, // 34 'X'
{ 0x88, 0x88, 0x50, 0x20, 0x20, 0x20, 0x20 }, // 35 'Y'
{ 0xF8, 0x08, 0x10, 0x20, 0x40, 0x80, 0xF8 }, // 36 'Z'
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60 }, // 37 '.'
{ 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x40 }, // 38 ','
{ 0x00, 0x60, 0x60, 0x00, 0x60, 0x60, 0x00 }, // 39 ':'
{ 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x20 }, // 40 '!'
{ 0x70, 0x88, 0x08, 0x10, 0x20, 0x00, 0x20 }, // 41 '?'
{ 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00 }, // 42 '-'
{ 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 43 '\''
{ 0x50, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 44 '"'
{ 0x00, 0x08, 0x10, 0x20, 0x40, 0x80, 0x00 }, // 45 '/'
{ 0x00, 0x60, 0x60, 0x00, 0x60, 0x60, 0x40 }, // 46 ';'
{ 0x00, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0x00 }, // 47 '='
{ 0x00, 0x20, 0x20, 0xF8, 0x20, 0x20, 0x00 }, // 48 '+'
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8 }, // 49 '_'
{ 0xF8, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF8 }, // 50 unknown box
};
static void buildPalette(jlSurfaceT *screen) { static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE]; uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t i; uint16_t i;
// Every entry gets a DISTINCT color (gray ramp for the unused
// ones, red for 15 since 0x0FFF is taken by the cursor) so a
// screenshot-based harness can decode the verification row's
// nibble pixels back to values on ports without direct
// framebuffer reads.
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = 0x0000; colors[i] = (uint16_t)((i << 8) | (i << 4) | i);
} }
colors[COLOR_BACKGROUND] = 0x0000; // black colors[COLOR_BACKGROUND] = 0x0000; // black
colors[COLOR_UNLIT] = 0x0333; // dark gray colors[COLOR_UNLIT] = 0x0333; // dark gray
colors[COLOR_LIT] = 0x00F0; // bright green colors[COLOR_LIT] = 0x00F0; // bright green
colors[COLOR_CURSOR] = 0x0FFF; // white colors[COLOR_CURSOR] = 0x0FFF; // white
colors[15] = 0x0F00; // red (0x0FFF is the cursor's)
jlPaletteSet(screen, 0, colors); jlPaletteSet(screen, 0, colors);
} }
@ -133,6 +263,77 @@ static void drawCursor(jlSurfaceT *screen, int16_t x, int16_t y) {
} }
static void drawTextLine(jlSurfaceT *screen, int16_t x, int16_t y, const char *text, uint8_t color) {
int16_t cx;
int16_t i;
int16_t row;
int16_t col;
int idx;
uint8_t bits;
cx = x;
for (i = 0; text[i] != '\0'; i++) {
idx = glyphIdx(text[i]);
for (row = 0; row < FONT_H; row++) {
bits = kFontGlyph[idx][row];
for (col = 0; col < FONT_W; col++) {
if (bits & (0x80 >> col)) {
jlDrawPixel(screen, (int16_t)(cx + col), (int16_t)(y + row), color);
}
}
}
cx = (int16_t)(cx + FONT_W + 1);
if (cx > SURFACE_WIDTH - FONT_W) {
return;
}
}
}
// Re-stamp the verification scanline: sentinel, received count, then
// every received character as two nibble pixels. Only the count and
// the newest characters actually change, but the row is cheap enough
// to redraw whole.
static void drawVerifyRow(jlSurfaceT *screen) {
int16_t i;
uint8_t ch;
jlDrawPixel(screen, 0, VERIFY_Y, VERIFY_SENTINEL_0);
jlDrawPixel(screen, 1, VERIFY_Y, VERIFY_SENTINEL_1);
jlDrawPixel(screen, 2, VERIFY_Y, VERIFY_SENTINEL_0);
jlDrawPixel(screen, 3, VERIFY_Y, VERIFY_SENTINEL_1);
jlDrawPixel(screen, VERIFY_COUNT_X, VERIFY_Y, (uint8_t)((gReceivedCount >> 4) & 0x0F));
jlDrawPixel(screen, VERIFY_COUNT_X + 1, VERIFY_Y, (uint8_t)(gReceivedCount & 0x0F));
for (i = 0; i < gReceivedCount; i++) {
ch = gReceived[i];
jlDrawPixel(screen, (int16_t)(VERIFY_CHARS_X + 2 * i), VERIFY_Y, (uint8_t)((ch >> 4) & 0x0F));
jlDrawPixel(screen, (int16_t)(VERIFY_CHARS_X + 2 * i + 1), VERIFY_Y, (uint8_t)(ch & 0x0F));
}
}
static int glyphIdx(char c) {
if (c == ' ') { return GLYPH_SPACE; }
if (c >= '0' && c <= '9') { return GLYPH_DIGIT_BASE + (c - '0'); }
if (c >= 'A' && c <= 'Z') { return GLYPH_ALPHA_BASE + (c - 'A'); }
if (c >= 'a' && c <= 'z') { return GLYPH_ALPHA_BASE + (c - 'a'); } // uppercase fallback
if (c == '.') { return GLYPH_PERIOD; }
if (c == ',') { return GLYPH_COMMA; }
if (c == ':') { return GLYPH_COLON; }
if (c == '!') { return GLYPH_BANG; }
if (c == '?') { return GLYPH_QUESTION; }
if (c == '-') { return GLYPH_MINUS; }
if (c == '\'') { return GLYPH_APOSTROPHE; }
if (c == '"') { return GLYPH_QUOTE; }
if (c == '/') { return GLYPH_SLASH; }
if (c == ';') { return GLYPH_SEMICOLON; }
if (c == '=') { return GLYPH_EQUALS; }
if (c == '+') { return GLYPH_PLUS; }
if (c == '_') { return GLYPH_UNDERSCORE; }
return GLYPH_UNKNOWN;
}
static void initialPaint(jlSurfaceT *screen) { static void initialPaint(jlSurfaceT *screen) {
int16_t col; int16_t col;
int16_t row; int16_t row;
@ -149,10 +350,32 @@ static void initialPaint(jlSurfaceT *screen) {
gCellLit[row][col] = false; gCellLit[row][col] = false;
} }
} }
redrawTextStrip(screen);
jlStagePresent(); jlStagePresent();
} }
// Emit the received-character history as hex on exit so an emulator
// harness can verify the typed-character path from the boot volume
// when it cannot read the framebuffer (see verify-amiga-input.sh).
static void logTypedHistory(void) {
static const char kHex[] = "0123456789ABCDEF";
char line[2 * MAX_RECEIVED + 8];
int16_t i;
int16_t n;
n = 0;
for (i = 0; i < gReceivedCount; i++) {
line[n] = kHex[(gReceived[i] >> 4) & 0x0F];
line[n + 1] = kHex[gReceived[i] & 0x0F];
n = (int16_t)(n + 2);
}
line[n] = '\0';
jlLogF("typed=%s", line);
jlLogFlush();
}
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) { static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
int16_t col; int16_t col;
int16_t row; int16_t row;
@ -169,8 +392,8 @@ static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t c
if (lit == gCellLit[row][col]) { if (lit == gCellLit[row][col]) {
continue; continue;
} }
/* drawCell marks the cell's rect dirty; jlStagePresent // drawCell marks the cell's rect dirty; jlStagePresent
* flushes that one band. */ // flushes that one band.
drawCell(screen, col, row, lit); drawCell(screen, col, row, lit);
jlStagePresent(); jlStagePresent();
gCellLit[row][col] = lit; gCellLit[row][col] = lit;
@ -179,6 +402,54 @@ static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t c
} }
// Pop everything the user typed since last frame, apply it to the
// text line (Backspace deletes, Return clears, Escape is the quit key
// and types nothing), and refresh the strip when anything changed.
static void processTypedChars(jlSurfaceT *screen) {
int ch;
bool changed;
changed = false;
while ((ch = jlInputGetChar()) != -1) {
if (gReceivedCount < MAX_RECEIVED) {
gReceived[gReceivedCount] = (uint8_t)ch;
gReceivedCount = (int16_t)(gReceivedCount + 1);
}
changed = true;
if (ch == JL_CHAR_BACKSPACE) {
if (gLineLen > 0) {
gLineLen = (int16_t)(gLineLen - 1);
}
} else if (ch == JL_CHAR_RETURN) {
gLineLen = 0;
} else if (ch == JL_CHAR_ESCAPE || ch == JL_CHAR_TAB) {
// Nothing to render for these.
} else if (gLineLen < MAX_LINE) {
gLine[gLineLen] = (char)ch;
gLineLen = (int16_t)(gLineLen + 1);
}
}
if (!changed) {
return;
}
redrawTextStrip(screen);
jlStagePresent();
}
// Repaint everything below the key grid: the prompt, the current text
// line, and the verification scanline. Shared by the typed-character
// refresh and the cursor-erase path (which may stamp background over
// this region).
static void redrawTextStrip(jlSurfaceT *screen) {
gLine[gLineLen] = '\0';
jlFillRect(screen, 0, TEXT_STRIP_Y, SURFACE_WIDTH, SURFACE_HEIGHT - TEXT_STRIP_Y, COLOR_BACKGROUND);
drawTextLine(screen, TEXT_X, TEXT_Y, "TYPE HERE:", COLOR_UNLIT);
drawTextLine(screen, TEXT_INPUT_X, TEXT_Y, gLine, COLOR_CURSOR);
drawVerifyRow(screen);
}
// Erase the previous cursor (by redrawing the cell that held it) and // Erase the previous cursor (by redrawing the cell that held it) and
// stamp the new cursor at the current mouse position. Both rects are // stamp the new cursor at the current mouse position. Both rects are
// presented; if the cursor stayed inside the same cell only one rect // presented; if the cursor stayed inside the same cell only one rect
@ -194,14 +465,19 @@ static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRo
if (gLastCursorCol != CELL_NONE) { if (gLastCursorCol != CELL_NONE) {
drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]); drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]);
} else if (gLastCursorX >= 0 && gLastCursorY >= 0) { } else if (gLastCursorX >= 0 && gLastCursorY >= 0) {
// Old cursor was in a gap region. Stamp background over it. // Old cursor was in a gap region. Stamp background over it,
// then repair the text strip / verify row if the stamp
// reached into them.
jlFillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND); jlFillRect(screen, gLastCursorX, gLastCursorY, CURSOR_W, CURSOR_H, COLOR_BACKGROUND);
if ((int16_t)(gLastCursorY + CURSOR_H) > TEXT_STRIP_Y) {
redrawTextStrip(screen);
}
} }
} }
drawCursor(screen, mouseX, mouseY); drawCursor(screen, mouseX, mouseY);
/* All draw calls above marked their rects dirty; one jlStagePresent // All draw calls above marked their rects dirty; one jlStagePresent
* flushes the union (cursor erase + cursor draw). */ // flushes the union (cursor erase + cursor draw).
jlStagePresent(); jlStagePresent();
gLastCursorX = mouseX; gLastCursorX = mouseX;
@ -245,8 +521,10 @@ int main(void) {
cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow); cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow);
presentChangedCells(screen, cursorCol, cursorRow); presentChangedCells(screen, cursorCol, cursorRow);
updateCursor(screen, cursorCol, cursorRow); updateCursor(screen, cursorCol, cursorRow);
processTypedChars(screen);
} }
logTypedHistory();
jlShutdown(); jlShutdown();
return 0; return 0;
} }

View file

@ -9,6 +9,16 @@
// jlKeyPressed(k) -- rising edge since the previous poll // jlKeyPressed(k) -- rising edge since the previous poll
// jlKeyReleased(k) -- falling edge since the previous poll // jlKeyReleased(k) -- falling edge since the previous poll
// //
// Typed-character input (text entry) is a separate path:
//
// jlInputGetChar() -- pop the next typed character, or -1
//
// The key predicates are the right shape for games (is the fire key
// held?); the character queue is the right shape for text fields
// (which character did the user type, with shift and the machine's
// keyboard layout already applied). Both are refreshed by the same
// jlInputPoll() and coexist freely.
//
// The mouse predicates return the pointer state: // The mouse predicates return the pointer state:
// //
// jlMouseX/Y() -- pointer position in surface // jlMouseX/Y() -- pointer position in surface
@ -87,8 +97,43 @@ typedef enum {
#define JOYSTICK_AXIS_MAX 127 #define JOYSTICK_AXIS_MAX 127
#define JOYSTICK_AXIS_MIN (-127) #define JOYSTICK_AXIS_MIN (-127)
// Control characters delivered through jlInputGetChar alongside
// printable ASCII. Everything else below 0x20, plus 0x7F, is
// filtered out of the queue.
#define JL_CHAR_BACKSPACE 0x08
#define JL_CHAR_TAB 0x09
#define JL_CHAR_RETURN 0x0D
#define JL_CHAR_ESCAPE 0x1B
// Typed-character queue capacity (ring buffer; one slot stays empty,
// so JL_CHAR_QUEUE_SIZE - 1 characters can be pending). Must be a
// power of two.
#define JL_CHAR_QUEUE_SIZE 32
void jlInputPoll(void); void jlInputPoll(void);
// Pop the next typed character as a 7-bit code, or -1 if the queue
// is empty. Codes: printable ASCII 0x20..0x7E, plus JL_CHAR_BACKSPACE,
// JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE. Shift, caps lock, and
// the keyboard layout are already applied by the backend, so
// punctuation and shifted symbols arrive correctly on every port.
// (The layout is the machine's own on IIgs/Amiga/ST/X68000; the DOS
// port translates with a fixed US layout -- see docs/input.md.)
//
// The queue is refilled by jlInputPoll() -- no extra bring-up is
// needed. It holds JL_CHAR_QUEUE_SIZE - 1 characters; when full,
// further characters are dropped (drop-newest) until the app pops.
// OS auto-repeat may deliver repeated characters on ports whose
// keyboard services repeat (the caller manages repeat policy).
// 7-bit ASCII only; no Unicode / IME. Arrow keys are not characters
// and stay on the predicate API (jlKeyDown(KEY_UP) etc.).
// jlWaitForAnyKey empties the queue when it returns -- its dismissing
// keystroke is consumed by the wait, not delivered as text.
//
// Returns int (not a stdint type) deliberately: getchar()-style
// "byte or -1" is the interface contract consumers expect.
int jlInputGetChar(void);
// Block until the user presses any key. Internally polls via // Block until the user presses any key. Internally polls via
// jlInputPoll, so per-port jlpInputPoll machinery (including // jlInputPoll, so per-port jlpInputPoll machinery (including
// audio-friendly IRQ-driven samplers) keeps working while the // audio-friendly IRQ-driven samplers) keeps working while the

View file

@ -151,15 +151,6 @@
#define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK #define JL_HAS_AUDIO_FRAME_TICK
// SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX
// overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate
// device from the OPM, so chip music and digital audio do not contend.
#define JL_HAS_AUDIO_PLAY_MOD
#define JL_HAS_AUDIO_STOP_MOD
#define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX
// lifecycle / present / input (every real port implements) // lifecycle / present / input (every real port implements)
#define JL_HAS_INIT #define JL_HAS_INIT
#define JL_HAS_SHUTDOWN #define JL_HAS_SHUTDOWN
@ -229,15 +220,6 @@
#define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK #define JL_HAS_AUDIO_FRAME_TICK
// SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX
// overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate
// device from the OPM, so chip music and digital audio do not contend.
#define JL_HAS_AUDIO_PLAY_MOD
#define JL_HAS_AUDIO_STOP_MOD
#define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX
// lifecycle / present / input (every real port implements) // lifecycle / present / input (every real port implements)
#define JL_HAS_INIT #define JL_HAS_INIT
#define JL_HAS_SHUTDOWN #define JL_HAS_SHUTDOWN
@ -307,15 +289,6 @@
#define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK #define JL_HAS_AUDIO_FRAME_TICK
// SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX
// overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate
// device from the OPM, so chip music and digital audio do not contend.
#define JL_HAS_AUDIO_PLAY_MOD
#define JL_HAS_AUDIO_STOP_MOD
#define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX
// lifecycle / present / input (every real port implements) // lifecycle / present / input (every real port implements)
#define JL_HAS_INIT #define JL_HAS_INIT
#define JL_HAS_SHUTDOWN #define JL_HAS_SHUTDOWN
@ -364,15 +337,6 @@
#define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK #define JL_HAS_AUDIO_FRAME_TICK
// SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX
// overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate
// device from the OPM, so chip music and digital audio do not contend.
#define JL_HAS_AUDIO_PLAY_MOD
#define JL_HAS_AUDIO_STOP_MOD
#define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX
// lifecycle / present / input (every real port implements) // lifecycle / present / input (every real port implements)
#define JL_HAS_INIT #define JL_HAS_INIT
#define JL_HAS_SHUTDOWN #define JL_HAS_SHUTDOWN

View file

@ -94,6 +94,7 @@ PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
SERIAL_SRC := $(EXAMPLES)/serial/serial.c SERIAL_SRC := $(EXAMPLES)/serial/serial.c
UBER_SRC := $(EXAMPLES)/uber/uber.c UBER_SRC := $(EXAMPLES)/uber/uber.c
AUDIO_SRC := $(EXAMPLES)/audio/audio.c AUDIO_SRC := $(EXAMPLES)/audio/audio.c
KEYS_SRC := $(EXAMPLES)/keys/keys.c
.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-golden clean-x68000 clean .PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-golden clean-x68000 clean
@ -101,7 +102,7 @@ all x68000: x68000-lib x68000-examples
x68000-lib: $(LIB) $(LIBXMP_AR) x68000-lib: $(LIB) $(LIBXMP_AR)
x68000-examples: $(BINDIR)/SERIAL.X $(BINDIR)/UBER.X $(BINDIR)/AUDIO.X $(BINDIR)/PATTERN.X x68000-examples: $(BINDIR)/SERIAL.X $(BINDIR)/UBER.X $(BINDIR)/AUDIO.X $(BINDIR)/PATTERN.X $(BINDIR)/KEYS.X
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@ -159,6 +160,12 @@ $(BINDIR)/AUDIO.X: $(AUDIO_SRC) $(LIB) $(LIBXMP_AR)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(X68K_CC) $(CFLAGS) $(AUDIO_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@ $(X68K_CC) $(CFLAGS) $(AUDIO_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
# KEYS is the input acceptance vehicle: key predicates on the grid plus
# the typed-character line (jlInputGetChar) and its verification row.
$(BINDIR)/KEYS.X: $(KEYS_SRC) $(LIB) $(LIBXMP_AR)
@mkdir -p $(dir $@)
$(X68K_CC) $(CFLAGS) $(KEYS_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
# Both-directions RS-232C gate (~1 min). # Both-directions RS-232C gate (~1 min).
x68000-verify-serial: $(BINDIR)/SERIAL.X x68000-verify-serial: $(BINDIR)/SERIAL.X
$(REPO_DIR)/scripts/verify-x68000-serial.sh $(REPO_DIR)/scripts/verify-x68000-serial.sh

23
scripts/check-input.sh Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# check-input.sh - Host-cc harness for the typed-character queue
# (jlInputGetChar / jlInputCharPush in src/core/input.c). Compiles the
# core input module with a mocked jlpInputPoll (tests/host/inputHost.c)
# and exercises FIFO order, the character filter, drop-newest overflow,
# reset, and the jlInputPoll refill path.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cc=${CC:-cc}
work=$(mktemp -d -t joeylib-input.XXXXXX)
trap 'rm -rf "$work"' EXIT
cd "$repo"
# The BLANK platform block registers JL_HAS_INPUT_POLL and
# JL_HAS_JOYSTICK_RESET (blank.c normally supplies them); inputHost.c
# provides the mocks here instead, so no extra -D flags are needed.
"$cc" -DJOEYLIB_PLATFORM_BLANK \
-Iinclude -Iinclude/joey -Isrc/core -Wall \
src/core/input.c tests/host/inputHost.c \
-o "$work/inputHost"
"$work/inputHost"

120
scripts/verify-amiga-input.sh Executable file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env bash
# verify-amiga-input.sh - Typed-character acceptance gate for the Amiga
# port. Boots the Keys example off a directory hard drive in FS-UAE
# under Xvfb, types the RetroNet definition-of-done string with xdotool
# (real X key events, so ':' goes through the emulated shift and
# keymap.library's MapRawKey), then presses Escape. On exit Keys logs
# its full received-character history as hex to joeylog.txt on the boot
# volume -- which is the staged host directory -- and the harness
# compares that against the typed ASCII.
#
# Usage: scripts/verify-amiga-input.sh
# Requires: fs-uae, Xvfb, xdotool; build/amiga/bin/Keys (make amiga);
# toolchains/emulators/support/kickstart.rom.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/amiga/bin
kickstart=$repo/toolchains/emulators/support/kickstart.rom
target="192.168.1.10:6510"
[ -f "$bin_dir/Keys" ] || { echo "verify-amiga-input: build Keys first (make amiga)" >&2; exit 2; }
[ -f "$kickstart" ] || { echo "verify-amiga-input: missing $kickstart" >&2; exit 2; }
work=$(mktemp -d -t joeylib-amiga-input.XXXXXX)
display_num=$((RANDOM % 500 + 300))
cleanup() {
[ -n "${fsuae_pid:-}" ] && kill "$fsuae_pid" 2>/dev/null
[ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null
rm -rf "$work"
}
trap cleanup EXIT
mkdir -p "$work/hd/s"
cp "$bin_dir/Keys" "$work/hd/"
echo ":Keys" > "$work/hd/s/startup-sequence"
Xvfb ":$display_num" -screen 0 1024x768x24 >/dev/null 2>&1 &
xvfb_pid=$!
sleep 1
# --fullscreen=1 is REQUIRED under a WM-less Xvfb: SDL only accepts
# keyboard input once its window has input focus, no window manager
# ever grants it to a plain window, and xdotool's windowfocus/
# XSendEvent routes are ignored by SDL. A fullscreen SDL window takes
# focus itself, after which plain XTEST typing (xdotool without
# --window) lands in the emulated Amiga.
DISPLAY=":$display_num" fs-uae \
--amiga_model=A500 \
--fast_memory=2048 \
--kickstart_file="$kickstart" \
--hard_drive_0="$work/hd" \
--hard_drive_0_label=JOEYLIB \
--fullscreen=1 \
--initial_input_grab=1 \
--floppy_drive_speed=800 \
>/dev/null 2>&1 &
fsuae_pid=$!
# Boot AmigaDOS off the directory HD and let startup-sequence launch
# Keys. No sentinel channel here, so give it a generous fixed wait.
sleep 28
# XTEST keyboard events land in whatever window HAS focus, and under
# a WM-less Xvfb the fullscreen SDL window latches it on its own
# schedule -- so wait until getwindowfocus actually reports the FS-UAE
# window before typing, nudging with windowfocus while waiting.
win=$(DISPLAY=":$display_num" xdotool search --name -- "FS-UAE" | head -1)
for i in $(seq 1 30); do
focus=$(DISPLAY=":$display_num" xdotool getwindowfocus 2>/dev/null || true)
[ -n "$win" ] && [ "$focus" = "$win" ] && break
[ -n "$win" ] && DISPLAY=":$display_num" xdotool windowfocus --sync "$win" 2>/dev/null || true
sleep 1
[ -z "$win" ] && win=$(DISPLAY=":$display_num" xdotool search --name -- "FS-UAE" | head -1)
done
# Up to three attempts: type, press Escape, then look for the typed=
# line Keys logs on exit. The verdict compares the history's SUFFIX
# against the target, so a partially-delivered early attempt can't
# poison a complete later one -- the proof is that the final 17
# characters arrived in order through the queue.
log="$work/hd/joeylog.txt"
delivered=""
for attempt in 1 2 3; do
DISPLAY=":$display_num" xdotool type --delay 200 -- "$target"
sleep 2
# Hold Escape across several emulated frames: an instantaneous
# down+up can land inside ONE drainMessages pass, where set-then-
# clear in the same poll is invisible to the edge predicate.
DISPLAY=":$display_num" xdotool keydown Escape
sleep 0.5
DISPLAY=":$display_num" xdotool keyup Escape
for i in $(seq 1 10); do
if [ -f "$log" ] && grep -q "typed=" "$log"; then
delivered=1
break 2
fi
sleep 1
done
done
if [ -z "$delivered" ]; then
echo "verify-amiga-input: FAIL - no typed= line in joeylog.txt (keystrokes never landed?)" >&2
ls -la "$work/hd" >&2
exit 1
fi
typedHex=$(grep -o "typed=[0-9A-Fa-f]*" "$log" | tail -1 | cut -d= -f2)
expect=$(printf '%s' "$target" | od -A n -t x1 | tr -d ' \n' | tr 'a-f' 'A-F')
echo "VERIFY-AMIGA-INPUT typed=$typedHex"
case "$typedHex" in
*"$expect")
echo "verify-amiga-input: PASS (typed \"$target\", all ${#target} chars arrived via jlInputGetChar)"
;;
*)
echo "verify-amiga-input: FAIL" >&2
echo " expected suffix: $expect" >&2
echo " got: $typedHex" >&2
exit 1
;;
esac

138
scripts/verify-atarist-input.sh Executable file
View file

@ -0,0 +1,138 @@
#!/usr/bin/env bash
# verify-atarist-input.sh - Headless typed-character acceptance gate for
# the Atari ST port. Autostarts KEYS.PRG in Hatari (EmuTOS, GEMDOS
# drive), injects the RetroNet definition-of-done string through
# Hatari's command FIFO as IKBD key events, and reads the example's
# verification scanline back out of ST RAM with the built-in debugger.
# The row is planar (4 interleaved bitplane words per 16 pixels); the
# decode below reassembles the nibble pixels, which must equal the
# typed ASCII exactly.
#
# Injection encoding (Hatari 2.4 `hatari-event keypress <key>` parses a
# single-character argument as an ASCII character and a multi-character
# argument as an ST scancode): digits go as ASCII, '.' as scancode 52,
# and ':' as shift make (42) + ';' scancode 39 + shift break -- which
# also exercises the ISR's shift handling through the TOS keymap.
#
# Usage: scripts/verify-atarist-input.sh
# Requires: hatari, EmuTOS image, build/atarist/bin/KEYS.PRG.
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/atarist/bin
tos=$repo/toolchains/emulators/support/emutos-512k.img
target="192.168.1.10:6510"
[ -f "$bin_dir/KEYS.PRG" ] || { echo "verify-atarist-input: build KEYS.PRG first (make atarist)" >&2; exit 2; }
[ -f "$tos" ] || { echo "verify-atarist-input: missing EmuTOS at $tos" >&2; exit 2; }
work=$(mktemp -d -t joeylib-st-input.XXXXXX)
cleanup() {
[ -n "${hatari_pid:-}" ] && kill "$hatari_pid" 2>/dev/null
rm -rf "$work"
}
trap cleanup EXIT
cd "$work"
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy hatari \
--tos "$tos" \
--harddrive "$bin_dir" \
--gemdos-drive C \
--auto 'C:\KEYS.PRG' \
--cmd-fifo "$work/cmd.fifo" \
--fast-forward on \
> "$work/hatari.log" 2>&1 &
hatari_pid=$!
send() {
timeout 5 bash -c "echo \"\$1\" > \"$work/cmd.fifo\"" _ "$1" || return 1
sleep 0.2
}
# Wait for the FIFO, then for KEYS' A5A5 sentinel in the verify row.
# The video base is read from the shifter regs each attempt (planar
# sentinel = plane words 5000 A000 5000 A000).
for i in $(seq 1 30); do
[ -p "$work/cmd.fifo" ] && break
sleep 1
done
[ -p "$work/cmd.fifo" ] || { echo "verify-atarist-input: FAIL - hatari never created the FIFO" >&2; exit 1; }
rowAddr=""
for i in $(seq 1 45); do
sleep 1
send 'hatari-debug m $ff8201 4' || continue
base=$(grep -E '^00FF8201:' "$work/hatari.log" | tail -1 | awk '{print $2 $4}')
[ -n "$base" ] || continue
addr=$(( (16#$base << 8) + 198 * 160 ))
send "hatari-debug m \$$(printf '%x' "$addr") 8" || continue
row=$(grep -E "^$(printf '%08X' "$addr"):" "$work/hatari.log" | tail -1)
if echo "$row" | grep -qi "50 00 a0 00 50 00 a0 00"; then
rowAddr=$addr
break
fi
done
if [ -z "$rowAddr" ]; then
echo "verify-atarist-input: FAIL - KEYS sentinel never appeared" >&2
tail -10 "$work/hatari.log" >&2
exit 1
fi
# Type the string: digits as ASCII characters, '.' as scancode 52,
# ':' as shift + scancode 39.
for c in 1 9 2; do send "hatari-event keypress $c"; done
send "hatari-event keypress 52"
for c in 1 6 8; do send "hatari-event keypress $c"; done
send "hatari-event keypress 52"
send "hatari-event keypress 1"
send "hatari-event keypress 52"
for c in 1 0; do send "hatari-event keypress $c"; done
send "hatari-event keydown 42"
send "hatari-event keypress 39"
send "hatari-event keyup 42"
for c in 6 5 1 0; do send "hatari-event keypress $c"; done
sleep 3
send "hatari-debug m \$$(printf '%x' "$rowAddr") 160"
sleep 2
python3 - "$work/hatari.log" "$rowAddr" "$target" <<'PY'
import re
import sys
log, rowAddr, target = sys.argv[1], int(sys.argv[2]), sys.argv[3]
data = open(log, "rb").read().decode(errors="replace")
lines = {}
# Keep only the LAST dump of each address (earlier sentinel probes also match).
for m in re.finditer(r"^(00[0-9A-F]{6}): ((?:[0-9a-f]{2} ){1,16})", data, re.M):
lines[int(m.group(1), 16)] = m.group(2)
raw = b""
for off in range(0, 160, 16):
if rowAddr + off not in lines:
print(f"verify-atarist-input: FAIL - missing dump line at +{off}")
sys.exit(1)
raw += bytes.fromhex(lines[rowAddr + off].replace(" ", ""))
def group(words):
p = [int.from_bytes(words[i * 2:i * 2 + 2], "big") for i in range(4)]
return [((p[0] >> b) & 1) | (((p[1] >> b) & 1) << 1) |
(((p[2] >> b) & 1) << 2) | (((p[3] >> b) & 1) << 3)
for b in range(15, -1, -1)]
pix = []
for g in range(len(raw) // 8):
pix += group(raw[g * 8:g * 8 + 8])
if pix[0:4] != [0xA, 0x5, 0xA, 0x5]:
print(f"verify-atarist-input: FAIL - sentinel wrong: {pix[0:4]}")
sys.exit(1)
count = (pix[4] << 4) | pix[5]
chars = bytes((pix[8 + 2 * i] << 4) | pix[8 + 2 * i + 1] for i in range(count))
print(f"VERIFY-ST-INPUT count={count} chars={chars!r}")
if chars.decode("ascii", errors="replace") == target:
print(f'verify-atarist-input: PASS (typed "{target}", all {len(target)} chars arrived via jlInputGetChar)')
else:
print(f'verify-atarist-input: FAIL - expected "{target}"')
sys.exit(1)
PY

116
scripts/verify-dos-input.sh Executable file
View file

@ -0,0 +1,116 @@
#!/usr/bin/env bash
# verify-dos-input.sh - Headless typed-character acceptance gate for the
# DOS port. Runs KEYS.EXE in DOSBox Staging under Xvfb, types the
# RetroNet definition-of-done string with xdotool (real X key events,
# shift and all, so ':' exercises the INT 9 shift path), takes a RAW
# native-resolution screenshot, and decodes the example's verification
# scanline (row 198) from the exact VGA DAC colors. KEYS gives all 16
# palette entries distinct colors precisely so this decode is exact.
#
# Usage: scripts/verify-dos-input.sh
# Requires: dosbox (staging), Xvfb, xdotool, python3 + PIL;
# build/dos/bin/KEYS.EXE (make dos).
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
bin_dir=$repo/build/dos/bin
conf=$repo/scripts/dosbox-386sx16.conf
target="192.168.1.10:6510"
[ -f "$bin_dir/KEYS.EXE" ] || { echo "verify-dos-input: build KEYS.EXE first (make dos)" >&2; exit 2; }
work=$(mktemp -d -t joeylib-dos-input.XXXXXX)
display_num=$((RANDOM % 500 + 300))
cleanup() {
[ -n "${dosbox_pid:-}" ] && kill "$dosbox_pid" 2>/dev/null
[ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null
rm -rf "$work"
}
trap cleanup EXIT
Xvfb ":$display_num" -screen 0 1024x768x24 >/dev/null 2>&1 &
xvfb_pid=$!
sleep 1
DISPLAY=":$display_num" dosbox \
-conf "$conf" \
-set "capture capture_dir=$work" \
-set "capture default_image_capture_formats=raw" \
-c "C:" -c "KEYS.EXE" \
"$bin_dir" >/dev/null 2>&1 &
dosbox_pid=$!
# Let DOS boot and KEYS reach its poll loop on the simulated 386SX-16.
sleep 12
# Staging titles its window "<PROG> - <n> cycles/ms"; it is the only
# client on this private display, so match any named window.
win=$(DISPLAY=":$display_num" xdotool search --name -- "." | head -1)
if [ -z "$win" ]; then
echo "verify-dos-input: FAIL - DOSBox window not found" >&2
exit 1
fi
DISPLAY=":$display_num" xdotool windowactivate --sync "$win" 2>/dev/null || true
DISPLAY=":$display_num" xdotool type --window "$win" --delay 150 -- "$target"
sleep 2
# Ctrl+F5 = DOSBox screenshot hotkey.
DISPLAY=":$display_num" xdotool key --window "$win" ctrl+F5
sleep 2
png=$(ls "$work"/*.png 2>/dev/null | head -1)
if [ -z "$png" ]; then
echo "verify-dos-input: FAIL - no screenshot captured" >&2
exit 1
fi
python3 - "$png" "$target" <<'PY'
import sys
from PIL import Image
png, target = sys.argv[1], sys.argv[2]
img = Image.open(png).convert("RGB")
w, h = img.size
# Raw capture is native 320x200; tolerate an integer upscale.
sx, sy = w // 320, h // 200
if sx < 1 or sy < 1 or w % 320 or h % 200:
print(f"verify-dos-input: FAIL - unexpected screenshot size {w}x{h}")
sys.exit(1)
def dac8(c4):
v6 = ((c4 << 2) | (c4 >> 2)) & 0x3F
return (v6 << 2) | (v6 >> 4)
# KEYS palette 0 (see buildPalette): gray ramp with the four demo
# colors and red at 15. 12-bit 0x0RGB -> exact 8-bit via the port's
# 6-bit DAC expansion.
pal12 = [(i << 8) | (i << 4) | i for i in range(16)]
pal12[0], pal12[1], pal12[2], pal12[3] = 0x000, 0x333, 0x0F0, 0xFFF
pal12[15] = 0xF00
# Nearest-color match: emulators differ in how they scale the 6-bit
# DAC to 8-bit (bit-replication vs rounding), but the palette entries
# sit ~17 units apart per channel, so nearest is unambiguous.
palRgb = [(dac8((v >> 8) & 0xF), dac8((v >> 4) & 0xF), dac8(v & 0xF)) for v in pal12]
def pix(px):
r, g, b = img.getpixel((px * sx, 198 * sy))
best, bestDist = 0, 1 << 30
for n, (pr, pg, pb) in enumerate(palRgb):
d = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2
if d < bestDist:
best, bestDist = n, d
if bestDist > 3 * 8 ** 2:
print(f"verify-dos-input: FAIL - pixel {px} color {(r, g, b)} too far from any palette entry")
sys.exit(1)
return best
row = bytes((pix(2 * i) << 4) | pix(2 * i + 1) for i in range(40))
expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii")
got = row[: len(expect)]
print("VERIFY-DOS-INPUT row=" + row.hex().upper())
if got == expect:
print(f'verify-dos-input: PASS (typed "{target}", all {len(target)} chars arrived via jlInputGetChar)')
else:
print(f"verify-dos-input: FAIL\n expected: {expect.hex().upper()}\n got: {got.hex().upper()}")
sys.exit(1)
PY

126
scripts/verify-iigs-input.sh Executable file
View file

@ -0,0 +1,126 @@
#!/usr/bin/env bash
# verify-iigs-input.sh - Headless typed-character acceptance gate for the
# IIgs port. Boots GS/OS under MAME, launches the KEYS example off
# joey.2mg, types the RetroNet definition-of-done string on the emulated
# keyboard, and reads the example's verification scanline (row 198)
# straight out of SHR memory. KEYS encodes every character it received
# from jlInputGetChar as raw nibble pixel pairs behind an A5A5 sentinel,
# so the row bytes must equal the typed ASCII exactly -- proving digits,
# '.', and ':' all arrive through the typed-character queue.
#
# Usage: scripts/verify-iigs-input.sh
# Requires: toolchains/env.sh sourced; build/iigs/bin/joey.2mg built
# with the current KEYS (make iigs-disk).
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
target="192.168.1.10:6510"
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
data_disk=$repo/build/iigs/bin/joey.2mg
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
typeFrame="${MAME_TYPE_FRAME:-6600}"
readFrame="${MAME_READ_FRAME:-12000}"
for f in "$sys_disk" "$data_disk"; do
[ -f "$f" ] || { echo "verify-iigs-input: missing $f (run 'make iigs-disk')" >&2; exit 2; }
done
work=$(mktemp -d -t joeylib-input-verify.XXXXXX)
trap 'rm -rf "$work"' EXIT
cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"
# Finder keystroke timeline as in verify-iigs.sh, then the typed string
# once KEYS is running, then a dump of verification row 198:
# base = $E1/2000 + 198*160 = $E1/9BC0
# byte 0..1 = A5 A5 sentinel, byte 2 = received count, byte 3 = pad,
# byte 4.. = one received character per byte.
cat > "$work/verify.lua" <<LUA
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local frame = 0
local idx = 1
local function field(port, name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[name]
end
local key_cmd = field(":macadb:KEY3", "Command / Open Apple")
local function press(f) if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end
local function report()
local base = 0xE19BC0
local scb0 = mem:read_u8(0xE19D00)
local bytes = {}
for i = 0, 39 do
bytes[#bytes + 1] = string.format("%02X", mem:read_u8(base + i))
end
io.write(string.format("VERIFY-IIGS-INPUT frame=%d scb0=%02X row=%s\n",
frame, scb0, table.concat(bytes)))
io.flush()
end
local steps = {
{3000, function() nat:post("J") end},
{3120, function() press(key_cmd) end},
{3126, function() nat:post("o") end},
{3180, function() release(key_cmd) end},
{3540, function() nat:post("KEYS") end},
{3660, function() press(key_cmd) end},
{3666, function() nat:post("o") end},
{3720, function() release(key_cmd) end},
{$typeFrame, function() nat:post("$target") end},
{$readFrame, function() report(); manager.machine:exit() end},
}
emu.register_frame_done(function()
frame = frame + 1
while idx <= #steps and frame >= steps[idx][1] do
steps[idx][2]()
idx = idx + 1
end
end)
LUA
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 300 mame apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
-autoboot_script "$work/verify.lua" </dev/null 2>&1) || true
line=$(echo "$out" | grep -E '^VERIFY-IIGS-INPUT ' | tail -1)
echo "$line"
if [ -z "$line" ]; then
echo "verify-iigs-input: FAIL - no report (boot/launch failed)" >&2
echo "$out" | tail -15 >&2
exit 1
fi
scb0=$(echo "$line" | sed -E 's/.*scb0=([0-9A-Fa-f]+).*/\1/')
if [ $((16#$scb0)) -ge 16 ]; then
echo "verify-iigs-input: FAIL (KEYS never launched - SCB[0]=0x$scb0)" >&2
exit 1
fi
row=$(echo "$line" | sed -E 's/.*row=([0-9A-Fa-f]+).*/\1/')
expect="A5A5"
count=$(printf '%02X' ${#target})
expect+="$count"
expect+="00"
expect+=$(printf '%s' "$target" | od -A n -t x1 | tr -d ' \n' | tr 'a-f' 'A-F')
got=${row:0:${#expect}}
if [ "$got" = "$expect" ]; then
echo "verify-iigs-input: PASS (typed \"$target\", all ${#target} chars arrived via jlInputGetChar)"
else
echo "verify-iigs-input: FAIL" >&2
echo " expected row prefix: $expect" >&2
echo " got: $got" >&2
exit 1
fi

120
scripts/verify-x68000-input.sh Executable file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env bash
# verify-x68000-input.sh - Headless typed-character acceptance gate for
# the X68000 port. Boots Human68k under the patched MAME with KEYS.X in
# AUTOEXEC.BAT, waits for the example's A5A5 sentinel to appear in
# GVRAM, types the RetroNet definition-of-done string on the emulated
# keyboard, and reads the verification scanline back out of GVRAM.
# KEYS encodes every character it received from jlInputGetChar as nibble
# pixel pairs, so the row must equal the typed ASCII exactly.
#
# GVRAM geometry (crtmod 13, 256-colour graphics plane): 16-bit word
# per pixel at $C00000, 512 words per row; the 320x200 stage is centred
# at origin (96, 156). Verification row = stage y 198 -> GVRAM y 354.
#
# X68K_SCRATCH=<dir with x68mame/> bash scripts/verify-x68000-input.sh
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}"
MAME="$repo/toolchains/cache/mame-mame0264/x68k"
TEMPLATE="$SP/x68mame/HUMAN302.XDF"
WALL="${X68K_INPUT_WALL:-900}"
target="192.168.1.10:6510"
[ -x "$MAME" ] || { echo "verify-x68000-input: missing patched MAME at $MAME" >&2; exit 2; }
[ -f "$TEMPLATE" ] || { echo "verify-x68000-input: missing $TEMPLATE" >&2; exit 2; }
[ -f "$repo/build/x68000/bin/KEYS.X" ] || { echo "verify-x68000-input: build KEYS.X first (make -f make/x68000.mk)" >&2; exit 2; }
work=$(mktemp -d -t joey-x68input.XXXXXX)
trap 'rm -rf "$work"' EXIT
printf 'A:\\KEYS.X\r\n\x1a' > "$work/AUTOEXEC.BAT"
cp "$TEMPLATE" "$work/boot.xdf"
for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do
python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1
done
python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$repo/build/x68000/bin/KEYS.X" KEYS.X >/dev/null || exit 1
python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null
cat > "$work/input.lua" <<LUA
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local frame = 0
local typedAt = 0
local done = false
-- Stage pixel (px, 198) -> GVRAM word at base + 2*px, value low byte.
local rowBase = 0xC00000 + ((156 + 198) * 512 + 96) * 2
local function pix(px)
return mem:read_u8(rowBase + 2 * px + 1)
end
local function report()
local bytes = {}
for i = 0, 39 do
bytes[#bytes + 1] = string.format("%02X", (pix(2 * i) << 4) | pix(2 * i + 1))
end
io.write(string.format("VERIFY-X68K-INPUT frame=%d typedAt=%d row=%s\n",
frame, typedAt, table.concat(bytes)))
io.flush()
end
emu.register_frame_done(function()
frame = frame + 1
if done then return end
if typedAt == 0 and frame > 600 and frame % 100 == 0 then
-- KEYS is up once initialPaint stamped the A5A5 sentinel.
if pix(0) == 0xA and pix(1) == 0x5 and pix(2) == 0xA and pix(3) == 0x5 then
typedAt = frame + 60
end
end
if typedAt ~= 0 and frame == typedAt then
nat:post("$target")
end
if typedAt ~= 0 and frame >= typedAt + 3000 then
done = true
report()
manager.machine:exit()
end
if frame > 30000 then
done = true
io.write("VERIFY-X68K-INPUT timeout: sentinel never appeared\n")
io.flush()
manager.machine:exit()
end
end)
LUA
# Run from the work dir so MAME's cfg/ and nvram/ droppings land there
# instead of the caller's cwd.
cd "$work"
out=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
-rompath "$SP/x68mame/roms" -flop1 "$work/boot.xdf" \
-video none -sound none -nothrottle \
-autoboot_script "$work/input.lua" </dev/null 2>&1) || true
line=$(echo "$out" | grep -E '^VERIFY-X68K-INPUT ' | tail -1)
echo "$line"
if [ -z "$line" ] || echo "$line" | grep -q timeout; then
echo "verify-x68000-input: FAIL - KEYS never came up" >&2
echo "$out" | tail -10 >&2
exit 1
fi
row=$(echo "$line" | sed -E 's/.*row=([0-9A-Fa-f]+).*/\1/')
expect="A5A5"
expect+=$(printf '%02X' ${#target})
expect+="00"
expect+=$(printf '%s' "$target" | od -A n -t x1 | tr -d ' \n' | tr 'a-f' 'A-F')
got=${row:0:${#expect}}
if [ "$got" = "$expect" ]; then
echo "verify-x68000-input: PASS (typed \"$target\", all ${#target} chars arrived via jlInputGetChar)"
else
echo "verify-x68000-input: FAIL" >&2
echo " expected row prefix: $expect" >&2
echo " got: $got" >&2
exit 1
fi

View file

@ -16,6 +16,13 @@
// for MOUSEMOVE events to fire continuously rather than only on button // for MOUSEMOVE events to fire continuously rather than only on button
// transitions. WFLG_RMBTRAP keeps the right mouse button from opening // transitions. WFLG_RMBTRAP keeps the right mouse button from opening
// Intuition's screen menu so right-clicks come to us as MENUDOWN. // 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 <string.h> #include <string.h>
@ -28,6 +35,7 @@
#include <proto/exec.h> #include <proto/exec.h>
#include <proto/intuition.h> #include <proto/intuition.h>
#include <proto/keymap.h>
#include <proto/lowlevel.h> #include <proto/lowlevel.h>
#include "port.h" #include "port.h"
@ -39,6 +47,24 @@
#define AMIGA_KEY_RELEASE_BIT 0x80 #define AMIGA_KEY_RELEASE_BIT 0x80
#define AMIGA_KEY_CODE_MASK 0x7F #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;
// ----- External from hal.c ----- // ----- External from hal.c -----
extern struct Screen *gScreen; extern struct Screen *gScreen;
@ -144,6 +170,10 @@ static struct Library *gLowLevelBase = NULL;
static void drainMessages(void) { static void drainMessages(void) {
struct IntuiMessage *msg; struct IntuiMessage *msg;
struct InputEvent ie;
UBYTE mapBuf[MAP_RAW_KEY_BUF_SIZE];
WORD mapCount;
WORD mapIdx;
UWORD msgClass; UWORD msgClass;
UWORD msgCode; UWORD msgCode;
int16_t msgMouseX; int16_t msgMouseX;
@ -160,6 +190,29 @@ static void drainMessages(void) {
msgCode = msg->Code; msgCode = msg->Code;
msgMouseX = (int16_t)msg->MouseX; msgMouseX = (int16_t)msg->MouseX;
msgMouseY = (int16_t)msg->MouseY; 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;
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); ReplyMsg((struct Message *)msg);
switch (msgClass) { switch (msgClass) {
@ -170,6 +223,11 @@ static void drainMessages(void) {
if (key != KEY_NONE) { if (key != KEY_NONE) {
gKeyState[key] = !isRelease; gKeyState[key] = !isRelease;
} }
// MapRawKey returns -1 on overflow (composition longer
// than the buffer) -- nothing usable, skip.
for (mapIdx = 0; mapIdx < mapCount; mapIdx++) {
jlInputCharPush((uint8_t)mapBuf[mapIdx]);
}
break; break;
case IDCMP_MOUSEMOVE: case IDCMP_MOUSEMOVE:
gMouseX = msgMouseX; gMouseX = msgMouseX;
@ -288,6 +346,11 @@ void jlpInputInit(void) {
// Kickstart, or stripped AROS), the joystick API silently reports // Kickstart, or stripped AROS), the joystick API silently reports
// disconnected sticks rather than failing init. // disconnected sticks rather than failing init.
gLowLevelBase = OpenLibrary((CONST_STRPTR)"lowlevel.library", 0); gLowLevelBase = 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);
} }
@ -298,6 +361,10 @@ void jlpInputPoll(void) {
void jlpInputShutdown(void) { void jlpInputShutdown(void) {
if (KeymapBase != NULL) {
CloseLibrary(KeymapBase);
KeymapBase = NULL;
}
if (gLowLevelBase != NULL) { if (gLowLevelBase != NULL) {
CloseLibrary(gLowLevelBase); CloseLibrary(gLowLevelBase);
gLowLevelBase = NULL; gLowLevelBase = NULL;

View file

@ -18,6 +18,17 @@
// jlKeyPressed edge detection requires that public gKeyState only // jlKeyPressed edge detection requires that public gKeyState only
// advance during jlpInputPoll, never at interrupt time -- jlInputPoll // advance during jlpInputPoll, never at interrupt time -- jlInputPoll
// snapshots gKeyState into gKeyPrev before jlpInputPoll runs. // snapshots gKeyState into gKeyPrev before jlpInputPoll runs.
//
// Typed characters: replacing ikbdsys means TOS's own keyboard
// processing never runs, so Bconin never sees our keys. Instead the
// ISR translates make codes with the TOS keyboard tables from
// Keytbl() -- the OS keymap, so national layouts are honored -- into
// a private ring that jlpInputPoll drains into the core typed-
// character queue. Shift/caps come from the ISR's own key state
// (caps-lock toggles on scan 0x3A); Ctrl chords are commands, not
// text, and produce no character. The IKBD sends one make per press
// (TOS's software auto-repeat is gone with ikbdsys), so held keys do
// not repeat characters.
#include <string.h> #include <string.h>
@ -37,6 +48,26 @@
#define SCAN_BREAK_BIT 0x80 #define SCAN_BREAK_BIT 0x80
#define SCAN_CODE_MASK 0x7F #define SCAN_CODE_MASK 0x7F
#define SCAN_TABLE_SIZE 128 #define SCAN_TABLE_SIZE 128
#define SCAN_CAPS_LOCK 0x3A
// Cursor/edit cluster scan codes. Their UNSHIFTED keytable entries are
// 0 (no character), but TOS's SHIFT table maps them to legacy numeric
// digits ('8' for shift+Up etc.), so the typed-character path must
// skip them outright. Keypad '-' (0x4A) and '+' (0x4E) sit between
// these codes and DO type.
#define SCAN_CLR_HOME 0x47
#define SCAN_CURSOR_UP 0x48
#define SCAN_CURSOR_LEFT 0x4B
#define SCAN_CURSOR_RIGHT 0x4D
#define SCAN_CURSOR_DOWN 0x50
#define SCAN_INSERT 0x52
// Kbshift(-1) state bits (TOS BIOS): bit 4 = caps lock engaged.
#define KBSHIFT_CAPS_BIT 0x10
// Private ISR->poll ring for translated characters. Power of two so
// the wrap is a mask; holds ISR_CHAR_QUEUE_SIZE - 1 pending bytes.
#define ISR_CHAR_QUEUE_SIZE 32
#define PKT_STATUS 0xF6 // + 7 bytes #define PKT_STATUS 0xF6 // + 7 bytes
#define PKT_ABS_MOUSE 0xF7 // + 5 bytes #define PKT_ABS_MOUSE 0xF7 // + 5 bytes
@ -74,9 +105,10 @@
// ----- Prototypes ----- // ----- Prototypes -----
static long ikbdHandler(void);
static long patchIkbdVector(void); static long patchIkbdVector(void);
static long restoreIkbdVector(void); static long restoreIkbdVector(void);
static long ikbdHandler(void); static bool scanIsCursorCluster(uint8_t code);
// ----- Module state ----- // ----- Module state -----
@ -169,15 +201,36 @@ static int16_t gMouseAbsY = SURFACE_HEIGHT / 2;
// jlpInputPoll can simply read the latest value. // jlpInputPoll can simply read the latest value.
static volatile uint8_t gIsrJoyByte[JOYSTICK_COUNT]; static volatile uint8_t gIsrJoyByte[JOYSTICK_COUNT];
// TOS keyboard translation tables (Keytbl), cached at init so the ISR
// only does array lookups. 128 bytes each, indexed by scan code; 0
// means the key types nothing.
static const uint8_t *gKeyTabUnshift = NULL;
static const uint8_t *gKeyTabShift = NULL;
static const uint8_t *gKeyTabCaps = NULL;
// ISR-tracked caps-lock toggle (scan 0x3A make flips it). uint8_t so
// the element size is pinned like the other ISR state.
static volatile uint8_t gIsrCapsLock = 0;
// Translated characters, ISR producer -> poll consumer. Single-
// producer single-consumer: the ISR only writes head + slots, the
// poll drain only writes tail, so no interrupt masking is needed.
static volatile uint8_t gIsrCharQueue[ISR_CHAR_QUEUE_SIZE];
static volatile uint8_t gIsrCharHead = 0;
static volatile uint8_t gIsrCharTail = 0;
// ----- Internal helpers ----- // ----- Internal helpers -----
// Runs in MFP ACIA interrupt context. Reads one byte from the ACIA, // Runs in MFP ACIA interrupt context. Reads one byte from the ACIA,
// dispatches to either keyboard handling, mouse-packet capture, or // dispatches to either keyboard handling, mouse-packet capture, or
// "discard remaining N bytes" for packets we do not yet care about. // "discard remaining N bytes" for packets we do not yet care about.
static long ikbdHandler(void) { static long ikbdHandler(void) {
const uint8_t *tab;
uint8_t byte; uint8_t byte;
uint8_t code; uint8_t code;
uint8_t key; uint8_t key;
uint8_t ch;
uint8_t next;
bool isBreak; bool isBreak;
byte = *ST_ACIA_DATA; byte = *ST_ACIA_DATA;
@ -264,6 +317,33 @@ static long ikbdHandler(void) {
if (key != KEY_NONE) { if (key != KEY_NONE) {
gIsrState[key] = !isBreak; gIsrState[key] = !isBreak;
} }
if (!isBreak) {
if (code == SCAN_CAPS_LOCK) {
gIsrCapsLock = (uint8_t)!gIsrCapsLock;
} else if (!gIsrState[KEY_LCTRL] && !gIsrState[KEY_LALT] &&
gKeyTabUnshift != NULL && !scanIsCursorCluster(code)) {
// Typed-character translation via the TOS keymap. Table
// priority mirrors TOS: shift wins over caps, caps over
// plain. A 0 entry types nothing (F-keys etc.); Ctrl/Alt
// chords are commands, not text.
if (gIsrState[KEY_LSHIFT] || gIsrState[KEY_RSHIFT]) {
tab = gKeyTabShift;
} else if (gIsrCapsLock) {
tab = gKeyTabCaps;
} else {
tab = gKeyTabUnshift;
}
ch = tab[code];
if (ch != 0) {
next = (uint8_t)((gIsrCharHead + 1u) & (ISR_CHAR_QUEUE_SIZE - 1u));
if (next != gIsrCharTail) {
gIsrCharQueue[gIsrCharHead] = ch;
gIsrCharHead = next;
}
}
}
}
return 0; return 0;
} }
@ -281,6 +361,13 @@ static long restoreIkbdVector(void) {
} }
static bool scanIsCursorCluster(uint8_t code) {
return code == SCAN_CLR_HOME || code == SCAN_CURSOR_UP ||
code == SCAN_CURSOR_LEFT || code == SCAN_CURSOR_RIGHT ||
code == SCAN_CURSOR_DOWN || code == SCAN_INSERT;
}
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
void jlpJoystickReset(jlJoystickE js) { void jlpJoystickReset(jlJoystickE js) {
@ -290,11 +377,29 @@ void jlpJoystickReset(jlJoystickE js) {
void jlpInputInit(void) { void jlpInputInit(void) {
_KEYTAB *keyTab;
memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyState, 0, sizeof(gKeyState));
memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset(gKeyPrev, 0, sizeof(gKeyPrev));
memset((void *)gIsrState, 0, sizeof(gIsrState)); memset((void *)gIsrState, 0, sizeof(gIsrState));
memset((void *)gIsrJoyByte, 0, sizeof(gIsrJoyByte)); memset((void *)gIsrJoyByte, 0, sizeof(gIsrJoyByte));
// Cache the TOS keyboard translation tables (the OS keymap) for
// the ISR's typed-character path. -1 leaves the tables unchanged
// and just returns the pointers.
keyTab = (_KEYTAB *)Keytbl((void *)-1L, (void *)-1L, (void *)-1L);
if (keyTab != NULL) {
gKeyTabUnshift = (const uint8_t *)keyTab->unshift;
gKeyTabShift = (const uint8_t *)keyTab->shift;
gKeyTabCaps = (const uint8_t *)keyTab->caps;
}
// Seed caps lock from TOS: taking over ikbdsys freezes the OS's
// own tracking, so an engaged caps lock at launch would otherwise
// be invisible until the user toggles it once.
gIsrCapsLock = (Kbshift(-1) & KBSHIFT_CAPS_BIT) != 0;
gIsrCharHead = 0;
gIsrCharTail = 0;
gMouseAbsX = SURFACE_WIDTH / 2; gMouseAbsX = SURFACE_WIDTH / 2;
gMouseAbsY = SURFACE_HEIGHT / 2; gMouseAbsY = SURFACE_HEIGHT / 2;
gMouseX = gMouseAbsX; gMouseX = gMouseAbsX;
@ -327,10 +432,21 @@ void jlpInputPoll(void) {
int32_t newY; int32_t newY;
uint8_t btn; uint8_t btn;
uint8_t joy; uint8_t joy;
uint8_t tail;
uint16_t i; uint16_t i;
memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState)); memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState));
// Drain ISR-translated characters into the core queue. Single-
// consumer side of the SPSC ring: only gIsrCharTail is written
// here, so the ISR may keep producing concurrently.
tail = gIsrCharTail;
while (tail != gIsrCharHead) {
jlInputCharPush(gIsrCharQueue[tail]);
tail = (uint8_t)((tail + 1u) & (ISR_CHAR_QUEUE_SIZE - 1u));
}
gIsrCharTail = tail;
// Drain accumulated mouse deltas + latch button state. // Drain accumulated mouse deltas + latch button state.
dx = gIsrMouseDx; dx = gIsrMouseDx;
dy = gIsrMouseDy; dy = gIsrMouseDy;

View file

@ -87,8 +87,16 @@ void jlpInputShutdown(void) {
// gKeyState[key] for every currently-held key (see joey/input.h for the jlKeyE // gKeyState[key] for every currently-held key (see joey/input.h for the jlKeyE
// codes); keys you can't map simply stay false. The cross-platform jlInputPoll // codes); keys you can't map simply stay false. The cross-platform jlInputPoll
// and joystick code build on this. // and joystick code build on this.
//
// Typed characters (jlInputGetChar) are a second, independent path: pass each
// OS/firmware-translated ASCII byte (shift and layout already applied) to
// jlInputCharPush. The core filters to the documented set, so pushing every
// translated byte is fine. If your keyboard handling runs at interrupt time,
// buffer the bytes in your own volatile ring and drain it here instead --
// jlInputCharPush is not interrupt-safe (see the DOS and ST ports).
void jlpInputPoll(void) { void jlpInputPoll(void) {
// TODO: read your keyboard and set gKeyState[...] = true for held keys. // TODO: read your keyboard and set gKeyState[...] = true for held keys.
// TODO: feed translated characters to jlInputCharPush(ch).
} }

View file

@ -10,6 +10,7 @@
#include "joey/core.h" #include "joey/core.h"
#include "codegenArenaInternal.h" #include "codegenArenaInternal.h"
#include "inputInternal.h"
#include "port.h" #include "port.h"
#include "spriteInternal.h" #include "spriteInternal.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
@ -127,6 +128,10 @@ bool jlInit(const jlConfigT *config) {
return false; return false;
} }
// Empty the typed-character queue before the port arms its
// keyboard: a shutdown/re-init cycle must not hand the app
// characters typed during the previous run.
jlInputCharReset();
jlpInputInit(); jlpInputInit();
gInitialized = true; gInitialized = true;

View file

@ -8,6 +8,12 @@
// Mouse position and joystick axes are plain current state with no // Mouse position and joystick axes are plain current state with no
// edge predicates -- games that want deltas track the previous values // edge predicates -- games that want deltas track the previous values
// themselves. // themselves.
//
// The typed-character queue (gCharQueue) is a separate path for text
// entry: ports push OS/firmware-translated ASCII via jlInputCharPush
// during jlpInputPoll, and apps pop with jlInputGetChar. The queue is
// a classic ring with one empty slot (head == tail means empty), so
// no element count is stored and push/pop touch a single index each.
#include <string.h> #include <string.h>
@ -32,6 +38,14 @@ uint8_t gJoyButtonState[JOYSTICK_COUNT][JOY_BUTTON_COUNT];
uint8_t gJoyButtonPrev [JOYSTICK_COUNT][JOY_BUTTON_COUNT]; uint8_t gJoyButtonPrev [JOYSTICK_COUNT][JOY_BUTTON_COUNT];
uint8_t gJoyDeadZone [JOYSTICK_COUNT]; uint8_t gJoyDeadZone [JOYSTICK_COUNT];
uint8_t gCharQueue [JL_CHAR_QUEUE_SIZE];
uint8_t gCharQueueHead = 0;
uint8_t gCharQueueTail = 0;
// Build-time check: the ring-index wrap masks with the size minus one,
// which only wraps correctly for a power-of-two size.
typedef int joey_charqueue_pow2_check[((JL_CHAR_QUEUE_SIZE & (JL_CHAR_QUEUE_SIZE - 1)) == 0) ? 1 : -1];
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
extern void iigsInputSnapshot(void); extern void iigsInputSnapshot(void);
@ -50,6 +64,48 @@ typedef int joey_mousebtn_size_check [(sizeof(gMouseButtonState) == MOUSE_BUTTON
typedef int joey_joybtn_size_check [(sizeof(gJoyButtonState) == JOYSTICK_COUNT * JOY_BUTTON_COUNT) ? 1 : -1]; typedef int joey_joybtn_size_check [(sizeof(gJoyButtonState) == JOYSTICK_COUNT * JOY_BUTTON_COUNT) ? 1 : -1];
#endif #endif
void jlInputCharPush(uint8_t ch) {
uint8_t head;
uint8_t next;
if (ch > 0x7E) {
return;
}
if (ch < 0x20 &&
ch != JL_CHAR_BACKSPACE && ch != JL_CHAR_TAB &&
ch != JL_CHAR_RETURN && ch != JL_CHAR_ESCAPE) {
return;
}
head = gCharQueueHead;
next = (uint8_t)((head + 1u) & (JL_CHAR_QUEUE_SIZE - 1u));
if (next == gCharQueueTail) {
return;
}
gCharQueue[head] = ch;
gCharQueueHead = next;
}
void jlInputCharReset(void) {
gCharQueueHead = 0;
gCharQueueTail = 0;
}
int jlInputGetChar(void) {
uint8_t tail;
int ch;
tail = gCharQueueTail;
if (tail == gCharQueueHead) {
return -1;
}
ch = gCharQueue[tail];
gCharQueueTail = (uint8_t)((tail + 1u) & (JL_CHAR_QUEUE_SIZE - 1u));
return ch;
}
void jlInputPoll(void) { void jlInputPoll(void) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
// One asm pass for: TTL decrement + key snapshot + mouse/joy // One asm pass for: TTL decrement + key snapshot + mouse/joy
@ -77,6 +133,11 @@ void jlWaitForAnyKey(void) {
jlInputPoll(); jlInputPoll();
for (i = (int16_t)(KEY_NONE + 1); i < (int16_t)KEY_COUNT; i++) { for (i = (int16_t)(KEY_NONE + 1); i < (int16_t)KEY_COUNT; i++) {
if (jlKeyPressed((jlKeyE)i)) { if (jlKeyPressed((jlKeyE)i)) {
// The dismissing keystroke (and anything typed during
// the wait) was consumed by this wait, not typed at a
// text field -- drop it from the character queue so a
// following jlInputGetChar never sees a stray char.
jlInputCharReset();
return; return;
} }
} }

View file

@ -37,6 +37,26 @@ extern uint8_t gJoyButtonPrev [JOYSTICK_COUNT][JOY_BUTTON_COUNT];
// with analog paddles (IIgs); ignored on digital-stick platforms. // with analog paddles (IIgs); ignored on digital-stick platforms.
extern uint8_t gJoyDeadZone [JOYSTICK_COUNT]; extern uint8_t gJoyDeadZone [JOYSTICK_COUNT];
// Typed-character FIFO backing jlInputGetChar. Ports feed it with
// jlInputCharPush from jlpInputPoll; the pop side is jlInputGetChar.
// NOT interrupt-safe: ports whose keyboard handling runs at interrupt
// time (DOS INT 9, ST ikbd) buffer translated characters in their own
// volatile ring and drain it into this queue at poll time.
extern uint8_t gCharQueue [JL_CHAR_QUEUE_SIZE];
extern uint8_t gCharQueueHead;
extern uint8_t gCharQueueTail;
// Append one translated character to the typed-character queue.
// Filters to the documented set (printable 0x20..0x7E plus
// JL_CHAR_BACKSPACE/TAB/RETURN/ESCAPE); anything else is discarded,
// so ports may push every translated byte without pre-filtering.
// Drops the character when the queue is full (drop-newest).
void jlInputCharPush(uint8_t ch);
// Empty the typed-character queue. Called by jlInit so a re-init
// never hands the app characters typed before/during a previous run.
void jlInputCharReset(void);
// (jlpJoystickReset hook -- called from jlJoystickReset to clear any auto- // (jlpJoystickReset hook -- called from jlJoystickReset to clear any auto-
// disconnect state and arm a fresh center capture -- is declared with the // disconnect state and arm a fresh center capture -- is declared with the
// other platform-only services in src/core/port.h.) // other platform-only services in src/core/port.h.)

View file

@ -5,6 +5,17 @@
// gIsrState buffer, and sends EOI to the PIC. jlpInputPoll snapshots // gIsrState buffer, and sends EOI to the PIC. jlpInputPoll snapshots
// gIsrState into gKeyState with interrupts disabled. // gIsrState into gKeyState with interrupts disabled.
// //
// Typed characters: because the hook replaces the BIOS INT 9 handler
// outright (no chaining, no EOI from the BIOS), INT 16h never sees our
// keys, so the ISR translates make codes itself with a US-layout
// scancode->ASCII pair of tables (normal/shifted) plus caps-lock
// tracking, seeded from the BIOS shift-flag byte at 0040:0017. The
// translated bytes land in a private locked ring (gIsrCharQueue) that
// jlpInputPoll drains into the core typed-character queue. Keys held
// with Ctrl or Alt are not characters and are skipped. The hard-coded
// US layout matches the port's existing hard-coded scan map; KEYB
// layouts are not applied (they hook the BIOS handler we replace).
//
// The two-buffer split is required for jlKeyPressed edge detection. // The two-buffer split is required for jlKeyPressed edge detection.
// jlInputPoll does memcpy(gKeyPrev, gKeyState) *before* jlpInputPoll // jlInputPoll does memcpy(gKeyPrev, gKeyState) *before* jlpInputPoll
// runs, so whatever gKeyState holds at that moment becomes gKeyPrev. // runs, so whatever gKeyState holds at that moment becomes gKeyPrev.
@ -29,6 +40,7 @@
#include <go32.h> #include <go32.h>
#include <pc.h> #include <pc.h>
#include <string.h> #include <string.h>
#include <sys/farptr.h>
#include "port.h" #include "port.h"
#include "inputInternal.h" #include "inputInternal.h"
@ -45,6 +57,47 @@
#define SCAN_TABLE_SIZE 128 #define SCAN_TABLE_SIZE 128
#define ISR_LOCK_SIZE 4096 #define ISR_LOCK_SIZE 4096
// Caps/num-lock make codes, and the BIOS keyboard shift-flag byte in
// the BIOS data area (0040:0017); bit 6 = caps-lock active, bit 5 =
// num-lock active. Read once at init to seed gIsrCapsLock/gIsrNumLock
// -- the BIOS can't update the byte after we take INT 9, so from then
// on the ISR tracks the toggles itself.
#define SCAN_CAPS_LOCK 0x3A
#define SCAN_NUM_LOCK 0x45
#define BIOS_KB_FLAG_ADDR 0x417
#define BIOS_KB_FLAG_CAPS 0x40
#define BIOS_KB_FLAG_NUM 0x20
// Numeric keypad scan range (set 1). With num-lock active these type
// digits and '.'; the grey arrow cluster shares the codes but arrives
// behind an 0xE0 prefix, which the ISR tracks to keep them apart.
#define SCAN_KEYPAD_FIRST 0x47
#define SCAN_KEYPAD_LAST 0x53
// AT keyboards bracket extended keys with fake-shift sequences for XT
// compatibility: 0xE0 0x2A / 0xE0 0xAA (fake left shift), and with
// right shift physically held, 0xE0 0xB6 / 0xE0 0x36 (fake right
// shift). All four decode to these codes after the break bit is
// masked and must be ignored, or the fakes corrupt the real shift
// state while an extended key is held.
#define SCAN_FAKE_LSHIFT 0x2A
#define SCAN_FAKE_RSHIFT 0x36
// Pause sends 0xE1 0x1D 0x45 / 0xE1 0x9D 0xC5 -- an 0xE1 prefix plus
// two payload bytes that would otherwise read as Ctrl and NumLock
// events. Swallow the payload to keep the tracked lock state honest.
#define SCAN_PAUSE_PREFIX 0xE1
#define PAUSE_PAYLOAD_BYTES 2
// Extended two-byte keys that still type a character: keypad Enter
// (0xE0 0x1C) and keypad '/' (0xE0 0x35).
#define SCAN_ENTER 0x1C
#define SCAN_SLASH 0x35
// Private ISR->poll ring for translated characters. Power of two so
// the wrap is a mask; holds ISR_CHAR_QUEUE_SIZE - 1 pending bytes.
#define ISR_CHAR_QUEUE_SIZE 32
// INT 33h mouse driver functions and button bits. // INT 33h mouse driver functions and button bits.
#define MOUSE_INT 0x33 #define MOUSE_INT 0x33
#define MOUSE_FN_RESET 0x0000 #define MOUSE_FN_RESET 0x0000
@ -149,6 +202,59 @@ static const uint8_t gScanToKey[SCAN_TABLE_SIZE] = {
[0x50] = KEY_DOWN, [0x50] = KEY_DOWN,
}; };
// US-layout set-1 scancode -> ASCII, normal and shifted. Only make
// codes reach these; slots left at 0 produce no character. Letters
// are lowercase here -- the ISR flips to the shifted table when the
// effective shift (shift XOR caps, letters only) is active.
static const uint8_t gScanAsciiNormal[SCAN_TABLE_SIZE] = {
[0x01] = JL_CHAR_ESCAPE,
[0x02] = '1', [0x03] = '2', [0x04] = '3', [0x05] = '4',
[0x06] = '5', [0x07] = '6', [0x08] = '7', [0x09] = '8',
[0x0A] = '9', [0x0B] = '0', [0x0C] = '-', [0x0D] = '=',
[0x0E] = JL_CHAR_BACKSPACE, [0x0F] = JL_CHAR_TAB,
[0x10] = 'q', [0x11] = 'w', [0x12] = 'e', [0x13] = 'r',
[0x14] = 't', [0x15] = 'y', [0x16] = 'u', [0x17] = 'i',
[0x18] = 'o', [0x19] = 'p', [0x1A] = '[', [0x1B] = ']',
[0x1C] = JL_CHAR_RETURN,
[0x1E] = 'a', [0x1F] = 's', [0x20] = 'd', [0x21] = 'f',
[0x22] = 'g', [0x23] = 'h', [0x24] = 'j', [0x25] = 'k',
[0x26] = 'l', [0x27] = ';', [0x28] = '\'', [0x29] = '`',
[0x2B] = '\\',
[0x2C] = 'z', [0x2D] = 'x', [0x2E] = 'c', [0x2F] = 'v',
[0x30] = 'b', [0x31] = 'n', [0x32] = 'm', [0x33] = ',',
[0x34] = '.', [0x35] = '/',
[0x37] = '*', [0x39] = ' ',
[0x4A] = '-', [0x4E] = '+',
};
static const uint8_t gScanAsciiShift[SCAN_TABLE_SIZE] = {
[0x01] = JL_CHAR_ESCAPE,
[0x02] = '!', [0x03] = '@', [0x04] = '#', [0x05] = '$',
[0x06] = '%', [0x07] = '^', [0x08] = '&', [0x09] = '*',
[0x0A] = '(', [0x0B] = ')', [0x0C] = '_', [0x0D] = '+',
[0x0E] = JL_CHAR_BACKSPACE, [0x0F] = JL_CHAR_TAB,
[0x10] = 'Q', [0x11] = 'W', [0x12] = 'E', [0x13] = 'R',
[0x14] = 'T', [0x15] = 'Y', [0x16] = 'U', [0x17] = 'I',
[0x18] = 'O', [0x19] = 'P', [0x1A] = '{', [0x1B] = '}',
[0x1C] = JL_CHAR_RETURN,
[0x1E] = 'A', [0x1F] = 'S', [0x20] = 'D', [0x21] = 'F',
[0x22] = 'G', [0x23] = 'H', [0x24] = 'J', [0x25] = 'K',
[0x26] = 'L', [0x27] = ':', [0x28] = '"', [0x29] = '~',
[0x2B] = '|',
[0x2C] = 'Z', [0x2D] = 'X', [0x2E] = 'C', [0x2F] = 'V',
[0x30] = 'B', [0x31] = 'N', [0x32] = 'M', [0x33] = '<',
[0x34] = '>', [0x35] = '?',
[0x37] = '*', [0x39] = ' ',
[0x4A] = '-', [0x4E] = '+',
};
// Keypad digits/'.' typed when num-lock is active (0x47..0x53). The
// '-'/'+' slots stay 0: those keys type regardless of num-lock and
// live in the main tables instead.
static const uint8_t gKeypadAscii[SCAN_KEYPAD_LAST - SCAN_KEYPAD_FIRST + 1] = {
'7', '8', '9', 0, '4', '5', '6', 0, '1', '2', '3', '0', '.'
};
static _go32_dpmi_seginfo gOldHandler; static _go32_dpmi_seginfo gOldHandler;
static _go32_dpmi_seginfo gNewHandler; static _go32_dpmi_seginfo gNewHandler;
static bool gHooked = false; static bool gHooked = false;
@ -156,6 +262,22 @@ static bool gHooked = false;
// src/core/inputInternal.h for the full rationale. // src/core/inputInternal.h for the full rationale.
static volatile uint8_t gIsrState[KEY_COUNT]; static volatile uint8_t gIsrState[KEY_COUNT];
// ISR-tracked lock/prefix state. uint8_t (not bool) so the locked-data
// regions have known one-byte elements.
static volatile uint8_t gIsrExtended = 0; // last byte was 0xE0
static volatile uint8_t gIsrE1Skip = 0; // Pause payload bytes left to swallow
static volatile uint8_t gIsrCapsLock = 0;
static volatile uint8_t gIsrCapsHeld = 0; // suppress typematic re-toggle
static volatile uint8_t gIsrNumLock = 0;
static volatile uint8_t gIsrNumHeld = 0;
// Translated characters, ISR producer -> poll consumer. Single-
// producer single-consumer: the ISR only writes head + slots, the
// poll drain only writes tail, so no interrupt masking is needed.
static volatile uint8_t gIsrCharQueue[ISR_CHAR_QUEUE_SIZE];
static volatile uint8_t gIsrCharHead = 0;
static volatile uint8_t gIsrCharTail = 0;
static bool gMousePresent = false; static bool gMousePresent = false;
static bool gJoystickPresent = false; static bool gJoystickPresent = false;
@ -165,17 +287,100 @@ static void keyboardIsr(void) {
uint8_t scan; uint8_t scan;
uint8_t code; uint8_t code;
uint8_t key; uint8_t key;
uint8_t extended;
uint8_t ch;
uint8_t next;
bool isBreak; bool isBreak;
bool useShift;
scan = inportb(KB_DATA_PORT); scan = inportb(KB_DATA_PORT);
if (scan != SCAN_EXTENDED) { // Pause's 0xE1-prefixed payload would otherwise read as Ctrl and
// NumLock events (0x1D 0x45 / 0x9D 0xC5) -- swallow it whole.
if (gIsrE1Skip != 0) {
gIsrE1Skip = (uint8_t)(gIsrE1Skip - 1);
outportb(PIC_CMD_PORT, PIC_EOI);
return;
}
if (scan == SCAN_PAUSE_PREFIX) {
gIsrE1Skip = PAUSE_PAYLOAD_BYTES;
outportb(PIC_CMD_PORT, PIC_EOI);
return;
}
if (scan == SCAN_EXTENDED) {
gIsrExtended = 1;
outportb(PIC_CMD_PORT, PIC_EOI);
return;
}
extended = gIsrExtended;
gIsrExtended = 0;
isBreak = (scan & SCAN_BREAK_BIT) != 0; isBreak = (scan & SCAN_BREAK_BIT) != 0;
code = (uint8_t)(scan & SCAN_CODE_MASK); code = (uint8_t)(scan & SCAN_CODE_MASK);
// Fake shifts bracket extended keys (arrows etc.) on AT keyboards;
// treating them as real shift makes would corrupt the live shift
// state while an arrow is held. Drop both variants entirely.
if (extended && (code == SCAN_FAKE_LSHIFT || code == SCAN_FAKE_RSHIFT)) {
outportb(PIC_CMD_PORT, PIC_EOI);
return;
}
key = gScanToKey[code]; key = gScanToKey[code];
if (key != KEY_NONE) { if (key != KEY_NONE) {
gIsrState[key] = !isBreak; gIsrState[key] = !isBreak;
} }
if (!isBreak) {
// Lock-key toggles fire once per press; typematic repeat of a
// held lock key must not flap the state (gIsr*Held mirrors the
// BIOS's held-bit for exactly this).
if (code == SCAN_CAPS_LOCK) {
if (!gIsrCapsHeld) {
gIsrCapsLock = (uint8_t)!gIsrCapsLock;
}
gIsrCapsHeld = 1;
} else if (code == SCAN_NUM_LOCK) {
if (!gIsrNumHeld) {
gIsrNumLock = (uint8_t)!gIsrNumLock;
}
gIsrNumHeld = 1;
} else if (!gIsrState[KEY_LCTRL] && !gIsrState[KEY_LALT]) {
// Typed-character translation. Ctrl/Alt chords are
// commands, not text, so they produce no character.
ch = 0;
if (extended) {
// Of the 0xE0 pairs only keypad Enter and keypad '/'
// type; the rest (arrows, Home/End, ...) do not.
if (code == SCAN_ENTER || code == SCAN_SLASH) {
ch = gScanAsciiNormal[code];
}
} else if (code >= SCAN_KEYPAD_FIRST && code <= SCAN_KEYPAD_LAST &&
gIsrNumLock && gKeypadAscii[code - SCAN_KEYPAD_FIRST] != 0) {
ch = gKeypadAscii[code - SCAN_KEYPAD_FIRST];
} else {
useShift = gIsrState[KEY_LSHIFT] || gIsrState[KEY_RSHIFT];
if (gIsrCapsLock &&
gScanAsciiNormal[code] >= 'a' && gScanAsciiNormal[code] <= 'z') {
useShift = !useShift;
}
ch = useShift ? gScanAsciiShift[code] : gScanAsciiNormal[code];
}
if (ch != 0) {
next = (uint8_t)((gIsrCharHead + 1u) & (ISR_CHAR_QUEUE_SIZE - 1u));
if (next != gIsrCharTail) {
gIsrCharQueue[gIsrCharHead] = ch;
gIsrCharHead = next;
}
}
}
} else {
if (code == SCAN_CAPS_LOCK) {
gIsrCapsHeld = 0;
} else if (code == SCAN_NUM_LOCK) {
gIsrNumHeld = 0;
}
} }
outportb(PIC_CMD_PORT, PIC_EOI); outportb(PIC_CMD_PORT, PIC_EOI);
@ -330,13 +535,39 @@ void jlpJoystickReset(jlJoystickE js) {
void jlpInputInit(void) { void jlpInputInit(void) {
uint8_t kbFlags;
memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyState, 0, sizeof(gKeyState));
memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset(gKeyPrev, 0, sizeof(gKeyPrev));
memset((void *)gIsrState, 0, sizeof(gIsrState)); memset((void *)gIsrState, 0, sizeof(gIsrState));
// Seed the lock toggles from the BIOS shift-flag byte -- the last
// state the BIOS saw before we take INT 9 away from it.
kbFlags = _farpeekb(_dos_ds, BIOS_KB_FLAG_ADDR);
gIsrCapsLock = (kbFlags & BIOS_KB_FLAG_CAPS) != 0;
gIsrNumLock = (kbFlags & BIOS_KB_FLAG_NUM) != 0;
gIsrExtended = 0;
gIsrE1Skip = 0;
gIsrCapsHeld = 0;
gIsrNumHeld = 0;
gIsrCharHead = 0;
gIsrCharTail = 0;
_go32_dpmi_lock_code(keyboardIsr, ISR_LOCK_SIZE); _go32_dpmi_lock_code(keyboardIsr, ISR_LOCK_SIZE);
_go32_dpmi_lock_data((void *)gScanToKey, sizeof(gScanToKey)); _go32_dpmi_lock_data((void *)gScanToKey, sizeof(gScanToKey));
_go32_dpmi_lock_data((void *)gIsrState, sizeof(gIsrState)); _go32_dpmi_lock_data((void *)gIsrState, sizeof(gIsrState));
_go32_dpmi_lock_data((void *)gScanAsciiNormal, sizeof(gScanAsciiNormal));
_go32_dpmi_lock_data((void *)gScanAsciiShift, sizeof(gScanAsciiShift));
_go32_dpmi_lock_data((void *)gKeypadAscii, sizeof(gKeypadAscii));
_go32_dpmi_lock_data((void *)gIsrCharQueue, sizeof(gIsrCharQueue));
_go32_dpmi_lock_data((void *)&gIsrExtended, sizeof(gIsrExtended));
_go32_dpmi_lock_data((void *)&gIsrE1Skip, sizeof(gIsrE1Skip));
_go32_dpmi_lock_data((void *)&gIsrCapsLock, sizeof(gIsrCapsLock));
_go32_dpmi_lock_data((void *)&gIsrCapsHeld, sizeof(gIsrCapsHeld));
_go32_dpmi_lock_data((void *)&gIsrNumLock, sizeof(gIsrNumLock));
_go32_dpmi_lock_data((void *)&gIsrNumHeld, sizeof(gIsrNumHeld));
_go32_dpmi_lock_data((void *)&gIsrCharHead, sizeof(gIsrCharHead));
_go32_dpmi_lock_data((void *)&gIsrCharTail, sizeof(gIsrCharTail));
_go32_dpmi_get_protected_mode_interrupt_vector(9, &gOldHandler); _go32_dpmi_get_protected_mode_interrupt_vector(9, &gOldHandler);
@ -354,9 +585,22 @@ void jlpInputInit(void) {
void jlpInputPoll(void) { void jlpInputPoll(void) {
uint8_t tail;
disable(); disable();
memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState)); memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState));
enable(); enable();
// Drain ISR-translated characters into the core queue. Single-
// consumer side of the SPSC ring: only gIsrCharTail is written
// here, so the ISR may keep producing concurrently.
tail = gIsrCharTail;
while (tail != gIsrCharHead) {
jlInputCharPush(gIsrCharQueue[tail]);
tail = (uint8_t)((tail + 1u) & (ISR_CHAR_QUEUE_SIZE - 1u));
}
gIsrCharTail = tail;
mousePoll(); mousePoll();
joystickPoll(); joystickPoll();
} }

View file

@ -6,6 +6,11 @@
// uninitialized corrupted KEGS' emulation state. Softswitches have no // uninitialized corrupted KEGS' emulation state. Softswitches have no
// such dependency: they are live memory-mapped hardware. // such dependency: they are live memory-mapped hardware.
// //
// The typed-character queue is fed from the same $C000 drain: the ADB
// firmware translates shift/caps/layout before the byte reaches the
// strobe register, so each drained event doubles as a typed character
// (jlInputCharPush) with no extra keymap work.
//
// Tradeoff: $C000 reports the *last* key pressed, not a per-key matrix. // Tradeoff: $C000 reports the *last* key pressed, not a per-key matrix.
// Holding multiple non-modifier keys simultaneously cannot be observed; // Holding multiple non-modifier keys simultaneously cannot be observed;
// the demo and any game using this port sees one typable key at a time, // the demo and any game using this port sees one typable key at a time,
@ -455,6 +460,19 @@ void jlpInputPoll(void) {
if (key != KEY_NONE) { if (key != KEY_NONE) {
gKeyState[key] = true; gKeyState[key] = true;
} }
// Typed-character path: the ADB firmware already applies
// shift/caps and the keyboard layout, so the strobe byte IS
// the translated character. The Delete key (0x7F, the IIgs
// backspace key) is normalized to JL_CHAR_BACKSPACE. A raw
// 0x08 is the LEFT-ARROW key (Apple II heritage), and arrows
// are not characters -- suppress it so cursor movement never
// types a backspace; the other arrows' codes are stopped by
// jlInputCharPush's filter anyway.
if (ascii == ASCII_DELETE) {
jlInputCharPush(JL_CHAR_BACKSPACE);
} else if (ascii != ASCII_LEFT) {
jlInputCharPush(ascii);
}
(void)*IIGS_KBDSTRB; (void)*IIGS_KBDSTRB;
} }

View file

@ -46,6 +46,10 @@
// _iocs_bitsns key-group numbers that carry the keys jlKeyE cares about. // _iocs_bitsns key-group numbers that carry the keys jlKeyE cares about.
#define X68K_KEYGROUP_COUNT 15 #define X68K_KEYGROUP_COUNT 15
// Cap on the per-poll IOCS type-ahead drain for the typed-character
// queue -- purely defensive, so a wedged _B_KEYSNS can't spin forever.
#define X68K_KEY_DRAIN_GUARD 32u
// MC68901 MFP general-purpose I/O. Bit 4 is the CRTC's V-DISP line. // MC68901 MFP general-purpose I/O. Bit 4 is the CRTC's V-DISP line.
#define X68K_MFP_GPIP ((volatile uint8_t *)0xE88001L) #define X68K_MFP_GPIP ((volatile uint8_t *)0xE88001L)
#define X68K_GPIP_VDISP 0x10u #define X68K_GPIP_VDISP 0x10u
@ -403,6 +407,17 @@ void jlpPresent(const jlSurfaceT *src) {
// to install, no vector to take over and no packet state machine -- unlike the // to install, no vector to take over and no packet state machine -- unlike the
// ST, which has to replace the TOS ikbdsys vector and decode IKBD packets. // ST, which has to replace the TOS ikbdsys vector and decode IKBD packets.
void jlpInputInit(void) { void jlpInputInit(void) {
uint16_t drain;
// Empty the IOCS type-ahead buffer so keys typed at the Human68k
// prompt (including the Enter that launched us) don't surface as
// the app's first typed characters.
for (drain = 0; drain < X68K_KEY_DRAIN_GUARD; drain++) {
if (_iocs_b_keysns() == 0) {
break;
}
(void)_iocs_b_keyinp();
}
} }
@ -458,22 +473,37 @@ void jlpInputPoll(void) {
uint8_t groups[X68K_KEYGROUP_COUNT]; uint8_t groups[X68K_KEYGROUP_COUNT];
uint16_t group; uint16_t group;
uint16_t key; uint16_t key;
uint16_t drain;
uint8_t scan; uint8_t scan;
for (group = 0; group < X68K_KEYGROUP_COUNT; group++) { for (group = 0; group < X68K_KEYGROUP_COUNT; group++) {
groups[group] = (uint8_t)(_iocs_bitsns((int)group) & 0xFF); groups[group] = (uint8_t)(_iocs_bitsns((int)group) & 0xFF);
} }
// Write both states each poll: the bitmap is live key-DOWN data,
// so a released key must drop back to 0 here (set-only left keys
// latched down forever, hanging any second jlWaitForAnyKey).
for (key = 1; key < KEY_COUNT; key++) { for (key = 1; key < KEY_COUNT; key++) {
scan = kScanForKey[key]; scan = kScanForKey[key];
if (scan == 0u) { if (scan == 0u) {
continue; continue;
} }
group = (uint16_t)(scan >> 3); group = (uint16_t)(scan >> 3);
if (group < X68K_KEYGROUP_COUNT && if (group < X68K_KEYGROUP_COUNT) {
(groups[group] & (uint8_t)(1u << (scan & 7u))) != 0u) { gKeyState[key] = (uint8_t)((groups[group] >> (scan & 7u)) & 1u);
gKeyState[key] = 1u;
} }
} }
// Typed-character path: the IOCS keyboard interrupt queues
// translated characters (shift/caps/layout applied) independently
// of the bitmap above; _B_KEYINP pops (scancode << 8) | char.
// Non-character keys carry 0 in the low byte and are dropped by
// jlInputCharPush's filter.
for (drain = 0; drain < X68K_KEY_DRAIN_GUARD; drain++) {
if (_iocs_b_keysns() == 0) {
break;
}
jlInputCharPush((uint8_t)(_iocs_b_keyinp() & 0xFFu));
}
} }

211
tests/host/inputHost.c Normal file
View file

@ -0,0 +1,211 @@
// inputHost.c - Host-cc harness for the typed-character queue
// (jlInputGetChar / jlInputCharPush in src/core/input.c). Exercises
// FIFO ordering, the character filter, drop-newest overflow, reset
// semantics, and the jlInputPoll refill path via a mocked
// jlpInputPoll.
//
// Built by scripts/check-input.sh with -DJOEYLIB_PLATFORM_BLANK so
// port.h routes jlpInputPoll/jlpJoystickReset to the mocks below.
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "joey/input.h"
#include "inputInternal.h"
#include "port.h"
// Characters the mocked jlpInputPoll delivers on its next call
// (one-shot; cleared after delivery).
static const uint8_t *gMockChars = NULL;
static int gMockCount = 0;
// Prototypes (harness-local, alphabetical).
static bool checkEmpty(const char *name);
static bool popExpect(const char *name, const uint8_t *expect, int count);
static void pushAll(const uint8_t *chars, int count);
static bool runFilter(void);
static bool runInterleave(void);
static bool runOrder(void);
static bool runOverflow(void);
static bool runPollRefill(void);
static bool runReset(void);
void jlpInputPoll(void) {
int i;
for (i = 0; i < gMockCount; i++) {
jlInputCharPush(gMockChars[i]);
}
gMockCount = 0;
}
void jlpJoystickReset(jlJoystickE js) {
(void)js;
}
static bool checkEmpty(const char *name) {
int got;
got = jlInputGetChar();
if (got != -1) {
printf(" %s: expected empty (-1), got %d\n", name, got);
return false;
}
return true;
}
static bool popExpect(const char *name, const uint8_t *expect, int count) {
int got;
int i;
for (i = 0; i < count; i++) {
got = jlInputGetChar();
if (got != (int)expect[i]) {
printf(" %s: index %d: expected 0x%02X, got %d\n",
name, i, expect[i], got);
return false;
}
}
return checkEmpty(name);
}
static void pushAll(const uint8_t *chars, int count) {
int i;
for (i = 0; i < count; i++) {
jlInputCharPush(chars[i]);
}
}
// Control characters other than BS/TAB/CR/ESC, plus DEL and 8-bit
// values, must never surface; the documented four and the printable
// range must pass through unchanged.
static bool runFilter(void) {
static const uint8_t rejected[] = { 0x00, 0x01, 0x07, 0x0A, 0x0C, 0x1F, 0x7F };
static const uint8_t accepted[] = {
JL_CHAR_BACKSPACE, JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE,
0x20, '0', 'Z', 'z', '~', 0x7E
};
bool ok;
jlInputCharReset();
pushAll(rejected, (int)sizeof(rejected));
ok = checkEmpty("filter-rejects");
pushAll(accepted, (int)sizeof(accepted));
ok &= popExpect("filter-accepts", accepted, (int)sizeof(accepted));
printf(" filter: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// Pops interleaved with pushes keep FIFO order across the wrap point.
static bool runInterleave(void) {
static const uint8_t first[] = { 'a', 'b', 'c' };
static const uint8_t second[] = { 'd', 'e' };
static const uint8_t rest[] = { 'b', 'c', 'd', 'e' };
bool ok;
int got;
jlInputCharReset();
pushAll(first, (int)sizeof(first));
got = jlInputGetChar();
ok = (got == 'a');
if (!ok) {
printf(" interleave: expected 'a', got %d\n", got);
}
pushAll(second, (int)sizeof(second));
ok &= popExpect("interleave", rest, (int)sizeof(rest));
printf(" interleave: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// The definition-of-done string from the RetroNet handoff must come
// back byte-for-byte.
static bool runOrder(void) {
static const char target[] = "192.168.1.10:6510";
bool ok;
jlInputCharReset();
pushAll((const uint8_t *)target, (int)(sizeof(target) - 1));
ok = popExpect("order", (const uint8_t *)target, (int)(sizeof(target) - 1));
printf(" order: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// Overflow drops the NEWEST characters: exactly JL_CHAR_QUEUE_SIZE - 1
// survive, and they are the first ones pushed.
static bool runOverflow(void) {
uint8_t burst[JL_CHAR_QUEUE_SIZE + 8];
bool ok;
int i;
jlInputCharReset();
for (i = 0; i < (int)sizeof(burst); i++) {
burst[i] = (uint8_t)('!' + (i % 0x5E));
}
pushAll(burst, (int)sizeof(burst));
ok = popExpect("overflow", burst, JL_CHAR_QUEUE_SIZE - 1);
printf(" overflow: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// The real app-facing path: jlInputPoll runs the port poll, which
// pushes translated characters; jlInputGetChar then pops them.
static bool runPollRefill(void) {
static const uint8_t typed[] = { 'H', 'i', '!', JL_CHAR_RETURN };
bool ok;
jlInputCharReset();
gMockChars = typed;
gMockCount = (int)sizeof(typed);
jlInputPoll();
ok = popExpect("poll-refill", typed, (int)sizeof(typed));
jlInputPoll();
ok &= checkEmpty("poll-refill-once");
printf(" poll-refill: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// jlInputCharReset empties the queue (the jlInit path).
static bool runReset(void) {
static const uint8_t some[] = { 'x', 'y', 'z' };
bool ok;
jlInputCharReset();
pushAll(some, (int)sizeof(some));
jlInputCharReset();
ok = checkEmpty("reset");
printf(" reset: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
int main(void) {
bool ok = true;
printf("inputHost: typed-character queue scenarios\n");
ok &= runOrder();
ok &= runFilter();
ok &= runOverflow();
ok &= runInterleave();
ok &= runReset();
ok &= runPollRefill();
if (!ok) {
printf("inputHost: FAIL\n");
return 1;
}
printf("inputHost: PASS\n");
return 0;
}