38 KiB
JoeyLib
A unified C game-development library targeting five early 16-bit platforms, one DOS desktop, today's desktops and browsers, Haiku and BeOS, and the PSP, 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)
- DVX (the DOS desktop GUI in
../dvxgui: each program a windowed.appthat can also take the whole screen, built with the DOS compiler against DVX's SDK) - SDL3 (a native windowed program for the machine you work on, Linux today:
the hosted core in
src/hostedwith an SDL3 backend, SDL 3 linked in statically; sprites are interpreted) - Web (the SDL3 port compiled to WebAssembly with Emscripten: each program a page, its DATA packed in, saves kept in the browser)
- macOS (the SDL3 port cross-built on Linux with zig against Apple's SDK: each program a universal app for Apple silicon and Intel Macs, macOS 13 or newer; built and checked here, but run only on a Mac)
- Win32 (Windows 95 through today: the hosted core with a backend written to the Win32 API, built with llvm-mingw against MSVCRT.DLL, tested under Wine)
- Win16 (Windows 3.1 on a 386: the hosted core with a backend written to the Windows 3.1 API, built with Open Watcom's Win386 target, tested on Windows for Workgroups 3.11 in DOSBox Staging)
- Haiku and BeOS (the hosted core with a backend written to the Be API, which Haiku reimplements: Haiku x86_64 and x86, and BeOS R5 x86, built with gcc 2.95 against R5's own headers and libraries; each tested in a VM under QEMU)
- PSP (homebrew for custom firmware: the SDL3 port built with pspdev, each
program an
EBOOT.PBPin a folder forPSP/GAME/, the stage at 1:1 in the 480x272 screen, a keyboard drawn on it for the keys a PSP lacks; tested under PPSSPP with every memory access checked for alignment as a PSP checks it)
The Apple IIgs defines the capability ceiling. Stronger platforms coast. Hot paths are hand-written assembly per port; the public API is C.
The manual (docs/, built by make docs) documents the whole API.
Quick start
JoeyLib builds on Linux x86-64, and on Windows inside WSL2. One script
installs every cross toolchain, pinned and checksum-verified, into
toolchains/; outside that directory it only installs ordinary packages
from your distribution (apt or dnf), and only when asked.
git clone https://forge.duensing.digital/AI_Slop/joeylib2.git joeylib
cd joeylib
./install.sh deps toolchains --install # distribution packages (sudo)
./install.sh install toolchains # every cross toolchain
make
The first install takes about an hour, almost all of it compiling the IIgs
and Amiga compilers. ./install.sh install toolchain:dos (or any
other port) installs just one. ./install.sh list shows every
component and whether it is current, and ./install.sh doctor
smoke-tests what is installed. The manual's Getting Started chapter
(docs/GettingStarted.adoc) has the full installer reference.
No environment setup is needed: make reads the tool paths the installer
records in toolchains/config.mk. For running a cross compiler by
hand, source toolchains/env.sh sets the same variables.
make builds libjoey.a for every port whose toolchain is installed, plus
the example programs (hello, pattern, draw, keys, serial,
sertest, save, tilebank, joy, sprite, scroll, audio, uber,
adventure, adventure2) for each. The IIgs is the exception:
make iigs builds the library, and make iigs-disk builds its examples and
packages them onto a disk image. DVX needs the dvx installer component,
which unpacks DVX and its SDK from a DVX tree's distribution, dist/dvx.zip
and dist/sdk.zip (./install.sh --dvx-dir PATH install dvx; the default
is ../dvxgui); its examples land in build/dvx/bin/apps/kpunch/<name>/,
laid out as they install in DVX. SDL3 needs the sdl3 component
(./install.sh install toolchain:sdl3), which builds SDL 3 from its release
source; its examples are native programs in build/sdl3/bin/, and running
one takes seconds rather than an emulator's minutes. The web port needs the
web components (./install.sh install toolchain:web: Emscripten through
emsdk, and SDL 3 built with it); its examples are pages in
build/web/bin/<NAME>/. The macOS port needs toolchain:macos (zig,
Apple's macOS SDK, and SDL 3 built with them); its examples are apps,
build/macos/bin/<NAME>.app, which nothing here runs: copy one to a Mac.
The Win32 port needs toolchain:win32 (llvm-mingw)
and, to run its programs, emulator:win32 (a portable Wine). The Win16 port
needs toolchain:win16 (Open Watcom) and, to run its programs,
emulator:win16 (Windows for Workgroups 3.11 in DOSBox Staging, set up from
your own install disc and S3 display driver). The PSP port
needs toolchain:psp (pspdev's prebuilt release, and SDL 3 built with it)
and, to run its programs, emulator:psp (PPSSPP's headless build, patched
for the gates); its examples are folders build/psp/bin/<NAME>/ holding
EBOOT.PBP and DATA/, ready for a memory stick's PSP/GAME/. The Haiku
ports need toolchain:haiku or toolchain:haiku32 (cross compilers built
from Haiku R1/beta6) and, to run their programs, emulator:haiku or
emulator:haiku32 (a Haiku VM for QEMU); the BeOS port needs
toolchain:beos and emulator:beos, both made from your own BeOS 5
Professional CD.
Your own program does not go into this tree: it lives in a directory of its
own whose Makefile describes it and includes make/project.mk and
make/programs.mk from a JoeyLib checkout (found through JOEYLIB, by
default ../joeylib). make there builds it for every installed target with
the same rules, flags, and packaging as the examples, into the project's own
build/; the manual's "Building Your Program" chapter
(docs/BuildIntegration.adoc) has the details. Two complete games are built
this way as separate projects: Space Taxi, a port of the 1984 Commodore 64
game, and JoeyAGI, an interpreter for Sierra's AGI adventures.
Running programs
joey-run starts a built program in the right emulator; the installer sets up
the emulators and their firmware:
./install.sh install emulators # or emulator:dos, emulator:iigs, ...
./joey-run build/dos/bin/DRAW.EXE
./joey-run --port amiga draw
./joey-run --headless --seconds 30 --snapshot draw.png --port atarist draw
./joey-run --port dvx draw # DVX in DOSBox, booting into the program
./joey-run --port sdl3 draw # natively, in a window (no emulator)
./joey-run --port web draw # served here, opened in your browser
./joey-run --port win32 draw # under Wine
./joey-run --port win16 draw # on Windows 3.11 in DOSBox
./joey-run --port psp --headless --snapshot draw.png draw # under headless PPSSPP
./joey-run --port haiku draw # in a Haiku VM under QEMU
./joey-run --port beos draw # in a BeOS R5 VM under QEMU
A few files are never downloaded -- the Amiga Kickstart, the Amiga
serial.device and lowlevel.library from Workbench 3.1, the X68000 font
ROM, the Windows for Workgroups 3.11 install disc and S3 Trio64V+ driver
the Win16 port runs on (and, optionally, Microsoft's WinG 1.0 SDK,
WING10.EXE), and the BeOS 5 Professional CD -- so supply your
copies once with ./install.sh import PATH (files, folders, .zip or
.7z archives, or Amiga .adf disk images; the Windows disc as its .iso,
the BeOS CD as its CloneCD .img).
The manual's Getting Started chapter covers joey-run's options and the
private-file bundle.
Building for a single target
make iigs
make amiga
make atarist
make ste
make dos
make x68000
make dvx
make atarist builds programs for every ST; make ste builds the STe
variant, which uses the STe's BLiTTER, DMA sound, and 4-bit palette, reads
Jaguar pads in a 1040 STe's enhanced joystick ports, and runs only on an STe
or Mega STe.
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.
build/tools/diffUberHashes <reference-log> <test-log> (make tools builds
it) 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, and DVX, SDL3, the
web, and the PSP, which share the DOS stage, are diffed against
tests/goldens/uber/dos.txt (scripts/bench-dvx.sh, scripts/bench-sdl3.sh,
scripts/bench-web.sh, or scripts/bench-psp.sh, then
build/tools/diffUberHashes). SDL3's run needs no emulator and no X server
(SDL's dummy drivers) and takes about fifteen seconds; the web's runs UBER's
page in headless Chromium; the PSP's runs UBER under headless PPSSPP in a few
seconds, with every memory access checked as a PSP checks it, so an unaligned
16- or 32-bit access anywhere fails it:
scripts/bench-sdl3.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/sdl3/bin/joeylog.txt
scripts/bench-web.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/web/bin/UBER/joeylog.txt
scripts/bench-psp.sh && build/tools/diffUberHashes tests/goldens/uber/dos.txt build/psp/bin/UBER/joeylog.txt
The X68000 has gates of its own:
make -f make/x68000.mk x68000-verify-golden # ~1 min under the patched MAME
make -f make/x68000.mk x68000-verify-serial # both-directions RS-232C, ~1 min
make -f make/x68000.mk x68000-verify-joy # both joysticks, every line, ~10 s
make -f make/x68000.mk x68000-verify-serial-scb # RS-232C under per-line palettes, ~3 min
These X68000 gates run under the patched MAME and the X68000 firmware that
./install.sh install emulator:x68000 sets up.
DVX's display is gated as well: scripts/verify-dvx-display.sh runs UBER's
probe build (make dvx puts it in build/dvx/probe) on nine desktop depths
and scales and on the full screen, and checks every displayed pixel against
the stage.
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, dvx)
src/hosted/ the hosted core (desktop OSes and browsers: RGB
present, virtual frame clock, input events, software
mixer) and its backends (src/hosted/sdl3, which the
sdl3, web and psp ports all build; src/hosted/win32;
src/hosted/win16)
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/x86/ x86 code DOS and DVX share (the sprite emitter, the
whole-map tile walker, the set-1 scan code table)
src/blank/ copy-to-start template for a new port
tools/ the host tools, all C (make tools builds them into build/tools/):
adfTool/ read files out of an Amiga disk image (.adf)
assetbake/ PNG -> native .tbk / .spr baker
common/ the C tools' shared code (PNG, DEFLATE, XML, JSON, base64)
diffUberHashes/ UBER golden-hash comparator
diffUberPerf/ UBER ops/sec comparator
iconBake/ one picture -> every system's program icon
iigsStart/ GS/OS boot launcher for the headless IIgs harnesses
joeymod/ Protracker .MOD converter (passthrough, .amod, or .NTP)
modloopend/ an E8F stop marker added to a .MOD (the Amiga's .amod)
modToNtp/ Protracker .MOD -> NinjaTrackerPlus .NTP (the IIgs)
rawCd/ a raw CD image's track or El Torito boot floppy
scanPtrBug/ scan IIgs assembly for a far-pointer miscompile pattern
songbake/ chip-tracker text notation -> JYM1 stream
spritebake/ offline .spr -> .spc pre-compiled sprite baker
szddExpand/ expand a COMPRESS.EXE (SZDD) file
tiled2joey/ a Tiled map (TMX or TMJ) -> tile sheet + tile maps
traceLogo/ the logo PNG -> its traced SVG (runs potrace)
uberPerfTable/ the UBER ops/sec table across ports
vgm2song/ a tracker's VGM export (Furnace) -> chip-tracker .song
winIni/ edit a Windows 3.x .INI file
xdftool/ Human68k FAT12 .XDF disk image reader / writer
examples/ example programs
joey-run run a built program in its emulator
scripts/ bench / verification scripts (scripts/lib: shared helpers)
tests/gate/ the gates' C helpers (screenshot, audio, and serial checks)
tests/goldens/uber/ frozen UBER conformance hashes
install.sh installs toolchains, emulators and firmware
installer/ the installer's component recipes, patches, lists
toolchains/ everything install.sh downloads and builds (not in
git; delete it any time and re-run install.sh)
make/ per-target Makefile fragments: each port's library and
program rules (make/<port>.mk), the shared program
rules (programs.mk), JoeyLib's examples (examples.mk),
the entry point for outside projects (project.mk);
make/support: build helpers
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
bool noAudio; // true: no audio; else jlInit starts it
} 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, DOS, DVX, SDL3, the web, and the PSP, 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 JL_SURFACE_WIDTH 320
#define JL_SURFACE_HEIGHT 200
#define JL_SURFACE_BYTES_PER_ROW 160
#define JL_SURFACE_PIXELS_SIZE (JL_SURFACE_BYTES_PER_ROW * JL_SURFACE_HEIGHT)
#define JL_SURFACE_PALETTE_COUNT 16
#define JL_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 presents to its text
plane, which is planar like the stage (so a present is a copy) but has
one 16-colour palette: a raster interrupt reloads that palette wherever
the SCB changes, which for a single-palette screen is never. A build
switch (X68K_PRESENT_TEXT=0) uses the 256-colour graphics plane the
way the DOS port uses its DAC instead -- each pixel byte is
(scb[y] << 4) | nibble, exact with no interrupt, but slower.
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 JL_TILE_PIXELS_PER_SIDE 8
#define JL_TILE_BYTES_PER_ROW 4
#define JL_TILE_BYTES (JL_TILE_BYTES_PER_ROW * JL_TILE_PIXELS_PER_SIDE)
#define JL_TILE_BLOCKS_PER_ROW (JL_SURFACE_WIDTH / JL_TILE_PIXELS_PER_SIDE) // 40
#define JL_TILE_BLOCKS_PER_COL (JL_SURFACE_HEIGHT / JL_TILE_PIXELS_PER_SIDE) // 25
#define JL_TILE_NO_GLYPH ((uint16_t)0xFFFFu)
typedef struct jlTileT { uint8_t pixels[JL_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 --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 --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
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 (C, built
into build/tools/). 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 and X68000 plane-major; Atari ST row-major-with- planes-per-row; DOS / IIgs chunky 4bpp). The loaderjlTileBankLoadrejects files baked for the wrong target..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.
build/tools/assetbake --type tile --target {amiga|atarist|dos|iigs|x68000} in.png out.tbk
build/tools/assetbake --type sprite --cell WxH in.png out.spr
A program's description wires its bake rules once for every target
(make/examples.mk, or a project's own Makefile), so make <target>
produces the baked blobs and stages them into the
runtime tree at build/<target>/.../DATA/ -- on the IIgs and DVX into
build/<target>/data/<name>/DATA/, which the disk packer and the DVX SDK
package with the program.
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.
Program icons (baked at build time)
Every program gets an icon on each system that shows programs by one --
the IIgs Finder, Workbench, the Program Managers of DVX and Windows 3.1,
Explorer, Tracker, the PSP's menu, the SDL3 window, and the web page --
baked by tools/iconBake from one picture: the program's ID_ICON (a
PNG), or JoeyLib's kangaroo (assets/joeyIcon.png). docs/BuildIntegration.adoc
(Icons) says what each system gets, and scripts/verify-all.sh icon checks
that each one shows it.
Content creation tools
docs/Assets.adoc (Content Creation Tools) recommends a tool for each kind
of asset: GrafX2 or LibreSprite for tile and sprite sheets, Furnace for
chip-tracker music (./install.sh install furnace; its VGM exports become
.song files through tools/vgm2song), MilkyTracker or OpenMPT for
.MOD music, and sfxr, jfxr, Audacity, or SoX for sound effects.
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.
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 claudeDocs/input.md
for per-port sources and limitations.
typedef enum { /* JL_KEY_NONE, JL_KEY_A..JL_KEY_Z, JL_KEY_0..JL_KEY_9, JL_KEY_SPACE,
JL_KEY_ESCAPE, JL_KEY_RETURN, JL_KEY_TAB, JL_KEY_BACKSPACE,
JL_KEY_UP/DOWN/LEFT/RIGHT, JL_KEY_LSHIFT/RSHIFT/LCTRL/LALT,
JL_KEY_F1..JL_KEY_F10, JL_KEY_COUNT */ } jlKeyE;
typedef enum { JL_MOUSE_BUTTON_NONE, JL_MOUSE_BUTTON_LEFT, JL_MOUSE_BUTTON_RIGHT,
JL_MOUSE_BUTTON_MIDDLE, JL_MOUSE_BUTTON_COUNT } jlMouseButtonE;
typedef enum { JL_JOYSTICK_0, JL_JOYSTICK_1, JL_JOYSTICK_COUNT } jlJoystickE;
typedef enum { JL_JOY_BUTTON_0, JL_JOY_BUTTON_1, JL_JOY_BUTTON_COUNT } jlJoyButtonE;
#define JL_JOYSTICK_AXIS_MAX 127
#define JL_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
DOS/ST/X68000; .amod for the Amiga; .ntp for the IIgs). The Amiga
plays .mod through PTPlayer and the IIgs through the NinjaTrackerPlus
replayer; DOS, the Atari ST, the X68000, and the hosted ports mix it
with JoeyLib's module player, PTPlayer's replay in C. jlInit starts
audio (jlConfigT.noAudio leaves it off) and jlShutdown stops it;
sound that does not start is non-fatal, and the rest of the API stays
callable as no-ops.
#define JL_AUDIO_SFX_SLOTS 5
#define JL_AUDIO_VOICES 3
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
// (from text notation, or from a tracker: write it in Furnace for the
// SN76489 or AY-3-8910, export VGM, convert with tools/vgm2song),
// 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: claudeDocs/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: claudeDocs/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
the x68kSerialPeer gate tool (tests/gate/x68kSerialPeer.c, built by
make -f make/tools.mk x68kSerialPeer into build/tools/), whose output is
line-buffered, so a killed peer keeps everything it captured:
build/tools/x68kSerialPeer --listen-only --until '----- end -----' --timeout 3600 | tee x68.log
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 / _X68000 / _DVX / _HOSTED // exactly one defined
JOEYLIB_HOSTED_SDL3 / _WIN32 / _WIN16 // the hosted backend
JOEYLIB_ATARI_STE // with _ATARIST: the STe build (make ste)
JL_SPRITE_INTERPRETED // no sprite code emitter (hosted but Win16, blank)
JL_CPU_65816 / _68000 / _X86
JL_ENDIAN_LITTLE / _BIG
JL_NATIVE_CHUNKY / _NATIVE_PLANAR
JL_HAS_BLITTER / _HAS_COPPER // Amiga only
JL_PLATFORM_NAME // human-readable string
JL_VERSION_MAJOR / _MINOR / _PATCH / _STRING
License
JoeyLib is distributed under the MIT License (LICENSE.md).
A program built with it has JoeyLib compiled in, so it carries JoeyLib's
copyright and permission notice, in its documentation for instance. Each
of JoeyLib's source files opens with a short header that says so
(SPDX-License-Identifier: MIT); a new file gets the same.
The components JoeyLib depends on, their licenses, and what a shipped
program owes them are in LICENSES.md. The patches in
installer/patches/ are under the licenses of the projects they patch.