Add docs/jtrm-errata.md: where the manual is wrong/inconsistent/hardware differs (A), where it is silent and the answer came from netlists, Verilator, MiSTer or games (B), Atari's own bug lists with a modeled? column (C), and the doc mistakes we already paid for (D). Point docs/agent/hardware.md and CLAUDE.md at it. Correct, against JTRM Rev 8 / Rev 10 / Software Reference v2.4 / Hardware Bugs & Warnings, the distilled docs that CLAUDE.md tells agents to trust: - jtrm-gpu-dsp.md: 27 wrong opcode numbers (14 instructions missing), SH direction, MULT/IMULT signedness, PACK/UNPACK, DSP-only opcodes (SAT32S 42, MIRROR 48), DSP flags and ctrl layout (int 5 at bits 16/17, INT_LAT5 at bit 16), wave table geometry, BUS_HOG/DMAEN gotchas - jtrm-register-map.md / jtrm-blitter.md: LFU encodings, PITCH 3, window width encoding, SRCZ1/SRCZ2 and the Rev 2.2 footnote, MEMCON1/MEMCON2 bit placement, ZMODE, GOURZ, G_HIDATA, D_FLAGS, misattributed bug list - jtrm-clocks-timing.md: video timing and pixel-divisor tables vs v10 p.6/p.8, CLK1/2/3 formulas, MEMCON register table - jtrm-jerry.md, jtrm-object-processor.md: wavetable entry count, STOPOBJ, unsourced VC=$FFFF claim - comments only (no behaviour change): stale PIT clock comment in jerry.c, blitter status value comment, bcompen_basic.s "differs from JTRM" claim, test_texdump.c LFU label, and several tests that cited our own source as the hardware spec Code that contradicts the manual (DSP DIV_OFFSET bit, STOP object interrupt gating, test_texdump LFU constant) is recorded as follow-ups, not changed. Refs #820 Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Purpose-built microbenchmark ROMs (#536)
Small, single-engine Jaguar cartridges with a fixed iteration count and a harness-assertable completion marker. They fill the gap #533 identified after auditing the public ROM corpus (jagniccc/yarc): real titles never give single-engine isolation or an exact, deterministic termination.
| ROM | engine | what it isolates | DONE magic | status |
|---|---|---|---|---|
bench68k.j64 |
68000 only | pure 68K loop throughput (addq.l/subq.l/bne.s) fetching from cart ROM |
$C0DE0068 |
shipped (task 1) |
benchgpu_arith.j64 |
GPU, arithmetic-heavy | back-to-back ALU ops (add/sub/and/or) out of GPU local RAM -- the write-back port conflict rule's worst case |
$C0DE0A01 |
shipped (task 2) |
benchgpu_branch.j64 |
GPU, branch-heavy | control flow (JR/NOP delay-slot pairs, taken JUMP) out of GPU local RAM -- structurally immune to the write-back conflict that hits the arith ROM |
$C0DE0B02 |
shipped (task 3) |
benchdsp.j64 |
DSP | the same 20-instruction ALU body as benchgpu_arith.j64, run on the DSP interpreter instead of the GPU's -- a direct same-clock GPU-vs-DSP comparison, and proof the DSP has no virtualjaguar_gpu_pipeline_timing-equivalent cost model |
$C0DE0D53 |
shipped (task 4) |
benchblit.j64 |
blitter | fixed-count 32x32 8bpp PATDSEL solid-fill launches to main RAM -- the only ROM whose hot path touches external memory every iteration, so it responds to virtualjaguar_dram_timing and virtualjaguar_blitter_timing where the other four don't |
$C0DE0B17 |
shipped (task 5) |
Run them:
make microbench # builds the core (TEST_EXPORTS=1) + tools, runs all
CI runs make microbench on every PR/push touching test/microbench/** or
test/tools/test_microbench_* (.github/workflows/microbench.yml) --
see "CI" below.
Each tool prints one machine-parseable line; done_frame is the point of the
exercise:
MICROBENCH engine=68k done=1 done_frame=114 start_frame=1 count=1000000 expect_count=1000000 budget=600
The .j64 images are committed, so CI never needs an assembler. Regenerate
them only after editing a .s:
make jaguar-toolchain-build
eval "$(tools/jaguar-toolchain/setup.sh env)"
test/microbench/build.sh
The cart bootstrap
cartboot.inc holds it; every ROM includes it first in .text. It is the
authority — this section explains why those bytes.
Header layout
$800000 .. $8003FF zero fill (retail carts put the encrypted
BootIntro block here; unused by us)
$800400 04 04 04 04 universal marker / cart-type byte
$800404 00 80 20 00 68K entry vector -> $802000
$800408 00 00 00 00
$80040C .. $801FFF $FF fill
$802000 first instruction
This is byte-identical to the two public ROMs already in the repo. Both
test/roms/yarc.j64 and test/roms/jagniccc.j64 read, at file offset $400:
00000400: 0404 0404 0080 2000 ...... .
That was the working reference; the layout was pattern-matched from it rather than inferred from spec text.
What the core actually does with those bytes
Three separate consumers, all in this repo — verified by reading them, not assumed:
src/core/file.cParseFileType()— a sub-1 MiB image is accepted asJST_ROMonly ifGET32(buffer, $400) == $04040404(CART_UNIVERSAL_MARKERatCART_UNIVERSAL_MARKER_OFFSET). Without the marker our 8 KB cart would fall through to the raw-binary / unrecognized arms. (An exact-megabyte file is accepted on size alone, but padding five ROMs to 1 MiB each just to skip a header is not a trade worth making.)src/core/file.cDetectPrependedHeaderSize()— reads file$600(=512 + $400) looking for the same marker to decide whether a 512-byte copier header is glued to the front. Our$FFpadding there is not the marker, so nothing is stripped. Keep the$40C..$1FFFfill non-$04040404.src/core/jaguar.cHLE BIOS state —cartTypeByte = jagMemSpace[CART_HEADER_BASE]($800400), andMEMCON1 = MEMCON1_BASE | (cartTypeByte & MEMCON1_CART_MASK)→$1861 | $04=$1865. The marker's first byte doubles as the cart-type byte, so we inherit exactly the MEMCON1 a commercial cart gets.
The entry vector at $800404 is read by file.c, range-checked (must be even
and land in main RAM / cart ROM / boot ROM), stored in jaguarRunAddress, and
copied by JaguarReset() to main RAM $000004 — which is where the 68000
fetches its reset PC. $00802000 passes the check.
Real-BIOS mode does not work — measured, not assumed
The plan's architecture note ("no BootIntro / jagcrypt") is confirmed empirically. All runs 300 frames, sentinels read from main RAM afterwards:
| image | mode | final 68K PC | sentinels |
|---|---|---|---|
bench68k.j64 |
HLE (harness default) | $80202C (the bra.s * park) |
all written |
bench68k.j64 |
--bios |
$0050B6 |
untouched |
jagcrypt -u signed variant |
--bios |
$0059A0 |
untouched |
jagcrypt -u signed variant |
HLE | $80202C |
all written |
The pinned pc_jagcrypt will run (jagcrypt -u bench68k.rom produces a
.U1, contrary to what the -tursi gap might suggest), but the RSA data it
generates is not what the emulated Series K BIOS validates against, so the
signed image is no more bootable under --bios than the plain one. It is also
worse for our purposes: the signing pass is not byte-reproducible.
Conclusion: these ROMs are HLE-boot only. That is fine — test/harness/
defaults to HLE (--bios is opt-in) and the microbench tools never pass it.
Sentinel convention — all five ROMs
$010000–$01000F is the reserved sentinel block. Benchmark working
buffers (blitter destinations, GPU scratch) must live at $020000 or
above so they can never reach it.
| address | meaning |
|---|---|
$010000 |
START magic $C0DE57A7 — written at the entry point by every ROM |
$010004 |
DONE magic — per-engine, see the table at the top |
$010008 |
iterations actually executed |
$01000C |
reserved |
Write the count before the magic, so a reader that sees DONE is guaranteed to see a settled count.
Why $010000 and not the plan's sketched $001000: HLE_SSP_CART is $4000
(src/core/jaguar.c), and the 68K stack grows down from there straight through
$1000. $010000 sits above the stack, above the HLE vector table
($0–$3FF) and its RTE stubs ($400/$404), and well inside the 2 MB of
main RAM. Main RAM reads back all-zero at reset in HLE mode, so "never ran" is
distinguishable from any magic value.
Having both a START and a DONE marker is what makes a failure legible: the
tool reports start_frame=-1 ("ROM never booted") separately from
done_frame=-1 with start_frame=1 ("booted, didn't finish in budget").
Frame budget
The harness runs a fixed frame count and then reads memory; the ROM never has
to signal exit (each parks in bra.s *). The budget must exceed the loop's
completion frame, and completion moves with the core's timing model.
Measured for bench68k.j64 (1,000,000 iterations):
| configuration | done_frame |
|---|---|
| harness defaults | 114 |
virtualjaguar_gpu_pipeline_timing=enabled |
114 |
| PAL | 114 |
virtualjaguar_dram_timing=enabled |
128 |
both + VJ_DRAM_SCALE=8 |
228 |
both + VJ_DRAM_SCALE=12 |
288 |
both + VJ_DRAM_SCALE=16 |
350 |
virtualjaguar_dram_timing and virtualjaguar_gpu_pipeline_timing both default to disabled, so the
default path is 114. The tools use 600 — clears the slowest measured
configuration with ~1.7x margin, at about a second of wall time.
Sanity check on 114: the 3-instruction body costs a nominal 26 cycles on a
68000 (addq.l 8 + subq.l 8 + taken bne.s 10), so 26 M cycles at ~13.3 MHz
≈ 1.95 s ≈ 117 fields at 60.05 Hz.
If you change an ITERATIONS value, re-measure and update both the .s
comment and the tool's budget block.
Measured for benchgpu_arith.j64 (2,000,000 iterations). Correction
(fix round 1, 2026-08-24): the virtualjaguar_gpu_pipeline_timing=enabled row below was
previously published as 86 (flat, no response). That was never re-measured
after being copied from an earlier draft of this table -- a direct run shows
it moves to 172, a ~100% jump, the largest relative move of any
configuration tested against this ROM:
| configuration | done_frame |
|---|---|
| harness defaults | 86 |
virtualjaguar_gpu_pipeline_timing=enabled |
172 |
virtualjaguar_dram_timing=enabled |
86 |
| PAL | 86 |
both + VJ_DRAM_SCALE=8 |
172 |
both + VJ_DRAM_SCALE=12 |
172 |
both + VJ_DRAM_SCALE=16 |
172 |
virtualjaguar_dram_timing alone doesn't move this ROM: the 2M-iteration hot loop runs
entirely out of GPU local RAM and touches external memory only for its two
sentinel STOREs, and VJ_DRAM_SCALE only scales external-memory latency.
virtualjaguar_gpu_pipeline_timing is the mover, and it roughly doubles the completion
frame -- see the explanation after the branch-ROM table below, which covers
both ROMs together. The tools use the same 600 budget as bench68k.j64
for consistency across the five ROMs; it clears 172 with ~3.5x margin.
Measured for benchgpu_branch.j64 (2,000,000 iterations, 21-instruction
body -- see the .s for the exact composition: 1 CMPQ + 8 not-taken
JR/NOP pairs + ADDQ + SUBQ + 1 JUMP/NOP loop closer, ~90% branch dispatch):
| configuration | done_frame |
|---|---|
| harness defaults | 91 |
virtualjaguar_dram_timing=enabled |
91 |
| PAL | 91 |
virtualjaguar_gpu_pipeline_timing=enabled |
113 |
both + VJ_DRAM_SCALE=8 |
113 |
both + VJ_DRAM_SCALE=12 |
113 |
both + VJ_DRAM_SCALE=16 |
113 |
virtualjaguar_gpu_pipeline_timing response: arith moves MORE than branch, not less
An earlier draft of this document (and task-3-report.md) claimed
benchgpu_arith.j64 stayed flat under every configuration including
virtualjaguar_gpu_pipeline_timing, and that only benchgpu_branch.j64 responded to it.
That was false and came from an unverified copy of the arith row above.
Directly measured, both ROMs respond, and arith responds more, both in
absolute frames (+86 vs. +22) and relative terms (+100% vs. +24%):
| ROM | defaults | virtualjaguar_gpu_pipeline_timing=enabled |
delta |
|---|---|---|---|
benchgpu_arith.j64 |
86 | 172 | +86 frames (+100%) |
benchgpu_branch.j64 |
91 | 113 | +22 frames (+24%) |
virtualjaguar_dram_timing alone still doesn't move either ROM (both loops run entirely
out of GPU local RAM), and VJ_DRAM_SCALE has no further effect once
virtualjaguar_gpu_pipeline_timing is already enabled for either ROM -- confirming the
extra cost in both cases comes from the pipeline model's internal-RISC-time
accounting, not from DRAM access latency.
The mechanism (read from src/tom/gpu.c GPUPipeCheckUse(), not guessed):
virtualjaguar_gpu_pipeline_timing implements two interlocks off the JTRM's
"Register Write-Back" section, checked in order. First, an ALU RAW
interlock: if either of the instruction's register reads targets the
previous instruction's write-back destination, that read isn't ready yet
and the instruction stalls 1 cycle -- this is the common case, since reading
a value the instruction immediately before you just produced is exactly what
chained ALU code does. Only if that check doesn't fire does the second rule
apply: a write-back port conflict -- the GPU register bank is dual-port,
so an instruction that reads two registers, neither of which is the
previous instruction's destination, still can't have both reads serviced
alongside the pending write-back and pays the same 1-cycle stall. The only
way to dodge both is to read fewer than two registers and, if you read
one, have it not be the previous instruction's destination -- back-to-back
register-writing ALU code essentially never satisfies that (gpu.c around
the "ALU RAW interlock" / "Write-back port conflict" comments).
benchgpu_arith.j64's loop body is 16 back-to-back ALU ops (add/sub/and/or), each of which both reads two registers and writes a third (JTRM ISA classification 7 = reads-both-writes-dest ingpu_pipe_flags[]). Consecutive pairs almost never share the prior instruction's destination as an operand, so nearly every one of the 16 ALU ops eats the 1-cycle write-back stall -- roughly +16 cycles on a ~20-cycle nominal body, which is why the completion frame very nearly doubles (86 -> 172, and 172/86 = 2.00).benchgpu_branch.j64's loop body is dominated by JR and its mandatory delay-slot NOP (16 of 21 instructions across the 8 not-taken pairs). Both JR and NOP are classified as reading zero registers (gpu_pipe_flags[]index 0 for both), so they structurally cannot trigger the two-register-read write-back conflict -- only the loop's CMPQ, ADDQ, and SUBQ (each of which reads at most one register per the same table) are even eligible, and none of those pairings triggers the rule either. What branch-heavy code does pay for instead is the JTRM's documented taken-JUMP refill cost and the flags interlock between CMPQ and a conditional JR/JUMP -- smaller, less frequent charges, which is why the ROM moves by 22 frames instead of 86.
In short: virtualjaguar_gpu_pipeline_timing's dominant cost in this model is a per-ALU-op
register-bank contention charge, not a branch-misprediction charge -- so a
tightly-chained ALU body (arith) is the worse case for it, and a
control-flow-heavy body built from register-free JR/NOP pairs (branch) is
comparatively cheap. This is the opposite of the "branch-heavy code is what
pipeline timing punishes" intuition #536 set out to test, and is worth
knowing before using either ROM as a stand-in for real branch-misprediction
cost. The tools use the same 600 budget as the other ROMs for both
ROMs; it clears the highest measured frame (172) with ~3.5x margin.
Measured for benchdsp.j64 (2,000,000 iterations, identical 20-instruction
body to benchgpu_arith.j64 for a direct GPU-vs-DSP interpreter comparison
at the same clock):
| configuration | done_frame |
|---|---|
| harness defaults | 86 |
virtualjaguar_gpu_pipeline_timing=enabled |
86 |
virtualjaguar_dram_timing=enabled |
86 |
| both | 86 |
Same default completion frame as benchgpu_arith.j64 (both 86, both engines
run this identical body at the same ~26.6 MHz full-system clock), but the
DSP does not respond to virtualjaguar_gpu_pipeline_timing the way the GPU does -- all
four configurations above were re-run directly against this ROM (not
inferred from the GPU table). This isn't a coincidence of this workload:
src/jerry/dsp.c's DSPExec() comment block states outright that "the DSP
has no pipeline-timing mode of its own (DSPExecP/DSPExecP2 are declared
in dsp.h but never called)" -- virtualjaguar_gpu_pipeline_timing only gates the DSP's
idle-skip fast-forward path (irrelevant here, since this loop never idles)
and adds no per-instruction cost model for the DSP the way GPUPipeCheckUse()
does for the GPU. virtualjaguar_dram_timing doesn't move it either, for the same reason
as the GPU ROM: the hot loop runs entirely out of DSP local RAM and touches
external memory only for its two sentinel STOREs. The tool uses the same
600 budget as the other four ROMs; it clears 86 with ~7x margin.
Measured for benchblit.j64 (150,000 launches of a 32x32 8bpp PATDSEL solid
fill; see "The blitter ROM is 68K-only" below for why this one has no
companion GPU/DSP source and no per-loop table like the previous three).
Unlike the other four ROMs, its fill destination ($020000, the benchmark
work buffer) is main RAM, so — checked directly, not assumed by analogy
with the GPU/DSP ROMs whose hot loops never leave local RAM — it responds to
virtualjaguar_dram_timing on its own, virtualjaguar_blitter_timing dominates, and the
combination can exceed the other ROMs' 600-frame budget:
| configuration | done_frame |
|---|---|
| harness defaults | 87 |
virtualjaguar_gpu_pipeline_timing=enabled |
87 |
| PAL | 87 |
virtualjaguar_dram_timing=enabled |
101 |
virtualjaguar_blitter_timing=enabled |
434 |
virtualjaguar_dram_timing + virtualjaguar_blitter_timing + virtualjaguar_gpu_pipeline_timing |
458 |
all three + VJ_DRAM_SCALE=8 |
564 |
all three + VJ_DRAM_SCALE=12 |
607 |
all three + VJ_DRAM_SCALE=16 (worst measured) |
631 |
virtualjaguar_blitter_timing (default disabled, off in make microbench's
run) is the mover by a wide margin — it is the option that charges the
blit's own bus-occupancy cost (BlitDurationSysclks() in
src/tom/blitter_mmio.c; the "Fixed setup" comment in benchblit.s covers
where that cost lands). virtualjaguar_gpu_pipeline_timing never moves this ROM at all
(it gates GPU/DSP RISC-instruction pipeline accounting; nothing here runs on
either processor). Because 631 exceeds the other ROMs' 600-frame budget,
test_microbench_blit's MB_FRAME_BUDGET is 1200, not 600 — a
deliberate, documented difference, not an oversight (see the tool's own
comment).
The blitter ROM is 68K-only
benchblit.s has no benchblit_gpu.s/benchblit_dsp.s companion and no
lyxass pass. The blitter is a DMA engine addressed entirely through 68K
writes to its MMIO register file ($F02200-$F0229F); there is no separate
program to load onto another processor, so this ROM reuses Task 1's
cart-boot shape directly (bench68k.s's .include "cartboot.inc" followed
by 68K code) rather than Tasks 2-4's 68K-bootstrap-plus-RISC-payload split.
IDLE poll is a no-op in this emulator — verified, not inferred
benchblit.s polls B_CMD's bit 0 between blit launches, matching the
hardware-programming idiom in docs/jtrm-blitter.md's "B_CMD Status"
section. In this core specifically, that poll never actually waits:
src/tom/blitter_mmio.c's BlitterReadByte hard-codes the status byte at
COMMAND+3 to $05 ("always idle/never stopped"), and BlitterWriteWord
dispatches the fill synchronously (calls straight into blitter_blit() /
BlitterMidsummer2()) before the write instruction that triggered it even
retires — there is no modeled "blitter running in the background" state for
a poll to observe. The poll is included because it is what real Jaguar code
does and because the ROM should look like a plausible blit-launch loop, not
because it changes this ROM's timing on this core.
Assembling: what actually worked
rmac -fb -o bench68k.o bench68k.s
rln -n -a 800000 x x -o bench68k.j64 bench68k.o
Gotchas, all learned from the tools' own errors:
- No
.orgin a 68K section. rmac: "Error: .org permitted only in GPU/DSP/OP, 56001, 6502 and 68k (with -fr switch) sections". The load address goes onrln -ainstead, which is how a real cart is built anyway — hence the header being laid out withdcb.bfill from offset 0 and linked at$800000. -fbselects BSD object format, which rmac's own usage text labels "use this for Jaguar" and is what rln consumes.rln -nsuppresses rln's file header, so the output is the raw big-endian cart image. rln pads the tail out to an 8-byte phrase, which is whybench68k.j64is 8240 bytes rather than the 8238 the source emits.rlnneeds the two positionalx xplaceholders (data/bss segment addresses) after-a <text>..includeand.incbinboth work in rmac (verified).build.shcds intotest/microbench/so.include "cartboot.inc"resolves without an-iflag.- Output is byte-reproducible. Two consecutive builds of an unchanged
source
cmpidentical, so a regenerated.j64diffs only when the source really changed.
For tasks 2–5: embedding a lyxass GPU/DSP blob
lyxass output is not raw code. It is a 12-byte BS94 header followed by
the code (see tools/jaguar-toolchain/smoke/hellogpu.s, which documents the
exact layout):
4253 3934 "BS94" magic
00F0 3000 run address
0000 0004 code length
D7E0 E400 <-- the actual code starts here, at offset 12
A naive .incbin of a lyxass .o therefore embeds the header, and the RISC
will happily execute BS94 + address + length as instructions. Skip the
first 12 bytes — either strip them when generating the blob, or have the 68K
loader copy from blob+12.
The other lyxass trap from that same file: it emits nothing for code placed
before its .run directive, while still exiting 0 and still writing a
12-byte-header file. Put .run after .org and before the first instruction.
CI
.github/workflows/microbench.yml runs make microbench on ubuntu-latest
for every PR/push (to develop/master) that touches test/microbench/**,
test/tools/test_microbench_*, test/harness/**, Makefile, or
Makefile.common, plus workflow_dispatch. Same shape as jaguar-demos.yml's
smoke job: build the core with TEST_EXPORTS=1, run the target, tee the log
to the step summary, upload it as an artefact on any outcome. It path-filters
on test/microbench/** and test/tools/test_microbench_* specifically --
not tools/jaguar-toolchain/** -- because the whole point of committing
the .j64 images is that this job never has to build or invoke the assembler
toolchain: make microbench builds the core plus the five
test_microbench_* tools and runs each against its committed .j64, and
nothing in that path touches tools/jaguar-toolchain/, rmac, rln, or
lyxass (verified by grepping the microbench target and its tool build
rules in the Makefile). test/microbench/build.sh is the only thing that
calls into the toolchain, and it's a manual regeneration step CI never runs.
What this job does NOT cover. The path filter deliberately omits
src/**, so a change to src/tom/gpu.c, src/jerry/dsp.c, or any other
emulation-accuracy source does not trigger this workflow -- see
jaguar-demos.yml/c-cpp.yml for the jobs that build+test on src/**
changes. Even when it does run, these tools assert on iteration count,
not wall-clock or cycle-count -- done=1 just means the fixed loop finished
inside the frame budget (600/1200 frames against a measured 86-114 default).
That makes this a "did the engine still run to completion in a sane window"
smoke check, not a performance regression gate; a change that quietly halves
or doubles a done_frame without ever exceeding the budget will not fail
here.
How to add a 6th ROM
Follow Task 1-5's pattern exactly; nothing here is per-engine special-cased in the harness or the Makefile beyond adding one more of each.
-
Source. Write
test/microbench/benchNNN.s(68K-only, followingbench68k.s), orbenchNNN.s+benchNNN_gpu.s/benchNNN_dsp.s(68K bootstrap + RISC payload, followingbenchgpu_arith.s/benchgpu_arith_gpu.s). Start from.include "cartboot.inc"-- do not hand-roll the cart header. Pick a fixedITERATIONSand a new DONE magic ($C0DE0xxxconvention; don't reuse one of the five above) written toMB_SENT_DONE($010004) after the count atMB_SENT_COUNT($010008), per the sentinel convention above. Any working buffer goes at$020000or above, never in$010000-$01000F. -
Assemble + commit the
.j64. AddbenchNNN.s(and any RISC companion) to the loop inbuild.sh, then run it (needs the toolchain:make jaguar-toolchain-build && eval "$(tools/jaguar-toolchain/setup.sh env)") and commit the resultingbenchNNN.j64alongside the source. CI never assembles -- if the committed.j64and the.sit should come from diverge, nothing catches that automatically, so regenerate and diff before committing. -
Harness tool. Copy
test/tools/test_microbench_68k.c(ortest_microbench_gpu_arith.cif it's a GPU/DSP payload) totest/tools/test_microbench_NNN.c, and update: the DONE magic, expected iteration count, andMB_FRAME_BUDGET(measure the real completion frame across at least harness defaults,virtualjaguar_dram_timing=enabled,virtualjaguar_gpu_pipeline_timing=enabled, and PAL before picking a budget -- see the "Frame budget" tables above for the methodology and how much headroom the existing five carry). The harness's--optionflag silently ignores any key it doesn't recognize -- there is no error, no warning, and thedone_framecomes back identical to the default run, which is indistinguishable from "this ROM doesn't respond to the option." This is exactly what produced the falsegpu_pipeline_timing=86(flat) claim fixed in commit10c9a62: the option name was missing itsvirtualjaguar_prefix. Always pass the full, prefixed core option key, e.g.:./test/tools/test_microbench_NNN ./virtualjaguar_libretro.dylib \ test/microbench/benchNNN.j64 \ --option virtualjaguar_gpu_pipeline_timing=enabledand confirm at least one row of your frame-budget table actually moves off the default
done_framebefore publishing a table where it doesn't -- a flat row you haven't independently verified is more likely a typo'd option key than a genuinely inert configuration. -
Wire the Makefile. Add a
test/tools/test_microbench_NNNbuild rule next to the other five (same recipe shape,test/tools/test_microbench_68kis the template), and append the new tool +.j64toMICROBENCH_TOOLS/MICROBENCH_ROMSin themicrobenchtarget. -
Document it. Add a row to the ROM table at the top of this file (what it isolates, DONE magic), and a "Measured for
benchNNN.j64" frame-budget table in the style of the existing five if the numbers are interesting. -
Verify.
make microbenchmust still exit 0 with all six PASS lines. No CI change is needed --.github/workflows/microbench.yml's path filters already cover any new file undertest/microbench/or atest/tools/test_microbench_*name.
Honesty notes
- The 68K loop executes from cart ROM at
$802000, so the measurement includes 68K instruction-fetch cost through the cart window — it is not pure register-op throughput. Relevant when comparing numbers across the five ROMs, since the GPU/DSP benches will run from local RISC RAM instead. - Interrupts are masked (
move.w #$2700,sr) before the loop. The HLE BIOS leaves video/timer IRQs live with RTE stubs installed; letting them fire would put BIOS-path 68K cycles inside the measured window and make the loop's cost depend on video timing. - TOM still renders a (blank) display throughout. Video cost is not isolated away and cannot be, short of blanking the screen.