Key Up/Down working.

This commit is contained in:
Scott Duensing 2026-04-24 16:29:31 -05:00
parent b3d9961e78
commit 9bacd2c68f
23 changed files with 1225 additions and 50 deletions

162
examples/keys/keys.c Normal file
View file

@ -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 <stdio.h>
#include <joey/joey.h>
#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;
}

47
include/joey/input.h Normal file
View file

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

View file

@ -13,5 +13,6 @@
#include "palette.h" #include "palette.h"
#include "draw.h" #include "draw.h"
#include "present.h" #include "present.h"
#include "input.h"
#endif #endif

View file

@ -31,9 +31,11 @@ HELLO_SRC := $(EXAMPLES)/hello/hello.c
HELLO_BIN := $(BINDIR)/Hello HELLO_BIN := $(BINDIR)/Hello
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
PATTERN_BIN := $(BINDIR)/Pattern PATTERN_BIN := $(BINDIR)/Pattern
KEYS_SRC := $(EXAMPLES)/keys/keys.c
KEYS_BIN := $(BINDIR)/Keys
.PHONY: all amiga clean-amiga .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 $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@ -63,5 +65,9 @@ $(PATTERN_BIN): $(PATTERN_SRC) $(LIB)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
$(KEYS_BIN): $(KEYS_SRC) $(LIB)
@mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
clean-amiga: clean-amiga:
rm -rf $(BUILD) rm -rf $(BUILD)

View file

@ -27,9 +27,11 @@ HELLO_SRC := $(EXAMPLES)/hello/hello.c
HELLO_BIN := $(BINDIR)/HELLO.PRG HELLO_BIN := $(BINDIR)/HELLO.PRG
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
PATTERN_BIN := $(BINDIR)/PATTERN.PRG PATTERN_BIN := $(BINDIR)/PATTERN.PRG
KEYS_SRC := $(EXAMPLES)/keys/keys.c
KEYS_BIN := $(BINDIR)/KEYS.PRG
.PHONY: all atarist clean-atarist .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 $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@ -59,5 +61,9 @@ $(PATTERN_BIN): $(PATTERN_SRC) $(LIB)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) $(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
$(KEYS_BIN): $(KEYS_SRC) $(LIB)
@mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
clean-atarist: clean-atarist:
rm -rf $(BUILD) rm -rf $(BUILD)

View file

@ -25,9 +25,11 @@ HELLO_SRC := $(EXAMPLES)/hello/hello.c
HELLO_BIN := $(BINDIR)/HELLO.EXE HELLO_BIN := $(BINDIR)/HELLO.EXE
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
PATTERN_BIN := $(BINDIR)/PATTERN.EXE PATTERN_BIN := $(BINDIR)/PATTERN.EXE
KEYS_SRC := $(EXAMPLES)/keys/keys.c
KEYS_BIN := $(BINDIR)/KEYS.EXE
.PHONY: all dos clean-dos .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 $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@ -55,5 +57,10 @@ $(PATTERN_BIN): $(PATTERN_SRC) $(LIB)
$(DOS_CC) $(CFLAGS) $< $(LIB) -o $@ $(DOS_CC) $(CFLAGS) $< $(LIB) -o $@
$(DOS_EMBED_DPMI) $@ $(DOS_EMBED_DPMI) $@
$(KEYS_BIN): $(KEYS_SRC) $(LIB)
@mkdir -p $(dir $@)
$(DOS_CC) $(CFLAGS) $< $(LIB) -o $@
$(DOS_EMBED_DPMI) $@
clean-dos: clean-dos:
rm -rf $(BUILD) rm -rf $(BUILD)

View file

@ -20,6 +20,8 @@ HELLO_SRC := $(EXAMPLES)/hello/hello.c
HELLO_BIN := $(BINDIR)/HELLO HELLO_BIN := $(BINDIR)/HELLO
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
PATTERN_BIN := $(BINDIR)/PATTERN PATTERN_BIN := $(BINDIR)/PATTERN
KEYS_SRC := $(EXAMPLES)/keys/keys.c
KEYS_BIN := $(BINDIR)/KEYS
DISK_IMG := $(BINDIR)/joey.2mg DISK_IMG := $(BINDIR)/joey.2mg
IIGS_PACKAGE := $(REPO_DIR)/toolchains/iigs/package-disk.sh IIGS_PACKAGE := $(REPO_DIR)/toolchains/iigs/package-disk.sh
@ -31,7 +33,7 @@ IIX_INCLUDES := \
-I $(SRC_CORE) -I $(SRC_CORE)
.PHONY: all iigs iigs-disk clean-iigs .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 # iix-build.sh takes MAIN.c first, then EXTRA sources (compiled with
# #pragma noroot). The example source supplies main(); libjoey sources # #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_BUILD) $(IIX_INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@ $(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. # mount in GSplus alongside a GS/OS boot volume.
iigs-disk: $(DISK_IMG) 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 $@) @mkdir -p $(dir $@)
$(IIGS_PACKAGE) $@ $(HELLO_BIN) $(PATTERN_BIN) $(IIGS_PACKAGE) $@ $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN)
clean-iigs: clean-iigs:
rm -rf $(BUILD) rm -rf $(BUILD)

