Support for using Furnace as audio tracker. Icon support for JoeyLib apps. MANY bugs fixed.

This commit is contained in:
Scott Duensing 2026-10-15 17:48:32 -05:00
parent 71d1cb47a4
commit d33d9c9c25
346 changed files with 25392 additions and 8104 deletions

View file

@ -5,9 +5,10 @@ file inventories every component JoeyLib depends on but does not contain:
what is linked into or shipped with the programs it builds, what builds
them, and what tests them. JoeyLib's own tree holds no third-party code or
assets, apart from the patches in `installer/patches/`, which are under
the licenses of the projects they patch (noted in those entries).
the licenses of the projects they patch (noted in those entries), and the
BeOS palette in `tools/iconBake.py` (Haiku's, MIT; noted below).
Components are grouped by role; each entry lists the platforms that use
it, where it comes from, its license and where the installer puts it.
it, where it comes from, its license, and where the installer puts it.
When upstream license terms change, update the corresponding entry here.
This file summarises; the license text that comes with each component is
@ -20,23 +21,13 @@ what counts.
These end up in what a JoeyLib program ships as, so their terms matter
most.
### libxmp-lite 4.7.0
- **Used on:** DOS, Atari ST, X68000, SDL3, PSP, web, Win16, Win32, Haiku,
BeOS
- **URL:** <https://github.com/libxmp/libxmp>
- **License:** MIT
- **Role:** ProTracker / Amiga MOD replayer + SMIX SFX overlay. Each
port's makefile compiles it into AUDIO programs.
- **Installed at:** `toolchains/opt/libxmp/` (installer component `libxmp`)
### SDL 3.4.18
- **Used on:** SDL3, PSP, web
- **URL:** <https://www.libsdl.org/>
- **License:** zlib
- **Role:** window, input and audio for the hosted SDL3 code. Built as a
static library for the host, for the PSP with pspdev and for Emscripten
- **Role:** window, input, and audio for the hosted SDL3 code. Built as a
static library for the host, for the PSP with pspdev, and for Emscripten
(with JoeyLib's patches, `installer/patches/sdl3Web/`, which are under
SDL's zlib license), and linked into each program.
- **Installed at:** `toolchains/opt/sdl3/`, `toolchains/opt/sdl3Psp/`,
@ -60,12 +51,15 @@ most.
### PTPlayer 6.4
- **Used on:** Amiga
- **Used on:** Amiga; and, translated to C, DOS, Atari ST, X68000, SDL3,
PSP, web, Win16, Win32, Haiku, BeOS
- **URL:** <http://aminet.net/mus/play/ptplayer.lha>
(author Frank Wille, <http://aminet.net/package/mus/play/ptplayer>)
- **License:** Public Domain (the archive's `license` file: the Unlicense)
- **Role:** ProTracker MOD replayer + `mt_playfx` SFX channel for the
Paula audio HAL.
Paula audio HAL. Its replay, translated to C (with its period table), is
JoeyLib's module player, `src/core/modPlayer.c`, which every port that
mixes modules in software links.
- **Installed at:** `toolchains/opt/ptplayer/` (installer component `ptplayer`)
### CWSDPMI r7
@ -84,7 +78,7 @@ most.
### The toolchains' C libraries and start files
Each toolchain below also links its own C library, start files and
Each toolchain below also links its own C library, start files, and
compiler runtime into the programs it builds. What a shipped program owes
them, in brief (the entries below have the detail):
@ -92,7 +86,7 @@ them, in brief (the entries below have the detail):
source if its users are told how to get DJGPP (`copying.dj`). DVX
programs are modules DVX loads, built with DVX's SDK.
- **Atari ST:** MiNTLib, file by file under its own license (COPYMINT),
the LGPL 2 or later, BSD, Henry Spencer's and musl's MIT terms; a linked
the LGPL 2 or later, BSD, and Henry Spencer's and musl's MIT terms; a linked
program meets all of them (MiNTLib's `LICENSES`).
- **Amiga:** libnix (public domain).
- **X68000:** newlib (a collection of permissive licenses,
@ -177,7 +171,7 @@ above.
(a fork of llvm-mos, <https://github.com/llvm-mos/llvm-mos>)
- **License:** Apache License 2.0 with LLVM Exceptions, llvm-mos's (and
LLVM's) license, for all of it: llvm-mos's code and the W65816 backend,
link816, omfEmit and the runtime (its `LICENSE.TXT`). Its README lists
link816, omfEmit, and the runtime (its `LICENSE.TXT`). Its README lists
the third-party code it holds under other licenses (the C++ runtime's
Embedded Template Library, MIT; test and demo sources), none of which
reaches a JoeyLib program.
@ -256,7 +250,7 @@ above.
(Vincent Riviere's builds; mintlib: <https://github.com/freemint/mintlib>)
- **License:** gcc 15.2 and binutils 2.45: GNU GPL v3, with the GCC
Runtime Library Exception for libgcc. MiNTLib 0.60.1: per file, under
the MiNTLib license (COPYMINT), the LGPL 2 or later, BSD, Henry
the MiNTLib license (COPYMINT), the LGPL 2 or later, BSD, or Henry
Spencer's or musl's MIT terms; a program linked with it meets all of
them (MiNTLib's `LICENSES`).
- **Role:** GEMDOS-native gcc + binutils + mintlib used to compile
@ -322,9 +316,9 @@ above.
- **Used on:** Win32
- **URL:** <https://github.com/mstorsjo/llvm-mingw>
- **License:** clang, lld and the LLVM runtimes: Apache License 2.0 with
- **License:** clang, lld, and the LLVM runtimes: Apache License 2.0 with
LLVM Exceptions. mingw-w64's headers and runtime: Zope Public License
2.1, with parts marked public domain, BSD or LGPL.
2.1, with parts marked public domain, BSD, or LGPL.
- **Role:** i686 cross compiler for the win32 port's programs, which link
Windows' MSVCRT.DLL.
- **Installed at:** `toolchains/opt/llvmMingw/` (installer component
@ -338,7 +332,7 @@ above.
Library Exception for libgcc. The PSPSDK: BSD. newlib: its collection
of permissive licenses. Every library pspdev carries has its license
under `psp/share/licenses/`.
- **Role:** psp-gcc, the PSPSDK and the tools that make an EBOOT.PBP.
- **Role:** psp-gcc, the PSPSDK, and the tools that make an EBOOT.PBP.
- **Installed at:** `toolchains/opt/pspdev/` (installer component `pspdev`)
### Emscripten 6.0.11 (emsdk)
@ -376,10 +370,45 @@ above.
distribution, keeping its notice).
- **Role:** `package` unpacks the `.hpkg` packages the sysroots are made
from; `bfs_shell` reads and writes the BFS disks the VMs take their jobs
on.
on; `xres` puts every Be API program's icons into its resources.
- **Installed at:** `toolchains/opt/haikuTools/` (installer component
`haikuTools`)
### The BeOS palette (in `tools/iconBake.py`)
- **Used on:** Haiku, Haiku x86, BeOS
- **URL:** <https://github.com/haiku/haiku> (`headers/private/interface/Palette.h`)
- **License:** MIT (Haiku).
- **Role:** the system's 256 colours, copied into `tools/iconBake.py` as
`BE_PALETTE`, which the Be API programs' icons are baked to (an icon is
indices into it; the palette itself is not in the program).
- **Installed at:** nowhere; it is part of `tools/iconBake.py`.
---
### libxmp-lite 4.7.0
- **Used on:** none. It played modules on DOS, the ST, the X68000 and the
hosted ports until 2026-10-14, when JoeyLib's own module player replaced
it; the installer still installs it (component `libxmp`).
- **URL:** <https://github.com/libxmp/libxmp>
- **License:** MIT
- **Installed at:** `toolchains/opt/libxmp/` (installer component `libxmp`)
## Content tools (optional; not shipped, not linked)
### Furnace 0.6.8.3
- **Used on:** every port (chip-tracker music, through `tools/vgm2song.py`)
- **URL:** <https://github.com/tildearrow/furnace>
- **License:** GPLv2 or later. Its demo songs are not under the GPL: they are
their authors', all rights reserved, and nothing made from them is kept or
shipped (`scripts/check-vgm.sh` converts them in a scratch folder).
- **Role:** the recommended tracker for writing chip-tracker songs
(docs/Assets.adoc); its VGM export is what `tools/vgm2song.py` converts.
- **Installed at:** `toolchains/opt/furnace/` (installer component `furnace`,
optional: `./install.sh install furnace`)
---
## Test-time emulators (not shipped, not linked)
@ -478,13 +507,13 @@ Installed from the Linux distribution by the components named below
numpy (BSD), installed by hand with pip. The script holds none of
potracer's code, and the logo it made is not covered by potracer's
license.
- Building MAME, QEMU, PPSSPP and SDL 3 also takes the distribution's
development packages (SDL2, X11, Wayland, PulseAudio, ALSA, GLib and the
- Building MAME, QEMU, PPSSPP, and SDL 3 also takes the distribution's
development packages (SDL2, X11, Wayland, PulseAudio, ALSA, GLib, and the
rest), each under its own license.
---
## System ROMs, operating systems and disk images (used for testing only)
## System ROMs, operating systems, and disk images (used for testing only)
These are **not** part of JoeyLib distribution. `./install.sh`
places them in `toolchains/firmware/` or `toolchains/opt/` for
@ -539,10 +568,23 @@ project hosts (`JOEY_BUNDLE_URL`); matched by the checksums in
Trio64V+ display driver** (its `.zip`): Microsoft and S3 proprietary.
Installed unattended into the DOSBox PC every win16 run uses. A Win16
program's users bring their own Windows. Component `win311`.
- **Microsoft WinG 1.0 SDK** (`WING10.EXE`): Microsoft proprietary, under
its Microsoft Development License (`LICENSE.TXT`, installed beside the
runtime): the SDK may be used to design, develop, and test software; its
runtime (`WING.DLL`, `WING32.DLL`, `WINGDE.DLL`, `WINGDIB.DRV`,
`WINGPAL.WND`, `SETUPHLP.DLL`, `DVA.386`) may be redistributed only as part
of your own application, not for further redistribution, and with an
indemnity to Microsoft. Microsoft no longer offers it; archive.org keeps a
copy (<https://archive.org/details/WING10>). The win16 port uses WinG when
it is installed on the machine and draws through GDI without it, so
JoeyLib never ships it; a game that wants WinG on its users' machines
ships the runtime under those terms. The installer takes the runtime out
of the SDK into `toolchains/opt/wing/` and the win16 gates install it in
their Windows. Component `wing`.
- **BeOS 5 Professional CD** (its image): Be, Inc. proprietary; the terms
for its start files are Unknown. Its boot floppy and x86 system become
the BeOS R5 VM (component `beosVm`), and its Development package's
headers, start files and system libraries the beos port's sysroot
headers, start files, and system libraries the beos port's sysroot
(component `beosR5`). Every beos program links the start files
(`start_dyn.o`, `init_term_dyn.o`, `crti.o`, `crtn.o`), as every program
built with Be's own tools did; check their terms before shipping one.

View file

@ -24,6 +24,7 @@
# make docs renders the manual to build/docs (HTML + PDF)
# make verify builds every port, then runs every gate (needs emulators)
# make verify-audio runs every port's audio gate (needs emulators)
# make verify-conform grades every recording port against the audio contract
# make verify-serial runs every port's serial loopback gate
# make clean removes all build outputs
#
@ -38,7 +39,7 @@ include $(dir $(firstword $(MAKEFILE_LIST)))make/common.mk
ALL_TARGETS := $(JOEY_PORTS_INSTALLED)
.PHONY: all iigs iigs-disk iigs-verify amiga atarist ste dos x68000 dvx sdl3 web win32 win16 psp haiku haiku32 beos blank-check tools clean help status verify verify-audio verify-serial docs
.PHONY: all iigs iigs-disk iigs-verify amiga atarist ste dos x68000 dvx sdl3 web win32 win16 psp haiku haiku32 beos blank-check tools clean help status verify verify-audio verify-conform verify-serial docs
all: $(ALL_TARGETS) tools
ifeq ($(ALL_TARGETS),)
@ -127,6 +128,12 @@ verify:
verify-audio:
@$(REPO_DIR)/scripts/verify-all.sh audio $(PORTS)
# Every recording port against the one audio contract (AUDPAR, graded by
# tools/gradeAudioConformance.py), sequentially, with the same exit codes.
# Takes a subset: make verify-conform PORTS="sdl3 dos"
verify-conform:
@$(REPO_DIR)/scripts/verify-all.sh conform $(PORTS)
# The host check of the shared serial layer, then every port's loopback gate
# (banner out, probe echoed back), sequentially, with the same exit codes.
# Takes a subset: make verify-serial PORTS="host dos"
@ -159,7 +166,8 @@ help:
@echo " iigs-disk iigs-verify (IIgs MAME visual check)"
@echo " blank-check (host-cc smoke test of the new-port template + gnu89 lint)"
@echo " verify (build every port, then run every gate; PORTS= subset)"
@echo " verify-audio (all 5 audio gates; PORTS=\"atarist amiga\" for a subset)"
@echo " verify-serial (host check + all 5 serial loopback gates; PORTS= subset)"
@echo " verify-audio (every port's audio gate; PORTS=\"atarist amiga\" for a subset)"
@echo " verify-conform (every recording port against the audio contract; PORTS= subset)"
@echo " verify-serial (host check + every port's serial loopback gate; PORTS= subset)"
@echo " docs (manual to build/docs; FORMAT=html for HTML only)"
@echo "Install toolchains first: ./install.sh install toolchains"

132
README.md
View file

@ -28,8 +28,9 @@ and the PSP, from a single codebase:
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; tested under PPSSPP with every memory access checked
for alignment as a PSP checks it)
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.
@ -97,7 +98,7 @@ 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
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
@ -127,9 +128,10 @@ the emulators and their firmware:
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 the BeOS 5 Professional CD -- so supply your
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`,
`.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.
@ -148,8 +150,9 @@ 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 and runs
only on an STe or Mega 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
@ -169,9 +172,9 @@ 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
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
`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
@ -227,6 +230,8 @@ 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
@ -264,6 +269,7 @@ is a quick reference. Every entry point is plain C, no C++ extensions.
```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);
@ -290,18 +296,18 @@ void jlRandomSeed (uint32_t seed);
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
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 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
#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
@ -377,14 +383,14 @@ A "tile" is just an 8x8-aligned region of any surface. The API moves
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)
#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[TILE_BYTES]; } jlTileT;
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);
@ -453,7 +459,6 @@ bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels,
uint16_t count, const uint16_t *palette);
bool jlSpriteCompile (jlSpriteT *sp); // build per-shift fast path
void jlSpritePrewarm (jlSpriteT *sp); // hint: compile if not already
uint32_t jlSpriteCompiledSize (const jlSpriteT *sp);
void jlSpriteDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
@ -517,6 +522,24 @@ 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`)
@ -544,17 +567,17 @@ every port. Refilled by the same `jlInputPoll`; see `claudeDocs/input.md`
for per-port sources and limitations.
```c
typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE,
KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
KEY_UP/DOWN/LEFT/RIGHT, KEY_LSHIFT/RSHIFT/LCTRL/LALT,
KEY_F1..KEY_F10, KEY_COUNT */ } jlKeyE;
typedef enum { MOUSE_BUTTON_NONE, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT,
MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } jlMouseButtonE;
typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE;
typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE;
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 JOYSTICK_AXIS_MAX 127
#define JOYSTICK_AXIS_MIN (-127)
#define JL_JOYSTICK_AXIS_MAX 127
#define JL_JOYSTICK_AXIS_MIN (-127)
#define JL_CHAR_BACKSPACE 0x08
#define JL_CHAR_TAB 0x09
@ -588,20 +611,19 @@ 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
PSG-style tone voices, and a noise channel. Module data must be the
platform-native form produced by `tools/joeymod` (`.mod` for
Amiga/DOS/ST/X68000; `.ntp` for IIgs; `.amod` if you want loop=false on
Amiga). Amiga plays `.mod` through PTPlayer and the IIgs through the
NinjaTrackerPlus replayer; DOS, Atari ST and X68000 decode with
libxmp-lite. A failed `jlAudioInit` is non-fatal; the rest of the API
stays callable as no-ops.
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 JOEY_AUDIO_SFX_SLOTS 5
#define JOEY_AUDIO_VOICES 3
bool jlAudioInit (void);
void jlAudioShutdown (void);
#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);
@ -613,7 +635,9 @@ 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,
// 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);
@ -754,13 +778,13 @@ 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)
JOEYLIB_SPRITE_INTERPRETED // no sprite code emitter (hosted, blank)
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
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
```
@ -772,6 +796,6 @@ 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
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.

BIN
assets/joeyIcon.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -2,25 +2,32 @@
Declared in `<joey/audio.h>`.
There are three layers, and on two platforms they share physical hardware:
There are three layers, and on some platforms they share physical hardware:
* *Module playback* -- `jlAudioPlayMod`, four-channel Protracker `.MOD`.
* *Chip tracker* -- `jlMusicPlay`, a portable event stream driving three tone
voices plus noise at register-write cost, with no CPU mixing. The recommended
music path.
* *Voices and effects* -- `jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx` and
* *Voices and effects* -- `jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and
the streaming forms.
[WARNING]
====
On the Atari ST and Amiga the tone voices *are* the hardware the module player
drives. While a `.MOD` plays, `jlAudioVoice` writes fight the player's register
writes and lose. Choose the module layer or the voice and tracker layer per
scene, never both at once.
On the Amiga the tone voices and the noise *are* the hardware the module
player drives: Paula's four channels. While a `.MOD` plays, `jlAudioVoice` and
`jlAudioNoise` writes fight the player's register writes and lose. On the
plain ST (not the STe build) digital sound -- a module, an effect, or a
stream -- plays through the sound chip's channel A, so tone voice 0 is silent
while it plays and sounds again when it stops; voices 1 and 2 play on. Choose
the module layer or the voice and tracker layer per scene, never both at once.
====
A failed `jlAudioInit` is not fatal: `jlInit` still succeeds and every audio
call becomes a silent no-op.
`jlInit` starts audio and `jlShutdown` stops it; a program that wants none
sets `jlConfigT.noAudio`, and every audio call is then silent. Sound that does
not start is not fatal: `jlInit` still succeeds, modules and sound effects are
silent, `jlAudioTickRegister` refuses, and the log says so. The tone and noise
voices, and so `jlMusicPlay`, still play where they drive a sound chip of
their own: on the Amiga, the ST, DOS, and the X68000.
=== Constants
@ -28,8 +35,8 @@ call becomes a silent no-op.
|===
|Name |Value |Meaning
|`JOEY_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4.
|`JOEY_AUDIO_VOICES` |3 |PSG-style tone voices, 0..2.
|`JL_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4.
|`JL_AUDIO_VOICES` |3 |PSG-style tone voices, 0..2.
|`JL_SFX_RAW_SIGNED8` |0 |Raw stream format: `int8_t` PCM.
|`JL_SFX_RAW_UNSIGNED8_NOZERO` |1 |Raw stream format: `uint8_t` biased 0x80,
and 0x00 is forbidden (it is the DOC stop byte).
@ -37,34 +44,15 @@ call becomes a silent no-op.
[TIP]
====
Prefer *slot 4* for game sound effects when music is playing. The IIgs maps
tracker tone voices onto slots 0..2 and the noise voice onto slot 3. On the
other ports the slots are independent of the voice layer.
Prefer *slot 4* for game sound effects when music is playing. The IIgs plays
tone voices 0..2 on sound effect slots 0..2 and the noise on slot 3: an effect
and a voice on the same slot cut each other off, and a voice still being held
takes its slot back at the next `jlAudioFrameTick`. On the Amiga effects, the
module, the voices, and the noise share Paula's four channels, so an effect can
take a channel a voice is using. On the other ports the slots are independent
of the voice layer.
====
=== jlAudioInit
[source,c]
----
bool jlAudioInit(void);
----
*Parameters:* none.
*Returns:* `true` if the platform has a working audio engine and started it;
`false` silently otherwise, with the rest of the API still callable but silent.
=== jlAudioShutdown
[source,c]
----
void jlAudioShutdown(void);
----
Tears the engine down. Safe when audio was never initialised.
*Parameters:* none. *Returns:* nothing.
=== jlAudioFrameTick
[source,c]
@ -72,10 +60,12 @@ Tears the engine down. Safe when audio was never initialised.
void jlAudioFrameTick(void);
----
Hooks the engine into your game loop. Most platforms drive their engines from a
hardware interrupt and ignore this, but it is required for any port that mixes
in user context, it advances tracker playback, and on IIgs and Amiga it is what
fires the next chunk of a streaming sound effect.
Hooks the engine into your game loop. Every port does its sound work here: DOS,
the ST, the X68000, DVX, and the hosted ports mix or refill their sound buffers
in it, so without it modules and effects stop or repeat; the IIgs and Amiga
feed each streaming sound effect its next chunk, notice a module's end, and
(on the IIgs) keep held notes sounding; and on every port it advances
`jlMusicPlay`.
Call it once per frame, unconditionally.
@ -93,8 +83,9 @@ Begins module playback. If a module is already playing it is replaced.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`data` |`const uint8_t *` |Platform-native module blob: `.mod` on most ports,
`.amod` on Amiga, `.ntp` on IIgs. The asset pipeline produces the right form.
|`data` |`const uint8_t *` |Platform-native module blob: `.mod` on every port
but the IIgs (an `.amod`, with its end marker, plays on the Amiga too), `.ntp`
on the IIgs. The asset pipeline produces the right form.
|`length` |`uint32_t` |Blob size in bytes.
|`loop` |`bool` |`true` plays forever.
|===
@ -103,9 +94,15 @@ Begins module playback. If a module is already playing it is replaced.
[NOTE]
====
`loop = false` needs help on the Amiga: PTPlayer has no native song-end signal,
so the module must contain an `E8FF` marker at song end. The `joeymod` tool's
`.amod` output injects it. A plain `.mod` with no marker loops anyway on Amiga.
`loop = false` plays the song once and stops it. The IIgs's replayer has a
play-once mode; the ports that mix modules in software stop where the song
comes back to a row it has played (its end, or a jump back), at an `F00`, or
at the Amiga's `E8F` stop marker; and the Amiga adds that marker as it copies
the module to chip RAM, sending the song's jumps back to it
(<<_music_protracker_modules>>). A module with no room for one, all 128 song
positions or all 128 patterns in use, loops there anyway, and so does one
whose jump back sits in a pattern it also plays where that jump goes
forward.
====
=== jlAudioStopMod
@ -130,7 +127,8 @@ bool jlAudioIsPlayingMod(void);
*Parameters:* none.
*Returns:* `true` while a module is producing output. `false` after a stop,
before the first play, and on platforms where audio init failed.
once a song played with `loop = false` has ended, before the first play, and on
platforms where audio init failed.
=== jlAudioPlaySfx
@ -146,7 +144,7 @@ new sample replaces it.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`slot` |`uint8_t` |Slot 0..`JOEY_AUDIO_SFX_SLOTS`-1. Prefer 4 alongside music.
|`slot` |`uint8_t` |Slot 0..`JL_AUDIO_SFX_SLOTS`-1. Prefer 4 alongside music.
|`sample` |`const uint8_t *` |Raw signed 8-bit PCM.
|`length` |`uint32_t` |Sample count in bytes.
|`rateHz` |`uint16_t` |The rate the sample was recorded at; the engine
@ -155,6 +153,12 @@ new sample replaces it.
*Returns:* nothing.
Most ports read the sample where it is while it plays, so keep it valid and
unchanged until it has finished or the slot has been stopped. The IIgs also
remembers the last sample each slot played by its address, length, and rate,
and plays its own copy when they match: changed bytes at the same address,
length, and rate are not heard there.
[NOTE]
====
The full -128..127 range is accepted everywhere, but the IIgs plays -128 as
@ -175,9 +179,9 @@ deactivates.
[WARNING]
====
This is called from the audio engine's refill context -- the main thread on DOS,
ST and SDL3 (on DVX, DVX's own sound pump, while the program is yielded), but the
*audio interrupt* on Amiga and IIgs. It must not block and must not allocate.
On every port it is called in program time, from the call that starts the
stream and then from `jlAudioFrameTick` (on DVX, also while the desktop runs).
It must not block, and it should be quick: it runs inside those calls.
====
=== jlAudioPlaySfxStream
@ -206,10 +210,11 @@ with no caller-side allocation.
[IMPORTANT]
====
On IIgs and Amiga the engine plays one chunk at a time and fires the next from
`jlAudioFrameTick`, so *you must be calling `jlAudioFrameTick` every frame* or
the stream stops after its first chunk. A chunk seam on those ports can carry
up to one frame of jitter.
Every port feeds streams from `jlAudioFrameTick`, so *you must be calling
`jlAudioFrameTick` every frame* or the stream stops. On the IIgs and Amiga the
engine plays one chunk at a time (7 KB on the IIgs, 4 KB on the Amiga) and
fires the next from it, so a chunk seam on those ports can carry up to one
frame of jitter.
====
=== jlAudioSfxRawFormat / jlAudioPlaySfxStreamRaw
@ -226,10 +231,10 @@ void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill,
`jlAudioPlaySfxStreamRaw` is `jlAudioPlaySfxStream` except your callback
delivers bytes *already in that format*, so the port skips its per-byte
conversion of every chunk. On the IIgs that conversion is a 65816 byte loop
over each 3.5 KB chunk, and it is enough to stall a game for a third of a
second per chunk of speech. Every other port streams signed PCM natively, so
there this is simply the plain call.
conversion of every chunk. On the IIgs that conversion costs about 7
microseconds a byte, some 50 ms for each 7 KB chunk. Every other port's raw
format is signed PCM, so there this is simply the plain call (DVX still flips
each byte for its sound library).
If your producer cannot make the port's format, use `jlAudioPlaySfxStream`.
@ -240,7 +245,7 @@ If your producer cannot make the port's format, use `jlAudioPlaySfxStream`.
[source,c]
----
typedef enum {
AUDIO_PAN_LEFT = 0, AUDIO_PAN_CENTER, AUDIO_PAN_RIGHT, AUDIO_PAN_COUNT
JL_AUDIO_PAN_LEFT = 0, JL_AUDIO_PAN_CENTER, JL_AUDIO_PAN_RIGHT, JL_AUDIO_PAN_COUNT
} jlAudioPanE;
void jlAudioSetPan(uint8_t slot, jlAudioPanE pan);
@ -268,14 +273,16 @@ What each port can actually do:
|IIgs |All three. The DOC pans per oscillator, so a centred sound plays on two
oscillators, one per side. A stock IIgs sums to mono, so this shows up only on
a stereo card or an emulator.
|Amiga |Left and right only, by choosing the Paula channel side. Paula cannot
place one voice in both ears, so `AUDIO_PAN_CENTER` leaves PTPlayer's own
channel choice alone.
|Amiga |Left and right by choosing the Paula channel side; a Paula channel plays
on one side only. A centred sound plays on a free channel each side when both
sides have one, so it reaches both ears; otherwise on the one channel
PTPlayer picks.
|X68000 |Ignored. Everything mixes into one ADPCM stream, so panning would move
all audio at once.
|DOS, ST |Ignored. Output is mono.
|DOS, ST, STe |Ignored. Output is mono.
|DVX |All three, through DVX's stereo mixer (on a stereo card).
|SDL3 |All three: the port mixes in stereo.
|SDL3, web, Win32, PSP, Haiku, BeOS |All three: the port mixes in stereo.
|Win16 |Ignored. Output is mono.
|===
=== jlAudioStopSfx
@ -300,13 +307,12 @@ void jlAudioStopSfx(uint8_t slot);
void jlAudioTone(uint16_t freqHz);
----
Direct hardware tone generator: plays a square wave continuously until the next
call. Designed for AGI-era music where the engine programs a divisor and lets
the chip play, so CPU cost during playback is zero. Single voice; uses voice 0
on platforms with several oscillators. On DOS this programs PIT counter 2 and
the speaker gate; on DVX it is a square tone of DVX's sound library, on the FM
chip when there is one; on SDL3 it is voice 0 of the port's mixer at full
volume.
A single tone that plays until the next call: `jlAudioVoice(0, freqHz, 0)`,
tone voice 0 at full volume, on every port. Designed for AGI-era music where
the engine programs a divisor and lets the chip play, so CPU cost during
playback is zero. A program that mixes it with `jlAudioVoice` or `jlMusicPlay`
shares voice 0 with them. On DOS without an OPL2 (AdLib) chip, voice 0, and so
this tone, is the PC speaker.
[cols="1,1,4", options="header"]
|===
@ -314,8 +320,7 @@ volume.
|`freqHz` |`uint16_t` |Frequency in Hz. 0 silences the generator.
|===
*Returns:* nothing. A no-op on platforms with no reachable tone generator --
fall back to `jlAudioPlaySfxStream` there if you need synthesised sound.
*Returns:* nothing. Silent wherever voice 0 is (see `jlAudioVoice`).
=== jlAudioVoice
@ -329,33 +334,32 @@ Three-voice PSG-style polyphonic tone generator.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`voice` |`uint8_t` |Voice 0, 1 or 2.
|`voice` |`uint8_t` |Voice 0, 1, or 2.
|`freqHz` |`uint16_t` |Frequency in Hz. 0 silences this voice.
|`atten` |`uint8_t` |AGI / SN76489-style 4-bit attenuation: 0 loudest,
15 silent, about 2 dB per step.
15 silent, 2 dB per step as near as the chip comes (the ST's YM2149 has
sixteen levels about 3.5 dB apart, so a step there is within 1.6 dB).
|===
*Returns:* nothing.
Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2; the ST uses
YM2149 channels A, B and C; the Amiga uses Paula channels 0..2 looping a
two-byte square sample; the IIgs uses Ensoniq DOC oscillators playing a
resident square; DVX plays square tones of its sound library, which puts them
on the FM chip when there is a free voice and mixes them otherwise; SDL3 mixes
square waves in software.
Voice state is sticky -- a voice keeps its last frequency until you change it
-- and CPU cost during sustained playback is zero, because the hardware
oscillates on its own.
Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2 (with no OPL2,
voice 0 is the PC speaker and voices 1 and 2 are silent); the ST uses YM2149
channels A, B, and C (on the plain ST voice 0 is silent while digital sound
plays); the Amiga uses Paula channels 0..2 looping a two-byte square sample;
the IIgs uses Ensoniq DOC oscillators playing a resident square on sound
effect slots 0..2; the X68000 uses YM2151 channels 0..2, one operator each,
so its tones are sine waves; DVX plays square tones of its sound library,
which puts them on the FM chip when there is a free voice and mixes them
otherwise; the hosted ports mix square waves in software. The OPL2's waves
are not square either.
[IMPORTANT]
====
The IIgs is the exception to stickiness: each call is an approximately 80 ms
retriggered one-shot, so a note held longer must be re-asserted about once a
frame. It also requires `jlAudioInit`, because the DOC path rides the tracker
engine. On DOS, ST and Amiga this is direct hardware programming and works
without `jlAudioInit`; on DVX it opens the program's sound on first use. On
SDL3 the voices are mixed in software and sound only after `jlAudioInit`.
A note sounds until the next call for that voice. The IIgs plays each note as
a sample a fraction of a second long and restarts the notes being held from
`jlAudioFrameTick`, so call that every frame, as every program should; a frame
much longer than 80 ms can leave a gap in a held note there.
====
=== jlAudioNoise
@ -378,13 +382,17 @@ layer.
*Returns:* nothing.
Mapping: the ST uses the YM2149 noise generator mixed onto channel C; DOS uses
the OPL2 rhythm-mode snare, leaving the three tone channels untouched; the
Amiga uses Paula channel 3 with an LFSR noise sample; the IIgs uses a resident
noise sample on slot 3 with the same retriggered one-shot model as its tone
voices; DVX uses a noise tone of its sound library; SDL3 mixes the YM2149's
own noise in software (its 17-bit generator, clocked at 125 kHz divided by
the pitch), so it sounds as on the ST.
The noise runs at 125000 / `pitch` Hz (`pitch` 0 as 1), the YM2149's rate, on
every port as far as its chip goes. Mapping: the ST uses the YM2149 noise
generator mixed onto channel C; the X68000 the YM2151's noise, at the same
rates exactly; the Amiga Paula channel 3 looping an LFSR noise sample, whose
fastest rate, 28.6 kHz, pitches 0 to 4 all play at; the IIgs a resident noise
sample on slot 3, up to 26 kHz, likewise for pitches 0 to 4; DVX a noise tone
of its sound library; and the hosted ports mix the YM2149's own noise in
software (its 17-bit generator), so it sounds as on the ST. DOS uses the OPL2
rhythm-mode snare, leaving the three tone channels untouched: a tuned drum,
not free-running noise, whose pitch spreads over the snare's range rather than
following the YM2149's rate.
[WARNING]
====
@ -406,7 +414,7 @@ is no CPU mixing, this is fully viable on the ST and cheap on DOS, which
`.MOD` playback is not.
Playback is pumped by `jlAudioFrameTick` and paced by `jlMillisElapsed`, so
tempo holds across 50, 60 and 70 Hz ports.
tempo holds across 50, 60, and 70 Hz ports.
[cols="1,1,4", options="header"]
|===
@ -419,13 +427,8 @@ tempo holds across 50, 60 and 70 Hz ports.
|===
*Returns:* `true` on success; `false` on a malformed stream, in which case
nothing plays.
[NOTE]
====
Call `jlAudioInit` first for the song to be audible on the IIgs -- see
`jlAudioVoice`.
====
nothing plays. With `jlConfigT.noAudio` the song runs silently, its time and
its end as usual.
=== jlMusicStop
@ -507,35 +510,55 @@ frame, so the chip only hears the last write and short notes vanish.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`fn` |`jlAudioTickFnT` |Callback, fired from interrupt context. Pass `NULL` to
uninstall.
|`fn` |`jlAudioTickFnT` |Callback, which may be fired from an interrupt. Pass
`NULL` to uninstall.
|`hz` |`uint16_t` |Firings per second.
|===
*Returns:* `true` if installed. Ports without a suitable timer source return
`false`.
*Returns:* `true` if installed. `false` when `hz` is 0, when audio is off or
did not start, or when the port cannot take the rate (above 1000 Hz on DVX, or
above the mixer's rate on the hosted ports). Uninstalling always returns
`true`.
[WARNING]
====
`fn` runs in interrupt context. Keep it to memory reads and direct hardware
register writes -- no allocation, no `printf`, nothing blocking.
`fn` may run in an interrupt: it does on DOS, the Amiga, the ST, and the
X68000. Keep it to memory reads and direct hardware register writes -- no
allocation, no `printf`, nothing blocking -- and share data with it through
`jlAudioCriticalEnter` and `jlAudioCriticalExit`.
====
On every port the calls average exactly `hz` a second; where they fall
differs. DOS, the Amiga, and the hosted ports place each one at its own time.
The ST and X68000 deliver them from the system's own timer, 200 and 100 times a
second, so the calls due between two of its ticks come together. The IIgs and
DVX deliver them in program time from `jlAudioFrameTick`, so a frame's calls
come together at it.
On DOS this reprograms PIT counter 0 and hooks INT 8, keeping the BIOS tick
counter advancing at the canonical 18.2 Hz through an accumulator so `time`
stays correct. `jlMillisElapsed` carries on from where it was when the tick
starts or stops; it never jumps back.
On the Amiga a `timer.device` request on the E-clock raises a software
interrupt at each tick. On the ST the tick rides TOS's 200 Hz Timer C
interrupt, and on the X68000 Human68k's 100 Hz Timer C interrupt.
On the IIgs `fn` runs in program time, at `jlAudioFrameTick`, as many times as
the display's beats since the last frame have made due, so the average is
exact whatever the program's frame rate.
On DVX a timer of DVX's (at most 1000 Hz) counts the ticks, and `fn` runs in
program time, once per counted tick, at the next `jlAudioFrameTick`: DVX's
sound library, which `jlAudioVoice` and the rest go through there, may not be
called from an interrupt. `fn` may therefore do anything, but what it does
lands on the frame, not between frames.
On SDL3 the sound is mixed in program time, a few frames ahead, and `fn` is
called by the mixer between exactly the two samples where its tick falls: in
program time, so it may do anything, yet a note it starts begins at the right
place in the sound. `hz` may be anything up to the mixer's 44100.
On the hosted ports the sound is mixed in program time, a few frames ahead,
and `fn` is called by the mixer between exactly the two samples where its tick
falls: in program time, so it may do anything, yet a note it starts begins at
the right place in the sound. `hz` may be anything up to the mixer's rate,
44100 (22050 on Win16).
=== jlAudioCriticalEnter / jlAudioCriticalExit
@ -547,8 +570,10 @@ void jlAudioCriticalExit(void);
Critical section against the tick interrupt. If you mutate state that the
registered tick callback reads, wrap the mutation in these so the interrupt
cannot observe a half-written value. On DOS this is `cli` and `sti`; on ports
with no audio interrupt, and on DVX and SDL3, where the callback runs in
program time, both are no-ops.
cannot observe a half-written value. On DOS, the Amiga, the ST, and the
X68000 a section holds every interrupt off, so keep it short. Sections nest:
only the outermost pair does anything, and its Exit puts back the interrupt
state its Enter found. On the IIgs, DVX, and the hosted ports, whose
callbacks run in program time, they do nothing, and need not.
*Parameters:* none. *Returns:* nothing.

View file

@ -8,6 +8,7 @@ Declared in `<joey/core.h>`, included by `<joey/joey.h>`.
----
typedef struct {
uint32_t codegenBytes;
bool noAudio;
} jlConfigT;
----
@ -18,13 +19,26 @@ typedef struct {
|`codegenBytes`
|`uint32_t`
|Size of the runtime compiled-sprite cache. `0` selects a per-platform
default: 8 KB on IIgs, 32 KB on Amiga, ST, DOS and DVX (SDL3 interprets every
sprite and leaves the cache unused). On the IIgs the cache is
one Memory Manager block that may not span a 64 KB bank boundary, so values
above 65536 are clamped to 65536, and even a clamped request can fail if no
free bank exists. Size it against `jlSpriteCompiledSize()`.
default: 8 KB on the IIgs, 64 KB on the Amiga and ST, and 32 KB on the
X68000, DOS, DVX, and Win16. The other hosted ports interpret every sprite
and never use the cache, but still allocate it (32 KB by default). The
cache is one block, and `jlInit` fails if it cannot be had -- except on the
IIgs, where it is up to four Memory Manager blocks of at most 64 KB, each
inside one 64 KB bank, so at most 256 KB; a bank that is refused is tried
again at 60 KB, and `jlInit` takes whatever part of the request it was
granted. Size it against `jlSpriteCompiledSize()`.
|`noAudio`
|`bool`
|`true` leaves audio off: every audio call is silent, as on a machine without
sound, and the program pays nothing for audio (on the IIgs no replayer read
from disk, on DOS and the Amiga no interrupt hooked). `false` has `jlInit`
start audio and `jlShutdown` stop it.
|===
Set every field: a `jlConfigT` is read whole, so a field left unset is
whatever the memory held.
=== jlInit
[source,c]
@ -33,8 +47,11 @@ bool jlInit(const jlConfigT *config);
----
Initialises the library: takes over the display, allocates the stage and the
codegen arena, and brings up input. Must be called before any other JoeyLib
function.
codegen arena, brings up input, and last starts audio (unless `noAudio`). Must
be called before any other JoeyLib function. Sound that does not start is not
a failure: modules and sound effects are then silent, and the log says so
(<<_debug_logging>>). The tone and noise voices still play on the ports whose
sound chip needs no engine: the Amiga, the ST, DOS, and the X68000.
[cols="1,1,4", options="header"]
|===
@ -43,7 +60,9 @@ function.
|===
*Returns:* `true` on success. On failure returns `false` and `jlLastError()`
describes the reason.
describes the reason. Where the port's own start-up fails without saying
why, as the IIgs, Amiga, ST, X68000, and DVX ports' can, the reason is just
`jlpInit failed`.
=== jlShutdown
@ -52,8 +71,10 @@ describes the reason.
void jlShutdown(void);
----
Shuts the library down, restoring the display mode, releasing all library
resources and un-hooking any interrupt vectors the port installed. Call it on
Shuts the library down: stops audio first, then restores the display mode,
releases the library's own resources, and un-hooks any interrupt vectors the
port installed. Surfaces and sprites the program created are not released:
destroy them with `jlSurfaceDestroy` and `jlSpriteDestroy` first. Call it on
every exit path; skipping it can leave the machine unusable.
*Parameters:* none. *Returns:* nothing.
@ -104,9 +125,10 @@ void *jlAlloc(uint32_t bytes);
Allocates a large or persistent buffer from the platform's native allocator
rather than the small C heap. *Use this instead of `malloc` for anything
substantial.* On the IIgs the C heap lives in bank 0 and is tiny, with a single
`malloc` capped around 32 KB, so multi-kilobyte caches and frame buffers must
come from the Memory Manager instead.
substantial.* On the IIgs the whole C heap is one Memory Manager block of
32 KB to 48 KB (and, if that block cannot be had, only what the program's link
left in its bank, which can be a few hundred bytes), so multi-kilobyte caches
and frame buffers must come from the Memory Manager instead.
[cols="1,1,4", options="header"]
|===
@ -124,7 +146,9 @@ zero-filled.
void jlFree(void *p);
----
Releases a buffer from `jlAlloc`.
Releases a buffer from `jlAlloc`. Never mix `jlFree` and `free`: on most
ports they are the same thing, but on the IIgs a `jlAlloc` block is a Memory
Manager handle, and either call given the other's block corrupts memory.
[cols="1,1,4", options="header"]
|===
@ -164,8 +188,11 @@ void jlWaitVBL(void);
----
Blocks until the next display vertical blank, pacing a game loop to the
display's native refresh rate. Cheap on every port, because the underlying
mechanism is always a hardware-level wait rather than a software timer.
display's native refresh rate. On the machine ports it waits for the
display's own vertical blank. The hosted ports, and DVX in a window, have no
vertical blank to wait for: there it waits for the next tick of a virtual
frame clock, 60 Hz on the hosted ports and 70 Hz on DVX, and nothing stops a
present from tearing.
*Parameters:* none. *Returns:* nothing.
@ -178,13 +205,15 @@ uint16_t jlFrameCount(void);
*Parameters:* none.
*Returns:* a monotonic 16-bit frame counter.
*Returns:* a monotonic 16-bit frame counter. An interrupt or a clock keeps it,
so it counts every frame however seldom you read it -- except on an X68000
whose system refuses JoeyLib's frame interrupt, where it counts only while
`jlWaitVBL`, `jlFrameCount`, or `jlStagePresent` is being called.
[WARNING]
====
You must poll faster than twice `jlFrameHz()` or edges are missed, and the
counter wraps roughly every 18 minutes at 60 Hz. For elapsed-time work prefer
`jlMillisElapsed`.
The counter wraps roughly every 18 minutes at 60 Hz, so compare two readings
by subtracting them. For elapsed-time work prefer `jlMillisElapsed`.
====
=== jlFrameHz
@ -196,13 +225,16 @@ uint16_t jlFrameHz(void);
*Parameters:* none.
*Returns:* the nominal display frame rate in Hz -- 50 on Amiga PAL, 60 on IIgs
and ST NTSC, 55 on the X68000, 70 on VGA mode 13h and on DVX, whose window has
a virtual 70 Hz (and whose full screen is mode 13h's, or a VBE mode's, with the
same frame count), and 60 under SDL3, whose window has a virtual 60 Hz. The X68000 times its display at `jlInit` and rounds to the
nearest whole number: 55.46 Hz on the machine, 56.07 Hz under MAME, which
therefore reports 56. The real cadence may drift slightly; this is the figure
to divide by when converting frames to seconds.
*Returns:* the nominal display frame rate in Hz -- 60 on the IIgs, or 50 on a
PAL machine; 50 on the Amiga, whose screen is PAL, or 60 on an NTSC Amiga that
cannot show PAL; 50 or 60 on the ST, as the machine is set; 55 on the X68000;
70 on VGA mode 13h and on DVX, whose window has a virtual 70 Hz (and whose full
screen is mode 13h's, or a VBE mode's, with the same frame count); and 60 on
every hosted port, whose window has a virtual 60 Hz. The X68000 times its
display at `jlInit` and rounds to the nearest whole number: 55.46 Hz on the
machine, 56.07 Hz under MAME, which therefore reports 56. The real cadence may
drift slightly; this is the figure to divide by when converting frames to
seconds.
=== jlMillisElapsed
@ -211,10 +243,9 @@ to divide by when converting frames to seconds.
uint32_t jlMillisElapsed(void);
----
Monotonic millisecond counter since `jlInit`. DOS, ST, X68000, DVX and SDL3 read a
real timer; IIgs and Amiga derive it from the frame counter, so the
value steps at frame granularity there. Two calls separated by a long render report the true
elapsed time, not the number of vertical blanks that fired.
Monotonic millisecond counter since `jlInit`. It may advance a frame at a
time. Two calls separated by a long render report the true elapsed time, not
the number of vertical blanks that fired.
*Parameters:* none.
@ -222,8 +253,8 @@ elapsed time, not the number of vertical blanks that fired.
[NOTE]
====
On the frame-derived ports you must poll at least once every 65535 frames --
about 18 minutes at 60 Hz -- or wrapped frames are lost.
Call it at least once every 15 minutes, or the time between two calls can be
lost.
====
=== jlRandom

View file

@ -2,20 +2,40 @@
Declared in `<joey/debug.h>`.
A crash-tracing logger that writes to a file beside the executable (on DVX,
in the program's settings folder, `CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`).
Debugging a program on an emulated 1988 machine is awkward, and this is
usually the fastest way to find out what it did before it stopped doing it.
The headless run scripts retrieve the log automatically.
A crash-tracing logger that writes `joeylog.txt` in the current directory
(on DVX, in the program's settings folder,
`CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`). Debugging a program on an emulated
1988 machine is awkward, and this is usually the fastest way to find out what
it did before it stopped doing it. The headless run scripts retrieve the log
automatically. The library itself writes to it only when something fails, such
as sound that does not start.
How the file is written differs:
* *Every port but the IIgs* opens the file on the first line and appends to
it, writing each line out as it is logged, so a crash or hang leaves every
line before it on disk. The file grows across runs until `jlLogReset`.
* *The IIgs* keeps the lines in an 8 KB ring in memory, because a write per
line costs seconds of floppy time there. Nothing reaches the disk until
`jlLogFlush`, which replaces the file with the last 8 KB logged. A program
that never calls it leaves no log.
* *The web* writes the file into the page's memory, where it is lost when the
page closes.
* *The hosted ports* make the program's folder the current directory in
`jlInit`: a line logged before that goes to a log in the directory the
program was started from, and the rest of the run's lines follow it there.
[NOTE]
====
The logger needs a multi-kilobyte ring buffer, which the tightest targets
cannot spare. Space Taxi compiles it out entirely on the IIgs and X68000 for
that reason. If you are debugging a memory-tight program, expect to do the
same and to observe hardware state through the emulator instead.
The logger's buffer is linked into every program, since the library logs its
own failures: the 8 KB ring on the IIgs, and a 16 KB file buffer on every
other port but DVX. A memory-tight program can still leave its own log calls
out and observe hardware state through the emulator instead.
====
On DOS the library also creates `CRASH.LOG` in the current directory at
`jlInit`, and writes the processor's registers there if the program faults.
=== jlLog
[source,c]
@ -41,13 +61,14 @@ void jlLogF(const char *fmt, ...);
----
Appends a formatted message, `printf` style, as one line: like `jlLog`, the
library ends the line, so the format needs no trailing `\n`.
library ends the line, so the format needs no trailing `\n`. On the IIgs a
line is cut at 199 characters.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`fmt` |`const char *` |Format string.
|`...` | |Arguments matching the format string.
|`+...+` | |Arguments matching the format string.
|===
*Returns:* nothing.
@ -66,8 +87,10 @@ time.
void jlLogFlush(void);
----
Flushes buffered log output to disk. Call it before anything that might crash
or exit, or the last and most interesting lines may never be written.
Flushes buffered log output to disk. On the IIgs it is the only way the log
reaches the disk: it rewrites the whole file with the last 8 KB logged, so call
it before anything that might crash or exit, or the last and most interesting
lines are never written. Every other port has already written each line.
*Parameters:* none. *Returns:* nothing.
@ -79,7 +102,8 @@ void jlLogReset(void);
----
Clears the log, discarding previous contents. Call once at startup so each run
produces a fresh log rather than appending to the last one.
produces a fresh log rather than appending to the last one. On the IIgs it
clears the ring only; the file on disk changes at the next `jlLogFlush`.
*Parameters:* none. *Returns:* nothing.
@ -89,6 +113,7 @@ int main(void) {
jlConfigT config;
config.codegenBytes = 32UL * 1024;
config.noAudio = true;
if (!jlInit(&config)) {
return 1;
}

View file

@ -7,8 +7,9 @@ no-ops, never errors, and there are no unclipped variants in the public API --
you cannot corrupt memory by drawing off the edge. Every primitive marks the
rows it touched, so `jlStagePresent` knows what to copy.
Primitives work in colour indices. Per-scanline palette resolution happens only
at display time, so drawing does not care which palette a line uses.
Primitives work in colour indices, and use only the low 4 bits of one.
Per-scanline palette resolution happens only at display time, so drawing does
not care which palette a line uses.
=== jlSurfaceClear
@ -185,8 +186,10 @@ void jlFloodFill(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
----
Flood-fills a 4-connected region, replacing every pixel of the original colour
reachable by north, south, east and west steps. A no-op if the start point is
off-surface or already `newColor`.
reachable by north, south, east, and west steps. A no-op if the start point is
off-surface or already `newColor`. The fill keeps a 512-entry stack of spans
still to visit, and a region complex enough to need more is left partly
filled, the same way on every port.
[cols="1,1,4", options="header"]
|===
@ -208,7 +211,9 @@ void jlFloodFillBounded(jlSurfaceT *s, int16_t x, int16_t y,
Flood-fills a 4-connected region, stopping at pixels of `boundaryColor` rather
than at pixels differing from the seed colour. Every reachable pixel that is
not the boundary colour becomes `newColor`.
not the boundary colour becomes `newColor`. Pixels that are `newColor` already
stop the fill too, so a region behind them is not reached, and a start on
either colour does nothing. The 512-entry span stack of `jlFloodFill` applies.
This is the primitive for vector-art playback, such as Sierra-style picture
drawing: outline a closed region with `jlDrawLine` in the boundary colour, then

View file

@ -5,27 +5,47 @@ Declared in `<joey/file.h>`.
Two directories sit beside the executable, staged there by each platform's disk
packager:
* `DATA/` -- read-only game content: levels, sprites, audio.
* `DATA/` -- game content: levels, sprites, audio.
* `SAVES/` -- writable saves and preferences.
DVX is the exception, because its programs share one current directory:
there `DATA/` is beside the program, in its folder under DVX's `APPS`, and
`SAVES/` is in the program's own settings folder, `CONFIG\APPS\JOEYLIB\NAME`,
which DVX keeps apart from the program files so a reinstall never overwrites
it. The calls below find both there by themselves.
Where the calls look for them:
* *The hosted ports* make the program's own folder the current directory in
`jlInit`, so both are found beside the program wherever it was started
from.
* *DOS, the ST, the Amiga, and the X68000* look in the current directory. A
program started from its own folder (as a desktop starts it) finds them;
one started from a command line elsewhere finds no `DATA/` and makes its
`SAVES/` there.
* *The IIgs* looks for `DATA/` under GS/OS prefix 0 and puts `SAVES/` in the
application's folder (prefix 1). When the two differ, data and saves come
from different folders.
* *DVX* is the exception, because its programs share one current directory:
there `DATA/` is beside the program, in its folder under DVX's `APPS`, and
`SAVES/` is in the program's own settings folder,
`CONFIG\APPS\JOEYLIB\NAME`, which DVX keeps apart from the program files so
a reinstall never overwrites it. The calls below find both there by
themselves.
Both are addressed by a *bare name relative to their directory*. The prefix is
added for you, so never hard-code `"DATA/"` or `"SAVES/"` yourself.
[IMPORTANT]
====
Keep names portable across the retro filesystems: at most 8 characters plus a
short extension, letters and digits only, so they satisfy both ProDOS and DOS
8.3. Do not depend on case being preserved.
Keep names portable across the retro filesystems: at most 8 characters plus
an extension of up to 3, letters and digits only, starting with a letter, so
they satisfy both ProDOS (whose names must start with a letter) and DOS 8.3.
The save calls refuse any other name on every port, as if the file could not
be opened; names in `DATA/`, which ship with the program, are passed through
as they are. Do not depend on case being preserved, and use the same case
every time: on Linux, Haiku, BeOS, and the web, `HERO.SAV` and `hero.sav` are
different files.
====
A failed open or write is non-fatal and simply reports failure, so save code
can degrade gracefully on a platform or volume where `SAVES/` is unavailable.
On the IIgs at most 21 files can be open at once, and saves are created as
text files (type TXT).
=== jlDataOpen
@ -34,7 +54,10 @@ can degrade gracefully on a platform or volume where `SAVES/` is unavailable.
FILE *jlDataOpen(const char *name, const char *mode);
----
Opens a read-only data file. The `DATA/` prefix is prepended automatically.
Opens a data file. The `DATA/` prefix is prepended automatically. The mode is
passed to `fopen` as it is, so a writing mode works where the medium allows it,
but `DATA/` is meant to be read: on the web, for instance, what is written
there is lost when the page closes.
[cols="1,1,4", options="header"]
|===
@ -74,7 +97,9 @@ whole-file blob prefer `jlSaveWrite` and `jlSaveRead`.
bool jlSaveWrite(const char *name, const void *buf, uint32_t len);
----
Writes a whole save file, replacing any existing contents.
Writes a whole save file, replacing any existing contents. The write is not
atomic: if it fails part way (a full disk, say) the save is left short. A
`NULL` `buf` with a non-zero `len` returns `false` and leaves the save alone.
[cols="1,1,4", options="header"]
|===
@ -145,8 +170,11 @@ uint32_t jlDiskFree(void);
*Parameters:* none.
*Returns:* free space in bytes on the volume holding `SAVES/`. Returns 0 if it
cannot be determined -- some ports have no query for it -- and saturates at
`UINT32_MAX` on volumes larger than 4 GB.
cannot be determined, and saturates at `UINT32_MAX` on volumes larger than
4 GB. On DOS and Win16 the query (DOS's own) stops near 2 GB on a larger
volume, and on the web it is the browser's own estimate of what the site may
still store (its quota less what it uses), which the browser may round or
pad.
[source,c]
----

View file

@ -2,13 +2,13 @@
Declared in `<joey/input.h>`.
Call `jlInputPoll()` exactly once per frame to refresh keyboard, mouse and
Call `jlInputPoll()` exactly once per frame to refresh keyboard, mouse, and
joystick state. Everything else in this section reads the state that poll
captured.
Two parallel keyboard interfaces share that one poll. The *predicates* answer
"is the fire key held?", which is the right shape for games. The *character
queue* answers "which character did the user type?", with shift, caps lock and
queue* answers "which character did the user type?", with shift, caps lock, and
the machine's keyboard layout already applied, which is the right shape for
text entry. They coexist freely.
@ -20,32 +20,33 @@ transition happened.
[source,c]
----
typedef enum {
KEY_NONE = 0,
KEY_A .. KEY_Z,
KEY_0 .. KEY_9,
KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT,
KEY_LSHIFT, KEY_RSHIFT, KEY_LCTRL, KEY_LALT,
KEY_F1 .. KEY_F10,
KEY_COUNT
JL_KEY_NONE = 0,
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, JL_KEY_DOWN, JL_KEY_LEFT, JL_KEY_RIGHT,
JL_KEY_LSHIFT, JL_KEY_RSHIFT, JL_KEY_LCTRL, JL_KEY_LALT,
JL_KEY_F1 .. JL_KEY_F10,
JL_KEY_COUNT
} jlKeyE;
typedef enum {
MOUSE_BUTTON_NONE = 0, MOUSE_BUTTON_LEFT,
MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT
JL_MOUSE_BUTTON_NONE = 0, JL_MOUSE_BUTTON_LEFT,
JL_MOUSE_BUTTON_RIGHT, JL_MOUSE_BUTTON_MIDDLE, JL_MOUSE_BUTTON_COUNT
} jlMouseButtonE;
typedef enum { JOYSTICK_0 = 0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE;
typedef enum { JOY_BUTTON_0 = 0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE;
typedef enum { JL_JOYSTICK_0 = 0, JL_JOYSTICK_1, JL_JOYSTICK_COUNT } jlJoystickE;
typedef enum { JL_JOY_BUTTON_0 = 0, JL_JOY_BUTTON_1, JL_JOY_BUTTON_COUNT } jlJoyButtonE;
----
Constants: `JOYSTICK_AXIS_MAX` (127) and `JOYSTICK_AXIS_MIN` (-127);
Constants: `JL_JOYSTICK_AXIS_MAX` (127) and `JL_JOYSTICK_AXIS_MIN` (-127);
`JL_CHAR_QUEUE_SIZE` (32).
Control codes delivered through the character queue: `JL_CHAR_BACKSPACE`
(0x08), `JL_CHAR_TAB` (0x09), `JL_CHAR_RETURN` (0x0D), `JL_CHAR_ESCAPE` (0x1B)
(0x08), `JL_CHAR_TAB` (0x09), `JL_CHAR_RETURN` (0x0D), `JL_CHAR_ESCAPE` (0x1B),
and `JL_CHAR_DELETE` (0x7F). That last one is *forward* delete, the key marked
Del -- a text editor needs both it and backspace.
Del -- a text editor needs both it and backspace. The IIgs never delivers it:
its Delete key is the backspace key, and types `JL_CHAR_BACKSPACE`.
=== jlInputPoll
@ -55,7 +56,9 @@ void jlInputPoll(void);
----
Refreshes all input state and refills the typed-character queue. Call once per
frame, typically just before drawing.
frame, typically just before drawing. A key pressed and let go between two
polls still reads down for one poll, so `jlKeyPressed` sees a quick tap
(except on DVX, which reads its keyboard only as it is at the poll).
*Parameters:* none. *Returns:* nothing.
@ -72,11 +75,13 @@ not polling means nothing ever changes.
int jlInputGetChar(void);
----
Pops the next typed character. Shift, caps lock and the keyboard layout have
Pops the next typed character. Shift, caps lock, and the keyboard layout have
already been applied by the backend, so punctuation and shifted symbols arrive
correctly. The layout is the machine's own on IIgs, Amiga, ST and X68000,
DVX's own on DVX and the operating system's on SDL3; the DOS port translates
with a fixed US layout.
correctly. The layout is the machine's own on the IIgs, Amiga, ST, and
X68000. It is DVX's own on DVX, and the operating system's on SDL3, the web,
Win32, Win16, Haiku, and BeOS. DOS, and the PSP's on-screen keyboard, use a
fixed US layout. On the ST a key held with Alt types nothing, so the
characters a national TOS puts on Alt cannot be typed.
Arrow keys are not characters and stay on the predicate API.
@ -88,9 +93,16 @@ Arrow keys are not characters and stay on the predicate API.
[NOTE]
====
The queue holds `JL_CHAR_QUEUE_SIZE - 1` characters, so 31. When full, further
characters are dropped until you pop some. Auto-repeat may deliver repeats on
ports whose keyboard services repeat; repeat policy is yours to manage. 7-bit
ASCII only -- no Unicode, no input methods.
characters are dropped until you pop some. A chord types nothing on any port:
Ctrl+M is not Return and Ctrl+1 is not 1, and Alt chords type nothing where
Alt is a command key (DOS, DVX, Windows, the Be ports' Command key, the PSP's
on-screen keyboard); the Amiga's Alt and Windows' AltGr type their layout's
characters, and so does the ST's Alt where TOS keeps its keyboard's Alt
layer in its tables (TOS 4.00 and later, and EmuTOS: a German keyboard's
@ [ ] { } \), and otherwise nothing (<<portsDifferInput>>). A held key
repeats its character on every port (the ST at the rate TOS is set to);
repeat policy is yours to manage. 7-bit ASCII only -- no Unicode, no input
methods.
The return type is plain `int`, not a stdint type, deliberately: this is the
`getchar()`-style "byte or -1" contract.
@ -103,9 +115,12 @@ The return type is plain `int`, not a stdint type, deliberately: this is the
void jlWaitForAnyKey(void);
----
Blocks until the user presses any key, polling internally so per-port input
machinery -- including interrupt-driven audio samplers -- keeps running during
the wait.
Blocks until the user presses a key -- one JoeyLib names (with a `jlKeyE`),
or one that types a character, punctuation for instance -- polling internally
so per-port input machinery, including interrupt-driven audio samplers, keeps
running during the wait. The mouse and joystick buttons do not end it, and a
key already held when the wait starts must be let go and pressed again (its
auto-repeat does not count).
*Parameters:* none. *Returns:* nothing. The character queue is emptied on
return, so the dismissing keystroke is consumed rather than delivered as text.
@ -129,6 +144,16 @@ bool jlKeyReleased(jlKeyE key);
`true` only in the frame the key went down; `jlKeyReleased` only in the frame
it came up.
A named key that two physical keys give is down while either is held, and
comes up with the last of them: `JL_KEY_RETURN` is Return and the keypad's
Enter; `JL_KEY_UP`, `JL_KEY_DOWN`, `JL_KEY_LEFT`, and `JL_KEY_RIGHT` are the
arrow keys and the keypad's 8, 2, 4, and 6; and `JL_KEY_LCTRL` and
`JL_KEY_LALT` are the left and right Ctrl and Alt, where the keyboard has
both (on the X68000, OPT.1 and OPT.2 are Alt). The keypad's keys still type
what the machine's keypad types (`jlInputGetChar`). The IIgs knows keys by
their characters, so its keypad reads as the digits
(<<portsDifferInput>>).
=== jlMousePresent
[source,c]
@ -139,13 +164,10 @@ bool jlMousePresent(void);
*Parameters:* none.
*Returns:* `true` if the platform confirmed a mouse at init. On DOS that means
the INT 33h driver reset successfully; on Amiga, ST, IIgs, X68000, DVX and
SDL3 the OS mouse is always wired. On DVX the position is the pointer's over
the program's window, divided by the window's scale; it keeps its last value
while the pointer is elsewhere. On DVX's full screen it is the pointer's
place on the stage (DVX draws no pointer there). On SDL3 it is the pointer's
place on the stage however the window scales it, held to the stage's edge
while the pointer is beside it; the OS pointer is hidden over the window.
the INT 33h driver reset successfully; on the Amiga, that JoeyLib's input
window opened; on Win32 and Win16, that Windows reports a mouse; on SDL3 and
the web, that SDL reports one. The IIgs, ST, X68000, DVX, Haiku, and BeOS
always report one, and the PSP never does.
=== jlMouseX / jlMouseY
@ -161,6 +183,14 @@ int16_t jlMouseY(void);
Until the platform reports the pointer, it is the middle of the stage,
(160, 100), on every port.
On DVX the position is the pointer's over the program's window, divided by
the window's scale; it keeps its last value while the pointer is elsewhere. On
DVX's full screen it is the pointer's place on the stage (DVX draws no pointer
there). On the hosted ports it is the pointer's place on the stage however the
window scales it, held to the stage's edge while the pointer is beside it; the
OS pointer is hidden over the window. On DOS the mouse driver keeps it in
range.
=== jlMouseDown / jlMousePressed / jlMouseReleased
[source,c]
@ -178,6 +208,10 @@ bool jlMouseReleased(jlMouseButtonE button);
*Returns:* held state, rising edge, and falling edge respectively.
The IIgs mouse has one button, `JL_MOUSE_BUTTON_LEFT`. The ST and X68000 have
left and right. The other ports report left, right, and middle, as far as the
mouse has them. The PSP has no mouse.
=== jlJoystickConnected
[source,c]
@ -188,13 +222,20 @@ bool jlJoystickConnected(jlJoystickE js);
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`js` |`jlJoystickE` |Which stick, `JOYSTICK_0` or `JOYSTICK_1`.
|`js` |`jlJoystickE` |Which stick, `JL_JOYSTICK_0` or `JL_JOYSTICK_1`.
|===
*Returns:* `true` if the platform reports a stick on that port. On the ST and
X68000 both ports always report one: an empty port cannot be told from a stick
at rest. The Amiga (`lowlevel.library`) has the same blind spot the other
way: it reports a stick only once it has moved, and never a mouse.
way: it reports a stick only once it has moved, and never a mouse. The IIgs
has one stick, `JL_JOYSTICK_0`, and stops looking for it after about a second
with none seen, until `jlJoystickReset`. On DOS a stick is connected while the
game port times both its axes. SDL3, the web, and the PSP see SDL's gamepads
and joysticks both, a joystick SDL has no gamepad mapping for read by its first
two axes, its first hat, and its first two buttons; Win32, Win16, Haiku, and
BeOS see the system's joysticks, and Haiku and BeOS notice a new one within a
second.
=== jlJoystickX / jlJoystickY
@ -210,8 +251,11 @@ int8_t jlJoystickY(jlJoystickE js);
|`js` |`jlJoystickE` |Which stick.
|===
*Returns:* axis value, -127 to +127. Digital sticks snap to the extremes;
analog sticks return the raw centred value. Test against a threshold rather
*Returns:* axis value, -127 to +127. Digital sticks (the Amiga, ST, and
X68000) snap to the extremes; analog sticks (the IIgs, DOS, DVX, and the
hosted ports) return their offset from the centre, scaled to that range, with
the dead zone applied. On the IIgs and DOS the axes read 0 until the stick's
centre has been read (<<_jljoystickreset>>). Test against a threshold rather
than for exact values, so the same code works with both.
=== jlJoyDown / jlJoyPressed / jlJoyReleased
@ -232,6 +276,15 @@ bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button);
*Returns:* held state, rising edge, and falling edge respectively.
Every stick has two buttons. On the ST and STe's own joystick ports, and on
the Amiga until a stick's second button is first pressed, a stick is taken to
have one: a quick press of it is `JL_JOY_BUTTON_0`, down for the one poll after
it is let go, and a press held for half a second is `JL_JOY_BUTTON_1`, down
from then until it is let go. So act on button 0 with `jlJoyPressed`, and do
not count on it staying down while the button is held. Elsewhere the two
buttons are the stick's own, and a one-button stick never reports
`JL_JOY_BUTTON_1`.
=== jlJoystickReset
[source,c]
@ -239,34 +292,35 @@ bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button);
void jlJoystickReset(jlJoystickE js, uint8_t deadZone);
----
Re-enables joystick polling and recalibrates the resting position.
Sets the stick's dead zone and, where the port can, re-centres it and looks
for it again:
This matters on the IIgs. That port *auto-disables* joystick polling after a
short window with no stick detected, because polling costs about 3 ms a frame
there -- and it does not re-probe on its own. If a user plugs a stick in while
your program runs, you must call this to resume polling.
* *IIgs*: polling starts again if it had stopped for want of a stick, and the
stick is re-centred at its next reading.
* *DOS*: the game port is probed again, and the stick is re-centred at its
next reading.
* *Amiga*: the port goes back to `lowlevel.library`'s own detection.
* *Elsewhere* only the dead zone changes.
On the IIgs polling for a stick that is not there stops. If a user may plug a
stick in while your program runs, call this, with the stick centred, to pick
it up.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`js` |`jlJoystickE` |Which stick.
|`deadZone` |`uint8_t` |Raw readings within this many units of centre clamp
to 0. Pass 0 to disable the dead zone.
|`deadZone` |`uint8_t` |Axis values within this many units of 0 (on the
-127..127 scale) read 0, on the ports with analog sticks: the IIgs, DOS, DVX,
and the hosted ports. Each stick starts with 8, so a stick left alone reads 0
though its pots jitter. Pass 0 to disable the dead zone.
|===
*Returns:* nothing.
[IMPORTANT]
====
The next poll after this call captures the stick's *current* raw position as
the new centre, so the user must be holding the stick centred when you call it.
On Amiga, ST and DOS the sticks are genuinely digital and already report -1, 0
or +1, so recalibration is a no-op and `deadZone` is ignored. DVX's sticks are
analog but calibrated, dead zone included, in DVX's own Control Panel, so the
call is a no-op there too. SDL3's sticks are gamepads the OS calibrates; there
the call only sets the dead zone, inside which the stick reads as centred.
The call still
clears any auto-disconnect state, so it is safe and correct to make it
unconditionally.
On the IIgs and DOS the next reading after this call is taken as the stick's
centre, so the user must be holding the stick centred when you call it. The
call is safe to make unconditionally.
====

View file

@ -10,11 +10,18 @@ which palette each scanline uses. That is how a JoeyLib program shows more than
[IMPORTANT]
====
Colour 0 of every palette is black, always. `jlPaletteSet` silently masks entry
0 to `$000` whatever you pass. This is deliberate: the Amiga and ST hardware
border follows colour 0 of the active palette, and colour 0 is the transparent
colour for sprite drawing. You have 15 usable colours per palette.
0 to `$000` whatever you pass, and `jlSurfaceLoadFile` does the same to every
palette it loads. This is deliberate: the Amiga and ST hardware border follows
colour 0 of the active palette, and colour 0 is the transparent colour for
sprite drawing. You have 15 usable colours per palette.
====
Palette and SCB changes to the stage reach the display at the next
`jlStagePresent`, not before. Out-of-range arguments are ignored
the same way on every port: a palette index above 15 or a line above 199 makes
the setters do nothing (`jlScbSetRange` stops its range at line 199), and
`jlScbGet` returns 0 for a line off the surface.
=== jlPaletteSet
[source,c]
@ -42,7 +49,9 @@ Loads all 16 colours of one palette.
void jlPaletteGet(const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
----
Reads all 16 colours of one palette.
Reads all 16 colours of one palette, as they were set: the values passed to
`jlPaletteSet` (with entry 0 forced to `$000`, and bits 12 to 15 kept as
passed), not what the display can show of them.
[cols="1,1,4", options="header"]
|===
@ -100,7 +109,7 @@ screen, whether that is one palette everywhere or several horizontal bands.
----
// Sky on the top third, ground below, using two palettes.
jlScbSetRange(stage, 0, 65, 0);
jlScbSetRange(stage, 66, SURFACE_HEIGHT - 1, 1);
jlScbSetRange(stage, 66, JL_SURFACE_HEIGHT - 1, 1);
----
=== jlScbGet

View file

@ -23,16 +23,15 @@ the ST goes through the BIOS Aux channel; the X68000 transmits through IOCS
and receives on its own SCC interrupt; DVX goes through DVX's serial library,
which owns the COM ports for every program on the desktop (a port another
program has open cannot be opened); Win32 and Win16 go through Windows' own
communications calls, whose driver owns the ports the same way. SDL3, the web
and the PSP have no serial port: `jlSerialOpen` fails there.
communications calls, whose driver owns the ports the same way. SDL3, the web,
the PSP, Haiku, and BeOS have no serial port: `jlSerialOpen` fails there.
The model is *non-blocking and buffered*. Received bytes accumulate in a
background ring -- a hardware interrupt on DOS, the X68000 and the IIgs, the
OS on Amiga, ST, DVX and Windows -- and reads never block. Call `jlSerialPoll` once per
frame: it is next to a no-op where the interrupt or OS already fills the ring,
and the hardware drain where nothing does, so calling it every frame is always
correct. On DVX it also gives the other programs their turn
(<<_dvx_programs>>).
background ring -- a hardware interrupt on DOS, the X68000, and the IIgs, the
OS on Amiga, ST, DVX, and Windows -- and reads never block. Call `jlSerialPoll`
once per frame. (The X68000 takes its interrupt only when the port is opened in
supervisor mode, which `jlInit` puts every program in; opened without it, the
port is drained from RSDRV.SYS's own 64-byte buffer by the serial calls.)
On the IIgs a Super Serial Card only interrupts with its interrupt switch
(SW2-6) on; with it off the card still works, drained by `jlSerialPoll`, but
@ -71,6 +70,10 @@ at 9600 baud, so:
At 19200 baud each lasts half as long, and so on.
A byte that arrives with a framing, parity, or overrun error is dropped on the
X68000 and delivered as it came on the IIgs and DOS; on the other ports the
system's driver decides.
JoeyLib's own work is safe while a port is open, with one exception on the
X68000 (below). On the IIgs, where the
display and sound code masks interrupts for long stretches, opening a port
@ -89,7 +92,8 @@ These can still lose bytes, and are the program's to schedule around:
* *Disk access.* On the IIgs, GS/OS holds interrupts off while it reads or
writes a 3.5-inch floppy: 65 to 220 ms at a time, measured. That is every
file call -- `jlFile*`, `jlSave*`, loading assets and sprite banks, and
`jlAudioInit`, which loads the replayer from disk. The other ports' disk
`jlInit` unless `jlConfigT.noAudio` is set, since starting audio loads the
replayer from disk. The other ports' disk
access goes through the operating system and has not been measured; treat
it as a risk there too.
* *Starting a song on the IIgs.* `jlAudioPlayMod` loads the song's
@ -100,13 +104,13 @@ These can still lose bytes, and are the program's to schedule around:
* *A Super Serial Card above 2400 baud.* The IIgs's own interrupt handling can
outlast the 6551's single byte, so the card runs loss-free at 2400 and above
that relies on the protocol to recover. The built-in modem and printer ports
stay loss-free at 9600 through full-screen presents, clears, joystick reads
stay loss-free at 9600 through full-screen presents, clears, joystick reads,
and sound effects.
* *Per-scanline palettes on the X68000.* The port reloads its one hardware
palette as the beam scans, and a run of palette changes on consecutive lines
holds serial receive off for the whole run, about 32 microseconds a line.
Receive stays loss-free while such runs are under about 90 lines at 9600
baud, 40 at 19200 and 15 at 38400; palette changes three or more lines apart
baud, 40 at 19200, and 15 at 38400; palette changes three or more lines apart
release it between bands.
* *The program's own code*, wherever it masks interrupts.
@ -121,6 +125,48 @@ Ways to live with them:
* For data that must arrive intact, use a protocol with checksums and
retransmission; XMODEM and its descendants exist for exactly this.
=== Flow Control
`flow` in `jlSerialConfigT` picks the handshake, and every port with a serial
port honours it:
* `JL_SERIAL_FLOW_RTSCTS`: transmit waits while the peer holds CTS down, and
the port drops RTS when its receive buffer is three quarters full, raising
it again once reads have it down to a quarter.
* `JL_SERIAL_FLOW_XONXOFF`: the peer's XOFF and XON (0x13 and 0x11) pause and
resume transmit and never reach `jlSerialRead`, and the port sends XOFF
when its receive buffer is three quarters full and XON at a quarter.
The receive buffer, which also decides how long a program may go without
reading when there is no flow control:
[cols="3,2,2", options="header"]
|===
|Port |Receive buffer |Lasts at 9600 baud
|IIgs |512 bytes |~0.5 s
|DOS, DVX |2 KB |~2 s
|Amiga (`serial.device`) |4 KB |~4 s
|Atari ST (TOS's, enlarged while the port is open) |4 KB |~4 s
|X68000 |4 KB |~4 s
|Win32, Win16 (Windows' driver) |4 KB |~4 s
|===
Where they differ:
* The IIgs modem and printer ports' connector (mini-DIN-8) has no RTS line:
its one handshake output is DTR. RTS/CTS there makes transmit wait for CTS,
but nothing can tell the peer to pause, so receive needs XON/XOFF or a
protocol that paces itself. A Super Serial Card has RTS, and its 6551 also
stops its own transmitter while RTS is down.
* On the ST, the Amiga, Win32, and Win16 the operating system's driver does
the handshaking, and under RTS/CTS it may change RTS after
`jlSerialSetRts` did. DVX's library holds the peer off at four fifths full
and lets it go at a fifth.
* The X68000 drops RTS from its receive interrupt, but sends XOFF and XON
from `jlSerialPoll`, `jlSerialRead`, and `jlSerialWrite`: under XON/XOFF, a
program that calls none of them for longer than the last quarter of the
buffer lasts (about a second at 9600 baud) can still lose bytes.
=== Types
[source,c]
@ -143,7 +189,7 @@ typedef enum {
} jlSerialFlowE;
typedef struct {
uint32_t baud; // 300 .. 115200, clamped per port
uint32_t baud; // 300 .. 115200; each port runs its nearest
uint8_t dataBits; // 5 .. 8
uint8_t stopBits; // 1 or 2
jlSerialParityE parity;
@ -152,10 +198,54 @@ typedef struct {
} jlSerialConfigT;
----
A field left 0 takes a default: `baud` 9600, `dataBits` 8, and `stopBits` 1.
`jlSerialOpen` fails for `dataBits` outside 5 to 8, `stopBits` other than 1 or
2, or a `parity` or `flow` the enumerations do not name.
`baud` is the rate asked for. Each port runs at the nearest rate it has:
[cols="2,5", options="header"]
|===
|Port |Rates
|IIgs modem and printer ports
|115200 divided by a whole number, up to 57600; a rate between two of those
runs at the one above it, and anything faster at 57600.
|IIgs Super Serial Card
|The 6551's fifteen rates, 50 to 19200; any other runs at the fastest one
below it.
|DOS
|115200 divided by a whole number, up to 115200; a rate between two of those
runs at the one above it.
|DVX
|DVX's library's rates, 50 to 115200; any other runs at the fastest one below
it.
|Amiga
|The rate as asked, handed to `serial.device`.
|Atari ST
|The MFP's sixteen rates, 50 to 19200; any other runs at the fastest one below
it.
|X68000
|75 to 38400, each double the last (38400 is really 39062); any other runs at
the fastest one below it.
|Win32
|The rate as asked, handed to Windows' driver.
|Win16
|The rate as asked up to 57600; above that, see below.
|===
The IIgs is the machine with three device choices; elsewhere `JL_SERIAL_MODEM`
and `JL_SERIAL_PRINTER` both mean the default port. `JL_SERIAL_DEFAULT` selects
the IIgs modem port, COM1 on DOS, DVX, Win32 and Win16, the ST Modem port,
Amiga unit 0 or the X68000's RS-232C port.
the IIgs modem port; COM1 on DOS, DVX, Win32, and Win16; the ST Modem port;
Amiga unit 0; or the X68000's RS-232C port.
On Win16 a `baud` above 57600 runs at 115200 through KPCOMM.DRV, the
communications driver JoeyLib's Windows runs with in place of Windows 3.1's
@ -167,14 +257,13 @@ RSDRV.SYS. Every Human68k system disk has it and the stock CONFIG.SYS loads
it (`DEVICE = \SYS\RSDRV.SYS`), as do JoeyLib's own X68000 boot disks; a
game shipped to boot on its own must keep it. Without it `jlSerialOpen`
returns `false` and `jlLastError` says the driver is missing. With it the
port runs at 75 to 38400 bps, and `JL_SERIAL_FLOW_RTSCTS` makes transmit wait
for CTS; RTS stays up, since JoeyLib's receive interrupt, not the driver's,
fills the receive ring. 38400 is really 39062 bps, 1.7% fast, within the
usual tolerance of a UART at the far end.
port runs at 75 to 38400 bps. 38400 is really 39062 bps, 1.7% fast, within
the usual tolerance of a UART at the far end.
`unit` disambiguates `JL_SERIAL_SLOT` and is ignored otherwise: an IIgs Super
Serial Card slot 1..7, a DOS or DVX COM number 1..4, a Win16 COM number
1..9, a Win32 COM number 1..255, or an Amiga `serial.device` unit.
1..9, a Win32 COM number 1..255, or an Amiga `serial.device` unit. The ST and
X68000 have one port, and open it for every device, `JL_SERIAL_SLOT` included.
=== jlSerialOpen
@ -194,9 +283,10 @@ open without an intervening close fails.
|===
*Returns:* `true` on success. `false` if the port cannot be opened or the
platform has no serial support -- and then every other call no-ops. On the
X68000 it is also `false` when RSDRV.SYS is not loaded, and `jlLastError`
says so.
platform has no serial support -- and then every other call no-ops. It is
also `false`, with `jlLastError` saying why, when no UART answers at the DOS
COM port, when no Super Serial Card answers in the IIgs slot, and when
RSDRV.SYS is not loaded on the X68000.
=== jlSerialClose
@ -207,9 +297,12 @@ void jlSerialClose(void);
Closes the port and releases its resources. Safe when not open. A port still
open when the program exits is closed then: the first successful
`jlSerialOpen` registers an `atexit` handler, because on DOS, the X68000 and
`jlSerialOpen` registers an `atexit` handler, because on DOS, the X68000, and
the IIgs the port's receive interrupt vector points into the program until it
is closed.
is closed. Bytes `jlSerialWrite` accepted are still sent, so a program can
write a farewell and close straight away. DOS, the ST, and the X68000 put back
the speed and framing the port had before the open; the IIgs (whose serial
chip's settings cannot be read back) and DVX leave it as JoeyLib set it.
Closing does *not* hang up: `jlSerialClose` leaves DTR exactly as it is, so
a modem call or a BBS caller survives the program handing the line back: a
@ -291,7 +384,11 @@ uint16_t jlSerialRead(uint8_t *buf, uint16_t max);
uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len);
----
Sends bytes. Transmit is a bounded poll.
Sends bytes without waiting long. Most ports feed the bytes to the line as it
takes them and give up after a bounded wait when it stalls (a peer holding
transmit off, say); the Amiga queues them in two 1 KB buffers it sends from in
the background, and Win16 in Windows' driver's queue, so neither waits at
all.
[cols="1,1,4", options="header"]
|===
@ -386,7 +483,11 @@ Asserts or drops RTS, for a caller doing its own receive-side flow control.
`JL_SERIAL_FLOW_RTSCTS` asks the *port* to handshake; this is the other half --
the application saying "my buffer is nearly full, stop sending", and later "go
ahead". That is what a client with a small ring and a peer that can be told to
pause actually needs.
pause actually needs. Under `JL_SERIAL_FLOW_RTSCTS` on DOS, the X68000, and a
IIgs Super Serial Card, RTS is up only while both the port's handshake and
this call want it; elsewhere the driver may change it again (see Flow
Control). The IIgs modem and printer ports have no RTS line on their
connector, so nothing reaches the peer there.
[cols="1,1,4", options="header"]
|===

View file

@ -7,14 +7,16 @@ colour-0 transparency. Sprite pixel data is `widthTiles * heightTiles * 32`
bytes of 4bpp packed data, laid out as a flat blob of tiles in row-major order.
The distinguishing feature is that *sprite blitters are compiled at run time*.
The library emits native 65816, 68000 or x86 code for each sprite, including
pre-shifted variants so smooth horizontal motion needs no runtime bit shifting.
On the IIgs, Amiga, ST, X68000, DOS, DVX, and Win16 the library emits native
65816, 68000, or x86 code for each sprite, including pre-shifted variants so
smooth horizontal motion needs no runtime bit shifting. SDL3, the web, Win32,
the PSP, Haiku, and BeOS have no code emitter and always use the interpreter.
[IMPORTANT]
====
`jlSpriteDraw` does *not* compile on demand. An uncompiled sprite draws through
an interpreter roughly *forty times slower*, and nothing reports this. Always
call `jlSpriteCompile` or `jlSpritePrewarm` during loading.
an interpreter *two to six times slower*, depending on the machine, and
nothing reports this. Always call `jlSpriteCompile` during loading.
====
=== jlSpriteT and jlSpriteBackupT
@ -35,33 +37,42 @@ typedef struct {
----
You supply `bytes` and its capacity; the library fills in the rest to describe
what it actually captured. On planar ports the captured region is the
*16-pixel-aligned window* containing the sprite, not the sprite rectangle, and
the `x`, `width`, `height` and `sizeBytes` fields report that true geometry. It
is the recorded window you must not overwrite before restoring.
what it actually captured, in `x`, `y`, `width`, `height`, and `sizeBytes`.
That is often not the sprite rectangle:
=== JOEY_SPRITE_BACKUP_BYTES
* `x` is rounded down to an even pixel, and `width` grows to match, on every
port.
* A compiled save on the Amiga, ST, and X68000 captures the *16-pixel-aligned
region* containing the sprite.
* A compiled save on the IIgs captures only the rows that hold an opaque
pixel, so `y` can be lower and `height` shorter than the sprite's.
* A save of a sprite partly off the surface captures only the visible part.
It is the recorded region you must not overwrite before restoring.
=== jlSpriteBackupLength
[source,c]
----
#define JOEY_SPRITE_BACKUP_BYTES(_wT, _hT) /* ... */
#define jlSpriteBackupLength(_type, _wT, _hT) /* ... */
----
Worst-case backup capacity for a `widthTiles` by `heightTiles` sprite at any x
position, on every port. Use it to size your backing array.
How many `_type` elements a backup buffer needs for a `widthTiles` by
`heightTiles` sprite at any x position, on every port, rounded up. Use it to
size your backing array; with `uint8_t` it is the size in bytes.
[source,c]
----
// 2-byte aligned by construction -- see the warning below.
static uint16_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4) / 2];
static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)];
----
[WARNING]
====
Give the backing array *at least 2-byte alignment*. An odd buffer address
routes save and restore onto slow interpreted paths on the planar ports,
because the 68000 cannot do word accesses at odd addresses. Declaring it
`uint16_t`, as above, guarantees this.
Give the backing array *at least 2-byte alignment*. On the Amiga, ST, and
X68000 an odd buffer address sends the save and restore calls down a much
slower path, and on the Amiga and ST it makes the trusted variants crash; the
other ports do not mind. Declaring it `uint16_t`, as above, guarantees the
alignment.
====
=== jlSpriteBankLoad
@ -109,8 +120,10 @@ replacement for `jlSpriteBankLoad` plus a compile pass.
*Parameters:* as `jlSpriteBankLoad`.
*Returns:* the number of cels written, or 0 if the file is missing or its
magic, target or shift count does not match this build.
*Returns:* the number of cels written, or 0 if the file is missing, its
magic, target, or shift count does not match this build, its cel size is
out of range, or the 64 KB the loader reads each cel's code into cannot be
allocated (on the ports that interpret every sprite too).
=== jlSpriteBankSavePrecompiled
@ -129,14 +142,16 @@ cache the large per-platform compiled form on first run.
|===
|Parameter |Type |Description
|`path` |`const char *` |Destination path.
|`cels` |`jlSpriteT **` |The sprites to cache. Uncompiled ones are cached
as-is and will compile again next run.
|`cels` |`jlSpriteT **` |The sprites to cache, all of one size. Uncompiled
ones are cached uncompiled, and nothing compiles them when the cache is
loaded: call `jlSpriteCompile` on any cel whose `jlSpriteCompiledSize` is 0.
|`count` |`uint16_t` |How many sprites.
|`palette` |`const uint16_t *` |Optional, may be `NULL`. Embedded if given.
|===
*Returns:* `true` on success; `false` if the destination is not writable, which
is harmless -- the program simply keeps compiling at startup.
*Returns:* `true` on success; `false` if the cels are not all one size or the
destination is not writable, which is harmless -- the program simply keeps
compiling at startup.
=== jlSpriteCreate
@ -157,8 +172,8 @@ Wraps a tile-data blob in a sprite.
|`heightTiles` |`uint8_t` |Height in tiles. Must be non-zero.
|===
*Returns:* the sprite, or `NULL` if either dimension is 0 or the codegen arena
cannot fit a placeholder entry.
*Returns:* the sprite, or `NULL` if `tileData` is `NULL`, either dimension is
0, or memory runs out. Creating a sprite does not touch the codegen arena.
=== jlSpriteDestroy
@ -185,8 +200,11 @@ built from is *not* freed -- that buffer is yours.
bool jlSpriteCompile(jlSpriteT *sp);
----
Compiles the sprite's draw routines into the codegen arena. Idempotent --
compiling an already-compiled sprite is a no-op that returns `true`.
Compiles the sprite's draw, save, and restore routines into the codegen arena,
right away. No draw ever compiles on demand, so call it while loading, not in
the frame loop (a bank loaded with `jlSpriteBankLoadPrecompiled` arrives
compiled already). Idempotent -- compiling an already-compiled sprite is a
no-op that returns `true`.
[cols="1,1,4", options="header"]
|===
@ -194,8 +212,20 @@ compiling an already-compiled sprite is a no-op that returns `true`.
|`sp` |`jlSpriteT *` |Sprite to compile.
|===
*Returns:* `true` if the sprite is compiled. `false` if the arena is full (you
may call `jlSpriteCompact` and retry) or the sprite has no source tile data.
*Returns:* `true` if the sprite is compiled. `false` if it will be drawn by the
interpreter instead: the arena is full even after compacting it (`jlLastError`
says so; raise `codegenBytes` or destroy sprites you no longer need), the
sprite has no source tile data, the 64 KB the compiler works in cannot be
allocated, no routine fits the compiler's limits (16 KB a routine), or the
port has no code emitter (the Sprites row of
<<_per_platform_behaviour_summary>>). Only the full arena sets `jlLastError`.
A `false` is never fatal: the sprite still draws, only slower.
`true` means at least part of the sprite compiled, not all of it. When the
arena is short, the Amiga, ST, and X68000 keep only the draws for x on an
8-pixel boundary, and a routine larger than 16 KB is left out on every port.
A draw the compiled routines do not cover, and every draw of a sprite partly
off the surface, goes through the interpreter.
=== jlSpriteCompiledSize
@ -216,24 +246,6 @@ the current platform, rather than guessing.
*Returns:* bytes occupied in the codegen arena, summed over all compiled shift
variants. Returns 0 if the sprite was never compiled.
=== jlSpritePrewarm
[source,c]
----
void jlSpritePrewarm(jlSpriteT *sp);
----
Hints that a sprite will be drawn soon. A wrapper around `jlSpriteCompile` that
ignores the return value, for callers that do not need to handle failure.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`sp` |`jlSpriteT *` |Sprite to compile.
|===
*Returns:* nothing.
=== jlSpriteDraw
[source,c]
@ -242,7 +254,8 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
----
Draws the sprite at a pixel position. Source pixels of colour 0 are skipped.
Off-surface portions are clipped.
Off-surface portions are clipped. Compiled routines do not clip, so a sprite
partly off the surface draws through the interpreter, compiled or not.
[cols="1,1,4", options="header"]
|===
@ -272,12 +285,15 @@ would write, so it can be restored later.
|`sp` |`jlSpriteT *` |Sprite whose footprint to capture.
|`x`, `y` |`int16_t` |Position the sprite will be drawn at.
|`backup` |`jlSpriteBackupT *` |Receives the capture. You must set `bytes` to
a buffer of at least `JOEY_SPRITE_BACKUP_BYTES(w, h)` capacity for fully
in-bounds draws. For clipped draws only the visible bytes are stored.
a buffer of at least `jlSpriteBackupLength(uint8_t, w, h)` bytes for fully
in-bounds draws. For clipped draws only the visible bytes are stored. On the
IIgs the buffer must not cross a 64 KB bank boundary; `jlAlloc`, the C heap,
and static arrays never give one that does. With `bytes` `NULL` nothing is
copied: the call only fills in the region a save would record.
|===
*Returns:* nothing. The captured geometry is reported in the backup's `x`,
`width`, `height` and `sizeBytes` fields.
`y`, `width`, `height`, and `sizeBytes` fields (see <<_jlspritet_and_jlspritebackupt>>).
=== jlSpriteRestoreUnder
@ -293,7 +309,7 @@ Repaints the destination from a previous capture.
|Parameter |Type |Description
|`s` |`jlSurfaceT *` |Destination surface.
|`backup` |`const jlSpriteBackupT *` |A backup from `jlSpriteSaveUnder`, not
invalidated by other writes overlapping its recorded window.
invalidated by other writes overlapping its recorded region.
|===
*Returns:* nothing.
@ -339,12 +355,15 @@ corrupting -- but the guarantees below are still yours to keep.
Behaviour is *undefined* -- out-of-bounds writes, crashes -- unless all of the
following hold:
* `s`, `backup`, `backup->sprite` and `backup->bytes` are non-`NULL`;
* `s`, `backup`, `+backup->sprite+`, and `+backup->bytes+` are non-`NULL`;
* the sprite is fully on-surface at `(x, y)`, because *no clipping happens*;
* for the restore, the backup came from a save that took the compiled path:
16-pixel-aligned x and width on planar ports, even on chunky, height equal
to the sprite's pixel height, and a buffer at least 2-byte aligned;
* the recorded window is fully on the destination surface.
16-pixel-aligned x and width on the Amiga, ST, and X68000, even on the
other ports, and a height equal to the sprite's pixel height (on the IIgs,
to the rows the compiled save keeps, those holding an opaque pixel);
* on the Amiga and ST, `+backup->bytes+` is at least 2-byte aligned, for the
trusted save and save-and-draw as well as the restore;
* the recorded region is fully on the destination surface.
Use the plain calls anywhere those invariants are not statically guaranteed.
====
@ -371,7 +390,9 @@ copied into a sprite-owned buffer, so the source may be modified afterwards.
|`widthTiles`, `heightTiles` |`uint8_t` |Size in *tiles*, each 8x8 pixels.
|===
*Returns:* the new sprite, or `NULL` if the coordinates are not tile-aligned.
*Returns:* the new sprite, or `NULL` if `src` is `NULL`, a dimension is 0,
`x` or `y` is not a multiple of 8, the region is not wholly on the surface, or
memory runs out.
=== jlSpriteCompact
@ -403,4 +424,9 @@ Arena introspection, for judging whether to compact or to raise
*Parameters:* none.
*Returns:* bytes in use, and total arena size. Free space is the difference,
though it may be fragmented across holes until `jlSpriteCompact` runs.
though it may be fragmented across holes until `jlSpriteCompact` runs. On the
IIgs the total is what `jlInit` was granted, which can be less than
`codegenBytes` asked for, and it is split into blocks of at most 64 KB that
compacting never merges: one sprite's code must fit in one block. On the
ports that interpret every sprite the total is the arena they allocated
anyway, and the bytes in use stay 0.

View file

@ -3,9 +3,10 @@
Declared in `<joey/surface.h>` and `<joey/present.h>`.
A surface is a 320x200 16-colour image with its own 200-entry SCB table and its
own 16x16 palette set. Storage is target-native -- chunky 4bpp on IIgs and DOS,
planar on Amiga, ST and X68000 -- but every entry point speaks in colour
indices, so your code never sees the difference.
own 16x16 palette set. Storage is target-native -- chunky 4bpp on the IIgs,
DOS, DVX, and the hosted ports, planar on the Amiga, ST, and X68000 -- but every
entry point speaks in colour indices, so your code never sees the difference.
A new surface starts with the 16-colour EGA palette in all 16 of its palettes.
=== Constants
@ -13,13 +14,13 @@ indices, so your code never sees the difference.
|===
|Name |Value |Meaning
|`SURFACE_WIDTH` |320 |Surface width in pixels.
|`SURFACE_HEIGHT` |200 |Surface height in pixels.
|`SURFACE_BYTES_PER_ROW` |160 |Bytes per row in chunky 4bpp form.
|`SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes.
|`SURFACE_PALETTE_COUNT` |16 |Palettes per surface.
|`SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette.
|`SURFACE_PALETTE_ENTRIES` |256 |Total palette entries per surface.
|`JL_SURFACE_WIDTH` |320 |Surface width in pixels.
|`JL_SURFACE_HEIGHT` |200 |Surface height in pixels.
|`JL_SURFACE_BYTES_PER_ROW` |160 |Bytes per row in chunky 4bpp form.
|`JL_SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes.
|`JL_SURFACE_PALETTE_COUNT` |16 |Palettes per surface.
|`JL_SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette.
|`JL_SURFACE_PALETTE_ENTRIES` |256 |Total palette entries per surface.
|===
=== jlSurfaceT
@ -29,7 +30,7 @@ indices, so your code never sees the difference.
typedef struct jlSurfaceT jlSurfaceT;
----
Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet` or a loader.
Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet`, or a loader.
=== jlSurfaceCreate
@ -38,7 +39,9 @@ Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet` or a loader.
jlSurfaceT *jlSurfaceCreate(void);
----
Allocates a new offscreen surface.
Allocates a new offscreen surface. Each one costs about 32 KB. On the Amiga
that comes from chip RAM, because the blitter works on the planes there, so on
a 512 KB machine, beside the display's own pages, only a few fit.
*Parameters:* none.
@ -83,7 +86,7 @@ the displayed framebuffer until the next present.
void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src);
----
Copies pixels, SCBs and palettes from `src` into `dst`.
Copies pixels, SCBs, and palettes from `src` into `dst`.
[cols="1,1,4", options="header"]
|===
@ -128,10 +131,10 @@ multiples of 16. Two consequences you own:
* up to 15 extra pixels each side are copied, so a snapped erase can clobber
on-screen content outside the rectangle you asked for;
* it silently invalidates any live `jlSpriteBackupT` whose recorded window it
* it silently invalidates any live `jlSpriteBackupT` whose recorded region it
touches.
Erase *all* windows, then draw *all* sprites. Never mix this with per-sprite
Erase *all* regions, then draw *all* sprites. Never mix this with per-sprite
save and restore in the same region.
====
@ -180,7 +183,10 @@ nothing is reallocated.
|===
*Returns:* `true` on success; `false` if the file is missing, the wrong size,
or unreadable.
has an SCB byte above 15, or unreadable. A read that fails part way can leave
the pixels partly overwritten; the SCBs and palettes are then untouched. Colour
0 of every palette loaded is set to black, as `jlPaletteSet` does. Neither file
call sets `jlLastError`.
=== jlSurfaceHash
@ -213,8 +219,10 @@ void jlStagePresent(void);
----
Flips the dirty regions of the stage to the display and clears the dirty state.
On chunky platforms this is a direct copy; on planar platforms it is a
chunky-to-planar conversion.
Where the stage already has the display's layout this is a copy (the IIgs's
4bpp pixels; the planes of the Amiga, ST, and X68000); DOS, DVX, and the hosted
ports expand each 4-bit pixel to their display's 8-bit or RGB pixels. Palette
and SCB changes reach the display here too.
Drawing primitives mark per-row dirty ranges as a side effect, so you only need
to call this at the end of a frame and everything you drew appears. Rows
@ -224,6 +232,6 @@ nothing touched since the last present are skipped.
[NOTE]
====
If you modify pixels by a route other than the library's primitives, the dirty
tracking does not know, and those rows will not be presented.
Every call that changes the stage's pixels marks the rows it touched, and the
API has no other way to write them, so there is nothing to mark by hand.
====

View file

@ -10,7 +10,7 @@ surfaces or fills them.
The consequence is that everything else works on tiles too. You can paint tile
content with `jlDrawPixel` and `jlFillRect`, generate it procedurally, or edit
it at run time, because a "tileset" is just a surface you agree to treat as
one. Fonts, terrain sets and spritesheets are all the same thing.
one. Fonts, terrain sets, and spritesheets are all the same thing.
=== Constants and Types
@ -18,19 +18,19 @@ one. Fonts, terrain sets and spritesheets are all the same thing.
|===
|Name |Value |Meaning
|`TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels.
|`TILE_BYTES_PER_ROW` |4 |Bytes per tile row (8 pixels at 4bpp).
|`TILE_BYTES` |32 |Bytes in one tile.
|`TILE_BLOCKS_PER_ROW` |40 |Tile columns on a surface.
|`TILE_BLOCKS_PER_COL` |25 |Tile rows on a surface.
|`TILE_NO_GLYPH` |0xFFFF |`jlDrawText` map entry meaning "skip this character".
|`TILE_MAP_SKIP` |0xFF |`jlTileMapPaste` entry meaning "leave this cell alone".
|`JL_TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels.
|`JL_TILE_BYTES_PER_ROW` |4 |Bytes per tile row (8 pixels at 4bpp).
|`JL_TILE_BYTES` |32 |Bytes in one tile.
|`JL_TILE_BLOCKS_PER_ROW` |40 |Tile columns on a surface.
|`JL_TILE_BLOCKS_PER_COL` |25 |Tile rows on a surface.
|`JL_TILE_NO_GLYPH` |0xFFFF |`jlDrawText` map entry meaning "skip this character".
|`JL_TILE_MAP_SKIP` |0xFF |`jlTileMapPaste` entry meaning "leave this cell alone".
|===
[source,c]
----
typedef struct jlTileT {
uint8_t pixels[TILE_BYTES];
uint8_t pixels[JL_TILE_BYTES];
} jlTileT;
----
@ -39,11 +39,12 @@ need a whole scratch surface.
[IMPORTANT]
====
A `jlTileT` is *target-native*: plane-major on the planar ports, chunky on the
others. Treat it as opaque. In particular, do not feed a tile captured by
`jlTileSnap` to `jlTilePasteMono`, which wants a chunky silhouette -- that is a
format mismatch and draws the wrong shape. Use `jlTilePasteGlyph` for 1-bit
glyph data instead.
A `jlTileT` is *target-native*: plane-major on the Amiga and X68000, row by row
with each row's four plane bytes together on the ST, and chunky on the others.
Treat it as opaque. In particular, do not feed a tile from `jlTileSnap` or
`jlTileBankLoad` to `jlTilePasteMono`, which wants a chunky silhouette -- on the
Amiga, ST, and X68000 that is a format mismatch and draws the wrong shape. Use
`jlTilePasteGlyph` for 1-bit glyph data instead.
====
=== jlTileCopy
@ -166,7 +167,7 @@ this needs only a small internal scratch buffer and one pass over the file.
|===
*Returns:* the number of tiles pasted. Blocks beyond the file's tile count keep
their previous content.
their previous content, and tiles past the surface's 1000 blocks are ignored.
=== jlTileSnap
@ -233,10 +234,10 @@ comes from a separate map, as in C64-style colour RAM. Opaque.
[WARNING]
====
This call wants a *chunky* silhouette, but a `jlTileT` from `jlTileSnap` is
target-native. Feeding one to this function on a planar port is a format
mismatch. For glyphs, use `jlTilePasteGlyph`, which takes unambiguous 1-bit
input.
This call wants a *chunky* silhouette, but a `jlTileT` from `jlTileSnap` or
`jlTileBankLoad` is target-native. Feeding one to this function on the Amiga,
ST, or X68000 is a format mismatch. For glyphs, use `jlTilePasteGlyph`, which
takes unambiguous 1-bit input.
====
=== jlTilePasteGlyph
@ -278,7 +279,10 @@ void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by,
Draws a NUL-terminated ASCII string using glyphs from a font surface. Glyph
colour 0 is transparent, so the background shows through. The cursor wraps at
the right edge and truncates at the bottom.
the right edge and truncates at the bottom. A start off the 40x25 grid uses up
characters without drawing them as the cursor moves on: a start column past
39 drops the first character and carries on at the start of the next row, and
a start row past 24 draws nothing.
[cols="1,1,4", options="header"]
|===
@ -288,7 +292,8 @@ the right edge and truncates at the bottom.
|`fontSurface` |`const jlSurfaceT *` |Surface holding the glyphs.
|`asciiMap` |`const uint16_t *` |256 entries mapping ASCII code to glyph
position: low byte is source block X, high byte is source block Y. An entry of
`TILE_NO_GLYPH` skips that character, advancing the cursor without drawing.
`JL_TILE_NO_GLYPH`, or one off the font surface's 40x25 grid, skips that
character, advancing the cursor without drawing.
|`str` |`const char *` |NUL-terminated string.
|===
@ -303,7 +308,7 @@ void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlTileT *tiles, const uint8_t *map);
----
Pastes a whole rectangle of tiles in one call, so validation, dispatch and the
Pastes a whole rectangle of tiles in one call, so validation, dispatch, and the
dirty-rectangle union are paid once for the rectangle instead of once per tile.
For anything larger than a few tiles this is substantially faster than looping
over `jlTilePaste`.
@ -316,7 +321,7 @@ over `jlTilePaste`.
|`wTiles`, `hTiles` |`uint8_t` |Rectangle size in tiles. Zero is a no-op.
|`tiles` |`const jlTileT *` |The tileset.
|`map` |`const uint8_t *` |`wTiles * hTiles` tileset indices, row-major, so
`map[row * wTiles + col]`. `TILE_MAP_SKIP` leaves that cell untouched.
`map[row * wTiles + col]`. `JL_TILE_MAP_SKIP` leaves that cell untouched.
|===
*Returns:* nothing. A rectangle whose edge crosses the surface boundary is
@ -324,7 +329,7 @@ rejected as a no-op.
[WARNING]
====
Any map index other than `TILE_MAP_SKIP` must be a valid tileset index. The
Any map index other than `JL_TILE_MAP_SKIP` must be a valid tileset index. The
batch does not know the tileset length and does *not* range-check it -- an
out-of-range index reads past the end of your array.
====

View file

@ -6,13 +6,13 @@
[cols="3,1,4", options="header"]
|===
|Name |Value |Meaning
|`SURFACE_WIDTH` |320 |Surface width in pixels.
|`SURFACE_HEIGHT` |200 |Surface height in pixels.
|`SURFACE_BYTES_PER_ROW` |160 |Bytes per row, chunky 4bpp.
|`SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes.
|`SURFACE_PALETTE_COUNT` |16 |Palettes per surface.
|`SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette.
|`SURFACE_PALETTE_ENTRIES` |256 |Total palette entries.
|`JL_SURFACE_WIDTH` |320 |Surface width in pixels.
|`JL_SURFACE_HEIGHT` |200 |Surface height in pixels.
|`JL_SURFACE_BYTES_PER_ROW` |160 |Bytes per row, chunky 4bpp.
|`JL_SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes.
|`JL_SURFACE_PALETTE_COUNT` |16 |Palettes per surface.
|`JL_SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette.
|`JL_SURFACE_PALETTE_ENTRIES` |256 |Total palette entries.
|===
=== Tiles
@ -20,13 +20,13 @@
[cols="3,1,4", options="header"]
|===
|Name |Value |Meaning
|`TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels.
|`TILE_BYTES_PER_ROW` |4 |Bytes per tile row.
|`TILE_BYTES` |32 |Bytes per tile.
|`TILE_BLOCKS_PER_ROW` |40 |Tile columns per surface.
|`TILE_BLOCKS_PER_COL` |25 |Tile rows per surface.
|`TILE_NO_GLYPH` |0xFFFF |`jlDrawText`: skip this character.
|`TILE_MAP_SKIP` |0xFF |`jlTileMapPaste`: leave this cell untouched.
|`JL_TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels.
|`JL_TILE_BYTES_PER_ROW` |4 |Bytes per tile row.
|`JL_TILE_BYTES` |32 |Bytes per tile.
|`JL_TILE_BLOCKS_PER_ROW` |40 |Tile columns per surface.
|`JL_TILE_BLOCKS_PER_COL` |25 |Tile rows per surface.
|`JL_TILE_NO_GLYPH` |0xFFFF |`jlDrawText`: skip this character.
|`JL_TILE_MAP_SKIP` |0xFF |`jlTileMapPaste`: leave this cell untouched.
|===
=== Input
@ -34,8 +34,8 @@
[cols="3,1,4", options="header"]
|===
|Name |Value |Meaning
|`JOYSTICK_AXIS_MAX` |127 |Maximum axis value.
|`JOYSTICK_AXIS_MIN` |-127 |Minimum axis value.
|`JL_JOYSTICK_AXIS_MAX` |127 |Maximum axis value.
|`JL_JOYSTICK_AXIS_MIN` |-127 |Minimum axis value.
|`JL_CHAR_QUEUE_SIZE` |32 |Queue capacity; 31 characters can be pending.
|`JL_CHAR_BACKSPACE` |0x08 |Backspace, deletes before the caret.
|`JL_CHAR_TAB` |0x09 |Tab.
@ -49,8 +49,8 @@
[cols="3,1,4", options="header"]
|===
|Name |Value |Meaning
|`JOEY_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4.
|`JOEY_AUDIO_VOICES` |3 |Tone voices, 0..2.
|`JL_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4.
|`JL_AUDIO_VOICES` |3 |Tone voices, 0..2.
|`JL_SFX_RAW_SIGNED8` |0 |Raw stream format: signed 8-bit PCM.
|`JL_SFX_RAW_UNSIGNED8_NOZERO` |1 |Raw stream format: unsigned biased 0x80.
|===
@ -91,7 +91,7 @@
|`sprite.h`
|`jlSpriteBankLoad`, `jlSpriteBankLoadPrecompiled`,
`jlSpriteBankSavePrecompiled`, `jlSpriteCreate`, `jlSpriteDestroy`,
`jlSpriteCompile`, `jlSpriteCompiledSize`, `jlSpritePrewarm`, `jlSpriteDraw`,
`jlSpriteCompile`, `jlSpriteCompiledSize`, `jlSpriteDraw`,
`jlSpriteDrawTrusted`, `jlSpriteSaveUnder`, `jlSpriteSaveUnderTrusted`,
`jlSpriteRestoreUnder`, `jlSpriteRestoreUnderTrusted`, `jlSpriteSaveAndDraw`,
`jlSpriteSaveAndDrawTrusted`, `jlSpriteCreateFromSurface`, `jlSpriteCompact`,
@ -105,7 +105,7 @@
`jlJoystickReset`
|`audio.h`
|`jlAudioInit`, `jlAudioShutdown`, `jlAudioPlayMod`, `jlAudioStopMod`,
|`jlAudioPlayMod`, `jlAudioStopMod`,
`jlAudioIsPlayingMod`, `jlAudioPlaySfx`, `jlAudioPlaySfxStream`,
`jlAudioSfxRawFormat`, `jlAudioPlaySfxStreamRaw`, `jlAudioSetPan`,
`jlAudioStopSfx`, `jlAudioTone`, `jlAudioTickRegister`,
@ -120,7 +120,7 @@
|`serial.h` (opt-in)
|`jlSerialOpen`, `jlSerialClose`, `jlSerialPoll`, `jlSerialAvailable`,
`jlSerialRead`, `jlSerialWrite`, `jlSerialWriteByte`, `jlSerialReadByte`,
`jlSerialFlush`, `jlSerialSetRts`
`jlSerialFlush`, `jlSerialSetDtr`, `jlSerialSetRts`
|`debug.h`
|`jlLog`, `jlLogF`, `jlLogFlush`, `jlLogReset`
@ -139,7 +139,7 @@
smoke-tests what is installed.
|Sprites draw but the game crawls
|The sprites were never compiled. Call `jlSpritePrewarm` during loading. Check
|The sprites were never compiled. Call `jlSpriteCompile` during loading. Check
`jlSpriteCompiledSize` returns non-zero.
|Sprites compile on some machines and not others
@ -147,7 +147,8 @@
eight times the IIgs figure. Measure with `jlSpriteCompiledSize`.
|Works everywhere except the IIgs, failing at startup
|A `malloc` too large for the bank-0 C heap. Use `jlAlloc`.
|A `malloc` too large for the IIgs's C heap, which is 32 KB to 48 KB in all.
Use `jlAlloc`.
|Crashes on IIgs deep in a call chain
|A large function-local array overflowing the soft stack. Make it `static`.
@ -156,10 +157,6 @@
|`jlStagePresent` was never called, or drawing went to a surface that is not
the stage.
|Part of the screen never updates
|Pixels were written by something other than the library primitives, so the
dirty tracking did not see them.
|Runs at different speeds on different machines
|Per-frame steps are hard-coded. Scale by `jlFrameHz()` or use
`jlMillisElapsed()`.
@ -171,23 +168,24 @@
|The character queue holds 31 entries and drops the rest. Pop it every frame.
|Music plays but `jlAudioVoice` does nothing
|On ST and Amiga the module player owns those channels (in the ST build; the
STe build plays modules through the DMA sound chip). Use one layer per scene.
|On the Amiga the module player owns those channels, and on the plain ST
voice 0 is silent while digital sound plays. Use one layer per scene.
|A note sounds for only a fraction of a second on the IIgs
|`jlAudioVoice` is a roughly 80 ms one-shot there. Re-assert it about once a
frame.
|A note sounds for only a fraction of a second
|`jlAudioFrameTick` is not being called every frame. The IIgs plays each note
as a short sample and restarts the notes being held from it.
|A streaming sound effect stops after its first chunk
|`jlAudioFrameTick` is not being called every frame. IIgs and Amiga fire the
next chunk from it.
|`jlAudioFrameTick` is not being called every frame. Every port feeds streams
from it.
|Sprite trails or corruption around moving objects
|Save-under and clean-buffer erasing are being mixed in one region, or a
`jlSurfaceCopyRect` invalidated a live backup by snapping outward to the
16-pixel grid.
|Save and restore are unexpectedly slow on Amiga or ST
|Save and restore are unexpectedly slow on the Amiga, ST, or X68000, or the
trusted variants crash on the Amiga or ST
|The backup buffer is at an odd address. Declare it `uint16_t`.
|A tile bank refuses to load

View file

@ -1,6 +1,6 @@
== Creating Assets
JoeyLib does not load PNGs, WAVs or MODs at run time. Every asset is converted
JoeyLib does not load PNGs, WAVs, or MODs at run time. Every asset is converted
on the build host into a compact binary the target can use directly, which is
what keeps loading fast and the runtime small. This chapter covers each asset
type: what you author, which tool bakes it, and which API call loads it.
@ -31,6 +31,11 @@ type: what you author, which tool bakes it, and which API call loads it.
|`.jym` or a C header
|`jlMusicPlay`
|A tracker song (Furnace) for the SN76489 or AY-3-8910, exported as VGM
|`vgm2song.py`, then `songbake.py`
|`.jym` or a C header
|`jlMusicPlay`
|Protracker `.MOD`
|`joeymod`
|`.mod`, `.amod`, `.ntp`
@ -45,6 +50,52 @@ type: what you author, which tool bakes it, and which API call loads it.
Baked assets are staged into `DATA/` beside the executable, and the program
opens them with bare names.
=== Content Creation Tools
These are the tools we recommend for making each kind of asset. All of them
run on Linux, and all but OpenMPT are native there.
[cols="2,4,3", options="header"]
|===
|You make |With |Notes
|Tile and sprite sheets
|*GrafX2* (GPLv2), a Deluxe Paint-style editor built around an indexed
palette. *LibreSprite* (GPLv2), for sprite sheets drawn as layers and
animation frames. *GIMP* (GPLv3), to bring existing art down to an indexed
PNG.
|In GrafX2, set the palette's RGB scale to 16 (right-click the palette
button for the secondary palette menu): every colour you pick is then one a
JoeyLib palette holds exactly, 4 bits a gun. The sheet rules are in
<<_authoring_the_png>>.
|Program icons
|Any of the above, saving an RGBA PNG with transparency.
|One picture for every system (<<_icons>>).
|Chip-tracker music
|*Furnace* (GPLv2 or later), a chiptune tracker that emulates the chips it
writes for. `./install.sh install furnace` puts it in
`toolchains/opt/furnace`.
|Write for the TI SN76489 or the AY-3-8910, export VGM, and convert it
(<<_writing_songs_in_furnace>>).
|`.MOD` music
|*MilkyTracker* (GPLv3), the *ProTracker 2 clone* (open source), or
*OpenMPT* (BSD; Windows, runs under Wine).
|Four channels, then `joeymod` (<<_music_protracker_modules>>).
|Sound effects
|*sfxr* (MIT) or *jfxr* (open source, in a browser) for generated game
sounds; *Audacity* (GPLv3) to edit recordings; *SoX* to convert.
|Save a WAV, then convert it to raw signed 8-bit PCM
(<<_sound_effects>>).
|===
None of these is needed to build a program: the baked files are what the
build uses. Only Furnace has an installer component, because
`scripts/check-vgm.sh` uses it to test the converter on real songs.
=== Graphics: Tiles and Sprites
==== Authoring the PNG
@ -75,11 +126,11 @@ $ python3 tools/assetbake/assetbake.py --type tile --target atarist \
Tile banks are *per-target*, because the 8x8 block layout is the platform's
native pixel format -- plane-major on Amiga and X68000, row-major-with-planes-
per-row on ST, chunky 4bpp on DOS and IIgs. You must bake one `.tbk` per platform, and the
per-row on ST, and chunky 4bpp on DOS and IIgs. You must bake one `.tbk` per platform, and the
loader refuses a file baked for a different target rather than drawing garbage.
`--target` is required for tiles, and accepts `amiga`, `atarist`, `dos`,
`iigs` or `x68000`.
`iigs`, or `x68000`.
==== Baking Sprites
@ -93,8 +144,8 @@ $ python3 tools/assetbake/assetbake.py --type sprite --cell 2x3 \
pixels. The sheet is cut into cels row by row.
Sprite `.spr` banks are *cross-target*: the same file works on every platform,
because the sprite walker reads chunky 4bpp and converts at draw time. Bake
once, ship everywhere.
because it holds chunky 4bpp tiles and each port converts a sprite to its own
form when the sprite is created. Bake once, ship everywhere.
Both formats can embed the 16-entry `$0RGB` palette, and both loaders will hand
it back through their `outPalette` parameter so artwork carries its own colours
@ -192,33 +243,88 @@ tone voice 2. Author noise hits and voice-2 notes so they do not sound at the
same time; `songbake` warns when they overlap.
====
==== Writing Songs in Furnace
A tracker is easier than a text file for anything longer than a jingle. Write
the song in Furnace, export it as VGM (a log of the chip's register writes as
the song plays), and `tools/vgm2song.py` turns that into a `.song` file, which
is still plain text you can read and touch up:
. Start the song on a system with a *TI SN76489* (the Sega Master System, BBC
Micro, or TI-99/4A) or an *AY-3-8910 / YM2149* (the ZX Spectrum, MSX, or
Atari ST's chip). The SN76489 is the closer match: three square tone voices
and a noise channel with its own volume, attenuated in 2 dB steps exactly as
JoeyLib's are. Use that one chip only; another chip's part (a Master System
FM unit, say) is left out.
. Export: *File > Export > VGM*, or `furnace -vgmout song.vgm song.fur` from a
shell.
. Convert and bake:
+
[source,console]
----
$ python3 tools/vgm2song.py song.vgm song.song
$ python3 tools/songbake/songbake.py -c gSong song.song song.h
----
What comes across:
* *Tones* at their exact frequencies (`@Hz` cells) and volumes; an AY volume
`v` becomes attenuation `15 - v`, exact on the ST.
* *Noise* at the nearest JoeyLib noise pitch. The SN76489's periodic noise (a
pitched buzz, the Master System's bass trick) plays as a square at its
pitch on a tone voice that is free, and as noise when all three are busy.
* *The AY's volume envelope*, followed row by row. An envelope repeating
faster than two rows is a timbre (the "buzzer"), not a volume: it plays at
full volume, and a channel whose tone is off sounds at the envelope's
frequency.
* *Timing.* One `.song` row a tick, by default the song's update interval
rounded down (16 ms at 60 Hz, 20 ms at 50 Hz), placed by the VGM's own clock,
so the length and the loop point are right to within a tick.
* *The loop point*, if the song has one.
A baked song must stay under 64 KB. A long, busy song that would not fit gets
a longer tick, a whole multiple of the default, which merges changes faster
than it (vibrato, arpeggios); `--tick MS` sets the tick yourself. The
converter warns about everything it approximates.
=== Music: Protracker Modules
For richer music, author a four-channel `.MOD` in any tracker (ProTracker,
MilkyTracker, OpenMPT). Then convert per target:
MilkyTracker, OpenMPT) -- one whose signature is `M.K.`, `M!K!`, `FLT4`, or
`4CHN`; the other module formats and the 15-sample Soundtracker files are not
played. Every port plays its effects as Protracker 2.3 does. Then convert
per target:
[source,console]
----
$ tools/joeymod/joeymod song.mod song.mod # DOS and ST: validated copy
$ tools/joeymod/joeymod song.mod song.amod # Amiga
$ tools/joeymod/joeymod song.mod song.mod # every port but the IIgs
$ tools/joeymod/joeymod song.mod song.amod # Amiga, with an end marker
$ tools/joeymod/joeymod song.mod song.ntp # Apple IIgs
----
The output extension selects the conversion:
* *`.mod`* -- a passthrough copy with the signature validated. DOS and ST
detect song end inside libxmp-lite, so the file is used as-is.
* *`.amod`* -- the same, plus an `E8FF` stop marker injected at song end. The
Amiga replayer, PTPlayer, has no native song-end signal, so without this
marker `jlAudioPlayMod(..., loop=false)` loops anyway. Other replayers ignore
`E8FF`, so an `.amod` is harmless if it reaches the wrong port.
* *`.mod`* -- a passthrough copy with the signature validated, played as it
is on every port but the IIgs. The ports that mix modules in software
(DOS, the ST, the X68000, and the hosted ports) find the song's end
themselves, and the Amiga adds an end marker as it copies the module to
chip RAM, so `+jlAudioPlayMod(..., loop=false)+` plays the song once.
* *`.amod`* -- the same, plus an `E8F` stop marker added at song end ahead of
time. The Amiga replayer, PTPlayer, has no song-end signal of its own and
needs the marker, but the Amiga adds it itself when a song is played with
`loop=false`, so a plain `.mod` works there too. A module with all 128 song
positions or all 128 patterns in use has no room for the marker either way,
and loops on the Amiga. The other ports' module player stops a song that
does not loop at the marker too, and the IIgs's replayer ignores `E8F`, so
an `.amod` plays the same anywhere (looping, with the marker's pattern of
silence at its end).
* *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the
NinjaTrackerPlus converter the installer's `ntp` component provides (part of
`toolchain:iigs`; it needs the PHP command-line interpreter, which the
installer's host packages include).
Keep modules modest. A four-channel mix is real CPU work on DOS and the ST, and
the sample data has to fit in memory alongside your game.
Keep modules modest. A four-channel mix is real CPU work on DOS, the ST, and
the X68000, and the sample data has to fit in memory alongside your game.
=== Sound Effects
@ -260,7 +366,7 @@ jlSurfaceSaveFile(surface, "title.sur");
jlSurfaceLoadFile(surface, "title.sur");
----
The file is 32000 bytes of pixels, 200 SCB bytes and 512 bytes of palette. It
The file is 32000 bytes of pixels, 200 SCB bytes, and 512 bytes of palette. It
is *not* portable between platforms -- no format conversion happens, so the
machine that wrote it must be the machine that reads it. It is best used as a
runtime cache (a generated title screen, say), not as a shipped asset. For
@ -292,10 +398,12 @@ $(call programDataDir,MYGAME)/font.tbk: assets/font.png
python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target $(ASSET_PLATFORM) $< $@
----
Tile banks and `.spc` files differ per platform, while `.spr`, `.jym` and
`.sfx` files are shared: `ASSET_PLATFORM` names the target a bake is for (DVX,
SDL3 and the web take the DOS bakes; the last two use only a `.spc`'s cels, not
their code) and `SPRITEBAKE` the matching host `spritebake`.
Tile banks and `.spc` files differ per platform, while `.spr`, `.jym`, and
`.sfx` files are shared: `ASSET_PLATFORM` names the target a bake is for (DVX
and every hosted port -- SDL3, the web, Win32, Win16, the PSP, Haiku, and
BeOS -- take the DOS bakes; DVX and Win16 run the x86 code of a `.spc`, the
others use only its cels; the STe build takes the ST's) and `SPRITEBAKE` the
matching host `spritebake`.
`examples/tilebank` is a working reference: one rule in `make/examples.mk`
bakes the same PNG for each target into its `DATA/tilebank` (`TILEBANK_DATA`),
@ -313,7 +421,7 @@ per-level banks marked optional, and speech baked with per-target flags.
// Tiles into a caller-owned array.
jlTileT tiles[256];
bool valid[256];
uint16_t palette[SURFACE_COLORS_PER_PALETTE];
uint16_t palette[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t n = jlTileBankLoad("font.tbk", tiles, 256, valid, palette);
// Or stream a big bank straight onto a surface, with no caller buffer.

View file

@ -1,11 +1,11 @@
== Building Your Program
JoeyLib does not install as a system library: each target needs its own cross
compiler, link step, packaging and disk image, and JoeyLib's makefiles own all
compiler, link step, packaging, and disk image; JoeyLib's makefiles own all
of that. Your program lives in a directory of its own -- its own repository,
usually -- next to a JoeyLib checkout. Its Makefile describes the program and
includes two of JoeyLib's makefiles, and that is enough to build it for every
target with exactly the flags, link order and packaging JoeyLib's own examples
target with exactly the flags, link order, and packaging JoeyLib's own examples
get. JoeyLib's examples are described the same way and built by the same
rules; there is one set of them.
@ -62,19 +62,20 @@ The project's outputs land in its own `build/`, laid out like JoeyLib's:
|Target |Output
|Apple IIgs |`build/iigs/bin/MYGAME` (an S16 OMF), its data staged in
`build/iigs/data/MYGAME/`, and with `MYGAME_IIGS_GAME_DISK` the disks
`build/iigs/bin/MYGAME.2mg` and `MYGAME-data.2mg`
|Amiga |`build/amiga/bin/Mygame`
`build/iigs/data/MYGAME/`, its Finder icon in `build/iigs/icon/MYGAME/`, and
with `MYGAME_IIGS_GAME_DISK` the disks `build/iigs/bin/MYGAME.2mg` and
`MYGAME-data.2mg`
|Amiga |`build/amiga/bin/Mygame` and its Workbench icon, `Mygame.info`
|Atari ST |`build/atarist/bin/MYGAME.PRG`, which runs on every ST
|Atari STe |`build/ste/bin/MYGAME.PRG`, the STe build: STe and Mega STe only
(<<_the_atari_st_family>>); same toolchain and baked data as the ST
|MS-DOS |`build/dos/bin/MYGAME.EXE`, with the DPMI server built in
|Sharp X68000 |`build/x68000/bin/MYGAME.X`
|DVX |`build/dvx/bin/apps/joeylib/mygame/`: `mygame.app`, `mygame.hlp` and its
|DVX |`build/dvx/bin/apps/joeylib/mygame/`: `mygame.app`, `mygame.hlp`, and its
`DATA/`
|SDL3 |`build/sdl3/bin/MYGAME`, a native program with SDL 3 linked in
|Web |`build/web/bin/MYGAME/`: `MYGAME.html`, `.js`, `.wasm`, and `.data`
(its `DATA/`, packed in)
(its `DATA/`, packed in), and `icon.png`
|Win32 |`build/win32/bin/MYGAME.EXE`, a 32-bit Windows program
|Win16 |`build/win16/bin/MYGAME.EXE`, a Windows 3.x program
|PSP |`build/psp/bin/MYGAME/`: `EBOOT.PBP` and its `DATA/`, a folder for the
@ -84,7 +85,8 @@ memory stick's `PSP/GAME/`; the ELF it is packed from in
A program with `MYGAME_FOLDER` set ships in a folder of its own with its
`DATA/` beside it (`build/dos/bin/MYGAME/MYGAME.EXE`); otherwise programs share
`bin/DATA/`.
`bin/DATA/`. A program's icon (<<_icons>>) is baked in
`build/<target>/icon/MYGAME/` on its way into the program.
JoeyLib's library for each target, and the host tools such as `spritebake`, are
built on demand where JoeyLib keeps them, in the checkout's `build/`, and shared
@ -132,13 +134,16 @@ probes for gates).
|`ID_PORTS` |The targets it builds for. Every target when unset.
|`ID_NAME` |Its file name without the extension (see <<_naming>>).
|`ID_FOLDER` |Set: it ships in a folder of its own with its `DATA/` (Amiga,
ST, DOS, X68000, SDL3, Win32, Win16; on the IIgs, DVX, the web and the PSP
ST, DOS, X68000, SDL3, Win32, Win16; on the IIgs, DVX, the web, and the PSP
every program is packaged apart anyway).
|`ID_BINDIR` |Build it there instead of `bin/`.
|`ID_DATA` |Its data files (<<_data_files>>).
|`ID_TITLE` |DVX: its window title. PSP: its title in the PSP's menu (by
default its name).
|`ID_HELP` |DVX: the line its help file says about it. No apostrophes.
|`ID_ICON` |Its icon: a picture (a PNG, best square, transparent around the
figure), baked for every target whose system shows programs by an icon
(<<_icons>>). JoeyLib's kangaroo by default.
|`ID_HELP` |DVX: the line its help file says about it.
|`ID_IIGS_OVERFLOW_CAP` |IIgs: link large overflow segments (`0xF000`), which
a big program needs to leave the Memory Manager room (see
`make/support/iigs/clang-build.sh`).
@ -174,11 +179,12 @@ set up:
|`$(call programDataDir,ID)` |Where its `DATA/` files go on this target.
|`$(call programOptionalDir,ID)` |Where the files go that it can run without
(<<_disk_images_and_packaging>>).
|`PLATFORM` |The target: `iigs`, `amiga`, `atarist`, `ste`, `dos`, `x68000`, `dvx`, `sdl3`, `web`, `win32`, `win16`, `psp`.
|`PLATFORM` |The target: `iigs`, `amiga`, `atarist`, `ste`, `dos`, `x68000`, `dvx`, `sdl3`, `web`, `win32`, `win16`, `psp`, `haiku`, `haiku32`, `beos`.
|`ASSET_PLATFORM` |The target its bakes are for. DVX shows the DOS stage and
runs x86 code, so it takes the DOS bakes; the hosted ports' stage (SDL3, the
web, Win32, Win16, the PSP) is the same chunky one, so they take them too
(the cels of a pre-compiled sprite bank, without their code).
web, Win32, Win16, the PSP, Haiku, BeOS) is the same chunky one, so they take
them too: Win16 runs a pre-compiled sprite bank's x86 code as DVX does, and
the others take its cels without their code. The STe build takes the ST's.
|`SPRITEBAKE` |The host `spritebake` for this target (its rule is
JoeyLib's).
|`PROJECT_DIR`, `PROJECT_BUILD` |The project and its `build/`.
@ -186,7 +192,7 @@ JoeyLib's).
|`HOST_CC`, `HOST_CFLAGS` |For a host tool of the project's own.
|===
A copy, a tile-bank bake and a pre-compiled sprite bank:
A copy, a tile-bank bake, and a pre-compiled sprite bank:
[source,makefile]
----
@ -252,7 +258,8 @@ off a CFFA2 card. `make iigs-game-disk GAME=NAME` builds one for any program,
`GAME_DISK_SIZE=2048KB` forces the size, and `make iigs-verify-game-disk`
boots it headlessly and checks it draws. The data the program needs goes on with
it; files listed under `programOptionalDir` (a program's optional data, which
it can run without) go on last, only while the volume has room.
it can run without) go on last, only while the volume has room, and the
programs' Finder icons last of all (<<_icons>>).
*Sharp X68000.* Programs ship on a Human68k floppy; `joey-run` builds one for
each run from the program and its `DATA/`. A floppy is 1232 KB, so a large
@ -266,6 +273,7 @@ seems to vanish.
needs to be on the disk.
*Amiga.* `joey-run` writes an `s/startup-sequence` that invokes your binary.
`Name.info` beside it is its Workbench icon, from which it starts too.
*DVX.* Every program is a folder laid out as it installs in DVX:
`build/dvx/bin/apps/joeylib/mygame/` holds `mygame.app`, its help file
@ -287,16 +295,64 @@ DLLs, so its folder, with its `DATA/`, is all it needs; it ships the way a
DOS program does.
*Win16.* A program is one `.EXE`, its 32-bit code bound to Open Watcom's
Win386 supervisor and calling only what Windows 3.1 has (USER, GDI, KERNEL
Win386 supervisor and calling only what Windows 3.1 has (USER, GDI, KERNEL,
and MMSYSTEM), so its folder, with its `DATA/`, is all it needs; it ships the
way a DOS program does.
*PSP.* A program is its folder, `build/psp/bin/MYGAME/`: `EBOOT.PBP` (the
program, SDL 3 linked in, packed as the PSPSDK packs it, with no icon or
pictures) and its `DATA/`. Copy the folder into `PSP/GAME/` on a memory
stick; the PSP shows it by its `ID_TITLE`. The unstripped ELF stays in
program, SDL 3 linked in, packed as the PSPSDK packs it, with its icon and no
other pictures) and its `DATA/`. Copy the folder into `PSP/GAME/` on a memory
stick; the PSP shows it by its icon and its `ID_TITLE`. The unstripped ELF stays in
`build/psp/elf/`, for `psp-addr2line` on an address a crash reports.
=== Icons
A program has an icon on every system that shows programs by one. `ID_ICON`
names a picture, a PNG, best square and transparent around the figure, and
`make` bakes it for each target with `tools/iconBake.py`; a program without
one gets JoeyLib's kangaroo, `assets/joeyIcon.png`. A picture of 64 pixels or
less is taken for pixel art and enlarged by whole pixels; a bigger one is
resampled smoothly. Where an icon has few colours, each pixel takes the
nearest.
[cols="1,5", options="header"]
|===
|Target |The icon
|Apple IIgs |The Finder's: an icon file (type `$CA`) for each program, which
the disk scripts put in the volume's `Icons` folder as `NAME.ICONS`, where the
Finder reads icon files when it starts. 24x20 and 12x10 colour icons, which
the Finder shows as they are: in its 640 mode each icon pixel is two screen
pixels, black, red, green, or white beside black, blue, yellow, or white, seen
together as one of 16 colours (an icon drawn at an odd screen column would
pair them with the other halves of the palette; the Finder drew every one at
an even column). A data disk takes the icons
last, while there is room; a game disk keeps its program's at its root. A
program copied to another volume takes its icon file along, into that
volume's `Icons` folder.
|Amiga |Workbench's: `Name.info` beside the program, a tool's icon in
Workbench's four colours, 48x24 hires pixels, which starts it with
`AMIGA_ICON_STACK` (32768) bytes of stack. Started from it, the program runs
in its own folder, as it does from a shell.
|DVX |Program Manager's: the program's `icon32` resource, 32x32 on DVX's grey.
|Win16 |Program Manager's and the minimised window's: 32x32 in the 16 VGA
colours, bound into the `.EXE`.
|Win32 |Explorer's and the window's: 16, 32, and 48 pixels, each in 256
colours and in 32-bit, linked into the `.EXE`.
|Haiku, BeOS |Tracker's and the Deskbar's: 32x32 and 16x16 in the BeOS
palette, in the program's resources. Tracker shows a file by the icons in its
attributes, which a program built on another machine does not have; the
program copies them across the first time it runs, so Tracker shows it by its
icon from then on (or at once, after `mimeset -F NAME`).
|PSP |The menu's: `ICON0` in the EBOOT, 144x80.
|SDL3 |The window's, 64x64, which the desktop shows where it shows one (title
bar, task bar, dock).
|Web |The page's: `icon.png`, 32x32, beside it.
|===
MS-DOS, the Atari ST, and the X68000 show no program by an icon of its own,
so their programs get none.
=== Verifying It Actually Runs
Build success is weak evidence on retro targets. Run it with `joey-run` (see
@ -353,7 +409,7 @@ Human68k floppies, environment checks) takes paths.
|`make verify-audio`
|Every port's audio gate: the IIgs DOC and X68000 chips are read directly,
the ST's YM2149 register writes are traced (and for the STe build the
samples the DMA sound chip plays), the Amiga, DOS and DVX output
samples the DMA sound chip plays), the Amiga, DOS, and DVX output
is recorded and graded, SDL3's mix is written to a file by SDL's disk
driver and graded, and the web page's sound is recorded in headless
Chromium and graded, as is the Win32 program's under Wine, the Win16
@ -365,7 +421,7 @@ if a gate had to be skipped.
|The IIgs actually reached the program and drew something.
|`make iigs-verify-game-disk`
|A program's bootable IIgs volume boots, launches the program and draws.
|A program's bootable IIgs volume boots, launches the program, and draws.
|`scripts/verify-<port>-tilebank.sh`
|The port's own tile-bank bake loads pixel-exact both ways, with the same
@ -373,6 +429,13 @@ if a gate had to be skipped.
for the other machines are refused. `scripts/verify-all.sh tilebank` runs
every port's.
|`scripts/verify-all.sh icon`
|Every program carries its icon where its system looks
(`scripts/check-icons.sh`, no emulator), and each system shows it: the IIgs
Finder's window, the Amiga's icon.library (and a Workbench start), DVX's
Program Manager, Windows 3.11's desktop with the program minimised, the
Win32 and SDL3 windows' icons on X, and Tracker on Haiku and BeOS.
|`scripts/bench-<port>.sh`
|Per-operation throughput for that port.

View file

@ -64,7 +64,7 @@ Toolchain availability:
==== Installer reference
`./install.sh [options] COMMAND [TARGETS...]`, run from the top of the
`+./install.sh [options] COMMAND [TARGETS...]+`, run from the top of the
repository.
[cols="1,3"]
@ -91,9 +91,9 @@ libraries, for example). It reports anything out of date, any component
whose files have gone, and any part of a port you installed only partly,
such as FS-UAE without a Kickstart.
| `import PATH...`
| `+import PATH...+`
| Installs the files that are not downloaded (see <<Firmware that is not
downloaded>>) from files, folders, `.zip` archives or Amiga `.adf` disk
downloaded>>) from files, folders, `.zip` archives, or Amiga `.adf` disk
images, matched by checksum.
| `bundle FILE`
@ -198,7 +198,7 @@ trees.
Nothing in `toolchains/` is part of the repository: delete the whole folder
to return the machine to a clean state, and `./install.sh` re-creates it. The
installer itself lives in `install.sh` and `installer/` (its component
recipes, patches and package lists), and the build helpers the makefiles call
recipes, patches, and package lists), and the build helpers the makefiles call
live in `make/support/`.
=== Building
@ -251,6 +251,7 @@ int main(void) {
jlConfigT config;
config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
@ -266,10 +267,13 @@ int main(void) {
}
----
Three things to notice, because they apply to every JoeyLib program:
Four things to notice, because they apply to every JoeyLib program:
. *One header.* `#include <joey/joey.h>` pulls in everything except the
optional serial add-on.
. *Set every field of `jlConfigT`.* It is read whole, so a field left unset
holds whatever was in memory. `noAudio` says whether the program wants sound:
`jlInit` starts audio unless it is `true`.
. *`jlInit` can fail*, and when it does `jlLastError()` tells you why. Always
check it.
. *`jlShutdown` is not optional.* It restores the display mode, releases the
@ -289,10 +293,11 @@ graphics on screen and waits for a keypress:
int main(void) {
jlConfigT config;
jlSurfaceT *stage;
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint8_t i;
config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) {
return 1;
}
@ -300,7 +305,7 @@ int main(void) {
stage = jlStageGet(); // the library-owned back buffer
// Build palette 0. Entry 0 is forced black whatever we put here.
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = 0x000;
}
colors[1] = 0xFFF; // white
@ -309,12 +314,12 @@ int main(void) {
jlPaletteSet(stage, 0, colors);
// Every scanline uses palette 0 (this is also the default).
jlScbSetRange(stage, 0, SURFACE_HEIGHT - 1, 0);
jlScbSetRange(stage, 0, JL_SURFACE_HEIGHT - 1, 0);
jlSurfaceClear(stage, 0);
jlFillRect(stage, 40, 30, 240, 140, 3);
jlDrawRect(stage, 40, 30, 240, 140, 1);
jlDrawLine(stage, 0, 0, SURFACE_WIDTH - 1, SURFACE_HEIGHT - 1, 2);
jlDrawLine(stage, 0, 0, JL_SURFACE_WIDTH - 1, JL_SURFACE_HEIGHT - 1, 2);
jlStagePresent(); // nothing is visible until this runs
jlWaitForAnyKey();
@ -375,7 +380,7 @@ the next. Nothing is written to your home directory.
| The machine, when naming an example rather than a file.
| `--data DIR`
| Copy `DIR`'s files into the program's `DATA` folder for this run, for
| Copy the files in `DIR` into the program's `DATA` folder for this run, for
example the game an interpreter plays.
| `--headless`
@ -392,7 +397,7 @@ example the game an interpreter plays.
| Apple IIgs: the ROM 01 machine instead of ROM 03.
| `--machine st`
| Atari: Hatari's machine, `st`, `megast`, `ste` or `megaste`. By default an
| Atari: Hatari's machine, `st`, `megast`, `ste`, or `megaste`. By default an
ST-build program (`make atarist`) gets a plain ST and an STe-build one
(`make ste`) an STe; the STe build runs only on `ste` and `megaste`.
@ -419,7 +424,7 @@ Kickstarts (`./install.sh install emulator:aga`).
default is 9600.
| `-- ARGS`
| Pass `ARGS` to the program. DOS, SDL3, Win32 and Win16 only: the other
| Pass `ARGS` to the program. DOS, SDL3, Win32, and Win16 only: the other
machines' autostart cannot pass arguments, and a page and a PSP program have
none.
|===
@ -440,7 +445,7 @@ desktop.
An SDL3 program is a native program, so `joey-run` simply starts it, in a
window; running the file yourself does the same. `--headless` runs it on a
private virtual display at one window pixel per stage pixel, so a
`--snapshot` is exactly the 320x200 stage. The window's size, shape and full
`--snapshot` is exactly the 320x200 stage. The window's size, shape, and full
screen are set by environment variables (<<_sdl3_programs>>). The SDL3 port
has no serial port.
@ -457,7 +462,7 @@ in when it is built, so `--data` does not apply.
A Windows program runs under Wine: in a window on your display, with its
Wine settings kept in the project's `build/win32/prefix` so they are set up
once (nothing goes in `~/.wine`; a run with `--serial` uses a private one, so
the kept COM1 is left alone). The window's size, shape and full screen are
the kept COM1 is left alone). The window's size, shape, and full screen are
set by the same environment variables as SDL3's, and Alt+Enter switches. With
`--headless` it runs on a private virtual display at one pixel per stage
pixel, so a `--snapshot` is the stage.
@ -474,7 +479,7 @@ up to 115200 baud). Their components, `kpjoy` and `kpcomm`, build them from
the kpjoy and mscomm projects, checkouts beside JoeyLib's (`../kpjoy`,
`../mscomm/drv`; `KPJOY_DIR` and `KPCOMM_DIR` name others). The program is
Windows' shell, so Windows ends when the program does, and its folder is
drive D:, so what it saves stays there. The window's size, shape and full
drive D:, so what it saves stays there. The window's size, shape, and full
screen are set by the same environment variables as SDL3's: `joey-run` puts
them in the DOS environment Windows starts from. Alt+Enter is DOSBox's own
full-screen key, so the program switches with Alt and the keypad's Enter.
@ -496,11 +501,11 @@ serial port.
==== Haiku and BeOS
A Haiku (`haiku`, `haiku32`) or BeOS R5 (`beos`) program runs in that port's
VM under QEMU (`./install.sh install emulator:haiku`, `emulator:haiku32` or
VM under QEMU (`./install.sh install emulator:haiku`, `emulator:haiku32`, or
`emulator:beos`), in QEMU's window, from a copy of its folder on a second
disk. The machine boots from a disk it never writes to, runs the program,
and powers off when the program ends; what the program wrote in its folder
(saves, its log) is then copied back. The window's size, shape and full
(saves, its log) is then copied back. The window's size, shape, and full
screen are set by the same environment variables as SDL3's. Haiku's VM has
sound; BeOS's has none, and is taken through R5's boot loader menu to its
fail-safe video mode (1024x768, 16-bit colour), as R5 has no driver for any
@ -585,7 +590,7 @@ arrives. To connect out, `socat` joins a pseudo-terminal to the network.
| As on the ST: Wine's COM1 can only be a device, so `socat` joins a
pseudo-terminal to the network both ways.
| SDL3, Web, PSP
| SDL3, Web, PSP, Haiku, BeOS
| None
| These ports have no serial port; `jlSerialOpen` fails there.
|===
@ -604,14 +609,14 @@ rate the program sets.
and X68000, where MAME prints `Unable to load image` and runs with the port
unplugged.
* While a port is listening, what the program sends waits for the connection,
except on DOS, DVX and Win16, where DOSBox drops it just as a cable with
except on DOS, DVX, and Win16, where DOSBox drops it just as a cable with
nothing on the other end would. `examples/sertest` sends its banner at once,
so there a client that connects late sees only the echo.
==== Firmware that is not downloaded
Almost everything the emulators need is downloaded: EmuTOS for the Atari ST,
the Apple IIgs ROMs, GS/OS 6.0.4 and the CFFA2 card's firmware, and Sharp's own
Almost everything the emulators need is downloaded: EmuTOS for the Atari ST;
the Apple IIgs ROMs, GS/OS 6.0.4, and the CFFA2 card's firmware; and Sharp's own
free release of the X68000 IPL-ROM and Human68k 3.02. A few files are not,
because they were never released for free: the Amiga Kickstart 3.1 ROM, the
Amiga `serial.device` and `lowlevel.library` from Workbench 3.1 (Kickstart
@ -621,7 +626,10 @@ Kickstart 3.1 (optional: only for running the Amiga programs on those
machines), the X68000 CG (font) ROM,
for the Win16 port the Windows for Workgroups 3.11 install disc (as an
`.iso`) and the S3 Trio64V+ 1.70.04 Windows 3.1 display driver, from which
the installer sets Windows up unattended, and for the BeOS port the BeOS 5
the installer sets Windows up unattended (and, optionally, Microsoft's WinG
1.0 SDK, `WING10.EXE`, whose runtime the Win16 port draws through when it is
installed: import it, then `./install.sh install wing`; archive.org keeps a
copy), and for the BeOS port the BeOS 5
Professional CD (its CloneCD `.img`, or the `.7z` holding it), from which
the installer takes R5's headers and libraries and makes the VM. Supply your
copies once:
@ -631,7 +639,7 @@ copies once:
$ ./install.sh import ~/my-roms
----
`import` takes files, folders, `.zip` and `.7z` archives and Amiga `.adf` disk images
`import` takes files, folders, `.zip` and `.7z` archives, and Amiga `.adf` disk images
(point it at your Workbench 3.1 disk for `serial.device` and
`lowlevel.library`, and at the S3
driver's `.zip` for its three files), identifies each file by its checksum
@ -655,9 +663,9 @@ built for every platform:
|`draw` |Every drawing primitive, laid out in four quadrants.
|`pattern` |Palette and per-scanline palette (SCB) work.
|`keys` |Typed-character input and the key predicates.
|`joy` |Joystick axes, buttons and recalibration.
|`sprite` |Sprite loading, compiling, save-under and restore.
|`audio` |Tracker music, `.MOD` playback, digital sound effects, a streamed sound and the noise generator.
|`joy` |Joystick axes, buttons, and recalibration.
|`sprite` |Sprite loading, compiling, save-under, and restore.
|`audio` |Tracker music, `.MOD` playback, digital sound effects, a streamed sound, and the noise generator.
|`save` |Save files and the disk-space query.
|`tilebank` |Loading a baked tile bank both ways, and a bank for another machine refused.
|`serial` |The optional RS-232 add-on.

View file

@ -3,8 +3,8 @@
=== What JoeyLib Is
JoeyLib is a single-codebase C game library for five 16-bit and 32-bit retro
machines, for DVX, a DOS desktop whose programs run in windows, and for
today's desktops, browsers and the PSP through SDL3, and for Windows from
machines, for DVX, a DOS desktop whose programs run in windows, for
today's desktops, browsers, and the PSP through SDL3, and for Windows from
3.1 on. You write one program against one API, and the same source compiles
and runs on all of them:
@ -28,8 +28,9 @@ and runs on all of them:
|512 KB
|Word-interleaved planar. Shares 68000 assembly with the Amiga port. Two
builds: the ST build runs on every ST and never uses STe hardware; the STe
build (`make ste`) uses the STe's 4-bit palette, DMA sound and BLiTTER, and a
Mega STe's 16 MHz, and runs only there. See <<_the_atari_st_family>>.
build (`make ste`) uses the STe's 4-bit palette, DMA sound, BLiTTER,
enhanced joystick ports, and a Mega STe's 16 MHz, and runs only there. See
<<_the_atari_st_family>>.
|MS-DOS
|386 or later
@ -73,16 +74,17 @@ and runs on all of them:
|what Windows 3.1 in 386 enhanced mode needs
|A Windows 3.x program: the hosted core with a backend written to the
Windows 3.1 API, in 32-bit code through Open Watcom's Win386 extender; 8-bit
indexed, so every palette is exact on a 256-colour display. See
<<_windows_programs>>.
indexed, so every palette is exact on a 256-colour display, and sprites
compiled as on DOS. See <<_windows_programs>>.
|PSP
|MIPS Allegrex at 222 MHz
|32 MB (PSP-1000)
|PSP homebrew, for custom firmware and PPSSPP: the SDL3 port built with
pspdev. The stage sits at 1:1 in the middle of the 480x272 screen; the
d-pad, nub and buttons are joystick 0, Start is Return and Select Escape.
See <<_psp_programs>>.
d-pad, nub, and buttons are joystick 0; Start is Return, Select is Escape,
and Square opens a keyboard on the screen for every other key. See
<<_psp_programs>>.
|===
The Apple IIgs is deliberately the slowest target and the design reference. If
@ -116,7 +118,7 @@ that out of your program:
|===
|Platform |In-memory pixel storage
|Apple IIgs, MS-DOS, DVX, SDL3, Web, PSP
|Apple IIgs, MS-DOS, DVX, SDL3, Web, PSP, Win32, Win16, Haiku, BeOS
|Chunky 4bpp packed: two pixels per byte, high nibble is the left pixel.
|Commodore Amiga
@ -126,7 +128,8 @@ that out of your program:
|Native planar, word-interleaved: four plane words per 16 pixels, per row.
|Sharp X68000
|Text-plane planes, four planes 128 KB apart.
|Native planar: four separate bitplanes, as on the Amiga. The display they are
copied to is the text plane, four planes 128 KB apart.
|===
Every public entry point speaks in *colour indices, 0 to 15*. Your code never
@ -135,11 +138,12 @@ documented and flagged in this manual -- see <<_writing_portable_code>>.
=== What You Get
* Surfaces, palettes and per-scanline palette selection
* Surfaces, palettes, and per-scanline palette selection
* Drawing primitives: pixels, lines, rectangles, circles, flood fill
* 8x8 tiles, tile banks, tile maps and bitmap text
* 8x8 tiles, tile banks, tile maps, and bitmap text
* Sprites with colour-0 transparency and *runtime-compiled blitters* -- the
library emits native 65816, 68000 or x86 code for each sprite at run time
library emits native 65816, 68000, or x86 code for each sprite at run time
(on the hosted ports other than Win16 every sprite is interpreted)
* Keyboard (both game-style predicates and a typed-character queue), mouse
and joystick
* Four-channel Protracker `.MOD` music, a portable chip-tracker music format,
@ -155,8 +159,8 @@ documented and flagged in this manual -- see <<_writing_portable_code>>.
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. A program also carries
code from the libraries each port links (libxmp-lite for music on most
ports, the toolchains' C libraries and start files), whose terms
code from the libraries each port links (PTPlayer for music on the Amiga,
the toolchains' C libraries and start files), whose terms
`LICENSES.md` lists with what a shipped program owes them. On the Atari ST
that includes MiNTLib's LGPL parts: a closed-source game lets its users
relink it against a changed MiNTLib (by shipping its object files, for
@ -169,7 +173,7 @@ If you are new to JoeyLib, read <<_getting_started>>, then
<<_anatomy_of_a_joeylib_program>>, then <<_writing_portable_code>>. Those three
chapters are the whole programming model.
<<_creating_assets>> covers getting graphics, music and sound into a form the
<<_creating_assets>> covers getting graphics, music, and sound into a form the
library can load.
<<_building_your_program>> explains how to get your own program, in a

View file

@ -1,7 +1,7 @@
== Writing Portable Code
A JoeyLib program that follows the rules in this chapter compiles and behaves
the same on all five machines, on DVX, under SDL3, in a browser and on Windows. Most of the rules exist because one
the same on all five machines, on DVX, under SDL3, in a browser, and on Windows. Most of the rules exist because one
particular target cannot do something the others can, and the Apple IIgs is
usually that target.
@ -13,7 +13,7 @@ usually that target.
. Compile your sprites before you draw them.
. Keep large objects out of function-local variables on the IIgs.
. Use `jlRandom`, not `rand`, when identical behaviour across ports matters.
. Keep file names short, uppercase-safe and 8.3-compatible.
. Keep file names short, uppercase-safe, and 8.3-compatible.
. Do not mix save-under with clean-buffer erasing in the same screen region.
. Pick one of the audio layers per scene, not two.
@ -21,11 +21,12 @@ The rest of this chapter explains each one.
=== Memory
This is the rule that catches everyone. *The C heap on the IIgs is tiny.* It
lives in bank 0, and a single `malloc` is capped at roughly 32 KB -- often far
less in practice, because the clang libc heap on that port can be a few hundred
bytes. A program that `malloc`s a 32000-byte buffer works on four targets and
fails on the reference one.
This is the rule that catches everyone. *The C heap on the IIgs is tiny.* The
whole heap is one Memory Manager block of 32 KB to 48 KB, and if that block
cannot be had at start-up it is only what the program's link left in its
bank, which can be a few hundred bytes. A program that `malloc`s a
32000-byte buffer works on every other target and can fail on the reference
one.
Use the library allocator, which is backed by each platform's native allocator
(the Memory Manager on the IIgs):
@ -40,7 +41,10 @@ if (buffer == NULL) {
jlFree(buffer);
----
`jlAlloc` does *not* zero the memory it returns.
`jlAlloc` does *not* zero the memory it returns. Never free a `jlAlloc` block
with `free`, or a `malloc` block with `jlFree`: on most ports they are the
same, but on the IIgs a `jlAlloc` block is a Memory Manager handle, and mixing
them corrupts memory.
For bulk copies, `jlMemCopy` and `memcpy` are the same thing: `jlMemCopy` is
`memcpy` with a 32-bit count. On the IIgs both are `MVN` block moves at 7
@ -71,12 +75,11 @@ void renderScene(void) {
=== Timing
Covered in <<_frame_rate_independence>>, and worth repeating because it is the
second most common portability bug: the targets run at 50, 60 and 70 Hz. Scale
by `jlFrameHz()` or work from `jlMillisElapsed()`.
second most common portability bug: the targets run at 50, 55, 60, and 70 Hz.
Scale by `jlFrameHz()` or work from `jlMillisElapsed()`.
`jlFrameCount()` is a 16-bit counter, so you must poll it faster than twice the
frame rate or you will miss edges, and it wraps roughly every 18 minutes at
60 Hz.
`jlFrameCount()` is a 16-bit counter that wraps roughly every 18 minutes at
60 Hz, so compare two readings by subtracting them.
=== Colour and the Palette
@ -94,28 +97,31 @@ have both opaque and masked forms.
Two rules.
*Compile before drawing.* `jlSpriteDraw` never compiles on demand. An
uncompiled sprite runs through the interpreter at roughly 1/40th the speed, and
nothing warns you.
uncompiled sprite runs through the interpreter, two to six times slower
depending on the machine, and nothing warns you.
*Align your backup buffers.* Give any `jlSpriteBackupT` backing array 2-byte
alignment, by declaring it `uint16_t` or with an alignment attribute. An odd
buffer address routes save and restore onto slow interpreted paths on the
planar ports, because the 68000 cannot perform word accesses at odd addresses.
alignment, by declaring it `uint16_t` or with an alignment attribute. On the
Amiga, ST, and X68000 an odd buffer address sends save and restore down a much
slower path, and on the Amiga and ST it crashes the trusted variants.
[source,c]
----
// Right: 2-byte aligned by construction.
static uint16_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4) / 2];
static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)];
// Risky: may land on an odd address.
static uint8_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4)];
static uint8_t gBacking[jlSpriteBackupLength(uint8_t, 4, 4)];
----
Note also that on the planar ports the region a save-under actually captures is
the *16-pixel-aligned window* containing the sprite, not the sprite rectangle.
`jlSpriteSaveUnder` reports the true geometry in the backup structure's
`x`, `width`, `height` and `sizeBytes` fields. It is that recorded window you
must not overwrite before restoring.
Note also that the region a save-under actually captures is often not the
sprite rectangle: it can be the *16-pixel-aligned region* containing the
sprite (the Amiga, ST, and X68000), only the rows holding an opaque pixel (the
IIgs), or only the visible part of a sprite partly off the surface (every
port). `jlSpriteSaveUnder` reports the true geometry in the backup structure's
`x`, `y`, `width`, `height`, and `sizeBytes` fields. It is that recorded region
you must not overwrite before restoring. On the IIgs the buffer must not cross
a 64 KB bank boundary, which a `jlAlloc` block and the C heap never do.
=== Randomness
@ -142,19 +148,24 @@ you:
Pass bare names: `jlDataOpen("levels/title.dat", "rb")`, never
`"DATA/levels/title.dat"`. On DVX the two are not in the current directory
at all (<<_dvx_programs>>), so a program that opens `"DATA/..."` itself finds
nothing there.
at all (<<_dvx_programs>>), so a program that opens `+"DATA/..."+` itself finds
nothing there. DOS, the ST, the Amiga, and the X68000 look in the current
directory, so start a program there from its own folder; the IIgs looks for
`DATA/` under GS/OS prefix 0 but keeps `SAVES/` in the application's folder
(<<_files_and_saves>>).
Keep names portable across the retro filesystems this library targets: *at most
8 characters plus a short extension, letters and digits only*, so they satisfy
both ProDOS and DOS 8.3. Do not rely on case being preserved.
8 characters plus a short extension, letters and digits only, starting with a
letter*, so they satisfy both ProDOS and DOS 8.3. Nothing checks them for you.
Do not rely on case being preserved, and spell a name the same way every
time: on Linux, Haiku, BeOS, and the web, case matters.
A failed save is not fatal -- the calls report failure and your program should
degrade gracefully, because on some volumes `SAVES/` simply is not writable.
=== Audio
JoeyLib gives you three sound layers, and on two platforms they share physical
JoeyLib gives you three sound layers, and on some platforms they share physical
hardware:
[cols="2,5", options="header"]
@ -162,41 +173,48 @@ hardware:
|Layer |What it is
|Module playback
|`jlAudioPlayMod` -- four-channel Protracker `.MOD`, mixed in software on
DOS and ST, hardware-replayed on Amiga and IIgs.
|`jlAudioPlayMod` -- four-channel Protracker `.MOD`, mixed in software by
JoeyLib's module player on DOS, the ST, the X68000, and the hosted ports,
played by DVX's music library on DVX, and replayed by the sound hardware on
the Amiga and IIgs. The module player is the Amiga's replayer, PTPlayer,
translated to C, so those ports play a module as the Amiga does, at a PAL
Amiga's pitch, Protracker 2.3's quirks and all.
|Chip tracker
|`jlMusicPlay` -- a portable event stream driving three tone voices plus
noise, at register-write cost with no CPU mixing.
|Voices and effects
|`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx` and the streaming forms.
|`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and the streaming forms.
|===
[WARNING]
====
On the Atari ST and Amiga, the tone voices *are* the same hardware the module
player drives. While a `.MOD` is playing, `jlAudioVoice` writes fight the
player's register writes and lose. Choose the module layer or the voice and
tracker layer *per scene*, not both at once. (The STe build plays modules and
sound effects through the STe's DMA sound chip instead, so there the voices
keep the YM2149 to themselves -- but a program that also ships the ST build
still has to follow the rule.)
On the Amiga the tone voices are the hardware the module player drives, and
while a `.MOD` is playing, `jlAudioVoice` writes fight the player's register
writes and lose. On the plain ST, digital sound takes the chip channel tone
voice 0 plays on, so voice 0 is silent while a module or effect plays. On the
IIgs the voices share sound effect slots 0 to 2 and the noise slot 3. Choose
the module layer or the voice and tracker layer *per scene*, not both at once.
====
The chip tracker is the portable choice for music. It costs almost nothing on
every port -- unlike `.MOD` playback, which needs real CPU mixing and is
marginal on the ST.
every port -- unlike `.MOD` playback, which needs real CPU mixing where the
sound hardware cannot replay a module. On a plain 8 MHz ST a four-channel
module takes about three quarters of the machine (measured under Hatari: a
third for the interrupt that plays each sample through the YM2149, the rest
the mix), leaving a quarter for the game; the STe's DMA sound needs no such
interrupt.
On the IIgs, `jlAudioVoice` and `jlAudioNoise` require `jlAudioInit` (they ride
the tracker engine's DOC path) and behave as retriggered one-shots of about
80 ms, so a note held longer must be re-asserted roughly once a frame. On DOS,
ST and Amiga those two calls are direct hardware programming and work without
`jlAudioInit`.
A `jlAudioVoice` or `jlAudioNoise` note sounds until the next call for it, as
long as `jlAudioFrameTick` runs every frame: the IIgs plays each note as a
short sample and restarts the held ones from there.
A failed `jlAudioInit` is *not* fatal. `jlInit` still succeeds and every audio
call becomes a silent no-op, so a program need not treat missing sound as an
error.
`jlInit` starts audio (`jlConfigT.noAudio` leaves it off) and `jlShutdown`
stops it. Sound that does not start is *not* fatal: `jlInit` still succeeds,
modules and sound effects become silent no-ops (the tone and noise voices
still play on the Amiga, the ST, DOS, and the X68000), so a program need not
treat missing sound as an error.
=== Per-Platform Behaviour Summary
@ -207,9 +225,9 @@ The API is uniform; these are the places where what you get differs.
|Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |DVX |SDL3, Web, Win32, PSP, Haiku, BeOS |Win16
|Frame rate
|60 Hz
|50 Hz (PAL)
|60 Hz (NTSC)
|60 Hz (50 on a PAL machine)
|50 Hz (PAL; 60 on an NTSC machine without PAL)
|50 or 60 Hz, as the machine is set
|70 Hz
|55 Hz (56 under MAME)
|70 Hz (virtual; the real retrace on the whole screen)
@ -221,7 +239,7 @@ The API is uniform; these are the places where what you get differs.
|planar
|planar, interleaved
|chunky 4bpp
|text-plane planar
|planar (shown through the text plane)
|chunky 4bpp
|chunky 4bpp
|chunky 4bpp
@ -234,11 +252,11 @@ The API is uniform; these are the places where what you get differs.
|compiled
|compiled
|interpreted
|interpreted
|compiled
|Stereo pan
|all three
|left and right
|all three (centred on a free channel each side)
|ignored (mono)
|ignored (mono)
|ignored
@ -246,24 +264,36 @@ The API is uniform; these are the places where what you get differs.
|all three
|ignored (mono)
|`jlAudioTone`
|DOC oscillator
|Paula
|YM2149
|PIT counter 2
|-
|a DVX tone (FM chip or mixer)
|voice 0 of the mixer
|voice 0 of the mixer
|Program icon (<<_icons>>)
|the Finder's (`Icons` folder)
|Workbench's (`Name.info`)
|none
|none
|none
|Program Manager's
|the window's (SDL3), the page's (web), Explorer's (Win32), the menu's
(PSP), Tracker's (Haiku, BeOS)
|Program Manager's
|Tone voices (`jlAudioTone` is voice 0)
|DOC oscillators on sound effect slots 0 to 2
|Paula channels 0 to 2
|YM2149 (voice 0 silent while digital sound plays)
|OPL2 (with none, voice 0 on the PC speaker)
|YM2151 (sine waves)
|DVX tones (FM chip or mixer)
|square waves in the mixer
|square waves in the mixer
|Joystick
|analog, auto-disables
|digital
|one, analog, auto-disables
|digital
|digital
|analog
|digital
|analog (DVX calibrates)
|the OS's gamepads or joysticks (OS calibrates); the PSP's own controls
|SDL's gamepads and joysticks (SDL3, web); Windows' joysticks (Win32); the system's joysticks
(Haiku, BeOS); the PSP's own controls
|Windows' joysticks (Control Panel calibrates)
|Keyboard layout
@ -273,7 +303,7 @@ The API is uniform; these are the places where what you get differs.
|fixed US
|machine's own
|DVX's
|the OS's; the PSP has no keyboard
|the OS's; on the PSP, a US one on the screen
|Windows'
|Serial ports
@ -281,28 +311,35 @@ The API is uniform; these are the places where what you get differs.
|`serial.device`
|Modem port
|COM1..COM4
|-
|RS-232C (needs RSDRV.SYS)
|COM1..COM4 (DVX's library)
|Win32: COM1 and up; the others -
|Win32: COM1 and up; SDL3, web, PSP, Haiku, BeOS: none
|COM1..COM9
|===
The joystick row deserves a note: the IIgs port *auto-disables* joystick
polling after a short window with no stick detected, because polling costs
about 3 ms per frame there. It does not re-probe on its own. If a user plugs a
stick in while the program is running, you must call `jlJoystickReset` to
resume polling.
The joystick row deserves a note: the IIgs port stops polling for a stick
after about a second (60 polls) with none detected, because polling for an
absent stick costs about 1 ms a frame there; `jlJoystickReset` starts it again
(<<_jljoystickreset>>).
On the X68000 both joystick ports always report a stick, as on the ST: an
empty port cannot be told from a stick at rest. `JOY_BUTTON_0` is a stick's
trigger A and `JOY_BUTTON_1` its trigger B. Holding both directions of an axis
empty port cannot be told from a stick at rest. `JL_JOY_BUTTON_0` is a stick's
trigger A and `JL_JOY_BUTTON_1` its trigger B. Holding both directions of an axis
reads as the positive one (right, down), as on the Amiga and ST.
On the ST, `JOYSTICK_0` is port 1, the joystick port, and `JOYSTICK_1` port 0.
On the ST, `JL_JOYSTICK_0` is port 1, the joystick port, and `JL_JOYSTICK_1` port 0.
JoeyLib keeps TOS's keyboard-processor mode, in which port 0 is the mouse's:
the ST reports no stick movement there, so `JOYSTICK_1` stays at rest whatever
is plugged in. Port 1's fire button is the same line as the mouse's right
button, so a press reads as both `JOY_BUTTON_0` and `MOUSE_BUTTON_RIGHT`.
the ST reports no stick movement there, so `JL_JOYSTICK_1` stays at rest whatever
is plugged in. A stick there has one button, so a quick press is
`JL_JOY_BUTTON_0` and a held one `JL_JOY_BUTTON_1` (<<_jljoydown_jljoypressed_jljoyreleased>>).
Port 1's fire button is the same line as the mouse's right button, so a press
reads as both the stick's button and `JL_MOUSE_BUTTON_RIGHT`.
The 1040 STe also has two enhanced joystick ports, and the STe build reads an
Atari Jaguar pad in each. A pad in enhanced port A is `JL_JOYSTICK_0` whenever
it was used more recently than port 1's stick, so either one plays, and a pad
in enhanced port B is `JL_JOYSTICK_1`. A pad's A and B buttons are
`JL_JOY_BUTTON_0` and `JL_JOY_BUTTON_1`. The Mega STe has no enhanced ports.
On the Amiga the sticks are read through `lowlevel.library`, which Kickstart
3.1 does not have in ROM: AmigaOS loads it from `LIBS:`, so the boot drive must
@ -312,12 +349,15 @@ on its own must do the same. `lowlevel.library` reports a stick only once it
has moved, and never a mouse. It also checks each port again now and then, and
a two-button stick with both buttons held can read as nothing at that moment,
so once a port has shown a stick JoeyLib keeps it one; `jlJoystickReset` hands
the port back to `lowlevel.library`'s own detection.
the port back to the library's own detection. `lowlevel.library` reports
a one-button stick and a two-button one alike, so a stick is taken for a
one-button one, a held button being `JL_JOY_BUTTON_1`, until its second button
is first pressed; a CD32 pad has both from the start.
On DOS the game port's counts depend on the stick and on the CPU's speed, so
the first reading after `jlInit` or `jlJoystickReset` is taken as the stick's
rest, and a push past half that value either way reads as a full push. Call
`jlJoystickReset` with the stick centred if it was not at the start.
On DOS the game port's counts depend on the stick and on the CPU's speed, so a
stick's first reading -- after `jlInit`, after `jlJoystickReset`, or when it is
plugged in -- is taken as its centre, and its axes are read in proportion to
that: a count of 0 or twice the centre is a full push.
The X68000's display refreshes at 55.46 Hz. `jlFrameHz` times it when the
program starts and rounds to the nearest whole number, so it reports 55 --
@ -325,18 +365,238 @@ and 56 under MAME, whose X68000 display runs slightly fast (56.07 Hz).
`jlMillisElapsed` does not depend on it: it reads the machine's 100 Hz system
timer to the millisecond.
Per-scanline palettes cost something on the X68000, and only there. Its
display has one 16-colour palette, so the port reloads it as the beam scans: an
interrupt stores the next band's colours in the horizontal blanking before the
band's first line. A screen with one palette costs nothing and arms no
interrupt. Each change of palette between one line and the next costs about
one line of CPU time (32 microseconds); a band more than two lines from the
one before costs about two. A screen that changes palette on every line
therefore spends a third of each frame in that interrupt, during which serial
and mouse interrupts wait (see the serial chapter for how long a run can be
before received bytes are lost). Palette and SCB changes take effect at the
start of the next frame rather than mid-frame, so a fade can lag its pixels by
one frame but never tears.
Per-scanline palettes cost CPU time on the ST and the X68000, whose displays
have one 16-colour palette, and a stall on the Amiga when the bands move. The
ST takes a timer interrupt at every change of palette, every frame, and copies
the next band's 16 colours there. The X68000 reloads its palette as the beam
scans: an interrupt stores the next band's colours in the horizontal blanking
before the band's first line. A screen with one palette costs nothing on
either and arms no interrupt. On the X68000 each change of palette between one
line and the next costs about one line of CPU time (32 microseconds); a band
more than two lines from the one before costs about two. A screen that changes
palette on every line therefore spends a third of each frame in that
interrupt, during which serial and mouse interrupts wait (see the serial
chapter for how long a run can be before received bytes are lost). On the
Amiga the copper changes the palettes at no CPU cost, but an SCB change that
moves where a band starts or ends rebuilds the copper list, which holds up
that `jlStagePresent` for about three frames on an A500; a change of colours,
or of which palette an unmoved band uses, costs nothing. On the ST a
present's colours and pixels show from the same frame: it hands the
interrupts their new colours with the page it flips to. On the X68000, a
screen of two or more bands takes palette and SCB changes at the start of the
next frame rather than mid-frame, so a fade can lag its pixels by one frame
but never tears; a screen of one palette has its colours written at the
present.
=== Where the Ports Still Differ
The API is the same on every port, and where a port behaved differently from
the rest because of the library, the library has been changed. What is left is
set by the machines and by the systems the ports run on, and stays. Each entry
names the ports, says how they differ, and gives the reason.
[[portsDifferDisplay]]
==== Display
* *IIgs: a big present can tear.* The Super Hi-Res display reads one fixed
buffer, with no second page to flip to, and a full-screen present takes
several frames, so the screen can show part of the old picture and part of
the new.
* *Win16, and DVX in a window: no vertical blank.* Windows 3.1 has no call that
waits for the display's retrace, and DVX syncs to it only on its full screen,
so `jlWaitVBL` keeps a virtual frame clock there and nothing stops a present
from tearing.
* *X68000: a small, wide picture.* The machine has no 320x200 mode, so the stage
sits in the middle of a 512x512 screen, a picture about 2.1 times as wide as
it is tall. That screen is a 31 kHz mode, so the monitor must take 31 kHz.
* *ST: eight levels a channel.* The plain ST's video chip has 3 bits per colour
channel, so `$0RGB` values one apart can look the same. The STe build shows
all 4 bits.
* *X68000: one intensity bit for three channels.* Its colours have 5 bits per
channel and an intensity bit the three share. JoeyLib sets that bit for every
colour but black, so white is full white, and a channel that is 0 in any
other colour shows at 1/63 rather than 0: `$F00` carries a trace of green and
blue.
* *Windows and DVX on a 256-colour display.* The display has one palette for
everything on it. Windows keeps 20 of its entries for itself, and DVX shares
it among all its programs, so Win32 and a DVX window show each colour as the
nearest one the display has, and Win16 shows them exactly only while the 236
entries left are enough (<<_windows_programs>>).
* *Amiga and X68000: colours a frame off their pixels.* A present's new
colours and new pixels show from the same frame on the other ports (the IIgs
and DOS change both at once, and can tear). The Amiga's pixels change at the
next frame, when the copper loads the new bitplane pointers, but the bands
below the beam take their new colours in the frame the present runs: the
copper list is graphics.library's, one per screen, and JoeyLib patches its
colours in place. The X68000 draws straight into the displayed text plane,
which has no second page, while a screen of two or more bands takes its new
colours at the start of the next frame, so there the colours come a frame
after the pixels.
* *ST and X68000: per-scanline palettes cost time.* Both displays have one
16-colour palette at a time, so the CPU loads each band's colours as the beam
reaches it: the ST in a timer interrupt at every change of palette, every
frame, and the X68000 in a raster interrupt. A screen with one palette costs
nothing (<<_per_platform_behaviour_summary>>).
[[portsDifferInput]]
==== Input
* *IIgs keyboard: one key at a time, by character.* The keyboard reports only
the last character typed and whether any key is down. So JoeyLib sees one key
besides the modifiers at a time, knows a key by the character it types rather
than by its place on the keyboard, and reads it released only when every key
is up. With Ctrl held, Ctrl+H, I, J, K, M, and U read as those letters, not as
the left arrow, Tab, the down and up arrows, Return, and the right arrow,
whose codes they share; a shifted digit key reads as its digit only on
the US layout; and the keypad's keys read as the characters they type, so
its 8, 2, 4, and 6 are the digits, not the arrows as on the other ports.
* *IIgs keyboard: the keys it lacks.* It has no F1 to F10, no forward Delete
(Delete is the backspace key), and one Shift as far as JoeyLib can tell:
`JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. Option is
`JL_KEY_LALT`.
* *ST keyboard: the Alt layer on TOS before 4.00.* A national keyboard types
some ASCII with Alt (a German one's @ [ ] { } \). TOS 4.00 and later, and
EmuTOS, keep that layer in the keyboard tables JoeyLib reads, so Alt types
it; older TOS keeps it in the keyboard code JoeyLib replaces, so there Alt
with a key types nothing, as on the ports where Alt is a command key.
* *X68000 keyboard: one Shift.* Both Shift keys send the same code, so
`JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. OPT.1 and OPT.2 are
`JL_KEY_LALT`.
* *IIgs joystick: one stick, sharing two keys.* The game port takes one stick,
and its buttons are wired to the same lines as the Command and Option keys,
so holding either key reads as a button.
* *Mouse buttons.* The IIgs mouse has one button, and the ST and X68000 mice
have two; the other ports report three where the mouse has them.
* *ST and X68000: a stick is always connected.* An empty joystick port reads
exactly as a stick at rest, so `jlJoystickConnected` is true for both.
* *ST: the mouse port's stick.* The keyboard processor reports port 0 as the
mouse in the mode JoeyLib keeps it in, so `JL_JOYSTICK_1` never moves, and
port 1's fire button is the mouse's right button too.
* *Amiga: one button or two.* `lowlevel.library` reports a one-button and a
two-button stick alike, so a stick counts as a one-button one until its
second button is first pressed.
* *PSP: no mouse, one key at a time.* The PSP has no mouse, and its on-screen
keyboard has one cursor, so it presses one key at a time (Shift, Ctrl, and
Alt stay down until the next key is let go).
[[portsDifferAudio]]
==== Audio
* *Plain ST: voice 0 is silent while digital sound plays.* The ST has no
digital sound chip, so JoeyLib plays modules and effects by changing the
sound chip's channel A volume 12288 times a second, and channel A is tone
voice 0's. Voice 0 sounds again when the digital sound stops, and voices 1
and 2 play on. The STe build uses the STe's DMA sound and keeps all three.
* *ST: noise shares voice 2's volume.* The YM2149 has one volume per channel,
and its noise is mixed into channel C, voice 2's. Author noise hits and
voice-2 notes apart; `songbake` warns when they overlap.
* *ST: volume steps.* The YM2149 has 16 volume levels about 3.5 dB apart, so a
2 dB attenuation step is matched to within 1.6 dB.
* *Amiga: four channels for everything.* Paula has four sound channels, two
on each side, shared by the module, the effects, the three tone voices, and
the noise. At most four sounds play at once, an effect can take a channel a
voice was using, and a module plays on all four. A channel plays on one side
only, so a centred effect needs a free channel on each side, and without
them it plays on one channel, from one side; and the tone voices, one
channel each, are heard from one side: voice 0 (and `jlAudioTone`) on the
left, voices 1 and 2 on the right.
* *IIgs: voices on the effect slots.* By design the IIgs plays tone voices 0 to
2 on sound effect slots 0 to 2 and the noise on slot 3, so an effect on one
of those slots and a voice cut each other off. Use slot 4 for effects while
voices or `jlMusicPlay` sound.
* *IIgs: mono.* A stock IIgs mixes its stereo channels into one speaker, so
panning is heard only with a stereo card.
* *Plain ST and X68000: no panning.* Each has one output for digital sound:
the ST its channel A, the X68000 its one ADPCM channel, so `jlAudioSetPan`
is ignored.
* *DOS: the noise is a drum.* The OPL2 has no free-running noise generator; its
rhythm-mode snare is the nearest, a tuned drum whose pitch cannot follow the
YM2149's noise rate.
* *DOS and X68000: tones are not square.* The OPL2 and the YM2151 make their
waves from sines and have no square wave, so their tone voices (DOS's when it
has an OPL2) are not the square waves the other ports play.
* *Amiga and IIgs: the brightest noise.* Paula plays a sample no faster than
28.6 kHz and the IIgs's DOC no faster than 26 kHz, so noise pitches 0 to 4
sound alike there.
* *Where tick callbacks fall.* The calls average exactly `hz` a second on every
port. The ST and X68000 deliver them from the system's own timer, at 200 and
100 Hz, so the calls due between two of its interrupts come together. DVX
delivers them at `jlAudioFrameTick`, because its sound library may not be
called from an interrupt, and so does the IIgs, counting them against the
display's beats.
* *DOS, the ST, and the X68000 mix in program time.* The module player and
the SFX overlay run too long for an interrupt, so their sound buffers are
refilled at `jlAudioFrameTick`: a program that stops calling it for longer
than the buffer lasts stops or repeats its sound.
* *Win16 with a Sound Blaster 2.0: 8-bit sound.* Its driver, SNDBLST2.DRV, takes
only 8-bit samples, so the quietest attenuation steps are coarser there.
[[portsDifferSerial]]
==== Serial
* *Top speeds.* The ST runs at 19200 baud at most (its MFP's rate table), a
IIgs Super Serial Card at 19200 (the 6551's), the X68000 at 38400 (RSDRV.SYS's
table; really 39062), and Win16 with Windows' own COMM.DRV at 57600.
* *One port.* The base ST has one RS-232 port, and so has the X68000, whose
second serial channel is the mouse's.
* *IIgs modem and printer ports: no RTS.* Their mini-DIN-8 connector's one
handshake output is DTR, so under RTS/CTS nothing can tell the peer to
pause: receive needs XON/XOFF or a protocol that paces itself. The printer
port's channel is also AppleTalk's.
* *IIgs Super Serial Card: lines tied together.* The 6551's DTR is also its
receiver's enable, and dropping its RTS stops its transmitter.
* *X68000: DTR rises at every line setup.* The ROM's and RSDRV.SYS's line setup
raise DTR whenever it runs.
* *Windows: DTR drops at close.* Win32's driver, and Win16 with Windows' own
COMM.DRV, drop DTR when the port is closed, and neither has a call that
prevents it.
* *IIgs: disk access stops interrupts.* GS/OS holds interrupts off for 65 to
220 ms at a time while it reads or writes a 3.5-inch floppy, which can lose
received bytes (<<_what_can_lose_received_bytes>>).
[[portsDifferFiles]]
==== Files and Saves
* *Name rules.* ProDOS names on the IIgs are at most 15 characters and start
with a letter; DOS, the ST, and Win16 use 8.3 names; and Linux, Haiku, BeOS,
and the web tell upper and lower case apart. Keep to the portable rule
(<<_files>>).
* *Pre-compiled sprites per processor.* A `.spc` holds machine code, so it is
baked per port: DVX and Win16 run DOS's x86 code, the STe the ST's, and the
ports that interpret sprites use only its pixels.
* *Web saves are the browser's.* A browser may clear a site's stored data to
make room, and a private window keeps nothing once it closes.
[[portsDifferMemory]]
==== Sprites and Memory
* *IIgs: code and copies stay inside 64 KB banks.* The 65816 runs code within
one 64 KB bank and its block copy stays within one, so each sprite's code
must fit in one bank of the codegen arena, the arena is at most four banks,
compacting never merges free space across banks, and a save-under buffer
must not cross a bank.
* *Code size differs by processor.* The 68000 and x86 emitters make code four
to eight times larger than the 65816 one, so with the same `codegenBytes` a
large sprite can compile on the IIgs and fall back to the interpreter
elsewhere.
* *IIgs: a small C heap, and `jlFree` apart from `free`.* The C library's heap
is one Memory Manager block inside one bank, 32 KB to 48 KB, and a `jlAlloc`
block is a Memory Manager handle, so `jlFree` and `free` must never be given
each other's blocks.
[[portsDifferTiming]]
==== Timing
* *Frame rates.* The displays run at 50, 55, 60, and 70 Hz, as the table above
shows; the hosted ports and DVX keep a virtual 60 and 70 Hz.
* *IIgs: the clock stops while interrupts are off.* The IIgs has no free-running
timer JoeyLib can read, so its clock counts the display's vertical blanks,
and blanks that come while interrupts are held off are lost: a long stretch
with interrupts off, a floppy access for one, leaves the clock behind.
* *DOS: frames counted by the clock.* VGA has no dependable retrace interrupt,
so `jlFrameCount` counts 70ths of a second of the millisecond clock while
`jlWaitVBL` waits for the real retrace (70.086 Hz), and the two slowly drift
apart.
=== A Portability Checklist
@ -365,7 +625,7 @@ audience (or both):
* *The ST build* (`make atarist`, programs in `build/atarist/bin`) is the
baseline. It is pure 68000 and *never uses STe hardware*: it runs on every
ST -- STF, STFM, Mega ST, STe and Mega STe alike -- exactly as on a plain
ST -- STF, STFM, Mega ST, STe, and Mega STe alike -- exactly as on a plain
ST, at no cost for the STe's existence.
* *The STe build* (`make ste`, programs in `build/ste/bin`) is the same port
compiled with `JOEYLIB_ATARI_STE`. It uses the STe's hardware
@ -406,7 +666,7 @@ Nothing in the API differs between them:
The pixels are identical: UBER's 51 surface hashes match the ST's golden for
the STe build on an STe and a Mega STe, and the gates check that under Hatari.
`./joey-run` gives an ST-build program a plain ST and an STe-build program an
STe; `--machine st`, `megast`, `ste` or `megaste` picks another. A project
STe; `--machine st`, `megast`, `ste`, or `megaste` picks another. A project
builds the STe variant with `make ste`, like any other port; it takes the ST's
baked assets.
@ -420,7 +680,7 @@ expect less on memory-bound operations there.
=== DVX Programs
DVX is a DOS desktop: a VESA graphics GUI with windows, cooperative
multitasking and its own libraries for sound, music, joysticks and serial
multitasking, and its own libraries for sound, music, joysticks, and serial
ports. On DVX a JoeyLib program is one of its programs, a `.app` module
running in a window of its own beside the others. The graphics are the
MS-DOS port's -- the same chunky stage and the same x86 code, so UBER's
@ -430,7 +690,7 @@ hashes are DOS's -- and the rest behaves like this:
at the largest whole scale, up to 3x, that fits the desktop: 1x at
640x480, 2x at 800x600, 3x at 1024x768. The stage reaches the desktop
through one colour table per palette, so SCB palettes are exact on 15-,
16- and 32-bit desktops. On a 256-colour desktop every colour is the
16-, and 32-bit desktops. On a 256-colour desktop every colour is the
nearest one in DVX's palette, which all programs share: gradients band.
* *Full screen.* Game > Full Screen, or Alt+Enter, puts the program on the
whole screen in DVX's full-screen mode, 320x200 in 256 colours (VGA mode
@ -453,18 +713,18 @@ hashes are DOS's -- and the rest behaves like this:
does not tear. The frame count is the same clock in both, so switching
never makes it jump.
* *Keys DVX keeps.* While the window is active, or the program has the whole
screen, every key goes to the program, F1, F10 and Alt included, except
screen, every key goes to the program, F1, F10, and Alt included, except
Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close the
window), and Alt+Enter, which the port keeps for full screen. The window's
Game menu has Full Screen, Help (the program's help file) and Exit. Typed
characters come through DVX's keyboard layout.
* *Closing ends the program.* Alt+F4, the close box and Game > Exit end the
program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent` or
* *Closing ends the program.* Alt+F4, the close box, and Game > Exit end the
program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent`, or
`jlSerialPoll`, as `exit()` does anywhere: its `atexit` handlers run, then
`jlShutdown`, and its log is closed.
* *Sharing the machine.* DVX multitasks cooperatively: the other programs,
and this one's own window events, run only while it yields. `jlWaitVBL`
yields, and so do `jlInputPoll`, `jlStagePresent` and `jlSerialPoll`, at
yields, and so do `jlInputPoll`, `jlStagePresent`, and `jlSerialPoll`, at
most once a frame. A loop that calls none of them freezes the desktop until
it ends.
* *Files.* DVX's programs share one current directory, so JoeyLib roots its
@ -518,32 +778,36 @@ this:
program presents.
* *Sprites are interpreted.* There is no code emitter for the host's CPU, so
`jlSpriteCompile` returns false and every sprite is drawn by the
interpreter -- far faster than any of the old machines all the same.
`joey/platform.h` defines `JOEYLIB_SPRITE_INTERPRETED`, for a program that
interpreter -- far faster than any of the old machines all the same. (The
Win16 port, below, compiles them as DOS does.)
`joey/platform.h` defines `JL_SPRITE_INTERPRETED`, for a program that
checks how many of its sprites compiled. A pre-compiled bank (`.spc`)
loads its cels' pixels and leaves their code: the DOS bake is used.
* *Input.* Keys by where they are on the keyboard, so another layout does not
move the arrow keys; typed characters through the OS's keyboard layout. The
mouse is the pointer over the stage, in stage pixels (the OS pointer is
hidden over the window: JoeyLib programs draw their own). The first two
gamepads are joysticks 0 and 1: the left stick or the d-pad, the south
button (A on an Xbox pad) as `JOY_BUTTON_0` and the east button (B) as
`JOY_BUTTON_1`; they come and go as they are plugged in.
gamepads or joysticks are joysticks 0 and 1, coming and going as they are
plugged in. A gamepad SDL knows how to map gives its left stick or its
d-pad, its south button (A on an Xbox pad) as `JL_JOY_BUTTON_0`, and its
east button (B) as `JL_JOY_BUTTON_1`; any other joystick its first two axes
or its first hat, and its first two buttons.
* *Closing ends the program.* Closing the window ends the program at its next
`jlWaitVBL`, `jlInputPoll` or `jlStagePresent`, as `exit()` does anywhere:
`jlWaitVBL`, `jlInputPoll`, or `jlStagePresent`, as `exit()` does anywhere:
its `atexit` handlers run and the window and the sound are closed.
* *Files.* The program's own folder is the current directory from `jlInit`
on, as on the machine ports: `DATA/`, `SAVES/` and `joeylog.txt` are beside
on, as on the machine ports: `DATA/`, `SAVES/`, and `joeylog.txt` are beside
the program.
* *Sound* is mixed in software into 16-bit stereo at 44.1 kHz: `.MOD` music
through libxmp-lite (as on DOS), the SFX slots in stereo, the three tone
voices as square waves and the noise voice as the ST's YM2149 makes it. It
through the module player (as on DOS), Paula's channels hard left and
right as on the Amiga, the SFX slots in stereo, the three tone
voices as square waves, and the noise voice as the ST's YM2149 makes it. It
is mixed in program time by `jlAudioFrameTick`, `jlWaitVBL`,
`jlInputPoll` and `jlStagePresent`, a few frames ahead, so a
`jlInputPoll`, and `jlStagePresent`, a few frames ahead, so a
`jlAudioTickRegister` callback runs in program time too -- but between
exactly the samples it falls between, so its notes start in the right
place in the sound. `jlAudioInit` is needed for every sound, the tone
voices included.
place in the sound. Every sound needs audio started (the default), the
tone voices included.
* *No serial port.*
`make sdl3` builds every example as a native program, each in
@ -571,9 +835,12 @@ are DOS's here too. Everything in <<_sdl3_programs>> holds, except:
`jlInputPoll` alone, as `jlWaitForAnyKey` is).
* *Sound starts with the first click or key press*, as browsers require; the
page says so.
* *Saves persist.* `SAVES/` is kept in the browser's IndexedDB for the page's
address, read back when the program starts and written back every two
seconds, so a save outlives the page.
* *Saves persist.* `SAVES/` is kept in the browser's IndexedDB, read back
when the program starts and written back as each save changes, so a save
outlives the page however it ends. The store is the site's, not the page's:
every JoeyLib program served from the same site shares one `SAVES/`, so give
each game's saves names of their own. `jlDiskFree` there is the browser's
own estimate of what the site may still store.
* *No full screen by default and no closing.* Alt+Enter asks the browser for
the whole screen; the program ends when the page is closed.
@ -591,22 +858,31 @@ is the same chunky one, so UBER's hashes are DOS's. Everything in
* *A folder for `PSP/GAME`.* A program is a folder, `build/psp/bin/NAME/`:
`EBOOT.PBP`, titled with the program's name (or its description's `TITLE`)
in the PSP's menu, and its `DATA/`. Copy the folder into `PSP/GAME/` on the
memory stick. Its folder is its current directory, so `DATA/`, `SAVES/`
memory stick. Its folder is its current directory, so `DATA/`, `SAVES/`,
and `joeylog.txt` are there. It runs on a PSP with custom firmware (it is
not signed) and in PPSSPP. The ELF it is packed from stays in
`build/psp/elf/`, for `psp-addr2line`.
* *The screen.* The PSP's 480x272 screen shows the stage at 1:1 in its
middle, black around it. The scale and aspect settings do not apply (a PSP
has no environment), and there is no full screen to switch to.
* *Its own controls, no keyboard.* The PSP's controls are joystick 0,
through SDL's gamepad mapping: the d-pad or the analog nub, cross as
`JOY_BUTTON_0` and circle as `JOY_BUTTON_1`; joystick 1 is never there.
Having no keyboard, the PSP also gives Start as the Return key and Select
as the Escape key, held (`KEY_RETURN`, `KEY_ESCAPE`) and typed
(`JL_CHAR_RETURN`, `JL_CHAR_ESCAPE` from `jlInputGetChar`) as those keys
are, so `jlWaitForAnyKey`, Return to go on and Escape to quit work. No
other key is ever down, nothing else is typed (the PSP's on-screen
keyboard is never opened), and there is no pointer to move.
* *Its own controls, and a keyboard on the screen.* The PSP's controls are
joystick 0, through SDL's gamepad mapping: the d-pad or the analog nub,
cross as `JL_JOY_BUTTON_0` and circle as `JL_JOY_BUTTON_1`; joystick 1 is
never there. Start is also the Return key and Select the Escape key, held
(`JL_KEY_RETURN`, `JL_KEY_ESCAPE`) and typed (`JL_CHAR_RETURN`,
`JL_CHAR_ESCAPE` from `jlInputGetChar`) as those keys are, so
`jlWaitForAnyKey`, Return to go on, and Escape to quit work at a press.
Square opens a PC keyboard drawn over the bottom of the screen, US layout,
for every other key: the d-pad moves its cursor, cross presses the key
under it (held down as long as cross is), L and R move the keyboard to
the top of the screen and back, out of the way of what the program shows
there, and Square closes it. Shift, Ctrl, and Alt stay down until the
next other key is let go (or until pressed again), and Caps turns capital
letters on and off. Its keys go down and up and type as a desktop's do,
so `jlKeyDown`, `jlKeyPressed`, and `jlInputGetChar` see the same as they
would there, F1 to F10, the arrows, and Ctrl and Alt chords included.
While it is open, joystick 0 rests: the d-pad and cross are the
keyboard's. There is no pointer to move.
* *HOME quits.* The HOME menu's Exit ends the program as closing a window
does.
* *Free space* (`jlDiskFree`) is that of the memory stick (or a PSP Go's
@ -630,26 +906,28 @@ window to play in here: to play, copy the folder to a PSP, or open its
The Win32 port is the hosted core with a backend of its own, written to the
Win32 API (`src/hosted/win32`), for the Windows SDL3 does not reach --
Windows 95, 98, ME, NT 4 and 2000 -- and every Windows since. Its stage is
Windows 95, 98, ME, NT 4, and 2000 -- and every Windows since. Its stage is
the same chunky one, so UBER's hashes are DOS's. Everything in
<<_sdl3_programs>> holds, except:
* *Old Windows on purpose.* A program is a 32-bit `NAME.EXE` built by
llvm-mingw against `MSVCRT.DLL` (the `win32` installer component), marked
for Windows 4.0 so 95 and NT 4 will load it, linked statically, and
calling only what USER, GDI, WINMM and KERNEL32 had in 1995. `MSVCRT.DLL`
calling only what USER, GDI, WINMM, and KERNEL32 had in 1995. `MSVCRT.DLL`
came with Windows 95 OSR2 and Internet Explorer; a first-release Windows
95 needs it installed.
* *One thread.* The window is made on the program's thread, so its messages
arrive there and the port dispatches them at `jlInputPoll`, `jlWaitVBL`
arrive there and the port dispatches them at `jlInputPoll`, `jlWaitVBL`,
and `jlStagePresent`; the sound goes out through waveOut buffers handed
over at the same calls. There are no callbacks and no locks.
* *The window.* The stage is drawn with `StretchDIBits` at the stage's shape,
4:3 or square (the same `JOEYLIB_SCALE`, `JOEYLIB_ASPECT` and
* *The window.* The stage is stretched with `StretchBlt` to the stage's
shape, 4:3 or square (the same `JOEYLIB_SCALE`, `JOEYLIB_ASPECT`, and
`JOEYLIB_FULLSCREEN` settings), black around it; full screen is a
borderless window over the whole monitor, which leaves the display mode
alone. On a 256-colour desktop Windows maps every colour to the nearest
one it has.
one it has. Each frame draws only what changed since the last: a game
that moves a few sprites pays for their rectangles, not for the whole
window, which on a Windows 95 machine is most of a frame's time.
* *Keys* by the PC scan code every key message carries (the same table as
DOS), typed characters through the user's keyboard layout, and the first
two joysticks through `joyGetPosEx`, calibrated in the Control Panel. Keys
@ -659,8 +937,9 @@ the same chunky one, so UBER's hashes are DOS's. Everything in
`make win32` builds every example as a Windows program in `build/win32/bin/`,
and `./joey-run --port win32 NAME` runs one under Wine (the `wine` installer
component, a portable Wine kept in `toolchains/`); the gates run there too.
None of it has run on a real Windows 9x or NT 4 here.
component, a portable Wine kept in `toolchains/`); the gates run there too,
`scripts/verify-win32-display.sh` among them, though that checks Wine's GDI
rather than Windows'. None of it has run on a real Windows 9x or NT 4 here.
The Win16 port is the hosted core with a backend written to the Windows 3.1
API (`src/hosted/win16`), for Windows 3.1 and Windows for Workgroups 3.11 on
@ -672,16 +951,54 @@ scan code, DTR dropped at close -- except:
with Open Watcom's Win386 target (the `win16` installer component): flat
32-bit code bound to Watcom's supervisor, which turns each call into the
16-bit one Windows takes, so it needs Windows in 386 enhanced mode. It
calls only what Windows 3.1 has: USER, GDI, KERNEL and MMSYSTEM.
* *Exact colours.* The frame is 8-bit, a DIB whose 256-entry colour table
holds all 16 palettes and is realized as a logical palette whenever a frame
is shown, so on a 256-colour display every colour is exact, SCB palettes
included. `JOEYLIB_SCALE` defaults to 2, and the settings are read from the
DOS environment Windows started from (`joey-run` puts its own there).
Under DOSBox, Alt+Enter is DOSBox's own full-screen key; use Alt with the
calls only what Windows 3.1 has: USER, GDI, KERNEL, and MMSYSTEM.
* *Exact colours.* The frame is 8-bit, each pixel one entry of a 256-entry
colour table holding all 16 palettes, which on a 256-colour display is a
logical palette too, so every colour is exact, SCB palettes included, as
far as the display has room: Windows keeps 20 of a 256-colour display's
entries for itself, so with more than 14 palettes in use and more than 236
different colours on screen, some show as the nearest it has.
`JOEYLIB_SCALE` defaults to 2, and the settings are read from the DOS
environment Windows started from (`joey-run` puts its own there). Under
DOSBox, Alt+Enter is DOSBox's own full-screen key; use Alt with the
keypad's Enter.
* *Sound* is 8-bit mono at 22.05 kHz through waveOut, what a Sound Blaster
and a 386 handle well; stereo pan is ignored.
* *Speed on a 386.* Sprites are compiled to x86 code, as on DOS. The port
expands the stage into the window's pixels itself, a presented row at a
time and scaled as it goes, and hands Windows only the rectangles that
changed, those close enough together as one. With Microsoft's WinG 1.0 on
the machine (its runtime in Windows' `SYSTEM` folder) the rows go straight
into a WinG bitmap the size of the window, and drawing a rectangle is a
plain copy to the screen; without it, each rectangle goes through a device
bitmap on a 256-colour display, and only another kind of display pays for
Windows matching every pixel's colour. `JOEYLIB_WING=0` leaves WinG alone.
JoeyLib does not ship WinG: a game that wants it on its players' machines
ships the runtime under Microsoft's terms (see `LICENSES.md`). While the
SCBs use 14 palettes or fewer, the frame's colours are the screen's own (an
identity palette, as WinG recommends), so a palette change, colour cycling
included, reaches the screen at once with nothing redrawn; more palettes
than that, or a display without a palette, redraw the window when colours
change. On a 386SX-16 (DOSBox's model of one), a game loop presenting
every frame runs at, in frames a second, with an Apple IIgs (under MAME)
for scale:
+
[cols="3,1,1,1,1", options="header"]
|===
| |2x with WinG |2x without |1:1 |IIgs
|Nothing moving, or colour cycling |60 |60 |60 |60
|One 16x16 sprite moving |60 |60 |60 |60
|Eight sprites moving |26 |20 |30 |20
|Eight sprites in a column |30 |20 |30 |20
|The whole screen changing |6 |4 |10 |8.5
|===
+
The 2x window has nearly five times the IIgs's pixels to fill (640x480 for
its 320x200): a 386 keeps up with that for sprites, but not for a whole new
screen every frame, so a game that redraws everything every frame wants
`JOEYLIB_SCALE=1` there.
* *Sound* is 16-bit mono at 22.05 kHz through waveOut, what a Sound Blaster
and a 386 handle well, or 8-bit on a card whose driver has no 16-bit (a
Sound Blaster 2.0, whose SNDBLST2.DRV is 8-bit only, which makes the quiet
attenuation steps coarser); stereo pan is ignored.
* *Joysticks* are the first two MMSYSTEM sticks, through `joyGetPos`, with
their buttons 1 and 2. Windows 3.x reads sticks only through a joystick
driver, which Windows 3.1 itself does not have; without one no stick is
@ -718,15 +1035,21 @@ which the installer puts beside the driver in `toolchains/opt/`.
`make win16` builds every example as a Windows 3.x program in
`build/win16/bin/`, and `./joey-run --port win16 NAME` runs one on Windows
for Workgroups 3.11 in DOSBox Staging (the `win311` installer component, set
up from your own install disc); the gates run there too. None of it has run
on a real 386 here.
up from your own install disc); the gates run there too, among them
`scripts/verify-win16-display.sh`, which checks every pixel the window shows
against the stage after every present, with WinG and without (the optional
`wing` installer component: `./install.sh import WING10.EXE`, Microsoft's
WinG 1.0 SDK, then `./install.sh install wing`; the gates then install its
runtime in their Windows). None of it has run on a real 386
here; the speeds above are DOSBox's 386SX-16 model's, and the IIgs's are
MAME's.
=== Haiku and BeOS Programs
The Be API ports are the hosted core with a backend written to the Be API
(`src/hosted/be`, C++), which Haiku reimplements, kept to the calls BeOS R5
already had. One source builds three ports: `haiku` (Haiku x86_64, gcc 13),
`haiku32` (Haiku x86, gcc 2.95, the BeOS C++ ABI) and `beos` (BeOS R5 x86,
`haiku32` (Haiku x86, gcc 2.95, the BeOS C++ ABI), and `beos` (BeOS R5 x86,
built with the same gcc 2.95 against R5's own headers and libraries, which
the `beosR5` installer component copies off your BeOS 5 Professional CD).
Their stage is the same chunky one, so UBER's hashes are DOS's. Everything in
@ -735,25 +1058,32 @@ Their stage is the same chunky one, so UBER's hashes are DOS's. Everything in
* *Threads.* The program's `main` keeps its own thread; the application and
the window run on threads of their own, as every Be program's do. Key and
mouse messages reach the program as events taken at `jlInputPoll`,
`jlWaitVBL` and `jlStagePresent`, and the sound comes from a
`jlWaitVBL`, and `jlStagePresent`, and the sound comes from a
`BSoundPlayer`.
* *The window* shows the stage from a bitmap the app_server scales without
filtering, at the stage's shape, 4:3 or square, black around it (the same
`JOEYLIB_SCALE`, `JOEYLIB_ASPECT` and `JOEYLIB_FULLSCREEN` settings;
`JOEYLIB_SCALE` defaults to 3). Alt+Enter switches between the window and
* *The window* shows the stage scaled without filtering, at the stage's
shape, 4:3 or square, black around it (the same `JOEYLIB_SCALE`,
`JOEYLIB_ASPECT`, and `JOEYLIB_FULLSCREEN` settings; `JOEYLIB_SCALE`
defaults to 3). The port scales the frame itself and draws only what
changed since the last frame. Alt+Enter switches between the window and
the whole screen. The pointer is hidden over the window.
* *Keys* by raw key code, the key's place on the keyboard whatever the
keymap; typed characters are the keymap's, 7-bit ASCII only. The first
two joysticks (`BJoystick`) are `JOYSTICK_0` and `JOYSTICK_1`, their first
two joysticks (`BJoystick`) are `JL_JOYSTICK_0` and `JL_JOYSTICK_1`, their first
two axes and buttons.
* *Files.* The program's folder is the current directory from the moment
the window opens.
* *Its icon* is in its resources (<<_icons>>), but Tracker shows a file by
the icons in its attributes, which a program built on another machine does
not have. When it opens its window without them, the program copies them
across from its resources (as `mimeset -F` does), so Tracker shows it by
its icon once it has run; that is the one time a program writes to its own
file.
* *No serial port.*
* *BeOS R5's loader* refuses any program with a dynamic tag it does not
know, so `make beos` links without combined relocations (`-z
nocombreloc`). Build for R5 with `make beos`, not `make haiku32`.
`make haiku`, `make haiku32` and `make beos` build every example into
`make haiku`, `make haiku32`, and `make beos` build every example into
`build/<port>/bin/`, and `./joey-run --port <port> NAME` runs one in that
port's VM under QEMU: Haiku R1/beta6 (the `haikuVm` and `haikuVm32` installer
components) or BeOS R5 (`beosVm`, made from your CD), booted from a disk it
@ -761,5 +1091,6 @@ never writes to, with the program's folder on a second disk. R5 drives none
of QEMU's video cards, so its VM is taken through the boot loader's menu to
the fail-safe (VESA) mode, 1024x768 in 16-bit colour; it has no sound card.
The gates run in the same VMs (`scripts/verify-be-input.sh`,
`scripts/bench-be.sh`). None of it has run on a real BeOS or Haiku machine
here.
`scripts/verify-be-display.sh`, `scripts/verify-be-icon.sh`, and
`scripts/bench-be.sh`). None of it has
run on a real BeOS or Haiku machine here.

View file

@ -12,6 +12,7 @@ copy.
jlConfigT config;
config.codegenBytes = 32UL * 1024; // sprite blitter cache
config.noAudio = false; // jlInit starts audio too
if (!jlInit(&config)) {
// jlLastError() explains why. There is no display yet, so on a
@ -24,22 +25,27 @@ if (!jlInit(&config)) {
native sprite blitter code into. Getting it wrong is the most common cause of
sprites drawing slowly, so it is worth understanding.
* `0` selects a per-platform default: 8 KB on the IIgs, 32 KB on
Amiga, ST, DOS and DVX. SDL3 interprets every sprite and never uses it.
* `0` selects a per-platform default: 8 KB on the IIgs, 64 KB on the
Amiga and ST, and 32 KB on the X68000, DOS, DVX, and Win16. The other
hosted ports interpret every sprite and never use it, though they still
allocate it.
* The 68000 and x86 emitters produce code roughly four to eight times less
dense than the 65816 one, so the same sprite needs far more arena space on
those ports. A 32x32 sprite can need 8 to 16 KB there.
* Measure, do not guess: after compiling a sprite, `jlSpriteCompiledSize()`
reports its exact cost on the current platform.
* On the IIgs the arena is one Memory Manager block that may not cross a 64 KB
bank boundary, so requests above 65536 are clamped to 65536 -- and even a
clamped request can fail if no free bank exists.
* On the IIgs a compiled sprite cannot cross a 64 KB bank boundary, so the
arena is up to four Memory Manager blocks of at most 64 KB, each inside one
bank: 256 KB at most. A bank that is refused is tried again at 60 KB, and
`jlInit` takes whatever part of the request it was granted. Elsewhere the
arena is one block, and `jlInit` fails if it cannot be had.
If the arena is too small, sprites still draw: they fall back to an
interpreter that is roughly *forty times slower*. Nothing reports this as an
error, so a program that never checks can be silently crawling.
interpreter that is *two to six times slower*, depending on the machine.
Nothing reports this as an error, so a program that never checks can be
silently crawling.
=== Part 2: The Stage, the Palette and Assets
=== Part 2: The Stage, the Palette, and Assets
[source,c]
----
@ -54,10 +60,10 @@ Set up colour before drawing:
[source,c]
----
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
// ... fill in 16 entries of $0RGB ...
jlPaletteSet(stage, 0, colors);
jlScbSetRange(stage, 0, SURFACE_HEIGHT - 1, 0);
jlScbSetRange(stage, 0, JL_SURFACE_HEIGHT - 1, 0);
----
Each surface carries *16 palettes of 16 colours* plus a 200-entry table (one
@ -71,22 +77,21 @@ Load assets after init, because sprite loading needs the codegen arena:
[source,c]
----
jlSpriteT *cels[16];
uint16_t palette[SURFACE_COLORS_PER_PALETTE];
uint16_t palette[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t count = jlSpriteBankLoad("player.spr", cels, 16, palette);
uint16_t i;
for (i = 0; i < count; i++) {
jlSpritePrewarm(cels[i]); // compile NOW, not mid-frame
(void)jlSpriteCompile(cels[i]); // compile NOW, not mid-frame
}
----
[IMPORTANT]
====
`jlSpriteDraw` does *not* compile a sprite on demand. An uncompiled sprite
draws through the interpreter every single time. Call `jlSpriteCompile` or
`jlSpritePrewarm` during loading -- otherwise the first draw of each sprite
either stalls a frame (if you compile lazily yourself) or never gets fast at
all.
draws through the interpreter every single time. Call `jlSpriteCompile` during
loading -- otherwise the first draw of each sprite either stalls a frame (if
you compile lazily yourself) or never gets fast at all.
====
=== Part 3: The Frame Loop
@ -100,7 +105,7 @@ while (running) {
jlInputPoll(); // refresh keyboard, mouse, joystick
jlAudioFrameTick(); // pump the audio engine
if (jlKeyPressed(KEY_ESCAPE)) {
if (jlKeyPressed(JL_KEY_ESCAPE)) {
running = false;
}
@ -113,73 +118,68 @@ while (running) {
Four library calls, in that order, and everything else is yours.
`jlWaitVBL()` blocks until the next vertical blank. It is cheap on every port
because the underlying mechanism is always a hardware wait, never a software
timer.
`jlWaitVBL()` blocks until the next vertical blank. On the machine ports that
is the display's own; the hosted ports, and DVX in a window, have none, and
wait for the next tick of a virtual frame clock instead.
`jlInputPoll()` must be called exactly once per frame. The edge predicates
(`jlKeyPressed`, `jlKeyReleased`, and the mouse and joystick equivalents) are
defined relative to the *previous poll*, so calling it twice per frame makes
edges disappear, and not calling it means nothing ever changes.
`jlAudioFrameTick()` is a no-op on ports whose audio runs from a hardware
interrupt, but it is required on ports that mix in user context, and it is what
advances tracker music and keeps streaming sound effects fed. Call it
unconditionally, once per frame.
`jlAudioFrameTick()` advances tracker music and keeps streaming sound effects
fed. Call it once per frame.
`jlStagePresent()` flips the dirty parts of the stage to the display. The
drawing primitives record which rows they touched, so presenting only copies
what actually changed. You do not need to tell it what you drew.
[NOTE]
====
If you write pixels by some route other than the library's primitives, the
dirty tracking does not know about it, and `jlStagePresent` will skip those
rows. That is the one case where you have to mark the region yourself.
====
what actually changed. You do not need to tell it what you drew: every call
that changes the stage's pixels marks them, and the API has no other way to
write them. Palette and SCB changes reach the display at the next present
too.
==== Frame-Rate Independence
The targets do not share a refresh rate: 50 Hz (Amiga PAL), 55 Hz
(X68000), 60 Hz (IIgs, ST NTSC, and SDL3's virtual rate) and 70 Hz (VGA
mode 13h, and DVX's virtual rate). A loop
The targets do not share a refresh rate: 50 Hz (the Amiga, and a PAL IIgs or
ST), 55 Hz (X68000), 60 Hz (the IIgs, an NTSC ST, and every hosted port's
virtual rate), and 70 Hz (VGA mode 13h, and DVX's virtual rate). A loop
that moves a sprite "two pixels per frame" runs 40 percent faster on DOS than
on a PAL Amiga.
Two ways to handle it. Ask the frame rate and scale your per-frame step:
So give every speed per second, never per frame, and work out each frame how
far that is. The millisecond clock does it best. `jlMillisElapsed()` reports
true elapsed wall-clock time, so a frame that took a long time to render
moves things as far as the time it took, rather than counting as one tick:
[source,c]
----
uint16_t hz = jlFrameHz(); // 50, 60 or 70
uint32_t now = jlMillisElapsed();
uint32_t elapsed = now - gLastMillis; // milliseconds since the last frame
gLastMillis = now;
----
Or drive from the millisecond clock, which is the more robust choice for
anything that must feel the same everywhere:
A thing then moves its speed times `elapsed` / 1000. At 70 frames a second
that is often a fraction of a pixel, which only adds up if positions keep the
fraction, so keep them in fixed point (the skeleton below uses 1/256 of a
pixel) and draw at the whole-pixel part.
[source,c]
----
uint32_t now = jlMillisElapsed();
uint32_t step = now - previous;
previous = now;
----
`jlMillisElapsed()` reports true elapsed wall-clock time, so a frame that took
a long time to render is accounted for correctly rather than counted as one
tick.
`jlFrameHz()` (50 to 70) is the other way: a per-frame step of speed /
`jlFrameHz()` is right while every frame takes one refresh, and falls behind
whenever one takes longer.
=== Part 4: Shut Down
[source,c]
----
jlAudioShutdown(); // if you called jlAudioInit
jlShutdown();
return 0;
----
`jlShutdown` restores the display mode, frees the library's allocations, and
un-hooks interrupt vectors. Sprites and surfaces you created are released with
it, but the buffers *you* allocated (including tile data you handed to
`jlSpriteCreate`, which is never copied) remain yours to free.
`jlShutdown` stops audio, restores the display mode, frees the library's allocations, and
un-hooks interrupt vectors. Sprites and surfaces you created are *not*
released with it: destroy them with `jlSpriteDestroy` and `jlSurfaceDestroy`
before you shut down. The buffers *you* allocated (including tile data you
handed to `jlSpriteCreate`, which is never copied) remain yours to free.
=== The Complete Skeleton
@ -189,25 +189,33 @@ This compiles and runs on every target as-is.
----
// A minimal but complete JoeyLib program.
#include <stddef.h>
#include <joey/joey.h>
#define PLAYER_SPEED 2
#define PLAYER_SIZE 16
#define PLAYER_SPEED 120 // pixels per second, the same on every port
#define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel
#define MAX_STEP_MILLIS 100 // after a stall, move no further than this
static jlSurfaceT *gStage;
static bool gRunning = true;
static int16_t gPlayerX = 160;
static int16_t gPlayerY = 100;
static bool gRunning = true;
static int32_t gPlayerX = 160L << SUBPIXEL_SHIFT;
static int32_t gPlayerY = 100L << SUBPIXEL_SHIFT;
static uint32_t gLastMillis = 0;
static void buildPalette(void);
static void handleInput(void);
static void renderFrame(void);
static void buildPalette(void);
static int32_t clampPosition(int32_t position, int16_t limit);
static void handleInput(int32_t step);
static void renderFrame(void);
static int32_t stepForFrame(void);
static void buildPalette(void) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint8_t i;
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = 0x000;
}
colors[1] = 0xFFF; // white
@ -215,58 +223,77 @@ static void buildPalette(void) {
colors[3] = 0x00C; // blue
jlPaletteSet(gStage, 0, colors);
jlScbSetRange(gStage, 0, SURFACE_HEIGHT - 1, 0);
jlScbSetRange(gStage, 0, JL_SURFACE_HEIGHT - 1, 0);
}
static void handleInput(void) {
if (jlKeyPressed(KEY_ESCAPE)) {
// A position (in 1/256 of a pixel) kept between 0 and limit pixels.
static int32_t clampPosition(int32_t position, int16_t limit) {
int32_t max = (int32_t)limit << SUBPIXEL_SHIFT;
if (position < 0) {
return 0;
}
if (position > max) {
return max;
}
return position;
}
// Moves the player by step (1/256 of a pixel) in each direction held.
static void handleInput(int32_t step) {
if (jlKeyPressed(JL_KEY_ESCAPE)) {
gRunning = false;
}
if (jlKeyDown(KEY_LEFT)) {
gPlayerX -= PLAYER_SPEED;
if (jlKeyDown(JL_KEY_LEFT)) {
gPlayerX -= step;
}
if (jlKeyDown(KEY_RIGHT)) {
gPlayerX += PLAYER_SPEED;
if (jlKeyDown(JL_KEY_RIGHT)) {
gPlayerX += step;
}
if (jlKeyDown(KEY_UP)) {
gPlayerY -= PLAYER_SPEED;
if (jlKeyDown(JL_KEY_UP)) {
gPlayerY -= step;
}
if (jlKeyDown(KEY_DOWN)) {
gPlayerY += PLAYER_SPEED;
if (jlKeyDown(JL_KEY_DOWN)) {
gPlayerY += step;
}
// A joystick, if one is attached, drives the same movement.
if (jlJoystickConnected(JOYSTICK_0)) {
if (jlJoystickX(JOYSTICK_0) < -64) {
gPlayerX -= PLAYER_SPEED;
if (jlJoystickConnected(JL_JOYSTICK_0)) {
if (jlJoystickX(JL_JOYSTICK_0) < -64) {
gPlayerX -= step;
}
if (jlJoystickX(JOYSTICK_0) > 64) {
gPlayerX += PLAYER_SPEED;
if (jlJoystickX(JL_JOYSTICK_0) > 64) {
gPlayerX += step;
}
}
// Keep the player on screen: the primitives would clip safely, but
// clamping keeps the object whole.
if (gPlayerX < 0) {
gPlayerX = 0;
}
if (gPlayerX > SURFACE_WIDTH - 16) {
gPlayerX = SURFACE_WIDTH - 16;
}
if (gPlayerY < 0) {
gPlayerY = 0;
}
if (gPlayerY > SURFACE_HEIGHT - 16) {
gPlayerY = SURFACE_HEIGHT - 16;
}
gPlayerX = clampPosition(gPlayerX, JL_SURFACE_WIDTH - PLAYER_SIZE);
gPlayerY = clampPosition(gPlayerY, JL_SURFACE_HEIGHT - PLAYER_SIZE);
}
static void renderFrame(void) {
jlSurfaceClear(gStage, 0);
jlDrawRect(gStage, 0, 0, SURFACE_WIDTH, SURFACE_HEIGHT, 1);
jlFillRect(gStage, gPlayerX, gPlayerY, 16, 16, 2);
jlDrawRect(gStage, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, 1);
jlFillRect(gStage, (int16_t)(gPlayerX >> SUBPIXEL_SHIFT), (int16_t)(gPlayerY >> SUBPIXEL_SHIFT), PLAYER_SIZE, PLAYER_SIZE, 2);
}
// How far the player moves this frame, in 1/256 of a pixel: PLAYER_SPEED
// times the time since the last frame, whatever the port's frame rate.
static int32_t stepForFrame(void) {
uint32_t now = jlMillisElapsed();
uint32_t elapsed = now - gLastMillis;
gLastMillis = now;
if (elapsed > MAX_STEP_MILLIS) {
elapsed = MAX_STEP_MILLIS;
}
return (int32_t)((((uint32_t)PLAYER_SPEED * elapsed) << SUBPIXEL_SHIFT) / 1000u);
}
@ -274,6 +301,7 @@ int main(void) {
jlConfigT config;
config.codegenBytes = 32UL * 1024;
config.noAudio = false;
if (!jlInit(&config)) {
return 1;
@ -287,13 +315,14 @@ int main(void) {
buildPalette();
jlRandomSeed(12345); // deterministic on every port
gLastMillis = jlMillisElapsed();
while (gRunning) {
jlWaitVBL();
jlInputPoll();
jlAudioFrameTick();
handleInput();
handleInput(stepForFrame());
renderFrame();
jlStagePresent();
@ -315,7 +344,7 @@ Before drawing a sprite, capture what is underneath; next frame, put it back.
[source,c]
----
uint16_t backing[JOEY_SPRITE_BACKUP_BYTES(2, 2) / 2];
uint16_t backing[jlSpriteBackupLength(uint16_t, 2, 2)];
jlSpriteBackupT backup;
backup.bytes = (uint8_t *)backing;
@ -326,10 +355,10 @@ jlSpriteRestoreUnder(stage, &backup); // erase last frame's sprite
jlSpriteSaveAndDraw(stage, sprite, x, y, &backup);
----
Size the buffer with the `JOEY_SPRITE_BACKUP_BYTES` macro, and declare it as
`uint16_t` (or otherwise 2-byte aligned) -- an odd address pushes save and
restore onto slow interpreted paths on the planar ports, because the 68000
cannot do word accesses at odd addresses.
Size the buffer with `jlSpriteBackupLength`, and declare it as `uint16_t` (or
otherwise 2-byte aligned): on the Amiga, ST, and X68000 an odd address sends
save and restore down a much slower path, and on the Amiga and ST it crashes
the trusted variants.
==== The Clean-Buffer Pattern
@ -341,12 +370,12 @@ jlSurfaceT *clean = jlSurfaceCreate();
// ... draw the static scene into clean, once ...
// each frame:
jlSurfaceCopyRect(stage, clean, x, y, w, h); // erase EVERY window first
jlSurfaceCopyRect(stage, clean, x, y, w, h); // erase EVERY region first
// ... then draw ALL sprites ...
----
This scales better with many sprites, and it is the right choice when sprites
overlap. Erase every window first, then draw every sprite -- never interleave
overlap. Erase every region first, then draw every sprite -- never interleave
erase and draw.
[WARNING]
@ -354,6 +383,6 @@ erase and draw.
`jlSurfaceCopyRect` widens the copied region outward to the 16-pixel group grid
on *every* port, so it can touch up to 15 pixels either side of the rectangle
you asked for. That means it can overwrite content just outside your rectangle,
and it silently invalidates any live `jlSpriteBackupT` whose recorded window it
and it silently invalidates any live `jlSpriteBackupT` whose recorded region it
touches. This is exactly why the two patterns must not be mixed in one region.
====

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -22,10 +22,10 @@
// keys on a timetable -- N once, S every 1.5 seconds -- for the IIgs and
// X68000 audio gates, which read the sound chips but cannot type.
//
// On platforms where the audio HAL is still a stub, jlAudioInit
// returns false, every audio call is a quiet no-op, and the demo runs
// as a silent input loop -- you can confirm the build links and the
// input plumbing still works without hearing anything.
// On a machine whose sound does not start (jlInit logs it), every audio
// call is a quiet no-op and the demo runs as a silent input loop -- you
// can confirm the build links and the input plumbing still works without
// hearing anything.
#include <stdio.h>
#include <stdlib.h>
@ -101,7 +101,7 @@
#define BAR_X 16
#define BAR_Y 88
#define BAR_W (SURFACE_WIDTH - 32)
#define BAR_W (JL_SURFACE_WIDTH - 32)
#define BAR_H 16
// The streamed tone's state, which streamFill advances.
@ -113,7 +113,7 @@ typedef struct {
static void buildPalette(jlSurfaceT *screen);
static void initialPaint(jlSurfaceT *screen, bool audioOk);
static void initialPaint(jlSurfaceT *screen);
static bool keyPressed(jlKeyE key, uint16_t frame);
static bool loadFile(const char *name, uint32_t maxLen, uint8_t **outBytes, uint32_t *outLen);
static bool noiseStep(uint16_t sweepFrame);
@ -129,10 +129,10 @@ static uint8_t gStreamTake = 0;
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i;
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = 0x0000;
}
colors[COLOR_BG] = 0x0000;
@ -146,10 +146,9 @@ static void buildPalette(jlSurfaceT *screen) {
// Visual feedback for "audio is running, but you cannot tell on a
// stub HAL": pulse a horizontal bar between the hint color and the
// active color whenever a sound has fired this frame.
static void initialPaint(jlSurfaceT *screen, bool audioOk) {
static void initialPaint(jlSurfaceT *screen) {
jlSurfaceClear(screen, COLOR_BG);
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H,
audioOk ? COLOR_HINT : COLOR_BG);
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT);
jlStagePresent();
}
@ -226,17 +225,17 @@ static bool noiseStep(uint16_t sweepFrame) {
// so a gate finds the noise sweep and a stream running.
static jlKeyE probeKey(uint16_t frame) {
if (frame == PROBE_NOISE_FRAME) {
return KEY_N;
return JL_KEY_N;
}
if (frame % PROBE_STREAM_FRAMES == PROBE_STREAM_FIRST) {
return KEY_S;
return JL_KEY_S;
}
return KEY_NONE;
return JL_KEY_NONE;
}
#else
static jlKeyE probeKey(uint16_t frame) {
(void)frame;
return KEY_NONE;
return JL_KEY_NONE;
}
#endif
@ -306,7 +305,6 @@ static void streamStart(void) {
int main(void) {
jlConfigT config;
jlSurfaceT *screen;
bool audioOk;
bool musicPaused;
bool noiseOn;
bool noiseResumes;
@ -319,6 +317,7 @@ int main(void) {
uint32_t sfxLen;
config.codegenBytes = 8 * 1024;
config.noAudio = false;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
@ -337,19 +336,16 @@ int main(void) {
modLen = 0;
sfxLen = 0;
audioOk = jlAudioInit();
if (audioOk) {
// Keep the MOD bytes resident so M can (re)start it later;
// jlAudioPlayMod copies them at play time.
(void)loadFile(TEST_MOD_NAME, DATA_FILE_MAX, &modBytes, &modLen);
(void)loadFile(TEST_SFX_NAME, DATA_FILE_MAX, &sfxBytes, &sfxLen);
gRawFormat = jlAudioSfxRawFormat();
}
// Keep the MOD bytes resident so M can (re)start it later;
// jlAudioPlayMod copies them at play time.
(void)loadFile(TEST_MOD_NAME, DATA_FILE_MAX, &modBytes, &modLen);
(void)loadFile(TEST_SFX_NAME, DATA_FILE_MAX, &sfxBytes, &sfxLen);
gRawFormat = jlAudioSfxRawFormat();
jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true);
buildPalette(screen);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
initialPaint(screen, audioOk);
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
initialPaint(screen);
musicPaused = false;
noiseOn = false;
@ -360,18 +356,18 @@ int main(void) {
jlWaitVBL();
jlInputPoll();
jlAudioFrameTick();
if (keyPressed(KEY_ESCAPE, frame)) {
if (keyPressed(JL_KEY_ESCAPE, frame)) {
break;
}
if (keyPressed(KEY_SPACE, frame) && sfxBytes != NULL) {
if (keyPressed(JL_KEY_SPACE, frame) && sfxBytes != NULL) {
jlAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ);
flashFrames = FLASH_FRAMES;
}
if (keyPressed(KEY_S, frame) && audioOk) {
if (keyPressed(JL_KEY_S, frame)) {
streamStart();
flashFrames = FLASH_FRAMES;
}
if (keyPressed(KEY_N, frame) && !noiseOn) {
if (keyPressed(JL_KEY_N, frame) && !noiseOn) {
// The song's own drums would fight the sweep for the noise
// generator, so it pauses and picks up again afterwards.
noiseResumes = jlMusicIsPlaying() && !musicPaused;
@ -379,15 +375,15 @@ int main(void) {
noiseOn = true;
noiseFrame = 0;
}
if (keyPressed(KEY_M, frame) && modBytes != NULL) {
if (keyPressed(JL_KEY_M, frame) && modBytes != NULL) {
jlMusicStop();
jlAudioPlayMod(modBytes, modLen, true);
}
if (keyPressed(KEY_T, frame)) {
if (keyPressed(JL_KEY_T, frame)) {
jlAudioStopMod();
jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true);
}
if (keyPressed(KEY_P, frame)) {
if (keyPressed(JL_KEY_P, frame)) {
if (musicPaused) {
jlMusicResume();
} else {
@ -416,10 +412,7 @@ int main(void) {
}
jlMusicStop();
if (audioOk) {
jlAudioStopMod();
}
jlAudioShutdown();
jlAudioStopMod();
if (modBytes != NULL) {
free(modBytes);
}

401
examples/audpar/audpar.c Normal file
View file

@ -0,0 +1,401 @@
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// Audio conformance probe (AUDPAR, a gate's program, not an example): one
// fixed timeline of what every port's audio must do alike, each step logged
// as it starts, so scripts/verify-audio-conformance.sh can record what a
// port played and tools/gradeAudioConformance.py can measure it against the
// API's contract: tone and voice pitch, 2 dB attenuation steps, every voice,
// the noise generator's rate and steps, sound effects at their rates and
// pans, a module that does not loop ending (and jlAudioIsPlayingMod saying
// so), and tick callbacks at their rates.
//
// The log, one line per event, "AUDPAR <ms> <event> <values>", ms counted
// from the first sync tone's start:
// sync <hz> a sync tone: the timeline starts and ends
// with one, so a recording can be aligned
// tone <hz> jlAudioTone
// voice <v> <hz> <atten> jlAudioVoice
// noise <pitch> <atten> jlAudioNoise
// sfx <slot> <rate> <hz> <pan> a one-shot sine sounding <hz> at <rate>
// mod start / mod end jlAudioPlayMod with loop false, and the
// first jlAudioIsPlayingMod false after it
// tick <hz> <ok> <calls> <ms> jlAudioTickRegister's result, the calls
// that came, and the time they had
// silence everything stopped
// done
// Escape ends it early.
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <joey/joey.h>
#include <joey/file.h>
#if defined(JOEYLIB_PLATFORM_IIGS)
# define TEST_MOD_NAME "TEST.NTP"
#else
# define TEST_MOD_NAME "test.mod"
#endif
#define DATA_FILE_MAX (64UL * 1024UL)
#define SYNC_HZ 1000u
#define SYNC_MS 300u
#define GAP_MS 100u
#define PAUSE_MS 200u
#define ATTEN_STEPS 15u // 0..14 audible, 15 silent
#define ATTEN_SILENT 15u
#define TONE_MS 400u
#define STEP_MS 300u
#define NOISE_MS 400u
#define NOISE_STEP_MS 200u
#define NOISE_STEP_PITCH 8u
#define LEVEL_HZ 440u
#define VOICE_HZ 660u
#define TICK_MS 2000u
#define MOD_WAIT_MS 12000u
#define SFX_SLOT 0u
#define SFX_SAMPLES 4000u
#define SFX_PERIOD 8u // samples a cycle: the tone is rate / 8
#define SFX_AMPLITUDE 100
#define MS_PER_SECOND 1000u
#define EVENTS_MAX 128u
typedef enum {
STEP_SYNC,
STEP_TONE,
STEP_VOICE,
STEP_NOISE,
STEP_SFX,
STEP_MOD,
STEP_TICK,
STEP_SILENCE
} StepKindE;
typedef struct {
StepKindE kind;
uint8_t channel; // voice, or the sfx pan
uint16_t value; // Hz, noise pitch, or sfx rate
uint8_t level; // attenuation
uint16_t ms; // how long it sounds (sfx: from its length)
} StepT;
// What the log will say of one step, kept in memory until the timeline is
// over: a log line can take a disk write (a fifth of a second on the ST),
// and written while it plays it would stall the program's sound.
typedef struct {
uint32_t ms;
StepKindE kind;
uint16_t value;
uint8_t channel;
uint8_t level;
uint32_t count; // mod: its length, then playing; tick: calls
uint32_t span; // tick: the time the calls had
bool ok; // tick: registered; mod end: still playing
bool modEnd;
} EventT;
static EventT *eventAdd(uint32_t ms, const StepT *step);
static void eventsLog(void);
static void loadMod(uint8_t **bytes, uint32_t *length);
static bool pump(void);
static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength);
static uint32_t since(void);
static void silenceAll(void);
static void tickCount(void);
static bool waitMs(uint16_t ms);
static const StepT kTimeline[] = {
{ STEP_SYNC, 0u, SYNC_HZ, 0u, SYNC_MS },
{ STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// jlAudioTone across the octaves every port can play.
{ STEP_TONE, 0u, 110u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_TONE, 0u, 220u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_TONE, 0u, 440u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_TONE, 0u, 880u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_TONE, 0u, 1760u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_TONE, 0u, 3136u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// Voice 0's attenuation, one step after another: 2 dB apart.
{ STEP_VOICE, 0u, LEVEL_HZ, 0u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 1u, STEP_MS },
{ STEP_VOICE, 0u, LEVEL_HZ, 2u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 3u, STEP_MS },
{ STEP_VOICE, 0u, LEVEL_HZ, 4u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 5u, STEP_MS },
{ STEP_VOICE, 0u, LEVEL_HZ, 6u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 7u, STEP_MS },
{ STEP_VOICE, 0u, LEVEL_HZ, 8u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 9u, STEP_MS },
{ STEP_VOICE, 0u, LEVEL_HZ, 10u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 11u, STEP_MS },
{ STEP_VOICE, 0u, LEVEL_HZ, 12u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 13u, STEP_MS },
{ STEP_VOICE, 0u, LEVEL_HZ, 14u, STEP_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// The other two voices.
{ STEP_VOICE, 1u, VOICE_HZ, 0u, STEP_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_VOICE, 2u, VOICE_HZ, 0u, STEP_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// The noise generator's rate, bright to dark.
{ STEP_NOISE, 0u, 1u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_NOISE, 0u, 2u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_NOISE, 0u, 4u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_NOISE, 0u, 8u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_NOISE, 0u, 16u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_NOISE, 0u, 31u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// The noise's attenuation steps.
{ STEP_NOISE, 0u, NOISE_STEP_PITCH, 0u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 2u, NOISE_STEP_MS },
{ STEP_NOISE, 0u, NOISE_STEP_PITCH, 4u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 6u, NOISE_STEP_MS },
{ STEP_NOISE, 0u, NOISE_STEP_PITCH, 8u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 10u, NOISE_STEP_MS },
{ STEP_NOISE, 0u, NOISE_STEP_PITCH, 12u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 14u, NOISE_STEP_MS },
{ STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// Sound effects at three rates, then panned each way.
{ STEP_SFX, JL_AUDIO_PAN_CENTER, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_SFX, JL_AUDIO_PAN_CENTER, 16000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_SFX, JL_AUDIO_PAN_CENTER, 22050u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_SFX, JL_AUDIO_PAN_LEFT, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_SFX, JL_AUDIO_PAN_RIGHT, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// A module that does not loop, to its end.
{ STEP_MOD, 0u, 0u, 0u, MOD_WAIT_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// Tick callbacks, slow and at a frame's rate.
{ STEP_TICK, 0u, 10u, 0u, TICK_MS },
{ STEP_TICK, 0u, 60u, 0u, TICK_MS },
{ STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
{ STEP_SYNC, 0u, SYNC_HZ, 0u, SYNC_MS },
{ STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }
};
// One cycle of a sine, SFX_PERIOD samples, so a sound of it is rate / 8 Hz.
static const int8_t kSineCycle[SFX_PERIOD] = { 0, 71, SFX_AMPLITUDE, 71, 0, -71, -SFX_AMPLITUDE, -71 };
static EventT gEvents[EVENTS_MAX];
static uint16_t gEventCount = 0u;
static int8_t gSine[SFX_SAMPLES];
static uint32_t gStartMs = 0u;
static volatile uint32_t gTickCalls = 0u;
// A new event for step at ms; NULL once EVENTS_MAX are taken.
static EventT *eventAdd(uint32_t ms, const StepT *step) {
EventT *event;
if (gEventCount >= EVENTS_MAX) {
return NULL;
}
event = &gEvents[gEventCount++];
event->ms = ms;
event->kind = step->kind;
event->value = step->value;
event->channel = step->channel;
event->level = step->level;
event->count = 0u;
event->span = 0u;
event->ok = false;
event->modEnd = false;
return event;
}
// The log, once the timeline is over (the format in the header).
static void eventsLog(void) {
const EventT *event;
uint16_t i;
for (i = 0u; i < gEventCount; i++) {
event = &gEvents[i];
switch (event->kind) {
case STEP_SYNC:
jlLogF("AUDPAR %lu sync %u", (unsigned long)event->ms, (unsigned)event->value);
break;
case STEP_TONE:
jlLogF("AUDPAR %lu tone %u", (unsigned long)event->ms, (unsigned)event->value);
break;
case STEP_VOICE:
jlLogF("AUDPAR %lu voice %u %u %u", (unsigned long)event->ms, (unsigned)event->channel, (unsigned)event->value, (unsigned)event->level);
break;
case STEP_NOISE:
jlLogF("AUDPAR %lu noise %u %u", (unsigned long)event->ms, (unsigned)event->value, (unsigned)event->level);
break;
case STEP_SFX:
jlLogF("AUDPAR %lu sfx %u %u %u %u", (unsigned long)event->ms, (unsigned)SFX_SLOT, (unsigned)event->value, (unsigned)(event->value / SFX_PERIOD), (unsigned)event->channel);
break;
case STEP_MOD:
if (event->modEnd) {
jlLogF("AUDPAR %lu mod end %u", (unsigned long)event->ms, (unsigned)event->ok);
} else {
jlLogF("AUDPAR %lu mod start %lu", (unsigned long)event->ms, (unsigned long)event->count);
}
break;
case STEP_TICK:
jlLogF("AUDPAR %lu tick %u %u %lu %lu", (unsigned long)event->ms, (unsigned)event->value, (unsigned)event->ok, (unsigned long)event->count, (unsigned long)event->span);
break;
case STEP_SILENCE:
jlLogF("AUDPAR %lu silence", (unsigned long)event->ms);
break;
}
}
}
// DATA's module, whole; none (NULL, 0) when it is missing or too big.
static void loadMod(uint8_t **bytes, uint32_t *length) {
FILE *fp;
long size;
*bytes = NULL;
*length = 0u;
fp = jlDataOpen(TEST_MOD_NAME, "rb");
if (fp == NULL) {
return;
}
size = -1L;
if (fseek(fp, 0L, SEEK_END) == 0) {
size = ftell(fp);
}
if (size > 0L && (uint32_t)size <= DATA_FILE_MAX && fseek(fp, 0L, SEEK_SET) == 0) {
*bytes = (uint8_t *)malloc((size_t)size);
if (*bytes != NULL && fread(*bytes, 1, (size_t)size, fp) == (size_t)size) {
*length = (uint32_t)size;
}
}
fclose(fp);
}
// One frame: the engine's pump, the display's pace, and input. False when
// Escape asks to stop.
static bool pump(void) {
jlAudioFrameTick();
jlWaitVBL();
jlInputPoll();
return !jlKeyPressed(JL_KEY_ESCAPE);
}
// Each step's time is taken as it starts.
static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength) {
uint32_t start;
EventT *event;
start = since();
event = eventAdd(start, step);
switch (step->kind) {
case STEP_SYNC:
case STEP_TONE:
jlAudioTone(step->value);
(void)waitMs(step->ms);
break;
case STEP_VOICE:
jlAudioVoice(step->channel, step->value, step->level);
(void)waitMs(step->ms);
break;
case STEP_NOISE:
jlAudioNoise((uint8_t)step->value, step->level);
(void)waitMs(step->ms);
break;
case STEP_SFX:
jlAudioSetPan(SFX_SLOT, (jlAudioPanE)step->channel);
jlAudioPlaySfx(SFX_SLOT, (const uint8_t *)gSine, SFX_SAMPLES, step->value);
(void)waitMs((uint16_t)((uint32_t)SFX_SAMPLES * MS_PER_SECOND / step->value));
break;
case STEP_MOD:
if (mod != NULL) {
jlAudioPlayMod(mod, modLength, false);
}
if (event != NULL) {
event->count = modLength;
}
while (jlAudioIsPlayingMod() && since() - start < step->ms) {
if (!pump()) {
break;
}
}
event = eventAdd(since(), step);
if (event != NULL) {
event->modEnd = true;
event->ok = jlAudioIsPlayingMod();
}
jlAudioStopMod();
break;
case STEP_TICK:
gTickCalls = 0u;
if (event != NULL) {
event->ok = jlAudioTickRegister(tickCount, step->value);
}
(void)waitMs(step->ms);
(void)jlAudioTickRegister(NULL, 0u);
if (event != NULL) {
event->count = gTickCalls;
event->span = since() - start;
}
break;
case STEP_SILENCE:
silenceAll();
(void)waitMs(step->ms);
break;
}
}
static uint32_t since(void) {
return jlMillisElapsed() - gStartMs;
}
static void silenceAll(void) {
uint8_t voice;
for (voice = 0u; voice < JL_AUDIO_VOICES; voice++) {
jlAudioVoice(voice, 0u, ATTEN_SILENT);
}
jlAudioNoise(0u, ATTEN_SILENT);
jlAudioStopSfx(SFX_SLOT);
}
static void tickCount(void) {
gTickCalls++;
}
static bool waitMs(uint16_t ms) {
uint32_t start;
start = since();
while (since() - start < ms) {
if (!pump()) {
return false;
}
}
return true;
}
int main(void) {
jlConfigT config;
uint8_t *mod;
uint32_t modLength;
uint16_t i;
config.codegenBytes = 8 * 1024;
config.noAudio = false;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return 1;
}
for (i = 0u; i < SFX_SAMPLES; i++) {
gSine[i] = kSineCycle[i % SFX_PERIOD];
}
loadMod(&mod, &modLength);
jlSurfaceClear(jlStageGet(), 0u);
jlStagePresent();
gStartMs = jlMillisElapsed();
for (i = 0u; i < (uint16_t)(sizeof(kTimeline) / sizeof(kTimeline[0])); i++) {
runStep(&kTimeline[i], mod, modLength);
if (jlKeyDown(JL_KEY_ESCAPE)) {
break;
}
}
silenceAll();
eventsLog();
jlLogF("AUDPAR %lu done", (unsigned long)since());
jlLogFlush();
free(mod);
jlShutdown();
return 0;
}

View file

@ -47,7 +47,7 @@ static void waitForKey(void);
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
// 16 distinct $0RGB entries. Index 0 is forced to black anyway.
colors[0] = 0x000;
@ -67,7 +67,7 @@ static void buildPalette(jlSurfaceT *screen) {
colors[14] = 0x088;
colors[15] = 0x808;
jlPaletteSet(screen, 0, colors);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
}
@ -112,8 +112,7 @@ static void drawPrimitivesPixelLine(jlSurfaceT *screen) {
// Pixel row: 14 single-pixel writes at consecutive x to exercise
// both odd and even nibble paths.
for (i = 0; i < 14; i++) {
jlDrawPixel(screen, (int16_t)(10 + i * 10), (int16_t)(CELL_H - 6),
(uint8_t)((i + 1) & 0x0F));
jlDrawPixel(screen, (int16_t)(10 + i * 10), (int16_t)(CELL_H - 6), (uint8_t)((i + 1) & 0x0F));
}
}
@ -183,7 +182,7 @@ static void drawPrimitivesTileFlood(jlSurfaceT *screen) {
int16_t by;
int16_t i;
int16_t px;
jlTileT snapBuf;
jlTileT snapBuf;
ox = CELL_W; // 160
oy = CELL_H; // 100
@ -202,8 +201,7 @@ static void drawPrimitivesTileFlood(jlSurfaceT *screen) {
jlTilePaste(screen, (uint8_t)(bx + 4), (uint8_t)by, &snapBuf);
// jlTileCopy from the green quadrant onto a fresh location below.
jlTileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1),
screen, (uint8_t)(bx + 1), (uint8_t)by);
jlTileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1), screen, (uint8_t)(bx + 1), (uint8_t)by);
// jlTileCopyMasked test: build a "transparent" striped pattern at
// (208, 132). The tile's source has color 0 in alternating
@ -213,23 +211,19 @@ static void drawPrimitivesTileFlood(jlSurfaceT *screen) {
// Build a vertical-stripe source at (240, 132): col-pixel = (px % 2 ? color : 0)
for (i = 0; i < 8; i++) {
for (px = 0; px < 16; px++) {
jlDrawPixel(screen, (int16_t)(ox + 112 + px), (int16_t)(oy + 32 + i),
(uint8_t)((px & 1) ? C_MAGENTA : 0));
jlDrawPixel(screen, (int16_t)(ox + 112 + px), (int16_t)(oy + 32 + i), (uint8_t)((px & 1) ? C_MAGENTA : 0));
}
}
// jlTileCopyMasked: source at block (ox+112)/8 = 34..35, by 16
// -> dst at backdrop block (ox+80)/8 = 30..31, by 16
jlTileCopyMasked(screen, (uint8_t)((ox + 80) / 8), (uint8_t)((oy + 32) / 8),
screen, (uint8_t)((ox + 112) / 8), (uint8_t)((oy + 32) / 8),
0);
jlTileCopyMasked(screen, (uint8_t)((ox + 80) / 8), (uint8_t)((oy + 32) / 8), screen, (uint8_t)((ox + 112) / 8), (uint8_t)((oy + 32) / 8), 0);
// Flood-fill region: a small bordered rectangle in the cell's
// lower portion. Outline drawn in C_BORDER; jlFloodFillBounded
// from a point inside should fill with C_CYAN, stopping at the
// border.
jlDrawRect(screen, ox + 16, oy + 60, 64, 32, C_BORDER);
jlFloodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76),
C_CYAN, C_BORDER);
jlFloodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76), C_CYAN, C_BORDER);
// Plain jlFloodFill: solid block then re-fill to a new color.
jlFillRect(screen, ox + 96, oy + 60, 48, 32, C_GREEN);
@ -243,10 +237,11 @@ static void waitForKey(void) {
int main(void) {
jlConfigT config;
jlSurfaceT *screen;
jlConfigT config;
jlSurfaceT *screen;
config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());

View file

@ -14,6 +14,7 @@ int main(void) {
jlConfigT config;
config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());

View file

@ -51,7 +51,7 @@
typedef struct {
int16_t dotX;
int16_t dotY;
bool btn[JOY_BUTTON_COUNT];
bool btn[JL_JOY_BUTTON_COUNT];
bool connected;
bool valid;
} StickViewT;
@ -65,14 +65,14 @@ static void drawFrame(jlSurfaceT *screen, int16_t left);
static void initialPaint(jlSurfaceT *screen);
static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left);
static StickViewT gView[JOYSTICK_COUNT];
static StickViewT gView[JL_JOYSTICK_COUNT];
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i;
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = 0x0000;
}
colors[COLOR_BACKGROUND] = 0x0000;
@ -104,7 +104,7 @@ static int16_t dotXFor(int16_t left, int8_t ax) {
int32_t off;
cx = left + FRAME_SIZE / 2 - DOT_SIZE / 2;
off = ((int32_t)ax * DOT_TRAVEL) / JOYSTICK_AXIS_MAX;
off = ((int32_t)ax * DOT_TRAVEL) / JL_JOYSTICK_AXIS_MAX;
return (int16_t)(cx + off);
}
@ -114,7 +114,7 @@ static int16_t dotYFor(int8_t ay) {
int32_t off;
cy = STICK_TOP + FRAME_SIZE / 2 - DOT_SIZE / 2;
off = ((int32_t)ay * DOT_TRAVEL) / JOYSTICK_AXIS_MAX;
off = ((int32_t)ay * DOT_TRAVEL) / JL_JOYSTICK_AXIS_MAX;
return (int16_t)(cy + off);
}
@ -142,7 +142,7 @@ static void initialPaint(jlSurfaceT *screen) {
int16_t left;
jlSurfaceClear(screen, COLOR_BACKGROUND);
for (i = 0; i < JOYSTICK_COUNT; i++) {
for (i = 0; i < JL_JOYSTICK_COUNT; i++) {
left = (i == 0) ? STICK0_LEFT : STICK1_LEFT;
drawFrame(screen, left);
gView[i].valid = false;
@ -160,7 +160,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
int8_t ay;
int16_t newDotX;
int16_t newDotY;
bool newBtn[JOY_BUTTON_COUNT];
bool newBtn[JL_JOY_BUTTON_COUNT];
bool connected;
int16_t i;
int16_t bx;
@ -172,7 +172,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
ay = jlJoystickY(js);
newDotX = dotXFor(left, ax);
newDotY = dotYFor(ay);
for (i = 0; i < JOY_BUTTON_COUNT; i++) {
for (i = 0; i < JL_JOY_BUTTON_COUNT; i++) {
newBtn[i] = jlJoyDown(js, (jlJoyButtonE)i);
}
@ -195,7 +195,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
// Button squares.
by = BUTTON_TOP;
for (i = 0; i < JOY_BUTTON_COUNT; i++) {
for (i = 0; i < JL_JOY_BUTTON_COUNT; i++) {
if (v->valid && v->btn[i] == newBtn[i]) {
continue;
}
@ -213,6 +213,7 @@ int main(void) {
jlSurfaceT *screen;
config.codegenBytes = CODEGEN_BYTES;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
@ -227,17 +228,17 @@ int main(void) {
}
buildPalette(screen);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
initialPaint(screen);
jlInputPoll();
for (;;) {
jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) {
if (jlKeyPressed(JL_KEY_ESCAPE)) {
break;
}
updateStick(screen, JOYSTICK_0, STICK0_LEFT);
updateStick(screen, JOYSTICK_1, STICK1_LEFT);
updateStick(screen, JL_JOYSTICK_0, STICK0_LEFT);
updateStick(screen, JL_JOYSTICK_1, STICK1_LEFT);
}
jlShutdown();

View file

@ -111,16 +111,16 @@ static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint16_t valu
static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
static void updateMouseVerifyRow(jlSurfaceT *screen);
// Keys laid out row-by-row. KEY_NONE cells stay blank. Shape roughly
// Keys laid out row-by-row. JL_KEY_NONE cells stay blank. Shape roughly
// resembles a real keyboard (top number row, then QWERTY rows, then a
// cluster of modifiers / arrows / function keys).
static const jlKeyE gKeyGrid[GRID_ROWS][GRID_COLS] = {
{ KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0 },
{ KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P },
{ KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_BACKSPACE },
{ KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_LSHIFT, KEY_RSHIFT, KEY_TAB },
{ KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_LCTRL, KEY_LALT, KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_NONE },
{ KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10 }
{ JL_KEY_1, JL_KEY_2, JL_KEY_3, JL_KEY_4, JL_KEY_5, JL_KEY_6, JL_KEY_7, JL_KEY_8, JL_KEY_9, JL_KEY_0 },
{ JL_KEY_Q, JL_KEY_W, JL_KEY_E, JL_KEY_R, JL_KEY_T, JL_KEY_Y, JL_KEY_U, JL_KEY_I, JL_KEY_O, JL_KEY_P },
{ JL_KEY_A, JL_KEY_S, JL_KEY_D, JL_KEY_F, JL_KEY_G, JL_KEY_H, JL_KEY_J, JL_KEY_K, JL_KEY_L, JL_KEY_BACKSPACE },
{ JL_KEY_Z, JL_KEY_X, JL_KEY_C, JL_KEY_V, JL_KEY_B, JL_KEY_N, JL_KEY_M, JL_KEY_LSHIFT, JL_KEY_RSHIFT, JL_KEY_TAB },
{ JL_KEY_SPACE, JL_KEY_ESCAPE, JL_KEY_RETURN, JL_KEY_LCTRL, JL_KEY_LALT, JL_KEY_UP, JL_KEY_DOWN, JL_KEY_LEFT, JL_KEY_RIGHT, JL_KEY_NONE },
{ JL_KEY_F1, JL_KEY_F2, JL_KEY_F3, JL_KEY_F4, JL_KEY_F5, JL_KEY_F6, JL_KEY_F7, JL_KEY_F8, JL_KEY_F9, JL_KEY_F10 }
};
static bool gCellLit[GRID_ROWS][GRID_COLS];
@ -224,7 +224,7 @@ static const uint8_t kFontGlyph[][FONT_H] = {
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i;
// Every entry gets a DISTINCT color (gray ramp for the unused
@ -232,7 +232,7 @@ static void buildPalette(jlSurfaceT *screen) {
// screenshot-based harness can decode the verification row's
// nibble pixels back to values on ports without direct
// framebuffer reads.
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = (uint16_t)((i << 8) | (i << 4) | i);
}
colors[COLOR_BACKGROUND] = 0x0000; // black
@ -271,13 +271,13 @@ static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t c
jlKeyE key;
key = gKeyGrid[row][col];
if (key == KEY_NONE) {
if (key == JL_KEY_NONE) {
return false;
}
if (jlKeyDown(key)) {
return true;
}
if (col == cursorCol && row == cursorRow && jlMouseDown(MOUSE_BUTTON_LEFT)) {
if (col == cursorCol && row == cursorRow && jlMouseDown(JL_MOUSE_BUTTON_LEFT)) {
return true;
}
return false;
@ -346,7 +346,7 @@ static void drawTextLine(jlSurfaceT *screen, int16_t x, int16_t y, const char *t
}
}
cx = (int16_t)(cx + FONT_W + 1);
if (cx > SURFACE_WIDTH - FONT_W) {
if (cx > JL_SURFACE_WIDTH - FONT_W) {
return;
}
}
@ -372,23 +372,57 @@ static void drawVerifyRow(jlSurfaceT *screen) {
static int glyphIdx(char c) {
if (c == ' ') { return GLYPH_SPACE; }
if (c >= '0' && c <= '9') { return GLYPH_DIGIT_BASE + (c - '0'); }
if (c >= 'A' && c <= 'Z') { return GLYPH_ALPHA_BASE + (c - 'A'); }
if (c >= 'a' && c <= 'z') { return GLYPH_ALPHA_BASE + (c - 'a'); } // uppercase fallback
if (c == '.') { return GLYPH_PERIOD; }
if (c == ',') { return GLYPH_COMMA; }
if (c == ':') { return GLYPH_COLON; }
if (c == '!') { return GLYPH_BANG; }
if (c == '?') { return GLYPH_QUESTION; }
if (c == '-') { return GLYPH_MINUS; }
if (c == '\'') { return GLYPH_APOSTROPHE; }
if (c == '"') { return GLYPH_QUOTE; }
if (c == '/') { return GLYPH_SLASH; }
if (c == ';') { return GLYPH_SEMICOLON; }
if (c == '=') { return GLYPH_EQUALS; }
if (c == '+') { return GLYPH_PLUS; }
if (c == '_') { return GLYPH_UNDERSCORE; }
if (c == ' ') {
return GLYPH_SPACE;
}
if (c >= '0' && c <= '9') {
return GLYPH_DIGIT_BASE + (c - '0');
}
if (c >= 'A' && c <= 'Z') {
return GLYPH_ALPHA_BASE + (c - 'A');
}
if (c >= 'a' && c <= 'z') {
return GLYPH_ALPHA_BASE + (c - 'a'); // uppercase fallback
}
if (c == '.') {
return GLYPH_PERIOD;
}
if (c == ',') {
return GLYPH_COMMA;
}
if (c == ':') {
return GLYPH_COLON;
}
if (c == '!') {
return GLYPH_BANG;
}
if (c == '?') {
return GLYPH_QUESTION;
}
if (c == '-') {
return GLYPH_MINUS;
}
if (c == '\'') {
return GLYPH_APOSTROPHE;
}
if (c == '"') {
return GLYPH_QUOTE;
}
if (c == '/') {
return GLYPH_SLASH;
}
if (c == ';') {
return GLYPH_SEMICOLON;
}
if (c == '=') {
return GLYPH_EQUALS;
}
if (c == '+') {
return GLYPH_PLUS;
}
if (c == '_') {
return GLYPH_UNDERSCORE;
}
return GLYPH_UNKNOWN;
}
@ -396,13 +430,13 @@ static int glyphIdx(char c) {
static void initialPaint(jlSurfaceT *screen) {
int16_t col;
int16_t row;
jlKeyE key;
jlKeyE key;
jlSurfaceClear(screen, COLOR_BACKGROUND);
for (row = 0; row < GRID_ROWS; row++) {
for (col = 0; col < GRID_COLS; col++) {
key = gKeyGrid[row][col];
if (key == KEY_NONE) {
if (key == JL_KEY_NONE) {
continue;
}
drawCell(screen, col, row, false);
@ -419,9 +453,9 @@ static void initialPaint(jlSurfaceT *screen) {
// when it cannot read the framebuffer (see verify-amiga-input.sh).
static void logTypedHistory(void) {
static const char kHex[] = "0123456789ABCDEF";
char line[2 * MAX_RECEIVED + 8];
int16_t i;
int16_t n;
char line[2 * MAX_RECEIVED + 8];
int16_t i;
int16_t n;
n = 0;
for (i = 0; i < gReceivedCount; i++) {
@ -439,23 +473,20 @@ static void logTypedHistory(void) {
// drawMouseVerifyRow stamps them. Sampled once per use so the drawn
// row and updateMouseVerifyRow's change check can never disagree.
static uint16_t mouseFlags(void) {
return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0) |
(jlMouseDown(MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0) |
(jlMouseDown(MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0) |
(jlMouseDown(MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0));
return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0) | (jlMouseDown(JL_MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0) | (jlMouseDown(JL_MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0) | (jlMouseDown(JL_MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0));
}
static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) {
int16_t col;
int16_t row;
jlKeyE key;
jlKeyE key;
bool lit;
for (row = 0; row < GRID_ROWS; row++) {
for (col = 0; col < GRID_COLS; col++) {
key = gKeyGrid[row][col];
if (key == KEY_NONE) {
if (key == JL_KEY_NONE) {
continue;
}
lit = cellTargetLit(col, row, cursorCol, cursorRow);
@ -513,7 +544,7 @@ static void processTypedChars(jlSurfaceT *screen) {
// this region).
static void redrawTextStrip(jlSurfaceT *screen) {
gLine[gLineLen] = '\0';
jlFillRect(screen, 0, TEXT_STRIP_Y, SURFACE_WIDTH, SURFACE_HEIGHT - TEXT_STRIP_Y, COLOR_BACKGROUND);
jlFillRect(screen, 0, TEXT_STRIP_Y, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT - TEXT_STRIP_Y, COLOR_BACKGROUND);
drawTextLine(screen, TEXT_X, TEXT_Y, "TYPE HERE:", COLOR_UNLIT);
drawTextLine(screen, TEXT_INPUT_X, TEXT_Y, gLine, COLOR_CURSOR);
drawVerifyRow(screen);
@ -593,12 +624,13 @@ static void updateMouseVerifyRow(jlSurfaceT *screen) {
int main(void) {
jlConfigT config;
jlSurfaceT *screen;
jlConfigT config;
jlSurfaceT *screen;
int16_t cursorCol;
int16_t cursorRow;
config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
@ -613,13 +645,13 @@ int main(void) {
}
buildPalette(screen);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
initialPaint(screen);
jlInputPoll();
for (;;) {
jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) {
if (jlKeyPressed(JL_KEY_ESCAPE)) {
break;
}
cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow);

View file

@ -41,10 +41,10 @@ static void timeChanges(const char *name, bool scb);
// Paint the bands, make the two changes, and hold the screen.
static void check(void) {
static const uint16_t firsts[BANDS] = { 0xF00, 0x0F0, 0x00F, 0xFFF };
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
jlSurfaceT *stage;
uint16_t band;
uint16_t frame;
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
jlSurfaceT *stage;
uint16_t band;
uint16_t frame;
stage = jlStageGet();
for (band = 0; band < BANDS; band++) {
@ -52,7 +52,7 @@ static void check(void) {
colors[PAINT_COLOR] = firsts[band];
jlPaletteSet(stage, (uint8_t)band, colors);
jlScbSetRange(stage, (uint16_t)(band * BAND_LINES), (uint16_t)(band * BAND_LINES + BAND_LINES - 1u), (uint8_t)band);
jlFillRect(stage, 0, (int16_t)(band * BAND_LINES), SURFACE_WIDTH, BAND_LINES, PAINT_COLOR);
jlFillRect(stage, 0, (int16_t)(band * BAND_LINES), JL_SURFACE_WIDTH, BAND_LINES, PAINT_COLOR);
}
jlStagePresent();
jlWaitVBL();
@ -71,7 +71,7 @@ static void check(void) {
// PROBE_FRAMES frames of palette changes (or SCB changes), each presented.
static void timeChanges(const char *name, bool scb) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
jlSurfaceT *stage;
uint16_t start;
uint32_t ms0;
@ -105,13 +105,14 @@ int main(void) {
uint16_t band;
cfg.codegenBytes = 0;
cfg.noAudio = true;
if (!jlInit(&cfg)) {
return 1;
}
stage = jlStageGet();
for (band = 0; band < BANDS; band++) {
jlScbSetRange(stage, (uint16_t)(band * BAND_LINES), (uint16_t)(band * BAND_LINES + BAND_LINES - 1u), (uint8_t)band);
jlFillRect(stage, 0, (int16_t)(band * BAND_LINES), SURFACE_WIDTH, BAND_LINES, (uint8_t)(band + 1u));
jlFillRect(stage, 0, (int16_t)(band * BAND_LINES), JL_SURFACE_WIDTH, BAND_LINES, (uint8_t)(band + 1u));
}
jlStagePresent();
timeChanges("palette+present", false);

View file

@ -16,9 +16,9 @@
#include <joey/joey.h>
#define BAND_COUNT 8
#define BAND_HEIGHT (SURFACE_HEIGHT / BAND_COUNT)
#define BAND_HEIGHT (JL_SURFACE_HEIGHT / BAND_COUNT)
#define STRIPE_COUNT 16
#define STRIPE_WIDTH (SURFACE_WIDTH / STRIPE_COUNT)
#define STRIPE_WIDTH (JL_SURFACE_WIDTH / STRIPE_COUNT)
static void buildPalettes(jlSurfaceT *screen);
static void buildScbs(jlSurfaceT *screen);
@ -27,7 +27,7 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn);
static void buildPalettes(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
// Palette 0: grayscale
makeGradient(colors, 1, 1, 1);
@ -83,7 +83,7 @@ static void drawStripes(jlSurfaceT *screen) {
jlSurfaceClear(screen, 0);
for (colorIndex = 0; colorIndex < STRIPE_COUNT; colorIndex++) {
x = (int16_t)(colorIndex * STRIPE_WIDTH);
jlFillRect(screen, x, 0, STRIPE_WIDTH, SURFACE_HEIGHT, colorIndex);
jlFillRect(screen, x, 0, STRIPE_WIDTH, JL_SURFACE_HEIGHT, colorIndex);
}
}
@ -94,7 +94,7 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn) {
uint8_t g;
uint8_t b;
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
r = (uint8_t)(redOn ? i : 0);
g = (uint8_t)(greenOn ? i : 0);
b = (uint8_t)(blueOn ? i : 0);
@ -106,10 +106,11 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn) {
int main(void) {
jlConfigT config;
jlSurfaceT *screen;
jlConfigT config;
jlSurfaceT *screen;
config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());

View file

@ -54,10 +54,10 @@ static bool storeCount(uint16_t count);
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i;
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = 0x0000;
}
colors[COLOR_BACKGROUND] = 0x0000; // black
@ -125,6 +125,7 @@ int main(void) {
bool saved;
config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
@ -143,7 +144,7 @@ int main(void) {
saved = storeCount(count);
buildPalette(screen);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
jlSurfaceClear(screen, COLOR_BACKGROUND);
drawBits(screen, count);
drawLamp(screen, saved);
@ -153,10 +154,10 @@ int main(void) {
for (;;) {
jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) {
if (jlKeyPressed(JL_KEY_ESCAPE)) {
break;
}
if (jlKeyPressed(KEY_SPACE)) {
if (jlKeyPressed(JL_KEY_SPACE)) {
// Erase the save and reset the counter to zero.
jlSaveDelete(SAVE_NAME);
count = 0u;

View file

@ -45,7 +45,7 @@
// ----- Constants -----
#define STRIPE_WIDTH 16u // two bytes of a plane: whole-byte fills
#define STRIPE_COUNT (SURFACE_WIDTH / STRIPE_WIDTH)
#define STRIPE_COUNT (JL_SURFACE_WIDTH / STRIPE_WIDTH)
#define COLOR_CHANNEL_MASK 0x0Fu
#define RED_SHIFT 8u
#define GREEN_SHIFT 4u
@ -75,13 +75,13 @@ static bool escapeTyped(void);
// ----- Helpers (alphabetical) -----
static void buildPalettes(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t pal;
uint16_t col;
uint16_t blue;
for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) {
for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) {
for (pal = 0u; pal < JL_SURFACE_PALETTE_COUNT; pal++) {
for (col = 0u; col < JL_SURFACE_COLORS_PER_PALETTE; col++) {
blue = (uint16_t)((pal * BLUE_PALETTE_STEP + col) & COLOR_CHANNEL_MASK);
colors[col] = (uint16_t)((pal << RED_SHIFT) | (col << GREEN_SHIFT) | blue);
}
@ -97,7 +97,7 @@ static void buildScb(jlSurfaceT *screen) {
uint8_t runPalette;
for (line = 0u; line < EVERY_LINE_END; line++) {
jlScbSet(screen, line, (uint8_t)(line % SURFACE_PALETTE_COUNT));
jlScbSet(screen, line, (uint8_t)(line % JL_SURFACE_PALETTE_COUNT));
}
run = 0u;
runLeft = 0u;
@ -105,13 +105,13 @@ static void buildScb(jlSurfaceT *screen) {
for (line = EVERY_LINE_END; line < IRREGULAR_END; line++) {
if (runLeft == 0u) {
runLeft = (uint16_t)(1u + (run * RUN_LENGTH_STEP) % RUN_LENGTH_SPAN);
runPalette = (uint8_t)((run * RUN_PALETTE_STEP + RUN_PALETTE_BASE) % SURFACE_PALETTE_COUNT);
runPalette = (uint8_t)((run * RUN_PALETTE_STEP + RUN_PALETTE_BASE) % JL_SURFACE_PALETTE_COUNT);
run++;
}
jlScbSet(screen, line, runPalette);
runLeft--;
}
for (line = IRREGULAR_END; line < SURFACE_HEIGHT; line++) {
for (line = IRREGULAR_END; line < JL_SURFACE_HEIGHT; line++) {
jlScbSet(screen, line, (uint8_t)(((line & 1u) != 0u) ? ODD_LINE_PALETTE : EVEN_LINE_PALETTE));
}
}
@ -121,7 +121,7 @@ static void drawStripes(jlSurfaceT *screen) {
uint16_t stripe;
for (stripe = 0u; stripe < STRIPE_COUNT; stripe++) {
jlFillRect(screen, (int16_t)(stripe * STRIPE_WIDTH), 0, STRIPE_WIDTH, SURFACE_HEIGHT, (uint8_t)(stripe % SURFACE_COLORS_PER_PALETTE));
jlFillRect(screen, (int16_t)(stripe * STRIPE_WIDTH), 0, STRIPE_WIDTH, JL_SURFACE_HEIGHT, (uint8_t)(stripe % JL_SURFACE_COLORS_PER_PALETTE));
}
}
@ -146,6 +146,7 @@ int main(void) {
jlSurfaceT *screen;
config.codegenBytes = CODEGEN_DEFAULT;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return EXIT_FAILURE;

View file

@ -79,17 +79,17 @@ typedef struct KeySendT {
} KeySendT;
static const KeySendT gKeySend[] = {
{ KEY_A, 'A' }, { KEY_B, 'B' }, { KEY_C, 'C' }, { KEY_D, 'D' },
{ KEY_E, 'E' }, { KEY_F, 'F' }, { KEY_G, 'G' }, { KEY_H, 'H' },
{ KEY_I, 'I' }, { KEY_J, 'J' }, { KEY_K, 'K' }, { KEY_L, 'L' },
{ KEY_M, 'M' }, { KEY_N, 'N' }, { KEY_O, 'O' }, { KEY_P, 'P' },
{ KEY_Q, 'Q' }, { KEY_R, 'R' }, { KEY_S, 'S' }, { KEY_T, 'T' },
{ KEY_U, 'U' }, { KEY_V, 'V' }, { KEY_W, 'W' }, { KEY_X, 'X' },
{ KEY_Y, 'Y' }, { KEY_Z, 'Z' },
{ KEY_0, '0' }, { KEY_1, '1' }, { KEY_2, '2' }, { KEY_3, '3' },
{ KEY_4, '4' }, { KEY_5, '5' }, { KEY_6, '6' }, { KEY_7, '7' },
{ KEY_8, '8' }, { KEY_9, '9' },
{ KEY_RETURN, '\r' }
{ JL_KEY_A, 'A' }, { JL_KEY_B, 'B' }, { JL_KEY_C, 'C' }, { JL_KEY_D, 'D' },
{ JL_KEY_E, 'E' }, { JL_KEY_F, 'F' }, { JL_KEY_G, 'G' }, { JL_KEY_H, 'H' },
{ JL_KEY_I, 'I' }, { JL_KEY_J, 'J' }, { JL_KEY_K, 'K' }, { JL_KEY_L, 'L' },
{ JL_KEY_M, 'M' }, { JL_KEY_N, 'N' }, { JL_KEY_O, 'O' }, { JL_KEY_P, 'P' },
{ JL_KEY_Q, 'Q' }, { JL_KEY_R, 'R' }, { JL_KEY_S, 'S' }, { JL_KEY_T, 'T' },
{ JL_KEY_U, 'U' }, { JL_KEY_V, 'V' }, { JL_KEY_W, 'W' }, { JL_KEY_X, 'X' },
{ JL_KEY_Y, 'Y' }, { JL_KEY_Z, 'Z' },
{ JL_KEY_0, '0' }, { JL_KEY_1, '1' }, { JL_KEY_2, '2' }, { JL_KEY_3, '3' },
{ JL_KEY_4, '4' }, { JL_KEY_5, '5' }, { JL_KEY_6, '6' }, { JL_KEY_7, '7' },
{ JL_KEY_8, '8' }, { JL_KEY_9, '9' },
{ JL_KEY_RETURN, '\r' }
};
#define KEY_SEND_COUNT ((int16_t)(sizeof(gKeySend) / sizeof(gKeySend[0])))
@ -107,10 +107,10 @@ static void sendBytes(const char *bytes, uint16_t len);
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i;
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = 0x0000;
}
colors[COLOR_BACKGROUND] = 0x0000; // black
@ -186,6 +186,7 @@ int main(void) {
bool linkOpen;
config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
@ -205,14 +206,14 @@ int main(void) {
serialCfg.parity = SERIAL_PARITY;
serialCfg.flow = SERIAL_FLOW;
serialCfg.unit = 0u;
linkOpen = jlSerialOpen(JL_SERIAL_DEFAULT, &serialCfg);
linkOpen = jlSerialOpen(JL_SERIAL_DEFAULT, &serialCfg);
if (linkOpen) {
// Greet on connect -- also a TX heartbeat that a loopback peer can see.
sendBytes(gBanner, (uint16_t)(sizeof(gBanner) - 1u));
}
buildPalette(screen);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
jlSurfaceClear(screen, COLOR_BACKGROUND);
drawBits(screen, gLastByte);
drawLamp(screen, LAMP_TX_X, false, COLOR_TX_ON);
@ -229,7 +230,7 @@ int main(void) {
bool dirty;
jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) {
if (jlKeyPressed(JL_KEY_ESCAPE)) {
break;
}
jlSerialPoll();
@ -238,7 +239,7 @@ int main(void) {
dirty = false;
// Transmit: a banner on SPACE, otherwise the pressed key's ASCII.
if (jlKeyPressed(KEY_SPACE)) {
if (jlKeyPressed(JL_KEY_SPACE)) {
sendBytes(gBanner, (uint16_t)(sizeof(gBanner) - 1u));
txFired = true;
} else {
@ -256,7 +257,7 @@ int main(void) {
if (got > 0u) {
gLastByte = (int16_t)rx[got - 1u];
drawBits(screen, gLastByte);
dirty = true;
dirty = true;
gRxTimer = BLINK_FRAMES;
sendBytes((const char *)rx, got); // echo
}

View file

@ -25,7 +25,8 @@
// spread is read jitter
// SERLOSS_SFX also keep a sound effect streaming and retrigger a one-shot
// effect once per frame, so NinjaTrackerPlus's entry calls and
// its DOC refill interrupt run throughout the receive
// its DOC refill interrupt run throughout the receive; the
// stream's refills are counted, the proof audio is running
// SERLOSS_LOOSE drop the serial add-on's hold on the tight interrupt-latency
// bound once the port is open: the negative control, which
// must lose bytes wherever a masked stretch outlasts the FIFO
@ -57,6 +58,7 @@
#define MB_JOY_MIN_Y 30
#define MB_JOY_MAX_Y 31
#define MB_SFX_SHOTS 32
#define MB_SFX_FILLS 34
#define MB_SIG_VALUE_0 0xA5u
#define MB_SIG_VALUE_1 0x5Au
#define MB_JOY_BIAS 128
@ -115,8 +117,9 @@ typedef struct {
#endif
#ifdef SERLOSS_SFX
static int8_t gSfxShot[SFX_SHOT_BYTES];
static uint8_t gSfxWave[SFX_WAVE_BYTES];
static int8_t gSfxShot[SFX_SHOT_BYTES];
static uint8_t gSfxWave[SFX_WAVE_BYTES];
static uint16_t gSfxFills = 0u;
#endif
static uint16_t benchLoop(jlSurfaceT *stage, bool present);
@ -126,7 +129,7 @@ static void joyPoll(JoySpreadT *spread);
static void mailbox16(uint16_t slot, uint16_t value);
static void publish(uint16_t received, uint16_t inSequence, uint16_t breaks, uint16_t presents, uint16_t ticks, uint16_t sum, uint16_t now);
#ifdef SERLOSS_SFX
static bool sfxStart(void);
static void sfxStart(void);
static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count);
#endif
@ -156,8 +159,8 @@ static void joyPoll(JoySpreadT *spread) {
int8_t y;
jlInputPoll();
x = jlJoystickX(JOYSTICK_0);
y = jlJoystickY(JOYSTICK_0);
x = jlJoystickX(JL_JOYSTICK_0);
y = jlJoystickY(JL_JOYSTICK_0);
if (spread->polls == 0u || x < spread->minX) {
spread->minX = x;
}
@ -200,14 +203,11 @@ __attribute__((noinline)) static void publish(uint16_t received, uint16_t inSequ
#ifdef SERLOSS_SFX
// Bring up audio, build the one-shot's square wave and the stream's sawtooth, and start the
// endless stream.
static bool sfxStart(void) {
// Build the one-shot's square wave and the stream's sawtooth, and start the endless stream
// (jlInit started audio).
static void sfxStart(void) {
uint16_t i;
if (!jlAudioInit()) {
return false;
}
for (i = 0u; i < SFX_SHOT_BYTES; i++) {
gSfxShot[i] = (int8_t)(((i / (SFX_SHOT_PERIOD / 2u)) & 1u) != 0u ? SFX_LEVEL : -SFX_LEVEL);
}
@ -219,15 +219,17 @@ static bool sfxStart(void) {
// overflow the receive ring.
jlAudioPlaySfx((uint8_t)SFX_SHOT_SLOT, (const uint8_t *)gSfxShot, SFX_SHOT_BYTES, SFX_SHOT_RATE);
jlAudioPlaySfxStreamRaw((uint8_t)SFX_STREAM_SLOT, sfxStreamFill, (void *)0, SFX_STREAM_RATE);
return true;
}
// Endless sawtooth, whole periods: never returns 0, so the stream runs until the program ends.
// Only a running engine asks for refills, so their count proves audio is up.
static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count) {
uint32_t done;
(void)ctx;
gSfxFills++;
mailbox16(MB_SFX_FILLS, gSfxFills);
done = 0u;
while (count - done >= SFX_WAVE_BYTES) {
jlMemCopy(dst + done, gSfxWave, SFX_WAVE_BYTES);
@ -269,6 +271,11 @@ int main(void) {
#endif
config.codegenBytes = 0u;
#ifdef SERLOSS_SFX
config.noAudio = false;
#else
config.noAudio = true;
#endif
if (!jlInit(&config)) {
MB_BASE[MB_PHASE] = PHASE_FAILED;
for (;;) {
@ -280,15 +287,12 @@ int main(void) {
MB_BASE[MB_PHASE] = PHASE_INIT;
mailbox16(MB_JOY_POLLS, 0u);
mailbox16(MB_SFX_SHOTS, 0u);
mailbox16(MB_SFX_FILLS, 0u);
stage = jlStageGet();
mailbox16(MB_FILL_TICKS, benchLoop(stage, false));
mailbox16(MB_PRESENT_TICKS, benchLoop(stage, true));
#ifdef SERLOSS_SFX
if (!sfxStart()) {
MB_BASE[MB_PHASE] = PHASE_FAILED;
for (;;) {
}
}
sfxStart();
shots = 0u;
#endif
@ -316,7 +320,7 @@ int main(void) {
#ifdef SERLOSS_JOY
// Calibrate on the first poll, in the latency mode the rest will run in, so a still stick
// reads 0 from then on.
jlJoystickReset(JOYSTICK_0, 0u);
jlJoystickReset(JL_JOYSTICK_0, 0u);
spread.polls = 0u;
joyPoll(&spread);
#endif

View file

@ -142,10 +142,10 @@ static void updateMouseRow(jlSurfaceT *screen, MouseRowT *last);
// A grey ramp, so every index the verification rows stamp is visible.
static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i;
for (i = 0u; i < SURFACE_COLORS_PER_PALETTE; i++) {
for (i = 0u; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = (uint16_t)((i << PALETTE_RED_SHIFT) | (i << PALETTE_GREEN_SHIFT) | i);
}
jlPaletteSet(screen, GREY_PALETTE, colors);
@ -155,15 +155,15 @@ static void buildPalette(jlSurfaceT *screen) {
// The grey palette everywhere, or DENSE_SCB_LINES lines alternating between
// it and the same ramp in blue.
static void buildScb(jlSurfaceT *screen) {
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, GREY_PALETTE);
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, GREY_PALETTE);
#if DENSE_SCB_LINES > 0
{
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i;
uint16_t line;
jlPaletteGet(screen, GREY_PALETTE, colors);
for (i = 1u; i < SURFACE_COLORS_PER_PALETTE; i++) {
for (i = 1u; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
colors[i] = (uint16_t)(colors[i] | BLUE_FLOOR);
}
jlPaletteSet(screen, BLUE_PALETTE, colors);
@ -183,8 +183,8 @@ static void fillPointer(jlSurfaceT *screen, int16_t x, int16_t y, uint8_t color)
w = POINTER_SIZE;
h = POINTER_SIZE;
if ((int16_t)(x + w) > SURFACE_WIDTH) {
w = (int16_t)(SURFACE_WIDTH - x);
if ((int16_t)(x + w) > JL_SURFACE_WIDTH) {
w = (int16_t)(JL_SURFACE_WIDTH - x);
}
if ((int16_t)(y + h) > SERIAL_ROW_Y) {
h = (int16_t)(SERIAL_ROW_Y - y);
@ -197,10 +197,7 @@ static void fillPointer(jlSurfaceT *screen, int16_t x, int16_t y, uint8_t color)
static uint16_t mouseFlags(void) {
return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0u) |
(jlMouseDown(MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0u) |
(jlMouseDown(MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0u) |
(jlMouseDown(MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0u));
return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0u) | (jlMouseDown(JL_MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0u) | (jlMouseDown(JL_MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0u) | (jlMouseDown(JL_MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0u));
}
@ -334,6 +331,7 @@ int main(void) {
bool paused;
config.codegenBytes = 0u;
config.noAudio = true;
if (!jlInit(&config)) {
return 1;
}
@ -371,10 +369,10 @@ int main(void) {
for (;;) {
jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) {
if (jlKeyPressed(JL_KEY_ESCAPE)) {
break;
}
if (jlKeyPressed(KEY_SPACE)) {
if (jlKeyPressed(JL_KEY_SPACE)) {
paused = !paused;
}
receiveStream(&stats);

View file

@ -66,7 +66,7 @@ static void sendMark(const char *text) {
int main(void) {
jlSerialConfigT cfg;
jlSerialConfigT cfg;
static const char banner[] = "JLSERTEST-READY\r\n";
cfg.baud = 9600u;

View file

@ -7,7 +7,9 @@
// per source line) and converted at startup into the 2x2 tile layout
// jlSpriteT expects. jlSpriteDraw handles transparent-color-0; we use
// jlSpriteSaveUnder/RestoreUnder to undraw the previous frame's ball
// without redrawing the whole screen.
// without redrawing the whole screen. The ball's speed is given per
// second and each frame moves it by the time since the last one
// (jlMillisElapsed), so it crosses the screen as fast on every port.
#include <stdio.h>
@ -18,17 +20,20 @@
#define BALL_TILES_X (BALL_W / 8)
#define BALL_TILES_Y (BALL_H / 8)
#define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * TILE_BYTES)
// SaveUnder rounds x down to the platform's storage alignment: 2 px
// for chunky 4bpp (1 extra byte/row worst case), 8 px for planar
// 4-plane (4 extra bytes/row worst case -- one per plane). The +4
// covers the planar case and is a no-op overhead on chunky.
// Sized by the library-wide worst case (16-px-group window on the
// planar ports); see JOEY_SPRITE_BACKUP_BYTES in joey/sprite.h.
#define BALL_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(BALL_W / 8, BALL_H / 8)
#define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * JL_TILE_BYTES)
// The save-under buffer is sized by jlSpriteBackupLength (joey/sprite.h):
// the worst case at any x, on every port.
#define BALL_PALETTE_IDX 0
#define BALL_SPEED_X 120 // pixels per second, the same on every port
#define BALL_SPEED_Y 60
#define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel
#define MAX_STEP_MILLIS 100 // after a stall, move no further than this
#define MILLIS_PER_SECOND 1000
#define BALL_X_MAX ((int32_t)(JL_SURFACE_WIDTH - BALL_W) << SUBPIXEL_SHIFT)
#define BALL_Y_MAX ((int32_t)(JL_SURFACE_HEIGHT - BALL_H) << SUBPIXEL_SHIFT)
#define COLOR_BG 0
// Authored layout: 16 pixels wide x 16 rows = 8 bytes/row x 16 rows.
@ -56,7 +61,7 @@ static const uint8_t gBallAuthored[BALL_H * (BALL_W / 2)] = {
};
static uint8_t gBallTiles[BALL_TILE_BYTES];
static uint8_t gBallBackup[BALL_BACKUP_BYTES] __attribute__((aligned(2)));
static uint16_t gBallBackup[jlSpriteBackupLength(uint16_t, BALL_W / 8, BALL_H / 8)];
// Repack from authored "wide row" layout to tile-major jlSpriteT
@ -71,12 +76,10 @@ static void repackBallTiles(void) {
for (ty = 0; ty < BALL_TILES_Y; ty++) {
for (tx = 0; tx < BALL_TILES_X; tx++) {
dst = &gBallTiles[(ty * BALL_TILES_X + tx) * TILE_BYTES];
dst = &gBallTiles[(ty * BALL_TILES_X + tx) * JL_TILE_BYTES];
for (row = 0; row < 8; row++) {
for (b = 0; b < 4; b++) {
dst[row * 4 + b] =
gBallAuthored[((ty * 8) + row) * (BALL_W / 2) +
(tx * 4) + b];
dst[row * 4 + b] = gBallAuthored[((ty * 8) + row) * (BALL_W / 2) + (tx * 4) + b];
}
}
}
@ -98,21 +101,24 @@ static void buildPalette(jlSurfaceT *screen) {
int main(void) {
jlConfigT config;
jlConfigT config;
jlSurfaceT *screen;
jlSpriteT *ball;
jlSpriteBackupT backup;
int16_t x;
int16_t y;
int16_t vx;
int16_t vy;
bool haveBackup;
int32_t x; // 1/256 of a pixel
int32_t y;
int32_t vx; // pixels per second
int32_t vy;
uint32_t lastMillis;
uint32_t now;
uint32_t elapsed;
/* Amiga planar emits 8 pre-shifted DRAW variants per sprite (one
* per x % 8 alignment) so the codegen arena needs roughly 8x what
* the chunky two-shift case asks for. 32 KB fits a 16x16 ball
* with all variants. */
config.codegenBytes = 32UL * 1024;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
@ -140,25 +146,24 @@ int main(void) {
(void)jlSpriteCompile(ball);
buildPalette(screen);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, BALL_PALETTE_IDX);
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, BALL_PALETTE_IDX);
jlSurfaceClear(screen, COLOR_BG);
jlStagePresent();
backup.bytes = gBallBackup;
backup.bytes = (uint8_t *)gBallBackup;
x = 40;
y = 30;
vx = 2;
vy = 1;
haveBackup = false;
x = (int32_t)40 << SUBPIXEL_SHIFT;
y = (int32_t)30 << SUBPIXEL_SHIFT;
vx = BALL_SPEED_X;
vy = BALL_SPEED_Y;
jlSpriteSaveAndDraw(screen, ball, x, y, &backup);
jlSpriteSaveAndDraw(screen, ball, (int16_t)(x >> SUBPIXEL_SHIFT), (int16_t)(y >> SUBPIXEL_SHIFT), &backup);
jlStagePresent();
haveBackup = true;
lastMillis = jlMillisElapsed();
for (;;) {
jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) {
if (jlKeyPressed(JL_KEY_ESCAPE)) {
break;
}
@ -170,22 +175,41 @@ int main(void) {
// chunky targets like IIgs SHR; on planar c2p platforms it
// also avoids c2p racing the raster). VBL wait is omitted
// here so the demo runs at the sprite pipeline's native
// throughput -- expect tearing on the ball.
if (haveBackup) {
jlSpriteRestoreUnder(screen, &backup);
// throughput -- expect tearing on the ball. The ball's speed
// does not depend on it: it moves by the time each frame took.
jlSpriteRestoreUnder(screen, &backup);
now = jlMillisElapsed();
elapsed = now - lastMillis;
lastMillis = now;
if (elapsed > MAX_STEP_MILLIS) {
elapsed = MAX_STEP_MILLIS;
}
x += (vx * (int32_t)elapsed * ((int32_t)1 << SUBPIXEL_SHIFT)) / MILLIS_PER_SECOND;
y += (vy * (int32_t)elapsed * ((int32_t)1 << SUBPIXEL_SHIFT)) / MILLIS_PER_SECOND;
// A wall sets the direction rather than flipping it, so a frame
// that moved nothing cannot turn the ball back twice.
if (x <= 0) {
x = 0;
vx = BALL_SPEED_X;
}
if (x >= BALL_X_MAX) {
x = BALL_X_MAX;
vx = -BALL_SPEED_X;
}
if (y <= 0) {
y = 0;
vy = BALL_SPEED_Y;
}
if (y >= BALL_Y_MAX) {
y = BALL_Y_MAX;
vy = -BALL_SPEED_Y;
}
x = (int16_t)(x + vx);
y = (int16_t)(y + vy);
if (x <= 0) { x = 0; vx = (int16_t)-vx; }
if (x >= SURFACE_WIDTH - BALL_W) { x = SURFACE_WIDTH - BALL_W; vx = (int16_t)-vx; }
if (y <= 0) { y = 0; vy = (int16_t)-vy; }
if (y >= SURFACE_HEIGHT - BALL_H) { y = SURFACE_HEIGHT - BALL_H; vy = (int16_t)-vy; }
jlSpriteSaveAndDraw(screen, ball, x, y, &backup);
jlSpriteSaveAndDraw(screen, ball, (int16_t)(x >> SUBPIXEL_SHIFT), (int16_t)(y >> SUBPIXEL_SHIFT), &backup);
jlStagePresent();
haveBackup = true;
}
jlSpriteDestroy(ball);

View file

@ -58,10 +58,10 @@
// expect.bin layout (see the header comment).
#define LE16_BYTES 2
#define BYTE_BITS 8
#define PALETTE_COLORS SURFACE_COLORS_PER_PALETTE
#define PALETTE_COLORS JL_SURFACE_COLORS_PER_PALETTE
#define EXPECT_OFF_PALETTE LE16_BYTES
#define EXPECT_HEADER_BYTES (EXPECT_OFF_PALETTE + PALETTE_COLORS * LE16_BYTES)
#define TILE_PIXELS (TILE_PIXELS_PER_SIDE * TILE_PIXELS_PER_SIDE)
#define TILE_PIXELS (JL_TILE_PIXELS_PER_SIDE * JL_TILE_PIXELS_PER_SIDE)
// The stage is cleared to this before every load. It MUST equal FILL_COLOR in
// tests/host/tileBankHost.c: the host's hash is of a surface cleared to it.
@ -74,8 +74,8 @@
// the lamp colours cannot collide with the bank's sixteen.
#define LAMP_W 64
#define LAMP_H 64
#define LAMP_X ((SURFACE_WIDTH - LAMP_W) / 2)
#define LAMP_Y ((SURFACE_HEIGHT - LAMP_H) / 2)
#define LAMP_X ((JL_SURFACE_WIDTH - LAMP_W) / 2)
#define LAMP_Y ((JL_SURFACE_HEIGHT - LAMP_H) / 2)
#define LAMP_PALETTE 1u
#define LAMP_PASS_COLOR 1u
#define LAMP_FAIL_COLOR 2u
@ -145,7 +145,7 @@ static void pasteTiles(uint16_t count) {
jlSurfaceClear(gScreen, FILL_COLOR);
for (i = 0u; i < count; i++) {
jlTilePaste(gScreen, (uint8_t)(i % TILE_BLOCKS_PER_ROW), (uint8_t)(i / TILE_BLOCKS_PER_ROW), &gTiles[i]);
jlTilePaste(gScreen, (uint8_t)(i % JL_TILE_BLOCKS_PER_ROW), (uint8_t)(i / JL_TILE_BLOCKS_PER_ROW), &gTiles[i]);
}
}
@ -275,12 +275,12 @@ static bool stageMatches(uint16_t tilesDrawn) {
uint16_t block;
uint8_t want;
for (y = 0; y < SURFACE_HEIGHT; y++) {
for (x = 0; x < SURFACE_WIDTH; x++) {
block = (uint16_t)((y / TILE_PIXELS_PER_SIDE) * TILE_BLOCKS_PER_ROW + x / TILE_PIXELS_PER_SIDE);
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
block = (uint16_t)((y / JL_TILE_PIXELS_PER_SIDE) * JL_TILE_BLOCKS_PER_ROW + x / JL_TILE_PIXELS_PER_SIDE);
want = FILL_COLOR;
if (block < tilesDrawn) {
want = gExpectPixels[(uint32_t)block * TILE_PIXELS + (y % TILE_PIXELS_PER_SIDE) * TILE_PIXELS_PER_SIDE + x % TILE_PIXELS_PER_SIDE];
want = gExpectPixels[(uint32_t)block * TILE_PIXELS + (y % JL_TILE_PIXELS_PER_SIDE) * JL_TILE_PIXELS_PER_SIDE + x % JL_TILE_PIXELS_PER_SIDE];
}
if (jlSamplePixel(gScreen, x, y) != want) {
return false;
@ -296,6 +296,7 @@ int main(void) {
bool pass;
config.codegenBytes = CODEGEN_DEFAULT;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return EXIT_FAILURE;

View file

@ -74,9 +74,9 @@
// +1 (9DC9) uint8 phase (UBER_PHASE_*)
// +2 (9DCA) uint16 heartbeat (one increment per timing-loop pass)
// +4 (9DCC) uint16 last jlFrameCount() value the timing loop saw
// No-ops on every other port.
// No-ops on every other port. Phase 2 was UBER's own audio start, which
// jlInit does now.
#define UBER_PHASE_SETUP_SPRITE 1u
#define UBER_PHASE_AUDIO_INIT 2u
#define UBER_PHASE_SHOWCASE 3u
#define UBER_PHASE_TIMED_RUN 4u
#define UBER_PHASE_CHECKS 5u
@ -137,10 +137,10 @@ extern uint16_t gJoeyLogRingHead;
typedef void (*OpFn)(void);
static jlSurfaceT *gStage = NULL;
static jlSpriteT *gSprite = NULL;
static jlSurfaceT *gStage = NULL;
static jlSpriteT *gSprite = NULL;
static jlSpriteBackupT gBackup;
static unsigned char gBackupBytes[JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2)));
static unsigned char gBackupBytes[jlSpriteBackupLength(unsigned char, 2, 2)] __attribute__((aligned(2)));
static jlTileT gTileScratch;
@ -154,7 +154,7 @@ static jlTileT gTileMapB;
#define UBER_FRAME_SPRITES 3
static jlSpriteBackupT gFrameBackups[UBER_FRAME_SPRITES];
static unsigned char gFrameBackupBytes[UBER_FRAME_SPRITES][JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2)));
static unsigned char gFrameBackupBytes[UBER_FRAME_SPRITES][jlSpriteBackupLength(unsigned char, 2, 2)] __attribute__((aligned(2)));
// Current timed-op number (1-based); mirrored into the mailbox. Lives
@ -239,11 +239,11 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *
endMillis = jlMillisElapsed();
*actualFramesOut = (uint16_t)(endFrame - startFrame);
if (*actualFramesOut == 0u) {
*actualFramesOut = 1u; /* defensive: avoid div-by-zero */
*actualFramesOut = 1u; // defensive: avoid div-by-zero
}
*millisDeltaOut = endMillis - startMillis;
if (*millisDeltaOut == 0u) {
*millisDeltaOut = 1u; /* defensive: avoid div-by-zero */
*millisDeltaOut = 1u; // defensive: avoid div-by-zero
}
return count;
}
@ -285,10 +285,8 @@ static void timeOp(const char *name, OpFn op) {
opsPerSec = (iters * 1000UL) / (unsigned long)millisDelta;
opsPerSecFr = (iters * (unsigned long)jlFrameHz()) / (unsigned long)actualFrames;
hash = jlSurfaceHash(gStage);
jlLogF("UBER: %s: %lu iters / %u frames = %lu ops/sec | hash=%08lX",
name, iters, actualFrames, opsPerSec, (unsigned long)hash);
jlLogF("UBER-CLK: %s: frame=%lu millis=%lu ops/sec (%lu ms)",
name, opsPerSecFr, opsPerSec, (unsigned long)millisDelta);
jlLogF("UBER: %s: %lu iters / %u frames = %lu ops/sec | hash=%08lX", name, iters, actualFrames, opsPerSec, (unsigned long)hash);
jlLogF("UBER-CLK: %s: frame=%lu millis=%lu ops/sec (%lu ms)", name, opsPerSecFr, opsPerSec, (unsigned long)millisDelta);
}
@ -336,15 +334,15 @@ static void op_spriteSaveAndDraw (void) { jlSpriteSaveAndDraw (gStage, gSprite,
static void op_stagePresent (void) { jlStagePresent(); }
static void op_inputPoll (void) { jlInputPoll(); }
static void op_keyDown (void) { (void)jlKeyDown(KEY_A); }
static void op_keyPressed (void) { (void)jlKeyPressed(KEY_A); }
static void op_keyDown (void) { (void)jlKeyDown(JL_KEY_A); }
static void op_keyPressed (void) { (void)jlKeyPressed(JL_KEY_A); }
static void op_mouseX (void) { (void)jlMouseX(); }
static void op_joyConnected (void) { (void)jlJoystickConnected(JOYSTICK_1); }
static void op_joyConnected (void) { (void)jlJoystickConnected(JL_JOYSTICK_1); }
static void op_audioFrameTick (void) { jlAudioFrameTick(); }
static void op_audioIsPlaying (void) { (void)jlAudioIsPlayingMod(); }
static void op_surfaceMarkDirty(void) { /* jlDrawPixel already marks; use fill instead */
static void op_surfaceMarkDirty(void) { // jlDrawPixel already marks; use fill instead
jlFillRect(gStage, 0, 0, 32, 32, 0); }
@ -600,7 +598,7 @@ static void PROBE_ATTR op_probePresentIdle(void) {
// rows in this same log. Stage pixels are never written (saves read;
// the restoreVal call fails validation), so these rows are hash-inert.
static jlSpriteBackupT gProbeSpriteBackup;
static unsigned char gProbeSpriteBackupBytes[JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2)));
static unsigned char gProbeSpriteBackupBytes[jlSpriteBackupLength(unsigned char, 2, 2)] __attribute__((aligned(2)));
static void PROBE_ATTR op_probeSpriteSaveVal(void) {
jlSpriteSaveUnder(gStage, gSprite, -100, -100, &gProbeSpriteBackup);
@ -631,7 +629,7 @@ static void PROBE_ATTR op_probeSpriteSaveG16(void) {
// and gBackup untouched, so the row is inert to the golden hashes.
static jlSurfaceT *gProbeRestoreWork = NULL;
static jlSpriteBackupT gProbeRestoreBackup;
static unsigned char gProbeRestoreBackupBytes[JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2)));
static unsigned char gProbeRestoreBackupBytes[jlSpriteBackupLength(unsigned char, 2, 2)] __attribute__((aligned(2)));
static void PROBE_ATTR op_probeRestoreWork(void) {
jlSpriteRestoreUnder(gProbeRestoreWork, &gProbeRestoreBackup);
@ -682,15 +680,15 @@ static void PROBE_ATTR op_probeDrawTrusted(void) {
// The band arrays are port-neutral, so the value must also be
// IDENTICAL ACROSS PORTS -- the C-mark ports are the reference any
// fused asm mark must reproduce.
extern uint8_t gStageMinWord[SURFACE_HEIGHT];
extern uint8_t gStageMaxWord[SURFACE_HEIGHT];
extern uint8_t gStageMinWord[JL_SURFACE_HEIGHT];
extern uint8_t gStageMaxWord[JL_SURFACE_HEIGHT];
static uint32_t PROBE_ATTR probeBandFold(void) {
uint16_t y;
uint32_t sum;
sum = 0;
for (y = 0; y < SURFACE_HEIGHT; y++) {
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
sum = (uint32_t)(sum * 33u + gStageMinWord[y]);
sum = (uint32_t)(sum * 33u + gStageMaxWord[y]);
}
@ -1042,80 +1040,123 @@ static void PROBE_ATTR op_probeVgaCheck(void) {
#endif
#ifdef JOEYLIB_PLATFORM_DVX
#if defined(JOEYLIB_PLATFORM_DVX) || defined(JOEYLIB_HOSTED_WIN16) || defined(JOEYLIB_HOSTED_WIN32) || defined(JOEYLIB_HOSTED_BE)
#if defined(JOEYLIB_PLATFORM_DVX)
extern uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared);
#define PROBE_WINDOW_MISMATCH(_s, _c) dvxPortProbeDisplayMismatch((_s), (_c))
#define PROBE_WINDOW_TAG "dvxDisplayMismatch"
#elif defined(JOEYLIB_HOSTED_WIN16)
extern uint32_t hostWin16ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared);
#define PROBE_WINDOW_MISMATCH(_s, _c) hostWin16ProbeDisplayMismatch((_s), (_c))
#define PROBE_WINDOW_TAG "win16DisplayMismatch"
#elif defined(JOEYLIB_HOSTED_WIN32)
extern uint32_t hostWin32ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared);
#define PROBE_WINDOW_MISMATCH(_s, _c) hostWin32ProbeDisplayMismatch((_s), (_c))
#define PROBE_WINDOW_TAG "win32DisplayMismatch"
#else
extern uint32_t hostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared);
#define PROBE_WINDOW_MISMATCH(_s, _c) hostBeProbeDisplayMismatch((_s), (_c))
#define PROBE_WINDOW_TAG "beDisplayMismatch"
#endif
// P13: DVX display-side parity, P12's battery on the DVX present, which
// P13: display-side parity in a window, P12's battery on the DVX, Win16,
// Win32, and Be presents. The golden hashes gate the stage, never what is shown. DVX
// expands dirty rows through per-palette tables into the window's content
// buffer (the desktop's pixel format, at the window's scale) or the
// full-screen backbuffer. The golden hashes gate the stage, never those.
// dvxPortProbeDisplayMismatch derives every pixel from the stage's own
// palette entries (not the present's tables) and counts the ones that
// differ. A strip of one-pixel stripes in all 16 colours stays on screen,
// full-screen backbuffer; dvxPortProbeDisplayMismatch derives every pixel
// from the stage's own palette entries (not the present's tables) and
// counts the ones that differ. The hosted backends draw only the
// rectangles that changed at each show; their probes count the frame's
// pixels that differ from the stage and the window's that differ from the
// whole frame drawn afresh. A strip of one-pixel stripes in all 16 colours stays on screen,
// so every table entry, and both pixels of a byte, are checked; the small
// rect is on odd pixels. After P12's steps (tall band, small inner rect,
// settle, idle) two more work the tables: the strip's rows moved to another
// palette (an SCB change: every row again), then that palette changed
// (its rows again; on the whole screen at 8 bits only the screen's
// palette); then the SCBs and the palette go back. Every count must be 0.
// palette); then every row a palette of its own, all 16 in use (more than
// Win16's identity layout holds, so its fallback and the way back); then
// the SCBs and the palette go back; then two rects far
// apart in one present (two changed rectangles in one show). Steps B and
// Two are the ones the hosted backends draw piecemeal. Every count must be
// 0.
#define PROBE_DVX_PALETTE 1u
#define PROBE_DVX_STRIP_H 10
#define PROBE_DVX_SCB_LAST 99u
static void PROBE_ATTR op_probeDvxDisplayCheck(void) {
static const uint16_t probePalette[SURFACE_COLORS_PER_PALETTE] = {
static void PROBE_ATTR op_probeWindowDisplayCheck(void) {
static const uint16_t probePalette[JL_SURFACE_COLORS_PER_PALETTE] = {
0x000, 0xF00, 0x0F0, 0x00F, 0xFF0, 0xF0F, 0x0FF, 0xFFF,
0x800, 0x080, 0x008, 0x880, 0x808, 0x088, 0x888, 0x444
};
static const uint16_t changedPalette[SURFACE_COLORS_PER_PALETTE] = {
static const uint16_t changedPalette[JL_SURFACE_COLORS_PER_PALETTE] = {
0x000, 0x123, 0x246, 0x369, 0x48C, 0x5AF, 0x6C2, 0x7E5,
0x9A1, 0xAB4, 0xBC7, 0xCDA, 0xDEC, 0xE1F, 0xF52, 0x3D8
};
uint8_t savedScb[SURFACE_HEIGHT];
uint16_t savedPalette[SURFACE_COLORS_PER_PALETTE];
uint8_t savedScb[JL_SURFACE_HEIGHT];
uint16_t savedPalette[JL_SURFACE_COLORS_PER_PALETTE];
uint32_t afterB;
uint32_t afterSettle;
uint32_t afterIdle;
uint32_t afterScb;
uint32_t afterPalette;
uint32_t afterAll;
uint32_t afterRestore;
uint32_t afterTwo;
uint32_t compared;
int16_t x;
uint16_t y;
for (y = 0; y < SURFACE_HEIGHT; y++) {
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
savedScb[y] = jlScbGet(gStage, y);
}
jlPaletteGet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette);
jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, probePalette);
// Shown before the steps, so the palette's own full redraw (Win16
// repaints its window when colours change) is behind them; and a frame
// between B's presents, so each is a show of its own (Win16 shows at
// most once a frame, everything changed since the last).
jlStagePresent();
jlWaitVBL();
jlSurfaceClear(gStage, 3);
for (x = 0; x < SURFACE_WIDTH; x++) {
jlFillRect(gStage, x, 0, 1, PROBE_DVX_STRIP_H, (uint8_t)(x % SURFACE_COLORS_PER_PALETTE));
for (x = 0; x < JL_SURFACE_WIDTH; x++) {
jlFillRect(gStage, x, 0, 1, PROBE_DVX_STRIP_H, (uint8_t)(x % JL_SURFACE_COLORS_PER_PALETTE));
}
jlStagePresent();
jlWaitVBL();
jlFillRect(gStage, 0, 50, 320, 100, 3);
jlStagePresent();
jlWaitVBL();
jlFillRect(gStage, 101, 61, 37, 9, 9);
jlStagePresent();
afterB = dvxPortProbeDisplayMismatch(gStage, &compared);
afterB = PROBE_WINDOW_MISMATCH(gStage, &compared);
jlStagePresent();
afterSettle = dvxPortProbeDisplayMismatch(gStage, &compared);
afterSettle = PROBE_WINDOW_MISMATCH(gStage, &compared);
jlStagePresent();
afterIdle = dvxPortProbeDisplayMismatch(gStage, &compared);
afterIdle = PROBE_WINDOW_MISMATCH(gStage, &compared);
jlScbSetRange(gStage, 0, PROBE_DVX_SCB_LAST, (uint8_t)PROBE_DVX_PALETTE);
jlStagePresent();
afterScb = dvxPortProbeDisplayMismatch(gStage, &compared);
afterScb = PROBE_WINDOW_MISMATCH(gStage, &compared);
jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, changedPalette);
jlStagePresent();
afterPalette = dvxPortProbeDisplayMismatch(gStage, &compared);
for (y = 0; y < SURFACE_HEIGHT; y++) {
afterPalette = PROBE_WINDOW_MISMATCH(gStage, &compared);
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
jlScbSet(gStage, y, (uint8_t)(y % JL_SURFACE_PALETTE_COUNT));
}
jlStagePresent();
afterAll = PROBE_WINDOW_MISMATCH(gStage, &compared);
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
jlScbSet(gStage, y, savedScb[y]);
}
jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette);
jlStagePresent();
afterRestore = dvxPortProbeDisplayMismatch(gStage, &compared);
jlLogF("UBER-PROBE: dvxDisplayMismatch afterB=%lu afterSettle=%lu afterIdle=%lu afterScb=%lu afterPalette=%lu afterRestore=%lu compared=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle, (unsigned long)afterScb, (unsigned long)afterPalette, (unsigned long)afterRestore, (unsigned long)compared);
afterRestore = PROBE_WINDOW_MISMATCH(gStage, &compared);
jlFillRect(gStage, 7, 21, 13, 5, 12);
jlFillRect(gStage, 251, 171, 31, 17, 6);
jlStagePresent();
afterTwo = PROBE_WINDOW_MISMATCH(gStage, &compared);
jlLogF("UBER-PROBE: " PROBE_WINDOW_TAG " afterB=%lu afterSettle=%lu afterIdle=%lu afterScb=%lu afterPalette=%lu afterAll=%lu afterRestore=%lu afterTwo=%lu compared=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle, (unsigned long)afterScb, (unsigned long)afterPalette, (unsigned long)afterAll, (unsigned long)afterRestore, (unsigned long)afterTwo, (unsigned long)compared);
}
#endif
@ -1218,8 +1259,7 @@ static void buildBallSprite(void) {
dst = &gBallTiles[(ty * BALL_TILES_X + tx) * 32u];
for (row = 0; row < 8; row++) {
for (b = 0; b < 4; b++) {
dst[row * 4 + b] =
gBallAuthored[((ty * 8) + row) * 8 + (tx * 4) + b];
dst[row * 4 + b] = gBallAuthored[((ty * 8) + row) * 8 + (tx * 4) + b];
}
}
}
@ -1465,7 +1505,7 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) {
// the headless perf-capture run still completes without a keypress.
held = jlFrameCount();
while ((uint16_t)(jlFrameCount() - held) < SC_HOLD_FRAMES) {
/* hold */
// hold
}
}
@ -1553,10 +1593,10 @@ static void __attribute__((noinline)) checkClippedSprites(void) {
static void __attribute__((noinline)) checkTileMonoFlood(void) {
static jlTileT mono;
uint8_t i;
uint8_t i;
uberMbOp(103);
for (i = 0; i < TILE_BYTES; i++) {
for (i = 0; i < JL_TILE_BYTES; i++) {
mono.pixels[i] = (uint8_t)((i & 1) ? 0x0F : 0xF0);
}
jlTilePasteMono(gStage, 10, 10, &mono, 5, 9);
@ -1570,11 +1610,11 @@ static void __attribute__((noinline)) checkTileMonoFlood(void) {
static void __attribute__((noinline)) checkDrawText(void) {
static uint16_t asciiMap[256];
uint16_t i;
uint16_t i;
uberMbOp(104);
for (i = 0; i < 256u; i++) {
asciiMap[i] = TILE_NO_GLYPH;
asciiMap[i] = JL_TILE_NO_GLYPH;
}
// 'A' -> tile (5,5), 'B' -> tile (6,6). Seed both glyph tiles here --
// the section-opening surface clear wiped whatever the timed tile ops
@ -1662,8 +1702,8 @@ static void __attribute__((noinline)) checkAllocator(void) {
// stabilization; the coarse marker froze on this check).
uberMbOp(111);
usedBefore = jlSpriteCodegenBytesUsed();
a = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
b = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
a = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
b = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
if (a == NULL || b == NULL) {
chkPassFail("arena-churn", false);
return;
@ -1710,12 +1750,12 @@ static void __attribute__((noinline)) checkAllocator(void) {
static void __attribute__((noinline)) checkTileMonoFloodExtra(void) {
// 0x00 -> combo idx 0 (bg:bg), 0xFF -> idx 3 (fg:fg), 0x0F -> idx 1
// (bg:fg), 0xF0 -> idx 2 (fg:bg): all four opacity pairs in one tile.
static const uint8_t comboBytes[4] = { 0x00u, 0xFFu, 0x0Fu, 0xF0u };
static const uint8_t comboBytes[4] = { 0x00u, 0xFFu, 0x0Fu, 0xF0u };
static jlTileT monoAll;
uint8_t i;
uberMbOp(121);
for (i = 0; i < TILE_BYTES; i++) {
for (i = 0; i < JL_TILE_BYTES; i++) {
monoAll.pixels[i] = comboBytes[i & 3u];
}
jlTilePasteMono(gStage, 10, 12, &monoAll, 5, 9);
@ -1967,7 +2007,7 @@ static void __attribute__((noinline)) runAllTests(void) {
// matches the golden restore's geometry exactly) into a dedicated
// buffer, then time a restore to a non-stage work surface. The
// difference vs the golden jlSpriteRestoreUnder row = the mark.
gProbeRestoreWork = jlSurfaceCreate();
gProbeRestoreWork = jlSurfaceCreate();
gProbeRestoreBackup.sprite = gSprite;
gProbeRestoreBackup.bytes = gProbeRestoreBackupBytes;
jlSpriteSaveUnder(gStage, gSprite, 40, 30, &gProbeRestoreBackup);
@ -2016,8 +2056,8 @@ static void __attribute__((noinline)) runAllTests(void) {
#ifdef JOEYLIB_PLATFORM_DOS
op_probeVgaCheck();
#endif
#ifdef JOEYLIB_PLATFORM_DVX
op_probeDvxDisplayCheck();
#if defined(JOEYLIB_PLATFORM_DVX) || defined(JOEYLIB_HOSTED_WIN16) || defined(JOEYLIB_HOSTED_WIN32) || defined(JOEYLIB_HOSTED_BE)
op_probeWindowDisplayCheck();
#endif
#endif
@ -2057,8 +2097,7 @@ static void __attribute__((noinline)) reportElapsed(uint16_t startFrame) {
endFrame = jlFrameCount();
elapsedFrames = (uint16_t)(endFrame - startFrame);
elapsedMs = ((unsigned long)elapsedFrames * 1000UL) / (unsigned long)jlFrameHz();
jlLogF("UBER: total wall time: %lu ms (%u frames @ %u Hz)",
elapsedMs, elapsedFrames, (unsigned)jlFrameHz());
jlLogF("UBER: total wall time: %lu ms (%u frames @ %u Hz)", elapsedMs, elapsedFrames, (unsigned)jlFrameHz());
}
@ -2080,7 +2119,7 @@ static bool __attribute__((noinline)) setupSprite(void) {
jlLog("UBER: jlSpriteCompile failed");
}
while (jlFrameCount() == before) {
/* wait for next VBL edge */
// wait for next VBL edge
}
jlLogF("UBER: jlSpriteCompile: 1 call in <= 1 frame");
gBackup.bytes = gBackupBytes;
@ -2099,6 +2138,8 @@ static bool __attribute__((noinline)) initJoeyLib(void) {
* thunk+table DRAWs ~12 KB; plus ~1-2 KB of save/restore classes).
* UL on the multiply because a 16-bit int overflows on 32 * 1024. */
config.codegenBytes = 32UL * 1024;
// Audio on: the timed run measures every op with the engine running.
config.noAudio = false;
return jlInit(&config);
}
@ -2138,21 +2179,15 @@ int main(void) {
*UBER_MB_SPRITEPTR = (uint32_t)gSprite;
#endif
// Audio: only init/shutdown is exercised. Triggering jlAudioPlaySfx
// without first calling jlAudioPlayMod leaves NTP's engine in a
// half-initialized state -- NTPstreamsound is designed to OVERLAY on
// an already-running module. Without NTPprepare/NTPplay first, the
// streamer oscillator is fired but no music tick ever advances or
// silences it, and you get a stuck high-pitched scream. UBER doesn't
// ship a MOD asset, so we skip the SFX exercise. The frame-tick and
// isPlayingMod calls below still get timed (both are no-op fast
// paths on IIgs).
uberMbPhase(UBER_PHASE_AUDIO_INIT);
if (jlAudioInit()) {
jlLogF("UBER: audioInit OK");
} else {
jlLogF("UBER: audioInit failed (skipping audio)");
}
// Audio: jlInit started the engine; nothing is played. Triggering
// jlAudioPlaySfx without first calling jlAudioPlayMod leaves NTP's
// engine in a half-initialized state -- NTPstreamsound is designed to
// OVERLAY on an already-running module. Without NTPprepare/NTPplay
// first, the streamer oscillator is fired but no music tick ever
// advances or silences it, and you get a stuck high-pitched scream.
// UBER doesn't ship a MOD asset, so we skip the SFX exercise. The
// frame-tick and isPlayingMod calls below still get timed (both are
// no-op fast paths on IIgs).
// Visual showcase: render one of each primitive into its own grid
// cell so the run shows something legible (the timed benchmark below

View file

@ -0,0 +1,229 @@
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// Workbench probe (WBLAUNCH, a gate's program, not an example, and the
// Amiga's alone): a program's icon read and drawn by the ROM's icon.library,
// and the program started from it the way Workbench starts a tool.
//
// Wblaunch FOLDER NAME
//
// GetDiskObject reads FOLDER/NAME.info; its type, stack and image are
// logged, and the image is drawn at ICON_X,ICON_Y on a hires screen of
// Workbench's four colours, with the mouse pointer hidden, held HOLD_TICKS
// for a screenshot. Then
// FOLDER/NAME is started as Workbench starts a tool -- LoadSeg, CreateProc
// with the icon's stack, and a WBStartup message whose one argument is the
// folder's lock and the program's name -- from a current directory that is
// not FOLDER (AWAY_FOLDER), and the message coming back says the program
// has ended. Each step is a "WBLAUNCH:" line of LOG_NAME, beside Wblaunch.
// scripts/verify-amiga-icon.sh is the gate.
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <dos/dos.h>
#include <dos/dosextens.h>
#include <exec/memory.h>
#include <exec/ports.h>
#include <graphics/modeid.h>
#include <intuition/intuition.h>
#include <intuition/screens.h>
#include <workbench/startup.h>
#include <workbench/workbench.h>
#include <proto/dos.h>
#include <proto/exec.h>
#include <proto/graphics.h>
#include <proto/icon.h>
#include <proto/intuition.h>
#define LOG_NAME "wblaunch.txt"
#define AWAY_FOLDER "RAM:"
#define PATH_BYTES 256u
#define SCREEN_W 640
#define SCREEN_H STDSCREENHEIGHT
#define SCREEN_DEPTH 2
#define ICON_X 64
#define ICON_Y 64
#define HOLD_TICKS (5 * TICKS_PER_SECOND)
#define GUN_MASK 0xFu
#define RED_SHIFT 8u
#define GREEN_SHIFT 4u
#define ARGS_WANTED 3
// A blank pointer, in chip memory: its two control words, one empty line of
// two words, and the two words that end it.
#define BLANK_POINTER_BYTES (6u * sizeof(UWORD))
#define BLANK_POINTER_H 1
#define BLANK_POINTER_W 16
static bool drawIcon(struct DiskObject *icon);
static bool launch(const char *folder, const char *name, ULONG stack);
static void logLine(const char *format, ...);
// Workbench's four colours (2.0 on), 4 bits a gun: grey, black, white and
// blue (tools/iconBake.py's AMIGA_COLOURS).
static const UWORD gWorkbenchColours[] = { 0xAAA, 0x000, 0xFFF, 0x68B };
static FILE *gLog = NULL;
// The icon's image on a screen of Workbench's colours, held for the gate's
// screenshot. A window over the whole screen, active, hides the pointer.
static bool drawIcon(struct DiskObject *icon) {
struct Screen *screen;
struct Window *window;
UWORD *blank;
UWORD i;
bool drawn;
drawn = false;
blank = (UWORD *)AllocMem(BLANK_POINTER_BYTES, MEMF_CHIP | MEMF_CLEAR);
screen = OpenScreenTags(NULL, SA_Width, SCREEN_W, SA_Height, SCREEN_H, SA_Depth, SCREEN_DEPTH, SA_DisplayID, HIRES_KEY, SA_Quiet, TRUE, SA_ShowTitle, FALSE, TAG_DONE);
window = NULL;
if (screen != NULL) {
window = OpenWindowTags(NULL, WA_CustomScreen, (ULONG)screen, WA_Backdrop, TRUE, WA_Borderless, TRUE, WA_Activate, TRUE, WA_RMBTrap, TRUE, TAG_DONE);
}
if (blank == NULL || window == NULL) {
logLine("FAIL cannot open a screen and a window");
} else {
SetPointer(window, blank, BLANK_POINTER_H, BLANK_POINTER_W, 0, 0);
for (i = 0; i < sizeof(gWorkbenchColours) / sizeof(gWorkbenchColours[0]); i++) {
SetRGB4(&screen->ViewPort, i, (gWorkbenchColours[i] >> RED_SHIFT) & GUN_MASK, (gWorkbenchColours[i] >> GREEN_SHIFT) & GUN_MASK, gWorkbenchColours[i] & GUN_MASK);
}
SetRast(window->RPort, 0);
DrawImage(window->RPort, (struct Image *)icon->do_Gadget.GadgetRender, ICON_X, ICON_Y);
logLine("drawn at %d,%d", ICON_X, ICON_Y);
Delay(HOLD_TICKS);
ClearPointer(window);
drawn = true;
}
if (window != NULL) {
CloseWindow(window);
}
if (screen != NULL) {
CloseScreen(screen);
}
if (blank != NULL) {
FreeMem(blank, BLANK_POINTER_BYTES);
}
return drawn;
}
// FOLDER/NAME started as Workbench starts a tool, from AWAY_FOLDER; true
// once its message has come back.
static bool launch(const char *folder, const char *name, ULONG stack) {
struct WBStartup startup;
struct WBArg arg;
struct MsgPort *reply;
struct MsgPort *process;
char path[PATH_BYTES];
BPTR lock;
BPTR away;
BPTR before;
BPTR segment;
strncpy(path, folder, sizeof(path) - 1u);
path[sizeof(path) - 1u] = '\0';
if (!AddPart((STRPTR)path, (STRPTR)name, sizeof(path))) {
logLine("FAIL the path is too long");
return false;
}
lock = Lock((STRPTR)folder, SHARED_LOCK);
if (lock == 0) {
logLine("FAIL cannot lock %s", folder);
return false;
}
segment = LoadSeg((STRPTR)path);
if (segment == 0) {
logLine("FAIL cannot load %s", path);
UnLock(lock);
return false;
}
reply = CreateMsgPort();
away = Lock((STRPTR)AWAY_FOLDER, SHARED_LOCK);
if (reply == NULL || away == 0) {
logLine("FAIL no reply port or no %s", AWAY_FOLDER);
UnLoadSeg(segment);
UnLock(lock);
return false;
}
before = CurrentDir(away);
process = CreateProc((STRPTR)name, 0, segment, (LONG)stack);
if (process == NULL) {
logLine("FAIL cannot start %s", path);
} else {
memset(&startup, 0, sizeof(startup));
startup.sm_Message.mn_Node.ln_Type = NT_MESSAGE;
startup.sm_Message.mn_ReplyPort = reply;
startup.sm_Message.mn_Length = sizeof(startup);
startup.sm_Process = process;
startup.sm_Segment = segment;
startup.sm_NumArgs = 1;
startup.sm_ArgList = &arg;
arg.wa_Lock = lock;
arg.wa_Name = (STRPTR)name;
logLine("started %s with %lu bytes of stack from %s", path, (unsigned long)stack, AWAY_FOLDER);
PutMsg(process, &startup.sm_Message);
WaitPort(reply);
GetMsg(reply);
}
CurrentDir(before);
UnLock(away);
DeleteMsgPort(reply);
UnLoadSeg(segment);
UnLock(lock);
return process != NULL;
}
static void logLine(const char *format, ...) {
va_list args;
fputs("WBLAUNCH: ", gLog);
va_start(args, format);
vfprintf(gLog, format, args);
va_end(args);
fputc('\n', gLog);
fflush(gLog);
}
int main(int argc, char **argv) {
struct DiskObject *icon;
struct Image *image;
char path[PATH_BYTES];
int status;
gLog = fopen(LOG_NAME, "w");
if (gLog == NULL) {
return RETURN_FAIL;
}
status = RETURN_FAIL;
if (argc != ARGS_WANTED) {
logLine("FAIL usage: Wblaunch FOLDER NAME");
} else if (IconBase == NULL) {
logLine("FAIL no icon.library");
} else {
strncpy(path, argv[1], sizeof(path) - 1u);
path[sizeof(path) - 1u] = '\0';
AddPart((STRPTR)path, (STRPTR)argv[2], sizeof(path));
icon = GetDiskObject((STRPTR)path);
if (icon == NULL) {
logLine("FAIL icon.library cannot read %s.info", path);
} else {
image = (struct Image *)icon->do_Gadget.GadgetRender;
logLine("icon type=%u stack=%ld gadget=%dx%d image=%dx%dx%d", (unsigned)icon->do_Type, (long)icon->do_StackSize, icon->do_Gadget.Width, icon->do_Gadget.Height, image->Width, image->Height, image->Depth);
if (drawIcon(icon) && launch(argv[1], argv[2], (ULONG)icon->do_StackSize)) {
logLine("returned");
status = RETURN_OK;
}
FreeDiskObject(icon);
}
}
fclose(gLog);
return status;
}

View file

@ -11,13 +11,19 @@
// Apple IIgs -- NinjaTrackerPlus .NTP, played by Ninjaforce's
// 65816 replayer linked into the binary.
// Amiga -- raw .MOD, played by Frank Wille's PTPlayer.
// DOS -- raw .MOD, played by libxmp-lite over SB DMA.
// Atari ST -- raw .MOD, played by libxmp-lite (parser only) plus
// a hand-rolled 68k 4-channel mixer that outputs via
// YM2149 4-bit PWM at ~12.5 kHz.
// DOS -- raw .MOD, mixed by JoeyLib's module player (PTPlayer's
// replay in C) into SB DMA (22 kHz mono 8-bit).
// Atari ST -- raw .MOD, mixed by the module player at 12288 Hz and
// played through the YM2149's channel A volume (PWM); the
// STe build plays it through DMA sound at 12517 Hz.
// X68000 -- raw .MOD, mixed by the module player at 7812 Hz into the
// MSM6258 ADPCM channel.
// DVX -- raw .MOD through DVX's music library.
// hosted -- raw .MOD, mixed by the module player in software.
//
// Game code always calls the same five entry points; the per-port
// HAL hides the engine choice. A failed audio init is non-fatal --
// Game code always calls the same entry points; the per-port HAL hides
// the engine choice. jlInit starts audio (jlConfigT.noAudio leaves it
// off) and jlShutdown stops it. Sound that does not start is non-fatal:
// jlInit still succeeds and audio calls become no-ops.
#ifndef JOEYLIB_AUDIO_H
@ -26,36 +32,31 @@
#include "platform.h"
#include "types.h"
// SFX slot convention: the IIgs maps tracker tone voices onto SFX
// slots 0-2 and the tracker noise voice onto slot 3, so applications
// that also play digital SFX while music runs should prefer slot 4.
// On the other ports the slots are independent of the voice layer.
#define JOEY_AUDIO_SFX_SLOTS 5
// SFX slot convention: the IIgs plays tone voices 0-2 on SFX slots 0-2
// and the noise voice on slot 3 (a voice and an effect on one slot cut
// each other off, and a held voice takes its slot back at the next
// jlAudioFrameTick), so applications that also play digital SFX while
// music runs should prefer slot 4. On the Amiga effects, the module, the
// voices, and the noise share Paula's four channels. On the other ports
// the slots are independent of the voice layer.
#define JL_AUDIO_SFX_SLOTS 5
// Number of PSG-style polyphonic tone voices (jlAudioVoice selects
// voice 0/1/2). The core gate range-checks against this so each
// per-port HAL can assume voice < JOEY_AUDIO_VOICES.
#define JOEY_AUDIO_VOICES 3
// Initialize audio. Returns true if the platform has a working audio
// engine and was able to start it. Returns false silently otherwise;
// the rest of the API stays callable but produces no sound.
bool jlAudioInit(void);
// Tear down the engine. Safe to call when audio is not initialized.
void jlAudioShutdown(void);
// per-port HAL can assume voice < JL_AUDIO_VOICES.
#define JL_AUDIO_VOICES 3
// Begin module playback. data points at the platform-native module
// blob (.MOD on most platforms, .NTP on IIgs); the asset pipeline
// blob (.MOD on every platform but the IIgs, .NTP there); the asset pipeline
// produces the right form for each target. If a module is already
// playing, it is replaced.
//
// loop=true plays forever. loop=false stops at song end, but Amiga
// requires the module to contain an E8FF effect at song end for the
// stop to fire (PTPlayer has no native song-end signal). The
// `joeymod` tool's .amod output extension injects that marker
// automatically; ship .amod for Amiga and .mod for the other ports.
// loop=false on a .mod (no E8FF) loops anyway on Amiga.
// loop=true plays forever. loop=false stops at song end on every port.
// PTPlayer (Amiga) has no native song-end signal, so the Amiga HAL adds an
// E8F stop marker to the module as it copies it to chip RAM; a module
// with no room for one (all 128 song positions or patterns in use) loops
// there anyway. The `joeymod` tool's .amod output adds the same marker
// ahead of time.
void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
// Stop the current module (if any). The playhead is reset so the next
@ -63,15 +64,18 @@ void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
void jlAudioStopMod(void);
// True if a module is currently producing output (false during silence
// after StopMod or before the first PlayMod, and on platforms where
// audio init failed).
// after StopMod, once a loop=false song has ended, before the first
// PlayMod, and on platforms where audio init failed).
bool jlAudioIsPlayingMod(void);
// Trigger a one-shot digital SFX on the given slot (0..JOEY_AUDIO_SFX_SLOTS-1).
// Trigger a one-shot digital SFX on the given slot (0..JL_AUDIO_SFX_SLOTS-1).
// sample points at raw signed 8-bit PCM. rateHz is the playback rate
// the sample was recorded at; the engine pitches as needed for its
// own output rate. If the slot is currently playing, the new sample
// replaces it.
// replaces it. Most ports read the sample in place while it plays, so it
// must stay valid and unchanged until it has finished or the slot is
// stopped; the IIgs keeps a copy per slot keyed by address, length, and
// rate, so changed bytes replayed with all three the same are not heard.
//
// The full -128..127 range is accepted everywhere, but the IIgs plays
// -128 as -127: its DOC reads 0x00 (which is what -128 becomes in the
@ -83,9 +87,10 @@ void jlAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16
// streaming slot's prefetch buffer; should write up to `count` signed
// 8-bit samples to `dst` and return the number written (see
// jlAudioPlaySfx for the one clamp the IIgs applies). Returning 0
// signals end-of-stream and the slot auto-deactivates. Called from
// the same context as the audio engine's refill (main thread on
// DOS/ST, audio ISR on Amiga/IIgs); must not block or allocate.
// signals end-of-stream and the slot auto-deactivates. Called in
// program time on every port, from the call that starts the stream and
// then from jlAudioFrameTick (on DVX also while the desktop runs); must
// not block, and should be quick.
typedef uint32_t (*jlAudioStreamFillT)(void *ctx, int8_t *dst, uint32_t count);
// Trigger streaming SFX playback on the given slot. The mixer pulls
@ -94,22 +99,22 @@ typedef uint32_t (*jlAudioStreamFillT)(void *ctx, int8_t *dst, uint32_t count);
// allocation. `rateHz` is the sample rate `fill` produces; the
// engine resamples (with linear interpolation) to its own output
// rate. If the slot is currently playing, the new stream replaces
// it. Implemented on every port: DOS/ST/X68000 pull through the shared
// SFX overlay mixer, while the IIgs (NTPstreamsound) and Amiga
// (PTPlayer mt_playfx) play one chunk at a time and fire the next from
// jlAudioFrameTick as the current one runs down -- so on those two the
// host must be calling jlAudioFrameTick for a stream to continue past
// its first chunk, and a seam can carry up to one frame of jitter.
// it. Implemented on every port: DOS/ST/X68000/hosted pull through the
// shared SFX overlay mixer as jlAudioFrameTick refills their sound
// buffers, while the IIgs (NTPstreamsound, 7 KB chunks) and Amiga
// (PTPlayer mt_playfx, 4 KB chunks) play one chunk at a time and fire the
// next from jlAudioFrameTick as the current one runs down -- so the host
// must be calling jlAudioFrameTick for a stream to continue, and on those
// two a seam can carry up to one frame of jitter.
void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz);
// Raw-format streaming: jlAudioPlaySfxStream, except `fill` delivers
// bytes already in the port's raw stream format (jlAudioSfxRawFormat),
// so the port skips its per-byte conversion of every chunk. On the IIgs
// that conversion is a 65816 byte loop over each 3.5 KB chunk -- it
// stalled Space Taxi's ride for a third of a second per chunk of speech.
// Every other port streams signed PCM natively, so there Raw is the
// plain call. A producer that cannot make the port's format uses
// jlAudioPlaySfxStream.
// that conversion costs ~7 us a byte, ~50 ms per 7 KB chunk. Every other
// port's raw format is signed PCM, so there Raw is the plain call (DVX
// still flips each byte for its sound library). A producer that cannot
// make the port's format uses jlAudioPlaySfxStream.
#define JL_SFX_RAW_SIGNED8 0u // int8_t PCM, the jlAudioPlaySfxStream contract
#define JL_SFX_RAW_UNSIGNED8_NOZERO 1u // uint8_t PCM biased 0x80; 0x00 is forbidden (DOC stop byte)
uint8_t jlAudioSfxRawFormat(void);
@ -117,14 +122,14 @@ void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, void *ctx
// Stereo placement for a SFX slot.
typedef enum {
AUDIO_PAN_LEFT = 0,
AUDIO_PAN_CENTER,
AUDIO_PAN_RIGHT,
JL_AUDIO_PAN_LEFT = 0,
JL_AUDIO_PAN_CENTER,
JL_AUDIO_PAN_RIGHT,
AUDIO_PAN_COUNT
JL_AUDIO_PAN_COUNT
} jlAudioPanE;
// Place a SFX slot in the stereo field. Slots start at AUDIO_PAN_CENTER, and
// Place a SFX slot in the stereo field. Slots start at JL_AUDIO_PAN_CENTER, and
// the setting sticks to the SLOT, so it survives retriggers -- set it once
// when a sound is meant to come from one side.
//
@ -135,26 +140,26 @@ typedef enum {
// A stock IIgs sums to mono anyway; this only shows up on a stereo
// card or an emulator.
// Amiga left and right, by picking the Paula channel side (0 and 3 are
// left, 1 and 2 right). Paula cannot place one voice in both ears,
// so AUDIO_PAN_CENTER keeps PTPlayer's own channel choice.
// left, 1 and 2 right). A channel plays on one side only, so a
// centred sound plays on a free channel each side when both sides
// have one, and otherwise on the one channel PTPlayer picks.
// X68000 ignored. Everything is mixed into ONE MSM6258 ADPCM stream, so
// its pan bits would move all audio at once, not one slot.
// DOS, ST ignored -- their output is mono.
// DOS, ST ignored -- their output is mono (the STe build's too).
// DVX all three, through DVX's stereo mixer.
// hosted all three (the mix is stereo), except Win16, which is mono.
// Passing an out-of-range pan is ignored.
void jlAudioSetPan(uint8_t slot, jlAudioPanE pan);
// Stop a SFX slot early. No-op if the slot is already idle.
void jlAudioStopSfx(uint8_t slot);
// Direct hardware tone generator: continuously play a square wave at
// `freqHz` until the next jlAudioTone call. freqHz == 0 silences the
// generator. Designed for AGI-era PSG-style music where the engine
// just programs a divisor and lets the chip play; CPU cost during
// playback is zero. Single voice on platforms with multiple
// hardware oscillators (uses voice 0). On DOS this programs PIT
// counter 2 and the speaker gate (port 0x61). No-op on platforms
// without a reachable tone generator -- callers should fall back to
// jlAudioPlaySfxStream when they need PCM-synth sound on those.
// A single tone at `freqHz` until the next call: jlAudioVoice(0, freqHz,
// 0), tone voice 0 at full volume, on every port. freqHz == 0 silences
// it. Designed for AGI-era PSG-style music where the engine just
// programs a divisor and lets the chip play; CPU cost during playback is
// zero. It shares voice 0 with jlAudioVoice and jlMusicPlay. On DOS
// without an OPL2, voice 0 (and so this tone) is the PC speaker.
void jlAudioTone(uint16_t freqHz);
// Periodic tick callback for sound schedulers that need better-than-
@ -164,56 +169,68 @@ void jlAudioTone(uint16_t freqHz);
// into one render frame, so the chip only "hears" the last write
// per batch and short notes are dropped.
//
// fn fires from interrupt context at `hz` times per second. Keep
// fn short: only memory reads, direct hardware-register writes,
// no malloc / printf / blocking. Pass fn = NULL to uninstall.
// fn fires `hz` times per second on average, on every port, and may
// fire from an interrupt (DOS, Amiga, ST, X68000): keep fn short, only
// memory reads and direct hardware-register writes, no malloc / printf /
// blocking, and share data with it through jlAudioCritical*. Pass
// fn = NULL to uninstall (always returns true). Returns false when hz is
// 0, audio is off or did not start, or the port cannot take the rate.
//
// Per-port impl: DOS reprograms PIT counter 0 + hooks INT 8,
// keeping the BIOS tick counter at $0040:$006C advancing at the
// canonical 18.2 Hz via an accumulator (so uclock / time stay
// correct). Other ports return false until a port-appropriate
// timer source is wired.
// correct). The Amiga uses a timer.device E-clock request and a
// software interrupt; the ST and X68000 ride the system's Timer C
// interrupt (200 and 100 Hz), so calls due between its ticks come
// together. The IIgs and DVX call fn in program time from
// jlAudioFrameTick, and the hosted ports from the mixer, between the
// samples where each tick falls.
typedef void (*jlAudioTickFnT)(void);
bool jlAudioTickRegister(jlAudioTickFnT fn, uint16_t hz);
// Critical section against the tick ISR. Callers that mutate
// state read by the registered tick callback must wrap the
// mutation in Enter/Exit so the ISR can't observe a half-written
// state. On DOS this is `cli` / `sti`; on ports without an
// audio ISR these are no-ops.
// state. Sections nest: the outermost pair holds the interrupt off
// (on DOS, the Amiga, the ST, and the X68000, every interrupt, so keep
// it short), and its Exit puts back the interrupt state its Enter found.
// No-ops where the callback runs in program time (IIgs, DVX, hosted).
void jlAudioCriticalEnter(void);
void jlAudioCriticalExit(void);
// 3-voice PSG-style polyphonic tone generator. `voice` selects 0/1/2;
// `freqHz` is the new tone (0 = silence this voice); `atten` is the
// AGI/SN76489-style 4-bit attenuation, 0 = loudest .. 15 = silent
// (2 dB per step). The mapping to underlying hardware:
// (2 dB per step, as near as the chip comes: the ST's YM2149 levels are
// ~3.5 dB apart, so within 1.6 dB there). The mapping to underlying
// hardware:
//
// DOS -- AdLib OPL2 melodic channels 0/1/2 (ports 0x388/0x389).
// Atari ST -- YM2149 PSG channels A/B/C.
// DOS -- AdLib OPL2 melodic channels 0/1/2 (ports 0x388/0x389);
// with no OPL2, voice 0 is the PC speaker, 1 and 2 silent.
// Atari ST -- YM2149 PSG channels A/B/C (plain ST: voice 0 silent while
// digital sound plays, which drives channel A).
// Amiga -- Paula channels 0/1/2 looping a 2-byte square sample.
// IIgs -- Ensoniq DOC oscillators playing a resident square,
// retriggered as an ~80 ms one-shot per call. Needs
// jlAudioInit (the DOC path rides the NTP engine);
// callers that hold a note longer than ~80 ms must
// re-assert it about once a frame (jlMusicPlay's
// sequencer and AGI's per-frame driver both do).
// IIgs -- Ensoniq DOC oscillators playing a resident square on SFX
// slots 0/1/2.
// X68000 -- YM2151 channels 0/1/2, one sine operator each.
// DVX -- square tones of DVX's sound library.
// hosted -- square waves mixed in software.
//
// Voice state is sticky: a voice keeps playing its last frequency
// until the next jlAudioVoice call on it (IIgs excepted, above). CPU
// cost during sustained playback is zero (the hardware oscillates on
// its own). Safe to call without jlAudioInit on DOS/ST/Amiga -- the
// voice path is direct hardware programming, independent of the
// MOD/SFX mixer. On ST and Amiga the voice channels are the same
// hardware the MOD engine drives: while a MOD is playing, voice
// writes fight the player's register writes and lose. Use the voice/
// tracker layer or the MOD layer per scene, not both at once.
// A note sounds until the next call for that voice. The IIgs plays a
// note as a short sample and restarts held notes from jlAudioFrameTick,
// so call that every frame. On the Amiga the voices are the channels the
// MOD engine drives, so while a MOD is playing, voice writes fight the
// player's and lose: use the voice/tracker layer or the MOD layer per
// scene, not both at once.
void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
// Noise/percussion generator, the 4th logical voice of the tracker
// layer. `pitch` is 0..31 in YM2149 noise-period style: 0 = the
// brightest hiss (hats), 31 = the lowest rumble (toms); atten as in
// jlAudioVoice, and atten >= 15 silences. The per-port mapping:
// brightest hiss (hats), 31 = the lowest rumble (toms); the noise runs at
// 125000 / pitch Hz (pitch 0 as 1) as far as each chip goes (the Amiga
// tops out at 28.6 kHz and the IIgs at 26 kHz, so pitches 0-4 sound
// alike there). atten as in jlAudioVoice, and atten >= 15 silences. The
// per-port mapping:
//
// Atari ST -- the YM2149 noise generator (reg 6) mixed onto
// channel C. Noise shares channel C's volume register
@ -221,20 +238,21 @@ void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
// apart (songbake warns on overlap).
// DOS -- OPL2 rhythm-mode snare drum (the chip's LFSR noise
// source) on channel 7; the three tone channels are
// untouched.
// untouched. A tuned drum: its pitch spreads over the
// snare's range instead of following the rate above.
// Amiga -- Paula channel 3 looping a chip-RAM LFSR noise sample
// (channels 0-2 are the tone voices).
// IIgs -- a resident LFSR noise sample on SFX slot 3, same
// ~80 ms retriggered one-shot model as the tone voices
// (jlMusicPlay's sequencer re-asserts it per frame).
// IIgs -- a resident LFSR noise sample on SFX slot 3.
// X68000 -- the YM2151's noise generator, at the rate exactly.
// DVX -- a noise tone of DVX's sound library.
// hosted -- the YM2149's 17-bit generator, mixed in software.
//
// Same init contract as jlAudioVoice: direct hardware programming on
// DOS/ST/Amiga, jlAudioInit required on the IIgs.
// The same contract as jlAudioVoice: it sounds until the next call.
void jlAudioNoise(uint8_t pitch, uint8_t atten);
// Portable chip-tracker music: play a JYM1 event stream (baked from
// tools/songbake text notation) through the jlAudioVoice tone layer.
// One authored song plays on all four ports at register-write cost --
// One authored song plays on every port at register-write cost --
// there is no CPU mixing, so unlike jlAudioPlayMod this path is fully
// viable on the ST and cheap on DOS. The stream is read IN PLACE:
// data must stay valid until jlMusicStop or song end. Playback is
@ -242,8 +260,7 @@ void jlAudioNoise(uint8_t pitch, uint8_t atten);
// paced by jlMillisElapsed, so tempo holds across 50/60/70 Hz ports.
// loop=true restarts at the song's authored loop point (or the top
// when it has none); loop=false silences the voices at song end.
// Returns false (and plays nothing) on a malformed stream. Call
// jlAudioInit first for IIgs audibility (see jlAudioVoice).
// Returns false (and plays nothing) on a malformed stream.
bool jlMusicPlay(const uint8_t *data, uint32_t length, bool loop);
// Stop tracker playback and silence the voices (tones and noise).
@ -270,10 +287,11 @@ void jlMusicResume(void);
// Step it over successive frames for fades.
void jlMusicSetAtten(uint8_t atten);
// Hook the engine into the game loop. Most platforms drive their
// engines off a hardware IRQ and ignore this call, but it's safe to
// invoke once per frame regardless and required for any port that
// runs its mixer in user-thread context.
// Hook the engine into the game loop; call it once per frame. Every
// port does sound work here: DOS, ST, X68000, DVX, and hosted mix or
// refill their sound buffers (modules and effects stop or repeat
// without it), the IIgs and Amiga feed streams and notice a module's
// end (and the IIgs holds notes), and every port advances jlMusicPlay.
void jlAudioFrameTick(void);
#endif

View file

@ -12,21 +12,35 @@
typedef struct {
// Runtime compiled-sprite cache size. 0 selects a per-platform
// default: 8 KB on IIgs, 32 KB on Amiga/ST/DOS (the 68k/x86
// emitters are ~4-8x less code-dense than the 65816's, so a
// 32x32 sprite can need 8-16 KB there). Size against
// jlSpriteCompiledSize() when tuning. On the IIgs the cache is one
// Memory Manager block allocated with attrNoCross (it may not span
// a 64 KB bank boundary), so values above 65536 are clamped to
// 65536; even a clamped request can fail if no free bank exists.
// default: 8 KB on the IIgs, 64 KB on the Amiga and ST, and 32 KB
// everywhere else (the 68k/x86 emitters are ~4-8x less code-dense
// than the 65816's, so a 32x32 sprite can need 8-16 KB there). The
// ports that interpret every sprite still allocate it. Size against
// jlSpriteCompiledSize() when tuning. Elsewhere the cache is one
// block, and jlInit fails if it cannot be had. On the IIgs it is up
// to four Memory Manager blocks of at most 64 KB, each inside one
// bank (a compiled sprite cannot cross a bank), so at most 256 KB;
// a bank that is refused is retried at 60 KB, and jlInit takes
// whatever part of the request it was granted.
uint32_t codegenBytes;
// True: leave audio off. Every audio call is then silent, as on a
// machine without sound, and the program pays nothing for audio:
// on the IIgs no replayer read from disk, on DOS and the Amiga no
// interrupt hooked. jlInit starts audio otherwise, and jlShutdown
// stops it.
bool noAudio;
} jlConfigT;
// Initialize the library. Returns true on success.
// On failure, jlLastError() returns a human-readable description.
// Initialize the library, audio too (unless config->noAudio). Returns true
// on success; sound that does not start is not a failure (modules and
// effects are then silent, and the log says so). On failure, jlLastError()
// returns a human-readable description; where the port's own start-up
// fails without saying why, that is just "jlpInit failed".
bool jlInit(const jlConfigT *config);
// Shut down the library, releasing all resources.
// Shut down the library, audio first, releasing the library's own
// resources. Surfaces and sprites the program created are not freed:
// destroy them first.
void jlShutdown(void);
// Returns the most recent error message, or NULL if none.
@ -41,13 +55,17 @@ const char *jlVersionString(void);
// Allocate a large or persistent buffer, backed by the platform's
// native allocator rather than the small C heap. Use this (not malloc)
// for anything that exceeds the per-allocation or total ceiling of the
// stock C heap -- on the IIgs the C heap lives in bank 0 and is tiny,
// and a single malloc is capped at ~32 KB, so multi-KB caches and frame
// buffers must come from the IIgs Memory Manager instead. Returns NULL
// on failure. The returned pointer is NOT zero-filled.
// stock C heap -- on the IIgs the whole C heap is one Memory Manager
// block of 32 KB to 48 KB (or, if that block cannot be had, what the
// link left in the program's bank, which can be tiny), so multi-KB
// caches and frame buffers must come from the IIgs Memory Manager
// instead. Returns NULL on failure. The returned pointer is NOT
// zero-filled.
void *jlAlloc(uint32_t bytes);
// Release a buffer returned by jlAlloc. NULL is accepted and ignored.
// Never mix jlFree and free: on the IIgs a jlAlloc block is a Memory
// Manager handle, and either one given the other's block corrupts memory.
void jlFree(void *p);
// memcpy with a 32-bit count. Same contract as memcpy (no overlap); every
@ -55,33 +73,36 @@ void jlFree(void *p);
// byte, split at bank boundaries).
void jlMemCopy(void *dst, const void *src, uint32_t bytes);
// Block the calling thread until the next display vertical blank.
// Used to pace game loops to the display's native refresh rate
// (~70 Hz on VGA mode 13h, ~50 or ~60 Hz on Amiga/ST PAL/NTSC, ~60 Hz
// on IIgs SHR). Cheap on every port since the underlying mechanism is
// always a hardware-level wait, not a software timer.
// Block the calling thread until the next display vertical blank, to
// pace a game loop to the display's refresh. On the machine ports it
// waits for the display's own vertical blank. The hosted ports, and DVX
// in a window, have none to wait for: there it waits for the next tick
// of a virtual frame clock (60 Hz hosted, 70 Hz DVX), and nothing keeps
// a present from tearing.
void jlWaitVBL(void);
// Monotonic 16-bit frame counter. Caller must poll faster than
// 2 * jlFrameHz() so no edge is missed. Used by benchmarks and
// frame-rate-independent animation. (Per-port edge-detect / VBL-ISR
// mechanism is documented at jlpFrameCount in src/core/port.h.)
// Monotonic 16-bit frame counter, kept by an interrupt or derived from a
// clock, so it does not miss frames however seldom it is read (except on
// an X68000 whose system refused JoeyLib's frame interrupt: there it
// counts only while jlWaitVBL, jlFrameCount, or jlStagePresent is
// called). It wraps about every 18 minutes at 60 Hz, so compare two
// readings by subtraction. Used by benchmarks and frame-rate-independent
// animation.
uint16_t jlFrameCount(void);
// Nominal display frame rate in Hz: 50 (Amiga PAL), 60 (IIgs / ST
// NTSC default), 70 (VGA mode 13h). The actual VBL cadence may
// Display frame rate in Hz: 60 on the IIgs (50 on a PAL machine), 50 on
// the Amiga (60 on an NTSC machine that cannot show PAL), 50 or 60 on
// the ST as the machine is set, 70 on DOS and DVX, 55 on the X68000 (56
// under MAME), and 60 on every hosted port. The actual VBL cadence may
// drift slightly; the value reported here is what benchmarks divide
// by to convert iters-per-N-frames to ops/sec.
uint16_t jlFrameHz(void);
// Monotonic wall-clock millisecond counter since jlInit. On DOS/ST it
// reads a real hardware timer; on IIgs/Amiga the generic implementation
// derives it from the frame counter (frame-granularity steps, and the
// caller must poll at least once per 65535 frames -- ~18 min at 60Hz --
// or wrapped frames are lost). Two consecutive calls separated by a long
// render report the true elapsed time, not the number of VBLs that
// fired. (Per-port millisecond timer sources are documented at
// jlpMillisElapsed in src/core/port.h.)
// Monotonic wall-clock millisecond counter since jlInit. It may
// advance a frame at a time. Call it at least once every 15 minutes, or
// the time between two calls can be lost. Two consecutive calls
// separated by a long render report the true elapsed time, not the
// number of VBLs that fired.
uint32_t jlMillisElapsed(void);
// Deterministic, seedable pseudo-random generator. Bit-identical

View file

@ -50,12 +50,15 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
// Flood fill a 4-connected region starting at (x, y). Replaces every
// pixel of the original color reached via N/S/E/W steps. No-op if
// (x, y) is off-surface or already matches newColor.
// (x, y) is off-surface or already matches newColor. The span stack
// holds 512 entries; a region that needs more is left partly filled
// (identically on every port).
void jlFloodFill(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
// Flood fill a 4-connected region starting at (x, y), stopping at
// boundary pixels of color boundaryColor. Replaces every reachable
// pixel that is not boundaryColor with newColor. Used for vector-art
// pixel that is not boundaryColor with newColor; pixels already
// newColor stop the fill too. Same 512-entry span stack. Used for vector-art
// rendering (e.g. Sierra-style picture playback): outline a closed
// region with jlDrawLine in boundaryColor, then fill with this.
void jlFloodFillBounded(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor, uint8_t boundaryColor);

View file

@ -7,10 +7,14 @@
//
// Two directories sit next to the executable, staged there by the per-platform
// disk / directory packagers:
// DATA/ -- read-only game content (levels, sprites, audio). jlDataOpen.
// DATA/ -- game content (levels, sprites, audio). jlDataOpen.
// SAVES/ -- writable save / preference files. jlSave*.
// Both are addressed by a bare name relative to their directory; the prefix is
// forced here so callers never hard-code "DATA/" or "SAVES/".
// forced here so callers never hard-code "DATA/" or "SAVES/". The hosted
// ports make the program's folder the current directory in jlInit; DOS, the
// ST, the Amiga, and the X68000 use the current directory as they find it;
// the IIgs finds DATA/ under GS/OS prefix 0 and SAVES/ in the application's
// folder (prefix 1); DVX roots both at run time (docs/ApiFile.adoc).
#ifndef JOEYLIB_FILE_H
#define JOEYLIB_FILE_H
@ -23,18 +27,21 @@
// Open a data file by its name relative to the DATA/ directory. The DATA/
// prefix is prepended automatically; pass "levels/title.dat", not
// "DATA/levels/title.dat". mode is a standard fopen mode string. Returns NULL
// if the resulting path would overflow the internal buffer or the file cannot
// be opened.
// "DATA/levels/title.dat". mode is a standard fopen mode string, passed on
// unchecked (DATA/ is meant to be read, but a writing mode is not refused).
// Returns NULL if mode is NULL, the resulting path would overflow the
// internal buffer, or the file cannot be opened.
FILE *jlDataOpen(const char *name, const char *mode);
// ----- Writable save files (SAVES/) -----
//
// Save / preference data lives under a SAVES/ directory beside the executable
// -- a writable sibling of the read-only DATA/ tree. Names are relative to
// SAVES/: pass "hero.sav", not "SAVES/hero.sav". Keep names portable across
// the retro filesystems: at most 8 characters plus a short extension, letters
// and digits only, so they fit ProDOS (15-char) and DOS 8.3.
// Save / preference data lives under a SAVES/ directory -- a writable
// sibling of the DATA/ tree (where, per port: the top of this file). Names
// are relative to SAVES/: pass "hero.sav", not "SAVES/hero.sav". A name is at
// most 8 letters and digits starting with a letter, then optionally a period
// and up to 3 more, so it fits ProDOS (15 characters, letter first) and DOS
// 8.3; every save call refuses any other name, on every port, as if the file
// could not be opened. Case must match on Linux, Haiku, BeOS, and the web.
//
// A failed open / write is non-fatal and simply reports failure (false / 0),
// so save code can degrade gracefully on a platform or volume where SAVES/ is
@ -47,7 +54,9 @@ FILE *jlDataOpen(const char *name, const char *mode);
FILE *jlSaveOpen(const char *name, const char *mode);
// Write `len` bytes from `buf` to save file `name`, replacing any existing
// contents. Returns true only if all `len` bytes were written.
// contents. Returns true only if all `len` bytes were written. Not atomic: a
// write that fails part way leaves the save short. buf NULL with len > 0
// returns false before the save is touched.
bool jlSaveWrite(const char *name, const void *buf, uint32_t len);
// Read up to `max` bytes from save file `name` into `buf`. Returns the number
@ -62,9 +71,11 @@ bool jlSaveDelete(const char *name);
// ----- Disk space -----
// Free space, in bytes, on the volume that holds SAVES/ (the program's
// volume). Returns 0 if it cannot be determined (e.g. the port has no query
// for it). Saturates at UINT32_MAX (4 GB - 1) on larger volumes.
// Free space, in bytes, on the volume that holds SAVES/. Returns 0 if it
// cannot be determined. Saturates at UINT32_MAX (4 GB - 1) on larger
// volumes; DOS's own query (DOS, Win16) stops near 2 GB, and the web's
// figure is a fixed number from Emscripten's file system, not the
// browser's real space.
uint32_t jlDiskFree(void);
#endif

View file

@ -6,7 +6,7 @@
//
// Call jlInputPoll() once per frame (typically right before
// drawing) to refresh keyboard, mouse, and joystick state. After
// polling, the joeyKey* predicates return the current state of every
// polling, the jlKey* predicates return the current state of every
// key:
//
// jlKeyDown(k) -- is key k held down right now
@ -26,8 +26,8 @@
// The mouse predicates return the pointer state:
//
// jlMouseX/Y() -- pointer position in surface
// coords (0..SURFACE_WIDTH-1,
// 0..SURFACE_HEIGHT-1)
// coords (0..JL_SURFACE_WIDTH-1,
// 0..JL_SURFACE_HEIGHT-1)
// jlMouseDown(b) -- is button b held right now
// jlMousePressed(b) -- rising edge since last poll
// jlMouseReleased(b) -- falling edge since last poll
@ -39,7 +39,8 @@
// jlJoystickX/Y(js) -- axis values, signed -127..+127.
// Digital sticks snap to the
// extremes; analog sticks return
// the raw centered value.
// their offset from centre, scaled,
// dead zone applied.
// jlJoyDown(js, b) -- is button b held on stick js
// jlJoyPressed(js, b) -- rising edge since last poll
// jlJoyReleased(js, b) -- falling edge since last poll
@ -54,63 +55,63 @@
#include "types.h"
typedef enum {
KEY_NONE = 0,
JL_KEY_NONE = 0,
KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I,
KEY_J, KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R,
KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z,
JL_KEY_A, JL_KEY_B, JL_KEY_C, JL_KEY_D, JL_KEY_E, JL_KEY_F, JL_KEY_G, JL_KEY_H, JL_KEY_I,
JL_KEY_J, JL_KEY_K, JL_KEY_L, JL_KEY_M, JL_KEY_N, JL_KEY_O, JL_KEY_P, JL_KEY_Q, JL_KEY_R,
JL_KEY_S, JL_KEY_T, JL_KEY_U, JL_KEY_V, JL_KEY_W, JL_KEY_X, JL_KEY_Y, JL_KEY_Z,
KEY_0, KEY_1, KEY_2, KEY_3, KEY_4,
KEY_5, KEY_6, KEY_7, KEY_8, KEY_9,
JL_KEY_0, JL_KEY_1, JL_KEY_2, JL_KEY_3, JL_KEY_4,
JL_KEY_5, JL_KEY_6, JL_KEY_7, JL_KEY_8, JL_KEY_9,
KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
JL_KEY_SPACE, JL_KEY_ESCAPE, JL_KEY_RETURN, JL_KEY_TAB, JL_KEY_BACKSPACE,
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT,
JL_KEY_UP, JL_KEY_DOWN, JL_KEY_LEFT, JL_KEY_RIGHT,
KEY_LSHIFT, KEY_RSHIFT, KEY_LCTRL, KEY_LALT,
JL_KEY_LSHIFT, JL_KEY_RSHIFT, JL_KEY_LCTRL, JL_KEY_LALT,
KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5,
KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10,
JL_KEY_F1, JL_KEY_F2, JL_KEY_F3, JL_KEY_F4, JL_KEY_F5,
JL_KEY_F6, JL_KEY_F7, JL_KEY_F8, JL_KEY_F9, JL_KEY_F10,
KEY_COUNT
JL_KEY_COUNT
} jlKeyE;
typedef enum {
MOUSE_BUTTON_NONE = 0,
MOUSE_BUTTON_LEFT,
MOUSE_BUTTON_RIGHT,
MOUSE_BUTTON_MIDDLE,
JL_MOUSE_BUTTON_NONE = 0,
JL_MOUSE_BUTTON_LEFT,
JL_MOUSE_BUTTON_RIGHT,
JL_MOUSE_BUTTON_MIDDLE,
MOUSE_BUTTON_COUNT
JL_MOUSE_BUTTON_COUNT
} jlMouseButtonE;
typedef enum {
JOYSTICK_0 = 0,
JOYSTICK_1,
JL_JOYSTICK_0 = 0,
JL_JOYSTICK_1,
JOYSTICK_COUNT
JL_JOYSTICK_COUNT
} jlJoystickE;
typedef enum {
JOY_BUTTON_0 = 0,
JOY_BUTTON_1,
JL_JOY_BUTTON_0 = 0,
JL_JOY_BUTTON_1,
JOY_BUTTON_COUNT
JL_JOY_BUTTON_COUNT
} jlJoyButtonE;
#define JOYSTICK_AXIS_MAX 127
#define JOYSTICK_AXIS_MIN (-127)
#define JL_JOYSTICK_AXIS_MAX 127
#define JL_JOYSTICK_AXIS_MIN (-127)
// Control characters delivered through jlInputGetChar alongside
// printable ASCII. Everything else below 0x20, plus 0x7F, is
// filtered out of the queue.
// printable ASCII. Everything else below 0x20, and everything above
// 0x7F, is filtered out of the queue.
#define JL_CHAR_BACKSPACE 0x08
#define JL_CHAR_TAB 0x09
#define JL_CHAR_RETURN 0x0D
#define JL_CHAR_ESCAPE 0x1B
// The FORWARD delete (the key labelled Del/Delete on a PC, ST or Amiga keyboard - not backspace). A text
// editor needs both: backspace removes the character BEFORE the caret, this one removes the character
// UNDER it. 0x7F is ASCII DEL, which is what every one of these keyboards already reports.
// UNDER it. 0x7F is ASCII DEL. The IIgs never delivers it: its Delete key is backspace.
#define JL_CHAR_DELETE 0x7F
// Typed-character queue capacity (ring buffer; one slot stays empty,
@ -125,16 +126,19 @@ void jlInputPoll(void);
// JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE, JL_CHAR_DELETE. Shift, caps lock, and
// the keyboard layout are already applied by the backend, so
// punctuation and shifted symbols arrive correctly on every port.
// (The layout is the machine's own on IIgs/Amiga/ST/X68000; the DOS
// port translates with a fixed US layout -- see claudeDocs/input.md.)
// (The layout is the machine's own on IIgs/Amiga/ST/X68000, DVX's on
// DVX, and the OS's on the hosted ports; DOS and the PSP's on-screen
// keyboard use a fixed US layout. On the ST a key held with Alt types
// nothing.)
//
// The queue is refilled by jlInputPoll() -- no extra bring-up is
// needed. It holds JL_CHAR_QUEUE_SIZE - 1 characters; when full,
// further characters are dropped (drop-newest) until the app pops.
// OS auto-repeat may deliver repeated characters on ports whose
// keyboard services repeat (the caller manages repeat policy).
// A held key repeats its character on every port (the caller manages
// repeat policy).
// A chord types nothing on any port: Ctrl+M is not Return.
// 7-bit ASCII only; no Unicode / IME. Arrow keys are not characters
// and stay on the predicate API (jlKeyDown(KEY_UP) etc.).
// and stay on the predicate API (jlKeyDown(JL_KEY_UP) etc.).
// jlWaitForAnyKey empties the queue when it returns -- its dismissing
// keystroke is consumed by the wait, not delivered as text.
//
@ -142,18 +146,26 @@ void jlInputPoll(void);
// "byte or -1" is the interface contract consumers expect.
int jlInputGetChar(void);
// Block until the user presses any key. Internally polls via
// jlInputPoll, so per-port jlpInputPoll machinery (including
// audio-friendly IRQ-driven samplers) keeps working while the
// wait loop runs.
// Block until the user presses a key: one with a jlKeyE, or one that types
// a character (punctuation, which has no jlKeyE). Mouse and joystick
// buttons do not end it, and a key held when the wait starts must be let go
// and pressed again.
// Internally polls via jlInputPoll, so per-port jlpInputPoll machinery
// (including audio-friendly IRQ-driven samplers) keeps working while
// the wait loop runs.
void jlWaitForAnyKey(void);
// A key two physical keys give (Return and the keypad's Enter, an arrow and
// the keypad's 8, 2, 4, or 6, the left and right Ctrl or Alt) is down while
// either is held.
bool jlKeyDown(jlKeyE key);
bool jlKeyPressed(jlKeyE key);
bool jlKeyReleased(jlKeyE key);
// True when the platform confirmed an attached mouse at init (DOS: the INT 33h driver reset;
// Amiga/ST/IIgs/X68000: the OS mouse is always wired).
// Amiga: JoeyLib's input window opened; Win32/Win16: Windows reports one; SDL3/web: SDL reports one;
// IIgs/ST/X68000/DVX/Haiku/BeOS: always; PSP: never). The IIgs mouse has one button, the ST's and
// X68000's two.
bool jlMousePresent(void);
int16_t jlMouseX(void);
int16_t jlMouseY(void);
@ -161,6 +173,12 @@ bool jlMouseDown(jlMouseButtonE button);
bool jlMousePressed(jlMouseButtonE button);
bool jlMouseReleased(jlMouseButtonE button);
// Every stick has two buttons. On the ST's own joystick ports, and on
// the Amiga until a stick's second button is first pressed, a stick is
// taken to have one: a quick press of it is JL_JOY_BUTTON_0, down for
// the one poll after it is let go, and a press held for half a second
// is JL_JOY_BUTTON_1, down from then until it is let go: act on button
// 0 with jlJoyPressed, not jlJoyDown.
bool jlJoystickConnected(jlJoystickE js);
int8_t jlJoystickX(jlJoystickE js);
int8_t jlJoystickY(jlJoystickE js);
@ -168,22 +186,16 @@ bool jlJoyDown(jlJoystickE js, jlJoyButtonE button);
bool jlJoyPressed(jlJoystickE js, jlJoyButtonE button);
bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button);
// Re-enable joystick polling and recalibrate the resting (center)
// position. The IIgs port auto-disables polling after a short window
// of detecting no stick (saves ~3 ms/frame of busy-wait). It does NOT
// auto-re-probe -- the application must call this function to resume
// polling after plugging a stick in.
//
// The next poll after this call captures the stick's CURRENT raw
// position as the new center -- so the user must hold the stick
// centered when calling. Subsequent polls report position relative
// to that center; raw readings within `deadZone` units of the center
// clamp to 0 (use 0 to disable the dead zone).
//
// On platforms with truly digital sticks (Amiga / ST / DOS) the
// recalibration is a no-op -- those ports already report -1 / 0 / +1
// directly -- and `deadZone` is ignored. The function still clears
// any auto-disconnect state so polling resumes.
// Set the stick's dead zone (8 at jlInit): axis values within `deadZone` of 0 (on the
// -127..127 scale) read 0 on the ports with analog sticks (IIgs, DOS,
// DVX, hosted); use 0 to disable it. On the IIgs and DOS the call also
// re-centres the stick: the next reading after it is taken as the
// centre, so the user must hold the stick centred when calling. The
// IIgs port stops polling after about a second (60 polls) with no stick
// seen (an absent stick costs ~1 ms a frame of busy-wait) and does NOT
// re-probe by itself: this call resumes polling. DOS probes the game
// port again; the Amiga hands the port back to lowlevel.library's own
// detection. Elsewhere only the dead zone changes.
void jlJoystickReset(jlJoystickE js, uint8_t deadZone);
#endif

View file

@ -7,7 +7,9 @@
// Colors are 12-bit $0RGB (bits 11..8 red, 7..4 green, 3..0 blue).
// The library forces color 0 of every palette to black; jlPaletteSet
// silently masks that entry to $000 regardless of what the caller
// passed. See claudeDocs/DESIGN.md sections 5 and 10.
// passed, and jlSurfaceLoadFile does the same. Palette and SCB changes
// to the stage reach the display at the next jlStagePresent. See
// claudeDocs/DESIGN.md sections 5 and 10.
#ifndef JOEYLIB_PALETTE_H
#define JOEYLIB_PALETTE_H
@ -20,7 +22,9 @@
// uint16_t values in $0RGB format.
void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
// Read all 16 colors of a palette into out16.
// Read all 16 colors of a palette into out16: the values set (color 0
// forced to $000, bits 12-15 as passed), not what the display shows of
// them. A paletteIndex above 15 is ignored by both calls.
void jlPaletteGet(const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
// Assign a single palette index (0..15) to a single scanline (0..199).

View file

@ -71,88 +71,96 @@
// ----- Derived CPU markers -----
#if defined(JOEYLIB_PLATFORM_IIGS)
#define JOEYLIB_CPU_65816
#define JOEYLIB_ENDIAN_LITTLE
#define JOEYLIB_NATIVE_CHUNKY
#define JOEYLIB_PLATFORM_NAME "Apple IIgs"
#define JL_CPU_65816
#define JL_ENDIAN_LITTLE
#define JL_NATIVE_CHUNKY
#define JL_PLATFORM_NAME "Apple IIgs"
#elif defined(JOEYLIB_PLATFORM_AMIGA)
#define JOEYLIB_CPU_68000
#define JOEYLIB_ENDIAN_BIG
#define JOEYLIB_NATIVE_PLANAR
#define JOEYLIB_HAS_BLITTER
#define JOEYLIB_HAS_COPPER
#define JOEYLIB_PLATFORM_NAME "Commodore Amiga"
#define JL_CPU_68000
#define JL_ENDIAN_BIG
#define JL_NATIVE_PLANAR
#define JL_HAS_BLITTER
#define JL_HAS_COPPER
#define JL_PLATFORM_NAME "Commodore Amiga"
#elif defined(JOEYLIB_PLATFORM_ATARIST)
#define JOEYLIB_CPU_68000
#define JOEYLIB_ENDIAN_BIG
#define JOEYLIB_NATIVE_PLANAR
#define JL_CPU_68000
#define JL_ENDIAN_BIG
#define JL_NATIVE_PLANAR
// JOEYLIB_ATARI_STE is the STe build (make ste): the same port using the
// BLiTTER, DMA sound and the 4-bit palette, for STe and Mega STe only.
// BLiTTER, DMA sound, the 4-bit palette and (1040 STe) the enhanced
// joystick ports, for STe and Mega STe only.
#if defined(JOEYLIB_ATARI_STE)
#define JOEYLIB_PLATFORM_NAME "Atari STe"
#define JL_PLATFORM_NAME "Atari STe"
#else
#define JOEYLIB_PLATFORM_NAME "Atari ST"
#define JL_PLATFORM_NAME "Atari ST"
#endif
#elif defined(JOEYLIB_PLATFORM_DOS)
#define JOEYLIB_CPU_X86
#define JOEYLIB_ENDIAN_LITTLE
#define JOEYLIB_NATIVE_CHUNKY
#define JOEYLIB_PLATFORM_NAME "MS-DOS"
#define JL_CPU_X86
#define JL_ENDIAN_LITTLE
#define JL_NATIVE_CHUNKY
#define JL_PLATFORM_NAME "MS-DOS"
#elif defined(JOEYLIB_PLATFORM_X68000)
// The stage is four separate 1bpp planes in main RAM (src/x68000/halPlanar.c,
// the Amiga's layout), and a present copies its dirty rows into the text
// plane at $E00000, which is four 1bpp planes too. The text plane has one
// 16-colour palette, so per-line SCB palettes are reloaded as the beam scans
// by a CRTC raster interrupt (src/x68000/x68kRasterIsr.s).
#define JOEYLIB_CPU_68000
#define JOEYLIB_ENDIAN_BIG
#define JOEYLIB_NATIVE_PLANAR
#define JOEYLIB_PLATFORM_NAME "Sharp X68000"
#define JL_CPU_68000
#define JL_ENDIAN_BIG
#define JL_NATIVE_PLANAR
#define JL_PLATFORM_NAME "Sharp X68000"
#elif defined(JOEYLIB_PLATFORM_DVX)
// The DOS stage (chunky 4bpp, x86 sprite JIT and tile walker from
// src/x86/), shown in a DVX window, or on DVX's full screen, instead of
// on the VGA.
#define JOEYLIB_CPU_X86
#define JOEYLIB_ENDIAN_LITTLE
#define JOEYLIB_NATIVE_CHUNKY
#define JOEYLIB_PLATFORM_NAME "DVX"
#define JL_CPU_X86
#define JL_ENDIAN_LITTLE
#define JL_NATIVE_CHUNKY
#define JL_PLATFORM_NAME "DVX"
#elif defined(JOEYLIB_PLATFORM_HOSTED)
// The generic chunky stage, resolved through its SCB palettes into a
// window's RGB pixels by one backend (src/hosted/<backend>). Whatever CPU
// the host has: no machine code is generated, sprites are interpreted.
// the host has, sprites are interpreted -- except on Windows 3.x, whose
// 32-bit code runs on a 386 like DOS's: sprites are compiled to x86 code
// as on DOS (Win386's flat code and data selectors share one base, so the
// code arena runs as it is).
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
#define JOEYLIB_ENDIAN_BIG
#define JL_ENDIAN_BIG
#else
#define JOEYLIB_ENDIAN_LITTLE
#define JL_ENDIAN_LITTLE
#endif
#define JL_NATIVE_CHUNKY
#if defined(JOEYLIB_HOSTED_WIN16)
#define JL_CPU_X86
#else
#define JL_SPRITE_INTERPRETED
#endif
#define JOEYLIB_NATIVE_CHUNKY
#define JOEYLIB_SPRITE_INTERPRETED
#if defined(JOEYLIB_HOSTED_SDL3) && defined(__PSP__)
#define JOEYLIB_PLATFORM_NAME "PSP"
#define JL_PLATFORM_NAME "PSP"
#elif defined(JOEYLIB_HOSTED_SDL3)
#define JOEYLIB_PLATFORM_NAME "SDL3"
#define JL_PLATFORM_NAME "SDL3"
#elif defined(JOEYLIB_HOSTED_WIN32)
#define JOEYLIB_PLATFORM_NAME "Windows"
#define JL_PLATFORM_NAME "Windows"
#elif defined(JOEYLIB_HOSTED_WIN16)
#define JOEYLIB_PLATFORM_NAME "Windows 3.x"
#define JL_PLATFORM_NAME "Windows 3.x"
#elif defined(JOEYLIB_HOSTED_BE) && defined(__HAIKU__)
#define JOEYLIB_PLATFORM_NAME "Haiku"
#define JL_PLATFORM_NAME "Haiku"
#elif defined(JOEYLIB_HOSTED_BE)
#define JOEYLIB_PLATFORM_NAME "BeOS"
#define JL_PLATFORM_NAME "BeOS"
#else
#error "JoeyLib: JOEYLIB_PLATFORM_HOSTED needs a backend, e.g. -DJOEYLIB_HOSTED_SDL3"
#endif
#elif defined(JOEYLIB_PLATFORM_BLANK)
// Chunky little-endian is the simplest baseline for a new port: the generic
// backend gives you fully-working software rendering out of the box. Adjust
// these (or switch to JOEYLIB_NATIVE_PLANAR) once you know your hardware.
#define JOEYLIB_ENDIAN_LITTLE
#define JOEYLIB_NATIVE_CHUNKY
#define JOEYLIB_SPRITE_INTERPRETED
#define JOEYLIB_PLATFORM_NAME "Blank Template"
// these (or switch to JL_NATIVE_PLANAR) once you know your hardware.
#define JL_ENDIAN_LITTLE
#define JL_NATIVE_CHUNKY
#define JL_SPRITE_INTERPRETED
#define JL_PLATFORM_NAME "Blank Template"
#endif
// JOEYLIB_SPRITE_INTERPRETED: the port has no sprite code emitter (it links
// JL_SPRITE_INTERPRETED: the port has no sprite code emitter (it links
// src/generic/spriteEmitStub.c), so every sprite is drawn by the
// interpreter. A pre-compiled bank (.spc) still gives it the cels' pixels;
// their machine code is skipped.
@ -193,7 +201,7 @@
#define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function
// (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord,
// the VBL heartbeat tick read -- no function override to register here.)
#define JL_HAS_WAIT_VBL // $C019 VBL poll, function
#define JL_HAS_WAIT_VBL // the VBL heartbeat count advancing, function
// audio: every real port implements the full engine
#define JL_HAS_AUDIO_INIT
#define JL_HAS_AUDIO_SHUTDOWN
@ -205,7 +213,6 @@
#define JL_HAS_AUDIO_PLAY_SFX_STREAM_RAW
#define JL_HAS_AUDIO_SET_PAN
#define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER
@ -233,7 +240,8 @@
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
#define JL_HAS_SERIAL_SET_RTS
// save files + disk space: delete (GS/OS Destroy), dir-ensure (GS/OS Create
// $2001, storageType 13), disk-free (GS/OS Volume $2008 via prefix 0).
// $2001, storageType 13), disk-free (GS/OS Volume $2008 on prefix 1's
// volume, where SAVES/ is).
#define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE
#define JL_HAS_DISK_FREE
@ -267,7 +275,7 @@
#define JL_HAS_SURFACE_ALLOC_PIXELS // amiga: NULL (planar storage), function
#define JL_HAS_SURFACE_ALLOC_PORT_DATA // amiga AmigaPlanarT + chip planes, function
#define JL_HAS_SURFACE_FREE_PORT_DATA // amiga FreeMem planes, function
#define JL_HAS_FRAME_HZ // PAL 50, function
#define JL_HAS_FRAME_HZ // 50 PAL / 60 NTSC, measured at init, function
#define JL_HAS_FRAME_COUNT // VBL-server counter, function
#define JL_HAS_WAIT_VBL // WaitTOF, function
// audio: every real port implements the full engine
@ -280,7 +288,6 @@
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_SET_PAN
#define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER
@ -305,6 +312,8 @@
#define JL_HAS_SERIAL_FLUSH
// Application-driven DTR: how a program hangs up (jlSerialSetDtr).
#define JL_HAS_SERIAL_SET_DTR
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
#define JL_HAS_SERIAL_SET_RTS
// save files + disk space (CreateDir/DeleteFile/Info via dos.library)
#define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE
@ -341,7 +350,7 @@
#define JL_HAS_SURFACE_ALLOC_PIXELS // st: NULL (planar storage), function
#define JL_HAS_SURFACE_ALLOC_PORT_DATA // st StPlanarT + interleaved buffer, function
#define JL_HAS_SURFACE_FREE_PORT_DATA // st free buffer, function
#define JL_HAS_FRAME_HZ // PAL 50, function
#define JL_HAS_FRAME_HZ // the machine's 50 / 60 (71 mono), function
#define JL_HAS_FRAME_COUNT // VBL-ISR counter, function
#define JL_HAS_WAIT_VBL // frame-counter wait, function
#define JL_HAS_MILLIS_ELAPSED // Supexec 200 Hz tick, function
@ -354,13 +363,13 @@
#define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER
#define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK
#define JL_HAS_MOD_MIX_RUN // the module player's 8-bit mix, src/m68k/modMix.s
// lifecycle / present / input (every real port implements)
#define JL_HAS_INIT
#define JL_HAS_SHUTDOWN
@ -382,6 +391,8 @@
#define JL_HAS_SERIAL_FLUSH
// Application-driven DTR: how a program hangs up (jlSerialSetDtr).
#define JL_HAS_SERIAL_SET_DTR
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
#define JL_HAS_SERIAL_SET_RTS
// save files + disk space (POSIX mkdir/remove + GEMDOS Dfree)
#define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE
@ -395,7 +406,7 @@
// (The planar dual-write hooks are not linked to generics: every chunky port
// elides them to ((void)0) in port.h's shared chunky block, PERF-AUDIT #42.)
#define JL_HAS_FRAME_HZ // VGA ~70, function
#define JL_HAS_FRAME_COUNT // $3DA retrace edge counter, function
#define JL_HAS_FRAME_COUNT // from the millisecond clock (70 a second), function
#define JL_HAS_WAIT_VBL // $3DA retrace poll, function
#define JL_HAS_MILLIS_ELAPSED // uclock / audio-ISR tick, function
// audio: every real port implements the full engine
@ -407,7 +418,6 @@
#define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER
@ -439,11 +449,9 @@
#define JL_HAS_SAVE_DELETE
#define JL_HAS_DISK_FREE
#elif defined(JOEYLIB_PLATFORM_X68000)
// Bring-up: platform SERVICES only (src/x68000/hal.c), plus the serial
// add-on (src/x68000/serial.c). Everything graphical comes from the portable
// generics, so the port renders correctly before any native primitive
// exists. Native ops arrive with the storage-model decision.
// Planar graphics, adapted from the Amiga (identical surface layout).
// Platform services in src/x68000/hal.c, the serial add-on in
// src/x68000/serial.c, and planar graphics adapted from the Amiga
// (identical surface layout, src/x68000/halPlanar.c).
#define JL_HAS_SURFACE_CLEAR
#define JL_HAS_DRAW_PIXEL
#define JL_HAS_DRAW_LINE
@ -474,7 +482,7 @@
#define JL_HAS_SURFACE_FREE_PORT_DATA
#define JL_HAS_INIT // _iocs_crtmod display bring-up
#define JL_HAS_SHUTDOWN // restore the previous CRT mode
#define JL_HAS_PRESENT // expand the chunky stage into GVRAM
#define JL_HAS_PRESENT // copy the stage's dirty plane rows into the text plane
#define JL_HAS_INPUT_INIT
#define JL_HAS_INPUT_SHUTDOWN
#define JL_HAS_INPUT_POLL // IOCS work area keys/mouse, PPI joystick ports
@ -489,7 +497,8 @@
#define JL_HAS_DISK_FREE
#define JL_HAS_SAVE_DELETE
#define JL_HAS_SAVE_DIR_ENSURE
// Serial add-on: IOCS polled RS-232C, structurally the ST HAL.
// Serial add-on: RSDRV.SYS transmit and line setup, receive on the
// port's own SCC interrupt (src/x68000/x68kSccRxIsr.s).
#define JL_HAS_SERIAL_OPEN
#define JL_HAS_SERIAL_CLOSE
#define JL_HAS_SERIAL_POLL
@ -499,17 +508,21 @@
#define JL_HAS_SERIAL_FLUSH
// Application-driven DTR: how a program hangs up (jlSerialSetDtr).
#define JL_HAS_SERIAL_SET_DTR
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
#define JL_HAS_SERIAL_SET_RTS
// Audio. CHIP side on the YM2151 (OPM): tone voices + noise, which is what
// the portable JYM1 tracker rides, with no CPU mixing.
#define JL_HAS_AUDIO_INIT
#define JL_HAS_AUDIO_SHUTDOWN
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER
#define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK
// SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX
#define JL_HAS_MOD_MIX_RUN // the module player's 8-bit mix, src/m68k/modMix.s
#define JL_HAS_CODE_WRITTEN // the X68030's caches, after codegen
// SAMPLE side (src/x68000/audioPcm.c): the module player + the shared 5-slot SFX
// overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate
// device from the OPM, so chip music and digital audio do not contend.
#define JL_HAS_AUDIO_PLAY_MOD
@ -554,7 +567,6 @@
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_SET_PAN
#define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER
@ -591,10 +603,10 @@
#define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE
#define JL_HAS_DISK_FREE
// audio: a software mixer (MOD through libxmp-lite, the SFX slots, three
// square-wave voices, YM-style noise and the speaker tone) run on the
// program's thread, so the tick callback and the stream fills run there
// too and the critical sections stay the generic no-ops.
// audio: a software mixer (MOD through the module player, the SFX slots, three
// square-wave voices and YM-style noise) run on the program's thread, so
// the tick callback and the stream fills run there too and the critical
// sections stay the generic no-ops.
#define JL_HAS_AUDIO_INIT
#define JL_HAS_AUDIO_SHUTDOWN
#define JL_HAS_AUDIO_PLAY_MOD
@ -604,7 +616,6 @@
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_SET_PAN
#define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER
@ -646,9 +657,9 @@
// ----- Library version -----
#define JOEYLIB_VERSION_MAJOR 1
#define JOEYLIB_VERSION_MINOR 0
#define JOEYLIB_VERSION_PATCH 0
#define JOEYLIB_VERSION_STRING "1.0.0"
#define JL_VERSION_MAJOR 1
#define JL_VERSION_MINOR 0
#define JL_VERSION_PATCH 0
#define JL_VERSION_STRING "1.0.0"
#endif

View file

@ -5,11 +5,13 @@
// Stage present.
//
// jlStagePresent flips the library-owned stage (back-buffer) to the
// display. On chunky platforms (IIgs, DOS) this is a direct copy; on
// planar platforms (Amiga, Atari ST) this is a chunky-to-planar
// conversion. Drawing primitives mark per-row dirty ranges on the
// stage as a side effect, so jlStagePresent only touches rows that
// actually changed since the last present. See claudeDocs/DESIGN.md.
// display. Where the stage already has the display's layout it is a
// copy (the IIgs's 4bpp pixels; the planes of the Amiga, ST, and
// X68000); DOS, DVX, and the hosted ports expand each 4-bit pixel to
// their display's 8-bit or RGB pixels. Drawing primitives mark per-row
// dirty ranges on the stage as a side effect, so jlStagePresent only
// touches rows that actually changed since the last present. Palette and
// SCB changes reach the display at the present too.
#ifndef JOEYLIB_PRESENT_H
#define JOEYLIB_PRESENT_H
@ -23,11 +25,9 @@
// callers only need to call jlStagePresent at the end of a frame --
// everything they drew shows up. Rows that nothing touched since the
// last present are skipped by the per-port present using the per-row
// dirty bands.
//
// To present a region you didn't draw with the standard primitives
// (e.g. direct framebuffer poking), call surfaceMarkDirtyRect on
// the same rect first, then jlStagePresent.
// dirty bands. Every public call that writes the stage's pixels marks
// them; the API has no other way to write them, and no call to mark
// rows by hand.
void jlStagePresent(void);
#endif

View file

@ -18,14 +18,17 @@
// Atari ST -- BIOS Aux (Modem port) via XBIOS Rsconf; TOS buffers RX.
// X68000 -- SCC channel A: Human68k's RSDRV.SYS for transmit and line
// setup (75-38400 bps, RTS/CTS), REQUIRED -- the open fails
// without it; its own RX interrupt fills the ring.
// without it; its own RX interrupt fills the ring (when
// opened in supervisor mode, as jlInit leaves every program).
// DVX -- a COM port through DVX's serial library.
// Win32/16 -- a COM port through Windows' communications calls.
// SDL3, the web, the PSP, Haiku, and BeOS have no serial port.
//
// Model: non-blocking + buffered. Received bytes accumulate in a background
// ring (a hardware IRQ on DOS, the X68000 and the IIgs, the OS on Amiga/ST);
// jlSerialRead / jlSerialAvailable never block. Call jlSerialPoll()
// once per game frame -- it is a no-op where the IRQ/OS already fills the
// ring, and the hardware drain where it does not, so calling it every frame is
// always correct. Transmit is a bounded poll.
// ring (a hardware IRQ on DOS, the X68000 and the IIgs, the OS on Amiga/ST,
// DVX, and Windows); jlSerialRead / jlSerialAvailable never block. Call
// jlSerialPoll() once per game frame. Transmit never waits long: a bounded
// poll on most ports, a queue on the Amiga and Win16.
//
// Received bytes CAN be lost. Each port's receive hardware holds 1 to 3 bytes
// (16 on a 16550), ~1-3 ms at 9600 baud, and anything that keeps interrupts off
@ -33,12 +36,12 @@
// open (on the IIgs it cuts its long interrupts-off stretches to ~1.7 ms while
// one is), but these are not: IIgs disk access (GS/OS holds interrupts off
// 65-220 ms per 3.5" floppy access, so every file call, asset load and
// jlAudioInit), IIgs jlAudioPlayMod (~12 ms per KB of samples), a Super Serial
// Card above 2400 baud, and the program's own interrupt masking. Disk access
// on the other ports is unmeasured. Schedule them between messages, pause the
// peer first (jlSerialSetRts(false) or XOFF), or use a protocol that
// retransmits. docs/ApiSerial.adoc, "What Can Lose Received Bytes", has the
// per-port table.
// jlInit, which reads the audio replayer), IIgs jlAudioPlayMod (~12 ms per
// KB of samples), a Super Serial Card above 2400 baud, and the program's own
// interrupt masking. Disk access on the other ports is unmeasured. Schedule
// them between messages, pause the peer first (jlSerialSetRts(false) or
// XOFF), or use a protocol that retransmits. docs/ApiSerial.adoc, "What Can
// Lose Received Bytes", has the per-port table.
//
// A failed jlSerialOpen is non-fatal: it returns false and every other call is
// a safe no-op (Available 0, Read 0), exactly like a failed audio init.
@ -54,10 +57,13 @@
// JL_SERIAL_PRINTER -- IIgs printer port (SCC channel A). Only the IIgs has
// a second built-in port; elsewhere == DEFAULT.
// JL_SERIAL_SLOT -- a card/port selected by jlSerialConfigT.unit: IIgs
// Super Serial Card slot (1..7); DOS COM number (1..4);
// Amiga serial.device unit.
// JL_SERIAL_DEFAULT -- the platform's natural RS-232 port (IIgs modem, DOS
// COM1, ST Modem port, Amiga unit 0).
// Super Serial Card slot (1..7); DOS or DVX COM number
// (1..4); Win16 COM (1..9); Win32 COM (1..255); Amiga
// serial.device unit. The ST and X68000 have one port
// and open it for every device.
// JL_SERIAL_DEFAULT -- the platform's natural RS-232 port (IIgs modem, DOS,
// DVX, and Windows COM1, ST Modem port, Amiga unit 0,
// X68000 RS-232C).
typedef enum {
JL_SERIAL_DEFAULT = 0,
JL_SERIAL_MODEM,
@ -78,24 +84,28 @@ typedef enum {
} jlSerialFlowE;
// Line configuration. `unit` disambiguates JL_SERIAL_SLOT (see jlSerialDeviceE)
// and is ignored for the other devices.
// and is ignored for the other devices. A field left 0 takes a default (baud
// 9600, 8 data bits, 1 stop bit); data bits outside 5..8, stop bits other
// than 1 or 2, or a parity or flow the enums do not name fail the open.
typedef struct {
uint32_t baud; // 300 .. 115200 (each port clamps to what it supports)
uint32_t baud; // 300 .. 115200 (each port runs at the nearest rate it has: docs/ApiSerial.adoc)
uint8_t dataBits; // 5 .. 8
uint8_t stopBits; // 1 or 2
jlSerialParityE parity;
jlSerialFlowE flow;
uint8_t unit; // JL_SERIAL_SLOT: IIgs slot 1..7 / DOS COM 1..4 / Amiga unit
uint8_t unit; // JL_SERIAL_SLOT: IIgs slot 1..7 / DOS, DVX COM 1..4 / Win16 COM 1..9 / Win32 COM 1..255 / Amiga unit
} jlSerialConfigT;
// Open + configure the port. Only one port is open at a time; a second
// jlSerialOpen without a jlSerialClose fails. Returns false (non-fatal) if the
// port cannot be opened or the platform has no serial support -- every other
// call then no-ops. On the X68000 it also fails when RSDRV.SYS is not loaded,
// and jlLastError() says so.
// call then no-ops. It also fails, with jlLastError() saying why, when no UART
// answers at the DOS COM port, when no Super Serial Card answers in the IIgs
// slot, and when RSDRV.SYS is not loaded on the X68000.
bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config);
// Close the open port and release its resources. Safe to call when not open.
// Bytes jlSerialWrite accepted are still sent.
// A port still open when the program exits is closed then (the first
// successful jlSerialOpen registers an atexit handler): where the port's RX
// interrupt is taken over (DOS, X68000, IIgs), the vector points into the
@ -109,10 +119,7 @@ bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config);
// (Hayes S25: 50 ms by default), then close: the line stays down.
void jlSerialClose(void);
// Pump the RX/TX buffers. Call once per game frame. Next to a no-op where a
// hardware IRQ or the OS already services the port; drains the hardware into
// the RX ring where nothing else does (an IIgs Super Serial Card whose
// interrupt switch is off). Calling it every frame is always correct.
// Pump the RX/TX buffers. Call once per game frame.
void jlSerialPoll(void);
// Bytes waiting in the RX buffer, readable now without blocking.
@ -123,7 +130,8 @@ uint16_t jlSerialAvailable(void);
uint16_t jlSerialRead(uint8_t *buf, uint16_t max);
// Send `len` bytes; returns the count accepted (may be < len if a flow-
// controlled peer or a full transmit path stalls). Transmit is a bounded poll.
// controlled peer or a full transmit path stalls). Never waits long: a
// bounded poll on most ports; the Amiga and Win16 queue instead.
uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len);
// Single-byte convenience. jlSerialReadByte returns -1 when none is ready.

View file

@ -17,13 +17,15 @@
//
// Performance contract: jlSpriteDraw should be at least as fast as the
// IIgs reference (the DESIGN.md spec calls for runtime-compiled draw
// code per CPU). Every port has a runtime emitter (DOS x86, Amiga/ST/
// X68000 68k planar, IIgs 65816); jlSpriteCompile compiles the shift/op
// variants into the codegen arena and the draw/save/restore calls
// dispatch to the compiled routines, falling back to the interpreter
// for clipped draws or uncompiled variants.
// jlSpritePrewarm compiles ahead of the first draw so the emit cost
// does not land mid-frame.
// code per CPU). The machine ports have runtime emitters (IIgs 65816,
// Amiga/X68000 68k planar, ST 68k interleaved, DOS/DVX x86), and so does
// Win16 (x86, as DOS);
// jlSpriteCompile compiles the shift/op variants into the codegen arena
// and the draw/save/restore calls dispatch to the compiled routines,
// falling back to the interpreter for clipped draws or uncompiled
// variants. Nothing compiles on demand: compile while loading, or every
// draw interprets. The other hosted ports (JL_SPRITE_INTERPRETED) have no
// emitter and always interpret.
#ifndef JOEYLIB_SPRITE_H
#define JOEYLIB_SPRITE_H
@ -49,7 +51,7 @@ typedef struct jlSpriteT jlSpriteT;
// stack-allocated jlSpriteBackupT plus a caller-owned byte buffer keeps
// the runtime allocation-free.
typedef struct {
jlSpriteT *sprite;
jlSpriteT *sprite;
int16_t x;
int16_t y;
uint16_t width; // pixels
@ -69,8 +71,7 @@ typedef struct {
// Returns the number of cels actually written into outCels[]. Returns
// 0 if the file is missing, the magic / target byte is wrong, or no
// cel could be allocated.
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels,
uint16_t maxCels, uint16_t *outPalette);
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette);
// Load up to maxCels cels from a baked .spc PRE-COMPILED sprite bank
// (tools/spritebake output for THIS platform). Identical contract to
@ -79,27 +80,29 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels,
// full compiled speed with NO runtime jlSpriteCompile. A cel that does not fit
// the arena is loaded uncompiled (interpreter fallback), never an error -- so
// this is a drop-in, faster-starting replacement for jlSpriteBankLoad + a
// jlSpriteCompile/Prewarm pass. Returns 0 if the file is missing or its magic
// / target / shift-count does not match this build.
uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels,
uint16_t maxCels, uint16_t *outPalette);
// jlSpriteCompile pass. Returns 0 if the file is missing, its magic /
// target / shift-count does not match this build, its cel size is out of
// range, or the 64 KB buffer a cel's code is read into cannot be allocated
// (the interpreting ports read it too, then drop the code).
uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette);
// Write an already-compiled sprite bank to a .spc cache file (the runtime twin
// of the offline tools/spritebake). Call after jlSpriteBankLoad + a
// jlSpriteCompile pass so a later launch can jlSpriteBankLoadPrecompiled the
// cache and skip the JIT -- i.e. SHIP the small cross-platform .spr, compile
// once, and cache the (large) per-platform compiled bank on first run. `cels`
// are `count` sprites (uncompiled ones are cached as-is and JIT again next
// run); `palette` (or NULL) is embedded. Returns false if the destination
// isn't writable (e.g. a write-protected disk), which is harmless -- the app
// just keeps compiling at startup.
bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels,
uint16_t count, const uint16_t *palette);
// are `count` sprites, all of one size (uncompiled ones are cached
// uncompiled, and nothing compiles them at load: jlSpriteCompile any whose
// jlSpriteCompiledSize is 0); `palette` (or NULL) is embedded. Returns false
// if the cels differ in size or the destination isn't writable (e.g. a
// write-protected disk), which is harmless -- the app just keeps compiling
// at startup.
bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, uint16_t count, const uint16_t *palette);
// Wrap a tile-data blob in a jlSpriteT. The tile data must outlive the
// jlSpriteT; we do not copy it. Returns NULL if widthTiles or
// heightTiles is 0, or if the codegen arena cannot fit a placeholder
// entry for this sprite.
// jlSpriteT; we do not copy it. Returns NULL if tileData is NULL,
// widthTiles or heightTiles is 0, or memory runs out. The codegen arena
// is not touched until jlSpriteCompile.
jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles);
// Release a jlSpriteT and any codegen entries cached for it. The tile
@ -107,12 +110,17 @@ jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t h
// owns that buffer.
void jlSpriteDestroy(jlSpriteT *sp);
// Compile the sprite's draw routines into the codegen arena. After
// this returns true, jlSpriteDraw uses the compiled fast path on
// platforms where the emitter is wired (all four ports). Returns
// false if the arena is full (caller may run jlSpriteCompact and
// retry), the platform doesn't have a real emitter yet, or the
// sprite has no source tile data.
// Compile the sprite's draw, save, and restore routines into the codegen
// arena. jlSpriteDraw never compiles on demand, so call this while
// loading (or load a pre-compiled bank). After it returns true the draw,
// save, and restore calls use the compiled routines that fit: a short
// arena keeps only the x-on-an-8-pixel-boundary draws on the Amiga, ST,
// and X68000, a routine over 16 KB is left out, and anything not compiled
// (and every clipped draw) interprets. Returns false if the arena is full
// even after compacting it (jlLastError says so), the sprite has no
// source tile data, the compiler's 64 KB working buffer cannot be
// allocated, no routine fits, or the port has no emitter
// (JL_SPRITE_INTERPRETED); the sprite still draws, interpreted.
//
// Idempotent: calling on a sprite that's already compiled is a
// no-op and returns true.
@ -125,12 +133,6 @@ bool jlSpriteCompile(jlSpriteT *sp);
// the actual per-cel cost of the current platform's emitter.
uint32_t jlSpriteCompiledSize(const jlSpriteT *sp);
// Hint that this sprite will be drawn soon. Currently a wrapper
// around jlSpriteCompile that ignores the return value, kept for API
// symmetry with the rest of the library and for callers that don't
// care about compile success.
void jlSpritePrewarm(jlSpriteT *sp);
// Draw the sprite at pixel (x,y) on the destination surface. Pixels
// equal to color 0 in the sprite source are skipped (transparent).
// Off-surface portions are clipped.
@ -144,27 +146,34 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
// sprites and on ports without a compiled fast path.
void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
// Worst-case backup capacity for a widthTiles x heightTiles sprite at
// ANY x, on every port. The planar 68k compiled save/restore operates
// on the 16-pixel-group-aligned window covering the sprite (x rounded
// DOWN to a 16-px boundary, width UP), so the worst case is one extra
// 16-px group: (ceil(w/16) + 1) * 8 bytes per row. This also covers the
// chunky ports' (w/2 + 1) * h trivially. Give the backing array 2-byte
// alignment (e.g. uint16_t backing or __attribute__((aligned(2)))) --
// odd buffer pointers route save/restore to the slower interpreted
// paths on planar ports (68000 word ops require even addresses).
#define JOEY_SPRITE_BACKUP_BYTES(_wT, _hT) \
((uint16_t)((((((uint16_t)(_wT) * 8u) + 15u) >> 4) + 1u) * 8u * ((uint16_t)(_hT) * 8u)))
// How many _type elements a backup buffer needs for a widthTiles x
// heightTiles sprite at ANY x, on every port (rounded up), so that
//
// static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)];
//
// is big enough, and jlSpriteBackupLength(uint8_t, w, h) is the size in
// bytes (for jlAlloc, say). The worst case is the 16-pixel-group-aligned
// region covering the sprite (x rounded DOWN to a 16-px boundary, width
// UP): one extra 16-px group, (ceil(w/16) + 1) * 8 bytes per row. Give
// the buffer 2-byte alignment (a uint16_t array has it): on the Amiga,
// ST, and X68000 an odd address sends save and restore down a much
// slower path, and on the Amiga and ST it crashes the trusted variants.
#define jlSpriteBackupLength(_type, _wT, _hT) \
((uint16_t)((((((((uint16_t)(_wT) * 8u) + 15u) >> 4) + 1u) * 8u * ((uint16_t)(_hT) * 8u)) + sizeof(_type) - 1u) / sizeof(_type)))
// Capture the destination region a subsequent jlSpriteDraw at the same
// (x,y) would write to. backup->bytes must have at least
// JOEY_SPRITE_BACKUP_BYTES(widthTiles, heightTiles) bytes of capacity
// for fully in-bounds draws; for clipped draws only the visible bytes
// are stored. The captured region's exact geometry is reported in
// backup->x/width/height/sizeBytes -- on planar ports (Amiga, ST) it is
// the 16-px-aligned window containing the sprite, so the region a
// caller must not invalidate before restoring is the RECORDED window,
// not just the sprite rect.
// (x,y) would write to. backup->bytes must have room for
// jlSpriteBackupLength(uint8_t, widthTiles, heightTiles) bytes for
// fully in-bounds draws; for clipped draws only the visible bytes are
// stored. The captured region's exact geometry is reported in
// backup->x/y/width/height/sizeBytes: x is rounded down to even on every
// port, a compiled save on the Amiga, ST, and X68000 takes the 16-px-
// aligned region containing the sprite, a compiled save on the IIgs only
// the rows holding an opaque pixel, and a clipped save the visible part.
// So the region a caller must not invalidate before restoring is the
// RECORDED region, not just the sprite rect. On the IIgs the buffer must
// not cross a 64 KB bank boundary. With backup->bytes NULL nothing is
// copied: the call only fills in the region a save would record.
void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
// TRUSTED save-under (P7-1). Same trust contract as
@ -188,14 +197,18 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup);
// * s and backup (and backup->sprite, backup->bytes) are non-NULL,
// * backup was produced by a jlSpriteSaveUnder that took the COMPILED
// path (a "trusted" backup: 16-px-aligned x/width on planar ports,
// even x/width on chunky, backup->height == the sprite's pixel
// height, buffer at least 2-byte aligned),
// * the recorded window is fully on the destination surface.
// even x/width on chunky, backup->height == the rows the compiled
// save covers -- the sprite's pixel height, or on the IIgs the rows
// holding an opaque pixel),
// * on the Amiga and ST, backup->bytes is at least 2-byte aligned (the
// trusted save and save-and-draw need that too),
// * the recorded region is fully on the destination surface.
// It is the expert inner-loop entry for a game that pre-compiles its
// sprites (jlSpritePrewarm) and already clamps positions on-screen --
// sprites (jlSpriteCompile) and already clamps positions on-screen --
// use jlSpriteRestoreUnder anywhere those invariants are not statically
// guaranteed. Anything the trusted fast path does not recognize (an
// uncompiled shift, a non-stage / non-16-row window on the 68k ports)
// uncompiled shift, a non-stage / non-16-row region on the 68k and x86
// ports)
// falls back to the fully-validated jlSpriteRestoreUnder, so a
// conservative caller degrades safely rather than corrupting -- but the
// NULL / on-surface guarantees above are still the caller's.
@ -229,9 +242,10 @@ void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t
// the source surface can be modified afterwards. Width and height
// are in TILES (each tile = 8x8 pixels). x and y are in pixels and
// must be aligned to a tile boundary (multiple of 8) on the source
// surface; misaligned coordinates return NULL.
jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y,
uint8_t widthTiles, uint8_t heightTiles);
// surface. Returns NULL if src is NULL, a dimension is 0, x or y is
// misaligned, the region is not wholly on the surface, or memory runs
// out.
jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y, uint8_t widthTiles, uint8_t heightTiles);
// Defragment the codegen arena. Walks live sprite slots and
// memmoves them down to consolidate free space; any holes left by
@ -245,7 +259,10 @@ void jlSpriteCompact(void);
// Arena introspection. Used to gauge whether jlSpriteCompact is
// worth running, or whether the codegenBytes budget needs to grow.
// Free space is (Total - Used), but it may be fragmented across
// holes until jlSpriteCompact runs.
// holes until jlSpriteCompact runs. On the IIgs Total is what jlInit was
// granted, in blocks of at most 64 KB that compacting never merges (one
// sprite's code must fit in one). The interpreting ports still allocate
// the arena: Total reports it and Used stays 0.
uint32_t jlSpriteCodegenBytesUsed(void);
uint32_t jlSpriteCodegenBytesTotal(void);

View file

@ -6,10 +6,11 @@
//
// All surfaces are 320x200 16-color images. In-memory storage is
// target-native: chunky 4bpp packed (two pixels per byte, high nibble
// is the left pixel) on IIgs and DOS; native planar (4 separate
// bitplanes on Amiga, word-interleaved planes on Atari ST) on the
// 68k ports. Public API entry points speak in color indices (0..15)
// regardless, so game code does not see the storage format.
// is the left pixel) on the IIgs, DOS, DVX, and the hosted ports;
// native planar (4 separate bitplanes on the Amiga and X68000,
// word-interleaved planes on the Atari ST) on the 68k ports. Public API
// entry points speak in color indices (0..15) regardless, so game code
// does not see the storage format.
//
// Each surface carries its own 200-entry SCB (scanline control byte)
// table and a 16-by-16 $0RGB palette set. See claudeDocs/DESIGN.md section 6.
@ -22,14 +23,14 @@
// ----- Fixed surface dimensions (library contract) -----
#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 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 SURFACE_PALETTE_COUNT 16
#define SURFACE_COLORS_PER_PALETTE 16
#define SURFACE_PALETTE_ENTRIES (SURFACE_PALETTE_COUNT * SURFACE_COLORS_PER_PALETTE)
#define JL_SURFACE_PALETTE_COUNT 16
#define JL_SURFACE_COLORS_PER_PALETTE 16
#define JL_SURFACE_PALETTE_ENTRIES (JL_SURFACE_PALETTE_COUNT * JL_SURFACE_COLORS_PER_PALETTE)
// ----- Opaque public type -----
@ -57,16 +58,16 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src);
// Copy a pixel rectangle from src into dst at the SAME position (no
// repositioning, so planar ports never shift bits). The rect is
// clipped to the surface, then widened OUTWARD to the 16-pixel group
// grid ON EVERY PORT -- the copied window is x rounded down and
// x + w rounded up to multiples of 16 (same window rule as
// grid ON EVERY PORT -- the copied region is x rounded down and
// x + w rounded up to multiples of 16 (same region rule as
// jlSpriteSaveUnder), so up to 15 extra pixels on each side are
// copied. Two consequences the caller owns: a snapped erase can
// clobber on-screen content up to 15 px outside the requested rect,
// and it silently invalidates any live jlSpriteBackupT whose
// recorded window it touches. Copies pixels only: SCBs and palettes
// recorded region it touches. Copies pixels only: SCBs and palettes
// are untouched. The intended use is restoring background
// rectangles from a pristine copy of the scene (the clean-buffer
// pattern) instead of per-sprite save/restore -- erase ALL windows,
// pattern) instead of per-sprite save/restore -- erase ALL regions,
// then redraw ALL sprites; do not mix with backup-restore in the
// same region.
void jlSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h);
@ -80,8 +81,12 @@ void jlSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, int16_t x, int16_
bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path);
// Read a surface from disk, overwriting dst. Returns false if the
// file is missing, the wrong size, or the read fails. dst keeps its
// identity (no reallocation).
// file is missing, the wrong size, has an SCB byte above 15, or the
// read fails; a read that fails part way can leave dst's pixels partly
// overwritten, but its SCBs and palettes are then untouched. Colour 0
// of every palette loaded is set to black, as jlPaletteSet does. dst
// keeps its identity (no reallocation). Neither file call sets
// jlLastError.
bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path);
// 32-bit content hash of the surface. NOT FNV-1a: a two-stream 16-bit
@ -95,7 +100,7 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path);
// low byte.
// SURFACE_HASH_MIX_BYTE in src/core/surfaceInternal.h is the single
// source of truth for the per-byte mix; a port override (the planar
// Amiga/ST readers) must reuse it byte-for-byte and feed the identical
// Amiga, ST, and X68000 readers) must reuse it byte-for-byte and feed the identical
// byte sequence, so the same logical pixels produce the same hash on
// every port regardless of internal storage format. Used by the UBER
// validation harness to pixel-compare ports against an IIgs golden

View file

@ -33,21 +33,23 @@
// ----- Constants -----
#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)
#define TILE_BLOCKS_PER_COL (SURFACE_HEIGHT / TILE_PIXELS_PER_SIDE)
#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)
#define JL_TILE_BLOCKS_PER_COL (JL_SURFACE_HEIGHT / JL_TILE_PIXELS_PER_SIDE)
// Sentinel for asciiMap entries that should not draw. jlDrawText
// advances the cursor past TILE_NO_GLYPH chars without writing.
#define TILE_NO_GLYPH ((uint16_t)0xFFFFu)
// advances the cursor past JL_TILE_NO_GLYPH chars without writing.
#define JL_TILE_NO_GLYPH ((uint16_t)0xFFFFu)
// ----- Types -----
// Stack-allocated 32-byte snapshot buffer for jlTileSnap / jlTilePaste.
// Target-native: plane-major on the Amiga and X68000, row by row with each
// row's four plane bytes together on the ST, chunky elsewhere.
typedef struct jlTileT {
uint8_t pixels[TILE_BYTES];
uint8_t pixels[JL_TILE_BYTES];
} jlTileT;
// ----- API -----
@ -55,17 +57,14 @@ typedef struct jlTileT {
// Copy the 8x8 block at (srcBx, srcBy) on src to (dstBx, dstBy) on
// dst, opaque. Out-of-range block coordinates on either side are
// silent no-ops; src and dst can be the same surface.
void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
// Like jlTileCopy but pixels equal to transparentIndex are skipped --
// the destination pixel keeps its original value. Use this for fonts
// (transparentIndex = 0 leaves the page background showing through
// glyph backgrounds) and for any tileset where some pixels are meant
// to be see-through.
void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy,
uint8_t transparentIndex);
void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex);
// Fill the 8x8 block at (bx, by) with a solid color. Equivalent to
// jlFillRect(s, bx*8, by*8, 8, 8, colorIndex) but skips the rect
@ -86,19 +85,19 @@ void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex);
//
// Returns the number of tiles actually written. Returns 0 if the
// file is missing, the magic / target byte is wrong, or maxTiles is 0.
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
bool *outValid, uint16_t *outPalette);
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles, bool *outValid, uint16_t *outPalette);
// Stream a baked .tbk straight onto a surface's 8x8 block grid: tile i
// lands at block (i % TILE_BLOCKS_PER_ROW, i / TILE_BLOCKS_PER_ROW),
// lands at block (i % JL_TILE_BLOCKS_PER_ROW, i / JL_TILE_BLOCKS_PER_ROW),
// the same row-major layout the bake tools author against. Use this
// instead of jlTileBankLoad + a jlTileT[] when the bank is too big for
// a caller-side buffer -- a full 1000-tile font sheet needs 32000
// bytes, which no IIgs example can heap-allocate (the clang libc heap
// is a few hundred bytes) or hold in bank-0 BSS. One file pass with a
// bytes, which no IIgs example can afford from the C heap (32 KB to
// 48 KB in all) or hold in bank-0 BSS. One file pass with a
// small internal scratch; per-target/magic validation and outPalette
// behave exactly like jlTileBankLoad. Blocks beyond the file's tile
// count keep their prior surface content. Returns tiles pasted.
// count keep their prior surface content; tiles past the surface's 1000
// blocks are ignored. Returns tiles pasted.
uint16_t jlTileBankLoadToSurface(const char *path, jlSurfaceT *dst, uint16_t *outPalette);
// Capture the 8x8 block at (bx, by) into the caller's jlTileT. Used
@ -118,8 +117,7 @@ void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in);
// to 4 bits. Opaque -- if you need transparent background, do
// jlTileFill(bg) first then mask separately, or follow up with a
// jlTileCopyMasked call.
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlTileT *in, uint8_t fgColor, uint8_t bgColor);
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor);
// Paste an 8x8 ONE-BIT-PER-PIXEL glyph at block (bx, by): bits[row] is
// that row of the glyph, most significant bit leftmost; a set bit draws
@ -129,13 +127,13 @@ void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by,
// This is the shape a character generator already has -- a font ROM, a
// C64 charset, anything 1bpp -- so nothing has to expand it into a
// jlTileT first. Prefer it over jlTilePasteMono for glyphs: a jlTileT
// is target-native (plane-major on the planar ports), whereas the
// is target-native (planar on the Amiga, ST, and X68000), whereas the
// silhouette a mono paste wants is chunky, so feeding one a tile that
// came from jlTileSnap is a format mismatch that draws the wrong shape.
// came from jlTileSnap or jlTileBankLoad is a format mismatch there that
// draws the wrong shape.
// Taking the bits directly makes the input unambiguous, and lets each
// port expand them once instead of twice.
void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by,
const uint8_t *bits, uint8_t fgColor, uint8_t bgColor);
void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor);
// Draw a NUL-terminated ASCII string at block (bx, by) using glyphs
@ -143,32 +141,33 @@ void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by,
//
// asciiMap is a 256-entry table mapping ASCII code to glyph location
// on fontSurface, encoded as a packed uint16_t: low byte = source
// blockX, high byte = source blockY. asciiMap[c] == TILE_NO_GLYPH
// causes that character to be skipped (cursor advances, nothing
// drawn). Glyph color 0 is treated as transparent so the underlying
// surface shows through glyph backgrounds.
// blockX, high byte = source blockY. asciiMap[c] == JL_TILE_NO_GLYPH,
// or an entry off the 40x25 grid, causes that character to be skipped
// (cursor advances, nothing drawn). Glyph color 0 is treated as
// transparent so the underlying surface shows through glyph backgrounds.
//
// The cursor wraps to the next row at the right edge and truncates
// at the bottom edge.
void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlSurfaceT *fontSurface, const uint16_t *asciiMap,
const char *str);
// at the bottom edge. A start off the grid uses up characters without
// drawing them: a start column past 39 drops the first character and
// carries on at the start of the next row; a start row past 24 draws
// nothing.
void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontSurface, const uint16_t *asciiMap, const char *str);
// Map entry value that leaves a cell untouched (nothing pasted, the
// destination shows through). Mirrors TILE_NO_GLYPH's role in
// destination shows through). Mirrors JL_TILE_NO_GLYPH's role in
// jlDrawText.
#define TILE_MAP_SKIP ((uint8_t)0xFFu)
#define JL_TILE_MAP_SKIP ((uint8_t)0xFFu)
// Paste a wTiles x hTiles rectangle of tiles in ONE call: the
// validation, dispatch, and dirty union are paid once for the whole
// map instead of per tile (the jlDrawText batching pattern
// generalized, NATIVE-PERF Phase 3). `tiles` is the tileset; `map`
// holds wTiles * hTiles tileset indices in row-major order
// (map[row * wTiles + col]). An index of TILE_MAP_SKIP leaves that
// (map[row * wTiles + col]). An index of JL_TILE_MAP_SKIP leaves that
// cell untouched; any other out-of-tileset index is the CALLER'S
// contract to avoid (the batch does not know the tileset length and
// does not range-check it). The whole rectangle must fit on the
// surface -- a map whose edge crosses TILE_BLOCKS_PER_ROW/COL is
// surface -- a map whose edge crosses JL_TILE_BLOCKS_PER_ROW/COL is
// rejected as a no-op, matching the per-tile ops' silent-reject
// convention. wTiles or hTiles of 0 is a no-op.
void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map);

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,10 +1,12 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# EmuTOS 1.4 (GPL): a free TOS replacement for the Atari ST emulator, the
# 512 KB US-English image. The gates and joey-run boot it instead of
# Atari's copyrighted TOS.
# Atari's copyrighted TOS. The German image, from the same download, is the
# input gate's national keyboard (its Alt layer types @ [ ] { } \).
compDescription="Atari ST firmware: EmuTOS 1.4 (free TOS replacement)"
compGroups="emulator:atarist"
@ -14,27 +16,31 @@ compUrl="https://sourceforge.net/projects/emutos/files/emutos/1.4/emutos-512k-1.
compSha256="1ef7bd25f61bcfc66d19debc2f0ebb0d5e5ba811ccd7f0fddfcb105bb72d1663"
compPin="$compSha256"
imgPath="$JOEY_FIRMWARE/atarist/emutos-512k.img"
imgPathDe="$JOEY_FIRMWARE/atarist/emutos-512k-de.img"
compInstall() {
local tmp
tmp="$(freshBuildDir "$compId")"
unzip -oq "$(fetchFile "$compUrl" "$compSha256" emutos-512k-1.4.zip)" 'emutos-512k-1.4/etos512us.img' -d "$tmp"
unzip -oq "$(fetchFile "$compUrl" "$compSha256" emutos-512k-1.4.zip)" 'emutos-512k-1.4/etos512us.img' 'emutos-512k-1.4/etos512de.img' -d "$tmp"
install -D -m 644 "$tmp/emutos-512k-1.4/etos512us.img" "$imgPath"
install -D -m 644 "$tmp/emutos-512k-1.4/etos512de.img" "$imgPathDe"
dropBuildDir "$compId"
}
compInstalled() {
[ -f "$imgPath" ]
[ -f "$imgPath" ] && [ -f "$imgPathDe" ]
}
compVerify() {
[ "$(sha1sum "$imgPath" | cut -d' ' -f1)" = "7d28ee58f00854954073198983572e6921b87a21" ]
[ "$(sha1sum "$imgPath" | cut -d' ' -f1)" = "7d28ee58f00854954073198983572e6921b87a21" ] &&
[ "$(sha1sum "$imgPathDe" | cut -d' ' -f1)" = "0a65f49d98896ad3e769be5f038a8d3086c00316" ]
}
compEnv() {
envVar EMUTOS_IMG "$imgPath"
envVar EMUTOS_DE_IMG "$imgPathDe"
}

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -0,0 +1,55 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# Furnace, tildearrow's open-source multi-system chiptune tracker (GPLv2 or
# later), the recommended tool for JoeyLib's chip-tracker music
# (docs/Assets.adoc, "Content Creation Tools"): write the song for the TI
# SN76489 (or the AY-3-8910 / YM2149), export it as VGM, and
# tools/vgm2song.py turns that into a .song for tools/songbake. Its own Linux
# x86_64 release, unpacked into toolchains/opt/furnace. Nothing builds with
# it, so no group installs it: ./install.sh install furnace.
# scripts/check-vgm.sh exports the SN76489 and AY demo songs it comes with
# through it when it is installed.
#
# Furnace keeps its settings in ~/.config/furnace; the gates run it with HOME
# pointed at their work folder so they leave none.
compDescription="content: Furnace 0.6.8.3 chiptune tracker (optional; chip-tracker music, docs/Assets.adoc)"
compGroups="optional"
compRequires="host"
compHostDeps="gl-dev asound-dev x11-dev"
compUrl="https://github.com/tildearrow/furnace/releases/download/v0.6.8.3/furnace-0.6.8.3-linux-x86_64.tar.gz"
compSha256="298a4e30bbdfe5ad88ed7f3bd90bd657a3f0b2e6e7dfc2091663dda71424d555"
compPin="$compSha256"
compInstall() {
local archive
archive="$(fetchFile "$compUrl" "$compSha256")"
rm -rf "$compDir"
extractArchive "$archive" "$compDir" 1
}
compInstalled() {
[ -x "$compDir/furnace" ] && [ -d "$compDir/demos" ]
}
# It starts and knows VGM export (its settings go to a scratch home).
compVerify() {
local tmp
local usage
tmp="$(mktemp -d)"
usage="$(HOME="$tmp" "$compDir/furnace" -help 2>&1 || true)"
rm -rf "$tmp"
[[ "$usage" == *"-vgmout"* ]]
}
compEnv() {
envVar FURNACE "$compDir/furnace"
envVar FURNACE_DEMOS "$compDir/demos"
}

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,17 +1,19 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# Haiku's own build-host tools, built from the R1/beta6 source: package
# (unpacks .hpkg packages: the Haiku sysroots are made from them) and
# bfs_shell (reads and writes Be File System volumes: the Haiku VM images
# get their job runner through it, and the gates hand programs to the VM
# and take results back on a BFS disk). jam, which builds them, comes from
# Haiku's buildtools at the same release. Installed into
# toolchains/opt/haikuTools; bin/ holds wrappers that find the tools'
# (unpacks .hpkg packages: the Haiku sysroots are made from them), bfs_shell
# (reads and writes Be File System volumes: the Haiku VM images get their
# job runner through it, and the gates hand programs to the VM and take
# results back on a BFS disk), and xres (adds resources to a Haiku or BeOS
# executable: make/be.mk gives every program its icons with it). jam, which
# builds them, comes from Haiku's buildtools at the same release. Installed
# into toolchains/opt/haikuTools; bin/ holds wrappers that find the tools'
# libraries.
compDescription="haiku: Haiku R1/beta6 host tools (package, bfs_shell; also read BeOS volumes)"
compDescription="haiku: Haiku R1/beta6 host tools (package, bfs_shell, xres; also read BeOS volumes)"
compGroups="toolchain:haiku toolchain:haiku32 toolchain:beos emulator:haiku emulator:haiku32 emulator:beos"
compRequires="host"
compHostDeps="build-tools git python3 zlib-dev zstd-dev bison flex"
@ -19,8 +21,8 @@ compUrl="https://github.com/haiku/haiku.git"
compCommit="f6d86031a107e16f4fff6814e9aea20a39450842"
buildtoolsUrl="https://github.com/haiku/buildtools.git"
buildtoolsCommit="bfa561e3708c0e2d0155fb403b988ba515c9b16d"
compPin="$compCommit:$buildtoolsCommit"
hostTools="bfs_shell package"
hostTools="bfs_shell package xres"
compPin="$compCommit:$buildtoolsCommit:$hostTools"
compInstall() {
@ -43,7 +45,7 @@ compInstall() {
# The tools keep the attributes of the host's files (which have none
# here) in a folder named when they are built: theirs, not the build
# tree's, which goes.
(cd "$src/haiku/generated" && PATH="$src/buildtools/jam/bin.linuxx86:$PATH" jam -q -j"$JOEY_JOBS" -sHAIKU_BUILD_ATTRIBUTES_DIR="$compDir/attributes" '<build>bfs_shell' '<build>package')
(cd "$src/haiku/generated" && PATH="$src/buildtools/jam/bin.linuxx86:$PATH" jam -q -j"$JOEY_JOBS" -sHAIKU_BUILD_ATTRIBUTES_DIR="$compDir/attributes" '<build>bfs_shell' '<build>package' '<build>xres')
objects="$src/haiku/generated/objects/linux"
rm -rf "$compDir"
mkdir -p "$compDir/bin" "$compDir/libexec" "$compDir/lib" "$compDir/attributes"
@ -58,17 +60,23 @@ compInstall() {
compInstalled() {
[ -x "$compDir/bin/bfs_shell" ] && [ -x "$compDir/bin/package" ] && [ -x "$compDir/libexec/bfs_shell" ] && [ -x "$compDir/libexec/package" ] && [ -d "$compDir/attributes" ]
local tool
for tool in $hostTools; do
[ -x "$compDir/bin/$tool" ] && [ -x "$compDir/libexec/$tool" ] || return 1
done
[ -d "$compDir/attributes" ]
}
# Make a small BFS volume, write a file onto it and read it back; package
# starts. (Outputs are captured, not piped into grep: under pipefail a tool's
# own exit status, or SIGPIPE from an early grep, would fail the check.)
# and xres start. (Outputs are captured, not piped into grep: under pipefail
# a tool's own exit status, or SIGPIPE from an early grep, would fail the
# check.)
compVerify() {
local tmp
local readBack
local usage
local xresUsage
tmp="$(mktemp -d)"
printf 'JoeyLib\n' > "$tmp/in.txt"
truncate -s 8M "$tmp/v.img"
@ -77,8 +85,9 @@ compVerify() {
printf 'cp :%s /myfs/in.txt\nsync\nquit\n' "$tmp/in.txt" | "$compDir/bin/bfs_shell" "$tmp/v.img" > /dev/null 2>&1 || true
readBack="$(printf 'cat /myfs/in.txt\nquit\n' | "$compDir/bin/bfs_shell" "$tmp/v.img" 2>&1 || true)"
usage="$("$compDir/bin/package" 2>&1 || true)"
xresUsage="$("$compDir/bin/xres" -h 2>&1 || true)"
rm -rf "$tmp"
[[ "$readBack" == *JoeyLib* && "$usage" == *"Usage: package"* ]]
[[ "$readBack" == *JoeyLib* && "$usage" == *"Usage: package"* && "$xresUsage" == *"Valid commands are"* ]]
}

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,12 +1,14 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# libxmp-lite 4.7.0: the MOD decoder the DOS, Atari ST, X68000, SDL3, PSP,
# web, Win16, Win32, Haiku and BeOS audio HALs compile in. Each port's
# makefile builds it from this source tree.
# web, Win16, Win32, Haiku and BeOS audio HALs compiled in until 2026-10-14,
# when JoeyLib's own module player (src/core/modPlayer.c) replaced it. No
# build uses it now; it stays a reference to compare that player against.
compDescription="audio: libxmp-lite 4.7.0 source (DOS, Atari ST, X68000, SDL3, PSP, web, Win16, Win32, Haiku, BeOS)"
compDescription="audio: libxmp-lite 4.7.0 source (no build links it; a reference for the module player)"
compGroups="toolchain:dos toolchain:atarist toolchain:x68000 toolchain:sdl3 toolchain:psp toolchain:web toolchain:win16 toolchain:win32 toolchain:haiku toolchain:haiku32 toolchain:beos"
compRequires="host"
compHostDeps="tar"

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,25 +1,34 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# PTPlayer 6.4: Frank Wille's ProTracker replayer for the Amiga, used by
# the Amiga audio HAL. Aminet's URL is not versioned, so the pin is the
# archive's checksum.
# the Amiga audio HAL, with JoeyLib's patches (installer/patches/ptplayer),
# which make it play as Protracker 2.3 and the other ports' module player do.
# Aminet's URL is not versioned, so the pin is the archive's checksum.
compDescription="Amiga audio: PTPlayer 6.4 (ProTracker replayer)"
compDescription="Amiga audio: PTPlayer 6.4 (ProTracker replayer) + JoeyLib patch"
compGroups="toolchain:amiga"
compRequires="host"
compHostDeps="lhasa"
compHostDeps="lhasa patch"
compUrl="http://aminet.net/mus/play/ptplayer.lha"
compSha256="5ba1285e91413f6b6cb72768c0c31cff90029af6920870d9f589689aa2e2161a"
compPin="$compSha256"
patchDir="$JOEY_PATCHES/ptplayer"
# Editing a patch reinstalls PTPlayer, so the patches are part of the pin.
compPin="$compSha256 $(cat "$patchDir"/*.patch | sha256sum | cut -d' ' -f1)"
compInstall() {
local lha
local p
lha="$(fetchFile "$compUrl" "$compSha256")"
rm -rf "$compDir"
extractArchive "$lha" "$compDir"
for p in "$patchDir"/*.patch; do
info "applying $(basename "$p")"
patch -d "$compDir" -p1 -s -F0 --no-backup-if-mismatch < "$p"
done
}
@ -28,8 +37,9 @@ compInstalled() {
}
# The patch is in: its volume slide that leaves the period alone.
compVerify() {
grep -qi 'ptplayer' "$compDir/ptplayer.asm"
grep -q 'mt_volslide_noper' "$compDir/ptplayer.asm"
}

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT

Some files were not shown because too many files have changed in this diff Show more