diff --git a/LICENSES.md b/LICENSES.md index ff4f274..e22b0ac 100644 --- a/LICENSES.md +++ b/LICENSES.md @@ -5,9 +5,10 @@ file inventories every component JoeyLib depends on but does not contain: what is linked into or shipped with the programs it builds, what builds them, and what tests them. JoeyLib's own tree holds no third-party code or assets, apart from the patches in `installer/patches/`, which are under -the licenses of the projects they patch (noted in those entries). +the licenses of the projects they patch (noted in those entries), and the +BeOS palette in `tools/iconBake.py` (Haiku's, MIT; noted below). Components are grouped by role; each entry lists the platforms that use -it, where it comes from, its license and where the installer puts it. +it, where it comes from, its license, and where the installer puts it. When upstream license terms change, update the corresponding entry here. This file summarises; the license text that comes with each component is @@ -20,23 +21,13 @@ what counts. These end up in what a JoeyLib program ships as, so their terms matter most. -### libxmp-lite 4.7.0 - -- **Used on:** DOS, Atari ST, X68000, SDL3, PSP, web, Win16, Win32, Haiku, - BeOS -- **URL:** -- **License:** MIT -- **Role:** ProTracker / Amiga MOD replayer + SMIX SFX overlay. Each - port's makefile compiles it into AUDIO programs. -- **Installed at:** `toolchains/opt/libxmp/` (installer component `libxmp`) - ### SDL 3.4.18 - **Used on:** SDL3, PSP, web - **URL:** - **License:** zlib -- **Role:** window, input and audio for the hosted SDL3 code. Built as a - static library for the host, for the PSP with pspdev and for Emscripten +- **Role:** window, input, and audio for the hosted SDL3 code. Built as a + static library for the host, for the PSP with pspdev, and for Emscripten (with JoeyLib's patches, `installer/patches/sdl3Web/`, which are under SDL's zlib license), and linked into each program. - **Installed at:** `toolchains/opt/sdl3/`, `toolchains/opt/sdl3Psp/`, @@ -60,12 +51,15 @@ most. ### PTPlayer 6.4 -- **Used on:** Amiga +- **Used on:** Amiga; and, translated to C, DOS, Atari ST, X68000, SDL3, + PSP, web, Win16, Win32, Haiku, BeOS - **URL:** (author Frank Wille, ) - **License:** Public Domain (the archive's `license` file: the Unlicense) - **Role:** ProTracker MOD replayer + `mt_playfx` SFX channel for the - Paula audio HAL. + Paula audio HAL. Its replay, translated to C (with its period table), is + JoeyLib's module player, `src/core/modPlayer.c`, which every port that + mixes modules in software links. - **Installed at:** `toolchains/opt/ptplayer/` (installer component `ptplayer`) ### CWSDPMI r7 @@ -84,7 +78,7 @@ most. ### The toolchains' C libraries and start files -Each toolchain below also links its own C library, start files and +Each toolchain below also links its own C library, start files, and compiler runtime into the programs it builds. What a shipped program owes them, in brief (the entries below have the detail): @@ -92,7 +86,7 @@ them, in brief (the entries below have the detail): source if its users are told how to get DJGPP (`copying.dj`). DVX programs are modules DVX loads, built with DVX's SDK. - **Atari ST:** MiNTLib, file by file under its own license (COPYMINT), - the LGPL 2 or later, BSD, Henry Spencer's and musl's MIT terms; a linked + the LGPL 2 or later, BSD, and Henry Spencer's and musl's MIT terms; a linked program meets all of them (MiNTLib's `LICENSES`). - **Amiga:** libnix (public domain). - **X68000:** newlib (a collection of permissive licenses, @@ -177,7 +171,7 @@ above. (a fork of llvm-mos, ) - **License:** Apache License 2.0 with LLVM Exceptions, llvm-mos's (and LLVM's) license, for all of it: llvm-mos's code and the W65816 backend, - link816, omfEmit and the runtime (its `LICENSE.TXT`). Its README lists + link816, omfEmit, and the runtime (its `LICENSE.TXT`). Its README lists the third-party code it holds under other licenses (the C++ runtime's Embedded Template Library, MIT; test and demo sources), none of which reaches a JoeyLib program. @@ -256,7 +250,7 @@ above. (Vincent Riviere's builds; mintlib: ) - **License:** gcc 15.2 and binutils 2.45: GNU GPL v3, with the GCC Runtime Library Exception for libgcc. MiNTLib 0.60.1: per file, under - the MiNTLib license (COPYMINT), the LGPL 2 or later, BSD, Henry + the MiNTLib license (COPYMINT), the LGPL 2 or later, BSD, or Henry Spencer's or musl's MIT terms; a program linked with it meets all of them (MiNTLib's `LICENSES`). - **Role:** GEMDOS-native gcc + binutils + mintlib used to compile @@ -322,9 +316,9 @@ above. - **Used on:** Win32 - **URL:** -- **License:** clang, lld and the LLVM runtimes: Apache License 2.0 with +- **License:** clang, lld, and the LLVM runtimes: Apache License 2.0 with LLVM Exceptions. mingw-w64's headers and runtime: Zope Public License - 2.1, with parts marked public domain, BSD or LGPL. + 2.1, with parts marked public domain, BSD, or LGPL. - **Role:** i686 cross compiler for the win32 port's programs, which link Windows' MSVCRT.DLL. - **Installed at:** `toolchains/opt/llvmMingw/` (installer component @@ -338,7 +332,7 @@ above. Library Exception for libgcc. The PSPSDK: BSD. newlib: its collection of permissive licenses. Every library pspdev carries has its license under `psp/share/licenses/`. -- **Role:** psp-gcc, the PSPSDK and the tools that make an EBOOT.PBP. +- **Role:** psp-gcc, the PSPSDK, and the tools that make an EBOOT.PBP. - **Installed at:** `toolchains/opt/pspdev/` (installer component `pspdev`) ### Emscripten 6.0.11 (emsdk) @@ -376,10 +370,45 @@ above. distribution, keeping its notice). - **Role:** `package` unpacks the `.hpkg` packages the sysroots are made from; `bfs_shell` reads and writes the BFS disks the VMs take their jobs - on. + on; `xres` puts every Be API program's icons into its resources. - **Installed at:** `toolchains/opt/haikuTools/` (installer component `haikuTools`) +### The BeOS palette (in `tools/iconBake.py`) + +- **Used on:** Haiku, Haiku x86, BeOS +- **URL:** (`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:** +- **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:** +- **License:** GPLv2 or later. Its demo songs are not under the GPL: they are + their authors', all rights reserved, and nothing made from them is kept or + shipped (`scripts/check-vgm.sh` converts them in a scratch folder). +- **Role:** the recommended tracker for writing chip-tracker songs + (docs/Assets.adoc); its VGM export is what `tools/vgm2song.py` converts. +- **Installed at:** `toolchains/opt/furnace/` (installer component `furnace`, + optional: `./install.sh install furnace`) + --- ## Test-time emulators (not shipped, not linked) @@ -478,13 +507,13 @@ Installed from the Linux distribution by the components named below numpy (BSD), installed by hand with pip. The script holds none of potracer's code, and the logo it made is not covered by potracer's license. -- Building MAME, QEMU, PPSSPP and SDL 3 also takes the distribution's - development packages (SDL2, X11, Wayland, PulseAudio, ALSA, GLib and the +- Building MAME, QEMU, PPSSPP, and SDL 3 also takes the distribution's + development packages (SDL2, X11, Wayland, PulseAudio, ALSA, GLib, and the rest), each under its own license. --- -## System ROMs, operating systems and disk images (used for testing only) +## System ROMs, operating systems, and disk images (used for testing only) These are **not** part of JoeyLib distribution. `./install.sh` places them in `toolchains/firmware/` or `toolchains/opt/` for @@ -539,10 +568,23 @@ project hosts (`JOEY_BUNDLE_URL`); matched by the checksums in Trio64V+ display driver** (its `.zip`): Microsoft and S3 proprietary. Installed unattended into the DOSBox PC every win16 run uses. A Win16 program's users bring their own Windows. Component `win311`. +- **Microsoft WinG 1.0 SDK** (`WING10.EXE`): Microsoft proprietary, under + its Microsoft Development License (`LICENSE.TXT`, installed beside the + runtime): the SDK may be used to design, develop, and test software; its + runtime (`WING.DLL`, `WING32.DLL`, `WINGDE.DLL`, `WINGDIB.DRV`, + `WINGPAL.WND`, `SETUPHLP.DLL`, `DVA.386`) may be redistributed only as part + of your own application, not for further redistribution, and with an + indemnity to Microsoft. Microsoft no longer offers it; archive.org keeps a + copy (). The win16 port uses WinG when + it is installed on the machine and draws through GDI without it, so + JoeyLib never ships it; a game that wants WinG on its users' machines + ships the runtime under those terms. The installer takes the runtime out + of the SDK into `toolchains/opt/wing/` and the win16 gates install it in + their Windows. Component `wing`. - **BeOS 5 Professional CD** (its image): Be, Inc. proprietary; the terms for its start files are Unknown. Its boot floppy and x86 system become the BeOS R5 VM (component `beosVm`), and its Development package's - headers, start files and system libraries the beos port's sysroot + headers, start files, and system libraries the beos port's sysroot (component `beosR5`). Every beos program links the start files (`start_dyn.o`, `init_term_dyn.o`, `crti.o`, `crtn.o`), as every program built with Be's own tools did; check their terms before shipping one. diff --git a/Makefile b/Makefile index 79b32e2..9297a9d 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ # make docs renders the manual to build/docs (HTML + PDF) # make verify builds every port, then runs every gate (needs emulators) # make verify-audio runs every port's audio gate (needs emulators) +# make verify-conform grades every recording port against the audio contract # make verify-serial runs every port's serial loopback gate # make clean removes all build outputs # @@ -38,7 +39,7 @@ include $(dir $(firstword $(MAKEFILE_LIST)))make/common.mk ALL_TARGETS := $(JOEY_PORTS_INSTALLED) -.PHONY: all iigs iigs-disk iigs-verify amiga atarist ste dos x68000 dvx sdl3 web win32 win16 psp haiku haiku32 beos blank-check tools clean help status verify verify-audio verify-serial docs +.PHONY: all iigs iigs-disk iigs-verify amiga atarist ste dos x68000 dvx sdl3 web win32 win16 psp haiku haiku32 beos blank-check tools clean help status verify verify-audio verify-conform verify-serial docs all: $(ALL_TARGETS) tools ifeq ($(ALL_TARGETS),) @@ -127,6 +128,12 @@ verify: verify-audio: @$(REPO_DIR)/scripts/verify-all.sh audio $(PORTS) +# Every recording port against the one audio contract (AUDPAR, graded by +# tools/gradeAudioConformance.py), sequentially, with the same exit codes. +# Takes a subset: make verify-conform PORTS="sdl3 dos" +verify-conform: + @$(REPO_DIR)/scripts/verify-all.sh conform $(PORTS) + # The host check of the shared serial layer, then every port's loopback gate # (banner out, probe echoed back), sequentially, with the same exit codes. # Takes a subset: make verify-serial PORTS="host dos" @@ -159,7 +166,8 @@ help: @echo " iigs-disk iigs-verify (IIgs MAME visual check)" @echo " blank-check (host-cc smoke test of the new-port template + gnu89 lint)" @echo " verify (build every port, then run every gate; PORTS= subset)" - @echo " verify-audio (all 5 audio gates; PORTS=\"atarist amiga\" for a subset)" - @echo " verify-serial (host check + all 5 serial loopback gates; PORTS= subset)" + @echo " verify-audio (every port's audio gate; PORTS=\"atarist amiga\" for a subset)" + @echo " verify-conform (every recording port against the audio contract; PORTS= subset)" + @echo " verify-serial (host check + every port's serial loopback gate; PORTS= subset)" @echo " docs (manual to build/docs; FORMAT=html for HTML only)" @echo "Install toolchains first: ./install.sh install toolchains" diff --git a/README.md b/README.md index c8aa7bb..53074d1 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,9 @@ and the PSP, from a single codebase: under QEMU) - PSP (homebrew for custom firmware: the SDL3 port built with pspdev, each program an `EBOOT.PBP` in a folder for `PSP/GAME/`, the stage at 1:1 in - the 480x272 screen; tested under PPSSPP with every memory access checked - for alignment as a PSP checks it) + the 480x272 screen, a keyboard drawn on it for the keys a PSP lacks; + tested under PPSSPP with every memory access checked for alignment as a + PSP checks it) The Apple IIgs defines the capability ceiling. Stronger platforms coast. Hot paths are hand-written assembly per port; the public API is C. @@ -97,7 +98,7 @@ Your own program does not go into this tree: it lives in a directory of its own whose Makefile describes it and includes `make/project.mk` and `make/programs.mk` from a JoeyLib checkout (found through `JOEYLIB`, by default `../joeylib`). `make` there builds it for every installed target with -the same rules, flags and packaging as the examples, into the project's own +the same rules, flags, and packaging as the examples, into the project's own `build/`; the manual's "Building Your Program" chapter (`docs/BuildIntegration.adoc`) has the details. Two complete games are built this way as separate projects: Space Taxi, a port of the 1984 Commodore 64 @@ -127,9 +128,10 @@ the emulators and their firmware: A few files are never downloaded -- the Amiga Kickstart, the Amiga `serial.device` and `lowlevel.library` from Workbench 3.1, the X68000 font ROM, the Windows for Workgroups 3.11 install disc and S3 Trio64V+ driver -the Win16 port runs on, and the BeOS 5 Professional CD -- so supply your +the Win16 port runs on (and, optionally, Microsoft's WinG 1.0 SDK, +`WING10.EXE`), and the BeOS 5 Professional CD -- so supply your copies once with `./install.sh import PATH` (files, folders, `.zip` or -`.7z` archives or Amiga `.adf` disk images; the Windows disc as its `.iso`, +`.7z` archives, or Amiga `.adf` disk images; the Windows disc as its `.iso`, the BeOS CD as its CloneCD `.img`). The manual's Getting Started chapter covers `joey-run`'s options and the private-file bundle. @@ -148,8 +150,9 @@ make dvx ``` `make atarist` builds programs for every ST; `make ste` builds the STe -variant, which uses the STe's BLiTTER, DMA sound and 4-bit palette and runs -only on an STe or Mega STe. +variant, which uses the STe's BLiTTER, DMA sound, and 4-bit palette, reads +Jaguar pads in a 1040 STe's enhanced joystick ports, and runs only on an STe +or Mega STe. Every fragment also answers to a bare `clean`, and every fragment that builds through `make/common.mk` -- all of them except `make/iigs.mk`, which assembles @@ -169,9 +172,9 @@ compares two `joeylog.txt` captures and exits non-zero on any mismatch. The frozen references live in `tests/goldens/uber/`. All five ports currently agree on all 51 hashes; the X68000 has no golden of its own and is diffed against `tests/goldens/uber/iigs.txt`, which it matches exactly, and DVX, SDL3, the -web and the PSP, which share the DOS stage, are diffed against +web, and the PSP, which share the DOS stage, are diffed against `tests/goldens/uber/dos.txt` (`scripts/bench-dvx.sh`, `scripts/bench-sdl3.sh`, -`scripts/bench-web.sh` or `scripts/bench-psp.sh`, then +`scripts/bench-web.sh`, or `scripts/bench-psp.sh`, then `tools/diff-uber-hashes`). SDL3's run needs no emulator and no X server (SDL's dummy drivers) and takes about fifteen seconds; the web's runs UBER's page in headless Chromium; the PSP's runs UBER under headless PPSSPP in a few @@ -227,6 +230,8 @@ tools/assetbake/ PNG -> native .tbk / .spr baker (Python) tools/joeymod/ Protracker .MOD converter (passthrough or .NTP) tools/spritebake/ offline .spr -> .spc pre-compiled sprite baker tools/songbake/ chip-tracker text notation -> JYM1 stream +tools/vgm2song.py a tracker's VGM export (Furnace) -> chip-tracker .song +tools/iconBake.py one picture -> every system's program icon tools/xdftool.py Human68k FAT12 .XDF disk image reader / writer tools/diff-uber-hashes UBER golden-hash comparator examples/ example programs @@ -264,6 +269,7 @@ is a quick reference. Every entry point is plain C, no C++ extensions. ```c typedef struct { uint32_t codegenBytes; // runtime compiled-sprite cache size + bool noAudio; // true: no audio; else jlInit starts it } jlConfigT; bool jlInit (const jlConfigT *config); @@ -290,18 +296,18 @@ void jlRandomSeed (uint32_t seed); All surfaces are 320x200 16-color images with a 200-entry SCB table and 16 palettes of 16 `$0RGB` colors. In-memory storage is -target-native: chunky 4bpp packed on IIgs, DOS, DVX, SDL3, the web and the PSP, native planar +target-native: chunky 4bpp packed on IIgs, DOS, DVX, SDL3, the web, and the PSP, native planar (separate bitplanes on Amiga and Sharp X68000, word-interleaved planes on Atari ST) on the 68k ports. The public API speaks in color indices (0..15) and hides the storage format. ```c -#define SURFACE_WIDTH 320 -#define SURFACE_HEIGHT 200 -#define SURFACE_BYTES_PER_ROW 160 -#define SURFACE_PIXELS_SIZE (SURFACE_BYTES_PER_ROW * SURFACE_HEIGHT) -#define SURFACE_PALETTE_COUNT 16 -#define SURFACE_COLORS_PER_PALETTE 16 +#define JL_SURFACE_WIDTH 320 +#define JL_SURFACE_HEIGHT 200 +#define JL_SURFACE_BYTES_PER_ROW 160 +#define JL_SURFACE_PIXELS_SIZE (JL_SURFACE_BYTES_PER_ROW * JL_SURFACE_HEIGHT) +#define JL_SURFACE_PALETTE_COUNT 16 +#define JL_SURFACE_COLORS_PER_PALETTE 16 typedef struct jlSurfaceT jlSurfaceT; // opaque @@ -377,14 +383,14 @@ A "tile" is just an 8x8-aligned region of any surface. The API moves type so callers can stash a copy without allocating a scratch surface. ```c -#define TILE_PIXELS_PER_SIDE 8 -#define TILE_BYTES_PER_ROW 4 -#define TILE_BYTES (TILE_BYTES_PER_ROW * TILE_PIXELS_PER_SIDE) -#define TILE_BLOCKS_PER_ROW (SURFACE_WIDTH / TILE_PIXELS_PER_SIDE) // 40 -#define TILE_BLOCKS_PER_COL (SURFACE_HEIGHT / TILE_PIXELS_PER_SIDE) // 25 -#define TILE_NO_GLYPH ((uint16_t)0xFFFFu) +#define JL_TILE_PIXELS_PER_SIDE 8 +#define JL_TILE_BYTES_PER_ROW 4 +#define JL_TILE_BYTES (JL_TILE_BYTES_PER_ROW * JL_TILE_PIXELS_PER_SIDE) +#define JL_TILE_BLOCKS_PER_ROW (JL_SURFACE_WIDTH / JL_TILE_PIXELS_PER_SIDE) // 40 +#define JL_TILE_BLOCKS_PER_COL (JL_SURFACE_HEIGHT / JL_TILE_PIXELS_PER_SIDE) // 25 +#define JL_TILE_NO_GLYPH ((uint16_t)0xFFFFu) -typedef struct jlTileT { uint8_t pixels[TILE_BYTES]; } jlTileT; +typedef struct jlTileT { uint8_t pixels[JL_TILE_BYTES]; } jlTileT; void jlTileCopy (jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy); @@ -453,7 +459,6 @@ bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, uint16_t count, const uint16_t *palette); bool jlSpriteCompile (jlSpriteT *sp); // build per-shift fast path -void jlSpritePrewarm (jlSpriteT *sp); // hint: compile if not already uint32_t jlSpriteCompiledSize (const jlSpriteT *sp); void jlSpriteDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); @@ -517,6 +522,24 @@ For runtime-extracted content (sprites peeled out of a procedural or captured surface), use `jlSpriteCreateFromSurface` -- the on-the-fly path stays alongside the baked load API. +### Program icons (baked at build time) + +Every program gets an icon on each system that shows programs by one -- +the IIgs Finder, Workbench, the Program Managers of DVX and Windows 3.1, +Explorer, Tracker, the PSP's menu, the SDL3 window, and the web page -- +baked by `tools/iconBake.py` from one picture: the program's `ID_ICON` (a +PNG), or JoeyLib's kangaroo (`assets/joeyIcon.png`). `docs/BuildIntegration.adoc` +(Icons) says what each system gets, and `scripts/verify-all.sh icon` checks +that each one shows it. + +### Content creation tools + +`docs/Assets.adoc` (Content Creation Tools) recommends a tool for each kind +of asset: GrafX2 or LibreSprite for tile and sprite sheets, Furnace for +chip-tracker music (`./install.sh install furnace`; its VGM exports become +`.song` files through `tools/vgm2song.py`), MilkyTracker or OpenMPT for +`.MOD` music, and sfxr, jfxr, Audacity, or SoX for sound effects. + ### Present (`joey/present.h`) @@ -544,17 +567,17 @@ every port. Refilled by the same `jlInputPoll`; see `claudeDocs/input.md` for per-port sources and limitations. ```c -typedef enum { /* KEY_NONE, KEY_A..KEY_Z, KEY_0..KEY_9, KEY_SPACE, - KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE, - KEY_UP/DOWN/LEFT/RIGHT, KEY_LSHIFT/RSHIFT/LCTRL/LALT, - KEY_F1..KEY_F10, KEY_COUNT */ } jlKeyE; -typedef enum { MOUSE_BUTTON_NONE, MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, - MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT } jlMouseButtonE; -typedef enum { JOYSTICK_0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE; -typedef enum { JOY_BUTTON_0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE; +typedef enum { /* JL_KEY_NONE, JL_KEY_A..JL_KEY_Z, JL_KEY_0..JL_KEY_9, JL_KEY_SPACE, + JL_KEY_ESCAPE, JL_KEY_RETURN, JL_KEY_TAB, JL_KEY_BACKSPACE, + JL_KEY_UP/DOWN/LEFT/RIGHT, JL_KEY_LSHIFT/RSHIFT/LCTRL/LALT, + JL_KEY_F1..JL_KEY_F10, JL_KEY_COUNT */ } jlKeyE; +typedef enum { JL_MOUSE_BUTTON_NONE, JL_MOUSE_BUTTON_LEFT, JL_MOUSE_BUTTON_RIGHT, + JL_MOUSE_BUTTON_MIDDLE, JL_MOUSE_BUTTON_COUNT } jlMouseButtonE; +typedef enum { JL_JOYSTICK_0, JL_JOYSTICK_1, JL_JOYSTICK_COUNT } jlJoystickE; +typedef enum { JL_JOY_BUTTON_0, JL_JOY_BUTTON_1, JL_JOY_BUTTON_COUNT } jlJoyButtonE; -#define JOYSTICK_AXIS_MAX 127 -#define JOYSTICK_AXIS_MIN (-127) +#define JL_JOYSTICK_AXIS_MAX 127 +#define JL_JOYSTICK_AXIS_MIN (-127) #define JL_CHAR_BACKSPACE 0x08 #define JL_CHAR_TAB 0x09 @@ -588,20 +611,19 @@ void jlJoystickReset (jlJoystickE js, uint8_t deadZone); ### Audio (`joey/audio.h`) 4-channel Protracker-style music plus five one-shot SFX slots, three -PSG-style tone voices and a noise channel. Module data must be the +PSG-style tone voices, and a noise channel. Module data must be the platform-native form produced by `tools/joeymod` (`.mod` for -Amiga/DOS/ST/X68000; `.ntp` for IIgs; `.amod` if you want loop=false on -Amiga). Amiga plays `.mod` through PTPlayer and the IIgs through the -NinjaTrackerPlus replayer; DOS, Atari ST and X68000 decode with -libxmp-lite. A failed `jlAudioInit` is non-fatal; the rest of the API -stays callable as no-ops. +DOS/ST/X68000; `.amod` for the Amiga; `.ntp` for the IIgs). The Amiga +plays `.mod` through PTPlayer and the IIgs through the NinjaTrackerPlus +replayer; DOS, the Atari ST, the X68000, and the hosted ports mix it +with JoeyLib's module player, PTPlayer's replay in C. `jlInit` starts +audio (`jlConfigT.noAudio` leaves it off) and `jlShutdown` stops it; +sound that does not start is non-fatal, and the rest of the API stays +callable as no-ops. ```c -#define JOEY_AUDIO_SFX_SLOTS 5 -#define JOEY_AUDIO_VOICES 3 - -bool jlAudioInit (void); -void jlAudioShutdown (void); +#define JL_AUDIO_SFX_SLOTS 5 +#define JL_AUDIO_VOICES 3 void jlAudioPlayMod (const uint8_t *data, uint32_t length, bool loop); void jlAudioStopMod (void); @@ -613,7 +635,9 @@ void jlAudioPlaySfxStream (uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz); void jlAudioStopSfx (uint8_t slot); -// Portable chip-tracker music: a JYM1 event stream baked by tools/songbake, +// Portable chip-tracker music: a JYM1 event stream baked by tools/songbake +// (from text notation, or from a tracker: write it in Furnace for the +// SN76489 or AY-3-8910, export VGM, convert with tools/vgm2song.py), // played through the tone/noise layer at register-write cost on every port. // No CPU mixing, so it is viable where jlAudioPlayMod is not. bool jlMusicPlay (const uint8_t *data, uint32_t length, bool loop); @@ -754,13 +778,13 @@ conditionally compile on these: JOEYLIB_PLATFORM_IIGS / _AMIGA / _ATARIST / _DOS / _X68000 / _DVX / _HOSTED // exactly one defined JOEYLIB_HOSTED_SDL3 / _WIN32 / _WIN16 // the hosted backend JOEYLIB_ATARI_STE // with _ATARIST: the STe build (make ste) -JOEYLIB_SPRITE_INTERPRETED // no sprite code emitter (hosted, blank) -JOEYLIB_CPU_65816 / _68000 / _X86 -JOEYLIB_ENDIAN_LITTLE / _BIG -JOEYLIB_NATIVE_CHUNKY / _NATIVE_PLANAR -JOEYLIB_HAS_BLITTER / _HAS_COPPER // Amiga only -JOEYLIB_PLATFORM_NAME // human-readable string -JOEYLIB_VERSION_MAJOR / _MINOR / _PATCH / _STRING +JL_SPRITE_INTERPRETED // no sprite code emitter (hosted but Win16, blank) +JL_CPU_65816 / _68000 / _X86 +JL_ENDIAN_LITTLE / _BIG +JL_NATIVE_CHUNKY / _NATIVE_PLANAR +JL_HAS_BLITTER / _HAS_COPPER // Amiga only +JL_PLATFORM_NAME // human-readable string +JL_VERSION_MAJOR / _MINOR / _PATCH / _STRING ``` @@ -772,6 +796,6 @@ copyright and permission notice, in its documentation for instance. Each of JoeyLib's source files opens with a short header that says so (`SPDX-License-Identifier: MIT`); a new file gets the same. -The components JoeyLib depends on, their licenses and what a shipped +The components JoeyLib depends on, their licenses, and what a shipped program owes them are in [LICENSES.md](LICENSES.md). The patches in `installer/patches/` are under the licenses of the projects they patch. diff --git a/assets/joeyIcon.png b/assets/joeyIcon.png new file mode 100644 index 0000000..e92f95a --- /dev/null +++ b/assets/joeyIcon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ae3a9d2cccbb2838f26a7663ab953a2880856681b9256fdc9d6e17b78247fe8 +size 9409 diff --git a/docs/ApiAudio.adoc b/docs/ApiAudio.adoc index 016302a..755fc19 100644 --- a/docs/ApiAudio.adoc +++ b/docs/ApiAudio.adoc @@ -2,25 +2,32 @@ Declared in ``. -There are three layers, and on two platforms they share physical hardware: +There are three layers, and on some platforms they share physical hardware: * *Module playback* -- `jlAudioPlayMod`, four-channel Protracker `.MOD`. * *Chip tracker* -- `jlMusicPlay`, a portable event stream driving three tone voices plus noise at register-write cost, with no CPU mixing. The recommended music path. -* *Voices and effects* -- `jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx` and +* *Voices and effects* -- `jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and the streaming forms. [WARNING] ==== -On the Atari ST and Amiga the tone voices *are* the hardware the module player -drives. While a `.MOD` plays, `jlAudioVoice` writes fight the player's register -writes and lose. Choose the module layer or the voice and tracker layer per -scene, never both at once. +On the Amiga the tone voices and the noise *are* the hardware the module +player drives: Paula's four channels. While a `.MOD` plays, `jlAudioVoice` and +`jlAudioNoise` writes fight the player's register writes and lose. On the +plain ST (not the STe build) digital sound -- a module, an effect, or a +stream -- plays through the sound chip's channel A, so tone voice 0 is silent +while it plays and sounds again when it stops; voices 1 and 2 play on. Choose +the module layer or the voice and tracker layer per scene, never both at once. ==== -A failed `jlAudioInit` is not fatal: `jlInit` still succeeds and every audio -call becomes a silent no-op. +`jlInit` starts audio and `jlShutdown` stops it; a program that wants none +sets `jlConfigT.noAudio`, and every audio call is then silent. Sound that does +not start is not fatal: `jlInit` still succeeds, modules and sound effects are +silent, `jlAudioTickRegister` refuses, and the log says so. The tone and noise +voices, and so `jlMusicPlay`, still play where they drive a sound chip of +their own: on the Amiga, the ST, DOS, and the X68000. === Constants @@ -28,8 +35,8 @@ call becomes a silent no-op. |=== |Name |Value |Meaning -|`JOEY_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4. -|`JOEY_AUDIO_VOICES` |3 |PSG-style tone voices, 0..2. +|`JL_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4. +|`JL_AUDIO_VOICES` |3 |PSG-style tone voices, 0..2. |`JL_SFX_RAW_SIGNED8` |0 |Raw stream format: `int8_t` PCM. |`JL_SFX_RAW_UNSIGNED8_NOZERO` |1 |Raw stream format: `uint8_t` biased 0x80, and 0x00 is forbidden (it is the DOC stop byte). @@ -37,34 +44,15 @@ call becomes a silent no-op. [TIP] ==== -Prefer *slot 4* for game sound effects when music is playing. The IIgs maps -tracker tone voices onto slots 0..2 and the noise voice onto slot 3. On the -other ports the slots are independent of the voice layer. +Prefer *slot 4* for game sound effects when music is playing. The IIgs plays +tone voices 0..2 on sound effect slots 0..2 and the noise on slot 3: an effect +and a voice on the same slot cut each other off, and a voice still being held +takes its slot back at the next `jlAudioFrameTick`. On the Amiga effects, the +module, the voices, and the noise share Paula's four channels, so an effect can +take a channel a voice is using. On the other ports the slots are independent +of the voice layer. ==== -=== jlAudioInit - -[source,c] ----- -bool jlAudioInit(void); ----- - -*Parameters:* none. - -*Returns:* `true` if the platform has a working audio engine and started it; -`false` silently otherwise, with the rest of the API still callable but silent. - -=== jlAudioShutdown - -[source,c] ----- -void jlAudioShutdown(void); ----- - -Tears the engine down. Safe when audio was never initialised. - -*Parameters:* none. *Returns:* nothing. - === jlAudioFrameTick [source,c] @@ -72,10 +60,12 @@ Tears the engine down. Safe when audio was never initialised. void jlAudioFrameTick(void); ---- -Hooks the engine into your game loop. Most platforms drive their engines from a -hardware interrupt and ignore this, but it is required for any port that mixes -in user context, it advances tracker playback, and on IIgs and Amiga it is what -fires the next chunk of a streaming sound effect. +Hooks the engine into your game loop. Every port does its sound work here: DOS, +the ST, the X68000, DVX, and the hosted ports mix or refill their sound buffers +in it, so without it modules and effects stop or repeat; the IIgs and Amiga +feed each streaming sound effect its next chunk, notice a module's end, and +(on the IIgs) keep held notes sounding; and on every port it advances +`jlMusicPlay`. Call it once per frame, unconditionally. @@ -93,8 +83,9 @@ Begins module playback. If a module is already playing it is replaced. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description -|`data` |`const uint8_t *` |Platform-native module blob: `.mod` on most ports, - `.amod` on Amiga, `.ntp` on IIgs. The asset pipeline produces the right form. +|`data` |`const uint8_t *` |Platform-native module blob: `.mod` on every port + but the IIgs (an `.amod`, with its end marker, plays on the Amiga too), `.ntp` + on the IIgs. The asset pipeline produces the right form. |`length` |`uint32_t` |Blob size in bytes. |`loop` |`bool` |`true` plays forever. |=== @@ -103,9 +94,15 @@ Begins module playback. If a module is already playing it is replaced. [NOTE] ==== -`loop = false` needs help on the Amiga: PTPlayer has no native song-end signal, -so the module must contain an `E8FF` marker at song end. The `joeymod` tool's -`.amod` output injects it. A plain `.mod` with no marker loops anyway on Amiga. +`loop = false` plays the song once and stops it. The IIgs's replayer has a +play-once mode; the ports that mix modules in software stop where the song +comes back to a row it has played (its end, or a jump back), at an `F00`, or +at the Amiga's `E8F` stop marker; and the Amiga adds that marker as it copies +the module to chip RAM, sending the song's jumps back to it +(<<_music_protracker_modules>>). A module with no room for one, all 128 song +positions or all 128 patterns in use, loops there anyway, and so does one +whose jump back sits in a pattern it also plays where that jump goes +forward. ==== === jlAudioStopMod @@ -130,7 +127,8 @@ bool jlAudioIsPlayingMod(void); *Parameters:* none. *Returns:* `true` while a module is producing output. `false` after a stop, -before the first play, and on platforms where audio init failed. +once a song played with `loop = false` has ended, before the first play, and on +platforms where audio init failed. === jlAudioPlaySfx @@ -146,7 +144,7 @@ new sample replaces it. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description -|`slot` |`uint8_t` |Slot 0..`JOEY_AUDIO_SFX_SLOTS`-1. Prefer 4 alongside music. +|`slot` |`uint8_t` |Slot 0..`JL_AUDIO_SFX_SLOTS`-1. Prefer 4 alongside music. |`sample` |`const uint8_t *` |Raw signed 8-bit PCM. |`length` |`uint32_t` |Sample count in bytes. |`rateHz` |`uint16_t` |The rate the sample was recorded at; the engine @@ -155,6 +153,12 @@ new sample replaces it. *Returns:* nothing. +Most ports read the sample where it is while it plays, so keep it valid and +unchanged until it has finished or the slot has been stopped. The IIgs also +remembers the last sample each slot played by its address, length, and rate, +and plays its own copy when they match: changed bytes at the same address, +length, and rate are not heard there. + [NOTE] ==== The full -128..127 range is accepted everywhere, but the IIgs plays -128 as @@ -175,9 +179,9 @@ deactivates. [WARNING] ==== -This is called from the audio engine's refill context -- the main thread on DOS, -ST and SDL3 (on DVX, DVX's own sound pump, while the program is yielded), but the -*audio interrupt* on Amiga and IIgs. It must not block and must not allocate. +On every port it is called in program time, from the call that starts the +stream and then from `jlAudioFrameTick` (on DVX, also while the desktop runs). +It must not block, and it should be quick: it runs inside those calls. ==== === jlAudioPlaySfxStream @@ -206,10 +210,11 @@ with no caller-side allocation. [IMPORTANT] ==== -On IIgs and Amiga the engine plays one chunk at a time and fires the next from -`jlAudioFrameTick`, so *you must be calling `jlAudioFrameTick` every frame* or -the stream stops after its first chunk. A chunk seam on those ports can carry -up to one frame of jitter. +Every port feeds streams from `jlAudioFrameTick`, so *you must be calling +`jlAudioFrameTick` every frame* or the stream stops. On the IIgs and Amiga the +engine plays one chunk at a time (7 KB on the IIgs, 4 KB on the Amiga) and +fires the next from it, so a chunk seam on those ports can carry up to one +frame of jitter. ==== === jlAudioSfxRawFormat / jlAudioPlaySfxStreamRaw @@ -226,10 +231,10 @@ void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, `jlAudioPlaySfxStreamRaw` is `jlAudioPlaySfxStream` except your callback delivers bytes *already in that format*, so the port skips its per-byte -conversion of every chunk. On the IIgs that conversion is a 65816 byte loop -over each 3.5 KB chunk, and it is enough to stall a game for a third of a -second per chunk of speech. Every other port streams signed PCM natively, so -there this is simply the plain call. +conversion of every chunk. On the IIgs that conversion costs about 7 +microseconds a byte, some 50 ms for each 7 KB chunk. Every other port's raw +format is signed PCM, so there this is simply the plain call (DVX still flips +each byte for its sound library). If your producer cannot make the port's format, use `jlAudioPlaySfxStream`. @@ -240,7 +245,7 @@ If your producer cannot make the port's format, use `jlAudioPlaySfxStream`. [source,c] ---- typedef enum { - AUDIO_PAN_LEFT = 0, AUDIO_PAN_CENTER, AUDIO_PAN_RIGHT, AUDIO_PAN_COUNT + JL_AUDIO_PAN_LEFT = 0, JL_AUDIO_PAN_CENTER, JL_AUDIO_PAN_RIGHT, JL_AUDIO_PAN_COUNT } jlAudioPanE; void jlAudioSetPan(uint8_t slot, jlAudioPanE pan); @@ -268,14 +273,16 @@ What each port can actually do: |IIgs |All three. The DOC pans per oscillator, so a centred sound plays on two oscillators, one per side. A stock IIgs sums to mono, so this shows up only on a stereo card or an emulator. -|Amiga |Left and right only, by choosing the Paula channel side. Paula cannot - place one voice in both ears, so `AUDIO_PAN_CENTER` leaves PTPlayer's own - channel choice alone. +|Amiga |Left and right by choosing the Paula channel side; a Paula channel plays + on one side only. A centred sound plays on a free channel each side when both + sides have one, so it reaches both ears; otherwise on the one channel + PTPlayer picks. |X68000 |Ignored. Everything mixes into one ADPCM stream, so panning would move all audio at once. -|DOS, ST |Ignored. Output is mono. +|DOS, ST, STe |Ignored. Output is mono. |DVX |All three, through DVX's stereo mixer (on a stereo card). -|SDL3 |All three: the port mixes in stereo. +|SDL3, web, Win32, PSP, Haiku, BeOS |All three: the port mixes in stereo. +|Win16 |Ignored. Output is mono. |=== === jlAudioStopSfx @@ -300,13 +307,12 @@ void jlAudioStopSfx(uint8_t slot); void jlAudioTone(uint16_t freqHz); ---- -Direct hardware tone generator: plays a square wave continuously until the next -call. Designed for AGI-era music where the engine programs a divisor and lets -the chip play, so CPU cost during playback is zero. Single voice; uses voice 0 -on platforms with several oscillators. On DOS this programs PIT counter 2 and -the speaker gate; on DVX it is a square tone of DVX's sound library, on the FM -chip when there is one; on SDL3 it is voice 0 of the port's mixer at full -volume. +A single tone that plays until the next call: `jlAudioVoice(0, freqHz, 0)`, +tone voice 0 at full volume, on every port. Designed for AGI-era music where +the engine programs a divisor and lets the chip play, so CPU cost during +playback is zero. A program that mixes it with `jlAudioVoice` or `jlMusicPlay` +shares voice 0 with them. On DOS without an OPL2 (AdLib) chip, voice 0, and so +this tone, is the PC speaker. [cols="1,1,4", options="header"] |=== @@ -314,8 +320,7 @@ volume. |`freqHz` |`uint16_t` |Frequency in Hz. 0 silences the generator. |=== -*Returns:* nothing. A no-op on platforms with no reachable tone generator -- -fall back to `jlAudioPlaySfxStream` there if you need synthesised sound. +*Returns:* nothing. Silent wherever voice 0 is (see `jlAudioVoice`). === jlAudioVoice @@ -329,33 +334,32 @@ Three-voice PSG-style polyphonic tone generator. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description -|`voice` |`uint8_t` |Voice 0, 1 or 2. +|`voice` |`uint8_t` |Voice 0, 1, or 2. |`freqHz` |`uint16_t` |Frequency in Hz. 0 silences this voice. |`atten` |`uint8_t` |AGI / SN76489-style 4-bit attenuation: 0 loudest, - 15 silent, about 2 dB per step. + 15 silent, 2 dB per step as near as the chip comes (the ST's YM2149 has + sixteen levels about 3.5 dB apart, so a step there is within 1.6 dB). |=== *Returns:* nothing. -Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2; the ST uses -YM2149 channels A, B and C; the Amiga uses Paula channels 0..2 looping a -two-byte square sample; the IIgs uses Ensoniq DOC oscillators playing a -resident square; DVX plays square tones of its sound library, which puts them -on the FM chip when there is a free voice and mixes them otherwise; SDL3 mixes -square waves in software. - -Voice state is sticky -- a voice keeps its last frequency until you change it --- and CPU cost during sustained playback is zero, because the hardware -oscillates on its own. +Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2 (with no OPL2, +voice 0 is the PC speaker and voices 1 and 2 are silent); the ST uses YM2149 +channels A, B, and C (on the plain ST voice 0 is silent while digital sound +plays); the Amiga uses Paula channels 0..2 looping a two-byte square sample; +the IIgs uses Ensoniq DOC oscillators playing a resident square on sound +effect slots 0..2; the X68000 uses YM2151 channels 0..2, one operator each, +so its tones are sine waves; DVX plays square tones of its sound library, +which puts them on the FM chip when there is a free voice and mixes them +otherwise; the hosted ports mix square waves in software. The OPL2's waves +are not square either. [IMPORTANT] ==== -The IIgs is the exception to stickiness: each call is an approximately 80 ms -retriggered one-shot, so a note held longer must be re-asserted about once a -frame. It also requires `jlAudioInit`, because the DOC path rides the tracker -engine. On DOS, ST and Amiga this is direct hardware programming and works -without `jlAudioInit`; on DVX it opens the program's sound on first use. On -SDL3 the voices are mixed in software and sound only after `jlAudioInit`. +A note sounds until the next call for that voice. The IIgs plays each note as +a sample a fraction of a second long and restarts the notes being held from +`jlAudioFrameTick`, so call that every frame, as every program should; a frame +much longer than 80 ms can leave a gap in a held note there. ==== === jlAudioNoise @@ -378,13 +382,17 @@ layer. *Returns:* nothing. -Mapping: the ST uses the YM2149 noise generator mixed onto channel C; DOS uses -the OPL2 rhythm-mode snare, leaving the three tone channels untouched; the -Amiga uses Paula channel 3 with an LFSR noise sample; the IIgs uses a resident -noise sample on slot 3 with the same retriggered one-shot model as its tone -voices; DVX uses a noise tone of its sound library; SDL3 mixes the YM2149's -own noise in software (its 17-bit generator, clocked at 125 kHz divided by -the pitch), so it sounds as on the ST. +The noise runs at 125000 / `pitch` Hz (`pitch` 0 as 1), the YM2149's rate, on +every port as far as its chip goes. Mapping: the ST uses the YM2149 noise +generator mixed onto channel C; the X68000 the YM2151's noise, at the same +rates exactly; the Amiga Paula channel 3 looping an LFSR noise sample, whose +fastest rate, 28.6 kHz, pitches 0 to 4 all play at; the IIgs a resident noise +sample on slot 3, up to 26 kHz, likewise for pitches 0 to 4; DVX a noise tone +of its sound library; and the hosted ports mix the YM2149's own noise in +software (its 17-bit generator), so it sounds as on the ST. DOS uses the OPL2 +rhythm-mode snare, leaving the three tone channels untouched: a tuned drum, +not free-running noise, whose pitch spreads over the snare's range rather than +following the YM2149's rate. [WARNING] ==== @@ -406,7 +414,7 @@ is no CPU mixing, this is fully viable on the ST and cheap on DOS, which `.MOD` playback is not. Playback is pumped by `jlAudioFrameTick` and paced by `jlMillisElapsed`, so -tempo holds across 50, 60 and 70 Hz ports. +tempo holds across 50, 60, and 70 Hz ports. [cols="1,1,4", options="header"] |=== @@ -419,13 +427,8 @@ tempo holds across 50, 60 and 70 Hz ports. |=== *Returns:* `true` on success; `false` on a malformed stream, in which case -nothing plays. - -[NOTE] -==== -Call `jlAudioInit` first for the song to be audible on the IIgs -- see -`jlAudioVoice`. -==== +nothing plays. With `jlConfigT.noAudio` the song runs silently, its time and +its end as usual. === jlMusicStop @@ -507,35 +510,55 @@ frame, so the chip only hears the last write and short notes vanish. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description -|`fn` |`jlAudioTickFnT` |Callback, fired from interrupt context. Pass `NULL` to - uninstall. +|`fn` |`jlAudioTickFnT` |Callback, which may be fired from an interrupt. Pass + `NULL` to uninstall. |`hz` |`uint16_t` |Firings per second. |=== -*Returns:* `true` if installed. Ports without a suitable timer source return -`false`. +*Returns:* `true` if installed. `false` when `hz` is 0, when audio is off or +did not start, or when the port cannot take the rate (above 1000 Hz on DVX, or +above the mixer's rate on the hosted ports). Uninstalling always returns +`true`. [WARNING] ==== -`fn` runs in interrupt context. Keep it to memory reads and direct hardware -register writes -- no allocation, no `printf`, nothing blocking. +`fn` may run in an interrupt: it does on DOS, the Amiga, the ST, and the +X68000. Keep it to memory reads and direct hardware register writes -- no +allocation, no `printf`, nothing blocking -- and share data with it through +`jlAudioCriticalEnter` and `jlAudioCriticalExit`. ==== +On every port the calls average exactly `hz` a second; where they fall +differs. DOS, the Amiga, and the hosted ports place each one at its own time. +The ST and X68000 deliver them from the system's own timer, 200 and 100 times a +second, so the calls due between two of its ticks come together. The IIgs and +DVX deliver them in program time from `jlAudioFrameTick`, so a frame's calls +come together at it. + On DOS this reprograms PIT counter 0 and hooks INT 8, keeping the BIOS tick counter advancing at the canonical 18.2 Hz through an accumulator so `time` stays correct. `jlMillisElapsed` carries on from where it was when the tick starts or stops; it never jumps back. +On the Amiga a `timer.device` request on the E-clock raises a software +interrupt at each tick. On the ST the tick rides TOS's 200 Hz Timer C +interrupt, and on the X68000 Human68k's 100 Hz Timer C interrupt. + +On the IIgs `fn` runs in program time, at `jlAudioFrameTick`, as many times as +the display's beats since the last frame have made due, so the average is +exact whatever the program's frame rate. + On DVX a timer of DVX's (at most 1000 Hz) counts the ticks, and `fn` runs in program time, once per counted tick, at the next `jlAudioFrameTick`: DVX's sound library, which `jlAudioVoice` and the rest go through there, may not be called from an interrupt. `fn` may therefore do anything, but what it does lands on the frame, not between frames. -On SDL3 the sound is mixed in program time, a few frames ahead, and `fn` is -called by the mixer between exactly the two samples where its tick falls: in -program time, so it may do anything, yet a note it starts begins at the right -place in the sound. `hz` may be anything up to the mixer's 44100. +On the hosted ports the sound is mixed in program time, a few frames ahead, +and `fn` is called by the mixer between exactly the two samples where its tick +falls: in program time, so it may do anything, yet a note it starts begins at +the right place in the sound. `hz` may be anything up to the mixer's rate, +44100 (22050 on Win16). === jlAudioCriticalEnter / jlAudioCriticalExit @@ -547,8 +570,10 @@ void jlAudioCriticalExit(void); Critical section against the tick interrupt. If you mutate state that the registered tick callback reads, wrap the mutation in these so the interrupt -cannot observe a half-written value. On DOS this is `cli` and `sti`; on ports -with no audio interrupt, and on DVX and SDL3, where the callback runs in -program time, both are no-ops. +cannot observe a half-written value. On DOS, the Amiga, the ST, and the +X68000 a section holds every interrupt off, so keep it short. Sections nest: +only the outermost pair does anything, and its Exit puts back the interrupt +state its Enter found. On the IIgs, DVX, and the hosted ports, whose +callbacks run in program time, they do nothing, and need not. *Parameters:* none. *Returns:* nothing. diff --git a/docs/ApiCore.adoc b/docs/ApiCore.adoc index 5d391ab..6e32f99 100644 --- a/docs/ApiCore.adoc +++ b/docs/ApiCore.adoc @@ -8,6 +8,7 @@ Declared in ``, included by ``. ---- typedef struct { uint32_t codegenBytes; + bool noAudio; } jlConfigT; ---- @@ -18,13 +19,26 @@ typedef struct { |`codegenBytes` |`uint32_t` |Size of the runtime compiled-sprite cache. `0` selects a per-platform - default: 8 KB on IIgs, 32 KB on Amiga, ST, DOS and DVX (SDL3 interprets every - sprite and leaves the cache unused). On the IIgs the cache is - one Memory Manager block that may not span a 64 KB bank boundary, so values - above 65536 are clamped to 65536, and even a clamped request can fail if no - free bank exists. Size it against `jlSpriteCompiledSize()`. + default: 8 KB on the IIgs, 64 KB on the Amiga and ST, and 32 KB on the + X68000, DOS, DVX, and Win16. The other hosted ports interpret every sprite + and never use the cache, but still allocate it (32 KB by default). The + cache is one block, and `jlInit` fails if it cannot be had -- except on the + IIgs, where it is up to four Memory Manager blocks of at most 64 KB, each + inside one 64 KB bank, so at most 256 KB; a bank that is refused is tried + again at 60 KB, and `jlInit` takes whatever part of the request it was + granted. Size it against `jlSpriteCompiledSize()`. + +|`noAudio` +|`bool` +|`true` leaves audio off: every audio call is silent, as on a machine without + sound, and the program pays nothing for audio (on the IIgs no replayer read + from disk, on DOS and the Amiga no interrupt hooked). `false` has `jlInit` + start audio and `jlShutdown` stop it. |=== +Set every field: a `jlConfigT` is read whole, so a field left unset is +whatever the memory held. + === jlInit [source,c] @@ -33,8 +47,11 @@ bool jlInit(const jlConfigT *config); ---- Initialises the library: takes over the display, allocates the stage and the -codegen arena, and brings up input. Must be called before any other JoeyLib -function. +codegen arena, brings up input, and last starts audio (unless `noAudio`). Must +be called before any other JoeyLib function. Sound that does not start is not +a failure: modules and sound effects are then silent, and the log says so +(<<_debug_logging>>). The tone and noise voices still play on the ports whose +sound chip needs no engine: the Amiga, the ST, DOS, and the X68000. [cols="1,1,4", options="header"] |=== @@ -43,7 +60,9 @@ function. |=== *Returns:* `true` on success. On failure returns `false` and `jlLastError()` -describes the reason. +describes the reason. Where the port's own start-up fails without saying +why, as the IIgs, Amiga, ST, X68000, and DVX ports' can, the reason is just +`jlpInit failed`. === jlShutdown @@ -52,8 +71,10 @@ describes the reason. void jlShutdown(void); ---- -Shuts the library down, restoring the display mode, releasing all library -resources and un-hooking any interrupt vectors the port installed. Call it on +Shuts the library down: stops audio first, then restores the display mode, +releases the library's own resources, and un-hooks any interrupt vectors the +port installed. Surfaces and sprites the program created are not released: +destroy them with `jlSurfaceDestroy` and `jlSpriteDestroy` first. Call it on every exit path; skipping it can leave the machine unusable. *Parameters:* none. *Returns:* nothing. @@ -104,9 +125,10 @@ void *jlAlloc(uint32_t bytes); Allocates a large or persistent buffer from the platform's native allocator rather than the small C heap. *Use this instead of `malloc` for anything -substantial.* On the IIgs the C heap lives in bank 0 and is tiny, with a single -`malloc` capped around 32 KB, so multi-kilobyte caches and frame buffers must -come from the Memory Manager instead. +substantial.* On the IIgs the whole C heap is one Memory Manager block of +32 KB to 48 KB (and, if that block cannot be had, only what the program's link +left in its bank, which can be a few hundred bytes), so multi-kilobyte caches +and frame buffers must come from the Memory Manager instead. [cols="1,1,4", options="header"] |=== @@ -124,7 +146,9 @@ zero-filled. void jlFree(void *p); ---- -Releases a buffer from `jlAlloc`. +Releases a buffer from `jlAlloc`. Never mix `jlFree` and `free`: on most +ports they are the same thing, but on the IIgs a `jlAlloc` block is a Memory +Manager handle, and either call given the other's block corrupts memory. [cols="1,1,4", options="header"] |=== @@ -164,8 +188,11 @@ void jlWaitVBL(void); ---- Blocks until the next display vertical blank, pacing a game loop to the -display's native refresh rate. Cheap on every port, because the underlying -mechanism is always a hardware-level wait rather than a software timer. +display's native refresh rate. On the machine ports it waits for the +display's own vertical blank. The hosted ports, and DVX in a window, have no +vertical blank to wait for: there it waits for the next tick of a virtual +frame clock, 60 Hz on the hosted ports and 70 Hz on DVX, and nothing stops a +present from tearing. *Parameters:* none. *Returns:* nothing. @@ -178,13 +205,15 @@ uint16_t jlFrameCount(void); *Parameters:* none. -*Returns:* a monotonic 16-bit frame counter. +*Returns:* a monotonic 16-bit frame counter. An interrupt or a clock keeps it, +so it counts every frame however seldom you read it -- except on an X68000 +whose system refuses JoeyLib's frame interrupt, where it counts only while +`jlWaitVBL`, `jlFrameCount`, or `jlStagePresent` is being called. [WARNING] ==== -You must poll faster than twice `jlFrameHz()` or edges are missed, and the -counter wraps roughly every 18 minutes at 60 Hz. For elapsed-time work prefer -`jlMillisElapsed`. +The counter wraps roughly every 18 minutes at 60 Hz, so compare two readings +by subtracting them. For elapsed-time work prefer `jlMillisElapsed`. ==== === jlFrameHz @@ -196,13 +225,16 @@ uint16_t jlFrameHz(void); *Parameters:* none. -*Returns:* the nominal display frame rate in Hz -- 50 on Amiga PAL, 60 on IIgs -and ST NTSC, 55 on the X68000, 70 on VGA mode 13h and on DVX, whose window has -a virtual 70 Hz (and whose full screen is mode 13h's, or a VBE mode's, with the -same frame count), and 60 under SDL3, whose window has a virtual 60 Hz. The X68000 times its display at `jlInit` and rounds to the -nearest whole number: 55.46 Hz on the machine, 56.07 Hz under MAME, which -therefore reports 56. The real cadence may drift slightly; this is the figure -to divide by when converting frames to seconds. +*Returns:* the nominal display frame rate in Hz -- 60 on the IIgs, or 50 on a +PAL machine; 50 on the Amiga, whose screen is PAL, or 60 on an NTSC Amiga that +cannot show PAL; 50 or 60 on the ST, as the machine is set; 55 on the X68000; +70 on VGA mode 13h and on DVX, whose window has a virtual 70 Hz (and whose full +screen is mode 13h's, or a VBE mode's, with the same frame count); and 60 on +every hosted port, whose window has a virtual 60 Hz. The X68000 times its +display at `jlInit` and rounds to the nearest whole number: 55.46 Hz on the +machine, 56.07 Hz under MAME, which therefore reports 56. The real cadence may +drift slightly; this is the figure to divide by when converting frames to +seconds. === jlMillisElapsed @@ -211,10 +243,9 @@ to divide by when converting frames to seconds. uint32_t jlMillisElapsed(void); ---- -Monotonic millisecond counter since `jlInit`. DOS, ST, X68000, DVX and SDL3 read a -real timer; IIgs and Amiga derive it from the frame counter, so the -value steps at frame granularity there. Two calls separated by a long render report the true -elapsed time, not the number of vertical blanks that fired. +Monotonic millisecond counter since `jlInit`. It may advance a frame at a +time. Two calls separated by a long render report the true elapsed time, not +the number of vertical blanks that fired. *Parameters:* none. @@ -222,8 +253,8 @@ elapsed time, not the number of vertical blanks that fired. [NOTE] ==== -On the frame-derived ports you must poll at least once every 65535 frames -- -about 18 minutes at 60 Hz -- or wrapped frames are lost. +Call it at least once every 15 minutes, or the time between two calls can be +lost. ==== === jlRandom diff --git a/docs/ApiDebug.adoc b/docs/ApiDebug.adoc index 28e5166..8637328 100644 --- a/docs/ApiDebug.adoc +++ b/docs/ApiDebug.adoc @@ -2,20 +2,40 @@ Declared in ``. -A crash-tracing logger that writes to a file beside the executable (on DVX, -in the program's settings folder, `CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`). -Debugging a program on an emulated 1988 machine is awkward, and this is -usually the fastest way to find out what it did before it stopped doing it. -The headless run scripts retrieve the log automatically. +A crash-tracing logger that writes `joeylog.txt` in the current directory +(on DVX, in the program's settings folder, +`CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`). Debugging a program on an emulated +1988 machine is awkward, and this is usually the fastest way to find out what +it did before it stopped doing it. The headless run scripts retrieve the log +automatically. The library itself writes to it only when something fails, such +as sound that does not start. + +How the file is written differs: + +* *Every port but the IIgs* opens the file on the first line and appends to + it, writing each line out as it is logged, so a crash or hang leaves every + line before it on disk. The file grows across runs until `jlLogReset`. +* *The IIgs* keeps the lines in an 8 KB ring in memory, because a write per + line costs seconds of floppy time there. Nothing reaches the disk until + `jlLogFlush`, which replaces the file with the last 8 KB logged. A program + that never calls it leaves no log. +* *The web* writes the file into the page's memory, where it is lost when the + page closes. +* *The hosted ports* make the program's folder the current directory in + `jlInit`: a line logged before that goes to a log in the directory the + program was started from, and the rest of the run's lines follow it there. [NOTE] ==== -The logger needs a multi-kilobyte ring buffer, which the tightest targets -cannot spare. Space Taxi compiles it out entirely on the IIgs and X68000 for -that reason. If you are debugging a memory-tight program, expect to do the -same and to observe hardware state through the emulator instead. +The logger's buffer is linked into every program, since the library logs its +own failures: the 8 KB ring on the IIgs, and a 16 KB file buffer on every +other port but DVX. A memory-tight program can still leave its own log calls +out and observe hardware state through the emulator instead. ==== +On DOS the library also creates `CRASH.LOG` in the current directory at +`jlInit`, and writes the processor's registers there if the program faults. + === jlLog [source,c] @@ -41,13 +61,14 @@ void jlLogF(const char *fmt, ...); ---- Appends a formatted message, `printf` style, as one line: like `jlLog`, the -library ends the line, so the format needs no trailing `\n`. +library ends the line, so the format needs no trailing `\n`. On the IIgs a +line is cut at 199 characters. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`fmt` |`const char *` |Format string. -|`...` | |Arguments matching the format string. +|`+...+` | |Arguments matching the format string. |=== *Returns:* nothing. @@ -66,8 +87,10 @@ time. void jlLogFlush(void); ---- -Flushes buffered log output to disk. Call it before anything that might crash -or exit, or the last and most interesting lines may never be written. +Flushes buffered log output to disk. On the IIgs it is the only way the log +reaches the disk: it rewrites the whole file with the last 8 KB logged, so call +it before anything that might crash or exit, or the last and most interesting +lines are never written. Every other port has already written each line. *Parameters:* none. *Returns:* nothing. @@ -79,7 +102,8 @@ void jlLogReset(void); ---- Clears the log, discarding previous contents. Call once at startup so each run -produces a fresh log rather than appending to the last one. +produces a fresh log rather than appending to the last one. On the IIgs it +clears the ring only; the file on disk changes at the next `jlLogFlush`. *Parameters:* none. *Returns:* nothing. @@ -89,6 +113,7 @@ int main(void) { jlConfigT config; config.codegenBytes = 32UL * 1024; + config.noAudio = true; if (!jlInit(&config)) { return 1; } diff --git a/docs/ApiDraw.adoc b/docs/ApiDraw.adoc index 8ee333e..4168463 100644 --- a/docs/ApiDraw.adoc +++ b/docs/ApiDraw.adoc @@ -7,8 +7,9 @@ no-ops, never errors, and there are no unclipped variants in the public API -- you cannot corrupt memory by drawing off the edge. Every primitive marks the rows it touched, so `jlStagePresent` knows what to copy. -Primitives work in colour indices. Per-scanline palette resolution happens only -at display time, so drawing does not care which palette a line uses. +Primitives work in colour indices, and use only the low 4 bits of one. +Per-scanline palette resolution happens only at display time, so drawing does +not care which palette a line uses. === jlSurfaceClear @@ -185,8 +186,10 @@ void jlFloodFill(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor); ---- Flood-fills a 4-connected region, replacing every pixel of the original colour -reachable by north, south, east and west steps. A no-op if the start point is -off-surface or already `newColor`. +reachable by north, south, east, and west steps. A no-op if the start point is +off-surface or already `newColor`. The fill keeps a 512-entry stack of spans +still to visit, and a region complex enough to need more is left partly +filled, the same way on every port. [cols="1,1,4", options="header"] |=== @@ -208,7 +211,9 @@ void jlFloodFillBounded(jlSurfaceT *s, int16_t x, int16_t y, Flood-fills a 4-connected region, stopping at pixels of `boundaryColor` rather than at pixels differing from the seed colour. Every reachable pixel that is -not the boundary colour becomes `newColor`. +not the boundary colour becomes `newColor`. Pixels that are `newColor` already +stop the fill too, so a region behind them is not reached, and a start on +either colour does nothing. The 512-entry span stack of `jlFloodFill` applies. This is the primitive for vector-art playback, such as Sierra-style picture drawing: outline a closed region with `jlDrawLine` in the boundary colour, then diff --git a/docs/ApiFile.adoc b/docs/ApiFile.adoc index 7797b5c..91f346e 100644 --- a/docs/ApiFile.adoc +++ b/docs/ApiFile.adoc @@ -5,27 +5,47 @@ Declared in ``. Two directories sit beside the executable, staged there by each platform's disk packager: -* `DATA/` -- read-only game content: levels, sprites, audio. +* `DATA/` -- game content: levels, sprites, audio. * `SAVES/` -- writable saves and preferences. -DVX is the exception, because its programs share one current directory: -there `DATA/` is beside the program, in its folder under DVX's `APPS`, and -`SAVES/` is in the program's own settings folder, `CONFIG\APPS\JOEYLIB\NAME`, -which DVX keeps apart from the program files so a reinstall never overwrites -it. The calls below find both there by themselves. +Where the calls look for them: + +* *The hosted ports* make the program's own folder the current directory in + `jlInit`, so both are found beside the program wherever it was started + from. +* *DOS, the ST, the Amiga, and the X68000* look in the current directory. A + program started from its own folder (as a desktop starts it) finds them; + one started from a command line elsewhere finds no `DATA/` and makes its + `SAVES/` there. +* *The IIgs* looks for `DATA/` under GS/OS prefix 0 and puts `SAVES/` in the + application's folder (prefix 1). When the two differ, data and saves come + from different folders. +* *DVX* is the exception, because its programs share one current directory: + there `DATA/` is beside the program, in its folder under DVX's `APPS`, and + `SAVES/` is in the program's own settings folder, + `CONFIG\APPS\JOEYLIB\NAME`, which DVX keeps apart from the program files so + a reinstall never overwrites it. The calls below find both there by + themselves. Both are addressed by a *bare name relative to their directory*. The prefix is added for you, so never hard-code `"DATA/"` or `"SAVES/"` yourself. [IMPORTANT] ==== -Keep names portable across the retro filesystems: at most 8 characters plus a -short extension, letters and digits only, so they satisfy both ProDOS and DOS -8.3. Do not depend on case being preserved. +Keep names portable across the retro filesystems: at most 8 characters plus +an extension of up to 3, letters and digits only, starting with a letter, so +they satisfy both ProDOS (whose names must start with a letter) and DOS 8.3. +The save calls refuse any other name on every port, as if the file could not +be opened; names in `DATA/`, which ship with the program, are passed through +as they are. Do not depend on case being preserved, and use the same case +every time: on Linux, Haiku, BeOS, and the web, `HERO.SAV` and `hero.sav` are +different files. ==== A failed open or write is non-fatal and simply reports failure, so save code can degrade gracefully on a platform or volume where `SAVES/` is unavailable. +On the IIgs at most 21 files can be open at once, and saves are created as +text files (type TXT). === jlDataOpen @@ -34,7 +54,10 @@ can degrade gracefully on a platform or volume where `SAVES/` is unavailable. FILE *jlDataOpen(const char *name, const char *mode); ---- -Opens a read-only data file. The `DATA/` prefix is prepended automatically. +Opens a data file. The `DATA/` prefix is prepended automatically. The mode is +passed to `fopen` as it is, so a writing mode works where the medium allows it, +but `DATA/` is meant to be read: on the web, for instance, what is written +there is lost when the page closes. [cols="1,1,4", options="header"] |=== @@ -74,7 +97,9 @@ whole-file blob prefer `jlSaveWrite` and `jlSaveRead`. bool jlSaveWrite(const char *name, const void *buf, uint32_t len); ---- -Writes a whole save file, replacing any existing contents. +Writes a whole save file, replacing any existing contents. The write is not +atomic: if it fails part way (a full disk, say) the save is left short. A +`NULL` `buf` with a non-zero `len` returns `false` and leaves the save alone. [cols="1,1,4", options="header"] |=== @@ -145,8 +170,11 @@ uint32_t jlDiskFree(void); *Parameters:* none. *Returns:* free space in bytes on the volume holding `SAVES/`. Returns 0 if it -cannot be determined -- some ports have no query for it -- and saturates at -`UINT32_MAX` on volumes larger than 4 GB. +cannot be determined, and saturates at `UINT32_MAX` on volumes larger than +4 GB. On DOS and Win16 the query (DOS's own) stops near 2 GB on a larger +volume, and on the web it is the browser's own estimate of what the site may +still store (its quota less what it uses), which the browser may round or +pad. [source,c] ---- diff --git a/docs/ApiInput.adoc b/docs/ApiInput.adoc index 9229a52..ec87b71 100644 --- a/docs/ApiInput.adoc +++ b/docs/ApiInput.adoc @@ -2,13 +2,13 @@ Declared in ``. -Call `jlInputPoll()` exactly once per frame to refresh keyboard, mouse and +Call `jlInputPoll()` exactly once per frame to refresh keyboard, mouse, and joystick state. Everything else in this section reads the state that poll captured. Two parallel keyboard interfaces share that one poll. The *predicates* answer "is the fire key held?", which is the right shape for games. The *character -queue* answers "which character did the user type?", with shift, caps lock and +queue* answers "which character did the user type?", with shift, caps lock, and the machine's keyboard layout already applied, which is the right shape for text entry. They coexist freely. @@ -20,32 +20,33 @@ transition happened. [source,c] ---- typedef enum { - KEY_NONE = 0, - KEY_A .. KEY_Z, - KEY_0 .. KEY_9, - KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE, - KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, - KEY_LSHIFT, KEY_RSHIFT, KEY_LCTRL, KEY_LALT, - KEY_F1 .. KEY_F10, - KEY_COUNT + JL_KEY_NONE = 0, + JL_KEY_A .. JL_KEY_Z, + JL_KEY_0 .. JL_KEY_9, + JL_KEY_SPACE, JL_KEY_ESCAPE, JL_KEY_RETURN, JL_KEY_TAB, JL_KEY_BACKSPACE, + JL_KEY_UP, JL_KEY_DOWN, JL_KEY_LEFT, JL_KEY_RIGHT, + JL_KEY_LSHIFT, JL_KEY_RSHIFT, JL_KEY_LCTRL, JL_KEY_LALT, + JL_KEY_F1 .. JL_KEY_F10, + JL_KEY_COUNT } jlKeyE; typedef enum { - MOUSE_BUTTON_NONE = 0, MOUSE_BUTTON_LEFT, - MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT + JL_MOUSE_BUTTON_NONE = 0, JL_MOUSE_BUTTON_LEFT, + JL_MOUSE_BUTTON_RIGHT, JL_MOUSE_BUTTON_MIDDLE, JL_MOUSE_BUTTON_COUNT } jlMouseButtonE; -typedef enum { JOYSTICK_0 = 0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE; -typedef enum { JOY_BUTTON_0 = 0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE; +typedef enum { JL_JOYSTICK_0 = 0, JL_JOYSTICK_1, JL_JOYSTICK_COUNT } jlJoystickE; +typedef enum { JL_JOY_BUTTON_0 = 0, JL_JOY_BUTTON_1, JL_JOY_BUTTON_COUNT } jlJoyButtonE; ---- -Constants: `JOYSTICK_AXIS_MAX` (127) and `JOYSTICK_AXIS_MIN` (-127); +Constants: `JL_JOYSTICK_AXIS_MAX` (127) and `JL_JOYSTICK_AXIS_MIN` (-127); `JL_CHAR_QUEUE_SIZE` (32). Control codes delivered through the character queue: `JL_CHAR_BACKSPACE` -(0x08), `JL_CHAR_TAB` (0x09), `JL_CHAR_RETURN` (0x0D), `JL_CHAR_ESCAPE` (0x1B) +(0x08), `JL_CHAR_TAB` (0x09), `JL_CHAR_RETURN` (0x0D), `JL_CHAR_ESCAPE` (0x1B), and `JL_CHAR_DELETE` (0x7F). That last one is *forward* delete, the key marked -Del -- a text editor needs both it and backspace. +Del -- a text editor needs both it and backspace. The IIgs never delivers it: +its Delete key is the backspace key, and types `JL_CHAR_BACKSPACE`. === jlInputPoll @@ -55,7 +56,9 @@ void jlInputPoll(void); ---- Refreshes all input state and refills the typed-character queue. Call once per -frame, typically just before drawing. +frame, typically just before drawing. A key pressed and let go between two +polls still reads down for one poll, so `jlKeyPressed` sees a quick tap +(except on DVX, which reads its keyboard only as it is at the poll). *Parameters:* none. *Returns:* nothing. @@ -72,11 +75,13 @@ not polling means nothing ever changes. int jlInputGetChar(void); ---- -Pops the next typed character. Shift, caps lock and the keyboard layout have +Pops the next typed character. Shift, caps lock, and the keyboard layout have already been applied by the backend, so punctuation and shifted symbols arrive -correctly. The layout is the machine's own on IIgs, Amiga, ST and X68000, -DVX's own on DVX and the operating system's on SDL3; the DOS port translates -with a fixed US layout. +correctly. The layout is the machine's own on the IIgs, Amiga, ST, and +X68000. It is DVX's own on DVX, and the operating system's on SDL3, the web, +Win32, Win16, Haiku, and BeOS. DOS, and the PSP's on-screen keyboard, use a +fixed US layout. On the ST a key held with Alt types nothing, so the +characters a national TOS puts on Alt cannot be typed. Arrow keys are not characters and stay on the predicate API. @@ -88,9 +93,16 @@ Arrow keys are not characters and stay on the predicate API. [NOTE] ==== The queue holds `JL_CHAR_QUEUE_SIZE - 1` characters, so 31. When full, further -characters are dropped until you pop some. Auto-repeat may deliver repeats on -ports whose keyboard services repeat; repeat policy is yours to manage. 7-bit -ASCII only -- no Unicode, no input methods. +characters are dropped until you pop some. A chord types nothing on any port: +Ctrl+M is not Return and Ctrl+1 is not 1, and Alt chords type nothing where +Alt is a command key (DOS, DVX, Windows, the Be ports' Command key, the PSP's +on-screen keyboard); the Amiga's Alt and Windows' AltGr type their layout's +characters, and so does the ST's Alt where TOS keeps its keyboard's Alt +layer in its tables (TOS 4.00 and later, and EmuTOS: a German keyboard's +@ [ ] { } \), and otherwise nothing (<>). A held key +repeats its character on every port (the ST at the rate TOS is set to); +repeat policy is yours to manage. 7-bit ASCII only -- no Unicode, no input +methods. The return type is plain `int`, not a stdint type, deliberately: this is the `getchar()`-style "byte or -1" contract. @@ -103,9 +115,12 @@ The return type is plain `int`, not a stdint type, deliberately: this is the void jlWaitForAnyKey(void); ---- -Blocks until the user presses any key, polling internally so per-port input -machinery -- including interrupt-driven audio samplers -- keeps running during -the wait. +Blocks until the user presses a key -- one JoeyLib names (with a `jlKeyE`), +or one that types a character, punctuation for instance -- polling internally +so per-port input machinery, including interrupt-driven audio samplers, keeps +running during the wait. The mouse and joystick buttons do not end it, and a +key already held when the wait starts must be let go and pressed again (its +auto-repeat does not count). *Parameters:* none. *Returns:* nothing. The character queue is emptied on return, so the dismissing keystroke is consumed rather than delivered as text. @@ -129,6 +144,16 @@ bool jlKeyReleased(jlKeyE key); `true` only in the frame the key went down; `jlKeyReleased` only in the frame it came up. +A named key that two physical keys give is down while either is held, and +comes up with the last of them: `JL_KEY_RETURN` is Return and the keypad's +Enter; `JL_KEY_UP`, `JL_KEY_DOWN`, `JL_KEY_LEFT`, and `JL_KEY_RIGHT` are the +arrow keys and the keypad's 8, 2, 4, and 6; and `JL_KEY_LCTRL` and +`JL_KEY_LALT` are the left and right Ctrl and Alt, where the keyboard has +both (on the X68000, OPT.1 and OPT.2 are Alt). The keypad's keys still type +what the machine's keypad types (`jlInputGetChar`). The IIgs knows keys by +their characters, so its keypad reads as the digits +(<>). + === jlMousePresent [source,c] @@ -139,13 +164,10 @@ bool jlMousePresent(void); *Parameters:* none. *Returns:* `true` if the platform confirmed a mouse at init. On DOS that means -the INT 33h driver reset successfully; on Amiga, ST, IIgs, X68000, DVX and -SDL3 the OS mouse is always wired. On DVX the position is the pointer's over -the program's window, divided by the window's scale; it keeps its last value -while the pointer is elsewhere. On DVX's full screen it is the pointer's -place on the stage (DVX draws no pointer there). On SDL3 it is the pointer's -place on the stage however the window scales it, held to the stage's edge -while the pointer is beside it; the OS pointer is hidden over the window. +the INT 33h driver reset successfully; on the Amiga, that JoeyLib's input +window opened; on Win32 and Win16, that Windows reports a mouse; on SDL3 and +the web, that SDL reports one. The IIgs, ST, X68000, DVX, Haiku, and BeOS +always report one, and the PSP never does. === jlMouseX / jlMouseY @@ -161,6 +183,14 @@ int16_t jlMouseY(void); Until the platform reports the pointer, it is the middle of the stage, (160, 100), on every port. +On DVX the position is the pointer's over the program's window, divided by +the window's scale; it keeps its last value while the pointer is elsewhere. On +DVX's full screen it is the pointer's place on the stage (DVX draws no pointer +there). On the hosted ports it is the pointer's place on the stage however the +window scales it, held to the stage's edge while the pointer is beside it; the +OS pointer is hidden over the window. On DOS the mouse driver keeps it in +range. + === jlMouseDown / jlMousePressed / jlMouseReleased [source,c] @@ -178,6 +208,10 @@ bool jlMouseReleased(jlMouseButtonE button); *Returns:* held state, rising edge, and falling edge respectively. +The IIgs mouse has one button, `JL_MOUSE_BUTTON_LEFT`. The ST and X68000 have +left and right. The other ports report left, right, and middle, as far as the +mouse has them. The PSP has no mouse. + === jlJoystickConnected [source,c] @@ -188,13 +222,20 @@ bool jlJoystickConnected(jlJoystickE js); [cols="1,1,4", options="header"] |=== |Parameter |Type |Description -|`js` |`jlJoystickE` |Which stick, `JOYSTICK_0` or `JOYSTICK_1`. +|`js` |`jlJoystickE` |Which stick, `JL_JOYSTICK_0` or `JL_JOYSTICK_1`. |=== *Returns:* `true` if the platform reports a stick on that port. On the ST and X68000 both ports always report one: an empty port cannot be told from a stick at rest. The Amiga (`lowlevel.library`) has the same blind spot the other -way: it reports a stick only once it has moved, and never a mouse. +way: it reports a stick only once it has moved, and never a mouse. The IIgs +has one stick, `JL_JOYSTICK_0`, and stops looking for it after about a second +with none seen, until `jlJoystickReset`. On DOS a stick is connected while the +game port times both its axes. SDL3, the web, and the PSP see SDL's gamepads +and joysticks both, a joystick SDL has no gamepad mapping for read by its first +two axes, its first hat, and its first two buttons; Win32, Win16, Haiku, and +BeOS see the system's joysticks, and Haiku and BeOS notice a new one within a +second. === jlJoystickX / jlJoystickY @@ -210,8 +251,11 @@ int8_t jlJoystickY(jlJoystickE js); |`js` |`jlJoystickE` |Which stick. |=== -*Returns:* axis value, -127 to +127. Digital sticks snap to the extremes; -analog sticks return the raw centred value. Test against a threshold rather +*Returns:* axis value, -127 to +127. Digital sticks (the Amiga, ST, and +X68000) snap to the extremes; analog sticks (the IIgs, DOS, DVX, and the +hosted ports) return their offset from the centre, scaled to that range, with +the dead zone applied. On the IIgs and DOS the axes read 0 until the stick's +centre has been read (<<_jljoystickreset>>). Test against a threshold rather than for exact values, so the same code works with both. === jlJoyDown / jlJoyPressed / jlJoyReleased @@ -232,6 +276,15 @@ bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button); *Returns:* held state, rising edge, and falling edge respectively. +Every stick has two buttons. On the ST and STe's own joystick ports, and on +the Amiga until a stick's second button is first pressed, a stick is taken to +have one: a quick press of it is `JL_JOY_BUTTON_0`, down for the one poll after +it is let go, and a press held for half a second is `JL_JOY_BUTTON_1`, down +from then until it is let go. So act on button 0 with `jlJoyPressed`, and do +not count on it staying down while the button is held. Elsewhere the two +buttons are the stick's own, and a one-button stick never reports +`JL_JOY_BUTTON_1`. + === jlJoystickReset [source,c] @@ -239,34 +292,35 @@ bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button); void jlJoystickReset(jlJoystickE js, uint8_t deadZone); ---- -Re-enables joystick polling and recalibrates the resting position. +Sets the stick's dead zone and, where the port can, re-centres it and looks +for it again: -This matters on the IIgs. That port *auto-disables* joystick polling after a -short window with no stick detected, because polling costs about 3 ms a frame -there -- and it does not re-probe on its own. If a user plugs a stick in while -your program runs, you must call this to resume polling. +* *IIgs*: polling starts again if it had stopped for want of a stick, and the + stick is re-centred at its next reading. +* *DOS*: the game port is probed again, and the stick is re-centred at its + next reading. +* *Amiga*: the port goes back to `lowlevel.library`'s own detection. +* *Elsewhere* only the dead zone changes. + +On the IIgs polling for a stick that is not there stops. If a user may plug a +stick in while your program runs, call this, with the stick centred, to pick +it up. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`js` |`jlJoystickE` |Which stick. -|`deadZone` |`uint8_t` |Raw readings within this many units of centre clamp - to 0. Pass 0 to disable the dead zone. +|`deadZone` |`uint8_t` |Axis values within this many units of 0 (on the + -127..127 scale) read 0, on the ports with analog sticks: the IIgs, DOS, DVX, + and the hosted ports. Each stick starts with 8, so a stick left alone reads 0 + though its pots jitter. Pass 0 to disable the dead zone. |=== *Returns:* nothing. [IMPORTANT] ==== -The next poll after this call captures the stick's *current* raw position as -the new centre, so the user must be holding the stick centred when you call it. - -On Amiga, ST and DOS the sticks are genuinely digital and already report -1, 0 -or +1, so recalibration is a no-op and `deadZone` is ignored. DVX's sticks are -analog but calibrated, dead zone included, in DVX's own Control Panel, so the -call is a no-op there too. SDL3's sticks are gamepads the OS calibrates; there -the call only sets the dead zone, inside which the stick reads as centred. -The call still -clears any auto-disconnect state, so it is safe and correct to make it -unconditionally. +On the IIgs and DOS the next reading after this call is taken as the stick's +centre, so the user must be holding the stick centred when you call it. The +call is safe to make unconditionally. ==== diff --git a/docs/ApiPalette.adoc b/docs/ApiPalette.adoc index b6496ee..85ac728 100644 --- a/docs/ApiPalette.adoc +++ b/docs/ApiPalette.adoc @@ -10,11 +10,18 @@ which palette each scanline uses. That is how a JoeyLib program shows more than [IMPORTANT] ==== Colour 0 of every palette is black, always. `jlPaletteSet` silently masks entry -0 to `$000` whatever you pass. This is deliberate: the Amiga and ST hardware -border follows colour 0 of the active palette, and colour 0 is the transparent -colour for sprite drawing. You have 15 usable colours per palette. +0 to `$000` whatever you pass, and `jlSurfaceLoadFile` does the same to every +palette it loads. This is deliberate: the Amiga and ST hardware border follows +colour 0 of the active palette, and colour 0 is the transparent colour for +sprite drawing. You have 15 usable colours per palette. ==== +Palette and SCB changes to the stage reach the display at the next +`jlStagePresent`, not before. Out-of-range arguments are ignored +the same way on every port: a palette index above 15 or a line above 199 makes +the setters do nothing (`jlScbSetRange` stops its range at line 199), and +`jlScbGet` returns 0 for a line off the surface. + === jlPaletteSet [source,c] @@ -42,7 +49,9 @@ Loads all 16 colours of one palette. void jlPaletteGet(const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16); ---- -Reads all 16 colours of one palette. +Reads all 16 colours of one palette, as they were set: the values passed to +`jlPaletteSet` (with entry 0 forced to `$000`, and bits 12 to 15 kept as +passed), not what the display can show of them. [cols="1,1,4", options="header"] |=== @@ -100,7 +109,7 @@ screen, whether that is one palette everywhere or several horizontal bands. ---- // Sky on the top third, ground below, using two palettes. jlScbSetRange(stage, 0, 65, 0); -jlScbSetRange(stage, 66, SURFACE_HEIGHT - 1, 1); +jlScbSetRange(stage, 66, JL_SURFACE_HEIGHT - 1, 1); ---- === jlScbGet diff --git a/docs/ApiSerial.adoc b/docs/ApiSerial.adoc index f2d1f38..aea5cf3 100644 --- a/docs/ApiSerial.adoc +++ b/docs/ApiSerial.adoc @@ -23,16 +23,15 @@ the ST goes through the BIOS Aux channel; the X68000 transmits through IOCS and receives on its own SCC interrupt; DVX goes through DVX's serial library, which owns the COM ports for every program on the desktop (a port another program has open cannot be opened); Win32 and Win16 go through Windows' own -communications calls, whose driver owns the ports the same way. SDL3, the web -and the PSP have no serial port: `jlSerialOpen` fails there. +communications calls, whose driver owns the ports the same way. SDL3, the web, +the PSP, Haiku, and BeOS have no serial port: `jlSerialOpen` fails there. The model is *non-blocking and buffered*. Received bytes accumulate in a -background ring -- a hardware interrupt on DOS, the X68000 and the IIgs, the -OS on Amiga, ST, DVX and Windows -- and reads never block. Call `jlSerialPoll` once per -frame: it is next to a no-op where the interrupt or OS already fills the ring, -and the hardware drain where nothing does, so calling it every frame is always -correct. On DVX it also gives the other programs their turn -(<<_dvx_programs>>). +background ring -- a hardware interrupt on DOS, the X68000, and the IIgs, the +OS on Amiga, ST, DVX, and Windows -- and reads never block. Call `jlSerialPoll` +once per frame. (The X68000 takes its interrupt only when the port is opened in +supervisor mode, which `jlInit` puts every program in; opened without it, the +port is drained from RSDRV.SYS's own 64-byte buffer by the serial calls.) On the IIgs a Super Serial Card only interrupts with its interrupt switch (SW2-6) on; with it off the card still works, drained by `jlSerialPoll`, but @@ -71,6 +70,10 @@ at 9600 baud, so: At 19200 baud each lasts half as long, and so on. +A byte that arrives with a framing, parity, or overrun error is dropped on the +X68000 and delivered as it came on the IIgs and DOS; on the other ports the +system's driver decides. + JoeyLib's own work is safe while a port is open, with one exception on the X68000 (below). On the IIgs, where the display and sound code masks interrupts for long stretches, opening a port @@ -89,7 +92,8 @@ These can still lose bytes, and are the program's to schedule around: * *Disk access.* On the IIgs, GS/OS holds interrupts off while it reads or writes a 3.5-inch floppy: 65 to 220 ms at a time, measured. That is every file call -- `jlFile*`, `jlSave*`, loading assets and sprite banks, and - `jlAudioInit`, which loads the replayer from disk. The other ports' disk + `jlInit` unless `jlConfigT.noAudio` is set, since starting audio loads the + replayer from disk. The other ports' disk access goes through the operating system and has not been measured; treat it as a risk there too. * *Starting a song on the IIgs.* `jlAudioPlayMod` loads the song's @@ -100,13 +104,13 @@ These can still lose bytes, and are the program's to schedule around: * *A Super Serial Card above 2400 baud.* The IIgs's own interrupt handling can outlast the 6551's single byte, so the card runs loss-free at 2400 and above that relies on the protocol to recover. The built-in modem and printer ports - stay loss-free at 9600 through full-screen presents, clears, joystick reads + stay loss-free at 9600 through full-screen presents, clears, joystick reads, and sound effects. * *Per-scanline palettes on the X68000.* The port reloads its one hardware palette as the beam scans, and a run of palette changes on consecutive lines holds serial receive off for the whole run, about 32 microseconds a line. Receive stays loss-free while such runs are under about 90 lines at 9600 - baud, 40 at 19200 and 15 at 38400; palette changes three or more lines apart + baud, 40 at 19200, and 15 at 38400; palette changes three or more lines apart release it between bands. * *The program's own code*, wherever it masks interrupts. @@ -121,6 +125,48 @@ Ways to live with them: * For data that must arrive intact, use a protocol with checksums and retransmission; XMODEM and its descendants exist for exactly this. +=== Flow Control + +`flow` in `jlSerialConfigT` picks the handshake, and every port with a serial +port honours it: + +* `JL_SERIAL_FLOW_RTSCTS`: transmit waits while the peer holds CTS down, and + the port drops RTS when its receive buffer is three quarters full, raising + it again once reads have it down to a quarter. +* `JL_SERIAL_FLOW_XONXOFF`: the peer's XOFF and XON (0x13 and 0x11) pause and + resume transmit and never reach `jlSerialRead`, and the port sends XOFF + when its receive buffer is three quarters full and XON at a quarter. + +The receive buffer, which also decides how long a program may go without +reading when there is no flow control: + +[cols="3,2,2", options="header"] +|=== +|Port |Receive buffer |Lasts at 9600 baud +|IIgs |512 bytes |~0.5 s +|DOS, DVX |2 KB |~2 s +|Amiga (`serial.device`) |4 KB |~4 s +|Atari ST (TOS's, enlarged while the port is open) |4 KB |~4 s +|X68000 |4 KB |~4 s +|Win32, Win16 (Windows' driver) |4 KB |~4 s +|=== + +Where they differ: + +* The IIgs modem and printer ports' connector (mini-DIN-8) has no RTS line: + its one handshake output is DTR. RTS/CTS there makes transmit wait for CTS, + but nothing can tell the peer to pause, so receive needs XON/XOFF or a + protocol that paces itself. A Super Serial Card has RTS, and its 6551 also + stops its own transmitter while RTS is down. +* On the ST, the Amiga, Win32, and Win16 the operating system's driver does + the handshaking, and under RTS/CTS it may change RTS after + `jlSerialSetRts` did. DVX's library holds the peer off at four fifths full + and lets it go at a fifth. +* The X68000 drops RTS from its receive interrupt, but sends XOFF and XON + from `jlSerialPoll`, `jlSerialRead`, and `jlSerialWrite`: under XON/XOFF, a + program that calls none of them for longer than the last quarter of the + buffer lasts (about a second at 9600 baud) can still lose bytes. + === Types [source,c] @@ -143,7 +189,7 @@ typedef enum { } jlSerialFlowE; typedef struct { - uint32_t baud; // 300 .. 115200, clamped per port + uint32_t baud; // 300 .. 115200; each port runs its nearest uint8_t dataBits; // 5 .. 8 uint8_t stopBits; // 1 or 2 jlSerialParityE parity; @@ -152,10 +198,54 @@ typedef struct { } jlSerialConfigT; ---- +A field left 0 takes a default: `baud` 9600, `dataBits` 8, and `stopBits` 1. +`jlSerialOpen` fails for `dataBits` outside 5 to 8, `stopBits` other than 1 or +2, or a `parity` or `flow` the enumerations do not name. + +`baud` is the rate asked for. Each port runs at the nearest rate it has: + +[cols="2,5", options="header"] +|=== +|Port |Rates + +|IIgs modem and printer ports +|115200 divided by a whole number, up to 57600; a rate between two of those + runs at the one above it, and anything faster at 57600. + +|IIgs Super Serial Card +|The 6551's fifteen rates, 50 to 19200; any other runs at the fastest one + below it. + +|DOS +|115200 divided by a whole number, up to 115200; a rate between two of those + runs at the one above it. + +|DVX +|DVX's library's rates, 50 to 115200; any other runs at the fastest one below + it. + +|Amiga +|The rate as asked, handed to `serial.device`. + +|Atari ST +|The MFP's sixteen rates, 50 to 19200; any other runs at the fastest one below + it. + +|X68000 +|75 to 38400, each double the last (38400 is really 39062); any other runs at + the fastest one below it. + +|Win32 +|The rate as asked, handed to Windows' driver. + +|Win16 +|The rate as asked up to 57600; above that, see below. +|=== + The IIgs is the machine with three device choices; elsewhere `JL_SERIAL_MODEM` and `JL_SERIAL_PRINTER` both mean the default port. `JL_SERIAL_DEFAULT` selects -the IIgs modem port, COM1 on DOS, DVX, Win32 and Win16, the ST Modem port, -Amiga unit 0 or the X68000's RS-232C port. +the IIgs modem port; COM1 on DOS, DVX, Win32, and Win16; the ST Modem port; +Amiga unit 0; or the X68000's RS-232C port. On Win16 a `baud` above 57600 runs at 115200 through KPCOMM.DRV, the communications driver JoeyLib's Windows runs with in place of Windows 3.1's @@ -167,14 +257,13 @@ RSDRV.SYS. Every Human68k system disk has it and the stock CONFIG.SYS loads it (`DEVICE = \SYS\RSDRV.SYS`), as do JoeyLib's own X68000 boot disks; a game shipped to boot on its own must keep it. Without it `jlSerialOpen` returns `false` and `jlLastError` says the driver is missing. With it the -port runs at 75 to 38400 bps, and `JL_SERIAL_FLOW_RTSCTS` makes transmit wait -for CTS; RTS stays up, since JoeyLib's receive interrupt, not the driver's, -fills the receive ring. 38400 is really 39062 bps, 1.7% fast, within the -usual tolerance of a UART at the far end. +port runs at 75 to 38400 bps. 38400 is really 39062 bps, 1.7% fast, within +the usual tolerance of a UART at the far end. `unit` disambiguates `JL_SERIAL_SLOT` and is ignored otherwise: an IIgs Super Serial Card slot 1..7, a DOS or DVX COM number 1..4, a Win16 COM number -1..9, a Win32 COM number 1..255, or an Amiga `serial.device` unit. +1..9, a Win32 COM number 1..255, or an Amiga `serial.device` unit. The ST and +X68000 have one port, and open it for every device, `JL_SERIAL_SLOT` included. === jlSerialOpen @@ -194,9 +283,10 @@ open without an intervening close fails. |=== *Returns:* `true` on success. `false` if the port cannot be opened or the -platform has no serial support -- and then every other call no-ops. On the -X68000 it is also `false` when RSDRV.SYS is not loaded, and `jlLastError` -says so. +platform has no serial support -- and then every other call no-ops. It is +also `false`, with `jlLastError` saying why, when no UART answers at the DOS +COM port, when no Super Serial Card answers in the IIgs slot, and when +RSDRV.SYS is not loaded on the X68000. === jlSerialClose @@ -207,9 +297,12 @@ void jlSerialClose(void); Closes the port and releases its resources. Safe when not open. A port still open when the program exits is closed then: the first successful -`jlSerialOpen` registers an `atexit` handler, because on DOS, the X68000 and +`jlSerialOpen` registers an `atexit` handler, because on DOS, the X68000, and the IIgs the port's receive interrupt vector points into the program until it -is closed. +is closed. Bytes `jlSerialWrite` accepted are still sent, so a program can +write a farewell and close straight away. DOS, the ST, and the X68000 put back +the speed and framing the port had before the open; the IIgs (whose serial +chip's settings cannot be read back) and DVX leave it as JoeyLib set it. Closing does *not* hang up: `jlSerialClose` leaves DTR exactly as it is, so a modem call or a BBS caller survives the program handing the line back: a @@ -291,7 +384,11 @@ uint16_t jlSerialRead(uint8_t *buf, uint16_t max); uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len); ---- -Sends bytes. Transmit is a bounded poll. +Sends bytes without waiting long. Most ports feed the bytes to the line as it +takes them and give up after a bounded wait when it stalls (a peer holding +transmit off, say); the Amiga queues them in two 1 KB buffers it sends from in +the background, and Win16 in Windows' driver's queue, so neither waits at +all. [cols="1,1,4", options="header"] |=== @@ -386,7 +483,11 @@ Asserts or drops RTS, for a caller doing its own receive-side flow control. `JL_SERIAL_FLOW_RTSCTS` asks the *port* to handshake; this is the other half -- the application saying "my buffer is nearly full, stop sending", and later "go ahead". That is what a client with a small ring and a peer that can be told to -pause actually needs. +pause actually needs. Under `JL_SERIAL_FLOW_RTSCTS` on DOS, the X68000, and a +IIgs Super Serial Card, RTS is up only while both the port's handshake and +this call want it; elsewhere the driver may change it again (see Flow +Control). The IIgs modem and printer ports have no RTS line on their +connector, so nothing reaches the peer there. [cols="1,1,4", options="header"] |=== diff --git a/docs/ApiSprite.adoc b/docs/ApiSprite.adoc index 9213269..8603fb1 100644 --- a/docs/ApiSprite.adoc +++ b/docs/ApiSprite.adoc @@ -7,14 +7,16 @@ colour-0 transparency. Sprite pixel data is `widthTiles * heightTiles * 32` bytes of 4bpp packed data, laid out as a flat blob of tiles in row-major order. The distinguishing feature is that *sprite blitters are compiled at run time*. -The library emits native 65816, 68000 or x86 code for each sprite, including -pre-shifted variants so smooth horizontal motion needs no runtime bit shifting. +On the IIgs, Amiga, ST, X68000, DOS, DVX, and Win16 the library emits native +65816, 68000, or x86 code for each sprite, including pre-shifted variants so +smooth horizontal motion needs no runtime bit shifting. SDL3, the web, Win32, +the PSP, Haiku, and BeOS have no code emitter and always use the interpreter. [IMPORTANT] ==== `jlSpriteDraw` does *not* compile on demand. An uncompiled sprite draws through -an interpreter roughly *forty times slower*, and nothing reports this. Always -call `jlSpriteCompile` or `jlSpritePrewarm` during loading. +an interpreter *two to six times slower*, depending on the machine, and +nothing reports this. Always call `jlSpriteCompile` during loading. ==== === jlSpriteT and jlSpriteBackupT @@ -35,33 +37,42 @@ typedef struct { ---- You supply `bytes` and its capacity; the library fills in the rest to describe -what it actually captured. On planar ports the captured region is the -*16-pixel-aligned window* containing the sprite, not the sprite rectangle, and -the `x`, `width`, `height` and `sizeBytes` fields report that true geometry. It -is the recorded window you must not overwrite before restoring. +what it actually captured, in `x`, `y`, `width`, `height`, and `sizeBytes`. +That is often not the sprite rectangle: -=== JOEY_SPRITE_BACKUP_BYTES +* `x` is rounded down to an even pixel, and `width` grows to match, on every + port. +* A compiled save on the Amiga, ST, and X68000 captures the *16-pixel-aligned + region* containing the sprite. +* A compiled save on the IIgs captures only the rows that hold an opaque + pixel, so `y` can be lower and `height` shorter than the sprite's. +* A save of a sprite partly off the surface captures only the visible part. + +It is the recorded region you must not overwrite before restoring. + +=== jlSpriteBackupLength [source,c] ---- -#define JOEY_SPRITE_BACKUP_BYTES(_wT, _hT) /* ... */ +#define jlSpriteBackupLength(_type, _wT, _hT) /* ... */ ---- -Worst-case backup capacity for a `widthTiles` by `heightTiles` sprite at any x -position, on every port. Use it to size your backing array. +How many `_type` elements a backup buffer needs for a `widthTiles` by +`heightTiles` sprite at any x position, on every port, rounded up. Use it to +size your backing array; with `uint8_t` it is the size in bytes. [source,c] ---- -// 2-byte aligned by construction -- see the warning below. -static uint16_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4) / 2]; +static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)]; ---- [WARNING] ==== -Give the backing array *at least 2-byte alignment*. An odd buffer address -routes save and restore onto slow interpreted paths on the planar ports, -because the 68000 cannot do word accesses at odd addresses. Declaring it -`uint16_t`, as above, guarantees this. +Give the backing array *at least 2-byte alignment*. On the Amiga, ST, and +X68000 an odd buffer address sends the save and restore calls down a much +slower path, and on the Amiga and ST it makes the trusted variants crash; the +other ports do not mind. Declaring it `uint16_t`, as above, guarantees the +alignment. ==== === jlSpriteBankLoad @@ -109,8 +120,10 @@ replacement for `jlSpriteBankLoad` plus a compile pass. *Parameters:* as `jlSpriteBankLoad`. -*Returns:* the number of cels written, or 0 if the file is missing or its -magic, target or shift count does not match this build. +*Returns:* the number of cels written, or 0 if the file is missing, its +magic, target, or shift count does not match this build, its cel size is +out of range, or the 64 KB the loader reads each cel's code into cannot be +allocated (on the ports that interpret every sprite too). === jlSpriteBankSavePrecompiled @@ -129,14 +142,16 @@ cache the large per-platform compiled form on first run. |=== |Parameter |Type |Description |`path` |`const char *` |Destination path. -|`cels` |`jlSpriteT **` |The sprites to cache. Uncompiled ones are cached - as-is and will compile again next run. +|`cels` |`jlSpriteT **` |The sprites to cache, all of one size. Uncompiled + ones are cached uncompiled, and nothing compiles them when the cache is + loaded: call `jlSpriteCompile` on any cel whose `jlSpriteCompiledSize` is 0. |`count` |`uint16_t` |How many sprites. |`palette` |`const uint16_t *` |Optional, may be `NULL`. Embedded if given. |=== -*Returns:* `true` on success; `false` if the destination is not writable, which -is harmless -- the program simply keeps compiling at startup. +*Returns:* `true` on success; `false` if the cels are not all one size or the +destination is not writable, which is harmless -- the program simply keeps +compiling at startup. === jlSpriteCreate @@ -157,8 +172,8 @@ Wraps a tile-data blob in a sprite. |`heightTiles` |`uint8_t` |Height in tiles. Must be non-zero. |=== -*Returns:* the sprite, or `NULL` if either dimension is 0 or the codegen arena -cannot fit a placeholder entry. +*Returns:* the sprite, or `NULL` if `tileData` is `NULL`, either dimension is +0, or memory runs out. Creating a sprite does not touch the codegen arena. === jlSpriteDestroy @@ -185,8 +200,11 @@ built from is *not* freed -- that buffer is yours. bool jlSpriteCompile(jlSpriteT *sp); ---- -Compiles the sprite's draw routines into the codegen arena. Idempotent -- -compiling an already-compiled sprite is a no-op that returns `true`. +Compiles the sprite's draw, save, and restore routines into the codegen arena, +right away. No draw ever compiles on demand, so call it while loading, not in +the frame loop (a bank loaded with `jlSpriteBankLoadPrecompiled` arrives +compiled already). Idempotent -- compiling an already-compiled sprite is a +no-op that returns `true`. [cols="1,1,4", options="header"] |=== @@ -194,8 +212,20 @@ compiling an already-compiled sprite is a no-op that returns `true`. |`sp` |`jlSpriteT *` |Sprite to compile. |=== -*Returns:* `true` if the sprite is compiled. `false` if the arena is full (you -may call `jlSpriteCompact` and retry) or the sprite has no source tile data. +*Returns:* `true` if the sprite is compiled. `false` if it will be drawn by the +interpreter instead: the arena is full even after compacting it (`jlLastError` +says so; raise `codegenBytes` or destroy sprites you no longer need), the +sprite has no source tile data, the 64 KB the compiler works in cannot be +allocated, no routine fits the compiler's limits (16 KB a routine), or the +port has no code emitter (the Sprites row of +<<_per_platform_behaviour_summary>>). Only the full arena sets `jlLastError`. +A `false` is never fatal: the sprite still draws, only slower. + +`true` means at least part of the sprite compiled, not all of it. When the +arena is short, the Amiga, ST, and X68000 keep only the draws for x on an +8-pixel boundary, and a routine larger than 16 KB is left out on every port. +A draw the compiled routines do not cover, and every draw of a sprite partly +off the surface, goes through the interpreter. === jlSpriteCompiledSize @@ -216,24 +246,6 @@ the current platform, rather than guessing. *Returns:* bytes occupied in the codegen arena, summed over all compiled shift variants. Returns 0 if the sprite was never compiled. -=== jlSpritePrewarm - -[source,c] ----- -void jlSpritePrewarm(jlSpriteT *sp); ----- - -Hints that a sprite will be drawn soon. A wrapper around `jlSpriteCompile` that -ignores the return value, for callers that do not need to handle failure. - -[cols="1,1,4", options="header"] -|=== -|Parameter |Type |Description -|`sp` |`jlSpriteT *` |Sprite to compile. -|=== - -*Returns:* nothing. - === jlSpriteDraw [source,c] @@ -242,7 +254,8 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); ---- Draws the sprite at a pixel position. Source pixels of colour 0 are skipped. -Off-surface portions are clipped. +Off-surface portions are clipped. Compiled routines do not clip, so a sprite +partly off the surface draws through the interpreter, compiled or not. [cols="1,1,4", options="header"] |=== @@ -272,12 +285,15 @@ would write, so it can be restored later. |`sp` |`jlSpriteT *` |Sprite whose footprint to capture. |`x`, `y` |`int16_t` |Position the sprite will be drawn at. |`backup` |`jlSpriteBackupT *` |Receives the capture. You must set `bytes` to - a buffer of at least `JOEY_SPRITE_BACKUP_BYTES(w, h)` capacity for fully - in-bounds draws. For clipped draws only the visible bytes are stored. + a buffer of at least `jlSpriteBackupLength(uint8_t, w, h)` bytes for fully + in-bounds draws. For clipped draws only the visible bytes are stored. On the + IIgs the buffer must not cross a 64 KB bank boundary; `jlAlloc`, the C heap, + and static arrays never give one that does. With `bytes` `NULL` nothing is + copied: the call only fills in the region a save would record. |=== *Returns:* nothing. The captured geometry is reported in the backup's `x`, -`width`, `height` and `sizeBytes` fields. +`y`, `width`, `height`, and `sizeBytes` fields (see <<_jlspritet_and_jlspritebackupt>>). === jlSpriteRestoreUnder @@ -293,7 +309,7 @@ Repaints the destination from a previous capture. |Parameter |Type |Description |`s` |`jlSurfaceT *` |Destination surface. |`backup` |`const jlSpriteBackupT *` |A backup from `jlSpriteSaveUnder`, not - invalidated by other writes overlapping its recorded window. + invalidated by other writes overlapping its recorded region. |=== *Returns:* nothing. @@ -339,12 +355,15 @@ corrupting -- but the guarantees below are still yours to keep. Behaviour is *undefined* -- out-of-bounds writes, crashes -- unless all of the following hold: -* `s`, `backup`, `backup->sprite` and `backup->bytes` are non-`NULL`; +* `s`, `backup`, `+backup->sprite+`, and `+backup->bytes+` are non-`NULL`; * the sprite is fully on-surface at `(x, y)`, because *no clipping happens*; * for the restore, the backup came from a save that took the compiled path: - 16-pixel-aligned x and width on planar ports, even on chunky, height equal - to the sprite's pixel height, and a buffer at least 2-byte aligned; -* the recorded window is fully on the destination surface. + 16-pixel-aligned x and width on the Amiga, ST, and X68000, even on the + other ports, and a height equal to the sprite's pixel height (on the IIgs, + to the rows the compiled save keeps, those holding an opaque pixel); +* on the Amiga and ST, `+backup->bytes+` is at least 2-byte aligned, for the + trusted save and save-and-draw as well as the restore; +* the recorded region is fully on the destination surface. Use the plain calls anywhere those invariants are not statically guaranteed. ==== @@ -371,7 +390,9 @@ copied into a sprite-owned buffer, so the source may be modified afterwards. |`widthTiles`, `heightTiles` |`uint8_t` |Size in *tiles*, each 8x8 pixels. |=== -*Returns:* the new sprite, or `NULL` if the coordinates are not tile-aligned. +*Returns:* the new sprite, or `NULL` if `src` is `NULL`, a dimension is 0, +`x` or `y` is not a multiple of 8, the region is not wholly on the surface, or +memory runs out. === jlSpriteCompact @@ -403,4 +424,9 @@ Arena introspection, for judging whether to compact or to raise *Parameters:* none. *Returns:* bytes in use, and total arena size. Free space is the difference, -though it may be fragmented across holes until `jlSpriteCompact` runs. +though it may be fragmented across holes until `jlSpriteCompact` runs. On the +IIgs the total is what `jlInit` was granted, which can be less than +`codegenBytes` asked for, and it is split into blocks of at most 64 KB that +compacting never merges: one sprite's code must fit in one block. On the +ports that interpret every sprite the total is the arena they allocated +anyway, and the bytes in use stay 0. diff --git a/docs/ApiSurface.adoc b/docs/ApiSurface.adoc index ac76881..3dd3a89 100644 --- a/docs/ApiSurface.adoc +++ b/docs/ApiSurface.adoc @@ -3,9 +3,10 @@ Declared in `` and ``. A surface is a 320x200 16-colour image with its own 200-entry SCB table and its -own 16x16 palette set. Storage is target-native -- chunky 4bpp on IIgs and DOS, -planar on Amiga, ST and X68000 -- but every entry point speaks in colour -indices, so your code never sees the difference. +own 16x16 palette set. Storage is target-native -- chunky 4bpp on the IIgs, +DOS, DVX, and the hosted ports, planar on the Amiga, ST, and X68000 -- but every +entry point speaks in colour indices, so your code never sees the difference. +A new surface starts with the 16-colour EGA palette in all 16 of its palettes. === Constants @@ -13,13 +14,13 @@ indices, so your code never sees the difference. |=== |Name |Value |Meaning -|`SURFACE_WIDTH` |320 |Surface width in pixels. -|`SURFACE_HEIGHT` |200 |Surface height in pixels. -|`SURFACE_BYTES_PER_ROW` |160 |Bytes per row in chunky 4bpp form. -|`SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes. -|`SURFACE_PALETTE_COUNT` |16 |Palettes per surface. -|`SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette. -|`SURFACE_PALETTE_ENTRIES` |256 |Total palette entries per surface. +|`JL_SURFACE_WIDTH` |320 |Surface width in pixels. +|`JL_SURFACE_HEIGHT` |200 |Surface height in pixels. +|`JL_SURFACE_BYTES_PER_ROW` |160 |Bytes per row in chunky 4bpp form. +|`JL_SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes. +|`JL_SURFACE_PALETTE_COUNT` |16 |Palettes per surface. +|`JL_SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette. +|`JL_SURFACE_PALETTE_ENTRIES` |256 |Total palette entries per surface. |=== === jlSurfaceT @@ -29,7 +30,7 @@ indices, so your code never sees the difference. typedef struct jlSurfaceT jlSurfaceT; ---- -Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet` or a loader. +Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet`, or a loader. === jlSurfaceCreate @@ -38,7 +39,9 @@ Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet` or a loader. jlSurfaceT *jlSurfaceCreate(void); ---- -Allocates a new offscreen surface. +Allocates a new offscreen surface. Each one costs about 32 KB. On the Amiga +that comes from chip RAM, because the blitter works on the planes there, so on +a 512 KB machine, beside the display's own pages, only a few fit. *Parameters:* none. @@ -83,7 +86,7 @@ the displayed framebuffer until the next present. void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src); ---- -Copies pixels, SCBs and palettes from `src` into `dst`. +Copies pixels, SCBs, and palettes from `src` into `dst`. [cols="1,1,4", options="header"] |=== @@ -128,10 +131,10 @@ multiples of 16. Two consequences you own: * up to 15 extra pixels each side are copied, so a snapped erase can clobber on-screen content outside the rectangle you asked for; -* it silently invalidates any live `jlSpriteBackupT` whose recorded window it +* it silently invalidates any live `jlSpriteBackupT` whose recorded region it touches. -Erase *all* windows, then draw *all* sprites. Never mix this with per-sprite +Erase *all* regions, then draw *all* sprites. Never mix this with per-sprite save and restore in the same region. ==== @@ -180,7 +183,10 @@ nothing is reallocated. |=== *Returns:* `true` on success; `false` if the file is missing, the wrong size, -or unreadable. +has an SCB byte above 15, or unreadable. A read that fails part way can leave +the pixels partly overwritten; the SCBs and palettes are then untouched. Colour +0 of every palette loaded is set to black, as `jlPaletteSet` does. Neither file +call sets `jlLastError`. === jlSurfaceHash @@ -213,8 +219,10 @@ void jlStagePresent(void); ---- Flips the dirty regions of the stage to the display and clears the dirty state. -On chunky platforms this is a direct copy; on planar platforms it is a -chunky-to-planar conversion. +Where the stage already has the display's layout this is a copy (the IIgs's +4bpp pixels; the planes of the Amiga, ST, and X68000); DOS, DVX, and the hosted +ports expand each 4-bit pixel to their display's 8-bit or RGB pixels. Palette +and SCB changes reach the display here too. Drawing primitives mark per-row dirty ranges as a side effect, so you only need to call this at the end of a frame and everything you drew appears. Rows @@ -224,6 +232,6 @@ nothing touched since the last present are skipped. [NOTE] ==== -If you modify pixels by a route other than the library's primitives, the dirty -tracking does not know, and those rows will not be presented. +Every call that changes the stage's pixels marks the rows it touched, and the +API has no other way to write them, so there is nothing to mark by hand. ==== diff --git a/docs/ApiTile.adoc b/docs/ApiTile.adoc index fdae8da..baa99cb 100644 --- a/docs/ApiTile.adoc +++ b/docs/ApiTile.adoc @@ -10,7 +10,7 @@ surfaces or fills them. The consequence is that everything else works on tiles too. You can paint tile content with `jlDrawPixel` and `jlFillRect`, generate it procedurally, or edit it at run time, because a "tileset" is just a surface you agree to treat as -one. Fonts, terrain sets and spritesheets are all the same thing. +one. Fonts, terrain sets, and spritesheets are all the same thing. === Constants and Types @@ -18,19 +18,19 @@ one. Fonts, terrain sets and spritesheets are all the same thing. |=== |Name |Value |Meaning -|`TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels. -|`TILE_BYTES_PER_ROW` |4 |Bytes per tile row (8 pixels at 4bpp). -|`TILE_BYTES` |32 |Bytes in one tile. -|`TILE_BLOCKS_PER_ROW` |40 |Tile columns on a surface. -|`TILE_BLOCKS_PER_COL` |25 |Tile rows on a surface. -|`TILE_NO_GLYPH` |0xFFFF |`jlDrawText` map entry meaning "skip this character". -|`TILE_MAP_SKIP` |0xFF |`jlTileMapPaste` entry meaning "leave this cell alone". +|`JL_TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels. +|`JL_TILE_BYTES_PER_ROW` |4 |Bytes per tile row (8 pixels at 4bpp). +|`JL_TILE_BYTES` |32 |Bytes in one tile. +|`JL_TILE_BLOCKS_PER_ROW` |40 |Tile columns on a surface. +|`JL_TILE_BLOCKS_PER_COL` |25 |Tile rows on a surface. +|`JL_TILE_NO_GLYPH` |0xFFFF |`jlDrawText` map entry meaning "skip this character". +|`JL_TILE_MAP_SKIP` |0xFF |`jlTileMapPaste` entry meaning "leave this cell alone". |=== [source,c] ---- typedef struct jlTileT { - uint8_t pixels[TILE_BYTES]; + uint8_t pixels[JL_TILE_BYTES]; } jlTileT; ---- @@ -39,11 +39,12 @@ need a whole scratch surface. [IMPORTANT] ==== -A `jlTileT` is *target-native*: plane-major on the planar ports, chunky on the -others. Treat it as opaque. In particular, do not feed a tile captured by -`jlTileSnap` to `jlTilePasteMono`, which wants a chunky silhouette -- that is a -format mismatch and draws the wrong shape. Use `jlTilePasteGlyph` for 1-bit -glyph data instead. +A `jlTileT` is *target-native*: plane-major on the Amiga and X68000, row by row +with each row's four plane bytes together on the ST, and chunky on the others. +Treat it as opaque. In particular, do not feed a tile from `jlTileSnap` or +`jlTileBankLoad` to `jlTilePasteMono`, which wants a chunky silhouette -- on the +Amiga, ST, and X68000 that is a format mismatch and draws the wrong shape. Use +`jlTilePasteGlyph` for 1-bit glyph data instead. ==== === jlTileCopy @@ -166,7 +167,7 @@ this needs only a small internal scratch buffer and one pass over the file. |=== *Returns:* the number of tiles pasted. Blocks beyond the file's tile count keep -their previous content. +their previous content, and tiles past the surface's 1000 blocks are ignored. === jlTileSnap @@ -233,10 +234,10 @@ comes from a separate map, as in C64-style colour RAM. Opaque. [WARNING] ==== -This call wants a *chunky* silhouette, but a `jlTileT` from `jlTileSnap` is -target-native. Feeding one to this function on a planar port is a format -mismatch. For glyphs, use `jlTilePasteGlyph`, which takes unambiguous 1-bit -input. +This call wants a *chunky* silhouette, but a `jlTileT` from `jlTileSnap` or +`jlTileBankLoad` is target-native. Feeding one to this function on the Amiga, +ST, or X68000 is a format mismatch. For glyphs, use `jlTilePasteGlyph`, which +takes unambiguous 1-bit input. ==== === jlTilePasteGlyph @@ -278,7 +279,10 @@ void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, Draws a NUL-terminated ASCII string using glyphs from a font surface. Glyph colour 0 is transparent, so the background shows through. The cursor wraps at -the right edge and truncates at the bottom. +the right edge and truncates at the bottom. A start off the 40x25 grid uses up +characters without drawing them as the cursor moves on: a start column past +39 drops the first character and carries on at the start of the next row, and +a start row past 24 draws nothing. [cols="1,1,4", options="header"] |=== @@ -288,7 +292,8 @@ the right edge and truncates at the bottom. |`fontSurface` |`const jlSurfaceT *` |Surface holding the glyphs. |`asciiMap` |`const uint16_t *` |256 entries mapping ASCII code to glyph position: low byte is source block X, high byte is source block Y. An entry of - `TILE_NO_GLYPH` skips that character, advancing the cursor without drawing. + `JL_TILE_NO_GLYPH`, or one off the font surface's 40x25 grid, skips that + character, advancing the cursor without drawing. |`str` |`const char *` |NUL-terminated string. |=== @@ -303,7 +308,7 @@ void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *tiles, const uint8_t *map); ---- -Pastes a whole rectangle of tiles in one call, so validation, dispatch and the +Pastes a whole rectangle of tiles in one call, so validation, dispatch, and the dirty-rectangle union are paid once for the rectangle instead of once per tile. For anything larger than a few tiles this is substantially faster than looping over `jlTilePaste`. @@ -316,7 +321,7 @@ over `jlTilePaste`. |`wTiles`, `hTiles` |`uint8_t` |Rectangle size in tiles. Zero is a no-op. |`tiles` |`const jlTileT *` |The tileset. |`map` |`const uint8_t *` |`wTiles * hTiles` tileset indices, row-major, so - `map[row * wTiles + col]`. `TILE_MAP_SKIP` leaves that cell untouched. + `map[row * wTiles + col]`. `JL_TILE_MAP_SKIP` leaves that cell untouched. |=== *Returns:* nothing. A rectangle whose edge crosses the surface boundary is @@ -324,7 +329,7 @@ rejected as a no-op. [WARNING] ==== -Any map index other than `TILE_MAP_SKIP` must be a valid tileset index. The +Any map index other than `JL_TILE_MAP_SKIP` must be a valid tileset index. The batch does not know the tileset length and does *not* range-check it -- an out-of-range index reads past the end of your array. ==== diff --git a/docs/Appendix.adoc b/docs/Appendix.adoc index a1cd009..ab845c9 100644 --- a/docs/Appendix.adoc +++ b/docs/Appendix.adoc @@ -6,13 +6,13 @@ [cols="3,1,4", options="header"] |=== |Name |Value |Meaning -|`SURFACE_WIDTH` |320 |Surface width in pixels. -|`SURFACE_HEIGHT` |200 |Surface height in pixels. -|`SURFACE_BYTES_PER_ROW` |160 |Bytes per row, chunky 4bpp. -|`SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes. -|`SURFACE_PALETTE_COUNT` |16 |Palettes per surface. -|`SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette. -|`SURFACE_PALETTE_ENTRIES` |256 |Total palette entries. +|`JL_SURFACE_WIDTH` |320 |Surface width in pixels. +|`JL_SURFACE_HEIGHT` |200 |Surface height in pixels. +|`JL_SURFACE_BYTES_PER_ROW` |160 |Bytes per row, chunky 4bpp. +|`JL_SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes. +|`JL_SURFACE_PALETTE_COUNT` |16 |Palettes per surface. +|`JL_SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette. +|`JL_SURFACE_PALETTE_ENTRIES` |256 |Total palette entries. |=== === Tiles @@ -20,13 +20,13 @@ [cols="3,1,4", options="header"] |=== |Name |Value |Meaning -|`TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels. -|`TILE_BYTES_PER_ROW` |4 |Bytes per tile row. -|`TILE_BYTES` |32 |Bytes per tile. -|`TILE_BLOCKS_PER_ROW` |40 |Tile columns per surface. -|`TILE_BLOCKS_PER_COL` |25 |Tile rows per surface. -|`TILE_NO_GLYPH` |0xFFFF |`jlDrawText`: skip this character. -|`TILE_MAP_SKIP` |0xFF |`jlTileMapPaste`: leave this cell untouched. +|`JL_TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels. +|`JL_TILE_BYTES_PER_ROW` |4 |Bytes per tile row. +|`JL_TILE_BYTES` |32 |Bytes per tile. +|`JL_TILE_BLOCKS_PER_ROW` |40 |Tile columns per surface. +|`JL_TILE_BLOCKS_PER_COL` |25 |Tile rows per surface. +|`JL_TILE_NO_GLYPH` |0xFFFF |`jlDrawText`: skip this character. +|`JL_TILE_MAP_SKIP` |0xFF |`jlTileMapPaste`: leave this cell untouched. |=== === Input @@ -34,8 +34,8 @@ [cols="3,1,4", options="header"] |=== |Name |Value |Meaning -|`JOYSTICK_AXIS_MAX` |127 |Maximum axis value. -|`JOYSTICK_AXIS_MIN` |-127 |Minimum axis value. +|`JL_JOYSTICK_AXIS_MAX` |127 |Maximum axis value. +|`JL_JOYSTICK_AXIS_MIN` |-127 |Minimum axis value. |`JL_CHAR_QUEUE_SIZE` |32 |Queue capacity; 31 characters can be pending. |`JL_CHAR_BACKSPACE` |0x08 |Backspace, deletes before the caret. |`JL_CHAR_TAB` |0x09 |Tab. @@ -49,8 +49,8 @@ [cols="3,1,4", options="header"] |=== |Name |Value |Meaning -|`JOEY_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4. -|`JOEY_AUDIO_VOICES` |3 |Tone voices, 0..2. +|`JL_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4. +|`JL_AUDIO_VOICES` |3 |Tone voices, 0..2. |`JL_SFX_RAW_SIGNED8` |0 |Raw stream format: signed 8-bit PCM. |`JL_SFX_RAW_UNSIGNED8_NOZERO` |1 |Raw stream format: unsigned biased 0x80. |=== @@ -91,7 +91,7 @@ |`sprite.h` |`jlSpriteBankLoad`, `jlSpriteBankLoadPrecompiled`, `jlSpriteBankSavePrecompiled`, `jlSpriteCreate`, `jlSpriteDestroy`, - `jlSpriteCompile`, `jlSpriteCompiledSize`, `jlSpritePrewarm`, `jlSpriteDraw`, + `jlSpriteCompile`, `jlSpriteCompiledSize`, `jlSpriteDraw`, `jlSpriteDrawTrusted`, `jlSpriteSaveUnder`, `jlSpriteSaveUnderTrusted`, `jlSpriteRestoreUnder`, `jlSpriteRestoreUnderTrusted`, `jlSpriteSaveAndDraw`, `jlSpriteSaveAndDrawTrusted`, `jlSpriteCreateFromSurface`, `jlSpriteCompact`, @@ -105,7 +105,7 @@ `jlJoystickReset` |`audio.h` -|`jlAudioInit`, `jlAudioShutdown`, `jlAudioPlayMod`, `jlAudioStopMod`, +|`jlAudioPlayMod`, `jlAudioStopMod`, `jlAudioIsPlayingMod`, `jlAudioPlaySfx`, `jlAudioPlaySfxStream`, `jlAudioSfxRawFormat`, `jlAudioPlaySfxStreamRaw`, `jlAudioSetPan`, `jlAudioStopSfx`, `jlAudioTone`, `jlAudioTickRegister`, @@ -120,7 +120,7 @@ |`serial.h` (opt-in) |`jlSerialOpen`, `jlSerialClose`, `jlSerialPoll`, `jlSerialAvailable`, `jlSerialRead`, `jlSerialWrite`, `jlSerialWriteByte`, `jlSerialReadByte`, - `jlSerialFlush`, `jlSerialSetRts` + `jlSerialFlush`, `jlSerialSetDtr`, `jlSerialSetRts` |`debug.h` |`jlLog`, `jlLogF`, `jlLogFlush`, `jlLogReset` @@ -139,7 +139,7 @@ smoke-tests what is installed. |Sprites draw but the game crawls -|The sprites were never compiled. Call `jlSpritePrewarm` during loading. Check +|The sprites were never compiled. Call `jlSpriteCompile` during loading. Check `jlSpriteCompiledSize` returns non-zero. |Sprites compile on some machines and not others @@ -147,7 +147,8 @@ eight times the IIgs figure. Measure with `jlSpriteCompiledSize`. |Works everywhere except the IIgs, failing at startup -|A `malloc` too large for the bank-0 C heap. Use `jlAlloc`. +|A `malloc` too large for the IIgs's C heap, which is 32 KB to 48 KB in all. + Use `jlAlloc`. |Crashes on IIgs deep in a call chain |A large function-local array overflowing the soft stack. Make it `static`. @@ -156,10 +157,6 @@ |`jlStagePresent` was never called, or drawing went to a surface that is not the stage. -|Part of the screen never updates -|Pixels were written by something other than the library primitives, so the - dirty tracking did not see them. - |Runs at different speeds on different machines |Per-frame steps are hard-coded. Scale by `jlFrameHz()` or use `jlMillisElapsed()`. @@ -171,23 +168,24 @@ |The character queue holds 31 entries and drops the rest. Pop it every frame. |Music plays but `jlAudioVoice` does nothing -|On ST and Amiga the module player owns those channels (in the ST build; the - STe build plays modules through the DMA sound chip). Use one layer per scene. +|On the Amiga the module player owns those channels, and on the plain ST + voice 0 is silent while digital sound plays. Use one layer per scene. -|A note sounds for only a fraction of a second on the IIgs -|`jlAudioVoice` is a roughly 80 ms one-shot there. Re-assert it about once a - frame. +|A note sounds for only a fraction of a second +|`jlAudioFrameTick` is not being called every frame. The IIgs plays each note + as a short sample and restarts the notes being held from it. |A streaming sound effect stops after its first chunk -|`jlAudioFrameTick` is not being called every frame. IIgs and Amiga fire the - next chunk from it. +|`jlAudioFrameTick` is not being called every frame. Every port feeds streams + from it. |Sprite trails or corruption around moving objects |Save-under and clean-buffer erasing are being mixed in one region, or a `jlSurfaceCopyRect` invalidated a live backup by snapping outward to the 16-pixel grid. -|Save and restore are unexpectedly slow on Amiga or ST +|Save and restore are unexpectedly slow on the Amiga, ST, or X68000, or the + trusted variants crash on the Amiga or ST |The backup buffer is at an odd address. Declare it `uint16_t`. |A tile bank refuses to load diff --git a/docs/Assets.adoc b/docs/Assets.adoc index 8ed9fc1..a0fd02e 100644 --- a/docs/Assets.adoc +++ b/docs/Assets.adoc @@ -1,6 +1,6 @@ == Creating Assets -JoeyLib does not load PNGs, WAVs or MODs at run time. Every asset is converted +JoeyLib does not load PNGs, WAVs, or MODs at run time. Every asset is converted on the build host into a compact binary the target can use directly, which is what keeps loading fast and the runtime small. This chapter covers each asset type: what you author, which tool bakes it, and which API call loads it. @@ -31,6 +31,11 @@ type: what you author, which tool bakes it, and which API call loads it. |`.jym` or a C header |`jlMusicPlay` +|A tracker song (Furnace) for the SN76489 or AY-3-8910, exported as VGM +|`vgm2song.py`, then `songbake.py` +|`.jym` or a C header +|`jlMusicPlay` + |Protracker `.MOD` |`joeymod` |`.mod`, `.amod`, `.ntp` @@ -45,6 +50,52 @@ type: what you author, which tool bakes it, and which API call loads it. Baked assets are staged into `DATA/` beside the executable, and the program opens them with bare names. +=== Content Creation Tools + +These are the tools we recommend for making each kind of asset. All of them +run on Linux, and all but OpenMPT are native there. + +[cols="2,4,3", options="header"] +|=== +|You make |With |Notes + +|Tile and sprite sheets +|*GrafX2* (GPLv2), a Deluxe Paint-style editor built around an indexed + palette. *LibreSprite* (GPLv2), for sprite sheets drawn as layers and + animation frames. *GIMP* (GPLv3), to bring existing art down to an indexed + PNG. +|In GrafX2, set the palette's RGB scale to 16 (right-click the palette + button for the secondary palette menu): every colour you pick is then one a + JoeyLib palette holds exactly, 4 bits a gun. The sheet rules are in + <<_authoring_the_png>>. + +|Program icons +|Any of the above, saving an RGBA PNG with transparency. +|One picture for every system (<<_icons>>). + +|Chip-tracker music +|*Furnace* (GPLv2 or later), a chiptune tracker that emulates the chips it + writes for. `./install.sh install furnace` puts it in + `toolchains/opt/furnace`. +|Write for the TI SN76489 or the AY-3-8910, export VGM, and convert it + (<<_writing_songs_in_furnace>>). + +|`.MOD` music +|*MilkyTracker* (GPLv3), the *ProTracker 2 clone* (open source), or + *OpenMPT* (BSD; Windows, runs under Wine). +|Four channels, then `joeymod` (<<_music_protracker_modules>>). + +|Sound effects +|*sfxr* (MIT) or *jfxr* (open source, in a browser) for generated game + sounds; *Audacity* (GPLv3) to edit recordings; *SoX* to convert. +|Save a WAV, then convert it to raw signed 8-bit PCM + (<<_sound_effects>>). +|=== + +None of these is needed to build a program: the baked files are what the +build uses. Only Furnace has an installer component, because +`scripts/check-vgm.sh` uses it to test the converter on real songs. + === Graphics: Tiles and Sprites ==== Authoring the PNG @@ -75,11 +126,11 @@ $ python3 tools/assetbake/assetbake.py --type tile --target atarist \ Tile banks are *per-target*, because the 8x8 block layout is the platform's native pixel format -- plane-major on Amiga and X68000, row-major-with-planes- -per-row on ST, chunky 4bpp on DOS and IIgs. You must bake one `.tbk` per platform, and the +per-row on ST, and chunky 4bpp on DOS and IIgs. You must bake one `.tbk` per platform, and the loader refuses a file baked for a different target rather than drawing garbage. `--target` is required for tiles, and accepts `amiga`, `atarist`, `dos`, -`iigs` or `x68000`. +`iigs`, or `x68000`. ==== Baking Sprites @@ -93,8 +144,8 @@ $ python3 tools/assetbake/assetbake.py --type sprite --cell 2x3 \ pixels. The sheet is cut into cels row by row. Sprite `.spr` banks are *cross-target*: the same file works on every platform, -because the sprite walker reads chunky 4bpp and converts at draw time. Bake -once, ship everywhere. +because it holds chunky 4bpp tiles and each port converts a sprite to its own +form when the sprite is created. Bake once, ship everywhere. Both formats can embed the 16-entry `$0RGB` palette, and both loaders will hand it back through their `outPalette` parameter so artwork carries its own colours @@ -192,33 +243,88 @@ tone voice 2. Author noise hits and voice-2 notes so they do not sound at the same time; `songbake` warns when they overlap. ==== +==== Writing Songs in Furnace + +A tracker is easier than a text file for anything longer than a jingle. Write +the song in Furnace, export it as VGM (a log of the chip's register writes as +the song plays), and `tools/vgm2song.py` turns that into a `.song` file, which +is still plain text you can read and touch up: + +. Start the song on a system with a *TI SN76489* (the Sega Master System, BBC + Micro, or TI-99/4A) or an *AY-3-8910 / YM2149* (the ZX Spectrum, MSX, or + Atari ST's chip). The SN76489 is the closer match: three square tone voices + and a noise channel with its own volume, attenuated in 2 dB steps exactly as + JoeyLib's are. Use that one chip only; another chip's part (a Master System + FM unit, say) is left out. +. Export: *File > Export > VGM*, or `furnace -vgmout song.vgm song.fur` from a + shell. +. Convert and bake: ++ +[source,console] +---- +$ python3 tools/vgm2song.py song.vgm song.song +$ python3 tools/songbake/songbake.py -c gSong song.song song.h +---- + +What comes across: + +* *Tones* at their exact frequencies (`@Hz` cells) and volumes; an AY volume + `v` becomes attenuation `15 - v`, exact on the ST. +* *Noise* at the nearest JoeyLib noise pitch. The SN76489's periodic noise (a + pitched buzz, the Master System's bass trick) plays as a square at its + pitch on a tone voice that is free, and as noise when all three are busy. +* *The AY's volume envelope*, followed row by row. An envelope repeating + faster than two rows is a timbre (the "buzzer"), not a volume: it plays at + full volume, and a channel whose tone is off sounds at the envelope's + frequency. +* *Timing.* One `.song` row a tick, by default the song's update interval + rounded down (16 ms at 60 Hz, 20 ms at 50 Hz), placed by the VGM's own clock, + so the length and the loop point are right to within a tick. +* *The loop point*, if the song has one. + +A baked song must stay under 64 KB. A long, busy song that would not fit gets +a longer tick, a whole multiple of the default, which merges changes faster +than it (vibrato, arpeggios); `--tick MS` sets the tick yourself. The +converter warns about everything it approximates. + === Music: Protracker Modules For richer music, author a four-channel `.MOD` in any tracker (ProTracker, -MilkyTracker, OpenMPT). Then convert per target: +MilkyTracker, OpenMPT) -- one whose signature is `M.K.`, `M!K!`, `FLT4`, or +`4CHN`; the other module formats and the 15-sample Soundtracker files are not +played. Every port plays its effects as Protracker 2.3 does. Then convert +per target: [source,console] ---- -$ tools/joeymod/joeymod song.mod song.mod # DOS and ST: validated copy -$ tools/joeymod/joeymod song.mod song.amod # Amiga +$ tools/joeymod/joeymod song.mod song.mod # every port but the IIgs +$ tools/joeymod/joeymod song.mod song.amod # Amiga, with an end marker $ tools/joeymod/joeymod song.mod song.ntp # Apple IIgs ---- The output extension selects the conversion: -* *`.mod`* -- a passthrough copy with the signature validated. DOS and ST - detect song end inside libxmp-lite, so the file is used as-is. -* *`.amod`* -- the same, plus an `E8FF` stop marker injected at song end. The - Amiga replayer, PTPlayer, has no native song-end signal, so without this - marker `jlAudioPlayMod(..., loop=false)` loops anyway. Other replayers ignore - `E8FF`, so an `.amod` is harmless if it reaches the wrong port. +* *`.mod`* -- a passthrough copy with the signature validated, played as it + is on every port but the IIgs. The ports that mix modules in software + (DOS, the ST, the X68000, and the hosted ports) find the song's end + themselves, and the Amiga adds an end marker as it copies the module to + chip RAM, so `+jlAudioPlayMod(..., loop=false)+` plays the song once. +* *`.amod`* -- the same, plus an `E8F` stop marker added at song end ahead of + time. The Amiga replayer, PTPlayer, has no song-end signal of its own and + needs the marker, but the Amiga adds it itself when a song is played with + `loop=false`, so a plain `.mod` works there too. A module with all 128 song + positions or all 128 patterns in use has no room for the marker either way, + and loops on the Amiga. The other ports' module player stops a song that + does not loop at the marker too, and the IIgs's replayer ignores `E8F`, so + an `.amod` plays the same anywhere (looping, with the marker's pattern of + silence at its end). * *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the NinjaTrackerPlus converter the installer's `ntp` component provides (part of `toolchain:iigs`; it needs the PHP command-line interpreter, which the installer's host packages include). -Keep modules modest. A four-channel mix is real CPU work on DOS and the ST, and -the sample data has to fit in memory alongside your game. +Keep modules modest. A four-channel mix is real CPU work on DOS, the ST, and +the X68000, and the sample data has to fit in memory alongside your game. === Sound Effects @@ -260,7 +366,7 @@ jlSurfaceSaveFile(surface, "title.sur"); jlSurfaceLoadFile(surface, "title.sur"); ---- -The file is 32000 bytes of pixels, 200 SCB bytes and 512 bytes of palette. It +The file is 32000 bytes of pixels, 200 SCB bytes, and 512 bytes of palette. It is *not* portable between platforms -- no format conversion happens, so the machine that wrote it must be the machine that reads it. It is best used as a runtime cache (a generated title screen, say), not as a shipped asset. For @@ -292,10 +398,12 @@ $(call programDataDir,MYGAME)/font.tbk: assets/font.png python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target $(ASSET_PLATFORM) $< $@ ---- -Tile banks and `.spc` files differ per platform, while `.spr`, `.jym` and -`.sfx` files are shared: `ASSET_PLATFORM` names the target a bake is for (DVX, -SDL3 and the web take the DOS bakes; the last two use only a `.spc`'s cels, not -their code) and `SPRITEBAKE` the matching host `spritebake`. +Tile banks and `.spc` files differ per platform, while `.spr`, `.jym`, and +`.sfx` files are shared: `ASSET_PLATFORM` names the target a bake is for (DVX +and every hosted port -- SDL3, the web, Win32, Win16, the PSP, Haiku, and +BeOS -- take the DOS bakes; DVX and Win16 run the x86 code of a `.spc`, the +others use only its cels; the STe build takes the ST's) and `SPRITEBAKE` the +matching host `spritebake`. `examples/tilebank` is a working reference: one rule in `make/examples.mk` bakes the same PNG for each target into its `DATA/tilebank` (`TILEBANK_DATA`), @@ -313,7 +421,7 @@ per-level banks marked optional, and speech baked with per-target flags. // Tiles into a caller-owned array. jlTileT tiles[256]; bool valid[256]; -uint16_t palette[SURFACE_COLORS_PER_PALETTE]; +uint16_t palette[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t n = jlTileBankLoad("font.tbk", tiles, 256, valid, palette); // Or stream a big bank straight onto a surface, with no caller buffer. diff --git a/docs/BuildIntegration.adoc b/docs/BuildIntegration.adoc index 58e42a3..1162f1f 100644 --- a/docs/BuildIntegration.adoc +++ b/docs/BuildIntegration.adoc @@ -1,11 +1,11 @@ == Building Your Program JoeyLib does not install as a system library: each target needs its own cross -compiler, link step, packaging and disk image, and JoeyLib's makefiles own all +compiler, link step, packaging, and disk image; JoeyLib's makefiles own all of that. Your program lives in a directory of its own -- its own repository, usually -- next to a JoeyLib checkout. Its Makefile describes the program and includes two of JoeyLib's makefiles, and that is enough to build it for every -target with exactly the flags, link order and packaging JoeyLib's own examples +target with exactly the flags, link order, and packaging JoeyLib's own examples get. JoeyLib's examples are described the same way and built by the same rules; there is one set of them. @@ -62,19 +62,20 @@ The project's outputs land in its own `build/`, laid out like JoeyLib's: |Target |Output |Apple IIgs |`build/iigs/bin/MYGAME` (an S16 OMF), its data staged in -`build/iigs/data/MYGAME/`, and with `MYGAME_IIGS_GAME_DISK` the disks -`build/iigs/bin/MYGAME.2mg` and `MYGAME-data.2mg` -|Amiga |`build/amiga/bin/Mygame` +`build/iigs/data/MYGAME/`, its Finder icon in `build/iigs/icon/MYGAME/`, and +with `MYGAME_IIGS_GAME_DISK` the disks `build/iigs/bin/MYGAME.2mg` and +`MYGAME-data.2mg` +|Amiga |`build/amiga/bin/Mygame` and its Workbench icon, `Mygame.info` |Atari ST |`build/atarist/bin/MYGAME.PRG`, which runs on every ST |Atari STe |`build/ste/bin/MYGAME.PRG`, the STe build: STe and Mega STe only (<<_the_atari_st_family>>); same toolchain and baked data as the ST |MS-DOS |`build/dos/bin/MYGAME.EXE`, with the DPMI server built in |Sharp X68000 |`build/x68000/bin/MYGAME.X` -|DVX |`build/dvx/bin/apps/joeylib/mygame/`: `mygame.app`, `mygame.hlp` and its +|DVX |`build/dvx/bin/apps/joeylib/mygame/`: `mygame.app`, `mygame.hlp`, and its `DATA/` |SDL3 |`build/sdl3/bin/MYGAME`, a native program with SDL 3 linked in |Web |`build/web/bin/MYGAME/`: `MYGAME.html`, `.js`, `.wasm`, and `.data` -(its `DATA/`, packed in) +(its `DATA/`, packed in), and `icon.png` |Win32 |`build/win32/bin/MYGAME.EXE`, a 32-bit Windows program |Win16 |`build/win16/bin/MYGAME.EXE`, a Windows 3.x program |PSP |`build/psp/bin/MYGAME/`: `EBOOT.PBP` and its `DATA/`, a folder for the @@ -84,7 +85,8 @@ memory stick's `PSP/GAME/`; the ELF it is packed from in A program with `MYGAME_FOLDER` set ships in a folder of its own with its `DATA/` beside it (`build/dos/bin/MYGAME/MYGAME.EXE`); otherwise programs share -`bin/DATA/`. +`bin/DATA/`. A program's icon (<<_icons>>) is baked in +`build//icon/MYGAME/` on its way into the program. JoeyLib's library for each target, and the host tools such as `spritebake`, are built on demand where JoeyLib keeps them, in the checkout's `build/`, and shared @@ -132,13 +134,16 @@ probes for gates). |`ID_PORTS` |The targets it builds for. Every target when unset. |`ID_NAME` |Its file name without the extension (see <<_naming>>). |`ID_FOLDER` |Set: it ships in a folder of its own with its `DATA/` (Amiga, -ST, DOS, X68000, SDL3, Win32, Win16; on the IIgs, DVX, the web and the PSP +ST, DOS, X68000, SDL3, Win32, Win16; on the IIgs, DVX, the web, and the PSP every program is packaged apart anyway). |`ID_BINDIR` |Build it there instead of `bin/`. |`ID_DATA` |Its data files (<<_data_files>>). |`ID_TITLE` |DVX: its window title. PSP: its title in the PSP's menu (by default its name). -|`ID_HELP` |DVX: the line its help file says about it. No apostrophes. +|`ID_ICON` |Its icon: a picture (a PNG, best square, transparent around the +figure), baked for every target whose system shows programs by an icon +(<<_icons>>). JoeyLib's kangaroo by default. +|`ID_HELP` |DVX: the line its help file says about it. |`ID_IIGS_OVERFLOW_CAP` |IIgs: link large overflow segments (`0xF000`), which a big program needs to leave the Memory Manager room (see `make/support/iigs/clang-build.sh`). @@ -174,11 +179,12 @@ set up: |`$(call programDataDir,ID)` |Where its `DATA/` files go on this target. |`$(call programOptionalDir,ID)` |Where the files go that it can run without (<<_disk_images_and_packaging>>). -|`PLATFORM` |The target: `iigs`, `amiga`, `atarist`, `ste`, `dos`, `x68000`, `dvx`, `sdl3`, `web`, `win32`, `win16`, `psp`. +|`PLATFORM` |The target: `iigs`, `amiga`, `atarist`, `ste`, `dos`, `x68000`, `dvx`, `sdl3`, `web`, `win32`, `win16`, `psp`, `haiku`, `haiku32`, `beos`. |`ASSET_PLATFORM` |The target its bakes are for. DVX shows the DOS stage and runs x86 code, so it takes the DOS bakes; the hosted ports' stage (SDL3, the -web, Win32, Win16, the PSP) is the same chunky one, so they take them too -(the cels of a pre-compiled sprite bank, without their code). +web, Win32, Win16, the PSP, Haiku, BeOS) is the same chunky one, so they take +them too: Win16 runs a pre-compiled sprite bank's x86 code as DVX does, and +the others take its cels without their code. The STe build takes the ST's. |`SPRITEBAKE` |The host `spritebake` for this target (its rule is JoeyLib's). |`PROJECT_DIR`, `PROJECT_BUILD` |The project and its `build/`. @@ -186,7 +192,7 @@ JoeyLib's). |`HOST_CC`, `HOST_CFLAGS` |For a host tool of the project's own. |=== -A copy, a tile-bank bake and a pre-compiled sprite bank: +A copy, a tile-bank bake, and a pre-compiled sprite bank: [source,makefile] ---- @@ -252,7 +258,8 @@ off a CFFA2 card. `make iigs-game-disk GAME=NAME` builds one for any program, `GAME_DISK_SIZE=2048KB` forces the size, and `make iigs-verify-game-disk` boots it headlessly and checks it draws. The data the program needs goes on with it; files listed under `programOptionalDir` (a program's optional data, which -it can run without) go on last, only while the volume has room. +it can run without) go on last, only while the volume has room, and the +programs' Finder icons last of all (<<_icons>>). *Sharp X68000.* Programs ship on a Human68k floppy; `joey-run` builds one for each run from the program and its `DATA/`. A floppy is 1232 KB, so a large @@ -266,6 +273,7 @@ seems to vanish. needs to be on the disk. *Amiga.* `joey-run` writes an `s/startup-sequence` that invokes your binary. +`Name.info` beside it is its Workbench icon, from which it starts too. *DVX.* Every program is a folder laid out as it installs in DVX: `build/dvx/bin/apps/joeylib/mygame/` holds `mygame.app`, its help file @@ -287,16 +295,64 @@ DLLs, so its folder, with its `DATA/`, is all it needs; it ships the way a DOS program does. *Win16.* A program is one `.EXE`, its 32-bit code bound to Open Watcom's -Win386 supervisor and calling only what Windows 3.1 has (USER, GDI, KERNEL +Win386 supervisor and calling only what Windows 3.1 has (USER, GDI, KERNEL, and MMSYSTEM), so its folder, with its `DATA/`, is all it needs; it ships the way a DOS program does. *PSP.* A program is its folder, `build/psp/bin/MYGAME/`: `EBOOT.PBP` (the -program, SDL 3 linked in, packed as the PSPSDK packs it, with no icon or -pictures) and its `DATA/`. Copy the folder into `PSP/GAME/` on a memory -stick; the PSP shows it by its `ID_TITLE`. The unstripped ELF stays in +program, SDL 3 linked in, packed as the PSPSDK packs it, with its icon and no +other pictures) and its `DATA/`. Copy the folder into `PSP/GAME/` on a memory +stick; the PSP shows it by its icon and its `ID_TITLE`. The unstripped ELF stays in `build/psp/elf/`, for `psp-addr2line` on an address a crash reports. +=== Icons + +A program has an icon on every system that shows programs by one. `ID_ICON` +names a picture, a PNG, best square and transparent around the figure, and +`make` bakes it for each target with `tools/iconBake.py`; a program without +one gets JoeyLib's kangaroo, `assets/joeyIcon.png`. A picture of 64 pixels or +less is taken for pixel art and enlarged by whole pixels; a bigger one is +resampled smoothly. Where an icon has few colours, each pixel takes the +nearest. + +[cols="1,5", options="header"] +|=== +|Target |The icon + +|Apple IIgs |The Finder's: an icon file (type `$CA`) for each program, which +the disk scripts put in the volume's `Icons` folder as `NAME.ICONS`, where the +Finder reads icon files when it starts. 24x20 and 12x10 colour icons, which +the Finder shows as they are: in its 640 mode each icon pixel is two screen +pixels, black, red, green, or white beside black, blue, yellow, or white, seen +together as one of 16 colours (an icon drawn at an odd screen column would +pair them with the other halves of the palette; the Finder drew every one at +an even column). A data disk takes the icons +last, while there is room; a game disk keeps its program's at its root. A +program copied to another volume takes its icon file along, into that +volume's `Icons` folder. +|Amiga |Workbench's: `Name.info` beside the program, a tool's icon in +Workbench's four colours, 48x24 hires pixels, which starts it with +`AMIGA_ICON_STACK` (32768) bytes of stack. Started from it, the program runs +in its own folder, as it does from a shell. +|DVX |Program Manager's: the program's `icon32` resource, 32x32 on DVX's grey. +|Win16 |Program Manager's and the minimised window's: 32x32 in the 16 VGA +colours, bound into the `.EXE`. +|Win32 |Explorer's and the window's: 16, 32, and 48 pixels, each in 256 +colours and in 32-bit, linked into the `.EXE`. +|Haiku, BeOS |Tracker's and the Deskbar's: 32x32 and 16x16 in the BeOS +palette, in the program's resources. Tracker shows a file by the icons in its +attributes, which a program built on another machine does not have; the +program copies them across the first time it runs, so Tracker shows it by its +icon from then on (or at once, after `mimeset -F NAME`). +|PSP |The menu's: `ICON0` in the EBOOT, 144x80. +|SDL3 |The window's, 64x64, which the desktop shows where it shows one (title +bar, task bar, dock). +|Web |The page's: `icon.png`, 32x32, beside it. +|=== + +MS-DOS, the Atari ST, and the X68000 show no program by an icon of its own, +so their programs get none. + === Verifying It Actually Runs Build success is weak evidence on retro targets. Run it with `joey-run` (see @@ -353,7 +409,7 @@ Human68k floppies, environment checks) takes paths. |`make verify-audio` |Every port's audio gate: the IIgs DOC and X68000 chips are read directly, the ST's YM2149 register writes are traced (and for the STe build the - samples the DMA sound chip plays), the Amiga, DOS and DVX output + samples the DMA sound chip plays), the Amiga, DOS, and DVX output is recorded and graded, SDL3's mix is written to a file by SDL's disk driver and graded, and the web page's sound is recorded in headless Chromium and graded, as is the Win32 program's under Wine, the Win16 @@ -365,7 +421,7 @@ if a gate had to be skipped. |The IIgs actually reached the program and drew something. |`make iigs-verify-game-disk` -|A program's bootable IIgs volume boots, launches the program and draws. +|A program's bootable IIgs volume boots, launches the program, and draws. |`scripts/verify--tilebank.sh` |The port's own tile-bank bake loads pixel-exact both ways, with the same @@ -373,6 +429,13 @@ if a gate had to be skipped. for the other machines are refused. `scripts/verify-all.sh tilebank` runs every port's. +|`scripts/verify-all.sh icon` +|Every program carries its icon where its system looks + (`scripts/check-icons.sh`, no emulator), and each system shows it: the IIgs + Finder's window, the Amiga's icon.library (and a Workbench start), DVX's + Program Manager, Windows 3.11's desktop with the program minimised, the + Win32 and SDL3 windows' icons on X, and Tracker on Haiku and BeOS. + |`scripts/bench-.sh` |Per-operation throughput for that port. diff --git a/docs/GettingStarted.adoc b/docs/GettingStarted.adoc index a836cda..c0f054e 100644 --- a/docs/GettingStarted.adoc +++ b/docs/GettingStarted.adoc @@ -64,7 +64,7 @@ Toolchain availability: ==== Installer reference -`./install.sh [options] COMMAND [TARGETS...]`, run from the top of the +`+./install.sh [options] COMMAND [TARGETS...]+`, run from the top of the repository. [cols="1,3"] @@ -91,9 +91,9 @@ libraries, for example). It reports anything out of date, any component whose files have gone, and any part of a port you installed only partly, such as FS-UAE without a Kickstart. -| `import PATH...` +| `+import PATH...+` | Installs the files that are not downloaded (see <>) from files, folders, `.zip` archives or Amiga `.adf` disk +downloaded>>) from files, folders, `.zip` archives, or Amiga `.adf` disk images, matched by checksum. | `bundle FILE` @@ -198,7 +198,7 @@ trees. Nothing in `toolchains/` is part of the repository: delete the whole folder to return the machine to a clean state, and `./install.sh` re-creates it. The installer itself lives in `install.sh` and `installer/` (its component -recipes, patches and package lists), and the build helpers the makefiles call +recipes, patches, and package lists), and the build helpers the makefiles call live in `make/support/`. === Building @@ -251,6 +251,7 @@ int main(void) { jlConfigT config; config.codegenBytes = 8 * 1024; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); @@ -266,10 +267,13 @@ int main(void) { } ---- -Three things to notice, because they apply to every JoeyLib program: +Four things to notice, because they apply to every JoeyLib program: . *One header.* `#include ` pulls in everything except the optional serial add-on. +. *Set every field of `jlConfigT`.* It is read whole, so a field left unset + holds whatever was in memory. `noAudio` says whether the program wants sound: + `jlInit` starts audio unless it is `true`. . *`jlInit` can fail*, and when it does `jlLastError()` tells you why. Always check it. . *`jlShutdown` is not optional.* It restores the display mode, releases the @@ -289,10 +293,11 @@ graphics on screen and waits for a keypress: int main(void) { jlConfigT config; jlSurfaceT *stage; - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint8_t i; config.codegenBytes = 8 * 1024; + config.noAudio = true; if (!jlInit(&config)) { return 1; } @@ -300,7 +305,7 @@ int main(void) { stage = jlStageGet(); // the library-owned back buffer // Build palette 0. Entry 0 is forced black whatever we put here. - for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = 0x000; } colors[1] = 0xFFF; // white @@ -309,12 +314,12 @@ int main(void) { jlPaletteSet(stage, 0, colors); // Every scanline uses palette 0 (this is also the default). - jlScbSetRange(stage, 0, SURFACE_HEIGHT - 1, 0); + jlScbSetRange(stage, 0, JL_SURFACE_HEIGHT - 1, 0); jlSurfaceClear(stage, 0); jlFillRect(stage, 40, 30, 240, 140, 3); jlDrawRect(stage, 40, 30, 240, 140, 1); - jlDrawLine(stage, 0, 0, SURFACE_WIDTH - 1, SURFACE_HEIGHT - 1, 2); + jlDrawLine(stage, 0, 0, JL_SURFACE_WIDTH - 1, JL_SURFACE_HEIGHT - 1, 2); jlStagePresent(); // nothing is visible until this runs jlWaitForAnyKey(); @@ -375,7 +380,7 @@ the next. Nothing is written to your home directory. | The machine, when naming an example rather than a file. | `--data DIR` -| Copy `DIR`'s files into the program's `DATA` folder for this run, for +| Copy the files in `DIR` into the program's `DATA` folder for this run, for example the game an interpreter plays. | `--headless` @@ -392,7 +397,7 @@ example the game an interpreter plays. | Apple IIgs: the ROM 01 machine instead of ROM 03. | `--machine st` -| Atari: Hatari's machine, `st`, `megast`, `ste` or `megaste`. By default an +| Atari: Hatari's machine, `st`, `megast`, `ste`, or `megaste`. By default an ST-build program (`make atarist`) gets a plain ST and an STe-build one (`make ste`) an STe; the STe build runs only on `ste` and `megaste`. @@ -419,7 +424,7 @@ Kickstarts (`./install.sh install emulator:aga`). default is 9600. | `-- ARGS` -| Pass `ARGS` to the program. DOS, SDL3, Win32 and Win16 only: the other +| Pass `ARGS` to the program. DOS, SDL3, Win32, and Win16 only: the other machines' autostart cannot pass arguments, and a page and a PSP program have none. |=== @@ -440,7 +445,7 @@ desktop. An SDL3 program is a native program, so `joey-run` simply starts it, in a window; running the file yourself does the same. `--headless` runs it on a private virtual display at one window pixel per stage pixel, so a -`--snapshot` is exactly the 320x200 stage. The window's size, shape and full +`--snapshot` is exactly the 320x200 stage. The window's size, shape, and full screen are set by environment variables (<<_sdl3_programs>>). The SDL3 port has no serial port. @@ -457,7 +462,7 @@ in when it is built, so `--data` does not apply. A Windows program runs under Wine: in a window on your display, with its Wine settings kept in the project's `build/win32/prefix` so they are set up once (nothing goes in `~/.wine`; a run with `--serial` uses a private one, so -the kept COM1 is left alone). The window's size, shape and full screen are +the kept COM1 is left alone). The window's size, shape, and full screen are set by the same environment variables as SDL3's, and Alt+Enter switches. With `--headless` it runs on a private virtual display at one pixel per stage pixel, so a `--snapshot` is the stage. @@ -474,7 +479,7 @@ up to 115200 baud). Their components, `kpjoy` and `kpcomm`, build them from the kpjoy and mscomm projects, checkouts beside JoeyLib's (`../kpjoy`, `../mscomm/drv`; `KPJOY_DIR` and `KPCOMM_DIR` name others). The program is Windows' shell, so Windows ends when the program does, and its folder is -drive D:, so what it saves stays there. The window's size, shape and full +drive D:, so what it saves stays there. The window's size, shape, and full screen are set by the same environment variables as SDL3's: `joey-run` puts them in the DOS environment Windows starts from. Alt+Enter is DOSBox's own full-screen key, so the program switches with Alt and the keypad's Enter. @@ -496,11 +501,11 @@ serial port. ==== Haiku and BeOS A Haiku (`haiku`, `haiku32`) or BeOS R5 (`beos`) program runs in that port's -VM under QEMU (`./install.sh install emulator:haiku`, `emulator:haiku32` or +VM under QEMU (`./install.sh install emulator:haiku`, `emulator:haiku32`, or `emulator:beos`), in QEMU's window, from a copy of its folder on a second disk. The machine boots from a disk it never writes to, runs the program, and powers off when the program ends; what the program wrote in its folder -(saves, its log) is then copied back. The window's size, shape and full +(saves, its log) is then copied back. The window's size, shape, and full screen are set by the same environment variables as SDL3's. Haiku's VM has sound; BeOS's has none, and is taken through R5's boot loader menu to its fail-safe video mode (1024x768, 16-bit colour), as R5 has no driver for any @@ -585,7 +590,7 @@ arrives. To connect out, `socat` joins a pseudo-terminal to the network. | As on the ST: Wine's COM1 can only be a device, so `socat` joins a pseudo-terminal to the network both ways. -| SDL3, Web, PSP +| SDL3, Web, PSP, Haiku, BeOS | None | These ports have no serial port; `jlSerialOpen` fails there. |=== @@ -604,14 +609,14 @@ rate the program sets. and X68000, where MAME prints `Unable to load image` and runs with the port unplugged. * While a port is listening, what the program sends waits for the connection, -except on DOS, DVX and Win16, where DOSBox drops it just as a cable with +except on DOS, DVX, and Win16, where DOSBox drops it just as a cable with nothing on the other end would. `examples/sertest` sends its banner at once, so there a client that connects late sees only the echo. ==== Firmware that is not downloaded -Almost everything the emulators need is downloaded: EmuTOS for the Atari ST, -the Apple IIgs ROMs, GS/OS 6.0.4 and the CFFA2 card's firmware, and Sharp's own +Almost everything the emulators need is downloaded: EmuTOS for the Atari ST; +the Apple IIgs ROMs, GS/OS 6.0.4, and the CFFA2 card's firmware; and Sharp's own free release of the X68000 IPL-ROM and Human68k 3.02. A few files are not, because they were never released for free: the Amiga Kickstart 3.1 ROM, the Amiga `serial.device` and `lowlevel.library` from Workbench 3.1 (Kickstart @@ -621,7 +626,10 @@ Kickstart 3.1 (optional: only for running the Amiga programs on those machines), the X68000 CG (font) ROM, for the Win16 port the Windows for Workgroups 3.11 install disc (as an `.iso`) and the S3 Trio64V+ 1.70.04 Windows 3.1 display driver, from which -the installer sets Windows up unattended, and for the BeOS port the BeOS 5 +the installer sets Windows up unattended (and, optionally, Microsoft's WinG +1.0 SDK, `WING10.EXE`, whose runtime the Win16 port draws through when it is +installed: import it, then `./install.sh install wing`; archive.org keeps a +copy), and for the BeOS port the BeOS 5 Professional CD (its CloneCD `.img`, or the `.7z` holding it), from which the installer takes R5's headers and libraries and makes the VM. Supply your copies once: @@ -631,7 +639,7 @@ copies once: $ ./install.sh import ~/my-roms ---- -`import` takes files, folders, `.zip` and `.7z` archives and Amiga `.adf` disk images +`import` takes files, folders, `.zip` and `.7z` archives, and Amiga `.adf` disk images (point it at your Workbench 3.1 disk for `serial.device` and `lowlevel.library`, and at the S3 driver's `.zip` for its three files), identifies each file by its checksum @@ -655,9 +663,9 @@ built for every platform: |`draw` |Every drawing primitive, laid out in four quadrants. |`pattern` |Palette and per-scanline palette (SCB) work. |`keys` |Typed-character input and the key predicates. -|`joy` |Joystick axes, buttons and recalibration. -|`sprite` |Sprite loading, compiling, save-under and restore. -|`audio` |Tracker music, `.MOD` playback, digital sound effects, a streamed sound and the noise generator. +|`joy` |Joystick axes, buttons, and recalibration. +|`sprite` |Sprite loading, compiling, save-under, and restore. +|`audio` |Tracker music, `.MOD` playback, digital sound effects, a streamed sound, and the noise generator. |`save` |Save files and the disk-space query. |`tilebank` |Loading a baked tile bank both ways, and a bank for another machine refused. |`serial` |The optional RS-232 add-on. diff --git a/docs/Overview.adoc b/docs/Overview.adoc index 8361850..a038123 100644 --- a/docs/Overview.adoc +++ b/docs/Overview.adoc @@ -3,8 +3,8 @@ === What JoeyLib Is JoeyLib is a single-codebase C game library for five 16-bit and 32-bit retro -machines, for DVX, a DOS desktop whose programs run in windows, and for -today's desktops, browsers and the PSP through SDL3, and for Windows from +machines, for DVX, a DOS desktop whose programs run in windows, for +today's desktops, browsers, and the PSP through SDL3, and for Windows from 3.1 on. You write one program against one API, and the same source compiles and runs on all of them: @@ -28,8 +28,9 @@ and runs on all of them: |512 KB |Word-interleaved planar. Shares 68000 assembly with the Amiga port. Two builds: the ST build runs on every ST and never uses STe hardware; the STe - build (`make ste`) uses the STe's 4-bit palette, DMA sound and BLiTTER, and a - Mega STe's 16 MHz, and runs only there. See <<_the_atari_st_family>>. + build (`make ste`) uses the STe's 4-bit palette, DMA sound, BLiTTER, + enhanced joystick ports, and a Mega STe's 16 MHz, and runs only there. See + <<_the_atari_st_family>>. |MS-DOS |386 or later @@ -73,16 +74,17 @@ and runs on all of them: |what Windows 3.1 in 386 enhanced mode needs |A Windows 3.x program: the hosted core with a backend written to the Windows 3.1 API, in 32-bit code through Open Watcom's Win386 extender; 8-bit - indexed, so every palette is exact on a 256-colour display. See - <<_windows_programs>>. + indexed, so every palette is exact on a 256-colour display, and sprites + compiled as on DOS. See <<_windows_programs>>. |PSP |MIPS Allegrex at 222 MHz |32 MB (PSP-1000) |PSP homebrew, for custom firmware and PPSSPP: the SDL3 port built with pspdev. The stage sits at 1:1 in the middle of the 480x272 screen; the - d-pad, nub and buttons are joystick 0, Start is Return and Select Escape. - See <<_psp_programs>>. + d-pad, nub, and buttons are joystick 0; Start is Return, Select is Escape, + and Square opens a keyboard on the screen for every other key. See + <<_psp_programs>>. |=== The Apple IIgs is deliberately the slowest target and the design reference. If @@ -116,7 +118,7 @@ that out of your program: |=== |Platform |In-memory pixel storage -|Apple IIgs, MS-DOS, DVX, SDL3, Web, PSP +|Apple IIgs, MS-DOS, DVX, SDL3, Web, PSP, Win32, Win16, Haiku, BeOS |Chunky 4bpp packed: two pixels per byte, high nibble is the left pixel. |Commodore Amiga @@ -126,7 +128,8 @@ that out of your program: |Native planar, word-interleaved: four plane words per 16 pixels, per row. |Sharp X68000 -|Text-plane planes, four planes 128 KB apart. +|Native planar: four separate bitplanes, as on the Amiga. The display they are + copied to is the text plane, four planes 128 KB apart. |=== Every public entry point speaks in *colour indices, 0 to 15*. Your code never @@ -135,11 +138,12 @@ documented and flagged in this manual -- see <<_writing_portable_code>>. === What You Get -* Surfaces, palettes and per-scanline palette selection +* Surfaces, palettes, and per-scanline palette selection * Drawing primitives: pixels, lines, rectangles, circles, flood fill -* 8x8 tiles, tile banks, tile maps and bitmap text +* 8x8 tiles, tile banks, tile maps, and bitmap text * Sprites with colour-0 transparency and *runtime-compiled blitters* -- the - library emits native 65816, 68000 or x86 code for each sprite at run time + library emits native 65816, 68000, or x86 code for each sprite at run time + (on the hosted ports other than Win16 every sprite is interpreted) * Keyboard (both game-style predicates and a typed-character queue), mouse and joystick * Four-channel Protracker `.MOD` music, a portable chip-tracker music format, @@ -155,8 +159,8 @@ documented and flagged in this manual -- see <<_writing_portable_code>>. JoeyLib is distributed under the MIT License (`LICENSE.md`). A program built with it has JoeyLib compiled in, so it carries JoeyLib's copyright and permission notice, in its documentation for instance. A program also carries -code from the libraries each port links (libxmp-lite for music on most -ports, the toolchains' C libraries and start files), whose terms +code from the libraries each port links (PTPlayer for music on the Amiga, +the toolchains' C libraries and start files), whose terms `LICENSES.md` lists with what a shipped program owes them. On the Atari ST that includes MiNTLib's LGPL parts: a closed-source game lets its users relink it against a changed MiNTLib (by shipping its object files, for @@ -169,7 +173,7 @@ If you are new to JoeyLib, read <<_getting_started>>, then <<_anatomy_of_a_joeylib_program>>, then <<_writing_portable_code>>. Those three chapters are the whole programming model. -<<_creating_assets>> covers getting graphics, music and sound into a form the +<<_creating_assets>> covers getting graphics, music, and sound into a form the library can load. <<_building_your_program>> explains how to get your own program, in a diff --git a/docs/Portability.adoc b/docs/Portability.adoc index 80be1f6..f097a7d 100644 --- a/docs/Portability.adoc +++ b/docs/Portability.adoc @@ -1,7 +1,7 @@ == Writing Portable Code A JoeyLib program that follows the rules in this chapter compiles and behaves -the same on all five machines, on DVX, under SDL3, in a browser and on Windows. Most of the rules exist because one +the same on all five machines, on DVX, under SDL3, in a browser, and on Windows. Most of the rules exist because one particular target cannot do something the others can, and the Apple IIgs is usually that target. @@ -13,7 +13,7 @@ usually that target. . Compile your sprites before you draw them. . Keep large objects out of function-local variables on the IIgs. . Use `jlRandom`, not `rand`, when identical behaviour across ports matters. -. Keep file names short, uppercase-safe and 8.3-compatible. +. Keep file names short, uppercase-safe, and 8.3-compatible. . Do not mix save-under with clean-buffer erasing in the same screen region. . Pick one of the audio layers per scene, not two. @@ -21,11 +21,12 @@ The rest of this chapter explains each one. === Memory -This is the rule that catches everyone. *The C heap on the IIgs is tiny.* It -lives in bank 0, and a single `malloc` is capped at roughly 32 KB -- often far -less in practice, because the clang libc heap on that port can be a few hundred -bytes. A program that `malloc`s a 32000-byte buffer works on four targets and -fails on the reference one. +This is the rule that catches everyone. *The C heap on the IIgs is tiny.* The +whole heap is one Memory Manager block of 32 KB to 48 KB, and if that block +cannot be had at start-up it is only what the program's link left in its +bank, which can be a few hundred bytes. A program that `malloc`s a +32000-byte buffer works on every other target and can fail on the reference +one. Use the library allocator, which is backed by each platform's native allocator (the Memory Manager on the IIgs): @@ -40,7 +41,10 @@ if (buffer == NULL) { jlFree(buffer); ---- -`jlAlloc` does *not* zero the memory it returns. +`jlAlloc` does *not* zero the memory it returns. Never free a `jlAlloc` block +with `free`, or a `malloc` block with `jlFree`: on most ports they are the +same, but on the IIgs a `jlAlloc` block is a Memory Manager handle, and mixing +them corrupts memory. For bulk copies, `jlMemCopy` and `memcpy` are the same thing: `jlMemCopy` is `memcpy` with a 32-bit count. On the IIgs both are `MVN` block moves at 7 @@ -71,12 +75,11 @@ void renderScene(void) { === Timing Covered in <<_frame_rate_independence>>, and worth repeating because it is the -second most common portability bug: the targets run at 50, 60 and 70 Hz. Scale -by `jlFrameHz()` or work from `jlMillisElapsed()`. +second most common portability bug: the targets run at 50, 55, 60, and 70 Hz. +Scale by `jlFrameHz()` or work from `jlMillisElapsed()`. -`jlFrameCount()` is a 16-bit counter, so you must poll it faster than twice the -frame rate or you will miss edges, and it wraps roughly every 18 minutes at -60 Hz. +`jlFrameCount()` is a 16-bit counter that wraps roughly every 18 minutes at +60 Hz, so compare two readings by subtracting them. === Colour and the Palette @@ -94,28 +97,31 @@ have both opaque and masked forms. Two rules. *Compile before drawing.* `jlSpriteDraw` never compiles on demand. An -uncompiled sprite runs through the interpreter at roughly 1/40th the speed, and -nothing warns you. +uncompiled sprite runs through the interpreter, two to six times slower +depending on the machine, and nothing warns you. *Align your backup buffers.* Give any `jlSpriteBackupT` backing array 2-byte -alignment, by declaring it `uint16_t` or with an alignment attribute. An odd -buffer address routes save and restore onto slow interpreted paths on the -planar ports, because the 68000 cannot perform word accesses at odd addresses. +alignment, by declaring it `uint16_t` or with an alignment attribute. On the +Amiga, ST, and X68000 an odd buffer address sends save and restore down a much +slower path, and on the Amiga and ST it crashes the trusted variants. [source,c] ---- // Right: 2-byte aligned by construction. -static uint16_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4) / 2]; +static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)]; // Risky: may land on an odd address. -static uint8_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4)]; +static uint8_t gBacking[jlSpriteBackupLength(uint8_t, 4, 4)]; ---- -Note also that on the planar ports the region a save-under actually captures is -the *16-pixel-aligned window* containing the sprite, not the sprite rectangle. -`jlSpriteSaveUnder` reports the true geometry in the backup structure's -`x`, `width`, `height` and `sizeBytes` fields. It is that recorded window you -must not overwrite before restoring. +Note also that the region a save-under actually captures is often not the +sprite rectangle: it can be the *16-pixel-aligned region* containing the +sprite (the Amiga, ST, and X68000), only the rows holding an opaque pixel (the +IIgs), or only the visible part of a sprite partly off the surface (every +port). `jlSpriteSaveUnder` reports the true geometry in the backup structure's +`x`, `y`, `width`, `height`, and `sizeBytes` fields. It is that recorded region +you must not overwrite before restoring. On the IIgs the buffer must not cross +a 64 KB bank boundary, which a `jlAlloc` block and the C heap never do. === Randomness @@ -142,19 +148,24 @@ you: Pass bare names: `jlDataOpen("levels/title.dat", "rb")`, never `"DATA/levels/title.dat"`. On DVX the two are not in the current directory -at all (<<_dvx_programs>>), so a program that opens `"DATA/..."` itself finds -nothing there. +at all (<<_dvx_programs>>), so a program that opens `+"DATA/..."+` itself finds +nothing there. DOS, the ST, the Amiga, and the X68000 look in the current +directory, so start a program there from its own folder; the IIgs looks for +`DATA/` under GS/OS prefix 0 but keeps `SAVES/` in the application's folder +(<<_files_and_saves>>). Keep names portable across the retro filesystems this library targets: *at most -8 characters plus a short extension, letters and digits only*, so they satisfy -both ProDOS and DOS 8.3. Do not rely on case being preserved. +8 characters plus a short extension, letters and digits only, starting with a +letter*, so they satisfy both ProDOS and DOS 8.3. Nothing checks them for you. +Do not rely on case being preserved, and spell a name the same way every +time: on Linux, Haiku, BeOS, and the web, case matters. A failed save is not fatal -- the calls report failure and your program should degrade gracefully, because on some volumes `SAVES/` simply is not writable. === Audio -JoeyLib gives you three sound layers, and on two platforms they share physical +JoeyLib gives you three sound layers, and on some platforms they share physical hardware: [cols="2,5", options="header"] @@ -162,41 +173,48 @@ hardware: |Layer |What it is |Module playback -|`jlAudioPlayMod` -- four-channel Protracker `.MOD`, mixed in software on - DOS and ST, hardware-replayed on Amiga and IIgs. +|`jlAudioPlayMod` -- four-channel Protracker `.MOD`, mixed in software by + JoeyLib's module player on DOS, the ST, the X68000, and the hosted ports, + played by DVX's music library on DVX, and replayed by the sound hardware on + the Amiga and IIgs. The module player is the Amiga's replayer, PTPlayer, + translated to C, so those ports play a module as the Amiga does, at a PAL + Amiga's pitch, Protracker 2.3's quirks and all. |Chip tracker |`jlMusicPlay` -- a portable event stream driving three tone voices plus noise, at register-write cost with no CPU mixing. |Voices and effects -|`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx` and the streaming forms. +|`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and the streaming forms. |=== [WARNING] ==== -On the Atari ST and Amiga, the tone voices *are* the same hardware the module -player drives. While a `.MOD` is playing, `jlAudioVoice` writes fight the -player's register writes and lose. Choose the module layer or the voice and -tracker layer *per scene*, not both at once. (The STe build plays modules and -sound effects through the STe's DMA sound chip instead, so there the voices -keep the YM2149 to themselves -- but a program that also ships the ST build -still has to follow the rule.) +On the Amiga the tone voices are the hardware the module player drives, and +while a `.MOD` is playing, `jlAudioVoice` writes fight the player's register +writes and lose. On the plain ST, digital sound takes the chip channel tone +voice 0 plays on, so voice 0 is silent while a module or effect plays. On the +IIgs the voices share sound effect slots 0 to 2 and the noise slot 3. Choose +the module layer or the voice and tracker layer *per scene*, not both at once. ==== The chip tracker is the portable choice for music. It costs almost nothing on -every port -- unlike `.MOD` playback, which needs real CPU mixing and is -marginal on the ST. +every port -- unlike `.MOD` playback, which needs real CPU mixing where the +sound hardware cannot replay a module. On a plain 8 MHz ST a four-channel +module takes about three quarters of the machine (measured under Hatari: a +third for the interrupt that plays each sample through the YM2149, the rest +the mix), leaving a quarter for the game; the STe's DMA sound needs no such +interrupt. -On the IIgs, `jlAudioVoice` and `jlAudioNoise` require `jlAudioInit` (they ride -the tracker engine's DOC path) and behave as retriggered one-shots of about -80 ms, so a note held longer must be re-asserted roughly once a frame. On DOS, -ST and Amiga those two calls are direct hardware programming and work without -`jlAudioInit`. +A `jlAudioVoice` or `jlAudioNoise` note sounds until the next call for it, as +long as `jlAudioFrameTick` runs every frame: the IIgs plays each note as a +short sample and restarts the held ones from there. -A failed `jlAudioInit` is *not* fatal. `jlInit` still succeeds and every audio -call becomes a silent no-op, so a program need not treat missing sound as an -error. +`jlInit` starts audio (`jlConfigT.noAudio` leaves it off) and `jlShutdown` +stops it. Sound that does not start is *not* fatal: `jlInit` still succeeds, +modules and sound effects become silent no-ops (the tone and noise voices +still play on the Amiga, the ST, DOS, and the X68000), so a program need not +treat missing sound as an error. === Per-Platform Behaviour Summary @@ -207,9 +225,9 @@ The API is uniform; these are the places where what you get differs. |Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |DVX |SDL3, Web, Win32, PSP, Haiku, BeOS |Win16 |Frame rate -|60 Hz -|50 Hz (PAL) -|60 Hz (NTSC) +|60 Hz (50 on a PAL machine) +|50 Hz (PAL; 60 on an NTSC machine without PAL) +|50 or 60 Hz, as the machine is set |70 Hz |55 Hz (56 under MAME) |70 Hz (virtual; the real retrace on the whole screen) @@ -221,7 +239,7 @@ The API is uniform; these are the places where what you get differs. |planar |planar, interleaved |chunky 4bpp -|text-plane planar +|planar (shown through the text plane) |chunky 4bpp |chunky 4bpp |chunky 4bpp @@ -234,11 +252,11 @@ The API is uniform; these are the places where what you get differs. |compiled |compiled |interpreted -|interpreted +|compiled |Stereo pan |all three -|left and right +|all three (centred on a free channel each side) |ignored (mono) |ignored (mono) |ignored @@ -246,24 +264,36 @@ The API is uniform; these are the places where what you get differs. |all three |ignored (mono) -|`jlAudioTone` -|DOC oscillator -|Paula -|YM2149 -|PIT counter 2 -|- -|a DVX tone (FM chip or mixer) -|voice 0 of the mixer -|voice 0 of the mixer +|Program icon (<<_icons>>) +|the Finder's (`Icons` folder) +|Workbench's (`Name.info`) +|none +|none +|none +|Program Manager's +|the window's (SDL3), the page's (web), Explorer's (Win32), the menu's +(PSP), Tracker's (Haiku, BeOS) +|Program Manager's + +|Tone voices (`jlAudioTone` is voice 0) +|DOC oscillators on sound effect slots 0 to 2 +|Paula channels 0 to 2 +|YM2149 (voice 0 silent while digital sound plays) +|OPL2 (with none, voice 0 on the PC speaker) +|YM2151 (sine waves) +|DVX tones (FM chip or mixer) +|square waves in the mixer +|square waves in the mixer |Joystick -|analog, auto-disables -|digital +|one, analog, auto-disables |digital |digital +|analog |digital |analog (DVX calibrates) -|the OS's gamepads or joysticks (OS calibrates); the PSP's own controls +|SDL's gamepads and joysticks (SDL3, web); Windows' joysticks (Win32); the system's joysticks + (Haiku, BeOS); the PSP's own controls |Windows' joysticks (Control Panel calibrates) |Keyboard layout @@ -273,7 +303,7 @@ The API is uniform; these are the places where what you get differs. |fixed US |machine's own |DVX's -|the OS's; the PSP has no keyboard +|the OS's; on the PSP, a US one on the screen |Windows' |Serial ports @@ -281,28 +311,35 @@ The API is uniform; these are the places where what you get differs. |`serial.device` |Modem port |COM1..COM4 -|- +|RS-232C (needs RSDRV.SYS) |COM1..COM4 (DVX's library) -|Win32: COM1 and up; the others - +|Win32: COM1 and up; SDL3, web, PSP, Haiku, BeOS: none |COM1..COM9 |=== -The joystick row deserves a note: the IIgs port *auto-disables* joystick -polling after a short window with no stick detected, because polling costs -about 3 ms per frame there. It does not re-probe on its own. If a user plugs a -stick in while the program is running, you must call `jlJoystickReset` to -resume polling. +The joystick row deserves a note: the IIgs port stops polling for a stick +after about a second (60 polls) with none detected, because polling for an +absent stick costs about 1 ms a frame there; `jlJoystickReset` starts it again +(<<_jljoystickreset>>). On the X68000 both joystick ports always report a stick, as on the ST: an -empty port cannot be told from a stick at rest. `JOY_BUTTON_0` is a stick's -trigger A and `JOY_BUTTON_1` its trigger B. Holding both directions of an axis +empty port cannot be told from a stick at rest. `JL_JOY_BUTTON_0` is a stick's +trigger A and `JL_JOY_BUTTON_1` its trigger B. Holding both directions of an axis reads as the positive one (right, down), as on the Amiga and ST. -On the ST, `JOYSTICK_0` is port 1, the joystick port, and `JOYSTICK_1` port 0. +On the ST, `JL_JOYSTICK_0` is port 1, the joystick port, and `JL_JOYSTICK_1` port 0. JoeyLib keeps TOS's keyboard-processor mode, in which port 0 is the mouse's: -the ST reports no stick movement there, so `JOYSTICK_1` stays at rest whatever -is plugged in. Port 1's fire button is the same line as the mouse's right -button, so a press reads as both `JOY_BUTTON_0` and `MOUSE_BUTTON_RIGHT`. +the ST reports no stick movement there, so `JL_JOYSTICK_1` stays at rest whatever +is plugged in. A stick there has one button, so a quick press is +`JL_JOY_BUTTON_0` and a held one `JL_JOY_BUTTON_1` (<<_jljoydown_jljoypressed_jljoyreleased>>). +Port 1's fire button is the same line as the mouse's right button, so a press +reads as both the stick's button and `JL_MOUSE_BUTTON_RIGHT`. + +The 1040 STe also has two enhanced joystick ports, and the STe build reads an +Atari Jaguar pad in each. A pad in enhanced port A is `JL_JOYSTICK_0` whenever +it was used more recently than port 1's stick, so either one plays, and a pad +in enhanced port B is `JL_JOYSTICK_1`. A pad's A and B buttons are +`JL_JOY_BUTTON_0` and `JL_JOY_BUTTON_1`. The Mega STe has no enhanced ports. On the Amiga the sticks are read through `lowlevel.library`, which Kickstart 3.1 does not have in ROM: AmigaOS loads it from `LIBS:`, so the boot drive must @@ -312,12 +349,15 @@ on its own must do the same. `lowlevel.library` reports a stick only once it has moved, and never a mouse. It also checks each port again now and then, and a two-button stick with both buttons held can read as nothing at that moment, so once a port has shown a stick JoeyLib keeps it one; `jlJoystickReset` hands -the port back to `lowlevel.library`'s own detection. +the port back to the library's own detection. `lowlevel.library` reports +a one-button stick and a two-button one alike, so a stick is taken for a +one-button one, a held button being `JL_JOY_BUTTON_1`, until its second button +is first pressed; a CD32 pad has both from the start. -On DOS the game port's counts depend on the stick and on the CPU's speed, so -the first reading after `jlInit` or `jlJoystickReset` is taken as the stick's -rest, and a push past half that value either way reads as a full push. Call -`jlJoystickReset` with the stick centred if it was not at the start. +On DOS the game port's counts depend on the stick and on the CPU's speed, so a +stick's first reading -- after `jlInit`, after `jlJoystickReset`, or when it is +plugged in -- is taken as its centre, and its axes are read in proportion to +that: a count of 0 or twice the centre is a full push. The X68000's display refreshes at 55.46 Hz. `jlFrameHz` times it when the program starts and rounds to the nearest whole number, so it reports 55 -- @@ -325,18 +365,238 @@ and 56 under MAME, whose X68000 display runs slightly fast (56.07 Hz). `jlMillisElapsed` does not depend on it: it reads the machine's 100 Hz system timer to the millisecond. -Per-scanline palettes cost something on the X68000, and only there. Its -display has one 16-colour palette, so the port reloads it as the beam scans: an -interrupt stores the next band's colours in the horizontal blanking before the -band's first line. A screen with one palette costs nothing and arms no -interrupt. Each change of palette between one line and the next costs about -one line of CPU time (32 microseconds); a band more than two lines from the -one before costs about two. A screen that changes palette on every line -therefore spends a third of each frame in that interrupt, during which serial -and mouse interrupts wait (see the serial chapter for how long a run can be -before received bytes are lost). Palette and SCB changes take effect at the -start of the next frame rather than mid-frame, so a fade can lag its pixels by -one frame but never tears. +Per-scanline palettes cost CPU time on the ST and the X68000, whose displays +have one 16-colour palette, and a stall on the Amiga when the bands move. The +ST takes a timer interrupt at every change of palette, every frame, and copies +the next band's 16 colours there. The X68000 reloads its palette as the beam +scans: an interrupt stores the next band's colours in the horizontal blanking +before the band's first line. A screen with one palette costs nothing on +either and arms no interrupt. On the X68000 each change of palette between one +line and the next costs about one line of CPU time (32 microseconds); a band +more than two lines from the one before costs about two. A screen that changes +palette on every line therefore spends a third of each frame in that +interrupt, during which serial and mouse interrupts wait (see the serial +chapter for how long a run can be before received bytes are lost). On the +Amiga the copper changes the palettes at no CPU cost, but an SCB change that +moves where a band starts or ends rebuilds the copper list, which holds up +that `jlStagePresent` for about three frames on an A500; a change of colours, +or of which palette an unmoved band uses, costs nothing. On the ST a +present's colours and pixels show from the same frame: it hands the +interrupts their new colours with the page it flips to. On the X68000, a +screen of two or more bands takes palette and SCB changes at the start of the +next frame rather than mid-frame, so a fade can lag its pixels by one frame +but never tears; a screen of one palette has its colours written at the +present. + +=== Where the Ports Still Differ + +The API is the same on every port, and where a port behaved differently from +the rest because of the library, the library has been changed. What is left is +set by the machines and by the systems the ports run on, and stays. Each entry +names the ports, says how they differ, and gives the reason. + +[[portsDifferDisplay]] +==== Display + +* *IIgs: a big present can tear.* The Super Hi-Res display reads one fixed + buffer, with no second page to flip to, and a full-screen present takes + several frames, so the screen can show part of the old picture and part of + the new. +* *Win16, and DVX in a window: no vertical blank.* Windows 3.1 has no call that + waits for the display's retrace, and DVX syncs to it only on its full screen, + so `jlWaitVBL` keeps a virtual frame clock there and nothing stops a present + from tearing. +* *X68000: a small, wide picture.* The machine has no 320x200 mode, so the stage + sits in the middle of a 512x512 screen, a picture about 2.1 times as wide as + it is tall. That screen is a 31 kHz mode, so the monitor must take 31 kHz. +* *ST: eight levels a channel.* The plain ST's video chip has 3 bits per colour + channel, so `$0RGB` values one apart can look the same. The STe build shows + all 4 bits. +* *X68000: one intensity bit for three channels.* Its colours have 5 bits per + channel and an intensity bit the three share. JoeyLib sets that bit for every + colour but black, so white is full white, and a channel that is 0 in any + other colour shows at 1/63 rather than 0: `$F00` carries a trace of green and + blue. +* *Windows and DVX on a 256-colour display.* The display has one palette for + everything on it. Windows keeps 20 of its entries for itself, and DVX shares + it among all its programs, so Win32 and a DVX window show each colour as the + nearest one the display has, and Win16 shows them exactly only while the 236 + entries left are enough (<<_windows_programs>>). +* *Amiga and X68000: colours a frame off their pixels.* A present's new + colours and new pixels show from the same frame on the other ports (the IIgs + and DOS change both at once, and can tear). The Amiga's pixels change at the + next frame, when the copper loads the new bitplane pointers, but the bands + below the beam take their new colours in the frame the present runs: the + copper list is graphics.library's, one per screen, and JoeyLib patches its + colours in place. The X68000 draws straight into the displayed text plane, + which has no second page, while a screen of two or more bands takes its new + colours at the start of the next frame, so there the colours come a frame + after the pixels. +* *ST and X68000: per-scanline palettes cost time.* Both displays have one + 16-colour palette at a time, so the CPU loads each band's colours as the beam + reaches it: the ST in a timer interrupt at every change of palette, every + frame, and the X68000 in a raster interrupt. A screen with one palette costs + nothing (<<_per_platform_behaviour_summary>>). + +[[portsDifferInput]] +==== Input + +* *IIgs keyboard: one key at a time, by character.* The keyboard reports only + the last character typed and whether any key is down. So JoeyLib sees one key + besides the modifiers at a time, knows a key by the character it types rather + than by its place on the keyboard, and reads it released only when every key + is up. With Ctrl held, Ctrl+H, I, J, K, M, and U read as those letters, not as + the left arrow, Tab, the down and up arrows, Return, and the right arrow, + whose codes they share; a shifted digit key reads as its digit only on + the US layout; and the keypad's keys read as the characters they type, so + its 8, 2, 4, and 6 are the digits, not the arrows as on the other ports. +* *IIgs keyboard: the keys it lacks.* It has no F1 to F10, no forward Delete + (Delete is the backspace key), and one Shift as far as JoeyLib can tell: + `JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. Option is + `JL_KEY_LALT`. +* *ST keyboard: the Alt layer on TOS before 4.00.* A national keyboard types + some ASCII with Alt (a German one's @ [ ] { } \). TOS 4.00 and later, and + EmuTOS, keep that layer in the keyboard tables JoeyLib reads, so Alt types + it; older TOS keeps it in the keyboard code JoeyLib replaces, so there Alt + with a key types nothing, as on the ports where Alt is a command key. +* *X68000 keyboard: one Shift.* Both Shift keys send the same code, so + `JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. OPT.1 and OPT.2 are + `JL_KEY_LALT`. +* *IIgs joystick: one stick, sharing two keys.* The game port takes one stick, + and its buttons are wired to the same lines as the Command and Option keys, + so holding either key reads as a button. +* *Mouse buttons.* The IIgs mouse has one button, and the ST and X68000 mice + have two; the other ports report three where the mouse has them. +* *ST and X68000: a stick is always connected.* An empty joystick port reads + exactly as a stick at rest, so `jlJoystickConnected` is true for both. +* *ST: the mouse port's stick.* The keyboard processor reports port 0 as the + mouse in the mode JoeyLib keeps it in, so `JL_JOYSTICK_1` never moves, and + port 1's fire button is the mouse's right button too. +* *Amiga: one button or two.* `lowlevel.library` reports a one-button and a + two-button stick alike, so a stick counts as a one-button one until its + second button is first pressed. +* *PSP: no mouse, one key at a time.* The PSP has no mouse, and its on-screen + keyboard has one cursor, so it presses one key at a time (Shift, Ctrl, and + Alt stay down until the next key is let go). + +[[portsDifferAudio]] +==== Audio + +* *Plain ST: voice 0 is silent while digital sound plays.* The ST has no + digital sound chip, so JoeyLib plays modules and effects by changing the + sound chip's channel A volume 12288 times a second, and channel A is tone + voice 0's. Voice 0 sounds again when the digital sound stops, and voices 1 + and 2 play on. The STe build uses the STe's DMA sound and keeps all three. +* *ST: noise shares voice 2's volume.* The YM2149 has one volume per channel, + and its noise is mixed into channel C, voice 2's. Author noise hits and + voice-2 notes apart; `songbake` warns when they overlap. +* *ST: volume steps.* The YM2149 has 16 volume levels about 3.5 dB apart, so a + 2 dB attenuation step is matched to within 1.6 dB. +* *Amiga: four channels for everything.* Paula has four sound channels, two + on each side, shared by the module, the effects, the three tone voices, and + the noise. At most four sounds play at once, an effect can take a channel a + voice was using, and a module plays on all four. A channel plays on one side + only, so a centred effect needs a free channel on each side, and without + them it plays on one channel, from one side; and the tone voices, one + channel each, are heard from one side: voice 0 (and `jlAudioTone`) on the + left, voices 1 and 2 on the right. +* *IIgs: voices on the effect slots.* By design the IIgs plays tone voices 0 to + 2 on sound effect slots 0 to 2 and the noise on slot 3, so an effect on one + of those slots and a voice cut each other off. Use slot 4 for effects while + voices or `jlMusicPlay` sound. +* *IIgs: mono.* A stock IIgs mixes its stereo channels into one speaker, so + panning is heard only with a stereo card. +* *Plain ST and X68000: no panning.* Each has one output for digital sound: + the ST its channel A, the X68000 its one ADPCM channel, so `jlAudioSetPan` + is ignored. +* *DOS: the noise is a drum.* The OPL2 has no free-running noise generator; its + rhythm-mode snare is the nearest, a tuned drum whose pitch cannot follow the + YM2149's noise rate. +* *DOS and X68000: tones are not square.* The OPL2 and the YM2151 make their + waves from sines and have no square wave, so their tone voices (DOS's when it + has an OPL2) are not the square waves the other ports play. +* *Amiga and IIgs: the brightest noise.* Paula plays a sample no faster than + 28.6 kHz and the IIgs's DOC no faster than 26 kHz, so noise pitches 0 to 4 + sound alike there. +* *Where tick callbacks fall.* The calls average exactly `hz` a second on every + port. The ST and X68000 deliver them from the system's own timer, at 200 and + 100 Hz, so the calls due between two of its interrupts come together. DVX + delivers them at `jlAudioFrameTick`, because its sound library may not be + called from an interrupt, and so does the IIgs, counting them against the + display's beats. +* *DOS, the ST, and the X68000 mix in program time.* The module player and + the SFX overlay run too long for an interrupt, so their sound buffers are + refilled at `jlAudioFrameTick`: a program that stops calling it for longer + than the buffer lasts stops or repeats its sound. +* *Win16 with a Sound Blaster 2.0: 8-bit sound.* Its driver, SNDBLST2.DRV, takes + only 8-bit samples, so the quietest attenuation steps are coarser there. + +[[portsDifferSerial]] +==== Serial + +* *Top speeds.* The ST runs at 19200 baud at most (its MFP's rate table), a + IIgs Super Serial Card at 19200 (the 6551's), the X68000 at 38400 (RSDRV.SYS's + table; really 39062), and Win16 with Windows' own COMM.DRV at 57600. +* *One port.* The base ST has one RS-232 port, and so has the X68000, whose + second serial channel is the mouse's. +* *IIgs modem and printer ports: no RTS.* Their mini-DIN-8 connector's one + handshake output is DTR, so under RTS/CTS nothing can tell the peer to + pause: receive needs XON/XOFF or a protocol that paces itself. The printer + port's channel is also AppleTalk's. +* *IIgs Super Serial Card: lines tied together.* The 6551's DTR is also its + receiver's enable, and dropping its RTS stops its transmitter. +* *X68000: DTR rises at every line setup.* The ROM's and RSDRV.SYS's line setup + raise DTR whenever it runs. +* *Windows: DTR drops at close.* Win32's driver, and Win16 with Windows' own + COMM.DRV, drop DTR when the port is closed, and neither has a call that + prevents it. +* *IIgs: disk access stops interrupts.* GS/OS holds interrupts off for 65 to + 220 ms at a time while it reads or writes a 3.5-inch floppy, which can lose + received bytes (<<_what_can_lose_received_bytes>>). + +[[portsDifferFiles]] +==== Files and Saves + +* *Name rules.* ProDOS names on the IIgs are at most 15 characters and start + with a letter; DOS, the ST, and Win16 use 8.3 names; and Linux, Haiku, BeOS, + and the web tell upper and lower case apart. Keep to the portable rule + (<<_files>>). +* *Pre-compiled sprites per processor.* A `.spc` holds machine code, so it is + baked per port: DVX and Win16 run DOS's x86 code, the STe the ST's, and the + ports that interpret sprites use only its pixels. +* *Web saves are the browser's.* A browser may clear a site's stored data to + make room, and a private window keeps nothing once it closes. + +[[portsDifferMemory]] +==== Sprites and Memory + +* *IIgs: code and copies stay inside 64 KB banks.* The 65816 runs code within + one 64 KB bank and its block copy stays within one, so each sprite's code + must fit in one bank of the codegen arena, the arena is at most four banks, + compacting never merges free space across banks, and a save-under buffer + must not cross a bank. +* *Code size differs by processor.* The 68000 and x86 emitters make code four + to eight times larger than the 65816 one, so with the same `codegenBytes` a + large sprite can compile on the IIgs and fall back to the interpreter + elsewhere. +* *IIgs: a small C heap, and `jlFree` apart from `free`.* The C library's heap + is one Memory Manager block inside one bank, 32 KB to 48 KB, and a `jlAlloc` + block is a Memory Manager handle, so `jlFree` and `free` must never be given + each other's blocks. + +[[portsDifferTiming]] +==== Timing + +* *Frame rates.* The displays run at 50, 55, 60, and 70 Hz, as the table above + shows; the hosted ports and DVX keep a virtual 60 and 70 Hz. +* *IIgs: the clock stops while interrupts are off.* The IIgs has no free-running + timer JoeyLib can read, so its clock counts the display's vertical blanks, + and blanks that come while interrupts are held off are lost: a long stretch + with interrupts off, a floppy access for one, leaves the clock behind. +* *DOS: frames counted by the clock.* VGA has no dependable retrace interrupt, + so `jlFrameCount` counts 70ths of a second of the millisecond clock while + `jlWaitVBL` waits for the real retrace (70.086 Hz), and the two slowly drift + apart. === A Portability Checklist @@ -365,7 +625,7 @@ audience (or both): * *The ST build* (`make atarist`, programs in `build/atarist/bin`) is the baseline. It is pure 68000 and *never uses STe hardware*: it runs on every - ST -- STF, STFM, Mega ST, STe and Mega STe alike -- exactly as on a plain + ST -- STF, STFM, Mega ST, STe, and Mega STe alike -- exactly as on a plain ST, at no cost for the STe's existence. * *The STe build* (`make ste`, programs in `build/ste/bin`) is the same port compiled with `JOEYLIB_ATARI_STE`. It uses the STe's hardware @@ -406,7 +666,7 @@ Nothing in the API differs between them: The pixels are identical: UBER's 51 surface hashes match the ST's golden for the STe build on an STe and a Mega STe, and the gates check that under Hatari. `./joey-run` gives an ST-build program a plain ST and an STe-build program an -STe; `--machine st`, `megast`, `ste` or `megaste` picks another. A project +STe; `--machine st`, `megast`, `ste`, or `megaste` picks another. A project builds the STe variant with `make ste`, like any other port; it takes the ST's baked assets. @@ -420,7 +680,7 @@ expect less on memory-bound operations there. === DVX Programs DVX is a DOS desktop: a VESA graphics GUI with windows, cooperative -multitasking and its own libraries for sound, music, joysticks and serial +multitasking, and its own libraries for sound, music, joysticks, and serial ports. On DVX a JoeyLib program is one of its programs, a `.app` module running in a window of its own beside the others. The graphics are the MS-DOS port's -- the same chunky stage and the same x86 code, so UBER's @@ -430,7 +690,7 @@ hashes are DOS's -- and the rest behaves like this: at the largest whole scale, up to 3x, that fits the desktop: 1x at 640x480, 2x at 800x600, 3x at 1024x768. The stage reaches the desktop through one colour table per palette, so SCB palettes are exact on 15-, - 16- and 32-bit desktops. On a 256-colour desktop every colour is the + 16-, and 32-bit desktops. On a 256-colour desktop every colour is the nearest one in DVX's palette, which all programs share: gradients band. * *Full screen.* Game > Full Screen, or Alt+Enter, puts the program on the whole screen in DVX's full-screen mode, 320x200 in 256 colours (VGA mode @@ -453,18 +713,18 @@ hashes are DOS's -- and the rest behaves like this: does not tear. The frame count is the same clock in both, so switching never makes it jump. * *Keys DVX keeps.* While the window is active, or the program has the whole - screen, every key goes to the program, F1, F10 and Alt included, except + screen, every key goes to the program, F1, F10, and Alt included, except Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close the window), and Alt+Enter, which the port keeps for full screen. The window's Game menu has Full Screen, Help (the program's help file) and Exit. Typed characters come through DVX's keyboard layout. -* *Closing ends the program.* Alt+F4, the close box and Game > Exit end the - program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent` or +* *Closing ends the program.* Alt+F4, the close box, and Game > Exit end the + program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent`, or `jlSerialPoll`, as `exit()` does anywhere: its `atexit` handlers run, then `jlShutdown`, and its log is closed. * *Sharing the machine.* DVX multitasks cooperatively: the other programs, and this one's own window events, run only while it yields. `jlWaitVBL` - yields, and so do `jlInputPoll`, `jlStagePresent` and `jlSerialPoll`, at + yields, and so do `jlInputPoll`, `jlStagePresent`, and `jlSerialPoll`, at most once a frame. A loop that calls none of them freezes the desktop until it ends. * *Files.* DVX's programs share one current directory, so JoeyLib roots its @@ -518,32 +778,36 @@ this: program presents. * *Sprites are interpreted.* There is no code emitter for the host's CPU, so `jlSpriteCompile` returns false and every sprite is drawn by the - interpreter -- far faster than any of the old machines all the same. - `joey/platform.h` defines `JOEYLIB_SPRITE_INTERPRETED`, for a program that + interpreter -- far faster than any of the old machines all the same. (The + Win16 port, below, compiles them as DOS does.) + `joey/platform.h` defines `JL_SPRITE_INTERPRETED`, for a program that checks how many of its sprites compiled. A pre-compiled bank (`.spc`) loads its cels' pixels and leaves their code: the DOS bake is used. * *Input.* Keys by where they are on the keyboard, so another layout does not move the arrow keys; typed characters through the OS's keyboard layout. The mouse is the pointer over the stage, in stage pixels (the OS pointer is hidden over the window: JoeyLib programs draw their own). The first two - gamepads are joysticks 0 and 1: the left stick or the d-pad, the south - button (A on an Xbox pad) as `JOY_BUTTON_0` and the east button (B) as - `JOY_BUTTON_1`; they come and go as they are plugged in. + gamepads or joysticks are joysticks 0 and 1, coming and going as they are + plugged in. A gamepad SDL knows how to map gives its left stick or its + d-pad, its south button (A on an Xbox pad) as `JL_JOY_BUTTON_0`, and its + east button (B) as `JL_JOY_BUTTON_1`; any other joystick its first two axes + or its first hat, and its first two buttons. * *Closing ends the program.* Closing the window ends the program at its next - `jlWaitVBL`, `jlInputPoll` or `jlStagePresent`, as `exit()` does anywhere: + `jlWaitVBL`, `jlInputPoll`, or `jlStagePresent`, as `exit()` does anywhere: its `atexit` handlers run and the window and the sound are closed. * *Files.* The program's own folder is the current directory from `jlInit` - on, as on the machine ports: `DATA/`, `SAVES/` and `joeylog.txt` are beside + on, as on the machine ports: `DATA/`, `SAVES/`, and `joeylog.txt` are beside the program. * *Sound* is mixed in software into 16-bit stereo at 44.1 kHz: `.MOD` music - through libxmp-lite (as on DOS), the SFX slots in stereo, the three tone - voices as square waves and the noise voice as the ST's YM2149 makes it. It + through the module player (as on DOS), Paula's channels hard left and + right as on the Amiga, the SFX slots in stereo, the three tone + voices as square waves, and the noise voice as the ST's YM2149 makes it. It is mixed in program time by `jlAudioFrameTick`, `jlWaitVBL`, - `jlInputPoll` and `jlStagePresent`, a few frames ahead, so a + `jlInputPoll`, and `jlStagePresent`, a few frames ahead, so a `jlAudioTickRegister` callback runs in program time too -- but between exactly the samples it falls between, so its notes start in the right - place in the sound. `jlAudioInit` is needed for every sound, the tone - voices included. + place in the sound. Every sound needs audio started (the default), the + tone voices included. * *No serial port.* `make sdl3` builds every example as a native program, each in @@ -571,9 +835,12 @@ are DOS's here too. Everything in <<_sdl3_programs>> holds, except: `jlInputPoll` alone, as `jlWaitForAnyKey` is). * *Sound starts with the first click or key press*, as browsers require; the page says so. -* *Saves persist.* `SAVES/` is kept in the browser's IndexedDB for the page's - address, read back when the program starts and written back every two - seconds, so a save outlives the page. +* *Saves persist.* `SAVES/` is kept in the browser's IndexedDB, read back + when the program starts and written back as each save changes, so a save + outlives the page however it ends. The store is the site's, not the page's: + every JoeyLib program served from the same site shares one `SAVES/`, so give + each game's saves names of their own. `jlDiskFree` there is the browser's + own estimate of what the site may still store. * *No full screen by default and no closing.* Alt+Enter asks the browser for the whole screen; the program ends when the page is closed. @@ -591,22 +858,31 @@ is the same chunky one, so UBER's hashes are DOS's. Everything in * *A folder for `PSP/GAME`.* A program is a folder, `build/psp/bin/NAME/`: `EBOOT.PBP`, titled with the program's name (or its description's `TITLE`) in the PSP's menu, and its `DATA/`. Copy the folder into `PSP/GAME/` on the - memory stick. Its folder is its current directory, so `DATA/`, `SAVES/` + memory stick. Its folder is its current directory, so `DATA/`, `SAVES/`, and `joeylog.txt` are there. It runs on a PSP with custom firmware (it is not signed) and in PPSSPP. The ELF it is packed from stays in `build/psp/elf/`, for `psp-addr2line`. * *The screen.* The PSP's 480x272 screen shows the stage at 1:1 in its middle, black around it. The scale and aspect settings do not apply (a PSP has no environment), and there is no full screen to switch to. -* *Its own controls, no keyboard.* The PSP's controls are joystick 0, - through SDL's gamepad mapping: the d-pad or the analog nub, cross as - `JOY_BUTTON_0` and circle as `JOY_BUTTON_1`; joystick 1 is never there. - Having no keyboard, the PSP also gives Start as the Return key and Select - as the Escape key, held (`KEY_RETURN`, `KEY_ESCAPE`) and typed - (`JL_CHAR_RETURN`, `JL_CHAR_ESCAPE` from `jlInputGetChar`) as those keys - are, so `jlWaitForAnyKey`, Return to go on and Escape to quit work. No - other key is ever down, nothing else is typed (the PSP's on-screen - keyboard is never opened), and there is no pointer to move. +* *Its own controls, and a keyboard on the screen.* The PSP's controls are + joystick 0, through SDL's gamepad mapping: the d-pad or the analog nub, + cross as `JL_JOY_BUTTON_0` and circle as `JL_JOY_BUTTON_1`; joystick 1 is + never there. Start is also the Return key and Select the Escape key, held + (`JL_KEY_RETURN`, `JL_KEY_ESCAPE`) and typed (`JL_CHAR_RETURN`, + `JL_CHAR_ESCAPE` from `jlInputGetChar`) as those keys are, so + `jlWaitForAnyKey`, Return to go on, and Escape to quit work at a press. + Square opens a PC keyboard drawn over the bottom of the screen, US layout, + for every other key: the d-pad moves its cursor, cross presses the key + under it (held down as long as cross is), L and R move the keyboard to + the top of the screen and back, out of the way of what the program shows + there, and Square closes it. Shift, Ctrl, and Alt stay down until the + next other key is let go (or until pressed again), and Caps turns capital + letters on and off. Its keys go down and up and type as a desktop's do, + so `jlKeyDown`, `jlKeyPressed`, and `jlInputGetChar` see the same as they + would there, F1 to F10, the arrows, and Ctrl and Alt chords included. + While it is open, joystick 0 rests: the d-pad and cross are the + keyboard's. There is no pointer to move. * *HOME quits.* The HOME menu's Exit ends the program as closing a window does. * *Free space* (`jlDiskFree`) is that of the memory stick (or a PSP Go's @@ -630,26 +906,28 @@ window to play in here: to play, copy the folder to a PSP, or open its The Win32 port is the hosted core with a backend of its own, written to the Win32 API (`src/hosted/win32`), for the Windows SDL3 does not reach -- -Windows 95, 98, ME, NT 4 and 2000 -- and every Windows since. Its stage is +Windows 95, 98, ME, NT 4, and 2000 -- and every Windows since. Its stage is the same chunky one, so UBER's hashes are DOS's. Everything in <<_sdl3_programs>> holds, except: * *Old Windows on purpose.* A program is a 32-bit `NAME.EXE` built by llvm-mingw against `MSVCRT.DLL` (the `win32` installer component), marked for Windows 4.0 so 95 and NT 4 will load it, linked statically, and - calling only what USER, GDI, WINMM and KERNEL32 had in 1995. `MSVCRT.DLL` + calling only what USER, GDI, WINMM, and KERNEL32 had in 1995. `MSVCRT.DLL` came with Windows 95 OSR2 and Internet Explorer; a first-release Windows 95 needs it installed. * *One thread.* The window is made on the program's thread, so its messages - arrive there and the port dispatches them at `jlInputPoll`, `jlWaitVBL` + arrive there and the port dispatches them at `jlInputPoll`, `jlWaitVBL`, and `jlStagePresent`; the sound goes out through waveOut buffers handed over at the same calls. There are no callbacks and no locks. -* *The window.* The stage is drawn with `StretchDIBits` at the stage's shape, - 4:3 or square (the same `JOEYLIB_SCALE`, `JOEYLIB_ASPECT` and +* *The window.* The stage is stretched with `StretchBlt` to the stage's + shape, 4:3 or square (the same `JOEYLIB_SCALE`, `JOEYLIB_ASPECT`, and `JOEYLIB_FULLSCREEN` settings), black around it; full screen is a borderless window over the whole monitor, which leaves the display mode alone. On a 256-colour desktop Windows maps every colour to the nearest - one it has. + one it has. Each frame draws only what changed since the last: a game + that moves a few sprites pays for their rectangles, not for the whole + window, which on a Windows 95 machine is most of a frame's time. * *Keys* by the PC scan code every key message carries (the same table as DOS), typed characters through the user's keyboard layout, and the first two joysticks through `joyGetPosEx`, calibrated in the Control Panel. Keys @@ -659,8 +937,9 @@ the same chunky one, so UBER's hashes are DOS's. Everything in `make win32` builds every example as a Windows program in `build/win32/bin/`, and `./joey-run --port win32 NAME` runs one under Wine (the `wine` installer -component, a portable Wine kept in `toolchains/`); the gates run there too. -None of it has run on a real Windows 9x or NT 4 here. +component, a portable Wine kept in `toolchains/`); the gates run there too, +`scripts/verify-win32-display.sh` among them, though that checks Wine's GDI +rather than Windows'. None of it has run on a real Windows 9x or NT 4 here. The Win16 port is the hosted core with a backend written to the Windows 3.1 API (`src/hosted/win16`), for Windows 3.1 and Windows for Workgroups 3.11 on @@ -672,16 +951,54 @@ scan code, DTR dropped at close -- except: with Open Watcom's Win386 target (the `win16` installer component): flat 32-bit code bound to Watcom's supervisor, which turns each call into the 16-bit one Windows takes, so it needs Windows in 386 enhanced mode. It - calls only what Windows 3.1 has: USER, GDI, KERNEL and MMSYSTEM. -* *Exact colours.* The frame is 8-bit, a DIB whose 256-entry colour table - holds all 16 palettes and is realized as a logical palette whenever a frame - is shown, so on a 256-colour display every colour is exact, SCB palettes - included. `JOEYLIB_SCALE` defaults to 2, and the settings are read from the - DOS environment Windows started from (`joey-run` puts its own there). - Under DOSBox, Alt+Enter is DOSBox's own full-screen key; use Alt with the + calls only what Windows 3.1 has: USER, GDI, KERNEL, and MMSYSTEM. +* *Exact colours.* The frame is 8-bit, each pixel one entry of a 256-entry + colour table holding all 16 palettes, which on a 256-colour display is a + logical palette too, so every colour is exact, SCB palettes included, as + far as the display has room: Windows keeps 20 of a 256-colour display's + entries for itself, so with more than 14 palettes in use and more than 236 + different colours on screen, some show as the nearest it has. + `JOEYLIB_SCALE` defaults to 2, and the settings are read from the DOS + environment Windows started from (`joey-run` puts its own there). Under + DOSBox, Alt+Enter is DOSBox's own full-screen key; use Alt with the keypad's Enter. -* *Sound* is 8-bit mono at 22.05 kHz through waveOut, what a Sound Blaster - and a 386 handle well; stereo pan is ignored. +* *Speed on a 386.* Sprites are compiled to x86 code, as on DOS. The port + expands the stage into the window's pixels itself, a presented row at a + time and scaled as it goes, and hands Windows only the rectangles that + changed, those close enough together as one. With Microsoft's WinG 1.0 on + the machine (its runtime in Windows' `SYSTEM` folder) the rows go straight + into a WinG bitmap the size of the window, and drawing a rectangle is a + plain copy to the screen; without it, each rectangle goes through a device + bitmap on a 256-colour display, and only another kind of display pays for + Windows matching every pixel's colour. `JOEYLIB_WING=0` leaves WinG alone. + JoeyLib does not ship WinG: a game that wants it on its players' machines + ships the runtime under Microsoft's terms (see `LICENSES.md`). While the + SCBs use 14 palettes or fewer, the frame's colours are the screen's own (an + identity palette, as WinG recommends), so a palette change, colour cycling + included, reaches the screen at once with nothing redrawn; more palettes + than that, or a display without a palette, redraw the window when colours + change. On a 386SX-16 (DOSBox's model of one), a game loop presenting + every frame runs at, in frames a second, with an Apple IIgs (under MAME) + for scale: ++ +[cols="3,1,1,1,1", options="header"] +|=== +| |2x with WinG |2x without |1:1 |IIgs +|Nothing moving, or colour cycling |60 |60 |60 |60 +|One 16x16 sprite moving |60 |60 |60 |60 +|Eight sprites moving |26 |20 |30 |20 +|Eight sprites in a column |30 |20 |30 |20 +|The whole screen changing |6 |4 |10 |8.5 +|=== ++ +The 2x window has nearly five times the IIgs's pixels to fill (640x480 for +its 320x200): a 386 keeps up with that for sprites, but not for a whole new +screen every frame, so a game that redraws everything every frame wants +`JOEYLIB_SCALE=1` there. +* *Sound* is 16-bit mono at 22.05 kHz through waveOut, what a Sound Blaster + and a 386 handle well, or 8-bit on a card whose driver has no 16-bit (a + Sound Blaster 2.0, whose SNDBLST2.DRV is 8-bit only, which makes the quiet + attenuation steps coarser); stereo pan is ignored. * *Joysticks* are the first two MMSYSTEM sticks, through `joyGetPos`, with their buttons 1 and 2. Windows 3.x reads sticks only through a joystick driver, which Windows 3.1 itself does not have; without one no stick is @@ -718,15 +1035,21 @@ which the installer puts beside the driver in `toolchains/opt/`. `make win16` builds every example as a Windows 3.x program in `build/win16/bin/`, and `./joey-run --port win16 NAME` runs one on Windows for Workgroups 3.11 in DOSBox Staging (the `win311` installer component, set -up from your own install disc); the gates run there too. None of it has run -on a real 386 here. +up from your own install disc); the gates run there too, among them +`scripts/verify-win16-display.sh`, which checks every pixel the window shows +against the stage after every present, with WinG and without (the optional +`wing` installer component: `./install.sh import WING10.EXE`, Microsoft's +WinG 1.0 SDK, then `./install.sh install wing`; the gates then install its +runtime in their Windows). None of it has run on a real 386 +here; the speeds above are DOSBox's 386SX-16 model's, and the IIgs's are +MAME's. === Haiku and BeOS Programs The Be API ports are the hosted core with a backend written to the Be API (`src/hosted/be`, C++), which Haiku reimplements, kept to the calls BeOS R5 already had. One source builds three ports: `haiku` (Haiku x86_64, gcc 13), -`haiku32` (Haiku x86, gcc 2.95, the BeOS C++ ABI) and `beos` (BeOS R5 x86, +`haiku32` (Haiku x86, gcc 2.95, the BeOS C++ ABI), and `beos` (BeOS R5 x86, built with the same gcc 2.95 against R5's own headers and libraries, which the `beosR5` installer component copies off your BeOS 5 Professional CD). Their stage is the same chunky one, so UBER's hashes are DOS's. Everything in @@ -735,25 +1058,32 @@ Their stage is the same chunky one, so UBER's hashes are DOS's. Everything in * *Threads.* The program's `main` keeps its own thread; the application and the window run on threads of their own, as every Be program's do. Key and mouse messages reach the program as events taken at `jlInputPoll`, - `jlWaitVBL` and `jlStagePresent`, and the sound comes from a + `jlWaitVBL`, and `jlStagePresent`, and the sound comes from a `BSoundPlayer`. -* *The window* shows the stage from a bitmap the app_server scales without - filtering, at the stage's shape, 4:3 or square, black around it (the same - `JOEYLIB_SCALE`, `JOEYLIB_ASPECT` and `JOEYLIB_FULLSCREEN` settings; - `JOEYLIB_SCALE` defaults to 3). Alt+Enter switches between the window and +* *The window* shows the stage scaled without filtering, at the stage's + shape, 4:3 or square, black around it (the same `JOEYLIB_SCALE`, + `JOEYLIB_ASPECT`, and `JOEYLIB_FULLSCREEN` settings; `JOEYLIB_SCALE` + defaults to 3). The port scales the frame itself and draws only what + changed since the last frame. Alt+Enter switches between the window and the whole screen. The pointer is hidden over the window. * *Keys* by raw key code, the key's place on the keyboard whatever the keymap; typed characters are the keymap's, 7-bit ASCII only. The first - two joysticks (`BJoystick`) are `JOYSTICK_0` and `JOYSTICK_1`, their first + two joysticks (`BJoystick`) are `JL_JOYSTICK_0` and `JL_JOYSTICK_1`, their first two axes and buttons. * *Files.* The program's folder is the current directory from the moment the window opens. +* *Its icon* is in its resources (<<_icons>>), but Tracker shows a file by + the icons in its attributes, which a program built on another machine does + not have. When it opens its window without them, the program copies them + across from its resources (as `mimeset -F` does), so Tracker shows it by + its icon once it has run; that is the one time a program writes to its own + file. * *No serial port.* * *BeOS R5's loader* refuses any program with a dynamic tag it does not know, so `make beos` links without combined relocations (`-z nocombreloc`). Build for R5 with `make beos`, not `make haiku32`. -`make haiku`, `make haiku32` and `make beos` build every example into +`make haiku`, `make haiku32`, and `make beos` build every example into `build//bin/`, and `./joey-run --port NAME` runs one in that port's VM under QEMU: Haiku R1/beta6 (the `haikuVm` and `haikuVm32` installer components) or BeOS R5 (`beosVm`, made from your CD), booted from a disk it @@ -761,5 +1091,6 @@ never writes to, with the program's folder on a second disk. R5 drives none of QEMU's video cards, so its VM is taken through the boot loader's menu to the fail-safe (VESA) mode, 1024x768 in 16-bit colour; it has no sound card. The gates run in the same VMs (`scripts/verify-be-input.sh`, -`scripts/bench-be.sh`). None of it has run on a real BeOS or Haiku machine -here. +`scripts/verify-be-display.sh`, `scripts/verify-be-icon.sh`, and +`scripts/bench-be.sh`). None of it has +run on a real BeOS or Haiku machine here. diff --git a/docs/ProgramLayout.adoc b/docs/ProgramLayout.adoc index c5fcf63..8a29a7d 100644 --- a/docs/ProgramLayout.adoc +++ b/docs/ProgramLayout.adoc @@ -12,6 +12,7 @@ copy. jlConfigT config; config.codegenBytes = 32UL * 1024; // sprite blitter cache +config.noAudio = false; // jlInit starts audio too if (!jlInit(&config)) { // jlLastError() explains why. There is no display yet, so on a @@ -24,22 +25,27 @@ if (!jlInit(&config)) { native sprite blitter code into. Getting it wrong is the most common cause of sprites drawing slowly, so it is worth understanding. -* `0` selects a per-platform default: 8 KB on the IIgs, 32 KB on - Amiga, ST, DOS and DVX. SDL3 interprets every sprite and never uses it. +* `0` selects a per-platform default: 8 KB on the IIgs, 64 KB on the + Amiga and ST, and 32 KB on the X68000, DOS, DVX, and Win16. The other + hosted ports interpret every sprite and never use it, though they still + allocate it. * The 68000 and x86 emitters produce code roughly four to eight times less dense than the 65816 one, so the same sprite needs far more arena space on those ports. A 32x32 sprite can need 8 to 16 KB there. * Measure, do not guess: after compiling a sprite, `jlSpriteCompiledSize()` reports its exact cost on the current platform. -* On the IIgs the arena is one Memory Manager block that may not cross a 64 KB - bank boundary, so requests above 65536 are clamped to 65536 -- and even a - clamped request can fail if no free bank exists. +* On the IIgs a compiled sprite cannot cross a 64 KB bank boundary, so the + arena is up to four Memory Manager blocks of at most 64 KB, each inside one + bank: 256 KB at most. A bank that is refused is tried again at 60 KB, and + `jlInit` takes whatever part of the request it was granted. Elsewhere the + arena is one block, and `jlInit` fails if it cannot be had. If the arena is too small, sprites still draw: they fall back to an -interpreter that is roughly *forty times slower*. Nothing reports this as an -error, so a program that never checks can be silently crawling. +interpreter that is *two to six times slower*, depending on the machine. +Nothing reports this as an error, so a program that never checks can be +silently crawling. -=== Part 2: The Stage, the Palette and Assets +=== Part 2: The Stage, the Palette, and Assets [source,c] ---- @@ -54,10 +60,10 @@ Set up colour before drawing: [source,c] ---- -uint16_t colors[SURFACE_COLORS_PER_PALETTE]; +uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; // ... fill in 16 entries of $0RGB ... jlPaletteSet(stage, 0, colors); -jlScbSetRange(stage, 0, SURFACE_HEIGHT - 1, 0); +jlScbSetRange(stage, 0, JL_SURFACE_HEIGHT - 1, 0); ---- Each surface carries *16 palettes of 16 colours* plus a 200-entry table (one @@ -71,22 +77,21 @@ Load assets after init, because sprite loading needs the codegen arena: [source,c] ---- jlSpriteT *cels[16]; -uint16_t palette[SURFACE_COLORS_PER_PALETTE]; +uint16_t palette[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t count = jlSpriteBankLoad("player.spr", cels, 16, palette); uint16_t i; for (i = 0; i < count; i++) { - jlSpritePrewarm(cels[i]); // compile NOW, not mid-frame + (void)jlSpriteCompile(cels[i]); // compile NOW, not mid-frame } ---- [IMPORTANT] ==== `jlSpriteDraw` does *not* compile a sprite on demand. An uncompiled sprite -draws through the interpreter every single time. Call `jlSpriteCompile` or -`jlSpritePrewarm` during loading -- otherwise the first draw of each sprite -either stalls a frame (if you compile lazily yourself) or never gets fast at -all. +draws through the interpreter every single time. Call `jlSpriteCompile` during +loading -- otherwise the first draw of each sprite either stalls a frame (if +you compile lazily yourself) or never gets fast at all. ==== === Part 3: The Frame Loop @@ -100,7 +105,7 @@ while (running) { jlInputPoll(); // refresh keyboard, mouse, joystick jlAudioFrameTick(); // pump the audio engine - if (jlKeyPressed(KEY_ESCAPE)) { + if (jlKeyPressed(JL_KEY_ESCAPE)) { running = false; } @@ -113,73 +118,68 @@ while (running) { Four library calls, in that order, and everything else is yours. -`jlWaitVBL()` blocks until the next vertical blank. It is cheap on every port -because the underlying mechanism is always a hardware wait, never a software -timer. +`jlWaitVBL()` blocks until the next vertical blank. On the machine ports that +is the display's own; the hosted ports, and DVX in a window, have none, and +wait for the next tick of a virtual frame clock instead. `jlInputPoll()` must be called exactly once per frame. The edge predicates (`jlKeyPressed`, `jlKeyReleased`, and the mouse and joystick equivalents) are defined relative to the *previous poll*, so calling it twice per frame makes edges disappear, and not calling it means nothing ever changes. -`jlAudioFrameTick()` is a no-op on ports whose audio runs from a hardware -interrupt, but it is required on ports that mix in user context, and it is what -advances tracker music and keeps streaming sound effects fed. Call it -unconditionally, once per frame. +`jlAudioFrameTick()` advances tracker music and keeps streaming sound effects +fed. Call it once per frame. `jlStagePresent()` flips the dirty parts of the stage to the display. The drawing primitives record which rows they touched, so presenting only copies -what actually changed. You do not need to tell it what you drew. - -[NOTE] -==== -If you write pixels by some route other than the library's primitives, the -dirty tracking does not know about it, and `jlStagePresent` will skip those -rows. That is the one case where you have to mark the region yourself. -==== +what actually changed. You do not need to tell it what you drew: every call +that changes the stage's pixels marks them, and the API has no other way to +write them. Palette and SCB changes reach the display at the next present +too. ==== Frame-Rate Independence -The targets do not share a refresh rate: 50 Hz (Amiga PAL), 55 Hz -(X68000), 60 Hz (IIgs, ST NTSC, and SDL3's virtual rate) and 70 Hz (VGA -mode 13h, and DVX's virtual rate). A loop +The targets do not share a refresh rate: 50 Hz (the Amiga, and a PAL IIgs or +ST), 55 Hz (X68000), 60 Hz (the IIgs, an NTSC ST, and every hosted port's +virtual rate), and 70 Hz (VGA mode 13h, and DVX's virtual rate). A loop that moves a sprite "two pixels per frame" runs 40 percent faster on DOS than on a PAL Amiga. -Two ways to handle it. Ask the frame rate and scale your per-frame step: +So give every speed per second, never per frame, and work out each frame how +far that is. The millisecond clock does it best. `jlMillisElapsed()` reports +true elapsed wall-clock time, so a frame that took a long time to render +moves things as far as the time it took, rather than counting as one tick: [source,c] ---- -uint16_t hz = jlFrameHz(); // 50, 60 or 70 +uint32_t now = jlMillisElapsed(); +uint32_t elapsed = now - gLastMillis; // milliseconds since the last frame + +gLastMillis = now; ---- -Or drive from the millisecond clock, which is the more robust choice for -anything that must feel the same everywhere: +A thing then moves its speed times `elapsed` / 1000. At 70 frames a second +that is often a fraction of a pixel, which only adds up if positions keep the +fraction, so keep them in fixed point (the skeleton below uses 1/256 of a +pixel) and draw at the whole-pixel part. -[source,c] ----- -uint32_t now = jlMillisElapsed(); -uint32_t step = now - previous; -previous = now; ----- - -`jlMillisElapsed()` reports true elapsed wall-clock time, so a frame that took -a long time to render is accounted for correctly rather than counted as one -tick. +`jlFrameHz()` (50 to 70) is the other way: a per-frame step of speed / +`jlFrameHz()` is right while every frame takes one refresh, and falls behind +whenever one takes longer. === Part 4: Shut Down [source,c] ---- -jlAudioShutdown(); // if you called jlAudioInit jlShutdown(); return 0; ---- -`jlShutdown` restores the display mode, frees the library's allocations, and -un-hooks interrupt vectors. Sprites and surfaces you created are released with -it, but the buffers *you* allocated (including tile data you handed to -`jlSpriteCreate`, which is never copied) remain yours to free. +`jlShutdown` stops audio, restores the display mode, frees the library's allocations, and +un-hooks interrupt vectors. Sprites and surfaces you created are *not* +released with it: destroy them with `jlSpriteDestroy` and `jlSurfaceDestroy` +before you shut down. The buffers *you* allocated (including tile data you +handed to `jlSpriteCreate`, which is never copied) remain yours to free. === The Complete Skeleton @@ -189,25 +189,33 @@ This compiles and runs on every target as-is. ---- // A minimal but complete JoeyLib program. +#include + #include -#define PLAYER_SPEED 2 +#define PLAYER_SIZE 16 +#define PLAYER_SPEED 120 // pixels per second, the same on every port +#define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel +#define MAX_STEP_MILLIS 100 // after a stall, move no further than this static jlSurfaceT *gStage; -static bool gRunning = true; -static int16_t gPlayerX = 160; -static int16_t gPlayerY = 100; +static bool gRunning = true; +static int32_t gPlayerX = 160L << SUBPIXEL_SHIFT; +static int32_t gPlayerY = 100L << SUBPIXEL_SHIFT; +static uint32_t gLastMillis = 0; -static void buildPalette(void); -static void handleInput(void); -static void renderFrame(void); +static void buildPalette(void); +static int32_t clampPosition(int32_t position, int16_t limit); +static void handleInput(int32_t step); +static void renderFrame(void); +static int32_t stepForFrame(void); static void buildPalette(void) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint8_t i; - for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = 0x000; } colors[1] = 0xFFF; // white @@ -215,58 +223,77 @@ static void buildPalette(void) { colors[3] = 0x00C; // blue jlPaletteSet(gStage, 0, colors); - jlScbSetRange(gStage, 0, SURFACE_HEIGHT - 1, 0); + jlScbSetRange(gStage, 0, JL_SURFACE_HEIGHT - 1, 0); } -static void handleInput(void) { - if (jlKeyPressed(KEY_ESCAPE)) { +// A position (in 1/256 of a pixel) kept between 0 and limit pixels. +static int32_t clampPosition(int32_t position, int16_t limit) { + int32_t max = (int32_t)limit << SUBPIXEL_SHIFT; + + if (position < 0) { + return 0; + } + if (position > max) { + return max; + } + return position; +} + + +// Moves the player by step (1/256 of a pixel) in each direction held. +static void handleInput(int32_t step) { + if (jlKeyPressed(JL_KEY_ESCAPE)) { gRunning = false; } - if (jlKeyDown(KEY_LEFT)) { - gPlayerX -= PLAYER_SPEED; + if (jlKeyDown(JL_KEY_LEFT)) { + gPlayerX -= step; } - if (jlKeyDown(KEY_RIGHT)) { - gPlayerX += PLAYER_SPEED; + if (jlKeyDown(JL_KEY_RIGHT)) { + gPlayerX += step; } - if (jlKeyDown(KEY_UP)) { - gPlayerY -= PLAYER_SPEED; + if (jlKeyDown(JL_KEY_UP)) { + gPlayerY -= step; } - if (jlKeyDown(KEY_DOWN)) { - gPlayerY += PLAYER_SPEED; + if (jlKeyDown(JL_KEY_DOWN)) { + gPlayerY += step; } // A joystick, if one is attached, drives the same movement. - if (jlJoystickConnected(JOYSTICK_0)) { - if (jlJoystickX(JOYSTICK_0) < -64) { - gPlayerX -= PLAYER_SPEED; + if (jlJoystickConnected(JL_JOYSTICK_0)) { + if (jlJoystickX(JL_JOYSTICK_0) < -64) { + gPlayerX -= step; } - if (jlJoystickX(JOYSTICK_0) > 64) { - gPlayerX += PLAYER_SPEED; + if (jlJoystickX(JL_JOYSTICK_0) > 64) { + gPlayerX += step; } } // Keep the player on screen: the primitives would clip safely, but // clamping keeps the object whole. - if (gPlayerX < 0) { - gPlayerX = 0; - } - if (gPlayerX > SURFACE_WIDTH - 16) { - gPlayerX = SURFACE_WIDTH - 16; - } - if (gPlayerY < 0) { - gPlayerY = 0; - } - if (gPlayerY > SURFACE_HEIGHT - 16) { - gPlayerY = SURFACE_HEIGHT - 16; - } + gPlayerX = clampPosition(gPlayerX, JL_SURFACE_WIDTH - PLAYER_SIZE); + gPlayerY = clampPosition(gPlayerY, JL_SURFACE_HEIGHT - PLAYER_SIZE); } static void renderFrame(void) { jlSurfaceClear(gStage, 0); - jlDrawRect(gStage, 0, 0, SURFACE_WIDTH, SURFACE_HEIGHT, 1); - jlFillRect(gStage, gPlayerX, gPlayerY, 16, 16, 2); + jlDrawRect(gStage, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, 1); + jlFillRect(gStage, (int16_t)(gPlayerX >> SUBPIXEL_SHIFT), (int16_t)(gPlayerY >> SUBPIXEL_SHIFT), PLAYER_SIZE, PLAYER_SIZE, 2); +} + + +// How far the player moves this frame, in 1/256 of a pixel: PLAYER_SPEED +// times the time since the last frame, whatever the port's frame rate. +static int32_t stepForFrame(void) { + uint32_t now = jlMillisElapsed(); + uint32_t elapsed = now - gLastMillis; + + gLastMillis = now; + if (elapsed > MAX_STEP_MILLIS) { + elapsed = MAX_STEP_MILLIS; + } + return (int32_t)((((uint32_t)PLAYER_SPEED * elapsed) << SUBPIXEL_SHIFT) / 1000u); } @@ -274,6 +301,7 @@ int main(void) { jlConfigT config; config.codegenBytes = 32UL * 1024; + config.noAudio = false; if (!jlInit(&config)) { return 1; @@ -287,13 +315,14 @@ int main(void) { buildPalette(); jlRandomSeed(12345); // deterministic on every port + gLastMillis = jlMillisElapsed(); while (gRunning) { jlWaitVBL(); jlInputPoll(); jlAudioFrameTick(); - handleInput(); + handleInput(stepForFrame()); renderFrame(); jlStagePresent(); @@ -315,7 +344,7 @@ Before drawing a sprite, capture what is underneath; next frame, put it back. [source,c] ---- -uint16_t backing[JOEY_SPRITE_BACKUP_BYTES(2, 2) / 2]; +uint16_t backing[jlSpriteBackupLength(uint16_t, 2, 2)]; jlSpriteBackupT backup; backup.bytes = (uint8_t *)backing; @@ -326,10 +355,10 @@ jlSpriteRestoreUnder(stage, &backup); // erase last frame's sprite jlSpriteSaveAndDraw(stage, sprite, x, y, &backup); ---- -Size the buffer with the `JOEY_SPRITE_BACKUP_BYTES` macro, and declare it as -`uint16_t` (or otherwise 2-byte aligned) -- an odd address pushes save and -restore onto slow interpreted paths on the planar ports, because the 68000 -cannot do word accesses at odd addresses. +Size the buffer with `jlSpriteBackupLength`, and declare it as `uint16_t` (or +otherwise 2-byte aligned): on the Amiga, ST, and X68000 an odd address sends +save and restore down a much slower path, and on the Amiga and ST it crashes +the trusted variants. ==== The Clean-Buffer Pattern @@ -341,12 +370,12 @@ jlSurfaceT *clean = jlSurfaceCreate(); // ... draw the static scene into clean, once ... // each frame: -jlSurfaceCopyRect(stage, clean, x, y, w, h); // erase EVERY window first +jlSurfaceCopyRect(stage, clean, x, y, w, h); // erase EVERY region first // ... then draw ALL sprites ... ---- This scales better with many sprites, and it is the right choice when sprites -overlap. Erase every window first, then draw every sprite -- never interleave +overlap. Erase every region first, then draw every sprite -- never interleave erase and draw. [WARNING] @@ -354,6 +383,6 @@ erase and draw. `jlSurfaceCopyRect` widens the copied region outward to the 16-pixel group grid on *every* port, so it can touch up to 15 pixels either side of the rectangle you asked for. That means it can overwrite content just outside your rectangle, -and it silently invalidates any live `jlSpriteBackupT` whose recorded window it +and it silently invalidates any live `jlSpriteBackupT` whose recorded region it touches. This is exactly why the two patterns must not be mixed in one region. ==== diff --git a/examples/adventure/adventure.c b/examples/adventure/adventure.c index d7421e9..19b1aeb 100644 --- a/examples/adventure/adventure.c +++ b/examples/adventure/adventure.c @@ -49,29 +49,26 @@ #define MSG_BAR_H 12 #define INV_BAR_H 20 -#define PLAY_AREA_H (SURFACE_HEIGHT - MSG_BAR_H - INV_BAR_H) // 168 -#define MSG_BAR_Y PLAY_AREA_H // 168 -#define INV_BAR_Y (PLAY_AREA_H + MSG_BAR_H) // 180 +#define PLAY_AREA_H (JL_SURFACE_HEIGHT - MSG_BAR_H - INV_BAR_H) // 168 +#define MSG_BAR_Y PLAY_AREA_H // 168 +#define INV_BAR_Y (PLAY_AREA_H + MSG_BAR_H) // 180 #define WALK_CELL_PX 4 -#define WALK_GRID_W (SURFACE_WIDTH / WALK_CELL_PX) // 80 -#define WALK_GRID_H (PLAY_AREA_H / WALK_CELL_PX) // 42 +#define WALK_GRID_W (JL_SURFACE_WIDTH / WALK_CELL_PX) // 80 +#define WALK_GRID_H (PLAY_AREA_H / WALK_CELL_PX) // 42 #define EGO_W 16 #define EGO_H 24 -#define EGO_TILES_X (EGO_W / 8) // 2 -#define EGO_TILES_Y (EGO_H / 8) // 3 -#define EGO_TILE_BYTES (EGO_TILES_X * EGO_TILES_Y * TILE_BYTES) // 192 -/* SaveUnder rounds x down to platform alignment: 8 px on Amiga - * planar, 2 px on chunky. Add 4 bytes/row of slack to cover the - * worst case on either. */ -#define EGO_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(EGO_W / 8, EGO_H / 8) +#define EGO_TILES_X (EGO_W / 8) // 2 +#define EGO_TILES_Y (EGO_H / 8) // 3 +#define EGO_TILE_BYTES (EGO_TILES_X * EGO_TILES_Y * JL_TILE_BYTES) // 192 +/* The save-under buffers are sized by jlSpriteBackupLength + * (joey/sprite.h): the worst case at any x, on every port. */ #define CURSOR_W 12 #define CURSOR_H 12 #define CURSOR_TILES_X 2 #define CURSOR_TILES_Y 2 -#define CURSOR_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(CURSOR_TILES_X, CURSOR_TILES_Y) #define ITEM_ICON_W 16 #define ITEM_ICON_H 16 @@ -79,8 +76,12 @@ #define MAX_HOTSPOTS 8 #define MAX_PROPS 6 #define MAX_PATH_NODES 128 -#define EGO_WALK_SPEED 1 /* pixels per frame */ -#define ANIM_TICKS 6 /* frames per walk-cycle step */ +#define EGO_WALK_SPEED 60 // pixels per second, the same on every port +#define EGO_STEP 1 // pixels advanceEgo moves the ego at a time +#define ANIM_TICKS 6 // steps per walk-cycle step +#define MAX_STEP_MILLIS 100 // after a stall, walk no further than this +#define MESSAGE_MILLIS 3000 // how long a message shows +#define MS_PER_SECOND 1000u // milliseconds in a second /* Palette layout. Palette 0 is the play-area palette; palette 1 is * the sky gradient (SCB swap by line); palette 2 is the UI palette @@ -165,14 +166,19 @@ typedef enum { } HotspotIdE; typedef struct { - HotspotIdE id; - int16_t x, y, w, h; + HotspotIdE id; + int16_t x; + int16_t y; + int16_t w; + int16_t h; const char *name; } HotspotT; typedef struct { - int16_t x, y; /* top-left of prop on screen */ - int16_t w, h; /* pixel size (for priority compare) */ + int16_t x; // top-left of prop on screen + int16_t y; + int16_t w; // pixel size (for priority compare) + int16_t h; jlSpriteT *sp; } PropT; @@ -187,9 +193,12 @@ typedef struct { PropT props[MAX_PROPS]; uint8_t propCount; - int16_t egoEnterFromE_x, egoEnterFromE_y; /* entry from east edge */ - int16_t egoEnterFromW_x, egoEnterFromW_y; /* entry from west edge */ - int16_t egoDefaultX, egoDefaultY; /* first time in room */ + int16_t egoEnterFromE_x; // entry from east edge + int16_t egoEnterFromE_y; + int16_t egoEnterFromW_x; // entry from west edge + int16_t egoEnterFromW_y; + int16_t egoDefaultX; // first time in room + int16_t egoDefaultY; } RoomT; @@ -198,48 +207,50 @@ typedef struct { * =================================================================== */ static jlSurfaceT *gScreen; -static jlSurfaceT *gBgScene[ROOM_COUNT]; /* pristine background per room */ +static jlSurfaceT *gBgScene[ROOM_COUNT]; // pristine background per room static RoomE gCurRoom; static RoomT gRooms[ROOM_COUNT]; -/* Ego sprites: [DIR][frame]. frame 0 = idle, 1 & 2 = walk cycle. */ +// Ego sprites: [DIR][frame]. frame 0 = idle, 1 & 2 = walk cycle. #define EGO_FRAMES 3 static jlSpriteT *gEgo[DIR_COUNT][EGO_FRAMES]; static uint8_t gEgoTiles[DIR_COUNT][EGO_FRAMES][EGO_TILE_BYTES]; -static uint8_t gEgoBackup[EGO_BACKUP_BYTES] __attribute__((aligned(2))); +static uint16_t gEgoBackup[jlSpriteBackupLength(uint16_t, EGO_W / 8, EGO_H / 8)]; static jlSpriteBackupT gEgoBak; -/* Cursor sprite per verb (+ item-icon for "using" mode) */ -static jlSpriteT *gCursor[VERB_COUNT]; -static jlSpriteT *gItemSprite[ITEM_COUNT]; /* index 0 = unused */ -static jlSpriteT *gLampLitSprite; /* lit-state inventory icon */ -static uint8_t gLampLitTiles[2 * 2 * TILE_BYTES]; -static uint8_t gCursorBackup[CURSOR_BACKUP_BYTES] __attribute__((aligned(2))); +// Cursor sprite per verb (+ item-icon for "using" mode) +static jlSpriteT *gCursor[VERB_COUNT]; +static jlSpriteT *gItemSprite[ITEM_COUNT]; // index 0 = unused +static jlSpriteT *gLampLitSprite; // lit-state inventory icon +static uint8_t gLampLitTiles[2 * 2 * JL_TILE_BYTES]; +static uint16_t gCursorBackup[jlSpriteBackupLength(uint16_t, CURSOR_TILES_X, CURSOR_TILES_Y)]; static jlSpriteBackupT gCursorBak; -/* Ego state */ -static int16_t gEgoX, gEgoY; /* top-left pixel of sprite */ +// Ego state +static int16_t gEgoX; // top-left pixel of sprite +static int16_t gEgoY; static DirE gEgoDir; static uint8_t gEgoFrameIdx; static uint8_t gEgoAnimTick; static bool gEgoMoving; +static uint32_t gEgoWalkCarry; // walked time not yet a step, ms x px/s -/* Path */ +// Path static int16_t gPathX[MAX_PATH_NODES]; static int16_t gPathY[MAX_PATH_NODES]; static uint16_t gPathLen; static uint16_t gPathStep; -/* UI / interaction */ +// UI / interaction static VerbE gCurVerb; -static ItemE gUsingItem; /* ITEM_NONE if not in use mode */ -static bool gHaveItem[ITEM_COUNT]; /* index 0 unused */ +static ItemE gUsingItem; // ITEM_NONE if not in use mode +static bool gHaveItem[ITEM_COUNT]; // index 0 unused static char gMessage[80]; -static uint8_t gMessageTtl; /* frames remaining */ +static uint32_t gMessageUntil; // jlMillisElapsed when it goes -/* Flags */ +// Flags static bool gLampIsLit; @@ -248,8 +259,12 @@ static bool gLampIsLit; * =================================================================== */ static int16_t clampi(int16_t v, int16_t lo, int16_t hi) { - if (v < lo) { return lo; } - if (v > hi) { return hi; } + if (v < lo) { + return lo; + } + if (v > hi) { + return hi; + } return v; } @@ -261,12 +276,12 @@ static void setMessage(const char *text) { n = sizeof(gMessage) - 1; } memcpy(gMessage, text, n); - gMessage[n] = '\0'; - gMessageTtl = 180; /* roughly 3 sec at 60 Hz */ + gMessage[n] = '\0'; + gMessageUntil = jlMillisElapsed() + MESSAGE_MILLIS; } -/* RGB444 helper. JoeyLib palette entries are 0x0RGB (4 bits each). */ +// RGB444 helper. JoeyLib palette entries are 0x0RGB (4 bits each). static uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) { return (uint16_t)(((r & 0x0F) << 8) | ((g & 0x0F) << 4) | (b & 0x0F)); } @@ -298,27 +313,49 @@ static uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) { static uint8_t pixCharToColor(char c) { switch (c) { - case ' ': case '.': return C_TRANS; - case '#': return C_BLACK; - case 'F': return C_FLESH; - case 'h': return C_BROWN; - case 'H': return C_DARK_BROWN; - case 'r': return C_RED; - case 'R': return C_DARK_BROWN; - case 'b': return C_BLUE; - case 'B': return C_DARK_GRAY; - case 'g': return C_GREEN; - case 'G': return C_DARK_GREEN; - case 'n': return C_BROWN; - case 'N': return C_DARK_BROWN; - case 'y': return C_YELLOW; - case 'Y': return C_ORANGE; - case 'w': return C_WHITE; - case 'W': return C_GRAY; - case 'T': return C_TAN; - case 'k': return C_DARK_GRAY; - case 'p': return C_PINK; - default: return C_TRANS; + case ' ': + case '.': + return C_TRANS; + case '#': + return C_BLACK; + case 'F': + return C_FLESH; + case 'h': + return C_BROWN; + case 'H': + return C_DARK_BROWN; + case 'r': + return C_RED; + case 'R': + return C_DARK_BROWN; + case 'b': + return C_BLUE; + case 'B': + return C_DARK_GRAY; + case 'g': + return C_GREEN; + case 'G': + return C_DARK_GREEN; + case 'n': + return C_BROWN; + case 'N': + return C_DARK_BROWN; + case 'y': + return C_YELLOW; + case 'Y': + return C_ORANGE; + case 'w': + return C_WHITE; + case 'W': + return C_GRAY; + case 'T': + return C_TAN; + case 'k': + return C_DARK_GRAY; + case 'p': + return C_PINK; + default: + return C_TRANS; } } @@ -328,30 +365,32 @@ static uint8_t pixCharToColor(char c) { * * Both dimensions must be multiples of 8. `rows[]` must contain * heightPx entries, each at least widthPx chars long. */ -static void authorSpriteFromRows(uint8_t *dstTiles, - uint16_t widthPx, uint16_t heightPx, - const char * const *rows) { +static void authorSpriteFromRows(uint8_t *dstTiles, uint16_t widthPx, uint16_t heightPx, const char * const *rows) { uint16_t tilesX; uint16_t tilesY; - uint16_t tx, ty; - uint16_t r, b; + uint16_t tx; + uint16_t ty; + uint16_t r; + uint16_t b; uint8_t *dst; - uint8_t pix0, pix1; - char c0, c1; + uint8_t pix0; + uint8_t pix1; + char c0; + char c1; tilesX = (uint16_t)(widthPx / 8); tilesY = (uint16_t)(heightPx / 8); for (ty = 0; ty < tilesY; ty++) { for (tx = 0; tx < tilesX; tx++) { - dst = &dstTiles[((ty * tilesX) + tx) * TILE_BYTES]; + dst = &dstTiles[((ty * tilesX) + tx) * JL_TILE_BYTES]; for (r = 0; r < 8; r++) { for (b = 0; b < 4; b++) { - /* Each byte = 2 pixels: high nibble = left. */ - c0 = rows[(ty * 8) + r][(tx * 8) + (b * 2)]; - c1 = rows[(ty * 8) + r][(tx * 8) + (b * 2) + 1]; - pix0 = pixCharToColor(c0); - pix1 = pixCharToColor(c1); + // Each byte = 2 pixels: high nibble = left. + c0 = rows[(ty * 8) + r][(tx * 8) + (b * 2)]; + c1 = rows[(ty * 8) + r][(tx * 8) + (b * 2) + 1]; + pix0 = pixCharToColor(c0); + pix1 = pixCharToColor(c1); dst[r * 4 + b] = (uint8_t)((pix0 << 4) | (pix1 & 0x0F)); } } @@ -689,7 +728,7 @@ static const char * const kEgoWalk2W[24] = { }; -/* --- Cursor sprites: 16x16, drawn at the mouse position. */ +// --- Cursor sprites: 16x16, drawn at the mouse position. static const char * const kCursorWalk[16] = { " ww ", @@ -787,7 +826,7 @@ static const char * const kCursorTalk[16] = { }; -/* --- Item-icon sprites: 16x16 for inventory bar & "using" cursor. */ +// --- Item-icon sprites: 16x16 for inventory bar & "using" cursor. static const char * const kItemLampUnlit[16] = { " ", @@ -861,20 +900,20 @@ static void drawForestBackground(jlSurfaceT *s) { /* Ground plane: dark green to lighter green band. The play area * is 168 px tall; horizon sits at y=70. */ jlSurfaceClear(s, C_DARK_GREEN); - jlFillRect(s, 0, 0, SURFACE_WIDTH, 70, C_BLUE); /* sky band */ - jlFillRect(s, 0, 70, SURFACE_WIDTH, 6, C_DARK_GREEN); /* far trees */ + jlFillRect(s, 0, 0, JL_SURFACE_WIDTH, 70, C_BLUE); // sky band + jlFillRect(s, 0, 70, JL_SURFACE_WIDTH, 6, C_DARK_GREEN); // far trees - /* Ground gradient: brighter green near the bottom suggests depth. */ + // Ground gradient: brighter green near the bottom suggests depth. for (y = 76; y < PLAY_AREA_H; y++) { if (((y - 76) & 7) == 0) { - jlDrawLine(s, 0, y, SURFACE_WIDTH - 1, y, C_GREEN); + jlDrawLine(s, 0, y, JL_SURFACE_WIDTH - 1, y, C_GREEN); } } - /* Distant tree-line silhouettes. */ + // Distant tree-line silhouettes. for (baseY = 64; baseY <= 72; baseY += 2) { int16_t x; - for (x = 0; x < SURFACE_WIDTH; x += 18) { + for (x = 0; x < JL_SURFACE_WIDTH; x += 18) { jlFillCircle(s, x, baseY, 5, C_DARK_GREEN); } } @@ -882,13 +921,13 @@ static void drawForestBackground(jlSurfaceT *s) { /* Right edge: a clearer "doorway" gap in the trees signaling the * exit to the cottage. Painted as a faint dirt path leading off * the right edge. */ - jlFillRect(s, SURFACE_WIDTH - 40, 110, 40, 14, C_TAN); - jlFillRect(s, SURFACE_WIDTH - 38, 112, 38, 10, C_BROWN); + jlFillRect(s, JL_SURFACE_WIDTH - 40, 110, 40, 14, C_TAN); + jlFillRect(s, JL_SURFACE_WIDTH - 38, 112, 38, 10, C_BROWN); - /* Decorative grass tufts on the lower band. */ + // Decorative grass tufts on the lower band. { int16_t i; - for (i = 0; i < SURFACE_WIDTH; i += 13) { + for (i = 0; i < JL_SURFACE_WIDTH; i += 13) { jlDrawLine(s, i, PLAY_AREA_H - 6, i, PLAY_AREA_H - 1, C_GREEN); jlDrawLine(s, i + 2, PLAY_AREA_H - 4, i + 2, PLAY_AREA_H - 1, C_GREEN); } @@ -896,30 +935,31 @@ static void drawForestBackground(jlSurfaceT *s) { } -/* Cottage interior: walls, floor, window, door, table, fireplace. */ +// Cottage interior: walls, floor, window, door, table, fireplace. static void drawCottageBackground(jlSurfaceT *s) { - int16_t x, y; + int16_t x; + int16_t y; jlSurfaceClear(s, C_DARK_BROWN); - /* Back wall (top half) tan, floor (bottom half) brown. */ - jlFillRect(s, 0, 0, SURFACE_WIDTH, 90, C_TAN); - jlFillRect(s, 0, 90, SURFACE_WIDTH, PLAY_AREA_H - 90, C_BROWN); + // Back wall (top half) tan, floor (bottom half) brown. + jlFillRect(s, 0, 0, JL_SURFACE_WIDTH, 90, C_TAN); + jlFillRect(s, 0, 90, JL_SURFACE_WIDTH, PLAY_AREA_H - 90, C_BROWN); - /* Wall planks. */ - for (x = 0; x < SURFACE_WIDTH; x += 24) { + // Wall planks. + for (x = 0; x < JL_SURFACE_WIDTH; x += 24) { jlDrawLine(s, x, 0, x, 88, C_DARK_BROWN); } - /* Floor planks. */ + // Floor planks. for (y = 100; y < PLAY_AREA_H; y += 10) { - jlDrawLine(s, 0, y, SURFACE_WIDTH - 1, y, C_DARK_BROWN); + jlDrawLine(s, 0, y, JL_SURFACE_WIDTH - 1, y, C_DARK_BROWN); } - /* Window with sky showing through. */ + // Window with sky showing through. jlFillRect(s, 60, 18, 50, 40, C_BLACK); jlFillRect(s, 62, 20, 46, 36, C_BLUE); - /* Window cross. */ + // Window cross. jlDrawLine(s, 85, 20, 85, 56, C_BLACK); jlDrawLine(s, 62, 38, 108, 38, C_BLACK); @@ -928,19 +968,19 @@ static void drawCottageBackground(jlSurfaceT *s) { * carries the geometry. */ jlFillRect(s, 220, 30, 70, 60, C_DARK_GRAY); jlFillRect(s, 228, 38, 54, 50, C_BLACK); - /* Logs. */ + // Logs. jlFillRect(s, 234, 78, 42, 6, C_DARK_BROWN); jlFillRect(s, 240, 72, 30, 5, C_BROWN); - /* Flame body. */ + // Flame body. jlFillCircle(s, 254, 64, 8, C_ORANGE); jlFillCircle(s, 254, 58, 6, C_YELLOW); jlFillCircle(s, 254, 54, 3, C_WHITE); - /* Door on the left edge: leads back to the forest. */ + // Door on the left edge: leads back to the forest. jlFillRect(s, 8, 60, 36, 80, C_DARK_BROWN); jlFillRect(s, 12, 64, 28, 72, C_BROWN); jlDrawLine(s, 26, 64, 26, 136, C_DARK_BROWN); - /* Doorknob. */ + // Doorknob. jlFillCircle(s, 38, 100, 2, C_YELLOW); } @@ -963,7 +1003,7 @@ static void programSkyScb(jlSurfaceT *s, RoomE room) { for (line = skyBottom; line < PLAY_AREA_H; line++) { jlScbSet(s, (uint16_t)line, PAL_SCENE); } - for (line = PLAY_AREA_H; line < SURFACE_HEIGHT; line++) { + for (line = PLAY_AREA_H; line < JL_SURFACE_HEIGHT; line++) { jlScbSet(s, (uint16_t)line, PAL_UI); } } @@ -997,7 +1037,7 @@ static void buildPalettes(jlSurfaceT *s) { ui[i] = 0; } - /* PAL_SCENE: the play-area colors that match C_* constants. */ + // PAL_SCENE: the play-area colors that match C_* constants. scene[C_BLACK] = rgb(0, 0, 0); scene[C_WHITE] = rgb(15, 15, 15); scene[C_RED] = rgb(13, 2, 2); @@ -1021,7 +1061,7 @@ static void buildPalettes(jlSurfaceT *s) { } sky[C_BLUE] = rgb(6, 10, 15); - /* PAL_UI: used on the message bar and inventory bar. */ + // PAL_UI: used on the message bar and inventory bar. ui[UI_BG] = rgb(2, 2, 3); ui[UI_INK] = rgb(15, 15, 15); ui[UI_HILITE] = rgb(15, 13, 3); @@ -1037,9 +1077,7 @@ static void buildPalettes(jlSurfaceT *s) { * Section 6 -- Sprite construction (called once at startup) * =================================================================== */ -static jlSpriteT *createSpriteFromRows(uint8_t *tilesDst, - uint16_t widthPx, uint16_t heightPx, - const char * const *rows) { +static jlSpriteT *createSpriteFromRows(uint8_t *tilesDst, uint16_t widthPx, uint16_t heightPx, const char * const *rows) { jlSpriteT *sp; authorSpriteFromRows(tilesDst, widthPx, heightPx, rows); @@ -1052,8 +1090,8 @@ static jlSpriteT *createSpriteFromRows(uint8_t *tilesDst, static bool buildAllSprites(void) { - static uint8_t cursorTiles[VERB_COUNT][CURSOR_TILES_X * CURSOR_TILES_Y * TILE_BYTES]; - static uint8_t itemTiles[ITEM_COUNT][2 * 2 * TILE_BYTES]; + static uint8_t cursorTiles[VERB_COUNT][CURSOR_TILES_X * CURSOR_TILES_Y * JL_TILE_BYTES]; + static uint8_t itemTiles[ITEM_COUNT][2 * 2 * JL_TILE_BYTES]; const char * const *egoSrc[DIR_COUNT][EGO_FRAMES] = { { kEgoIdleS, kEgoWalk1S, kEgoWalk2S }, @@ -1072,19 +1110,22 @@ static bool buildAllSprites(void) { for (d = 0; d < DIR_COUNT; d++) { for (f = 0; f < EGO_FRAMES; f++) { gEgo[d][f] = createSpriteFromRows(gEgoTiles[d][f], EGO_W, EGO_H, egoSrc[d][f]); - if (gEgo[d][f] == NULL) { return false; } + if (gEgo[d][f] == NULL) { + return false; + } } } for (v = 0; v < VERB_COUNT; v++) { gCursor[v] = createSpriteFromRows(cursorTiles[v], 16, 16, cursorSrc[v]); - if (gCursor[v] == NULL) { return false; } + if (gCursor[v] == NULL) { + return false; + } } gItemSprite[ITEM_LAMP] = createSpriteFromRows(itemTiles[ITEM_LAMP], 16, 16, kItemLampUnlit); gItemSprite[ITEM_KEY] = createSpriteFromRows(itemTiles[ITEM_KEY], 16, 16, kItemKey); gLampLitSprite = createSpriteFromRows(gLampLitTiles, 16, 16, kItemLampLit); - if (gItemSprite[ITEM_LAMP] == NULL || gItemSprite[ITEM_KEY] == NULL || - gLampLitSprite == NULL) { + if (gItemSprite[ITEM_LAMP] == NULL || gItemSprite[ITEM_KEY] == NULL || gLampLitSprite == NULL) { return false; } @@ -1102,9 +1143,9 @@ static bool buildAllSprites(void) { * =================================================================== */ #define WM_FOREST_W "############################################################" -/* The walk-mask is 80 columns wide; we author it in 80-char strings. */ +// The walk-mask is 80 columns wide; we author it in 80-char strings. static const char * const kWalkForest[WALK_GRID_H] = { - /* Sky band 0..17 (rows 0..17 = y 0..71): no-walk */ + // Sky band 0..17 (rows 0..17 = y 0..71): no-walk "################################################################################", "################################################################################", "################################################################################", @@ -1123,7 +1164,7 @@ static const char * const kWalkForest[WALK_GRID_H] = { "################################################################################", "################################################################################", "################################################################################", - /* row 18, y=72: top of walkable ground */ + // row 18, y=72: top of walkable ground "................................................................................", "................................................................................", "................................................................................", @@ -1150,7 +1191,7 @@ static const char * const kWalkForest[WALK_GRID_H] = { "................................................................................", }; -/* Cottage walkable area: bounded by walls. */ +// Cottage walkable area: bounded by walls. static const char * const kWalkCottage[WALK_GRID_H] = { "################################################################################", "################################################################################", @@ -1304,9 +1345,9 @@ static const char * const kPropLamp[LAMP_H] = { }; -static uint8_t gPropTreeTiles[(TREE_W / 8) * (TREE_H / 8) * TILE_BYTES]; -static uint8_t gPropTableTiles[(TABLE_W / 8) * (TABLE_H / 8) * TILE_BYTES]; -static uint8_t gPropLampTiles[(LAMP_W / 8) * (LAMP_H / 8) * TILE_BYTES]; +static uint8_t gPropTreeTiles[(TREE_W / 8) * (TREE_H / 8) * JL_TILE_BYTES]; +static uint8_t gPropTableTiles[(TABLE_W / 8) * (TABLE_H / 8) * JL_TILE_BYTES]; +static uint8_t gPropLampTiles[(LAMP_W / 8) * (LAMP_H / 8) * JL_TILE_BYTES]; static jlSpriteT *gSpTree; static jlSpriteT *gSpTable; @@ -1421,12 +1462,12 @@ static void resolvePropSprites(void) { * init*Room so we don't capture NULL. */ RoomT *r; - r = &gRooms[ROOM_FOREST]; + r = &gRooms[ROOM_FOREST]; r->props[0].sp = gSpTree; r->props[1].sp = gSpTree; r->props[2].sp = gSpTree; - r = &gRooms[ROOM_COTTAGE]; + r = &gRooms[ROOM_COTTAGE]; r->props[0].sp = gSpTable; r->props[1].sp = gSpLamp; } @@ -1507,10 +1548,18 @@ static __attribute__((noinline)) void bfsExpand(RoomE room, int16_t cx, int16_t nx = cx; ny = cy; switch (d) { - case 0: ny = (int16_t)(cy - 1); break; - case 1: nx = (int16_t)(cx + 1); break; - case 2: ny = (int16_t)(cy + 1); break; - default: nx = (int16_t)(cx - 1); break; + case 0: + ny = (int16_t)(cy - 1); + break; + case 1: + nx = (int16_t)(cx + 1); + break; + case 2: + ny = (int16_t)(cy + 1); + break; + default: + nx = (int16_t)(cx - 1); + break; } if (!walkable(room, nx, ny)) { return; @@ -1532,8 +1581,8 @@ static __attribute__((noinline)) bool bfsSearch(RoomE room, int16_t scx, int16_t int16_t cy; uint8_t d; - qhead = 0; - qtail = 0; + qhead = 0; + qtail = 0; gBfsQueueX[qtail] = scx; gBfsQueueY[qtail] = scy; qtail++; @@ -1567,7 +1616,7 @@ static __attribute__((noinline)) void buildPath(int16_t tcx, int16_t tcy) { int16_t cy; uint8_t d; - n = 0; + n = 0; cx = tcx; cy = tcy; while (n < MAX_PATH_NODES) { @@ -1579,10 +1628,18 @@ static __attribute__((noinline)) void buildPath(int16_t tcx, int16_t tcy) { break; } switch (d) { - case 0: cy = (int16_t)(cy + 1); break; - case 1: cx = (int16_t)(cx - 1); break; - case 2: cy = (int16_t)(cy - 1); break; - default: cx = (int16_t)(cx + 1); break; + case 0: + cy = (int16_t)(cy + 1); + break; + case 1: + cx = (int16_t)(cx - 1); + break; + case 2: + cy = (int16_t)(cy - 1); + break; + default: + cx = (int16_t)(cx + 1); + break; } } @@ -1683,33 +1740,39 @@ static void egoSetFeet(int16_t fx, int16_t fy) { static void advanceEgo(void) { - int16_t fx, fy; - int16_t tx, ty; - int16_t dx, dy; - int16_t adx, ady; + int16_t fx; + int16_t fy; + int16_t tx; + int16_t ty; + int16_t dx; + int16_t dy; + int16_t adx; + int16_t ady; int16_t step; - if (!gEgoMoving) { return; } + if (!gEgoMoving) { + return; + } if (gPathStep >= gPathLen) { gEgoMoving = false; return; } - fx = egoFeetX(); - fy = egoFeetY(); - tx = gPathX[gPathStep]; - ty = gPathY[gPathStep]; - dx = (int16_t)(tx - fx); - dy = (int16_t)(ty - fy); + fx = egoFeetX(); + fy = egoFeetY(); + tx = gPathX[gPathStep]; + ty = gPathY[gPathStep]; + dx = (int16_t)(tx - fx); + dy = (int16_t)(ty - fy); adx = (int16_t)(dx < 0 ? -dx : dx); ady = (int16_t)(dy < 0 ? -dy : dy); - if (adx <= EGO_WALK_SPEED && ady <= EGO_WALK_SPEED) { - /* Reached this waypoint. */ + if (adx <= EGO_STEP && ady <= EGO_STEP) { + // Reached this waypoint. egoSetFeet(tx, ty); gPathStep++; if (gPathStep >= gPathLen) { - gEgoMoving = false; + gEgoMoving = false; gEgoFrameIdx = 0; return; } @@ -1717,12 +1780,12 @@ static void advanceEgo(void) { } if (adx > ady) { - step = (int16_t)(dx > 0 ? EGO_WALK_SPEED : -EGO_WALK_SPEED); - fx = (int16_t)(fx + step); + step = (int16_t)(dx > 0 ? EGO_STEP : -EGO_STEP); + fx = (int16_t)(fx + step); gEgoDir = (step > 0) ? DIR_E : DIR_W; } else { - step = (int16_t)(dy > 0 ? EGO_WALK_SPEED : -EGO_WALK_SPEED); - fy = (int16_t)(fy + step); + step = (int16_t)(dy > 0 ? EGO_STEP : -EGO_STEP); + fy = (int16_t)(fy + step); gEgoDir = (step > 0) ? DIR_S : DIR_N; } egoSetFeet(fx, fy); @@ -1731,8 +1794,31 @@ static void advanceEgo(void) { if (gEgoAnimTick >= ANIM_TICKS) { gEgoAnimTick = 0; gEgoFrameIdx++; - if (gEgoFrameIdx >= EGO_FRAMES) { gEgoFrameIdx = 1; } - if (gEgoFrameIdx == 0) { gEgoFrameIdx = 1; } + if (gEgoFrameIdx >= EGO_FRAMES) { + gEgoFrameIdx = 1; + } + if (gEgoFrameIdx == 0) { + gEgoFrameIdx = 1; + } + } +} + + +// Walks the ego as far as EGO_WALK_SPEED takes it in `elapsed` +// milliseconds, EGO_STEP pixels at a time; what is left over, short of a +// step, carries into the next frame. +static void walkEgo(uint32_t elapsed) { + uint32_t steps; + + if (!gEgoMoving) { + gEgoWalkCarry = 0; + return; + } + gEgoWalkCarry += elapsed * EGO_WALK_SPEED; + steps = gEgoWalkCarry / (MS_PER_SECOND * EGO_STEP); + gEgoWalkCarry %= MS_PER_SECOND * EGO_STEP; + for (; steps > 0; steps--) { + advanceEgo(); } } @@ -1743,12 +1829,18 @@ static void advanceEgo(void) { static const char *verbName(VerbE v) { switch (v) { - case VERB_WALK: return "Walk"; - case VERB_LOOK: return "Look"; - case VERB_TAKE: return "Take"; - case VERB_USE: return "Use"; - case VERB_TALK: return "Talk"; - default: return "?"; + case VERB_WALK: + return "Walk"; + case VERB_LOOK: + return "Look"; + case VERB_TAKE: + return "Take"; + case VERB_USE: + return "Use"; + case VERB_TALK: + return "Talk"; + default: + return "?"; } } @@ -1759,14 +1851,15 @@ static int hotspotAt(int16_t mx, int16_t my) { const RoomT *r; uint8_t i; - if (my >= PLAY_AREA_H) { return -1; } + if (my >= PLAY_AREA_H) { + return -1; + } r = &gRooms[gCurRoom]; for (i = 0; i < r->hotspotCount; i++) { const HotspotT *h; h = &r->hotspots[i]; - if (mx >= h->x && mx < (h->x + h->w) && - my >= h->y && my < (h->y + h->h)) { + if (mx >= h->x && mx < (h->x + h->w) && my >= h->y && my < (h->y + h->h)) { return (int)i; } } @@ -1790,66 +1883,90 @@ static int hotspotAt(int16_t mx, int16_t my) { #define FONT_H 7 static const uint8_t kFontGlyph[][FONT_H] = { - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 0 space */ - { 0x70, 0x88, 0x98, 0xA8, 0xC8, 0x88, 0x70 }, /* 1 '0' */ - { 0x20, 0x60, 0x20, 0x20, 0x20, 0x20, 0x70 }, /* 2 '1' */ - { 0x70, 0x88, 0x08, 0x10, 0x20, 0x40, 0xF8 }, /* 3 '2' */ - { 0x70, 0x88, 0x08, 0x30, 0x08, 0x88, 0x70 }, /* 4 '3' */ - { 0x10, 0x30, 0x50, 0x90, 0xF8, 0x10, 0x10 }, /* 5 '4' */ - { 0xF8, 0x80, 0xF0, 0x08, 0x08, 0x88, 0x70 }, /* 6 '5' */ - { 0x30, 0x40, 0x80, 0xF0, 0x88, 0x88, 0x70 }, /* 7 '6' */ - { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x40, 0x40 }, /* 8 '7' */ - { 0x70, 0x88, 0x88, 0x70, 0x88, 0x88, 0x70 }, /* 9 '8' */ - { 0x70, 0x88, 0x88, 0x78, 0x08, 0x10, 0x60 }, /* 10 '9' */ - { 0x70, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, /* 11 'A' */ - { 0xF0, 0x88, 0x88, 0xF0, 0x88, 0x88, 0xF0 }, /* 12 'B' */ - { 0x70, 0x88, 0x80, 0x80, 0x80, 0x88, 0x70 }, /* 13 'C' */ - { 0xF0, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF0 }, /* 14 'D' */ - { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0xF8 }, /* 15 'E' */ - { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0x80 }, /* 16 'F' */ - { 0x70, 0x88, 0x80, 0xB8, 0x88, 0x88, 0x70 }, /* 17 'G' */ - { 0x88, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, /* 18 'H' */ - { 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70 }, /* 19 'I' */ - { 0x38, 0x10, 0x10, 0x10, 0x90, 0x90, 0x60 }, /* 20 'J' */ - { 0x88, 0x90, 0xA0, 0xC0, 0xA0, 0x90, 0x88 }, /* 21 'K' */ - { 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xF8 }, /* 22 'L' */ - { 0x88, 0xD8, 0xA8, 0xA8, 0x88, 0x88, 0x88 }, /* 23 'M' */ - { 0x88, 0xC8, 0xA8, 0x98, 0x88, 0x88, 0x88 }, /* 24 'N' */ - { 0x70, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, /* 25 'O' */ - { 0xF0, 0x88, 0x88, 0xF0, 0x80, 0x80, 0x80 }, /* 26 'P' */ - { 0x70, 0x88, 0x88, 0x88, 0xA8, 0x90, 0x68 }, /* 27 'Q' */ - { 0xF0, 0x88, 0x88, 0xF0, 0xA0, 0x90, 0x88 }, /* 28 'R' */ - { 0x70, 0x88, 0x80, 0x70, 0x08, 0x88, 0x70 }, /* 29 'S' */ - { 0xF8, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, /* 30 'T' */ - { 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, /* 31 'U' */ - { 0x88, 0x88, 0x88, 0x88, 0x88, 0x50, 0x20 }, /* 32 'V' */ - { 0x88, 0x88, 0x88, 0xA8, 0xA8, 0xD8, 0x88 }, /* 33 'W' */ - { 0x88, 0x88, 0x50, 0x20, 0x50, 0x88, 0x88 }, /* 34 'X' */ - { 0x88, 0x88, 0x50, 0x20, 0x20, 0x20, 0x20 }, /* 35 'Y' */ - { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x80, 0xF8 }, /* 36 'Z' */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60 }, /* 37 '.' */ - { 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x40 }, /* 38 ',' */ - { 0x00, 0x60, 0x60, 0x00, 0x60, 0x60, 0x00 }, /* 39 ':' */ - { 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x20 }, /* 40 '!' */ - { 0x70, 0x88, 0x08, 0x10, 0x20, 0x00, 0x20 }, /* 41 '?' */ - { 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00 }, /* 42 '-' */ - { 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 43 '\'' */ - { 0x50, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 44 '"' */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0 space + { 0x70, 0x88, 0x98, 0xA8, 0xC8, 0x88, 0x70 }, // 1 '0' + { 0x20, 0x60, 0x20, 0x20, 0x20, 0x20, 0x70 }, // 2 '1' + { 0x70, 0x88, 0x08, 0x10, 0x20, 0x40, 0xF8 }, // 3 '2' + { 0x70, 0x88, 0x08, 0x30, 0x08, 0x88, 0x70 }, // 4 '3' + { 0x10, 0x30, 0x50, 0x90, 0xF8, 0x10, 0x10 }, // 5 '4' + { 0xF8, 0x80, 0xF0, 0x08, 0x08, 0x88, 0x70 }, // 6 '5' + { 0x30, 0x40, 0x80, 0xF0, 0x88, 0x88, 0x70 }, // 7 '6' + { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x40, 0x40 }, // 8 '7' + { 0x70, 0x88, 0x88, 0x70, 0x88, 0x88, 0x70 }, // 9 '8' + { 0x70, 0x88, 0x88, 0x78, 0x08, 0x10, 0x60 }, // 10 '9' + { 0x70, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, // 11 'A' + { 0xF0, 0x88, 0x88, 0xF0, 0x88, 0x88, 0xF0 }, // 12 'B' + { 0x70, 0x88, 0x80, 0x80, 0x80, 0x88, 0x70 }, // 13 'C' + { 0xF0, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF0 }, // 14 'D' + { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0xF8 }, // 15 'E' + { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0x80 }, // 16 'F' + { 0x70, 0x88, 0x80, 0xB8, 0x88, 0x88, 0x70 }, // 17 'G' + { 0x88, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, // 18 'H' + { 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70 }, // 19 'I' + { 0x38, 0x10, 0x10, 0x10, 0x90, 0x90, 0x60 }, // 20 'J' + { 0x88, 0x90, 0xA0, 0xC0, 0xA0, 0x90, 0x88 }, // 21 'K' + { 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xF8 }, // 22 'L' + { 0x88, 0xD8, 0xA8, 0xA8, 0x88, 0x88, 0x88 }, // 23 'M' + { 0x88, 0xC8, 0xA8, 0x98, 0x88, 0x88, 0x88 }, // 24 'N' + { 0x70, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, // 25 'O' + { 0xF0, 0x88, 0x88, 0xF0, 0x80, 0x80, 0x80 }, // 26 'P' + { 0x70, 0x88, 0x88, 0x88, 0xA8, 0x90, 0x68 }, // 27 'Q' + { 0xF0, 0x88, 0x88, 0xF0, 0xA0, 0x90, 0x88 }, // 28 'R' + { 0x70, 0x88, 0x80, 0x70, 0x08, 0x88, 0x70 }, // 29 'S' + { 0xF8, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, // 30 'T' + { 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, // 31 'U' + { 0x88, 0x88, 0x88, 0x88, 0x88, 0x50, 0x20 }, // 32 'V' + { 0x88, 0x88, 0x88, 0xA8, 0xA8, 0xD8, 0x88 }, // 33 'W' + { 0x88, 0x88, 0x50, 0x20, 0x50, 0x88, 0x88 }, // 34 'X' + { 0x88, 0x88, 0x50, 0x20, 0x20, 0x20, 0x20 }, // 35 'Y' + { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x80, 0xF8 }, // 36 'Z' + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60 }, // 37 '.' + { 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x40 }, // 38 ',' + { 0x00, 0x60, 0x60, 0x00, 0x60, 0x60, 0x00 }, // 39 ':' + { 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x20 }, // 40 '!' + { 0x70, 0x88, 0x08, 0x10, 0x20, 0x00, 0x20 }, // 41 '?' + { 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00 }, // 42 '-' + { 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 43 '\'' + { 0x50, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 44 '"' }; static int glyphIdx(char c) { - if (c == ' ') return 0; - if (c >= '0' && c <= '9') return 1 + (c - '0'); - if (c >= 'A' && c <= 'Z') return 11 + (c - 'A'); - if (c >= 'a' && c <= 'z') return 11 + (c - 'a'); /* uppercase fallback */ - if (c == '.') return 37; - if (c == ',') return 38; - if (c == ':') return 39; - if (c == '!') return 40; - if (c == '?') return 41; - if (c == '-') return 42; - if (c == '\'') return 43; - if (c == '"') return 44; + if (c == ' ') { + return 0; + } + if (c >= '0' && c <= '9') { + return 1 + (c - '0'); + } + if (c >= 'A' && c <= 'Z') { + return 11 + (c - 'A'); + } + if (c >= 'a' && c <= 'z') { + return 11 + (c - 'a'); // uppercase fallback + } + if (c == '.') { + return 37; + } + if (c == ',') { + return 38; + } + if (c == ':') { + return 39; + } + if (c == '!') { + return 40; + } + if (c == '?') { + return 41; + } + if (c == '-') { + return 42; + } + if (c == '\'') { + return 43; + } + if (c == '"') { + return 44; + } return 0; } @@ -1873,7 +1990,9 @@ static void drawTextLine(int16_t x, int16_t y, const char *text, uint8_t color) } } cx = (int16_t)(cx + FONT_W + 1); - if (cx > SURFACE_WIDTH - FONT_W) { return; } + if (cx > JL_SURFACE_WIDTH - FONT_W) { + return; + } } } @@ -1887,8 +2006,8 @@ static void drawTextLine(int16_t x, int16_t y, const char *text, uint8_t color) * =================================================================== */ static void drawMessageBar(void) { - jlFillRect(gScreen, 0, MSG_BAR_Y, SURFACE_WIDTH, MSG_BAR_H, UI_BG); - jlFillRect(gScreen, 0, MSG_BAR_Y, SURFACE_WIDTH, 1, UI_DIM); + jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, MSG_BAR_H, UI_BG); + jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); if (gMessage[0] != '\0') { drawTextLine(4, MSG_BAR_Y + 2, gMessage, UI_INK); } @@ -1900,15 +2019,15 @@ static void drawInventoryBar(void) { int n; int16_t x; - jlFillRect(gScreen, 0, INV_BAR_Y, SURFACE_WIDTH, INV_BAR_H, UI_BG); - jlFillRect(gScreen, 0, INV_BAR_Y, SURFACE_WIDTH, 1, UI_DIM); + jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, INV_BAR_H, UI_BG); + jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); - /* Verb readout at the left. */ + // Verb readout at the left. snprintf(buf, sizeof(buf), "VERB: %s", verbName(gCurVerb)); drawTextLine(4, INV_BAR_Y + 2, buf, UI_HILITE); - /* Item icons at the right. */ - x = SURFACE_WIDTH - (ITEM_ICON_W + 4); + // Item icons at the right. + x = JL_SURFACE_WIDTH - (ITEM_ICON_W + 4); n = 0; if (gHaveItem[ITEM_LAMP]) { jlSpriteT *sp; @@ -1943,25 +2062,27 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) { int16_t egoBottom; uint8_t i; - /* 1. background */ + // 1. background jlSurfaceCopy(gScreen, gBgScene[gCurRoom]); - /* 2. ego */ + // 2. ego jlSpriteDraw(gScreen, gEgo[gEgoDir][gEgoFrameIdx], gEgoX, gEgoY); - /* 3. prop priority redraw */ - r = &gRooms[gCurRoom]; + // 3. prop priority redraw + r = &gRooms[gCurRoom]; egoBottom = (int16_t)(gEgoY + EGO_H); for (i = 0; i < r->propCount; i++) { const PropT *p; p = &r->props[i]; - if (p->sp == NULL) { continue; } + if (p->sp == NULL) { + continue; + } if ((p->y + p->h) > egoBottom) { jlSpriteDraw(gScreen, p->sp, p->x, p->y); } } - /* 4. UI bars */ + // 4. UI bars drawMessageBar(); drawInventoryBar(); @@ -1969,7 +2090,8 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) { * The cursor is drawn last so it sits on top of everything. */ { jlSpriteT *cs; - int16_t cx, cy; + int16_t cx; + int16_t cy; cx = (int16_t)(cursorX - 6); cy = (int16_t)(cursorY - 6); if (gUsingItem != ITEM_NONE) { @@ -1982,7 +2104,7 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) { } } - /* 6. flush */ + // 6. flush jlStagePresent(); } @@ -1999,7 +2121,7 @@ static void enterRoom(RoomE room, bool fromEast, bool fromWest); static void doVerbOnHotspot(VerbE verb, HotspotIdE hs) { if (gUsingItem != ITEM_NONE) { - /* Item-on-hotspot combinations. */ + // Item-on-hotspot combinations. if (gUsingItem == ITEM_LAMP && hs == HS_FIRE) { if (gLampIsLit) { setMessage("The lamp is already lit."); @@ -2059,8 +2181,8 @@ static void doVerbOnHotspot(VerbE verb, HotspotIdE hs) { } gHaveItem[ITEM_LAMP] = true; setMessage("You take the small brass lamp."); - /* Remove the lamp prop and its hotspot. */ - r = &gRooms[ROOM_COTTAGE]; + // Remove the lamp prop and its hotspot. + r = &gRooms[ROOM_COTTAGE]; r->props[1].sp = NULL; { uint8_t k; @@ -2149,12 +2271,14 @@ static void handleClick(int16_t mx, int16_t my, bool leftClick, bool rightClick) return; } - if (!leftClick) { return; } + if (!leftClick) { + return; + } - /* Click on inventory bar -> "use this item next". */ + // Click on inventory bar -> "use this item next". if (my >= INV_BAR_Y) { int16_t x; - x = (int16_t)(SURFACE_WIDTH - (ITEM_ICON_W + 4)); + x = (int16_t)(JL_SURFACE_WIDTH - (ITEM_ICON_W + 4)); if (gHaveItem[ITEM_LAMP]) { if (mx >= x && mx < (x + ITEM_ICON_W)) { gUsingItem = ITEM_LAMP; @@ -2173,19 +2297,19 @@ static void handleClick(int16_t mx, int16_t my, bool leftClick, bool rightClick) return; } - /* Message bar swallows the click but does nothing. */ + // Message bar swallows the click but does nothing. if (my >= MSG_BAR_Y && my < INV_BAR_Y) { return; } - /* Inside play area. */ + // Inside play area. hs = hotspotAt(mx, my); if (hs >= 0) { HotspotIdE id; id = gRooms[gCurRoom].hotspots[hs].id; if (id != HS_NONE) { if (gCurVerb == VERB_WALK && gUsingItem == ITEM_NONE) { - /* "Walk to" hotspot = walk near it. */ + // "Walk to" hotspot = walk near it. int16_t tx; int16_t ty; tx = (int16_t)(gRooms[gCurRoom].hotspots[hs].x + (gRooms[gCurRoom].hotspots[hs].w / 2)); @@ -2219,7 +2343,7 @@ static void enterRoom(RoomE room, bool fromEast, bool fromWest) { const RoomT *r; gCurRoom = room; - r = &gRooms[room]; + r = &gRooms[room]; if (fromEast) { egoSetFeet(r->egoEnterFromE_x, r->egoEnterFromE_y); @@ -2228,10 +2352,10 @@ static void enterRoom(RoomE room, bool fromEast, bool fromWest) { } else { egoSetFeet(r->egoDefaultX, r->egoDefaultY); } - gEgoMoving = false; + gEgoMoving = false; gEgoFrameIdx = 0; - gPathLen = 0; - gPathStep = 0; + gPathLen = 0; + gPathStep = 0; programSkyScb(gScreen, room); } @@ -2266,6 +2390,9 @@ int main(void) { jlConfigT config; bool leftPressed; bool rightPressed; + uint32_t lastMillis; + uint32_t now; + uint32_t elapsed; /* Sprite codegen budget: 12 ego frames + 5 cursors + 3 props + * 2 items = 22 sprites. Amiga planar emits 8 shifts/sprite at @@ -2279,6 +2406,7 @@ int main(void) { #else config.codegenBytes = 256UL * 1024; #endif + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); @@ -2311,33 +2439,45 @@ int main(void) { return 1; } - gEgoBak.bytes = gEgoBackup; - gCursorBak.bytes = gCursorBackup; + gEgoBak.bytes = (uint8_t *)gEgoBackup; + gCursorBak.bytes = (uint8_t *)gCursorBackup; - gCurVerb = VERB_WALK; - gUsingItem = ITEM_NONE; + gCurVerb = VERB_WALK; + gUsingItem = ITEM_NONE; gMessage[0] = '\0'; - gMessageTtl = 0; enterRoom(ROOM_FOREST, false, false); setMessage("Click to walk. Space or right-click cycles verb."); + lastMillis = jlMillisElapsed(); for (;;) { int16_t mx; int16_t my; jlInputPoll(); - if (jlKeyPressed(KEY_ESCAPE)) { break; } - if (jlKeyPressed(KEY_1)) { gCurVerb = VERB_WALK; } - if (jlKeyPressed(KEY_2)) { gCurVerb = VERB_LOOK; } - if (jlKeyPressed(KEY_3)) { gCurVerb = VERB_TAKE; } - if (jlKeyPressed(KEY_4)) { gCurVerb = VERB_USE; } - if (jlKeyPressed(KEY_5)) { gCurVerb = VERB_TALK; } + if (jlKeyPressed(JL_KEY_ESCAPE)) { + break; + } + if (jlKeyPressed(JL_KEY_1)) { + gCurVerb = VERB_WALK; + } + if (jlKeyPressed(JL_KEY_2)) { + gCurVerb = VERB_LOOK; + } + if (jlKeyPressed(JL_KEY_3)) { + gCurVerb = VERB_TAKE; + } + if (jlKeyPressed(JL_KEY_4)) { + gCurVerb = VERB_USE; + } + if (jlKeyPressed(JL_KEY_5)) { + gCurVerb = VERB_TALK; + } /* SPACE cycles verb -- one-button-mouse fallback for the * IIgs, where right-click does not exist. Mirrors the * right-click behaviour: drops any in-progress item-use * mode first, then advances the verb. */ - if (jlKeyPressed(KEY_SPACE)) { + if (jlKeyPressed(JL_KEY_SPACE)) { if (gUsingItem != ITEM_NONE) { gUsingItem = ITEM_NONE; setMessage(""); @@ -2348,25 +2488,41 @@ int main(void) { mx = jlMouseX(); my = jlMouseY(); - if (mx < 0) { mx = 0; } - if (my < 0) { my = 0; } - if (mx >= SURFACE_WIDTH) { mx = SURFACE_WIDTH - 1; } - if (my >= SURFACE_HEIGHT) { my = SURFACE_HEIGHT - 1; } + if (mx < 0) { + mx = 0; + } + if (my < 0) { + my = 0; + } + if (mx >= JL_SURFACE_WIDTH) { + mx = JL_SURFACE_WIDTH - 1; + } + if (my >= JL_SURFACE_HEIGHT) { + my = JL_SURFACE_HEIGHT - 1; + } - leftPressed = jlMousePressed(MOUSE_BUTTON_LEFT); - rightPressed = jlMousePressed(MOUSE_BUTTON_RIGHT); + leftPressed = jlMousePressed(JL_MOUSE_BUTTON_LEFT); + rightPressed = jlMousePressed(JL_MOUSE_BUTTON_RIGHT); if (leftPressed || rightPressed) { handleClick(mx, my, leftPressed, rightPressed); } - advanceEgo(); + now = jlMillisElapsed(); + elapsed = now - lastMillis; + lastMillis = now; + if (elapsed > MAX_STEP_MILLIS) { + elapsed = MAX_STEP_MILLIS; + } + walkEgo(elapsed); - if (gMessageTtl > 0) { gMessageTtl--; if (gMessageTtl == 0) gMessage[0] = '\0'; } + if (gMessage[0] != '\0' && (int32_t)(now - gMessageUntil) >= 0) { + gMessage[0] = '\0'; + } composeFrame(mx, my); - gEgoX = clampi(gEgoX, 0, SURFACE_WIDTH - EGO_W); - gEgoY = clampi(gEgoY, 0, PLAY_AREA_H - EGO_H); + gEgoX = clampi(gEgoX, 0, JL_SURFACE_WIDTH - EGO_W); + gEgoY = clampi(gEgoY, 0, PLAY_AREA_H - EGO_H); } jlShutdown(); diff --git a/examples/adventure2/adventure2.c b/examples/adventure2/adventure2.c index 886d44f..5427715 100644 --- a/examples/adventure2/adventure2.c +++ b/examples/adventure2/adventure2.c @@ -85,9 +85,10 @@ #define MSG_BAR_H 12 #define INV_BAR_H 20 -#define PLAY_AREA_H (SURFACE_HEIGHT - MSG_BAR_H - INV_BAR_H) /* 168 */ +#define PLAY_AREA_H (JL_SURFACE_HEIGHT - MSG_BAR_H - INV_BAR_H) // 168 #define MSG_BAR_Y PLAY_AREA_H #define INV_BAR_Y (PLAY_AREA_H + MSG_BAR_H) +#define MESSAGE_MILLIS 3000 // how long a message shows /* Priority bands: sky (y<70) is priority 0, ground (y=70..167) is * split into 14 bands, each PRI_BAND_HEIGHT pixels tall. A picture @@ -104,15 +105,15 @@ /* Control colors. 4-bit values stored as the pixel value in the * control surface. Sierra used 16 slots but most demos only need a * handful. */ -#define CTRL_BLOCK 0 /* unwalkable */ -#define CTRL_WALK 1 /* walkable floor */ -#define CTRL_TRIGGER_LOOK 2 /* "you smell the forest" trigger zone */ +#define CTRL_BLOCK 0 // unwalkable +#define CTRL_WALK 1 // walkable floor +#define CTRL_TRIGGER_LOOK 2 // "you smell the forest" trigger zone /* Pathfinding grid -- one cell per 4x4 pixel block, queried against * the control surface (sampled at the cell centre). */ #define WALK_CELL_PX 4 -#define WALK_GRID_W (SURFACE_WIDTH / WALK_CELL_PX) /* 80 */ -#define WALK_GRID_H (PLAY_AREA_H / WALK_CELL_PX) /* 42 */ +#define WALK_GRID_W (JL_SURFACE_WIDTH / WALK_CELL_PX) // 80 +#define WALK_GRID_H (PLAY_AREA_H / WALK_CELL_PX) // 42 /* Ego dimensions and animation. Larger sprite would be more * impressive but per-pixel priority masking scales linearly with @@ -120,15 +121,15 @@ * sweet spot on the slowest target (IIgs 2.8 MHz). */ #define EGO_W 16 #define EGO_H 24 -#define EGO_FRAMES 3 /* idle + 2 walk frames */ +#define EGO_FRAMES 3 // idle + 2 walk frames -/* Palettes. */ +// Palettes. #define PAL_SCENE 0 #define PAL_DEBUG_PRI 1 #define PAL_DEBUG_CTRL 2 #define PAL_UI 3 -/* Scene palette slots (same convention as adventure.c). */ +// Scene palette slots (same convention as adventure.c). #define C_TRANS 0 #define C_BLACK 1 #define C_WHITE 2 @@ -146,7 +147,7 @@ #define C_FLESH 14 #define C_PINK 15 -/* UI palette slots. */ +// UI palette slots. #define UI_BG 1 #define UI_INK 2 #define UI_HILITE 3 @@ -180,15 +181,17 @@ typedef enum { * =================================================================== */ static jlSurfaceT *gScreen; -static jlSurfaceT *gVisual; /* room background -- "picture" buffer */ -static jlSurfaceT *gPriority; /* per-pixel priority codes */ -static jlSurfaceT *gControl; /* per-pixel control codes */ +static jlSurfaceT *gVisual; // room background -- "picture" buffer +static jlSurfaceT *gPriority; // per-pixel priority codes +static jlSurfaceT *gControl; // per-pixel control codes -static int16_t gEgoX, gEgoY; /* top-left of ego sprite */ +static int16_t gEgoX; // top-left of ego sprite +static int16_t gEgoY; static DirE gEgoDir; static uint8_t gEgoFrameIdx; static uint8_t gEgoAnimTick; static bool gEgoMoving; +static uint32_t gEgoWalkCarry; // walked time not yet a step, ms x px/s static int16_t gPathX[256]; static int16_t gPathY[256]; @@ -197,7 +200,7 @@ static uint16_t gPathStep; static VerbE gCurVerb; static char gMessage[80]; -static uint8_t gMessageTtl; +static uint32_t gMessageUntil; // jlMillisElapsed when it goes static DebugViewE gDebugView; @@ -207,25 +210,45 @@ static DebugViewE gDebugView; static uint8_t pixCharToColor(char c) { switch (c) { - case ' ': case '.': return C_TRANS; - case '#': return C_BLACK; - case 'F': return C_FLESH; - case 'h': return C_BROWN; - case 'H': return C_DARK_BROWN; - case 'r': return C_RED; - case 'b': return C_BLUE; - case 'g': return C_GREEN; - case 'G': return C_DARK_GREEN; - case 'n': return C_BROWN; - case 'N': return C_DARK_BROWN; - case 'y': return C_YELLOW; - case 'Y': return C_ORANGE; - case 'w': return C_WHITE; - case 'W': return C_GRAY; - case 'T': return C_TAN; - case 'k': return C_DARK_GRAY; - case 'p': return C_PINK; - default: return C_TRANS; + case ' ': + case '.': + return C_TRANS; + case '#': + return C_BLACK; + case 'F': + return C_FLESH; + case 'h': + return C_BROWN; + case 'H': + return C_DARK_BROWN; + case 'r': + return C_RED; + case 'b': + return C_BLUE; + case 'g': + return C_GREEN; + case 'G': + return C_DARK_GREEN; + case 'n': + return C_BROWN; + case 'N': + return C_DARK_BROWN; + case 'y': + return C_YELLOW; + case 'Y': + return C_ORANGE; + case 'w': + return C_WHITE; + case 'W': + return C_GRAY; + case 'T': + return C_TAN; + case 'k': + return C_DARK_GRAY; + case 'p': + return C_PINK; + default: + return C_TRANS; } } @@ -392,7 +415,7 @@ static const char * const * const gEgoFrames[DIR_COUNT][EGO_FRAMES] = { }; -/* Cursor sprites: 16x16 each, drawn at the mouse position. */ +// Cursor sprites: 16x16 each, drawn at the mouse position. static const char * const kCursorWalk[16] = { " ww ", " ww ", " w ww w ", " ww ww ww ", " www ww www ", "wwwwwwwwwwwwww ", @@ -488,12 +511,12 @@ static const char * const kPropGrass[GRASS_H] = { typedef struct { - int16_t baseX; /* top-left of the prop sprite on the visual screen */ + int16_t baseX; // top-left of the prop sprite on the visual screen int16_t baseY; int16_t w; int16_t h; const char * const *rows; - bool blocksWalk; /* if false, only trunk-class chars block */ + bool blocksWalk; // if false, only trunk-class chars block } PropDef; /* Five trees, three grass tufts, two rocks. Base Ys span the ground @@ -508,7 +531,7 @@ static const PropDef kProps[] = { /* Rocks: every non-transparent pixel blocks. */ { 100, 138, ROCK_W, ROCK_H, kPropRock, true }, { 200, 150, ROCK_W, ROCK_H, kPropRock, true }, - /* Grass tufts: never block (the actor walks through them). */ + // Grass tufts: never block (the actor walks through them). { 40, 152, GRASS_W, GRASS_H, kPropGrass, false }, { 180, 158, GRASS_W, GRASS_H, kPropGrass, false }, { 260, 152, GRASS_W, GRASS_H, kPropGrass, false }, @@ -525,58 +548,74 @@ static const PropDef kProps[] = { #define FONT_H 7 static const uint8_t kFontGlyph[][FONT_H] = { - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 0 space */ - { 0x70, 0x88, 0x98, 0xA8, 0xC8, 0x88, 0x70 }, /* 1 '0' */ - { 0x20, 0x60, 0x20, 0x20, 0x20, 0x20, 0x70 }, /* 2 '1' */ - { 0x70, 0x88, 0x08, 0x10, 0x20, 0x40, 0xF8 }, /* 3 '2' */ - { 0x70, 0x88, 0x08, 0x30, 0x08, 0x88, 0x70 }, /* 4 '3' */ - { 0x10, 0x30, 0x50, 0x90, 0xF8, 0x10, 0x10 }, /* 5 '4' */ - { 0xF8, 0x80, 0xF0, 0x08, 0x08, 0x88, 0x70 }, /* 6 '5' */ - { 0x30, 0x40, 0x80, 0xF0, 0x88, 0x88, 0x70 }, /* 7 '6' */ - { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x40, 0x40 }, /* 8 '7' */ - { 0x70, 0x88, 0x88, 0x70, 0x88, 0x88, 0x70 }, /* 9 '8' */ - { 0x70, 0x88, 0x88, 0x78, 0x08, 0x10, 0x60 }, /* 10 '9' */ - { 0x70, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, /* 11 'A' */ - { 0xF0, 0x88, 0x88, 0xF0, 0x88, 0x88, 0xF0 }, /* 12 'B' */ - { 0x70, 0x88, 0x80, 0x80, 0x80, 0x88, 0x70 }, /* 13 'C' */ - { 0xF0, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF0 }, /* 14 'D' */ - { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0xF8 }, /* 15 'E' */ - { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0x80 }, /* 16 'F' */ - { 0x70, 0x88, 0x80, 0xB8, 0x88, 0x88, 0x70 }, /* 17 'G' */ - { 0x88, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, /* 18 'H' */ - { 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70 }, /* 19 'I' */ - { 0x38, 0x10, 0x10, 0x10, 0x90, 0x90, 0x60 }, /* 20 'J' */ - { 0x88, 0x90, 0xA0, 0xC0, 0xA0, 0x90, 0x88 }, /* 21 'K' */ - { 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xF8 }, /* 22 'L' */ - { 0x88, 0xD8, 0xA8, 0xA8, 0x88, 0x88, 0x88 }, /* 23 'M' */ - { 0x88, 0xC8, 0xA8, 0x98, 0x88, 0x88, 0x88 }, /* 24 'N' */ - { 0x70, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, /* 25 'O' */ - { 0xF0, 0x88, 0x88, 0xF0, 0x80, 0x80, 0x80 }, /* 26 'P' */ - { 0x70, 0x88, 0x88, 0x88, 0xA8, 0x90, 0x68 }, /* 27 'Q' */ - { 0xF0, 0x88, 0x88, 0xF0, 0xA0, 0x90, 0x88 }, /* 28 'R' */ - { 0x70, 0x88, 0x80, 0x70, 0x08, 0x88, 0x70 }, /* 29 'S' */ - { 0xF8, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, /* 30 'T' */ - { 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, /* 31 'U' */ - { 0x88, 0x88, 0x88, 0x88, 0x88, 0x50, 0x20 }, /* 32 'V' */ - { 0x88, 0x88, 0x88, 0xA8, 0xA8, 0xD8, 0x88 }, /* 33 'W' */ - { 0x88, 0x88, 0x50, 0x20, 0x50, 0x88, 0x88 }, /* 34 'X' */ - { 0x88, 0x88, 0x50, 0x20, 0x20, 0x20, 0x20 }, /* 35 'Y' */ - { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x80, 0xF8 }, /* 36 'Z' */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60 }, /* 37 '.' */ - { 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x40 }, /* 38 ',' */ - { 0x00, 0x60, 0x60, 0x00, 0x60, 0x60, 0x00 }, /* 39 ':' */ - { 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x20 }, /* 40 '!' */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0 space + { 0x70, 0x88, 0x98, 0xA8, 0xC8, 0x88, 0x70 }, // 1 '0' + { 0x20, 0x60, 0x20, 0x20, 0x20, 0x20, 0x70 }, // 2 '1' + { 0x70, 0x88, 0x08, 0x10, 0x20, 0x40, 0xF8 }, // 3 '2' + { 0x70, 0x88, 0x08, 0x30, 0x08, 0x88, 0x70 }, // 4 '3' + { 0x10, 0x30, 0x50, 0x90, 0xF8, 0x10, 0x10 }, // 5 '4' + { 0xF8, 0x80, 0xF0, 0x08, 0x08, 0x88, 0x70 }, // 6 '5' + { 0x30, 0x40, 0x80, 0xF0, 0x88, 0x88, 0x70 }, // 7 '6' + { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x40, 0x40 }, // 8 '7' + { 0x70, 0x88, 0x88, 0x70, 0x88, 0x88, 0x70 }, // 9 '8' + { 0x70, 0x88, 0x88, 0x78, 0x08, 0x10, 0x60 }, // 10 '9' + { 0x70, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, // 11 'A' + { 0xF0, 0x88, 0x88, 0xF0, 0x88, 0x88, 0xF0 }, // 12 'B' + { 0x70, 0x88, 0x80, 0x80, 0x80, 0x88, 0x70 }, // 13 'C' + { 0xF0, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF0 }, // 14 'D' + { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0xF8 }, // 15 'E' + { 0xF8, 0x80, 0x80, 0xF0, 0x80, 0x80, 0x80 }, // 16 'F' + { 0x70, 0x88, 0x80, 0xB8, 0x88, 0x88, 0x70 }, // 17 'G' + { 0x88, 0x88, 0x88, 0xF8, 0x88, 0x88, 0x88 }, // 18 'H' + { 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70 }, // 19 'I' + { 0x38, 0x10, 0x10, 0x10, 0x90, 0x90, 0x60 }, // 20 'J' + { 0x88, 0x90, 0xA0, 0xC0, 0xA0, 0x90, 0x88 }, // 21 'K' + { 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xF8 }, // 22 'L' + { 0x88, 0xD8, 0xA8, 0xA8, 0x88, 0x88, 0x88 }, // 23 'M' + { 0x88, 0xC8, 0xA8, 0x98, 0x88, 0x88, 0x88 }, // 24 'N' + { 0x70, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, // 25 'O' + { 0xF0, 0x88, 0x88, 0xF0, 0x80, 0x80, 0x80 }, // 26 'P' + { 0x70, 0x88, 0x88, 0x88, 0xA8, 0x90, 0x68 }, // 27 'Q' + { 0xF0, 0x88, 0x88, 0xF0, 0xA0, 0x90, 0x88 }, // 28 'R' + { 0x70, 0x88, 0x80, 0x70, 0x08, 0x88, 0x70 }, // 29 'S' + { 0xF8, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, // 30 'T' + { 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x70 }, // 31 'U' + { 0x88, 0x88, 0x88, 0x88, 0x88, 0x50, 0x20 }, // 32 'V' + { 0x88, 0x88, 0x88, 0xA8, 0xA8, 0xD8, 0x88 }, // 33 'W' + { 0x88, 0x88, 0x50, 0x20, 0x50, 0x88, 0x88 }, // 34 'X' + { 0x88, 0x88, 0x50, 0x20, 0x20, 0x20, 0x20 }, // 35 'Y' + { 0xF8, 0x08, 0x10, 0x20, 0x40, 0x80, 0xF8 }, // 36 'Z' + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60 }, // 37 '.' + { 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x40 }, // 38 ',' + { 0x00, 0x60, 0x60, 0x00, 0x60, 0x60, 0x00 }, // 39 ':' + { 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x20 }, // 40 '!' }; static int glyphIdx(char c) { - if (c == ' ') return 0; - if (c >= '0' && c <= '9') return 1 + (c - '0'); - if (c >= 'A' && c <= 'Z') return 11 + (c - 'A'); - if (c >= 'a' && c <= 'z') return 11 + (c - 'a'); - if (c == '.') return 37; - if (c == ',') return 38; - if (c == ':') return 39; - if (c == '!') return 40; + if (c == ' ') { + return 0; + } + if (c >= '0' && c <= '9') { + return 1 + (c - '0'); + } + if (c >= 'A' && c <= 'Z') { + return 11 + (c - 'A'); + } + if (c >= 'a' && c <= 'z') { + return 11 + (c - 'a'); + } + if (c == '.') { + return 37; + } + if (c == ',') { + return 38; + } + if (c == ':') { + return 39; + } + if (c == '!') { + return 40; + } return 0; } @@ -600,7 +639,9 @@ static void drawText(int16_t x, int16_t y, const char *text, uint8_t color) { } } cx = (int16_t)(cx + FONT_W + 1); - if (cx > SURFACE_WIDTH - FONT_W) { return; } + if (cx > JL_SURFACE_WIDTH - FONT_W) { + return; + } } } @@ -608,10 +649,12 @@ static void drawText(int16_t x, int16_t y, const char *text, uint8_t color) { static void setMessage(const char *text) { size_t n; n = strlen(text); - if (n >= sizeof(gMessage)) { n = sizeof(gMessage) - 1; } + if (n >= sizeof(gMessage)) { + n = sizeof(gMessage) - 1; + } memcpy(gMessage, text, n); - gMessage[n] = '\0'; - gMessageTtl = 180; + gMessage[n] = '\0'; + gMessageUntil = jlMillisElapsed() + MESSAGE_MILLIS; } @@ -633,10 +676,13 @@ static void buildPalettes(void) { uint16_t i; for (i = 0; i < 16; i++) { - scene[i] = 0; pri[i] = 0; ctl[i] = 0; ui[i] = 0; + scene[i] = 0; + pri[i] = 0; + ctl[i] = 0; + ui[i] = 0; } - /* Scene palette: matches the C_* indices above. */ + // Scene palette: matches the C_* indices above. scene[C_BLACK] = rgb(0, 0, 0); scene[C_WHITE] = rgb(15, 15, 15); scene[C_RED] = rgb(13, 2, 2); @@ -680,9 +726,11 @@ static void buildPalettes(void) { ctl[CTRL_BLOCK] = rgb(13, 1, 1); ctl[CTRL_WALK] = rgb(2, 10, 3); ctl[CTRL_TRIGGER_LOOK] = rgb(15, 14, 3); - for (i = 3; i < 16; i++) { ctl[i] = rgb(6, 6, 6); } + for (i = 3; i < 16; i++) { + ctl[i] = rgb(6, 6, 6); + } - /* UI palette: bottom message + inventory bar. */ + // UI palette: bottom message + inventory bar. ui[UI_BG] = rgb(2, 2, 3); ui[UI_INK] = rgb(15, 15, 15); ui[UI_HILITE] = rgb(15, 13, 3); @@ -704,14 +752,20 @@ static void programScbForView(DebugViewE view) { uint8_t playPal; switch (view) { - case VIEW_PRIORITY: playPal = PAL_DEBUG_PRI; break; - case VIEW_CONTROL: playPal = PAL_DEBUG_CTRL; break; - default: playPal = PAL_SCENE; break; + case VIEW_PRIORITY: + playPal = PAL_DEBUG_PRI; + break; + case VIEW_CONTROL: + playPal = PAL_DEBUG_CTRL; + break; + default: + playPal = PAL_SCENE; + break; } for (line = 0; line < PLAY_AREA_H; line++) { jlScbSet(gScreen, (uint16_t)line, playPal); } - for (line = PLAY_AREA_H; line < SURFACE_HEIGHT; line++) { + for (line = PLAY_AREA_H; line < JL_SURFACE_HEIGHT; line++) { jlScbSet(gScreen, (uint16_t)line, PAL_UI); } } @@ -736,7 +790,9 @@ static uint8_t priorityForY(int16_t y) { } band = (int16_t)((y - PRI_BAND_TOP) / PRI_BAND_HEIGHT); band++; - if (band > PRI_MAX) { band = PRI_MAX; } + if (band > PRI_MAX) { + band = PRI_MAX; + } return (uint8_t)band; } @@ -749,24 +805,24 @@ static void paintForestVisual(void) { jlSurfaceClear(gVisual, C_DARK_GREEN); - /* Sky band. */ - jlFillRect(gVisual, 0, 0, SURFACE_WIDTH, PRI_BAND_TOP, C_BLUE); + // Sky band. + jlFillRect(gVisual, 0, 0, JL_SURFACE_WIDTH, PRI_BAND_TOP, C_BLUE); - /* Far tree-line silhouette at the horizon. */ - for (i = 0; i < SURFACE_WIDTH; i += 16) { + // Far tree-line silhouette at the horizon. + for (i = 0; i < JL_SURFACE_WIDTH; i += 16) { jlFillCircle(gVisual, i, (int16_t)(PRI_BAND_TOP - 6), 6, C_DARK_GREEN); } - /* Ground gradient: brighter green stripes near the camera. */ + // Ground gradient: brighter green stripes near the camera. for (y = PRI_BAND_TOP + 4; y < PLAY_AREA_H; y += 8) { - jlDrawLine(gVisual, 0, y, SURFACE_WIDTH - 1, y, C_GREEN); + jlDrawLine(gVisual, 0, y, JL_SURFACE_WIDTH - 1, y, C_GREEN); } /* A vague dirt path that hints at the east-exit trigger zone * but does NOT correspond perfectly to it -- the trigger logic * is in the control buffer, the path is just visual flavor. */ - jlFillRect(gVisual, SURFACE_WIDTH - 30, 130, 30, 14, C_TAN); - jlFillRect(gVisual, SURFACE_WIDTH - 28, 132, 28, 10, C_BROWN); + jlFillRect(gVisual, JL_SURFACE_WIDTH - 30, 130, 30, 14, C_TAN); + jlFillRect(gVisual, JL_SURFACE_WIDTH - 28, 132, 28, 10, C_BROWN); } @@ -784,12 +840,12 @@ static void paintForestVisual(void) { static void paintForestPriority(void) { int16_t y; - /* Sky band. */ - jlFillRect(gPriority, 0, 0, SURFACE_WIDTH, PRI_BAND_TOP, PRI_SKY); + // Sky band. + jlFillRect(gPriority, 0, 0, JL_SURFACE_WIDTH, PRI_BAND_TOP, PRI_SKY); - /* Ground bands. */ + // Ground bands. for (y = PRI_BAND_TOP; y < PLAY_AREA_H; y++) { - jlDrawLine(gPriority, 0, y, SURFACE_WIDTH - 1, y, priorityForY(y)); + jlDrawLine(gPriority, 0, y, JL_SURFACE_WIDTH - 1, y, priorityForY(y)); } } @@ -805,19 +861,19 @@ static void paintForestPriority(void) { static void paintForestControl(void) { int16_t y; - jlFillRect(gControl, 0, 0, SURFACE_WIDTH, PRI_BAND_TOP, CTRL_BLOCK); - jlFillRect(gControl, 0, PRI_BAND_TOP, SURFACE_WIDTH, PLAY_AREA_H - PRI_BAND_TOP, CTRL_WALK); + jlFillRect(gControl, 0, 0, JL_SURFACE_WIDTH, PRI_BAND_TOP, CTRL_BLOCK); + jlFillRect(gControl, 0, PRI_BAND_TOP, JL_SURFACE_WIDTH, PLAY_AREA_H - PRI_BAND_TOP, CTRL_WALK); /* East-edge trigger zone: a 14 px wide vertical strip the actor * walks into to "smell the deeper forest." Painted only over the * walkable region so the actor can step into it. */ for (y = PRI_BAND_TOP; y < PLAY_AREA_H; y++) { - jlDrawLine(gControl, SURFACE_WIDTH - 14, y, SURFACE_WIDTH - 1, y, CTRL_TRIGGER_LOOK); + jlDrawLine(gControl, JL_SURFACE_WIDTH - 14, y, JL_SURFACE_WIDTH - 1, y, CTRL_TRIGGER_LOOK); } } -/* Stamp one prop into all three screens. */ +// Stamp one prop into all three screens. static void paintProp(const PropDef *p) { int16_t r; int16_t c; @@ -827,17 +883,23 @@ static void paintProp(const PropDef *p) { uint8_t color; uint8_t pri; - pri = priorityForY((int16_t)(p->baseY + p->h - 1)); /* prop priority = priority at its bottom */ + pri = priorityForY((int16_t)(p->baseY + p->h - 1)); // prop priority = priority at its bottom for (r = 0; r < p->h; r++) { py = (int16_t)(p->baseY + r); - if (py < 0 || py >= PLAY_AREA_H) { continue; } + if (py < 0 || py >= PLAY_AREA_H) { + continue; + } for (c = 0; c < p->w; c++) { - ch = p->rows[r][c]; + ch = p->rows[r][c]; color = pixCharToColor(ch); - if (color == C_TRANS) { continue; } + if (color == C_TRANS) { + continue; + } px = (int16_t)(p->baseX + c); - if (px < 0 || px >= SURFACE_WIDTH) { continue; } + if (px < 0 || px >= JL_SURFACE_WIDTH) { + continue; + } jlDrawPixel(gVisual, px, py, color); jlDrawPixel(gPriority, px, py, pri); @@ -886,31 +948,37 @@ static uint8_t actorPriority(int16_t feetY) { static void drawEgoMasked(int16_t x, int16_t y, DirE dir, uint8_t frame) { const char * const *rows; - int16_t r; - int16_t c; - int16_t px; - int16_t py; - uint8_t actorPri; - uint8_t picPri; - char ch; - uint8_t color; + int16_t r; + int16_t c; + int16_t px; + int16_t py; + uint8_t actorPri; + uint8_t picPri; + char ch; + uint8_t color; - rows = gEgoFrames[dir][frame]; - actorPri = actorPriority((int16_t)(y + EGO_H - 1)); /* feet = bottom of sprite */ + rows = gEgoFrames[dir][frame]; + actorPri = actorPriority((int16_t)(y + EGO_H - 1)); // feet = bottom of sprite for (r = 0; r < EGO_H; r++) { py = (int16_t)(y + r); - if (py < 0 || py >= PLAY_AREA_H) { continue; } + if (py < 0 || py >= PLAY_AREA_H) { + continue; + } for (c = 0; c < EGO_W_PAD; c++) { - ch = rows[r][c]; + ch = rows[r][c]; color = pixCharToColor(ch); - if (color == C_TRANS) { continue; } + if (color == C_TRANS) { + continue; + } px = (int16_t)(x + c); - if (px < 0 || px >= SURFACE_WIDTH) { continue; } + if (px < 0 || px >= JL_SURFACE_WIDTH) { + continue; + } picPri = jlSamplePixel(gPriority, px, py); if (actorPri < picPri) { - /* Picture pixel is in front. Skip the actor pixel. */ + // Picture pixel is in front. Skip the actor pixel. continue; } jlDrawPixel(gScreen, px, py, color); @@ -938,7 +1006,7 @@ static bool cellWalkable(int16_t cx, int16_t cy) { } px = (int16_t)((cx * WALK_CELL_PX) + (WALK_CELL_PX / 2)); py = (int16_t)((cy * WALK_CELL_PX) + (WALK_CELL_PX / 2)); - v = jlSamplePixel(gControl, px, py); + v = jlSamplePixel(gControl, px, py); return v != CTRL_BLOCK; } @@ -951,35 +1019,47 @@ static int16_t gBfsQueueY[WALK_GRID_H * WALK_GRID_W]; static bool findPath(int16_t sx, int16_t sy, int16_t tx, int16_t ty) { int16_t qhead; int16_t qtail; - int16_t cx, cy; - int16_t nx, ny; + int16_t cx; + int16_t cy; + int16_t nx; + int16_t ny; uint8_t d; - int16_t scx, scy, tcx, tcy; - int16_t row, col; + int16_t scx; + int16_t scy; + int16_t tcx; + int16_t tcy; + int16_t row; + int16_t col; - scx = (int16_t)(sx / WALK_CELL_PX); scy = (int16_t)(sy / WALK_CELL_PX); - tcx = (int16_t)(tx / WALK_CELL_PX); tcy = (int16_t)(ty / WALK_CELL_PX); + scx = (int16_t)(sx / WALK_CELL_PX); + scy = (int16_t)(sy / WALK_CELL_PX); + tcx = (int16_t)(tx / WALK_CELL_PX); + tcy = (int16_t)(ty / WALK_CELL_PX); /* Snap unwalkable target to nearest walkable neighbor. Keeps * "click on a tree trunk" working: the actor walks to the foot * of the tree, not into the trunk pixels. */ if (!cellWalkable(tcx, tcy)) { int16_t r; - int16_t dx, dy; + int16_t dx; + int16_t dy; bool found; found = false; for (r = 1; r < 12 && !found; r++) { for (dy = -r; dy <= r && !found; dy++) { for (dx = -r; dx <= r && !found; dx++) { if (cellWalkable((int16_t)(tcx + dx), (int16_t)(tcy + dy))) { - tcx = (int16_t)(tcx + dx); - tcy = (int16_t)(tcy + dy); + tcx = (int16_t)(tcx + dx); + tcy = (int16_t)(tcy + dy); found = true; } } } } - if (!found) { gPathLen = 0; return false; } + if (!found) { + gPathLen = 0; + return false; + } } for (row = 0; row < WALK_GRID_H; row++) { @@ -988,8 +1068,8 @@ static bool findPath(int16_t sx, int16_t sy, int16_t tx, int16_t ty) { } } - qhead = 0; - qtail = 0; + qhead = 0; + qtail = 0; gBfsQueueX[qtail] = scx; gBfsQueueY[qtail] = scy; qtail++; @@ -999,26 +1079,43 @@ static bool findPath(int16_t sx, int16_t sy, int16_t tx, int16_t ty) { cx = gBfsQueueX[qhead]; cy = gBfsQueueY[qhead]; qhead++; - if (cx == tcx && cy == tcy) { break; } + if (cx == tcx && cy == tcy) { + break; + } for (d = 0; d < 4; d++) { nx = cx; ny = cy; switch (d) { - case 0: ny = (int16_t)(cy - 1); break; - case 1: nx = (int16_t)(cx + 1); break; - case 2: ny = (int16_t)(cy + 1); break; - default: nx = (int16_t)(cx - 1); break; + case 0: + ny = (int16_t)(cy - 1); + break; + case 1: + nx = (int16_t)(cx + 1); + break; + case 2: + ny = (int16_t)(cy + 1); + break; + default: + nx = (int16_t)(cx - 1); + break; + } + if (!cellWalkable(nx, ny)) { + continue; + } + if (gBfsParent[ny][nx] != 0xFF) { + continue; } - if (!cellWalkable(nx, ny)) { continue; } - if (gBfsParent[ny][nx] != 0xFF) { continue; } gBfsParent[ny][nx] = d; - gBfsQueueX[qtail] = nx; - gBfsQueueY[qtail] = ny; + gBfsQueueX[qtail] = nx; + gBfsQueueY[qtail] = ny; qtail++; } } - if (gBfsParent[tcy][tcx] == 0xFF) { gPathLen = 0; return false; } + if (gBfsParent[tcy][tcx] == 0xFF) { + gPathLen = 0; + return false; + } { int16_t pathRevX[256]; @@ -1026,19 +1123,30 @@ static bool findPath(int16_t sx, int16_t sy, int16_t tx, int16_t ty) { int16_t n; int16_t i; - n = 0; - cx = tcx; cy = tcy; + n = 0; + cx = tcx; + cy = tcy; while (n < 256) { pathRevX[n] = cx; pathRevY[n] = cy; n++; d = gBfsParent[cy][cx]; - if (d == 0xFE) { break; } + if (d == 0xFE) { + break; + } switch (d) { - case 0: cy = (int16_t)(cy + 1); break; - case 1: cx = (int16_t)(cx - 1); break; - case 2: cy = (int16_t)(cy - 1); break; - default: cx = (int16_t)(cx + 1); break; + case 0: + cy = (int16_t)(cy + 1); + break; + case 1: + cx = (int16_t)(cx - 1); + break; + case 2: + cy = (int16_t)(cy - 1); + break; + default: + cx = (int16_t)(cx + 1); + break; } } @@ -1047,7 +1155,9 @@ static bool findPath(int16_t sx, int16_t sy, int16_t tx, int16_t ty) { gPathX[gPathLen] = (int16_t)((pathRevX[i] * WALK_CELL_PX) + (WALK_CELL_PX / 2)); gPathY[gPathLen] = (int16_t)((pathRevY[i] * WALK_CELL_PX) + (WALK_CELL_PX / 2)); gPathLen++; - if (gPathLen >= 256) { break; } + if (gPathLen >= 256) { + break; + } } gPathStep = 0; } @@ -1067,42 +1177,57 @@ static void egoSetFeet(int16_t fx, int16_t fy) { gEgoY = (int16_t)(fy - EGO_H); } -#define EGO_WALK_SPEED 1 -#define ANIM_TICKS 6 +#define EGO_WALK_SPEED 60 // pixels per second, the same on every port +#define EGO_STEP 1 // pixels advanceEgo moves the ego at a time +#define ANIM_TICKS 6 // steps per walk-cycle step +#define MAX_STEP_MILLIS 100 // after a stall, walk no further than this +#define MS_PER_SECOND 1000u // milliseconds in a second static void advanceEgo(void) { - int16_t fx, fy; - int16_t tx, ty; - int16_t dx, dy; - int16_t adx, ady; + int16_t fx; + int16_t fy; + int16_t tx; + int16_t ty; + int16_t dx; + int16_t dy; + int16_t adx; + int16_t ady; int16_t step; - if (!gEgoMoving) { return; } - if (gPathStep >= gPathLen) { gEgoMoving = false; return; } + if (!gEgoMoving) { + return; + } + if (gPathStep >= gPathLen) { + gEgoMoving = false; + return; + } - fx = egoFeetX(); fy = egoFeetY(); - tx = gPathX[gPathStep]; ty = gPathY[gPathStep]; - dx = (int16_t)(tx - fx); dy = (int16_t)(ty - fy); + fx = egoFeetX(); + fy = egoFeetY(); + tx = gPathX[gPathStep]; + ty = gPathY[gPathStep]; + dx = (int16_t)(tx - fx); + dy = (int16_t)(ty - fy); adx = (int16_t)(dx < 0 ? -dx : dx); ady = (int16_t)(dy < 0 ? -dy : dy); - if (adx <= EGO_WALK_SPEED && ady <= EGO_WALK_SPEED) { + if (adx <= EGO_STEP && ady <= EGO_STEP) { egoSetFeet(tx, ty); gPathStep++; if (gPathStep >= gPathLen) { - gEgoMoving = false; + gEgoMoving = false; gEgoFrameIdx = 0; } return; } if (adx > ady) { - step = (int16_t)(dx > 0 ? EGO_WALK_SPEED : -EGO_WALK_SPEED); - fx = (int16_t)(fx + step); + step = (int16_t)(dx > 0 ? EGO_STEP : -EGO_STEP); + fx = (int16_t)(fx + step); gEgoDir = (step > 0) ? DIR_E : DIR_W; } else { - step = (int16_t)(dy > 0 ? EGO_WALK_SPEED : -EGO_WALK_SPEED); - fy = (int16_t)(fy + step); + step = (int16_t)(dy > 0 ? EGO_STEP : -EGO_STEP); + fy = (int16_t)(fy + step); gEgoDir = (step > 0) ? DIR_S : DIR_N; } egoSetFeet(fx, fy); @@ -1111,8 +1236,31 @@ static void advanceEgo(void) { if (gEgoAnimTick >= ANIM_TICKS) { gEgoAnimTick = 0; gEgoFrameIdx++; - if (gEgoFrameIdx >= EGO_FRAMES) { gEgoFrameIdx = 1; } - if (gEgoFrameIdx == 0) { gEgoFrameIdx = 1; } + if (gEgoFrameIdx >= EGO_FRAMES) { + gEgoFrameIdx = 1; + } + if (gEgoFrameIdx == 0) { + gEgoFrameIdx = 1; + } + } +} + + +// Walks the ego as far as EGO_WALK_SPEED takes it in `elapsed` +// milliseconds, EGO_STEP pixels at a time; what is left over, short of a +// step, carries into the next frame. +static void walkEgo(uint32_t elapsed) { + uint32_t steps; + + if (!gEgoMoving) { + gEgoWalkCarry = 0; + return; + } + gEgoWalkCarry += elapsed * EGO_WALK_SPEED; + steps = gEgoWalkCarry / (MS_PER_SECOND * EGO_STEP); + gEgoWalkCarry %= MS_PER_SECOND * EGO_STEP; + for (; steps > 0; steps--) { + advanceEgo(); } } @@ -1123,7 +1271,7 @@ static void runFootTriggers(void) { uint8_t v; v = jlSamplePixel(gControl, egoFeetX(), (int16_t)(egoFeetY() - 1)); if (v == CTRL_TRIGGER_LOOK) { - if (gMessageTtl == 0) { + if (gMessage[0] == '\0') { setMessage("You smell pine and damp earth."); } } @@ -1136,10 +1284,14 @@ static void runFootTriggers(void) { static const char *verbName(VerbE v) { switch (v) { - case VERB_WALK: return "Walk"; - case VERB_LOOK: return "Look"; - case VERB_TAKE: return "Take"; - default: return "?"; + case VERB_WALK: + return "Walk"; + case VERB_LOOK: + return "Look"; + case VERB_TAKE: + return "Take"; + default: + return "?"; } } @@ -1150,20 +1302,27 @@ static const char *verbName(VerbE v) { * always on top. */ static void drawCursor(int16_t mx, int16_t my, VerbE v) { const char * const *rows; - int16_t r; - int16_t c; - int16_t px, py; - uint8_t color; + int16_t r; + int16_t c; + int16_t px; + int16_t py; + uint8_t color; rows = gCursorFrames[v]; for (r = 0; r < 16; r++) { py = (int16_t)(my - 6 + r); - if (py < 0 || py >= SURFACE_HEIGHT) { continue; } + if (py < 0 || py >= JL_SURFACE_HEIGHT) { + continue; + } for (c = 0; c < 16; c++) { px = (int16_t)(mx - 6 + c); - if (px < 0 || px >= SURFACE_WIDTH) { continue; } + if (px < 0 || px >= JL_SURFACE_WIDTH) { + continue; + } color = pixCharToColor(rows[r][c]); - if (color == C_TRANS) { continue; } + if (color == C_TRANS) { + continue; + } jlDrawPixel(gScreen, px, py, color); } } @@ -1171,8 +1330,8 @@ static void drawCursor(int16_t mx, int16_t my, VerbE v) { static void drawMessageBar(void) { - jlFillRect(gScreen, 0, MSG_BAR_Y, SURFACE_WIDTH, MSG_BAR_H, UI_BG); - jlFillRect(gScreen, 0, MSG_BAR_Y, SURFACE_WIDTH, 1, UI_DIM); + jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, MSG_BAR_H, UI_BG); + jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); if (gMessage[0] != '\0') { drawText(4, (int16_t)(MSG_BAR_Y + 2), gMessage, UI_INK); } @@ -1180,19 +1339,25 @@ static void drawMessageBar(void) { static void drawInventoryBar(void) { - char buf[40]; + char buf[40]; const char *vw; - jlFillRect(gScreen, 0, INV_BAR_Y, SURFACE_WIDTH, INV_BAR_H, UI_BG); - jlFillRect(gScreen, 0, INV_BAR_Y, SURFACE_WIDTH, 1, UI_DIM); + jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, INV_BAR_H, UI_BG); + jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM); snprintf(buf, sizeof(buf), "VERB: %s", verbName(gCurVerb)); drawText(4, (int16_t)(INV_BAR_Y + 2), buf, UI_HILITE); switch (gDebugView) { - case VIEW_PRIORITY: vw = "VIEW: PRIORITY (C ctrl, N normal)"; break; - case VIEW_CONTROL: vw = "VIEW: CONTROL (P pri, N normal)"; break; - default: vw = "VIEW: NORMAL (P pri, C ctrl)"; break; + case VIEW_PRIORITY: + vw = "VIEW: PRIORITY (C ctrl, N normal)"; + break; + case VIEW_CONTROL: + vw = "VIEW: CONTROL (P pri, N normal)"; + break; + default: + vw = "VIEW: NORMAL (P pri, C ctrl)"; + break; } drawText(4, (int16_t)(INV_BAR_Y + 11), vw, UI_INK); } @@ -1221,18 +1386,24 @@ static void composeFrameDebug(int16_t mx, int16_t my) { * with priority always-on. */ { const char * const *rows; - int16_t r; - int16_t c; - int16_t px; - int16_t py; + int16_t r; + int16_t c; + int16_t px; + int16_t py; rows = gEgoFrames[gEgoDir][gEgoFrameIdx]; for (r = 0; r < EGO_H; r++) { py = (int16_t)(gEgoY + r); - if (py < 0 || py >= PLAY_AREA_H) { continue; } + if (py < 0 || py >= PLAY_AREA_H) { + continue; + } for (c = 0; c < EGO_W_PAD; c++) { px = (int16_t)(gEgoX + c); - if (px < 0 || px >= SURFACE_WIDTH) { continue; } - if (pixCharToColor(rows[r][c]) == C_TRANS) { continue; } + if (px < 0 || px >= JL_SURFACE_WIDTH) { + continue; + } + if (pixCharToColor(rows[r][c]) == C_TRANS) { + continue; + } jlDrawPixel(gScreen, px, py, 15); } } @@ -1256,9 +1427,16 @@ static void cycleVerb(void) { static void handleClick(int16_t mx, int16_t my, bool leftClick, bool rightClick) { uint8_t ctrlHere; - if (rightClick) { cycleVerb(); return; } - if (!leftClick) { return; } - if (my >= PLAY_AREA_H) { return; } + if (rightClick) { + cycleVerb(); + return; + } + if (!leftClick) { + return; + } + if (my >= PLAY_AREA_H) { + return; + } /* Inspect the control screen under the click. Trigger codes * give the player a "look" hint without walking. */ @@ -1315,12 +1493,16 @@ int main(void) { jlConfigT config; bool leftPressed; bool rightPressed; + uint32_t lastMillis; + uint32_t now; + uint32_t elapsed; /* No compiled sprites used in this demo -- everything is plotted * pixel-by-pixel against the priority screen. Codegen budget can * be small. We need 4 surfaces (stage + visual + priority + * control). */ config.codegenBytes = 4UL * 1024; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); @@ -1352,63 +1534,89 @@ int main(void) { paintForestControl(); paintAllProps(); - /* Plant the ego near the front of the scene, idle, facing south. */ - egoSetFeet(SURFACE_WIDTH / 2, 150); + // Plant the ego near the front of the scene, idle, facing south. + egoSetFeet(JL_SURFACE_WIDTH / 2, 150); gEgoDir = DIR_S; gEgoFrameIdx = 0; gEgoAnimTick = 0; gEgoMoving = false; - gPathLen = 0; gPathStep = 0; + gPathLen = 0; + gPathStep = 0; gCurVerb = VERB_WALK; gMessage[0] = '\0'; - gMessageTtl = 0; setMessage("Click to walk. P = priority view, C = control view."); + lastMillis = jlMillisElapsed(); for (;;) { - int16_t mx, my; + int16_t mx; + int16_t my; jlInputPoll(); - if (jlKeyPressed(KEY_ESCAPE)) { break; } - if (jlKeyPressed(KEY_1)) { gCurVerb = VERB_WALK; } - if (jlKeyPressed(KEY_2)) { gCurVerb = VERB_LOOK; } - if (jlKeyPressed(KEY_3)) { gCurVerb = VERB_TAKE; } - if (jlKeyPressed(KEY_SPACE)) { cycleVerb(); } + if (jlKeyPressed(JL_KEY_ESCAPE)) { + break; + } + if (jlKeyPressed(JL_KEY_1)) { + gCurVerb = VERB_WALK; + } + if (jlKeyPressed(JL_KEY_2)) { + gCurVerb = VERB_LOOK; + } + if (jlKeyPressed(JL_KEY_3)) { + gCurVerb = VERB_TAKE; + } + if (jlKeyPressed(JL_KEY_SPACE)) { + cycleVerb(); + } /* Debug overlay toggles. Letters chosen because the IIgs's * stock Apple Keyboard II has no function keys; P/C/N work * on every target's standard keyboard. */ - if (jlKeyPressed(KEY_P)) { + if (jlKeyPressed(JL_KEY_P)) { gDebugView = (gDebugView == VIEW_PRIORITY) ? VIEW_NORMAL : VIEW_PRIORITY; programScbForView(gDebugView); } - if (jlKeyPressed(KEY_C)) { + if (jlKeyPressed(JL_KEY_C)) { gDebugView = (gDebugView == VIEW_CONTROL) ? VIEW_NORMAL : VIEW_CONTROL; programScbForView(gDebugView); } - if (jlKeyPressed(KEY_N)) { + if (jlKeyPressed(JL_KEY_N)) { gDebugView = VIEW_NORMAL; programScbForView(gDebugView); } - mx = jlMouseX(); my = jlMouseY(); - if (mx < 0) { mx = 0; } - if (my < 0) { my = 0; } - if (mx >= SURFACE_WIDTH) { mx = SURFACE_WIDTH - 1; } - if (my >= SURFACE_HEIGHT) { my = SURFACE_HEIGHT - 1; } + mx = jlMouseX(); + my = jlMouseY(); + if (mx < 0) { + mx = 0; + } + if (my < 0) { + my = 0; + } + if (mx >= JL_SURFACE_WIDTH) { + mx = JL_SURFACE_WIDTH - 1; + } + if (my >= JL_SURFACE_HEIGHT) { + my = JL_SURFACE_HEIGHT - 1; + } - leftPressed = jlMousePressed(MOUSE_BUTTON_LEFT); - rightPressed = jlMousePressed(MOUSE_BUTTON_RIGHT); + leftPressed = jlMousePressed(JL_MOUSE_BUTTON_LEFT); + rightPressed = jlMousePressed(JL_MOUSE_BUTTON_RIGHT); if (leftPressed || rightPressed) { handleClick(mx, my, leftPressed, rightPressed); } - advanceEgo(); + now = jlMillisElapsed(); + elapsed = now - lastMillis; + lastMillis = now; + if (elapsed > MAX_STEP_MILLIS) { + elapsed = MAX_STEP_MILLIS; + } + walkEgo(elapsed); runFootTriggers(); - if (gMessageTtl > 0) { - gMessageTtl--; - if (gMessageTtl == 0) { gMessage[0] = '\0'; } + if (gMessage[0] != '\0' && (int32_t)(now - gMessageUntil) >= 0) { + gMessage[0] = '\0'; } if (gDebugView == VIEW_NORMAL) { diff --git a/examples/audio/audio.c b/examples/audio/audio.c index 97db167..3968654 100644 --- a/examples/audio/audio.c +++ b/examples/audio/audio.c @@ -22,10 +22,10 @@ // keys on a timetable -- N once, S every 1.5 seconds -- for the IIgs and // X68000 audio gates, which read the sound chips but cannot type. // -// On platforms where the audio HAL is still a stub, jlAudioInit -// returns false, every audio call is a quiet no-op, and the demo runs -// as a silent input loop -- you can confirm the build links and the -// input plumbing still works without hearing anything. +// On a machine whose sound does not start (jlInit logs it), every audio +// call is a quiet no-op and the demo runs as a silent input loop -- you +// can confirm the build links and the input plumbing still works without +// hearing anything. #include #include @@ -101,7 +101,7 @@ #define BAR_X 16 #define BAR_Y 88 -#define BAR_W (SURFACE_WIDTH - 32) +#define BAR_W (JL_SURFACE_WIDTH - 32) #define BAR_H 16 // The streamed tone's state, which streamFill advances. @@ -113,7 +113,7 @@ typedef struct { static void buildPalette(jlSurfaceT *screen); -static void initialPaint(jlSurfaceT *screen, bool audioOk); +static void initialPaint(jlSurfaceT *screen); static bool keyPressed(jlKeyE key, uint16_t frame); static bool loadFile(const char *name, uint32_t maxLen, uint8_t **outBytes, uint32_t *outLen); static bool noiseStep(uint16_t sweepFrame); @@ -129,10 +129,10 @@ static uint8_t gStreamTake = 0; static void buildPalette(jlSurfaceT *screen) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t i; - for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = 0x0000; } colors[COLOR_BG] = 0x0000; @@ -146,10 +146,9 @@ static void buildPalette(jlSurfaceT *screen) { // Visual feedback for "audio is running, but you cannot tell on a // stub HAL": pulse a horizontal bar between the hint color and the // active color whenever a sound has fired this frame. -static void initialPaint(jlSurfaceT *screen, bool audioOk) { +static void initialPaint(jlSurfaceT *screen) { jlSurfaceClear(screen, COLOR_BG); - jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, - audioOk ? COLOR_HINT : COLOR_BG); + jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT); jlStagePresent(); } @@ -226,17 +225,17 @@ static bool noiseStep(uint16_t sweepFrame) { // so a gate finds the noise sweep and a stream running. static jlKeyE probeKey(uint16_t frame) { if (frame == PROBE_NOISE_FRAME) { - return KEY_N; + return JL_KEY_N; } if (frame % PROBE_STREAM_FRAMES == PROBE_STREAM_FIRST) { - return KEY_S; + return JL_KEY_S; } - return KEY_NONE; + return JL_KEY_NONE; } #else static jlKeyE probeKey(uint16_t frame) { (void)frame; - return KEY_NONE; + return JL_KEY_NONE; } #endif @@ -306,7 +305,6 @@ static void streamStart(void) { int main(void) { jlConfigT config; jlSurfaceT *screen; - bool audioOk; bool musicPaused; bool noiseOn; bool noiseResumes; @@ -319,6 +317,7 @@ int main(void) { uint32_t sfxLen; config.codegenBytes = 8 * 1024; + config.noAudio = false; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); @@ -337,19 +336,16 @@ int main(void) { modLen = 0; sfxLen = 0; - audioOk = jlAudioInit(); - if (audioOk) { - // Keep the MOD bytes resident so M can (re)start it later; - // jlAudioPlayMod copies them at play time. - (void)loadFile(TEST_MOD_NAME, DATA_FILE_MAX, &modBytes, &modLen); - (void)loadFile(TEST_SFX_NAME, DATA_FILE_MAX, &sfxBytes, &sfxLen); - gRawFormat = jlAudioSfxRawFormat(); - } + // Keep the MOD bytes resident so M can (re)start it later; + // jlAudioPlayMod copies them at play time. + (void)loadFile(TEST_MOD_NAME, DATA_FILE_MAX, &modBytes, &modLen); + (void)loadFile(TEST_SFX_NAME, DATA_FILE_MAX, &sfxBytes, &sfxLen); + gRawFormat = jlAudioSfxRawFormat(); jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true); buildPalette(screen); - jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); - initialPaint(screen, audioOk); + jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0); + initialPaint(screen); musicPaused = false; noiseOn = false; @@ -360,18 +356,18 @@ int main(void) { jlWaitVBL(); jlInputPoll(); jlAudioFrameTick(); - if (keyPressed(KEY_ESCAPE, frame)) { + if (keyPressed(JL_KEY_ESCAPE, frame)) { break; } - if (keyPressed(KEY_SPACE, frame) && sfxBytes != NULL) { + if (keyPressed(JL_KEY_SPACE, frame) && sfxBytes != NULL) { jlAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ); flashFrames = FLASH_FRAMES; } - if (keyPressed(KEY_S, frame) && audioOk) { + if (keyPressed(JL_KEY_S, frame)) { streamStart(); flashFrames = FLASH_FRAMES; } - if (keyPressed(KEY_N, frame) && !noiseOn) { + if (keyPressed(JL_KEY_N, frame) && !noiseOn) { // The song's own drums would fight the sweep for the noise // generator, so it pauses and picks up again afterwards. noiseResumes = jlMusicIsPlaying() && !musicPaused; @@ -379,15 +375,15 @@ int main(void) { noiseOn = true; noiseFrame = 0; } - if (keyPressed(KEY_M, frame) && modBytes != NULL) { + if (keyPressed(JL_KEY_M, frame) && modBytes != NULL) { jlMusicStop(); jlAudioPlayMod(modBytes, modLen, true); } - if (keyPressed(KEY_T, frame)) { + if (keyPressed(JL_KEY_T, frame)) { jlAudioStopMod(); jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true); } - if (keyPressed(KEY_P, frame)) { + if (keyPressed(JL_KEY_P, frame)) { if (musicPaused) { jlMusicResume(); } else { @@ -416,10 +412,7 @@ int main(void) { } jlMusicStop(); - if (audioOk) { - jlAudioStopMod(); - } - jlAudioShutdown(); + jlAudioStopMod(); if (modBytes != NULL) { free(modBytes); } diff --git a/examples/audpar/audpar.c b/examples/audpar/audpar.c new file mode 100644 index 0000000..2943023 --- /dev/null +++ b/examples/audpar/audpar.c @@ -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 counted +// from the first sync tone's start: +// sync a sync tone: the timeline starts and ends +// with one, so a recording can be aligned +// tone jlAudioTone +// voice jlAudioVoice +// noise jlAudioNoise +// sfx a one-shot sine sounding at +// mod start / mod end jlAudioPlayMod with loop false, and the +// first jlAudioIsPlayingMod false after it +// tick jlAudioTickRegister's result, the calls +// that came, and the time they had +// silence everything stopped +// done +// Escape ends it early. + +#include +#include +#include +#include + +#include +#include + +#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; +} diff --git a/examples/draw/draw.c b/examples/draw/draw.c index 44c28af..9f1072d 100644 --- a/examples/draw/draw.c +++ b/examples/draw/draw.c @@ -47,7 +47,7 @@ static void waitForKey(void); static void buildPalette(jlSurfaceT *screen) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; // 16 distinct $0RGB entries. Index 0 is forced to black anyway. colors[0] = 0x000; @@ -67,7 +67,7 @@ static void buildPalette(jlSurfaceT *screen) { colors[14] = 0x088; colors[15] = 0x808; jlPaletteSet(screen, 0, colors); - jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); + jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0); } @@ -112,8 +112,7 @@ static void drawPrimitivesPixelLine(jlSurfaceT *screen) { // Pixel row: 14 single-pixel writes at consecutive x to exercise // both odd and even nibble paths. for (i = 0; i < 14; i++) { - jlDrawPixel(screen, (int16_t)(10 + i * 10), (int16_t)(CELL_H - 6), - (uint8_t)((i + 1) & 0x0F)); + jlDrawPixel(screen, (int16_t)(10 + i * 10), (int16_t)(CELL_H - 6), (uint8_t)((i + 1) & 0x0F)); } } @@ -183,7 +182,7 @@ static void drawPrimitivesTileFlood(jlSurfaceT *screen) { int16_t by; int16_t i; int16_t px; - jlTileT snapBuf; + jlTileT snapBuf; ox = CELL_W; // 160 oy = CELL_H; // 100 @@ -202,8 +201,7 @@ static void drawPrimitivesTileFlood(jlSurfaceT *screen) { jlTilePaste(screen, (uint8_t)(bx + 4), (uint8_t)by, &snapBuf); // jlTileCopy from the green quadrant onto a fresh location below. - jlTileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1), - screen, (uint8_t)(bx + 1), (uint8_t)by); + jlTileCopy(screen, (uint8_t)(bx + 4), (uint8_t)(by + 1), screen, (uint8_t)(bx + 1), (uint8_t)by); // jlTileCopyMasked test: build a "transparent" striped pattern at // (208, 132). The tile's source has color 0 in alternating @@ -213,23 +211,19 @@ static void drawPrimitivesTileFlood(jlSurfaceT *screen) { // Build a vertical-stripe source at (240, 132): col-pixel = (px % 2 ? color : 0) for (i = 0; i < 8; i++) { for (px = 0; px < 16; px++) { - jlDrawPixel(screen, (int16_t)(ox + 112 + px), (int16_t)(oy + 32 + i), - (uint8_t)((px & 1) ? C_MAGENTA : 0)); + jlDrawPixel(screen, (int16_t)(ox + 112 + px), (int16_t)(oy + 32 + i), (uint8_t)((px & 1) ? C_MAGENTA : 0)); } } // jlTileCopyMasked: source at block (ox+112)/8 = 34..35, by 16 // -> dst at backdrop block (ox+80)/8 = 30..31, by 16 - jlTileCopyMasked(screen, (uint8_t)((ox + 80) / 8), (uint8_t)((oy + 32) / 8), - screen, (uint8_t)((ox + 112) / 8), (uint8_t)((oy + 32) / 8), - 0); + jlTileCopyMasked(screen, (uint8_t)((ox + 80) / 8), (uint8_t)((oy + 32) / 8), screen, (uint8_t)((ox + 112) / 8), (uint8_t)((oy + 32) / 8), 0); // Flood-fill region: a small bordered rectangle in the cell's // lower portion. Outline drawn in C_BORDER; jlFloodFillBounded // from a point inside should fill with C_CYAN, stopping at the // border. jlDrawRect(screen, ox + 16, oy + 60, 64, 32, C_BORDER); - jlFloodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76), - C_CYAN, C_BORDER); + jlFloodFillBounded(screen, (int16_t)(ox + 48), (int16_t)(oy + 76), C_CYAN, C_BORDER); // Plain jlFloodFill: solid block then re-fill to a new color. jlFillRect(screen, ox + 96, oy + 60, 48, 32, C_GREEN); @@ -243,10 +237,11 @@ static void waitForKey(void) { int main(void) { - jlConfigT config; - jlSurfaceT *screen; + jlConfigT config; + jlSurfaceT *screen; config.codegenBytes = 8 * 1024; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/hello/hello.c b/examples/hello/hello.c index 2ba99d8..4646d40 100644 --- a/examples/hello/hello.c +++ b/examples/hello/hello.c @@ -14,6 +14,7 @@ int main(void) { jlConfigT config; config.codegenBytes = 8 * 1024; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/joy/joy.c b/examples/joy/joy.c index c1ff0c5..cacc514 100644 --- a/examples/joy/joy.c +++ b/examples/joy/joy.c @@ -51,7 +51,7 @@ typedef struct { int16_t dotX; int16_t dotY; - bool btn[JOY_BUTTON_COUNT]; + bool btn[JL_JOY_BUTTON_COUNT]; bool connected; bool valid; } StickViewT; @@ -65,14 +65,14 @@ static void drawFrame(jlSurfaceT *screen, int16_t left); static void initialPaint(jlSurfaceT *screen); static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left); -static StickViewT gView[JOYSTICK_COUNT]; +static StickViewT gView[JL_JOYSTICK_COUNT]; static void buildPalette(jlSurfaceT *screen) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t i; - for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = 0x0000; } colors[COLOR_BACKGROUND] = 0x0000; @@ -104,7 +104,7 @@ static int16_t dotXFor(int16_t left, int8_t ax) { int32_t off; cx = left + FRAME_SIZE / 2 - DOT_SIZE / 2; - off = ((int32_t)ax * DOT_TRAVEL) / JOYSTICK_AXIS_MAX; + off = ((int32_t)ax * DOT_TRAVEL) / JL_JOYSTICK_AXIS_MAX; return (int16_t)(cx + off); } @@ -114,7 +114,7 @@ static int16_t dotYFor(int8_t ay) { int32_t off; cy = STICK_TOP + FRAME_SIZE / 2 - DOT_SIZE / 2; - off = ((int32_t)ay * DOT_TRAVEL) / JOYSTICK_AXIS_MAX; + off = ((int32_t)ay * DOT_TRAVEL) / JL_JOYSTICK_AXIS_MAX; return (int16_t)(cy + off); } @@ -142,7 +142,7 @@ static void initialPaint(jlSurfaceT *screen) { int16_t left; jlSurfaceClear(screen, COLOR_BACKGROUND); - for (i = 0; i < JOYSTICK_COUNT; i++) { + for (i = 0; i < JL_JOYSTICK_COUNT; i++) { left = (i == 0) ? STICK0_LEFT : STICK1_LEFT; drawFrame(screen, left); gView[i].valid = false; @@ -160,7 +160,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) { int8_t ay; int16_t newDotX; int16_t newDotY; - bool newBtn[JOY_BUTTON_COUNT]; + bool newBtn[JL_JOY_BUTTON_COUNT]; bool connected; int16_t i; int16_t bx; @@ -172,7 +172,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) { ay = jlJoystickY(js); newDotX = dotXFor(left, ax); newDotY = dotYFor(ay); - for (i = 0; i < JOY_BUTTON_COUNT; i++) { + for (i = 0; i < JL_JOY_BUTTON_COUNT; i++) { newBtn[i] = jlJoyDown(js, (jlJoyButtonE)i); } @@ -195,7 +195,7 @@ static void updateStick(jlSurfaceT *screen, jlJoystickE js, int16_t left) { // Button squares. by = BUTTON_TOP; - for (i = 0; i < JOY_BUTTON_COUNT; i++) { + for (i = 0; i < JL_JOY_BUTTON_COUNT; i++) { if (v->valid && v->btn[i] == newBtn[i]) { continue; } @@ -213,6 +213,7 @@ int main(void) { jlSurfaceT *screen; config.codegenBytes = CODEGEN_BYTES; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); @@ -227,17 +228,17 @@ int main(void) { } buildPalette(screen); - jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); + jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0); initialPaint(screen); jlInputPoll(); for (;;) { jlInputPoll(); - if (jlKeyPressed(KEY_ESCAPE)) { + if (jlKeyPressed(JL_KEY_ESCAPE)) { break; } - updateStick(screen, JOYSTICK_0, STICK0_LEFT); - updateStick(screen, JOYSTICK_1, STICK1_LEFT); + updateStick(screen, JL_JOYSTICK_0, STICK0_LEFT); + updateStick(screen, JL_JOYSTICK_1, STICK1_LEFT); } jlShutdown(); diff --git a/examples/keys/keys.c b/examples/keys/keys.c index 53f14e5..a76bb0f 100644 --- a/examples/keys/keys.c +++ b/examples/keys/keys.c @@ -111,16 +111,16 @@ static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint16_t valu static void updateCursor(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow); static void updateMouseVerifyRow(jlSurfaceT *screen); -// Keys laid out row-by-row. KEY_NONE cells stay blank. Shape roughly +// Keys laid out row-by-row. JL_KEY_NONE cells stay blank. Shape roughly // resembles a real keyboard (top number row, then QWERTY rows, then a // cluster of modifiers / arrows / function keys). static const jlKeyE gKeyGrid[GRID_ROWS][GRID_COLS] = { - { KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0 }, - { KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P }, - { KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_BACKSPACE }, - { KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_LSHIFT, KEY_RSHIFT, KEY_TAB }, - { KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_LCTRL, KEY_LALT, KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_NONE }, - { KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10 } + { JL_KEY_1, JL_KEY_2, JL_KEY_3, JL_KEY_4, JL_KEY_5, JL_KEY_6, JL_KEY_7, JL_KEY_8, JL_KEY_9, JL_KEY_0 }, + { JL_KEY_Q, JL_KEY_W, JL_KEY_E, JL_KEY_R, JL_KEY_T, JL_KEY_Y, JL_KEY_U, JL_KEY_I, JL_KEY_O, JL_KEY_P }, + { JL_KEY_A, JL_KEY_S, JL_KEY_D, JL_KEY_F, JL_KEY_G, JL_KEY_H, JL_KEY_J, JL_KEY_K, JL_KEY_L, JL_KEY_BACKSPACE }, + { JL_KEY_Z, JL_KEY_X, JL_KEY_C, JL_KEY_V, JL_KEY_B, JL_KEY_N, JL_KEY_M, JL_KEY_LSHIFT, JL_KEY_RSHIFT, JL_KEY_TAB }, + { JL_KEY_SPACE, JL_KEY_ESCAPE, JL_KEY_RETURN, JL_KEY_LCTRL, JL_KEY_LALT, JL_KEY_UP, JL_KEY_DOWN, JL_KEY_LEFT, JL_KEY_RIGHT, JL_KEY_NONE }, + { JL_KEY_F1, JL_KEY_F2, JL_KEY_F3, JL_KEY_F4, JL_KEY_F5, JL_KEY_F6, JL_KEY_F7, JL_KEY_F8, JL_KEY_F9, JL_KEY_F10 } }; static bool gCellLit[GRID_ROWS][GRID_COLS]; @@ -224,7 +224,7 @@ static const uint8_t kFontGlyph[][FONT_H] = { static void buildPalette(jlSurfaceT *screen) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t i; // Every entry gets a DISTINCT color (gray ramp for the unused @@ -232,7 +232,7 @@ static void buildPalette(jlSurfaceT *screen) { // screenshot-based harness can decode the verification row's // nibble pixels back to values on ports without direct // framebuffer reads. - for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = (uint16_t)((i << 8) | (i << 4) | i); } colors[COLOR_BACKGROUND] = 0x0000; // black @@ -271,13 +271,13 @@ static bool cellTargetLit(int16_t col, int16_t row, int16_t cursorCol, int16_t c jlKeyE key; key = gKeyGrid[row][col]; - if (key == KEY_NONE) { + if (key == JL_KEY_NONE) { return false; } if (jlKeyDown(key)) { return true; } - if (col == cursorCol && row == cursorRow && jlMouseDown(MOUSE_BUTTON_LEFT)) { + if (col == cursorCol && row == cursorRow && jlMouseDown(JL_MOUSE_BUTTON_LEFT)) { return true; } return false; @@ -346,7 +346,7 @@ static void drawTextLine(jlSurfaceT *screen, int16_t x, int16_t y, const char *t } } cx = (int16_t)(cx + FONT_W + 1); - if (cx > SURFACE_WIDTH - FONT_W) { + if (cx > JL_SURFACE_WIDTH - FONT_W) { return; } } @@ -372,23 +372,57 @@ static void drawVerifyRow(jlSurfaceT *screen) { static int glyphIdx(char c) { - if (c == ' ') { return GLYPH_SPACE; } - if (c >= '0' && c <= '9') { return GLYPH_DIGIT_BASE + (c - '0'); } - if (c >= 'A' && c <= 'Z') { return GLYPH_ALPHA_BASE + (c - 'A'); } - if (c >= 'a' && c <= 'z') { return GLYPH_ALPHA_BASE + (c - 'a'); } // uppercase fallback - if (c == '.') { return GLYPH_PERIOD; } - if (c == ',') { return GLYPH_COMMA; } - if (c == ':') { return GLYPH_COLON; } - if (c == '!') { return GLYPH_BANG; } - if (c == '?') { return GLYPH_QUESTION; } - if (c == '-') { return GLYPH_MINUS; } - if (c == '\'') { return GLYPH_APOSTROPHE; } - if (c == '"') { return GLYPH_QUOTE; } - if (c == '/') { return GLYPH_SLASH; } - if (c == ';') { return GLYPH_SEMICOLON; } - if (c == '=') { return GLYPH_EQUALS; } - if (c == '+') { return GLYPH_PLUS; } - if (c == '_') { return GLYPH_UNDERSCORE; } + if (c == ' ') { + return GLYPH_SPACE; + } + if (c >= '0' && c <= '9') { + return GLYPH_DIGIT_BASE + (c - '0'); + } + if (c >= 'A' && c <= 'Z') { + return GLYPH_ALPHA_BASE + (c - 'A'); + } + if (c >= 'a' && c <= 'z') { + return GLYPH_ALPHA_BASE + (c - 'a'); // uppercase fallback + } + if (c == '.') { + return GLYPH_PERIOD; + } + if (c == ',') { + return GLYPH_COMMA; + } + if (c == ':') { + return GLYPH_COLON; + } + if (c == '!') { + return GLYPH_BANG; + } + if (c == '?') { + return GLYPH_QUESTION; + } + if (c == '-') { + return GLYPH_MINUS; + } + if (c == '\'') { + return GLYPH_APOSTROPHE; + } + if (c == '"') { + return GLYPH_QUOTE; + } + if (c == '/') { + return GLYPH_SLASH; + } + if (c == ';') { + return GLYPH_SEMICOLON; + } + if (c == '=') { + return GLYPH_EQUALS; + } + if (c == '+') { + return GLYPH_PLUS; + } + if (c == '_') { + return GLYPH_UNDERSCORE; + } return GLYPH_UNKNOWN; } @@ -396,13 +430,13 @@ static int glyphIdx(char c) { static void initialPaint(jlSurfaceT *screen) { int16_t col; int16_t row; - jlKeyE key; + jlKeyE key; jlSurfaceClear(screen, COLOR_BACKGROUND); for (row = 0; row < GRID_ROWS; row++) { for (col = 0; col < GRID_COLS; col++) { key = gKeyGrid[row][col]; - if (key == KEY_NONE) { + if (key == JL_KEY_NONE) { continue; } drawCell(screen, col, row, false); @@ -419,9 +453,9 @@ static void initialPaint(jlSurfaceT *screen) { // when it cannot read the framebuffer (see verify-amiga-input.sh). static void logTypedHistory(void) { static const char kHex[] = "0123456789ABCDEF"; - char line[2 * MAX_RECEIVED + 8]; - int16_t i; - int16_t n; + char line[2 * MAX_RECEIVED + 8]; + int16_t i; + int16_t n; n = 0; for (i = 0; i < gReceivedCount; i++) { @@ -439,23 +473,20 @@ static void logTypedHistory(void) { // drawMouseVerifyRow stamps them. Sampled once per use so the drawn // row and updateMouseVerifyRow's change check can never disagree. static uint16_t mouseFlags(void) { - return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0) | - (jlMouseDown(MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0) | - (jlMouseDown(MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0) | - (jlMouseDown(MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0)); + return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0) | (jlMouseDown(JL_MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0) | (jlMouseDown(JL_MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0) | (jlMouseDown(JL_MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0)); } static void presentChangedCells(jlSurfaceT *screen, int16_t cursorCol, int16_t cursorRow) { int16_t col; int16_t row; - jlKeyE key; + jlKeyE key; bool lit; for (row = 0; row < GRID_ROWS; row++) { for (col = 0; col < GRID_COLS; col++) { key = gKeyGrid[row][col]; - if (key == KEY_NONE) { + if (key == JL_KEY_NONE) { continue; } lit = cellTargetLit(col, row, cursorCol, cursorRow); @@ -513,7 +544,7 @@ static void processTypedChars(jlSurfaceT *screen) { // this region). static void redrawTextStrip(jlSurfaceT *screen) { gLine[gLineLen] = '\0'; - jlFillRect(screen, 0, TEXT_STRIP_Y, SURFACE_WIDTH, SURFACE_HEIGHT - TEXT_STRIP_Y, COLOR_BACKGROUND); + jlFillRect(screen, 0, TEXT_STRIP_Y, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT - TEXT_STRIP_Y, COLOR_BACKGROUND); drawTextLine(screen, TEXT_X, TEXT_Y, "TYPE HERE:", COLOR_UNLIT); drawTextLine(screen, TEXT_INPUT_X, TEXT_Y, gLine, COLOR_CURSOR); drawVerifyRow(screen); @@ -593,12 +624,13 @@ static void updateMouseVerifyRow(jlSurfaceT *screen) { int main(void) { - jlConfigT config; - jlSurfaceT *screen; + jlConfigT config; + jlSurfaceT *screen; int16_t cursorCol; int16_t cursorRow; config.codegenBytes = 8 * 1024; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); @@ -613,13 +645,13 @@ int main(void) { } buildPalette(screen); - jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); + jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0); initialPaint(screen); jlInputPoll(); for (;;) { jlInputPoll(); - if (jlKeyPressed(KEY_ESCAPE)) { + if (jlKeyPressed(JL_KEY_ESCAPE)) { break; } cellAtPoint(jlMouseX(), jlMouseY(), &cursorCol, &cursorRow); diff --git a/examples/palprobe/palprobe.c b/examples/palprobe/palprobe.c index 5f2572f..765f5a8 100644 --- a/examples/palprobe/palprobe.c +++ b/examples/palprobe/palprobe.c @@ -41,10 +41,10 @@ static void timeChanges(const char *name, bool scb); // Paint the bands, make the two changes, and hold the screen. static void check(void) { static const uint16_t firsts[BANDS] = { 0xF00, 0x0F0, 0x00F, 0xFFF }; - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; - jlSurfaceT *stage; - uint16_t band; - uint16_t frame; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; + jlSurfaceT *stage; + uint16_t band; + uint16_t frame; stage = jlStageGet(); for (band = 0; band < BANDS; band++) { @@ -52,7 +52,7 @@ static void check(void) { colors[PAINT_COLOR] = firsts[band]; jlPaletteSet(stage, (uint8_t)band, colors); jlScbSetRange(stage, (uint16_t)(band * BAND_LINES), (uint16_t)(band * BAND_LINES + BAND_LINES - 1u), (uint8_t)band); - jlFillRect(stage, 0, (int16_t)(band * BAND_LINES), SURFACE_WIDTH, BAND_LINES, PAINT_COLOR); + jlFillRect(stage, 0, (int16_t)(band * BAND_LINES), JL_SURFACE_WIDTH, BAND_LINES, PAINT_COLOR); } jlStagePresent(); jlWaitVBL(); @@ -71,7 +71,7 @@ static void check(void) { // PROBE_FRAMES frames of palette changes (or SCB changes), each presented. static void timeChanges(const char *name, bool scb) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; jlSurfaceT *stage; uint16_t start; uint32_t ms0; @@ -105,13 +105,14 @@ int main(void) { uint16_t band; cfg.codegenBytes = 0; + cfg.noAudio = true; if (!jlInit(&cfg)) { return 1; } stage = jlStageGet(); for (band = 0; band < BANDS; band++) { jlScbSetRange(stage, (uint16_t)(band * BAND_LINES), (uint16_t)(band * BAND_LINES + BAND_LINES - 1u), (uint8_t)band); - jlFillRect(stage, 0, (int16_t)(band * BAND_LINES), SURFACE_WIDTH, BAND_LINES, (uint8_t)(band + 1u)); + jlFillRect(stage, 0, (int16_t)(band * BAND_LINES), JL_SURFACE_WIDTH, BAND_LINES, (uint8_t)(band + 1u)); } jlStagePresent(); timeChanges("palette+present", false); diff --git a/examples/pattern/pattern.c b/examples/pattern/pattern.c index 7d27bfd..7e2509c 100644 --- a/examples/pattern/pattern.c +++ b/examples/pattern/pattern.c @@ -16,9 +16,9 @@ #include #define BAND_COUNT 8 -#define BAND_HEIGHT (SURFACE_HEIGHT / BAND_COUNT) +#define BAND_HEIGHT (JL_SURFACE_HEIGHT / BAND_COUNT) #define STRIPE_COUNT 16 -#define STRIPE_WIDTH (SURFACE_WIDTH / STRIPE_COUNT) +#define STRIPE_WIDTH (JL_SURFACE_WIDTH / STRIPE_COUNT) static void buildPalettes(jlSurfaceT *screen); static void buildScbs(jlSurfaceT *screen); @@ -27,7 +27,7 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn); static void buildPalettes(jlSurfaceT *screen) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; // Palette 0: grayscale makeGradient(colors, 1, 1, 1); @@ -83,7 +83,7 @@ static void drawStripes(jlSurfaceT *screen) { jlSurfaceClear(screen, 0); for (colorIndex = 0; colorIndex < STRIPE_COUNT; colorIndex++) { x = (int16_t)(colorIndex * STRIPE_WIDTH); - jlFillRect(screen, x, 0, STRIPE_WIDTH, SURFACE_HEIGHT, colorIndex); + jlFillRect(screen, x, 0, STRIPE_WIDTH, JL_SURFACE_HEIGHT, colorIndex); } } @@ -94,7 +94,7 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn) { uint8_t g; uint8_t b; - for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { r = (uint8_t)(redOn ? i : 0); g = (uint8_t)(greenOn ? i : 0); b = (uint8_t)(blueOn ? i : 0); @@ -106,10 +106,11 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn) { int main(void) { - jlConfigT config; - jlSurfaceT *screen; + jlConfigT config; + jlSurfaceT *screen; config.codegenBytes = 8 * 1024; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/save/save.c b/examples/save/save.c index 814bcf5..0d57b47 100644 --- a/examples/save/save.c +++ b/examples/save/save.c @@ -54,10 +54,10 @@ static bool storeCount(uint16_t count); static void buildPalette(jlSurfaceT *screen) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t i; - for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = 0x0000; } colors[COLOR_BACKGROUND] = 0x0000; // black @@ -125,6 +125,7 @@ int main(void) { bool saved; config.codegenBytes = 8 * 1024; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); @@ -143,7 +144,7 @@ int main(void) { saved = storeCount(count); buildPalette(screen); - jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); + jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0); jlSurfaceClear(screen, COLOR_BACKGROUND); drawBits(screen, count); drawLamp(screen, saved); @@ -153,10 +154,10 @@ int main(void) { for (;;) { jlInputPoll(); - if (jlKeyPressed(KEY_ESCAPE)) { + if (jlKeyPressed(JL_KEY_ESCAPE)) { break; } - if (jlKeyPressed(KEY_SPACE)) { + if (jlKeyPressed(JL_KEY_SPACE)) { // Erase the save and reset the counter to zero. jlSaveDelete(SAVE_NAME); count = 0u; diff --git a/examples/scb/scb.c b/examples/scb/scb.c index 6179d4e..b254eee 100644 --- a/examples/scb/scb.c +++ b/examples/scb/scb.c @@ -45,7 +45,7 @@ // ----- Constants ----- #define STRIPE_WIDTH 16u // two bytes of a plane: whole-byte fills -#define STRIPE_COUNT (SURFACE_WIDTH / STRIPE_WIDTH) +#define STRIPE_COUNT (JL_SURFACE_WIDTH / STRIPE_WIDTH) #define COLOR_CHANNEL_MASK 0x0Fu #define RED_SHIFT 8u #define GREEN_SHIFT 4u @@ -75,13 +75,13 @@ static bool escapeTyped(void); // ----- Helpers (alphabetical) ----- static void buildPalettes(jlSurfaceT *screen) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t pal; uint16_t col; uint16_t blue; - for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) { - for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) { + for (pal = 0u; pal < JL_SURFACE_PALETTE_COUNT; pal++) { + for (col = 0u; col < JL_SURFACE_COLORS_PER_PALETTE; col++) { blue = (uint16_t)((pal * BLUE_PALETTE_STEP + col) & COLOR_CHANNEL_MASK); colors[col] = (uint16_t)((pal << RED_SHIFT) | (col << GREEN_SHIFT) | blue); } @@ -97,7 +97,7 @@ static void buildScb(jlSurfaceT *screen) { uint8_t runPalette; for (line = 0u; line < EVERY_LINE_END; line++) { - jlScbSet(screen, line, (uint8_t)(line % SURFACE_PALETTE_COUNT)); + jlScbSet(screen, line, (uint8_t)(line % JL_SURFACE_PALETTE_COUNT)); } run = 0u; runLeft = 0u; @@ -105,13 +105,13 @@ static void buildScb(jlSurfaceT *screen) { for (line = EVERY_LINE_END; line < IRREGULAR_END; line++) { if (runLeft == 0u) { runLeft = (uint16_t)(1u + (run * RUN_LENGTH_STEP) % RUN_LENGTH_SPAN); - runPalette = (uint8_t)((run * RUN_PALETTE_STEP + RUN_PALETTE_BASE) % SURFACE_PALETTE_COUNT); + runPalette = (uint8_t)((run * RUN_PALETTE_STEP + RUN_PALETTE_BASE) % JL_SURFACE_PALETTE_COUNT); run++; } jlScbSet(screen, line, runPalette); runLeft--; } - for (line = IRREGULAR_END; line < SURFACE_HEIGHT; line++) { + for (line = IRREGULAR_END; line < JL_SURFACE_HEIGHT; line++) { jlScbSet(screen, line, (uint8_t)(((line & 1u) != 0u) ? ODD_LINE_PALETTE : EVEN_LINE_PALETTE)); } } @@ -121,7 +121,7 @@ static void drawStripes(jlSurfaceT *screen) { uint16_t stripe; for (stripe = 0u; stripe < STRIPE_COUNT; stripe++) { - jlFillRect(screen, (int16_t)(stripe * STRIPE_WIDTH), 0, STRIPE_WIDTH, SURFACE_HEIGHT, (uint8_t)(stripe % SURFACE_COLORS_PER_PALETTE)); + jlFillRect(screen, (int16_t)(stripe * STRIPE_WIDTH), 0, STRIPE_WIDTH, JL_SURFACE_HEIGHT, (uint8_t)(stripe % JL_SURFACE_COLORS_PER_PALETTE)); } } @@ -146,6 +146,7 @@ int main(void) { jlSurfaceT *screen; config.codegenBytes = CODEGEN_DEFAULT; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); return EXIT_FAILURE; diff --git a/examples/serial/serial.c b/examples/serial/serial.c index 25cc659..026c134 100644 --- a/examples/serial/serial.c +++ b/examples/serial/serial.c @@ -79,17 +79,17 @@ typedef struct KeySendT { } KeySendT; static const KeySendT gKeySend[] = { - { KEY_A, 'A' }, { KEY_B, 'B' }, { KEY_C, 'C' }, { KEY_D, 'D' }, - { KEY_E, 'E' }, { KEY_F, 'F' }, { KEY_G, 'G' }, { KEY_H, 'H' }, - { KEY_I, 'I' }, { KEY_J, 'J' }, { KEY_K, 'K' }, { KEY_L, 'L' }, - { KEY_M, 'M' }, { KEY_N, 'N' }, { KEY_O, 'O' }, { KEY_P, 'P' }, - { KEY_Q, 'Q' }, { KEY_R, 'R' }, { KEY_S, 'S' }, { KEY_T, 'T' }, - { KEY_U, 'U' }, { KEY_V, 'V' }, { KEY_W, 'W' }, { KEY_X, 'X' }, - { KEY_Y, 'Y' }, { KEY_Z, 'Z' }, - { KEY_0, '0' }, { KEY_1, '1' }, { KEY_2, '2' }, { KEY_3, '3' }, - { KEY_4, '4' }, { KEY_5, '5' }, { KEY_6, '6' }, { KEY_7, '7' }, - { KEY_8, '8' }, { KEY_9, '9' }, - { KEY_RETURN, '\r' } + { JL_KEY_A, 'A' }, { JL_KEY_B, 'B' }, { JL_KEY_C, 'C' }, { JL_KEY_D, 'D' }, + { JL_KEY_E, 'E' }, { JL_KEY_F, 'F' }, { JL_KEY_G, 'G' }, { JL_KEY_H, 'H' }, + { JL_KEY_I, 'I' }, { JL_KEY_J, 'J' }, { JL_KEY_K, 'K' }, { JL_KEY_L, 'L' }, + { JL_KEY_M, 'M' }, { JL_KEY_N, 'N' }, { JL_KEY_O, 'O' }, { JL_KEY_P, 'P' }, + { JL_KEY_Q, 'Q' }, { JL_KEY_R, 'R' }, { JL_KEY_S, 'S' }, { JL_KEY_T, 'T' }, + { JL_KEY_U, 'U' }, { JL_KEY_V, 'V' }, { JL_KEY_W, 'W' }, { JL_KEY_X, 'X' }, + { JL_KEY_Y, 'Y' }, { JL_KEY_Z, 'Z' }, + { JL_KEY_0, '0' }, { JL_KEY_1, '1' }, { JL_KEY_2, '2' }, { JL_KEY_3, '3' }, + { JL_KEY_4, '4' }, { JL_KEY_5, '5' }, { JL_KEY_6, '6' }, { JL_KEY_7, '7' }, + { JL_KEY_8, '8' }, { JL_KEY_9, '9' }, + { JL_KEY_RETURN, '\r' } }; #define KEY_SEND_COUNT ((int16_t)(sizeof(gKeySend) / sizeof(gKeySend[0]))) @@ -107,10 +107,10 @@ static void sendBytes(const char *bytes, uint16_t len); static void buildPalette(jlSurfaceT *screen) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t i; - for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { + for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = 0x0000; } colors[COLOR_BACKGROUND] = 0x0000; // black @@ -186,6 +186,7 @@ int main(void) { bool linkOpen; config.codegenBytes = 8 * 1024; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); @@ -205,14 +206,14 @@ int main(void) { serialCfg.parity = SERIAL_PARITY; serialCfg.flow = SERIAL_FLOW; serialCfg.unit = 0u; - linkOpen = jlSerialOpen(JL_SERIAL_DEFAULT, &serialCfg); + linkOpen = jlSerialOpen(JL_SERIAL_DEFAULT, &serialCfg); if (linkOpen) { // Greet on connect -- also a TX heartbeat that a loopback peer can see. sendBytes(gBanner, (uint16_t)(sizeof(gBanner) - 1u)); } buildPalette(screen); - jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); + jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0); jlSurfaceClear(screen, COLOR_BACKGROUND); drawBits(screen, gLastByte); drawLamp(screen, LAMP_TX_X, false, COLOR_TX_ON); @@ -229,7 +230,7 @@ int main(void) { bool dirty; jlInputPoll(); - if (jlKeyPressed(KEY_ESCAPE)) { + if (jlKeyPressed(JL_KEY_ESCAPE)) { break; } jlSerialPoll(); @@ -238,7 +239,7 @@ int main(void) { dirty = false; // Transmit: a banner on SPACE, otherwise the pressed key's ASCII. - if (jlKeyPressed(KEY_SPACE)) { + if (jlKeyPressed(JL_KEY_SPACE)) { sendBytes(gBanner, (uint16_t)(sizeof(gBanner) - 1u)); txFired = true; } else { @@ -256,7 +257,7 @@ int main(void) { if (got > 0u) { gLastByte = (int16_t)rx[got - 1u]; drawBits(screen, gLastByte); - dirty = true; + dirty = true; gRxTimer = BLINK_FRAMES; sendBytes((const char *)rx, got); // echo } diff --git a/examples/serloss/serloss.c b/examples/serloss/serloss.c index a3b9b82..4a36b37 100644 --- a/examples/serloss/serloss.c +++ b/examples/serloss/serloss.c @@ -25,7 +25,8 @@ // spread is read jitter // SERLOSS_SFX also keep a sound effect streaming and retrigger a one-shot // effect once per frame, so NinjaTrackerPlus's entry calls and -// its DOC refill interrupt run throughout the receive +// its DOC refill interrupt run throughout the receive; the +// stream's refills are counted, the proof audio is running // SERLOSS_LOOSE drop the serial add-on's hold on the tight interrupt-latency // bound once the port is open: the negative control, which // must lose bytes wherever a masked stretch outlasts the FIFO @@ -57,6 +58,7 @@ #define MB_JOY_MIN_Y 30 #define MB_JOY_MAX_Y 31 #define MB_SFX_SHOTS 32 +#define MB_SFX_FILLS 34 #define MB_SIG_VALUE_0 0xA5u #define MB_SIG_VALUE_1 0x5Au #define MB_JOY_BIAS 128 @@ -115,8 +117,9 @@ typedef struct { #endif #ifdef SERLOSS_SFX -static int8_t gSfxShot[SFX_SHOT_BYTES]; -static uint8_t gSfxWave[SFX_WAVE_BYTES]; +static int8_t gSfxShot[SFX_SHOT_BYTES]; +static uint8_t gSfxWave[SFX_WAVE_BYTES]; +static uint16_t gSfxFills = 0u; #endif static uint16_t benchLoop(jlSurfaceT *stage, bool present); @@ -126,7 +129,7 @@ static void joyPoll(JoySpreadT *spread); static void mailbox16(uint16_t slot, uint16_t value); static void publish(uint16_t received, uint16_t inSequence, uint16_t breaks, uint16_t presents, uint16_t ticks, uint16_t sum, uint16_t now); #ifdef SERLOSS_SFX -static bool sfxStart(void); +static void sfxStart(void); static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count); #endif @@ -156,8 +159,8 @@ static void joyPoll(JoySpreadT *spread) { int8_t y; jlInputPoll(); - x = jlJoystickX(JOYSTICK_0); - y = jlJoystickY(JOYSTICK_0); + x = jlJoystickX(JL_JOYSTICK_0); + y = jlJoystickY(JL_JOYSTICK_0); if (spread->polls == 0u || x < spread->minX) { spread->minX = x; } @@ -200,14 +203,11 @@ __attribute__((noinline)) static void publish(uint16_t received, uint16_t inSequ #ifdef SERLOSS_SFX -// Bring up audio, build the one-shot's square wave and the stream's sawtooth, and start the -// endless stream. -static bool sfxStart(void) { +// Build the one-shot's square wave and the stream's sawtooth, and start the endless stream +// (jlInit started audio). +static void sfxStart(void) { uint16_t i; - if (!jlAudioInit()) { - return false; - } for (i = 0u; i < SFX_SHOT_BYTES; i++) { gSfxShot[i] = (int8_t)(((i / (SFX_SHOT_PERIOD / 2u)) & 1u) != 0u ? SFX_LEVEL : -SFX_LEVEL); } @@ -219,15 +219,17 @@ static bool sfxStart(void) { // overflow the receive ring. jlAudioPlaySfx((uint8_t)SFX_SHOT_SLOT, (const uint8_t *)gSfxShot, SFX_SHOT_BYTES, SFX_SHOT_RATE); jlAudioPlaySfxStreamRaw((uint8_t)SFX_STREAM_SLOT, sfxStreamFill, (void *)0, SFX_STREAM_RATE); - return true; } // Endless sawtooth, whole periods: never returns 0, so the stream runs until the program ends. +// Only a running engine asks for refills, so their count proves audio is up. static uint32_t sfxStreamFill(void *ctx, int8_t *dst, uint32_t count) { uint32_t done; (void)ctx; + gSfxFills++; + mailbox16(MB_SFX_FILLS, gSfxFills); done = 0u; while (count - done >= SFX_WAVE_BYTES) { jlMemCopy(dst + done, gSfxWave, SFX_WAVE_BYTES); @@ -269,6 +271,11 @@ int main(void) { #endif config.codegenBytes = 0u; +#ifdef SERLOSS_SFX + config.noAudio = false; +#else + config.noAudio = true; +#endif if (!jlInit(&config)) { MB_BASE[MB_PHASE] = PHASE_FAILED; for (;;) { @@ -280,15 +287,12 @@ int main(void) { MB_BASE[MB_PHASE] = PHASE_INIT; mailbox16(MB_JOY_POLLS, 0u); mailbox16(MB_SFX_SHOTS, 0u); + mailbox16(MB_SFX_FILLS, 0u); stage = jlStageGet(); mailbox16(MB_FILL_TICKS, benchLoop(stage, false)); mailbox16(MB_PRESENT_TICKS, benchLoop(stage, true)); #ifdef SERLOSS_SFX - if (!sfxStart()) { - MB_BASE[MB_PHASE] = PHASE_FAILED; - for (;;) { - } - } + sfxStart(); shots = 0u; #endif @@ -316,7 +320,7 @@ int main(void) { #ifdef SERLOSS_JOY // Calibrate on the first poll, in the latency mode the rest will run in, so a still stick // reads 0 from then on. - jlJoystickReset(JOYSTICK_0, 0u); + jlJoystickReset(JL_JOYSTICK_0, 0u); spread.polls = 0u; joyPoll(&spread); #endif diff --git a/examples/sermouse/sermouse.c b/examples/sermouse/sermouse.c index 6a5a60b..cd80f3e 100644 --- a/examples/sermouse/sermouse.c +++ b/examples/sermouse/sermouse.c @@ -142,10 +142,10 @@ static void updateMouseRow(jlSurfaceT *screen, MouseRowT *last); // A grey ramp, so every index the verification rows stamp is visible. static void buildPalette(jlSurfaceT *screen) { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t i; - for (i = 0u; i < SURFACE_COLORS_PER_PALETTE; i++) { + for (i = 0u; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = (uint16_t)((i << PALETTE_RED_SHIFT) | (i << PALETTE_GREEN_SHIFT) | i); } jlPaletteSet(screen, GREY_PALETTE, colors); @@ -155,15 +155,15 @@ static void buildPalette(jlSurfaceT *screen) { // The grey palette everywhere, or DENSE_SCB_LINES lines alternating between // it and the same ramp in blue. static void buildScb(jlSurfaceT *screen) { - jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, GREY_PALETTE); + jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, GREY_PALETTE); #if DENSE_SCB_LINES > 0 { - uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t i; uint16_t line; jlPaletteGet(screen, GREY_PALETTE, colors); - for (i = 1u; i < SURFACE_COLORS_PER_PALETTE; i++) { + for (i = 1u; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = (uint16_t)(colors[i] | BLUE_FLOOR); } jlPaletteSet(screen, BLUE_PALETTE, colors); @@ -183,8 +183,8 @@ static void fillPointer(jlSurfaceT *screen, int16_t x, int16_t y, uint8_t color) w = POINTER_SIZE; h = POINTER_SIZE; - if ((int16_t)(x + w) > SURFACE_WIDTH) { - w = (int16_t)(SURFACE_WIDTH - x); + if ((int16_t)(x + w) > JL_SURFACE_WIDTH) { + w = (int16_t)(JL_SURFACE_WIDTH - x); } if ((int16_t)(y + h) > SERIAL_ROW_Y) { h = (int16_t)(SERIAL_ROW_Y - y); @@ -197,10 +197,7 @@ static void fillPointer(jlSurfaceT *screen, int16_t x, int16_t y, uint8_t color) static uint16_t mouseFlags(void) { - return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0u) | - (jlMouseDown(MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0u) | - (jlMouseDown(MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0u) | - (jlMouseDown(MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0u)); + return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0u) | (jlMouseDown(JL_MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0u) | (jlMouseDown(JL_MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0u) | (jlMouseDown(JL_MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0u)); } @@ -334,6 +331,7 @@ int main(void) { bool paused; config.codegenBytes = 0u; + config.noAudio = true; if (!jlInit(&config)) { return 1; } @@ -371,10 +369,10 @@ int main(void) { for (;;) { jlInputPoll(); - if (jlKeyPressed(KEY_ESCAPE)) { + if (jlKeyPressed(JL_KEY_ESCAPE)) { break; } - if (jlKeyPressed(KEY_SPACE)) { + if (jlKeyPressed(JL_KEY_SPACE)) { paused = !paused; } receiveStream(&stats); diff --git a/examples/sertest/sertest.c b/examples/sertest/sertest.c index c3500df..6b2a28e 100644 --- a/examples/sertest/sertest.c +++ b/examples/sertest/sertest.c @@ -66,7 +66,7 @@ static void sendMark(const char *text) { int main(void) { - jlSerialConfigT cfg; + jlSerialConfigT cfg; static const char banner[] = "JLSERTEST-READY\r\n"; cfg.baud = 9600u; diff --git a/examples/sprite/sprite.c b/examples/sprite/sprite.c index ed2ac50..f5b09f6 100644 --- a/examples/sprite/sprite.c +++ b/examples/sprite/sprite.c @@ -7,7 +7,9 @@ // per source line) and converted at startup into the 2x2 tile layout // jlSpriteT expects. jlSpriteDraw handles transparent-color-0; we use // jlSpriteSaveUnder/RestoreUnder to undraw the previous frame's ball -// without redrawing the whole screen. +// without redrawing the whole screen. The ball's speed is given per +// second and each frame moves it by the time since the last one +// (jlMillisElapsed), so it crosses the screen as fast on every port. #include @@ -18,17 +20,20 @@ #define BALL_TILES_X (BALL_W / 8) #define BALL_TILES_Y (BALL_H / 8) -#define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * TILE_BYTES) -// SaveUnder rounds x down to the platform's storage alignment: 2 px -// for chunky 4bpp (1 extra byte/row worst case), 8 px for planar -// 4-plane (4 extra bytes/row worst case -- one per plane). The +4 -// covers the planar case and is a no-op overhead on chunky. -// Sized by the library-wide worst case (16-px-group window on the -// planar ports); see JOEY_SPRITE_BACKUP_BYTES in joey/sprite.h. -#define BALL_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(BALL_W / 8, BALL_H / 8) +#define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * JL_TILE_BYTES) +// The save-under buffer is sized by jlSpriteBackupLength (joey/sprite.h): +// the worst case at any x, on every port. #define BALL_PALETTE_IDX 0 +#define BALL_SPEED_X 120 // pixels per second, the same on every port +#define BALL_SPEED_Y 60 +#define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel +#define MAX_STEP_MILLIS 100 // after a stall, move no further than this +#define MILLIS_PER_SECOND 1000 +#define BALL_X_MAX ((int32_t)(JL_SURFACE_WIDTH - BALL_W) << SUBPIXEL_SHIFT) +#define BALL_Y_MAX ((int32_t)(JL_SURFACE_HEIGHT - BALL_H) << SUBPIXEL_SHIFT) + #define COLOR_BG 0 // Authored layout: 16 pixels wide x 16 rows = 8 bytes/row x 16 rows. @@ -56,7 +61,7 @@ static const uint8_t gBallAuthored[BALL_H * (BALL_W / 2)] = { }; static uint8_t gBallTiles[BALL_TILE_BYTES]; -static uint8_t gBallBackup[BALL_BACKUP_BYTES] __attribute__((aligned(2))); +static uint16_t gBallBackup[jlSpriteBackupLength(uint16_t, BALL_W / 8, BALL_H / 8)]; // Repack from authored "wide row" layout to tile-major jlSpriteT @@ -71,12 +76,10 @@ static void repackBallTiles(void) { for (ty = 0; ty < BALL_TILES_Y; ty++) { for (tx = 0; tx < BALL_TILES_X; tx++) { - dst = &gBallTiles[(ty * BALL_TILES_X + tx) * TILE_BYTES]; + dst = &gBallTiles[(ty * BALL_TILES_X + tx) * JL_TILE_BYTES]; for (row = 0; row < 8; row++) { for (b = 0; b < 4; b++) { - dst[row * 4 + b] = - gBallAuthored[((ty * 8) + row) * (BALL_W / 2) + - (tx * 4) + b]; + dst[row * 4 + b] = gBallAuthored[((ty * 8) + row) * (BALL_W / 2) + (tx * 4) + b]; } } } @@ -98,21 +101,24 @@ static void buildPalette(jlSurfaceT *screen) { int main(void) { - jlConfigT config; + jlConfigT config; jlSurfaceT *screen; jlSpriteT *ball; jlSpriteBackupT backup; - int16_t x; - int16_t y; - int16_t vx; - int16_t vy; - bool haveBackup; + int32_t x; // 1/256 of a pixel + int32_t y; + int32_t vx; // pixels per second + int32_t vy; + uint32_t lastMillis; + uint32_t now; + uint32_t elapsed; /* Amiga planar emits 8 pre-shifted DRAW variants per sprite (one * per x % 8 alignment) so the codegen arena needs roughly 8x what * the chunky two-shift case asks for. 32 KB fits a 16x16 ball * with all variants. */ config.codegenBytes = 32UL * 1024; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); @@ -140,25 +146,24 @@ int main(void) { (void)jlSpriteCompile(ball); buildPalette(screen); - jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, BALL_PALETTE_IDX); + jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, BALL_PALETTE_IDX); jlSurfaceClear(screen, COLOR_BG); jlStagePresent(); - backup.bytes = gBallBackup; + backup.bytes = (uint8_t *)gBallBackup; - x = 40; - y = 30; - vx = 2; - vy = 1; - haveBackup = false; + x = (int32_t)40 << SUBPIXEL_SHIFT; + y = (int32_t)30 << SUBPIXEL_SHIFT; + vx = BALL_SPEED_X; + vy = BALL_SPEED_Y; - jlSpriteSaveAndDraw(screen, ball, x, y, &backup); + jlSpriteSaveAndDraw(screen, ball, (int16_t)(x >> SUBPIXEL_SHIFT), (int16_t)(y >> SUBPIXEL_SHIFT), &backup); jlStagePresent(); - haveBackup = true; + lastMillis = jlMillisElapsed(); for (;;) { jlInputPoll(); - if (jlKeyPressed(KEY_ESCAPE)) { + if (jlKeyPressed(JL_KEY_ESCAPE)) { break; } @@ -170,22 +175,41 @@ int main(void) { // chunky targets like IIgs SHR; on planar c2p platforms it // also avoids c2p racing the raster). VBL wait is omitted // here so the demo runs at the sprite pipeline's native - // throughput -- expect tearing on the ball. - if (haveBackup) { - jlSpriteRestoreUnder(screen, &backup); + // throughput -- expect tearing on the ball. The ball's speed + // does not depend on it: it moves by the time each frame took. + jlSpriteRestoreUnder(screen, &backup); + + now = jlMillisElapsed(); + elapsed = now - lastMillis; + lastMillis = now; + if (elapsed > MAX_STEP_MILLIS) { + elapsed = MAX_STEP_MILLIS; + } + x += (vx * (int32_t)elapsed * ((int32_t)1 << SUBPIXEL_SHIFT)) / MILLIS_PER_SECOND; + y += (vy * (int32_t)elapsed * ((int32_t)1 << SUBPIXEL_SHIFT)) / MILLIS_PER_SECOND; + + // A wall sets the direction rather than flipping it, so a frame + // that moved nothing cannot turn the ball back twice. + if (x <= 0) { + x = 0; + vx = BALL_SPEED_X; + } + if (x >= BALL_X_MAX) { + x = BALL_X_MAX; + vx = -BALL_SPEED_X; + } + if (y <= 0) { + y = 0; + vy = BALL_SPEED_Y; + } + if (y >= BALL_Y_MAX) { + y = BALL_Y_MAX; + vy = -BALL_SPEED_Y; } - x = (int16_t)(x + vx); - y = (int16_t)(y + vy); - if (x <= 0) { x = 0; vx = (int16_t)-vx; } - if (x >= SURFACE_WIDTH - BALL_W) { x = SURFACE_WIDTH - BALL_W; vx = (int16_t)-vx; } - if (y <= 0) { y = 0; vy = (int16_t)-vy; } - if (y >= SURFACE_HEIGHT - BALL_H) { y = SURFACE_HEIGHT - BALL_H; vy = (int16_t)-vy; } - - jlSpriteSaveAndDraw(screen, ball, x, y, &backup); + jlSpriteSaveAndDraw(screen, ball, (int16_t)(x >> SUBPIXEL_SHIFT), (int16_t)(y >> SUBPIXEL_SHIFT), &backup); jlStagePresent(); - haveBackup = true; } jlSpriteDestroy(ball); diff --git a/examples/tilebank/tilebank.c b/examples/tilebank/tilebank.c index 3dbc13d..6ff9163 100644 --- a/examples/tilebank/tilebank.c +++ b/examples/tilebank/tilebank.c @@ -58,10 +58,10 @@ // expect.bin layout (see the header comment). #define LE16_BYTES 2 #define BYTE_BITS 8 -#define PALETTE_COLORS SURFACE_COLORS_PER_PALETTE +#define PALETTE_COLORS JL_SURFACE_COLORS_PER_PALETTE #define EXPECT_OFF_PALETTE LE16_BYTES #define EXPECT_HEADER_BYTES (EXPECT_OFF_PALETTE + PALETTE_COLORS * LE16_BYTES) -#define TILE_PIXELS (TILE_PIXELS_PER_SIDE * TILE_PIXELS_PER_SIDE) +#define TILE_PIXELS (JL_TILE_PIXELS_PER_SIDE * JL_TILE_PIXELS_PER_SIDE) // The stage is cleared to this before every load. It MUST equal FILL_COLOR in // tests/host/tileBankHost.c: the host's hash is of a surface cleared to it. @@ -74,8 +74,8 @@ // the lamp colours cannot collide with the bank's sixteen. #define LAMP_W 64 #define LAMP_H 64 -#define LAMP_X ((SURFACE_WIDTH - LAMP_W) / 2) -#define LAMP_Y ((SURFACE_HEIGHT - LAMP_H) / 2) +#define LAMP_X ((JL_SURFACE_WIDTH - LAMP_W) / 2) +#define LAMP_Y ((JL_SURFACE_HEIGHT - LAMP_H) / 2) #define LAMP_PALETTE 1u #define LAMP_PASS_COLOR 1u #define LAMP_FAIL_COLOR 2u @@ -145,7 +145,7 @@ static void pasteTiles(uint16_t count) { jlSurfaceClear(gScreen, FILL_COLOR); for (i = 0u; i < count; i++) { - jlTilePaste(gScreen, (uint8_t)(i % TILE_BLOCKS_PER_ROW), (uint8_t)(i / TILE_BLOCKS_PER_ROW), &gTiles[i]); + jlTilePaste(gScreen, (uint8_t)(i % JL_TILE_BLOCKS_PER_ROW), (uint8_t)(i / JL_TILE_BLOCKS_PER_ROW), &gTiles[i]); } } @@ -275,12 +275,12 @@ static bool stageMatches(uint16_t tilesDrawn) { uint16_t block; uint8_t want; - for (y = 0; y < SURFACE_HEIGHT; y++) { - for (x = 0; x < SURFACE_WIDTH; x++) { - block = (uint16_t)((y / TILE_PIXELS_PER_SIDE) * TILE_BLOCKS_PER_ROW + x / TILE_PIXELS_PER_SIDE); + for (y = 0; y < JL_SURFACE_HEIGHT; y++) { + for (x = 0; x < JL_SURFACE_WIDTH; x++) { + block = (uint16_t)((y / JL_TILE_PIXELS_PER_SIDE) * JL_TILE_BLOCKS_PER_ROW + x / JL_TILE_PIXELS_PER_SIDE); want = FILL_COLOR; if (block < tilesDrawn) { - want = gExpectPixels[(uint32_t)block * TILE_PIXELS + (y % TILE_PIXELS_PER_SIDE) * TILE_PIXELS_PER_SIDE + x % TILE_PIXELS_PER_SIDE]; + want = gExpectPixels[(uint32_t)block * TILE_PIXELS + (y % JL_TILE_PIXELS_PER_SIDE) * JL_TILE_PIXELS_PER_SIDE + x % JL_TILE_PIXELS_PER_SIDE]; } if (jlSamplePixel(gScreen, x, y) != want) { return false; @@ -296,6 +296,7 @@ int main(void) { bool pass; config.codegenBytes = CODEGEN_DEFAULT; + config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); return EXIT_FAILURE; diff --git a/examples/uber/uber.c b/examples/uber/uber.c index 1c2a3cb..cbf2ad2 100644 --- a/examples/uber/uber.c +++ b/examples/uber/uber.c @@ -74,9 +74,9 @@ // +1 (9DC9) uint8 phase (UBER_PHASE_*) // +2 (9DCA) uint16 heartbeat (one increment per timing-loop pass) // +4 (9DCC) uint16 last jlFrameCount() value the timing loop saw -// No-ops on every other port. +// No-ops on every other port. Phase 2 was UBER's own audio start, which +// jlInit does now. #define UBER_PHASE_SETUP_SPRITE 1u -#define UBER_PHASE_AUDIO_INIT 2u #define UBER_PHASE_SHOWCASE 3u #define UBER_PHASE_TIMED_RUN 4u #define UBER_PHASE_CHECKS 5u @@ -137,10 +137,10 @@ extern uint16_t gJoeyLogRingHead; typedef void (*OpFn)(void); -static jlSurfaceT *gStage = NULL; -static jlSpriteT *gSprite = NULL; +static jlSurfaceT *gStage = NULL; +static jlSpriteT *gSprite = NULL; static jlSpriteBackupT gBackup; -static unsigned char gBackupBytes[JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2))); +static unsigned char gBackupBytes[jlSpriteBackupLength(unsigned char, 2, 2)] __attribute__((aligned(2))); static jlTileT gTileScratch; @@ -154,7 +154,7 @@ static jlTileT gTileMapB; #define UBER_FRAME_SPRITES 3 static jlSpriteBackupT gFrameBackups[UBER_FRAME_SPRITES]; -static unsigned char gFrameBackupBytes[UBER_FRAME_SPRITES][JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2))); +static unsigned char gFrameBackupBytes[UBER_FRAME_SPRITES][jlSpriteBackupLength(unsigned char, 2, 2)] __attribute__((aligned(2))); // Current timed-op number (1-based); mirrored into the mailbox. Lives @@ -239,11 +239,11 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t * endMillis = jlMillisElapsed(); *actualFramesOut = (uint16_t)(endFrame - startFrame); if (*actualFramesOut == 0u) { - *actualFramesOut = 1u; /* defensive: avoid div-by-zero */ + *actualFramesOut = 1u; // defensive: avoid div-by-zero } *millisDeltaOut = endMillis - startMillis; if (*millisDeltaOut == 0u) { - *millisDeltaOut = 1u; /* defensive: avoid div-by-zero */ + *millisDeltaOut = 1u; // defensive: avoid div-by-zero } return count; } @@ -285,10 +285,8 @@ static void timeOp(const char *name, OpFn op) { opsPerSec = (iters * 1000UL) / (unsigned long)millisDelta; opsPerSecFr = (iters * (unsigned long)jlFrameHz()) / (unsigned long)actualFrames; hash = jlSurfaceHash(gStage); - jlLogF("UBER: %s: %lu iters / %u frames = %lu ops/sec | hash=%08lX", - name, iters, actualFrames, opsPerSec, (unsigned long)hash); - jlLogF("UBER-CLK: %s: frame=%lu millis=%lu ops/sec (%lu ms)", - name, opsPerSecFr, opsPerSec, (unsigned long)millisDelta); + jlLogF("UBER: %s: %lu iters / %u frames = %lu ops/sec | hash=%08lX", name, iters, actualFrames, opsPerSec, (unsigned long)hash); + jlLogF("UBER-CLK: %s: frame=%lu millis=%lu ops/sec (%lu ms)", name, opsPerSecFr, opsPerSec, (unsigned long)millisDelta); } @@ -336,15 +334,15 @@ static void op_spriteSaveAndDraw (void) { jlSpriteSaveAndDraw (gStage, gSprite, static void op_stagePresent (void) { jlStagePresent(); } static void op_inputPoll (void) { jlInputPoll(); } -static void op_keyDown (void) { (void)jlKeyDown(KEY_A); } -static void op_keyPressed (void) { (void)jlKeyPressed(KEY_A); } +static void op_keyDown (void) { (void)jlKeyDown(JL_KEY_A); } +static void op_keyPressed (void) { (void)jlKeyPressed(JL_KEY_A); } static void op_mouseX (void) { (void)jlMouseX(); } -static void op_joyConnected (void) { (void)jlJoystickConnected(JOYSTICK_1); } +static void op_joyConnected (void) { (void)jlJoystickConnected(JL_JOYSTICK_1); } static void op_audioFrameTick (void) { jlAudioFrameTick(); } static void op_audioIsPlaying (void) { (void)jlAudioIsPlayingMod(); } -static void op_surfaceMarkDirty(void) { /* jlDrawPixel already marks; use fill instead */ +static void op_surfaceMarkDirty(void) { // jlDrawPixel already marks; use fill instead jlFillRect(gStage, 0, 0, 32, 32, 0); } @@ -600,7 +598,7 @@ static void PROBE_ATTR op_probePresentIdle(void) { // rows in this same log. Stage pixels are never written (saves read; // the restoreVal call fails validation), so these rows are hash-inert. static jlSpriteBackupT gProbeSpriteBackup; -static unsigned char gProbeSpriteBackupBytes[JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2))); +static unsigned char gProbeSpriteBackupBytes[jlSpriteBackupLength(unsigned char, 2, 2)] __attribute__((aligned(2))); static void PROBE_ATTR op_probeSpriteSaveVal(void) { jlSpriteSaveUnder(gStage, gSprite, -100, -100, &gProbeSpriteBackup); @@ -631,7 +629,7 @@ static void PROBE_ATTR op_probeSpriteSaveG16(void) { // and gBackup untouched, so the row is inert to the golden hashes. static jlSurfaceT *gProbeRestoreWork = NULL; static jlSpriteBackupT gProbeRestoreBackup; -static unsigned char gProbeRestoreBackupBytes[JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2))); +static unsigned char gProbeRestoreBackupBytes[jlSpriteBackupLength(unsigned char, 2, 2)] __attribute__((aligned(2))); static void PROBE_ATTR op_probeRestoreWork(void) { jlSpriteRestoreUnder(gProbeRestoreWork, &gProbeRestoreBackup); @@ -682,15 +680,15 @@ static void PROBE_ATTR op_probeDrawTrusted(void) { // The band arrays are port-neutral, so the value must also be // IDENTICAL ACROSS PORTS -- the C-mark ports are the reference any // fused asm mark must reproduce. -extern uint8_t gStageMinWord[SURFACE_HEIGHT]; -extern uint8_t gStageMaxWord[SURFACE_HEIGHT]; +extern uint8_t gStageMinWord[JL_SURFACE_HEIGHT]; +extern uint8_t gStageMaxWord[JL_SURFACE_HEIGHT]; static uint32_t PROBE_ATTR probeBandFold(void) { uint16_t y; uint32_t sum; sum = 0; - for (y = 0; y < SURFACE_HEIGHT; y++) { + for (y = 0; y < JL_SURFACE_HEIGHT; y++) { sum = (uint32_t)(sum * 33u + gStageMinWord[y]); sum = (uint32_t)(sum * 33u + gStageMaxWord[y]); } @@ -1042,80 +1040,123 @@ static void PROBE_ATTR op_probeVgaCheck(void) { #endif -#ifdef JOEYLIB_PLATFORM_DVX +#if defined(JOEYLIB_PLATFORM_DVX) || defined(JOEYLIB_HOSTED_WIN16) || defined(JOEYLIB_HOSTED_WIN32) || defined(JOEYLIB_HOSTED_BE) +#if defined(JOEYLIB_PLATFORM_DVX) extern uint32_t dvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +#define PROBE_WINDOW_MISMATCH(_s, _c) dvxPortProbeDisplayMismatch((_s), (_c)) +#define PROBE_WINDOW_TAG "dvxDisplayMismatch" +#elif defined(JOEYLIB_HOSTED_WIN16) +extern uint32_t hostWin16ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +#define PROBE_WINDOW_MISMATCH(_s, _c) hostWin16ProbeDisplayMismatch((_s), (_c)) +#define PROBE_WINDOW_TAG "win16DisplayMismatch" +#elif defined(JOEYLIB_HOSTED_WIN32) +extern uint32_t hostWin32ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +#define PROBE_WINDOW_MISMATCH(_s, _c) hostWin32ProbeDisplayMismatch((_s), (_c)) +#define PROBE_WINDOW_TAG "win32DisplayMismatch" +#else +extern uint32_t hostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared); +#define PROBE_WINDOW_MISMATCH(_s, _c) hostBeProbeDisplayMismatch((_s), (_c)) +#define PROBE_WINDOW_TAG "beDisplayMismatch" +#endif -// P13: DVX display-side parity, P12's battery on the DVX present, which +// P13: display-side parity in a window, P12's battery on the DVX, Win16, +// Win32, and Be presents. The golden hashes gate the stage, never what is shown. DVX // expands dirty rows through per-palette tables into the window's content // buffer (the desktop's pixel format, at the window's scale) or the -// full-screen backbuffer. The golden hashes gate the stage, never those. -// dvxPortProbeDisplayMismatch derives every pixel from the stage's own -// palette entries (not the present's tables) and counts the ones that -// differ. A strip of one-pixel stripes in all 16 colours stays on screen, +// full-screen backbuffer; dvxPortProbeDisplayMismatch derives every pixel +// from the stage's own palette entries (not the present's tables) and +// counts the ones that differ. The hosted backends draw only the +// rectangles that changed at each show; their probes count the frame's +// pixels that differ from the stage and the window's that differ from the +// whole frame drawn afresh. A strip of one-pixel stripes in all 16 colours stays on screen, // so every table entry, and both pixels of a byte, are checked; the small // rect is on odd pixels. After P12's steps (tall band, small inner rect, // settle, idle) two more work the tables: the strip's rows moved to another // palette (an SCB change: every row again), then that palette changed // (its rows again; on the whole screen at 8 bits only the screen's -// palette); then the SCBs and the palette go back. Every count must be 0. +// palette); then every row a palette of its own, all 16 in use (more than +// Win16's identity layout holds, so its fallback and the way back); then +// the SCBs and the palette go back; then two rects far +// apart in one present (two changed rectangles in one show). Steps B and +// Two are the ones the hosted backends draw piecemeal. Every count must be +// 0. #define PROBE_DVX_PALETTE 1u #define PROBE_DVX_STRIP_H 10 #define PROBE_DVX_SCB_LAST 99u -static void PROBE_ATTR op_probeDvxDisplayCheck(void) { - static const uint16_t probePalette[SURFACE_COLORS_PER_PALETTE] = { +static void PROBE_ATTR op_probeWindowDisplayCheck(void) { + static const uint16_t probePalette[JL_SURFACE_COLORS_PER_PALETTE] = { 0x000, 0xF00, 0x0F0, 0x00F, 0xFF0, 0xF0F, 0x0FF, 0xFFF, 0x800, 0x080, 0x008, 0x880, 0x808, 0x088, 0x888, 0x444 }; - static const uint16_t changedPalette[SURFACE_COLORS_PER_PALETTE] = { + static const uint16_t changedPalette[JL_SURFACE_COLORS_PER_PALETTE] = { 0x000, 0x123, 0x246, 0x369, 0x48C, 0x5AF, 0x6C2, 0x7E5, 0x9A1, 0xAB4, 0xBC7, 0xCDA, 0xDEC, 0xE1F, 0xF52, 0x3D8 }; - uint8_t savedScb[SURFACE_HEIGHT]; - uint16_t savedPalette[SURFACE_COLORS_PER_PALETTE]; + uint8_t savedScb[JL_SURFACE_HEIGHT]; + uint16_t savedPalette[JL_SURFACE_COLORS_PER_PALETTE]; uint32_t afterB; uint32_t afterSettle; uint32_t afterIdle; uint32_t afterScb; uint32_t afterPalette; + uint32_t afterAll; uint32_t afterRestore; + uint32_t afterTwo; uint32_t compared; int16_t x; uint16_t y; - for (y = 0; y < SURFACE_HEIGHT; y++) { + for (y = 0; y < JL_SURFACE_HEIGHT; y++) { savedScb[y] = jlScbGet(gStage, y); } jlPaletteGet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette); jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, probePalette); + // Shown before the steps, so the palette's own full redraw (Win16 + // repaints its window when colours change) is behind them; and a frame + // between B's presents, so each is a show of its own (Win16 shows at + // most once a frame, everything changed since the last). + jlStagePresent(); + jlWaitVBL(); jlSurfaceClear(gStage, 3); - for (x = 0; x < SURFACE_WIDTH; x++) { - jlFillRect(gStage, x, 0, 1, PROBE_DVX_STRIP_H, (uint8_t)(x % SURFACE_COLORS_PER_PALETTE)); + for (x = 0; x < JL_SURFACE_WIDTH; x++) { + jlFillRect(gStage, x, 0, 1, PROBE_DVX_STRIP_H, (uint8_t)(x % JL_SURFACE_COLORS_PER_PALETTE)); } jlStagePresent(); + jlWaitVBL(); jlFillRect(gStage, 0, 50, 320, 100, 3); jlStagePresent(); + jlWaitVBL(); jlFillRect(gStage, 101, 61, 37, 9, 9); jlStagePresent(); - afterB = dvxPortProbeDisplayMismatch(gStage, &compared); + afterB = PROBE_WINDOW_MISMATCH(gStage, &compared); jlStagePresent(); - afterSettle = dvxPortProbeDisplayMismatch(gStage, &compared); + afterSettle = PROBE_WINDOW_MISMATCH(gStage, &compared); jlStagePresent(); - afterIdle = dvxPortProbeDisplayMismatch(gStage, &compared); + afterIdle = PROBE_WINDOW_MISMATCH(gStage, &compared); jlScbSetRange(gStage, 0, PROBE_DVX_SCB_LAST, (uint8_t)PROBE_DVX_PALETTE); jlStagePresent(); - afterScb = dvxPortProbeDisplayMismatch(gStage, &compared); + afterScb = PROBE_WINDOW_MISMATCH(gStage, &compared); jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, changedPalette); jlStagePresent(); - afterPalette = dvxPortProbeDisplayMismatch(gStage, &compared); - for (y = 0; y < SURFACE_HEIGHT; y++) { + afterPalette = PROBE_WINDOW_MISMATCH(gStage, &compared); + for (y = 0; y < JL_SURFACE_HEIGHT; y++) { + jlScbSet(gStage, y, (uint8_t)(y % JL_SURFACE_PALETTE_COUNT)); + } + jlStagePresent(); + afterAll = PROBE_WINDOW_MISMATCH(gStage, &compared); + for (y = 0; y < JL_SURFACE_HEIGHT; y++) { jlScbSet(gStage, y, savedScb[y]); } jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette); jlStagePresent(); - afterRestore = dvxPortProbeDisplayMismatch(gStage, &compared); - jlLogF("UBER-PROBE: dvxDisplayMismatch afterB=%lu afterSettle=%lu afterIdle=%lu afterScb=%lu afterPalette=%lu afterRestore=%lu compared=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle, (unsigned long)afterScb, (unsigned long)afterPalette, (unsigned long)afterRestore, (unsigned long)compared); + afterRestore = PROBE_WINDOW_MISMATCH(gStage, &compared); + jlFillRect(gStage, 7, 21, 13, 5, 12); + jlFillRect(gStage, 251, 171, 31, 17, 6); + jlStagePresent(); + afterTwo = PROBE_WINDOW_MISMATCH(gStage, &compared); + jlLogF("UBER-PROBE: " PROBE_WINDOW_TAG " afterB=%lu afterSettle=%lu afterIdle=%lu afterScb=%lu afterPalette=%lu afterAll=%lu afterRestore=%lu afterTwo=%lu compared=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle, (unsigned long)afterScb, (unsigned long)afterPalette, (unsigned long)afterAll, (unsigned long)afterRestore, (unsigned long)afterTwo, (unsigned long)compared); } #endif @@ -1218,8 +1259,7 @@ static void buildBallSprite(void) { dst = &gBallTiles[(ty * BALL_TILES_X + tx) * 32u]; for (row = 0; row < 8; row++) { for (b = 0; b < 4; b++) { - dst[row * 4 + b] = - gBallAuthored[((ty * 8) + row) * 8 + (tx * 4) + b]; + dst[row * 4 + b] = gBallAuthored[((ty * 8) + row) * 8 + (tx * 4) + b]; } } } @@ -1465,7 +1505,7 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) { // the headless perf-capture run still completes without a keypress. held = jlFrameCount(); while ((uint16_t)(jlFrameCount() - held) < SC_HOLD_FRAMES) { - /* hold */ + // hold } } @@ -1553,10 +1593,10 @@ static void __attribute__((noinline)) checkClippedSprites(void) { static void __attribute__((noinline)) checkTileMonoFlood(void) { static jlTileT mono; - uint8_t i; + uint8_t i; uberMbOp(103); - for (i = 0; i < TILE_BYTES; i++) { + for (i = 0; i < JL_TILE_BYTES; i++) { mono.pixels[i] = (uint8_t)((i & 1) ? 0x0F : 0xF0); } jlTilePasteMono(gStage, 10, 10, &mono, 5, 9); @@ -1570,11 +1610,11 @@ static void __attribute__((noinline)) checkTileMonoFlood(void) { static void __attribute__((noinline)) checkDrawText(void) { static uint16_t asciiMap[256]; - uint16_t i; + uint16_t i; uberMbOp(104); for (i = 0; i < 256u; i++) { - asciiMap[i] = TILE_NO_GLYPH; + asciiMap[i] = JL_TILE_NO_GLYPH; } // 'A' -> tile (5,5), 'B' -> tile (6,6). Seed both glyph tiles here -- // the section-opening surface clear wiped whatever the timed tile ops @@ -1662,8 +1702,8 @@ static void __attribute__((noinline)) checkAllocator(void) { // stabilization; the coarse marker froze on this check). uberMbOp(111); usedBefore = jlSpriteCodegenBytesUsed(); - a = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y); - b = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y); + a = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y); + b = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y); if (a == NULL || b == NULL) { chkPassFail("arena-churn", false); return; @@ -1710,12 +1750,12 @@ static void __attribute__((noinline)) checkAllocator(void) { static void __attribute__((noinline)) checkTileMonoFloodExtra(void) { // 0x00 -> combo idx 0 (bg:bg), 0xFF -> idx 3 (fg:fg), 0x0F -> idx 1 // (bg:fg), 0xF0 -> idx 2 (fg:bg): all four opacity pairs in one tile. - static const uint8_t comboBytes[4] = { 0x00u, 0xFFu, 0x0Fu, 0xF0u }; + static const uint8_t comboBytes[4] = { 0x00u, 0xFFu, 0x0Fu, 0xF0u }; static jlTileT monoAll; uint8_t i; uberMbOp(121); - for (i = 0; i < TILE_BYTES; i++) { + for (i = 0; i < JL_TILE_BYTES; i++) { monoAll.pixels[i] = comboBytes[i & 3u]; } jlTilePasteMono(gStage, 10, 12, &monoAll, 5, 9); @@ -1967,7 +2007,7 @@ static void __attribute__((noinline)) runAllTests(void) { // matches the golden restore's geometry exactly) into a dedicated // buffer, then time a restore to a non-stage work surface. The // difference vs the golden jlSpriteRestoreUnder row = the mark. - gProbeRestoreWork = jlSurfaceCreate(); + gProbeRestoreWork = jlSurfaceCreate(); gProbeRestoreBackup.sprite = gSprite; gProbeRestoreBackup.bytes = gProbeRestoreBackupBytes; jlSpriteSaveUnder(gStage, gSprite, 40, 30, &gProbeRestoreBackup); @@ -2016,8 +2056,8 @@ static void __attribute__((noinline)) runAllTests(void) { #ifdef JOEYLIB_PLATFORM_DOS op_probeVgaCheck(); #endif -#ifdef JOEYLIB_PLATFORM_DVX - op_probeDvxDisplayCheck(); +#if defined(JOEYLIB_PLATFORM_DVX) || defined(JOEYLIB_HOSTED_WIN16) || defined(JOEYLIB_HOSTED_WIN32) || defined(JOEYLIB_HOSTED_BE) + op_probeWindowDisplayCheck(); #endif #endif @@ -2057,8 +2097,7 @@ static void __attribute__((noinline)) reportElapsed(uint16_t startFrame) { endFrame = jlFrameCount(); elapsedFrames = (uint16_t)(endFrame - startFrame); elapsedMs = ((unsigned long)elapsedFrames * 1000UL) / (unsigned long)jlFrameHz(); - jlLogF("UBER: total wall time: %lu ms (%u frames @ %u Hz)", - elapsedMs, elapsedFrames, (unsigned)jlFrameHz()); + jlLogF("UBER: total wall time: %lu ms (%u frames @ %u Hz)", elapsedMs, elapsedFrames, (unsigned)jlFrameHz()); } @@ -2080,7 +2119,7 @@ static bool __attribute__((noinline)) setupSprite(void) { jlLog("UBER: jlSpriteCompile failed"); } while (jlFrameCount() == before) { - /* wait for next VBL edge */ + // wait for next VBL edge } jlLogF("UBER: jlSpriteCompile: 1 call in <= 1 frame"); gBackup.bytes = gBackupBytes; @@ -2099,6 +2138,8 @@ static bool __attribute__((noinline)) initJoeyLib(void) { * thunk+table DRAWs ~12 KB; plus ~1-2 KB of save/restore classes). * UL on the multiply because a 16-bit int overflows on 32 * 1024. */ config.codegenBytes = 32UL * 1024; + // Audio on: the timed run measures every op with the engine running. + config.noAudio = false; return jlInit(&config); } @@ -2138,21 +2179,15 @@ int main(void) { *UBER_MB_SPRITEPTR = (uint32_t)gSprite; #endif - // Audio: only init/shutdown is exercised. Triggering jlAudioPlaySfx - // without first calling jlAudioPlayMod leaves NTP's engine in a - // half-initialized state -- NTPstreamsound is designed to OVERLAY on - // an already-running module. Without NTPprepare/NTPplay first, the - // streamer oscillator is fired but no music tick ever advances or - // silences it, and you get a stuck high-pitched scream. UBER doesn't - // ship a MOD asset, so we skip the SFX exercise. The frame-tick and - // isPlayingMod calls below still get timed (both are no-op fast - // paths on IIgs). - uberMbPhase(UBER_PHASE_AUDIO_INIT); - if (jlAudioInit()) { - jlLogF("UBER: audioInit OK"); - } else { - jlLogF("UBER: audioInit failed (skipping audio)"); - } + // Audio: jlInit started the engine; nothing is played. Triggering + // jlAudioPlaySfx without first calling jlAudioPlayMod leaves NTP's + // engine in a half-initialized state -- NTPstreamsound is designed to + // OVERLAY on an already-running module. Without NTPprepare/NTPplay + // first, the streamer oscillator is fired but no music tick ever + // advances or silences it, and you get a stuck high-pitched scream. + // UBER doesn't ship a MOD asset, so we skip the SFX exercise. The + // frame-tick and isPlayingMod calls below still get timed (both are + // no-op fast paths on IIgs). // Visual showcase: render one of each primitive into its own grid // cell so the run shows something legible (the timed benchmark below diff --git a/examples/wblaunch/wblaunch.c b/examples/wblaunch/wblaunch.c new file mode 100644 index 0000000..623bd2f --- /dev/null +++ b/examples/wblaunch/wblaunch.c @@ -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 +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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; +} diff --git a/include/joey/audio.h b/include/joey/audio.h index 511a4bc..ecd3e8a 100644 --- a/include/joey/audio.h +++ b/include/joey/audio.h @@ -11,13 +11,19 @@ // Apple IIgs -- NinjaTrackerPlus .NTP, played by Ninjaforce's // 65816 replayer linked into the binary. // Amiga -- raw .MOD, played by Frank Wille's PTPlayer. -// DOS -- raw .MOD, played by libxmp-lite over SB DMA. -// Atari ST -- raw .MOD, played by libxmp-lite (parser only) plus -// a hand-rolled 68k 4-channel mixer that outputs via -// YM2149 4-bit PWM at ~12.5 kHz. +// DOS -- raw .MOD, mixed by JoeyLib's module player (PTPlayer's +// replay in C) into SB DMA (22 kHz mono 8-bit). +// Atari ST -- raw .MOD, mixed by the module player at 12288 Hz and +// played through the YM2149's channel A volume (PWM); the +// STe build plays it through DMA sound at 12517 Hz. +// X68000 -- raw .MOD, mixed by the module player at 7812 Hz into the +// MSM6258 ADPCM channel. +// DVX -- raw .MOD through DVX's music library. +// hosted -- raw .MOD, mixed by the module player in software. // -// Game code always calls the same five entry points; the per-port -// HAL hides the engine choice. A failed audio init is non-fatal -- +// Game code always calls the same entry points; the per-port HAL hides +// the engine choice. jlInit starts audio (jlConfigT.noAudio leaves it +// off) and jlShutdown stops it. Sound that does not start is non-fatal: // jlInit still succeeds and audio calls become no-ops. #ifndef JOEYLIB_AUDIO_H @@ -26,36 +32,31 @@ #include "platform.h" #include "types.h" -// SFX slot convention: the IIgs maps tracker tone voices onto SFX -// slots 0-2 and the tracker noise voice onto slot 3, so applications -// that also play digital SFX while music runs should prefer slot 4. -// On the other ports the slots are independent of the voice layer. -#define JOEY_AUDIO_SFX_SLOTS 5 +// SFX slot convention: the IIgs plays tone voices 0-2 on SFX slots 0-2 +// and the noise voice on slot 3 (a voice and an effect on one slot cut +// each other off, and a held voice takes its slot back at the next +// jlAudioFrameTick), so applications that also play digital SFX while +// music runs should prefer slot 4. On the Amiga effects, the module, the +// voices, and the noise share Paula's four channels. On the other ports +// the slots are independent of the voice layer. +#define JL_AUDIO_SFX_SLOTS 5 // Number of PSG-style polyphonic tone voices (jlAudioVoice selects // voice 0/1/2). The core gate range-checks against this so each -// per-port HAL can assume voice < JOEY_AUDIO_VOICES. -#define JOEY_AUDIO_VOICES 3 - -// Initialize audio. Returns true if the platform has a working audio -// engine and was able to start it. Returns false silently otherwise; -// the rest of the API stays callable but produces no sound. -bool jlAudioInit(void); - -// Tear down the engine. Safe to call when audio is not initialized. -void jlAudioShutdown(void); +// per-port HAL can assume voice < JL_AUDIO_VOICES. +#define JL_AUDIO_VOICES 3 // Begin module playback. data points at the platform-native module -// blob (.MOD on most platforms, .NTP on IIgs); the asset pipeline +// blob (.MOD on every platform but the IIgs, .NTP there); the asset pipeline // produces the right form for each target. If a module is already // playing, it is replaced. // -// loop=true plays forever. loop=false stops at song end, but Amiga -// requires the module to contain an E8FF effect at song end for the -// stop to fire (PTPlayer has no native song-end signal). The -// `joeymod` tool's .amod output extension injects that marker -// automatically; ship .amod for Amiga and .mod for the other ports. -// loop=false on a .mod (no E8FF) loops anyway on Amiga. +// loop=true plays forever. loop=false stops at song end on every port. +// PTPlayer (Amiga) has no native song-end signal, so the Amiga HAL adds an +// E8F stop marker to the module as it copies it to chip RAM; a module +// with no room for one (all 128 song positions or patterns in use) loops +// there anyway. The `joeymod` tool's .amod output adds the same marker +// ahead of time. void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop); // Stop the current module (if any). The playhead is reset so the next @@ -63,15 +64,18 @@ void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop); void jlAudioStopMod(void); // True if a module is currently producing output (false during silence -// after StopMod or before the first PlayMod, and on platforms where -// audio init failed). +// after StopMod, once a loop=false song has ended, before the first +// PlayMod, and on platforms where audio init failed). bool jlAudioIsPlayingMod(void); -// Trigger a one-shot digital SFX on the given slot (0..JOEY_AUDIO_SFX_SLOTS-1). +// Trigger a one-shot digital SFX on the given slot (0..JL_AUDIO_SFX_SLOTS-1). // sample points at raw signed 8-bit PCM. rateHz is the playback rate // the sample was recorded at; the engine pitches as needed for its // own output rate. If the slot is currently playing, the new sample -// replaces it. +// replaces it. Most ports read the sample in place while it plays, so it +// must stay valid and unchanged until it has finished or the slot is +// stopped; the IIgs keeps a copy per slot keyed by address, length, and +// rate, so changed bytes replayed with all three the same are not heard. // // The full -128..127 range is accepted everywhere, but the IIgs plays // -128 as -127: its DOC reads 0x00 (which is what -128 becomes in the @@ -83,9 +87,10 @@ void jlAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16 // streaming slot's prefetch buffer; should write up to `count` signed // 8-bit samples to `dst` and return the number written (see // jlAudioPlaySfx for the one clamp the IIgs applies). Returning 0 -// signals end-of-stream and the slot auto-deactivates. Called from -// the same context as the audio engine's refill (main thread on -// DOS/ST, audio ISR on Amiga/IIgs); must not block or allocate. +// signals end-of-stream and the slot auto-deactivates. Called in +// program time on every port, from the call that starts the stream and +// then from jlAudioFrameTick (on DVX also while the desktop runs); must +// not block, and should be quick. typedef uint32_t (*jlAudioStreamFillT)(void *ctx, int8_t *dst, uint32_t count); // Trigger streaming SFX playback on the given slot. The mixer pulls @@ -94,22 +99,22 @@ typedef uint32_t (*jlAudioStreamFillT)(void *ctx, int8_t *dst, uint32_t count); // allocation. `rateHz` is the sample rate `fill` produces; the // engine resamples (with linear interpolation) to its own output // rate. If the slot is currently playing, the new stream replaces -// it. Implemented on every port: DOS/ST/X68000 pull through the shared -// SFX overlay mixer, while the IIgs (NTPstreamsound) and Amiga -// (PTPlayer mt_playfx) play one chunk at a time and fire the next from -// jlAudioFrameTick as the current one runs down -- so on those two the -// host must be calling jlAudioFrameTick for a stream to continue past -// its first chunk, and a seam can carry up to one frame of jitter. +// it. Implemented on every port: DOS/ST/X68000/hosted pull through the +// shared SFX overlay mixer as jlAudioFrameTick refills their sound +// buffers, while the IIgs (NTPstreamsound, 7 KB chunks) and Amiga +// (PTPlayer mt_playfx, 4 KB chunks) play one chunk at a time and fire the +// next from jlAudioFrameTick as the current one runs down -- so the host +// must be calling jlAudioFrameTick for a stream to continue, and on those +// two a seam can carry up to one frame of jitter. void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz); // Raw-format streaming: jlAudioPlaySfxStream, except `fill` delivers // bytes already in the port's raw stream format (jlAudioSfxRawFormat), // so the port skips its per-byte conversion of every chunk. On the IIgs -// that conversion is a 65816 byte loop over each 3.5 KB chunk -- it -// stalled Space Taxi's ride for a third of a second per chunk of speech. -// Every other port streams signed PCM natively, so there Raw is the -// plain call. A producer that cannot make the port's format uses -// jlAudioPlaySfxStream. +// that conversion costs ~7 us a byte, ~50 ms per 7 KB chunk. Every other +// port's raw format is signed PCM, so there Raw is the plain call (DVX +// still flips each byte for its sound library). A producer that cannot +// make the port's format uses jlAudioPlaySfxStream. #define JL_SFX_RAW_SIGNED8 0u // int8_t PCM, the jlAudioPlaySfxStream contract #define JL_SFX_RAW_UNSIGNED8_NOZERO 1u // uint8_t PCM biased 0x80; 0x00 is forbidden (DOC stop byte) uint8_t jlAudioSfxRawFormat(void); @@ -117,14 +122,14 @@ void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, void *ctx // Stereo placement for a SFX slot. typedef enum { - AUDIO_PAN_LEFT = 0, - AUDIO_PAN_CENTER, - AUDIO_PAN_RIGHT, + JL_AUDIO_PAN_LEFT = 0, + JL_AUDIO_PAN_CENTER, + JL_AUDIO_PAN_RIGHT, - AUDIO_PAN_COUNT + JL_AUDIO_PAN_COUNT } jlAudioPanE; -// Place a SFX slot in the stereo field. Slots start at AUDIO_PAN_CENTER, and +// Place a SFX slot in the stereo field. Slots start at JL_AUDIO_PAN_CENTER, and // the setting sticks to the SLOT, so it survives retriggers -- set it once // when a sound is meant to come from one side. // @@ -135,26 +140,26 @@ typedef enum { // A stock IIgs sums to mono anyway; this only shows up on a stereo // card or an emulator. // Amiga left and right, by picking the Paula channel side (0 and 3 are -// left, 1 and 2 right). Paula cannot place one voice in both ears, -// so AUDIO_PAN_CENTER keeps PTPlayer's own channel choice. +// left, 1 and 2 right). A channel plays on one side only, so a +// centred sound plays on a free channel each side when both sides +// have one, and otherwise on the one channel PTPlayer picks. // X68000 ignored. Everything is mixed into ONE MSM6258 ADPCM stream, so // its pan bits would move all audio at once, not one slot. -// DOS, ST ignored -- their output is mono. +// DOS, ST ignored -- their output is mono (the STe build's too). +// DVX all three, through DVX's stereo mixer. +// hosted all three (the mix is stereo), except Win16, which is mono. // Passing an out-of-range pan is ignored. void jlAudioSetPan(uint8_t slot, jlAudioPanE pan); // Stop a SFX slot early. No-op if the slot is already idle. void jlAudioStopSfx(uint8_t slot); -// Direct hardware tone generator: continuously play a square wave at -// `freqHz` until the next jlAudioTone call. freqHz == 0 silences the -// generator. Designed for AGI-era PSG-style music where the engine -// just programs a divisor and lets the chip play; CPU cost during -// playback is zero. Single voice on platforms with multiple -// hardware oscillators (uses voice 0). On DOS this programs PIT -// counter 2 and the speaker gate (port 0x61). No-op on platforms -// without a reachable tone generator -- callers should fall back to -// jlAudioPlaySfxStream when they need PCM-synth sound on those. +// A single tone at `freqHz` until the next call: jlAudioVoice(0, freqHz, +// 0), tone voice 0 at full volume, on every port. freqHz == 0 silences +// it. Designed for AGI-era PSG-style music where the engine just +// programs a divisor and lets the chip play; CPU cost during playback is +// zero. It shares voice 0 with jlAudioVoice and jlMusicPlay. On DOS +// without an OPL2, voice 0 (and so this tone) is the PC speaker. void jlAudioTone(uint16_t freqHz); // Periodic tick callback for sound schedulers that need better-than- @@ -164,56 +169,68 @@ void jlAudioTone(uint16_t freqHz); // into one render frame, so the chip only "hears" the last write // per batch and short notes are dropped. // -// fn fires from interrupt context at `hz` times per second. Keep -// fn short: only memory reads, direct hardware-register writes, -// no malloc / printf / blocking. Pass fn = NULL to uninstall. +// fn fires `hz` times per second on average, on every port, and may +// fire from an interrupt (DOS, Amiga, ST, X68000): keep fn short, only +// memory reads and direct hardware-register writes, no malloc / printf / +// blocking, and share data with it through jlAudioCritical*. Pass +// fn = NULL to uninstall (always returns true). Returns false when hz is +// 0, audio is off or did not start, or the port cannot take the rate. // // Per-port impl: DOS reprograms PIT counter 0 + hooks INT 8, // keeping the BIOS tick counter at $0040:$006C advancing at the // canonical 18.2 Hz via an accumulator (so uclock / time stay -// correct). Other ports return false until a port-appropriate -// timer source is wired. +// correct). The Amiga uses a timer.device E-clock request and a +// software interrupt; the ST and X68000 ride the system's Timer C +// interrupt (200 and 100 Hz), so calls due between its ticks come +// together. The IIgs and DVX call fn in program time from +// jlAudioFrameTick, and the hosted ports from the mixer, between the +// samples where each tick falls. typedef void (*jlAudioTickFnT)(void); bool jlAudioTickRegister(jlAudioTickFnT fn, uint16_t hz); // Critical section against the tick ISR. Callers that mutate // state read by the registered tick callback must wrap the // mutation in Enter/Exit so the ISR can't observe a half-written -// state. On DOS this is `cli` / `sti`; on ports without an -// audio ISR these are no-ops. +// state. Sections nest: the outermost pair holds the interrupt off +// (on DOS, the Amiga, the ST, and the X68000, every interrupt, so keep +// it short), and its Exit puts back the interrupt state its Enter found. +// No-ops where the callback runs in program time (IIgs, DVX, hosted). void jlAudioCriticalEnter(void); void jlAudioCriticalExit(void); // 3-voice PSG-style polyphonic tone generator. `voice` selects 0/1/2; // `freqHz` is the new tone (0 = silence this voice); `atten` is the // AGI/SN76489-style 4-bit attenuation, 0 = loudest .. 15 = silent -// (2 dB per step). The mapping to underlying hardware: +// (2 dB per step, as near as the chip comes: the ST's YM2149 levels are +// ~3.5 dB apart, so within 1.6 dB there). The mapping to underlying +// hardware: // -// DOS -- AdLib OPL2 melodic channels 0/1/2 (ports 0x388/0x389). -// Atari ST -- YM2149 PSG channels A/B/C. +// DOS -- AdLib OPL2 melodic channels 0/1/2 (ports 0x388/0x389); +// with no OPL2, voice 0 is the PC speaker, 1 and 2 silent. +// Atari ST -- YM2149 PSG channels A/B/C (plain ST: voice 0 silent while +// digital sound plays, which drives channel A). // Amiga -- Paula channels 0/1/2 looping a 2-byte square sample. -// IIgs -- Ensoniq DOC oscillators playing a resident square, -// retriggered as an ~80 ms one-shot per call. Needs -// jlAudioInit (the DOC path rides the NTP engine); -// callers that hold a note longer than ~80 ms must -// re-assert it about once a frame (jlMusicPlay's -// sequencer and AGI's per-frame driver both do). +// IIgs -- Ensoniq DOC oscillators playing a resident square on SFX +// slots 0/1/2. +// X68000 -- YM2151 channels 0/1/2, one sine operator each. +// DVX -- square tones of DVX's sound library. +// hosted -- square waves mixed in software. // -// Voice state is sticky: a voice keeps playing its last frequency -// until the next jlAudioVoice call on it (IIgs excepted, above). CPU -// cost during sustained playback is zero (the hardware oscillates on -// its own). Safe to call without jlAudioInit on DOS/ST/Amiga -- the -// voice path is direct hardware programming, independent of the -// MOD/SFX mixer. On ST and Amiga the voice channels are the same -// hardware the MOD engine drives: while a MOD is playing, voice -// writes fight the player's register writes and lose. Use the voice/ -// tracker layer or the MOD layer per scene, not both at once. +// A note sounds until the next call for that voice. The IIgs plays a +// note as a short sample and restarts held notes from jlAudioFrameTick, +// so call that every frame. On the Amiga the voices are the channels the +// MOD engine drives, so while a MOD is playing, voice writes fight the +// player's and lose: use the voice/tracker layer or the MOD layer per +// scene, not both at once. void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten); // Noise/percussion generator, the 4th logical voice of the tracker // layer. `pitch` is 0..31 in YM2149 noise-period style: 0 = the -// brightest hiss (hats), 31 = the lowest rumble (toms); atten as in -// jlAudioVoice, and atten >= 15 silences. The per-port mapping: +// brightest hiss (hats), 31 = the lowest rumble (toms); the noise runs at +// 125000 / pitch Hz (pitch 0 as 1) as far as each chip goes (the Amiga +// tops out at 28.6 kHz and the IIgs at 26 kHz, so pitches 0-4 sound +// alike there). atten as in jlAudioVoice, and atten >= 15 silences. The +// per-port mapping: // // Atari ST -- the YM2149 noise generator (reg 6) mixed onto // channel C. Noise shares channel C's volume register @@ -221,20 +238,21 @@ void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten); // apart (songbake warns on overlap). // DOS -- OPL2 rhythm-mode snare drum (the chip's LFSR noise // source) on channel 7; the three tone channels are -// untouched. +// untouched. A tuned drum: its pitch spreads over the +// snare's range instead of following the rate above. // Amiga -- Paula channel 3 looping a chip-RAM LFSR noise sample // (channels 0-2 are the tone voices). -// IIgs -- a resident LFSR noise sample on SFX slot 3, same -// ~80 ms retriggered one-shot model as the tone voices -// (jlMusicPlay's sequencer re-asserts it per frame). +// IIgs -- a resident LFSR noise sample on SFX slot 3. +// X68000 -- the YM2151's noise generator, at the rate exactly. +// DVX -- a noise tone of DVX's sound library. +// hosted -- the YM2149's 17-bit generator, mixed in software. // -// Same init contract as jlAudioVoice: direct hardware programming on -// DOS/ST/Amiga, jlAudioInit required on the IIgs. +// The same contract as jlAudioVoice: it sounds until the next call. void jlAudioNoise(uint8_t pitch, uint8_t atten); // Portable chip-tracker music: play a JYM1 event stream (baked from // tools/songbake text notation) through the jlAudioVoice tone layer. -// One authored song plays on all four ports at register-write cost -- +// One authored song plays on every port at register-write cost -- // there is no CPU mixing, so unlike jlAudioPlayMod this path is fully // viable on the ST and cheap on DOS. The stream is read IN PLACE: // data must stay valid until jlMusicStop or song end. Playback is @@ -242,8 +260,7 @@ void jlAudioNoise(uint8_t pitch, uint8_t atten); // paced by jlMillisElapsed, so tempo holds across 50/60/70 Hz ports. // loop=true restarts at the song's authored loop point (or the top // when it has none); loop=false silences the voices at song end. -// Returns false (and plays nothing) on a malformed stream. Call -// jlAudioInit first for IIgs audibility (see jlAudioVoice). +// Returns false (and plays nothing) on a malformed stream. bool jlMusicPlay(const uint8_t *data, uint32_t length, bool loop); // Stop tracker playback and silence the voices (tones and noise). @@ -270,10 +287,11 @@ void jlMusicResume(void); // Step it over successive frames for fades. void jlMusicSetAtten(uint8_t atten); -// Hook the engine into the game loop. Most platforms drive their -// engines off a hardware IRQ and ignore this call, but it's safe to -// invoke once per frame regardless and required for any port that -// runs its mixer in user-thread context. +// Hook the engine into the game loop; call it once per frame. Every +// port does sound work here: DOS, ST, X68000, DVX, and hosted mix or +// refill their sound buffers (modules and effects stop or repeat +// without it), the IIgs and Amiga feed streams and notice a module's +// end (and the IIgs holds notes), and every port advances jlMusicPlay. void jlAudioFrameTick(void); #endif diff --git a/include/joey/core.h b/include/joey/core.h index b068237..4853d7b 100644 --- a/include/joey/core.h +++ b/include/joey/core.h @@ -12,21 +12,35 @@ typedef struct { // Runtime compiled-sprite cache size. 0 selects a per-platform - // default: 8 KB on IIgs, 32 KB on Amiga/ST/DOS (the 68k/x86 - // emitters are ~4-8x less code-dense than the 65816's, so a - // 32x32 sprite can need 8-16 KB there). Size against - // jlSpriteCompiledSize() when tuning. On the IIgs the cache is one - // Memory Manager block allocated with attrNoCross (it may not span - // a 64 KB bank boundary), so values above 65536 are clamped to - // 65536; even a clamped request can fail if no free bank exists. + // default: 8 KB on the IIgs, 64 KB on the Amiga and ST, and 32 KB + // everywhere else (the 68k/x86 emitters are ~4-8x less code-dense + // than the 65816's, so a 32x32 sprite can need 8-16 KB there). The + // ports that interpret every sprite still allocate it. Size against + // jlSpriteCompiledSize() when tuning. Elsewhere the cache is one + // block, and jlInit fails if it cannot be had. On the IIgs it is up + // to four Memory Manager blocks of at most 64 KB, each inside one + // bank (a compiled sprite cannot cross a bank), so at most 256 KB; + // a bank that is refused is retried at 60 KB, and jlInit takes + // whatever part of the request it was granted. uint32_t codegenBytes; + // True: leave audio off. Every audio call is then silent, as on a + // machine without sound, and the program pays nothing for audio: + // on the IIgs no replayer read from disk, on DOS and the Amiga no + // interrupt hooked. jlInit starts audio otherwise, and jlShutdown + // stops it. + bool noAudio; } jlConfigT; -// Initialize the library. Returns true on success. -// On failure, jlLastError() returns a human-readable description. +// Initialize the library, audio too (unless config->noAudio). Returns true +// on success; sound that does not start is not a failure (modules and +// effects are then silent, and the log says so). On failure, jlLastError() +// returns a human-readable description; where the port's own start-up +// fails without saying why, that is just "jlpInit failed". bool jlInit(const jlConfigT *config); -// Shut down the library, releasing all resources. +// Shut down the library, audio first, releasing the library's own +// resources. Surfaces and sprites the program created are not freed: +// destroy them first. void jlShutdown(void); // Returns the most recent error message, or NULL if none. @@ -41,13 +55,17 @@ const char *jlVersionString(void); // Allocate a large or persistent buffer, backed by the platform's // native allocator rather than the small C heap. Use this (not malloc) // for anything that exceeds the per-allocation or total ceiling of the -// stock C heap -- on the IIgs the C heap lives in bank 0 and is tiny, -// and a single malloc is capped at ~32 KB, so multi-KB caches and frame -// buffers must come from the IIgs Memory Manager instead. Returns NULL -// on failure. The returned pointer is NOT zero-filled. +// stock C heap -- on the IIgs the whole C heap is one Memory Manager +// block of 32 KB to 48 KB (or, if that block cannot be had, what the +// link left in the program's bank, which can be tiny), so multi-KB +// caches and frame buffers must come from the IIgs Memory Manager +// instead. Returns NULL on failure. The returned pointer is NOT +// zero-filled. void *jlAlloc(uint32_t bytes); // Release a buffer returned by jlAlloc. NULL is accepted and ignored. +// Never mix jlFree and free: on the IIgs a jlAlloc block is a Memory +// Manager handle, and either one given the other's block corrupts memory. void jlFree(void *p); // memcpy with a 32-bit count. Same contract as memcpy (no overlap); every @@ -55,33 +73,36 @@ void jlFree(void *p); // byte, split at bank boundaries). void jlMemCopy(void *dst, const void *src, uint32_t bytes); -// Block the calling thread until the next display vertical blank. -// Used to pace game loops to the display's native refresh rate -// (~70 Hz on VGA mode 13h, ~50 or ~60 Hz on Amiga/ST PAL/NTSC, ~60 Hz -// on IIgs SHR). Cheap on every port since the underlying mechanism is -// always a hardware-level wait, not a software timer. +// Block the calling thread until the next display vertical blank, to +// pace a game loop to the display's refresh. On the machine ports it +// waits for the display's own vertical blank. The hosted ports, and DVX +// in a window, have none to wait for: there it waits for the next tick +// of a virtual frame clock (60 Hz hosted, 70 Hz DVX), and nothing keeps +// a present from tearing. void jlWaitVBL(void); -// Monotonic 16-bit frame counter. Caller must poll faster than -// 2 * jlFrameHz() so no edge is missed. Used by benchmarks and -// frame-rate-independent animation. (Per-port edge-detect / VBL-ISR -// mechanism is documented at jlpFrameCount in src/core/port.h.) +// Monotonic 16-bit frame counter, kept by an interrupt or derived from a +// clock, so it does not miss frames however seldom it is read (except on +// an X68000 whose system refused JoeyLib's frame interrupt: there it +// counts only while jlWaitVBL, jlFrameCount, or jlStagePresent is +// called). It wraps about every 18 minutes at 60 Hz, so compare two +// readings by subtraction. Used by benchmarks and frame-rate-independent +// animation. uint16_t jlFrameCount(void); -// Nominal display frame rate in Hz: 50 (Amiga PAL), 60 (IIgs / ST -// NTSC default), 70 (VGA mode 13h). The actual VBL cadence may +// Display frame rate in Hz: 60 on the IIgs (50 on a PAL machine), 50 on +// the Amiga (60 on an NTSC machine that cannot show PAL), 50 or 60 on +// the ST as the machine is set, 70 on DOS and DVX, 55 on the X68000 (56 +// under MAME), and 60 on every hosted port. The actual VBL cadence may // drift slightly; the value reported here is what benchmarks divide // by to convert iters-per-N-frames to ops/sec. uint16_t jlFrameHz(void); -// Monotonic wall-clock millisecond counter since jlInit. On DOS/ST it -// reads a real hardware timer; on IIgs/Amiga the generic implementation -// derives it from the frame counter (frame-granularity steps, and the -// caller must poll at least once per 65535 frames -- ~18 min at 60Hz -- -// or wrapped frames are lost). Two consecutive calls separated by a long -// render report the true elapsed time, not the number of VBLs that -// fired. (Per-port millisecond timer sources are documented at -// jlpMillisElapsed in src/core/port.h.) +// Monotonic wall-clock millisecond counter since jlInit. It may +// advance a frame at a time. Call it at least once every 15 minutes, or +// the time between two calls can be lost. Two consecutive calls +// separated by a long render report the true elapsed time, not the +// number of VBLs that fired. uint32_t jlMillisElapsed(void); // Deterministic, seedable pseudo-random generator. Bit-identical diff --git a/include/joey/draw.h b/include/joey/draw.h index 4462b10..4444ca1 100644 --- a/include/joey/draw.h +++ b/include/joey/draw.h @@ -50,12 +50,15 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col // Flood fill a 4-connected region starting at (x, y). Replaces every // pixel of the original color reached via N/S/E/W steps. No-op if -// (x, y) is off-surface or already matches newColor. +// (x, y) is off-surface or already matches newColor. The span stack +// holds 512 entries; a region that needs more is left partly filled +// (identically on every port). void jlFloodFill(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor); // Flood fill a 4-connected region starting at (x, y), stopping at // boundary pixels of color boundaryColor. Replaces every reachable -// pixel that is not boundaryColor with newColor. Used for vector-art +// pixel that is not boundaryColor with newColor; pixels already +// newColor stop the fill too. Same 512-entry span stack. Used for vector-art // rendering (e.g. Sierra-style picture playback): outline a closed // region with jlDrawLine in boundaryColor, then fill with this. void jlFloodFillBounded(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor, uint8_t boundaryColor); diff --git a/include/joey/file.h b/include/joey/file.h index e809861..19c5217 100644 --- a/include/joey/file.h +++ b/include/joey/file.h @@ -7,10 +7,14 @@ // // Two directories sit next to the executable, staged there by the per-platform // disk / directory packagers: -// DATA/ -- read-only game content (levels, sprites, audio). jlDataOpen. +// DATA/ -- game content (levels, sprites, audio). jlDataOpen. // SAVES/ -- writable save / preference files. jlSave*. // Both are addressed by a bare name relative to their directory; the prefix is -// forced here so callers never hard-code "DATA/" or "SAVES/". +// forced here so callers never hard-code "DATA/" or "SAVES/". The hosted +// ports make the program's folder the current directory in jlInit; DOS, the +// ST, the Amiga, and the X68000 use the current directory as they find it; +// the IIgs finds DATA/ under GS/OS prefix 0 and SAVES/ in the application's +// folder (prefix 1); DVX roots both at run time (docs/ApiFile.adoc). #ifndef JOEYLIB_FILE_H #define JOEYLIB_FILE_H @@ -23,18 +27,21 @@ // Open a data file by its name relative to the DATA/ directory. The DATA/ // prefix is prepended automatically; pass "levels/title.dat", not -// "DATA/levels/title.dat". mode is a standard fopen mode string. Returns NULL -// if the resulting path would overflow the internal buffer or the file cannot -// be opened. +// "DATA/levels/title.dat". mode is a standard fopen mode string, passed on +// unchecked (DATA/ is meant to be read, but a writing mode is not refused). +// Returns NULL if mode is NULL, the resulting path would overflow the +// internal buffer, or the file cannot be opened. FILE *jlDataOpen(const char *name, const char *mode); // ----- Writable save files (SAVES/) ----- // -// Save / preference data lives under a SAVES/ directory beside the executable -// -- a writable sibling of the read-only DATA/ tree. Names are relative to -// SAVES/: pass "hero.sav", not "SAVES/hero.sav". Keep names portable across -// the retro filesystems: at most 8 characters plus a short extension, letters -// and digits only, so they fit ProDOS (15-char) and DOS 8.3. +// Save / preference data lives under a SAVES/ directory -- a writable +// sibling of the DATA/ tree (where, per port: the top of this file). Names +// are relative to SAVES/: pass "hero.sav", not "SAVES/hero.sav". A name is at +// most 8 letters and digits starting with a letter, then optionally a period +// and up to 3 more, so it fits ProDOS (15 characters, letter first) and DOS +// 8.3; every save call refuses any other name, on every port, as if the file +// could not be opened. Case must match on Linux, Haiku, BeOS, and the web. // // A failed open / write is non-fatal and simply reports failure (false / 0), // so save code can degrade gracefully on a platform or volume where SAVES/ is @@ -47,7 +54,9 @@ FILE *jlDataOpen(const char *name, const char *mode); FILE *jlSaveOpen(const char *name, const char *mode); // Write `len` bytes from `buf` to save file `name`, replacing any existing -// contents. Returns true only if all `len` bytes were written. +// contents. Returns true only if all `len` bytes were written. Not atomic: a +// write that fails part way leaves the save short. buf NULL with len > 0 +// returns false before the save is touched. bool jlSaveWrite(const char *name, const void *buf, uint32_t len); // Read up to `max` bytes from save file `name` into `buf`. Returns the number @@ -62,9 +71,11 @@ bool jlSaveDelete(const char *name); // ----- Disk space ----- -// Free space, in bytes, on the volume that holds SAVES/ (the program's -// volume). Returns 0 if it cannot be determined (e.g. the port has no query -// for it). Saturates at UINT32_MAX (4 GB - 1) on larger volumes. +// Free space, in bytes, on the volume that holds SAVES/. Returns 0 if it +// cannot be determined. Saturates at UINT32_MAX (4 GB - 1) on larger +// volumes; DOS's own query (DOS, Win16) stops near 2 GB, and the web's +// figure is a fixed number from Emscripten's file system, not the +// browser's real space. uint32_t jlDiskFree(void); #endif diff --git a/include/joey/input.h b/include/joey/input.h index 094cc53..74b5efc 100644 --- a/include/joey/input.h +++ b/include/joey/input.h @@ -6,7 +6,7 @@ // // Call jlInputPoll() once per frame (typically right before // drawing) to refresh keyboard, mouse, and joystick state. After -// polling, the joeyKey* predicates return the current state of every +// polling, the jlKey* predicates return the current state of every // key: // // jlKeyDown(k) -- is key k held down right now @@ -26,8 +26,8 @@ // The mouse predicates return the pointer state: // // jlMouseX/Y() -- pointer position in surface -// coords (0..SURFACE_WIDTH-1, -// 0..SURFACE_HEIGHT-1) +// coords (0..JL_SURFACE_WIDTH-1, +// 0..JL_SURFACE_HEIGHT-1) // jlMouseDown(b) -- is button b held right now // jlMousePressed(b) -- rising edge since last poll // jlMouseReleased(b) -- falling edge since last poll @@ -39,7 +39,8 @@ // jlJoystickX/Y(js) -- axis values, signed -127..+127. // Digital sticks snap to the // extremes; analog sticks return -// the raw centered value. +// their offset from centre, scaled, +// dead zone applied. // jlJoyDown(js, b) -- is button b held on stick js // jlJoyPressed(js, b) -- rising edge since last poll // jlJoyReleased(js, b) -- falling edge since last poll @@ -54,63 +55,63 @@ #include "types.h" typedef enum { - KEY_NONE = 0, + JL_KEY_NONE = 0, - KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, - KEY_J, KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, - KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z, + JL_KEY_A, JL_KEY_B, JL_KEY_C, JL_KEY_D, JL_KEY_E, JL_KEY_F, JL_KEY_G, JL_KEY_H, JL_KEY_I, + JL_KEY_J, JL_KEY_K, JL_KEY_L, JL_KEY_M, JL_KEY_N, JL_KEY_O, JL_KEY_P, JL_KEY_Q, JL_KEY_R, + JL_KEY_S, JL_KEY_T, JL_KEY_U, JL_KEY_V, JL_KEY_W, JL_KEY_X, JL_KEY_Y, JL_KEY_Z, - KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, - KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, + JL_KEY_0, JL_KEY_1, JL_KEY_2, JL_KEY_3, JL_KEY_4, + JL_KEY_5, JL_KEY_6, JL_KEY_7, JL_KEY_8, JL_KEY_9, - KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE, + JL_KEY_SPACE, JL_KEY_ESCAPE, JL_KEY_RETURN, JL_KEY_TAB, JL_KEY_BACKSPACE, - KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, + JL_KEY_UP, JL_KEY_DOWN, JL_KEY_LEFT, JL_KEY_RIGHT, - KEY_LSHIFT, KEY_RSHIFT, KEY_LCTRL, KEY_LALT, + JL_KEY_LSHIFT, JL_KEY_RSHIFT, JL_KEY_LCTRL, JL_KEY_LALT, - KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, - KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, + JL_KEY_F1, JL_KEY_F2, JL_KEY_F3, JL_KEY_F4, JL_KEY_F5, + JL_KEY_F6, JL_KEY_F7, JL_KEY_F8, JL_KEY_F9, JL_KEY_F10, - KEY_COUNT + JL_KEY_COUNT } jlKeyE; typedef enum { - MOUSE_BUTTON_NONE = 0, - MOUSE_BUTTON_LEFT, - MOUSE_BUTTON_RIGHT, - MOUSE_BUTTON_MIDDLE, + JL_MOUSE_BUTTON_NONE = 0, + JL_MOUSE_BUTTON_LEFT, + JL_MOUSE_BUTTON_RIGHT, + JL_MOUSE_BUTTON_MIDDLE, - MOUSE_BUTTON_COUNT + JL_MOUSE_BUTTON_COUNT } jlMouseButtonE; typedef enum { - JOYSTICK_0 = 0, - JOYSTICK_1, + JL_JOYSTICK_0 = 0, + JL_JOYSTICK_1, - JOYSTICK_COUNT + JL_JOYSTICK_COUNT } jlJoystickE; typedef enum { - JOY_BUTTON_0 = 0, - JOY_BUTTON_1, + JL_JOY_BUTTON_0 = 0, + JL_JOY_BUTTON_1, - JOY_BUTTON_COUNT + JL_JOY_BUTTON_COUNT } jlJoyButtonE; -#define JOYSTICK_AXIS_MAX 127 -#define JOYSTICK_AXIS_MIN (-127) +#define JL_JOYSTICK_AXIS_MAX 127 +#define JL_JOYSTICK_AXIS_MIN (-127) // Control characters delivered through jlInputGetChar alongside -// printable ASCII. Everything else below 0x20, plus 0x7F, is -// filtered out of the queue. +// printable ASCII. Everything else below 0x20, and everything above +// 0x7F, is filtered out of the queue. #define JL_CHAR_BACKSPACE 0x08 #define JL_CHAR_TAB 0x09 #define JL_CHAR_RETURN 0x0D #define JL_CHAR_ESCAPE 0x1B // The FORWARD delete (the key labelled Del/Delete on a PC, ST or Amiga keyboard - not backspace). A text // editor needs both: backspace removes the character BEFORE the caret, this one removes the character -// UNDER it. 0x7F is ASCII DEL, which is what every one of these keyboards already reports. +// UNDER it. 0x7F is ASCII DEL. The IIgs never delivers it: its Delete key is backspace. #define JL_CHAR_DELETE 0x7F // Typed-character queue capacity (ring buffer; one slot stays empty, @@ -125,16 +126,19 @@ void jlInputPoll(void); // JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE, JL_CHAR_DELETE. Shift, caps lock, and // the keyboard layout are already applied by the backend, so // punctuation and shifted symbols arrive correctly on every port. -// (The layout is the machine's own on IIgs/Amiga/ST/X68000; the DOS -// port translates with a fixed US layout -- see claudeDocs/input.md.) +// (The layout is the machine's own on IIgs/Amiga/ST/X68000, DVX's on +// DVX, and the OS's on the hosted ports; DOS and the PSP's on-screen +// keyboard use a fixed US layout. On the ST a key held with Alt types +// nothing.) // // The queue is refilled by jlInputPoll() -- no extra bring-up is // needed. It holds JL_CHAR_QUEUE_SIZE - 1 characters; when full, // further characters are dropped (drop-newest) until the app pops. -// OS auto-repeat may deliver repeated characters on ports whose -// keyboard services repeat (the caller manages repeat policy). +// A held key repeats its character on every port (the caller manages +// repeat policy). +// A chord types nothing on any port: Ctrl+M is not Return. // 7-bit ASCII only; no Unicode / IME. Arrow keys are not characters -// and stay on the predicate API (jlKeyDown(KEY_UP) etc.). +// and stay on the predicate API (jlKeyDown(JL_KEY_UP) etc.). // jlWaitForAnyKey empties the queue when it returns -- its dismissing // keystroke is consumed by the wait, not delivered as text. // @@ -142,18 +146,26 @@ void jlInputPoll(void); // "byte or -1" is the interface contract consumers expect. int jlInputGetChar(void); -// Block until the user presses any key. Internally polls via -// jlInputPoll, so per-port jlpInputPoll machinery (including -// audio-friendly IRQ-driven samplers) keeps working while the -// wait loop runs. +// Block until the user presses a key: one with a jlKeyE, or one that types +// a character (punctuation, which has no jlKeyE). Mouse and joystick +// buttons do not end it, and a key held when the wait starts must be let go +// and pressed again. +// Internally polls via jlInputPoll, so per-port jlpInputPoll machinery +// (including audio-friendly IRQ-driven samplers) keeps working while +// the wait loop runs. void jlWaitForAnyKey(void); +// A key two physical keys give (Return and the keypad's Enter, an arrow and +// the keypad's 8, 2, 4, or 6, the left and right Ctrl or Alt) is down while +// either is held. bool jlKeyDown(jlKeyE key); bool jlKeyPressed(jlKeyE key); bool jlKeyReleased(jlKeyE key); // True when the platform confirmed an attached mouse at init (DOS: the INT 33h driver reset; -// Amiga/ST/IIgs/X68000: the OS mouse is always wired). +// Amiga: JoeyLib's input window opened; Win32/Win16: Windows reports one; SDL3/web: SDL reports one; +// IIgs/ST/X68000/DVX/Haiku/BeOS: always; PSP: never). The IIgs mouse has one button, the ST's and +// X68000's two. bool jlMousePresent(void); int16_t jlMouseX(void); int16_t jlMouseY(void); @@ -161,6 +173,12 @@ bool jlMouseDown(jlMouseButtonE button); bool jlMousePressed(jlMouseButtonE button); bool jlMouseReleased(jlMouseButtonE button); +// Every stick has two buttons. On the ST's own joystick ports, and on +// the Amiga until a stick's second button is first pressed, a stick is +// taken to have one: a quick press of it is JL_JOY_BUTTON_0, down for +// the one poll after it is let go, and a press held for half a second +// is JL_JOY_BUTTON_1, down from then until it is let go: act on button +// 0 with jlJoyPressed, not jlJoyDown. bool jlJoystickConnected(jlJoystickE js); int8_t jlJoystickX(jlJoystickE js); int8_t jlJoystickY(jlJoystickE js); @@ -168,22 +186,16 @@ bool jlJoyDown(jlJoystickE js, jlJoyButtonE button); bool jlJoyPressed(jlJoystickE js, jlJoyButtonE button); bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button); -// Re-enable joystick polling and recalibrate the resting (center) -// position. The IIgs port auto-disables polling after a short window -// of detecting no stick (saves ~3 ms/frame of busy-wait). It does NOT -// auto-re-probe -- the application must call this function to resume -// polling after plugging a stick in. -// -// The next poll after this call captures the stick's CURRENT raw -// position as the new center -- so the user must hold the stick -// centered when calling. Subsequent polls report position relative -// to that center; raw readings within `deadZone` units of the center -// clamp to 0 (use 0 to disable the dead zone). -// -// On platforms with truly digital sticks (Amiga / ST / DOS) the -// recalibration is a no-op -- those ports already report -1 / 0 / +1 -// directly -- and `deadZone` is ignored. The function still clears -// any auto-disconnect state so polling resumes. +// Set the stick's dead zone (8 at jlInit): axis values within `deadZone` of 0 (on the +// -127..127 scale) read 0 on the ports with analog sticks (IIgs, DOS, +// DVX, hosted); use 0 to disable it. On the IIgs and DOS the call also +// re-centres the stick: the next reading after it is taken as the +// centre, so the user must hold the stick centred when calling. The +// IIgs port stops polling after about a second (60 polls) with no stick +// seen (an absent stick costs ~1 ms a frame of busy-wait) and does NOT +// re-probe by itself: this call resumes polling. DOS probes the game +// port again; the Amiga hands the port back to lowlevel.library's own +// detection. Elsewhere only the dead zone changes. void jlJoystickReset(jlJoystickE js, uint8_t deadZone); #endif diff --git a/include/joey/palette.h b/include/joey/palette.h index aa817c7..39b9438 100644 --- a/include/joey/palette.h +++ b/include/joey/palette.h @@ -7,7 +7,9 @@ // Colors are 12-bit $0RGB (bits 11..8 red, 7..4 green, 3..0 blue). // The library forces color 0 of every palette to black; jlPaletteSet // silently masks that entry to $000 regardless of what the caller -// passed. See claudeDocs/DESIGN.md sections 5 and 10. +// passed, and jlSurfaceLoadFile does the same. Palette and SCB changes +// to the stage reach the display at the next jlStagePresent. See +// claudeDocs/DESIGN.md sections 5 and 10. #ifndef JOEYLIB_PALETTE_H #define JOEYLIB_PALETTE_H @@ -20,7 +22,9 @@ // uint16_t values in $0RGB format. void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16); -// Read all 16 colors of a palette into out16. +// Read all 16 colors of a palette into out16: the values set (color 0 +// forced to $000, bits 12-15 as passed), not what the display shows of +// them. A paletteIndex above 15 is ignored by both calls. void jlPaletteGet(const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16); // Assign a single palette index (0..15) to a single scanline (0..199). diff --git a/include/joey/platform.h b/include/joey/platform.h index b67dc94..2d10e67 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -71,88 +71,96 @@ // ----- Derived CPU markers ----- #if defined(JOEYLIB_PLATFORM_IIGS) - #define JOEYLIB_CPU_65816 - #define JOEYLIB_ENDIAN_LITTLE - #define JOEYLIB_NATIVE_CHUNKY - #define JOEYLIB_PLATFORM_NAME "Apple IIgs" + #define JL_CPU_65816 + #define JL_ENDIAN_LITTLE + #define JL_NATIVE_CHUNKY + #define JL_PLATFORM_NAME "Apple IIgs" #elif defined(JOEYLIB_PLATFORM_AMIGA) - #define JOEYLIB_CPU_68000 - #define JOEYLIB_ENDIAN_BIG - #define JOEYLIB_NATIVE_PLANAR - #define JOEYLIB_HAS_BLITTER - #define JOEYLIB_HAS_COPPER - #define JOEYLIB_PLATFORM_NAME "Commodore Amiga" + #define JL_CPU_68000 + #define JL_ENDIAN_BIG + #define JL_NATIVE_PLANAR + #define JL_HAS_BLITTER + #define JL_HAS_COPPER + #define JL_PLATFORM_NAME "Commodore Amiga" #elif defined(JOEYLIB_PLATFORM_ATARIST) - #define JOEYLIB_CPU_68000 - #define JOEYLIB_ENDIAN_BIG - #define JOEYLIB_NATIVE_PLANAR + #define JL_CPU_68000 + #define JL_ENDIAN_BIG + #define JL_NATIVE_PLANAR // JOEYLIB_ATARI_STE is the STe build (make ste): the same port using the - // BLiTTER, DMA sound and the 4-bit palette, for STe and Mega STe only. + // BLiTTER, DMA sound, the 4-bit palette and (1040 STe) the enhanced + // joystick ports, for STe and Mega STe only. #if defined(JOEYLIB_ATARI_STE) - #define JOEYLIB_PLATFORM_NAME "Atari STe" + #define JL_PLATFORM_NAME "Atari STe" #else - #define JOEYLIB_PLATFORM_NAME "Atari ST" + #define JL_PLATFORM_NAME "Atari ST" #endif #elif defined(JOEYLIB_PLATFORM_DOS) - #define JOEYLIB_CPU_X86 - #define JOEYLIB_ENDIAN_LITTLE - #define JOEYLIB_NATIVE_CHUNKY - #define JOEYLIB_PLATFORM_NAME "MS-DOS" + #define JL_CPU_X86 + #define JL_ENDIAN_LITTLE + #define JL_NATIVE_CHUNKY + #define JL_PLATFORM_NAME "MS-DOS" #elif defined(JOEYLIB_PLATFORM_X68000) // The stage is four separate 1bpp planes in main RAM (src/x68000/halPlanar.c, // the Amiga's layout), and a present copies its dirty rows into the text // plane at $E00000, which is four 1bpp planes too. The text plane has one // 16-colour palette, so per-line SCB palettes are reloaded as the beam scans // by a CRTC raster interrupt (src/x68000/x68kRasterIsr.s). - #define JOEYLIB_CPU_68000 - #define JOEYLIB_ENDIAN_BIG - #define JOEYLIB_NATIVE_PLANAR - #define JOEYLIB_PLATFORM_NAME "Sharp X68000" + #define JL_CPU_68000 + #define JL_ENDIAN_BIG + #define JL_NATIVE_PLANAR + #define JL_PLATFORM_NAME "Sharp X68000" #elif defined(JOEYLIB_PLATFORM_DVX) // The DOS stage (chunky 4bpp, x86 sprite JIT and tile walker from // src/x86/), shown in a DVX window, or on DVX's full screen, instead of // on the VGA. - #define JOEYLIB_CPU_X86 - #define JOEYLIB_ENDIAN_LITTLE - #define JOEYLIB_NATIVE_CHUNKY - #define JOEYLIB_PLATFORM_NAME "DVX" + #define JL_CPU_X86 + #define JL_ENDIAN_LITTLE + #define JL_NATIVE_CHUNKY + #define JL_PLATFORM_NAME "DVX" #elif defined(JOEYLIB_PLATFORM_HOSTED) // The generic chunky stage, resolved through its SCB palettes into a // window's RGB pixels by one backend (src/hosted/). Whatever CPU - // the host has: no machine code is generated, sprites are interpreted. + // the host has, sprites are interpreted -- except on Windows 3.x, whose + // 32-bit code runs on a 386 like DOS's: sprites are compiled to x86 code + // as on DOS (Win386's flat code and data selectors share one base, so the + // code arena runs as it is). #if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ - #define JOEYLIB_ENDIAN_BIG + #define JL_ENDIAN_BIG #else - #define JOEYLIB_ENDIAN_LITTLE + #define JL_ENDIAN_LITTLE + #endif + #define JL_NATIVE_CHUNKY + #if defined(JOEYLIB_HOSTED_WIN16) + #define JL_CPU_X86 + #else + #define JL_SPRITE_INTERPRETED #endif - #define JOEYLIB_NATIVE_CHUNKY - #define JOEYLIB_SPRITE_INTERPRETED #if defined(JOEYLIB_HOSTED_SDL3) && defined(__PSP__) - #define JOEYLIB_PLATFORM_NAME "PSP" + #define JL_PLATFORM_NAME "PSP" #elif defined(JOEYLIB_HOSTED_SDL3) - #define JOEYLIB_PLATFORM_NAME "SDL3" + #define JL_PLATFORM_NAME "SDL3" #elif defined(JOEYLIB_HOSTED_WIN32) - #define JOEYLIB_PLATFORM_NAME "Windows" + #define JL_PLATFORM_NAME "Windows" #elif defined(JOEYLIB_HOSTED_WIN16) - #define JOEYLIB_PLATFORM_NAME "Windows 3.x" + #define JL_PLATFORM_NAME "Windows 3.x" #elif defined(JOEYLIB_HOSTED_BE) && defined(__HAIKU__) - #define JOEYLIB_PLATFORM_NAME "Haiku" + #define JL_PLATFORM_NAME "Haiku" #elif defined(JOEYLIB_HOSTED_BE) - #define JOEYLIB_PLATFORM_NAME "BeOS" + #define JL_PLATFORM_NAME "BeOS" #else #error "JoeyLib: JOEYLIB_PLATFORM_HOSTED needs a backend, e.g. -DJOEYLIB_HOSTED_SDL3" #endif #elif defined(JOEYLIB_PLATFORM_BLANK) // Chunky little-endian is the simplest baseline for a new port: the generic // backend gives you fully-working software rendering out of the box. Adjust - // these (or switch to JOEYLIB_NATIVE_PLANAR) once you know your hardware. - #define JOEYLIB_ENDIAN_LITTLE - #define JOEYLIB_NATIVE_CHUNKY - #define JOEYLIB_SPRITE_INTERPRETED - #define JOEYLIB_PLATFORM_NAME "Blank Template" + // these (or switch to JL_NATIVE_PLANAR) once you know your hardware. + #define JL_ENDIAN_LITTLE + #define JL_NATIVE_CHUNKY + #define JL_SPRITE_INTERPRETED + #define JL_PLATFORM_NAME "Blank Template" #endif -// JOEYLIB_SPRITE_INTERPRETED: the port has no sprite code emitter (it links +// JL_SPRITE_INTERPRETED: the port has no sprite code emitter (it links // src/generic/spriteEmitStub.c), so every sprite is drawn by the // interpreter. A pre-compiled bank (.spc) still gives it the cels' pixels; // their machine code is skipped. @@ -193,7 +201,7 @@ #define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function // (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord, // the VBL heartbeat tick read -- no function override to register here.) - #define JL_HAS_WAIT_VBL // $C019 VBL poll, function + #define JL_HAS_WAIT_VBL // the VBL heartbeat count advancing, function // audio: every real port implements the full engine #define JL_HAS_AUDIO_INIT #define JL_HAS_AUDIO_SHUTDOWN @@ -205,7 +213,6 @@ #define JL_HAS_AUDIO_PLAY_SFX_STREAM_RAW #define JL_HAS_AUDIO_SET_PAN #define JL_HAS_AUDIO_STOP_SFX - #define JL_HAS_AUDIO_TONE #define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_TICK_REGISTER @@ -233,7 +240,8 @@ // Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts). #define JL_HAS_SERIAL_SET_RTS // save files + disk space: delete (GS/OS Destroy), dir-ensure (GS/OS Create - // $2001, storageType 13), disk-free (GS/OS Volume $2008 via prefix 0). + // $2001, storageType 13), disk-free (GS/OS Volume $2008 on prefix 1's + // volume, where SAVES/ is). #define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DELETE #define JL_HAS_DISK_FREE @@ -267,7 +275,7 @@ #define JL_HAS_SURFACE_ALLOC_PIXELS // amiga: NULL (planar storage), function #define JL_HAS_SURFACE_ALLOC_PORT_DATA // amiga AmigaPlanarT + chip planes, function #define JL_HAS_SURFACE_FREE_PORT_DATA // amiga FreeMem planes, function - #define JL_HAS_FRAME_HZ // PAL 50, function + #define JL_HAS_FRAME_HZ // 50 PAL / 60 NTSC, measured at init, function #define JL_HAS_FRAME_COUNT // VBL-server counter, function #define JL_HAS_WAIT_VBL // WaitTOF, function // audio: every real port implements the full engine @@ -280,7 +288,6 @@ #define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_SET_PAN #define JL_HAS_AUDIO_STOP_SFX - #define JL_HAS_AUDIO_TONE #define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_TICK_REGISTER @@ -305,6 +312,8 @@ #define JL_HAS_SERIAL_FLUSH // Application-driven DTR: how a program hangs up (jlSerialSetDtr). #define JL_HAS_SERIAL_SET_DTR + // Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts). + #define JL_HAS_SERIAL_SET_RTS // save files + disk space (CreateDir/DeleteFile/Info via dos.library) #define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DELETE @@ -341,7 +350,7 @@ #define JL_HAS_SURFACE_ALLOC_PIXELS // st: NULL (planar storage), function #define JL_HAS_SURFACE_ALLOC_PORT_DATA // st StPlanarT + interleaved buffer, function #define JL_HAS_SURFACE_FREE_PORT_DATA // st free buffer, function - #define JL_HAS_FRAME_HZ // PAL 50, function + #define JL_HAS_FRAME_HZ // the machine's 50 / 60 (71 mono), function #define JL_HAS_FRAME_COUNT // VBL-ISR counter, function #define JL_HAS_WAIT_VBL // frame-counter wait, function #define JL_HAS_MILLIS_ELAPSED // Supexec 200 Hz tick, function @@ -354,13 +363,13 @@ #define JL_HAS_AUDIO_PLAY_SFX #define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_STOP_SFX - #define JL_HAS_AUDIO_TONE #define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_TICK_REGISTER #define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_FRAME_TICK + #define JL_HAS_MOD_MIX_RUN // the module player's 8-bit mix, src/m68k/modMix.s // lifecycle / present / input (every real port implements) #define JL_HAS_INIT #define JL_HAS_SHUTDOWN @@ -382,6 +391,8 @@ #define JL_HAS_SERIAL_FLUSH // Application-driven DTR: how a program hangs up (jlSerialSetDtr). #define JL_HAS_SERIAL_SET_DTR + // Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts). + #define JL_HAS_SERIAL_SET_RTS // save files + disk space (POSIX mkdir/remove + GEMDOS Dfree) #define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DELETE @@ -395,7 +406,7 @@ // (The planar dual-write hooks are not linked to generics: every chunky port // elides them to ((void)0) in port.h's shared chunky block, PERF-AUDIT #42.) #define JL_HAS_FRAME_HZ // VGA ~70, function - #define JL_HAS_FRAME_COUNT // $3DA retrace edge counter, function + #define JL_HAS_FRAME_COUNT // from the millisecond clock (70 a second), function #define JL_HAS_WAIT_VBL // $3DA retrace poll, function #define JL_HAS_MILLIS_ELAPSED // uclock / audio-ISR tick, function // audio: every real port implements the full engine @@ -407,7 +418,6 @@ #define JL_HAS_AUDIO_PLAY_SFX #define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_STOP_SFX - #define JL_HAS_AUDIO_TONE #define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_TICK_REGISTER @@ -439,11 +449,9 @@ #define JL_HAS_SAVE_DELETE #define JL_HAS_DISK_FREE #elif defined(JOEYLIB_PLATFORM_X68000) - // Bring-up: platform SERVICES only (src/x68000/hal.c), plus the serial - // add-on (src/x68000/serial.c). Everything graphical comes from the portable - // generics, so the port renders correctly before any native primitive - // exists. Native ops arrive with the storage-model decision. - // Planar graphics, adapted from the Amiga (identical surface layout). + // Platform services in src/x68000/hal.c, the serial add-on in + // src/x68000/serial.c, and planar graphics adapted from the Amiga + // (identical surface layout, src/x68000/halPlanar.c). #define JL_HAS_SURFACE_CLEAR #define JL_HAS_DRAW_PIXEL #define JL_HAS_DRAW_LINE @@ -474,7 +482,7 @@ #define JL_HAS_SURFACE_FREE_PORT_DATA #define JL_HAS_INIT // _iocs_crtmod display bring-up #define JL_HAS_SHUTDOWN // restore the previous CRT mode - #define JL_HAS_PRESENT // expand the chunky stage into GVRAM + #define JL_HAS_PRESENT // copy the stage's dirty plane rows into the text plane #define JL_HAS_INPUT_INIT #define JL_HAS_INPUT_SHUTDOWN #define JL_HAS_INPUT_POLL // IOCS work area keys/mouse, PPI joystick ports @@ -489,7 +497,8 @@ #define JL_HAS_DISK_FREE #define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DIR_ENSURE - // Serial add-on: IOCS polled RS-232C, structurally the ST HAL. + // Serial add-on: RSDRV.SYS transmit and line setup, receive on the + // port's own SCC interrupt (src/x68000/x68kSccRxIsr.s). #define JL_HAS_SERIAL_OPEN #define JL_HAS_SERIAL_CLOSE #define JL_HAS_SERIAL_POLL @@ -499,17 +508,21 @@ #define JL_HAS_SERIAL_FLUSH // Application-driven DTR: how a program hangs up (jlSerialSetDtr). #define JL_HAS_SERIAL_SET_DTR + // Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts). + #define JL_HAS_SERIAL_SET_RTS // Audio. CHIP side on the YM2151 (OPM): tone voices + noise, which is what // the portable JYM1 tracker rides, with no CPU mixing. #define JL_HAS_AUDIO_INIT #define JL_HAS_AUDIO_SHUTDOWN - #define JL_HAS_AUDIO_TONE #define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_NOISE + #define JL_HAS_AUDIO_TICK_REGISTER #define JL_HAS_AUDIO_CRITICAL_ENTER #define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_FRAME_TICK - // SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX + #define JL_HAS_MOD_MIX_RUN // the module player's 8-bit mix, src/m68k/modMix.s + #define JL_HAS_CODE_WRITTEN // the X68030's caches, after codegen + // SAMPLE side (src/x68000/audioPcm.c): the module player + the shared 5-slot SFX // overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate // device from the OPM, so chip music and digital audio do not contend. #define JL_HAS_AUDIO_PLAY_MOD @@ -554,7 +567,6 @@ #define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_SET_PAN #define JL_HAS_AUDIO_STOP_SFX - #define JL_HAS_AUDIO_TONE #define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_TICK_REGISTER @@ -591,10 +603,10 @@ #define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DELETE #define JL_HAS_DISK_FREE - // audio: a software mixer (MOD through libxmp-lite, the SFX slots, three - // square-wave voices, YM-style noise and the speaker tone) run on the - // program's thread, so the tick callback and the stream fills run there - // too and the critical sections stay the generic no-ops. + // audio: a software mixer (MOD through the module player, the SFX slots, three + // square-wave voices and YM-style noise) run on the program's thread, so + // the tick callback and the stream fills run there too and the critical + // sections stay the generic no-ops. #define JL_HAS_AUDIO_INIT #define JL_HAS_AUDIO_SHUTDOWN #define JL_HAS_AUDIO_PLAY_MOD @@ -604,7 +616,6 @@ #define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_SET_PAN #define JL_HAS_AUDIO_STOP_SFX - #define JL_HAS_AUDIO_TONE #define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_TICK_REGISTER @@ -646,9 +657,9 @@ // ----- Library version ----- -#define JOEYLIB_VERSION_MAJOR 1 -#define JOEYLIB_VERSION_MINOR 0 -#define JOEYLIB_VERSION_PATCH 0 -#define JOEYLIB_VERSION_STRING "1.0.0" +#define JL_VERSION_MAJOR 1 +#define JL_VERSION_MINOR 0 +#define JL_VERSION_PATCH 0 +#define JL_VERSION_STRING "1.0.0" #endif diff --git a/include/joey/present.h b/include/joey/present.h index 729b43e..ee96b5a 100644 --- a/include/joey/present.h +++ b/include/joey/present.h @@ -5,11 +5,13 @@ // Stage present. // // jlStagePresent flips the library-owned stage (back-buffer) to the -// display. On chunky platforms (IIgs, DOS) this is a direct copy; on -// planar platforms (Amiga, Atari ST) this is a chunky-to-planar -// conversion. Drawing primitives mark per-row dirty ranges on the -// stage as a side effect, so jlStagePresent only touches rows that -// actually changed since the last present. See claudeDocs/DESIGN.md. +// display. Where the stage already has the display's layout it is a +// copy (the IIgs's 4bpp pixels; the planes of the Amiga, ST, and +// X68000); DOS, DVX, and the hosted ports expand each 4-bit pixel to +// their display's 8-bit or RGB pixels. Drawing primitives mark per-row +// dirty ranges on the stage as a side effect, so jlStagePresent only +// touches rows that actually changed since the last present. Palette and +// SCB changes reach the display at the present too. #ifndef JOEYLIB_PRESENT_H #define JOEYLIB_PRESENT_H @@ -23,11 +25,9 @@ // callers only need to call jlStagePresent at the end of a frame -- // everything they drew shows up. Rows that nothing touched since the // last present are skipped by the per-port present using the per-row -// dirty bands. -// -// To present a region you didn't draw with the standard primitives -// (e.g. direct framebuffer poking), call surfaceMarkDirtyRect on -// the same rect first, then jlStagePresent. +// dirty bands. Every public call that writes the stage's pixels marks +// them; the API has no other way to write them, and no call to mark +// rows by hand. void jlStagePresent(void); #endif diff --git a/include/joey/serial.h b/include/joey/serial.h index 099e174..533e5f3 100644 --- a/include/joey/serial.h +++ b/include/joey/serial.h @@ -18,14 +18,17 @@ // Atari ST -- BIOS Aux (Modem port) via XBIOS Rsconf; TOS buffers RX. // X68000 -- SCC channel A: Human68k's RSDRV.SYS for transmit and line // setup (75-38400 bps, RTS/CTS), REQUIRED -- the open fails -// without it; its own RX interrupt fills the ring. +// without it; its own RX interrupt fills the ring (when +// opened in supervisor mode, as jlInit leaves every program). +// DVX -- a COM port through DVX's serial library. +// Win32/16 -- a COM port through Windows' communications calls. +// SDL3, the web, the PSP, Haiku, and BeOS have no serial port. // // Model: non-blocking + buffered. Received bytes accumulate in a background -// ring (a hardware IRQ on DOS, the X68000 and the IIgs, the OS on Amiga/ST); -// jlSerialRead / jlSerialAvailable never block. Call jlSerialPoll() -// once per game frame -- it is a no-op where the IRQ/OS already fills the -// ring, and the hardware drain where it does not, so calling it every frame is -// always correct. Transmit is a bounded poll. +// ring (a hardware IRQ on DOS, the X68000 and the IIgs, the OS on Amiga/ST, +// DVX, and Windows); jlSerialRead / jlSerialAvailable never block. Call +// jlSerialPoll() once per game frame. Transmit never waits long: a bounded +// poll on most ports, a queue on the Amiga and Win16. // // Received bytes CAN be lost. Each port's receive hardware holds 1 to 3 bytes // (16 on a 16550), ~1-3 ms at 9600 baud, and anything that keeps interrupts off @@ -33,12 +36,12 @@ // open (on the IIgs it cuts its long interrupts-off stretches to ~1.7 ms while // one is), but these are not: IIgs disk access (GS/OS holds interrupts off // 65-220 ms per 3.5" floppy access, so every file call, asset load and -// jlAudioInit), IIgs jlAudioPlayMod (~12 ms per KB of samples), a Super Serial -// Card above 2400 baud, and the program's own interrupt masking. Disk access -// on the other ports is unmeasured. Schedule them between messages, pause the -// peer first (jlSerialSetRts(false) or XOFF), or use a protocol that -// retransmits. docs/ApiSerial.adoc, "What Can Lose Received Bytes", has the -// per-port table. +// jlInit, which reads the audio replayer), IIgs jlAudioPlayMod (~12 ms per +// KB of samples), a Super Serial Card above 2400 baud, and the program's own +// interrupt masking. Disk access on the other ports is unmeasured. Schedule +// them between messages, pause the peer first (jlSerialSetRts(false) or +// XOFF), or use a protocol that retransmits. docs/ApiSerial.adoc, "What Can +// Lose Received Bytes", has the per-port table. // // A failed jlSerialOpen is non-fatal: it returns false and every other call is // a safe no-op (Available 0, Read 0), exactly like a failed audio init. @@ -54,10 +57,13 @@ // JL_SERIAL_PRINTER -- IIgs printer port (SCC channel A). Only the IIgs has // a second built-in port; elsewhere == DEFAULT. // JL_SERIAL_SLOT -- a card/port selected by jlSerialConfigT.unit: IIgs -// Super Serial Card slot (1..7); DOS COM number (1..4); -// Amiga serial.device unit. -// JL_SERIAL_DEFAULT -- the platform's natural RS-232 port (IIgs modem, DOS -// COM1, ST Modem port, Amiga unit 0). +// Super Serial Card slot (1..7); DOS or DVX COM number +// (1..4); Win16 COM (1..9); Win32 COM (1..255); Amiga +// serial.device unit. The ST and X68000 have one port +// and open it for every device. +// JL_SERIAL_DEFAULT -- the platform's natural RS-232 port (IIgs modem, DOS, +// DVX, and Windows COM1, ST Modem port, Amiga unit 0, +// X68000 RS-232C). typedef enum { JL_SERIAL_DEFAULT = 0, JL_SERIAL_MODEM, @@ -78,24 +84,28 @@ typedef enum { } jlSerialFlowE; // Line configuration. `unit` disambiguates JL_SERIAL_SLOT (see jlSerialDeviceE) -// and is ignored for the other devices. +// and is ignored for the other devices. A field left 0 takes a default (baud +// 9600, 8 data bits, 1 stop bit); data bits outside 5..8, stop bits other +// than 1 or 2, or a parity or flow the enums do not name fail the open. typedef struct { - uint32_t baud; // 300 .. 115200 (each port clamps to what it supports) + uint32_t baud; // 300 .. 115200 (each port runs at the nearest rate it has: docs/ApiSerial.adoc) uint8_t dataBits; // 5 .. 8 uint8_t stopBits; // 1 or 2 jlSerialParityE parity; jlSerialFlowE flow; - uint8_t unit; // JL_SERIAL_SLOT: IIgs slot 1..7 / DOS COM 1..4 / Amiga unit + uint8_t unit; // JL_SERIAL_SLOT: IIgs slot 1..7 / DOS, DVX COM 1..4 / Win16 COM 1..9 / Win32 COM 1..255 / Amiga unit } jlSerialConfigT; // Open + configure the port. Only one port is open at a time; a second // jlSerialOpen without a jlSerialClose fails. Returns false (non-fatal) if the // port cannot be opened or the platform has no serial support -- every other -// call then no-ops. On the X68000 it also fails when RSDRV.SYS is not loaded, -// and jlLastError() says so. +// call then no-ops. It also fails, with jlLastError() saying why, when no UART +// answers at the DOS COM port, when no Super Serial Card answers in the IIgs +// slot, and when RSDRV.SYS is not loaded on the X68000. bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config); // Close the open port and release its resources. Safe to call when not open. +// Bytes jlSerialWrite accepted are still sent. // A port still open when the program exits is closed then (the first // successful jlSerialOpen registers an atexit handler): where the port's RX // interrupt is taken over (DOS, X68000, IIgs), the vector points into the @@ -109,10 +119,7 @@ bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config); // (Hayes S25: 50 ms by default), then close: the line stays down. void jlSerialClose(void); -// Pump the RX/TX buffers. Call once per game frame. Next to a no-op where a -// hardware IRQ or the OS already services the port; drains the hardware into -// the RX ring where nothing else does (an IIgs Super Serial Card whose -// interrupt switch is off). Calling it every frame is always correct. +// Pump the RX/TX buffers. Call once per game frame. void jlSerialPoll(void); // Bytes waiting in the RX buffer, readable now without blocking. @@ -123,7 +130,8 @@ uint16_t jlSerialAvailable(void); uint16_t jlSerialRead(uint8_t *buf, uint16_t max); // Send `len` bytes; returns the count accepted (may be < len if a flow- -// controlled peer or a full transmit path stalls). Transmit is a bounded poll. +// controlled peer or a full transmit path stalls). Never waits long: a +// bounded poll on most ports; the Amiga and Win16 queue instead. uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len); // Single-byte convenience. jlSerialReadByte returns -1 when none is ready. diff --git a/include/joey/sprite.h b/include/joey/sprite.h index 8fada6b..3957fd3 100644 --- a/include/joey/sprite.h +++ b/include/joey/sprite.h @@ -17,13 +17,15 @@ // // Performance contract: jlSpriteDraw should be at least as fast as the // IIgs reference (the DESIGN.md spec calls for runtime-compiled draw -// code per CPU). Every port has a runtime emitter (DOS x86, Amiga/ST/ -// X68000 68k planar, IIgs 65816); jlSpriteCompile compiles the shift/op -// variants into the codegen arena and the draw/save/restore calls -// dispatch to the compiled routines, falling back to the interpreter -// for clipped draws or uncompiled variants. -// jlSpritePrewarm compiles ahead of the first draw so the emit cost -// does not land mid-frame. +// code per CPU). The machine ports have runtime emitters (IIgs 65816, +// Amiga/X68000 68k planar, ST 68k interleaved, DOS/DVX x86), and so does +// Win16 (x86, as DOS); +// jlSpriteCompile compiles the shift/op variants into the codegen arena +// and the draw/save/restore calls dispatch to the compiled routines, +// falling back to the interpreter for clipped draws or uncompiled +// variants. Nothing compiles on demand: compile while loading, or every +// draw interprets. The other hosted ports (JL_SPRITE_INTERPRETED) have no +// emitter and always interpret. #ifndef JOEYLIB_SPRITE_H #define JOEYLIB_SPRITE_H @@ -49,7 +51,7 @@ typedef struct jlSpriteT jlSpriteT; // stack-allocated jlSpriteBackupT plus a caller-owned byte buffer keeps // the runtime allocation-free. typedef struct { - jlSpriteT *sprite; + jlSpriteT *sprite; int16_t x; int16_t y; uint16_t width; // pixels @@ -69,8 +71,7 @@ typedef struct { // Returns the number of cels actually written into outCels[]. Returns // 0 if the file is missing, the magic / target byte is wrong, or no // cel could be allocated. -uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, - uint16_t maxCels, uint16_t *outPalette); +uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette); // Load up to maxCels cels from a baked .spc PRE-COMPILED sprite bank // (tools/spritebake output for THIS platform). Identical contract to @@ -79,27 +80,29 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, // full compiled speed with NO runtime jlSpriteCompile. A cel that does not fit // the arena is loaded uncompiled (interpreter fallback), never an error -- so // this is a drop-in, faster-starting replacement for jlSpriteBankLoad + a -// jlSpriteCompile/Prewarm pass. Returns 0 if the file is missing or its magic -// / target / shift-count does not match this build. -uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, - uint16_t maxCels, uint16_t *outPalette); +// jlSpriteCompile pass. Returns 0 if the file is missing, its magic / +// target / shift-count does not match this build, its cel size is out of +// range, or the 64 KB buffer a cel's code is read into cannot be allocated +// (the interpreting ports read it too, then drop the code). +uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels, uint16_t maxCels, uint16_t *outPalette); // Write an already-compiled sprite bank to a .spc cache file (the runtime twin // of the offline tools/spritebake). Call after jlSpriteBankLoad + a // jlSpriteCompile pass so a later launch can jlSpriteBankLoadPrecompiled the // cache and skip the JIT -- i.e. SHIP the small cross-platform .spr, compile // once, and cache the (large) per-platform compiled bank on first run. `cels` -// are `count` sprites (uncompiled ones are cached as-is and JIT again next -// run); `palette` (or NULL) is embedded. Returns false if the destination -// isn't writable (e.g. a write-protected disk), which is harmless -- the app -// just keeps compiling at startup. -bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, - uint16_t count, const uint16_t *palette); +// are `count` sprites, all of one size (uncompiled ones are cached +// uncompiled, and nothing compiles them at load: jlSpriteCompile any whose +// jlSpriteCompiledSize is 0); `palette` (or NULL) is embedded. Returns false +// if the cels differ in size or the destination isn't writable (e.g. a +// write-protected disk), which is harmless -- the app just keeps compiling +// at startup. +bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels, uint16_t count, const uint16_t *palette); // Wrap a tile-data blob in a jlSpriteT. The tile data must outlive the -// jlSpriteT; we do not copy it. Returns NULL if widthTiles or -// heightTiles is 0, or if the codegen arena cannot fit a placeholder -// entry for this sprite. +// jlSpriteT; we do not copy it. Returns NULL if tileData is NULL, +// widthTiles or heightTiles is 0, or memory runs out. The codegen arena +// is not touched until jlSpriteCompile. jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles); // Release a jlSpriteT and any codegen entries cached for it. The tile @@ -107,12 +110,17 @@ jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t h // owns that buffer. void jlSpriteDestroy(jlSpriteT *sp); -// Compile the sprite's draw routines into the codegen arena. After -// this returns true, jlSpriteDraw uses the compiled fast path on -// platforms where the emitter is wired (all four ports). Returns -// false if the arena is full (caller may run jlSpriteCompact and -// retry), the platform doesn't have a real emitter yet, or the -// sprite has no source tile data. +// Compile the sprite's draw, save, and restore routines into the codegen +// arena. jlSpriteDraw never compiles on demand, so call this while +// loading (or load a pre-compiled bank). After it returns true the draw, +// save, and restore calls use the compiled routines that fit: a short +// arena keeps only the x-on-an-8-pixel-boundary draws on the Amiga, ST, +// and X68000, a routine over 16 KB is left out, and anything not compiled +// (and every clipped draw) interprets. Returns false if the arena is full +// even after compacting it (jlLastError says so), the sprite has no +// source tile data, the compiler's 64 KB working buffer cannot be +// allocated, no routine fits, or the port has no emitter +// (JL_SPRITE_INTERPRETED); the sprite still draws, interpreted. // // Idempotent: calling on a sprite that's already compiled is a // no-op and returns true. @@ -125,12 +133,6 @@ bool jlSpriteCompile(jlSpriteT *sp); // the actual per-cel cost of the current platform's emitter. uint32_t jlSpriteCompiledSize(const jlSpriteT *sp); -// Hint that this sprite will be drawn soon. Currently a wrapper -// around jlSpriteCompile that ignores the return value, kept for API -// symmetry with the rest of the library and for callers that don't -// care about compile success. -void jlSpritePrewarm(jlSpriteT *sp); - // Draw the sprite at pixel (x,y) on the destination surface. Pixels // equal to color 0 in the sprite source are skipped (transparent). // Off-surface portions are clipped. @@ -144,27 +146,34 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); // sprites and on ports without a compiled fast path. void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); -// Worst-case backup capacity for a widthTiles x heightTiles sprite at -// ANY x, on every port. The planar 68k compiled save/restore operates -// on the 16-pixel-group-aligned window covering the sprite (x rounded -// DOWN to a 16-px boundary, width UP), so the worst case is one extra -// 16-px group: (ceil(w/16) + 1) * 8 bytes per row. This also covers the -// chunky ports' (w/2 + 1) * h trivially. Give the backing array 2-byte -// alignment (e.g. uint16_t backing or __attribute__((aligned(2)))) -- -// odd buffer pointers route save/restore to the slower interpreted -// paths on planar ports (68000 word ops require even addresses). -#define JOEY_SPRITE_BACKUP_BYTES(_wT, _hT) \ - ((uint16_t)((((((uint16_t)(_wT) * 8u) + 15u) >> 4) + 1u) * 8u * ((uint16_t)(_hT) * 8u))) +// How many _type elements a backup buffer needs for a widthTiles x +// heightTiles sprite at ANY x, on every port (rounded up), so that +// +// static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)]; +// +// is big enough, and jlSpriteBackupLength(uint8_t, w, h) is the size in +// bytes (for jlAlloc, say). The worst case is the 16-pixel-group-aligned +// region covering the sprite (x rounded DOWN to a 16-px boundary, width +// UP): one extra 16-px group, (ceil(w/16) + 1) * 8 bytes per row. Give +// the buffer 2-byte alignment (a uint16_t array has it): on the Amiga, +// ST, and X68000 an odd address sends save and restore down a much +// slower path, and on the Amiga and ST it crashes the trusted variants. +#define jlSpriteBackupLength(_type, _wT, _hT) \ + ((uint16_t)((((((((uint16_t)(_wT) * 8u) + 15u) >> 4) + 1u) * 8u * ((uint16_t)(_hT) * 8u)) + sizeof(_type) - 1u) / sizeof(_type))) // Capture the destination region a subsequent jlSpriteDraw at the same -// (x,y) would write to. backup->bytes must have at least -// JOEY_SPRITE_BACKUP_BYTES(widthTiles, heightTiles) bytes of capacity -// for fully in-bounds draws; for clipped draws only the visible bytes -// are stored. The captured region's exact geometry is reported in -// backup->x/width/height/sizeBytes -- on planar ports (Amiga, ST) it is -// the 16-px-aligned window containing the sprite, so the region a -// caller must not invalidate before restoring is the RECORDED window, -// not just the sprite rect. +// (x,y) would write to. backup->bytes must have room for +// jlSpriteBackupLength(uint8_t, widthTiles, heightTiles) bytes for +// fully in-bounds draws; for clipped draws only the visible bytes are +// stored. The captured region's exact geometry is reported in +// backup->x/y/width/height/sizeBytes: x is rounded down to even on every +// port, a compiled save on the Amiga, ST, and X68000 takes the 16-px- +// aligned region containing the sprite, a compiled save on the IIgs only +// the rows holding an opaque pixel, and a clipped save the visible part. +// So the region a caller must not invalidate before restoring is the +// RECORDED region, not just the sprite rect. On the IIgs the buffer must +// not cross a 64 KB bank boundary. With backup->bytes NULL nothing is +// copied: the call only fills in the region a save would record. void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); // TRUSTED save-under (P7-1). Same trust contract as @@ -188,14 +197,18 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup); // * s and backup (and backup->sprite, backup->bytes) are non-NULL, // * backup was produced by a jlSpriteSaveUnder that took the COMPILED // path (a "trusted" backup: 16-px-aligned x/width on planar ports, -// even x/width on chunky, backup->height == the sprite's pixel -// height, buffer at least 2-byte aligned), -// * the recorded window is fully on the destination surface. +// even x/width on chunky, backup->height == the rows the compiled +// save covers -- the sprite's pixel height, or on the IIgs the rows +// holding an opaque pixel), +// * on the Amiga and ST, backup->bytes is at least 2-byte aligned (the +// trusted save and save-and-draw need that too), +// * the recorded region is fully on the destination surface. // It is the expert inner-loop entry for a game that pre-compiles its -// sprites (jlSpritePrewarm) and already clamps positions on-screen -- +// sprites (jlSpriteCompile) and already clamps positions on-screen -- // use jlSpriteRestoreUnder anywhere those invariants are not statically // guaranteed. Anything the trusted fast path does not recognize (an -// uncompiled shift, a non-stage / non-16-row window on the 68k ports) +// uncompiled shift, a non-stage / non-16-row region on the 68k and x86 +// ports) // falls back to the fully-validated jlSpriteRestoreUnder, so a // conservative caller degrades safely rather than corrupting -- but the // NULL / on-surface guarantees above are still the caller's. @@ -229,9 +242,10 @@ void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t // the source surface can be modified afterwards. Width and height // are in TILES (each tile = 8x8 pixels). x and y are in pixels and // must be aligned to a tile boundary (multiple of 8) on the source -// surface; misaligned coordinates return NULL. -jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y, - uint8_t widthTiles, uint8_t heightTiles); +// surface. Returns NULL if src is NULL, a dimension is 0, x or y is +// misaligned, the region is not wholly on the surface, or memory runs +// out. +jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y, uint8_t widthTiles, uint8_t heightTiles); // Defragment the codegen arena. Walks live sprite slots and // memmoves them down to consolidate free space; any holes left by @@ -245,7 +259,10 @@ void jlSpriteCompact(void); // Arena introspection. Used to gauge whether jlSpriteCompact is // worth running, or whether the codegenBytes budget needs to grow. // Free space is (Total - Used), but it may be fragmented across -// holes until jlSpriteCompact runs. +// holes until jlSpriteCompact runs. On the IIgs Total is what jlInit was +// granted, in blocks of at most 64 KB that compacting never merges (one +// sprite's code must fit in one). The interpreting ports still allocate +// the arena: Total reports it and Used stays 0. uint32_t jlSpriteCodegenBytesUsed(void); uint32_t jlSpriteCodegenBytesTotal(void); diff --git a/include/joey/surface.h b/include/joey/surface.h index 46f51a0..35b7c09 100644 --- a/include/joey/surface.h +++ b/include/joey/surface.h @@ -6,10 +6,11 @@ // // All surfaces are 320x200 16-color images. In-memory storage is // target-native: chunky 4bpp packed (two pixels per byte, high nibble -// is the left pixel) on IIgs and DOS; native planar (4 separate -// bitplanes on Amiga, word-interleaved planes on Atari ST) on the -// 68k ports. Public API entry points speak in color indices (0..15) -// regardless, so game code does not see the storage format. +// is the left pixel) on the IIgs, DOS, DVX, and the hosted ports; +// native planar (4 separate bitplanes on the Amiga and X68000, +// word-interleaved planes on the Atari ST) on the 68k ports. Public API +// entry points speak in color indices (0..15) regardless, so game code +// does not see the storage format. // // Each surface carries its own 200-entry SCB (scanline control byte) // table and a 16-by-16 $0RGB palette set. See claudeDocs/DESIGN.md section 6. @@ -22,14 +23,14 @@ // ----- Fixed surface dimensions (library contract) ----- -#define SURFACE_WIDTH 320 -#define SURFACE_HEIGHT 200 -#define SURFACE_BYTES_PER_ROW 160 -#define SURFACE_PIXELS_SIZE (SURFACE_BYTES_PER_ROW * SURFACE_HEIGHT) +#define JL_SURFACE_WIDTH 320 +#define JL_SURFACE_HEIGHT 200 +#define JL_SURFACE_BYTES_PER_ROW 160 +#define JL_SURFACE_PIXELS_SIZE (JL_SURFACE_BYTES_PER_ROW * JL_SURFACE_HEIGHT) -#define SURFACE_PALETTE_COUNT 16 -#define SURFACE_COLORS_PER_PALETTE 16 -#define SURFACE_PALETTE_ENTRIES (SURFACE_PALETTE_COUNT * SURFACE_COLORS_PER_PALETTE) +#define JL_SURFACE_PALETTE_COUNT 16 +#define JL_SURFACE_COLORS_PER_PALETTE 16 +#define JL_SURFACE_PALETTE_ENTRIES (JL_SURFACE_PALETTE_COUNT * JL_SURFACE_COLORS_PER_PALETTE) // ----- Opaque public type ----- @@ -57,16 +58,16 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src); // Copy a pixel rectangle from src into dst at the SAME position (no // repositioning, so planar ports never shift bits). The rect is // clipped to the surface, then widened OUTWARD to the 16-pixel group -// grid ON EVERY PORT -- the copied window is x rounded down and -// x + w rounded up to multiples of 16 (same window rule as +// grid ON EVERY PORT -- the copied region is x rounded down and +// x + w rounded up to multiples of 16 (same region rule as // jlSpriteSaveUnder), so up to 15 extra pixels on each side are // copied. Two consequences the caller owns: a snapped erase can // clobber on-screen content up to 15 px outside the requested rect, // and it silently invalidates any live jlSpriteBackupT whose -// recorded window it touches. Copies pixels only: SCBs and palettes +// recorded region it touches. Copies pixels only: SCBs and palettes // are untouched. The intended use is restoring background // rectangles from a pristine copy of the scene (the clean-buffer -// pattern) instead of per-sprite save/restore -- erase ALL windows, +// pattern) instead of per-sprite save/restore -- erase ALL regions, // then redraw ALL sprites; do not mix with backup-restore in the // same region. void jlSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h); @@ -80,8 +81,12 @@ void jlSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, int16_t x, int16_ bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path); // Read a surface from disk, overwriting dst. Returns false if the -// file is missing, the wrong size, or the read fails. dst keeps its -// identity (no reallocation). +// file is missing, the wrong size, has an SCB byte above 15, or the +// read fails; a read that fails part way can leave dst's pixels partly +// overwritten, but its SCBs and palettes are then untouched. Colour 0 +// of every palette loaded is set to black, as jlPaletteSet does. dst +// keeps its identity (no reallocation). Neither file call sets +// jlLastError. bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path); // 32-bit content hash of the surface. NOT FNV-1a: a two-stream 16-bit @@ -95,7 +100,7 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path); // low byte. // SURFACE_HASH_MIX_BYTE in src/core/surfaceInternal.h is the single // source of truth for the per-byte mix; a port override (the planar -// Amiga/ST readers) must reuse it byte-for-byte and feed the identical +// Amiga, ST, and X68000 readers) must reuse it byte-for-byte and feed the identical // byte sequence, so the same logical pixels produce the same hash on // every port regardless of internal storage format. Used by the UBER // validation harness to pixel-compare ports against an IIgs golden diff --git a/include/joey/tile.h b/include/joey/tile.h index a2019c0..f721adc 100644 --- a/include/joey/tile.h +++ b/include/joey/tile.h @@ -33,21 +33,23 @@ // ----- Constants ----- -#define TILE_PIXELS_PER_SIDE 8 -#define TILE_BYTES_PER_ROW 4 -#define TILE_BYTES (TILE_BYTES_PER_ROW * TILE_PIXELS_PER_SIDE) -#define TILE_BLOCKS_PER_ROW (SURFACE_WIDTH / TILE_PIXELS_PER_SIDE) -#define TILE_BLOCKS_PER_COL (SURFACE_HEIGHT / TILE_PIXELS_PER_SIDE) +#define JL_TILE_PIXELS_PER_SIDE 8 +#define JL_TILE_BYTES_PER_ROW 4 +#define JL_TILE_BYTES (JL_TILE_BYTES_PER_ROW * JL_TILE_PIXELS_PER_SIDE) +#define JL_TILE_BLOCKS_PER_ROW (JL_SURFACE_WIDTH / JL_TILE_PIXELS_PER_SIDE) +#define JL_TILE_BLOCKS_PER_COL (JL_SURFACE_HEIGHT / JL_TILE_PIXELS_PER_SIDE) // Sentinel for asciiMap entries that should not draw. jlDrawText -// advances the cursor past TILE_NO_GLYPH chars without writing. -#define TILE_NO_GLYPH ((uint16_t)0xFFFFu) +// advances the cursor past JL_TILE_NO_GLYPH chars without writing. +#define JL_TILE_NO_GLYPH ((uint16_t)0xFFFFu) // ----- Types ----- // Stack-allocated 32-byte snapshot buffer for jlTileSnap / jlTilePaste. +// Target-native: plane-major on the Amiga and X68000, row by row with each +// row's four plane bytes together on the ST, chunky elsewhere. typedef struct jlTileT { - uint8_t pixels[TILE_BYTES]; + uint8_t pixels[JL_TILE_BYTES]; } jlTileT; // ----- API ----- @@ -55,17 +57,14 @@ typedef struct jlTileT { // Copy the 8x8 block at (srcBx, srcBy) on src to (dstBx, dstBy) on // dst, opaque. Out-of-range block coordinates on either side are // silent no-ops; src and dst can be the same surface. -void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, - const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy); +void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy); // Like jlTileCopy but pixels equal to transparentIndex are skipped -- // the destination pixel keeps its original value. Use this for fonts // (transparentIndex = 0 leaves the page background showing through // glyph backgrounds) and for any tileset where some pixels are meant // to be see-through. -void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, - const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, - uint8_t transparentIndex); +void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex); // Fill the 8x8 block at (bx, by) with a solid color. Equivalent to // jlFillRect(s, bx*8, by*8, 8, 8, colorIndex) but skips the rect @@ -86,19 +85,19 @@ void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex); // // Returns the number of tiles actually written. Returns 0 if the // file is missing, the magic / target byte is wrong, or maxTiles is 0. -uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles, - bool *outValid, uint16_t *outPalette); +uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles, bool *outValid, uint16_t *outPalette); // Stream a baked .tbk straight onto a surface's 8x8 block grid: tile i -// lands at block (i % TILE_BLOCKS_PER_ROW, i / TILE_BLOCKS_PER_ROW), +// lands at block (i % JL_TILE_BLOCKS_PER_ROW, i / JL_TILE_BLOCKS_PER_ROW), // the same row-major layout the bake tools author against. Use this // instead of jlTileBankLoad + a jlTileT[] when the bank is too big for // a caller-side buffer -- a full 1000-tile font sheet needs 32000 -// bytes, which no IIgs example can heap-allocate (the clang libc heap -// is a few hundred bytes) or hold in bank-0 BSS. One file pass with a +// bytes, which no IIgs example can afford from the C heap (32 KB to +// 48 KB in all) or hold in bank-0 BSS. One file pass with a // small internal scratch; per-target/magic validation and outPalette // behave exactly like jlTileBankLoad. Blocks beyond the file's tile -// count keep their prior surface content. Returns tiles pasted. +// count keep their prior surface content; tiles past the surface's 1000 +// blocks are ignored. Returns tiles pasted. uint16_t jlTileBankLoadToSurface(const char *path, jlSurfaceT *dst, uint16_t *outPalette); // Capture the 8x8 block at (bx, by) into the caller's jlTileT. Used @@ -118,8 +117,7 @@ void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in); // to 4 bits. Opaque -- if you need transparent background, do // jlTileFill(bg) first then mask separately, or follow up with a // jlTileCopyMasked call. -void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, - const jlTileT *in, uint8_t fgColor, uint8_t bgColor); +void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor); // Paste an 8x8 ONE-BIT-PER-PIXEL glyph at block (bx, by): bits[row] is // that row of the glyph, most significant bit leftmost; a set bit draws @@ -129,13 +127,13 @@ void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, // This is the shape a character generator already has -- a font ROM, a // C64 charset, anything 1bpp -- so nothing has to expand it into a // jlTileT first. Prefer it over jlTilePasteMono for glyphs: a jlTileT -// is target-native (plane-major on the planar ports), whereas the +// is target-native (planar on the Amiga, ST, and X68000), whereas the // silhouette a mono paste wants is chunky, so feeding one a tile that -// came from jlTileSnap is a format mismatch that draws the wrong shape. +// came from jlTileSnap or jlTileBankLoad is a format mismatch there that +// draws the wrong shape. // Taking the bits directly makes the input unambiguous, and lets each // port expand them once instead of twice. -void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, - const uint8_t *bits, uint8_t fgColor, uint8_t bgColor); +void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor); // Draw a NUL-terminated ASCII string at block (bx, by) using glyphs @@ -143,32 +141,33 @@ void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, // // asciiMap is a 256-entry table mapping ASCII code to glyph location // on fontSurface, encoded as a packed uint16_t: low byte = source -// blockX, high byte = source blockY. asciiMap[c] == TILE_NO_GLYPH -// causes that character to be skipped (cursor advances, nothing -// drawn). Glyph color 0 is treated as transparent so the underlying -// surface shows through glyph backgrounds. +// blockX, high byte = source blockY. asciiMap[c] == JL_TILE_NO_GLYPH, +// or an entry off the 40x25 grid, causes that character to be skipped +// (cursor advances, nothing drawn). Glyph color 0 is treated as +// transparent so the underlying surface shows through glyph backgrounds. // // The cursor wraps to the next row at the right edge and truncates -// at the bottom edge. -void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, - const jlSurfaceT *fontSurface, const uint16_t *asciiMap, - const char *str); +// at the bottom edge. A start off the grid uses up characters without +// drawing them: a start column past 39 drops the first character and +// carries on at the start of the next row; a start row past 24 draws +// nothing. +void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontSurface, const uint16_t *asciiMap, const char *str); // Map entry value that leaves a cell untouched (nothing pasted, the -// destination shows through). Mirrors TILE_NO_GLYPH's role in +// destination shows through). Mirrors JL_TILE_NO_GLYPH's role in // jlDrawText. -#define TILE_MAP_SKIP ((uint8_t)0xFFu) +#define JL_TILE_MAP_SKIP ((uint8_t)0xFFu) // Paste a wTiles x hTiles rectangle of tiles in ONE call: the // validation, dispatch, and dirty union are paid once for the whole // map instead of per tile (the jlDrawText batching pattern // generalized, NATIVE-PERF Phase 3). `tiles` is the tileset; `map` // holds wTiles * hTiles tileset indices in row-major order -// (map[row * wTiles + col]). An index of TILE_MAP_SKIP leaves that +// (map[row * wTiles + col]). An index of JL_TILE_MAP_SKIP leaves that // cell untouched; any other out-of-tileset index is the CALLER'S // contract to avoid (the batch does not know the tileset length and // does not range-check it). The whole rectangle must fit on the -// surface -- a map whose edge crosses TILE_BLOCKS_PER_ROW/COL is +// surface -- a map whose edge crosses JL_TILE_BLOCKS_PER_ROW/COL is // rejected as a no-op, matching the per-tile ops' silent-reject // convention. wTiles or hTiles of 0 is a no-op. void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map); diff --git a/installer/components/amigaGcc.sh b/installer/components/amigaGcc.sh index b660135..1ea6b3d 100644 --- a/installer/components/amigaGcc.sh +++ b/installer/components/amigaGcc.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/amigaLowlevel.sh b/installer/components/amigaLowlevel.sh index 414daee..ce2034c 100644 --- a/installer/components/amigaLowlevel.sh +++ b/installer/components/amigaLowlevel.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/amigaSerial.sh b/installer/components/amigaSerial.sh index d99f5f4..93d605d 100644 --- a/installer/components/amigaSerial.sh +++ b/installer/components/amigaSerial.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/beosR5.sh b/installer/components/beosR5.sh index 8975878..e18fd5e 100644 --- a/installer/components/beosR5.sh +++ b/installer/components/beosR5.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/beosVm.sh b/installer/components/beosVm.sh index fc84ec5..5b98b77 100644 --- a/installer/components/beosVm.sh +++ b/installer/components/beosVm.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/cadius.sh b/installer/components/cadius.sh index c6a10db..0ec1dd6 100644 --- a/installer/components/cadius.sh +++ b/installer/components/cadius.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/cwsdpmi.sh b/installer/components/cwsdpmi.sh index ad2d871..a9ef4ff 100644 --- a/installer/components/cwsdpmi.sh +++ b/installer/components/cwsdpmi.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/djgpp.sh b/installer/components/djgpp.sh index 48389d3..42648cc 100644 --- a/installer/components/djgpp.sh +++ b/installer/components/djgpp.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/dosbox.sh b/installer/components/dosbox.sh index 0485624..7705297 100644 --- a/installer/components/dosbox.sh +++ b/installer/components/dosbox.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/dvx.sh b/installer/components/dvx.sh index 5736076..598df3e 100644 --- a/installer/components/dvx.sh +++ b/installer/components/dvx.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/elf2x68k.sh b/installer/components/elf2x68k.sh index 1dfd626..450fae2 100644 --- a/installer/components/elf2x68k.sh +++ b/installer/components/elf2x68k.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/emsdk.sh b/installer/components/emsdk.sh index 7a9408b..7f580e0 100644 --- a/installer/components/emsdk.sh +++ b/installer/components/emsdk.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/emutos.sh b/installer/components/emutos.sh index 38075e4..e480f06 100644 --- a/installer/components/emutos.sh +++ b/installer/components/emutos.sh @@ -1,10 +1,12 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT # # EmuTOS 1.4 (GPL): a free TOS replacement for the Atari ST emulator, the # 512 KB US-English image. The gates and joey-run boot it instead of -# Atari's copyrighted TOS. +# Atari's copyrighted TOS. The German image, from the same download, is the +# input gate's national keyboard (its Alt layer types @ [ ] { } \). compDescription="Atari ST firmware: EmuTOS 1.4 (free TOS replacement)" compGroups="emulator:atarist" @@ -14,27 +16,31 @@ compUrl="https://sourceforge.net/projects/emutos/files/emutos/1.4/emutos-512k-1. compSha256="1ef7bd25f61bcfc66d19debc2f0ebb0d5e5ba811ccd7f0fddfcb105bb72d1663" compPin="$compSha256" imgPath="$JOEY_FIRMWARE/atarist/emutos-512k.img" +imgPathDe="$JOEY_FIRMWARE/atarist/emutos-512k-de.img" compInstall() { local tmp tmp="$(freshBuildDir "$compId")" - unzip -oq "$(fetchFile "$compUrl" "$compSha256" emutos-512k-1.4.zip)" 'emutos-512k-1.4/etos512us.img' -d "$tmp" + unzip -oq "$(fetchFile "$compUrl" "$compSha256" emutos-512k-1.4.zip)" 'emutos-512k-1.4/etos512us.img' 'emutos-512k-1.4/etos512de.img' -d "$tmp" install -D -m 644 "$tmp/emutos-512k-1.4/etos512us.img" "$imgPath" + install -D -m 644 "$tmp/emutos-512k-1.4/etos512de.img" "$imgPathDe" dropBuildDir "$compId" } compInstalled() { - [ -f "$imgPath" ] + [ -f "$imgPath" ] && [ -f "$imgPathDe" ] } compVerify() { - [ "$(sha1sum "$imgPath" | cut -d' ' -f1)" = "7d28ee58f00854954073198983572e6921b87a21" ] + [ "$(sha1sum "$imgPath" | cut -d' ' -f1)" = "7d28ee58f00854954073198983572e6921b87a21" ] && + [ "$(sha1sum "$imgPathDe" | cut -d' ' -f1)" = "0a65f49d98896ad3e769be5f038a8d3086c00316" ] } compEnv() { envVar EMUTOS_IMG "$imgPath" + envVar EMUTOS_DE_IMG "$imgPathDe" } diff --git a/installer/components/fsuae.sh b/installer/components/fsuae.sh index caf2f6c..2fdccb0 100644 --- a/installer/components/fsuae.sh +++ b/installer/components/fsuae.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/furnace.sh b/installer/components/furnace.sh new file mode 100644 index 0000000..3a935f3 --- /dev/null +++ b/installer/components/furnace.sh @@ -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" +} diff --git a/installer/components/haikuGcc.sh b/installer/components/haikuGcc.sh index b554ccf..af51cae 100644 --- a/installer/components/haikuGcc.sh +++ b/installer/components/haikuGcc.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/haikuGcc2.sh b/installer/components/haikuGcc2.sh index 6c831ab..fc0309a 100644 --- a/installer/components/haikuGcc2.sh +++ b/installer/components/haikuGcc2.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/haikuTools.sh b/installer/components/haikuTools.sh index 17f5e84..ef6ec48 100644 --- a/installer/components/haikuTools.sh +++ b/installer/components/haikuTools.sh @@ -1,17 +1,19 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT # # Haiku's own build-host tools, built from the R1/beta6 source: package -# (unpacks .hpkg packages: the Haiku sysroots are made from them) and -# bfs_shell (reads and writes Be File System volumes: the Haiku VM images -# get their job runner through it, and the gates hand programs to the VM -# and take results back on a BFS disk). jam, which builds them, comes from -# Haiku's buildtools at the same release. Installed into -# toolchains/opt/haikuTools; bin/ holds wrappers that find the tools' +# (unpacks .hpkg packages: the Haiku sysroots are made from them), bfs_shell +# (reads and writes Be File System volumes: the Haiku VM images get their +# job runner through it, and the gates hand programs to the VM and take +# results back on a BFS disk), and xres (adds resources to a Haiku or BeOS +# executable: make/be.mk gives every program its icons with it). jam, which +# builds them, comes from Haiku's buildtools at the same release. Installed +# into toolchains/opt/haikuTools; bin/ holds wrappers that find the tools' # libraries. -compDescription="haiku: Haiku R1/beta6 host tools (package, bfs_shell; also read BeOS volumes)" +compDescription="haiku: Haiku R1/beta6 host tools (package, bfs_shell, xres; also read BeOS volumes)" compGroups="toolchain:haiku toolchain:haiku32 toolchain:beos emulator:haiku emulator:haiku32 emulator:beos" compRequires="host" compHostDeps="build-tools git python3 zlib-dev zstd-dev bison flex" @@ -19,8 +21,8 @@ compUrl="https://github.com/haiku/haiku.git" compCommit="f6d86031a107e16f4fff6814e9aea20a39450842" buildtoolsUrl="https://github.com/haiku/buildtools.git" buildtoolsCommit="bfa561e3708c0e2d0155fb403b988ba515c9b16d" -compPin="$compCommit:$buildtoolsCommit" -hostTools="bfs_shell package" +hostTools="bfs_shell package xres" +compPin="$compCommit:$buildtoolsCommit:$hostTools" compInstall() { @@ -43,7 +45,7 @@ compInstall() { # The tools keep the attributes of the host's files (which have none # here) in a folder named when they are built: theirs, not the build # tree's, which goes. - (cd "$src/haiku/generated" && PATH="$src/buildtools/jam/bin.linuxx86:$PATH" jam -q -j"$JOEY_JOBS" -sHAIKU_BUILD_ATTRIBUTES_DIR="$compDir/attributes" 'bfs_shell' 'package') + (cd "$src/haiku/generated" && PATH="$src/buildtools/jam/bin.linuxx86:$PATH" jam -q -j"$JOEY_JOBS" -sHAIKU_BUILD_ATTRIBUTES_DIR="$compDir/attributes" 'bfs_shell' 'package' 'xres') objects="$src/haiku/generated/objects/linux" rm -rf "$compDir" mkdir -p "$compDir/bin" "$compDir/libexec" "$compDir/lib" "$compDir/attributes" @@ -58,17 +60,23 @@ compInstall() { compInstalled() { - [ -x "$compDir/bin/bfs_shell" ] && [ -x "$compDir/bin/package" ] && [ -x "$compDir/libexec/bfs_shell" ] && [ -x "$compDir/libexec/package" ] && [ -d "$compDir/attributes" ] + local tool + for tool in $hostTools; do + [ -x "$compDir/bin/$tool" ] && [ -x "$compDir/libexec/$tool" ] || return 1 + done + [ -d "$compDir/attributes" ] } # Make a small BFS volume, write a file onto it and read it back; package -# starts. (Outputs are captured, not piped into grep: under pipefail a tool's -# own exit status, or SIGPIPE from an early grep, would fail the check.) +# and xres start. (Outputs are captured, not piped into grep: under pipefail +# a tool's own exit status, or SIGPIPE from an early grep, would fail the +# check.) compVerify() { local tmp local readBack local usage + local xresUsage tmp="$(mktemp -d)" printf 'JoeyLib\n' > "$tmp/in.txt" truncate -s 8M "$tmp/v.img" @@ -77,8 +85,9 @@ compVerify() { printf 'cp :%s /myfs/in.txt\nsync\nquit\n' "$tmp/in.txt" | "$compDir/bin/bfs_shell" "$tmp/v.img" > /dev/null 2>&1 || true readBack="$(printf 'cat /myfs/in.txt\nquit\n' | "$compDir/bin/bfs_shell" "$tmp/v.img" 2>&1 || true)" usage="$("$compDir/bin/package" 2>&1 || true)" + xresUsage="$("$compDir/bin/xres" -h 2>&1 || true)" rm -rf "$tmp" - [[ "$readBack" == *JoeyLib* && "$usage" == *"Usage: package"* ]] + [[ "$readBack" == *JoeyLib* && "$usage" == *"Usage: package"* && "$xresUsage" == *"Valid commands are"* ]] } diff --git a/installer/components/haikuVm.sh b/installer/components/haikuVm.sh index 0bd6ad9..5e77c3a 100644 --- a/installer/components/haikuVm.sh +++ b/installer/components/haikuVm.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/haikuVm32.sh b/installer/components/haikuVm32.sh index 0bfbc75..9ea64a0 100644 --- a/installer/components/haikuVm32.sh +++ b/installer/components/haikuVm32.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/hatari.sh b/installer/components/hatari.sh index 1e7279c..25c457d 100644 --- a/installer/components/hatari.sh +++ b/installer/components/hatari.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/headless.sh b/installer/components/headless.sh index 6d254c8..505ba52 100644 --- a/installer/components/headless.sh +++ b/installer/components/headless.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/host.sh b/installer/components/host.sh index 565d910..b3ddd70 100644 --- a/installer/components/host.sh +++ b/installer/components/host.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/iigsFirmware.sh b/installer/components/iigsFirmware.sh index f66d33e..2ebff3b 100644 --- a/installer/components/iigsFirmware.sh +++ b/installer/components/iigsFirmware.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/kickstart.sh b/installer/components/kickstart.sh index bcba908..ab566ca 100644 --- a/installer/components/kickstart.sh +++ b/installer/components/kickstart.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/kickstartA1200.sh b/installer/components/kickstartA1200.sh index e4281d1..24982ee 100644 --- a/installer/components/kickstartA1200.sh +++ b/installer/components/kickstartA1200.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/kickstartA4000.sh b/installer/components/kickstartA4000.sh index ee25fc4..3b49bed 100644 --- a/installer/components/kickstartA4000.sh +++ b/installer/components/kickstartA4000.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/kpcomm.sh b/installer/components/kpcomm.sh index b89e062..310268c 100644 --- a/installer/components/kpcomm.sh +++ b/installer/components/kpcomm.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/kpjoy.sh b/installer/components/kpjoy.sh index bce0705..1ee3c4c 100644 --- a/installer/components/kpjoy.sh +++ b/installer/components/kpjoy.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/libxmp.sh b/installer/components/libxmp.sh index 52d4b6e..68d8a06 100644 --- a/installer/components/libxmp.sh +++ b/installer/components/libxmp.sh @@ -1,12 +1,14 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT # # libxmp-lite 4.7.0: the MOD decoder the DOS, Atari ST, X68000, SDL3, PSP, -# web, Win16, Win32, Haiku and BeOS audio HALs compile in. Each port's -# makefile builds it from this source tree. +# web, Win16, Win32, Haiku and BeOS audio HALs compiled in until 2026-10-14, +# when JoeyLib's own module player (src/core/modPlayer.c) replaced it. No +# build uses it now; it stays a reference to compare that player against. -compDescription="audio: libxmp-lite 4.7.0 source (DOS, Atari ST, X68000, SDL3, PSP, web, Win16, Win32, Haiku, BeOS)" +compDescription="audio: libxmp-lite 4.7.0 source (no build links it; a reference for the module player)" compGroups="toolchain:dos toolchain:atarist toolchain:x68000 toolchain:sdl3 toolchain:psp toolchain:web toolchain:win16 toolchain:win32 toolchain:haiku toolchain:haiku32 toolchain:beos" compRequires="host" compHostDeps="tar" diff --git a/installer/components/llvm816.sh b/installer/components/llvm816.sh index e88121e..f3b6aaf 100644 --- a/installer/components/llvm816.sh +++ b/installer/components/llvm816.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/llvmMingw.sh b/installer/components/llvmMingw.sh index edec261..01ce8b6 100644 --- a/installer/components/llvmMingw.sh +++ b/installer/components/llvmMingw.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/mame.sh b/installer/components/mame.sh index 72cf7e7..5b55f02 100644 --- a/installer/components/mame.sh +++ b/installer/components/mame.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/merlin32.sh b/installer/components/merlin32.sh index be11840..bc01f71 100644 --- a/installer/components/merlin32.sh +++ b/installer/components/merlin32.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/mintGcc.sh b/installer/components/mintGcc.sh index 0d0bb13..78cd3ef 100644 --- a/installer/components/mintGcc.sh +++ b/installer/components/mintGcc.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/nasm.sh b/installer/components/nasm.sh index 978d718..cba5ac3 100644 --- a/installer/components/nasm.sh +++ b/installer/components/nasm.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/ntp.sh b/installer/components/ntp.sh index 779e560..e1aebe3 100644 --- a/installer/components/ntp.sh +++ b/installer/components/ntp.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/openWatcom.sh b/installer/components/openWatcom.sh index 9743fa5..a67b6ea 100644 --- a/installer/components/openWatcom.sh +++ b/installer/components/openWatcom.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/ppsspp.sh b/installer/components/ppsspp.sh index 3303e05..870d8c2 100644 --- a/installer/components/ppsspp.sh +++ b/installer/components/ppsspp.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/pspdev.sh b/installer/components/pspdev.sh index 7609d4f..eb8e6f8 100644 --- a/installer/components/pspdev.sh +++ b/installer/components/pspdev.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/ptplayer.sh b/installer/components/ptplayer.sh index 0c164a4..c0e51e9 100644 --- a/installer/components/ptplayer.sh +++ b/installer/components/ptplayer.sh @@ -1,25 +1,34 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT # # PTPlayer 6.4: Frank Wille's ProTracker replayer for the Amiga, used by -# the Amiga audio HAL. Aminet's URL is not versioned, so the pin is the -# archive's checksum. +# the Amiga audio HAL, with JoeyLib's patches (installer/patches/ptplayer), +# which make it play as Protracker 2.3 and the other ports' module player do. +# Aminet's URL is not versioned, so the pin is the archive's checksum. -compDescription="Amiga audio: PTPlayer 6.4 (ProTracker replayer)" +compDescription="Amiga audio: PTPlayer 6.4 (ProTracker replayer) + JoeyLib patch" compGroups="toolchain:amiga" compRequires="host" -compHostDeps="lhasa" +compHostDeps="lhasa patch" compUrl="http://aminet.net/mus/play/ptplayer.lha" compSha256="5ba1285e91413f6b6cb72768c0c31cff90029af6920870d9f589689aa2e2161a" -compPin="$compSha256" +patchDir="$JOEY_PATCHES/ptplayer" +# Editing a patch reinstalls PTPlayer, so the patches are part of the pin. +compPin="$compSha256 $(cat "$patchDir"/*.patch | sha256sum | cut -d' ' -f1)" compInstall() { local lha + local p lha="$(fetchFile "$compUrl" "$compSha256")" rm -rf "$compDir" extractArchive "$lha" "$compDir" + for p in "$patchDir"/*.patch; do + info "applying $(basename "$p")" + patch -d "$compDir" -p1 -s -F0 --no-backup-if-mismatch < "$p" + done } @@ -28,8 +37,9 @@ compInstalled() { } +# The patch is in: its volume slide that leaves the period alone. compVerify() { - grep -qi 'ptplayer' "$compDir/ptplayer.asm" + grep -q 'mt_volslide_noper' "$compDir/ptplayer.asm" } diff --git a/installer/components/qemu.sh b/installer/components/qemu.sh index 87b6a5e..b5b86b2 100644 --- a/installer/components/qemu.sh +++ b/installer/components/qemu.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/sdl3.sh b/installer/components/sdl3.sh index 8dc492d..e8071dd 100644 --- a/installer/components/sdl3.sh +++ b/installer/components/sdl3.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/sdl3Psp.sh b/installer/components/sdl3Psp.sh index 9635312..30e739e 100644 --- a/installer/components/sdl3Psp.sh +++ b/installer/components/sdl3Psp.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/sdl3Web.sh b/installer/components/sdl3Web.sh index 58863ef..b516b40 100644 --- a/installer/components/sdl3Web.sh +++ b/installer/components/sdl3Web.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/win311.sh b/installer/components/win311.sh index bec5c31..461bd87 100644 --- a/installer/components/win311.sh +++ b/installer/components/win311.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/wine.sh b/installer/components/wine.sh index 3a78e2c..1bc3821 100644 --- a/installer/components/wine.sh +++ b/installer/components/wine.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/wing.sh b/installer/components/wing.sh new file mode 100644 index 0000000..c8921bf --- /dev/null +++ b/installer/components/wing.sh @@ -0,0 +1,62 @@ +# 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 +# +# Microsoft WinG 1.0, the fast DIB-to-screen blitter for Windows 3.1 that +# the win16 port uses when it is installed (src/hosted/win16): its runtime +# (WING.DLL, WINGDE.DLL, WINGDIB.DRV, WINGPAL.WND, and the DVA.386 virtual +# device) taken out of the WinG SDK, WING10.EXE, into toolchains/opt/wing; +# scripts/lib/win16Gate.sh installs it in each run's Windows as WinG's own +# setup would. The SDK's license lets you use it to develop and test +# software, and lets a program ship those runtime files only as part of +# itself (its LICENSE.TXT, kept beside them); it is not JoeyLib's to hand +# out, so it is a private file: ./install.sh import WING10.EXE. Microsoft +# no longer offers it; archive.org keeps a copy (archive.org/details/WING10). +# Optional (the port draws through GDI without it), so no group installs it: +# ./install.sh install wing. + +compDescription="win16: Microsoft WinG 1.0 runtime for its Windows (optional; private: import WING10.EXE)" +compGroups="optional" +compRequires="win311" +compHostDeps="python3 unzip" +compPrivateFile="toolchains/firmware/win16/WING10.EXE" +compPin="$(privateSha1For "$compPrivateFile")" + +wingRuntime="WING.DLL WINGDE.DLL WINGDIB.DRV WINGPAL.WND DVA.386" + + +compInstall() { + local tmp + local file + privateEnsure "$compPrivateFile" + tmp="$(mktemp -d)" + unzip -qq -o "$JOEY_REPO/$compPrivateFile" -d "$tmp" || { rm -rf "$tmp"; die "WING10.EXE did not unpack"; } + rm -rf "$compDir" + mkdir -p "$compDir" + # The SDK's compressed names end in _ where the last letter was. + for file in $wingRuntime; do + python3 "$JOEY_REPO/tools/szddExpand.py" "$tmp/${file%?}_" "$compDir/$file" || { rm -rf "$tmp"; die "$file did not expand"; } + done + cp "$tmp/LICENSE.TXT" "$compDir/LICENSE.TXT" + rm -rf "$tmp" +} + + +compInstalled() { + [ -s "$compDir/WING.DLL" ] && [ -s "$compDir/DVA.386" ] +} + + +compVerify() { + local file + for file in $wingRuntime; do + [ "$(head -c 2 "$compDir/$file")" = "MZ" ] || return 1 + done + LC_ALL=C grep -q -a 'WINGDE' "$compDir/WING.DLL" +} + + +compEnv() { + envVar WING_DIR "$compDir" +} diff --git a/installer/components/x68kCgrom.sh b/installer/components/x68kCgrom.sh index 6c35df9..aef5d92 100644 --- a/installer/components/x68kCgrom.sh +++ b/installer/components/x68kCgrom.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/components/x68kFirmware.sh b/installer/components/x68kFirmware.sh index 06d9e36..45d5338 100644 --- a/installer/components/x68kFirmware.sh +++ b/installer/components/x68kFirmware.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/files.sha1 b/installer/files.sha1 index 80eb868..4a63fd6 100644 --- a/installer/files.sha1 +++ b/installer/files.sha1 @@ -14,4 +14,5 @@ be25df4493d2d7bf296e640c2eb878779c7c904e toolchains/firmware/amiga/Libs/lowle 6cab1d3f694b75cb185273619e2bfa4484d4b051 toolchains/firmware/win16/s3/S3TRIO.DRV S3 Trio64V+ 1.70.04 Windows 3.1 display driver f432e53217965941e69a65345b8c2f9c88c1a1bc toolchains/firmware/win16/s3/VDDS3764.386 S3 Trio64V+ 1.70.04 virtual display device da1dc6518baa8f91de1210dd2630b6fec19d76f2 toolchains/firmware/win16/s3/VGA_ENG.3GR S3 Trio64V+ 1.70.04 DOS-box grabber +f27081b91fabd8202ec48eb32e17c0a1dd3a0e84 toolchains/firmware/win16/WING10.EXE Microsoft WinG 1.0 SDK (self-extracting zip, 856034 bytes) 2158c54a8be0a979e4c551b58fbacc0c89e84fd4 toolchains/firmware/beos/beos5pro.img BeOS 5 Professional install CD (CloneCD .img, raw sectors, 717056592 bytes) diff --git a/installer/lib/beos.sh b/installer/lib/beos.sh index 118b70b..2414c5e 100644 --- a/installer/lib/beos.sh +++ b/installer/lib/beos.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/lib/bundle.sh b/installer/lib/bundle.sh index dd705f2..02eb262 100644 --- a/installer/lib/bundle.sh +++ b/installer/lib/bundle.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/lib/common.sh b/installer/lib/common.sh index bb3140a..be62c0c 100644 --- a/installer/lib/common.sh +++ b/installer/lib/common.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/lib/config.sh b/installer/lib/config.sh index 4268d52..4028dc0 100644 --- a/installer/lib/config.sh +++ b/installer/lib/config.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/lib/distro.sh b/installer/lib/distro.sh index 826c798..62f2540 100644 --- a/installer/lib/distro.sh +++ b/installer/lib/distro.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/lib/haiku.sh b/installer/lib/haiku.sh index 3f3c389..665d199 100644 --- a/installer/lib/haiku.sh +++ b/installer/lib/haiku.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/lib/win16.sh b/installer/lib/win16.sh index c9f9d36..2419f9b 100644 --- a/installer/lib/win16.sh +++ b/installer/lib/win16.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/lib/winDriver.sh b/installer/lib/winDriver.sh index 6cb17ca..e93dafc 100644 --- a/installer/lib/winDriver.sh +++ b/installer/lib/winDriver.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT diff --git a/installer/patches/ptplayer/0001-protracker-2.3-effects.patch b/installer/patches/ptplayer/0001-protracker-2.3-effects.patch new file mode 100644 index 0000000..cbbb82f --- /dev/null +++ b/installer/patches/ptplayer/0001-protracker-2.3-effects.patch @@ -0,0 +1,225 @@ +--- a/ptplayer.asm ++++ b/ptplayer.asm +@@ -2495,6 +2495,10 @@ + mt_arpeggio: + ; cmd 0 x y (x = first arpeggio offset, y = second arpeggio offset) + ; d4 = xy ++; As Protracker 2.3: the period, wherever a slide has left it, is looked ++; up in the channel's finetune row, and the note x or y above taken from ++; there -- past B-3, from the next finetune's row, which the table runs on ++; into. (Counting from n_noteoff instead kept the note from before a slide.) + + moveq #0,d0 + move.b mt_Counter(a4),d0 +@@ -2509,16 +2513,26 @@ + ; step 2, arpeggio by right nibble + .1: and.w #$000f,d4 + +- ; offset current note ++ ; find the period in the tuned table: 37 entries, as Protracker reads + .2: add.w d4,d4 +- add.w n_noteoff(a2),d4 +- cmp.w #2*36,d4 ++ lea mt_PeriodTableEnd(pc),a1 ++ move.l n_pertab(a2),a0 ++ move.w n_period(a2),d1 ++ moveq #37-1,d0 ++.3: cmp.l a1,a0 ++ bhs .5 ++ cmp.w (a0),d1 + bhs .4 ++ addq.l #2,a0 ++ dbf d0,.3 ++ rts + +- ; set period with arpeggio offset from note table +- move.l n_pertab(a2),a0 +- move.w (a0,d4.w),AUDPER(a5) +-.4: rts ++ ; set period with arpeggio offset, unless it runs off the table ++.4: add.w d4,a0 ++ cmp.l a1,a0 ++ bhs .5 ++ move.w (a0),AUDPER(a5) ++.5: rts + + arptab: + dc.b 0,1,-1,0,1,-1,0,1,-1,0,1,-1,0,1,-1,0 +@@ -2689,7 +2703,7 @@ + ; cmd 5 x y (x = volume-up, y = volume-down) + ; d4 = xy + +- pea mt_volumeslide(pc) ++ pea mt_volslide_noper(pc) + bra mt_toneporta_nc + + +@@ -2698,12 +2712,13 @@ + ; d4 = xy + + move.w d4,d3 ++ moveq #0,d2 ; the depth indexes a table: no stale high byte + move.b n_vibratoamp(a2),d2 + move.b n_vibratospd(a2),d4 + bsr mt_vibrato_nc + + move.w d3,d4 +- bra mt_volumeslide ++ bra mt_volslide_noper + + + mt_tremolo: +@@ -2723,47 +2738,53 @@ + bra .4 + .3: move.b n_tremolospd(a2),d4 + +- ; calculate tremolo table offset: 64 * amplitude + (pos & 63) +-.4: lsl.w #6,d2 +- moveq #63,d0 +- and.b n_tremolopos(a2),d0 +- add.w d0,d2 +- +- ifne ENABLE_SAWRECT +- ; select tremolo waveform ++ ; As Protracker 2.3: twice a vibrato's depth (the wave times y, over ++ ; 64), and the saw ramping as the channel's VIBRATO position says. ++.4: moveq #31,d0 ++ and.b n_tremolopos(a2),d0 ; d0 = position in the wave's half + moveq #3,d1 + and.b n_tremoloctrl(a2),d1 + beq .6 ++ ++ lsl.b #3,d0 ; saw and square: the ramp's step + subq.b #1,d1 +- beq .5 ++ bne .5 + +- ; ctrl 2 & 3 select a rectangle tremolo +- lea mt_VibratoRectTable(pc),a0 ++ ; ctrl 1 selects a saw, falling in the vibrato's negative half ++ btst #5,n_vibratopos(a2) ++ beq .9 ++ not.b d0 ; 255 - ramp + bra .9 + +- ; ctrl 1 selects a sawtooth tremolo +-.5: lea mt_VibratoSawTable(pc),a0 ++ ; ctrl 2 & 3 select a square ++.5: st d0 + bra .9 +- endc ; ENABLE_SAWRECT + +- ; ctrl 0 selects a sine tremolo +-.6: lea mt_VibratoSineTable(pc),a0 ++ ; ctrl 0 selects a sine ++.6: move.b mt_ProTrackerSine(pc,d0.w),d0 + +- ; add tremolo-offset to volume +-.9: move.w n_volume(a2),d0 +- add.b (a0,d2.w),d0 +- bpl .10 +- moveq #0,d0 +-.10: cmp.w #64,d0 ++ ; scale by the depth, then add or subtract by the position's half ++.9: and.w #$00ff,d0 ++ mulu d2,d0 ++ lsr.w #6,d0 ++ move.w n_volume(a2),d1 ++ btst #5,n_tremolopos(a2) ++ beq .10 ++ sub.w d0,d1 ++ bpl .11 ++ moveq #0,d1 ++ bra .11 ++.10: add.w d0,d1 ++ cmp.w #64,d1 + bls .11 +- moveq #64,d0 ++ moveq #64,d1 + .11: move.w n_period(a2),AUDPER(a5) + ifeq MINIMAL + move.l mt_MasterVolTab(a4),a0 +- move.b (a0,d0.w),d0 +- and.b n_enable(a2),d0 ++ move.b (a0,d1.w),d1 ++ and.b n_enable(a2),d1 + endc +- move.w d0,AUDVOL(a5) ++ move.w d1,AUDVOL(a5) + ifd VOL0_UNLOOPS + bne .12 + move.b d7,n_looped(a2) +@@ -2773,6 +2794,12 @@ + .12: add.b d4,n_tremolopos(a2) + rts + ++mt_ProTrackerSine: ++ ; Protracker's mt_VibratoTable: the first half of the sine ++ dc.b 0,24,49,74,97,120,141,161,180,197,212,224,235,244,250,253 ++ dc.b 255,253,250,244,235,224,212,197,180,161,141,120,97,74,49,24 ++ even ++ + + mt_volumeslide: + ; cmd A x y (x = volume-up, y = volume-down) +@@ -2815,6 +2842,34 @@ + .1: rts + + ++mt_volslide_noper: ++; The volume slide of 5xy and 6xy, and EAx/EBx's: as mt_volumeslide but ++; leaving Paula's period alone, as Protracker 2.3 does -- setting it back to ++; n_period undid 6xy's vibrato, 5xy's glissando, and a tick of either. ++; d4 = xy ++ ++ move.w n_volume(a2),d0 ++ moveq #$0f,d1 ++ and.b d4,d1 ++ lsr.b #4,d4 ++ beq vol_slide_down_np ++ ++ ; slide up, until 64 ++ add.b d4,d0 ++vol_slide_up_np: ++ cmp.b #64,d0 ++ bls set_vol ++ moveq #64,d0 ++ bra set_vol ++ ++ ; slide down, until 0 ++vol_slide_down_np: ++ sub.b d1,d0 ++ bpl set_vol ++ moveq #0,d0 ++ bra set_vol ++ ++ + mt_posjump: + ; cmd B x y (xy = new song position) + ; d4 = xy +@@ -3079,7 +3134,7 @@ + rts + + .1: add.w n_volume(a2),d0 +- bra vol_slide_up ++ bra vol_slide_up_np + + + mt_volfinedn: +@@ -3092,7 +3147,7 @@ + + .1: move.b d0,d1 + move.w n_volume(a2),d0 +- bra vol_slide_down ++ bra vol_slide_down_np + + + mt_notecut: +@@ -3448,6 +3503,7 @@ + dc.w 862,814,768,725,684,646,610,575,543,513,484,457 + dc.w 431,407,384,363,342,323,305,288,272,256,242,228 + dc.w 216,203,192,181,171,161,152,144,136,128,121,114 ++mt_PeriodTableEnd: + + ifeq MINIMAL + MasterVolTab0: diff --git a/installer/patches/ptplayer/README.md b/installer/patches/ptplayer/README.md new file mode 100644 index 0000000..522232d --- /dev/null +++ b/installer/patches/ptplayer/README.md @@ -0,0 +1,17 @@ +# PTPlayer 6.4 patches + +install.sh (component `ptplayer`) applies these to PTPlayer 6.4's +`ptplayer.asm` in filename order as it installs it. The Amiga plays modules +through PTPlayer and every other port through JoeyLib's module player +(`src/core/modPlayer.c`, PTPlayer's replay translated to C), so these keep +the Amiga's playback the same as theirs. + +| Patch | What it fixes | Needed by | +|---|---|---| +| 0001-protracker-2.3-effects | Four places PTPlayer strays from Protracker 2.3, whose behaviour the module player keeps: its volume slide sets Paula's period back to the note's, which is right for `Axy` but undid `6xy`'s vibrato, `5xy`'s glissando, and a tick of either under `EAx`/`EBx`; its arpeggio counts from the note's index, not the period a slide has left (and never reads on into the next finetune's periods past B-3); and its tremolo is half Protracker's depth, with its saw ramping by its own position rather than the vibrato's. And `6xy` read its vibrato depth into a register whose high byte held whatever came before, so the vibrato read past its table (hidden while the period was set back each tick). | the Amiga's module playback; `tests/host/modPlayerHost.c` checks the module player's side | + +## License + +The patch modifies PTPlayer and contains its code, so it is under +PTPlayer's terms -- public domain (the archive's `license` file: the +Unlicense) -- not JoeyLib's MIT License. diff --git a/make/amiga.mk b/make/amiga.mk index 85721b0..ad62d51 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -43,25 +43,20 @@ PORT_C_SRCS := $(wildcard $(SRC_DIR)/amiga/*.c) PORT_S_SRCS := $(wildcard $(SRC_DIR)/amiga/*.s) SHARED_S := $(wildcard $(SRC_68K)/*.s) -# Amiga uses PTPlayer's mt_playfx for SFX, not the libxmp+overlay -# combo that DOS and ST share, so the shared SFX overlay mixer in -# src/core/audioSfxMix.c is unused here. Filter it out to keep dead -# code out of every Amiga binary. -CORE_C_SRCS_AMIGA := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS)) - -# Sprite codegen: the Amiga-planar emitter (spriteEmitPlanar68k.c) lives in -# src/m68k/ and builds into obj/68k/; only the CPU-independent dispatch stays -# in src/codegen. +# Sprite codegen: the Amiga-planar emitter (spriteEmitPlanar68k.c) and the +# planar sprite interpreter (planarSprite.c) live in src/m68k/ and build into +# obj/68k/; only the CPU-independent dispatch stays in src/codegen. CODEGEN_DIR := $(REPO_DIR)/src/codegen LIB_OBJS := \ - $(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS_AMIGA)) \ + $(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS_NO_MIX)) \ $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \ $(patsubst $(SRC_DIR)/amiga/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \ $(patsubst $(SRC_DIR)/amiga/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ $(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \ $(BUILD)/obj/port/ptplayer.o \ $(BUILD)/obj/68k/spriteEmitPlanar68k.o \ + $(BUILD)/obj/68k/planarSprite.o \ $(BUILD)/obj/codegen/spriteCompile.o \ $(BUILD)/obj/codegen/spriteStage.o @@ -120,11 +115,16 @@ $(LIB): $(LIB_OBJS) # ---- programs ----------------------------------------------------------------- # A program is a mixed-case Name with no extension (an AmigaDOS name), in bin/ -# or in its own bin/Name/ with its DATA beside it; joey-run boots the folder -# as the JOEYLIB hard drive. +# or in its own bin/Name/ with its DATA beside it, and Name.info beside it, +# its Workbench icon; joey-run boots the folder as the JOEYLIB hard drive. PROGRAM_EXT := PROGRAM_FOLDERS := yes programNameRule = $(call capFirst,$(call lc,$(1))) +programTarget = $(call programBin,$(1)) $(call programBin,$(1)).info + +# The stack a program started from its icon gets, in bytes: what the gates' +# shells give every program (stack 32768). +AMIGA_ICON_STACK := 32768 # One program, $(1) its id: compiled and linked in one step with the library. # gcc's -MMD leaves Name.d beside it, for header edits. @@ -133,6 +133,10 @@ $(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) @mkdir -p $$(dir $$@) $(AMIGA_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(LIB) -o $$@ $(LDFLAGS) $(call programVar,$(1),LDFLAGS) -include $(call programBin,$(1)).d + +$(call programBin,$(1)).info: $(call programIcon,$(1)) $(ICON_BAKE_DEPS) + @mkdir -p $$(dir $$@) + $(ICON_BAKE) info $$< $$@ $(AMIGA_ICON_STACK) endef # ---- JoeyLib's own build: its examples ------------------------------------------ diff --git a/make/atarist.mk b/make/atarist.mk index 3929c42..b31baca 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -17,7 +17,6 @@ PLATFORM := $(or $(ATARI_VARIANT),atarist) include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk $(call requireTool,$(ST_CC),toolchain:atarist) -$(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:atarist) ST_SHIM := $(REPO_DIR)/make/support/atarist/include-shim @@ -29,30 +28,17 @@ STE_C_SRCS := $(wildcard $(STE_SRC_DIR)/*.c) endif # so flag experiments never edit a tracked file (perf-hunt-2 rank 17). -CFLAGS := $(COMMON_CFLAGS) $(STE_CFLAGS) -m68000 -fomit-frame-pointer -I$(LIBXMP_DIR)/include -I$(ST_SHIM) -I$(SRC_DIR)/atarist -I$(SRC_68K) -MMD -MP +CFLAGS := $(COMMON_CFLAGS) $(STE_CFLAGS) -m68000 -fomit-frame-pointer -I$(ST_SHIM) -I$(SRC_DIR)/atarist -I$(SRC_68K) -MMD -MP LDFLAGS := -# libxmp-lite shared with the DOS port. Built as a static archive that -# provides the Protracker pattern decoder for the ST audio HAL; the -# 68k 4-channel mixer + YM2149 PWM output stage layer on top. -# LIBXMP_DIR comes from the installer's config.mk. -LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c)) -LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite -LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC)) $(LIBXMP_OBJDIR)/math_shim.o -LIBXMP_AR := $(LIBDIR)/libxmplite.a -# LIBXMP_CORE_DISABLE_IT skips the IT-format filter code so libxmp-lite -# doesn't reach for math.h / libm -- mintlib doesn't ship either. We -# target MOD as the authoring format anyway, so dropping IT filter -# support has no impact on JoeyLib's audio. -LIBXMP_CFLAGS := -DLIBXMP_CORE_PLAYER -DLIBXMP_CORE_DISABLE_IT -DHAVE_FNMATCH=0 -I$(LIBXMP_DIR)/include -I$(LIBXMP_DIR)/include/libxmp-lite -I$(LIBXMP_DIR)/src -Wno-error -w - PORT_C_SRCS := $(wildcard $(SRC_DIR)/atarist/*.c) PORT_S_SRCS := $(wildcard $(SRC_DIR)/atarist/*.s) SHARED_S := $(wildcard $(SRC_68K)/*.s) # Sprite codegen: the ST-interleaved emitter (spriteEmitInterleaved68k.c) -# lives in src/m68k/ and builds into obj/68k/; only the CPU-independent -# dispatch stays in src/codegen. +# and the planar sprite interpreter (planarSprite.c) live in src/m68k/ and +# build into obj/68k/; only the CPU-independent dispatch stays in +# src/codegen. CODEGEN_DIR := $(REPO_DIR)/src/codegen LIB_OBJS := \ @@ -63,8 +49,10 @@ LIB_OBJS := \ $(patsubst $(STE_SRC_DIR)/%.c,$(BUILD)/obj/ste/%.o,$(STE_C_SRCS)) \ $(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \ $(BUILD)/obj/68k/spriteEmitInterleaved68k.o \ + $(BUILD)/obj/68k/planarSprite.o \ $(BUILD)/obj/codegen/spriteCompile.o \ - $(BUILD)/obj/codegen/spriteStage.o + $(BUILD)/obj/codegen/spriteStage.o \ + $(BUILD)/obj/shim/math_shim.o LIB := $(LIBDIR)/libjoey.a @@ -114,19 +102,12 @@ $(LIB): $(LIB_OBJS) rm -f $@ $(ST_AR) rcs $@ $^ -$(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c - @mkdir -p $(dir $@) - $(ST_CC) $(CFLAGS) $(LIBXMP_CFLAGS) -c $< -o $@ - -$(LIBXMP_OBJDIR)/math_shim.o: $(ST_SHIM)/math_shim.c +# mintlib has no libm: the shim's few functions, for a program that +# includes , which -I$(ST_SHIM) gives it. +$(BUILD)/obj/shim/math_shim.o: $(ST_SHIM)/math_shim.c @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) -c $< -o $@ -$(LIBXMP_AR): $(LIBXMP_OBJS) - @mkdir -p $(dir $@) - rm -f $@ - $(ST_AR) rcs $@ $^ - # Pull in per-object header-dependency files generated by gcc -MMD/-MP. # Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild # the .c files that include it, leaving a frankenstein binary where @@ -139,12 +120,12 @@ $(LIBXMP_AR): $(LIBXMP_OBJS) PROGRAM_EXT := .PRG PROGRAM_FOLDERS := yes -# One program, $(1) its id: compiled and linked in one step with the library -# and libxmp-lite. gcc's -MMD leaves NAME.d beside it, for header edits. +# One program, $(1) its id: compiled and linked in one step with the +# library. gcc's -MMD leaves NAME.d beside it, for header edits. define programRules -$(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) +$(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) @mkdir -p $$(dir $$@) - $(ST_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) -o $$@ $(LDFLAGS) $(call programVar,$(1),LDFLAGS) + $(ST_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(LIB) -o $$@ $(LDFLAGS) $(call programVar,$(1),LDFLAGS) -include $(basename $(call programBin,$(1))).d endef @@ -156,7 +137,7 @@ include $(REPO_DIR)/make/programs.mk .DEFAULT_GOAL := all .PHONY: all $(PLATFORM) clean-$(PLATFORM) clean -all $(PLATFORM): $(LIB) $(LIBXMP_AR) programs +all $(PLATFORM): $(LIB) programs clean-$(PLATFORM): rm -rf $(BUILD) diff --git a/make/be.mk b/make/be.mk index 3d9f472..18a52ca 100644 --- a/make/be.mk +++ b/make/be.mk @@ -11,7 +11,8 @@ # # A program is a Haiku executable NAME (no extension), in bin/ or in its own # bin/NAME/ with its DATA beside it: the port makes the executable's folder -# the current directory when the window opens. Cross-compiled on this +# the current directory when the window opens. Its icon (ID_ICON) is in its +# resources, where Tracker and the Deskbar find it. Cross-compiled on this # machine; joey-run and the gates run it in a Haiku VM under QEMU. Each # includes common.mk, sets HOSTED_CC, HOSTED_CXX and HOSTED_AR, and # BE_CXX_RUNTIME (its C++ runtime library), then includes this file. @@ -24,5 +25,21 @@ HOSTED_SDK_CFLAGS := # linked by the C compiler, so its runtime is named too. HOSTED_LINK := -lbe -lmedia -ldevice $(BE_CXX_RUNTIME) PROGRAM_EXT := +# Its icons, the BeOS kind Haiku shows too: 32x32 and 16x16 in the BeOS +# palette, raw (tools/iconBake.py be), put into the linked program's +# resources by xres (component haikuTools) as BEOS:L:STD_ICON and +# BEOS:M:STD_ICON, BE_ICON_ID each. +BE_XRES := $(HAIKU_TOOLS)/xres +$(call requireTool,$(BE_XRES),toolchain:$(PLATFORM)) +BE_ICON_ID := 101 +HOSTED_ICON = $(call programIconDir,$(1))/icon32.raw +HOSTED_ICON_BAKE = $(ICON_BAKE) be $(call programIcon,$(1)) $(call HOSTED_ICON,$(1)) $(call programIconDir,$(1))/icon16.raw +HOSTED_PACK = $(BE_XRES) -o $(call programBin,$(1)) -a ICON:$(BE_ICON_ID):BEOS:L:STD_ICON $(call HOSTED_ICON,$(1)) -a MICN:$(BE_ICON_ID):BEOS:M:STD_ICON $(call programIconDir,$(1))/icon16.raw include $(REPO_DIR)/make/hosted.mk + +# UBER's probe build is part of JoeyLib's own `all` +# (scripts/verify-be-display.sh). +ifndef JOEY_PROJECT +all $(PLATFORM): $(call programTarget,UBERPROB) +endif diff --git a/make/beos.mk b/make/beos.mk index 7d5f5a7..f401b2b 100644 --- a/make/beos.mk +++ b/make/beos.mk @@ -13,7 +13,6 @@ include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk $(call requireTool,$(HAIKU32_ROOT)/cross/bin/i586-pc-haiku-g++,toolchain:beos) $(call requireTool,$(BEOS_R5_SYSROOT)/system-lib/libbe.so,toolchain:beos) -$(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:beos) BEOS_GCC2 := BE_R5_SYSROOT=$(BEOS_R5_SYSROOT) $(HAIKU32_GCC2) $(HAIKU32_ROOT) HOSTED_CC := $(BEOS_GCC2) gcc diff --git a/make/common.mk b/make/common.mk index 9325242..fc9ce57 100644 --- a/make/common.mk +++ b/make/common.mk @@ -24,6 +24,11 @@ requireTool = $(if $(filter-out clean%,$(or $(MAKECMDGOALS),all)),$(if $(wildcar # derives from a program's id (Hello on the Amiga, hello on DVX). lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(1))))))))))))))))))))))))))) uc = $(subst a,A,$(subst b,B,$(subst c,C,$(subst d,D,$(subst e,E,$(subst f,F,$(subst g,G,$(subst h,H,$(subst i,I,$(subst j,J,$(subst k,K,$(subst l,L,$(subst m,M,$(subst n,N,$(subst o,O,$(subst p,P,$(subst q,Q,$(subst r,R,$(subst s,S,$(subst t,T,$(subst u,U,$(subst v,V,$(subst w,W,$(subst x,X,$(subst y,Y,$(subst z,Z,$(1))))))))))))))))))))))))))) +# Quoting, for text a recipe writes out: $(call shellQuote,TEXT) is TEXT as +# one single-quoted shell word whatever quotes it holds, and +# $(call cQuote,TEXT) TEXT as a C string literal. +shellQuote = '$(subst ','\'',$(1))' +cQuote = "$(subst ",\",$(subst \,\\,$(1)))" capFirst = $(patsubst a%,A%,$(patsubst b%,B%,$(patsubst c%,C%,$(patsubst d%,D%,$(patsubst e%,E%,$(patsubst f%,F%,$(patsubst g%,G%,$(patsubst h%,H%,$(patsubst i%,I%,$(patsubst j%,J%,$(patsubst k%,K%,$(patsubst l%,L%,$(patsubst m%,M%,$(patsubst n%,N%,$(patsubst o%,O%,$(patsubst p%,P%,$(patsubst q%,Q%,$(patsubst r%,R%,$(patsubst s%,S%,$(patsubst t%,T%,$(patsubst u%,U%,$(patsubst v%,V%,$(patsubst w%,W%,$(patsubst x%,X%,$(patsubst y%,Y%,$(patsubst z%,Z%,$(1))))))))))))))))))))))))))) # The ports, and which of them can build here: a port is available when its @@ -80,6 +85,12 @@ HOST_CFLAGS := -std=c99 -Wall -Wextra -Werror -O2 # Portable C sources for libjoey -- present on every target. CORE_C_SRCS := $(wildcard $(SRC_CORE)/*.c) +# The software sound mixer -- the module player and the SFX overlay -- for the +# ports that mix on the CPU. The Amiga (PTPlayer), the IIgs (NinjaTrackerPlus) +# and DVX (its own music library) play through theirs and build without it. +CORE_MIX_C_SRCS := $(SRC_CORE)/audioSfxMix.c $(SRC_CORE)/modPlayer.c +CORE_C_SRCS_NO_MIX := $(filter-out $(CORE_MIX_C_SRCS), $(CORE_C_SRCS)) + # Generic portable-C op defaults (the src/generic backend) -- present on every # target. spriteEmitStub.c is excluded: it is the no-codegen template and # redefines jlSpriteCompile / spriteCompiledDraw, which the real @@ -103,8 +114,9 @@ PORT_TAG := $(call uc,$(PLATFORM)) # Baked assets (sprite and tile banks) carry the target they were baked for. # DVX shows the DOS port's stage and runs its x86 code, so it takes the DOS # bakes; the hosted ports' stage (SDL3, the web, Win32, Win16, the PSP, -# Haiku, BeOS) is chunky like the DOS one, so they take them too (the cels of a -# pre-compiled bank, without their code); the STe build is the ST port, so it +# Haiku, BeOS) is chunky like the DOS one, so they take them too (Win16 runs a +# pre-compiled bank's x86 code, as DVX does; the rest take its cels without +# their code); the STe build is the ST port, so it # takes the ST's (same planes, same compiled sprites); every other port its # own. ASSET_PLATFORM := $(if $(filter dvx sdl3 web win32 win16 psp haiku haiku32 beos,$(PLATFORM)),dos,$(if $(filter ste,$(PLATFORM)),atarist,$(PLATFORM))) @@ -157,6 +169,19 @@ programDataDir = $(if $(PROGRAM_DATA_APART),$(PROGRAM_BUILD)/data/$(call program # the DATA folder itself. programOptionalDir = $(if $(PROGRAM_OPTIONAL_APART),$(PROGRAM_BUILD)/data/$(call programName,$(1))/OPTIONAL,$(call programDataDir,$(1))) +# $(call programIcon,ID): the picture its icon is baked from (ID_ICON, else +# JoeyLib's kangaroo), on a port that shows programs by an icon; and +# $(call programIconDir,ID), where the port's bakes of it go, by its name (the +# IIgs disk scripts look icons up by it), or by its id for a program built +# apart (ID_BINDIR: a probe build may share a shipped program's name). +# ICON_BAKE is the baker (tools/iconBake.py says what each port gets); a rule +# that runs it depends on ICON_BAKE_DEPS too, so a change to the baker +# rebakes. +programIcon = $(or $(call programVar,$(1),ICON),$(REPO_DIR)/assets/joeyIcon.png) +programIconDir = $(PROGRAM_BUILD)/icon/$(if $(call programVar,$(1),BINDIR),$(1),$(call programName,$(1))) +ICON_BAKE := python3 $(REPO_DIR)/tools/iconBake.py +ICON_BAKE_DEPS := $(REPO_DIR)/tools/iconBake.py + # Rules that hand a host tool's build to make/tools.mk depend on FORCE, so # tools.mk -- which owns each tool's source list -- is always asked; it does diff --git a/make/dos.mk b/make/dos.mk index 2b8a7de..d97e43a 100644 --- a/make/dos.mk +++ b/make/dos.mk @@ -13,31 +13,22 @@ include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk $(call requireTool,$(DOS_CC),toolchain:dos) $(call requireTool,$(DOS_AS),toolchain:dos) $(call requireTool,$(DOS_CWSDSTUB),toolchain:dos) -$(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:dos) # -fomit-frame-pointer: flag parity with the ST/Amiga CFLAGS (both had # it; DOS never did). Frees EBP for allocation in the hot loops under # DOSBox's per-instruction-charged core (perf-hunt-2 rank 2). # so flag experiments never edit a tracked file (perf-hunt-2 rank 17). -CFLAGS := $(COMMON_CFLAGS) -march=i386 -m32 -fomit-frame-pointer -I$(SRC_DIR)/dos -I$(SRC_X86) -I$(LIBXMP_DIR)/include -MMD -MP +CFLAGS := $(COMMON_CFLAGS) -march=i386 -m32 -fomit-frame-pointer -I$(SRC_DIR)/dos -I$(SRC_X86) -MMD -MP ASFLAGS := -f coff -# libxmp-lite (MIT, libxmp.org) -- 4-channel-Protracker-and-friends -# replayer + SMIX SFX overlay. Installed by ./install.sh -# (LIBXMP_DIR, from config.mk). Built as a static archive that the DOS -# audio HAL links against. -LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c)) -LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite -LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC)) -LIBXMP_AR := $(LIBDIR)/libxmplite.a -LIBXMP_CFLAGS := -DJOEY_LIBXMP_LITE -DLIBXMP_CORE_PLAYER -DHAVE_FNMATCH=0 -I$(LIBXMP_DIR)/include -I$(LIBXMP_DIR)/include/libxmp-lite -I$(LIBXMP_DIR)/src -Wno-error -w - -# The x86 code DOS shares with DVX (the sprite emitter, the whole-map tile -# walker, the set-1 scan code table) lives in src/x86 and builds into obj/x86; +# The x86 code DOS shares with DVX (the sprite emitter, the sprite +# interpreter's rows, the whole-map tile walker, the set-1 scan code table) +# lives in src/x86 and builds into obj/x86; # only the CPU-independent sprite dispatch stays in src/codegen. PORT_C_SRCS := $(wildcard $(SRC_DIR)/dos/*.c) PORT_S_SRCS := $(wildcard $(SRC_DIR)/dos/*.asm) X86_C_SRCS := $(wildcard $(SRC_X86)/*.c) +X86_S_SRCS := $(wildcard $(SRC_X86)/*.s) CODEGEN_DIR := $(REPO_DIR)/src/codegen LIB_OBJS := \ @@ -46,6 +37,7 @@ LIB_OBJS := \ $(patsubst $(SRC_DIR)/dos/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \ $(patsubst $(SRC_DIR)/dos/%.asm,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \ $(patsubst $(SRC_X86)/%.c,$(BUILD)/obj/x86/%.o,$(X86_C_SRCS)) \ + $(patsubst $(SRC_X86)/%.s,$(BUILD)/obj/x86/%.o,$(X86_S_SRCS)) \ $(BUILD)/obj/codegen/spriteCompile.o \ $(BUILD)/obj/codegen/spriteStage.o @@ -71,6 +63,10 @@ $(BUILD)/obj/x86/%.o: $(SRC_X86)/%.c @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@ +$(BUILD)/obj/x86/%.o: $(SRC_X86)/%.s + @mkdir -p $(dir $@) + $(DOS_CC) -c $< -o $@ + $(BUILD)/obj/codegen/%.o: $(CODEGEN_DIR)/%.c @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@ @@ -82,15 +78,6 @@ $(LIB): $(LIB_OBJS) rm -f $@ $(DOS_AR) rcs $@ $^ -$(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c - @mkdir -p $(dir $@) - $(DOS_CC) $(CFLAGS) $(LIBXMP_CFLAGS) -c $< -o $@ - -$(LIBXMP_AR): $(LIBXMP_OBJS) - @mkdir -p $(dir $@) - rm -f $@ - $(DOS_AR) rcs $@ $^ - # Pull in per-object header-dependency files generated by gcc -MMD/-MP. # Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild # the .c files that include it, leaving a frankenstein binary where @@ -104,13 +91,13 @@ $(LIBXMP_AR): $(LIBXMP_OBJS) PROGRAM_EXT := .EXE PROGRAM_FOLDERS := yes -# One program, $(1) its id: compiled and linked in one step with the library -# and libxmp-lite, then given the CWSDPMI DPMI server, so the .EXE needs no -# CWSDPMI.EXE beside it. gcc's -MMD leaves NAME.d beside it, for header edits. +# One program, $(1) its id: compiled and linked in one step with the library, +# then given the CWSDPMI DPMI server, so the .EXE needs no CWSDPMI.EXE beside +# it. gcc's -MMD leaves NAME.d beside it, for header edits. define programRules -$(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) +$(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) @mkdir -p $$(dir $$@) - $(DOS_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) -o $$@ $(call programVar,$(1),LDFLAGS) + $(DOS_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(LIB) -o $$@ $(call programVar,$(1),LDFLAGS) $(DOS_EMBED_DPMI) $$@ -include $(basename $(call programBin,$(1))).d endef @@ -123,7 +110,7 @@ include $(REPO_DIR)/make/programs.mk .DEFAULT_GOAL := all .PHONY: all dos clean-dos clean -all dos: $(LIB) $(LIBXMP_AR) programs +all dos: $(LIB) programs clean-dos: rm -rf $(BUILD) diff --git a/make/dvx.mk b/make/dvx.mk index 5504c6a..29528bd 100644 --- a/make/dvx.mk +++ b/make/dvx.mk @@ -58,12 +58,14 @@ APP_CFLAGS := -DJOEYLIB_PLATFORM_DVX -Dmain=jlGameMain $(PROGRAM_RENAMES) -fomit PORT_ENTRY := $(PORT_DIR)/dvxApp.c PORT_C_SRCS := $(filter-out $(PORT_ENTRY),$(wildcard $(PORT_DIR)/*.c)) X86_C_SRCS := $(wildcard $(SRC_X86)/*.c) +X86_S_SRCS := $(wildcard $(SRC_X86)/*.s) LIB_OBJS := \ - $(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \ + $(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS_NO_MIX)) \ $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \ $(patsubst $(PORT_DIR)/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \ $(patsubst $(SRC_X86)/%.c,$(BUILD)/obj/x86/%.o,$(X86_C_SRCS)) \ + $(patsubst $(SRC_X86)/%.s,$(BUILD)/obj/x86/%.o,$(X86_S_SRCS)) \ $(BUILD)/obj/codegen/spriteCompile.o \ $(BUILD)/obj/codegen/spriteStage.o @@ -81,6 +83,10 @@ $(BUILD)/obj/x86/%.o: $(SRC_X86)/%.c @mkdir -p $(dir $@) $(DOS_CC) $(LIB_CFLAGS) -c $< -o $@ +$(BUILD)/obj/x86/%.o: $(SRC_X86)/%.s + @mkdir -p $(dir $@) + $(DOS_CC) -c $< -o $@ + $(BUILD)/obj/codegen/%.o: $(SRC_CG)/%.c @mkdir -p $(dir $@) $(DOS_CC) $(LIB_CFLAGS) -c $< -o $@ @@ -111,6 +117,10 @@ DVX_KIND := JoeyLib DVX_AUTHOR := JoeyLib PROGRAM_EXT := .app PROGRAM_DATA_APART := yes + +# What every program's help says after " is a <kind> program.". +DVX_HELP_WINDOW := It runs in its own window at a virtual 70 frames a second; while the window is active every key goes to it except Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close it). Game > Exit also closes it. +DVX_HELP_SCREEN := Game > Full Screen, or Alt+Enter, puts it on the whole screen at 320x200 in 256 colours, and back in its window. It starts the way it was left. Alt+Tab or Ctrl+Esc brings the desktop back while it runs on; coming back to it gives it the whole screen again. programNameRule = $(call lc,$(1)) programDir = $(or $(call programVar,$(1),BINDIR),$(BINDIR))/apps/$(DVX_VENDOR)/$(call programName,$(1)) programTarget = app-$(call programName,$(1)) @@ -118,54 +128,64 @@ programTitle = $(or $(call programVar,$(1),TITLE),$(call capFirst,$(call # One program, $(1) its id, as the phony app-<name>: (re)write its SDK project # in build/dvx/apps/<name>/ -- the Makefile, symlinks to its sources and the -# port's entry, the help source and the resource list -- then let sdk.mk build -# and package it, and copy the package into bin/ (or ID_BINDIR). sdk.mk -# rebuilds only what changed; the project files are rewritten only when their -# contents change, so an unchanged program is not relinked. Its CFLAGS are -# added to the program's flags. +# port's entry, the help source, the resource list, and its icon -- then let +# sdk.mk build and package it, and copy the package into bin/ (or +# ID_BINDIR). sdk.mk rebuilds only what changed; the project files are +# rewritten only when their contents change, so an unchanged program is not +# relinked. Its CFLAGS are added to the program's flags. Its sources are +# linked by absolute path, so a project may name them relative to its own +# folder. programRules = $(call programDvxRules,$(1),$(call programName,$(1))) # $(1) the id, $(2) the name. Everything that may hold a comma (the title, -# the help line) is looked up here rather than passed in. +# the help line) is looked up here rather than passed in, and every line +# written out is shellQuote'd, so a title or help line may hold quotes. define programDvxRules .PHONY: app-$(2) -app-$(2): $(LIB) $(call programVar,$(1),SRCS) $(PORT_ENTRY) $(call programVar,$(1),DATA) +app-$(2): $(LIB) $(call programVar,$(1),SRCS) $(PORT_ENTRY) $(call programVar,$(1),DATA) $(APPSDIR)/$(2)/icon32.bmp @mkdir -p $(APPSDIR)/$(2) - @for src in $(call programVar,$(1),SRCS) $(PORT_ENTRY); do ln -sfn $$$$src $(APPSDIR)/$(2)/$$$$(basename $$$$src); done + @for src in $(abspath $(call programVar,$(1),SRCS)) $(PORT_ENTRY); do ln -sfn $$$$src $(APPSDIR)/$(2)/$$$$(basename $$$$src); done @printf '%s\n' \ - '# Generated by make/dvx.mk: the DVX SDK project of the $(DVX_KIND) $(2).' \ + $(call shellQuote,# Generated by make/dvx.mk: the DVX SDK project of the $(DVX_KIND) $(2).) \ 'SDK_DIR = $(DVX_SDK)' \ 'KIND = app' \ 'NAME = $(2)' \ 'VENDOR = $(DVX_VENDOR)' \ 'LIBS = $(LIB)' \ 'DATA = $(if $(call programVar,$(1),DATA),$(call programDataDir,$(1)))' \ - "CFLAGS = $(APP_CFLAGS) $(call programVar,$(1),CFLAGS) -I$(dir $(firstword $(call programVar,$(1),SRCS))) -DDVX_PORT_APP_NAME='\"$(call programTitle,$(1))\"'" \ + $(call shellQuote,CFLAGS = $(APP_CFLAGS) $(call programVar,$(1),CFLAGS) -I$(dir $(abspath $(firstword $(call programVar,$(1),SRCS)))) -DDVX_PORT_APP_NAME=$(call shellQuote,$(call cQuote,$(call programTitle,$(1))))) \ 'include $$$$(SDK_DIR)/sdk.mk' | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(2)/Makefile @printf '%s\n' \ '.topic $(2).overview' \ - '.title $(call programTitle,$(1))' \ - '.toc 0 $(call programTitle,$(1))' \ + $(call shellQuote,.title $(call programTitle,$(1))) \ + $(call shellQuote,.toc 0 $(call programTitle,$(1))) \ '.default' \ - '.index $(call programTitle,$(1))' \ + $(call shellQuote,.index $(call programTitle,$(1))) \ '' \ - '.h1 $(call programTitle,$(1))' \ + $(call shellQuote,.h1 $(call programTitle,$(1))) \ '' \ - '$(call programVar,$(1),HELP)' \ + $(call shellQuote,$(call programVar,$(1),HELP)) \ '' \ - '$(call programTitle,$(1)) is a $(DVX_KIND) program. It runs in its own window at a virtual 70 frames a second; while the window is active every key goes to it except Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close it). Game > Exit also closes it.' \ + $(call shellQuote,$(call programTitle,$(1)) is a $(DVX_KIND) program. $(DVX_HELP_WINDOW)) \ '' \ - 'Game > Full Screen, or Alt+Enter, puts it on the whole screen at 320x200 in 256 colours, and back in its window. It starts the way it was left. Alt+Tab or Ctrl+Esc brings the desktop back while it runs on; coming back to it gives it the whole screen again.' \ + $(call shellQuote,$(DVX_HELP_SCREEN)) \ | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(2)/$(2).dhs @printf '%s\n' \ - 'name text "$(call programTitle,$(1))"' \ - 'author text "$(DVX_AUTHOR)"' \ - 'description text "$(DVX_KIND): $(call programTitle,$(1))"' \ + 'icon32 icon icon32.bmp' \ + $(call shellQuote,name text "$(call programTitle,$(1))") \ + $(call shellQuote,author text "$(DVX_AUTHOR)") \ + $(call shellQuote,description text "$(DVX_KIND): $(call programTitle,$(1))") \ | $(REPO_DIR)/make/support/dvx/writeIfChanged.sh $(APPSDIR)/$(2)/$(2).res $$(MAKE) -C $(APPSDIR)/$(2) DJGPP_PREFIX=$(DOS_PREFIX) package @mkdir -p $(or $(call programVar,$(1),BINDIR),$(BINDIR)) rm -rf $(call programDir,$(1)) cp -R $(APPSDIR)/$(2)/pkg/apps $(or $(call programVar,$(1),BINDIR),$(BINDIR))/ + +# Its icon, the one Program Manager shows: icon32 among its resources, a +# 32x32 BMP on DVX's grey, which sdk.mk takes from the project's folder. +$(APPSDIR)/$(2)/icon32.bmp: $(call programIcon,$(1)) $(ICON_BAKE_DEPS) + @mkdir -p $$(dir $$@) + $(ICON_BAKE) bmp $$< $$@ endef # ---- JoeyLib's own build: its examples ------------------------------------------ diff --git a/make/examples.mk b/make/examples.mk index 4871b30..6d69ca6 100644 --- a/make/examples.mk +++ b/make/examples.mk @@ -32,9 +32,15 @@ # PALPROBE Amiga: the palette gate's program: how fast palette and SCB # changes reach the screen through the copper, and a held screen # of bands to check that they reach it right. +# WBLAUNCH Amiga: the icon gate's program: a program's icon read and +# drawn by icon.library, and the program started from it the way +# Workbench starts a tool. +# AUDPAR the audio conformance gate's program: one logged timeline of +# tones, voice and noise steps, sound effects, a module's end +# and tick rates, which every port must play alike. PROGRAMS := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK SCB JOY SPRITE AUDIO UBER ADV ADV2 -TEST_PROGRAMS := SCCPROBE SERLOSS SERLOSSS DIRTYMRK UBERPROB AUDIOPRB PALPROBE +TEST_PROGRAMS := SCCPROBE SERLOSS SERLOSSS DIRTYMRK UBERPROB AUDIOPRB AUDPAR PALPROBE WBLAUNCH # DVX's help and resource texts call these JoeyLib examples. DVX_KIND := JoeyLib example @@ -123,6 +129,9 @@ DIRTYMRK_BINDIR := $(PROGRAM_BUILD)/test PALPROBE_SRCS := $(EXAMPLES)/palprobe/palprobe.c PALPROBE_PORTS := amiga PALPROBE_BINDIR := $(PROGRAM_BUILD)/test +WBLAUNCH_SRCS := $(EXAMPLES)/wblaunch/wblaunch.c +WBLAUNCH_PORTS := amiga +WBLAUNCH_BINDIR := $(PROGRAM_BUILD)/test DIRTYMRK_CFLAGS := -DDIRTY_MARK_SCENARIOS=$(DIRTYMARK_SCENARIOS) -DDIRTY_MARK_LOG='"$(DIRTYMARK_LOG)"' # UBER with its UBER_PROBE rows (the present checked against the stage), @@ -130,16 +139,24 @@ DIRTYMRK_CFLAGS := -DDIRTY_MARK_SCENARIOS=$(DIRTYMARK_SCENARIOS) -DDIRTY_MAR # changes. -DUBER_FRAMES=1: the run is wanted for its probe rows, not its # timings, and a probe run at the golden 16 frames does not fit # bench-iigs.sh's MAME cap. The IIgs packs it as UBER, the name its disks -# and bench-iigs.sh look for (iigs-verify-shrtail); DVX builds it as part of -# `all` (scripts/verify-dvx-display.sh), packaged in build/dvx/probe. -UBERPROB_SRCS := $(UBER_SRCS) -UBERPROB_PORTS := iigs dvx -UBERPROB_CFLAGS := -DUBER_PROBE -DUBER_FRAMES=1 -UBERPROB_TITLE := Uber Probe -UBERPROB_HELP := UBER with its probe rows: the present checked against the stage on screen. Its results go to joeylog.txt in its settings folder. -UBERPROB_IIGS_NAME := UBER -UBERPROB_IIGS_BINDIR := $(PROGRAM_BUILD)/probe/bin -UBERPROB_DVX_BINDIR := $(PROGRAM_BUILD)/probe +# and bench-iigs.sh look for (iigs-verify-shrtail). DVX and the windowed +# hosted ports build it as part of `all`, into build/<port>/probe, for +# their display gates (scripts/verify-dvx-display.sh, +# verify-win16-display.sh, verify-win32-display.sh, and +# verify-be-display.sh). +UBERPROB_SRCS := $(UBER_SRCS) +UBERPROB_PORTS := iigs dvx win16 win32 haiku haiku32 beos +UBERPROB_CFLAGS := -DUBER_PROBE -DUBER_FRAMES=1 +UBERPROB_TITLE := Uber Probe +UBERPROB_HELP := UBER with its probe rows: the present checked against the stage on screen. Its results go to joeylog.txt in its settings folder. +UBERPROB_IIGS_NAME := UBER +UBERPROB_IIGS_BINDIR := $(PROGRAM_BUILD)/probe/bin +UBERPROB_DVX_BINDIR := $(PROGRAM_BUILD)/probe +UBERPROB_WIN16_BINDIR := $(PROGRAM_BUILD)/probe +UBERPROB_WIN32_BINDIR := $(PROGRAM_BUILD)/probe +UBERPROB_HAIKU_BINDIR := $(PROGRAM_BUILD)/probe +UBERPROB_HAIKU32_BINDIR := $(PROGRAM_BUILD)/probe +UBERPROB_BEOS_BINDIR := $(PROGRAM_BUILD)/probe # AUDIO with its keys pressed on a timetable (-DAUDIO_PROBE): N once, S every # 1.5 seconds, under the tracker song. The IIgs and X68000 audio gates read @@ -153,6 +170,23 @@ AUDIOPRB_CFLAGS := -DAUDIO_PROBE AUDIOPRB_BINDIR := $(PROGRAM_BUILD)/probe AUDIOPRB_IIGS_BINDIR := $(PROGRAM_BUILD)/probe/bin +# AUDPAR (scripts/verify-audio-conformance.sh): built apart like AUDIOPRB, +# with the module AUDIO plays in its DATA: the IIgs's as NinjaTrackerPlus's +# .NTP, made by tools/joeymod. +AUDPAR_SRCS := $(EXAMPLES)/audpar/audpar.c +AUDPAR_BINDIR := $(PROGRAM_BUILD)/probe +AUDPAR_IIGS_BINDIR := $(PROGRAM_BUILD)/probe/bin +AUDPAR_DATA := $(call programDataDir,AUDPAR)/test.mod +AUDPAR_IIGS_DATA := $(call programDataDir,AUDPAR)/TEST.NTP + +$(call programDataDir,AUDPAR)/TEST.NTP: $(REPO_DIR)/assets/test.mod $(REPO_DIR)/tools/joeymod/joeymod + @mkdir -p $(dir $@) + $(REPO_DIR)/tools/joeymod/joeymod $< $@ + +$(call programDataDir,AUDPAR)/%: $(REPO_DIR)/assets/% + @mkdir -p $(dir $@) + cp $< $@ + # AUDIO loads DATA/test.mod and DATA/test.sfx from beside the program. The # IIgs plays its chip music through NTP and stages no MOD. AUDIO_DATA := $(addprefix $(call programDataDir,AUDIO)/,test.mod test.sfx) diff --git a/make/haiku.mk b/make/haiku.mk index b9f9e2f..b339cda 100644 --- a/make/haiku.mk +++ b/make/haiku.mk @@ -10,7 +10,6 @@ PLATFORM := haiku include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk $(call requireTool,$(HAIKU_CROSS)g++,toolchain:haiku) -$(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:haiku) HOSTED_CC := $(HAIKU_CROSS)gcc --sysroot=$(HAIKU_SYSROOT) HOSTED_CXX := $(HAIKU_CROSS)g++ --sysroot=$(HAIKU_SYSROOT) diff --git a/make/haiku32.mk b/make/haiku32.mk index ab3328e..9643e91 100644 --- a/make/haiku32.mk +++ b/make/haiku32.mk @@ -12,7 +12,6 @@ PLATFORM := haiku32 include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk $(call requireTool,$(HAIKU32_ROOT)/cross/bin/i586-pc-haiku-g++,toolchain:haiku32) -$(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:haiku32) HOSTED_CC := $(HAIKU32_GCC2) $(HAIKU32_ROOT) gcc HOSTED_CXX := $(HAIKU32_GCC2) $(HAIKU32_ROOT) g++ diff --git a/make/hosted.mk b/make/hosted.mk index d3bba3b..a007c84 100644 --- a/make/hosted.mk +++ b/make/hosted.mk @@ -3,8 +3,8 @@ # SPDX-License-Identifier: MIT # # The hosted ports' shared build rules (src/hosted): JoeyLib's library for -# the hosted core and one backend, libxmp-lite, and programRules, the rules -# for one program, which make/programs.mk instantiates for every program. +# the hosted core and one backend, and programRules, the rules for one +# program, which make/programs.mk instantiates for every program. # Included by make/sdl3.mk (a native program), make/web.mk (the same # SDL3 backend compiled to WebAssembly), make/psp.mk (the same again, for # the PSP), make/win32.mk, make/win16.mk and make/be.mk (Haiku), after each @@ -14,7 +14,7 @@ # HOSTED_BACKEND the backend folder under src/hosted # HOSTED_DEFINES the platform and backend macros # HOSTED_SDK_CFLAGS the backend's library headers (the backend only) -# HOSTED_LINK what a program links after the library and libxmp-lite +# HOSTED_LINK what a program links after the library # PROGRAM_EXT the program file's extension # and, when the backend is C++ (src/hosted/<backend>/*.cpp, the Be API's), # HOSTED_CXX its C++ compiler @@ -25,6 +25,14 @@ # packed from what is linked (the PSP's EBOOT.PBP, from an ELF), the port # sets HOSTED_LINK_OUT, the file one program is linked to, and HOSTED_PACK, # the command that packs it into the program file ($(1) the id in both). +# A port whose programs show an icon (make/common.mk's programIcon, baked by +# tools/iconBake.py) sets HOSTED_ICON, the file one program's icon is baked +# to, and HOSTED_ICON_BAKE, the command that bakes it there from the +# program's picture; the program is rebuilt after it, and HOSTED_ICON_LINK +# is what of it the link takes (a resource object, a resource script), or +# HOSTED_PACK packs it in ($(1) the id in all three). Where the system keeps +# the icon in the program's resources (Windows), HOSTED_ICON_ID is its +# number there; the backend is compiled knowing it. # # The stage is the chunky generic one, as on the BLANK port: the core, every # generic (the sprite interpreter stub among them -- no machine code is @@ -33,30 +41,35 @@ HOSTED_DIR := $(SRC_DIR)/hosted BACKEND_DIR := $(HOSTED_DIR)/$(HOSTED_BACKEND) -# LIBXMP_STATIC: libxmp-lite is linked in, never a DLL (xmp.h marks its -# functions dllimport on Windows without it). -CFLAGS := $(COMMON_CFLAGS) $(HOSTED_DEFINES) -DLIBXMP_STATIC -I$(HOSTED_DIR) -I$(LIBXMP_DIR)/include -MMD -MP +CFLAGS := $(COMMON_CFLAGS) $(HOSTED_DEFINES) -I$(HOSTED_DIR) -MMD -MP -# libxmp-lite (MIT, libxmp.org): the MOD player the hosted mixer runs, the -# same sources and flags as the DOS port's -- and its win32.c, libxmp's own -# snprintf for a C library without a safe one (Open Watcom's, the win16 -# port's), which compiles to nothing for the others. -LIBXMP_SRC := $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c) -LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite -LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC)) -LIBXMP_AR := $(LIBDIR)/libxmplite.a -LIBXMP_CFLAGS := -DJOEY_LIBXMP_LITE -DLIBXMP_CORE_PLAYER -DHAVE_FNMATCH=0 -I$(LIBXMP_DIR)/include -I$(LIBXMP_DIR)/include/libxmp-lite -I$(LIBXMP_DIR)/src -Wno-error -w +# HOSTED_X86 (make/win16.mk): sprites are compiled to x86 code as on DOS -- +# src/x86's emitter and src/codegen's dispatch in place of the stub. +ifeq ($(HOSTED_X86),1) +CFLAGS += -I$(SRC_X86) +HOSTED_SPRITE_OBJS := $(BUILD)/obj/x86/spriteEmitX86.o $(BUILD)/obj/codegen/spriteCompile.o $(BUILD)/obj/codegen/spriteStage.o +else +HOSTED_SPRITE_OBJS := $(BUILD)/obj/generic/spriteEmitStub.o +endif +# HOSTED_BACKEND_EXTRA (make/psp.mk): a folder of the backend's own for one +# system it runs on, built with the backend's flags; the backend includes +# its headers. HOSTED_C_SRCS := $(wildcard $(HOSTED_DIR)/*.c) BACKEND_C_SRCS := $(wildcard $(BACKEND_DIR)/*.c) BACKEND_CXX_SRCS := $(wildcard $(BACKEND_DIR)/*.cpp) +EXTRA_C_SRCS := $(if $(HOSTED_BACKEND_EXTRA),$(wildcard $(HOSTED_BACKEND_EXTRA)/*.c)) +HOSTED_ICON_ID := 1 +BACKEND_CFLAGS := $(CFLAGS) $(HOSTED_SDK_CFLAGS) $(if $(HOSTED_BACKEND_EXTRA),-I$(HOSTED_BACKEND_EXTRA)) -DHOSTED_ICON_ID=$(HOSTED_ICON_ID) LIB_OBJS := \ $(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \ - $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS) $(SRC_DIR)/generic/spriteEmitStub.c) \ + $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \ + $(HOSTED_SPRITE_OBJS) \ $(patsubst $(HOSTED_DIR)/%.c,$(BUILD)/obj/hosted/%.o,$(HOSTED_C_SRCS)) \ $(patsubst $(BACKEND_DIR)/%.c,$(BUILD)/obj/backend/%.o,$(BACKEND_C_SRCS)) \ - $(patsubst $(BACKEND_DIR)/%.cpp,$(BUILD)/obj/backend/%.o,$(BACKEND_CXX_SRCS)) + $(patsubst $(BACKEND_DIR)/%.cpp,$(BUILD)/obj/backend/%.o,$(BACKEND_CXX_SRCS)) \ + $(patsubst $(HOSTED_BACKEND_EXTRA)/%.c,$(BUILD)/obj/backendExtra/%.o,$(EXTRA_C_SRCS)) LIB := $(LIBDIR)/libjoey.a @@ -68,6 +81,14 @@ $(BUILD)/obj/generic/%.o: $(SRC_DIR)/generic/%.c @mkdir -p $(dir $@) $(HOSTED_CC) $(CFLAGS) -c $< -o $@ +$(BUILD)/obj/x86/%.o: $(SRC_X86)/%.c + @mkdir -p $(dir $@) + $(HOSTED_CC) $(CFLAGS) -I$(SRC_CG) -c $< -o $@ + +$(BUILD)/obj/codegen/%.o: $(SRC_CG)/%.c + @mkdir -p $(dir $@) + $(HOSTED_CC) $(CFLAGS) -I$(SRC_CG) -c $< -o $@ + $(BUILD)/obj/hosted/%.o: $(HOSTED_DIR)/%.c @mkdir -p $(dir $@) $(HOSTED_CC) $(CFLAGS) -c $< -o $@ @@ -75,11 +96,15 @@ $(BUILD)/obj/hosted/%.o: $(HOSTED_DIR)/%.c # Only the backend sees its library's headers. $(BUILD)/obj/backend/%.o: $(BACKEND_DIR)/%.c @mkdir -p $(dir $@) - $(HOSTED_CC) $(CFLAGS) $(HOSTED_SDK_CFLAGS) -c $< -o $@ + $(HOSTED_CC) $(BACKEND_CFLAGS) -c $< -o $@ $(BUILD)/obj/backend/%.o: $(BACKEND_DIR)/%.cpp @mkdir -p $(dir $@) - $(HOSTED_CXX) $(CFLAGS) $(HOSTED_SDK_CFLAGS) -c $< -o $@ + $(HOSTED_CXX) $(BACKEND_CFLAGS) -c $< -o $@ + +$(BUILD)/obj/backendExtra/%.o: $(HOSTED_BACKEND_EXTRA)/%.c + @mkdir -p $(dir $@) + $(HOSTED_CC) $(BACKEND_CFLAGS) -c $< -o $@ # Archived from scratch, so the members follow LIB_OBJS rather than the order # earlier builds added them in. @@ -88,15 +113,6 @@ $(LIB): $(LIB_OBJS) rm -f $@ $(HOSTED_AR) rcs $@ $^ -$(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c - @mkdir -p $(dir $@) - $(HOSTED_CC) $(CFLAGS) $(LIBXMP_CFLAGS) -c $< -o $@ - -$(LIBXMP_AR): $(LIBXMP_OBJS) - @mkdir -p $(dir $@) - rm -f $@ - $(HOSTED_AR) rcs $@ $^ - # Pull in per-object header-dependency files generated by -MMD/-MP. -include $(LIB_OBJS:.o=.d) @@ -107,17 +123,32 @@ PROGRAM_FOLDERS := yes # unless the port packs it afterwards. hostedLinkOut = $(or $(call HOSTED_LINK_OUT,$(1)),$(call programBin,$(1))) -# One program, $(1) its id: compiled and linked in one step with the library, -# libxmp-lite and the backend's libraries (and the maths library the mixer's -# tables use), its DATA too where the port packs it in, then packed where the -# port packs it. -MMD leaves the dependency file beside what is linked, for -# header edits. +# One program, $(1) its id: compiled and linked in one step with the library +# and the backend's libraries (and the maths library the mixer's tables use), its DATA too where the port packs it in, and its icon where +# the link takes it, then packed where the port packs it. -MMD leaves the +# dependency file beside what is linked, for header edits; it is named after +# the program only while the link compiles one source, so an icon reaches +# the link compiled. define programRules -$(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) $(HOSTED_PROGRAM_DEPS) $(if $(HOSTED_LINK_DATA),$(call programVar,$(1),DATA)) +$(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) $(HOSTED_PROGRAM_DEPS) $(if $(HOSTED_LINK_DATA),$(call programVar,$(1),DATA)) $(call HOSTED_ICON,$(1)) @mkdir -p $$(dir $$@) $(dir $(call hostedLinkOut,$(1))) - $(HOSTED_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(LIB) $(LIBXMP_AR) $(HOSTED_LINK) $(if $(HOSTED_LINK_DATA),$(call HOSTED_LINK_DATA,$(1))) -lm -o $(call hostedLinkOut,$(1)) $(call programVar,$(1),LDFLAGS) + $(HOSTED_CC) $(CFLAGS) $(call programVar,$(1),CFLAGS) $(call programVar,$(1),SRCS) $(call HOSTED_ICON_LINK,$(1)) $(LIB) $(HOSTED_LINK) $(if $(HOSTED_LINK_DATA),$(call HOSTED_LINK_DATA,$(1))) -lm -o $(call hostedLinkOut,$(1)) $(call programVar,$(1),LDFLAGS) $(call HOSTED_PACK,$(1)) -include $(basename $(call hostedLinkOut,$(1))).d +$(if $(call HOSTED_ICON,$(1)),$(call hostedIconRules,$(1))) +endef + +# $(call hostedIconScript,ID,FORMAT): a Windows port's bake: the program's +# icon in iconBake.py's FORMAT (ico, ico16) as icon.ico in its icon folder, +# and icon.rc, the resource script that gives it the number HOSTED_ICON_ID +# (by its whole path, so the resource compiler may run anywhere). +hostedIconScript = $(ICON_BAKE) $(2) $(call programIcon,$(1)) $(call programIconDir,$(1))/icon.ico && printf '%s ICON "%s"\n' $(HOSTED_ICON_ID) $(call programIconDir,$(1))/icon.ico > $(call programIconDir,$(1))/icon.rc + +# One program's icon, $(1) its id, baked by the port's command. +define hostedIconRules +$(call HOSTED_ICON,$(1)): $(call programIcon,$(1)) $(ICON_BAKE_DEPS) + @mkdir -p $$(dir $$@) + $(call HOSTED_ICON_BAKE,$(1)) endef # ---- JoeyLib's own build: its examples ------------------------------------------ @@ -128,7 +159,7 @@ include $(REPO_DIR)/make/programs.mk .DEFAULT_GOAL := all .PHONY: all $(PLATFORM) clean-$(PLATFORM) clean -all $(PLATFORM): $(LIB) $(LIBXMP_AR) programs +all $(PLATFORM): $(LIB) programs clean-$(PLATFORM): rm -rf $(BUILD) diff --git a/make/iigs.mk b/make/iigs.mk index b1fca29..b4c99b5 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -45,16 +45,12 @@ PORT_C_SRCS := $(wildcard $(SRC_DIR)/iigs/*.c) # Hand-rolled asm, migrated to llvm-mc GAS .s. PORT_ASM_SRCS := $(wildcard $(SRC_DIR)/iigs/*.s) -# IIgs uses NTPstreamsound for SFX, not the libxmp+overlay combo that -# DOS and ST share, so src/core/audioSfxMix.c is unused here. -CORE_C_SRCS_IIGS := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS)) - # Sprite codegen: the 65816 emitter (spriteEmitIigs.c) now lives in src/iigs/ # and is picked up by PORT_C_SRCS; only the CPU-independent compile dispatch # remains in src/codegen. CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteCompile.c $(REPO_DIR)/src/codegen/spriteStage.c -LIB_C_SRCS := $(CORE_C_SRCS_IIGS) $(GENERIC_C_SRCS) $(PORT_C_SRCS) $(CODEGEN_SRCS) +LIB_C_SRCS := $(CORE_C_SRCS_NO_MIX) $(GENERIC_C_SRCS) $(PORT_C_SRCS) $(CODEGEN_SRCS) LIB_SRCS := $(LIB_C_SRCS) $(PORT_ASM_SRCS) # Library header set for archive rebuild triggering (coarse on purpose: any @@ -117,9 +113,14 @@ PROGRAM_EXT := PROGRAM_DATA_APART := yes PROGRAM_OPTIONAL_APART := yes DEP_DIR := $(PROGRAM_BUILD)/dep +# Its Finder icon (ID_ICON): an icon file, staged in its icon folder, which +# the disk scripts put in the volume's Icons folder. +programIconFile = $(call programIconDir,$(1))/finder.icons +programTarget = $(call programBin,$(1)) $(call programIconFile,$(1)) # One program, $(1) its id: clang-build.sh compiles, links and wraps it, and -# writes one header-dependency file for all its sources. ID_IIGS_OVERFLOW_CAP +# writes one header-dependency file for all its sources; iconBake.py bakes +# its Finder icon. ID_IIGS_OVERFLOW_CAP # opts into large overflow segments (see clang-build.sh). NTP rides on every # disk a program goes on, so it is built first. define programRules @@ -127,6 +128,10 @@ $(if $(call programVar,$(1),OVERFLOW_CAP),$(call programBin,$(1)): export JOEY_O $(call programBin,$(1)): $(call programVar,$(1),SRCS) $(LIB) $(IIGS_CLANG_BUILD) | $(NTP_BIN) @mkdir -p $$(dir $$@) $(DEP_DIR) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/$(1).d $(call programVar,$(1),CFLAGS) $(INCLUDES) -o $$@ $(call programVar,$(1),SRCS) $(LIB) + +$(call programIconFile,$(1)): $(call programIcon,$(1)) $(ICON_BAKE_DEPS) + @mkdir -p $$(dir $$@) + $(ICON_BAKE) iigs $$< $$@ $(call programName,$(1)) endef -include $(wildcard $(DEP_DIR)/*.d) diff --git a/make/programs.mk b/make/programs.mk index f348ca8..77c264b 100644 --- a/make/programs.mk +++ b/make/programs.mk @@ -33,6 +33,12 @@ # ID_DATA its DATA files, as the files the description's own rules # make under $(call programDataDir,ID) (or # programOptionalDir, for files it can run without) +# ID_ICON its icon: a picture (PNG, best square, transparent +# where there is no icon), baked for each port that shows +# programs by an icon -- the IIgs Finder, the Amiga's +# Workbench, DVX, Win16, Win32, Haiku and BeOS, the PSP's +# menu, the SDL3 window, and the web page; by default +# JoeyLib's kangaroo (docs/BuildIntegration.adoc, Icons) # ID_TITLE DVX: its window title (default: its Amiga-style name) # ID_HELP DVX: the line of its help file (no apostrophes: the help is # written through the shell in quotes) @@ -55,6 +61,11 @@ $(foreach p,$(PORT_PROGRAMS) $(PORT_TEST_PROGRAMS),$(eval $(call programRules,$( .PHONY: programs programs: $(foreach p,$(PORT_PROGRAMS),$(call programTarget,$(p)) $(call programVar,$(p),DATA)) +# One program by its id, a test program's too, with its DATA: what a gate +# asks for (make -f make/<port>.mk program-AUDPAR). +.PHONY: $(addprefix program-,$(PORT_PROGRAMS) $(PORT_TEST_PROGRAMS)) +$(foreach p,$(PORT_PROGRAMS) $(PORT_TEST_PROGRAMS),$(eval program-$(p): $(call programTarget,$(p)) $(call programVar,$(p),DATA))) + # The host sprite pre-compiler a description's bakes use; make/tools.mk owns # its sources. $(SPRITEBAKE): FORCE diff --git a/make/psp.mk b/make/psp.mk index f82afdc..35f7461 100644 --- a/make/psp.mk +++ b/make/psp.mk @@ -10,7 +10,8 @@ # make/project.mk. # # A program is a folder of its own, bin/NAME/: EBOOT.PBP, titled ID_TITLE -# (or NAME) in the PSP's menu, and its DATA/ beside it. The folder goes into +# (or NAME) in the PSP's menu with its icon (ID_ICON, ICON0 in the EBOOT), +# and its DATA/ beside it. The folder goes into # PSP/GAME/ on the memory stick as it is: the program's folder is its # current directory, as on every port, so DATA/, SAVES/ and joeylog.txt are # there. The ELF the EBOOT.PBP is packed from (make/support/psp/packEboot.sh) @@ -22,7 +23,6 @@ include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk $(call requireTool,$(PSP_CC),toolchain:psp) SDL3_PSP_LIB := $(SDL3_PSP_DIR)/lib/libSDL3.a $(call requireTool,$(SDL3_PSP_LIB),toolchain:psp) -$(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:psp) # Only this SDL's own .pc: a host SDL3 must not answer for the PSP's. SDL3_PSP_PKG_CONFIG := PKG_CONFIG_LIBDIR=$(SDL3_PSP_DIR)/lib/pkgconfig pkg-config @@ -34,6 +34,8 @@ PSP_PACK := $(REPO_DIR)/make/support/psp/packEboot.sh HOSTED_CC := $(PSP_CC) HOSTED_AR := $(PSPDEV)/bin/psp-ar HOSTED_BACKEND := sdl3 +# The PSP's keyboard on the screen (a PSP has none). +HOSTED_BACKEND_EXTRA := $(SRC_DIR)/hosted/psp HOSTED_DEFINES := -DJOEYLIB_PLATFORM_HOSTED -DJOEYLIB_HOSTED_SDL3 HOSTED_SDK_CFLAGS := $(shell $(SDL3_PSP_PKG_CONFIG) --cflags sdl3) -I$(PSP_SDK)/include -D_PSP_FW_VERSION=$(PSP_FW_VERSION) # Section alignment as the PSPSDK links: smaller programs. Every program @@ -45,7 +47,12 @@ PROGRAM_EXT := HOSTED_PROGRAM_DEPS := $(SDL3_PSP_LIB) # A program is linked to its ELF, then packed into its folder's EBOOT.PBP. HOSTED_LINK_OUT = $(PROGRAM_BUILD)/elf/$(call programName,$(1)).elf -HOSTED_PACK = $(PSP_PACK) $(call HOSTED_LINK_OUT,$(1)) '$(or $(call programVar,$(1),TITLE),$(call programName,$(1)))' $(call programBin,$(1)) +HOSTED_PACK = $(PSP_PACK) $(call HOSTED_LINK_OUT,$(1)) $(call shellQuote,$(or $(call programVar,$(1),TITLE),$(call programName,$(1)))) $(call HOSTED_ICON,$(1)) $(call programBin,$(1)) +# Its icon: the EBOOT's ICON0, the picture the PSP's menu shows, always +# PSP_ICON0_SIZE. +PSP_ICON0_SIZE := 144x80 +HOSTED_ICON = $(call programIconDir,$(1))/icon0.png +HOSTED_ICON_BAKE = $(ICON_BAKE) png $(call programIcon,$(1)) $(call HOSTED_ICON,$(1)) $(PSP_ICON0_SIZE) # Every program in a folder of its own, as PSP/GAME/ holds it. programDir = $(or $(call programVar,$(1),BINDIR),$(BINDIR))/$(call programName,$(1)) diff --git a/make/sdl3.mk b/make/sdl3.mk index c3cf4e0..9b6373e 100644 --- a/make/sdl3.mk +++ b/make/sdl3.mk @@ -18,7 +18,6 @@ include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk SDL3_LIB := $(SDL3_DIR)/lib/libSDL3.a $(call requireTool,$(SDL3_LIB),toolchain:sdl3) -$(call requireTool,$(LIBXMP_DIR)/include/libxmp-lite/xmp.h,toolchain:sdl3) SDL3_PKG_CONFIG := PKG_CONFIG_PATH=$(SDL3_DIR)/lib/pkgconfig pkg-config @@ -26,10 +25,16 @@ HOSTED_CC := $(HOST_CC) HOSTED_AR := ar HOSTED_BACKEND := sdl3 HOSTED_DEFINES := -DJOEYLIB_PLATFORM_HOSTED -DJOEYLIB_HOSTED_SDL3 -HOSTED_SDK_CFLAGS := $(shell $(SDL3_PKG_CONFIG) --cflags sdl3) +HOSTED_SDK_CFLAGS := $(shell $(SDL3_PKG_CONFIG) --cflags sdl3) -DHOSTED_WINDOW_ICON HOSTED_LINK := $(shell $(SDL3_PKG_CONFIG) --static --libs sdl3) PROGRAM_EXT := # A program is relinked when SDL changes. HOSTED_PROGRAM_DEPS := $(SDL3_LIB) +# Its icon on its window (HOSTED_WINDOW_ICON): SDL3_ICON_SIZE pixels square, +# baked to C and compiled, which the backend gives SDL_SetWindowIcon. +SDL3_ICON_SIZE := 64 +HOSTED_ICON = $(call programIconDir,$(1))/icon.o +HOSTED_ICON_BAKE = $(ICON_BAKE) c $(call programIcon,$(1)) $(call programIconDir,$(1))/icon.c $(SDL3_ICON_SIZE) && $(HOSTED_CC) $(COMMON_CFLAGS) -c $(call programIconDir,$(1))/icon.c -o $(call HOSTED_ICON,$(1)) +HOSTED_ICON_LINK = $(call HOSTED_ICON,$(1)) include $(REPO_DIR)/make/hosted.mk diff --git a/make/ste.mk b/make/ste.mk index 33a67a5..dd6caf2 100644 --- a/make/ste.mk +++ b/make/ste.mk @@ -4,8 +4,9 @@ # # Atari STe / Mega STe build rules: the ST port (src/atarist) built as its # STe variant, with src/atarist/ste and JOEYLIB_ATARI_STE -- the BLiTTER for -# fills and copies, DMA sound, the exact 4-bit palette, and a Mega STe's -# 16 MHz clock and cache. The rules are make/atarist.mk's; this file says +# fills and copies, DMA sound, the exact 4-bit palette, Jaguar pads in a +# 1040 STe's enhanced joystick ports, and a Mega STe's 16 MHz clock and +# cache. The rules are make/atarist.mk's; this file says # what is the STe's. Programs land in build/ste/bin. The STe build's jlInit # refuses any machine that is not an STe or Mega STe; the ST build (make # atarist) runs everywhere and never uses STe hardware. A project reaches diff --git a/make/support/atarist/include-shim/math.h b/make/support/atarist/include-shim/math.h index 156751c..101643a 100644 --- a/make/support/atarist/include-shim/math.h +++ b/make/support/atarist/include-shim/math.h @@ -3,10 +3,10 @@ // SPDX-License-Identifier: MIT // // Minimal math.h shim for the m68k-atari-mint toolchain (mintlib -// doesn't ship <math.h> or libm). Declares only what libxmp-lite -// reaches for; the implementations live in math_shim.c next to this -// header. Accuracy is sufficient for libxmp-lite's note-period -// conversion, not for general-purpose use. +// doesn't ship <math.h> or libm). Declares the few functions libxmp-lite +// once reached for; the implementations live in math_shim.c next to this +// header, built into the ST's libjoey.a. Accuracy is a few hundred parts +// per million, not for general-purpose use. #ifndef JOEY_ATARIST_MATH_SHIM_H #define JOEY_ATARIST_MATH_SHIM_H diff --git a/make/support/atarist/include-shim/math_shim.c b/make/support/atarist/include-shim/math_shim.c index f46be82..73429a9 100644 --- a/make/support/atarist/include-shim/math_shim.c +++ b/make/support/atarist/include-shim/math_shim.c @@ -2,8 +2,8 @@ // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // -// Tiny libm replacement used by libxmp-lite on the m68k-atari-mint -// toolchain. Only the functions libxmp-lite calls are implemented. +// Tiny libm replacement on the m68k-atari-mint toolchain, built into the +// ST's libjoey.a. Only the functions libxmp-lite once called are implemented. // Accuracy targets: a few hundred parts per million, plenty for // note-frequency math. @@ -36,11 +36,7 @@ static double logSeries(double m) { y = (m - 1.0) / (m + 1.0); y2 = y * y; - return 2.0 * y * (1.0 - + y2 * (1.0 / 3.0 - + y2 * (1.0 / 5.0 - + y2 * (1.0 / 7.0 - + y2 * (1.0 / 9.0))))); + return 2.0 * y * (1.0 + y2 * (1.0 / 3.0 + y2 * (1.0 / 5.0 + y2 * (1.0 / 7.0 + y2 * (1.0 / 9.0))))); } @@ -51,19 +47,21 @@ double log(double x) { return 0.0; } k = 0; - while (x >= 2.0) { x *= 0.5; k++; } - while (x < 1.0) { x *= 2.0; k--; } + while (x >= 2.0) { + x *= 0.5; + k++; + } + while (x < 1.0) { + x *= 2.0; + k--; + } return (double)k * M_LN2 + logSeries(x); } // 2^f for f in [0, 1) -- 5-term polynomial good to ~1e-6. static double exp2Frac(double f) { - return 1.0 + f * (0.69314718 - + f * (0.24022650 - + f * (0.05550410 - + f * (0.00961813 - + f * 0.00133335)))); + return 1.0 + f * (0.69314718 + f * (0.24022650 + f * (0.05550410 + f * (0.00961813 + f * 0.00133335)))); } @@ -86,8 +84,14 @@ double pow(double base, double exp) { } frac = k - (double)ki; result = exp2Frac(frac); - while (ki > 0) { result *= 2.0; ki--; } - while (ki < 0) { result *= 0.5; ki++; } + while (ki > 0) { + result *= 2.0; + ki--; + } + while (ki < 0) { + result *= 0.5; + ki++; + } return result; } diff --git a/make/support/iigs/ntpIrqWindow.py b/make/support/iigs/ntpIrqWindow.py index a5fd30b..3700fff 100755 --- a/make/support/iigs/ntpIrqWindow.py +++ b/make/support/iigs/ntpIrqWindow.py @@ -20,7 +20,9 @@ # the handler is re-entered at once, after anything the ROM's interrupt # manager puts first (the SCC is on its fast path). # Both only while the word at offset 27 of the replayer (irq_window, right -# after the documented jump table) is nonzero. audio_full.c sets it from +# after the documented jump table) is nonzero. The word after it, at offset +# 29 (playing_addr), holds the address of the replayer's playing flag, so +# audio_full.c can see a song that does not loop come to its end. audio_full.c sets it from # hal.c's tight interrupt-latency bound before every NTPstreamsound, so with # the bound loose the replayer does exactly what it always did. # @@ -36,6 +38,10 @@ TABLE_END_NEW = """ jmp streamsound ; irq_window (JoeyLib, offset 27): nonzero bounds the interrupts-off ; stretches -- see stream_window and interrupt_handler_exit. irq_window dw 0 + +; playing_addr (JoeyLib, offset 29): where the replayer keeps its playing +; flag (0 once a song played once has ended), for jlAudioIsPlayingMod. +playing_addr dw playing """ FILL = """ ldy streamsound_osc2 jsr stream_copy ;fill buffer with 512 bytes for first play diff --git a/make/support/psp/packEboot.sh b/make/support/psp/packEboot.sh index 9c0c1f9..ae4223a 100755 --- a/make/support/psp/packEboot.sh +++ b/make/support/psp/packEboot.sh @@ -5,25 +5,27 @@ # # Pack a linked PSP program into the EBOOT.PBP the PSP runs, the way the # PSPSDK's own build.mak does: fix up its module imports in place, write a -# PARAM.SFO with its title, and pack that and a stripped copy of the ELF -# (no icon, pictures or sound) into the EBOOT.PBP. The unstripped ELF stays -# where it is, for psp-addr2line. Run as make/psp.mk's packing step. +# PARAM.SFO with its title, and pack that, its icon (ICON0, a 144x80 PNG) +# and a stripped copy of the ELF (no other pictures, no sound) into the +# EBOOT.PBP. The unstripped ELF stays where it is, for psp-addr2line. Run as +# make/psp.mk's packing step. # -# Usage: packEboot.sh PROGRAM.elf TITLE EBOOT.PBP +# Usage: packEboot.sh PROGRAM.elf TITLE ICON0.png EBOOT.PBP # # Requires $PSPDEV (exported by make from the installer's # toolchains/config.mk; toolchains/env.sh for a shell). set -euo pipefail -if [[ $# -ne 3 ]]; then - echo "usage: $0 PROGRAM.elf TITLE EBOOT.PBP" >&2 +if [[ $# -ne 4 ]]; then + echo "usage: $0 PROGRAM.elf TITLE ICON0.png EBOOT.PBP" >&2 exit 2 fi elf=$1 title=$2 -eboot=$3 +icon=$3 +eboot=$4 # PARAM.SFO's MEMSIZE: the PSPSDK's default for firmware after 3.90 (the # whole user memory a model has). memSize=2 @@ -37,4 +39,4 @@ stem=${elf%.elf} "$PSPDEV/bin/psp-fixup-imports" "$elf" "$PSPDEV/bin/psp-strip" "$elf" -o "$stem.stripped.elf" "$PSPDEV/bin/mksfoex" -d MEMSIZE="$memSize" "$title" "$stem.sfo" -"$PSPDEV/bin/pack-pbp" "$eboot" "$stem.sfo" NULL NULL NULL NULL NULL "$stem.stripped.elf" NULL > /dev/null +"$PSPDEV/bin/pack-pbp" "$eboot" "$stem.sfo" "$icon" NULL NULL NULL NULL "$stem.stripped.elf" NULL > /dev/null diff --git a/make/support/web/shell.html b/make/support/web/shell.html index 950610a..2f84b85 100644 --- a/make/support/web/shell.html +++ b/make/support/web/shell.html @@ -22,6 +22,7 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>JoeyLib +