View file

@ -1,6 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Launch the built Amiga example in FS-UAE. Defaults to PATTERN; pass # Launch the built Amiga example in FS-UAE. Defaults to PATTERN.
# "hello" to run HELLO instead.
# #
# Kickstart and Workbench: # Kickstart and Workbench:
# - If toolchains/emulators/support/kickstart.rom is present, it is # - If toolchains/emulators/support/kickstart.rom is present, it is
@ -18,6 +17,7 @@
# #
# scripts/run-amiga.sh # runs Pattern # scripts/run-amiga.sh # runs Pattern
# scripts/run-amiga.sh hello # runs Hello # scripts/run-amiga.sh hello # runs Hello
# scripts/run-amiga.sh keys # runs Keys
set -euo pipefail set -euo pipefail
@ -29,7 +29,8 @@ support=$repo/toolchains/emulators/support
case $prog in case $prog in
hello) file=Hello ;; hello) file=Hello ;;
pattern) file=Pattern ;; pattern) file=Pattern ;;
*) echo "usage: $0 [hello|pattern]" >&2; exit 2 ;; keys) file=Keys ;;
*) echo "usage: $0 [hello|pattern|keys]" >&2; exit 2 ;;
esac esac
if [[ ! -f "$bin_dir/$file" ]]; then 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" mkdir -p "$work/s"
cp "$bin_dir/Hello" "$work/" 2>/dev/null || true cp "$bin_dir/Hello" "$work/" 2>/dev/null || true
cp "$bin_dir/Pattern" "$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 # ':' prefix anchors to the root of the current volume; otherwise
# AmigaDOS looks in C: and the command is not found. # AmigaDOS looks in C: and the command is not found.
echo ":$file" > "$work/s/startup-sequence" echo ":$file" > "$work/s/startup-sequence"

View file

@ -1,12 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Launch the built Atari ST example in Hatari. Defaults to PATTERN; # Launch the built Atari ST example in Hatari. Defaults to PATTERN.
# pass "hello" to run HELLO instead. Hatari autostarts the .PRG via # Hatari autostarts the .PRG via the --auto flag; EmuTOS
# the --auto flag; EmuTOS (toolchains/emulators/support/emutos-512k.img) # (toolchains/emulators/support/emutos-512k.img) provides the TOS ROM
# provides the TOS ROM since the Ubuntu hatari package does not bundle # since the Ubuntu hatari package does not bundle one.
# one.
# #
# scripts/run-atarist.sh # runs PATTERN.PRG # scripts/run-atarist.sh # runs PATTERN.PRG
# scripts/run-atarist.sh hello # runs HELLO.PRG # scripts/run-atarist.sh hello # runs HELLO.PRG
# scripts/run-atarist.sh keys # runs KEYS.PRG
set -euo pipefail set -euo pipefail
@ -17,7 +17,8 @@ bin_dir=$repo/build/atarist/bin
case $prog in case $prog in
hello) file=HELLO.PRG ;; hello) file=HELLO.PRG ;;
pattern) file=PATTERN.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 esac
tos=$repo/toolchains/emulators/support/emutos-512k.img tos=$repo/toolchains/emulators/support/emutos-512k.img

