diff --git a/LICENSES.md b/LICENSES.md
index e22b0ac..87889d8 100644
--- a/LICENSES.md
+++ b/LICENSES.md
@@ -23,16 +23,17 @@ most.
### SDL 3.4.18
-- **Used on:** SDL3, PSP, web
+- **Used on:** SDL3, PSP, web, macOS
- **URL:**
- **License:** zlib
- **Role:** window, input, and audio for the hosted SDL3 code. Built as a
- static library for the host, for the PSP with pspdev, and for Emscripten
+ static library for the host, for the PSP with pspdev, for Emscripten
(with JoeyLib's patches, `installer/patches/sdl3Web/`, which are under
- SDL's zlib license), and linked into each program.
+ SDL's zlib license), and for macOS with zig (universal), and linked into
+ each program.
- **Installed at:** `toolchains/opt/sdl3/`, `toolchains/opt/sdl3Psp/`,
- `toolchains/opt/sdl3Web/` (installer components `sdl3`, `sdl3Psp`,
- `sdl3Web`)
+ `toolchains/opt/sdl3Web/`, `toolchains/opt/sdl3Macos/` (installer
+ components `sdl3`, `sdl3Psp`, `sdl3Web`, `sdl3Macos`)
### NinjaTrackerPlus (NTP)
@@ -52,7 +53,7 @@ most.
### PTPlayer 6.4
- **Used on:** Amiga; and, translated to C, DOS, Atari ST, X68000, SDL3,
- PSP, web, Win16, Win32, Haiku, BeOS
+ PSP, web, macOS, Win16, Win32, Haiku, BeOS
- **URL:**
(author Frank Wille, )
- **License:** Public Domain (the archive's `license` file: the Unlicense)
@@ -109,6 +110,8 @@ them, in brief (the entries below have the detail):
terms Unknown; see the BeOS entry under the never-downloaded media
below).
- **Haiku:** the system's own shared libraries, at run time (MIT).
+- **macOS:** the system's own libraries and frameworks, at run time
+ (Apple's); zig's compiler runtime, linked in (MIT).
- **Every gcc port:** libgcc, under the GCC Runtime Library Exception,
which puts no conditions on the programs it is linked into.
@@ -303,7 +306,7 @@ above.
every Win16 program -- must, by section 2.2(d), include a prominent
notice, in the program and its documentation, that the source is
available under the license and where to get it. Every Win16 program
- carries it (`hostWin16OwplNotice`, src/hosted/win16/win16Backend.c), as
+ carries it (`jlHostWin16OwplNotice`, src/hosted/win16/win16Backend.c), as
do KPJOY.DRV and KPCOMM.DRV, as a string never shown; docs/Portability.adoc
("Shipping") gives the same line for a program's documentation.
- **Role:** the Win386 target compiles the win16 port's 32-bit programs
@@ -324,6 +327,34 @@ above.
- **Installed at:** `toolchains/opt/llvmMingw/` (installer component
`llvmMingw`)
+### zig 0.15.2
+
+- **Used on:** macOS
+- **URL:**
+- **License:** MIT. The clang, LLVM, and lld it is built from: Apache
+ License 2.0 with LLVM Exceptions. The macOS C library headers and
+ libSystem stubs it carries: Apple's, from Apple's open-source releases
+ (the Apple Public Source License 2.0, among others).
+- **Role:** the macos port's C compiler and Mach-O linker; it signs the
+ Apple silicon programs ad hoc as it links them.
+- **Installed at:** `toolchains/opt/zig/` (installer component `zig`)
+
+### Apple MacOSX 15.5 SDK
+
+- **Used on:** macOS
+- **URL:** Apple ships it only inside Xcode; the installer takes it from
+ the mirror osxcross users share,
+ , pinned to its SHA-256
+- **License:** Apple's Xcode and Apple SDKs Agreement. None of it is
+ shipped: it gives the build the frameworks' headers and stub libraries to
+ link against, and a program uses the Mac's own frameworks when it runs.
+- **Role:** the frameworks (Cocoa, CoreAudio, IOKit, and the rest) the
+ macos port's programs and its SDL 3 link against. The host's LLVM tools
+ (`llvm-lipo`, `llvm-libtool-darwin`; Apache License 2.0 with LLVM
+ Exceptions) join and archive what is built for both Mac processors.
+- **Installed at:** `toolchains/opt/macosSdk/` (installer component
+ `macosSdk`)
+
### pspdev v20261001
- **Used on:** PSP
diff --git a/Makefile b/Makefile
index 9297a9d..d8e5bb4 100644
--- a/Makefile
+++ b/Makefile
@@ -15,6 +15,7 @@
# make dvx builds DVX (JoeyLib programs as DVX desktop programs)
# make sdl3 builds SDL3 (JoeyLib programs as native windowed programs)
# make web builds the web port (JoeyLib programs as web pages)
+# make macos builds macOS (JoeyLib programs as universal Mac apps)
# make win32 builds Win32 (Windows 95 through today, as .EXE programs)
# make win16 builds Win16 (Windows 3.1 on a 386, as .EXE programs)
# make psp builds the PSP port (JoeyLib programs as PSP homebrew EBOOT.PBPs)
@@ -39,7 +40,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-conform verify-serial docs
+.PHONY: all iigs iigs-disk iigs-verify amiga atarist ste dos x68000 dvx sdl3 web macos 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),)
@@ -83,6 +84,9 @@ sdl3:
web:
@$(MAKE) -f $(REPO_DIR)/make/web.mk
+macos:
+ @$(MAKE) -f $(REPO_DIR)/make/macos.mk
+
win32:
@$(MAKE) -f $(REPO_DIR)/make/win32.mk
@@ -154,6 +158,7 @@ status:
@echo " dvx: $(if $(HAVE_DVX),yes,NO)"
@echo " sdl3: $(if $(HAVE_SDL3),yes,NO)"
@echo " web: $(if $(HAVE_WEB),yes,NO)"
+ @echo " macos: $(if $(HAVE_MACOS),yes,NO)"
@echo " win32: $(if $(HAVE_WIN32),yes,NO)"
@echo " win16: $(if $(HAVE_WIN16),yes,NO)"
@echo " psp: $(if $(HAVE_PSP),yes,NO)"
@@ -162,7 +167,7 @@ status:
@echo " beos: $(if $(HAVE_BEOS),yes,NO)"
help:
- @echo "Targets: iigs amiga atarist ste dos x68000 dvx sdl3 web win32 win16 psp haiku haiku32 beos all clean status help"
+ @echo "Targets: iigs amiga atarist ste dos x68000 dvx sdl3 web macos win32 win16 psp haiku haiku32 beos all clean status 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)"
diff --git a/README.md b/README.md
index 53074d1..8bf92cb 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,9 @@ and the PSP, from a single codebase:
statically; sprites are interpreted)
- Web (the SDL3 port compiled to WebAssembly with Emscripten: each program
a page, its DATA packed in, saves kept in the browser)
+- macOS (the SDL3 port cross-built on Linux with zig against Apple's SDK:
+ each program a universal app for Apple silicon and Intel Macs, macOS 13
+ or newer; built and checked here, but run only on a Mac)
- Win32 (Windows 95 through today: the hosted core with a backend written to
the Win32 API, built with llvm-mingw against MSVCRT.DLL, tested under Wine)
- Win16 (Windows 3.1 on a 386: the hosted core with a backend written to the
@@ -66,20 +69,23 @@ hand, `source toolchains/env.sh` sets the same variables.
`make` builds `libjoey.a` for every port whose toolchain is installed, plus
the example programs (`hello`, `pattern`, `draw`, `keys`, `serial`,
-`sertest`, `save`, `tilebank`, `joy`, `sprite`, `audio`, `uber`, `adventure`,
-`adventure2`) for each. The IIgs is the exception:
+`sertest`, `save`, `tilebank`, `joy`, `sprite`, `scroll`, `audio`, `uber`,
+`adventure`, `adventure2`) for each. The IIgs is the exception:
`make iigs` builds the library, and `make iigs-disk` builds its examples and
packages them onto a disk image. DVX needs the `dvx` installer component,
which unpacks DVX and its SDK from a DVX tree's distribution, `dist/dvx.zip`
and `dist/sdk.zip` (`./install.sh --dvx-dir PATH install dvx`; the default
-is `../dvxgui`); its examples land in `build/dvx/bin/apps/joeylib//`,
+is `../dvxgui`); its examples land in `build/dvx/bin/apps/kpunch//`,
laid out as they install in DVX. SDL3 needs the `sdl3` component
(`./install.sh install toolchain:sdl3`), which builds SDL 3 from its release
source; its examples are native programs in `build/sdl3/bin/`, and running
one takes seconds rather than an emulator's minutes. The web port needs the
`web` components (`./install.sh install toolchain:web`: Emscripten through
emsdk, and SDL 3 built with it); its examples are pages in
-`build/web/bin//`. The Win32 port needs `toolchain:win32` (llvm-mingw)
+`build/web/bin//`. The macOS port needs `toolchain:macos` (zig,
+Apple's macOS SDK, and SDL 3 built with them); its examples are apps,
+`build/macos/bin/.app`, which nothing here runs: copy one to a Mac.
+The Win32 port needs `toolchain:win32` (llvm-mingw)
and, to run its programs, `emulator:win32` (a portable Wine). The Win16 port
needs `toolchain:win16` (Open Watcom) and, to run its programs,
`emulator:win16` (Windows for Workgroups 3.11 in DOSBox Staging, set up from
diff --git a/docs/ApiCore.adoc b/docs/ApiCore.adoc
index 7c6d0ae..0dd27e7 100644
--- a/docs/ApiCore.adoc
+++ b/docs/ApiCore.adoc
@@ -104,8 +104,8 @@ const char *jlPlatformName(void);
*Parameters:* none.
*Returns:* a human-readable platform identifier, for example `"Apple IIgs"`,
-`"MS-DOS"`, `"DVX"`, `"SDL3"` (the web port is the SDL3 port in a browser) or
-`"Windows"`.
+`"MS-DOS"`, `"DVX"`, `"SDL3"` (the web port is the SDL3 port in a browser),
+`"macOS"`, or `"Windows"`.
Library-owned.
=== jlVersionString
diff --git a/docs/ApiDebug.adoc b/docs/ApiDebug.adoc
index 8db23f7..8e5ac54 100644
--- a/docs/ApiDebug.adoc
+++ b/docs/ApiDebug.adoc
@@ -4,7 +4,8 @@ Declared in ``.
A crash-tracing logger that writes `joeylog.txt` in the program's folder,
beside `DATA/` and `SAVES/` (<<_files>>: on the ST, the current directory; on
-DVX, the program's settings folder, `CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`).
+DVX, the program's settings folder, `CONFIG\APPS\VENDOR\NAME\JOEYLOG.TXT`;
+on macOS, its Application Support folder).
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
diff --git a/docs/ApiFile.adoc b/docs/ApiFile.adoc
index 1733926..db78a0b 100644
--- a/docs/ApiFile.adoc
+++ b/docs/ApiFile.adoc
@@ -25,8 +25,8 @@ goes there too):
* *DVX* keeps the two apart, 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.
+ `CONFIG\APPS\VENDOR\NAME` (`VENDOR` its maker's, <<_a_project>>), which
+ DVX keeps apart from the program files so a reinstall never overwrites it.
* *The web* keeps them apart too: `DATA/` is packed into the program, and
`SAVES/` and the log are in a folder named after the page
(`games/hero.html` has `/games/hero/`), its `SAVES/` kept in the browser's
diff --git a/docs/ApiSprite.adoc b/docs/ApiSprite.adoc
index b5a3d92..4425fe8 100644
--- a/docs/ApiSprite.adoc
+++ b/docs/ApiSprite.adoc
@@ -42,11 +42,12 @@ That is often not the sprite rectangle:
* `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.
+* On the Amiga, ST, and X68000 a save captures the *16-pixel-aligned region*
+ containing the sprite (or its visible part).
* 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.
+* A save of a sprite partly off the surface captures only the visible part
+ (on the Amiga, ST, and X68000, the 16-pixel-aligned region holding it).
It is the recorded region you must not overwrite before restoring.
diff --git a/docs/ApiTile.adoc b/docs/ApiTile.adoc
index baa99cb..feeb913 100644
--- a/docs/ApiTile.adoc
+++ b/docs/ApiTile.adoc
@@ -24,7 +24,8 @@ one. Fonts, terrain sets, and spritesheets are all the same thing.
|`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".
+|`JL_TILE_MAP_SKIP` |0xFF |`jlTileMapPaste` entry meaning "leave this cell alone"; in a tile layer's map, colour 0.
+|`JL_TILE_LAYER_MAX_SIDE` |4096 |Largest tile layer map side, in tiles.
|===
[source,c]
@@ -333,3 +334,141 @@ 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.
====
+
+=== Scrolling Tile Layers
+
+A tile layer is a map of tile indices, up to `JL_TILE_LAYER_MAX_SIDE` tiles a
+side, seen through a band of the stage: whole tile rows, the full width (rows
+outside the band are yours, for a status bar or a score). `jlTileLayerDraw`
+fills the band with the map as seen from a pixel origin, to the pixel both
+ways, so moving the origin a little each frame scrolls it. The map wraps at its
+edges: an origin past them, or a map smaller than the view, repeats it.
+
+The layer keeps the tiles around the view drawn, so a draw only draws the edge
+tiles the move uncovered (a move of a whole view or more draws them all), then
+copies the band. A draw covers the band and never reads what was there, so a
+scrolling frame is: draw the layer, draw the sprites with `jlSpriteDraw` (no
+save-under, since the next draw covers them), present.
+
+[source,c]
+----
+static jlTileT tiles[64]; // from jlTileBankLoad
+static uint8_t map[256 * 25]; // 256 x 25 tile indices
+jlSpriteT *ship; // from jlSpriteCreate, compiled
+jlTileLayerT *layer;
+uint16_t x = 0;
+
+layer = jlTileLayerCreate(tiles, map, 256, 25, 0, JL_TILE_BLOCKS_PER_COL);
+for (;;) {
+ jlTileLayerDraw(jlStageGet(), layer, x, 0);
+ jlSpriteDraw(jlStageGet(), ship, 40, 96);
+ jlStagePresent();
+ x++;
+}
+----
+
+The layer keeps a pointer to your tileset and map rather than copying them, so
+both must outlive it. Change a cell with `jlTileLayerSetTile`, not by writing
+the map: the layer would not see the write until that cell scrolled out of the
+view and back. A tile whose pixels you change in the tileset is likewise only
+seen where a cell showing it is set again.
+
+What a layer costs in memory is its view: about 1.3 KB per tile row of the band,
+plus one, on the Amiga, ST, and X68000 (35 KB for a full-height band), twice that on the
+others, which keep a second copy of the view shifted by a pixel so an odd
+origin copies whole bytes.
+
+[NOTE]
+====
+Every whole-band scroll redraws the band and presents it, so a scrolling game
+runs at the rate of a full-screen present plus a full-band copy. On the
+slowest machines JoeyLib targets that is a few frames a second (the IIgs about
+5.8, the plain ST about 8, a 386SX-16 under DOS about 6.5 in the UBER
+benchmark; the Amiga and STe copy the band with their blitters, about 20), so
+plan scrolling games around a band no taller than the game needs.
+====
+
+=== jlTileLayerCreate
+
+[source,c]
+----
+jlTileLayerT *jlTileLayerCreate(const jlTileT *tiles, uint8_t *map,
+ uint16_t mapWidth, uint16_t mapHeight,
+ uint8_t viewBy, uint8_t viewHeight);
+----
+
+Makes a layer over `map`. Nothing is drawn until `jlTileLayerDraw`.
+
+[cols="1,1,4", options="header"]
+|===
+|Parameter |Type |Description
+|`tiles` |`const jlTileT *` |The tileset. Kept, not copied.
+|`map` |`uint8_t *` |`mapWidth * mapHeight` tileset indices, row-major, so
+ `map[row * mapWidth + col]`. `JL_TILE_MAP_SKIP` draws colour 0. Kept, not
+ copied; `jlTileLayerSetTile` writes it.
+|`mapWidth`, `mapHeight` |`uint16_t` |The map's size in tiles, 1 to
+ `JL_TILE_LAYER_MAX_SIDE`.
+|`viewBy` |`uint8_t` |The band's first tile row on the stage.
+|`viewHeight` |`uint8_t` |The band's height in tile rows, at least 1;
+ `viewBy + viewHeight` at most `JL_TILE_BLOCKS_PER_COL`.
+|===
+
+*Returns:* the layer, or `NULL` if an argument is `NULL` or out of range, or
+memory runs out.
+
+[WARNING]
+====
+Like `jlTileMapPaste`, a layer does not range-check map indices against the
+tileset: any index other than `JL_TILE_MAP_SKIP` must be a valid one.
+====
+
+=== jlTileLayerDestroy
+
+[source,c]
+----
+void jlTileLayerDestroy(jlTileLayerT *layer);
+----
+
+Frees a layer, not its tileset or map. `NULL` is a no-op.
+
+=== jlTileLayerDraw
+
+[source,c]
+----
+void jlTileLayerDraw(jlSurfaceT *dst, jlTileLayerT *layer,
+ uint16_t x, uint16_t y);
+----
+
+Fills the layer's band of `dst` with the map seen from pixel `(x, y)`: the
+band's top-left pixel shows map pixel `(x, y)`, each taken modulo the map's
+size in pixels. Any pixel origin, with no alignment.
+
+[cols="1,1,4", options="header"]
+|===
+|Parameter |Type |Description
+|`dst` |`jlSurfaceT *` |Destination surface, normally the stage.
+|`layer` |`jlTileLayerT *` |The layer.
+|`x`, `y` |`uint16_t` |The origin, in map pixels.
+|===
+
+*Returns:* nothing.
+
+=== jlTileLayerSetTile
+
+[source,c]
+----
+void jlTileLayerSetTile(jlTileLayerT *layer, uint16_t tx, uint16_t ty,
+ uint8_t tile);
+----
+
+Sets map cell `(tx, ty)` to `tile`. The next draw shows it.
+
+[cols="1,1,4", options="header"]
+|===
+|Parameter |Type |Description
+|`layer` |`jlTileLayerT *` |The layer.
+|`tx`, `ty` |`uint16_t` |The cell, in tiles. Out-of-range cells are ignored.
+|`tile` |`uint8_t` |The tileset index, or `JL_TILE_MAP_SKIP` for colour 0.
+|===
+
+*Returns:* nothing.
diff --git a/docs/Assets.adoc b/docs/Assets.adoc
index a0fd02e..6b0e0ac 100644
--- a/docs/Assets.adoc
+++ b/docs/Assets.adoc
@@ -16,6 +16,11 @@ type: what you author, which tool bakes it, and which API call loads it.
|`.tbk`
|`jlTileBankLoad`, `jlTileBankLoadToSurface`
+|A Tiled map (TMX or TMJ) with indexed tilesets
+|`tiled2joey.py`, then `assetbake.py --type tile` for its sheet
+|a `.map` per layer, and a `.tbk`
+|`jlTileMapPaste` (the map read with `jlDataOpen`)
+
|Indexed PNG sprite sheet
|`assetbake.py --type sprite`
|`.spr`
@@ -69,6 +74,12 @@ run on Linux, and all but OpenMPT are native there.
JoeyLib palette holds exactly, 4 bits a gun. The sheet rules are in
<<_authoring_the_png>>.
+|Tile maps
+|*Tiled* (GPLv2; what you make with it is yours), a map editor that places
+ tiles from tilesets in layers.
+|Orthogonal maps of 8x8 tiles from indexed tilesets, then `tiled2joey.py`
+ (<<_maps_from_tiled>>).
+
|Program icons
|Any of the above, saving an RGBA PNG with transparency.
|One picture for every system (<<_icons>>).
@@ -132,6 +143,61 @@ 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`.
+==== Maps from Tiled
+
+`tools/tiled2joey.py` turns a map made in Tiled into what a JoeyLib program
+pastes: a tile sheet, which `assetbake.py` bakes into a tile bank for each
+port, and a map for each tile layer, which `jlTileMapPaste` takes as it is:
+
+[source,console]
+----
+$ python3 tools/tiled2joey.py --header build/level1.h level1.tmx \
+ build/level1.png build/level1
+tiled2joey: 60x25 map, 87 tiles, 14 colours, layers: ground.map, detail.map
+$ python3 tools/assetbake/assetbake.py --type tile --target iigs \
+ build/level1.png build/iigs/bin/DATA/level1.tbk
+----
+
+* *The map* is Tiled's own TMX or its JSON (TMJ), orthogonal and finite, with
+ 8x8 tiles. Its tilesets, in the map or in files of their own (TSX or TSJ),
+ are single images: indexed PNGs whose colours make one palette of at most
+ 16 between them. The first tileset's colours keep their indices, so its
+ colour 0 stays the background; another's are matched by colour or take a
+ slot the first does not use.
+* *The sheet* holds the tiles the map's layers use, in the order they first
+ appear, so the bank holds nothing the map does not place. A tile drawn
+ flipped or rotated is baked as a tile of its own (JoeyLib draws tiles only
+ as they are), so a map can use up to 255 distinct tiles and orientations
+ between its layers.
+* *Each layer* becomes `NAME.map` in the folder given: width x height bytes,
+ row after row, each a tile's index in the bank or `JL_TILE_MAP_SKIP` where
+ the layer is empty, so pasting a layer over another leaves the one below
+ showing through. The files are the same on every port.
+* `--header` writes the map's size and tile count as C constants, and lists
+ each layer's file. Object layers, image layers, tile animations, and tile
+ properties are not carried over.
+
+A map no bigger than the screen pastes in one call. A bigger one -- a level
+the program shows a screen of at a time -- pastes a row of tiles a call,
+starting each row at its place in the whole map:
+
+[source,c]
+----
+for (row = 0; row < 25u; row++) {
+ jlTileMapPaste(jlStageGet(), 0, (uint8_t)row, 40, 1, tiles, &level[(top + row) * LEVEL1_WIDTH + left]);
+}
+----
+
+A level that scrolls is a tile layer over the whole map (see
+<<_scrolling_tile_layers>>). A layer is opaque and draws `JL_TILE_MAP_SKIP` as
+colour 0, so it scrolls one map layer; flatten the scenery into one layer in
+Tiled.
+
+[source,c]
+----
+layer = jlTileLayerCreate(tiles, level, LEVEL1_WIDTH, LEVEL1_HEIGHT, 0, 25);
+----
+
==== Baking Sprites
[source,console]
@@ -399,11 +465,12 @@ $(call programDataDir,MYGAME)/font.tbk: assets/font.png
----
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`.
+`.sfx` files are shared, as are the `.map` files `tiled2joey.py` writes:
+`ASSET_PLATFORM` names the target a bake is for (DVX and every hosted port --
+SDL3, the web, macOS, 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`),
diff --git a/docs/BuildIntegration.adoc b/docs/BuildIntegration.adoc
index 1162f1f..fc1ca61 100644
--- a/docs/BuildIntegration.adoc
+++ b/docs/BuildIntegration.adoc
@@ -29,6 +29,10 @@ The whole Makefile for a program with no data files:
JOEYLIB ?= ../joeylib
include $(JOEYLIB)/make/project.mk
+DVX_VENDOR := acme
+DVX_AUTHOR := Acme Games
+MACOS_ID_PREFIX := com.example.acme
+
PROGRAMS := MYGAME
MYGAME_SRCS := mygame.c world.c
MYGAME_TITLE := My Game
@@ -39,7 +43,16 @@ include $(JOEYLIB)/make/programs.mk
`make/project.mk` comes first and sets up the target being built; the
description comes next and may use what it set up (see <<_data_files>>);
-`make/programs.mk` comes last and makes the rules. Run make in the project's
+`make/programs.mk` comes last and makes the rules.
+
+The first three lines say whose the programs are, which only you can say:
+JoeyLib never makes it up, and building for DVX or macOS stops until they
+are set. `DVX_VENDOR` is the folder your programs install in under DVX's
+`APPS\` (1 to 8 letters, digits, or `_`; DVX names a package `vendor.name`)
+and `DVX_AUTHOR` the author DVX shows; `MACOS_ID_PREFIX`, a reverse-DNS name
+of yours, starts each Mac app's identifier, which ends in the program's name
+(`com.example.acme.MYGAME`). JoeyLib's own examples carry their author's
+(`kpunch`, `com.kangaroopunch.joeylib`). Run make in the project's
directory -- the paths in the Makefile are relative to it -- or with `make -C`:
[source,console]
@@ -71,11 +84,14 @@ with `MYGAME_IIGS_GAME_DISK` the disks `build/iigs/bin/MYGAME.2mg` and
(<<_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/acme/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), and `icon.png`
+|macOS |`build/macos/bin/MYGAME.app`, a universal app: the program in
+`Contents/MacOS/`, its `DATA/` and icon in `Contents/Resources/`; the program
+it is packed from in `build/macos/link/MYGAME`
|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
@@ -134,12 +150,12 @@ 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
-every program is packaged apart anyway).
+ST, DOS, X68000, SDL3, Win32, Win16; on the IIgs, DVX, the web, macOS, 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_TITLE` |DVX: its window title. PSP: its title in the PSP's menu.
+macOS: its app's display name (`CFBundleDisplayName`). By default its name.
|`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.
@@ -160,9 +176,9 @@ MYGAME_X68000_LDFLAGS := -s
MYGAME_IIGS_DATA := # nothing staged on the IIgs
----
-DVX's help and resource texts call a program "a `$(DVX_KIND)` program" and
-name `$(DVX_AUTHOR)` as its author (both `JoeyLib` by default); `DVX_VENDOR`
-(`joeylib`) is the folder it installs in under `APPS\`. Set them in the
+DVX's help and resource texts call a program "a `$(DVX_KIND)` program"
+(`JoeyLib` by default) and name `$(DVX_AUTHOR)` as its author; `DVX_VENDOR`
+is the folder it installs in under `APPS\` (<<_a_project>>). Set them in the
description.
=== Data Files
@@ -179,10 +195,10 @@ 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`, `haiku`, `haiku32`, `beos`.
+|`PLATFORM` |The target: `iigs`, `amiga`, `atarist`, `ste`, `dos`, `x68000`, `dvx`, `sdl3`, `web`, `macos`, `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, Haiku, BeOS) is the same chunky one, so they take
+web, macOS, 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
@@ -236,9 +252,10 @@ program's id:
|Amiga |Mixed case, no extension: the id Capitalised unless `ID_AMIGA_NAME`
says otherwise |`Mygame`
|Sharp X68000 |Uppercase 8.3, `.X` |`MYGAME.X`
-|DVX |Lowercase, 8 characters, a folder of its own |`apps/joeylib/mygame/mygame.app`
+|DVX |Lowercase, 8 characters, a folder of its own |`apps/acme/mygame/mygame.app`
|SDL3 |The id, no extension |`MYGAME`
|Web |The id, a folder of its own |`MYGAME/MYGAME.html`
+|macOS |The id, an app |`MYGAME.app`
|Win32 |Uppercase 8.3, `.EXE` |`MYGAME.EXE`
|Win16 |Uppercase 8.3, `.EXE` |`MYGAME.EXE`
|PSP |The id, a folder of its own holding `EBOOT.PBP` |`MYGAME/EBOOT.PBP`
@@ -276,10 +293,10 @@ needs to be on the disk.
`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
+`build/dvx/bin/apps/acme/mygame/` holds `mygame.app`, its help file
`mygame.hlp` (DVX gives every program one; the rules write a one-topic help
from `MYGAME_HELP`) and its `DATA/`. Copy that folder into a DVX installation's
-`APPS\JOEYLIB\` and start it from DVX, or let `joey-run` do it
+`APPS\ACME\` and start it from DVX, or let `joey-run` do it
(<<_dvx_programs>>).
*SDL3.* A program is one native executable with SDL 3 linked in statically,
@@ -299,6 +316,13 @@ 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.
+*macOS.* A program is its app, `build/macos/bin/MYGAME.app`, which holds
+everything it needs: the program (SDL 3 linked in, for Apple silicon and
+Intel Macs both), its `DATA/`, its icon, and the `Info.plist` that names
+them. Copy the app to a Mac, or zip it to send it; a Mac opens it from
+anywhere. Its identifier is `MACOS_ID_PREFIX` and its name
+(`com.example.acme.MYGAME`, <<_a_project>>).
+
*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 its icon and no
other pictures) and its `DATA/`. Copy the folder into `PSP/GAME/` on a memory
@@ -340,13 +364,19 @@ 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
+palette, in the program's resources, and on Haiku the same two as one vector
+icon, which draws each at its own size and the 32x32 cleanly at any larger
+one (an icon with too many colour patches for the vector format goes
+without). 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).
+|macOS |Finder's and the Dock's: `icon.icns` in the app's `Contents/Resources/`,
+16 to 128 pixels and twice that for Retina screens (up to 512, a picture that
+big or pixel art).
|Web |The page's: `icon.png`, 32x32, beside it.
|===
diff --git a/docs/GettingStarted.adoc b/docs/GettingStarted.adoc
index c0f054e..f6f3715 100644
--- a/docs/GettingStarted.adoc
+++ b/docs/GettingStarted.adoc
@@ -5,7 +5,8 @@
JoeyLib cross-compiles for five machines and for DVX, a DOS desktop whose
programs run in windows (see <<_dvx_programs>>), builds native programs for
the machine you work on through SDL3 (see <<_sdl3_programs>>), builds web
-pages through Emscripten (see <<_web_programs>>), and builds PSP homebrew
+pages through Emscripten (see <<_web_programs>>), builds Mac apps through
+zig and Apple's SDK (see <<_macos_programs>>), and builds PSP homebrew
through pspdev (see <<_psp_programs>>). It builds on Linux x86-64,
and on Windows inside WSL2. One script installs every cross toolchain, pinned
to a known-good version and checked against a published checksum, into
@@ -111,12 +112,13 @@ A target is a component name from `list`, or a group:
| Everything needed to build for one port: `iigs`, `amiga`, `atarist`, `dos`,
`x68000`, `dvx`, `sdl3` (SDL 3, built as a static library that every SDL3
program links in, so it runs without SDL installed), `web` (Emscripten,
-through emsdk, and SDL 3 built with it), `win32` (llvm-mingw; its
-emulator is a portable Wine), `win16` (Open Watcom; its emulator is Windows
-for Workgroups 3.11 in DOSBox Staging, set up from your own install disc, see
-<>) or `psp` (pspdev's prebuilt release,
-and SDL 3 built with it; its emulator is PPSSPP's headless build, patched for
-the gates).
+through emsdk, and SDL 3 built with it), `macos` (zig, Apple's macOS SDK,
+and SDL 3 built with them; there is no emulator: nothing here runs Mac
+programs), `win32` (llvm-mingw; its emulator is a portable Wine), `win16`
+(Open Watcom; its emulator is Windows for Workgroups 3.11 in DOSBox Staging,
+set up from your own install disc, see <>),
+or `psp` (pspdev's prebuilt release, and SDL 3 built with it; its emulator
+is PPSSPP's headless build, patched for the gates).
| `emulator:PORT`
| Everything needed to run that port's programs: the emulator and its
@@ -214,6 +216,7 @@ $ make x68000 # Sharp X68000 only
$ make dvx # DVX only
$ make sdl3 # native programs through SDL3 only
$ make web # web pages through Emscripten only
+$ make macos # universal Mac apps only
$ make win32 # Windows programs only
$ make win16 # Windows 3.x programs only
$ make psp # PSP homebrew (EBOOT.PBP) only
@@ -457,6 +460,12 @@ server. With `--headless` it runs in headless Chromium instead, at one canvas
pixel per stage pixel, so a `--snapshot` is the stage. Its `DATA/` is packed
in when it is built, so `--data` does not apply.
+==== macOS
+
+Nothing here runs a Mac program: copy its app, `build/macos/bin/NAME.app`, to
+a Mac and open it there (<<_macos_programs>>). `joey-run` knows one when it
+sees it and says so.
+
==== Win32
A Windows program runs under Wine: in a window on your display, with its
@@ -665,6 +674,7 @@ built for every platform:
|`keys` |Typed-character input and the key predicates.
|`joy` |Joystick axes, buttons, and recalibration.
|`sprite` |Sprite loading, compiling, save-under, and restore.
+|`scroll` |A scrolling tile layer under a sprite, with a frame-rate bar.
|`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.
@@ -679,9 +689,10 @@ game, and JoeyAGI, an interpreter for Sierra's AGI adventures. Each builds
from its own directory against a JoeyLib checkout, as any program of yours
would (<<_building_your_program>>).
-On DVX every example is a folder of its own under `build/dvx/bin/apps/joeylib/`
+On DVX every example is a folder of its own under `build/dvx/bin/apps/kpunch/`
with its help file (<<_dvx_programs>>). On SDL3 every example is a native
program in `build/sdl3/bin/`, beside the shared `DATA/` (<<_sdl3_programs>>),
on the web a page in a folder of its own, `build/web/bin/NAME/`
-(<<_web_programs>>), and on the PSP a folder for `PSP/GAME/`,
+(<<_web_programs>>), on macOS an app, `build/macos/bin/NAME.app`, with its
+`DATA/` inside (<<_macos_programs>>), and on the PSP a folder for `PSP/GAME/`,
`build/psp/bin/NAME/` with `EBOOT.PBP` and its `DATA/` (<<_psp_programs>>).
diff --git a/docs/Portability.adoc b/docs/Portability.adoc
index a64b238..2f37eab 100644
--- a/docs/Portability.adoc
+++ b/docs/Portability.adoc
@@ -222,7 +222,7 @@ The API is uniform; these are the places where what you get differs.
[cols="2,2,2,2,2,2,2,2,2", options="header"]
|===
-|Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |DVX |SDL3, Web, Win32, PSP, Haiku, BeOS |Win16
+|Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |DVX |SDL3, Web, macOS, Win32, PSP, Haiku, BeOS |Win16
|Frame rate
|60 Hz (50 on a PAL machine)
@@ -271,8 +271,8 @@ The API is uniform; these are the places where what you get differs.
|none
|none
|Program Manager's
-|the window's (SDL3), the page's (web), Explorer's (Win32), the menu's
-(PSP), Tracker's (Haiku, BeOS)
+|the window's (SDL3), the page's (web), Finder's and the Dock's (macOS),
+Explorer's (Win32), the menu's (PSP), Tracker's (Haiku, BeOS)
|Program Manager's
|Tone voices (`jlAudioTone` is voice 0)
@@ -292,7 +292,7 @@ The API is uniform; these are the places where what you get differs.
|analog
|digital
|analog (DVX calibrates)
-|SDL's gamepads and joysticks (SDL3, web); Windows' joysticks (Win32); the system's joysticks
+|SDL's gamepads and joysticks (SDL3, web, macOS); Windows' joysticks (Win32); the system's joysticks
(Haiku, BeOS); the PSP's own controls
|Windows' joysticks (Control Panel calibrates)
@@ -313,7 +313,7 @@ The API is uniform; these are the places where what you get differs.
|COM1..COM4
|RS-232C (needs RSDRV.SYS)
|COM1..COM4 (DVX's library)
-|Win32: COM1 and up; SDL3 (Linux), Haiku, BeOS: terminal devices; web, PSP: none
+|Win32: COM1 and up; SDL3 (Linux), macOS, Haiku, BeOS: terminal devices; web, PSP: none
|COM1..COM9
|===
@@ -612,6 +612,9 @@ names the ports, says how they differ, and gives the reason.
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.
+* *macOS: saves in Application Support.* An app's own folder may not be
+ writable, so `SAVES/` and the log go in the user's
+ `~/Library/Application Support/NAME/` (<<_macos_programs>>).
[[portsDifferMemory]]
==== Sprites and Memory
@@ -705,9 +708,9 @@ Nothing in the API differs between them:
|Fills and copies
|The CPU
-|The BLiTTER, for fills of 8 or more rows (24 for one 16-pixel column) and
- copies of 4 or more rows; smaller work stays on the CPU, which is faster
- there
+|The BLiTTER, for fills of 8 or more rows (24 for one 16-pixel column),
+ copies of 4 or more rows, and a tile layer's band; smaller work stays on
+ the CPU, which is faster there
|CPU clock
|As the machine is (8 MHz; TOS's setting on a Mega STe)
@@ -788,7 +791,7 @@ hashes are DOS's -- and the rest behaves like this:
* *Files.* DVX's programs share one current directory, so JoeyLib roots its
files at run time: `DATA/` is found beside the program (its folder under
`APPS`), while `SAVES/` and `joeylog.txt` go to the program's own settings
- folder, `CONFIG\APPS\JOEYLIB\NAME`, which DVX keeps apart from the program
+ folder, `CONFIG\APPS\VENDOR\NAME`, which DVX keeps apart from the program
files so a reinstall never overwrites them.
* *Sound* goes through DVX's sound and music libraries, mixed with every
other program's: `.MOD` music, SFX in stereo, and the tone voices and the
@@ -918,6 +921,41 @@ are DOS's here too. Everything in <<_sdl3_programs>> holds, except:
`make web` builds every example as a page, and `./joey-run --port web NAME`
serves its folder on this machine and opens it in your browser.
+=== macOS Programs
+
+The macOS port is the SDL3 port built as Mac apps, on Linux: the same hosted
+core, the same backend, the same stage (UBER's hashes are DOS's). It is
+compiled with zig against Apple's macOS SDK, once for Apple silicon and once
+for Intel, the two joined into one universal program that runs on either,
+on macOS 13 (Ventura) or newer, with SDL 3 built the same way and linked in
+(the `macos` installer components: zig, the SDK, and SDL 3). Everything in
+<<_sdl3_programs>> holds, except:
+
+* *An app.* A program is `build/macos/bin/NAME.app`: the program in
+ `Contents/MacOS/`, its `DATA/` and its icon in `Contents/Resources/`, which
+ is its folder and the current directory, and the `Info.plist` that names
+ them. Copy the app to a Mac, or zip it to send it; it opens from anywhere.
+* *Saves in Application Support.* `SAVES/` and `joeylog.txt` go in
+ `~/Library/Application Support/NAME/`, not beside the program: an app in
+ `/Applications` may not be writable by its user, and macOS runs a
+ downloaded app from a read-only copy until it is moved. `jlDiskFree` is
+ the free space there.
+* *Retina screens.* The window draws at the screen's own resolution, so the
+ stage is scaled up once, sharply, and not blurred again by the system.
+* *Serial* through termios, as on Linux: a Mac has no serial port of its
+ own, so the ports are USB adapters' call-out devices (`/dev/cu.*`, the
+ Mac's own Bluetooth and debug devices left out), in name order, or the
+ device `JOEYLIB_SERIAL` names.
+* *Untested on a Mac.* Nothing here runs Mac programs: `make verify` builds
+ every example and checks every app is one macOS opens -- both processors'
+ programs, the Apple silicon one signed, for macOS 13 on, linking only the
+ system's own libraries, with its `Info.plist` and icon
+ (`scripts/check-macos-apps.sh`) -- but what an app does on a Mac has not
+ been seen yet.
+
+`make macos` builds every example as an app; `./joey-run` recognises one
+and says it runs on a Mac.
+
=== PSP Programs
The PSP port is the SDL3 port built as PSP homebrew: the same hosted core
diff --git a/docs/ProgramLayout.adoc b/docs/ProgramLayout.adoc
index 6f6d498..7c50777 100644
--- a/docs/ProgramLayout.adoc
+++ b/docs/ProgramLayout.adoc
@@ -199,7 +199,7 @@ This compiles and runs on every target as-is.
#define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel
#define MAX_STEP_MILLIS 100 // after a stall, move no further than this
-static jlSurfaceT *gStage;
+static jlSurfaceT *jlgStage;
static bool gRunning = true;
static int32_t gPlayerX = 160L << SUBPIXEL_SHIFT;
static int32_t gPlayerY = 100L << SUBPIXEL_SHIFT;
@@ -223,8 +223,8 @@ static void buildPalette(void) {
colors[2] = 0x0C0; // green
colors[3] = 0x00C; // blue
- jlPaletteSet(gStage, 0, colors);
- jlScbSetRange(gStage, 0, JL_SURFACE_HEIGHT - 1, 0);
+ jlPaletteSet(jlgStage, 0, colors);
+ jlScbSetRange(jlgStage, 0, JL_SURFACE_HEIGHT - 1, 0);
}
@@ -278,9 +278,9 @@ static void handleInput(int32_t step) {
static void renderFrame(void) {
- jlSurfaceClear(gStage, 0);
- 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);
+ jlSurfaceClear(jlgStage, 0);
+ jlDrawRect(jlgStage, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, 1);
+ jlFillRect(jlgStage, (int16_t)(gPlayerX >> SUBPIXEL_SHIFT), (int16_t)(gPlayerY >> SUBPIXEL_SHIFT), PLAYER_SIZE, PLAYER_SIZE, 2);
}
@@ -308,8 +308,8 @@ int main(void) {
return 1;
}
- gStage = jlStageGet();
- if (gStage == NULL) {
+ jlgStage = jlStageGet();
+ if (jlgStage == NULL) {
jlShutdown();
return 1;
}
diff --git a/examples/adventure/adventure.c b/examples/adventure/adventure.c
index 19b1aeb..31c18a8 100644
--- a/examples/adventure/adventure.c
+++ b/examples/adventure/adventure.c
@@ -206,7 +206,7 @@ typedef struct {
* Section 2 -- Globals (game state)
* =================================================================== */
-static jlSurfaceT *gScreen;
+static jlSurfaceT *jlgScreen;
static jlSurfaceT *gBgScene[ROOM_COUNT]; // pristine background per room
static RoomE gCurRoom;
@@ -1985,7 +1985,7 @@ static void drawTextLine(int16_t x, int16_t y, const char *text, uint8_t color)
bits = kFontGlyph[idx][row];
for (col = 0; col < FONT_W; col++) {
if (bits & (0x80 >> col)) {
- jlDrawPixel(gScreen, (int16_t)(cx + col), (int16_t)(y + row), color);
+ jlDrawPixel(jlgScreen, (int16_t)(cx + col), (int16_t)(y + row), color);
}
}
}
@@ -2006,8 +2006,8 @@ static void drawTextLine(int16_t x, int16_t y, const char *text, uint8_t color)
* =================================================================== */
static void drawMessageBar(void) {
- jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, MSG_BAR_H, UI_BG);
- jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM);
+ jlFillRect(jlgScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, MSG_BAR_H, UI_BG);
+ jlFillRect(jlgScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM);
if (gMessage[0] != '\0') {
drawTextLine(4, MSG_BAR_Y + 2, gMessage, UI_INK);
}
@@ -2019,8 +2019,8 @@ static void drawInventoryBar(void) {
int n;
int16_t x;
- jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, INV_BAR_H, UI_BG);
- jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM);
+ jlFillRect(jlgScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, INV_BAR_H, UI_BG);
+ jlFillRect(jlgScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM);
// Verb readout at the left.
snprintf(buf, sizeof(buf), "VERB: %s", verbName(gCurVerb));
@@ -2032,12 +2032,12 @@ static void drawInventoryBar(void) {
if (gHaveItem[ITEM_LAMP]) {
jlSpriteT *sp;
sp = gLampIsLit ? gLampLitSprite : gItemSprite[ITEM_LAMP];
- jlSpriteDraw(gScreen, sp, x, INV_BAR_Y + 2);
+ jlSpriteDraw(jlgScreen, sp, x, INV_BAR_Y + 2);
x = (int16_t)(x - (ITEM_ICON_W + 2));
n++;
}
if (gHaveItem[ITEM_KEY]) {
- jlSpriteDraw(gScreen, gItemSprite[ITEM_KEY], x, INV_BAR_Y + 2);
+ jlSpriteDraw(jlgScreen, gItemSprite[ITEM_KEY], x, INV_BAR_Y + 2);
x = (int16_t)(x - (ITEM_ICON_W + 2));
n++;
}
@@ -2063,10 +2063,10 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) {
uint8_t i;
// 1. background
- jlSurfaceCopy(gScreen, gBgScene[gCurRoom]);
+ jlSurfaceCopy(jlgScreen, gBgScene[gCurRoom]);
// 2. ego
- jlSpriteDraw(gScreen, gEgo[gEgoDir][gEgoFrameIdx], gEgoX, gEgoY);
+ jlSpriteDraw(jlgScreen, gEgo[gEgoDir][gEgoFrameIdx], gEgoX, gEgoY);
// 3. prop priority redraw
r = &gRooms[gCurRoom];
@@ -2078,7 +2078,7 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) {
continue;
}
if ((p->y + p->h) > egoBottom) {
- jlSpriteDraw(gScreen, p->sp, p->x, p->y);
+ jlSpriteDraw(jlgScreen, p->sp, p->x, p->y);
}
}
@@ -2100,7 +2100,7 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) {
cs = gCursor[gCurVerb];
}
if (cs != NULL) {
- jlSpriteDraw(gScreen, cs, cx, cy);
+ jlSpriteDraw(jlgScreen, cs, cx, cy);
}
}
@@ -2357,7 +2357,7 @@ static void enterRoom(RoomE room, bool fromEast, bool fromWest) {
gPathLen = 0;
gPathStep = 0;
- programSkyScb(gScreen, room);
+ programSkyScb(jlgScreen, room);
}
@@ -2413,15 +2413,15 @@ int main(void) {
return 1;
}
- gScreen = jlStageGet();
- if (gScreen == NULL) {
+ jlgScreen = jlStageGet();
+ if (jlgScreen == NULL) {
fprintf(stderr, "jlStageGet returned NULL\n");
jlShutdown();
return 1;
}
- buildPalettes(gScreen);
- programSkyScb(gScreen, ROOM_FOREST);
+ buildPalettes(jlgScreen);
+ programSkyScb(jlgScreen, ROOM_FOREST);
if (!buildAllSprites()) {
fprintf(stderr, "buildAllSprites failed: %s\n", jlLastError());
diff --git a/examples/adventure2/adventure2.c b/examples/adventure2/adventure2.c
index 5427715..57705ce 100644
--- a/examples/adventure2/adventure2.c
+++ b/examples/adventure2/adventure2.c
@@ -180,7 +180,7 @@ typedef enum {
* Section 2 -- Globals
* =================================================================== */
-static jlSurfaceT *gScreen;
+static jlSurfaceT *jlgScreen;
static jlSurfaceT *gVisual; // room background -- "picture" buffer
static jlSurfaceT *gPriority; // per-pixel priority codes
static jlSurfaceT *gControl; // per-pixel control codes
@@ -634,7 +634,7 @@ static void drawText(int16_t x, int16_t y, const char *text, uint8_t color) {
bits = kFontGlyph[idx][row];
for (col = 0; col < FONT_W; col++) {
if (bits & (0x80 >> col)) {
- jlDrawPixel(gScreen, (int16_t)(cx + col), (int16_t)(y + row), color);
+ jlDrawPixel(jlgScreen, (int16_t)(cx + col), (int16_t)(y + row), color);
}
}
}
@@ -736,10 +736,10 @@ static void buildPalettes(void) {
ui[UI_HILITE] = rgb(15, 13, 3);
ui[UI_DIM] = rgb(7, 7, 8);
- jlPaletteSet(gScreen, PAL_SCENE, scene);
- jlPaletteSet(gScreen, PAL_DEBUG_PRI, pri);
- jlPaletteSet(gScreen, PAL_DEBUG_CTRL, ctl);
- jlPaletteSet(gScreen, PAL_UI, ui);
+ jlPaletteSet(jlgScreen, PAL_SCENE, scene);
+ jlPaletteSet(jlgScreen, PAL_DEBUG_PRI, pri);
+ jlPaletteSet(jlgScreen, PAL_DEBUG_CTRL, ctl);
+ jlPaletteSet(jlgScreen, PAL_UI, ui);
}
@@ -763,10 +763,10 @@ static void programScbForView(DebugViewE view) {
break;
}
for (line = 0; line < PLAY_AREA_H; line++) {
- jlScbSet(gScreen, (uint16_t)line, playPal);
+ jlScbSet(jlgScreen, (uint16_t)line, playPal);
}
for (line = PLAY_AREA_H; line < JL_SURFACE_HEIGHT; line++) {
- jlScbSet(gScreen, (uint16_t)line, PAL_UI);
+ jlScbSet(jlgScreen, (uint16_t)line, PAL_UI);
}
}
@@ -981,7 +981,7 @@ static void drawEgoMasked(int16_t x, int16_t y, DirE dir, uint8_t frame) {
// Picture pixel is in front. Skip the actor pixel.
continue;
}
- jlDrawPixel(gScreen, px, py, color);
+ jlDrawPixel(jlgScreen, px, py, color);
}
}
}
@@ -1323,15 +1323,15 @@ static void drawCursor(int16_t mx, int16_t my, VerbE v) {
if (color == C_TRANS) {
continue;
}
- jlDrawPixel(gScreen, px, py, color);
+ jlDrawPixel(jlgScreen, px, py, color);
}
}
}
static void drawMessageBar(void) {
- jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, MSG_BAR_H, UI_BG);
- jlFillRect(gScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM);
+ jlFillRect(jlgScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, MSG_BAR_H, UI_BG);
+ jlFillRect(jlgScreen, 0, MSG_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM);
if (gMessage[0] != '\0') {
drawText(4, (int16_t)(MSG_BAR_Y + 2), gMessage, UI_INK);
}
@@ -1342,8 +1342,8 @@ static void drawInventoryBar(void) {
char buf[40];
const char *vw;
- jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, INV_BAR_H, UI_BG);
- jlFillRect(gScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM);
+ jlFillRect(jlgScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, INV_BAR_H, UI_BG);
+ jlFillRect(jlgScreen, 0, INV_BAR_Y, JL_SURFACE_WIDTH, 1, UI_DIM);
snprintf(buf, sizeof(buf), "VERB: %s", verbName(gCurVerb));
drawText(4, (int16_t)(INV_BAR_Y + 2), buf, UI_HILITE);
@@ -1368,7 +1368,7 @@ static void drawInventoryBar(void) {
* =================================================================== */
static void composeFrameNormal(int16_t mx, int16_t my) {
- jlSurfaceCopy(gScreen, gVisual);
+ jlSurfaceCopy(jlgScreen, gVisual);
drawEgoMasked(gEgoX, gEgoY, gEgoDir, gEgoFrameIdx);
drawMessageBar();
drawInventoryBar();
@@ -1381,7 +1381,7 @@ static void composeFrameDebug(int16_t mx, int16_t my) {
/* The debug overlays blit the raw priority/control buffer as the
* play-area picture; the palette swap (PAL_DEBUG_PRI or
* PAL_DEBUG_CTRL) makes the codes visible. */
- jlSurfaceCopy(gScreen, (gDebugView == VIEW_PRIORITY) ? gPriority : gControl);
+ jlSurfaceCopy(jlgScreen, (gDebugView == VIEW_PRIORITY) ? gPriority : gControl);
/* Show where the actor is by overlaying ego pixels in pure white
* with priority always-on. */
{
@@ -1404,7 +1404,7 @@ static void composeFrameDebug(int16_t mx, int16_t my) {
if (pixCharToColor(rows[r][c]) == C_TRANS) {
continue;
}
- jlDrawPixel(gScreen, px, py, 15);
+ jlDrawPixel(jlgScreen, px, py, 15);
}
}
}
@@ -1509,8 +1509,8 @@ int main(void) {
return 1;
}
- gScreen = jlStageGet();
- if (gScreen == NULL) {
+ jlgScreen = jlStageGet();
+ if (jlgScreen == NULL) {
fprintf(stderr, "jlStageGet returned NULL\n");
jlShutdown();
return 1;
diff --git a/examples/scroll/scroll.c b/examples/scroll/scroll.c
new file mode 100644
index 0000000..0bdff79
--- /dev/null
+++ b/examples/scroll/scroll.c
@@ -0,0 +1,344 @@
+// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
+// See LICENSE.md for license information.
+// SPDX-License-Identifier: MIT
+//
+// Scroll demo: a tile layer (jlTileLayer*) over a landscape three screens
+// wide and two tall, with a ball drawn over it every frame -- no
+// save-under, since the next layer draw covers it. The arrow keys or a
+// joystick move the view; left alone it drifts. The two tile rows under
+// the layer are a status bar whose bar grows with the frames drawn a
+// second, so the demo shows how fast each port scrolls. Escape ends it.
+//
+// The tiles are painted with the drawing calls and snapped, since a
+// jlTileT is in each port's own format; the map is generated.
+
+#include
+
+#include
+
+#define MAP_W 96
+#define MAP_H 40
+#define VIEW_ROWS 23 // tile rows of the layer
+#define BAR_TOP (VIEW_ROWS * JL_TILE_PIXELS_PER_SIDE)
+#define BAR_H (JL_SURFACE_HEIGHT - BAR_TOP)
+#define BAR_PIXELS_PER_FPS 5 // the bar's growth for each frame a second
+#define BAR_INSET 4
+
+#define TILE_SKY 0
+#define TILE_GRASS 1
+#define TILE_DIRT 2
+#define TILE_BRICK 3
+#define TILE_WATER 4
+#define TILE_CLOUD 5
+#define TILE_STONE 6
+#define TILE_FLOWER 7
+#define TILE_COUNT 8
+
+#define COLOR_SKY 1
+#define COLOR_DIRT 2
+#define COLOR_GRASS 3
+#define COLOR_DIRT_DARK 4
+#define COLOR_BRICK 5
+#define COLOR_GREY 6
+#define COLOR_WATER 7
+#define COLOR_FOAM 8
+#define COLOR_CLOUD 9
+#define COLOR_GREY_DARK 10
+#define COLOR_PETAL 11
+#define COLOR_BALL 12
+#define COLOR_SHINE 13
+#define COLOR_BAR_BACK 14
+#define COLOR_BAR 15
+
+#define GROUND_LOW 34 // the ground's highest and lowest rows
+#define GROUND_HIGH 22
+#define WATER_LINE 31 // ground below this row floods
+#define STONE_DEPTH 4 // dirt rows above the stone
+#define LEDGES 30 // brick ledges and clouds, turn about
+#define LEDGE_MIN 2 // their length in tiles
+#define LEDGE_MAX 6
+#define LEDGE_TOP 4 // their highest row
+#define LEDGE_GAP 4 // rows kept clear above the highest ground
+
+#define BALL_TILES 2
+#define BALL_SIDE (BALL_TILES * JL_TILE_PIXELS_PER_SIDE)
+#define BALL_X ((JL_SURFACE_WIDTH - BALL_SIDE) / 2)
+#define BALL_Y ((BAR_TOP - BALL_SIDE) / 2)
+
+#define SPEED 90 // pixels per second, the same on every port
+#define DRIFT_X 40
+#define DRIFT_Y 12
+#define STICK_THRESHOLD 32
+#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 MAP_PX_W ((int32_t)MAP_W * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT)
+#define MAP_PX_H ((int32_t)MAP_H * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT)
+#define CODEGEN_BYTES (32UL * 1024)
+
+static const uint16_t gPalette[16] = {
+ 0x000, 0x6AF, 0x852, 0x3B3, 0x531, 0xB33, 0x999, 0x24C,
+ 0x8CF, 0xFFF, 0x555, 0xFC2, 0xFD0, 0xFFF, 0x113, 0x4E4
+};
+
+// The ball, 16x16 at 4 bits a pixel, high nibble left; colour 0 is clear.
+static const uint8_t gBallRows[BALL_SIDE * BALL_SIDE / 2] = {
+ 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00,
+ 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x00,
+ 0x0C, 0xCC, 0xDC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0,
+ 0x0C, 0xCD, 0xDC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0,
+ 0xCC, 0xDD, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
+ 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
+ 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
+ 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
+ 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
+ 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
+ 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
+ 0x0C, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0,
+ 0x0C, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0,
+ 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x00,
+ 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xCC, 0xCC, 0x00, 0x00, 0x00
+};
+
+static jlTileT gTiles[TILE_COUNT];
+static uint8_t gMap[MAP_W * MAP_H];
+static uint8_t gBallTiles[BALL_TILES * BALL_TILES * JL_TILE_BYTES];
+static uint32_t gSeed = 2026u;
+
+static void drawBar(jlSurfaceT *screen, uint16_t framesPerSecond);
+static void makeBall(void);
+static void makeMap(void);
+static void makeTiles(jlSurfaceT *screen);
+static uint16_t nextRandom(uint16_t below);
+static int32_t stickAxis(int8_t stick, bool minus, bool plus, int32_t drift);
+static int32_t wrap(int32_t v, int32_t size);
+
+
+// The status bar under the layer: its bar is BAR_PIXELS_PER_FPS a frame a second.
+static void drawBar(jlSurfaceT *screen, uint16_t framesPerSecond) {
+ uint16_t w;
+
+ w = (uint16_t)(framesPerSecond * BAR_PIXELS_PER_FPS);
+ if (w > JL_SURFACE_WIDTH - 2 * BAR_INSET) {
+ w = JL_SURFACE_WIDTH - 2 * BAR_INSET;
+ }
+ jlFillRect(screen, 0, BAR_TOP, JL_SURFACE_WIDTH, BAR_H, COLOR_BAR_BACK);
+ if (w > 0) {
+ jlFillRect(screen, BAR_INSET, BAR_TOP + BAR_INSET, w, BAR_H - 2 * BAR_INSET, COLOR_BAR);
+ }
+}
+
+
+// The ball's rows, repacked into the tile-major layout jlSpriteCreate takes.
+static void makeBall(void) {
+ uint16_t tx;
+ uint16_t ty;
+ uint16_t row;
+ uint16_t b;
+
+ for (ty = 0; ty < BALL_TILES; ty++) {
+ for (tx = 0; tx < BALL_TILES; tx++) {
+ for (row = 0; row < JL_TILE_PIXELS_PER_SIDE; row++) {
+ for (b = 0; b < JL_TILE_BYTES_PER_ROW; b++) {
+ gBallTiles[(ty * BALL_TILES + tx) * JL_TILE_BYTES + row * JL_TILE_BYTES_PER_ROW + b] = gBallRows[(ty * JL_TILE_PIXELS_PER_SIDE + row) * (BALL_SIDE / 2) + tx * JL_TILE_BYTES_PER_ROW + b];
+ }
+ }
+ }
+ }
+}
+
+
+// Rolling ground with flooded hollows and flowers, and brick ledges and
+// clouds above it.
+static void makeMap(void) {
+ uint16_t tx;
+ uint16_t ty;
+ uint16_t ground;
+ uint16_t len;
+ uint16_t i;
+ uint16_t k;
+ uint8_t t;
+
+ ground = GROUND_HIGH + 4;
+ for (tx = 0; tx < MAP_W; tx++) {
+ if (nextRandom(3) == 0 && ground < GROUND_LOW) {
+ ground++;
+ } else if (nextRandom(3) == 0 && ground > GROUND_HIGH) {
+ ground--;
+ }
+ for (ty = 0; ty < MAP_H; ty++) {
+ if (ty < ground) {
+ t = (ty >= WATER_LINE) ? TILE_WATER : TILE_SKY;
+ } else if (ty == ground) {
+ t = (ty >= WATER_LINE) ? TILE_STONE : TILE_GRASS;
+ } else if (ty < ground + STONE_DEPTH) {
+ t = TILE_DIRT;
+ } else {
+ t = TILE_STONE;
+ }
+ gMap[ty * MAP_W + tx] = t;
+ }
+ if (ground < WATER_LINE && nextRandom(5) == 0) {
+ gMap[(ground - 1) * MAP_W + tx] = TILE_FLOWER;
+ }
+ }
+ for (i = 0; i < LEDGES; i++) {
+ tx = nextRandom(MAP_W - LEDGE_MAX);
+ ty = (uint16_t)(LEDGE_TOP + nextRandom(GROUND_HIGH - LEDGE_TOP - LEDGE_GAP));
+ len = (uint16_t)(LEDGE_MIN + nextRandom(LEDGE_MAX - LEDGE_MIN));
+ for (k = 0; k < len; k++) {
+ gMap[ty * MAP_W + tx + k] = ((i & 1u) != 0u) ? TILE_BRICK : TILE_CLOUD;
+ }
+ }
+}
+
+
+// Paints the eight tiles in block row 0 of the screen and snaps them.
+static void makeTiles(jlSurfaceT *screen) {
+ jlFillRect(screen, 0, 0, 8, 8, COLOR_SKY);
+ jlFillRect(screen, 8, 0, 8, 8, COLOR_DIRT);
+ jlFillRect(screen, 8, 0, 8, 3, COLOR_GRASS);
+ jlDrawPixel(screen, 10, 3, COLOR_GRASS);
+ jlDrawPixel(screen, 13, 3, COLOR_GRASS);
+ jlFillRect(screen, 16, 0, 8, 8, COLOR_DIRT);
+ jlDrawPixel(screen, 17, 2, COLOR_DIRT_DARK);
+ jlDrawPixel(screen, 21, 5, COLOR_DIRT_DARK);
+ jlFillRect(screen, 24, 0, 8, 8, COLOR_BRICK);
+ jlDrawLine(screen, 24, 3, 31, 3, COLOR_GREY);
+ jlDrawLine(screen, 24, 7, 31, 7, COLOR_GREY);
+ jlDrawLine(screen, 24, 0, 24, 2, COLOR_GREY);
+ jlDrawLine(screen, 28, 4, 28, 6, COLOR_GREY);
+ jlFillRect(screen, 32, 0, 8, 8, COLOR_WATER);
+ jlDrawLine(screen, 32, 1, 34, 1, COLOR_FOAM);
+ jlDrawLine(screen, 36, 5, 38, 5, COLOR_FOAM);
+ jlFillRect(screen, 40, 0, 8, 8, COLOR_SKY);
+ jlFillRect(screen, 41, 3, 6, 3, COLOR_CLOUD);
+ jlFillRect(screen, 43, 2, 3, 1, COLOR_CLOUD);
+ jlFillRect(screen, 48, 0, 8, 8, COLOR_GREY);
+ jlDrawPixel(screen, 50, 2, COLOR_GREY_DARK);
+ jlDrawPixel(screen, 53, 5, COLOR_GREY_DARK);
+ jlDrawPixel(screen, 49, 6, COLOR_GREY_DARK);
+ jlFillRect(screen, 56, 0, 8, 8, COLOR_SKY);
+ jlDrawLine(screen, 59, 3, 59, 7, COLOR_GRASS);
+ jlFillRect(screen, 58, 1, 3, 2, COLOR_PETAL);
+ jlTileSnap(screen, TILE_SKY, 0, &gTiles[TILE_SKY]);
+ jlTileSnap(screen, TILE_GRASS, 0, &gTiles[TILE_GRASS]);
+ jlTileSnap(screen, TILE_DIRT, 0, &gTiles[TILE_DIRT]);
+ jlTileSnap(screen, TILE_BRICK, 0, &gTiles[TILE_BRICK]);
+ jlTileSnap(screen, TILE_WATER, 0, &gTiles[TILE_WATER]);
+ jlTileSnap(screen, TILE_CLOUD, 0, &gTiles[TILE_CLOUD]);
+ jlTileSnap(screen, TILE_STONE, 0, &gTiles[TILE_STONE]);
+ jlTileSnap(screen, TILE_FLOWER, 0, &gTiles[TILE_FLOWER]);
+}
+
+
+// A number in [0, below), from a fixed sequence so every port builds the
+// same landscape.
+static uint16_t nextRandom(uint16_t below) {
+ gSeed = gSeed * 1103515245u + 12345u;
+ return (uint16_t)((gSeed >> 16) % below);
+}
+
+
+// The view's speed along one axis, pixels per second: the stick or the two
+// keys, else the drift.
+static int32_t stickAxis(int8_t stick, bool minus, bool plus, int32_t drift) {
+ if (minus || stick < -STICK_THRESHOLD) {
+ return -SPEED;
+ }
+ if (plus || stick > STICK_THRESHOLD) {
+ return SPEED;
+ }
+ return drift;
+}
+
+
+// v moved into [0, size).
+static int32_t wrap(int32_t v, int32_t size) {
+ while (v < 0) {
+ v += size;
+ }
+ while (v >= size) {
+ v -= size;
+ }
+ return v;
+}
+
+
+int main(void) {
+ jlConfigT config;
+ jlSurfaceT *screen;
+ jlSpriteT *ball;
+ jlTileLayerT *layer;
+ int32_t x; // the view's origin, 1/256 of a pixel
+ int32_t y;
+ int32_t vx; // pixels per second
+ int32_t vy;
+ uint32_t lastMillis;
+ uint32_t barMillis;
+ uint32_t now;
+ uint32_t elapsed;
+ uint16_t frames;
+
+ config.codegenBytes = CODEGEN_BYTES;
+ config.noAudio = true;
+ if (!jlInit(&config)) {
+ fprintf(stderr, "jlInit failed: %s\n", jlLastError());
+ return 1;
+ }
+ screen = jlStageGet();
+ jlPaletteSet(screen, 0, gPalette);
+ jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
+ makeTiles(screen);
+ makeMap();
+ makeBall();
+ ball = jlSpriteCreate(gBallTiles, BALL_TILES, BALL_TILES);
+ layer = jlTileLayerCreate(gTiles, gMap, MAP_W, MAP_H, 0, VIEW_ROWS);
+ if (ball == NULL || layer == NULL) {
+ fprintf(stderr, "out of memory\n");
+ jlShutdown();
+ return 1;
+ }
+ (void)jlSpriteCompile(ball);
+ jlSurfaceClear(screen, 0);
+ drawBar(screen, 0);
+
+ x = 0;
+ y = (int32_t)(GROUND_HIGH - VIEW_ROWS / 2) * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT;
+ frames = 0;
+ lastMillis = jlMillisElapsed();
+ barMillis = lastMillis;
+ for (;;) {
+ jlInputPoll();
+ if (jlKeyPressed(JL_KEY_ESCAPE)) {
+ break;
+ }
+ vx = stickAxis(jlJoystickX(JL_JOYSTICK_1), jlKeyDown(JL_KEY_LEFT), jlKeyDown(JL_KEY_RIGHT), DRIFT_X);
+ vy = stickAxis(jlJoystickY(JL_JOYSTICK_1), jlKeyDown(JL_KEY_UP), jlKeyDown(JL_KEY_DOWN), DRIFT_Y);
+
+ now = jlMillisElapsed();
+ elapsed = now - lastMillis;
+ lastMillis = now;
+ if (elapsed > MAX_STEP_MILLIS) {
+ elapsed = MAX_STEP_MILLIS;
+ }
+ x = wrap(x + (vx * (int32_t)elapsed << SUBPIXEL_SHIFT) / MILLIS_PER_SECOND, MAP_PX_W);
+ y = wrap(y + (vy * (int32_t)elapsed << SUBPIXEL_SHIFT) / MILLIS_PER_SECOND, MAP_PX_H);
+
+ jlTileLayerDraw(screen, layer, (uint16_t)(x >> SUBPIXEL_SHIFT), (uint16_t)(y >> SUBPIXEL_SHIFT));
+ jlSpriteDraw(screen, ball, BALL_X, BALL_Y);
+ frames++;
+ if (now - barMillis >= MILLIS_PER_SECOND) {
+ drawBar(screen, (uint16_t)((uint32_t)frames * MILLIS_PER_SECOND / (now - barMillis)));
+ frames = 0;
+ barMillis = now;
+ }
+ jlStagePresent();
+ }
+
+ jlTileLayerDestroy(layer);
+ jlSpriteDestroy(ball);
+ jlShutdown();
+ return 0;
+}
diff --git a/examples/serloss/serloss.c b/examples/serloss/serloss.c
index 4a36b37..f0b57ca 100644
--- a/examples/serloss/serloss.c
+++ b/examples/serloss/serloss.c
@@ -100,10 +100,10 @@
// shadows a card's 6551 at $C0A8).
#define SLTROMSEL ((volatile uint8_t *)0x00C02DL)
-extern uint16_t gIigsRxOverruns;
-extern uint16_t gIigsRxDropped;
+extern uint16_t jlgIigsRxOverruns;
+extern uint16_t jlgIigsRxDropped;
#ifdef SERLOSS_LOOSE
-extern void iigsIrqLatencyRelax(void);
+extern void jlIigsIrqLatencyRelax(void);
#endif
#ifdef SERLOSS_JOY
@@ -195,8 +195,8 @@ __attribute__((noinline)) static void publish(uint16_t received, uint16_t inSequ
mailbox16(MB_BREAKS, breaks);
mailbox16(MB_PRESENTS, presents);
mailbox16(MB_TICKS, ticks);
- mailbox16(MB_OVERRUNS, gIigsRxOverruns);
- mailbox16(MB_DROPPED, gIigsRxDropped);
+ mailbox16(MB_OVERRUNS, jlgIigsRxOverruns);
+ mailbox16(MB_DROPPED, jlgIigsRxDropped);
mailbox16(MB_SUM, sum);
mailbox16(MB_NOW, now);
}
@@ -315,7 +315,7 @@ int main(void) {
}
}
#ifdef SERLOSS_LOOSE
- iigsIrqLatencyRelax();
+ jlIigsIrqLatencyRelax();
#endif
#ifdef SERLOSS_JOY
// Calibrate on the first poll, in the latency mode the rest will run in, so a still stick
diff --git a/examples/sertest/sertest.c b/examples/sertest/sertest.c
index 1ea323c..0435641 100644
--- a/examples/sertest/sertest.c
+++ b/examples/sertest/sertest.c
@@ -4,7 +4,7 @@
//
// Minimal headless serial self-test -- NO jlInit (no video/graphics), so it
// runs even where the graphics HAL can't (e.g. AROS on the Amiga). Opens the
-// DEFAULT port (or SERTEST_DEVICE) at 9600 8N1, sends a banner (TX
+// DEFAULT port (or SERTEST_DEVICE) at 9600 8N1 (or SERTEST_BAUD), sends a banner (TX
// heartbeat), then echoes every received byte forever. A host serial peer
// verifies banner (TX) + echo (RX).
//
@@ -27,6 +27,11 @@
#ifndef SERTEST_UNIT
#define SERTEST_UNIT 0u
#endif
+// The line rate, overridable the same way (-DSERTEST_BAUD=115200u: SERFAST,
+// the Win16 throughput gate's build).
+#ifndef SERTEST_BAUD
+#define SERTEST_BAUD 9600u
+#endif
static void dtrCheck(const jlSerialConfigT *cfg);
static void sendMark(const char *text);
@@ -79,7 +84,7 @@ int main(void) {
jlSerialConfigT cfg;
static const char banner[] = "JLSERTEST-READY\r\n";
- cfg.baud = 9600u;
+ cfg.baud = SERTEST_BAUD;
cfg.dataBits = 8u;
cfg.stopBits = 1u;
cfg.parity = JL_SERIAL_PARITY_NONE;
diff --git a/examples/tilebank/tilebank.c b/examples/tilebank/tilebank.c
index 6ff9163..1625643 100644
--- a/examples/tilebank/tilebank.c
+++ b/examples/tilebank/tilebank.c
@@ -101,7 +101,7 @@ static uint16_t gBankPalette[PALETTE_COLORS];
static uint16_t gExpectCount = 0u;
static uint16_t gExpectPalette[PALETTE_COLORS];
static uint8_t *gExpectPixels = NULL;
-static jlSurfaceT *gScreen = NULL;
+static jlSurfaceT *jlgScreen = NULL;
static jlTileT *gTiles = NULL;
static bool *gValid = NULL;
@@ -143,9 +143,9 @@ static bool loadExpected(void) {
static void pasteTiles(uint16_t count) {
uint16_t i;
- jlSurfaceClear(gScreen, FILL_COLOR);
+ jlSurfaceClear(jlgScreen, FILL_COLOR);
for (i = 0u; i < count; i++) {
- jlTilePaste(gScreen, (uint8_t)(i % JL_TILE_BLOCKS_PER_ROW), (uint8_t)(i / JL_TILE_BLOCKS_PER_ROW), &gTiles[i]);
+ jlTilePaste(jlgScreen, (uint8_t)(i % JL_TILE_BLOCKS_PER_ROW), (uint8_t)(i / JL_TILE_BLOCKS_PER_ROW), &gTiles[i]);
}
}
@@ -198,16 +198,16 @@ static uint16_t refuseCheck(uint16_t *banks) {
uint16_t loaded;
uint16_t streamed;
- jlSurfaceClear(gScreen, FILL_COLOR);
- fillHash = jlSurfaceHash(gScreen);
+ jlSurfaceClear(jlgScreen, FILL_COLOR);
+ fillHash = jlSurfaceHash(jlgScreen);
refused = 0u;
*banks = 0u;
while (refuseBankFound(name, (uint16_t)(*banks + 1u))) {
(*banks)++;
- jlSurfaceClear(gScreen, FILL_COLOR);
+ jlSurfaceClear(jlgScreen, FILL_COLOR);
loaded = jlTileBankLoad(name, gTiles, gExpectCount, gValid, NULL);
- streamed = jlTileBankLoadToSurface(name, gScreen, NULL);
- if (loaded == 0u && !gValid[0] && streamed == 0u && jlSurfaceHash(gScreen) == fillHash) {
+ streamed = jlTileBankLoadToSurface(name, jlgScreen, NULL);
+ if (loaded == 0u && !gValid[0] && streamed == 0u && jlSurfaceHash(jlgScreen) == fillHash) {
refused++;
}
}
@@ -231,16 +231,16 @@ static bool runChecks(void) {
refused = refuseCheck(&banks);
- jlSurfaceClear(gScreen, FILL_COLOR);
- streamed = jlTileBankLoadToSurface(OWN_BANK, gScreen, gBankPalette);
+ jlSurfaceClear(jlgScreen, FILL_COLOR);
+ streamed = jlTileBankLoadToSurface(OWN_BANK, jlgScreen, gBankPalette);
streamOk = streamed == gExpectCount && stageMatches(streamed);
- streamHash = jlSurfaceHash(gScreen);
+ streamHash = jlSurfaceHash(jlgScreen);
paletteOk = memcmp(gBankPalette, gExpectPalette, sizeof(gBankPalette)) == 0;
pasted = jlTileBankLoad(OWN_BANK, gTiles, gExpectCount, gValid, NULL);
pasteTiles(pasted);
pasteOk = pasted == gExpectCount && stageMatches(pasted);
- pasteHash = jlSurfaceHash(gScreen);
+ pasteHash = jlSurfaceHash(jlgScreen);
pass = streamOk && pasteOk && paletteOk && streamHash == pasteHash && banks > 0u && refused == banks;
jlLogF("TILEBANK: %s tiles=%u streamed=%u pasted=%u palette=%s streamHash=%08lX pasteHash=%08lX refused=%u/%u", pass ? "PASS" : "FAIL", (unsigned)gExpectCount, (unsigned)streamed, (unsigned)pasted, paletteOk ? "ok" : "wrong", (unsigned long)streamHash, (unsigned long)pasteHash, (unsigned)refused, (unsigned)banks);
@@ -258,10 +258,10 @@ static void showVerdict(bool pass) {
}
colors[LAMP_PASS_COLOR] = LAMP_PASS_RGB;
colors[LAMP_FAIL_COLOR] = LAMP_FAIL_RGB;
- jlPaletteSet(gScreen, 0u, gBankPalette);
- jlPaletteSet(gScreen, LAMP_PALETTE, colors);
- jlScbSetRange(gScreen, LAMP_Y, LAMP_Y + LAMP_H - 1, LAMP_PALETTE);
- jlFillRect(gScreen, LAMP_X, LAMP_Y, LAMP_W, LAMP_H, pass ? LAMP_PASS_COLOR : LAMP_FAIL_COLOR);
+ jlPaletteSet(jlgScreen, 0u, gBankPalette);
+ jlPaletteSet(jlgScreen, LAMP_PALETTE, colors);
+ jlScbSetRange(jlgScreen, LAMP_Y, LAMP_Y + LAMP_H - 1, LAMP_PALETTE);
+ jlFillRect(jlgScreen, LAMP_X, LAMP_Y, LAMP_W, LAMP_H, pass ? LAMP_PASS_COLOR : LAMP_FAIL_COLOR);
jlStagePresent();
}
@@ -282,7 +282,7 @@ static bool stageMatches(uint16_t tilesDrawn) {
if (block < tilesDrawn) {
want = gExpectPixels[(uint32_t)block * TILE_PIXELS + (y % JL_TILE_PIXELS_PER_SIDE) * JL_TILE_PIXELS_PER_SIDE + x % JL_TILE_PIXELS_PER_SIDE];
}
- if (jlSamplePixel(gScreen, x, y) != want) {
+ if (jlSamplePixel(jlgScreen, x, y) != want) {
return false;
}
}
@@ -301,7 +301,7 @@ int main(void) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return EXIT_FAILURE;
}
- gScreen = jlStageGet();
+ jlgScreen = jlStageGet();
pass = loadExpected();
if (pass) {
diff --git a/examples/uber/uber.c b/examples/uber/uber.c
index cbf2ad2..b19632a 100644
--- a/examples/uber/uber.c
+++ b/examples/uber/uber.c
@@ -88,7 +88,7 @@
#define UBER_MB_HEARTBEAT ((volatile uint16_t *)0xE19DCAL)
#define UBER_MB_TICK ((volatile uint16_t *)0xE19DCCL)
// One-time layout facts for probe correlation: +8 stage pointer value,
-// +12 address of uber.c's gStage static, +16 log-ring base address,
+// +12 address of uber.c's jlgStage static, +16 log-ring base address,
// +20 log-ring head-counter address (see src/core/debug.c's IIgs RAM
// ring -- the Lua probe drains the log live through these).
#define UBER_MB_MAGIC ((volatile uint16_t *)0xE19DCEL)
@@ -102,13 +102,13 @@
// "JL" -- the Lua probes refuse to trust the layout words until this
// appears (the SCB region holds 0x80 fill before the app writes it).
#define UBER_MB_MAGIC_VAL 0x4A4Cu
-extern uint8_t gJoeyLogRing[];
-extern uint16_t gJoeyLogRingHead;
+extern uint8_t jlgJoeyLogRing[];
+extern uint16_t jlgJoeyLogRingHead;
#define uberMbOp(_i) (*UBER_MB_OP = (uint8_t)(_i))
#define uberMbPhase(_p) (*UBER_MB_PHASE = (uint8_t)(_p))
#define uberMbBeat() (*UBER_MB_HEARTBEAT = (uint16_t)(*UBER_MB_HEARTBEAT + 1u))
#define uberMbTick(_t) (*UBER_MB_TICK = (uint16_t)(_t))
-#define uberMbLayout() (*UBER_MB_STAGEPTR = (uint32_t)gStage, *UBER_MB_GSTAGEAT = (uint32_t)&gStage, *UBER_MB_LOGRING = (uint32_t)&gJoeyLogRing[0], *UBER_MB_LOGHEAD = (uint32_t)&gJoeyLogRingHead, *UBER_MB_MAGIC = UBER_MB_MAGIC_VAL)
+#define uberMbLayout() (*UBER_MB_STAGEPTR = (uint32_t)jlgStage, *UBER_MB_GSTAGEAT = (uint32_t)&jlgStage, *UBER_MB_LOGRING = (uint32_t)&jlgJoeyLogRing[0], *UBER_MB_LOGHEAD = (uint32_t)&jlgJoeyLogRingHead, *UBER_MB_MAGIC = UBER_MB_MAGIC_VAL)
#elif defined(JOEYLIB_PLATFORM_X68000)
// Same mailbox words at the same addresses, for the same reason: a headless
// probe needs to know WHERE the run is when the serial log has gone quiet.
@@ -137,7 +137,7 @@ extern uint16_t gJoeyLogRingHead;
typedef void (*OpFn)(void);
-static jlSurfaceT *gStage = NULL;
+static jlSurfaceT *jlgStage = NULL;
static jlSpriteT *gSprite = NULL;
static jlSpriteBackupT gBackup;
static unsigned char gBackupBytes[jlSpriteBackupLength(unsigned char, 2, 2)] __attribute__((aligned(2)));
@@ -256,7 +256,11 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *
// pixel-perfect comparison input for the cross-port validation
// harness (tools/diff-uber-hashes.py). Captured against IIgs as the
// golden reference; planar 68k rewrites validate by matching it.
-static void timeOp(const char *name, OpFn op) {
+// The window is `frames` long. settle (NULL for none) runs between the
+// timing and the hash, for an op whose last call leaves a picture that
+// depends on how many calls the window held (a moving origin): it redraws
+// one that does not.
+static void timeOpSettled(const char *name, OpFn op, OpFn settle, unsigned int frames) {
unsigned long iters;
unsigned long opsPerSec;
unsigned long opsPerSecFr;
@@ -267,7 +271,7 @@ static void timeOp(const char *name, OpFn op) {
gOpIndex++;
uberMbOp(gOpIndex);
- iters = runForFrames(op, UBER_FRAMES, &actualFrames, &millisDelta);
+ iters = runForFrames(op, frames, &actualFrames, &millisDelta);
if (iters == 0UL) {
jlLogF("UBER: %s: 0 iters (op too slow?)", name);
@@ -284,52 +288,58 @@ static void timeOp(const char *name, OpFn op) {
* the jlpFrameHz fix the two agree; a divergence flags a clock bug. */
opsPerSec = (iters * 1000UL) / (unsigned long)millisDelta;
opsPerSecFr = (iters * (unsigned long)jlFrameHz()) / (unsigned long)actualFrames;
- hash = jlSurfaceHash(gStage);
+ if (settle != NULL) {
+ settle();
+ }
+ hash = jlSurfaceHash(jlgStage);
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);
}
+static void timeOp(const char *name, OpFn op) {
+ timeOpSettled(name, op, NULL, UBER_FRAMES);
+}
// ----- Test ops -----
-static void op_drawPixel (void) { jlDrawPixel (gStage, 100, 100, 5); }
-static void op_drawLineH (void) { jlDrawLine (gStage, 0, 50, 319, 50, 5); }
-static void op_drawLineV (void) { jlDrawLine (gStage, 50, 0, 50, 199, 5); }
-static void op_drawLineDiag (void) { jlDrawLine (gStage, 0, 0, 319, 199, 5); }
-static void op_drawRect (void) { jlDrawRect (gStage, 10, 10, 100, 100, 5); }
-static void op_drawCircleSmall (void) { jlDrawCircle (gStage, 160, 100, 16, 5); }
-static void op_drawCircleLarge (void) { jlDrawCircle (gStage, 160, 100, 80, 5); }
-static void op_fillRectSmall (void) { jlFillRect (gStage, 20, 20, 16, 16, 7); }
-static void op_fillRectMid (void) { jlFillRect (gStage, 20, 20, 80, 80, 7); }
-static void op_fillRectFull (void) { jlFillRect (gStage, 0, 0, 320, 200, 7); }
-static void op_fillCircle (void) { jlFillCircle (gStage, 160, 100, 40, 7); }
-static void op_samplePixel (void) { (void)jlSamplePixel(gStage, 100, 100); }
-static void op_surfaceClear (void) { jlSurfaceClear (gStage, 0); }
+static void op_drawPixel (void) { jlDrawPixel (jlgStage, 100, 100, 5); }
+static void op_drawLineH (void) { jlDrawLine (jlgStage, 0, 50, 319, 50, 5); }
+static void op_drawLineV (void) { jlDrawLine (jlgStage, 50, 0, 50, 199, 5); }
+static void op_drawLineDiag (void) { jlDrawLine (jlgStage, 0, 0, 319, 199, 5); }
+static void op_drawRect (void) { jlDrawRect (jlgStage, 10, 10, 100, 100, 5); }
+static void op_drawCircleSmall (void) { jlDrawCircle (jlgStage, 160, 100, 16, 5); }
+static void op_drawCircleLarge (void) { jlDrawCircle (jlgStage, 160, 100, 80, 5); }
+static void op_fillRectSmall (void) { jlFillRect (jlgStage, 20, 20, 16, 16, 7); }
+static void op_fillRectMid (void) { jlFillRect (jlgStage, 20, 20, 80, 80, 7); }
+static void op_fillRectFull (void) { jlFillRect (jlgStage, 0, 0, 320, 200, 7); }
+static void op_fillCircle (void) { jlFillCircle (jlgStage, 160, 100, 40, 7); }
+static void op_samplePixel (void) { (void)jlSamplePixel(jlgStage, 100, 100); }
+static void op_surfaceClear (void) { jlSurfaceClear (jlgStage, 0); }
static void op_paletteSet(void) {
static uint16_t colors[16] = {
0x000, 0xF00, 0x0F0, 0x00F, 0xFF0, 0xF0F, 0x0FF, 0xFFF,
0x800, 0x080, 0x008, 0x880, 0x808, 0x088, 0x888, 0x444
};
- jlPaletteSet(gStage, 0, colors);
+ jlPaletteSet(jlgStage, 0, colors);
}
-static void op_scbSetRange (void) { jlScbSetRange (gStage, 0, 199, 0); }
+static void op_scbSetRange (void) { jlScbSetRange (jlgStage, 0, 199, 0); }
-static void op_tileFill (void) { jlTileFill (gStage, 5, 5, 7); }
-static void op_tileCopy (void) { jlTileCopy (gStage, 6, 6, gStage, 5, 5); }
-static void op_tileCopyMasked (void) { jlTileCopyMasked (gStage, 7, 7, gStage, 5, 5, 0); }
-static void op_tilePaste (void) { jlTilePaste (gStage, 8, 8, &gTileScratch); }
-static void op_tileSnap (void) { jlTileSnap (gStage, 5, 5, &gTileScratch); }
+static void op_tileFill (void) { jlTileFill (jlgStage, 5, 5, 7); }
+static void op_tileCopy (void) { jlTileCopy (jlgStage, 6, 6, jlgStage, 5, 5); }
+static void op_tileCopyMasked (void) { jlTileCopyMasked (jlgStage, 7, 7, jlgStage, 5, 5, 0); }
+static void op_tilePaste (void) { jlTilePaste (jlgStage, 8, 8, &gTileScratch); }
+static void op_tileSnap (void) { jlTileSnap (jlgStage, 5, 5, &gTileScratch); }
static int16_t gSpriteX = 40;
static int16_t gSpriteY = 30;
-static void op_spriteSave (void) { jlSpriteSaveUnder (gStage, gSprite, gSpriteX, gSpriteY, &gBackup); }
-static void op_spriteDraw (void) { jlSpriteDraw (gStage, gSprite, gSpriteX, gSpriteY); }
-static void op_spriteRestore (void) { jlSpriteRestoreUnder(gStage, &gBackup); }
-static void op_spriteSaveAndDraw (void) { jlSpriteSaveAndDraw (gStage, gSprite, gSpriteX, gSpriteY, &gBackup); }
+static void op_spriteSave (void) { jlSpriteSaveUnder (jlgStage, gSprite, gSpriteX, gSpriteY, &gBackup); }
+static void op_spriteDraw (void) { jlSpriteDraw (jlgStage, gSprite, gSpriteX, gSpriteY); }
+static void op_spriteRestore (void) { jlSpriteRestoreUnder(jlgStage, &gBackup); }
+static void op_spriteSaveAndDraw (void) { jlSpriteSaveAndDraw (jlgStage, gSprite, gSpriteX, gSpriteY, &gBackup); }
static void op_stagePresent (void) { jlStagePresent(); }
@@ -343,7 +353,7 @@ static void op_audioFrameTick (void) { jlAudioFrameTick(); }
static void op_audioIsPlaying (void) { (void)jlAudioIsPlayingMod(); }
static void op_surfaceMarkDirty(void) { // jlDrawPixel already marks; use fill instead
- jlFillRect(gStage, 0, 0, 32, 32, 0); }
+ jlFillRect(jlgStage, 0, 0, 32, 32, 0); }
// ----- Phase 0 workload ops (claudeDocs/NATIVE-PERF-PLAN.md item 3) -----
@@ -357,7 +367,7 @@ static void op_surfaceMarkDirty(void) { // jlDrawPixel already marks; use fill i
#define UBER_UNALIGNED_X 71
#define UBER_UNALIGNED_Y 120
-static void op_spriteDrawUnaligned(void) { jlSpriteDraw(gStage, gSprite, UBER_UNALIGNED_X, UBER_UNALIGNED_Y); }
+static void op_spriteDrawUnaligned(void) { jlSpriteDraw(jlgStage, gSprite, UBER_UNALIGNED_X, UBER_UNALIGNED_Y); }
// All 16 x phases in ONE call so the op is idempotent: per-port
@@ -373,7 +383,7 @@ static void op_spriteDrawSweep(void) {
int16_t x;
for (x = UBER_SWEEP_X0; x < (int16_t)(UBER_SWEEP_X0 + 16); x++) {
- jlSpriteDraw(gStage, gSprite, x, UBER_SWEEP_Y);
+ jlSpriteDraw(jlgStage, gSprite, x, UBER_SWEEP_Y);
}
}
@@ -395,7 +405,7 @@ static jlTileT gTileSet[2];
static uint8_t gTileMapIndices[UBER_TILEMAP_W * UBER_TILEMAP_H];
static void op_tileMapPasteBatch(void) {
- jlTileMapPaste(gStage, UBER_TILEMAP_X0, UBER_TILEMAP_B_Y0, UBER_TILEMAP_W, UBER_TILEMAP_H, gTileSet, gTileMapIndices);
+ jlTileMapPaste(jlgStage, UBER_TILEMAP_X0, UBER_TILEMAP_B_Y0, UBER_TILEMAP_W, UBER_TILEMAP_H, gTileSet, gTileMapIndices);
}
@@ -406,9 +416,9 @@ static void op_tileMapPaste(void) {
for (ty = 0; ty < UBER_TILEMAP_H; ty++) {
for (tx = 0; tx < UBER_TILEMAP_W; tx++) {
if (((tx + ty) & 1u) == 0u) {
- jlTilePaste(gStage, (uint8_t)(UBER_TILEMAP_X0 + tx), (uint8_t)(UBER_TILEMAP_Y0 + ty), &gTileMapA);
+ jlTilePaste(jlgStage, (uint8_t)(UBER_TILEMAP_X0 + tx), (uint8_t)(UBER_TILEMAP_Y0 + ty), &gTileMapA);
} else {
- jlTilePaste(gStage, (uint8_t)(UBER_TILEMAP_X0 + tx), (uint8_t)(UBER_TILEMAP_Y0 + ty), &gTileMapB);
+ jlTilePaste(jlgStage, (uint8_t)(UBER_TILEMAP_X0 + tx), (uint8_t)(UBER_TILEMAP_Y0 + ty), &gTileMapB);
}
}
}
@@ -443,12 +453,12 @@ static const int16_t gFrameSpriteY[UBER_FRAME_SPRITES] = { 80, 96, 112 };
static void paintFrameStatic(void) {
uint8_t t;
- jlFillRect(gStage, UBER_FRAME_RX, UBER_FRAME_RY, UBER_FRAME_RW, UBER_FRAME_RH, UBER_FRAME_BG);
+ jlFillRect(jlgStage, UBER_FRAME_RX, UBER_FRAME_RY, UBER_FRAME_RW, UBER_FRAME_RH, UBER_FRAME_BG);
for (t = 0; t < UBER_FRAME_TILES; t++) {
if ((t & 1u) == 0u) {
- jlTilePaste(gStage, (uint8_t)(UBER_FRAME_TILE_TX + t), UBER_FRAME_TILE_TY, &gTileMapA);
+ jlTilePaste(jlgStage, (uint8_t)(UBER_FRAME_TILE_TX + t), UBER_FRAME_TILE_TY, &gTileMapA);
} else {
- jlTilePaste(gStage, (uint8_t)(UBER_FRAME_TILE_TX + t), UBER_FRAME_TILE_TY, &gTileMapB);
+ jlTilePaste(jlgStage, (uint8_t)(UBER_FRAME_TILE_TX + t), UBER_FRAME_TILE_TY, &gTileMapB);
}
}
}
@@ -462,10 +472,10 @@ static void gameFrameBody(void) {
paintFrameStatic();
for (i = 0; i < UBER_FRAME_SPRITES; i++) {
- jlSpriteSaveAndDraw(gStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i], &gFrameBackups[i]);
+ jlSpriteSaveAndDraw(jlgStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i], &gFrameBackups[i]);
}
for (i = UBER_FRAME_SPRITES - 1; i >= 0; i--) {
- jlSpriteRestoreUnder(gStage, &gFrameBackups[i]);
+ jlSpriteRestoreUnder(jlgStage, &gFrameBackups[i]);
}
}
@@ -492,15 +502,87 @@ static void op_gameFrameClean(void) {
int16_t i;
for (i = 0; i < UBER_FRAME_SPRITES; i++) {
- jlSurfaceCopyRect(gStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16);
+ jlSurfaceCopyRect(jlgStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16);
}
for (i = 0; i < UBER_FRAME_SPRITES; i++) {
- jlSpriteDraw(gStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i]);
+ jlSpriteDraw(jlgStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i]);
}
jlStagePresent();
}
+// ----- Tile layer rows (scrolling) -----
+//
+// A map bigger than the view both ways, of tiles snapped from portable
+// draws (a jlTileT is target-native, so only a snap round-trips), with a
+// few JL_TILE_MAP_SKIP cells. The timed rows scroll it diagonally a pixel
+// a call: every pixel phase, and a new tile column and row every 8 calls.
+// A moving origin would make the post-window hash depend on the call
+// count, so they settle on a fixed origin first. checkTileLayer (in the
+// UBER-CHK block) walks moves of every size and both seams. A scroll frame
+// takes several display frames on the slow ports, so the rows time over a
+// longer window than the rest.
+#define UBER_LAYER_FRAMES (UBER_FRAMES * 8u)
+#define UBER_LAYER_MAP_W 64
+#define UBER_LAYER_MAP_H 40
+#define UBER_LAYER_TILES 8
+#define UBER_LAYER_SETTLE_X 37
+#define UBER_LAYER_SETTLE_Y 21
+
+static jlTileT gLayerTiles[UBER_LAYER_TILES];
+static uint8_t gLayerMap[UBER_LAYER_MAP_W * UBER_LAYER_MAP_H];
+static jlTileLayerT *gLayer = NULL;
+static uint16_t gLayerX;
+static uint16_t gLayerY;
+
+
+// Paints and snaps the tiles (block row 0 of the stage), and fills the map.
+static void layerData(void) {
+ uint8_t t;
+ int16_t x;
+ uint16_t tx;
+ uint16_t ty;
+
+ for (t = 0; t < UBER_LAYER_TILES; t++) {
+ x = (int16_t)(t * 8);
+ jlFillRect(jlgStage, x, 0, 8, 8, (uint8_t)((t * 2u + 1u) & 15u));
+ jlFillRect(jlgStage, x, 0, 3, 5, (uint8_t)((t * 5u + 2u) & 15u));
+ jlDrawLine(jlgStage, x, 7, (int16_t)(x + 7), 0, (uint8_t)((t + 9u) & 15u));
+ jlDrawPixel(jlgStage, (int16_t)(x + 6), 1, (uint8_t)((t * 3u + 4u) & 15u));
+ jlTileSnap(jlgStage, t, 0, &gLayerTiles[t]);
+ }
+ for (ty = 0; ty < UBER_LAYER_MAP_H; ty++) {
+ for (tx = 0; tx < UBER_LAYER_MAP_W; tx++) {
+ if ((tx * 7u + ty * 11u) % 23u == 0u) {
+ gLayerMap[ty * UBER_LAYER_MAP_W + tx] = JL_TILE_MAP_SKIP;
+ } else {
+ gLayerMap[ty * UBER_LAYER_MAP_W + tx] = (uint8_t)((tx * 5u + ty * 3u + (tx ^ ty)) % UBER_LAYER_TILES);
+ }
+ }
+ }
+}
+
+
+static void op_layerDraw(void) {
+ gLayerX++;
+ gLayerY++;
+ jlTileLayerDraw(jlgStage, gLayer, gLayerX, gLayerY);
+}
+
+
+static void op_layerFrame(void) {
+ gLayerX++;
+ gLayerY++;
+ jlTileLayerDraw(jlgStage, gLayer, gLayerX, gLayerY);
+ jlStagePresent();
+}
+
+
+static void settleLayer(void) {
+ jlTileLayerDraw(jlgStage, gLayer, UBER_LAYER_SETTLE_X, UBER_LAYER_SETTLE_Y);
+}
+
+
#ifdef UBER_PROBE
// NATIVE-PERF Phase 6 W0 probe rows. NOT part of the golden contract:
// this block only exists in the scratch probe binary (-DUBER_PROBE,
@@ -511,15 +593,22 @@ static void op_gameFrameClean(void) {
// logged around each row to split O(rows) scan cost from O(bytes)
// copy cost and to expose over-copy.
// Unconditional since W2 item 0: the IIgs present asm now increments
-// the counters too (iigsBlitStageToShr), so every port logs deltas.
-extern uint32_t gPresentCopiedBytes;
-extern uint32_t gPresentScannedRows;
+// the counters too (jlIigsBlitStageToShr), so every port logs deltas.
+extern uint32_t jlgPresentCopiedBytes;
+extern uint32_t jlgPresentScannedRows;
+extern uint8_t jlgPresentTelemetry;
#ifdef JOEYLIB_PLATFORM_IIGS
-extern void iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord);
-#define probeMarkRows(_y0, _y1, _w0, _w1) iigsMarkDirtyRowsInner((uint16_t)(_y0), (uint16_t)(_y1), (uint16_t)(_w0), (uint16_t)(_w1))
+extern void jlIigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord);
+#define probeMarkRows(_y0, _y1, _w0, _w1) jlIigsMarkDirtyRowsInner((uint16_t)(_y0), (uint16_t)(_y1), (uint16_t)(_w0), (uint16_t)(_w1))
+#elif defined(JL_CPU_68000)
+// The 68000 ports' marker is asm (src/m68k/stageBands.s), handed the rows.
+extern uint8_t jlgStageMinWord[];
+extern uint8_t jlgStageMaxWord[];
+extern void jlStageBandsMark68k(uint8_t *minRow, uint8_t *maxRow, uint16_t rows, uint8_t minWord, uint8_t maxWord);
+#define probeMarkRows(_y0, _y1, _w0, _w1) jlStageBandsMark68k(&jlgStageMinWord[_y0], &jlgStageMaxWord[_y0], (uint16_t)((_y1) - (_y0)), (uint8_t)(_w0), (uint8_t)(_w1))
#else
-extern void surfaceMarkDirtyRows(uint16_t yStart, uint16_t yEnd, uint8_t minWord, uint8_t maxWord);
-#define probeMarkRows(_y0, _y1, _w0, _w1) surfaceMarkDirtyRows((uint16_t)(_y0), (uint16_t)(_y1), (uint8_t)(_w0), (uint8_t)(_w1))
+extern void jlSurfaceMarkDirtyRows(uint16_t yStart, uint16_t yEnd, uint8_t minWord, uint8_t maxWord);
+#define probeMarkRows(_y0, _y1, _w0, _w1) jlSurfaceMarkDirtyRows((uint16_t)(_y0), (uint16_t)(_y1), (uint8_t)(_w0), (uint8_t)(_w1))
#endif
// Same w65816 register-allocator escape hatch as DRAWSHOWCASE_ATTR:
@@ -601,31 +690,31 @@ static jlSpriteBackupT gProbeSpriteBackup;
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);
+ jlSpriteSaveUnder(jlgStage, gSprite, -100, -100, &gProbeSpriteBackup);
}
static void PROBE_ATTR op_probeSpriteRestoreVal(void) {
- jlSpriteRestoreUnder(gStage, &gProbeSpriteBackup);
+ jlSpriteRestoreUnder(jlgStage, &gProbeSpriteBackup);
}
static void PROBE_ATTR op_probeSpriteSaveG16(void) {
- jlSpriteSaveUnder(gStage, gSprite, 48, 30, &gProbeSpriteBackup);
+ jlSpriteSaveUnder(jlgStage, gSprite, 48, 30, &gProbeSpriteBackup);
}
// P7-1 restore-mark cost isolation (scoping the restore-mark asm
// fusion, 2026-07-11). The compiled restore fast path runs body +
// dispatch identically whether the destination is the stage or a work
-// surface, but spriteMarkDirty early-returns on s != gStage -- so a
+// surface, but spriteMarkDirty early-returns on s != jlgStage -- so a
// restore to a NON-stage surface runs the whole call MINUS the 16-row
// dirty-band widen. Differencing the golden jlSpriteRestoreUnder row
-// (restores gBackup to gStage -> marks) against this row (restores an
+// (restores gBackup to jlgStage -> marks) against this row (restores an
// identical class-1 backup to gProbeRestoreWork -> mark skipped) yields
// the mark body + its call overhead: the UPPER BOUND on what fusing the
// mark into the emitted asm could recover (fusion keeps the widen, only
-// removes the C call boundary). Dedicated backup + surface keep gStage
+// removes the C call boundary). Dedicated backup + surface keep jlgStage
// and gBackup untouched, so the row is inert to the golden hashes.
static jlSurfaceT *gProbeRestoreWork = NULL;
static jlSpriteBackupT gProbeRestoreBackup;
@@ -638,12 +727,12 @@ static void PROBE_ATTR op_probeRestoreWork(void) {
// P7-1 trusted-restore lever: jlSpriteRestoreUnderTrusted skips the
// geometry-validation chain. Restores the SAME valid class-1 backup to
-// gStage as the golden jlSpriteRestoreUnder row (same body + inline
+// jlgStage as the golden jlSpriteRestoreUnder row (same body + inline
// mark), so the difference vs that row is exactly the validation cost
// the trusted entry saves. Hash-inert: writes the saved-under pixels
-// back to gStage (idempotent) behind UBER_PROBE.
+// back to jlgStage (idempotent) behind UBER_PROBE.
static void PROBE_ATTR op_probeRestoreTrusted(void) {
- jlSpriteRestoreUnderTrusted(gStage, &gProbeRestoreBackup);
+ jlSpriteRestoreUnderTrusted(jlgStage, &gProbeRestoreBackup);
}
@@ -652,12 +741,12 @@ static void PROBE_ATTR op_probeRestoreTrusted(void) {
// valid backup), so each difference vs its golden row is the validation
// the trusted entry skips. Hash-inert behind UBER_PROBE.
static void PROBE_ATTR op_probeSaveTrusted(void) {
- jlSpriteSaveUnderTrusted(gStage, gSprite, 40, 30, &gProbeRestoreBackup);
+ jlSpriteSaveUnderTrusted(jlgStage, gSprite, 40, 30, &gProbeRestoreBackup);
}
static void PROBE_ATTR op_probeSaveAndDrawTrusted(void) {
- jlSpriteSaveAndDrawTrusted(gStage, gSprite, 40, 30, &gProbeRestoreBackup);
+ jlSpriteSaveAndDrawTrusted(jlgStage, gSprite, 40, 30, &gProbeRestoreBackup);
}
@@ -665,7 +754,7 @@ static void PROBE_ATTR op_probeSaveAndDrawTrusted(void) {
// (sprite at 40,30) so the difference vs that row is exactly the
// validation the trusted entry skips. Hash-inert behind UBER_PROBE.
static void PROBE_ATTR op_probeDrawTrusted(void) {
- jlSpriteDrawTrusted(gStage, gSprite, 40, 30);
+ jlSpriteDrawTrusted(jlgStage, gSprite, 40, 30);
}
@@ -680,8 +769,10 @@ 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[JL_SURFACE_HEIGHT];
-extern uint8_t gStageMaxWord[JL_SURFACE_HEIGHT];
+extern uint8_t jlgStageMinWord[JL_SURFACE_HEIGHT];
+extern uint8_t jlgStageMaxWord[JL_SURFACE_HEIGHT];
+// A clean row's band minimum (surfaceInternal.h STAGE_DIRTY_CLEAN_MIN).
+#define UBER_BAND_CLEAN_MIN 0xFFu
static uint32_t PROBE_ATTR probeBandFold(void) {
uint16_t y;
@@ -689,21 +780,22 @@ static uint32_t PROBE_ATTR probeBandFold(void) {
sum = 0;
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
- sum = (uint32_t)(sum * 33u + gStageMinWord[y]);
- sum = (uint32_t)(sum * 33u + gStageMaxWord[y]);
+ sum = (uint32_t)(sum * 33u + jlgStageMinWord[y]);
+ sum = (uint32_t)(sum * 33u + jlgStageMaxWord[y]);
}
return sum;
}
+
static void PROBE_ATTR op_probeSpriteBandSum(void) {
uint32_t sum;
jlStagePresent();
gameFrameBody();
- jlSpriteDraw(gStage, gSprite, -8, 100);
- jlSpriteDraw(gStage, gSprite, 150, -8);
- jlSpriteDraw(gStage, gSprite, 150, 189);
- jlSpriteDraw(gStage, gSprite, 71, 120);
+ jlSpriteDraw(jlgStage, gSprite, -8, 100);
+ jlSpriteDraw(jlgStage, gSprite, 150, -8);
+ jlSpriteDraw(jlgStage, gSprite, 150, 189);
+ jlSpriteDraw(jlgStage, gSprite, 71, 120);
sum = probeBandFold();
jlLogF("UBER-PROBE: spriteBandSum=%lu", (unsigned long)sum);
}
@@ -716,19 +808,19 @@ static void PROBE_ATTR op_probeSpriteBandSum(void) {
// batched-mark callers of the NoMark twin (jlDrawRect outline,
// jlFillCircle spans). Cross-port identity is the gate: the three
// C-mark ports produce the reference band state the IIgs fused asm
-// mark (stageRectMark) must reproduce exactly.
+// mark (jlStageRectMark) must reproduce exactly.
static void PROBE_ATTR op_probeFillBandSum(void) {
uint32_t sum;
jlStagePresent();
- jlFillRect(gStage, 13, 7, 51, 3, 5);
- jlFillRect(gStage, 3, 5, 2, 4, 6);
- jlFillRect(gStage, 2, 9, 1, 1, 7);
- jlFillRect(gStage, 319, 0, 1, 1, 8);
- jlFillRect(gStage, 1, 20, 3, 2, 9);
- jlFillRect(gStage, 40, 40, 16, 16, 10);
- jlDrawRect(gStage, 60, 60, 100, 100, 11);
- jlFillCircle(gStage, 200, 120, 30, 12);
+ jlFillRect(jlgStage, 13, 7, 51, 3, 5);
+ jlFillRect(jlgStage, 3, 5, 2, 4, 6);
+ jlFillRect(jlgStage, 2, 9, 1, 1, 7);
+ jlFillRect(jlgStage, 319, 0, 1, 1, 8);
+ jlFillRect(jlgStage, 1, 20, 3, 2, 9);
+ jlFillRect(jlgStage, 40, 40, 16, 16, 10);
+ jlDrawRect(jlgStage, 60, 60, 100, 100, 11);
+ jlFillCircle(jlgStage, 200, 120, 30, 12);
sum = probeBandFold();
jlLogF("UBER-PROBE: fillBandSum=%lu", (unsigned long)sum);
}
@@ -792,12 +884,12 @@ static void PROBE_ATTR probeShrTailCheck(uint16_t *badScb, uint16_t *badPal, uin
*badPal = 0;
*firstBadPal = SHR_TAIL_NONE;
for (line = 0; line < SHR_TAIL_SCB_BYTES; line++) {
- if (shrScb[line] != jlScbGet(gStage, line)) {
+ if (shrScb[line] != jlScbGet(jlgStage, line)) {
(*badScb)++;
}
}
for (p = 0; p < SHR_TAIL_PALETTES; p++) {
- jlPaletteGet(gStage, (uint8_t)p, want);
+ jlPaletteGet(jlgStage, (uint8_t)p, want);
for (c = 0; c < SHR_TAIL_COLORS; c++) {
if (shrPal[(p * SHR_TAIL_COLORS) + c] != want[c]) {
(*badPal)++;
@@ -824,8 +916,8 @@ static void PROBE_ATTR probeShrTailSeed(void) {
for (c = 0; c < SHR_TAIL_COLORS; c++) {
colors[c] = (uint16_t)(((p & 0x0Fu) << 8) | ((c & 0x0Fu) << 4) | ((p + c) & 0x0Fu));
}
- jlPaletteSet(gStage, (uint8_t)p, colors);
- jlScbSetRange(gStage, (uint16_t)(p * 12u), (uint16_t)((p * 12u) + 11u), (uint8_t)p);
+ jlPaletteSet(jlgStage, (uint8_t)p, colors);
+ jlScbSetRange(jlgStage, (uint16_t)(p * 12u), (uint16_t)((p * 12u) + 11u), (uint8_t)p);
}
}
@@ -834,6 +926,7 @@ static void PROBE_ATTR op_probeShrParity(void) {
const uint8_t *stagePx;
const uint8_t *shrPx;
uint16_t badRows;
+ uint16_t staleRows;
uint16_t firstBad;
uint16_t badScbUp;
uint16_t badPalUp;
@@ -844,25 +937,29 @@ static void PROBE_ATTR op_probeShrParity(void) {
uint16_t x;
uint16_t y;
- jlSurfaceClear(gStage, 3);
+ jlSurfaceClear(jlgStage, 3);
jlStagePresent(); // 5 chunks x 40-row full runs
probeMarkRows(0, 1, 0, 79);
jlStagePresent(); // 1-row full run, top edge
probeMarkRows(199, 200, 10, 60);
jlStagePresent(); // 1-row computed-entry run, bottom edge
- jlFillRect(gStage, 240, 50, 80, 3, 9);
+ jlFillRect(jlgStage, 240, 50, 80, 3, 9);
jlStagePresent(); // words 60..79, W=20 -> MVN both rules
- jlFillRect(gStage, 0, 55, 64, 1, 10);
+ jlFillRect(jlgStage, 0, 55, 64, 1, 10);
jlStagePresent(); // words 0..15, W=16 -> old MVN, NEW SLAM
- jlFillRect(gStage, 160, 57, 160, 1, 11);
+ jlFillRect(jlgStage, 160, 57, 160, 1, 11);
jlStagePresent(); // words 40..79, W=40, 80 > 60 -> old slam, NEW MVN
- jlFillRect(gStage, 0, 60, 144, 2, 4);
+ jlFillRect(jlgStage, 0, 60, 144, 2, 4);
jlStagePresent(); // words 0..35 -> computed entry
probeMarkRows(38, 44, 0, 79);
jlStagePresent(); // run clipped at the 39/40 chunk edge
- jlFillRect(gStage, 32, 100, 256, 6, 12);
- jlFillRect(gStage, 300, 106, 16, 5, 13);
+ jlFillRect(jlgStage, 32, 100, 256, 6, 12);
+ jlFillRect(jlgStage, 300, 106, 16, 5, 13);
jlStagePresent(); // wide run + narrow MVN in one present
+ jlDrawPixel(jlgStage, 77, 13, 7);
+ jlStagePresent(); // one pixel mid-group in a clean group (asm mark)
+ op_tileMapPasteBatch();
+ jlStagePresent(); // a tile map (asm mark)
stagePx = (const uint8_t *)0x012000L;
shrPx = (const uint8_t *)0xE12000L;
badRows = 0;
@@ -882,6 +979,21 @@ static void PROBE_ATTR op_probeShrParity(void) {
}
jlLogF("UBER-PROBE: shrParity %s badRows=%u firstBad=%u", (badRows == 0u) ? "OK" : "FAIL", (unsigned int)badRows, (unsigned int)firstBad);
+ // Every band CLEAN after the presents: a mark that skips its group's
+ // flag (jlgStageDirtyGroup) is never cleared, and its rows are copied
+ // again at every present.
+ staleRows = 0;
+ firstBad = 0xFFFFu;
+ for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
+ if (jlgStageMinWord[y] != UBER_BAND_CLEAN_MIN) {
+ staleRows++;
+ if (firstBad == 0xFFFFu) {
+ firstBad = y;
+ }
+ }
+ }
+ jlLogF("UBER-PROBE: shrBands %s staleRows=%u firstStale=%u", (staleRows == 0u) ? "OK" : "FAIL", (unsigned int)staleRows, (unsigned int)firstBad);
+
// Tail phase 1: seed all 16 palettes + the SCB bands, then present.
// jlPaletteSet / jlScbSetRange set the dirty flags, so THIS present
// is the one that uploads them. A mismatch here means the C-side
@@ -903,7 +1015,7 @@ static void PROBE_ATTR op_probeShrParity(void) {
jlStagePresent();
probeMarkRows(196, 200, 70, 79);
jlStagePresent();
- jlFillRect(gStage, 0, 180, 320, 20, 6);
+ jlFillRect(jlgStage, 0, 180, 320, 20, 6);
jlStagePresent();
probeShrTailCheck(&badScbSlam, &badPalSlam, &firstPalSlam);
jlLogF("UBER-PROBE: shrTailSlam %s badScb=%u badPal=%u firstPal=%u", ((badScbSlam | badPalSlam) == 0u) ? "OK" : "FAIL", (unsigned int)badScbSlam, (unsigned int)badPalSlam, (unsigned int)firstPalSlam);
@@ -929,22 +1041,22 @@ static void PROBE_ATTR op_probeDisplayCheck(void) {
uint32_t afterSettle;
uint32_t afterIdle;
- jlFillRect(gStage, 0, 50, 320, 100, 3);
+ jlFillRect(jlgStage, 0, 50, 320, 100, 3);
jlStagePresent();
- jlFillRect(gStage, 100, 60, 40, 10, 9);
+ jlFillRect(jlgStage, 100, 60, 40, 10, 9);
jlStagePresent();
- afterB = jlpProbeDisplayMismatch(gStage);
+ afterB = jlpProbeDisplayMismatch(jlgStage);
jlStagePresent();
- afterSettle = jlpProbeDisplayMismatch(gStage);
+ afterSettle = jlpProbeDisplayMismatch(jlgStage);
jlStagePresent();
- afterIdle = jlpProbeDisplayMismatch(gStage);
+ afterIdle = jlpProbeDisplayMismatch(jlgStage);
jlLogF("UBER-PROBE: displayMismatch afterB=%lu afterSettle=%lu afterIdle=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle);
}
#endif
#ifdef JOEYLIB_PLATFORM_AMIGA
-extern void amigaProbePresentPageSums(const jlSurfaceT *src, uint32_t *frontSum, uint32_t *shadowSum);
+extern void jlAmigaProbePresentPageSums(const jlSurfaceT *src, uint32_t *frontSum, uint32_t *shadowSum);
// P11 (W2): display-side present verification. After EVERY present in
// a battery covering each run arm -- scattered THIN runs, a full-width
@@ -961,7 +1073,7 @@ static uint8_t PROBE_ATTR probePageSumCheck(void) {
uint32_t shadowSum;
jlStagePresent();
- amigaProbePresentPageSums(gStage, &frontSum, &shadowSum);
+ jlAmigaProbePresentPageSums(jlgStage, &frontSum, &shadowSum);
return (uint8_t)(frontSum != shadowSum);
}
@@ -970,22 +1082,22 @@ static void PROBE_ATTR op_probePresentPageSum(void) {
uint8_t fails;
fails = 0;
- jlSurfaceClear(gStage, 3);
+ jlSurfaceClear(jlgStage, 3);
fails = (uint8_t)(fails + probePageSumCheck());
- jlFillRect(gStage, 48, 80, 16, 16, 5);
- jlFillRect(gStage, 144, 96, 16, 16, 9);
- jlFillRect(gStage, 240, 112, 16, 16, 12);
+ jlFillRect(jlgStage, 48, 80, 16, 16, 5);
+ jlFillRect(jlgStage, 144, 96, 16, 16, 9);
+ jlFillRect(jlgStage, 240, 112, 16, 16, 12);
fails = (uint8_t)(fails + probePageSumCheck());
- jlFillRect(gStage, 0, 0, 320, 1, 7);
- jlFillRect(gStage, 316, 199, 4, 1, 8);
- jlFillRect(gStage, 100, 150, 8, 1, 4);
- jlFillRect(gStage, 108, 151, 8, 1, 5);
- jlFillRect(gStage, 90, 152, 40, 1, 6);
+ jlFillRect(jlgStage, 0, 0, 320, 1, 7);
+ jlFillRect(jlgStage, 316, 199, 4, 1, 8);
+ jlFillRect(jlgStage, 100, 150, 8, 1, 4);
+ jlFillRect(jlgStage, 108, 151, 8, 1, 5);
+ jlFillRect(jlgStage, 90, 152, 40, 1, 6);
fails = (uint8_t)(fails + probePageSumCheck());
fails = (uint8_t)(fails + probePageSumCheck());
- jlFillRect(gStage, 8, 8, 16, 16, 10);
+ jlFillRect(jlgStage, 8, 8, 16, 16, 10);
fails = (uint8_t)(fails + probePageSumCheck());
- jlFillRect(gStage, 288, 176, 16, 16, 11);
+ jlFillRect(jlgStage, 288, 176, 16, 16, 11);
fails = (uint8_t)(fails + probePageSumCheck());
// Narrow-tall column (P7-3 audit gap): clear + settle to isolate the
// run, then a 16-px-wide x 96-row fill coalesces to ONE run whose
@@ -993,10 +1105,10 @@ static void PROBE_ATTR op_probePresentPageSum(void) {
// threshold with a NARROW span (blitWords ~2, modBytes ~36). The
// wide present96 band (modBytes 8, blitWords 16) never exercises the
// narrow modulo-blit path; this arm verifies it displays correctly.
- jlSurfaceClear(gStage, 3);
+ jlSurfaceClear(jlgStage, 3);
fails = (uint8_t)(fails + probePageSumCheck());
fails = (uint8_t)(fails + probePageSumCheck());
- jlFillRect(gStage, 48, 40, 16, 96, 13);
+ jlFillRect(jlgStage, 48, 40, 16, 96, 13);
fails = (uint8_t)(fails + probePageSumCheck());
fails = (uint8_t)(fails + probePageSumCheck());
fails = (uint8_t)(fails + probePageSumCheck());
@@ -1006,8 +1118,55 @@ static void PROBE_ATTR op_probePresentPageSum(void) {
#endif
+#ifdef JOEYLIB_PLATFORM_X68000
+extern uint32_t jlX68kProbeDisplayMismatch(const jlSurfaceT *src);
+
+// The X68000's display-side check: the page each present draws (the one
+// the next display start shows) must equal the stage's planes. It draws
+// into the page not shown, which holds the frame before last, from the
+// union of two frames' dirty bands, taken four rows at a time: so a tall
+// band then a small rect inside it (P10's trap), one row, a staircase
+// across a group boundary, both corner rows, and two windows two presents
+// apart, each followed by a present and a check, then the settle and idle
+// presents. Every mismatch count must be 0.
+static uint32_t PROBE_ATTR probeX68kPresentCheck(void) {
+ jlStagePresent();
+ return jlX68kProbeDisplayMismatch(jlgStage);
+}
+
+
+static void PROBE_ATTR op_probeX68kDisplayCheck(void) {
+ uint32_t mismatch;
+
+ jlSurfaceClear(jlgStage, 3);
+ mismatch = probeX68kPresentCheck();
+ mismatch += probeX68kPresentCheck();
+ jlFillRect(jlgStage, 0, 50, 320, 100, 12);
+ mismatch += probeX68kPresentCheck();
+ jlFillRect(jlgStage, 100, 60, 40, 10, 9);
+ mismatch += probeX68kPresentCheck();
+ jlFillRect(jlgStage, 64, 33, 16, 1, 5);
+ mismatch += probeX68kPresentCheck();
+ jlFillRect(jlgStage, 100, 150, 8, 1, 4);
+ jlFillRect(jlgStage, 108, 151, 8, 1, 5);
+ jlFillRect(jlgStage, 90, 152, 40, 1, 6);
+ mismatch += probeX68kPresentCheck();
+ jlFillRect(jlgStage, 0, 0, 320, 1, 7);
+ jlFillRect(jlgStage, 316, 199, 4, 1, 8);
+ mismatch += probeX68kPresentCheck();
+ jlFillRect(jlgStage, 8, 8, 16, 16, 10);
+ mismatch += probeX68kPresentCheck();
+ jlFillRect(jlgStage, 288, 176, 16, 16, 11);
+ mismatch += probeX68kPresentCheck();
+ mismatch += probeX68kPresentCheck();
+ mismatch += probeX68kPresentCheck();
+ jlLogF("UBER-PROBE: x68kDisplay %s (mismatch=%lu)", mismatch == 0ul ? "PASS" : "FAIL", (unsigned long)mismatch);
+}
+#endif
+
+
#ifdef JOEYLIB_PLATFORM_DOS
-extern uint32_t dosProbeVgaMismatch(const jlSurfaceT *src);
+extern uint32_t jlDosProbeVgaMismatch(const jlSurfaceT *src);
// P12 (perf-hunt-2 rank 4): DOS display-side parity -- the gate the
// other three ports already had (P9/P10/P11) and DOS lacked. DOS
@@ -1017,24 +1176,24 @@ extern uint32_t dosProbeVgaMismatch(const jlSurfaceT *src);
// row, short expand, wrong LUT word) is invisible without this.
// Battery mirrors P10: full clear, tall band, small inner rect,
// settle present, idle present -- mismatch must be 0 after each.
-// dosProbeVgaMismatch re-derives every byte independently of the
+// jlDosProbeVgaMismatch re-derives every byte independently of the
// gExpandLut fast path.
static void PROBE_ATTR op_probeVgaCheck(void) {
uint32_t afterB;
uint32_t afterSettle;
uint32_t afterIdle;
- jlSurfaceClear(gStage, 3);
+ jlSurfaceClear(jlgStage, 3);
jlStagePresent();
- jlFillRect(gStage, 0, 50, 320, 100, 3);
+ jlFillRect(jlgStage, 0, 50, 320, 100, 3);
jlStagePresent();
- jlFillRect(gStage, 100, 60, 40, 10, 9);
+ jlFillRect(jlgStage, 100, 60, 40, 10, 9);
jlStagePresent();
- afterB = dosProbeVgaMismatch(gStage);
+ afterB = jlDosProbeVgaMismatch(jlgStage);
jlStagePresent();
- afterSettle = dosProbeVgaMismatch(gStage);
+ afterSettle = jlDosProbeVgaMismatch(jlgStage);
jlStagePresent();
- afterIdle = dosProbeVgaMismatch(gStage);
+ afterIdle = jlDosProbeVgaMismatch(jlgStage);
jlLogF("UBER-PROBE: vgaMismatch afterB=%lu afterSettle=%lu afterIdle=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle);
}
#endif
@@ -1042,20 +1201,20 @@ static void PROBE_ATTR op_probeVgaCheck(void) {
#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))
+extern uint32_t jlDvxPortProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared);
+#define PROBE_WINDOW_MISMATCH(_s, _c) jlDvxPortProbeDisplayMismatch((_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))
+extern uint32_t jlHostWin16ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared);
+#define PROBE_WINDOW_MISMATCH(_s, _c) jlHostWin16ProbeDisplayMismatch((_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))
+extern uint32_t jlHostWin32ProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared);
+#define PROBE_WINDOW_MISMATCH(_s, _c) jlHostWin32ProbeDisplayMismatch((_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))
+extern uint32_t jlHostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *compared);
+#define PROBE_WINDOW_MISMATCH(_s, _c) jlHostBeProbeDisplayMismatch((_s), (_c))
#define PROBE_WINDOW_TAG "beDisplayMismatch"
#endif
@@ -1063,7 +1222,7 @@ extern uint32_t hostBeProbeDisplayMismatch(const jlSurfaceT *src, uint32_t *comp
// 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; dvxPortProbeDisplayMismatch derives every pixel
+// full-screen backbuffer; jlDvxPortProbeDisplayMismatch 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
@@ -1108,10 +1267,10 @@ static void PROBE_ATTR op_probeWindowDisplayCheck(void) {
uint16_t y;
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
- savedScb[y] = jlScbGet(gStage, y);
+ savedScb[y] = jlScbGet(jlgStage, y);
}
- jlPaletteGet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette);
- jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, probePalette);
+ jlPaletteGet(jlgStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette);
+ jlPaletteSet(jlgStage, (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
@@ -1119,43 +1278,43 @@ static void PROBE_ATTR op_probeWindowDisplayCheck(void) {
jlStagePresent();
jlWaitVBL();
- jlSurfaceClear(gStage, 3);
+ jlSurfaceClear(jlgStage, 3);
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));
+ jlFillRect(jlgStage, x, 0, 1, PROBE_DVX_STRIP_H, (uint8_t)(x % JL_SURFACE_COLORS_PER_PALETTE));
}
jlStagePresent();
jlWaitVBL();
- jlFillRect(gStage, 0, 50, 320, 100, 3);
+ jlFillRect(jlgStage, 0, 50, 320, 100, 3);
jlStagePresent();
jlWaitVBL();
- jlFillRect(gStage, 101, 61, 37, 9, 9);
+ jlFillRect(jlgStage, 101, 61, 37, 9, 9);
jlStagePresent();
- afterB = PROBE_WINDOW_MISMATCH(gStage, &compared);
+ afterB = PROBE_WINDOW_MISMATCH(jlgStage, &compared);
jlStagePresent();
- afterSettle = PROBE_WINDOW_MISMATCH(gStage, &compared);
+ afterSettle = PROBE_WINDOW_MISMATCH(jlgStage, &compared);
jlStagePresent();
- afterIdle = PROBE_WINDOW_MISMATCH(gStage, &compared);
- jlScbSetRange(gStage, 0, PROBE_DVX_SCB_LAST, (uint8_t)PROBE_DVX_PALETTE);
+ afterIdle = PROBE_WINDOW_MISMATCH(jlgStage, &compared);
+ jlScbSetRange(jlgStage, 0, PROBE_DVX_SCB_LAST, (uint8_t)PROBE_DVX_PALETTE);
jlStagePresent();
- afterScb = PROBE_WINDOW_MISMATCH(gStage, &compared);
- jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, changedPalette);
+ afterScb = PROBE_WINDOW_MISMATCH(jlgStage, &compared);
+ jlPaletteSet(jlgStage, (uint8_t)PROBE_DVX_PALETTE, changedPalette);
jlStagePresent();
- afterPalette = PROBE_WINDOW_MISMATCH(gStage, &compared);
+ afterPalette = PROBE_WINDOW_MISMATCH(jlgStage, &compared);
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
- jlScbSet(gStage, y, (uint8_t)(y % JL_SURFACE_PALETTE_COUNT));
+ jlScbSet(jlgStage, y, (uint8_t)(y % JL_SURFACE_PALETTE_COUNT));
}
jlStagePresent();
- afterAll = PROBE_WINDOW_MISMATCH(gStage, &compared);
+ afterAll = PROBE_WINDOW_MISMATCH(jlgStage, &compared);
for (y = 0; y < JL_SURFACE_HEIGHT; y++) {
- jlScbSet(gStage, y, savedScb[y]);
+ jlScbSet(jlgStage, y, savedScb[y]);
}
- jlPaletteSet(gStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette);
+ jlPaletteSet(jlgStage, (uint8_t)PROBE_DVX_PALETTE, savedPalette);
jlStagePresent();
- afterRestore = PROBE_WINDOW_MISMATCH(gStage, &compared);
- jlFillRect(gStage, 7, 21, 13, 5, 12);
- jlFillRect(gStage, 251, 171, 31, 17, 6);
+ afterRestore = PROBE_WINDOW_MISMATCH(jlgStage, &compared);
+ jlFillRect(jlgStage, 7, 21, 13, 5, 12);
+ jlFillRect(jlgStage, 251, 171, 31, 17, 6);
jlStagePresent();
- afterTwo = PROBE_WINDOW_MISMATCH(gStage, &compared);
+ afterTwo = PROBE_WINDOW_MISMATCH(jlgStage, &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
@@ -1171,10 +1330,10 @@ static void PROBE_ATTR op_probeCleanNoPresent(void) {
int16_t i;
for (i = 0; i < UBER_FRAME_SPRITES; i++) {
- jlSurfaceCopyRect(gStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16);
+ jlSurfaceCopyRect(jlgStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16);
}
for (i = 0; i < UBER_FRAME_SPRITES; i++) {
- jlSpriteDraw(gStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i]);
+ jlSpriteDraw(jlgStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i]);
}
}
@@ -1183,7 +1342,7 @@ static void PROBE_ATTR op_probeCleanErases(void) {
int16_t i;
for (i = 0; i < UBER_FRAME_SPRITES; i++) {
- jlSurfaceCopyRect(gStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16);
+ jlSurfaceCopyRect(jlgStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16);
}
}
#endif
@@ -1203,19 +1362,19 @@ static void PROBE_ATTR probeOp(const char *name, void (*fn)(void)) {
uint32_t bytes0;
uint32_t rows0;
- bytes0 = gPresentCopiedBytes;
- rows0 = gPresentScannedRows;
+ bytes0 = jlgPresentCopiedBytes;
+ rows0 = jlgPresentScannedRows;
timeOp(name, fn);
- jlLogF("UBER-PROBE: %s: copied=%lu scanned=%lu", name, (unsigned long)(gPresentCopiedBytes - bytes0), (unsigned long)(gPresentScannedRows - rows0));
+ jlLogF("UBER-PROBE: %s: copied=%lu scanned=%lu", name, (unsigned long)(jlgPresentCopiedBytes - bytes0), (unsigned long)(jlgPresentScannedRows - rows0));
// Steady-delta second pass (W2): one more call OUTSIDE the timed
// window. Its single-op delta is the exact steady-state gate value
// -- the timed average carries a first-iteration union carry-in on
// the A/B-page ports (iteration 1 unions with the PREVIOUS row's
// leftover band, e.g. P2's first present drags P1's 96-row band).
- bytes0 = gPresentCopiedBytes;
- rows0 = gPresentScannedRows;
+ bytes0 = jlgPresentCopiedBytes;
+ rows0 = jlgPresentScannedRows;
fn();
- jlLogF("UBER-PROBE: %s steady: copied=%lu scanned=%lu", name, (unsigned long)(gPresentCopiedBytes - bytes0), (unsigned long)(gPresentScannedRows - rows0));
+ jlLogF("UBER-PROBE: %s steady: copied=%lu scanned=%lu", name, (unsigned long)(jlgPresentCopiedBytes - bytes0), (unsigned long)(jlgPresentScannedRows - rows0));
}
#endif
#endif
@@ -1283,7 +1442,7 @@ static void PROBE_ATTR op_probeCompactEpoch(void) {
jlSpriteT *victim;
jlSpriteT *survivor;
- jlSurfaceClear(gStage, 0);
+ jlSurfaceClear(jlgStage, 0);
jlStagePresent();
victim = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
survivor = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
@@ -1297,15 +1456,15 @@ static void PROBE_ATTR op_probeCompactEpoch(void) {
}
return;
}
- jlSpriteDraw(gStage, survivor, 32, 40);
+ jlSpriteDraw(jlgStage, survivor, 32, 40);
jlSpriteDestroy(victim);
jlSpriteCompact();
- jlSpriteDraw(gStage, survivor, 96, 40);
- jlSpriteDraw(gStage, survivor, 129, 60);
+ jlSpriteDraw(jlgStage, survivor, 96, 40);
+ jlSpriteDraw(jlgStage, survivor, 129, 60);
jlSpriteCompact();
- jlSpriteDraw(gStage, survivor, 160, 80);
+ jlSpriteDraw(jlgStage, survivor, 160, 80);
jlSpriteDestroy(survivor);
- jlLogF("UBER-PROBE: compactEpoch hash=%08lX bandSum=%lu", (unsigned long)jlSurfaceHash(gStage), (unsigned long)probeBandFold());
+ jlLogF("UBER-PROBE: compactEpoch hash=%08lX bandSum=%lu", (unsigned long)jlSurfaceHash(jlgStage), (unsigned long)probeBandFold());
}
#endif
@@ -1394,18 +1553,18 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) {
uint16_t held;
uberMbPhase(UBER_PHASE_SHOWCASE);
- jlPaletteSet(gStage, 0, gShowcasePal);
- jlScbSetRange(gStage, 0, 199, 0);
- jlSurfaceClear(gStage, SC_BG_COLOR);
+ jlPaletteSet(jlgStage, 0, gShowcasePal);
+ jlScbSetRange(jlgStage, 0, 199, 0);
+ jlSurfaceClear(jlgStage, SC_BG_COLOR);
// Top strip: all 16 palette entries as swatches.
for (k = 0; k < SC_PAL_COUNT; k++) {
- jlFillRect(gStage, (int16_t)(k * SC_PAL_SWATCH_W), 0, SC_PAL_SWATCH_W, SC_PAL_STRIP_H, (uint8_t)k);
+ jlFillRect(jlgStage, (int16_t)(k * SC_PAL_SWATCH_W), 0, SC_PAL_SWATCH_W, SC_PAL_STRIP_H, (uint8_t)k);
}
for (cell = 0; cell < SC_CELLS; cell++) {
showcaseCellRect(cell, &x, &y, &w, &h);
- jlDrawRect(gStage, x, y, (uint16_t)w, (uint16_t)h, SC_BORDER_COLOR);
+ jlDrawRect(jlgStage, x, y, (uint16_t)w, (uint16_t)h, SC_BORDER_COLOR);
ix = (int16_t)(x + SC_INSET);
iy = (int16_t)(y + SC_INSET);
iw = (int16_t)(w - 2 * SC_INSET);
@@ -1419,51 +1578,51 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) {
// Pixels: a scatter of single plotted pixels.
for (py = iy; py < iy + ih; py += 3) {
for (px = ix; px < ix + iw; px += 3) {
- jlDrawPixel(gStage, px, py, (uint8_t)(2 + ((px + py) % 14)));
+ jlDrawPixel(jlgStage, px, py, (uint8_t)(2 + ((px + py) % 14)));
}
}
break;
case 1:
// Horizontal lines.
for (k = 0; k < ih; k += 4) {
- jlDrawLine(gStage, ix, (int16_t)(iy + k), (int16_t)(ix + iw - 1), (int16_t)(iy + k), (uint8_t)(2 + (k / 4) % 14));
+ jlDrawLine(jlgStage, ix, (int16_t)(iy + k), (int16_t)(ix + iw - 1), (int16_t)(iy + k), (uint8_t)(2 + (k / 4) % 14));
}
break;
case 2:
// Vertical lines.
for (k = 0; k < iw; k += 4) {
- jlDrawLine(gStage, (int16_t)(ix + k), iy, (int16_t)(ix + k), (int16_t)(iy + ih - 1), (uint8_t)(2 + (k / 4) % 14));
+ jlDrawLine(jlgStage, (int16_t)(ix + k), iy, (int16_t)(ix + k), (int16_t)(iy + ih - 1), (uint8_t)(2 + (k / 4) % 14));
}
break;
case 3:
// Diagonals: an X plus a fan from the center.
- jlDrawLine(gStage, ix, iy, (int16_t)(ix + iw - 1), (int16_t)(iy + ih - 1), 5);
- jlDrawLine(gStage, ix, (int16_t)(iy + ih - 1), (int16_t)(ix + iw - 1), iy, 6);
+ jlDrawLine(jlgStage, ix, iy, (int16_t)(ix + iw - 1), (int16_t)(iy + ih - 1), 5);
+ jlDrawLine(jlgStage, ix, (int16_t)(iy + ih - 1), (int16_t)(ix + iw - 1), iy, 6);
for (k = 0; k < iw; k += 8) {
- jlDrawLine(gStage, cx, cy, (int16_t)(ix + k), iy, (uint8_t)(8 + (k / 8) % 8));
+ jlDrawLine(jlgStage, cx, cy, (int16_t)(ix + k), iy, (uint8_t)(8 + (k / 8) % 8));
}
break;
case 4:
// Rectangle outlines, concentric.
for (k = 0; 2 * k < minDim - 4; k += 5) {
- jlDrawRect(gStage, (int16_t)(ix + k), (int16_t)(iy + k), (uint16_t)(iw - 2 * k), (uint16_t)(ih - 2 * k), (uint8_t)(2 + (k / 5) % 14));
+ jlDrawRect(jlgStage, (int16_t)(ix + k), (int16_t)(iy + k), (uint16_t)(iw - 2 * k), (uint16_t)(ih - 2 * k), (uint8_t)(2 + (k / 5) % 14));
}
break;
case 5:
// Filled rectangles, overlapping.
- jlFillRect(gStage, ix, iy, (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 2);
- jlFillRect(gStage, (int16_t)(ix + iw / 3), (int16_t)(iy + ih / 3), (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 4);
+ jlFillRect(jlgStage, ix, iy, (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 2);
+ jlFillRect(jlgStage, (int16_t)(ix + iw / 3), (int16_t)(iy + ih / 3), (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 4);
break;
case 6:
// Circle outlines, concentric.
for (r = (int16_t)(minDim / 2); r > 2; r -= 4) {
- jlDrawCircle(gStage, cx, cy, (uint16_t)r, (uint8_t)(2 + (r / 4) % 14));
+ jlDrawCircle(jlgStage, cx, cy, (uint16_t)r, (uint8_t)(2 + (r / 4) % 14));
}
break;
case 7:
// Filled circles.
- jlFillCircle(gStage, cx, cy, (uint16_t)(minDim / 2 - 1), 8);
- jlFillCircle(gStage, cx, cy, (uint16_t)(minDim / 4), 5);
+ jlFillCircle(jlgStage, cx, cy, (uint16_t)(minDim / 2 - 1), 8);
+ jlFillCircle(jlgStage, cx, cy, (uint16_t)(minDim / 4), 5);
break;
case 8:
// Tiles: an 8x8-block checkerboard inside the cell.
@@ -1473,27 +1632,27 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) {
byEnd = (uint8_t)((iy + ih) / 8);
for (by = byStart; by < byEnd; by++) {
for (bx = bxStart; bx < bxEnd; bx++) {
- jlTileFill(gStage, bx, by, (uint8_t)(((bx + by) & 1) ? 6 : 8));
+ jlTileFill(jlgStage, bx, by, (uint8_t)(((bx + by) & 1) ? 6 : 8));
}
}
break;
case 9:
// Sprite: the compiled ball at a few positions.
- jlSpriteDraw(gStage, gSprite, ix, iy);
- jlSpriteDraw(gStage, gSprite, (int16_t)(ix + iw - 16), (int16_t)(iy + ih - 16));
- jlSpriteDraw(gStage, gSprite, (int16_t)(cx - 8), (int16_t)(cy - 8));
+ jlSpriteDraw(jlgStage, gSprite, ix, iy);
+ jlSpriteDraw(jlgStage, gSprite, (int16_t)(ix + iw - 16), (int16_t)(iy + ih - 16));
+ jlSpriteDraw(jlgStage, gSprite, (int16_t)(cx - 8), (int16_t)(cy - 8));
break;
case 10:
// Flood fill: outline a circle, then flood its interior.
- jlDrawCircle(gStage, cx, cy, (uint16_t)(minDim / 2 - 2), 1);
- jlFloodFill(gStage, cx, cy, 12);
+ jlDrawCircle(jlgStage, cx, cy, (uint16_t)(minDim / 2 - 2), 1);
+ jlFloodFill(jlgStage, cx, cy, 12);
break;
default:
// Mini scene: ground, sun, horizon, ball.
- jlFillRect(gStage, ix, (int16_t)(iy + ih / 2), (uint16_t)iw, (uint16_t)(ih - ih / 2), 4);
- jlFillCircle(gStage, cx, (int16_t)(iy + ih / 3), (uint16_t)(ih / 4), 5);
- jlDrawLine(gStage, ix, (int16_t)(iy + ih / 2), (int16_t)(ix + iw - 1), (int16_t)(iy + ih / 2), 1);
- jlSpriteDraw(gStage, gSprite, (int16_t)(cx - 8), (int16_t)(iy + ih / 2 - 16));
+ jlFillRect(jlgStage, ix, (int16_t)(iy + ih / 2), (uint16_t)iw, (uint16_t)(ih - ih / 2), 4);
+ jlFillCircle(jlgStage, cx, (int16_t)(iy + ih / 3), (uint16_t)(ih / 4), 5);
+ jlDrawLine(jlgStage, ix, (int16_t)(iy + ih / 2), (int16_t)(ix + iw - 1), (int16_t)(iy + ih / 2), 1);
+ jlSpriteDraw(jlgStage, gSprite, (int16_t)(cx - 8), (int16_t)(iy + ih / 2 - 16));
break;
}
}
@@ -1524,7 +1683,7 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) {
// PRNG golden sequence, arena bookkeeping).
static void __attribute__((noinline)) chkHash(const char *name) {
- jlLogF("UBER-CHK: %s: hash=%08lX", name, (unsigned long)jlSurfaceHash(gStage));
+ jlLogF("UBER-CHK: %s: hash=%08lX", name, (unsigned long)jlSurfaceHash(jlgStage));
}
@@ -1539,30 +1698,30 @@ static void __attribute__((noinline)) chkPassFail(const char *name, bool pass) {
// globals; Phase 1 turns these into true no-ops.
static void __attribute__((noinline)) checkEdgeDraws(void) {
uberMbOp(101);
- jlDrawPixel(gStage, -5, 100, 5);
- jlDrawPixel(gStage, 330, 100, 5);
- jlDrawPixel(gStage, 100, -3, 5);
- jlDrawPixel(gStage, 100, 210, 5);
+ jlDrawPixel(jlgStage, -5, 100, 5);
+ jlDrawPixel(jlgStage, 330, 100, 5);
+ jlDrawPixel(jlgStage, 100, -3, 5);
+ jlDrawPixel(jlgStage, 100, 210, 5);
chkHash("edge-pixels");
// w=65535 draws phantom edges today (finding #11); h==2 exercises the
// zero-height interior-edge fills; the third rect clips normally.
- jlDrawRect(gStage, 10, 10, 65535u, 100, 5);
- jlDrawRect(gStage, 50, 20, 30, 2, 6);
- jlDrawRect(gStage, -10, 150, 340, 40, 7);
+ jlDrawRect(jlgStage, 10, 10, 65535u, 100, 5);
+ jlDrawRect(jlgStage, 50, 20, 30, 2, 6);
+ jlDrawRect(jlgStage, -10, 150, 340, 40, 7);
chkHash("edge-drawRect");
// r=300 clips every span; r=40000 should cover the surface but draws
// nothing today (finding #12).
- jlFillCircle(gStage, 160, 100, 300, 4);
- jlFillCircle(gStage, 160, 100, 40000u, 9);
- jlDrawCircle(gStage, 10, 10, 40000u, 3);
+ jlFillCircle(jlgStage, 160, 100, 300, 4);
+ jlFillCircle(jlgStage, 160, 100, 40000u, 9);
+ jlDrawCircle(jlgStage, 10, 10, 40000u, 3);
chkHash("edge-circles");
// Far-off-surface endpoints. The H line should fill the whole visible
// row but draws nothing today (finding #72: the H/V fast path clamps
// span to 320 BEFORE clipping, so a huge span anchored off-surface
// clips away entirely). The diagonal clips per-pixel and is correct.
- jlDrawLine(gStage, -20000, 50, 20000, 50, 2);
- jlDrawLine(gStage, 60, -20000, 60, 20000, 2);
- jlDrawLine(gStage, -300, -200, 620, 400, 8);
+ jlDrawLine(jlgStage, -20000, 50, 20000, 50, 2);
+ jlDrawLine(jlgStage, 60, -20000, 60, 20000, 2);
+ jlDrawLine(jlgStage, -300, -200, 620, 400, 8);
chkHash("edge-lines");
}
@@ -1576,18 +1735,18 @@ static void __attribute__((noinline)) checkClippedSprites(void) {
uint32_t before;
uberMbOp(102);
- jlSpriteDraw(gStage, gSprite, -8, 50);
- jlSpriteDraw(gStage, gSprite, 312, 50);
- jlSpriteDraw(gStage, gSprite, 150, -8);
- jlSpriteDraw(gStage, gSprite, 150, 192);
- jlSpriteDraw(gStage, gSprite, -8, -8);
- jlSpriteDraw(gStage, gSprite, 400, 100);
+ jlSpriteDraw(jlgStage, gSprite, -8, 50);
+ jlSpriteDraw(jlgStage, gSprite, 312, 50);
+ jlSpriteDraw(jlgStage, gSprite, 150, -8);
+ jlSpriteDraw(jlgStage, gSprite, 150, 192);
+ jlSpriteDraw(jlgStage, gSprite, -8, -8);
+ jlSpriteDraw(jlgStage, gSprite, 400, 100);
chkHash("sprite-clipped");
- before = jlSurfaceHash(gStage);
- jlSpriteSaveUnder(gStage, gSprite, -8, 100, &gBackup);
- jlSpriteDraw(gStage, gSprite, -8, 100);
- jlSpriteRestoreUnder(gStage, &gBackup);
- chkPassFail("sprite-clip-roundtrip", jlSurfaceHash(gStage) == before);
+ before = jlSurfaceHash(jlgStage);
+ jlSpriteSaveUnder(jlgStage, gSprite, -8, 100, &gBackup);
+ jlSpriteDraw(jlgStage, gSprite, -8, 100);
+ jlSpriteRestoreUnder(jlgStage, &gBackup);
+ chkPassFail("sprite-clip-roundtrip", jlSurfaceHash(jlgStage) == before);
}
@@ -1599,11 +1758,11 @@ static void __attribute__((noinline)) checkTileMonoFlood(void) {
for (i = 0; i < JL_TILE_BYTES; i++) {
mono.pixels[i] = (uint8_t)((i & 1) ? 0x0F : 0xF0);
}
- jlTilePasteMono(gStage, 10, 10, &mono, 5, 9);
- jlTilePasteMono(gStage, 11, 10, &mono, 14, 0);
+ jlTilePasteMono(jlgStage, 10, 10, &mono, 5, 9);
+ jlTilePasteMono(jlgStage, 11, 10, &mono, 14, 0);
chkHash("tilePasteMono");
- jlDrawRect(gStage, 240, 150, 40, 30, 1);
- jlFloodFill(gStage, 250, 160, 12);
+ jlDrawRect(jlgStage, 240, 150, 40, 30, 1);
+ jlFloodFill(jlgStage, 250, 160, 12);
chkHash("floodFill");
}
@@ -1619,11 +1778,11 @@ static void __attribute__((noinline)) checkDrawText(void) {
// 'A' -> tile (5,5), 'B' -> tile (6,6). Seed both glyph tiles here --
// the section-opening surface clear wiped whatever the timed tile ops
// left there.
- jlTileFill(gStage, 5, 5, 9);
- jlTileFill(gStage, 6, 6, 3);
+ jlTileFill(jlgStage, 5, 5, 9);
+ jlTileFill(jlgStage, 6, 6, 3);
asciiMap['A'] = (uint16_t)(5u | (5u << 8));
asciiMap['B'] = (uint16_t)(6u | (6u << 8));
- jlDrawText(gStage, 2, 20, gStage, asciiMap, "ABBA");
+ jlDrawText(jlgStage, 2, 20, jlgStage, asciiMap, "ABBA");
chkHash("drawText");
// Out-of-range start: the entry sanitation loop (Phase 5, #22)
// reproduces the historical absorb-and-wrap semantics bit-for-bit
@@ -1631,7 +1790,7 @@ static void __attribute__((noinline)) checkDrawText(void) {
// to (0,21) and draws) while guaranteeing jlpTileCopyMasked never
// sees an out-of-range destination. Separate hash line so any
// future semantic change stays isolated.
- jlDrawText(gStage, 200, 20, gStage, asciiMap, "AB");
+ jlDrawText(jlgStage, 200, 20, jlgStage, asciiMap, "AB");
chkHash("drawText-offgrid");
}
@@ -1649,8 +1808,8 @@ static void __attribute__((noinline)) checkPalette(void) {
// is forced to $000 even when the caller passes nonzero, and
// conforming $0RGB entries 1..15 round-trip exactly.
uberMbOp(105);
- jlPaletteSet(gStage, 2, conforming);
- jlPaletteGet(gStage, 2, readBack);
+ jlPaletteSet(jlgStage, 2, conforming);
+ jlPaletteGet(jlgStage, 2, readBack);
ok = (readBack[0] == 0x0000u);
for (i = 1; i < 16u; i++) {
if (readBack[i] != conforming[i]) {
@@ -1729,11 +1888,11 @@ static void __attribute__((noinline)) checkAllocator(void) {
uberMbOp(117);
chkPassFail("arena-churn", jlSpriteCodegenBytesUsed() == usedBefore);
uberMbOp(118);
- a = jlSpriteCreateFromSurface(gStage, 0, 0, 2, 2);
+ a = jlSpriteCreateFromSurface(jlgStage, 0, 0, 2, 2);
if (a != NULL) {
(void)jlSpriteCompile(a);
uberMbOp(119);
- jlSpriteDraw(gStage, a, 280, 20);
+ jlSpriteDraw(jlgStage, a, 280, 20);
jlSpriteDestroy(a);
}
chkPassFail("sprite-from-surface", a != NULL);
@@ -1758,25 +1917,146 @@ static void __attribute__((noinline)) checkTileMonoFloodExtra(void) {
for (i = 0; i < JL_TILE_BYTES; i++) {
monoAll.pixels[i] = comboBytes[i & 3u];
}
- jlTilePasteMono(gStage, 10, 12, &monoAll, 5, 9);
- jlTilePasteMono(gStage, 11, 12, &monoAll, 14, 0);
+ jlTilePasteMono(jlgStage, 10, 12, &monoAll, 5, 9);
+ jlTilePasteMono(jlgStage, 11, 12, &monoAll, 14, 0);
chkHash("tilePasteMonoAll");
// Bounded flood: an enclosed color-3 box over a pre-cleared interior
// so the fill stays contained and drives the bounded stop mask
// (pix == boundary || pix == new) plus the planar group-skip across
// several 16px groups. matchColor = boundaryColor = 3, matchEqual = 0.
- jlFillRect(gStage, 40, 140, 64, 40, 0);
- jlDrawRect(gStage, 44, 144, 52, 32, 3);
- jlFloodFillBounded(gStage, 70, 160, 7, 3);
+ jlFillRect(jlgStage, 40, 140, 64, 40, 0);
+ jlDrawRect(jlgStage, 44, 144, 52, 32, 3);
+ jlFloodFillBounded(jlgStage, 70, 160, 7, 3);
chkHash("floodFillBounded");
}
+// Tile layers over moves of every size: a pixel, a tile, back, a jump
+// across the map's seams (64x40 tiles = 512x320 pixels) both ways, a
+// jump of more than a view, an origin past the map's size, and cell
+// changes in and out of view. The layer is a band (rows 24-159) over a
+// stage of colour 6, so the rows outside it are in every hash. One hash
+// folds the stage's after each draw.
+#define UBER_LAYER_CHECK_MOVES 20
+
+static void __attribute__((noinline)) checkTileLayer(void) {
+ static const uint16_t moves[UBER_LAYER_CHECK_MOVES][2] = {
+ { 0, 0 }, { 1, 0 }, { 2, 1 }, { 3, 3 }, { 8, 8 },
+ { 9, 15 }, { 17, 16 }, { 16, 17 }, { 15, 9 }, { 200, 5 },
+ { 505, 318 }, { 511, 319 }, { 0, 0 }, { 7, 316 }, { 330, 170 },
+ { 10, 10 }, { 1023, 650 }, { 4, 4 }, { 3, 2 }, { 2, 7 }
+ };
+ jlTileLayerT *layer;
+ uint32_t h;
+ uint8_t i;
+
+ layerData();
+ jlSurfaceClear(jlgStage, 6);
+ layer = jlTileLayerCreate(gLayerTiles, gLayerMap, UBER_LAYER_MAP_W, UBER_LAYER_MAP_H, 3, 17);
+ if (layer == NULL) {
+ chkPassFail("tileLayer moves (layer alloc)", false);
+ return;
+ }
+ h = 0;
+ for (i = 0; i < UBER_LAYER_CHECK_MOVES; i++) {
+ if (i == 6) {
+ // In the last draw's view (origin 9,15: cells 1-41 by 1-18) and out of it.
+ jlTileLayerSetTile(layer, 5, 4, 3);
+ jlTileLayerSetTile(layer, 50, 30, JL_TILE_MAP_SKIP);
+ }
+ jlTileLayerDraw(jlgStage, layer, moves[i][0], moves[i][1]);
+ h = h * 31u + jlSurfaceHash(jlgStage);
+ }
+ jlLogF("UBER-CHK: tileLayer moves: hash=%08lX", (unsigned long)h);
+ jlTileLayerDestroy(layer);
+}
+
+
+// jlTilePasteGlyph, which nothing else here calls: every colour on both
+// sides of a glyph whose rows hold every pixel pair, on the stage (each
+// port's own paste, mark included) and on another surface, with colours
+// above 15 (their low nibble is the colour) and the calls that must draw
+// nothing -- a cell off the surface, no bits, no surface. One hash folds
+// both surfaces'.
+static void __attribute__((noinline)) checkTilePasteGlyph(void) {
+ static const uint8_t glyph[8] = { 0x00u, 0xFFu, 0x0Fu, 0xF0u, 0x55u, 0xAAu, 0x3Cu, 0x81u };
+ jlSurfaceT *other;
+ uint32_t h;
+ uint8_t c;
+
+ uberMbOp(122);
+ jlSurfaceClear(jlgStage, 6);
+ for (c = 0; c < 16u; c++) {
+ jlTilePasteGlyph(jlgStage, c, 3, glyph, c, (uint8_t)(15u - c));
+ jlTilePasteGlyph(jlgStage, (uint8_t)(c + 16u), 4, glyph, (uint8_t)(c | 0x30u), c);
+ jlTilePasteGlyph(jlgStage, (uint8_t)(c + 8u), 22, glyph, (uint8_t)(c ^ 5u), (uint8_t)(c | 0xC0u));
+ }
+ jlTilePasteGlyph(jlgStage, 39, 24, glyph, 1, 2);
+ jlTilePasteGlyph(jlgStage, 40, 5, glyph, 1, 2);
+ jlTilePasteGlyph(jlgStage, 5, 25, glyph, 1, 2);
+ jlTilePasteGlyph(jlgStage, 6, 6, NULL, 1, 2);
+ jlTilePasteGlyph(NULL, 6, 6, glyph, 1, 2);
+ h = jlSurfaceHash(jlgStage);
+ other = jlSurfaceCreate();
+ if (other == NULL) {
+ chkPassFail("tilePasteGlyph (surface alloc)", false);
+ return;
+ }
+ jlSurfaceClear(other, 9);
+ for (c = 0; c < 16u; c++) {
+ jlTilePasteGlyph(other, (uint8_t)(c * 2u), (uint8_t)(c + 2u), glyph, c, (uint8_t)(c + 7u));
+ }
+ jlTilePasteGlyph(other, 0, 0, glyph, 15, 0);
+ h = h * 31u + jlSurfaceHash(other);
+ jlSurfaceDestroy(other);
+ jlLogF("UBER-CHK: tilePasteGlyph: hash=%08lX", (unsigned long)h);
+}
+
+
+// jlFillRect one row high, the shape a planar picture is pushed in, which
+// UBER's timed fills never are: widths 1 to 41 at every pixel phase, all
+// sixteen colours, both edges of the surface, and rows that clip or are
+// empty -- on the stage and on another surface. One hash folds both.
+static void __attribute__((noinline)) checkFillRectRows(void) {
+ jlSurfaceT *other;
+ uint32_t h;
+ uint16_t i;
+
+ uberMbOp(123);
+ jlSurfaceClear(jlgStage, 5);
+ for (i = 0; i < 160u; i++) {
+ jlFillRect(jlgStage, (int16_t)((i * 37u) % 300u), (int16_t)(i + 20u), (uint16_t)(1u + (i % 41u)), 1, (uint8_t)(i & 15u));
+ }
+ jlFillRect(jlgStage, 0, 0, 320, 1, 7);
+ jlFillRect(jlgStage, 316, 199, 4, 1, 8);
+ jlFillRect(jlgStage, 319, 10, 1, 1, 9);
+ jlFillRect(jlgStage, 0, 11, 1, 1, 10);
+ jlFillRect(jlgStage, -5, 12, 10, 1, 11);
+ jlFillRect(jlgStage, 310, 13, 20, 1, 12);
+ jlFillRect(jlgStage, 5, 200, 3, 1, 13);
+ jlFillRect(jlgStage, 5, 14, 0, 1, 14);
+ jlFillRect(jlgStage, 64, 15, 32, 1, 0x1F);
+ h = jlSurfaceHash(jlgStage);
+ other = jlSurfaceCreate();
+ if (other == NULL) {
+ chkPassFail("fillRectRows (surface alloc)", false);
+ return;
+ }
+ jlSurfaceClear(other, 3);
+ for (i = 0; i < 64u; i++) {
+ jlFillRect(other, (int16_t)((i * 53u) % 310u), (int16_t)(i * 3u), (uint16_t)(1u + (i % 33u)), 1, (uint8_t)(15u - (i & 15u)));
+ }
+ h = h * 31u + jlSurfaceHash(other);
+ jlSurfaceDestroy(other);
+ jlLogF("UBER-CHK: fillRectRows: hash=%08lX", (unsigned long)h);
+}
+
+
static void __attribute__((noinline)) runCorrectnessChecks(void) {
uberMbPhase(UBER_PHASE_CHECKS);
jlLogF("UBER-CHK: ----- begin -----");
- jlSurfaceClear(gStage, 0);
+ jlSurfaceClear(jlgStage, 0);
checkEdgeDraws();
checkClippedSprites();
checkTileMonoFlood();
@@ -1785,6 +2065,9 @@ static void __attribute__((noinline)) runCorrectnessChecks(void) {
checkRandom();
checkAllocator();
checkTileMonoFloodExtra();
+ checkTileLayer();
+ checkTilePasteGlyph();
+ checkFillRectRows();
// jlShutdown -> jlInit -> stale-sprite-destroy (#34) needs a teardown
// of the whole library mid-run; it gets a dedicated micro-example in
// Phase 1 rather than risking the benchmark's display state here.
@@ -1818,8 +2101,8 @@ static void __attribute__((noinline)) runAllTests(void) {
timeOp("jlSamplePixel", op_samplePixel);
// Tiles. Seed scratch tile + dest cells with non-zero pixels first.
- jlFillRect(gStage, 0, 0, 320, 64, 7);
- jlTileSnap(gStage, 5, 5, &gTileScratch);
+ jlFillRect(jlgStage, 0, 0, 320, 64, 7);
+ jlTileSnap(jlgStage, 5, 5, &gTileScratch);
timeOp("jlTileFill", op_tileFill);
timeOp("jlTileCopy", op_tileCopy);
timeOp("jlTileCopyMasked", op_tileCopyMasked);
@@ -1828,7 +2111,7 @@ static void __attribute__((noinline)) runAllTests(void) {
// Sprites. Background must be non-empty so save-under has work
// to do (otherwise it's a 4 KB memset of zeros, atypical).
- jlSurfaceClear(gStage, 4);
+ jlSurfaceClear(jlgStage, 4);
timeOp("jlSpriteSaveUnder", op_spriteSave);
timeOp("jlSpriteDraw", op_spriteDraw);
timeOp("jlSpriteRestoreUnder", op_spriteRestore);
@@ -1867,14 +2150,14 @@ static void __attribute__((noinline)) runAllTests(void) {
// Seed two patterned tiles via portable draws + snap. jlTileT.pixels
// is platform-private (plane bytes on ST/Amiga), so hand-written
// bytes would render differently per port; only a snap round-trips.
- jlFillRect(gStage, 0, 72, 4, 4, 3);
- jlFillRect(gStage, 4, 72, 4, 4, 5);
- jlFillRect(gStage, 0, 76, 4, 4, 5);
- jlFillRect(gStage, 4, 76, 4, 4, 3);
- jlTileSnap(gStage, 0, 9, &gTileMapA);
- jlFillRect(gStage, 8, 72, 8, 8, 9);
- jlDrawLine(gStage, 8, 72, 15, 79, 1);
- jlTileSnap(gStage, 1, 9, &gTileMapB);
+ jlFillRect(jlgStage, 0, 72, 4, 4, 3);
+ jlFillRect(jlgStage, 4, 72, 4, 4, 5);
+ jlFillRect(jlgStage, 0, 76, 4, 4, 5);
+ jlFillRect(jlgStage, 4, 76, 4, 4, 3);
+ jlTileSnap(jlgStage, 0, 9, &gTileMapA);
+ jlFillRect(jlgStage, 8, 72, 8, 8, 9);
+ jlDrawLine(jlgStage, 8, 72, 15, 79, 1);
+ jlTileSnap(jlgStage, 1, 9, &gTileMapB);
gTileSet[0] = gTileMapA;
gTileSet[1] = gTileMapB;
{
@@ -1902,7 +2185,7 @@ static void __attribute__((noinline)) runAllTests(void) {
gCleanSurface = jlSurfaceCreate();
if (gCleanSurface != NULL) {
paintFrameStatic();
- jlSurfaceCopy(gCleanSurface, gStage);
+ jlSurfaceCopy(gCleanSurface, jlgStage);
timeOp("gameFrameClean composite", op_gameFrameClean);
jlSurfaceDestroy(gCleanSurface);
gCleanSurface = NULL;
@@ -1910,7 +2193,26 @@ static void __attribute__((noinline)) runAllTests(void) {
jlLogF("UBER: gameFrameClean composite: SKIPPED (surface alloc failed)");
}
+ // Tile layer rows (appended last per the cumulative-hash safety rule).
+ // The first draw fills the whole ring, so it happens before the window.
+ layerData();
+ gLayer = jlTileLayerCreate(gLayerTiles, gLayerMap, UBER_LAYER_MAP_W, UBER_LAYER_MAP_H, 0, JL_TILE_BLOCKS_PER_COL);
+ if (gLayer != NULL) {
+ gLayerX = 0;
+ gLayerY = 0;
+ jlTileLayerDraw(jlgStage, gLayer, gLayerX, gLayerY);
+ timeOpSettled("jlTileLayerDraw scroll", op_layerDraw, settleLayer, UBER_LAYER_FRAMES);
+ timeOpSettled("jlTileLayerDraw scroll + present", op_layerFrame, settleLayer, UBER_LAYER_FRAMES);
+ jlTileLayerDestroy(gLayer);
+ gLayer = NULL;
+ } else {
+ jlLogF("UBER: jlTileLayerDraw scroll: SKIPPED (layer alloc failed)");
+ }
+
#ifdef UBER_PROBE
+ // The presents count from here (the IIgs and hosted ones only while
+ // asked).
+ jlgPresentTelemetry = 1u;
// W0 probe rows, present-cost forensics. Order matters: the idle
// present row above (timed earlier) is P3; P1/P2 mark-and-present
// are self-consistent per call; P4 runs last among the TIMED rows
@@ -1928,22 +2230,22 @@ static void __attribute__((noinline)) runAllTests(void) {
uint32_t probeBytes0;
uint32_t probeRows0;
- probeBytes0 = gPresentCopiedBytes;
- probeRows0 = gPresentScannedRows;
+ probeBytes0 = jlgPresentCopiedBytes;
+ probeRows0 = jlgPresentScannedRows;
timeOp("probe present96", op_probePresent96);
- jlLogF("UBER-PROBE: probe present96: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
- probeBytes0 = gPresentCopiedBytes;
- probeRows0 = gPresentScannedRows;
+ jlLogF("UBER-PROBE: probe present96: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0));
+ probeBytes0 = jlgPresentCopiedBytes;
+ probeRows0 = jlgPresentScannedRows;
op_probePresent96();
- jlLogF("UBER-PROBE: probe present96 steady: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
- probeBytes0 = gPresentCopiedBytes;
- probeRows0 = gPresentScannedRows;
+ jlLogF("UBER-PROBE: probe present96 steady: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0));
+ probeBytes0 = jlgPresentCopiedBytes;
+ probeRows0 = jlgPresentScannedRows;
timeOp("probe present3win", op_probePresent3win);
- jlLogF("UBER-PROBE: probe present3win: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
- probeBytes0 = gPresentCopiedBytes;
- probeRows0 = gPresentScannedRows;
+ jlLogF("UBER-PROBE: probe present3win: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0));
+ probeBytes0 = jlgPresentCopiedBytes;
+ probeRows0 = jlgPresentScannedRows;
op_probePresent3win();
- jlLogF("UBER-PROBE: probe present3win steady: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
+ jlLogF("UBER-PROBE: probe present3win steady: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0));
timeOp("probe frameNoPresent", op_probeFrameNoPresent);
// P7-1 IIgs trusted rows: same geometry (sprite at 40,30 into a
// valid backup) as the golden jlSpriteSaveUnder / RestoreUnder /
@@ -1954,21 +2256,21 @@ static void __attribute__((noinline)) runAllTests(void) {
// rest of this arm (see probeOp).
gProbeRestoreBackup.sprite = gSprite;
gProbeRestoreBackup.bytes = gProbeRestoreBackupBytes;
- jlSpriteSaveUnder(gStage, gSprite, 40, 30, &gProbeRestoreBackup);
+ jlSpriteSaveUnder(jlgStage, gSprite, 40, 30, &gProbeRestoreBackup);
timeOp("probe restoreTrusted", op_probeRestoreTrusted);
timeOp("probe saveTrusted", op_probeSaveTrusted);
timeOp("probe saveAndDrawTrusted", op_probeSaveAndDrawTrusted);
timeOp("probe drawTrusted", op_probeDrawTrusted);
jlStagePresent();
jlStagePresent();
- probeBytes0 = gPresentCopiedBytes;
- probeRows0 = gPresentScannedRows;
+ probeBytes0 = jlgPresentCopiedBytes;
+ probeRows0 = jlgPresentScannedRows;
timeOp("probe presentIdle", op_probePresentIdle);
- jlLogF("UBER-PROBE: probe presentIdle: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
- probeBytes0 = gPresentCopiedBytes;
- probeRows0 = gPresentScannedRows;
+ jlLogF("UBER-PROBE: probe presentIdle: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0));
+ probeBytes0 = jlgPresentCopiedBytes;
+ probeRows0 = jlgPresentScannedRows;
op_probePresentIdle();
- jlLogF("UBER-PROBE: probe presentIdle steady: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0));
+ jlLogF("UBER-PROBE: probe presentIdle steady: copied=%lu scanned=%lu", (unsigned long)(jlgPresentCopiedBytes - probeBytes0), (unsigned long)(jlgPresentScannedRows - probeRows0));
// Hunt-3 rank 1: gameFrameClean component split on IIgs (the
// 16-21 ms unattributed residual). Same recipe as the golden
// row (gCleanSurface was destroyed after it): re-create with
@@ -1978,7 +2280,7 @@ static void __attribute__((noinline)) runAllTests(void) {
gCleanSurface = jlSurfaceCreate();
if (gCleanSurface != NULL) {
paintFrameStatic();
- jlSurfaceCopy(gCleanSurface, gStage);
+ jlSurfaceCopy(gCleanSurface, jlgStage);
timeOp("probe cleanNoPresent", op_probeCleanNoPresent);
timeOp("probe cleanErases", op_probeCleanErases);
jlSurfaceDestroy(gCleanSurface);
@@ -2010,7 +2312,7 @@ static void __attribute__((noinline)) runAllTests(void) {
gProbeRestoreWork = jlSurfaceCreate();
gProbeRestoreBackup.sprite = gSprite;
gProbeRestoreBackup.bytes = gProbeRestoreBackupBytes;
- jlSpriteSaveUnder(gStage, gSprite, 40, 30, &gProbeRestoreBackup);
+ jlSpriteSaveUnder(jlgStage, gSprite, 40, 30, &gProbeRestoreBackup);
if (gProbeRestoreWork != NULL) {
probeOp("probe restoreWork", op_probeRestoreWork);
}
@@ -2025,7 +2327,7 @@ static void __attribute__((noinline)) runAllTests(void) {
gCleanSurface = jlSurfaceCreate();
if (gCleanSurface != NULL) {
paintFrameStatic();
- jlSurfaceCopy(gCleanSurface, gStage);
+ jlSurfaceCopy(gCleanSurface, jlgStage);
probeOp("probe cleanNoPresent", op_probeCleanNoPresent);
probeOp("probe cleanErases", op_probeCleanErases);
jlSurfaceDestroy(gCleanSurface);
@@ -2056,6 +2358,9 @@ static void __attribute__((noinline)) runAllTests(void) {
#ifdef JOEYLIB_PLATFORM_DOS
op_probeVgaCheck();
#endif
+#ifdef JOEYLIB_PLATFORM_X68000
+ op_probeX68kDisplayCheck();
+#endif
#if defined(JOEYLIB_PLATFORM_DVX) || defined(JOEYLIB_HOSTED_WIN16) || defined(JOEYLIB_HOSTED_WIN32) || defined(JOEYLIB_HOSTED_BE)
op_probeWindowDisplayCheck();
#endif
@@ -2083,8 +2388,8 @@ static void __attribute__((noinline)) setupPalette(void) {
pal[ 5] = 0xFF0; // yellow (test pixels)
pal[ 7] = 0xFFF; // white (fills)
pal[15] = 0xF00; // red
- jlPaletteSet(gStage, 0, pal);
- jlScbSetRange(gStage, 0, 199, 0);
+ jlPaletteSet(jlgStage, 0, pal);
+ jlScbSetRange(jlgStage, 0, 199, 0);
}
@@ -2156,8 +2461,8 @@ int main(void) {
* cost the user is chasing; runAllTests dominates. */
startFrame = jlFrameCount();
- gStage = jlStageGet();
- if (gStage == NULL) {
+ jlgStage = jlStageGet();
+ if (jlgStage == NULL) {
jlShutdown();
return 1;
}
@@ -2167,8 +2472,8 @@ int main(void) {
setupPalette();
// Indicate "running": red bar at top of screen.
- jlSurfaceClear(gStage, 0);
- jlFillRect(gStage, 0, 0, 320, 8, 1);
+ jlSurfaceClear(jlgStage, 0);
+ jlFillRect(jlgStage, 0, 0, 320, 8, 1);
jlStagePresent();
if (!setupSprite()) {
@@ -2203,7 +2508,7 @@ int main(void) {
// Done. Green screen + waitForKey.
uberMbPhase(UBER_PHASE_DONE);
- jlSurfaceClear(gStage, 2);
+ jlSurfaceClear(jlgStage, 2);
jlStagePresent();
jlLogF("UBER: press any key to exit");
diff --git a/include/joey/platform.h b/include/joey/platform.h
index e687ced..9553745 100644
--- a/include/joey/platform.h
+++ b/include/joey/platform.h
@@ -104,7 +104,7 @@
// 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).
+ // by a CRTC raster interrupt (src/x68000/jlX68kRasterIsr.s).
#define JL_CPU_68000
#define JL_ENDIAN_BIG
#define JL_NATIVE_PLANAR
@@ -137,6 +137,8 @@
#endif
#if defined(JOEYLIB_HOSTED_SDL3) && defined(__PSP__)
#define JL_PLATFORM_NAME "PSP"
+ #elif defined(JOEYLIB_HOSTED_SDL3) && defined(__APPLE__)
+ #define JL_PLATFORM_NAME "macOS"
#elif defined(JOEYLIB_HOSTED_SDL3)
#define JL_PLATFORM_NAME "SDL3"
#elif defined(JOEYLIB_HOSTED_WIN32)
@@ -175,32 +177,34 @@
// original joeylib's joey.h JL_HAS_* table.
#if defined(JOEYLIB_PLATFORM_IIGS)
- #define JL_HAS_SURFACE_CLEAR // iigsSurfaceClearFastInner (stage), macro
- #define JL_HAS_DRAW_PIXEL // iigsDrawPixelInner, macro
- #define JL_HAS_DRAW_LINE // iigsDrawLineInner, macro
- #define JL_HAS_DRAW_CIRCLE // iigsDrawCircleInner, macro
- #define JL_HAS_FILL_CIRCLE // iigsFillCircleInner (stage), macro -- returns bool
- #define JL_HAS_FILL_RECT // iigsFillRectMark (stage, fused mark), macro -> generic else
- #define JL_HAS_TILE_FILL // iigsTileFillInner, macro
- #define JL_HAS_TILE_COPY // iigsTileCopyInner, macro
- #define JL_HAS_TILE_COPY_MASKED // iigsTileCopyMaskedInner, macro
- #define JL_HAS_TILE_PASTE // iigsTilePasteInner, macro
- #define JL_HAS_TILE_PASTE_MONO // iigsTilePasteMonoInner (stage), macro -> generic else
- #define JL_HAS_TILE_PASTE_GLYPH // iigsTilePasteGlyphMark (stage, mark fused), macro -> generic else
- #define JL_HAS_TILE_SNAP // iigsTileSnapInner, macro
- #define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: iigsCopyRectRows
+ #define JL_HAS_SURFACE_CLEAR // jlIigsSurfaceClearFastInner (stage), macro
+ #define JL_HAS_DRAW_PIXEL // jlIigsDrawPixelInner, macro
+ #define JL_HAS_DRAW_LINE // jlIigsDrawLineInner, macro
+ #define JL_HAS_DRAW_CIRCLE // jlIigsDrawCircleInner, macro
+ #define JL_HAS_FILL_CIRCLE // jlIigsFillCircleInner (stage), macro -- returns bool
+ #define JL_HAS_FILL_RECT // jlIigsFillRectMark (stage, fused mark), macro -> generic else
+ #define JL_HAS_TILE_FILL // jlIigsTileFillInner, macro
+ #define JL_HAS_TILE_COPY // jlIigsTileCopyInner, macro
+ #define JL_HAS_TILE_COPY_MASKED // jlIigsTileCopyMaskedInner, macro
+ #define JL_HAS_TILE_PASTE // jlIigsTilePasteInner, macro
+ #define JL_HAS_TILE_PASTE_MONO // jlIigsTilePasteMonoInner (stage), macro -> generic else
+ #define JL_HAS_TILE_PASTE_GLYPH_ENTRY // jlTilePasteGlyph itself, asm (joeyDraw.s): stage, mark fused; generic else
+ #define JL_HAS_TILE_SNAP // jlIigsTileSnapInner, macro
+ #define JL_HAS_SURFACE_COPY_RECT // jlpSurfaceCopyRect: jlIigsCopyRectRows
// [dp],y row copies (short rows lose to
// memcpy's per-call setup), function
+ #define JL_HAS_TILE_LAYER_BLIT // the generic rings, an unrolled stage blit (iigsTileLayer.c)
+ #define JL_HAS_TILE_LAYER_PUT // the generic rings, an asm put (iigsTileLayer.c)
#define JL_HAS_SPRITE_DRAW // chunky -> codegen; planes mirror is noop macro
#define JL_HAS_SPRITE_SAVE // noop macro (codegen path saves)
#define JL_HAS_SPRITE_RESTORE // noop macro
- #define JL_HAS_FLOOD_WALK_AND_SCANS // iigsFloodWalkAndScansInner (all-in-one), macro
+ #define JL_HAS_FLOOD_WALK_AND_SCANS // jlIigsFloodWalkAndScansInner (all-in-one), macro
#define JL_HAS_BIG_ALLOC // Memory Manager NewHandle/DisposeHandle, function
#define JL_HAS_STAGE_ALLOC_PIXELS // pinned $01:2000 display memory, function
#define JL_HAS_STAGE_FREE_PIXELS // no-op (pinned), function
#define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function
#define JL_HAS_MILLIS_ELAPSED // heartbeat frames plus the beam's line
- // (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord,
+ // (frameCount: port.h #defines jlpFrameCount straight to jlIigsGetTickWord,
// the VBL heartbeat tick read -- no function override to register here.)
#define JL_HAS_WAIT_VBL // the VBL heartbeat count advancing, function
// audio: every real port implements the full engine
@@ -227,7 +231,9 @@
#define JL_HAS_INPUT_INIT
#define JL_HAS_INPUT_SHUTDOWN
#define JL_HAS_INPUT_POLL
+ #define JL_HAS_INPUT_PREDICATES // jlKey/jlMouse Down/Pressed/Released themselves, asm (joeyDraw.s)
#define JL_HAS_JOYSTICK_RESET
+ #define JL_HAS_JOYSTICK_AXES_READ // the paddles when asked for, not every poll (input.c)
// serial add-on (optional; SCC modem/printer + 6551 Super Serial Card)
#define JL_HAS_SERIAL_OPEN
#define JL_HAS_SERIAL_CLOSE
@@ -252,7 +258,7 @@
// millisElapsed: generic frameCount*1000/frameHz (no override)
#elif defined(JOEYLIB_PLATFORM_AMIGA)
#define JL_HAS_SURFACE_CLEAR // planar clear, function
- #define JL_HAS_DRAW_PIXEL // amigaPlanarSetPixel, function
+ #define JL_HAS_DRAW_PIXEL // planarDrawPixel, inline (amigaTile.h)
#define JL_HAS_DRAW_LINE // amigaPlanarLine, function
#define JL_HAS_DRAW_CIRCLE // amiga planar circle, function
#define JL_HAS_FILL_CIRCLE // amiga planar fill circle, function -- returns bool
@@ -263,15 +269,16 @@
#define JL_HAS_TILE_PASTE // amiga planar tile paste, inline (amigaTile.h)
#define JL_HAS_TILE_SNAP // amiga planar tile snap, inline (amigaTile.h)
#define JL_HAS_TILE_PASTE_MONO // amiga planar mono colorize+paste, inline (amigaTile.h)
- #define JL_HAS_TILE_PASTE_GLYPH // amiga 1bpp glyph -> planes, inline (amigaTile.h)
+ #define JL_HAS_TILE_PASTE_GLYPH_ENTRY // jlTilePasteGlyph itself, asm (src/m68k/planarShape.s): planes, mark fused
+ #define JL_HAS_FILL_RECT_ENTRY // jlFillRect itself, asm (src/m68k/planarFill.s): a row; draw.c's is jlFillRectClip
#define JL_HAS_SPRITE_DRAW // amiga planar sprite draw, function
- #define JL_HAS_SPRITE_SAVE // amiga planar sprite save, function
- #define JL_HAS_SPRITE_RESTORE // amiga planar sprite restore, function
+ #define JL_HAS_SPRITE_SAVE // planarSpriteSavePlanes, inline (amigaTile.h)
+ #define JL_HAS_SPRITE_RESTORE // planarSpriteRestorePlanes, inline (amigaTile.h)
#define JL_HAS_FLOOD_WALK_PLANES // amiga planar flood walk-out, function
#define JL_HAS_FLOOD_SCAN_ROW_PLANES // amiga planar flood scan-row, function
#define JL_HAS_SURFACE_COPY_PLANES // amiga blitter plane copy, function
#define JL_HAS_SURFACE_COPY_RECT // amiga per-plane rect row copy, function
- #define JL_HAS_SAMPLE_PIXEL // amiga planar nibble decode, function
+ #define JL_HAS_SAMPLE_PIXEL // planarSamplePixel, inline (amigaTile.h)
#define JL_HAS_SURFACE_HASH // amiga planar hash, function
#define JL_HAS_SURFACE_LOAD_FILE // amiga planar load, function
#define JL_HAS_SURFACE_SAVE_FILE // amiga planar save, function
@@ -279,6 +286,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_TILE_LAYER // the shared separate-plane ring + blit (src/m68k/planarTileLayer.c)
#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
@@ -334,26 +342,27 @@
// millisElapsed: generic frameCount*1000/frameHz (no override)
#elif defined(JOEYLIB_PLATFORM_ATARIST)
#define JL_HAS_SURFACE_CLEAR // planar clear, function
- #define JL_HAS_DRAW_PIXEL // stPlanarSetPixel, function
+ #define JL_HAS_DRAW_PIXEL // stDrawPixel, inline (stTile.h)
#define JL_HAS_DRAW_LINE // stPlanarLine, function
#define JL_HAS_DRAW_CIRCLE // st planar circle, function
#define JL_HAS_FILL_CIRCLE // st planar fill circle, function -- returns bool
- #define JL_HAS_FILL_RECT // st planar fill rect, function
+ #define JL_HAS_FILL_RECT // stFillRect, inline (stTile.h)
+ #define JL_HAS_FILL_RECT_ENTRY // jlFillRect itself, asm (lineSpan.s); draw.c's is jlFillRectClip
#define JL_HAS_TILE_FILL // st planar tile fill, inline (stTile.h)
#define JL_HAS_TILE_COPY // st planar tile copy, inline (stTile.h)
#define JL_HAS_TILE_COPY_MASKED // st planar masked copy, inline (stTile.h)
#define JL_HAS_TILE_PASTE // st planar tile paste, inline (stTile.h)
#define JL_HAS_TILE_SNAP // st planar tile snap, inline (stTile.h)
#define JL_HAS_TILE_PASTE_MONO // st planar mono colorize+paste, inline (stTile.h)
- #define JL_HAS_TILE_PASTE_GLYPH // st 1bpp glyph -> planes, inline (stTile.h)
+ #define JL_HAS_TILE_PASTE_GLYPH_ENTRY // jlTilePasteGlyph itself, asm (stGlyph.s): planes, mark fused
#define JL_HAS_SPRITE_DRAW // st planar sprite draw, function
- #define JL_HAS_SPRITE_SAVE // st planar sprite save, function
- #define JL_HAS_SPRITE_RESTORE // st planar sprite restore, function
+ #define JL_HAS_SPRITE_SAVE // stSpriteSavePlanes, inline (stTile.h)
+ #define JL_HAS_SPRITE_RESTORE // stSpriteRestorePlanes, inline (stTile.h)
#define JL_HAS_FLOOD_WALK_PLANES // st planar flood walk-out, function
#define JL_HAS_FLOOD_SCAN_ROW_PLANES // st planar flood scan-row, function
#define JL_HAS_SURFACE_COPY_PLANES // st planar buffer memcpy, function
#define JL_HAS_SURFACE_COPY_RECT // st interleaved rect row memcpy, function
- #define JL_HAS_SAMPLE_PIXEL // st planar nibble decode, function
+ #define JL_HAS_SAMPLE_PIXEL // stSamplePixel, inline (stTile.h)
#define JL_HAS_SURFACE_HASH // st planar hash, function
#define JL_HAS_SURFACE_LOAD_FILE // st planar load, function
#define JL_HAS_SURFACE_SAVE_FILE // st planar save, function
@@ -361,6 +370,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_TILE_LAYER // st plane-separated ring + shifting blit (stTileLayer.c)
#define JL_HAS_FRAME_HZ // the machine's 50 / 60, function
#define JL_HAS_FRAME_COUNT // VBL-ISR counter, function
#define JL_HAS_WAIT_VBL // frame-counter wait, function
@@ -448,6 +458,7 @@
#define JL_HAS_INPUT_SHUTDOWN
#define JL_HAS_INPUT_POLL
#define JL_HAS_JOYSTICK_RESET
+ #define JL_HAS_JOYSTICK_AXES_READ // the BIOS's axis read when asked for, not every poll (input.c)
// serial add-on (optional; 8250/16550 UART on a COM port, IRQ RX ring)
#define JL_HAS_SERIAL_OPEN
#define JL_HAS_SERIAL_CLOSE
@@ -483,7 +494,8 @@
#define JL_HAS_TILE_COPY_MASKED
#define JL_HAS_TILE_PASTE
#define JL_HAS_TILE_SNAP
- #define JL_HAS_TILE_PASTE_GLYPH // x68k 1bpp glyph -> planes, inline (x68kTile.h)
+ #define JL_HAS_TILE_PASTE_GLYPH_ENTRY // jlTilePasteGlyph itself, asm (src/m68k/planarShape.s): planes, mark fused
+ #define JL_HAS_FILL_RECT_ENTRY // jlFillRect itself, asm (src/m68k/planarFill.s): a row; draw.c's is jlFillRectClip
#define JL_HAS_TILE_PASTE_MONO
#define JL_HAS_SPRITE_DRAW
#define JL_HAS_SPRITE_SAVE
@@ -500,6 +512,7 @@
#define JL_HAS_SURFACE_ALLOC_PIXELS
#define JL_HAS_SURFACE_ALLOC_PORT_DATA
#define JL_HAS_SURFACE_FREE_PORT_DATA
+ #define JL_HAS_TILE_LAYER
#define JL_HAS_INIT // _iocs_crtmod display bring-up
#define JL_HAS_SHUTDOWN // restore the previous CRT mode
#define JL_HAS_PRESENT // copy the stage's dirty plane rows into the text plane
@@ -521,7 +534,7 @@
// block (fileRoot.h).
#define JL_HAS_FILE_ROOTS
// Serial add-on: RSDRV.SYS transmit and line setup, receive on the
- // port's own SCC interrupt (src/x68000/x68kSccRxIsr.s).
+ // port's own SCC interrupt (src/x68000/jlX68kSccRxIsr.s).
#define JL_HAS_SERIAL_OPEN
#define JL_HAS_SERIAL_CLOSE
#define JL_HAS_SERIAL_POLL
@@ -629,7 +642,9 @@
#define JL_HAS_SAVE_DELETE
#define JL_HAS_DISK_FREE
// DATA/, SAVES/ and joeylog.txt in the program's folder, from the
- // executable's path (hostBackendProgramFolder, fileRoot.h).
+ // executable's path (jlHostBackendProgramFolder, fileRoot.h); in a browser
+ // SAVES/ and the log in the page's folder, and on macOS in the program's
+ // Application Support folder (jlHostBackendWriteFolder).
#define JL_HAS_FILE_ROOTS
// 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
@@ -687,7 +702,7 @@
#define JL_HAS_PRESENT // blit the chunky stage to your framebuffer
#define JL_HAS_INPUT_INIT
#define JL_HAS_INPUT_SHUTDOWN
- #define JL_HAS_INPUT_POLL // read your keyboard into gKeyState[]
+ #define JL_HAS_INPUT_POLL // read your keyboard into jlgKeyState[]
#define JL_HAS_JOYSTICK_RESET
#define JL_HAS_WAIT_VBL // pace to the display
#define JL_HAS_FRAME_COUNT // a monotonic frame/refresh counter
diff --git a/include/joey/sprite.h b/include/joey/sprite.h
index 36e79bf..d50a01d 100644
--- a/include/joey/sprite.h
+++ b/include/joey/sprite.h
@@ -167,9 +167,10 @@ void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
// 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.
+// port, a save on the Amiga, ST, and X68000 takes the 16-px-aligned region
+// containing the sprite (or its visible part), 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
diff --git a/include/joey/tile.h b/include/joey/tile.h
index f721adc..cff8a94 100644
--- a/include/joey/tile.h
+++ b/include/joey/tile.h
@@ -172,4 +172,47 @@ void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontS
// 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);
+
+// ----- Tile layers (scrolling) -----
+//
+// A tile layer is a map of tile indices, up to JL_TILE_LAYER_MAX_SIDE
+// tiles a side, seen through a band of a surface: whole tile rows
+// [viewBy, viewBy + viewHeight), the full width. jlTileLayerDraw fills
+// the band with the map as seen from a pixel origin, to the pixel both
+// ways, so moving the origin a little each frame scrolls it. The map
+// wraps at its edges (an origin past them, or a map smaller than the
+// view, repeats it). An index of JL_TILE_MAP_SKIP draws colour 0.
+//
+// The layer keeps the tiles around the view drawn, so a draw only draws
+// the edge tiles a move uncovered (a move of a whole view or more draws
+// them all) before copying the band. A draw covers the band and never
+// reads what was there: draw the layer, then the sprites (no save-under
+// needed), then present.
+//
+// The layer keeps the tileset and the map you give it rather than
+// copying them, so both must outlive it. Change a cell with
+// jlTileLayerSetTile, not by writing the map: the layer would not see
+// the write until the cell scrolled out of the view and back.
+typedef struct jlTileLayerT jlTileLayerT;
+
+#define JL_TILE_LAYER_MAX_SIDE 4096u
+
+// Make a layer over `map` (mapWidth x mapHeight tile indices into `tiles`,
+// row-major: map[row * mapWidth + col]) shown in tile rows [viewBy,
+// viewBy + viewHeight). Returns NULL if an argument is NULL or out of
+// range, or memory runs out. Nothing is drawn until jlTileLayerDraw.
+jlTileLayerT *jlTileLayerCreate(const jlTileT *tiles, uint8_t *map, uint16_t mapWidth, uint16_t mapHeight, uint8_t viewBy, uint8_t viewHeight);
+
+// Free a layer (not its tileset or map). NULL is a no-op.
+void jlTileLayerDestroy(jlTileLayerT *layer);
+
+// Fill the layer's band of dst with the map seen from pixel (x, y): the
+// band's top-left pixel shows map pixel (x, y), taken modulo the map's
+// size in pixels.
+void jlTileLayerDraw(jlSurfaceT *dst, jlTileLayerT *layer, uint16_t x, uint16_t y);
+
+// Set map cell (tx, ty) to `tile`; the next draw shows it. Out-of-range
+// cells are ignored.
+void jlTileLayerSetTile(jlTileLayerT *layer, uint16_t tx, uint16_t ty, uint8_t tile);
+
#endif
diff --git a/install.sh b/install.sh
index 9203e50..5b2508f 100755
--- a/install.sh
+++ b/install.sh
@@ -23,7 +23,7 @@
#
# Targets are component names (see `list`) or groups:
# toolchain:PORT emulator:PORT PORT toolchains emulators all
-# where PORT is iigs, amiga, atarist, dos, x68000, dvx, sdl3, web, win32, win16, psp, haiku, haiku32 or beos. `all`, `toolchains`
+# where PORT is iigs, amiga, atarist, dos, x68000, dvx, sdl3, web, macos, win32, win16, psp, haiku, haiku32 or beos. `all`, `toolchains`
# and `emulators` leave out optional components (dvx, which unpacks a DVX
# tree's dist/ zips you have); name those explicitly.
#
@@ -62,7 +62,7 @@ source "$JOEY_INSTALLER/lib/config.sh"
# shellcheck source=installer/lib/bundle.sh
source "$JOEY_INSTALLER/lib/bundle.sh"
-JOEY_PORTS="iigs amiga atarist dos x68000 dvx sdl3 web win32 win16 psp haiku haiku32 beos"
+JOEY_PORTS="iigs amiga atarist dos x68000 dvx sdl3 web macos win32 win16 psp haiku haiku32 beos"
depsInstall=0
@@ -94,7 +94,7 @@ groupMembers() {
printf '%s\n' "$id"
fi
;;
- iigs|amiga|atarist|dos|x68000|dvx|sdl3|web|win32|win16|psp)
+ iigs|amiga|atarist|dos|x68000|dvx|sdl3|web|macos|win32|win16|psp|haiku|haiku32|beos)
if [[ "$groups" == *" toolchain:$group "* || "$groups" == *" emulator:$group "* ]]; then
printf '%s\n' "$id"
fi
diff --git a/installer/components/amigaGcc.sh b/installer/components/amigaGcc.sh
index 1ea6b3d..c7364e7 100644
--- a/installer/components/amigaGcc.sh
+++ b/installer/components/amigaGcc.sh
@@ -15,10 +15,12 @@
compDescription="Amiga compiler: m68k-amigaos-gcc 6.5 + NDK 3.2 + vasm (source build)"
compGroups="toolchain:amiga"
compRequires="host"
-compHostDeps="build-tools git which bison flex m4 texinfo autoconf automake gettext rsync wget lhasa gmp-dev mpfr-dev mpc-dev ncurses-dev readline-dev"
+compHostDeps="build-tools git patch which bison flex m4 texinfo autoconf automake gettext rsync wget lhasa gmp-dev mpfr-dev mpc-dev ncurses-dev readline-dev"
compUrl="https://github.com/AmigaPorts/m68k-amigaos-gcc.git"
compCommit="7d3e35f58e61bdf325d84b912b139f973d9b7452"
-compPin="$compCommit"
+patchDir="$JOEY_PATCHES/amigaGcc"
+# Editing a patch rebuilds the toolchain, so the patches are part of the pin.
+compPin="$compCommit $(cat "$patchDir"/*.patch | sha256sum | cut -d' ' -f1)"
# name commit (the URL comes from the checkout's default-repos file)
subProjects=(
@@ -51,6 +53,7 @@ compInstall() {
local sha
local url
local dir
+ local p
src="$(freshBuildDir "$compId")"
gitFetchCommit "$compUrl" "$compCommit" "$src"
for entry in "${subProjects[@]}"; do
@@ -59,6 +62,10 @@ compInstall() {
[ -n "$url" ] || die "amiga-gcc: no URL for sub-project $name in default-repos"
gitFetchCommit "$url" "$sha" "$src/projects/$name"
done
+ for p in "$patchDir"/*.patch; do
+ info "applying $(basename "$p")"
+ patch -d "$src/projects/gcc" -p1 -s -F0 --no-backup-if-mismatch < "$p"
+ done
rm -rf "$compDir"
make -C "$src" all PREFIX="$compDir" -j"$JOEY_JOBS"
# Anything cloned by the build itself is unpinned; say so.
diff --git a/installer/components/macosSdk.sh b/installer/components/macosSdk.sh
new file mode 100644
index 0000000..3b22f4e
--- /dev/null
+++ b/installer/components/macosSdk.sh
@@ -0,0 +1,57 @@
+# 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
+#
+# The Apple SDK for macOS 15.5, and the Mach-O tools of the host's LLVM: what
+# the macOS port (make macos) links its programs against and joins them
+# with. The SDK holds the frameworks (Cocoa, CoreAudio, IOKit and the rest)
+# and their stub libraries; zig (component zig) brings the C library. Apple
+# ships the SDK only inside Xcode and offers no download of it, so it comes
+# from the mirror osxcross users share (github.com/joseluisq/macosx-sdks),
+# pinned to the release's SHA-256; a copy of the same tarball put in
+# toolchains/cache/downloads first (MacOSX15.5.sdk.tar.xz) is used instead.
+# Its licence is Apple's (LICENSES.md). llvm-lipo joins each architecture's
+# build into one universal file and llvm-libtool-darwin archives universal
+# objects (make/support/macos/cc.sh, make/macos.mk).
+
+compDescription="macos: Apple MacOSX 15.5 SDK + LLVM's lipo and libtool"
+compGroups="toolchain:macos"
+compRequires="host"
+compHostDeps="tar xz llvm"
+compUrl="https://github.com/joseluisq/macosx-sdks/releases/download/15.5/MacOSX15.5.sdk.tar.xz"
+compSha256="c15cf0f3f17d714d1aa5a642da8e118db53d79429eb015771ba816aa7c6c1cbd"
+compPin="$compSha256"
+
+
+# llvmTool NAME -- the path of LLVM's NAME: on the PATH, else the newest
+# versioned LLVM's (Debian and Ubuntu keep llvm-lipo in /usr/lib/llvm-18/bin
+# and the like, with only some tools linked into /usr/bin).
+llvmTool() {
+ command -v "$1" 2>/dev/null || find /usr/lib -maxdepth 3 -path "/usr/lib/llvm-*/bin/$1" 2>/dev/null | sort -V | tail -n 1
+}
+
+
+compInstall() {
+ local archive
+ archive="$(fetchFile "$compUrl" "$compSha256")"
+ rm -rf "$compDir"
+ extractArchive "$archive" "$compDir" 1
+}
+
+
+compInstalled() {
+ [ -d "$compDir/System/Library/Frameworks/Cocoa.framework" ]
+}
+
+
+compVerify() {
+ compInstalled && [ -x "$(llvmTool llvm-lipo)" ] && [ -x "$(llvmTool llvm-libtool-darwin)" ]
+}
+
+
+compEnv() {
+ envVar MACOS_SDK "$compDir"
+ envVar MACOS_LIPO "$(llvmTool llvm-lipo)"
+ envVar MACOS_LIBTOOL "$(llvmTool llvm-libtool-darwin)"
+}
diff --git a/installer/components/sdl3Macos.sh b/installer/components/sdl3Macos.sh
new file mode 100644
index 0000000..fa10607
--- /dev/null
+++ b/installer/components/sdl3Macos.sh
@@ -0,0 +1,116 @@
+# 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
+#
+# SDL 3 for macOS, static and universal, for the macOS port (make macos):
+# the release source the sdl3 component builds for the host (its pin is read
+# from sdl3.sh, so there is one), built once for Apple silicon and once for
+# Intel with the port's own compiler (make/support/macos/cc.sh, zig and the
+# Apple SDK), and the two libraries joined with llvm-lipo into
+# toolchains/opt/sdl3Macos.
+
+compDescription="macos: SDL 3 for macOS, static, universal (macos port)"
+compGroups="toolchain:macos"
+compRequires="host zig macosSdk"
+compHostDeps="cmake pkg-config llvm"
+sdl3Url="$(compQuery sdl3 compUrl)"
+sdl3Sha256="$(compQuery sdl3 compSha256)"
+macosCc="$JOEY_REPO/make/support/macos/cc.sh"
+# How the compiler is driven is part of what is built, so the wrapper is part
+# of the pin.
+compPin="$sdl3Sha256:$(compQuery zig compPin):$(compQuery macosSdk compPin):$(sha256sum "$macosCc" | cut -d' ' -f1)"
+
+
+# The tools the compiler wrapper reads from the environment, as make's
+# recipes have them from toolchains/config.mk: zig's and the SDK's.
+macosToolsExport() {
+ local kind
+ local name
+ local value
+ while IFS=$'\t' read -r kind name value; do
+ if [ "$kind" = var ]; then
+ export "$name=$value"
+ fi
+ done < <(compRun zig compEnv; compRun macosSdk compEnv)
+}
+
+
+compInstall() {
+ local archive
+ local src
+ local arch
+ local processor
+ local libs=()
+ archive="$(fetchFile "$sdl3Url" "$sdl3Sha256")"
+ src="$(freshBuildDir "$compId")"
+ extractArchive "$archive" "$src" 1
+ macosToolsExport
+ for arch in aarch64 x86_64; do
+ export MACOS_ARCHS="$arch"
+ # Apple calls aarch64 arm64.
+ processor="${arch/aarch64/arm64}"
+ # Nothing here links dynamically, but CMake's Darwin support wants an
+ # install_name_tool all the same (LLVM's, beside its lipo); and no
+ # Xcode sysroot or -arch flags, since the wrapper names the SDK and
+ # the target itself.
+ cmake -S "$src" -B "$src/build-$arch" \
+ -DCMAKE_SYSTEM_NAME=Darwin \
+ -DCMAKE_SYSTEM_PROCESSOR="$processor" \
+ -DCMAKE_C_COMPILER="$macosCc" \
+ -DCMAKE_OBJC_COMPILER="$macosCc" \
+ -DCMAKE_AR="$(dirname "$MACOS_LIPO")/llvm-ar" \
+ -DCMAKE_RANLIB="$(dirname "$MACOS_LIPO")/llvm-ranlib" \
+ -DCMAKE_INSTALL_NAME_TOOL="$(dirname "$MACOS_LIPO")/llvm-install-name-tool" \
+ -DCMAKE_OSX_SYSROOT= \
+ -DCMAKE_OSX_ARCHITECTURES= \
+ -DCMAKE_FIND_ROOT_PATH="$MACOS_SDK" \
+ -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER \
+ -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \
+ -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \
+ -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_INSTALL_PREFIX="$compDir" \
+ -DCMAKE_INSTALL_LIBDIR=lib \
+ -DSDL_SHARED=OFF \
+ -DSDL_STATIC=ON \
+ -DSDL_TESTS=OFF \
+ -DSDL_EXAMPLES=OFF
+ cmake --build "$src/build-$arch" -j"$JOEY_JOBS"
+ libs+=("$src/build-$arch/libSDL3.a")
+ done
+ rm -rf "$compDir"
+ # The headers, pkg-config file and CMake package are the same for both;
+ # the library is replaced by the universal one.
+ cmake --install "$src/build-aarch64"
+ "$MACOS_LIPO" -create "${libs[@]}" -output "$compDir/lib/libSDL3.a"
+ dropBuildDir "$compId"
+}
+
+
+compInstalled() {
+ [ -f "$compDir/lib/libSDL3.a" ] && [ -f "$compDir/lib/pkgconfig/sdl3.pc" ]
+}
+
+
+# Link a program against the library the way make/macos.mk does: it must
+# come out universal.
+compVerify() {
+ local tmp
+ local status=1
+ macosToolsExport
+ tmp="$(mktemp -d)"
+ printf '#include \nint main(void) { return SDL_GetVersion() == 0; }\n' > "$tmp/t.c"
+ # shellcheck disable=SC2046
+ if "$macosCc" "$tmp/t.c" -o "$tmp/t" $(PKG_CONFIG_LIBDIR="$compDir/lib/pkgconfig" pkg-config --cflags --static --libs sdl3) \
+ && "$MACOS_LIPO" "$tmp/t" -verify_arch arm64 x86_64; then
+ status=0
+ fi
+ rm -rf "$tmp"
+ return $status
+}
+
+
+compEnv() {
+ envVar SDL3_MACOS_DIR "$compDir"
+}
diff --git a/installer/components/zig.sh b/installer/components/zig.sh
new file mode 100644
index 0000000..ecbe9e6
--- /dev/null
+++ b/installer/components/zig.sh
@@ -0,0 +1,44 @@
+# 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
+#
+# zig 0.15.2: the C compiler the macOS port (make macos) builds with. zig is
+# clang with its own Mach-O linker, the macOS C library's headers and
+# libSystem's stubs, so it builds Mac programs on Linux and signs the Apple
+# silicon ones (ad hoc) as it links them; the frameworks come from the Apple
+# SDK (component macosSdk). Prebuilt for Linux x86-64. Its cache stays inside
+# toolchains/opt/zig, not in the user's home directory.
+
+compDescription="macos: zig 0.15.2 (clang with a Mach-O linker)"
+compGroups="toolchain:macos"
+compRequires="host"
+compHostDeps="tar xz"
+compUrl="https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz"
+compSha256="02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239"
+compPin="$compSha256"
+
+
+compInstall() {
+ local archive
+ archive="$(fetchFile "$compUrl" "$compSha256")"
+ rm -rf "$compDir"
+ extractArchive "$archive" "$compDir" 1
+}
+
+
+compInstalled() {
+ [ -x "$compDir/zig" ]
+}
+
+
+compVerify() {
+ ZIG_GLOBAL_CACHE_DIR="$compDir/cache" ZIG_LOCAL_CACHE_DIR="$compDir/cache" verifyCompiles "$compDir/zig" cc -target aarch64-macos
+}
+
+
+compEnv() {
+ envVar ZIG "$compDir/zig"
+ envVar ZIG_GLOBAL_CACHE_DIR "$compDir/cache"
+ envVar ZIG_LOCAL_CACHE_DIR "$compDir/cache"
+}
diff --git a/installer/distroPackages.txt b/installer/distroPackages.txt
index fac6568..1cc69b0 100644
--- a/installer/distroPackages.txt
+++ b/installer/distroPackages.txt
@@ -91,6 +91,10 @@ gbm-dev libgbm-dev mesa-libgbm-devel
egl-dev libegl-dev mesa-libEGL-devel
gles-dev libgles-dev mesa-libGLES-devel
+# ---- the macOS port: LLVM's Mach-O tools (llvm-lipo, llvm-libtool-darwin,
+# llvm-install-name-tool), which zig does not carry
+llvm llvm llvm
+
# ---- emulators packaged by the distribution
hatari hatari hatari
fs-uae fs-uae -
diff --git a/installer/patches/amigaGcc/0001-bbb-moveq-mask.patch b/installer/patches/amigaGcc/0001-bbb-moveq-mask.patch
new file mode 100644
index 0000000..4e7ef7f
--- /dev/null
+++ b/installer/patches/amigaGcc/0001-bbb-moveq-mask.patch
@@ -0,0 +1,21 @@
+--- a/gcc/bbb-opts.c
++++ b/gcc/bbb-opts.c
+@@ -325,7 +325,17 @@
+ if (GET_CODE(src) == CONST_INT && (mode == HImode || mode == QImode))
+ {
+ unsigned iv = UINTVAL(src);
+- setMask(regno, iv, mode);
++ /* output_move_himode and output_move_qimode load a data register's
++ word or byte constant in -128..127 with moveq, which writes all 32
++ bits sign-extended, where a strict_low_part move keeps the upper
++ ones. A negative one (but a byte's -1, which is st) may so leave
++ the upper bits set: keeping their old mask let opt_elim_dead_assign2
++ delete a later zero-extension's clear as superfluous. */
++ if (regno < 8 && INTVAL(src) >= -128 && INTVAL(src) < 0
++ && !(mode == QImode && INTVAL(src) == -1))
++ setMask(regno, iv, SImode);
++ else
++ setMask(regno, iv, mode);
+ value[regno] = gen_rtx_CONST_INT(mode, 0x100000000000000LL | ((long long int ) (regno) << 32) | index);
+ used[regno] = 1 << FIRST_PSEUDO_REGISTER;
+ clearRefsByMask(1<": a web program's page
+ cafebabe*|cffaedfe*) printf 'macos\n' ;; # a Mach-O program, universal or 64-bit
00504250*) printf 'psp\n' ;; # "\0PBP": a PSP program's EBOOT.PBP
*)
# Apple IIgs OMF: segment header byte 14 (NUMLEN) is 4 and
@@ -1108,7 +1111,7 @@ runBe() {
while [ $# -gt 0 ]; do
case "$1" in
- --port) port="${2:?--port needs iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, win32, win16, psp, haiku, haiku32 or beos}"; shift ;;
+ --port) port="${2:?--port needs iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, macos, win32, win16, psp, haiku, haiku32 or beos}"; shift ;;
--project)
[ -d "${2:-}" ] || fail "--project needs a project folder (one built on make/project.mk)"
projectRoot="$(cd "$2" && pwd)"
@@ -1178,7 +1181,9 @@ if [ "$serialMode" = "listen" ]; then
fi
# Resolve PROGRAM to a file and its machine.
-if [ -d "$program" ]; then
+if [ -d "$program/Contents/MacOS" ]; then
+ program="$(folderProgram "$program/Contents/MacOS")"
+elif [ -d "$program" ]; then
program="$(folderProgram "$program")"
elif [ ! -e "$program" ]; then
[ -n "$port" ] || fail "$program is not a file; for a program name add --port"
@@ -1191,6 +1196,7 @@ detected="$(magicPort "$program")"
if [ -n "$port" ] && [ "$port" != "$detected" ]; then
fail "$program is a $detected program, not $port"
fi
+[ "$detected" != macos ] || fail "$program is a Mac app's: copy its .app to a Mac and open it there (nothing here runs macOS programs)"
if [ ${#progArgs[@]} -gt 0 ] && [ "$detected" != "dos" ] && [ "$detected" != "sdl3" ] && [ "$detected" != "win32" ] && [ "$detected" != "win16" ]; then
fail "program arguments only work on DOS, SDL3, Win32 and Win16"
fi
diff --git a/make/amiga.mk b/make/amiga.mk
index ad62d51..43878f4 100644
--- a/make/amiga.mk
+++ b/make/amiga.mk
@@ -20,7 +20,9 @@ $(call requireTool,$(AMIGA_PTPLAYER)/ptplayer.asm,toolchain:amiga)
# independently. -I on $(SRC_DIR)/amiga lets ptplayer.h resolve
# from the machine-local shim alongside our code.
PTPLAYER_DIR := $(AMIGA_PTPLAYER)
-CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT -I$(SRC_DIR)/amiga -I$(SRC_68K) -I$(PTPLAYER_DIR) -MMD -MP
+# -fno-common: gcc 6 merges a program's uninitialised global into a library
+# one of the same name; gcc 10 on (every other port) refuses the link.
+CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -fno-common -noixemul -D__OSCOMPAT -I$(SRC_DIR)/amiga -I$(SRC_68K) -I$(PTPLAYER_DIR) -MMD -MP
# OSCOMPAT=1 selects PTPlayer's audio.device-friendly variant (uses
# CIA-B + audio.device interrupts via the OS rather than taking over
# Paula directly), matching the way our HAL cooperates with Intuition.
@@ -33,11 +35,7 @@ CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT
# "file format not recognized" failure mode is silent at archive time
# and surfaces as undefined references at every binary's link step).
PTPLAYER_ASFLAGS := -Fhunk -m68000 -quiet -DOSCOMPAT=1
-# --allow-multiple-definition lets our user-space tzset stub
-# (src/amiga/libinit.c) win over libnix's version in
-# __gmtoffset.o. libnix's tzset dereferences a possibly-NULL
-# LocaleBase; our no-op skips the deref.
-LDFLAGS := -noixemul -Wl,--allow-multiple-definition
+LDFLAGS := -noixemul
PORT_C_SRCS := $(wildcard $(SRC_DIR)/amiga/*.c)
PORT_S_SRCS := $(wildcard $(SRC_DIR)/amiga/*.s)
@@ -56,7 +54,10 @@ LIB_OBJS := \
$(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \
$(BUILD)/obj/port/ptplayer.o \
$(BUILD)/obj/68k/spriteEmitPlanar68k.o \
+ $(BUILD)/obj/68k/planarHash.o \
$(BUILD)/obj/68k/planarSprite.o \
+ $(BUILD)/obj/68k/spriteC2p.o \
+ $(BUILD)/obj/68k/planarTileLayer.o \
$(BUILD)/obj/codegen/spriteCompile.o \
$(BUILD)/obj/codegen/spriteStage.o
@@ -75,10 +76,19 @@ $(BUILD)/obj/port/%.o: $(SRC_DIR)/amiga/%.c
$(AMIGA_CC) $(CFLAGS) -c $< -o $@
# Hand-written 68k assembly: GAS syntax, fed through the gcc driver
-# so the bundled m68k-amigaos-as (binutils) does the work.
+# so the bundled m68k-amigaos-as (binutils) does the work. It exports every
+# .equ and .set as a global absolute symbol (in a hunk object), where every
+# other port's assembler keeps them local; they would be in every program's
+# link, sfxMix.s's loop counter "sum" among them, so each object's are made
+# local again.
+AMIGA_NM := $(dir $(AMIGA_CC))m68k-amigaos-nm
+AMIGA_OBJCOPY := $(dir $(AMIGA_CC))m68k-amigaos-objcopy
+amigaAbsLocal = $(AMIGA_OBJCOPY) $$($(AMIGA_NM) $(1) | awk '$$2 == "A" { print "-L", $$3 }') $(1)
+
$(BUILD)/obj/port/%.o: $(SRC_DIR)/amiga/%.s
@mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) -c $< -o $@
+ $(call amigaAbsLocal,$@)
# PTPlayer is third-party Devpac/vasm-syntax assembly with no
# maintained GAS port; vasm is invoked only here.
@@ -90,9 +100,13 @@ $(BUILD)/obj/codegen/%.o: $(CODEGEN_DIR)/%.c
@mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@
+# gcc-amigaos spells a C symbol with a leading underscore; the shared asm
+# that names one says it through C_OP (src/m68k/m68kCSym.inc, which the
+# assembler finds through -I: the gcc driver hands it no include path).
$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s
@mkdir -p $(dir $@)
- $(AMIGA_CC) $(CFLAGS) -c $< -o $@
+ $(AMIGA_CC) $(CFLAGS) -Wa,--defsym,JL_ASM_USCORE=1 -Wa,-I$(SRC_68K) -c $< -o $@
+ $(call amigaAbsLocal,$@)
# 68k sprite emitters (spriteEmitPlanar68k.c) live in src/m68k/;
# -I$(CODEGEN_DIR) lets them resolve spriteEmitter.h.
@@ -158,3 +172,7 @@ clean-amiga:
clean: clean-amiga
endif
+
+# UBER with its probe rows, for the display gate (scripts/verify-amiga-display.sh).
+.PHONY: amiga-uberprob
+amiga-uberprob: $(call programTarget,UBERPROB)
diff --git a/make/atarist.mk b/make/atarist.mk
index b31baca..3feba9f 100644
--- a/make/atarist.mk
+++ b/make/atarist.mk
@@ -24,6 +24,7 @@ ST_SHIM := $(REPO_DIR)/make/support/atarist/include-shim
ifeq ($(PLATFORM),ste)
STE_SRC_DIR := $(SRC_DIR)/atarist/ste
STE_CFLAGS := -DJOEYLIB_ATARI_STE -I$(STE_SRC_DIR)
+STE_ASFLAGS := -Wa,--defsym,JOEYLIB_ATARI_STE=1
STE_C_SRCS := $(wildcard $(STE_SRC_DIR)/*.c)
endif
@@ -33,7 +34,10 @@ LDFLAGS :=
PORT_C_SRCS := $(wildcard $(SRC_DIR)/atarist/*.c)
PORT_S_SRCS := $(wildcard $(SRC_DIR)/atarist/*.s)
-SHARED_S := $(wildcard $(SRC_68K)/*.s)
+# planarFill.s and planarShape.s are the separate-plane ports' (the Amiga,
+# the X68000): the ST's planes interleave, and its jlFillRect and
+# jlTilePasteGlyph are its own (lineSpan.s, stGlyph.s).
+SHARED_S := $(filter-out $(SRC_68K)/planarFill.s $(SRC_68K)/planarShape.s,$(wildcard $(SRC_68K)/*.s))
# Sprite codegen: the ST-interleaved emitter (spriteEmitInterleaved68k.c)
# and the planar sprite interpreter (planarSprite.c) live in src/m68k/ and
@@ -49,7 +53,9 @@ LIB_OBJS := \
$(patsubst $(STE_SRC_DIR)/%.c,$(BUILD)/obj/ste/%.o,$(STE_C_SRCS)) \
$(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \
$(BUILD)/obj/68k/spriteEmitInterleaved68k.o \
+ $(BUILD)/obj/68k/planarHash.o \
$(BUILD)/obj/68k/planarSprite.o \
+ $(BUILD)/obj/68k/spriteC2p.o \
$(BUILD)/obj/codegen/spriteCompile.o \
$(BUILD)/obj/codegen/spriteStage.o \
$(BUILD)/obj/shim/math_shim.o
@@ -76,10 +82,11 @@ endif
# Hand-written 68k assembly: GAS syntax, fed through the gcc driver
# so the bundled m68k-atari-mint-as (binutils) does the work. No vasm
-# dependency on the ST path.
+# dependency on the ST path. The STe build defines JOEYLIB_ATARI_STE for
+# the port's .ifdef as well (the .s files see no preprocessor).
$(BUILD)/obj/port/%.o: $(SRC_DIR)/atarist/%.s
@mkdir -p $(dir $@)
- $(ST_CC) $(CFLAGS) -c $< -o $@
+ $(ST_CC) $(CFLAGS) $(STE_ASFLAGS) -c $< -o $@
$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s
@mkdir -p $(dir $@)
@@ -147,6 +154,10 @@ clean-$(PLATFORM):
# looks like success while leaving stale objects the next build links.
clean: clean-$(PLATFORM)
+# UBER with its probe rows, for the display gate (scripts/verify-atarist-display.sh).
+.PHONY: $(PLATFORM)-uberprob
+$(PLATFORM)-uberprob: $(call programTarget,UBERPROB)
+
# The dirty-mark check is the ST build's (DIRTYMRK_PORTS).
ifeq ($(PLATFORM),atarist)
.PHONY: atarist-dirtymark atarist-verify-dirtymark
diff --git a/make/be.mk b/make/be.mk
index c903681..cbb7f4c 100644
--- a/make/be.mk
+++ b/make/be.mk
@@ -28,13 +28,16 @@ PROGRAM_EXT :=
# Its icons, the BeOS kind Haiku shows too: 32x32 and 16x16 in the BeOS
# palette, raw (tools/iconBake.py be), put into the linked program's
# resources by xres (component haikuTools) as BEOS:L:STD_ICON and
-# BEOS:M:STD_ICON, BE_ICON_ID each.
+# BEOS:M:STD_ICON, BE_ICON_ID each; and the 32x32 one as Haiku's vector
+# icon, BEOS:ICON, which Haiku draws at any size (BeOS R5 ignores it). An
+# icon too busy for one bakes an empty file and goes without.
BE_XRES := $(HAIKU_TOOLS)/xres
$(call requireTool,$(BE_XRES),toolchain:$(PLATFORM))
BE_ICON_ID := 101
HOSTED_ICON = $(call programIconDir,$(1))/icon32.raw
-HOSTED_ICON_BAKE = $(ICON_BAKE) be $(call programIcon,$(1)) $(call HOSTED_ICON,$(1)) $(call programIconDir,$(1))/icon16.raw
-HOSTED_PACK = $(BE_XRES) -o $(call programBin,$(1)) -a ICON:$(BE_ICON_ID):BEOS:L:STD_ICON $(call HOSTED_ICON,$(1)) -a MICN:$(BE_ICON_ID):BEOS:M:STD_ICON $(call programIconDir,$(1))/icon16.raw
+BE_ICON_VECTOR = $(call programIconDir,$(1))/icon.hvif
+HOSTED_ICON_BAKE = $(ICON_BAKE) be $(call programIcon,$(1)) $(call HOSTED_ICON,$(1)) $(call programIconDir,$(1))/icon16.raw $(call BE_ICON_VECTOR,$(1))
+HOSTED_PACK = $(BE_XRES) -o $(call programBin,$(1)) -a ICON:$(BE_ICON_ID):BEOS:L:STD_ICON $(call HOSTED_ICON,$(1)) -a MICN:$(BE_ICON_ID):BEOS:M:STD_ICON $(call programIconDir,$(1))/icon16.raw $$$$([ -s $(call BE_ICON_VECTOR,$(1)) ] && echo "-a VICN:$(BE_ICON_ID):BEOS:ICON $(call BE_ICON_VECTOR,$(1))")
include $(REPO_DIR)/make/hosted.mk
diff --git a/make/beos.mk b/make/beos.mk
index f401b2b..569c960 100644
--- a/make/beos.mk
+++ b/make/beos.mk
@@ -15,7 +15,9 @@ $(call requireTool,$(HAIKU32_ROOT)/cross/bin/i586-pc-haiku-g++,toolchain:beos)
$(call requireTool,$(BEOS_R5_SYSROOT)/system-lib/libbe.so,toolchain:beos)
BEOS_GCC2 := BE_R5_SYSROOT=$(BEOS_R5_SYSROOT) $(HAIKU32_GCC2) $(HAIKU32_ROOT)
-HOSTED_CC := $(BEOS_GCC2) gcc
+# -fno-common: gcc 2.95 merges a program's uninitialised global into a
+# library one of the same name; every other port's compiler refuses the link.
+HOSTED_CC := $(BEOS_GCC2) gcc -fno-common
HOSTED_CXX := $(BEOS_GCC2) g++
HOSTED_AR := $(BEOS_GCC2) ar
BE_CXX_RUNTIME := -lstdc++.r4
diff --git a/make/common.mk b/make/common.mk
index fc9ce57..db1d976 100644
--- a/make/common.mk
+++ b/make/common.mk
@@ -20,6 +20,12 @@ REPO_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..)
# `./install.sh doctor` names exactly what is wrong.
requireTool = $(if $(filter-out clean%,$(or $(MAKECMDGOALS),all)),$(if $(wildcard $(1)),,$(error $(2) is not installed; run: ./install.sh install $(2))))
+# $(call requireSet,VARIABLE,WHAT): stop with a clear message when a part of
+# a program's identity is not set -- who makes it (DVX's vendor and author),
+# a Mac app's identifier -- which is the project's own and never JoeyLib's.
+# WHAT says what to set it to. Goals that only clean need none.
+requireSet = $(if $(filter-out clean%,$(or $(MAKECMDGOALS),all)),$(if $(strip $($(1))),,$(error $(1) is not set: $(2))))
+
# Letter case, for the port tags (IIGS) and the program names each port
# derives from a program's id (Hello on the Amiga, hello on DVX).
lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(1)))))))))))))))))))))))))))
@@ -29,12 +35,14 @@ uc = $(subst a,A,$(subst b,B,$(subst c,C,$(subst d,D,$(subst e,E,$(subst f
# $(call cQuote,TEXT) TEXT as a C string literal.
shellQuote = '$(subst ','\'',$(1))'
cQuote = "$(subst ",\",$(subst \,\\,$(1)))"
+# A comma, for text inside a $(call) argument.
+comma = ,
capFirst = $(patsubst a%,A%,$(patsubst b%,B%,$(patsubst c%,C%,$(patsubst d%,D%,$(patsubst e%,E%,$(patsubst f%,F%,$(patsubst g%,G%,$(patsubst h%,H%,$(patsubst i%,I%,$(patsubst j%,J%,$(patsubst k%,K%,$(patsubst l%,L%,$(patsubst m%,M%,$(patsubst n%,N%,$(patsubst o%,O%,$(patsubst p%,P%,$(patsubst q%,Q%,$(patsubst r%,R%,$(patsubst s%,S%,$(patsubst t%,T%,$(patsubst u%,U%,$(patsubst v%,V%,$(patsubst w%,W%,$(patsubst x%,X%,$(patsubst y%,Y%,$(patsubst z%,Z%,$(1)))))))))))))))))))))))))))
# The ports, and which of them can build here: a port is available when its
# real compiler binary is installed (DVX also needs the DVX SDK). The top
# Makefile's `make` and a project's `make` build exactly these.
-JOEY_PORTS := iigs amiga atarist ste dos x68000 dvx sdl3 web win32 win16 psp haiku haiku32 beos
+JOEY_PORTS := iigs amiga atarist ste dos x68000 dvx sdl3 web macos win32 win16 psp haiku haiku32 beos
HAVE_IIGS := $(if $(wildcard $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang),1)
HAVE_AMIGA := $(if $(wildcard $(AMIGA_CC)),1)
HAVE_ATARIST := $(if $(wildcard $(ST_CC)),1)
@@ -44,6 +52,7 @@ HAVE_X68000 := $(if $(wildcard $(X68K_ROOT)/bin/m68k-xelf-gcc),1)
HAVE_DVX := $(if $(and $(wildcard $(DOS_CC)),$(wildcard $(DVX_SDK)/sdk.mk)),1)
HAVE_SDL3 := $(if $(wildcard $(SDL3_DIR)/lib/libSDL3.a),1)
HAVE_WEB := $(if $(and $(wildcard $(EMCC)),$(wildcard $(SDL3_WEB_DIR)/lib/libSDL3.a)),1)
+HAVE_MACOS := $(if $(and $(wildcard $(ZIG)),$(wildcard $(SDL3_MACOS_DIR)/lib/libSDL3.a)),1)
HAVE_WIN32 := $(if $(wildcard $(WIN32_CC)),1)
HAVE_WIN16 := $(if $(wildcard $(WIN16_WATCOM)/binl64/owcc),1)
HAVE_PSP := $(if $(and $(wildcard $(PSP_CC)),$(wildcard $(SDL3_PSP_DIR)/lib/libSDL3.a)),1)
@@ -68,8 +77,8 @@ INCLUDE_DIR := $(REPO_DIR)/include
# Machine-specific code lives in src// (iigs, amiga, atarist, dos,
# x68000, dvx); code shared across machines of one CPU lives in a processor
# folder (src/m68k, src/x86); the hosted core and its backends in src/hosted
-# (sdl3, web and psp build the same SDL3 backend, natively, for a browser
-# and for the PSP; win32 and win16 their own).
+# (sdl3, web, macos and psp build the same SDL3 backend, natively, for a
+# browser, for the Mac and for the PSP; win32 and win16 their own).
SRC_DIR := $(REPO_DIR)/src
SRC_CORE := $(REPO_DIR)/src/core
SRC_CG := $(REPO_DIR)/src/codegen
@@ -93,7 +102,7 @@ CORE_C_SRCS_NO_MIX := $(filter-out $(CORE_MIX_C_SRCS), $(CORE_C_SRCS))
# Generic portable-C op defaults (the src/generic backend) -- present on every
# target. spriteEmitStub.c is excluded: it is the no-codegen template and
-# redefines jlSpriteCompile / spriteCompiledDraw, which the real
+# redefines jlSpriteCompile / jlSpriteCompiledDraw, which the real
# src/codegen/spriteCompile.c already provides.
GENERIC_C_SRCS := $(filter-out %/spriteEmitStub.c, $(wildcard $(SRC_DIR)/generic/*.c))
@@ -113,13 +122,12 @@ BINDIR := $(PROGRAM_BUILD)/bin
PORT_TAG := $(call uc,$(PLATFORM))
# Baked assets (sprite and tile banks) carry the target they were baked for.
# DVX shows the DOS port's stage and runs its x86 code, so it takes the DOS
-# bakes; the hosted ports' stage (SDL3, the web, Win32, Win16, the PSP,
-# Haiku, BeOS) is chunky like the DOS one, so they take them too (Win16 runs a
-# pre-compiled bank's x86 code, as DVX does; the rest take its cels without
-# their code); the STe build is the ST port, so it
-# takes the ST's (same planes, same compiled sprites); every other port its
-# own.
-ASSET_PLATFORM := $(if $(filter dvx sdl3 web win32 win16 psp haiku haiku32 beos,$(PLATFORM)),dos,$(if $(filter ste,$(PLATFORM)),atarist,$(PLATFORM)))
+# bakes; the hosted ports' stage (SDL3, the web, macOS, Win32, Win16, the
+# PSP, Haiku, BeOS) is chunky like the DOS one, so they take them too (Win16
+# runs a pre-compiled bank's x86 code, as DVX does; the rest take its cels
+# without their code); the STe build is the ST port, so it takes the ST's
+# (same planes, same compiled sprites); every other port its own.
+ASSET_PLATFORM := $(if $(filter dvx sdl3 web macos win32 win16 psp haiku haiku32 beos,$(PLATFORM)),dos,$(if $(filter ste,$(PLATFORM)),atarist,$(PLATFORM)))
# The host sprite pre-compiler for this port's bakes (.spr -> .spc); its rule
# is make/programs.mk's.
SPRITEBAKE := $(JOEY_BUILD)/tools/spritebake-$(ASSET_PLATFORM)
diff --git a/make/dvx.mk b/make/dvx.mk
index 29528bd..a4f780f 100644
--- a/make/dvx.mk
+++ b/make/dvx.mk
@@ -14,7 +14,7 @@
# port's entry (src/dvx/dvxApp.c), a generated one-topic help file and
# resource list -- built and packaged by the SDK's own sdk.mk (KIND = app,
# LIBS = libjoey.a, DATA = its data files) and copied into build/dvx/bin/
-# laid out as it goes into DVX: apps/joeylib//.app, .hlp
+# laid out as it goes into DVX: apps///.app, .hlp
# and DATA/. The gates and joey-run copy that tree into a DVX installation.
#
# This file holds the library and programRules, the DVX rules for one
@@ -43,7 +43,7 @@ DVX_SDK_INCS := $(addprefix -I,$(filter-out %/basic %/net,$(patsubst %/,%,$(wild
# A program's exit() and atexit() are the port's (src/dvx/program.c): the
# handlers run, and JoeyLib shuts down, when the program ends rather than
# when DVX does. Applied to the library and the programs, never to the port.
-PROGRAM_RENAMES := -Dexit=dvxPortExit -Datexit=dvxPortAtExit
+PROGRAM_RENAMES := -Dexit=jlDvxPortExit -Datexit=jlDvxPortAtExit
CFLAGS := $(COMMON_CFLAGS) $(DVX_ARCH) -DJOEYLIB_PLATFORM_DVX -I$(SRC_X86) -I$(SRC_CG) -MMD -MP
LIB_CFLAGS := $(CFLAGS) $(PROGRAM_RENAMES)
@@ -108,13 +108,13 @@ $(LIB): $(LIB_OBJS)
# under bin/: .app, .hlp and its DATA/. The name is the id in lower
# case (DVX runs on DOS: an 8.3 name). DATA is staged apart, in
# build/dvx/data//DATA, for the SDK to package.
-# DVX_VENDOR the vendor folder (default joeylib)
+# DVX_VENDOR the vendor folder: whoever makes the program, 1 to 8
+# letters, digits or _ (DVX names its package vendor.name)
+# DVX_AUTHOR the resource list's author
# DVX_KIND what the help and resource texts call a program: " is a
# program" (default JoeyLib)
-# DVX_AUTHOR the resource list's author (default JoeyLib)
-DVX_VENDOR := joeylib
+# The vendor and the author are the project's own: it must set them.
DVX_KIND := JoeyLib
-DVX_AUTHOR := JoeyLib
PROGRAM_EXT := .app
PROGRAM_DATA_APART := yes
@@ -141,6 +141,8 @@ programRules = $(call programDvxRules,$(1),$(call programName,$(1)))
# the help line) is looked up here rather than passed in, and every line
# written out is shellQuote'd, so a title or help line may hold quotes.
define programDvxRules
+$(call requireSet,DVX_VENDOR,the project's own vendor folder$(comma) which its programs install in: 1 to 8 letters$(comma) digits or _)
+$(call requireSet,DVX_AUTHOR,the author DVX names for the project's programs)
.PHONY: app-$(2)
app-$(2): $(LIB) $(call programVar,$(1),SRCS) $(PORT_ENTRY) $(call programVar,$(1),DATA) $(APPSDIR)/$(2)/icon32.bmp
@mkdir -p $(APPSDIR)/$(2)
diff --git a/make/examples.mk b/make/examples.mk
index 38dadb3..948a09f 100644
--- a/make/examples.mk
+++ b/make/examples.mk
@@ -34,6 +34,8 @@
# of bands to check that they reach it right.
# SERSLOT ST: SERTEST opening JL_SERIAL_SLOT unit 2 -- a Mega STe's SCC
# channel B through Bconmap -- for verify-atarist-serial-slot.sh.
+# SERFAST Win16: SERTEST at 115200 for the throughput and loss gate,
+# verify-win16-serial-fast.sh.
# FASTRAM Amiga: the Fast RAM gate's program: a surface made once chip
# RAM has run out draws and copies as a chip one does.
# WBLAUNCH Amiga: the icon gate's program: a program's icon read and
@@ -43,11 +45,17 @@
# tones, voice and noise steps, sound effects, a module's end
# and tick rates, which every port must play alike.
-PROGRAMS := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK SCB JOY SPRITE AUDIO UBER ADV ADV2
-TEST_PROGRAMS := SCCPROBE SERLOSS SERLOSSS SERSLOT DIRTYMRK UBERPROB AUDIOPRB AUDPAR PALPROBE FASTRAM WBLAUNCH
+PROGRAMS := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK SCB JOY SPRITE SCROLL AUDIO UBER ADV ADV2
+TEST_PROGRAMS := SCCPROBE SERLOSS SERLOSSS SERSLOT SERFAST DIRTYMRK UBERPROB AUDIOPRB AUDPAR PALPROBE FASTRAM WBLAUNCH
-# DVX's help and resource texts call these JoeyLib examples.
-DVX_KIND := JoeyLib example
+# Whose these are, as a project names its own: on DVX the vendor folder
+# they install in and their author (make/dvx.mk), on macOS the start of
+# their apps' identifiers (make/macos.mk). DVX's help and resource texts
+# call them JoeyLib examples.
+DVX_VENDOR := kpunch
+DVX_AUTHOR := Scott Duensing
+DVX_KIND := JoeyLib example
+MACOS_ID_PREFIX := com.kangaroopunch.joeylib
# The IIgs build has never included HELLO.
HELLO_SRCS := $(EXAMPLES)/hello/hello.c
@@ -87,6 +95,9 @@ JOY_HELP := Joysticks: shows the position and buttons of each joystick.
SPRITE_SRCS := $(EXAMPLES)/sprite/sprite.c
SPRITE_TITLE := Sprite
SPRITE_HELP := Compiled sprites moving over a background.
+SCROLL_SRCS := $(EXAMPLES)/scroll/scroll.c
+SCROLL_TITLE := Scroll
+SCROLL_HELP := A tile layer scrolling a landscape under a sprite, with a bar showing the frames drawn a second.
AUDIO_SRCS := $(EXAMPLES)/audio/audio.c
AUDIO_TITLE := Audio
AUDIO_HELP := Sound: tracker music, a MOD, sound effects, a streamed tone and a noise sweep.
@@ -102,7 +113,7 @@ ADV2_HELP := A second small adventure game.
# Every example that ships as its own bootable IIgs volume (make
# iigs-game-disks).
-$(foreach p,DRAW PATTERN KEYS JOY SPRITE SAVE AUDIO ADV ADV2 UBER SERIAL SERTEST,$(eval $(p)_IIGS_GAME_DISK := yes))
+$(foreach p,DRAW PATTERN KEYS JOY SPRITE SCROLL SAVE AUDIO ADV ADV2 UBER SERIAL SERTEST,$(eval $(p)_IIGS_GAME_DISK := yes))
# IIgs serial probes for the gates: SCCPROBE (verify-iigs-serial.sh: does
# opening the modem port leave the printer port, AppleTalk's channel, alive?)
@@ -137,6 +148,10 @@ SERSLOT_SRCS := $(EXAMPLES)/sertest/sertest.c
SERSLOT_PORTS := atarist
SERSLOT_BINDIR := $(PROGRAM_BUILD)/test
SERSLOT_CFLAGS := -DSERTEST_DEVICE=JL_SERIAL_SLOT -DSERTEST_UNIT=2u
+SERFAST_SRCS := $(EXAMPLES)/sertest/sertest.c
+SERFAST_PORTS := win16
+SERFAST_BINDIR := $(PROGRAM_BUILD)/test
+SERFAST_CFLAGS := -DSERTEST_BAUD=115200u
FASTRAM_SRCS := $(REPO_DIR)/tests/amiga/fastRam.c
FASTRAM_PORTS := amiga
FASTRAM_BINDIR := $(PROGRAM_BUILD)/test
@@ -153,10 +168,10 @@ DIRTYMRK_CFLAGS := -DDIRTY_MARK_SCENARIOS=$(DIRTYMARK_SCENARIOS) -DDIRTY_MAR
# and bench-iigs.sh look for (iigs-verify-shrtail). DVX and the windowed
# hosted ports build it as part of `all`, into build//probe, for
# their display gates (scripts/verify-dvx-display.sh,
-# verify-win16-display.sh, verify-win32-display.sh, and
-# verify-be-display.sh).
+# verify-win16-display.sh, verify-win32-display.sh, verify-be-display.sh,
+# and verify-atarist-display.sh).
UBERPROB_SRCS := $(UBER_SRCS)
-UBERPROB_PORTS := iigs dvx win16 win32 haiku haiku32 beos
+UBERPROB_PORTS := iigs dvx win16 win32 haiku haiku32 beos atarist ste amiga
UBERPROB_CFLAGS := -DUBER_PROBE -DUBER_FRAMES=1
UBERPROB_TITLE := Uber Probe
UBERPROB_HELP := UBER with its probe rows: the present checked against the stage on screen. Its results go to joeylog.txt in its settings folder.
@@ -168,6 +183,9 @@ UBERPROB_WIN32_BINDIR := $(PROGRAM_BUILD)/probe
UBERPROB_HAIKU_BINDIR := $(PROGRAM_BUILD)/probe
UBERPROB_HAIKU32_BINDIR := $(PROGRAM_BUILD)/probe
UBERPROB_BEOS_BINDIR := $(PROGRAM_BUILD)/probe
+UBERPROB_ATARIST_BINDIR := $(PROGRAM_BUILD)/probe
+UBERPROB_STE_BINDIR := $(PROGRAM_BUILD)/probe
+UBERPROB_AMIGA_BINDIR := $(PROGRAM_BUILD)/probe
# AUDIO with its keys pressed on a timetable (-DAUDIO_PROBE): N once, S every
# 1.5 seconds, under the tracker song. The IIgs and X68000 audio gates read
diff --git a/make/haiku32.mk b/make/haiku32.mk
index 9643e91..ba19b46 100644
--- a/make/haiku32.mk
+++ b/make/haiku32.mk
@@ -13,7 +13,9 @@ include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
$(call requireTool,$(HAIKU32_ROOT)/cross/bin/i586-pc-haiku-g++,toolchain:haiku32)
-HOSTED_CC := $(HAIKU32_GCC2) $(HAIKU32_ROOT) gcc
+# -fno-common: gcc 2.95 merges a program's uninitialised global into a
+# library one of the same name; every other port's compiler refuses the link.
+HOSTED_CC := $(HAIKU32_GCC2) $(HAIKU32_ROOT) gcc -fno-common
HOSTED_CXX := $(HAIKU32_GCC2) $(HAIKU32_ROOT) g++
HOSTED_AR := $(HAIKU32_GCC2) $(HAIKU32_ROOT) ar
BE_CXX_RUNTIME := -lstdc++.r4
diff --git a/make/hosted.mk b/make/hosted.mk
index 20845ef..fef807d 100644
--- a/make/hosted.mk
+++ b/make/hosted.mk
@@ -7,8 +7,9 @@
# program, which make/programs.mk instantiates for every program.
# Included by make/sdl3.mk (a native program), make/web.mk (the same
# SDL3 backend compiled to WebAssembly), make/psp.mk (the same again, for
-# the PSP), make/win32.mk, make/win16.mk and make/be.mk (Haiku), after each
-# has included common.mk and set:
+# the PSP), make/macos.mk (and again, for macOS), make/win32.mk,
+# make/win16.mk and make/be.mk (Haiku), after each has included common.mk
+# and set:
# HOSTED_CC the compiler
# HOSTED_AR the archiver
# HOSTED_BACKEND the backend folder under src/hosted
@@ -18,13 +19,16 @@
# PROGRAM_EXT the program file's extension
# and, when the backend is C++ (src/hosted//*.cpp, the Be API's),
# HOSTED_CXX its C++ compiler
-# and may set HOSTED_PROGRAM_DEPS, files every program is rebuilt after (the
-# web page's shell), and, when a program needs its DATA at link time (the web
-# port packs it into the program), HOSTED_LINK_DATA: the link flags for one
-# program's DATA folder, $(1) the program's id. When the program file is
-# packed from what is linked (the PSP's EBOOT.PBP, from an ELF), the port
-# sets HOSTED_LINK_OUT, the file one program is linked to, and HOSTED_PACK,
-# the command that packs it into the program file ($(1) the id in both).
+# and may set HOSTED_ARFLAGS, what the archiver is given before the
+# library's name (rcs; LLVM's libtool, which archives macOS's universal
+# objects, takes -static -o), and HOSTED_PROGRAM_DEPS, files every program
+# is rebuilt after (the web page's shell), and, when a program needs its
+# DATA at link time (the web port packs it into the program),
+# HOSTED_LINK_DATA: the link flags for one program's DATA folder, $(1) the
+# program's id. When the program file is packed from what is linked (the
+# PSP's EBOOT.PBP, from an ELF; a Mac app, from a program), the port sets
+# HOSTED_LINK_OUT, the file one program is linked to, and HOSTED_PACK, the
+# command that packs it into the program file ($(1) the id in both).
# A port whose programs show an icon (make/common.mk's programIcon, baked by
# tools/iconBake.py) sets HOSTED_ICON, the file one program's icon is baked
# to, and HOSTED_ICON_BAKE, the command that bakes it there from the
@@ -42,6 +46,7 @@ HOSTED_DIR := $(SRC_DIR)/hosted
BACKEND_DIR := $(HOSTED_DIR)/$(HOSTED_BACKEND)
CFLAGS := $(COMMON_CFLAGS) $(HOSTED_DEFINES) -I$(HOSTED_DIR) -MMD -MP
+HOSTED_ARFLAGS ?= rcs
# HOSTED_X86 (make/win16.mk): sprites are compiled to x86 code as on DOS --
# src/x86's emitter and src/codegen's dispatch in place of the stub.
@@ -56,11 +61,14 @@ endif
# system it runs on, built with the backend's flags; the backend includes
# its headers.
HOSTED_C_SRCS := $(wildcard $(HOSTED_DIR)/*.c)
-# -DJOEYLIB_HOSTED_POSIX_SERIAL in HOSTED_DEFINES (make/sdl3.mk, make/be.mk):
+# -DJOEYLIB_HOSTED_POSIX_SERIAL in HOSTED_DEFINES (make/sdl3.mk, make/macos.mk,
+# make/be.mk):
# the serial add-on through termios, src/hosted/posix.
POSIX_C_SRCS := $(if $(filter -DJOEYLIB_HOSTED_POSIX_SERIAL,$(HOSTED_DEFINES)),$(wildcard $(HOSTED_DIR)/posix/*.c))
BACKEND_C_SRCS := $(wildcard $(BACKEND_DIR)/*.c)
BACKEND_CXX_SRCS := $(wildcard $(BACKEND_DIR)/*.cpp)
+# Assembly of the backend's own, assembled by HOSTED_AS (make/win16.mk).
+BACKEND_ASM_SRCS := $(wildcard $(BACKEND_DIR)/*.asm)
EXTRA_C_SRCS := $(if $(HOSTED_BACKEND_EXTRA),$(wildcard $(HOSTED_BACKEND_EXTRA)/*.c))
HOSTED_ICON_ID := 1
BACKEND_CFLAGS := $(CFLAGS) $(HOSTED_SDK_CFLAGS) $(if $(HOSTED_BACKEND_EXTRA),-I$(HOSTED_BACKEND_EXTRA)) -DHOSTED_ICON_ID=$(HOSTED_ICON_ID)
@@ -73,6 +81,7 @@ LIB_OBJS := \
$(patsubst $(HOSTED_DIR)/posix/%.c,$(BUILD)/obj/posix/%.o,$(POSIX_C_SRCS)) \
$(patsubst $(BACKEND_DIR)/%.c,$(BUILD)/obj/backend/%.o,$(BACKEND_C_SRCS)) \
$(patsubst $(BACKEND_DIR)/%.cpp,$(BUILD)/obj/backend/%.o,$(BACKEND_CXX_SRCS)) \
+ $(patsubst $(BACKEND_DIR)/%.asm,$(BUILD)/obj/backend/%.o,$(BACKEND_ASM_SRCS)) \
$(patsubst $(HOSTED_BACKEND_EXTRA)/%.c,$(BUILD)/obj/backendExtra/%.o,$(EXTRA_C_SRCS))
LIB := $(LIBDIR)/libjoey.a
@@ -110,6 +119,10 @@ $(BUILD)/obj/backend/%.o: $(BACKEND_DIR)/%.cpp
@mkdir -p $(dir $@)
$(HOSTED_CXX) $(BACKEND_CFLAGS) -c $< -o $@
+$(BUILD)/obj/backend/%.o: $(BACKEND_DIR)/%.asm
+ @mkdir -p $(dir $@)
+ $(HOSTED_AS) $< $@
+
$(BUILD)/obj/backendExtra/%.o: $(HOSTED_BACKEND_EXTRA)/%.c
@mkdir -p $(dir $@)
$(HOSTED_CC) $(BACKEND_CFLAGS) -c $< -o $@
@@ -119,7 +132,7 @@ $(BUILD)/obj/backendExtra/%.o: $(HOSTED_BACKEND_EXTRA)/%.c
$(LIB): $(LIB_OBJS)
@mkdir -p $(dir $@)
rm -f $@
- $(HOSTED_AR) rcs $@ $^
+ $(HOSTED_AR) $(HOSTED_ARFLAGS) $@ $^
# Pull in per-object header-dependency files generated by -MMD/-MP.
-include $(LIB_OBJS:.o=.d)
diff --git a/make/iigs.mk b/make/iigs.mk
index b4c99b5..fc89463 100644
--- a/make/iigs.mk
+++ b/make/iigs.mk
@@ -192,7 +192,7 @@ iigs-examples: programs
# examples as ProDOS S16 apps (an 800KB floppy can't hold every example; see
# make-iigs-disk.sh) with their DATA. NTP_BIN rides along so jlpAudioInit's
# runtime load of "ntpplayer" finds it on the volume -- without it every
-# example logs "audioInit failed".
+# example logs "jlAudioOpen failed".
iigs-disk: programs $(NTP_BIN)
BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg
@@ -204,7 +204,7 @@ iigs-disk: programs $(NTP_BIN)
iigs-disk-all: programs $(NTP_BIN)
BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \
JOEY_DISK_SIZE="16MB" \
- JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE TILEBANK AUDIO UBER ADV2 ADV" \
+ JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SCROLL SERIAL SERTEST SAVE TILEBANK AUDIO UBER ADV2 ADV" \
$(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey-all.2mg
# Headless visual-verification gate: boot GS/OS under MAME, launch the DRAW
@@ -240,7 +240,11 @@ iigs-verify-serial-loss: $(call programBin,SERLOSS) $(call programBin,SERLOSSS)
iigs-verify-all: iigs-disk
$(REPO_DIR)/scripts/verify-iigs-all.sh
-# Gate the 712 display-state bytes ABOVE the pixel block: the SCB array
+# Gate what the present leaves on the screen: every $E1 pixel byte equal to
+# the stage after a run of draws, the marks' asm included (shrParity), every
+# dirty band CLEAN again after its present (shrBands: a mark that skips its
+# 8-row group flag is never cleared, and the present can skip its rows), and
+# the 712 display-state bytes ABOVE the pixel block: the SCB array
# ($E1:9D00-$9DC7) and all 16 palettes ($E1:9E00-$9FFF). Nothing else in the
# tree can see them -- the golden hash covers the STAGE, not $E1, and P9's
# pixel compare stops at $9CFF -- so a wrong palette is invisible to every
@@ -259,8 +263,14 @@ iigs-verify-shrtail: $(call programBin,UBERPROB) $(NTP_BIN)
$(REPO_DIR)/scripts/make-iigs-disk.sh $(IIGS_PROBE_DIR)/uberprobe.2mg
BENCH_DATA_DISK="$(IIGS_PROBE_DIR)/uberprobe.2mg" BENCH_OUT_LOG="$(IIGS_PROBE_DIR)/probelog.txt" \
BENCH_MAX_FRAMES="$(or $(PROBE_MAX_FRAMES),45000)" $(REPO_DIR)/scripts/bench-iigs.sh
- @grep -E 'shrParity|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt || \
+ @grep -E 'shrParity|shrBands|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt || \
{ echo "iigs-verify-shrtail: FAIL - no probe rows captured" >&2; exit 1; }
+ @test "$$(grep -cE 'shrParity|shrBands|shrTailUpload|shrTailSlam' $(IIGS_PROBE_DIR)/probelog.txt)" -eq 4 || \
+ { echo "iigs-verify-shrtail: FAIL - not every probe row ran" >&2; exit 1; }
+ @grep -qE 'shrParity FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \
+ { echo "iigs-verify-shrtail: FAIL - \$$E1 pixels mismatched the stage" >&2; exit 1; } || true
+ @grep -qE 'shrBands FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \
+ { echo "iigs-verify-shrtail: FAIL - a band stayed dirty past its present" >&2; exit 1; } || true
@grep -qE 'shrTail(Upload|Slam) FAIL' $(IIGS_PROBE_DIR)/probelog.txt && \
{ echo "iigs-verify-shrtail: FAIL - \$$E1 SCB/palette block mismatched the stage" >&2; exit 1; } || true
@echo "iigs-verify-shrtail: PASS"
diff --git a/make/macos.mk b/make/macos.mk
new file mode 100644
index 0000000..40b6d8c
--- /dev/null
+++ b/make/macos.mk
@@ -0,0 +1,65 @@
+# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
+# See LICENSE.md for license information.
+# SPDX-License-Identifier: MIT
+#
+# macOS build rules (make macos): JoeyLib programs as Mac apps, through the
+# hosted core and its SDL3 backend (src/hosted, src/hosted/sdl3), built on
+# Linux. The rules are make/hosted.mk's; this file says what is macOS's. A
+# project reaches it through make/project.mk.
+#
+# Built with make/support/macos/cc.sh: zig for Apple silicon and for Intel,
+# against the Apple SDK, the two joined into universal files that run on
+# either (macOS 13 or newer). Programs link the universal static SDL3 the
+# installer builds (SDL3_MACOS_DIR), so they run without an SDL3 on the Mac.
+# A program is an app, bin/NAME.app, with the program in Contents/MacOS and
+# its DATA and icon in Contents/Resources, its folder, which the port makes
+# the current directory when the window opens (SAVES/ and joeylog.txt go in
+# the user's Application Support folder: src/hosted/sdl3/sdl3Backend.c). The
+# app's identifier is MACOS_ID_PREFIX, a reverse-DNS name of the project's
+# own, and the program's name; the project must set it. The Apple silicon
+# half is signed ad hoc as zig links it, which macOS needs to run it at all.
+
+PLATFORM := macos
+include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
+
+SDL3_MACOS_LIB := $(SDL3_MACOS_DIR)/lib/libSDL3.a
+$(call requireTool,$(ZIG),toolchain:macos)
+$(call requireTool,$(MACOS_LIBTOOL),toolchain:macos)
+$(call requireTool,$(SDL3_MACOS_LIB),toolchain:macos)
+
+SDL3_MACOS_PKG_CONFIG := PKG_CONFIG_LIBDIR=$(SDL3_MACOS_DIR)/lib/pkgconfig pkg-config
+MACOS_SUPPORT := $(REPO_DIR)/make/support/macos
+
+# LLVM's libtool archives the universal objects (it warns that audio.o and
+# input.o are named twice, src/core's and src/hosted's, and keeps both).
+HOSTED_CC := $(MACOS_SUPPORT)/cc.sh
+HOSTED_AR := $(MACOS_LIBTOOL)
+HOSTED_ARFLAGS := -static -o
+HOSTED_BACKEND := sdl3
+HOSTED_DEFINES := -DJOEYLIB_PLATFORM_HOSTED -DJOEYLIB_HOSTED_SDL3 -DJOEYLIB_HOSTED_POSIX_SERIAL
+HOSTED_SDK_CFLAGS := $(shell $(SDL3_MACOS_PKG_CONFIG) --cflags sdl3)
+HOSTED_LINK := $(shell $(SDL3_MACOS_PKG_CONFIG) --static --libs sdl3) -Wl,-dead_strip
+PROGRAM_EXT :=
+# A program is relinked when SDL changes, and with it repacked when the
+# packing does.
+HOSTED_PROGRAM_DEPS := $(SDL3_MACOS_LIB) $(MACOS_SUPPORT)/packApp.sh
+
+# One program's app, $(1) its id: in bin/ (or ID_BINDIR), never in a folder
+# of its own (an app is one). Its folder is the app's Contents/Resources,
+# where its DATA goes, and SDL's base path for it.
+macosApp = $(or $(call programVar,$(1),BINDIR),$(BINDIR))/$(call programName,$(1)).app
+programDir = $(call macosApp,$(1))/Contents/Resources
+programBin = $(call macosApp,$(1))/Contents/MacOS/$(call programName,$(1))
+
+# Linked outside the app, which then gets a copy: the dependency file stays
+# out of it. By the id, which a probe build (ID_BINDIR) does not share with
+# the program of its name.
+HOSTED_LINK_OUT = $(PROGRAM_BUILD)/link/$(1)
+# Its identifier: the project's prefix and its name.
+macosId = $(call requireSet,MACOS_ID_PREFIX,the start of its apps' identifiers$(comma) a reverse-DNS name of the project's own (com.example.games))$(MACOS_ID_PREFIX).$(call programName,$(1))
+HOSTED_PACK = $(MACOS_SUPPORT)/packApp.sh $(call HOSTED_LINK_OUT,$(1)) $(call macosApp,$(1)) $(call macosId,$(1)) $(call shellQuote,$(or $(call programVar,$(1),TITLE),$(call programName,$(1))))
+# Its icon is the app's (Finder, the Dock), not the window's.
+HOSTED_ICON = $(call programDir,$(1))/icon.icns
+HOSTED_ICON_BAKE = $(ICON_BAKE) icns $(call programIcon,$(1)) $(call HOSTED_ICON,$(1))
+
+include $(REPO_DIR)/make/hosted.mk
diff --git a/make/programs.mk b/make/programs.mk
index 77c264b..2f92d30 100644
--- a/make/programs.mk
+++ b/make/programs.mk
@@ -28,7 +28,8 @@
# on the Amiga Capitalised (Hello), on DVX lower case
# ID_FOLDER non-empty: it ships in a folder of its own, bin//,
# with its DATA beside it (the Amiga, ST, DOS and X68000;
-# on the IIgs and DVX every program is packaged apart anyway)
+# on the IIgs, DVX and macOS every program is packaged
+# apart anyway)
# ID_BINDIR build it here instead of bin/ (probe builds)
# ID_DATA its DATA files, as the files the description's own rules
# make under $(call programDataDir,ID) (or
@@ -37,9 +38,12 @@
# where there is no icon), baked for each port that shows
# programs by an icon -- the IIgs Finder, the Amiga's
# Workbench, DVX, Win16, Win32, Haiku and BeOS, the PSP's
-# menu, the SDL3 window, and the web page; by default
-# JoeyLib's kangaroo (docs/BuildIntegration.adoc, Icons)
-# ID_TITLE DVX: its window title (default: its Amiga-style name)
+# menu, the SDL3 window, the Mac's Finder and Dock, and the
+# web page; by default JoeyLib's kangaroo
+# (docs/BuildIntegration.adoc, Icons)
+# ID_TITLE its title: DVX's window title (default: its Amiga-style
+# name), the PSP menu's, and a Mac app's display name
+# (default: its name)
# ID_HELP DVX: the line of its help file (no apostrophes: the help is
# written through the shell in quotes)
# ID_IIGS_OVERFLOW_CAP IIgs: link large overflow segments (clang-build.sh's
diff --git a/make/support/iigs/clang-build.sh b/make/support/iigs/clang-build.sh
index 9adc13b..1f831a1 100755
--- a/make/support/iigs/clang-build.sh
+++ b/make/support/iigs/clang-build.sh
@@ -148,6 +148,13 @@ linkMap="$tmpdir/link.map"
linkReloc="$tmpdir/link.reloc"
linkManifest="$tmpdir/link.json"
linkErr="$tmpdir/link.err"
+# A kept map (JOEY_LINK_MAP, below) lists the local symbols too: static
+# functions, which a profile otherwise folds into the global before them.
+# link816 only: omfEmit rejects the flag.
+mapLocals=()
+if [ -n "${JOEY_LINK_MAP:-}" ]; then
+ mapLocals=(--map-locals)
+fi
# The GS/OS load name is the output basename, uppercased to <=8 alnum chars.
omfName="$(basename "$output" | tr '[:lower:]' '[:upper:]' | tr -cd 'A-Z0-9' | cut -c1-8)"
@@ -171,7 +178,7 @@ emitMultiSeg() { # $1 = segment-cap, $2 = bss
"$LINK" -o "$linkBin" --text-base 0 --bss-base "$2" --segment-cap "$1" \
--overflow-cap "$JOEY_OVERFLOW_CAP" --full-bank \
--manifest "$linkManifest" --map "$linkMap" --reloc-out "$linkReloc" \
- --multi-seg-expressload "${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" || return 1
+ --multi-seg-expressload "${mapLocals[@]}" "${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" || return 1
"$OMFEMIT" --manifest "$linkManifest" --expressload --relocs "$linkReloc" \
--output "$output"
}
@@ -313,7 +320,7 @@ emitMultiSegFit() {
if "$LINK" -o "$linkBin" --text-base 0 --bss-base 0xA000 --full-bank \
--map "$linkMap" --reloc-out "$linkReloc" \
- "${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" && heapUsable; then
+ "${mapLocals[@]}" "${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" && heapUsable; then
# Single-segment AND the heap is big enough: wrap as-is.
"$OMFEMIT" --input "$linkBin" --map "$linkMap" --base 0 --entry __start \
--output "$output" --name "$omfName" --expressload --relocs "$linkReloc"
@@ -325,10 +332,28 @@ fi
# The link map is the only record of where a symbol landed, and the OMF
# carries no symbols at all, so anything that inspects a running program
-# (the PC profiler, the MAME state gates) needs it kept. Off by default.
+# (the PC profiler, the MAME state gates) needs it kept. Off by default:
+# JOEY_LINK_MAP names a directory, which gets