26 KiB
JoeyLib
A unified C game-development library targeting five early 16-bit platforms from a single codebase:
- Apple IIgs (reference platform)
- Commodore Amiga (A500 / 68000 baseline)
- Atari ST (STF / 68000 baseline)
- MS-DOS (386 / VGA, DJGPP)
- Sharp X68000 (68000 @ 10 MHz, Human68k, elf2x68k)
The Apple IIgs defines the capability ceiling. Stronger platforms coast. Hot paths are hand-written assembly per port; the public API is C.
See docs/DESIGN.md for the full 1.0 design.
Quick start
git clone <repo> joeylib
cd joeylib
./toolchains/install.sh
# follow any non-free-tool placement instructions the script prints
source toolchains/env.sh
make
This builds libjoey.a for every target whose toolchain is installed,
plus the example programs (hello, pattern, draw, keys, serial,
sertest, save, joy, sprite, audio, uber, adventure,
adventure2, agi, spacetaxi) for each. The IIgs is the exception:
make iigs compile-checks the library and runs the clang smoke test, and
make -f make/iigs.mk iigs-examples builds its example binaries. The
Sharp X68000 is not covered at all -- see below.
Building for a single target
source toolchains/env.sh
make iigs
make amiga
make atarist
make dos
The Sharp X68000 has no top-level target: toolchains/install.sh does not
fetch elf2x68k and the root Makefile has no x68000 rule. Stage the
elf2x68k tarball under toolchains/x68000/m68k-xelf and drive the fragment
directly -- it resolves its own compiler relative to the repo, so this one
does not need env.sh:
make -f make/x68000.mk # libjoey.a plus SERIAL.X, UBER.X, AUDIO.X
Every fragment also answers to a bare clean, and every fragment that builds
through make/common.mk -- all of them except make/iigs.mk, which assembles
its own clang flags -- honors EXTRA_CFLAGS:
make -f make/atarist.mk clean
make -f make/x68000.mk EXTRA_CFLAGS=-DJOEY_LOG_SERIAL_TEE
Conformance gate
examples/uber is the conformance vehicle: it exercises every public op and
logs a surface hash per op. tools/diff-uber-hashes <reference-log> <test-log>
compares two joeylog.txt captures and exits non-zero on any mismatch. The
frozen references live in tests/goldens/uber/. All five ports currently agree
on all 51 hashes; the X68000 has no golden of its own and is diffed against
tests/goldens/uber/iigs.txt, which it matches exactly:
make -f make/x68000.mk x68000-verify-golden # ~70 min under the patched MAME
make -f make/x68000.mk x68000-verify-serial # both-directions RS-232C, ~1 min
Both X68000 gates want X68K_SCRATCH pointing at a work dir containing
x68mame/, and a MAME built with the patches in patches/.
Repository layout
docs/ design and reference documentation
include/joey/ public headers
src/core/ portable library code
src/generic/ portable-C default for every overridable op
src/codegen/ shared sprite-codegen staging / compile machinery
src/<machine>/ per-machine HAL + per-CPU sprite emitter
(iigs, amiga, atarist, dos, x68000)
src/m68k/ 68k code shared across machines (planar and word-
interleaved sprite emitters; surface68k.s, which is
linked by Amiga and Atari ST only)
src/blank/ copy-to-start template for a new port
tools/assetbake/ PNG -> native .tbk / .spr baker (Python)
tools/joeymod/ Protracker .MOD converter (passthrough or .NTP)
tools/spritebake/ offline .spr -> .spc pre-compiled sprite baker
tools/songbake/ chip-tracker text notation -> JYM1 stream
tools/xdftool.py Human68k FAT12 .XDF disk image reader / writer
tools/diff-uber-hashes UBER golden-hash comparator
examples/ example programs
scripts/ run / bench / verification scripts
patches/ MAME patches the X68000 gates need
tests/goldens/uber/ frozen UBER conformance hashes
toolchains/ self-contained cross-build tools
make/ per-target Makefile fragments
build/<plat>/ per-target build outputs
Public API
Game code includes a single umbrella header:
#include <joey/joey.h>
That pulls in every public surface listed below. Full documentation
lives in the per-feature headers under include/joey/; what follows
is a quick reference. Every entry point is plain C, no C++ extensions.
Lifecycle (joey/core.h)
typedef struct {
uint32_t codegenBytes; // runtime compiled-sprite cache size
uint32_t audioBytes; // reserved; not yet consulted by any engine
} jlConfigT;
bool jlInit (const jlConfigT *config);
void jlShutdown (void);
const char *jlLastError (void);
const char *jlPlatformName (void);
const char *jlVersionString(void);
void *jlAlloc (uint32_t bytes); // native allocator, not malloc
void jlFree (void *p);
void jlWaitVBL (void); // block until next VBL
uint16_t jlFrameCount (void); // monotonic 16-bit frame counter
uint16_t jlFrameHz (void); // 50 / 55 / 60 / 70 depending on port
uint32_t jlMillisElapsed(void); // monotonic ms since jlInit
uint32_t jlRandom (void); // portable, bit-identical per seed
uint16_t jlRandomRange (uint16_t bound);
void jlRandomSeed (uint32_t seed);
Surfaces (joey/surface.h)
All surfaces are 320x200 16-color images with a 200-entry SCB table
and 16 palettes of 16 $0RGB colors. In-memory storage is
target-native: chunky 4bpp packed on IIgs and DOS, native planar
(separate bitplanes on Amiga and Sharp X68000, word-interleaved planes
on Atari ST) on the 68k ports. The public API speaks in color indices
(0..15) and hides the storage format.
#define SURFACE_WIDTH 320
#define SURFACE_HEIGHT 200
#define SURFACE_BYTES_PER_ROW 160
#define SURFACE_PIXELS_SIZE (SURFACE_BYTES_PER_ROW * SURFACE_HEIGHT)
#define SURFACE_PALETTE_COUNT 16
#define SURFACE_COLORS_PER_PALETTE 16
typedef struct jlSurfaceT jlSurfaceT; // opaque
jlSurfaceT *jlSurfaceCreate (void);
void jlSurfaceDestroy(jlSurfaceT *s);
jlSurfaceT *jlStageGet (void); // library back-buffer
void jlSurfaceCopy (jlSurfaceT *dst, const jlSurfaceT *src);
bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path);
bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path);
uint32_t jlSurfaceHash (const jlSurfaceT *s); // FNV-1a of logical pixels
jlSurfaceSaveFile writes the surface in target-native form. Files
are NOT cross-port portable; the asset pipeline handles conversion.
Drawing (joey/draw.h)
All primitives clip to the surface; off-surface coords are silent no-ops. Color 0 is plotted normally (use the masked variants if you need transparency).
void jlSurfaceClear (jlSurfaceT *s, uint8_t color);
void jlDrawPixel (jlSurfaceT *s, int16_t x, int16_t y, uint8_t color);
uint8_t jlSamplePixel (const jlSurfaceT *s, int16_t x, int16_t y);
void jlDrawLine (jlSurfaceT *s, int16_t x0, int16_t y0,
int16_t x1, int16_t y1, uint8_t color);
void jlDrawRect (jlSurfaceT *s, int16_t x, int16_t y,
uint16_t w, uint16_t h, uint8_t color);
void jlFillRect (jlSurfaceT *s, int16_t x, int16_t y,
uint16_t w, uint16_t h, uint8_t color);
void jlDrawCircle (jlSurfaceT *s, int16_t cx, int16_t cy,
uint16_t r, uint8_t color);
void jlFillCircle (jlSurfaceT *s, int16_t cx, int16_t cy,
uint16_t r, uint8_t color);
void jlFloodFill (jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
void jlFloodFillBounded (jlSurfaceT *s, int16_t x, int16_t y,
uint8_t newColor, uint8_t boundaryColor);
Palette and SCB (joey/palette.h)
Colors are 12-bit $0RGB. Color 0 of every palette is forced to
black on jlPaletteSet. Each scanline picks one of the 16 palettes
via the SCB. Every port honours it. The X68000 does it the same way
the DOS port does: it renders through the 256-colour graphics plane,
so all 16 palettes are resident at once and each pixel byte is
(scb[y] << 4) | nibble -- the pixel value selects the band, with no
raster interrupt involved.
void jlPaletteSet (jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
void jlPaletteGet (const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
void jlScbSet (jlSurfaceT *s, uint16_t line, uint8_t paletteIndex);
void jlScbSetRange (jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine,
uint8_t paletteIndex);
uint8_t jlScbGet (const jlSurfaceT *s, uint16_t line);
Tiles (joey/tile.h)
A "tile" is just an 8x8-aligned region of any surface. The API moves
32-byte chunks between surfaces and provides a small jlTileT value
type so callers can stash a copy without allocating a scratch surface.
#define TILE_PIXELS_PER_SIDE 8
#define TILE_BYTES_PER_ROW 4
#define TILE_BYTES (TILE_BYTES_PER_ROW * TILE_PIXELS_PER_SIDE)
#define TILE_BLOCKS_PER_ROW (SURFACE_WIDTH / TILE_PIXELS_PER_SIDE) // 40
#define TILE_BLOCKS_PER_COL (SURFACE_HEIGHT / TILE_PIXELS_PER_SIDE) // 25
#define TILE_NO_GLYPH ((uint16_t)0xFFFFu)
typedef struct jlTileT { uint8_t pixels[TILE_BYTES]; } jlTileT;
void jlTileCopy (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
void jlTileCopyMasked (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy,
uint8_t transparentIndex);
void jlTileFill (jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t color);
void jlTileSnap (const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out);
void jlTilePaste (jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in);
void jlTilePasteMono (jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlTileT *in, uint8_t fgColor, uint8_t bgColor);
// Load up to maxTiles tiles from a baked .tbk file (per-target planar
// bytes from tools/assetbake/assetbake.py --type tile --target ...).
// Refuses files baked for the wrong target. outValid (optional) marks
// each loaded slot; outPalette (optional) receives the embedded
// 16-entry $0RGB palette if present.
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
bool *outValid, uint16_t *outPalette);
void jlDrawText (jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlSurfaceT *fontSurface, const uint16_t *asciiMap,
const char *str);
Sprites (joey/sprite.h)
Rectangles of 8x8 tiles drawn at arbitrary pixel positions with
color-0 transparency. Tile data is widthTiles * heightTiles * 32
bytes, tile-major 4bpp packed. Sprites can be runtime-compiled
into per-shift code variants for fast draws.
typedef struct jlSpriteT jlSpriteT; // opaque
typedef struct {
jlSpriteT *sprite;
int16_t x, y;
uint16_t width, height; // pixels
uint8_t *bytes; // caller-owned save-under buffer
uint16_t sizeBytes;
} jlSpriteBackupT;
jlSpriteT *jlSpriteCreate (const uint8_t *tileData,
uint8_t widthTiles, uint8_t heightTiles);
jlSpriteT *jlSpriteCreateFromSurface (const jlSurfaceT *src, int16_t x, int16_t y,
uint8_t widthTiles, uint8_t heightTiles);
void jlSpriteDestroy (jlSpriteT *sp);
// Load up to maxCels cels from a baked .spr file (cross-target chunky
// 4bpp blob from tools/assetbake/assetbake.py --type sprite). Each
// cel becomes a freshly-allocated jlSpriteT (release with
// jlSpriteDestroy). outPalette (optional) receives the embedded
// 16-entry $0RGB palette if present.
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels,
uint16_t maxCels, uint16_t *outPalette);
// Same contract, but reading a `.spc` bank pre-compiled for THIS target by
// the offline baker `tools/spritebake`: the native blit routines are copied
// straight into the codegen arena, so there is no startup JIT. The runtime
// twin writes one back out after a jlSpriteCompile pass.
uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels,
uint16_t maxCels, uint16_t *outPalette);
bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels,
uint16_t count, const uint16_t *palette);
bool jlSpriteCompile (jlSpriteT *sp); // build per-shift fast path
void jlSpritePrewarm (jlSpriteT *sp); // hint: compile if not already
uint32_t jlSpriteCompiledSize (const jlSpriteT *sp);
void jlSpriteDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
void jlSpriteSaveUnder (const jlSurfaceT *s, jlSpriteT *sp,
int16_t x, int16_t y, jlSpriteBackupT *backup);
void jlSpriteRestoreUnder (jlSurfaceT *s, const jlSpriteBackupT *backup);
void jlSpriteSaveAndDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
jlSpriteBackupT *backup);
// TRUSTED variants: skip the per-call geometry validation for a caller that
// pre-compiles its sprites and guarantees the sprite is FULLY on-surface.
// UB if that is violated -- no clipping happens. They fall back to the
// validated entry for uncompiled sprites and on ports with no compiled path.
void jlSpriteDrawTrusted (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
void jlSpriteSaveUnderTrusted (const jlSurfaceT *s, jlSpriteT *sp,
int16_t x, int16_t y, jlSpriteBackupT *backup);
void jlSpriteRestoreUnderTrusted(jlSurfaceT *s, const jlSpriteBackupT *backup);
void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
jlSpriteBackupT *backup);
void jlSpriteCompact (void); // defrag the codegen arena
uint32_t jlSpriteCodegenBytesUsed (void);
uint32_t jlSpriteCodegenBytesTotal (void);
Assets (baked at build time)
Asset PNGs live in each example's assets/ directory and are baked
to native binary blobs at build time by tools/assetbake/assetbake.py
(Python+PIL). The runtime loads those blobs directly with no
conversion -- there is no in-memory chunky-to-planar or palette
re-indexing on the device.
Two blob formats:
.tbk(tile bank) -- one or more 8x8 tiles in per-target planar layout (Amiga plane-major; Atari ST row-major-with-planes- per-row; DOS / IIgs chunky 4bpp). The loaderjlTileBankLoadrejects files baked for the wrong target. There is no X68000 target:assetbake.pybakes only the four above, and the loader still expects the DOS chunky target byte there (src/core/assetLoad.c) even though that port's surfaces and tile ops are now planar -- so.tbkbaking is not wired up for the X68000 yet..spr(sprite cel set) -- one or more uniform-sized sprite cels in cross-target chunky 4bpp. The Phase 11 walker reads chunky and converts to planar at draw time, so the same blob serves every platform.
Both formats embed an optional 16-entry $0RGB palette.
tools/assetbake/assetbake.py --type tile --target {amiga|atarist|dos|iigs} in.png out.tbk
tools/assetbake/assetbake.py --type sprite --cell WxH in.png out.spr
The Makefiles (make/{amiga,atarist,dos}.mk) wire bake rules
per target so make <target> produces baked blobs under
examples/<game>/generated/<target>/ and stages them into the
runtime tree at build/<target>/.../DATA/. make/iigs.mk runs no
assetbake.py: its generated/iigs tree is produced out-of-band and the
fragment only bakes .spc from the committed .spr. make/x68000.mk
has no asset rules at all.
For runtime-extracted content (sprites peeled out of a procedural
or captured surface), use jlSpriteCreateFromSurface -- the
on-the-fly path stays alongside the baked load API.
Present (joey/present.h)
void jlStagePresent(void);
Flips the dirty rows of the stage to the display, then clears dirty
state. Drawing primitives mark dirty as a side effect, so calling
jlStagePresent once at end-of-frame is enough.
Input (joey/input.h)
Call jlInputPoll once per frame, then query the state predicates.
Edge predicates (*Pressed, *Released) fire only in the frame the
transition happened.
typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE,
KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
KEY_UP/DOWN/LEFT/RIGHT, KEY_LSHIFT/RSHIFT/LCTRL/LALT,
KEY_F1..KEY_F10, KEY_COUNT */ } jlKeyE;
typedef enum { MOUSE_BUTTON_NONE, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT,
MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } jlMouseButtonE;
typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE;
typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE;
#define JOYSTICK_AXIS_MAX 127
#define JOYSTICK_AXIS_MIN (-127)
void jlInputPoll (void);
void jlWaitForAnyKey (void);
bool jlKeyDown (jlKeyE key);
bool jlKeyPressed (jlKeyE key);
bool jlKeyReleased (jlKeyE key);
int16_t jlMouseX (void);
int16_t jlMouseY (void);
bool jlMouseDown (jlMouseButtonE b);
bool jlMousePressed (jlMouseButtonE b);
bool jlMouseReleased (jlMouseButtonE b);
bool jlJoystickConnected(jlJoystickE js);
int8_t jlJoystickX (jlJoystickE js);
int8_t jlJoystickY (jlJoystickE js);
bool jlJoyDown (jlJoystickE js, jlJoyButtonE b);
bool jlJoyPressed (jlJoystickE js, jlJoyButtonE b);
bool jlJoyReleased (jlJoystickE js, jlJoyButtonE b);
void jlJoystickReset (jlJoystickE js, uint8_t deadZone);
Audio (joey/audio.h)
4-channel Protracker-style music plus five one-shot SFX slots, three
PSG-style tone voices and a noise channel. Module data must be the
platform-native form produced by tools/joeymod (.mod for
Amiga/DOS/ST/X68000; .ntp for IIgs; .amod if you want loop=false on
Amiga). Amiga plays .mod through PTPlayer and the IIgs through the
NinjaTrackerPlus replayer; DOS, Atari ST and X68000 decode with
libxmp-lite. A failed jlAudioInit is non-fatal; the rest of the API
stays callable as no-ops.
#define JOEY_AUDIO_SFX_SLOTS 5
#define JOEY_AUDIO_VOICES 3
bool jlAudioInit (void);
void jlAudioShutdown (void);
void jlAudioPlayMod (const uint8_t *data, uint32_t length, bool loop);
void jlAudioStopMod (void);
bool jlAudioIsPlayingMod (void);
void jlAudioPlaySfx (uint8_t slot, const uint8_t *sample,
uint32_t length, uint16_t rateHz);
void jlAudioPlaySfxStream (uint8_t slot, jlAudioStreamFillT fill, void *ctx,
uint16_t rateHz);
void jlAudioStopSfx (uint8_t slot);
// Portable chip-tracker music: a JYM1 event stream baked by tools/songbake,
// played through the tone/noise layer at register-write cost on every port.
// No CPU mixing, so it is viable where jlAudioPlayMod is not.
bool jlMusicPlay (const uint8_t *data, uint32_t length, bool loop);
void jlMusicStop (void);
bool jlMusicIsPlaying (void);
void jlMusicPause (void);
void jlMusicResume (void);
void jlMusicSetAtten (uint8_t atten);
void jlAudioTone (uint16_t freqHz);
void jlAudioVoice (uint8_t voice, uint16_t freqHz, uint8_t atten);
void jlAudioNoise (uint8_t pitch, uint8_t atten);
void jlAudioFrameTick (void);
Serial (joey/serial.h) -- opt-in add-on
RS-232 serial communications. This is an opt-in add-on: it is the one
public header the umbrella <joey/joey.h> does not include, so pull in
<joey/serial.h> yourself. Ports that have no serial hardware, and the BLANK
template, link a no-op stub, so the API stays callable everywhere -- a failed
jlSerialOpen leaves every other call an inert no-op.
The API is non-blocking and buffered: received bytes accumulate in a
background ring that you drain once per frame with jlSerialPoll, and writes
never stall the frame (they report a short count under flow control -- resend
the remainder next frame). One port is open at a time.
typedef enum jlSerialDeviceE {
JL_SERIAL_DEFAULT = 0, // the platform's natural port
JL_SERIAL_MODEM, // IIgs modem port (SCC channel A)
JL_SERIAL_PRINTER, // IIgs printer port (SCC channel B)
JL_SERIAL_SLOT // a card in slot/unit `config.unit`
} jlSerialDeviceE;
typedef struct jlSerialConfigT {
uint32_t baud; // 0 -> 9600
uint8_t dataBits; // 5..8, 0 -> 8
uint8_t stopBits; // 1..2, 0 -> 1
jlSerialParityE parity; // NONE / ODD / EVEN
jlSerialFlowE flow; // NONE / RTSCTS / XONXOFF
uint8_t unit; // slot (IIgs SSC) or COM/device index
} jlSerialConfigT;
bool jlSerialOpen (jlSerialDeviceE device, const jlSerialConfigT *config);
void jlSerialClose (void);
void jlSerialPoll (void); // call once per frame to drain RX
uint16_t jlSerialAvailable(void);
uint16_t jlSerialRead (uint8_t *buf, uint16_t max);
int16_t jlSerialReadByte (void); // -1 if none
uint16_t jlSerialWrite (const uint8_t *buf, uint16_t len);
bool jlSerialWriteByte(uint8_t b);
void jlSerialFlush (void); // discard buffered input
Per-port device mapping:
| Device | IIgs | DOS | Amiga | Atari ST | X68000 |
|---|---|---|---|---|---|
JL_SERIAL_DEFAULT |
modem port (SCC A) | COM1 | serial.device 0 | Modem/RS-232 | RS-232C (SCC A) |
JL_SERIAL_MODEM |
modem port (SCC A) | COM1 | serial.device 0 | Modem/RS-232 | RS-232C (SCC A) |
JL_SERIAL_PRINTER |
printer port (SCC B) | COM1 | serial.device 0 | Modem/RS-232 | RS-232C (SCC A) |
JL_SERIAL_SLOT |
6551 SSC in slot unit |
COMunit |
serial.device unit |
Modem/RS-232 | RS-232C (SCC A) |
Only the IIgs has multiple built-in ports; elsewhere MODEM/PRINTER fall
back to the natural port. The serial example is an echo terminal and
loopback self-test. scripts/check-serial.sh unit-tests the portable gate
layer against a loopback mock with no hardware. Full reference, per-port
behavior, and porting notes: docs/serial.md.
Files, saves & disk space (joey/file.h) -- opt-in
File access, not pulled in by <joey/joey.h> -- include <joey/file.h>. Two
directories sit beside the executable: read-only DATA/ (game content) and
writable SAVES/ (save / preference files, created on demand). Names are
relative to their directory ("hero.sav", not "SAVES/hero.sav"). Keep save
names to 8 characters plus a short extension, letters/digits only, so they fit
ProDOS and 8.3. Every call is non-fatal: a failed save reports false/0.
FILE * jlDataOpen (const char *name, const char *mode); // DATA/ (read-only)
FILE * jlSaveOpen (const char *name, const char *mode); // SAVES/ stream
bool jlSaveWrite (const char *name, const void *buf, uint32_t len);
uint32_t jlSaveRead (const char *name, void *buf, uint32_t max);
bool jlSaveExists(const char *name);
bool jlSaveDelete(const char *name);
uint32_t jlDiskFree (void); // free bytes on the save volume (0 if unknown)
Directory create / delete / free-space use a per-port HAL (POSIX mkdir +
remove + INT 21h on DOS; mkdir + GEMDOS Dfree on ST; dos.library
CreateDir/DeleteFile/Info on Amiga; GS/OS Create + a
GetDevNumber/DInfo/Volume chain on the IIgs; Human68k _dos_mkdir +
remove + _dos_dskfre on the X68000). scripts/check-save.sh
round-trips the core against real files; make iigs-verify-save boots the
IIgs save path under MAME. Full reference, per-port behavior, and porting
notes: docs/save.md.
Debug logging (joey/debug.h)
Crash-tracing logger. Writes are buffered and durable across normal
exit; call jlLogFlush ahead of suspected hang points if you want
a guaranteed last-line-on-disk.
void jlLog (const char *msg);
void jlLogF (const char *fmt, ...);
void jlLogFlush(void);
void jlLogReset(void);
Output goes to joeylog.txt in the program's working directory.
Building with EXTRA_CFLAGS=-DJOEY_LOG_SERIAL_TEE additionally mirrors every
jlLog / jlLogF line out the port's serial line as it is written --
unbounded, needs no disk, and arrives in real time, which is what makes a
wedged run diagnosable. It is OFF by default and is currently implemented
only for the X68000 (jlpLogTee in src/x68000/hal.c); the host end is
scripts/x68kSerialPeer.py, which must be run under python3 -u -- it never
passes flush=True, so a killed peer loses its whole capture to stdio
buffering.
Platform macros (joey/platform.h)
The build system normally sets the platform via -D; auto-detection
from compiler-predefined macros is a fallback. Game code can
conditionally compile on these:
JOEYLIB_PLATFORM_IIGS / _AMIGA / _ATARIST / _DOS // exactly one defined
JOEYLIB_CPU_65816 / _68000 / _X86
JOEYLIB_ENDIAN_LITTLE / _BIG
JOEYLIB_NATIVE_CHUNKY / _NATIVE_PLANAR
JOEYLIB_HAS_BLITTER / _HAS_COPPER // Amiga only
JOEYLIB_PLATFORM_NAME // human-readable string
JOEYLIB_VERSION_MAJOR / _MINOR / _PATCH / _STRING
License
TBD.