View file

@ -1,9 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Launch the built DOS example in DOSBox. Defaults to PATTERN; pass # Launch the built DOS example in DOSBox. Defaults to PATTERN.
# "hello" to run HELLO instead.
# #
# scripts/run-dos.sh # runs PATTERN # scripts/run-dos.sh # runs PATTERN
# scripts/run-dos.sh hello # runs HELLO # scripts/run-dos.sh hello # runs HELLO
# scripts/run-dos.sh keys # runs KEYS
set -euo pipefail set -euo pipefail
@ -14,7 +14,8 @@ bin_dir=$repo/build/dos/bin
case $prog in case $prog in
hello) file=HELLO.EXE ;; hello) file=HELLO.EXE ;;
pattern) file=PATTERN.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 esac
if [[ ! -f "$bin_dir/$file" ]]; then if [[ ! -f "$bin_dir/$file" ]]; then

View file

@ -2,15 +2,16 @@
# Launch the built Apple IIgs examples in GSplus. GSplus is booted from # 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) # 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 # 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 # navigates to the JOEYLIB volume in Finder and double-clicks the
# PATTERN to run it. # example to run it.
# #
# Unlike the other emulators, GS/OS does not auto-run on boot -- 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 # drops to Finder. The argument just prints a reminder of which
# which example to launch. # example to launch.
# #
# scripts/run-iigs.sh # boots and waits for user (Pattern hint) # scripts/run-iigs.sh # boots (Pattern hint)
# scripts/run-iigs.sh hello # same, but hints to click HELLO # scripts/run-iigs.sh hello # boots, hints HELLO
# scripts/run-iigs.sh keys # boots, hints KEYS
set -euo pipefail set -euo pipefail
@ -21,19 +22,20 @@ gsplus=$repo/toolchains/emulators/gsplus/bin/gsplus
rom=$repo/toolchains/emulators/support/apple-iigs.rom rom=$repo/toolchains/emulators/support/apple-iigs.rom
sys_disk=$repo/toolchains/emulators/support/gsos-system.po sys_disk=$repo/toolchains/emulators/support/gsos-system.po
data_disk=$repo/build/iigs/bin/joey.2mg data_disk=$repo/build/iigs/bin/joey.2mg
null_c600=$repo/toolchains/emulators/support/iigs-null-c600.rom
case $prog in case $prog in
hello|pattern) ;; hello|pattern|keys) ;;
*) echo "usage: $0 [hello|pattern]" >&2; exit 2 ;; *) echo "usage: $0 [hello|pattern|keys]" >&2; exit 2 ;;
esac 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 if [[ ! -f $f ]]; then
echo "missing: $f" >&2 echo "missing: $f" >&2
if [[ $f == "$data_disk" ]]; then if [[ $f == "$data_disk" ]]; then
echo "run 'make iigs-disk' to build it." >&2 echo "run 'make iigs-disk' to build it." >&2
else 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 fi
exit 1 exit 1
fi fi
@ -47,6 +49,15 @@ trap 'rm -rf "$work"' EXIT
cp "$sys_disk" "$work/boot.po" cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg" 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:]') target=$(echo "$prog" | tr '[:lower:]' '[:upper:]')
cat <<EOF cat <<EOF
GSplus launching GS/OS 6.0.4. GSplus launching GS/OS 6.0.4.

View file

@ -10,6 +10,7 @@
#define JOEYLIB_HAL_H #define JOEYLIB_HAL_H
#include "joey/core.h" #include "joey/core.h"
#include "joey/input.h"
#include "joey/surface.h" #include "joey/surface.h"
// Per-port one-shot initialization. Called from joeyInit after config // Per-port one-shot initialization. Called from joeyInit after config
@ -34,4 +35,14 @@ void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint1
// HAL failure, or NULL if none. Ports may return NULL always. // HAL failure, or NULL if none. Ports may return NULL always.
const char *halLastError(void); const char *halLastError(void);
// Input: per-port keyboard setup, polling, and teardown.
// halInputInit is called at the end of joeyInit; halInputShutdown
// from joeyShutdown before halShutdown. halInputPoll refreshes the
// core-owned key-state array (declared in core/inputInternal.h) --
// the port writes true into gKeyState[key] for held keys. Keys the
// port does not recognize simply stay zero.
void halInputInit(void);
void halInputShutdown(void);
void halInputPoll(void);
#endif #endif

