More bugs fixed. Performance improvements. More tilemap features.

This commit is contained in:
Scott Duensing 2026-10-17 15:39:14 -05:00
parent 484d22691b
commit 423f992583
255 changed files with 16289 additions and 8839 deletions

View file

@ -23,16 +23,17 @@ most.
### SDL 3.4.18
- **Used on:** SDL3, PSP, web
- **Used on:** SDL3, PSP, web, macOS
- **URL:** <https://www.libsdl.org/>
- **License:** zlib
- **Role:** window, input, and audio for the hosted SDL3 code. Built as a
static library for the host, for the PSP with pspdev, and for Emscripten
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:** <http://aminet.net/mus/play/ptplayer.lha>
(author Frank Wille, <http://aminet.net/package/mus/play/ptplayer>)
- **License:** Public Domain (the archive's `license` file: the Unlicense)
@ -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:** <https://ziglang.org/>
- **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,
<https://github.com/joseluisq/macosx-sdks>, 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

View file

@ -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)"

View file

@ -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/<name>/`,
is `../dvxgui`); its examples land in `build/dvx/bin/apps/kpunch/<name>/`,
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/<NAME>/`. The Win32 port needs `toolchain:win32` (llvm-mingw)
`build/web/bin/<NAME>/`. The macOS port needs `toolchain:macos` (zig,
Apple's macOS SDK, and SDL 3 built with them); its examples are apps,
`build/macos/bin/<NAME>.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

View file

@ -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

View file

@ -4,7 +4,8 @@ Declared in `<joey/debug.h>`.
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

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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`),

View file

@ -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.
|===

View file

@ -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
<<Firmware that is not downloaded>>) 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 <<Firmware that is not downloaded>>),
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>>).

View file

@ -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

View file

@ -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;
}

View file

@ -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());

View file

@ -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;

344
examples/scroll/scroll.c Normal file
View file

@ -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 <stdio.h>
#include <joey/joey.h>
#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;
}

View file

@ -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

View file

@ -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;

View file

@ -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) {

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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)"
}

View file

@ -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 <SDL3/SDL.h>\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"
}

View file

@ -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"
}

View file

@ -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 -

View file

@ -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<<regno, index);

View file

@ -0,0 +1,14 @@
# m68k-amigaos-gcc 6.5 patches
install.sh (component `amigaGcc`) applies these to the pinned `gcc`
sub-project (`projects/gcc`) in filename order before it builds the
toolchain.
| Patch | What it fixes | Needed by |
|---|---|---|
| 0001-bbb-moveq-mask | A miscompile in Bebbo's post-pass optimizer (`gcc/bbb-opts.c`, on by default at `-O2`). The backend loads a data register's word or byte constant in -128..127 with `moveq`, which writes all 32 bits, sign-extended; the optimizer's register tracker took it for a word or byte write and kept the upper word's old "known clear" mask. After a negative one (a `moveq #-1,dN` for a compare with 0xFFFF, say), its `(e0)` step then deleted a later zero-extension's `moveq #0,dN` before `move.w x,dN` as superfluous, so a 16-bit offset carried 0xFFFF in its upper word into a 32-bit pointer add. It struck `jlSpriteRestoreUnder`'s fused stage restore, which wrote 64 KB below the planes once a neighbouring inline asm call changed the register allocation. Upstream (`amiga6.5`, 2026-09-26) has the same code. | every Amiga build |
## License
The patch modifies GCC and contains its code, so it is under GCC's license,
the GNU GPL v3 or later, not JoeyLib's MIT License.

View file

@ -4,6 +4,8 @@
# SDL3 port, natively, for the web port, in a browser, for the Win32 port,
# under Wine, and for the Win16 port, on Windows 3.11 in DOSBox; the PSP
# port's in headless PPSSPP; the Haiku and BeOS ports' in their QEMU VMs).
# A macOS port's app is recognised but not run: nothing here runs Mac
# programs.
#
# Usage:
# ./joey-run [options] PROGRAM [-- ARGS...]
@ -11,12 +13,12 @@
# PROGRAM is any of:
# a program file build/dos/bin/DRAW.EXE, build/atarist/bin/DRAW.PRG (or build/ste),
# build/amiga/bin/Draw, build/x68000/bin/DRAW.X,
# build/iigs/bin/DRAW, build/dvx/bin/apps/joeylib/draw/draw.app,
# build/iigs/bin/DRAW, build/dvx/bin/apps/kpunch/draw/draw.app,
# build/sdl3/bin/DRAW, build/web/bin/DRAW/DRAW.html,
# build/psp/bin/DRAW/EBOOT.PBP, build/haiku/bin/DRAW
# (or build/haiku32, build/beos)
# (or build/haiku32, build/beos), build/macos/bin/DRAW.app
# an app folder a program in a folder of its own with its DATA:
# build/dvx/bin/apps/joeylib/audio
# build/dvx/bin/apps/kpunch/audio
# an IIgs disk image build/iigs/bin/DRAW.2mg (make iigs-game-disk), or
# a data disk such as joey.2mg (boots GS/OS with it)
# an example name draw, audio, uber, ... together with --port
@ -25,8 +27,8 @@
# image in the project's build/, or give its name with --project and --port.
#
# Options:
# --port PORT iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, win32, win16,
# psp, haiku, haiku32 or beos
# --port PORT iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, macos, win32,
# win16, psp, haiku, haiku32 or beos
# --project DIR look a program name up in the project at DIR (its
# build/PORT/bin) instead of JoeyLib's examples; DVX runs
# keep their DVX in DIR's build/dvx/run
@ -273,6 +275,7 @@ magicPort() {
fi
;;
3c21646f*) printf 'web\n' ;; # "<!do(ctype html>": 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

View file

@ -20,7 +20,9 @@ $(call requireTool,$(AMIGA_PTPLAYER)/ptplayer.asm,toolchain:amiga)
# independently. -I on $(SRC_DIR)/amiga lets ptplayer.h resolve
# <SDI_compiler.h> 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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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/<machine>/ (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)

View file

@ -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/<name>/<name>.app, <name>.hlp
# laid out as it goes into DVX: apps/<vendor>/<name>/<name>.app, <name>.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/: <name>.app, <name>.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/<name>/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: "<title> is a
# <kind> 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)

View file

@ -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/<port>/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

View file

@ -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

View file

@ -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/<backend>/*.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)

View file

@ -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"

65
make/macos.mk Normal file
View file

@ -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

View file

@ -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/<name>/,
# 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

View file

@ -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 <output name>.map and the
# segment images beside it, <output name>.seg<N>.bin (segment 1 is
# link816's main output), with link816's segment manifest, <output
# name>.json, for a multi-segment one. The Loader puts each segment in whatever bank it
# likes, and scripts/profile-iigs.sh finds them there by their bytes. A
# directory, not a file, because one make links several programs (a game
# disk's Start launcher, say) and each keeps its own.
if [ -n "${JOEY_LINK_MAP:-}" ] && [ -f "$linkMap" ]; then
mkdir -p "$(dirname "$JOEY_LINK_MAP")"
cp "$linkMap" "$JOEY_LINK_MAP"
mapBase="$JOEY_LINK_MAP/$(basename "$output")"
mkdir -p "$JOEY_LINK_MAP"
cp "$linkMap" "$mapBase.map"
rm -f "$mapBase".seg*.bin
cp "$linkBin" "$mapBase.seg1.bin"
for seg in "$tmpdir"/link.seg*.bin; do
[ -f "$seg" ] || continue
cp "$seg" "$mapBase.${seg##*/link.}"
done
rm -f "$mapBase.json"
if [ -f "$linkManifest" ]; then
cp "$linkManifest" "$mapBase.json"
fi
fi
# Combined header-dependency file: union of clang -MM over the C sources,

View file

@ -5,7 +5,7 @@
#
# clang-smoke.sh - End-to-end smoke test of the llvm-mos w65816 IIgs
# build path. Compiles a tiny C main that calls JoeyLib's ported asm
# (iigsInitRowLut), links it with clang-build.sh, runs it under MAME's
# (jlIigsInitRowLut), links it with clang-build.sh, runs it under MAME's
# apple2gs, and verifies the LUT the asm computed.
#
# This proves the whole toolchain end to end: clang -> llvm-mc ->
@ -32,15 +32,15 @@ export MAME_ROMPATH="$IIGS_ROMPATH"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
# gRowOffsetLut[5] = 5 * 160 = 800 = 0x0320. Publish it to a 24-bit
# jlgRowOffsetLut[5] = 5 * 160 = 800 = 0x0320. Publish it to a 24-bit
# address MAME can read back (bank 2 is clear of IIgs ROM scribbling).
cat > "$tmp/smoke.c" <<'EOF'
#include <stdint.h>
extern void iigsInitRowLut(void);
extern uint16_t gRowOffsetLut[];
extern void jlIigsInitRowLut(void);
extern uint16_t jlgRowOffsetLut[];
int main(void) {
iigsInitRowLut();
*(volatile uint16_t *)0x025000 = gRowOffsetLut[5];
jlIigsInitRowLut();
*(volatile uint16_t *)0x025000 = jlgRowOffsetLut[5];
return 0;
}
EOF
@ -63,7 +63,7 @@ LINK="$LLVM816_ROOT/tools/link816"
"$RT/crt0.o" "$RT/libgcc.o" "$RT/libc.o" "$RT/extras.o" \
"$tmp/smoke.o" "$tmp/joeyDraw.o"
echo "[clang-smoke] running under MAME apple2gs, checking gRowOffsetLut[5] == 0x0320"
echo "[clang-smoke] running under MAME apple2gs, checking jlgRowOffsetLut[5] == 0x0320"
bash "$RUN_IN_MAME" "$tmp/smoke.bin" --check 0x025000=0320
echo "[clang-smoke] PASS"

106
make/support/macos/cc.sh Executable file
View file

