src/player/load.i expands both codebooks to word-per-pixel form and packs the palette to GGGGGRRRRRBBBBBI out of the RAW container header, byte-exact against tools/bench/dlxload.py on both CPU cores. The palette half is gated on words read back out of the palette registers at $E82000, so "the words reached the hardware" is part of what passes. ROADMAP P1 is done; P2's encoder half (a reserved black entry, 23.4) is not, and is a re-encode rather than an edit. A scene change costs 18.96 ms of 68000 time, 22.8% of one 12 fps frame; boot costs 24.70 ms. The scratch tables describe the CRTC, not the scene, so pal_tables is a separate entry point built once at boot -- 5.29 ms off every scene change. The one that moves something: the scene header is 5,920 B that no rate table in this tree included, because it belongs to no frame record. In FINDINGS 51.3's currency it is divided by the surplus pipe - wire, so it is hypersensitive: 138 ms of extra refill climb at 488 KB/s and 1.099 s at 451.4 KB/s, for the same bytes. tools/analysis/22_scene_load.py prices it across explicit rates. Recorded as open: the two CPU cores agree to <3% on every stage but the table build, where they differ by 16.4%. px68k's C68K charges a flat 50 clocks for MULU/MULS (c68kmacro.h:1869) where the 68000 charges 38+2n, which explains 4,608 of the 8,703 clock gap. 4,095 clocks are unexplained. Nothing else in src/player/ multiplies, so no figure in FINDINGS 24-52 is affected. decode.s and stream.s are untouched; decode.bin is still 1,296 B at the same MD5. check.sh gains a stage that gates byte-exactness on both cores and deliberately does not gate the cycle counts -- MAME's clock is 1/55.46 s and a wall timing would make the green light host-sensitive. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
451 lines
22 KiB
C
451 lines
22 KiB
C
/* 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 LFLAG 0x18040u /* src/player/load.i's control block */
|
|
#define LHDR 0x18044u
|
|
#define LDARK 0x18048u
|
|
#define LMODE 0x18054u
|
|
#define LITER 0x18058u
|
|
#define GPAL 0xE82000u
|
|
#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. */
|
|
/* Which flag word the run watches. decode.s and stream.s use FLAG; the
|
|
* load-time transforms of src/player/load.i use their own, so that a player
|
|
* could eventually contain both without one clearing the other's state. The
|
|
* VALUES mean the same thing in both (1 running, $FF done, $EE failed), which
|
|
* is why one hook serves both. */
|
|
static unsigned int flag_adr = FLAG;
|
|
|
|
static void note_flag(void)
|
|
{
|
|
unsigned int v = rd32(flag_adr);
|
|
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_adr && a < flag_adr + 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;
|
|
}
|
|
|
|
/* ---- the load-time transforms (ROADMAP P1+P2, FINDINGS 53) --------------
|
|
* The same question this harness asks of the decoder, asked of the loader: does
|
|
* a SECOND 68000 core, with its own cycle table and its own memory model,
|
|
* produce the same bytes and agree about what they cost? It also counts BUS
|
|
* cycles, which MAME cannot report -- and the bus is the resource this project
|
|
* established is the binding one (FINDINGS 38).
|
|
*/
|
|
static int run_load(const char *fcode, const char *fraw, const char *dump,
|
|
unsigned int mode, unsigned int iter,
|
|
unsigned int cb1_len, unsigned int cb4_len)
|
|
{
|
|
size_t nc, nr;
|
|
unsigned char *code = slurp(fcode, &nc), *raw = slurp(fraw, &nr);
|
|
push(STREAM, raw, nr); /* the RAW container header */
|
|
push(CODE, code, nc);
|
|
/* Poison every destination, so that a transform which writes NOTHING
|
|
* cannot pass by leaving the harness's own zeros in place. */
|
|
for (unsigned int a = CB1; a < CB1 + cb1_len; a += 2) wr16(a, 0xDEAD);
|
|
for (unsigned int a = CB4; a < CB4 + cb4_len; a += 2) wr16(a, 0xDEAD);
|
|
for (unsigned int c = 0; c < 256; c++) wr16(GPAL + c*2, 0xDEAD);
|
|
wr32(LDARK, 0xFFFFFFFFu);
|
|
/* The three scratch tables are poisoned only before a run that claims to
|
|
* build them. A run that only PACKS the palette is entitled to find them
|
|
* already built -- that is the point of pricing it separately -- so when
|
|
* this process is asked for one, it does the boot pass first, untimed,
|
|
* exactly as a player would have done at boot. Without that the pack runs
|
|
* on zeros: every entry then takes the same branch and the darkest entry
|
|
* comes out 0, which is a measurement of nothing. */
|
|
if (mode & 4)
|
|
for (unsigned int a = 0x19000; a < 0x19340; a += 2) wr16(a, 0xDEAD);
|
|
|
|
flag_adr = LFLAG;
|
|
if ((mode & 2) && !(mode & 4)) {
|
|
cyc_start = cyc_stop = -1; desync = 0;
|
|
wr32(LFLAG, 0); wr32(LHDR, STREAM); wr32(LMODE, 4); wr32(LITER, 1);
|
|
C68k_Reset(&C68K);
|
|
C68k_Set_Reg(&C68K, C68K_SR, 0x2700);
|
|
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 in the table pre-pass\n"); return 4; }
|
|
}
|
|
cyc_start = cyc_stop = -1; desync = 0; bus_r = bus_w = 0;
|
|
wr32(LFLAG, 0); wr32(LHDR, STREAM); wr32(LMODE, mode); wr32(LITER, iter);
|
|
C68k_Reset(&C68K);
|
|
C68k_Set_Reg(&C68K, C68K_SR, 0x2700);
|
|
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 -- loader never set LFLAG\n"); return 4; }
|
|
if (desync) { fprintf(stderr, "BAD HEADER -- load.i found no 'DLX3' magic\n"); return 5; }
|
|
|
|
long long cyc = (cyc_stop - cyc_start) / (iter ? iter : 1);
|
|
fprintf(stderr, "[C68K] load mode %u: %lld cyc/pass (%.2f ms at 10MHz, "
|
|
"%.1f%% of a 12fps frame), dark=%u\n", mode, cyc, cyc / 10000.0,
|
|
100.0 * cyc / (10000000.0 / 12), rd32(LDARK));
|
|
/* A 68000 bus cycle is 4 clocks. Prefetch is not counted (C68K reads
|
|
* opcodes straight through the fetch pointer), so this is a LOWER bound on
|
|
* occupancy and the headroom it implies is an UPPER bound -- same caveat as
|
|
* the decoder's figure above. */
|
|
{
|
|
double slots = (double)cyc / 4.0;
|
|
double used = (double)(bus_r + bus_w) / (iter ? iter : 1);
|
|
fprintf(stderr, "[C68K] data bus: %.0f reads + %.0f writes = %.0f of "
|
|
"%.0f cycles = %.1f%% occupied (prefetch NOT counted)\n",
|
|
(double)bus_r / iter, (double)bus_w / iter, used, slots,
|
|
100.0 * used / slots);
|
|
}
|
|
if (dump) {
|
|
FILE *g = fopen(dump, "wb");
|
|
if (!g) { perror(dump); return 2; }
|
|
for (unsigned int a = CB1; a < CB1 + cb1_len; a++) { unsigned char b = rd8(a); fwrite(&b,1,1,g); }
|
|
for (unsigned int a = CB4; a < CB4 + cb4_len; a++) { unsigned char b = rd8(a); fwrite(&b,1,1,g); }
|
|
for (unsigned int c = 0; c < 256; c++) {
|
|
unsigned short w = rd16(GPAL + c*2);
|
|
unsigned char b[2] = { (unsigned char)(w >> 8), (unsigned char)w };
|
|
fwrite(b, 1, 2, g);
|
|
}
|
|
fclose(g);
|
|
fprintf(stderr, "[C68K] load output dumped to %s (%u B)\n",
|
|
dump, cb1_len + cb4_len + 512);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
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;
|
|
const char *fraw = NULL, *loaddump = NULL;
|
|
unsigned int loadmode = 7, loaditer = 1;
|
|
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], "--loadraw")) fraw = argv[++i];
|
|
else if (!strcmp(argv[i], "--loaddump")) loaddump = argv[++i];
|
|
else if (!strcmp(argv[i], "--loadmode")) loadmode = (unsigned)atoi(argv[++i]);
|
|
else if (!strcmp(argv[i], "--loaditer")) loaditer = (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 (!fraw && (!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);
|
|
|
|
if (fraw) {
|
|
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);
|
|
return run_load(fcode, fraw, loaddump, loadmode, loaditer,
|
|
cb1_len ? cb1_len : 8192, cb4_len ? cb4_len : 2048);
|
|
}
|
|
|
|
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;
|
|
}
|