JoeyLib 2.0
Find a file
2026-05-27 20:22:24 -05:00
assets MOD and SFX support. 2026-04-25 13:11:46 -05:00
examples Optimized a bit better. Examples are a mess of broken crap. 2026-05-27 20:22:24 -05:00
include/joey Optimized a bit better. Examples are a mess of broken crap. 2026-05-27 20:22:24 -05:00
make Optimized a bit better. Examples are a mess of broken crap. 2026-05-27 20:22:24 -05:00
scripts Optimized a bit better. Examples are a mess of broken crap. 2026-05-27 20:22:24 -05:00
src Optimized a bit better. Examples are a mess of broken crap. 2026-05-27 20:22:24 -05:00
tests/agi Optimized a bit better. Examples are a mess of broken crap. 2026-05-27 20:22:24 -05:00
toolchains LOTS of optimizations for the IIgs. 2026-04-30 01:27:17 -05:00
tools Optimized a bit better. Examples are a mess of broken crap. 2026-05-27 20:22:24 -05:00
.gitattributes MOD and SFX support. 2026-04-25 13:11:46 -05:00
.gitignore Optimized a bit better. Examples are a mess of broken crap. 2026-05-27 20:22:24 -05:00
AGI-SESSION-HANDOFF.md Optimized a bit better. Examples are a mess of broken crap. 2026-05-27 20:22:24 -05:00
LICENSES.md JoeyLib 68k ASM code changed to GAS format to avoid vasm commerical license restrictions. 2026-04-26 21:03:06 -05:00
Makefile (Untested) Joystick support. Basic sprites. 2026-04-24 20:09:18 -05:00
PERF.md Optimized a bit better. Examples are a mess of broken crap. 2026-05-27 20:22:24 -05:00
README.md Optimized a bit better. Examples are a mess of broken crap. 2026-05-27 20:22:24 -05:00

JoeyLib

A unified C game-development library targeting four 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)

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, keys, joy, sprite, audio) for each.

Building for a single target

source toolchains/env.sh
make iigs
make amiga
make atarist
make dos

Repository layout

docs/                 design and reference documentation
include/joey/         public headers
src/core/             portable library code
src/codegen/          runtime sprite codegen (per-CPU emitters)
src/port/<plat>/      per-platform HAL implementations
src/shared68k/        assembly shared by Amiga and Atari ST
tools/assetbake/      PNG -> native .tbk / .spr baker (Python)
tools/joeymod/        Protracker .MOD converter (passthrough or .NTP)
examples/             example programs
toolchains/           self-contained cross-build tools
make/                 per-target Makefile fragments
build/<plat>/         per-target build outputs

Public API

Game code includes a single umbrella header:

#include <joey/joey.h>

That pulls in every public surface listed below. Full documentation lives in the per-feature headers under include/joey/; what follows is a quick reference. Every entry point is plain C, no C++ extensions.

Lifecycle (joey/core.h)

typedef struct {
    uint32_t  codegenBytes;   // runtime compiled-sprite cache size
    uint16_t  maxSurfaces;    // maximum concurrent surfaces
    uint32_t  audioBytes;     // audio sample / module RAM pool
} jlConfigT;

bool        jlInit         (const jlConfigT *config);
void        jlShutdown     (void);
const char *jlLastError    (void);
const char *jlPlatformName (void);
const char *jlVersionString(void);

void        jlWaitVBL      (void);     // block until next VBL
uint16_t    jlFrameCount   (void);     // monotonic 16-bit frame counter
uint16_t    jlFrameHz      (void);     // 50 / 60 / 70 depending on port

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, word-interleaved planes on Atari ST) on the 68k ports. The public API speaks in color indices (0..15) and hides the storage format.

#define SURFACE_WIDTH               320
#define SURFACE_HEIGHT              200
#define SURFACE_BYTES_PER_ROW       160
#define SURFACE_PIXELS_SIZE         (SURFACE_BYTES_PER_ROW * SURFACE_HEIGHT)
#define SURFACE_PALETTE_COUNT       16
#define SURFACE_COLORS_PER_PALETTE  16

typedef struct jlSurfaceT jlSurfaceT;     // opaque

jlSurfaceT *jlSurfaceCreate (void);
void      jlSurfaceDestroy(jlSurfaceT *s);
jlSurfaceT *jlStageGet      (void);                              // library back-buffer
void      jlSurfaceCopy   (jlSurfaceT *dst, const jlSurfaceT *src);

