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:
@@ -13,3 +13,4 @@ __pycache__/
|
||||
*.dlx
|
||||
a.out
|
||||
tmp/
|
||||
tools/bench/c68k/c68k_bench
|
||||
|
||||
@@ -6,6 +6,11 @@ This is fundamentally a **video codec problem**, not a game-logic problem: the
|
||||
game logic is a scene table with branching input windows; the difficulty is
|
||||
pushing ~22 minutes of Don Bluth animation through a 10MHz 68000.
|
||||
|
||||
**And the binding resource is the 68000's local BUS, not its clock.** The
|
||||
decoder occupies 86.7% of it once instruction prefetch is counted, and 52 of the
|
||||
53 frames that miss the 12fps budget miss it on the bus, not the CPU
|
||||
(FINDINGS 38). Read that before optimising anything for cycles.
|
||||
|
||||
**Green-light check:** `./tools/bench/check.sh` (~3 min, needs the Blu-ray
|
||||
mounted) re-runs both display regression tests, the rate-control drift test, the
|
||||
display-path coherency counterexample and a 120-frame 68000 decode, then prints
|
||||
@@ -42,6 +47,12 @@ tools/analysis/ measurement scripts, numbered in the order they were written
|
||||
scene cut still fits at 12fps; 13 measures what fitting the
|
||||
CPU budget costs in dB (FINDINGS 31) and caches H.build so the
|
||||
search loop is seconds, not minutes.
|
||||
14 prices the HD63450 array-chain against the v6 span
|
||||
(FINDINGS 39) and prints the sensitivity that decides it;
|
||||
15 measures how much of the 68000's LOCAL bus the decoder
|
||||
occupies (FINDINGS 38) and exits non-zero if its derived
|
||||
model stops matching the harness's measurement.
|
||||
buscost.py is the shared bus-cycle table both import.
|
||||
tools/bench/ MAME Lua injection harness + 68000 benchmark sources.
|
||||
`check.sh` re-runs both display regression tests (~40 s).
|
||||
`blit.s`/`blit.lua` time the full-frame GVRAM blit on the
|
||||
@@ -54,6 +65,14 @@ tools/bench/ MAME Lua injection harness + 68000 benchmark sources.
|
||||
and R20 — do not write CRTC values anywhere else.
|
||||
`prep_dlx.py`/`decode.lua`/`verify_decode.py` load, time and
|
||||
verify `src/player/decode.s`; the verify pass is in check.sh.
|
||||
tools/bench/c68k/ headless px68k C68K harness -- a SECOND emulator for every
|
||||
68000 cycle figure (FINDINGS 37). Links only px68k's CPU core:
|
||||
no SDL, no ROMs, no emulated machine. `make PX68K=~/src/px68k`
|
||||
then `run.sh`; `verify_c68k.py` checks the decode is
|
||||
pixel-exact, which is what licenses the cycle numbers. It also
|
||||
counts BUS cycles, which MAME cannot report.
|
||||
The Makefile's -no-pie and the harness's MAP_32BIT arena are
|
||||
load-bearing: C68K truncates host pointers to 32 bits.
|
||||
tools/vasm/ vasm m68k assembler (built from source)
|
||||
tools/encoder/ hybrid VQ encoder + DLX2 container writer (working).
|
||||
DLX2 4-byte-aligns every frame record: an odd `move.l` is an
|
||||
|
||||
@@ -1428,6 +1428,11 @@ without closing it.
|
||||
|
||||
## 29. Trading bytes for cycles: the bus has 4x the headroom the CPU has (session 7)
|
||||
|
||||
> **ALSO SUPERSEDED IN PART BY 38.** "The bus has 4x the headroom the CPU has"
|
||||
> is about the SCSI pipe. The 68000's LOCAL bus is a different resource and the
|
||||
> decoder occupies 86.7% of it, so trading cycles for bytes is not free in the
|
||||
> currency that turned out to bind. 29.6's DMAC idea is costed in 39.
|
||||
>
|
||||
> **SUPERSEDED IN PART BY 30, which measured it.** The mode survives and the
|
||||
> conclusion holds, but every number in this section moved: a span costs 43.7
|
||||
> cycles + 9.152/pixel *only* in an encoder-assisted format (the obvious
|
||||
@@ -1978,6 +1983,14 @@ progress.**
|
||||
|
||||
## 35. The CPU budget has never had the disk in it (session 9)
|
||||
|
||||
> **TESTED BY 38 AND IT STANDS.** Session 10 first argued that the flat
|
||||
> subtraction here is too pessimistic -- that the disk DMA could hide in bus
|
||||
> cycles the CPU was not using -- and scored the same window at 53/120 instead
|
||||
> of 84/120. **That was wrong.** A 68000 has no cache and a two-word prefetch
|
||||
> queue, so it stalls as soon as another master takes the bus; DMA time is
|
||||
> additive, which is exactly what this section assumed. The 84/120 stands and
|
||||
> 38.3 now reproduces it.
|
||||
|
||||
**Raised by the user: "PIO is such a CPU killer. DMA is not. I'm concerned about
|
||||
us drawing the wrong conclusions."** The concern is correct, and it is larger
|
||||
than the labelling question of 32.4. This is the seventh false premise this
|
||||
@@ -2129,3 +2142,273 @@ parsing lengths out of unmapped memory and walking wherever they pointed. Any
|
||||
"the decoder is 4x slower than the model on RAW-heavy streams" conclusion drawn
|
||||
from that run would have been entirely false, which is the third time in this
|
||||
session that an unobservable run nearly produced a wrong finding.
|
||||
|
||||
## 37. A second emulator, and MAME is not running the core we thought (session 10)
|
||||
|
||||
Every 68000 cycle figure in FINDINGS 24-35 came from one instrument. This is a
|
||||
second one, run against byte-for-byte the same `decode.bin` and the same
|
||||
container.
|
||||
|
||||
`tools/bench/c68k/` links **px68k's C68K core** into a headless harness: a
|
||||
hand-built X68000 memory map, no SDL, no ROMs, no emulated machine. The decoder
|
||||
touches nothing but RAM, the control block and GVRAM, so the machine around it
|
||||
was never part of the measurement.
|
||||
|
||||
### 37.1 What the two instruments actually are
|
||||
**MAME 0.277's `M68000` is not Musashi.** `src/devices/cpu/m68000/m68000.lst`
|
||||
plus `m68000gen.py`: it is the microcode core, where timing emerges from the
|
||||
modelled micro-sequence and 4-clock bus cycles. C68K is a static per-instruction
|
||||
cycle table (`ORI_CLOCKS_*` / `EA_CLOCKS_*` in `c68kmacro.h`), hand-transcribed
|
||||
from the Motorola manual by a different author.
|
||||
|
||||
Those are two different ways of arriving at a number, which is what makes the
|
||||
agreement worth something. It would be worth much less if both were tables.
|
||||
|
||||
### 37.2 The harness is self-validating
|
||||
It decodes all 80 frames and dumps the screen; `verify_c68k.py` checks it
|
||||
against `tools/encoder/dlx.py` **pixel for pixel, on palette indices**. That is
|
||||
the licence for the cycle numbers: the harness rebuilds px68k's memory model
|
||||
from scratch -- byte-swapped RAM (`mem_wrap.c:420`), GVRAM word writes that
|
||||
discard the high byte -- and any of it being subtly wrong would still print
|
||||
plausible cycles. It could not print a pixel-exact 80-frame temporal recursion.
|
||||
|
||||
It does. **`decode.s` is now pixel-exact under two independent CPU cores.**
|
||||
|
||||
### 37.3 The numbers
|
||||
```
|
||||
anchor MAME C68K delta MAME C68K of a 12fps frame
|
||||
min non-SKIP 42.8% 600982 620760 +3.29% 72.1% 74.5%
|
||||
median 65.2% 841038 869036 +3.33% 100.9% 104.3%
|
||||
p90 72.9% 836124 856872 +2.48% 100.3% 102.8%
|
||||
max non-SKIP 100.0% 921187 923090 +0.21% 110.5% 110.8%
|
||||
synthetic all-SKIP 40729 40946 +0.53% 4.9% 4.9%
|
||||
synthetic all-V1 921187 923090 +0.21% 110.5% 110.8%
|
||||
synthetic all-V4 1376881 1420754 +3.19% 165.2% 170.5%
|
||||
synthetic all-RAW -- 1273298 -- 152.8%
|
||||
```
|
||||
|
||||
The `all-RAW` cell is empty because MAME's timed pass did not reach it. That is
|
||||
an operational fact worth recording: with `-video soft -nothrottle` this box runs
|
||||
`x68000` at about **0.033x realtime**, so `decode.lua`'s eight anchors plus two
|
||||
full passes — ~48 emulated seconds — cost ~25 minutes of wall clock, and two runs
|
||||
were killed by their own `timeout`. The C68K harness does the same work in
|
||||
seconds because it emulates a CPU and not a machine. **Anchor MAME runs by wall
|
||||
clock, not by `-seconds_to_run`.**
|
||||
|
||||
**Cycle-table error is bounded at 3.3%, and it runs against us** -- C68K reads
|
||||
high on every anchor. Nothing here rescues FINDINGS 35. The disagreement is
|
||||
mode-dependent (all-V1 +0.21%, all-V4 +3.19%), so it localises to the V4 path's
|
||||
indexed two-register `movem.l`, not to a systematic clock difference.
|
||||
|
||||
**FINDINGS 28.8 is confirmed independently**: under C68K, V4 (170.5%) still
|
||||
costs more than RAW (152.8%). That conclusion inverts the encoder's mode
|
||||
preference, so having it from a second core matters more than most.
|
||||
|
||||
### 37.4 What it does not settle
|
||||
px68k has no bus-timing model anywhere in `x68k/*.c` -- grep it. Neither
|
||||
instrument charges GVRAM wait states, so this is **the same lower bound,
|
||||
measured twice**. It bounds cycle-table error. It says nothing about the
|
||||
distance to a real X68000; that is still BENCHMARK.md Tier 3.
|
||||
|
||||
### 37.5 One trap, recorded because it will catch the next person
|
||||
C68K is 64-bit-unsafe by construction: its `MOVEM` macros do
|
||||
`src = (UINT32)(&D0)` -- they truncate the host address of the register file and
|
||||
dereference it -- and `C68k_Set_Fetch` keeps the opcode-fetch base in a `UINT32`.
|
||||
Under the default PIE the binary loads near `0x555555550000` and the first
|
||||
`movem` segfaults. The Makefile builds `-no-pie` and the harness mmaps its arena
|
||||
`MAP_32BIT`. Both are load-bearing, not tidiness.
|
||||
|
||||
## 38. The bus, measured: the project is bus-bound, not CPU-bound (session 10)
|
||||
|
||||
> **This supersedes part of 29 and part of 35.** FINDINGS 29's "the bus has 4x
|
||||
> the headroom the CPU has" is true of the SCSI pipe and false of the 68000's
|
||||
> local bus, and they are different resources. FINDINGS 35's flat CPU debit for
|
||||
> the disk charges the CPU for bus cycles it was not going to use.
|
||||
|
||||
Everything since FINDINGS 24 has been costed in CPU clocks. The 68000 has
|
||||
another budget nobody had counted: its **memory bus**, one 4-clock cycle at a
|
||||
time, carrying instruction prefetch as well as data.
|
||||
|
||||
### 38.1 Two sources that check each other
|
||||
`tools/bench/c68k/c68k_bench` counts every Read/Write callback the C68K core
|
||||
makes -- exact, because C68K splits a long access into two word calls, which is
|
||||
what the 16-bit bus does. It cannot count **instruction prefetch**: C68K reads
|
||||
opcodes straight through a host pointer with no callback, and MAME exposes no
|
||||
fetch count either.
|
||||
|
||||
So `tools/analysis/15_bus_occupancy.py` derives prefetch by walking
|
||||
`decode.s`'s straight-line paths in `tools/bench/decode.lst` and multiplying by
|
||||
each frame's mode histogram. The same walk also predicts the data half -- and
|
||||
that half is measurable:
|
||||
|
||||
```
|
||||
measured mean 66,700 data bus cycles/frame
|
||||
derived mean 66,672 error -0.04% mean, 0.06% worst
|
||||
```
|
||||
|
||||
The walk reproduces the measurement, so its prefetch figure stands on the same
|
||||
footing. `15_bus_occupancy.py` exits non-zero if that check ever stops holding.
|
||||
|
||||
### 38.2 The result
|
||||
```
|
||||
mean median worst frame
|
||||
bus slots in a frame 201,497 211,013 230,772
|
||||
data accesses 66,672 68,044 105,216
|
||||
instruction prefetch 108,002 110,982 122,910
|
||||
total bus cycles 174,674 181,998 193,248
|
||||
bus OCCUPANCY 86.7% 86.8% 88.3%
|
||||
slots left for a DMAC 26,823 26,618 21,115
|
||||
```
|
||||
|
||||
**The decoder occupies 86.7% of its own bus, and prefetch is 62% of that.** A
|
||||
data-only count understates occupancy by about 2x, which is exactly the mistake
|
||||
an instrumented emulator would lead you into.
|
||||
|
||||
Per mode, bus clocks against measured clocks: V1 204/299.9 (68%), V4 308/448.2
|
||||
(69%), RAW 316/400.4 (79%), and the v6 span **9.0/9.152 (98%)**.
|
||||
|
||||
### 38.3 What that does to the frame budget -- and one wrong turn
|
||||
The first thing done with 86.7% was to argue that FINDINGS 35's flat CPU debit
|
||||
for the disk is too pessimistic: the decoder leaves ~26,800 bus slots a frame
|
||||
idle against the disk's ~23,000, so score it as contention,
|
||||
`frame = max(CPU clocks, 4 x bus cycles)`, and the window misses 53/120 rather
|
||||
than 84/120.
|
||||
|
||||
**That is wrong, and the MC68450 manual is what says so.** A 68000 relinquishes
|
||||
the bus on BGACK and cannot execute without it -- no cache, a two-word prefetch
|
||||
queue that empties immediately. Worse, the DMAC does not interleave at operand
|
||||
granularity by default: limited-rate auto-request hands it the bus in *bursts*
|
||||
of `2(BT+4)` clocks out of a sample period of `2(BT+BR+5)`, taking
|
||||
`2^-(BR+1)` of the bandwidth in slabs (MC68450 sect 5.2.3.2, Fig 5-2). During a
|
||||
slab the CPU is stopped.
|
||||
|
||||
So **DMA time is additive to CPU time**, which is what FINDINGS 35 assumed all
|
||||
along. `14_dmac_chain.py` reproduces its 84/120 exactly in the `today` column.
|
||||
|
||||
What 86.7% *does* say is worse than the thing it appeared to rescue: **there is
|
||||
almost no room to overlap anything.** The 13.3% of bus slots the decoder leaves
|
||||
idle are single gaps inside a `movem`-heavy loop, not windows a bus master can
|
||||
be handed. Any design whose case rests on DMA hiding under CPU work on this
|
||||
machine should be assumed dead until measured on hardware.
|
||||
|
||||
The measurement still earns its place: it is what prices the span painter
|
||||
against a DMAC in 39, and it is the reason the answer there came out the way it
|
||||
did.
|
||||
|
||||
### 38.4 What is not counted
|
||||
Bus arbitration. The 68000's BR/BG/BGACK handover costs cycles a cycle-steal
|
||||
DMA cannot avoid, and the disk debit here embeds it only insofar as FINDINGS 5's
|
||||
8 clocks/word already does. Also: no GVRAM wait states, as everywhere since 24.
|
||||
Both make the real occupancy **higher** than 86.7%, not lower.
|
||||
|
||||
## 39. The DMAC chain against the span: the datasheet says no (session 10)
|
||||
|
||||
FINDINGS 29.6 named "let the DMAC do the copy" the one lever that could move the
|
||||
budget without spending a byte, and left it uncosted. This costs it, and the
|
||||
answer is **no** -- but only after the constants came from the MC68450 manual
|
||||
rather than from bus arithmetic, which is the whole lesson of the section.
|
||||
|
||||
### 39.1 They are the same container
|
||||
v6's record is `{u32 absolute GVRAM address, u16 jump displacement}` = 6 bytes.
|
||||
An MC68450/HD63450 **array-chaining entry** is `{u32 memory address, u16
|
||||
transfer count}` = 6 bytes. Set the channel dual-address, direction
|
||||
device->memory, Sequence Control counting both addresses up: MAR reloads per
|
||||
entry (the GVRAM destination), DAR walks the stream buffer, MTC is the span's
|
||||
word count. **The chain array IS the span table.** Every byte figure in
|
||||
FINDINGS 30 carries over, and this is not a fork in the format -- the encoder
|
||||
emits the same thing either way, only the executor changes. That much is real
|
||||
and survives everything below.
|
||||
|
||||
### 39.2 The first answer was wrong by a clock
|
||||
Session 10 first derived the DMAC's cost from bus arithmetic: moving a pixel is
|
||||
a read cycle plus a write cycle, 2 bus cycles, 8 clocks, against v6's measured
|
||||
9.152 -- a 12.6% edge. On that basis the design scored 1/120 frames over budget
|
||||
against v6's 10/120 and looked decisive.
|
||||
|
||||
The datasheet does not agree. **MC68450 Fig 4-25 sheet 4**, dual address /
|
||||
operand size WORD / device size 16 bits, D->M or M->D:
|
||||
`{WORD READ, WORD WRITE}` = **9 CLOCKS**. Confirmed by the long-operand row, two
|
||||
of each for 18. And **Fig 4-25 note 2** says why: the DMAC's reads take four
|
||||
clocks and its **writes take five**. The 68000 writes in four.
|
||||
|
||||
| per pixel | clocks | source |
|
||||
|---|---:|---|
|
||||
| DMAC, dual-address word, two 16-bit ports | **9.000** | MC68450 Fig 4-25 sheet 4 |
|
||||
| v6 `movem` chain | **9.152** | MEASURED, FINDINGS 30 |
|
||||
|
||||
**1.7%.** One clock on every DMAC write is the entire difference between a
|
||||
12.6% win and a rounding error. Per span, sequential array chaining costs
|
||||
**36 clocks** (Fig 4-25 sheet 1: three word reads for the 6-byte entry, plus
|
||||
reload) against v6's measured 43.7 -- the DMAC's one genuine edge, and it is
|
||||
7.7 clocks.
|
||||
|
||||
### 39.3 Scored additively, as 38.3 requires
|
||||
```
|
||||
today v6 span v6 fine tail DMAC chain
|
||||
bitrate KB/s 270.8 479.2 479.9 479.9
|
||||
frame, median 108.1% 99.3% 96.5% 95.0%
|
||||
frame, worst 114.7% 112.0% 111.4% 110.3%
|
||||
frames missing 84/120 55/120 18/120 12/120
|
||||
blocks spanned/frame 0 727 839 845
|
||||
```
|
||||
`today` reproduces FINDINGS 35's 84/120 exactly, which is the check that the
|
||||
scenario lines up.
|
||||
|
||||
### 39.4 What the DMAC actually buys, and who else can sell it
|
||||
`v6 fine tail` is the decomposition. v6 pads every span up to 24 pixels because
|
||||
its copy is an unrolled chain of 12-register `movem` units; adding a second,
|
||||
finer chain of 2-register units caps the padding at 3 pixels instead of 23, for
|
||||
the price of some more unrolled code and **nothing per span**. Priced
|
||||
conservatively (a 4-pixel unit costs 56 clocks against a full unit's 220 for 24,
|
||||
so it is dearer per pixel and paid at most once a span):
|
||||
|
||||
| | frames over |
|
||||
|---|---:|
|
||||
| v6 as built | 55/120 |
|
||||
| **v6 with a finer chain tail -- software only** | **18/120** |
|
||||
| DMAC chain | 12/120 |
|
||||
|
||||
**86% of the DMAC's advantage over v6 is the 24-pixel padding quantum**, and
|
||||
that is a property of v6's unrolled chain, not of the CPU. The residual is 1.7%
|
||||
a pixel and 7.7 clocks a span, worth 6 frames of 120.
|
||||
|
||||
Break-even against all-V1 moves the same way: v6 as built needs a run of 4
|
||||
blocks, v6 with the finer tail needs 3, the DMAC needs 1.
|
||||
|
||||
### 39.5 The verdict
|
||||
**Fix the quantum in software.** Six frames of 120 does not buy a reserved DMAC
|
||||
channel, a two-region container layout, and a dependency on transfer timing that
|
||||
cannot be verified in either emulator on this box. The `v6 fine tail` figure is
|
||||
itself DERIVED and should be measured with `span.sh` before it is believed --
|
||||
that is a day's work in a tool that already exists, against a hardware
|
||||
dependency that needs an actual X68000.
|
||||
|
||||
Keep 39.1 on the record. If a later measurement moves the DMAC's per-pixel cost
|
||||
below 8 clocks -- for instance if GVRAM tolerates a four-clock DMAC write in a
|
||||
way the datasheet's typical-system assumption does not model -- the container
|
||||
does not have to change to take advantage of it.
|
||||
|
||||
### 39.6 What else would have to be true, if it is ever revisited
|
||||
- **A free channel.** Four exist; channel 3 is ADPCM (`adpcm_drq_tick` asserts
|
||||
`drq3_w`) and the SCSI stream needs one.
|
||||
- **Two regions per frame.** Chaining fetches entries from an array while DAR
|
||||
walks the pixel data, so the span table and the literal words cannot be
|
||||
interleaved as v6 interleaves them.
|
||||
- **The mode-map walk stays on the CPU.** 39.3 charges it; FINDINGS 30.7 flagged
|
||||
that 12_span_tradeoff.py did not.
|
||||
|
||||
### 39.7 A number the datasheet settled on the way past
|
||||
FINDINGS 5's **8 clocks/word** for the SCSI DMA has been an unsourced estimate
|
||||
since session 1 and STATUS has called it the most load-bearing unmeasured number
|
||||
in the project. Fig 4-25 sheet 3 gives single-address `W/B READ` 4 clocks and
|
||||
`W/B WRITE` 5; a device->memory disk transfer is one memory write. So it is
|
||||
**5 clocks/word if the DMAC holds the bus** and about **12 if it arbitrates per
|
||||
word** (front-end 5 best case / 8 worst, sect 4.5.2.1; back-end 2, sect 4.5.2.2).
|
||||
The feature list's "up to 5 Megabytes per Second at 10 MHz, no wait states"
|
||||
is the held-bus case: 2 bytes per 4-clock cycle.
|
||||
|
||||
**8 is the midpoint of a bracket the datasheet supports, not a guess.** Which
|
||||
end applies depends on how the MB89352 drives REQ and whether cycle-steal-with-
|
||||
hold is used, which is a design decision the player has not made yet -- and it
|
||||
is worth 7 clocks a word on a 480 KB/s stream, so it is worth making
|
||||
deliberately.
|
||||
|
||||
+91
-46
@@ -1,47 +1,84 @@
|
||||
# Status & next-session handoff — end of session 9 (2026-08-23)
|
||||
# Status & next-session handoff — end of session 10 (2026-08-23)
|
||||
|
||||
## Where this stands
|
||||
|
||||
Session 9 did three things: dropped a profile on the user's instruction, closed
|
||||
the last encoder gap, validated the cost model against the machine — and then
|
||||
found that **the CPU budget every one of those numbers was scored against has
|
||||
never had the disk in it**.
|
||||
Session 10 cross-checked the whole cycle model against a second emulator, then
|
||||
found that the model was denominated in the wrong currency.
|
||||
|
||||
**FINDINGS 35 is the headline, and it is bad news.** Raised by the user ("PIO is
|
||||
such a CPU killer. DMA is not. I'm concerned about us drawing the wrong
|
||||
conclusions"). Every CPU figure in FINDINGS 24-34 is measured against the full
|
||||
833,333 cycles/frame, with nothing subtracted for moving the bitstream off SCSI.
|
||||
Debiting the HD63450 cycle-steal at the long-standing 8 clocks/word estimate:
|
||||
**FINDINGS 38 is the headline: the project is BUS-bound, not CPU-bound.** 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. Measured, the decoder
|
||||
occupies **86.7%** of it, and **prefetch is 62% of that traffic**. Scoring the
|
||||
`scsi` window with the bus as the shared resource, **52 of 53 missed frames are
|
||||
bus-limited and one is CPU-limited**. Every optimisation since 24 has been aimed
|
||||
at the budget that is not binding.
|
||||
|
||||
| `scsi` container | budget left | median frame | worst | frames missing |
|
||||
|---|---:|---:|---:|---:|
|
||||
| no I/O — the pre-session-9 premise | 833,333 | 99.6% | 110.6% | **1/120** |
|
||||
| **DMA** (8 clk/word, ESTIMATED) | 738,234 | **112.4%** | 124.8% | **84/120** |
|
||||
| PIO (12 clk/B, hand-derived floor) | 548,036 | 151.4% | 168.1% | **120/120** |
|
||||
The measurement is two sources checking each other: `c68k_bench` counts every bus
|
||||
callback exactly, and a static walk of `decode.lst` adds the prefetch no emulator
|
||||
here can report. The walk reproduces the measured data half to **0.04%**, which
|
||||
is what licenses its prefetch half.
|
||||
|
||||
So "1 frame of 120 misses" was against the wrong budget. `11_cpu_budget.py` now
|
||||
takes `--io dma|pio|none`, defaults to `dma`, and warns if asked for `none`.
|
||||
**The DMAC array-chain LOSES, on the datasheet.** My first pass derived its cost
|
||||
from bus arithmetic — a read cycle plus a write cycle, 8 clocks a pixel — and
|
||||
scored it at 1/120 frames over budget against v6's 10/120. Then I read the
|
||||
MC68450 manual (Motorola Jul 1989, bitsavers). **Fig 4-25 sheet 4: a
|
||||
dual-address word between two 16-bit ports is 9 clocks, because note 2 gives the
|
||||
DMAC 4-clock reads and 5-clock WRITES.** The 68000 writes in 4.
|
||||
|
||||
**The way out is 10 fps, and it works on paper**: same container, DMA debited,
|
||||
10 fps -> median 93.7%, worst 104.0%, **1/120** — and that is conservative,
|
||||
because it holds the 12 fps byte rate. FINDINGS 35.5.
|
||||
| per pixel | clocks | source |
|
||||
|---|---:|---|
|
||||
| DMAC dual-address word | **9.000** | MC68450 Fig 4-25 sheet 4 |
|
||||
| v6 `movem` chain | **9.152** | MEASURED, FINDINGS 30 |
|
||||
|
||||
**SASI is dropped (USER DECISION).** A SASI volume is 40 MB and the game's 22.8
|
||||
minutes of unique footage is 146 MiB at the *lowest* rate this codec makes.
|
||||
`scsi` is the only profile. Delivery is SD-backed SCSI, as locked in session 2 —
|
||||
capacity does not bind there, and it is what rules CD-ROM out. FINDINGS 32.
|
||||
Scored additively (see below), against the same mode maps:
|
||||
|
||||
**The encoder gap is closed.** `encode.py` emits **DLX2**, 4-byte-aligned frame
|
||||
records: 94/120 record starts were on odd addresses, now 0/120, for 16 B/s.
|
||||
Re-encoding reproduces FINDINGS 31.1 exactly. FINDINGS 33.
|
||||
| | frames over budget |
|
||||
|---|---:|
|
||||
| today | 84/120 |
|
||||
| v6 span as built | 55/120 |
|
||||
| **v6 with a finer chain tail — software only** | **18/120** |
|
||||
| DMAC chain | 12/120 |
|
||||
|
||||
**The cost model is validated on a cost-aware container** (STATUS item 1, done):
|
||||
errors -3.07% to +0.01% against the 68000, whole-window mean -1.22%. It
|
||||
under-predicts light frames and is exact on heavy ones. FINDINGS 34.
|
||||
**86% of the DMAC's advantage is v6's 24-pixel padding quantum**, which is a
|
||||
property of v6's unrolled `movem` chain and fixable in software: add a second
|
||||
chain of 2-register units for the tail and the padding drops from 23 pixels to
|
||||
3, at no per-span cost. **Recommendation (mine): fix the quantum, drop the
|
||||
DMAC.** Six frames of 120 does not buy a reserved channel, a two-region
|
||||
container layout, and a timing dependency neither emulator here can verify.
|
||||
FINDINGS 39.
|
||||
|
||||
**A `scsi` window does not fit the 2 MB machine the rig emulates** — 2.84 MB of
|
||||
stream against a 0x200000 ceiling. The decoder gate now verifies 80 of 120
|
||||
frames and says so, rather than failing as a phantom pixel diff. FINDINGS 36.
|
||||
The container work is not wasted either way: v6's record and an HD63450 chaining
|
||||
entry are both 6 bytes, so **the chain array is the span table** and nothing has
|
||||
to change if a hardware measurement later moves the DMAC's number. FINDINGS 39.1.
|
||||
|
||||
**I also got FINDINGS 35 wrong before the datasheet corrected me.** I argued its
|
||||
flat CPU debit was too pessimistic and rescored the window at 53/120 using
|
||||
`max(CPU, bus)`. 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.** What 86.7% bus occupancy really
|
||||
says is that there is almost no room to overlap anything on this machine.
|
||||
FINDINGS 38.3.
|
||||
|
||||
**And the datasheet settled STATUS's own "most load-bearing unmeasured number".**
|
||||
FINDINGS 5's 8 clocks/word for the SCSI DMA: Fig 4-25 sheet 3 gives
|
||||
single-address write 5 clocks, so it is **5 clk/word with the bus held and ~12
|
||||
if the DMAC arbitrates per word**. 8 is the midpoint of a bracket the datasheet
|
||||
supports. Which end applies is a player design decision — how the MB89352 drives
|
||||
REQ, and whether cycle-steal-with-hold is used — worth 7 clocks a word on a
|
||||
480 KB/s stream. FINDINGS 39.7.
|
||||
|
||||
**The decoder is now pixel-exact under two independent CPU cores.**
|
||||
`tools/bench/c68k/` links px68k's C68K core into a headless harness and decodes
|
||||
the same container to the same pixels. Cycle-table error against MAME is bounded
|
||||
at **3.3%**, and it runs against us. Two incidental results worth keeping: MAME
|
||||
0.277's `M68000` is the **microcode core**, not Musashi (`m68000.lst` +
|
||||
`m68000gen.py`), so this is two structurally different timing models agreeing
|
||||
rather than two tables; and FINDINGS 28.8's "V4 costs more than RAW" reproduces
|
||||
independently. FINDINGS 37.
|
||||
|
||||
Everything below this line is from session 9 and still stands unless a session-10
|
||||
section above says otherwise.
|
||||
|
||||
## NEXT SESSION, in order
|
||||
|
||||
@@ -58,13 +95,13 @@ frames and says so, rather than failing as a phantom pixel diff. FINDINGS 36.
|
||||
in the tree can only ever audit a prefix of a window.** Getting the coverage
|
||||
back means gating on more than one window, not one longer pass.
|
||||
|
||||
1. **Measure the DMA cycle-steal, or derive it from primary sources.** This is
|
||||
now the most load-bearing unmeasured number in the project: at 8 clocks/word
|
||||
the port is marginal, at 12 it is dead, at 4 it is comfortable. It has been
|
||||
an estimate from a session-2 datasheet reading since FINDINGS 5. MAME cannot
|
||||
answer it (functional models, not transfer-timing accurate) — this needs the
|
||||
HD63450 and MB89352 datasheets or real hardware. **Everything below is
|
||||
contingent on it.**
|
||||
1. **Measure the finer chain tail with `span.sh`.** It is the largest single win
|
||||
on the table — 55/120 to 18/120 — it is software only, and the tool that
|
||||
measures it already exists: `blit.s` gains a v7 with a 2-register tail chain,
|
||||
`prep_spans.py` generates the streams, `span.sh` times all of it in ~25 s and
|
||||
asserts every config drew a pixel-exact frame. The 18/120 figure is DERIVED
|
||||
from a conservative 56-clocks-per-4-pixel-unit model and should not be
|
||||
believed until it is measured. FINDINGS 39.4.
|
||||
|
||||
2. **Make sure the player actually gets DMA.** DMA-vs-PIO is a property of our
|
||||
code, not the board: the CZ-6BS1's DMA path is real and modelled
|
||||
@@ -79,12 +116,11 @@ frames and says so, rather than failing as a phantom pixel diff. FINDINGS 36.
|
||||
10 fps absorbs the DMA steal on current estimates. This was item 5 and a
|
||||
quality question; it is now arithmetic. It is still the user's call.
|
||||
|
||||
4. **Then spans.** Fully specified by measurement (format FINDINGS 30.2, costs
|
||||
30.5, scene-cut arithmetic 30.6). Note what 35.2 does to the case for them:
|
||||
spans buy cycles by spending bandwidth, and the bandwidth comes back as
|
||||
steal, so 31.6's "fits completely" becomes a worst frame of 114.3%. They are
|
||||
still worth 14 points at the median — they are just no longer sufficient
|
||||
alone.
|
||||
4. **Then spans, on the CPU.** The format is fully specified by measurement
|
||||
(FINDINGS 30.2, costs 30.5, scene-cut arithmetic 30.6) and `decode.s` does
|
||||
not implement them yet. Build the v7 tail from item 1 into it. The DMAC
|
||||
variant is costed and loses (39.5); the container is identical either way
|
||||
(39.1), so nothing is foreclosed.
|
||||
|
||||
5. **Re-run the ring-buffer simulation at the surviving rate** and confirm the
|
||||
488 KB/s figure's provenance (FINDINGS 29.5/30.7, still open).
|
||||
@@ -742,6 +778,15 @@ with `extract.py`; the earlier ones lived in `/tmp` and do not survive a reboot.
|
||||
|
||||
## Reference material on this box (not in the repo)
|
||||
|
||||
- **MC68450 DMAC manual: `~/src/mc68450.pdf`** (Motorola, Jul 1989, from
|
||||
bitsavers; `curl` it with a browser User-Agent or you get a 403). This is the
|
||||
primary source for FINDINGS 39 and it overturned two derived answers in one
|
||||
session. Fig 4-25 is the timing table — sheet 1 chaining, sheet 3
|
||||
single-address, sheet 4 dual-address; note 2 (4-clock reads, 5-clock writes)
|
||||
is the one that mattered. Sects 4.5.2.1-3 are the arbitration overheads,
|
||||
5.2.3.2 the limited-rate auto-request slabs. `pdftotext` handles it.
|
||||
- **px68k source: `~/src/px68k`** — only `m68000/c68k.c` is used, by
|
||||
`tools/bench/c68k/`.
|
||||
- **MAME 0.277 source: `~/src/mame-mame0277/`** (tarball `~/src/mame0277.tar.gz`).
|
||||
Downloaded this session to settle the graphics-layer question. The files that
|
||||
matter are `src/mame/sharp/x68k_v.cpp`, `x68k_crtc.cpp`, `x68k_crtc.h`,
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Would letting the HD63450 paint the spans beat letting the 68000 do it?
|
||||
|
||||
python3 tools/analysis/14_dmac_chain.py [container.dlx] [--bus 488]
|
||||
[--dma-px-bus 2] [--disk-bus-byte 1]
|
||||
|
||||
FINDINGS 29.6 called this the one lever that could move the CPU budget without
|
||||
spending a byte, and left it uncosted. FINDINGS 30 measured the alternative --
|
||||
the 68000 painting spans itself, 43.7 cycles per span + 9.152 per pixel. This
|
||||
prices the two against each other, and the answer turns on a resource neither
|
||||
section costed: the 68000's own LOCAL BUS.
|
||||
|
||||
FINDINGS 29's "the bus has 4x the headroom the CPU has" is about the SCSI pipe,
|
||||
110 KB/s of 488. That is a different bus. The 68000's memory bus runs one 4-clock
|
||||
cycle at a time and carries instruction prefetch as well as data, and
|
||||
tools/analysis/15_bus_occupancy.py measures the decoder using 86.7% of it.
|
||||
|
||||
THE TWO DESIGNS ARE THE SAME CONTAINER. v6's record is {u32 absolute GVRAM
|
||||
address, u16 jump displacement} = 6 bytes; an MC68450/HD63450 array-chaining
|
||||
entry is {u32 memory address, u16 transfer count} = 6 bytes. Set the channel to
|
||||
dual-address, direction device->memory, Sequence Control counting both addresses
|
||||
up: MAR reloads per entry (the GVRAM destination), DAR walks the stream buffer,
|
||||
MTC is the span's word count. The chain array IS the span table.
|
||||
|
||||
THE DMAC CONSTANTS ARE NOW SOURCED, and they killed the first answer. From the
|
||||
MC68450 manual (Motorola, Jul 1989, bitsavers), Fig 4-25 sheet 4: a dual-address
|
||||
WORD operand between two 16-bit ports is **9 clocks**, because note 2 gives the
|
||||
DMAC 4-clock reads and **5-clock writes**. The 68000 writes in 4. So:
|
||||
|
||||
DMAC 9.000 clocks/pixel (datasheet)
|
||||
v6 9.152 clocks/pixel (measured, FINDINGS 30)
|
||||
|
||||
A 1.7% difference. Session 10's first pass guessed 2 bus cycles = 8 clocks from
|
||||
bus arithmetic and was 12% optimistic; the extra clock on every DMAC write is
|
||||
the whole story. Per span, sequential array chaining costs 36 clocks (Fig 4-25
|
||||
sheet 1) against v6's measured 43.7 -- the DMAC's one real edge, and it is small.
|
||||
|
||||
AND DMA DOES NOT OVERLAP. The 68000 has no cache and a two-word prefetch queue,
|
||||
so it stalls as soon as another master takes the bus. Frame time is therefore
|
||||
CPU + DMA, additive. Session 10's first pass used max(CPU, bus) and got 53/120
|
||||
where the additive model gives 84/120; FINDINGS 35's flat debit was right.
|
||||
|
||||
So the only material difference left is v6's 24-pixel padding quantum -- and
|
||||
that is a property of v6's unrolled chain, not of the CPU. The `v6 fine tail`
|
||||
column prices fixing it in software instead.
|
||||
"""
|
||||
import sys, os, argparse
|
||||
sys.path.insert(0, "tools/encoder")
|
||||
sys.path.insert(0, "tools/analysis")
|
||||
import numpy as np
|
||||
from dlx import DLX
|
||||
import buscost as B
|
||||
|
||||
FRAME_CYC = 833333.0
|
||||
AUDIO_KBPS = 7.8
|
||||
C_V1, C_V4, C_RAW = 299.9, 448.2, 400.4 # FINDINGS 28.2 (MEASURED)
|
||||
C_SKIP_CLUSTERED, C_SKIP_MIXED = 13.25, 45.0
|
||||
SPAN_BYTES_PX, SPAN_HDR = 2, 6
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("container", nargs="?", default="tmp/rc_fr_singe_scsi_cpufit.dlx")
|
||||
ap.add_argument("--bus", type=float, default=488.0, help="SCSI pipe, KB/s")
|
||||
ap.add_argument("--fps", type=float, default=12.0)
|
||||
ap.add_argument("--dma-px-clk", type=float, default=B.DMA_PX_CLK,
|
||||
help="clocks the DMAC spends per pixel, dual-address word "
|
||||
"between two 16-bit ports. 9 is the DATASHEET figure "
|
||||
"(MC68450 Fig 4-25 sheet 4).")
|
||||
ap.add_argument("--disk-clk-word", type=float, default=8.0,
|
||||
help="clocks the SCSI DMA steals per word. The datasheet "
|
||||
"brackets it at 5 (DMAC holds the bus) to 12 (arbitrates "
|
||||
"per word); FINDINGS 5's estimate of 8 is the midpoint.")
|
||||
a = ap.parse_args()
|
||||
if not os.path.exists(a.container):
|
||||
sys.exit(f"missing {a.container}")
|
||||
|
||||
BYTE_BUD = (a.bus - AUDIO_KBPS) * 1024 / a.fps
|
||||
BUS_SLOTS = FRAME_CYC / B.BUS_CLK
|
||||
d = DLX(a.container)
|
||||
BLK_C = {1: C_V1, 2: C_V4, 3: C_RAW}
|
||||
BLK_B = {1: 1, 2: 4, 3: 16}
|
||||
|
||||
|
||||
def runs(m, by):
|
||||
dirty = m[by] != 0
|
||||
i = 0
|
||||
while i < d.nbx:
|
||||
if not dirty[i]:
|
||||
i += 1; continue
|
||||
j = i
|
||||
while j < d.nbx and dirty[j]:
|
||||
j += 1
|
||||
yield i, j
|
||||
i = j
|
||||
|
||||
|
||||
def span_cost(design, L):
|
||||
"""(pixels carried, clocks charged to the frame) for a run of L blocks,
|
||||
as 4 rows of 4L pixels. Every design is charged additively: the 68000
|
||||
cannot execute while the DMAC owns the bus."""
|
||||
if design == "v6":
|
||||
px = B.pad24(4 * L)
|
||||
return 4 * px, 4 * (B.V6_SPAN_CYC + px * B.V6_PX_CYC)
|
||||
if design == "v6fine":
|
||||
px, c = B.v6_fine(4 * L)
|
||||
return 4 * px, 4 * c
|
||||
px = 4 * L
|
||||
return 4 * px, 4 * (B.DMA_CHAIN_CLK + px * a.dma_px_clk)
|
||||
|
||||
|
||||
def score(design):
|
||||
"""Greedy, as 12_span_tradeoff.py: buy the best clocks-saved per byte spent
|
||||
until the frame's byte budget is gone. Unlike 12, a spanned block still pays
|
||||
its mode-map dispatch, which FINDINGS 30.7 flagged as uncounted."""
|
||||
out = []
|
||||
for f in range(d.nframes):
|
||||
m = d.modes(f).reshape(d.nby, d.nbx)
|
||||
byt = d.mode_bytes + sum(BLK_B.get(int(x), 0) for x in m.ravel())
|
||||
spanned = np.zeros_like(m, bool)
|
||||
span_clk = 0.0
|
||||
|
||||
cand = []
|
||||
if design != "none":
|
||||
for by in range(d.nby):
|
||||
for i, j in runs(m, by):
|
||||
L = j - i
|
||||
cur_c = sum(BLK_C[int(b)] for b in m[by][i:j])
|
||||
cur_b = sum(BLK_B[int(b)] for b in m[by][i:j])
|
||||
px, sc = span_cost(design, L)
|
||||
sc += L * C_SKIP_MIXED # the dispatch still happens
|
||||
span_b = 4 * SPAN_HDR + px * SPAN_BYTES_PX
|
||||
if sc < cur_c:
|
||||
cand.append((cur_c - sc, span_b - cur_b, by, i, j, sc, L))
|
||||
cand.sort(key=lambda s: -(s[0] / max(s[1], 1)))
|
||||
for dc, db, by, i, j, sc, L in cand:
|
||||
if byt + db <= BYTE_BUD:
|
||||
byt += db
|
||||
spanned[by][i:j] = True
|
||||
span_clk += sc - L * C_SKIP_MIXED
|
||||
g = m.copy()
|
||||
g[spanned] = 0
|
||||
gg = g.reshape(-1, 4)
|
||||
allskip = (gg == 0).all(1)
|
||||
cpu = allskip.sum() * 4 * C_SKIP_CLUSTERED
|
||||
mm = gg[~allskip]
|
||||
cpu += (mm == 0).sum() * C_SKIP_MIXED
|
||||
for k, c in BLK_C.items():
|
||||
cpu += (mm == k).sum() * c
|
||||
pref, data = B.block_bus(m, spanned)
|
||||
disk = byt / 2.0 * a.disk_clk_word
|
||||
# additive: CPU work, then span painting, then the disk stealing the bus
|
||||
out.append((cpu + span_clk + disk, (pref + data) * B.BUS_CLK, byt,
|
||||
spanned.sum()))
|
||||
return np.array(out).T
|
||||
|
||||
|
||||
DESIGNS = [("today", "none"), ("v6 span", "v6"),
|
||||
("v6 fine tail", "v6fine"), ("DMAC chain", "dmac")]
|
||||
res = {n: score(k) for n, k in DESIGNS}
|
||||
|
||||
print(f"{a.container}: {d.nframes} frames, {d.nb} blocks, {a.fps:g} fps")
|
||||
print(f"SCSI pipe {a.bus:.0f} KB/s -> {BYTE_BUD:,.0f} B/frame; "
|
||||
f"68000 bus {BUS_SLOTS:,.0f} cycles/frame; CPU {FRAME_CYC:,.0f} clocks\n")
|
||||
|
||||
print("PER PIXEL AND PER SPAN -- datasheet against measurement")
|
||||
print(f" DMAC dual-address word, two 16-bit ports {B.DMA_PX_CLK:.3f} clocks "
|
||||
f"MC68450 Fig 4-25 sheet 4")
|
||||
print(f" v6 movem chain {B.V6_PX_CYC:.3f} clocks "
|
||||
f"MEASURED, FINDINGS 30")
|
||||
print(f" -> the DMAC is {100*(B.V6_PX_CYC-B.DMA_PX_CLK)/B.V6_PX_CYC:+.1f}% per pixel. "
|
||||
f"The 68000 writes in 4 clocks; the DMAC takes 5.")
|
||||
print(f" per span: DMAC array chaining {B.DMA_CHAIN_CLK} clocks against v6's "
|
||||
f"{B.V6_SPAN_CYC:.1f}\n")
|
||||
|
||||
w = 15
|
||||
print(f"{'':<26}" + "".join(f"{n:>{w}}" for n, _ in DESIGNS))
|
||||
def row(label, fmt, get):
|
||||
print(f" {label:<24}" + "".join(f"{fmt(get(res[n])):>{w}}" for n, _ in DESIGNS))
|
||||
|
||||
row("bitrate KB/s", lambda v: f"{v:.1f}", lambda r: r[2].mean() * a.fps / 1024)
|
||||
row("frame, median", lambda v: f"{v:.1f}%", lambda r: 100*np.median(r[0])/FRAME_CYC)
|
||||
row("frame, worst", lambda v: f"{v:.1f}%", lambda r: 100*r[0].max()/FRAME_CYC)
|
||||
row("frames missing", lambda v: f"{v}/{d.nframes}",
|
||||
lambda r: int((r[0] > FRAME_CYC).sum()))
|
||||
row("blocks spanned/frame", lambda v: f"{v:,.0f}", lambda r: r[3].mean())
|
||||
print(f"\n ADDITIVE: frame = CPU + span painting + disk DMA. The 68000 has no"
|
||||
f"\n cache and a two-word prefetch queue, so it stalls the moment another"
|
||||
f"\n master takes the bus. Disk debited at {a.disk_clk_word:g} clocks/word.")
|
||||
|
||||
# What is left of the case, isolated.
|
||||
v6m = int((res["v6 span"][0] > FRAME_CYC).sum())
|
||||
finem = int((res["v6 fine tail"][0] > FRAME_CYC).sum())
|
||||
dmam = int((res["DMAC chain"][0] > FRAME_CYC).sum())
|
||||
print(f"\nWHAT THE DMAC ACTUALLY BUYS, decomposed")
|
||||
print(f" v6 as built {v6m}/{d.nframes} frames over")
|
||||
print(f" v6 with a finer chain tail (software) {finem}/{d.nframes}")
|
||||
print(f" DMAC chain {dmam}/{d.nframes}")
|
||||
print(f" -> of the gap between v6 and the DMAC, "
|
||||
f"{100*(v6m-finem)/max(v6m-dmam,1):.0f}% is the 24-pixel padding")
|
||||
print(f" quantum, which is a property of v6's unrolled chain and fixable")
|
||||
print(f" in software. The rest is 1.7% a pixel and 7.7 clocks a span.")
|
||||
|
||||
# The additive model here IS FINDINGS 35's flat debit, and reproduces its
|
||||
# 84/120 exactly in the "today" column. Session 10's first pass replaced it with
|
||||
# max(CPU, bus) and got 53/120; that was wrong, because a 68000 cannot execute
|
||||
# while the DMAC holds the bus.
|
||||
|
||||
print(f"\nbreak-even against all-V1 ({C_V1:.1f} cycles/block), clocks per block")
|
||||
print(f" {'L':<16}" + "".join(f"{L:>8}" for L in (1, 2, 3, 4, 8, 16, 64)))
|
||||
for nm, dz in (("v6 as built", "v6"), ("v6 fine tail", "v6fine"), ("DMAC chain", "dmac")):
|
||||
print(f" {nm:<16}" + "".join(f"{span_cost(dz, L)[1]/L:>8.0f}"
|
||||
for L in (1, 2, 3, 4, 8, 16, 64)))
|
||||
for nm, dz in (("v6 as built", "v6"), ("v6 fine tail", "v6fine"), ("DMAC chain", "dmac")):
|
||||
brk = next((L for L in range(1, 65) if span_cost(dz, L)[1] < L * C_V1), None)
|
||||
print(f" {nm:<16} beats all-V1 from L={brk} blocks up")
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""How much of the bus does the 68000 decoder actually leave for a DMAC?
|
||||
|
||||
python3 tools/analysis/15_bus_occupancy.py [container.dlx] [--nframes N]
|
||||
|
||||
FINDINGS 29.6's DMAC idea only pays if the DMAC can find bus slots the CPU is
|
||||
not using. That is not a cycle count, it is a BUS count, and nothing in the tree
|
||||
had one.
|
||||
|
||||
Two sources, and the point is that they check each other:
|
||||
|
||||
DATA accesses MEASURED by tools/bench/c68k/c68k_bench, which counts every
|
||||
Read/Write callback the C68K core makes. Exact.
|
||||
INSTRUCTION DERIVED here by walking src/player/decode.s's straight-line
|
||||
prefetch paths in tools/bench/decode.lst and multiplying by the mode
|
||||
histogram. Not measurable from either emulator: MAME's core
|
||||
does not expose a fetch count and C68K reads opcodes straight
|
||||
through a host pointer with no callback.
|
||||
|
||||
If the derived DATA figure matches the measured one, the derived PREFETCH figure
|
||||
from the same walk is trustworthy too. That check is the first thing printed,
|
||||
and this script exits non-zero if it fails.
|
||||
|
||||
A 68000 bus cycle is 4 clocks, so a frame of C clocks holds C/4 bus slots.
|
||||
"""
|
||||
import sys, os, argparse, csv
|
||||
sys.path.insert(0, "tools/encoder")
|
||||
import numpy as np
|
||||
from dlx import DLX
|
||||
|
||||
BUS_CLK = 4
|
||||
|
||||
# --- straight-line path costs, read off tools/bench/decode.lst -------------
|
||||
# (instruction words, data bus cycles). A long access is two bus cycles on the
|
||||
# 68000's 16-bit bus; movem.l of N registers is 2N.
|
||||
#
|
||||
# dispatch move.b (a1),d0 / lsr.b / and.w #3 / beq .sk 6w, 1 read
|
||||
# + subq / beq .v1 -> 8w
|
||||
# + subq / bne .rw -> 10w
|
||||
# V4 body $10090..$100E2 = 82 B = 41w; 4 x (1 byte read
|
||||
# + movem.l 2 regs = 4 reads + 2 move.l = 4 writes) = 36
|
||||
# V1 body $100E2..$10106 = 36 B = 18w; 1 byte read
|
||||
# + movem.l 8 regs = 16 reads + 4 x movem.l 2 = 16 w = 33
|
||||
# RAW body $10106..$10164 = 94 B = 47w; 8 x (2 byte reads
|
||||
# + 1 move.l = 2 writes) = 32
|
||||
# .sk tail addq.l #8,a4 1w
|
||||
# BLOCK 0 has no lsr.b, so one of the four dispatches in a group is 1w cheaper.
|
||||
DISPATCH_SK, DISPATCH_V1, DISPATCH_V4 = 6, 8, 10
|
||||
BODY = {0: (0, 0), 1: (18, 33), 2: (41, 36), 3: (47, 32)}
|
||||
DISPATCH = {0: DISPATCH_SK, 1: DISPATCH_V1, 2: DISPATCH_V4, 3: DISPATCH_V4}
|
||||
SK_TAIL = 1
|
||||
GROUP_HEAD = 3 # tst.b (a1) 1w + beq allskip 2w
|
||||
GROUP_TAIL = 4 # addq.l #1,a1 / cmpa.l a5,a4 / bne byteloop
|
||||
ALLSKIP = 9 # the whole four-block fast path, tst.b included
|
||||
ROW_HEAD, ROW_TAIL = 3, 7
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("container", nargs="?", default="tmp/rc_fr_singe_scsi_cpufit.dlx")
|
||||
ap.add_argument("--csv", default="tmp/c68k_frames.csv",
|
||||
help="per-frame output of tools/bench/c68k/run.sh")
|
||||
ap.add_argument("--nframes", type=int, default=None)
|
||||
a = ap.parse_args()
|
||||
if not os.path.exists(a.container):
|
||||
sys.exit(f"missing {a.container}")
|
||||
|
||||
d = DLX(a.container)
|
||||
meas = {}
|
||||
if os.path.exists(a.csv):
|
||||
for r in csv.DictReader(open(a.csv)):
|
||||
meas[int(r["frame"])] = (int(r["cycles"]),
|
||||
int(r["bus_reads"]) + int(r["bus_writes"]))
|
||||
NF = a.nframes or (max(meas) + 1 if meas else d.nframes)
|
||||
|
||||
pref_t, data_t, cyc_t = [], [], []
|
||||
for f in range(NF):
|
||||
m = d.modes(f).reshape(d.nby, d.nbx)
|
||||
pref = d.nby * (ROW_HEAD + ROW_TAIL)
|
||||
data = 0
|
||||
for by in range(d.nby):
|
||||
row = m[by]
|
||||
for gi in range(0, d.nbx, 4):
|
||||
g = row[gi:gi+4]
|
||||
if (g == 0).all():
|
||||
pref += ALLSKIP; data += 1
|
||||
continue
|
||||
pref += GROUP_HEAD + GROUP_TAIL - 1 # BLOCK 0 has no lsr.b
|
||||
data += 1
|
||||
for b in g:
|
||||
b = int(b)
|
||||
pw, pd = BODY[b]
|
||||
pref += DISPATCH[b] + pw + SK_TAIL
|
||||
data += 1 + pd
|
||||
pref_t.append(pref); data_t.append(data)
|
||||
cyc_t.append(meas.get(f, (0, 0))[0])
|
||||
|
||||
pref_t, data_t, cyc_t = map(np.array, (pref_t, data_t, cyc_t))
|
||||
|
||||
print(f"{a.container}: {NF} frames, {d.nb} blocks/frame\n")
|
||||
if meas:
|
||||
md = np.array([meas[f][1] for f in range(NF)])
|
||||
err = 100 * (data_t - md) / md
|
||||
print("CHECK -- derived DATA bus cycles against the C68K harness's measurement")
|
||||
print(f" measured mean {md.mean():>10,.0f} /frame")
|
||||
print(f" derived mean {data_t.mean():>10,.0f} /frame "
|
||||
f"error {err.mean():+.2f}% mean, {np.abs(err).max():.2f}% worst")
|
||||
if np.abs(err).max() > 2.0:
|
||||
sys.exit("\nFAIL: the path walk does not reproduce the measured data "
|
||||
"accesses, so its prefetch figure cannot be trusted either.")
|
||||
print(" the walk reproduces the measurement, so its prefetch count stands\n")
|
||||
|
||||
slots = cyc_t / BUS_CLK
|
||||
tot = pref_t + data_t
|
||||
print(f"{'':<22}{'mean':>12}{'median':>12}{'worst frame':>14}")
|
||||
for label, v in (("bus slots in a frame", slots),
|
||||
(" data accesses", data_t),
|
||||
(" instruction prefetch", pref_t),
|
||||
(" total bus cycles", tot)):
|
||||
print(f"{label:<22}{v.mean():>12,.0f}{np.median(v):>12,.0f}{v.max():>14,.0f}")
|
||||
occ = 100 * tot / slots
|
||||
print(f"{'bus OCCUPANCY':<22}{occ.mean():>11.1f}%{np.median(occ):>11.1f}%"
|
||||
f"{occ.max():>13.1f}%")
|
||||
free = slots - tot
|
||||
print(f"{'slots left for a DMAC':<22}{free.mean():>12,.0f}{np.median(free):>12,.0f}"
|
||||
f"{free.min():>14,.0f} (worst = fewest)")
|
||||
print(f"\nprefetch is {100*pref_t.sum()/tot.sum():.0f}% of the decoder's bus traffic: "
|
||||
f"the data-only\nfigure the harness prints understates occupancy by about 2x.")
|
||||
print(f"A DMAC painting spans at 8 clocks (2 bus cycles) per pixel could use at\n"
|
||||
f"most {free.mean()/2:,.0f} pixels' worth of the mean frame's spare slots "
|
||||
f"-- against {d.nb*16:,} pixels\nin a whole screen.")
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Bus-cycle cost of src/player/decode.s and of tools/bench/blit.s's v6 spans.
|
||||
|
||||
A 68000 bus cycle is 4 clocks (S0-S7) with no wait states, and the 68000
|
||||
prefetches every instruction word over the same bus. So a block's bus cost is
|
||||
`instruction words + data accesses`, a long access counting twice on the 16-bit
|
||||
bus and `movem.l` of N registers counting 2N.
|
||||
|
||||
The per-path word counts are read off tools/bench/decode.lst and
|
||||
tools/bench/blit.s. tools/analysis/15_bus_occupancy.py checks the DATA half of
|
||||
this table against tools/bench/c68k/c68k_bench, which counts every bus callback
|
||||
the C68K core makes: they agree to 0.04%. The prefetch half cannot be measured
|
||||
from either emulator -- MAME does not expose a fetch count and C68K reads
|
||||
opcodes through a host pointer with no callback -- so it rests on that check.
|
||||
"""
|
||||
BUS_CLK = 4
|
||||
|
||||
# --- decode.s, per block ---------------------------------------------------
|
||||
# dispatch move.b (a1),d0 / lsr.b / and.w #3 / beq .sk 6w, 1 read
|
||||
# + subq / beq .v1 -> 8w
|
||||
# + subq / bne .rw -> 10w
|
||||
# V4 body $10090..$100E2 = 82 B = 41w; 4 x (1 byte read
|
||||
# + movem.l 2 = 4 reads + 2 move.l = 4 writes) = 36
|
||||
# V1 body $100E2..$10106 = 36 B = 18w; 1 byte read
|
||||
# + movem.l 8 = 16 reads + 4 x movem.l 2 = 16 wr = 33
|
||||
# RAW body $10106..$10164 = 94 B = 47w; 8 x (2 byte reads
|
||||
# + 1 move.l = 2 writes) = 32
|
||||
BODY = {0: (0, 0), 1: (18, 33), 2: (41, 36), 3: (47, 32)}
|
||||
DISPATCH = {0: 6, 1: 8, 2: 10, 3: 10}
|
||||
SK_TAIL = 1 # addq.l #8,a4
|
||||
GROUP_HEAD = 3 # tst.b (a1) + beq allskip
|
||||
GROUP_TAIL = 4 # addq.l #1,a1 / cmpa.l a5,a4 / bne byteloop
|
||||
ALLSKIP = 9 # the whole four-block fast path, tst.b included
|
||||
ROW_HEAD, ROW_TAIL = 3, 7
|
||||
|
||||
# --- blit.s v6 spans -------------------------------------------------------
|
||||
# One chain unit moves 12 registers = 48 B = 24 pixels:
|
||||
# movem.l (a0)+,12 = 2w instr + 24 word reads = 26
|
||||
# movem.l 12,(a2) = 2w instr + 24 word writes = 26
|
||||
# lea 48(a2),a2 = 2w instr = 2
|
||||
# Per span: move.l (a0)+,a2 (1w + 2 reads) + move.w (a0)+,d0 (1w + 1 read)
|
||||
# + jmp v6ch(pc,d0.w) (2w) + dbra (2w) = 9
|
||||
V6_UNIT_PX = 24
|
||||
V6_UNIT_BUS = 54
|
||||
V6_SPAN_BUS = 9
|
||||
V6_SPAN_CYC = 43.7 # MEASURED, FINDINGS 30
|
||||
V6_PX_CYC = 9.152 # MEASURED, FINDINGS 30
|
||||
|
||||
# --- a DMAC array-chaining span -------------------------------------------
|
||||
# SOURCED, MC68450 Direct Memory Access Controller, Motorola, Jul 1989
|
||||
# (bitsavers). These replace session-10's first pass, which guessed 2 bus
|
||||
# cycles a pixel from bus arithmetic and was 12% optimistic.
|
||||
#
|
||||
# Fig 4-25 sheet 4, DUAL ADDRESS / OPERAND SIZE IS WORD / DEVICE SIZE IS
|
||||
# 16-BITS, D->M or M->D: {WORD READ, WORD WRITE} = 9 CLOCKS.
|
||||
# Confirmed by the long-operand row: two of each = 18 clocks.
|
||||
# Fig 4-25 note 2: reads are 4 clocks and WRITES ARE 5. That extra clock on
|
||||
# every write is the whole story -- it is why the DMAC does not beat a 68000
|
||||
# movem chain, which writes in 4.
|
||||
DMA_PX_CLK = 9
|
||||
# Fig 4-25 sheet 1, SEQUENTIAL ARRAY CHAINING: 36 CLOCKS per entry (three
|
||||
# word reads to fetch the 6-byte entry, plus reload).
|
||||
DMA_CHAIN_CLK = 36
|
||||
# Sect 4.5.2.1 front-end overhead 5 clocks best case, 8 worst; 4.5.2.2
|
||||
# back-end 2 clocks best. Once per period of bus ownership, not per span.
|
||||
DMA_FRONT_CLK, DMA_BACK_CLK = 5, 2
|
||||
# Fig 4-25 sheet 3, SINGLE ADDRESS: W/B READ 4 clocks, W/B WRITE 5 clocks.
|
||||
# A device->memory disk transfer is one memory WRITE = 5 clocks if the DMAC
|
||||
# holds the bus, or 5 + front + back = 12 if it arbitrates per word.
|
||||
# FINDINGS 5's long-standing 8 clk/word ESTIMATE sits inside that range.
|
||||
DMA_DISK_CLK_WORD_HELD, DMA_DISK_CLK_WORD_ARB = 5, 12
|
||||
|
||||
# The 68000 cannot execute while another master owns the bus: no cache, and a
|
||||
# two-word prefetch queue that empties immediately. So DMA time is ADDITIVE to
|
||||
# CPU time, not overlapped -- which is what FINDINGS 35's flat debit assumed
|
||||
# and session 10's first pass wrongly "refined".
|
||||
DMA_OVERLAPS = False
|
||||
|
||||
|
||||
def pad24(npix):
|
||||
return -(-npix // V6_UNIT_PX) * V6_UNIT_PX
|
||||
|
||||
|
||||
def block_bus(mode_map, spanned=None):
|
||||
"""(instruction words, data accesses) for one frame's CPU block decode.
|
||||
|
||||
`spanned` is a boolean array the same shape as mode_map marking blocks a
|
||||
span will paint instead; those blocks still cost their dispatch, because
|
||||
the mode map is walked either way, but not their body."""
|
||||
nby, nbx = mode_map.shape
|
||||
pref = nby * (ROW_HEAD + ROW_TAIL)
|
||||
data = 0
|
||||
for by in range(nby):
|
||||
row = mode_map[by]
|
||||
sp = spanned[by] if spanned is not None else None
|
||||
for gi in range(0, nbx, 4):
|
||||
g = row[gi:gi + 4]
|
||||
if (g == 0).all():
|
||||
pref += ALLSKIP
|
||||
data += 1
|
||||
continue
|
||||
pref += GROUP_HEAD + GROUP_TAIL - 1 # BLOCK 0 has no lsr.b
|
||||
data += 1
|
||||
for k, b in enumerate(g):
|
||||
b = int(b)
|
||||
if sp is not None and sp[gi + k]:
|
||||
b = 0 # the span paints it
|
||||
pw, pd = BODY[b]
|
||||
pref += DISPATCH[b] + pw + SK_TAIL
|
||||
data += 1 + pd
|
||||
return pref, data
|
||||
|
||||
|
||||
# --- v6 with a finer tail (PROPOSAL, unmeasured -- Claude's, session 10) ----
|
||||
# v6 pads every span up to 24 pixels because its unrolled chain is built from
|
||||
# 12-register movem units. Adding a second, finer chain of 2-register units
|
||||
# (4 pixels) for the tail caps the padding at 3 pixels instead of 23, for the
|
||||
# price of some more unrolled code and nothing per span.
|
||||
# A 4-pixel unit: movem.l (a0)+,2 = 2w instr + 4 reads; movem.l 2,(a2) = 2w +
|
||||
# 4 writes; lea = 2w. 14 bus cycles for 4 pixels = 56 clocks, against a full
|
||||
# unit's 24 x 9.152 = 220 for 24. Dearer per pixel, paid at most once a span.
|
||||
V6_TAIL_PX, V6_TAIL_CLK = 4, 56
|
||||
|
||||
|
||||
def v6_fine(npix):
|
||||
"""(pixels carried, CPU clocks) for a span with the finer tail."""
|
||||
k, r = divmod(npix, V6_UNIT_PX)
|
||||
t = -(-r // V6_TAIL_PX)
|
||||
return (k * V6_UNIT_PX + t * V6_TAIL_PX,
|
||||
V6_SPAN_CYC + k * V6_UNIT_PX * V6_PX_CYC + t * V6_TAIL_CLK)
|
||||
@@ -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")
|
||||
@@ -93,4 +93,29 @@ grep -q "snapshot taken" tmp/decode_check.log || {
|
||||
tail -5 tmp/decode_check.log; exit 1; }
|
||||
python3 tools/bench/verify_decode.py "$DLX" --nframes "$NF"
|
||||
|
||||
echo "--- session 10: the same decode on a second CPU core (FINDINGS 37) ---"
|
||||
# A SECOND emulator, and the cheapest strong test in the tree: seconds, no MAME,
|
||||
# no ROMs. px68k's C68K core has its own cycle table and its own memory model,
|
||||
# so a pass here says decode.s is pixel-exact under two independent cores and
|
||||
# that the harness's byte-swapped RAM / high-byte-discarding GVRAM is right --
|
||||
# which is what licenses its cycle and bus numbers.
|
||||
# Skipped rather than failed when px68k is not checked out: it is an external
|
||||
# tree, not part of this repo.
|
||||
PX68K=${PX68K:-$HOME/src/px68k}
|
||||
if [ -f "$PX68K/m68000/c68k.c" ]; then
|
||||
make -s -C tools/bench/c68k PX68K="$PX68K"
|
||||
bash tools/bench/c68k/run.sh tmp/c68k_frames.csv 2>tmp/c68k.log
|
||||
grep -a "sequential pass" tmp/c68k.log
|
||||
python3 tools/bench/c68k/verify_c68k.py "$DLX" --nframes "$NF"
|
||||
|
||||
echo "--- session 10: the bus model still matches the machine (FINDINGS 38) ---"
|
||||
# 15_bus_occupancy.py derives instruction prefetch, which no emulator here can
|
||||
# report, and validates itself against the DATA accesses the harness counts.
|
||||
# If that check ever stops holding, every bus figure in FINDINGS 38/39 is
|
||||
# unfounded -- so it is a gate, not a report.
|
||||
python3 tools/analysis/15_bus_occupancy.py "$DLX" | sed -n '3,7p'
|
||||
else
|
||||
echo " SKIPPED: no px68k at $PX68K (set PX68K= to point at a checkout)"
|
||||
fi
|
||||
|
||||
echo "ALL GREEN"
|
||||
|
||||
Reference in New Issue
Block a user