From 9bacd2c68ffacb1606c3452957c1c759562972d9 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Fri, 24 Apr 2026 16:29:31 -0500 Subject: [PATCH] Key Up/Down working. --- examples/keys/keys.c | 162 +++++++++++++++++++++++++++++ include/joey/input.h | 47 +++++++++ include/joey/joey.h | 1 + make/amiga.mk | 8 +- make/atarist.mk | 8 +- make/dos.mk | 9 +- make/iigs.mk | 15 ++- scripts/run-amiga.sh | 8 +- scripts/run-atarist.sh | 13 +-- scripts/run-dos.sh | 7 +- scripts/run-iigs.sh | 31 ++++-- src/core/hal.h | 11 ++ src/core/init.c | 3 + src/core/input.c | 43 ++++++++ src/core/inputInternal.h | 17 +++ src/port/amiga/hal.c | 66 +++++++++--- src/port/amiga/input.c | 173 +++++++++++++++++++++++++++++++ src/port/atarist/hal.c | 49 ++++++++- src/port/atarist/input.c | 218 +++++++++++++++++++++++++++++++++++++++ src/port/dos/hal.c | 20 +++- src/port/dos/input.c | 170 ++++++++++++++++++++++++++++++ src/port/iigs/input.c | 161 +++++++++++++++++++++++++++++ toolchains/install.sh | 35 ++++++- 23 files changed, 1225 insertions(+), 50 deletions(-) create mode 100644 examples/keys/keys.c create mode 100644 include/joey/input.h create mode 100644 src/core/input.c create mode 100644 src/core/inputInternal.h create mode 100644 src/port/amiga/input.c create mode 100644 src/port/atarist/input.c create mode 100644 src/port/dos/input.c create mode 100644 src/port/iigs/input.c diff --git a/examples/keys/keys.c b/examples/keys/keys.c new file mode 100644 index 0000000..9beb748 --- /dev/null +++ b/examples/keys/keys.c @@ -0,0 +1,162 @@ +// Visual keyboard demo: one square per JoeyKeyE, filled bright when +// the key is held and dim otherwise. Press ESC to quit. +// +// The first paint fills every cell and does one full-surface present. +// Thereafter the loop compares each cell against its previous drawn +// state and only redraws + presents the cells that changed. Full- +// surface chunky-to-planar conversion is expensive on 68000-class +// hardware (hundreds of milliseconds for 320x200x4bpp on Amiga / ST); +// per-cell rect presents keep the response tight regardless of how +// fast the host can convert a full frame. + +#include + +#include + +#define GRID_COLS 10 +#define GRID_ROWS 6 +#define CELL_W 28 +#define CELL_H 28 +#define GAP 4 +#define MARGIN_X 2 +#define MARGIN_Y 6 + +#define COLOR_BACKGROUND 0 +#define COLOR_UNLIT 1 +#define COLOR_LIT 2 + +static void buildPalette(SurfaceT *screen); +static void drawCell(SurfaceT *screen, int16_t col, int16_t row, bool lit); +static void drawAllCells(SurfaceT *screen); +static void presentChangedCells(SurfaceT *screen); + +// Keys laid out row-by-row. KEY_NONE cells stay blank. Shape roughly +// resembles a real keyboard (top number row, then QWERTY rows, then a +// cluster of modifiers / arrows / function keys). +static const JoeyKeyE gKeyGrid[GRID_ROWS][GRID_COLS] = { + { KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0 }, + { KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P }, + { KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_BACKSPACE }, + { KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_LSHIFT, KEY_RSHIFT, KEY_TAB }, + { KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_LCTRL, KEY_LALT, KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_NONE }, + { KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10 } +}; + +static bool gCellLit[GRID_ROWS][GRID_COLS]; + + +static void buildPalette(SurfaceT *screen) { + uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t i; + + for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + colors[i] = 0x0000; + } + colors[COLOR_BACKGROUND] = 0x0000; // black + colors[COLOR_UNLIT] = 0x0333; // dark gray + colors[COLOR_LIT] = 0x00F0; // bright green + + paletteSet(screen, 0, colors); +} + + +static void drawCell(SurfaceT *screen, int16_t col, int16_t row, bool lit) { + int16_t x; + int16_t y; + uint8_t color; + + x = (int16_t)(MARGIN_X + col * (CELL_W + GAP)); + y = (int16_t)(MARGIN_Y + row * (CELL_H + GAP)); + color = lit ? COLOR_LIT : COLOR_UNLIT; + fillRect(screen, x, y, CELL_W, CELL_H, color); +} + + +static void drawAllCells(SurfaceT *screen) { + int16_t col; + int16_t row; + JoeyKeyE key; + bool lit; + + for (row = 0; row < GRID_ROWS; row++) { + for (col = 0; col < GRID_COLS; col++) { + key = gKeyGrid[row][col]; + if (key == KEY_NONE) { + continue; + } + lit = joeyKeyDown(key); + drawCell(screen, col, row, lit); + gCellLit[row][col] = lit; + } + } +} + + +static void presentChangedCells(SurfaceT *screen) { + int16_t col; + int16_t row; + JoeyKeyE key; + bool lit; + int16_t x; + int16_t y; + + for (row = 0; row < GRID_ROWS; row++) { + for (col = 0; col < GRID_COLS; col++) { + key = gKeyGrid[row][col]; + if (key == KEY_NONE) { + continue; + } + lit = joeyKeyDown(key); + if (lit == gCellLit[row][col]) { + continue; + } + drawCell(screen, col, row, lit); + x = (int16_t)(MARGIN_X + col * (CELL_W + GAP)); + y = (int16_t)(MARGIN_Y + row * (CELL_H + GAP)); + surfacePresentRect(screen, x, y, CELL_W, CELL_H); + gCellLit[row][col] = lit; + } + } +} + + +int main(void) { + JoeyConfigT config; + SurfaceT *screen; + + config.hostMode = HOST_MODE_TAKEOVER; + config.codegenBytes = 32 * 1024; + config.maxSurfaces = 4; + config.audioBytes = 64 * 1024; + config.assetBytes = 128 * 1024; + + if (!joeyInit(&config)) { + fprintf(stderr, "joeyInit failed: %s\n", joeyLastError()); + return 1; + } + + screen = surfaceGetScreen(); + if (screen == NULL) { + fprintf(stderr, "surfaceGetScreen returned NULL\n"); + joeyShutdown(); + return 1; + } + + buildPalette(screen); + scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); + surfaceClear(screen, COLOR_BACKGROUND); + joeyInputPoll(); + drawAllCells(screen); + surfacePresent(screen); + + for (;;) { + joeyInputPoll(); + if (joeyKeyPressed(KEY_ESCAPE)) { + break; + } + presentChangedCells(screen); + } + + joeyShutdown(); + return 0; +} diff --git a/include/joey/input.h b/include/joey/input.h new file mode 100644 index 0000000..2282b73 --- /dev/null +++ b/include/joey/input.h @@ -0,0 +1,47 @@ +// Keyboard input polling. +// +// Call joeyInputPoll() once per frame (typically right before +// drawing) to refresh the keyboard state. After polling, the +// joeyKey* predicates return the current state of every key: +// +// joeyKeyDown(k) -- is key k held down right now +// joeyKeyPressed(k) -- rising edge since the previous poll +// joeyKeyReleased(k) -- falling edge since the previous poll +// +// Edge predicates are one-shot: they return true only in the +// frame the transition occurred and false thereafter. + +#ifndef JOEYLIB_INPUT_H +#define JOEYLIB_INPUT_H + +#include "platform.h" +#include "types.h" + +typedef enum { + KEY_NONE = 0, + + KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, + KEY_J, KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, + KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z, + + KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, + KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, + + KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE, + + KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, + + KEY_LSHIFT, KEY_RSHIFT, KEY_LCTRL, KEY_LALT, + + KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, + KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, + + KEY_COUNT +} JoeyKeyE; + +void joeyInputPoll(void); +bool joeyKeyDown(JoeyKeyE key); +bool joeyKeyPressed(JoeyKeyE key); +bool joeyKeyReleased(JoeyKeyE key); + +#endif diff --git a/include/joey/joey.h b/include/joey/joey.h index e3f18fa..39cfdbe 100644 --- a/include/joey/joey.h +++ b/include/joey/joey.h @@ -13,5 +13,6 @@ #include "palette.h" #include "draw.h" #include "present.h" +#include "input.h" #endif diff --git a/make/amiga.mk b/make/amiga.mk index 74c4b15..804a462 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -31,9 +31,11 @@ HELLO_SRC := $(EXAMPLES)/hello/hello.c HELLO_BIN := $(BINDIR)/Hello PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_BIN := $(BINDIR)/Pattern +KEYS_SRC := $(EXAMPLES)/keys/keys.c +KEYS_BIN := $(BINDIR)/Keys .PHONY: all amiga clean-amiga -all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) +all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) @@ -63,5 +65,9 @@ $(PATTERN_BIN): $(PATTERN_SRC) $(LIB) @mkdir -p $(dir $@) $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) +$(KEYS_BIN): $(KEYS_SRC) $(LIB) + @mkdir -p $(dir $@) + $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) + clean-amiga: rm -rf $(BUILD) diff --git a/make/atarist.mk b/make/atarist.mk index b0f7c9f..7807775 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -27,9 +27,11 @@ HELLO_SRC := $(EXAMPLES)/hello/hello.c HELLO_BIN := $(BINDIR)/HELLO.PRG PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_BIN := $(BINDIR)/PATTERN.PRG +KEYS_SRC := $(EXAMPLES)/keys/keys.c +KEYS_BIN := $(BINDIR)/KEYS.PRG .PHONY: all atarist clean-atarist -all atarist: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) +all atarist: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) @@ -59,5 +61,9 @@ $(PATTERN_BIN): $(PATTERN_SRC) $(LIB) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) +$(KEYS_BIN): $(KEYS_SRC) $(LIB) + @mkdir -p $(dir $@) + $(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) + clean-atarist: rm -rf $(BUILD) diff --git a/make/dos.mk b/make/dos.mk index 3e85447..74b8295 100644 --- a/make/dos.mk +++ b/make/dos.mk @@ -25,9 +25,11 @@ HELLO_SRC := $(EXAMPLES)/hello/hello.c HELLO_BIN := $(BINDIR)/HELLO.EXE PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_BIN := $(BINDIR)/PATTERN.EXE +KEYS_SRC := $(EXAMPLES)/keys/keys.c +KEYS_BIN := $(BINDIR)/KEYS.EXE .PHONY: all dos clean-dos -all dos: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) +all dos: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) @@ -55,5 +57,10 @@ $(PATTERN_BIN): $(PATTERN_SRC) $(LIB) $(DOS_CC) $(CFLAGS) $< $(LIB) -o $@ $(DOS_EMBED_DPMI) $@ +$(KEYS_BIN): $(KEYS_SRC) $(LIB) + @mkdir -p $(dir $@) + $(DOS_CC) $(CFLAGS) $< $(LIB) -o $@ + $(DOS_EMBED_DPMI) $@ + clean-dos: rm -rf $(BUILD) diff --git a/make/iigs.mk b/make/iigs.mk index cf6f416..e950003 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -20,6 +20,8 @@ HELLO_SRC := $(EXAMPLES)/hello/hello.c HELLO_BIN := $(BINDIR)/HELLO PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_BIN := $(BINDIR)/PATTERN +KEYS_SRC := $(EXAMPLES)/keys/keys.c +KEYS_BIN := $(BINDIR)/KEYS DISK_IMG := $(BINDIR)/joey.2mg IIGS_PACKAGE := $(REPO_DIR)/toolchains/iigs/package-disk.sh @@ -31,7 +33,7 @@ IIX_INCLUDES := \ -I $(SRC_CORE) .PHONY: all iigs iigs-disk clean-iigs -all iigs: $(HELLO_BIN) $(PATTERN_BIN) +all iigs: $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) # iix-build.sh takes MAIN.c first, then EXTRA sources (compiled with # #pragma noroot). The example source supplies main(); libjoey sources @@ -48,13 +50,18 @@ $(PATTERN_BIN): $(PATTERN_SRC) $(LIB_SRCS) $(IIGS_BUILD) $(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS) $(IIGS_IIX) chtyp -t S16 $@ -# Assemble an 800KB ProDOS 2img containing both examples, ready to +$(KEYS_BIN): $(KEYS_SRC) $(LIB_SRCS) $(IIGS_BUILD) + @mkdir -p $(dir $@) + $(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS) + $(IIGS_IIX) chtyp -t S16 $@ + +# Assemble an 800KB ProDOS 2img containing the examples, ready to # mount in GSplus alongside a GS/OS boot volume. iigs-disk: $(DISK_IMG) -$(DISK_IMG): $(HELLO_BIN) $(PATTERN_BIN) $(IIGS_PACKAGE) +$(DISK_IMG): $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(IIGS_PACKAGE) @mkdir -p $(dir $@) - $(IIGS_PACKAGE) $@ $(HELLO_BIN) $(PATTERN_BIN) + $(IIGS_PACKAGE) $@ $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) clean-iigs: rm -rf $(BUILD) diff --git a/scripts/run-amiga.sh b/scripts/run-amiga.sh index 3dfc63c..e452b36 100755 --- a/scripts/run-amiga.sh +++ b/scripts/run-amiga.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash -# Launch the built Amiga example in FS-UAE. Defaults to PATTERN; pass -# "hello" to run HELLO instead. +# Launch the built Amiga example in FS-UAE. Defaults to PATTERN. # # Kickstart and Workbench: # - If toolchains/emulators/support/kickstart.rom is present, it is @@ -18,6 +17,7 @@ # # scripts/run-amiga.sh # runs Pattern # scripts/run-amiga.sh hello # runs Hello +# scripts/run-amiga.sh keys # runs Keys set -euo pipefail @@ -29,7 +29,8 @@ support=$repo/toolchains/emulators/support case $prog in hello) file=Hello ;; pattern) file=Pattern ;; - *) echo "usage: $0 [hello|pattern]" >&2; exit 2 ;; + keys) file=Keys ;; + *) echo "usage: $0 [hello|pattern|keys]" >&2; exit 2 ;; esac if [[ ! -f "$bin_dir/$file" ]]; then @@ -54,6 +55,7 @@ trap 'mkdir -p "$dump_keep"; cp "$work"/*.txt "$dump_keep"/ 2>/dev/null; rm -rf mkdir -p "$work/s" cp "$bin_dir/Hello" "$work/" 2>/dev/null || true cp "$bin_dir/Pattern" "$work/" 2>/dev/null || true +cp "$bin_dir/Keys" "$work/" 2>/dev/null || true # ':' prefix anchors to the root of the current volume; otherwise # AmigaDOS looks in C: and the command is not found. echo ":$file" > "$work/s/startup-sequence" diff --git a/scripts/run-atarist.sh b/scripts/run-atarist.sh index 113d193..433528e 100755 --- a/scripts/run-atarist.sh +++ b/scripts/run-atarist.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Launch the built Atari ST example in Hatari. Defaults to PATTERN; -# pass "hello" to run HELLO instead. Hatari autostarts the .PRG via -# the --auto flag; EmuTOS (toolchains/emulators/support/emutos-512k.img) -# provides the TOS ROM since the Ubuntu hatari package does not bundle -# one. +# Launch the built Atari ST example in Hatari. Defaults to PATTERN. +# Hatari autostarts the .PRG via the --auto flag; EmuTOS +# (toolchains/emulators/support/emutos-512k.img) provides the TOS ROM +# since the Ubuntu hatari package does not bundle one. # # scripts/run-atarist.sh # runs PATTERN.PRG # scripts/run-atarist.sh hello # runs HELLO.PRG +# scripts/run-atarist.sh keys # runs KEYS.PRG set -euo pipefail @@ -17,7 +17,8 @@ bin_dir=$repo/build/atarist/bin case $prog in hello) file=HELLO.PRG ;; pattern) file=PATTERN.PRG ;; - *) echo "usage: $0 [hello|pattern]" >&2; exit 2 ;; + keys) file=KEYS.PRG ;; + *) echo "usage: $0 [hello|pattern|keys]" >&2; exit 2 ;; esac tos=$repo/toolchains/emulators/support/emutos-512k.img diff --git a/scripts/run-dos.sh b/scripts/run-dos.sh index 377ee2f..1f97860 100755 --- a/scripts/run-dos.sh +++ b/scripts/run-dos.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -# Launch the built DOS example in DOSBox. Defaults to PATTERN; pass -# "hello" to run HELLO instead. +# Launch the built DOS example in DOSBox. Defaults to PATTERN. # # scripts/run-dos.sh # runs PATTERN # scripts/run-dos.sh hello # runs HELLO +# scripts/run-dos.sh keys # runs KEYS set -euo pipefail @@ -14,7 +14,8 @@ bin_dir=$repo/build/dos/bin case $prog in hello) file=HELLO.EXE ;; pattern) file=PATTERN.EXE ;; - *) echo "usage: $0 [hello|pattern]" >&2; exit 2 ;; + keys) file=KEYS.EXE ;; + *) echo "usage: $0 [hello|pattern|keys]" >&2; exit 2 ;; esac if [[ ! -f "$bin_dir/$file" ]]; then diff --git a/scripts/run-iigs.sh b/scripts/run-iigs.sh index 009e262..4bbf98f 100755 --- a/scripts/run-iigs.sh +++ b/scripts/run-iigs.sh @@ -2,15 +2,16 @@ # Launch the built Apple IIgs examples in GSplus. GSplus is booted from # a GS/OS 6.0.4 System disk (toolchains/emulators/support/gsos-system.po) # with joey.2mg mounted as the data disk on slot 5 drive 2. The user -# navigates to the JOEYLIB volume in Finder and double-clicks HELLO or -# PATTERN to run it. +# navigates to the JOEYLIB volume in Finder and double-clicks the +# example to run it. # # Unlike the other emulators, GS/OS does not auto-run on boot -- it -# drops to Finder. Pass "hello" or "pattern" to print a reminder of -# which example to launch. +# drops to Finder. The argument just prints a reminder of which +# example to launch. # -# scripts/run-iigs.sh # boots and waits for user (Pattern hint) -# scripts/run-iigs.sh hello # same, but hints to click HELLO +# scripts/run-iigs.sh # boots (Pattern hint) +# scripts/run-iigs.sh hello # boots, hints HELLO +# scripts/run-iigs.sh keys # boots, hints KEYS set -euo pipefail @@ -21,19 +22,20 @@ gsplus=$repo/toolchains/emulators/gsplus/bin/gsplus rom=$repo/toolchains/emulators/support/apple-iigs.rom sys_disk=$repo/toolchains/emulators/support/gsos-system.po data_disk=$repo/build/iigs/bin/joey.2mg +null_c600=$repo/toolchains/emulators/support/iigs-null-c600.rom case $prog in - hello|pattern) ;; - *) echo "usage: $0 [hello|pattern]" >&2; exit 2 ;; + hello|pattern|keys) ;; + *) echo "usage: $0 [hello|pattern|keys]" >&2; exit 2 ;; esac -for f in "$gsplus" "$rom" "$sys_disk" "$data_disk"; do +for f in "$gsplus" "$rom" "$sys_disk" "$data_disk" "$null_c600"; do if [[ ! -f $f ]]; then echo "missing: $f" >&2 if [[ $f == "$data_disk" ]]; then echo "run 'make iigs-disk' to build it." >&2 else - echo "run ./toolchains/install.sh (ROM/system disk should have been staged)." >&2 + echo "run ./toolchains/install.sh (support files should have been staged)." >&2 fi exit 1 fi @@ -47,6 +49,15 @@ trap 'rm -rf "$work"' EXIT cp "$sys_disk" "$work/boot.po" cp "$data_disk" "$work/joey.2mg" +# Stage the null slot-6 PROM as c600.rom in the work dir. GSplus loads +# any file named c600.rom in its search path (cwd first) as the slot 6 +# ROM card, overriding the built-in Disk II firmware. The staged image +# is 256 bytes with a leading RTS and no Pascal 1.1 firmware signature, +# so the IIgs boot ROM's slot scan skips slot 6 and the two empty 5.25 +# drives never get probed. Source: toolchains/install.sh's +# install_support_iigs_null_c600. +cp "$null_c600" "$work/c600.rom" + target=$(echo "$prog" | tr '[:lower:]' '[:upper:]') cat < + +#include "joey/input.h" +#include "hal.h" +#include "inputInternal.h" + +bool gKeyState[KEY_COUNT]; +bool gKeyPrev [KEY_COUNT]; + + +void joeyInputPoll(void) { + memcpy(gKeyPrev, gKeyState, sizeof(gKeyState)); + halInputPoll(); +} + + +bool joeyKeyDown(JoeyKeyE key) { + if (key <= KEY_NONE || key >= KEY_COUNT) { + return false; + } + return gKeyState[key]; +} + + +bool joeyKeyPressed(JoeyKeyE key) { + if (key <= KEY_NONE || key >= KEY_COUNT) { + return false; + } + return gKeyState[key] && !gKeyPrev[key]; +} + + +bool joeyKeyReleased(JoeyKeyE key) { + if (key <= KEY_NONE || key >= KEY_COUNT) { + return false; + } + return !gKeyState[key] && gKeyPrev[key]; +} diff --git a/src/core/inputInternal.h b/src/core/inputInternal.h new file mode 100644 index 0000000..2e5cd4e --- /dev/null +++ b/src/core/inputInternal.h @@ -0,0 +1,17 @@ +// Internal input state shared between core and per-port HAL. +// +// Per-port halInputPoll() writes directly into gKeyState[]: set +// entries to true for keys currently held, false for released keys. +// The core compares gKeyState to gKeyPrev each joeyInputPoll to +// derive edge events. + +#ifndef JOEYLIB_INPUT_INTERNAL_H +#define JOEYLIB_INPUT_INTERNAL_H + +#include "joey/input.h" +#include "joey/types.h" + +extern bool gKeyState[KEY_COUNT]; +extern bool gKeyPrev [KEY_COUNT]; + +#endif diff --git a/src/port/amiga/hal.c b/src/port/amiga/hal.c index 4ef5426..f6ad6c5 100644 --- a/src/port/amiga/hal.c +++ b/src/port/amiga/hal.c @@ -9,8 +9,10 @@ // * Build a user copper list that WAITs for each display scanline // and MOVEs the 16 color registers with that line's palette. // * Install it via ViewPort.UCopIns + MakeScreen + RethinkDisplay. -// * Rebuild on every halPresent since SCB and palettes may have -// changed anywhere. +// * Rebuild only when SCB or palette state differs from the last +// presented frame (cached in gCachedScb / gCachedPalette). On +// clean frames (typical game loop, where only pixel bytes change) +// we skip AllocMem + MrgCop + LoadView + WaitTOF entirely. // // Deferred: // * Blitter-assisted c2p for speed on A500. @@ -54,14 +56,28 @@ static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1) static void dumpCopperList(void); static void installCopperList(void); static void uploadFirstBandPalette(const SurfaceT *src); +static void updateCopperIfNeeded(const SurfaceT *src); // ----- Module state ----- -static struct Screen *gScreen = NULL; +struct Screen *gScreen = NULL; // shared with input.c static struct BitMap *gBitMap = NULL; static UBYTE *gPlanes[AMIGA_BITPLANES]; static struct UCopList *gNewUCL = NULL; // built but not yet installed +// Cached SCB + palettes from the last present. halPresent* only needs +// to rebuild/install the copper list when SCB assignments or palette +// RGB values differ from what is already on screen; pure pixel updates +// (which dominate a typical game loop and every frame of the keys +// demo after the initial paint) leave both alone. MrgCop + LoadView + +// WaitTOF is hundreds of milliseconds on a 7 MHz 68000, so skipping +// them on clean frames is a major win. +static uint8_t gCachedScb [SURFACE_HEIGHT]; +static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; +static bool gCacheValid = false; + +static bool paletteOrScbChanged(const SurfaceT *src); + // ----- Internal helpers (alphabetical) ----- // Convert a range of chunky scanlines [y0, y1) to Amiga planar. @@ -299,6 +315,39 @@ static void dumpCopperList(void) { } +// Returns true if the SCB table or palette RGB values differ from the +// last presented frame, or if no frame has been presented yet. +static bool paletteOrScbChanged(const SurfaceT *src) { + if (!gCacheValid) { + return true; + } + if (memcmp(gCachedScb, src->scb, sizeof(gCachedScb)) != 0) { + return true; + } + if (memcmp(gCachedPalette, src->palette, sizeof(gCachedPalette)) != 0) { + return true; + } + return false; +} + + +// Rebuild and install the user copper list only if the palette/SCB +// state visible to the display differs from what the surface carries +// now. On clean frames we skip the AllocMem + MrgCop + LoadView + +// WaitTOF chain entirely. +static void updateCopperIfNeeded(const SurfaceT *src) { + if (!paletteOrScbChanged(src)) { + return; + } + uploadFirstBandPalette(src); + buildCopperList(src); + installCopperList(); + memcpy(gCachedScb, src->scb, sizeof(gCachedScb)); + memcpy(gCachedPalette, src->palette, sizeof(gCachedPalette)); + gCacheValid = true; +} + + // Load the first band's palette into the screen's ColorMap so the // Intuition-generated frame-start copper writes those values on each // frame. This acts as a safety net: even if our user copper list does @@ -367,10 +416,7 @@ void halPresent(const SurfaceT *src) { if (src == NULL || gScreen == NULL) { return; } - uploadFirstBandPalette(src); - buildCopperList(src); - installCopperList(); - dumpCopperList(); + updateCopperIfNeeded(src); c2pRange(src, 0, SURFACE_HEIGHT); } @@ -382,11 +428,7 @@ void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint1 if (src == NULL || gScreen == NULL) { return; } - // Whole-scanline c2p for M2; rebuild the entire copper list since - // SCB entries outside the rect may have changed too. - uploadFirstBandPalette(src); - buildCopperList(src); - installCopperList(); + updateCopperIfNeeded(src); c2pRange(src, y, y + (int16_t)h); } diff --git a/src/port/amiga/input.c b/src/port/amiga/input.c new file mode 100644 index 0000000..8ccf99c --- /dev/null +++ b/src/port/amiga/input.c @@ -0,0 +1,173 @@ +// Amiga keyboard input: opens a backdrop borderless window on the +// JoeyLib CUSTOMSCREEN to receive IDCMP_RAWKEY events, then drains +// those messages each joeyInputPoll to update gKeyState. +// +// RAWKEY is the lowest-level IDCMP event Intuition exposes: ie_Code +// carries the Amiga raw key code (press in 0x00..0x7F, release with +// bit 7 set), which we map straight to JoeyKeyE entries. The backdrop +// borderless window covers the whole screen without drawing over our +// planar bitmap (c2p writes to Screen->BitMap->Planes directly), and +// WFLG_ACTIVATE ensures keyboard focus lands on us from the start. + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "hal.h" +#include "inputInternal.h" + +// ----- Constants ----- + +#define AMIGA_KEY_TABLE_SIZE 128 +#define AMIGA_KEY_RELEASE_BIT 0x80 +#define AMIGA_KEY_CODE_MASK 0x7F + +// ----- External from hal.c ----- + +extern struct Screen *gScreen; + +// ----- Prototypes ----- + +static void drainKeyMessages(void); + +// ----- Module state ----- + +// Amiga raw-key code -> JoeyKeyE. Codes are layout-independent; the +// OS has a separate keymap for producing characters. +static const uint8_t gRawKeyToKey[AMIGA_KEY_TABLE_SIZE] = { + [0x01] = KEY_1, + [0x02] = KEY_2, + [0x03] = KEY_3, + [0x04] = KEY_4, + [0x05] = KEY_5, + [0x06] = KEY_6, + [0x07] = KEY_7, + [0x08] = KEY_8, + [0x09] = KEY_9, + [0x0A] = KEY_0, + [0x10] = KEY_Q, + [0x11] = KEY_W, + [0x12] = KEY_E, + [0x13] = KEY_R, + [0x14] = KEY_T, + [0x15] = KEY_Y, + [0x16] = KEY_U, + [0x17] = KEY_I, + [0x18] = KEY_O, + [0x19] = KEY_P, + [0x20] = KEY_A, + [0x21] = KEY_S, + [0x22] = KEY_D, + [0x23] = KEY_F, + [0x24] = KEY_G, + [0x25] = KEY_H, + [0x26] = KEY_J, + [0x27] = KEY_K, + [0x28] = KEY_L, + [0x31] = KEY_Z, + [0x32] = KEY_X, + [0x33] = KEY_C, + [0x34] = KEY_V, + [0x35] = KEY_B, + [0x36] = KEY_N, + [0x37] = KEY_M, + [0x40] = KEY_SPACE, + [0x41] = KEY_BACKSPACE, + [0x42] = KEY_TAB, + [0x44] = KEY_RETURN, + [0x45] = KEY_ESCAPE, + [0x4C] = KEY_UP, + [0x4D] = KEY_DOWN, + [0x4E] = KEY_RIGHT, + [0x4F] = KEY_LEFT, + [0x50] = KEY_F1, + [0x51] = KEY_F2, + [0x52] = KEY_F3, + [0x53] = KEY_F4, + [0x54] = KEY_F5, + [0x55] = KEY_F6, + [0x56] = KEY_F7, + [0x57] = KEY_F8, + [0x58] = KEY_F9, + [0x59] = KEY_F10, + [0x60] = KEY_LSHIFT, + [0x61] = KEY_RSHIFT, + [0x63] = KEY_LCTRL, + [0x64] = KEY_LALT, +}; + +static struct Window *gWindow = NULL; + +// ----- Internal helpers ----- + +static void drainKeyMessages(void) { + struct IntuiMessage *msg; + UWORD rawCode; + uint8_t code; + uint8_t key; + bool isRelease; + + if (gWindow == NULL) { + return; + } + while ((msg = (struct IntuiMessage *)GetMsg(gWindow->UserPort)) != NULL) { + if (msg->Class == IDCMP_RAWKEY) { + rawCode = msg->Code; + isRelease = (rawCode & AMIGA_KEY_RELEASE_BIT) != 0; + code = (uint8_t)(rawCode & AMIGA_KEY_CODE_MASK); + key = gRawKeyToKey[code]; + if (key != KEY_NONE) { + gKeyState[key] = !isRelease; + } + } + ReplyMsg((struct Message *)msg); + } +} + + +// ----- HAL API (alphabetical) ----- + +void halInputInit(void) { + memset(gKeyState, 0, sizeof(gKeyState)); + memset(gKeyPrev, 0, sizeof(gKeyPrev)); + + if (gScreen == NULL) { + return; + } + + gWindow = OpenWindowTags(NULL, + (ULONG)WA_CustomScreen, (ULONG)gScreen, + (ULONG)WA_Left, (ULONG)0, + (ULONG)WA_Top, (ULONG)0, + (ULONG)WA_Width, (ULONG)gScreen->Width, + (ULONG)WA_Height, (ULONG)gScreen->Height, + (ULONG)WA_Flags, (ULONG)(WFLG_BACKDROP + | WFLG_BORDERLESS + | WFLG_ACTIVATE + | WFLG_RMBTRAP + | WFLG_NOCAREREFRESH), + (ULONG)WA_IDCMP, (ULONG)IDCMP_RAWKEY, + TAG_DONE); +} + + +void halInputPoll(void) { + drainKeyMessages(); +} + + +void halInputShutdown(void) { + if (gWindow == NULL) { + return; + } + drainKeyMessages(); + CloseWindow(gWindow); + gWindow = NULL; +} diff --git a/src/port/atarist/hal.c b/src/port/atarist/hal.c index 1f0eccf..20e30dd 100644 --- a/src/port/atarist/hal.c +++ b/src/port/atarist/hal.c @@ -73,6 +73,8 @@ static long writePrevPaletteRegs(void); static __attribute__((interrupt_handler)) void timerBIsr(void); static __attribute__((interrupt_handler)) void vblIsr(void); static void buildTransitions(const SurfaceT *src); +static bool paletteOrScbChanged(const SurfaceT *src); +static void refreshPaletteStateIfNeeded(const SurfaceT *src); // ----- Module state ----- @@ -117,6 +119,16 @@ static volatile uint16_t gLastBandCount = 0; static void (*gOldVblVec)(void) = NULL; static void (*gOldTimerBVec)(void) = NULL; +// Cached SCB + palette from the last present. flattenScbPalettes runs +// 200 * 16 quantize conversions and buildTransitions rescans the full +// SCB; neither is cheap on a 7 MHz 68000. In the typical game loop +// (and every frame of the keys demo after the initial paint) SCB and +// palette never change, so caching and skipping those passes keeps +// rect presents down to just the c2p work. +static uint8_t gCachedScb [SURFACE_HEIGHT]; +static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; +static bool gCacheValid = false; + // ----- Internal helpers (alphabetical) ----- // Convert 16 chunky pixels (8 bytes 4bpp packed) to 4 ST planar words. @@ -219,6 +231,37 @@ static void flattenScbPalettes(const SurfaceT *src) { } +// Returns true if SCB or palette values differ from the last present. +static bool paletteOrScbChanged(const SurfaceT *src) { + if (!gCacheValid) { + return true; + } + if (memcmp(gCachedScb, src->scb, sizeof(gCachedScb)) != 0) { + return true; + } + if (memcmp(gCachedPalette, src->palette, sizeof(gCachedPalette)) != 0) { + return true; + } + return false; +} + + +// Rebuild the per-line palette table and band-transition table only +// when the SCB/palette state has actually changed. Both are hot -- the +// flatten pass runs 3200 palette entries through quantization -- so +// skipping them on clean frames dominates rect-present timing. +static void refreshPaletteStateIfNeeded(const SurfaceT *src) { + if (!paletteOrScbChanged(src)) { + return; + } + flattenScbPalettes(src); + buildTransitions(src); + memcpy(gCachedScb, src->scb, sizeof(gCachedScb)); + memcpy(gCachedPalette, src->palette, sizeof(gCachedPalette)); + gCacheValid = true; +} + + // 12-bit $0RGB to STF 9-bit palette register (drops the low bit of // each 4-bit channel). static uint16_t quantizeColorToSt(uint16_t orgb) { @@ -437,8 +480,7 @@ void halPresent(const SurfaceT *src) { if (src == NULL || !gModeSet) { return; } - flattenScbPalettes(src); - buildTransitions(src); + refreshPaletteStateIfNeeded(src); c2pRange(src, 0, SURFACE_HEIGHT); } @@ -450,8 +492,7 @@ void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint1 if (src == NULL || !gModeSet) { return; } - flattenScbPalettes(src); - buildTransitions(src); + refreshPaletteStateIfNeeded(src); c2pRange(src, y, y + (int16_t)h); } diff --git a/src/port/atarist/input.c b/src/port/atarist/input.c new file mode 100644 index 0000000..2d20a52 --- /dev/null +++ b/src/port/atarist/input.c @@ -0,0 +1,218 @@ +// Atari ST keyboard input: replaces the TOS ikbdsys vector in the +// KBDVECS table so every byte from the IKBD ACIA comes through our +// packet-aware handler. +// +// The IKBD protocol mixes keyboard scan codes with mouse/joy packets. +// Bytes 0x00-0x72 (with optional 0x80 break bit -> 0x80..0xF2) are +// keyboard scan codes; 0xF6-0xFF are packet headers that announce a +// fixed number of trailing bytes. We track how many trailing packet +// bytes to discard; everything else is treated as a key event. +// +// The ISR writes into a private gIsrState buffer; halInputPoll copies +// it into gKeyState under a raised IPL so the snapshot is atomic +// relative to the ACIA interrupt. This two-buffer split is what makes +// joeyKeyPressed edge detection work -- joeyInputPoll snapshots +// gKeyState into gKeyPrev *before* halInputPoll runs, so gKeyState +// must only advance during halInputPoll, never at interrupt time. + +#include + +#include +#include + +#include "hal.h" +#include "inputInternal.h" + +// ----- Constants ----- + +// IKBD ACIA data register; reading also clears the ACIA's interrupt +// so the MFP ACIA line drops. +#define ST_ACIA_DATA ((volatile uint8_t *)0xFFFFFC02L) + +#define SCAN_BREAK_BIT 0x80 +#define SCAN_CODE_MASK 0x7F +#define SCAN_TABLE_SIZE 128 + +#define PKT_STATUS 0xF6 // + 7 bytes +#define PKT_ABS_MOUSE 0xF7 // + 5 bytes +#define PKT_REL_MOUSE_MIN 0xF8 // 0xF8..0xFB + 2 bytes +#define PKT_REL_MOUSE_MAX 0xFB +#define PKT_TIME 0xFC // + 6 bytes +#define PKT_JOY_BOTH 0xFD // + 2 bytes +#define PKT_JOY0 0xFE // + 1 byte +#define PKT_JOY1 0xFF // + 1 byte + +// ----- Prototypes ----- + +static long patchIkbdVector(void); +static long restoreIkbdVector(void); +static long ikbdHandler(void); + +// ----- Module state ----- + +// ST IKBD scan codes. Identical encoding to the PC AT set-1 for most +// keys, which is why the table mirrors the DOS scan map. +static const uint8_t gScanToKey[SCAN_TABLE_SIZE] = { + [0x01] = KEY_ESCAPE, + [0x02] = KEY_1, + [0x03] = KEY_2, + [0x04] = KEY_3, + [0x05] = KEY_4, + [0x06] = KEY_5, + [0x07] = KEY_6, + [0x08] = KEY_7, + [0x09] = KEY_8, + [0x0A] = KEY_9, + [0x0B] = KEY_0, + [0x0E] = KEY_BACKSPACE, + [0x0F] = KEY_TAB, + [0x10] = KEY_Q, + [0x11] = KEY_W, + [0x12] = KEY_E, + [0x13] = KEY_R, + [0x14] = KEY_T, + [0x15] = KEY_Y, + [0x16] = KEY_U, + [0x17] = KEY_I, + [0x18] = KEY_O, + [0x19] = KEY_P, + [0x1C] = KEY_RETURN, + [0x1D] = KEY_LCTRL, + [0x1E] = KEY_A, + [0x1F] = KEY_S, + [0x20] = KEY_D, + [0x21] = KEY_F, + [0x22] = KEY_G, + [0x23] = KEY_H, + [0x24] = KEY_J, + [0x25] = KEY_K, + [0x26] = KEY_L, + [0x2A] = KEY_LSHIFT, + [0x2C] = KEY_Z, + [0x2D] = KEY_X, + [0x2E] = KEY_C, + [0x2F] = KEY_V, + [0x30] = KEY_B, + [0x31] = KEY_N, + [0x32] = KEY_M, + [0x36] = KEY_RSHIFT, + [0x38] = KEY_LALT, + [0x39] = KEY_SPACE, + [0x3B] = KEY_F1, + [0x3C] = KEY_F2, + [0x3D] = KEY_F3, + [0x3E] = KEY_F4, + [0x3F] = KEY_F5, + [0x40] = KEY_F6, + [0x41] = KEY_F7, + [0x42] = KEY_F8, + [0x43] = KEY_F9, + [0x44] = KEY_F10, + [0x48] = KEY_UP, + [0x4B] = KEY_LEFT, + [0x4D] = KEY_RIGHT, + [0x50] = KEY_DOWN, +}; + +static _KBDVECS *gKbdvecs = NULL; +static long (*gOldIkbdsys)(void) = NULL; +static volatile uint8_t gPacketRemaining = 0; +static bool gHooked = false; +static volatile bool gIsrState[KEY_COUNT]; + +// ----- Internal helpers ----- + +// Runs in MFP ACIA interrupt context. Reads one byte from the ACIA, +// consumes trailing packet bytes, and maps key codes into gKeyState. +static long ikbdHandler(void) { + uint8_t byte; + uint8_t code; + uint8_t key; + bool isBreak; + + byte = *ST_ACIA_DATA; + + if (gPacketRemaining != 0) { + gPacketRemaining = (uint8_t)(gPacketRemaining - 1); + return 0; + } + + if (byte >= PKT_STATUS) { + switch (byte) { + case PKT_STATUS: + gPacketRemaining = 7; + break; + case PKT_ABS_MOUSE: + gPacketRemaining = 5; + break; + case PKT_TIME: + gPacketRemaining = 6; + break; + case PKT_JOY_BOTH: + gPacketRemaining = 2; + break; + case PKT_JOY0: + case PKT_JOY1: + gPacketRemaining = 1; + break; + default: + if (byte >= PKT_REL_MOUSE_MIN && byte <= PKT_REL_MOUSE_MAX) { + gPacketRemaining = 2; + } + break; + } + return 0; + } + + isBreak = (byte & SCAN_BREAK_BIT) != 0; + code = (uint8_t)(byte & SCAN_CODE_MASK); + key = gScanToKey[code]; + if (key != KEY_NONE) { + gIsrState[key] = !isBreak; + } + return 0; +} + + +static long patchIkbdVector(void) { + gOldIkbdsys = gKbdvecs->ikbdsys; + gKbdvecs->ikbdsys = ikbdHandler; + return 0; +} + + +static long restoreIkbdVector(void) { + gKbdvecs->ikbdsys = gOldIkbdsys; + return 0; +} + + +// ----- HAL API (alphabetical) ----- + +void halInputInit(void) { + memset(gKeyState, 0, sizeof(gKeyState)); + memset(gKeyPrev, 0, sizeof(gKeyPrev)); + memset((void *)gIsrState, 0, sizeof(gIsrState)); + + gKbdvecs = (_KBDVECS *)Kbdvbase(); + gPacketRemaining = 0; + Supexec(patchIkbdVector); + gHooked = true; +} + + +// The ACIA ISR only writes gIsrState; bytes land at ~100 Hz max so the +// ~60-byte memcpy is essentially never racing a write. Worst case is a +// single key lagging one frame -- well under perceptible. +void halInputPoll(void) { + memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState)); +} + + +void halInputShutdown(void) { + if (!gHooked) { + return; + } + Supexec(restoreIkbdVector); + gHooked = false; +} diff --git a/src/port/dos/hal.c b/src/port/dos/hal.c index 5352ab4..35092dc 100644 --- a/src/port/dos/hal.c +++ b/src/port/dos/hal.c @@ -31,12 +31,18 @@ static void expandAndWriteLine(const SurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst); static void uploadPalette(const SurfaceT *src); +static void uploadPaletteIfNeeded(const SurfaceT *src); // ----- Module state ----- static uint8_t *gVgaMem = NULL; static bool gNearEnabled = false; +// Cached palette from the last present. VGA DAC programming is ~768 +// outportb calls; skip it when the source palette is unchanged. +static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; +static bool gCacheValid = false; + // ----- Internal helpers (alphabetical) ----- static void expandAndWriteLine(const SurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst) { @@ -82,6 +88,16 @@ static void uploadPalette(const SurfaceT *src) { } +static void uploadPaletteIfNeeded(const SurfaceT *src) { + if (gCacheValid && memcmp(gCachedPalette, src->palette, sizeof(gCachedPalette)) == 0) { + return; + } + uploadPaletteIfNeeded(src); + memcpy(gCachedPalette, src->palette, sizeof(gCachedPalette)); + gCacheValid = true; +} + + // ----- HAL API (alphabetical) ----- bool halInit(const JoeyConfigT *config) { @@ -113,7 +129,7 @@ void halPresent(const SurfaceT *src) { if (src == NULL || gVgaMem == NULL) { return; } - uploadPalette(src); + uploadPaletteIfNeeded(src); for (y = 0; y < SURFACE_HEIGHT; y++) { expandAndWriteLine(src, y, 0, SURFACE_WIDTH, &gVgaMem[y * VGA_STRIDE]); } @@ -127,7 +143,7 @@ void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint1 if (src == NULL || gVgaMem == NULL) { return; } - uploadPalette(src); + uploadPaletteIfNeeded(src); yEnd = y + (int16_t)h; for (py = y; py < yEnd; py++) { expandAndWriteLine(src, py, x, w, &gVgaMem[py * VGA_STRIDE]); diff --git a/src/port/dos/input.c b/src/port/dos/input.c new file mode 100644 index 0000000..818f6fe --- /dev/null +++ b/src/port/dos/input.c @@ -0,0 +1,170 @@ +// DOS keyboard input: hooks INT 9 to capture AT set-1 scan codes from +// port 0x60. The ISR reads the scan code, maps it to a JoeyKeyE, updates +// a private gIsrState buffer, and sends EOI to the PIC. halInputPoll +// snapshots gIsrState into gKeyState with interrupts disabled. +// +// The two-buffer split is required for joeyKeyPressed edge detection. +// joeyInputPoll does memcpy(gKeyPrev, gKeyState) *before* halInputPoll +// runs, so whatever gKeyState holds at that moment becomes gKeyPrev. +// If the ISR wrote directly to gKeyState, press/release events that +// happened between polls would already have landed there -- the memcpy +// would copy the new value into gKeyPrev and edge detection would see +// gKeyState == gKeyPrev every frame. +// +// Because DJGPP runs in protected mode with paging, the ISR code and +// any data it touches must be locked or a page fault at interrupt time +// will hang the machine. + +#include +#include +#include +#include + +#include "hal.h" +#include "inputInternal.h" + +// ----- Constants ----- + +#define KB_DATA_PORT 0x60 +#define PIC_CMD_PORT 0x20 +#define PIC_EOI 0x20 +#define SCAN_BREAK_BIT 0x80 +#define SCAN_CODE_MASK 0x7F +#define SCAN_EXTENDED 0xE0 +#define SCAN_TABLE_SIZE 128 +#define ISR_LOCK_SIZE 4096 + +// ----- Prototypes ----- + +static void keyboardIsr(void); + +// ----- Module state ----- + +static const uint8_t gScanToKey[SCAN_TABLE_SIZE] = { + [0x01] = KEY_ESCAPE, + [0x02] = KEY_1, + [0x03] = KEY_2, + [0x04] = KEY_3, + [0x05] = KEY_4, + [0x06] = KEY_5, + [0x07] = KEY_6, + [0x08] = KEY_7, + [0x09] = KEY_8, + [0x0A] = KEY_9, + [0x0B] = KEY_0, + [0x0E] = KEY_BACKSPACE, + [0x0F] = KEY_TAB, + [0x10] = KEY_Q, + [0x11] = KEY_W, + [0x12] = KEY_E, + [0x13] = KEY_R, + [0x14] = KEY_T, + [0x15] = KEY_Y, + [0x16] = KEY_U, + [0x17] = KEY_I, + [0x18] = KEY_O, + [0x19] = KEY_P, + [0x1C] = KEY_RETURN, + [0x1D] = KEY_LCTRL, + [0x1E] = KEY_A, + [0x1F] = KEY_S, + [0x20] = KEY_D, + [0x21] = KEY_F, + [0x22] = KEY_G, + [0x23] = KEY_H, + [0x24] = KEY_J, + [0x25] = KEY_K, + [0x26] = KEY_L, + [0x2A] = KEY_LSHIFT, + [0x2C] = KEY_Z, + [0x2D] = KEY_X, + [0x2E] = KEY_C, + [0x2F] = KEY_V, + [0x30] = KEY_B, + [0x31] = KEY_N, + [0x32] = KEY_M, + [0x36] = KEY_RSHIFT, + [0x38] = KEY_LALT, + [0x39] = KEY_SPACE, + [0x3B] = KEY_F1, + [0x3C] = KEY_F2, + [0x3D] = KEY_F3, + [0x3E] = KEY_F4, + [0x3F] = KEY_F5, + [0x40] = KEY_F6, + [0x41] = KEY_F7, + [0x42] = KEY_F8, + [0x43] = KEY_F9, + [0x44] = KEY_F10, + [0x48] = KEY_UP, + [0x4B] = KEY_LEFT, + [0x4D] = KEY_RIGHT, + [0x50] = KEY_DOWN, +}; + +static _go32_dpmi_seginfo gOldHandler; +static _go32_dpmi_seginfo gNewHandler; +static bool gHooked = false; +static volatile bool gIsrState[KEY_COUNT]; + +// ----- Internal helpers ----- + +static void keyboardIsr(void) { + uint8_t scan; + uint8_t code; + uint8_t key; + bool isBreak; + + scan = inportb(KB_DATA_PORT); + + if (scan != SCAN_EXTENDED) { + isBreak = (scan & SCAN_BREAK_BIT) != 0; + code = (uint8_t)(scan & SCAN_CODE_MASK); + key = gScanToKey[code]; + if (key != KEY_NONE) { + gIsrState[key] = !isBreak; + } + } + + outportb(PIC_CMD_PORT, PIC_EOI); +} + + +// ----- HAL API (alphabetical) ----- + +void halInputInit(void) { + memset(gKeyState, 0, sizeof(gKeyState)); + memset(gKeyPrev, 0, sizeof(gKeyPrev)); + memset((void *)gIsrState, 0, sizeof(gIsrState)); + + _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_get_protected_mode_interrupt_vector(9, &gOldHandler); + + gNewHandler.pm_offset = (unsigned long)keyboardIsr; + gNewHandler.pm_selector = _go32_my_cs(); + if (_go32_dpmi_allocate_iret_wrapper(&gNewHandler) != 0) { + return; + } + _go32_dpmi_set_protected_mode_interrupt_vector(9, &gNewHandler); + gHooked = true; +} + + +void halInputPoll(void) { + disable(); + memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState)); + enable(); +} + + +void halInputShutdown(void) { + if (!gHooked) { + return; + } + _go32_dpmi_set_protected_mode_interrupt_vector(9, &gOldHandler); + _go32_dpmi_free_iret_wrapper(&gNewHandler); + gHooked = false; +} diff --git a/src/port/iigs/input.c b/src/port/iigs/input.c new file mode 100644 index 0000000..1a9aa0d --- /dev/null +++ b/src/port/iigs/input.c @@ -0,0 +1,161 @@ +// Apple IIgs keyboard input via the classic Apple II softswitches. +// +// The first cut used the Event Manager, but that requires EMStartUp +// from a toolbox-aware program, and our S16 binary does not go through +// the full ToolBox bring-up -- calling GetNextEvent uninitialized +// corrupted KEGS' emulation state. Softswitches have no such +// dependency: they are live memory-mapped hardware that the monitor +// ROM, BASIC, and ProDOS have always relied on. +// +// 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, +// plus live shift/ctrl/option state from the modifier register at +// $C025. This matches what every Apple II game ever shipped does, and +// it is enough for feature parity with the other platforms on typical +// "press a key, act on it" flows. +// +// Held-key state is synthesized via a TTL counter: a fresh strobe on +// $C000 refreshes the TTL; each halInputPoll decays it; when TTL hits +// zero we assume the key was released. KEY_TTL is sized to cover the +// typematic initial delay so that a held key does not flicker. + +#include + +#include + +#include "hal.h" +#include "inputInternal.h" + +// ----- Hardware registers ----- + +#define IIGS_KBD ((volatile uint8_t *)0x00C000L) +#define IIGS_KBDSTRB ((volatile uint8_t *)0x00C010L) +#define IIGS_MODIFIERS ((volatile uint8_t *)0x00C025L) + +#define KBD_STROBE_BIT 0x80 +#define KBD_ASCII_MASK 0x7F + +// $C025 layout (IIgs Hardware Reference): bit 0 = shift, bit 1 = ctrl, +// bit 6 = option (Closed-Apple), bit 7 = command (Open-Apple). +#define MOD_SHIFT 0x01 +#define MOD_CONTROL 0x02 +#define MOD_OPTION 0x40 + +// Polls a key stays "down" after the last observed strobe. Covers the +// typematic initial delay so a held key does not flicker off/on between +// repeats. +#define KEY_TTL 45 + +#define ASCII_TABLE_SIZE 128 + +// Apple II arrow-key ASCII conventions. +#define ASCII_LEFT 0x08 +#define ASCII_RIGHT 0x15 +#define ASCII_UP 0x0B +#define ASCII_DOWN 0x0A +#define ASCII_RETURN 0x0D +#define ASCII_TAB 0x09 +#define ASCII_ESCAPE 0x1B +#define ASCII_DELETE 0x7F +#define ASCII_SPACE 0x20 + +// ----- Prototypes ----- + +static void buildAsciiTable(void); +static void readModifierKeys(void); + +// ----- Module state ----- + +// ASCII -> JoeyKeyE, filled once at halInputInit. ORCA/C is C89 and +// does not accept designated initializers; runtime fill keeps lookup +// O(1) instead of a 40-plus-case switch. +static uint8_t gAsciiToKey[ASCII_TABLE_SIZE]; +static uint8_t gKeyTtl [KEY_COUNT]; + +// ----- Internal helpers ----- + +static void buildAsciiTable(void) { + uint16_t i; + + memset(gAsciiToKey, 0, sizeof(gAsciiToKey)); + + for (i = 'A'; i <= 'Z'; i++) { + gAsciiToKey[i] = (uint8_t)(KEY_A + (i - 'A')); + gAsciiToKey[i - 'A' + 'a'] = (uint8_t)(KEY_A + (i - 'A')); + } + for (i = '0'; i <= '9'; i++) { + gAsciiToKey[i] = (uint8_t)(KEY_0 + (i - '0')); + } + + gAsciiToKey[ASCII_SPACE] = KEY_SPACE; + gAsciiToKey[ASCII_ESCAPE] = KEY_ESCAPE; + gAsciiToKey[ASCII_RETURN] = KEY_RETURN; + gAsciiToKey[ASCII_TAB] = KEY_TAB; + gAsciiToKey[ASCII_DELETE] = KEY_BACKSPACE; + // The left-arrow key produces 0x08, which is also ASCII backspace + // on a classic Apple II. Prefer the arrow interpretation since + // there is a dedicated Delete key that reports 0x7F. + gAsciiToKey[ASCII_LEFT] = KEY_LEFT; + gAsciiToKey[ASCII_RIGHT] = KEY_RIGHT; + gAsciiToKey[ASCII_UP] = KEY_UP; + gAsciiToKey[ASCII_DOWN] = KEY_DOWN; +} + + +static void readModifierKeys(void) { + uint8_t mods; + + mods = *IIGS_MODIFIERS; + gKeyState[KEY_LSHIFT] = (mods & MOD_SHIFT) != 0; + gKeyState[KEY_LCTRL] = (mods & MOD_CONTROL) != 0; + gKeyState[KEY_LALT] = (mods & MOD_OPTION) != 0; +} + + +// ----- HAL API (alphabetical) ----- + +void halInputInit(void) { + memset(gKeyState, 0, sizeof(gKeyState)); + memset(gKeyPrev, 0, sizeof(gKeyPrev)); + memset(gKeyTtl, 0, sizeof(gKeyTtl)); + buildAsciiTable(); + + // Clear any pending strobe from before we started. + (void)*IIGS_KBDSTRB; +} + + +void halInputPoll(void) { + uint8_t kbd; + uint8_t ascii; + uint8_t key; + uint16_t i; + + for (i = 0; i < KEY_COUNT; i++) { + if (gKeyTtl[i] > 0) { + gKeyTtl[i]--; + if (gKeyTtl[i] == 0) { + gKeyState[i] = false; + } + } + } + + kbd = *IIGS_KBD; + if (kbd & KBD_STROBE_BIT) { + ascii = (uint8_t)(kbd & KBD_ASCII_MASK); + key = gAsciiToKey[ascii]; + if (key != KEY_NONE) { + gKeyState[key] = true; + gKeyTtl[key] = KEY_TTL; + } + (void)*IIGS_KBDSTRB; + } + + readModifierKeys(); +} + + +void halInputShutdown(void) { + (void)*IIGS_KBDSTRB; +} diff --git a/toolchains/install.sh b/toolchains/install.sh index 11f3762..9c525bf 100755 --- a/toolchains/install.sh +++ b/toolchains/install.sh @@ -1194,14 +1194,43 @@ install_support_gsos() { } +install_support_iigs_null_c600() { + local support_dir="$1" + local dest="${support_dir}/iigs-null-c600.rom" + local key="emu_support_iigs_null_c600" + + if [ "${FORCE_INSTALL}" -eq 1 ]; then + rm -f "${dest}" + clear_done "${key}" + fi + if is_done "${key}" && [ -f "${dest}" ]; then + ok "IIgs null slot-6 PROM already staged" + STATUS[${key}]="ok" + return 0 + fi + + # 256-byte null PROM for slot 6. Leading RTS (so any accidental + # call returns cleanly), then zero-filled -- no Pascal 1.1 firmware + # signature, so the IIgs boot ROM's slot scan skips slot 6 and the + # two empty 5.25 drives never get probed. run-iigs.sh stages this + # into each session's work dir as c600.rom; GSplus picks it up from + # cwd and overrides its built-in Disk II PROM. + { printf '\x60'; head -c 255 /dev/zero; } > "${dest}" + mark_done "${key}" + ok "IIgs null slot-6 PROM staged at ${dest}" + STATUS[${key}]="ok" +} + + install_emulator_support() { header "Emulator support files" local support_dir="${SCRIPT_DIR}/emulators/support" mkdir -p "${support_dir}" - install_support_emutos "${support_dir}" - install_support_iigs_rom "${support_dir}" - install_support_gsos "${support_dir}" + install_support_emutos "${support_dir}" + install_support_iigs_rom "${support_dir}" + install_support_iigs_null_c600 "${support_dir}" + install_support_gsos "${support_dir}" }