Full keyboard/typing support added. A few bugs fixed.
This commit is contained in:
parent
b8b8c7dd93
commit
74bc0eca7a
21 changed files with 1792 additions and 61 deletions
14
README.md
14
README.md
|
|
@ -406,6 +406,14 @@ Call `jlInputPoll` once per frame, then query the state predicates.
|
|||
Edge predicates (`*Pressed`, `*Released`) fire only in the frame the
|
||||
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
|
||||
typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE,
|
||||
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_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 jlWaitForAnyKey (void);
|
||||
int jlInputGetChar (void);
|
||||
|
||||
bool jlKeyDown (jlKeyE key);
|
||||
bool jlKeyPressed (jlKeyE key);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,15 @@
|
|||
// Holding the left mouse button while the pointer is over a cell lights
|
||||
// it as if the corresponding key were down. Press ESC to quit.
|
||||
//
|
||||
// The 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
|
||||
// since last frame -- so on idle frames, the only work is the cursor
|
||||
// erase + redraw + a tiny rect-present pair. The cursor erase is
|
||||
|
|
@ -14,14 +23,15 @@
|
|||
#include <stdio.h>
|
||||
|
||||
#include <joey/joey.h>
|
||||
#include <joey/debug.h>
|
||||
|
||||
#define GRID_COLS 10
|
||||
#define GRID_ROWS 6
|
||||
#define CELL_W 28
|
||||
#define CELL_H 28
|
||||
#define CELL_H 26
|
||||
#define GAP 4
|
||||
#define MARGIN_X 2
|
||||
#define MARGIN_Y 6
|
||||
#define MARGIN_Y 2
|
||||
|
||||
#define CURSOR_W 4
|
||||
#define CURSOR_H 4
|
||||
|
|
@ -33,13 +43,40 @@
|
|||
|
||||
#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 cellAtPoint(int16_t px, int16_t py, int16_t *outCol, int16_t *outRow);
|
||||
static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t cursorRow);
|
||||
static void drawCell(jlSurfaceT *screen, int16_t col, int16_t row, bool lit);
|
||||
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 logTypedHistory(void);
|
||||
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);
|
||||
|
||||
// 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 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) {
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
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++) {
|
||||
colors[i] = 0x0000;
|
||||
colors[i] = (uint16_t)((i << 8) | (i << 4) | i);
|
||||
}
|
||||
colors[COLOR_BACKGROUND] = 0x0000; // black
|
||||
colors[COLOR_UNLIT] = 0x0333; // dark gray
|
||||
colors[COLOR_LIT] = 0x00F0; // bright green
|
||||
colors[COLOR_CURSOR] = 0x0FFF; // white
|
||||
colors[15] = 0x0F00; // red (0x0FFF is the cursor's)
|
||||
|
||||
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) {
|
||||
int16_t col;
|
||||
int16_t row;
|
||||
|
|
@ -149,10 +350,32 @@ static void initialPaint(jlSurfaceT *screen) {
|
|||
gCellLit[row][col] = false;
|
||||
}
|
||||
}
|
||||
redrawTextStrip(screen);
|
||||
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) {
|
||||
int16_t col;
|
||||
int16_t row;
|
||||
|
|
@ -169,8 +392,8 @@ static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t c
|
|||
if (lit == gCellLit[row][col]) {
|
||||
continue;
|
||||
}
|
||||
/* drawCell marks the cell's rect dirty; jlStagePresent
|
||||
* flushes that one band. */
|
||||
// drawCell marks the cell's rect dirty; jlStagePresent
|
||||
// flushes that one band.
|
||||
drawCell(screen, col, row, lit);
|
||||
jlStagePresent();
|
||||
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
|
||||
// stamp the new cursor at the current mouse position. Both rects are
|
||||
// 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) {
|
||||
drawCell(screen, gLastCursorCol, gLastCursorRow, gCellLit[gLastCursorRow][gLastCursorCol]);
|
||||
} 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);
|
||||
if ((int16_t)(gLastCursorY + CURSOR_H) > TEXT_STRIP_Y) {
|
||||
redrawTextStrip(screen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawCursor(screen, mouseX, mouseY);
|
||||
/* All draw calls above marked their rects dirty; one jlStagePresent
|
||||
* flushes the union (cursor erase + cursor draw). */
|
||||
// All draw calls above marked their rects dirty; one jlStagePresent
|
||||
// flushes the union (cursor erase + cursor draw).
|
||||
jlStagePresent();
|
||||
|
||||
gLastCursorX = mouseX;
|
||||
|
|
@ -245,8 +521,10 @@ int main(void) {
|
|||
cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow);
|
||||
presentChangedCells(screen, cursorCol, cursorRow);
|
||||
updateCursor(screen, cursorCol, cursorRow);
|
||||
processTypedChars(screen);
|
||||
}
|
||||
|
||||
logTypedHistory();
|
||||
jlShutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,16 @@
|
|||
// jlKeyPressed(k) -- rising 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:
|
||||
//
|
||||
// jlMouseX/Y() -- pointer position in surface
|
||||
|
|
@ -87,8 +97,43 @@ typedef enum {
|
|||
#define JOYSTICK_AXIS_MAX 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);
|
||||
|
||||
// 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
|
||||
// jlInputPoll, so per-port jlpInputPoll machinery (including
|
||||
// audio-friendly IRQ-driven samplers) keeps working while the
|
||||
|
|
|
|||
|
|
@ -151,15 +151,6 @@
|
|||
#define JL_HAS_AUDIO_CRITICAL_ENTER
|
||||
#define JL_HAS_AUDIO_CRITICAL_EXIT
|
||||
#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)
|
||||
#define JL_HAS_INIT
|
||||
#define JL_HAS_SHUTDOWN
|
||||
|
|
@ -229,15 +220,6 @@
|
|||
#define JL_HAS_AUDIO_CRITICAL_ENTER
|
||||
#define JL_HAS_AUDIO_CRITICAL_EXIT
|
||||
#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)
|
||||
#define JL_HAS_INIT
|
||||
#define JL_HAS_SHUTDOWN
|
||||
|
|
@ -307,15 +289,6 @@
|
|||
#define JL_HAS_AUDIO_CRITICAL_ENTER
|
||||
#define JL_HAS_AUDIO_CRITICAL_EXIT
|
||||
#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)
|
||||
#define JL_HAS_INIT
|
||||
#define JL_HAS_SHUTDOWN
|
||||
|
|
@ -364,15 +337,6 @@
|
|||
#define JL_HAS_AUDIO_CRITICAL_ENTER
|
||||
#define JL_HAS_AUDIO_CRITICAL_EXIT
|
||||
#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)
|
||||
#define JL_HAS_INIT
|
||||
#define JL_HAS_SHUTDOWN
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
|
|||
SERIAL_SRC := $(EXAMPLES)/serial/serial.c
|
||||
UBER_SRC := $(EXAMPLES)/uber/uber.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
|
||||
|
||||
|
|
@ -101,7 +102,7 @@ all x68000: x68000-lib x68000-examples
|
|||
|
||||
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
|
||||
@mkdir -p $(dir $@)
|
||||
|
|
@ -159,6 +160,12 @@ $(BINDIR)/AUDIO.X: $(AUDIO_SRC) $(LIB) $(LIBXMP_AR)
|
|||
@mkdir -p $(dir $@)
|
||||
$(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).
|
||||
x68000-verify-serial: $(BINDIR)/SERIAL.X
|
||||
$(REPO_DIR)/scripts/verify-x68000-serial.sh
|
||||
|
|
|
|||
23
scripts/check-input.sh
Executable file
23
scripts/check-input.sh
Executable 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
120
scripts/verify-amiga-input.sh
Executable 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
138
scripts/verify-atarist-input.sh
Executable 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
116
scripts/verify-dos-input.sh
Executable 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
126
scripts/verify-iigs-input.sh
Executable 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
120
scripts/verify-x68000-input.sh
Executable 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
|
||||
|
|
@ -16,6 +16,13 @@
|
|||
// for MOUSEMOVE events to fire continuously rather than only on button
|
||||
// transitions. WFLG_RMBTRAP keeps the right mouse button from opening
|
||||
// Intuition's screen menu so right-clicks come to us as MENUDOWN.
|
||||
//
|
||||
// Typed characters: RAWKEY press events are also run through
|
||||
// keymap.library's MapRawKey with the message qualifier and dead-key
|
||||
// context, which applies the user's keymap (shift, caps, national
|
||||
// layouts, dead keys) and yields translated bytes for the core typed-
|
||||
// character queue. Intuition delivers key repeats as further RAWKEY
|
||||
// events, so OS auto-repeat produces repeated characters for free.
|
||||
|
||||
#include <string.h>
|
||||
|
||||
|
|
@ -28,6 +35,7 @@
|
|||
|
||||
#include <proto/exec.h>
|
||||
#include <proto/intuition.h>
|
||||
#include <proto/keymap.h>
|
||||
#include <proto/lowlevel.h>
|
||||
|
||||
#include "port.h"
|
||||
|
|
@ -39,6 +47,24 @@
|
|||
#define AMIGA_KEY_RELEASE_BIT 0x80
|
||||
#define AMIGA_KEY_CODE_MASK 0x7F
|
||||
|
||||
// MapRawKey output buffer. A single keystroke maps to at most a few
|
||||
// bytes (dead-key compositions); 8 is comfortably above any keymap's
|
||||
// output.
|
||||
#define MAP_RAW_KEY_BUF_SIZE 8
|
||||
|
||||
// Keymaps encode arrows / F-keys / Help as ANSI control sequences
|
||||
// beginning with CSI (0x9B). The CSI byte itself would be filtered,
|
||||
// but the sequence's TAIL bytes are printable ('A' for cursor up,
|
||||
// "0~" for F1, ...) and would leak into the typed-character queue as
|
||||
// stray text -- so a mapping that starts with CSI is discarded whole.
|
||||
#define AMIGA_CSI 0x9B
|
||||
|
||||
// keymap.library base referenced by the MapRawKey inline stub. Defined
|
||||
// here and opened explicitly (not left to libnix auto-open) so a
|
||||
// system without the library degrades to "no typed characters" instead
|
||||
// of failing at startup.
|
||||
struct Library *KeymapBase = NULL;
|
||||
|
||||
// ----- External from hal.c -----
|
||||
|
||||
extern struct Screen *gScreen;
|
||||
|
|
@ -144,6 +170,10 @@ static struct Library *gLowLevelBase = NULL;
|
|||
|
||||
static void drainMessages(void) {
|
||||
struct IntuiMessage *msg;
|
||||
struct InputEvent ie;
|
||||
UBYTE mapBuf[MAP_RAW_KEY_BUF_SIZE];
|
||||
WORD mapCount;
|
||||
WORD mapIdx;
|
||||
UWORD msgClass;
|
||||
UWORD msgCode;
|
||||
int16_t msgMouseX;
|
||||
|
|
@ -160,6 +190,29 @@ static void drainMessages(void) {
|
|||
msgCode = msg->Code;
|
||||
msgMouseX = (int16_t)msg->MouseX;
|
||||
msgMouseY = (int16_t)msg->MouseY;
|
||||
|
||||
// Typed-character translation must run BEFORE ReplyMsg: the
|
||||
// qualifier and the dead-key context behind IAddress belong
|
||||
// to the message and may be recycled once replied. Release
|
||||
// events (bit 7) type nothing.
|
||||
mapCount = 0;
|
||||
if (msgClass == IDCMP_RAWKEY &&
|
||||
(msgCode & AMIGA_KEY_RELEASE_BIT) == 0 &&
|
||||
KeymapBase != NULL) {
|
||||
ie.ie_NextEvent = NULL;
|
||||
ie.ie_Class = IECLASS_RAWKEY;
|
||||
ie.ie_SubClass = 0;
|
||||
ie.ie_Code = msgCode;
|
||||
ie.ie_Qualifier = msg->Qualifier;
|
||||
// Dead-key context: IAddress points at the previous-code
|
||||
// storage MapRawKey wants in ie_EventAddress (RKM recipe).
|
||||
ie.ie_EventAddress = (APTR)*((ULONG *)msg->IAddress);
|
||||
mapCount = MapRawKey(&ie, (STRPTR)mapBuf, MAP_RAW_KEY_BUF_SIZE, NULL);
|
||||
if (mapCount > 0 && mapBuf[0] == AMIGA_CSI) {
|
||||
mapCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ReplyMsg((struct Message *)msg);
|
||||
|
||||
switch (msgClass) {
|
||||
|
|
@ -170,6 +223,11 @@ static void drainMessages(void) {
|
|||
if (key != KEY_NONE) {
|
||||
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;
|
||||
case IDCMP_MOUSEMOVE:
|
||||
gMouseX = msgMouseX;
|
||||
|
|
@ -288,6 +346,11 @@ void jlpInputInit(void) {
|
|||
// Kickstart, or stripped AROS), the joystick API silently reports
|
||||
// disconnected sticks rather than failing init.
|
||||
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) {
|
||||
if (KeymapBase != NULL) {
|
||||
CloseLibrary(KeymapBase);
|
||||
KeymapBase = NULL;
|
||||
}
|
||||
if (gLowLevelBase != NULL) {
|
||||
CloseLibrary(gLowLevelBase);
|
||||
gLowLevelBase = NULL;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,17 @@
|
|||
// jlKeyPressed edge detection requires that public gKeyState only
|
||||
// advance during jlpInputPoll, never at interrupt time -- jlInputPoll
|
||||
// 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>
|
||||
|
||||
|
|
@ -37,6 +48,26 @@
|
|||
#define SCAN_BREAK_BIT 0x80
|
||||
#define SCAN_CODE_MASK 0x7F
|
||||
#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_ABS_MOUSE 0xF7 // + 5 bytes
|
||||
|
|
@ -74,9 +105,10 @@
|
|||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static long ikbdHandler(void);
|
||||
static long patchIkbdVector(void);
|
||||
static long restoreIkbdVector(void);
|
||||
static long ikbdHandler(void);
|
||||
static bool scanIsCursorCluster(uint8_t code);
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
|
|
@ -169,15 +201,36 @@ static int16_t gMouseAbsY = SURFACE_HEIGHT / 2;
|
|||
// jlpInputPoll can simply read the latest value.
|
||||
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 -----
|
||||
|
||||
// Runs in MFP ACIA interrupt context. Reads one byte from the ACIA,
|
||||
// dispatches to either keyboard handling, mouse-packet capture, or
|
||||
// "discard remaining N bytes" for packets we do not yet care about.
|
||||
static long ikbdHandler(void) {
|
||||
const uint8_t *tab;
|
||||
uint8_t byte;
|
||||
uint8_t code;
|
||||
uint8_t key;
|
||||
uint8_t ch;
|
||||
uint8_t next;
|
||||
bool isBreak;
|
||||
|
||||
byte = *ST_ACIA_DATA;
|
||||
|
|
@ -264,6 +317,33 @@ static long ikbdHandler(void) {
|
|||
if (key != KEY_NONE) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -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) -----
|
||||
|
||||
void jlpJoystickReset(jlJoystickE js) {
|
||||
|
|
@ -290,11 +377,29 @@ void jlpJoystickReset(jlJoystickE js) {
|
|||
|
||||
|
||||
void jlpInputInit(void) {
|
||||
_KEYTAB *keyTab;
|
||||
|
||||
memset(gKeyState, 0, sizeof(gKeyState));
|
||||
memset(gKeyPrev, 0, sizeof(gKeyPrev));
|
||||
memset((void *)gIsrState, 0, sizeof(gIsrState));
|
||||
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;
|
||||
gMouseAbsY = SURFACE_HEIGHT / 2;
|
||||
gMouseX = gMouseAbsX;
|
||||
|
|
@ -327,10 +432,21 @@ void jlpInputPoll(void) {
|
|||
int32_t newY;
|
||||
uint8_t btn;
|
||||
uint8_t joy;
|
||||
uint8_t tail;
|
||||
uint16_t i;
|
||||
|
||||
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.
|
||||
dx = gIsrMouseDx;
|
||||
dy = gIsrMouseDy;
|
||||
|
|
|
|||
|
|
@ -87,8 +87,16 @@ void jlpInputShutdown(void) {
|
|||
// 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
|
||||
// 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) {
|
||||
// TODO: read your keyboard and set gKeyState[...] = true for held keys.
|
||||
// TODO: feed translated characters to jlInputCharPush(ch).
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
#include "joey/core.h"
|
||||
#include "codegenArenaInternal.h"
|
||||
#include "inputInternal.h"
|
||||
#include "port.h"
|
||||
#include "spriteInternal.h"
|
||||
#include "surfaceInternal.h"
|
||||
|
|
@ -127,6 +128,10 @@ bool jlInit(const jlConfigT *config) {
|
|||
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();
|
||||
|
||||
gInitialized = true;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@
|
|||
// Mouse position and joystick axes are plain current state with no
|
||||
// edge predicates -- games that want deltas track the previous values
|
||||
// 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>
|
||||
|
||||
|
|
@ -32,6 +38,14 @@ uint8_t gJoyButtonState[JOYSTICK_COUNT][JOY_BUTTON_COUNT];
|
|||
uint8_t gJoyButtonPrev [JOYSTICK_COUNT][JOY_BUTTON_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
|
||||
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];
|
||||
#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) {
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
// One asm pass for: TTL decrement + key snapshot + mouse/joy
|
||||
|
|
@ -77,6 +133,11 @@ void jlWaitForAnyKey(void) {
|
|||
jlInputPoll();
|
||||
for (i = (int16_t)(KEY_NONE + 1); i < (int16_t)KEY_COUNT; 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,26 @@ extern uint8_t gJoyButtonPrev [JOYSTICK_COUNT][JOY_BUTTON_COUNT];
|
|||
// with analog paddles (IIgs); ignored on digital-stick platforms.
|
||||
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-
|
||||
// disconnect state and arm a fresh center capture -- is declared with the
|
||||
// other platform-only services in src/core/port.h.)
|
||||
|
|
|
|||
246
src/dos/input.c
246
src/dos/input.c
|
|
@ -5,6 +5,17 @@
|
|||
// gIsrState buffer, and sends EOI to the PIC. jlpInputPoll snapshots
|
||||
// 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.
|
||||
// jlInputPoll does memcpy(gKeyPrev, gKeyState) *before* jlpInputPoll
|
||||
// runs, so whatever gKeyState holds at that moment becomes gKeyPrev.
|
||||
|
|
@ -29,6 +40,7 @@
|
|||
#include <go32.h>
|
||||
#include <pc.h>
|
||||
#include <string.h>
|
||||
#include <sys/farptr.h>
|
||||
|
||||
#include "port.h"
|
||||
#include "inputInternal.h"
|
||||
|
|
@ -45,6 +57,47 @@
|
|||
#define SCAN_TABLE_SIZE 128
|
||||
#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.
|
||||
#define MOUSE_INT 0x33
|
||||
#define MOUSE_FN_RESET 0x0000
|
||||
|
|
@ -149,6 +202,59 @@ static const uint8_t gScanToKey[SCAN_TABLE_SIZE] = {
|
|||
[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 gNewHandler;
|
||||
static bool gHooked = false;
|
||||
|
|
@ -156,6 +262,22 @@ static bool gHooked = false;
|
|||
// src/core/inputInternal.h for the full rationale.
|
||||
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 gJoystickPresent = false;
|
||||
|
||||
|
|
@ -165,17 +287,100 @@ static void keyboardIsr(void) {
|
|||
uint8_t scan;
|
||||
uint8_t code;
|
||||
uint8_t key;
|
||||
uint8_t extended;
|
||||
uint8_t ch;
|
||||
uint8_t next;
|
||||
bool isBreak;
|
||||
bool useShift;
|
||||
|
||||
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;
|
||||
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];
|
||||
if (key != KEY_NONE) {
|
||||
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);
|
||||
|
|
@ -330,13 +535,39 @@ void jlpJoystickReset(jlJoystickE js) {
|
|||
|
||||
|
||||
void jlpInputInit(void) {
|
||||
uint8_t kbFlags;
|
||||
|
||||
memset(gKeyState, 0, sizeof(gKeyState));
|
||||
memset(gKeyPrev, 0, sizeof(gKeyPrev));
|
||||
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_data((void *)gScanToKey, sizeof(gScanToKey));
|
||||
_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);
|
||||
|
||||
|
|
@ -354,9 +585,22 @@ void jlpInputInit(void) {
|
|||
|
||||
|
||||
void jlpInputPoll(void) {
|
||||
uint8_t tail;
|
||||
|
||||
disable();
|
||||
memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState));
|
||||
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();
|
||||
joystickPoll();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
// uninitialized corrupted KEGS' emulation state. Softswitches have no
|
||||
// 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.
|
||||
// Holding multiple non-modifier keys simultaneously cannot be observed;
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@
|
|||
// _iocs_bitsns key-group numbers that carry the keys jlKeyE cares about.
|
||||
#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.
|
||||
#define X68K_MFP_GPIP ((volatile uint8_t *)0xE88001L)
|
||||
#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
|
||||
// ST, which has to replace the TOS ikbdsys vector and decode IKBD packets.
|
||||
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];
|
||||
uint16_t group;
|
||||
uint16_t key;
|
||||
uint16_t drain;
|
||||
uint8_t scan;
|
||||
|
||||
for (group = 0; group < X68K_KEYGROUP_COUNT; group++) {
|
||||
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++) {
|
||||
scan = kScanForKey[key];
|
||||
if (scan == 0u) {
|
||||
continue;
|
||||
}
|
||||
group = (uint16_t)(scan >> 3);
|
||||
if (group < X68K_KEYGROUP_COUNT &&
|
||||
(groups[group] & (uint8_t)(1u << (scan & 7u))) != 0u) {
|
||||
gKeyState[key] = 1u;
|
||||
if (group < X68K_KEYGROUP_COUNT) {
|
||||
gKeyState[key] = (uint8_t)((groups[group] >> (scan & 7u)) & 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
211
tests/host/inputHost.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue