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 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 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 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 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. When upstream license terms change, update the corresponding entry here.
This file summarises; the license text that comes with each component is 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 These end up in what a JoeyLib program ships as, so their terms matter
most. 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 ### SDL 3.4.18
- **Used on:** SDL3, PSP, web - **Used on:** SDL3, PSP, web
- **URL:** <https://www.libsdl.org/> - **URL:** <https://www.libsdl.org/>
- **License:** zlib - **License:** zlib
- **Role:** window, input and audio for the hosted SDL3 code. Built as a - **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 static library for the host, for the PSP with pspdev, and for Emscripten
(with JoeyLib's patches, `installer/patches/sdl3Web/`, which are under (with JoeyLib's patches, `installer/patches/sdl3Web/`, which are under
SDL's zlib license), and linked into each program. SDL's zlib license), and linked into each program.
- **Installed at:** `toolchains/opt/sdl3/`, `toolchains/opt/sdl3Psp/`, - **Installed at:** `toolchains/opt/sdl3/`, `toolchains/opt/sdl3Psp/`,
@ -60,12 +51,15 @@ most.
### PTPlayer 6.4 ### 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> - **URL:** <http://aminet.net/mus/play/ptplayer.lha>
(author Frank Wille, <http://aminet.net/package/mus/play/ptplayer>) (author Frank Wille, <http://aminet.net/package/mus/play/ptplayer>)
- **License:** Public Domain (the archive's `license` file: the Unlicense) - **License:** Public Domain (the archive's `license` file: the Unlicense)
- **Role:** ProTracker MOD replayer + `mt_playfx` SFX channel for the - **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`) - **Installed at:** `toolchains/opt/ptplayer/` (installer component `ptplayer`)
### CWSDPMI r7 ### CWSDPMI r7
@ -84,7 +78,7 @@ most.
### The toolchains' C libraries and start files ### 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 compiler runtime into the programs it builds. What a shipped program owes
them, in brief (the entries below have the detail): 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 source if its users are told how to get DJGPP (`copying.dj`). DVX
programs are modules DVX loads, built with DVX's SDK. programs are modules DVX loads, built with DVX's SDK.
- **Atari ST:** MiNTLib, file by file under its own license (COPYMINT), - **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`). program meets all of them (MiNTLib's `LICENSES`).
- **Amiga:** libnix (public domain). - **Amiga:** libnix (public domain).
- **X68000:** newlib (a collection of permissive licenses, - **X68000:** newlib (a collection of permissive licenses,
@ -177,7 +171,7 @@ above.
(a fork of llvm-mos, <https://github.com/llvm-mos/llvm-mos>) (a fork of llvm-mos, <https://github.com/llvm-mos/llvm-mos>)
- **License:** Apache License 2.0 with LLVM Exceptions, llvm-mos's (and - **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, 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 the third-party code it holds under other licenses (the C++ runtime's
Embedded Template Library, MIT; test and demo sources), none of which Embedded Template Library, MIT; test and demo sources), none of which
reaches a JoeyLib program. reaches a JoeyLib program.
@ -256,7 +250,7 @@ above.
(Vincent Riviere's builds; mintlib: <https://github.com/freemint/mintlib>) (Vincent Riviere's builds; mintlib: <https://github.com/freemint/mintlib>)
- **License:** gcc 15.2 and binutils 2.45: GNU GPL v3, with the GCC - **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 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 Spencer's or musl's MIT terms; a program linked with it meets all of
them (MiNTLib's `LICENSES`). them (MiNTLib's `LICENSES`).
- **Role:** GEMDOS-native gcc + binutils + mintlib used to compile - **Role:** GEMDOS-native gcc + binutils + mintlib used to compile
@ -322,9 +316,9 @@ above.
- **Used on:** Win32 - **Used on:** Win32
- **URL:** <https://github.com/mstorsjo/llvm-mingw> - **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 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 - **Role:** i686 cross compiler for the win32 port's programs, which link
Windows' MSVCRT.DLL. Windows' MSVCRT.DLL.
- **Installed at:** `toolchains/opt/llvmMingw/` (installer component - **Installed at:** `toolchains/opt/llvmMingw/` (installer component
@ -338,7 +332,7 @@ above.
Library Exception for libgcc. The PSPSDK: BSD. newlib: its collection Library Exception for libgcc. The PSPSDK: BSD. newlib: its collection
of permissive licenses. Every library pspdev carries has its license of permissive licenses. Every library pspdev carries has its license
under `psp/share/licenses/`. 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`) - **Installed at:** `toolchains/opt/pspdev/` (installer component `pspdev`)
### Emscripten 6.0.11 (emsdk) ### Emscripten 6.0.11 (emsdk)
@ -376,10 +370,45 @@ above.
distribution, keeping its notice). distribution, keeping its notice).
- **Role:** `package` unpacks the `.hpkg` packages the sysroots are made - **Role:** `package` unpacks the `.hpkg` packages the sysroots are made
from; `bfs_shell` reads and writes the BFS disks the VMs take their jobs 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 - **Installed at:** `toolchains/opt/haikuTools/` (installer component
`haikuTools`) `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) ## 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 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 potracer's code, and the logo it made is not covered by potracer's
license. license.
- Building MAME, QEMU, PPSSPP and SDL 3 also takes the distribution's - Building MAME, QEMU, PPSSPP, and SDL 3 also takes the distribution's
development packages (SDL2, X11, Wayland, PulseAudio, ALSA, GLib and the development packages (SDL2, X11, Wayland, PulseAudio, ALSA, GLib, and the
rest), each under its own license. 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` These are **not** part of JoeyLib distribution. `./install.sh`
places them in `toolchains/firmware/` or `toolchains/opt/` for 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. Trio64V+ display driver** (its `.zip`): Microsoft and S3 proprietary.
Installed unattended into the DOSBox PC every win16 run uses. A Win16 Installed unattended into the DOSBox PC every win16 run uses. A Win16
program's users bring their own Windows. Component `win311`. 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 - **BeOS 5 Professional CD** (its image): Be, Inc. proprietary; the terms
for its start files are Unknown. Its boot floppy and x86 system become for its start files are Unknown. Its boot floppy and x86 system become
the BeOS R5 VM (component `beosVm`), and its Development package's 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 (component `beosR5`). Every beos program links the start files
(`start_dyn.o`, `init_term_dyn.o`, `crti.o`, `crtn.o`), as every program (`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. 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 docs renders the manual to build/docs (HTML + PDF)
# make verify builds every port, then runs every gate (needs emulators) # make verify builds every port, then runs every gate (needs emulators)
# make verify-audio runs every port's audio 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 verify-serial runs every port's serial loopback gate
# make clean removes all build outputs # make clean removes all build outputs
# #
@ -38,7 +39,7 @@ include $(dir $(firstword $(MAKEFILE_LIST)))make/common.mk
ALL_TARGETS := $(JOEY_PORTS_INSTALLED) 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 all: $(ALL_TARGETS) tools
ifeq ($(ALL_TARGETS),) ifeq ($(ALL_TARGETS),)
@ -127,6 +128,12 @@ verify:
verify-audio: verify-audio:
@$(REPO_DIR)/scripts/verify-all.sh audio $(PORTS) @$(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 # 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. # (banner out, probe echoed back), sequentially, with the same exit codes.
# Takes a subset: make verify-serial PORTS="host dos" # Takes a subset: make verify-serial PORTS="host dos"
@ -159,7 +166,8 @@ help:
@echo " iigs-disk iigs-verify (IIgs MAME visual check)" @echo " iigs-disk iigs-verify (IIgs MAME visual check)"
@echo " blank-check (host-cc smoke test of the new-port template + gnu89 lint)" @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 (build every port, then run every gate; PORTS= subset)"
@echo " verify-audio (all 5 audio gates; PORTS=\"atarist amiga\" for a subset)" @echo " verify-audio (every port's audio gate; PORTS=\"atarist amiga\" for a subset)"
@echo " verify-serial (host check + all 5 serial loopback gates; PORTS= 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 " docs (manual to build/docs; FORMAT=html for HTML only)"
@echo "Install toolchains first: ./install.sh install toolchains" @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) under QEMU)
- PSP (homebrew for custom firmware: the SDL3 port built with pspdev, each - 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 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 the 480x272 screen, a keyboard drawn on it for the keys a PSP lacks;
for alignment as a PSP checks it) 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. The Apple IIgs defines the capability ceiling. Stronger platforms coast.
Hot paths are hand-written assembly per port; the public API is C. 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 own whose Makefile describes it and includes `make/project.mk` and
`make/programs.mk` from a JoeyLib checkout (found through `JOEYLIB`, by `make/programs.mk` from a JoeyLib checkout (found through `JOEYLIB`, by
default `../joeylib`). `make` there builds it for every installed target with 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 `build/`; the manual's "Building Your Program" chapter
(`docs/BuildIntegration.adoc`) has the details. Two complete games are built (`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 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 A few files are never downloaded -- the Amiga Kickstart, the Amiga
`serial.device` and `lowlevel.library` from Workbench 3.1, the X68000 font `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 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 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 BeOS CD as its CloneCD `.img`).
The manual's Getting Started chapter covers `joey-run`'s options and the The manual's Getting Started chapter covers `joey-run`'s options and the
private-file bundle. private-file bundle.
@ -148,8 +150,9 @@ make dvx
``` ```
`make atarist` builds programs for every ST; `make ste` builds the STe `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 variant, which uses the STe's BLiTTER, DMA sound, and 4-bit palette, reads
only on an STe or Mega STe. 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 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 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 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 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 `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`, `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 `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 (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 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/joeymod/ Protracker .MOD converter (passthrough or .NTP)
tools/spritebake/ offline .spr -> .spc pre-compiled sprite baker tools/spritebake/ offline .spr -> .spc pre-compiled sprite baker
tools/songbake/ chip-tracker text notation -> JYM1 stream 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/xdftool.py Human68k FAT12 .XDF disk image reader / writer
tools/diff-uber-hashes UBER golden-hash comparator tools/diff-uber-hashes UBER golden-hash comparator
examples/ example programs examples/ example programs
@ -264,6 +269,7 @@ is a quick reference. Every entry point is plain C, no C++ extensions.
```c ```c
typedef struct { typedef struct {
uint32_t codegenBytes; // runtime compiled-sprite cache size uint32_t codegenBytes; // runtime compiled-sprite cache size
bool noAudio; // true: no audio; else jlInit starts it
} jlConfigT; } jlConfigT;
bool jlInit (const jlConfigT *config); 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 All surfaces are 320x200 16-color images with a 200-entry SCB table
and 16 palettes of 16 `$0RGB` colors. In-memory storage is 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 (separate bitplanes on Amiga and Sharp X68000, word-interleaved planes
on Atari ST) on the 68k ports. The public API speaks in color indices on Atari ST) on the 68k ports. The public API speaks in color indices
(0..15) and hides the storage format. (0..15) and hides the storage format.
```c ```c
#define SURFACE_WIDTH 320 #define JL_SURFACE_WIDTH 320
#define SURFACE_HEIGHT 200 #define JL_SURFACE_HEIGHT 200
#define SURFACE_BYTES_PER_ROW 160 #define JL_SURFACE_BYTES_PER_ROW 160
#define SURFACE_PIXELS_SIZE (SURFACE_BYTES_PER_ROW * SURFACE_HEIGHT) #define JL_SURFACE_PIXELS_SIZE (JL_SURFACE_BYTES_PER_ROW * JL_SURFACE_HEIGHT)
#define SURFACE_PALETTE_COUNT 16 #define JL_SURFACE_PALETTE_COUNT 16
#define SURFACE_COLORS_PER_PALETTE 16 #define JL_SURFACE_COLORS_PER_PALETTE 16
typedef struct jlSurfaceT jlSurfaceT; // opaque 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. type so callers can stash a copy without allocating a scratch surface.
```c ```c
#define TILE_PIXELS_PER_SIDE 8 #define JL_TILE_PIXELS_PER_SIDE 8
#define TILE_BYTES_PER_ROW 4 #define JL_TILE_BYTES_PER_ROW 4
#define TILE_BYTES (TILE_BYTES_PER_ROW * TILE_PIXELS_PER_SIDE) #define JL_TILE_BYTES (JL_TILE_BYTES_PER_ROW * JL_TILE_PIXELS_PER_SIDE)
#define TILE_BLOCKS_PER_ROW (SURFACE_WIDTH / TILE_PIXELS_PER_SIDE) // 40 #define JL_TILE_BLOCKS_PER_ROW (JL_SURFACE_WIDTH / JL_TILE_PIXELS_PER_SIDE) // 40
#define TILE_BLOCKS_PER_COL (SURFACE_HEIGHT / TILE_PIXELS_PER_SIDE) // 25 #define JL_TILE_BLOCKS_PER_COL (JL_SURFACE_HEIGHT / JL_TILE_PIXELS_PER_SIDE) // 25
#define TILE_NO_GLYPH ((uint16_t)0xFFFFu) #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, void jlTileCopy (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy); 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); uint16_t count, const uint16_t *palette);
bool jlSpriteCompile (jlSpriteT *sp); // build per-shift fast path bool jlSpriteCompile (jlSpriteT *sp); // build per-shift fast path
void jlSpritePrewarm (jlSpriteT *sp); // hint: compile if not already
uint32_t jlSpriteCompiledSize (const jlSpriteT *sp); uint32_t jlSpriteCompiledSize (const jlSpriteT *sp);
void jlSpriteDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); 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 or captured surface), use `jlSpriteCreateFromSurface` -- the
on-the-fly path stays alongside the baked load API. 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`) ### 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. for per-port sources and limitations.
```c ```c
typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE, typedef enum { /* JL_KEY_NONE, JL_KEY_A..JL_KEY_Z, JL_KEY_0..JL_KEY_9, JL_KEY_SPACE,
KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE, JL_KEY_ESCAPE, JL_KEY_RETURN, JL_KEY_TAB, JL_KEY_BACKSPACE,
KEY_UP/DOWN/LEFT/RIGHT, KEY_LSHIFT/RSHIFT/LCTRL/LALT, JL_KEY_UP/DOWN/LEFT/RIGHT, JL_KEY_LSHIFT/RSHIFT/LCTRL/LALT,
KEY_F1..KEY_F10, KEY_COUNT */ } jlKeyE; JL_KEY_F1..JL_KEY_F10, JL_KEY_COUNT */ } jlKeyE;
typedef enum { MOUSE_BUTTON_NONE, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, typedef enum { JL_MOUSE_BUTTON_NONE, JL_MOUSE_BUTTON_LEFT, JL_MOUSE_BUTTON_RIGHT,
MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } jlMouseButtonE; JL_MOUSE_BUTTON_MIDDLE, JL_MOUSE_BUTTON_COUNT } jlMouseButtonE;
typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE; typedef enum { JL_JOYSTICK_0, JL_JOYSTICK_1, JL_JOYSTICK_COUNT } jlJoystickE;
typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE; typedef enum { JL_JOY_BUTTON_0, JL_JOY_BUTTON_1, JL_JOY_BUTTON_COUNT } jlJoyButtonE;
#define JOYSTICK_AXIS_MAX 127 #define JL_JOYSTICK_AXIS_MAX 127
#define JOYSTICK_AXIS_MIN (-127) #define JL_JOYSTICK_AXIS_MIN (-127)
#define JL_CHAR_BACKSPACE 0x08 #define JL_CHAR_BACKSPACE 0x08
#define JL_CHAR_TAB 0x09 #define JL_CHAR_TAB 0x09
@ -588,20 +611,19 @@ void jlJoystickReset (jlJoystickE js, uint8_t deadZone);
### Audio (`joey/audio.h`) ### Audio (`joey/audio.h`)
4-channel Protracker-style music plus five one-shot SFX slots, three 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 platform-native form produced by `tools/joeymod` (`.mod` for
Amiga/DOS/ST/X68000; `.ntp` for IIgs; `.amod` if you want loop=false on DOS/ST/X68000; `.amod` for the Amiga; `.ntp` for the IIgs). The Amiga
Amiga). Amiga plays `.mod` through PTPlayer and the IIgs through the plays `.mod` through PTPlayer and the IIgs through the NinjaTrackerPlus
NinjaTrackerPlus replayer; DOS, Atari ST and X68000 decode with replayer; DOS, the Atari ST, the X68000, and the hosted ports mix it
libxmp-lite. A failed `jlAudioInit` is non-fatal; the rest of the API with JoeyLib's module player, PTPlayer's replay in C. `jlInit` starts
stays callable as no-ops. 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 ```c
#define JOEY_AUDIO_SFX_SLOTS 5 #define JL_AUDIO_SFX_SLOTS 5
#define JOEY_AUDIO_VOICES 3 #define JL_AUDIO_VOICES 3
bool jlAudioInit (void);
void jlAudioShutdown (void);
void jlAudioPlayMod (const uint8_t *data, uint32_t length, bool loop); void jlAudioPlayMod (const uint8_t *data, uint32_t length, bool loop);
void jlAudioStopMod (void); void jlAudioStopMod (void);
@ -613,7 +635,9 @@ void jlAudioPlaySfxStream (uint8_t slot, jlAudioStreamFillT fill, void *ctx,
uint16_t rateHz); uint16_t rateHz);
void jlAudioStopSfx (uint8_t slot); 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. // played through the tone/noise layer at register-write cost on every port.
// No CPU mixing, so it is viable where jlAudioPlayMod is not. // No CPU mixing, so it is viable where jlAudioPlayMod is not.
bool jlMusicPlay (const uint8_t *data, uint32_t length, bool loop); 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_PLATFORM_IIGS / _AMIGA / _ATARIST / _DOS / _X68000 / _DVX / _HOSTED // exactly one defined
JOEYLIB_HOSTED_SDL3 / _WIN32 / _WIN16 // the hosted backend JOEYLIB_HOSTED_SDL3 / _WIN32 / _WIN16 // the hosted backend
JOEYLIB_ATARI_STE // with _ATARIST: the STe build (make ste) JOEYLIB_ATARI_STE // with _ATARIST: the STe build (make ste)
JOEYLIB_SPRITE_INTERPRETED // no sprite code emitter (hosted, blank) JL_SPRITE_INTERPRETED // no sprite code emitter (hosted but Win16, blank)
JOEYLIB_CPU_65816 / _68000 / _X86 JL_CPU_65816 / _68000 / _X86
JOEYLIB_ENDIAN_LITTLE / _BIG JL_ENDIAN_LITTLE / _BIG
JOEYLIB_NATIVE_CHUNKY / _NATIVE_PLANAR JL_NATIVE_CHUNKY / _NATIVE_PLANAR
JOEYLIB_HAS_BLITTER / _HAS_COPPER // Amiga only JL_HAS_BLITTER / _HAS_COPPER // Amiga only
JOEYLIB_PLATFORM_NAME // human-readable string JL_PLATFORM_NAME // human-readable string
JOEYLIB_VERSION_MAJOR / _MINOR / _PATCH / _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 of JoeyLib's source files opens with a short header that says so
(`SPDX-License-Identifier: MIT`); a new file gets the same. (`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 program owes them are in [LICENSES.md](LICENSES.md). The patches in
`installer/patches/` are under the licenses of the projects they patch. `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>`. 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`. * *Module playback* -- `jlAudioPlayMod`, four-channel Protracker `.MOD`.
* *Chip tracker* -- `jlMusicPlay`, a portable event stream driving three tone * *Chip tracker* -- `jlMusicPlay`, a portable event stream driving three tone
voices plus noise at register-write cost, with no CPU mixing. The recommended voices plus noise at register-write cost, with no CPU mixing. The recommended
music path. music path.
* *Voices and effects* -- `jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx` and * *Voices and effects* -- `jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and
the streaming forms. the streaming forms.
[WARNING] [WARNING]
==== ====
On the Atari ST and Amiga the tone voices *are* the hardware the module player On the Amiga the tone voices and the noise *are* the hardware the module
drives. While a `.MOD` plays, `jlAudioVoice` writes fight the player's register player drives: Paula's four channels. While a `.MOD` plays, `jlAudioVoice` and
writes and lose. Choose the module layer or the voice and tracker layer per `jlAudioNoise` writes fight the player's register writes and lose. On the
scene, never both at once. 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 `jlInit` starts audio and `jlShutdown` stops it; a program that wants none
call becomes a silent no-op. 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 === Constants
@ -28,8 +35,8 @@ call becomes a silent no-op.
|=== |===
|Name |Value |Meaning |Name |Value |Meaning
|`JOEY_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4. |`JL_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4.
|`JOEY_AUDIO_VOICES` |3 |PSG-style tone voices, 0..2. |`JL_AUDIO_VOICES` |3 |PSG-style tone voices, 0..2.
|`JL_SFX_RAW_SIGNED8` |0 |Raw stream format: `int8_t` PCM. |`JL_SFX_RAW_SIGNED8` |0 |Raw stream format: `int8_t` PCM.
|`JL_SFX_RAW_UNSIGNED8_NOZERO` |1 |Raw stream format: `uint8_t` biased 0x80, |`JL_SFX_RAW_UNSIGNED8_NOZERO` |1 |Raw stream format: `uint8_t` biased 0x80,
and 0x00 is forbidden (it is the DOC stop byte). and 0x00 is forbidden (it is the DOC stop byte).
@ -37,34 +44,15 @@ call becomes a silent no-op.
[TIP] [TIP]
==== ====
Prefer *slot 4* for game sound effects when music is playing. The IIgs maps Prefer *slot 4* for game sound effects when music is playing. The IIgs plays
tracker tone voices onto slots 0..2 and the noise voice onto slot 3. On the tone voices 0..2 on sound effect slots 0..2 and the noise on slot 3: an effect
other ports the slots are independent of the voice layer. 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 === jlAudioFrameTick
[source,c] [source,c]
@ -72,10 +60,12 @@ Tears the engine down. Safe when audio was never initialised.
void jlAudioFrameTick(void); void jlAudioFrameTick(void);
---- ----
Hooks the engine into your game loop. Most platforms drive their engines from a Hooks the engine into your game loop. Every port does its sound work here: DOS,
hardware interrupt and ignore this, but it is required for any port that mixes the ST, the X68000, DVX, and the hosted ports mix or refill their sound buffers
in user context, it advances tracker playback, and on IIgs and Amiga it is what in it, so without it modules and effects stop or repeat; the IIgs and Amiga
fires the next chunk of a streaming sound effect. 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. 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"] [cols="1,1,4", options="header"]
|=== |===
|Parameter |Type |Description |Parameter |Type |Description
|`data` |`const uint8_t *` |Platform-native module blob: `.mod` on most ports, |`data` |`const uint8_t *` |Platform-native module blob: `.mod` on every port
`.amod` on Amiga, `.ntp` on IIgs. The asset pipeline produces the right form. 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. |`length` |`uint32_t` |Blob size in bytes.
|`loop` |`bool` |`true` plays forever. |`loop` |`bool` |`true` plays forever.
|=== |===
@ -103,9 +94,15 @@ Begins module playback. If a module is already playing it is replaced.
[NOTE] [NOTE]
==== ====
`loop = false` needs help on the Amiga: PTPlayer has no native song-end signal, `loop = false` plays the song once and stops it. The IIgs's replayer has a
so the module must contain an `E8FF` marker at song end. The `joeymod` tool's play-once mode; the ports that mix modules in software stop where the song
`.amod` output injects it. A plain `.mod` with no marker loops anyway on Amiga. 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 === jlAudioStopMod
@ -130,7 +127,8 @@ bool jlAudioIsPlayingMod(void);
*Parameters:* none. *Parameters:* none.
*Returns:* `true` while a module is producing output. `false` after a stop, *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 === jlAudioPlaySfx
@ -146,7 +144,7 @@ new sample replaces it.
[cols="1,1,4", options="header"] [cols="1,1,4", options="header"]
|=== |===
|Parameter |Type |Description |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. |`sample` |`const uint8_t *` |Raw signed 8-bit PCM.
|`length` |`uint32_t` |Sample count in bytes. |`length` |`uint32_t` |Sample count in bytes.
|`rateHz` |`uint16_t` |The rate the sample was recorded at; the engine |`rateHz` |`uint16_t` |The rate the sample was recorded at; the engine
@ -155,6 +153,12 @@ new sample replaces it.
*Returns:* nothing. *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] [NOTE]
==== ====
The full -128..127 range is accepted everywhere, but the IIgs plays -128 as The full -128..127 range is accepted everywhere, but the IIgs plays -128 as
@ -175,9 +179,9 @@ deactivates.
[WARNING] [WARNING]
==== ====
This is called from the audio engine's refill context -- the main thread on DOS, On every port it is called in program time, from the call that starts the
ST and SDL3 (on DVX, DVX's own sound pump, while the program is yielded), but the stream and then from `jlAudioFrameTick` (on DVX, also while the desktop runs).
*audio interrupt* on Amiga and IIgs. It must not block and must not allocate. It must not block, and it should be quick: it runs inside those calls.
==== ====
=== jlAudioPlaySfxStream === jlAudioPlaySfxStream
@ -206,10 +210,11 @@ with no caller-side allocation.
[IMPORTANT] [IMPORTANT]
==== ====
On IIgs and Amiga the engine plays one chunk at a time and fires the next from Every port feeds streams from `jlAudioFrameTick`, so *you must be calling
`jlAudioFrameTick`, so *you must be calling `jlAudioFrameTick` every frame* or `jlAudioFrameTick` every frame* or the stream stops. On the IIgs and Amiga the
the stream stops after its first chunk. A chunk seam on those ports can carry engine plays one chunk at a time (7 KB on the IIgs, 4 KB on the Amiga) and
up to one frame of jitter. fires the next from it, so a chunk seam on those ports can carry up to one
frame of jitter.
==== ====
=== jlAudioSfxRawFormat / jlAudioPlaySfxStreamRaw === jlAudioSfxRawFormat / jlAudioPlaySfxStreamRaw
@ -226,10 +231,10 @@ void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill,
`jlAudioPlaySfxStreamRaw` is `jlAudioPlaySfxStream` except your callback `jlAudioPlaySfxStreamRaw` is `jlAudioPlaySfxStream` except your callback
delivers bytes *already in that format*, so the port skips its per-byte 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 conversion of every chunk. On the IIgs that conversion costs about 7
over each 3.5 KB chunk, and it is enough to stall a game for a third of a microseconds a byte, some 50 ms for each 7 KB chunk. Every other port's raw
second per chunk of speech. Every other port streams signed PCM natively, so format is signed PCM, so there this is simply the plain call (DVX still flips
there this is simply the plain call. each byte for its sound library).
If your producer cannot make the port's format, use `jlAudioPlaySfxStream`. 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] [source,c]
---- ----
typedef enum { 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; } jlAudioPanE;
void jlAudioSetPan(uint8_t slot, jlAudioPanE pan); 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 |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 oscillators, one per side. A stock IIgs sums to mono, so this shows up only on
a stereo card or an emulator. a stereo card or an emulator.
|Amiga |Left and right only, by choosing the Paula channel side. Paula cannot |Amiga |Left and right by choosing the Paula channel side; a Paula channel plays
place one voice in both ears, so `AUDIO_PAN_CENTER` leaves PTPlayer's own on one side only. A centred sound plays on a free channel each side when both
channel choice alone. 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 |X68000 |Ignored. Everything mixes into one ADPCM stream, so panning would move
all audio at once. 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). |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 === jlAudioStopSfx
@ -300,13 +307,12 @@ void jlAudioStopSfx(uint8_t slot);
void jlAudioTone(uint16_t freqHz); void jlAudioTone(uint16_t freqHz);
---- ----
Direct hardware tone generator: plays a square wave continuously until the next A single tone that plays until the next call: `jlAudioVoice(0, freqHz, 0)`,
call. Designed for AGI-era music where the engine programs a divisor and lets tone voice 0 at full volume, on every port. Designed for AGI-era music where
the chip play, so CPU cost during playback is zero. Single voice; uses voice 0 the engine programs a divisor and lets the chip play, so CPU cost during
on platforms with several oscillators. On DOS this programs PIT counter 2 and playback is zero. A program that mixes it with `jlAudioVoice` or `jlMusicPlay`
the speaker gate; on DVX it is a square tone of DVX's sound library, on the FM shares voice 0 with them. On DOS without an OPL2 (AdLib) chip, voice 0, and so
chip when there is one; on SDL3 it is voice 0 of the port's mixer at full this tone, is the PC speaker.
volume.
[cols="1,1,4", options="header"] [cols="1,1,4", options="header"]
|=== |===
@ -314,8 +320,7 @@ volume.
|`freqHz` |`uint16_t` |Frequency in Hz. 0 silences the generator. |`freqHz` |`uint16_t` |Frequency in Hz. 0 silences the generator.
|=== |===
*Returns:* nothing. A no-op on platforms with no reachable tone generator -- *Returns:* nothing. Silent wherever voice 0 is (see `jlAudioVoice`).
fall back to `jlAudioPlaySfxStream` there if you need synthesised sound.
=== jlAudioVoice === jlAudioVoice
@ -329,33 +334,32 @@ Three-voice PSG-style polyphonic tone generator.
[cols="1,1,4", options="header"] [cols="1,1,4", options="header"]
|=== |===
|Parameter |Type |Description |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. |`freqHz` |`uint16_t` |Frequency in Hz. 0 silences this voice.
|`atten` |`uint8_t` |AGI / SN76489-style 4-bit attenuation: 0 loudest, |`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. *Returns:* nothing.
Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2; the ST uses Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2 (with no OPL2,
YM2149 channels A, B and C; the Amiga uses Paula channels 0..2 looping a voice 0 is the PC speaker and voices 1 and 2 are silent); the ST uses YM2149
two-byte square sample; the IIgs uses Ensoniq DOC oscillators playing a channels A, B, and C (on the plain ST voice 0 is silent while digital sound
resident square; DVX plays square tones of its sound library, which puts them plays); the Amiga uses Paula channels 0..2 looping a two-byte square sample;
on the FM chip when there is a free voice and mixes them otherwise; SDL3 mixes the IIgs uses Ensoniq DOC oscillators playing a resident square on sound
square waves in software. 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,
Voice state is sticky -- a voice keeps its last frequency until you change it which puts them on the FM chip when there is a free voice and mixes them
-- and CPU cost during sustained playback is zero, because the hardware otherwise; the hosted ports mix square waves in software. The OPL2's waves
oscillates on its own. are not square either.
[IMPORTANT] [IMPORTANT]
==== ====
The IIgs is the exception to stickiness: each call is an approximately 80 ms A note sounds until the next call for that voice. The IIgs plays each note as
retriggered one-shot, so a note held longer must be re-asserted about once a a sample a fraction of a second long and restarts the notes being held from
frame. It also requires `jlAudioInit`, because the DOC path rides the tracker `jlAudioFrameTick`, so call that every frame, as every program should; a frame
engine. On DOS, ST and Amiga this is direct hardware programming and works much longer than 80 ms can leave a gap in a held note there.
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`.
==== ====
=== jlAudioNoise === jlAudioNoise
@ -378,13 +382,17 @@ layer.
*Returns:* nothing. *Returns:* nothing.
Mapping: the ST uses the YM2149 noise generator mixed onto channel C; DOS uses The noise runs at 125000 / `pitch` Hz (`pitch` 0 as 1), the YM2149's rate, on
the OPL2 rhythm-mode snare, leaving the three tone channels untouched; the every port as far as its chip goes. Mapping: the ST uses the YM2149 noise
Amiga uses Paula channel 3 with an LFSR noise sample; the IIgs uses a resident generator mixed onto channel C; the X68000 the YM2151's noise, at the same
noise sample on slot 3 with the same retriggered one-shot model as its tone rates exactly; the Amiga Paula channel 3 looping an LFSR noise sample, whose
voices; DVX uses a noise tone of its sound library; SDL3 mixes the YM2149's fastest rate, 28.6 kHz, pitches 0 to 4 all play at; the IIgs a resident noise
own noise in software (its 17-bit generator, clocked at 125 kHz divided by sample on slot 3, up to 26 kHz, likewise for pitches 0 to 4; DVX a noise tone
the pitch), so it sounds as on the ST. 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] [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. `.MOD` playback is not.
Playback is pumped by `jlAudioFrameTick` and paced by `jlMillisElapsed`, so 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"] [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 *Returns:* `true` on success; `false` on a malformed stream, in which case
nothing plays. nothing plays. With `jlConfigT.noAudio` the song runs silently, its time and
its end as usual.
[NOTE]
====
Call `jlAudioInit` first for the song to be audible on the IIgs -- see
`jlAudioVoice`.
====
=== jlMusicStop === jlMusicStop
@ -507,35 +510,55 @@ frame, so the chip only hears the last write and short notes vanish.
[cols="1,1,4", options="header"] [cols="1,1,4", options="header"]
|=== |===
|Parameter |Type |Description |Parameter |Type |Description
|`fn` |`jlAudioTickFnT` |Callback, fired from interrupt context. Pass `NULL` to |`fn` |`jlAudioTickFnT` |Callback, which may be fired from an interrupt. Pass
uninstall. `NULL` to uninstall.
|`hz` |`uint16_t` |Firings per second. |`hz` |`uint16_t` |Firings per second.
|=== |===
*Returns:* `true` if installed. Ports without a suitable timer source return *Returns:* `true` if installed. `false` when `hz` is 0, when audio is off or
`false`. 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] [WARNING]
==== ====
`fn` runs in interrupt context. Keep it to memory reads and direct hardware `fn` may run in an interrupt: it does on DOS, the Amiga, the ST, and the
register writes -- no allocation, no `printf`, nothing blocking. 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 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` 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 stays correct. `jlMillisElapsed` carries on from where it was when the tick
starts or stops; it never jumps back. 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 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 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 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 called from an interrupt. `fn` may therefore do anything, but what it does
lands on the frame, not between frames. lands on the frame, not between frames.
On SDL3 the sound is mixed in program time, a few frames ahead, and `fn` is On the hosted ports the sound is mixed in program time, a few frames ahead,
called by the mixer between exactly the two samples where its tick falls: in and `fn` is called by the mixer between exactly the two samples where its tick
program time, so it may do anything, yet a note it starts begins at the right falls: in program time, so it may do anything, yet a note it starts begins at
place in the sound. `hz` may be anything up to the mixer's 44100. the right place in the sound. `hz` may be anything up to the mixer's rate,
44100 (22050 on Win16).
=== jlAudioCriticalEnter / jlAudioCriticalExit === jlAudioCriticalEnter / jlAudioCriticalExit
@ -547,8 +570,10 @@ void jlAudioCriticalExit(void);
Critical section against the tick interrupt. If you mutate state that the Critical section against the tick interrupt. If you mutate state that the
registered tick callback reads, wrap the mutation in these so the interrupt 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 cannot observe a half-written value. On DOS, the Amiga, the ST, and the
with no audio interrupt, and on DVX and SDL3, where the callback runs in X68000 a section holds every interrupt off, so keep it short. Sections nest:
program time, both are no-ops. 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. *Parameters:* none. *Returns:* nothing.

View file

@ -8,6 +8,7 @@ Declared in `<joey/core.h>`, included by `<joey/joey.h>`.
---- ----
typedef struct { typedef struct {
uint32_t codegenBytes; uint32_t codegenBytes;
bool noAudio;
} jlConfigT; } jlConfigT;
---- ----
@ -18,13 +19,26 @@ typedef struct {
|`codegenBytes` |`codegenBytes`
|`uint32_t` |`uint32_t`
|Size of the runtime compiled-sprite cache. `0` selects a per-platform |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 default: 8 KB on the IIgs, 64 KB on the Amiga and ST, and 32 KB on the
sprite and leaves the cache unused). On the IIgs the cache is X68000, DOS, DVX, and Win16. The other hosted ports interpret every sprite
one Memory Manager block that may not span a 64 KB bank boundary, so values and never use the cache, but still allocate it (32 KB by default). The
above 65536 are clamped to 65536, and even a clamped request can fail if no cache is one block, and `jlInit` fails if it cannot be had -- except on the
free bank exists. Size it against `jlSpriteCompiledSize()`. 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 === jlInit
[source,c] [source,c]
@ -33,8 +47,11 @@ bool jlInit(const jlConfigT *config);
---- ----
Initialises the library: takes over the display, allocates the stage and the 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 codegen arena, brings up input, and last starts audio (unless `noAudio`). Must
function. 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"] [cols="1,1,4", options="header"]
|=== |===
@ -43,7 +60,9 @@ function.
|=== |===
*Returns:* `true` on success. On failure returns `false` and `jlLastError()` *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 === jlShutdown
@ -52,8 +71,10 @@ describes the reason.
void jlShutdown(void); void jlShutdown(void);
---- ----
Shuts the library down, restoring the display mode, releasing all library Shuts the library down: stops audio first, then restores the display mode,
resources and un-hooking any interrupt vectors the port installed. Call it on 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. every exit path; skipping it can leave the machine unusable.
*Parameters:* none. *Returns:* nothing. *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 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 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 substantial.* On the IIgs the whole C heap is one Memory Manager block of
`malloc` capped around 32 KB, so multi-kilobyte caches and frame buffers must 32 KB to 48 KB (and, if that block cannot be had, only what the program's link
come from the Memory Manager instead. 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"] [cols="1,1,4", options="header"]
|=== |===
@ -124,7 +146,9 @@ zero-filled.
void jlFree(void *p); 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"] [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 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 display's native refresh rate. On the machine ports it waits for the
mechanism is always a hardware-level wait rather than a software timer. 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. *Parameters:* none. *Returns:* nothing.
@ -178,13 +205,15 @@ uint16_t jlFrameCount(void);
*Parameters:* none. *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] [WARNING]
==== ====
You must poll faster than twice `jlFrameHz()` or edges are missed, and the The counter wraps roughly every 18 minutes at 60 Hz, so compare two readings
counter wraps roughly every 18 minutes at 60 Hz. For elapsed-time work prefer by subtracting them. For elapsed-time work prefer `jlMillisElapsed`.
`jlMillisElapsed`.
==== ====
=== jlFrameHz === jlFrameHz
@ -196,13 +225,16 @@ uint16_t jlFrameHz(void);
*Parameters:* none. *Parameters:* none.
*Returns:* the nominal display frame rate in Hz -- 50 on Amiga PAL, 60 on IIgs *Returns:* the nominal display frame rate in Hz -- 60 on the IIgs, or 50 on a
and ST NTSC, 55 on the X68000, 70 on VGA mode 13h and on DVX, whose window has PAL machine; 50 on the Amiga, whose screen is PAL, or 60 on an NTSC Amiga that
a virtual 70 Hz (and whose full screen is mode 13h's, or a VBE mode's, with the cannot show PAL; 50 or 60 on the ST, as the machine is set; 55 on the X68000;
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 70 on VGA mode 13h and on DVX, whose window has a virtual 70 Hz (and whose full
nearest whole number: 55.46 Hz on the machine, 56.07 Hz under MAME, which screen is mode 13h's, or a VBE mode's, with the same frame count); and 60 on
therefore reports 56. The real cadence may drift slightly; this is the figure every hosted port, whose window has a virtual 60 Hz. The X68000 times its
to divide by when converting frames to seconds. 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 === jlMillisElapsed
@ -211,10 +243,9 @@ to divide by when converting frames to seconds.
uint32_t jlMillisElapsed(void); uint32_t jlMillisElapsed(void);
---- ----
Monotonic millisecond counter since `jlInit`. DOS, ST, X68000, DVX and SDL3 read a Monotonic millisecond counter since `jlInit`. It may advance a frame at a
real timer; IIgs and Amiga derive it from the frame counter, so the time. Two calls separated by a long render report the true elapsed time, not
value steps at frame granularity there. Two calls separated by a long render report the true the number of vertical blanks that fired.
elapsed time, not the number of vertical blanks that fired.
*Parameters:* none. *Parameters:* none.
@ -222,8 +253,8 @@ elapsed time, not the number of vertical blanks that fired.
[NOTE] [NOTE]
==== ====
On the frame-derived ports you must poll at least once every 65535 frames -- Call it at least once every 15 minutes, or the time between two calls can be
about 18 minutes at 60 Hz -- or wrapped frames are lost. lost.
==== ====
=== jlRandom === jlRandom

View file

@ -2,20 +2,40 @@
Declared in `<joey/debug.h>`. Declared in `<joey/debug.h>`.
A crash-tracing logger that writes to a file beside the executable (on DVX, A crash-tracing logger that writes `joeylog.txt` in the current directory
in the program's settings folder, `CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`). (on DVX, in the program's settings folder,
Debugging a program on an emulated 1988 machine is awkward, and this is `CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`). Debugging a program on an emulated
usually the fastest way to find out what it did before it stopped doing it. 1988 machine is awkward, and this is usually the fastest way to find out what
The headless run scripts retrieve the log automatically. 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] [NOTE]
==== ====
The logger needs a multi-kilobyte ring buffer, which the tightest targets The logger's buffer is linked into every program, since the library logs its
cannot spare. Space Taxi compiles it out entirely on the IIgs and X68000 for own failures: the 8 KB ring on the IIgs, and a 16 KB file buffer on every
that reason. If you are debugging a memory-tight program, expect to do the other port but DVX. A memory-tight program can still leave its own log calls
same and to observe hardware state through the emulator instead. 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 === jlLog
[source,c] [source,c]
@ -41,13 +61,14 @@ void jlLogF(const char *fmt, ...);
---- ----
Appends a formatted message, `printf` style, as one line: like `jlLog`, the 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"] [cols="1,1,4", options="header"]
|=== |===
|Parameter |Type |Description |Parameter |Type |Description
|`fmt` |`const char *` |Format string. |`fmt` |`const char *` |Format string.
|`...` | |Arguments matching the format string. |`+...+` | |Arguments matching the format string.
|=== |===
*Returns:* nothing. *Returns:* nothing.
@ -66,8 +87,10 @@ time.
void jlLogFlush(void); void jlLogFlush(void);
---- ----
Flushes buffered log output to disk. Call it before anything that might crash Flushes buffered log output to disk. On the IIgs it is the only way the log
or exit, or the last and most interesting lines may never be written. 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. *Parameters:* none. *Returns:* nothing.
@ -79,7 +102,8 @@ void jlLogReset(void);
---- ----
Clears the log, discarding previous contents. Call once at startup so each run 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. *Parameters:* none. *Returns:* nothing.
@ -89,6 +113,7 @@ int main(void) {
jlConfigT config; jlConfigT config;
config.codegenBytes = 32UL * 1024; config.codegenBytes = 32UL * 1024;
config.noAudio = true;
if (!jlInit(&config)) { if (!jlInit(&config)) {
return 1; 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 you cannot corrupt memory by drawing off the edge. Every primitive marks the
rows it touched, so `jlStagePresent` knows what to copy. rows it touched, so `jlStagePresent` knows what to copy.
Primitives work in colour indices. Per-scanline palette resolution happens only Primitives work in colour indices, and use only the low 4 bits of one.
at display time, so drawing does not care which palette a line uses. Per-scanline palette resolution happens only at display time, so drawing does
not care which palette a line uses.
=== jlSurfaceClear === 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 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 reachable by north, south, east, and west steps. A no-op if the start point is
off-surface or already `newColor`. 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"] [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 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 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 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 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 Two directories sit beside the executable, staged there by each platform's disk
packager: packager:
* `DATA/` -- read-only game content: levels, sprites, audio. * `DATA/` -- game content: levels, sprites, audio.
* `SAVES/` -- writable saves and preferences. * `SAVES/` -- writable saves and preferences.
DVX is the exception, because its programs share one current directory: Where the calls look for them:
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`, * *The hosted ports* make the program's own folder the current directory in
which DVX keeps apart from the program files so a reinstall never overwrites `jlInit`, so both are found beside the program wherever it was started
it. The calls below find both there by themselves. 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 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. added for you, so never hard-code `"DATA/"` or `"SAVES/"` yourself.
[IMPORTANT] [IMPORTANT]
==== ====
Keep names portable across the retro filesystems: at most 8 characters plus a Keep names portable across the retro filesystems: at most 8 characters plus
short extension, letters and digits only, so they satisfy both ProDOS and DOS an extension of up to 3, letters and digits only, starting with a letter, so
8.3. Do not depend on case being preserved. 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 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. 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 === 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); 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"] [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); 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"] [cols="1,1,4", options="header"]
|=== |===
@ -145,8 +170,11 @@ uint32_t jlDiskFree(void);
*Parameters:* none. *Parameters:* none.
*Returns:* free space in bytes on the volume holding `SAVES/`. Returns 0 if it *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 cannot be determined, and saturates at `UINT32_MAX` on volumes larger than
`UINT32_MAX` on volumes larger than 4 GB. 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] [source,c]
---- ----