bool      jlSurfaceSaveFile(const jlSurfaceT *src, const char *path);
bool      jlSurfaceLoadFile(jlSurfaceT       *dst, const char *path);
uint32_t  jlSurfaceHash    (const jlSurfaceT *s);                // FNV-1a of logical pixels

jlSurfaceSaveFile writes the surface in target-native form. Files are NOT cross-port portable; the asset pipeline handles conversion.

Drawing (joey/draw.h)

All primitives clip to the surface; off-surface coords are silent no-ops. Color 0 is plotted normally (use the masked variants if you need transparency).

void jlSurfaceClear      (jlSurfaceT *s, uint8_t color);
void jlDrawPixel         (jlSurfaceT *s, int16_t x, int16_t y, uint8_t color);
uint8_t jlSamplePixel    (const jlSurfaceT *s, int16_t x, int16_t y);

void jlDrawLine          (jlSurfaceT *s, int16_t x0, int16_t y0,
                        int16_t x1, int16_t y1, uint8_t color);
void jlDrawRect          (jlSurfaceT *s, int16_t x, int16_t y,
                        uint16_t w, uint16_t h, uint8_t color);
void jlFillRect          (jlSurfaceT *s, int16_t x, int16_t y,
                        uint16_t w, uint16_t h, uint8_t color);
void jlDrawCircle        (jlSurfaceT *s, int16_t cx, int16_t cy,
                        uint16_t r, uint8_t color);
void jlFillCircle        (jlSurfaceT *s, int16_t cx, int16_t cy,
                        uint16_t r, uint8_t color);

void jlFloodFill         (jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
void jlFloodFillBounded  (jlSurfaceT *s, int16_t x, int16_t y,
                        uint8_t newColor, uint8_t boundaryColor);

Palette and SCB (joey/palette.h)

Colors are 12-bit $0RGB. Color 0 of every palette is forced to black on jlPaletteSet. Each scanline picks one of the 16 palettes via the SCB.

void    jlPaletteSet  (jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
void    jlPaletteGet  (const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
void    jlScbSet      (jlSurfaceT *s, uint16_t line, uint8_t paletteIndex);
void    jlScbSetRange (jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine,
                     uint8_t paletteIndex);
uint8_t jlScbGet      (const jlSurfaceT *s, uint16_t line);

Tiles (joey/tile.h)

A "tile" is just an 8x8-aligned region of any surface. The API moves 32-byte chunks between surfaces and provides a small jlTileT value type so callers can stash a copy without allocating a scratch surface.

#define TILE_PIXELS_PER_SIDE  8
#define TILE_BYTES_PER_ROW    4
#define TILE_BYTES            (TILE_BYTES_PER_ROW * TILE_PIXELS_PER_SIDE)
#define TILE_BLOCKS_PER_ROW   (SURFACE_WIDTH  / TILE_PIXELS_PER_SIDE)  // 40
#define TILE_BLOCKS_PER_COL   (SURFACE_HEIGHT / TILE_PIXELS_PER_SIDE)  // 25
#define TILE_NO_GLYPH         ((uint16_t)0xFFFFu)

typedef struct jlTileT { uint8_t pixels[TILE_BYTES]; } jlTileT;

void jlTileCopy       (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
                     const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
void jlTileCopyMasked (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
                     const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy,
                     uint8_t transparentIndex);
void jlTileFill       (jlSurfaceT *s,   uint8_t bx,    uint8_t by,    uint8_t color);
void jlTileSnap       (const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out);
void jlTilePaste      (jlSurfaceT *dst, uint8_t bx, uint8_t by,       const jlTileT *in);
void jlTilePasteMono  (jlSurfaceT *dst, uint8_t bx, uint8_t by,
                     const jlTileT *in, uint8_t fgColor, uint8_t bgColor);

// Load up to maxTiles tiles from a baked .tbk file (per-target planar
// bytes from tools/assetbake/assetbake.py --type tile --target ...).
// Refuses files baked for the wrong target. outValid (optional) marks
// each loaded slot; outPalette (optional) receives the embedded
// 16-entry $0RGB palette if present.
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
                       bool *outValid, uint16_t *outPalette);

void jlDrawText       (jlSurfaceT *dst, uint8_t bx, uint8_t by,
                     const jlSurfaceT *fontSurface, const uint16_t *asciiMap,
                     const char *str);

Sprites (joey/sprite.h)

Rectangles of 8x8 tiles drawn at arbitrary pixel positions with color-0 transparency. Tile data is widthTiles * heightTiles * 32 bytes, tile-major 4bpp packed. Sprites can be runtime-compiled into per-shift code variants for fast draws.

typedef struct jlSpriteT jlSpriteT;            // opaque

typedef struct {
    jlSpriteT  *sprite;
    int16_t   x, y;
    uint16_t  width, height;               // pixels
    uint8_t  *bytes;                       // caller-owned save-under buffer
    uint16_t  sizeBytes;
} jlSpriteBackupT;

jlSpriteT *jlSpriteCreate            (const uint8_t *tileData,
                                  uint8_t widthTiles, uint8_t heightTiles);
jlSpriteT *jlSpriteCreateFromSurface (const jlSurfaceT *src, int16_t x, int16_t y,
                                  uint8_t widthTiles, uint8_t heightTiles);
void     jlSpriteDestroy           (jlSpriteT *sp);

// Load up to maxCels cels from a baked .spr file (cross-target chunky
// 4bpp blob from tools/assetbake/assetbake.py --type sprite). Each
// cel becomes a freshly-allocated jlSpriteT (release with
// jlSpriteDestroy). outPalette (optional) receives the embedded
// 16-entry $0RGB palette if present.
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels,
                         uint16_t maxCels, uint16_t *outPalette);

bool     jlSpriteCompile           (jlSpriteT *sp);   // build per-shift fast path
void     jlSpritePrewarm           (jlSpriteT *sp);   // hint: compile if not already

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

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.
  • .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,iigs}.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/.

For runtime-extracted content (sprites peeled out of a procedural or captured surface), use jlSpriteCreateFromSurface -- the on-the-fly path stays alongside the baked load API.

Present (joey/present.h)

void jlStagePresent(void);

Flips the dirty rows of the stage to the display, then clears dirty state. Drawing primitives mark dirty as a side effect, so calling jlStagePresent once at end-of-frame is enough.

Input (joey/input.h)

Call jlInputPoll once per frame, then query the state predicates. Edge predicates (*Pressed, *Released) fire only in the frame the transition happened.

typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE,
                  KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
                  KEY_UP/DOWN/LEFT/RIGHT, KEY_LSHIFT/RSHIFT/LCTRL/LALT,
                  KEY_F1..KEY_F10, KEY_COUNT */ } jlKeyE;
