807 lines
37 KiB
Markdown
807 lines
37 KiB
Markdown
# 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 `.app`
|
|
that 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/hosted` with 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.PBP` in a folder for `PSP/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.
|
|
|
|
See `claudeDocs/DESIGN.md` for the full 1.0 design.
|
|
|
|
|
|
## 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. `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, 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
|
|
`tools/diff-uber-hashes`). 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 && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/sdl3/bin/joeylog.txt
|
|
scripts/bench-web.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/web/bin/UBER/joeylog.txt
|
|
scripts/bench-psp.sh && tools/diff-uber-hashes 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/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/vgm2song.py a tracker's VGM export (Furnace) -> chip-tracker .song
|
|
tools/iconBake.py one picture -> every system's program icon
|
|
tools/xdftool.py Human68k FAT12 .XDF disk image reader / writer
|
|
tools/diff-uber-hashes UBER golden-hash comparator
|
|
examples/ example programs
|
|
joey-run run a built program in its emulator
|
|
scripts/ bench / verification scripts (scripts/lib: shared helpers)
|
|
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:
|
|
|
|
```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
|
|
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.
|
|
|
|
```c
|
|
#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).
|
|
|
|
```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 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.
|
|
|
|
```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 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/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
|
|
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 and X68000 plane-major; Atari ST row-major-with-
|
|
planes-per-row; DOS / IIgs chunky 4bpp). The loader `jlTileBankLoad`
|
|
rejects 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.
|
|
|
|
```
|
|
tools/assetbake/assetbake.py --type tile --target {amiga|atarist|dos|iigs|x68000} in.png out.tbk
|
|
tools/assetbake/assetbake.py --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.py` 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.py`), MilkyTracker or OpenMPT for
|
|
`.MOD` music, and sfxr, jfxr, Audacity, or SoX for sound effects.
|
|
|
|
|
|
### 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 `claudeDocs/input.md`
|
|
for per-port sources and limitations.
|
|
|
|
```c
|
|
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.
|
|
|
|
```c
|
|
#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.py),
|
|
// 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: [`claudeDocs/serial.md`](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`.
|
|
|
|
```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: [`claudeDocs/save.md`](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.
|
|
|
|
```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 / _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](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](LICENSES.md). The patches in
|
|
`installer/patches/` are under the licenses of the projects they patch.
|