View file

@ -2,13 +2,13 @@
Declared in `<joey/input.h>`. 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 joystick state. Everything else in this section reads the state that poll
captured. captured.
Two parallel keyboard interfaces share that one poll. The *predicates* answer 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 "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 the machine's keyboard layout already applied, which is the right shape for
text entry. They coexist freely. text entry. They coexist freely.
@ -20,32 +20,33 @@ transition happened.
[source,c] [source,c]
---- ----
typedef enum { typedef enum {
KEY_NONE = 0, JL_KEY_NONE = 0,
KEY_A .. KEY_Z, JL_KEY_A .. JL_KEY_Z,
KEY_0 .. KEY_9, JL_KEY_0 .. 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_F10, JL_KEY_F1 .. JL_KEY_F10,
KEY_COUNT JL_KEY_COUNT
} jlKeyE; } jlKeyE;
typedef enum { typedef enum {
MOUSE_BUTTON_NONE = 0, MOUSE_BUTTON_LEFT, JL_MOUSE_BUTTON_NONE = 0, JL_MOUSE_BUTTON_LEFT,
MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT JL_MOUSE_BUTTON_RIGHT, JL_MOUSE_BUTTON_MIDDLE, JL_MOUSE_BUTTON_COUNT
} jlMouseButtonE; } jlMouseButtonE;
typedef enum { JOYSTICK_0 = 0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE; typedef enum { JL_JOYSTICK_0 = 0, JL_JOYSTICK_1, JL_JOYSTICK_COUNT } jlJoystickE;
typedef enum { JOY_BUTTON_0 = 0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE; 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). `JL_CHAR_QUEUE_SIZE` (32).
Control codes delivered through the character queue: `JL_CHAR_BACKSPACE` 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 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 === jlInputPoll
@ -55,7 +56,9 @@ void jlInputPoll(void);
---- ----
Refreshes all input state and refills the typed-character queue. Call once per 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. *Parameters:* none. *Returns:* nothing.
@ -72,11 +75,13 @@ not polling means nothing ever changes.
int jlInputGetChar(void); 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 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, correctly. The layout is the machine's own on the IIgs, Amiga, ST, and
DVX's own on DVX and the operating system's on SDL3; the DOS port translates X68000. It is DVX's own on DVX, and the operating system's on SDL3, the web,
with a fixed US layout. 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. 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] [NOTE]
==== ====
The queue holds `JL_CHAR_QUEUE_SIZE - 1` characters, so 31. When full, further 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 characters are dropped until you pop some. A chord types nothing on any port:
ports whose keyboard services repeat; repeat policy is yours to manage. 7-bit Ctrl+M is not Return and Ctrl+1 is not 1, and Alt chords type nothing where
ASCII only -- no Unicode, no input methods. 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 The return type is plain `int`, not a stdint type, deliberately: this is the
`getchar()`-style "byte or -1" contract. `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); void jlWaitForAnyKey(void);
---- ----
Blocks until the user presses any key, polling internally so per-port input Blocks until the user presses a key -- one JoeyLib names (with a `jlKeyE`),
machinery -- including interrupt-driven audio samplers -- keeps running during or one that types a character, punctuation for instance -- polling internally
the wait. 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 *Parameters:* none. *Returns:* nothing. The character queue is emptied on
return, so the dismissing keystroke is consumed rather than delivered as text. 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 `true` only in the frame the key went down; `jlKeyReleased` only in the frame
it came up. 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 === jlMousePresent
[source,c] [source,c]
@ -139,13 +164,10 @@ bool jlMousePresent(void);
*Parameters:* none. *Parameters:* none.
*Returns:* `true` if the platform confirmed a mouse at init. On DOS that means *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 the INT 33h driver reset successfully; on the Amiga, that JoeyLib's input
SDL3 the OS mouse is always wired. On DVX the position is the pointer's over window opened; on Win32 and Win16, that Windows reports a mouse; on SDL3 and
the program's window, divided by the window's scale; it keeps its last value the web, that SDL reports one. The IIgs, ST, X68000, DVX, Haiku, and BeOS
while the pointer is elsewhere. On DVX's full screen it is the pointer's always report one, and the PSP never does.
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.
=== jlMouseX / jlMouseY === jlMouseX / jlMouseY
@ -161,6 +183,14 @@ int16_t jlMouseY(void);
Until the platform reports the pointer, it is the middle of the stage, Until the platform reports the pointer, it is the middle of the stage,
(160, 100), on every port. (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 === jlMouseDown / jlMousePressed / jlMouseReleased
[source,c] [source,c]
@ -178,6 +208,10 @@ bool jlMouseReleased(jlMouseButtonE button);
*Returns:* held state, rising edge, and falling edge respectively. *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 === jlJoystickConnected
[source,c] [source,c]
@ -188,13 +222,20 @@ bool jlJoystickConnected(jlJoystickE js);
[cols="1,1,4", options="header"] [cols="1,1,4", options="header"]
|=== |===
|Parameter |Type |Description |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 *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 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 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 === jlJoystickX / jlJoystickY
@ -210,8 +251,11 @@ int8_t jlJoystickY(jlJoystickE js);
|`js` |`jlJoystickE` |Which stick. |`js` |`jlJoystickE` |Which stick.
|=== |===
*Returns:* axis value, -127 to +127. Digital sticks snap to the extremes; *Returns:* axis value, -127 to +127. Digital sticks (the Amiga, ST, and
analog sticks return the raw centred value. Test against a threshold rather 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. than for exact values, so the same code works with both.
=== jlJoyDown / jlJoyPressed / jlJoyReleased === jlJoyDown / jlJoyPressed / jlJoyReleased
@ -232,6 +276,15 @@ bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button);
*Returns:* held state, rising edge, and falling edge respectively. *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 === jlJoystickReset
[source,c] [source,c]
@ -239,34 +292,35 @@ bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button);
void jlJoystickReset(jlJoystickE js, uint8_t deadZone); 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 * *IIgs*: polling starts again if it had stopped for want of a stick, and the
short window with no stick detected, because polling costs about 3 ms a frame stick is re-centred at its next reading.
there -- and it does not re-probe on its own. If a user plugs a stick in while * *DOS*: the game port is probed again, and the stick is re-centred at its
your program runs, you must call this to resume polling. 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"] [cols="1,1,4", options="header"]
|=== |===
|Parameter |Type |Description |Parameter |Type |Description
|`js` |`jlJoystickE` |Which stick. |`js` |`jlJoystickE` |Which stick.
|`deadZone` |`uint8_t` |Raw readings within this many units of centre clamp |`deadZone` |`uint8_t` |Axis values within this many units of 0 (on the
to 0. Pass 0 to disable the dead zone. -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. *Returns:* nothing.
[IMPORTANT] [IMPORTANT]
==== ====
The next poll after this call captures the stick's *current* raw position as On the IIgs and DOS the next reading after this call is taken as the stick's
the new centre, so the user must be holding the stick centred when you call it. centre, so the user must be holding the stick centred when you call it. The
call is safe to make unconditionally.
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.
==== ====

View file

@ -10,11 +10,18 @@ which palette each scanline uses. That is how a JoeyLib program shows more than
[IMPORTANT] [IMPORTANT]
==== ====
Colour 0 of every palette is black, always. `jlPaletteSet` silently masks entry 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 0 to `$000` whatever you pass, and `jlSurfaceLoadFile` does the same to every
border follows colour 0 of the active palette, and colour 0 is the transparent palette it loads. This is deliberate: the Amiga and ST hardware border follows
colour for sprite drawing. You have 15 usable colours per palette. 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 === jlPaletteSet
[source,c] [source,c]
@ -42,7 +49,9 @@ Loads all 16 colours of one palette.
void jlPaletteGet(const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16); 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"] [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. // Sky on the top third, ground below, using two palettes.
jlScbSetRange(stage, 0, 65, 0); jlScbSetRange(stage, 0, 65, 0);
jlScbSetRange(stage, 66, SURFACE_HEIGHT - 1, 1); jlScbSetRange(stage, 66, JL_SURFACE_HEIGHT - 1, 1);
---- ----
=== jlScbGet === 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, 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 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 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 communications calls, whose driver owns the ports the same way. SDL3, the web,
and the PSP have no serial port: `jlSerialOpen` fails there. the PSP, Haiku, and BeOS have no serial port: `jlSerialOpen` fails there.
The model is *non-blocking and buffered*. Received bytes accumulate in a 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 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 OS on Amiga, ST, DVX, and Windows -- and reads never block. Call `jlSerialPoll`
frame: it is next to a no-op where the interrupt or OS already fills the ring, once per frame. (The X68000 takes its interrupt only when the port is opened in
and the hardware drain where nothing does, so calling it every frame is always supervisor mode, which `jlInit` puts every program in; opened without it, the
correct. On DVX it also gives the other programs their turn port is drained from RSDRV.SYS's own 64-byte buffer by the serial calls.)
(<<_dvx_programs>>).
On the IIgs a Super Serial Card only interrupts with its interrupt switch 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 (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. 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 JoeyLib's own work is safe while a port is open, with one exception on the
X68000 (below). On the IIgs, where the X68000 (below). On the IIgs, where the
display and sound code masks interrupts for long stretches, opening a port 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 * *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 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 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 access goes through the operating system and has not been measured; treat
it as a risk there too. it as a risk there too.
* *Starting a song on the IIgs.* `jlAudioPlayMod` loads the song's * *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 * *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 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 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. and sound effects.
* *Per-scanline palettes on the X68000.* The port reloads its one hardware * *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 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. 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 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. release it between bands.
* *The program's own code*, wherever it masks interrupts. * *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 * For data that must arrive intact, use a protocol with checksums and
retransmission; XMODEM and its descendants exist for exactly this. 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 === Types
[source,c] [source,c]
@ -143,7 +189,7 @@ typedef enum {
} jlSerialFlowE; } jlSerialFlowE;
typedef struct { 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 dataBits; // 5 .. 8
uint8_t stopBits; // 1 or 2 uint8_t stopBits; // 1 or 2
jlSerialParityE parity; jlSerialParityE parity;
@ -152,10 +198,54 @@ typedef struct {
} jlSerialConfigT; } 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` 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 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, 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. Amiga unit 0; or the X68000's RS-232C port.
On Win16 a `baud` above 57600 runs at 115200 through KPCOMM.DRV, the 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 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 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` 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 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 port runs at 75 to 38400 bps. 38400 is really 39062 bps, 1.7% fast, within
for CTS; RTS stays up, since JoeyLib's receive interrupt, not the driver's, the usual tolerance of a UART at the far end.
fills the receive ring. 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 `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 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 === jlSerialOpen
@ -194,9 +283,10 @@ open without an intervening close fails.
|=== |===
*Returns:* `true` on success. `false` if the port cannot be opened or the *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 platform has no serial support -- and then every other call no-ops. It is
X68000 it is also `false` when RSDRV.SYS is not loaded, and `jlLastError` also `false`, with `jlLastError` saying why, when no UART answers at the DOS
says so. COM port, when no Super Serial Card answers in the IIgs slot, and when
RSDRV.SYS is not loaded on the X68000.
=== jlSerialClose === jlSerialClose
@ -207,9 +297,12 @@ void jlSerialClose(void);
Closes the port and releases its resources. Safe when not open. A port still 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 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 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 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 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); 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"] [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 -- `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 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 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"] [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. 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 distinguishing feature is that *sprite blitters are compiled at run time*.
The library emits native 65816, 68000 or x86 code for each sprite, including On the IIgs, Amiga, ST, X68000, DOS, DVX, and Win16 the library emits native
pre-shifted variants so smooth horizontal motion needs no runtime bit shifting. 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] [IMPORTANT]
==== ====
`jlSpriteDraw` does *not* compile on demand. An uncompiled sprite draws through `jlSpriteDraw` does *not* compile on demand. An uncompiled sprite draws through
an interpreter roughly *forty times slower*, and nothing reports this. Always an interpreter *two to six times slower*, depending on the machine, and
call `jlSpriteCompile` or `jlSpritePrewarm` during loading. nothing reports this. Always call `jlSpriteCompile` during loading.
==== ====
=== jlSpriteT and jlSpriteBackupT === jlSpriteT and jlSpriteBackupT
@ -35,33 +37,42 @@ typedef struct {
---- ----
You supply `bytes` and its capacity; the library fills in the rest to describe 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 what it actually captured, in `x`, `y`, `width`, `height`, and `sizeBytes`.
*16-pixel-aligned window* containing the sprite, not the sprite rectangle, and That is often not the sprite rectangle:
the `x`, `width`, `height` and `sizeBytes` fields report that true geometry. It
is the recorded window you must not overwrite before restoring.
=== 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] [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 How many `_type` elements a backup buffer needs for a `widthTiles` by
position, on every port. Use it to size your backing array. `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] [source,c]
---- ----
// 2-byte aligned by construction -- see the warning below. static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)];
static uint16_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4) / 2];
---- ----
[WARNING] [WARNING]
==== ====
Give the backing array *at least 2-byte alignment*. An odd buffer address Give the backing array *at least 2-byte alignment*. On the Amiga, ST, and
routes save and restore onto slow interpreted paths on the planar ports, X68000 an odd buffer address sends the save and restore calls down a much
because the 68000 cannot do word accesses at odd addresses. Declaring it slower path, and on the Amiga and ST it makes the trusted variants crash; the
`uint16_t`, as above, guarantees this. other ports do not mind. Declaring it `uint16_t`, as above, guarantees the
alignment.
==== ====
=== jlSpriteBankLoad === jlSpriteBankLoad
@ -109,8 +120,10 @@ replacement for `jlSpriteBankLoad` plus a compile pass.
*Parameters:* as `jlSpriteBankLoad`. *Parameters:* as `jlSpriteBankLoad`.
*Returns:* the number of cels written, or 0 if the file is missing or its *Returns:* the number of cels written, or 0 if the file is missing, its
magic, target or shift count does not match this build. 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 === jlSpriteBankSavePrecompiled
@ -129,14 +142,16 @@ cache the large per-platform compiled form on first run.
|=== |===
|Parameter |Type |Description |Parameter |Type |Description
|`path` |`const char *` |Destination path. |`path` |`const char *` |Destination path.
|`cels` |`jlSpriteT **` |The sprites to cache. Uncompiled ones are cached |`cels` |`jlSpriteT **` |The sprites to cache, all of one size. Uncompiled
as-is and will compile again next run. 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. |`count` |`uint16_t` |How many sprites.
|`palette` |`const uint16_t *` |Optional, may be `NULL`. Embedded if given. |`palette` |`const uint16_t *` |Optional, may be `NULL`. Embedded if given.
|=== |===
*Returns:* `true` on success; `false` if the destination is not writable, which *Returns:* `true` on success; `false` if the cels are not all one size or the
is harmless -- the program simply keeps compiling at startup. destination is not writable, which is harmless -- the program simply keeps
compiling at startup.
=== jlSpriteCreate === jlSpriteCreate
@ -157,8 +172,8 @@ Wraps a tile-data blob in a sprite.
|`heightTiles` |`uint8_t` |Height in tiles. Must be non-zero. |`heightTiles` |`uint8_t` |Height in tiles. Must be non-zero.
|=== |===
*Returns:* the sprite, or `NULL` if either dimension is 0 or the codegen arena *Returns:* the sprite, or `NULL` if `tileData` is `NULL`, either dimension is
cannot fit a placeholder entry. 0, or memory runs out. Creating a sprite does not touch the codegen arena.
=== jlSpriteDestroy === jlSpriteDestroy
@ -185,8 +200,11 @@ built from is *not* freed -- that buffer is yours.
bool jlSpriteCompile(jlSpriteT *sp); bool jlSpriteCompile(jlSpriteT *sp);
---- ----
Compiles the sprite's draw routines into the codegen arena. Idempotent -- Compiles the sprite's draw, save, and restore routines into the codegen arena,
compiling an already-compiled sprite is a no-op that returns `true`. 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"] [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. |`sp` |`jlSpriteT *` |Sprite to compile.
|=== |===
*Returns:* `true` if the sprite is compiled. `false` if the arena is full (you *Returns:* `true` if the sprite is compiled. `false` if it will be drawn by the
may call `jlSpriteCompact` and retry) or the sprite has no source tile data. 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 === jlSpriteCompiledSize
@ -216,24 +246,6 @@ the current platform, rather than guessing.
*Returns:* bytes occupied in the codegen arena, summed over all compiled shift *Returns:* bytes occupied in the codegen arena, summed over all compiled shift
variants. Returns 0 if the sprite was never compiled. 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 === jlSpriteDraw
[source,c] [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. 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"] [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. |`sp` |`jlSpriteT *` |Sprite whose footprint to capture.
|`x`, `y` |`int16_t` |Position the sprite will be drawn at. |`x`, `y` |`int16_t` |Position the sprite will be drawn at.
|`backup` |`jlSpriteBackupT *` |Receives the capture. You must set `bytes` to |`backup` |`jlSpriteBackupT *` |Receives the capture. You must set `bytes` to
a buffer of at least `JOEY_SPRITE_BACKUP_BYTES(w, h)` capacity for fully 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. 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`, *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 === jlSpriteRestoreUnder
@ -293,7 +309,7 @@ Repaints the destination from a previous capture.
|Parameter |Type |Description |Parameter |Type |Description
|`s` |`jlSurfaceT *` |Destination surface. |`s` |`jlSurfaceT *` |Destination surface.
|`backup` |`const jlSpriteBackupT *` |A backup from `jlSpriteSaveUnder`, not |`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. *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 Behaviour is *undefined* -- out-of-bounds writes, crashes -- unless all of the
following hold: 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*; * 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: * 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 16-pixel-aligned x and width on the Amiga, ST, and X68000, even on the
to the sprite's pixel height, and a buffer at least 2-byte aligned; other ports, and a height equal to the sprite's pixel height (on the IIgs,
* the recorded window is fully on the destination surface. 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. 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. |`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 === jlSpriteCompact
@ -403,4 +424,9 @@ Arena introspection, for judging whether to compact or to raise
*Parameters:* none. *Parameters:* none.
*Returns:* bytes in use, and total arena size. Free space is the difference, *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>`. 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 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, own 16x16 palette set. Storage is target-native -- chunky 4bpp on the IIgs,
planar on Amiga, ST and X68000 -- but every entry point speaks in colour DOS, DVX, and the hosted ports, planar on the Amiga, ST, and X68000 -- but every
indices, so your code never sees the difference. 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 === Constants
@ -13,13 +14,13 @@ indices, so your code never sees the difference.
|=== |===
|Name |Value |Meaning |Name |Value |Meaning
|`SURFACE_WIDTH` |320 |Surface width in pixels. |`JL_SURFACE_WIDTH` |320 |Surface width in pixels.
|`SURFACE_HEIGHT` |200 |Surface height in pixels. |`JL_SURFACE_HEIGHT` |200 |Surface height in pixels.
|`SURFACE_BYTES_PER_ROW` |160 |Bytes per row in chunky 4bpp form. |`JL_SURFACE_BYTES_PER_ROW` |160 |Bytes per row in chunky 4bpp form.
|`SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes. |`JL_SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes.
|`SURFACE_PALETTE_COUNT` |16 |Palettes per surface. |`JL_SURFACE_PALETTE_COUNT` |16 |Palettes per surface.
|`SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette. |`JL_SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette.
|`SURFACE_PALETTE_ENTRIES` |256 |Total palette entries per surface. |`JL_SURFACE_PALETTE_ENTRIES` |256 |Total palette entries per surface.
|=== |===
=== jlSurfaceT === jlSurfaceT
@ -29,7 +30,7 @@ indices, so your code never sees the difference.
typedef struct jlSurfaceT jlSurfaceT; typedef struct jlSurfaceT jlSurfaceT;
---- ----
Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet` or a loader. Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet`, or a loader.
=== jlSurfaceCreate === jlSurfaceCreate
@ -38,7 +39,9 @@ Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet` or a loader.
jlSurfaceT *jlSurfaceCreate(void); 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. *Parameters:* none.
@ -83,7 +86,7 @@ the displayed framebuffer until the next present.
void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src); 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"] [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 * up to 15 extra pixels each side are copied, so a snapped erase can clobber
on-screen content outside the rectangle you asked for; 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. 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. 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, *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 === jlSurfaceHash
@ -213,8 +219,10 @@ void jlStagePresent(void);
---- ----
Flips the dirty regions of the stage to the display and clears the dirty state. 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 Where the stage already has the display's layout this is a copy (the IIgs's
chunky-to-planar conversion. 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 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 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] [NOTE]
==== ====
If you modify pixels by a route other than the library's primitives, the dirty Every call that changes the stage's pixels marks the rows it touched, and the
tracking does not know, and those rows will not be presented. 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 The consequence is that everything else works on tiles too. You can paint tile
content with `jlDrawPixel` and `jlFillRect`, generate it procedurally, or edit 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 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 === Constants and Types
@ -18,19 +18,19 @@ one. Fonts, terrain sets and spritesheets are all the same thing.
|=== |===
|Name |Value |Meaning |Name |Value |Meaning
|`TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels. |`JL_TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels.
|`TILE_BYTES_PER_ROW` |4 |Bytes per tile row (8 pixels at 4bpp). |`JL_TILE_BYTES_PER_ROW` |4 |Bytes per tile row (8 pixels at 4bpp).
|`TILE_BYTES` |32 |Bytes in one tile. |`JL_TILE_BYTES` |32 |Bytes in one tile.
|`TILE_BLOCKS_PER_ROW` |40 |Tile columns on a surface. |`JL_TILE_BLOCKS_PER_ROW` |40 |Tile columns on a surface.
|`TILE_BLOCKS_PER_COL` |25 |Tile rows on a surface. |`JL_TILE_BLOCKS_PER_COL` |25 |Tile rows on a surface.
|`TILE_NO_GLYPH` |0xFFFF |`jlDrawText` map entry meaning "skip this character". |`JL_TILE_NO_GLYPH` |0xFFFF |`jlDrawText` map entry meaning "skip this character".
|`TILE_MAP_SKIP` |0xFF |`jlTileMapPaste` entry meaning "leave this cell alone". |`JL_TILE_MAP_SKIP` |0xFF |`jlTileMapPaste` entry meaning "leave this cell alone".
|=== |===
[source,c] [source,c]
---- ----
typedef struct jlTileT { typedef struct jlTileT {
uint8_t pixels[TILE_BYTES]; uint8_t pixels[JL_TILE_BYTES];
} jlTileT; } jlTileT;
---- ----
@ -39,11 +39,12 @@ need a whole scratch surface.
[IMPORTANT] [IMPORTANT]
==== ====
A `jlTileT` is *target-native*: plane-major on the planar ports, chunky on the A `jlTileT` is *target-native*: plane-major on the Amiga and X68000, row by row
others. Treat it as opaque. In particular, do not feed a tile captured by with each row's four plane bytes together on the ST, and chunky on the others.
`jlTileSnap` to `jlTilePasteMono`, which wants a chunky silhouette -- that is a Treat it as opaque. In particular, do not feed a tile from `jlTileSnap` or
format mismatch and draws the wrong shape. Use `jlTilePasteGlyph` for 1-bit `jlTileBankLoad` to `jlTilePasteMono`, which wants a chunky silhouette -- on the
glyph data instead. Amiga, ST, and X68000 that is a format mismatch and draws the wrong shape. Use
`jlTilePasteGlyph` for 1-bit glyph data instead.
==== ====
=== jlTileCopy === 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 *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 === jlTileSnap
@ -233,10 +234,10 @@ comes from a separate map, as in C64-style colour RAM. Opaque.
[WARNING] [WARNING]
==== ====
This call wants a *chunky* silhouette, but a `jlTileT` from `jlTileSnap` is This call wants a *chunky* silhouette, but a `jlTileT` from `jlTileSnap` or
target-native. Feeding one to this function on a planar port is a format `jlTileBankLoad` is target-native. Feeding one to this function on the Amiga,
mismatch. For glyphs, use `jlTilePasteGlyph`, which takes unambiguous 1-bit ST, or X68000 is a format mismatch. For glyphs, use `jlTilePasteGlyph`, which
input. takes unambiguous 1-bit input.
==== ====
=== jlTilePasteGlyph === 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 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 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"] [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. |`fontSurface` |`const jlSurfaceT *` |Surface holding the glyphs.
|`asciiMap` |`const uint16_t *` |256 entries mapping ASCII code to glyph |`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 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. |`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); 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. 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 For anything larger than a few tiles this is substantially faster than looping
over `jlTilePaste`. over `jlTilePaste`.
@ -316,7 +321,7 @@ over `jlTilePaste`.
|`wTiles`, `hTiles` |`uint8_t` |Rectangle size in tiles. Zero is a no-op. |`wTiles`, `hTiles` |`uint8_t` |Rectangle size in tiles. Zero is a no-op.
|`tiles` |`const jlTileT *` |The tileset. |`tiles` |`const jlTileT *` |The tileset.
|`map` |`const uint8_t *` |`wTiles * hTiles` tileset indices, row-major, so |`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 *Returns:* nothing. A rectangle whose edge crosses the surface boundary is
@ -324,7 +329,7 @@ rejected as a no-op.
[WARNING] [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 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. out-of-range index reads past the end of your array.
==== ====

View file

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

View file

@ -1,6 +1,6 @@
== Creating Assets == 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 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 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. 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 |`.jym` or a C header
|`jlMusicPlay` |`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` |Protracker `.MOD`
|`joeymod` |`joeymod`
|`.mod`, `.amod`, `.ntp` |`.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 Baked assets are staged into `DATA/` beside the executable, and the program
opens them with bare names. 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 === Graphics: Tiles and Sprites
==== Authoring the PNG ==== 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 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- 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. loader refuses a file baked for a different target rather than drawing garbage.
`--target` is required for tiles, and accepts `amiga`, `atarist`, `dos`, `--target` is required for tiles, and accepts `amiga`, `atarist`, `dos`,
`iigs` or `x68000`. `iigs`, or `x68000`.
==== Baking Sprites ==== 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. pixels. The sheet is cut into cels row by row.
Sprite `.spr` banks are *cross-target*: the same file works on every platform, 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 because it holds chunky 4bpp tiles and each port converts a sprite to its own
once, ship everywhere. form when the sprite is created. Bake once, ship everywhere.
Both formats can embed the 16-entry `$0RGB` palette, and both loaders will hand 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 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. 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 === Music: Protracker Modules
For richer music, author a four-channel `.MOD` in any tracker (ProTracker, 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] [source,console]
---- ----
$ tools/joeymod/joeymod song.mod song.mod # DOS and ST: validated copy $ tools/joeymod/joeymod song.mod song.mod # every port but the IIgs
$ tools/joeymod/joeymod song.mod song.amod # Amiga $ tools/joeymod/joeymod song.mod song.amod # Amiga, with an end marker
$ tools/joeymod/joeymod song.mod song.ntp # Apple IIgs $ tools/joeymod/joeymod song.mod song.ntp # Apple IIgs
---- ----
The output extension selects the conversion: The output extension selects the conversion:
* *`.mod`* -- a passthrough copy with the signature validated. DOS and ST * *`.mod`* -- a passthrough copy with the signature validated, played as it
detect song end inside libxmp-lite, so the file is used as-is. is on every port but the IIgs. The ports that mix modules in software
* *`.amod`* -- the same, plus an `E8FF` stop marker injected at song end. The (DOS, the ST, the X68000, and the hosted ports) find the song's end
Amiga replayer, PTPlayer, has no native song-end signal, so without this themselves, and the Amiga adds an end marker as it copies the module to
marker `jlAudioPlayMod(..., loop=false)` loops anyway. Other replayers ignore chip RAM, so `+jlAudioPlayMod(..., loop=false)+` plays the song once.
`E8FF`, so an `.amod` is harmless if it reaches the wrong port. * *`.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 * *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the
NinjaTrackerPlus converter the installer's `ntp` component provides (part of NinjaTrackerPlus converter the installer's `ntp` component provides (part of
`toolchain:iigs`; it needs the PHP command-line interpreter, which the `toolchain:iigs`; it needs the PHP command-line interpreter, which the
installer's host packages include). installer's host packages include).
Keep modules modest. A four-channel mix is real CPU work on DOS and the ST, and Keep modules modest. A four-channel mix is real CPU work on DOS, the ST, and
the sample data has to fit in memory alongside your game. the X68000, and the sample data has to fit in memory alongside your game.
=== Sound Effects === Sound Effects
@ -260,7 +366,7 @@ jlSurfaceSaveFile(surface, "title.sur");
jlSurfaceLoadFile(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 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 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 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) $< $@ python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target $(ASSET_PLATFORM) $< $@
---- ----
Tile banks and `.spc` files differ per platform, while `.spr`, `.jym` and 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, `.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 and every hosted port -- SDL3, the web, Win32, Win16, the PSP, Haiku, and
their code) and `SPRITEBAKE` the matching host `spritebake`. 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` `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`), 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. // Tiles into a caller-owned array.
jlTileT tiles[256]; jlTileT tiles[256];
bool valid[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); uint16_t n = jlTileBankLoad("font.tbk", tiles, 256, valid, palette);
// Or stream a big bank straight onto a surface, with no caller buffer. // Or stream a big bank straight onto a surface, with no caller buffer.

View file

@ -1,11 +1,11 @@
== Building Your Program == Building Your Program
JoeyLib does not install as a system library: each target needs its own cross 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, 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 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 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 get. JoeyLib's examples are described the same way and built by the same
rules; there is one set of them. 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 |Target |Output
|Apple IIgs |`build/iigs/bin/MYGAME` (an S16 OMF), its data staged in |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/data/MYGAME/`, its Finder icon in `build/iigs/icon/MYGAME/`, and
`build/iigs/bin/MYGAME.2mg` and `MYGAME-data.2mg` with `MYGAME_IIGS_GAME_DISK` the disks `build/iigs/bin/MYGAME.2mg` and
|Amiga |`build/amiga/bin/Mygame` `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 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 |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 (<<_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 |MS-DOS |`build/dos/bin/MYGAME.EXE`, with the DPMI server built in
|Sharp X68000 |`build/x68000/bin/MYGAME.X` |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/` `DATA/`
|SDL3 |`build/sdl3/bin/MYGAME`, a native program with SDL 3 linked in |SDL3 |`build/sdl3/bin/MYGAME`, a native program with SDL 3 linked in
|Web |`build/web/bin/MYGAME/`: `MYGAME.html`, `.js`, `.wasm`, and `.data` |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 |Win32 |`build/win32/bin/MYGAME.EXE`, a 32-bit Windows program
|Win16 |`build/win16/bin/MYGAME.EXE`, a Windows 3.x 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 |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 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 `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 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 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_PORTS` |The targets it builds for. Every target when unset.
|`ID_NAME` |Its file name without the extension (see <<_naming>>). |`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, |`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). every program is packaged apart anyway).
|`ID_BINDIR` |Build it there instead of `bin/`. |`ID_BINDIR` |Build it there instead of `bin/`.
|`ID_DATA` |Its data files (<<_data_files>>). |`ID_DATA` |Its data files (<<_data_files>>).
|`ID_TITLE` |DVX: its window title. PSP: its title in the PSP's menu (by |`ID_TITLE` |DVX: its window title. PSP: its title in the PSP's menu (by
default its name). 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 |`ID_IIGS_OVERFLOW_CAP` |IIgs: link large overflow segments (`0xF000`), which
a big program needs to leave the Memory Manager room (see a big program needs to leave the Memory Manager room (see
`make/support/iigs/clang-build.sh`). `make/support/iigs/clang-build.sh`).
@ -174,11 +179,12 @@ set up:
|`$(call programDataDir,ID)` |Where its `DATA/` files go on this target. |`$(call programDataDir,ID)` |Where its `DATA/` files go on this target.
|`$(call programOptionalDir,ID)` |Where the files go that it can run without |`$(call programOptionalDir,ID)` |Where the files go that it can run without
(<<_disk_images_and_packaging>>). (<<_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 |`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 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 web, Win32, Win16, the PSP, Haiku, BeOS) is the same chunky one, so they take
(the cels of a pre-compiled sprite bank, without their code). 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 |`SPRITEBAKE` |The host `spritebake` for this target (its rule is
JoeyLib's). JoeyLib's).
|`PROJECT_DIR`, `PROJECT_BUILD` |The project and its `build/`. |`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. |`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] [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` `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 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; 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 *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 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. needs to be on the disk.
*Amiga.* `joey-run` writes an `s/startup-sequence` that invokes your binary. *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: *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 `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. DOS program does.
*Win16.* A program is one `.EXE`, its 32-bit code bound to Open Watcom's *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 and MMSYSTEM), so its folder, with its `DATA/`, is all it needs; it ships the
way a DOS program does. way a DOS program does.
*PSP.* A program is its folder, `build/psp/bin/MYGAME/`: `EBOOT.PBP` (the *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 program, SDL 3 linked in, packed as the PSPSDK packs it, with its icon and no
pictures) and its `DATA/`. Copy the folder into `PSP/GAME/` on a memory other 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 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. `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 === Verifying It Actually Runs
Build success is weak evidence on retro targets. Run it with `joey-run` (see 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` |`make verify-audio`
|Every port's audio gate: the IIgs DOC and X68000 chips are read directly, |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 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 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 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 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. |The IIgs actually reached the program and drew something.
|`make iigs-verify-game-disk` |`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` |`scripts/verify-<port>-tilebank.sh`
|The port's own tile-bank bake loads pixel-exact both ways, with the same |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 for the other machines are refused. `scripts/verify-all.sh tilebank` runs
every port's. 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` |`scripts/bench-<port>.sh`
|Per-operation throughput for that port. |Per-operation throughput for that port.

View file

@ -64,7 +64,7 @@ Toolchain availability:
==== Installer reference ==== 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. repository.
[cols="1,3"] [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, whose files have gone, and any part of a port you installed only partly,
such as FS-UAE without a Kickstart. such as FS-UAE without a Kickstart.
| `import PATH...` | `+import PATH...+`
| Installs the files that are not downloaded (see <<Firmware that is not | 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. images, matched by checksum.
| `bundle FILE` | `bundle FILE`
@ -198,7 +198,7 @@ trees.
Nothing in `toolchains/` is part of the repository: delete the whole folder 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 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 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/`. live in `make/support/`.
=== Building === Building
@ -251,6 +251,7 @@ int main(void) {
jlConfigT config; jlConfigT config;
config.codegenBytes = 8 * 1024; config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) { if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError()); 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 . *One header.* `#include <joey/joey.h>` pulls in everything except the
optional serial add-on. 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 . *`jlInit` can fail*, and when it does `jlLastError()` tells you why. Always
check it. check it.
. *`jlShutdown` is not optional.* It restores the display mode, releases the . *`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) { int main(void) {
jlConfigT config; jlConfigT config;
jlSurfaceT *stage; jlSurfaceT *stage;
uint16_t colors[SURFACE_COLORS_PER_PALETTE]; uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint8_t i; uint8_t i;
config.codegenBytes = 8 * 1024; config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) { if (!jlInit(&config)) {
return 1; return 1;
} }
@ -300,7 +305,7 @@ int main(void) {
stage = jlStageGet(); // the library-owned back buffer stage = jlStageGet(); // the library-owned back buffer
// Build palette 0. Entry 0 is forced black whatever we put here. // 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[i] = 0x000;
} }
colors[1] = 0xFFF; // white colors[1] = 0xFFF; // white
@ -309,12 +314,12 @@ int main(void) {
jlPaletteSet(stage, 0, colors); jlPaletteSet(stage, 0, colors);
// Every scanline uses palette 0 (this is also the default). // 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); jlSurfaceClear(stage, 0);
jlFillRect(stage, 40, 30, 240, 140, 3); jlFillRect(stage, 40, 30, 240, 140, 3);
jlDrawRect(stage, 40, 30, 240, 140, 1); 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 jlStagePresent(); // nothing is visible until this runs
jlWaitForAnyKey(); jlWaitForAnyKey();
@ -375,7 +380,7 @@ the next. Nothing is written to your home directory.
| The machine, when naming an example rather than a file. | The machine, when naming an example rather than a file.
| `--data DIR` | `--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. example the game an interpreter plays.
| `--headless` | `--headless`
@ -392,7 +397,7 @@ example the game an interpreter plays.
| Apple IIgs: the ROM 01 machine instead of ROM 03. | Apple IIgs: the ROM 01 machine instead of ROM 03.
| `--machine st` | `--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 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`. (`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. default is 9600.
| `-- ARGS` | `-- 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 machines' autostart cannot pass arguments, and a page and a PSP program have
none. none.
|=== |===
@ -440,7 +445,7 @@ desktop.
An SDL3 program is a native program, so `joey-run` simply starts it, in a 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 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 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 screen are set by environment variables (<<_sdl3_programs>>). The SDL3 port
has no serial 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 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 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 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 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 `--headless` it runs on a private virtual display at one pixel per stage
pixel, so a `--snapshot` is the 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`, the kpjoy and mscomm projects, checkouts beside JoeyLib's (`../kpjoy`,
`../mscomm/drv`; `KPJOY_DIR` and `KPCOMM_DIR` name others). The program is `../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 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 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 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. full-screen key, so the program switches with Alt and the keypad's Enter.
@ -496,11 +501,11 @@ serial port.
==== Haiku and BeOS ==== Haiku and BeOS
A Haiku (`haiku`, `haiku32`) or BeOS R5 (`beos`) program runs in that port's 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 `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, 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 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 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 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 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 | As on the ST: Wine's COM1 can only be a device, so `socat` joins a
pseudo-terminal to the network both ways. pseudo-terminal to the network both ways.
| SDL3, Web, PSP | SDL3, Web, PSP, Haiku, BeOS
| None | None
| These ports have no serial port; `jlSerialOpen` fails there. | 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 and X68000, where MAME prints `Unable to load image` and runs with the port
unplugged. unplugged.
* While a port is listening, what the program sends waits for the connection, * 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, nothing on the other end would. `examples/sertest` sends its banner at once,
so there a client that connects late sees only the echo. so there a client that connects late sees only the echo.
==== Firmware that is not downloaded ==== Firmware that is not downloaded
Almost everything the emulators need is downloaded: EmuTOS for the Atari ST, 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 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, 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 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 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, machines), the X68000 CG (font) ROM,
for the Win16 port the Windows for Workgroups 3.11 install disc (as an 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 `.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 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 the installer takes R5's headers and libraries and makes the VM. Supply your
copies once: copies once:
@ -631,7 +639,7 @@ copies once:
$ ./install.sh import ~/my-roms $ ./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 (point it at your Workbench 3.1 disk for `serial.device` and
`lowlevel.library`, and at the S3 `lowlevel.library`, and at the S3
driver's `.zip` for its three files), identifies each file by its checksum 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. |`draw` |Every drawing primitive, laid out in four quadrants.
|`pattern` |Palette and per-scanline palette (SCB) work. |`pattern` |Palette and per-scanline palette (SCB) work.
|`keys` |Typed-character input and the key predicates. |`keys` |Typed-character input and the key predicates.
|`joy` |Joystick axes, buttons and recalibration. |`joy` |Joystick axes, buttons, and recalibration.
|`sprite` |Sprite loading, compiling, save-under and restore. |`sprite` |Sprite loading, compiling, save-under, and restore.
|`audio` |Tracker music, `.MOD` playback, digital sound effects, a streamed sound and the noise generator. |`audio` |Tracker music, `.MOD` playback, digital sound effects, a streamed sound, and the noise generator.
|`save` |Save files and the disk-space query. |`save` |Save files and the disk-space query.
|`tilebank` |Loading a baked tile bank both ways, and a bank for another machine refused. |`tilebank` |Loading a baked tile bank both ways, and a bank for another machine refused.
|`serial` |The optional RS-232 add-on. |`serial` |The optional RS-232 add-on.

View file

@ -3,8 +3,8 @@
=== What JoeyLib Is === What JoeyLib Is
JoeyLib is a single-codebase C game library for five 16-bit and 32-bit retro 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 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 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 3.1 on. You write one program against one API, and the same source compiles
and runs on all of them: and runs on all of them:
@ -28,8 +28,9 @@ and runs on all of them:
|512 KB |512 KB
|Word-interleaved planar. Shares 68000 assembly with the Amiga port. Two |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 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 build (`make ste`) uses the STe's 4-bit palette, DMA sound, BLiTTER,
Mega STe's 16 MHz, and runs only there. See <<_the_atari_st_family>>. enhanced joystick ports, and a Mega STe's 16 MHz, and runs only there. See
<<_the_atari_st_family>>.
|MS-DOS |MS-DOS
|386 or later |386 or later
@ -73,16 +74,17 @@ and runs on all of them:
|what Windows 3.1 in 386 enhanced mode needs |what Windows 3.1 in 386 enhanced mode needs
|A Windows 3.x program: the hosted core with a backend written to the |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 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 indexed, so every palette is exact on a 256-colour display, and sprites
<<_windows_programs>>. compiled as on DOS. See <<_windows_programs>>.
|PSP |PSP
|MIPS Allegrex at 222 MHz |MIPS Allegrex at 222 MHz
|32 MB (PSP-1000) |32 MB (PSP-1000)
|PSP homebrew, for custom firmware and PPSSPP: the SDL3 port built with |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 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. d-pad, nub, and buttons are joystick 0; Start is Return, Select is Escape,
See <<_psp_programs>>. 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 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 |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. |Chunky 4bpp packed: two pixels per byte, high nibble is the left pixel.
|Commodore Amiga |Commodore Amiga
@ -126,7 +128,8 @@ that out of your program:
|Native planar, word-interleaved: four plane words per 16 pixels, per row. |Native planar, word-interleaved: four plane words per 16 pixels, per row.
|Sharp X68000 |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 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 === 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 * 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 * 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 * Keyboard (both game-style predicates and a typed-character queue), mouse
and joystick and joystick
* Four-channel Protracker `.MOD` music, a portable chip-tracker music format, * 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 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 with it has JoeyLib compiled in, so it carries JoeyLib's copyright and
permission notice, in its documentation for instance. A program also carries permission notice, in its documentation for instance. A program also carries
code from the libraries each port links (libxmp-lite for music on most code from the libraries each port links (PTPlayer for music on the Amiga,
ports, the toolchains' C libraries and start files), whose terms the toolchains' C libraries and start files), whose terms
`LICENSES.md` lists with what a shipped program owes them. On the Atari ST `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 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 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 <<_anatomy_of_a_joeylib_program>>, then <<_writing_portable_code>>. Those three
chapters are the whole programming model. 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. library can load.
<<_building_your_program>> explains how to get your own program, in a <<_building_your_program>> explains how to get your own program, in a

View file

@ -1,7 +1,7 @@
== Writing Portable Code == Writing Portable Code
A JoeyLib program that follows the rules in this chapter compiles and behaves 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 particular target cannot do something the others can, and the Apple IIgs is
usually that target. usually that target.
@ -13,7 +13,7 @@ usually that target.
. Compile your sprites before you draw them. . Compile your sprites before you draw them.
. Keep large objects out of function-local variables on the IIgs. . Keep large objects out of function-local variables on the IIgs.
. Use `jlRandom`, not `rand`, when identical behaviour across ports matters. . 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. . Do not mix save-under with clean-buffer erasing in the same screen region.
. Pick one of the audio layers per scene, not two. . Pick one of the audio layers per scene, not two.
@ -21,11 +21,12 @@ The rest of this chapter explains each one.
=== Memory === Memory
This is the rule that catches everyone. *The C heap on the IIgs is tiny.* It This is the rule that catches everyone. *The C heap on the IIgs is tiny.* The
lives in bank 0, and a single `malloc` is capped at roughly 32 KB -- often far whole heap is one Memory Manager block of 32 KB to 48 KB, and if that block
less in practice, because the clang libc heap on that port can be a few hundred cannot be had at start-up it is only what the program's link left in its
bytes. A program that `malloc`s a 32000-byte buffer works on four targets and bank, which can be a few hundred bytes. A program that `malloc`s a
fails on the reference one. 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 Use the library allocator, which is backed by each platform's native allocator
(the Memory Manager on the IIgs): (the Memory Manager on the IIgs):
@ -40,7 +41,10 @@ if (buffer == NULL) {
jlFree(buffer); 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 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 `memcpy` with a 32-bit count. On the IIgs both are `MVN` block moves at 7
@ -71,12 +75,11 @@ void renderScene(void) {
=== Timing === Timing
Covered in <<_frame_rate_independence>>, and worth repeating because it is the 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 second most common portability bug: the targets run at 50, 55, 60, and 70 Hz.
by `jlFrameHz()` or work from `jlMillisElapsed()`. Scale by `jlFrameHz()` or work from `jlMillisElapsed()`.
`jlFrameCount()` is a 16-bit counter, so you must poll it faster than twice the `jlFrameCount()` is a 16-bit counter that wraps roughly every 18 minutes at
frame rate or you will miss edges, and it wraps roughly every 18 minutes at 60 Hz, so compare two readings by subtracting them.
60 Hz.
=== Colour and the Palette === Colour and the Palette
@ -94,28 +97,31 @@ have both opaque and masked forms.
Two rules. Two rules.
*Compile before drawing.* `jlSpriteDraw` never compiles on demand. An *Compile before drawing.* `jlSpriteDraw` never compiles on demand. An
uncompiled sprite runs through the interpreter at roughly 1/40th the speed, and uncompiled sprite runs through the interpreter, two to six times slower
nothing warns you. depending on the machine, and nothing warns you.
*Align your backup buffers.* Give any `jlSpriteBackupT` backing array 2-byte *Align your backup buffers.* Give any `jlSpriteBackupT` backing array 2-byte
alignment, by declaring it `uint16_t` or with an alignment attribute. An odd alignment, by declaring it `uint16_t` or with an alignment attribute. On the
buffer address routes save and restore onto slow interpreted paths on the Amiga, ST, and X68000 an odd buffer address sends save and restore down a much
planar ports, because the 68000 cannot perform word accesses at odd addresses. slower path, and on the Amiga and ST it crashes the trusted variants.
[source,c] [source,c]
---- ----
// Right: 2-byte aligned by construction. // 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. // 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 Note also that the region a save-under actually captures is often not the
the *16-pixel-aligned window* containing the sprite, not the sprite rectangle. sprite rectangle: it can be the *16-pixel-aligned region* containing the
`jlSpriteSaveUnder` reports the true geometry in the backup structure's sprite (the Amiga, ST, and X68000), only the rows holding an opaque pixel (the
`x`, `width`, `height` and `sizeBytes` fields. It is that recorded window you IIgs), or only the visible part of a sprite partly off the surface (every
must not overwrite before restoring. 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 === Randomness
@ -142,19 +148,24 @@ you:
Pass bare names: `jlDataOpen("levels/title.dat", "rb")`, never Pass bare names: `jlDataOpen("levels/title.dat", "rb")`, never
`"DATA/levels/title.dat"`. On DVX the two are not in the current directory `"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 at all (<<_dvx_programs>>), so a program that opens `+"DATA/..."+` itself finds
nothing there. 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 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 8 characters plus a short extension, letters and digits only, starting with a
both ProDOS and DOS 8.3. Do not rely on case being preserved. 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 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. degrade gracefully, because on some volumes `SAVES/` simply is not writable.
=== Audio === 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: hardware:
[cols="2,5", options="header"] [cols="2,5", options="header"]
@ -162,41 +173,48 @@ hardware:
|Layer |What it is |Layer |What it is
|Module playback |Module playback
|`jlAudioPlayMod` -- four-channel Protracker `.MOD`, mixed in software on |`jlAudioPlayMod` -- four-channel Protracker `.MOD`, mixed in software by
DOS and ST, hardware-replayed on Amiga and IIgs. 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 |Chip tracker
|`jlMusicPlay` -- a portable event stream driving three tone voices plus |`jlMusicPlay` -- a portable event stream driving three tone voices plus
noise, at register-write cost with no CPU mixing. noise, at register-write cost with no CPU mixing.
|Voices and effects |Voices and effects
|`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx` and the streaming forms. |`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and the streaming forms.
|=== |===
[WARNING] [WARNING]
==== ====
On the Atari ST and Amiga, the tone voices *are* the same hardware the module On the Amiga the tone voices are the hardware the module player drives, and
player drives. While a `.MOD` is playing, `jlAudioVoice` writes fight the while a `.MOD` is playing, `jlAudioVoice` writes fight the player's register
player's register writes and lose. Choose the module layer or the voice and writes and lose. On the plain ST, digital sound takes the chip channel tone
tracker layer *per scene*, not both at once. (The STe build plays modules and voice 0 plays on, so voice 0 is silent while a module or effect plays. On the
sound effects through the STe's DMA sound chip instead, so there the voices IIgs the voices share sound effect slots 0 to 2 and the noise slot 3. Choose
keep the YM2149 to themselves -- but a program that also ships the ST build the module layer or the voice and tracker layer *per scene*, not both at once.
still has to follow the rule.)
==== ====
The chip tracker is the portable choice for music. It costs almost nothing on 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 every port -- unlike `.MOD` playback, which needs real CPU mixing where the
marginal on the ST. 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 A `jlAudioVoice` or `jlAudioNoise` note sounds until the next call for it, as
the tracker engine's DOC path) and behave as retriggered one-shots of about long as `jlAudioFrameTick` runs every frame: the IIgs plays each note as a
80 ms, so a note held longer must be re-asserted roughly once a frame. On DOS, short sample and restarts the held ones from there.
ST and Amiga those two calls are direct hardware programming and work without
`jlAudioInit`.
A failed `jlAudioInit` is *not* fatal. `jlInit` still succeeds and every audio `jlInit` starts audio (`jlConfigT.noAudio` leaves it off) and `jlShutdown`
call becomes a silent no-op, so a program need not treat missing sound as an stops it. Sound that does not start is *not* fatal: `jlInit` still succeeds,
error. 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 === 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 |Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |DVX |SDL3, Web, Win32, PSP, Haiku, BeOS |Win16
|Frame rate |Frame rate
|60 Hz |60 Hz (50 on a PAL machine)
|50 Hz (PAL) |50 Hz (PAL; 60 on an NTSC machine without PAL)
|60 Hz (NTSC) |50 or 60 Hz, as the machine is set
|70 Hz |70 Hz
|55 Hz (56 under MAME) |55 Hz (56 under MAME)
|70 Hz (virtual; the real retrace on the whole screen) |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
|planar, interleaved |planar, interleaved
|chunky 4bpp |chunky 4bpp
|text-plane planar |planar (shown through the text plane)
|chunky 4bpp |chunky 4bpp
|chunky 4bpp |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
|compiled |compiled
|interpreted |interpreted
|interpreted |compiled
|Stereo pan |Stereo pan
|all three |all three
|left and right |all three (centred on a free channel each side)
|ignored (mono) |ignored (mono)
|ignored (mono) |ignored (mono)
|ignored |ignored
@ -246,24 +264,36 @@ The API is uniform; these are the places where what you get differs.
|all three |all three
|ignored (mono) |ignored (mono)
|`jlAudioTone` |Program icon (<<_icons>>)
|DOC oscillator |the Finder's (`Icons` folder)
|Paula |Workbench's (`Name.info`)
|YM2149 |none
|PIT counter 2 |none
|- |none
|a DVX tone (FM chip or mixer) |Program Manager's
|voice 0 of the mixer |the window's (SDL3), the page's (web), Explorer's (Win32), the menu's
|voice 0 of the mixer (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 |Joystick
|analog, auto-disables |one, analog, auto-disables
|digital
|digital |digital
|digital |digital
|analog
|digital |digital
|analog (DVX calibrates) |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) |Windows' joysticks (Control Panel calibrates)
|Keyboard layout |Keyboard layout
@ -273,7 +303,7 @@ The API is uniform; these are the places where what you get differs.
|fixed US |fixed US
|machine's own |machine's own
|DVX's |DVX's
|the OS's; the PSP has no keyboard |the OS's; on the PSP, a US one on the screen
|Windows' |Windows'
|Serial ports |Serial ports
@ -281,28 +311,35 @@ The API is uniform; these are the places where what you get differs.
|`serial.device` |`serial.device`
|Modem port |Modem port
|COM1..COM4 |COM1..COM4
|- |RS-232C (needs RSDRV.SYS)
|COM1..COM4 (DVX's library) |COM1..COM4 (DVX's library)
|Win32: COM1 and up; the others - |Win32: COM1 and up; SDL3, web, PSP, Haiku, BeOS: none
|COM1..COM9 |COM1..COM9
|=== |===
The joystick row deserves a note: the IIgs port *auto-disables* joystick The joystick row deserves a note: the IIgs port stops polling for a stick
polling after a short window with no stick detected, because polling costs after about a second (60 polls) with none detected, because polling for an
about 3 ms per frame there. It does not re-probe on its own. If a user plugs a absent stick costs about 1 ms a frame there; `jlJoystickReset` starts it again
stick in while the program is running, you must call `jlJoystickReset` to (<<_jljoystickreset>>).
resume polling.
On the X68000 both joystick ports always report a stick, as on the ST: an 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 empty port cannot be told from a stick at rest. `JL_JOY_BUTTON_0` is a stick's
trigger A and `JOY_BUTTON_1` its trigger B. Holding both directions of an axis 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. 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: 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 the ST reports no stick movement there, so `JL_JOYSTICK_1` stays at rest whatever
is plugged in. Port 1's fire button is the same line as the mouse's right is plugged in. A stick there has one button, so a quick press is
button, so a press reads as both `JOY_BUTTON_0` and `MOUSE_BUTTON_RIGHT`. `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 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 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 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, 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 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 On DOS the game port's counts depend on the stick and on the CPU's speed, so a
the first reading after `jlInit` or `jlJoystickReset` is taken as the stick's stick's first reading -- after `jlInit`, after `jlJoystickReset`, or when it is
rest, and a push past half that value either way reads as a full push. Call plugged in -- is taken as its centre, and its axes are read in proportion to
`jlJoystickReset` with the stick centred if it was not at the start. 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 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 -- 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 `jlMillisElapsed` does not depend on it: it reads the machine's 100 Hz system
timer to the millisecond. timer to the millisecond.
Per-scanline palettes cost something on the X68000, and only there. Its Per-scanline palettes cost CPU time on the ST and the X68000, whose displays
display has one 16-colour palette, so the port reloads it as the beam scans: an have one 16-colour palette, and a stall on the Amiga when the bands move. The
interrupt stores the next band's colours in the horizontal blanking before the ST takes a timer interrupt at every change of palette, every frame, and copies
band's first line. A screen with one palette costs nothing and arms no the next band's 16 colours there. The X68000 reloads its palette as the beam
interrupt. Each change of palette between one line and the next costs about scans: an interrupt stores the next band's colours in the horizontal blanking
one line of CPU time (32 microseconds); a band more than two lines from the before the band's first line. A screen with one palette costs nothing on
one before costs about two. A screen that changes palette on every line either and arms no interrupt. On the X68000 each change of palette between one
therefore spends a third of each frame in that interrupt, during which serial line and the next costs about one line of CPU time (32 microseconds); a band
and mouse interrupts wait (see the serial chapter for how long a run can be more than two lines from the one before costs about two. A screen that changes
before received bytes are lost). Palette and SCB changes take effect at the palette on every line therefore spends a third of each frame in that
start of the next frame rather than mid-frame, so a fade can lag its pixels by interrupt, during which serial and mouse interrupts wait (see the serial
one frame but never tears. 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 === A Portability Checklist
@ -365,7 +625,7 @@ audience (or both):
* *The ST build* (`make atarist`, programs in `build/atarist/bin`) is the * *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 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. ST, at no cost for the STe's existence.
* *The STe build* (`make ste`, programs in `build/ste/bin`) is the same port * *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 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 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. 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 `./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 builds the STe variant with `make ste`, like any other port; it takes the ST's
baked assets. baked assets.
@ -420,7 +680,7 @@ expect less on memory-bound operations there.
=== DVX Programs === DVX Programs
DVX is a DOS desktop: a VESA graphics GUI with windows, cooperative 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 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 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 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 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 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-, 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. 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 * *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 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 does not tear. The frame count is the same clock in both, so switching
never makes it jump. never makes it jump.
* *Keys DVX keeps.* While the window is active, or the program has the whole * *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 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 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 Game menu has Full Screen, Help (the program's help file) and Exit. Typed
characters come through DVX's keyboard layout. characters come through DVX's keyboard layout.
* *Closing ends the program.* Alt+F4, the close box and Game > Exit end the * *Closing ends the program.* Alt+F4, the close box, and Game > Exit end the
program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent` or program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent`, or
`jlSerialPoll`, as `exit()` does anywhere: its `atexit` handlers run, then `jlSerialPoll`, as `exit()` does anywhere: its `atexit` handlers run, then
`jlShutdown`, and its log is closed. `jlShutdown`, and its log is closed.
* *Sharing the machine.* DVX multitasks cooperatively: the other programs, * *Sharing the machine.* DVX multitasks cooperatively: the other programs,
and this one's own window events, run only while it yields. `jlWaitVBL` 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 most once a frame. A loop that calls none of them freezes the desktop until
it ends. it ends.
* *Files.* DVX's programs share one current directory, so JoeyLib roots its * *Files.* DVX's programs share one current directory, so JoeyLib roots its
@ -518,32 +778,36 @@ this:
program presents. program presents.
* *Sprites are interpreted.* There is no code emitter for the host's CPU, so * *Sprites are interpreted.* There is no code emitter for the host's CPU, so
`jlSpriteCompile` returns false and every sprite is drawn by the `jlSpriteCompile` returns false and every sprite is drawn by the
interpreter -- far faster than any of the old machines all the same. interpreter -- far faster than any of the old machines all the same. (The
`joey/platform.h` defines `JOEYLIB_SPRITE_INTERPRETED`, for a program that 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`) 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. 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 * *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 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 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 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 gamepads or joysticks are joysticks 0 and 1, coming and going as they are
button (A on an Xbox pad) as `JOY_BUTTON_0` and the east button (B) as plugged in. A gamepad SDL knows how to map gives its left stick or its
`JOY_BUTTON_1`; they come and go as they are plugged in. 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 * *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. its `atexit` handlers run and the window and the sound are closed.
* *Files.* The program's own folder is the current directory from `jlInit` * *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. the program.
* *Sound* is mixed in software into 16-bit stereo at 44.1 kHz: `.MOD` music * *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 through the module player (as on DOS), Paula's channels hard left and
voices as square waves and the noise voice as the ST's YM2149 makes it. It 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`, 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 `jlAudioTickRegister` callback runs in program time too -- but between
exactly the samples it falls between, so its notes start in the right 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 place in the sound. Every sound needs audio started (the default), the
voices included. tone voices included.
* *No serial port.* * *No serial port.*
`make sdl3` builds every example as a native program, each in `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). `jlInputPoll` alone, as `jlWaitForAnyKey` is).
* *Sound starts with the first click or key press*, as browsers require; the * *Sound starts with the first click or key press*, as browsers require; the
page says so. page says so.
* *Saves persist.* `SAVES/` is kept in the browser's IndexedDB for the page's * *Saves persist.* `SAVES/` is kept in the browser's IndexedDB, read back
address, read back when the program starts and written back every two when the program starts and written back as each save changes, so a save
seconds, so a save outlives the page. 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 * *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. 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/`: * *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`) `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 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 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 not signed) and in PPSSPP. The ELF it is packed from stays in
`build/psp/elf/`, for `psp-addr2line`. `build/psp/elf/`, for `psp-addr2line`.
* *The screen.* The PSP's 480x272 screen shows the stage at 1:1 in its * *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 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. has no environment), and there is no full screen to switch to.
* *Its own controls, no keyboard.* The PSP's controls are joystick 0, * *Its own controls, and a keyboard on the screen.* The PSP's controls are
through SDL's gamepad mapping: the d-pad or the analog nub, cross as joystick 0, through SDL's gamepad mapping: the d-pad or the analog nub,
`JOY_BUTTON_0` and circle as `JOY_BUTTON_1`; joystick 1 is never there. cross as `JL_JOY_BUTTON_0` and circle as `JL_JOY_BUTTON_1`; joystick 1 is
Having no keyboard, the PSP also gives Start as the Return key and Select never there. Start is also the Return key and Select the Escape key, held
as the Escape key, held (`KEY_RETURN`, `KEY_ESCAPE`) and typed (`JL_KEY_RETURN`, `JL_KEY_ESCAPE`) and typed (`JL_CHAR_RETURN`,
(`JL_CHAR_RETURN`, `JL_CHAR_ESCAPE` from `jlInputGetChar`) as those keys `JL_CHAR_ESCAPE` from `jlInputGetChar`) as those keys are, so
are, so `jlWaitForAnyKey`, Return to go on and Escape to quit work. No `jlWaitForAnyKey`, Return to go on, and Escape to quit work at a press.
other key is ever down, nothing else is typed (the PSP's on-screen Square opens a PC keyboard drawn over the bottom of the screen, US layout,
keyboard is never opened), and there is no pointer to move. 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 * *HOME quits.* The HOME menu's Exit ends the program as closing a window
does. does.
* *Free space* (`jlDiskFree`) is that of the memory stick (or a PSP Go's * *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 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 -- 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 the same chunky one, so UBER's hashes are DOS's. Everything in
<<_sdl3_programs>> holds, except: <<_sdl3_programs>> holds, except:
* *Old Windows on purpose.* A program is a 32-bit `NAME.EXE` built by * *Old Windows on purpose.* A program is a 32-bit `NAME.EXE` built by
llvm-mingw against `MSVCRT.DLL` (the `win32` installer component), marked 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 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 came with Windows 95 OSR2 and Internet Explorer; a first-release Windows
95 needs it installed. 95 needs it installed.
* *One thread.* The window is made on the program's thread, so its messages * *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 and `jlStagePresent`; the sound goes out through waveOut buffers handed
over at the same calls. There are no callbacks and no locks. 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, * *The window.* The stage is stretched with `StretchBlt` to the stage's
4:3 or square (the same `JOEYLIB_SCALE`, `JOEYLIB_ASPECT` and shape, 4:3 or square (the same `JOEYLIB_SCALE`, `JOEYLIB_ASPECT`, and
`JOEYLIB_FULLSCREEN` settings), black around it; full screen is a `JOEYLIB_FULLSCREEN` settings), black around it; full screen is a
borderless window over the whole monitor, which leaves the display mode borderless window over the whole monitor, which leaves the display mode
alone. On a 256-colour desktop Windows maps every colour to the nearest 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 * *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 DOS), typed characters through the user's keyboard layout, and the first
two joysticks through `joyGetPosEx`, calibrated in the Control Panel. Keys 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/`, `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 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. 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. `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 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 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 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 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 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. 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 * *Exact colours.* The frame is 8-bit, each pixel one entry of a 256-entry
holds all 16 palettes and is realized as a logical palette whenever a frame colour table holding all 16 palettes, which on a 256-colour display is a
is shown, so on a 256-colour display every colour is exact, SCB palettes logical palette too, so every colour is exact, SCB palettes included, as
included. `JOEYLIB_SCALE` defaults to 2, and the settings are read from the far as the display has room: Windows keeps 20 of a 256-colour display's
DOS environment Windows started from (`joey-run` puts its own there). entries for itself, so with more than 14 palettes in use and more than 236
Under DOSBox, Alt+Enter is DOSBox's own full-screen key; use Alt with the 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. keypad's Enter.
* *Sound* is 8-bit mono at 22.05 kHz through waveOut, what a Sound Blaster * *Speed on a 386.* Sprites are compiled to x86 code, as on DOS. The port
and a 386 handle well; stereo pan is ignored. 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 * *Joysticks* are the first two MMSYSTEM sticks, through `joyGetPos`, with
their buttons 1 and 2. Windows 3.x reads sticks only through a joystick 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 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 `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 `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 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 up from your own install disc); the gates run there too, among them
on a real 386 here. `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 === Haiku and BeOS Programs
The Be API ports are the hosted core with a backend written to the Be API 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 (`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), 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 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). 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 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 * *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 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`, 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`. `BSoundPlayer`.
* *The window* shows the stage from a bitmap the app_server scales without * *The window* shows the stage scaled without filtering, at the stage's
filtering, at the stage's shape, 4:3 or square, black around it (the same shape, 4:3 or square, black around it (the same `JOEYLIB_SCALE`,
`JOEYLIB_SCALE`, `JOEYLIB_ASPECT` and `JOEYLIB_FULLSCREEN` settings; `JOEYLIB_ASPECT`, and `JOEYLIB_FULLSCREEN` settings; `JOEYLIB_SCALE`
`JOEYLIB_SCALE` defaults to 3). Alt+Enter switches between the window and 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. the whole screen. The pointer is hidden over the window.
* *Keys* by raw key code, the key's place on the keyboard whatever the * *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 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. two axes and buttons.
* *Files.* The program's folder is the current directory from the moment * *Files.* The program's folder is the current directory from the moment
the window opens. 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.* * *No serial port.*
* *BeOS R5's loader* refuses any program with a dynamic tag it does not * *BeOS R5's loader* refuses any program with a dynamic tag it does not
know, so `make beos` links without combined relocations (`-z know, so `make beos` links without combined relocations (`-z
nocombreloc`). Build for R5 with `make beos`, not `make haiku32`. 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 `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 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 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 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 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`, 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 `scripts/verify-be-display.sh`, `scripts/verify-be-icon.sh`, and
here. `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; jlConfigT config;
config.codegenBytes = 32UL * 1024; // sprite blitter cache config.codegenBytes = 32UL * 1024; // sprite blitter cache
config.noAudio = false; // jlInit starts audio too
if (!jlInit(&config)) { if (!jlInit(&config)) {
// jlLastError() explains why. There is no display yet, so on a // 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 native sprite blitter code into. Getting it wrong is the most common cause of
sprites drawing slowly, so it is worth understanding. sprites drawing slowly, so it is worth understanding.
* `0` selects a per-platform default: 8 KB on the IIgs, 32 KB on * `0` selects a per-platform default: 8 KB on the IIgs, 64 KB on the
Amiga, ST, DOS and DVX. SDL3 interprets every sprite and never uses it. 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 * 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 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. those ports. A 32x32 sprite can need 8 to 16 KB there.
* Measure, do not guess: after compiling a sprite, `jlSpriteCompiledSize()` * Measure, do not guess: after compiling a sprite, `jlSpriteCompiledSize()`
reports its exact cost on the current platform. 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 * On the IIgs a compiled sprite cannot cross a 64 KB bank boundary, so the
bank boundary, so requests above 65536 are clamped to 65536 -- and even a arena is up to four Memory Manager blocks of at most 64 KB, each inside one
clamped request can fail if no free bank exists. 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 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 interpreter that is *two to six times slower*, depending on the machine.
error, so a program that never checks can be silently crawling. 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] [source,c]
---- ----
@ -54,10 +60,10 @@ Set up colour before drawing:
[source,c] [source,c]
---- ----
uint16_t colors[SURFACE_COLORS_PER_PALETTE]; uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
// ... fill in 16 entries of $0RGB ... // ... fill in 16 entries of $0RGB ...
jlPaletteSet(stage, 0, colors); 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 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] [source,c]
---- ----
jlSpriteT *cels[16]; 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 count = jlSpriteBankLoad("player.spr", cels, 16, palette);
uint16_t i; uint16_t i;
for (i = 0; i < count; 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] [IMPORTANT]
==== ====
`jlSpriteDraw` does *not* compile a sprite on demand. An uncompiled sprite `jlSpriteDraw` does *not* compile a sprite on demand. An uncompiled sprite
draws through the interpreter every single time. Call `jlSpriteCompile` or draws through the interpreter every single time. Call `jlSpriteCompile` during
`jlSpritePrewarm` during loading -- otherwise the first draw of each sprite loading -- otherwise the first draw of each sprite either stalls a frame (if
either stalls a frame (if you compile lazily yourself) or never gets fast at you compile lazily yourself) or never gets fast at all.
all.
==== ====
=== Part 3: The Frame Loop === Part 3: The Frame Loop
@ -100,7 +105,7 @@ while (running) {
jlInputPoll(); // refresh keyboard, mouse, joystick jlInputPoll(); // refresh keyboard, mouse, joystick
jlAudioFrameTick(); // pump the audio engine jlAudioFrameTick(); // pump the audio engine
if (jlKeyPressed(KEY_ESCAPE)) { if (jlKeyPressed(JL_KEY_ESCAPE)) {
running = false; running = false;
} }
@ -113,73 +118,68 @@ while (running) {
Four library calls, in that order, and everything else is yours. Four library calls, in that order, and everything else is yours.
`jlWaitVBL()` blocks until the next vertical blank. It is cheap on every port `jlWaitVBL()` blocks until the next vertical blank. On the machine ports that
because the underlying mechanism is always a hardware wait, never a software is the display's own; the hosted ports, and DVX in a window, have none, and
timer. wait for the next tick of a virtual frame clock instead.
`jlInputPoll()` must be called exactly once per frame. The edge predicates `jlInputPoll()` must be called exactly once per frame. The edge predicates
(`jlKeyPressed`, `jlKeyReleased`, and the mouse and joystick equivalents) are (`jlKeyPressed`, `jlKeyReleased`, and the mouse and joystick equivalents) are
defined relative to the *previous poll*, so calling it twice per frame makes defined relative to the *previous poll*, so calling it twice per frame makes
edges disappear, and not calling it means nothing ever changes. edges disappear, and not calling it means nothing ever changes.
`jlAudioFrameTick()` is a no-op on ports whose audio runs from a hardware `jlAudioFrameTick()` advances tracker music and keeps streaming sound effects
interrupt, but it is required on ports that mix in user context, and it is what fed. Call it once per frame.
advances tracker music and keeps streaming sound effects fed. Call it
unconditionally, once per frame.
`jlStagePresent()` flips the dirty parts of the stage to the display. The `jlStagePresent()` flips the dirty parts of the stage to the display. The
drawing primitives record which rows they touched, so presenting only copies drawing primitives record which rows they touched, so presenting only copies
what actually changed. You do not need to tell it what you drew. 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
[NOTE] write them. Palette and SCB changes reach the display at the next present
==== too.
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.
====
==== Frame-Rate Independence ==== Frame-Rate Independence
The targets do not share a refresh rate: 50 Hz (Amiga PAL), 55 Hz The targets do not share a refresh rate: 50 Hz (the Amiga, and a PAL IIgs or
(X68000), 60 Hz (IIgs, ST NTSC, and SDL3's virtual rate) and 70 Hz (VGA ST), 55 Hz (X68000), 60 Hz (the IIgs, an NTSC ST, and every hosted port's
mode 13h, and DVX's virtual rate). A loop 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 that moves a sprite "two pixels per frame" runs 40 percent faster on DOS than
on a PAL Amiga. 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
[source,c] 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:
uint16_t hz = jlFrameHz(); // 50, 60 or 70
----
Or drive from the millisecond clock, which is the more robust choice for
anything that must feel the same everywhere:
[source,c] [source,c]
---- ----
uint32_t now = jlMillisElapsed(); uint32_t now = jlMillisElapsed();
uint32_t step = now - previous; uint32_t elapsed = now - gLastMillis; // milliseconds since the last frame
previous = now;
gLastMillis = now;
---- ----
`jlMillisElapsed()` reports true elapsed wall-clock time, so a frame that took A thing then moves its speed times `elapsed` / 1000. At 70 frames a second
a long time to render is accounted for correctly rather than counted as one that is often a fraction of a pixel, which only adds up if positions keep the
tick. fraction, so keep them in fixed point (the skeleton below uses 1/256 of a
pixel) and draw at the whole-pixel part.
`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 === Part 4: Shut Down
[source,c] [source,c]
---- ----
jlAudioShutdown(); // if you called jlAudioInit
jlShutdown(); jlShutdown();
return 0; return 0;
---- ----
`jlShutdown` restores the display mode, frees the library's allocations, and `jlShutdown` stops audio, restores the display mode, frees the library's allocations, and
un-hooks interrupt vectors. Sprites and surfaces you created are released with un-hooks interrupt vectors. Sprites and surfaces you created are *not*
it, but the buffers *you* allocated (including tile data you handed to released with it: destroy them with `jlSpriteDestroy` and `jlSurfaceDestroy`
`jlSpriteCreate`, which is never copied) remain yours to free. 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 === The Complete Skeleton
@ -189,25 +189,33 @@ This compiles and runs on every target as-is.
---- ----
// A minimal but complete JoeyLib program. // A minimal but complete JoeyLib program.
#include <stddef.h>
#include <joey/joey.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 jlSurfaceT *gStage;
static bool gRunning = true; static bool gRunning = true;
static int16_t gPlayerX = 160; static int32_t gPlayerX = 160L << SUBPIXEL_SHIFT;
static int16_t gPlayerY = 100; static int32_t gPlayerY = 100L << SUBPIXEL_SHIFT;
static uint32_t gLastMillis = 0;
static void buildPalette(void); static void buildPalette(void);
static void handleInput(void); static int32_t clampPosition(int32_t position, int16_t limit);
static void handleInput(int32_t step);
static void renderFrame(void); static void renderFrame(void);
static int32_t stepForFrame(void);
static void buildPalette(void) { static void buildPalette(void) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE]; uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint8_t i; 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[i] = 0x000;
} }
colors[1] = 0xFFF; // white colors[1] = 0xFFF; // white
@ -215,58 +223,77 @@ static void buildPalette(void) {
colors[3] = 0x00C; // blue colors[3] = 0x00C; // blue
jlPaletteSet(gStage, 0, colors); jlPaletteSet(gStage, 0, colors);
jlScbSetRange(gStage, 0, SURFACE_HEIGHT - 1, 0); jlScbSetRange(gStage, 0, JL_SURFACE_HEIGHT - 1, 0);
} }
static void handleInput(void) { // A position (in 1/256 of a pixel) kept between 0 and limit pixels.
if (jlKeyPressed(KEY_ESCAPE)) { 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; gRunning = false;
} }
if (jlKeyDown(KEY_LEFT)) { if (jlKeyDown(JL_KEY_LEFT)) {
gPlayerX -= PLAYER_SPEED; gPlayerX -= step;
} }
if (jlKeyDown(KEY_RIGHT)) { if (jlKeyDown(JL_KEY_RIGHT)) {
gPlayerX += PLAYER_SPEED; gPlayerX += step;
} }
if (jlKeyDown(KEY_UP)) { if (jlKeyDown(JL_KEY_UP)) {
gPlayerY -= PLAYER_SPEED; gPlayerY -= step;
} }
if (jlKeyDown(KEY_DOWN)) { if (jlKeyDown(JL_KEY_DOWN)) {
gPlayerY += PLAYER_SPEED; gPlayerY += step;
} }
// A joystick, if one is attached, drives the same movement. // A joystick, if one is attached, drives the same movement.
if (jlJoystickConnected(JOYSTICK_0)) { if (jlJoystickConnected(JL_JOYSTICK_0)) {
if (jlJoystickX(JOYSTICK_0) < -64) { if (jlJoystickX(JL_JOYSTICK_0) < -64) {
gPlayerX -= PLAYER_SPEED; gPlayerX -= step;
} }
if (jlJoystickX(JOYSTICK_0) > 64) { if (jlJoystickX(JL_JOYSTICK_0) > 64) {
gPlayerX += PLAYER_SPEED; gPlayerX += step;
} }
} }
// Keep the player on screen: the primitives would clip safely, but // Keep the player on screen: the primitives would clip safely, but
// clamping keeps the object whole. // clamping keeps the object whole.
if (gPlayerX < 0) { gPlayerX = clampPosition(gPlayerX, JL_SURFACE_WIDTH - PLAYER_SIZE);
gPlayerX = 0; gPlayerY = clampPosition(gPlayerY, JL_SURFACE_HEIGHT - PLAYER_SIZE);
}
if (gPlayerX > SURFACE_WIDTH - 16) {
gPlayerX = SURFACE_WIDTH - 16;
}
if (gPlayerY < 0) {
gPlayerY = 0;
}
if (gPlayerY > SURFACE_HEIGHT - 16) {
gPlayerY = SURFACE_HEIGHT - 16;
}
} }
static void renderFrame(void) { static void renderFrame(void) {
jlSurfaceClear(gStage, 0); jlSurfaceClear(gStage, 0);
jlDrawRect(gStage, 0, 0, SURFACE_WIDTH, SURFACE_HEIGHT, 1); jlDrawRect(gStage, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, 1);
jlFillRect(gStage, gPlayerX, gPlayerY, 16, 16, 2); 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; jlConfigT config;
config.codegenBytes = 32UL * 1024; config.codegenBytes = 32UL * 1024;
config.noAudio = false;
if (!jlInit(&config)) { if (!jlInit(&config)) {
return 1; return 1;
@ -287,13 +315,14 @@ int main(void) {
buildPalette(); buildPalette();
jlRandomSeed(12345); // deterministic on every port jlRandomSeed(12345); // deterministic on every port
gLastMillis = jlMillisElapsed();
while (gRunning) { while (gRunning) {
jlWaitVBL(); jlWaitVBL();
jlInputPoll(); jlInputPoll();
jlAudioFrameTick(); jlAudioFrameTick();
handleInput(); handleInput(stepForFrame());
renderFrame(); renderFrame();
jlStagePresent(); jlStagePresent();
@ -315,7 +344,7 @@ Before drawing a sprite, capture what is underneath; next frame, put it back.
[source,c] [source,c]
---- ----
uint16_t backing[JOEY_SPRITE_BACKUP_BYTES(2, 2) / 2]; uint16_t backing[jlSpriteBackupLength(uint16_t, 2, 2)];
jlSpriteBackupT backup; jlSpriteBackupT backup;
backup.bytes = (uint8_t *)backing; backup.bytes = (uint8_t *)backing;
@ -326,10 +355,10 @@ jlSpriteRestoreUnder(stage, &backup); // erase last frame's sprite
jlSpriteSaveAndDraw(stage, sprite, x, y, &backup); jlSpriteSaveAndDraw(stage, sprite, x, y, &backup);
---- ----
Size the buffer with the `JOEY_SPRITE_BACKUP_BYTES` macro, and declare it as Size the buffer with `jlSpriteBackupLength`, and declare it as `uint16_t` (or
`uint16_t` (or otherwise 2-byte aligned) -- an odd address pushes save and otherwise 2-byte aligned): on the Amiga, ST, and X68000 an odd address sends
restore onto slow interpreted paths on the planar ports, because the 68000 save and restore down a much slower path, and on the Amiga and ST it crashes
cannot do word accesses at odd addresses. the trusted variants.
==== The Clean-Buffer Pattern ==== The Clean-Buffer Pattern
@ -341,12 +370,12 @@ jlSurfaceT *clean = jlSurfaceCreate();
// ... draw the static scene into clean, once ... // ... draw the static scene into clean, once ...
// each frame: // 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 ... // ... then draw ALL sprites ...
---- ----
This scales better with many sprites, and it is the right choice when 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. erase and draw.
[WARNING] [WARNING]
@ -354,6 +383,6 @@ erase and draw.
`jlSurfaceCopyRect` widens the copied region outward to the 16-pixel group grid `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 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, 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. 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 // 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. // X68000 audio gates, which read the sound chips but cannot type.
// //
// On platforms where the audio HAL is still a stub, jlAudioInit // On a machine whose sound does not start (jlInit logs it), every audio
// returns false, every audio call is a quiet no-op, and the demo runs // call is a quiet no-op and the demo runs as a silent input loop -- you
// as a silent input loop -- you can confirm the build links and the // can confirm the build links and the input plumbing still works without
// input plumbing still works without hearing anything. // hearing anything.
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -101,7 +101,7 @@
#define BAR_X 16 #define BAR_X 16
#define BAR_Y 88 #define BAR_Y 88
#define BAR_W (SURFACE_WIDTH - 32) #define BAR_W (JL_SURFACE_WIDTH - 32)
#define BAR_H 16 #define BAR_H 16
// The streamed tone's state, which streamFill advances. // The streamed tone's state, which streamFill advances.
@ -113,7 +113,7 @@ typedef struct {
static void buildPalette(jlSurfaceT *screen); 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 keyPressed(jlKeyE key, uint16_t frame);
static bool loadFile(const char *name, uint32_t maxLen, uint8_t **outBytes, uint32_t *outLen); static bool loadFile(const char *name, uint32_t maxLen, uint8_t **outBytes, uint32_t *outLen);
static bool noiseStep(uint16_t sweepFrame); static bool noiseStep(uint16_t sweepFrame);
@ -129,10 +129,10 @@ static uint8_t gStreamTake = 0;
static void buildPalette(jlSurfaceT *screen) { static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE]; uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i; 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[i] = 0x0000;
} }
colors[COLOR_BG] = 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 // Visual feedback for "audio is running, but you cannot tell on a
// stub HAL": pulse a horizontal bar between the hint color and the // stub HAL": pulse a horizontal bar between the hint color and the
// active color whenever a sound has fired this frame. // 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); jlSurfaceClear(screen, COLOR_BG);
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT);
audioOk ? COLOR_HINT : COLOR_BG);
jlStagePresent(); jlStagePresent();
} }
@ -226,17 +225,17 @@ static bool noiseStep(uint16_t sweepFrame) {
// so a gate finds the noise sweep and a stream running. // so a gate finds the noise sweep and a stream running.
static jlKeyE probeKey(uint16_t frame) { static jlKeyE probeKey(uint16_t frame) {
if (frame == PROBE_NOISE_FRAME) { if (frame == PROBE_NOISE_FRAME) {
return KEY_N; return JL_KEY_N;
} }
if (frame % PROBE_STREAM_FRAMES == PROBE_STREAM_FIRST) { if (frame % PROBE_STREAM_FRAMES == PROBE_STREAM_FIRST) {
return KEY_S; return JL_KEY_S;
} }
return KEY_NONE; return JL_KEY_NONE;
} }
#else #else
static jlKeyE probeKey(uint16_t frame) { static jlKeyE probeKey(uint16_t frame) {
(void)frame; (void)frame;
return KEY_NONE; return JL_KEY_NONE;
} }
#endif #endif
@ -306,7 +305,6 @@ static void streamStart(void) {
int main(void) { int main(void) {
jlConfigT config; jlConfigT config;
jlSurfaceT *screen; jlSurfaceT *screen;
bool audioOk;
bool musicPaused; bool musicPaused;
bool noiseOn; bool noiseOn;
bool noiseResumes; bool noiseResumes;
@ -319,6 +317,7 @@ int main(void) {
uint32_t sfxLen; uint32_t sfxLen;
config.codegenBytes = 8 * 1024; config.codegenBytes = 8 * 1024;
config.noAudio = false;
if (!jlInit(&config)) { if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError()); fprintf(stderr, "jlInit failed: %s\n", jlLastError());
@ -337,19 +336,16 @@ int main(void) {
modLen = 0; modLen = 0;
sfxLen = 0; sfxLen = 0;
audioOk = jlAudioInit();
if (audioOk) {
// Keep the MOD bytes resident so M can (re)start it later; // Keep the MOD bytes resident so M can (re)start it later;
// jlAudioPlayMod copies them at play time. // jlAudioPlayMod copies them at play time.
(void)loadFile(TEST_MOD_NAME, DATA_FILE_MAX, &modBytes, &modLen); (void)loadFile(TEST_MOD_NAME, DATA_FILE_MAX, &modBytes, &modLen);
(void)loadFile(TEST_SFX_NAME, DATA_FILE_MAX, &sfxBytes, &sfxLen); (void)loadFile(TEST_SFX_NAME, DATA_FILE_MAX, &sfxBytes, &sfxLen);
gRawFormat = jlAudioSfxRawFormat(); gRawFormat = jlAudioSfxRawFormat();
}
jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true); jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true);
buildPalette(screen); buildPalette(screen);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
initialPaint(screen, audioOk); initialPaint(screen);
musicPaused = false; musicPaused = false;
noiseOn = false; noiseOn = false;
@ -360,18 +356,18 @@ int main(void) {
jlWaitVBL(); jlWaitVBL();
jlInputPoll(); jlInputPoll();
jlAudioFrameTick(); jlAudioFrameTick();
if (keyPressed(KEY_ESCAPE, frame)) { if (keyPressed(JL_KEY_ESCAPE, frame)) {
break; break;
} }
if (keyPressed(KEY_SPACE, frame) && sfxBytes != NULL) { if (keyPressed(JL_KEY_SPACE, frame) && sfxBytes != NULL) {
jlAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ); jlAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ);
flashFrames = FLASH_FRAMES; flashFrames = FLASH_FRAMES;
} }
if (keyPressed(KEY_S, frame) && audioOk) { if (keyPressed(JL_KEY_S, frame)) {
streamStart(); streamStart();
flashFrames = FLASH_FRAMES; 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 // The song's own drums would fight the sweep for the noise
// generator, so it pauses and picks up again afterwards. // generator, so it pauses and picks up again afterwards.
noiseResumes = jlMusicIsPlaying() && !musicPaused; noiseResumes = jlMusicIsPlaying() && !musicPaused;
@ -379,15 +375,15 @@ int main(void) {
noiseOn = true; noiseOn = true;
noiseFrame = 0; noiseFrame = 0;
} }
if (keyPressed(KEY_M, frame) && modBytes != NULL) { if (keyPressed(JL_KEY_M, frame) && modBytes != NULL) {
jlMusicStop(); jlMusicStop();
jlAudioPlayMod(modBytes, modLen, true); jlAudioPlayMod(modBytes, modLen, true);
} }
if (keyPressed(KEY_T, frame)) { if (keyPressed(JL_KEY_T, frame)) {
jlAudioStopMod(); jlAudioStopMod();
jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true); jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true);
} }
if (keyPressed(KEY_P, frame)) { if (keyPressed(JL_KEY_P, frame)) {
if (musicPaused) { if (musicPaused) {
jlMusicResume(); jlMusicResume();
} else { } else {
@ -416,10 +412,7 @@ int main(void) {
} }
jlMusicStop(); jlMusicStop();
if (audioOk) {
jlAudioStopMod(); jlAudioStopMod();
}
jlAudioShutdown();
if (modBytes != NULL) { if (modBytes != NULL) {
free(modBytes); 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) { 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. // 16 distinct $0RGB entries. Index 0 is forced to black anyway.
colors[0] = 0x000; colors[0] = 0x000;
@ -67,7 +67,7 @@ static void buildPalette(jlSurfaceT *screen) {
colors[14] = 0x088; colors[14] = 0x088;
colors[15] = 0x808; colors[15] = 0x808;
jlPaletteSet(screen, 0, colors); 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 // Pixel row: 14 single-pixel writes at consecutive x to exercise
// both odd and even nibble paths. // both odd and even nibble paths.
for (i = 0; i < 14; i++) { for (i = 0; i < 14; i++) {
jlDrawPixel(screen, (int16_t)(10 + i * 10), (int16_t)(CELL_H - 6), jlDrawPixel(screen, (int16_t)(10 + i * 10), (int16_t)(CELL_H - 6), (uint8_t)((i + 1) & 0x0F));
(uint8_t)((i + 1) & 0x0F));
} }
} }
@ -202,8 +201,7 @@ static void drawPrimitivesTileFlood(jlSurfaceT *screen) {
jlTilePaste(screen, (uint8_t)(bx + 4), (uint8_t)by, &snapBuf); jlTilePaste(screen, (uint8_t)(bx + 4), (uint8_t)by, &snapBuf);
// jlTileCopy from the green quadrant onto a fresh location below. // jlTileCopy from the green quadrant onto a fresh location below.
jlTileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1), jlTileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1), screen, (uint8_t)(bx + 1), (uint8_t)by);
screen, (uint8_t)(bx + 1), (uint8_t)by);
// jlTileCopyMasked test: build a "transparent" striped pattern at // jlTileCopyMasked test: build a "transparent" striped pattern at
// (208, 132). The tile's source has color 0 in alternating // (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) // Build a vertical-stripe source at (240, 132): col-pixel = (px % 2 ? color : 0)
for (i = 0; i < 8; i++) { for (i = 0; i < 8; i++) {
for (px = 0; px < 16; px++) { for (px = 0; px < 16; px++) {
jlDrawPixel(screen, (int16_t)(ox + 112 + px), (int16_t)(oy + 32 + i), jlDrawPixel(screen, (int16_t)(ox + 112 + px), (int16_t)(oy + 32 + i), (uint8_t)((px & 1) ? C_MAGENTA : 0));
(uint8_t)((px & 1) ? C_MAGENTA : 0));
} }
} }
// jlTileCopyMasked: source at block (ox+112)/8 = 34..35, by 16 // jlTileCopyMasked: source at block (ox+112)/8 = 34..35, by 16
// -> dst at backdrop block (ox+80)/8 = 30..31, 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), jlTileCopyMasked(screen, (uint8_t)((ox + 80) / 8), (uint8_t)((oy + 32) / 8), screen, (uint8_t)((ox + 112) / 8), (uint8_t)((oy + 32) / 8), 0);
screen, (uint8_t)((ox + 112) / 8), (uint8_t)((oy + 32) / 8),
0);
// Flood-fill region: a small bordered rectangle in the cell's // Flood-fill region: a small bordered rectangle in the cell's
// lower portion. Outline drawn in C_BORDER; jlFloodFillBounded // lower portion. Outline drawn in C_BORDER; jlFloodFillBounded
// from a point inside should fill with C_CYAN, stopping at the // from a point inside should fill with C_CYAN, stopping at the
// border. // border.
jlDrawRect(screen, ox + 16, oy + 60, 64, 32, C_BORDER); jlDrawRect(screen, ox + 16, oy + 60, 64, 32, C_BORDER);
jlFloodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76), jlFloodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76), C_CYAN, C_BORDER);
C_CYAN, C_BORDER);
// Plain jlFloodFill: solid block then re-fill to a new color. // Plain jlFloodFill: solid block then re-fill to a new color.
jlFillRect(screen, ox + 96, oy + 60, 48, 32, C_GREEN); jlFillRect(screen, ox + 96, oy + 60, 48, 32, C_GREEN);
@ -247,6 +241,7 @@ int main(void) {
jlSurfaceT *screen; jlSurfaceT *screen;
config.codegenBytes = 8 * 1024; config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) { if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError()); fprintf(stderr, "jlInit failed: %s\n", jlLastError());

View file

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

View file

@ -51,7 +51,7 @@
typedef struct { typedef struct {
int16_t dotX; int16_t dotX;
int16_t dotY; int16_t dotY;
bool btn[JOY_BUTTON_COUNT]; bool btn[JL_JOY_BUTTON_COUNT];
bool connected; bool connected;
bool valid; bool valid;
} StickViewT; } StickViewT;
@ -65,14 +65,14 @@ static void drawFrame(jlSurfaceT *screen, int16_t left);
static void initialPaint(jlSurfaceT *screen); static void initialPaint(jlSurfaceT *screen);
static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left); 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) { static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE]; uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i; 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[i] = 0x0000;
} }
colors[COLOR_BACKGROUND] = 0x0000; colors[COLOR_BACKGROUND] = 0x0000;
@ -104,7 +104,7 @@ static int16_t dotXFor(int16_t left, int8_t ax) {
int32_t off; int32_t off;
cx = left + FRAME_SIZE / 2 - DOT_SIZE / 2; 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); return (int16_t)(cx + off);
} }
@ -114,7 +114,7 @@ static int16_t dotYFor(int8_t ay) {
int32_t off; int32_t off;
cy = STICK_TOP + FRAME_SIZE / 2 - DOT_SIZE / 2; 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); return (int16_t)(cy + off);
} }
@ -142,7 +142,7 @@ static void initialPaint(jlSurfaceT *screen) {
int16_t left; int16_t left;
jlSurfaceClear(screen, COLOR_BACKGROUND); 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; left = (i == 0) ? STICK0_LEFT : STICK1_LEFT;
drawFrame(screen, left); drawFrame(screen, left);
gView[i].valid = false; gView[i].valid = false;
@ -160,7 +160,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
int8_t ay; int8_t ay;
int16_t newDotX; int16_t newDotX;
int16_t newDotY; int16_t newDotY;
bool newBtn[JOY_BUTTON_COUNT]; bool newBtn[JL_JOY_BUTTON_COUNT];
bool connected; bool connected;
int16_t i; int16_t i;
int16_t bx; int16_t bx;
@ -172,7 +172,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
ay = jlJoystickY(js); ay = jlJoystickY(js);
newDotX = dotXFor(left, ax); newDotX = dotXFor(left, ax);
newDotY = dotYFor(ay); 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); newBtn[i] = jlJoyDown(js, (jlJoyButtonE)i);
} }
@ -195,7 +195,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) {
// Button squares. // Button squares.
by = BUTTON_TOP; 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]) { if (v->valid && v->btn[i] == newBtn[i]) {
continue; continue;
} }
@ -213,6 +213,7 @@ int main(void) {
jlSurfaceT *screen; jlSurfaceT *screen;
config.codegenBytes = CODEGEN_BYTES; config.codegenBytes = CODEGEN_BYTES;
config.noAudio = true;
if (!jlInit(&config)) { if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError()); fprintf(stderr, "jlInit failed: %s\n", jlLastError());
@ -227,17 +228,17 @@ int main(void) {
} }
buildPalette(screen); buildPalette(screen);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
initialPaint(screen); initialPaint(screen);
jlInputPoll(); jlInputPoll();
for (;;) { for (;;) {
jlInputPoll(); jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) { if (jlKeyPressed(JL_KEY_ESCAPE)) {
break; break;
} }
updateStick(screen, JOYSTICK_0, STICK0_LEFT); updateStick(screen, JL_JOYSTICK_0, STICK0_LEFT);
updateStick(screen, JOYSTICK_1, STICK1_LEFT); updateStick(screen, JL_JOYSTICK_1, STICK1_LEFT);
} }
jlShutdown(); 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 updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow);
static void updateMouseVerifyRow(jlSurfaceT *screen); 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 // resembles a real keyboard (top number row, then QWERTY rows, then a
// cluster of modifiers / arrows / function keys). // cluster of modifiers / arrows / function keys).
static const jlKeyE gKeyGrid[GRID_ROWS][GRID_COLS] = { 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 }, { 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 },
{ KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P }, { 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 },
{ KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_BACKSPACE }, { 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 },
{ KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_LSHIFT, KEY_RSHIFT, KEY_TAB }, { 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 },
{ KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_LCTRL, KEY_LALT, KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_NONE }, { 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 },
{ 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 }
}; };
static bool gCellLit[GRID_ROWS][GRID_COLS]; static bool gCellLit[GRID_ROWS][GRID_COLS];
@ -224,7 +224,7 @@ static const uint8_t kFontGlyph[][FONT_H] = {
static void buildPalette(jlSurfaceT *screen) { static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE]; uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i; uint16_t i;
// Every entry gets a DISTINCT color (gray ramp for the unused // 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 // screenshot-based harness can decode the verification row's
// nibble pixels back to values on ports without direct // nibble pixels back to values on ports without direct
// framebuffer reads. // 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[i] = (uint16_t)((i << 8) | (i << 4) | i);
} }
colors[COLOR_BACKGROUND] = 0x0000; // black 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; jlKeyE key;
key = gKeyGrid[row][col]; key = gKeyGrid[row][col];
if (key == KEY_NONE) { if (key == JL_KEY_NONE) {
return false; return false;
} }
if (jlKeyDown(key)) { if (jlKeyDown(key)) {
return true; return true;
} }
if (col == cursorCol && row == cursorRow && jlMouseDown(MOUSE_BUTTON_LEFT)) { if (col == cursorCol && row == cursorRow && jlMouseDown(JL_MOUSE_BUTTON_LEFT)) {
return true; return true;
} }
return false; 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); cx = (int16_t)(cx + FONT_W + 1);
if (cx > SURFACE_WIDTH - FONT_W) { if (cx > JL_SURFACE_WIDTH - FONT_W) {
return; return;
} }
} }
@ -372,23 +372,57 @@ static void drawVerifyRow(jlSurfaceT *screen) {
static int glyphIdx(char c) { static int glyphIdx(char c) {
if (c == ' ') { return GLYPH_SPACE; } if (c == ' ') {
if (c >= '0' && c <= '9') { return GLYPH_DIGIT_BASE + (c - '0'); } return GLYPH_SPACE;
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 >= '0' && c <= '9') {
if (c == '.') { return GLYPH_PERIOD; } return GLYPH_DIGIT_BASE + (c - '0');
if (c == ',') { return GLYPH_COMMA; } }
if (c == ':') { return GLYPH_COLON; } if (c >= 'A' && c <= 'Z') {
if (c == '!') { return GLYPH_BANG; } return GLYPH_ALPHA_BASE + (c - 'A');
if (c == '?') { return GLYPH_QUESTION; } }
if (c == '-') { return GLYPH_MINUS; } if (c >= 'a' && c <= 'z') {
if (c == '\'') { return GLYPH_APOSTROPHE; } return GLYPH_ALPHA_BASE + (c - 'a'); // uppercase fallback
if (c == '"') { return GLYPH_QUOTE; } }
if (c == '/') { return GLYPH_SLASH; } if (c == '.') {
if (c == ';') { return GLYPH_SEMICOLON; } return GLYPH_PERIOD;
if (c == '=') { return GLYPH_EQUALS; } }
if (c == '+') { return GLYPH_PLUS; } if (c == ',') {
if (c == '_') { return GLYPH_UNDERSCORE; } 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; return GLYPH_UNKNOWN;
} }
@ -402,7 +436,7 @@ static void initialPaint(jlSurfaceT *screen) {
for (row = 0; row < GRID_ROWS; row++) { for (row = 0; row < GRID_ROWS; row++) {
for (col = 0; col < GRID_COLS; col++) { for (col = 0; col < GRID_COLS; col++) {
key = gKeyGrid[row][col]; key = gKeyGrid[row][col];
if (key == KEY_NONE) { if (key == JL_KEY_NONE) {
continue; continue;
} }
drawCell(screen, col, row, false); drawCell(screen, col, row, false);
@ -439,10 +473,7 @@ static void logTypedHistory(void) {
// drawMouseVerifyRow stamps them. Sampled once per use so the drawn // drawMouseVerifyRow stamps them. Sampled once per use so the drawn
// row and updateMouseVerifyRow's change check can never disagree. // row and updateMouseVerifyRow's change check can never disagree.
static uint16_t mouseFlags(void) { static uint16_t mouseFlags(void) {
return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 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));
(jlMouseDown(MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0) |
(jlMouseDown(MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0) |
(jlMouseDown(MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0));
} }
@ -455,7 +486,7 @@ static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t c
for (row = 0; row < GRID_ROWS; row++) { for (row = 0; row < GRID_ROWS; row++) {
for (col = 0; col < GRID_COLS; col++) { for (col = 0; col < GRID_COLS; col++) {
key = gKeyGrid[row][col]; key = gKeyGrid[row][col];
if (key == KEY_NONE) { if (key == JL_KEY_NONE) {
continue; continue;
} }
lit = cellTargetLit(col, row, cursorCol, cursorRow); lit = cellTargetLit(col, row, cursorCol, cursorRow);
@ -513,7 +544,7 @@ static void processTypedChars(jlSurfaceT *screen) {
// this region). // this region).
static void redrawTextStrip(jlSurfaceT *screen) { static void redrawTextStrip(jlSurfaceT *screen) {
gLine[gLineLen] = '\0'; 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_X, TEXT_Y, "TYPE HERE:", COLOR_UNLIT);
drawTextLine(screen, TEXT_INPUT_X, TEXT_Y, gLine, COLOR_CURSOR); drawTextLine(screen, TEXT_INPUT_X, TEXT_Y, gLine, COLOR_CURSOR);
drawVerifyRow(screen); drawVerifyRow(screen);
@ -599,6 +630,7 @@ int main(void) {
int16_t cursorRow; int16_t cursorRow;
config.codegenBytes = 8 * 1024; config.codegenBytes = 8 * 1024;
config.noAudio = true;
if (!jlInit(&config)) { if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError()); fprintf(stderr, "jlInit failed: %s\n", jlLastError());
@ -613,13 +645,13 @@ int main(void) {
} }
buildPalette(screen); buildPalette(screen);
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
initialPaint(screen); initialPaint(screen);
jlInputPoll(); jlInputPoll();
for (;;) { for (;;) {
jlInputPoll(); jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) { if (jlKeyPressed(JL_KEY_ESCAPE)) {
break; break;
} }
cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow); cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,7 +25,8 @@
// spread is read jitter // spread is read jitter
// SERLOSS_SFX also keep a sound effect streaming and retrigger a one-shot // SERLOSS_SFX also keep a sound effect streaming and retrigger a one-shot
// effect once per frame, so NinjaTrackerPlus's entry calls and // 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 // SERLOSS_LOOSE drop the serial add-on's hold on the tight interrupt-latency
// bound once the port is open: the negative control, which // bound once the port is open: the negative control, which
// must lose bytes wherever a masked stretch outlasts the FIFO // must lose bytes wherever a masked stretch outlasts the FIFO
@ -57,6 +58,7 @@
#define MB_JOY_MIN_Y 30 #define MB_JOY_MIN_Y 30
#define MB_JOY_MAX_Y 31 #define MB_JOY_MAX_Y 31
#define MB_SFX_SHOTS 32 #define MB_SFX_SHOTS 32
#define MB_SFX_FILLS 34
#define MB_SIG_VALUE_0 0xA5u #define MB_SIG_VALUE_0 0xA5u
#define MB_SIG_VALUE_1 0x5Au #define MB_SIG_VALUE_1 0x5Au
#define MB_JOY_BIAS 128 #define MB_JOY_BIAS 128
@ -117,6 +119,7 @@ typedef struct {
#ifdef SERLOSS_SFX #ifdef SERLOSS_SFX
static int8_t gSfxShot[SFX_SHOT_BYTES]; static int8_t gSfxShot[SFX_SHOT_BYTES];
static uint8_t gSfxWave[SFX_WAVE_BYTES]; static uint8_t gSfxWave[SFX_WAVE_BYTES];
static uint16_t gSfxFills = 0u;
#endif #endif
static uint16_t benchLoop(jlSurfaceT *stage, bool present); 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 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); 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 #ifdef SERLOSS_SFX
static bool sfxStart(void); static void sfxStart(void);
static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count); static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count);
#endif #endif
@ -156,8 +159,8 @@ static void joyPoll(JoySpreadT *spread) {
int8_t y; int8_t y;
jlInputPoll(); jlInputPoll();
x = jlJoystickX(JOYSTICK_0); x = jlJoystickX(JL_JOYSTICK_0);
y = jlJoystickY(JOYSTICK_0); y = jlJoystickY(JL_JOYSTICK_0);
if (spread->polls == 0u || x < spread->minX) { if (spread->polls == 0u || x < spread->minX) {
spread->minX = x; spread->minX = x;
} }
@ -200,14 +203,11 @@ __attribute__((noinline)) static void publish(uint16_t received, uint16_t inSequ
#ifdef SERLOSS_SFX #ifdef SERLOSS_SFX
// Bring up audio, build the one-shot's square wave and the stream's sawtooth, and start the // Build the one-shot's square wave and the stream's sawtooth, and start the endless stream
// endless stream. // (jlInit started audio).
static bool sfxStart(void) { static void sfxStart(void) {
uint16_t i; uint16_t i;
if (!jlAudioInit()) {
return false;
}
for (i = 0u; i < SFX_SHOT_BYTES; i++) { for (i = 0u; i < SFX_SHOT_BYTES; i++) {
gSfxShot[i] = (int8_t)(((i / (SFX_SHOT_PERIOD / 2u)) & 1u) != 0u ? SFX_LEVEL : -SFX_LEVEL); 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. // overflow the receive ring.
jlAudioPlaySfx((uint8_t)SFX_SHOT_SLOT, (const uint8_t *)gSfxShot, SFX_SHOT_BYTES, SFX_SHOT_RATE); 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); 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. // 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) { static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count) {
uint32_t done; uint32_t done;
(void)ctx; (void)ctx;
gSfxFills++;
mailbox16(MB_SFX_FILLS, gSfxFills);
done = 0u; done = 0u;
while (count - done >= SFX_WAVE_BYTES) { while (count - done >= SFX_WAVE_BYTES) {
jlMemCopy(dst + done, gSfxWave, SFX_WAVE_BYTES); jlMemCopy(dst + done, gSfxWave, SFX_WAVE_BYTES);
@ -269,6 +271,11 @@ int main(void) {
#endif #endif
config.codegenBytes = 0u; config.codegenBytes = 0u;
#ifdef SERLOSS_SFX
config.noAudio = false;
#else
config.noAudio = true;
#endif
if (!jlInit(&config)) { if (!jlInit(&config)) {
MB_BASE[MB_PHASE] = PHASE_FAILED; MB_BASE[MB_PHASE] = PHASE_FAILED;
for (;;) { for (;;) {
@ -280,15 +287,12 @@ int main(void) {
MB_BASE[MB_PHASE] = PHASE_INIT; MB_BASE[MB_PHASE] = PHASE_INIT;
mailbox16(MB_JOY_POLLS, 0u); mailbox16(MB_JOY_POLLS, 0u);
mailbox16(MB_SFX_SHOTS, 0u); mailbox16(MB_SFX_SHOTS, 0u);
mailbox16(MB_SFX_FILLS, 0u);
stage = jlStageGet(); stage = jlStageGet();
mailbox16(MB_FILL_TICKS, benchLoop(stage, false)); mailbox16(MB_FILL_TICKS, benchLoop(stage, false));
mailbox16(MB_PRESENT_TICKS, benchLoop(stage, true)); mailbox16(MB_PRESENT_TICKS, benchLoop(stage, true));
#ifdef SERLOSS_SFX #ifdef SERLOSS_SFX
if (!sfxStart()) { sfxStart();
MB_BASE[MB_PHASE] = PHASE_FAILED;
for (;;) {
}
}
shots = 0u; shots = 0u;
#endif #endif
@ -316,7 +320,7 @@ int main(void) {
#ifdef SERLOSS_JOY #ifdef SERLOSS_JOY
// Calibrate on the first poll, in the latency mode the rest will run in, so a still stick // Calibrate on the first poll, in the latency mode the rest will run in, so a still stick
// reads 0 from then on. // reads 0 from then on.
jlJoystickReset(JOYSTICK_0, 0u); jlJoystickReset(JL_JOYSTICK_0, 0u);
spread.polls = 0u; spread.polls = 0u;
joyPoll(&spread); joyPoll(&spread);
#endif #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. // A grey ramp, so every index the verification rows stamp is visible.
static void buildPalette(jlSurfaceT *screen) { static void buildPalette(jlSurfaceT *screen) {
uint16_t colors[SURFACE_COLORS_PER_PALETTE]; uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
uint16_t i; 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); colors[i] = (uint16_t)((i << PALETTE_RED_SHIFT) | (i << PALETTE_GREEN_SHIFT) | i);
} }
jlPaletteSet(screen, GREY_PALETTE, colors); 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 // The grey palette everywhere, or DENSE_SCB_LINES lines alternating between
// it and the same ramp in blue. // it and the same ramp in blue.
static void buildScb(jlSurfaceT *screen) { 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 #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 i;
uint16_t line; uint16_t line;
jlPaletteGet(screen, GREY_PALETTE, colors); 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); colors[i] = (uint16_t)(colors[i] | BLUE_FLOOR);
} }
jlPaletteSet(screen, BLUE_PALETTE, colors); 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; w = POINTER_SIZE;
h = POINTER_SIZE; h = POINTER_SIZE;
if ((int16_t)(x + w) > SURFACE_WIDTH) { if ((int16_t)(x + w) > JL_SURFACE_WIDTH) {
w = (int16_t)(SURFACE_WIDTH - x); w = (int16_t)(JL_SURFACE_WIDTH - x);
} }
if ((int16_t)(y + h) > SERIAL_ROW_Y) { if ((int16_t)(y + h) > SERIAL_ROW_Y) {
h = (int16_t)(SERIAL_ROW_Y - 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) { static uint16_t mouseFlags(void) {
return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 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));
(jlMouseDown(MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0u) |
(jlMouseDown(MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0u) |
(jlMouseDown(MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0u));
} }
@ -334,6 +331,7 @@ int main(void) {
bool paused; bool paused;
config.codegenBytes = 0u; config.codegenBytes = 0u;
config.noAudio = true;
if (!jlInit(&config)) { if (!jlInit(&config)) {
return 1; return 1;
} }
@ -371,10 +369,10 @@ int main(void) {
for (;;) { for (;;) {
jlInputPoll(); jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) { if (jlKeyPressed(JL_KEY_ESCAPE)) {
break; break;
} }
if (jlKeyPressed(KEY_SPACE)) { if (jlKeyPressed(JL_KEY_SPACE)) {
paused = !paused; paused = !paused;
} }
receiveStream(&stats); receiveStream(&stats);

View file

@ -7,7 +7,9 @@
// per source line) and converted at startup into the 2x2 tile layout // per source line) and converted at startup into the 2x2 tile layout
// jlSpriteT expects. jlSpriteDraw handles transparent-color-0; we use // jlSpriteT expects. jlSpriteDraw handles transparent-color-0; we use
// jlSpriteSaveUnder/RestoreUnder to undraw the previous frame's ball // 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> #include <stdio.h>
@ -18,17 +20,20 @@
#define BALL_TILES_X (BALL_W / 8) #define BALL_TILES_X (BALL_W / 8)
#define BALL_TILES_Y (BALL_H / 8) #define BALL_TILES_Y (BALL_H / 8)
#define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * TILE_BYTES) #define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * JL_TILE_BYTES)
// SaveUnder rounds x down to the platform's storage alignment: 2 px // The save-under buffer is sized by jlSpriteBackupLength (joey/sprite.h):
// for chunky 4bpp (1 extra byte/row worst case), 8 px for planar // the worst case at any x, on every port.
// 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_PALETTE_IDX 0 #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 #define COLOR_BG 0
// Authored layout: 16 pixels wide x 16 rows = 8 bytes/row x 16 rows. // 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 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 // 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 (ty = 0; ty < BALL_TILES_Y; ty++) {
for (tx = 0; tx < BALL_TILES_X; tx++) { 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 (row = 0; row < 8; row++) {
for (b = 0; b < 4; b++) { for (b = 0; b < 4; b++) {
dst[row * 4 + b] = dst[row * 4 + b] = gBallAuthored[((ty * 8) + row) * (BALL_W / 2) + (tx * 4) + b];
gBallAuthored[((ty * 8) + row) * (BALL_W / 2) +
(tx * 4) + b];
} }
} }
} }
@ -102,17 +105,20 @@ int main(void) {
jlSurfaceT *screen; jlSurfaceT *screen;
jlSpriteT *ball; jlSpriteT *ball;
jlSpriteBackupT backup; jlSpriteBackupT backup;
int16_t x; int32_t x; // 1/256 of a pixel
int16_t y; int32_t y;
int16_t vx; int32_t vx; // pixels per second
int16_t vy; int32_t vy;
bool haveBackup; uint32_t lastMillis;
uint32_t now;
uint32_t elapsed;
/* Amiga planar emits 8 pre-shifted DRAW variants per sprite (one /* Amiga planar emits 8 pre-shifted DRAW variants per sprite (one
* per x % 8 alignment) so the codegen arena needs roughly 8x what * 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 * the chunky two-shift case asks for. 32 KB fits a 16x16 ball
* with all variants. */ * with all variants. */
config.codegenBytes = 32UL * 1024; config.codegenBytes = 32UL * 1024;
config.noAudio = true;
if (!jlInit(&config)) { if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError()); fprintf(stderr, "jlInit failed: %s\n", jlLastError());
@ -140,25 +146,24 @@ int main(void) {
(void)jlSpriteCompile(ball); (void)jlSpriteCompile(ball);
buildPalette(screen); 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); jlSurfaceClear(screen, COLOR_BG);
jlStagePresent(); jlStagePresent();
backup.bytes = gBallBackup; backup.bytes = (uint8_t *)gBallBackup;
x = 40; x = (int32_t)40 << SUBPIXEL_SHIFT;
y = 30; y = (int32_t)30 << SUBPIXEL_SHIFT;
vx = 2; vx = BALL_SPEED_X;
vy = 1; vy = BALL_SPEED_Y;
haveBackup = false;
jlSpriteSaveAndDraw(screen, ball, x, y, &backup); jlSpriteSaveAndDraw(screen, ball, (int16_t)(x >> SUBPIXEL_SHIFT), (int16_t)(y >> SUBPIXEL_SHIFT), &backup);
jlStagePresent(); jlStagePresent();
haveBackup = true; lastMillis = jlMillisElapsed();
for (;;) { for (;;) {
jlInputPoll(); jlInputPoll();
if (jlKeyPressed(KEY_ESCAPE)) { if (jlKeyPressed(JL_KEY_ESCAPE)) {
break; break;
} }
@ -170,22 +175,41 @@ int main(void) {
// chunky targets like IIgs SHR; on planar c2p platforms it // chunky targets like IIgs SHR; on planar c2p platforms it
// also avoids c2p racing the raster). VBL wait is omitted // also avoids c2p racing the raster). VBL wait is omitted
// here so the demo runs at the sprite pipeline's native // here so the demo runs at the sprite pipeline's native
// throughput -- expect tearing on the ball. // throughput -- expect tearing on the ball. The ball's speed
if (haveBackup) { // does not depend on it: it moves by the time each frame took.
jlSpriteRestoreUnder(screen, &backup); 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); jlSpriteSaveAndDraw(screen, ball, (int16_t)(x >> SUBPIXEL_SHIFT), (int16_t)(y >> SUBPIXEL_SHIFT), &backup);
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);
jlStagePresent(); jlStagePresent();
haveBackup = true;
} }
jlSpriteDestroy(ball); jlSpriteDestroy(ball);

View file

@ -58,10 +58,10 @@
// expect.bin layout (see the header comment). // expect.bin layout (see the header comment).
#define LE16_BYTES 2 #define LE16_BYTES 2
#define BYTE_BITS 8 #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_OFF_PALETTE LE16_BYTES
#define EXPECT_HEADER_BYTES (EXPECT_OFF_PALETTE + PALETTE_COLORS * 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 // 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. // 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. // the lamp colours cannot collide with the bank's sixteen.
#define LAMP_W 64 #define LAMP_W 64
#define LAMP_H 64 #define LAMP_H 64
#define LAMP_X ((SURFACE_WIDTH - LAMP_W) / 2) #define LAMP_X ((JL_SURFACE_WIDTH - LAMP_W) / 2)
#define LAMP_Y ((SURFACE_HEIGHT - LAMP_H) / 2) #define LAMP_Y ((JL_SURFACE_HEIGHT - LAMP_H) / 2)
#define LAMP_PALETTE 1u #define LAMP_PALETTE 1u
#define LAMP_PASS_COLOR 1u #define LAMP_PASS_COLOR 1u
#define LAMP_FAIL_COLOR 2u #define LAMP_FAIL_COLOR 2u
@ -145,7 +145,7 @@ static void pasteTiles(uint16_t count) {
jlSurfaceClear(gScreen, FILL_COLOR); jlSurfaceClear(gScreen, FILL_COLOR);
for (i = 0u; i < count; i++) { 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; uint16_t block;
uint8_t want; uint8_t want;
for (y = 0; y < SURFACE_HEIGHT; y++) { for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
for (x = 0; x < SURFACE_WIDTH; x++) { for (x = 0; x < JL_SURFACE_WIDTH; x++) {
block = (uint16_t)((y / TILE_PIXELS_PER_SIDE) * TILE_BLOCKS_PER_ROW + x / TILE_PIXELS_PER_SIDE); block = (uint16_t)((y / JL_TILE_PIXELS_PER_SIDE) * JL_TILE_BLOCKS_PER_ROW + x / JL_TILE_PIXELS_PER_SIDE);
want = FILL_COLOR; want = FILL_COLOR;
if (block < tilesDrawn) { 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) { if (jlSamplePixel(gScreen, x, y) != want) {
return false; return false;
@ -296,6 +296,7 @@ int main(void) {
bool pass; bool pass;
config.codegenBytes = CODEGEN_DEFAULT; config.codegenBytes = CODEGEN_DEFAULT;
config.noAudio = true;
if (!jlInit(&config)) { if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError()); fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return EXIT_FAILURE; return EXIT_FAILURE;

View file

@ -74,9 +74,9 @@
// +1 (9DC9) uint8 phase (UBER_PHASE_*) // +1 (9DC9) uint8 phase (UBER_PHASE_*)
// +2 (9DCA) uint16 heartbeat (one increment per timing-loop pass) // +2 (9DCA) uint16 heartbeat (one increment per timing-loop pass)
// +4 (9DCC) uint16 last jlFrameCount() value the timing loop saw // +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_SETUP_SPRITE 1u
#define UBER_PHASE_AUDIO_INIT 2u
#define UBER_PHASE_SHOWCASE 3u #define UBER_PHASE_SHOWCASE 3u
#define UBER_PHASE_TIMED_RUN 4u #define UBER_PHASE_TIMED_RUN 4u
#define UBER_PHASE_CHECKS 5u #define UBER_PHASE_CHECKS 5u
@ -140,7 +140,7 @@ typedef void (*OpFn)(void);
static jlSurfaceT *gStage = NULL; static jlSurfaceT *gStage = NULL;
static jlSpriteT *gSprite = NULL; static jlSpriteT *gSprite = NULL;
static jlSpriteBackupT gBackup; 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; static jlTileT gTileScratch;
@ -154,7 +154,7 @@ static jlTileT gTileMapB;
#define UBER_FRAME_SPRITES 3 #define UBER_FRAME_SPRITES 3
static jlSpriteBackupT gFrameBackups[UBER_FRAME_SPRITES]; 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 // 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(); endMillis = jlMillisElapsed();
*actualFramesOut = (uint16_t)(endFrame - startFrame); *actualFramesOut = (uint16_t)(endFrame - startFrame);
if (*actualFramesOut == 0u) { if (*actualFramesOut == 0u) {
*actualFramesOut = 1u; /* defensive: avoid div-by-zero */ *actualFramesOut = 1u; // defensive: avoid div-by-zero
} }
*millisDeltaOut = endMillis - startMillis; *millisDeltaOut = endMillis - startMillis;
if (*millisDeltaOut == 0u) { if (*millisDeltaOut == 0u) {
*millisDeltaOut = 1u; /* defensive: avoid div-by-zero */ *millisDeltaOut = 1u; // defensive: avoid div-by-zero
} }
return count; return count;
} }
@ -285,10 +285,8 @@ static void timeOp(const char *name, OpFn op) {
opsPerSec = (iters * 1000UL) / (unsigned long)millisDelta; opsPerSec = (iters * 1000UL) / (unsigned long)millisDelta;
opsPerSecFr = (iters * (unsigned long)jlFrameHz()) / (unsigned long)actualFrames; opsPerSecFr = (iters * (unsigned long)jlFrameHz()) / (unsigned long)actualFrames;
hash = jlSurfaceHash(gStage); hash = jlSurfaceHash(gStage);
jlLogF("UBER: %s: %lu iters / %u frames = %lu ops/sec | hash=%08lX", jlLogF("UBER: %s: %lu iters / %u frames = %lu ops/sec | hash=%08lX", name, iters, actualFrames, opsPerSec, (unsigned long)hash);
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-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_stagePresent (void) { jlStagePresent(); }
static void op_inputPoll (void) { jlInputPoll(); } static void op_inputPoll (void) { jlInputPoll(); }
static void op_keyDown (void) { (void)jlKeyDown(KEY_A); } static void op_keyDown (void) { (void)jlKeyDown(JL_KEY_A); }
static void op_keyPressed (void) { (void)jlKeyPressed(KEY_A); } static void op_keyPressed (void) { (void)jlKeyPressed(JL_KEY_A); }
static void op_mouseX (void) { (void)jlMouseX(); } 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_audioFrameTick (void) { jlAudioFrameTick(); }
static void op_audioIsPlaying (void) { (void)jlAudioIsPlayingMod(); } 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); } 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; // rows in this same log. Stage pixels are never written (saves read;
// the restoreVal call fails validation), so these rows are hash-inert. // the restoreVal call fails validation), so these rows are hash-inert.
static jlSpriteBackupT gProbeSpriteBackup; 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) { static void PROBE_ATTR op_probeSpriteSaveVal(void) {
jlSpriteSaveUnder(gStage, gSprite, -100, -100, &gProbeSpriteBackup); 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. // and gBackup untouched, so the row is inert to the golden hashes.
static jlSurfaceT *gProbeRestoreWork = NULL; static jlSurfaceT *gProbeRestoreWork = NULL;
static jlSpriteBackupT gProbeRestoreBackup; 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) { static void PROBE_ATTR op_probeRestoreWork(void) {
jlSpriteRestoreUnder(gProbeRestoreWork, &gProbeRestoreBackup); 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 // The band arrays are port-neutral, so the value must also be
// IDENTICAL ACROSS PORTS -- the C-mark ports are the reference any // IDENTICAL ACROSS PORTS -- the C-mark ports are the reference any
// fused asm mark must reproduce. // fused asm mark must reproduce.
extern uint8_t gStageMinWord[SURFACE_HEIGHT]; extern uint8_t gStageMinWord[JL_SURFACE_HEIGHT];
extern uint8_t gStageMaxWord[SURFACE_HEIGHT]; extern uint8_t gStageMaxWord[JL_SURFACE_HEIGHT];
static uint32_t PROBE_ATTR probeBandFold(void) { static uint32_t PROBE_ATTR probeBandFold(void) {
uint16_t y; uint16_t y;
uint32_t sum; uint32_t sum;
sum = 0; 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 + gStageMinWord[y]);
sum = (uint32_t)(sum * 33u + gStageMaxWord[y]); sum = (uint32_t)(sum * 33u + gStageMaxWord[y]);
} }
@ -1042,80 +1040,123 @@ static void PROBE_ATTR op_probeVgaCheck(void) {
#endif #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); 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 // 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 // buffer (the desktop's pixel format, at the window's scale) or the
// full-screen backbuffer. The golden hashes gate the stage, never those. // full-screen backbuffer; dvxPortProbeDisplayMismatch derives every pixel
// dvxPortProbeDisplayMismatch derives every pixel from the stage's own // from the stage's own palette entries (not the present's tables) and
// palette entries (not the present's tables) and counts the ones that // counts the ones that differ. The hosted backends draw only the
// differ. A strip of one-pixel stripes in all 16 colours stays on screen, // 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 // 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, // 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 // 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 // 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 // (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_PALETTE 1u
#define PROBE_DVX_STRIP_H 10 #define PROBE_DVX_STRIP_H 10
#define PROBE_DVX_SCB_LAST 99u #define PROBE_DVX_SCB_LAST 99u
static void PROBE_ATTR op_probeDvxDisplayCheck(void) { static void PROBE_ATTR op_probeWindowDisplayCheck(void) {
static const uint16_t probePalette[SURFACE_COLORS_PER_PALETTE] = { static const uint16_t probePalette[JL_SURFACE_COLORS_PER_PALETTE] = {
0x000, 0xF00, 0x0F0, 0x00F, 0xFF0, 0xF0F, 0x0FF, 0xFFF, 0x000, 0xF00, 0x0F0, 0x00F, 0xFF0, 0xF0F, 0x0FF, 0xFFF,
0x800, 0x080, 0x008, 0x880, 0x808, 0x088, 0x888, 0x444 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, 0x000, 0x123, 0x246, 0x369, 0x48C, 0x5AF, 0x6C2, 0x7E5,
0x9A1, 0xAB4, 0xBC7, 0xCDA, 0xDEC, 0xE1F, 0xF52, 0x3D8 0x9A1, 0xAB4, 0xBC7, 0xCDA, 0xDEC, 0xE1F, 0xF52, 0x3D8
}; };
uint8_t savedScb[SURFACE_HEIGHT]; uint8_t savedScb[JL_SURFACE_HEIGHT];
uint16_t savedPalette[SURFACE_COLORS_PER_PALETTE]; uint16_t savedPalette[JL_SURFACE_COLORS_PER_PALETTE];
uint32_t afterB; uint32_t afterB;
uint32_t afterSettle; uint32_t afterSettle;
uint32_t afterIdle; uint32_t afterIdle;
uint32_t afterScb; uint32_t afterScb;
uint32_t afterPalette; uint32_t afterPalette;
uint32_t afterAll;
uint32_t afterRestore; uint32_t afterRestore;
uint32_t afterTwo;
uint32_t compared; uint32_t compared;
int16_t x; int16_t x;
uint16_t y; uint16_t y;
for (y = 0; y < SURFACE_HEIGHT; y++) { for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
savedScb[y] = jlScbGet(gStage, y); savedScb[y] = jlScbGet(gStage, y);
} }
jlPaletteGet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette); jlPaletteGet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette);
jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, probePalette); 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); jlSurfaceClear(gStage, 3);
for (x = 0; x < SURFACE_WIDTH; x++) { for (x = 0; x < JL_SURFACE_WIDTH; x++) {
jlFillRect(gStage, x, 0, 1, PROBE_DVX_STRIP_H, (uint8_t)(x % SURFACE_COLORS_PER_PALETTE)); jlFillRect(gStage, x, 0, 1, PROBE_DVX_STRIP_H, (uint8_t)(x % JL_SURFACE_COLORS_PER_PALETTE));
} }
jlStagePresent(); jlStagePresent();
jlWaitVBL();
jlFillRect(gStage, 0, 50, 320, 100, 3); jlFillRect(gStage, 0, 50, 320, 100, 3);
jlStagePresent(); jlStagePresent();
jlWaitVBL();
jlFillRect(gStage, 101, 61, 37, 9, 9); jlFillRect(gStage, 101, 61, 37, 9, 9);
jlStagePresent(); jlStagePresent();
afterB = dvxPortProbeDisplayMismatch(gStage, &compared); afterB = PROBE_WINDOW_MISMATCH(gStage, &compared);
jlStagePresent(); jlStagePresent();
afterSettle = dvxPortProbeDisplayMismatch(gStage, &compared); afterSettle = PROBE_WINDOW_MISMATCH(gStage, &compared);
jlStagePresent(); jlStagePresent();
afterIdle = dvxPortProbeDisplayMismatch(gStage, &compared); afterIdle = PROBE_WINDOW_MISMATCH(gStage, &compared);
jlScbSetRange(gStage, 0, PROBE_DVX_SCB_LAST, (uint8_t)PROBE_DVX_PALETTE); jlScbSetRange(gStage, 0, PROBE_DVX_SCB_LAST, (uint8_t)PROBE_DVX_PALETTE);
jlStagePresent(); jlStagePresent();
afterScb = dvxPortProbeDisplayMismatch(gStage, &compared); afterScb = PROBE_WINDOW_MISMATCH(gStage, &compared);
jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, changedPalette); jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, changedPalette);
jlStagePresent(); jlStagePresent();
afterPalette = dvxPortProbeDisplayMismatch(gStage, &compared); afterPalette = PROBE_WINDOW_MISMATCH(gStage, &compared);
for (y = 0; y < SURFACE_HEIGHT; y++) { 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]); jlScbSet(gStage, y, savedScb[y]);
} }
jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette); jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette);
jlStagePresent(); jlStagePresent();
afterRestore = dvxPortProbeDisplayMismatch(gStage, &compared); afterRestore = PROBE_WINDOW_MISMATCH(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); 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 #endif
@ -1218,8 +1259,7 @@ static void buildBallSprite(void) {
dst = &gBallTiles[(ty * BALL_TILES_X + tx) * 32u]; dst = &gBallTiles[(ty * BALL_TILES_X + tx) * 32u];
for (row = 0; row < 8; row++) { for (row = 0; row < 8; row++) {
for (b = 0; b < 4; b++) { for (b = 0; b < 4; b++) {
dst[row * 4 + b] = dst[row * 4 + b] = gBallAuthored[((ty * 8) + row) * 8 + (tx * 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. // the headless perf-capture run still completes without a keypress.
held = jlFrameCount(); held = jlFrameCount();
while ((uint16_t)(jlFrameCount() - held) < SC_HOLD_FRAMES) { while ((uint16_t)(jlFrameCount() - held) < SC_HOLD_FRAMES) {
/* hold */ // hold
} }
} }
@ -1556,7 +1596,7 @@ static void __attribute__((noinline)) checkTileMonoFlood(void) {
uint8_t i; uint8_t i;
uberMbOp(103); 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); mono.pixels[i] = (uint8_t)((i & 1) ? 0x0F : 0xF0);
} }
jlTilePasteMono(gStage, 10, 10, &mono, 5, 9); jlTilePasteMono(gStage, 10, 10, &mono, 5, 9);
@ -1574,7 +1614,7 @@ static void __attribute__((noinline)) checkDrawText(void) {
uberMbOp(104); uberMbOp(104);
for (i = 0; i < 256u; i++) { 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 -- // 'A' -> tile (5,5), 'B' -> tile (6,6). Seed both glyph tiles here --
// the section-opening surface clear wiped whatever the timed tile ops // the section-opening surface clear wiped whatever the timed tile ops
@ -1715,7 +1755,7 @@ static void __attribute__((noinline)) checkTileMonoFloodExtra(void) {
uint8_t i; uint8_t i;
uberMbOp(121); uberMbOp(121);
for (i = 0; i < TILE_BYTES; i++) { for (i = 0; i < JL_TILE_BYTES; i++) {
monoAll.pixels[i] = comboBytes[i & 3u]; monoAll.pixels[i] = comboBytes[i & 3u];
} }
jlTilePasteMono(gStage, 10, 12, &monoAll, 5, 9); jlTilePasteMono(gStage, 10, 12, &monoAll, 5, 9);
@ -2016,8 +2056,8 @@ static void __attribute__((noinline)) runAllTests(void) {
#ifdef JOEYLIB_PLATFORM_DOS #ifdef JOEYLIB_PLATFORM_DOS
op_probeVgaCheck(); op_probeVgaCheck();
#endif #endif
#ifdef JOEYLIB_PLATFORM_DVX #if defined(JOEYLIB_PLATFORM_DVX) || defined(JOEYLIB_HOSTED_WIN16) || defined(JOEYLIB_HOSTED_WIN32) || defined(JOEYLIB_HOSTED_BE)
op_probeDvxDisplayCheck(); op_probeWindowDisplayCheck();
#endif #endif
#endif #endif
@ -2057,8 +2097,7 @@ static void __attribute__((noinline)) reportElapsed(uint16_t startFrame) {
endFrame = jlFrameCount(); endFrame = jlFrameCount();
elapsedFrames = (uint16_t)(endFrame - startFrame); elapsedFrames = (uint16_t)(endFrame - startFrame);
elapsedMs = ((unsigned long)elapsedFrames * 1000UL) / (unsigned long)jlFrameHz(); elapsedMs = ((unsigned long)elapsedFrames * 1000UL) / (unsigned long)jlFrameHz();
jlLogF("UBER: total wall time: %lu ms (%u frames @ %u Hz)", jlLogF("UBER: total wall time: %lu ms (%u frames @ %u Hz)", elapsedMs, elapsedFrames, (unsigned)jlFrameHz());
elapsedMs, elapsedFrames, (unsigned)jlFrameHz());
} }
@ -2080,7 +2119,7 @@ static bool __attribute__((noinline)) setupSprite(void) {
jlLog("UBER: jlSpriteCompile failed"); jlLog("UBER: jlSpriteCompile failed");
} }
while (jlFrameCount() == before) { while (jlFrameCount() == before) {
/* wait for next VBL edge */ // wait for next VBL edge
} }
jlLogF("UBER: jlSpriteCompile: 1 call in <= 1 frame"); jlLogF("UBER: jlSpriteCompile: 1 call in <= 1 frame");
gBackup.bytes = gBackupBytes; 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). * 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. */ * UL on the multiply because a 16-bit int overflows on 32 * 1024. */
config.codegenBytes = 32UL * 1024; config.codegenBytes = 32UL * 1024;
// Audio on: the timed run measures every op with the engine running.
config.noAudio = false;
return jlInit(&config); return jlInit(&config);
} }
@ -2138,21 +2179,15 @@ int main(void) {
*UBER_MB_SPRITEPTR = (uint32_t)gSprite; *UBER_MB_SPRITEPTR = (uint32_t)gSprite;
#endif #endif
// Audio: only init/shutdown is exercised. Triggering jlAudioPlaySfx // Audio: jlInit started the engine; nothing is played. Triggering
// without first calling jlAudioPlayMod leaves NTP's engine in a // jlAudioPlaySfx without first calling jlAudioPlayMod leaves NTP's
// half-initialized state -- NTPstreamsound is designed to OVERLAY on // engine in a half-initialized state -- NTPstreamsound is designed to
// an already-running module. Without NTPprepare/NTPplay first, the // OVERLAY on an already-running module. Without NTPprepare/NTPplay
// streamer oscillator is fired but no music tick ever advances or // first, the streamer oscillator is fired but no music tick ever
// silences it, and you get a stuck high-pitched scream. UBER doesn't // advances or silences it, and you get a stuck high-pitched scream.
// ship a MOD asset, so we skip the SFX exercise. The frame-tick and // UBER doesn't ship a MOD asset, so we skip the SFX exercise. The
// isPlayingMod calls below still get timed (both are no-op fast // frame-tick and isPlayingMod calls below still get timed (both are
// paths on IIgs). // no-op fast paths on IIgs).
uberMbPhase(UBER_PHASE_AUDIO_INIT);
if (jlAudioInit()) {
jlLogF("UBER: audioInit OK");
} else {
jlLogF("UBER: audioInit failed (skipping audio)");
}
// Visual showcase: render one of each primitive into its own grid // Visual showcase: render one of each primitive into its own grid
// cell so the run shows something legible (the timed benchmark below // 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 // Apple IIgs -- NinjaTrackerPlus .NTP, played by Ninjaforce's
// 65816 replayer linked into the binary. // 65816 replayer linked into the binary.
// Amiga -- raw .MOD, played by Frank Wille's PTPlayer. // Amiga -- raw .MOD, played by Frank Wille's PTPlayer.
// DOS -- raw .MOD, played by libxmp-lite over SB DMA. // DOS -- raw .MOD, mixed by JoeyLib's module player (PTPlayer's
// Atari ST -- raw .MOD, played by libxmp-lite (parser only) plus // replay in C) into SB DMA (22 kHz mono 8-bit).
// a hand-rolled 68k 4-channel mixer that outputs via // Atari ST -- raw .MOD, mixed by the module player at 12288 Hz and
// YM2149 4-bit PWM at ~12.5 kHz. // 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 // Game code always calls the same entry points; the per-port HAL hides
// HAL hides the engine choice. A failed audio init is non-fatal -- // 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. // jlInit still succeeds and audio calls become no-ops.
#ifndef JOEYLIB_AUDIO_H #ifndef JOEYLIB_AUDIO_H
@ -26,36 +32,31 @@
#include "platform.h" #include "platform.h"
#include "types.h" #include "types.h"
// SFX slot convention: the IIgs maps tracker tone voices onto SFX // SFX slot convention: the IIgs plays tone voices 0-2 on SFX slots 0-2
// slots 0-2 and the tracker noise voice onto slot 3, so applications // and the noise voice on slot 3 (a voice and an effect on one slot cut
// that also play digital SFX while music runs should prefer slot 4. // each other off, and a held voice takes its slot back at the next
// On the other ports the slots are independent of the voice layer. // jlAudioFrameTick), so applications that also play digital SFX while
#define JOEY_AUDIO_SFX_SLOTS 5 // 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 // Number of PSG-style polyphonic tone voices (jlAudioVoice selects
// voice 0/1/2). The core gate range-checks against this so each // voice 0/1/2). The core gate range-checks against this so each
// per-port HAL can assume voice < JOEY_AUDIO_VOICES. // per-port HAL can assume voice < JL_AUDIO_VOICES.
#define JOEY_AUDIO_VOICES 3 #define JL_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);
// Begin module playback. data points at the platform-native module // 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 // produces the right form for each target. If a module is already
// playing, it is replaced. // playing, it is replaced.
// //
// loop=true plays forever. loop=false stops at song end, but Amiga // loop=true plays forever. loop=false stops at song end on every port.
// requires the module to contain an E8FF effect at song end for the // PTPlayer (Amiga) has no native song-end signal, so the Amiga HAL adds an
// stop to fire (PTPlayer has no native song-end signal). The // E8F stop marker to the module as it copies it to chip RAM; a module
// `joeymod` tool's .amod output extension injects that marker // with no room for one (all 128 song positions or patterns in use) loops
// automatically; ship .amod for Amiga and .mod for the other ports. // there anyway. The `joeymod` tool's .amod output adds the same marker
// loop=false on a .mod (no E8FF) loops anyway on Amiga. // ahead of time.
void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop); void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
// Stop the current module (if any). The playhead is reset so the next // 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); void jlAudioStopMod(void);
// True if a module is currently producing output (false during silence // True if a module is currently producing output (false during silence
// after StopMod or before the first PlayMod, and on platforms where // after StopMod, once a loop=false song has ended, before the first
// audio init failed). // PlayMod, and on platforms where audio init failed).
bool jlAudioIsPlayingMod(void); 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 // 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 // the sample was recorded at; the engine pitches as needed for its
// own output rate. If the slot is currently playing, the new sample // 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 // 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 // -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 // streaming slot's prefetch buffer; should write up to `count` signed
// 8-bit samples to `dst` and return the number written (see // 8-bit samples to `dst` and return the number written (see
// jlAudioPlaySfx for the one clamp the IIgs applies). Returning 0 // jlAudioPlaySfx for the one clamp the IIgs applies). Returning 0
// signals end-of-stream and the slot auto-deactivates. Called from // signals end-of-stream and the slot auto-deactivates. Called in
// the same context as the audio engine's refill (main thread on // program time on every port, from the call that starts the stream and
// DOS/ST, audio ISR on Amiga/IIgs); must not block or allocate. // 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); typedef uint32_t (*jlAudioStreamFillT)(void *ctx, int8_t *dst, uint32_t count);
// Trigger streaming SFX playback on the given slot. The mixer pulls // 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 // allocation. `rateHz` is the sample rate `fill` produces; the
// engine resamples (with linear interpolation) to its own output // engine resamples (with linear interpolation) to its own output
// rate. If the slot is currently playing, the new stream replaces // rate. If the slot is currently playing, the new stream replaces
// it. Implemented on every port: DOS/ST/X68000 pull through the shared // it. Implemented on every port: DOS/ST/X68000/hosted pull through the
// SFX overlay mixer, while the IIgs (NTPstreamsound) and Amiga // shared SFX overlay mixer as jlAudioFrameTick refills their sound
// (PTPlayer mt_playfx) play one chunk at a time and fire the next from // buffers, while the IIgs (NTPstreamsound, 7 KB chunks) and Amiga
// jlAudioFrameTick as the current one runs down -- so on those two the // (PTPlayer mt_playfx, 4 KB chunks) play one chunk at a time and fire the
// host must be calling jlAudioFrameTick for a stream to continue past // next from jlAudioFrameTick as the current one runs down -- so the host
// its first chunk, and a seam can carry up to one frame of jitter. // 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); void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz);
// Raw-format streaming: jlAudioPlaySfxStream, except `fill` delivers // Raw-format streaming: jlAudioPlaySfxStream, except `fill` delivers
// bytes already in the port's raw stream format (jlAudioSfxRawFormat), // bytes already in the port's raw stream format (jlAudioSfxRawFormat),
// so the port skips its per-byte conversion of every chunk. On the IIgs // 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 // that conversion costs ~7 us a byte, ~50 ms per 7 KB chunk. Every other
// stalled Space Taxi's ride for a third of a second per chunk of speech. // port's raw format is signed PCM, so there Raw is the plain call (DVX
// Every other port streams signed PCM natively, so there Raw is the // still flips each byte for its sound library). A producer that cannot
// plain call. A producer that cannot make the port's format uses // make the port's format uses jlAudioPlaySfxStream.
// jlAudioPlaySfxStream.
#define JL_SFX_RAW_SIGNED8 0u // int8_t PCM, the jlAudioPlaySfxStream contract #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) #define JL_SFX_RAW_UNSIGNED8_NOZERO 1u // uint8_t PCM biased 0x80; 0x00 is forbidden (DOC stop byte)
uint8_t jlAudioSfxRawFormat(void); uint8_t jlAudioSfxRawFormat(void);
@ -117,14 +122,14 @@ void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, void *ctx
// Stereo placement for a SFX slot. // Stereo placement for a SFX slot.
typedef enum { typedef enum {
AUDIO_PAN_LEFT = 0, JL_AUDIO_PAN_LEFT = 0,
AUDIO_PAN_CENTER, JL_AUDIO_PAN_CENTER,
AUDIO_PAN_RIGHT, JL_AUDIO_PAN_RIGHT,
AUDIO_PAN_COUNT JL_AUDIO_PAN_COUNT
} jlAudioPanE; } 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 // the setting sticks to the SLOT, so it survives retriggers -- set it once
// when a sound is meant to come from one side. // 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 // A stock IIgs sums to mono anyway; this only shows up on a stereo
// card or an emulator. // card or an emulator.
// Amiga left and right, by picking the Paula channel side (0 and 3 are // 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, // left, 1 and 2 right). A channel plays on one side only, so a
// so AUDIO_PAN_CENTER keeps PTPlayer's own channel choice. // 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 // X68000 ignored. Everything is mixed into ONE MSM6258 ADPCM stream, so
// its pan bits would move all audio at once, not one slot. // 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. // Passing an out-of-range pan is ignored.
void jlAudioSetPan(uint8_t slot, jlAudioPanE pan); void jlAudioSetPan(uint8_t slot, jlAudioPanE pan);
// Stop a SFX slot early. No-op if the slot is already idle. // Stop a SFX slot early. No-op if the slot is already idle.
void jlAudioStopSfx(uint8_t slot); void jlAudioStopSfx(uint8_t slot);
// Direct hardware tone generator: continuously play a square wave at // A single tone at `freqHz` until the next call: jlAudioVoice(0, freqHz,
// `freqHz` until the next jlAudioTone call. freqHz == 0 silences the // 0), tone voice 0 at full volume, on every port. freqHz == 0 silences
// generator. Designed for AGI-era PSG-style music where the engine // it. Designed for AGI-era PSG-style music where the engine just
// just programs a divisor and lets the chip play; CPU cost during // programs a divisor and lets the chip play; CPU cost during playback is
// playback is zero. Single voice on platforms with multiple // zero. It shares voice 0 with jlAudioVoice and jlMusicPlay. On DOS
// hardware oscillators (uses voice 0). On DOS this programs PIT // without an OPL2, voice 0 (and so this tone) is the PC speaker.
// 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.
void jlAudioTone(uint16_t freqHz); void jlAudioTone(uint16_t freqHz);
// Periodic tick callback for sound schedulers that need better-than- // 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 // into one render frame, so the chip only "hears" the last write
// per batch and short notes are dropped. // per batch and short notes are dropped.
// //
// fn fires from interrupt context at `hz` times per second. Keep // fn fires `hz` times per second on average, on every port, and may
// fn short: only memory reads, direct hardware-register writes, // fire from an interrupt (DOS, Amiga, ST, X68000): keep fn short, only
// no malloc / printf / blocking. Pass fn = NULL to uninstall. // 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, // Per-port impl: DOS reprograms PIT counter 0 + hooks INT 8,
// keeping the BIOS tick counter at $0040:$006C advancing at the // keeping the BIOS tick counter at $0040:$006C advancing at the
// canonical 18.2 Hz via an accumulator (so uclock / time stay // canonical 18.2 Hz via an accumulator (so uclock / time stay
// correct). Other ports return false until a port-appropriate // correct). The Amiga uses a timer.device E-clock request and a
// timer source is wired. // 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); typedef void (*jlAudioTickFnT)(void);
bool jlAudioTickRegister(jlAudioTickFnT fn, uint16_t hz); bool jlAudioTickRegister(jlAudioTickFnT fn, uint16_t hz);
// Critical section against the tick ISR. Callers that mutate // Critical section against the tick ISR. Callers that mutate
// state read by the registered tick callback must wrap the // state read by the registered tick callback must wrap the
// mutation in Enter/Exit so the ISR can't observe a half-written // mutation in Enter/Exit so the ISR can't observe a half-written
// state. On DOS this is `cli` / `sti`; on ports without an // state. Sections nest: the outermost pair holds the interrupt off
// audio ISR these are no-ops. // (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 jlAudioCriticalEnter(void);
void jlAudioCriticalExit(void); void jlAudioCriticalExit(void);
// 3-voice PSG-style polyphonic tone generator. `voice` selects 0/1/2; // 3-voice PSG-style polyphonic tone generator. `voice` selects 0/1/2;
// `freqHz` is the new tone (0 = silence this voice); `atten` is the // `freqHz` is the new tone (0 = silence this voice); `atten` is the
// AGI/SN76489-style 4-bit attenuation, 0 = loudest .. 15 = silent // 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). // DOS -- AdLib OPL2 melodic channels 0/1/2 (ports 0x388/0x389);
// Atari ST -- YM2149 PSG channels A/B/C. // 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. // Amiga -- Paula channels 0/1/2 looping a 2-byte square sample.
// IIgs -- Ensoniq DOC oscillators playing a resident square, // IIgs -- Ensoniq DOC oscillators playing a resident square on SFX
// retriggered as an ~80 ms one-shot per call. Needs // slots 0/1/2.
// jlAudioInit (the DOC path rides the NTP engine); // X68000 -- YM2151 channels 0/1/2, one sine operator each.
// callers that hold a note longer than ~80 ms must // DVX -- square tones of DVX's sound library.
// re-assert it about once a frame (jlMusicPlay's // hosted -- square waves mixed in software.
// sequencer and AGI's per-frame driver both do).
// //
// Voice state is sticky: a voice keeps playing its last frequency // A note sounds until the next call for that voice. The IIgs plays a
// until the next jlAudioVoice call on it (IIgs excepted, above). CPU // note as a short sample and restarts held notes from jlAudioFrameTick,
// cost during sustained playback is zero (the hardware oscillates on // so call that every frame. On the Amiga the voices are the channels the
// its own). Safe to call without jlAudioInit on DOS/ST/Amiga -- the // MOD engine drives, so while a MOD is playing, voice writes fight the
// voice path is direct hardware programming, independent of the // player's and lose: use the voice/tracker layer or the MOD layer per
// MOD/SFX mixer. On ST and Amiga the voice channels are the same // scene, not both at once.
// 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.
void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten); void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
// Noise/percussion generator, the 4th logical voice of the tracker // Noise/percussion generator, the 4th logical voice of the tracker
// layer. `pitch` is 0..31 in YM2149 noise-period style: 0 = the // layer. `pitch` is 0..31 in YM2149 noise-period style: 0 = the
// brightest hiss (hats), 31 = the lowest rumble (toms); atten as in // brightest hiss (hats), 31 = the lowest rumble (toms); the noise runs at
// jlAudioVoice, and atten >= 15 silences. The per-port mapping: // 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 // Atari ST -- the YM2149 noise generator (reg 6) mixed onto
// channel C. Noise shares channel C's volume register // 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). // apart (songbake warns on overlap).
// DOS -- OPL2 rhythm-mode snare drum (the chip's LFSR noise // DOS -- OPL2 rhythm-mode snare drum (the chip's LFSR noise
// source) on channel 7; the three tone channels are // 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 // Amiga -- Paula channel 3 looping a chip-RAM LFSR noise sample
// (channels 0-2 are the tone voices). // (channels 0-2 are the tone voices).
// IIgs -- a resident LFSR noise sample on SFX slot 3, same // IIgs -- a resident LFSR noise sample on SFX slot 3.
// ~80 ms retriggered one-shot model as the tone voices // X68000 -- the YM2151's noise generator, at the rate exactly.
// (jlMusicPlay's sequencer re-asserts it per frame). // 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 // The same contract as jlAudioVoice: it sounds until the next call.
// DOS/ST/Amiga, jlAudioInit required on the IIgs.
void jlAudioNoise(uint8_t pitch, uint8_t atten); void jlAudioNoise(uint8_t pitch, uint8_t atten);
// Portable chip-tracker music: play a JYM1 event stream (baked from // Portable chip-tracker music: play a JYM1 event stream (baked from
// tools/songbake text notation) through the jlAudioVoice tone layer. // 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 // 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: // 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 // 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. // 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 // 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. // when it has none); loop=false silences the voices at song end.
// Returns false (and plays nothing) on a malformed stream. Call // Returns false (and plays nothing) on a malformed stream.
// jlAudioInit first for IIgs audibility (see jlAudioVoice).
bool jlMusicPlay(const uint8_t *data, uint32_t length, bool loop); bool jlMusicPlay(const uint8_t *data, uint32_t length, bool loop);
// Stop tracker playback and silence the voices (tones and noise). // Stop tracker playback and silence the voices (tones and noise).
@ -270,10 +287,11 @@ void jlMusicResume(void);
// Step it over successive frames for fades. // Step it over successive frames for fades.
void jlMusicSetAtten(uint8_t atten); void jlMusicSetAtten(uint8_t atten);
// Hook the engine into the game loop. Most platforms drive their // Hook the engine into the game loop; call it once per frame. Every
// engines off a hardware IRQ and ignore this call, but it's safe to // port does sound work here: DOS, ST, X68000, DVX, and hosted mix or
// invoke once per frame regardless and required for any port that // refill their sound buffers (modules and effects stop or repeat
// runs its mixer in user-thread context. // 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); void jlAudioFrameTick(void);
#endif #endif

View file

@ -12,21 +12,35 @@
typedef struct { typedef struct {
// Runtime compiled-sprite cache size. 0 selects a per-platform // Runtime compiled-sprite cache size. 0 selects a per-platform
// default: 8 KB on IIgs, 32 KB on Amiga/ST/DOS (the 68k/x86 // default: 8 KB on the IIgs, 64 KB on the Amiga and ST, and 32 KB
// emitters are ~4-8x less code-dense than the 65816's, so a // everywhere else (the 68k/x86 emitters are ~4-8x less code-dense
// 32x32 sprite can need 8-16 KB there). Size against // than the 65816's, so a 32x32 sprite can need 8-16 KB there). The
// jlSpriteCompiledSize() when tuning. On the IIgs the cache is one // ports that interpret every sprite still allocate it. Size against
// Memory Manager block allocated with attrNoCross (it may not span // jlSpriteCompiledSize() when tuning. Elsewhere the cache is one
// a 64 KB bank boundary), so values above 65536 are clamped to // block, and jlInit fails if it cannot be had. On the IIgs it is up
// 65536; even a clamped request can fail if no free bank exists. // 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; 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; } jlConfigT;
// Initialize the library. Returns true on success. // Initialize the library, audio too (unless config->noAudio). Returns true
// On failure, jlLastError() returns a human-readable description. // 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); 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); void jlShutdown(void);
// Returns the most recent error message, or NULL if none. // 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 // Allocate a large or persistent buffer, backed by the platform's
// native allocator rather than the small C heap. Use this (not malloc) // native allocator rather than the small C heap. Use this (not malloc)
// for anything that exceeds the per-allocation or total ceiling of the // 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, // stock C heap -- on the IIgs the whole C heap is one Memory Manager
// and a single malloc is capped at ~32 KB, so multi-KB caches and frame // block of 32 KB to 48 KB (or, if that block cannot be had, what the
// buffers must come from the IIgs Memory Manager instead. Returns NULL // link left in the program's bank, which can be tiny), so multi-KB
// on failure. The returned pointer is NOT zero-filled. // 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); void *jlAlloc(uint32_t bytes);
// Release a buffer returned by jlAlloc. NULL is accepted and ignored. // 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); void jlFree(void *p);
// memcpy with a 32-bit count. Same contract as memcpy (no overlap); every // 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). // byte, split at bank boundaries).
void jlMemCopy(void *dst, const void *src, uint32_t bytes); void jlMemCopy(void *dst, const void *src, uint32_t bytes);
// Block the calling thread until the next display vertical blank. // Block the calling thread until the next display vertical blank, to
// Used to pace game loops to the display's native refresh rate // pace a game loop to the display's refresh. On the machine ports it
// (~70 Hz on VGA mode 13h, ~50 or ~60 Hz on Amiga/ST PAL/NTSC, ~60 Hz // waits for the display's own vertical blank. The hosted ports, and DVX
// on IIgs SHR). Cheap on every port since the underlying mechanism is // in a window, have none to wait for: there it waits for the next tick
// always a hardware-level wait, not a software timer. // of a virtual frame clock (60 Hz hosted, 70 Hz DVX), and nothing keeps
// a present from tearing.
void jlWaitVBL(void); void jlWaitVBL(void);
// Monotonic 16-bit frame counter. Caller must poll faster than // Monotonic 16-bit frame counter, kept by an interrupt or derived from a
// 2 * jlFrameHz() so no edge is missed. Used by benchmarks and // clock, so it does not miss frames however seldom it is read (except on
// frame-rate-independent animation. (Per-port edge-detect / VBL-ISR // an X68000 whose system refused JoeyLib's frame interrupt: there it
// mechanism is documented at jlpFrameCount in src/core/port.h.) // 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); uint16_t jlFrameCount(void);
// Nominal display frame rate in Hz: 50 (Amiga PAL), 60 (IIgs / ST // Display frame rate in Hz: 60 on the IIgs (50 on a PAL machine), 50 on
// NTSC default), 70 (VGA mode 13h). The actual VBL cadence may // 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 // drift slightly; the value reported here is what benchmarks divide
// by to convert iters-per-N-frames to ops/sec. // by to convert iters-per-N-frames to ops/sec.
uint16_t jlFrameHz(void); uint16_t jlFrameHz(void);
// Monotonic wall-clock millisecond counter since jlInit. On DOS/ST it // Monotonic wall-clock millisecond counter since jlInit. It may
// reads a real hardware timer; on IIgs/Amiga the generic implementation // advance a frame at a time. Call it at least once every 15 minutes, or
// derives it from the frame counter (frame-granularity steps, and the // the time between two calls can be lost. Two consecutive calls
// caller must poll at least once per 65535 frames -- ~18 min at 60Hz -- // separated by a long render report the true elapsed time, not the
// or wrapped frames are lost). Two consecutive calls separated by a long // number of VBLs that fired.
// 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.)
uint32_t jlMillisElapsed(void); uint32_t jlMillisElapsed(void);
// Deterministic, seedable pseudo-random generator. Bit-identical // 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 // 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 // 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); 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 // Flood fill a 4-connected region starting at (x, y), stopping at
// boundary pixels of color boundaryColor. Replaces every reachable // 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 // rendering (e.g. Sierra-style picture playback): outline a closed
// region with jlDrawLine in boundaryColor, then fill with this. // 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); 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 // Two directories sit next to the executable, staged there by the per-platform
// disk / directory packagers: // 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*. // SAVES/ -- writable save / preference files. jlSave*.
// Both are addressed by a bare name relative to their directory; the prefix is // 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 #ifndef JOEYLIB_FILE_H
#define 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/ // Open a data file by its name relative to the DATA/ directory. The DATA/
// prefix is prepended automatically; pass "levels/title.dat", not // prefix is prepended automatically; pass "levels/title.dat", not
// "DATA/levels/title.dat". mode is a standard fopen mode string. Returns NULL // "DATA/levels/title.dat". mode is a standard fopen mode string, passed on
// if the resulting path would overflow the internal buffer or the file cannot // unchecked (DATA/ is meant to be read, but a writing mode is not refused).
// be opened. // 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); FILE *jlDataOpen(const char *name, const char *mode);
// ----- Writable save files (SAVES/) ----- // ----- Writable save files (SAVES/) -----
// //
// Save / preference data lives under a SAVES/ directory beside the executable // Save / preference data lives under a SAVES/ directory -- a writable
// -- a writable sibling of the read-only DATA/ tree. Names are relative to // sibling of the DATA/ tree (where, per port: the top of this file). Names
// SAVES/: pass "hero.sav", not "SAVES/hero.sav". Keep names portable across // are relative to SAVES/: pass "hero.sav", not "SAVES/hero.sav". A name is at
// the retro filesystems: at most 8 characters plus a short extension, letters // most 8 letters and digits starting with a letter, then optionally a period
// and digits only, so they fit ProDOS (15-char) and DOS 8.3. // 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), // 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 // 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); FILE *jlSaveOpen(const char *name, const char *mode);
// Write `len` bytes from `buf` to save file `name`, replacing any existing // 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); 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 // 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 ----- // ----- Disk space -----
// Free space, in bytes, on the volume that holds SAVES/ (the program's // Free space, in bytes, on the volume that holds SAVES/. Returns 0 if it
// volume). Returns 0 if it cannot be determined (e.g. the port has no query // cannot be determined. Saturates at UINT32_MAX (4 GB - 1) on larger
// for it). Saturates at UINT32_MAX (4 GB - 1) on larger volumes. // 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); uint32_t jlDiskFree(void);
#endif #endif

View file

@ -6,7 +6,7 @@
// //
// Call jlInputPoll() once per frame (typically right before // Call jlInputPoll() once per frame (typically right before
// drawing) to refresh keyboard, mouse, and joystick state. After // 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: // key:
// //
// jlKeyDown(k) -- is key k held down right now // jlKeyDown(k) -- is key k held down right now
@ -26,8 +26,8 @@
// The mouse predicates return the pointer state: // The mouse predicates return the pointer state:
// //
// jlMouseX/Y() -- pointer position in surface // jlMouseX/Y() -- pointer position in surface
// coords (0..SURFACE_WIDTH-1, // coords (0..JL_SURFACE_WIDTH-1,
// 0..SURFACE_HEIGHT-1) // 0..JL_SURFACE_HEIGHT-1)
// jlMouseDown(b) -- is button b held right now // jlMouseDown(b) -- is button b held right now
// jlMousePressed(b) -- rising edge since last poll // jlMousePressed(b) -- rising edge since last poll
// jlMouseReleased(b) -- falling edge since last poll // jlMouseReleased(b) -- falling edge since last poll
@ -39,7 +39,8 @@
// jlJoystickX/Y(js) -- axis values, signed -127..+127. // jlJoystickX/Y(js) -- axis values, signed -127..+127.
// Digital sticks snap to the // Digital sticks snap to the
// extremes; analog sticks return // 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 // jlJoyDown(js, b) -- is button b held on stick js
// jlJoyPressed(js, b) -- rising edge since last poll // jlJoyPressed(js, b) -- rising edge since last poll
// jlJoyReleased(js, b) -- falling edge since last poll // jlJoyReleased(js, b) -- falling edge since last poll
@ -54,63 +55,63 @@
#include "types.h" #include "types.h"
typedef enum { 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, 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,
KEY_J, KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, 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,
KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z, 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, JL_KEY_0, JL_KEY_1, JL_KEY_2, JL_KEY_3, JL_KEY_4,
KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, 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, JL_KEY_F1, JL_KEY_F2, JL_KEY_F3, JL_KEY_F4, JL_KEY_F5,
KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, JL_KEY_F6, JL_KEY_F7, JL_KEY_F8, JL_KEY_F9, JL_KEY_F10,
KEY_COUNT JL_KEY_COUNT
} jlKeyE; } jlKeyE;
typedef enum { typedef enum {
MOUSE_BUTTON_NONE = 0, JL_MOUSE_BUTTON_NONE = 0,
MOUSE_BUTTON_LEFT, JL_MOUSE_BUTTON_LEFT,
MOUSE_BUTTON_RIGHT, JL_MOUSE_BUTTON_RIGHT,
MOUSE_BUTTON_MIDDLE, JL_MOUSE_BUTTON_MIDDLE,
MOUSE_BUTTON_COUNT JL_MOUSE_BUTTON_COUNT
} jlMouseButtonE; } jlMouseButtonE;
typedef enum { typedef enum {
JOYSTICK_0 = 0, JL_JOYSTICK_0 = 0,
JOYSTICK_1, JL_JOYSTICK_1,
JOYSTICK_COUNT JL_JOYSTICK_COUNT
} jlJoystickE; } jlJoystickE;
typedef enum { typedef enum {
JOY_BUTTON_0 = 0, JL_JOY_BUTTON_0 = 0,
JOY_BUTTON_1, JL_JOY_BUTTON_1,
JOY_BUTTON_COUNT JL_JOY_BUTTON_COUNT
} jlJoyButtonE; } jlJoyButtonE;
#define JOYSTICK_AXIS_MAX 127 #define JL_JOYSTICK_AXIS_MAX 127
#define JOYSTICK_AXIS_MIN (-127) #define JL_JOYSTICK_AXIS_MIN (-127)
// Control characters delivered through jlInputGetChar alongside // Control characters delivered through jlInputGetChar alongside
// printable ASCII. Everything else below 0x20, plus 0x7F, is // printable ASCII. Everything else below 0x20, and everything above
// filtered out of the queue. // 0x7F, is filtered out of the queue.
#define JL_CHAR_BACKSPACE 0x08 #define JL_CHAR_BACKSPACE 0x08
#define JL_CHAR_TAB 0x09 #define JL_CHAR_TAB 0x09
#define JL_CHAR_RETURN 0x0D #define JL_CHAR_RETURN 0x0D
#define JL_CHAR_ESCAPE 0x1B #define JL_CHAR_ESCAPE 0x1B
// The FORWARD delete (the key labelled Del/Delete on a PC, ST or Amiga keyboard - not backspace). A text // 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 // 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 #define JL_CHAR_DELETE 0x7F
// Typed-character queue capacity (ring buffer; one slot stays empty, // 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 // 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 // the keyboard layout are already applied by the backend, so
// punctuation and shifted symbols arrive correctly on every port. // punctuation and shifted symbols arrive correctly on every port.
// (The layout is the machine's own on IIgs/Amiga/ST/X68000; the DOS // (The layout is the machine's own on IIgs/Amiga/ST/X68000, DVX's on
// port translates with a fixed US layout -- see claudeDocs/input.md.) // 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 // The queue is refilled by jlInputPoll() -- no extra bring-up is
// needed. It holds JL_CHAR_QUEUE_SIZE - 1 characters; when full, // needed. It holds JL_CHAR_QUEUE_SIZE - 1 characters; when full,
// further characters are dropped (drop-newest) until the app pops. // further characters are dropped (drop-newest) until the app pops.
// OS auto-repeat may deliver repeated characters on ports whose // A held key repeats its character on every port (the caller manages
// keyboard services repeat (the caller manages repeat policy). // 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 // 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 // jlWaitForAnyKey empties the queue when it returns -- its dismissing
// keystroke is consumed by the wait, not delivered as text. // 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. // "byte or -1" is the interface contract consumers expect.
int jlInputGetChar(void); int jlInputGetChar(void);
// Block until the user presses any key. Internally polls via // Block until the user presses a key: one with a jlKeyE, or one that types
// jlInputPoll, so per-port jlpInputPoll machinery (including // a character (punctuation, which has no jlKeyE). Mouse and joystick
// audio-friendly IRQ-driven samplers) keeps working while the // buttons do not end it, and a key held when the wait starts must be let go
// wait loop runs. // 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); 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 jlKeyDown(jlKeyE key);
bool jlKeyPressed(jlKeyE key); bool jlKeyPressed(jlKeyE key);
bool jlKeyReleased(jlKeyE key); bool jlKeyReleased(jlKeyE key);
// True when the platform confirmed an attached mouse at init (DOS: the INT 33h driver reset; // 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); bool jlMousePresent(void);
int16_t jlMouseX(void); int16_t jlMouseX(void);
int16_t jlMouseY(void); int16_t jlMouseY(void);
@ -161,6 +173,12 @@ bool jlMouseDown(jlMouseButtonE button);
bool jlMousePressed(jlMouseButtonE button); bool jlMousePressed(jlMouseButtonE button);
bool jlMouseReleased(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); bool jlJoystickConnected(jlJoystickE js);
int8_t jlJoystickX(jlJoystickE js); int8_t jlJoystickX(jlJoystickE js);
int8_t jlJoystickY(jlJoystickE js); int8_t jlJoystickY(jlJoystickE js);
@ -168,22 +186,16 @@ bool jlJoyDown(jlJoystickE js, jlJoyButtonE button);
bool jlJoyPressed(jlJoystickE js, jlJoyButtonE button); bool jlJoyPressed(jlJoystickE js, jlJoyButtonE button);
bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button); bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button);
// Re-enable joystick polling and recalibrate the resting (center) // Set the stick's dead zone (8 at jlInit): axis values within `deadZone` of 0 (on the
// position. The IIgs port auto-disables polling after a short window // -127..127 scale) read 0 on the ports with analog sticks (IIgs, DOS,
// of detecting no stick (saves ~3 ms/frame of busy-wait). It does NOT // DVX, hosted); use 0 to disable it. On the IIgs and DOS the call also
// auto-re-probe -- the application must call this function to resume // re-centres the stick: the next reading after it is taken as the
// polling after plugging a stick in. // 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
// The next poll after this call captures the stick's CURRENT raw // seen (an absent stick costs ~1 ms a frame of busy-wait) and does NOT
// position as the new center -- so the user must hold the stick // re-probe by itself: this call resumes polling. DOS probes the game
// centered when calling. Subsequent polls report position relative // port again; the Amiga hands the port back to lowlevel.library's own
// to that center; raw readings within `deadZone` units of the center // detection. Elsewhere only the dead zone changes.
// 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.
void jlJoystickReset(jlJoystickE js, uint8_t deadZone); void jlJoystickReset(jlJoystickE js, uint8_t deadZone);
#endif #endif

View file

@ -7,7 +7,9 @@
// Colors are 12-bit $0RGB (bits 11..8 red, 7..4 green, 3..0 blue). // 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 // The library forces color 0 of every palette to black; jlPaletteSet
// silently masks that entry to $000 regardless of what the caller // 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 #ifndef JOEYLIB_PALETTE_H
#define JOEYLIB_PALETTE_H #define JOEYLIB_PALETTE_H
@ -20,7 +22,9 @@
// uint16_t values in $0RGB format. // uint16_t values in $0RGB format.
void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16); 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); void jlPaletteGet(const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
// Assign a single palette index (0..15) to a single scanline (0..199). // Assign a single palette index (0..15) to a single scanline (0..199).

View file

@ -71,88 +71,96 @@
// ----- Derived CPU markers ----- // ----- Derived CPU markers -----
#if defined(JOEYLIB_PLATFORM_IIGS) #if defined(JOEYLIB_PLATFORM_IIGS)
#define JOEYLIB_CPU_65816 #define JL_CPU_65816
#define JOEYLIB_ENDIAN_LITTLE #define JL_ENDIAN_LITTLE
#define JOEYLIB_NATIVE_CHUNKY #define JL_NATIVE_CHUNKY
#define JOEYLIB_PLATFORM_NAME "Apple IIgs" #define JL_PLATFORM_NAME "Apple IIgs"
#elif defined(JOEYLIB_PLATFORM_AMIGA) #elif defined(JOEYLIB_PLATFORM_AMIGA)
#define JOEYLIB_CPU_68000 #define JL_CPU_68000
#define JOEYLIB_ENDIAN_BIG #define JL_ENDIAN_BIG
#define JOEYLIB_NATIVE_PLANAR #define JL_NATIVE_PLANAR
#define JOEYLIB_HAS_BLITTER #define JL_HAS_BLITTER
#define JOEYLIB_HAS_COPPER #define JL_HAS_COPPER
#define JOEYLIB_PLATFORM_NAME "Commodore Amiga" #define JL_PLATFORM_NAME "Commodore Amiga"
#elif defined(JOEYLIB_PLATFORM_ATARIST) #elif defined(JOEYLIB_PLATFORM_ATARIST)
#define JOEYLIB_CPU_68000 #define JL_CPU_68000
#define JOEYLIB_ENDIAN_BIG #define JL_ENDIAN_BIG
#define JOEYLIB_NATIVE_PLANAR #define JL_NATIVE_PLANAR
// JOEYLIB_ATARI_STE is the STe build (make ste): the same port using the // 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) #if defined(JOEYLIB_ATARI_STE)
#define JOEYLIB_PLATFORM_NAME "Atari STe" #define JL_PLATFORM_NAME "Atari STe"
#else #else
#define JOEYLIB_PLATFORM_NAME "Atari ST" #define JL_PLATFORM_NAME "Atari ST"
#endif #endif
#elif defined(JOEYLIB_PLATFORM_DOS) #elif defined(JOEYLIB_PLATFORM_DOS)
#define JOEYLIB_CPU_X86 #define JL_CPU_X86
#define JOEYLIB_ENDIAN_LITTLE #define JL_ENDIAN_LITTLE
#define JOEYLIB_NATIVE_CHUNKY #define JL_NATIVE_CHUNKY
#define JOEYLIB_PLATFORM_NAME "MS-DOS" #define JL_PLATFORM_NAME "MS-DOS"
#elif defined(JOEYLIB_PLATFORM_X68000) #elif defined(JOEYLIB_PLATFORM_X68000)
// The stage is four separate 1bpp planes in main RAM (src/x68000/halPlanar.c, // 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 // 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 // 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 // 16-colour palette, so per-line SCB palettes are reloaded as the beam scans
// by a CRTC raster interrupt (src/x68000/x68kRasterIsr.s). // by a CRTC raster interrupt (src/x68000/x68kRasterIsr.s).
#define JOEYLIB_CPU_68000 #define JL_CPU_68000
#define JOEYLIB_ENDIAN_BIG #define JL_ENDIAN_BIG
#define JOEYLIB_NATIVE_PLANAR #define JL_NATIVE_PLANAR
#define JOEYLIB_PLATFORM_NAME "Sharp X68000" #define JL_PLATFORM_NAME "Sharp X68000"
#elif defined(JOEYLIB_PLATFORM_DVX) #elif defined(JOEYLIB_PLATFORM_DVX)
// The DOS stage (chunky 4bpp, x86 sprite JIT and tile walker from // 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 // src/x86/), shown in a DVX window, or on DVX's full screen, instead of
// on the VGA. // on the VGA.
#define JOEYLIB_CPU_X86 #define JL_CPU_X86
#define JOEYLIB_ENDIAN_LITTLE #define JL_ENDIAN_LITTLE
#define JOEYLIB_NATIVE_CHUNKY #define JL_NATIVE_CHUNKY
#define JOEYLIB_PLATFORM_NAME "DVX" #define JL_PLATFORM_NAME "DVX"
#elif defined(JOEYLIB_PLATFORM_HOSTED) #elif defined(JOEYLIB_PLATFORM_HOSTED)
// The generic chunky stage, resolved through its SCB palettes into a // The generic chunky stage, resolved through its SCB palettes into a
// window's RGB pixels by one backend (src/hosted/<backend>). Whatever CPU // 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__ #if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
#define JOEYLIB_ENDIAN_BIG #define JL_ENDIAN_BIG
#else #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 #endif
#define JOEYLIB_NATIVE_CHUNKY
#define JOEYLIB_SPRITE_INTERPRETED
#if defined(JOEYLIB_HOSTED_SDL3) && defined(__PSP__) #if defined(JOEYLIB_HOSTED_SDL3) && defined(__PSP__)
#define JOEYLIB_PLATFORM_NAME "PSP" #define JL_PLATFORM_NAME "PSP"
#elif defined(JOEYLIB_HOSTED_SDL3) #elif defined(JOEYLIB_HOSTED_SDL3)
#define JOEYLIB_PLATFORM_NAME "SDL3" #define JL_PLATFORM_NAME "SDL3"
#elif defined(JOEYLIB_HOSTED_WIN32) #elif defined(JOEYLIB_HOSTED_WIN32)
#define JOEYLIB_PLATFORM_NAME "Windows" #define JL_PLATFORM_NAME "Windows"
#elif defined(JOEYLIB_HOSTED_WIN16) #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__) #elif defined(JOEYLIB_HOSTED_BE) && defined(__HAIKU__)
#define JOEYLIB_PLATFORM_NAME "Haiku" #define JL_PLATFORM_NAME "Haiku"
#elif defined(JOEYLIB_HOSTED_BE) #elif defined(JOEYLIB_HOSTED_BE)
#define JOEYLIB_PLATFORM_NAME "BeOS" #define JL_PLATFORM_NAME "BeOS"
#else #else
#error "JoeyLib: JOEYLIB_PLATFORM_HOSTED needs a backend, e.g. -DJOEYLIB_HOSTED_SDL3" #error "JoeyLib: JOEYLIB_PLATFORM_HOSTED needs a backend, e.g. -DJOEYLIB_HOSTED_SDL3"
#endif #endif
#elif defined(JOEYLIB_PLATFORM_BLANK) #elif defined(JOEYLIB_PLATFORM_BLANK)
// Chunky little-endian is the simplest baseline for a new port: the generic // 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 // backend gives you fully-working software rendering out of the box. Adjust
// these (or switch to JOEYLIB_NATIVE_PLANAR) once you know your hardware. // these (or switch to JL_NATIVE_PLANAR) once you know your hardware.
#define JOEYLIB_ENDIAN_LITTLE #define JL_ENDIAN_LITTLE
#define JOEYLIB_NATIVE_CHUNKY #define JL_NATIVE_CHUNKY
#define JOEYLIB_SPRITE_INTERPRETED #define JL_SPRITE_INTERPRETED
#define JOEYLIB_PLATFORM_NAME "Blank Template" #define JL_PLATFORM_NAME "Blank Template"
#endif #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 // src/generic/spriteEmitStub.c), so every sprite is drawn by the
// interpreter. A pre-compiled bank (.spc) still gives it the cels' pixels; // interpreter. A pre-compiled bank (.spc) still gives it the cels' pixels;
// their machine code is skipped. // their machine code is skipped.
@ -193,7 +201,7 @@
#define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function #define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function
// (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord, // (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord,
// the VBL heartbeat tick read -- no function override to register here.) // 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 // audio: every real port implements the full engine
#define JL_HAS_AUDIO_INIT #define JL_HAS_AUDIO_INIT
#define JL_HAS_AUDIO_SHUTDOWN #define JL_HAS_AUDIO_SHUTDOWN
@ -205,7 +213,6 @@
#define JL_HAS_AUDIO_PLAY_SFX_STREAM_RAW #define JL_HAS_AUDIO_PLAY_SFX_STREAM_RAW
#define JL_HAS_AUDIO_SET_PAN #define JL_HAS_AUDIO_SET_PAN
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER #define JL_HAS_AUDIO_TICK_REGISTER
@ -233,7 +240,8 @@
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts). // Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
#define JL_HAS_SERIAL_SET_RTS #define JL_HAS_SERIAL_SET_RTS
// save files + disk space: delete (GS/OS Destroy), dir-ensure (GS/OS Create // 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_DIR_ENSURE
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
#define JL_HAS_DISK_FREE #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_PIXELS // amiga: NULL (planar storage), function
#define JL_HAS_SURFACE_ALLOC_PORT_DATA // amiga AmigaPlanarT + chip planes, 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_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_FRAME_COUNT // VBL-server counter, function
#define JL_HAS_WAIT_VBL // WaitTOF, function #define JL_HAS_WAIT_VBL // WaitTOF, function
// audio: every real port implements the full engine // audio: every real port implements the full engine
@ -280,7 +288,6 @@
#define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_SET_PAN #define JL_HAS_AUDIO_SET_PAN
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER #define JL_HAS_AUDIO_TICK_REGISTER
@ -305,6 +312,8 @@
#define JL_HAS_SERIAL_FLUSH #define JL_HAS_SERIAL_FLUSH
// Application-driven DTR: how a program hangs up (jlSerialSetDtr). // Application-driven DTR: how a program hangs up (jlSerialSetDtr).
#define JL_HAS_SERIAL_SET_DTR #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) // save files + disk space (CreateDir/DeleteFile/Info via dos.library)
#define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE #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_PIXELS // st: NULL (planar storage), function
#define JL_HAS_SURFACE_ALLOC_PORT_DATA // st StPlanarT + interleaved buffer, 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_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_FRAME_COUNT // VBL-ISR counter, function
#define JL_HAS_WAIT_VBL // frame-counter wait, function #define JL_HAS_WAIT_VBL // frame-counter wait, function
#define JL_HAS_MILLIS_ELAPSED // Supexec 200 Hz tick, 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
#define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER #define JL_HAS_AUDIO_TICK_REGISTER
#define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK #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) // lifecycle / present / input (every real port implements)
#define JL_HAS_INIT #define JL_HAS_INIT
#define JL_HAS_SHUTDOWN #define JL_HAS_SHUTDOWN
@ -382,6 +391,8 @@
#define JL_HAS_SERIAL_FLUSH #define JL_HAS_SERIAL_FLUSH
// Application-driven DTR: how a program hangs up (jlSerialSetDtr). // Application-driven DTR: how a program hangs up (jlSerialSetDtr).
#define JL_HAS_SERIAL_SET_DTR #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) // save files + disk space (POSIX mkdir/remove + GEMDOS Dfree)
#define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
@ -395,7 +406,7 @@
// (The planar dual-write hooks are not linked to generics: every chunky port // (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.) // 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_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_WAIT_VBL // $3DA retrace poll, function
#define JL_HAS_MILLIS_ELAPSED // uclock / audio-ISR tick, function #define JL_HAS_MILLIS_ELAPSED // uclock / audio-ISR tick, function
// audio: every real port implements the full engine // audio: every real port implements the full engine
@ -407,7 +418,6 @@
#define JL_HAS_AUDIO_PLAY_SFX #define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER #define JL_HAS_AUDIO_TICK_REGISTER
@ -439,11 +449,9 @@
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
#define JL_HAS_DISK_FREE #define JL_HAS_DISK_FREE
#elif defined(JOEYLIB_PLATFORM_X68000) #elif defined(JOEYLIB_PLATFORM_X68000)
// Bring-up: platform SERVICES only (src/x68000/hal.c), plus the serial // Platform services in src/x68000/hal.c, the serial add-on in
// add-on (src/x68000/serial.c). Everything graphical comes from the portable // src/x68000/serial.c, and planar graphics adapted from the Amiga
// generics, so the port renders correctly before any native primitive // (identical surface layout, src/x68000/halPlanar.c).
// exists. Native ops arrive with the storage-model decision.
// Planar graphics, adapted from the Amiga (identical surface layout).
#define JL_HAS_SURFACE_CLEAR #define JL_HAS_SURFACE_CLEAR
#define JL_HAS_DRAW_PIXEL #define JL_HAS_DRAW_PIXEL
#define JL_HAS_DRAW_LINE #define JL_HAS_DRAW_LINE
@ -474,7 +482,7 @@
#define JL_HAS_SURFACE_FREE_PORT_DATA #define JL_HAS_SURFACE_FREE_PORT_DATA
#define JL_HAS_INIT // _iocs_crtmod display bring-up #define JL_HAS_INIT // _iocs_crtmod display bring-up
#define JL_HAS_SHUTDOWN // restore the previous CRT mode #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_INIT
#define JL_HAS_INPUT_SHUTDOWN #define JL_HAS_INPUT_SHUTDOWN
#define JL_HAS_INPUT_POLL // IOCS work area keys/mouse, PPI joystick ports #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_DISK_FREE
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
#define JL_HAS_SAVE_DIR_ENSURE #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_OPEN
#define JL_HAS_SERIAL_CLOSE #define JL_HAS_SERIAL_CLOSE
#define JL_HAS_SERIAL_POLL #define JL_HAS_SERIAL_POLL
@ -499,17 +508,21 @@
#define JL_HAS_SERIAL_FLUSH #define JL_HAS_SERIAL_FLUSH
// Application-driven DTR: how a program hangs up (jlSerialSetDtr). // Application-driven DTR: how a program hangs up (jlSerialSetDtr).
#define JL_HAS_SERIAL_SET_DTR #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 // Audio. CHIP side on the YM2151 (OPM): tone voices + noise, which is what
// the portable JYM1 tracker rides, with no CPU mixing. // the portable JYM1 tracker rides, with no CPU mixing.
#define JL_HAS_AUDIO_INIT #define JL_HAS_AUDIO_INIT
#define JL_HAS_AUDIO_SHUTDOWN #define JL_HAS_AUDIO_SHUTDOWN
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER
#define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_ENTER
#define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK #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 // overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate
// device from the OPM, so chip music and digital audio do not contend. // device from the OPM, so chip music and digital audio do not contend.
#define JL_HAS_AUDIO_PLAY_MOD #define JL_HAS_AUDIO_PLAY_MOD
@ -554,7 +567,6 @@
#define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_SET_PAN #define JL_HAS_AUDIO_SET_PAN
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER #define JL_HAS_AUDIO_TICK_REGISTER
@ -591,10 +603,10 @@
#define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
#define JL_HAS_DISK_FREE #define JL_HAS_DISK_FREE
// audio: a software mixer (MOD through libxmp-lite, the SFX slots, three // audio: a software mixer (MOD through the module player, the SFX slots, three
// square-wave voices, YM-style noise and the speaker tone) run on the // square-wave voices and YM-style noise) run on the program's thread, so
// program's thread, so the tick callback and the stream fills run there // the tick callback and the stream fills run there too and the critical
// too and the critical sections stay the generic no-ops. // sections stay the generic no-ops.
#define JL_HAS_AUDIO_INIT #define JL_HAS_AUDIO_INIT
#define JL_HAS_AUDIO_SHUTDOWN #define JL_HAS_AUDIO_SHUTDOWN
#define JL_HAS_AUDIO_PLAY_MOD #define JL_HAS_AUDIO_PLAY_MOD
@ -604,7 +616,6 @@
#define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_SET_PAN #define JL_HAS_AUDIO_SET_PAN
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_TONE
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_NOISE
#define JL_HAS_AUDIO_TICK_REGISTER #define JL_HAS_AUDIO_TICK_REGISTER
@ -646,9 +657,9 @@
// ----- Library version ----- // ----- Library version -----
#define JOEYLIB_VERSION_MAJOR 1 #define JL_VERSION_MAJOR 1
#define JOEYLIB_VERSION_MINOR 0 #define JL_VERSION_MINOR 0
#define JOEYLIB_VERSION_PATCH 0 #define JL_VERSION_PATCH 0
#define JOEYLIB_VERSION_STRING "1.0.0" #define JL_VERSION_STRING "1.0.0"
#endif #endif

View file

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

View file

@ -18,14 +18,17 @@
// Atari ST -- BIOS Aux (Modem port) via XBIOS Rsconf; TOS buffers RX. // Atari ST -- BIOS Aux (Modem port) via XBIOS Rsconf; TOS buffers RX.
// X68000 -- SCC channel A: Human68k's RSDRV.SYS for transmit and line // X68000 -- SCC channel A: Human68k's RSDRV.SYS for transmit and line
// setup (75-38400 bps, RTS/CTS), REQUIRED -- the open fails // 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 // 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); // ring (a hardware IRQ on DOS, the X68000 and the IIgs, the OS on Amiga/ST,
// jlSerialRead / jlSerialAvailable never block. Call jlSerialPoll() // DVX, and Windows); jlSerialRead / jlSerialAvailable never block. Call
// once per game frame -- it is a no-op where the IRQ/OS already fills the // jlSerialPoll() once per game frame. Transmit never waits long: a bounded
// ring, and the hardware drain where it does not, so calling it every frame is // poll on most ports, a queue on the Amiga and Win16.
// always correct. Transmit is a bounded poll.
// //
// Received bytes CAN be lost. Each port's receive hardware holds 1 to 3 bytes // 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 // (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 // 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 // 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 // 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 // jlInit, which reads the audio replayer), IIgs jlAudioPlayMod (~12 ms per
// Card above 2400 baud, and the program's own interrupt masking. Disk access // KB of samples), a Super Serial Card above 2400 baud, and the program's own
// on the other ports is unmeasured. Schedule them between messages, pause the // interrupt masking. Disk access on the other ports is unmeasured. Schedule
// peer first (jlSerialSetRts(false) or XOFF), or use a protocol that // them between messages, pause the peer first (jlSerialSetRts(false) or
// retransmits. docs/ApiSerial.adoc, "What Can Lose Received Bytes", has the // XOFF), or use a protocol that retransmits. docs/ApiSerial.adoc, "What Can
// per-port table. // Lose Received Bytes", has the per-port table.
// //
// A failed jlSerialOpen is non-fatal: it returns false and every other call is // 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. // 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 // JL_SERIAL_PRINTER -- IIgs printer port (SCC channel A). Only the IIgs has
// a second built-in port; elsewhere == DEFAULT. // a second built-in port; elsewhere == DEFAULT.
// JL_SERIAL_SLOT -- a card/port selected by jlSerialConfigT.unit: IIgs // JL_SERIAL_SLOT -- a card/port selected by jlSerialConfigT.unit: IIgs
// Super Serial Card slot (1..7); DOS COM number (1..4); // Super Serial Card slot (1..7); DOS or DVX COM number
// Amiga serial.device unit. // (1..4); Win16 COM (1..9); Win32 COM (1..255); Amiga
// JL_SERIAL_DEFAULT -- the platform's natural RS-232 port (IIgs modem, DOS // serial.device unit. The ST and X68000 have one port
// COM1, ST Modem port, Amiga unit 0). // 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 { typedef enum {
JL_SERIAL_DEFAULT = 0, JL_SERIAL_DEFAULT = 0,
JL_SERIAL_MODEM, JL_SERIAL_MODEM,
@ -78,24 +84,28 @@ typedef enum {
} jlSerialFlowE; } jlSerialFlowE;
// Line configuration. `unit` disambiguates JL_SERIAL_SLOT (see jlSerialDeviceE) // 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 { 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 dataBits; // 5 .. 8
uint8_t stopBits; // 1 or 2 uint8_t stopBits; // 1 or 2
jlSerialParityE parity; jlSerialParityE parity;
jlSerialFlowE flow; 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; } jlSerialConfigT;
// Open + configure the port. Only one port is open at a time; a second // 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 // jlSerialOpen without a jlSerialClose fails. Returns false (non-fatal) if the
// port cannot be opened or the platform has no serial support -- every other // 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, // call then no-ops. It also fails, with jlLastError() saying why, when no UART
// and jlLastError() says so. // 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); bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config);
// Close the open port and release its resources. Safe to call when not open. // 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 // A port still open when the program exits is closed then (the first
// successful jlSerialOpen registers an atexit handler): where the port's RX // successful jlSerialOpen registers an atexit handler): where the port's RX
// interrupt is taken over (DOS, X68000, IIgs), the vector points into the // 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. // (Hayes S25: 50 ms by default), then close: the line stays down.
void jlSerialClose(void); void jlSerialClose(void);
// Pump the RX/TX buffers. Call once per game frame. Next to a no-op where a // Pump the RX/TX buffers. Call once per game frame.
// 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.
void jlSerialPoll(void); void jlSerialPoll(void);
// Bytes waiting in the RX buffer, readable now without blocking. // 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); uint16_t jlSerialRead(uint8_t *buf, uint16_t max);
// Send `len` bytes; returns the count accepted (may be < len if a flow- // 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); uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len);
// Single-byte convenience. jlSerialReadByte returns -1 when none is ready. // 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 // Performance contract: jlSpriteDraw should be at least as fast as the
// IIgs reference (the DESIGN.md spec calls for runtime-compiled draw // IIgs reference (the DESIGN.md spec calls for runtime-compiled draw
// code per CPU). Every port has a runtime emitter (DOS x86, Amiga/ST/ // code per CPU). The machine ports have runtime emitters (IIgs 65816,
// X68000 68k planar, IIgs 65816); jlSpriteCompile compiles the shift/op // Amiga/X68000 68k planar, ST 68k interleaved, DOS/DVX x86), and so does
// variants into the codegen arena and the draw/save/restore calls // Win16 (x86, as DOS);
// dispatch to the compiled routines, falling back to the interpreter // jlSpriteCompile compiles the shift/op variants into the codegen arena
// for clipped draws or uncompiled variants. // and the draw/save/restore calls dispatch to the compiled routines,
// jlSpritePrewarm compiles ahead of the first draw so the emit cost // falling back to the interpreter for clipped draws or uncompiled
// does not land mid-frame. // 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 #ifndef JOEYLIB_SPRITE_H
#define JOEYLIB_SPRITE_H #define JOEYLIB_SPRITE_H
@ -69,8 +71,7 @@ typedef struct {
// Returns the number of cels actually written into outCels[]. Returns // Returns the number of cels actually written into outCels[]. Returns
// 0 if the file is missing, the magic / target byte is wrong, or no // 0 if the file is missing, the magic / target byte is wrong, or no
// cel could be allocated. // cel could be allocated.
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette);
uint16_t maxCels, uint16_t *outPalette);
// Load up to maxCels cels from a baked .spc PRE-COMPILED sprite bank // Load up to maxCels cels from a baked .spc PRE-COMPILED sprite bank
// (tools/spritebake output for THIS platform). Identical contract to // (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 // full compiled speed with NO runtime jlSpriteCompile. A cel that does not fit
// the arena is loaded uncompiled (interpreter fallback), never an error -- so // the arena is loaded uncompiled (interpreter fallback), never an error -- so
// this is a drop-in, faster-starting replacement for jlSpriteBankLoad + a // this is a drop-in, faster-starting replacement for jlSpriteBankLoad + a
// jlSpriteCompile/Prewarm pass. Returns 0 if the file is missing or its magic // jlSpriteCompile pass. Returns 0 if the file is missing, its magic /
// / target / shift-count does not match this build. // target / shift-count does not match this build, its cel size is out of
uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, // range, or the 64 KB buffer a cel's code is read into cannot be allocated
uint16_t maxCels, uint16_t *outPalette); // (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 // Write an already-compiled sprite bank to a .spc cache file (the runtime twin
// of the offline tools/spritebake). Call after jlSpriteBankLoad + a // of the offline tools/spritebake). Call after jlSpriteBankLoad + a
// jlSpriteCompile pass so a later launch can jlSpriteBankLoadPrecompiled the // jlSpriteCompile pass so a later launch can jlSpriteBankLoadPrecompiled the
// cache and skip the JIT -- i.e. SHIP the small cross-platform .spr, compile // 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` // 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 // are `count` sprites, all of one size (uncompiled ones are cached
// run); `palette` (or NULL) is embedded. Returns false if the destination // uncompiled, and nothing compiles them at load: jlSpriteCompile any whose
// isn't writable (e.g. a write-protected disk), which is harmless -- the app // jlSpriteCompiledSize is 0); `palette` (or NULL) is embedded. Returns false
// just keeps compiling at startup. // if the cels differ in size or the destination isn't writable (e.g. a
bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, // write-protected disk), which is harmless -- the app just keeps compiling
uint16_t count, const uint16_t *palette); // 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 // 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 // jlSpriteT; we do not copy it. Returns NULL if tileData is NULL,
// heightTiles is 0, or if the codegen arena cannot fit a placeholder // widthTiles or heightTiles is 0, or memory runs out. The codegen arena
// entry for this sprite. // is not touched until jlSpriteCompile.
jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles); jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles);
// Release a jlSpriteT and any codegen entries cached for it. The tile // 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. // owns that buffer.
void jlSpriteDestroy(jlSpriteT *sp); void jlSpriteDestroy(jlSpriteT *sp);
// Compile the sprite's draw routines into the codegen arena. After // Compile the sprite's draw, save, and restore routines into the codegen
// this returns true, jlSpriteDraw uses the compiled fast path on // arena. jlSpriteDraw never compiles on demand, so call this while
// platforms where the emitter is wired (all four ports). Returns // loading (or load a pre-compiled bank). After it returns true the draw,
// false if the arena is full (caller may run jlSpriteCompact and // save, and restore calls use the compiled routines that fit: a short
// retry), the platform doesn't have a real emitter yet, or the // arena keeps only the x-on-an-8-pixel-boundary draws on the Amiga, ST,
// sprite has no source tile data. // 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 // Idempotent: calling on a sprite that's already compiled is a
// no-op and returns true. // no-op and returns true.
@ -125,12 +133,6 @@ bool jlSpriteCompile(jlSpriteT *sp);
// the actual per-cel cost of the current platform's emitter. // the actual per-cel cost of the current platform's emitter.
uint32_t jlSpriteCompiledSize(const jlSpriteT *sp); 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 // Draw the sprite at pixel (x,y) on the destination surface. Pixels
// equal to color 0 in the sprite source are skipped (transparent). // equal to color 0 in the sprite source are skipped (transparent).
// Off-surface portions are clipped. // 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. // sprites and on ports without a compiled fast path.
void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
// Worst-case backup capacity for a widthTiles x heightTiles sprite at // How many _type elements a backup buffer needs for a widthTiles x
// ANY x, on every port. The planar 68k compiled save/restore operates // heightTiles sprite at ANY x, on every port (rounded up), so that
// 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 // static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)];
// 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 // is big enough, and jlSpriteBackupLength(uint8_t, w, h) is the size in
// alignment (e.g. uint16_t backing or __attribute__((aligned(2)))) -- // bytes (for jlAlloc, say). The worst case is the 16-pixel-group-aligned
// odd buffer pointers route save/restore to the slower interpreted // region covering the sprite (x rounded DOWN to a 16-px boundary, width
// paths on planar ports (68000 word ops require even addresses). // UP): one extra 16-px group, (ceil(w/16) + 1) * 8 bytes per row. Give
#define JOEY_SPRITE_BACKUP_BYTES(_wT, _hT) \ // the buffer 2-byte alignment (a uint16_t array has it): on the Amiga,
((uint16_t)((((((uint16_t)(_wT) * 8u) + 15u) >> 4) + 1u) * 8u * ((uint16_t)(_hT) * 8u))) // 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 // Capture the destination region a subsequent jlSpriteDraw at the same
// (x,y) would write to. backup->bytes must have at least // (x,y) would write to. backup->bytes must have room for
// JOEY_SPRITE_BACKUP_BYTES(widthTiles, heightTiles) bytes of capacity // jlSpriteBackupLength(uint8_t, widthTiles, heightTiles) bytes for
// for fully in-bounds draws; for clipped draws only the visible bytes // fully in-bounds draws; for clipped draws only the visible bytes are
// are stored. The captured region's exact geometry is reported in // stored. The captured region's exact geometry is reported in
// backup->x/width/height/sizeBytes -- on planar ports (Amiga, ST) it is // backup->x/y/width/height/sizeBytes: x is rounded down to even on every
// the 16-px-aligned window containing the sprite, so the region a // port, a compiled save on the Amiga, ST, and X68000 takes the 16-px-
// caller must not invalidate before restoring is the RECORDED window, // aligned region containing the sprite, a compiled save on the IIgs only
// not just the sprite rect. // 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); void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
// TRUSTED save-under (P7-1). Same trust contract as // 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, // * s and backup (and backup->sprite, backup->bytes) are non-NULL,
// * backup was produced by a jlSpriteSaveUnder that took the COMPILED // * backup was produced by a jlSpriteSaveUnder that took the COMPILED
// path (a "trusted" backup: 16-px-aligned x/width on planar ports, // path (a "trusted" backup: 16-px-aligned x/width on planar ports,
// even x/width on chunky, backup->height == the sprite's pixel // even x/width on chunky, backup->height == the rows the compiled
// height, buffer at least 2-byte aligned), // save covers -- the sprite's pixel height, or on the IIgs the rows
// * the recorded window is fully on the destination surface. // 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 // 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 // use jlSpriteRestoreUnder anywhere those invariants are not statically
// guaranteed. Anything the trusted fast path does not recognize (an // 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 // falls back to the fully-validated jlSpriteRestoreUnder, so a
// conservative caller degrades safely rather than corrupting -- but the // conservative caller degrades safely rather than corrupting -- but the
// NULL / on-surface guarantees above are still the caller's. // 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 // the source surface can be modified afterwards. Width and height
// are in TILES (each tile = 8x8 pixels). x and y are in pixels and // 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 // must be aligned to a tile boundary (multiple of 8) on the source
// surface; misaligned coordinates return NULL. // surface. Returns NULL if src is NULL, a dimension is 0, x or y is
jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y, // misaligned, the region is not wholly on the surface, or memory runs
uint8_t widthTiles, uint8_t heightTiles); // 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 // Defragment the codegen arena. Walks live sprite slots and
// memmoves them down to consolidate free space; any holes left by // 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 // Arena introspection. Used to gauge whether jlSpriteCompact is
// worth running, or whether the codegenBytes budget needs to grow. // worth running, or whether the codegenBytes budget needs to grow.
// Free space is (Total - Used), but it may be fragmented across // 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 jlSpriteCodegenBytesUsed(void);
uint32_t jlSpriteCodegenBytesTotal(void); uint32_t jlSpriteCodegenBytesTotal(void);

View file

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

View file

@ -33,21 +33,23 @@
// ----- Constants ----- // ----- Constants -----
#define TILE_PIXELS_PER_SIDE 8 #define JL_TILE_PIXELS_PER_SIDE 8
#define TILE_BYTES_PER_ROW 4 #define JL_TILE_BYTES_PER_ROW 4
#define TILE_BYTES (TILE_BYTES_PER_ROW * TILE_PIXELS_PER_SIDE) #define JL_TILE_BYTES (JL_TILE_BYTES_PER_ROW * JL_TILE_PIXELS_PER_SIDE)
#define TILE_BLOCKS_PER_ROW (SURFACE_WIDTH / TILE_PIXELS_PER_SIDE) #define JL_TILE_BLOCKS_PER_ROW (JL_SURFACE_WIDTH / JL_TILE_PIXELS_PER_SIDE)
#define TILE_BLOCKS_PER_COL (SURFACE_HEIGHT / 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 // Sentinel for asciiMap entries that should not draw. jlDrawText
// advances the cursor past TILE_NO_GLYPH chars without writing. // advances the cursor past JL_TILE_NO_GLYPH chars without writing.
#define TILE_NO_GLYPH ((uint16_t)0xFFFFu) #define JL_TILE_NO_GLYPH ((uint16_t)0xFFFFu)
// ----- Types ----- // ----- Types -----
// Stack-allocated 32-byte snapshot buffer for jlTileSnap / jlTilePaste. // 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 { typedef struct jlTileT {
uint8_t pixels[TILE_BYTES]; uint8_t pixels[JL_TILE_BYTES];
} jlTileT; } jlTileT;
// ----- API ----- // ----- API -----
@ -55,17 +57,14 @@ typedef struct jlTileT {
// Copy the 8x8 block at (srcBx, srcBy) on src to (dstBx, dstBy) on // Copy the 8x8 block at (srcBx, srcBy) on src to (dstBx, dstBy) on
// dst, opaque. Out-of-range block coordinates on either side are // dst, opaque. Out-of-range block coordinates on either side are
// silent no-ops; src and dst can be the same surface. // silent no-ops; src and dst can be the same surface.
void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
// Like jlTileCopy but pixels equal to transparentIndex are skipped -- // Like jlTileCopy but pixels equal to transparentIndex are skipped --
// the destination pixel keeps its original value. Use this for fonts // the destination pixel keeps its original value. Use this for fonts
// (transparentIndex = 0 leaves the page background showing through // (transparentIndex = 0 leaves the page background showing through
// glyph backgrounds) and for any tileset where some pixels are meant // glyph backgrounds) and for any tileset where some pixels are meant
// to be see-through. // to be see-through.
void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex);
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 // 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 // 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 // Returns the number of tiles actually written. Returns 0 if the
// file is missing, the magic / target byte is wrong, or maxTiles is 0. // file is missing, the magic / target byte is wrong, or maxTiles is 0.
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles, uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles, bool *outValid, uint16_t *outPalette);
bool *outValid, uint16_t *outPalette);
// Stream a baked .tbk straight onto a surface's 8x8 block grid: tile i // 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 // the same row-major layout the bake tools author against. Use this
// instead of jlTileBankLoad + a jlTileT[] when the bank is too big for // instead of jlTileBankLoad + a jlTileT[] when the bank is too big for
// a caller-side buffer -- a full 1000-tile font sheet needs 32000 // a caller-side buffer -- a full 1000-tile font sheet needs 32000
// bytes, which no IIgs example can heap-allocate (the clang libc heap // bytes, which no IIgs example can afford from the C heap (32 KB to
// is a few hundred bytes) or hold in bank-0 BSS. One file pass with a // 48 KB in all) or hold in bank-0 BSS. One file pass with a
// small internal scratch; per-target/magic validation and outPalette // small internal scratch; per-target/magic validation and outPalette
// behave exactly like jlTileBankLoad. Blocks beyond the file's tile // 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); uint16_t jlTileBankLoadToSurface(const char *path, jlSurfaceT *dst, uint16_t *outPalette);
// Capture the 8x8 block at (bx, by) into the caller's jlTileT. Used // 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 // to 4 bits. Opaque -- if you need transparent background, do
// jlTileFill(bg) first then mask separately, or follow up with a // jlTileFill(bg) first then mask separately, or follow up with a
// jlTileCopyMasked call. // jlTileCopyMasked call.
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor);
const jlTileT *in, uint8_t fgColor, uint8_t bgColor);
// Paste an 8x8 ONE-BIT-PER-PIXEL glyph at block (bx, by): bits[row] is // 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 // 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 // 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 // C64 charset, anything 1bpp -- so nothing has to expand it into a
// jlTileT first. Prefer it over jlTilePasteMono for glyphs: a jlTileT // 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 // 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 // Taking the bits directly makes the input unambiguous, and lets each
// port expand them once instead of twice. // port expand them once instead of twice.
void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor);
const uint8_t *bits, uint8_t fgColor, uint8_t bgColor);
// Draw a NUL-terminated ASCII string at block (bx, by) using glyphs // 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 // asciiMap is a 256-entry table mapping ASCII code to glyph location
// on fontSurface, encoded as a packed uint16_t: low byte = source // on fontSurface, encoded as a packed uint16_t: low byte = source
// blockX, high byte = source blockY. asciiMap[c] == TILE_NO_GLYPH // blockX, high byte = source blockY. asciiMap[c] == JL_TILE_NO_GLYPH,
// causes that character to be skipped (cursor advances, nothing // or an entry off the 40x25 grid, causes that character to be skipped
// drawn). Glyph color 0 is treated as transparent so the underlying // (cursor advances, nothing drawn). Glyph color 0 is treated as
// surface shows through glyph backgrounds. // transparent so the underlying surface shows through glyph backgrounds.
// //
// The cursor wraps to the next row at the right edge and truncates // The cursor wraps to the next row at the right edge and truncates
// at the bottom edge. // at the bottom edge. A start off the grid uses up characters without
void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, // drawing them: a start column past 39 drops the first character and
const jlSurfaceT *fontSurface, const uint16_t *asciiMap, // carries on at the start of the next row; a start row past 24 draws
const char *str); // 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 // 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. // 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 // Paste a wTiles x hTiles rectangle of tiles in ONE call: the
// validation, dispatch, and dirty union are paid once for the whole // validation, dispatch, and dirty union are paid once for the whole
// map instead of per tile (the jlDrawText batching pattern // map instead of per tile (the jlDrawText batching pattern
// generalized, NATIVE-PERF Phase 3). `tiles` is the tileset; `map` // generalized, NATIVE-PERF Phase 3). `tiles` is the tileset; `map`
// holds wTiles * hTiles tileset indices in row-major order // 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 // cell untouched; any other out-of-tileset index is the CALLER'S
// contract to avoid (the batch does not know the tileset length and // contract to avoid (the batch does not know the tileset length and
// does not range-check it). The whole rectangle must fit on the // 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 // rejected as a no-op, matching the per-tile ops' silent-reject
// convention. wTiles or hTiles of 0 is a no-op. // 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); 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
# #
# EmuTOS 1.4 (GPL): a free TOS replacement for the Atari ST emulator, the # 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 # 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)" compDescription="Atari ST firmware: EmuTOS 1.4 (free TOS replacement)"
compGroups="emulator:atarist" compGroups="emulator:atarist"
@ -14,27 +16,31 @@ compUrl="https://sourceforge.net/projects/emutos/files/emutos/1.4/emutos-512k-1.
compSha256="1ef7bd25f61bcfc66d19debc2f0ebb0d5e5ba811ccd7f0fddfcb105bb72d1663" compSha256="1ef7bd25f61bcfc66d19debc2f0ebb0d5e5ba811ccd7f0fddfcb105bb72d1663"
compPin="$compSha256" compPin="$compSha256"
imgPath="$JOEY_FIRMWARE/atarist/emutos-512k.img" imgPath="$JOEY_FIRMWARE/atarist/emutos-512k.img"
imgPathDe="$JOEY_FIRMWARE/atarist/emutos-512k-de.img"
compInstall() { compInstall() {
local tmp local tmp
tmp="$(freshBuildDir "$compId")" 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/etos512us.img" "$imgPath"
install -D -m 644 "$tmp/emutos-512k-1.4/etos512de.img" "$imgPathDe"
dropBuildDir "$compId" dropBuildDir "$compId"
} }
compInstalled() { compInstalled() {
[ -f "$imgPath" ] [ -f "$imgPath" ] && [ -f "$imgPathDe" ]
} }
compVerify() { compVerify() {
[ "$(sha1sum "$imgPath" | cut -d' ' -f1)" = "7d28ee58f00854954073198983572e6921b87a21" ] [ "$(sha1sum "$imgPath" | cut -d' ' -f1)" = "7d28ee58f00854954073198983572e6921b87a21" ] &&
[ "$(sha1sum "$imgPathDe" | cut -d' ' -f1)" = "0a65f49d98896ad3e769be5f038a8d3086c00316" ]
} }
compEnv() { compEnv() {
envVar EMUTOS_IMG "$imgPath" 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
# #
# Haiku's own build-host tools, built from the R1/beta6 source: package # Haiku's own build-host tools, built from the R1/beta6 source: package
# (unpacks .hpkg packages: the Haiku sysroots are made from them) and # (unpacks .hpkg packages: the Haiku sysroots are made from them), bfs_shell
# bfs_shell (reads and writes Be File System volumes: the Haiku VM images # (reads and writes Be File System volumes: the Haiku VM images get their
# get their job runner through it, and the gates hand programs to the VM # job runner through it, and the gates hand programs to the VM and take
# and take results back on a BFS disk). jam, which builds them, comes from # results back on a BFS disk), and xres (adds resources to a Haiku or BeOS
# Haiku's buildtools at the same release. Installed into # executable: make/be.mk gives every program its icons with it). jam, which
# toolchains/opt/haikuTools; bin/ holds wrappers that find the tools' # builds them, comes from Haiku's buildtools at the same release. Installed
# into toolchains/opt/haikuTools; bin/ holds wrappers that find the tools'
# libraries. # 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" compGroups="toolchain:haiku toolchain:haiku32 toolchain:beos emulator:haiku emulator:haiku32 emulator:beos"
compRequires="host" compRequires="host"
compHostDeps="build-tools git python3 zlib-dev zstd-dev bison flex" compHostDeps="build-tools git python3 zlib-dev zstd-dev bison flex"
@ -19,8 +21,8 @@ compUrl="https://github.com/haiku/haiku.git"
compCommit="f6d86031a107e16f4fff6814e9aea20a39450842" compCommit="f6d86031a107e16f4fff6814e9aea20a39450842"
buildtoolsUrl="https://github.com/haiku/buildtools.git" buildtoolsUrl="https://github.com/haiku/buildtools.git"
buildtoolsCommit="bfa561e3708c0e2d0155fb403b988ba515c9b16d" buildtoolsCommit="bfa561e3708c0e2d0155fb403b988ba515c9b16d"
compPin="$compCommit:$buildtoolsCommit" hostTools="bfs_shell package xres"
hostTools="bfs_shell package" compPin="$compCommit:$buildtoolsCommit:$hostTools"
compInstall() { compInstall() {
@ -43,7 +45,7 @@ compInstall() {
# The tools keep the attributes of the host's files (which have none # 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 # here) in a folder named when they are built: theirs, not the build
# tree's, which goes. # 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" objects="$src/haiku/generated/objects/linux"
rm -rf "$compDir" rm -rf "$compDir"
mkdir -p "$compDir/bin" "$compDir/libexec" "$compDir/lib" "$compDir/attributes" mkdir -p "$compDir/bin" "$compDir/libexec" "$compDir/lib" "$compDir/attributes"
@ -58,17 +60,23 @@ compInstall() {
compInstalled() { 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 # 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 # and xres start. (Outputs are captured, not piped into grep: under pipefail
# own exit status, or SIGPIPE from an early grep, would fail the check.) # a tool's own exit status, or SIGPIPE from an early grep, would fail the
# check.)
compVerify() { compVerify() {
local tmp local tmp
local readBack local readBack
local usage local usage
local xresUsage
tmp="$(mktemp -d)" tmp="$(mktemp -d)"
printf 'JoeyLib\n' > "$tmp/in.txt" printf 'JoeyLib\n' > "$tmp/in.txt"
truncate -s 8M "$tmp/v.img" 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 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)" 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)" usage="$("$compDir/bin/package" 2>&1 || true)"
xresUsage="$("$compDir/bin/xres" -h 2>&1 || true)"
rm -rf "$tmp" 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
# #
# libxmp-lite 4.7.0: the MOD decoder the DOS, Atari ST, X68000, SDL3, PSP, # 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 # web, Win16, Win32, Haiku and BeOS audio HALs compiled in until 2026-10-14,
# makefile builds it from this source tree. # 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" 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" compRequires="host"
compHostDeps="tar" compHostDeps="tar"

View file

@ -1,3 +1,4 @@
# shellcheck shell=bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
# #
# PTPlayer 6.4: Frank Wille's ProTracker replayer for the Amiga, used by # 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 # the Amiga audio HAL, with JoeyLib's patches (installer/patches/ptplayer),
# archive's checksum. # 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" compGroups="toolchain:amiga"
compRequires="host" compRequires="host"
compHostDeps="lhasa" compHostDeps="lhasa patch"
compUrl="http://aminet.net/mus/play/ptplayer.lha" compUrl="http://aminet.net/mus/play/ptplayer.lha"
compSha256="5ba1285e91413f6b6cb72768c0c31cff90029af6920870d9f589689aa2e2161a" 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() { compInstall() {
local lha local lha
local p
lha="$(fetchFile "$compUrl" "$compSha256")" lha="$(fetchFile "$compUrl" "$compSha256")"
rm -rf "$compDir" rm -rf "$compDir"
extractArchive "$lha" "$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() { 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # 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. # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT

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