View file

@ -63,6 +63,8 @@ bool joeyInit(const JoeyConfigT *config) {
return false; return false;
} }
halInputInit();
gInitialized = true; gInitialized = true;
return true; return true;
} }
@ -82,6 +84,7 @@ void joeyShutdown(void) {
if (!gInitialized) { if (!gInitialized) {
return; return;
} }
halInputShutdown();
halShutdown(); halShutdown();
surfaceFreeScreen(); surfaceFreeScreen();
gInitialized = false; gInitialized = false;

43
src/core/input.c Normal file
View file

@ -0,0 +1,43 @@
// Public input API. Maintains two key-state buffers: the current
// state populated by the port's halInputPoll, and the previous
// state captured just before polling so rising/falling edges can
// be computed without the port having to track them.
#include <string.h>
#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];
}

17
src/core/inputInternal.h Normal file
View file

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

View file

@ -9,8 +9,10 @@
// * Build a user copper list that WAITs for each display scanline // * Build a user copper list that WAITs for each display scanline
// and MOVEs the 16 color registers with that line's palette. // and MOVEs the 16 color registers with that line's palette.
// * Install it via ViewPort.UCopIns + MakeScreen + RethinkDisplay. // * Install it via ViewPort.UCopIns + MakeScreen + RethinkDisplay.
// * Rebuild on every halPresent since SCB and palettes may have // * Rebuild only when SCB or palette state differs from the last
// changed anywhere. // 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: // Deferred:
// * Blitter-assisted c2p for speed on A500. // * 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 dumpCopperList(void);
static void installCopperList(void); static void installCopperList(void);
static void uploadFirstBandPalette(const SurfaceT *src); static void uploadFirstBandPalette(const SurfaceT *src);
static void updateCopperIfNeeded(const SurfaceT *src);
// ----- Module state ----- // ----- Module state -----
static struct Screen *gScreen = NULL; struct Screen *gScreen = NULL; // shared with input.c
static struct BitMap *gBitMap = NULL; static struct BitMap *gBitMap = NULL;
static UBYTE *gPlanes[AMIGA_BITPLANES]; static UBYTE *gPlanes[AMIGA_BITPLANES];
static struct UCopList *gNewUCL = NULL; // built but not yet installed 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) ----- // ----- Internal helpers (alphabetical) -----
// Convert a range of chunky scanlines [y0, y1) to Amiga planar. // 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 // Load the first band's palette into the screen's ColorMap so the
// Intuition-generated frame-start copper writes those values on each // Intuition-generated frame-start copper writes those values on each
// frame. This acts as a safety net: even if our user copper list does // 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) { if (src == NULL || gScreen == NULL) {
return; return;
} }
uploadFirstBandPalette(src); updateCopperIfNeeded(src);
buildCopperList(src);
installCopperList();
dumpCopperList();
c2pRange(src, 0, SURFACE_HEIGHT); 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) { if (src == NULL || gScreen == NULL) {
return; return;
} }
// Whole-scanline c2p for M2; rebuild the entire copper list since updateCopperIfNeeded(src);
// SCB entries outside the rect may have changed too.
uploadFirstBandPalette(src);
buildCopperList(src);
installCopperList();
c2pRange(src, y, y + (int16_t)h); c2pRange(src, y, y + (int16_t)h);
} }

173
src/port/amiga/input.c Normal file
View file

@ -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 <string.h>
#include <exec/types.h>
#include <exec/ports.h>
#include <intuition/intuition.h>
#include <intuition/screens.h>
#include <devices/inputevent.h>
#include <proto/exec.h>
#include <proto/intuition.h>
#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;
}

View file

