From e228efba2ae66f5bb5e24a911a0ec9eed94abd4a Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 4 May 2026 11:43:38 -0500 Subject: [PATCH] More minor API cleanup. --- PERF.md | 36 +++++++++ README.md | 1 - examples/audio/audio.c | 1 - examples/draw/draw.c | 4 +- examples/hello/hello.c | 3 +- examples/joy/joy.c | 1 - examples/keys/keys.c | 1 - examples/pattern/pattern.c | 1 - examples/sprite/sprite.c | 1 - examples/uber/uber.c | 1 - include/joey/core.h | 1 - include/joey/types.h | 5 -- scripts/run-amiga.sh | 4 +- src/port/amiga/input.c | 29 +++++++ tools/uber-perf-table | 151 +++++++++++++++++++++++++++++++++++++ 15 files changed, 221 insertions(+), 19 deletions(-) create mode 100644 PERF.md create mode 100755 tools/uber-perf-table diff --git a/PERF.md b/PERF.md new file mode 100644 index 0000000..8493493 --- /dev/null +++ b/PERF.md @@ -0,0 +1,36 @@ +| Op | IIgs (ops/sec) | Amiga (vs IIGS) | Atari ST (vs IIGS) | DOS (vs IIGS) | +| --- | --- | --- | --- | --- | +| surfaceClear | 28 | 2.54x | 1.11x | 638.57x | +| paletteSet | 678 | 8.68x | 4.23x | 26.13x | +| scbSetRange | 1005 | 3.66x | 1.86x | 11.16x | +| drawPixel | 1755 | 1.85x | 1.01x | 9.70x | +| drawLine H | 682 | 2.19x | 1.22x | 19.26x | +| drawLine V | 90 | 1.67x | 1.11x | 115.30x | +| drawLine diag | 35 | 1.14x | 1.06x | 261.49x | +| drawRect 100x100 | 75 | 1.57x | 1.04x | 250.77x | +| drawCircle r=16 | 232 | 1.21x | 0.65x | 71.24x | +| drawCircle r=80 | 56 | 1.16x | 0.61x | 310.93x | +| fillRect 16x16 | 450 | 1.24x | 1.04x | 39.38x | +| fillRect 80x80 | 75 | 0.95x | 1.28x | 206.73x | +| fillRect 320x200 | 60 | 0.93x | 0.43x | 184.62x | +| fillCircle r=40 | 38 | 0.97x | 1.39x | 347.24x | +| samplePixel | 1916 | 3.48x | 1.92x | 18.04x | +| tileFill | 1252 | 1.73x | 0.93x | 10.44x | +| tileCopy | 997 | 1.80x | 1.02x | 16.96x | +| tileCopyMasked | 498 | 1.76x | 1.08x | 28.55x | +| tilePaste | 1106 | 1.94x | 1.09x | 13.53x | +| tileSnap | 1473 | 2.26x | 1.28x | 9.44x | +| spriteSaveUnder | 528 | 2.21x | 1.29x | 20.14x | +| spriteDraw | 438 | 1.82x | 1.28x | 38.39x | +| spriteRestoreUnder | 487 | 2.00x | 1.11x | 36.71x | +| spriteSaveAndDraw | 277 | 1.82x | 1.06x | 72.79x | +| stagePresent full | 42 | 6.31x | 1.40x | 373.83x | +| joeyInputPoll | 273 | 13.90x | 4.19x | 57.15x | +| joeyKeyDown | 3382 | 7.82x | 3.76x | 18.93x | +| joeyKeyPressed | 3345 | 7.65x | 3.81x | 18.72x | +| joeyMouseX | 4170 | 10.26x | 5.22x | 26.27x | +| joeyJoyConnected | 3378 | 7.76x | 3.71x | 18.95x | +| joeyAudioFrameTick | 4106 | 7.99x | 3.04x | 12.37x | +| joeyAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x | +| surfaceMarkDirtyRect (via fillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x | +| c2p full screen | - | - | - | - | diff --git a/README.md b/README.md index eddbc74..4e78d06 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,6 @@ is a quick reference. Every entry point is plain C, no C++ extensions. ```c typedef struct { - HostModeE hostMode; // HOST_MODE_TAKEOVER or HOST_MODE_OS uint32_t codegenBytes; // runtime compiled-sprite cache size uint16_t maxSurfaces; // maximum concurrent surfaces uint32_t audioBytes; // audio sample / module RAM pool diff --git a/examples/audio/audio.c b/examples/audio/audio.c index 163c14c..36bfafa 100644 --- a/examples/audio/audio.c +++ b/examples/audio/audio.c @@ -117,7 +117,6 @@ int main(void) { uint8_t *sfxBytes; uint32_t sfxLen; - config.hostMode = HOST_MODE_TAKEOVER; config.codegenBytes = 8 * 1024; config.maxSurfaces = 4; config.audioBytes = 64UL * 1024; diff --git a/examples/draw/draw.c b/examples/draw/draw.c index cd83ef1..b45807a 100644 --- a/examples/draw/draw.c +++ b/examples/draw/draw.c @@ -11,8 +11,7 @@ // disk at center). // BR: tileCopy / tileCopyMasked / tileSnap+tilePaste / floodFill. // -// Runs in HOST_MODE_TAKEOVER and holds the frame until the user -// presses ESC / RETURN / SPACE. +// Holds the frame until the user presses ESC / RETURN / SPACE. #include @@ -243,7 +242,6 @@ int main(void) { JoeyConfigT config; SurfaceT *screen; - config.hostMode = HOST_MODE_TAKEOVER; config.codegenBytes = 8 * 1024; config.maxSurfaces = 4; config.audioBytes = 64UL * 1024; diff --git a/examples/hello/hello.c b/examples/hello/hello.c index 8440484..bd1a81b 100644 --- a/examples/hello/hello.c +++ b/examples/hello/hello.c @@ -1,7 +1,7 @@ // JoeyLib hello-world example. // // Validates that the C API headers, library, and per-platform link -// path all work end-to-end. Runs in HOST_MODE_OS so it can use stdio. +// path all work end-to-end. #include #include @@ -9,7 +9,6 @@ int main(void) { JoeyConfigT config; - config.hostMode = HOST_MODE_OS; config.codegenBytes = 8 * 1024; config.maxSurfaces = 4; config.audioBytes = 64UL * 1024; diff --git a/examples/joy/joy.c b/examples/joy/joy.c index 24de236..669b057 100644 --- a/examples/joy/joy.c +++ b/examples/joy/joy.c @@ -217,7 +217,6 @@ int main(void) { JoeyConfigT config; SurfaceT *screen; - config.hostMode = HOST_MODE_TAKEOVER; config.codegenBytes = 8 * 1024; config.maxSurfaces = 4; config.audioBytes = 64UL * 1024; diff --git a/examples/keys/keys.c b/examples/keys/keys.c index 253977b..d81e338 100644 --- a/examples/keys/keys.c +++ b/examples/keys/keys.c @@ -217,7 +217,6 @@ int main(void) { int16_t cursorCol; int16_t cursorRow; - config.hostMode = HOST_MODE_TAKEOVER; config.codegenBytes = 8 * 1024; config.maxSurfaces = 4; config.audioBytes = 64UL * 1024; diff --git a/examples/pattern/pattern.c b/examples/pattern/pattern.c index 2fe3286..ab5006b 100644 --- a/examples/pattern/pattern.c +++ b/examples/pattern/pattern.c @@ -105,7 +105,6 @@ int main(void) { JoeyConfigT config; SurfaceT *screen; - config.hostMode = HOST_MODE_TAKEOVER; config.codegenBytes = 8 * 1024; config.maxSurfaces = 4; config.audioBytes = 64UL * 1024; diff --git a/examples/sprite/sprite.c b/examples/sprite/sprite.c index 1416280..48e7ce8 100644 --- a/examples/sprite/sprite.c +++ b/examples/sprite/sprite.c @@ -102,7 +102,6 @@ int main(void) { int16_t vy; bool haveBackup; - config.hostMode = HOST_MODE_TAKEOVER; /* Amiga planar emits 8 pre-shifted DRAW variants per sprite (one * per x % 8 alignment) so the codegen arena needs roughly 8x what * the chunky two-shift case asks for. 32 KB fits a 16x16 ball diff --git a/examples/uber/uber.c b/examples/uber/uber.c index fd0855f..b0c1e27 100644 --- a/examples/uber/uber.c +++ b/examples/uber/uber.c @@ -294,7 +294,6 @@ int main(void) { uint16_t elapsedFrames; unsigned long elapsedMs; - config.hostMode = HOST_MODE_TAKEOVER; /* 32 KB fits the 8 pre-shifted DRAW variants the Amiga planar * compiled sprite emitter generates. UL on the multiply because * ORCA-C's 16-bit int overflows on 32 * 1024. */ diff --git a/include/joey/core.h b/include/joey/core.h index 256f95e..9521eb3 100644 --- a/include/joey/core.h +++ b/include/joey/core.h @@ -7,7 +7,6 @@ #include "types.h" typedef struct { - HostModeE hostMode; // takeover or cooperate with host OS uint32_t codegenBytes; // runtime compiled-sprite cache size uint16_t maxSurfaces; // maximum concurrent surfaces uint32_t audioBytes; // audio sample and module RAM pool diff --git a/include/joey/types.h b/include/joey/types.h index a1a40bb..dc07777 100644 --- a/include/joey/types.h +++ b/include/joey/types.h @@ -6,11 +6,6 @@ #include #include -typedef enum { - HOST_MODE_TAKEOVER, - HOST_MODE_OS -} HostModeE; - typedef enum { VIDEO_REGION_NTSC, VIDEO_REGION_PAL, diff --git a/scripts/run-amiga.sh b/scripts/run-amiga.sh index 1030396..e9056bb 100755 --- a/scripts/run-amiga.sh +++ b/scripts/run-amiga.sh @@ -56,8 +56,10 @@ workbench=$support/workbench.adf work=$(mktemp -d -t joeylib-amiga.XXXXXX) # Preserve any diagnostic dumps Pattern writes to the virtual HD # (copper.txt, etc.) before the temp dir is removed on script exit. +# Also stage UBER's joeylog.txt back to build/amiga/bin/ so the +# tools/uber-perf-table picks it up at the standard path. dump_keep=/tmp/joeylib-amiga-dump -trap 'mkdir -p "$dump_keep"; cp "$work"/*.txt "$dump_keep"/ 2>/dev/null; rm -rf "$work"' EXIT +trap 'mkdir -p "$dump_keep"; cp "$work"/*.txt "$dump_keep"/ 2>/dev/null; cp "$work"/joeylog.txt "$bin_dir/joeylog.txt" 2>/dev/null; rm -rf "$work"' EXIT mkdir -p "$work/s" # Stage every built binary (executable file at top of bin_dir, no diff --git a/src/port/amiga/input.c b/src/port/amiga/input.c index 1bccc81..c30db2b 100644 --- a/src/port/amiga/input.c +++ b/src/port/amiga/input.c @@ -43,6 +43,22 @@ extern struct Screen *gScreen; +// ----- Empty pointer image ----- +// +// Full-screen custom-screen games conventionally hide the system +// pointer; we install an all-zero sprite via SetPointer so Workbench's +// arrow doesn't drift over the playfield. Game code that needs a +// cursor draws its own from joeyMouseX/Y. +// +// Amiga hardware sprite data: 2 leading control words (VSTART/HSTART/ +// VSTOP), one line of (planeA, planeB) data words, 2 trailing zero +// words as terminator. All zeros yields a transparent pointer. Must +// live in chip RAM since sprite DMA reads it. + +#define EMPTY_POINTER_BYTES (6 * sizeof(UWORD)) + +static UWORD *gEmptyPointer = NULL; + // ----- Prototypes ----- static void drainMessages(void); @@ -260,6 +276,12 @@ void halInputInit(void) { if (gWindow != NULL) { gMouseX = (int16_t)(gWindow->Width / 2); gMouseY = (int16_t)(gWindow->Height / 2); + + gEmptyPointer = (UWORD *)AllocMem((ULONG)EMPTY_POINTER_BYTES, + (ULONG)(MEMF_CHIP | MEMF_CLEAR)); + if (gEmptyPointer != NULL) { + SetPointer(gWindow, gEmptyPointer, 1, 16, 0, 0); + } } // lowlevel.library shipped with AmigaOS 2.05+. If absent (very old @@ -284,6 +306,13 @@ void halInputShutdown(void) { return; } drainMessages(); + if (gEmptyPointer != NULL) { + // Restore the system pointer before freeing our image so + // Intuition isn't holding a stale chip-RAM reference. + ClearPointer(gWindow); + FreeMem(gEmptyPointer, (ULONG)EMPTY_POINTER_BYTES); + gEmptyPointer = NULL; + } CloseWindow(gWindow); gWindow = NULL; } diff --git a/tools/uber-perf-table b/tools/uber-perf-table new file mode 100755 index 0000000..ad6d41f --- /dev/null +++ b/tools/uber-perf-table @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Render a cross-port UBER perf table in Markdown. + +Reads each port's joeylog.txt (the file UBER appends ops/sec lines to) +and emits a Markdown table comparing all ports against a reference. +The reference column shows absolute ops/sec; every other port shows a +ratio of (port / reference) so a glance tells you whether each target +is at, above, or below the reference. + +Usage: + tools/uber-perf-table [--ref iigs] [--out PATH] [--repo PATH] + +Defaults pull the 4 standard log paths under the repo's build/ tree; +pass --repo if running from elsewhere. The reference port (default +IIgs, which DESIGN.md calls the perf floor) defines the ratio +denominator. Ports whose log file is missing are listed in the table +header as "(no log)" and their columns are filled with "-". + +Example, run from the repo root: + + tools/uber-perf-table > PERF.md + +To compare against a different reference (e.g. profile DOS as the +floor for a DOS-only project): + + tools/uber-perf-table --ref dos +""" + +import argparse +import os +import re +import sys + +LINE_RE = re.compile( + r"UBER:\s+(?P[^:]+):\s+\d+\s+iters\s+/\s+\d+\s+frames\s+=\s+(?P\d+)\s+ops/sec" +) + +# Per-port log path relative to the repo root, in the column order the +# table renders. Reference port appears first. +PORTS = [ + ("iigs", "IIgs", "build/iigs/bin/joeylog.txt"), + ("amiga", "Amiga", "build/amiga/bin/joeylog.txt"), + ("atarist", "Atari ST", "build/atarist/bin/joeylog.txt"), + ("dos", "DOS", "build/dos/bin/JOEYLOG.TXT"), +] + + +def parse_log(path): + """Return {op: ops/sec} from a UBER log. Last entry per op wins.""" + perf = {} + with open(path) as f: + for line in f: + m = LINE_RE.search(line) + if m: + perf[m.group("op").strip()] = int(m.group("ops")) + return perf + + +def format_ratio(port_ops, ref_ops): + if port_ops is None or ref_ops is None or ref_ops == 0: + return "-" + ratio = port_ops / ref_ops + return f"{ratio:.2f}x" + + +def render_table(port_data, ops_in_order, ref_key): + headers = ["Op"] + for key, label, _ in PORTS: + if not port_data[key]["loaded"]: + label = f"{label} (no log)" + if key == ref_key: + headers.append(f"{label} (ops/sec)") + else: + headers.append(f"{label} (vs {ref_key.upper()})") + + rows = [headers, ["---"] * len(headers)] + for op in ops_in_order: + row = [op] + ref_ops = port_data[ref_key]["perf"].get(op) + for key, _, _ in PORTS: + ops = port_data[key]["perf"].get(op) + if key == ref_key: + row.append(str(ops) if ops is not None else "-") + else: + row.append(format_ratio(ops, ref_ops)) + rows.append(row) + + return "\n".join("| " + " | ".join(r) + " |" for r in rows) + + +def main(argv): + parser = argparse.ArgumentParser( + description="Render a cross-port UBER perf table in Markdown." + ) + parser.add_argument("--ref", default="iigs", + choices=[p[0] for p in PORTS], + help="Reference port (default: iigs)") + parser.add_argument("--out", default=None, + help="Write table to this file instead of stdout") + parser.add_argument("--repo", default=None, + help="Repo root (default: parent of this script)") + args = parser.parse_args(argv[1:]) + + if args.repo is None: + args.repo = os.path.dirname(os.path.dirname(os.path.abspath(argv[0]))) + + port_data = {} + union_ops = [] + seen = set() + for key, _, rel in PORTS: + path = os.path.join(args.repo, rel) + try: + perf = parse_log(path) + loaded = True + except OSError: + perf = {} + loaded = False + port_data[key] = {"perf": perf, "loaded": loaded, "path": path} + for op in perf: + if op not in seen: + seen.add(op) + union_ops.append(op) + + if not port_data[args.ref]["loaded"]: + sys.stderr.write( + f"error: reference log missing: {port_data[args.ref]['path']}\n" + f" run UBER on {args.ref} first, then retry.\n" + ) + return 2 + + if not union_ops: + sys.stderr.write("error: no UBER ops parsed from any log\n") + return 2 + + # Use the reference port's op order so the table matches UBER's + # natural sequence (surfaceClear first, joeyAudioIsPlayingMod last). + ref_ops = list(port_data[args.ref]["perf"].keys()) + extras = [op for op in union_ops if op not in port_data[args.ref]["perf"]] + ordered = ref_ops + extras + + table = render_table(port_data, ordered, args.ref) + if args.out: + with open(args.out, "w") as f: + f.write(table + "\n") + else: + sys.stdout.write(table + "\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv))