(Untested) Joystick support. Basic sprites.

This commit is contained in:
Scott Duensing 2026-04-24 20:09:18 -05:00
parent b10a7802b9
commit 3e9c7f4926
32 changed files with 1698 additions and 55 deletions

View file

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

252
examples/joy/joy.c Normal file
View file

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

148
examples/sprite/sprite.c Normal file
View file

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

65
include/joey/asset.h Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

26
make/tools.mk Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

158
src/core/asset.c Normal file
View file

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

View file

@ -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, &copyW, &copyH,
(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, &copyW, &copyH,
(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;

View file

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

View file

@ -95,3 +95,8 @@ void joeyShutdown(void) {
const char *joeyVersionString(void) {
return JOEYLIB_VERSION_STRING;
}
void joeyWaitVBL(void) {
halWaitVBL();
}

View file

@ -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 <string.h>
@ -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];
}

View file

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

View file

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

View file

@ -24,9 +24,11 @@
#include <intuition/intuition.h>
#include <intuition/screens.h>
#include <devices/inputevent.h>
#include <libraries/lowlevel.h>
#include <proto/exec.h>
#include <proto/intuition.h>
#include <proto/lowlevel.h>
#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;
}

View file

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

View file

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

View file

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

View file

@ -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();
}

View file

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

View file

@ -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();
}

306
tools/joeyasset/joeyasset.c Normal file
View file

@ -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 <ctype.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}