413 lines
15 KiB
Markdown
413 lines
15 KiB
Markdown
# 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:
|
|
|
|
```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
|
|
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.
|
|
|
|
```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.
|
|
|
|
```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);
|
|
|
|
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`)
|
|
|
|
```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.
|
|
|
|
```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)
|
|
|
|
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.
|
|
|
|
```c
|
|
#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.
|
|
|
|
```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.
|
|
|
|
|
|
### 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.
|