typedef enum { MOUSE_BUTTON_NONE, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT,
               MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } jlMouseButtonE;
typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE;
typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE;

#define JOYSTICK_AXIS_MAX  127
#define JOYSTICK_AXIS_MIN  (-127)

void    jlInputPoll        (void);
void    jlWaitForAnyKey    (void);

bool    jlKeyDown          (jlKeyE key);
bool    jlKeyPressed       (jlKeyE key);
bool    jlKeyReleased      (jlKeyE key);

int16_t jlMouseX           (void);
int16_t jlMouseY           (void);
bool    jlMouseDown        (jlMouseButtonE b);
bool    jlMousePressed     (jlMouseButtonE b);
bool    jlMouseReleased    (jlMouseButtonE b);

bool    jlJoystickConnected(jlJoystickE js);
int8_t  jlJoystickX        (jlJoystickE js);
int8_t  jlJoystickY        (jlJoystickE js);
bool    jlJoyDown          (jlJoystickE js, jlJoyButtonE b);
bool    jlJoyPressed       (jlJoystickE js, jlJoyButtonE b);
bool    jlJoyReleased      (jlJoystickE js, jlJoyButtonE b);
void    jlJoystickReset    (jlJoystickE js, uint8_t deadZone);

Audio (joey/audio.h)

4-channel Protracker-style music plus four one-shot SFX slots. Module data must be the platform-native form produced by tools/joeymod (.mod for Amiga/DOS/ST; .ntp for IIgs; .amod if you want loop=false on Amiga). A failed jlAudioInit is non-fatal; the rest of the API stays callable as no-ops.

#define JOEY_AUDIO_SFX_SLOTS  4

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 jlAudioStopSfx       (uint8_t slot);

void jlAudioFrameTick     (void);

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.

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.