# 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:

```c
#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`)

```c
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.

```c
#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).

```c
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.

```c
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.

```c
#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.

```c
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 loader `jlTileBankLoad`
  rejects files baked for the wrong target. There is no X68000 target:
  `assetbake.py` bakes 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 `.tbk` baking
  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`)

```c
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.

Text entry uses the separate typed-character queue: `jlInputGetChar`
pops the next typed character (printable ASCII 0x20..0x7E plus
`JL_CHAR_BACKSPACE`/`TAB`/`RETURN`/`ESCAPE`) or -1 when empty, with
shift, caps lock, and the machine's keyboard layout already applied by
the backend -- so punctuation like `.` and `:` arrives correctly on
every port. Refilled by the same `jlInputPoll`; see `docs/input.md`
for per-port sources and limitations.

```c
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)

#define JL_CHAR_BACKSPACE  0x08
#define JL_CHAR_TAB        0x09
#define JL_CHAR_RETURN     0x0D
#define JL_CHAR_ESCAPE     0x1B

void    jlInputPoll        (void);
void    jlWaitForAnyKey    (void);
int     jlInputGetChar     (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.

```c
#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.

```c
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` | COM`unit` | 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`](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`.

```c
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`](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.

```c
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.
