Tile generation tests.

This commit is contained in:
Scott Duensing 2026-09-29 17:23:29 -05:00
parent fe84be766f
commit 2c8d726ed7
84 changed files with 4137 additions and 2318 deletions

View file

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

View file

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

View file

@ -8,7 +8,6 @@ Declared in `<joey/core.h>`, included by `<joey/joey.h>`.
----
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

View file

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

View file

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

View file

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

View file

@ -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-<port>-tilebank.sh` gates run it on
every port.
=== Loading Assets at Run Time
[source,c]

View file

@ -154,6 +154,12 @@ against your own program too.
|`make iigs-verify`
|The IIgs actually reached the program and drew something.
|`scripts/verify-<port>-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-<port>.sh`
|Per-operation throughput for that port.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,10 +23,6 @@
#include <string.h>
#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

View file

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

View file

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

View file

@ -22,10 +22,6 @@
#include <string.h>
#ifdef JOEYLIB_PLATFORM_IIGS
#endif
#define GLYPH_W 8u
#define GLYPH_H 8u
#define FIRST_CHAR 0x20u // space

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -265,7 +265,6 @@ int main(void) {
#endif
config.codegenBytes = 0u;
config.audioBytes = 0u;
if (!jlInit(&config)) {
MB_BASE[MB_PHASE] = PHASE_FAILED;
for (;;) {

View file

@ -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 <joey/serial.h>, which the
// umbrella <joey/joey.h> deliberately does NOT pull in.
#include <joey/joey.h>
#include <joey/serial.h>
#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;
}

View file

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

BIN
examples/spacetaxi/assets/font.png (Stored with Git LFS)

Binary file not shown.

View file

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

Binary file not shown.

BIN
examples/spacetaxi/assets/tiles/tbank0.png (Stored with Git LFS)

Binary file not shown.

BIN
examples/spacetaxi/assets/tiles/tbank1.png (Stored with Git LFS)

Binary file not shown.

BIN
examples/spacetaxi/assets/tiles/tbank2.png (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

View file

@ -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-<labelN>.bin (VICE `save` output;
# 2-byte LE start-addr header + 64 KB RAM).
# Writes: examples/spacetaxi/assets/tiles/tilebank<N>.png
# examples/spacetaxi/assets/levels/level<NN>.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: <dump-path> <level-name-without-extension> <tilebank-id>
#
# 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 <char> <screencode> -- 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 <dump-path> <level-name> <tilebank-id>", 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()

View file

@ -983,7 +983,6 @@ int main(void) {
#else
config.codegenBytes = 160UL * 1024;
#endif
config.audioBytes = 32UL * 1024;
if (!jlInit(&config)) {
return 1;
}

View file

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

View file

@ -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/<port>.mk), from tests/host/tileBank.png:
// own.tbk this port's bake: the bank that must load
// refuse<n>.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<n>.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=<hash> pasteHash=<hash> refused=4/4
// which scripts/verify-<port>-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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <joey/joey.h>
#include <joey/file.h>
// ----- 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;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

200
joey-run
View file

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

View file

@ -17,7 +17,7 @@ BINDIR := $(BUILD)/bin
# independently. -I on $(SRC_DIR)/amiga lets ptplayer.h resolve
# <SDI_compiler.h> from the machine-local shim alongside our code.
PTPLAYER_DIR := $(AMIGA_PTPLAYER)
CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT -I$(SRC_DIR)/amiga -I$(SRC_68K) -I$(PTPLAYER_DIR) -MMD -MP $(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.
@ -78,6 +78,8 @@ 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
@ -96,11 +98,9 @@ STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLeve
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 $< $@

View file

@ -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
@ -65,6 +64,8 @@ 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
@ -84,7 +85,6 @@ 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 $< $@

View file

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

View file

@ -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 :=
@ -61,6 +60,8 @@ 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
@ -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/<port> -- 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 $< $@

View file

@ -64,6 +64,7 @@ SERTEST_SRC := $(EXAMPLES)/sertest/sertest.c
SCCPROBE_SRC := $(EXAMPLES)/sccprobe/sccprobe.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
@ -73,6 +74,11 @@ AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/ag
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
# disk at run time (see the $(BINDIR)/AUDIO rule). The source is assembled
@ -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)

View file

@ -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,13 +93,19 @@ 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.
# 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
@ -107,12 +113,15 @@ 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
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)

45
scripts/check-tilebank.sh Executable file
View file

@ -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/<port>.mk stages them for examples/tilebank on
# the real ports. The harness prints the surface hash of its to-surface load;
# scripts/verify-<port>-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[@]}"

View file

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

View file

@ -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('<?xml version="1.0"?>\n<mameconfig version="10">\n <system name="%s">\n <input>' % system)
for name, default, value in ports:
print(f' <port tag="{tag}:{name}" type="CONFIG" mask="255" defvalue="{default}" value="{value}" />')
print(' </input>\n </system>\n</mameconfig>')
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))

122
scripts/lib/serialStreamPeer.py Executable file
View file

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

144
scripts/lib/tileBankGate.sh Normal file
View file

@ -0,0 +1,144 @@
# Shared by the tile-bank gates, scripts/verify-<port>-tilebank.sh. Each runs
# examples/tilebank on its port with the DATA/tilebank make/<port>.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/<port>.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
)"
}

View file

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

View file

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

View file

@ -1,16 +1,26 @@
#!/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:
# 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
# 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
@ -18,28 +28,41 @@
# 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
#
# `all` first BUILDS every selected port (make <port>, 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 <<EOF
host host blank scripts/check-blank.sh
host host input scripts/check-input.sh
host host millis scripts/check-millis.sh
host host save scripts/check-save.sh
host host serial scripts/check-serial.sh
host host sfxmix scripts/check-sfxmix.sh
host host tilebank scripts/check-tilebank.sh
golden iigs uber scripts/bench-iigs.sh && tools/diff-uber-hashes tests/goldens/uber/iigs.txt build/iigs/bin/joeylog.txt
golden x68000 uber scripts/verify-x68000-golden.sh
golden atarist uber scripts/bench-atarist.sh && tools/diff-uber-hashes tests/goldens/uber/atarist.txt build/atarist/bin/joeylog.txt
golden amiga uber TIMEOUT=$AMIGA_UBER_SECONDS scripts/bench-amiga.sh && tools/diff-uber-hashes tests/goldens/uber/amiga.txt build/amiga/bin/joeylog.txt
golden dos uber scripts/bench-dos.sh && tools/diff-uber-hashes tests/goldens/uber/dos.txt build/dos/bin/JOEYLOG.TXT
visual iigs examples scripts/verify-iigs-all.sh
tilebank iigs tilebank make -s -f make/iigs.mk iigs-verify-tilebank || exit 1
tilebank x68000 tilebank scripts/verify-x68000-tilebank.sh
tilebank atarist tilebank scripts/verify-atarist-tilebank.sh
tilebank amiga tilebank scripts/verify-amiga-tilebank.sh
tilebank dos tilebank scripts/verify-dos-tilebank.sh
input iigs input scripts/verify-iigs-input.sh
input x68000 input scripts/verify-x68000-input.sh
input atarist input scripts/verify-atarist-input.sh
input amiga input scripts/verify-amiga-input.sh
input dos input scripts/verify-dos-input.sh
audio iigs audio scripts/verify-iigs-audio.sh staxi
audio x68000 audio scripts/verify-x68000-audio.sh
audio atarist audio scripts/verify-atarist-audio.sh
audio amiga audio scripts/verify-amiga-audio.sh
audio dos audio scripts/verify-dos-audio.sh
serial host serial scripts/check-serial.sh
serial iigs serial scripts/verify-iigs-serial.sh
serial x68000 serial scripts/verify-x68000-serial.sh
serial atarist serial scripts/verify-atarist-serial.sh
serial amiga serial scripts/verify-amiga-serial.sh
serial dos serial scripts/verify-dos-serial.sh
mouse x68000 mouse scripts/verify-x68000-mouse.sh
mouse x68000 sermouse scripts/verify-x68000-serial-mouse.sh
save iigs save make -s -f make/iigs.mk iigs-verify-save || exit 1
loss iigs loss make -s -f make/iigs.mk iigs-verify-serial-loss || exit 1
disk iigs staxi make -s -f make/iigs.mk iigs-verify-game-disk GAME=STAXI || exit 1
disk iigs staxi01 GAME_VERIFY_MACHINE=apple2gsr1 scripts/verify-iigs-game-disk.sh build/iigs/bin/STAXI.2mg
disk iigs agi make -s -f make/iigs.mk iigs-game-disk-agi || exit 1; scripts/verify-iigs-game-disk.sh build/iigs/bin/AGI.2mg 2 8M
EOF
)
case " all $KINDS " in
*" $kind "*) ;;
*) echo "usage: scripts/verify-all.sh all|$(echo "$KINDS" | tr ' ' '|') [port ...]" >&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 )
rc=$?
fi
elapsed[$port]=$((SECONDS - start))
case $rc in
0) passed="$passed $port" ;;
2) skipped="$skipped $port" ;;
*) failed="$failed $port" ;;
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
done
echo
echo "=== $label: build $port ($cmd) ==="
if ! runLogged "$cmd"; then
echo "$label: FAIL - building $port failed" >&2
exit 1
fi
done
}
count() { set -- $1; echo $#; }
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=$?
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
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

View file

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

View file

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

75
scripts/verify-dos-tilebank.sh Executable file
View file

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

196
scripts/verify-iigs-tilebank.sh Executable file
View file

@ -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" <<LUA
local mem = manager.machine.devices[":maincpu"].spaces["program"]
local nat = manager.machine.natkeyboard
local FRAMES = $frames
local FIRST_LOOK = 3800 -- the Finder launch below is done by then
local SAMPLE_EVERY = 10 -- frames between lamp looks
local LAMP_STABLE = 3 -- looks in a row that must agree
local EXIT_SETTLE = 120 -- let the program settle before MAME writes the disk back
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 idx = 1
local seen = 0
local lamp = 0
local exitAt = 0
local done = false
local function field(port, name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[name]
end
local keyCmd = field(":macadb:KEY3", "Command / Open Apple")
local function press(f) if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end
local function stagePixel(x, y)
local b = mem:read_u8(0xE12000 + y * 160 + (x >> 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

View file

@ -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" </dev/null > "$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)"

View file

@ -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('<?xml version="1.0"?>\n<mameconfig version="10">\n <system name="x68000">\n <input>')
for name, default, value in ports:
print(f' <port tag="{tag}{name}" type="CONFIG" mask="255" defvalue="{default}" value="{value}" />')
print(' </input>\n </system>\n</mameconfig>')
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

170
scripts/verify-x68000-tilebank.sh Executable file
View file

@ -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" <<LUA
local mem = manager.machine.devices[":maincpu"].spaces["program"]
local nat = manager.machine.natkeyboard
local FRAMES = $frames
local SAMPLE_EVERY = 10 -- frames between lamp looks
local LAMP_STABLE = 3 -- looks in a row that must agree
local KEY_EVERY = 60 -- frames between key presses once the lamp shows
local EXIT_SETTLE = 900 -- keypress -> 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

View file

@ -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 <stdint.h>
#include <stdbool.h>
#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

View file

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

View file

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

View file

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

377
src/m68k/planarTile.h Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
#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

View file

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

View file

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

View file

@ -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) {
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]);
}
}
return n;
}

View file

@ -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 <stdint.h>
#include <stdbool.h>
#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

View file

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

BIN
tests/host/tileBank.png (Stored with Git LFS) Normal file

Binary file not shown.

503
tests/host/tileBankHost.c Normal file
View file

@ -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<n>.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 <limits.h>
#include <stdio.h>
#include <string.h>
#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-<port>-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;
}

View file

@ -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<n>.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/<port>.mk stages its DATA/tilebank with
# TARGET = the port; scripts/verify-<port>-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("<H", (w // TILE_PX) * (h // TILE_PX)))
for i in range(COLORS):
rgb = raw[i * 3:i * 3 + 3]
if len(rgb) < 3:
rgb = [0, 0, 0]
r, g, b = (c >> NIBBLE_BITS for c in rgb)
out += struct.pack("<H", (r << (2 * NIBBLE_BITS)) | (g << NIBBLE_BITS) | b)
for ty in range(0, h, TILE_PX):
for tx in range(0, w, TILE_PX):
for y in range(TILE_PX):
for x in range(TILE_PX):
out.append(px[tx + x, ty + y])
with open(outPath, "wb") as f:
f.write(out)
def main():
if len(sys.argv) != 4:
sys.exit("usage: tileBankStage.py TARGET PNG OUTDIR")
target, pngPath, outDir = sys.argv[1:]
if target not in TARGET_NAMES:
sys.exit("tileBankStage: unknown target '%s' (assetbake knows: %s)" % (target, " ".join(TARGET_NAMES)))
os.makedirs(outDir, exist_ok=True)
# A target assetbake dropped must not leave its old bake behind to be
# counted as a refusal.
for stale in glob.glob(os.path.join(outDir, REFUSE_PATTERN.replace("%d", "*"))):
os.remove(stale)
bakeTileBank(pngPath, os.path.join(outDir, OWN_BANK), TARGET_NAMES[target])
n = 0
for other in TARGET_NAMES:
if other == target:
continue
n += 1
bakeTileBank(pngPath, os.path.join(outDir, REFUSE_PATTERN % n), TARGET_NAMES[other])
writeExpected(pngPath, os.path.join(outDir, EXPECT_FILE))
if __name__ == "__main__":
main()

View file

@ -95,7 +95,6 @@ int main(void) {
uint8_t k;
config.codegenBytes = 160UL * 1024;
config.audioBytes = 32UL * 1024;
if (!jlInit(&config)) {
printf("jlInit failed\n");
return 1;