Support for using Furnace as audio tracker. Icon support for JoeyLib apps. MANY bugs fixed.
This commit is contained in:
parent
71d1cb47a4
commit
d33d9c9c25
346 changed files with 25392 additions and 8104 deletions
98
LICENSES.md
98
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:** <https://github.com/libxmp/libxmp>
|
||||
- **License:** MIT
|
||||
- **Role:** ProTracker / Amiga MOD replayer + SMIX SFX overlay. Each
|
||||
port's makefile compiles it into AUDIO programs.
|
||||
- **Installed at:** `toolchains/opt/libxmp/` (installer component `libxmp`)
|
||||
|
||||
### SDL 3.4.18
|
||||
|
||||
- **Used on:** SDL3, PSP, web
|
||||
- **URL:** <https://www.libsdl.org/>
|
||||
- **License:** zlib
|
||||
- **Role:** window, input and audio for the hosted SDL3 code. Built as a
|
||||
static library for the host, for the PSP with pspdev and for Emscripten
|
||||
- **Role:** window, input, and audio for the hosted SDL3 code. Built as a
|
||||
static library for the host, for the PSP with pspdev, and for Emscripten
|
||||
(with JoeyLib's patches, `installer/patches/sdl3Web/`, which are under
|
||||
SDL's zlib license), and linked into each program.
|
||||
- **Installed at:** `toolchains/opt/sdl3/`, `toolchains/opt/sdl3Psp/`,
|
||||
|
|
@ -60,12 +51,15 @@ most.
|
|||
|
||||
### PTPlayer 6.4
|
||||
|
||||
- **Used on:** Amiga
|
||||
- **Used on:** Amiga; and, translated to C, DOS, Atari ST, X68000, SDL3,
|
||||
PSP, web, Win16, Win32, Haiku, BeOS
|
||||
- **URL:** <http://aminet.net/mus/play/ptplayer.lha>
|
||||
(author Frank Wille, <http://aminet.net/package/mus/play/ptplayer>)
|
||||
- **License:** Public Domain (the archive's `license` file: the Unlicense)
|
||||
- **Role:** ProTracker MOD replayer + `mt_playfx` SFX channel for the
|
||||
Paula audio HAL.
|
||||
Paula audio HAL. Its replay, translated to C (with its period table), is
|
||||
JoeyLib's module player, `src/core/modPlayer.c`, which every port that
|
||||
mixes modules in software links.
|
||||
- **Installed at:** `toolchains/opt/ptplayer/` (installer component `ptplayer`)
|
||||
|
||||
### CWSDPMI r7
|
||||
|
|
@ -84,7 +78,7 @@ most.
|
|||
|
||||
### The toolchains' C libraries and start files
|
||||
|
||||
Each toolchain below also links its own C library, start files and
|
||||
Each toolchain below also links its own C library, start files, and
|
||||
compiler runtime into the programs it builds. What a shipped program owes
|
||||
them, in brief (the entries below have the detail):
|
||||
|
||||
|
|
@ -92,7 +86,7 @@ them, in brief (the entries below have the detail):
|
|||
source if its users are told how to get DJGPP (`copying.dj`). DVX
|
||||
programs are modules DVX loads, built with DVX's SDK.
|
||||
- **Atari ST:** MiNTLib, file by file under its own license (COPYMINT),
|
||||
the LGPL 2 or later, BSD, Henry Spencer's and musl's MIT terms; a linked
|
||||
the LGPL 2 or later, BSD, and Henry Spencer's and musl's MIT terms; a linked
|
||||
program meets all of them (MiNTLib's `LICENSES`).
|
||||
- **Amiga:** libnix (public domain).
|
||||
- **X68000:** newlib (a collection of permissive licenses,
|
||||
|
|
@ -177,7 +171,7 @@ above.
|
|||
(a fork of llvm-mos, <https://github.com/llvm-mos/llvm-mos>)
|
||||
- **License:** Apache License 2.0 with LLVM Exceptions, llvm-mos's (and
|
||||
LLVM's) license, for all of it: llvm-mos's code and the W65816 backend,
|
||||
link816, omfEmit and the runtime (its `LICENSE.TXT`). Its README lists
|
||||
link816, omfEmit, and the runtime (its `LICENSE.TXT`). Its README lists
|
||||
the third-party code it holds under other licenses (the C++ runtime's
|
||||
Embedded Template Library, MIT; test and demo sources), none of which
|
||||
reaches a JoeyLib program.
|
||||
|
|
@ -256,7 +250,7 @@ above.
|
|||
(Vincent Riviere's builds; mintlib: <https://github.com/freemint/mintlib>)
|
||||
- **License:** gcc 15.2 and binutils 2.45: GNU GPL v3, with the GCC
|
||||
Runtime Library Exception for libgcc. MiNTLib 0.60.1: per file, under
|
||||
the MiNTLib license (COPYMINT), the LGPL 2 or later, BSD, Henry
|
||||
the MiNTLib license (COPYMINT), the LGPL 2 or later, BSD, or Henry
|
||||
Spencer's or musl's MIT terms; a program linked with it meets all of
|
||||
them (MiNTLib's `LICENSES`).
|
||||
- **Role:** GEMDOS-native gcc + binutils + mintlib used to compile
|
||||
|
|
@ -322,9 +316,9 @@ above.
|
|||
|
||||
- **Used on:** Win32
|
||||
- **URL:** <https://github.com/mstorsjo/llvm-mingw>
|
||||
- **License:** clang, lld and the LLVM runtimes: Apache License 2.0 with
|
||||
- **License:** clang, lld, and the LLVM runtimes: Apache License 2.0 with
|
||||
LLVM Exceptions. mingw-w64's headers and runtime: Zope Public License
|
||||
2.1, with parts marked public domain, BSD or LGPL.
|
||||
2.1, with parts marked public domain, BSD, or LGPL.
|
||||
- **Role:** i686 cross compiler for the win32 port's programs, which link
|
||||
Windows' MSVCRT.DLL.
|
||||
- **Installed at:** `toolchains/opt/llvmMingw/` (installer component
|
||||
|
|
@ -338,7 +332,7 @@ above.
|
|||
Library Exception for libgcc. The PSPSDK: BSD. newlib: its collection
|
||||
of permissive licenses. Every library pspdev carries has its license
|
||||
under `psp/share/licenses/`.
|
||||
- **Role:** psp-gcc, the PSPSDK and the tools that make an EBOOT.PBP.
|
||||
- **Role:** psp-gcc, the PSPSDK, and the tools that make an EBOOT.PBP.
|
||||
- **Installed at:** `toolchains/opt/pspdev/` (installer component `pspdev`)
|
||||
|
||||
### Emscripten 6.0.11 (emsdk)
|
||||
|
|
@ -376,10 +370,45 @@ above.
|
|||
distribution, keeping its notice).
|
||||
- **Role:** `package` unpacks the `.hpkg` packages the sysroots are made
|
||||
from; `bfs_shell` reads and writes the BFS disks the VMs take their jobs
|
||||
on.
|
||||
on; `xres` puts every Be API program's icons into its resources.
|
||||
- **Installed at:** `toolchains/opt/haikuTools/` (installer component
|
||||
`haikuTools`)
|
||||
|
||||
### The BeOS palette (in `tools/iconBake.py`)
|
||||
|
||||
- **Used on:** Haiku, Haiku x86, BeOS
|
||||
- **URL:** <https://github.com/haiku/haiku> (`headers/private/interface/Palette.h`)
|
||||
- **License:** MIT (Haiku).
|
||||
- **Role:** the system's 256 colours, copied into `tools/iconBake.py` as
|
||||
`BE_PALETTE`, which the Be API programs' icons are baked to (an icon is
|
||||
indices into it; the palette itself is not in the program).
|
||||
- **Installed at:** nowhere; it is part of `tools/iconBake.py`.
|
||||
|
||||
---
|
||||
|
||||
### libxmp-lite 4.7.0
|
||||
|
||||
- **Used on:** none. It played modules on DOS, the ST, the X68000 and the
|
||||
hosted ports until 2026-10-14, when JoeyLib's own module player replaced
|
||||
it; the installer still installs it (component `libxmp`).
|
||||
- **URL:** <https://github.com/libxmp/libxmp>
|
||||
- **License:** MIT
|
||||
- **Installed at:** `toolchains/opt/libxmp/` (installer component `libxmp`)
|
||||
|
||||
## Content tools (optional; not shipped, not linked)
|
||||
|
||||
### Furnace 0.6.8.3
|
||||
|
||||
- **Used on:** every port (chip-tracker music, through `tools/vgm2song.py`)
|
||||
- **URL:** <https://github.com/tildearrow/furnace>
|
||||
- **License:** GPLv2 or later. Its demo songs are not under the GPL: they are
|
||||
their authors', all rights reserved, and nothing made from them is kept or
|
||||
shipped (`scripts/check-vgm.sh` converts them in a scratch folder).
|
||||
- **Role:** the recommended tracker for writing chip-tracker songs
|
||||
(docs/Assets.adoc); its VGM export is what `tools/vgm2song.py` converts.
|
||||
- **Installed at:** `toolchains/opt/furnace/` (installer component `furnace`,
|
||||
optional: `./install.sh install furnace`)
|
||||
|
||||
---
|
||||
|
||||
## Test-time emulators (not shipped, not linked)
|
||||
|
|
@ -478,13 +507,13 @@ Installed from the Linux distribution by the components named below
|
|||
numpy (BSD), installed by hand with pip. The script holds none of
|
||||
potracer's code, and the logo it made is not covered by potracer's
|
||||
license.
|
||||
- Building MAME, QEMU, PPSSPP and SDL 3 also takes the distribution's
|
||||
development packages (SDL2, X11, Wayland, PulseAudio, ALSA, GLib and the
|
||||
- Building MAME, QEMU, PPSSPP, and SDL 3 also takes the distribution's
|
||||
development packages (SDL2, X11, Wayland, PulseAudio, ALSA, GLib, and the
|
||||
rest), each under its own license.
|
||||
|
||||
---
|
||||
|
||||
## System ROMs, operating systems and disk images (used for testing only)
|
||||
## System ROMs, operating systems, and disk images (used for testing only)
|
||||
|
||||
These are **not** part of JoeyLib distribution. `./install.sh`
|
||||
places them in `toolchains/firmware/` or `toolchains/opt/` for
|
||||
|
|
@ -539,10 +568,23 @@ project hosts (`JOEY_BUNDLE_URL`); matched by the checksums in
|
|||
Trio64V+ display driver** (its `.zip`): Microsoft and S3 proprietary.
|
||||
Installed unattended into the DOSBox PC every win16 run uses. A Win16
|
||||
program's users bring their own Windows. Component `win311`.
|
||||
- **Microsoft WinG 1.0 SDK** (`WING10.EXE`): Microsoft proprietary, under
|
||||
its Microsoft Development License (`LICENSE.TXT`, installed beside the
|
||||
runtime): the SDK may be used to design, develop, and test software; its
|
||||
runtime (`WING.DLL`, `WING32.DLL`, `WINGDE.DLL`, `WINGDIB.DRV`,
|
||||
`WINGPAL.WND`, `SETUPHLP.DLL`, `DVA.386`) may be redistributed only as part
|
||||
of your own application, not for further redistribution, and with an
|
||||
indemnity to Microsoft. Microsoft no longer offers it; archive.org keeps a
|
||||
copy (<https://archive.org/details/WING10>). The win16 port uses WinG when
|
||||
it is installed on the machine and draws through GDI without it, so
|
||||
JoeyLib never ships it; a game that wants WinG on its users' machines
|
||||
ships the runtime under those terms. The installer takes the runtime out
|
||||
of the SDK into `toolchains/opt/wing/` and the win16 gates install it in
|
||||
their Windows. Component `wing`.
|
||||
- **BeOS 5 Professional CD** (its image): Be, Inc. proprietary; the terms
|
||||
for its start files are Unknown. Its boot floppy and x86 system become
|
||||
the BeOS R5 VM (component `beosVm`), and its Development package's
|
||||
headers, start files and system libraries the beos port's sysroot
|
||||
headers, start files, and system libraries the beos port's sysroot
|
||||
(component `beosR5`). Every beos program links the start files
|
||||
(`start_dyn.o`, `init_term_dyn.o`, `crti.o`, `crtn.o`), as every program
|
||||
built with Be's own tools did; check their terms before shipping one.
|
||||
|
|
|
|||
14
Makefile
14
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"
|
||||
|
|
|
|||
132
README.md
132
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.
|
||||
|
|
|
|||
BIN
assets/joeyIcon.png
(Stored with Git LFS)
Normal file
BIN
assets/joeyIcon.png
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -2,25 +2,32 @@
|
|||
|
||||
Declared in `<joey/audio.h>`.
|
||||
|
||||
There are three layers, and on two platforms they share physical hardware:
|
||||
There are three layers, and on some platforms they share physical hardware:
|
||||
|
||||
* *Module playback* -- `jlAudioPlayMod`, four-channel Protracker `.MOD`.
|
||||
* *Chip tracker* -- `jlMusicPlay`, a portable event stream driving three tone
|
||||
voices plus noise at register-write cost, with no CPU mixing. The recommended
|
||||
music path.
|
||||
* *Voices and effects* -- `jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx` and
|
||||
* *Voices and effects* -- `jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and
|
||||
the streaming forms.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
On the Atari ST and Amiga the tone voices *are* the hardware the module player
|
||||
drives. While a `.MOD` plays, `jlAudioVoice` writes fight the player's register
|
||||
writes and lose. Choose the module layer or the voice and tracker layer per
|
||||
scene, never both at once.
|
||||
On the Amiga the tone voices and the noise *are* the hardware the module
|
||||
player drives: Paula's four channels. While a `.MOD` plays, `jlAudioVoice` and
|
||||
`jlAudioNoise` writes fight the player's register writes and lose. On the
|
||||
plain ST (not the STe build) digital sound -- a module, an effect, or a
|
||||
stream -- plays through the sound chip's channel A, so tone voice 0 is silent
|
||||
while it plays and sounds again when it stops; voices 1 and 2 play on. Choose
|
||||
the module layer or the voice and tracker layer per scene, never both at once.
|
||||
====
|
||||
|
||||
A failed `jlAudioInit` is not fatal: `jlInit` still succeeds and every audio
|
||||
call becomes a silent no-op.
|
||||
`jlInit` starts audio and `jlShutdown` stops it; a program that wants none
|
||||
sets `jlConfigT.noAudio`, and every audio call is then silent. Sound that does
|
||||
not start is not fatal: `jlInit` still succeeds, modules and sound effects are
|
||||
silent, `jlAudioTickRegister` refuses, and the log says so. The tone and noise
|
||||
voices, and so `jlMusicPlay`, still play where they drive a sound chip of
|
||||
their own: on the Amiga, the ST, DOS, and the X68000.
|
||||
|
||||
=== Constants
|
||||
|
||||
|
|
@ -28,8 +35,8 @@ call becomes a silent no-op.
|
|||
|===
|
||||
|Name |Value |Meaning
|
||||
|
||||
|`JOEY_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4.
|
||||
|`JOEY_AUDIO_VOICES` |3 |PSG-style tone voices, 0..2.
|
||||
|`JL_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4.
|
||||
|`JL_AUDIO_VOICES` |3 |PSG-style tone voices, 0..2.
|
||||
|`JL_SFX_RAW_SIGNED8` |0 |Raw stream format: `int8_t` PCM.
|
||||
|`JL_SFX_RAW_UNSIGNED8_NOZERO` |1 |Raw stream format: `uint8_t` biased 0x80,
|
||||
and 0x00 is forbidden (it is the DOC stop byte).
|
||||
|
|
@ -37,34 +44,15 @@ call becomes a silent no-op.
|
|||
|
||||
[TIP]
|
||||
====
|
||||
Prefer *slot 4* for game sound effects when music is playing. The IIgs maps
|
||||
tracker tone voices onto slots 0..2 and the noise voice onto slot 3. On the
|
||||
other ports the slots are independent of the voice layer.
|
||||
Prefer *slot 4* for game sound effects when music is playing. The IIgs plays
|
||||
tone voices 0..2 on sound effect slots 0..2 and the noise on slot 3: an effect
|
||||
and a voice on the same slot cut each other off, and a voice still being held
|
||||
takes its slot back at the next `jlAudioFrameTick`. On the Amiga effects, the
|
||||
module, the voices, and the noise share Paula's four channels, so an effect can
|
||||
take a channel a voice is using. On the other ports the slots are independent
|
||||
of the voice layer.
|
||||
====
|
||||
|
||||
=== jlAudioInit
|
||||
|
||||
[source,c]
|
||||
----
|
||||
bool jlAudioInit(void);
|
||||
----
|
||||
|
||||
*Parameters:* none.
|
||||
|
||||
*Returns:* `true` if the platform has a working audio engine and started it;
|
||||
`false` silently otherwise, with the rest of the API still callable but silent.
|
||||
|
||||
=== jlAudioShutdown
|
||||
|
||||
[source,c]
|
||||
----
|
||||
void jlAudioShutdown(void);
|
||||
----
|
||||
|
||||
Tears the engine down. Safe when audio was never initialised.
|
||||
|
||||
*Parameters:* none. *Returns:* nothing.
|
||||
|
||||
=== jlAudioFrameTick
|
||||
|
||||
[source,c]
|
||||
|
|
@ -72,10 +60,12 @@ Tears the engine down. Safe when audio was never initialised.
|
|||
void jlAudioFrameTick(void);
|
||||
----
|
||||
|
||||
Hooks the engine into your game loop. Most platforms drive their engines from a
|
||||
hardware interrupt and ignore this, but it is required for any port that mixes
|
||||
in user context, it advances tracker playback, and on IIgs and Amiga it is what
|
||||
fires the next chunk of a streaming sound effect.
|
||||
Hooks the engine into your game loop. Every port does its sound work here: DOS,
|
||||
the ST, the X68000, DVX, and the hosted ports mix or refill their sound buffers
|
||||
in it, so without it modules and effects stop or repeat; the IIgs and Amiga
|
||||
feed each streaming sound effect its next chunk, notice a module's end, and
|
||||
(on the IIgs) keep held notes sounding; and on every port it advances
|
||||
`jlMusicPlay`.
|
||||
|
||||
Call it once per frame, unconditionally.
|
||||
|
||||
|
|
@ -93,8 +83,9 @@ Begins module playback. If a module is already playing it is replaced.
|
|||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|Parameter |Type |Description
|
||||
|`data` |`const uint8_t *` |Platform-native module blob: `.mod` on most ports,
|
||||
`.amod` on Amiga, `.ntp` on IIgs. The asset pipeline produces the right form.
|
||||
|`data` |`const uint8_t *` |Platform-native module blob: `.mod` on every port
|
||||
but the IIgs (an `.amod`, with its end marker, plays on the Amiga too), `.ntp`
|
||||
on the IIgs. The asset pipeline produces the right form.
|
||||
|`length` |`uint32_t` |Blob size in bytes.
|
||||
|`loop` |`bool` |`true` plays forever.
|
||||
|===
|
||||
|
|
@ -103,9 +94,15 @@ Begins module playback. If a module is already playing it is replaced.
|
|||
|
||||
[NOTE]
|
||||
====
|
||||
`loop = false` needs help on the Amiga: PTPlayer has no native song-end signal,
|
||||
so the module must contain an `E8FF` marker at song end. The `joeymod` tool's
|
||||
`.amod` output injects it. A plain `.mod` with no marker loops anyway on Amiga.
|
||||
`loop = false` plays the song once and stops it. The IIgs's replayer has a
|
||||
play-once mode; the ports that mix modules in software stop where the song
|
||||
comes back to a row it has played (its end, or a jump back), at an `F00`, or
|
||||
at the Amiga's `E8F` stop marker; and the Amiga adds that marker as it copies
|
||||
the module to chip RAM, sending the song's jumps back to it
|
||||
(<<_music_protracker_modules>>). A module with no room for one, all 128 song
|
||||
positions or all 128 patterns in use, loops there anyway, and so does one
|
||||
whose jump back sits in a pattern it also plays where that jump goes
|
||||
forward.
|
||||
====
|
||||
|
||||
=== jlAudioStopMod
|
||||
|
|
@ -130,7 +127,8 @@ bool jlAudioIsPlayingMod(void);
|
|||
*Parameters:* none.
|
||||
|
||||
*Returns:* `true` while a module is producing output. `false` after a stop,
|
||||
before the first play, and on platforms where audio init failed.
|
||||
once a song played with `loop = false` has ended, before the first play, and on
|
||||
platforms where audio init failed.
|
||||
|
||||
=== jlAudioPlaySfx
|
||||
|
||||
|
|
@ -146,7 +144,7 @@ new sample replaces it.
|
|||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|Parameter |Type |Description
|
||||
|`slot` |`uint8_t` |Slot 0..`JOEY_AUDIO_SFX_SLOTS`-1. Prefer 4 alongside music.
|
||||
|`slot` |`uint8_t` |Slot 0..`JL_AUDIO_SFX_SLOTS`-1. Prefer 4 alongside music.
|
||||
|`sample` |`const uint8_t *` |Raw signed 8-bit PCM.
|
||||
|`length` |`uint32_t` |Sample count in bytes.
|
||||
|`rateHz` |`uint16_t` |The rate the sample was recorded at; the engine
|
||||
|
|
@ -155,6 +153,12 @@ new sample replaces it.
|
|||
|
||||
*Returns:* nothing.
|
||||
|
||||
Most ports read the sample where it is while it plays, so keep it valid and
|
||||
unchanged until it has finished or the slot has been stopped. The IIgs also
|
||||
remembers the last sample each slot played by its address, length, and rate,
|
||||
and plays its own copy when they match: changed bytes at the same address,
|
||||
length, and rate are not heard there.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The full -128..127 range is accepted everywhere, but the IIgs plays -128 as
|
||||
|
|
@ -175,9 +179,9 @@ deactivates.
|
|||
|
||||
[WARNING]
|
||||
====
|
||||
This is called from the audio engine's refill context -- the main thread on DOS,
|
||||
ST and SDL3 (on DVX, DVX's own sound pump, while the program is yielded), but the
|
||||
*audio interrupt* on Amiga and IIgs. It must not block and must not allocate.
|
||||
On every port it is called in program time, from the call that starts the
|
||||
stream and then from `jlAudioFrameTick` (on DVX, also while the desktop runs).
|
||||
It must not block, and it should be quick: it runs inside those calls.
|
||||
====
|
||||
|
||||
=== jlAudioPlaySfxStream
|
||||
|
|
@ -206,10 +210,11 @@ with no caller-side allocation.
|
|||
|
||||
[IMPORTANT]
|
||||
====
|
||||
On IIgs and Amiga the engine plays one chunk at a time and fires the next from
|
||||
`jlAudioFrameTick`, so *you must be calling `jlAudioFrameTick` every frame* or
|
||||
the stream stops after its first chunk. A chunk seam on those ports can carry
|
||||
up to one frame of jitter.
|
||||
Every port feeds streams from `jlAudioFrameTick`, so *you must be calling
|
||||
`jlAudioFrameTick` every frame* or the stream stops. On the IIgs and Amiga the
|
||||
engine plays one chunk at a time (7 KB on the IIgs, 4 KB on the Amiga) and
|
||||
fires the next from it, so a chunk seam on those ports can carry up to one
|
||||
frame of jitter.
|
||||
====
|
||||
|
||||
=== jlAudioSfxRawFormat / jlAudioPlaySfxStreamRaw
|
||||
|
|
@ -226,10 +231,10 @@ void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill,
|
|||
|
||||
`jlAudioPlaySfxStreamRaw` is `jlAudioPlaySfxStream` except your callback
|
||||
delivers bytes *already in that format*, so the port skips its per-byte
|
||||
conversion of every chunk. On the IIgs that conversion is a 65816 byte loop
|
||||
over each 3.5 KB chunk, and it is enough to stall a game for a third of a
|
||||
second per chunk of speech. Every other port streams signed PCM natively, so
|
||||
there this is simply the plain call.
|
||||
conversion of every chunk. On the IIgs that conversion costs about 7
|
||||
microseconds a byte, some 50 ms for each 7 KB chunk. Every other port's raw
|
||||
format is signed PCM, so there this is simply the plain call (DVX still flips
|
||||
each byte for its sound library).
|
||||
|
||||
If your producer cannot make the port's format, use `jlAudioPlaySfxStream`.
|
||||
|
||||
|
|
@ -240,7 +245,7 @@ If your producer cannot make the port's format, use `jlAudioPlaySfxStream`.
|
|||
[source,c]
|
||||
----
|
||||
typedef enum {
|
||||
AUDIO_PAN_LEFT = 0, AUDIO_PAN_CENTER, AUDIO_PAN_RIGHT, AUDIO_PAN_COUNT
|
||||
JL_AUDIO_PAN_LEFT = 0, JL_AUDIO_PAN_CENTER, JL_AUDIO_PAN_RIGHT, JL_AUDIO_PAN_COUNT
|
||||
} jlAudioPanE;
|
||||
|
||||
void jlAudioSetPan(uint8_t slot, jlAudioPanE pan);
|
||||
|
|
@ -268,14 +273,16 @@ What each port can actually do:
|
|||
|IIgs |All three. The DOC pans per oscillator, so a centred sound plays on two
|
||||
oscillators, one per side. A stock IIgs sums to mono, so this shows up only on
|
||||
a stereo card or an emulator.
|
||||
|Amiga |Left and right only, by choosing the Paula channel side. Paula cannot
|
||||
place one voice in both ears, so `AUDIO_PAN_CENTER` leaves PTPlayer's own
|
||||
channel choice alone.
|
||||
|Amiga |Left and right by choosing the Paula channel side; a Paula channel plays
|
||||
on one side only. A centred sound plays on a free channel each side when both
|
||||
sides have one, so it reaches both ears; otherwise on the one channel
|
||||
PTPlayer picks.
|
||||
|X68000 |Ignored. Everything mixes into one ADPCM stream, so panning would move
|
||||
all audio at once.
|
||||
|DOS, ST |Ignored. Output is mono.
|
||||
|DOS, ST, STe |Ignored. Output is mono.
|
||||
|DVX |All three, through DVX's stereo mixer (on a stereo card).
|
||||
|SDL3 |All three: the port mixes in stereo.
|
||||
|SDL3, web, Win32, PSP, Haiku, BeOS |All three: the port mixes in stereo.
|
||||
|Win16 |Ignored. Output is mono.
|
||||
|===
|
||||
|
||||
=== jlAudioStopSfx
|
||||
|
|
@ -300,13 +307,12 @@ void jlAudioStopSfx(uint8_t slot);
|
|||
void jlAudioTone(uint16_t freqHz);
|
||||
----
|
||||
|
||||
Direct hardware tone generator: plays a square wave continuously until the next
|
||||
call. Designed for AGI-era music where the engine programs a divisor and lets
|
||||
the chip play, so CPU cost during playback is zero. Single voice; uses voice 0
|
||||
on platforms with several oscillators. On DOS this programs PIT counter 2 and
|
||||
the speaker gate; on DVX it is a square tone of DVX's sound library, on the FM
|
||||
chip when there is one; on SDL3 it is voice 0 of the port's mixer at full
|
||||
volume.
|
||||
A single tone that plays until the next call: `jlAudioVoice(0, freqHz, 0)`,
|
||||
tone voice 0 at full volume, on every port. Designed for AGI-era music where
|
||||
the engine programs a divisor and lets the chip play, so CPU cost during
|
||||
playback is zero. A program that mixes it with `jlAudioVoice` or `jlMusicPlay`
|
||||
shares voice 0 with them. On DOS without an OPL2 (AdLib) chip, voice 0, and so
|
||||
this tone, is the PC speaker.
|
||||
|
||||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|
|
@ -314,8 +320,7 @@ volume.
|
|||
|`freqHz` |`uint16_t` |Frequency in Hz. 0 silences the generator.
|
||||
|===
|
||||
|
||||
*Returns:* nothing. A no-op on platforms with no reachable tone generator --
|
||||
fall back to `jlAudioPlaySfxStream` there if you need synthesised sound.
|
||||
*Returns:* nothing. Silent wherever voice 0 is (see `jlAudioVoice`).
|
||||
|
||||
=== jlAudioVoice
|
||||
|
||||
|
|
@ -329,33 +334,32 @@ Three-voice PSG-style polyphonic tone generator.
|
|||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|Parameter |Type |Description
|
||||
|`voice` |`uint8_t` |Voice 0, 1 or 2.
|
||||
|`voice` |`uint8_t` |Voice 0, 1, or 2.
|
||||
|`freqHz` |`uint16_t` |Frequency in Hz. 0 silences this voice.
|
||||
|`atten` |`uint8_t` |AGI / SN76489-style 4-bit attenuation: 0 loudest,
|
||||
15 silent, about 2 dB per step.
|
||||
15 silent, 2 dB per step as near as the chip comes (the ST's YM2149 has
|
||||
sixteen levels about 3.5 dB apart, so a step there is within 1.6 dB).
|
||||
|===
|
||||
|
||||
*Returns:* nothing.
|
||||
|
||||
Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2; the ST uses
|
||||
YM2149 channels A, B and C; the Amiga uses Paula channels 0..2 looping a
|
||||
two-byte square sample; the IIgs uses Ensoniq DOC oscillators playing a
|
||||
resident square; DVX plays square tones of its sound library, which puts them
|
||||
on the FM chip when there is a free voice and mixes them otherwise; SDL3 mixes
|
||||
square waves in software.
|
||||
|
||||
Voice state is sticky -- a voice keeps its last frequency until you change it
|
||||
-- and CPU cost during sustained playback is zero, because the hardware
|
||||
oscillates on its own.
|
||||
Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2 (with no OPL2,
|
||||
voice 0 is the PC speaker and voices 1 and 2 are silent); the ST uses YM2149
|
||||
channels A, B, and C (on the plain ST voice 0 is silent while digital sound
|
||||
plays); the Amiga uses Paula channels 0..2 looping a two-byte square sample;
|
||||
the IIgs uses Ensoniq DOC oscillators playing a resident square on sound
|
||||
effect slots 0..2; the X68000 uses YM2151 channels 0..2, one operator each,
|
||||
so its tones are sine waves; DVX plays square tones of its sound library,
|
||||
which puts them on the FM chip when there is a free voice and mixes them
|
||||
otherwise; the hosted ports mix square waves in software. The OPL2's waves
|
||||
are not square either.
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
The IIgs is the exception to stickiness: each call is an approximately 80 ms
|
||||
retriggered one-shot, so a note held longer must be re-asserted about once a
|
||||
frame. It also requires `jlAudioInit`, because the DOC path rides the tracker
|
||||
engine. On DOS, ST and Amiga this is direct hardware programming and works
|
||||
without `jlAudioInit`; on DVX it opens the program's sound on first use. On
|
||||
SDL3 the voices are mixed in software and sound only after `jlAudioInit`.
|
||||
A note sounds until the next call for that voice. The IIgs plays each note as
|
||||
a sample a fraction of a second long and restarts the notes being held from
|
||||
`jlAudioFrameTick`, so call that every frame, as every program should; a frame
|
||||
much longer than 80 ms can leave a gap in a held note there.
|
||||
====
|
||||
|
||||
=== jlAudioNoise
|
||||
|
|
@ -378,13 +382,17 @@ layer.
|
|||
|
||||
*Returns:* nothing.
|
||||
|
||||
Mapping: the ST uses the YM2149 noise generator mixed onto channel C; DOS uses
|
||||
the OPL2 rhythm-mode snare, leaving the three tone channels untouched; the
|
||||
Amiga uses Paula channel 3 with an LFSR noise sample; the IIgs uses a resident
|
||||
noise sample on slot 3 with the same retriggered one-shot model as its tone
|
||||
voices; DVX uses a noise tone of its sound library; SDL3 mixes the YM2149's
|
||||
own noise in software (its 17-bit generator, clocked at 125 kHz divided by
|
||||
the pitch), so it sounds as on the ST.
|
||||
The noise runs at 125000 / `pitch` Hz (`pitch` 0 as 1), the YM2149's rate, on
|
||||
every port as far as its chip goes. Mapping: the ST uses the YM2149 noise
|
||||
generator mixed onto channel C; the X68000 the YM2151's noise, at the same
|
||||
rates exactly; the Amiga Paula channel 3 looping an LFSR noise sample, whose
|
||||
fastest rate, 28.6 kHz, pitches 0 to 4 all play at; the IIgs a resident noise
|
||||
sample on slot 3, up to 26 kHz, likewise for pitches 0 to 4; DVX a noise tone
|
||||
of its sound library; and the hosted ports mix the YM2149's own noise in
|
||||
software (its 17-bit generator), so it sounds as on the ST. DOS uses the OPL2
|
||||
rhythm-mode snare, leaving the three tone channels untouched: a tuned drum,
|
||||
not free-running noise, whose pitch spreads over the snare's range rather than
|
||||
following the YM2149's rate.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
|
|
@ -406,7 +414,7 @@ is no CPU mixing, this is fully viable on the ST and cheap on DOS, which
|
|||
`.MOD` playback is not.
|
||||
|
||||
Playback is pumped by `jlAudioFrameTick` and paced by `jlMillisElapsed`, so
|
||||
tempo holds across 50, 60 and 70 Hz ports.
|
||||
tempo holds across 50, 60, and 70 Hz ports.
|
||||
|
||||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|
|
@ -419,13 +427,8 @@ tempo holds across 50, 60 and 70 Hz ports.
|
|||
|===
|
||||
|
||||
*Returns:* `true` on success; `false` on a malformed stream, in which case
|
||||
nothing plays.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Call `jlAudioInit` first for the song to be audible on the IIgs -- see
|
||||
`jlAudioVoice`.
|
||||
====
|
||||
nothing plays. With `jlConfigT.noAudio` the song runs silently, its time and
|
||||
its end as usual.
|
||||
|
||||
=== jlMusicStop
|
||||
|
||||
|
|
@ -507,35 +510,55 @@ frame, so the chip only hears the last write and short notes vanish.
|
|||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|Parameter |Type |Description
|
||||
|`fn` |`jlAudioTickFnT` |Callback, fired from interrupt context. Pass `NULL` to
|
||||
uninstall.
|
||||
|`fn` |`jlAudioTickFnT` |Callback, which may be fired from an interrupt. Pass
|
||||
`NULL` to uninstall.
|
||||
|`hz` |`uint16_t` |Firings per second.
|
||||
|===
|
||||
|
||||
*Returns:* `true` if installed. Ports without a suitable timer source return
|
||||
`false`.
|
||||
*Returns:* `true` if installed. `false` when `hz` is 0, when audio is off or
|
||||
did not start, or when the port cannot take the rate (above 1000 Hz on DVX, or
|
||||
above the mixer's rate on the hosted ports). Uninstalling always returns
|
||||
`true`.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
`fn` runs in interrupt context. Keep it to memory reads and direct hardware
|
||||
register writes -- no allocation, no `printf`, nothing blocking.
|
||||
`fn` may run in an interrupt: it does on DOS, the Amiga, the ST, and the
|
||||
X68000. Keep it to memory reads and direct hardware register writes -- no
|
||||
allocation, no `printf`, nothing blocking -- and share data with it through
|
||||
`jlAudioCriticalEnter` and `jlAudioCriticalExit`.
|
||||
====
|
||||
|
||||
On every port the calls average exactly `hz` a second; where they fall
|
||||
differs. DOS, the Amiga, and the hosted ports place each one at its own time.
|
||||
The ST and X68000 deliver them from the system's own timer, 200 and 100 times a
|
||||
second, so the calls due between two of its ticks come together. The IIgs and
|
||||
DVX deliver them in program time from `jlAudioFrameTick`, so a frame's calls
|
||||
come together at it.
|
||||
|
||||
On DOS this reprograms PIT counter 0 and hooks INT 8, keeping the BIOS tick
|
||||
counter advancing at the canonical 18.2 Hz through an accumulator so `time`
|
||||
stays correct. `jlMillisElapsed` carries on from where it was when the tick
|
||||
starts or stops; it never jumps back.
|
||||
|
||||
On the Amiga a `timer.device` request on the E-clock raises a software
|
||||
interrupt at each tick. On the ST the tick rides TOS's 200 Hz Timer C
|
||||
interrupt, and on the X68000 Human68k's 100 Hz Timer C interrupt.
|
||||
|
||||
On the IIgs `fn` runs in program time, at `jlAudioFrameTick`, as many times as
|
||||
the display's beats since the last frame have made due, so the average is
|
||||
exact whatever the program's frame rate.
|
||||
|
||||
On DVX a timer of DVX's (at most 1000 Hz) counts the ticks, and `fn` runs in
|
||||
program time, once per counted tick, at the next `jlAudioFrameTick`: DVX's
|
||||
sound library, which `jlAudioVoice` and the rest go through there, may not be
|
||||
called from an interrupt. `fn` may therefore do anything, but what it does
|
||||
lands on the frame, not between frames.
|
||||
|
||||
On SDL3 the sound is mixed in program time, a few frames ahead, and `fn` is
|
||||
called by the mixer between exactly the two samples where its tick falls: in
|
||||
program time, so it may do anything, yet a note it starts begins at the right
|
||||
place in the sound. `hz` may be anything up to the mixer's 44100.
|
||||
On the hosted ports the sound is mixed in program time, a few frames ahead,
|
||||
and `fn` is called by the mixer between exactly the two samples where its tick
|
||||
falls: in program time, so it may do anything, yet a note it starts begins at
|
||||
the right place in the sound. `hz` may be anything up to the mixer's rate,
|
||||
44100 (22050 on Win16).
|
||||
|
||||
=== jlAudioCriticalEnter / jlAudioCriticalExit
|
||||
|
||||
|
|
@ -547,8 +570,10 @@ void jlAudioCriticalExit(void);
|
|||
|
||||
Critical section against the tick interrupt. If you mutate state that the
|
||||
registered tick callback reads, wrap the mutation in these so the interrupt
|
||||
cannot observe a half-written value. On DOS this is `cli` and `sti`; on ports
|
||||
with no audio interrupt, and on DVX and SDL3, where the callback runs in
|
||||
program time, both are no-ops.
|
||||
cannot observe a half-written value. On DOS, the Amiga, the ST, and the
|
||||
X68000 a section holds every interrupt off, so keep it short. Sections nest:
|
||||
only the outermost pair does anything, and its Exit puts back the interrupt
|
||||
state its Enter found. On the IIgs, DVX, and the hosted ports, whose
|
||||
callbacks run in program time, they do nothing, and need not.
|
||||
|
||||
*Parameters:* none. *Returns:* nothing.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ Declared in `<joey/core.h>`, included by `<joey/joey.h>`.
|
|||
----
|
||||
typedef struct {
|
||||
uint32_t codegenBytes;
|
||||
bool noAudio;
|
||||
} jlConfigT;
|
||||
----
|
||||
|
||||
|
|
@ -18,13 +19,26 @@ typedef struct {
|
|||
|`codegenBytes`
|
||||
|`uint32_t`
|
||||
|Size of the runtime compiled-sprite cache. `0` selects a per-platform
|
||||
default: 8 KB on IIgs, 32 KB on Amiga, ST, DOS and DVX (SDL3 interprets every
|
||||
sprite and leaves the cache unused). On the IIgs the cache is
|
||||
one Memory Manager block that may not span a 64 KB bank boundary, so values
|
||||
above 65536 are clamped to 65536, and even a clamped request can fail if no
|
||||
free bank exists. Size it against `jlSpriteCompiledSize()`.
|
||||
default: 8 KB on the IIgs, 64 KB on the Amiga and ST, and 32 KB on the
|
||||
X68000, DOS, DVX, and Win16. The other hosted ports interpret every sprite
|
||||
and never use the cache, but still allocate it (32 KB by default). The
|
||||
cache is one block, and `jlInit` fails if it cannot be had -- except on the
|
||||
IIgs, where it is up to four Memory Manager blocks of at most 64 KB, each
|
||||
inside one 64 KB bank, so at most 256 KB; a bank that is refused is tried
|
||||
again at 60 KB, and `jlInit` takes whatever part of the request it was
|
||||
granted. Size it against `jlSpriteCompiledSize()`.
|
||||
|
||||
|`noAudio`
|
||||
|`bool`
|
||||
|`true` leaves audio off: every audio call is silent, as on a machine without
|
||||
sound, and the program pays nothing for audio (on the IIgs no replayer read
|
||||
from disk, on DOS and the Amiga no interrupt hooked). `false` has `jlInit`
|
||||
start audio and `jlShutdown` stop it.
|
||||
|===
|
||||
|
||||
Set every field: a `jlConfigT` is read whole, so a field left unset is
|
||||
whatever the memory held.
|
||||
|
||||
=== jlInit
|
||||
|
||||
[source,c]
|
||||
|
|
@ -33,8 +47,11 @@ bool jlInit(const jlConfigT *config);
|
|||
----
|
||||
|
||||
Initialises the library: takes over the display, allocates the stage and the
|
||||
codegen arena, and brings up input. Must be called before any other JoeyLib
|
||||
function.
|
||||
codegen arena, brings up input, and last starts audio (unless `noAudio`). Must
|
||||
be called before any other JoeyLib function. Sound that does not start is not
|
||||
a failure: modules and sound effects are then silent, and the log says so
|
||||
(<<_debug_logging>>). The tone and noise voices still play on the ports whose
|
||||
sound chip needs no engine: the Amiga, the ST, DOS, and the X68000.
|
||||
|
||||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|
|
@ -43,7 +60,9 @@ function.
|
|||
|===
|
||||
|
||||
*Returns:* `true` on success. On failure returns `false` and `jlLastError()`
|
||||
describes the reason.
|
||||
describes the reason. Where the port's own start-up fails without saying
|
||||
why, as the IIgs, Amiga, ST, X68000, and DVX ports' can, the reason is just
|
||||
`jlpInit failed`.
|
||||
|
||||
=== jlShutdown
|
||||
|
||||
|
|
@ -52,8 +71,10 @@ describes the reason.
|
|||
void jlShutdown(void);
|
||||
----
|
||||
|
||||
Shuts the library down, restoring the display mode, releasing all library
|
||||
resources and un-hooking any interrupt vectors the port installed. Call it on
|
||||
Shuts the library down: stops audio first, then restores the display mode,
|
||||
releases the library's own resources, and un-hooks any interrupt vectors the
|
||||
port installed. Surfaces and sprites the program created are not released:
|
||||
destroy them with `jlSurfaceDestroy` and `jlSpriteDestroy` first. Call it on
|
||||
every exit path; skipping it can leave the machine unusable.
|
||||
|
||||
*Parameters:* none. *Returns:* nothing.
|
||||
|
|
@ -104,9 +125,10 @@ void *jlAlloc(uint32_t bytes);
|
|||
|
||||
Allocates a large or persistent buffer from the platform's native allocator
|
||||
rather than the small C heap. *Use this instead of `malloc` for anything
|
||||
substantial.* On the IIgs the C heap lives in bank 0 and is tiny, with a single
|
||||
`malloc` capped around 32 KB, so multi-kilobyte caches and frame buffers must
|
||||
come from the Memory Manager instead.
|
||||
substantial.* On the IIgs the whole C heap is one Memory Manager block of
|
||||
32 KB to 48 KB (and, if that block cannot be had, only what the program's link
|
||||
left in its bank, which can be a few hundred bytes), so multi-kilobyte caches
|
||||
and frame buffers must come from the Memory Manager instead.
|
||||
|
||||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|
|
@ -124,7 +146,9 @@ zero-filled.
|
|||
void jlFree(void *p);
|
||||
----
|
||||
|
||||
Releases a buffer from `jlAlloc`.
|
||||
Releases a buffer from `jlAlloc`. Never mix `jlFree` and `free`: on most
|
||||
ports they are the same thing, but on the IIgs a `jlAlloc` block is a Memory
|
||||
Manager handle, and either call given the other's block corrupts memory.
|
||||
|
||||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|
|
@ -164,8 +188,11 @@ void jlWaitVBL(void);
|
|||
----
|
||||
|
||||
Blocks until the next display vertical blank, pacing a game loop to the
|
||||
display's native refresh rate. Cheap on every port, because the underlying
|
||||
mechanism is always a hardware-level wait rather than a software timer.
|
||||
display's native refresh rate. On the machine ports it waits for the
|
||||
display's own vertical blank. The hosted ports, and DVX in a window, have no
|
||||
vertical blank to wait for: there it waits for the next tick of a virtual
|
||||
frame clock, 60 Hz on the hosted ports and 70 Hz on DVX, and nothing stops a
|
||||
present from tearing.
|
||||
|
||||
*Parameters:* none. *Returns:* nothing.
|
||||
|
||||
|
|
@ -178,13 +205,15 @@ uint16_t jlFrameCount(void);
|
|||
|
||||
*Parameters:* none.
|
||||
|
||||
*Returns:* a monotonic 16-bit frame counter.
|
||||
*Returns:* a monotonic 16-bit frame counter. An interrupt or a clock keeps it,
|
||||
so it counts every frame however seldom you read it -- except on an X68000
|
||||
whose system refuses JoeyLib's frame interrupt, where it counts only while
|
||||
`jlWaitVBL`, `jlFrameCount`, or `jlStagePresent` is being called.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
You must poll faster than twice `jlFrameHz()` or edges are missed, and the
|
||||
counter wraps roughly every 18 minutes at 60 Hz. For elapsed-time work prefer
|
||||
`jlMillisElapsed`.
|
||||
The counter wraps roughly every 18 minutes at 60 Hz, so compare two readings
|
||||
by subtracting them. For elapsed-time work prefer `jlMillisElapsed`.
|
||||
====
|
||||
|
||||
=== jlFrameHz
|
||||
|
|
@ -196,13 +225,16 @@ uint16_t jlFrameHz(void);
|
|||
|
||||
*Parameters:* none.
|
||||
|
||||
*Returns:* the nominal display frame rate in Hz -- 50 on Amiga PAL, 60 on IIgs
|
||||
and ST NTSC, 55 on the X68000, 70 on VGA mode 13h and on DVX, whose window has
|
||||
a virtual 70 Hz (and whose full screen is mode 13h's, or a VBE mode's, with the
|
||||
same frame count), and 60 under SDL3, whose window has a virtual 60 Hz. The X68000 times its display at `jlInit` and rounds to the
|
||||
nearest whole number: 55.46 Hz on the machine, 56.07 Hz under MAME, which
|
||||
therefore reports 56. The real cadence may drift slightly; this is the figure
|
||||
to divide by when converting frames to seconds.
|
||||
*Returns:* the nominal display frame rate in Hz -- 60 on the IIgs, or 50 on a
|
||||
PAL machine; 50 on the Amiga, whose screen is PAL, or 60 on an NTSC Amiga that
|
||||
cannot show PAL; 50 or 60 on the ST, as the machine is set; 55 on the X68000;
|
||||
70 on VGA mode 13h and on DVX, whose window has a virtual 70 Hz (and whose full
|
||||
screen is mode 13h's, or a VBE mode's, with the same frame count); and 60 on
|
||||
every hosted port, whose window has a virtual 60 Hz. The X68000 times its
|
||||
display at `jlInit` and rounds to the nearest whole number: 55.46 Hz on the
|
||||
machine, 56.07 Hz under MAME, which therefore reports 56. The real cadence may
|
||||
drift slightly; this is the figure to divide by when converting frames to
|
||||
seconds.
|
||||
|
||||
=== jlMillisElapsed
|
||||
|
||||
|
|
@ -211,10 +243,9 @@ to divide by when converting frames to seconds.
|
|||
uint32_t jlMillisElapsed(void);
|
||||
----
|
||||
|
||||
Monotonic millisecond counter since `jlInit`. DOS, ST, X68000, DVX and SDL3 read a
|
||||
real timer; IIgs and Amiga derive it from the frame counter, so the
|
||||
value steps at frame granularity there. Two calls separated by a long render report the true
|
||||
elapsed time, not the number of vertical blanks that fired.
|
||||
Monotonic millisecond counter since `jlInit`. It may advance a frame at a
|
||||
time. Two calls separated by a long render report the true elapsed time, not
|
||||
the number of vertical blanks that fired.
|
||||
|
||||
*Parameters:* none.
|
||||
|
||||
|
|
@ -222,8 +253,8 @@ elapsed time, not the number of vertical blanks that fired.
|
|||
|
||||
[NOTE]
|
||||
====
|
||||
On the frame-derived ports you must poll at least once every 65535 frames --
|
||||
about 18 minutes at 60 Hz -- or wrapped frames are lost.
|
||||
Call it at least once every 15 minutes, or the time between two calls can be
|
||||
lost.
|
||||
====
|
||||
|
||||
=== jlRandom
|
||||
|
|
|
|||
|
|
@ -2,20 +2,40 @@
|
|||
|
||||
Declared in `<joey/debug.h>`.
|
||||
|
||||
A crash-tracing logger that writes to a file beside the executable (on DVX,
|
||||
in the program's settings folder, `CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`).
|
||||
Debugging a program on an emulated 1988 machine is awkward, and this is
|
||||
usually the fastest way to find out what it did before it stopped doing it.
|
||||
The headless run scripts retrieve the log automatically.
|
||||
A crash-tracing logger that writes `joeylog.txt` in the current directory
|
||||
(on DVX, in the program's settings folder,
|
||||
`CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`). Debugging a program on an emulated
|
||||
1988 machine is awkward, and this is usually the fastest way to find out what
|
||||
it did before it stopped doing it. The headless run scripts retrieve the log
|
||||
automatically. The library itself writes to it only when something fails, such
|
||||
as sound that does not start.
|
||||
|
||||
How the file is written differs:
|
||||
|
||||
* *Every port but the IIgs* opens the file on the first line and appends to
|
||||
it, writing each line out as it is logged, so a crash or hang leaves every
|
||||
line before it on disk. The file grows across runs until `jlLogReset`.
|
||||
* *The IIgs* keeps the lines in an 8 KB ring in memory, because a write per
|
||||
line costs seconds of floppy time there. Nothing reaches the disk until
|
||||
`jlLogFlush`, which replaces the file with the last 8 KB logged. A program
|
||||
that never calls it leaves no log.
|
||||
* *The web* writes the file into the page's memory, where it is lost when the
|
||||
page closes.
|
||||
* *The hosted ports* make the program's folder the current directory in
|
||||
`jlInit`: a line logged before that goes to a log in the directory the
|
||||
program was started from, and the rest of the run's lines follow it there.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The logger needs a multi-kilobyte ring buffer, which the tightest targets
|
||||
cannot spare. Space Taxi compiles it out entirely on the IIgs and X68000 for
|
||||
that reason. If you are debugging a memory-tight program, expect to do the
|
||||
same and to observe hardware state through the emulator instead.
|
||||
The logger's buffer is linked into every program, since the library logs its
|
||||
own failures: the 8 KB ring on the IIgs, and a 16 KB file buffer on every
|
||||
other port but DVX. A memory-tight program can still leave its own log calls
|
||||
out and observe hardware state through the emulator instead.
|
||||
====
|
||||
|
||||
On DOS the library also creates `CRASH.LOG` in the current directory at
|
||||
`jlInit`, and writes the processor's registers there if the program faults.
|
||||
|
||||
=== jlLog
|
||||
|
||||
[source,c]
|
||||
|
|
@ -41,13 +61,14 @@ void jlLogF(const char *fmt, ...);
|
|||
----
|
||||
|
||||
Appends a formatted message, `printf` style, as one line: like `jlLog`, the
|
||||
library ends the line, so the format needs no trailing `\n`.
|
||||
library ends the line, so the format needs no trailing `\n`. On the IIgs a
|
||||
line is cut at 199 characters.
|
||||
|
||||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|Parameter |Type |Description
|
||||
|`fmt` |`const char *` |Format string.
|
||||
|`...` | |Arguments matching the format string.
|
||||
|`+...+` | |Arguments matching the format string.
|
||||
|===
|
||||
|
||||
*Returns:* nothing.
|
||||
|
|
@ -66,8 +87,10 @@ time.
|
|||
void jlLogFlush(void);
|
||||
----
|
||||
|
||||
Flushes buffered log output to disk. Call it before anything that might crash
|
||||
or exit, or the last and most interesting lines may never be written.
|
||||
Flushes buffered log output to disk. On the IIgs it is the only way the log
|
||||
reaches the disk: it rewrites the whole file with the last 8 KB logged, so call
|
||||
it before anything that might crash or exit, or the last and most interesting
|
||||
lines are never written. Every other port has already written each line.
|
||||
|
||||
*Parameters:* none. *Returns:* nothing.
|
||||
|
||||
|
|
@ -79,7 +102,8 @@ void jlLogReset(void);
|
|||
----
|
||||
|
||||
Clears the log, discarding previous contents. Call once at startup so each run
|
||||
produces a fresh log rather than appending to the last one.
|
||||
produces a fresh log rather than appending to the last one. On the IIgs it
|
||||
clears the ring only; the file on disk changes at the next `jlLogFlush`.
|
||||
|
||||
*Parameters:* none. *Returns:* nothing.
|
||||
|
||||
|
|
@ -89,6 +113,7 @@ int main(void) {
|
|||
jlConfigT config;
|
||||
|
||||
config.codegenBytes = 32UL * 1024;
|
||||
config.noAudio = true;
|
||||
if (!jlInit(&config)) {
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,27 +5,47 @@ Declared in `<joey/file.h>`.
|
|||
Two directories sit beside the executable, staged there by each platform's disk
|
||||
packager:
|
||||
|
||||
* `DATA/` -- read-only game content: levels, sprites, audio.
|
||||
* `DATA/` -- game content: levels, sprites, audio.
|
||||
* `SAVES/` -- writable saves and preferences.
|
||||
|
||||
DVX is the exception, because its programs share one current directory:
|
||||
there `DATA/` is beside the program, in its folder under DVX's `APPS`, and
|
||||
`SAVES/` is in the program's own settings folder, `CONFIG\APPS\JOEYLIB\NAME`,
|
||||
which DVX keeps apart from the program files so a reinstall never overwrites
|
||||
it. The calls below find both there by themselves.
|
||||
Where the calls look for them:
|
||||
|
||||
* *The hosted ports* make the program's own folder the current directory in
|
||||
`jlInit`, so both are found beside the program wherever it was started
|
||||
from.
|
||||
* *DOS, the ST, the Amiga, and the X68000* look in the current directory. A
|
||||
program started from its own folder (as a desktop starts it) finds them;
|
||||
one started from a command line elsewhere finds no `DATA/` and makes its
|
||||
`SAVES/` there.
|
||||
* *The IIgs* looks for `DATA/` under GS/OS prefix 0 and puts `SAVES/` in the
|
||||
application's folder (prefix 1). When the two differ, data and saves come
|
||||
from different folders.
|
||||
* *DVX* is the exception, because its programs share one current directory:
|
||||
there `DATA/` is beside the program, in its folder under DVX's `APPS`, and
|
||||
`SAVES/` is in the program's own settings folder,
|
||||
`CONFIG\APPS\JOEYLIB\NAME`, which DVX keeps apart from the program files so
|
||||
a reinstall never overwrites it. The calls below find both there by
|
||||
themselves.
|
||||
|
||||
Both are addressed by a *bare name relative to their directory*. The prefix is
|
||||
added for you, so never hard-code `"DATA/"` or `"SAVES/"` yourself.
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
Keep names portable across the retro filesystems: at most 8 characters plus a
|
||||
short extension, letters and digits only, so they satisfy both ProDOS and DOS
|
||||
8.3. Do not depend on case being preserved.
|
||||
Keep names portable across the retro filesystems: at most 8 characters plus
|
||||
an extension of up to 3, letters and digits only, starting with a letter, so
|
||||
they satisfy both ProDOS (whose names must start with a letter) and DOS 8.3.
|
||||
The save calls refuse any other name on every port, as if the file could not
|
||||
be opened; names in `DATA/`, which ship with the program, are passed through
|
||||
as they are. Do not depend on case being preserved, and use the same case
|
||||
every time: on Linux, Haiku, BeOS, and the web, `HERO.SAV` and `hero.sav` are
|
||||
different files.
|
||||
====
|
||||
|
||||
A failed open or write is non-fatal and simply reports failure, so save code
|
||||
can degrade gracefully on a platform or volume where `SAVES/` is unavailable.
|
||||
On the IIgs at most 21 files can be open at once, and saves are created as
|
||||
text files (type TXT).
|
||||
|
||||
=== jlDataOpen
|
||||
|
||||
|
|
@ -34,7 +54,10 @@ can degrade gracefully on a platform or volume where `SAVES/` is unavailable.
|
|||
FILE *jlDataOpen(const char *name, const char *mode);
|
||||
----
|
||||
|
||||
Opens a read-only data file. The `DATA/` prefix is prepended automatically.
|
||||
Opens a data file. The `DATA/` prefix is prepended automatically. The mode is
|
||||
passed to `fopen` as it is, so a writing mode works where the medium allows it,
|
||||
but `DATA/` is meant to be read: on the web, for instance, what is written
|
||||
there is lost when the page closes.
|
||||
|
||||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|
|
@ -74,7 +97,9 @@ whole-file blob prefer `jlSaveWrite` and `jlSaveRead`.
|
|||
bool jlSaveWrite(const char *name, const void *buf, uint32_t len);
|
||||
----
|
||||
|
||||
Writes a whole save file, replacing any existing contents.
|
||||
Writes a whole save file, replacing any existing contents. The write is not
|
||||
atomic: if it fails part way (a full disk, say) the save is left short. A
|
||||
`NULL` `buf` with a non-zero `len` returns `false` and leaves the save alone.
|
||||
|
||||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|
|
@ -145,8 +170,11 @@ uint32_t jlDiskFree(void);
|
|||
*Parameters:* none.
|
||||
|
||||
*Returns:* free space in bytes on the volume holding `SAVES/`. Returns 0 if it
|
||||
cannot be determined -- some ports have no query for it -- and saturates at
|
||||
`UINT32_MAX` on volumes larger than 4 GB.
|
||||
cannot be determined, and saturates at `UINT32_MAX` on volumes larger than
|
||||
4 GB. On DOS and Win16 the query (DOS's own) stops near 2 GB on a larger
|
||||
volume, and on the web it is the browser's own estimate of what the site may
|
||||
still store (its quota less what it uses), which the browser may round or
|
||||
pad.
|
||||
|
||||
[source,c]
|
||||
----
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
Declared in `<joey/input.h>`.
|
||||
|
||||
Call `jlInputPoll()` exactly once per frame to refresh keyboard, mouse and
|
||||
Call `jlInputPoll()` exactly once per frame to refresh keyboard, mouse, and
|
||||
joystick state. Everything else in this section reads the state that poll
|
||||
captured.
|
||||
|
||||
Two parallel keyboard interfaces share that one poll. The *predicates* answer
|
||||
"is the fire key held?", which is the right shape for games. The *character
|
||||
queue* answers "which character did the user type?", with shift, caps lock and
|
||||
queue* answers "which character did the user type?", with shift, caps lock, and
|
||||
the machine's keyboard layout already applied, which is the right shape for
|
||||
text entry. They coexist freely.
|
||||
|
||||
|
|
@ -20,32 +20,33 @@ transition happened.
|
|||
[source,c]
|
||||
----
|
||||
typedef enum {
|
||||
KEY_NONE = 0,
|
||||
KEY_A .. KEY_Z,
|
||||
KEY_0 .. KEY_9,
|
||||
KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
|
||||
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT,
|
||||
KEY_LSHIFT, KEY_RSHIFT, KEY_LCTRL, KEY_LALT,
|
||||
KEY_F1 .. KEY_F10,
|
||||
KEY_COUNT
|
||||
JL_KEY_NONE = 0,
|
||||
JL_KEY_A .. JL_KEY_Z,
|
||||
JL_KEY_0 .. JL_KEY_9,
|
||||
JL_KEY_SPACE, JL_KEY_ESCAPE, JL_KEY_RETURN, JL_KEY_TAB, JL_KEY_BACKSPACE,
|
||||
JL_KEY_UP, JL_KEY_DOWN, JL_KEY_LEFT, JL_KEY_RIGHT,
|
||||
JL_KEY_LSHIFT, JL_KEY_RSHIFT, JL_KEY_LCTRL, JL_KEY_LALT,
|
||||
JL_KEY_F1 .. JL_KEY_F10,
|
||||
JL_KEY_COUNT
|
||||
} jlKeyE;
|
||||
|
||||
typedef enum {
|
||||
MOUSE_BUTTON_NONE = 0, MOUSE_BUTTON_LEFT,
|
||||
MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT
|
||||
JL_MOUSE_BUTTON_NONE = 0, JL_MOUSE_BUTTON_LEFT,
|
||||
JL_MOUSE_BUTTON_RIGHT, JL_MOUSE_BUTTON_MIDDLE, JL_MOUSE_BUTTON_COUNT
|
||||
} jlMouseButtonE;
|
||||
|
||||
typedef enum { JOYSTICK_0 = 0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE;
|
||||
typedef enum { JOY_BUTTON_0 = 0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE;
|
||||
typedef enum { JL_JOYSTICK_0 = 0, JL_JOYSTICK_1, JL_JOYSTICK_COUNT } jlJoystickE;
|
||||
typedef enum { JL_JOY_BUTTON_0 = 0, JL_JOY_BUTTON_1, JL_JOY_BUTTON_COUNT } jlJoyButtonE;
|
||||
----
|
||||
|
||||
Constants: `JOYSTICK_AXIS_MAX` (127) and `JOYSTICK_AXIS_MIN` (-127);
|
||||
Constants: `JL_JOYSTICK_AXIS_MAX` (127) and `JL_JOYSTICK_AXIS_MIN` (-127);
|
||||
`JL_CHAR_QUEUE_SIZE` (32).
|
||||
|
||||
Control codes delivered through the character queue: `JL_CHAR_BACKSPACE`
|
||||
(0x08), `JL_CHAR_TAB` (0x09), `JL_CHAR_RETURN` (0x0D), `JL_CHAR_ESCAPE` (0x1B)
|
||||
(0x08), `JL_CHAR_TAB` (0x09), `JL_CHAR_RETURN` (0x0D), `JL_CHAR_ESCAPE` (0x1B),
|
||||
and `JL_CHAR_DELETE` (0x7F). That last one is *forward* delete, the key marked
|
||||
Del -- a text editor needs both it and backspace.
|
||||
Del -- a text editor needs both it and backspace. The IIgs never delivers it:
|
||||
its Delete key is the backspace key, and types `JL_CHAR_BACKSPACE`.
|
||||
|
||||
=== jlInputPoll
|
||||
|
||||
|
|
@ -55,7 +56,9 @@ void jlInputPoll(void);
|
|||
----
|
||||
|
||||
Refreshes all input state and refills the typed-character queue. Call once per
|
||||
frame, typically just before drawing.
|
||||
frame, typically just before drawing. A key pressed and let go between two
|
||||
polls still reads down for one poll, so `jlKeyPressed` sees a quick tap
|
||||
(except on DVX, which reads its keyboard only as it is at the poll).
|
||||
|
||||
*Parameters:* none. *Returns:* nothing.
|
||||
|
||||
|
|
@ -72,11 +75,13 @@ not polling means nothing ever changes.
|
|||
int jlInputGetChar(void);
|
||||
----
|
||||
|
||||
Pops the next typed character. Shift, caps lock and the keyboard layout have
|
||||
Pops the next typed character. Shift, caps lock, and the keyboard layout have
|
||||
already been applied by the backend, so punctuation and shifted symbols arrive
|
||||
correctly. The layout is the machine's own on IIgs, Amiga, ST and X68000,
|
||||
DVX's own on DVX and the operating system's on SDL3; the DOS port translates
|
||||
with a fixed US layout.
|
||||
correctly. The layout is the machine's own on the IIgs, Amiga, ST, and
|
||||
X68000. It is DVX's own on DVX, and the operating system's on SDL3, the web,
|
||||
Win32, Win16, Haiku, and BeOS. DOS, and the PSP's on-screen keyboard, use a
|
||||
fixed US layout. On the ST a key held with Alt types nothing, so the
|
||||
characters a national TOS puts on Alt cannot be typed.
|
||||
|
||||
Arrow keys are not characters and stay on the predicate API.
|
||||
|
||||
|
|
@ -88,9 +93,16 @@ Arrow keys are not characters and stay on the predicate API.
|
|||
[NOTE]
|
||||
====
|
||||
The queue holds `JL_CHAR_QUEUE_SIZE - 1` characters, so 31. When full, further
|
||||
characters are dropped until you pop some. Auto-repeat may deliver repeats on
|
||||
ports whose keyboard services repeat; repeat policy is yours to manage. 7-bit
|
||||
ASCII only -- no Unicode, no input methods.
|
||||
characters are dropped until you pop some. A chord types nothing on any port:
|
||||
Ctrl+M is not Return and Ctrl+1 is not 1, and Alt chords type nothing where
|
||||
Alt is a command key (DOS, DVX, Windows, the Be ports' Command key, the PSP's
|
||||
on-screen keyboard); the Amiga's Alt and Windows' AltGr type their layout's
|
||||
characters, and so does the ST's Alt where TOS keeps its keyboard's Alt
|
||||
layer in its tables (TOS 4.00 and later, and EmuTOS: a German keyboard's
|
||||
@ [ ] { } \), and otherwise nothing (<<portsDifferInput>>). A held key
|
||||
repeats its character on every port (the ST at the rate TOS is set to);
|
||||
repeat policy is yours to manage. 7-bit ASCII only -- no Unicode, no input
|
||||
methods.
|
||||
|
||||
The return type is plain `int`, not a stdint type, deliberately: this is the
|
||||
`getchar()`-style "byte or -1" contract.
|
||||
|
|
@ -103,9 +115,12 @@ The return type is plain `int`, not a stdint type, deliberately: this is the
|
|||
void jlWaitForAnyKey(void);
|
||||
----
|
||||
|
||||
Blocks until the user presses any key, polling internally so per-port input
|
||||
machinery -- including interrupt-driven audio samplers -- keeps running during
|
||||
the wait.
|
||||
Blocks until the user presses a key -- one JoeyLib names (with a `jlKeyE`),
|
||||
or one that types a character, punctuation for instance -- polling internally
|
||||
so per-port input machinery, including interrupt-driven audio samplers, keeps
|
||||
running during the wait. The mouse and joystick buttons do not end it, and a
|
||||
key already held when the wait starts must be let go and pressed again (its
|
||||
auto-repeat does not count).
|
||||
|
||||
*Parameters:* none. *Returns:* nothing. The character queue is emptied on
|
||||
return, so the dismissing keystroke is consumed rather than delivered as text.
|
||||
|
|
@ -129,6 +144,16 @@ bool jlKeyReleased(jlKeyE key);
|
|||
`true` only in the frame the key went down; `jlKeyReleased` only in the frame
|
||||
it came up.
|
||||
|
||||
A named key that two physical keys give is down while either is held, and
|
||||
comes up with the last of them: `JL_KEY_RETURN` is Return and the keypad's
|
||||
Enter; `JL_KEY_UP`, `JL_KEY_DOWN`, `JL_KEY_LEFT`, and `JL_KEY_RIGHT` are the
|
||||
arrow keys and the keypad's 8, 2, 4, and 6; and `JL_KEY_LCTRL` and
|
||||
`JL_KEY_LALT` are the left and right Ctrl and Alt, where the keyboard has
|
||||
both (on the X68000, OPT.1 and OPT.2 are Alt). The keypad's keys still type
|
||||
what the machine's keypad types (`jlInputGetChar`). The IIgs knows keys by
|
||||
their characters, so its keypad reads as the digits
|
||||
(<<portsDifferInput>>).
|
||||
|
||||
=== jlMousePresent
|
||||
|
||||
[source,c]
|
||||
|
|
@ -139,13 +164,10 @@ bool jlMousePresent(void);
|
|||
*Parameters:* none.
|
||||
|
||||
*Returns:* `true` if the platform confirmed a mouse at init. On DOS that means
|
||||
the INT 33h driver reset successfully; on Amiga, ST, IIgs, X68000, DVX and
|
||||
SDL3 the OS mouse is always wired. On DVX the position is the pointer's over
|
||||
the program's window, divided by the window's scale; it keeps its last value
|
||||
while the pointer is elsewhere. On DVX's full screen it is the pointer's
|
||||
place on the stage (DVX draws no pointer there). On SDL3 it is the pointer's
|
||||
place on the stage however the window scales it, held to the stage's edge
|
||||
while the pointer is beside it; the OS pointer is hidden over the window.
|
||||
the INT 33h driver reset successfully; on the Amiga, that JoeyLib's input
|
||||
window opened; on Win32 and Win16, that Windows reports a mouse; on SDL3 and
|
||||
the web, that SDL reports one. The IIgs, ST, X68000, DVX, Haiku, and BeOS
|
||||
always report one, and the PSP never does.
|
||||
|
||||
=== jlMouseX / jlMouseY
|
||||
|
||||
|
|
@ -161,6 +183,14 @@ int16_t jlMouseY(void);
|
|||
Until the platform reports the pointer, it is the middle of the stage,
|
||||
(160, 100), on every port.
|
||||
|
||||
On DVX the position is the pointer's over the program's window, divided by
|
||||
the window's scale; it keeps its last value while the pointer is elsewhere. On
|
||||
DVX's full screen it is the pointer's place on the stage (DVX draws no pointer
|
||||
there). On the hosted ports it is the pointer's place on the stage however the
|
||||
window scales it, held to the stage's edge while the pointer is beside it; the
|
||||
OS pointer is hidden over the window. On DOS the mouse driver keeps it in
|
||||
range.
|
||||
|
||||
=== jlMouseDown / jlMousePressed / jlMouseReleased
|
||||
|
||||
[source,c]
|
||||
|
|
@ -178,6 +208,10 @@ bool jlMouseReleased(jlMouseButtonE button);
|
|||
|
||||
*Returns:* held state, rising edge, and falling edge respectively.
|
||||
|
||||
The IIgs mouse has one button, `JL_MOUSE_BUTTON_LEFT`. The ST and X68000 have
|
||||
left and right. The other ports report left, right, and middle, as far as the
|
||||
mouse has them. The PSP has no mouse.
|
||||
|
||||
=== jlJoystickConnected
|
||||
|
||||
[source,c]
|
||||
|
|
@ -188,13 +222,20 @@ bool jlJoystickConnected(jlJoystickE js);
|
|||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|Parameter |Type |Description
|
||||
|`js` |`jlJoystickE` |Which stick, `JOYSTICK_0` or `JOYSTICK_1`.
|
||||
|`js` |`jlJoystickE` |Which stick, `JL_JOYSTICK_0` or `JL_JOYSTICK_1`.
|
||||
|===
|
||||
|
||||
*Returns:* `true` if the platform reports a stick on that port. On the ST and
|
||||
X68000 both ports always report one: an empty port cannot be told from a stick
|
||||
at rest. The Amiga (`lowlevel.library`) has the same blind spot the other
|
||||
way: it reports a stick only once it has moved, and never a mouse.
|
||||
way: it reports a stick only once it has moved, and never a mouse. The IIgs
|
||||
has one stick, `JL_JOYSTICK_0`, and stops looking for it after about a second
|
||||
with none seen, until `jlJoystickReset`. On DOS a stick is connected while the
|
||||
game port times both its axes. SDL3, the web, and the PSP see SDL's gamepads
|
||||
and joysticks both, a joystick SDL has no gamepad mapping for read by its first
|
||||
two axes, its first hat, and its first two buttons; Win32, Win16, Haiku, and
|
||||
BeOS see the system's joysticks, and Haiku and BeOS notice a new one within a
|
||||
second.
|
||||
|
||||
=== jlJoystickX / jlJoystickY
|
||||
|
||||
|
|
@ -210,8 +251,11 @@ int8_t jlJoystickY(jlJoystickE js);
|
|||
|`js` |`jlJoystickE` |Which stick.
|
||||
|===
|
||||
|
||||
*Returns:* axis value, -127 to +127. Digital sticks snap to the extremes;
|
||||
analog sticks return the raw centred value. Test against a threshold rather
|
||||
*Returns:* axis value, -127 to +127. Digital sticks (the Amiga, ST, and
|
||||
X68000) snap to the extremes; analog sticks (the IIgs, DOS, DVX, and the
|
||||
hosted ports) return their offset from the centre, scaled to that range, with
|
||||
the dead zone applied. On the IIgs and DOS the axes read 0 until the stick's
|
||||
centre has been read (<<_jljoystickreset>>). Test against a threshold rather
|
||||
than for exact values, so the same code works with both.
|
||||
|
||||
=== jlJoyDown / jlJoyPressed / jlJoyReleased
|
||||
|
|
@ -232,6 +276,15 @@ bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button);
|
|||
|
||||
*Returns:* held state, rising edge, and falling edge respectively.
|
||||
|
||||
Every stick has two buttons. On the ST and STe's own joystick ports, and on
|
||||
the Amiga until a stick's second button is first pressed, a stick is taken to
|
||||
have one: a quick press of it is `JL_JOY_BUTTON_0`, down for the one poll after
|
||||
it is let go, and a press held for half a second is `JL_JOY_BUTTON_1`, down
|
||||
from then until it is let go. So act on button 0 with `jlJoyPressed`, and do
|
||||
not count on it staying down while the button is held. Elsewhere the two
|
||||
buttons are the stick's own, and a one-button stick never reports
|
||||
`JL_JOY_BUTTON_1`.
|
||||
|
||||
=== jlJoystickReset
|
||||
|
||||
[source,c]
|
||||
|
|
@ -239,34 +292,35 @@ bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button);
|
|||
void jlJoystickReset(jlJoystickE js, uint8_t deadZone);
|
||||
----
|
||||
|
||||
Re-enables joystick polling and recalibrates the resting position.
|
||||
Sets the stick's dead zone and, where the port can, re-centres it and looks
|
||||
for it again:
|
||||
|
||||
This matters on the IIgs. That port *auto-disables* joystick polling after a
|
||||
short window with no stick detected, because polling costs about 3 ms a frame
|
||||
there -- and it does not re-probe on its own. If a user plugs a stick in while
|
||||
your program runs, you must call this to resume polling.
|
||||
* *IIgs*: polling starts again if it had stopped for want of a stick, and the
|
||||
stick is re-centred at its next reading.
|
||||
* *DOS*: the game port is probed again, and the stick is re-centred at its
|
||||
next reading.
|
||||
* *Amiga*: the port goes back to `lowlevel.library`'s own detection.
|
||||
* *Elsewhere* only the dead zone changes.
|
||||
|
||||
On the IIgs polling for a stick that is not there stops. If a user may plug a
|
||||
stick in while your program runs, call this, with the stick centred, to pick
|
||||
it up.
|
||||
|
||||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|Parameter |Type |Description
|
||||
|`js` |`jlJoystickE` |Which stick.
|
||||
|`deadZone` |`uint8_t` |Raw readings within this many units of centre clamp
|
||||
to 0. Pass 0 to disable the dead zone.
|
||||
|`deadZone` |`uint8_t` |Axis values within this many units of 0 (on the
|
||||
-127..127 scale) read 0, on the ports with analog sticks: the IIgs, DOS, DVX,
|
||||
and the hosted ports. Each stick starts with 8, so a stick left alone reads 0
|
||||
though its pots jitter. Pass 0 to disable the dead zone.
|
||||
|===
|
||||
|
||||
*Returns:* nothing.
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
The next poll after this call captures the stick's *current* raw position as
|
||||
the new centre, so the user must be holding the stick centred when you call it.
|
||||
|
||||
On Amiga, ST and DOS the sticks are genuinely digital and already report -1, 0
|
||||
or +1, so recalibration is a no-op and `deadZone` is ignored. DVX's sticks are
|
||||
analog but calibrated, dead zone included, in DVX's own Control Panel, so the
|
||||
call is a no-op there too. SDL3's sticks are gamepads the OS calibrates; there
|
||||
the call only sets the dead zone, inside which the stick reads as centred.
|
||||
The call still
|
||||
clears any auto-disconnect state, so it is safe and correct to make it
|
||||
unconditionally.
|
||||
On the IIgs and DOS the next reading after this call is taken as the stick's
|
||||
centre, so the user must be holding the stick centred when you call it. The
|
||||
call is safe to make unconditionally.
|
||||
====
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|===
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
Declared in `<joey/surface.h>` and `<joey/present.h>`.
|
||||
|
||||
A surface is a 320x200 16-colour image with its own 200-entry SCB table and its
|
||||
own 16x16 palette set. Storage is target-native -- chunky 4bpp on IIgs and DOS,
|
||||
planar on Amiga, ST and X68000 -- but every entry point speaks in colour
|
||||
indices, so your code never sees the difference.
|
||||
own 16x16 palette set. Storage is target-native -- chunky 4bpp on the IIgs,
|
||||
DOS, DVX, and the hosted ports, planar on the Amiga, ST, and X68000 -- but every
|
||||
entry point speaks in colour indices, so your code never sees the difference.
|
||||
A new surface starts with the 16-colour EGA palette in all 16 of its palettes.
|
||||
|
||||
=== Constants
|
||||
|
||||
|
|
@ -13,13 +14,13 @@ indices, so your code never sees the difference.
|
|||
|===
|
||||
|Name |Value |Meaning
|
||||
|
||||
|`SURFACE_WIDTH` |320 |Surface width in pixels.
|
||||
|`SURFACE_HEIGHT` |200 |Surface height in pixels.
|
||||
|`SURFACE_BYTES_PER_ROW` |160 |Bytes per row in chunky 4bpp form.
|
||||
|`SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes.
|
||||
|`SURFACE_PALETTE_COUNT` |16 |Palettes per surface.
|
||||
|`SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette.
|
||||
|`SURFACE_PALETTE_ENTRIES` |256 |Total palette entries per surface.
|
||||
|`JL_SURFACE_WIDTH` |320 |Surface width in pixels.
|
||||
|`JL_SURFACE_HEIGHT` |200 |Surface height in pixels.
|
||||
|`JL_SURFACE_BYTES_PER_ROW` |160 |Bytes per row in chunky 4bpp form.
|
||||
|`JL_SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes.
|
||||
|`JL_SURFACE_PALETTE_COUNT` |16 |Palettes per surface.
|
||||
|`JL_SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette.
|
||||
|`JL_SURFACE_PALETTE_ENTRIES` |256 |Total palette entries per surface.
|
||||
|===
|
||||
|
||||
=== jlSurfaceT
|
||||
|
|
@ -29,7 +30,7 @@ indices, so your code never sees the difference.
|
|||
typedef struct jlSurfaceT jlSurfaceT;
|
||||
----
|
||||
|
||||
Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet` or a loader.
|
||||
Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet`, or a loader.
|
||||
|
||||
=== jlSurfaceCreate
|
||||
|
||||
|
|
@ -38,7 +39,9 @@ Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet` or a loader.
|
|||
jlSurfaceT *jlSurfaceCreate(void);
|
||||
----
|
||||
|
||||
Allocates a new offscreen surface.
|
||||
Allocates a new offscreen surface. Each one costs about 32 KB. On the Amiga
|
||||
that comes from chip RAM, because the blitter works on the planes there, so on
|
||||
a 512 KB machine, beside the display's own pages, only a few fit.
|
||||
|
||||
*Parameters:* none.
|
||||
|
||||
|
|
@ -83,7 +86,7 @@ the displayed framebuffer until the next present.
|
|||
void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src);
|
||||
----
|
||||
|
||||
Copies pixels, SCBs and palettes from `src` into `dst`.
|
||||
Copies pixels, SCBs, and palettes from `src` into `dst`.
|
||||
|
||||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|
|
@ -128,10 +131,10 @@ multiples of 16. Two consequences you own:
|
|||
|
||||
* up to 15 extra pixels each side are copied, so a snapped erase can clobber
|
||||
on-screen content outside the rectangle you asked for;
|
||||
* it silently invalidates any live `jlSpriteBackupT` whose recorded window it
|
||||
* it silently invalidates any live `jlSpriteBackupT` whose recorded region it
|
||||
touches.
|
||||
|
||||
Erase *all* windows, then draw *all* sprites. Never mix this with per-sprite
|
||||
Erase *all* regions, then draw *all* sprites. Never mix this with per-sprite
|
||||
save and restore in the same region.
|
||||
====
|
||||
|
||||
|
|
@ -180,7 +183,10 @@ nothing is reallocated.
|
|||
|===
|
||||
|
||||
*Returns:* `true` on success; `false` if the file is missing, the wrong size,
|
||||
or unreadable.
|
||||
has an SCB byte above 15, or unreadable. A read that fails part way can leave
|
||||
the pixels partly overwritten; the SCBs and palettes are then untouched. Colour
|
||||
0 of every palette loaded is set to black, as `jlPaletteSet` does. Neither file
|
||||
call sets `jlLastError`.
|
||||
|
||||
=== jlSurfaceHash
|
||||
|
||||
|
|
@ -213,8 +219,10 @@ void jlStagePresent(void);
|
|||
----
|
||||
|
||||
Flips the dirty regions of the stage to the display and clears the dirty state.
|
||||
On chunky platforms this is a direct copy; on planar platforms it is a
|
||||
chunky-to-planar conversion.
|
||||
Where the stage already has the display's layout this is a copy (the IIgs's
|
||||
4bpp pixels; the planes of the Amiga, ST, and X68000); DOS, DVX, and the hosted
|
||||
ports expand each 4-bit pixel to their display's 8-bit or RGB pixels. Palette
|
||||
and SCB changes reach the display here too.
|
||||
|
||||
Drawing primitives mark per-row dirty ranges as a side effect, so you only need
|
||||
to call this at the end of a frame and everything you drew appears. Rows
|
||||
|
|
@ -224,6 +232,6 @@ nothing touched since the last present are skipped.
|
|||
|
||||
[NOTE]
|
||||
====
|
||||
If you modify pixels by a route other than the library's primitives, the dirty
|
||||
tracking does not know, and those rows will not be presented.
|
||||
Every call that changes the stage's pixels marks the rows it touched, and the
|
||||
API has no other way to write them, so there is nothing to mark by hand.
|
||||
====
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
====
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
152
docs/Assets.adoc
152
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.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
== Building Your Program
|
||||
|
||||
JoeyLib does not install as a system library: each target needs its own cross
|
||||
compiler, link step, packaging and disk image, and JoeyLib's makefiles own all
|
||||
compiler, link step, packaging, and disk image; JoeyLib's makefiles own all
|
||||
of that. Your program lives in a directory of its own -- its own repository,
|
||||
usually -- next to a JoeyLib checkout. Its Makefile describes the program and
|
||||
includes two of JoeyLib's makefiles, and that is enough to build it for every
|
||||
target with exactly the flags, link order and packaging JoeyLib's own examples
|
||||
target with exactly the flags, link order, and packaging JoeyLib's own examples
|
||||
get. JoeyLib's examples are described the same way and built by the same
|
||||
rules; there is one set of them.
|
||||
|
||||
|
|
@ -62,19 +62,20 @@ The project's outputs land in its own `build/`, laid out like JoeyLib's:
|
|||
|Target |Output
|
||||
|
||||
|Apple IIgs |`build/iigs/bin/MYGAME` (an S16 OMF), its data staged in
|
||||
`build/iigs/data/MYGAME/`, and with `MYGAME_IIGS_GAME_DISK` the disks
|
||||
`build/iigs/bin/MYGAME.2mg` and `MYGAME-data.2mg`
|
||||
|Amiga |`build/amiga/bin/Mygame`
|
||||
`build/iigs/data/MYGAME/`, its Finder icon in `build/iigs/icon/MYGAME/`, and
|
||||
with `MYGAME_IIGS_GAME_DISK` the disks `build/iigs/bin/MYGAME.2mg` and
|
||||
`MYGAME-data.2mg`
|
||||
|Amiga |`build/amiga/bin/Mygame` and its Workbench icon, `Mygame.info`
|
||||
|Atari ST |`build/atarist/bin/MYGAME.PRG`, which runs on every ST
|
||||
|Atari STe |`build/ste/bin/MYGAME.PRG`, the STe build: STe and Mega STe only
|
||||
(<<_the_atari_st_family>>); same toolchain and baked data as the ST
|
||||
|MS-DOS |`build/dos/bin/MYGAME.EXE`, with the DPMI server built in
|
||||
|Sharp X68000 |`build/x68000/bin/MYGAME.X`
|
||||
|DVX |`build/dvx/bin/apps/joeylib/mygame/`: `mygame.app`, `mygame.hlp` and its
|
||||
|DVX |`build/dvx/bin/apps/joeylib/mygame/`: `mygame.app`, `mygame.hlp`, and its
|
||||
`DATA/`
|
||||
|SDL3 |`build/sdl3/bin/MYGAME`, a native program with SDL 3 linked in
|
||||
|Web |`build/web/bin/MYGAME/`: `MYGAME.html`, `.js`, `.wasm`, and `.data`
|
||||
(its `DATA/`, packed in)
|
||||
(its `DATA/`, packed in), and `icon.png`
|
||||
|Win32 |`build/win32/bin/MYGAME.EXE`, a 32-bit Windows program
|
||||
|Win16 |`build/win16/bin/MYGAME.EXE`, a Windows 3.x program
|
||||
|PSP |`build/psp/bin/MYGAME/`: `EBOOT.PBP` and its `DATA/`, a folder for the
|
||||
|
|
@ -84,7 +85,8 @@ memory stick's `PSP/GAME/`; the ELF it is packed from in
|
|||
|
||||
A program with `MYGAME_FOLDER` set ships in a folder of its own with its
|
||||
`DATA/` beside it (`build/dos/bin/MYGAME/MYGAME.EXE`); otherwise programs share
|
||||
`bin/DATA/`.
|
||||
`bin/DATA/`. A program's icon (<<_icons>>) is baked in
|
||||
`build/<target>/icon/MYGAME/` on its way into the program.
|
||||
|
||||
JoeyLib's library for each target, and the host tools such as `spritebake`, are
|
||||
built on demand where JoeyLib keeps them, in the checkout's `build/`, and shared
|
||||
|
|
@ -132,13 +134,16 @@ probes for gates).
|
|||
|`ID_PORTS` |The targets it builds for. Every target when unset.
|
||||
|`ID_NAME` |Its file name without the extension (see <<_naming>>).
|
||||
|`ID_FOLDER` |Set: it ships in a folder of its own with its `DATA/` (Amiga,
|
||||
ST, DOS, X68000, SDL3, Win32, Win16; on the IIgs, DVX, the web and the PSP
|
||||
ST, DOS, X68000, SDL3, Win32, Win16; on the IIgs, DVX, the web, and the PSP
|
||||
every program is packaged apart anyway).
|
||||
|`ID_BINDIR` |Build it there instead of `bin/`.
|
||||
|`ID_DATA` |Its data files (<<_data_files>>).
|
||||
|`ID_TITLE` |DVX: its window title. PSP: its title in the PSP's menu (by
|
||||
default its name).
|
||||
|`ID_HELP` |DVX: the line its help file says about it. No apostrophes.
|
||||
|`ID_ICON` |Its icon: a picture (a PNG, best square, transparent around the
|
||||
figure), baked for every target whose system shows programs by an icon
|
||||
(<<_icons>>). JoeyLib's kangaroo by default.
|
||||
|`ID_HELP` |DVX: the line its help file says about it.
|
||||
|`ID_IIGS_OVERFLOW_CAP` |IIgs: link large overflow segments (`0xF000`), which
|
||||
a big program needs to leave the Memory Manager room (see
|
||||
`make/support/iigs/clang-build.sh`).
|
||||
|
|
@ -174,11 +179,12 @@ set up:
|
|||
|`$(call programDataDir,ID)` |Where its `DATA/` files go on this target.
|
||||
|`$(call programOptionalDir,ID)` |Where the files go that it can run without
|
||||
(<<_disk_images_and_packaging>>).
|
||||
|`PLATFORM` |The target: `iigs`, `amiga`, `atarist`, `ste`, `dos`, `x68000`, `dvx`, `sdl3`, `web`, `win32`, `win16`, `psp`.
|
||||
|`PLATFORM` |The target: `iigs`, `amiga`, `atarist`, `ste`, `dos`, `x68000`, `dvx`, `sdl3`, `web`, `win32`, `win16`, `psp`, `haiku`, `haiku32`, `beos`.
|
||||
|`ASSET_PLATFORM` |The target its bakes are for. DVX shows the DOS stage and
|
||||
runs x86 code, so it takes the DOS bakes; the hosted ports' stage (SDL3, the
|
||||
web, Win32, Win16, the PSP) is the same chunky one, so they take them too
|
||||
(the cels of a pre-compiled sprite bank, without their code).
|
||||
web, Win32, Win16, the PSP, Haiku, BeOS) is the same chunky one, so they take
|
||||
them too: Win16 runs a pre-compiled sprite bank's x86 code as DVX does, and
|
||||
the others take its cels without their code. The STe build takes the ST's.
|
||||
|`SPRITEBAKE` |The host `spritebake` for this target (its rule is
|
||||
JoeyLib's).
|
||||
|`PROJECT_DIR`, `PROJECT_BUILD` |The project and its `build/`.
|
||||
|
|
@ -186,7 +192,7 @@ JoeyLib's).
|
|||
|`HOST_CC`, `HOST_CFLAGS` |For a host tool of the project's own.
|
||||
|===
|
||||
|
||||
A copy, a tile-bank bake and a pre-compiled sprite bank:
|
||||
A copy, a tile-bank bake, and a pre-compiled sprite bank:
|
||||
|
||||
[source,makefile]
|
||||
----
|
||||
|
|
@ -252,7 +258,8 @@ off a CFFA2 card. `make iigs-game-disk GAME=NAME` builds one for any program,
|
|||
`GAME_DISK_SIZE=2048KB` forces the size, and `make iigs-verify-game-disk`
|
||||
boots it headlessly and checks it draws. The data the program needs goes on with
|
||||
it; files listed under `programOptionalDir` (a program's optional data, which
|
||||
it can run without) go on last, only while the volume has room.
|
||||
it can run without) go on last, only while the volume has room, and the
|
||||
programs' Finder icons last of all (<<_icons>>).
|
||||
|
||||
*Sharp X68000.* Programs ship on a Human68k floppy; `joey-run` builds one for
|
||||
each run from the program and its `DATA/`. A floppy is 1232 KB, so a large
|
||||
|
|
@ -266,6 +273,7 @@ seems to vanish.
|
|||
needs to be on the disk.
|
||||
|
||||
*Amiga.* `joey-run` writes an `s/startup-sequence` that invokes your binary.
|
||||
`Name.info` beside it is its Workbench icon, from which it starts too.
|
||||
|
||||
*DVX.* Every program is a folder laid out as it installs in DVX:
|
||||
`build/dvx/bin/apps/joeylib/mygame/` holds `mygame.app`, its help file
|
||||
|
|
@ -287,16 +295,64 @@ DLLs, so its folder, with its `DATA/`, is all it needs; it ships the way a
|
|||
DOS program does.
|
||||
|
||||
*Win16.* A program is one `.EXE`, its 32-bit code bound to Open Watcom's
|
||||
Win386 supervisor and calling only what Windows 3.1 has (USER, GDI, KERNEL
|
||||
Win386 supervisor and calling only what Windows 3.1 has (USER, GDI, KERNEL,
|
||||
and MMSYSTEM), so its folder, with its `DATA/`, is all it needs; it ships the
|
||||
way a DOS program does.
|
||||
|
||||
*PSP.* A program is its folder, `build/psp/bin/MYGAME/`: `EBOOT.PBP` (the
|
||||
program, SDL 3 linked in, packed as the PSPSDK packs it, with no icon or
|
||||
pictures) and its `DATA/`. Copy the folder into `PSP/GAME/` on a memory
|
||||
stick; the PSP shows it by its `ID_TITLE`. The unstripped ELF stays in
|
||||
program, SDL 3 linked in, packed as the PSPSDK packs it, with its icon and no
|
||||
other pictures) and its `DATA/`. Copy the folder into `PSP/GAME/` on a memory
|
||||
stick; the PSP shows it by its icon and its `ID_TITLE`. The unstripped ELF stays in
|
||||
`build/psp/elf/`, for `psp-addr2line` on an address a crash reports.
|
||||
|
||||
=== Icons
|
||||
|
||||
A program has an icon on every system that shows programs by one. `ID_ICON`
|
||||
names a picture, a PNG, best square and transparent around the figure, and
|
||||
`make` bakes it for each target with `tools/iconBake.py`; a program without
|
||||
one gets JoeyLib's kangaroo, `assets/joeyIcon.png`. A picture of 64 pixels or
|
||||
less is taken for pixel art and enlarged by whole pixels; a bigger one is
|
||||
resampled smoothly. Where an icon has few colours, each pixel takes the
|
||||
nearest.
|
||||
|
||||
[cols="1,5", options="header"]
|
||||
|===
|
||||
|Target |The icon
|
||||
|
||||
|Apple IIgs |The Finder's: an icon file (type `$CA`) for each program, which
|
||||
the disk scripts put in the volume's `Icons` folder as `NAME.ICONS`, where the
|
||||
Finder reads icon files when it starts. 24x20 and 12x10 colour icons, which
|
||||
the Finder shows as they are: in its 640 mode each icon pixel is two screen
|
||||
pixels, black, red, green, or white beside black, blue, yellow, or white, seen
|
||||
together as one of 16 colours (an icon drawn at an odd screen column would
|
||||
pair them with the other halves of the palette; the Finder drew every one at
|
||||
an even column). A data disk takes the icons
|
||||
last, while there is room; a game disk keeps its program's at its root. A
|
||||
program copied to another volume takes its icon file along, into that
|
||||
volume's `Icons` folder.
|
||||
|Amiga |Workbench's: `Name.info` beside the program, a tool's icon in
|
||||
Workbench's four colours, 48x24 hires pixels, which starts it with
|
||||
`AMIGA_ICON_STACK` (32768) bytes of stack. Started from it, the program runs
|
||||
in its own folder, as it does from a shell.
|
||||
|DVX |Program Manager's: the program's `icon32` resource, 32x32 on DVX's grey.
|
||||
|Win16 |Program Manager's and the minimised window's: 32x32 in the 16 VGA
|
||||
colours, bound into the `.EXE`.
|
||||
|Win32 |Explorer's and the window's: 16, 32, and 48 pixels, each in 256
|
||||
colours and in 32-bit, linked into the `.EXE`.
|
||||
|Haiku, BeOS |Tracker's and the Deskbar's: 32x32 and 16x16 in the BeOS
|
||||
palette, in the program's resources. Tracker shows a file by the icons in its
|
||||
attributes, which a program built on another machine does not have; the
|
||||
program copies them across the first time it runs, so Tracker shows it by its
|
||||
icon from then on (or at once, after `mimeset -F NAME`).
|
||||
|PSP |The menu's: `ICON0` in the EBOOT, 144x80.
|
||||
|SDL3 |The window's, 64x64, which the desktop shows where it shows one (title
|
||||
bar, task bar, dock).
|
||||
|Web |The page's: `icon.png`, 32x32, beside it.
|
||||
|===
|
||||
|
||||
MS-DOS, the Atari ST, and the X68000 show no program by an icon of its own,
|
||||
so their programs get none.
|
||||
|
||||
=== Verifying It Actually Runs
|
||||
|
||||
Build success is weak evidence on retro targets. Run it with `joey-run` (see
|
||||
|
|
@ -353,7 +409,7 @@ Human68k floppies, environment checks) takes paths.
|
|||
|`make verify-audio`
|
||||
|Every port's audio gate: the IIgs DOC and X68000 chips are read directly,
|
||||
the ST's YM2149 register writes are traced (and for the STe build the
|
||||
samples the DMA sound chip plays), the Amiga, DOS and DVX output
|
||||
samples the DMA sound chip plays), the Amiga, DOS, and DVX output
|
||||
is recorded and graded, SDL3's mix is written to a file by SDL's disk
|
||||
driver and graded, and the web page's sound is recorded in headless
|
||||
Chromium and graded, as is the Win32 program's under Wine, the Win16
|
||||
|
|
@ -365,7 +421,7 @@ if a gate had to be skipped.
|
|||
|The IIgs actually reached the program and drew something.
|
||||
|
||||
|`make iigs-verify-game-disk`
|
||||
|A program's bootable IIgs volume boots, launches the program and draws.
|
||||
|A program's bootable IIgs volume boots, launches the program, and draws.
|
||||
|
||||
|`scripts/verify-<port>-tilebank.sh`
|
||||
|The port's own tile-bank bake loads pixel-exact both ways, with the same
|
||||
|
|
@ -373,6 +429,13 @@ if a gate had to be skipped.
|
|||
for the other machines are refused. `scripts/verify-all.sh tilebank` runs
|
||||
every port's.
|
||||
|
||||
|`scripts/verify-all.sh icon`
|
||||
|Every program carries its icon where its system looks
|
||||
(`scripts/check-icons.sh`, no emulator), and each system shows it: the IIgs
|
||||
Finder's window, the Amiga's icon.library (and a Workbench start), DVX's
|
||||
Program Manager, Windows 3.11's desktop with the program minimised, the
|
||||
Win32 and SDL3 windows' icons on X, and Tracker on Haiku and BeOS.
|
||||
|
||||
|`scripts/bench-<port>.sh`
|
||||
|Per-operation throughput for that port.
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ Toolchain availability:
|
|||
|
||||
==== Installer reference
|
||||
|
||||
`./install.sh [options] COMMAND [TARGETS...]`, run from the top of the
|
||||
`+./install.sh [options] COMMAND [TARGETS...]+`, run from the top of the
|
||||
repository.
|
||||
|
||||
[cols="1,3"]
|
||||
|
|
@ -91,9 +91,9 @@ libraries, for example). It reports anything out of date, any component
|
|||
whose files have gone, and any part of a port you installed only partly,
|
||||
such as FS-UAE without a Kickstart.
|
||||
|
||||
| `import PATH...`
|
||||
| `+import PATH...+`
|
||||
| Installs the files that are not downloaded (see <<Firmware that is not
|
||||
downloaded>>) from files, folders, `.zip` archives or Amiga `.adf` disk
|
||||
downloaded>>) from files, folders, `.zip` archives, or Amiga `.adf` disk
|
||||
images, matched by checksum.
|
||||
|
||||
| `bundle FILE`
|
||||
|
|
@ -198,7 +198,7 @@ trees.
|
|||
Nothing in `toolchains/` is part of the repository: delete the whole folder
|
||||
to return the machine to a clean state, and `./install.sh` re-creates it. The
|
||||
installer itself lives in `install.sh` and `installer/` (its component
|
||||
recipes, patches and package lists), and the build helpers the makefiles call
|
||||
recipes, patches, and package lists), and the build helpers the makefiles call
|
||||
live in `make/support/`.
|
||||
|
||||
=== Building
|
||||
|
|
@ -251,6 +251,7 @@ int main(void) {
|
|||
jlConfigT config;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.noAudio = true;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
|
|
@ -266,10 +267,13 @@ int main(void) {
|
|||
}
|
||||
----
|
||||
|
||||
Three things to notice, because they apply to every JoeyLib program:
|
||||
Four things to notice, because they apply to every JoeyLib program:
|
||||
|
||||
. *One header.* `#include <joey/joey.h>` pulls in everything except the
|
||||
optional serial add-on.
|
||||
. *Set every field of `jlConfigT`.* It is read whole, so a field left unset
|
||||
holds whatever was in memory. `noAudio` says whether the program wants sound:
|
||||
`jlInit` starts audio unless it is `true`.
|
||||
. *`jlInit` can fail*, and when it does `jlLastError()` tells you why. Always
|
||||
check it.
|
||||
. *`jlShutdown` is not optional.* It restores the display mode, releases the
|
||||
|
|
@ -289,10 +293,11 @@ graphics on screen and waits for a keypress:
|
|||
int main(void) {
|
||||
jlConfigT config;
|
||||
jlSurfaceT *stage;
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
|
||||
uint8_t i;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.noAudio = true;
|
||||
if (!jlInit(&config)) {
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -300,7 +305,7 @@ int main(void) {
|
|||
stage = jlStageGet(); // the library-owned back buffer
|
||||
|
||||
// Build palette 0. Entry 0 is forced black whatever we put here.
|
||||
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
colors[i] = 0x000;
|
||||
}
|
||||
colors[1] = 0xFFF; // white
|
||||
|
|
@ -309,12 +314,12 @@ int main(void) {
|
|||
jlPaletteSet(stage, 0, colors);
|
||||
|
||||
// Every scanline uses palette 0 (this is also the default).
|
||||
jlScbSetRange(stage, 0, SURFACE_HEIGHT - 1, 0);
|
||||
jlScbSetRange(stage, 0, JL_SURFACE_HEIGHT - 1, 0);
|
||||
|
||||
jlSurfaceClear(stage, 0);
|
||||
jlFillRect(stage, 40, 30, 240, 140, 3);
|
||||
jlDrawRect(stage, 40, 30, 240, 140, 1);
|
||||
jlDrawLine(stage, 0, 0, SURFACE_WIDTH - 1, SURFACE_HEIGHT - 1, 2);
|
||||
jlDrawLine(stage, 0, 0, JL_SURFACE_WIDTH - 1, JL_SURFACE_HEIGHT - 1, 2);
|
||||
|
||||
jlStagePresent(); // nothing is visible until this runs
|
||||
jlWaitForAnyKey();
|
||||
|
|
@ -375,7 +380,7 @@ the next. Nothing is written to your home directory.
|
|||
| The machine, when naming an example rather than a file.
|
||||
|
||||
| `--data DIR`
|
||||
| Copy `DIR`'s files into the program's `DATA` folder for this run, for
|
||||
| Copy the files in `DIR` into the program's `DATA` folder for this run, for
|
||||
example the game an interpreter plays.
|
||||
|
||||
| `--headless`
|
||||
|
|
@ -392,7 +397,7 @@ example the game an interpreter plays.
|
|||
| Apple IIgs: the ROM 01 machine instead of ROM 03.
|
||||
|
||||
| `--machine st`
|
||||
| Atari: Hatari's machine, `st`, `megast`, `ste` or `megaste`. By default an
|
||||
| Atari: Hatari's machine, `st`, `megast`, `ste`, or `megaste`. By default an
|
||||
ST-build program (`make atarist`) gets a plain ST and an STe-build one
|
||||
(`make ste`) an STe; the STe build runs only on `ste` and `megaste`.
|
||||
|
||||
|
|
@ -419,7 +424,7 @@ Kickstarts (`./install.sh install emulator:aga`).
|
|||
default is 9600.
|
||||
|
||||
| `-- ARGS`
|
||||
| Pass `ARGS` to the program. DOS, SDL3, Win32 and Win16 only: the other
|
||||
| Pass `ARGS` to the program. DOS, SDL3, Win32, and Win16 only: the other
|
||||
machines' autostart cannot pass arguments, and a page and a PSP program have
|
||||
none.
|
||||
|===
|
||||
|
|
@ -440,7 +445,7 @@ desktop.
|
|||
An SDL3 program is a native program, so `joey-run` simply starts it, in a
|
||||
window; running the file yourself does the same. `--headless` runs it on a
|
||||
private virtual display at one window pixel per stage pixel, so a
|
||||
`--snapshot` is exactly the 320x200 stage. The window's size, shape and full
|
||||
`--snapshot` is exactly the 320x200 stage. The window's size, shape, and full
|
||||
screen are set by environment variables (<<_sdl3_programs>>). The SDL3 port
|
||||
has no serial port.
|
||||
|
||||
|
|
@ -457,7 +462,7 @@ in when it is built, so `--data` does not apply.
|
|||
A Windows program runs under Wine: in a window on your display, with its
|
||||
Wine settings kept in the project's `build/win32/prefix` so they are set up
|
||||
once (nothing goes in `~/.wine`; a run with `--serial` uses a private one, so
|
||||
the kept COM1 is left alone). The window's size, shape and full screen are
|
||||
the kept COM1 is left alone). The window's size, shape, and full screen are
|
||||
set by the same environment variables as SDL3's, and Alt+Enter switches. With
|
||||
`--headless` it runs on a private virtual display at one pixel per stage
|
||||
pixel, so a `--snapshot` is the stage.
|
||||
|
|
@ -474,7 +479,7 @@ up to 115200 baud). Their components, `kpjoy` and `kpcomm`, build them from
|
|||
the kpjoy and mscomm projects, checkouts beside JoeyLib's (`../kpjoy`,
|
||||
`../mscomm/drv`; `KPJOY_DIR` and `KPCOMM_DIR` name others). The program is
|
||||
Windows' shell, so Windows ends when the program does, and its folder is
|
||||
drive D:, so what it saves stays there. The window's size, shape and full
|
||||
drive D:, so what it saves stays there. The window's size, shape, and full
|
||||
screen are set by the same environment variables as SDL3's: `joey-run` puts
|
||||
them in the DOS environment Windows starts from. Alt+Enter is DOSBox's own
|
||||
full-screen key, so the program switches with Alt and the keypad's Enter.
|
||||
|
|
@ -496,11 +501,11 @@ serial port.
|
|||
==== Haiku and BeOS
|
||||
|
||||
A Haiku (`haiku`, `haiku32`) or BeOS R5 (`beos`) program runs in that port's
|
||||
VM under QEMU (`./install.sh install emulator:haiku`, `emulator:haiku32` or
|
||||
VM under QEMU (`./install.sh install emulator:haiku`, `emulator:haiku32`, or
|
||||
`emulator:beos`), in QEMU's window, from a copy of its folder on a second
|
||||
disk. The machine boots from a disk it never writes to, runs the program,
|
||||
and powers off when the program ends; what the program wrote in its folder
|
||||
(saves, its log) is then copied back. The window's size, shape and full
|
||||
(saves, its log) is then copied back. The window's size, shape, and full
|
||||
screen are set by the same environment variables as SDL3's. Haiku's VM has
|
||||
sound; BeOS's has none, and is taken through R5's boot loader menu to its
|
||||
fail-safe video mode (1024x768, 16-bit colour), as R5 has no driver for any
|
||||
|
|
@ -585,7 +590,7 @@ arrives. To connect out, `socat` joins a pseudo-terminal to the network.
|
|||
| As on the ST: Wine's COM1 can only be a device, so `socat` joins a
|
||||
pseudo-terminal to the network both ways.
|
||||
|
||||
| SDL3, Web, PSP
|
||||
| SDL3, Web, PSP, Haiku, BeOS
|
||||
| None
|
||||
| These ports have no serial port; `jlSerialOpen` fails there.
|
||||
|===
|
||||
|
|
@ -604,14 +609,14 @@ rate the program sets.
|
|||
and X68000, where MAME prints `Unable to load image` and runs with the port
|
||||
unplugged.
|
||||
* While a port is listening, what the program sends waits for the connection,
|
||||
except on DOS, DVX and Win16, where DOSBox drops it just as a cable with
|
||||
except on DOS, DVX, and Win16, where DOSBox drops it just as a cable with
|
||||
nothing on the other end would. `examples/sertest` sends its banner at once,
|
||||
so there a client that connects late sees only the echo.
|
||||
|
||||
==== Firmware that is not downloaded
|
||||
|
||||
Almost everything the emulators need is downloaded: EmuTOS for the Atari ST,
|
||||
the Apple IIgs ROMs, GS/OS 6.0.4 and the CFFA2 card's firmware, and Sharp's own
|
||||
Almost everything the emulators need is downloaded: EmuTOS for the Atari ST;
|
||||
the Apple IIgs ROMs, GS/OS 6.0.4, and the CFFA2 card's firmware; and Sharp's own
|
||||
free release of the X68000 IPL-ROM and Human68k 3.02. A few files are not,
|
||||
because they were never released for free: the Amiga Kickstart 3.1 ROM, the
|
||||
Amiga `serial.device` and `lowlevel.library` from Workbench 3.1 (Kickstart
|
||||
|
|
@ -621,7 +626,10 @@ Kickstart 3.1 (optional: only for running the Amiga programs on those
|
|||
machines), the X68000 CG (font) ROM,
|
||||
for the Win16 port the Windows for Workgroups 3.11 install disc (as an
|
||||
`.iso`) and the S3 Trio64V+ 1.70.04 Windows 3.1 display driver, from which
|
||||
the installer sets Windows up unattended, and for the BeOS port the BeOS 5
|
||||
the installer sets Windows up unattended (and, optionally, Microsoft's WinG
|
||||
1.0 SDK, `WING10.EXE`, whose runtime the Win16 port draws through when it is
|
||||
installed: import it, then `./install.sh install wing`; archive.org keeps a
|
||||
copy), and for the BeOS port the BeOS 5
|
||||
Professional CD (its CloneCD `.img`, or the `.7z` holding it), from which
|
||||
the installer takes R5's headers and libraries and makes the VM. Supply your
|
||||
copies once:
|
||||
|
|
@ -631,7 +639,7 @@ copies once:
|
|||
$ ./install.sh import ~/my-roms
|
||||
----
|
||||
|
||||
`import` takes files, folders, `.zip` and `.7z` archives and Amiga `.adf` disk images
|
||||
`import` takes files, folders, `.zip` and `.7z` archives, and Amiga `.adf` disk images
|
||||
(point it at your Workbench 3.1 disk for `serial.device` and
|
||||
`lowlevel.library`, and at the S3
|
||||
driver's `.zip` for its three files), identifies each file by its checksum
|
||||
|
|
@ -655,9 +663,9 @@ built for every platform:
|
|||
|`draw` |Every drawing primitive, laid out in four quadrants.
|
||||
|`pattern` |Palette and per-scanline palette (SCB) work.
|
||||
|`keys` |Typed-character input and the key predicates.
|
||||
|`joy` |Joystick axes, buttons and recalibration.
|
||||
|`sprite` |Sprite loading, compiling, save-under and restore.
|
||||
|`audio` |Tracker music, `.MOD` playback, digital sound effects, a streamed sound and the noise generator.
|
||||
|`joy` |Joystick axes, buttons, and recalibration.
|
||||
|`sprite` |Sprite loading, compiling, save-under, and restore.
|
||||
|`audio` |Tracker music, `.MOD` playback, digital sound effects, a streamed sound, and the noise generator.
|
||||
|`save` |Save files and the disk-space query.
|
||||
|`tilebank` |Loading a baked tile bank both ways, and a bank for another machine refused.
|
||||
|`serial` |The optional RS-232 add-on.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
== Writing Portable Code
|
||||
|
||||
A JoeyLib program that follows the rules in this chapter compiles and behaves
|
||||
the same on all five machines, on DVX, under SDL3, in a browser and on Windows. Most of the rules exist because one
|
||||
the same on all five machines, on DVX, under SDL3, in a browser, and on Windows. Most of the rules exist because one
|
||||
particular target cannot do something the others can, and the Apple IIgs is
|
||||
usually that target.
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ usually that target.
|
|||
. Compile your sprites before you draw them.
|
||||
. Keep large objects out of function-local variables on the IIgs.
|
||||
. Use `jlRandom`, not `rand`, when identical behaviour across ports matters.
|
||||
. Keep file names short, uppercase-safe and 8.3-compatible.
|
||||
. Keep file names short, uppercase-safe, and 8.3-compatible.
|
||||
. Do not mix save-under with clean-buffer erasing in the same screen region.
|
||||
. Pick one of the audio layers per scene, not two.
|
||||
|
||||
|
|
@ -21,11 +21,12 @@ The rest of this chapter explains each one.
|
|||
|
||||
=== Memory
|
||||
|
||||
This is the rule that catches everyone. *The C heap on the IIgs is tiny.* It
|
||||
lives in bank 0, and a single `malloc` is capped at roughly 32 KB -- often far
|
||||
less in practice, because the clang libc heap on that port can be a few hundred
|
||||
bytes. A program that `malloc`s a 32000-byte buffer works on four targets and
|
||||
fails on the reference one.
|
||||
This is the rule that catches everyone. *The C heap on the IIgs is tiny.* The
|
||||
whole heap is one Memory Manager block of 32 KB to 48 KB, and if that block
|
||||
cannot be had at start-up it is only what the program's link left in its
|
||||
bank, which can be a few hundred bytes. A program that `malloc`s a
|
||||
32000-byte buffer works on every other target and can fail on the reference
|
||||
one.
|
||||
|
||||
Use the library allocator, which is backed by each platform's native allocator
|
||||
(the Memory Manager on the IIgs):
|
||||
|
|
@ -40,7 +41,10 @@ if (buffer == NULL) {
|
|||
jlFree(buffer);
|
||||
----
|
||||
|
||||
`jlAlloc` does *not* zero the memory it returns.
|
||||
`jlAlloc` does *not* zero the memory it returns. Never free a `jlAlloc` block
|
||||
with `free`, or a `malloc` block with `jlFree`: on most ports they are the
|
||||
same, but on the IIgs a `jlAlloc` block is a Memory Manager handle, and mixing
|
||||
them corrupts memory.
|
||||
|
||||
For bulk copies, `jlMemCopy` and `memcpy` are the same thing: `jlMemCopy` is
|
||||
`memcpy` with a 32-bit count. On the IIgs both are `MVN` block moves at 7
|
||||
|
|
@ -71,12 +75,11 @@ void renderScene(void) {
|
|||
=== Timing
|
||||
|
||||
Covered in <<_frame_rate_independence>>, and worth repeating because it is the
|
||||
second most common portability bug: the targets run at 50, 60 and 70 Hz. Scale
|
||||
by `jlFrameHz()` or work from `jlMillisElapsed()`.
|
||||
second most common portability bug: the targets run at 50, 55, 60, and 70 Hz.
|
||||
Scale by `jlFrameHz()` or work from `jlMillisElapsed()`.
|
||||
|
||||
`jlFrameCount()` is a 16-bit counter, so you must poll it faster than twice the
|
||||
frame rate or you will miss edges, and it wraps roughly every 18 minutes at
|
||||
60 Hz.
|
||||
`jlFrameCount()` is a 16-bit counter that wraps roughly every 18 minutes at
|
||||
60 Hz, so compare two readings by subtracting them.
|
||||
|
||||
=== Colour and the Palette
|
||||
|
||||
|
|
@ -94,28 +97,31 @@ have both opaque and masked forms.
|
|||
Two rules.
|
||||
|
||||
*Compile before drawing.* `jlSpriteDraw` never compiles on demand. An
|
||||
uncompiled sprite runs through the interpreter at roughly 1/40th the speed, and
|
||||
nothing warns you.
|
||||
uncompiled sprite runs through the interpreter, two to six times slower
|
||||
depending on the machine, and nothing warns you.
|
||||
|
||||
*Align your backup buffers.* Give any `jlSpriteBackupT` backing array 2-byte
|
||||
alignment, by declaring it `uint16_t` or with an alignment attribute. An odd
|
||||
buffer address routes save and restore onto slow interpreted paths on the
|
||||
planar ports, because the 68000 cannot perform word accesses at odd addresses.
|
||||
alignment, by declaring it `uint16_t` or with an alignment attribute. On the
|
||||
Amiga, ST, and X68000 an odd buffer address sends save and restore down a much
|
||||
slower path, and on the Amiga and ST it crashes the trusted variants.
|
||||
|
||||
[source,c]
|
||||
----
|
||||
// Right: 2-byte aligned by construction.
|
||||
static uint16_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4) / 2];
|
||||
static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)];
|
||||
|
||||
// Risky: may land on an odd address.
|
||||
static uint8_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4)];
|
||||
static uint8_t gBacking[jlSpriteBackupLength(uint8_t, 4, 4)];
|
||||
----
|
||||
|
||||
Note also that on the planar ports the region a save-under actually captures is
|
||||
the *16-pixel-aligned window* containing the sprite, not the sprite rectangle.
|
||||
`jlSpriteSaveUnder` reports the true geometry in the backup structure's
|
||||
`x`, `width`, `height` and `sizeBytes` fields. It is that recorded window you
|
||||
must not overwrite before restoring.
|
||||
Note also that the region a save-under actually captures is often not the
|
||||
sprite rectangle: it can be the *16-pixel-aligned region* containing the
|
||||
sprite (the Amiga, ST, and X68000), only the rows holding an opaque pixel (the
|
||||
IIgs), or only the visible part of a sprite partly off the surface (every
|
||||
port). `jlSpriteSaveUnder` reports the true geometry in the backup structure's
|
||||
`x`, `y`, `width`, `height`, and `sizeBytes` fields. It is that recorded region
|
||||
you must not overwrite before restoring. On the IIgs the buffer must not cross
|
||||
a 64 KB bank boundary, which a `jlAlloc` block and the C heap never do.
|
||||
|
||||
=== Randomness
|
||||
|
||||
|
|
@ -142,19 +148,24 @@ you:
|
|||
|
||||
Pass bare names: `jlDataOpen("levels/title.dat", "rb")`, never
|
||||
`"DATA/levels/title.dat"`. On DVX the two are not in the current directory
|
||||
at all (<<_dvx_programs>>), so a program that opens `"DATA/..."` itself finds
|
||||
nothing there.
|
||||
at all (<<_dvx_programs>>), so a program that opens `+"DATA/..."+` itself finds
|
||||
nothing there. DOS, the ST, the Amiga, and the X68000 look in the current
|
||||
directory, so start a program there from its own folder; the IIgs looks for
|
||||
`DATA/` under GS/OS prefix 0 but keeps `SAVES/` in the application's folder
|
||||
(<<_files_and_saves>>).
|
||||
|
||||
Keep names portable across the retro filesystems this library targets: *at most
|
||||
8 characters plus a short extension, letters and digits only*, so they satisfy
|
||||
both ProDOS and DOS 8.3. Do not rely on case being preserved.
|
||||
8 characters plus a short extension, letters and digits only, starting with a
|
||||
letter*, so they satisfy both ProDOS and DOS 8.3. Nothing checks them for you.
|
||||
Do not rely on case being preserved, and spell a name the same way every
|
||||
time: on Linux, Haiku, BeOS, and the web, case matters.
|
||||
|
||||
A failed save is not fatal -- the calls report failure and your program should
|
||||
degrade gracefully, because on some volumes `SAVES/` simply is not writable.
|
||||
|
||||
=== Audio
|
||||
|
||||
JoeyLib gives you three sound layers, and on two platforms they share physical
|
||||
JoeyLib gives you three sound layers, and on some platforms they share physical
|
||||
hardware:
|
||||
|
||||
[cols="2,5", options="header"]
|
||||
|
|
@ -162,41 +173,48 @@ hardware:
|
|||
|Layer |What it is
|
||||
|
||||
|Module playback
|
||||
|`jlAudioPlayMod` -- four-channel Protracker `.MOD`, mixed in software on
|
||||
DOS and ST, hardware-replayed on Amiga and IIgs.
|
||||
|`jlAudioPlayMod` -- four-channel Protracker `.MOD`, mixed in software by
|
||||
JoeyLib's module player on DOS, the ST, the X68000, and the hosted ports,
|
||||
played by DVX's music library on DVX, and replayed by the sound hardware on
|
||||
the Amiga and IIgs. The module player is the Amiga's replayer, PTPlayer,
|
||||
translated to C, so those ports play a module as the Amiga does, at a PAL
|
||||
Amiga's pitch, Protracker 2.3's quirks and all.
|
||||
|
||||
|Chip tracker
|
||||
|`jlMusicPlay` -- a portable event stream driving three tone voices plus
|
||||
noise, at register-write cost with no CPU mixing.
|
||||
|
||||
|Voices and effects
|
||||
|`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx` and the streaming forms.
|
||||
|`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and the streaming forms.
|
||||
|===
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
On the Atari ST and Amiga, the tone voices *are* the same hardware the module
|
||||
player drives. While a `.MOD` is playing, `jlAudioVoice` writes fight the
|
||||
player's register writes and lose. Choose the module layer or the voice and
|
||||
tracker layer *per scene*, not both at once. (The STe build plays modules and
|
||||
sound effects through the STe's DMA sound chip instead, so there the voices
|
||||
keep the YM2149 to themselves -- but a program that also ships the ST build
|
||||
still has to follow the rule.)
|
||||
On the Amiga the tone voices are the hardware the module player drives, and
|
||||
while a `.MOD` is playing, `jlAudioVoice` writes fight the player's register
|
||||
writes and lose. On the plain ST, digital sound takes the chip channel tone
|
||||
voice 0 plays on, so voice 0 is silent while a module or effect plays. On the
|
||||
IIgs the voices share sound effect slots 0 to 2 and the noise slot 3. Choose
|
||||
the module layer or the voice and tracker layer *per scene*, not both at once.
|
||||
====
|
||||
|
||||
The chip tracker is the portable choice for music. It costs almost nothing on
|
||||
every port -- unlike `.MOD` playback, which needs real CPU mixing and is
|
||||
marginal on the ST.
|
||||
every port -- unlike `.MOD` playback, which needs real CPU mixing where the
|
||||
sound hardware cannot replay a module. On a plain 8 MHz ST a four-channel
|
||||
module takes about three quarters of the machine (measured under Hatari: a
|
||||
third for the interrupt that plays each sample through the YM2149, the rest
|
||||
the mix), leaving a quarter for the game; the STe's DMA sound needs no such
|
||||
interrupt.
|
||||
|
||||
On the IIgs, `jlAudioVoice` and `jlAudioNoise` require `jlAudioInit` (they ride
|
||||
the tracker engine's DOC path) and behave as retriggered one-shots of about
|
||||
80 ms, so a note held longer must be re-asserted roughly once a frame. On DOS,
|
||||
ST and Amiga those two calls are direct hardware programming and work without
|
||||
`jlAudioInit`.
|
||||
A `jlAudioVoice` or `jlAudioNoise` note sounds until the next call for it, as
|
||||
long as `jlAudioFrameTick` runs every frame: the IIgs plays each note as a
|
||||
short sample and restarts the held ones from there.
|
||||
|
||||
A failed `jlAudioInit` is *not* fatal. `jlInit` still succeeds and every audio
|
||||
call becomes a silent no-op, so a program need not treat missing sound as an
|
||||
error.
|
||||
`jlInit` starts audio (`jlConfigT.noAudio` leaves it off) and `jlShutdown`
|
||||
stops it. Sound that does not start is *not* fatal: `jlInit` still succeeds,
|
||||
modules and sound effects become silent no-ops (the tone and noise voices
|
||||
still play on the Amiga, the ST, DOS, and the X68000), so a program need not
|
||||
treat missing sound as an error.
|
||||
|
||||
=== Per-Platform Behaviour Summary
|
||||
|
||||
|
|
@ -207,9 +225,9 @@ The API is uniform; these are the places where what you get differs.
|
|||
|Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |DVX |SDL3, Web, Win32, PSP, Haiku, BeOS |Win16
|
||||
|
||||
|Frame rate
|
||||
|60 Hz
|
||||
|50 Hz (PAL)
|
||||
|60 Hz (NTSC)
|
||||
|60 Hz (50 on a PAL machine)
|
||||
|50 Hz (PAL; 60 on an NTSC machine without PAL)
|
||||
|50 or 60 Hz, as the machine is set
|
||||
|70 Hz
|
||||
|55 Hz (56 under MAME)
|
||||
|70 Hz (virtual; the real retrace on the whole screen)
|
||||
|
|
@ -221,7 +239,7 @@ The API is uniform; these are the places where what you get differs.
|
|||
|planar
|
||||
|planar, interleaved
|
||||
|chunky 4bpp
|
||||
|text-plane planar
|
||||
|planar (shown through the text plane)
|
||||
|chunky 4bpp
|
||||
|chunky 4bpp
|
||||
|chunky 4bpp
|
||||
|
|
@ -234,11 +252,11 @@ The API is uniform; these are the places where what you get differs.
|
|||
|compiled
|
||||
|compiled
|
||||
|interpreted
|
||||
|interpreted
|
||||
|compiled
|
||||
|
||||
|Stereo pan
|
||||
|all three
|
||||
|left and right
|
||||
|all three (centred on a free channel each side)
|
||||
|ignored (mono)
|
||||
|ignored (mono)
|
||||
|ignored
|
||||
|
|
@ -246,24 +264,36 @@ The API is uniform; these are the places where what you get differs.
|
|||
|all three
|
||||
|ignored (mono)
|
||||
|
||||
|`jlAudioTone`
|
||||
|DOC oscillator
|
||||
|Paula
|
||||
|YM2149
|
||||
|PIT counter 2
|
||||
|-
|
||||
|a DVX tone (FM chip or mixer)
|
||||
|voice 0 of the mixer
|
||||
|voice 0 of the mixer
|
||||
|Program icon (<<_icons>>)
|
||||
|the Finder's (`Icons` folder)
|
||||
|Workbench's (`Name.info`)
|
||||
|none
|
||||
|none
|
||||
|none
|
||||
|Program Manager's
|
||||
|the window's (SDL3), the page's (web), Explorer's (Win32), the menu's
|
||||
(PSP), Tracker's (Haiku, BeOS)
|
||||
|Program Manager's
|
||||
|
||||
|Tone voices (`jlAudioTone` is voice 0)
|
||||
|DOC oscillators on sound effect slots 0 to 2
|
||||
|Paula channels 0 to 2
|
||||
|YM2149 (voice 0 silent while digital sound plays)
|
||||
|OPL2 (with none, voice 0 on the PC speaker)
|
||||
|YM2151 (sine waves)
|
||||
|DVX tones (FM chip or mixer)
|
||||
|square waves in the mixer
|
||||
|square waves in the mixer
|
||||
|
||||
|Joystick
|
||||
|analog, auto-disables
|
||||
|digital
|
||||
|one, analog, auto-disables
|
||||
|digital
|
||||
|digital
|
||||
|analog
|
||||
|digital
|
||||
|analog (DVX calibrates)
|
||||
|the OS's gamepads or joysticks (OS calibrates); the PSP's own controls
|
||||
|SDL's gamepads and joysticks (SDL3, web); Windows' joysticks (Win32); the system's joysticks
|
||||
(Haiku, BeOS); the PSP's own controls
|
||||
|Windows' joysticks (Control Panel calibrates)
|
||||
|
||||
|Keyboard layout
|
||||
|
|
@ -273,7 +303,7 @@ The API is uniform; these are the places where what you get differs.
|
|||
|fixed US
|
||||
|machine's own
|
||||
|DVX's
|
||||
|the OS's; the PSP has no keyboard
|
||||
|the OS's; on the PSP, a US one on the screen
|
||||
|Windows'
|
||||
|
||||
|Serial ports
|
||||
|
|
@ -281,28 +311,35 @@ The API is uniform; these are the places where what you get differs.
|
|||
|`serial.device`
|
||||
|Modem port
|
||||
|COM1..COM4
|
||||
|-
|
||||
|RS-232C (needs RSDRV.SYS)
|
||||
|COM1..COM4 (DVX's library)
|
||||
|Win32: COM1 and up; the others -
|
||||
|Win32: COM1 and up; SDL3, web, PSP, Haiku, BeOS: none
|
||||
|COM1..COM9
|
||||
|===
|
||||
|
||||
The joystick row deserves a note: the IIgs port *auto-disables* joystick
|
||||
polling after a short window with no stick detected, because polling costs
|
||||
about 3 ms per frame there. It does not re-probe on its own. If a user plugs a
|
||||
stick in while the program is running, you must call `jlJoystickReset` to
|
||||
resume polling.
|
||||
The joystick row deserves a note: the IIgs port stops polling for a stick
|
||||
after about a second (60 polls) with none detected, because polling for an
|
||||
absent stick costs about 1 ms a frame there; `jlJoystickReset` starts it again
|
||||
(<<_jljoystickreset>>).
|
||||
|
||||
On the X68000 both joystick ports always report a stick, as on the ST: an
|
||||
empty port cannot be told from a stick at rest. `JOY_BUTTON_0` is a stick's
|
||||
trigger A and `JOY_BUTTON_1` its trigger B. Holding both directions of an axis
|
||||
empty port cannot be told from a stick at rest. `JL_JOY_BUTTON_0` is a stick's
|
||||
trigger A and `JL_JOY_BUTTON_1` its trigger B. Holding both directions of an axis
|
||||
reads as the positive one (right, down), as on the Amiga and ST.
|
||||
|
||||
On the ST, `JOYSTICK_0` is port 1, the joystick port, and `JOYSTICK_1` port 0.
|
||||
On the ST, `JL_JOYSTICK_0` is port 1, the joystick port, and `JL_JOYSTICK_1` port 0.
|
||||
JoeyLib keeps TOS's keyboard-processor mode, in which port 0 is the mouse's:
|
||||
the ST reports no stick movement there, so `JOYSTICK_1` stays at rest whatever
|
||||
is plugged in. Port 1's fire button is the same line as the mouse's right
|
||||
button, so a press reads as both `JOY_BUTTON_0` and `MOUSE_BUTTON_RIGHT`.
|
||||
the ST reports no stick movement there, so `JL_JOYSTICK_1` stays at rest whatever
|
||||
is plugged in. A stick there has one button, so a quick press is
|
||||
`JL_JOY_BUTTON_0` and a held one `JL_JOY_BUTTON_1` (<<_jljoydown_jljoypressed_jljoyreleased>>).
|
||||
Port 1's fire button is the same line as the mouse's right button, so a press
|
||||
reads as both the stick's button and `JL_MOUSE_BUTTON_RIGHT`.
|
||||
|
||||
The 1040 STe also has two enhanced joystick ports, and the STe build reads an
|
||||
Atari Jaguar pad in each. A pad in enhanced port A is `JL_JOYSTICK_0` whenever
|
||||
it was used more recently than port 1's stick, so either one plays, and a pad
|
||||
in enhanced port B is `JL_JOYSTICK_1`. A pad's A and B buttons are
|
||||
`JL_JOY_BUTTON_0` and `JL_JOY_BUTTON_1`. The Mega STe has no enhanced ports.
|
||||
|
||||
On the Amiga the sticks are read through `lowlevel.library`, which Kickstart
|
||||
3.1 does not have in ROM: AmigaOS loads it from `LIBS:`, so the boot drive must
|
||||
|
|
@ -312,12 +349,15 @@ on its own must do the same. `lowlevel.library` reports a stick only once it
|
|||
has moved, and never a mouse. It also checks each port again now and then, and
|
||||
a two-button stick with both buttons held can read as nothing at that moment,
|
||||
so once a port has shown a stick JoeyLib keeps it one; `jlJoystickReset` hands
|
||||
the port back to `lowlevel.library`'s own detection.
|
||||
the port back to the library's own detection. `lowlevel.library` reports
|
||||
a one-button stick and a two-button one alike, so a stick is taken for a
|
||||
one-button one, a held button being `JL_JOY_BUTTON_1`, until its second button
|
||||
is first pressed; a CD32 pad has both from the start.
|
||||
|
||||
On DOS the game port's counts depend on the stick and on the CPU's speed, so
|
||||
the first reading after `jlInit` or `jlJoystickReset` is taken as the stick's
|
||||
rest, and a push past half that value either way reads as a full push. Call
|
||||
`jlJoystickReset` with the stick centred if it was not at the start.
|
||||
On DOS the game port's counts depend on the stick and on the CPU's speed, so a
|
||||
stick's first reading -- after `jlInit`, after `jlJoystickReset`, or when it is
|
||||
plugged in -- is taken as its centre, and its axes are read in proportion to
|
||||
that: a count of 0 or twice the centre is a full push.
|
||||
|
||||
The X68000's display refreshes at 55.46 Hz. `jlFrameHz` times it when the
|
||||
program starts and rounds to the nearest whole number, so it reports 55 --
|
||||
|
|
@ -325,18 +365,238 @@ and 56 under MAME, whose X68000 display runs slightly fast (56.07 Hz).
|
|||
`jlMillisElapsed` does not depend on it: it reads the machine's 100 Hz system
|
||||
timer to the millisecond.
|
||||
|
||||
Per-scanline palettes cost something on the X68000, and only there. Its
|
||||
display has one 16-colour palette, so the port reloads it as the beam scans: an
|
||||
interrupt stores the next band's colours in the horizontal blanking before the
|
||||
band's first line. A screen with one palette costs nothing and arms no
|
||||
interrupt. Each change of palette between one line and the next costs about
|
||||
one line of CPU time (32 microseconds); a band more than two lines from the
|
||||
one before costs about two. A screen that changes palette on every line
|
||||
therefore spends a third of each frame in that interrupt, during which serial
|
||||
and mouse interrupts wait (see the serial chapter for how long a run can be
|
||||
before received bytes are lost). Palette and SCB changes take effect at the
|
||||
start of the next frame rather than mid-frame, so a fade can lag its pixels by
|
||||
one frame but never tears.
|
||||
Per-scanline palettes cost CPU time on the ST and the X68000, whose displays
|
||||
have one 16-colour palette, and a stall on the Amiga when the bands move. The
|
||||
ST takes a timer interrupt at every change of palette, every frame, and copies
|
||||
the next band's 16 colours there. The X68000 reloads its palette as the beam
|
||||
scans: an interrupt stores the next band's colours in the horizontal blanking
|
||||
before the band's first line. A screen with one palette costs nothing on
|
||||
either and arms no interrupt. On the X68000 each change of palette between one
|
||||
line and the next costs about one line of CPU time (32 microseconds); a band
|
||||
more than two lines from the one before costs about two. A screen that changes
|
||||
palette on every line therefore spends a third of each frame in that
|
||||
interrupt, during which serial and mouse interrupts wait (see the serial
|
||||
chapter for how long a run can be before received bytes are lost). On the
|
||||
Amiga the copper changes the palettes at no CPU cost, but an SCB change that
|
||||
moves where a band starts or ends rebuilds the copper list, which holds up
|
||||
that `jlStagePresent` for about three frames on an A500; a change of colours,
|
||||
or of which palette an unmoved band uses, costs nothing. On the ST a
|
||||
present's colours and pixels show from the same frame: it hands the
|
||||
interrupts their new colours with the page it flips to. On the X68000, a
|
||||
screen of two or more bands takes palette and SCB changes at the start of the
|
||||
next frame rather than mid-frame, so a fade can lag its pixels by one frame
|
||||
but never tears; a screen of one palette has its colours written at the
|
||||
present.
|
||||
|
||||
=== Where the Ports Still Differ
|
||||
|
||||
The API is the same on every port, and where a port behaved differently from
|
||||
the rest because of the library, the library has been changed. What is left is
|
||||
set by the machines and by the systems the ports run on, and stays. Each entry
|
||||
names the ports, says how they differ, and gives the reason.
|
||||
|
||||
[[portsDifferDisplay]]
|
||||
==== Display
|
||||
|
||||
* *IIgs: a big present can tear.* The Super Hi-Res display reads one fixed
|
||||
buffer, with no second page to flip to, and a full-screen present takes
|
||||
several frames, so the screen can show part of the old picture and part of
|
||||
the new.
|
||||
* *Win16, and DVX in a window: no vertical blank.* Windows 3.1 has no call that
|
||||
waits for the display's retrace, and DVX syncs to it only on its full screen,
|
||||
so `jlWaitVBL` keeps a virtual frame clock there and nothing stops a present
|
||||
from tearing.
|
||||
* *X68000: a small, wide picture.* The machine has no 320x200 mode, so the stage
|
||||
sits in the middle of a 512x512 screen, a picture about 2.1 times as wide as
|
||||
it is tall. That screen is a 31 kHz mode, so the monitor must take 31 kHz.
|
||||
* *ST: eight levels a channel.* The plain ST's video chip has 3 bits per colour
|
||||
channel, so `$0RGB` values one apart can look the same. The STe build shows
|
||||
all 4 bits.
|
||||
* *X68000: one intensity bit for three channels.* Its colours have 5 bits per
|
||||
channel and an intensity bit the three share. JoeyLib sets that bit for every
|
||||
colour but black, so white is full white, and a channel that is 0 in any
|
||||
other colour shows at 1/63 rather than 0: `$F00` carries a trace of green and
|
||||
blue.
|
||||
* *Windows and DVX on a 256-colour display.* The display has one palette for
|
||||
everything on it. Windows keeps 20 of its entries for itself, and DVX shares
|
||||
it among all its programs, so Win32 and a DVX window show each colour as the
|
||||
nearest one the display has, and Win16 shows them exactly only while the 236
|
||||
entries left are enough (<<_windows_programs>>).
|
||||
* *Amiga and X68000: colours a frame off their pixels.* A present's new
|
||||
colours and new pixels show from the same frame on the other ports (the IIgs
|
||||
and DOS change both at once, and can tear). The Amiga's pixels change at the
|
||||
next frame, when the copper loads the new bitplane pointers, but the bands
|
||||
below the beam take their new colours in the frame the present runs: the
|
||||
copper list is graphics.library's, one per screen, and JoeyLib patches its
|
||||
colours in place. The X68000 draws straight into the displayed text plane,
|
||||
which has no second page, while a screen of two or more bands takes its new
|
||||
colours at the start of the next frame, so there the colours come a frame
|
||||
after the pixels.
|
||||
* *ST and X68000: per-scanline palettes cost time.* Both displays have one
|
||||
16-colour palette at a time, so the CPU loads each band's colours as the beam
|
||||
reaches it: the ST in a timer interrupt at every change of palette, every
|
||||
frame, and the X68000 in a raster interrupt. A screen with one palette costs
|
||||
nothing (<<_per_platform_behaviour_summary>>).
|
||||
|
||||
[[portsDifferInput]]
|
||||
==== Input
|
||||
|
||||
* *IIgs keyboard: one key at a time, by character.* The keyboard reports only
|
||||
the last character typed and whether any key is down. So JoeyLib sees one key
|
||||
besides the modifiers at a time, knows a key by the character it types rather
|
||||
than by its place on the keyboard, and reads it released only when every key
|
||||
is up. With Ctrl held, Ctrl+H, I, J, K, M, and U read as those letters, not as
|
||||
the left arrow, Tab, the down and up arrows, Return, and the right arrow,
|
||||
whose codes they share; a shifted digit key reads as its digit only on
|
||||
the US layout; and the keypad's keys read as the characters they type, so
|
||||
its 8, 2, 4, and 6 are the digits, not the arrows as on the other ports.
|
||||
* *IIgs keyboard: the keys it lacks.* It has no F1 to F10, no forward Delete
|
||||
(Delete is the backspace key), and one Shift as far as JoeyLib can tell:
|
||||
`JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. Option is
|
||||
`JL_KEY_LALT`.
|
||||
* *ST keyboard: the Alt layer on TOS before 4.00.* A national keyboard types
|
||||
some ASCII with Alt (a German one's @ [ ] { } \). TOS 4.00 and later, and
|
||||
EmuTOS, keep that layer in the keyboard tables JoeyLib reads, so Alt types
|
||||
it; older TOS keeps it in the keyboard code JoeyLib replaces, so there Alt
|
||||
with a key types nothing, as on the ports where Alt is a command key.
|
||||
* *X68000 keyboard: one Shift.* Both Shift keys send the same code, so
|
||||
`JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. OPT.1 and OPT.2 are
|
||||
`JL_KEY_LALT`.
|
||||
* *IIgs joystick: one stick, sharing two keys.* The game port takes one stick,
|
||||
and its buttons are wired to the same lines as the Command and Option keys,
|
||||
so holding either key reads as a button.
|
||||
* *Mouse buttons.* The IIgs mouse has one button, and the ST and X68000 mice
|
||||
have two; the other ports report three where the mouse has them.
|
||||
* *ST and X68000: a stick is always connected.* An empty joystick port reads
|
||||
exactly as a stick at rest, so `jlJoystickConnected` is true for both.
|
||||
* *ST: the mouse port's stick.* The keyboard processor reports port 0 as the
|
||||
mouse in the mode JoeyLib keeps it in, so `JL_JOYSTICK_1` never moves, and
|
||||
port 1's fire button is the mouse's right button too.
|
||||
* *Amiga: one button or two.* `lowlevel.library` reports a one-button and a
|
||||
two-button stick alike, so a stick counts as a one-button one until its
|
||||
second button is first pressed.
|
||||
* *PSP: no mouse, one key at a time.* The PSP has no mouse, and its on-screen
|
||||
keyboard has one cursor, so it presses one key at a time (Shift, Ctrl, and
|
||||
Alt stay down until the next key is let go).
|
||||
|
||||
[[portsDifferAudio]]
|
||||
==== Audio
|
||||
|
||||
* *Plain ST: voice 0 is silent while digital sound plays.* The ST has no
|
||||
digital sound chip, so JoeyLib plays modules and effects by changing the
|
||||
sound chip's channel A volume 12288 times a second, and channel A is tone
|
||||
voice 0's. Voice 0 sounds again when the digital sound stops, and voices 1
|
||||
and 2 play on. The STe build uses the STe's DMA sound and keeps all three.
|
||||
* *ST: noise shares voice 2's volume.* The YM2149 has one volume per channel,
|
||||
and its noise is mixed into channel C, voice 2's. Author noise hits and
|
||||
voice-2 notes apart; `songbake` warns when they overlap.
|
||||
* *ST: volume steps.* The YM2149 has 16 volume levels about 3.5 dB apart, so a
|
||||
2 dB attenuation step is matched to within 1.6 dB.
|
||||
* *Amiga: four channels for everything.* Paula has four sound channels, two
|
||||
on each side, shared by the module, the effects, the three tone voices, and
|
||||
the noise. At most four sounds play at once, an effect can take a channel a
|
||||
voice was using, and a module plays on all four. A channel plays on one side
|
||||
only, so a centred effect needs a free channel on each side, and without
|
||||
them it plays on one channel, from one side; and the tone voices, one
|
||||
channel each, are heard from one side: voice 0 (and `jlAudioTone`) on the
|
||||
left, voices 1 and 2 on the right.
|
||||
* *IIgs: voices on the effect slots.* By design the IIgs plays tone voices 0 to
|
||||
2 on sound effect slots 0 to 2 and the noise on slot 3, so an effect on one
|
||||
of those slots and a voice cut each other off. Use slot 4 for effects while
|
||||
voices or `jlMusicPlay` sound.
|
||||
* *IIgs: mono.* A stock IIgs mixes its stereo channels into one speaker, so
|
||||
panning is heard only with a stereo card.
|
||||
* *Plain ST and X68000: no panning.* Each has one output for digital sound:
|
||||
the ST its channel A, the X68000 its one ADPCM channel, so `jlAudioSetPan`
|
||||
is ignored.
|
||||
* *DOS: the noise is a drum.* The OPL2 has no free-running noise generator; its
|
||||
rhythm-mode snare is the nearest, a tuned drum whose pitch cannot follow the
|
||||
YM2149's noise rate.
|
||||
* *DOS and X68000: tones are not square.* The OPL2 and the YM2151 make their
|
||||
waves from sines and have no square wave, so their tone voices (DOS's when it
|
||||
has an OPL2) are not the square waves the other ports play.
|
||||
* *Amiga and IIgs: the brightest noise.* Paula plays a sample no faster than
|
||||
28.6 kHz and the IIgs's DOC no faster than 26 kHz, so noise pitches 0 to 4
|
||||
sound alike there.
|
||||
* *Where tick callbacks fall.* The calls average exactly `hz` a second on every
|
||||
port. The ST and X68000 deliver them from the system's own timer, at 200 and
|
||||
100 Hz, so the calls due between two of its interrupts come together. DVX
|
||||
delivers them at `jlAudioFrameTick`, because its sound library may not be
|
||||
called from an interrupt, and so does the IIgs, counting them against the
|
||||
display's beats.
|
||||
* *DOS, the ST, and the X68000 mix in program time.* The module player and
|
||||
the SFX overlay run too long for an interrupt, so their sound buffers are
|
||||
refilled at `jlAudioFrameTick`: a program that stops calling it for longer
|
||||
than the buffer lasts stops or repeats its sound.
|
||||
* *Win16 with a Sound Blaster 2.0: 8-bit sound.* Its driver, SNDBLST2.DRV, takes
|
||||
only 8-bit samples, so the quietest attenuation steps are coarser there.
|
||||
|
||||
[[portsDifferSerial]]
|
||||
==== Serial
|
||||
|
||||
* *Top speeds.* The ST runs at 19200 baud at most (its MFP's rate table), a
|
||||
IIgs Super Serial Card at 19200 (the 6551's), the X68000 at 38400 (RSDRV.SYS's
|
||||
table; really 39062), and Win16 with Windows' own COMM.DRV at 57600.
|
||||
* *One port.* The base ST has one RS-232 port, and so has the X68000, whose
|
||||
second serial channel is the mouse's.
|
||||
* *IIgs modem and printer ports: no RTS.* Their mini-DIN-8 connector's one
|
||||
handshake output is DTR, so under RTS/CTS nothing can tell the peer to
|
||||
pause: receive needs XON/XOFF or a protocol that paces itself. The printer
|
||||
port's channel is also AppleTalk's.
|
||||
* *IIgs Super Serial Card: lines tied together.* The 6551's DTR is also its
|
||||
receiver's enable, and dropping its RTS stops its transmitter.
|
||||
* *X68000: DTR rises at every line setup.* The ROM's and RSDRV.SYS's line setup
|
||||
raise DTR whenever it runs.
|
||||
* *Windows: DTR drops at close.* Win32's driver, and Win16 with Windows' own
|
||||
COMM.DRV, drop DTR when the port is closed, and neither has a call that
|
||||
prevents it.
|
||||
* *IIgs: disk access stops interrupts.* GS/OS holds interrupts off for 65 to
|
||||
220 ms at a time while it reads or writes a 3.5-inch floppy, which can lose
|
||||
received bytes (<<_what_can_lose_received_bytes>>).
|
||||
|
||||
[[portsDifferFiles]]
|
||||
==== Files and Saves
|
||||
|
||||
* *Name rules.* ProDOS names on the IIgs are at most 15 characters and start
|
||||
with a letter; DOS, the ST, and Win16 use 8.3 names; and Linux, Haiku, BeOS,
|
||||
and the web tell upper and lower case apart. Keep to the portable rule
|
||||
(<<_files>>).
|
||||
* *Pre-compiled sprites per processor.* A `.spc` holds machine code, so it is
|
||||
baked per port: DVX and Win16 run DOS's x86 code, the STe the ST's, and the
|
||||
ports that interpret sprites use only its pixels.
|
||||
* *Web saves are the browser's.* A browser may clear a site's stored data to
|
||||
make room, and a private window keeps nothing once it closes.
|
||||
|
||||
[[portsDifferMemory]]
|
||||
==== Sprites and Memory
|
||||
|
||||
* *IIgs: code and copies stay inside 64 KB banks.* The 65816 runs code within
|
||||
one 64 KB bank and its block copy stays within one, so each sprite's code
|
||||
must fit in one bank of the codegen arena, the arena is at most four banks,
|
||||
compacting never merges free space across banks, and a save-under buffer
|
||||
must not cross a bank.
|
||||
* *Code size differs by processor.* The 68000 and x86 emitters make code four
|
||||
to eight times larger than the 65816 one, so with the same `codegenBytes` a
|
||||
large sprite can compile on the IIgs and fall back to the interpreter
|
||||
elsewhere.
|
||||
* *IIgs: a small C heap, and `jlFree` apart from `free`.* The C library's heap
|
||||
is one Memory Manager block inside one bank, 32 KB to 48 KB, and a `jlAlloc`
|
||||
block is a Memory Manager handle, so `jlFree` and `free` must never be given
|
||||
each other's blocks.
|
||||
|
||||
[[portsDifferTiming]]
|
||||
==== Timing
|
||||
|
||||
* *Frame rates.* The displays run at 50, 55, 60, and 70 Hz, as the table above
|
||||
shows; the hosted ports and DVX keep a virtual 60 and 70 Hz.
|
||||
* *IIgs: the clock stops while interrupts are off.* The IIgs has no free-running
|
||||
timer JoeyLib can read, so its clock counts the display's vertical blanks,
|
||||
and blanks that come while interrupts are held off are lost: a long stretch
|
||||
with interrupts off, a floppy access for one, leaves the clock behind.
|
||||
* *DOS: frames counted by the clock.* VGA has no dependable retrace interrupt,
|
||||
so `jlFrameCount` counts 70ths of a second of the millisecond clock while
|
||||
`jlWaitVBL` waits for the real retrace (70.086 Hz), and the two slowly drift
|
||||
apart.
|
||||
|
||||
=== A Portability Checklist
|
||||
|
||||
|
|
@ -365,7 +625,7 @@ audience (or both):
|
|||
|
||||
* *The ST build* (`make atarist`, programs in `build/atarist/bin`) is the
|
||||
baseline. It is pure 68000 and *never uses STe hardware*: it runs on every
|
||||
ST -- STF, STFM, Mega ST, STe and Mega STe alike -- exactly as on a plain
|
||||
ST -- STF, STFM, Mega ST, STe, and Mega STe alike -- exactly as on a plain
|
||||
ST, at no cost for the STe's existence.
|
||||
* *The STe build* (`make ste`, programs in `build/ste/bin`) is the same port
|
||||
compiled with `JOEYLIB_ATARI_STE`. It uses the STe's hardware
|
||||
|
|
@ -406,7 +666,7 @@ Nothing in the API differs between them:
|
|||
The pixels are identical: UBER's 51 surface hashes match the ST's golden for
|
||||
the STe build on an STe and a Mega STe, and the gates check that under Hatari.
|
||||
`./joey-run` gives an ST-build program a plain ST and an STe-build program an
|
||||
STe; `--machine st`, `megast`, `ste` or `megaste` picks another. A project
|
||||
STe; `--machine st`, `megast`, `ste`, or `megaste` picks another. A project
|
||||
builds the STe variant with `make ste`, like any other port; it takes the ST's
|
||||
baked assets.
|
||||
|
||||
|
|
@ -420,7 +680,7 @@ expect less on memory-bound operations there.
|
|||
=== DVX Programs
|
||||
|
||||
DVX is a DOS desktop: a VESA graphics GUI with windows, cooperative
|
||||
multitasking and its own libraries for sound, music, joysticks and serial
|
||||
multitasking, and its own libraries for sound, music, joysticks, and serial
|
||||
ports. On DVX a JoeyLib program is one of its programs, a `.app` module
|
||||
running in a window of its own beside the others. The graphics are the
|
||||
MS-DOS port's -- the same chunky stage and the same x86 code, so UBER's
|
||||
|
|
@ -430,7 +690,7 @@ hashes are DOS's -- and the rest behaves like this:
|
|||
at the largest whole scale, up to 3x, that fits the desktop: 1x at
|
||||
640x480, 2x at 800x600, 3x at 1024x768. The stage reaches the desktop
|
||||
through one colour table per palette, so SCB palettes are exact on 15-,
|
||||
16- and 32-bit desktops. On a 256-colour desktop every colour is the
|
||||
16-, and 32-bit desktops. On a 256-colour desktop every colour is the
|
||||
nearest one in DVX's palette, which all programs share: gradients band.
|
||||
* *Full screen.* Game > Full Screen, or Alt+Enter, puts the program on the
|
||||
whole screen in DVX's full-screen mode, 320x200 in 256 colours (VGA mode
|
||||
|
|
@ -453,18 +713,18 @@ hashes are DOS's -- and the rest behaves like this:
|
|||
does not tear. The frame count is the same clock in both, so switching
|
||||
never makes it jump.
|
||||
* *Keys DVX keeps.* While the window is active, or the program has the whole
|
||||
screen, every key goes to the program, F1, F10 and Alt included, except
|
||||
screen, every key goes to the program, F1, F10, and Alt included, except
|
||||
Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close the
|
||||
window), and Alt+Enter, which the port keeps for full screen. The window's
|
||||
Game menu has Full Screen, Help (the program's help file) and Exit. Typed
|
||||
characters come through DVX's keyboard layout.
|
||||
* *Closing ends the program.* Alt+F4, the close box and Game > Exit end the
|
||||
program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent` or
|
||||
* *Closing ends the program.* Alt+F4, the close box, and Game > Exit end the
|
||||
program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent`, or
|
||||
`jlSerialPoll`, as `exit()` does anywhere: its `atexit` handlers run, then
|
||||
`jlShutdown`, and its log is closed.
|
||||
* *Sharing the machine.* DVX multitasks cooperatively: the other programs,
|
||||
and this one's own window events, run only while it yields. `jlWaitVBL`
|
||||
yields, and so do `jlInputPoll`, `jlStagePresent` and `jlSerialPoll`, at
|
||||
yields, and so do `jlInputPoll`, `jlStagePresent`, and `jlSerialPoll`, at
|
||||
most once a frame. A loop that calls none of them freezes the desktop until
|
||||
it ends.
|
||||
* *Files.* DVX's programs share one current directory, so JoeyLib roots its
|
||||
|
|
@ -518,32 +778,36 @@ this:
|
|||
program presents.
|
||||
* *Sprites are interpreted.* There is no code emitter for the host's CPU, so
|
||||
`jlSpriteCompile` returns false and every sprite is drawn by the
|
||||
interpreter -- far faster than any of the old machines all the same.
|
||||
`joey/platform.h` defines `JOEYLIB_SPRITE_INTERPRETED`, for a program that
|
||||
interpreter -- far faster than any of the old machines all the same. (The
|
||||
Win16 port, below, compiles them as DOS does.)
|
||||
`joey/platform.h` defines `JL_SPRITE_INTERPRETED`, for a program that
|
||||
checks how many of its sprites compiled. A pre-compiled bank (`.spc`)
|
||||
loads its cels' pixels and leaves their code: the DOS bake is used.
|
||||
* *Input.* Keys by where they are on the keyboard, so another layout does not
|
||||
move the arrow keys; typed characters through the OS's keyboard layout. The
|
||||
mouse is the pointer over the stage, in stage pixels (the OS pointer is
|
||||
hidden over the window: JoeyLib programs draw their own). The first two
|
||||
gamepads are joysticks 0 and 1: the left stick or the d-pad, the south
|
||||
button (A on an Xbox pad) as `JOY_BUTTON_0` and the east button (B) as
|
||||
`JOY_BUTTON_1`; they come and go as they are plugged in.
|
||||
gamepads or joysticks are joysticks 0 and 1, coming and going as they are
|
||||
plugged in. A gamepad SDL knows how to map gives its left stick or its
|
||||
d-pad, its south button (A on an Xbox pad) as `JL_JOY_BUTTON_0`, and its
|
||||
east button (B) as `JL_JOY_BUTTON_1`; any other joystick its first two axes
|
||||
or its first hat, and its first two buttons.
|
||||
* *Closing ends the program.* Closing the window ends the program at its next
|
||||
`jlWaitVBL`, `jlInputPoll` or `jlStagePresent`, as `exit()` does anywhere:
|
||||
`jlWaitVBL`, `jlInputPoll`, or `jlStagePresent`, as `exit()` does anywhere:
|
||||
its `atexit` handlers run and the window and the sound are closed.
|
||||
* *Files.* The program's own folder is the current directory from `jlInit`
|
||||
on, as on the machine ports: `DATA/`, `SAVES/` and `joeylog.txt` are beside
|
||||
on, as on the machine ports: `DATA/`, `SAVES/`, and `joeylog.txt` are beside
|
||||
the program.
|
||||
* *Sound* is mixed in software into 16-bit stereo at 44.1 kHz: `.MOD` music
|
||||
through libxmp-lite (as on DOS), the SFX slots in stereo, the three tone
|
||||
voices as square waves and the noise voice as the ST's YM2149 makes it. It
|
||||
through the module player (as on DOS), Paula's channels hard left and
|
||||
right as on the Amiga, the SFX slots in stereo, the three tone
|
||||
voices as square waves, and the noise voice as the ST's YM2149 makes it. It
|
||||
is mixed in program time by `jlAudioFrameTick`, `jlWaitVBL`,
|
||||
`jlInputPoll` and `jlStagePresent`, a few frames ahead, so a
|
||||
`jlInputPoll`, and `jlStagePresent`, a few frames ahead, so a
|
||||
`jlAudioTickRegister` callback runs in program time too -- but between
|
||||
exactly the samples it falls between, so its notes start in the right
|
||||
place in the sound. `jlAudioInit` is needed for every sound, the tone
|
||||
voices included.
|
||||
place in the sound. Every sound needs audio started (the default), the
|
||||
tone voices included.
|
||||
* *No serial port.*
|
||||
|
||||
`make sdl3` builds every example as a native program, each in
|
||||
|
|
@ -571,9 +835,12 @@ are DOS's here too. Everything in <<_sdl3_programs>> holds, except:
|
|||
`jlInputPoll` alone, as `jlWaitForAnyKey` is).
|
||||
* *Sound starts with the first click or key press*, as browsers require; the
|
||||
page says so.
|
||||
* *Saves persist.* `SAVES/` is kept in the browser's IndexedDB for the page's
|
||||
address, read back when the program starts and written back every two
|
||||
seconds, so a save outlives the page.
|
||||
* *Saves persist.* `SAVES/` is kept in the browser's IndexedDB, read back
|
||||
when the program starts and written back as each save changes, so a save
|
||||
outlives the page however it ends. The store is the site's, not the page's:
|
||||
every JoeyLib program served from the same site shares one `SAVES/`, so give
|
||||
each game's saves names of their own. `jlDiskFree` there is the browser's
|
||||
own estimate of what the site may still store.
|
||||
* *No full screen by default and no closing.* Alt+Enter asks the browser for
|
||||
the whole screen; the program ends when the page is closed.
|
||||
|
||||
|
|
@ -591,22 +858,31 @@ is the same chunky one, so UBER's hashes are DOS's. Everything in
|
|||
* *A folder for `PSP/GAME`.* A program is a folder, `build/psp/bin/NAME/`:
|
||||
`EBOOT.PBP`, titled with the program's name (or its description's `TITLE`)
|
||||
in the PSP's menu, and its `DATA/`. Copy the folder into `PSP/GAME/` on the
|
||||
memory stick. Its folder is its current directory, so `DATA/`, `SAVES/`
|
||||
memory stick. Its folder is its current directory, so `DATA/`, `SAVES/`,
|
||||
and `joeylog.txt` are there. It runs on a PSP with custom firmware (it is
|
||||
not signed) and in PPSSPP. The ELF it is packed from stays in
|
||||
`build/psp/elf/`, for `psp-addr2line`.
|
||||
* *The screen.* The PSP's 480x272 screen shows the stage at 1:1 in its
|
||||
middle, black around it. The scale and aspect settings do not apply (a PSP
|
||||
has no environment), and there is no full screen to switch to.
|
||||
* *Its own controls, no keyboard.* The PSP's controls are joystick 0,
|
||||
through SDL's gamepad mapping: the d-pad or the analog nub, cross as
|
||||
`JOY_BUTTON_0` and circle as `JOY_BUTTON_1`; joystick 1 is never there.
|
||||
Having no keyboard, the PSP also gives Start as the Return key and Select
|
||||
as the Escape key, held (`KEY_RETURN`, `KEY_ESCAPE`) and typed
|
||||
(`JL_CHAR_RETURN`, `JL_CHAR_ESCAPE` from `jlInputGetChar`) as those keys
|
||||
are, so `jlWaitForAnyKey`, Return to go on and Escape to quit work. No
|
||||
other key is ever down, nothing else is typed (the PSP's on-screen
|
||||
keyboard is never opened), and there is no pointer to move.
|
||||
* *Its own controls, and a keyboard on the screen.* The PSP's controls are
|
||||
joystick 0, through SDL's gamepad mapping: the d-pad or the analog nub,
|
||||
cross as `JL_JOY_BUTTON_0` and circle as `JL_JOY_BUTTON_1`; joystick 1 is
|
||||
never there. Start is also the Return key and Select the Escape key, held
|
||||
(`JL_KEY_RETURN`, `JL_KEY_ESCAPE`) and typed (`JL_CHAR_RETURN`,
|
||||
`JL_CHAR_ESCAPE` from `jlInputGetChar`) as those keys are, so
|
||||
`jlWaitForAnyKey`, Return to go on, and Escape to quit work at a press.
|
||||
Square opens a PC keyboard drawn over the bottom of the screen, US layout,
|
||||
for every other key: the d-pad moves its cursor, cross presses the key
|
||||
under it (held down as long as cross is), L and R move the keyboard to
|
||||
the top of the screen and back, out of the way of what the program shows
|
||||
there, and Square closes it. Shift, Ctrl, and Alt stay down until the
|
||||
next other key is let go (or until pressed again), and Caps turns capital
|
||||
letters on and off. Its keys go down and up and type as a desktop's do,
|
||||
so `jlKeyDown`, `jlKeyPressed`, and `jlInputGetChar` see the same as they
|
||||
would there, F1 to F10, the arrows, and Ctrl and Alt chords included.
|
||||
While it is open, joystick 0 rests: the d-pad and cross are the
|
||||
keyboard's. There is no pointer to move.
|
||||
* *HOME quits.* The HOME menu's Exit ends the program as closing a window
|
||||
does.
|
||||
* *Free space* (`jlDiskFree`) is that of the memory stick (or a PSP Go's
|
||||
|
|
@ -630,26 +906,28 @@ window to play in here: to play, copy the folder to a PSP, or open its
|
|||
|
||||
The Win32 port is the hosted core with a backend of its own, written to the
|
||||
Win32 API (`src/hosted/win32`), for the Windows SDL3 does not reach --
|
||||
Windows 95, 98, ME, NT 4 and 2000 -- and every Windows since. Its stage is
|
||||
Windows 95, 98, ME, NT 4, and 2000 -- and every Windows since. Its stage is
|
||||
the same chunky one, so UBER's hashes are DOS's. Everything in
|
||||
<<_sdl3_programs>> holds, except:
|
||||
|
||||
* *Old Windows on purpose.* A program is a 32-bit `NAME.EXE` built by
|
||||
llvm-mingw against `MSVCRT.DLL` (the `win32` installer component), marked
|
||||
for Windows 4.0 so 95 and NT 4 will load it, linked statically, and
|
||||
calling only what USER, GDI, WINMM and KERNEL32 had in 1995. `MSVCRT.DLL`
|
||||
calling only what USER, GDI, WINMM, and KERNEL32 had in 1995. `MSVCRT.DLL`
|
||||
came with Windows 95 OSR2 and Internet Explorer; a first-release Windows
|
||||
95 needs it installed.
|
||||
* *One thread.* The window is made on the program's thread, so its messages
|
||||
arrive there and the port dispatches them at `jlInputPoll`, `jlWaitVBL`
|
||||
arrive there and the port dispatches them at `jlInputPoll`, `jlWaitVBL`,
|
||||
and `jlStagePresent`; the sound goes out through waveOut buffers handed
|
||||
over at the same calls. There are no callbacks and no locks.
|
||||
* *The window.* The stage is drawn with `StretchDIBits` at the stage's shape,
|
||||
4:3 or square (the same `JOEYLIB_SCALE`, `JOEYLIB_ASPECT` and
|
||||
* *The window.* The stage is stretched with `StretchBlt` to the stage's
|
||||
shape, 4:3 or square (the same `JOEYLIB_SCALE`, `JOEYLIB_ASPECT`, and
|
||||
`JOEYLIB_FULLSCREEN` settings), black around it; full screen is a
|
||||
borderless window over the whole monitor, which leaves the display mode
|
||||
alone. On a 256-colour desktop Windows maps every colour to the nearest
|
||||
one it has.
|
||||
one it has. Each frame draws only what changed since the last: a game
|
||||
that moves a few sprites pays for their rectangles, not for the whole
|
||||
window, which on a Windows 95 machine is most of a frame's time.
|
||||
* *Keys* by the PC scan code every key message carries (the same table as
|
||||
DOS), typed characters through the user's keyboard layout, and the first
|
||||
two joysticks through `joyGetPosEx`, calibrated in the Control Panel. Keys
|
||||
|
|
@ -659,8 +937,9 @@ the same chunky one, so UBER's hashes are DOS's. Everything in
|
|||
|
||||
`make win32` builds every example as a Windows program in `build/win32/bin/`,
|
||||
and `./joey-run --port win32 NAME` runs one under Wine (the `wine` installer
|
||||
component, a portable Wine kept in `toolchains/`); the gates run there too.
|
||||
None of it has run on a real Windows 9x or NT 4 here.
|
||||
component, a portable Wine kept in `toolchains/`); the gates run there too,
|
||||
`scripts/verify-win32-display.sh` among them, though that checks Wine's GDI
|
||||
rather than Windows'. None of it has run on a real Windows 9x or NT 4 here.
|
||||
|
||||
The Win16 port is the hosted core with a backend written to the Windows 3.1
|
||||
API (`src/hosted/win16`), for Windows 3.1 and Windows for Workgroups 3.11 on
|
||||
|
|
@ -672,16 +951,54 @@ scan code, DTR dropped at close -- except:
|
|||
with Open Watcom's Win386 target (the `win16` installer component): flat
|
||||
32-bit code bound to Watcom's supervisor, which turns each call into the
|
||||
16-bit one Windows takes, so it needs Windows in 386 enhanced mode. It
|
||||
calls only what Windows 3.1 has: USER, GDI, KERNEL and MMSYSTEM.
|
||||
* *Exact colours.* The frame is 8-bit, a DIB whose 256-entry colour table
|
||||
holds all 16 palettes and is realized as a logical palette whenever a frame
|
||||
is shown, so on a 256-colour display every colour is exact, SCB palettes
|
||||
included. `JOEYLIB_SCALE` defaults to 2, and the settings are read from the
|
||||
DOS environment Windows started from (`joey-run` puts its own there).
|
||||
Under DOSBox, Alt+Enter is DOSBox's own full-screen key; use Alt with the
|
||||
calls only what Windows 3.1 has: USER, GDI, KERNEL, and MMSYSTEM.
|
||||
* *Exact colours.* The frame is 8-bit, each pixel one entry of a 256-entry
|
||||
colour table holding all 16 palettes, which on a 256-colour display is a
|
||||
logical palette too, so every colour is exact, SCB palettes included, as
|
||||
far as the display has room: Windows keeps 20 of a 256-colour display's
|
||||
entries for itself, so with more than 14 palettes in use and more than 236
|
||||
different colours on screen, some show as the nearest it has.
|
||||
`JOEYLIB_SCALE` defaults to 2, and the settings are read from the DOS
|
||||
environment Windows started from (`joey-run` puts its own there). Under
|
||||
DOSBox, Alt+Enter is DOSBox's own full-screen key; use Alt with the
|
||||
keypad's Enter.
|
||||
* *Sound* is 8-bit mono at 22.05 kHz through waveOut, what a Sound Blaster
|
||||
and a 386 handle well; stereo pan is ignored.
|
||||
* *Speed on a 386.* Sprites are compiled to x86 code, as on DOS. The port
|
||||
expands the stage into the window's pixels itself, a presented row at a
|
||||
time and scaled as it goes, and hands Windows only the rectangles that
|
||||
changed, those close enough together as one. With Microsoft's WinG 1.0 on
|
||||
the machine (its runtime in Windows' `SYSTEM` folder) the rows go straight
|
||||
into a WinG bitmap the size of the window, and drawing a rectangle is a
|
||||
plain copy to the screen; without it, each rectangle goes through a device
|
||||
bitmap on a 256-colour display, and only another kind of display pays for
|
||||
Windows matching every pixel's colour. `JOEYLIB_WING=0` leaves WinG alone.
|
||||
JoeyLib does not ship WinG: a game that wants it on its players' machines
|
||||
ships the runtime under Microsoft's terms (see `LICENSES.md`). While the
|
||||
SCBs use 14 palettes or fewer, the frame's colours are the screen's own (an
|
||||
identity palette, as WinG recommends), so a palette change, colour cycling
|
||||
included, reaches the screen at once with nothing redrawn; more palettes
|
||||
than that, or a display without a palette, redraw the window when colours
|
||||
change. On a 386SX-16 (DOSBox's model of one), a game loop presenting
|
||||
every frame runs at, in frames a second, with an Apple IIgs (under MAME)
|
||||
for scale:
|
||||
+
|
||||
[cols="3,1,1,1,1", options="header"]
|
||||
|===
|
||||
| |2x with WinG |2x without |1:1 |IIgs
|
||||
|Nothing moving, or colour cycling |60 |60 |60 |60
|
||||
|One 16x16 sprite moving |60 |60 |60 |60
|
||||
|Eight sprites moving |26 |20 |30 |20
|
||||
|Eight sprites in a column |30 |20 |30 |20
|
||||
|The whole screen changing |6 |4 |10 |8.5
|
||||
|===
|
||||
+
|
||||
The 2x window has nearly five times the IIgs's pixels to fill (640x480 for
|
||||
its 320x200): a 386 keeps up with that for sprites, but not for a whole new
|
||||
screen every frame, so a game that redraws everything every frame wants
|
||||
`JOEYLIB_SCALE=1` there.
|
||||
* *Sound* is 16-bit mono at 22.05 kHz through waveOut, what a Sound Blaster
|
||||
and a 386 handle well, or 8-bit on a card whose driver has no 16-bit (a
|
||||
Sound Blaster 2.0, whose SNDBLST2.DRV is 8-bit only, which makes the quiet
|
||||
attenuation steps coarser); stereo pan is ignored.
|
||||
* *Joysticks* are the first two MMSYSTEM sticks, through `joyGetPos`, with
|
||||
their buttons 1 and 2. Windows 3.x reads sticks only through a joystick
|
||||
driver, which Windows 3.1 itself does not have; without one no stick is
|
||||
|
|
@ -718,15 +1035,21 @@ which the installer puts beside the driver in `toolchains/opt/`.
|
|||
`make win16` builds every example as a Windows 3.x program in
|
||||
`build/win16/bin/`, and `./joey-run --port win16 NAME` runs one on Windows
|
||||
for Workgroups 3.11 in DOSBox Staging (the `win311` installer component, set
|
||||
up from your own install disc); the gates run there too. None of it has run
|
||||
on a real 386 here.
|
||||
up from your own install disc); the gates run there too, among them
|
||||
`scripts/verify-win16-display.sh`, which checks every pixel the window shows
|
||||
against the stage after every present, with WinG and without (the optional
|
||||
`wing` installer component: `./install.sh import WING10.EXE`, Microsoft's
|
||||
WinG 1.0 SDK, then `./install.sh install wing`; the gates then install its
|
||||
runtime in their Windows). None of it has run on a real 386
|
||||
here; the speeds above are DOSBox's 386SX-16 model's, and the IIgs's are
|
||||
MAME's.
|
||||
|
||||
=== Haiku and BeOS Programs
|
||||
|
||||
The Be API ports are the hosted core with a backend written to the Be API
|
||||
(`src/hosted/be`, C++), which Haiku reimplements, kept to the calls BeOS R5
|
||||
already had. One source builds three ports: `haiku` (Haiku x86_64, gcc 13),
|
||||
`haiku32` (Haiku x86, gcc 2.95, the BeOS C++ ABI) and `beos` (BeOS R5 x86,
|
||||
`haiku32` (Haiku x86, gcc 2.95, the BeOS C++ ABI), and `beos` (BeOS R5 x86,
|
||||
built with the same gcc 2.95 against R5's own headers and libraries, which
|
||||
the `beosR5` installer component copies off your BeOS 5 Professional CD).
|
||||
Their stage is the same chunky one, so UBER's hashes are DOS's. Everything in
|
||||
|
|
@ -735,25 +1058,32 @@ Their stage is the same chunky one, so UBER's hashes are DOS's. Everything in
|
|||
* *Threads.* The program's `main` keeps its own thread; the application and
|
||||
the window run on threads of their own, as every Be program's do. Key and
|
||||
mouse messages reach the program as events taken at `jlInputPoll`,
|
||||
`jlWaitVBL` and `jlStagePresent`, and the sound comes from a
|
||||
`jlWaitVBL`, and `jlStagePresent`, and the sound comes from a
|
||||
`BSoundPlayer`.
|
||||
* *The window* shows the stage from a bitmap the app_server scales without
|
||||
filtering, at the stage's shape, 4:3 or square, black around it (the same
|
||||
`JOEYLIB_SCALE`, `JOEYLIB_ASPECT` and `JOEYLIB_FULLSCREEN` settings;
|
||||
`JOEYLIB_SCALE` defaults to 3). Alt+Enter switches between the window and
|
||||
* *The window* shows the stage scaled without filtering, at the stage's
|
||||
shape, 4:3 or square, black around it (the same `JOEYLIB_SCALE`,
|
||||
`JOEYLIB_ASPECT`, and `JOEYLIB_FULLSCREEN` settings; `JOEYLIB_SCALE`
|
||||
defaults to 3). The port scales the frame itself and draws only what
|
||||
changed since the last frame. Alt+Enter switches between the window and
|
||||
the whole screen. The pointer is hidden over the window.
|
||||
* *Keys* by raw key code, the key's place on the keyboard whatever the
|
||||
keymap; typed characters are the keymap's, 7-bit ASCII only. The first
|
||||
two joysticks (`BJoystick`) are `JOYSTICK_0` and `JOYSTICK_1`, their first
|
||||
two joysticks (`BJoystick`) are `JL_JOYSTICK_0` and `JL_JOYSTICK_1`, their first
|
||||
two axes and buttons.
|
||||
* *Files.* The program's folder is the current directory from the moment
|
||||
the window opens.
|
||||
* *Its icon* is in its resources (<<_icons>>), but Tracker shows a file by
|
||||
the icons in its attributes, which a program built on another machine does
|
||||
not have. When it opens its window without them, the program copies them
|
||||
across from its resources (as `mimeset -F` does), so Tracker shows it by
|
||||
its icon once it has run; that is the one time a program writes to its own
|
||||
file.
|
||||
* *No serial port.*
|
||||
* *BeOS R5's loader* refuses any program with a dynamic tag it does not
|
||||
know, so `make beos` links without combined relocations (`-z
|
||||
nocombreloc`). Build for R5 with `make beos`, not `make haiku32`.
|
||||
|
||||
`make haiku`, `make haiku32` and `make beos` build every example into
|
||||
`make haiku`, `make haiku32`, and `make beos` build every example into
|
||||
`build/<port>/bin/`, and `./joey-run --port <port> NAME` runs one in that
|
||||
port's VM under QEMU: Haiku R1/beta6 (the `haikuVm` and `haikuVm32` installer
|
||||
components) or BeOS R5 (`beosVm`, made from your CD), booted from a disk it
|
||||
|
|
@ -761,5 +1091,6 @@ never writes to, with the program's folder on a second disk. R5 drives none
|
|||
of QEMU's video cards, so its VM is taken through the boot loader's menu to
|
||||
the fail-safe (VESA) mode, 1024x768 in 16-bit colour; it has no sound card.
|
||||
The gates run in the same VMs (`scripts/verify-be-input.sh`,
|
||||
`scripts/bench-be.sh`). None of it has run on a real BeOS or Haiku machine
|
||||
here.
|
||||
`scripts/verify-be-display.sh`, `scripts/verify-be-icon.sh`, and
|
||||
`scripts/bench-be.sh`). None of it has
|
||||
run on a real BeOS or Haiku machine here.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ copy.
|
|||
jlConfigT config;
|
||||
|
||||
config.codegenBytes = 32UL * 1024; // sprite blitter cache
|
||||
config.noAudio = false; // jlInit starts audio too
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
// jlLastError() explains why. There is no display yet, so on a
|
||||
|
|
@ -24,22 +25,27 @@ if (!jlInit(&config)) {
|
|||
native sprite blitter code into. Getting it wrong is the most common cause of
|
||||
sprites drawing slowly, so it is worth understanding.
|
||||
|
||||
* `0` selects a per-platform default: 8 KB on the IIgs, 32 KB on
|
||||
Amiga, ST, DOS and DVX. SDL3 interprets every sprite and never uses it.
|
||||
* `0` selects a per-platform default: 8 KB on the IIgs, 64 KB on the
|
||||
Amiga and ST, and 32 KB on the X68000, DOS, DVX, and Win16. The other
|
||||
hosted ports interpret every sprite and never use it, though they still
|
||||
allocate it.
|
||||
* The 68000 and x86 emitters produce code roughly four to eight times less
|
||||
dense than the 65816 one, so the same sprite needs far more arena space on
|
||||
those ports. A 32x32 sprite can need 8 to 16 KB there.
|
||||
* Measure, do not guess: after compiling a sprite, `jlSpriteCompiledSize()`
|
||||
reports its exact cost on the current platform.
|
||||
* On the IIgs the arena is one Memory Manager block that may not cross a 64 KB
|
||||
bank boundary, so requests above 65536 are clamped to 65536 -- and even a
|
||||
clamped request can fail if no free bank exists.
|
||||
* On the IIgs a compiled sprite cannot cross a 64 KB bank boundary, so the
|
||||
arena is up to four Memory Manager blocks of at most 64 KB, each inside one
|
||||
bank: 256 KB at most. A bank that is refused is tried again at 60 KB, and
|
||||
`jlInit` takes whatever part of the request it was granted. Elsewhere the
|
||||
arena is one block, and `jlInit` fails if it cannot be had.
|
||||
|
||||
If the arena is too small, sprites still draw: they fall back to an
|
||||
interpreter that is roughly *forty times slower*. Nothing reports this as an
|
||||
error, so a program that never checks can be silently crawling.
|
||||
interpreter that is *two to six times slower*, depending on the machine.
|
||||
Nothing reports this as an error, so a program that never checks can be
|
||||
silently crawling.
|
||||
|
||||
=== Part 2: The Stage, the Palette and Assets
|
||||
=== Part 2: The Stage, the Palette, and Assets
|
||||
|
||||
[source,c]
|
||||
----
|
||||
|
|
@ -54,10 +60,10 @@ Set up colour before drawing:
|
|||
|
||||
[source,c]
|
||||
----
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
|
||||
// ... fill in 16 entries of $0RGB ...
|
||||
jlPaletteSet(stage, 0, colors);
|
||||
jlScbSetRange(stage, 0, SURFACE_HEIGHT - 1, 0);
|
||||
jlScbSetRange(stage, 0, JL_SURFACE_HEIGHT - 1, 0);
|
||||
----
|
||||
|
||||
Each surface carries *16 palettes of 16 colours* plus a 200-entry table (one
|
||||
|
|
@ -71,22 +77,21 @@ Load assets after init, because sprite loading needs the codegen arena:
|
|||
[source,c]
|
||||
----
|
||||
jlSpriteT *cels[16];
|
||||
uint16_t palette[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t palette[JL_SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t count = jlSpriteBankLoad("player.spr", cels, 16, palette);
|
||||
uint16_t i;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
jlSpritePrewarm(cels[i]); // compile NOW, not mid-frame
|
||||
(void)jlSpriteCompile(cels[i]); // compile NOW, not mid-frame
|
||||
}
|
||||
----
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
`jlSpriteDraw` does *not* compile a sprite on demand. An uncompiled sprite
|
||||
draws through the interpreter every single time. Call `jlSpriteCompile` or
|
||||
`jlSpritePrewarm` during loading -- otherwise the first draw of each sprite
|
||||
either stalls a frame (if you compile lazily yourself) or never gets fast at
|
||||
all.
|
||||
draws through the interpreter every single time. Call `jlSpriteCompile` during
|
||||
loading -- otherwise the first draw of each sprite either stalls a frame (if
|
||||
you compile lazily yourself) or never gets fast at all.
|
||||
====
|
||||
|
||||
=== Part 3: The Frame Loop
|
||||
|
|
@ -100,7 +105,7 @@ while (running) {
|
|||
jlInputPoll(); // refresh keyboard, mouse, joystick
|
||||
jlAudioFrameTick(); // pump the audio engine
|
||||
|
||||
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||
if (jlKeyPressed(JL_KEY_ESCAPE)) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
|
|
@ -113,73 +118,68 @@ while (running) {
|
|||
|
||||
Four library calls, in that order, and everything else is yours.
|
||||
|
||||
`jlWaitVBL()` blocks until the next vertical blank. It is cheap on every port
|
||||
because the underlying mechanism is always a hardware wait, never a software
|
||||
timer.
|
||||
`jlWaitVBL()` blocks until the next vertical blank. On the machine ports that
|
||||
is the display's own; the hosted ports, and DVX in a window, have none, and
|
||||
wait for the next tick of a virtual frame clock instead.
|
||||
|
||||
`jlInputPoll()` must be called exactly once per frame. The edge predicates
|
||||
(`jlKeyPressed`, `jlKeyReleased`, and the mouse and joystick equivalents) are
|
||||
defined relative to the *previous poll*, so calling it twice per frame makes
|
||||
edges disappear, and not calling it means nothing ever changes.
|
||||
|
||||
`jlAudioFrameTick()` is a no-op on ports whose audio runs from a hardware
|
||||
interrupt, but it is required on ports that mix in user context, and it is what
|
||||
advances tracker music and keeps streaming sound effects fed. Call it
|
||||
unconditionally, once per frame.
|
||||
`jlAudioFrameTick()` advances tracker music and keeps streaming sound effects
|
||||
fed. Call it once per frame.
|
||||
|
||||
`jlStagePresent()` flips the dirty parts of the stage to the display. The
|
||||
drawing primitives record which rows they touched, so presenting only copies
|
||||
what actually changed. You do not need to tell it what you drew.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
If you write pixels by some route other than the library's primitives, the
|
||||
dirty tracking does not know about it, and `jlStagePresent` will skip those
|
||||
rows. That is the one case where you have to mark the region yourself.
|
||||
====
|
||||
what actually changed. You do not need to tell it what you drew: every call
|
||||
that changes the stage's pixels marks them, and the API has no other way to
|
||||
write them. Palette and SCB changes reach the display at the next present
|
||||
too.
|
||||
|
||||
==== Frame-Rate Independence
|
||||
|
||||
The targets do not share a refresh rate: 50 Hz (Amiga PAL), 55 Hz
|
||||
(X68000), 60 Hz (IIgs, ST NTSC, and SDL3's virtual rate) and 70 Hz (VGA
|
||||
mode 13h, and DVX's virtual rate). A loop
|
||||
The targets do not share a refresh rate: 50 Hz (the Amiga, and a PAL IIgs or
|
||||
ST), 55 Hz (X68000), 60 Hz (the IIgs, an NTSC ST, and every hosted port's
|
||||
virtual rate), and 70 Hz (VGA mode 13h, and DVX's virtual rate). A loop
|
||||
that moves a sprite "two pixels per frame" runs 40 percent faster on DOS than
|
||||
on a PAL Amiga.
|
||||
|
||||
Two ways to handle it. Ask the frame rate and scale your per-frame step:
|
||||
So give every speed per second, never per frame, and work out each frame how
|
||||
far that is. The millisecond clock does it best. `jlMillisElapsed()` reports
|
||||
true elapsed wall-clock time, so a frame that took a long time to render
|
||||
moves things as far as the time it took, rather than counting as one tick:
|
||||
|
||||
[source,c]
|
||||
----
|
||||
uint16_t hz = jlFrameHz(); // 50, 60 or 70
|
||||
uint32_t now = jlMillisElapsed();
|
||||
uint32_t elapsed = now - gLastMillis; // milliseconds since the last frame
|
||||
|
||||
gLastMillis = now;
|
||||
----
|
||||
|
||||
Or drive from the millisecond clock, which is the more robust choice for
|
||||
anything that must feel the same everywhere:
|
||||
A thing then moves its speed times `elapsed` / 1000. At 70 frames a second
|
||||
that is often a fraction of a pixel, which only adds up if positions keep the
|
||||
fraction, so keep them in fixed point (the skeleton below uses 1/256 of a
|
||||
pixel) and draw at the whole-pixel part.
|
||||
|
||||
[source,c]
|
||||
----
|
||||
uint32_t now = jlMillisElapsed();
|
||||
uint32_t step = now - previous;
|
||||
previous = now;
|
||||
----
|
||||
|
||||
`jlMillisElapsed()` reports true elapsed wall-clock time, so a frame that took
|
||||
a long time to render is accounted for correctly rather than counted as one
|
||||
tick.
|
||||
`jlFrameHz()` (50 to 70) is the other way: a per-frame step of speed /
|
||||
`jlFrameHz()` is right while every frame takes one refresh, and falls behind
|
||||
whenever one takes longer.
|
||||
|
||||
=== Part 4: Shut Down
|
||||
|
||||
[source,c]
|
||||
----
|
||||
jlAudioShutdown(); // if you called jlAudioInit
|
||||
jlShutdown();
|
||||
return 0;
|
||||
----
|
||||
|
||||
`jlShutdown` restores the display mode, frees the library's allocations, and
|
||||
un-hooks interrupt vectors. Sprites and surfaces you created are released with
|
||||
it, but the buffers *you* allocated (including tile data you handed to
|
||||
`jlSpriteCreate`, which is never copied) remain yours to free.
|
||||
`jlShutdown` stops audio, restores the display mode, frees the library's allocations, and
|
||||
un-hooks interrupt vectors. Sprites and surfaces you created are *not*
|
||||
released with it: destroy them with `jlSpriteDestroy` and `jlSurfaceDestroy`
|
||||
before you shut down. The buffers *you* allocated (including tile data you
|
||||
handed to `jlSpriteCreate`, which is never copied) remain yours to free.
|
||||
|
||||
=== The Complete Skeleton
|
||||
|
||||
|
|
@ -189,25 +189,33 @@ This compiles and runs on every target as-is.
|
|||
----
|
||||
// A minimal but complete JoeyLib program.
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include <joey/joey.h>
|
||||
|
||||
#define PLAYER_SPEED 2
|
||||
#define PLAYER_SIZE 16
|
||||
#define PLAYER_SPEED 120 // pixels per second, the same on every port
|
||||
#define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel
|
||||
#define MAX_STEP_MILLIS 100 // after a stall, move no further than this
|
||||
|
||||
static jlSurfaceT *gStage;
|
||||
static bool gRunning = true;
|
||||
static int16_t gPlayerX = 160;
|
||||
static int16_t gPlayerY = 100;
|
||||
static bool gRunning = true;
|
||||
static int32_t gPlayerX = 160L << SUBPIXEL_SHIFT;
|
||||
static int32_t gPlayerY = 100L << SUBPIXEL_SHIFT;
|
||||
static uint32_t gLastMillis = 0;
|
||||
|
||||
static void buildPalette(void);
|
||||
static void handleInput(void);
|
||||
static void renderFrame(void);
|
||||
static void buildPalette(void);
|
||||
static int32_t clampPosition(int32_t position, int16_t limit);
|
||||
static void handleInput(int32_t step);
|
||||
static void renderFrame(void);
|
||||
static int32_t stepForFrame(void);
|
||||
|
||||
|
||||
static void buildPalette(void) {
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
colors[i] = 0x000;
|
||||
}
|
||||
colors[1] = 0xFFF; // white
|
||||
|
|
@ -215,58 +223,77 @@ static void buildPalette(void) {
|
|||
colors[3] = 0x00C; // blue
|
||||
|
||||
jlPaletteSet(gStage, 0, colors);
|
||||
jlScbSetRange(gStage, 0, SURFACE_HEIGHT - 1, 0);
|
||||
jlScbSetRange(gStage, 0, JL_SURFACE_HEIGHT - 1, 0);
|
||||
}
|
||||
|
||||
|
||||
static void handleInput(void) {
|
||||
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||
// A position (in 1/256 of a pixel) kept between 0 and limit pixels.
|
||||
static int32_t clampPosition(int32_t position, int16_t limit) {
|
||||
int32_t max = (int32_t)limit << SUBPIXEL_SHIFT;
|
||||
|
||||
if (position < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (position > max) {
|
||||
return max;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
|
||||
// Moves the player by step (1/256 of a pixel) in each direction held.
|
||||
static void handleInput(int32_t step) {
|
||||
if (jlKeyPressed(JL_KEY_ESCAPE)) {
|
||||
gRunning = false;
|
||||
}
|
||||
if (jlKeyDown(KEY_LEFT)) {
|
||||
gPlayerX -= PLAYER_SPEED;
|
||||
if (jlKeyDown(JL_KEY_LEFT)) {
|
||||
gPlayerX -= step;
|
||||
}
|
||||
if (jlKeyDown(KEY_RIGHT)) {
|
||||
gPlayerX += PLAYER_SPEED;
|
||||
if (jlKeyDown(JL_KEY_RIGHT)) {
|
||||
gPlayerX += step;
|
||||
}
|
||||
if (jlKeyDown(KEY_UP)) {
|
||||
gPlayerY -= PLAYER_SPEED;
|
||||
if (jlKeyDown(JL_KEY_UP)) {
|
||||
gPlayerY -= step;
|
||||
}
|
||||
if (jlKeyDown(KEY_DOWN)) {
|
||||
gPlayerY += PLAYER_SPEED;
|
||||
if (jlKeyDown(JL_KEY_DOWN)) {
|
||||
gPlayerY += step;
|
||||
}
|
||||
|
||||
// A joystick, if one is attached, drives the same movement.
|
||||
if (jlJoystickConnected(JOYSTICK_0)) {
|
||||
if (jlJoystickX(JOYSTICK_0) < -64) {
|
||||
gPlayerX -= PLAYER_SPEED;
|
||||
if (jlJoystickConnected(JL_JOYSTICK_0)) {
|
||||
if (jlJoystickX(JL_JOYSTICK_0) < -64) {
|
||||
gPlayerX -= step;
|
||||
}
|
||||
if (jlJoystickX(JOYSTICK_0) > 64) {
|
||||
gPlayerX += PLAYER_SPEED;
|
||||
if (jlJoystickX(JL_JOYSTICK_0) > 64) {
|
||||
gPlayerX += step;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the player on screen: the primitives would clip safely, but
|
||||
// clamping keeps the object whole.
|
||||
if (gPlayerX < 0) {
|
||||
gPlayerX = 0;
|
||||
}
|
||||
if (gPlayerX > SURFACE_WIDTH - 16) {
|
||||
gPlayerX = SURFACE_WIDTH - 16;
|
||||
}
|
||||
if (gPlayerY < 0) {
|
||||
gPlayerY = 0;
|
||||
}
|
||||
if (gPlayerY > SURFACE_HEIGHT - 16) {
|
||||
gPlayerY = SURFACE_HEIGHT - 16;
|
||||
}
|
||||
gPlayerX = clampPosition(gPlayerX, JL_SURFACE_WIDTH - PLAYER_SIZE);
|
||||
gPlayerY = clampPosition(gPlayerY, JL_SURFACE_HEIGHT - PLAYER_SIZE);
|
||||
}
|
||||
|
||||
|
||||
static void renderFrame(void) {
|
||||
jlSurfaceClear(gStage, 0);
|
||||
jlDrawRect(gStage, 0, 0, SURFACE_WIDTH, SURFACE_HEIGHT, 1);
|
||||
jlFillRect(gStage, gPlayerX, gPlayerY, 16, 16, 2);
|
||||
jlDrawRect(gStage, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, 1);
|
||||
jlFillRect(gStage, (int16_t)(gPlayerX >> SUBPIXEL_SHIFT), (int16_t)(gPlayerY >> SUBPIXEL_SHIFT), PLAYER_SIZE, PLAYER_SIZE, 2);
|
||||
}
|
||||
|
||||
|
||||
// How far the player moves this frame, in 1/256 of a pixel: PLAYER_SPEED
|
||||
// times the time since the last frame, whatever the port's frame rate.
|
||||
static int32_t stepForFrame(void) {
|
||||
uint32_t now = jlMillisElapsed();
|
||||
uint32_t elapsed = now - gLastMillis;
|
||||
|
||||
gLastMillis = now;
|
||||
if (elapsed > MAX_STEP_MILLIS) {
|
||||
elapsed = MAX_STEP_MILLIS;
|
||||
}
|
||||
return (int32_t)((((uint32_t)PLAYER_SPEED * elapsed) << SUBPIXEL_SHIFT) / 1000u);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -274,6 +301,7 @@ int main(void) {
|
|||
jlConfigT config;
|
||||
|
||||
config.codegenBytes = 32UL * 1024;
|
||||
config.noAudio = false;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
return 1;
|
||||
|
|
@ -287,13 +315,14 @@ int main(void) {
|
|||
|
||||
buildPalette();
|
||||
jlRandomSeed(12345); // deterministic on every port
|
||||
gLastMillis = jlMillisElapsed();
|
||||
|
||||
while (gRunning) {
|
||||
jlWaitVBL();
|
||||
jlInputPoll();
|
||||
jlAudioFrameTick();
|
||||
|
||||
handleInput();
|
||||
handleInput(stepForFrame());
|
||||
renderFrame();
|
||||
|
||||
jlStagePresent();
|
||||
|
|
@ -315,7 +344,7 @@ Before drawing a sprite, capture what is underneath; next frame, put it back.
|
|||
|
||||
[source,c]
|
||||
----
|
||||
uint16_t backing[JOEY_SPRITE_BACKUP_BYTES(2, 2) / 2];
|
||||
uint16_t backing[jlSpriteBackupLength(uint16_t, 2, 2)];
|
||||
jlSpriteBackupT backup;
|
||||
|
||||
backup.bytes = (uint8_t *)backing;
|
||||
|
|
@ -326,10 +355,10 @@ jlSpriteRestoreUnder(stage, &backup); // erase last frame's sprite
|
|||
jlSpriteSaveAndDraw(stage, sprite, x, y, &backup);
|
||||
----
|
||||
|
||||
Size the buffer with the `JOEY_SPRITE_BACKUP_BYTES` macro, and declare it as
|
||||
`uint16_t` (or otherwise 2-byte aligned) -- an odd address pushes save and
|
||||
restore onto slow interpreted paths on the planar ports, because the 68000
|
||||
cannot do word accesses at odd addresses.
|
||||
Size the buffer with `jlSpriteBackupLength`, and declare it as `uint16_t` (or
|
||||
otherwise 2-byte aligned): on the Amiga, ST, and X68000 an odd address sends
|
||||
save and restore down a much slower path, and on the Amiga and ST it crashes
|
||||
the trusted variants.
|
||||
|
||||
==== The Clean-Buffer Pattern
|
||||
|
||||
|
|
@ -341,12 +370,12 @@ jlSurfaceT *clean = jlSurfaceCreate();
|
|||
// ... draw the static scene into clean, once ...
|
||||
|
||||
// each frame:
|
||||
jlSurfaceCopyRect(stage, clean, x, y, w, h); // erase EVERY window first
|
||||
jlSurfaceCopyRect(stage, clean, x, y, w, h); // erase EVERY region first
|
||||
// ... then draw ALL sprites ...
|
||||
----
|
||||
|
||||
This scales better with many sprites, and it is the right choice when sprites
|
||||
overlap. Erase every window first, then draw every sprite -- never interleave
|
||||
overlap. Erase every region first, then draw every sprite -- never interleave
|
||||
erase and draw.
|
||||
|
||||
[WARNING]
|
||||
|
|
@ -354,6 +383,6 @@ erase and draw.
|
|||
`jlSurfaceCopyRect` widens the copied region outward to the 16-pixel group grid
|
||||
on *every* port, so it can touch up to 15 pixels either side of the rectangle
|
||||
you asked for. That means it can overwrite content just outside your rectangle,
|
||||
and it silently invalidates any live `jlSpriteBackupT` whose recorded window it
|
||||
and it silently invalidates any live `jlSpriteBackupT` whose recorded region it
|
||||
touches. This is exactly why the two patterns must not be mixed in one region.
|
||||
====
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -22,10 +22,10 @@
|
|||
// keys on a timetable -- N once, S every 1.5 seconds -- for the IIgs and
|
||||
// X68000 audio gates, which read the sound chips but cannot type.
|
||||
//
|
||||
// On platforms where the audio HAL is still a stub, jlAudioInit
|
||||
// returns false, every audio call is a quiet no-op, and the demo runs
|
||||
// as a silent input loop -- you can confirm the build links and the
|
||||
// input plumbing still works without hearing anything.
|
||||
// On a machine whose sound does not start (jlInit logs it), every audio
|
||||
// call is a quiet no-op and the demo runs as a silent input loop -- you
|
||||
// can confirm the build links and the input plumbing still works without
|
||||
// hearing anything.
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
|
||||
#define BAR_X 16
|
||||
#define BAR_Y 88
|
||||
#define BAR_W (SURFACE_WIDTH - 32)
|
||||
#define BAR_W (JL_SURFACE_WIDTH - 32)
|
||||
#define BAR_H 16
|
||||
|
||||
// The streamed tone's state, which streamFill advances.
|
||||
|
|
@ -113,7 +113,7 @@ typedef struct {
|
|||
|
||||
|
||||
static void buildPalette(jlSurfaceT *screen);
|
||||
static void initialPaint(jlSurfaceT *screen, bool audioOk);
|
||||
static void initialPaint(jlSurfaceT *screen);
|
||||
static bool keyPressed(jlKeyE key, uint16_t frame);
|
||||
static bool loadFile(const char *name, uint32_t maxLen, uint8_t **outBytes, uint32_t *outLen);
|
||||
static bool noiseStep(uint16_t sweepFrame);
|
||||
|
|
@ -129,10 +129,10 @@ static uint8_t gStreamTake = 0;
|
|||
|
||||
|
||||
static void buildPalette(jlSurfaceT *screen) {
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t i;
|
||||
|
||||
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
colors[i] = 0x0000;
|
||||
}
|
||||
colors[COLOR_BG] = 0x0000;
|
||||
|
|
@ -146,10 +146,9 @@ static void buildPalette(jlSurfaceT *screen) {
|
|||
// Visual feedback for "audio is running, but you cannot tell on a
|
||||
// stub HAL": pulse a horizontal bar between the hint color and the
|
||||
// active color whenever a sound has fired this frame.
|
||||
static void initialPaint(jlSurfaceT *screen, bool audioOk) {
|
||||
static void initialPaint(jlSurfaceT *screen) {
|
||||
jlSurfaceClear(screen, COLOR_BG);
|
||||
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H,
|
||||
audioOk ? COLOR_HINT : COLOR_BG);
|
||||
jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT);
|
||||
jlStagePresent();
|
||||
}
|
||||
|
||||
|
|
@ -226,17 +225,17 @@ static bool noiseStep(uint16_t sweepFrame) {
|
|||
// so a gate finds the noise sweep and a stream running.
|
||||
static jlKeyE probeKey(uint16_t frame) {
|
||||
if (frame == PROBE_NOISE_FRAME) {
|
||||
return KEY_N;
|
||||
return JL_KEY_N;
|
||||
}
|
||||
if (frame % PROBE_STREAM_FRAMES == PROBE_STREAM_FIRST) {
|
||||
return KEY_S;
|
||||
return JL_KEY_S;
|
||||
}
|
||||
return KEY_NONE;
|
||||
return JL_KEY_NONE;
|
||||
}
|
||||
#else
|
||||
static jlKeyE probeKey(uint16_t frame) {
|
||||
(void)frame;
|
||||
return KEY_NONE;
|
||||
return JL_KEY_NONE;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
@ -306,7 +305,6 @@ static void streamStart(void) {
|
|||
int main(void) {
|
||||
jlConfigT config;
|
||||
jlSurfaceT *screen;
|
||||
bool audioOk;
|
||||
bool musicPaused;
|
||||
bool noiseOn;
|
||||
bool noiseResumes;
|
||||
|
|
@ -319,6 +317,7 @@ int main(void) {
|
|||
uint32_t sfxLen;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.noAudio = false;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
|
|
@ -337,19 +336,16 @@ int main(void) {
|
|||
modLen = 0;
|
||||
sfxLen = 0;
|
||||
|
||||
audioOk = jlAudioInit();
|
||||
if (audioOk) {
|
||||
// Keep the MOD bytes resident so M can (re)start it later;
|
||||
// jlAudioPlayMod copies them at play time.
|
||||
(void)loadFile(TEST_MOD_NAME, DATA_FILE_MAX, &modBytes, &modLen);
|
||||
(void)loadFile(TEST_SFX_NAME, DATA_FILE_MAX, &sfxBytes, &sfxLen);
|
||||
gRawFormat = jlAudioSfxRawFormat();
|
||||
}
|
||||
// Keep the MOD bytes resident so M can (re)start it later;
|
||||
// jlAudioPlayMod copies them at play time.
|
||||
(void)loadFile(TEST_MOD_NAME, DATA_FILE_MAX, &modBytes, &modLen);
|
||||
(void)loadFile(TEST_SFX_NAME, DATA_FILE_MAX, &sfxBytes, &sfxLen);
|
||||
gRawFormat = jlAudioSfxRawFormat();
|
||||
jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true);
|
||||
|
||||
buildPalette(screen);
|
||||
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||
initialPaint(screen, audioOk);
|
||||
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
|
||||
initialPaint(screen);
|
||||
|
||||
musicPaused = false;
|
||||
noiseOn = false;
|
||||
|
|
@ -360,18 +356,18 @@ int main(void) {
|
|||
jlWaitVBL();
|
||||
jlInputPoll();
|
||||
jlAudioFrameTick();
|
||||
if (keyPressed(KEY_ESCAPE, frame)) {
|
||||
if (keyPressed(JL_KEY_ESCAPE, frame)) {
|
||||
break;
|
||||
}
|
||||
if (keyPressed(KEY_SPACE, frame) && sfxBytes != NULL) {
|
||||
if (keyPressed(JL_KEY_SPACE, frame) && sfxBytes != NULL) {
|
||||
jlAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ);
|
||||
flashFrames = FLASH_FRAMES;
|
||||
}
|
||||
if (keyPressed(KEY_S, frame) && audioOk) {
|
||||
if (keyPressed(JL_KEY_S, frame)) {
|
||||
streamStart();
|
||||
flashFrames = FLASH_FRAMES;
|
||||
}
|
||||
if (keyPressed(KEY_N, frame) && !noiseOn) {
|
||||
if (keyPressed(JL_KEY_N, frame) && !noiseOn) {
|
||||
// The song's own drums would fight the sweep for the noise
|
||||
// generator, so it pauses and picks up again afterwards.
|
||||
noiseResumes = jlMusicIsPlaying() && !musicPaused;
|
||||
|
|
@ -379,15 +375,15 @@ int main(void) {
|
|||
noiseOn = true;
|
||||
noiseFrame = 0;
|
||||
}
|
||||
if (keyPressed(KEY_M, frame) && modBytes != NULL) {
|
||||
if (keyPressed(JL_KEY_M, frame) && modBytes != NULL) {
|
||||
jlMusicStop();
|
||||
jlAudioPlayMod(modBytes, modLen, true);
|
||||
}
|
||||
if (keyPressed(KEY_T, frame)) {
|
||||
if (keyPressed(JL_KEY_T, frame)) {
|
||||
jlAudioStopMod();
|
||||
jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true);
|
||||
}
|
||||
if (keyPressed(KEY_P, frame)) {
|
||||
if (keyPressed(JL_KEY_P, frame)) {
|
||||
if (musicPaused) {
|
||||
jlMusicResume();
|
||||
} else {
|
||||
|
|
@ -416,10 +412,7 @@ int main(void) {
|
|||
}
|
||||
|
||||
jlMusicStop();
|
||||
if (audioOk) {
|
||||
jlAudioStopMod();
|
||||
}
|
||||
jlAudioShutdown();
|
||||
jlAudioStopMod();
|
||||
if (modBytes != NULL) {
|
||||
free(modBytes);
|
||||
}
|
||||
|
|
|
|||
401
examples/audpar/audpar.c
Normal file
401
examples/audpar/audpar.c
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
// See LICENSE.md for license information.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Audio conformance probe (AUDPAR, a gate's program, not an example): one
|
||||
// fixed timeline of what every port's audio must do alike, each step logged
|
||||
// as it starts, so scripts/verify-audio-conformance.sh can record what a
|
||||
// port played and tools/gradeAudioConformance.py can measure it against the
|
||||
// API's contract: tone and voice pitch, 2 dB attenuation steps, every voice,
|
||||
// the noise generator's rate and steps, sound effects at their rates and
|
||||
// pans, a module that does not loop ending (and jlAudioIsPlayingMod saying
|
||||
// so), and tick callbacks at their rates.
|
||||
//
|
||||
// The log, one line per event, "AUDPAR <ms> <event> <values>", ms counted
|
||||
// from the first sync tone's start:
|
||||
// sync <hz> a sync tone: the timeline starts and ends
|
||||
// with one, so a recording can be aligned
|
||||
// tone <hz> jlAudioTone
|
||||
// voice <v> <hz> <atten> jlAudioVoice
|
||||
// noise <pitch> <atten> jlAudioNoise
|
||||
// sfx <slot> <rate> <hz> <pan> a one-shot sine sounding <hz> at <rate>
|
||||
// mod start / mod end jlAudioPlayMod with loop false, and the
|
||||
// first jlAudioIsPlayingMod false after it
|
||||
// tick <hz> <ok> <calls> <ms> jlAudioTickRegister's result, the calls
|
||||
// that came, and the time they had
|
||||
// silence everything stopped
|
||||
// done
|
||||
// Escape ends it early.
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <joey/joey.h>
|
||||
#include <joey/file.h>
|
||||
|
||||
#if defined(JOEYLIB_PLATFORM_IIGS)
|
||||
# define TEST_MOD_NAME "TEST.NTP"
|
||||
#else
|
||||
# define TEST_MOD_NAME "test.mod"
|
||||
#endif
|
||||
#define DATA_FILE_MAX (64UL * 1024UL)
|
||||
|
||||
#define SYNC_HZ 1000u
|
||||
#define SYNC_MS 300u
|
||||
#define GAP_MS 100u
|
||||
#define PAUSE_MS 200u
|
||||
#define ATTEN_STEPS 15u // 0..14 audible, 15 silent
|
||||
#define ATTEN_SILENT 15u
|
||||
#define TONE_MS 400u
|
||||
#define STEP_MS 300u
|
||||
#define NOISE_MS 400u
|
||||
#define NOISE_STEP_MS 200u
|
||||
#define NOISE_STEP_PITCH 8u
|
||||
#define LEVEL_HZ 440u
|
||||
#define VOICE_HZ 660u
|
||||
#define TICK_MS 2000u
|
||||
#define MOD_WAIT_MS 12000u
|
||||
#define SFX_SLOT 0u
|
||||
#define SFX_SAMPLES 4000u
|
||||
#define SFX_PERIOD 8u // samples a cycle: the tone is rate / 8
|
||||
#define SFX_AMPLITUDE 100
|
||||
#define MS_PER_SECOND 1000u
|
||||
#define EVENTS_MAX 128u
|
||||
|
||||
typedef enum {
|
||||
STEP_SYNC,
|
||||
STEP_TONE,
|
||||
STEP_VOICE,
|
||||
STEP_NOISE,
|
||||
STEP_SFX,
|
||||
STEP_MOD,
|
||||
STEP_TICK,
|
||||
STEP_SILENCE
|
||||
} StepKindE;
|
||||
|
||||
typedef struct {
|
||||
StepKindE kind;
|
||||
uint8_t channel; // voice, or the sfx pan
|
||||
uint16_t value; // Hz, noise pitch, or sfx rate
|
||||
uint8_t level; // attenuation
|
||||
uint16_t ms; // how long it sounds (sfx: from its length)
|
||||
} StepT;
|
||||
|
||||
// What the log will say of one step, kept in memory until the timeline is
|
||||
// over: a log line can take a disk write (a fifth of a second on the ST),
|
||||
// and written while it plays it would stall the program's sound.
|
||||
typedef struct {
|
||||
uint32_t ms;
|
||||
StepKindE kind;
|
||||
uint16_t value;
|
||||
uint8_t channel;
|
||||
uint8_t level;
|
||||
uint32_t count; // mod: its length, then playing; tick: calls
|
||||
uint32_t span; // tick: the time the calls had
|
||||
bool ok; // tick: registered; mod end: still playing
|
||||
bool modEnd;
|
||||
} EventT;
|
||||
|
||||
static EventT *eventAdd(uint32_t ms, const StepT *step);
|
||||
static void eventsLog(void);
|
||||
static void loadMod(uint8_t **bytes, uint32_t *length);
|
||||
static bool pump(void);
|
||||
static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength);
|
||||
static uint32_t since(void);
|
||||
static void silenceAll(void);
|
||||
static void tickCount(void);
|
||||
static bool waitMs(uint16_t ms);
|
||||
|
||||
static const StepT kTimeline[] = {
|
||||
{ STEP_SYNC, 0u, SYNC_HZ, 0u, SYNC_MS },
|
||||
{ STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
|
||||
// jlAudioTone across the octaves every port can play.
|
||||
{ STEP_TONE, 0u, 110u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_TONE, 0u, 220u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_TONE, 0u, 440u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_TONE, 0u, 880u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_TONE, 0u, 1760u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_TONE, 0u, 3136u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
|
||||
// Voice 0's attenuation, one step after another: 2 dB apart.
|
||||
{ STEP_VOICE, 0u, LEVEL_HZ, 0u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 1u, STEP_MS },
|
||||
{ STEP_VOICE, 0u, LEVEL_HZ, 2u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 3u, STEP_MS },
|
||||
{ STEP_VOICE, 0u, LEVEL_HZ, 4u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 5u, STEP_MS },
|
||||
{ STEP_VOICE, 0u, LEVEL_HZ, 6u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 7u, STEP_MS },
|
||||
{ STEP_VOICE, 0u, LEVEL_HZ, 8u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 9u, STEP_MS },
|
||||
{ STEP_VOICE, 0u, LEVEL_HZ, 10u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 11u, STEP_MS },
|
||||
{ STEP_VOICE, 0u, LEVEL_HZ, 12u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 13u, STEP_MS },
|
||||
{ STEP_VOICE, 0u, LEVEL_HZ, 14u, STEP_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
|
||||
// The other two voices.
|
||||
{ STEP_VOICE, 1u, VOICE_HZ, 0u, STEP_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_VOICE, 2u, VOICE_HZ, 0u, STEP_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
|
||||
// The noise generator's rate, bright to dark.
|
||||
{ STEP_NOISE, 0u, 1u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_NOISE, 0u, 2u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_NOISE, 0u, 4u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_NOISE, 0u, 8u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_NOISE, 0u, 16u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_NOISE, 0u, 31u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
|
||||
// The noise's attenuation steps.
|
||||
{ STEP_NOISE, 0u, NOISE_STEP_PITCH, 0u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 2u, NOISE_STEP_MS },
|
||||
{ STEP_NOISE, 0u, NOISE_STEP_PITCH, 4u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 6u, NOISE_STEP_MS },
|
||||
{ STEP_NOISE, 0u, NOISE_STEP_PITCH, 8u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 10u, NOISE_STEP_MS },
|
||||
{ STEP_NOISE, 0u, NOISE_STEP_PITCH, 12u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 14u, NOISE_STEP_MS },
|
||||
{ STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
|
||||
// Sound effects at three rates, then panned each way.
|
||||
{ STEP_SFX, JL_AUDIO_PAN_CENTER, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_SFX, JL_AUDIO_PAN_CENTER, 16000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_SFX, JL_AUDIO_PAN_CENTER, 22050u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_SFX, JL_AUDIO_PAN_LEFT, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
|
||||
{ STEP_SFX, JL_AUDIO_PAN_RIGHT, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
|
||||
// A module that does not loop, to its end.
|
||||
{ STEP_MOD, 0u, 0u, 0u, MOD_WAIT_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
|
||||
// Tick callbacks, slow and at a frame's rate.
|
||||
{ STEP_TICK, 0u, 10u, 0u, TICK_MS },
|
||||
{ STEP_TICK, 0u, 60u, 0u, TICK_MS },
|
||||
{ STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
|
||||
{ STEP_SYNC, 0u, SYNC_HZ, 0u, SYNC_MS },
|
||||
{ STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }
|
||||
};
|
||||
|
||||
// One cycle of a sine, SFX_PERIOD samples, so a sound of it is rate / 8 Hz.
|
||||
static const int8_t kSineCycle[SFX_PERIOD] = { 0, 71, SFX_AMPLITUDE, 71, 0, -71, -SFX_AMPLITUDE, -71 };
|
||||
|
||||
static EventT gEvents[EVENTS_MAX];
|
||||
static uint16_t gEventCount = 0u;
|
||||
static int8_t gSine[SFX_SAMPLES];
|
||||
static uint32_t gStartMs = 0u;
|
||||
static volatile uint32_t gTickCalls = 0u;
|
||||
|
||||
|
||||
// A new event for step at ms; NULL once EVENTS_MAX are taken.
|
||||
static EventT *eventAdd(uint32_t ms, const StepT *step) {
|
||||
EventT *event;
|
||||
|
||||
if (gEventCount >= EVENTS_MAX) {
|
||||
return NULL;
|
||||
}
|
||||
event = &gEvents[gEventCount++];
|
||||
event->ms = ms;
|
||||
event->kind = step->kind;
|
||||
event->value = step->value;
|
||||
event->channel = step->channel;
|
||||
event->level = step->level;
|
||||
event->count = 0u;
|
||||
event->span = 0u;
|
||||
event->ok = false;
|
||||
event->modEnd = false;
|
||||
return event;
|
||||
}
|
||||
|
||||
|
||||
// The log, once the timeline is over (the format in the header).
|
||||
static void eventsLog(void) {
|
||||
const EventT *event;
|
||||
uint16_t i;
|
||||
|
||||
for (i = 0u; i < gEventCount; i++) {
|
||||
event = &gEvents[i];
|
||||
switch (event->kind) {
|
||||
case STEP_SYNC:
|
||||
jlLogF("AUDPAR %lu sync %u", (unsigned long)event->ms, (unsigned)event->value);
|
||||
break;
|
||||
case STEP_TONE:
|
||||
jlLogF("AUDPAR %lu tone %u", (unsigned long)event->ms, (unsigned)event->value);
|
||||
break;
|
||||
case STEP_VOICE:
|
||||
jlLogF("AUDPAR %lu voice %u %u %u", (unsigned long)event->ms, (unsigned)event->channel, (unsigned)event->value, (unsigned)event->level);
|
||||
break;
|
||||
case STEP_NOISE:
|
||||
jlLogF("AUDPAR %lu noise %u %u", (unsigned long)event->ms, (unsigned)event->value, (unsigned)event->level);
|
||||
break;
|
||||
case STEP_SFX:
|
||||
jlLogF("AUDPAR %lu sfx %u %u %u %u", (unsigned long)event->ms, (unsigned)SFX_SLOT, (unsigned)event->value, (unsigned)(event->value / SFX_PERIOD), (unsigned)event->channel);
|
||||
break;
|
||||
case STEP_MOD:
|
||||
if (event->modEnd) {
|
||||
jlLogF("AUDPAR %lu mod end %u", (unsigned long)event->ms, (unsigned)event->ok);
|
||||
} else {
|
||||
jlLogF("AUDPAR %lu mod start %lu", (unsigned long)event->ms, (unsigned long)event->count);
|
||||
}
|
||||
break;
|
||||
case STEP_TICK:
|
||||
jlLogF("AUDPAR %lu tick %u %u %lu %lu", (unsigned long)event->ms, (unsigned)event->value, (unsigned)event->ok, (unsigned long)event->count, (unsigned long)event->span);
|
||||
break;
|
||||
case STEP_SILENCE:
|
||||
jlLogF("AUDPAR %lu silence", (unsigned long)event->ms);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// DATA's module, whole; none (NULL, 0) when it is missing or too big.
|
||||
static void loadMod(uint8_t **bytes, uint32_t *length) {
|
||||
FILE *fp;
|
||||
long size;
|
||||
|
||||
*bytes = NULL;
|
||||
*length = 0u;
|
||||
fp = jlDataOpen(TEST_MOD_NAME, "rb");
|
||||
if (fp == NULL) {
|
||||
return;
|
||||
}
|
||||
size = -1L;
|
||||
if (fseek(fp, 0L, SEEK_END) == 0) {
|
||||
size = ftell(fp);
|
||||
}
|
||||
if (size > 0L && (uint32_t)size <= DATA_FILE_MAX && fseek(fp, 0L, SEEK_SET) == 0) {
|
||||
*bytes = (uint8_t *)malloc((size_t)size);
|
||||
if (*bytes != NULL && fread(*bytes, 1, (size_t)size, fp) == (size_t)size) {
|
||||
*length = (uint32_t)size;
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
|
||||
// One frame: the engine's pump, the display's pace, and input. False when
|
||||
// Escape asks to stop.
|
||||
static bool pump(void) {
|
||||
jlAudioFrameTick();
|
||||
jlWaitVBL();
|
||||
jlInputPoll();
|
||||
return !jlKeyPressed(JL_KEY_ESCAPE);
|
||||
}
|
||||
|
||||
|
||||
// Each step's time is taken as it starts.
|
||||
static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength) {
|
||||
uint32_t start;
|
||||
EventT *event;
|
||||
|
||||
start = since();
|
||||
event = eventAdd(start, step);
|
||||
switch (step->kind) {
|
||||
case STEP_SYNC:
|
||||
case STEP_TONE:
|
||||
jlAudioTone(step->value);
|
||||
(void)waitMs(step->ms);
|
||||
break;
|
||||
case STEP_VOICE:
|
||||
jlAudioVoice(step->channel, step->value, step->level);
|
||||
(void)waitMs(step->ms);
|
||||
break;
|
||||
case STEP_NOISE:
|
||||
jlAudioNoise((uint8_t)step->value, step->level);
|
||||
(void)waitMs(step->ms);
|
||||
break;
|
||||
case STEP_SFX:
|
||||
jlAudioSetPan(SFX_SLOT, (jlAudioPanE)step->channel);
|
||||
jlAudioPlaySfx(SFX_SLOT, (const uint8_t *)gSine, SFX_SAMPLES, step->value);
|
||||
(void)waitMs((uint16_t)((uint32_t)SFX_SAMPLES * MS_PER_SECOND / step->value));
|
||||
break;
|
||||
case STEP_MOD:
|
||||
if (mod != NULL) {
|
||||
jlAudioPlayMod(mod, modLength, false);
|
||||
}
|
||||
if (event != NULL) {
|
||||
event->count = modLength;
|
||||
}
|
||||
while (jlAudioIsPlayingMod() && since() - start < step->ms) {
|
||||
if (!pump()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
event = eventAdd(since(), step);
|
||||
if (event != NULL) {
|
||||
event->modEnd = true;
|
||||
event->ok = jlAudioIsPlayingMod();
|
||||
}
|
||||
jlAudioStopMod();
|
||||
break;
|
||||
case STEP_TICK:
|
||||
gTickCalls = 0u;
|
||||
if (event != NULL) {
|
||||
event->ok = jlAudioTickRegister(tickCount, step->value);
|
||||
}
|
||||
(void)waitMs(step->ms);
|
||||
(void)jlAudioTickRegister(NULL, 0u);
|
||||
if (event != NULL) {
|
||||
event->count = gTickCalls;
|
||||
event->span = since() - start;
|
||||
}
|
||||
break;
|
||||
case STEP_SILENCE:
|
||||
silenceAll();
|
||||
(void)waitMs(step->ms);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static uint32_t since(void) {
|
||||
return jlMillisElapsed() - gStartMs;
|
||||
}
|
||||
|
||||
|
||||
static void silenceAll(void) {
|
||||
uint8_t voice;
|
||||
|
||||
for (voice = 0u; voice < JL_AUDIO_VOICES; voice++) {
|
||||
jlAudioVoice(voice, 0u, ATTEN_SILENT);
|
||||
}
|
||||
jlAudioNoise(0u, ATTEN_SILENT);
|
||||
jlAudioStopSfx(SFX_SLOT);
|
||||
}
|
||||
|
||||
|
||||
static void tickCount(void) {
|
||||
gTickCalls++;
|
||||
}
|
||||
|
||||
|
||||
static bool waitMs(uint16_t ms) {
|
||||
uint32_t start;
|
||||
|
||||
start = since();
|
||||
while (since() - start < ms) {
|
||||
if (!pump()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
int main(void) {
|
||||
jlConfigT config;
|
||||
uint8_t *mod;
|
||||
uint32_t modLength;
|
||||
uint16_t i;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.noAudio = false;
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
return 1;
|
||||
}
|
||||
for (i = 0u; i < SFX_SAMPLES; i++) {
|
||||
gSine[i] = kSineCycle[i % SFX_PERIOD];
|
||||
}
|
||||
loadMod(&mod, &modLength);
|
||||
jlSurfaceClear(jlStageGet(), 0u);
|
||||
jlStagePresent();
|
||||
|
||||
gStartMs = jlMillisElapsed();
|
||||
for (i = 0u; i < (uint16_t)(sizeof(kTimeline) / sizeof(kTimeline[0])); i++) {
|
||||
runStep(&kTimeline[i], mod, modLength);
|
||||
if (jlKeyDown(JL_KEY_ESCAPE)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
silenceAll();
|
||||
eventsLog();
|
||||
jlLogF("AUDPAR %lu done", (unsigned long)since());
|
||||
jlLogFlush();
|
||||
free(mod);
|
||||
jlShutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
#include <joey/joey.h>
|
||||
|
||||
#define BAND_COUNT 8
|
||||
#define BAND_HEIGHT (SURFACE_HEIGHT / BAND_COUNT)
|
||||
#define BAND_HEIGHT (JL_SURFACE_HEIGHT / BAND_COUNT)
|
||||
#define STRIPE_COUNT 16
|
||||
#define STRIPE_WIDTH (SURFACE_WIDTH / STRIPE_COUNT)
|
||||
#define STRIPE_WIDTH (JL_SURFACE_WIDTH / STRIPE_COUNT)
|
||||
|
||||
static void buildPalettes(jlSurfaceT *screen);
|
||||
static void buildScbs(jlSurfaceT *screen);
|
||||
|
|
@ -27,7 +27,7 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn);
|
|||
|
||||
|
||||
static void buildPalettes(jlSurfaceT *screen) {
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE];
|
||||
|
||||
// Palette 0: grayscale
|
||||
makeGradient(colors, 1, 1, 1);
|
||||
|
|
@ -83,7 +83,7 @@ static void drawStripes(jlSurfaceT *screen) {
|
|||
jlSurfaceClear(screen, 0);
|
||||
for (colorIndex = 0; colorIndex < STRIPE_COUNT; colorIndex++) {
|
||||
x = (int16_t)(colorIndex * STRIPE_WIDTH);
|
||||
jlFillRect(screen, x, 0, STRIPE_WIDTH, SURFACE_HEIGHT, colorIndex);
|
||||
jlFillRect(screen, x, 0, STRIPE_WIDTH, JL_SURFACE_HEIGHT, colorIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn) {
|
|||
uint8_t g;
|
||||
uint8_t b;
|
||||
|
||||
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
r = (uint8_t)(redOn ? i : 0);
|
||||
g = (uint8_t)(greenOn ? i : 0);
|
||||
b = (uint8_t)(blueOn ? i : 0);
|
||||
|
|
@ -106,10 +106,11 @@ static void makeGradient(uint16_t *out16, int redOn, int greenOn, int blueOn) {
|
|||
|
||||
|
||||
int main(void) {
|
||||
jlConfigT config;
|
||||
jlSurfaceT *screen;
|
||||
jlConfigT config;
|
||||
jlSurfaceT *screen;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.noAudio = true;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
// per source line) and converted at startup into the 2x2 tile layout
|
||||
// jlSpriteT expects. jlSpriteDraw handles transparent-color-0; we use
|
||||
// jlSpriteSaveUnder/RestoreUnder to undraw the previous frame's ball
|
||||
// without redrawing the whole screen.
|
||||
// without redrawing the whole screen. The ball's speed is given per
|
||||
// second and each frame moves it by the time since the last one
|
||||
// (jlMillisElapsed), so it crosses the screen as fast on every port.
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
|
|
@ -18,17 +20,20 @@
|
|||
#define BALL_TILES_X (BALL_W / 8)
|
||||
#define BALL_TILES_Y (BALL_H / 8)
|
||||
|
||||
#define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * TILE_BYTES)
|
||||
// SaveUnder rounds x down to the platform's storage alignment: 2 px
|
||||
// for chunky 4bpp (1 extra byte/row worst case), 8 px for planar
|
||||
// 4-plane (4 extra bytes/row worst case -- one per plane). The +4
|
||||
// covers the planar case and is a no-op overhead on chunky.
|
||||
// Sized by the library-wide worst case (16-px-group window on the
|
||||
// planar ports); see JOEY_SPRITE_BACKUP_BYTES in joey/sprite.h.
|
||||
#define BALL_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(BALL_W / 8, BALL_H / 8)
|
||||
#define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * JL_TILE_BYTES)
|
||||
// The save-under buffer is sized by jlSpriteBackupLength (joey/sprite.h):
|
||||
// the worst case at any x, on every port.
|
||||
|
||||
#define BALL_PALETTE_IDX 0
|
||||
|
||||
#define BALL_SPEED_X 120 // pixels per second, the same on every port
|
||||
#define BALL_SPEED_Y 60
|
||||
#define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel
|
||||
#define MAX_STEP_MILLIS 100 // after a stall, move no further than this
|
||||
#define MILLIS_PER_SECOND 1000
|
||||
#define BALL_X_MAX ((int32_t)(JL_SURFACE_WIDTH - BALL_W) << SUBPIXEL_SHIFT)
|
||||
#define BALL_Y_MAX ((int32_t)(JL_SURFACE_HEIGHT - BALL_H) << SUBPIXEL_SHIFT)
|
||||
|
||||
#define COLOR_BG 0
|
||||
|
||||
// Authored layout: 16 pixels wide x 16 rows = 8 bytes/row x 16 rows.
|
||||
|
|
@ -56,7 +61,7 @@ static const uint8_t gBallAuthored[BALL_H * (BALL_W / 2)] = {
|
|||
};
|
||||
|
||||
static uint8_t gBallTiles[BALL_TILE_BYTES];
|
||||
static uint8_t gBallBackup[BALL_BACKUP_BYTES] __attribute__((aligned(2)));
|
||||
static uint16_t gBallBackup[jlSpriteBackupLength(uint16_t, BALL_W / 8, BALL_H / 8)];
|
||||
|
||||
|
||||
// Repack from authored "wide row" layout to tile-major jlSpriteT
|
||||
|
|
@ -71,12 +76,10 @@ static void repackBallTiles(void) {
|
|||
|
||||
for (ty = 0; ty < BALL_TILES_Y; ty++) {
|
||||
for (tx = 0; tx < BALL_TILES_X; tx++) {
|
||||
dst = &gBallTiles[(ty * BALL_TILES_X + tx) * TILE_BYTES];
|
||||
dst = &gBallTiles[(ty * BALL_TILES_X + tx) * JL_TILE_BYTES];
|
||||
for (row = 0; row < 8; row++) {
|
||||
for (b = 0; b < 4; b++) {
|
||||
dst[row * 4 + b] =
|
||||
gBallAuthored[((ty * 8) + row) * (BALL_W / 2) +
|
||||
(tx * 4) + b];
|
||||
dst[row * 4 + b] = gBallAuthored[((ty * 8) + row) * (BALL_W / 2) + (tx * 4) + b];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -98,21 +101,24 @@ static void buildPalette(jlSurfaceT *screen) {
|
|||
|
||||
|
||||
int main(void) {
|
||||
jlConfigT config;
|
||||
jlConfigT config;
|
||||
jlSurfaceT *screen;
|
||||
jlSpriteT *ball;
|
||||
jlSpriteBackupT backup;
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t vx;
|
||||
int16_t vy;
|
||||
bool haveBackup;
|
||||
int32_t x; // 1/256 of a pixel
|
||||
int32_t y;
|
||||
int32_t vx; // pixels per second
|
||||
int32_t vy;
|
||||
uint32_t lastMillis;
|
||||
uint32_t now;
|
||||
uint32_t elapsed;
|
||||
|
||||
/* Amiga planar emits 8 pre-shifted DRAW variants per sprite (one
|
||||
* per x % 8 alignment) so the codegen arena needs roughly 8x what
|
||||
* the chunky two-shift case asks for. 32 KB fits a 16x16 ball
|
||||
* with all variants. */
|
||||
config.codegenBytes = 32UL * 1024;
|
||||
config.noAudio = true;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
|
|
@ -140,25 +146,24 @@ int main(void) {
|
|||
(void)jlSpriteCompile(ball);
|
||||
|
||||
buildPalette(screen);
|
||||
jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, BALL_PALETTE_IDX);
|
||||
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, BALL_PALETTE_IDX);
|
||||
jlSurfaceClear(screen, COLOR_BG);
|
||||
jlStagePresent();
|
||||
|
||||
backup.bytes = gBallBackup;
|
||||
backup.bytes = (uint8_t *)gBallBackup;
|
||||
|
||||
x = 40;
|
||||
y = 30;
|
||||
vx = 2;
|
||||
vy = 1;
|
||||
haveBackup = false;
|
||||
x = (int32_t)40 << SUBPIXEL_SHIFT;
|
||||
y = (int32_t)30 << SUBPIXEL_SHIFT;
|
||||
vx = BALL_SPEED_X;
|
||||
vy = BALL_SPEED_Y;
|
||||
|
||||
jlSpriteSaveAndDraw(screen, ball, x, y, &backup);
|
||||
jlSpriteSaveAndDraw(screen, ball, (int16_t)(x >> SUBPIXEL_SHIFT), (int16_t)(y >> SUBPIXEL_SHIFT), &backup);
|
||||
jlStagePresent();
|
||||
haveBackup = true;
|
||||
lastMillis = jlMillisElapsed();
|
||||
|
||||
for (;;) {
|
||||
jlInputPoll();
|
||||
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||
if (jlKeyPressed(JL_KEY_ESCAPE)) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -170,22 +175,41 @@ int main(void) {
|
|||
// chunky targets like IIgs SHR; on planar c2p platforms it
|
||||
// also avoids c2p racing the raster). VBL wait is omitted
|
||||
// here so the demo runs at the sprite pipeline's native
|
||||
// throughput -- expect tearing on the ball.
|
||||
if (haveBackup) {
|
||||
jlSpriteRestoreUnder(screen, &backup);
|
||||
// throughput -- expect tearing on the ball. The ball's speed
|
||||
// does not depend on it: it moves by the time each frame took.
|
||||
jlSpriteRestoreUnder(screen, &backup);
|
||||
|
||||
now = jlMillisElapsed();
|
||||
elapsed = now - lastMillis;
|
||||
lastMillis = now;
|
||||
if (elapsed > MAX_STEP_MILLIS) {
|
||||
elapsed = MAX_STEP_MILLIS;
|
||||
}
|
||||
x += (vx * (int32_t)elapsed * ((int32_t)1 << SUBPIXEL_SHIFT)) / MILLIS_PER_SECOND;
|
||||
y += (vy * (int32_t)elapsed * ((int32_t)1 << SUBPIXEL_SHIFT)) / MILLIS_PER_SECOND;
|
||||
|
||||
// A wall sets the direction rather than flipping it, so a frame
|
||||
// that moved nothing cannot turn the ball back twice.
|
||||
if (x <= 0) {
|
||||
x = 0;
|
||||
vx = BALL_SPEED_X;
|
||||
}
|
||||
if (x >= BALL_X_MAX) {
|
||||
x = BALL_X_MAX;
|
||||
vx = -BALL_SPEED_X;
|
||||
}
|
||||
if (y <= 0) {
|
||||
y = 0;
|
||||
vy = BALL_SPEED_Y;
|
||||
}
|
||||
if (y >= BALL_Y_MAX) {
|
||||
y = BALL_Y_MAX;
|
||||
vy = -BALL_SPEED_Y;
|
||||
}
|
||||
|
||||
x = (int16_t)(x + vx);
|
||||
y = (int16_t)(y + vy);
|
||||
if (x <= 0) { x = 0; vx = (int16_t)-vx; }
|
||||
if (x >= SURFACE_WIDTH - BALL_W) { x = SURFACE_WIDTH - BALL_W; vx = (int16_t)-vx; }
|
||||
if (y <= 0) { y = 0; vy = (int16_t)-vy; }
|
||||
if (y >= SURFACE_HEIGHT - BALL_H) { y = SURFACE_HEIGHT - BALL_H; vy = (int16_t)-vy; }
|
||||
|
||||
jlSpriteSaveAndDraw(screen, ball, x, y, &backup);
|
||||
jlSpriteSaveAndDraw(screen, ball, (int16_t)(x >> SUBPIXEL_SHIFT), (int16_t)(y >> SUBPIXEL_SHIFT), &backup);
|
||||
|
||||
jlStagePresent();
|
||||
haveBackup = true;
|
||||
}
|
||||
|
||||
jlSpriteDestroy(ball);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
229
examples/wblaunch/wblaunch.c
Normal file
229
examples/wblaunch/wblaunch.c
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
// See LICENSE.md for license information.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Workbench probe (WBLAUNCH, a gate's program, not an example, and the
|
||||
// Amiga's alone): a program's icon read and drawn by the ROM's icon.library,
|
||||
// and the program started from it the way Workbench starts a tool.
|
||||
//
|
||||
// Wblaunch FOLDER NAME
|
||||
//
|
||||
// GetDiskObject reads FOLDER/NAME.info; its type, stack and image are
|
||||
// logged, and the image is drawn at ICON_X,ICON_Y on a hires screen of
|
||||
// Workbench's four colours, with the mouse pointer hidden, held HOLD_TICKS
|
||||
// for a screenshot. Then
|
||||
// FOLDER/NAME is started as Workbench starts a tool -- LoadSeg, CreateProc
|
||||
// with the icon's stack, and a WBStartup message whose one argument is the
|
||||
// folder's lock and the program's name -- from a current directory that is
|
||||
// not FOLDER (AWAY_FOLDER), and the message coming back says the program
|
||||
// has ended. Each step is a "WBLAUNCH:" line of LOG_NAME, beside Wblaunch.
|
||||
// scripts/verify-amiga-icon.sh is the gate.
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <dos/dos.h>
|
||||
#include <dos/dosextens.h>
|
||||
#include <exec/memory.h>
|
||||
#include <exec/ports.h>
|
||||
#include <graphics/modeid.h>
|
||||
#include <intuition/intuition.h>
|
||||
#include <intuition/screens.h>
|
||||
#include <workbench/startup.h>
|
||||
#include <workbench/workbench.h>
|
||||
|
||||
#include <proto/dos.h>
|
||||
#include <proto/exec.h>
|
||||
#include <proto/graphics.h>
|
||||
#include <proto/icon.h>
|
||||
#include <proto/intuition.h>
|
||||
|
||||
#define LOG_NAME "wblaunch.txt"
|
||||
#define AWAY_FOLDER "RAM:"
|
||||
#define PATH_BYTES 256u
|
||||
#define SCREEN_W 640
|
||||
#define SCREEN_H STDSCREENHEIGHT
|
||||
#define SCREEN_DEPTH 2
|
||||
#define ICON_X 64
|
||||
#define ICON_Y 64
|
||||
#define HOLD_TICKS (5 * TICKS_PER_SECOND)
|
||||
#define GUN_MASK 0xFu
|
||||
#define RED_SHIFT 8u
|
||||
#define GREEN_SHIFT 4u
|
||||
#define ARGS_WANTED 3
|
||||
// A blank pointer, in chip memory: its two control words, one empty line of
|
||||
// two words, and the two words that end it.
|
||||
#define BLANK_POINTER_BYTES (6u * sizeof(UWORD))
|
||||
#define BLANK_POINTER_H 1
|
||||
#define BLANK_POINTER_W 16
|
||||
|
||||
static bool drawIcon(struct DiskObject *icon);
|
||||
static bool launch(const char *folder, const char *name, ULONG stack);
|
||||
static void logLine(const char *format, ...);
|
||||
|
||||
// Workbench's four colours (2.0 on), 4 bits a gun: grey, black, white and
|
||||
// blue (tools/iconBake.py's AMIGA_COLOURS).
|
||||
static const UWORD gWorkbenchColours[] = { 0xAAA, 0x000, 0xFFF, 0x68B };
|
||||
|
||||
static FILE *gLog = NULL;
|
||||
|
||||
|
||||
// The icon's image on a screen of Workbench's colours, held for the gate's
|
||||
// screenshot. A window over the whole screen, active, hides the pointer.
|
||||
static bool drawIcon(struct DiskObject *icon) {
|
||||
struct Screen *screen;
|
||||
struct Window *window;
|
||||
UWORD *blank;
|
||||
UWORD i;
|
||||
bool drawn;
|
||||
|
||||
drawn = false;
|
||||
blank = (UWORD *)AllocMem(BLANK_POINTER_BYTES, MEMF_CHIP | MEMF_CLEAR);
|
||||
screen = OpenScreenTags(NULL, SA_Width, SCREEN_W, SA_Height, SCREEN_H, SA_Depth, SCREEN_DEPTH, SA_DisplayID, HIRES_KEY, SA_Quiet, TRUE, SA_ShowTitle, FALSE, TAG_DONE);
|
||||
window = NULL;
|
||||
if (screen != NULL) {
|
||||
window = OpenWindowTags(NULL, WA_CustomScreen, (ULONG)screen, WA_Backdrop, TRUE, WA_Borderless, TRUE, WA_Activate, TRUE, WA_RMBTrap, TRUE, TAG_DONE);
|
||||
}
|
||||
if (blank == NULL || window == NULL) {
|
||||
logLine("FAIL cannot open a screen and a window");
|
||||
} else {
|
||||
SetPointer(window, blank, BLANK_POINTER_H, BLANK_POINTER_W, 0, 0);
|
||||
for (i = 0; i < sizeof(gWorkbenchColours) / sizeof(gWorkbenchColours[0]); i++) {
|
||||
SetRGB4(&screen->ViewPort, i, (gWorkbenchColours[i] >> RED_SHIFT) & GUN_MASK, (gWorkbenchColours[i] >> GREEN_SHIFT) & GUN_MASK, gWorkbenchColours[i] & GUN_MASK);
|
||||
}
|
||||
SetRast(window->RPort, 0);
|
||||
DrawImage(window->RPort, (struct Image *)icon->do_Gadget.GadgetRender, ICON_X, ICON_Y);
|
||||
logLine("drawn at %d,%d", ICON_X, ICON_Y);
|
||||
Delay(HOLD_TICKS);
|
||||
ClearPointer(window);
|
||||
drawn = true;
|
||||
}
|
||||
if (window != NULL) {
|
||||
CloseWindow(window);
|
||||
}
|
||||
if (screen != NULL) {
|
||||
CloseScreen(screen);
|
||||
}
|
||||
if (blank != NULL) {
|
||||
FreeMem(blank, BLANK_POINTER_BYTES);
|
||||
}
|
||||
return drawn;
|
||||
}
|
||||
|
||||
|
||||
// FOLDER/NAME started as Workbench starts a tool, from AWAY_FOLDER; true
|
||||
// once its message has come back.
|
||||
static bool launch(const char *folder, const char *name, ULONG stack) {
|
||||
struct WBStartup startup;
|
||||
struct WBArg arg;
|
||||
struct MsgPort *reply;
|
||||
struct MsgPort *process;
|
||||
char path[PATH_BYTES];
|
||||
BPTR lock;
|
||||
BPTR away;
|
||||
BPTR before;
|
||||
BPTR segment;
|
||||
|
||||
strncpy(path, folder, sizeof(path) - 1u);
|
||||
path[sizeof(path) - 1u] = '\0';
|
||||
if (!AddPart((STRPTR)path, (STRPTR)name, sizeof(path))) {
|
||||
logLine("FAIL the path is too long");
|
||||
return false;
|
||||
}
|
||||
lock = Lock((STRPTR)folder, SHARED_LOCK);
|
||||
if (lock == 0) {
|
||||
logLine("FAIL cannot lock %s", folder);
|
||||
return false;
|
||||
}
|
||||
segment = LoadSeg((STRPTR)path);
|
||||
if (segment == 0) {
|
||||
logLine("FAIL cannot load %s", path);
|
||||
UnLock(lock);
|
||||
return false;
|
||||
}
|
||||
reply = CreateMsgPort();
|
||||
away = Lock((STRPTR)AWAY_FOLDER, SHARED_LOCK);
|
||||
if (reply == NULL || away == 0) {
|
||||
logLine("FAIL no reply port or no %s", AWAY_FOLDER);
|
||||
UnLoadSeg(segment);
|
||||
UnLock(lock);
|
||||
return false;
|
||||
}
|
||||
before = CurrentDir(away);
|
||||
process = CreateProc((STRPTR)name, 0, segment, (LONG)stack);
|
||||
if (process == NULL) {
|
||||
logLine("FAIL cannot start %s", path);
|
||||
} else {
|
||||
memset(&startup, 0, sizeof(startup));
|
||||
startup.sm_Message.mn_Node.ln_Type = NT_MESSAGE;
|
||||
startup.sm_Message.mn_ReplyPort = reply;
|
||||
startup.sm_Message.mn_Length = sizeof(startup);
|
||||
startup.sm_Process = process;
|
||||
startup.sm_Segment = segment;
|
||||
startup.sm_NumArgs = 1;
|
||||
startup.sm_ArgList = &arg;
|
||||
arg.wa_Lock = lock;
|
||||
arg.wa_Name = (STRPTR)name;
|
||||
logLine("started %s with %lu bytes of stack from %s", path, (unsigned long)stack, AWAY_FOLDER);
|
||||
PutMsg(process, &startup.sm_Message);
|
||||
WaitPort(reply);
|
||||
GetMsg(reply);
|
||||
}
|
||||
CurrentDir(before);
|
||||
UnLock(away);
|
||||
DeleteMsgPort(reply);
|
||||
UnLoadSeg(segment);
|
||||
UnLock(lock);
|
||||
return process != NULL;
|
||||
}
|
||||
|
||||
|
||||
static void logLine(const char *format, ...) {
|
||||
va_list args;
|
||||
|
||||
fputs("WBLAUNCH: ", gLog);
|
||||
va_start(args, format);
|
||||
vfprintf(gLog, format, args);
|
||||
va_end(args);
|
||||
fputc('\n', gLog);
|
||||
fflush(gLog);
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
struct DiskObject *icon;
|
||||
struct Image *image;
|
||||
char path[PATH_BYTES];
|
||||
int status;
|
||||
|
||||
gLog = fopen(LOG_NAME, "w");
|
||||
if (gLog == NULL) {
|
||||
return RETURN_FAIL;
|
||||
}
|
||||
status = RETURN_FAIL;
|
||||
if (argc != ARGS_WANTED) {
|
||||
logLine("FAIL usage: Wblaunch FOLDER NAME");
|
||||
} else if (IconBase == NULL) {
|
||||
logLine("FAIL no icon.library");
|
||||
} else {
|
||||
strncpy(path, argv[1], sizeof(path) - 1u);
|
||||
path[sizeof(path) - 1u] = '\0';
|
||||
AddPart((STRPTR)path, (STRPTR)argv[2], sizeof(path));
|
||||
icon = GetDiskObject((STRPTR)path);
|
||||
if (icon == NULL) {
|
||||
logLine("FAIL icon.library cannot read %s.info", path);
|
||||
} else {
|
||||
image = (struct Image *)icon->do_Gadget.GadgetRender;
|
||||
logLine("icon type=%u stack=%ld gadget=%dx%d image=%dx%dx%d", (unsigned)icon->do_Type, (long)icon->do_StackSize, icon->do_Gadget.Width, icon->do_Gadget.Height, image->Width, image->Height, image->Depth);
|
||||
if (drawIcon(icon) && launch(argv[1], argv[2], (ULONG)icon->do_StackSize)) {
|
||||
logLine("returned");
|
||||
status = RETURN_OK;
|
||||
}
|
||||
FreeDiskObject(icon);
|
||||
}
|
||||
}
|
||||
fclose(gLog);
|
||||
return status;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -71,88 +71,96 @@
|
|||
// ----- Derived CPU markers -----
|
||||
|
||||
#if defined(JOEYLIB_PLATFORM_IIGS)
|
||||
#define JOEYLIB_CPU_65816
|
||||
#define JOEYLIB_ENDIAN_LITTLE
|
||||
#define JOEYLIB_NATIVE_CHUNKY
|
||||
#define JOEYLIB_PLATFORM_NAME "Apple IIgs"
|
||||
#define JL_CPU_65816
|
||||
#define JL_ENDIAN_LITTLE
|
||||
#define JL_NATIVE_CHUNKY
|
||||
#define JL_PLATFORM_NAME "Apple IIgs"
|
||||
#elif defined(JOEYLIB_PLATFORM_AMIGA)
|
||||
#define JOEYLIB_CPU_68000
|
||||
#define JOEYLIB_ENDIAN_BIG
|
||||
#define JOEYLIB_NATIVE_PLANAR
|
||||
#define JOEYLIB_HAS_BLITTER
|
||||
#define JOEYLIB_HAS_COPPER
|
||||
#define JOEYLIB_PLATFORM_NAME "Commodore Amiga"
|
||||
#define JL_CPU_68000
|
||||
#define JL_ENDIAN_BIG
|
||||
#define JL_NATIVE_PLANAR
|
||||
#define JL_HAS_BLITTER
|
||||
#define JL_HAS_COPPER
|
||||
#define JL_PLATFORM_NAME "Commodore Amiga"
|
||||
#elif defined(JOEYLIB_PLATFORM_ATARIST)
|
||||
#define JOEYLIB_CPU_68000
|
||||
#define JOEYLIB_ENDIAN_BIG
|
||||
#define JOEYLIB_NATIVE_PLANAR
|
||||
#define JL_CPU_68000
|
||||
#define JL_ENDIAN_BIG
|
||||
#define JL_NATIVE_PLANAR
|
||||
// JOEYLIB_ATARI_STE is the STe build (make ste): the same port using the
|
||||
// BLiTTER, DMA sound and the 4-bit palette, for STe and Mega STe only.
|
||||
// BLiTTER, DMA sound, the 4-bit palette and (1040 STe) the enhanced
|
||||
// joystick ports, for STe and Mega STe only.
|
||||
#if defined(JOEYLIB_ATARI_STE)
|
||||
#define JOEYLIB_PLATFORM_NAME "Atari STe"
|
||||
#define JL_PLATFORM_NAME "Atari STe"
|
||||
#else
|
||||
#define JOEYLIB_PLATFORM_NAME "Atari ST"
|
||||
#define JL_PLATFORM_NAME "Atari ST"
|
||||
#endif
|
||||
#elif defined(JOEYLIB_PLATFORM_DOS)
|
||||
#define JOEYLIB_CPU_X86
|
||||
#define JOEYLIB_ENDIAN_LITTLE
|
||||
#define JOEYLIB_NATIVE_CHUNKY
|
||||
#define JOEYLIB_PLATFORM_NAME "MS-DOS"
|
||||
#define JL_CPU_X86
|
||||
#define JL_ENDIAN_LITTLE
|
||||
#define JL_NATIVE_CHUNKY
|
||||
#define JL_PLATFORM_NAME "MS-DOS"
|
||||
#elif defined(JOEYLIB_PLATFORM_X68000)
|
||||
// The stage is four separate 1bpp planes in main RAM (src/x68000/halPlanar.c,
|
||||
// the Amiga's layout), and a present copies its dirty rows into the text
|
||||
// plane at $E00000, which is four 1bpp planes too. The text plane has one
|
||||
// 16-colour palette, so per-line SCB palettes are reloaded as the beam scans
|
||||
// by a CRTC raster interrupt (src/x68000/x68kRasterIsr.s).
|
||||
#define JOEYLIB_CPU_68000
|
||||
#define JOEYLIB_ENDIAN_BIG
|
||||
#define JOEYLIB_NATIVE_PLANAR
|
||||
#define JOEYLIB_PLATFORM_NAME "Sharp X68000"
|
||||
#define JL_CPU_68000
|
||||
#define JL_ENDIAN_BIG
|
||||
#define JL_NATIVE_PLANAR
|
||||
#define JL_PLATFORM_NAME "Sharp X68000"
|
||||
#elif defined(JOEYLIB_PLATFORM_DVX)
|
||||
// The DOS stage (chunky 4bpp, x86 sprite JIT and tile walker from
|
||||
// src/x86/), shown in a DVX window, or on DVX's full screen, instead of
|
||||
// on the VGA.
|
||||
#define JOEYLIB_CPU_X86
|
||||
#define JOEYLIB_ENDIAN_LITTLE
|
||||
#define JOEYLIB_NATIVE_CHUNKY
|
||||
#define JOEYLIB_PLATFORM_NAME "DVX"
|
||||
#define JL_CPU_X86
|
||||
#define JL_ENDIAN_LITTLE
|
||||
#define JL_NATIVE_CHUNKY
|
||||
#define JL_PLATFORM_NAME "DVX"
|
||||
#elif defined(JOEYLIB_PLATFORM_HOSTED)
|
||||
// The generic chunky stage, resolved through its SCB palettes into a
|
||||
// window's RGB pixels by one backend (src/hosted/<backend>). Whatever CPU
|
||||
// the host has: no machine code is generated, sprites are interpreted.
|
||||
// the host has, sprites are interpreted -- except on Windows 3.x, whose
|
||||
// 32-bit code runs on a 386 like DOS's: sprites are compiled to x86 code
|
||||
// as on DOS (Win386's flat code and data selectors share one base, so the
|
||||
// code arena runs as it is).
|
||||
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
||||
#define JOEYLIB_ENDIAN_BIG
|
||||
#define JL_ENDIAN_BIG
|
||||
#else
|
||||
#define JOEYLIB_ENDIAN_LITTLE
|
||||
#define JL_ENDIAN_LITTLE
|
||||
#endif
|
||||
#define JL_NATIVE_CHUNKY
|
||||
#if defined(JOEYLIB_HOSTED_WIN16)
|
||||
#define JL_CPU_X86
|
||||
#else
|
||||
#define JL_SPRITE_INTERPRETED
|
||||
#endif
|
||||
#define JOEYLIB_NATIVE_CHUNKY
|
||||
#define JOEYLIB_SPRITE_INTERPRETED
|
||||
#if defined(JOEYLIB_HOSTED_SDL3) && defined(__PSP__)
|
||||
#define JOEYLIB_PLATFORM_NAME "PSP"
|
||||
#define JL_PLATFORM_NAME "PSP"
|
||||
#elif defined(JOEYLIB_HOSTED_SDL3)
|
||||
#define JOEYLIB_PLATFORM_NAME "SDL3"
|
||||
#define JL_PLATFORM_NAME "SDL3"
|
||||
#elif defined(JOEYLIB_HOSTED_WIN32)
|
||||
#define JOEYLIB_PLATFORM_NAME "Windows"
|
||||
#define JL_PLATFORM_NAME "Windows"
|
||||
#elif defined(JOEYLIB_HOSTED_WIN16)
|
||||
#define JOEYLIB_PLATFORM_NAME "Windows 3.x"
|
||||
#define JL_PLATFORM_NAME "Windows 3.x"
|
||||
#elif defined(JOEYLIB_HOSTED_BE) && defined(__HAIKU__)
|
||||
#define JOEYLIB_PLATFORM_NAME "Haiku"
|
||||
#define JL_PLATFORM_NAME "Haiku"
|
||||
#elif defined(JOEYLIB_HOSTED_BE)
|
||||
#define JOEYLIB_PLATFORM_NAME "BeOS"
|
||||
#define JL_PLATFORM_NAME "BeOS"
|
||||
#else
|
||||
#error "JoeyLib: JOEYLIB_PLATFORM_HOSTED needs a backend, e.g. -DJOEYLIB_HOSTED_SDL3"
|
||||
#endif
|
||||
#elif defined(JOEYLIB_PLATFORM_BLANK)
|
||||
// Chunky little-endian is the simplest baseline for a new port: the generic
|
||||
// backend gives you fully-working software rendering out of the box. Adjust
|
||||
// these (or switch to JOEYLIB_NATIVE_PLANAR) once you know your hardware.
|
||||
#define JOEYLIB_ENDIAN_LITTLE
|
||||
#define JOEYLIB_NATIVE_CHUNKY
|
||||
#define JOEYLIB_SPRITE_INTERPRETED
|
||||
#define JOEYLIB_PLATFORM_NAME "Blank Template"
|
||||
// these (or switch to JL_NATIVE_PLANAR) once you know your hardware.
|
||||
#define JL_ENDIAN_LITTLE
|
||||
#define JL_NATIVE_CHUNKY
|
||||
#define JL_SPRITE_INTERPRETED
|
||||
#define JL_PLATFORM_NAME "Blank Template"
|
||||
#endif
|
||||
|
||||
// JOEYLIB_SPRITE_INTERPRETED: the port has no sprite code emitter (it links
|
||||
// JL_SPRITE_INTERPRETED: the port has no sprite code emitter (it links
|
||||
// src/generic/spriteEmitStub.c), so every sprite is drawn by the
|
||||
// interpreter. A pre-compiled bank (.spc) still gives it the cels' pixels;
|
||||
// their machine code is skipped.
|
||||
|
|
@ -193,7 +201,7 @@
|
|||
#define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function
|
||||
// (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord,
|
||||
// the VBL heartbeat tick read -- no function override to register here.)
|
||||
#define JL_HAS_WAIT_VBL // $C019 VBL poll, function
|
||||
#define JL_HAS_WAIT_VBL // the VBL heartbeat count advancing, function
|
||||
// audio: every real port implements the full engine
|
||||
#define JL_HAS_AUDIO_INIT
|
||||
#define JL_HAS_AUDIO_SHUTDOWN
|
||||
|
|
@ -205,7 +213,6 @@
|
|||
#define JL_HAS_AUDIO_PLAY_SFX_STREAM_RAW
|
||||
#define JL_HAS_AUDIO_SET_PAN
|
||||
#define JL_HAS_AUDIO_STOP_SFX
|
||||
#define JL_HAS_AUDIO_TONE
|
||||
#define JL_HAS_AUDIO_VOICE
|
||||
#define JL_HAS_AUDIO_NOISE
|
||||
#define JL_HAS_AUDIO_TICK_REGISTER
|
||||
|
|
@ -233,7 +240,8 @@
|
|||
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
|
||||
#define JL_HAS_SERIAL_SET_RTS
|
||||
// save files + disk space: delete (GS/OS Destroy), dir-ensure (GS/OS Create
|
||||
// $2001, storageType 13), disk-free (GS/OS Volume $2008 via prefix 0).
|
||||
// $2001, storageType 13), disk-free (GS/OS Volume $2008 on prefix 1's
|
||||
// volume, where SAVES/ is).
|
||||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
#define JL_HAS_DISK_FREE
|
||||
|
|
@ -267,7 +275,7 @@
|
|||
#define JL_HAS_SURFACE_ALLOC_PIXELS // amiga: NULL (planar storage), function
|
||||
#define JL_HAS_SURFACE_ALLOC_PORT_DATA // amiga AmigaPlanarT + chip planes, function
|
||||
#define JL_HAS_SURFACE_FREE_PORT_DATA // amiga FreeMem planes, function
|
||||
#define JL_HAS_FRAME_HZ // PAL 50, function
|
||||
#define JL_HAS_FRAME_HZ // 50 PAL / 60 NTSC, measured at init, function
|
||||
#define JL_HAS_FRAME_COUNT // VBL-server counter, function
|
||||
#define JL_HAS_WAIT_VBL // WaitTOF, function
|
||||
// audio: every real port implements the full engine
|
||||
|
|
@ -280,7 +288,6 @@
|
|||
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
|
||||
#define JL_HAS_AUDIO_SET_PAN
|
||||
#define JL_HAS_AUDIO_STOP_SFX
|
||||
#define JL_HAS_AUDIO_TONE
|
||||
#define JL_HAS_AUDIO_VOICE
|
||||
#define JL_HAS_AUDIO_NOISE
|
||||
#define JL_HAS_AUDIO_TICK_REGISTER
|
||||
|
|
@ -305,6 +312,8 @@
|
|||
#define JL_HAS_SERIAL_FLUSH
|
||||
// Application-driven DTR: how a program hangs up (jlSerialSetDtr).
|
||||
#define JL_HAS_SERIAL_SET_DTR
|
||||
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
|
||||
#define JL_HAS_SERIAL_SET_RTS
|
||||
// save files + disk space (CreateDir/DeleteFile/Info via dos.library)
|
||||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
|
|
@ -341,7 +350,7 @@
|
|||
#define JL_HAS_SURFACE_ALLOC_PIXELS // st: NULL (planar storage), function
|
||||
#define JL_HAS_SURFACE_ALLOC_PORT_DATA // st StPlanarT + interleaved buffer, function
|
||||
#define JL_HAS_SURFACE_FREE_PORT_DATA // st free buffer, function
|
||||
#define JL_HAS_FRAME_HZ // PAL 50, function
|
||||
#define JL_HAS_FRAME_HZ // the machine's 50 / 60 (71 mono), function
|
||||
#define JL_HAS_FRAME_COUNT // VBL-ISR counter, function
|
||||
#define JL_HAS_WAIT_VBL // frame-counter wait, function
|
||||
#define JL_HAS_MILLIS_ELAPSED // Supexec 200 Hz tick, function
|
||||
|
|
@ -354,13 +363,13 @@
|
|||
#define JL_HAS_AUDIO_PLAY_SFX
|
||||
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
|
||||
#define JL_HAS_AUDIO_STOP_SFX
|
||||
#define JL_HAS_AUDIO_TONE
|
||||
#define JL_HAS_AUDIO_VOICE
|
||||
#define JL_HAS_AUDIO_NOISE
|
||||
#define JL_HAS_AUDIO_TICK_REGISTER
|
||||
#define JL_HAS_AUDIO_CRITICAL_ENTER
|
||||
#define JL_HAS_AUDIO_CRITICAL_EXIT
|
||||
#define JL_HAS_AUDIO_FRAME_TICK
|
||||
#define JL_HAS_MOD_MIX_RUN // the module player's 8-bit mix, src/m68k/modMix.s
|
||||
// lifecycle / present / input (every real port implements)
|
||||
#define JL_HAS_INIT
|
||||
#define JL_HAS_SHUTDOWN
|
||||
|
|
@ -382,6 +391,8 @@
|
|||
#define JL_HAS_SERIAL_FLUSH
|
||||
// Application-driven DTR: how a program hangs up (jlSerialSetDtr).
|
||||
#define JL_HAS_SERIAL_SET_DTR
|
||||
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
|
||||
#define JL_HAS_SERIAL_SET_RTS
|
||||
// save files + disk space (POSIX mkdir/remove + GEMDOS Dfree)
|
||||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
|
|
@ -395,7 +406,7 @@
|
|||
// (The planar dual-write hooks are not linked to generics: every chunky port
|
||||
// elides them to ((void)0) in port.h's shared chunky block, PERF-AUDIT #42.)
|
||||
#define JL_HAS_FRAME_HZ // VGA ~70, function
|
||||
#define JL_HAS_FRAME_COUNT // $3DA retrace edge counter, function
|
||||
#define JL_HAS_FRAME_COUNT // from the millisecond clock (70 a second), function
|
||||
#define JL_HAS_WAIT_VBL // $3DA retrace poll, function
|
||||
#define JL_HAS_MILLIS_ELAPSED // uclock / audio-ISR tick, function
|
||||
// audio: every real port implements the full engine
|
||||
|
|
@ -407,7 +418,6 @@
|
|||
#define JL_HAS_AUDIO_PLAY_SFX
|
||||
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
|
||||
#define JL_HAS_AUDIO_STOP_SFX
|
||||
#define JL_HAS_AUDIO_TONE
|
||||
#define JL_HAS_AUDIO_VOICE
|
||||
#define JL_HAS_AUDIO_NOISE
|
||||
#define JL_HAS_AUDIO_TICK_REGISTER
|
||||
|
|
@ -439,11 +449,9 @@
|
|||
#define JL_HAS_SAVE_DELETE
|
||||
#define JL_HAS_DISK_FREE
|
||||
#elif defined(JOEYLIB_PLATFORM_X68000)
|
||||
// Bring-up: platform SERVICES only (src/x68000/hal.c), plus the serial
|
||||
// add-on (src/x68000/serial.c). Everything graphical comes from the portable
|
||||
// generics, so the port renders correctly before any native primitive
|
||||
// exists. Native ops arrive with the storage-model decision.
|
||||
// Planar graphics, adapted from the Amiga (identical surface layout).
|
||||
// Platform services in src/x68000/hal.c, the serial add-on in
|
||||
// src/x68000/serial.c, and planar graphics adapted from the Amiga
|
||||
// (identical surface layout, src/x68000/halPlanar.c).
|
||||
#define JL_HAS_SURFACE_CLEAR
|
||||
#define JL_HAS_DRAW_PIXEL
|
||||
#define JL_HAS_DRAW_LINE
|
||||
|
|
@ -474,7 +482,7 @@
|
|||
#define JL_HAS_SURFACE_FREE_PORT_DATA
|
||||
#define JL_HAS_INIT // _iocs_crtmod display bring-up
|
||||
#define JL_HAS_SHUTDOWN // restore the previous CRT mode
|
||||
#define JL_HAS_PRESENT // expand the chunky stage into GVRAM
|
||||
#define JL_HAS_PRESENT // copy the stage's dirty plane rows into the text plane
|
||||
#define JL_HAS_INPUT_INIT
|
||||
#define JL_HAS_INPUT_SHUTDOWN
|
||||
#define JL_HAS_INPUT_POLL // IOCS work area keys/mouse, PPI joystick ports
|
||||
|
|
@ -489,7 +497,8 @@
|
|||
#define JL_HAS_DISK_FREE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
// Serial add-on: IOCS polled RS-232C, structurally the ST HAL.
|
||||
// Serial add-on: RSDRV.SYS transmit and line setup, receive on the
|
||||
// port's own SCC interrupt (src/x68000/x68kSccRxIsr.s).
|
||||
#define JL_HAS_SERIAL_OPEN
|
||||
#define JL_HAS_SERIAL_CLOSE
|
||||
#define JL_HAS_SERIAL_POLL
|
||||
|
|
@ -499,17 +508,21 @@
|
|||
#define JL_HAS_SERIAL_FLUSH
|
||||
// Application-driven DTR: how a program hangs up (jlSerialSetDtr).
|
||||
#define JL_HAS_SERIAL_SET_DTR
|
||||
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
|
||||
#define JL_HAS_SERIAL_SET_RTS
|
||||
// Audio. CHIP side on the YM2151 (OPM): tone voices + noise, which is what
|
||||
// the portable JYM1 tracker rides, with no CPU mixing.
|
||||
#define JL_HAS_AUDIO_INIT
|
||||
#define JL_HAS_AUDIO_SHUTDOWN
|
||||
#define JL_HAS_AUDIO_TONE
|
||||
#define JL_HAS_AUDIO_VOICE
|
||||
#define JL_HAS_AUDIO_NOISE
|
||||
#define JL_HAS_AUDIO_TICK_REGISTER
|
||||
#define JL_HAS_AUDIO_CRITICAL_ENTER
|
||||
#define JL_HAS_AUDIO_CRITICAL_EXIT
|
||||
#define JL_HAS_AUDIO_FRAME_TICK
|
||||
// SAMPLE side (src/x68000/audioPcm.c): libxmp-lite + the shared 5-slot SFX
|
||||
#define JL_HAS_MOD_MIX_RUN // the module player's 8-bit mix, src/m68k/modMix.s
|
||||
#define JL_HAS_CODE_WRITTEN // the X68030's caches, after codegen
|
||||
// SAMPLE side (src/x68000/audioPcm.c): the module player + the shared 5-slot SFX
|
||||
// overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate
|
||||
// device from the OPM, so chip music and digital audio do not contend.
|
||||
#define JL_HAS_AUDIO_PLAY_MOD
|
||||
|
|
@ -554,7 +567,6 @@
|
|||
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
|
||||
#define JL_HAS_AUDIO_SET_PAN
|
||||
#define JL_HAS_AUDIO_STOP_SFX
|
||||
#define JL_HAS_AUDIO_TONE
|
||||
#define JL_HAS_AUDIO_VOICE
|
||||
#define JL_HAS_AUDIO_NOISE
|
||||
#define JL_HAS_AUDIO_TICK_REGISTER
|
||||
|
|
@ -591,10 +603,10 @@
|
|||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
#define JL_HAS_DISK_FREE
|
||||
// audio: a software mixer (MOD through libxmp-lite, the SFX slots, three
|
||||
// square-wave voices, YM-style noise and the speaker tone) run on the
|
||||
// program's thread, so the tick callback and the stream fills run there
|
||||
// too and the critical sections stay the generic no-ops.
|
||||
// audio: a software mixer (MOD through the module player, the SFX slots, three
|
||||
// square-wave voices and YM-style noise) run on the program's thread, so
|
||||
// the tick callback and the stream fills run there too and the critical
|
||||
// sections stay the generic no-ops.
|
||||
#define JL_HAS_AUDIO_INIT
|
||||
#define JL_HAS_AUDIO_SHUTDOWN
|
||||
#define JL_HAS_AUDIO_PLAY_MOD
|
||||
|
|
@ -604,7 +616,6 @@
|
|||
#define JL_HAS_AUDIO_PLAY_SFX_STREAM
|
||||
#define JL_HAS_AUDIO_SET_PAN
|
||||
#define JL_HAS_AUDIO_STOP_SFX
|
||||
#define JL_HAS_AUDIO_TONE
|
||||
#define JL_HAS_AUDIO_VOICE
|
||||
#define JL_HAS_AUDIO_NOISE
|
||||
#define JL_HAS_AUDIO_TICK_REGISTER
|
||||
|
|
@ -646,9 +657,9 @@
|
|||
|
||||
// ----- Library version -----
|
||||
|
||||
#define JOEYLIB_VERSION_MAJOR 1
|
||||
#define JOEYLIB_VERSION_MINOR 0
|
||||
#define JOEYLIB_VERSION_PATCH 0
|
||||
#define JOEYLIB_VERSION_STRING "1.0.0"
|
||||
#define JL_VERSION_MAJOR 1
|
||||
#define JL_VERSION_MINOR 0
|
||||
#define JL_VERSION_PATCH 0
|
||||
#define JL_VERSION_STRING "1.0.0"
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
55
installer/components/furnace.sh
Normal file
55
installer/components/furnace.sh
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# shellcheck shell=bash
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# Furnace, tildearrow's open-source multi-system chiptune tracker (GPLv2 or
|
||||
# later), the recommended tool for JoeyLib's chip-tracker music
|
||||
# (docs/Assets.adoc, "Content Creation Tools"): write the song for the TI
|
||||
# SN76489 (or the AY-3-8910 / YM2149), export it as VGM, and
|
||||
# tools/vgm2song.py turns that into a .song for tools/songbake. Its own Linux
|
||||
# x86_64 release, unpacked into toolchains/opt/furnace. Nothing builds with
|
||||
# it, so no group installs it: ./install.sh install furnace.
|
||||
# scripts/check-vgm.sh exports the SN76489 and AY demo songs it comes with
|
||||
# through it when it is installed.
|
||||
#
|
||||
# Furnace keeps its settings in ~/.config/furnace; the gates run it with HOME
|
||||
# pointed at their work folder so they leave none.
|
||||
|
||||
compDescription="content: Furnace 0.6.8.3 chiptune tracker (optional; chip-tracker music, docs/Assets.adoc)"
|
||||
compGroups="optional"
|
||||
compRequires="host"
|
||||
compHostDeps="gl-dev asound-dev x11-dev"
|
||||
compUrl="https://github.com/tildearrow/furnace/releases/download/v0.6.8.3/furnace-0.6.8.3-linux-x86_64.tar.gz"
|
||||
compSha256="298a4e30bbdfe5ad88ed7f3bd90bd657a3f0b2e6e7dfc2091663dda71424d555"
|
||||
compPin="$compSha256"
|
||||
|
||||
|
||||
compInstall() {
|
||||
local archive
|
||||
archive="$(fetchFile "$compUrl" "$compSha256")"
|
||||
rm -rf "$compDir"
|
||||
extractArchive "$archive" "$compDir" 1
|
||||
}
|
||||
|
||||
|
||||
compInstalled() {
|
||||
[ -x "$compDir/furnace" ] && [ -d "$compDir/demos" ]
|
||||
}
|
||||
|
||||
|
||||
# It starts and knows VGM export (its settings go to a scratch home).
|
||||
compVerify() {
|
||||
local tmp
|
||||
local usage
|
||||
tmp="$(mktemp -d)"
|
||||
usage="$(HOME="$tmp" "$compDir/furnace" -help 2>&1 || true)"
|
||||
rm -rf "$tmp"
|
||||
[[ "$usage" == *"-vgmout"* ]]
|
||||
}
|
||||
|
||||
|
||||
compEnv() {
|
||||
envVar FURNACE "$compDir/furnace"
|
||||
envVar FURNACE_DEMOS "$compDir/demos"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
# shellcheck shell=bash
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# Haiku's own build-host tools, built from the R1/beta6 source: package
|
||||
# (unpacks .hpkg packages: the Haiku sysroots are made from them) and
|
||||
# bfs_shell (reads and writes Be File System volumes: the Haiku VM images
|
||||
# get their job runner through it, and the gates hand programs to the VM
|
||||
# and take results back on a BFS disk). jam, which builds them, comes from
|
||||
# Haiku's buildtools at the same release. Installed into
|
||||
# toolchains/opt/haikuTools; bin/ holds wrappers that find the tools'
|
||||
# (unpacks .hpkg packages: the Haiku sysroots are made from them), bfs_shell
|
||||
# (reads and writes Be File System volumes: the Haiku VM images get their
|
||||
# job runner through it, and the gates hand programs to the VM and take
|
||||
# results back on a BFS disk), and xres (adds resources to a Haiku or BeOS
|
||||
# executable: make/be.mk gives every program its icons with it). jam, which
|
||||
# builds them, comes from Haiku's buildtools at the same release. Installed
|
||||
# into toolchains/opt/haikuTools; bin/ holds wrappers that find the tools'
|
||||
# libraries.
|
||||
|
||||
compDescription="haiku: Haiku R1/beta6 host tools (package, bfs_shell; also read BeOS volumes)"
|
||||
compDescription="haiku: Haiku R1/beta6 host tools (package, bfs_shell, xres; also read BeOS volumes)"
|
||||
compGroups="toolchain:haiku toolchain:haiku32 toolchain:beos emulator:haiku emulator:haiku32 emulator:beos"
|
||||
compRequires="host"
|
||||
compHostDeps="build-tools git python3 zlib-dev zstd-dev bison flex"
|
||||
|
|
@ -19,8 +21,8 @@ compUrl="https://github.com/haiku/haiku.git"
|
|||
compCommit="f6d86031a107e16f4fff6814e9aea20a39450842"
|
||||
buildtoolsUrl="https://github.com/haiku/buildtools.git"
|
||||
buildtoolsCommit="bfa561e3708c0e2d0155fb403b988ba515c9b16d"
|
||||
compPin="$compCommit:$buildtoolsCommit"
|
||||
hostTools="bfs_shell package"
|
||||
hostTools="bfs_shell package xres"
|
||||
compPin="$compCommit:$buildtoolsCommit:$hostTools"
|
||||
|
||||
|
||||
compInstall() {
|
||||
|
|
@ -43,7 +45,7 @@ compInstall() {
|
|||
# The tools keep the attributes of the host's files (which have none
|
||||
# here) in a folder named when they are built: theirs, not the build
|
||||
# tree's, which goes.
|
||||
(cd "$src/haiku/generated" && PATH="$src/buildtools/jam/bin.linuxx86:$PATH" jam -q -j"$JOEY_JOBS" -sHAIKU_BUILD_ATTRIBUTES_DIR="$compDir/attributes" '<build>bfs_shell' '<build>package')
|
||||
(cd "$src/haiku/generated" && PATH="$src/buildtools/jam/bin.linuxx86:$PATH" jam -q -j"$JOEY_JOBS" -sHAIKU_BUILD_ATTRIBUTES_DIR="$compDir/attributes" '<build>bfs_shell' '<build>package' '<build>xres')
|
||||
objects="$src/haiku/generated/objects/linux"
|
||||
rm -rf "$compDir"
|
||||
mkdir -p "$compDir/bin" "$compDir/libexec" "$compDir/lib" "$compDir/attributes"
|
||||
|
|
@ -58,17 +60,23 @@ compInstall() {
|
|||
|
||||
|
||||
compInstalled() {
|
||||
[ -x "$compDir/bin/bfs_shell" ] && [ -x "$compDir/bin/package" ] && [ -x "$compDir/libexec/bfs_shell" ] && [ -x "$compDir/libexec/package" ] && [ -d "$compDir/attributes" ]
|
||||
local tool
|
||||
for tool in $hostTools; do
|
||||
[ -x "$compDir/bin/$tool" ] && [ -x "$compDir/libexec/$tool" ] || return 1
|
||||
done
|
||||
[ -d "$compDir/attributes" ]
|
||||
}
|
||||
|
||||
|
||||
# Make a small BFS volume, write a file onto it and read it back; package
|
||||
# starts. (Outputs are captured, not piped into grep: under pipefail a tool's
|
||||
# own exit status, or SIGPIPE from an early grep, would fail the check.)
|
||||
# and xres start. (Outputs are captured, not piped into grep: under pipefail
|
||||
# a tool's own exit status, or SIGPIPE from an early grep, would fail the
|
||||
# check.)
|
||||
compVerify() {
|
||||
local tmp
|
||||
local readBack
|
||||
local usage
|
||||
local xresUsage
|
||||
tmp="$(mktemp -d)"
|
||||
printf 'JoeyLib\n' > "$tmp/in.txt"
|
||||
truncate -s 8M "$tmp/v.img"
|
||||
|
|
@ -77,8 +85,9 @@ compVerify() {
|
|||
printf 'cp :%s /myfs/in.txt\nsync\nquit\n' "$tmp/in.txt" | "$compDir/bin/bfs_shell" "$tmp/v.img" > /dev/null 2>&1 || true
|
||||
readBack="$(printf 'cat /myfs/in.txt\nquit\n' | "$compDir/bin/bfs_shell" "$tmp/v.img" 2>&1 || true)"
|
||||
usage="$("$compDir/bin/package" 2>&1 || true)"
|
||||
xresUsage="$("$compDir/bin/xres" -h 2>&1 || true)"
|
||||
rm -rf "$tmp"
|
||||
[[ "$readBack" == *JoeyLib* && "$usage" == *"Usage: package"* ]]
|
||||
[[ "$readBack" == *JoeyLib* && "$usage" == *"Usage: package"* && "$xresUsage" == *"Valid commands are"* ]]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# shellcheck shell=bash
|
||||
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
||||
# See LICENSE.md for license information.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue