More minor API cleanup.

This commit is contained in:
Scott Duensing 2026-05-04 11:43:38 -05:00
parent ac44ac1303
commit e228efba2a
15 changed files with 221 additions and 19 deletions

36
PERF.md Normal file
View file

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

View file

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

View file

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

View file

@ -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 <stdio.h>
@ -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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,11 +6,6 @@
#include <stdint.h>
#include <stdbool.h>
typedef enum {
HOST_MODE_TAKEOVER,
HOST_MODE_OS
} HostModeE;
typedef enum {
VIDEO_REGION_NTSC,
VIDEO_REGION_PAL,

View file

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

View file

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

151
tools/uber-perf-table Executable file
View file

@ -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<op>[^:]+):\s+\d+\s+iters\s+/\s+\d+\s+frames\s+=\s+(?P<ops>\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))