diff --git a/Makefile b/Makefile index d32132f..37fc655 100644 --- a/Makefile +++ b/Makefile @@ -35,14 +35,17 @@ ifeq ($(HAVE_DOS),1) ALL_TARGETS += dos endif -.PHONY: all iigs iigs-disk amiga atarist dos clean help status +.PHONY: all iigs iigs-disk amiga atarist dos tools clean help status -all: $(ALL_TARGETS) +all: $(ALL_TARGETS) tools ifeq ($(ALL_TARGETS),) @echo "No toolchains detected. Run ./toolchains/install.sh and source toolchains/env.sh." @exit 1 endif +tools: + @$(MAKE) -f $(REPO_DIR)/make/tools.mk + iigs: @$(MAKE) -f $(REPO_DIR)/make/iigs.mk diff --git a/examples/joy/joy.c b/examples/joy/joy.c new file mode 100644 index 0000000..a6235f4 --- /dev/null +++ b/examples/joy/joy.c @@ -0,0 +1,252 @@ +// Joystick demo: shows position and button state for both joystick +// ports as live, redraw-on-change visualizations. Each stick gets a +// frame with a dot indicating the current axis values, plus two +// button indicators below. A small status patch shows whether each +// port reports a connected stick. Press ESC to quit. +// +// Like the keys demo, the loop only redraws cells that changed since +// last frame so the per-cell rect-present cost stays small. + +#include + +#include + +#define COLOR_BACKGROUND 0 +#define COLOR_FRAME 1 +#define COLOR_DOT_OFF 2 +#define COLOR_DOT_ON 3 +#define COLOR_BUTTON_OFF 4 +#define COLOR_BUTTON_ON 5 +#define COLOR_CONNECTED 6 +#define COLOR_DISCONNECTED 7 + +#define FRAME_SIZE 80 +#define FRAME_INSET 2 +#define DOT_SIZE 8 +#define DOT_TRAVEL ((FRAME_SIZE - DOT_SIZE) / 2) + +#define BUTTON_W 24 +#define BUTTON_H 16 +#define BUTTON_GAP 4 + +#define STATUS_W 80 +#define STATUS_H 4 + +#define STICK_TOP 16 +#define BUTTON_TOP (STICK_TOP + FRAME_SIZE + 6) +#define STATUS_TOP (BUTTON_TOP + BUTTON_H + 6) + +#define STICK0_LEFT 24 +#define STICK1_LEFT 216 + +static void buildPalette(SurfaceT *screen); +static void drawFrame(SurfaceT *screen, int16_t left); +static void drawAndPresent(SurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color); +static int16_t dotXFor(int16_t left, int8_t ax); +static int16_t dotYFor(int8_t ay); +static int16_t buttonXFor(int16_t left, int idx); +static void initialPaint(SurfaceT *screen); +static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left); + +typedef struct { + int16_t dotX; + int16_t dotY; + bool btn[JOY_BUTTON_COUNT]; + bool connected; + bool valid; +} StickViewT; + +static StickViewT gView[JOYSTICK_COUNT]; + + +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; + colors[COLOR_FRAME] = 0x0444; + colors[COLOR_DOT_OFF] = 0x0666; + colors[COLOR_DOT_ON] = 0x00F0; // green + colors[COLOR_BUTTON_OFF] = 0x0333; + colors[COLOR_BUTTON_ON] = 0x0FF0; // yellow + colors[COLOR_CONNECTED] = 0x00F0; + colors[COLOR_DISCONNECTED] = 0x0F00; // red + + paletteSet(screen, 0, colors); +} + + +static void drawAndPresent(SurfaceT *screen, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) { + fillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color); + surfacePresentRect(screen, x, y, (uint16_t)w, (uint16_t)h); +} + + +// Draw the static stick frame (just an outlined square -- four edges +// with the inset filled background). +static void drawFrame(SurfaceT *screen, int16_t left) { + int16_t top; + + top = STICK_TOP; + fillRect(screen, left, top, FRAME_SIZE, FRAME_SIZE, COLOR_FRAME); + fillRect(screen, + (int16_t)(left + FRAME_INSET), + (int16_t)(top + FRAME_INSET), + (uint16_t)(FRAME_SIZE - 2 * FRAME_INSET), + (uint16_t)(FRAME_SIZE - 2 * FRAME_INSET), + COLOR_BACKGROUND); +} + + +static int16_t dotXFor(int16_t left, int8_t ax) { + int32_t cx; + int32_t off; + + cx = left + FRAME_SIZE / 2 - DOT_SIZE / 2; + off = ((int32_t)ax * DOT_TRAVEL) / JOYSTICK_AXIS_MAX; + return (int16_t)(cx + off); +} + + +static int16_t dotYFor(int8_t ay) { + int32_t cy; + int32_t off; + + cy = STICK_TOP + FRAME_SIZE / 2 - DOT_SIZE / 2; + off = ((int32_t)ay * DOT_TRAVEL) / JOYSTICK_AXIS_MAX; + return (int16_t)(cy + off); +} + + +static int16_t buttonXFor(int16_t left, int idx) { + int16_t centerX; + + centerX = (int16_t)(left + FRAME_SIZE / 2); + if (idx == 0) { + return (int16_t)(centerX - BUTTON_W - BUTTON_GAP / 2); + } + return (int16_t)(centerX + BUTTON_GAP / 2); +} + + +static void initialPaint(SurfaceT *screen) { + int16_t i; + + surfaceClear(screen, COLOR_BACKGROUND); + for (i = 0; i < JOYSTICK_COUNT; i++) { + int16_t left; + left = (i == 0) ? STICK0_LEFT : STICK1_LEFT; + drawFrame(screen, left); + gView[i].valid = false; + gView[i].connected = false; + } + surfacePresent(screen); +} + + +// Compare current joystick state against gView and redraw / present +// only the visual elements that changed. +static void updateStick(SurfaceT *screen, JoeyJoystickE js, int16_t left) { + StickViewT *v; + int8_t ax; + int8_t ay; + int16_t newDotX; + int16_t newDotY; + bool newBtn[JOY_BUTTON_COUNT]; + bool connected; + int16_t i; + int16_t bx; + int16_t by; + + v = &gView[js]; + connected = joeyJoystickConnected(js); + ax = joeyJoystickX(js); + ay = joeyJoystickY(js); + newDotX = dotXFor(left, ax); + newDotY = dotYFor(ay); + for (i = 0; i < JOY_BUTTON_COUNT; i++) { + newBtn[i] = joeyJoyDown(js, (JoeyJoyButtonE)i); + } + + if (!v->valid || v->connected != connected) { + drawAndPresent(screen, + (int16_t)(left + (FRAME_SIZE - STATUS_W) / 2), + STATUS_TOP, + STATUS_W, STATUS_H, + connected ? COLOR_CONNECTED : COLOR_DISCONNECTED); + v->connected = connected; + } + + // Move the position dot: erase the old square (paint background), + // then stamp the new one. If position didn't change, skip. + if (!v->valid || v->dotX != newDotX || v->dotY != newDotY) { + if (v->valid) { + drawAndPresent(screen, v->dotX, v->dotY, DOT_SIZE, DOT_SIZE, COLOR_BACKGROUND); + } + drawAndPresent(screen, newDotX, newDotY, DOT_SIZE, DOT_SIZE, + (ax != 0 || ay != 0) ? COLOR_DOT_ON : COLOR_DOT_OFF); + v->dotX = newDotX; + v->dotY = newDotY; + } else if (!v->valid) { + // First paint of a centered dot: still need to draw it once. + drawAndPresent(screen, newDotX, newDotY, DOT_SIZE, DOT_SIZE, COLOR_DOT_OFF); + } + + // Button squares. + by = BUTTON_TOP; + for (i = 0; i < JOY_BUTTON_COUNT; i++) { + if (v->valid && v->btn[i] == newBtn[i]) { + continue; + } + bx = buttonXFor(left, i); + drawAndPresent(screen, bx, by, BUTTON_W, BUTTON_H, + newBtn[i] ? COLOR_BUTTON_ON : COLOR_BUTTON_OFF); + v->btn[i] = newBtn[i]; + } + + v->valid = true; +} + + +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); + initialPaint(screen); + joeyInputPoll(); + + for (;;) { + joeyInputPoll(); + if (joeyKeyPressed(KEY_ESCAPE)) { + break; + } + updateStick(screen, JOYSTICK_0, STICK0_LEFT); + updateStick(screen, JOYSTICK_1, STICK1_LEFT); + } + + joeyShutdown(); + return 0; +} diff --git a/examples/sprite/sprite.c b/examples/sprite/sprite.c new file mode 100644 index 0000000..0b68434 --- /dev/null +++ b/examples/sprite/sprite.c @@ -0,0 +1,148 @@ +// Sprite demo: bounces a 16x16 ball sprite around the screen using +// surfaceBlitMasked. The ball is embedded as a `const JoeyAssetT` +// directly in this file, so no .jas file or runtime allocation is +// involved -- this exercises the static / embedded path. Press ESC +// to quit. +// +// Each frame we redraw only the ball's old and new bounding boxes +// (and present those two small rects), so the cost stays small even +// with the slow 68000-class c2p in the ST and Amiga ports. + +#include + +#include + +#define BALL_W 16 +#define BALL_H 16 + +#define BALL_PALETTE_IDX 0 + +#define COLOR_BG 0 +#define COLOR_TRANSPARENT 0 // first palette slot doubles as mask + +// 16x16 ball sprite, 4bpp packed (8 bytes per row): +// 0 = transparent (mask) +// 2 = ball body (yellow) +// 3 = highlight (white) +// High nibble of each byte is the LEFT pixel. +static const uint8_t gBallPixels[BALL_H * 8] = { + 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, // row 0 + 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, // row 1 + 0x02, 0x22, 0x32, 0x22, 0x22, 0x22, 0x22, 0x20, // row 2 + 0x02, 0x23, 0x32, 0x22, 0x22, 0x22, 0x22, 0x20, // row 3 + 0x22, 0x33, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 4 + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 5 + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 6 + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 7 + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 8 + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 9 + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 10 + 0x02, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x20, // row 11 + 0x02, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x20, // row 12 + 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, // row 13 + 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, // row 14 + 0x00, 0x00, 0x00, 0x22, 0x22, 0x00, 0x00, 0x00 // row 15 +}; + +// Build the embedded ball asset at runtime. We could declare a +// `static const JoeyAssetT` with this same data, but ORCA/C 2.1 +// does not handle a file-scope static struct whose pointer field is +// initialized from another static array's address (linker complains +// of unresolved references). Building the struct in main() with a +// local sidesteps the quirk and costs only a handful of stores. +static void buildBallAsset(JoeyAssetT *ball) { + uint16_t i; + + ball->width = BALL_W; + ball->height = BALL_H; + ball->hasPalette = true; + ball->pixels = gBallPixels; + for (i = 0; i < 16; i++) { + ball->palette[i] = 0x0000; + } + ball->palette[2] = 0x0FF0; // yellow body + ball->palette[3] = 0x0FFF; // white highlight +} + + +static void initialPaint(SurfaceT *screen) { + surfaceClear(screen, COLOR_BG); + surfacePresent(screen); +} + + +int main(void) { + JoeyConfigT config; + SurfaceT *screen; + JoeyAssetT ball; + int16_t x; + int16_t y; + int16_t vx; + int16_t vy; + int16_t lastX; + int16_t lastY; + + 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; + } + + buildBallAsset(&ball); + joeyAssetApplyPalette(screen, BALL_PALETTE_IDX, &ball); + scbSetRange(screen, 0, SURFACE_HEIGHT - 1, BALL_PALETTE_IDX); + initialPaint(screen); + + x = 40; + y = 30; + vx = 2; + vy = 1; + lastX = x; + lastY = y; + + surfaceBlitMasked(screen, &ball, x, y, COLOR_TRANSPARENT); + surfacePresentRect(screen, x, y, BALL_W, BALL_H); + + for (;;) { + joeyWaitVBL(); + joeyInputPoll(); + if (joeyKeyPressed(KEY_ESCAPE)) { + break; + } + + // Erase old ball position by clearing its bounding rect. Any + // pixel outside the ball that we wrote (we wrote no pixels + // outside, since blitMasked respects transparency) stays as + // background already. + fillRect(screen, lastX, lastY, BALL_W, BALL_H, COLOR_BG); + surfacePresentRect(screen, lastX, lastY, BALL_W, BALL_H); + + x = (int16_t)(x + vx); + y = (int16_t)(y + vy); + if (x <= 0) { x = 0; vx = (int16_t)-vx; } + if (x >= SURFACE_WIDTH - BALL_W) { x = SURFACE_WIDTH - BALL_W; vx = (int16_t)-vx; } + if (y <= 0) { y = 0; vy = (int16_t)-vy; } + if (y >= SURFACE_HEIGHT - BALL_H) { y = SURFACE_HEIGHT - BALL_H; vy = (int16_t)-vy; } + + surfaceBlitMasked(screen, &ball, x, y, COLOR_TRANSPARENT); + surfacePresentRect(screen, x, y, BALL_W, BALL_H); + + lastX = x; + lastY = y; + } + + joeyShutdown(); + return 0; +} diff --git a/include/joey/asset.h b/include/joey/asset.h new file mode 100644 index 0000000..f9f67a5 --- /dev/null +++ b/include/joey/asset.h @@ -0,0 +1,65 @@ +// Sprite-style asset loading. +// +// A JoeyLib asset is a small bitmap with the same 4bpp packed pixel +// format as the rest of the library, plus an optional 16-entry $0RGB +// palette. Assets blit onto SurfaceT via surfaceBlit / surfaceBlitMasked +// (defined in draw.h). +// +// Two production paths: +// +// 1. Embedded -- declare a `const JoeyAssetT` directly in a .c file +// and bundle it into the executable. No allocation, no I/O. Best +// for small game assets that always ship with the binary. +// +// 2. File / memory -- joeyAssetLoadFile reads a .jas blob from disk; +// joeyAssetFromMem parses one already in RAM. Both allocate the +// JoeyAssetT plus its pixel buffer; release with joeyAssetFree. +// +// .jas binary format (little-endian, byte-stream so endianness of the +// host doesn't matter): +// +// offset bytes field +// ------ ----- -------------------------------------------- +// 0 4 magic "JAS1" +// 4 2 width in pixels +// 6 2 height in pixels +// 8 1 hasPalette (0 or 1) +// 9 3 reserved (zero) +// 12 32 palette[16] of $0RGB; valid only if hasPalette +// 44 ... pixels: rowBytes * height where rowBytes = (width+1)/2 + +#ifndef JOEYLIB_ASSET_H +#define JOEYLIB_ASSET_H + +#include "platform.h" +#include "surface.h" +#include "types.h" + +typedef struct { + uint16_t width; + uint16_t height; + bool hasPalette; + uint16_t palette[16]; + const uint8_t *pixels; +} JoeyAssetT; + +// Allocates a new asset by reading a .jas file. Returns NULL if the +// file is missing, malformed, or too large. +JoeyAssetT *joeyAssetLoadFile(const char *path); + +// Parses a .jas blob already in memory. Allocates a JoeyAssetT and +// copies the pixel run into a fresh buffer so the caller can free its +// own copy of the bytes after the call returns. +JoeyAssetT *joeyAssetFromMem(const uint8_t *data, uint32_t length); + +// Releases an asset previously returned by a loader. Calling free on +// a static / embedded JoeyAssetT is forbidden -- those structs do not +// own their pixels and must not be passed here. NULL is OK. +void joeyAssetFree(JoeyAssetT *asset); + +// Copies the asset's 16-entry palette into one of the surface's +// palette slots so that subsequent blits land on the right colors. +// No-op if the asset has no palette. +void joeyAssetApplyPalette(SurfaceT *dst, uint8_t paletteIndex, const JoeyAssetT *asset); + +#endif diff --git a/include/joey/core.h b/include/joey/core.h index ec7a009..95f55e7 100644 --- a/include/joey/core.h +++ b/include/joey/core.h @@ -30,4 +30,11 @@ const char *joeyPlatformName(void); // Returns the library version string (e.g., "1.0.0"). const char *joeyVersionString(void); +// Block the calling thread until the next display vertical blank. +// Used to pace game loops to the display's native refresh rate +// (~70 Hz on VGA mode 13h, ~50 or ~60 Hz on Amiga/ST PAL/NTSC, ~60 Hz +// on IIgs SHR). Cheap on every port since the underlying mechanism is +// always a hardware-level wait, not a software timer. +void joeyWaitVBL(void); + #endif diff --git a/include/joey/draw.h b/include/joey/draw.h index d0d13df..d851805 100644 --- a/include/joey/draw.h +++ b/include/joey/draw.h @@ -8,6 +8,7 @@ #ifndef JOEYLIB_DRAW_H #define JOEYLIB_DRAW_H +#include "asset.h" #include "platform.h" #include "surface.h" #include "types.h" @@ -25,4 +26,16 @@ uint8_t samplePixel(const SurfaceT *s, int16_t x, int16_t y); // Fill a solid rectangle. Negative or zero dimensions are no-ops. void fillRect(SurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex); +// Blit an asset onto the surface at (x, y). Source nibbles overwrite +// destination nibbles verbatim -- the caller is responsible for +// matching the asset's palette to the destination palette (typically +// via joeyAssetApplyPalette). Clipped to the surface; off-surface +// rows / columns are skipped. +void surfaceBlit(SurfaceT *dst, const JoeyAssetT *src, int16_t x, int16_t y); + +// Like surfaceBlit, but source pixels equal to transparentIndex are +// skipped, leaving the destination pixel unchanged at that position. +// The standard convention is transparentIndex = 0. +void surfaceBlitMasked(SurfaceT *dst, const JoeyAssetT *src, int16_t x, int16_t y, uint8_t transparentIndex); + #endif diff --git a/include/joey/input.h b/include/joey/input.h index 8bf0ea3..c4a16a8 100644 --- a/include/joey/input.h +++ b/include/joey/input.h @@ -1,14 +1,15 @@ -// Keyboard and mouse input polling. +// Keyboard, mouse, and joystick input polling. // // Call joeyInputPoll() once per frame (typically right before -// drawing) to refresh both keyboard and mouse state. After polling, -// the joeyKey* predicates return the current state of every key: +// drawing) to refresh keyboard, mouse, and joystick state. After +// polling, the joeyKey* predicates return the current state of every +// key: // // joeyKeyDown(k) -- is key k held down right now // joeyKeyPressed(k) -- rising edge since the previous poll // joeyKeyReleased(k) -- falling edge since the previous poll // -// And the mouse predicates return the pointer state: +// The mouse predicates return the pointer state: // // joeyMouseX/Y() -- pointer position in surface // coords (0..SURFACE_WIDTH-1, @@ -17,6 +18,18 @@ // joeyMousePressed(b) -- rising edge since last poll // joeyMouseReleased(b) -- falling edge since last poll // +// The joystick predicates return per-stick state: +// +// joeyJoystickConnected(js) -- true if the platform reports a +// stick on this port +// joeyJoystickX/Y(js) -- axis values, signed -127..+127. +// Digital sticks snap to the +// extremes; analog sticks return +// the raw centered value. +// joeyJoyDown(js, b) -- is button b held on stick js +// joeyJoyPressed(js, b) -- rising edge since last poll +// joeyJoyReleased(js, b) -- falling edge since last poll +// // Edge predicates are one-shot: they return true only in the // frame the transition occurred and false thereafter. @@ -57,6 +70,23 @@ typedef enum { MOUSE_BUTTON_COUNT } JoeyMouseButtonE; +typedef enum { + JOYSTICK_0 = 0, + JOYSTICK_1, + + JOYSTICK_COUNT +} JoeyJoystickE; + +typedef enum { + JOY_BUTTON_0 = 0, + JOY_BUTTON_1, + + JOY_BUTTON_COUNT +} JoeyJoyButtonE; + +#define JOYSTICK_AXIS_MAX 127 +#define JOYSTICK_AXIS_MIN (-127) + void joeyInputPoll(void); bool joeyKeyDown(JoeyKeyE key); @@ -69,4 +99,11 @@ bool joeyMouseDown(JoeyMouseButtonE button); bool joeyMousePressed(JoeyMouseButtonE button); bool joeyMouseReleased(JoeyMouseButtonE button); +bool joeyJoystickConnected(JoeyJoystickE js); +int8_t joeyJoystickX(JoeyJoystickE js); +int8_t joeyJoystickY(JoeyJoystickE js); +bool joeyJoyDown(JoeyJoystickE js, JoeyJoyButtonE button); +bool joeyJoyPressed(JoeyJoystickE js, JoeyJoyButtonE button); +bool joeyJoyReleased(JoeyJoystickE js, JoeyJoyButtonE button); + #endif diff --git a/include/joey/joey.h b/include/joey/joey.h index 39cfdbe..d6dce51 100644 --- a/include/joey/joey.h +++ b/include/joey/joey.h @@ -11,6 +11,7 @@ #include "core.h" #include "surface.h" #include "palette.h" +#include "asset.h" #include "draw.h" #include "present.h" #include "input.h" diff --git a/make/amiga.mk b/make/amiga.mk index 804a462..74c41aa 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -33,9 +33,13 @@ PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_BIN := $(BINDIR)/Pattern KEYS_SRC := $(EXAMPLES)/keys/keys.c KEYS_BIN := $(BINDIR)/Keys +JOY_SRC := $(EXAMPLES)/joy/joy.c +JOY_BIN := $(BINDIR)/Joy +SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c +SPRITE_BIN := $(BINDIR)/Sprite .PHONY: all amiga clean-amiga -all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) +all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) @@ -69,5 +73,13 @@ $(KEYS_BIN): $(KEYS_SRC) $(LIB) @mkdir -p $(dir $@) $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) +$(JOY_BIN): $(JOY_SRC) $(LIB) + @mkdir -p $(dir $@) + $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) + +$(SPRITE_BIN): $(SPRITE_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 7807775..f0bebf8 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -29,9 +29,13 @@ PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_BIN := $(BINDIR)/PATTERN.PRG KEYS_SRC := $(EXAMPLES)/keys/keys.c KEYS_BIN := $(BINDIR)/KEYS.PRG +JOY_SRC := $(EXAMPLES)/joy/joy.c +JOY_BIN := $(BINDIR)/JOY.PRG +SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c +SPRITE_BIN := $(BINDIR)/SPRITE.PRG .PHONY: all atarist clean-atarist -all atarist: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) +all atarist: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) @@ -65,5 +69,13 @@ $(KEYS_BIN): $(KEYS_SRC) $(LIB) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) +$(JOY_BIN): $(JOY_SRC) $(LIB) + @mkdir -p $(dir $@) + $(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) + +$(SPRITE_BIN): $(SPRITE_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 74b8295..2f1d8f0 100644 --- a/make/dos.mk +++ b/make/dos.mk @@ -27,9 +27,13 @@ PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_BIN := $(BINDIR)/PATTERN.EXE KEYS_SRC := $(EXAMPLES)/keys/keys.c KEYS_BIN := $(BINDIR)/KEYS.EXE +JOY_SRC := $(EXAMPLES)/joy/joy.c +JOY_BIN := $(BINDIR)/JOY.EXE +SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c +SPRITE_BIN := $(BINDIR)/SPRITE.EXE .PHONY: all dos clean-dos -all dos: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) +all dos: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) @@ -62,5 +66,15 @@ $(KEYS_BIN): $(KEYS_SRC) $(LIB) $(DOS_CC) $(CFLAGS) $< $(LIB) -o $@ $(DOS_EMBED_DPMI) $@ +$(JOY_BIN): $(JOY_SRC) $(LIB) + @mkdir -p $(dir $@) + $(DOS_CC) $(CFLAGS) $< $(LIB) -o $@ + $(DOS_EMBED_DPMI) $@ + +$(SPRITE_BIN): $(SPRITE_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 e950003..4843082 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -22,6 +22,10 @@ PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_BIN := $(BINDIR)/PATTERN KEYS_SRC := $(EXAMPLES)/keys/keys.c KEYS_BIN := $(BINDIR)/KEYS +JOY_SRC := $(EXAMPLES)/joy/joy.c +JOY_BIN := $(BINDIR)/JOY +SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c +SPRITE_BIN := $(BINDIR)/SPRITE DISK_IMG := $(BINDIR)/joey.2mg IIGS_PACKAGE := $(REPO_DIR)/toolchains/iigs/package-disk.sh @@ -33,7 +37,7 @@ IIX_INCLUDES := \ -I $(SRC_CORE) .PHONY: all iigs iigs-disk clean-iigs -all iigs: $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) +all iigs: $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) # iix-build.sh takes MAIN.c first, then EXTRA sources (compiled with # #pragma noroot). The example source supplies main(); libjoey sources @@ -55,13 +59,23 @@ $(KEYS_BIN): $(KEYS_SRC) $(LIB_SRCS) $(IIGS_BUILD) $(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS) $(IIGS_IIX) chtyp -t S16 $@ +$(JOY_BIN): $(JOY_SRC) $(LIB_SRCS) $(IIGS_BUILD) + @mkdir -p $(dir $@) + $(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS) + $(IIGS_IIX) chtyp -t S16 $@ + +$(SPRITE_BIN): $(SPRITE_SRC) $(LIB_SRCS) $(IIGS_BUILD) + @mkdir -p $(dir $@) + $(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(SPRITE_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) $(KEYS_BIN) $(IIGS_PACKAGE) +$(DISK_IMG): $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(IIGS_PACKAGE) @mkdir -p $(dir $@) - $(IIGS_PACKAGE) $@ $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) + $(IIGS_PACKAGE) $@ $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) clean-iigs: rm -rf $(BUILD) diff --git a/make/tools.mk b/make/tools.mk new file mode 100644 index 0000000..dfe10f6 --- /dev/null +++ b/make/tools.mk @@ -0,0 +1,26 @@ +# Host-side tools for the JoeyLib asset pipeline. +# +# These build with the host's default `cc` (no cross-toolchain) into +# build/tools/. Currently: +# joeyasset -- PPM (P6) -> .jas converter + +include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk + +BUILD_DIR := $(REPO_DIR)/build/tools +TOOLS_DIR := $(REPO_DIR)/tools + +HOST_CC ?= cc +HOST_CFLAGS := -std=c99 -Wall -Wextra -O2 + +JOEYASSET_SRC := $(TOOLS_DIR)/joeyasset/joeyasset.c +JOEYASSET_BIN := $(BUILD_DIR)/joeyasset + +.PHONY: all tools clean-tools +all tools: $(JOEYASSET_BIN) + +$(JOEYASSET_BIN): $(JOEYASSET_SRC) + @mkdir -p $(dir $@) + $(HOST_CC) $(HOST_CFLAGS) $< -o $@ + +clean-tools: + rm -rf $(BUILD_DIR) diff --git a/scripts/run-amiga.sh b/scripts/run-amiga.sh index e452b36..b7e99ff 100755 --- a/scripts/run-amiga.sh +++ b/scripts/run-amiga.sh @@ -30,7 +30,9 @@ case $prog in hello) file=Hello ;; pattern) file=Pattern ;; keys) file=Keys ;; - *) echo "usage: $0 [hello|pattern|keys]" >&2; exit 2 ;; + joy) file=Joy ;; + sprite) file=Sprite ;; + *) echo "usage: $0 [hello|pattern|keys|joy|sprite]" >&2; exit 2 ;; esac if [[ ! -f "$bin_dir/$file" ]]; then @@ -56,6 +58,8 @@ 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 +cp "$bin_dir/Joy" "$work/" 2>/dev/null || true +cp "$bin_dir/Sprite" "$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 433528e..f2a5eec 100755 --- a/scripts/run-atarist.sh +++ b/scripts/run-atarist.sh @@ -18,7 +18,9 @@ case $prog in hello) file=HELLO.PRG ;; pattern) file=PATTERN.PRG ;; keys) file=KEYS.PRG ;; - *) echo "usage: $0 [hello|pattern|keys]" >&2; exit 2 ;; + joy) file=JOY.PRG ;; + sprite) file=SPRITE.PRG ;; + *) echo "usage: $0 [hello|pattern|keys|joy|sprite]" >&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 b1cf932..46021e7 100755 --- a/scripts/run-dos.sh +++ b/scripts/run-dos.sh @@ -15,7 +15,9 @@ case $prog in hello) file=HELLO.EXE ;; pattern) file=PATTERN.EXE ;; keys) file=KEYS.EXE ;; - *) echo "usage: $0 [hello|pattern|keys]" >&2; exit 2 ;; + joy) file=JOY.EXE ;; + sprite) file=SPRITE.EXE ;; + *) echo "usage: $0 [hello|pattern|keys|joy|sprite]" >&2; exit 2 ;; esac if [[ ! -f "$bin_dir/$file" ]]; then diff --git a/scripts/run-iigs.sh b/scripts/run-iigs.sh index 005aebe..251e207 100755 --- a/scripts/run-iigs.sh +++ b/scripts/run-iigs.sh @@ -12,6 +12,8 @@ # scripts/run-iigs.sh # boots (Pattern hint) # scripts/run-iigs.sh hello # boots, hints HELLO # scripts/run-iigs.sh keys # boots, hints KEYS +# scripts/run-iigs.sh joy # boots, hints JOY +# scripts/run-iigs.sh sprite # boots, hints SPRITE set -euo pipefail @@ -25,8 +27,8 @@ data_disk=$repo/build/iigs/bin/joey.2mg null_c600=$repo/toolchains/emulators/support/iigs-null-c600.rom case $prog in - hello|pattern|keys) ;; - *) echo "usage: $0 [hello|pattern|keys]" >&2; exit 2 ;; + hello|pattern|keys|joy|sprite) ;; + *) echo "usage: $0 [hello|pattern|keys|joy|sprite]" >&2; exit 2 ;; esac for f in "$gsplus" "$rom" "$sys_disk" "$data_disk" "$null_c600"; do diff --git a/src/core/asset.c b/src/core/asset.c new file mode 100644 index 0000000..68e3b16 --- /dev/null +++ b/src/core/asset.c @@ -0,0 +1,158 @@ +// Asset loader: parses .jas blobs into JoeyAssetT, with companion +// helpers for file I/O and palette application. +// +// The .jas format is little-endian byte-stream so the same binary +// works on every target -- 6502/x86 (LE) read it directly, 68k (BE) +// reassembles via byte-by-byte decode in jasDecodeWord. + +#include +#include +#include + +#include "joey/asset.h" +#include "joey/palette.h" + +#define JAS_HEADER_SIZE 44 +#define JAS_PIXELS_OFFSET JAS_HEADER_SIZE +#define JAS_PALETTE_OFFSET 12 +#define JAS_PALETTE_ENTRIES 16 +#define JAS_MAX_BYTES (1L << 24) // 16MB sanity cap on file size + +// ----- Prototypes ----- + +static uint16_t jasDecodeWord(const uint8_t *p); +static bool jasMagicMatches(const uint8_t *data); + +// ----- Internal helpers ----- + +static uint16_t jasDecodeWord(const uint8_t *p) { + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + + +static bool jasMagicMatches(const uint8_t *data) { + return data[0] == 'J' && data[1] == 'A' && data[2] == 'S' && data[3] == '1'; +} + + +// ----- Public API (alphabetical) ----- + +void joeyAssetApplyPalette(SurfaceT *dst, uint8_t paletteIndex, const JoeyAssetT *asset) { + if (dst == NULL || asset == NULL) { + return; + } + if (!asset->hasPalette) { + return; + } + paletteSet(dst, paletteIndex, asset->palette); +} + + +void joeyAssetFree(JoeyAssetT *asset) { + if (asset == NULL) { + return; + } + // The const cast is intentional: loader-owned assets carry + // pixels we allocated. Static / embedded assets point at .rodata + // and the contract documents that they must not be passed here. + free((void *)asset->pixels); + free(asset); +} + + +JoeyAssetT *joeyAssetFromMem(const uint8_t *data, uint32_t length) { + JoeyAssetT *asset; + uint8_t *pixelBuf; + uint32_t rowBytes; + uint32_t pixelLen; + uint16_t i; + + if (data == NULL || length < JAS_HEADER_SIZE) { + return NULL; + } + if (!jasMagicMatches(data)) { + return NULL; + } + + asset = (JoeyAssetT *)malloc(sizeof(JoeyAssetT)); + if (asset == NULL) { + return NULL; + } + memset(asset, 0, sizeof(*asset)); + + asset->width = jasDecodeWord(data + 4); + asset->height = jasDecodeWord(data + 6); + asset->hasPalette = data[8] != 0; + for (i = 0; i < JAS_PALETTE_ENTRIES; i++) { + asset->palette[i] = jasDecodeWord(data + JAS_PALETTE_OFFSET + (i * 2)); + } + + if (asset->width == 0 || asset->height == 0) { + free(asset); + return NULL; + } + + rowBytes = (uint32_t)((asset->width + 1) >> 1); + pixelLen = rowBytes * (uint32_t)asset->height; + if (length < (JAS_PIXELS_OFFSET + pixelLen)) { + free(asset); + return NULL; + } + + pixelBuf = (uint8_t *)malloc(pixelLen); + if (pixelBuf == NULL) { + free(asset); + return NULL; + } + memcpy(pixelBuf, data + JAS_PIXELS_OFFSET, pixelLen); + asset->pixels = pixelBuf; + + return asset; +} + + +JoeyAssetT *joeyAssetLoadFile(const char *path) { + FILE *fp; + uint8_t *buf; + long size; + size_t readBytes; + JoeyAssetT *asset; + + if (path == NULL) { + return NULL; + } + fp = fopen(path, "rb"); + if (fp == NULL) { + return NULL; + } + + if (fseek(fp, 0L, SEEK_END) != 0) { + fclose(fp); + return NULL; + } + size = ftell(fp); + if (size <= 0 || size > JAS_MAX_BYTES) { + fclose(fp); + return NULL; + } + if (fseek(fp, 0L, SEEK_SET) != 0) { + fclose(fp); + return NULL; + } + + buf = (uint8_t *)malloc((size_t)size); + if (buf == NULL) { + fclose(fp); + return NULL; + } + readBytes = fread(buf, 1, (size_t)size, fp); + fclose(fp); + if (readBytes != (size_t)size) { + free(buf); + return NULL; + } + + asset = joeyAssetFromMem(buf, (uint32_t)size); + free(buf); + return asset; +} diff --git a/src/core/draw.c b/src/core/draw.c index cf4de9e..8bdecc6 100644 --- a/src/core/draw.c +++ b/src/core/draw.c @@ -12,11 +12,50 @@ // ----- Prototypes ----- -static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible); -static void fillRectClipped(SurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex); +static bool blitClip(int16_t *dstX, int16_t *dstY, int16_t *srcX, int16_t *srcY, int16_t *w, int16_t *h, int16_t srcW, int16_t srcH); +static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible); +static void fillRectClipped(SurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex); +static uint8_t srcPixel(const uint8_t *row, int16_t x); +static void dstPixel(uint8_t *row, int16_t x, uint8_t nibble); // ----- Internal helpers (alphabetical) ----- +// Clip a sprite blit to the destination surface. dst* is where the +// sprite would land at full size; src* is offset into the sprite's +// own pixel grid (incremented when clipping the left/top edges); w,h +// are reduced to fit the visible region. Returns false if the blit +// is entirely off-surface. +static bool blitClip(int16_t *dstX, int16_t *dstY, int16_t *srcX, int16_t *srcY, int16_t *w, int16_t *h, int16_t srcW, int16_t srcH) { + *srcX = 0; + *srcY = 0; + *w = srcW; + *h = srcH; + if (*w <= 0 || *h <= 0) { + return false; + } + if (*dstX < 0) { + *srcX = -(*dstX); + *w -= *srcX; + *dstX = 0; + } + if (*dstY < 0) { + *srcY = -(*dstY); + *h -= *srcY; + *dstY = 0; + } + if (*dstX >= SURFACE_WIDTH || *dstY >= SURFACE_HEIGHT) { + return false; + } + if (*dstX + *w > SURFACE_WIDTH) { + *w = SURFACE_WIDTH - *dstX; + } + if (*dstY + *h > SURFACE_HEIGHT) { + *h = SURFACE_HEIGHT - *dstY; + } + return (*w > 0 && *h > 0); +} + + // Clip a rectangle to the surface bounds. Outputs *outVisible = false // if the rect is entirely off-surface. static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible) { @@ -78,6 +117,29 @@ static void fillRectClipped(SurfaceT *s, int16_t x, int16_t y, int16_t w, int16_ } +static void dstPixel(uint8_t *row, int16_t x, uint8_t nibble) { + uint8_t *byte; + + byte = &row[x >> 1]; + if (x & 1) { + *byte = (uint8_t)((*byte & 0xF0) | nibble); + } else { + *byte = (uint8_t)((*byte & 0x0F) | (nibble << 4)); + } +} + + +static uint8_t srcPixel(const uint8_t *row, int16_t x) { + uint8_t byte; + + byte = row[x >> 1]; + if (x & 1) { + return (uint8_t)(byte & 0x0F); + } + return (uint8_t)(byte >> 4); +} + + // ----- Public API (alphabetical) ----- void drawPixel(SurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) { @@ -142,6 +204,75 @@ uint8_t samplePixel(const SurfaceT *s, int16_t x, int16_t y) { } +void surfaceBlit(SurfaceT *dst, const JoeyAssetT *src, int16_t x, int16_t y) { + int16_t srcX0; + int16_t srcY0; + int16_t copyW; + int16_t copyH; + int16_t srcRowBytes; + int16_t row; + int16_t col; + const uint8_t *srcRow; + uint8_t *dstRow; + uint8_t nibble; + + if (dst == NULL || src == NULL || src->pixels == NULL) { + return; + } + if (!blitClip(&x, &y, &srcX0, &srcY0, ©W, ©H, + (int16_t)src->width, (int16_t)src->height)) { + return; + } + + srcRowBytes = (int16_t)((src->width + 1) >> 1); + for (row = 0; row < copyH; row++) { + srcRow = &src->pixels[(srcY0 + row) * srcRowBytes]; + dstRow = &dst->pixels[(y + row) * SURFACE_BYTES_PER_ROW]; + for (col = 0; col < copyW; col++) { + nibble = srcPixel(srcRow, srcX0 + col); + dstPixel(dstRow, x + col, nibble); + } + } +} + + +void surfaceBlitMasked(SurfaceT *dst, const JoeyAssetT *src, int16_t x, int16_t y, uint8_t transparentIndex) { + int16_t srcX0; + int16_t srcY0; + int16_t copyW; + int16_t copyH; + int16_t srcRowBytes; + int16_t row; + int16_t col; + const uint8_t *srcRow; + uint8_t *dstRow; + uint8_t nibble; + uint8_t transparent; + + if (dst == NULL || src == NULL || src->pixels == NULL) { + return; + } + if (!blitClip(&x, &y, &srcX0, &srcY0, ©W, ©H, + (int16_t)src->width, (int16_t)src->height)) { + return; + } + + transparent = (uint8_t)(transparentIndex & 0x0F); + srcRowBytes = (int16_t)((src->width + 1) >> 1); + for (row = 0; row < copyH; row++) { + srcRow = &src->pixels[(srcY0 + row) * srcRowBytes]; + dstRow = &dst->pixels[(y + row) * SURFACE_BYTES_PER_ROW]; + for (col = 0; col < copyW; col++) { + nibble = srcPixel(srcRow, srcX0 + col); + if (nibble == transparent) { + continue; + } + dstPixel(dstRow, x + col, nibble); + } + } +} + + void surfaceClear(SurfaceT *s, uint8_t colorIndex) { uint8_t nibble; uint8_t doubled; diff --git a/src/core/hal.h b/src/core/hal.h index cfd583b..ce0c3d3 100644 --- a/src/core/hal.h +++ b/src/core/hal.h @@ -45,4 +45,9 @@ void halInputInit(void); void halInputShutdown(void); void halInputPoll(void); +// Block until the next display vertical blank. Each port implements +// this with whatever native wait the hardware provides (VGA $3DA, +// graphics.library WaitTOF, XBIOS Vsync, $C019 polling). +void halWaitVBL(void); + #endif diff --git a/src/core/init.c b/src/core/init.c index b4299b4..b9b22aa 100644 --- a/src/core/init.c +++ b/src/core/init.c @@ -95,3 +95,8 @@ void joeyShutdown(void) { const char *joeyVersionString(void) { return JOEYLIB_VERSION_STRING; } + + +void joeyWaitVBL(void) { + halWaitVBL(); +} diff --git a/src/core/input.c b/src/core/input.c index 73e33ee..acb81d4 100644 --- a/src/core/input.c +++ b/src/core/input.c @@ -1,12 +1,13 @@ -// Public input API. Maintains current/previous state buffers for both -// the keyboard (gKeyState/gKeyPrev) and the mouse buttons -// (gMouseButtonState/gMouseButtonPrev). joeyInputPoll snapshots the -// previous state, then asks the port HAL to refresh the current state; -// the predicates derive held/edge values from those two buffers. +// Public input API. Maintains current/previous state buffers for the +// keyboard (gKeyState/gKeyPrev), mouse buttons (gMouseButtonState/ +// gMouseButtonPrev), and joystick buttons (gJoyButtonState/ +// gJoyButtonPrev). joeyInputPoll snapshots the previous state, then +// asks the port HAL to refresh the current state; the predicates +// derive held/edge values from those two buffers. // -// Mouse position (gMouseX/gMouseY) is plain current state -- there is -// no "previous position" exposed; games that want delta movement can -// remember the previous joeyMouseX/Y themselves. +// Mouse position and joystick axes are plain current state with no +// edge predicates -- games that want deltas track the previous values +// themselves. #include @@ -22,10 +23,17 @@ int16_t gMouseY = 0; bool gMouseButtonState[MOUSE_BUTTON_COUNT]; bool gMouseButtonPrev [MOUSE_BUTTON_COUNT]; +bool gJoyConnected [JOYSTICK_COUNT]; +int8_t gJoyAxisX [JOYSTICK_COUNT]; +int8_t gJoyAxisY [JOYSTICK_COUNT]; +bool gJoyButtonState[JOYSTICK_COUNT][JOY_BUTTON_COUNT]; +bool gJoyButtonPrev [JOYSTICK_COUNT][JOY_BUTTON_COUNT]; + void joeyInputPoll(void) { memcpy(gKeyPrev, gKeyState, sizeof(gKeyState)); memcpy(gMouseButtonPrev, gMouseButtonState, sizeof(gMouseButtonState)); + memcpy(gJoyButtonPrev, gJoyButtonState, sizeof(gJoyButtonState)); halInputPoll(); } @@ -86,3 +94,60 @@ int16_t joeyMouseX(void) { int16_t joeyMouseY(void) { return gMouseY; } + + +bool joeyJoystickConnected(JoeyJoystickE js) { + if ((int)js < 0 || (int)js >= JOYSTICK_COUNT) { + return false; + } + return gJoyConnected[js]; +} + + +int8_t joeyJoystickX(JoeyJoystickE js) { + if ((int)js < 0 || (int)js >= JOYSTICK_COUNT) { + return 0; + } + return gJoyAxisX[js]; +} + + +int8_t joeyJoystickY(JoeyJoystickE js) { + if ((int)js < 0 || (int)js >= JOYSTICK_COUNT) { + return 0; + } + return gJoyAxisY[js]; +} + + +bool joeyJoyDown(JoeyJoystickE js, JoeyJoyButtonE button) { + if ((int)js < 0 || (int)js >= JOYSTICK_COUNT) { + return false; + } + if ((int)button < 0 || (int)button >= JOY_BUTTON_COUNT) { + return false; + } + return gJoyButtonState[js][button]; +} + + +bool joeyJoyPressed(JoeyJoystickE js, JoeyJoyButtonE button) { + if ((int)js < 0 || (int)js >= JOYSTICK_COUNT) { + return false; + } + if ((int)button < 0 || (int)button >= JOY_BUTTON_COUNT) { + return false; + } + return gJoyButtonState[js][button] && !gJoyButtonPrev[js][button]; +} + + +bool joeyJoyReleased(JoeyJoystickE js, JoeyJoyButtonE button) { + if ((int)js < 0 || (int)js >= JOYSTICK_COUNT) { + return false; + } + if ((int)button < 0 || (int)button >= JOY_BUTTON_COUNT) { + return false; + } + return !gJoyButtonState[js][button] && gJoyButtonPrev[js][button]; +} diff --git a/src/core/inputInternal.h b/src/core/inputInternal.h index 8e865bc..5b94418 100644 --- a/src/core/inputInternal.h +++ b/src/core/inputInternal.h @@ -1,11 +1,10 @@ // Internal input state shared between core and per-port HAL. // -// Per-port halInputPoll() writes directly into gKeyState[] and the -// gMouse* globals: set key entries to true for keys currently held, -// store pointer position in gMouseX/gMouseY (surface-space pixels), -// and set gMouseButtonState[] entries true for buttons currently held. -// The core compares against the *Prev shadow each joeyInputPoll to -// derive edge events. +// Per-port halInputPoll() writes directly into the public state +// globals: gKeyState[] for keyboard, gMouse* for mouse, gJoy* for +// joysticks. The core compares against the matching *Prev shadow +// each joeyInputPoll to derive edge events for keys/buttons. Mouse +// position and joystick axes have no edge semantics, so no shadow. #ifndef JOEYLIB_INPUT_INTERNAL_H #define JOEYLIB_INPUT_INTERNAL_H @@ -21,4 +20,10 @@ extern int16_t gMouseY; extern bool gMouseButtonState[MOUSE_BUTTON_COUNT]; extern bool gMouseButtonPrev [MOUSE_BUTTON_COUNT]; +extern bool gJoyConnected[JOYSTICK_COUNT]; +extern int8_t gJoyAxisX [JOYSTICK_COUNT]; +extern int8_t gJoyAxisY [JOYSTICK_COUNT]; +extern bool gJoyButtonState[JOYSTICK_COUNT][JOY_BUTTON_COUNT]; +extern bool gJoyButtonPrev [JOYSTICK_COUNT][JOY_BUTTON_COUNT]; + #endif diff --git a/src/port/amiga/hal.c b/src/port/amiga/hal.c index f6ad6c5..bd5b9ba 100644 --- a/src/port/amiga/hal.c +++ b/src/port/amiga/hal.c @@ -433,6 +433,14 @@ void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint1 } +// WaitTOF() blocks the calling task until the next "top of frame" +// VBlank interrupt -- 50 Hz on PAL, 60 Hz on NTSC. graphics.library +// is auto-opened by libnix so no extra plumbing is needed. +void halWaitVBL(void) { + WaitTOF(); +} + + void halShutdown(void) { if (gScreen != NULL) { // CloseScreen should free attached UCopList, but be explicit diff --git a/src/port/amiga/input.c b/src/port/amiga/input.c index 0d39dbe..ee28cc3 100644 --- a/src/port/amiga/input.c +++ b/src/port/amiga/input.c @@ -24,9 +24,11 @@ #include #include #include +#include #include #include +#include #include "hal.h" #include "inputInternal.h" @@ -44,6 +46,7 @@ extern struct Screen *gScreen; // ----- Prototypes ----- static void drainMessages(void); +static void pollJoysticks(void); // ----- Module state ----- @@ -111,7 +114,15 @@ static const uint8_t gRawKeyToKey[AMIGA_KEY_TABLE_SIZE] = { [0x64] = KEY_LALT, }; -static struct Window *gWindow = NULL; +static struct Window *gWindow = NULL; +static struct Library *gLowLevelBase = NULL; + +// AmigaOS exposes joystick port 1 (the dedicated stick) via lowlevel +// ReadJoyPort(1); port 0 shares the mouse port. We map our public +// JOYSTICK_0 to Amiga port 1 so the primary stick is what retro games +// expect. +#define AMIGA_PORT_FOR_JS0 1 +#define AMIGA_PORT_FOR_JS1 0 // ----- Internal helpers ----- @@ -169,6 +180,50 @@ static void drainMessages(void) { } +// Read both joystick ports via lowlevel.library. Devices that report +// JP_TYPE_JOYSTK or JP_TYPE_GAMECTLR populate gJoy*; anything else +// (mouse on port 0, no device, unknown) leaves the stick disconnected +// with zeroed state. +static void pollJoysticks(void) { + static const int amigaPortForStick[JOYSTICK_COUNT] = { + AMIGA_PORT_FOR_JS0, + AMIGA_PORT_FOR_JS1 + }; + ULONG state; + ULONG type; + int stick; + + if (gLowLevelBase == NULL) { + return; + } + for (stick = 0; stick < JOYSTICK_COUNT; stick++) { + state = ReadJoyPort((UWORD)amigaPortForStick[stick]); + type = state & JP_TYPE_MASK; + if (type != JP_TYPE_JOYSTK && type != JP_TYPE_GAMECTLR) { + gJoyConnected[stick] = false; + gJoyAxisX[stick] = 0; + gJoyAxisY[stick] = 0; + gJoyButtonState[stick][JOY_BUTTON_0] = false; + gJoyButtonState[stick][JOY_BUTTON_1] = false; + continue; + } + gJoyConnected[stick] = true; + + gJoyAxisX[stick] = 0; + if (state & JPF_JOY_LEFT) { gJoyAxisX[stick] = JOYSTICK_AXIS_MIN; } + if (state & JPF_JOY_RIGHT) { gJoyAxisX[stick] = JOYSTICK_AXIS_MAX; } + gJoyAxisY[stick] = 0; + if (state & JPF_JOY_UP) { gJoyAxisY[stick] = JOYSTICK_AXIS_MIN; } + if (state & JPF_JOY_DOWN) { gJoyAxisY[stick] = JOYSTICK_AXIS_MAX; } + + // Standard 1-button stick reports JPF_BUTTON_RED only. CD32 + // game controllers also report BLUE for the second face button. + gJoyButtonState[stick][JOY_BUTTON_0] = (state & JPF_BUTTON_RED) != 0; + gJoyButtonState[stick][JOY_BUTTON_1] = (state & JPF_BUTTON_BLUE) != 0; + } +} + + // ----- HAL API (alphabetical) ----- void halInputInit(void) { @@ -200,15 +255,25 @@ void halInputInit(void) { gMouseX = (int16_t)(gWindow->Width / 2); gMouseY = (int16_t)(gWindow->Height / 2); } + + // lowlevel.library shipped with AmigaOS 2.05+. If absent (very old + // Kickstart, or stripped AROS), the joystick API silently reports + // disconnected sticks rather than failing init. + gLowLevelBase = OpenLibrary((CONST_STRPTR)"lowlevel.library", 0); } void halInputPoll(void) { drainMessages(); + pollJoysticks(); } void halInputShutdown(void) { + if (gLowLevelBase != NULL) { + CloseLibrary(gLowLevelBase); + gLowLevelBase = NULL; + } if (gWindow == NULL) { return; } diff --git a/src/port/atarist/hal.c b/src/port/atarist/hal.c index 20e30dd..1fe88e1 100644 --- a/src/port/atarist/hal.c +++ b/src/port/atarist/hal.c @@ -497,6 +497,13 @@ void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint1 } +// Vsync() is XBIOS opcode 37; mintlib exposes it directly. It blocks +// until the next 50 Hz (PAL) or 60 Hz (NTSC) vertical blank. +void halWaitVBL(void) { + Vsync(); +} + + void halShutdown(void) { if (!gModeSet) { return; diff --git a/src/port/atarist/input.c b/src/port/atarist/input.c index 5a639ed..7411dd5 100644 --- a/src/port/atarist/input.c +++ b/src/port/atarist/input.c @@ -51,10 +51,26 @@ #define MOUSE_HDR_LEFT_BTN 0x02 #define MOUSE_HDR_RIGHT_BTN 0x01 +// IKBD joystick byte: bit set = direction held / fire pressed. +#define JOY_BIT_UP 0x01 +#define JOY_BIT_DOWN 0x02 +#define JOY_BIT_LEFT 0x04 +#define JOY_BIT_RIGHT 0x08 +#define JOY_BIT_FIRE 0x80 + // Packet kinds for the ISR's small state machine. #define PKT_KIND_NONE 0 #define PKT_KIND_DISCARD 1 #define PKT_KIND_REL_MOUSE 2 +#define PKT_KIND_JOY0 3 // 0xFE -- 1 byte for ST joy port 0 (mouse port) +#define PKT_KIND_JOY1 4 // 0xFF -- 1 byte for ST joy port 1 (dedicated) +#define PKT_KIND_JOY_BOTH 5 // 0xFD -- 2 bytes (joy0 then joy1) + +// Mapping from ST joystick port -> JoeyJoystickE. Port 1 is the +// dedicated stick that retro games use; expose it as the primary +// JOYSTICK_0. Port 0 (mouse port) becomes JOYSTICK_1. +#define ST_PORT0_AS_JS JOYSTICK_1 +#define ST_PORT1_AS_JS JOYSTICK_0 // ----- Prototypes ----- @@ -146,6 +162,11 @@ static volatile uint8_t gIsrMouseButtons = 0; static int16_t gMouseAbsX = SURFACE_WIDTH / 2; static int16_t gMouseAbsY = SURFACE_HEIGHT / 2; +// Latched joystick state, written by the IKBD ISR. The byte stays +// unchanged between packets while a direction or fire is held, so +// halInputPoll can simply read the latest value. +static volatile uint8_t gIsrJoyByte[JOYSTICK_COUNT]; + // ----- Internal helpers ----- // Runs in MFP ACIA interrupt context. Reads one byte from the ACIA, @@ -160,15 +181,35 @@ static long ikbdHandler(void) { byte = *ST_ACIA_DATA; if (gPacketRemaining != 0) { - if (gPacketKind == PKT_KIND_REL_MOUSE) { - // mouse-packet payload: byte 0 = dx (signed), byte 1 = dy - if (gMousePacketByte == 0) { - gIsrMouseDx += (int8_t)byte; - gMousePacketByte = 1; - } else { - gIsrMouseDy += (int8_t)byte; - gMousePacketByte = 0; - } + switch (gPacketKind) { + case PKT_KIND_REL_MOUSE: + // mouse-packet payload: byte 0 = dx, byte 1 = dy + if (gMousePacketByte == 0) { + gIsrMouseDx += (int8_t)byte; + gMousePacketByte = 1; + } else { + gIsrMouseDy += (int8_t)byte; + gMousePacketByte = 0; + } + break; + case PKT_KIND_JOY0: + gIsrJoyByte[ST_PORT0_AS_JS] = byte; + break; + case PKT_KIND_JOY1: + gIsrJoyByte[ST_PORT1_AS_JS] = byte; + break; + case PKT_KIND_JOY_BOTH: + // 0xFD payload is joy0 then joy1. + if (gMousePacketByte == 0) { + gIsrJoyByte[ST_PORT0_AS_JS] = byte; + gMousePacketByte = 1; + } else { + gIsrJoyByte[ST_PORT1_AS_JS] = byte; + gMousePacketByte = 0; + } + break; + default: + break; } gPacketRemaining = (uint8_t)(gPacketRemaining - 1); if (gPacketRemaining == 0) { @@ -192,10 +233,15 @@ static long ikbdHandler(void) { break; case PKT_JOY_BOTH: gPacketRemaining = 2; + gPacketKind = PKT_KIND_JOY_BOTH; break; case PKT_JOY0: + gPacketRemaining = 1; + gPacketKind = PKT_KIND_JOY0; + break; case PKT_JOY1: gPacketRemaining = 1; + gPacketKind = PKT_KIND_JOY1; break; default: if (byte >= PKT_REL_MOUSE_MIN && byte <= PKT_REL_MOUSE_MAX) { @@ -236,9 +282,10 @@ static long restoreIkbdVector(void) { // ----- HAL API (alphabetical) ----- void halInputInit(void) { - memset(gKeyState, 0, sizeof(gKeyState)); - memset(gKeyPrev, 0, sizeof(gKeyPrev)); - memset((void *)gIsrState, 0, sizeof(gIsrState)); + memset(gKeyState, 0, sizeof(gKeyState)); + memset(gKeyPrev, 0, sizeof(gKeyPrev)); + memset((void *)gIsrState, 0, sizeof(gIsrState)); + memset((void *)gIsrJoyByte, 0, sizeof(gIsrJoyByte)); gMouseAbsX = SURFACE_WIDTH / 2; gMouseAbsY = SURFACE_HEIGHT / 2; @@ -248,6 +295,11 @@ void halInputInit(void) { gIsrMouseDy = 0; gIsrMouseButtons = 0; + // Both ports always announce themselves on the ST -- packets + // arrive even when no stick is plugged in (state stays at zero). + gJoyConnected[JOYSTICK_0] = true; + gJoyConnected[JOYSTICK_1] = true; + gKbdvecs = (_KBDVECS *)Kbdvbase(); gPacketRemaining = 0; gPacketKind = PKT_KIND_NONE; @@ -256,16 +308,18 @@ void halInputInit(void) { } -// The ACIA ISR writes gIsrState (keys) and the gIsrMouse* deltas at -// ~100 Hz max; the ~60-byte memcpy is essentially never racing a write. -// Worst case is a single key or one mouse packet lagging one frame -- -// well under perceptible. +// The ACIA ISR writes gIsrState (keys), gIsrMouse* deltas, and +// gIsrJoyByte[] at ~100 Hz max; the ~60-byte memcpy is essentially +// never racing a write. Worst case is a single key or one packet +// lagging one frame -- well under perceptible. void halInputPoll(void) { int32_t dx; int32_t dy; int32_t newX; int32_t newY; uint8_t btn; + uint8_t joy; + uint16_t i; memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState)); @@ -291,6 +345,20 @@ void halInputPoll(void) { gMouseButtonState[MOUSE_BUTTON_LEFT] = (btn & MOUSE_HDR_LEFT_BTN) != 0; gMouseButtonState[MOUSE_BUTTON_RIGHT] = (btn & MOUSE_HDR_RIGHT_BTN) != 0; gMouseButtonState[MOUSE_BUTTON_MIDDLE] = false; + + // Decode latest joystick byte for each stick. Direction is digital + // on the ST, so axes snap to the JOYSTICK_AXIS_* extremes. + for (i = 0; i < JOYSTICK_COUNT; i++) { + joy = gIsrJoyByte[i]; + gJoyAxisX[i] = 0; + gJoyAxisY[i] = 0; + if (joy & JOY_BIT_LEFT) { gJoyAxisX[i] = JOYSTICK_AXIS_MIN; } + if (joy & JOY_BIT_RIGHT) { gJoyAxisX[i] = JOYSTICK_AXIS_MAX; } + if (joy & JOY_BIT_UP) { gJoyAxisY[i] = JOYSTICK_AXIS_MIN; } + if (joy & JOY_BIT_DOWN) { gJoyAxisY[i] = JOYSTICK_AXIS_MAX; } + gJoyButtonState[i][JOY_BUTTON_0] = (joy & JOY_BIT_FIRE) != 0; + gJoyButtonState[i][JOY_BUTTON_1] = false; // ST has 1 fire button + } } diff --git a/src/port/dos/hal.c b/src/port/dos/hal.c index a67151b..58f7f79 100644 --- a/src/port/dos/hal.c +++ b/src/port/dos/hal.c @@ -30,6 +30,8 @@ #define VGA_STRIDE 320u #define DAC_INDEX_PORT 0x3C8 #define DAC_DATA_PORT 0x3C9 +#define VGA_INPUT_STAT_1 0x3DA +#define VGA_VRETRACE_BIT 0x08 // ----- Prototypes ----- @@ -231,6 +233,22 @@ void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint1 } +// VGA mode 13h vertical refresh on a real CRT runs at ~70 Hz. We +// detect the start of vertical retrace by polling input status +// register 1 ($3DA) bit 3: 1 = currently in vretrace. To get a +// rising-edge wait (so back-to-back calls each yield one frame), +// first wait while we're already in retrace, then wait until we +// re-enter retrace. +void halWaitVBL(void) { + while (inportb(VGA_INPUT_STAT_1) & VGA_VRETRACE_BIT) { + /* spin while still in current retrace */; + } + while (!(inportb(VGA_INPUT_STAT_1) & VGA_VRETRACE_BIT)) { + /* spin until next retrace begins */; + } +} + + void halShutdown(void) { __dpmi_regs regs; diff --git a/src/port/dos/input.c b/src/port/dos/input.c index f403bdc..201880f 100644 --- a/src/port/dos/input.c +++ b/src/port/dos/input.c @@ -56,11 +56,34 @@ #define MOUSE_BTN_RIGHT 0x0002 #define MOUSE_BTN_MIDDLE 0x0004 +// INT 15h game-port BIOS functions: +// AH=84h, DX=0: read switches (AL bits 4-7 = buttons, 0=pressed) +// AH=84h, DX=1: read positions (AX/BX/CX/DX = stickA X/Y, stickB X/Y) +// The position values are raw R-C timer counts; magnitude depends on +// the joystick's potentiometers and the host BIOS calibration. We +// threshold around a nominal center to derive digital direction; for +// DOSBox-mapped sticks 128/384 covers the default emulated range and +// gives clean L/R/U/D snaps. +#define BIOS_INT 0x15 +#define BIOS_FN_GAMEPORT 0x84 +#define BIOS_GAMEPORT_DX_SWITCHES 0x0000 +#define BIOS_GAMEPORT_DX_POS 0x0001 +#define BIOS_FLAGS_CARRY 0x0001 +#define BIOS_BTN_A1 0x10 +#define BIOS_BTN_A2 0x20 +#define BIOS_BTN_B1 0x40 +#define BIOS_BTN_B2 0x80 +#define JOY_AXIS_LO_THRESHOLD 128 +#define JOY_AXIS_HI_THRESHOLD 384 + // ----- Prototypes ----- -static void keyboardIsr(void); -static bool mouseInit(void); -static void mousePoll(void); +static bool joystickInit(void); +static void joystickPoll(void); +static void keyboardIsr(void); +static bool mouseInit(void); +static void mousePoll(void); +static int8_t scaleAxis(uint16_t v); // ----- Module state ----- @@ -131,7 +154,8 @@ static _go32_dpmi_seginfo gNewHandler; static bool gHooked = false; static volatile bool gIsrState[KEY_COUNT]; -static bool gMousePresent = false; +static bool gMousePresent = false; +static bool gJoystickPresent = false; // ----- Internal helpers ----- @@ -192,6 +216,74 @@ static bool mouseInit(void) { } +static int8_t scaleAxis(uint16_t v) { + if (v < JOY_AXIS_LO_THRESHOLD) { + return JOYSTICK_AXIS_MIN; + } + if (v > JOY_AXIS_HI_THRESHOLD) { + return JOYSTICK_AXIS_MAX; + } + return 0; +} + + +// Detect a game-port BIOS handler. Some BIOSes return CF=1 for "no +// joystick port" -- we treat that as no joystick. DOSBox-Staging with +// any host stick mapped will return CF=0. +static bool joystickInit(void) { + __dpmi_regs r; + + memset(&r, 0, sizeof(r)); + r.h.ah = BIOS_FN_GAMEPORT; + r.x.dx = BIOS_GAMEPORT_DX_SWITCHES; + __dpmi_int(BIOS_INT, &r); + if (r.x.flags & BIOS_FLAGS_CARRY) { + return false; + } + return true; +} + + +static void joystickPoll(void) { + __dpmi_regs r; + uint8_t btn; + + if (!gJoystickPresent) { + return; + } + + memset(&r, 0, sizeof(r)); + r.h.ah = BIOS_FN_GAMEPORT; + r.x.dx = BIOS_GAMEPORT_DX_SWITCHES; + __dpmi_int(BIOS_INT, &r); + if (r.x.flags & BIOS_FLAGS_CARRY) { + gJoystickPresent = false; + return; + } + btn = r.h.al; + // Active-low: bit set in AL means button is *not* pressed. + gJoyButtonState[JOYSTICK_0][JOY_BUTTON_0] = (btn & BIOS_BTN_A1) == 0; + gJoyButtonState[JOYSTICK_0][JOY_BUTTON_1] = (btn & BIOS_BTN_A2) == 0; + gJoyButtonState[JOYSTICK_1][JOY_BUTTON_0] = (btn & BIOS_BTN_B1) == 0; + gJoyButtonState[JOYSTICK_1][JOY_BUTTON_1] = (btn & BIOS_BTN_B2) == 0; + + memset(&r, 0, sizeof(r)); + r.h.ah = BIOS_FN_GAMEPORT; + r.x.dx = BIOS_GAMEPORT_DX_POS; + __dpmi_int(BIOS_INT, &r); + if (r.x.flags & BIOS_FLAGS_CARRY) { + return; + } + gJoyAxisX[JOYSTICK_0] = scaleAxis(r.x.ax); + gJoyAxisY[JOYSTICK_0] = scaleAxis(r.x.bx); + gJoyAxisX[JOYSTICK_1] = scaleAxis(r.x.cx); + gJoyAxisY[JOYSTICK_1] = scaleAxis(r.x.dx); + + gJoyConnected[JOYSTICK_0] = true; + gJoyConnected[JOYSTICK_1] = true; +} + + static void mousePoll(void) { __dpmi_regs r; uint16_t btn; @@ -232,7 +324,8 @@ void halInputInit(void) { _go32_dpmi_set_protected_mode_interrupt_vector(9, &gNewHandler); gHooked = true; - gMousePresent = mouseInit(); + gMousePresent = mouseInit(); + gJoystickPresent = joystickInit(); } @@ -241,6 +334,7 @@ void halInputPoll(void) { memcpy(gKeyState, (const void *)gIsrState, sizeof(gKeyState)); enable(); mousePoll(); + joystickPoll(); } diff --git a/src/port/iigs/hal.c b/src/port/iigs/hal.c index b519f8a..b112df1 100644 --- a/src/port/iigs/hal.c +++ b/src/port/iigs/hal.c @@ -24,10 +24,13 @@ // ----- Hardware addresses (24-bit / long pointers) ----- #define IIGS_NEWVIDEO_REG ((volatile uint8_t *)0x00C029L) +#define IIGS_VBL_STATUS ((volatile uint8_t *)0x00C019L) #define IIGS_SHR_PIXELS ((uint8_t *)0xE12000L) #define IIGS_SHR_SCB ((uint8_t *)0xE19D00L) #define IIGS_SHR_PALETTE ((uint16_t *)0xE19E00L) +#define VBL_BAR_BIT 0x80 + // NEWVIDEO bit masks #define NEWVIDEO_SHR_ON 0x80 #define NEWVIDEO_LINEARIZE 0x40 @@ -100,6 +103,20 @@ void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint1 } +// $C019 RDVBLBAR: bit 7 = 0 during vertical blank, 1 during active +// scan. To produce a rising-edge wait (one VBL per call), first spin +// while VBL is currently active (bit 7 = 0), then spin until VBL +// fires again (bit 7 returns to 0). The IIgs SHR refresh is 60 Hz. +void halWaitVBL(void) { + while ((*IIGS_VBL_STATUS & VBL_BAR_BIT) == 0) { + /* already in VBL: wait for active scan */; + } + while ((*IIGS_VBL_STATUS & VBL_BAR_BIT) != 0) { + /* scanning: wait for next VBL */; + } +} + + void halShutdown(void) { if (gModeSet) { *IIGS_NEWVIDEO_REG = gPreviousNewVideo; diff --git a/src/port/iigs/input.c b/src/port/iigs/input.c index bdcea94..18089d1 100644 --- a/src/port/iigs/input.c +++ b/src/port/iigs/input.c @@ -45,6 +45,18 @@ #define IIGS_MODIFIERS ((volatile uint8_t *)0x00C025L) #define IIGS_KMSTATUS ((volatile uint8_t *)0x00C027L) +// Joystick / paddle softswitches. +#define IIGS_BTN0 ((volatile uint8_t *)0x00C061L) +#define IIGS_BTN1 ((volatile uint8_t *)0x00C062L) +#define IIGS_PADDLE0 ((volatile uint8_t *)0x00C064L) +#define IIGS_PADDLE1 ((volatile uint8_t *)0x00C065L) +#define IIGS_PTRIG ((volatile uint8_t *)0x00C070L) +#define IIGS_BUTTON_BIT 0x80 +#define IIGS_PADDLE_BUSY 0x80 +#define PADDLE_TIMEOUT 256 +#define PADDLE_LO_THRESHOLD 64 +#define PADDLE_HI_THRESHOLD 192 + #define KBD_STROBE_BIT 0x80 #define KBD_ASCII_MASK 0x7F @@ -85,9 +97,11 @@ // ----- Prototypes ----- static void buildAsciiTable(void); +static void pollJoystick(void); static void pollMouse(void); static void readModifierKeys(void); static int8_t signExtend7(uint8_t raw); +static int8_t thresholdPaddle(uint8_t v); // ----- Module state ----- @@ -152,6 +166,68 @@ static int8_t signExtend7(uint8_t raw) { } +// Threshold a 0..255 paddle reading into a digital direction so the +// IIgs analog stick presents the same axis semantics as the digital +// sticks on ST/Amiga/DOS. Center range is treated as zero. +static int8_t thresholdPaddle(uint8_t v) { + if (v < PADDLE_LO_THRESHOLD) { + return JOYSTICK_AXIS_MIN; + } + if (v > PADDLE_HI_THRESHOLD) { + return JOYSTICK_AXIS_MAX; + } + return 0; +} + + +// Read the Apple IIgs joystick (paddle 0/1 + buttons 0/1). Buttons at +// $C061/$C062 are tied to the Open-Apple/Closed-Apple keys, so holding +// either modifier key looks like a fire press -- intentional Apple +// behavior, accept it. Only one stick is exposed; the IIgs second +// "stick" wiring (paddles 2/3) is rarely used by retro games. +// +// Each paddle read triggers an RC scan via $C070 and then polls the +// paddle softswitch until bit 7 clears; the iteration count +// approximates the paddle's 0..255 position (the Apple firmware +// PREAD routine works the same way). The two reads are inlined here +// rather than factored into a helper because ORCA/C 2.1 trips over +// `volatile uint8_t *` function parameters. +static void pollJoystick(void) { + uint16_t count; + uint8_t px; + uint8_t py; + uint8_t byte; + + byte = *IIGS_PTRIG; + px = 255; + for (count = 0; count < PADDLE_TIMEOUT; count++) { + byte = *IIGS_PADDLE0; + if ((byte & IIGS_PADDLE_BUSY) == 0) { + px = (uint8_t)count; + break; + } + } + + byte = *IIGS_PTRIG; + py = 255; + for (count = 0; count < PADDLE_TIMEOUT; count++) { + byte = *IIGS_PADDLE1; + if ((byte & IIGS_PADDLE_BUSY) == 0) { + py = (uint8_t)count; + break; + } + } + + gJoyAxisX[JOYSTICK_0] = thresholdPaddle(px); + gJoyAxisY[JOYSTICK_0] = thresholdPaddle(py); + gJoyButtonState[JOYSTICK_0][JOY_BUTTON_0] = (*IIGS_BTN0 & IIGS_BUTTON_BIT) != 0; + gJoyButtonState[JOYSTICK_0][JOY_BUTTON_1] = (*IIGS_BTN1 & IIGS_BUTTON_BIT) != 0; + + gJoyConnected[JOYSTICK_0] = true; + gJoyConnected[JOYSTICK_1] = false; +} + + // Drain one X+Y delta pair from the ADB mouse FIFO. $C027 bit 1 tells // us which coordinate the next $C024 read will return; we honor that // rather than assuming an order, so we stay in sync even if a stray @@ -241,6 +317,7 @@ void halInputPoll(void) { readModifierKeys(); pollMouse(); + pollJoystick(); } diff --git a/tools/joeyasset/joeyasset.c b/tools/joeyasset/joeyasset.c new file mode 100644 index 0000000..2e9df70 --- /dev/null +++ b/tools/joeyasset/joeyasset.c @@ -0,0 +1,306 @@ +// joeyasset: convert a PPM (P6) bitmap into JoeyLib's .jas asset +// format. Designed for hosts with a C99 compiler; no external library +// dependencies. +// +// PPM is a stable, trivially parseable RGB format. ImageMagick / GIMP / +// Photoshop / paint.net all export it directly, so the conversion +// pipeline from a PNG is one extra command: +// +// convert sprite.png sprite.ppm # ImageMagick / GraphicsMagick +// joeyasset sprite.ppm sprite.jas +// +// Quantization rule for v1: every input RGB triple is reduced to the +// IIgs-style 4-bit-per-channel $0RGB form by taking the high nibble of +// each channel. After that reduction the input is required to use no +// more than 16 distinct $0RGB colors. We do NOT dither or do real +// colorspace quantization yet -- inputs are expected to already use a +// 16-color palette. If you need dithering, run the input through a +// quantizer (gimp, pngquant, etc.) first. +// +// Output format is documented in include/joey/asset.h. + +#include +#include +#include +#include +#include +#include + +#define JAS_MAGIC0 'J' +#define JAS_MAGIC1 'A' +#define JAS_MAGIC2 'S' +#define JAS_MAGIC3 '1' + +#define JAS_PALETTE_ENTRIES 16 +#define JAS_HEADER_SIZE 44 + +#define PPM_TOKEN_MAX 64 + +static int parsePpmToken(FILE *fp, char *out, int outLen); +static int loadPpm(const char *path, int *outWidth, int *outHeight, uint8_t **outPixels); +static int buildPalette(const uint8_t *rgb, int width, int height, uint16_t *outPalette, uint8_t *outIndices); +static int writeJas(const char *path, int width, int height, const uint16_t *palette, const uint8_t *indices); +static void writeLE16(uint8_t *p, uint16_t v); + + +static void writeLE16(uint8_t *p, uint16_t v) { + p[0] = (uint8_t)(v & 0xFF); + p[1] = (uint8_t)((v >> 8) & 0xFF); +} + + +// Reads a single whitespace-separated token from a PPM header, +// skipping comments (# to end-of-line). +static int parsePpmToken(FILE *fp, char *out, int outLen) { + int c; + int pos; + + pos = 0; + for (;;) { + c = fgetc(fp); + if (c == EOF) { + return -1; + } + if (isspace(c)) { + continue; + } + if (c == '#') { + while ((c = fgetc(fp)) != EOF && c != '\n') { + /* skip */; + } + continue; + } + break; + } + while (c != EOF && !isspace(c) && c != '#') { + if (pos < outLen - 1) { + out[pos++] = (char)c; + } + c = fgetc(fp); + } + out[pos] = 0; + return 0; +} + + +static int loadPpm(const char *path, int *outWidth, int *outHeight, uint8_t **outPixels) { + FILE *fp; + char tok[PPM_TOKEN_MAX]; + int width; + int height; + int maxval; + size_t pixelBytes; + uint8_t *buf; + size_t read; + + fp = fopen(path, "rb"); + if (fp == NULL) { + fprintf(stderr, "joeyasset: cannot open %s: %s\n", path, strerror(errno)); + return 1; + } + if (parsePpmToken(fp, tok, sizeof(tok)) != 0 || strcmp(tok, "P6") != 0) { + fprintf(stderr, "joeyasset: %s is not a PPM (P6) file\n", path); + fclose(fp); + return 1; + } + if (parsePpmToken(fp, tok, sizeof(tok)) != 0) { + fclose(fp); + return 1; + } + width = atoi(tok); + if (parsePpmToken(fp, tok, sizeof(tok)) != 0) { + fclose(fp); + return 1; + } + height = atoi(tok); + if (parsePpmToken(fp, tok, sizeof(tok)) != 0) { + fclose(fp); + return 1; + } + maxval = atoi(tok); + if (width <= 0 || height <= 0) { + fprintf(stderr, "joeyasset: %s has non-positive dimensions\n", path); + fclose(fp); + return 1; + } + if (maxval != 255) { + fprintf(stderr, "joeyasset: %s maxval %d unsupported (must be 255)\n", path, maxval); + fclose(fp); + return 1; + } + // PPM raster begins after exactly one whitespace byte following the + // maxval token (already consumed by parsePpmToken's trailing read). + // Re-read one byte? No -- parsePpmToken stops after consuming the + // whitespace, so we are positioned at the first raster byte. + + pixelBytes = (size_t)width * (size_t)height * 3u; + buf = (uint8_t *)malloc(pixelBytes); + if (buf == NULL) { + fprintf(stderr, "joeyasset: out of memory (%zu bytes)\n", pixelBytes); + fclose(fp); + return 1; + } + read = fread(buf, 1, pixelBytes, fp); + fclose(fp); + if (read != pixelBytes) { + fprintf(stderr, "joeyasset: short raster in %s (got %zu, need %zu)\n", + path, read, pixelBytes); + free(buf); + return 1; + } + + *outWidth = width; + *outHeight = height; + *outPixels = buf; + return 0; +} + + +// Reduce every input RGB triple to a 12-bit $0RGB color. Up to 16 +// distinct colors are accepted; the first color encountered is +// assigned palette index 0, the second index 1, etc. Returns the +// number of distinct colors found, or -1 if more than 16. +static int buildPalette(const uint8_t *rgb, int width, int height, uint16_t *outPalette, uint8_t *outIndices) { + int total; + int paletteCount; + int i; + int j; + uint8_t r; + uint8_t g; + uint8_t b; + uint16_t color; + + total = width * height; + paletteCount = 0; + for (i = 0; i < JAS_PALETTE_ENTRIES; i++) { + outPalette[i] = 0x0000; + } + for (i = 0; i < total; i++) { + r = rgb[i * 3 + 0] >> 4; + g = rgb[i * 3 + 1] >> 4; + b = rgb[i * 3 + 2] >> 4; + color = (uint16_t)((r << 8) | (g << 4) | b); + for (j = 0; j < paletteCount; j++) { + if (outPalette[j] == color) { + break; + } + } + if (j == paletteCount) { + if (paletteCount >= JAS_PALETTE_ENTRIES) { + return -1; + } + outPalette[paletteCount] = color; + paletteCount++; + } + outIndices[i] = (uint8_t)j; + } + return paletteCount; +} + + +static int writeJas(const char *path, int width, int height, const uint16_t *palette, const uint8_t *indices) { + FILE *fp; + uint8_t header[JAS_HEADER_SIZE]; + int rowBytes; + int x; + int y; + uint8_t byte; + int written; + + memset(header, 0, sizeof(header)); + header[0] = JAS_MAGIC0; + header[1] = JAS_MAGIC1; + header[2] = JAS_MAGIC2; + header[3] = JAS_MAGIC3; + writeLE16(header + 4, (uint16_t)width); + writeLE16(header + 6, (uint16_t)height); + header[8] = 1; + for (x = 0; x < JAS_PALETTE_ENTRIES; x++) { + writeLE16(header + 12 + (x * 2), palette[x]); + } + + fp = fopen(path, "wb"); + if (fp == NULL) { + fprintf(stderr, "joeyasset: cannot create %s: %s\n", path, strerror(errno)); + return 1; + } + if (fwrite(header, 1, sizeof(header), fp) != sizeof(header)) { + fprintf(stderr, "joeyasset: short write to %s\n", path); + fclose(fp); + return 1; + } + + rowBytes = (width + 1) >> 1; + written = 0; + for (y = 0; y < height; y++) { + for (x = 0; x < rowBytes; x++) { + int p0; + int p1; + p0 = x * 2; + p1 = p0 + 1; + byte = (uint8_t)((indices[y * width + p0] & 0x0F) << 4); + if (p1 < width) { + byte = (uint8_t)(byte | (indices[y * width + p1] & 0x0F)); + } + if (fputc(byte, fp) == EOF) { + fprintf(stderr, "joeyasset: short write to %s\n", path); + fclose(fp); + return 1; + } + written++; + } + } + + fclose(fp); + return 0; +} + + +int main(int argc, char **argv) { + int width; + int height; + uint8_t *rgb; + uint8_t *indices; + uint16_t palette[JAS_PALETTE_ENTRIES]; + int paletteCount; + int rc; + + if (argc != 3) { + fprintf(stderr, "usage: joeyasset INPUT.ppm OUTPUT.jas\n"); + return 2; + } + + if (loadPpm(argv[1], &width, &height, &rgb) != 0) { + return 1; + } + + indices = (uint8_t *)malloc((size_t)width * (size_t)height); + if (indices == NULL) { + fprintf(stderr, "joeyasset: out of memory for index buffer\n"); + free(rgb); + return 1; + } + + paletteCount = buildPalette(rgb, width, height, palette, indices); + if (paletteCount < 0) { + fprintf(stderr, + "joeyasset: input has more than 16 distinct $0RGB colors after\n" + " 4-bit-per-channel quantization. Reduce the input palette and\n" + " retry (e.g. pngquant --nofs 16, or GIMP -> Image -> Mode ->\n" + " Indexed... with 16 colors and no dithering).\n"); + free(rgb); + free(indices); + return 1; + } + + rc = writeJas(argv[2], width, height, palette, indices); + if (rc == 0) { + printf("joeyasset: wrote %s (%d x %d, %d color%s)\n", + argv[2], width, height, paletteCount, + paletteCount == 1 ? "" : "s"); + } + + free(rgb); + free(indices); + return rc; +}