A second emulator agrees, the bus was never counted, and the DMAC loses by one clock
Three things, and the last one reversed itself when the datasheet arrived.
A SECOND EMULATOR. tools/bench/c68k/ links px68k's C68K core into a headless
harness -- no SDL, no ROMs, no emulated machine, because the decoder touches
nothing but RAM, the control block and GVRAM. decode.s is now pixel-exact under
two independent CPU cores, and cycle-table error against MAME is bounded at
3.3%, running against us. MAME 0.277's M68000 turns out to be the MICROCODE
core, not Musashi (m68000.lst + m68000gen.py), so this is two structurally
different timing models agreeing rather than two tables. FINDINGS 28.8's "V4
costs more than RAW" reproduces independently. FINDINGS 37.
THE BUS. Nothing since FINDINGS 24 had counted the 68000's local memory bus --
one 4-clock cycle at a time, carrying instruction prefetch as well as data. The
decoder occupies 86.7% of it and PREFETCH IS 62% OF THAT TRAFFIC, so a data-only
count understates occupancy by 2x. Two sources check each other: c68k_bench
counts every bus callback exactly, and a static walk of decode.lst supplies the
prefetch no emulator here can report. The walk reproduces the measured data half
to 0.04%, which is what licenses its prefetch half, and 15_bus_occupancy.py is a
gate rather than a report because every bus figure depends on that check.
FINDINGS 38.
THE DMAC CHAIN LOSES. FINDINGS 29.6 named it the one uncosted lever. Costed from
bus arithmetic -- a read cycle plus a write cycle, 8 clocks a pixel -- it scored
1/120 frames over budget against the v6 span's 10/120 and looked decisive. Then
the MC68450 manual (Motorola Jul 1989, now at ~/src/mc68450.pdf): Fig 4-25 sheet
4 puts a dual-address word between two 16-bit ports at 9 CLOCKS, because note 2
gives the DMAC 4-clock reads and 5-clock WRITES. The 68000 writes in 4.
DMAC 9.000 clocks/pixel datasheet
v6 9.152 clocks/pixel measured, FINDINGS 30
1.7%. Scored additively, 86% of what remains of the DMAC's advantage is v6's
24-pixel padding quantum -- a property of its unrolled movem chain, fixable in
software with a finer tail chain, worth 55/120 -> 18/120 against the DMAC's
12/120. Recommendation: fix the quantum, drop the DMAC. Six frames does not buy
a reserved channel, a two-region container layout and a timing dependency
neither emulator here can verify. The container is identical either way -- v6's
record and an HD63450 chaining entry are both 6 bytes, so the chain array IS the
span table -- so nothing is foreclosed. FINDINGS 39.
TWO CORRECTIONS TO MY OWN WORK IN THE SAME SESSION:
- I argued FINDINGS 35's flat CPU debit for the disk was too pessimistic and
rescored the window at 53/120 with max(CPU, bus). Wrong. A 68000 has no cache
and a two-word prefetch queue, so it stalls the moment another master takes
the bus, and the MC68450 hands the bus over in SLABS under limited-rate
auto-request rather than interleaving per operand. DMA is additive. 84/120
stands and 14_dmac_chain.py reproduces it exactly. What 86.7% occupancy really
says is that there is almost no room to overlap anything. FINDINGS 38.3.
- The first DMAC costing was derived where a primary source existed. Both wrong
answers were confident and both were caught by reading the manual.
Also landed:
- FINDINGS 5's 8 clocks/word for the SCSI DMA, STATUS's own "most load-bearing
unmeasured number", is now bracketed by the datasheet: 5 clk/word with the bus
held, ~12 if the DMAC arbitrates per word. 8 is a supported midpoint, and
which end applies is a player design decision worth 7 clocks a word on a
480 KB/s stream. FINDINGS 39.7.
- check.sh gains two gates: the C68K pixel-exact decode (seconds, no MAME) and
the bus-model self-check. Both skip cleanly without a px68k checkout.
- spanned blocks are now charged their mode-map dispatch, which FINDINGS 30.7
flagged as uncounted in 12_span_tradeoff.py.
- MAME timed runs must be budgeted by WALL CLOCK, not -seconds_to_run: this box
runs x68000 at ~0.033x realtime and two runs were killed by their own timeout.
That is why the all-RAW cell in 37.3 is empty. The C68K harness does the same
work in seconds because it emulates a CPU and not a machine.
Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# Build the headless C68K cycle harness. PX68K points at a px68k checkout;
|
||||
# only m68000/c68k.c and the two header dirs are used -- no SDL, no ROMs.
|
||||
PX68K ?= $(HOME)/src/px68k
|
||||
# -no-pie is LOAD-BEARING, not a tidy-up. C68K is 64-bit-unsafe on purpose:
|
||||
# its MOVEM macros do `src = (UINT32)(&D0)` -- they truncate the host address of
|
||||
# the CPU register file to 32 bits and dereference it -- and C68k_Set_Fetch
|
||||
# stores the opcode-fetch base in a UINT32 too. Under the default PIE the
|
||||
# binary loads near 0x555555550000 and the first movem segfaults. -no-pie puts
|
||||
# the image at 0x400000, and the harness mmaps its arena with MAP_32BIT, so
|
||||
# every pointer C68K truncates still round-trips.
|
||||
CFLAGS = -O2 -fno-strict-aliasing -no-pie -Wall -Wno-unused-result \
|
||||
-Wno-int-to-pointer-cast -Wno-pointer-to-int-cast \
|
||||
-I$(PX68K)/m68000 -I$(PX68K)/x11 -I$(PX68K)/win32api
|
||||
|
||||
c68k_bench: harness.c $(PX68K)/m68000/c68k.c
|
||||
$(CC) $(CFLAGS) -no-pie -o $@ harness.c $(PX68K)/m68000/c68k.c
|
||||
|
||||
clean:
|
||||
rm -f c68k_bench
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Two emulators, one decoder: MAME's cycles against px68k's C68K core.
|
||||
|
||||
python3 tools/bench/c68k/compare.py [--mame tmp/mame_timed.log]
|
||||
[--c68k tmp/c68k.log]
|
||||
|
||||
WHY THIS EXISTS. Every 68000 cycle figure in FINDINGS 24-35 comes from one
|
||||
instrument. This puts a second, structurally different one next to it:
|
||||
|
||||
MAME 0.277 M68000 is the microcode core (src/devices/cpu/m68000/m68000.lst
|
||||
+ m68000gen.py), NOT Musashi -- timing emerges from the 68000's
|
||||
modelled micro-sequence and 4-clock bus cycles.
|
||||
C68K a static per-instruction cycle table hand-transcribed from the
|
||||
Motorola manual (ORI_CLOCKS_* / EA_CLOCKS_* in c68kmacro.h).
|
||||
|
||||
Those are two different ways of being right, so agreement is evidence and
|
||||
disagreement localises to whichever instruction the anchors separate. NEITHER
|
||||
charges GVRAM wait states, so both are the same lower bound on real hardware.
|
||||
"""
|
||||
import argparse, re, sys
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--mame", default="tmp/mame_timed.log")
|
||||
ap.add_argument("--c68k", default="tmp/c68k.log")
|
||||
ap.add_argument("--meta", default="tmp/decode_meta.lua")
|
||||
a = ap.parse_args()
|
||||
|
||||
meta = open(a.meta).read()
|
||||
fps = int(re.search(r"fps=(\d+)", meta).group(1))
|
||||
budget = 10_000_000 / fps
|
||||
# anchor name -> stream offset, so the two logs can be joined: decode.lua
|
||||
# reports by name, the C68K harness by offset.
|
||||
names = {int(o): n for n, o in re.findall(r'name="([^"]+)", off=(\d+)', meta)}
|
||||
|
||||
mame = {}
|
||||
txt = open(a.mame, errors="replace").read()
|
||||
for nm, cyc in re.findall(r"\[DEC\] frame @ (.+?)\n.*?->\s+(\d+) cycles/frame", txt):
|
||||
mame[nm.strip()] = int(cyc)
|
||||
m_seq = re.search(r"full \d+-frame pass.*?\n.*?->\s+(\d+) cycles/frame", txt)
|
||||
|
||||
c68k, c_seq = {}, None
|
||||
for line in open(a.c68k, errors="replace"):
|
||||
m = re.search(r"anchor off=(\d+)\s+(\d+) cyc", line)
|
||||
if m and int(m.group(1)) in names:
|
||||
c68k[names[int(m.group(1))]] = int(m.group(2))
|
||||
m = re.search(r"sequential pass = (\d+) cyc, mean (\d+)", line)
|
||||
if m:
|
||||
c_seq = int(m.group(2))
|
||||
|
||||
if not mame:
|
||||
sys.exit(f"no MAME anchor timings in {a.mame} -- run decode.lua WITHOUT "
|
||||
f"DLX_VERIFY_ONLY=1 and give -seconds_to_run enough to finish")
|
||||
|
||||
w = max(len(n) for n in c68k) + 2
|
||||
print(f"{'anchor':<{w}}{'MAME':>10}{'C68K':>10}{'delta':>9} {'MAME':>7}{'C68K':>7} of a {fps}fps frame")
|
||||
rows = []
|
||||
for nm, c in c68k.items():
|
||||
m = mame.get(nm)
|
||||
if m is None:
|
||||
print(f"{nm:<{w}}{'--':>10}{c:>10}{'':>9} {'--':>7}{100*c/budget:>6.1f}% (MAME run did not reach it)")
|
||||
continue
|
||||
d = 100 * (c - m) / m
|
||||
rows.append(d)
|
||||
print(f"{nm:<{w}}{m:>10}{c:>10}{d:>+8.2f}% {100*m/budget:>6.1f}%{100*c/budget:>6.1f}%")
|
||||
|
||||
if m_seq and c_seq:
|
||||
m, c = int(m_seq.group(1)), c_seq
|
||||
d = 100 * (c - m) / m
|
||||
print(f"{'MEAN over the window':<{w}}{m:>10}{c:>10}{d:>+8.2f}% "
|
||||
f"{100*m/budget:>6.1f}%{100*c/budget:>6.1f}%")
|
||||
|
||||
if rows:
|
||||
print(f"\nspread over {len(rows)} anchors: {min(rows):+.2f}% .. {max(rows):+.2f}%")
|
||||
print("C68K reads HIGH throughout." if min(rows) > 0 else
|
||||
"C68K reads high on some anchors and low on others.")
|
||||
print("Neither instrument charges GVRAM wait states, so both are the same\n"
|
||||
"LOWER BOUND: this bounds cycle-table error, not the distance to a\n"
|
||||
"real X68000 (docs/BENCHMARK.md Tier 3).")
|
||||
@@ -0,0 +1,329 @@
|
||||
/* Headless C68K cycle harness -- an independent second opinion on every
|
||||
* 68000 cycle figure in FINDINGS 24-35.
|
||||
*
|
||||
* WHY. Every one of those numbers comes from ONE instrument: MAME 0.277's
|
||||
* Musashi core, timed host-side from manager.machine.time. A cycle table is a
|
||||
* hand-transcribed artefact; if Musashi's is wrong for our instruction mix, the
|
||||
* 833,333-cycle budget is wrong by the same amount and nothing in the tree
|
||||
* would show it. This runs the SAME decode.bin against the SAME
|
||||
* decode_data.bin under px68k's C68K core, which has a completely separate
|
||||
* cycle table (ORI_CLOCKS_* + EA_CLOCKS_* in c68kmacro.h) written by a
|
||||
* different author from the same Motorola manual.
|
||||
*
|
||||
* WHAT IT DOES AND DOES NOT SETTLE. C68K, like MAMEs x68000, charges NO
|
||||
* GVRAM wait states -- grep the px68k tree, there is no bus-timing model
|
||||
* anywhere in x68k/*.c. So this is the same LOWER BOUND, measured twice. It
|
||||
* cross-checks the cycle table. It says nothing about real-hardware wait
|
||||
* states; that needs XM6 TypeG or an actual X68000 (docs/BENCHMARK.md Tier 3).
|
||||
*
|
||||
* WHY NOT JUST RUN px68k. The decoder touches nothing but RAM, the control
|
||||
* block and GVRAM: no IPL, no CRTC, no MFP, no interrupts (the MAME rig masks
|
||||
* them with SR=$2700). Booting a whole emulated machine would add SDL, ROMs
|
||||
* and a 55Hz sampling clock to a measurement that wants none of them. Linking
|
||||
* the core alone also buys EXACTNESS: the stop cycle is captured inside the
|
||||
* write callback, so a frame's cost is known to within one instruction rather
|
||||
* than MAME's 1/55.46 s. That is why the anchors here run iter=1 -- decode.lua
|
||||
* only iterates to beat its own timing granularity.
|
||||
*
|
||||
* MEMORY MODEL mirrors px68k exactly, because the core requires it: RAM is
|
||||
* stored BYTE-SWAPPED (MEM[addr ^ 1], mem_wrap.c:420) so C68K's
|
||||
* READ_IMM_16() = *(UINT16 *)PC works with no swap on a little-endian host.
|
||||
* GVRAM word writes discard the high byte, as the hardware and MAME's
|
||||
* gvram_w case 0x0100 both do.
|
||||
*
|
||||
* The harness is self-validating: --dump writes the decoded screen and
|
||||
* verify_c68k.py checks it pixel-for-pixel against tools/encoder/dlx.py. If
|
||||
* the byte-swap or the memory map were wrong the decode could not come out
|
||||
* exact, so a green verify is what licenses the cycle numbers next to it.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include "c68k.h"
|
||||
|
||||
/* c68k.c declares these extern and tests BusErrHandling every instruction. */
|
||||
unsigned int BusErrHandling = 0;
|
||||
unsigned int BusErrAdr = 0;
|
||||
void Error(const char *s) { fprintf(stderr, "c68k: %s\n", s); exit(3); }
|
||||
void p6logd(const char *fmt, ...) { (void)fmt; }
|
||||
|
||||
#define ADRMASK 0xFFFFFFu
|
||||
#define ARENA (16u << 20)
|
||||
#define RAMTOP 0x200000u
|
||||
#define GV_LO 0xC00000u
|
||||
#define GV_HI 0xC80000u
|
||||
|
||||
#define FLAG 0x18000u
|
||||
#define ITER 0x18008u
|
||||
#define NFR 0x1800Cu
|
||||
#define FPTR 0x18010u
|
||||
#define CB1 0x20000u
|
||||
#define CB4 0x22000u
|
||||
#define STREAM 0x30000u
|
||||
#define CODE 0x10000u
|
||||
#define STACK 0x8000u
|
||||
#define GVBASE 0xC00000u
|
||||
#define ROWBYTES 1024u
|
||||
#define CPUHZ 10000000.0
|
||||
|
||||
static unsigned char *buf; /* byte-swapped, px68k convention */
|
||||
|
||||
/* Data bus cycles the 68000 issues. Every callback below is exactly one
|
||||
* 68000 bus cycle -- C68K splits a long access into two word calls, which is
|
||||
* what the 16-bit bus does too -- so counting calls counts bus cycles. This
|
||||
* does NOT include instruction prefetch, which C68K reads straight through the
|
||||
* fetch pointer with no callback; the count is therefore a LOWER BOUND on the
|
||||
* CPU's bus occupancy, and the headroom it implies is an UPPER BOUND.
|
||||
* It is still the measurement that matters for FINDINGS 29.6: if the decoder's
|
||||
* data accesses alone left no room, a DMAC could not overlap with it at all. */
|
||||
static long long bus_r, bus_w;
|
||||
static int in_exec = 0;
|
||||
|
||||
/* Cycle capture. A single C68k_Exec slice runs the whole pass; the FLAG
|
||||
* writes inside it record where the timed region starts and ends, so the
|
||||
* count excludes nothing and includes no spin-loop tail. */
|
||||
static long long slice;
|
||||
static long long cyc_start = -1, cyc_stop = -1;
|
||||
static int desync = 0;
|
||||
|
||||
static unsigned char rd8 (unsigned int a){ if (in_exec) bus_r++; return buf[(a & ADRMASK) ^ 1]; }
|
||||
static unsigned short rd16(unsigned int a){ if (in_exec) bus_r++; a &= ADRMASK; return (unsigned short)(buf[a] | (buf[a+1] << 8)); }
|
||||
static unsigned short peek16(unsigned int a){ a &= ADRMASK; return (unsigned short)(buf[a] | (buf[a+1] << 8)); }
|
||||
static unsigned int rd32(unsigned int a){ return ((unsigned int)peek16(a) << 16) | peek16(a+2); }
|
||||
|
||||
static void wr8(unsigned int a, unsigned char d)
|
||||
{
|
||||
if (in_exec) bus_w++;
|
||||
a &= ADRMASK;
|
||||
if (a >= GV_LO && a < GV_HI) { if (a & 1) buf[a ^ 1] = d; return; } /* high byte discarded */
|
||||
buf[a ^ 1] = d;
|
||||
}
|
||||
|
||||
/* Only writes made BY the 68000 mean anything here. The harness sets FLAG
|
||||
* itself during setup, and a `move.l` to FLAG arrives as two word writes, so
|
||||
* the hook sees a half-updated long in between -- clearing FLAG from $FF to 0
|
||||
* momentarily reads back as $FF again. Without in_exec that transient
|
||||
* recorded a run's stop cycle before the run had started, and every frame
|
||||
* after the first came out as the whole slice. */
|
||||
static void note_flag(void)
|
||||
{
|
||||
unsigned int v = rd32(FLAG);
|
||||
long long now = slice - C68K.ICount;
|
||||
if (!in_exec) return;
|
||||
if (v == 1 && cyc_start < 0) cyc_start = now;
|
||||
else if (v == 0xFF || v == 0xEE) {
|
||||
if (cyc_stop < 0) { cyc_stop = now; desync = (v == 0xEE); }
|
||||
C68K.ICount = 0; /* stop the slice; we keep our own count */
|
||||
}
|
||||
}
|
||||
|
||||
static void wr16(unsigned int a, unsigned short d)
|
||||
{
|
||||
if (in_exec) bus_w++;
|
||||
a &= ADRMASK;
|
||||
if (a >= GV_LO && a < GV_HI) { buf[a] = (unsigned char)d; buf[a+1] = 0; return; }
|
||||
buf[a] = (unsigned char)d; buf[a+1] = (unsigned char)(d >> 8);
|
||||
if (a >= FLAG && a < FLAG + 4) note_flag();
|
||||
}
|
||||
|
||||
static void wr32(unsigned int a, unsigned int d){ wr16(a, (unsigned short)(d >> 16)); wr16(a+2, (unsigned short)d); }
|
||||
|
||||
static void push(unsigned int a, const unsigned char *s, size_t n)
|
||||
{
|
||||
for (size_t i = 0; i < n; i++) wr8((unsigned int)(a + i), s[i]);
|
||||
}
|
||||
|
||||
/* Prime the screen exactly as decode.lua's setup() does: active area at index
|
||||
* 0, letterbox at the darkest palette entry. A SKIP block in frame 0 is a
|
||||
* claim about THIS, so it is part of the decode contract. Pass 2 re-primes,
|
||||
* because pass 1 left one frame's worth of residue on the screen and frame 0's
|
||||
* SKIP blocks would otherwise inherit it. */
|
||||
static void prime(unsigned int W, unsigned int H, unsigned int yoff, unsigned int dark)
|
||||
{
|
||||
for (unsigned int y = 0; y < 256; y++) {
|
||||
unsigned short v = (y < yoff || y >= yoff + H) ? (unsigned short)dark : 0;
|
||||
for (unsigned int x = 0; x < W; x++) wr16(GVBASE + y*ROWBYTES + x*2, v);
|
||||
}
|
||||
}
|
||||
|
||||
static unsigned char *slurp(const char *p, size_t *n)
|
||||
{
|
||||
FILE *f = fopen(p, "rb");
|
||||
if (!f) { fprintf(stderr, "cannot open %s\n", p); exit(2); }
|
||||
fseek(f, 0, SEEK_END); long L = ftell(f); fseek(f, 0, SEEK_SET);
|
||||
unsigned char *b = malloc((size_t)L);
|
||||
if (fread(b, 1, (size_t)L, f) != (size_t)L) { fprintf(stderr, "short read %s\n", p); exit(2); }
|
||||
fclose(f); *n = (size_t)L; return b;
|
||||
}
|
||||
|
||||
/* Run one pass and return its exact cycle count. */
|
||||
static long long run(unsigned int off, unsigned int nfr, unsigned int iter)
|
||||
{
|
||||
cyc_start = cyc_stop = -1; desync = 0; bus_r = bus_w = 0;
|
||||
wr32(FLAG, 0); wr32(ITER, iter); wr32(NFR, nfr); wr32(FPTR, STREAM + off);
|
||||
C68k_Reset(&C68K);
|
||||
C68k_Set_Reg(&C68K, C68K_SR, 0x2700); /* supervisor, all IRQs masked */
|
||||
C68k_Set_Reg(&C68K, C68K_A7, STACK);
|
||||
C68k_Set_Reg(&C68K, C68K_PC, CODE);
|
||||
slice = 2000000000LL;
|
||||
in_exec = 1;
|
||||
C68k_Exec(&C68K, (INT32)slice);
|
||||
in_exec = 0;
|
||||
if (cyc_stop < 0) { fprintf(stderr, "TIMEOUT off=%u nfr=%u -- decoder never set FLAG\n", off, nfr); exit(4); }
|
||||
if (desync) { fprintf(stderr, "BITSTREAM DESYNC off=%u nfr=%u\n", off, nfr); exit(5); }
|
||||
/* A runaway is not a slow frame. Without this a bad record walk reports a
|
||||
* two-billion-cycle "frame" as if it were a measurement. */
|
||||
if (cyc_stop - cyc_start > 40LL * nfr * iter * 833333LL) {
|
||||
fprintf(stderr, "RUNAWAY off=%u nfr=%u: %lld cyc (start=%lld stop=%lld) "
|
||||
"PC=%06X FLAG=%08X SCR_N=%08X SCR_END=%08X len=%u\n",
|
||||
off, nfr, cyc_stop - cyc_start, cyc_start, cyc_stop,
|
||||
C68k_Get_Reg(&C68K, C68K_PC) & 0xFFFFFF, rd32(FLAG),
|
||||
rd32(0x18014), rd32(0x18018), rd32(STREAM + off));
|
||||
exit(6);
|
||||
}
|
||||
return cyc_stop - cyc_start;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *fcode = "tmp/decode.bin", *fdata = "tmp/decode_data.bin", *dump = NULL;
|
||||
unsigned int cb1_len=0, cb4_len=0, pal_len=0, stream_len=0, nframes=0, H=192, W=256, fps=12;
|
||||
unsigned int dark = 255;
|
||||
unsigned int anch[32]; int nanch = 0;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "--code")) fcode = argv[++i];
|
||||
else if (!strcmp(argv[i], "--data")) fdata = argv[++i];
|
||||
else if (!strcmp(argv[i], "--dump")) dump = argv[++i];
|
||||
else if (!strcmp(argv[i], "--cb1")) cb1_len = (unsigned)atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--cb4")) cb4_len = (unsigned)atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--pal")) pal_len = (unsigned)atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--stream")) stream_len = (unsigned)atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--nframes"))nframes = (unsigned)atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--W")) W = (unsigned)atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--H")) H = (unsigned)atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--fps")) fps = (unsigned)atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--dark")) dark = (unsigned)atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--anchor")) { if (nanch < 32) anch[nanch++] = (unsigned)strtoul(argv[++i], NULL, 10); }
|
||||
else { fprintf(stderr, "unknown arg %s\n", argv[i]); return 2; }
|
||||
}
|
||||
if (!nframes || !stream_len) { fprintf(stderr, "need --nframes and --stream (from decode_meta.lua)\n"); return 2; }
|
||||
|
||||
/* MAP_32BIT: C68K keeps its fetch base in a UINT32, so the arena must live
|
||||
* below 4 GB or every opcode fetch reads a truncated pointer. */
|
||||
buf = mmap(NULL, ARENA, PROT_READ|PROT_WRITE,
|
||||
MAP_PRIVATE|MAP_ANONYMOUS|MAP_32BIT, -1, 0);
|
||||
if (buf == MAP_FAILED) { perror("mmap MAP_32BIT"); return 2; }
|
||||
fprintf(stderr, "[C68K] arena at %p\n", (void *)buf);
|
||||
|
||||
size_t nc, nd;
|
||||
unsigned char *code = slurp(fcode, &nc), *data = slurp(fdata, &nd);
|
||||
size_t need = (size_t)cb1_len + cb4_len + pal_len + stream_len;
|
||||
if (nd < need) { fprintf(stderr, "data blob %zu B < meta's %zu B\n", nd, need); return 2; }
|
||||
|
||||
size_t o = 0;
|
||||
push(CB1, data + o, cb1_len); o += cb1_len;
|
||||
push(CB4, data + o, cb4_len); o += cb4_len;
|
||||
o += pal_len; /* palette: display only */
|
||||
push(STREAM, data + o, stream_len);
|
||||
push(CODE, code, nc);
|
||||
|
||||
/* Prime the screen exactly as decode.lua's setup() does: the active area
|
||||
* starts at index 0 and the letterbox gets the darkest palette entry.
|
||||
* A SKIP block in frame 0 is a claim about THIS, so it is part of the
|
||||
* decode contract, not decoration. */
|
||||
unsigned int yoff = (256u - H) / 2;
|
||||
prime(W, H, yoff, dark);
|
||||
|
||||
C68k_Init(&C68K);
|
||||
C68k_Set_ReadB (&C68K, rd8);
|
||||
C68k_Set_ReadW (&C68K, rd16);
|
||||
C68k_Set_WriteB(&C68K, wr8);
|
||||
C68k_Set_WriteW(&C68K, wr16);
|
||||
C68k_Set_Fetch (&C68K, 0x000000, 0xFFFFFF, (UINT32)(unsigned long)buf);
|
||||
|
||||
double frame_budget = CPUHZ / fps;
|
||||
fprintf(stderr, "[C68K] %u frames, stream %u B, budget %.0f cyc/frame @ %u fps\n",
|
||||
nframes, stream_len, frame_budget, fps);
|
||||
|
||||
/* Pass 1 -- every frame timed on its own. MAME could only afford eight
|
||||
* anchor frames because its clock is 1/55.46 s; here each frame is exact,
|
||||
* so the whole distribution comes out, which is what FINDINGS 31/35 score
|
||||
* against. Record layout: [u32 len][768 mode][payload], next record start
|
||||
* rounded up to 4 (FINDINGS 28.3). `len` counts the mode header TOO --
|
||||
* decode.s sets SCR_END from the address AFTER the length word, so the
|
||||
* record is 4 + len bytes, not 4 + 768 + len. */
|
||||
printf("frame,offset,cycles,pct_of_frame,bus_reads,bus_writes,bus_pct\n");
|
||||
unsigned int off = 0;
|
||||
long long sum = 0, busr_tot = 0, busw_tot = 0;
|
||||
for (unsigned int f = 0; f < nframes; f++) {
|
||||
long long c = run(off, 1, 1);
|
||||
sum += c;
|
||||
long long br = bus_r, bw = bus_w;
|
||||
busr_tot += br; busw_tot += bw;
|
||||
printf("%u,%u,%lld,%.2f,%lld,%lld,%.2f\n", f, off, c,
|
||||
100.0 * c / frame_budget, br, bw, 100.0 * 4.0 * (br + bw) / c);
|
||||
unsigned int len = rd32(STREAM + off);
|
||||
off = (off + 4 + len + 3) & ~3u;
|
||||
}
|
||||
fprintf(stderr, "[C68K] per-frame sum = %lld cyc, mean %.0f (%.1f%% of a %u fps frame)\n",
|
||||
sum, (double)sum / nframes, 100.0 * sum / nframes / frame_budget, fps);
|
||||
/* The number FINDINGS 29.6 needs. A 68000 bus cycle is 4 clocks, so a
|
||||
* frame of `sum/nframes` clocks has room for a quarter that many bus
|
||||
* cycles. What the decoder's DATA accesses do not use is the headroom a
|
||||
* DMAC could paint spans in -- minus instruction prefetch, which is not
|
||||
* counted here, so this OVERSTATES the headroom. */
|
||||
{
|
||||
double mean_cyc = (double)sum / nframes;
|
||||
double slots = mean_cyc / 4.0;
|
||||
double used = (double)(busr_tot + busw_tot) / nframes;
|
||||
fprintf(stderr, "[C68K] data bus: %.0f reads + %.0f writes = %.0f cycles/frame "
|
||||
"of %.0f slots = %.1f%% occupied\n",
|
||||
(double)busr_tot / nframes, (double)busw_tot / nframes, used, slots,
|
||||
100.0 * used / slots);
|
||||
fprintf(stderr, "[C68K] headroom >= %.0f bus cycles/frame "
|
||||
"(%.1f%%), MINUS instruction prefetch, which is not counted\n",
|
||||
slots - used, 100.0 * (slots - used) / slots);
|
||||
}
|
||||
|
||||
/* Pass 2 -- one sequential run of the whole window. Two jobs: it is the
|
||||
* only honest correctness test (SKIP makes every frame a claim about the
|
||||
* one before it), and its total against pass 1's sum prices the outer
|
||||
* frame-loop overhead the per-frame runs each pay once. */
|
||||
prime(W, H, yoff, dark);
|
||||
long long seq = run(0, nframes, 1);
|
||||
fprintf(stderr, "[C68K] sequential pass = %lld cyc, mean %.0f (%.1f%%); "
|
||||
"per-frame sum is %+.3f%% of it\n",
|
||||
seq, (double)seq / nframes, 100.0 * seq / nframes / frame_budget,
|
||||
100.0 * (sum - seq) / seq);
|
||||
|
||||
/* Dump BEFORE the anchors run. They decode single frames onto this same
|
||||
* screen, so anything after them is not the sequential reconstruction and
|
||||
* verify_c68k.py would report every pixel wrong. */
|
||||
if (dump) {
|
||||
/* Active area only, one byte per pixel -- the low byte of each GVRAM
|
||||
* word, which is all the hardware keeps. */
|
||||
FILE *g = fopen(dump, "wb");
|
||||
if (!g) { perror(dump); return 2; }
|
||||
for (unsigned int y = 0; y < H; y++)
|
||||
for (unsigned int x = 0; x < W; x++) {
|
||||
unsigned char p = (unsigned char)rd16(GVBASE + (yoff + y)*ROWBYTES + x*2);
|
||||
fwrite(&p, 1, 1, g);
|
||||
}
|
||||
fclose(g);
|
||||
fprintf(stderr, "[C68K] screen dumped to %s (%ux%u indices)\n", dump, W, H);
|
||||
}
|
||||
/* Pass 3 -- decode.lua's timing anchors, at the same stream offsets, so the
|
||||
* two instruments are quoted on the same eight frames. The four synthetic
|
||||
* single-mode frames live past the end of the real stream and so are not
|
||||
* reachable by the record walk in pass 1; they are the ones that price the
|
||||
* modes separately (prep_dlx.py), which is where two cycle tables are most
|
||||
* likely to disagree. */
|
||||
for (int i = 0; i < nanch; i++) {
|
||||
long long c = run(anch[i], 1, 1);
|
||||
fprintf(stderr, "[C68K] anchor off=%-8u %8lld cyc %5.1f%% of a %u fps frame\n",
|
||||
anch[i], c, 100.0 * c / frame_budget, fps);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# Run the C68K harness against whatever tools/bench/prep_dlx.py last laid out,
|
||||
# so it measures byte-for-byte the same code and container MAME did.
|
||||
# tools/bench/c68k/run.sh [out.csv]
|
||||
set -e
|
||||
cd "$(dirname "$0")/../../.."
|
||||
M=tmp/decode_meta.lua
|
||||
[ -f "$M" ] || { echo "no $M -- run tools/bench/prep_dlx.py first"; exit 2; }
|
||||
g() { sed -n "s/.*[ ,{]$1=\([0-9]*\).*/\1/p" "$M" | head -1; }
|
||||
# Same anchor offsets decode.lua times, so the two instruments are quoted on the
|
||||
# same frames -- including the four synthetic single-mode ones, which sit past
|
||||
# the end of the real stream and price each block mode on its own.
|
||||
ANCH=()
|
||||
while read -r o; do ANCH+=(--anchor "$o"); done < <(sed -n 's/.*off=\([0-9]*\).*/\1/p' "$M")
|
||||
tools/bench/c68k/c68k_bench \
|
||||
--code tmp/decode.bin --data tmp/decode_data.bin \
|
||||
--cb1 "$(g cb1_len)" --cb4 "$(g cb4_len)" --pal "$(g pal_len)" \
|
||||
--stream "$(g stream_len)" --nframes "$(g nframes)" \
|
||||
--W "$(g W)" --H "$(g H)" --fps "$(g fps)" --dark "$(g dark)" \
|
||||
"${ANCH[@]}" \
|
||||
--dump tmp/c68k_screen.bin > "${1:-tmp/c68k_frames.csv}" 2> >(tee tmp/c68k.log >&2)
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Is the C68K harness's decode pixel-exact against the reference decoder?
|
||||
|
||||
python3 tools/bench/c68k/verify_c68k.py <in.dlx> --nframes N
|
||||
|
||||
This is the licence for every cycle number the harness prints. The harness
|
||||
rebuilds px68k's memory model from scratch -- byte-swapped RAM, GVRAM word
|
||||
writes that discard the high byte, a hand-rolled 24-bit map -- and any of that
|
||||
being subtly wrong would still produce plausible-looking cycle counts. It could
|
||||
not produce a pixel-exact 80-frame temporal recursion.
|
||||
|
||||
Unlike tools/bench/verify_decode.py this compares palette INDICES, not rendered
|
||||
RGB: the harness dumps the low byte of each GVRAM word directly, so there is no
|
||||
palette round-trip to model and no snapshot geometry to unpick.
|
||||
"""
|
||||
import argparse, sys
|
||||
sys.path.insert(0, "tools/encoder")
|
||||
import numpy as np
|
||||
from dlx import DLX
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("container")
|
||||
ap.add_argument("--dump", default="tmp/c68k_screen.bin")
|
||||
ap.add_argument("--nframes", type=int, default=None)
|
||||
a = ap.parse_args()
|
||||
|
||||
d = DLX(a.container)
|
||||
NF = a.nframes if a.nframes is not None else d.nframes
|
||||
if NF > d.nframes:
|
||||
sys.exit(f"--nframes {NF} exceeds the container's {d.nframes}")
|
||||
|
||||
canvas = np.zeros((d.H, d.W), np.uint8)
|
||||
for f in range(NF):
|
||||
d.paint(canvas, f)
|
||||
|
||||
got = np.fromfile(a.dump, np.uint8)
|
||||
if got.size != d.H * d.W:
|
||||
sys.exit(f"FAIL 1. dump is {got.size} B, expected {d.H*d.W}")
|
||||
got = got.reshape(d.H, d.W)
|
||||
|
||||
if not np.array_equal(got, canvas):
|
||||
bad = got != canvas
|
||||
by, bx = np.where(bad)
|
||||
blocks = sorted(set(zip((by // 4).tolist(), (bx // 4).tolist())))
|
||||
sys.exit(f"FAIL 2. frame {NF-1} not pixel-exact under C68K: {bad.sum()} px in "
|
||||
f"{len(blocks)} blocks differ; first block "
|
||||
f"(by={blocks[0][0]}, bx={blocks[0][1]})")
|
||||
|
||||
print(f"OK {NF} frames decoded on px68k's C68K core, final frame pixel-exact "
|
||||
f"against tools/encoder/dlx.py")
|
||||
print(f" {d.W}x{d.H}, {d.nb} blocks/frame, k1={d.k1} k4={d.k4}; the memory "
|
||||
f"model (byte-swapped RAM, high-byte-discarding GVRAM) is therefore right")
|
||||
Reference in New Issue
Block a user