@ -0,0 +1,106 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# The macOS port's C compiler (make/macos.mk; the installer builds SDL with
# it too): zig cc once for each architecture in MACOS_ARCHS -- by default
# Apple silicon and Intel -- with the results joined into one universal file
# by llvm-lipo, as Apple's own clang does when given two -arch flags. It
# takes what a C compiler takes: with -c it makes a universal object, else a
# universal program. The first architecture's run is exactly the call it was
# given, so its dependency file (-MMD) comes out as a plain compiler's; the
# other runs leave theirs out. With one architecture, or with no -o (a
# preprocessing run), it is zig cc for the first architecture alone.
#
# Programs run on macOS MACOS_MIN or newer: Ventura, which every Apple
# silicon Mac runs and the Intel ones from 2017 on.
#
# From the environment (toolchains/config.mk, which make exports): ZIG and
# its cache folders, MACOS_SDK and MACOS_LIPO.
#
# Usage: cc.sh COMPILER-ARGUMENTS...
# cc.sh --minimum prints MACOS_MIN (for an Info.plist)
set -euo pipefail
MACOS_MIN=13.0
read -r -a archs <<< "${MACOS_ARCHS:-aarch64 x86_64}"
if [[ "${1:-}" == --minimum ]]; then
printf '%s\n' "$MACOS_MIN"
exit 0
fi
if [[ -z "${ZIG:-}" || -z "${MACOS_SDK:-}" || -z "${MACOS_LIPO:-}" ]]; then
echo "$0: ZIG, MACOS_SDK or MACOS_LIPO is not set; run ./install.sh install toolchain:macos" >&2
exit 1
fi
out=""
compile=0
preprocess=0
prev=""
for arg in "$@"; do
if [[ "$prev" == -o ]]; then
out="$arg"
fi
case "$arg" in
-c) compile=1 ;;
-E) preprocess=1 ;;
esac
prev="$arg"
done
# zig brings the C library's headers and libSystem's stubs; the SDK the
# rest, after them (so the caller's own come first): its other headers, its
# frameworks, and, for a link, the stub libraries beside them. zig cc
# builds for UBSan unless told not to, and would then want its runtime; and
# it warns, as no other port's compiler does, at __DATE__ and __TIME__ (a
# build that uses them cannot be reproduced), which -Werror would make fatal.
flags=(-fno-sanitize=undefined -Wno-date-time -idirafter "$MACOS_SDK/usr/include" -F"$MACOS_SDK/System/Library/Frameworks")
if [[ $compile -eq 0 && $preprocess -eq 0 ]]; then
flags+=(-L"$MACOS_SDK/usr/lib")
fi
# zig refuses a target with a version for a run that only preprocesses (a
# build system asking what the compiler defines), and the version makes no
# difference to that.
target() {
if [[ $preprocess -eq 1 ]]; then
printf '%s-macos\n' "$1"
else
printf '%s-macos.%s\n' "$1" "$MACOS_MIN"
fi
}
if [[ ${#archs[@]} -eq 1 || -z "$out" ]]; then
exec "$ZIG" cc -target "$(target "${archs[0]}")" "$@" "${flags[@]}"
fi
slices=()
trap 'rm -f "${slices[@]}"' EXIT
for arch in "${archs[@]}"; do
slice="$out.$arch"
slices+=("$slice")
if [[ ${#slices[@]} -eq 1 ]]; then
"$ZIG" cc -target "$(target "$arch")" "$@" "${flags[@]}"
mv -f "$out" "$slice"
continue
fi
args=()
skip=0
for arg in "$@"; do
if [[ $skip -eq 1 ]]; then
skip=0
continue
fi
case "$arg" in
-o) args+=(-o "$slice"); skip=1 ;;
-MD|-MMD|-MP) ;;
-MF|-MT|-MQ) skip=1 ;;
*) args+=("$arg") ;;
esac
done
"$ZIG" cc -target "$(target "$arch")" "${args[@]}" "${flags[@]}"
done
"$MACOS_LIPO" -create "${slices[@]}" -output "$out"

64
make/support/macos/packApp.sh Executable file
View file

@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# Pack a linked macOS program into its app, as make/macos.mk's packing step:
# the program as Contents/MacOS/NAME (NAME the app's, without .app), and
# Contents/Info.plist, which tells macOS that is the program to run, the
# app's identifier and title, its icon (Contents/Resources/icon.icns, baked
# by make beside its DATA), the oldest macOS it runs on (cc.sh's), and that
# it draws at a Retina screen's own resolution. The program's Apple silicon
# half carries the ad hoc signature zig gave it, which a copy keeps.
#
# Usage: packApp.sh PROGRAM NAME.app IDENTIFIER TITLE
set -euo pipefail
if [[ $# -ne 4 ]]; then
echo "usage: $0 PROGRAM NAME.app IDENTIFIER TITLE" >&2
exit 2
fi
program=$1
app=$2
identifier=$3
title=$4
name=$(basename "$app" .app)
minimum=$("$(dirname "$0")/cc.sh" --minimum)
# The title as XML text.
title=${title//&/&amp;}
title=${title//</&lt;}
title=${title//>/&gt;}
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources"
cp -f "$program" "$app/Contents/MacOS/$name"
cat > "$app/Contents/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>$title</string>
<key>CFBundleExecutable</key>
<string>$name</string>
<key>CFBundleIconFile</key>
<string>icon</string>
<key>CFBundleIdentifier</key>
<string>$identifier</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$name</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSMinimumSystemVersion</key>
<string>$minimum</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
EOF

View file

@ -9,6 +9,7 @@
# target, bound to its Windows 3.x supervisor.
#
# owcc.sh cc ARGS... compile (-c), or compile and link a program
# owcc.sh as SOURCE OBJECT assemble (Watcom's wasm: MASM syntax, OMF)
# owcc.sh ar rcs LIB OBJ... make a library (Watcom's wlib)
#
# WIN16_WATCOM is the Open Watcom tree (make exports it from
@ -47,13 +48,18 @@ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
mode="$1"
shift
if [ "$mode" = "as" ]; then
# as SOURCE OBJECT, quietly. wasm writes no object for a source that
# draws a warning, so a warning fails the build here too.
exec wasm -q -fo="$2" "$1"
fi
if [ "$mode" = "ar" ]; then
# ar rcs LIB OBJ...: the library is made new each time (-n), quietly, with
# case-sensitive names and no .bak copy.
shift
exec wlib -q -n -b -c "$@"
fi
[ "$mode" = "cc" ] || { echo "owcc.sh: cc or ar, not '$mode'" >&2; exit 2; }
[ "$mode" = "cc" ] || { echo "owcc.sh: cc, as or ar, not '$mode'" >&2; exit 2; }
flags=(-bwin386 -fpc -I"$WATCOM/h" -include "$here/noAttributes.h" -Wc,-wcd=202)
sources=()
@ -127,7 +133,10 @@ done
if [ "$depFile" = 1 ]; then
phonyHeaders "$deps"
fi
owcc -bwin386 -o "$work/$name.rex" "${objects[@]}" "${libraries[@]}" "-Wl,option stack=$STACK_BYTES"
# noredefsok: a name the program and the library both define is an error,
# as on every other port; Watcom's linker otherwise warns and keeps the
# first, so the program's variable would silently stand in for JoeyLib's.
owcc -bwin386 -o "$work/$name.rex" "${objects[@]}" "${libraries[@]}" "-Wl,option stack=$STACK_BYTES" "-Wl,option noredefsok"
if [ -n "$resources" ]; then
(cd "$work" && wbind "$name" -q -s "$WATCOM/binw/win386.ext" -R -q -bt=windows "$resources" "$name.exe")
else

View file

@ -53,17 +53,17 @@ $(BUILD_DIR)/spritebake-dos: $(SPRITEBAKE_SRC) $(SRC_X86)/spriteEmitX86.c $(SPRI
@mkdir -p $(dir $@)
$(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_DOS $(SPRITEBAKE_INC) -I$(SRC_X86) $(filter %.c,$^) -o $@
$(BUILD_DIR)/spritebake-amiga: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.c $(SPRITEBAKE_HDRS)
$(BUILD_DIR)/spritebake-amiga: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.c $(SRC_68K)/spriteC2p.c $(SPRITEBAKE_HDRS)
@mkdir -p $(dir $@)
$(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_AMIGA $(SPRITEBAKE_INC) -I$(SRC_DIR)/amiga -I$(SRC_68K) $(filter %.c,$^) -o $@
# The X68000 runs the Amiga's planar 68k emitter unchanged (see
# make/x68000.mk), so its bake is the same source under its own target byte.
$(BUILD_DIR)/spritebake-x68000: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.c $(SPRITEBAKE_HDRS)
$(BUILD_DIR)/spritebake-x68000: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitPlanar68k.c $(SRC_68K)/spriteC2p.c $(SPRITEBAKE_HDRS)
@mkdir -p $(dir $@)
$(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_X68000 $(SPRITEBAKE_INC) -I$(SRC_DIR)/x68000 -I$(SRC_68K) $(filter %.c,$^) -o $@
$(BUILD_DIR)/spritebake-atarist: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitInterleaved68k.c $(SPRITEBAKE_HDRS)
$(BUILD_DIR)/spritebake-atarist: $(SPRITEBAKE_SRC) $(SRC_68K)/spriteEmitInterleaved68k.c $(SRC_68K)/spriteC2p.c $(SPRITEBAKE_HDRS)
@mkdir -p $(dir $@)
$(HOST_CC) $(HOST_CFLAGS) -DJOEYLIB_PLATFORM_ATARIST $(SPRITEBAKE_INC) -I$(SRC_DIR)/atarist -I$(SRC_68K) $(filter %.c,$^) -o $@

View file

@ -27,6 +27,7 @@ $(call requireTool,$(WIN16_WATCOM)/binl64/owcc,toolchain:win16)
WIN16_OWCC := $(REPO_DIR)/make/support/win16/owcc.sh
HOSTED_CC := $(WIN16_OWCC) cc
HOSTED_AS := $(WIN16_OWCC) as
HOSTED_AR := $(WIN16_OWCC) ar
HOSTED_BACKEND := win16
HOSTED_DEFINES := -DJOEYLIB_PLATFORM_HOSTED -DJOEYLIB_HOSTED_WIN16 -DHOSTED_FRAME_INDEXED -DHOSTED_AUDIO_RATE=22050u -DHOSTED_AUDIO_CHANNELS=1u
@ -49,4 +50,8 @@ include $(REPO_DIR)/make/hosted.mk
# (scripts/verify-win16-display.sh).
ifndef JOEY_PROJECT
all $(PLATFORM): $(call programTarget,UBERPROB)
# The serial throughput gate's program (scripts/verify-win16-serial-fast.sh).
.PHONY: win16-serfast
win16-serfast: $(call programTarget,SERFAST)
endif

View file

@ -55,13 +55,18 @@ LIB_OBJS := \
$(patsubst $(SRC_DIR)/x68000/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \
$(patsubst $(SRC_DIR)/x68000/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \
$(BUILD)/obj/68k/planarFill.o \
$(BUILD)/obj/68k/planarShape.o \
$(BUILD)/obj/68k/planarPlot.o \
$(BUILD)/obj/68k/stageBands.o \
$(BUILD)/obj/68k/modMix.o \
$(BUILD)/obj/68k/sfxMix.o \
$(BUILD)/obj/68k/planarSpriteWalk.o \
$(BUILD)/obj/68k/planarLayerBlit.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
@ -81,9 +86,11 @@ $(BUILD)/obj/port/%.o: $(SRC_DIR)/x68000/%.s
@mkdir -p $(dir $@)
$(X68K_CC) $(CFLAGS) -c $< -o $@
# The assembler finds src/m68k/m68kCSym.inc through -I: the gcc driver hands
# it no include path.
$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s
@mkdir -p $(dir $@)
$(X68K_CC) $(CFLAGS) -c $< -o $@
$(X68K_CC) $(CFLAGS) -Wa,-I$(SRC_68K) -c $< -o $@
$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.c
@mkdir -p $(dir $@)

View file

@ -14,6 +14,9 @@
#
# scripts/bench-amiga.sh # default 150s timeout
# TIMEOUT=240 scripts/bench-amiga.sh # override
# BENCH_PROG=Uberprob BENCH_BIN_DIR=build/amiga/probe scripts/bench-amiga.sh
# # another build of UBER (the probe:
# # scripts/verify-amiga-display.sh)
#
# Real Kickstart (installer component `kickstart`) is
# required -- the HAL drives OCS directly and AROS is unreliable for it.
@ -25,25 +28,29 @@ source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed FSUAE fsuae
source "$repo/scripts/lib/amigaGate.sh"
amigaMachine
bin_dir=$repo/build/amiga/bin
bin_dir=${BENCH_BIN_DIR:-$repo/build/amiga/bin}
prog=${BENCH_PROG:-Uber}
timeout_s=${TIMEOUT:-150}
if [[ ! -f $bin_dir/Uber ]]; then
echo "Uber not built. Run 'make amiga' first." >&2
if [[ ! -f $bin_dir/$prog ]]; then
echo "$prog not built in $bin_dir. Run 'make amiga' first." >&2
exit 1
fi
work=$(mktemp -d -t joeylib-amiga-bench.XXXXXX)
mkdir -p "$work/s"
find "$bin_dir" -maxdepth 1 -type f -executable -exec cp -t "$work/" {} +
# A probe build's folder has no DATA of its own: UBER's is the main build's.
if [[ -d "$bin_dir/DATA" ]]; then
cp -r "$bin_dir/DATA" "$work/"
elif [[ -d "$repo/build/amiga/bin/DATA" ]]; then
cp -r "$repo/build/amiga/bin/DATA" "$work/"
fi
# ':' anchors to the boot volume root. 32 KB task stack: the shell
# default (~4 KB) left UBER one deep call chain from overflow -- the
# Phase-1 sprite-compile path found it (emitter accumulator frame +
# vsnprintf underneath = nondeterministic wandering crashes).
printf 'stack 32768\n:Uber\n' > "$work/s/startup-sequence"
printf 'stack 32768\n:%s\n' "$prog" > "$work/s/startup-sequence"
log=$work/joeylog.txt

View file

@ -28,6 +28,8 @@
# scripts/bench-atarist.sh ste # the STe build on an STe
# scripts/bench-atarist.sh megaste # the STe build on a Mega STe
# TIMEOUT=120 scripts/bench-atarist.sh # override
# BENCH_PRG / BENCH_BIN_DIR # another program and its folder
# # (UBERPROB, verify-atarist-display.sh)
#
# NOTE: requires the ST supervisor-mode fix in src/atarist/hal.c
# (jlpInit Super(0L)); without it UBER bus-errors on the first present.
@ -44,13 +46,14 @@ case $machine in
ste|megaste) build=ste ;;
*) echo "usage: scripts/bench-atarist.sh [st|megast|ste|megaste]" >&2; exit 2 ;;
esac
bin_dir=$repo/build/$build/bin
bin_dir=${BENCH_BIN_DIR:-$repo/build/$build/bin}
prg=${BENCH_PRG:-UBER.PRG}
tos=$EMUTOS_IMG
log=$bin_dir/joeylog.txt
timeout_s=${TIMEOUT:-90}
if [[ ! -f $bin_dir/UBER.PRG ]]; then
echo "UBER.PRG not built. Run 'make $build' first." >&2
if [[ ! -f $bin_dir/$prg ]]; then
echo "$prg not built. Run 'make $build' first." >&2
exit 1
fi
if [[ ! -f $tos ]]; then
@ -71,7 +74,7 @@ cleanup() {
}
trap cleanup EXIT
echo "bench-atarist: running the $build build's UBER headless on a $machine (timeout ${timeout_s}s)..." >&2
echo "bench-atarist: running the $build build's $prg headless on a $machine (timeout ${timeout_s}s)..." >&2
DISPLAY=$display "$HATARI" \
--machine "$machine" \
--tos "$tos" \
@ -81,7 +84,7 @@ DISPLAY=$display "$HATARI" \
--sound off \
--fast-forward on \
--frameskips 4 \
--auto "C:\\UBER.PRG" >/tmp/bench-atarist-hatari.log 2>&1 &
--auto "C:\\$prg" >/tmp/bench-atarist-hatari.log 2>&1 &
hat_pid=$!
# Poll for the completion marker UBER flushes just before its key wait.

View file

@ -8,7 +8,7 @@
# Stages a scratch DVX (scripts/lib/dvxGate.sh) whose desktop is UBER, boots
# it in DOSBox Staging under Xvfb, and waits for UBER's "press any key to
# exit" line in its joeylog.txt -- which a DVX program writes to its settings
# folder, CONFIG\APPS\JOEYLIB\UBER on the staged drive. The log is copied to
# folder, CONFIG\APPS\<VENDOR>\UBER on the staged drive. The log is copied to
# build/dvx/bin/joeylog.txt (tools/diff-uber-hashes and tools/uber-perf-table
# read it from there) and DOSBox is stopped.
#

View file

@ -27,6 +27,8 @@
# for hundreds of frames rather than one.
#
# bash scripts/bench-x68000.sh
# X68K_BENCH_CFLAGS="-DUBER_PROBE -DUBER_FRAMES=1" UBER built with more
# flags: its probe rows (scripts/verify-x68000-display.sh)
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
@ -52,7 +54,8 @@ trap 'rm -rf "$work"' EXIT
# stock ~900 KB of Human68k utilities in SYS/ BIN/ BASIC2/ ... still allocated
# and only ~330 KB usable. `xdftool empty` frees the whole 1221-cluster disk,
# so the size pressure is gone -- the strip is now just tidiness.)
m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \
# shellcheck disable=SC2086 # X68K_BENCH_CFLAGS is a list of flags
m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer ${X68K_BENCH_CFLAGS:-} \
-DJOEYLIB_PLATFORM_X68000 \
-I"$repo/include" -I"$repo/src/core" -I"$repo/src/x68000" \
"$repo/examples/uber/uber.c" \
@ -67,7 +70,7 @@ x68kDataDisk "$work/data.xdf" || exit 1
x68kXdf add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1
# Completion-driven exit. UBER's last act before jlWaitForAnyKey is
# jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM
# jlSurfaceClear(jlgStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM
# is the "hashes are computed and flushed" signal. The benchmark's own
# jlSurfaceClear/jlStagePresent ops can paint that colour for an instant, so the
# reading only counts once it has held for GREEN_STABLE frames -- the finished

View file

@ -54,7 +54,7 @@ for o in glob.glob(sys.argv[1]+"/*.o"):
if len(p)>=3 and p[1] in "TDBRtdbr": defs.add(p[2])
elif len(p)>=3 and p[1]=="U": und.add(p[2])
elif len(p)==2 and p[0]=="U": und.add(p[1])
print(" ".join(sorted(s for s in und-defs if s.startswith(("jlp","jlSprite","spriteCompiledDraw")))))
print(" ".join(sorted(s for s in und-defs if s.startswith(("jlp","jlSprite","jlSpriteCompiledDraw")))))
PY
)
if [ -n "$missing" ]; then

View file

@ -4,7 +4,7 @@
# SPDX-License-Identifier: MIT
#
# check-dirtymark.sh - Host-cc gate for the stage's dirty-band writers: the
# multi-row mark (surfaceMarkDirtyRows, whose four-row groups reuse a group's
# multi-row mark (jlSurfaceMarkDirtyRows, whose four-row groups reuse a group's
# result for the next group that held the same bytes) and the whole-stage
# fills, held byte for byte to the plain per-row widen over randomized band
# states and marks of every height and phase (tests/host/dirtyMarkHost.c).

View file

@ -13,7 +13,8 @@
# win32 a group icon, number HOSTED_ICON_ID, among its resources
# (llvm-readobj)
# win16 the same in its NE resource table
# sdl3 the window icon linked in (gHostedIconRgba)
# sdl3 the window icon linked in (jlgHostedIconRgba)
# macos the app's icon: Info.plist naming an ICNS file in its Resources
# web icon.png beside the page, the page naming it its icon
# psp the EBOOT's ICON0, the baked PNG, 144x80
# haiku, haiku32, beos BEOS:L:STD_ICON and BEOS:M:STD_ICON resources
@ -47,7 +48,7 @@ makeValue() {
make -s -f "$repo/make/$1.mk" --eval "valueOf: ; @echo \$($2)" valueOf 2> /dev/null
}
for port in amiga win32 win16 sdl3 web psp haiku haiku32 beos dvx iigs; do
for port in amiga win32 win16 sdl3 macos web psp haiku haiku32 beos dvx iigs; do
program=$(joeyExampleProgram draw "$port" 2> /dev/null) || continue
[ -f "$program" ] || continue
checked="${checked:+$checked }$port"
@ -106,7 +107,21 @@ PY
;;
sdl3)
symbols=$(nm "$program" 2> /dev/null)
grep -q ' gHostedIconRgba$' <<< "$symbols" || fail sdl3 "DRAW links no window icon"
grep -q ' jlgHostedIconRgba$' <<< "$symbols" || fail sdl3 "DRAW links no window icon"
;;
macos)
python3 - "$(dirname "$(dirname "$(dirname "$program")")")" <<'PY' || fail macos "DRAW.app's icon"
import plistlib
import sys
from PIL import Image
app = sys.argv[1]
with open(app + "/Contents/Info.plist", "rb") as f:
info = plistlib.load(f)
icon = Image.open("%s/Contents/Resources/%s.icns" % (app, info["CFBundleIconFile"]))
sys.exit(icon.format != "ICNS")
PY
;;
web)
folder=$(dirname "$program")

98
scripts/check-macos-apps.sh Executable file
View file

@ -0,0 +1,98 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# check-macos-apps.sh - Host gate for the macOS port's apps (make macos),
# which nothing here can run: every app in build/macos/bin is one macOS can
# open. Its Info.plist parses and names, as an application for the macOS
# make/support/macos/cc.sh builds for, a program that is in Contents/MacOS
# and an icon that is in Contents/Resources. The program is universal: an
# Apple silicon (arm64) and an Intel (x86_64) program, each built for that
# macOS and linking nothing but the system's own libraries, the Apple silicon
# one signed (zig signs it ad hoc as it links; macOS will not run an
# unsigned arm64 program at all). Read with LLVM's Mach-O tools.
#
# Usage: scripts/check-macos-apps.sh
# Exit: 0 PASS, 1 FAIL, 2 SKIP (the port not built, or no LLVM tools)
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
label=check-macos-apps
joeyNeed MACOS_LIPO toolchain:macos
objdump="$(dirname "$MACOS_LIPO")/llvm-objdump"
joeyNeedFile "$objdump" toolchain:macos
minimum=$("$repo/make/support/macos/cc.sh" --minimum)
bin="$repo/build/macos/bin"
if ! compgen -G "$bin/*.app" > /dev/null; then
echo "$label: SKIP - no app in $bin (make macos)"
exit 2
fi
python3 - "$bin" "$MACOS_LIPO" "$objdump" "$minimum" "$label" <<'PY'
import glob
import os
import plistlib
import subprocess
import sys
bin, lipo, objdump, minimum, label = sys.argv[1:]
ARCHS = {"arm64", "x86_64"}
SIGNED = {"arm64"}
SYSTEM = ("/System/Library/", "/usr/lib/")
def run(*args):
return subprocess.run(args, capture_output=True, text=True, check=True).stdout
def sliceProblems(program, arch):
problems = []
headers = run(objdump, "--macho", "--private-headers", "--arch=" + arch, program).split()
if "LC_BUILD_VERSION" not in headers or headers[headers.index("minos") + 1] != minimum:
problems.append("%s not built for macOS %s" % (arch, minimum))
if arch in SIGNED and "LC_CODE_SIGNATURE" not in headers:
problems.append("%s unsigned" % arch)
for line in run(objdump, "--macho", "--dylibs-used", "--arch=" + arch, program).splitlines()[1:]:
library = line.split()[0]
if not library.startswith(SYSTEM):
problems.append("%s links %s" % (arch, library))
return problems
def appProblems(app):
contents = os.path.join(app, "Contents")
with open(os.path.join(contents, "Info.plist"), "rb") as f:
info = plistlib.load(f)
problems = []
if info.get("CFBundlePackageType") != "APPL" or info.get("LSMinimumSystemVersion") != minimum:
problems.append("Info.plist is not an application for macOS %s" % minimum)
if not os.path.isfile(os.path.join(contents, "Resources", info.get("CFBundleIconFile", "") + ".icns")):
problems.append("no icon")
program = os.path.join(contents, "MacOS", info.get("CFBundleExecutable", ""))
if not (os.path.isfile(program) and os.access(program, os.X_OK)):
return problems + ["no program"]
archs = set(run(lipo, "-archs", program).split())
if archs != ARCHS:
problems.append("holds %s, not %s" % (" ".join(sorted(archs)), " ".join(sorted(ARCHS))))
for arch in sorted(archs & ARCHS):
problems += sliceProblems(program, arch)
return problems
apps = sorted(glob.glob(os.path.join(bin, "*.app")))
fails = []
for app in apps:
try:
problems = appProblems(app)
except (OSError, ValueError, KeyError, plistlib.InvalidFileException, subprocess.CalledProcessError) as error:
problems = [str(error)]
if problems:
fails.append("%s: %s" % (os.path.basename(app), ", ".join(problems)))
if fails:
print("%s: FAIL - %s" % (label, "; ".join(fails)))
sys.exit(1)
print("%s: PASS (%d app%s: universal %s, arm64 signed, macOS %s on, system libraries only)" % (label, len(apps), "" if len(apps) == 1 else "s", "+".join(sorted(ARCHS)), minimum))
PY

34
scripts/check-tiled.sh Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# check-tiled.sh - Host gate for tools/tiled2joey.py, which turns a map
# made in Tiled into a tile sheet (baked into tile banks by assetbake.py) and
# one jlTileMapPaste map per tile layer (docs/Assets.adoc). tests/host/
# tiledCheck.py writes one map as TMX and as TMJ, with every flip Tiled can
# ask for, two tilesets (one external, with a margin and spacing), and every
# layer encoding the tool reads; both must convert to the sheet and maps
# worked out by hand, the sheet must bake, and maps it cannot take (16x16
# tiles, an RGB tileset) must be refused.
#
# Usage: scripts/check-tiled.sh
# Exit: 0 PASS, 1 FAIL
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
label=check-tiled
work=$(mktemp -d -t joeylib-tiled.XXXXXX)
trap 'rm -rf "$work"' EXIT
out=$(python3 "$repo/tests/host/tiledCheck.py" "$repo" "$work")
status=$?
echo "$out"
checks=$(grep -c '^TILED:' <<< "$out")
if [ "$status" -ne 0 ]; then
echo "$label: FAIL ($(grep -c 'FAIL' <<< "$out") of $checks checks failed)"
exit 1
fi
echo "$label: PASS ($checks checks: TMX and TMJ, every flip, two tilesets, three layer encodings, the bake, two refusals)"
exit 0

32
scripts/check-tilelayer.sh Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# check-tilelayer.sh - Host gate for tile layers: the BLANK port built with
# the host cc, and tests/host/tileLayerHost.c checking every draw of 300 runs
# of random moves, bands, maps (some smaller than the view), and cell changes
# pixel for pixel against the map itself, plus the band's dirty marks, the
# rows outside it, and jlTileLayerCreate's refusals.
#
# Usage: scripts/check-tilelayer.sh
# Exit: 0 PASS, 1 FAIL
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cc=${CC:-cc}
work=$(mktemp -d -t joeylib-tilelayer.XXXXXX)
trap 'rm -rf "$work"' EXIT
cd "$repo"
"$cc" -O2 -DJOEYLIB_PLATFORM_BLANK \
-Iinclude -Iinclude/joey -Isrc/core -Isrc/codegen -Wall -Wextra -Werror \
src/core/*.c src/generic/*.c src/blank/blank.c tests/host/tileLayerHost.c \
-lm -o "$work/tileLayerHost"
if "$work/tileLayerHost"; then
echo "check-tilelayer: PASS (every draw matches the map pixel for pixel)"
else
echo "check-tilelayer: FAIL"
exit 1
fi

View file

@ -10,16 +10,16 @@
# one of them the desktop, so DVX boots straight into it; and the DOSBox
# Staging run of that folder as drive C:. Nothing here writes to the DVX
# project or to toolchains/opt/dvx. Source after joeyEnv.sh. A project built
# on make/project.mk sets DVX_BIN (and DVX_VENDOR, if it changed it) to its
# own before sourcing.
# on make/project.mk sets DVX_BIN and DVX_VENDOR to its own before sourcing.
DVX_GATE="$(basename "$0" .sh)"
DVX_BIN="${DVX_BIN:-$JOEY_REPO/build/dvx/bin}"
# UBER's probe build (make dvx-probe), packaged apart from bin/.
DVX_PROBE_DIR="${DVX_PROBE_DIR:-$JOEY_REPO/build/dvx/probe}"
# The folder a program installs in under apps/ (make/dvx.mk's DVX_VENDOR), and
# the name of its log.
DVX_VENDOR="${DVX_VENDOR:-joeylib}"
# The folder a program installs in under apps/ (make/dvx.mk's DVX_VENDOR; for
# JoeyLib's examples make/examples.mk's, asked of make), and the name of its
# log.
DVX_VENDOR="${DVX_VENDOR:-$(make -s -f "$JOEY_REPO/make/dvx.mk" --eval 'dvxVendor: ; @echo $(DVX_VENDOR)' dvxVendor 2> /dev/null)}"
DVX_LOG_NAME=joeylog.txt
# The port's own settings file in a program's settings folder, and its
# full-screen and square-pixels switches (src/dvx/hal.c).
@ -68,7 +68,7 @@ dvxNeedBuild() {
# dvxStage DIR APPDIR... -- stage DVX in DIR/DVX with the programs in the
# APPDIRs (each a folder holding NAME.app, from dvxAppDir or anywhere else),
# installed as apps/joeylib/NAME. The first one is the desktop. DIR may hold
# installed as apps/VENDOR/NAME. The first one is the desktop. DIR may hold
# an earlier staging: the runtime and the programs are copied over it, and
# what DVX wrote there (its settings, the programs' CONFIG folders) stays.
dvxStage() {

View file

@ -65,6 +65,11 @@ joeyExampleProgram() {
printf '%s\n' "$bin/$up/EBOOT.PBP"
return 0
;;
macos)
# Every program is an app, the program inside it.
printf '%s\n' "$bin/$up.app/Contents/MacOS/$up"
return 0
;;
amiga)
# Mixed-case names.
up="${name,,}"
@ -81,11 +86,11 @@ joeyExampleProgram() {
# apps/<vendor>/<name>/, whatever the vendor.
up="${up,,}"
candidate="$(find "$bin/apps" -mindepth 3 -maxdepth 3 -type f -path "*/$up/$up.app" -print -quit 2>/dev/null)"
printf '%s\n' "${candidate:-$bin/apps/joeylib/$up/$up.app}"
printf '%s\n' "${candidate:-$bin/apps/VENDOR/$up/$up.app}"
return 0
;;
*)
echo "$(basename "$0"): unknown port '$2' (iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, win32, win16, psp, haiku, haiku32 or beos)" >&2
echo "$(basename "$0"): unknown port '$2' (iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, macos, win32, win16, psp, haiku, haiku32 or beos)" >&2
return 2
;;
esac

View file

@ -25,6 +25,13 @@ CLOSE FAIL line more, and keeps the pty pair open until FILE exists or WALL
runs out, so the gate can read the guest end's settings after the close
(verify-sdl3-serial.sh).
--stream N --rate RATE (TCP, nullmodem transparent:1) then streams N bytes of printable
ASCII through SERTEST's echo, at most STREAM_WINDOW bytes ahead of what has
come back, and checks every byte returns once, in order. Prints one STREAM
PASS / STREAM FAIL line more, with the round trip's bytes a second; PASS
needs that to be at least STREAM_MIN_PERCENT of what the line rate RATE
(bits a second) carries.
--dtr-check (TCP, with DOSBox's nullmodem in transparent:0 mode, which puts
the guest's modem lines in the byte stream as they change: 0xFF 0xFF is a
data 0xFF, 0xFF <b> the lines, bit 0 RTS, bit 1 DTR) then runs SERTEST's
@ -52,6 +59,17 @@ CONNECT_RETRY_SECONDS = 0.5
READ_SIZE = 256
SHOWN_BYTES = 80
# --stream: what the window, the chunks, and the pass mark are, and the bytes
# it sends (printable ASCII, so never SERTEST's DTR_CHECK).
STREAM_WINDOW = 1024
STREAM_CHUNK = 64
STREAM_POLL_SECONDS = 0.01
STREAM_SETTLE_SECONDS = 1.0
STREAM_MIN_PERCENT = 75
STREAM_FIRST_BYTE = 0x20
STREAM_BYTE_VALUES = 95
LINE_BITS_PER_BYTE = 10 # 8N1: a start bit, 8 data bits, a stop bit
# SERTEST's DTR check (examples/sertest/sertest.c).
DTR_CHECK = b"\x04"
DTR_ACK = b"\x06"
@ -239,15 +257,59 @@ def loop(send, receive, wall, start, banner):
return 1, got
def stream(send, receive, wall, start, total, rate):
# The probe's last echoes go first, then the stream and its echo.
pattern = bytes(STREAM_FIRST_BYTE + (k % STREAM_BYTE_VALUES) for k in range(total))
settle = time.time() + STREAM_SETTLE_SECONDS
while time.time() < settle:
if receive() is None:
print("STREAM FAIL: the connection closed before the stream")
return 1
sent = 0
got = b""
began = time.time()
while len(got) < total and time.time() - start < wall:
if sent < total and sent - len(got) < STREAM_WINDOW:
chunk = pattern[sent:sent + STREAM_CHUNK]
send(chunk)
sent += len(chunk)
data = receive()
if data is None:
break
got += data
elapsed = time.time() - began
perSecond = len(got) / elapsed if elapsed > 0 else 0.0
floor = rate / LINE_BITS_PER_BYTE * STREAM_MIN_PERCENT / 100.0
if got != pattern:
at = next((k for k in range(min(len(got), total)) if got[k] != pattern[k]), min(len(got), total))
print("STREAM FAIL: %d of %d bytes back, the first wrong at %d; %r" % (len(got), total, at, got[at:at + 16]))
return 1
if perSecond < floor:
print("STREAM FAIL: %d bytes back in order, %.0f a second, under %.0f (%d%% of %d baud)" % (total, perSecond, floor, STREAM_MIN_PERCENT, rate))
return 1
print("STREAM PASS: %d bytes back in order, none lost, %.0f a second (%d baud carries %d)" % (total, perSecond, rate, rate // LINE_BITS_PER_BYTE))
return 0
def main(argv):
withDtrCheck = "--dtr-check" in argv
argv = [arg for arg in argv if arg != "--dtr-check"]
streamBytes = 0
rate = 0
if "--stream" in argv and argv.index("--stream") + 1 < len(argv):
at = argv.index("--stream")
streamBytes = int(argv[at + 1])
argv = argv[:at] + argv[at + 2:]
if "--rate" in argv and argv.index("--rate") + 1 < len(argv):
at = argv.index("--rate")
rate = int(argv[at + 1])
argv = argv[:at] + argv[at + 2:]
holdFile = None
if "--close-hold" in argv and argv.index("--close-hold") + 1 < len(argv):
at = argv.index("--close-hold")
holdFile = argv[at + 1]
argv = argv[:at] + argv[at + 2:]
if len(argv) < 4 or argv[1] not in ("tcp-server", "tcp-client", "pty") or (withDtrCheck and argv[1] == "pty") or (holdFile is not None and argv[1] != "pty"):
if len(argv) < 4 or argv[1] not in ("tcp-server", "tcp-client", "pty") or (withDtrCheck and argv[1] == "pty") or (holdFile is not None and argv[1] != "pty") or (streamBytes > 0 and (argv[1] == "pty" or withDtrCheck or rate <= 0)):
sys.stderr.write(__doc__)
return 2
mode = argv[1]
@ -294,7 +356,12 @@ def main(argv):
return lines.feed(data) if withDtrCheck else data
status, got = loop(conn.sendall, receive, wall, start, banner)
if status != 0 or not withDtrCheck:
if status != 0:
return status
if streamBytes > 0:
conn.settimeout(STREAM_POLL_SECONDS)
return stream(conn.sendall, receive, wall, start, streamBytes, rate)
if not withDtrCheck:
return status
return dtrCheck(conn.sendall, receive, wall, start, lines, got)

View file

@ -56,7 +56,7 @@ VOLSIZE="${JOEY_DISK_SIZE:-800KB}"
# Priority order: small primitive demos first so DRAW/PATTERN/etc. always fit;
# the larger adventures land last and are dropped if the floppy is full.
order=(${JOEY_DISK_EXAMPLES:-DRAW PATTERN KEYS JOY SPRITE UBER ADV2 ADV})
order=(${JOEY_DISK_EXAMPLES:-DRAW PATTERN KEYS JOY SPRITE UBER SCROLL ADV2 ADV})
work=$(mktemp -d -t joeylib-disk.XXXXXX)
trap 'rm -rf "$work"' EXIT

234
scripts/profile-iigs.sh Executable file
View file

@ -0,0 +1,234 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# profile-iigs.sh - PC-sampling profiler for a JoeyLib program on the IIgs.
#
# Boots a self-booting program volume (make-iigs-game-disk.sh's) under
# headless MAME and samples the 65816's program bank and counter every
# PROFILE_PERIOD_US of EMULATED time, from a Lua coroutine that emu.wait()s
# between samples. A per-frame sample would land at the same point of a
# frame-synced game loop every time; a period that is not a multiple of the
# frame's spreads the samples over the whole frame.
#
# The OMF carries no symbols and the GS/OS Loader puts each bank-aligned
# segment in whatever bank it likes, so the samples are tied back to the
# link map by the segments' BYTES: at the end the script reads the start of
# every bank a sample landed in and scores it against every segment image
# (the Loader's relocations change only a few bytes). A bank that matches
# no segment is GS/OS, a tool, NTP or the ROM, and is reported by bank.
#
# The map comes from building the program with JOEY_LINK_MAP=<dir>, which
# keeps <dir>/NAME.map, NAME.seg<N>.bin and NAME.json (clang-build.sh):
#
# rm -f build/iigs/bin/STAXI && JOEY_LINK_MAP=/some/dir make iigs
#
# The map lists static functions too (JOEY_LINK_MAP links with link816's
# --map-locals), but one the compiler inlined has no code of its own: its
# time is its caller's row.
#
# Usage: scripts/profile-iigs.sh <image> <map> [seconds] [skipSeconds]
# image the program's boot volume: .po (800KB, the 3.5
# drive) or .2mg (anything larger, a CFFA2 card)
# map NAME.map, with NAME.seg<N>.bin beside it
# seconds emulated seconds to run, default 120
# skipSeconds ignore samples before this, default 40 (boot)
# Env: PROFILE_PERIOD_US sample period in emulated microseconds,
# default 997
# PROFILE_TOP rows to print, default 40
# PROFILE_RAMSIZE MAME -ramsize, default 4M
# PROFILE_SAMPLES a file to keep the raw samples in (S addr
# count, F bank bytes, T total), for a look
# inside a bank no segment matched
# Requires: ./install.sh install iigs.
set -euo pipefail
img=${1:?usage: profile-iigs.sh <image> <map> [seconds] [skipSeconds]}
map=${2:?usage: profile-iigs.sh <image> <map> [seconds] [skipSeconds]}
seconds=${3:-120}
skip=${4:-40}
periodUs=${PROFILE_PERIOD_US:-997}
top=${PROFILE_TOP:-40}
ramsize=${PROFILE_RAMSIZE:-4M}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame
joeyNeed IIGS_ROMPATH iigsFirmware
[ -f "$img" ] || { echo "profile-iigs: no such image $img" >&2; exit 2; }
[ -f "$map" ] || { echo "profile-iigs: no such map $map" >&2; exit 2; }
[ -f "${map%.map}.seg1.bin" ] || { echo "profile-iigs: ${map%.map}.seg1.bin missing (build with JOEY_LINK_MAP=<dir>)" >&2; exit 2; }
# MAME opens a hard-disk image read-write: run a private copy.
work=$(mktemp -d -t joeylib-iigsprof.XXXXXX)
trap 'rm -rf "$work"' EXIT
name=$(basename "$img")
cp "$img" "$work/$name"
if [ "$(stat -c %s "$img")" -le 900000 ]; then
media=(-flop3 "$work/$name")
else
media=(-sl7 cffa2 -hard1 "$work/$name")
fi
# Bytes of a bank compared against a segment's image.
fingerprintBytes=512
cat > "$work/prof.lua" <<LUA
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local out = io.open("samples.txt", "w")
local counts = {}
local banks = {}
local taken = 0
local function finish()
for addr, n in pairs(counts) do
out:write(string.format("S %06X %d\n", addr, n))
end
for bank, _ in pairs(banks) do
local bytes = {}
for i = 0, $fingerprintBytes - 1 do
bytes[#bytes + 1] = string.format("%02X", mem:read_u8((bank << 16) + i))
end
out:write(string.format("F %02X %s\n", bank, table.concat(bytes)))
end
out:write(string.format("T %d\n", taken))
out:close()
manager.machine:exit()
end
local sampler = coroutine.create(function()
while manager.machine.time:as_double() < $seconds do
emu.wait($periodUs / 1000000.0)
if manager.machine.time:as_double() >= $skip then
local pb = cpu.state["PB"].value
local addr = (pb << 16) | cpu.state["PC"].value
counts[addr] = (counts[addr] or 0) + 1
banks[pb] = true
taken = taken + 1
end
end
finish()
end)
local started = false
emu.register_frame_done(function()
if not started then
started = true
local ok, err = coroutine.resume(sampler)
if not ok then
io.write("PROFILE-IIGS lua error: " .. tostring(err) .. "\n")
manager.machine:exit()
end
end
end)
LUA
echo "profile-iigs: $name, ${seconds}s emulated (samples from ${skip}s, every ${periodUs} us)..." >&2
(cd "$work" && QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 3600 "$MAME" apple2gs \
-ramsize "$ramsize" -rompath "$IIGS_ROMPATH" "${media[@]}" \
-video none -sound none -nothrottle -skip_gameinfo \
-autoboot_script "$work/prof.lua" </dev/null >"$work/mame.log" 2>&1) || true
if [ ! -s "$work/samples.txt" ]; then
echo "profile-iigs: FAIL -- no samples (MAME output follows)" >&2
tail -15 "$work/mame.log" >&2
exit 1
fi
grep 'PROFILE-IIGS' "$work/mame.log" >&2 || true
if [ -n "${PROFILE_SAMPLES:-}" ]; then
cp "$work/samples.txt" "$PROFILE_SAMPLES"
fi
python3 - "$work/samples.txt" "$map" "$top" <<'PY'
import bisect
import json
import os
import re
import sys
samplesPath, mapPath, top = sys.argv[1], sys.argv[2], int(sys.argv[3])
mapBase = mapPath[:-4] if mapPath.endswith('.map') else mapPath
# The segments: number -> (link bank, image bytes).
segments = {}
manifest = mapBase + '.json'
if os.path.exists(manifest):
for seg in json.load(open(manifest))['segments']:
segments[seg['num']] = int(seg['base'], 16) >> 16
else:
segments[1] = 0
images = {num: open('%s.seg%d.bin' % (mapBase, num), 'rb').read() for num in segments}
# The symbols, by address: the globals, and the locals (static functions)
# when the map has them; a global wins an address both name.
byAddr = {}
section = None
for line in open(mapPath):
if line.startswith('#'):
section = 'global' if line.startswith('# global symbols') else ('local' if line.startswith('# local symbols') else None)
continue
m = re.match(r'^0x([0-9a-fA-F]{6})\s+(\S+)$', line.strip())
if m and section is not None:
addr = int(m.group(1), 16)
if section == 'global' or addr not in byAddr:
byAddr[addr] = m.group(2)
syms = sorted(byAddr.items())
symAddrs = [a for a, _ in syms]
counts = {}
fingerprints = {}
total = 0
for line in open(samplesPath):
parts = line.split()
if parts[0] == 'S':
counts[int(parts[1], 16)] = int(parts[2])
elif parts[0] == 'F':
fingerprints[int(parts[1], 16)] = bytes.fromhex(parts[2])
elif parts[0] == 'T':
total = int(parts[1])
# Runtime bank -> link bank, by the best-scoring segment image.
bankMap = {}
for bank, fp in fingerprints.items():
best = None
for num, image in images.items():
n = min(len(fp), len(image))
if n < 16:
continue
same = sum(1 for i in range(n) if fp[i] == image[i])
score = same / n
if best is None or score > best[0]:
best = (score, num)
if best is not None and best[0] >= 0.85:
bankMap[bank] = (segments[best[1]], best[1], best[0])
def where(addr):
bank = addr >> 16
if bank in bankMap:
link = (bankMap[bank][0] << 16) | (addr & 0xFFFF)
i = bisect.bisect_right(symAddrs, link) - 1
if i >= 0 and (symAddrs[i] >> 16) == (link >> 16):
return syms[i][1]
return 'seg%d+%04X' % (bankMap[bank][1], addr & 0xFFFF)
if bank >= 0xFC:
return 'ROM $%02X' % bank
if bank in (0xE0, 0xE1):
return 'bank $%02X (firmware/vectors)' % bank
return 'bank $%02X (GS/OS, tools, NTP)' % bank
byName = {}
for addr, n in counts.items():
key = where(addr)
byName[key] = byName.get(key, 0) + n
print('profile-iigs: %d samples' % total)
for bank in sorted(bankMap):
link, num, score = bankMap[bank]
print(' bank $%02X = segment %d (link bank $%02X, %.0f%% of %d bytes match)' % (bank, num, link, score * 100, len(fingerprints[bank])))
print('%7s %6s %s' % ('samples', 'pct', 'where'))
for key, n in sorted(byName.items(), key=lambda kv: -kv[1])[:top]:
print('%7d %5.1f%% %s' % (n, 100.0 * n / max(total, 1), key))
PY

View file

@ -16,12 +16,12 @@
# separates them is that Human68k RELOCATES the copy it runs: at a known
# relocation site the running image holds fileValue + base, while the disk
# buffer still holds fileValue. From then on
# it samples the PC once per emulated frame, buckets by offset into .text, and
# attributes the buckets to functions with nm.
#
# Sampling once per frame is coarse (55 Hz), but this port runs a game frame in
# 150 ms or more, so each frame still gets several samples and the shape of the
# profile is clear. Read it as "which functions dominate", never as a timing.
# it samples the PC every PROFILE_PERIOD_US of emulated time (a Lua coroutine
# that emu.wait()s between samples; a period that is not a multiple of the
# frame's spreads them over the whole frame, where a sample a frame landed on
# the same instructions every time), buckets by offset into .text, and
# attributes the buckets to functions with nm. Read it as "which functions
# dominate", never as a timing.
#
# Usage: scripts/profile-x68000.sh program [seconds] [skipSeconds]
# program the built .X: its path (a project's, say), or an
@ -29,12 +29,16 @@
# seconds emulated seconds to run, default 240
# skipSeconds ignore samples before this, default 130 (boot + menu)
# Env: X68K_DIR dir holding HUMAN302.XDF + roms/ (default: the installer's toolchains/firmware/x68000)
# PROFILE_PERIOD_US sample period in emulated microseconds, default 997
# PROFILE_SAMPLES a file to keep the raw samples in (offset count
# lines, -1 outside the program; the load base)
# Requires: the program built WITH symbols (<NAME>.X.elf beside it).
set -uo pipefail
prog=${1:?usage: profile-x68000.sh program [seconds] [skipSeconds]}
seconds=${2:-240}
skip=${3:-130}
periodUs=${PROFILE_PERIOD_US:-997}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
@ -66,9 +70,9 @@ o = 64 + $relocSite
print(int.from_bytes(d[o:o+4],'big'))
")
# gMouseX is written once per input poll, i.e. once per game frame.
mouseXOff=$(m68k-xelf-nm "$elf" | awk '$3 == "gMouseX" { print strtonum("0x" $1); exit }')
[[ -n ${mouseXOff:-} ]] || { echo "profile-x68000: gMouseX not found; cannot count frames" >&2; exit 2; }
# jlgMouseX is written once per input poll, i.e. once per game frame.
mouseXOff=$(m68k-xelf-nm "$elf" | awk '$3 == "jlgMouseX" { print strtonum("0x" $1); exit }')
[[ -n ${mouseXOff:-} ]] || { echo "profile-x68000: jlgMouseX not found; cannot count frames" >&2; exit 2; }
work=$(mktemp -d -t joey-x68prof.XXXXXX)
trap 'rm -rf "$work"' EXIT
@ -155,6 +159,46 @@ local function pickBase()
return nil
end
-- The samples come every PROFILE_PERIOD_US of emulated time, from a
-- coroutine that emu.wait()s between them, not once a frame: a frame-synced
-- game loop puts the same instructions under every frame's sample, so a
-- short routine came out as a few hot addresses or none at all.
local samplerStarted = false
local sampler = coroutine.create(function()
while true do
emu.wait($periodUs / 1000000.0)
local t = manager.machine.time:as_double()
if t >= skipUntil then
local pc = cpu.state[pcReg].value
local key
if pc >= base and pc < base + textSize then
-- The EXACT offset, not a 64-byte bucket. Buckets straddle
-- function boundaries, and a bucket credited to the symbol at
-- its start hands one function's samples to its neighbour:
-- that is how a vblank wait came out looking like an
-- expensive input poll.
key = pc - base
else
key = -1 -- ROM, IOCS, anything not ours
end
buckets[key] = (buckets[key] or 0) + 1
total = total + 1
end
if t >= stopAt then
out:write(string.format("fps %.2f frames %d window %.1f tapAt %08X\n",
(t > fpsStart) and (frames / (t - fpsStart)) or 0.0, frames, t - fpsStart, base + mouseXOff))
out:write(string.format("total %d\n", total))
for k, v in pairs(buckets) do
out:write(string.format("%d %d\n", k, v))
end
out:write("DONE\n")
out:close()
manager.machine:exit()
return
end
end
end)
emu.register_frame_done(function()
local t = manager.machine.time:as_double()
if base == nil then
@ -166,7 +210,7 @@ emu.register_frame_done(function()
if base ~= nil then
out:write(string.format("base %08X at %.1fs (of %d candidates)\n", base, t, #cands))
out:flush()
-- gMouseX is written once per jlpInputPoll, which the game calls
-- jlgMouseX is written once per jlpInputPoll, which the game calls
-- once a frame, so counting writes to it counts game frames. That
-- turns "how much of the frame is X" into "and how long is a
-- frame", which is the difference between a real speedup and one
@ -186,30 +230,14 @@ emu.register_frame_done(function()
scanDone = false
return
end
if t < skipUntil then return end
local pc = cpu.state[pcReg].value
local key
if pc >= base and pc < base + textSize then
-- The EXACT offset, not a 64-byte bucket. Buckets straddle function
-- boundaries, and a bucket credited to the symbol at its start hands
-- one function's samples to its neighbour: that is how a vblank wait
-- came out looking like an expensive input poll.
key = pc - base
else
key = -1 -- ROM, IOCS, anything not ours
end
buckets[key] = (buckets[key] or 0) + 1
total = total + 1
if t >= stopAt then
out:write(string.format("fps %.2f frames %d window %.1f tapAt %08X\n",
(t > fpsStart) and (frames / (t - fpsStart)) or 0.0, frames, t - fpsStart, base + mouseXOff))
out:write(string.format("total %d\n", total))
for k, v in pairs(buckets) do
out:write(string.format("%d %d\n", k, v))
if not samplerStarted then
samplerStarted = true
local ok, err = coroutine.resume(sampler)
if not ok then
out:write("sampler error: " .. tostring(err) .. "\n")
out:close()
manager.machine:exit()
end
out:write("DONE\n")
out:close()
manager.machine:exit()
end
end)
LUA
@ -222,6 +250,9 @@ LUA
[[ -s $work/prof.txt ]] || { echo "profile-x68000: FAIL (no samples)" >&2; exit 1; }
grep -q '^base ' "$work/prof.txt" || { echo "profile-x68000: FAIL (never found the load base)" >&2; exit 1; }
if [[ -n ${PROFILE_SAMPLES:-} ]]; then
cp "$work/prof.txt" "$PROFILE_SAMPLES"
fi
m68k-xelf-nm -n "$elf" > "$work/syms.txt"
python3 - "$work/prof.txt" "$work/syms.txt" <<'PY'

View file

@ -12,9 +12,10 @@
# template (blank), file roots, the dirty-band marker, the line
# and circle fallbacks at the far ends of their coordinates, input,
# millis, the module player the software-mixing ports share,
# save, serial, sfxmix, tilebank, and the VGM converter (with
# Furnace installed, on its demo songs too)
# golden UBER on each port, its 51 per-op surface hashes against
# save, serial, sfxmix, tilebank, tile layers against their maps,
# the Tiled map importer, and the VGM converter (with Furnace
# installed, on its demo songs too)
# golden UBER on each port, its 54 per-op surface hashes against
# tests/goldens/uber (x68000 against the IIgs's, its own gate;
# dvx, sdl3, web, win32, win16, psp, haiku, haiku32 and beos
# against the DOS port's, whose chunky stage they share; psp under
@ -31,6 +32,13 @@
# which build runs where -- the STe build refuses a plain ST and
# a Mega ST through jlLastError, the ST build runs on an STe
# visual iigs: every launchable example on joey.2mg boots and draws
# iigs: also the display -- UBER's probe build's runs of draws,
# each presented, the $E1 pixels then equal to the stage, every
# dirty band clean again, and the SCB and palette blocks the
# stage's (make iigs-verify-shrtail)
# x68000 and amiga: the display -- UBER's probe build's battery
# of draws, each presented, the page or planes shown checked
# against the stage's
# dvx: full screen -- UBER's hashes through a suspend and a
# resume, and KEYS's row read off the 320x200 VGA screen, then
# in its window, then on the whole screen again
@ -63,6 +71,12 @@
# the port's VM)
# (dvx: and the present shows it, decoded from the desktop, also
# at 8/15/32 bpp and 2x/3x; Ctrl+Esc must not end the program)
# (iigs: and its asm key predicates -- examples/keys lights A's
# cell only while A is held, jlKeyDown, and Escape quits it,
# jlKeyPressed)
# (dos: and the mouse through the driver's INT 33h event
# callback -- moves both ways, all four edges, every column, both
# buttons)
# joy (every port but psp, whose input gate covers it, and
# haiku, haiku32 and beos, as QEMU's PC has no joystick to give
# them; win16 through KPJOY.DRV, JoeyLib's own joystick driver for
@ -147,9 +161,14 @@
# modem port at 9600 and a Super Serial Card at 2400
# x68000: both serial streams byte-exact and the mouse right while
# the SCB changes palette on every line of a run (9600, 19200, 38400)
# disk iigs: a one-program boot volume (DRAW's) boots, launches the
# program and draws: as a 2048KB volume off a CFFA2 card, then as
# an 800KB floppy on ROM 3 and on ROM 01
# disk programs packaged to ship:
# iigs a one-program boot volume (DRAW's) boots, launches the
# program and draws: as a 2048KB volume off a CFFA2
# card, then as an 800KB floppy on ROM 3 and on ROM 01
# macos scripts/check-macos-apps.sh: every app is one macOS
# opens -- universal (Apple silicon and Intel), the Apple
# silicon half signed, for macOS 13 on, its Info.plist,
# program and icon in place (no Mac runs it here)
# icon a program's icon (DRAW's), where its system shows it:
# host scripts/check-icons.sh: every built port's DRAW carries
# its baked icon where the system looks (no emulator)
@ -191,8 +210,8 @@
#
# Usage: scripts/verify-all.sh KIND|all [port ...]
# with no ports, every port the kind has. Ports: host iigs
# x68000 atarist ste amiga dos dvx sdl3 web win32 win16 psp
# haiku haiku32 beos
# x68000 atarist ste amiga dos dvx sdl3 web macos win32 win16
# psp haiku haiku32 beos
# Env: VERIFY_ALL_LOG also append the full transcript to this file
# Requires: whatever each selected gate requires; missing ones SKIP.
set -uo pipefail
@ -203,7 +222,7 @@ shift || true
label="verify-all $kind"
KINDS="host golden visual tilebank input audio conform serial mouse save loss disk icon"
ALL_PORTS="host iigs x68000 atarist ste amiga dos dvx sdl3 web win32 win16 psp haiku haiku32 beos"
ALL_PORTS="host iigs x68000 atarist ste amiga dos dvx sdl3 web macos win32 win16 psp haiku haiku32 beos"
# UBER on the Amiga needs longer than bench-amiga.sh's interactive default.
AMIGA_UBER_SECONDS=400
@ -220,6 +239,8 @@ host host save scripts/check-save.sh
host host serial scripts/check-serial.sh
host host sfxmix scripts/check-sfxmix.sh
host host tilebank scripts/check-tilebank.sh
host host layer scripts/check-tilelayer.sh
host host tiled scripts/check-tiled.sh
host host vgm scripts/check-vgm.sh
golden iigs uber scripts/bench-iigs.sh && tools/diff-uber-hashes tests/goldens/uber/iigs.txt build/iigs/bin/joeylog.txt
golden x68000 uber scripts/verify-x68000-golden.sh
@ -242,6 +263,7 @@ golden haiku uber scripts/bench-be.sh haiku && tools/diff-uber-hashes te
golden haiku32 uber scripts/bench-be.sh haiku32 && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/haiku32/bin/joeylog.txt
golden beos uber scripts/bench-be.sh beos && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/beos/bin/joeylog.txt
visual iigs examples scripts/verify-iigs-all.sh
visual iigs display make -s -f make/iigs.mk iigs-verify-shrtail || exit 1
visual dvx full scripts/verify-dvx-fullscreen.sh
visual dvx display scripts/verify-dvx-display.sh
visual win16 display scripts/verify-win16-display.sh
@ -250,12 +272,16 @@ visual haiku display scripts/verify-be-display.sh haiku
visual haiku32 display scripts/verify-be-display.sh haiku32
visual beos display scripts/verify-be-display.sh beos
visual x68000 scb scripts/verify-x68000-scb.sh
visual x68000 display scripts/verify-x68000-display.sh
visual atarist palette scripts/verify-atarist-palette.sh st
visual ste palette scripts/verify-atarist-palette.sh ste megaste
visual atarist display scripts/verify-atarist-display.sh st
visual ste display scripts/verify-atarist-display.sh ste megaste
visual amiga palette scripts/verify-amiga-palette.sh
visual amiga a1200pal JOEY_AMIGA_MODEL=A1200 scripts/verify-amiga-palette.sh
visual amiga a4000pal JOEY_AMIGA_MODEL=A4000 scripts/verify-amiga-palette.sh
visual amiga fastram scripts/verify-amiga-fastram.sh
visual amiga display scripts/verify-amiga-display.sh
tilebank iigs tilebank make -s -f make/iigs.mk iigs-verify-tilebank || exit 1
tilebank x68000 tilebank scripts/verify-x68000-tilebank.sh
tilebank atarist tilebank scripts/verify-atarist-tilebank.sh
@ -268,6 +294,7 @@ tilebank win32 tilebank scripts/verify-win32-tilebank.sh
tilebank win16 tilebank scripts/verify-win16-tilebank.sh
tilebank psp tilebank scripts/verify-psp-tilebank.sh
input iigs input scripts/verify-iigs-input.sh
input iigs keys scripts/verify-iigs-keys.sh
input x68000 input scripts/verify-x68000-input.sh
input x68000 joy scripts/verify-x68000-joy.sh
input iigs joy scripts/verify-iigs-joy.sh
@ -277,6 +304,7 @@ input ste joy scripts/verify-atarist-joy.sh ste
input amiga input scripts/verify-amiga-input.sh
input amiga joy scripts/verify-amiga-joy.sh
input dos input scripts/verify-dos-input.sh
input dos mouse scripts/verify-dos-mouse.sh
input dos joy scripts/verify-dos-joy.sh
input dvx input scripts/verify-dvx-input.sh
input dvx depths scripts/verify-dvx-input.sh 640x480x8:1 800x600x15:2 1024x768x32:3
@ -329,6 +357,7 @@ serial dos serial scripts/verify-dos-serial.sh
serial dvx serial scripts/verify-dvx-serial.sh
serial win32 serial scripts/verify-win32-serial.sh
serial win16 serial scripts/verify-win16-serial.sh
serial win16 fast scripts/verify-win16-serial-fast.sh
serial sdl3 serial scripts/verify-sdl3-serial.sh
serial haiku serial scripts/verify-be-serial.sh haiku
serial haiku32 serial scripts/verify-be-serial.sh haiku32
@ -345,6 +374,7 @@ loss x68000 scb scripts/verify-x68000-serial-scb.sh
disk iigs cffa2 make -s -f make/iigs.mk iigs-verify-game-disk GAME=DRAW GAME_DISK_SIZE=2048KB || exit 1
disk iigs floppy make -s -f make/iigs.mk iigs-verify-game-disk GAME=DRAW || exit 1
disk iigs rom01 GAME_VERIFY_MACHINE=apple2gsr1 scripts/verify-iigs-game-disk.sh build/iigs/bin/DRAW.2mg
disk macos apps scripts/check-macos-apps.sh
icon host icons scripts/check-icons.sh
icon iigs finder scripts/verify-iigs-icon.sh
icon amiga icon scripts/verify-amiga-icon.sh

39
scripts/verify-amiga-display.sh Executable file
View file

@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# verify-amiga-display.sh - the Amiga's present, checked on the planes the
# copper shows. UBER's golden hashes are of the stage; a present that leaves
# stale rows on screen passes them. The display flips between two plane
# sets: each present draws into the one not shown, which holds the frame
# before last, from the union of two frames' dirty bands
# (src/amiga/presentScan.s). UBERPROB (UBER with UBER_PROBE,
# make/examples.mk) runs its Amiga display row (op_probePresentPageSum): a
# battery of draws, each presented and the shown planes summed against the
# stage's (jlAmigaProbePresentPageSums in src/amiga/hal.c). It must PASS.
#
# Usage: scripts/verify-amiga-display.sh
# Exit: 0 PASS, 1 FAIL
# Requires: FS-UAE and a Kickstart (install.sh install emulator:amiga); it
# builds the probe (make -f make/amiga.mk amiga-uberprob).
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
label=verify-amiga-display
dir=$repo/build/amiga/probe
if ! make -s -f "$repo/make/amiga.mk" amiga-uberprob; then
echo "$label: FAIL - building the UBERPROB failed" >&2
exit 1
fi
rm -f "$dir/joeylog.txt"
BENCH_PROG=Uberprob BENCH_BIN_DIR="$dir" TIMEOUT="${TIMEOUT:-300}" "$repo/scripts/bench-amiga.sh" > /dev/null 2>&1
line=$(grep "UBER-PROBE: presentPageSum" "$dir/joeylog.txt" 2>/dev/null)
if [[ "$line" == *"presentPageSum PASS"* ]]; then
echo "$label: PASS - ${line#UBER-PROBE: }"
exit 0
fi
echo "$label: FAIL - ${line:-no presentPageSum line in its log}" >&2
exit 1

View file

@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# verify-atarist-display.sh - the ST family's present, checked on the page
# the shifter shows. UBER's golden hashes are of the stage; a present that
# leaves stale rows on screen passes them. UBERPROB (UBER with UBER_PROBE,
# make/examples.mk) runs its display row (op_probeDisplayCheck): a tall band
# presented, then a small rect inside it, then a settle present and an idle
# one, comparing the shown page with the stage's planes after each
# (jlpProbeDisplayMismatch in src/atarist/hal.c). Every count must be 0.
#
# Usage: scripts/verify-atarist-display.sh [MACHINE...] (default: st ste)
# st and megast run the ST build, ste and megaste the STe build.
# Exit: 0 PASS, 1 FAIL, 2 SKIP (Hatari or EmuTOS not installed)
# Requires: Hatari + EmuTOS (install.sh install emulator:atarist); it builds
# the probe (make PLATFORM-uberprob).
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed HATARI hatari
joeyNeed EMUTOS_IMG emutos
label=verify-atarist-display
machines=("$@")
[ ${#machines[@]} -gt 0 ] || machines=(st ste)
failed=0
for machine in "${machines[@]}"; do
case $machine in
st|megast) build=atarist ;;
ste|megaste) build=ste ;;
*) echo "$label: unknown machine '$machine' (st, megast, ste, megaste)" >&2; exit 2 ;;
esac
makefile=$repo/make/$build.mk
if ! make -s -f "$makefile" "$build-uberprob"; then
echo "$label: FAIL - building the $build UBERPROB failed" >&2
exit 1
fi
dir=$repo/build/$build/probe
rm -f "$dir/joeylog.txt"
BENCH_PRG=UBERPROB.PRG BENCH_BIN_DIR="$dir" TIMEOUT="${TIMEOUT:-120}" "$repo/scripts/bench-atarist.sh" "$machine" > /dev/null 2>&1
line=$(grep "UBER-PROBE: displayMismatch" "$dir/joeylog.txt" 2>/dev/null)
if [[ "$line" =~ afterB=0\ afterSettle=0\ afterIdle=0 ]]; then
echo "$label: $build build on $machine OK - ${line#UBER-PROBE: }"
else
echo "$label: $build build on $machine FAIL - ${line:-no displayMismatch line in its log}" >&2
failed=1
fi
done
if [ "$failed" = 0 ]; then
echo "$label: PASS"
fi
exit "$failed"

View file

@ -12,7 +12,7 @@
# so a stale or misplaced rectangle would never reach a hash. UBER's probe
# build (make <port> builds it, into build/<port>/probe) ends with P13, the
# battery the other display gates run, which after every step asks the
# backend (hostBeProbeDisplayMismatch in src/hosted/be/beBackend.cpp) how many
# backend (jlHostBeProbeDisplayMismatch in src/hosted/be/beBackend.cpp) how many
# pixels differ: the frame's against the stage, the bitmap's against the
# frame, and the window as the shows left it against the window fully
# repainted, both read back from the screen (BScreen::ReadBitmap). Every

View file

@ -32,8 +32,9 @@
# unambiguous. This also covers the overlay specifically -- SFX on DOS mix
# into the module's buffer rather than a separate channel, so "SFX audible
# while the module player is loaded but paused" is exactly the path that
# can break. Measured under DOSBox Staging 0.83 (whose mixer
# runs quieter than the old 0.74's): music ~1220, paused ~17, sfx ~659.
# can break. Measured under DOSBox Staging 0.83: music ~1590, paused ~17,
# sfx ~108 -- the card's mixer holds digital 16 dB under FM (the canonical
# mix, src/dos/audio.c mixerBalance), so an effect is as loud as a voice.
#
# Keys are HELD (keydown, pause, keyup), not tapped: the demo tests
# jlKeyPressed, an edge predicate polled once a frame, and a make/break
@ -174,12 +175,13 @@ parecPid=""
[ -s "$work/out.wav" ] || { echo "$label: FAIL - recorded no audio at all" >&2; exit 1; }
# Thresholds sit at about half the measured levels (music ~1220, paused
# ~17, sfx ~659) so ordinary mixing jitter cannot flip the verdict.
# Thresholds sit at about half the measured levels (music ~1590, sfx
# ~108; paused ~17 under its ceiling) so ordinary mixing jitter cannot
# flip the verdict.
python3 "$grader" "$label" "$work/out.wav" \
"music:1:5:600:" \
"paused:8:13::200" \
"sfx:16:22:330:" \
"paused:8:13::40" \
"sfx:16:22:54:" \
"resumed:27:32:600:"
status=$?
[ -n "${DOS_AUDIO_KEEP:-}" ] && cp "$work/out.wav" "$repo/build/dos/headless-audio.wav" 2>/dev/null

155
scripts/verify-dos-mouse.sh Executable file
View file

@ -0,0 +1,155 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# verify-dos-mouse.sh - Headless mouse acceptance gate for the DOS port.
# The port has the mouse driver call it on every move and button change
# (INT 33h function 0Ch, a DPMI real-mode callback, src/dos/input.c); a
# callback that never runs leaves the pointer where it started. Runs
# KEYS.EXE in DOSBox Staging under Xvfb with the mouse captured from the
# start, moves it with xdotool (real X pointer motion), and decodes KEYS's
# mouse verification scanline (row 199: 5A5A, the button flags, X, Y as
# nibble pixels) from a raw screenshot after each step. Covered: the start
# at the stage's middle, a move each way on both axes, clamping at all
# four edges, an odd column, and press and release of the left and right
# buttons.
# DOSBox scales host motion into mickeys its own way, so a move is checked
# by its direction and only the clamps by position, and a one-pixel nudge
# by landing on an odd column: in mode 13h the driver steps X by 2 in a
# 640-wide virtual screen, which the port halves, so every column counts.
#
# Usage: scripts/verify-dos-mouse.sh
# Requires: DOSBox Staging (install.sh install emulator:dos), Xvfb, xdotool, python3 + PIL;
# build/dos/bin/KEYS.EXE (make dos).
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
source "$repo/scripts/lib/dosboxKeys.sh"
joeyNeed DOSBOX dosbox
bin_dir=$repo/build/dos/bin
conf=$repo/scripts/dosbox-386sx16.conf
label=verify-dos-mouse
[ -f "$bin_dir/KEYS.EXE" ] || { echo "$label: build KEYS.EXE first (make dos)" >&2; exit 2; }
work=$(mktemp -d -t joeylib-dos-mouse.XXXXXX)
display_num=$((RANDOM % 500 + 300))
cleanup() {
joeyStop "${dosbox_pid:-}"
[ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null
rm -rf "$work"
}
trap cleanup EXIT
mkdir -p "$work/shots"
dosboxKeysMapper "$work/mapper.txt"
Xvfb ":$display_num" -screen 0 1024x768x24 >/dev/null 2>&1 &
xvfb_pid=$!
sleep 1
XDG_CONFIG_HOME="$work/xdg" DISPLAY=":$display_num" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
--conf "$conf" \
--set "capture_dir=$work/shots" \
--set "mapperfile=$work/mapper.txt" \
--set "mouse_capture=onstart" \
-c "MOUNT C $bin_dir" -c "C:" -c "KEYS.EXE" \
>"$work/dosbox.log" 2>&1 &
dosbox_pid=$!
# Let DOS boot and KEYS reach its poll loop on the simulated 386SX-16.
sleep 12
win=$(joeyFindWindow ":$display_num" "$dosbox_pid")
if [ -z "$win" ]; then
echo "$label: FAIL - DOSBox window not found" >&2
exit 1
fi
DISPLAY=":$display_num" xdotool windowactivate --sync "$win" 2>/dev/null || true
# The mouse row of a fresh screenshot as "flags x y" (hex flags).
readRow() {
local png="$work/$1.png"
dosboxShot ":$display_num" "$win" "$work/shots" "$png" || return 1
python3 - "$png" <<'PY'
import sys
from PIL import Image
img = Image.open(sys.argv[1]).convert("RGB")
w, h = img.size
sx, sy = w // 320, h // 200
if sx < 1 or sy < 1 or w % 320 or h % 200:
sys.exit(1)
def dac8(c4):
v6 = ((c4 << 2) | (c4 >> 2)) & 0x3F
return (v6 << 2) | (v6 >> 4)
# KEYS palette 0 (as verify-dos-input.sh decodes it).
pal12 = [(i << 8) | (i << 4) | i for i in range(16)]
pal12[0], pal12[1], pal12[2], pal12[3] = 0x000, 0x333, 0x0F0, 0xFFF
pal12[15] = 0xF00
palRgb = [(dac8((v >> 8) & 0xF), dac8((v >> 4) & 0xF), dac8(v & 0xF)) for v in pal12]
def pix(px):
r, g, b = img.getpixel((px * sx, 199 * sy))
return min(range(16), key=lambda n: (r - palRgb[n][0]) ** 2 + (g - palRgb[n][1]) ** 2 + (b - palRgb[n][2]) ** 2)
nib = [pix(i) for i in range(16)]
words = [(nib[4 * k] << 12) | (nib[4 * k + 1] << 8) | (nib[4 * k + 2] << 4) | nib[4 * k + 3] for k in range(4)]
if words[0] != 0x5A5A:
sys.exit(1)
print("%04X %d %d" % (words[1], words[2], words[3]))
PY
}
fails=()
step() {
local tag="$1"
local row
shift
if [ $# -gt 0 ]; then
DISPLAY=":$display_num" xdotool "$@"
sleep 1
fi
if ! row=$(readRow "$tag"); then
fails+=("$tag: no mouse row in the screenshot")
row="---- -1 -1"
fi
read -r flags mx my <<< "$row"
echo " $tag: flags=$flags x=$mx y=$my"
}
step start
[ "$flags $mx $my" = "1000 160 100" ] || fails+=("start: expected flags 1000 at 160,100")
step upLeft mousemove_relative -- -60 -40
[ "$flags" = "1000" ] && [ "$mx" -lt 160 ] && [ "$my" -lt 100 ] || fails+=("a move up and left did not arrive")
lastX=$mx
lastY=$my
step downRight mousemove_relative -- 120 80
[ "$flags" = "1000" ] && [ "$mx" -gt "$lastX" ] && [ "$my" -gt "$lastY" ] || fails+=("a move down and right did not arrive")
step topLeft mousemove_relative -- -2000 -2000
[ "$flags $mx $my" = "1000 0 0" ] || fails+=("not held at the top left (0,0)")
step nudge mousemove_relative -- 1 0
[ "$flags" = "1000" ] && [ $((mx % 2)) -eq 1 ] || fails+=("a one-pixel nudge did not reach an odd column")
step bottomRight mousemove_relative -- 2000 2000
[ "$flags $mx $my" = "1000 319 199" ] || fails+=("not held at the bottom right (319,199)")
step leftDown mousedown 1
[ "$flags" = "1100" ] || fails+=("the left button down not seen")
step leftUp mouseup 1
[ "$flags" = "1000" ] || fails+=("the left button up not seen")
step rightDown mousedown 3
[ "$flags" = "1010" ] || fails+=("the right button down not seen")
step rightUp mouseup 3
[ "$flags" = "1000" ] || fails+=("the right button up not seen")
if [ ${#fails[@]} -eq 0 ]; then
echo "$label: PASS (moves both ways, all four edges, every column, both buttons down and up)"
exit 0
fi
for f in "${fails[@]}"; do
echo "$label: FAIL - $f" >&2
done
exit 1

View file

@ -12,7 +12,7 @@
# (P13) that draws one-pixel stripes in all 16 colours, bands and a small rect, moves
# rows to another palette and changes that palette, presenting after each,
# and asks the port after every present how many pixels differ from the
# stage (dvxPortProbeDisplayMismatch in src/dvx/hal.c): the window's content
# stage (jlDvxPortProbeDisplayMismatch in src/dvx/hal.c): the window's content
# buffer in the desktop's own pixel format, every screen pixel of a stage
# pixel's block (scale across; down, the rows the window's shape gives it:
# 4:3 stretches every fifth row one more, square pixels take scale each),

View file

@ -12,7 +12,7 @@
# Manager takes the focus, the desktop comes back and UBER presents into
# its window) and resumed (the Task Manager's Switch To): the
# screenshots must be the 320x200 mode, the desktop, then 320x200 again;
# DVX.LOG must show the mode set twice; and UBER's 51 hashes must equal
# DVX.LOG must show the mode set twice; and UBER's 54 hashes must equal
# the DOS port's (tests/goldens/uber/dos.txt): the stage is DOS's
# wherever it is shown.
# 2. KEYS on the whole screen: the typed string must reach jlInputGetChar
@ -227,4 +227,4 @@ if [ -n "$fails" ]; then
echo "verify-dvx-fullscreen: FAIL - $fails"
exit 1
fi
echo "verify-dvx-fullscreen: PASS (UBER 51/51 on the whole screen through a suspend and resume; KEYS's typed row read off the 320x200 screen at (0,198), in its window, and full screen again)"
echo "verify-dvx-fullscreen: PASS (UBER 54/54 on the whole screen through a suspend and resume; KEYS's typed row read off the 320x200 screen at (0,198), in its window, and full screen again)"

120
scripts/verify-iigs-keys.sh Executable file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# verify-iigs-keys.sh - Headless gate for the IIgs's key predicates, the
# asm jlKeyDown and jlKeyPressed (JL_HAS_INPUT_PREDICATES, joeyDraw.s).
# Boots GS/OS under MAME and launches the KEYS example off joey.2mg, which
# lights a key's cell while jlKeyDown says it is held and quits on
# jlKeyPressed(JL_KEY_ESCAPE). Holds A on the emulated ADB keyboard and
# reads the A cell straight out of SHR memory: unlit before, lit while
# held, unlit after. Then presses Escape: KEYS must quit to the Finder,
# whose 640-mode SCB replaces KEYS' 320-mode one.
#
# Usage: scripts/verify-iigs-keys.sh
# Requires: ./install.sh install iigs; build/iigs/bin/joey.2mg built
# with the current KEYS (make iigs-disk).
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed MAME mame
joeyNeed IIGS_ROMPATH iigsFirmware
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
sys_disk=$GSOS_SYSTEM_DISK
data_disk=$repo/build/iigs/bin/joey.2mg
for f in "$sys_disk" "$data_disk"; do
[ -f "$f" ] || { echo "verify-iigs-keys: missing $f (run 'make iigs-disk')" >&2; exit 2; }
done
work=$(mktemp -d -t joeylib-keys-verify.XXXXXX)
trap 'rm -rf "$work"' EXIT
cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"
# Finder keystroke timeline as in verify-iigs-input.sh. The A cell is
# KEYS' row 2, column 0 (examples/keys: MARGIN 2, CELL 28x26, GAP 4); its
# middle, x 16 y 75, is the SHR byte $E1/2000 + 75*160 + 8 = $E1/4EE8:
# $11 unlit (COLOR_UNLIT), $22 lit (COLOR_LIT). SCB 0 is $E1/9D00.
cat > "$work/verify.lua" <<'LUA'
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local frame = 0
local idx = 1
local function field(port, name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[name]
end
local key_cmd = field(":macadb:KEY3", "Command / Open Apple")
local key_a = field(":macadb:KEY0", "a A")
local key_esc = field(":macadb:KEY3", "Esc")
local function press(f) if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end
local function report(tag)
io.write(string.format("VERIFY-IIGS-KEYS %s scb0=%02X cell=%02X\n", tag, mem:read_u8(0xE19D00), mem:read_u8(0xE14EE8)))
io.flush()
end
local steps = {
{3000, function() nat:post("J") end},
{3120, function() press(key_cmd) end},
{3126, function() nat:post("o") end},
{3180, function() release(key_cmd) end},
{3540, function() nat:post("KEYS") end},
{3660, function() press(key_cmd) end},
{3666, function() nat:post("o") end},
{3720, function() release(key_cmd) end},
{6600, function() report("before") end},
{6610, function() press(key_a) end},
{6700, function() report("held") end},
{6710, function() release(key_a) end},
{6800, function() report("released") end},
{6810, function() press(key_esc) end},
{6830, function() release(key_esc) end},
{9000, function() report("escape"); manager.machine:exit() end},
}
emu.register_frame_done(function()
frame = frame + 1
while idx <= #steps and frame >= steps[idx][1] do
steps[idx][2]()
idx = idx + 1
end
end)
LUA
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 300 "$MAME" apple2gs \
-rompath "$IIGS_ROMPATH" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
-autoboot_script "$work/verify.lua" </dev/null 2>&1) || true
report=$(echo "$out" | grep -E '^VERIFY-IIGS-KEYS ' || true)
echo "$report"
value() {
echo "$report" | grep " $1 " | sed -E "s/.*$2=([0-9A-Fa-f]+).*/\1/"
}
fails=()
[ "$(value before scb0)" = "00" ] || fails+=("KEYS never launched (SCB 0 $(value before scb0))")
[ "$(value before cell)" = "11" ] || fails+=("the A cell lit before A was held ($(value before cell))")
[ "$(value held cell)" = "22" ] || fails+=("jlKeyDown: the A cell unlit while A was held ($(value held cell))")
[ "$(value released cell)" = "11" ] || fails+=("jlKeyDown: the A cell still lit after A was let go ($(value released cell))")
scb=$(value escape scb0)
[ -n "$scb" ] && [ $((16#$scb)) -ge 128 ] || fails+=("jlKeyPressed: Escape did not quit KEYS (SCB 0 ${scb:-none})")
if [ ${#fails[@]} -eq 0 ]; then
echo "verify-iigs-keys: PASS (A lit only while held; Escape quit KEYS)"
exit 0
fi
for f in "${fails[@]}"; do
echo "verify-iigs-keys: FAIL - $f" >&2
done
exit 1

View file

@ -11,7 +11,7 @@
# so a stale or misplaced rectangle would never reach a hash. UBER's probe
# build (make win16 builds it, into build/win16/probe) ends with P13, the
# battery verify-dvx-display.sh runs on DVX, which after every step asks the
# backend (hostWin16ProbeDisplayMismatch in src/hosted/win16/win16Backend.c)
# backend (jlHostWin16ProbeDisplayMismatch in src/hosted/win16/win16Backend.c)
# how many pixels differ: the frame's against the stage, the window as the
# shows left it against the window fully repainted, and, at a whole-number
# scale, that repaint against GDI's own stretch of the frame. In the

View file

@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# verify-win16-serial-fast.sh - the Win16 port's serial throughput and loss
# gate. SERFAST (SERTEST at 115200, make/examples.mk) runs on Windows 3.11
# under DOSBox with COM1 a nullmodem to the host peer
# (scripts/lib/serialLoopPeer.py), and the KPCOMM driver every Win16 run gets
# (win16Stage), which takes a COM port past 57600. After the banner and the
# probe's echo the peer streams STREAM_BYTES bytes through SERTEST's echo, a
# window at a time, and every one must come back once and in order, at no
# less than three quarters of what 115200 baud carries. The rate was measured
# once by hand when KPCOMM came in (11485 bytes a second echoed); this keeps
# it, and its loss, under test.
#
# Usage: scripts/verify-win16-serial-fast.sh
# Env: WIN16_SERIAL_PORT the nullmodem's TCP port (default 6805)
# Exit: 0 PASS, 1 FAIL, 2 SKIP
# Requires: Windows on DOSBox (install.sh install emulator:win16), python3;
# the win16 toolchain (it builds SERFAST.EXE, make win16-serfast).
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
source "$repo/scripts/lib/win16Gate.sh"
label=verify-win16-serial-fast
win16Need
make -s -f "$repo/make/win16.mk" win16-serfast || {
echo "$label: FAIL - building SERFAST.EXE failed" >&2
exit 1
}
program=$(find "$repo/build/win16/test" -name SERFAST.EXE | head -1)
[ -f "$program" ] || { echo "$label: FAIL - no SERFAST.EXE after the build" >&2; exit 1; }
port="${WIN16_SERIAL_PORT:-6805}"
baud=115200
streamBytes=16384
# The peer's budget: Windows' boot, the banner and the echo, then the stream.
wall=120
work=$(mktemp -d -t joeylib-win16-serial-fast.XXXXXX)
peerPid=""
cleanup() {
joeyStop "$peerPid"
win16Stop
rm -rf "$work"
}
trap cleanup EXIT
python3 "$repo/scripts/lib/serialLoopPeer.py" tcp-server "$port" "$wall" --stream "$streamBytes" --rate "$baud" > "$work/peer.log" 2>&1 &
peerPid=$!
sleep 1
mkdir -p "$work/program"
cp "$program" "$work/program/SERFAST.EXE"
win16Stage "$work" "$work/program" SERFAST.EXE
# transparent:1 -- the bytes alone, no modem lines in the stream.
WIN16_DOSBOX_ARGS=(--set "serial1=nullmodem server:127.0.0.1 port:$port transparent:1")
win16Start "$work"
wait "$peerPid"
rc=$?
peerPid=""
cat "$work/peer.log"
if [ "$rc" -eq 0 ]; then
echo "$label: PASS ($streamBytes bytes through COM1 at $baud baud and back, none lost)"
else
echo "$label: FAIL" >&2
fi
exit "$rc"

View file

@ -12,7 +12,7 @@
# a hash. UBER's probe build (make win32 builds it, into build/win32/probe)
# ends with P13, the battery verify-dvx-display.sh and
# verify-win16-display.sh run, which after every step asks the backend
# (hostWin32ProbeDisplayMismatch in src/hosted/win32/win32Backend.c) how
# (jlHostWin32ProbeDisplayMismatch in src/hosted/win32/win32Backend.c) how
# many pixels differ: the frame's against the stage, the window as the
# shows left it against the window fully repainted, and that repaint
# against GDI stretching the core's frame itself. Every count must be 0,

View file

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# verify-x68000-display.sh - the X68000's present, checked on the text-plane
# page it draws. UBER's golden hashes are of the stage; a present that leaves
# stale rows on screen passes them. This page flips: each present draws into
# the page not shown, which holds the frame before last, from the union of
# two frames' dirty bands. UBER built with UBER_PROBE runs its X68000 display
# row (op_probeX68kDisplayCheck): a battery of draws, each presented and the
# drawn page compared with the stage's planes (jlX68kProbeDisplayMismatch in
# src/x68000/hal.c). The total must be 0.
#
# Usage: scripts/verify-x68000-display.sh
# Exit: 0 PASS, 1 FAIL
# Requires: MAME and the X68000 firmware (install.sh install emulator:x68000),
# the library built (make x68000).
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
label=verify-x68000-display
log=$repo/build/x68000/joeylog.txt
rm -f "$log"
if ! X68K_BENCH_CFLAGS="-DUBER_PROBE -DUBER_FRAMES=1" "$repo/scripts/bench-x68000.sh" > /dev/null 2>&1; then
echo "$label: FAIL - the UBER_PROBE run did not finish" >&2
exit 1
fi
line=$(grep "UBER-PROBE: x68kDisplay" "$log" 2>/dev/null)
if [[ "$line" == *" PASS (mismatch=0)" ]]; then
echo "$label: PASS - ${line#UBER-PROBE: }"
exit 0
fi
echo "$label: FAIL - ${line:-no x68kDisplay line in its log}" >&2
exit 1

View file

@ -61,7 +61,7 @@ x68kDataDisk "$work/data.xdf" || exit 1
x68kXdf add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1
# Completion-driven exit. UBER's last act before jlWaitForAnyKey is
# jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM
# jlSurfaceClear(jlgStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM
# is the "hashes are computed and flushed" signal. The benchmark's own
# jlSurfaceClear/jlStagePresent ops can paint that colour for an instant, so the
# reading only counts once it has held for GREEN_STABLE frames -- the finished

View file

@ -9,7 +9,7 @@
#
# The X68000 port presents to the text plane, which has ONE 16-entry palette,
# so it honours the SCB by reloading that palette as the beam scans: a CRTC
# raster interrupt (src/x68000/x68kRasterIsr.s) stores a band's sixteen
# raster interrupt (src/x68000/jlX68kRasterIsr.s) stores a band's sixteen
# colours in the horizontal blanking before the band's first line. Each reload
# has a window of about 400 dots (~18 us): after the last stage pixel of the
# line above and before the first stage pixel of the band. Land early and the
@ -48,19 +48,19 @@
# taps.
# exit after ESC the program exits and gives the $138 vector and MFP
# channel 14 back -- and the two clock vectors it held while it
# ran, which are checked then: $114 (x68kTickIsr, chained onto
# Human68k's Timer C) and $134 (x68kVdispIsr, the frame counter
# ran, which are checked then: $114 (jlX68kTickIsr, chained onto
# Human68k's Timer C) and $134 (jlX68kVdispIsr, the frame counter
# _VDISPST installs, with MFP channel 13 enabled).
#
# It also reports the interrupt's cost (time inside it per frame), the latency
# from each raster match to the vector fetch, and the slack of the compare
# write the interrupt's timing rests on (x68kRasterIsr.s step 2).
# write the interrupt's timing rests on (jlX68kRasterIsr.s step 2).
#
# Human68k's Timer C tick runs throughout, and text is typed during the whole
# capture so the IOCS keyboard interrupt runs too: both are MFP interrupts at
# IPL 6, like the raster interrupt, and a raster match that falls inside one of
# them is taken late (the Timer C dispatcher used to make reloads miss their
# line; see src/x68000/x68kTickIsr.s). The DMAC-driven ADPCM path is not
# line; see src/x68000/jlX68kTickIsr.s). The DMAC-driven ADPCM path is not
# exercised: JoeyLib's audio does its work on the main thread
# (src/x68000/audioPcm.c).
#
@ -132,12 +132,12 @@ local TEXT_PALETTE, COLORS = 0xE82200, 16
local CRTC, CRTC_R09 = 0xE80000, 0xE80012
local MFP_IERA, MFP_IMRA, MFP_ISRA = 0xE88007, 0xE88013, 0xE8800F
local RASTER_VECTOR = 0x138
local TICK_VECTOR = 0x114 -- MFP channel 5, Timer C: x68kTickIsr
local VDISP_VECTOR = 0x134 -- MFP channel 13, Timer A: x68kVdispIsr
local TICK_VECTOR = 0x114 -- MFP channel 5, Timer C: jlX68kTickIsr
local VDISP_VECTOR = 0x134 -- MFP channel 13, Timer A: jlX68kVdispIsr
local TIMER_A_BIT = 0x20 -- channel 13 in IERA / IMRA
local PROGRAM_RAM_END = 0x01000000 -- IOCS's own test for "a handler is installed"
local GPIP6_BIT = 0x40
local ISRA_EXIT = 0xBF -- x68kRasterIsr's in-service ack
local ISRA_EXIT = 0xBF -- jlX68kRasterIsr's in-service ack
local CPU_HZ = 10000000 -- 40 MHz / 4 (MAME x68k.cpp)
local IRQ_HPOS = 2 -- x68k_crtc.cpp: R09 = N fires at (N - 1, 2)
@ -410,7 +410,7 @@ end
-- Interrupt cost, latency and slack from the entry/exit/R09 events. The first
-- R09 write after an entry re-points the compare at the raster of the line
-- above the band (x68kRasterIsr.s step 2); it must land before that raster
-- above the band (jlX68kRasterIsr.s step 2); it must land before that raster
-- begins, and its slack is how far ahead it did. The last R09 write before an
-- entry is the arm that raised it.
local function reportInterrupt()

View file

@ -6,7 +6,7 @@
# verify-x68000-serial-mouse.sh - the X68000's RS-232C port and mouse at once.
#
# Both live on the one Z8530 SCC: RS-232C on channel A, where JoeyLib's serial
# add-on owns the receive interrupt (src/x68000/x68kSccRxIsr.s), and the mouse
# add-on owns the receive interrupt (src/x68000/jlX68kSccRxIsr.s), and the mouse
# on channel B, where the IOCS ROM owns the receive interrupt and solicits a
# packet every other Timer C tick. The chip has ONE register pointer for both
# channels, so a select-then-access register sequence split by the other

View file

@ -7,7 +7,7 @@
# per-line palette change: the SCB raster interrupt and the SCC together.
#
# The X68000 port honours the SCB by reloading the text plane's one palette as
# the beam scans (src/x68000/x68kRasterIsr.s). A palette change on every line
# the beam scans (src/x68000/jlX68kRasterIsr.s). A palette change on every line
# keeps that interrupt at IPL 6 for the whole run, about one raster (31.8 us)
# a change plus two rasters of lead, and the SCC is IPL 5, so every byte that
# arrives in the run waits in the Z8530's receive FIFO. No handler can let the

View file

@ -19,4 +19,17 @@ extern struct Custom custom;
static volatile struct Custom *const gCust = &custom;
// BLTSIZE packs a blit's row count in its high 10 bits (<< 6) and its word
// count in the low 6 (a width of 0 would mean 64 words).
#define AMIGA_BLT_SIZESHIFT 6
#define AMIGA_BLT_MASK_ALL 0xFFFFu
// Minterms that keep the destination through C. With A the constant $FFFF
// cut by the first/last-word masks (amigaBlitterMaskedFillOne), set-bits is
// D = A OR C (the masked bits on, the rest kept) and clear-bits D = (NOT A)
// AND C (the masked bits off); with A source data, D = A OR C adds A's bits
// (amigaTileLayer.c's edge words).
#define AMIGA_BLT_MINTERM_SET 0xFAu // D = A | C
#define AMIGA_BLT_MINTERM_CLEAR 0x0Au // D = ~A & C
#endif

199
src/amiga/amigaTileLayer.c Normal file
View file

@ -0,0 +1,199 @@
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// The Amiga's tile-layer ring and blit. The ring is the shared
// separate-plane one (src/m68k/planarTileLayer.h has the layout,
// planarTileLayer.c the Put and the CPU blit), put in chip RAM when there is
// room, so that the blitter can copy the band into a chip-RAM surface; the
// CPU blit copies it otherwise.
//
// Word k of a band plane row is (P[w + k] << s) | (P[w + k + 1] >> (16 - s)),
// P being that plane's 21 words of the ring row, w = ringX / 16, s = ringX &
// 15, and the indices wrapping at 21. Words 0..19 take their P[w + k] from P[w]
// to the ring row's end and then on from P[0]: at most two runs of whole
// source words, the second landing at band word 21 - w. A blit's channels
// walk the same number of words, and a run's last word needs one source word
// more than the run has, so a run is two blits:
// * the run itself, descending, so the blitter's shift goes left: A is
// the run's words shifted s, D the band's. Each band word takes its low
// bits from the source word to its right; the run's last band word, with
// none in the run, takes them from what the blitter shifted last.
// * its edge (s > 0): the source word after the run, ascending, shifted
// right 16 - s into the low s bits and or'd into the run's last band
// word through C.
// What a blit shifts in at a row's first word is the last A word of the row
// before, masked -- or, on its first row, of the blit before. So the masks
// keep each blit's last word to the bits the next one may take: a run's
// last-word mask clears the high s bits of each row's leftmost source word
// (the bits a run's next row would take), and an edge's masks keep only the
// high s bits it moves (an edge takes none). The runs all go first, then
// the edges, each group after a blit of a masked-off word, so its first row
// shifts in zeros too: the runs leave their last band words' low s bits
// clear for the edges to or into.
//
// The band's rows wrap at the ring's last row too, so each run and edge is a
// blit per plane per vertical stretch: at most 32 a band, each a register
// setup while the blitter moves 20 words a row per plane.
#include <stdbool.h>
#include <stdint.h>
#include <exec/memory.h>
#include <hardware/blit.h>
#include <proto/exec.h>
#include <proto/graphics.h>
#include "amigaCustom.h"
#include "planarTileLayer.h"
#include "port.h"
#define BAND_WORDS (PLANAR_TILE_BYTES_PER_ROW / 2u)
#define WORD_BITS 16u
#define SHIFT_MASK (WORD_BITS - 1u)
static void blitBandRows(const uint8_t *srcRow, uint8_t *dstRow, uint16_t rows, uint16_t w, uint16_t s, bool edges);
static void blitEdge(const uint8_t *src, uint8_t *dst, uint16_t rows, uint16_t s);
static void blitRun(const uint8_t *src, uint8_t *dst, uint16_t words, uint16_t rows, uint16_t s);
static void blitZeroCarry(void);
// `rows` rows of one plane, its runs or (edges) its edges: source plane rows
// from srcRow (one ring row apart), band plane rows from dstRow.
static void blitBandRows(const uint8_t *srcRow, uint8_t *dstRow, uint16_t rows, uint16_t w, uint16_t s, bool edges) {
uint16_t words1;
uint16_t words2;
words1 = (uint16_t)(PLANAR_RING_PLANE_WORDS - w);
if (words1 > BAND_WORDS) {
words1 = BAND_WORDS;
}
words2 = (uint16_t)(BAND_WORDS - words1);
if (edges) {
blitEdge(srcRow + ((w + words1) % PLANAR_RING_PLANE_WORDS) * 2u, dstRow + (words1 - 1u) * 2u, rows, s);
if (words2 != 0u) {
blitEdge(srcRow + words2 * 2u, dstRow + (BAND_WORDS - 1u) * 2u, rows, s);
}
return;
}
blitRun(srcRow + w * 2u, dstRow, words1, rows, s);
if (words2 != 0u) {
blitRun(srcRow, dstRow + words1 * 2u, words2, rows, s);
}
}
// The source word at src, shifted right 16 - s, or'd into the band word at
// dst, row after row.
static void blitEdge(const uint8_t *src, uint8_t *dst, uint16_t rows, uint16_t s) {
uint16_t keep;
keep = (uint16_t)~(AMIGA_BLT_MASK_ALL >> s);
WaitBlit();
gCust->bltcon0 = (uint16_t)(((WORD_BITS - s) << ASHIFTSHIFT) | SRCA | SRCC | DEST | AMIGA_BLT_MINTERM_SET);
gCust->bltcon1 = 0u;
gCust->bltafwm = keep;
gCust->bltalwm = keep;
gCust->bltamod = (int16_t)(PLANAR_RING_ROW_BYTES - 2u);
gCust->bltcmod = (int16_t)(PLANAR_TILE_BYTES_PER_ROW - 2u);
gCust->bltdmod = (int16_t)(PLANAR_TILE_BYTES_PER_ROW - 2u);
gCust->bltapt = (APTR)src;
gCust->bltcpt = (APTR)dst;
gCust->bltdpt = (APTR)dst;
gCust->bltsize = (uint16_t)((rows << AMIGA_BLT_SIZESHIFT) | 1u);
}
// `words` source words from src shifted left s into the band words from
// dst, row after row, descending: the pointers start at the last row's last
// word.
static void blitRun(const uint8_t *src, uint8_t *dst, uint16_t words, uint16_t rows, uint16_t s) {
uint32_t last;
last = (uint32_t)(words - 1u) * 2u;
WaitBlit();
gCust->bltcon0 = (uint16_t)((s << ASHIFTSHIFT) | SRCA | DEST | A_TO_D);
gCust->bltcon1 = BLITREVERSE;
gCust->bltafwm = AMIGA_BLT_MASK_ALL;
gCust->bltalwm = (uint16_t)(AMIGA_BLT_MASK_ALL >> s);
gCust->bltamod = (int16_t)(PLANAR_RING_ROW_BYTES - words * 2u);
gCust->bltdmod = (int16_t)(PLANAR_TILE_BYTES_PER_ROW - words * 2u);
gCust->bltapt = (APTR)(src + (uint32_t)(rows - 1u) * PLANAR_RING_ROW_BYTES + last);
gCust->bltdpt = (APTR)(dst + (uint32_t)(rows - 1u) * PLANAR_TILE_BYTES_PER_ROW + last);
gCust->bltsize = (uint16_t)((rows << AMIGA_BLT_SIZESHIFT) | words);
}
// A one-word blit of a word the masks clear, through no channel, so the next
// blit's first row shifts in zeros.
static void blitZeroCarry(void) {
WaitBlit();
gCust->bltcon0 = 0u;
gCust->bltcon1 = 0u;
gCust->bltadat = 0u;
gCust->bltafwm = 0u;
gCust->bltalwm = 0u;
gCust->bltsize = (uint16_t)((1u << AMIGA_BLT_SIZESHIFT) | 1u);
}
void jlpTileLayerBlit(jlSurfaceT *dst, const jlTileLayerT *layer, uint16_t ringX, uint16_t ringY) {
const AmigaPlanarT *pd;
const uint8_t *ring;
uint8_t *band;
uint16_t rows;
uint16_t rows1;
uint16_t w;
uint16_t s;
uint16_t p;
uint16_t pass;
pd = (const AmigaPlanarT *)dst->portData;
if (!pd->chip || (TypeOfMem(layer->ring) & MEMF_CHIP) == 0u) {
jlPlanarTileLayerCpuBlit(dst, layer, ringX, ringY);
return;
}
ring = (const uint8_t *)layer->ring;
rows = (uint16_t)((uint16_t)layer->viewHeight * JL_TILE_PIXELS_PER_SIDE);
rows1 = (uint16_t)((uint16_t)layer->ringRows * JL_TILE_PIXELS_PER_SIDE - ringY);
if (rows1 > rows) {
rows1 = rows;
}
w = (uint16_t)(ringX >> 4);
s = (uint16_t)(ringX & SHIFT_MASK);
OwnBlitter();
// Pass 0 the runs, pass 1 (s > 0) the edges.
for (pass = 0u; pass < (s != 0u ? 2u : 1u); pass++) {
blitZeroCarry();
for (p = 0u; p < PLANAR_TILE_PLANES; p++) {
band = pd->planes[p] + (uint16_t)layer->viewBy * JL_TILE_PIXELS_PER_SIDE * PLANAR_TILE_BYTES_PER_ROW;
blitBandRows(ring + (uint32_t)ringY * PLANAR_RING_ROW_BYTES + p * PLANAR_RING_PLANE_BYTES, band, rows1, w, s, pass != 0u);
if (rows1 < rows) {
blitBandRows(ring + p * PLANAR_RING_PLANE_BYTES, band + (uint32_t)rows1 * PLANAR_TILE_BYTES_PER_ROW, (uint16_t)(rows - rows1), w, s, pass != 0u);
}
}
}
WaitBlit();
DisownBlitter();
}
// Chip RAM first, so the blitter can read it; anywhere if chip RAM is short.
bool jlpTileLayerRingAlloc(jlTileLayerT *layer) {
ULONG bytes;
bytes = (ULONG)layer->ringRows * PLANAR_RING_SLOT_BYTES;
layer->ring = AllocMem(bytes, MEMF_CHIP | MEMF_CLEAR);
if (layer->ring == NULL) {
layer->ring = AllocMem(bytes, MEMF_ANY | MEMF_CLEAR);
}
return layer->ring != NULL;
}
void jlpTileLayerRingFree(jlTileLayerT *layer) {
if (layer->ring != NULL) {
FreeMem(layer->ring, (ULONG)layer->ringRows * PLANAR_RING_SLOT_BYTES);
layer->ring = NULL;
}
}

View file

@ -52,7 +52,7 @@ extern UBYTE mt_E8Trigger;
#define PAULA_PERIOD_MIN 124 // Paula HW limit
#define PAULA_PERIOD_MAX 65535
// The fastest rate Paula plays: an effect faster than this plays decimated
// (audioDecimate), at its pitch.
// (jlAudioDecimate), at its pitch.
#define PAULA_MAX_RATE ((uint16_t)(PAULA_CLOCK_PAL / PAULA_PERIOD_MIN))
// A one-shot's head, in Chip RAM before it starts: the rest is copied while
// Paula plays it (jlpAudioPlaySfx).
@ -122,7 +122,7 @@ extern UBYTE mt_E8Trigger;
typedef struct {
UBYTE *samples; // Chip-RAM copy of one SFX sample
ULONG sampleBytes; // its allocation
ULONG sampleBytes; // its allocation
// ---- streaming (jlpAudioPlaySfxStream) ----
UBYTE *chunk; // Chip-RAM chunk buffer, SFX_STREAM_CHUNK
BYTE cha; // Paula channel, CHANNEL_PICK = centred
@ -132,8 +132,8 @@ typedef struct {
void *ctx;
uint16_t rateHz;
uint16_t dueFrame; // the VBL count the next chunk goes out at
uint8_t decimate; // every decimate-th sample plays (audioDecimate), at rateHz
uint8_t phase; // audioDecimate's phase between chunks
uint8_t decimate; // every decimate-th sample plays (jlAudioDecimate), at rateHz
uint8_t phase; // jlAudioDecimate's phase between chunks
bool active;
} SfxSlotT;
@ -150,11 +150,11 @@ static bool gNoiseOn = false;
// never take it (maskNoiseChannel), so it is most often free.
static const BYTE kLeftChannels[SIDE_CHANNELS] = { NOISE_CHANNEL, 0 };
static const BYTE kRightChannels[SIDE_CHANNELS] = { 2, 1 };
static bool gInstalled = false;
static bool gModPlaying = false;
static bool gWantLoopOnce = false; // honor loop=false via E8 marker
static BYTE *gVoiceChip = NULL; // chip-RAM square shared by all voices
static BYTE *gNoiseChip = NULL; // chip-RAM LFSR noise for channel 3
static bool gInstalled = false;
static bool gModPlaying = false;
static bool gWantLoopOnce = false; // honor loop=false via E8 marker
static BYTE *gVoiceChip = NULL; // chip-RAM square shared by all voices
static BYTE *gNoiseChip = NULL; // chip-RAM LFSR noise for channel 3
// Paula periods for noise pitch 0..31: the sample plays at the rate every
// port gives that pitch, the YM2149's 125000 / pitch Hz (pitch 0 as 1), so
@ -403,7 +403,7 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
if (chip == NULL) {
return;
}
if (loop || !modStopMarkerAdd(data, length, chip)) {
if (loop || !jlModStopMarkerAdd(data, length, chip)) {
memcpy(chip, data, length);
}
gModuleChip = chip;
@ -483,7 +483,7 @@ static bool streamChunk(uint8_t slot) {
return false;
}
if (s->decimate > 1u) {
got = audioDecimate(s->chunk, s->chunk, got, s->decimate, &s->phase);
got = jlAudioDecimate(s->chunk, s->chunk, got, s->decimate, &s->phase);
}
} while (got < 2u);
got &= ~1u; // Paula counts 16-bit words
@ -523,9 +523,9 @@ static bool tickOpen(void) {
gTickPort.mp_Flags = PA_SOFTINT;
gTickPort.mp_SoftInt = &gTickSoftInt;
// An empty list, as amiga.lib's NewList makes one.
gTickPort.mp_MsgList.lh_Head = (struct Node *)&gTickPort.mp_MsgList.lh_Tail;
gTickPort.mp_MsgList.lh_Tail = NULL;
gTickPort.mp_MsgList.lh_TailPred = (struct Node *)&gTickPort.mp_MsgList.lh_Head;
gTickPort.mp_MsgList.lh_Head = (struct Node *)&gTickPort.mp_MsgList.lh_Tail;
gTickPort.mp_MsgList.lh_Tail = NULL;
gTickPort.mp_MsgList.lh_TailPred = (struct Node *)&gTickPort.mp_MsgList.lh_Head;
gTickRequest.tr_node.io_Message.mn_Node.ln_Type = NT_REPLYMSG;
gTickRequest.tr_node.io_Message.mn_ReplyPort = &gTickPort;
gTickRequest.tr_node.io_Message.mn_Length = (UWORD)sizeof(gTickRequest);
@ -601,7 +601,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
return;
}
// Faster than Paula plays: every factor-th sample, at its pitch.
factor = audioDecimationFactor(rateHz, PAULA_MAX_RATE);
factor = jlAudioDecimationFactor(rateHz, PAULA_MAX_RATE);
kept = (length + factor - 1u) / factor;
if (kept < 2 || kept > 0x1FFFEUL) {
return; // Paula sample length is 16 bits of words = 128KB max
@ -626,7 +626,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
// long or fast sample starts as soon as a short one.
head = kept < SFX_HEAD_BYTES ? kept : SFX_HEAD_BYTES;
phase = 0u;
(void)audioDecimate(chip, sample, head * factor, factor, &phase);
(void)jlAudioDecimate(chip, sample, head * factor, factor, &phase);
words = (UWORD)(kept >> 1);
s->samples = chip;
@ -639,7 +639,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
sfx.sfx_pri = SLOT_PRIORITY(slot);
sfxPlay(slot, &sfx);
if (kept > head) {
(void)audioDecimate(chip + head, sample + head * factor, length - head * factor, factor, &phase);
(void)jlAudioDecimate(chip + head, sample + head * factor, length - head * factor, factor, &phase);
}
}
@ -668,7 +668,7 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
}
}
// Faster than Paula plays: every decimate-th sample, at its pitch.
s->decimate = audioDecimationFactor(rateHz, PAULA_MAX_RATE);
s->decimate = jlAudioDecimationFactor(rateHz, PAULA_MAX_RATE);
s->phase = 0u;
s->fill = fill;
s->ctx = ctx;

View file

@ -86,7 +86,6 @@ static void amigaBlitterCopyPlanes(uint8_t *dst[AMIGA_BITPLANES],
static void amigaBlitterCopyPlanesOwned(uint8_t *dst[AMIGA_BITPLANES], uint8_t *src[AMIGA_BITPLANES], uint16_t offsetBytes, uint16_t widthWords, uint16_t height, uint16_t modBytes);
static void amigaBlitterFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height, uint16_t dstModBytes, bool setBits);
static void amigaBlitterMaskedFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height, uint16_t dstModBytes, uint16_t fwm, uint16_t lwm, bool setBits);
static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t color);
static void amigaPresentFlushRun(AmigaPlanarT *pd, UBYTE *destPlanes[AMIGA_BITPLANES], int16_t runFirst, int16_t runLast, uint16_t byteStart, uint16_t byteLen, bool *blitOwned);
static void amigaPresentRectInner(AmigaPlanarT *pd, UBYTE *destPlanes[AMIGA_BITPLANES], int16_t firstRow, int16_t lastRow, uint16_t byteStart, uint16_t bytesPerRow, bool *blitOwned);
static uint8_t bandListUpdate(const jlSurfaceT *src);
@ -105,7 +104,7 @@ static void trampolinePoint(const uint16_t *list);
// ----- Module state -----
struct Screen *gScreen = NULL; // shared with input.c
struct Screen *jlgScreen = NULL; // shared with input.c
static struct BitMap *gBitMap = NULL;
// Phase 10 page-flip: two CHIP plane sets. gPlanes is set A (kept
// under the original name to keep diffs minimal); gPlanesB is set B.
@ -216,7 +215,7 @@ static uint8_t gPrevStageMaxWord[JL_SURFACE_HEIGHT] __attribute__((aligned(2)))
// present scan emits these into gPresentRuns; a separate loop flushes
// each (blitter or CPU per amigaPresentRectInner). Splitting scan from
// flush lets the register-pressure-bound scan move to hand-rolled asm
// (amigaPresentScan68k) without touching the flush path. Field layout
// (jlAmigaPresentScan68k) without touching the flush path. Field layout
// (firstRow@0, lastRow@2, byteStart@4, byteLen@6; 8 bytes) is an ABI
// contract with that asm. At most one run per row, so JL_SURFACE_HEIGHT
// entries always suffice.
@ -236,7 +235,7 @@ static AmigaPresentRunT gPresentRuns[JL_SURFACE_HEIGHT];
// 17. The asm hardcodes JL_SURFACE_HEIGHT, SURFACE_WORDS_PER_ROW, the clean
// sentinel, and the 8-byte run record; the guards below make a mismatch a
// compile error rather than a display bug.
extern uint32_t amigaPresentScan68k(const uint8_t *curMin, const uint8_t *curMax, uint8_t *prvMin, uint8_t *prvMax, AmigaPresentRunT *runList);
extern uint32_t jlAmigaPresentScan68k(const uint8_t *curMin, const uint8_t *curMax, uint8_t *prvMin, uint8_t *prvMax, AmigaPresentRunT *runList);
typedef char amigaPresentScanAbiHeight[(JL_SURFACE_HEIGHT == 200) ? 1 : -1];
typedef char amigaPresentScanAbiWords[(SURFACE_WORDS_PER_ROW == 80) ? 1 : -1];
@ -253,19 +252,15 @@ typedef char amigaPresentScanAbiRec[(sizeof(AmigaPresentRunT) == 8) ? 1 : -1];
// presents still run.
static bool gPrevAnyDirty = true;
// Change detection is the core gStageScbDirty / gStagePaletteDirty flags
// Change detection is the core jlgStageScbDirty / jlgStagePaletteDirty flags
// (surfaceInternal.h): the mutation APIs set them, and jlpPresent clears
// them once it has written the band list they ask for (bandListUpdate).
// ----- Internal helpers (alphabetical) -----
// Blitter geometry shared by every block op here: each plane row is
// AMIGA_BYTES_PER_ROW (40) bytes = 20 words. BLTSIZE packs the row
// count in the high 10 bits (<< 6) and the word count in the low 6
// bits (a width of 0 would mean 64 words, but our rows are 20 wide).
// AMIGA_BYTES_PER_ROW (40) bytes = 20 words (amigaCustom.h has BLTSIZE).
#define AMIGA_BLT_WORDS_PER_ROW (AMIGA_BYTES_PER_ROW / 2u) // 20
#define AMIGA_BLT_SIZESHIFT 6
#define AMIGA_BLT_MASK_ALL 0xFFFFu
// BLTSIZE field guard (P7-3 audit follow-up): height occupies bits
// 15..6 (10-bit == max 1024 rows) and width bits 5..0 (6-bit == max 64
@ -275,14 +270,6 @@ static bool gPrevAnyDirty = true;
// catches a future taller/wider surface before it silently truncates.
typedef char amigaBltSizeFieldsFit[(JL_SURFACE_HEIGHT <= 1024 && AMIGA_BLT_WORDS_PER_ROW <= 64) ? 1 : -1];
// Minterms for the masked solid-fill blit (amigaBlitterMaskedFillOne):
// A is the constant $FFFF cut to the rect by the first/last-word masks,
// C and D both address the destination. Set-bits writes D = A OR C
// (force the masked bits on, preserve the rest); clear-bits writes
// D = (NOT A) AND C (force the masked bits off, preserve the rest).
#define AMIGA_BLT_MINTERM_SET 0xFAu // D = A | C
#define AMIGA_BLT_MINTERM_CLEAR 0x0Au // D = ~A & C
// Blitter-vs-CPU crossover for jlpSurfaceCopyRect, in per-plane words
// (height * widthWords). Below this the OwnBlitter + per-plane
// WaitBlit + register-setup fixed cost (~1,300 CPU cycles) beats a
@ -424,9 +411,9 @@ static uint8_t bandListUpdate(const jlSurfaceT *src) {
if (gTrampoline == NULL) {
copperInstall(src);
if (gCopperReady) {
gStageScbDirty = false;
gStagePaletteDirty = false;
gStagePaletteDirtyMask = 0;
jlgStageScbDirty = false;
jlgStagePaletteDirty = false;
jlgStagePaletteDirtyMask = 0;
}
return 0u;
}
@ -437,7 +424,7 @@ static uint8_t bandListUpdate(const jlSurfaceT *src) {
return 0u;
}
flip = FLIP_PLANES;
} else if (!gStageScbDirty && !gStagePaletteDirty) {
} else if (!jlgStageScbDirty && !jlgStagePaletteDirty) {
return 0u;
}
flipWait(FLIP_LIST);
@ -445,10 +432,10 @@ static uint8_t bandListUpdate(const jlSurfaceT *src) {
if (!bandListWrite(src, back)) {
return flip;
}
gListFront = back;
gStageScbDirty = false;
gStagePaletteDirty = false;
gStagePaletteDirtyMask = 0;
gListFront = back;
jlgStageScbDirty = false;
jlgStagePaletteDirty = false;
jlgStagePaletteDirtyMask = 0;
return (uint8_t)(flip | FLIP_LIST);
}
@ -593,12 +580,12 @@ static void copperInstall(const jlSurfaceT *src) {
CMOVE(ucl, custom.copjmp2, 0);
CEND(ucl);
Forbid();
if (gScreen->ViewPort.UCopIns != NULL) {
FreeVPortCopLists(&gScreen->ViewPort);
if (jlgScreen->ViewPort.UCopIns != NULL) {
FreeVPortCopLists(&jlgScreen->ViewPort);
}
gScreen->ViewPort.UCopIns = ucl;
jlgScreen->ViewPort.UCopIns = ucl;
Permit();
MakeScreen(gScreen);
MakeScreen(jlgScreen);
view = ViewAddress();
Forbid();
MrgCop(view);
@ -610,7 +597,7 @@ static void copperInstall(const jlSurfaceT *src) {
patchTopColors(gListBand0[0]);
} else {
gCopStart[(gCopJump + COPPER_JUMP_STROBE) * COPPER_INSTR_WORDS] = COPPER_NOOP;
gCopperReady = false;
gCopperReady = false;
}
}
LoadView(view);
@ -841,8 +828,8 @@ static void trampolinePoint(const uint16_t *list) {
// reaching the display -- the failure class the golden stage hashes
// cannot see. Reads gFrontIdx's planes while the VBL has not yet made
// the flip a present asked for, and before the first copper install. Always compiled (the probe build links the stock library;
// gPresentCopiedBytes precedent).
void amigaProbePresentPageSums(const jlSurfaceT *src, uint32_t *frontSum, uint32_t *shadowSum) {
// jlgPresentCopiedBytes precedent).
void jlAmigaProbePresentPageSums(const jlSurfaceT *src, uint32_t *frontSum, uint32_t *shadowSum) {
AmigaPlanarT *pd;
const uint8_t *fp;
const uint8_t *sp;
@ -897,7 +884,7 @@ bool jlpInit(const jlConfigT *config) {
// defaults.
gBitMap = (struct BitMap *)AllocMem((ULONG)sizeof(struct BitMap), (ULONG)(MEMF_PUBLIC | MEMF_CLEAR));
if (gBitMap == NULL) {
coreSetError("out of memory for the screen's bitmap");
jlCoreSetError("out of memory for the screen's bitmap");
return false;
}
InitBitMap(gBitMap, (LONG)AMIGA_BITPLANES, (LONG)JL_SURFACE_WIDTH, (LONG)JL_SURFACE_HEIGHT);
@ -910,7 +897,7 @@ bool jlpInit(const jlConfigT *config) {
gPlanesB[i] = (UBYTE *)AllocMem((ULONG)AMIGA_PLANE_SIZE, (ULONG)(MEMF_CHIP | MEMF_CLEAR));
if (gPlanes[i] == NULL || gPlanesB[i] == NULL) {
screenPlanesFree();
coreSetError("not enough chip RAM for the two screen pages");
jlCoreSetError("not enough chip RAM for the two screen pages");
return false;
}
// Set A is the initial front buffer; BPL DMA fetches from it
@ -931,11 +918,11 @@ bool jlpInit(const jlConfigT *config) {
// real planar screen rather than an RTG substitute. SA_BitMap
// makes Intuition use OUR pre-allocated planes; CloseScreen will
// not free them -- our jlpShutdown does.
gScreen = OpenScreenTags(NULL, (ULONG)SA_Width, (ULONG)JL_SURFACE_WIDTH, (ULONG)SA_Height, (ULONG)JL_SURFACE_HEIGHT, (ULONG)SA_Depth, (ULONG)AMIGA_BITPLANES, (ULONG)SA_DisplayID, (ULONG)(PAL_MONITOR_ID | LORES_KEY), (ULONG)SA_DetailPen, (ULONG)0, (ULONG)SA_BlockPen, (ULONG)1, (ULONG)SA_Title, (ULONG)"JoeyLib", (ULONG)SA_Type, (ULONG)CUSTOMSCREEN, (ULONG)SA_Quiet, (ULONG)TRUE, (ULONG)SA_BitMap, (ULONG)gBitMap, TAG_DONE);
jlgScreen = OpenScreenTags(NULL, (ULONG)SA_Width, (ULONG)JL_SURFACE_WIDTH, (ULONG)SA_Height, (ULONG)JL_SURFACE_HEIGHT, (ULONG)SA_Depth, (ULONG)AMIGA_BITPLANES, (ULONG)SA_DisplayID, (ULONG)(PAL_MONITOR_ID | LORES_KEY), (ULONG)SA_DetailPen, (ULONG)0, (ULONG)SA_BlockPen, (ULONG)1, (ULONG)SA_Title, (ULONG)"JoeyLib", (ULONG)SA_Type, (ULONG)CUSTOMSCREEN, (ULONG)SA_Quiet, (ULONG)TRUE, (ULONG)SA_BitMap, (ULONG)gBitMap, TAG_DONE);
if (gScreen == NULL) {
if (jlgScreen == NULL) {
screenPlanesFree();
coreSetError("Intuition could not open the 320x200 PAL screen");
jlCoreSetError("Intuition could not open the 320x200 PAL screen");
return false;
}
// Force COLOR00 to black so the overscan/border region around the
@ -943,7 +930,7 @@ bool jlpInit(const jlConfigT *config) {
// on the first jlStagePresent. Apps that paint a non-black bg need
// do nothing -- the first present's band list loads their palette[0]
// into COLOR00, above the screen too (patchTopColors).
SetRGB4(&gScreen->ViewPort, 0, 0, 0, 0);
SetRGB4(&jlgScreen->ViewPort, 0, 0, 0, 0);
installVblServer();
frameHzMeasure();
// The millisecond clock's E-clock (jlpMillisElapsed); without it the
@ -975,7 +962,7 @@ bool jlpInit(const jlConfigT *config) {
* the copy must move the counter with it. */
static void amigaPresentFlushRun(AmigaPlanarT *pd, UBYTE *destPlanes[AMIGA_BITPLANES], int16_t runFirst, int16_t runLast, uint16_t byteStart, uint16_t byteLen, bool *blitOwned) {
amigaPresentRectInner(pd, destPlanes, runFirst, runLast, byteStart, byteLen, blitOwned);
gPresentCopiedBytes += (uint32_t)(runLast - runFirst + 1) * byteLen * AMIGA_BITPLANES;
jlgPresentCopiedBytes += (uint32_t)(runLast - runFirst + 1) * byteLen * AMIGA_BITPLANES;
}
@ -1093,13 +1080,18 @@ void jlpPresent(const jlSurfaceT *src) {
uint8_t backIdx;
uint8_t flip;
if (src == NULL || gScreen == NULL) {
// Every way out leaves the stage's bands clean (JL_PRESENT_CLEARS_BANDS):
// the union scan clears them as it reads them, the idle early-out finds
// them clean, and the returns before them clear them here.
if (src == NULL || jlgScreen == NULL) {
jlStageDirtyClearAll();
return;
}
flip = bandListUpdate(src);
pd = (AmigaPlanarT *)src->portData;
if (pd == NULL) {
jlStageDirtyClearAll();
flipRequest(flip);
return;
}
@ -1113,7 +1105,7 @@ void jlpPresent(const jlSurfaceT *src) {
// clean row's minWord is the 0xFF CLEAN sentinel and any touched
// row's is <= STAGE_DIRTY_FULL_MAX (79), so 4 clean rows read
// exactly 0xFFFFFFFF -- 50 long tests replace 200 byte tests on
// the common all-clean idle frame (gStageMinWord is aligned(2),
// the common all-clean idle frame (jlgStageMinWord is aligned(2),
// all the 68000 needs for move.l). A failing group byte-walks its
// 4 rows with the same per-row test as before, so the first-dirty
// row and the scanned-rows accounting are exactly the
@ -1123,7 +1115,7 @@ void jlpPresent(const jlSurfaceT *src) {
// guards that), preserving the 2-buffer staleness convergence.
if (!gPrevAnyDirty) {
typedef uint32_t __attribute__((may_alias)) BandGroupT;
const BandGroupT *minGroups = (const BandGroupT *)(const void *)gStageMinWord;
const BandGroupT *minGroups = (const BandGroupT *)(const void *)jlgStageMinWord;
bool anyDirty = false;
uint8_t g;
@ -1143,7 +1135,7 @@ void jlpPresent(const jlSurfaceT *src) {
}
}
}
gPresentScannedRows += (uint32_t)(anyDirty ? (y + 1) : JL_SURFACE_HEIGHT);
jlgPresentScannedRows += (uint32_t)(anyDirty ? (y + 1) : JL_SURFACE_HEIGHT);
if (!anyDirty) {
flipRequest(flip);
return;
@ -1184,11 +1176,11 @@ void jlpPresent(const jlSurfaceT *src) {
// register. Returns the run count (low 16) plus anyUnionDirty (bit
// 16) and anyCurDirty (bit 17) -- the two flags the flip and
// idle-early-out logic below need.
scanRes = amigaPresentScan68k(gStageMinWord, gStageMaxWord, gPrevStageMinWord, gPrevStageMaxWord, gPresentRuns);
scanRes = jlAmigaPresentScan68k(jlgStageMinWord, jlgStageMaxWord, gPrevStageMinWord, gPrevStageMaxWord, gPresentRuns);
numRuns = (uint16_t)scanRes;
anyUnionDirty = (scanRes & 0x00010000UL) != 0u;
anyCurDirty = (scanRes & 0x00020000UL) != 0u;
gPresentScannedRows += JL_SURFACE_HEIGHT;
jlgPresentScannedRows += JL_SURFACE_HEIGHT;
// Flush the collected runs. Kept separate from the scan so the
// scan lives in hand-rolled asm (presentScan.s) without touching the
@ -1230,8 +1222,8 @@ void jlpPresent(const jlSurfaceT *src) {
// now folded into the scan loop above, eliminating the second
// 200-row pass this block used to run. gPrevAnyDirty records whether
// the CURRENT frame touched any row so the next present can take the
// idle-frame early-out. jlStagePresent clears the live cur bands
// after jlpPresent returns; the fused snapshot already captured them.
// idle-frame early-out. The scan has left the live cur bands clean
// (JL_PRESENT_CLEARS_BANDS); the fused snapshot captured them first.
gPrevAnyDirty = anyCurDirty;
}
@ -1419,16 +1411,16 @@ void jlpShutdown(void) {
// Tear down the VBL server before closing the screen so the
// interrupt chain is clean if anything else is watching.
removeVblServer();
if (gScreen != NULL) {
if (jlgScreen != NULL) {
// CloseScreen should free attached UCopList, but be explicit
// to catch any case where the screen close path skips it.
Forbid();
if (gScreen->ViewPort.UCopIns != NULL) {
FreeVPortCopLists(&gScreen->ViewPort);
if (jlgScreen->ViewPort.UCopIns != NULL) {
FreeVPortCopLists(&jlgScreen->ViewPort);
}
Permit();
CloseScreen(gScreen);
gScreen = NULL;
CloseScreen(jlgScreen);
jlgScreen = NULL;
// Intuition loads a display without the screen, and its jump to
// the band lists, at the next VBL: past that, nothing runs them.
WaitTOF();
@ -1447,15 +1439,14 @@ void jlpShutdown(void) {
// four separate plane buffers of 40 bytes per row, so a span of pixels is the
// same bytes in the same places. src/m68k/planarFill.s does the area fills,
// src/m68k/planarPlot.s the pixel walkers.
extern void surface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color);
extern void surface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color);
extern void jlSurface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color);
// src/m68k/planarPlot.s, shared with the X68000: Bresenham walkers with 16-way
// colour dispatch, so every plane write is one memory read-modify-write with
// the set-or-clear choice already made. Both are bit-identical to draw.c's own
// walkers. Straight lines never reach jlpDrawLine -- draw.c diverts those to
// jlFillRect -- so the line body walks the general diagonal case only.
extern void surface68kPlanarCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color);
extern void surface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color);
extern void jlSurface68kPlanarCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color);
extern void jlSurface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color);
// Amiga is pure planar (s->pixels is NULL). Each jlp<Op> below does the planar
@ -1476,7 +1467,7 @@ void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
colorIndex = (uint8_t)(doubled & 0x0Fu);
pd = (AmigaPlanarT *)s->portData;
if (!pd->chip) {
surface68kFillRect(pd->planes, 0u, 0u, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, colorIndex);
jlSurface68kFillRect(pd->planes, 0u, 0u, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, colorIndex);
return;
}
@ -1532,7 +1523,7 @@ void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint
return;
}
if (!pd->chip) {
surface68kFillRect(pd->planes, (uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h, colorIndex);
jlSurface68kFillRect(pd->planes, (uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h, colorIndex);
return;
}
@ -1594,198 +1585,6 @@ void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint
}
// Phase 9 sprite save/restore plane-backup hooks.
// Sprite save at (x, y, w, h) records the destination region into the
// caller's backup buffer; restore writes it back. x and w are 2-pixel
// aligned by cross-platform code.
//
// Backup layout matches the CHUNKY-packed format the cross-platform
// contract sizes for ((widthTiles*4)+1)*(heightTiles*8) bytes (see
// include/joey/sprite.h): one byte per pixel-pair, 2 pixels/byte,
// high nibble = even (left) pixel, low nibble = odd (right) pixel,
// (w/2) bytes per row, (w/2)*h bytes total. This is the same format
// the Atari ST port uses and never overruns the contract buffer --
// unlike a plane-major layout, which would round x down to an 8-pixel
// boundary and width up and then write (4*roundedW/8)*h = up to
// (w/2 + 3)*h bytes for an odd-of-8 x, overrunning the buffer. Each
// pixel's 4bpp color is assembled from / written to the 4 plane bits
// at byte x>>3, bit 0x80 >> (x & 7).
void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
const AmigaPlanarT *pd;
uint16_t pairs;
uint16_t row;
uint16_t pair;
uint8_t *dst;
pd = (const AmigaPlanarT *)s->portData;
if (pd == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) {
return;
}
/* Byte-aligned fast path: raw plane-byte copy, no per-pixel c2p.
* When x and w are both 8-px aligned and the rect is fully on
* surface, each sprite tile column maps to whole plane bytes, so we
* save the raw bytes -- ~8x cheaper than the chunky walker below and
* an exact round-trip on restore (the sprite fills its bytes, so
* restoring whole bytes touches no neighbor pixels). The backup row
* holds plane0..plane3, each `wb` bytes, contiguous. The chunky
* walker below stays as the arbitrary-x fallback. */
if ((x & 7) == 0 && (w & 7) == 0 && x >= 0 && (x + (int16_t)w) <= JL_SURFACE_WIDTH && y >= 0 && (y + (int16_t)h) <= JL_SURFACE_HEIGHT) {
uint16_t wb;
uint16_t wb2;
uint16_t wb3;
uint16_t startByte;
uint16_t rowOff;
uint8_t *bdst;
uint16_t col;
wb = (uint16_t)(w >> 3);
wb2 = (uint16_t)(wb << 1);
wb3 = (uint16_t)(wb2 + wb);
startByte = (uint16_t)((uint16_t)x >> 3);
rowOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + startByte);
bdst = dstPlaneBytes;
for (row = 0; row < (uint16_t)h; row++) {
const uint8_t *s0 = pd->planes[0] + rowOff;
const uint8_t *s1 = pd->planes[1] + rowOff;
const uint8_t *s2 = pd->planes[2] + rowOff;
const uint8_t *s3 = pd->planes[3] + rowOff;
for (col = 0; col < wb; col++) {
bdst[col] = s0[col];
bdst[wb + col] = s1[col];
bdst[wb2 + col] = s2[col];
bdst[wb3 + col] = s3[col];
}
bdst += (uint16_t)(wb << 2);
rowOff += AMIGA_BYTES_PER_ROW;
}
return;
}
pairs = (uint16_t)(w >> 1);
for (row = 0; row < (uint16_t)h; row++) {
const uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
const uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
const uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
const uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
dst = dstPlaneBytes + (uint16_t)(row * pairs);
for (pair = 0; pair < pairs; pair++) {
uint16_t px = (uint16_t)(x + (int16_t)(pair << 1));
uint16_t byteOff = (uint16_t)(px >> 3);
uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u));
uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u));
uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3);
uint8_t hi = 0u;
uint8_t lo = 0u;
if (p0[byteOff] & maskHi) {
hi = (uint8_t)(hi | 0x01u);
}
if (p1[byteOff] & maskHi) {
hi = (uint8_t)(hi | 0x02u);
}
if (p2[byteOff] & maskHi) {
hi = (uint8_t)(hi | 0x04u);
}
if (p3[byteOff] & maskHi) {
hi = (uint8_t)(hi | 0x08u);
}
if (p0[byteOffLo] & maskLo) {
lo = (uint8_t)(lo | 0x01u);
}
if (p1[byteOffLo] & maskLo) {
lo = (uint8_t)(lo | 0x02u);
}
if (p2[byteOffLo] & maskLo) {
lo = (uint8_t)(lo | 0x04u);
}
if (p3[byteOffLo] & maskLo) {
lo = (uint8_t)(lo | 0x08u);
}
dst[pair] = (uint8_t)((hi << 4) | lo);
}
}
}
void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
AmigaPlanarT *pd;
uint16_t pairs;
uint16_t row;
uint16_t pair;
const uint8_t *src;
pd = (AmigaPlanarT *)s->portData;
if (pd == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) {
return;
}
/* Byte-aligned fast path mirroring jlpSpriteSavePlanes: writes the
* raw plane bytes straight back (whole-byte round-trip), skipping
* the per-pixel chunky->planar masking of the walker below. */
if ((x & 7) == 0 && (w & 7) == 0 && x >= 0 && (x + (int16_t)w) <= JL_SURFACE_WIDTH && y >= 0 && (y + (int16_t)h) <= JL_SURFACE_HEIGHT) {
uint16_t wb;
uint16_t wb2;
uint16_t wb3;
uint16_t startByte;
uint16_t rowOff;
const uint8_t *bsrc;
uint16_t col;
wb = (uint16_t)(w >> 3);
wb2 = (uint16_t)(wb << 1);
wb3 = (uint16_t)(wb2 + wb);
startByte = (uint16_t)((uint16_t)x >> 3);
rowOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + startByte);
bsrc = srcPlaneBytes;
for (row = 0; row < (uint16_t)h; row++) {
uint8_t *d0 = pd->planes[0] + rowOff;
uint8_t *d1 = pd->planes[1] + rowOff;
uint8_t *d2 = pd->planes[2] + rowOff;
uint8_t *d3 = pd->planes[3] + rowOff;
for (col = 0; col < wb; col++) {
d0[col] = bsrc[col];
d1[col] = bsrc[wb + col];
d2[col] = bsrc[wb2 + col];
d3[col] = bsrc[wb3 + col];
}
bsrc += (uint16_t)(wb << 2);
rowOff += AMIGA_BYTES_PER_ROW;
}
return;
}
pairs = (uint16_t)(w >> 1);
for (row = 0; row < (uint16_t)h; row++) {
uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
src = srcPlaneBytes + (uint16_t)(row * pairs);
for (pair = 0; pair < pairs; pair++) {
uint8_t b = src[pair];
uint8_t hi = (uint8_t)(b >> 4);
uint8_t lo = (uint8_t)(b & 0x0Fu);
uint16_t px = (uint16_t)(x + (int16_t)(pair << 1));
uint16_t byteOff = (uint16_t)(px >> 3);
uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u));
uint8_t notHi = (uint8_t)(~maskHi);
uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3);
uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u));
uint8_t notLo = (uint8_t)(~maskLo);
p0[byteOff] = (uint8_t)((p0[byteOff] & notHi) | ((hi & 0x01u) ? maskHi : 0u));
p1[byteOff] = (uint8_t)((p1[byteOff] & notHi) | ((hi & 0x02u) ? maskHi : 0u));
p2[byteOff] = (uint8_t)((p2[byteOff] & notHi) | ((hi & 0x04u) ? maskHi : 0u));
p3[byteOff] = (uint8_t)((p3[byteOff] & notHi) | ((hi & 0x08u) ? maskHi : 0u));
p0[byteOffLo] = (uint8_t)((p0[byteOffLo] & notLo) | ((lo & 0x01u) ? maskLo : 0u));
p1[byteOffLo] = (uint8_t)((p1[byteOffLo] & notLo) | ((lo & 0x02u) ? maskLo : 0u));
p2[byteOffLo] = (uint8_t)((p2[byteOffLo] & notLo) | ((lo & 0x04u) ? maskLo : 0u));
p3[byteOffLo] = (uint8_t)((p3[byteOffLo] & notLo) | ((lo & 0x08u) ? maskLo : 0u));
}
}
}
// The sprite interpreter (uncompiled sprites, and compiled ones drawn partly
// off the surface): the planar ports' shared walker over the sprite's
// planar cache, src/m68k/planarSprite.c.
@ -1796,7 +1595,7 @@ void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
if (pd == NULL) {
return;
}
planarSpriteDraw(pd->planes, sp, x, y);
jlPlanarSpriteDraw(pd->planes, sp, x, y);
}
@ -1884,76 +1683,13 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint
}
// Phase 4 planar dual-write helper: set one pixel's bit in each of
// the four shadow planes. Caller (the per-primitive walker below) has
// already validated (x, y) is on-surface. byteOff is computed once
// and reused across all four planes since stride is the same in each.
static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t color) {
uint16_t byteOff;
uint8_t bitMask;
uint8_t notMask;
uint8_t *p0;
uint8_t *p1;
uint8_t *p2;
uint8_t *p3;
byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3));
bitMask = (uint8_t)(0x80u >> ((uint16_t)x & 7u));
notMask = (uint8_t)~bitMask;
/* Unroll the 4-plane loop. Loop counter + array indexing inside
* the hot per-pixel path is the gating cost on circle outlines:
* UBER jlDrawCircle r=80 calls this ~640 times per call. */
p0 = &pd->planes[0][byteOff];
p1 = &pd->planes[1][byteOff];
p2 = &pd->planes[2][byteOff];
p3 = &pd->planes[3][byteOff];
if (color & 0x01u) {
*p0 = (uint8_t)(*p0 | bitMask);
} else {
*p0 = (uint8_t)(*p0 & notMask);
}
if (color & 0x02u) {
*p1 = (uint8_t)(*p1 | bitMask);
} else {
*p1 = (uint8_t)(*p1 & notMask);
}
if (color & 0x04u) {
*p2 = (uint8_t)(*p2 | bitMask);
} else {
*p2 = (uint8_t)(*p2 & notMask);
}
if (color & 0x08u) {
*p3 = (uint8_t)(*p3 | bitMask);
} else {
*p3 = (uint8_t)(*p3 & notMask);
}
}
void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
AmigaPlanarT *pd;
uint8_t nibLo;
if (s->portData == NULL) {
jlpGenericDrawPixel(s, x, y, colorIndex);
return;
}
nibLo = (uint8_t)(colorIndex & 0x0Fu);
pd = (AmigaPlanarT *)s->portData;
amigaPlanarSetPixel(pd, (int16_t)x, (int16_t)y, nibLo);
}
void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
AmigaPlanarT *pd;
if (s->portData == NULL) {
return;
}
pd = (AmigaPlanarT *)s->portData;
surface68kPlanarLine(pd->planes, x0, y0, x1, y1, (uint16_t)(colorIndex & 0x0Fu));
jlSurface68kPlanarLine(pd->planes, x0, y0, x1, y1, (uint16_t)(colorIndex & 0x0Fu));
}
@ -1963,7 +1699,7 @@ void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t co
return;
}
pd = (AmigaPlanarT *)s->portData;
surface68kPlanarCircle(pd->planes, (uint16_t)cx, (uint16_t)cy, r, (uint16_t)(colorIndex & 0x0Fu));
jlSurface68kPlanarCircle(pd->planes, (uint16_t)cx, (uint16_t)cy, r, (uint16_t)(colorIndex & 0x0Fu));
}
@ -1988,7 +1724,7 @@ bool jlpFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t co
if (s->portData == NULL) {
return false;
}
surface68kFillCircle(((AmigaPlanarT *)s->portData)->planes, (uint16_t)cx, (uint16_t)cy, r, colorIndex);
jlSurface68kFillCircle(((AmigaPlanarT *)s->portData)->planes, (uint16_t)cx, (uint16_t)cy, r, colorIndex);
return true;
}
@ -2144,132 +1880,12 @@ bool jlpFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, i
}
/* ===== Phase 9 reader hooks: pure-planar Amiga implementations =====
*
* Cross-platform code that USED to read s->pixels (chunky shadow) now
* goes through these. On Amiga the chunky shadow doesn't exist;
* pixels are derived on demand by walking the plane bits.
*
* Per-pixel assembly: for pixel (x, y), read bit (7 - x%8) from plane
* byte at row*40 + x/8 in each of the 4 planes. Color index = sum
* of (bit_p << p). */
uint8_t jlpSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
AmigaPlanarT *pd;
uint16_t byteOff;
uint8_t bitMask;
uint8_t color;
uint16_t i;
pd = (AmigaPlanarT *)s->portData;
if (pd == NULL) {
return 0u;
}
byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3));
bitMask = (uint8_t)(0x80u >> ((uint16_t)x & 7u));
color = 0u;
for (i = 0; i < AMIGA_BITPLANES; i++) {
if (pd->planes[i][byteOff] & bitMask) {
color = (uint8_t)(color | (1u << i));
}
}
return color;
}
/* Reverse-c2p: per row, derive 160 chunky bytes from 40 plane bytes
* (per plane, 4 planes). Used by jlpSurfaceHash to fold the planar
* surface into the same byte-stream the chunky ports hash, so cross-
* port hash comparisons stay valid.
* Walks 8 pixels per planar-byte column; per pixel assembles nibble
* from 4 plane bits. Output: 4 chunky bytes per planar-byte column
* (since 8 pixels = 4 chunky bytes at 2px/byte). */
static void amigaPlanesToChunkyRow(const AmigaPlanarT *pd, int16_t y, uint8_t *dstChunkyRow) {
uint16_t col;
uint16_t byteOff;
uint8_t b0;
uint8_t b1;
uint8_t b2;
uint8_t b3;
uint8_t pix;
uint8_t bitMask;
uint16_t p;
for (col = 0; col < AMIGA_BYTES_PER_ROW; col++) {
byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + col);
b0 = pd->planes[0][byteOff];
b1 = pd->planes[1][byteOff];
b2 = pd->planes[2][byteOff];
b3 = pd->planes[3][byteOff];
/* For each of 8 pixels in this planar byte (bit 7 = leftmost),
* assemble nibble from the 4 plane bits and pack into chunky
* bytes (high nibble = even pixel, low nibble = odd pixel). */
for (p = 0; p < 8u; p++) {
bitMask = (uint8_t)(0x80u >> p);
pix = 0u;
if (b0 & bitMask) {
pix = (uint8_t)(pix | 1u);
}
if (b1 & bitMask) {
pix = (uint8_t)(pix | 2u);
}
if (b2 & bitMask) {
pix = (uint8_t)(pix | 4u);
}
if (b3 & bitMask) {
pix = (uint8_t)(pix | 8u);
}
if ((p & 1u) == 0u) {
dstChunkyRow[col * 4u + (p >> 1)] = (uint8_t)(pix << 4);
} else {
dstChunkyRow[col * 4u + (p >> 1)] = (uint8_t)(dstChunkyRow[col * 4u + (p >> 1)] | pix);
}
}
}
}
// The shared planar hash (src/m68k/planarHash.c), over separate planes.
uint32_t jlpSurfaceHash(const jlSurfaceT *s) {
AmigaPlanarT *pd;
uint16_t lo = 0xACE1u;
uint16_t hi = 0x1357u;
uint16_t n;
uint16_t v;
int16_t row;
uint8_t b;
uint8_t chunkyRow[JL_SURFACE_BYTES_PER_ROW];
const uint16_t *w;
const AmigaPlanarT *pd;
pd = (AmigaPlanarT *)s->portData;
if (pd == NULL) {
return 0u;
}
/* Pixel hash: derive chunky one row at a time, fold byte-by-byte
* via the shared SURFACE_HASH_MIX_BYTE so cross-port hash
* matches. */
for (row = 0; row < JL_SURFACE_HEIGHT; row++) {
amigaPlanesToChunkyRow(pd, row, chunkyRow);
for (n = 0; n < JL_SURFACE_BYTES_PER_ROW; n++) {
b = chunkyRow[n];
SURFACE_HASH_MIX_BYTE(lo, hi, b);
}
}
// SCB: byte sequence, endian-independent.
for (n = 0; n < (uint16_t)JL_SURFACE_HEIGHT; n++) {
b = s->scb[n];
SURFACE_HASH_MIX_BYTE(lo, hi, b);
}
/* Palette: read uint16 values, fold high-then-low for endian-
* independence. */
w = &s->palette[0][0];
for (n = 0; n < (uint16_t)JL_SURFACE_PALETTE_ENTRIES; n++) {
v = *w++;
b = (uint8_t)((v >> 8) & 0xFFu);
SURFACE_HASH_MIX_BYTE(lo, hi, b);
b = (uint8_t)(v & 0xFFu);
SURFACE_HASH_MIX_BYTE(lo, hi, b);
}
return ((uint32_t)hi << 16) | (uint32_t)lo;
pd = (const AmigaPlanarT *)s->portData;
return jlPlanarSurfaceHash(s, (const uint8_t *const *)pd->planes, AMIGA_BYTES_PER_ROW, PLANAR_HASH_SEPARATE_PAIR_STEP);
}
@ -2313,7 +1929,7 @@ uint8_t *jlpStageAllocPixels(void) {
/* Phase 9: Amiga has no chunky shadow. The stage pixels pointer
* stays NULL; cross-platform code reads pixels via jlpSamplePixel
* (or the other chunky reader ops) which read from planes. NULL is
* a valid return -- cross-platform stageAlloc treats NULL as
* a valid return -- cross-platform jlStageAlloc treats NULL as
* "port has no chunky storage" and skips the chunky memset. */
return NULL;
}

View file

@ -76,7 +76,7 @@ struct Library *LowLevelBase = NULL;
// ----- External from hal.c -----
extern struct Screen *gScreen;
extern struct Screen *jlgScreen;
// ----- Empty pointer image -----
//
@ -192,16 +192,19 @@ static bool gJoyLocked[JL_JOYSTICK_COUNT];
// lowlevel.library reports a one-button stick and a two-button one alike
// (a second button is pin 9, which reads as released until it is pressed),
// and most Amiga owners use one-button Atari sticks. So a joystick counts as
// one-button (gJoyOneButton: a hold of its button is button 1) until its
// one-button (jlgJoyOneButton: a hold of its button is button 1) until its
// second button has been seen pressed, and again once its port has no
// controller. A CD32 game controller always has both.
static bool gJoySecondButton[JL_JOYSTICK_COUNT];
// Keys and mouse buttons as Intuition's messages leave them, and pressed
// since the last poll: a tap between two polls still reads down for one
// (inputLatch).
// (jlInputLatch). The keys are latched only when a message changed them or
// the last latch showed a tap that must now go up.
static uint8_t gKeyHeld [JL_KEY_COUNT];
static uint8_t gKeyTapped [JL_KEY_COUNT];
static bool gKeysChanged;
static bool gKeyTapShown;
static uint8_t gMouseHeld [JL_MOUSE_BUTTON_COUNT];
static uint8_t gMouseTapped [JL_MOUSE_BUTTON_COUNT];
@ -266,23 +269,25 @@ static void drainMessages(void) {
if (!isRelease) {
gKeyTapped[key] = 1u;
}
gKeysChanged = true;
}
// MapRawKey returns -1 on overflow (composition longer
// than the buffer) -- nothing usable, skip.
// A Ctrl chord types nothing (inputCharTyped); Alt is a
// A Ctrl chord types nothing (jlInputCharTyped); Alt is a
// layer of the keymap's characters.
for (mapIdx = 0; mapIdx < mapCount; mapIdx++) {
inputCharTyped((uint8_t)mapBuf[mapIdx], mapChord);
jlInputCharTyped((uint8_t)mapBuf[mapIdx], mapChord);
}
break;
case IDCMP_MOUSEMOVE:
inputMouseMove(msgMouseX, msgMouseY);
jlInputMouseMove(msgMouseX, msgMouseY);
break;
case IDCMP_INACTIVEWINDOW:
// The keys and buttons it lets go elsewhere are never
// reported here: let them all go now.
memset(gKeyHeld, 0, sizeof(gKeyHeld));
memset(gMouseHeld, 0, sizeof(gMouseHeld));
gKeysChanged = true;
break;
case IDCMP_MOUSEBUTTONS:
switch (msgCode) {
@ -308,7 +313,7 @@ static void drainMessages(void) {
break;
}
// MOUSEBUTTONS messages also carry the mouse position.
inputMouseMove(msgMouseX, msgMouseY);
jlInputMouseMove(msgMouseX, msgMouseY);
break;
default:
break;
@ -357,40 +362,40 @@ static void pollJoysticks(void) {
gJoyLocked[stick] = SetJoyPortAttrs(gAmigaPortForStick[stick], SJA_Type, SJA_TYPE_JOYSTK, TAG_DONE) != FALSE;
}
if (type != JP_TYPE_JOYSTK && type != JP_TYPE_GAMECTLR) {
gJoyConnected[stick] = false;
gJoyAxisX[stick] = 0;
gJoyAxisY[stick] = 0;
gJoyOneButton[stick] = false;
gJoySecondButton[stick] = false;
gJoyButtonState[stick][JL_JOY_BUTTON_0] = false;
gJoyButtonState[stick][JL_JOY_BUTTON_1] = false;
jlgJoyConnected[stick] = false;
jlgJoyAxisX[stick] = 0;
jlgJoyAxisY[stick] = 0;
jlgJoyOneButton[stick] = false;
gJoySecondButton[stick] = false;
jlgJoyButtonState[stick][JL_JOY_BUTTON_0] = false;
jlgJoyButtonState[stick][JL_JOY_BUTTON_1] = false;
continue;
}
gJoyConnected[stick] = true;
jlgJoyConnected[stick] = true;
gJoyAxisX[stick] = 0;
jlgJoyAxisX[stick] = 0;
if (state & JPF_JOY_LEFT) {
gJoyAxisX[stick] = JL_JOYSTICK_AXIS_MIN;
jlgJoyAxisX[stick] = JL_JOYSTICK_AXIS_MIN;
}
if (state & JPF_JOY_RIGHT) {
gJoyAxisX[stick] = JL_JOYSTICK_AXIS_MAX;
jlgJoyAxisX[stick] = JL_JOYSTICK_AXIS_MAX;
}
gJoyAxisY[stick] = 0;
jlgJoyAxisY[stick] = 0;
if (state & JPF_JOY_UP) {
gJoyAxisY[stick] = JL_JOYSTICK_AXIS_MIN;
jlgJoyAxisY[stick] = JL_JOYSTICK_AXIS_MIN;
}
if (state & JPF_JOY_DOWN) {
gJoyAxisY[stick] = JL_JOYSTICK_AXIS_MAX;
jlgJoyAxisY[stick] = JL_JOYSTICK_AXIS_MAX;
}
// Standard 1-button stick reports JPF_BUTTON_RED only. CD32
// game controllers also report BLUE for the second face button.
gJoyButtonState[stick][JL_JOY_BUTTON_0] = (state & JPF_BUTTON_RED) != 0;
gJoyButtonState[stick][JL_JOY_BUTTON_1] = (state & JPF_BUTTON_BLUE) != 0;
jlgJoyButtonState[stick][JL_JOY_BUTTON_0] = (state & JPF_BUTTON_RED) != 0;
jlgJoyButtonState[stick][JL_JOY_BUTTON_1] = (state & JPF_BUTTON_BLUE) != 0;
if (type == JP_TYPE_GAMECTLR || (state & JPF_BUTTON_BLUE) != 0) {
gJoySecondButton[stick] = true;
}
gJoyOneButton[stick] = !gJoySecondButton[stick];
jlgJoyOneButton[stick] = !gJoySecondButton[stick];
}
}
@ -405,21 +410,22 @@ void jlpJoystickReset(jlJoystickE js) {
void jlpInputInit(void) {
memset(gKeyState, 0, sizeof(gKeyState));
memset(gKeyPrev, 0, sizeof(gKeyPrev));
memset(jlgKeyState, 0, sizeof(jlgKeyState));
memset(jlgKeyPrev, 0, sizeof(jlgKeyPrev));
memset(gKeyHeld, 0, sizeof(gKeyHeld));
memset(gKeyTapped, 0, sizeof(gKeyTapped));
gKeysChanged = true;
memset(gMouseHeld, 0, sizeof(gMouseHeld));
memset(gMouseTapped, 0, sizeof(gMouseTapped));
if (gScreen == NULL) {
if (jlgScreen == NULL) {
return;
}
gWindow = OpenWindowTags(NULL, (ULONG)WA_CustomScreen, (ULONG)gScreen, (ULONG)WA_Left, (ULONG)0, (ULONG)WA_Top, (ULONG)0, (ULONG)WA_Width, (ULONG)gScreen->Width, (ULONG)WA_Height, (ULONG)gScreen->Height, (ULONG)WA_Flags, (ULONG)(WFLG_BACKDROP | WFLG_BORDERLESS | WFLG_ACTIVATE | WFLG_RMBTRAP | WFLG_NOCAREREFRESH | WFLG_REPORTMOUSE), (ULONG)WA_IDCMP, (ULONG)(IDCMP_RAWKEY | IDCMP_MOUSEMOVE | IDCMP_MOUSEBUTTONS | IDCMP_INACTIVEWINDOW), TAG_DONE);
gWindow = OpenWindowTags(NULL, (ULONG)WA_CustomScreen, (ULONG)jlgScreen, (ULONG)WA_Left, (ULONG)0, (ULONG)WA_Top, (ULONG)0, (ULONG)WA_Width, (ULONG)jlgScreen->Width, (ULONG)WA_Height, (ULONG)jlgScreen->Height, (ULONG)WA_Flags, (ULONG)(WFLG_BACKDROP | WFLG_BORDERLESS | WFLG_ACTIVATE | WFLG_RMBTRAP | WFLG_NOCAREREFRESH | WFLG_REPORTMOUSE), (ULONG)WA_IDCMP, (ULONG)(IDCMP_RAWKEY | IDCMP_MOUSEMOVE | IDCMP_MOUSEBUTTONS | IDCMP_INACTIVEWINDOW), TAG_DONE);
if (gWindow != NULL) {
gMouseAttached = true; // Intuition delivers IDCMP mouse events on this window
jlgMouseAttached = true; // Intuition delivers IDCMP mouse events on this window
gEmptyPointer = (UWORD *)AllocMem((ULONG)EMPTY_POINTER_BYTES, (ULONG)(MEMF_CHIP | MEMF_CLEAR));
if (gEmptyPointer != NULL) {
@ -442,8 +448,11 @@ void jlpInputInit(void) {
void jlpInputPoll(void) {
drainMessages();
inputLatch(gKeyState, gKeyHeld, gKeyTapped, JL_KEY_COUNT);
inputLatch(gMouseButtonState, gMouseHeld, gMouseTapped, JL_MOUSE_BUTTON_COUNT);
if (gKeysChanged || gKeyTapShown) {
gKeysChanged = false;
gKeyTapShown = jlInputLatch(jlgKeyState, gKeyHeld, gKeyTapped, JL_KEY_COUNT);
}
jlInputLatch(jlgMouseButtonState, gMouseHeld, gMouseTapped, JL_MOUSE_BUTTON_COUNT);
pollJoysticks();
}

View file

@ -9,11 +9,12 @@
// "<name> failed to load" to the CLI and exits(20) before main() runs.
//
// That behavior kills the examples on AROS, which does not provide
// locale.library as a disk library. libnix unconditionally pulls the
// locale.library entry into the auto-open list because C runtime
// utilities (e.g. tzset via time()) take LocaleBase as an argument to
// ConvToLower -- even when the runtime does not actually need a real
// Locale to run.
// locale.library as a disk library, nor does a Kickstart 3.1 machine whose
// boot disk lacks LIBS:locale.library. A program that uses the time
// (time(), localtime()) has locale.library on that list: libnix's tzset,
// which its start-up runs, reads the time zone from the locale. It checks
// LocaleBase first, so with the library missing it leaves the time zone at
// GMT.
//
// Our replacement walks the same list but records NULL for any
// library that cannot be opened, then continues. Code that actually
@ -72,14 +73,3 @@ void __exitlibraries(void) {
ADD2INIT(__initlibraries, -79);
ADD2EXIT(__exitlibraries, -79);
// libnix's tzset dereferences LocaleBase to call locale.library's
// ConvToLower. On AROS (where locale.library is unavailable), our
// tolerant __initlibraries leaves LocaleBase NULL, which would crash
// the first time time() is called (via tzset). Our pattern example
// uses time() for the 5-second display; override tzset with a no-op
// so time() can run. Difference-based timing is unaffected because
// any DST correction applied consistently cancels out.
void tzset(void) {
}

View file

@ -17,6 +17,9 @@
| * dirty row: snapshot gPrev[y] <- cur[y], accumulate anyCurDirty,
| fold union(cur,prev) into a plane-byte span, clamp, then coalesce
| consecutive same-span rows into runs.
| and leaves the stage's bands clean as it reads them, so jlStagePresent
| does not clear them again (JL_PRESENT_CLEARS_BANDS): a clean group is
| clean already, and a dirty one's four rows are reset, as longs, once read.
| Runs are emitted into the caller's gPresentRuns[] as 8-byte records
| {firstRow.w, lastRow.w, byteStart.w, byteLen.w}; the caller flushes
| them (blitter or CPU). lastRow is NOT tracked in a register: a run is
@ -30,7 +33,7 @@
| ABI: cdecl. d2-d7/a2-a6 callee-save. Byte loads use moveq #0 first
| (partial-register-write hygiene -- the fold and emit do word math).
|
| uint32_t amigaPresentScan68k(const uint8_t *curMin, const uint8_t *curMax,
| uint32_t jlAmigaPresentScan68k(const uint8_t *curMin, const uint8_t *curMax,
| uint8_t *prvMin, uint8_t *prvMax,
| AmigaPresentRunT *runList);
|
@ -39,7 +42,7 @@
| bit 17 = anyCurDirty (current frame touched >=1 row).
|
| Register allocation across the row loop:
| a0 = gStageMinWord walker a1 = gStageMaxWord walker
| a0 = jlgStageMinWord walker a1 = jlgStageMaxWord walker
| a2 = gPrevStageMinWord (r/w) a3 = gPrevStageMaxWord (r/w)
| a4 = run-list write cursor a5 = run-list base (for the count)
| d2 = y (row index 0..199) d4 = runFirst (-1.w == no open run)
@ -49,9 +52,9 @@
.text
.globl _amigaPresentScan68k
.globl _jlAmigaPresentScan68k
_amigaPresentScan68k:
_jlAmigaPresentScan68k:
movem.l %d2-%d7/%a2-%a6,-(%sp) | 44 bytes saved
move.l 48(%sp),%a0 | curMin base
move.l 52(%sp),%a1 | curMax base
@ -157,10 +160,8 @@ _amigaPresentScan68k:
cmp.w #200,%d2
bge .scanEnd
move.l (%a0),%d0 | 4 cur min bytes
addq.l #1,%d0 | 0xFFFFFFFF+1 == 0 <=> all clean
bne .groupDirty
move.l (%a2),%d0 | 4 prev min bytes
addq.l #1,%d0
and.l (%a2),%d0 | and the 4 prev ones
addq.l #1,%d0 | 0xFFFFFFFF+1 == 0 <=> all clean in both
bne .groupDirty
| all 4 rows clean-clean: close any open run, skip 4
tst.w %d4
@ -182,11 +183,17 @@ _amigaPresentScan68k:
.groupDirty:
| Some row in the group is touched (cur or prev): process
| all 4 rows with the exact old one-row logic, fall-through.
| all 4 rows with the exact old one-row logic, fall-through,
| then leave the stage's four rows clean for the next frame
| (JL_PRESENT_CLEARS_BANDS), as longs.
PS_ONE_ROW
PS_ONE_ROW
PS_ONE_ROW
PS_ONE_ROW
moveq #-1,%d0
move.l %d0,-4(%a0)
moveq #0,%d0
move.l %d0,-4(%a1)
bra .groupLoop
.scanEnd:

View file

@ -231,10 +231,10 @@ void jlpSerialFlush(void) {
// Why the last jlpSerialOpen failed, for host-side diagnosis: a silent false was unattributable
// through three swallowing layers (this file -> core gate -> the client's "non-fatal" open).
// stage: 1=CreateMsgPort 2=CreateIORequest 3=OpenDevice 4=SDCMD_SETPARAMS 5=write IORequest.
uint8_t gAmigaSerialFailStage = 0u;
BYTE gAmigaSerialFailErr = 0;
BYTE gAmigaSerialFailIoErr = 0;
uint8_t gAmigaSerialFailDevice = 0u;
uint8_t jlgAmigaSerialFailStage = 0u;
BYTE jlgAmigaSerialFailErr = 0;
BYTE jlgAmigaSerialFailIoErr = 0;
uint8_t jlgAmigaSerialFailDevice = 0u;
bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
@ -245,17 +245,17 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
if (gOpen) {
return false;
}
gAmigaSerialFailStage = 0u;
gAmigaSerialFailErr = 0;
jlgAmigaSerialFailStage = 0u;
jlgAmigaSerialFailErr = 0;
gPort = CreateMsgPort();
if (gPort == NULL) {
gAmigaSerialFailStage = 1u;
jlgAmigaSerialFailStage = 1u;
return false;
}
gReadReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer));
if (gReadReq == NULL) {
gAmigaSerialFailStage = 2u;
jlgAmigaSerialFailStage = 2u;
serialTeardown(false);
return false;
}
@ -294,12 +294,12 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
err = OpenDevice((CONST_STRPTR)"serial.device", unit, (struct IORequest *)gReadReq, 0ul);
}
if (err != 0) {
gAmigaSerialFailStage = 3u;
gAmigaSerialFailErr = err;
jlgAmigaSerialFailStage = 3u;
jlgAmigaSerialFailErr = err;
// Distinguish a REAL open failure from a lying return value: io_Error is the device's own
// verdict and io_Device is non-NULL if the device actually attached.
gAmigaSerialFailIoErr = gReadReq->IOSer.io_Error;
gAmigaSerialFailDevice = (gReadReq->IOSer.io_Device != NULL) ? 1u : 0u;
jlgAmigaSerialFailIoErr = gReadReq->IOSer.io_Error;
jlgAmigaSerialFailDevice = (gReadReq->IOSer.io_Device != NULL) ? 1u : 0u;
serialTeardown(false);
return false;
}
@ -316,8 +316,8 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
// SERF_SHARED fallback) -- do not strip it here.
gReadReq->IOSer.io_Command = SDCMD_SETPARAMS;
if (DoIO((struct IORequest *)gReadReq) != 0) {
gAmigaSerialFailStage = 4u;
gAmigaSerialFailErr = gReadReq->IOSer.io_Error;
jlgAmigaSerialFailStage = 4u;
jlgAmigaSerialFailErr = gReadReq->IOSer.io_Error;
serialTeardown(false);
return false;
}
@ -325,7 +325,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
// Second request sharing the same open device (copy carries io_Device/Unit).
gWriteReq = (struct IOExtSer *)CreateIORequest(gPort, sizeof(struct IOExtSer));
if (gWriteReq == NULL) {
gAmigaSerialFailStage = 5u;
jlgAmigaSerialFailStage = 5u;
serialTeardown(false);
return false;
}

View file

@ -15,7 +15,7 @@
// * jlpAudioPlayMod starts the module player at the same 12288 Hz mono
// unsigned-8-bit output that the ISR consumes one byte at a time.
// * Each Timer A tick the ISR pops one byte off the buffer, writes its
// YM level (gYmLevel: the YM's sixteen levels are logarithmic, so each
// YM level (jlgYmLevel: the YM's sixteen levels are logarithmic, so each
// sample plays as the level nearest it on a straight line) to YM
// register 8 (channel A volume), and advances the play pointer; the
// frame tick sees which half it has left. The refill itself happens in
@ -101,8 +101,8 @@
// The module player and the SFX overlay mix unsigned samples centred on $80.
#define MIX_SILENCE_UNSIGNED MOD_PLAYER_SILENCE_U8
// The 8-bit mix's unit (joey/audio.h's mix): a module channel at full volume
// and a full-scale sound effect each swing half a byte (modPlayerSetU8Shift,
// audioSfxSetShift). On the STe a side's two channels fill it; the plain
// and a full-scale sound effect each swing half a byte (jlModPlayerSetU8Shift,
// jlAudioSfxSetShift). On the STe a side's two channels fill it; the plain
// ST's mono sums four, which clip when three or four peak together. The
// voices come down to the same level (kYmVolume).
#define MIX_UNIT_SHIFT 1u
@ -156,7 +156,7 @@
#define MIX_SILENCE MIX_SILENCE_UNSIGNED
// The YM's channel-A volume is the DAC, and its sixteen levels are
// logarithmic, about 3 dB apart: each sample is played as the level nearest
// its value on a straight line from level 0 to level 15 (gYmLevel), so the
// its value on a straight line from level 0 to level 15 (jlgYmLevel), so the
// waveform keeps its shape and silence sits near the middle, level 13. The
// levels' amplitudes, as the YM2149 puts them out (ST-Sound's measured
// table).
@ -179,7 +179,7 @@ static const uint16_t kYmAmplitude[YM_LEVELS] = {
// jlAudioTickRegister rides TOS's 200 Hz Timer C (both builds: the plain
// ST's Timer A is the PWM pump's, B the per-line palettes', D the serial
// port's), chained in front of TOS's own handler by stTickIsr
// port's), chained in front of TOS's own handler by jlStTickIsr
// (audio_isr.s). A carry paces the program's rate against the 200 ticks a
// second, so the average is exact at any rate; at most 200 a second come
// one to an interrupt, faster rates several to one. The callback runs in
@ -194,7 +194,7 @@ static const uint16_t kYmAmplitude[YM_LEVELS] = {
// these must match) or the STe's DMA chip's. Even-aligned: the DMA chip can
// only start a frame on an even address (2 is also mint a.out's alignment
// cap).
volatile uint8_t gMixBuf[MIX_BUFFER] __attribute__((aligned(2)));
volatile uint8_t jlgMixBuf[MIX_BUFFER] __attribute__((aligned(2)));
// The halves to refill: a frame tick that finds the play position has left
// a half since the last one raises its flag (pollRefill), and refills it.
static uint8_t gNeedRefill[2] = { 0, 0 };
@ -203,11 +203,11 @@ static uint8_t gPlayHalf = 0u;
#ifndef JOEYLIB_ATARI_STE
// The PWM ISR's play pointer and the buffer's end (audio_isr.s).
volatile uint8_t *gMixPos = NULL;
volatile uint8_t *gMixEnd = NULL;
volatile uint8_t *jlgMixPos = NULL;
volatile uint8_t *jlgMixEnd = NULL;
// Each sample's YM level (kYmAmplitude), built at jlpAudioInit: the ISR
// plays gYmLevel[sample], from beside its own code (audio_isr.s).
extern uint8_t gYmLevel[SAMPLE_VALUES];
// plays jlgYmLevel[sample], from beside its own code (audio_isr.s).
extern uint8_t jlgYmLevel[SAMPLE_VALUES];
// The drain (PWM_RAMP_SAMPLES): the ramp down is written, from
// PWM_DRAIN_MARGIN past gDrainFrom, where the ISR was; and the silence
// before it (PWM_IDLE_MS), since gIdleSinceMs.
@ -216,7 +216,7 @@ static uint16_t gDrainFrom = 0u;
static bool gIdle = false;
static uint32_t gIdleSinceMs = 0u;
extern void mfpTimerAIsr(void);
extern void jlMfpTimerAIsr(void);
static void (*gOldTimerAVec)(void) = NULL;
@ -227,8 +227,8 @@ static volatile uint16_t gVoiceAFreqHz;
static volatile uint8_t gVoiceAAtten = 15u;
#endif
// Read by stTickIsr (audio_isr.s), so not static.
void (*gStTickOldVec)(void) = NULL;
// Read by jlStTickIsr (audio_isr.s), so not static.
void (*jlgStTickOldVec)(void) = NULL;
static volatile jlAudioTickFnT gStTickFn = NULL;
static volatile uint32_t gStTickHz = 0u;
static volatile uint32_t gStTickCarry = 0u;
@ -236,8 +236,8 @@ static bool gStTickHooked = false;
// The status register at the outermost jlAudioCriticalEnter.
static uint16_t gCriticalSr = 0u;
extern void stTickIsr(void);
void stTickRun(void);
extern void jlStTickIsr(void);
void jlStTickRun(void);
static bool gReady = false;
@ -323,7 +323,7 @@ static long uninstallDma(void);
static void ymWrite(uint8_t reg, uint8_t val) {
uint16_t saved;
saved = m68kInterruptsOff();
saved = m68kInterruptsOff();
*ST_YM_SELECT = reg;
*ST_YM_DATA = val;
m68kInterruptsRestore(saved);
@ -343,8 +343,8 @@ static long installTimerA(void) {
// = 128) before opening the IRQ gate.
silenceMixBuffer();
rampWrite(0u, 0u, MIX_SILENCE);
gMixPos = (volatile uint8_t *)gMixBuf;
gMixEnd = (volatile uint8_t *)(gMixBuf + MIX_BUFFER);
jlgMixPos = (volatile uint8_t *)jlgMixBuf;
jlgMixEnd = (volatile uint8_t *)(jlgMixBuf + MIX_BUFFER);
gNeedRefill[0] = 0;
gNeedRefill[1] = 0;
gPlayHalf = 0u;
@ -381,7 +381,7 @@ static long installTimerA(void) {
// MFP Timer A: stop, install our vector, set prescaler 200 + data
// 1 (= 2.4576 MHz / 200 = 12288 Hz), then start.
*ST_MFP_TACR = MFP_TACR_STOP;
gOldTimerAVec = (void (*)(void))Setexc(VEC_MFP_TA, (long)mfpTimerAIsr);
gOldTimerAVec = (void (*)(void))Setexc(VEC_MFP_TA, (long)jlMfpTimerAIsr);
*ST_MFP_TADR = 1;
*ST_MFP_TACR = MFP_TACR_DIV200;
Jenabint(INT_TIMER_A);
@ -412,7 +412,7 @@ static long uninstallTimerA(void) {
// Silence in the output stage's own sample format (MIX_SILENCE).
static void silenceMixBuffer(void) {
memset((void *)gMixBuf, MIX_SILENCE, MIX_BUFFER);
memset((void *)jlgMixBuf, MIX_SILENCE, MIX_BUFFER);
}
@ -432,7 +432,7 @@ static uint16_t dmaPlayOffset(void) {
mid = *STE_DMA_COUNT_MID;
lo = *STE_DMA_COUNT_LO;
} while (hi != *STE_DMA_COUNT_HI);
return (uint16_t)((((uint32_t)hi << 16) | ((uint32_t)mid << 8) | lo) - (uint32_t)gMixBuf);
return (uint16_t)((((uint32_t)hi << 16) | ((uint32_t)mid << 8) | lo) - (uint32_t)jlgMixBuf);
}
@ -471,8 +471,8 @@ static long installDma(void) {
gNeedRefill[0] = 0;
gNeedRefill[1] = 0;
gPlayHalf = 0u;
dmaSetFrame(STE_DMA_START_HI, STE_DMA_START_MID, STE_DMA_START_LO, (uint32_t)gMixBuf);
dmaSetFrame(STE_DMA_END_HI, STE_DMA_END_MID, STE_DMA_END_LO, (uint32_t)(gMixBuf + MIX_BUFFER));
dmaSetFrame(STE_DMA_START_HI, STE_DMA_START_MID, STE_DMA_START_LO, (uint32_t)jlgMixBuf);
dmaSetFrame(STE_DMA_END_HI, STE_DMA_END_MID, STE_DMA_END_LO, (uint32_t)(jlgMixBuf + MIX_BUFFER));
*STE_DMA_MODE = STE_DMA_STEREO_12K;
*STE_DMA_CTRL = STE_DMA_PLAY_LOOP;
return 0;
@ -494,7 +494,7 @@ static uint16_t playOffset(void) {
#ifdef JOEYLIB_ATARI_STE
return dmaPlayOffset();
#else
return (uint16_t)(gMixPos - gMixBuf);
return (uint16_t)(jlgMixPos - jlgMixBuf);
#endif
}
@ -523,10 +523,10 @@ static void refillHalf(uint8_t half) {
volatile uint8_t *dst;
bool silentFill;
dst = gMixBuf + (uint16_t)half * MIX_HALF;
silentFill = !modPlayerActive() && !anySfxActive();
if (modPlayerActive()) {
if (!modPlayerRenderU8((uint8_t *)dst, MIX_HALF_FRAMES, MIX_CHANNELS)) {
dst = jlgMixBuf + (uint16_t)half * MIX_HALF;
silentFill = !jlModPlayerActive() && !anySfxActive();
if (jlModPlayerActive()) {
if (!jlModPlayerRenderU8((uint8_t *)dst, MIX_HALF_FRAMES, MIX_CHANNELS)) {
gModTail = true;
gModTailHalf = half;
}
@ -534,9 +534,9 @@ static void refillHalf(uint8_t half) {
memset((void *)dst, MIX_SILENCE_UNSIGNED, MIX_HALF);
}
#ifdef JOEYLIB_ATARI_STE
audioSfxOverlayMixStereo(dst, MIX_HALF_FRAMES, gSfxSlots, JL_AUDIO_SFX_SLOTS, gSfxPan, AUDIO_SFX_DST_UNSIGNED);
jlAudioSfxOverlayMixStereo(dst, MIX_HALF_FRAMES, gSfxSlots, JL_AUDIO_SFX_SLOTS, gSfxPan, AUDIO_SFX_DST_UNSIGNED);
#else
audioSfxOverlayMix(dst, MIX_HALF, gSfxSlots, JL_AUDIO_SFX_SLOTS);
jlAudioSfxOverlayMix(dst, MIX_HALF, gSfxSlots, JL_AUDIO_SFX_SLOTS);
#endif
#ifdef JOEYLIB_ATARI_STE
dmaSignBytes(dst, MIX_HALF);
@ -552,9 +552,9 @@ static void refillHalf(uint8_t half) {
// mixed as they stand, in order, never flipped to unsigned and back.
static void sfxOverlayAt(uint16_t at, uint16_t len, uint8_t slot) {
#ifdef JOEYLIB_ATARI_STE
audioSfxOverlayMixStereo(gMixBuf + at, (uint16_t)(len / MIX_CHANNELS), &gSfxSlots[slot], 1u, &gSfxPan[slot], AUDIO_SFX_DST_SIGNED);
jlAudioSfxOverlayMixStereo(jlgMixBuf + at, (uint16_t)(len / MIX_CHANNELS), &gSfxSlots[slot], 1u, &gSfxPan[slot], AUDIO_SFX_DST_SIGNED);
#else
audioSfxOverlayMix(gMixBuf + at, len, &gSfxSlots[slot], 1u);
jlAudioSfxOverlayMix(jlgMixBuf + at, len, &gSfxSlots[slot], 1u);
#endif
}
@ -596,7 +596,7 @@ static void sfxStartNow(uint8_t slot, bool fresh) {
if (start < halfEnd) {
sfxOverlayAt(start, (uint16_t)(halfEnd - start), slot);
gHalfSilent[half] = false;
start = halfEnd;
start = halfEnd;
}
if (start < aheadEnd) {
sfxOverlayAt((uint16_t)((half ^ 1u) * MIX_HALF + (start - halfEnd)), (uint16_t)(aheadEnd - start), slot);
@ -630,7 +630,7 @@ static void drainCancel(void) {
gDraining = false;
at = (uint16_t)((playOffset() + PWM_DRAIN_MARGIN) % MIX_BUFFER);
for (i = 0u; i < MIX_BUFFER - 2u * PWM_DRAIN_MARGIN; i++) {
gMixBuf[(at + i) % MIX_BUFFER] = MIX_SILENCE;
jlgMixBuf[(at + i) % MIX_BUFFER] = MIX_SILENCE;
}
rampWrite(at, 0u, MIX_SILENCE);
gHalfSilent[0] = false;
@ -655,7 +655,7 @@ static void drainStart(void) {
gDrainFrom = playOffset();
at = (uint16_t)((gDrainFrom + PWM_DRAIN_MARGIN) % MIX_BUFFER);
for (i = 0u; i < MIX_BUFFER - 2u * PWM_DRAIN_MARGIN; i++) {
gMixBuf[(at + i) % MIX_BUFFER] = 0u;
jlgMixBuf[(at + i) % MIX_BUFFER] = 0u;
}
rampWrite(at, MIX_SILENCE, 0u);
gDraining = true;
@ -668,12 +668,12 @@ static void rampWrite(uint16_t at, uint8_t from, uint8_t to) {
uint16_t i;
for (i = 0u; i < PWM_RAMP_SAMPLES; i++) {
gMixBuf[(at + i) % MIX_BUFFER] = (uint8_t)((int16_t)from + ((int16_t)to - (int16_t)from) * (int16_t)i / (int16_t)PWM_RAMP_SAMPLES);
jlgMixBuf[(at + i) % MIX_BUFFER] = (uint8_t)((int16_t)from + ((int16_t)to - (int16_t)from) * (int16_t)i / (int16_t)PWM_RAMP_SAMPLES);
}
}
// gYmLevel: for each sample value, the level whose amplitude is nearest
// jlgYmLevel: for each sample value, the level whose amplitude is nearest
// the value's place on the line from level 0's amplitude to level 15's.
static void ymLevelsBuild(void) {
uint32_t target;
@ -683,14 +683,14 @@ static void ymLevelsBuild(void) {
uint8_t v;
for (s = 0u; s < SAMPLE_VALUES; s++) {
target = kYmAmplitude[0] + ((uint32_t)(kYmAmplitude[YM_LEVELS - 1u] - kYmAmplitude[0]) * s) / YM_SAMPLE_MAX;
best = UINT32_MAX;
gYmLevel[s] = 0u;
target = kYmAmplitude[0] + ((uint32_t)(kYmAmplitude[YM_LEVELS - 1u] - kYmAmplitude[0]) * s) / YM_SAMPLE_MAX;
best = UINT32_MAX;
jlgYmLevel[s] = 0u;
for (v = 0u; v < YM_LEVELS; v++) {
gap = kYmAmplitude[v] > target ? kYmAmplitude[v] - target : target - kYmAmplitude[v];
if (gap < best) {
best = gap;
gYmLevel[s] = v;
best = gap;
jlgYmLevel[s] = v;
}
}
}
@ -746,7 +746,7 @@ bool jlpAudioInit(void) {
}
#ifdef JOEYLIB_ATARI_STE
// The STe build's output stage is the DMA chip: no chip, no audio.
if (!steMachineCheck()) {
if (!jlSteMachineCheck()) {
return false;
}
#else
@ -755,8 +755,8 @@ bool jlpAudioInit(void) {
#ifdef JOEYLIB_ATARI_STE
memset(gSfxPan, JL_AUDIO_PAN_CENTER, sizeof(gSfxPan));
#endif
modPlayerSetU8Shift(MIX_UNIT_SHIFT);
audioSfxSetShift(MIX_UNIT_SHIFT);
jlModPlayerSetU8Shift(MIX_UNIT_SHIFT);
jlAudioSfxSetShift(MIX_UNIT_SHIFT);
gReady = true;
return true;
}
@ -789,13 +789,13 @@ void jlpAudioShutdown(void) {
return;
}
silenceMixBuffer();
modPlayerStop();
jlModPlayerStop();
gReady = false;
}
bool jlpAudioIsPlayingMod(void) {
return gReady && (modPlayerActive() || gModTail);
return gReady && (jlModPlayerActive() || gModTail);
}
@ -804,7 +804,7 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
return;
}
gModTail = false;
if (!modPlayerStart(data, length, MIX_RATE, loop)) {
if (!jlModPlayerStart(data, length, MIX_RATE, loop)) {
return;
}
(void)pumpStart();
@ -815,7 +815,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
if (!gReady || slot >= JL_AUDIO_SFX_SLOTS) {
return;
}
audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
jlAudioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
sfxStartNow(slot, pumpStart());
}
@ -824,7 +824,7 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
if (!gReady || slot >= JL_AUDIO_SFX_SLOTS) {
return;
}
audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE);
jlAudioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE);
sfxStartNow(slot, pumpStart());
}
@ -965,10 +965,10 @@ static long ymPsgAllOff(void) {
}
// The tick rides TOS's 200 Hz Timer C (stTickIsr chains to TOS's handler),
// The tick rides TOS's 200 Hz Timer C (jlStTickIsr chains to TOS's handler),
// counting the rate against it: see the Timer C notes at the top. The hook
// may stay in place for the pump (tickHookUpdate), so the tick is switched
// off while its fields change -- at rate 0 stTickRun's carry never comes
// off while its fields change -- at rate 0 jlStTickRun's carry never comes
// due -- and its rate goes in last. Each store is one instruction, so this
// needs no interrupts held off, and runs before jlInit's supervisor mode.
bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
@ -999,18 +999,18 @@ static void tickHookUpdate(void) {
return;
}
if (want) {
gStTickOldVec = (void (*)(void))Setexc(VEC_MFP_TC, SETEXC_READ);
(void)Setexc(VEC_MFP_TC, (long)stTickIsr);
jlgStTickOldVec = (void (*)(void))Setexc(VEC_MFP_TC, SETEXC_READ);
(void)Setexc(VEC_MFP_TC, (long)jlStTickIsr);
} else {
(void)Setexc(VEC_MFP_TC, (long)gStTickOldVec);
(void)Setexc(VEC_MFP_TC, (long)jlgStTickOldVec);
}
gStTickHooked = want;
}
// Called from stTickIsr (audio_isr.s) at each Timer C interrupt: the
// Called from jlStTickIsr (audio_isr.s) at each Timer C interrupt: the
// program's tick as many times as its rate has come due since the last.
void stTickRun(void) {
void jlStTickRun(void) {
gStTickCarry += gStTickHz;
while (gStTickCarry >= ST_TIMER_C_HZ) {
gStTickCarry -= ST_TIMER_C_HZ;
@ -1019,14 +1019,14 @@ void stTickRun(void) {
}
// The tick callback runs from Timer C (stTickIsr), so a section holds
// The tick callback runs from Timer C (jlStTickIsr), so a section holds
// interrupts off, as ymWrite does; keep it short, as the PWM pump's Timer A
// is held off with it. The core calls these for the outermost section only,
// and Exit puts back the status register Enter found.
void jlpAudioCriticalEnter(void) {
uint16_t saved;
saved = m68kInterruptsOff();
saved = m68kInterruptsOff();
gCriticalSr = saved;
}
@ -1076,7 +1076,7 @@ void jlpAudioStopMod(void) {
if (!gReady) {
return;
}
modPlayerStop();
jlModPlayerStop();
gModTail = false;
silenceMixBuffer();
// StopMod discards the mixed tail by design (buffer just went
@ -1103,7 +1103,7 @@ void jlpAudioStopSfx(uint8_t slot) {
// Soft-real-time constraint: a half must be refilled within MIX_HALF /
// MIX_RATE = 1024 / 12288 ~= 83 ms of being left, or the play position
// comes back round to it stale: a tick that comes a whole buffer late
// replays the old half once. A full MOD-frame mix plus audioSfxOverlayMix
// replays the old half once. A full MOD-frame mix plus jlAudioSfxOverlayMix
// can approach that budget under load, so callers should run the game loop
// comfortably faster than ~12 Hz.
void jlpAudioFrameTick(void) {
@ -1134,7 +1134,7 @@ void jlpAudioFrameTick(void) {
// is playing, so the ISR would stream silence forever -- park it (on
// the plain ST once PWM_IDLE_MS of it have gone by, after a ramp down
// to level 0: drainStart).
if (!modPlayerActive() && gHalfSilent[0] && gHalfSilent[1] && !anySfxActive()) {
if (!jlModPlayerActive() && gHalfSilent[0] && gHalfSilent[1] && !anySfxActive()) {
#ifdef JOEYLIB_ATARI_STE
pumpStop();
#else

View file

@ -7,7 +7,7 @@
|
| Each interrupt:
| 1. Reads one byte from the mix buffer (unsigned 8-bit).
| 2. Writes its YM level (_gYmLevel[byte], the level nearest the
| 2. Writes its YM level (_jlgYmLevel[byte], the level nearest the
| sample's value: the YM's levels are logarithmic) to YM register 8
| (channel A volume), so the analog filter on the YM output emits
| that sample for that tick.
@ -19,17 +19,17 @@
| are reached by short addresses.
|
| Communication with the C HAL is via three globals defined in audio.c:
| _gMixBuf -- start of the mix buffer (uint8_t array)
| _gMixPos -- volatile pointer to the next sample to play
| _gMixEnd -- end of buffer (used to detect wrap)
| and _gYmLevel, each sample value's YM level (256 bytes), defined here
| _jlgMixBuf -- start of the mix buffer (uint8_t array)
| _jlgMixPos -- volatile pointer to the next sample to play
| _jlgMixEnd -- end of buffer (used to detect wrap)
| and _jlgYmLevel, each sample value's YM level (256 bytes), defined here
| and filled by audio.c.
|
| GAS-syntax (binutils m68k); assembled by m68k-atari-mint-as via the
| gcc driver.
.text
.globl _mfpTimerAIsr
.globl _jlMfpTimerAIsr
.equ YM_SELECT, 0xFFFF8800
.equ YM_DATA, 0xFFFF8802
@ -37,19 +37,19 @@
.equ MFP_ISRA, 0xFFFFFA0F
.equ ISRA_ACK, 0xDF | clear Timer A's in-service bit
_mfpTimerAIsr:
_jlMfpTimerAIsr:
movem.l %d0/%a0,-(%sp)
movea.l _gMixPos,%a0
movea.l _jlgMixPos,%a0
moveq #0,%d0
move.b (%a0)+,%d0
move.b (_gYmLevel,%pc,%d0.w),%d0
move.b (_jlgYmLevel,%pc,%d0.w),%d0
move.b #YM_VOL_A,(YM_SELECT).w
move.b %d0,(YM_DATA).w
cmpa.l _gMixEnd,%a0
cmpa.l _jlgMixEnd,%a0
bcs.s 1f
lea _gMixBuf,%a0
lea _jlgMixBuf,%a0
1:
move.l %a0,_gMixPos
move.l %a0,_jlgMixPos
move.b #ISRA_ACK,(MFP_ISRA).w
movem.l (%sp)+,%d0/%a0
rte
@ -59,5 +59,5 @@ _mfpTimerAIsr:
| anywhere else would cost an address register (saved and restored) and
| its load every sample, and on the plain ST the mix already runs about as
| fast as the interrupt plays it. TOS does not protect a program's text.
.globl _gYmLevel
_gYmLevel: .space 256
.globl _jlgYmLevel
_jlgYmLevel: .space 256

View file

@ -17,7 +17,7 @@
|
| ABI: cdecl. d2-d7/a2-a6 callee-save.
|
| void surface68kStCircleOutline(uint8_t *base,
| void jlSurface68kStCircleOutline(uint8_t *base,
| uint16_t cx, uint16_t cy,
| uint16_t r, uint8_t color);
|
@ -28,7 +28,7 @@
| d5.w = cx (cached)
| a4 = cy (cached, sign-extended)
| a3 = base
| a5 = _gStBitMaskWordLut
| a5 = _jlgStBitMaskWordLut
| d0,d1,d6,d7 = scratch
|
| Scratch block (40 bytes) at sp+0..39. Each xp record is 8 bytes but
@ -201,17 +201,17 @@
.equ SP_R, SP_OFF + 12 + 2
.equ SP_COLOR, SP_OFF + 16 + 3
.globl _surface68kStCircleOutline
.globl _jlSurface68kStCircleOutline
_surface68kStCircleOutline:
_jlSurface68kStCircleOutline:
movem.l %d2-%d7/%a2-%a6,-(%sp)
lea -SP_LOCAL(%sp),%sp
| Load base (a3) and bitMaskLut (a5). The mask LUT is
| the shared _gStBitMaskWordLut in lineSpan.s; reference
| absolute (same pattern as _gStRowOffsetLut below).
| the shared _jlgStBitMaskWordLut in lineSpan.s; reference
| absolute (same pattern as _jlgStRowOffsetLut below).
move.l SP_BASE(%sp),%a3
lea _gStBitMaskWordLut,%a5
lea _jlgStBitMaskWordLut,%a5
| Cache cx in d5, cy (sign-extended) in a4.
move.w SP_CX(%sp),%d5
@ -230,7 +230,7 @@ _surface68kStCircleOutline:
| chain we used to do per YP_REC. Saved across all 4
| YP_RECs per Bresenham iter (~120 cyc/iter).
| Shared LUT lives in lineSpan.s; reference absolute.
lea _gStRowOffsetLut,%a6
lea _jlgStRowOffsetLut,%a6
| Dispatch on color (low 4 bits) -> one of 16 main loops.
moveq #0,%d6
@ -283,4 +283,4 @@ _surface68kStCircleOutline:
| (bitMaskWordLut and yLut both live in lineSpan.s as the shared
| _gStBitMaskWordLut / _gStRowOffsetLut.)
| _jlgStBitMaskWordLut / _jlgStRowOffsetLut.)

View file

@ -13,7 +13,7 @@
| (its movem path beats a move.l loop past the crossover), so this only
| ever sees short spans.
|
| void stCopyRectSpans68k(uint8_t *dst, const uint8_t *src,
| void jlStCopyRectSpans68k(uint8_t *dst, const uint8_t *src,
| int longsPerRow, int numRows);
| Copies numRows rows of longsPerRow longs each, both pointers strided by
| ST_BYTES_PER_ROW (160) between row STARTS. dst/src are long-aligned
@ -22,9 +22,9 @@
.text
.globl _stCopyRectSpans68k
.globl _jlStCopyRectSpans68k
_stCopyRectSpans68k:
_jlStCopyRectSpans68k:
movem.l %d2/%d3,-(%sp) | 8 bytes saved
move.l 12(%sp),%a1 | dst
move.l 16(%sp),%a0 | src

View file

@ -21,7 +21,7 @@
|
| ABI: cdecl. d2-d7/a2-a6 callee-save.
|
| void surface68kStFillCircle(uint8_t *base,
| void jlSurface68kStFillCircle(uint8_t *base,
| uint16_t cx, uint16_t cy,
| uint16_t r, uint8_t color);
@ -56,13 +56,13 @@
lsr.w #4,%d0 | groupLast
sub.w %d1,%d0 | numGroups
move.w %d0,4(%sp)
| leftMask via LUT[bitFirst]; a5 = _gStLeftMaskLut base
| leftMask via LUT[bitFirst]; a5 = _jlgStLeftMaskLut base
move.w 0(%sp),%d0
and.w #15,%d0
add.w %d0,%d0
move.w (%a5,%d0.w),%d1
move.w %d1,0(%sp)
| rightMask via LUT[bitLast]; a6 = _gStRightMaskLut base
| rightMask via LUT[bitLast]; a6 = _jlgStRightMaskLut base
move.w 2(%sp),%d0
and.w #15,%d0
add.w %d0,%d0
@ -111,7 +111,7 @@
.macro SPAN_BODY color
| a4 = base + y*160 + groupFirstByteOff
| y*160 via shared _gStRowOffsetLut (a2 holds lut base).
| y*160 via shared _jlgStRowOffsetLut (a2 holds lut base).
| byteOff (y*160 + groupFirstByteOff) fits in 16 bits
| (max 31992), so word-only ops + .w-indexed lea.
add.w %d0,%d0 | y * 2 (word index)
@ -204,9 +204,9 @@
.endm
.globl _surface68kStFillCircle
.globl _jlSurface68kStFillCircle
_surface68kStFillCircle:
_jlSurface68kStFillCircle:
movem.l %d2-%d7/%a2-%a6,-(%sp)
lea -SP_FC_LOCAL(%sp),%sp
@ -216,12 +216,12 @@ _surface68kStFillCircle:
move.b SP_FC_COLOR(%sp),%d7
| LUT bases. a5/a6 = mask LUTs (used by COMPUTE_PAIR_MASKS).
| a2 = shared _gStRowOffsetLut (used by SPAN_BODY for y*160).
| a2 = shared _jlgStRowOffsetLut (used by SPAN_BODY for y*160).
| All three LUTs are the shared definitions in lineSpan.s;
| reference them absolute (single source of truth).
lea _gStLeftMaskLut,%a5
lea _gStRightMaskLut,%a6
lea _gStRowOffsetLut,%a2
lea _jlgStLeftMaskLut,%a5
lea _jlgStRightMaskLut,%a6
lea _jlgStRowOffsetLut,%a2
| loLong = ((c&1)?0xFFFF0000:0) | ((c&2)?0x0000FFFF:0)
moveq #0,%d5
@ -307,4 +307,4 @@ _surface68kStFillCircle:
| (leftMaskLut/rightMaskLut now live in lineSpan.s as the shared
| _gStLeftMaskLut / _gStRightMaskLut, referenced absolute above.)
| _jlgStLeftMaskLut / _jlgStRightMaskLut, referenced absolute above.)

File diff suppressed because it is too large Load diff

View file

@ -19,9 +19,9 @@
//
// The ISR writes into private gIsrState (keyboard) and gIsrMouse*
// buffers; jlpInputPoll copies them into the public globals.
// jlKeyPressed edge detection requires that public gKeyState only
// jlKeyPressed edge detection requires that public jlgKeyState only
// advance during jlpInputPoll, never at interrupt time -- jlInputPoll
// snapshots gKeyState into gKeyPrev before jlpInputPoll runs.
// snapshots jlgKeyState into jlgKeyPrev before jlpInputPoll runs.
//
// Typed characters: replacing ikbdsys means TOS's own keyboard
// processing never runs, so Bconin never sees our keys. Instead the
@ -160,7 +160,7 @@ static void altLayerFind(const KeyTabT *keyTab);
static void charRepeat(void);
static long ikbdHandler(void);
#ifdef JOEYLIB_ATARI_STE
static void padToStick(const StePadT *pad, jlJoystickE js);
static inline void padToStick(uint16_t held, uint16_t buttonA, uint16_t buttonB, jlJoystickE js);
#endif
static long patchIkbdVector(void);
#ifdef JOEYLIB_ATARI_STE
@ -247,17 +247,23 @@ static volatile uint8_t gPacketRemaining = 0;
static volatile uint8_t gPacketKind = PKT_KIND_NONE;
static volatile uint8_t gMousePacketByte = 0; // bytes consumed in current packet
static bool gHooked = false;
// uint8_t (not bool) so element size matches gKeyState's. See
// uint8_t (not bool) so element size matches jlgKeyState's. See
// src/core/inputInternal.h for the full rationale.
static volatile uint8_t gIsrState[JL_KEY_COUNT];
// Each key's presses, counted by the ISR (which alone writes them), and the
// count the last poll saw: a key whose count moved was pressed since, so a
// tap between two polls still reads down for one (inputLatch's meaning).
// tap between two polls still reads down for one (jlInputLatch's meaning).
// The poll writes nothing the ISR does, so it needs no interrupts held off:
// held off across the keys, the plain ST's 12288 Hz PWM pump lost five
// samples a poll, and digital sound played 1.4% flat.
static volatile uint8_t gIsrTaps[JL_KEY_COUNT];
static uint8_t gTapsSeen[JL_KEY_COUNT];
// Key events the ISR has taken (after it updated the key), and the count the
// last poll scanned the keys at: a poll scans them only when one came since,
// or when the last scan showed a tap that must now go up.
static volatile uint16_t gIsrKeyEvents;
static uint16_t gKeyEventsSeen;
static bool gKeyTapShown;
// Mouse delta accumulator. Each ACIA mouse packet adds dx/dy here; the
// poll routine clamps the running absolute position into the surface
@ -309,9 +315,26 @@ static uint32_t gRepeatDelayMs = 0u;
static uint32_t gRepeatRateMs = 0u;
#ifdef JOEYLIB_ATARI_STE
// Whether JL_JOYSTICK_0 is the Jaguar pad in enhanced port A, used more
// recently than port 1's stick.
// Whether the machine has the enhanced ports (jlSteMachineHasPads, kept by
// jlpInputInit), and whether JL_JOYSTICK_0 is the Jaguar pad in port A,
// used more recently than port 1's stick.
static bool gStePads = false;
static bool gPadAIsStick0 = false;
// A pad's four direction bits (STE_PAD_UP, DOWN, LEFT, RIGHT) as its axes.
// Both directions of an axis read as the positive one, as on the IKBD ports.
static const int8_t kPadAxisX[STE_PAD_DIR_MASK + 1u] = {
0, 0, 0, 0,
JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MIN,
JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX,
JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX
};
static const int8_t kPadAxisY[STE_PAD_DIR_MASK + 1u] = {
0, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX,
0, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX,
0, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX,
0, JL_JOYSTICK_AXIS_MIN, JL_JOYSTICK_AXIS_MAX, JL_JOYSTICK_AXIS_MAX
};
#endif
// ----- Internal helpers -----
@ -343,7 +366,7 @@ static void altLayerFind(const KeyTabT *keyTab) {
} else if (os->os_version >= TOS_ALT_LAYER_VERSION) {
inTables = true;
} else {
(void)stAltLayerFind((uint8_t)(os->os_conf >> TOS_CONF_COUNTRY_SHIFT), &gKeyTabAltUnshift, &gKeyTabAltShift, &gKeyTabAltCaps);
(void)jlStAltLayerFind((uint8_t)(os->os_conf >> TOS_CONF_COUNTRY_SHIFT), &gKeyTabAltUnshift, &gKeyTabAltShift, &gKeyTabAltCaps);
return;
}
if (inTables) {
@ -355,11 +378,16 @@ static void altLayerFind(const KeyTabT *keyTab) {
// The held key's character again, once TOS's delay has passed since it went
// down and then at TOS's rate (the ISR names the key, gIsrRepeatCode).
// down and then at TOS's rate (the ISR names the key, gIsrRepeatCode). No
// key held, the usual poll, reads no clock: a key the ISR takes just after
// the check is the next poll's.
static void charRepeat(void) {
uint32_t now;
uint8_t n;
if (gIsrRepeatNew == 0u && gIsrRepeatCode == 0u) {
return;
}
now = jlpMillisElapsed();
if (gIsrRepeatNew != 0u) {
gIsrRepeatNew = 0u;
@ -480,6 +508,7 @@ static long ikbdHandler(void) {
if (!isBreak) {
gIsrTaps[key]++;
}
gIsrKeyEvents++;
}
if (isBreak && code == gIsrRepeatCode) {
@ -520,14 +549,14 @@ static long ikbdHandler(void) {
#ifdef JOEYLIB_ATARI_STE
// A Jaguar pad as a stick: its directions, and its A and B as the two
// buttons.
static void padToStick(const StePadT *pad, jlJoystickE js) {
gJoyAxisX[js] = pad->axisX;
gJoyAxisY[js] = pad->axisY;
gJoyButtonState[js][JL_JOY_BUTTON_0] = pad->buttonA;
gJoyButtonState[js][JL_JOY_BUTTON_1] = pad->buttonB;
gJoyOneButton[js] = false;
// A Jaguar pad as a stick: its held direction bits, and its A and B (any
// nonzero is down) as the two buttons.
static inline void padToStick(uint16_t held, uint16_t buttonA, uint16_t buttonB, jlJoystickE js) {
jlgJoyAxisX[js] = kPadAxisX[held];
jlgJoyAxisY[js] = kPadAxisY[held];
jlgJoyButtonState[js][JL_JOY_BUTTON_0] = buttonA != 0u;
jlgJoyButtonState[js][JL_JOY_BUTTON_1] = buttonB != 0u;
jlgJoyOneButton[js] = false;
}
#endif
@ -545,22 +574,23 @@ static long patchIkbdVector(void) {
// last, and a pad in port B is JL_JOYSTICK_1 (port 0, the mouse's, gives
// no stick in TOS's IKBD mode).
static void readStePads(void) {
StePadT padA;
StePadT padB;
StePadsT pads;
uint16_t heldA;
if (!steMachineHasPads()) {
if (!gStePads) {
return;
}
stePadsRead(&padA, &padB);
if (padA.used) {
stePadsRead(&pads);
heldA = (uint16_t)((pads.directions >> STE_PAD_A_SHIFT) & STE_PAD_DIR_MASK);
if (heldA != 0u || ((pads.row0 | pads.row1) & STE_PAD_A_FIRE) != 0u) {
gPadAIsStick0 = true;
} else if (gIsrJoyByte[ST_PORT1_AS_JS] != 0u || gJoyButtonState[ST_PORT1_AS_JS][JL_JOY_BUTTON_0] != 0u) {
} else if (gIsrJoyByte[ST_PORT1_AS_JS] != 0u || jlgJoyButtonState[ST_PORT1_AS_JS][JL_JOY_BUTTON_0] != 0u) {
gPadAIsStick0 = false;
}
if (gPadAIsStick0) {
padToStick(&padA, ST_PORT1_AS_JS);
padToStick(heldA, (uint16_t)(pads.row0 & STE_PAD_A_FIRE), (uint16_t)(pads.row1 & STE_PAD_A_FIRE), ST_PORT1_AS_JS);
}
padToStick(&padB, ST_PORT0_AS_JS);
padToStick((uint16_t)((pads.directions >> STE_PAD_B_SHIFT) & STE_PAD_DIR_MASK), (uint16_t)(pads.row0 & STE_PAD_B_FIRE), (uint16_t)(pads.row1 & STE_PAD_B_FIRE), ST_PORT0_AS_JS);
}
#endif
@ -588,13 +618,19 @@ void jlpInputInit(void) {
KeyTabT *keyTab;
uint32_t kbrate;
gMouseAttached = true; // the IKBD mouse line is always wired on an ST
memset(gKeyState, 0, sizeof(gKeyState));
memset(gKeyPrev, 0, sizeof(gKeyPrev));
jlgMouseAttached = true; // the IKBD mouse line is always wired on an ST
memset(jlgKeyState, 0, sizeof(jlgKeyState));
memset(jlgKeyPrev, 0, sizeof(jlgKeyPrev));
memset((void *)gIsrState, 0, sizeof(gIsrState));
memset((void *)gIsrTaps, 0, sizeof(gIsrTaps));
memset(gTapsSeen, 0, sizeof(gTapsSeen));
memset((void *)gIsrJoyByte, 0, sizeof(gIsrJoyByte));
gKeyEventsSeen = gIsrKeyEvents;
gKeyTapShown = false;
#ifdef JOEYLIB_ATARI_STE
gStePads = jlSteMachineHasPads();
gPadAIsStick0 = false;
#endif
// Cache the TOS keyboard translation tables (the OS keymap) for
// the ISR's typed-character path. -1 leaves the tables unchanged
@ -624,16 +660,16 @@ void jlpInputInit(void) {
gMouseAbsX = JL_SURFACE_WIDTH / 2;
gMouseAbsY = JL_SURFACE_HEIGHT / 2;
gMouseX = gMouseAbsX;
gMouseY = gMouseAbsY;
jlgMouseX = gMouseAbsX;
jlgMouseY = gMouseAbsY;
gIsrMouseDx = 0;
gIsrMouseDy = 0;
gIsrMouseButtons = 0;
// Both ports always announce themselves on the ST -- packets
// arrive even when no stick is plugged in (state stays at zero).
gJoyConnected[JL_JOYSTICK_0] = true;
gJoyConnected[JL_JOYSTICK_1] = true;
jlgJoyConnected[JL_JOYSTICK_0] = true;
jlgJoyConnected[JL_JOYSTICK_1] = true;
gKbdvecs = (_KBDVECS *)Kbdvbase();
gPacketRemaining = 0;
@ -655,14 +691,26 @@ void jlpInputPoll(void) {
uint8_t btn;
uint8_t joy;
uint8_t tail;
uint8_t held;
uint8_t taps;
uint8_t tapOnly;
uint16_t events;
uint16_t i;
// Down when held, or pressed since the last poll (gIsrTaps).
for (i = 0u; i < JL_KEY_COUNT; i++) {
taps = gIsrTaps[i];
gKeyState[i] = (uint8_t)(gIsrState[i] | (taps != gTapsSeen[i]));
gTapsSeen[i] = taps;
// Down when held, or pressed since the last poll (gIsrTaps). The count
// is read first: an event the scan misses moves it for the next poll.
events = gIsrKeyEvents;
if (events != gKeyEventsSeen || gKeyTapShown) {
gKeyEventsSeen = events;
tapOnly = 0u;
for (i = 0u; i < JL_KEY_COUNT; i++) {
held = gIsrState[i];
taps = gIsrTaps[i];
jlgKeyState[i] = (uint8_t)(held | (taps != gTapsSeen[i]));
tapOnly = (uint8_t)(tapOnly | (jlgKeyState[i] & (uint8_t)(held == 0u)));
gTapsSeen[i] = taps;
}
gKeyTapShown = tapOnly != 0u;
}
// Drain ISR-translated characters into the core queue. Single-
@ -700,39 +748,39 @@ void jlpInputPoll(void) {
gMouseAbsX = (int16_t)newX;
gMouseAbsY = (int16_t)newY;
gMouseX = gMouseAbsX;
gMouseY = gMouseAbsY;
jlgMouseX = gMouseAbsX;
jlgMouseY = gMouseAbsY;
gMouseButtonState[JL_MOUSE_BUTTON_LEFT] = (btn & MOUSE_HDR_LEFT_BTN) != 0;
gMouseButtonState[JL_MOUSE_BUTTON_RIGHT] = (btn & MOUSE_HDR_RIGHT_BTN) != 0;
gMouseButtonState[JL_MOUSE_BUTTON_MIDDLE] = false;
jlgMouseButtonState[JL_MOUSE_BUTTON_LEFT] = (btn & MOUSE_HDR_LEFT_BTN) != 0;
jlgMouseButtonState[JL_MOUSE_BUTTON_RIGHT] = (btn & MOUSE_HDR_RIGHT_BTN) != 0;
jlgMouseButtonState[JL_MOUSE_BUTTON_MIDDLE] = false;
// Decode latest joystick byte for each stick. Direction is digital
// on the ST, so axes snap to the JOYSTICK_AXIS_* extremes.
for (i = 0; i < JL_JOYSTICK_COUNT; i++) {
joy = gIsrJoyByte[i];
gJoyAxisX[i] = 0;
gJoyAxisY[i] = 0;
joy = gIsrJoyByte[i];
jlgJoyAxisX[i] = 0;
jlgJoyAxisY[i] = 0;
if (joy & JOY_BIT_LEFT) {
gJoyAxisX[i] = JL_JOYSTICK_AXIS_MIN;
jlgJoyAxisX[i] = JL_JOYSTICK_AXIS_MIN;
}
if (joy & JOY_BIT_RIGHT) {
gJoyAxisX[i] = JL_JOYSTICK_AXIS_MAX;
jlgJoyAxisX[i] = JL_JOYSTICK_AXIS_MAX;
}
if (joy & JOY_BIT_UP) {
gJoyAxisY[i] = JL_JOYSTICK_AXIS_MIN;
jlgJoyAxisY[i] = JL_JOYSTICK_AXIS_MIN;
}
if (joy & JOY_BIT_DOWN) {
gJoyAxisY[i] = JL_JOYSTICK_AXIS_MAX;
jlgJoyAxisY[i] = JL_JOYSTICK_AXIS_MAX;
}
gJoyButtonState[i][JL_JOY_BUTTON_0] = (joy & JOY_BIT_FIRE) != 0;
gJoyOneButton[i] = true; // one fire button: a hold is button 1
jlgJoyButtonState[i][JL_JOY_BUTTON_0] = (joy & JOY_BIT_FIRE) != 0;
jlgJoyOneButton[i] = true; // one fire button: a hold is button 1
}
// Port 1's fire and the mouse's right button are one IKBD line: while
// the IKBD reports the mouse (TOS's mode, kept here) a press on it
// arrives as the right button, never in a joystick packet.
if ((btn & MOUSE_HDR_RIGHT_BTN) != 0) {
gJoyButtonState[ST_PORT1_AS_JS][JL_JOY_BUTTON_0] = true;
jlgJoyButtonState[ST_PORT1_AS_JS][JL_JOY_BUTTON_0] = true;
}
#ifdef JOEYLIB_ATARI_STE
readStePads();

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