@ -73,6 +73,8 @@ static long writePrevPaletteRegs(void);
static __attribute__((interrupt_handler)) void timerBIsr(void); static __attribute__((interrupt_handler)) void timerBIsr(void);
static __attribute__((interrupt_handler)) void vblIsr(void); static __attribute__((interrupt_handler)) void vblIsr(void);
static void buildTransitions(const SurfaceT *src); static void buildTransitions(const SurfaceT *src);
static bool paletteOrScbChanged(const SurfaceT *src);
static void refreshPaletteStateIfNeeded(const SurfaceT *src);
// ----- Module state ----- // ----- Module state -----
@ -117,6 +119,16 @@ static volatile uint16_t gLastBandCount = 0;
static void (*gOldVblVec)(void) = NULL; static void (*gOldVblVec)(void) = NULL;
static void (*gOldTimerBVec)(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) ----- // ----- Internal helpers (alphabetical) -----
// Convert 16 chunky pixels (8 bytes 4bpp packed) to 4 ST planar words. // 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 // 12-bit $0RGB to STF 9-bit palette register (drops the low bit of
// each 4-bit channel). // each 4-bit channel).
static uint16_t quantizeColorToSt(uint16_t orgb) { static uint16_t quantizeColorToSt(uint16_t orgb) {
@ -437,8 +480,7 @@ void halPresent(const SurfaceT *src) {
if (src == NULL || !gModeSet) { if (src == NULL || !gModeSet) {
return; return;
} }
flattenScbPalettes(src); refreshPaletteStateIfNeeded(src);
buildTransitions(src);
c2pRange(src, 0, SURFACE_HEIGHT); 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) { if (src == NULL || !gModeSet) {
return; return;
} }
flattenScbPalettes(src); refreshPaletteStateIfNeeded(src);
buildTransitions(src);
c2pRange(src, y, y + (int16_t)h); c2pRange(src, y, y + (int16_t)h);
} }

218
src/port/atarist/input.c Normal file
View file

@ -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 <string.h>
#include <mint/osbind.h>
#include <mint/ostruct.h>
#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;
}

View file

@ -31,12 +31,18 @@
static void expandAndWriteLine(const SurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst); 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 uploadPalette(const SurfaceT *src);
static void uploadPaletteIfNeeded(const SurfaceT *src);
// ----- Module state ----- // ----- Module state -----
static uint8_t *gVgaMem = NULL; static uint8_t *gVgaMem = NULL;
static bool gNearEnabled = false; 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) ----- // ----- Internal helpers (alphabetical) -----
static void expandAndWriteLine(const SurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst) { 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) ----- // ----- HAL API (alphabetical) -----
bool halInit(const JoeyConfigT *config) { bool halInit(const JoeyConfigT *config) {
@ -113,7 +129,7 @@ void halPresent(const SurfaceT *src) {
if (src == NULL || gVgaMem == NULL) { if (src == NULL || gVgaMem == NULL) {
return; return;
} }
uploadPalette(src); uploadPaletteIfNeeded(src);
for (y = 0; y < SURFACE_HEIGHT; y++) { for (y = 0; y < SURFACE_HEIGHT; y++) {
expandAndWriteLine(src, y, 0, SURFACE_WIDTH, &gVgaMem[y * VGA_STRIDE]); 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) { if (src == NULL || gVgaMem == NULL) {
return; return;
} }
uploadPalette(src); uploadPaletteIfNeeded(src);
yEnd = y + (int16_t)h; yEnd = y + (int16_t)h;
for (py = y; py < yEnd; py++) { for (py = y; py < yEnd; py++) {
expandAndWriteLine(src, py, x, w, &gVgaMem[py * VGA_STRIDE]); expandAndWriteLine(src, py, x, w, &gVgaMem[py * VGA_STRIDE]);

170
src/port/dos/input.c Normal file
View file

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

161
src/port/iigs/input.c Normal file
View file

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

View file

@ -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() { install_emulator_support() {
header "Emulator support files" header "Emulator support files"
local support_dir="${SCRIPT_DIR}/emulators/support" local support_dir="${SCRIPT_DIR}/emulators/support"
mkdir -p "${support_dir}" mkdir -p "${support_dir}"
install_support_emutos "${support_dir}" install_support_emutos "${support_dir}"
install_support_iigs_rom "${support_dir}" install_support_iigs_rom "${support_dir}"
install_support_gsos "${support_dir}" install_support_iigs_null_c600 "${support_dir}"
install_support_gsos "${support_dir}"
} }