diff --git a/Makefile b/Makefile index 135f4e9..3dc0b30 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ # make dos builds MS-DOS only # make x68000 builds Sharp X68000 only # make docs renders the manual to build/docs (HTML + PDF) +# make verify builds every port, then runs every gate (needs emulators) # make verify-audio runs every port's audio gate (needs emulators) # make verify-serial runs every port's serial loopback gate # make clean removes all build outputs @@ -44,7 +45,7 @@ ifeq ($(HAVE_X68000),1) ALL_TARGETS += x68000 endif -.PHONY: all iigs iigs-disk iigs-verify amiga atarist dos x68000 blank-check tools clean help status verify-audio verify-serial docs +.PHONY: all iigs iigs-disk iigs-verify amiga atarist dos x68000 blank-check tools clean help status verify verify-audio verify-serial docs all: $(ALL_TARGETS) tools ifeq ($(ALL_TARGETS),) @@ -87,6 +88,14 @@ blank-check: docs: @$(REPO_DIR)/scripts/build-docs.sh $(FORMAT) +# EVERYTHING: build every port, then run every gate -- host checks, UBER +# golden hashes, the IIgs example boots, input, audio, serial, the X68000 +# mouse, IIgs save / serial loss / game disks -- sequentially, with a +# per-gate summary. Same exit codes as below. Takes a subset of ports: +# make verify PORTS="host dos" +verify: + @$(REPO_DIR)/scripts/verify-all.sh all $(PORTS) + # Every port's audio gate, run sequentially (the recording gates grade # fixed time windows, so a competing emulator can slide a phase boundary). # Exits 1 on a real failure and 3 if a gate SKIPPED for an unbuilt port -- @@ -116,6 +125,7 @@ help: @echo "Targets: iigs amiga atarist dos x68000 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)" @echo " verify-audio (all 5 audio gates; PORTS=\"atarist amiga\" for a subset)" @echo " verify-serial (host check + all 5 serial loopback gates; PORTS= subset)" @echo " docs (manual to build/docs; FORMAT=html for HTML only)" diff --git a/README.md b/README.md index 9033366..61a2e36 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ 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`, `joy`, `sprite`, `audio`, `uber`, `adventure`, +`sertest`, `save`, `tilebank`, `joy`, `sprite`, `audio`, `uber`, `adventure`, `adventure2`, `agi`, `spacetaxi`) 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. @@ -161,7 +161,6 @@ is a quick reference. Every entry point is plain C, no C++ extensions. ```c typedef struct { uint32_t codegenBytes; // runtime compiled-sprite cache size - uint32_t audioBytes; // reserved; not yet consulted by any engine } jlConfigT; bool jlInit (const jlConfigT *config); diff --git a/docs/ApiCore.adoc b/docs/ApiCore.adoc index ec58784..7225223 100644 --- a/docs/ApiCore.adoc +++ b/docs/ApiCore.adoc @@ -8,7 +8,6 @@ Declared in ``, included by ``. ---- typedef struct { uint32_t codegenBytes; - uint32_t audioBytes; } jlConfigT; ---- @@ -23,10 +22,6 @@ typedef struct { one Memory Manager block that may not span a 64 KB bank boundary, so values above 65536 are clamped to 65536, and even a clamped request can fail if no free bank exists. Size it against `jlSpriteCompiledSize()`. - -|`audioBytes` -|`uint32_t` -|Reserved. Not yet consulted by any audio engine. |=== === jlInit diff --git a/docs/ApiDebug.adoc b/docs/ApiDebug.adoc index fec92ae..56faa1d 100644 --- a/docs/ApiDebug.adoc +++ b/docs/ApiDebug.adoc @@ -39,7 +39,8 @@ Appends a message to the log. void jlLogF(const char *fmt, ...); ---- -Appends a formatted message, `printf` style. +Appends a formatted message, `printf` style, as one line: like `jlLog`, the +library ends the line, so the format needs no trailing `\n`. [cols="1,1,4", options="header"] |=== @@ -87,7 +88,6 @@ int main(void) { jlConfigT config; config.codegenBytes = 32UL * 1024; - config.audioBytes = 0; if (!jlInit(&config)) { return 1; } diff --git a/docs/ApiSerial.adoc b/docs/ApiSerial.adoc index f6a2744..5801ab1 100644 --- a/docs/ApiSerial.adoc +++ b/docs/ApiSerial.adoc @@ -143,16 +143,15 @@ and `JL_SERIAL_PRINTER` both mean the default port. `JL_SERIAL_DEFAULT` selects the IIgs modem port, DOS COM1, the ST Modem port, Amiga unit 0 or the X68000's RS-232C port. -On the X68000 the line is set by the RS-232C routines in charge. With -Human68k's RS-232C driver, RSDRV.SYS, loaded (the stock Human68k 3.02 system -disk's CONFIG.SYS loads it, and so do JoeyLib's own X68000 boot disks), the -port runs at 75 to 38400 bps and `JL_SERIAL_FLOW_RTSCTS` makes transmit wait +On the X68000 the serial add-on *requires* Human68k's RS-232C driver, +RSDRV.SYS. Every Human68k system disk has it and the stock CONFIG.SYS loads +it (`DEVICE = \SYS\RSDRV.SYS`), as do JoeyLib's own X68000 boot disks; a +game shipped to boot on its own must keep it. Without it `jlSerialOpen` +returns `false` and `jlLastError` says the driver is missing. With it the +port runs at 75 to 38400 bps, and `JL_SERIAL_FLOW_RTSCTS` makes transmit wait for CTS; RTS stays up, since JoeyLib's receive interrupt, not the driver's, fills the receive ring. 38400 is really 39062 bps, 1.7% fast, within the -usual tolerance of a UART at the far end. Without RSDRV.SYS the ROM's -routines allow 75 to 9600 bps: a faster request clamps to 9600, and -`JL_SERIAL_FLOW_RTSCTS` falls back to no handshaking. JoeyLib checks for the -driver at every `jlSerialOpen`. +usual tolerance of a UART at the far end. `unit` disambiguates `JL_SERIAL_SLOT` and is ignored otherwise: an IIgs Super Serial Card slot 1..7, a DOS COM number 1..4, or an Amiga `serial.device` unit. @@ -175,7 +174,9 @@ open without an intervening close fails. |=== *Returns:* `true` on success. `false` if the port cannot be opened or the -platform has no serial support -- and then every other call no-ops. +platform has no serial support -- and then every other call no-ops. On the +X68000 it is also `false` when RSDRV.SYS is not loaded, and `jlLastError` +says so. === jlSerialClose diff --git a/docs/ApiTile.adoc b/docs/ApiTile.adoc index 8906e50..fdae8da 100644 --- a/docs/ApiTile.adoc +++ b/docs/ApiTile.adoc @@ -131,8 +131,10 @@ against the build platform and mismatches are refused. |`path` |`const char *` |File path. |`outTiles` |`jlTileT *` |Receives the tiles. Caller-owned. |`maxTiles` |`uint16_t` |Capacity of `outTiles`. -|`outValid` |`bool *` |Optional, may be `NULL`. Set `true` per loaded slot, so - loaded tiles can be told from leftovers in a fixed-size array. +|`outValid` |`bool *` |Optional, may be `NULL`. One flag per slot up to + `maxTiles`: `true` for each loaded tile, `false` for the rest (all of them when + the file is refused), so loaded tiles can be told from leftovers in a + fixed-size array. |`outPalette` |`uint16_t *` |Optional, may be `NULL`. Receives the embedded 16-entry `$0RGB` palette if the file has one. |=== diff --git a/docs/Assets.adoc b/docs/Assets.adoc index d707556..cce118c 100644 --- a/docs/Assets.adoc +++ b/docs/Assets.adoc @@ -288,13 +288,19 @@ And a bake rule looks like: ---- $(DATA_DIR)/font.tbk: $(MY_ASSETS)/font.png @mkdir -p $(DATA_DIR) - python3 $(ASSETBAKE_BIN) --type tile --target atarist $< $@ + python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target atarist $< $@ ---- Add the baked files to the port's `all` target so they are built alongside the binaries. Remember that tile banks and `.spc` files need one rule *per platform*, while `.spr`, `.jym` and `.sfx` files are shared. +`examples/tilebank` is a working reference: every port's makefile bakes the +same PNG for its own target into `DATA/tilebank` (its `TILEBANK_DATA` rule), +and the program loads that bank both ways and shows the loader refusing the +other targets' bakes. Its `scripts/verify--tilebank.sh` gates run it on +every port. + === Loading Assets at Run Time [source,c] diff --git a/docs/BuildIntegration.adoc b/docs/BuildIntegration.adoc index 0248c81..c86207f 100644 --- a/docs/BuildIntegration.adoc +++ b/docs/BuildIntegration.adoc @@ -154,6 +154,12 @@ against your own program too. |`make iigs-verify` |The IIgs actually reached the program and drew something. +|`scripts/verify--tilebank.sh` +|The port's own tile-bank bake loads pixel-exact both ways, with the same + surface hash as the host check `scripts/check-tilebank.sh`, and banks baked + for the other machines are refused. `scripts/verify-all.sh tilebank` runs + every port's. + |`scripts/bench-.sh` |Per-operation throughput for that port. diff --git a/docs/GettingStarted.adoc b/docs/GettingStarted.adoc index 2f76b6c..b474e60 100644 --- a/docs/GettingStarted.adoc +++ b/docs/GettingStarted.adoc @@ -229,7 +229,6 @@ int main(void) { jlConfigT config; config.codegenBytes = 8 * 1024; - config.audioBytes = 64UL * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); @@ -272,7 +271,6 @@ int main(void) { uint8_t i; config.codegenBytes = 8 * 1024; - config.audioBytes = 0; if (!jlInit(&config)) { return 1; } @@ -368,6 +366,17 @@ example an AGI game's files. | `--debug` | Apple IIgs and X68000: start MAME's debugger. +| `--serial HOST:PORT` +| Connect the machine's serial port to the TCP server at `HOST:PORT`. See +<>. + +| `--serial-listen PORT` +| The machine's serial port waits for a TCP connection on `PORT` instead. + +| `--serial-baud N` +| Apple IIgs and X68000: the rate MAME's end of the wire runs at. The +default is 9600. + | `-- ARGS` | Pass `ARGS` to the program. DOS only: the other machines' autostart cannot pass arguments. @@ -389,6 +398,70 @@ GS/OS beside it and you open the program in the Finder. Given a bare program, `build/iigs/bin/DRAW`, it builds both disks itself, which needs the IIgs toolchain. +==== Serial Ports + +`--serial HOST:PORT` plugs the emulated machine's serial port into a TCP +connection: to a BBS, a program of your own, `nc -l PORT`, or another +`joey-run` started with `--serial-listen PORT`, which waits for the +connection instead. `HOST` is a name or an IPv4 address, and a listening +port accepts connections on every network interface. The link carries raw +bytes, with no telnet negotiation. `examples/sertest` sends a banner and then +echoes whatever it receives, which makes it a quick test of a link: + +[source,console] +---- +$ nc -l 2323 # in one terminal +$ ./joey-run --serial localhost:2323 --port dos sertest +---- + +The port is each machine's usual one: + +[cols="1,2,3",options="header"] +|=== +| Machine | Port | How the link is made + +| Apple IIgs +| The modem port +| MAME connects by itself. To listen, `socat` holds the TCP port and MAME +connects to it. The printer port and a Super Serial Card are not connected. + +| X68000 +| The RS-232C port +| As on the IIgs. + +| Atari ST +| The Modem port +| Hatari's serial port can only be a device, so `socat` joins a +pseudo-terminal to the network both ways. + +| Amiga +| `serial.device` unit 0 +| FS-UAE listens by itself, and holds the whole machine until the connection +arrives. To connect out, `socat` joins a pseudo-terminal to the network. + +| DOS +| COM1 +| DOSBox does both by itself. +|=== + +`socat` comes from your distribution: `./install.sh deps host --install` +fetches it with the other host tools, and `joey-run` says so when a run needs +it and it is missing. + +A few differences are worth knowing: + +* On the IIgs and X68000 MAME's null modem clocks the wire, at 9600 baud, +8N1, unless `--serial-baud` says otherwise. A program that opens the port at +another rate receives garbage. The other emulators pass bytes at whatever +rate the program sets. +* When nothing answers at `HOST:PORT`, `joey-run` stops, except on the IIgs +and X68000, where MAME prints `Unable to load image` and runs with the port +unplugged. +* While a port is listening, what the program sends waits for the connection, +except on DOS, which drops it just as a cable with nothing on the other end +would. `examples/sertest` sends its banner at once, so on DOS a client that +connects late sees only the echo. + ==== Firmware that is not downloaded Almost everything the emulators need is downloaded: EmuTOS for the Atari ST, @@ -432,6 +505,7 @@ built for every platform: |`sprite` |Sprite loading, compiling, save-under and restore. |`audio` |Tracker music, `.MOD` playback and digital sound effects. |`save` |Save files and the disk-space query. +|`tilebank` |Loading a baked tile bank both ways, and a bank for another machine refused. |`serial` |The optional RS-232 add-on. |`adventure` |A small complete game. |`agi` |A Sierra AGI interpreter, showing the library under real load. diff --git a/docs/ProgramLayout.adoc b/docs/ProgramLayout.adoc index d821417..e84c790 100644 --- a/docs/ProgramLayout.adoc +++ b/docs/ProgramLayout.adoc @@ -12,7 +12,6 @@ copy. jlConfigT config; config.codegenBytes = 32UL * 1024; // sprite blitter cache -config.audioBytes = 0; // reserved, not yet consulted if (!jlInit(&config)) { // jlLastError() explains why. There is no display yet, so on a @@ -273,7 +272,6 @@ int main(void) { jlConfigT config; config.codegenBytes = 32UL * 1024; - config.audioBytes = 0; if (!jlInit(&config)) { return 1; diff --git a/examples/adventure/adventure.c b/examples/adventure/adventure.c index 5cc4b10..f83b92d 100644 --- a/examples/adventure/adventure.c +++ b/examples/adventure/adventure.c @@ -270,13 +270,6 @@ static uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) { } -/* This single-file example can blow the 65816's 64 KB code-bank - * limit, so on the IIgs we split code across segments at section - * boundaries to spread the load across multiple banks. */ -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - /* =================================================================== * Section 4 -- Procedural sprite art * @@ -1097,10 +1090,6 @@ static bool buildAllSprites(void) { } -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - /* =================================================================== * Section 7 -- Room definitions * @@ -1646,10 +1635,6 @@ static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty) } -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - /* =================================================================== * Section 9 -- Ego animation / movement * @@ -1911,10 +1896,6 @@ static void drawInventoryBar(void) { } -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - /* =================================================================== * Section 13 -- Per-frame compositor * @@ -1976,10 +1957,6 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) { } -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - /* =================================================================== * Section 14 -- Verb / item interactions * @@ -2253,10 +2230,6 @@ static bool buildBackgrounds(void) { } -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - /* =================================================================== * Section 18 -- main() * =================================================================== */ @@ -2278,7 +2251,6 @@ int main(void) { #else config.codegenBytes = 256UL * 1024; #endif - config.audioBytes = 64UL * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/adventure2/adventure2.c b/examples/adventure2/adventure2.c index ed2ab3d..2df1d0f 100644 --- a/examples/adventure2/adventure2.c +++ b/examples/adventure2/adventure2.c @@ -235,13 +235,6 @@ static bool isTrunkChar(char c) { } -/* One TU's code can overflow the 65816's 64 KB code bank if it lands - * in a single segment. Split the heavier sections across segments to - * spread the load across multiple banks. */ -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - /* =================================================================== * Section 4 -- Sprite ASCII art (ego + cursors) * =================================================================== */ @@ -720,10 +713,6 @@ static void programScbForView(DebugViewE view) { } -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - /* =================================================================== * Section 8 -- Painting the three room screens (visual / priority / * control) @@ -926,10 +915,6 @@ static void drawEgoMasked(int16_t x, int16_t y, DirE dir, uint8_t frame) { } -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - /* =================================================================== * Section 10 -- Pathfinding via the control buffer * @@ -1209,10 +1194,6 @@ static void drawInventoryBar(void) { } -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - /* =================================================================== * Section 13 -- Frame compositor * =================================================================== */ @@ -1322,10 +1303,6 @@ static void handleClick(int16_t mx, int16_t my, bool leftClick, bool rightClick) } -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - /* =================================================================== * Section 15 -- main() * =================================================================== */ @@ -1340,7 +1317,6 @@ int main(void) { * be small. We need 4 surfaces (stage + visual + priority + * control). */ config.codegenBytes = 4UL * 1024; - config.audioBytes = 0; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/agi/agi.c b/examples/agi/agi.c index 16cde9e..13e2f8b 100644 --- a/examples/agi/agi.c +++ b/examples/agi/agi.c @@ -14,7 +14,6 @@ // whatever logic.0 picks", same as omitting the argument.) - #include "agi.h" #include "joey/audio.h" @@ -314,9 +313,6 @@ static uint32_t gAgiTickLastWallMs; static uint32_t gAgiV11SubTickAccum; // counts AGI ticks toward next v11++ -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - // ----- Internal helpers (alphabetical) ----- static void cbAddToPic(void *ctx, uint8_t viewId, uint8_t loop, uint8_t cel, @@ -1884,9 +1880,6 @@ static void updateEgoFromInput(void) { } -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - // ----- Entry point ----- int main(int argc, char **argv) { @@ -1903,7 +1896,6 @@ int main(int argc, char **argv) { // through the Sound Blaster auto-init DMA path (see agiSynthFill // and src/port/dos/audio.c). The audio pool covers the 4 KB // DMA buffer + libxmp-lite SMIX state. - cfg.audioBytes = 32u * 1024u; if (!jlInit(&cfg)) { return 1; } diff --git a/examples/agi/agiObj.c b/examples/agi/agiObj.c index 747ac3b..24629a7 100644 --- a/examples/agi/agiObj.c +++ b/examples/agi/agiObj.c @@ -23,10 +23,6 @@ #include -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - // AGI direction encoding. 0 means "no motion this cycle"; 1..8 // circle clockwise starting at north. // 1=N 2=NE 3=E 4=SE diff --git a/examples/agi/agiPic.c b/examples/agi/agiPic.c index efb216b..ae92566 100644 --- a/examples/agi/agiPic.c +++ b/examples/agi/agiPic.c @@ -81,9 +81,6 @@ static void opRelLine(AgiPicT *pic, const PicStateT *state, const uint8_t *d static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y); -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - // ----- Internal helpers (alphabetical) ----- static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data, uint16_t length, uint16_t *pos, bool yFirst) { diff --git a/examples/agi/agiRes.c b/examples/agi/agiRes.c index 6627a59..32da2cf 100644 --- a/examples/agi/agiRes.c +++ b/examples/agi/agiRes.c @@ -48,13 +48,6 @@ #define AGI_DIR_EMPTY_VOLUME 0xFF -// IIgs 64 KB code-bank rule: park this TU in its own load segment -// so it doesn't pile onto _ROOT (which still holds main() and the -// runtime startup). Cross-platform builds ignore this pragma. -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - // ----- Prototypes ----- static bool buildVolName(char *out, size_t outSize, uint8_t volNum); diff --git a/examples/agi/agiText.c b/examples/agi/agiText.c index 80bdc97..744e355 100644 --- a/examples/agi/agiText.c +++ b/examples/agi/agiText.c @@ -22,10 +22,6 @@ #include -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - #define GLYPH_W 8u #define GLYPH_H 8u #define FIRST_CHAR 0x20u // space diff --git a/examples/agi/agiView.c b/examples/agi/agiView.c index 4c75b9a..137d0d3 100644 --- a/examples/agi/agiView.c +++ b/examples/agi/agiView.c @@ -39,10 +39,6 @@ #define AGI_PRI_GROUND_BAND_PX 12 -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - // ----- File-local types ----- // Per-draw invariants for one cel blit, grouped so they live in memory @@ -284,9 +280,6 @@ uint8_t agiActorPriorityForY(int16_t y) { } -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, int16_t x, int16_t y, uint8_t actorPri, const uint8_t *picPriority, diff --git a/examples/agi/agiVm.c b/examples/agi/agiVm.c index 35515ee..4e7ec40 100644 --- a/examples/agi/agiVm.c +++ b/examples/agi/agiVm.c @@ -272,10 +272,6 @@ #define ACTION_UNKNOWN 0xFFu -#ifdef JOEYLIB_PLATFORM_IIGS -#endif - - // Result of one opcode-group handler in the split agiVmRun dispatch. // VM_DISPATCH_CONTINUE: opcode handled, advance to the next. RETURN: // handled, stop and let agiVmRun return vm->haltReason. UNHANDLED: opcode diff --git a/examples/audio/audio.c b/examples/audio/audio.c index 4ef718e..237af91 100644 --- a/examples/audio/audio.c +++ b/examples/audio/audio.c @@ -127,7 +127,6 @@ int main(void) { uint32_t sfxLen; config.codegenBytes = 8 * 1024; - config.audioBytes = 64UL * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/draw/draw.c b/examples/draw/draw.c index 5174bda..22651d3 100644 --- a/examples/draw/draw.c +++ b/examples/draw/draw.c @@ -243,7 +243,6 @@ int main(void) { jlSurfaceT *screen; config.codegenBytes = 8 * 1024; - config.audioBytes = 64UL * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/hello/hello.c b/examples/hello/hello.c index a47f82c..3caf34a 100644 --- a/examples/hello/hello.c +++ b/examples/hello/hello.c @@ -10,7 +10,6 @@ int main(void) { jlConfigT config; config.codegenBytes = 8 * 1024; - config.audioBytes = 64UL * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/joy/joy.c b/examples/joy/joy.c index 975cfe8..82495d0 100644 --- a/examples/joy/joy.c +++ b/examples/joy/joy.c @@ -218,7 +218,6 @@ int main(void) { jlSurfaceT *screen; config.codegenBytes = 8 * 1024; - config.audioBytes = 64UL * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/keys/keys.c b/examples/keys/keys.c index b07047f..3b54aac 100644 --- a/examples/keys/keys.c +++ b/examples/keys/keys.c @@ -595,7 +595,6 @@ int main(void) { int16_t cursorRow; config.codegenBytes = 8 * 1024; - config.audioBytes = 64UL * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/pattern/pattern.c b/examples/pattern/pattern.c index a64de86..ab3542e 100644 --- a/examples/pattern/pattern.c +++ b/examples/pattern/pattern.c @@ -106,7 +106,6 @@ int main(void) { jlSurfaceT *screen; config.codegenBytes = 8 * 1024; - config.audioBytes = 64UL * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/save/save.c b/examples/save/save.c index 5604cb1..9c77d4f 100644 --- a/examples/save/save.c +++ b/examples/save/save.c @@ -121,7 +121,6 @@ int main(void) { bool saved; config.codegenBytes = 8 * 1024; - config.audioBytes = 64UL * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/serial/serial.c b/examples/serial/serial.c index 204f236..312fce5 100644 --- a/examples/serial/serial.c +++ b/examples/serial/serial.c @@ -182,7 +182,6 @@ int main(void) { bool linkOpen; config.codegenBytes = 8 * 1024; - config.audioBytes = 64UL * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/serloss/serloss.c b/examples/serloss/serloss.c index 9dc94fb..8817f4c 100644 --- a/examples/serloss/serloss.c +++ b/examples/serloss/serloss.c @@ -265,7 +265,6 @@ int main(void) { #endif config.codegenBytes = 0u; - config.audioBytes = 0u; if (!jlInit(&config)) { MB_BASE[MB_PHASE] = PHASE_FAILED; for (;;) { diff --git a/examples/sermouse/sermouse.c b/examples/sermouse/sermouse.c new file mode 100644 index 0000000..ec58515 --- /dev/null +++ b/examples/sermouse/sermouse.c @@ -0,0 +1,355 @@ +// Serial and mouse together: traffic both ways on the serial port while the +// mouse is read and drawn, for a machine whose serial port and mouse share a +// chip. The X68000 is one: its Z8530 SCC carries the RS-232C port on channel A +// and the mouse on channel B, and the chip has ONE register pointer for both +// channels, so a select-then-access register sequence on one side must never +// be split by the other. scripts/verify-x68000-serial-mouse.sh runs this under +// MAME with a host peer on the line and Lua moving the mouse, and checks that +// neither side disturbs the other. +// +// Serial: the default port at 9600 8N1, both directions at once. +// - Transmits an endless stream, byte k = streamByte(k, TX_STREAM_MASK), +// STREAM_CHUNK bytes per loop. +// - Checks the peer's endless stream in order, byte k = streamByte(k, +// RX_STREAM_MASK), counting every byte that is not the one expected. +// - Raises DTR, which is already up, DTR_WRITES_PER_LOOP times per loop. A +// program raises it once; doing it constantly keeps the port's own +// select-then-write register sequence in the path of both channels' +// interrupts for the whole run. +// SPACE pauses and resumes transmitting. The peer answers byte for byte +// (scripts/lib/serialStreamPeer.py), so a pause quiets the line both ways. +// ESC quits. +// +// Mouse: a small pointer follows it. +// +// The bottom two stage rows are verification scanlines: one nibble per pixel, +// high nibble first, behind a sentinel, for a harness that reads the +// framebuffer back: +// row 198 serial: 3C3C, status, received, bad, sent, short writes, first bad +// row 199 mouse: 5A5A, present/left/right/middle, x, y (examples/keys' row) +// The serial row is restamped at most every SERIAL_ROW_FRAMES frames, so a +// reader that sees the same row on two consecutive frames knows it did not +// catch a present half done. The mouse row changes only with the mouse. +// +// Serial is an OPT-IN add-on: it is reached through , which the +// umbrella deliberately does NOT pull in. + +#include +#include + +#define LINE_BAUD 9600u +#define LINE_DATA_BITS 8u +#define LINE_STOP_BITS 1u + +#define STREAM_CHUNK 16u // bytes sent per loop, ~17 ms of line at 9600 +#define READ_CHUNK 64u +#define DTR_WRITES_PER_LOOP 16u +#define RX_STREAM_MASK 0x00u // the peer's stream +#define TX_STREAM_MASK 0xA5u // ours, so the two can never be mistaken +#define BYTE_BITS 8 +#define NO_BAD_INDEX 0xFFFFFFFFul +#define COUNT_SATURATED 0xFFFFu + +#define STATUS_OPEN 0x0001u +#define STATUS_PAUSED 0x0002u + +// Verification rows. Every field is a 16-bit word (4 nibbles) or a 32-bit +// long (8 nibbles), laid end to end from the sentinel at x = 0. +#define NIBBLE_BITS 4 +#define NIBBLE_MASK 0x0Fu +#define WORD_NIBBLES 4 +#define LONG_NIBBLES 8 +#define SERIAL_ROW_Y 198 +#define SERIAL_ROW_SENTINEL 0x3C3Cu +#define SERIAL_STATUS_X WORD_NIBBLES +#define SERIAL_RECEIVED_X (SERIAL_STATUS_X + WORD_NIBBLES) +#define SERIAL_BAD_X (SERIAL_RECEIVED_X + LONG_NIBBLES) +#define SERIAL_SENT_X (SERIAL_BAD_X + WORD_NIBBLES) +#define SERIAL_SHORT_X (SERIAL_SENT_X + LONG_NIBBLES) +#define SERIAL_FIRST_BAD_X (SERIAL_SHORT_X + WORD_NIBBLES) +#define SERIAL_ROW_FRAMES 4u +#define MOUSE_ROW_Y 199 +#define MOUSE_ROW_SENTINEL 0x5A5Au +#define MOUSE_FLAGS_X WORD_NIBBLES +#define MOUSE_XPOS_X (MOUSE_FLAGS_X + WORD_NIBBLES) +#define MOUSE_YPOS_X (MOUSE_XPOS_X + WORD_NIBBLES) + +// The mouse flags word, one nibble per predicate, in examples/keys' order. +#define MOUSE_FLAG_PRESENT 0x1000u +#define MOUSE_FLAG_LEFT 0x0100u +#define MOUSE_FLAG_RIGHT 0x0010u +#define MOUSE_FLAG_MIDDLE 0x0001u +#define MOUSE_FLAGS_NONE 0xFFFFu // never a real flags word: forces the first stamp + +#define POINTER_SIZE 4 +#define COLOR_BACKGROUND 0 +#define COLOR_POINTER 15 +#define PALETTE_RED_SHIFT 8 +#define PALETTE_GREEN_SHIFT 4 + +typedef struct StreamStatsT { + uint32_t received; // peer bytes read so far = index of the next one expected + uint32_t sent; // our bytes accepted by jlSerialWrite + uint32_t firstBad; // index of the first unexpected byte, NO_BAD_INDEX if none + uint16_t bad; // unexpected bytes, saturating + uint16_t shortWrites; // jlSerialWrite calls that took less than asked, saturating +} StreamStatsT; + +typedef struct MouseRowT { + int16_t x; + int16_t y; + uint16_t flags; +} MouseRowT; + +typedef struct PointerT { + int16_t x; + int16_t y; + bool drawn; +} PointerT; + +static void buildPalette(jlSurfaceT *screen); +static void fillPointer(jlSurfaceT *screen, int16_t x, int16_t y, uint8_t color); +static uint16_t mouseFlags(void); +static void movePointer(jlSurfaceT *screen, PointerT *pointer); +static void receiveStream(StreamStatsT *stats); +static void sendStream(StreamStatsT *stats); +static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint32_t value, int16_t nibbles); +static void stampSerialRow(jlSurfaceT *screen, const StreamStatsT *stats, uint16_t status); +static uint8_t streamByte(uint32_t index, uint8_t mask); +static void updateMouseRow(jlSurfaceT *screen, MouseRowT *last); + + +// A grey ramp, so every index the verification rows stamp is visible. +static void buildPalette(jlSurfaceT *screen) { + uint16_t colors[SURFACE_COLORS_PER_PALETTE]; + uint16_t i; + + for (i = 0u; i < SURFACE_COLORS_PER_PALETTE; i++) { + colors[i] = (uint16_t)((i << PALETTE_RED_SHIFT) | (i << PALETTE_GREEN_SHIFT) | i); + } + jlPaletteSet(screen, 0, colors); +} + + +// The pointer block at (x, y), clipped above the verification rows so it can +// never draw over them: a harness samples those between frames. +static void fillPointer(jlSurfaceT *screen, int16_t x, int16_t y, uint8_t color) { + int16_t w; + int16_t h; + + w = POINTER_SIZE; + h = POINTER_SIZE; + if ((int16_t)(x + w) > SURFACE_WIDTH) { + w = (int16_t)(SURFACE_WIDTH - x); + } + if ((int16_t)(y + h) > SERIAL_ROW_Y) { + h = (int16_t)(SERIAL_ROW_Y - y); + } + if (w <= 0 || h <= 0) { + return; + } + jlFillRect(screen, x, y, (uint16_t)w, (uint16_t)h, color); +} + + +static uint16_t mouseFlags(void) { + return (uint16_t)((jlMousePresent() ? MOUSE_FLAG_PRESENT : 0u) | + (jlMouseDown(MOUSE_BUTTON_LEFT) ? MOUSE_FLAG_LEFT : 0u) | + (jlMouseDown(MOUSE_BUTTON_RIGHT) ? MOUSE_FLAG_RIGHT : 0u) | + (jlMouseDown(MOUSE_BUTTON_MIDDLE) ? MOUSE_FLAG_MIDDLE : 0u)); +} + + +// Erase the pointer where it was and draw it where the mouse is now. +static void movePointer(jlSurfaceT *screen, PointerT *pointer) { + int16_t x; + int16_t y; + + x = jlMouseX(); + y = jlMouseY(); + if (pointer->drawn && x == pointer->x && y == pointer->y) { + return; + } + if (pointer->drawn) { + fillPointer(screen, pointer->x, pointer->y, COLOR_BACKGROUND); + } + fillPointer(screen, x, y, COLOR_POINTER); + pointer->x = x; + pointer->y = y; + pointer->drawn = true; +} + + +// Read everything that has arrived and check it against the peer's stream. +static void receiveStream(StreamStatsT *stats) { + uint8_t buf[READ_CHUNK]; + uint16_t got; + uint16_t i; + + jlSerialPoll(); + got = jlSerialRead(buf, READ_CHUNK); + while (got > 0u) { + for (i = 0u; i < got; i++) { + if (buf[i] != streamByte(stats->received, RX_STREAM_MASK)) { + if (stats->firstBad == NO_BAD_INDEX) { + stats->firstBad = stats->received; + } + if (stats->bad < COUNT_SATURATED) { + stats->bad++; + } + } + stats->received++; + } + got = jlSerialRead(buf, READ_CHUNK); + } +} + + +// Send the next STREAM_CHUNK bytes of our stream. A short write resumes from +// the first byte not taken, so the stream stays gapless either way. +static void sendStream(StreamStatsT *stats) { + uint8_t chunk[STREAM_CHUNK]; + uint16_t sent; + uint16_t i; + + for (i = 0u; i < STREAM_CHUNK; i++) { + chunk[i] = streamByte(stats->sent + i, TX_STREAM_MASK); + } + sent = jlSerialWrite(chunk, STREAM_CHUNK); + stats->sent += sent; + if (sent < STREAM_CHUNK && stats->shortWrites < COUNT_SATURATED) { + stats->shortWrites++; + } +} + + +// Stamp `value` into `nibbles` consecutive pixels, high nibble first. +static void stampNibbles(jlSurfaceT *screen, int16_t x, int16_t y, uint32_t value, int16_t nibbles) { + int16_t i; + int16_t shift; + + for (i = 0; i < nibbles; i++) { + shift = (int16_t)(NIBBLE_BITS * (nibbles - 1 - i)); + jlDrawPixel(screen, (int16_t)(x + i), y, (uint8_t)((value >> shift) & NIBBLE_MASK)); + } +} + + +static void stampSerialRow(jlSurfaceT *screen, const StreamStatsT *stats, uint16_t status) { + stampNibbles(screen, 0, SERIAL_ROW_Y, SERIAL_ROW_SENTINEL, WORD_NIBBLES); + stampNibbles(screen, SERIAL_STATUS_X, SERIAL_ROW_Y, status, WORD_NIBBLES); + stampNibbles(screen, SERIAL_RECEIVED_X, SERIAL_ROW_Y, stats->received, LONG_NIBBLES); + stampNibbles(screen, SERIAL_BAD_X, SERIAL_ROW_Y, stats->bad, WORD_NIBBLES); + stampNibbles(screen, SERIAL_SENT_X, SERIAL_ROW_Y, stats->sent, LONG_NIBBLES); + stampNibbles(screen, SERIAL_SHORT_X, SERIAL_ROW_Y, stats->shortWrites, WORD_NIBBLES); + stampNibbles(screen, SERIAL_FIRST_BAD_X, SERIAL_ROW_Y, stats->firstBad, LONG_NIBBLES); +} + + +// Byte `index` of a stream: the index's low byte XOR its next byte, XOR the +// stream's mask. A lost or repeated byte throws every byte after it off its +// expected value, where a plain counter would miss any gap that is a multiple +// of 256. scripts/lib/serialStreamPeer.py uses the same formula and masks. +static uint8_t streamByte(uint32_t index, uint8_t mask) { + return (uint8_t)(index ^ (index >> BYTE_BITS) ^ mask); +} + + +// Restamp the mouse row when the reported state has changed. +static void updateMouseRow(jlSurfaceT *screen, MouseRowT *last) { + int16_t x; + int16_t y; + uint16_t flags; + + x = jlMouseX(); + y = jlMouseY(); + flags = mouseFlags(); + if (x == last->x && y == last->y && flags == last->flags) { + return; + } + stampNibbles(screen, 0, MOUSE_ROW_Y, MOUSE_ROW_SENTINEL, WORD_NIBBLES); + stampNibbles(screen, MOUSE_FLAGS_X, MOUSE_ROW_Y, flags, WORD_NIBBLES); + stampNibbles(screen, MOUSE_XPOS_X, MOUSE_ROW_Y, (uint16_t)x, WORD_NIBBLES); + stampNibbles(screen, MOUSE_YPOS_X, MOUSE_ROW_Y, (uint16_t)y, WORD_NIBBLES); + last->x = x; + last->y = y; + last->flags = flags; +} + + +int main(void) { + jlConfigT config; + jlSerialConfigT line; + jlSurfaceT *screen; + StreamStatsT stats; + MouseRowT mouseRow; + PointerT pointer; + uint16_t status; + uint16_t rowFrame; + uint16_t i; + bool paused; + + config.codegenBytes = 0u; + if (!jlInit(&config)) { + return 1; + } + screen = jlStageGet(); + buildPalette(screen); + jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); + jlSurfaceClear(screen, COLOR_BACKGROUND); + + line.baud = LINE_BAUD; + line.dataBits = LINE_DATA_BITS; + line.stopBits = LINE_STOP_BITS; + line.parity = JL_SERIAL_PARITY_NONE; + line.flow = JL_SERIAL_FLOW_NONE; + line.unit = 0u; + status = jlSerialOpen(JL_SERIAL_DEFAULT, &line) ? STATUS_OPEN : 0u; + + stats.received = 0ul; + stats.sent = 0ul; + stats.firstBad = NO_BAD_INDEX; + stats.bad = 0u; + stats.shortWrites = 0u; + mouseRow.x = -1; + mouseRow.y = -1; + mouseRow.flags = MOUSE_FLAGS_NONE; + pointer.x = 0; + pointer.y = 0; + pointer.drawn = false; + paused = false; + + jlInputPoll(); + stampSerialRow(screen, &stats, status); + updateMouseRow(screen, &mouseRow); + jlStagePresent(); + rowFrame = jlFrameCount(); + + for (;;) { + jlInputPoll(); + if (jlKeyPressed(KEY_ESCAPE)) { + break; + } + if (jlKeyPressed(KEY_SPACE)) { + paused = !paused; + } + receiveStream(&stats); + if (!paused) { + sendStream(&stats); + } + for (i = 0u; i < DTR_WRITES_PER_LOOP; i++) { + jlSerialSetDtr(true); + } + movePointer(screen, &pointer); + updateMouseRow(screen, &mouseRow); + if ((uint16_t)(jlFrameCount() - rowFrame) >= SERIAL_ROW_FRAMES) { + rowFrame = jlFrameCount(); + stampSerialRow(screen, &stats, (uint16_t)(status | (paused ? STATUS_PAUSED : 0u))); + } + jlStagePresent(); + } + + jlSerialClose(); + jlShutdown(); + return 0; +} diff --git a/examples/spacetaxi/assets/extractFlame.py b/examples/spacetaxi/assets/extractFlame.py deleted file mode 100644 index 8fc7d21..0000000 --- a/examples/spacetaxi/assets/extractFlame.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -# extractFlame.py -- wire the engine-flame sprite cels from the C64 ROM -# image (stuff/spacetaxi/raw.bin) into the sprite sheet's flame row. -# -# The flame is C64 sprite 2, MULTICOLOR (verified: $D01C bit2 set). Its -# per-direction cel is chosen from the pointer table at $6DB0 (indexed by -# the direction mask $716A). Reading raw.bin at $6DB0 (2-byte load-addr -# header -> +2) gives, and the port's kFlameCelByDirMask mirrors exactly: -# mask 1 UP -> block $D8 mask 8 RIGHT -> block $D7 -# mask 2 DOWN -> block $D4 mask 9 UP+RIGHT -> block $D3 -# mask 4 LEFT -> block $D5 mask 10 DOWN+RIGHT-> block $D6 -# mask 5 UP+LEFT -> block $D2 -# mask 6 DOWN+LEFT -> block $D1 -# flameCels[0..7] load from .spr cels 18..25 (sheet row 2, cols 0..7), so -# the cel order below is the kFlameCelByDirMask index order. -# -# Sprite data lives at block*64 in VIC bank 0 ($3000-$37FF). Each C64 -# multicolor sprite is 12 MC-pixels x 21 rows (3 bytes/row, 2 bits/pixel); -# each MC pixel is 2 hi-res pixels, so it maps 1 MC pixel -> 2 of the -# port's 24-wide 4bpp cel pixels. Colors are the live-level flame palette -# ($7D05->$D025=$02 red = MC0, $7D06->$D026=$07 yellow = MC1; sprite-2 -# colour = orange), mapped to sprites.png's indexed palette. -# -# Patches ONLY the flame row (row 2) of sprites.png -- taxi (row 0), -# passenger (row 1), and font.png are left untouched. Re-run the .spr -# bake afterwards (make/.mk STAXI_SPR rule). - -import os -import sys - -try: - from PIL import Image -except ImportError: - sys.exit("extractFlame.py: needs Pillow (pip install pillow)") - -HERE = os.path.dirname(os.path.abspath(__file__)) -RAW = os.path.join(HERE, "..", "..", "..", "stuff", "spacetaxi", "raw.bin") -PNG = os.path.join(HERE, "sprites", "sprites.png") - -CEL = 24 # cel is 24x24 px -LOAD_HDR = 2 # raw.bin 2-byte load-address header - -# flameCels index -> C64 sprite block (from $6DB0 / kFlameCelByDirMask). -FLAME_BLOCKS = [0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6] - -# Multicolor 2-bit pixel -> palette index. The engine renders sprite -# cels through kDefaultPalette (stRender.c), whose index IS the C64 VIC-II -# colour code -- the sprite's own baked palette is ignored (loadSpriteSheet -# passes NULL). So the C64 flame colours map DIRECTLY by code: -# 00 transparent(0), 01 MC0=$D025=$02 red(2), 11 MC1=$D026=$07 yellow(7), -# 10 sprite-2 colour -> orange(8) (barely used by these cels). -# (NOT the sprites.png preview-palette indices, which differ.) -MC_TO_INDEX = {0: 0, 1: 2, 2: 8, 3: 7} - -FLAME_ROW_Y = 2 * CEL # sheet row 2 (flame row) top edge - - -def loadRaw(): - with open(RAW, "rb") as f: - return f.read() - - -def celFromBlock(raw, block): - # Return a 24x24 list-of-rows of palette indices for one flame block. - base = LOAD_HDR + block * 64 - cel = [[0] * CEL for _ in range(CEL)] - for row in range(21): # C64 sprite is 21 rows tall - for byteIdx in range(3): # 3 bytes/row - b = raw[base + row * 3 + byteIdx] - for pair in range(4): # 4 MC pixels per byte (MSB first) - mc = (b >> ((3 - pair) * 2)) & 3 - idx = MC_TO_INDEX[mc] - px = (byteIdx * 4 + pair) * 2 # MC pixel -> 2 cel pixels - cel[row][px] = idx - cel[row][px + 1] = idx - return cel - - -def main(): - raw = loadRaw() - im = Image.open(PNG) - if im.mode != "P": - sys.exit(f"extractFlame.py: {PNG} is {im.mode}, expected indexed 'P'") - px = im.load() - nonEmpty = 0 - for celIdx, block in enumerate(FLAME_BLOCKS): - cel = celFromBlock(raw, block) - x0 = celIdx * CEL - opaque = 0 - for y in range(CEL): - for x in range(CEL): - v = cel[y][x] - px[x0 + x, FLAME_ROW_Y + y] = v - if v != 0: - opaque += 1 - nonEmpty += 1 if opaque else 0 - print(f" flameCels[{celIdx}] block ${block:02X}: {opaque} opaque px -> cel {18 + celIdx}") - im.save(PNG) - print(f"extractFlame: patched {nonEmpty}/8 flame cels into {os.path.relpath(PNG, HERE)}") - - -if __name__ == "__main__": - main() diff --git a/examples/spacetaxi/assets/font.png b/examples/spacetaxi/assets/font.png deleted file mode 100644 index 3bad103..0000000 --- a/examples/spacetaxi/assets/font.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:74c85ec34cf69de066faabfa9ddab61768e254c5aeabf48f73dbf5315dccc469 -size 1379 diff --git a/examples/spacetaxi/assets/genPlaceholderArt.py b/examples/spacetaxi/assets/genPlaceholderArt.py deleted file mode 100644 index 6b7e84a..0000000 --- a/examples/spacetaxi/assets/genPlaceholderArt.py +++ /dev/null @@ -1,301 +0,0 @@ -#!/usr/bin/env python3 -# genPlaceholderArt.py -- generate generic geometric placeholder art -# for the Space Taxi port. Output is 320x200 indexed PNG, 16-color. -# Replace these with hand-authored art any time; the build pipeline -# picks up whichever PNG sits in assets/. - -import os -from PIL import Image, ImageDraw - -ROOT = os.path.dirname(os.path.abspath(__file__)) - -# 16-color palette matching the engine's kDefaultPalette (see stRender.c). -# Each entry is (R, G, B) in 0..255. The .jas format will store the -# 4-bit-per-channel quantization automatically via joeyasset. -PALETTE_RGB = [ - (0x00, 0x00, 0x00), # 0 black - (0x00, 0x00, 0x77), # 1 dark blue - (0xCC, 0x00, 0x00), # 2 red - (0x00, 0x77, 0x00), # 3 dark green - (0x66, 0x44, 0x00), # 4 brown - (0xEE, 0xEE, 0x00), # 5 yellow - (0x88, 0x88, 0x88), # 6 mid gray - (0xBB, 0xBB, 0xBB), # 7 light gray - (0x44, 0x66, 0xFF), # 8 sky blue - (0x77, 0x88, 0xFF), # 9 light blue - (0xFF, 0x88, 0x88), # 10 light red - (0x88, 0xFF, 0x44), # 11 light green - (0xFF, 0x88, 0x00), # 12 orange - (0xFF, 0x00, 0xFF), # 13 magenta - (0x00, 0xFF, 0xFF), # 14 cyan - (0xFF, 0xFF, 0xFF), # 15 white -] - - -def new_canvas(w=320, h=200): - """Returns a fresh w x h indexed PIL image with the palette installed.""" - img = Image.new('P', (w, h), 0) - flat = [] - for r, g, b in PALETTE_RGB: - flat.extend([r, g, b]) - flat.extend([0] * (768 - len(flat))) # pad to 256 entries - img.putpalette(flat) - return img - - -def write_indexed_png(img, path): - """Save with mode='P' so PNG output is indexed (joeyasset wants this - when converted through ppm; ImageMagick's `convert` preserves - indexed mode when writing PPM).""" - img.save(path, 'PNG', optimize=True) - print(f" wrote {path}") - - -def gen_tilebank(): - """tilebank0.png: 320x200, 40x25 cells of 8x8 each (= 1000 tile - slots). The engine convention is: - index 0 = empty (non-solid) - index 1..63 = solid walls - index 64..127 = landing-pad surfaces - index 128..255 = decorative non-solid - We fill a few representative slots; the rest stay black so they - show as "empty" if a level references them. - """ - img = new_canvas() - px = img.load() - - def draw_tile(idx, painter): - bx = (idx % 40) * 8 - by = (idx // 40) * 8 - painter(bx, by) - - def block(color): - return lambda x, y: [px.__setitem__((x+dx, y+dy), color) - for dx in range(8) for dy in range(8)] - - def bricks(color_a, color_b): - def paint(x, y): - for dy in range(8): - row_offset = 4 if (y // 8 + dy // 4) % 2 == 0 else 0 - for dx in range(8): - is_grout = (dx == (row_offset % 8)) or (dy == 3 or dy == 7) - px[x + dx, y + dy] = color_b if is_grout else color_a - return paint - - def stripes(color_a, color_b): - def paint(x, y): - for dy in range(8): - c = color_a if (dy + (x // 8)) % 2 == 0 else color_b - for dx in range(8): - px[x + dx, y + dy] = c - return paint - - def landing_pad(color_a, color_b): - # diagonal stripes signal a landing-pad surface - def paint(x, y): - for dy in range(8): - for dx in range(8): - px[x + dx, y + dy] = color_a if ((dx + dy) // 2) % 2 == 0 else color_b - return paint - - # tile 0 -> already black (empty); leave as-is - - # walls 1..7: a variety of brick/stripe patterns - draw_tile(1, bricks(6, 0)) # gray brick - draw_tile(2, bricks(4, 0)) # brown brick - draw_tile(3, block(6)) # solid gray - draw_tile(4, block(4)) # solid brown - draw_tile(5, stripes(6, 7)) # gray stripes (ceiling) - draw_tile(6, stripes(1, 8)) # blue stripes (sky deco) - draw_tile(7, block(7)) # solid light gray - - # landing pad tiles 64..67 - draw_tile(64, landing_pad(5, 12)) # yellow/orange pad - draw_tile(65, landing_pad(11, 3)) # green pad - draw_tile(66, landing_pad(9, 8)) # blue pad - draw_tile(67, landing_pad(15, 7)) # white pad - - write_indexed_png(img, os.path.join(ROOT, 'tiles', 'tilebank0.png')) - - -def gen_sprites(): - """RETIRED 2026-07-20: sprites.png is now fully extracted from the - C64 dump by stuff/spacetaxi/extractSprites.py (taxi $C1/$C0/$DC/ - $DD, passenger $C4-$CB/$D9, flame $6DB0 table, warp $E2-$E8 -- - 216x96, 4 rows). Regenerating placeholder art here would clobber - the real sheet, the same trap gen_font has with the hand-edited - font.png. Refuses to run; use extractSprites.py instead.""" - print("gen_sprites: REFUSING -- sprites.png is real C64 art now.") - print(" regenerate via: python3 stuff/spacetaxi/extractSprites.py " - "stuff/spacetaxi/raw.bin examples/spacetaxi/assets/sprites/sprites.png") - return - - -def gen_sprites_retired(): - img = new_canvas(9 * 24, 3 * 24) # 216x72: 9 cols x 3 rows of 24x24 - d = ImageDraw.Draw(img) - - # ---- Taxi cels: simple side-view "pod with thruster" ---- - # Cel 0 = idle (thruster off) - # Cel 1..3 = thrust-on, alternating flame frames - for cel in range(4): - x = cel * 24 - # Pod body: rounded rectangle in gray - d.rectangle([x+2, 4, x+21, 16], fill=7, outline=15) - # Cockpit window in blue - d.rectangle([x+13, 6, x+19, 10], fill=8, outline=15) - # Landing skid - d.line([(x+3, 17), (x+20, 17)], fill=6) - d.line([(x+3, 17), (x+3, 19)], fill=6) - d.line([(x+20,17), (x+20, 19)], fill=6) - # Thruster flame (cels 1..3 only, alternating shape) - if cel >= 1: - flame_color = [2, 12, 5][(cel - 1) % 3] - flame_top = 18 - flame_bot = 22 if cel != 2 else 23 - d.polygon([(x+8, flame_top), - (x+15, flame_top), - (x+12, flame_bot)], fill=flame_color) - - # ---- Taxi shrink cels (transporter warp): the cab receding into the - # distance, 5 progressively smaller sizes, centered in the 24x24 cell. - # Placed in the free row-0 slots (cels 4..8). No flame -- it is warping - # away, not thrusting. The renderer draws one per warp step ($E2..$E9 - # on the C64 = 8 shrink cels; the port approximates with 5 sizes). - for i in range(5): - cx = (4 + i) * 24 + 12 - scale = 1.0 - i * 0.19 # 1.0, 0.81, 0.62, 0.43, 0.24 - bw = max(3, int(round(19 * scale))) - bh = max(2, int(round(12 * scale))) - x0 = cx - bw // 2 - y0 = 12 - bh // 2 - # Pod body (gray, white outline) - d.rectangle([x0, y0, x0 + bw, y0 + bh], fill=7, outline=15) - # Cockpit window (blue) while big enough to show - if bw >= 8: - d.rectangle([x0 + bw - 5, y0 + 1, x0 + bw - 1, y0 + 3], fill=8) - - # ---- Passenger cels: small stick figure, walk cycle ---- - # Centered in 16x16, walking right. - for cel in range(4): - x = cel * 16 - y = 24 - # Head - d.ellipse([x+5, y+1, x+10, y+6], fill=10, outline=15) - # Body - d.line([(x+7, y+7), (x+7, y+12)], fill=15) - # Arms (swing alternately based on cel) - if cel == 0 or cel == 2: - d.line([(x+4, y+10), (x+10, y+10)], fill=15) - else: - d.line([(x+3, y+9), (x+11, y+11)], fill=15) - # Legs (alternating stride) - if cel == 0 or cel == 2: - d.line([(x+7, y+12), (x+4, y+15)], fill=15) - d.line([(x+7, y+12), (x+10,y+15)], fill=15) - else: - d.line([(x+7, y+12), (x+5, y+15)], fill=15) - d.line([(x+7, y+12), (x+9, y+15)], fill=15) - - write_indexed_png(img, os.path.join(ROOT, 'sprites', 'sprites.png')) - - -def gen_font(): - """font.png: 320x200 (40x25 grid of 8x8 ASCII glyphs). - Authored manually for the printable subset (0x20..0x7E). - Each glyph cell is at (ascii%40, ascii/40) when ascii<128. - Unfilled cells stay black (treated as TILE_NO_GLYPH at runtime). - - WARNING: this generator is UPPERCASE-ONLY (lowercase maps to the - uppercase glyphs). The COMMITTED font.png has been hand-extended with - real lowercase glyphs, so running this clobbers it -- the HUD reverts - to all-caps. Regenerate SPRITES only (gen_sprites) unless you also add - the lowercase glyphs here first; then `git checkout` font.png. - """ - # A compact 5-pixel-wide bitmap font, drawn in 7 rows with 1px - # padding on all sides (so 5x7 active inside an 8x8 cell, baseline - # at row 6). The font supports digits, uppercase letters, space, - # and basic punctuation -- enough for HUD text. Lowercase letters - # map to the same glyphs as uppercase (room to add later). - GLYPHS = { - ' ': [], - '!': ['..#..', '..#..', '..#..', '..#..', '.....', '..#..', '.....'], - '"': ['.#.#.', '.#.#.', '.....', '.....', '.....', '.....', '.....'], - '0': ['.###.', '#...#', '#..##', '#.#.#', '##..#', '#...#', '.###.'], - '1': ['..#..', '.##..', '..#..', '..#..', '..#..', '..#..', '.###.'], - '2': ['.###.', '#...#', '....#', '...#.', '..#..', '.#...', '#####'], - '3': ['.###.', '#...#', '....#', '..##.', '....#', '#...#', '.###.'], - '4': ['...#.', '..##.', '.#.#.', '#..#.', '#####', '...#.', '...#.'], - '5': ['#####', '#....', '####.', '....#', '....#', '#...#', '.###.'], - '6': ['.###.', '#....', '#....', '####.', '#...#', '#...#', '.###.'], - '7': ['#####', '....#', '...#.', '..#..', '..#..', '..#..', '..#..'], - '8': ['.###.', '#...#', '#...#', '.###.', '#...#', '#...#', '.###.'], - '9': ['.###.', '#...#', '#...#', '.####', '....#', '....#', '.###.'], - ':': ['.....', '..#..', '.....', '.....', '.....', '..#..', '.....'], - '.': ['.....', '.....', '.....', '.....', '.....', '..#..', '.....'], - '-': ['.....', '.....', '.....', '#####', '.....', '.....', '.....'], - '/': ['....#', '...#.', '...#.', '..#..', '.#...', '.#...', '#....'], - 'A': ['.###.', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'], - 'B': ['####.', '#...#', '#...#', '####.', '#...#', '#...#', '####.'], - 'C': ['.###.', '#...#', '#....', '#....', '#....', '#...#', '.###.'], - 'D': ['####.', '#...#', '#...#', '#...#', '#...#', '#...#', '####.'], - 'E': ['#####', '#....', '#....', '####.', '#....', '#....', '#####'], - 'F': ['#####', '#....', '#....', '####.', '#....', '#....', '#....'], - 'G': ['.###.', '#...#', '#....', '#..##', '#...#', '#...#', '.###.'], - 'H': ['#...#', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'], - 'I': ['.###.', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'], - 'J': ['..###', '...#.', '...#.', '...#.', '...#.', '#..#.', '.##..'], - 'K': ['#...#', '#..#.', '#.#..', '##...', '#.#..', '#..#.', '#...#'], - 'L': ['#....', '#....', '#....', '#....', '#....', '#....', '#####'], - 'M': ['#...#', '##.##', '#.#.#', '#.#.#', '#...#', '#...#', '#...#'], - 'N': ['#...#', '##..#', '#.#.#', '#.#.#', '#..##', '#...#', '#...#'], - 'O': ['.###.', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'], - 'P': ['####.', '#...#', '#...#', '####.', '#....', '#....', '#....'], - 'Q': ['.###.', '#...#', '#...#', '#...#', '#.#.#', '#..#.', '.##.#'], - 'R': ['####.', '#...#', '#...#', '####.', '#.#..', '#..#.', '#...#'], - 'S': ['.###.', '#...#', '#....', '.###.', '....#', '#...#', '.###.'], - 'T': ['#####', '..#..', '..#..', '..#..', '..#..', '..#..', '..#..'], - 'U': ['#...#', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'], - 'V': ['#...#', '#...#', '#...#', '#...#', '.#.#.', '.#.#.', '..#..'], - 'W': ['#...#', '#...#', '#...#', '#.#.#', '#.#.#', '##.##', '#...#'], - 'X': ['#...#', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '#...#'], - 'Y': ['#...#', '#...#', '.#.#.', '..#..', '..#..', '..#..', '..#..'], - 'Z': ['#####', '....#', '...#.', '..#..', '.#...', '#....', '#####'], - } - - img = new_canvas() - px = img.load() - - def paint_glyph(ascii_code, glyph_rows): - col = ascii_code % 40 - row = ascii_code // 40 - bx = col * 8 - by = row * 8 - # active 5x7 area: cell-x 1..5, cell-y 0..6 (top row, 1px left pad) - for ry, line in enumerate(glyph_rows): - for rx, ch in enumerate(line): - if ch == '#': - px[bx + 1 + rx, by + ry] = 15 # white - - for ascii_code in range(32, 127): - c = chr(ascii_code) - if c in GLYPHS: - paint_glyph(ascii_code, GLYPHS[c]) - elif 'a' <= c <= 'z': - paint_glyph(ascii_code, GLYPHS[c.upper()]) - - write_indexed_png(img, os.path.join(ROOT, 'font.png')) - - -def main(): - os.makedirs(os.path.join(ROOT, 'tiles'), exist_ok=True) - os.makedirs(os.path.join(ROOT, 'sprites'), exist_ok=True) - print("genPlaceholderArt: emitting 320x200 indexed PNGs...") - gen_tilebank() - gen_sprites() - gen_font() - print("done.") - - -if __name__ == '__main__': - main() diff --git a/examples/spacetaxi/assets/sprites/sprites.png b/examples/spacetaxi/assets/sprites/sprites.png deleted file mode 100644 index 85b7bb7..0000000 --- a/examples/spacetaxi/assets/sprites/sprites.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f6c0257eff1f87dc285fb951c460ae91c546a79883be959ebf5ee445979ab59a -size 843 diff --git a/examples/spacetaxi/assets/tiles/tbank0.png b/examples/spacetaxi/assets/tiles/tbank0.png deleted file mode 100644 index fc5ee47..0000000 --- a/examples/spacetaxi/assets/tiles/tbank0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc2c7c8b5e7e76db87264621c938e9328059949ffa097199c428b83c51fce9ee -size 1879 diff --git a/examples/spacetaxi/assets/tiles/tbank1.png b/examples/spacetaxi/assets/tiles/tbank1.png deleted file mode 100644 index f83877b..0000000 --- a/examples/spacetaxi/assets/tiles/tbank1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6fee43fab89a4a9784e0900fd4d58c4b0e0b2ed1872bdf746fcfaa14f59eca9f -size 1201 diff --git a/examples/spacetaxi/assets/tiles/tbank2.png b/examples/spacetaxi/assets/tiles/tbank2.png deleted file mode 100644 index 1cb395a..0000000 --- a/examples/spacetaxi/assets/tiles/tbank2.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a056acda9a6cb50b2a7941830750e820898087880995d65c514d98d19d0a56ea -size 1237 diff --git a/examples/spacetaxi/assets/tiles/tilebank0.png b/examples/spacetaxi/assets/tiles/tilebank0.png deleted file mode 100644 index 8dedcfd..0000000 --- a/examples/spacetaxi/assets/tiles/tilebank0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a60d804d43b02325963da525328818da646f44cb7fcb9cdcf95686a83325c98 -size 1039 diff --git a/examples/spacetaxi/extractFromDump.py b/examples/spacetaxi/extractFromDump.py deleted file mode 100644 index e7b792d..0000000 --- a/examples/spacetaxi/extractFromDump.py +++ /dev/null @@ -1,496 +0,0 @@ -#!/usr/bin/env python3 -# extractFromDump.py -- convert a VICE C64 memory dump into JoeyLib -# Space Taxi assets. -# -# Reads: /tmp/spacetaxi/mem0000-.bin (VICE `save` output; -# 2-byte LE start-addr header + 64 KB RAM). -# Writes: examples/spacetaxi/assets/tiles/tilebank.png -# examples/spacetaxi/assets/levels/level.txt -# examples/spacetaxi/assets/sprites/sprites.png (level1 only) -# examples/spacetaxi/assets/font.png (level1 only) -# -# Per-level: -# - Custom charset at $2800-$2FFF (2 KB, 256 chars * 8 bytes mono) -# becomes a tile bank PNG: 40 cols x 25 rows of 8x8 cells, with -# tile index N at cell (N%40, N/40). The PNG uses the EGA-ish -# 16-color palette we already install on stage. -# - Screen RAM at $0400-$07E7 (1000 bytes = 40x25 char codes) and -# color RAM at $D800-$DBE7 (1000 bytes = 40x25 palette nybbles) -# become the tilemap+colormap in the level .txt source. -# -# Sprite data at $3000-$37FF + sprite pointer table at $07F8-$07FF -# is decoded once (it's the same across levels for the active set): -# - sprite 0 / 7: the taxi cab body & shadow -# - sprite 1..6: passenger frames / animation cels -# 64 bytes per sprite, 24x21 1-bit, MSB-leftmost. Color comes from -# $D027-$D02E (one color per sprite). -# -# Usage: -# python3 extractFromDump.py stuff/spacetaxi/mem0000-level1.bin level01 1 -# python3 extractFromDump.py stuff/spacetaxi/mem0000-level2.bin level02 2 -# args: -# -# Dumps live in stuff/spacetaxi/ (committed-ish, repo-local) rather -# than /tmp so they survive reboots and stay with the project. - -import os -import struct -import sys -from collections import Counter - -from PIL import Image - - -# C64 standard color palette (EGA-ish 16-color JoeyLib mapping). -# Indices 0..15 match the C64 VIC-II color register order so a value -# read from $D8xx or $D02x maps directly to a JoeyLib palette slot. -# Colors below approximate Pepto's standard PAL palette in $RGB form. -C64_PALETTE_RGB = [ - (0x00, 0x00, 0x00), # 0 black - (0xFF, 0xFF, 0xFF), # 1 white - (0x88, 0x39, 0x32), # 2 red - (0x67, 0xB6, 0xBD), # 3 cyan - (0x8B, 0x3F, 0x96), # 4 purple - (0x55, 0xA0, 0x49), # 5 green - (0x40, 0x31, 0x8D), # 6 blue - (0xBF, 0xCE, 0x72), # 7 yellow - (0x8B, 0x54, 0x29), # 8 orange - (0x57, 0x42, 0x00), # 9 brown - (0xB8, 0x69, 0x62), # 10 light red - (0x50, 0x50, 0x50), # 11 dark gray - (0x78, 0x78, 0x78), # 12 mid gray - (0x94, 0xE0, 0x89), # 13 light green - (0x78, 0x69, 0xC4), # 14 light blue - (0x9F, 0x9F, 0x9F), # 15 light gray -] - - -ROOT = os.path.dirname(os.path.abspath(__file__)) - - -def load_dump(path): - with open(path, "rb") as f: - raw = f.read() - start = raw[0] | (raw[1] << 8) - if start != 0: - raise SystemExit(f"{path}: unexpected start ${start:04X}, want $0000") - if len(raw) - 2 != 0x10000: - raise SystemExit(f"{path}: not a 64K dump (got {len(raw) - 2} bytes)") - return raw[2:] - - -def new_canvas(w, h): - img = Image.new("P", (w, h), 0) - flat = bytearray() - for r, g, b in C64_PALETTE_RGB: - flat.extend((r, g, b)) - flat.extend(b"\x00" * (768 - len(flat))) - img.putpalette(bytes(flat)) - return img - - -def paint_charset_glyph(px, mem, src_code, bx, by): - """Plot the 8x8 glyph for C64 screen code `src_code` at pixel - (bx, by) of the indexed-palette PIL image. Foreground = palette - index 1, background untouched (already index 0 from new_canvas). - """ - src = 0x2800 + src_code * 8 - for r in range(8): - byte = mem[src + r] - for c in range(8): - if byte & (0x80 >> c): - px[bx + c, by + r] = 1 - - -def extract_tilebank(mem, out_path, code_to_slot=None): - """Render the custom charset at $2800-$2FFF as a 320x200 PNG. - When `code_to_slot` is None, lays out all 256 chars at their raw - C64 positions (40 cols x 25 rows). When `code_to_slot` is given, - writes each charset glyph at the slot the level loader expects - -- i.e. the bank tile at position N is the glyph for screen-code - `code_for_slot[N]`. That keeps level data and tilebank aligned - after the level extractor's frequency-sorted code remapping. - """ - img = new_canvas(320, 200) - px = img.load() - if code_to_slot is None: - for ch in range(256): - paint_charset_glyph(px, mem, ch, (ch % 40) * 8, (ch // 40) * 8) - else: - # Invert: slot -> code. Multiple codes can collide into slot 0 - # (background); only the first one we see "owns" that slot for - # painting purposes -- the rest just fall through to the same - # blank tile. Highest priority is the explicit space mapping. - slot_to_code = {} - for code, slot in code_to_slot.items(): - slot_to_code.setdefault(slot, code) - for slot, code in slot_to_code.items(): - if slot >= 256: - continue - paint_charset_glyph(px, mem, code, (slot % 40) * 8, (slot // 40) * 8) - img.save(out_path, "PNG", optimize=True) - - -def build_code_to_slot(mem): - """Build the screen-code -> alphabet-slot map shared between the - tilebank emission and the level emission. The slot index is what - the runtime sees in level.tilemap[]; the tilebank must mirror this - ordering so a level-data slot-N cell renders the charset glyph - that was actually used in the original screen RAM at that code. - """ - screen = mem[0x0400:0x0400 + 1000] - codes = sorted(Counter(screen).items(), key=lambda kv: (-kv[1], kv[0])) - code_to_slot = {} - next_slot = 0 - for code, _count in codes: - if code == 0x20: # space -> always index 0 - code_to_slot[code] = 0 - continue - if code in code_to_slot: - continue - if next_slot >= len(ALPHABET): - code_to_slot[code] = 0 - continue - # Reserve slot 0 for background (space). The very first - # non-space code claims slot 1, etc. - if next_slot == 0: - next_slot = 1 - code_to_slot[code] = next_slot - next_slot += 1 - return code_to_slot - - -def screencode_to_char(code): - """Convert a C64 screen code into a printable tilemap character - using the encoding the .txt format already understands: - 0 -> '.' (treated as 0) - 1..9 -> '1'..'9' (digits encode the lower 10 indices) - but we shift to use up most of the index space below. - For arbitrary 0..255 we need a 256-symbol alphabet -- not feasible - in a single character per cell. So we encode as TWO chars per cell - using base-16 hex would still only cover 0..255. Decision: keep - .txt cells one-char and map screen codes via a custom 64-char - alphabet plus an @char directive table emitted into the file so - the loader knows exactly which screen code each char represents. - """ - # This function is unused as a one-char encoder; we encode via the - # generic alphabet below instead. - return code - - -# Generic 1-char alphabet (62 codes); the level loader's index map -# was: '.' or ' '=0, '0'..'9'=0..9, 'A'..'Z'=10..35, 'a'..'z'=36..61. -# That's only 62 codes, not 256. The original C64 levels use ~30 -# distinct screen codes per level so most levels fit. We map the N -# most-frequent screen codes to those alphabet slots. -ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - - -def extract_level(mem, level_name, tilebank_id, out_path, code_to_alpha): - """Render screen RAM + color RAM as an STL1-format .txt source - using the prebuilt screen-code -> alphabet-slot mapping. Caller - must pass the same mapping that was used to reorder the tilebank - PNG; otherwise the level tilemap and tilebank diverge. - """ - screen = mem[0x0400:0x0400 + 1000] # 40x25 - color = mem[0xD800:0xD800 + 1000] # 40x25, low nybble = palette slot - codes = sorted(Counter(screen).items(), key=lambda kv: (-kv[1], kv[0])) - - # Full 25-row C64 screen. The title screen uses all 25 rows - # (PUBLISHED ... SOFTWARE credit wraps across rows 21, 22, 24 - # with frame chars interleaved). Gameplay HUD draws over the - # bottom 3 rows at runtime. - playfield_rows = 25 - - lines = [] - lines.append(f"# Extracted from VICE memory dump. Edit at your own risk;") - lines.append(f"# regenerating will overwrite. Keep the @tile mappings in") - lines.append(f"# sync with the tilebank PNG (extracted from $2800).") - lines.append(f"") - lines.append(f"@name {level_name.upper()}") - lines.append(f"@tilebank {tilebank_id}") - lines.append(f"@music 0") - bg = mem[0xD021] & 0x0F - border = mem[0xD020] & 0x0F - lines.append(f"@bgColor {bg}") - lines.append(f"@borderColor {border}") - - # Best-effort taxi spawn: middle-top of playfield. The original - # script-driven spawn position would need to be lifted out of - # game state at dump time; for now hand-edit if you want it - # to match the cracker's preferred spawn. - lines.append(f"@taxiSpawn 18 2") - lines.append(f"") - - # Pads: scan the playfield for "landable surfaces" -- runs of - # >= 3 identical non-background tile cells with two or more - # empty rows directly above (so the taxi can approach from the - # top). Per-row scanning runs top-to-bottom; for the same - # surface tile across rows we only take the topmost contiguous - # platform edge. - bg_code = codes[0][0] if codes else 0x20 - def is_bg(code): - return code == bg_code or code == 0x20 - - pads = [] - seen_used = [False] * (playfield_rows * 40) - for row in range(2, playfield_rows): - line = screen[row * 40:(row + 1) * 40] - prev = screen[(row - 1) * 40:row * 40] - prev2 = screen[(row - 2) * 40:(row - 1) * 40] - col = 0 - while col < 40 and len(pads) < 8: - if is_bg(line[col]): - col += 1 - continue - j = col - while j < 40 and line[j] == line[col] and not seen_used[row * 40 + j]: - j += 1 - width = j - col - # Approachable from above? At least 2/3 of cells in the - # 2 rows above this stretch must be background. - above_clear = 0 - for k in range(col, j): - if is_bg(prev[k]): - above_clear += 1 - if is_bg(prev2[k]): - above_clear += 1 - if width >= 3 and above_clear >= width: - pads.append((row, col, width)) - for k in range(col, j): - seen_used[row * 40 + k] = True - col = j - - lines.append("# Pads auto-detected from landable surfaces (>= 3 wide,") - lines.append("# approachable from above). Verify and adjust as needed.") - pad_letter = ord("A") - for (row, col, width) in pads: - if pad_letter > ord("H"): - break - lines.append(f"@pad {chr(pad_letter)} {col} {row} {width}") - pad_letter += 1 - if pad_letter == ord("A"): - lines.append("# (No landable surfaces auto-detected; emitting placeholders.)") - lines.append("@pad A 4 10 4") - lines.append("@pad B 32 10 4") - pad_letter = ord("C") - - lines.append("") - # Fare list: simple round-robin between detected pads. - pad_count = pad_letter - ord("A") - if pad_count >= 2: - for k in range(pad_count): - src = chr(ord("A") + k) - dst = chr(ord("A") + ((k + 1) % pad_count)) - lines.append(f"@fare {src} {dst} 45") - else: - lines.append("@fare A B 45") - lines.append("") - - # Emit the @tile mapping for round-trip clarity (and so the - # author can read a level .txt and tell which screen code maps - # to which char). Format: @tile -- the - # mkstlevel parser ignores these today but they're useful - # documentation, and a future loader could honor them. - lines.append("# Screen-code -> alphabet-char mapping (one per unique code).") - inv = {v: k for k, v in code_to_alpha.items()} - for idx in sorted(inv): - ch = '.' if idx == 0 else ALPHABET[idx] - code = inv[idx] - lines.append(f"# @tile '{ch}' = $C64_screencode_{code:02X}") - lines.append("") - - # Tilemap (22 rows of 40 chars each). - lines.append("@tilemap") - for r in range(playfield_rows): - row = screen[r * 40:(r + 1) * 40] - s = "" - for code in row: - idx = code_to_alpha.get(code, 0) - s += '.' if idx == 0 else ALPHABET[idx] - lines.append(s) - - lines.append("") - lines.append("@colormap") - for r in range(playfield_rows): - row = color[r * 40:(r + 1) * 40] - s = "".join( - ('0' if (c & 0x0F) == 0 - else ALPHABET[(c & 0x0F)] - if (c & 0x0F) < len(ALPHABET) else '0') - for c in row - ) - lines.append(s) - - with open(out_path, "w") as fp: - fp.write("\n".join(lines) + "\n") - - -def extract_sprites(mem, out_path): - """Render C64 hardware sprites into the JoeyLib-expected sprite - sheet layout (320x200, 16-color indexed PNG): - y= 0..23 : 1 taxi cel (24x24, x = 0) - sprite 0 ptr (in level1 dump: $DC -> $3700) - The adjacent slot $DD is the landing-gear-down - pose, not a thrust frame -- ignored for now. - y= 24..47 : 4 passenger cels (16x16 each, x = 0, 16, 32, 48) - Sprite 3 ptr ($07FB) is the passenger data; sprites 3-6 share it. - """ - img = new_canvas(320, 200) - px = img.load() - - # Shared multicolor sprite colors from VIC registers. - mcm_color0 = mem[0xD025] & 0x0F # 2-bit value 01 - mcm_color1 = mem[0xD026] & 0x0F # 2-bit value 11 - mcm_mask = mem[0xD01C] # bit per sprite: 1 = multicolor - - def paint_sprite_at(data_addr, sprite_color, dst_x, dst_y, dst_w, dst_h, multicolor=False): - # Mono C64 sprite is 24x21, 1 bit per pixel, 3 bytes per row. - # Multicolor C64 sprite is 12 "double-wide pixels" x 21 rows, - # 2 bits per pixel, still 3 bytes per row. Color mapping: - # 00 -> transparent - # 01 -> mcm_color0 ($D025) - # 10 -> sprite_color (sprite's own $D027+sp) - # 11 -> mcm_color1 ($D026) - if sprite_color == 0: - sprite_color = 1 - sx_max = min(24, dst_w) - sy_max = min(21, dst_h) - for r in range(sy_max): - if multicolor: - for byte_idx in range(3): - byte = mem[data_addr + r * 3 + byte_idx] - # 4 multicolor pixels per byte; each spans 2 - # hardware pixels wide. - for pp in range(4): - bits = (byte >> ((3 - pp) * 2)) & 0x03 - if bits == 0: - continue - if bits == 1: - col = mcm_color0 - elif bits == 2: - col = sprite_color - else: - col = mcm_color1 - sx0 = byte_idx * 8 + pp * 2 - if sx0 + 1 >= sx_max: - break - px[dst_x + sx0, dst_y + r] = col - px[dst_x + sx0 + 1, dst_y + r] = col - else: - for byte_idx in range(3): - byte = mem[data_addr + r * 3 + byte_idx] - for bit in range(8): - sx = byte_idx * 8 + bit - if sx >= sx_max: - break - if byte & (0x80 >> bit): - px[dst_x + sx, dst_y + r] = sprite_color - - def paint_c64_sprite(sp_index, dst_x, dst_y, dst_w, dst_h): - ptr = mem[0x07F8 + sp_index] - if ptr == 0: - return False - sprite_color = mem[0xD027 + sp_index] & 0x0F - is_multicolor = bool(mcm_mask & (1 << sp_index)) - paint_sprite_at(ptr * 64, sprite_color, dst_x, dst_y, dst_w, dst_h, - multicolor=is_multicolor) - return True - - # Single taxi cel from sprite 0's actual pointer. - paint_c64_sprite(0, 0, 0, 24, 24) - - # 4 passenger cels = the shared C64 sprite 3 (sprites 3..6 all - # point to the same data in the level-1 dump). - for cel in range(4): - paint_c64_sprite(3, cel * 16, 24, 16, 16) - - img.save(out_path, "PNG", optimize=True) - - -def extract_font(mem, out_path): - """Render the custom charset (which IS the in-game font in C64 - text-mode games) as the 320x200 font.png. Same layout as - tilebank, but here we want each glyph at cell (ascii%40, - ascii/40) for jlDrawText's ASCII map. - - For Space Taxi specifically, the charset doubles as the level - tile bank AND any in-screen text Sierra-style. We map the C64 - screen codes used for ASCII display (codes 1..26 = A..Z, - 32..63 = punctuation/digits) directly to their ASCII positions - in the JoeyLib font sheet. - """ - img = new_canvas(320, 200) - px = img.load() - - def render_at(ascii_code, charset_idx): - if ascii_code < 32 or ascii_code >= 128: - return - src = 0x2800 + (charset_idx & 0xFF) * 8 - col = ascii_code % 40 - row = ascii_code // 40 - bx = col * 8 - by = row * 8 - for r in range(8): - byte = mem[src + r] - for c in range(8): - if byte & (0x80 >> c): - px[bx + c, by + r] = 1 - - # C64 upper-only screen codes: 1..26 = A..Z, 27=[ 28=£ 29=] 30=↑ 31=← - # 32 = space, 33..63 = !"# ... ?, etc. We map ASCII directly. - for c in range(32, 64): - render_at(c, c) # symbols + digits 1:1 - for c, code in enumerate("ABCDEFGHIJKLMNOPQRSTUVWXYZ"): - render_at(ord(code), 1 + c) # screen code 1..26 -> A..Z - - img.save(out_path, "PNG", optimize=True) - - -def main(): - if len(sys.argv) != 4: - print("usage: extractFromDump.py ", file=sys.stderr) - sys.exit(2) - dump_path = sys.argv[1] - level_name = sys.argv[2] - tilebank_id = int(sys.argv[3]) - - mem = load_dump(dump_path) - print(f"loaded {dump_path}: 64 KB OK") - - tiles_dir = os.path.join(ROOT, "assets", "tiles") - sprites_dir = os.path.join(ROOT, "assets", "sprites") - levels_dir = os.path.join(ROOT, "assets", "levels") - os.makedirs(tiles_dir, exist_ok=True) - os.makedirs(sprites_dir, exist_ok=True) - os.makedirs(levels_dir, exist_ok=True) - - # Build the screen-code -> bank-slot mapping once and feed it to - # both extractors so the tilemap and tilebank stay aligned. - code_to_slot = build_code_to_slot(mem) - - # 8.3 DOS naming -- "tilebank0" is 9 chars and fopen fails under - # DOSBox strict 8.3 mode. Keep the short form across the pipeline. - tilebank_path = os.path.join(tiles_dir, f"tbank{tilebank_id}.png") - extract_tilebank(mem, tilebank_path, code_to_slot) - print(f" wrote {tilebank_path}") - - level_path = os.path.join(levels_dir, f"{level_name}.txt") - extract_level(mem, level_name, tilebank_id, level_path, code_to_slot) - print(f" wrote {level_path}") - - # Sprites + font come from level 1 only (sprite pointer table is - # script-driven so it differs by scene; we use the first dump's - # set as the canonical sheet, plus its charset as the font). - if tilebank_id == 1: - sprite_path = os.path.join(sprites_dir, "sprites.png") - extract_sprites(mem, sprite_path) - print(f" wrote {sprite_path}") - - font_path = os.path.join(ROOT, "assets", "font.png") - extract_font(mem, font_path) - print(f" wrote {font_path}") - - -if __name__ == "__main__": - main() diff --git a/examples/spacetaxi/spacetaxi.c b/examples/spacetaxi/spacetaxi.c index b8b5571..10232d6 100644 --- a/examples/spacetaxi/spacetaxi.c +++ b/examples/spacetaxi/spacetaxi.c @@ -983,7 +983,6 @@ int main(void) { #else config.codegenBytes = 160UL * 1024; #endif - config.audioBytes = 32UL * 1024; if (!jlInit(&config)) { return 1; } diff --git a/examples/sprite/sprite.c b/examples/sprite/sprite.c index 2c816fc..ffcfc30 100644 --- a/examples/sprite/sprite.c +++ b/examples/sprite/sprite.c @@ -109,7 +109,6 @@ int main(void) { * the chunky two-shift case asks for. 32 KB fits a 16x16 ball * with all variants. */ config.codegenBytes = 32UL * 1024; - config.audioBytes = 64UL * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); diff --git a/examples/tilebank/tilebank.c b/examples/tilebank/tilebank.c new file mode 100644 index 0000000..43ec7d1 --- /dev/null +++ b/examples/tilebank/tilebank.c @@ -0,0 +1,318 @@ +// Tile-bank check on a real port: loads this port's bake of the host gate's +// fixture both ways, checks every pixel, and makes sure every other target's +// bake is refused. It is the on-machine half of scripts/check-tilebank.sh: +// that host gate proves the shared loader and the generic paste; this proves +// that assetbake's encoder for THIS target writes exactly the layout this +// port's native tile paste reads, and that this build refuses the others. +// +// DATA/tilebank/ is staged by tests/host/tileBankStage.py (the make rule in +// make/.mk), from tests/host/tileBank.png: +// own.tbk this port's bake: the bank that must load +// refuse.tbk n = 1, 2, ...: every other target's bake +// expect.bin the PNG decoded by PIL, independent of assetbake's +// encoders: tileCount LE16, palette[16] LE16 $0RGB, then +// tileCount * 64 colour indices (tiles in bank order, each +// row-major) +// +// The checks, all on the stage: +// refused each refuse.tbk loads nothing through jlTileBankLoad and +// pastes nothing through jlTileBankLoadToSurface +// streamed jlTileBankLoadToSurface pastes every tile of own.tbk, the +// palette it returns is the PNG's, and every pixel of the stage +// is right: blocks below the tile count show the PNG's tiles in +// bank order, the rest keep FILL_COLOR +// pasted the same pixels again through jlTileBankLoad + jlTilePaste +// Both loads draw onto a stage cleared to the host harness's FILL_COLOR with +// the default palette and SCBs, exactly the surface whose hash the host +// harness prints, so both hashes must equal that one cross-port value. +// +// Result: one jlLog line, flushed before anything is shown, +// TILEBANK: PASS tiles=55 streamed=55 pasted=55 palette=ok streamHash= pasteHash= refused=4/4 +// which scripts/verify--tilebank.sh reads (and holds the hashes against +// the host's), then the tiles in the bank's own colours over a lamp centred on +// the stage: LAMP_PASS_COLOR or LAMP_FAIL_COLOR in palette LAMP_PALETTE, with +// the rest of its rows left at FILL_COLOR. The MAME gates find the verdict +// there before they read the log. Any key exits. +// +// Portable C only: nothing here knows which port it runs on. + +#include +#include +#include + +#include +#include + +// ----- Constants ----- + +// The staged files (tests/host/tileBankStage.py names them). +#define OWN_BANK "tilebank/own.tbk" +#define EXPECT_NAME "tilebank/expect.bin" +#define REFUSE_BANK_FORMAT "tilebank/refuse%u.tbk" +#define BANK_NAME_BYTES 32 + +// expect.bin layout (see the header comment). +#define LE16_BYTES 2 +#define BYTE_BITS 8 +#define PALETTE_COLORS SURFACE_COLORS_PER_PALETTE +#define EXPECT_OFF_PALETTE LE16_BYTES +#define EXPECT_HEADER_BYTES (EXPECT_OFF_PALETTE + PALETTE_COLORS * LE16_BYTES) +#define TILE_PIXELS (TILE_PIXELS_PER_SIDE * TILE_PIXELS_PER_SIDE) + +// The stage is cleared to this before every load. It MUST equal FILL_COLOR in +// tests/host/tileBankHost.c: the host's hash is of a surface cleared to it. +#define FILL_COLOR 9u + +// No sprites here, so the port's default codegen cache is plenty. +#define CODEGEN_DEFAULT 0u + +// The verdict lamp, centred on the stage. Its rows get their own palette so +// the lamp colours cannot collide with the bank's sixteen. +#define LAMP_W 64 +#define LAMP_H 64 +#define LAMP_X ((SURFACE_WIDTH - LAMP_W) / 2) +#define LAMP_Y ((SURFACE_HEIGHT - LAMP_H) / 2) +#define LAMP_PALETTE 1u +#define LAMP_PASS_COLOR 1u +#define LAMP_FAIL_COLOR 2u +#define LAMP_DARK_RGB 0x0000u +#define LAMP_PASS_RGB 0x00F0u +#define LAMP_FAIL_RGB 0x0F00u + +// ----- Prototypes ----- + +static bool loadExpected(void); +static void pasteTiles(uint16_t count); +static bool readAll(FILE *fp, uint8_t *dst, uint32_t bytes); +static uint16_t readLe16(const uint8_t *bytes); +static bool refuseBankFound(char *name, uint16_t n); +static uint16_t refuseCheck(uint16_t *banks); +static bool runChecks(void); +static void showVerdict(bool pass); +static bool stageMatches(uint16_t tilesDrawn); + +// ----- State ----- + +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 jlTileT *gTiles = NULL; +static bool *gValid = NULL; + + +// Read expect.bin, and size the tile buffers to its tile count. +static bool loadExpected(void) { + FILE *fp; + uint8_t head[EXPECT_HEADER_BYTES]; + uint32_t pixelBytes; + uint16_t i; + bool ok; + + fp = jlDataOpen(EXPECT_NAME, "rb"); + if (fp == NULL) { + return false; + } + ok = readAll(fp, head, sizeof(head)); + if (ok) { + gExpectCount = readLe16(head); + ok = gExpectCount > 0u; + } + if (ok) { + for (i = 0u; i < PALETTE_COLORS; i++) { + gExpectPalette[i] = readLe16(&head[EXPECT_OFF_PALETTE + i * LE16_BYTES]); + } + pixelBytes = (uint32_t)gExpectCount * TILE_PIXELS; + gExpectPixels = (uint8_t *)jlAlloc(pixelBytes); + gTiles = (jlTileT *)jlAlloc((uint32_t)gExpectCount * sizeof(jlTileT)); + gValid = (bool *)jlAlloc((uint32_t)gExpectCount * sizeof(bool)); + ok = gExpectPixels != NULL && gTiles != NULL && gValid != NULL && readAll(fp, gExpectPixels, pixelBytes); + } + fclose(fp); + return ok; +} + + +// Clear the stage, then paste gTiles[0..count) in the row-major block order +// jlTileBankLoadToSurface uses. +static void pasteTiles(uint16_t count) { + uint16_t i; + + jlSurfaceClear(gScreen, FILL_COLOR); + for (i = 0u; i < count; i++) { + jlTilePaste(gScreen, (uint8_t)(i % TILE_BLOCKS_PER_ROW), (uint8_t)(i / TILE_BLOCKS_PER_ROW), &gTiles[i]); + } +} + + +// fread until done: one fread need not fill a large request (the IIgs libc +// stops short), so a short read is only an error when nothing more comes. +static bool readAll(FILE *fp, uint8_t *dst, uint32_t bytes) { + uint32_t done; + size_t got; + + done = 0u; + while (done < bytes) { + got = fread(dst + done, 1, (size_t)(bytes - done), fp); + if (got == 0u) { + return false; + } + done += (uint32_t)got; + } + return true; +} + + +static uint16_t readLe16(const uint8_t *bytes) { + return (uint16_t)(bytes[0] | (bytes[1] << BYTE_BITS)); +} + + +// Put refuse bank n's name in name (BANK_NAME_BYTES long); true when that +// bank was staged. +static bool refuseBankFound(char *name, uint16_t n) { + FILE *fp; + + snprintf(name, BANK_NAME_BYTES, REFUSE_BANK_FORMAT, (unsigned)n); + fp = jlDataOpen(name, "rb"); + if (fp == NULL) { + return false; + } + fclose(fp); + return true; +} + + +// Every staged refuse bank, both ways: jlTileBankLoad must load nothing and +// mark nothing valid, jlTileBankLoadToSurface must paste nothing. Returns how +// many were refused both ways; *banks gets how many were staged. +static uint16_t refuseCheck(uint16_t *banks) { + char name[BANK_NAME_BYTES]; + uint32_t fillHash; + uint16_t refused; + uint16_t loaded; + uint16_t streamed; + + jlSurfaceClear(gScreen, FILL_COLOR); + fillHash = jlSurfaceHash(gScreen); + refused = 0u; + *banks = 0u; + while (refuseBankFound(name, (uint16_t)(*banks + 1u))) { + (*banks)++; + jlSurfaceClear(gScreen, FILL_COLOR); + loaded = jlTileBankLoad(name, gTiles, gExpectCount, gValid, NULL); + streamed = jlTileBankLoadToSurface(name, gScreen, NULL); + if (loaded == 0u && !gValid[0] && streamed == 0u && jlSurfaceHash(gScreen) == fillHash) { + refused++; + } + } + return refused; +} + + +// The checks in the header comment, reported as one log line. The paste runs +// last so its tiles are what the verdict screen shows. +static bool runChecks(void) { + uint16_t banks; + uint16_t refused; + uint16_t streamed; + uint16_t pasted; + uint32_t streamHash; + uint32_t pasteHash; + bool paletteOk; + bool streamOk; + bool pasteOk; + bool pass; + + refused = refuseCheck(&banks); + + jlSurfaceClear(gScreen, FILL_COLOR); + streamed = jlTileBankLoadToSurface(OWN_BANK, gScreen, gBankPalette); + streamOk = streamed == gExpectCount && stageMatches(streamed); + streamHash = jlSurfaceHash(gScreen); + 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); + + 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); + return pass; +} + + +// Show the bank's tiles in its own colours, and the lamp. +static void showVerdict(bool pass) { + uint16_t colors[PALETTE_COLORS]; + uint16_t i; + + for (i = 0u; i < PALETTE_COLORS; i++) { + colors[i] = LAMP_DARK_RGB; + } + 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); + jlStagePresent(); +} + + +// Every pixel of the stage: blocks below tilesDrawn must show the PNG's tile +// of that number, every other block FILL_COLOR. tilesDrawn never exceeds +// gExpectCount (the callers check the count first). +static bool stageMatches(uint16_t tilesDrawn) { + int16_t x; + int16_t y; + uint16_t block; + uint8_t want; + + for (y = 0; y < SURFACE_HEIGHT; y++) { + for (x = 0; x < SURFACE_WIDTH; x++) { + block = (uint16_t)((y / TILE_PIXELS_PER_SIDE) * TILE_BLOCKS_PER_ROW + x / TILE_PIXELS_PER_SIDE); + want = FILL_COLOR; + if (block < tilesDrawn) { + want = gExpectPixels[(uint32_t)block * TILE_PIXELS + (y % TILE_PIXELS_PER_SIDE) * TILE_PIXELS_PER_SIDE + x % TILE_PIXELS_PER_SIDE]; + } + if (jlSamplePixel(gScreen, x, y) != want) { + return false; + } + } + } + return true; +} + + +int main(void) { + jlConfigT config; + bool pass; + + config.codegenBytes = CODEGEN_DEFAULT; + if (!jlInit(&config)) { + fprintf(stderr, "jlInit failed: %s\n", jlLastError()); + return EXIT_FAILURE; + } + gScreen = jlStageGet(); + + pass = loadExpected(); + if (pass) { + pass = runChecks(); + } else { + jlLog("TILEBANK: FAIL DATA/" EXPECT_NAME " is missing or short, or memory ran out"); + } + // Flushed BEFORE the verdict shows: a gate that sees the lamp can read + // the log at once (on the IIgs the flush is what writes the file). + jlLogFlush(); + showVerdict(pass); + + jlWaitForAnyKey(); + jlFree(gValid); + jlFree(gTiles); + jlFree(gExpectPixels); + jlShutdown(); + return pass ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/examples/uber/uber.c b/examples/uber/uber.c index 6eb2c29..e75477e 100644 --- a/examples/uber/uber.c +++ b/examples/uber/uber.c @@ -260,7 +260,7 @@ static void timeOp(const char *name, OpFn op) { iters = runForFrames(op, UBER_FRAMES, &actualFrames, &millisDelta); if (iters == 0UL) { - jlLogF("UBER: %s: 0 iters (op too slow?)\n", name); + jlLogF("UBER: %s: 0 iters (op too slow?)", name); return; } @@ -275,9 +275,9 @@ static void timeOp(const char *name, OpFn op) { opsPerSec = (iters * 1000UL) / (unsigned long)millisDelta; opsPerSecFr = (iters * (unsigned long)jlFrameHz()) / (unsigned long)actualFrames; hash = jlSurfaceHash(gStage); - jlLogF("UBER: %s: %lu iters / %u frames = %lu ops/sec | hash=%08lX\n", + jlLogF("UBER: %s: %lu iters / %u frames = %lu ops/sec | hash=%08lX", name, iters, actualFrames, opsPerSec, (unsigned long)hash); - jlLogF("UBER-CLK: %s: frame=%lu millis=%lu ops/sec (%lu ms)\n", + jlLogF("UBER-CLK: %s: frame=%lu millis=%lu ops/sec (%lu ms)", name, opsPerSecFr, opsPerSec, (unsigned long)millisDelta); } @@ -697,7 +697,7 @@ static void PROBE_ATTR op_probeSpriteBandSum(void) { jlSpriteDraw(gStage, gSprite, 150, 189); jlSpriteDraw(gStage, gSprite, 71, 120); sum = probeBandFold(); - jlLogF("UBER-PROBE: spriteBandSum=%lu\n", (unsigned long)sum); + jlLogF("UBER-PROBE: spriteBandSum=%lu", (unsigned long)sum); } @@ -722,7 +722,7 @@ static void PROBE_ATTR op_probeFillBandSum(void) { jlDrawRect(gStage, 60, 60, 100, 100, 11); jlFillCircle(gStage, 200, 120, 30, 12); sum = probeBandFold(); - jlLogF("UBER-PROBE: fillBandSum=%lu\n", (unsigned long)sum); + jlLogF("UBER-PROBE: fillBandSum=%lu", (unsigned long)sum); } @@ -872,7 +872,7 @@ static void PROBE_ATTR op_probeShrParity(void) { stagePx += 160u; shrPx += 160u; } - jlLogF("UBER-PROBE: shrParity %s badRows=%u firstBad=%u\n", (badRows == 0u) ? "OK" : "FAIL", (unsigned int)badRows, (unsigned int)firstBad); + jlLogF("UBER-PROBE: shrParity %s badRows=%u firstBad=%u", (badRows == 0u) ? "OK" : "FAIL", (unsigned int)badRows, (unsigned int)firstBad); // Tail phase 1: seed all 16 palettes + the SCB bands, then present. // jlPaletteSet / jlScbSetRange set the dirty flags, so THIS present @@ -881,7 +881,7 @@ static void PROBE_ATTR op_probeShrParity(void) { probeShrTailSeed(); jlStagePresent(); probeShrTailCheck(&badScbUp, &badPalUp, &firstPalUp); - jlLogF("UBER-PROBE: shrTailUpload %s badScb=%u badPal=%u firstPal=%u\n", ((badScbUp | badPalUp) == 0u) ? "OK" : "FAIL", (unsigned int)badScbUp, (unsigned int)badPalUp, (unsigned int)firstPalUp); + jlLogF("UBER-PROBE: shrTailUpload %s badScb=%u badPal=%u firstPal=%u", ((badScbUp | badPalUp) == 0u) ? "OK" : "FAIL", (unsigned int)badScbUp, (unsigned int)badPalUp, (unsigned int)firstPalUp); // Tail phase 2: pixel-only presents. The dirty flags were cleared by // the upload above and nothing below touches a palette or an SCB, so @@ -898,7 +898,7 @@ static void PROBE_ATTR op_probeShrParity(void) { jlFillRect(gStage, 0, 180, 320, 20, 6); jlStagePresent(); probeShrTailCheck(&badScbSlam, &badPalSlam, &firstPalSlam); - jlLogF("UBER-PROBE: shrTailSlam %s badScb=%u badPal=%u firstPal=%u\n", ((badScbSlam | badPalSlam) == 0u) ? "OK" : "FAIL", (unsigned int)badScbSlam, (unsigned int)badPalSlam, (unsigned int)firstPalSlam); + jlLogF("UBER-PROBE: shrTailSlam %s badScb=%u badPal=%u firstPal=%u", ((badScbSlam | badPalSlam) == 0u) ? "OK" : "FAIL", (unsigned int)badScbSlam, (unsigned int)badPalSlam, (unsigned int)firstPalSlam); } #endif @@ -930,7 +930,7 @@ static void PROBE_ATTR op_probeDisplayCheck(void) { afterSettle = jlpProbeDisplayMismatch(gStage); jlStagePresent(); afterIdle = jlpProbeDisplayMismatch(gStage); - jlLogF("UBER-PROBE: displayMismatch afterB=%lu afterSettle=%lu afterIdle=%lu\n", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle); + jlLogF("UBER-PROBE: displayMismatch afterB=%lu afterSettle=%lu afterIdle=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle); } #endif @@ -992,7 +992,7 @@ static void PROBE_ATTR op_probePresentPageSum(void) { fails = (uint8_t)(fails + probePageSumCheck()); fails = (uint8_t)(fails + probePageSumCheck()); fails = (uint8_t)(fails + probePageSumCheck()); - jlLogF("UBER-PROBE: presentPageSum %s (fails=%u)\n", fails == 0 ? "PASS" : "FAIL", (unsigned)fails); + jlLogF("UBER-PROBE: presentPageSum %s (fails=%u)", fails == 0 ? "PASS" : "FAIL", (unsigned)fails); } #endif @@ -1027,7 +1027,7 @@ static void PROBE_ATTR op_probeVgaCheck(void) { afterSettle = dosProbeVgaMismatch(gStage); jlStagePresent(); afterIdle = dosProbeVgaMismatch(gStage); - jlLogF("UBER-PROBE: vgaMismatch afterB=%lu afterSettle=%lu afterIdle=%lu\n", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle); + jlLogF("UBER-PROBE: vgaMismatch afterB=%lu afterSettle=%lu afterIdle=%lu", (unsigned long)afterB, (unsigned long)afterSettle, (unsigned long)afterIdle); } #endif @@ -1077,7 +1077,7 @@ static void PROBE_ATTR probeOp(const char *name, void (*fn)(void)) { bytes0 = gPresentCopiedBytes; rows0 = gPresentScannedRows; timeOp(name, fn); - jlLogF("UBER-PROBE: %s: copied=%lu scanned=%lu\n", name, (unsigned long)(gPresentCopiedBytes - bytes0), (unsigned long)(gPresentScannedRows - rows0)); + jlLogF("UBER-PROBE: %s: copied=%lu scanned=%lu", name, (unsigned long)(gPresentCopiedBytes - bytes0), (unsigned long)(gPresentScannedRows - rows0)); // Steady-delta second pass (W2): one more call OUTSIDE the timed // window. Its single-op delta is the exact steady-state gate value // -- the timed average carries a first-iteration union carry-in on @@ -1086,7 +1086,7 @@ static void PROBE_ATTR probeOp(const char *name, void (*fn)(void)) { bytes0 = gPresentCopiedBytes; rows0 = gPresentScannedRows; fn(); - jlLogF("UBER-PROBE: %s steady: copied=%lu scanned=%lu\n", name, (unsigned long)(gPresentCopiedBytes - bytes0), (unsigned long)(gPresentScannedRows - rows0)); + jlLogF("UBER-PROBE: %s steady: copied=%lu scanned=%lu", name, (unsigned long)(gPresentCopiedBytes - bytes0), (unsigned long)(gPresentScannedRows - rows0)); } #endif #endif @@ -1160,7 +1160,7 @@ static void PROBE_ATTR op_probeCompactEpoch(void) { victim = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y); survivor = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y); if (victim == NULL || survivor == NULL || !jlSpriteCompile(victim) || !jlSpriteCompile(survivor)) { - jlLogF("UBER-PROBE: compactEpoch SKIPPED (create/compile failed)\n"); + jlLogF("UBER-PROBE: compactEpoch SKIPPED (create/compile failed)"); if (victim != NULL) { jlSpriteDestroy(victim); } @@ -1177,7 +1177,7 @@ static void PROBE_ATTR op_probeCompactEpoch(void) { jlSpriteCompact(); jlSpriteDraw(gStage, survivor, 160, 80); jlSpriteDestroy(survivor); - jlLogF("UBER-PROBE: compactEpoch hash=%08lX bandSum=%lu\n", (unsigned long)jlSurfaceHash(gStage), (unsigned long)probeBandFold()); + jlLogF("UBER-PROBE: compactEpoch hash=%08lX bandSum=%lu", (unsigned long)jlSurfaceHash(gStage), (unsigned long)probeBandFold()); } #endif @@ -1370,7 +1370,7 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) { } } - jlLogF("UBER: showcase cells: 0=pixels 1=lineH 2=lineV 3=diag 4=rect 5=fillRect 6=circle 7=fillCircle 8=tiles 9=sprite 10=flood 11=scene\n"); + jlLogF("UBER: showcase cells: 0=pixels 1=lineH 2=lineV 3=diag 4=rect 5=fillRect 6=circle 7=fillCircle 8=tiles 9=sprite 10=flood 11=scene"); jlStagePresent(); // Hold the showcase on screen, then auto-advance to the benchmark so @@ -1396,12 +1396,12 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) { // PRNG golden sequence, arena bookkeeping). static void __attribute__((noinline)) chkHash(const char *name) { - jlLogF("UBER-CHK: %s: hash=%08lX\n", name, (unsigned long)jlSurfaceHash(gStage)); + jlLogF("UBER-CHK: %s: hash=%08lX", name, (unsigned long)jlSurfaceHash(gStage)); } static void __attribute__((noinline)) chkPassFail(const char *name, bool pass) { - jlLogF("UBER-CHK: %s: %s\n", name, pass ? "PASS" : "FAIL"); + jlLogF("UBER-CHK: %s: %s", name, pass ? "PASS" : "FAIL"); } @@ -1647,7 +1647,7 @@ static void __attribute__((noinline)) checkTileMonoFloodExtra(void) { static void __attribute__((noinline)) runCorrectnessChecks(void) { uberMbPhase(UBER_PHASE_CHECKS); - jlLogF("UBER-CHK: ----- begin -----\n"); + jlLogF("UBER-CHK: ----- begin -----"); jlSurfaceClear(gStage, 0); checkEdgeDraws(); checkClippedSprites(); @@ -1660,7 +1660,7 @@ static void __attribute__((noinline)) runCorrectnessChecks(void) { // jlShutdown -> jlInit -> stale-sprite-destroy (#34) needs a teardown // of the whole library mid-run; it gets a dedicated micro-example in // Phase 1 rather than risking the benchmark's display state here. - jlLogF("UBER-CHK: ----- end -----\n"); + jlLogF("UBER-CHK: ----- end -----"); } @@ -1668,7 +1668,7 @@ static void __attribute__((noinline)) runCorrectnessChecks(void) { static void __attribute__((noinline)) runAllTests(void) { uberMbPhase(UBER_PHASE_TIMED_RUN); - jlLogF("UBER: ----- begin -----\n"); + jlLogF("UBER: ----- begin -----"); // Surface / palette / SCB. timeOp("jlSurfaceClear", op_surfaceClear); @@ -1779,7 +1779,7 @@ static void __attribute__((noinline)) runAllTests(void) { jlSurfaceDestroy(gCleanSurface); gCleanSurface = NULL; } else { - jlLogF("UBER: gameFrameClean composite: SKIPPED (surface alloc failed)\n"); + jlLogF("UBER: gameFrameClean composite: SKIPPED (surface alloc failed)"); } #ifdef UBER_PROBE @@ -1803,19 +1803,19 @@ static void __attribute__((noinline)) runAllTests(void) { probeBytes0 = gPresentCopiedBytes; probeRows0 = gPresentScannedRows; timeOp("probe present96", op_probePresent96); - jlLogF("UBER-PROBE: probe present96: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); + jlLogF("UBER-PROBE: probe present96: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); probeBytes0 = gPresentCopiedBytes; probeRows0 = gPresentScannedRows; op_probePresent96(); - jlLogF("UBER-PROBE: probe present96 steady: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); + jlLogF("UBER-PROBE: probe present96 steady: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); probeBytes0 = gPresentCopiedBytes; probeRows0 = gPresentScannedRows; timeOp("probe present3win", op_probePresent3win); - jlLogF("UBER-PROBE: probe present3win: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); + jlLogF("UBER-PROBE: probe present3win: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); probeBytes0 = gPresentCopiedBytes; probeRows0 = gPresentScannedRows; op_probePresent3win(); - jlLogF("UBER-PROBE: probe present3win steady: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); + jlLogF("UBER-PROBE: probe present3win steady: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); timeOp("probe frameNoPresent", op_probeFrameNoPresent); // P7-1 IIgs trusted rows: same geometry (sprite at 40,30 into a // valid backup) as the golden jlSpriteSaveUnder / RestoreUnder / @@ -1836,11 +1836,11 @@ static void __attribute__((noinline)) runAllTests(void) { probeBytes0 = gPresentCopiedBytes; probeRows0 = gPresentScannedRows; timeOp("probe presentIdle", op_probePresentIdle); - jlLogF("UBER-PROBE: probe presentIdle: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); + jlLogF("UBER-PROBE: probe presentIdle: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); probeBytes0 = gPresentCopiedBytes; probeRows0 = gPresentScannedRows; op_probePresentIdle(); - jlLogF("UBER-PROBE: probe presentIdle steady: copied=%lu scanned=%lu\n", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); + jlLogF("UBER-PROBE: probe presentIdle steady: copied=%lu scanned=%lu", (unsigned long)(gPresentCopiedBytes - probeBytes0), (unsigned long)(gPresentScannedRows - probeRows0)); // Hunt-3 rank 1: gameFrameClean component split on IIgs (the // 16-21 ms unattributed residual). Same recipe as the golden // row (gCleanSurface was destroyed after it): re-create with @@ -1930,7 +1930,7 @@ static void __attribute__((noinline)) runAllTests(void) { #endif #endif - jlLogF("UBER: ----- end -----\n"); + jlLogF("UBER: ----- end -----"); } @@ -1966,7 +1966,7 @@ static void __attribute__((noinline)) reportElapsed(uint16_t startFrame) { endFrame = jlFrameCount(); elapsedFrames = (uint16_t)(endFrame - startFrame); elapsedMs = ((unsigned long)elapsedFrames * 1000UL) / (unsigned long)jlFrameHz(); - jlLogF("UBER: total wall time: %lu ms (%u frames @ %u Hz)\n", + jlLogF("UBER: total wall time: %lu ms (%u frames @ %u Hz)", elapsedMs, elapsedFrames, (unsigned)jlFrameHz()); } @@ -1991,7 +1991,7 @@ static bool __attribute__((noinline)) setupSprite(void) { while (jlFrameCount() == before) { /* wait for next VBL edge */ } - jlLogF("UBER: jlSpriteCompile: 1 call in <= 1 frame\n"); + jlLogF("UBER: jlSpriteCompile: 1 call in <= 1 frame"); gBackup.bytes = gBackupBytes; return true; } @@ -2008,7 +2008,6 @@ static bool __attribute__((noinline)) initJoeyLib(void) { * thunk+table DRAWs ~12 KB; plus ~1-2 KB of save/restore classes). * UL on the multiply because a 16-bit int overflows on 32 * 1024. */ config.codegenBytes = 32UL * 1024; - config.audioBytes = 64UL * 1024; return jlInit(&config); } @@ -2059,9 +2058,9 @@ int main(void) { // paths on IIgs). uberMbPhase(UBER_PHASE_AUDIO_INIT); if (jlAudioInit()) { - jlLogF("UBER: audioInit OK\n"); + jlLogF("UBER: audioInit OK"); } else { - jlLogF("UBER: audioInit failed (skipping audio)\n"); + jlLogF("UBER: audioInit failed (skipping audio)"); } // Visual showcase: render one of each primitive into its own grid @@ -2081,7 +2080,7 @@ int main(void) { jlSurfaceClear(gStage, 2); jlStagePresent(); - jlLogF("UBER: press any key to exit\n"); + jlLogF("UBER: press any key to exit"); // Flush the log to disk BEFORE the blocking key wait, so an automated // (headless) run that kills the process at this prompt still captures // the results -- joeyLog otherwise only flushes at the atexit fclose. diff --git a/include/joey/core.h b/include/joey/core.h index bc6b431..112d1dd 100644 --- a/include/joey/core.h +++ b/include/joey/core.h @@ -16,7 +16,6 @@ typedef struct { // a 64 KB bank boundary), so values above 65536 are clamped to // 65536; even a clamped request can fail if no free bank exists. uint32_t codegenBytes; - uint32_t audioBytes; // reserved; not yet consulted by any audio engine } jlConfigT; // Initialize the library. Returns true on success. diff --git a/include/joey/serial.h b/include/joey/serial.h index 4e7367e..227cbad 100644 --- a/include/joey/serial.h +++ b/include/joey/serial.h @@ -12,9 +12,9 @@ // DOS -- 8250/16550 UART on a COM port; interrupt-driven RX ring. // Amiga -- serial.device (async exec IO); the OS buffers RX. // Atari ST -- BIOS Aux (Modem port) via XBIOS Rsconf; TOS buffers RX. -// X68000 -- SCC channel A: IOCS for transmit and line setup (75-9600 -// bps, no RTS/CTS; 75-38400 bps and RTS/CTS with Human68k's -// RSDRV.SYS loaded); its own RX interrupt fills the ring. +// X68000 -- SCC channel A: Human68k's RSDRV.SYS for transmit and line +// setup (75-38400 bps, RTS/CTS), REQUIRED -- the open fails +// without it; its own RX interrupt fills the ring. // // Model: non-blocking + buffered. Received bytes accumulate in a background // ring (a hardware IRQ on DOS, the X68000 and the IIgs, the OS on Amiga/ST); @@ -87,7 +87,8 @@ typedef struct { // Open + configure the port. Only one port is open at a time; a second // jlSerialOpen without a jlSerialClose fails. Returns false (non-fatal) if the // port cannot be opened or the platform has no serial support -- every other -// call then no-ops. +// call then no-ops. On the X68000 it also fails when RSDRV.SYS is not loaded, +// and jlLastError() says so. bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config); // Close the open port and release its resources. Safe to call when not open. diff --git a/include/joey/sprite.h b/include/joey/sprite.h index 6dbc134..fa13f92 100644 --- a/include/joey/sprite.h +++ b/include/joey/sprite.h @@ -13,12 +13,11 @@ // // Performance contract: jlSpriteDraw should be at least as fast as the // IIgs reference (the DESIGN.md spec calls for runtime-compiled draw -// code per CPU). Every port has a runtime emitter (DOS x86, Amiga/ST -// 68k planar, IIgs 65816); jlSpriteCompile compiles the shift/op +// code per CPU). Every port has a runtime emitter (DOS x86, Amiga/ST/ +// X68000 68k planar, IIgs 65816); jlSpriteCompile compiles the shift/op // variants into the codegen arena and the draw/save/restore calls // dispatch to the compiled routines, falling back to the interpreter -// for clipped draws or uncompiled variants. On the IIgs the compiled -// path is temporarily toolchain-gated off (claudeDocs/PERF-AUDIT.md #77). +// for clipped draws or uncompiled variants. // jlSpritePrewarm compiles ahead of the first draw so the emit cost // does not land mid-frame. diff --git a/include/joey/tile.h b/include/joey/tile.h index 6a88931..f656f7f 100644 --- a/include/joey/tile.h +++ b/include/joey/tile.h @@ -72,9 +72,10 @@ void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex); // tools/assetbake/assetbake.py. The file's per-target byte is checked // against the build platform and the loader refuses mismatches. // -// outValid (optional, NULL OK) is set to true for each loaded slot -// so the caller can distinguish loaded tiles from any leftover slots -// in a fixed-size jlTileT[] array. +// outValid (optional, NULL OK) gets one flag per slot in [0, maxTiles): +// true for each loaded tile, false for every other slot (all of them +// when the file is refused), so the caller can distinguish loaded +// tiles from any leftover slots in a fixed-size jlTileT[] array. // // outPalette (optional, NULL OK) receives the 16-entry $0RGB LE16 // palette if the file embeds one. diff --git a/installer/components/host.sh b/installer/components/host.sh index e56795d..837a5bd 100644 --- a/installer/components/host.sh +++ b/installer/components/host.sh @@ -1,13 +1,13 @@ # Host tools every JoeyLib setup needs: a native compiler for the in-repo # tools, GNU make, git with LFS (the sample assets live in LFS), the -# archive tools the other components unpack with, and Python 3 with -# Pillow for the asset converters. +# archive tools the other components unpack with, Python 3 with Pillow +# for the asset converters, and socat for joey-run's serial bridging. -compDescription="host tools: make, host compiler, git-lfs, python3 + Pillow, archivers" +compDescription="host tools: make, host compiler, git-lfs, python3 + Pillow, archivers, socat" # Not in any port group: every other component requires it. compGroups="host" compRequires="" -compHostDeps="build-tools make git git-lfs curl tar xz bzip2 unzip lhasa python3 python3-pil procps diffutils" +compHostDeps="build-tools make git git-lfs curl tar xz bzip2 unzip lhasa python3 python3-pil procps diffutils socat" compPin="1" diff --git a/installer/distroPackages.txt b/installer/distroPackages.txt index 7e8d224..fd71d34 100644 --- a/installer/distroPackages.txt +++ b/installer/distroPackages.txt @@ -22,6 +22,8 @@ python3 python3 python3 python3-pil python3-pil python3-pillow php-cli php-cli php-cli patch patch patch +# joey-run --serial bridges some emulators' serial ports to TCP with socat. +socat socat socat # ---- runtime libraries the prebuilt toolchains link against libfl libfl2 libfl2 diff --git a/joey-run b/joey-run index 99bd343..2317219 100755 --- a/joey-run +++ b/joey-run @@ -27,11 +27,32 @@ # --machine rom01 IIgs: the ROM 01 machine instead of ROM 03 # --ram SIZE IIgs: memory size for MAME (default 4M; AGI wants 8M) # --debug IIgs and X68000: start MAME's debugger +# --serial HOST:PORT connect the machine's serial port to the TCP server +# at HOST:PORT: a BBS, a program of your own, another +# run's --serial-listen, or `nc -l PORT` +# --serial-listen PORT +# the serial port waits for a TCP connection on PORT +# (on every network interface) instead +# --serial-baud N IIgs and X68000: the rate MAME's end of the wire runs +# at, which must match the program's (default 9600, +# always 8N1); the other emulators pass bytes at any rate # -h, --help this text # # Arguments after -- are passed to the program (DOS only: the other # machines' autostart cannot pass arguments). # +# The serial port is the machine's usual one: the IIgs modem port (the +# printer port and a Super Serial Card are not connected), the X68000's +# RS-232C port, the ST's Modem port, the Amiga's serial port (serial.device +# unit 0) and DOS's COM1. The link is raw TCP, without telnet negotiation. +# socat relays it where the emulator cannot make it itself: the ST, the +# Amiga with --serial, and the IIgs and X68000 with --serial-listen. When +# --serial's server is not there, joey-run stops, except on the IIgs and +# X68000, where MAME prints "Unable to load image" and runs with the port +# unplugged. Listening, what the program sends before the connection +# arrives waits for it, except on DOS, which drops it; the Amiga holds the +# whole machine until the connection arrives. +# # The emulators and firmware come from ./install.sh # (install emulator:PORT). Interactive runs use the program's own folder # or disk image, so saves and logs persist; --headless and --data runs @@ -56,6 +77,20 @@ progArgs=() emuPid="" xvfbPid="" work="" +serialMode="" # connect (--serial), listen (--serial-listen) or off +serialHost="" +serialPort="" +serialBaud=9600 # MAME's null modem's own default rate +serialArgs=() # MAME's options for the link (mameSerial) +serialPty="" # the pty socat hands Hatari and FS-UAE +relayPid="" +tcpPortMax=65535 +# How long socat keeps trying to reach --serial's server before the run +# stops; an unreachable host otherwise hangs for the kernel's SYN timeout. +serialConnectSeconds=10 +# DOSBox logs whether COM1 reached --serial's server as it starts. +dosSerialLogSeconds=5 +pollSeconds=0.1 usage() { @@ -71,6 +106,7 @@ fail() { cleanup() { joeyStop "$emuPid" + joeyStop "$relayPid" if [ -n "$xvfbPid" ]; then kill "$xvfbPid" 2>/dev/null || true fi @@ -234,6 +270,107 @@ runMame() { } +# ---- the serial link ---------------------------------------------------------- + +# portFree PORT -- nothing on this machine listens on TCP PORT yet. Checked +# up front because an emulator that cannot take the port only says so in a +# log nobody reads, and runs with the serial port unplugged. +portFree() { + python3 - "$1" <<'PY' +import socket +import sys + +probe = socket.socket() +probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +try: + probe.bind(("", int(sys.argv[1]))) +except OSError: + sys.exit(1) +PY +} + + +# serialNote NAME -- tell the user where the serial port (NAME) is wired. +serialNote() { + case "$serialMode" in + connect) echo "joey-run: $1 connects to $serialHost:$serialPort." ;; + listen) echo "joey-run: $1 waits for a connection on TCP port $serialPort." ;; + esac +} + + +# relaySerial NEAR PATH -- run socat between the emulator's end of the link +# (NEAR, a pty or unix socket that appears at PATH) and the network, and +# wait for PATH. socat opens its first address before its second, so the +# order matters. Connecting, the TCP connection goes first: an unreachable +# server stops the run before the machine boots. Listening, NEAR goes +# first: the emulator can open it at once, and what the program sends +# waits in the kernel until a client connects. +relaySerial() { + local near="$1" + local path="$2" + local deadline + command -v socat >/dev/null 2>&1 || + fail "the serial link on this machine goes through socat, which is not installed (package socat: apt install socat, dnf install socat)" + if [ "$serialMode" = "connect" ]; then + (cd "$work" && exec socat "tcp:$serialHost:$serialPort,connect-timeout=$serialConnectSeconds" "$near") 2>"$work/socat.log" & + else + (cd "$work" && exec socat "$near" "tcp-listen:$serialPort,reuseaddr") 2>"$work/socat.log" & + fi + relayPid=$! + deadline=$((SECONDS + serialConnectSeconds + 1)) + while [ ! -e "$path" ]; do + if ! joeyAlive "$relayPid" || [ "$SECONDS" -ge "$deadline" ]; then + cat "$work/socat.log" >&2 + if [ "$serialMode" = "connect" ]; then + fail "cannot connect to $serialHost:$serialPort" + fi + fail "socat cannot listen on TCP port $serialPort" + fi + sleep "$pollSeconds" + done +} + + +# mameSerial DRIVER SLOT -- set serialArgs to the MAME options that plug a +# null modem into SLOT and wire it up. MAME's bitbanger connects out by +# itself; to listen, socat takes the TCP port and MAME connects to socat +# through a unix socket. The null modem clocks the wire itself, at +# --serial-baud, which cfg/DRIVER.cfg sets. +mameSerial() { + local driver="$1" + local slot="$2" + local bitb="socket.$serialHost:$serialPort" + [ -n "$serialMode" ] || return 0 + mkdir -p "$work/cfg" + python3 "$repo/scripts/lib/mameNullModemCfg.py" "$driver" ":$slot:null_modem" "$serialBaud" \ + > "$work/cfg/$driver.cfg" || fail "--serial-baud $serialBaud is not a rate MAME's null modem has" + if [ "$serialMode" = "listen" ]; then + relaySerial "unix-listen:serial.sock" "$work/serial.sock" + bitb="domain.serial.sock" + fi + serialArgs=(-"$slot" null_modem -bitb "$bitb") +} + + +# dosSerialCheck -- DOSBox tries --serial's server as it starts and, when +# that fails, only says so in its log and runs with COM1 unplugged; stop +# with the reason instead. +dosSerialCheck() { + local deadline=$((SECONDS + dosSerialLogSeconds)) + local line + [ "$serialMode" = "connect" ] || return 0 + while [ "$SECONDS" -lt "$deadline" ]; do + line="$(grep -m 1 'SERIAL: Port 1 ' "$work/dosbox.log" 2>/dev/null)" || true + case "$line" in + *"connection failed"*) fail "DOSBox cannot connect COM1 to $serialHost:$serialPort" ;; + ?*) return 0 ;; + esac + sleep "$pollSeconds" + done +} + + # ---- one function per machine ------------------------------------------------- runIigs() { @@ -287,8 +424,10 @@ runIigs() { media=(-flop3 "$work/boot.po" -flop4 "$work/data.2mg") ;; esac + mameSerial "$machine" modem echo "joey-run: booting the IIgs ($machine); the program comes up in about a minute. Close MAME to stop." - runMame "$machine" -ramsize "$ramSize" -rompath "$IIGS_ROMPATH" "${media[@]}" + serialNote "the modem port" + runMame "$machine" -ramsize "$ramSize" -rompath "$IIGS_ROMPATH" "${media[@]}" "${serialArgs[@]}" } @@ -327,7 +466,24 @@ runAmiga() { args+=(--joystick_port_0=none --joystick_port_1=none) export ALSOFT_DRIVERS=null fi + case "$serialMode" in + connect) + # FS-UAE's tcp:// serial port only ever listens, so socat makes + # the connection and hands FS-UAE a pty. + relaySerial "pty,rawer,link=$serialPty" "$serialPty" + args+=(--amiga_enable_serial_port=1 --serial_port="$serialPty") + ;; + listen) + # /wait holds the machine until the client connects, so nothing + # the program sends goes to nobody. + args+=(--amiga_enable_serial_port=1 --serial_port="tcp://0.0.0.0:$serialPort/wait") + ;; + esac echo "joey-run: booting the Amiga; $name starts from the JOEYLIB hard drive. Close FS-UAE to stop." + serialNote "the serial port" + if [ "$serialMode" = "listen" ]; then + echo "joey-run: the Amiga does not start until that connection arrives." + fi "$FSUAE" "${args[@]}" >"$work/fs-uae.log" 2>&1 & emuPid=$! waitOrStop @@ -351,7 +507,14 @@ runAtarist() { startDisplay args+=(--sound off) fi + if [ -n "$serialMode" ]; then + # Hatari's serial port can only be a file or a tty, so socat + # stands between a pty and the network. + relaySerial "pty,rawer,link=$serialPty" "$serialPty" + args+=(--rs232-in "$serialPty" --rs232-out "$serialPty") + fi echo "joey-run: booting the Atari ST (EmuTOS); $(basename "$program") starts from drive C. Close Hatari to stop." + serialNote "the Modem port" "$HATARI" "${args[@]}" >"$work/hatari.log" 2>&1 & emuPid=$! waitOrStop @@ -374,6 +537,12 @@ runDos() { fi args=("${JOEY_DOSBOX_ISOLATE[@]}" --conf "$repo/scripts/dosbox-386sx16.conf" --set "mouse_capture=seamless" -c "MOUNT C \"$dir\"" -c "C:" -c "$command") + # transparent:1 -- data bytes only: DOSBox's in-band escapes for the + # control lines would reach a plain TCP peer as data. + case "$serialMode" in + connect) args+=(--set "serial1=nullmodem server:$serialHost port:$serialPort transparent:1") ;; + listen) args+=(--set "serial1=nullmodem port:$serialPort transparent:1") ;; + esac if [ "$headless" = "1" ]; then startDisplay else @@ -382,8 +551,10 @@ runDos() { fi args+=(--exit) echo "joey-run: booting DOS (386SX-16); $command runs from drive C. Close DOSBox to stop." + serialNote "COM1" XDG_CONFIG_HOME="$work/xdg" "$DOSBOX" "${args[@]}" >"$work/dosbox.log" 2>&1 & emuPid=$! + dosSerialCheck waitOrStop } @@ -414,8 +585,10 @@ runX68000() { if [ -d "$stage/DATA" ]; then x68kAddTree "$work/data.xdf" "$stage/DATA" DATA || fail "the DATA files do not fit on the 1232 KB data disk" fi + mameSerial x68000 rs232c echo "joey-run: booting the X68000 (Human68k 3.02); $name runs from drive B. Close MAME to stop." - runMame x68000 -bios ipl10 -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" + serialNote "the RS-232C port" + runMame x68000 -bios ipl10 -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" "${serialArgs[@]}" } @@ -438,6 +611,22 @@ while [ $# -gt 0 ]; do ;; --ram) ramSize="${2:?--ram needs a size such as 4M}"; shift ;; --debug) debug=1 ;; + --serial) + [ -z "$serialMode" ] || fail "one --serial or --serial-listen per run" + [[ "${2:-}" =~ ^([^:]+):([0-9]+)$ ]] || fail "--serial takes HOST:PORT (a host name or IPv4 address)" + serialMode=connect + serialHost="${BASH_REMATCH[1]}" + serialPort="${BASH_REMATCH[2]}" + shift + ;; + --serial-listen) + [ -z "$serialMode" ] || fail "one --serial or --serial-listen per run" + [[ "${2:-}" =~ ^[0-9]+$ ]] || fail "--serial-listen takes a port number" + serialMode=listen + serialPort="$2" + shift + ;; + --serial-baud) serialBaud="${2:?--serial-baud needs a rate such as 2400}"; shift ;; -h|--help) usage; exit 0 ;; --) shift; progArgs=("$@"); break ;; -*) fail "unknown option $1 (see: joey-run --help)" ;; @@ -462,6 +651,12 @@ if [ -n "$dataDir" ]; then [ -d "$dataDir" ] || fail "--data: no such folder $dataDir" dataDir="$(cd "$dataDir" && pwd)" fi +if [ -n "$serialMode" ]; then + [ "$serialPort" -ge 1 ] && [ "$serialPort" -le "$tcpPortMax" ] || fail "TCP port $serialPort is not 1 to $tcpPortMax" +fi +if [ "$serialMode" = "listen" ]; then + portFree "$serialPort" || fail "TCP port $serialPort is already in use" +fi # Resolve PROGRAM to a file and its machine. if [ -d "$program" ]; then @@ -482,6 +677,7 @@ if [ ${#progArgs[@]} -gt 0 ] && [ "$detected" != "dos" ]; then fi work="$(mktemp -d -t joey-run.XXXXXX)" +serialPty="$work/serial.pty" trap cleanup EXIT INT TERM case "$detected" in diff --git a/make/amiga.mk b/make/amiga.mk index 80b1b6c..eb324a7 100644 --- a/make/amiga.mk +++ b/make/amiga.mk @@ -17,7 +17,7 @@ BINDIR := $(BUILD)/bin # independently. -I on $(SRC_DIR)/amiga lets ptplayer.h resolve # from the machine-local shim alongside our code. PTPLAYER_DIR := $(AMIGA_PTPLAYER) -CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT -I$(SRC_DIR)/amiga -I$(SRC_68K) -I$(PTPLAYER_DIR) -MMD -MP $(CFLAGS_EXTRA) +CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -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. @@ -64,43 +64,43 @@ LIB_OBJS := \ LIB := $(LIBDIR)/libjoey.a -HELLO_SRC := $(EXAMPLES)/hello/hello.c -HELLO_BIN := $(BINDIR)/Hello -PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c -PATTERN_BIN := $(BINDIR)/Pattern -DRAW_SRC := $(EXAMPLES)/draw/draw.c -DRAW_BIN := $(BINDIR)/Draw -KEYS_SRC := $(EXAMPLES)/keys/keys.c -KEYS_BIN := $(BINDIR)/Keys -SERIAL_SRC := $(EXAMPLES)/serial/serial.c -SERIAL_BIN := $(BINDIR)/Serial -SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c -SERTEST_BIN := $(BINDIR)/SerTest -SAVE_SRC := $(EXAMPLES)/save/save.c -SAVE_BIN := $(BINDIR)/Save -JOY_SRC := $(EXAMPLES)/joy/joy.c -JOY_BIN := $(BINDIR)/Joy -SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c -SPRITE_BIN := $(BINDIR)/Sprite -AUDIO_SRC := $(EXAMPLES)/audio/audio.c -AUDIO_BIN := $(BINDIR)/Audio -UBER_SRC := $(EXAMPLES)/uber/uber.c -UBER_BIN := $(BINDIR)/Uber -ADV_SRC := $(EXAMPLES)/adventure/adventure.c -ADV_BIN := $(BINDIR)/Adv -ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c -ADV2_BIN := $(BINDIR)/Adv2 -AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -AGI_BIN := $(BINDIR)/Agi +HELLO_SRC := $(EXAMPLES)/hello/hello.c +HELLO_BIN := $(BINDIR)/Hello +PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c +PATTERN_BIN := $(BINDIR)/Pattern +DRAW_SRC := $(EXAMPLES)/draw/draw.c +DRAW_BIN := $(BINDIR)/Draw +KEYS_SRC := $(EXAMPLES)/keys/keys.c +KEYS_BIN := $(BINDIR)/Keys +SERIAL_SRC := $(EXAMPLES)/serial/serial.c +SERIAL_BIN := $(BINDIR)/Serial +SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c +SERTEST_BIN := $(BINDIR)/SerTest +SAVE_SRC := $(EXAMPLES)/save/save.c +SAVE_BIN := $(BINDIR)/Save +TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c +TILEBANK_BIN := $(BINDIR)/TileBank +JOY_SRC := $(EXAMPLES)/joy/joy.c +JOY_BIN := $(BINDIR)/Joy +SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c +SPRITE_BIN := $(BINDIR)/Sprite +AUDIO_SRC := $(EXAMPLES)/audio/audio.c +AUDIO_BIN := $(BINDIR)/Audio +UBER_SRC := $(EXAMPLES)/uber/uber.c +UBER_BIN := $(BINDIR)/Uber +ADV_SRC := $(EXAMPLES)/adventure/adventure.c +ADV_BIN := $(BINDIR)/Adv +ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c +ADV2_BIN := $(BINDIR)/Adv2 +AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c +AGI_BIN := $(BINDIR)/Agi STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c STAXI_INSTALL_DIR := $(BINDIR)/Taxi STAXI_BIN := $(STAXI_INSTALL_DIR)/Taxi -# Space Taxi asset staging: same pattern as the DOS port -- sources -# under examples/spacetaxi/assets, generated artifacts under -# examples/spacetaxi/generated (nukeable), runtime tree under -# bin/assets/. Mirrors the dos.mk wiring exactly. -STAXI_SRC_DIR := $(EXAMPLES)/spacetaxi/assets +# Space Taxi asset staging: same pattern as the DOS port -- generated +# artifacts under examples/spacetaxi/generated (nukeable), the runtime +# tree in DATA/ beside the binary. Mirrors the dos.mk wiring exactly. STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/amiga STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA # Level data is extracted from the C64 ROM (see dos.mk for the long @@ -109,22 +109,10 @@ STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ $(STAXI_GEN_DIR)/levels/title.dat STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN) -# Tile-bank PNGs (font.png and any tiles/*.png) bake to .tbk with -# Amiga plane-major bytes. Sprite-sheet PNGs bake to cross-target -# chunky 4bpp .spr (the Phase 11 walker reads chunky). The runtime -# loader (jlTileBankLoad / jlSpriteBankLoad) refuses .tbk files -# baked for the wrong target byte, so a stale build won't silently -# render garbage. -STAXI_TILE_PNGS := $(STAXI_SRC_DIR)/font.png $(wildcard $(STAXI_SRC_DIR)/tiles/*.png) -STAXI_SPRITE_PNGS := $(wildcard $(STAXI_SRC_DIR)/sprites/*.png) -STAXI_TBK_GEN := $(patsubst $(STAXI_SRC_DIR)/%.png,$(STAXI_GEN_DIR)/%.tbk,$(STAXI_TILE_PNGS)) -STAXI_SPR_GEN := $(patsubst $(STAXI_SRC_DIR)/%.png,$(STAXI_GEN_DIR)/%.spr,$(STAXI_SPRITE_PNGS)) STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) -STAXI_TBK_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_TBK_GEN)) -STAXI_SPR_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPR_GEN)) STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) # Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake -# tool for THIS target and staged next to the .spr, so the runtime loads +# tool for THIS target and staged in DATA/sprites, so the runtime loads # compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled). SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-amiga # The C64-exact cel bank, generated by staxibake (from the shared cel table @@ -160,19 +148,18 @@ STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICEL # not reach the game (found 2026-09-23 when a new cel made the runtime # reject the bank as too short). STAXI_ASSET_DSTS += $(STAXICELS_SPC_RUN) -STAXI_SPC_GEN := $(STAXI_SPR_GEN:.spr=.spc) -STAXI_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPC_GEN)) -ASSETBAKE_BIN := $(REPO_DIR)/tools/assetbake/assetbake.py -ASSETBAKE_TARGET := amiga # Game data lives under bin/DATA/, ready to be copied into the # scratch JOEYLIB hard-drive dir staged by joey-run. # audio.c fopens "DATA/test.mod" etc. relative to the boot volume. DATA_DIR := $(BINDIR)/DATA DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx +# TileBank's DATA/tilebank: this port's bake of the tile-bank fixture, the +# other targets' bakes it must refuse, and the expected pixels (common.mk). +TILEBANK_DATA := $(DATA_DIR)/tilebank/own.tbk .PHONY: all amiga clean-amiga clean -all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(call staxiData,$(STAXI_ASSET_DSTS)) +all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(TILEBANK_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(TILEBANK_DATA) $(call staxiData,$(STAXI_ASSET_DSTS)) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) @@ -244,6 +231,10 @@ $(SAVE_BIN): $(SAVE_SRC) $(LIB) @mkdir -p $(dir $@) $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) +$(TILEBANK_BIN): $(TILEBANK_SRC) $(LIB) + @mkdir -p $(dir $@) + $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) + $(JOY_BIN): $(JOY_SRC) $(LIB) @mkdir -p $(dir $@) $(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) @@ -278,20 +269,12 @@ $(STAXI_BIN): $(STAXI_SRCS) $(LIB) # Space Taxi asset pipeline. .SECONDARY keeps the generated/ artifacts # from being auto-deleted after staging. -.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) +.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) $(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) @mkdir -p $(STAXI_GEN_DIR)/levels python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(STAXI_GEN_DIR)/levels -$(STAXI_GEN_DIR)/%.tbk: $(STAXI_SRC_DIR)/%.png $(ASSETBAKE_BIN) - @mkdir -p $(dir $@) - python3 $(ASSETBAKE_BIN) --type tile --target $(ASSETBAKE_TARGET) $< $@ - -$(STAXI_GEN_DIR)/%.spr: $(STAXI_SRC_DIR)/%.png $(ASSETBAKE_BIN) - @mkdir -p $(dir $@) - python3 $(ASSETBAKE_BIN) --type sprite --cell 3x3 $< $@ - $(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN) @mkdir -p $(dir $@) $(SPRITEBAKE_BIN) $< $@ @@ -302,8 +285,7 @@ $(SPRITEBAKE_BIN): $(STAXIBAKE_BIN): $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake -# Explicit rule (overrides the %.spr-from-%.png pattern): the cel bank is -# generated from the game's cel table, not a PNG. +# The cel bank is generated from the game's cel table (stCels.c). $(STAXICELS_SPR_GEN): $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c @mkdir -p $(dir $@) $(STAXIBAKE_BIN) $@ @@ -312,14 +294,6 @@ $(STAXI_RUN_DIR)/levels/%.dat: $(STAXI_GEN_DIR)/levels/%.dat @mkdir -p $(dir $@) cp $< $@ -$(STAXI_RUN_DIR)/%.tbk: $(STAXI_GEN_DIR)/%.tbk - @mkdir -p $(dir $@) - cp $< $@ - -$(STAXI_RUN_DIR)/%.spr: $(STAXI_GEN_DIR)/%.spr - @mkdir -p $(dir $@) - cp $< $@ - $(STAXI_RUN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spc @mkdir -p $(dir $@) cp $< $@ @@ -332,6 +306,9 @@ $(DATA_DIR)/test.sfx: $(REPO_DIR)/assets/test.sfx @mkdir -p $(DATA_DIR) cp $< $@ +$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS) + python3 $(TILEBANK_STAGE) $(PLATFORM) $(TILEBANK_PNG) $(dir $@) + clean-amiga: rm -rf $(BUILD) @@ -346,7 +323,7 @@ clean: clean-amiga # different TUs see different struct layouts. -include $(LIB_OBJS:.o=.d) -# Shorter stem than the %.spr-from-%.png pattern, so it wins for level banks. +# The per-level cel banks, baked from each level's file. $(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stLevelCels.c @mkdir -p $(dir $@) $(STAXIBAKE_BIN) --level $< $@ diff --git a/make/atarist.mk b/make/atarist.mk index b886099..a22fbbd 100644 --- a/make/atarist.mk +++ b/make/atarist.mk @@ -11,9 +11,8 @@ BUILD := $(REPO_DIR)/build/$(PLATFORM) LIBDIR := $(BUILD)/lib BINDIR := $(BUILD)/bin -# CFLAGS_EXTRA: command-line A/B hook (e.g. make CFLAGS_EXTRA=-DX) # so flag experiments never edit a tracked file (perf-hunt-2 rank 17). -CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -I$(LIBXMP_DIR)/include -I$(ST_SHIM) -I$(SRC_DIR)/atarist -I$(SRC_68K) -MMD -MP $(CFLAGS_EXTRA) +CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -I$(LIBXMP_DIR)/include -I$(ST_SHIM) -I$(SRC_DIR)/atarist -I$(SRC_68K) -MMD -MP LDFLAGS := # libxmp-lite shared with the DOS port. Built as a static archive that @@ -51,40 +50,41 @@ LIB_OBJS := \ LIB := $(LIBDIR)/libjoey.a -HELLO_SRC := $(EXAMPLES)/hello/hello.c -HELLO_BIN := $(BINDIR)/HELLO.PRG -PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c -PATTERN_BIN := $(BINDIR)/PATTERN.PRG -DRAW_SRC := $(EXAMPLES)/draw/draw.c -DRAW_BIN := $(BINDIR)/DRAW.PRG -KEYS_SRC := $(EXAMPLES)/keys/keys.c -KEYS_BIN := $(BINDIR)/KEYS.PRG -SERIAL_SRC := $(EXAMPLES)/serial/serial.c -SERIAL_BIN := $(BINDIR)/SERIAL.PRG -SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c -SERTEST_BIN := $(BINDIR)/SERTEST.PRG -SAVE_SRC := $(EXAMPLES)/save/save.c -SAVE_BIN := $(BINDIR)/SAVE.PRG -JOY_SRC := $(EXAMPLES)/joy/joy.c -JOY_BIN := $(BINDIR)/JOY.PRG -SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c -SPRITE_BIN := $(BINDIR)/SPRITE.PRG -AUDIO_SRC := $(EXAMPLES)/audio/audio.c -AUDIO_BIN := $(BINDIR)/AUDIO.PRG -UBER_SRC := $(EXAMPLES)/uber/uber.c -UBER_BIN := $(BINDIR)/UBER.PRG -ADV_SRC := $(EXAMPLES)/adventure/adventure.c -ADV_BIN := $(BINDIR)/ADV.PRG -ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c -ADV2_BIN := $(BINDIR)/ADV2.PRG -AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -AGI_BIN := $(BINDIR)/AGI.PRG -STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c +HELLO_SRC := $(EXAMPLES)/hello/hello.c +HELLO_BIN := $(BINDIR)/HELLO.PRG +PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c +PATTERN_BIN := $(BINDIR)/PATTERN.PRG +DRAW_SRC := $(EXAMPLES)/draw/draw.c +DRAW_BIN := $(BINDIR)/DRAW.PRG +KEYS_SRC := $(EXAMPLES)/keys/keys.c +KEYS_BIN := $(BINDIR)/KEYS.PRG +SERIAL_SRC := $(EXAMPLES)/serial/serial.c +SERIAL_BIN := $(BINDIR)/SERIAL.PRG +SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c +SERTEST_BIN := $(BINDIR)/SERTEST.PRG +SAVE_SRC := $(EXAMPLES)/save/save.c +SAVE_BIN := $(BINDIR)/SAVE.PRG +TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c +TILEBANK_BIN := $(BINDIR)/TILEBANK.PRG +JOY_SRC := $(EXAMPLES)/joy/joy.c +JOY_BIN := $(BINDIR)/JOY.PRG +SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c +SPRITE_BIN := $(BINDIR)/SPRITE.PRG +AUDIO_SRC := $(EXAMPLES)/audio/audio.c +AUDIO_BIN := $(BINDIR)/AUDIO.PRG +UBER_SRC := $(EXAMPLES)/uber/uber.c +UBER_BIN := $(BINDIR)/UBER.PRG +ADV_SRC := $(EXAMPLES)/adventure/adventure.c +ADV_BIN := $(BINDIR)/ADV.PRG +ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c +ADV2_BIN := $(BINDIR)/ADV2.PRG +AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c +AGI_BIN := $(BINDIR)/AGI.PRG +STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c STAXI_INSTALL_DIR := $(BINDIR)/STAXI STAXI_BIN := $(STAXI_INSTALL_DIR)/STAXI.PRG # Space Taxi asset pipeline (mirrors dos.mk / amiga.mk). -STAXI_SRC_DIR := $(EXAMPLES)/spacetaxi/assets STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/atarist STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA # Level data is extracted from the C64 ROM (see dos.mk for the long @@ -93,16 +93,10 @@ STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ $(STAXI_GEN_DIR)/levels/title.dat STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN) -STAXI_TILE_PNGS := $(STAXI_SRC_DIR)/font.png $(wildcard $(STAXI_SRC_DIR)/tiles/*.png) -STAXI_SPRITE_PNGS := $(wildcard $(STAXI_SRC_DIR)/sprites/*.png) -STAXI_TBK_GEN := $(patsubst $(STAXI_SRC_DIR)/%.png,$(STAXI_GEN_DIR)/%.tbk,$(STAXI_TILE_PNGS)) -STAXI_SPR_GEN := $(patsubst $(STAXI_SRC_DIR)/%.png,$(STAXI_GEN_DIR)/%.spr,$(STAXI_SPRITE_PNGS)) STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) -STAXI_TBK_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_TBK_GEN)) -STAXI_SPR_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPR_GEN)) STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) # Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake -# tool for THIS target and staged next to the .spr, so the runtime loads +# tool for THIS target and staged in DATA/sprites, so the runtime loads # compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled). ST is # the one port whose .spc carries a reloc list (the shifted-DRAW thunk's jmp to # stSpriteDrawShiftWalker), re-stamped by the loader. @@ -140,19 +134,18 @@ STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICEL # not reach the game (found 2026-09-23 when a new cel made the runtime # reject the bank as too short). STAXI_ASSET_DSTS += $(STAXICELS_SPC_RUN) -STAXI_SPC_GEN := $(STAXI_SPR_GEN:.spr=.spc) -STAXI_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPC_GEN)) -ASSETBAKE_BIN := $(REPO_DIR)/tools/assetbake/assetbake.py -ASSETBAKE_TARGET := atarist # Game data lives under bin/DATA/, alongside the binaries Hatari picks # up when bin/ is mounted as the GEMDOS C: drive. audio.c fopens # "DATA/test.mod" etc. DATA_DIR := $(BINDIR)/DATA DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx +# TILEBANK's DATA/tilebank: this port's bake of the tile-bank fixture, the +# other targets' bakes it must refuse, and the expected pixels (common.mk). +TILEBANK_DATA := $(DATA_DIR)/tilebank/own.tbk .PHONY: all atarist clean-atarist clean -all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(call staxiData,$(STAXI_ASSET_DSTS)) +all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(TILEBANK_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(TILEBANK_DATA) $(call staxiData,$(STAXI_ASSET_DSTS)) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) @@ -231,6 +224,10 @@ $(SAVE_BIN): $(SAVE_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) +$(TILEBANK_BIN): $(TILEBANK_SRC) $(LIB) $(LIBXMP_AR) + @mkdir -p $(dir $@) + $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) + $(JOY_BIN): $(JOY_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) @@ -264,20 +261,12 @@ $(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) $(ST_CC) $(CFLAGS) $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS) # Space Taxi asset pipeline (mirrors dos.mk + amiga.mk). -.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) +.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) $(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) @mkdir -p $(STAXI_GEN_DIR)/levels python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(STAXI_GEN_DIR)/levels -$(STAXI_GEN_DIR)/%.tbk: $(STAXI_SRC_DIR)/%.png $(ASSETBAKE_BIN) - @mkdir -p $(dir $@) - python3 $(ASSETBAKE_BIN) --type tile --target $(ASSETBAKE_TARGET) $< $@ - -$(STAXI_GEN_DIR)/%.spr: $(STAXI_SRC_DIR)/%.png $(ASSETBAKE_BIN) - @mkdir -p $(dir $@) - python3 $(ASSETBAKE_BIN) --type sprite --cell 3x3 $< $@ - $(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN) @mkdir -p $(dir $@) $(SPRITEBAKE_BIN) $< $@ @@ -288,8 +277,7 @@ $(SPRITEBAKE_BIN): $(STAXIBAKE_BIN): $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake -# Explicit rule (overrides the %.spr-from-%.png pattern): the cel bank is -# generated from the game's cel table, not a PNG. +# The cel bank is generated from the game's cel table (stCels.c). $(STAXICELS_SPR_GEN): $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c @mkdir -p $(dir $@) $(STAXIBAKE_BIN) $@ @@ -298,14 +286,6 @@ $(STAXI_RUN_DIR)/levels/%.dat: $(STAXI_GEN_DIR)/levels/%.dat @mkdir -p $(dir $@) cp $< $@ -$(STAXI_RUN_DIR)/%.tbk: $(STAXI_GEN_DIR)/%.tbk - @mkdir -p $(dir $@) - cp $< $@ - -$(STAXI_RUN_DIR)/%.spr: $(STAXI_GEN_DIR)/%.spr - @mkdir -p $(dir $@) - cp $< $@ - $(STAXI_RUN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spc @mkdir -p $(dir $@) cp $< $@ @@ -318,6 +298,9 @@ $(DATA_DIR)/test.sfx: $(REPO_DIR)/assets/test.sfx @mkdir -p $(DATA_DIR) cp $< $@ +$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS) + python3 $(TILEBANK_STAGE) $(PLATFORM) $(TILEBANK_PNG) $(dir $@) + clean-atarist: rm -rf $(BUILD) @@ -332,7 +315,7 @@ clean: clean-atarist # different TUs see different struct layouts. -include $(LIB_OBJS:.o=.d) -# Shorter stem than the %.spr-from-%.png pattern, so it wins for level banks. +# The per-level cel banks, baked from each level's file. $(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stLevelCels.c @mkdir -p $(dir $@) $(STAXIBAKE_BIN) --level $< $@ diff --git a/make/common.mk b/make/common.mk index 9ff1baf..efefb8a 100644 --- a/make/common.mk +++ b/make/common.mk @@ -36,6 +36,14 @@ SRC_CG := $(REPO_DIR)/src/codegen SRC_68K := $(REPO_DIR)/src/m68k EXAMPLES := $(REPO_DIR)/examples +# The tile-bank example (examples/tilebank) loads the host gate's fixture, so +# the host (scripts/check-tilebank.sh) and every port share one expected hash. +# Each port stages its DATA/tilebank with TILEBANK_STAGE: its own bake, every +# other target's bake (to be refused) and the expected pixels. +TILEBANK_PNG := $(REPO_DIR)/tests/host/tileBank.png +TILEBANK_STAGE := $(REPO_DIR)/tests/host/tileBankStage.py +TILEBANK_BAKE_DEPS := $(TILEBANK_PNG) $(TILEBANK_STAGE) $(REPO_DIR)/tools/assetbake/assetbake.py + # Portable C sources for libjoey -- present on every target. CORE_C_SRCS := $(wildcard $(SRC_CORE)/*.c) @@ -46,5 +54,7 @@ CORE_C_SRCS := $(wildcard $(SRC_CORE)/*.c) GENERIC_C_SRCS := $(filter-out %/spriteEmitStub.c, $(wildcard $(SRC_DIR)/generic/*.c)) # Common include flags. Per-port code can include port.h / surfaceInternal.h -# directly because SRC_CORE is in the include path. +# directly because SRC_CORE is in the include path. EXTRA_CFLAGS is the one +# command-line hook for extra flags on every port (make EXTRA_CFLAGS=-DX); +# make/iigs.mk, which does not use COMMON_CFLAGS, honours it too. COMMON_CFLAGS := -I$(INCLUDE_DIR) -I$(SRC_CORE) -Wall -Wextra -Werror -O2 $(EXTRA_CFLAGS) diff --git a/make/dos.mk b/make/dos.mk index 96d9cb0..fed33b7 100644 --- a/make/dos.mk +++ b/make/dos.mk @@ -15,9 +15,8 @@ BINDIR := $(BUILD)/bin # -fomit-frame-pointer: flag parity with the ST/Amiga CFLAGS (both had # it; DOS never did). Frees EBP for allocation in the hot loops under # DOSBox's per-instruction-charged core (perf-hunt-2 rank 2). -# CFLAGS_EXTRA: command-line A/B hook (e.g. make CFLAGS_EXTRA=-DX) # so flag experiments never edit a tracked file (perf-hunt-2 rank 17). -CFLAGS := $(COMMON_CFLAGS) -march=i386 -m32 -fomit-frame-pointer -I$(SRC_DIR)/dos -I$(LIBXMP_DIR)/include -MMD -MP $(CFLAGS_EXTRA) +CFLAGS := $(COMMON_CFLAGS) -march=i386 -m32 -fomit-frame-pointer -I$(SRC_DIR)/dos -I$(LIBXMP_DIR)/include -MMD -MP ASFLAGS := -f coff LDFLAGS := @@ -47,34 +46,36 @@ LIB_OBJS := \ LIB := $(LIBDIR)/libjoey.a -HELLO_SRC := $(EXAMPLES)/hello/hello.c -HELLO_BIN := $(BINDIR)/HELLO.EXE -PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c -PATTERN_BIN := $(BINDIR)/PATTERN.EXE -DRAW_SRC := $(EXAMPLES)/draw/draw.c -DRAW_BIN := $(BINDIR)/DRAW.EXE -KEYS_SRC := $(EXAMPLES)/keys/keys.c -KEYS_BIN := $(BINDIR)/KEYS.EXE -SERIAL_SRC := $(EXAMPLES)/serial/serial.c -SERIAL_BIN := $(BINDIR)/SERIAL.EXE -SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c -SERTEST_BIN := $(BINDIR)/SERTEST.EXE -SAVE_SRC := $(EXAMPLES)/save/save.c -SAVE_BIN := $(BINDIR)/SAVE.EXE -JOY_SRC := $(EXAMPLES)/joy/joy.c -JOY_BIN := $(BINDIR)/JOY.EXE -SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c -SPRITE_BIN := $(BINDIR)/SPRITE.EXE -AUDIO_SRC := $(EXAMPLES)/audio/audio.c -AUDIO_BIN := $(BINDIR)/AUDIO.EXE -UBER_SRC := $(EXAMPLES)/uber/uber.c -UBER_BIN := $(BINDIR)/UBER.EXE -ADV_SRC := $(EXAMPLES)/adventure/adventure.c -ADV_BIN := $(BINDIR)/ADV.EXE -ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c -ADV2_BIN := $(BINDIR)/ADV2.EXE -AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -AGI_BIN := $(BINDIR)/AGI.EXE +HELLO_SRC := $(EXAMPLES)/hello/hello.c +HELLO_BIN := $(BINDIR)/HELLO.EXE +PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c +PATTERN_BIN := $(BINDIR)/PATTERN.EXE +DRAW_SRC := $(EXAMPLES)/draw/draw.c +DRAW_BIN := $(BINDIR)/DRAW.EXE +KEYS_SRC := $(EXAMPLES)/keys/keys.c +KEYS_BIN := $(BINDIR)/KEYS.EXE +SERIAL_SRC := $(EXAMPLES)/serial/serial.c +SERIAL_BIN := $(BINDIR)/SERIAL.EXE +SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c +SERTEST_BIN := $(BINDIR)/SERTEST.EXE +SAVE_SRC := $(EXAMPLES)/save/save.c +SAVE_BIN := $(BINDIR)/SAVE.EXE +TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c +TILEBANK_BIN := $(BINDIR)/TILEBANK.EXE +JOY_SRC := $(EXAMPLES)/joy/joy.c +JOY_BIN := $(BINDIR)/JOY.EXE +SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c +SPRITE_BIN := $(BINDIR)/SPRITE.EXE +AUDIO_SRC := $(EXAMPLES)/audio/audio.c +AUDIO_BIN := $(BINDIR)/AUDIO.EXE +UBER_SRC := $(EXAMPLES)/uber/uber.c +UBER_BIN := $(BINDIR)/UBER.EXE +ADV_SRC := $(EXAMPLES)/adventure/adventure.c +ADV_BIN := $(BINDIR)/ADV.EXE +ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c +ADV2_BIN := $(BINDIR)/ADV2.EXE +AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c +AGI_BIN := $(BINDIR)/AGI.EXE STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c # JoeyLib install convention: each app lives in its own bin subdir # with a DATA/ folder alongside the binary for runtime assets. @@ -85,15 +86,17 @@ STAXI_BIN := $(STAXI_INSTALL_DIR)/STAXI.EXE # up when bin/ is mounted as C:. audio.c fopens "DATA/test.mod" etc. DATA_DIR := $(BINDIR)/DATA DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx +# TILEBANK's DATA/tilebank: this port's bake of the tile-bank fixture, the +# other targets' bakes it must refuse, and the expected pixels (common.mk). +TILEBANK_DATA := $(DATA_DIR)/tilebank/own.tbk # Space Taxi asset pipeline. # -# Sources live in examples/spacetaxi/assets/ (txt, png, etc. -- the -# stuff the user hand-authors). Build artifacts go to -# examples/spacetaxi/generated/ (dat, jas) -- safe to nuke; recreated -# by this makefile. Runtime files are then copied from generated/ -# into the per-target bin/assets/ tree. -STAXI_SRC_DIR := $(EXAMPLES)/spacetaxi/assets +# Build artifacts go to examples/spacetaxi/generated/ -- safe to +# nuke; recreated by this makefile. What the game loads is then copied +# into the DATA tree beside its binary: the levels, the cel bank, the +# per-level cel banks and the speech. Tiles and the font come from the +# C64 data compiled into the binary (stC64Data.c). STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/dos STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA @@ -103,29 +106,18 @@ STAXI_RUN_DIR := $(STAXI_INSTALL_DIR)/DATA # decompressor through a Python 6502 emulator and writes STL2 .dat # files directly. Input is the raw RAM dump. mkstlevel + title.txt # is no longer used: the C64 title encodes its screen with the game's -# custom charset (extracted into tbank0.png), so a hand-authored tile +# custom charset (C64 data in stC64Data.c), so a hand-authored tile # layout with a different glyph mapping renders as garbage. STAXI_ROM_LEVEL_GEN := $(foreach n,$(STAXI_LEVEL_NUMS),$(STAXI_GEN_DIR)/levels/level$(n).dat) \ $(STAXI_GEN_DIR)/levels/title.dat STAXI_LEVEL_GEN := $(STAXI_ROM_LEVEL_GEN) -# Tile-bank PNGs (font.png and any tiles/*.png) bake to .tbk with -# DOS / IIgs chunky 4bpp bytes. Sprite-sheet PNGs bake to cross- -# target chunky 4bpp .spr blobs. -STAXI_TILE_PNGS := $(STAXI_SRC_DIR)/font.png $(wildcard $(STAXI_SRC_DIR)/tiles/*.png) -STAXI_SPRITE_PNGS := $(wildcard $(STAXI_SRC_DIR)/sprites/*.png) -STAXI_TBK_GEN := $(patsubst $(STAXI_SRC_DIR)/%.png,$(STAXI_GEN_DIR)/%.tbk,$(STAXI_TILE_PNGS)) -STAXI_SPR_GEN := $(patsubst $(STAXI_SRC_DIR)/%.png,$(STAXI_GEN_DIR)/%.spr,$(STAXI_SPRITE_PNGS)) -ASSETBAKE_BIN := $(REPO_DIR)/tools/assetbake/assetbake.py -ASSETBAKE_TARGET := dos -# Final runtime tree (mirrors generated/ into bin/assets/). +# The runtime tree beside the binary. STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN)) -STAXI_TBK_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_TBK_GEN)) -STAXI_SPR_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPR_GEN)) STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) # Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake -# tool for THIS target and staged next to the .spr, so the runtime loads +# tool for THIS target and staged in DATA/sprites, so the runtime loads # compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled). SPRITEBAKE_BIN := $(REPO_DIR)/build/tools/spritebake-dos # The C64-exact cel bank, generated by staxibake (from the shared cel table @@ -161,13 +153,11 @@ STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICEL # not reach the game (found 2026-09-23 when a new cel made the runtime # reject the bank as too short). STAXI_ASSET_DSTS += $(STAXICELS_SPC_RUN) -STAXI_SPC_GEN := $(STAXI_SPR_GEN:.spr=.spc) -STAXI_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPC_GEN)) MKSTLEVEL_BIN := $(REPO_DIR)/build/tools/mkstlevel .PHONY: all dos clean-dos clean -all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(call staxiData,$(STAXI_ASSET_DSTS)) +all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(SERIAL_BIN) $(SERTEST_BIN) $(SAVE_BIN) $(TILEBANK_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(DATA_FILES) $(TILEBANK_DATA) $(call staxiData,$(STAXI_ASSET_DSTS)) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) @@ -236,6 +226,11 @@ $(SAVE_BIN): $(SAVE_SRC) $(LIB) $(LIBXMP_AR) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(DOS_EMBED_DPMI) $@ +$(TILEBANK_BIN): $(TILEBANK_SRC) $(LIB) $(LIBXMP_AR) + @mkdir -p $(dir $@) + $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ + $(DOS_EMBED_DPMI) $@ + $(JOY_BIN): $(JOY_SRC) $(LIB) $(LIBXMP_AR) @mkdir -p $(dir $@) $(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ @@ -280,7 +275,7 @@ $(STAXI_BIN): $(STAXI_SRCS) $(LIB) $(LIBXMP_AR) # Mark these .SECONDARY so make doesn't auto-delete them after the # stage-B copy into bin/ -- they're the canonical distribution # artifacts. -.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXI_TBK_GEN) $(STAXI_SPR_GEN) $(STAXI_SPC_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) +.SECONDARY: $(STAXI_LEVEL_GEN) $(STAXICELS_SPC_GEN) $(STAXILEVEL_SPR_GEN) $(STAXILEVEL_SPC_GEN) # Bulk-extract all 24 canonical levels plus the title from the C64 # ROM in one invocation. Grouped target (&:) means make treats the @@ -290,14 +285,6 @@ $(STAXI_ROM_LEVEL_GEN) &: $(STAXI_RAW_DUMP) $(ROMTOLEVEL_BIN) @mkdir -p $(STAXI_GEN_DIR)/levels python3 $(ROMTOLEVEL_BIN) $(STAXI_RAW_DUMP) $(STAXI_GEN_DIR)/levels -$(STAXI_GEN_DIR)/%.tbk: $(STAXI_SRC_DIR)/%.png $(ASSETBAKE_BIN) - @mkdir -p $(dir $@) - python3 $(ASSETBAKE_BIN) --type tile --target $(ASSETBAKE_TARGET) $< $@ - -$(STAXI_GEN_DIR)/%.spr: $(STAXI_SRC_DIR)/%.png $(ASSETBAKE_BIN) - @mkdir -p $(dir $@) - python3 $(ASSETBAKE_BIN) --type sprite --cell 3x3 $< $@ - $(STAXI_GEN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spr $(SPRITEBAKE_BIN) @mkdir -p $(dir $@) $(SPRITEBAKE_BIN) $< $@ @@ -308,25 +295,16 @@ $(SPRITEBAKE_BIN): $(STAXIBAKE_BIN): $(MAKE) -f $(REPO_DIR)/make/tools.mk staxibake -# Explicit rule (overrides the %.spr-from-%.png pattern): the cel bank is -# generated from the game's cel table, not a PNG. +# The cel bank is generated from the game's cel table (stCels.c). $(STAXICELS_SPR_GEN): $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stC64Data.c @mkdir -p $(dir $@) $(STAXIBAKE_BIN) $@ -# Stage B: copy generated/ into the per-target bin/assets/ runtime tree +# Stage B: copy generated/ into the runtime tree beside the binary $(STAXI_RUN_DIR)/levels/%.dat: $(STAXI_GEN_DIR)/levels/%.dat @mkdir -p $(dir $@) cp $< $@ -$(STAXI_RUN_DIR)/%.tbk: $(STAXI_GEN_DIR)/%.tbk - @mkdir -p $(dir $@) - cp $< $@ - -$(STAXI_RUN_DIR)/%.spr: $(STAXI_GEN_DIR)/%.spr - @mkdir -p $(dir $@) - cp $< $@ - $(STAXI_RUN_DIR)/%.spc: $(STAXI_GEN_DIR)/%.spc @mkdir -p $(dir $@) cp $< $@ @@ -342,6 +320,9 @@ $(DATA_DIR)/test.sfx: $(REPO_DIR)/assets/test.sfx @mkdir -p $(DATA_DIR) cp $< $@ +$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS) + python3 $(TILEBANK_STAGE) $(PLATFORM) $(TILEBANK_PNG) $(dir $@) + clean-dos: rm -rf $(BUILD) @@ -363,7 +344,7 @@ clean-spacetaxi: # different TUs see different struct layouts. -include $(LIB_OBJS:.o=.d) -# Shorter stem than the %.spr-from-%.png pattern, so it wins for level banks. +# The per-level cel banks, baked from each level's file. $(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIBAKE_BIN) $(EXAMPLES)/spacetaxi/stLevelCels.c @mkdir -p $(dir $@) $(STAXIBAKE_BIN) --level $< $@ diff --git a/make/iigs.mk b/make/iigs.mk index d636b31..042ab78 100644 --- a/make/iigs.mk +++ b/make/iigs.mk @@ -56,22 +56,28 @@ LIB_SRCS := $(LIB_C_SRCS) $(PORT_ASM_SRCS) IIGS_LIB_HDRS := $(wildcard $(INCLUDE_DIR)/joey/*.h) $(wildcard $(SRC_CORE)/*.h) $(wildcard $(REPO_DIR)/src/codegen/*.h) $(wildcard $(SRC_DIR)/iigs/*.h) $(wildcard $(REPO_DIR)/src/m68k/*.h) # Example sources (single-TU unless noted). -PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c -DRAW_SRC := $(EXAMPLES)/draw/draw.c -KEYS_SRC := $(EXAMPLES)/keys/keys.c -SERIAL_SRC := $(EXAMPLES)/serial/serial.c -SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c +PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c +DRAW_SRC := $(EXAMPLES)/draw/draw.c +KEYS_SRC := $(EXAMPLES)/keys/keys.c +SERIAL_SRC := $(EXAMPLES)/serial/serial.c +SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c SCCPROBE_SRC := $(EXAMPLES)/sccprobe/sccprobe.c -SERLOSS_SRC := $(EXAMPLES)/serloss/serloss.c -SAVE_SRC := $(EXAMPLES)/save/save.c -JOY_SRC := $(EXAMPLES)/joy/joy.c -SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c -UBER_SRC := $(EXAMPLES)/uber/uber.c -ADV_SRC := $(EXAMPLES)/adventure/adventure.c -ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c -AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -STAXI_SRCS := $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c -AUDIO_SRC := $(EXAMPLES)/audio/audio.c +SERLOSS_SRC := $(EXAMPLES)/serloss/serloss.c +SAVE_SRC := $(EXAMPLES)/save/save.c +TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c +JOY_SRC := $(EXAMPLES)/joy/joy.c +SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c +UBER_SRC := $(EXAMPLES)/uber/uber.c +ADV_SRC := $(EXAMPLES)/adventure/adventure.c +ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c +AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c +STAXI_SRCS := $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c +AUDIO_SRC := $(EXAMPLES)/audio/audio.c + +# TILEBANK's DATA/tilebank: this port's bake of the tile-bank fixture, the +# other targets' bakes it must refuse, and the expected pixels (common.mk). +# make-iigs-disk.sh packs it as /JOEYLIB/DATA/TILEBANK beside the program. +TILEBANK_DATA := $(BINDIR)/DATA/tilebank/own.tbk # NinjaTrackerPlus replayer: Merlin32-assemble ninjatrackerplus.s to a 34KB # binary that ships next to the program; jlpAudioInit loads "ntpplayer" from @@ -82,7 +88,7 @@ NTP_PATCH := $(REPO_DIR)/make/support/iigs/ntpIrqWindow.py NTP_BIN := $(BUILD)/audio/ntpplayer.bin IIGS_MERLIN := $(IIGS_AS) -.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-disk-all iigs-game-disk iigs-game-disks iigs-game-disk-agi iigs-verify-game-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-serial iigs-verify-serial-loss iigs-verify-shrtail clean-iigs clean +.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-disk-all iigs-game-disk iigs-game-disks iigs-game-disk-agi iigs-verify-game-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-serial iigs-verify-serial-loss iigs-verify-shrtail iigs-verify-tilebank clean-iigs clean # Default: build (compile-check) the library. The MAME smoke test is # `iigs-clang-smoke`; disks and examples are `iigs-disk`. @@ -94,10 +100,11 @@ all iigs: iigs-lib # fix), and AUDIO's 34KB-rodata blocker was resolved by loading the # NTP replayer from disk (see the $(BINDIR)/AUDIO rule). Leaving them # out made `make iigs` silently test 8-day-old STAXI binaries against -# new assets (the 2026-07-20 gear-out/no-flame reports). +# new assets (the 2026-07-20 gear-out/no-flame reports). TILEBANK's staged +# DATA rides along, so every disk that packs TILEBANK finds it. iigs-examples: $(BINDIR)/PATTERN $(BINDIR)/DRAW $(BINDIR)/KEYS $(BINDIR)/SERIAL $(BINDIR)/SERTEST $(BINDIR)/SAVE $(BINDIR)/JOY \ $(BINDIR)/SPRITE $(BINDIR)/UBER $(BINDIR)/ADV $(BINDIR)/ADV2 \ - $(BINDIR)/AGI $(BINDIR)/STAXI $(BINDIR)/AUDIO + $(BINDIR)/AGI $(BINDIR)/STAXI $(BINDIR)/AUDIO $(BINDIR)/TILEBANK $(TILEBANK_DATA) # Build the JOEYLIB data disk (build/iigs/bin/joey.2mg) that the gates, and # `joey-run build/iigs/bin/joey.2mg`, mount beside GS/OS. Packs the launchable examples as ProDOS S16 apps @@ -210,7 +217,7 @@ iigs-verify-game-disk: iigs-game-disk iigs-disk-all: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(call staxiData,$(STAXI_IIGS_LEVELS) $(STAXILEVEL_IIGS_SPC)) $(STAXI_IIGS_SPEECH) BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" \ JOEY_DISK_SIZE="16MB" \ - JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE AUDIO UBER ADV2 ADV AGI STAXI" \ + JOEY_DISK_EXAMPLES="DRAW PATTERN KEYS JOY SPRITE SERIAL SERTEST SAVE TILEBANK AUDIO UBER ADV2 ADV AGI STAXI" \ $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey-all.2mg # Headless visual-verification gate: boot GS/OS under MAME, launch the DRAW @@ -270,6 +277,12 @@ iigs-verify-shrtail: $(LIB) $(NTP_BIN) { echo "iigs-verify-shrtail: FAIL - \$$E1 SCB/palette block mismatched the stage" >&2; exit 1; } || true @echo "iigs-verify-shrtail: PASS" +# Tile-bank gate: TILEBANK loads this port's own .tbk bake both ways off its +# own one-example disk; both must hash like the host gate's, and every other +# target's bake must be refused. NTP_BIN keeps the disk's audio init quiet. +iigs-verify-tilebank: $(BINDIR)/TILEBANK $(TILEBANK_DATA) $(NTP_BIN) + $(REPO_DIR)/scripts/verify-iigs-tilebank.sh + # Runtime gate for the save HAL's GS/OS calls (Create + GetDevNumber/DInfo/ # Volume): boots SAVE under MAME and checks the save-OK lamp + disk-free bar. iigs-verify-save: $(BINDIR)/SAVE $(BINDIR)/DRAW @@ -360,6 +373,13 @@ $(BINDIR)/SAVE: $(SAVE_SRC) $(LIB) $(IIGS_CLANG_BUILD) @mkdir -p $(dir $@) $(DEP_DIR) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SAVE.d $(INCLUDES) -o $@ $(SAVE_SRC) $(LIB) +$(BINDIR)/TILEBANK: $(TILEBANK_SRC) $(LIB) $(IIGS_CLANG_BUILD) + @mkdir -p $(dir $@) $(DEP_DIR) + $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/TILEBANK.d $(INCLUDES) -o $@ $(TILEBANK_SRC) $(LIB) + +$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS) + python3 $(TILEBANK_STAGE) $(PLATFORM) $(TILEBANK_PNG) $(dir $@) + $(BINDIR)/JOY: $(JOY_SRC) $(LIB) $(IIGS_CLANG_BUILD) @mkdir -p $(dir $@) $(DEP_DIR) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/JOY.d $(INCLUDES) -o $@ $(JOY_SRC) $(LIB) diff --git a/make/x68000.mk b/make/x68000.mk index d751305..14721e0 100644 --- a/make/x68000.mk +++ b/make/x68000.mk @@ -35,7 +35,7 @@ export PATH := $(X68K_ROOT)/bin:$(PATH) CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer \ -DJOEYLIB_PLATFORM_X68000 \ -I$(LIBXMP_DIR)/include \ - -I$(SRC_DIR)/x68000 -I$(SRC_68K) -I$(REPO_DIR)/src/codegen -MMD -MP $(CFLAGS_EXTRA) + -I$(SRC_DIR)/x68000 -I$(SRC_68K) -I$(REPO_DIR)/src/codegen -MMD -MP LDFLAGS := -lm PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c) @@ -93,26 +93,35 @@ LIB := $(LIBDIR)/libjoey.a # KEYS the input and mouse vehicle: key predicates, the typed line # (jlInputGetChar) and its verification row. # SERIAL the RS-232C gate's echo program. -HELLO_SRC := $(EXAMPLES)/hello/hello.c -PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c -DRAW_SRC := $(EXAMPLES)/draw/draw.c -KEYS_SRC := $(EXAMPLES)/keys/keys.c -SERIAL_SRC := $(EXAMPLES)/serial/serial.c -SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c -SAVE_SRC := $(EXAMPLES)/save/save.c -JOY_SRC := $(EXAMPLES)/joy/joy.c -SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c -AUDIO_SRC := $(EXAMPLES)/audio/audio.c -UBER_SRC := $(EXAMPLES)/uber/uber.c -ADV_SRC := $(EXAMPLES)/adventure/adventure.c -ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c -AGI_SRC := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c -EXAMPLES_X := HELLO PATTERN DRAW KEYS SERIAL SERTEST SAVE JOY SPRITE AUDIO UBER ADV ADV2 AGI +# SERMOUSE the serial + mouse gate's program: streams both ways on the +# RS-232C port (SCC channel A) while the mouse (channel B) moves. +# TILEBANK the tile-bank gate's program: this port's .tbk bake loaded both +# ways and pixel-checked, every other target's bake refused. +HELLO_SRC := $(EXAMPLES)/hello/hello.c +PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c +DRAW_SRC := $(EXAMPLES)/draw/draw.c +KEYS_SRC := $(EXAMPLES)/keys/keys.c +SERIAL_SRC := $(EXAMPLES)/serial/serial.c +SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c +SERMOUSE_SRC := $(EXAMPLES)/sermouse/sermouse.c +SAVE_SRC := $(EXAMPLES)/save/save.c +TILEBANK_SRC := $(EXAMPLES)/tilebank/tilebank.c +JOY_SRC := $(EXAMPLES)/joy/joy.c +SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c +AUDIO_SRC := $(EXAMPLES)/audio/audio.c +UBER_SRC := $(EXAMPLES)/uber/uber.c +ADV_SRC := $(EXAMPLES)/adventure/adventure.c +ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c +AGI_SRC := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c +EXAMPLES_X := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK JOY SPRITE AUDIO UBER ADV ADV2 AGI EXAMPLE_BINS := $(foreach e,$(EXAMPLES_X),$(BINDIR)/$(e).X) # AUDIO loads DATA/test.mod and DATA/test.sfx from beside the program. DATA_DIR := $(BINDIR)/DATA DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx +# TILEBANK's DATA/tilebank: this port's bake of the tile-bank fixture, the +# other targets' bakes it must refuse, and the expected pixels (common.mk). +TILEBANK_DATA := $(DATA_DIR)/tilebank/own.tbk # Space Taxi ships in its own folder with its DATA, as on the other ports, # so its megabyte of data never rides onto every other program's floppy. @@ -120,13 +129,13 @@ STAXI_DIR := $(BINDIR)/STAXI STAXI_BIN := $(STAXI_DIR)/STAXI.X STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stLevelFile.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stSim.c $(EXAMPLES)/spacetaxi/stFare.c $(EXAMPLES)/spacetaxi/stHooks.c $(EXAMPLES)/spacetaxi/stTitle.c $(EXAMPLES)/spacetaxi/stC64Data.c $(EXAMPLES)/spacetaxi/stCels.c $(EXAMPLES)/spacetaxi/stAudio.c $(EXAMPLES)/spacetaxi/stLevelCels.c -.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-golden clean-x68000 clean +.PHONY: all x68000 x68000-lib x68000-examples x68000-verify-serial x68000-verify-mouse x68000-verify-serial-mouse x68000-verify-tilebank x68000-verify-golden clean-x68000 clean all x68000: x68000-lib x68000-examples x68000-lib: $(LIB) $(LIBXMP_AR) -x68000-examples: $(EXAMPLE_BINS) $(STAXI_BIN) $(DATA_FILES) +x68000-examples: $(EXAMPLE_BINS) $(STAXI_BIN) $(DATA_FILES) $(TILEBANK_DATA) $(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c @mkdir -p $(dir $@) @@ -171,6 +180,9 @@ $(DATA_DIR)/%: $(REPO_DIR)/assets/% @mkdir -p $(dir $@) cp $< $@ +$(TILEBANK_DATA): $(TILEBANK_BAKE_DEPS) + python3 $(TILEBANK_STAGE) $(PLATFORM) $(TILEBANK_PNG) $(dir $@) + # Space Taxi asset variables (the rules follow the binary below); make # expands prerequisites when it reads the rule, so these come first. STAXI_GEN_DIR := $(EXAMPLES)/spacetaxi/generated/x68000 @@ -273,6 +285,16 @@ x68000-verify-serial: $(BINDIR)/SERIAL.X x68000-verify-mouse: $(BINDIR)/KEYS.X $(REPO_DIR)/scripts/verify-x68000-mouse.sh +# Serial + mouse together: both share the SCC, so stream both ways on the +# RS-232C port while the mouse moves, and check neither disturbs the other. +x68000-verify-serial-mouse: $(BINDIR)/SERMOUSE.X + $(REPO_DIR)/scripts/verify-x68000-serial-mouse.sh + +# Tile-bank gate: this port's own .tbk bake loaded both ways must hash like +# the host gate's, and every other target's bake must be refused (~1 min). +x68000-verify-tilebank: $(BINDIR)/TILEBANK.X $(TILEBANK_DATA) + $(REPO_DIR)/scripts/verify-x68000-tilebank.sh + # Golden-hash gate against the Apple IIgs reference (~70 min: UBER on the # generic renderer is slow and must reach jlLogFlush before hashes exist). x68000-verify-golden: $(LIB) $(LIBXMP_AR) diff --git a/scripts/check-tilebank.sh b/scripts/check-tilebank.sh new file mode 100755 index 0000000..194b895 --- /dev/null +++ b/scripts/check-tilebank.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# check-tilebank.sh - Host-cc gate for the baked tile-bank loaders +# (jlTileBankLoad / jlTileBankLoadToSurface in src/core/assetLoad.c), end to +# end: assetbake.py bakes tests/host/tileBank.png, the real loader reads it, +# the real generic tile paste draws it, and tests/host/tileBankHost.c checks +# every pixel against the PNG. Also checks the refusals (a bank baked for any +# other target, bad magic, short header, missing file), a truncated bank, +# maxTiles below the bank size, a bank without a palette, and a bank bigger +# than a surface. +# +# Built as a whole BLANK-port program (the check-blank.sh source set plus the +# harness). BLANK draws chunky 4bpp like DOS, and assetLoad.c expects the DOS +# target byte there, so the DOS bake is the bank that must load. Every other +# target assetbake knows is baked too and must be refused. +# +# tests/host/tileBankStage.py stages the banks and the expected pixels and +# palette (decoded from the PNG with PIL, independent of assetbake's +# encoders), exactly as make/.mk stages them for examples/tilebank on +# the real ports. The harness prints the surface hash of its to-surface load; +# scripts/verify--tilebank.sh run this script and require each port's +# own bake to hash the same. Exits nonzero on any failed check. +set -euo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cc=${CC:-cc} +work=$(mktemp -d -t joeylib-tilebank.XXXXXX) +trap 'rm -rf "$work"' EXIT +png="$repo/tests/host/tileBank.png" +hostTarget=dos + +cd "$repo" +"$cc" -DJOEYLIB_PLATFORM_BLANK \ + -Iinclude -Iinclude/joey -Isrc/core -Isrc/codegen -Wall \ + src/core/*.c src/generic/*.c src/blank/blank.c tests/host/tileBankHost.c \ + -o "$work/tileBankHost" + +python3 "$repo/tests/host/tileBankStage.py" "$hostTarget" "$png" "$work/DATA" +refuse=() +for bank in "$work"/DATA/refuse*.tbk; do + refuse+=("$(basename "$bank")") +done + +# Run from the temp dir: the harness reads and writes DATA/ there. +cd "$work" +"$work/tileBankHost" "${refuse[@]}" diff --git a/scripts/lib/joeyEnv.sh b/scripts/lib/joeyEnv.sh index 1c85807..b814324 100644 --- a/scripts/lib/joeyEnv.sh +++ b/scripts/lib/joeyEnv.sh @@ -76,6 +76,13 @@ joeyExampleProgram() { return 0 fi done + # A mixed-case Amiga name (SerTest) is not derivable from the example + # name, so take the file in any case. + candidate="$(find "$bin" -maxdepth 1 -type f -iname "$up$ext" -print -quit 2>/dev/null)" + if [ -n "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi printf '%s\n' "$bin/$up$ext" } diff --git a/scripts/lib/mameNullModemCfg.py b/scripts/lib/mameNullModemCfg.py new file mode 100644 index 0000000..cc54165 --- /dev/null +++ b/scripts/lib/mameNullModemCfg.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""mameNullModemCfg.py - a MAME cfg file that sets a null_modem's line. + +MAME's null_modem clocks the wire itself, at 9600 8 N 1 unless its +configuration says otherwise, so a guest whose UART runs at any other rate +sees garbage. The line is a CONFIG input port on the device, which MAME +reads back from cfg/SYSTEM.cfg in its -cfg_directory. + + mameNullModemCfg.py SYSTEM TAG BAUD [DATABITS PARITY STOPBITS] + +SYSTEM is the driver (x68000, apple2gs, apple2gsr1), TAG the null_modem's +device tag (":rs232c:null_modem", ":modem:null_modem"). The line may also +come as one quoted argument ("4800 7 E 1"). DATABITS PARITY STOPBITS +default to 8 N 1. Prints the cfg; exit 2 for a line MAME cannot set. +""" + +import sys + +# MAME's RS232_* enum values (src/devices/bus/rs232/rs232.h). +BAUD = {"50": 14, "75": 15, "110": 0, "150": 1, "200": 17, "300": 2, "600": 3, + "1200": 4, "1800": 18, "2000": 19, "2400": 5, "3600": 20, "4800": 6, + "7200": 21, "9600": 7, "14400": 8, "19200": 9, "28800": 10, + "38400": 11, "57600": 12, "115200": 13} +DATA = {"5": 0, "6": 1, "7": 2, "8": 3} +PARITY = {"N": 0, "O": 1, "E": 2} +STOP = {"1": 1, "2": 3} +DEFAULT_FORMAT = ["8", "N", "1"] +# The null_modem's own defaults (src/devices/bus/rs232/null_modem.cpp). +BAUD_DEFAULT = 7 +DATA_DEFAULT = 3 +PARITY_DEFAULT = 0 +STOP_DEFAULT = 1 + + +def main(argv): + if len(argv) < 4: + sys.stderr.write(__doc__) + return 2 + system = argv[1] + tag = argv[2] + line = " ".join(argv[3:]).split() + if len(line) == 1: + line += DEFAULT_FORMAT + if len(line) != 4: + sys.stderr.write("mameNullModemCfg.py: the line is BAUD [DATABITS PARITY STOPBITS]\n") + return 2 + baud, data, parity, stop = line + parity = parity.upper() + for value, table, what in ((baud, BAUD, "baud rate"), (data, DATA, "data bits"), + (parity, PARITY, "parity"), (stop, STOP, "stop bits")): + if value not in table: + sys.stderr.write("mameNullModemCfg.py: MAME's null_modem has no %s %s (it takes %s)\n" + % (what, value, " ".join(table))) + return 2 + ports = [("RS232_TXBAUD", BAUD_DEFAULT, BAUD[baud]), ("RS232_RXBAUD", BAUD_DEFAULT, BAUD[baud]), + ("RS232_DATABITS", DATA_DEFAULT, DATA[data]), ("RS232_PARITY", PARITY_DEFAULT, PARITY[parity]), + ("RS232_STOPBITS", STOP_DEFAULT, STOP[stop])] + print('\n\n \n ' % system) + for name, default, value in ports: + print(f' ') + print(' \n \n') + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/scripts/lib/serialStreamPeer.py b/scripts/lib/serialStreamPeer.py new file mode 100755 index 0000000..86d6b69 --- /dev/null +++ b/scripts/lib/serialStreamPeer.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""serialStreamPeer.py - host end of a both-directions serial stream gate. + +The guest (examples/sermouse) transmits an endless stream and checks an +endless stream from us, both byte-exact: + + byte k = (k ^ (k >> 8) ^ mask) & 0xFF + +with mask GUEST_MASK for the guest's stream and PEER_MASK for ours (the same +formula and masks as streamByte() in examples/sermouse/sermouse.c). A lost or +repeated byte throws every byte after it off its expected value. + +This peer listens (the emulator's null_modem connects out), checks the guest's +stream in order, and answers it: our stream is paced byte for byte on the +guest's, never more than WINDOW bytes ahead of what has come back. So when the +guest pauses its stream the line goes quiet both ways, and once the guest has +been silent for QUIET_SECONDS the counts are recorded as a quiet snapshot +"sent:received" -- at that moment every byte either side sent has arrived, so a +harness that reads the guest's own counts while it is paused can require them +to equal one of these exactly. + + serialStreamPeer.py PORT WALL MIN_BYTES + +Runs until the emulator closes the link or WALL seconds pass, then prints one +line, STREAM PASS or STREAM FAIL, with the counts and the quiet snapshots. PASS +means the guest's stream arrived with no bad byte and at least MIN_BYTES long. +Exit 0 on pass, 1 on fail. +""" + +import select +import sys +import time + +from serialLoopPeer import acceptOne + +GUEST_MASK = 0xA5 +PEER_MASK = 0x00 +WINDOW = 256 # our bytes allowed ahead of the guest's +QUIET_SECONDS = 0.25 # guest silence that counts as a pause +POLL_SECONDS = 0.05 +READ_SIZE = 4096 +BYTE_MASK = 0xFF +BYTE_BITS = 8 +NO_BAD_INDEX = -1 + + +def streamByte(index, mask): + return (index ^ (index >> BYTE_BITS) ^ mask) & BYTE_MASK + + +def run(conn, wall, start): + received = 0 + sent = 0 + bad = 0 + firstBad = NO_BAD_INDEX + lastRx = 0.0 + quietArmed = False + snapshots = [] + closed = False + + conn.setblocking(False) + while time.time() - start < wall: + # Our stream starts once the guest's has: it is open and listening. + want = received + WINDOW - sent if received > 0 else 0 + writers = [conn] if want > 0 else [] + readable, writable, _ = select.select([conn], writers, [], POLL_SECONDS) + now = time.time() + if readable: + try: + data = conn.recv(READ_SIZE) + except OSError: + data = b"" + if not data: + closed = True + break + for value in data: + if value != streamByte(received, GUEST_MASK): + if firstBad == NO_BAD_INDEX: + firstBad = received + bad += 1 + received += 1 + lastRx = now + quietArmed = True + if writable and want > 0: + chunk = bytes(streamByte(sent + i, PEER_MASK) for i in range(want)) + try: + sent += conn.send(chunk) + except OSError: + pass + if quietArmed and now - lastRx >= QUIET_SECONDS: + snapshots.append("%d:%d" % (sent, received)) + quietArmed = False + return received, sent, bad, firstBad, snapshots, closed + + +def main(argv): + if len(argv) != 4: + sys.stderr.write(__doc__) + return 2 + port = int(argv[1]) + wall = float(argv[2]) + minBytes = int(argv[3]) + start = time.time() + + print("STREAM listening on 127.0.0.1:%d" % port, flush=True) + conn = acceptOne(port, wall) + if conn is None: + print("STREAM FAIL: the emulator never connected") + return 1 + received, sent, bad, firstBad, snapshots, closed = run(conn, wall, start) + conn.close() + + ok = bad == 0 and received >= minBytes + print("STREAM %s received=%d sent=%d bad=%d firstBad=%d quiet=%s end=%s" % + ("PASS" if ok else "FAIL", received, sent, bad, firstBad, + ",".join(snapshots) if snapshots else "none", + "closed" if closed else "wall")) + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/scripts/lib/tileBankGate.sh b/scripts/lib/tileBankGate.sh new file mode 100644 index 0000000..5c4a780 --- /dev/null +++ b/scripts/lib/tileBankGate.sh @@ -0,0 +1,144 @@ +# Shared by the tile-bank gates, scripts/verify--tilebank.sh. Each runs +# examples/tilebank on its port with the DATA/tilebank make/.mk staged +# (tests/host/tileBankStage.py: the port's own .tbk bake of +# tests/host/tileBank.png, every other target's bake, the expected pixels), +# gets the program's TILEBANK log line off the machine, and judges it here +# against the host gate: scripts/check-tilebank.sh is run for its surface +# hash, so the expected value has exactly one source. +# Source after joeyEnv.sh. + +TILEBANK_GATE="$(basename "$0" .sh)" + + +# tileBankAwaitLine DIR SECONDS PID -- wait for the program's log (joeylog.txt +# in any case, as DOS and GEMDOS name it) in DIR to hold a TILEBANK line, for +# up to SECONDS or until PID exits; print the log's path if it does. +tileBankAwaitLine() { + local dir="$1" + local seconds="$2" + local pid="$3" + local log + local waited + + for ((waited = 0; waited < seconds; waited++)); do + log="$(find "$dir" -maxdepth 1 -iname joeylog.txt -print -quit)" + if [ -n "$log" ] && grep -q '^TILEBANK: ' "$log"; then + printf '%s\n' "$log" + return 0 + fi + joeyAlive "$pid" || return 1 + sleep 1 + done + return 1 +} + + +# tileBankChild PID -- print the child of PID: the emulator a `timeout -s KILL` +# wrapper started. Stop the child first; stopping only the wrapper can orphan +# an emulator that ignores SIGTERM (Hatari does). +tileBankChild() { + local tries + local child + + for ((tries = 0; tries < 20; tries++)); do + child="$(pgrep -P "$1" | head -1)" + if [ -n "$child" ]; then + printf '%s\n' "$child" + return 0 + fi + sleep 0.1 + done + return 1 +} + + +# tileBankData BINDIR -- print the DATA/tilebank to stage: the one +# make/.mk staged beside the program, or TILEBANK_DATA_DIR when set (a +# negative control stages a deliberately broken bake that way). +tileBankData() { + printf '%s\n' "${TILEBANK_DATA_DIR:-$1/DATA/tilebank}" +} + + +# tileBankJudge LOG -- judge examples/tilebank's TILEBANK line in LOG: the +# program's own verdict (both loads pixel-exact, the palette, every staged +# foreign bank refused both ways), both surface hashes equal to the host's, +# and every other target assetbake knows refused. Prints the line and one +# PASS or FAIL line; returns 0 on PASS. +tileBankJudge() { + local line + local verdict + local streamHash + local pasteHash + local refused + local wantRefused="$TILEBANK_OTHERS/$TILEBANK_OTHERS" + local fails=() + local why + + line="$(tr -d '\r' < "$1" | grep -E '^TILEBANK: ' | tail -1)" + if [ -z "$line" ]; then + echo "$TILEBANK_GATE: FAIL - no TILEBANK line in the program's log" >&2 + return 1 + fi + echo "$line" + verdict="$(sed -n 's/^TILEBANK: \([A-Z]*\).*/\1/p' <<< "$line")" + streamHash="$(sed -n 's/.* streamHash=\([0-9A-F]*\).*/\1/p' <<< "$line")" + pasteHash="$(sed -n 's/.* pasteHash=\([0-9A-F]*\).*/\1/p' <<< "$line")" + refused="$(sed -n 's/.* refused=\([0-9]*\/[0-9]*\).*/\1/p' <<< "$line")" + [ "$verdict" = PASS ] || fails+=("the program's own checks failed") + [ "$streamHash" = "$TILEBANK_HASH" ] || fails+=("streamHash ${streamHash:-missing}, host $TILEBANK_HASH") + [ "$pasteHash" = "$TILEBANK_HASH" ] || fails+=("pasteHash ${pasteHash:-missing}, host $TILEBANK_HASH") + [ "$refused" = "$wantRefused" ] || fails+=("refused ${refused:-missing}, want $wantRefused") + if [ "${#fails[@]}" -gt 0 ]; then + why="$(printf '%s; ' "${fails[@]}")" + echo "$TILEBANK_GATE: FAIL - ${why%; }" >&2 + return 1 + fi + echo "$TILEBANK_GATE: PASS (own bake pixel-exact both ways, hash $TILEBANK_HASH = host; $wantRefused other targets refused)" +} + + +# tileBankNeedBuild PROGRAM DATADIR BUILD-HINT -- exit 2 unless the port's +# TILEBANK program and its DATA/tilebank are there. +tileBankNeedBuild() { + local what + + for what in "$1" "$2/own.tbk" "$2/expect.bin"; do + if [ ! -e "$what" ]; then + echo "$TILEBANK_GATE: $what is missing; run: $3" >&2 + exit 2 + fi + done +} + + +# tileBankReference -- run the host gate, scripts/check-tilebank.sh, which must +# pass (its pixel checks are what make its hash worth matching), and set +# TILEBANK_HASH the surface hash of its to-surface load of its own bake +# TILEBANK_OTHERS how many targets besides any one port assetbake knows +# Exits 2 without a host C compiler, 1 if the host gate fails. +tileBankReference() { + local out + + if ! command -v "${CC:-cc}" > /dev/null; then + echo "$TILEBANK_GATE: no host C compiler (${CC:-cc}) for the reference run of scripts/check-tilebank.sh" >&2 + exit 2 + fi + if ! out="$("$JOEY_REPO/scripts/check-tilebank.sh" 2>&1)"; then + echo "$out" | tail -15 >&2 + echo "$TILEBANK_GATE: FAIL - the host reference, scripts/check-tilebank.sh, failed" >&2 + exit 1 + fi + TILEBANK_HASH="$(sed -n 's/.*to-surface hash \([0-9A-F]\{8\}\).*/\1/p' <<< "$out" | head -1)" + if [ -z "$TILEBANK_HASH" ]; then + echo "$TILEBANK_GATE: FAIL - scripts/check-tilebank.sh printed no to-surface hash" >&2 + exit 1 + fi + TILEBANK_OTHERS="$(python3 - "$JOEY_REPO/tools/assetbake" <<'PY' +import sys +sys.path.insert(0, sys.argv[1]) +from assetbake import TARGET_NAMES +print(len(TARGET_NAMES) - 1) +PY +)" +} diff --git a/scripts/lib/x68kDisk.sh b/scripts/lib/x68kDisk.sh index 85b812d..f0df1a8 100644 --- a/scripts/lib/x68kDisk.sh +++ b/scripts/lib/x68kDisk.sh @@ -32,7 +32,7 @@ x68kAddTree() { # (~900 KB, in subdirectories only deltree can free): nothing a JoeyLib # program needs -- except \SYS\RSDRV.SYS, Human68k's RS-232C driver, which # comes back: the stock CONFIG.SYS loads it (DEVICE = \SYS\RSDRV.SYS), and -# with it the serial add-on runs above 9600 bps and does RTS/CTS. +# the serial add-on requires it (src/x68000/serial.c). x68kBootDisk() { local out="$1" local name @@ -44,10 +44,11 @@ x68kBootDisk() { for name in SYS HIS BIN BASIC2 ASK ETC; do x68kXdf deltree "$out" "$name" >/dev/null 2>&1 || true done - # Optional: without it the serial add-on keeps the ROM's limits. - if x68kXdf extract "$X68K_SYSTEM_DISK" SYS/RSDRV.SYS "$out.rsdrv" >/dev/null 2>&1; then - x68kXdf add "$out" "$out.rsdrv" SYS/RSDRV.SYS >/dev/null || return 1 + if ! x68kXdf extract "$X68K_SYSTEM_DISK" SYS/RSDRV.SYS "$out.rsdrv" >/dev/null 2>&1; then + echo "x68kBootDisk: $X68K_SYSTEM_DISK has no SYS/RSDRV.SYS, which the serial add-on requires" >&2 + return 1 fi + x68kXdf add "$out" "$out.rsdrv" SYS/RSDRV.SYS >/dev/null || return 1 rm -f "$out.rsdrv" { printf '%s\r\n' "$@"; printf '\x1a'; } > "$out.autoexec" x68kXdf add "$out" "$out.autoexec" AUTOEXEC.BAT >/dev/null || return 1 diff --git a/scripts/make-iigs-disk.sh b/scripts/make-iigs-disk.sh index ca86f57..096032a 100755 --- a/scripts/make-iigs-disk.sh +++ b/scripts/make-iigs-disk.sh @@ -58,6 +58,17 @@ freeBlocks() { | grep -oE 'Free : [0-9]+' | grep -oE '[0-9]+' | head -1 } +# addDataFile FILE FOLDER -- add FILE to FOLDER on the volume as a plain ProDOS +# BIN ($06) file under its upper-cased name. GS/OS ProDOS lookups are +# case-insensitive, so a program's lowercase fopen paths still match. +addDataFile() { + local base + base=$(basename "$1") + cp "$1" "$work/${base^^}#060000" + "$CADIUS" ADDFILE "$OUT" "$2" "$work/${base^^}#060000" >/dev/null + rm -f "$work/${base^^}#060000" +} + added=() skipped=() for name in "${order[@]}"; do @@ -112,49 +123,43 @@ make-iigs-disk.sh: WARNING - NTPPLAYER is NOT on $OUT. EOF fi -# Space Taxi runtime data: the game fopen()s a DATA/ tree (levels/*.dat, -# tiles/tbank*.tbk, sprites/sprites.spr, font.tbk -- see stRender.c / -# stLevel.c). Packed only when STAXI made it onto the disk, from the IIgs -# asset bake at examples/spacetaxi/generated/iigs (levels and .spr are -# cross-target; .tbk banks are IIgs-baked -- assetLoad.c validates the -# header target byte). GS/OS ProDOS lookups are case-insensitive, so the -# uppercase ProDOS names match the game's lowercase fopen paths. +# Space Taxi runtime data: the game fopen()s a DATA/ tree -- levels/*.dat, +# sprites/staxicels.spc, the optional sprites/levelNN.spc and speech.bin +# (stLevel.c, stRender.c, stAudio.c); tiles and the font are C64 data +# compiled into the binary. Packed only when STAXI made it onto the disk, +# from the IIgs bake at examples/spacetaxi/generated/iigs, and ONLY those +# files: the bake directory also holds intermediates (the .spr the .spc +# compile from) and, in an old tree, art the game stopped loading. staxiData="$repo/examples/spacetaxi/generated/iigs" if [[ " ${added[*]:-} " == *" STAXI "* ]]; then if [ -d "$staxiData" ]; then "$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA" >/dev/null "$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA/LEVELS" >/dev/null - "$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA/TILES" >/dev/null "$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA/SPRITES" >/dev/null dataAdded=0 while IFS= read -r f; do rel=${f#"$staxiData/"} dir=$(dirname "$rel") - base=$(basename "$rel") dst="/$VOL/DATA" if [ "$dir" != "." ]; then dst="/$VOL/DATA/${dir^^}" fi - cp "$f" "$work/${base^^}#060000" - "$CADIUS" ADDFILE "$OUT" "$dst" "$work/${base^^}#060000" >/dev/null - rm -f "$work/${base^^}#060000" + addDataFile "$f" "$dst" dataAdded=$((dataAdded + 1)) - done < <(find "$staxiData" -type f -not -path '*/sprites/level*.spc' | sort) + done < <(find "$staxiData/levels" -type f -name '*.dat' | sort + printf '%s\n' "$staxiData/sprites/staxicels.spc" "$staxiData/speech.bin") # The per-level hook-sprite banks are optional (the game JIT-compiles # a level's cels at entry when its bank is missing) and large, so # they go on last and only while the floppy has room for them. levelSkipped=0 while IFS= read -r f; do - base=$(basename "$f") sz=$(stat -c%s "$f") need=$(( (sz + 511) / 512 + (sz + 131071) / 131072 + 2 )) if [ "$need" -gt "$(freeBlocks)" ]; then levelSkipped=$((levelSkipped + 1)) continue fi - cp "$f" "$work/${base^^}#060000" - "$CADIUS" ADDFILE "$OUT" "/$VOL/DATA/SPRITES" "$work/${base^^}#060000" >/dev/null - rm -f "$work/${base^^}#060000" + addDataFile "$f" "/$VOL/DATA/SPRITES" dataAdded=$((dataAdded + 1)) done < <(find "$staxiData" -type f -path '*/sprites/level*.spc' | sort) added+=("DATA[$dataAdded files]") @@ -166,6 +171,26 @@ if [[ " ${added[*]:-} " == *" STAXI "* ]]; then fi fi +# The tile-bank example's DATA/tilebank, staged by make/iigs.mk from the host +# gate's fixture (tests/host/tileBankStage.py): its own bake, every other +# target's bake (each must be refused) and the expected pixels. Packed only +# when TILEBANK made it onto the disk. CREATEFOLDER makes the missing parent +# and leaves a DATA folder Space Taxi's data already made alone. +tileBankData="$BINDIR/DATA/tilebank" +if [[ " ${added[*]:-} " == *" TILEBANK "* ]]; then + if [ -d "$tileBankData" ]; then + "$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA/TILEBANK" >/dev/null + tileBankAdded=0 + for f in "$tileBankData"/*; do + addDataFile "$f" "/$VOL/DATA/TILEBANK" + tileBankAdded=$((tileBankAdded + 1)) + done + added+=("DATA/TILEBANK[$tileBankAdded files]") + else + echo " WARNING: TILEBANK packed without its DATA/tilebank ($tileBankData missing)" >&2 + fi +fi + echo "iigs-disk: $OUT (volume /$VOL)" echo " added: ${added[*]:-none}" if [ ${#skipped[@]} -gt 0 ]; then diff --git a/scripts/verify-all.sh b/scripts/verify-all.sh index 691f586..615d494 100755 --- a/scripts/verify-all.sh +++ b/scripts/verify-all.sh @@ -1,45 +1,68 @@ #!/usr/bin/env bash -# verify-all.sh - Run one kind of gate on every port and summarise. +# verify-all.sh - Run JoeyLib's gates, one kind or all of them, and summarise. # -# Driven by `make verify-audio` and `make verify-serial`. +# Driven by `make verify` (all), `make verify-audio` and `make verify-serial`. # -# audio: five gates, two kinds: -# iigs DOC wavetable + sound IRQ vector (register tap) -# x68000 YM2151 noise + PPI port C + ADPCM rate (register tap) -# atarist YM2149 write trace, with the pc (register tap) -# amiga recorded sound, four loudness phases (Paula has no tap) -# dos recorded sound, four loudness phases (SB has no tap) +# Kinds, in the order `all` runs them: +# host the shared layer on the host, no emulator: the new-port +# template (blank), input, millis, save, serial, sfxmix, tilebank +# golden UBER on each port, its 51 per-op surface hashes against +# tests/goldens/uber (x68000 against the IIgs's, its own gate) +# visual iigs: every launchable example on joey.2mg boots and draws +# tilebank each port: examples/tilebank loads the port's own .tbk bake +# of the host fixture both ways, pixel-exact and with the host +# gate's surface hash, and refuses every other target's bake (the +# host half is check-tilebank.sh under `host`) +# input each port: a typed string reaches jlInputGetChar byte-exact +# audio five gates, two kinds: +# iigs DOC wavetable + sound IRQ vector (register tap) +# x68000 YM2151 noise + PPI port C + ADPCM rate (register tap) +# atarist YM2149 write trace, with the pc (register tap) +# amiga recorded sound, four loudness phases (Paula has no tap) +# dos recorded sound, four loudness phases (SB has no tap) +# serial the host check of the shared layer, then each port's loopback +# (banner out, probe echoed back): +# host scripts/check-serial.sh (no emulator) +# iigs SCC modem port -> MAME null_modem +# x68000 SCC channel A -> MAME null_modem +# atarist Modem port -> Hatari pty +# amiga serial.device -> FS-UAE TCP +# dos COM1 -> DOSBox nullmodem +# mouse x68000: deltas, clamps and buttons through the IOCS mouse +# (and with serial streaming both ways on the same SCC) +# save iigs: a SAVES/ blob written and read back through GS/OS +# loss iigs: 4096 bytes through full-screen presents, lossless on the +# modem port at 9600 and a Super Serial Card at 2400 +# disk iigs: the one-disk STAXI (ROM 3 and ROM 01) and AGI images boot +# and draw # -# serial: the host check of the shared layer, then each port's loopback -# (banner out, probe echoed back): -# host scripts/check-serial.sh (no emulator) -# iigs SCC modem port -> MAME null_modem -# x68000 SCC channel A -> MAME null_modem -# atarist Modem port -> Hatari pty -# amiga serial.device -> FS-UAE TCP -# dos COM1 -> DOSBox nullmodem +# `all` first BUILDS every selected port (make , and iigs-disk plus the +# loss probes and game disks for the IIgs), stopping on a build failure, then +# runs every kind. A gate for a port that was not selected is left out. # # SEQUENTIAL, deliberately. The two recording audio gates drive the emulator on # wall-clock sleeps and grade fixed time windows of the capture, so a # second emulator competing for CPU can slide a phase boundary and fail a -# gate that is actually fine. Running five emulators at once also makes -# the timing-sensitive register gates noisier. The whole set is ~2.5 -# minutes; parallelism is not worth the flakiness. +# gate that is actually fine. Running several emulators at once also makes +# the timing-sensitive register gates noisier. # # Exit codes, distinguishing "it works" from "it did not run": # 0 every selected gate PASSED -# 1 at least one gate FAILED (a real defect) +# 1 at least one gate FAILED (a real defect), or an `all` build failed # 3 no failures, but at least one gate SKIPPED for a missing # prerequisite -- an unbuilt port or absent emulator. Non-zero on # purpose: a run that quietly tested nothing must not look green. +# A gate reached through make cannot tell a skip from a failure (make +# exits 2 for both), so those count as failures; `all` builds their +# prerequisites first, so a missing one is a real problem anyway. # # Each gate already reports its own evidence line; this only adds the # roll-up. Gate stdout/stderr is echoed as it happens, so a failure shows # its diagnosis here and not just a verdict. # -# Usage: scripts/verify-all.sh audio|serial [port ...] -# with no ports, all of that kind's. Otherwise any of: -# iigs x68000 atarist amiga dos (and host, for serial) +# Usage: scripts/verify-all.sh KIND|all [port ...] +# with no ports, every port the kind has. Ports: host iigs +# x68000 atarist amiga dos # Env: VERIFY_ALL_LOG also append the full transcript to this file # Requires: whatever each selected gate requires; missing ones SKIP. set -uo pipefail @@ -49,77 +72,158 @@ kind=${1:-} shift || true label="verify-all $kind" -case $kind in - audio) ALL_PORTS="iigs x68000 atarist amiga dos" ;; - serial) ALL_PORTS="host iigs x68000 atarist amiga dos" ;; - *) echo "usage: scripts/verify-all.sh audio|serial [port ...]" >&2; exit 2 ;; -esac -ports=${*:-$ALL_PORTS} +KINDS="host golden visual tilebank input audio serial mouse save loss disk" +ALL_PORTS="host iigs x68000 atarist amiga dos" +# UBER on the Amiga needs longer than bench-amiga.sh's interactive default. +AMIGA_UBER_SECONDS=400 +# kind port name command...: every gate, in run order within its kind. +GATES=$(cat <&2; exit 2 ;; +esac +if [ "$kind" = all ]; then + kinds=$KINDS + kindPorts=$ALL_PORTS +else + kinds=$kind + kindPorts=$(echo "$GATES" | awk -v k="$kind" '$1 == k { print $2 }' | awk '!seen[$0]++' | tr '\n' ' ') +fi +ports=${*:-$kindPorts} for p in $ports; do - case " $ALL_PORTS " in + case " $kindPorts " in *" $p "*) ;; - *) echo "$label: unknown port '$p' (choose from: $ALL_PORTS)" >&2; exit 2 ;; + *) echo "$label: unknown port '$p' (choose from: $kindPorts)" >&2; exit 2 ;; esac done -gateFor() { # $1 = port -> script + args - case $kind:$1 in - audio:iigs) echo "scripts/verify-iigs-audio.sh staxi" ;; - audio:*) echo "scripts/verify-$1-audio.sh" ;; - serial:host) echo "scripts/check-serial.sh" ;; - serial:*) echo "scripts/verify-$1-serial.sh" ;; - esac +# Echo a command's output as it happens, and into VERIFY_ALL_LOG if set; +# returns the command's exit code. stdin is /dev/null so no gate can read +# the rest of the gate list. +runLogged() { + if [ -n "${VERIFY_ALL_LOG:-}" ]; then + ( cd "$repo" && bash -c "$1" ) < /dev/null 2>&1 | tee -a "$VERIFY_ALL_LOG" + return "${PIPESTATUS[0]}" + fi + ( cd "$repo" && bash -c "$1" ) < /dev/null } -passed="" -failed="" -skipped="" -declare -A elapsed +# The build step of `all`: each selected port, then the IIgs extras its +# gates need (make iigs builds no examples; iigs-disk does). +buildPorts() { + local port + local cmd -for port in $ports; do - gate=$(gateFor "$port") - echo - echo "=== $label: $port ($gate) ===" - start=$SECONDS - if [ -n "${VERIFY_ALL_LOG:-}" ]; then - ( cd "$repo" && $gate ) 2>&1 | tee -a "$VERIFY_ALL_LOG" - rc=${PIPESTATUS[0]} - else - ( cd "$repo" && $gate ) + for port in $ports; do + case $port in + host) continue ;; + iigs) cmd="make -f make/iigs.mk iigs-disk $repo/build/iigs/bin/SCCPROBE $repo/build/iigs/bin/SERLOSS $repo/build/iigs/bin/SERLOSSS" ;; + *) cmd="make $port" ;; + esac + echo + echo "=== $label: build $port ($cmd) ===" + if ! runLogged "$cmd"; then + echo "$label: FAIL - building $port failed" >&2 + exit 1 + fi + done +} + +if [ "$kind" = all ]; then + buildPorts +fi + +gateNames=() +gateVerdicts=() +gateSeconds=() +passed=0 +failed=0 +skipped=0 + +for k in $kinds; do + while read -r gKind gPort gName gCmd; do + [ "$gKind" = "$k" ] || continue + case " $ports " in *" $gPort "*) ;; *) continue ;; esac + echo + echo "=== $label: $gKind $gPort $gName ($gCmd) ===" + start=$SECONDS + runLogged "$gCmd" rc=$? - fi - elapsed[$port]=$((SECONDS - start)) - case $rc in - 0) passed="$passed $port" ;; - 2) skipped="$skipped $port" ;; - *) failed="$failed $port" ;; - esac + case $rc in + 0) verdict=PASS; passed=$((passed + 1)) ;; + 2) verdict="SKIP (prerequisite missing)"; skipped=$((skipped + 1)) ;; + *) verdict=FAIL; failed=$((failed + 1)) ;; + esac + gateNames+=("$gKind $gPort $gName") + gateVerdicts+=("$verdict") + gateSeconds+=("$((SECONDS - start))") + done <<< "$GATES" done -count() { set -- $1; echo $#; } - echo echo "================ $label summary ================" -for port in $ports; do - case " $passed " in *" $port "*) verdict=PASS ;; *) - case " $skipped " in *" $port "*) verdict="SKIP (prerequisite missing)" ;; *) - verdict=FAIL ;; - esac ;; - esac - printf " %-8s %-28s %3ds\n" "$port" "$verdict" "${elapsed[$port]}" +for i in "${!gateNames[@]}"; do + printf " %-26s %-28s %4ds\n" "${gateNames[$i]}" "${gateVerdicts[$i]}" "${gateSeconds[$i]}" done echo " ----" -printf " %d passed, %d failed, %d skipped\n" \ - "$(count "$passed")" "$(count "$failed")" "$(count "$skipped")" +printf " %d passed, %d failed, %d skipped\n" "$passed" "$failed" "$skipped" -if [ -n "$failed" ]; then - echo "$label: FAIL -$failed" >&2 +if [ "$failed" -gt 0 ]; then + echo "$label: FAIL" >&2 exit 1 fi -if [ -n "$skipped" ]; then - echo "$label: INCOMPLETE - skipped$skipped (build those ports, then re-run)" >&2 +if [ "$skipped" -gt 0 ]; then + echo "$label: INCOMPLETE - some gates skipped (build those ports, then re-run)" >&2 exit 3 fi -echo "$label: PASS (all $(count "$passed") $kind gates)" +if [ "$passed" -eq 0 ]; then + echo "$label: nothing selected" >&2 + exit 2 +fi +echo "$label: PASS (all $passed gates)" exit 0 diff --git a/scripts/verify-amiga-tilebank.sh b/scripts/verify-amiga-tilebank.sh new file mode 100755 index 0000000..5da2492 --- /dev/null +++ b/scripts/verify-amiga-tilebank.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# verify-amiga-tilebank.sh - Tile-bank gate for the Amiga: proves that +# assetbake's amiga encoder (plane-major: 8 rows of plane 0, then plane 1, ...) +# writes exactly the layout the Amiga's native tile paste +# (src/m68k/planarTile.h via src/amiga/amigaTile.h) reads, and that the Amiga +# build refuses a bank baked for any other target. +# +# Boots an A500 (real Kickstart) in FS-UAE under Xvfb from a scratch directory +# hard drive whose startup-sequence runs TileBank (examples/tilebank), with +# the DATA/tilebank make/amiga.mk staged from the host gate's fixture beside +# it. The program loads its own bake through jlTileBankLoadToSurface and +# through jlTileBankLoad + jlTilePaste, checks every pixel of both against the +# PNG, hashes both, and tries every other target's bake; its TILEBANK line in +# joeylog.txt must say PASS with both hashes equal to the host gate's +# (scripts/check-tilebank.sh, run here for the value) and every other target +# refused. FS-UAE is stopped as soon as the line appears. +# +# Usage: scripts/verify-amiga-tilebank.sh +# Env: TILEBANK_DATA_DIR stage this DATA/tilebank instead (negative controls) +# Requires: fs-uae, the Kickstart, Xvfb, a host cc; build/amiga/bin/TileBank +# and its DATA/tilebank (make amiga). +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +source "$repo/scripts/lib/tileBankGate.sh" +joeyNeed FSUAE fsuae +joeyNeed KICKSTART_ROM kickstart + +binDir=$repo/build/amiga/bin +dataDir=$(tileBankData "$binDir") +wall=180 # hard cap on FS-UAE, whatever happens to this script +lineSeconds=150 # a real-time boot plus the checks + +tileBankNeedBuild "$binDir/TileBank" "$dataDir" "make amiga" +tileBankReference + +work=$(mktemp -d -t joeylib-amiga-tilebank.XXXXXX) +guard="" +fsuae="" +xvfb="" +# 2>/dev/null: bash reports the SIGKILLed timeout wrapper as "Killed". +cleanup() { + joeyStop "$fsuae" "$guard" "$xvfb" 2> /dev/null + rm -rf "$work" +} +trap cleanup EXIT + +mkdir -p "$work/hd/s" "$work/hd/DATA" +cp "$binDir/TileBank" "$work/hd/" +cp -r "$dataDir" "$work/hd/DATA/tilebank" +echo ":TileBank" > "$work/hd/s/startup-sequence" + +display=":$((RANDOM % 400 + 1300))" +Xvfb "$display" -screen 0 800x600x24 -nolisten tcp > /dev/null 2>&1 & +xvfb=$! +sleep 1 + +# 2 MB of fast memory: a bare 512 KB A500 fails jlInit. +DISPLAY="$display" timeout -s KILL "$wall" "$FSUAE" \ + --amiga_model=A500 \ + --fast_memory=2048 \ + --kickstart_file="$KICKSTART_ROM" \ + --hard_drive_0="$work/hd" \ + --hard_drive_0_label=JOEYLIB \ + --automatic_input_grab=0 \ + --fullscreen=0 \ + --fade_out_duration=0 > "$work/fsuae.log" 2>&1 & +guard=$! +fsuae=$(tileBankChild "$guard") + +log=$(tileBankAwaitLine "$work/hd" "$lineSeconds" "$guard") +joeyStop "$fsuae" "$guard" 2> /dev/null +fsuae="" +guard="" +if [ -z "$log" ]; then + echo "verify-amiga-tilebank: FAIL - no TILEBANK line within ${lineSeconds}s" >&2 + tail -5 "$work/fsuae.log" >&2 + exit 1 +fi +tileBankJudge "$log" diff --git a/scripts/verify-atarist-tilebank.sh b/scripts/verify-atarist-tilebank.sh new file mode 100755 index 0000000..2eeaabc --- /dev/null +++ b/scripts/verify-atarist-tilebank.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# verify-atarist-tilebank.sh - Tile-bank gate for the Atari ST: proves that +# assetbake's atarist encoder (interleaved planes, 4 plane bytes per row) +# writes exactly the layout the ST's native tile paste (src/atarist/stTile.h) +# reads, and that the ST build refuses a bank baked for any other target. +# +# Runs TILEBANK.PRG (examples/tilebank) under Hatari from a scratch GEMDOS +# drive holding it and the DATA/tilebank make/atarist.mk staged from the host +# gate's fixture. The program loads its own bake through +# jlTileBankLoadToSurface and through jlTileBankLoad + jlTilePaste, checks +# every pixel of both against the PNG, hashes both, and tries every other +# target's bake; its TILEBANK log line must say PASS with both hashes equal +# to the host gate's (scripts/check-tilebank.sh, run here for the value) and +# every other target refused. Hatari is stopped as soon as the line appears. +# +# Usage: scripts/verify-atarist-tilebank.sh +# Env: TILEBANK_DATA_DIR stage this DATA/tilebank instead (negative controls) +# Requires: hatari, EmuTOS, a host cc; build/atarist/bin/TILEBANK.PRG and its +# DATA/tilebank (make atarist). +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +source "$repo/scripts/lib/tileBankGate.sh" +joeyNeed HATARI hatari +joeyNeed EMUTOS_IMG emutos + +binDir=$repo/build/atarist/bin +dataDir=$(tileBankData "$binDir") +wall=150 # hard cap on Hatari, whatever happens to this script +lineSeconds=120 # boot plus the checks, fast-forwarded, take ~10 s + +tileBankNeedBuild "$binDir/TILEBANK.PRG" "$dataDir" "make atarist" +tileBankReference + +work=$(mktemp -d -t joeylib-st-tilebank.XXXXXX) +guard="" +hatari="" +# 2>/dev/null: bash reports the SIGKILLed timeout wrapper as "Killed". +cleanup() { + joeyStop "$hatari" "$guard" 2> /dev/null + rm -rf "$work" +} +trap cleanup EXIT + +mkdir -p "$work/c/DATA" +cp "$binDir/TILEBANK.PRG" "$work/c/" +cp -r "$dataDir" "$work/c/DATA/tilebank" + +SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy timeout -s KILL "$wall" "$HATARI" \ + --tos "$EMUTOS_IMG" \ + --harddrive "$work/c" \ + --gemdos-drive C \ + --drive-a off --drive-b off \ + --sound off \ + --fast-forward on \ + --auto 'C:\TILEBANK.PRG' > "$work/hatari.log" 2>&1 & +guard=$! +hatari=$(tileBankChild "$guard") + +log=$(tileBankAwaitLine "$work/c" "$lineSeconds" "$guard") +joeyStop "$hatari" "$guard" 2> /dev/null +hatari="" +guard="" +if [ -z "$log" ]; then + echo "verify-atarist-tilebank: FAIL - no TILEBANK line within ${lineSeconds}s" >&2 + tail -5 "$work/hatari.log" >&2 + exit 1 +fi +tileBankJudge "$log" diff --git a/scripts/verify-dos-tilebank.sh b/scripts/verify-dos-tilebank.sh new file mode 100755 index 0000000..7cde64b --- /dev/null +++ b/scripts/verify-dos-tilebank.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# verify-dos-tilebank.sh - Tile-bank gate for DOS: proves that assetbake's +# dos encoder (chunky 4bpp, high nibble = left pixel) writes exactly the +# layout the DOS tile paste reads, and that the DOS build refuses a bank baked +# for any other target. +# +# Runs TILEBANK.EXE (examples/tilebank) in DOSBox Staging on the 386SX-16 +# reference config, from a scratch C: holding it and the DATA/tilebank +# make/dos.mk staged from the host gate's fixture. The program loads its own +# bake through jlTileBankLoadToSurface and through jlTileBankLoad + +# jlTilePaste, checks every pixel of both against the PNG, hashes both, and +# tries every other target's bake; its TILEBANK line in JOEYLOG.TXT must say +# PASS with both hashes equal to the host gate's (scripts/check-tilebank.sh, +# run here for the value) and every other target refused. DOSBox is stopped +# as soon as the line appears. +# +# Usage: scripts/verify-dos-tilebank.sh +# Env: TILEBANK_DATA_DIR stage this DATA/tilebank instead (negative controls) +# Requires: DOSBox Staging, Xvfb, a host cc; build/dos/bin/TILEBANK.EXE and +# its DATA/tilebank (make dos). +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +source "$repo/scripts/lib/tileBankGate.sh" +joeyNeed DOSBOX dosbox + +binDir=$repo/build/dos/bin +dataDir=$(tileBankData "$binDir") +conf=$repo/scripts/dosbox-386sx16.conf +wall=150 # hard cap on DOSBox, whatever happens to this script +lineSeconds=120 # DOS boot plus the checks at 386SX-16 speed + +tileBankNeedBuild "$binDir/TILEBANK.EXE" "$dataDir" "make dos" +tileBankReference + +work=$(mktemp -d -t joeylib-dos-tilebank.XXXXXX) +guard="" +dosbox="" +xvfb="" +# 2>/dev/null: bash reports the SIGKILLed timeout wrapper as "Killed". +cleanup() { + joeyStop "$dosbox" "$guard" "$xvfb" 2> /dev/null + rm -rf "$work" +} +trap cleanup EXIT + +mkdir -p "$work/c/DATA" "$work/xdg" +cp "$binDir/TILEBANK.EXE" "$work/c/" +cp -r "$dataDir" "$work/c/DATA/tilebank" + +display=":$((RANDOM % 400 + 1300))" +Xvfb "$display" -screen 0 800x600x24 -nolisten tcp > /dev/null 2>&1 & +xvfb=$! +sleep 1 + +# The mount is an explicit -c: DOSBox runs every -c line before it would +# auto-mount a trailing directory (see bench-dos.sh). +XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ + timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ + --conf "$conf" \ + -c "MOUNT C \"$work/c\"" -c "C:" -c "TILEBANK.EXE" > "$work/dosbox.log" 2>&1 & +guard=$! +dosbox=$(tileBankChild "$guard") + +log=$(tileBankAwaitLine "$work/c" "$lineSeconds" "$guard") +joeyStop "$dosbox" "$guard" 2> /dev/null +dosbox="" +guard="" +if [ -z "$log" ]; then + echo "verify-dos-tilebank: FAIL - no TILEBANK line within ${lineSeconds}s" >&2 + tail -5 "$work/dosbox.log" >&2 + exit 1 +fi +tileBankJudge "$log" diff --git a/scripts/verify-iigs-tilebank.sh b/scripts/verify-iigs-tilebank.sh new file mode 100755 index 0000000..3d9d866 --- /dev/null +++ b/scripts/verify-iigs-tilebank.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# verify-iigs-tilebank.sh - Tile-bank gate for the Apple IIgs: proves that +# assetbake's iigs encoder (chunky 4bpp, high nibble = left pixel) writes +# exactly the layout the IIgs tile paste reads, and that the IIgs build +# refuses a bank baked for any other target. +# +# Builds a one-example JOEYLIB disk (TILEBANK, examples/tilebank, with the +# DATA/tilebank make/iigs.mk staged from the host gate's fixture), boots GS/OS +# under headless MAME and launches TILEBANK from the Finder (verify-iigs- +# save.sh's keystroke timeline). The program loads its own bake through +# jlTileBankLoadToSurface and through jlTileBankLoad + jlTilePaste, checks +# every pixel of both against the PNG, hashes both, tries every other +# target's bake, writes one TILEBANK line to JOEYLOG.TXT (jlLogFlush: one +# open/write/close) and only then shows its verdict lamp. The Lua below waits +# for the lamp in the Super Hi-Res framebuffer and stops MAME; the line, +# extracted from the work disk with cadius, must say PASS with both hashes +# equal to the host gate's (scripts/check-tilebank.sh, run here for the +# value) and every other target refused, and the lamp must be the PASS +# colour. +# +# SHR geometry: 160 bytes per row at $E12000, high nibble = left pixel; the +# stage is the whole 320x200 screen. The lamp is examples/tilebank's: 64x64 +# at (128, 68), colour 1 for PASS and 2 for FAIL, with FILL_COLOR (9) either +# side of it on its rows. +# +# Usage: scripts/verify-iigs-tilebank.sh +# (make -f make/iigs.mk iigs-verify-tilebank builds what it needs first) +# Env: TILEBANK_DATA_DIR stage this DATA/tilebank instead (negative controls) +# Requires: ./install.sh install iigs; MAME with the apple2gs ROM and the GS/OS +# system disk; a host cc; build/iigs/bin/TILEBANK and its +# DATA/tilebank. +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +source "$repo/scripts/lib/tileBankGate.sh" +joeyNeed LLVM816_ROOT toolchain:iigs +joeyNeed CADIUS cadius +joeyNeed MAME mame +joeyNeed IIGS_ROMPATH iigsFirmware +joeyNeed GSOS_SYSTEM_DISK iigsFirmware + +binDir=$repo/build/iigs/bin +dataDir=$(tileBankData "$binDir") +wall=900 # hard cap on MAME, whatever happens to this script +frames=24000 # emulated frames before the Lua gives up on the lamp + +tileBankNeedBuild "$binDir/TILEBANK" "$dataDir" "make -f make/iigs.mk iigs-verify-tilebank" +tileBankReference + +work=$(mktemp -d -t joeylib-iigs-tilebank.XXXXXX) +trap 'rm -rf "$work"' EXIT + +# make-iigs-disk.sh packs BINDIR's TILEBANK and BINDIR/DATA/tilebank, so a +# scratch BINDIR carries whichever DATA/tilebank this run stages. +mkdir -p "$work/bin/DATA" "$work/log" +cp "$binDir/TILEBANK" "$work/bin/" +cp -r "$dataDir" "$work/bin/DATA/tilebank" +BINDIR="$work/bin" JOEY_DISK_EXAMPLES="TILEBANK" "$repo/scripts/make-iigs-disk.sh" "$work/joey.2mg" > /dev/null || exit 1 +if ! "$CADIUS" CATALOG "$work/joey.2mg" 2> /dev/null | grep -qE '^ +OWN\.TBK +BIN'; then + echo "verify-iigs-tilebank: FAIL - the disk has no DATA/TILEBANK/OWN.TBK" >&2 + exit 1 +fi +cp "$GSOS_SYSTEM_DISK" "$work/boot.po" + +cat > "$work/tilebank.lua" <> 1)) + if (x & 1) == 0 then return (b >> 4) & 0x0F else return b & 0x0F end +end + +-- The lamp colour when the whole lamp shows one verdict colour with +-- FILL_COLOR on its rows either side, else 0. +local function lampColor() + local left = LAMP_X + MARGIN + local right = LAMP_X + LAMP_W - 1 - MARGIN + local top = LAMP_Y + MARGIN + local bottom = LAMP_Y + LAMP_H - 1 - MARGIN + local midY = LAMP_Y + LAMP_H // 2 + local c = stagePixel(left, top) + if c ~= PASS_COLOR and c ~= FAIL_COLOR then + return 0 + end + for _, p in ipairs({ {right, top}, {left, bottom}, {right, bottom}, {LAMP_X + LAMP_W // 2, midY} }) do + if stagePixel(p[1], p[2]) ~= c then + return 0 + end + end + if stagePixel(LAMP_X - BESIDE, midY) ~= FILL_COLOR or stagePixel(LAMP_X + LAMP_W - 1 + BESIDE, midY) ~= FILL_COLOR then + return 0 + end + return c +end + +-- Select the JOEYLIB volume, open it, select TILEBANK, open it. +local steps = { + {3000, function() nat:post("J") end}, + {3120, function() press(keyCmd) end}, + {3126, function() nat:post("o") end}, + {3180, function() release(keyCmd) end}, + {3540, function() nat:post("TILEBANK") end}, + {3660, function() press(keyCmd) end}, + {3666, function() nat:post("o") end}, + {3720, function() release(keyCmd) end}, +} + +emu.register_frame_done(function() + frame = frame + 1 + if done then return end + while idx <= #steps and frame >= steps[idx][1] do + steps[idx][2]() + idx = idx + 1 + end + if exitAt == 0 then + if frame >= FIRST_LOOK and frame % SAMPLE_EVERY == 0 then + local c = lampColor() + if c ~= 0 and c == lamp then + seen = seen + 1 + else + seen = (c ~= 0) and 1 or 0 + end + lamp = c + if seen >= LAMP_STABLE then + io.write(string.format("TILEBANK-IIGS lamp=%s f%d\n", (lamp == PASS_COLOR) and "PASS" or "FAIL", frame)) + io.flush() + exitAt = frame + EXIT_SETTLE + end + end + elseif frame >= exitAt then + done = true + manager.machine:exit() + end + if frame > FRAMES then + done = true + io.write("TILEBANK-IIGS no-lamp f"..frame.."\n") + io.flush() + manager.machine:exit() + end +end) +LUA + +# From the work dir, so MAME's cfg/ and nvram/ never persist between runs. +out=$(cd "$work" && QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + timeout -s KILL "$wall" "$MAME" apple2gs \ + -rompath "$IIGS_ROMPATH" \ + -flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \ + -video none -sound none -nothrottle \ + -autoboot_script "$work/tilebank.lua" < /dev/null 2>&1 | grep '^TILEBANK-IIGS') +echo "${out:-TILEBANK-IIGS: no report from the Lua (MAME died, or hit the ${wall}s cap)}" + +lampOk=1 +if ! grep -q 'lamp=PASS' <<< "$out"; then + lampOk=0 +fi +# cadius names the extracted file with its ProDOS type appended. +"$CADIUS" EXTRACTFILE "$work/joey.2mg" "/JOEYLIB/JOEYLOG.TXT" "$work/log/" > /dev/null 2>&1 +log=$(find "$work/log" -maxdepth 1 -iname 'joeylog.txt*' -print -quit) +if [ -z "$log" ]; then + echo "verify-iigs-tilebank: FAIL - no JOEYLOG.TXT on the work disk (the program never flushed its log)" >&2 + exit 1 +fi +tileBankJudge "$log" || exit 1 +if [ "$lampOk" -eq 0 ]; then + echo "verify-iigs-tilebank: FAIL - the on-screen lamp did not show PASS" >&2 + exit 1 +fi diff --git a/scripts/verify-x68000-serial-mouse.sh b/scripts/verify-x68000-serial-mouse.sh new file mode 100755 index 0000000..b921eab --- /dev/null +++ b/scripts/verify-x68000-serial-mouse.sh @@ -0,0 +1,541 @@ +#!/usr/bin/env bash +# 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 +# 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 +# side's interrupt goes to the wrong register, and the interrupt-under-service +# reset it swallows wedges the chip's interrupt chain. The serial and mouse +# gates each run one side alone; this one runs both together, hard, for long +# enough to matter. +# +# Boots SERMOUSE.X (examples/sermouse) under the patched MAME with SCC channel +# A on a null_modem socket and every SCC channel at the real chip's 3-byte +# receive FIFO (JOEY_Z80SCC_RX_FIFO=3), so an interrupt held off too long loses +# bytes here as it would on the machine. Then, all at once: +# serial the guest streams byte k = streamByte(k, 0xA5) and checks the +# peer's stream, byte k = streamByte(k, 0x00), in order; the host +# peer (scripts/lib/serialStreamPeer.py) checks the guest's and +# answers byte for byte. The guest also raises DTR 16 times a loop, +# the add-on's own select-then-write of channel A's WR5, so that +# sequence runs under both channels' interrupts all the time. +# mouse the Lua visits the four stage corners, pushing both axes into the +# corner EVERY frame -- a packet on channel B at every solicitation, +# while the pointer stays pinned where the clamp puts it -- checks +# the position there, then presses a button and checks it, then +# releases it (checked at the next corner). Clamp edges only: MAME's +# relative-axis injection is timing-sensitive, so interior positions +# are not testable (see verify-x68000-mouse.sh). Two left clicks a +# pass also prove the ROM's software keyboard stays off (hal.c +# X68K_SKEY_OFF): the second would bring it up and eat the next +# right click. +# Timeline: PASSES corner passes with the traffic flowing, every check also +# requiring both stream counts to have moved and no bad byte; SPACE pauses the +# guest's stream, the peer follows, and once quiet the guest's counts must +# hold still and equal a quiet snapshot of the peer's EXACTLY -- every byte +# sent either way arrived, in order; one more pass in the quiet; SPACE resumes +# and both counts must move again (the receive interrupt still fires at the +# end, the program still runs). +# +# MAME 0.264 needs one correction here, made in the Lua and not in MAME. Its +# Z8530 (src/devices/machine/z80scc.cpp) answers an interrupt acknowledge +# wrongly when BOTH channels have one pending: z80daisy_irq_ack puts the +# highest-priority source in service but returns m_chanB->m_rr2, the vector of +# whichever source was triggered LAST. The CPU then runs the other channel's +# handler, whose interrupt-under-service reset clears the source the chip +# acknowledged, and that channel's byte is left in its FIFO with nothing +# pending: the channel runs a byte behind until its FIFO overruns (serial +# loses bytes) or a stale byte starts a mouse packet (pointer and buttons go +# wrong). Measured before the correction: both, within seconds. Silicon +# returns the vector of the source it acknowledges, so a tap on the CPU's +# level-5 acknowledge rewrites the vector to that source's, and the verdict +# line reports how many acknowledges it corrected (iackFixed). Only this gate +# keeps both channels busy at once, so no other gate needs it. +# +# The guest reports through two verification rows at the bottom of the stage +# (one nibble per pixel, high nibble first), read from the text plane: +# row 198 3C3C, status, received(8), bad, sent(8), short writes, first bad(8) +# row 199 5A5A, present/left/right/middle, x, y (examples/keys' mouse row) +# The serial row changes at most every 4 frames, so a read counts only when +# two consecutive frames agree (a present caught half done cannot). +# +# PASS: every Lua check right, the peer's stream PASS, at least MIN_BYTES each +# way, and the guest's quiet counts among the peer's quiet snapshots. +# +# Requires: ./install.sh install x68000 -- the patched MAME +# (installer/patches/mame: 0002 for the FIFO depth, 0004 for the +# RS-232C slot and the mouse) and the Human68k system disk. +# +# bash scripts/verify-x68000-serial-mouse.sh +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +joeyNeed MAME mame +joeyNeed X68K_DIR x68kFirmware +joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom +source "$repo/scripts/lib/x68kDisk.sh" +PORT="${X68K_SERMOUSE_PORT:-6801}" +WALL="${X68K_SERMOUSE_WALL:-600}" +PASSES="${X68K_SERMOUSE_PASSES:-3}" +MIN_BYTES="${X68K_SERMOUSE_MIN_BYTES:-16384}" +PROG="$repo/build/x68000/bin/SERMOUSE.X" +FIFO_DEPTH=3 +# The peer outlives MAME by this much, so it always sees the link close, and +# is given this long to report once MAME is gone. +PEER_MARGIN=30 +PEER_GRACE=10 + +[ -f "$X68K_SYSTEM_DISK" ] || { echo "verify-x68000-serial-mouse: missing $X68K_SYSTEM_DISK" >&2; exit 2; } +[ -f "$PROG" ] || { echo "verify-x68000-serial-mouse: build SERMOUSE.X first (make -f make/x68000.mk)" >&2; exit 2; } + +work=$(mktemp -d -t joey-x68sermouse.XXXXXX) +peer="" +mame="" +mameChild="" +cleanup() { + joeyStop "$mameChild" "$mame" "$peer" + rm -rf "$work" +} +trap cleanup EXIT + +x68kBootDisk "$work/boot.xdf" 'A:\SERMOUSE.X' || exit 1 +x68kXdf add "$work/boot.xdf" "$PROG" SERMOUSE.X >/dev/null || exit 1 + +{ + printf 'local PASSES = %d\n' "$PASSES" + printf 'local MIN_BYTES = %d\n' "$MIN_BYTES" + cat <<'LUA' +local cpu = manager.machine.devices[":maincpu"] +local mem = cpu.spaces["program"] +local nat = manager.machine.natkeyboard +local scc = manager.machine.devices[":scc"] +local sccA = manager.machine.devices[":scc:cha"] +local frame = 0 +local t0 = 0 +local biased = false +local done = false +local checks = 0 +local fails = {} +local steps = {} +local pending = nil +local lastRaw = nil +local prev = nil +local quiet = nil +local pushX = 0 +local pushY = 0 +local axisValX = 0 +local axisValY = 0 + +-- Stage geometry on the text plane (src/x68000/hal.c X68K_PRESENT_TEXT): four +-- bit planes 128 KB apart at 0xE00000, 128 bytes per row, leftmost pixel in +-- bit 7, the stage centred at (96, 156). +local TEXT_BASE = 0xE00000 +local PLANES = 4 +local PLANE_BYTES = 0x20000 +local ROW_BYTES = 128 +local ORIGIN_X = 96 +local ORIGIN_Y = 156 +-- The verification rows (examples/sermouse/sermouse.c): 16-bit words and +-- 32-bit longs, one nibble per pixel, laid end to end from the sentinel. +local WORD = 4 +local LONG = 8 +local SERIAL_ROW = 198 +local SERIAL_SENTINEL = 0x3C3C +local SERIAL_NIBBLES = 4 * WORD + 3 * LONG +local MOUSE_ROW = 199 +local MOUSE_SENTINEL = 0x5A5A +local MOUSE_NIBBLES = 4 * WORD +local STAGE_RIGHT = 319 +local STAGE_BOTTOM = 199 +local STATUS_OPEN = 0x0001 +local STATUS_PAUSED = 0x0002 +local NO_BAD_INDEX = 0xFFFFFFFF + +-- Mouse drive: the x68k mouse's X/Y are 12-bit absolute counters it diffs +-- into delta packets; biased mid-range so a pass (net zero per axis, at most +-- two phases one way) never wraps them. +local AXIS_BIAS = 2048 +local PUSH_STEP = 6 -- counter units per frame, per axis +local PUSH_FRAMES = 90 -- 540 units per phase: over the 319-pixel stage with room to spare +local BUTTON_FRAMES = 40 +local CORNER_FRAMES = PUSH_FRAMES + BUTTON_FRAMES +local PASS_FRAMES = 4 * CORNER_FRAMES +local QUIET_SETTLE = 120 -- frames after SPACE before the line is read as quiet +local QUIET_HOLD = 60 -- and again this much later: it must not move +local RESUME_FRAMES = 120 +local BOOT_BIAS_FRAME = 300 +local SENTINEL_FIRST = 600 +local SENTINEL_EVERY = 50 +local SETTLE_FRAMES = 60 +local STEADY_LIMIT = 60 -- frames a serial read may take to see two agreeing frames +local BOOT_LIMIT = 30000 + +-- The acknowledge-vector correction (see the script header). MAME keeps the +-- chip's six interrupt sources in m_int_state, highest priority first, +-- channel A's three then channel B's, each INT (pending) and/or IEO (in +-- service); m_int_source holds each one's type, which with the channel is the +-- 3-bit status the vector carries in bits 3-1 (WR9 status-low, as the X68000 +-- runs it). The 68000 fetches the level-5 vector at 0xFFFFFA in cpu space. +local IACK5 = 0xFFFFFA +local SCC_SOURCES = 6 +local SCC_A_SOURCES = 3 +local DAISY_IEO = 0x02 +local WR9_VIS = 0x01 +local WR9_NV_SHSL = 0x12 -- no vector / status high: vectors this does not model +local STATUS_CHANNEL_A = 0x04 +local STATUS_TYPE_MASK = 0x03 +local VECTOR_STATUS_KEEP = 0xF1 +local VECTOR_BYTE = 0xFF +local intState = emu.item(scc.items["0/m_int_state"]) +local intSource = emu.item(scc.items["0/m_int_source"]) +local wr9 = emu.item(scc.items["0/m_wr9"]) +local wr2 = emu.item(sccA.items["0/m_wr2"]) +local iackFixed = 0 + +local function textPix(px, y) + local sx = ORIGIN_X + px + local addr = TEXT_BASE + (ORIGIN_Y + y) * ROW_BYTES + (sx >> 3) + local bit = 7 - (sx & 7) + local v = 0 + for p = 0, PLANES - 1 do + v = v | (((mem:read_u8(addr + p * PLANE_BYTES) >> bit) & 1) << p) + end + return v +end + +local function rowNibbles(y, count) + local t = {} + for i = 0, count - 1 do + t[#t + 1] = textPix(i, y) + end + return t +end + +-- A reader over a row's nibbles: each call takes the next field, high nibble +-- first. +local function fields(t) + local pos = 0 + return function(count) + local v = 0 + for i = pos + 1, pos + count do + v = (v << 4) | t[i] + end + pos = pos + count + return v + end +end + +local function parseSerial(t) + local take = fields(t) + local s = {} + s.sentinel = take(WORD) == SERIAL_SENTINEL + s.status = take(WORD) + s.rx = take(LONG) + s.bad = take(WORD) + s.tx = take(LONG) + s.short = take(WORD) + s.firstBad = take(LONG) + return s +end + +-- The flags word is one nibble per predicate: present, left, right, middle. +local function readMouse() + local take = fields(rowNibbles(MOUSE_ROW, MOUSE_NIBBLES)) + local m = {} + m.sentinel = take(WORD) == MOUSE_SENTINEL + m.present = take(1) + m.left = take(1) + m.right = take(1) + m.middle = take(1) + m.x = take(WORD) + m.y = take(WORD) + return m +end + +local ports = manager.machine.ioport.ports +local axisX, axisY, btnLeft, btnRight +local portX = ports[":mouse_port:x68k:X"] +local portY = ports[":mouse_port:x68k:Y"] +local portB = ports[":mouse_port:x68k:BTN"] +if portX then for _, f in pairs(portX.fields) do axisX = f end end +if portY then for _, f in pairs(portY.fields) do axisY = f end end +if portB then + for _, f in pairs(portB.fields) do + if f.mask == 2 then btnLeft = f end + if f.mask == 1 then btnRight = f end + end +end + +-- After MAME has acknowledged: the source now in service is the one the +-- vector must name. A global, because a collected tap handle removes the tap. +iackTap = cpu.spaces["cpu_space"]:install_read_tap(IACK5, IACK5 + 1, "iackVector", function(offset, data, mask) + local w = wr9:read(0) + if (w & WR9_VIS) == 0 or (w & WR9_NV_SHSL) ~= 0 then + return nil + end + for i = 0, SCC_SOURCES - 1 do + if (intState:read(i) & DAISY_IEO) ~= 0 then + local status = (intSource:read(i) & STATUS_TYPE_MASK) | (i < SCC_A_SOURCES and STATUS_CHANNEL_A or 0) + local vector = (wr2:read(0) & VECTOR_STATUS_KEEP) | (status << 1) + if vector == (data & VECTOR_BYTE) then + return nil + end + iackFixed = iackFixed + 1 + return (data & ~VECTOR_BYTE) | vector + end + end + return nil +end) + +local function finish(verdict) + done = true + local s = prev or {} + io.write(string.format("VERIFY-X68K-SERMOUSE %s checks=%d fails=%d rx=%d tx=%d quiet=%s iackFixed=%d%s\n", + verdict, checks, #fails, s.rx or 0, s.tx or 0, + quiet and string.format("%d:%d", quiet.rx, quiet.tx) or "none", iackFixed, + #fails > 0 and (" detail=" .. table.concat(fails, ",")) or "")) + io.flush() + manager.machine:exit() +end + +if not (axisX and axisY and btnLeft and btnRight) then + done = true + io.write("VERIFY-X68K-SERMOUSE error: mouse ioports not found\n") + io.flush() + manager.machine:exit() +end + +-- One check; the first failure is the verdict, so the run stops there. +local function expect(name, ok, detail) + if done then return end + checks = checks + 1 + if not ok then + fails[#fails + 1] = name .. (detail and ("(" .. detail .. ")") or "") + finish("FAIL") + end +end + +local function expectMouse(step, x, y, l, r) + local m = readMouse() + expect(step .. ":sentinel", m.sentinel) + expect(step .. ":present", m.present == 1, tostring(m.present)) + expect(step .. ":xy", m.x == x and m.y == y, string.format("%d,%d want %d,%d", m.x, m.y, x, y)) + expect(step .. ":buttons", m.left == l and m.right == r and m.middle == 0, + string.format("%d%d%d want %d%d0", m.left, m.right, m.middle, l, r)) +end + +-- A steady serial read: fn runs with the row once two consecutive frames +-- agree on it. +local function whenSteady(name, fn) + pending = { name = name, fn = fn, since = frame } + lastRaw = nil +end + +local function serialHealthy(step, s) + expect(step .. ":sentinel", s.sentinel) + expect(step .. ":open", (s.status & STATUS_OPEN) ~= 0, string.format("status %04X", s.status)) + expect(step .. ":bad", s.bad == 0 and s.firstBad == NO_BAD_INDEX, string.format("%d first %d", s.bad, s.firstBad)) + expect(step .. ":short", s.short == 0, tostring(s.short)) +end + +-- Traffic flowing: both counts moved since the last read. +local function expectMoving(step) + whenSteady(step, function(s) + serialHealthy(step, s) + expect(step .. ":paused", (s.status & STATUS_PAUSED) == 0) + if prev then + expect(step .. ":rxMoved", s.rx > prev.rx, string.format("%d -> %d", prev.rx, s.rx)) + expect(step .. ":txMoved", s.tx > prev.tx, string.format("%d -> %d", prev.tx, s.tx)) + end + prev = s + end) +end + +-- Line quiet: both counts exactly where the pause left them. +local function expectFrozen(step) + whenSteady(step, function(s) + serialHealthy(step, s) + expect(step .. ":paused", (s.status & STATUS_PAUSED) ~= 0) + if quiet == nil then + quiet = s + expect(step .. ":rxMin", s.rx >= MIN_BYTES, tostring(s.rx)) + expect(step .. ":txMin", s.tx >= MIN_BYTES, tostring(s.tx)) + else + expect(step .. ":rxFrozen", s.rx == quiet.rx, string.format("%d -> %d", quiet.rx, s.rx)) + expect(step .. ":txFrozen", s.tx == quiet.tx, string.format("%d -> %d", quiet.tx, s.tx)) + end + prev = s + end) +end + +local function at(t, fn) + steps[#steps + 1] = { at = t, run = fn } +end + +-- Corners in visiting order: where the clamp pins the pointer, the push +-- direction that pins it there, and the button pressed once it is. +local corners = { + { name = "topLeft", x = 0, y = 0, dx = -1, dy = -1, button = "left" }, + { name = "topRight", x = STAGE_RIGHT, y = 0, dx = 1, dy = -1, button = "right" }, + { name = "bottomRight", x = STAGE_RIGHT, y = STAGE_BOTTOM, dx = 1, dy = 1, button = "left" }, + { name = "bottomLeft", x = 0, y = STAGE_BOTTOM, dx = -1, dy = 1, button = "right" }, +} + +-- One pass over the corners from frame `base`; `serialCheck` runs beside +-- each position check. +local function pass(base, label, serialCheck) + for i, c in ipairs(corners) do + local t = base + (i - 1) * CORNER_FRAMES + local step = label .. ":" .. c.name + local button = c.button == "left" and btnLeft or btnRight + local l = c.button == "left" and 1 or 0 + at(t, function() pushX = c.dx; pushY = c.dy end) + at(t + PUSH_FRAMES, function() + expectMouse(step, c.x, c.y, 0, 0) + serialCheck(step) + button:set_value(1) + end) + at(t + CORNER_FRAMES - 1, function() + expectMouse(step .. "Button", c.x, c.y, l, 1 - l) + button:clear_value() + end) + end +end + +local t = 0 +for p = 1, PASSES do + pass(t, "busy" .. p, expectMoving) + t = t + PASS_FRAMES +end +at(t, function() nat:post(" ") end) +at(t + QUIET_SETTLE, function() expectFrozen("pause") end) +at(t + QUIET_SETTLE + QUIET_HOLD, function() expectFrozen("pauseHold") end) +t = t + QUIET_SETTLE + QUIET_HOLD + 1 +pass(t, "quiet", expectFrozen) +t = t + PASS_FRAMES +at(t, function() nat:post(" ") end) +at(t + RESUME_FRAMES, function() + prev = quiet + expectMoving("resumed") +end) +local finalFrame = t + RESUME_FRAMES + +emu.register_frame_done(function() + frame = frame + 1 + if done then return end + if not biased and frame == BOOT_BIAS_FRAME then + biased = true + axisValX = AXIS_BIAS + axisValY = AXIS_BIAS + axisX:set_value(axisValX) + axisY:set_value(axisValY) + end + if t0 == 0 then + if frame > SENTINEL_FIRST and frame % SENTINEL_EVERY == 0 then + local s = parseSerial(rowNibbles(SERIAL_ROW, SERIAL_NIBBLES)) + if s.sentinel and readMouse().sentinel then + t0 = frame + SETTLE_FRAMES + end + end + if frame > BOOT_LIMIT then + done = true + io.write("VERIFY-X68K-SERMOUSE timeout: SERMOUSE never came up\n") + io.flush() + manager.machine:exit() + end + return + end + if pushX ~= 0 or pushY ~= 0 then + axisValX = axisValX + pushX * PUSH_STEP + axisValY = axisValY + pushY * PUSH_STEP + axisX:set_value(axisValX) + axisY:set_value(axisValY) + end + for _, s in ipairs(steps) do + if frame == t0 + s.at and not done then s.run() end + end + if pending and not done then + local raw = table.concat(rowNibbles(SERIAL_ROW, SERIAL_NIBBLES), ",") + if raw == lastRaw then + local p = pending + pending = nil + p.fn(parseSerial(rowNibbles(SERIAL_ROW, SERIAL_NIBBLES))) + elseif frame - pending.since > STEADY_LIMIT then + expect(pending.name .. ":steady", false) + else + lastRaw = raw + end + end + if not done and not pending and frame > t0 + finalFrame then + finish(#fails == 0 and "PASS" or "FAIL") + end +end) +LUA +} > "$work/sermouse.lua" + +python3 "$repo/scripts/lib/serialStreamPeer.py" "$PORT" "$((WALL + PEER_MARGIN))" "$MIN_BYTES" \ + > "$work/peer.out" 2>&1 & +peer=$! +# The peer must be listening before MAME connects out to it. +for _ in $(seq 1 50); do + grep -q '^STREAM listening' "$work/peer.out" 2>/dev/null && break + sleep 0.1 +done + +# From the work dir, so MAME's cfg/ and nvram/ land there. Under timeout so a +# hung run cannot outlive WALL whatever happens to this script; mameChild is +# MAME itself, which cleanup stops first (stopping only the timeout process +# could leave MAME running). +cd "$work" || exit 1 +JOEY_Z80SCC_RX_FIFO=$FIFO_DEPTH timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \ + -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" \ + -rs232c null_modem -bitb "socket.127.0.0.1:$PORT" \ + -video none -sound none -nothrottle \ + -autoboot_script "$work/sermouse.lua" "$work/mame.out" 2>&1 & +mame=$! +sleep 1 +mameChild=$(pgrep -P "$mame" | head -1) + +# The Lua ends the run: after its last check, or at its first failure. +wait "$mame" +mame="" +mameChild="" +for _ in $(seq 1 "$PEER_GRACE"); do + joeyAlive "$peer" || break + sleep 1 +done +joeyStop "$peer" +peer="" + +lua=$(grep -E '^VERIFY-X68K-SERMOUSE ' "$work/mame.out" | tail -1) +stream=$(grep -E '^STREAM (PASS|FAIL)' "$work/peer.out" | tail -1) +echo "${lua:-VERIFY-X68K-SERMOUSE: no verdict from the Lua}" +echo "${stream:-STREAM: no verdict from the peer}" + +if ! grep -q "RX FIFO depth $FIFO_DEPTH" "$work/mame.out"; then + echo "verify-x68000-serial-mouse: SKIP - this MAME does not take JOEY_Z80SCC_RX_FIFO (reinstall: ./install.sh install mame)" >&2 + exit 2 +fi +fail=0 +if ! echo "$lua" | grep -q '^VERIFY-X68K-SERMOUSE PASS .* fails=0 '; then + echo "verify-x68000-serial-mouse: FAIL - guest or mouse check (Lua line above)" >&2 + tail -5 "$work/mame.out" >&2 + fail=1 +fi +if ! echo "$stream" | grep -q '^STREAM PASS '; then + echo "verify-x68000-serial-mouse: FAIL - the guest's stream to the host (peer line above)" >&2 + fail=1 +fi +guestQuiet=$(echo "$lua" | sed -n 's/.* quiet=\([0-9]*:[0-9]*\).*/\1/p') +peerQuiet=$(echo "$stream" | sed -n 's/.* quiet=\([0-9:,]*\) .*/\1/p') +if [ -z "$guestQuiet" ] || [[ ",$peerQuiet," != *",$guestQuiet,"* ]]; then + echo "verify-x68000-serial-mouse: FAIL - quiet counts differ: guest received:sent $guestQuiet, peer sent:received $peerQuiet" >&2 + fail=1 +fi +if [ "$fail" -ne 0 ]; then + exit 1 +fi +echo "verify-x68000-serial-mouse: PASS (both streams byte-exact and complete at the pause, mouse corners and buttons right throughout, receive interrupt alive at the end)" diff --git a/scripts/verify-x68000-serial.sh b/scripts/verify-x68000-serial.sh index 3d8928b..666c55b 100755 --- a/scripts/verify-x68000-serial.sh +++ b/scripts/verify-x68000-serial.sh @@ -61,25 +61,9 @@ sleep 2 # cwd where the next run would read them back. cd "$work" || exit 1 if [ -n "$LINE" ]; then - python3 - "$LINE" > "$work/x68000.cfg" <<'PY' || exit 2 -import sys -# MAME's RS232_* enum values (src/devices/bus/rs232/rs232.h). -BAUD = {"300": 2, "600": 3, "1200": 4, "2400": 5, "4800": 6, "9600": 7, "19200": 9, "38400": 11} -DATA = {"5": 0, "6": 1, "7": 2, "8": 3} -PARITY = {"N": 0, "O": 1, "E": 2} -STOP = {"1": 1, "2": 3} -baud, data, parity, stop = sys.argv[1].split() -tag = ":rs232c:null_modem:" -ports = [("RS232_TXBAUD", 7, BAUD[baud]), ("RS232_RXBAUD", 7, BAUD[baud]), - ("RS232_DATABITS", 3, DATA[data]), ("RS232_PARITY", 0, PARITY[parity.upper()]), - ("RS232_STOPBITS", 1, STOP[stop])] -print('\n\n \n ') -for name, default, value in ports: - print(f' ') -print(' \n \n') -PY mkdir -p "$work/cfg" - mv "$work/x68000.cfg" "$work/cfg/x68000.cfg" + python3 "$repo/scripts/lib/mameNullModemCfg.py" x68000 :rs232c:null_modem "$LINE" \ + > "$work/cfg/x68000.cfg" || exit 2 fi # MAME runs until the peer has its verdict and is then stopped by PID: waiting # out a whole run once the probe has echoed cost minutes per gate. The peer's diff --git a/scripts/verify-x68000-tilebank.sh b/scripts/verify-x68000-tilebank.sh new file mode 100755 index 0000000..9239f8f --- /dev/null +++ b/scripts/verify-x68000-tilebank.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# verify-x68000-tilebank.sh - Tile-bank gate for the X68000: proves that +# assetbake's x68000 encoder (plane-major, like the Amiga's, under its own +# target byte) writes exactly the layout the X68000's native tile paste +# (src/m68k/planarTile.h via src/x68000/x68kTile.h) reads, and that the +# X68000 build refuses a bank baked for any other target. +# +# Boots Human68k headless under MAME with TILEBANK.X (examples/tilebank) and +# the DATA/tilebank make/x68000.mk staged from the host gate's fixture on a +# data disk in drive B. The program loads its own bake through +# jlTileBankLoadToSurface and through jlTileBankLoad + jlTilePaste, checks +# every pixel of both against the PNG, hashes both, tries every other +# target's bake, logs one TILEBANK line and shows its verdict lamp. The Lua +# below waits for the lamp in text VRAM, then presses a key so the program +# exits -- Human68k only finalises joeylog.txt's directory entry when the +# program's stdio closes it -- and stops MAME. The line, extracted from the +# data disk, must say PASS with both hashes equal to the host gate's +# (scripts/check-tilebank.sh, run here for the value) and every other target +# refused, and the lamp must be the PASS colour. +# +# Text-plane geometry (see verify-x68000-input.sh): four 1bpp planes 0x20000 +# apart at 0xE00000, 128 bytes per row, leftmost pixel in bit 7, the 320x200 +# stage centred at (96, 156). The lamp is examples/tilebank's: 64x64 at stage +# (128, 68), colour 1 for PASS and 2 for FAIL, with FILL_COLOR (9) either side +# of it on its rows. +# +# Usage: scripts/verify-x68000-tilebank.sh +# Env: TILEBANK_DATA_DIR stage this DATA/tilebank instead (negative controls) +# Requires: MAME, the X68000 firmware and CG ROM, a host cc; +# build/x68000/bin/TILEBANK.X and its DATA/tilebank (make x68000). +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/scripts/lib/joeyEnv.sh" +source "$repo/scripts/lib/tileBankGate.sh" +joeyNeed MAME mame +joeyNeed X68K_DIR x68kFirmware +joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom +source "$repo/scripts/lib/x68kDisk.sh" +joeyNeedFile "$X68K_SYSTEM_DISK" x68kFirmware + +binDir=$repo/build/x68000/bin +dataDir=$(tileBankData "$binDir") +wall=600 # hard cap on MAME, whatever happens to this script +frames=20000 # emulated frames before the Lua gives up on the lamp + +tileBankNeedBuild "$binDir/TILEBANK.X" "$dataDir" "make x68000" +tileBankReference + +work=$(mktemp -d -t joeylib-x68-tilebank.XXXXXX) +trap 'rm -rf "$work"' EXIT + +# The boot disk carries only Human68k and AUTOEXEC; the program, its DATA and +# its log live on the data disk in B:, which AUTOEXEC makes current. +x68kBootDisk "$work/boot.xdf" 'B:' 'B:\TILEBANK.X' || exit 1 +x68kDataDisk "$work/data.xdf" || exit 1 +x68kXdf add "$work/data.xdf" "$binDir/TILEBANK.X" TILEBANK.X > /dev/null || exit 1 +x68kAddTree "$work/data.xdf" "$dataDir" DATA/TILEBANK || exit 1 + +cat > "$work/tilebank.lua" < jlShutdown -> stdio closes the log + +local LAMP_X, LAMP_Y, LAMP_W, LAMP_H = 128, 68, 64, 64 +local PASS_COLOR, FAIL_COLOR, FILL_COLOR = 1, 2, 9 +local MARGIN = 4 -- inside probes keep off the lamp's edge +local BESIDE = 8 -- outside probes sit this far off it + +local frame = 0 +local seen = 0 +local lamp = 0 +local exitAt = 0 +local done = false + +local function stagePixel(px, py) + local x = 96 + px + local byteOff = (156 + py) * 128 + (x >> 3) + local bit = 0x80 >> (x & 7) + local v = 0 + for plane = 0, 3 do + if (mem:read_u8(0xE00000 + plane * 0x20000 + byteOff) & bit) ~= 0 then + v = v | (1 << plane) + end + end + return v +end + +-- The lamp colour when the whole lamp shows one verdict colour with +-- FILL_COLOR on its rows either side, else 0. +local function lampColor() + local left = LAMP_X + MARGIN + local right = LAMP_X + LAMP_W - 1 - MARGIN + local top = LAMP_Y + MARGIN + local bottom = LAMP_Y + LAMP_H - 1 - MARGIN + local midY = LAMP_Y + LAMP_H // 2 + local c = stagePixel(left, top) + if c ~= PASS_COLOR and c ~= FAIL_COLOR then + return 0 + end + for _, p in ipairs({ {right, top}, {left, bottom}, {right, bottom}, {LAMP_X + LAMP_W // 2, midY} }) do + if stagePixel(p[1], p[2]) ~= c then + return 0 + end + end + if stagePixel(LAMP_X - BESIDE, midY) ~= FILL_COLOR or stagePixel(LAMP_X + LAMP_W - 1 + BESIDE, midY) ~= FILL_COLOR then + return 0 + end + return c +end + +emu.register_frame_done(function() + frame = frame + 1 + if done then return end + if exitAt == 0 then + if frame % SAMPLE_EVERY == 0 then + local c = lampColor() + if c ~= 0 and c == lamp then + seen = seen + 1 + else + seen = (c ~= 0) and 1 or 0 + end + lamp = c + if seen >= LAMP_STABLE then + io.write(string.format("TILEBANK-X68K lamp=%s f%d\n", (lamp == PASS_COLOR) and "PASS" or "FAIL", frame)) + io.flush() + exitAt = frame + EXIT_SETTLE + end + end + else + if frame % KEY_EVERY == 0 then nat:post(" ") end + if frame >= exitAt then + done = true + manager.machine:exit() + end + end + if frame > FRAMES then + done = true + io.write("TILEBANK-X68K no-lamp f"..frame.."\n") + io.flush() + manager.machine:exit() + end +end) +LUA + +# From the work dir, so MAME's cfg/ and nvram/ never persist between runs. +out=$(cd "$work" && timeout -s KILL "$wall" "$MAME" x68000 -bios ipl10 \ + -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" \ + -video none -sound none -nothrottle \ + -autoboot_script "$work/tilebank.lua" < /dev/null 2>&1 | grep '^TILEBANK-X68K') +echo "${out:-TILEBANK-X68K: no report from the Lua (MAME died, or hit the ${wall}s cap)}" + +lampOk=1 +if ! grep -q 'lamp=PASS' <<< "$out"; then + lampOk=0 +fi +if ! x68kXdf extract "$work/data.xdf" joeylog.txt "$work/joeylog.txt" > /dev/null 2>&1; then + echo "verify-x68000-tilebank: FAIL - no joeylog.txt on the data disk (the program never logged, or never exited)" >&2 + exit 1 +fi +tileBankJudge "$work/joeylog.txt" || exit 1 +if [ "$lampOk" -eq 0 ]; then + echo "verify-x68000-tilebank: FAIL - the on-screen lamp did not show PASS" >&2 + exit 1 +fi diff --git a/src/amiga/amigaTile.h b/src/amiga/amigaTile.h index 333b508..14a5607 100644 --- a/src/amiga/amigaTile.h +++ b/src/amiga/amigaTile.h @@ -1,372 +1,16 @@ -// Amiga tile ops as always-inline functions (NATIVE-PERF Phase 2 -// R4a). Bodies moved verbatim from src/amiga/hal.c so the public -// wrappers in src/core/tile.c inline them, deleting the second full -// cdecl call layer (~130-190 cycles/op on the 68000). The jlpTile* -// dispatch names are macro-aliased to these in port.h, which also -// #includes this header at its tail -- AFTER the jlpGenericTile* -// prototypes the portData == NULL fallbacks call. Do not include this -// header directly; it comes with port.h on the Amiga. +// Amiga tile ops: the shared planar bodies in src/m68k/planarTile.h, bound to +// the Amiga's planar surface data. The X68000 uses the same bodies +// (src/x68000/x68kTile.h). Do not include this header directly; it comes with +// port.h on the Amiga. #ifndef JOEY_AMIGA_TILE_H #define JOEY_AMIGA_TILE_H -#include -#include - -#include "joey/tile.h" #include "amigaPlanar.h" -// 8-row unrolled tile access (NATIVE-PERF Phase 2 R8): row offsets -// are compile-time constants row * 40, so each row is one d16(An) -// access with no pointer stepping and no loop control. gcc-amigaos -// -O2 keeps the equivalent row loops rolled as dbf (objdump-verified, -// ~18 cycles/row of pure loop overhead). Only the row dimension -// unrolls -- the 4-plane loop keeps real per-plane setup and stays -// register-light per the m68k-gcc-unroll-backfire lesson. -#define AMIGA_TILE_ROW_OFF(_r) ((uint16_t)((_r) * AMIGA_BYTES_PER_ROW)) +#define PLANAR_TILE_DATA_T AmigaPlanarT +#define PLANAR_TILE_BYTES_PER_ROW AMIGA_BYTES_PER_ROW +#define PLANAR_TILE_PLANES AMIGA_BITPLANES -// Phase 5 planar dual-write for tile ops, fully planar after Phase 9 -// dropped the chunky shadow. All tiles are 8-pixel aligned (8x8 blocks -// at multiples of 8), so plane writes are byte-aligned -- one plane -// byte per row, 8 rows per tile, no edge masks. Stride between rows -// in a plane is AMIGA_BYTES_PER_ROW (40). - -static inline __attribute__((always_inline)) void amigaTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) { - AmigaPlanarT *pd; - uint16_t plane; - uint8_t fillByte; - uint8_t *p; - - pd = (AmigaPlanarT *)s->portData; - if (pd == NULL) { - jlpGenericTileFill(s, bx, by, colorIndex); - return; - } - for (plane = 0; plane < AMIGA_BITPLANES; plane++) { - fillByte = ((colorIndex >> plane) & 1u) ? 0xFFu : 0x00u; - p = pd->planes[plane] + (uint16_t)by * 8u * AMIGA_BYTES_PER_ROW + bx; - p[AMIGA_TILE_ROW_OFF(0)] = fillByte; - p[AMIGA_TILE_ROW_OFF(1)] = fillByte; - p[AMIGA_TILE_ROW_OFF(2)] = fillByte; - p[AMIGA_TILE_ROW_OFF(3)] = fillByte; - p[AMIGA_TILE_ROW_OFF(4)] = fillByte; - p[AMIGA_TILE_ROW_OFF(5)] = fillByte; - p[AMIGA_TILE_ROW_OFF(6)] = fillByte; - p[AMIGA_TILE_ROW_OFF(7)] = fillByte; - } -} - - -static inline __attribute__((always_inline)) void amigaTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) { - AmigaPlanarT *dstPd; - AmigaPlanarT *srcPd; - uint16_t plane; - uint8_t *dp; - const uint8_t *sp; - - dstPd = (AmigaPlanarT *)dst->portData; - srcPd = (AmigaPlanarT *)src->portData; - if (dstPd == NULL || srcPd == NULL) { - jlpGenericTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy); - return; - } - for (plane = 0; plane < AMIGA_BITPLANES; plane++) { - dp = dstPd->planes[plane] + (uint16_t)dstBy * 8u * AMIGA_BYTES_PER_ROW + dstBx; - sp = srcPd->planes[plane] + (uint16_t)srcBy * 8u * AMIGA_BYTES_PER_ROW + srcBx; - dp[AMIGA_TILE_ROW_OFF(0)] = sp[AMIGA_TILE_ROW_OFF(0)]; - dp[AMIGA_TILE_ROW_OFF(1)] = sp[AMIGA_TILE_ROW_OFF(1)]; - dp[AMIGA_TILE_ROW_OFF(2)] = sp[AMIGA_TILE_ROW_OFF(2)]; - dp[AMIGA_TILE_ROW_OFF(3)] = sp[AMIGA_TILE_ROW_OFF(3)]; - dp[AMIGA_TILE_ROW_OFF(4)] = sp[AMIGA_TILE_ROW_OFF(4)]; - dp[AMIGA_TILE_ROW_OFF(5)] = sp[AMIGA_TILE_ROW_OFF(5)]; - dp[AMIGA_TILE_ROW_OFF(6)] = sp[AMIGA_TILE_ROW_OFF(6)]; - dp[AMIGA_TILE_ROW_OFF(7)] = sp[AMIGA_TILE_ROW_OFF(7)]; - } -} - - -// Pure-planar masked copy. For each row of the 8x8 tile, read 4 src -// plane bytes; compute a per-pixel "non-transparent" mask via XOR -// against the transparent index's per-plane bit pattern (a pixel -// matches transparent IFF all 4 plane bits match transparent's 4 -// bits = OR of 4 XOR'd bytes is 0 in that bit). Then for each plane, -// dst = (dst & ~mask) | (src & mask) -- copy src bits at mask-set -// positions, preserve dst bits elsewhere. -static inline __attribute__((always_inline)) void amigaTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparent) { - AmigaPlanarT *dstPd; - AmigaPlanarT *srcPd; - uint8_t transBitByte[AMIGA_BITPLANES]; - uint16_t i; - uint8_t row; - uint16_t srcByteOff; - uint16_t dstByteOff; - uint8_t srcPlaneBytes[AMIGA_BITPLANES]; - uint8_t maskByte; - - dstPd = (AmigaPlanarT *)dst->portData; - srcPd = (AmigaPlanarT *)src->portData; - if (dstPd == NULL || srcPd == NULL) { - jlpGenericTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparent); - return; - } - transparent = (uint8_t)(transparent & 0x0Fu); - /* Per-plane "all bits set if transparent's bit at this plane is 1 - * else all 0" -- so XOR gives bit set where pixel differs from - * transparent in that plane. */ - for (i = 0; i < AMIGA_BITPLANES; i++) { - transBitByte[i] = ((transparent >> i) & 1u) ? 0xFFu : 0x00u; - } - - for (row = 0; row < 8u; row++) { - srcByteOff = (uint16_t)((uint16_t)srcBy * 8u + row) * AMIGA_BYTES_PER_ROW + srcBx; - dstByteOff = (uint16_t)((uint16_t)dstBy * 8u + row) * AMIGA_BYTES_PER_ROW + dstBx; - srcPlaneBytes[0] = srcPd->planes[0][srcByteOff]; - srcPlaneBytes[1] = srcPd->planes[1][srcByteOff]; - srcPlaneBytes[2] = srcPd->planes[2][srcByteOff]; - srcPlaneBytes[3] = srcPd->planes[3][srcByteOff]; - /* maskByte: bit set where pixel differs from transparent in - * ANY plane -- i.e., where the pixel is non-transparent. */ - maskByte = (uint8_t)((srcPlaneBytes[0] ^ transBitByte[0]) - | (srcPlaneBytes[1] ^ transBitByte[1]) - | (srcPlaneBytes[2] ^ transBitByte[2]) - | (srcPlaneBytes[3] ^ transBitByte[3])); - for (i = 0; i < AMIGA_BITPLANES; i++) { - uint8_t existing = dstPd->planes[i][dstByteOff]; - dstPd->planes[i][dstByteOff] = (uint8_t)((existing & (uint8_t)~maskByte) - | (srcPlaneBytes[i] & maskByte)); - } - } -} - - -// Phase 9 plane-to-chunky derivation for jlTileSnap. Reads 8 plane -// bytes (1 byte per row x 8 rows) from each of 4 planes for the -// 8-pixel-aligned tile column at bx, then assembles 32 chunky bytes -// (4 per row x 8 rows, packed 2 px/byte high-then-low nibble) into -// On Amiga, jlTileT.pixels is opaque port-specific storage (cross- -// platform tile.c never reads it directly when s->pixels is NULL). -// We use those 32 bytes as 4 planes x 8 rows, plane-major: -// bytes [0..7] = plane 0, rows 0..7 -// bytes [8..15] = plane 1, rows 0..7 -// bytes [16..23] = plane 2, rows 0..7 -// bytes [24..31] = plane 3, rows 0..7 -// snap/paste then become 32 plain byte loads + stores -- no chunky -// <-> planar conversion at all. The previous c2p-based path paid -// 4 KB LUT lookups + bit shuffling per pixel; this is ~50x cheaper. -#define AMIGA_TILE_PLANE_STRIDE 8 -static inline __attribute__((always_inline)) void amigaTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut) { - AmigaPlanarT *pd; - uint16_t rowBase; - uint8_t plane; - - pd = (AmigaPlanarT *)src->portData; - if (pd == NULL) { - jlpGenericTileSnap(src, bx, by, chunkyOut); - return; - } - rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx; - for (plane = 0; plane < AMIGA_BITPLANES; plane++) { - const uint8_t *p = pd->planes[plane] + rowBase; - uint8_t *q = chunkyOut + plane * AMIGA_TILE_PLANE_STRIDE; - q[0] = p[AMIGA_TILE_ROW_OFF(0)]; - q[1] = p[AMIGA_TILE_ROW_OFF(1)]; - q[2] = p[AMIGA_TILE_ROW_OFF(2)]; - q[3] = p[AMIGA_TILE_ROW_OFF(3)]; - q[4] = p[AMIGA_TILE_ROW_OFF(4)]; - q[5] = p[AMIGA_TILE_ROW_OFF(5)]; - q[6] = p[AMIGA_TILE_ROW_OFF(6)]; - q[7] = p[AMIGA_TILE_ROW_OFF(7)]; - } -} - - -static inline __attribute__((always_inline)) void amigaTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) { - AmigaPlanarT *pd; - uint8_t plane; - uint16_t rowBase; - - pd = (AmigaPlanarT *)dst->portData; - if (pd == NULL) { - jlpGenericTilePaste(dst, bx, by, chunkyTile); - return; - } - /* jlTileT.pixels[] holds plane-major planar bytes (see - * jlpTileSnap header for layout). Paste = 32 byte stores - * with no chunky -> planar conversion. */ - rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx; - for (plane = 0; plane < AMIGA_BITPLANES; plane++) { - const uint8_t *q = chunkyTile + plane * AMIGA_TILE_PLANE_STRIDE; - uint8_t *p = pd->planes[plane] + rowBase; - p[AMIGA_TILE_ROW_OFF(0)] = q[0]; - p[AMIGA_TILE_ROW_OFF(1)] = q[1]; - p[AMIGA_TILE_ROW_OFF(2)] = q[2]; - p[AMIGA_TILE_ROW_OFF(3)] = q[3]; - p[AMIGA_TILE_ROW_OFF(4)] = q[4]; - p[AMIGA_TILE_ROW_OFF(5)] = q[5]; - p[AMIGA_TILE_ROW_OFF(6)] = q[6]; - p[AMIGA_TILE_ROW_OFF(7)] = q[7]; - } -} - - -// Whole-map paste engine (NATIVE-PERF W3 item 9). jlTileMapPaste's -// shared whole-map arm calls this ONCE per map: the portData load + -// NULL test, the by*320 row-base derive, and the four plane base -// pointers are hoisted out of the per-cell loop -- the per-cell -// amigaTilePaste route reloads portData, rebuilds the cell offset, -// and re-reads pd->planes[0..3] from the struct every cell (~172 of -// its ~776 cyc/cell, objdump-measured). The four dst pointers stay -// resident in a0-a3 (one byte per column, one row stride per tile -// row), so each cell is 32 move.b off compile-time displacements -// with zero per-plane setup. Plane order, row order, and written -// values are identical to per-cell amigaTilePaste -- pixel output is -// byte-identical. movep is unusable here (plane bytes sit 8000 bytes -// apart across separate AllocMem planes, vs the ST's interleaved +2 -// layout), so C is the right weapon on this port. -#define AMIGA_TILE_MAP_ROW_STRIDE ((uint16_t)(TILE_PIXELS_PER_SIDE * AMIGA_BYTES_PER_ROW)) - -#define AMIGA_MAP_CELL_PLANE(_dp, _plane) \ - do { \ - (_dp)[AMIGA_TILE_ROW_OFF(0)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 0]; \ - (_dp)[AMIGA_TILE_ROW_OFF(1)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 1]; \ - (_dp)[AMIGA_TILE_ROW_OFF(2)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 2]; \ - (_dp)[AMIGA_TILE_ROW_OFF(3)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 3]; \ - (_dp)[AMIGA_TILE_ROW_OFF(4)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 4]; \ - (_dp)[AMIGA_TILE_ROW_OFF(5)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 5]; \ - (_dp)[AMIGA_TILE_ROW_OFF(6)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 6]; \ - (_dp)[AMIGA_TILE_ROW_OFF(7)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 7]; \ - } while (0) - -static inline __attribute__((always_inline)) void amigaTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map) { - AmigaPlanarT *pd; - const uint8_t *tileBytes; - const uint8_t *src; - uint8_t *p0; - uint8_t *p1; - uint8_t *p2; - uint8_t *p3; - uint16_t rowBase; - uint16_t rewind; - uint8_t idx; - uint8_t ty; - uint8_t tx; - - tileBytes = (const uint8_t *)tiles; - pd = (AmigaPlanarT *)dst->portData; - if (pd == NULL) { - for (ty = 0; ty < hTiles; ty++) { - for (tx = 0; tx < wTiles; tx++) { - idx = map[tx]; - if (idx != TILE_MAP_SKIP) { - jlpGenericTilePaste(dst, (uint8_t)(bx + tx), (uint8_t)(by + ty), tileBytes + ((uint16_t)idx << 5)); - } - } - map += wTiles; - } - return; - } - rowBase = (uint16_t)((uint16_t)by * AMIGA_TILE_MAP_ROW_STRIDE + bx); - rewind = (uint16_t)(AMIGA_TILE_MAP_ROW_STRIDE - wTiles); - p0 = pd->planes[0] + rowBase; - p1 = pd->planes[1] + rowBase; - p2 = pd->planes[2] + rowBase; - p3 = pd->planes[3] + rowBase; - for (ty = hTiles; ty != 0u; ty--) { - for (tx = wTiles; tx != 0u; tx--) { - idx = *map++; - if (idx != TILE_MAP_SKIP) { - src = tileBytes + ((uint16_t)idx << 5); - AMIGA_MAP_CELL_PLANE(p0, 0); - AMIGA_MAP_CELL_PLANE(p1, 1); - AMIGA_MAP_CELL_PLANE(p2, 2); - AMIGA_MAP_CELL_PLANE(p3, 3); - } - p0++; - p1++; - p2++; - p3++; - } - p0 += rewind; - p1 += rewind; - p2 += rewind; - p3 += rewind; - } -} -#undef AMIGA_MAP_CELL_PLANE -#undef AMIGA_TILE_MAP_ROW_STRIDE - - -// Planar monochrome paste. monoTile follows the cross-port mono -// contract (include/joey/tile.h + jlpGenericTilePasteMono): 32 chunky -// nibble-pair bytes, row-major, TILE_BYTES_PER_ROW bytes per row, two -// pixels per byte. A pixel renders fgColor when its source nibble is -// nonzero, bgColor when zero; the HIGH nibble is the LEFT pixel. Each -// row's 4 source bytes fold into an 8-bit shape mask (bit 7 = -// leftmost pixel, matching the planar bit order), then each plane k -// writes -// outPlaneK = (shape & maskFgK) | (~shape & maskBgK) -// where maskXK = $FF if (X & (1 << k)) else $00. -// Eight 1bpp shape rows into the bitplanes: set bits take the -// foreground's plane bit, clear bits the background's. Shared by the -// mono paste (which folds a chunky silhouette into these rows first) -// and the glyph paste (whose input already IS these rows). -static inline __attribute__((always_inline)) void amigaTilePasteShape(AmigaPlanarT *pd, uint8_t bx, uint8_t by, const uint8_t *shape, uint8_t fgColor, uint8_t bgColor) { - uint16_t rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx; - uint8_t plane; - uint8_t row; - - for (plane = 0; plane < AMIGA_BITPLANES; plane++) { - uint8_t maskFg = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u); - uint8_t maskBg = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u); - uint8_t *p = pd->planes[plane] + rowBase; - for (row = 0; row < 8u; row++) { - uint8_t s = shape[row]; - p[row * AMIGA_BYTES_PER_ROW] = (uint8_t)((uint8_t)(s & maskFg) | (uint8_t)((uint8_t)(~s) & maskBg)); - } - } -} - - -static inline __attribute__((always_inline)) void amigaTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) { - AmigaPlanarT *pd; - uint8_t shape[TILE_PIXELS_PER_SIDE]; - uint8_t row; - uint8_t col; - - pd = (AmigaPlanarT *)dst->portData; - if (pd == NULL) { - jlpGenericTilePasteMono(dst, bx, by, monoTile, fgColor, bgColor); - return; - } - // Fold each chunky nibble-pair row into one 1bpp shape byte. - for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) { - uint8_t bits = 0u; - for (col = 0; col < TILE_BYTES_PER_ROW; col++) { - uint8_t srcByte = *monoTile++; - bits = (uint8_t)(bits << 2); - if (srcByte & 0xF0u) { - bits = (uint8_t)(bits | 0x02u); - } - if (srcByte & 0x0Fu) { - bits = (uint8_t)(bits | 0x01u); - } - } - shape[row] = bits; - } - amigaTilePasteShape(pd, bx, by, shape, fgColor, bgColor); -} - - -// 1bpp glyph rows straight to the planes: a glyph byte IS the row shape -// the mono paste has to fold out of a chunky silhouette, so this skips -// the fold entirely (and cannot be handed the wrong tile format). -static inline __attribute__((always_inline)) void amigaTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) { - AmigaPlanarT *pd = (AmigaPlanarT *)dst->portData; - - if (pd == NULL) { - jlpGenericTilePasteGlyph(dst, bx, by, bits, fgColor, bgColor); - return; - } - amigaTilePasteShape(pd, bx, by, bits, fgColor, bgColor); -} +#include "planarTile.h" #endif diff --git a/src/core/assetLoad.c b/src/core/assetLoad.c index e4e362e..7317fb6 100644 --- a/src/core/assetLoad.c +++ b/src/core/assetLoad.c @@ -100,7 +100,7 @@ typedef char AssetTileSizeCheckT[(sizeof(jlTileT) == TILE_BYTES) ? 1 : -1]; #elif defined(JOEYLIB_PLATFORM_BLANK) #define EXPECTED_TILE_TARGET 3u // chunky 4bpp, same as DOS (uses the generic tile ops) #elif defined(JOEYLIB_PLATFORM_X68000) -// Plane-major like the Amiga (src/x68000/x68kTile.h reads jlTileT.pixels as +// Plane-major like the Amiga (src/m68k/planarTile.h reads jlTileT.pixels as // 4 planes x 8 rows), baked by assetbake --target x68000. #define EXPECTED_TILE_TARGET 5u #else @@ -578,26 +578,29 @@ uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles, uint16_t toLoad; size_t got; - if (path == NULL || outTiles == NULL || maxTiles == 0u) { + if (outTiles == NULL || maxTiles == 0u) { return 0u; } - // The target byte for .tbk must match the build platform. .spr - // files carry target=0; tiles carry the per-platform value. A - // mismatch is a build-system bug -- the per-target make rule - // should have produced a .tbk with the matching target byte. - if (!openReadValidateHeader(path, "JTB1", EXPECTED_TILE_TARGET, header, outPalette, &fp)) { - return 0u; - } - tileCount = (uint16_t)(header[6] | (header[7] << 8)); - - // Define the whole outValid array up front so leftover slots read - // false exactly as the tile.h contract promises, regardless of - // caller pre-zeroing. + // Define the whole outValid array up front -- BEFORE the file is + // opened -- so leftover slots read false exactly as the tile.h + // contract promises, regardless of caller pre-zeroing. Clearing it + // only after the header validated left a refused, missing or + // short-header load reporting whatever an earlier load into the + // same array had marked valid (caught by scripts/check-tilebank.sh). if (outValid != NULL) { for (i = 0; i < maxTiles; i++) { outValid[i] = false; } } + // The target byte for .tbk must match the build platform. .spr + // files carry target=0; tiles carry the per-platform value. A + // mismatch is a build-system bug -- the per-target make rule + // should have produced a .tbk with the matching target byte. A + // NULL path needs no guard here: jlDataOpen refuses it. + if (!openReadValidateHeader(path, "JTB1", EXPECTED_TILE_TARGET, header, outPalette, &fp)) { + return 0u; + } + tileCount = (uint16_t)(header[6] | (header[7] << 8)); toLoad = (tileCount < maxTiles) ? tileCount : maxTiles; diff --git a/src/core/debug.c b/src/core/debug.c index 5b224e1..627bea8 100644 --- a/src/core/debug.c +++ b/src/core/debug.c @@ -96,8 +96,7 @@ void jlLogF(const char *fmt, ...) { (void)vsnprintf(line, sizeof(line), fmt, args); va_end(args); ringAppend(line); - // Callers' formats mostly end in \n already; jlLog adds one for - // plain strings, so nothing is appended here. + ringAppend("\n"); // one call is one line, as with jlLog, on every port } diff --git a/src/core/port.h b/src/core/port.h index 81ab8e1..8046fc9 100644 --- a/src/core/port.h +++ b/src/core/port.h @@ -383,8 +383,9 @@ void iigsIrqLatencyTighten(void); // ===================================================================== // 68k tile ops as always-inline headers (NATIVE-PERF Phase 2 R4a). // -// The ST/Amiga tile overrides live in stTile.h / amigaTile.h as -// static inline functions named stTile* / amigaTile*. Aliasing the +// The ST tile overrides live in stTile.h as static inline functions named +// stTile*; the Amiga and X68000 share planarTile* (src/m68k/planarTile.h, +// through the amigaTile.h / x68kTile.h shims). Aliasing the // jlpTile* dispatch names to them HERE (before the dispatch tail) // makes the tail's #if !defined(jlpTile*) guards skip their extern // prototypes -- same mechanism as the IIgs macro block above -- and @@ -407,24 +408,16 @@ void iigsIrqLatencyTighten(void); // union-mark stanza lives exactly twice in tile.c (shared arm + // generic) no matter how many ports bring engines. #define jlpTileMapPaste stTileMapPaste -#elif defined(JOEYLIB_PLATFORM_AMIGA) - #define jlpTileFill amigaTileFill - #define jlpTileCopy amigaTileCopy - #define jlpTileCopyMasked amigaTileCopyMasked - #define jlpTilePaste amigaTilePaste - #define jlpTilePasteMono amigaTilePasteMono - #define jlpTilePasteGlyph amigaTilePasteGlyph - #define jlpTileSnap amigaTileSnap - #define jlpTileMapPaste amigaTileMapPaste -#elif defined(JOEYLIB_PLATFORM_X68000) - #define jlpTileFill x68kTileFill - #define jlpTileCopy x68kTileCopy - #define jlpTileCopyMasked x68kTileCopyMasked - #define jlpTilePaste x68kTilePaste - #define jlpTilePasteMono x68kTilePasteMono - #define jlpTilePasteGlyph x68kTilePasteGlyph - #define jlpTileSnap x68kTileSnap - #define jlpTileMapPaste x68kTileMapPaste +#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_X68000) + // One set of planar bodies for both (src/m68k/planarTile.h). + #define jlpTileFill planarTileFill + #define jlpTileCopy planarTileCopy + #define jlpTileCopyMasked planarTileCopyMasked + #define jlpTilePaste planarTilePaste + #define jlpTilePasteMono planarTilePasteMono + #define jlpTilePasteGlyph planarTilePasteGlyph + #define jlpTileSnap planarTileSnap + #define jlpTileMapPaste planarTileMapPaste #elif defined(JOEYLIB_PLATFORM_DOS) // DOS overrides only the whole-map walker (dosTile.h, W3); the // single-tile ops stay on the chunky generics. diff --git a/src/m68k/planarTile.h b/src/m68k/planarTile.h new file mode 100644 index 0000000..59e3ce7 --- /dev/null +++ b/src/m68k/planarTile.h @@ -0,0 +1,377 @@ +// Planar tile ops shared by the Amiga and the X68000, as always-inline +// functions (NATIVE-PERF Phase 2 R4a). Both ports keep their surfaces as four +// separate bitplanes of 40 bytes a row with the same portData layout +// (AmigaPlanarT / X68kPlanarT, field for field), so an 8x8 tile is 8 rows x 4 +// planes x 1 byte on both and one body serves them. The public wrappers in +// src/core/tile.c inline these, deleting the second full cdecl call layer +// (~130-190 cycles/op on the 68000). The jlpTile* dispatch names are +// macro-aliased to these in port.h, which also #includes the port's shim +// (amigaTile.h / x68kTile.h) at its tail -- AFTER the jlpGenericTile* +// prototypes the portData == NULL fallbacks call. Do not include this header +// directly: the shim defines, before including it, +// PLANAR_TILE_DATA_T the port's planar portData type +// PLANAR_TILE_BYTES_PER_ROW bytes per plane row (40) +// PLANAR_TILE_PLANES bitplanes (4) +#ifndef JOEY_PLANAR_TILE_H +#define JOEY_PLANAR_TILE_H + +#include +#include + +#include "joey/tile.h" + +// 8-row unrolled tile access (NATIVE-PERF Phase 2 R8): row offsets +// are compile-time constants row * 40, so each row is one d16(An) +// access with no pointer stepping and no loop control. gcc +// -O2 keeps the equivalent row loops rolled as dbf (objdump-verified, +// ~18 cycles/row of pure loop overhead). Only the row dimension +// unrolls -- the 4-plane loop keeps real per-plane setup and stays +// register-light per the m68k-gcc-unroll-backfire lesson. +#define PLANAR_TILE_ROW_OFF(_r) ((uint16_t)((_r) * PLANAR_TILE_BYTES_PER_ROW)) + +// Phase 5 planar dual-write for tile ops, fully planar after Phase 9 +// dropped the chunky shadow. All tiles are 8-pixel aligned (8x8 blocks +// at multiples of 8), so plane writes are byte-aligned -- one plane +// byte per row, 8 rows per tile, no edge masks. Stride between rows +// in a plane is PLANAR_TILE_BYTES_PER_ROW (40). + +static inline __attribute__((always_inline)) void planarTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) { + PLANAR_TILE_DATA_T *pd; + uint16_t plane; + uint8_t fillByte; + uint8_t *p; + + pd = (PLANAR_TILE_DATA_T *)s->portData; + if (pd == NULL) { + jlpGenericTileFill(s, bx, by, colorIndex); + return; + } + for (plane = 0; plane < PLANAR_TILE_PLANES; plane++) { + fillByte = ((colorIndex >> plane) & 1u) ? 0xFFu : 0x00u; + p = pd->planes[plane] + (uint16_t)by * 8u * PLANAR_TILE_BYTES_PER_ROW + bx; + p[PLANAR_TILE_ROW_OFF(0)] = fillByte; + p[PLANAR_TILE_ROW_OFF(1)] = fillByte; + p[PLANAR_TILE_ROW_OFF(2)] = fillByte; + p[PLANAR_TILE_ROW_OFF(3)] = fillByte; + p[PLANAR_TILE_ROW_OFF(4)] = fillByte; + p[PLANAR_TILE_ROW_OFF(5)] = fillByte; + p[PLANAR_TILE_ROW_OFF(6)] = fillByte; + p[PLANAR_TILE_ROW_OFF(7)] = fillByte; + } +} + + +static inline __attribute__((always_inline)) void planarTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) { + PLANAR_TILE_DATA_T *dstPd; + PLANAR_TILE_DATA_T *srcPd; + uint16_t plane; + uint8_t *dp; + const uint8_t *sp; + + dstPd = (PLANAR_TILE_DATA_T *)dst->portData; + srcPd = (PLANAR_TILE_DATA_T *)src->portData; + if (dstPd == NULL || srcPd == NULL) { + jlpGenericTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy); + return; + } + for (plane = 0; plane < PLANAR_TILE_PLANES; plane++) { + dp = dstPd->planes[plane] + (uint16_t)dstBy * 8u * PLANAR_TILE_BYTES_PER_ROW + dstBx; + sp = srcPd->planes[plane] + (uint16_t)srcBy * 8u * PLANAR_TILE_BYTES_PER_ROW + srcBx; + dp[PLANAR_TILE_ROW_OFF(0)] = sp[PLANAR_TILE_ROW_OFF(0)]; + dp[PLANAR_TILE_ROW_OFF(1)] = sp[PLANAR_TILE_ROW_OFF(1)]; + dp[PLANAR_TILE_ROW_OFF(2)] = sp[PLANAR_TILE_ROW_OFF(2)]; + dp[PLANAR_TILE_ROW_OFF(3)] = sp[PLANAR_TILE_ROW_OFF(3)]; + dp[PLANAR_TILE_ROW_OFF(4)] = sp[PLANAR_TILE_ROW_OFF(4)]; + dp[PLANAR_TILE_ROW_OFF(5)] = sp[PLANAR_TILE_ROW_OFF(5)]; + dp[PLANAR_TILE_ROW_OFF(6)] = sp[PLANAR_TILE_ROW_OFF(6)]; + dp[PLANAR_TILE_ROW_OFF(7)] = sp[PLANAR_TILE_ROW_OFF(7)]; + } +} + + +// Pure-planar masked copy. For each row of the 8x8 tile, read 4 src +// plane bytes; compute a per-pixel "non-transparent" mask via XOR +// against the transparent index's per-plane bit pattern (a pixel +// matches transparent IFF all 4 plane bits match transparent's 4 +// bits = OR of 4 XOR'd bytes is 0 in that bit). Then for each plane, +// dst = (dst & ~mask) | (src & mask) -- copy src bits at mask-set +// positions, preserve dst bits elsewhere. +static inline __attribute__((always_inline)) void planarTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparent) { + PLANAR_TILE_DATA_T *dstPd; + PLANAR_TILE_DATA_T *srcPd; + uint8_t transBitByte[PLANAR_TILE_PLANES]; + uint16_t i; + uint8_t row; + uint16_t srcByteOff; + uint16_t dstByteOff; + uint8_t srcPlaneBytes[PLANAR_TILE_PLANES]; + uint8_t maskByte; + + dstPd = (PLANAR_TILE_DATA_T *)dst->portData; + srcPd = (PLANAR_TILE_DATA_T *)src->portData; + if (dstPd == NULL || srcPd == NULL) { + jlpGenericTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparent); + return; + } + transparent = (uint8_t)(transparent & 0x0Fu); + /* Per-plane "all bits set if transparent's bit at this plane is 1 + * else all 0" -- so XOR gives bit set where pixel differs from + * transparent in that plane. */ + for (i = 0; i < PLANAR_TILE_PLANES; i++) { + transBitByte[i] = ((transparent >> i) & 1u) ? 0xFFu : 0x00u; + } + + for (row = 0; row < 8u; row++) { + srcByteOff = (uint16_t)((uint16_t)srcBy * 8u + row) * PLANAR_TILE_BYTES_PER_ROW + srcBx; + dstByteOff = (uint16_t)((uint16_t)dstBy * 8u + row) * PLANAR_TILE_BYTES_PER_ROW + dstBx; + srcPlaneBytes[0] = srcPd->planes[0][srcByteOff]; + srcPlaneBytes[1] = srcPd->planes[1][srcByteOff]; + srcPlaneBytes[2] = srcPd->planes[2][srcByteOff]; + srcPlaneBytes[3] = srcPd->planes[3][srcByteOff]; + /* maskByte: bit set where pixel differs from transparent in + * ANY plane -- i.e., where the pixel is non-transparent. */ + maskByte = (uint8_t)((srcPlaneBytes[0] ^ transBitByte[0]) + | (srcPlaneBytes[1] ^ transBitByte[1]) + | (srcPlaneBytes[2] ^ transBitByte[2]) + | (srcPlaneBytes[3] ^ transBitByte[3])); + for (i = 0; i < PLANAR_TILE_PLANES; i++) { + uint8_t existing = dstPd->planes[i][dstByteOff]; + dstPd->planes[i][dstByteOff] = (uint8_t)((existing & (uint8_t)~maskByte) + | (srcPlaneBytes[i] & maskByte)); + } + } +} + + +// Phase 9 plane-to-chunky derivation for jlTileSnap. Reads 8 plane +// bytes (1 byte per row x 8 rows) from each of 4 planes for the +// 8-pixel-aligned tile column at bx, then assembles 32 chunky bytes +// (4 per row x 8 rows, packed 2 px/byte high-then-low nibble) into +// On these ports, jlTileT.pixels is opaque port-specific storage (cross- +// platform tile.c never reads it directly when s->pixels is NULL). +// We use those 32 bytes as 4 planes x 8 rows, plane-major: +// bytes [0..7] = plane 0, rows 0..7 +// bytes [8..15] = plane 1, rows 0..7 +// bytes [16..23] = plane 2, rows 0..7 +// bytes [24..31] = plane 3, rows 0..7 +// snap/paste then become 32 plain byte loads + stores -- no chunky +// <-> planar conversion at all. The previous c2p-based path paid +// 4 KB LUT lookups + bit shuffling per pixel; this is ~50x cheaper. +#define PLANAR_TILE_PLANE_STRIDE 8 +static inline __attribute__((always_inline)) void planarTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut) { + PLANAR_TILE_DATA_T *pd; + uint16_t rowBase; + uint8_t plane; + + pd = (PLANAR_TILE_DATA_T *)src->portData; + if (pd == NULL) { + jlpGenericTileSnap(src, bx, by, chunkyOut); + return; + } + rowBase = (uint16_t)((uint16_t)by * 8u) * PLANAR_TILE_BYTES_PER_ROW + bx; + for (plane = 0; plane < PLANAR_TILE_PLANES; plane++) { + const uint8_t *p = pd->planes[plane] + rowBase; + uint8_t *q = chunkyOut + plane * PLANAR_TILE_PLANE_STRIDE; + q[0] = p[PLANAR_TILE_ROW_OFF(0)]; + q[1] = p[PLANAR_TILE_ROW_OFF(1)]; + q[2] = p[PLANAR_TILE_ROW_OFF(2)]; + q[3] = p[PLANAR_TILE_ROW_OFF(3)]; + q[4] = p[PLANAR_TILE_ROW_OFF(4)]; + q[5] = p[PLANAR_TILE_ROW_OFF(5)]; + q[6] = p[PLANAR_TILE_ROW_OFF(6)]; + q[7] = p[PLANAR_TILE_ROW_OFF(7)]; + } +} + + +static inline __attribute__((always_inline)) void planarTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) { + PLANAR_TILE_DATA_T *pd; + uint8_t plane; + uint16_t rowBase; + + pd = (PLANAR_TILE_DATA_T *)dst->portData; + if (pd == NULL) { + jlpGenericTilePaste(dst, bx, by, chunkyTile); + return; + } + /* jlTileT.pixels[] holds plane-major planar bytes (see + * jlpTileSnap header for layout). Paste = 32 byte stores + * with no chunky -> planar conversion. */ + rowBase = (uint16_t)((uint16_t)by * 8u) * PLANAR_TILE_BYTES_PER_ROW + bx; + for (plane = 0; plane < PLANAR_TILE_PLANES; plane++) { + const uint8_t *q = chunkyTile + plane * PLANAR_TILE_PLANE_STRIDE; + uint8_t *p = pd->planes[plane] + rowBase; + p[PLANAR_TILE_ROW_OFF(0)] = q[0]; + p[PLANAR_TILE_ROW_OFF(1)] = q[1]; + p[PLANAR_TILE_ROW_OFF(2)] = q[2]; + p[PLANAR_TILE_ROW_OFF(3)] = q[3]; + p[PLANAR_TILE_ROW_OFF(4)] = q[4]; + p[PLANAR_TILE_ROW_OFF(5)] = q[5]; + p[PLANAR_TILE_ROW_OFF(6)] = q[6]; + p[PLANAR_TILE_ROW_OFF(7)] = q[7]; + } +} + + +// Whole-map paste engine (NATIVE-PERF W3 item 9). jlTileMapPaste's +// shared whole-map arm calls this ONCE per map: the portData load + +// NULL test, the by*320 row-base derive, and the four plane base +// pointers are hoisted out of the per-cell loop -- the per-cell +// planarTilePaste route reloads portData, rebuilds the cell offset, +// and re-reads pd->planes[0..3] from the struct every cell (~172 of +// its ~776 cyc/cell, objdump-measured). The four dst pointers stay +// resident in a0-a3 (one byte per column, one row stride per tile +// row), so each cell is 32 move.b off compile-time displacements +// with zero per-plane setup. Plane order, row order, and written +// values are identical to per-cell planarTilePaste -- pixel output is +// byte-identical. movep is unusable here (plane bytes sit 8000 bytes +// apart across separate AllocMem planes, vs the ST's interleaved +2 +// layout), so C is the right weapon on this port. +#define PLANAR_TILE_MAP_ROW_STRIDE ((uint16_t)(TILE_PIXELS_PER_SIDE * PLANAR_TILE_BYTES_PER_ROW)) + +#define PLANAR_MAP_CELL_PLANE(_dp, _plane) \ + do { \ + (_dp)[PLANAR_TILE_ROW_OFF(0)] = src[(_plane) * PLANAR_TILE_PLANE_STRIDE + 0]; \ + (_dp)[PLANAR_TILE_ROW_OFF(1)] = src[(_plane) * PLANAR_TILE_PLANE_STRIDE + 1]; \ + (_dp)[PLANAR_TILE_ROW_OFF(2)] = src[(_plane) * PLANAR_TILE_PLANE_STRIDE + 2]; \ + (_dp)[PLANAR_TILE_ROW_OFF(3)] = src[(_plane) * PLANAR_TILE_PLANE_STRIDE + 3]; \ + (_dp)[PLANAR_TILE_ROW_OFF(4)] = src[(_plane) * PLANAR_TILE_PLANE_STRIDE + 4]; \ + (_dp)[PLANAR_TILE_ROW_OFF(5)] = src[(_plane) * PLANAR_TILE_PLANE_STRIDE + 5]; \ + (_dp)[PLANAR_TILE_ROW_OFF(6)] = src[(_plane) * PLANAR_TILE_PLANE_STRIDE + 6]; \ + (_dp)[PLANAR_TILE_ROW_OFF(7)] = src[(_plane) * PLANAR_TILE_PLANE_STRIDE + 7]; \ + } while (0) + +static inline __attribute__((always_inline)) void planarTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map) { + PLANAR_TILE_DATA_T *pd; + const uint8_t *tileBytes; + const uint8_t *src; + uint8_t *p0; + uint8_t *p1; + uint8_t *p2; + uint8_t *p3; + uint16_t rowBase; + uint16_t rewind; + uint8_t idx; + uint8_t ty; + uint8_t tx; + + tileBytes = (const uint8_t *)tiles; + pd = (PLANAR_TILE_DATA_T *)dst->portData; + if (pd == NULL) { + for (ty = 0; ty < hTiles; ty++) { + for (tx = 0; tx < wTiles; tx++) { + idx = map[tx]; + if (idx != TILE_MAP_SKIP) { + jlpGenericTilePaste(dst, (uint8_t)(bx + tx), (uint8_t)(by + ty), tileBytes + ((uint16_t)idx << 5)); + } + } + map += wTiles; + } + return; + } + rowBase = (uint16_t)((uint16_t)by * PLANAR_TILE_MAP_ROW_STRIDE + bx); + rewind = (uint16_t)(PLANAR_TILE_MAP_ROW_STRIDE - wTiles); + p0 = pd->planes[0] + rowBase; + p1 = pd->planes[1] + rowBase; + p2 = pd->planes[2] + rowBase; + p3 = pd->planes[3] + rowBase; + for (ty = hTiles; ty != 0u; ty--) { + for (tx = wTiles; tx != 0u; tx--) { + idx = *map++; + if (idx != TILE_MAP_SKIP) { + src = tileBytes + ((uint16_t)idx << 5); + PLANAR_MAP_CELL_PLANE(p0, 0); + PLANAR_MAP_CELL_PLANE(p1, 1); + PLANAR_MAP_CELL_PLANE(p2, 2); + PLANAR_MAP_CELL_PLANE(p3, 3); + } + p0++; + p1++; + p2++; + p3++; + } + p0 += rewind; + p1 += rewind; + p2 += rewind; + p3 += rewind; + } +} +#undef PLANAR_MAP_CELL_PLANE +#undef PLANAR_TILE_MAP_ROW_STRIDE + + +// Planar monochrome paste. monoTile follows the cross-port mono +// contract (include/joey/tile.h + jlpGenericTilePasteMono): 32 chunky +// nibble-pair bytes, row-major, TILE_BYTES_PER_ROW bytes per row, two +// pixels per byte. A pixel renders fgColor when its source nibble is +// nonzero, bgColor when zero; the HIGH nibble is the LEFT pixel. Each +// row's 4 source bytes fold into an 8-bit shape mask (bit 7 = +// leftmost pixel, matching the planar bit order), then each plane k +// writes +// outPlaneK = (shape & maskFgK) | (~shape & maskBgK) +// where maskXK = $FF if (X & (1 << k)) else $00. +// Eight 1bpp shape rows into the bitplanes: set bits take the +// foreground's plane bit, clear bits the background's. Shared by the +// mono paste (which folds a chunky silhouette into these rows first) +// and the glyph paste (whose input already IS these rows). +static inline __attribute__((always_inline)) void planarTilePasteShape(PLANAR_TILE_DATA_T *pd, uint8_t bx, uint8_t by, const uint8_t *shape, uint8_t fgColor, uint8_t bgColor) { + uint16_t rowBase = (uint16_t)((uint16_t)by * 8u) * PLANAR_TILE_BYTES_PER_ROW + bx; + uint8_t plane; + uint8_t row; + + for (plane = 0; plane < PLANAR_TILE_PLANES; plane++) { + uint8_t maskFg = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u); + uint8_t maskBg = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u); + uint8_t *p = pd->planes[plane] + rowBase; + for (row = 0; row < 8u; row++) { + uint8_t s = shape[row]; + p[row * PLANAR_TILE_BYTES_PER_ROW] = (uint8_t)((uint8_t)(s & maskFg) | (uint8_t)((uint8_t)(~s) & maskBg)); + } + } +} + + +static inline __attribute__((always_inline)) void planarTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) { + PLANAR_TILE_DATA_T *pd; + uint8_t shape[TILE_PIXELS_PER_SIDE]; + uint8_t row; + uint8_t col; + + pd = (PLANAR_TILE_DATA_T *)dst->portData; + if (pd == NULL) { + jlpGenericTilePasteMono(dst, bx, by, monoTile, fgColor, bgColor); + return; + } + // Fold each chunky nibble-pair row into one 1bpp shape byte. + for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) { + uint8_t bits = 0u; + for (col = 0; col < TILE_BYTES_PER_ROW; col++) { + uint8_t srcByte = *monoTile++; + bits = (uint8_t)(bits << 2); + if (srcByte & 0xF0u) { + bits = (uint8_t)(bits | 0x02u); + } + if (srcByte & 0x0Fu) { + bits = (uint8_t)(bits | 0x01u); + } + } + shape[row] = bits; + } + planarTilePasteShape(pd, bx, by, shape, fgColor, bgColor); +} + + +// 1bpp glyph rows straight to the planes: a glyph byte IS the row shape +// the mono paste has to fold out of a chunky silhouette, so this skips +// the fold entirely (and cannot be handed the wrong tile format). +static inline __attribute__((always_inline)) void planarTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) { + PLANAR_TILE_DATA_T *pd = (PLANAR_TILE_DATA_T *)dst->portData; + + if (pd == NULL) { + jlpGenericTilePasteGlyph(dst, bx, by, bits, fgColor, bgColor); + return; + } + planarTilePasteShape(pd, bx, by, bits, fgColor, bgColor); +} + +#endif diff --git a/src/x68000/audioPcm.c b/src/x68000/audioPcm.c index 3a9e4d6..488221e 100644 --- a/src/x68000/audioPcm.c +++ b/src/x68000/audioPcm.c @@ -324,7 +324,7 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { XMP_FORMAT_MONO | XMP_FORMAT_8BIT | XMP_FORMAT_UNSIGNED); if (gX68kModStartRc != 0) { // Do not fail silently -- that is exactly how the rate bug survived. - jlLogF("x68k: xmp_start_player failed rc=%d rate=%d\n", + jlLogF("x68k: xmp_start_player failed rc=%d rate=%d", (int)gX68kModStartRc, (int)MSM6258_RATE); xmp_release_module(gXmpCtx); gXmpLoaded = false; diff --git a/src/x68000/hal.c b/src/x68000/hal.c index 5c60441..3941c6e 100644 --- a/src/x68000/hal.c +++ b/src/x68000/hal.c @@ -51,6 +51,17 @@ // queue -- purely defensive, so a wedged _B_KEYSNS can't spin forever. #define X68K_KEY_DRAIN_GUARD 32u +// _iocs_skey_mod modes (IOCS $7D). The IPL ROM powers up in AUTO -- its boot +// copies $FF into the mode byte -- and in AUTO every press of the LEFT mouse +// button steps the ROM's own display: its mouse pointer on, then its software +// keyboard, then both off (IPL 1.0, from $FF467E). Both draw into the text +// plane, which is this port's display, and while that keyboard is up a RIGHT +// press over it types a key and never reaches _MS_GETDT (found by +// verify-x68000-serial-mouse.sh: two left clicks, and the next right click +// went missing). OFF for as long as a program runs; AUTO is handed back. +#define X68K_SKEY_OFF 0 +#define X68K_SKEY_AUTO (-1) + // MC68901 MFP general-purpose I/O. Bit 4 is the CRTC's V-DISP line. #define X68K_MFP_GPIP ((volatile uint8_t *)0xE88001L) #define X68K_GPIP_VDISP 0x10u @@ -294,7 +305,11 @@ void jlpShutdown(void) { // next Human68k program's pointer to a corner of the screen. _MS_INIT // re-establishes IOCS's own defaults, and it runs AFTER the crtmod restore // above so those defaults match the screen the user is going back to. + // The software keyboard's AUTO mode goes back too (see X68K_SKEY_OFF): IOCS + // has no call that reports the mode, so the ROM's power-on default is + // what is restored. _iocs_ms_init(); + (void)_iocs_skey_mod(X68K_SKEY_AUTO, 0, 0); gModeSet = false; if (gPrevSsp >= 0) { (void)_dos_super(gPrevSsp); // back to user mode for Human68k @@ -919,9 +934,13 @@ void jlpInputInit(void) { // verify-x68000-mouse.sh), so re-reading it between packets // re-counts stale motion. The tracker integrates each packet // exactly once at receive time and clamps to the _MS_LIMIT box, - // which is set to stage coordinates here -- no cursor is ever - // shown (_MS_CURON is never called), the tracker just does the - // bookkeeping. + // which is set to stage coordinates here -- the tracker just does + // the bookkeeping. No IOCS pointer is ever shown: the ROM's + // software keyboard goes to OFF first (X68K_SKEY_OFF), so no left + // click can bring its pointer or keyboard up, and _MS_INIT takes + // down a pointer already up (switching the keyboard off can leave + // one). + (void)_iocs_skey_mod(X68K_SKEY_OFF, 0, 0); _iocs_ms_init(); _iocs_ms_limit(0, 0, SURFACE_WIDTH - 1, SURFACE_HEIGHT - 1); _iocs_ms_curst(SURFACE_WIDTH / 2, SURFACE_HEIGHT / 2); diff --git a/src/x68000/serial.c b/src/x68000/serial.c index 9e7ef14..648e7a8 100644 --- a/src/x68000/serial.c +++ b/src/x68000/serial.c @@ -23,30 +23,31 @@ // ----- Line configuration -------------------------------------------------- // -// _iocs_set232c's mode word, from the IOCS call reference (reproduced on Data -// Crystal's X68k/IOCS page): +// RSDRV.SYS, Human68k's RS-232C driver, is REQUIRED: jlpSerialOpen fails +// without it. It is on every Human68k system disk, whose stock CONFIG.SYS +// loads it (DEVICE = \SYS\RSDRV.SYS), and JoeyLib's X68000 boot disks keep it +// (scripts/lib/x68kDisk.sh). The ROM's own RS-232C routines (IPL-ROM 1.0) +// stop at 9600, have no RTS/CTS, and write the SCC's register pairs with +// interrupts on, so a mouse packet arriving during an open or close could +// split one; RSDRV's mask themselves. One implementation also means one set +// of limits to document and one configuration to test. +// +// _iocs_set232c's mode word as RSDRV 1.13 reads it (from its code; the ROM's +// is the same below bit 7 and indexes 0..7): // bits 15-14 stop bits 1 = 1, 2 = 1.5, 0 or 3 = 2 // bits 13-12 parity 1 = odd, 3 = even, 0 or 2 = none // bits 11-10 data bits 0 = 5 or fewer, 1 = 6, 2 = 7, 3 = 8 // bit 9 XON/XOFF -// bit 8 SI/SO (does not work in ROM IOCS; never set) -// bit 7 RTS/CTS (RSDRV.SYS only, below) -// bits 6-0 rate index 0..7 = 75 150 300 600 1200 2400 4800 9600 -// It returns the mode it replaced, which jlpSerialClose puts back. -// -// Two implementations answer it, and they differ above 9600 (both read from -// their code). The ROM's (IPL-ROM 1.0) takes indexes 0..7 and turns anything -// else -- bit 7 included -- into 1200. Human68k's RS-232C driver RSDRV.SYS -// (1.13 on the Human68k 3.02 system disk, whose CONFIG.SYS loads it) replaces -// the RS-232C IOCS calls, takes indexes 0..9 -- 8 = 19200, 9 = 38400 (time -// constant 2, really 39062 bps, 1.7% fast) -- and reads bit 7 as RTS/CTS, -// which overrides XON/XOFF: its _OSNS232C reports "not ready" while CTS is -// off, and its receive side drops RTS as its buffer fills. So the rates above -// 9600 and RTS/CTS are used only when RSDRV is detected (x68kRsdrvPresent); -// without it a faster request clamps to 9600 and RTS/CTS falls back to no -// handshaking. With this HAL's receive interrupt installed (below) RSDRV's -// receive side never runs, so RTS stays up and the 4 KB soft ring is the -// receive buffer; transmit still waits for CTS. +// bit 8 SI/SO (never set) +// bit 7 RTS/CTS, which overrides XON/XOFF +// bits 6-0 rate index 0..9 = 75 150 300 600 1200 2400 4800 9600 19200 +// 38400 (time constant 2, really 39062 bps, 1.7% fast) +// It returns the mode it replaced, which jlpSerialClose puts back. With +// RTS/CTS, RSDRV's _OSNS232C reports "not ready" while CTS is off, and its +// receive side drops RTS as its buffer fills. With this HAL's receive +// interrupt installed (below) RSDRV's receive side never runs, so RTS stays +// up and the 4 KB soft ring is the receive buffer; transmit still waits for +// CTS. #define X68K_MODE_STOP_1 0x4000u #define X68K_MODE_STOP_2 0xC000u #define X68K_MODE_PARITY_ODD 0x1000u @@ -57,10 +58,9 @@ #define X68K_MODE_RTSCTS 0x0080u #define X68K_DATA_BITS_MIN 5u #define X68K_DATA_BITS_MAX 8u -#define X68K_RATE_COUNT_ROM 8u -#define X68K_RATE_COUNT_RSDRV 10u +#define X68K_RATE_COUNT 10u -static const uint16_t kX68kRates[X68K_RATE_COUNT_RSDRV] = { 75u, 150u, 300u, 600u, 1200u, 2400u, 4800u, 9600u, 19200u, 38400u }; +static const uint16_t kX68kRates[X68K_RATE_COUNT] = { 75u, 150u, 300u, 600u, 1200u, 2400u, 4800u, 9600u, 19200u, 38400u }; // RSDRV.SYS detection. The driver adds an extended RS-232C IOCS call, $F1 // (d2 = function | port << 8), whose function $37 answers $0201 and touches @@ -74,6 +74,7 @@ static const uint16_t kX68kRates[X68K_RATE_COUNT_RSDRV] = { 75u, 150u, 300u, 600 #define X68K_RSDRV_IDENT_FN 0x37 #define X68K_RSDRV_IDENT 0x0201 #define X68K_RAM_END 0x00C00000ul +#define X68K_RSDRV_MISSING "serial: RSDRV.SYS is not loaded (DEVICE = \\SYS\\RSDRV.SYS in CONFIG.SYS)" // WR5 as _SET232C writes it -- the ROM and RSDRV alike: DTR, Tx enable and // RTS, plus the Tx bits-per-char code in bits 6-5 -- so jlpSerialSetDtr can @@ -135,7 +136,7 @@ static bool x68kInSupervisor(void); static uint16_t x68kIrqMask(void); static void x68kIrqRestore(uint16_t sr); static bool x68kRsdrvPresent(void); -static uint16_t x68kSerialMode(const jlSerialConfigT *config, bool rsdrv); +static uint16_t x68kSerialMode(const jlSerialConfigT *config); static void x68kSerialPump(void); static int32_t x68kSuperEnter(void); static void x68kSuperLeave(int32_t ssp); @@ -162,8 +163,7 @@ static void sccIsrInstall(void) { // Only a program already in supervisor mode (every jlInit app) takes the // interrupt over. One in user mode (SERTEST) stays on the polled pump, - // which needs no privilege and races nothing -- and since there is then - // no ISR, jlpSerialWrite needs no interrupt mask either. + // which needs no privilege and races nothing. if (gSccIsrInstalled || !x68kInSupervisor()) { return; } @@ -252,17 +252,14 @@ static bool x68kRsdrvPresent(void) { // The _iocs_set232c mode word for a JoeyLib line configuration (see the table -// above): the rate snaps down to the nearest one the IOCS in charge supports, -// and RTS/CTS becomes no handshaking without RSDRV.SYS. -static uint16_t x68kSerialMode(const jlSerialConfigT *config, bool rsdrv) { +// above): the rate snaps down to the nearest one RSDRV.SYS supports. +static uint16_t x68kSerialMode(const jlSerialConfigT *config) { uint16_t mode; uint8_t rate; - uint8_t rateCount; uint8_t dataBits; - rateCount = rsdrv ? X68K_RATE_COUNT_RSDRV : X68K_RATE_COUNT_ROM; - rate = 0u; - while (rate + 1u < rateCount && kX68kRates[rate + 1u] <= config->baud) { + rate = 0u; + while (rate + 1u < X68K_RATE_COUNT && kX68kRates[rate + 1u] <= config->baud) { rate++; } dataBits = config->dataBits; @@ -282,7 +279,7 @@ static uint16_t x68kSerialMode(const jlSerialConfigT *config, bool rsdrv) { if (config->flow == JL_SERIAL_FLOW_XONXOFF) { mode |= X68K_MODE_XONXOFF; } - if (rsdrv && config->flow == JL_SERIAL_FLOW_RTSCTS) { + if (config->flow == JL_SERIAL_FLOW_RTSCTS) { mode |= X68K_MODE_RTSCTS; } return mode; @@ -332,10 +329,18 @@ static uint8_t x68kWr5ForMode(uint16_t mode) { } -// Write channel A's WR5 and remember it. The pointer write and the data write -// are two accesses to the chip-wide register pointer, so no interrupt (our RX -// ISR, the channel B mouse) may come between them -- see jlpSerialWrite. The -// SCC is supervisor-only, and the app may have left supervisor mode. +// Write channel A's WR5 and remember it. +// +// The Z8530's register pointer is ONE register for the whole chip, shared by +// both channels, and selecting WR5 is a separate access from writing it. So no +// interrupt may come between the two writes below: a handler's own control +// write -- our receive ISR's on channel A, the IOCS mouse handler's on channel +// B -- would land as the DATA for WR5, our value would then reach WR0 as a +// command, and the handler's interrupt-under-service reset would be lost, +// which wedges the chip's interrupt chain. verify-x68000-serial-mouse.sh +// rewrites WR5 constantly while both channels interrupt, and fails within +// seconds without this mask. The SCC is supervisor-only, and the app may have +// left supervisor mode. static void x68kWr5Write(uint8_t wr5) { int32_t ssp; uint16_t sr; @@ -392,7 +397,11 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { if (config == NULL) { return false; } - mode = x68kSerialMode(config, x68kRsdrvPresent()); + if (!x68kRsdrvPresent()) { + coreSetError(X68K_RSDRV_MISSING); + return false; + } + mode = x68kSerialMode(config); gPrevMode = _iocs_set232c((int)mode); gX68kWr5 = x68kWr5ForMode(mode); // _SET232C just wrote this, DTR on gSerialOpened = true; @@ -436,62 +445,36 @@ void jlpSerialSetDtr(bool on) { } -// The Z8530's register pointer is ONE register for the whole chip, shared by -// both channels, and selecting a register is a separate access from using it. -// So any two-access sequence has to be atomic or the two accesses belong to -// different transactions: the interrupting write lands as DATA for whatever -// register the interrupted code had selected, and the interrupted code's own -// data write then goes to WR0 as a command. +// Transmit through IOCS: a bounded ready spin, then one trap per byte. // -// Both IOCS traps below do such a sequence on channel A, and our own channel A -// receive ISR writes the control port to drop the interrupt-under-service. An -// arriving byte can therefore land between a trap's pointer write and its data -// access -- corrupting a receiver register AND losing the IUS reset, which -// wedges the SCC's interrupt daisy chain so no further byte ever interrupts. -// Masking around each trap closes that window. +// The traps need no interrupt mask, although our receive ISR and the IOCS +// mouse handler both write the SCC's chip-wide register pointer (see +// x68kWr5Write). In both implementations -- the IPL 1.0 ROM and RSDRV.SYS 1.13, +// read from their code -- _OSNS232C selects register 0 and reads RR0, and +// _OUT232C writes the data port, which has no pointer. A handler that runs +// between the select and the read writes WR0 itself, which leaves the pointer +// at 0, so the read still gets RR0. (RSDRV's RTS/CTS status read masks +// itself.) The masks this call used to put around each trap held every +// interrupt off for most of the spin and protected nothing: +// verify-x68000-serial-mouse.sh passes without them. // -// The mask goes around each CALL and not around the spin loop: holding it for -// a whole byte time would be the receive latency the ISR exists to avoid. One -// trap is a few microseconds, far inside a byte at any supported baud. -// -// The two INTERRUPT handlers cannot collide with each other -- IOCS's channel B -// mouse handler and our channel A handler are the same 68000 interrupt level, -// so taking one masks the other -- and serial.c's own direct RR2 read in -// sccIsrInstall masks already. What is left is IOCS's internals interrupting -// each other, which is the ROM's business and not reachable from here. +// Nor can the handlers collide with each other: IOCS's channel B mouse handler +// and our channel A handler are the same 68000 interrupt level, so taking one +// masks the other, and the Timer C mouse solicitation (level 6) finishes its +// select-then-write before it returns. uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { uint16_t n; uint32_t guard; - uint16_t sr; - int ready; - // The masks keep our RX ISR out of the IOCS calls' SCC accesses; with no - // ISR installed (the polled path) there is nothing to keep out. for (n = 0u; n < len; n++) { guard = 0ul; - for (;;) { - if (gSccIsrInstalled) { - sr = x68kIrqMask(); - ready = _iocs_osns232c(); - x68kIrqRestore(sr); - } else { - ready = _iocs_osns232c(); - } - if (ready != 0) { - break; - } + while (_iocs_osns232c() == 0) { guard++; if (guard > X68K_TX_SPIN_LIMIT) { return n; } } - if (gSccIsrInstalled) { - sr = x68kIrqMask(); - _iocs_out232c((int)buf[n]); - x68kIrqRestore(sr); - } else { - _iocs_out232c((int)buf[n]); - } + _iocs_out232c((int)buf[n]); } return n; } diff --git a/src/x68000/x68kTile.h b/src/x68000/x68kTile.h index ee77f80..a60da98 100644 --- a/src/x68000/x68kTile.h +++ b/src/x68000/x68kTile.h @@ -1,375 +1,18 @@ -// Sharp X68000 planar tile ops -- adapted verbatim from src/amiga/amigaTile.h. -// The surface layout is identical (40 bytes per row per plane), so an 8x8 tile -// is 8 rows x 4 planes x 1 byte on both machines and the .tbk payload is -// byte-compatible (assetbake --target x68000 writes the Amiga layout under its -// own target byte); only the type names differ. -// Tile ops as always-inline functions, so the public wrappers in -// src/core/tile.c inline them, deleting the second full cdecl call layer -// (~130-190 cycles/op on the 68000). The jlpTile* dispatch names are -// macro-aliased to these in port.h, which also #includes this header at its -// tail -- AFTER the jlpGenericTile* prototypes the portData == NULL fallbacks -// call. Do not include this header directly; it comes with port.h. +// X68000 tile ops: the shared planar bodies in src/m68k/planarTile.h, bound to +// the X68000's planar surface data, which mirrors the Amiga's field for field +// (40 bytes a row, 4 planes), so the .tbk payload is byte-compatible too +// (assetbake --target x68000 writes the Amiga layout under its own target +// byte). Do not include this header directly; it comes with port.h on the +// X68000. #ifndef JOEY_X68K_TILE_H #define JOEY_X68K_TILE_H -#include -#include - -#include "joey/tile.h" #include "x68kPlanar.h" -// 8-row unrolled tile access (NATIVE-PERF Phase 2 R8): row offsets -// are compile-time constants row * 40, so each row is one d16(An) -// access with no pointer stepping and no loop control. gcc-amigaos -// -O2 keeps the equivalent row loops rolled as dbf (objdump-verified, -// ~18 cycles/row of pure loop overhead). Only the row dimension -// unrolls -- the 4-plane loop keeps real per-plane setup and stays -// register-light per the m68k-gcc-unroll-backfire lesson. -#define AMIGA_TILE_ROW_OFF(_r) ((uint16_t)((_r) * X68K_BYTES_PER_ROW)) +#define PLANAR_TILE_DATA_T X68kPlanarT +#define PLANAR_TILE_BYTES_PER_ROW X68K_BYTES_PER_ROW +#define PLANAR_TILE_PLANES X68K_BITPLANES -// Phase 5 planar dual-write for tile ops, fully planar after Phase 9 -// dropped the chunky shadow. All tiles are 8-pixel aligned (8x8 blocks -// at multiples of 8), so plane writes are byte-aligned -- one plane -// byte per row, 8 rows per tile, no edge masks. Stride between rows -// in a plane is X68K_BYTES_PER_ROW (40). - -static inline __attribute__((always_inline)) void x68kTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) { - X68kPlanarT *pd; - uint16_t plane; - uint8_t fillByte; - uint8_t *p; - - pd = (X68kPlanarT *)s->portData; - if (pd == NULL) { - jlpGenericTileFill(s, bx, by, colorIndex); - return; - } - for (plane = 0; plane < X68K_BITPLANES; plane++) { - fillByte = ((colorIndex >> plane) & 1u) ? 0xFFu : 0x00u; - p = pd->planes[plane] + (uint16_t)by * 8u * X68K_BYTES_PER_ROW + bx; - p[AMIGA_TILE_ROW_OFF(0)] = fillByte; - p[AMIGA_TILE_ROW_OFF(1)] = fillByte; - p[AMIGA_TILE_ROW_OFF(2)] = fillByte; - p[AMIGA_TILE_ROW_OFF(3)] = fillByte; - p[AMIGA_TILE_ROW_OFF(4)] = fillByte; - p[AMIGA_TILE_ROW_OFF(5)] = fillByte; - p[AMIGA_TILE_ROW_OFF(6)] = fillByte; - p[AMIGA_TILE_ROW_OFF(7)] = fillByte; - } -} - - -static inline __attribute__((always_inline)) void x68kTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) { - X68kPlanarT *dstPd; - X68kPlanarT *srcPd; - uint16_t plane; - uint8_t *dp; - const uint8_t *sp; - - dstPd = (X68kPlanarT *)dst->portData; - srcPd = (X68kPlanarT *)src->portData; - if (dstPd == NULL || srcPd == NULL) { - jlpGenericTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy); - return; - } - for (plane = 0; plane < X68K_BITPLANES; plane++) { - dp = dstPd->planes[plane] + (uint16_t)dstBy * 8u * X68K_BYTES_PER_ROW + dstBx; - sp = srcPd->planes[plane] + (uint16_t)srcBy * 8u * X68K_BYTES_PER_ROW + srcBx; - dp[AMIGA_TILE_ROW_OFF(0)] = sp[AMIGA_TILE_ROW_OFF(0)]; - dp[AMIGA_TILE_ROW_OFF(1)] = sp[AMIGA_TILE_ROW_OFF(1)]; - dp[AMIGA_TILE_ROW_OFF(2)] = sp[AMIGA_TILE_ROW_OFF(2)]; - dp[AMIGA_TILE_ROW_OFF(3)] = sp[AMIGA_TILE_ROW_OFF(3)]; - dp[AMIGA_TILE_ROW_OFF(4)] = sp[AMIGA_TILE_ROW_OFF(4)]; - dp[AMIGA_TILE_ROW_OFF(5)] = sp[AMIGA_TILE_ROW_OFF(5)]; - dp[AMIGA_TILE_ROW_OFF(6)] = sp[AMIGA_TILE_ROW_OFF(6)]; - dp[AMIGA_TILE_ROW_OFF(7)] = sp[AMIGA_TILE_ROW_OFF(7)]; - } -} - - -// Pure-planar masked copy. For each row of the 8x8 tile, read 4 src -// plane bytes; compute a per-pixel "non-transparent" mask via XOR -// against the transparent index's per-plane bit pattern (a pixel -// matches transparent IFF all 4 plane bits match transparent's 4 -// bits = OR of 4 XOR'd bytes is 0 in that bit). Then for each plane, -// dst = (dst & ~mask) | (src & mask) -- copy src bits at mask-set -// positions, preserve dst bits elsewhere. -static inline __attribute__((always_inline)) void x68kTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparent) { - X68kPlanarT *dstPd; - X68kPlanarT *srcPd; - uint8_t transBitByte[X68K_BITPLANES]; - uint16_t i; - uint8_t row; - uint16_t srcByteOff; - uint16_t dstByteOff; - uint8_t srcPlaneBytes[X68K_BITPLANES]; - uint8_t maskByte; - - dstPd = (X68kPlanarT *)dst->portData; - srcPd = (X68kPlanarT *)src->portData; - if (dstPd == NULL || srcPd == NULL) { - jlpGenericTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparent); - return; - } - transparent = (uint8_t)(transparent & 0x0Fu); - /* Per-plane "all bits set if transparent's bit at this plane is 1 - * else all 0" -- so XOR gives bit set where pixel differs from - * transparent in that plane. */ - for (i = 0; i < X68K_BITPLANES; i++) { - transBitByte[i] = ((transparent >> i) & 1u) ? 0xFFu : 0x00u; - } - - for (row = 0; row < 8u; row++) { - srcByteOff = (uint16_t)((uint16_t)srcBy * 8u + row) * X68K_BYTES_PER_ROW + srcBx; - dstByteOff = (uint16_t)((uint16_t)dstBy * 8u + row) * X68K_BYTES_PER_ROW + dstBx; - srcPlaneBytes[0] = srcPd->planes[0][srcByteOff]; - srcPlaneBytes[1] = srcPd->planes[1][srcByteOff]; - srcPlaneBytes[2] = srcPd->planes[2][srcByteOff]; - srcPlaneBytes[3] = srcPd->planes[3][srcByteOff]; - /* maskByte: bit set where pixel differs from transparent in - * ANY plane -- i.e., where the pixel is non-transparent. */ - maskByte = (uint8_t)((srcPlaneBytes[0] ^ transBitByte[0]) - | (srcPlaneBytes[1] ^ transBitByte[1]) - | (srcPlaneBytes[2] ^ transBitByte[2]) - | (srcPlaneBytes[3] ^ transBitByte[3])); - for (i = 0; i < X68K_BITPLANES; i++) { - uint8_t existing = dstPd->planes[i][dstByteOff]; - dstPd->planes[i][dstByteOff] = (uint8_t)((existing & (uint8_t)~maskByte) - | (srcPlaneBytes[i] & maskByte)); - } - } -} - - -// Phase 9 plane-to-chunky derivation for jlTileSnap. Reads 8 plane -// bytes (1 byte per row x 8 rows) from each of 4 planes for the -// 8-pixel-aligned tile column at bx, then assembles 32 chunky bytes -// (4 per row x 8 rows, packed 2 px/byte high-then-low nibble) into -// On Amiga, jlTileT.pixels is opaque port-specific storage (cross- -// platform tile.c never reads it directly when s->pixels is NULL). -// We use those 32 bytes as 4 planes x 8 rows, plane-major: -// bytes [0..7] = plane 0, rows 0..7 -// bytes [8..15] = plane 1, rows 0..7 -// bytes [16..23] = plane 2, rows 0..7 -// bytes [24..31] = plane 3, rows 0..7 -// snap/paste then become 32 plain byte loads + stores -- no chunky -// <-> planar conversion at all. The previous c2p-based path paid -// 4 KB LUT lookups + bit shuffling per pixel; this is ~50x cheaper. -#define AMIGA_TILE_PLANE_STRIDE 8 -static inline __attribute__((always_inline)) void x68kTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut) { - X68kPlanarT *pd; - uint16_t rowBase; - uint8_t plane; - - pd = (X68kPlanarT *)src->portData; - if (pd == NULL) { - jlpGenericTileSnap(src, bx, by, chunkyOut); - return; - } - rowBase = (uint16_t)((uint16_t)by * 8u) * X68K_BYTES_PER_ROW + bx; - for (plane = 0; plane < X68K_BITPLANES; plane++) { - const uint8_t *p = pd->planes[plane] + rowBase; - uint8_t *q = chunkyOut + plane * AMIGA_TILE_PLANE_STRIDE; - q[0] = p[AMIGA_TILE_ROW_OFF(0)]; - q[1] = p[AMIGA_TILE_ROW_OFF(1)]; - q[2] = p[AMIGA_TILE_ROW_OFF(2)]; - q[3] = p[AMIGA_TILE_ROW_OFF(3)]; - q[4] = p[AMIGA_TILE_ROW_OFF(4)]; - q[5] = p[AMIGA_TILE_ROW_OFF(5)]; - q[6] = p[AMIGA_TILE_ROW_OFF(6)]; - q[7] = p[AMIGA_TILE_ROW_OFF(7)]; - } -} - - -static inline __attribute__((always_inline)) void x68kTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) { - X68kPlanarT *pd; - uint8_t plane; - uint16_t rowBase; - - pd = (X68kPlanarT *)dst->portData; - if (pd == NULL) { - jlpGenericTilePaste(dst, bx, by, chunkyTile); - return; - } - /* jlTileT.pixels[] holds plane-major planar bytes (see - * jlpTileSnap header for layout). Paste = 32 byte stores - * with no chunky -> planar conversion. */ - rowBase = (uint16_t)((uint16_t)by * 8u) * X68K_BYTES_PER_ROW + bx; - for (plane = 0; plane < X68K_BITPLANES; plane++) { - const uint8_t *q = chunkyTile + plane * AMIGA_TILE_PLANE_STRIDE; - uint8_t *p = pd->planes[plane] + rowBase; - p[AMIGA_TILE_ROW_OFF(0)] = q[0]; - p[AMIGA_TILE_ROW_OFF(1)] = q[1]; - p[AMIGA_TILE_ROW_OFF(2)] = q[2]; - p[AMIGA_TILE_ROW_OFF(3)] = q[3]; - p[AMIGA_TILE_ROW_OFF(4)] = q[4]; - p[AMIGA_TILE_ROW_OFF(5)] = q[5]; - p[AMIGA_TILE_ROW_OFF(6)] = q[6]; - p[AMIGA_TILE_ROW_OFF(7)] = q[7]; - } -} - - -// Whole-map paste engine (NATIVE-PERF W3 item 9). jlTileMapPaste's -// shared whole-map arm calls this ONCE per map: the portData load + -// NULL test, the by*320 row-base derive, and the four plane base -// pointers are hoisted out of the per-cell loop -- the per-cell -// x68kTilePaste route reloads portData, rebuilds the cell offset, -// and re-reads pd->planes[0..3] from the struct every cell (~172 of -// its ~776 cyc/cell, objdump-measured). The four dst pointers stay -// resident in a0-a3 (one byte per column, one row stride per tile -// row), so each cell is 32 move.b off compile-time displacements -// with zero per-plane setup. Plane order, row order, and written -// values are identical to per-cell x68kTilePaste -- pixel output is -// byte-identical. movep is unusable here (plane bytes sit 8000 bytes -// apart across separate AllocMem planes, vs the ST's interleaved +2 -// layout), so C is the right weapon on this port. -#define AMIGA_TILE_MAP_ROW_STRIDE ((uint16_t)(TILE_PIXELS_PER_SIDE * X68K_BYTES_PER_ROW)) - -#define AMIGA_MAP_CELL_PLANE(_dp, _plane) \ - do { \ - (_dp)[AMIGA_TILE_ROW_OFF(0)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 0]; \ - (_dp)[AMIGA_TILE_ROW_OFF(1)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 1]; \ - (_dp)[AMIGA_TILE_ROW_OFF(2)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 2]; \ - (_dp)[AMIGA_TILE_ROW_OFF(3)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 3]; \ - (_dp)[AMIGA_TILE_ROW_OFF(4)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 4]; \ - (_dp)[AMIGA_TILE_ROW_OFF(5)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 5]; \ - (_dp)[AMIGA_TILE_ROW_OFF(6)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 6]; \ - (_dp)[AMIGA_TILE_ROW_OFF(7)] = src[(_plane) * AMIGA_TILE_PLANE_STRIDE + 7]; \ - } while (0) - -static inline __attribute__((always_inline)) void x68kTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map) { - X68kPlanarT *pd; - const uint8_t *tileBytes; - const uint8_t *src; - uint8_t *p0; - uint8_t *p1; - uint8_t *p2; - uint8_t *p3; - uint16_t rowBase; - uint16_t rewind; - uint8_t idx; - uint8_t ty; - uint8_t tx; - - tileBytes = (const uint8_t *)tiles; - pd = (X68kPlanarT *)dst->portData; - if (pd == NULL) { - for (ty = 0; ty < hTiles; ty++) { - for (tx = 0; tx < wTiles; tx++) { - idx = map[tx]; - if (idx != TILE_MAP_SKIP) { - jlpGenericTilePaste(dst, (uint8_t)(bx + tx), (uint8_t)(by + ty), tileBytes + ((uint16_t)idx << 5)); - } - } - map += wTiles; - } - return; - } - rowBase = (uint16_t)((uint16_t)by * AMIGA_TILE_MAP_ROW_STRIDE + bx); - rewind = (uint16_t)(AMIGA_TILE_MAP_ROW_STRIDE - wTiles); - p0 = pd->planes[0] + rowBase; - p1 = pd->planes[1] + rowBase; - p2 = pd->planes[2] + rowBase; - p3 = pd->planes[3] + rowBase; - for (ty = hTiles; ty != 0u; ty--) { - for (tx = wTiles; tx != 0u; tx--) { - idx = *map++; - if (idx != TILE_MAP_SKIP) { - src = tileBytes + ((uint16_t)idx << 5); - AMIGA_MAP_CELL_PLANE(p0, 0); - AMIGA_MAP_CELL_PLANE(p1, 1); - AMIGA_MAP_CELL_PLANE(p2, 2); - AMIGA_MAP_CELL_PLANE(p3, 3); - } - p0++; - p1++; - p2++; - p3++; - } - p0 += rewind; - p1 += rewind; - p2 += rewind; - p3 += rewind; - } -} -#undef AMIGA_MAP_CELL_PLANE -#undef AMIGA_TILE_MAP_ROW_STRIDE - - -// Planar monochrome paste. monoTile follows the cross-port mono -// contract (include/joey/tile.h + jlpGenericTilePasteMono): 32 chunky -// nibble-pair bytes, row-major, TILE_BYTES_PER_ROW bytes per row, two -// pixels per byte. A pixel renders fgColor when its source nibble is -// nonzero, bgColor when zero; the HIGH nibble is the LEFT pixel. Each -// row's 4 source bytes fold into an 8-bit shape mask (bit 7 = -// leftmost pixel, matching the planar bit order), then each plane k -// writes -// outPlaneK = (shape & maskFgK) | (~shape & maskBgK) -// where maskXK = $FF if (X & (1 << k)) else $00. -// Eight 1bpp shape rows into the bitplanes: set bits take the -// foreground's plane bit, clear bits the background's. Shared by the -// mono paste (which folds a chunky silhouette into these rows first) -// and the glyph paste (whose input already IS these rows). -static inline __attribute__((always_inline)) void x68kTilePasteShape(X68kPlanarT *pd, uint8_t bx, uint8_t by, const uint8_t *shape, uint8_t fgColor, uint8_t bgColor) { - uint16_t rowBase = (uint16_t)((uint16_t)by * 8u) * X68K_BYTES_PER_ROW + bx; - uint8_t plane; - uint8_t row; - - for (plane = 0; plane < X68K_BITPLANES; plane++) { - uint8_t maskFg = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u); - uint8_t maskBg = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u); - uint8_t *p = pd->planes[plane] + rowBase; - for (row = 0; row < 8u; row++) { - uint8_t s = shape[row]; - p[row * X68K_BYTES_PER_ROW] = (uint8_t)((uint8_t)(s & maskFg) | (uint8_t)((uint8_t)(~s) & maskBg)); - } - } -} - - -static inline __attribute__((always_inline)) void x68kTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) { - X68kPlanarT *pd; - uint8_t shape[TILE_PIXELS_PER_SIDE]; - uint8_t row; - uint8_t col; - - pd = (X68kPlanarT *)dst->portData; - if (pd == NULL) { - jlpGenericTilePasteMono(dst, bx, by, monoTile, fgColor, bgColor); - return; - } - // Fold each chunky nibble-pair row into one 1bpp shape byte. - for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) { - uint8_t bits = 0u; - for (col = 0; col < TILE_BYTES_PER_ROW; col++) { - uint8_t srcByte = *monoTile++; - bits = (uint8_t)(bits << 2); - if (srcByte & 0xF0u) { - bits = (uint8_t)(bits | 0x02u); - } - if (srcByte & 0x0Fu) { - bits = (uint8_t)(bits | 0x01u); - } - } - shape[row] = bits; - } - x68kTilePasteShape(pd, bx, by, shape, fgColor, bgColor); -} - - -// 1bpp glyph rows straight to the planes: a glyph byte IS the row shape -// the mono paste has to fold out of a chunky silhouette, so this skips -// the fold entirely (and cannot be handed the wrong tile format). -static inline __attribute__((always_inline)) void x68kTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *bits, uint8_t fgColor, uint8_t bgColor) { - X68kPlanarT *pd = (X68kPlanarT *)dst->portData; - - if (pd == NULL) { - jlpGenericTilePasteGlyph(dst, bx, by, bits, fgColor, bgColor); - return; - } - x68kTilePasteShape(pd, bx, by, bits, fgColor, bgColor); -} +#include "planarTile.h" #endif diff --git a/tests/host/inputHost.c b/tests/host/inputHost.c index bf5e6ed..86362af 100644 --- a/tests/host/inputHost.c +++ b/tests/host/inputHost.c @@ -85,14 +85,14 @@ static void pushAll(const uint8_t *chars, int count) { } -// Control characters other than BS/TAB/CR/ESC, plus DEL and 8-bit -// values, must never surface; the documented four and the printable -// range must pass through unchanged. +// Control characters other than BS/TAB/CR/ESC, and 8-bit values, must +// never surface; those four, the printable range and JL_CHAR_DELETE +// (0x7F, forward delete) must pass through unchanged. static bool runFilter(void) { - static const uint8_t rejected[] = { 0x00, 0x01, 0x07, 0x0A, 0x0C, 0x1F, 0x7F }; + static const uint8_t rejected[] = { 0x00, 0x01, 0x07, 0x0A, 0x0C, 0x1F, 0x80, 0xA0, 0xFF }; static const uint8_t accepted[] = { JL_CHAR_BACKSPACE, JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE, - 0x20, '0', 'Z', 'z', '~', 0x7E + 0x20, '0', 'Z', 'z', '~', 0x7E, JL_CHAR_DELETE }; bool ok; diff --git a/tests/host/tileBank.png b/tests/host/tileBank.png new file mode 100644 index 0000000..a2001e6 --- /dev/null +++ b/tests/host/tileBank.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b227380e692749dca4f97af54ee35a7baa6fd21418c33484e797d7b6a1992a67 +size 1928 diff --git a/tests/host/tileBankHost.c b/tests/host/tileBankHost.c new file mode 100644 index 0000000..66de522 --- /dev/null +++ b/tests/host/tileBankHost.c @@ -0,0 +1,503 @@ +// tileBankHost.c - Host-cc gate for the baked tile-bank loaders, +// jlTileBankLoad and jlTileBankLoadToSurface (src/core/assetLoad.c). +// +// Built by scripts/check-tilebank.sh as a whole BLANK-port program (core + +// generics + src/blank/blank.c + this file), so the REAL loader feeds the +// REAL generic tile paste and every pixel is read back through the public +// jlSamplePixel. BLANK draws chunky 4bpp like DOS and assetLoad.c expects the +// DOS target byte there, so the script stages tests/host/tileBank.png with +// tests/host/tileBankStage.py for target dos: the DOS bake is the bank that +// must load, and every other assetbake target's bake must be refused. +// +// Runs in the script's temp directory, whose DATA/ holds (the staging +// script's names and layout, shared with examples/tilebank): +// own.tbk the DOS bake of tileBank.png +// refuse.tbk one bake per other target; their names arrive as argv[1..] +// expect.bin the PNG decoded by PIL, independent of assetbake's +// encoders: tileCount LE16, palette[16] LE16 $0RGB, then +// tileCount * 64 colour indices (tiles in bank order, each +// row-major) +// The malformed banks (bad magic, short header, truncated payload, no +// palette, more tiles than a surface holds) are cut from own.tbk here, so +// their parameters live beside the checks that depend on them. +// +// Prints one line per check and a final tally; exits nonzero on any failure. + +#include +#include +#include + +#include "joey/joey.h" +#include "joey/file.h" + +// ----- Constants ----- + +#define BAD_MAGIC_BANK "badmagic.tbk" +#define BIG_BANK "big.tbk" +#define EXPECTED_FILE "expect.bin" +#define GOOD_BANK "own.tbk" +#define MISSING_BANK "missing.tbk" +#define NO_PALETTE_BANK "nopal.tbk" +#define SHORT_HEADER_BANK "shorthdr.tbk" +#define TRUNCATED_BANK "trunc.tbk" + +// .tbk header fields (the format is documented in tools/assetbake/assetbake.py). +#define TBK_HEADER_BYTES 44 +#define TBK_OFF_MAGIC 0 +#define TBK_OFF_HAS_PALETTE 5 +#define TBK_OFF_TILE_COUNT 6 + +#define LE16_BYTES 2 +#define PALETTE_COLORS SURFACE_COLORS_PER_PALETTE +#define TILE_PIXELS (TILE_PIXELS_PER_SIDE * TILE_PIXELS_PER_SIDE) +#define SURFACE_TILE_BLOCKS (TILE_BLOCKS_PER_ROW * TILE_BLOCKS_PER_COL) + +// expect.bin layout (see the header comment). +#define EXPECTED_OFF_PALETTE LE16_BYTES +#define EXPECTED_OFF_PIXELS (EXPECTED_OFF_PALETTE + PALETTE_COLORS * LE16_BYTES) +#define EXPECTED_MAX_TILES SURFACE_TILE_BLOCKS + +// One tile more than a surface holds, so jlTileBankLoadToSurface's 40x25 +// cap is exercised. Also the capacity of the jlTileT buffer every load uses. +#define BIG_BANK_TILES (SURFACE_TILE_BLOCKS + 1) +#define TILE_CAPACITY BIG_BANK_TILES + +#define BAD_MAGIC_BYTE 'X' +#define FILL_COLOR 9u // pre-paste colour; blocks past a bank must keep it (examples/tilebank clears to the same, so its hash can match ours) +#define PALETTE_SENTINEL 0xBEEFu +#define SHORT_HEADER_BYTES (TBK_HEADER_BYTES / 2) +#define SMALL_MAX_TILES 5u +#define TILE_SENTINEL 0xA5u +#define TRUNCATED_EXTRA_BYTES (TILE_BYTES / 2) // half a tile after the last whole one +#define TRUNCATED_TILES 10u + +// ----- Prototypes ----- + +static void check(const char *bank, const char *what, bool ok); +static bool craftBanks(void); +static bool loadExpected(void); +static bool paletteIsExpected(void); +static bool paletteUntouched(void); +static void pasteLoadedTiles(uint16_t count); +static uint16_t readLe16(const uint8_t *bytes); +static void resetBuffers(void); +static bool surfaceMatches(uint16_t tilesPasted); +static void testArgumentGuards(void); +static void testBigBank(void); +static void testGoodBank(void); +static void testMaxTiles(void); +static void testNoPalette(void); +static void testRefused(const char *bank); +static void testToSurface(void); +static void testTruncated(void); +static bool tilesUntouched(uint16_t first, uint16_t end); +static bool validRangeIs(uint16_t first, uint16_t end, bool want); +static bool writeBank(const char *name, uint32_t length); +static void writeLe16(uint8_t *bytes, uint16_t value); + +// ----- State ----- + +static uint16_t gExpectedCount; +static uint16_t gExpectedPalette[PALETTE_COLORS]; +static uint8_t gExpectedPixels[EXPECTED_MAX_TILES * TILE_PIXELS]; +static uint16_t gFail = 0u; +static uint8_t gFile[TBK_HEADER_BYTES + BIG_BANK_TILES * TILE_BYTES]; +static uint16_t gPalette[PALETTE_COLORS]; +static uint16_t gPass = 0u; +static jlSurfaceT *gSurface = NULL; +static jlTileT gTiles[TILE_CAPACITY]; +static bool gValid[TILE_CAPACITY]; + + +static void check(const char *bank, const char *what, bool ok) { + if (ok) { + gPass++; + printf(" ok %s: %s\n", bank, what); + } else { + gFail++; + printf(" FAIL %s: %s\n", bank, what); + } +} + + +// Cut the malformed banks from own.tbk (see the header comment). The big +// bank goes last because it rewrites gFile's count and extends its payload. +static bool craftBanks(void) { + FILE *fp; + uint32_t goodLength; + uint16_t i; + uint8_t saved; + bool ok; + + fp = jlDataOpen(GOOD_BANK, "rb"); + if (fp == NULL) { + return false; + } + goodLength = (uint32_t)fread(gFile, 1, sizeof(gFile), fp); + fclose(fp); + if (goodLength != TBK_HEADER_BYTES + (uint32_t)gExpectedCount * TILE_BYTES) { + return false; + } + if (!writeBank(SHORT_HEADER_BANK, SHORT_HEADER_BYTES)) { + return false; + } + if (!writeBank(TRUNCATED_BANK, TBK_HEADER_BYTES + TRUNCATED_TILES * TILE_BYTES + TRUNCATED_EXTRA_BYTES)) { + return false; + } + + saved = gFile[TBK_OFF_MAGIC]; + gFile[TBK_OFF_MAGIC] = BAD_MAGIC_BYTE; + ok = writeBank(BAD_MAGIC_BANK, goodLength); + gFile[TBK_OFF_MAGIC] = saved; + if (!ok) { + return false; + } + + saved = gFile[TBK_OFF_HAS_PALETTE]; + gFile[TBK_OFF_HAS_PALETTE] = 0u; + ok = writeBank(NO_PALETTE_BANK, goodLength); + gFile[TBK_OFF_HAS_PALETTE] = saved; + if (!ok) { + return false; + } + + // Tile i of the big bank repeats tile (i % tileCount) of the good one. + for (i = gExpectedCount; i < BIG_BANK_TILES; i++) { + memcpy(&gFile[TBK_HEADER_BYTES + (uint32_t)i * TILE_BYTES], &gFile[TBK_HEADER_BYTES + (uint32_t)(i % gExpectedCount) * TILE_BYTES], TILE_BYTES); + } + writeLe16(&gFile[TBK_OFF_TILE_COUNT], BIG_BANK_TILES); + return writeBank(BIG_BANK, TBK_HEADER_BYTES + (uint32_t)BIG_BANK_TILES * TILE_BYTES); +} + + +// Read expect.bin. The fixture must span more than one block row (so the +// 40-block wrap is exercised) and fewer blocks than a surface (so blocks past +// the bank can be seen keeping their prior content). +static bool loadExpected(void) { + FILE *fp; + uint8_t head[EXPECTED_OFF_PIXELS]; + uint32_t pixelBytes; + uint16_t i; + bool ok; + + fp = jlDataOpen(EXPECTED_FILE, "rb"); + if (fp == NULL) { + return false; + } + ok = fread(head, 1, sizeof(head), fp) == sizeof(head); + if (ok) { + gExpectedCount = readLe16(head); + ok = gExpectedCount > TILE_BLOCKS_PER_ROW && gExpectedCount < SURFACE_TILE_BLOCKS; + } + if (ok) { + for (i = 0; i < PALETTE_COLORS; i++) { + gExpectedPalette[i] = readLe16(&head[EXPECTED_OFF_PALETTE + i * LE16_BYTES]); + } + pixelBytes = (uint32_t)gExpectedCount * TILE_PIXELS; + ok = fread(gExpectedPixels, 1, pixelBytes, fp) == pixelBytes && fgetc(fp) == EOF; + } + fclose(fp); + return ok; +} + + +static bool paletteIsExpected(void) { + return memcmp(gPalette, gExpectedPalette, sizeof(gPalette)) == 0; +} + + +static bool paletteUntouched(void) { + uint16_t i; + + for (i = 0; i < PALETTE_COLORS; i++) { + if (gPalette[i] != PALETTE_SENTINEL) { + return false; + } + } + return true; +} + + +// Clear the surface to FILL_COLOR, then paste gTiles[0..count) in the same +// row-major block order jlTileBankLoadToSurface uses. +static void pasteLoadedTiles(uint16_t count) { + uint16_t i; + + jlSurfaceClear(gSurface, FILL_COLOR); + for (i = 0; i < count; i++) { + jlTilePaste(gSurface, (uint8_t)(i % TILE_BLOCKS_PER_ROW), (uint8_t)(i / TILE_BLOCKS_PER_ROW), &gTiles[i]); + } +} + + +static uint16_t readLe16(const uint8_t *bytes) { + return (uint16_t)(bytes[0] | (bytes[1] << CHAR_BIT)); +} + + +// Poison every output so a check can tell written slots from untouched ones: +// tiles and palette get sentinels, and outValid is preset TRUE so a loader +// that fails to clear it is caught. +static void resetBuffers(void) { + uint16_t i; + + memset(gTiles, TILE_SENTINEL, sizeof(gTiles)); + for (i = 0; i < TILE_CAPACITY; i++) { + gValid[i] = true; + } + for (i = 0; i < PALETTE_COLORS; i++) { + gPalette[i] = PALETTE_SENTINEL; + } +} + + +// Every pixel of gSurface: blocks below tilesPasted must show the PNG's tile +// (block % tileCount), every other block FILL_COLOR. +static bool surfaceMatches(uint16_t tilesPasted) { + int16_t x; + int16_t y; + uint16_t block; + uint8_t want; + uint8_t got; + + for (y = 0; y < SURFACE_HEIGHT; y++) { + for (x = 0; x < SURFACE_WIDTH; x++) { + block = (uint16_t)((y / TILE_PIXELS_PER_SIDE) * TILE_BLOCKS_PER_ROW + x / TILE_PIXELS_PER_SIDE); + want = FILL_COLOR; + if (block < tilesPasted) { + want = gExpectedPixels[(uint32_t)(block % gExpectedCount) * TILE_PIXELS + (y % TILE_PIXELS_PER_SIDE) * TILE_PIXELS_PER_SIDE + x % TILE_PIXELS_PER_SIDE]; + } + got = jlSamplePixel(gSurface, x, y); + if (got != want) { + printf(" first mismatch at (%d,%d): got %u, want %u\n", x, y, got, want); + return false; + } + } + } + return true; +} + + +static void testArgumentGuards(void) { + resetBuffers(); + check("args", "NULL path loads nothing", jlTileBankLoad(NULL, gTiles, TILE_CAPACITY, gValid, gPalette) == 0u); + check("args", "NULL tile buffer loads nothing", jlTileBankLoad(GOOD_BANK, NULL, TILE_CAPACITY, gValid, gPalette) == 0u); + check("args", "maxTiles 0 loads nothing", jlTileBankLoad(GOOD_BANK, gTiles, 0u, gValid, gPalette) == 0u); + check("args", "no tile written by any of those", tilesUntouched(0u, TILE_CAPACITY)); + check("args", "to-surface NULL path pastes nothing", jlTileBankLoadToSurface(NULL, gSurface, gPalette) == 0u); + check("args", "to-surface NULL surface pastes nothing", jlTileBankLoadToSurface(GOOD_BANK, NULL, gPalette) == 0u); +} + + +static void testBigBank(void) { + uint16_t loaded; + uint16_t last; + + resetBuffers(); + loaded = jlTileBankLoad(BIG_BANK, gTiles, TILE_CAPACITY, gValid, gPalette); + check(BIG_BANK, "loads more tiles than a surface holds", loaded == BIG_BANK_TILES); + check(BIG_BANK, "valid true for every tile", validRangeIs(0u, BIG_BANK_TILES, true)); + pasteLoadedTiles(SURFACE_TILE_BLOCKS); + check(BIG_BANK, "a surface-full of loaded tiles matches the PNG", surfaceMatches(SURFACE_TILE_BLOCKS)); + last = BIG_BANK_TILES - 1u; + check(BIG_BANK, "the tile past a surface-full matches the PNG", memcmp(&gTiles[last], &gTiles[last % gExpectedCount], sizeof(jlTileT)) == 0); + + jlSurfaceClear(gSurface, FILL_COLOR); + loaded = jlTileBankLoadToSurface(BIG_BANK, gSurface, gPalette); + check(BIG_BANK, "to-surface stops at a full surface", loaded == SURFACE_TILE_BLOCKS); + check(BIG_BANK, "to-surface fills the surface from the PNG", surfaceMatches(SURFACE_TILE_BLOCKS)); +} + + +static void testGoodBank(void) { + uint16_t loaded; + + resetBuffers(); + loaded = jlTileBankLoad(GOOD_BANK, gTiles, TILE_CAPACITY, gValid, gPalette); + check(GOOD_BANK, "loads every tile", loaded == gExpectedCount); + check(GOOD_BANK, "valid true for the loaded slots", validRangeIs(0u, gExpectedCount, true)); + check(GOOD_BANK, "valid false past the bank", validRangeIs(gExpectedCount, TILE_CAPACITY, false)); + check(GOOD_BANK, "slots past the bank untouched", tilesUntouched(gExpectedCount, TILE_CAPACITY)); + check(GOOD_BANK, "palette matches the PNG", paletteIsExpected()); + pasteLoadedTiles(gExpectedCount); + check(GOOD_BANK, "pasted tiles match the PNG", surfaceMatches(gExpectedCount)); + + resetBuffers(); + loaded = jlTileBankLoad(GOOD_BANK, gTiles, TILE_CAPACITY, NULL, NULL); + check(GOOD_BANK, "loads with NULL outValid and outPalette", loaded == gExpectedCount); +} + + +static void testMaxTiles(void) { + uint16_t loaded; + + resetBuffers(); + loaded = jlTileBankLoad(GOOD_BANK, gTiles, SMALL_MAX_TILES, gValid, gPalette); + check(GOOD_BANK, "a small maxTiles stops the load there", loaded == SMALL_MAX_TILES); + check(GOOD_BANK, "valid true up to maxTiles", validRangeIs(0u, SMALL_MAX_TILES, true)); + check(GOOD_BANK, "valid past maxTiles untouched", validRangeIs(SMALL_MAX_TILES, TILE_CAPACITY, true)); + check(GOOD_BANK, "tiles past maxTiles untouched", tilesUntouched(SMALL_MAX_TILES, TILE_CAPACITY)); + pasteLoadedTiles(SMALL_MAX_TILES); + check(GOOD_BANK, "the maxTiles tiles match the PNG", surfaceMatches(SMALL_MAX_TILES)); +} + + +static void testNoPalette(void) { + uint16_t loaded; + + resetBuffers(); + loaded = jlTileBankLoad(NO_PALETTE_BANK, gTiles, TILE_CAPACITY, gValid, gPalette); + check(NO_PALETTE_BANK, "loads every tile", loaded == gExpectedCount); + check(NO_PALETTE_BANK, "palette untouched", paletteUntouched()); + + jlSurfaceClear(gSurface, FILL_COLOR); + loaded = jlTileBankLoadToSurface(NO_PALETTE_BANK, gSurface, gPalette); + check(NO_PALETTE_BANK, "to-surface pastes every tile", loaded == gExpectedCount); + check(NO_PALETTE_BANK, "to-surface palette untouched", paletteUntouched()); +} + + +// A refused bank loads nothing, clears outValid, and leaves the tiles, the +// palette and the surface alone. +static void testRefused(const char *bank) { + uint16_t loaded; + + resetBuffers(); + loaded = jlTileBankLoad(bank, gTiles, TILE_CAPACITY, gValid, gPalette); + check(bank, "refused (loads nothing)", loaded == 0u); + check(bank, "valid false everywhere", validRangeIs(0u, TILE_CAPACITY, false)); + check(bank, "tiles untouched", tilesUntouched(0u, TILE_CAPACITY)); + check(bank, "palette untouched", paletteUntouched()); + + jlSurfaceClear(gSurface, FILL_COLOR); + loaded = jlTileBankLoadToSurface(bank, gSurface, gPalette); + check(bank, "to-surface refused (pastes nothing)", loaded == 0u); + check(bank, "to-surface surface untouched", surfaceMatches(0u)); + check(bank, "to-surface palette untouched", paletteUntouched()); +} + + +static void testToSurface(void) { + uint16_t loaded; + + resetBuffers(); + jlSurfaceClear(gSurface, FILL_COLOR); + loaded = jlTileBankLoadToSurface(GOOD_BANK, gSurface, gPalette); + check(GOOD_BANK, "to-surface pastes every tile", loaded == gExpectedCount); + check(GOOD_BANK, "to-surface palette matches the PNG", paletteIsExpected()); + check(GOOD_BANK, "to-surface pixels match the PNG, later blocks keep theirs", surfaceMatches(gExpectedCount)); + // scripts/verify--tilebank.sh read this line: each port's own bake, + // drawn by examples/tilebank, must hash the same. + printf(" info %s: to-surface hash %08lX (a port loading its own bake must match)\n", GOOD_BANK, (unsigned long)jlSurfaceHash(gSurface)); + + jlSurfaceClear(gSurface, FILL_COLOR); + loaded = jlTileBankLoadToSurface(GOOD_BANK, gSurface, NULL); + check(GOOD_BANK, "to-surface pastes with NULL outPalette", loaded == gExpectedCount); +} + + +static void testTruncated(void) { + uint16_t loaded; + + resetBuffers(); + loaded = jlTileBankLoad(TRUNCATED_BANK, gTiles, TILE_CAPACITY, gValid, gPalette); + check(TRUNCATED_BANK, "loads only the whole tiles", loaded == TRUNCATED_TILES); + check(TRUNCATED_BANK, "valid true for the whole tiles", validRangeIs(0u, TRUNCATED_TILES, true)); + check(TRUNCATED_BANK, "valid false from the cut on", validRangeIs(TRUNCATED_TILES, TILE_CAPACITY, false)); + pasteLoadedTiles(TRUNCATED_TILES); + check(TRUNCATED_BANK, "the whole tiles match the PNG", surfaceMatches(TRUNCATED_TILES)); + + jlSurfaceClear(gSurface, FILL_COLOR); + loaded = jlTileBankLoadToSurface(TRUNCATED_BANK, gSurface, gPalette); + check(TRUNCATED_BANK, "to-surface pastes only the whole tiles", loaded == TRUNCATED_TILES); + check(TRUNCATED_BANK, "to-surface leaves the cut tile's block alone", surfaceMatches(TRUNCATED_TILES)); +} + + +static bool tilesUntouched(uint16_t first, uint16_t end) { + uint16_t i; + uint16_t b; + + for (i = first; i < end; i++) { + for (b = 0; b < TILE_BYTES; b++) { + if (gTiles[i].pixels[b] != TILE_SENTINEL) { + return false; + } + } + } + return true; +} + + +static bool validRangeIs(uint16_t first, uint16_t end, bool want) { + uint16_t i; + + for (i = first; i < end; i++) { + if (gValid[i] != want) { + return false; + } + } + return true; +} + + +static bool writeBank(const char *name, uint32_t length) { + FILE *fp; + bool ok; + + fp = jlDataOpen(name, "wb"); + if (fp == NULL) { + return false; + } + ok = fwrite(gFile, 1, length, fp) == length; + return (fclose(fp) == 0) && ok; +} + + +static void writeLe16(uint8_t *bytes, uint16_t value) { + bytes[0] = (uint8_t)value; + bytes[1] = (uint8_t)(value >> CHAR_BIT); +} + + +int main(int argc, char **argv) { + jlConfigT config; + int i; + bool ready; + + printf("tilebank-host: jlTileBankLoad + jlTileBankLoadToSurface on the BLANK port (real files)\n"); + memset(&config, 0, sizeof(config)); + if (!jlInit(&config)) { + printf("tilebank-host: jlInit failed\n"); + return 1; + } + gSurface = jlSurfaceCreate(); + ready = gSurface != NULL; + check("setup", "jlSurfaceCreate", ready); + ready = ready && loadExpected(); + check("setup", EXPECTED_FILE " read (fixture spans more than a block row, less than a surface)", ready); + ready = ready && craftBanks(); + check("setup", "malformed banks cut from " GOOD_BANK, ready); + check("setup", "other targets' bakes to refuse were passed in", argc > 1); + + if (ready) { + testArgumentGuards(); + testGoodBank(); + testMaxTiles(); + testToSurface(); + testTruncated(); + testNoPalette(); + testBigBank(); + for (i = 1; i < argc; i++) { + testRefused(argv[i]); + } + testRefused(BAD_MAGIC_BANK); + testRefused(SHORT_HEADER_BANK); + testRefused(MISSING_BANK); + } + + jlSurfaceDestroy(gSurface); + jlShutdown(); + printf("tilebank-host: %u passed, %u failed\n", (unsigned)gPass, (unsigned)gFail); + return (gFail == 0u) ? 0 : 1; +} diff --git a/tests/host/tileBankStage.py b/tests/host/tileBankStage.py new file mode 100644 index 0000000..399fb5b --- /dev/null +++ b/tests/host/tileBankStage.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# tileBankStage.py -- stage the tile-bank fixture for one target's check. +# +# Bakes an indexed PNG (tests/host/tileBank.png) with assetbake for TARGET and +# for every other target assetbake knows, and decodes the PNG to the pixels a +# correct load must show, all into OUTDIR: +# own.tbk the TARGET bake: the bank that must load +# refuse.tbk n = 1, 2, ...: every other target's bake, in TARGET_NAMES +# order; each must be refused. Read from assetbake's own +# TARGET_NAMES, so a new target is covered on arrival. +# expect.bin the PNG decoded here with PIL, independent of assetbake's +# encoders: tileCount LE16, the first 16 palette entries as +# LE16 $0RGB, then each tile's 64 colour indices (tiles +# row-major across the sheet, each tile row-major) +# +# Two readers share these names and this layout: tests/host/tileBankHost.c +# (the host gate, scripts/check-tilebank.sh, TARGET dos) and +# examples/tilebank/tilebank.c (make/.mk stages its DATA/tilebank with +# TARGET = the port; scripts/verify--tilebank.sh runs it). +# +# Usage: tileBankStage.py TARGET PNG OUTDIR + +import glob +import os +import struct +import sys + +from PIL import Image + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "tools", "assetbake")) +from assetbake import TARGET_NAMES, bakeTileBank # noqa: E402 + +TILE_PX = 8 +COLORS = 16 +NIBBLE_BITS = 4 +OWN_BANK = "own.tbk" +REFUSE_PATTERN = "refuse%d.tbk" +EXPECT_FILE = "expect.bin" + + +def writeExpected(pngPath, outPath): + img = Image.open(pngPath) + if img.mode != "P": + sys.exit("tileBankStage: %s is not an indexed PNG" % pngPath) + w, h = img.size + px = img.load() + raw = img.getpalette() or [] + out = bytearray(struct.pack("> NIBBLE_BITS for c in rgb) + out += struct.pack("