Put the player on a real volume, and find the write window is the frame
ROADMAP K3. src/player/packed.s (2,898 B) brings up its own display, builds
its own 193-entry DMA chain, keeps its own frame clock off V-DISP and fetches
every record itself with READ(10) off a CZ-6BS1. The rig writes no picture
byte, no palette entry and no CRTC register.
120 of 120 frames pixel-exact, every one compared, in both palette orders --
the gate had to grow to do it, because a packed frame is a LITERAL and the
codec's recursion was what made one comparison audit 120.
And the write window turns out to be the frame. A packed write requires R20
bit 11, buffer mode blanks the layer, and a DMAC-direct player holds the
window open for the whole data phase, so
dark fraction of a slot = record bytes / (DATA-PHASE rate x slot)
which is 1.0 at the container's own 582.0 KB/s: every frame delivered, on
time, pixel-exact, and none of them displayed. The rate in that expression is
the BURST rate, a third hardware number B1 has no test for. It reverses 61.5's
ranking -- a packed player that DMAs to RAM and paints with the measured 27.3%
blit is on screen 72.7% of every slot at any rate, and the two are equal only
at 2,131 KB/s = 3.7x the wire.
And a held channel costs the frame clock half its ticks without the clock
being able to tell: 487 of 1,038 V-DISP edges lost, zero late frames reported,
the player believing 12 fps while the screen ran at 6.37.
FINDINGS 64. ROADMAP K4 opened and fenced behind B2.
check.sh ALL GREEN before and after.
Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""HOW LONG IS THE PICTURE ACTUALLY ON SCREEN? ROADMAP K3, FINDINGS 64.
|
||||
|
||||
python3 tools/analysis/31_display_duty.py [container.dlxp] [--rate KB/s ...]
|
||||
|
||||
THE QUESTION NOTHING IN THIS TREE HAD ASKED. Every budget in docs/FINDINGS.md
|
||||
asks what a frame COSTS -- clocks, bus cycles, bytes on the wire. Session 32
|
||||
built the packed player and ran it (src/player/packed.s), and the run reported a
|
||||
number no budget has a column for: the write window was open on 99.5% of the
|
||||
host frames, so the graphics layer was DARK for 99.5% of the scene. Every frame
|
||||
was pixel-exact and almost none of them was visible.
|
||||
|
||||
WHY THAT IS ARITHMETIC AND NOT AN EMULATOR ARTEFACT. 256-colour GVRAM masks the
|
||||
high byte of every write unless CRTC R20 bit 11 is set (46.5/47.1), and the
|
||||
packed layout's whole 1.0 B/pixel claim is that one word carries two pixels --
|
||||
so a packed write REQUIRES the bit. If buffer mode blanks the layer while the
|
||||
bit is set (47.4/ROADMAP B2 -- MAME says it does, and 48.1's prior leans that
|
||||
way), then the layer is dark for exactly as long as the window is open, and for
|
||||
a DMAC-direct player the window is open for the whole transfer. There is no
|
||||
second page to hide behind: the packed layout SPENDS both 256-colour pages,
|
||||
which is the same fact that made a frame one channel start (FINDINGS 62).
|
||||
|
||||
dark fraction of a slot = record bytes / (data-phase rate x slot)
|
||||
|
||||
AND THE RATE IN THAT EXPRESSION IS THE BURST RATE, NOT THE SUSTAINED ONE. This
|
||||
is the correction the session had to make to itself. The container's 582.0 KB/s
|
||||
is a SUSTAINED requirement -- it decides whether record i arrives before slot i.
|
||||
The dark fraction is set by how fast bytes move DURING THE DATA PHASE, which for
|
||||
a drive with a read-ahead cache can be several times the sustained figure. The
|
||||
two are independent, and a medium can pass one and fail the other:
|
||||
|
||||
sustained >= 582.0 KB/s or frames arrive late (B1, known)
|
||||
data phase >> 582.0 KB/s or the frame is never displayed (NEW, and B1 has
|
||||
no test for it)
|
||||
|
||||
THE OTHER PLAYER IN THE FAMILY DOES NOT HAVE THIS PROPERTY. A packed player
|
||||
that DMAs the record into RAM and paints it with the CPU opens the window only
|
||||
for the paint -- tools/bench/blit.s V8, MEASURED, not assumed -- which is a
|
||||
fixed share of the slot no matter what the medium does. It costs more clocks
|
||||
and 49 KB of RAM and it buys a picture that is on screen. FINDINGS 61.5 already
|
||||
priced both in CLOCKS and ranked DMAC-direct first; this file is the column that
|
||||
was missing from that table, and it reverses the ranking under B2-blanks.
|
||||
"""
|
||||
import argparse, os, re, sys
|
||||
sys.path.insert(0, "tools/encoder")
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from dlxp import DLXP
|
||||
import buscost as B
|
||||
|
||||
CPUHZ = 10e6 # stock X68000, MAME 0.277 x68k.cpp:1133
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("container", nargs="?", default="tmp/packed_singe.dlxp")
|
||||
ap.add_argument("--blit-log", default="tmp/blit_v8.log",
|
||||
help="tools/bench/blit.lua's log -- the MEASURED packed paint. "
|
||||
"Not a constant in this file: 47.6.1 filed the movem shape "
|
||||
"as an assumption and session 29 measured it, and a second "
|
||||
"copy of a measured number is how one of them goes stale.")
|
||||
ap.add_argument("--rate", type=float, nargs="*", default=None,
|
||||
help="data-phase rates to price, KB/s. REQUIRED to mean "
|
||||
"anything: this project has no delivery figure and will "
|
||||
"not default to one (FINDINGS 50).")
|
||||
ap.add_argument("--run-log", default="tmp/packed_free_steal.log",
|
||||
help="a free-running tools/bench/packed.lua log, for the "
|
||||
"measured corroboration section")
|
||||
a = ap.parse_args()
|
||||
|
||||
d = DLXP(a.container)
|
||||
SLOT_S = 1.0 / d.fps
|
||||
FRAME_CLK = CPUHZ * SLOT_S
|
||||
wire = d.kbps() # KB/s, and fixed by geometry
|
||||
|
||||
blit = {}
|
||||
if os.path.exists(a.blit_log):
|
||||
for line in open(a.blit_log, errors="replace"):
|
||||
m = re.search(r"V(\d+)\s+(\d+) cyc", line)
|
||||
if m:
|
||||
blit[int(m.group(1))] = int(m.group(2))
|
||||
if 8 not in blit:
|
||||
sys.exit(f"{a.blit_log} has no V8 result. The packed paint is a MEASUREMENT "
|
||||
f"(tools/bench/blit.lua) and this tool will not substitute a "
|
||||
f"constant for it -- run the blit bench, or point --blit-log at "
|
||||
f"its log.")
|
||||
PAINT_CLK = blit[8]
|
||||
PAINT_FRAC = PAINT_CLK / FRAME_CLK
|
||||
|
||||
print(f"""THE CONTAINER (tools/encoder/dlxp.py)
|
||||
{a.container}: {d.W}x{d.H} {d.fps} fps, {d.nframes} frames
|
||||
record {d.rec_bytes:,} B = {d.rec_bytes // 512} sectors, palette \
|
||||
{'LAST' if d.palette_last else 'FIRST'}
|
||||
slot {SLOT_S*1000:.2f} ms = {FRAME_CLK:,.0f} clocks
|
||||
wire {wire:.1f} KB/s -- FIXED by geometry. A codec's bitrate is a lever and a
|
||||
literal frame's is not (61.6), so nothing an encoder does moves this.
|
||||
|
||||
THE TWO PACKED PLAYERS, and the difference is WHEN the write window is open
|
||||
|
||||
A. DMAC-DIRECT (src/player/packed.s, ROADMAP K3, and the one that is built).
|
||||
One channel start, 193 destinations, the CPU halted or nearly. The window
|
||||
must be open for the WHOLE data phase, because the channel writes when the
|
||||
bytes arrive and the CPU cannot know when that is -- and a packed write
|
||||
that lands with the bit clear is masked to its low byte and silently wrong.
|
||||
B. DMA-TO-RAM + CPU PAINT. The record lands in RAM with the window shut; the
|
||||
68000 then paints it with the packed movem blit. The window is open for the
|
||||
PAINT and nothing else: {PAINT_CLK:,} clocks, {100*PAINT_FRAC:.1f}% of a slot,
|
||||
MEASURED by tools/bench/blit.lua (V8), and INDEPENDENT of the medium.
|
||||
|
||||
Under 47.4-blanks the dark interval IS the open window, so B is on screen for
|
||||
{100*(1-PAINT_FRAC):.1f}% of every slot at ANY rate that delivers the record at all,
|
||||
and A's visibility is a function of the rate.
|
||||
""")
|
||||
|
||||
rates = a.rate
|
||||
if not rates:
|
||||
print("""NO RATES GIVEN, so no table. This project retired its delivery
|
||||
constant outright (FINDINGS 50, USER DECISION) and every tool requires an
|
||||
explicit rate; a default here would be the same mistake in a new place. Pass
|
||||
--rate with the figures you want priced. The three thresholds already derived
|
||||
elsewhere, for reference and NOT as defaults:
|
||||
453.6 KB/s the DLX5 codec gate container needs no prefill (49.5/60)
|
||||
576.0 KB/s a packed container with no per-frame palette (61.5)
|
||||
582.0 KB/s THIS container, palette included (63)
|
||||
and note that all three are SUSTAINED figures. The dark fraction below is set by
|
||||
the DATA-PHASE rate, which is a different measurement nothing has taken.""")
|
||||
sys.exit(0)
|
||||
|
||||
print("A's VISIBILITY, against the DATA-PHASE rate\n")
|
||||
print(f" {'data phase':>12} | {'transfer':>9} | {'window open':>11} | "
|
||||
f"{'PICTURE ON SCREEN':>17} | vs B")
|
||||
print(f" {'KB/s':>12} | {'ms':>9} | {'% of slot':>11} | "
|
||||
f"{'% of slot':>17} |")
|
||||
print(" " + "-"*12 + "-+-" + "-"*9 + "-+-" + "-"*11 + "-+-" + "-"*17 + "-+-----")
|
||||
for R in sorted(rates):
|
||||
t_ms = d.rec_bytes / (R * 1024) * 1000
|
||||
openf = min(1.0, t_ms / (SLOT_S * 1000))
|
||||
vis = 1.0 - openf
|
||||
verdict = ("A wins" if vis > 1 - PAINT_FRAC else
|
||||
"B wins" if vis < 1 - PAINT_FRAC else "equal")
|
||||
late = " LATE" if R < wire else ""
|
||||
print(f" {R:>12.1f} | {t_ms:>9.2f} | {100*openf:>11.1f} | "
|
||||
f"{100*vis:>17.1f} | {verdict}{late}")
|
||||
|
||||
# The crossover, stated as a rate rather than left to be read off the table: it
|
||||
# is the one number in here a hardware acceptance test can be written against.
|
||||
cross = d.rec_bytes / (PAINT_FRAC * SLOT_S) / 1024
|
||||
print(f"""
|
||||
A and B show the picture for the same share of the slot at a data-phase rate
|
||||
of {cross:,.0f} KB/s. Below that, THE PLAYER WITH THE CPU IN THE LOOP IS ON
|
||||
SCREEN LONGER than the one without it -- which is the reverse of FINDINGS
|
||||
61.5's ranking, and 61.5 is not wrong: it ranked them in CLOCKS, and this is
|
||||
the column that table does not have.
|
||||
|
||||
{cross:,.0f} KB/s is {cross/wire:.1f}x the container's own wire. So a medium that exactly
|
||||
meets the sustained requirement puts the DMAC-direct player's picture on
|
||||
screen for {100*max(0.0, 1-wire/wire):.0f}% of every slot: it delivers every frame, on time,
|
||||
pixel-exact, and displays none of them.""")
|
||||
|
||||
print(f"""
|
||||
THE CPU SIDE, so the trade is priced on both axes (FINDINGS 61.5's ladder)
|
||||
|
||||
W is clocks stolen per delivered byte. Only the dual-address rungs have a code
|
||||
path on this machine (59.2), and 9 is the floor: a 4-clock read of the device
|
||||
plus a 5-clock write to memory.
|
||||
""")
|
||||
print(f" {'W':>3} | {'A: DMAC-direct':>15} | {'B: DMA + CPU paint':>19}")
|
||||
print(" " + "-"*3 + "-+-" + "-"*15 + "-+-" + "-"*19)
|
||||
AUDIO = B.ADPCM_BYTES_PER_S / d.fps * B.ADPCM_CLK_BYTE_BEST
|
||||
for W in (5, 9, 12, 16, 19):
|
||||
xfer = d.rec_bytes * W
|
||||
ca = (xfer + AUDIO) / FRAME_CLK
|
||||
cb = (xfer + AUDIO + PAINT_CLK) / FRAME_CLK
|
||||
print(f" {W:>3} | {100*ca:>14.1f}% | {100*cb:>18.1f}%")
|
||||
print(f"""
|
||||
Both include the audio DMA at {AUDIO:,.0f} clocks a frame ({100*AUDIO/FRAME_CLK:.2f}%), charged from
|
||||
the IPL ROM's own channel-3 setup (21_iplrom_dmac.py, 52.5). Neither includes
|
||||
a decoder, because neither has one.
|
||||
|
||||
So B costs the paint -- {100*PAINT_FRAC:.1f}% of a frame -- and TWO record buffers,
|
||||
{2*d.rec_bytes:,} B of RAM. Two and not one: at any rate near the wire the delivery
|
||||
of record i+1 occupies most of the slot the paint of record i happens in, so
|
||||
they overlap by construction. On a 2 MB machine that is {200*d.rec_bytes/(2*1024*1024):.1f}% of memory and
|
||||
it is the resource this design has spare -- the ring the packed branch deleted
|
||||
was 256 KB (FINDINGS 49). That is what a visible picture costs if 47.4 blanks.""")
|
||||
|
||||
# ---- the measured corroboration. It is a SEPARATE section and it is bounded
|
||||
# on purpose: MAME's device models carry no transfer timing (docs/BENCHMARK.md,
|
||||
# 42.5), so the run cannot supply a rate for the table above -- what it can do
|
||||
# is show that the mechanism is real and that the arithmetic predicts it.
|
||||
if os.path.exists(a.run_log):
|
||||
txt = open(a.run_log, errors="replace").read()
|
||||
m_rate = re.search(r"record lands in ([\d.]+) ms, i\.e\. ([\d.]+) KB/s", txt)
|
||||
m_open = re.search(r"WRITE WINDOW OPEN on (\d+) of (\d+) host frames", txt)
|
||||
if m_rate and m_open:
|
||||
ms, kbps = float(m_rate.group(1)), float(m_rate.group(2))
|
||||
op, tot = int(m_open.group(1)), int(m_open.group(2))
|
||||
pred = min(1.0, d.rec_bytes / (kbps * 1024) / SLOT_S)
|
||||
print(f"""
|
||||
MEASURED, on the emulated machine (tools/bench/packed_run.sh, free-running)
|
||||
|
||||
{a.run_log}: a {d.rec_bytes:,} B record landed in {ms:.2f} ms = {kbps:.1f} KB/s, and the
|
||||
write window was open on {op} of {tot} host frames = {100*op/tot:.1f}%.
|
||||
The expression above predicts {100*pred:.1f}% at that rate.
|
||||
|
||||
THIS IS NOT A RATE MEASUREMENT AND {kbps:.0f} KB/s IS NOT A MEDIUM. MAME's
|
||||
device models carry no transfer timing (42.5); the figure is a property of the
|
||||
apparatus. What the run DOES establish is that the mechanism is the one the
|
||||
arithmetic describes -- and one thing more that no arithmetic could have
|
||||
given: the DMAC CONFIGURATION DOES NOT MOVE IT. Held and stealing delivered
|
||||
the same record within 0.5% of each other, so what a channel configuration
|
||||
buys is who owns the CPU, not when the picture appears.""")
|
||||
else:
|
||||
print(f"""
|
||||
NO MEASURED SECTION: {a.run_log} is absent. Run
|
||||
tools/bench/packed_run.sh to produce it. The arithmetic above stands without
|
||||
it -- it is geometry -- but the run is what showed the effect was there to be
|
||||
derived at all.""")
|
||||
@@ -703,4 +703,36 @@ else
|
||||
echo " SKIPPED: no px68k at $PX68K -- the container's bytes were not rendered"
|
||||
fi
|
||||
|
||||
echo "--- session 32: the PACKED PLAYER, end to end off the disc (FINDINGS 64) ---"
|
||||
# ROADMAP K3. src/player/packed.s brings up its own display, builds its own
|
||||
# 193-entry DMA chain, keeps its own frame clock off V-DISP and fetches every
|
||||
# record itself with READ(10) off a CZ-6BS1. This script writes no picture byte
|
||||
# and no palette entry.
|
||||
#
|
||||
# WHY EVERY FRAME IS COMPARED AND THE CODEC'S GATE COMPARES ONE. The codec is
|
||||
# temporally recursive -- a SKIP block is a claim about the previous frame -- so
|
||||
# its last frame audits all 120. A packed frame is a LITERAL and frame 119 says
|
||||
# nothing about frame 60. The simplification that deleted the ring also deleted
|
||||
# the gate's free lunch.
|
||||
#
|
||||
# AND WHY IT IS PACED AT HALF RATE. The write window has to be OPEN for the
|
||||
# whole transfer and buffer mode blanks the graphics layer, so at the
|
||||
# container's own 12 fps there is no instant at which a complete frame is
|
||||
# displayable and there is nothing to snapshot (FINDINGS 64.2 -- that is the
|
||||
# session's finding, not a rig limitation being worked around). Half rate opens
|
||||
# a display interval without changing one byte of the transfer.
|
||||
#
|
||||
# GATE ONLY: runs 2-4 of packed_run.sh measure the apparatus rather than gate
|
||||
# it, and they are three more MAME jobs for numbers that cannot change unless
|
||||
# MAME does. tools/bench/packed_run.sh with no DLX_PK_GATE_ONLY runs all four.
|
||||
if [ -f "$HOME/mame/roms/x68000.zip" ] || [ -d "$HOME/mame/roms/x68000" ]; then
|
||||
DLX_PK_GATE_ONLY=1 DLX_PK_NFR="$NF" bash tools/bench/packed_run.sh \
|
||||
tmp/packed_singe.dlxp > tmp/packed_gate_stage.log 2>&1 \
|
||||
|| { cat tmp/packed_gate_stage.log; exit 1; }
|
||||
grep -aE "^ (FLAG|array|chain|frame clock|late frames|WRITE WINDOW)|^OK " \
|
||||
tmp/packed_gate_stage.log
|
||||
else
|
||||
echo " SKIPPED: no x68000 romset -- the player was not run"
|
||||
fi
|
||||
|
||||
echo "ALL GREEN"
|
||||
|
||||
+31
-8
@@ -3,7 +3,21 @@
|
||||
# instantiate the card. Sourced-by-calling from tools/bench/scsi_run.sh and
|
||||
# tools/bench/pace_run.sh so there is ONE copy of the layout.
|
||||
#
|
||||
# tools/bench/mkvol.sh [container.dlx]
|
||||
# tools/bench/mkvol.sh [container.dlx | container.dlxp]
|
||||
#
|
||||
# TWO CONTAINERS, ONE VOLUME BUILDER. A DLX volume is tools/bench/prep_stream.py's
|
||||
# disk image -- the codec's records, laid down from sector 0. A DLXP volume
|
||||
# (ROADMAP K2/K3) needs no preparation at all: the container is ALREADY a
|
||||
# sector-aligned image of itself -- a 512 B header, then fixed 97-sector records
|
||||
# -- so the file IS the volume and copying it is the whole build. That is not a
|
||||
# convenience, it is the format's central claim (tools/encoder/dlxp.py) arriving
|
||||
# at the disc, and a builder that transformed it on the way would be hiding the
|
||||
# claim rather than testing it.
|
||||
#
|
||||
# The two get DIFFERENT CHD NAMES. Alternating between the packed rig and the
|
||||
# codec rig would otherwise rebuild the volume on every run, and -- much worse --
|
||||
# a stale CHD under the name the other rig expected would serve one container's
|
||||
# bytes to the other's gate, which reads as a decode failure and is not one.
|
||||
#
|
||||
# ONE COPY, ON PURPOSE. The volume is tmp/stream_disk.bin -- byte for byte the
|
||||
# file the host-file ring rig reads -- laid out as 512 B sectors. If two scripts
|
||||
@@ -22,15 +36,24 @@ set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
DLX=${1:-tmp/rc_fr_singe_scsi_span.dlx}
|
||||
|
||||
[ -f tmp/stream_disk.bin ] || python3 tools/bench/prep_stream.py "$DLX" > /dev/null
|
||||
if [ ! -f tmp/dlxdisk.chd ] || [ tmp/stream_disk.bin -nt tmp/dlxdisk.chd ]; then
|
||||
python3 - <<'PY'
|
||||
d = open("tmp/stream_disk.bin", "rb").read()
|
||||
case "$DLX" in
|
||||
*.dlxp) SRC="$DLX"
|
||||
IMG=tmp/dlxpdisk.img; CHD=tmp/dlxpdisk.chd ;;
|
||||
*) SRC=tmp/stream_disk.bin
|
||||
IMG=tmp/dlxdisk.img; CHD=tmp/dlxdisk.chd
|
||||
[ -f "$SRC" ] || python3 tools/bench/prep_stream.py "$DLX" > /dev/null ;;
|
||||
esac
|
||||
|
||||
if [ ! -f "$CHD" ] || [ "$SRC" -nt "$CHD" ]; then
|
||||
SRC="$SRC" IMG="$IMG" python3 - <<'PY'
|
||||
import os
|
||||
src, img = os.environ["SRC"], os.environ["IMG"]
|
||||
d = open(src, "rb").read()
|
||||
n = (len(d) + 511) // 512
|
||||
open("tmp/dlxdisk.img", "wb").write(d + b"\0" * (n * 512 - len(d)))
|
||||
open(img, "wb").write(d + b"\0" * (n * 512 - len(d)))
|
||||
print(f" disc image: {len(d)} B of records -> {n} sectors")
|
||||
PY
|
||||
rm -f tmp/dlxdisk.chd
|
||||
rm -f "$CHD"
|
||||
# -c none IS LOAD-BEARING, and it was found by a gate rather than by taste.
|
||||
# Session 28, on the DLX5 volume: with the default (lzma/zlib/huff/flac) MAME
|
||||
# 0.277 served the CHD FILE'S OWN BYTES as sector data -- the destination
|
||||
@@ -45,7 +68,7 @@ PY
|
||||
# disc space: the failure is SILENT at the transport layer -- every READ(10)
|
||||
# reports success and returns the wrong bytes -- and only the byte comparison
|
||||
# in tools/bench/scsi.lua catches it.
|
||||
chdman createhd -i tmp/dlxdisk.img -o tmp/dlxdisk.chd -ss 512 -c none > /dev/null 2>&1
|
||||
chdman createhd -i "$IMG" -o "$CHD" -ss 512 -c none > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
mkdir -p tmp/p4roms/x68k_cz6bs1
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
-- Drive src/player/packed.s: THE DECODER-FREE PACKED PLAYER, END TO END,
|
||||
-- OFF A REAL VOLUME. ROADMAP K3.
|
||||
--
|
||||
-- WHAT THIS SCRIPT DOES NOT DO IS THE POINT OF IT. tools/bench/stream.lua
|
||||
-- pushes expanded codebooks and a packed palette into RAM, plays a transport at
|
||||
-- a modelled byte rate, and writes the frame tick. This one pushes 2,898 bytes
|
||||
-- of 68000 code and eleven mailbox words, and then READS. It moves no picture
|
||||
-- byte, models no rate, sets no CRTC register and writes no palette entry: the
|
||||
-- machine brings up its own display, builds its own chain, keeps its own clock
|
||||
-- off V-DISP and fetches every record itself with READ(10) off a CZ-6BS1.
|
||||
--
|
||||
-- SO THE GATE IS NOT THE ONE THE CODEC USES, AND IT HAD TO CHANGE.
|
||||
-- tools/bench/verify_decode.py checks ONE frame -- the last -- and that audits
|
||||
-- the whole run because the codec is temporally recursive: a SKIP block is a
|
||||
-- claim about the previous frame still being on screen, so the final frame is
|
||||
-- only right if all 120 were. A packed frame is a LITERAL. Frame 119 being
|
||||
-- pixel-exact says nothing whatever about frame 60. This script therefore
|
||||
-- snapshots EVERY frame and tools/bench/verify_packed.py compares all of them;
|
||||
-- the simplification that deleted the ring also deleted the gate's free lunch.
|
||||
--
|
||||
-- WHEN A SNAPSHOT IS TAKEN, and why not on the frame it changed. PG_SHOWN is
|
||||
-- bumped by the 68000 after it clears R20 bit 11, so a change means "a complete
|
||||
-- frame is now displayable". But MAME's screen bitmap for the host frame in
|
||||
-- progress was drawn partly before that instant, so snapshotting immediately
|
||||
-- would sample the write window -- which BLANKS the graphics layer -- for part
|
||||
-- of the picture. A 12 fps frame lasts 4 or 5 host refreshes at 56.69 Hz, so
|
||||
-- waiting SNAP_DELAY whole host frames is safely inside the slot and safely
|
||||
-- after the window closed.
|
||||
--
|
||||
-- Env:
|
||||
-- DLX_PK_HELD 1 = the channel HOLDS THE BUS (burst, max rate), 0 = it
|
||||
-- steals cycles. Not two speeds of one thing: 59.3 showed an
|
||||
-- auto-requested channel is charged by TIME, so held is the
|
||||
-- 68000 stopped for as long as the record takes to arrive.
|
||||
-- Default 1.
|
||||
-- DLX_PK_PACE 1 = the machine holds itself to the container's fps off
|
||||
-- V-DISP (default). 0 free-runs, which tests the CHAIN with
|
||||
-- the clock out of the way.
|
||||
-- DLX_PK_ITER passes over the scene (default 1). >1 exercises the seek,
|
||||
-- which for this container is arithmetic and nothing else.
|
||||
-- DLX_PK_NFR play only the first N frames (default: all of them)
|
||||
-- DLX_PK_FPS pace at this rate instead of the container's. NOT a
|
||||
-- cosmetic knob and not a way to make a number look better:
|
||||
-- under MAME the emulated transport takes about a whole 12 fps
|
||||
-- slot to deliver a 49,664 B record, and the write window has
|
||||
-- to be OPEN for all of it -- so at 12 fps there is no instant
|
||||
-- at which a complete frame is displayable and nothing can be
|
||||
-- snapshotted. Pacing slower opens a display interval without
|
||||
-- changing one byte of the transfer, which is what lets the
|
||||
-- PIXEL-EXACTNESS of all 120 frames be gated separately from
|
||||
-- the RATE the emulated transport happens to run at. The two
|
||||
-- are different questions and this is the knob that separates
|
||||
-- them.
|
||||
-- DLX_PK_CSV write the per-frame arrival series here
|
||||
|
||||
M = manager.machine
|
||||
SP = M.devices[":maincpu"].spaces["program"]
|
||||
|
||||
local function findfile(n)
|
||||
for _,p in ipairs{"../tools/bench/"..n, "tools/bench/"..n, n} do
|
||||
local f = io.open(p,"rb"); if f then f:close(); return p end
|
||||
end
|
||||
error(n.." not found")
|
||||
end
|
||||
local META = loadfile("packed_meta.lua")()
|
||||
|
||||
-- src/player/packed.s. Inputs first, then outputs; the split is the file's.
|
||||
local PG_FLAG, PG_NFR, PG_FPS, PG_LBA0 = 0x18900, 0x18904, 0x18908, 0x1890C
|
||||
local PG_RECS, PG_PALL, PG_HELD = 0x18910, 0x18914, 0x18918
|
||||
local PG_PACEON, PG_ITER = 0x1891C, 0x18920
|
||||
local PG_SHOWN, PG_ERR, PG_ERRAT = 0x18930, 0x18934, 0x18938
|
||||
local PG_LATE, PG_LATE1, PG_LATEM = 0x1893C, 0x18940, 0x18944
|
||||
local PG_VDISP, PG_VD0, PG_TSPIN = 0x18948, 0x1894C, 0x18950
|
||||
local PG_GSPIN, PG_LOSTV, PG_ARRN = 0x18954, 0x18958, 0x1895C
|
||||
local PG_ARR = 0x1B000
|
||||
-- src/player/clock.i and src/player/scsi.i, read for diagnosis only.
|
||||
local CLK_PACE, CLK_VDISP, CLK_ERR = 0x18034, 0x18064, 0x1806C
|
||||
local SC_ERR = 0x18200
|
||||
local CRTC_R20 = 0xE80028
|
||||
|
||||
local HELD = (os.getenv("DLX_PK_HELD") or "1") == "1"
|
||||
local PACED = (os.getenv("DLX_PK_PACE") or "1") == "1"
|
||||
local ITERS = tonumber(os.getenv("DLX_PK_ITER") or "") or 1
|
||||
local NFR = tonumber(os.getenv("DLX_PK_NFR") or "") or META.nframes
|
||||
local FPS = tonumber(os.getenv("DLX_PK_FPS") or "") or META.fps
|
||||
local CSV = os.getenv("DLX_PK_CSV")
|
||||
local SNAP_DELAY = 2
|
||||
|
||||
local SCERRNAME = {[0]="OK", "SELECTION TIMEOUT -- no target answered",
|
||||
"UNEXPECTED PHASE", "POLL TIMEOUT -- a phase never arrived",
|
||||
"NON-ZERO SCSI STATUS",
|
||||
"WINDOWED READ REFUSED -- a channel cannot drop bytes"}
|
||||
|
||||
local code do local f=assert(io.open("packed.bin","rb")); code=f:read("a"); f:close() end
|
||||
|
||||
local function T() local t=M.time; return t.seconds + t.attoseconds/1e18 end
|
||||
local function P(s) print("[PK] "..s) end
|
||||
|
||||
local function setup()
|
||||
for i = 1, #code do SP:write_u8(0x10000+i-1, string.byte(code,i)) end
|
||||
SP:write_u32(PG_FLAG, 0)
|
||||
SP:write_u32(PG_NFR, NFR)
|
||||
SP:write_u32(PG_FPS, FPS)
|
||||
SP:write_u32(PG_LBA0, META.lba0)
|
||||
SP:write_u32(PG_RECS, META.rec_sectors)
|
||||
SP:write_u32(PG_PALL, META.palette_last)
|
||||
SP:write_u32(PG_HELD, HELD and 1 or 0)
|
||||
SP:write_u32(PG_PACEON, PACED and 1 or 0)
|
||||
SP:write_u32(PG_ITER, ITERS)
|
||||
SP:write_u32(PG_SHOWN, 0)
|
||||
P(string.format("packed.bin=%d B, %dx%d %d fps, %d of %d frames, %d passes",
|
||||
#code, META.W, META.H, META.fps, NFR, META.nframes, ITERS))
|
||||
P(string.format("record %d B = %d sectors at LBA %d + i*%d, palette %s",
|
||||
META.rec_bytes, META.rec_sectors, META.lba0,
|
||||
META.rec_sectors, META.palette_last == 1 and "LAST" or "FIRST"))
|
||||
P(string.format("channel: %s, %s",
|
||||
HELD and "BUS HELD (burst, max rate)" or "CYCLE STEALING",
|
||||
PACED and ("SELF-PACED at "..FPS.." fps off V-DISP"
|
||||
..(FPS ~= META.fps and (" -- NOT the container's "
|
||||
..META.fps..", see DLX_PK_FPS") or ""))
|
||||
or "FREE-RUNNING (tests the chain, not the clock)"))
|
||||
P("this script writes NO picture byte, NO palette entry and NO CRTC register: "
|
||||
.."the machine brings up its own display and fetches its own records.")
|
||||
end
|
||||
|
||||
local function launch()
|
||||
local cpu = M.devices[":maincpu"]
|
||||
cpu.state["SR"].value = 0x2700 -- supervisor, all interrupts masked;
|
||||
cpu.state["SP"].value = 0x8000 -- clk_init lowers it to $2500 itself
|
||||
cpu.state["PC"].value = 0x10000
|
||||
end
|
||||
|
||||
local st, t0 = "boot", nil
|
||||
local shown, pending, snaps = 0, nil, 0
|
||||
local arrive, hostfr, missed = {}, 0, 0
|
||||
-- WHICH FRAME EACH SNAPSHOT IS. MAME numbers snapshots 0000, 0001, ... in the
|
||||
-- order they were taken, and a frame that could not be sampled leaves no gap in
|
||||
-- that sequence -- so the file name is NOT the frame index and a verifier that
|
||||
-- assumed it was would compare frame 61 against record 60 and report a codec
|
||||
-- bug that is really a bookkeeping one. This is the map, written out for it.
|
||||
local snapfr = {}
|
||||
local r20seen = {}
|
||||
|
||||
SUB = emu.add_machine_frame_notifier(function()
|
||||
local ok, err = pcall(function()
|
||||
local t = T()
|
||||
if st == "boot" then
|
||||
if t < 3.0 then return end
|
||||
setup(); launch(); t0 = t; st = "running"; return
|
||||
end
|
||||
if st ~= "running" then return end
|
||||
hostfr = hostfr + 1
|
||||
|
||||
-- WHAT THE SCREEN MODE WAS, sampled every host frame. R20 bit 11 blanks
|
||||
-- the graphics layer, so this is the only way to see the shutter the player
|
||||
-- is running: the fraction of host frames that found the window OPEN is the
|
||||
-- fraction of the scene the display spent dark, and it is a MEASUREMENT of
|
||||
-- 47.4's cost under MAME rather than a restatement of the prior.
|
||||
local r20 = SP:read_u16(CRTC_R20)
|
||||
r20seen[#r20seen+1] = ((r20 >> 11) & 1)
|
||||
|
||||
local s = SP:read_u32(PG_SHOWN)
|
||||
if s > shown then
|
||||
-- Only the LAST change matters if several landed in one host frame; that
|
||||
-- cannot happen at 12 fps on a 56.69 Hz raster, and if it ever does the
|
||||
-- gate below catches it as a missing snapshot rather than a wrong one.
|
||||
arrive[#arrive+1] = {n = s, t = t - t0}
|
||||
shown = s
|
||||
pending = SNAP_DELAY
|
||||
end
|
||||
if pending then
|
||||
pending = pending - 1
|
||||
if pending <= 0 then
|
||||
pending = nil
|
||||
if (SP:read_u16(CRTC_R20) >> 11) & 1 == 0 then
|
||||
M.video:snapshot(); snaps = snaps + 1
|
||||
snapfr[#snapfr+1] = shown - 1
|
||||
else
|
||||
-- The window was open again when the delay expired: the frame we
|
||||
-- meant to sample is being overwritten. COUNTED, NOT PRINTED -- when
|
||||
-- the transfer is longer than the slot EVERY frame misses, and 119
|
||||
-- identical lines bury the four numbers the run exists to report.
|
||||
-- The count is reported once at the end and the gate reads it there.
|
||||
missed = missed + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local flag = SP:read_u32(PG_FLAG)
|
||||
if flag ~= 1 and pending then
|
||||
-- THE LAST FRAME IS STILL PENDING. packed.s spins in pg_hold with the
|
||||
-- window CLOSED once the scene is over, so the delay can simply run out;
|
||||
-- returning here rather than reporting is what stops the final frame
|
||||
-- being the one frame the gate never sees.
|
||||
return
|
||||
end
|
||||
if flag ~= 1 then
|
||||
st = "done"
|
||||
local wall = t - t0
|
||||
P(string.format("FLAG=$%02X after %.3f s, %d frames shown, %d snapshots, "
|
||||
.."%d frames NOT SAMPLED (the write window had reopened "
|
||||
.."-- the transfer is longer than the display interval)",
|
||||
flag, wall, shown, snaps, missed))
|
||||
local err = SP:read_u32(PG_ERR)
|
||||
if err ~= 0 then
|
||||
P(string.format("TRANSPORT FAILED on frame %d: %s",
|
||||
SP:read_u32(PG_ERRAT), SCERRNAME[err] or ("code "..err)))
|
||||
end
|
||||
P(string.format("array: the 68000 built %d entries (the container wants "
|
||||
.."%d)", SP:read_u32(PG_ARRN), META.entries))
|
||||
-- The first entry, read back out of the machine's own RAM. It is the one
|
||||
-- place palette-first and palette-last are visible as a FACT rather than
|
||||
-- as a flag the rig passed in and the rig read back.
|
||||
P(string.format("chain[0] = MAR $%06X MTC %d ; chain[1] = MAR $%06X MTC %d",
|
||||
SP:read_u32(PG_ARR), SP:read_u16(PG_ARR+4),
|
||||
SP:read_u32(PG_ARR+6), SP:read_u16(PG_ARR+10)))
|
||||
if PACED then
|
||||
local vd = SP:read_u32(PG_VDISP) - SP:read_u32(PG_VD0)
|
||||
-- THE CLOCK, AGAINST THE RASTER THAT DROVE IT. CLK_VDISP counts the
|
||||
-- edges the 68000's ISR SAW. hostfr counts the frames MAME actually
|
||||
-- drew. A held channel halts the CPU, and the MFP's pending bit is one
|
||||
-- bit, so an edge that falls inside a transfer long enough to span two
|
||||
-- of them is an edge the machine can never count. Nothing in this
|
||||
-- project has ever run a transfer and a clock at once, so nothing could
|
||||
-- have seen this before.
|
||||
P(string.format("frame clock: PACE=%d ticks, V-DISP edges SEEN=%d, "
|
||||
.."host frames drawn=%d -> %d edges LOST (%.1f%%)",
|
||||
SP:read_u32(CLK_PACE), vd, hostfr, hostfr - vd,
|
||||
hostfr > 0 and (hostfr-vd)*100/hostfr or 0))
|
||||
local nlate = SP:read_u32(PG_LATE)
|
||||
P(string.format("late frames (tick already past at the gate): %d%s",
|
||||
nlate, nlate > 0 and string.format(", first %d, worst "
|
||||
.."%d ticks", SP:read_u32(PG_LATE1),
|
||||
SP:read_u32(PG_LATEM)) or ""))
|
||||
-- AND WHY `late = 0` IS NOT `on time`. The gate compares the frame
|
||||
-- index against PACE, and PACE is advanced by the ISR that the held
|
||||
-- channel stops the CPU from running. A clock that loses edges loses
|
||||
-- them from BOTH sides of the comparison, so a player whose own clock
|
||||
-- has halved still reports every frame early. The LOST figure above is
|
||||
-- the only thing in this run that can contradict it, and it comes from
|
||||
-- the host's raster count rather than from the machine.
|
||||
if hostfr - vd > 0 then
|
||||
P(string.format(" ...and %d of those ticks were never "
|
||||
.."issued, so `late=%d` is measured against a clock "
|
||||
.."running at %.1f%% of the raster. The player "
|
||||
.."believes it is at %d fps and the screen is at "
|
||||
.."%.2f.", hostfr - vd, nlate, vd*100/hostfr, FPS,
|
||||
FPS * vd / hostfr))
|
||||
end
|
||||
end
|
||||
P(string.format("CPU: %d trips round the TRANSFER wait in total, %d round "
|
||||
.."the PACE gate", SP:read_u32(PG_TSPIN),
|
||||
SP:read_u32(PG_GSPIN)))
|
||||
local open = 0
|
||||
for _,v in ipairs(r20seen) do open = open + v end
|
||||
P(string.format("WRITE WINDOW OPEN on %d of %d host frames (%.1f%%) -- "
|
||||
.."buffer mode blanks the graphics layer, so that is the "
|
||||
.."share of the scene the display spent DARK under MAME",
|
||||
open, #r20seen, #r20seen > 0 and open*100/#r20seen or 0))
|
||||
if #arrive >= 2 then
|
||||
local dts, first, last = {}, arrive[1].t, arrive[#arrive].t
|
||||
for i = 2, #arrive do dts[#dts+1] = arrive[i].t - arrive[i-1].t end
|
||||
table.sort(dts)
|
||||
-- CADENCE, AND THE GRANULARITY IT IS MEASURED AT. PG_SHOWN is sampled
|
||||
-- once per host frame, so a single inter-frame figure is quantised to
|
||||
-- 1/56.69 s = 17.6 ms and the min/median/max below are multiples of it.
|
||||
-- The MEAN over the whole run is not: the quantisation error is bounded
|
||||
-- by one host frame at each END, so over n-1 intervals it is 35 ms/(n-1)
|
||||
-- -- 0.30 ms a frame over 120. Read the mean; the spread is the
|
||||
-- sampler's, not the player's.
|
||||
local mean = (last-first)/(#arrive-1)
|
||||
P(string.format("cadence: %d frames in %.3f s = %.3f fps, mean "
|
||||
.."%.2f ms/frame (+/- %.2f ms, the sampler's); "
|
||||
.."inter-frame min %.1f median %.1f max %.1f ms",
|
||||
#arrive, last-first, (#arrive-1)/(last-first),
|
||||
mean*1000, 35.3/(#arrive-1),
|
||||
dts[1]*1000, dts[math.ceil(#dts/2)]*1000,
|
||||
dts[#dts]*1000))
|
||||
-- THE TRANSPORT'S TIME IS ONLY READABLE OFF A FREE-RUNNING RUN.
|
||||
-- Paced, the mean inter-frame IS THE PACE PERIOD: the player waits for
|
||||
-- its tick and the record's delivery hides inside the slot, so dividing
|
||||
-- the record by that mean reports the clock rather than the transport
|
||||
-- and reports it as a rate. The first cut of this script printed
|
||||
-- "297.4 KB/s" off a 6 fps gate run, which is the pace and not the
|
||||
-- disc. Free-running there is no gate and the loop is transfer-bound,
|
||||
-- so the mean is the transfer.
|
||||
if not PACED then
|
||||
P(string.format(" -> FREE-RUNNING, so the mean IS the "
|
||||
.."transport: a %d B record lands in %.2f ms, i.e. "
|
||||
.."%.1f KB/s and %.1f%% of a %d fps slot. MAME's "
|
||||
.."device models carry no transfer timing "
|
||||
.."(docs/BENCHMARK.md, 42.5), so this is a property "
|
||||
.."of the APPARATUS -- it is not W and it is not a "
|
||||
.."measurement of any medium.",
|
||||
META.rec_bytes, mean*1000,
|
||||
META.rec_bytes/mean/1024,
|
||||
mean*META.fps*100, META.fps))
|
||||
else
|
||||
-- What a paced run CAN say about the transfer, and it says it from
|
||||
-- the display rather than from the clock: the window is open for
|
||||
-- exactly as long as the record takes, so the open fraction times the
|
||||
-- slot is the transfer time, sampled at the host's frame rate.
|
||||
P(string.format(" -> PACED, so this mean is the PACE and "
|
||||
.."NOT the transport. What the run does bound is the "
|
||||
.."transfer: the window was open %.1f%% of a "
|
||||
.."%.2f ms slot = %.1f ms a record.",
|
||||
open*100/#r20seen, mean*1000,
|
||||
open/#r20seen*mean*1000))
|
||||
end
|
||||
end
|
||||
if CSV then
|
||||
local f = io.open(CSV, "w")
|
||||
f:write("frame,t_s\n")
|
||||
for _,a in ipairs(arrive) do f:write(string.format("%d,%.6f\n", a.n-1, a.t)) end
|
||||
f:close()
|
||||
P("arrivals -> "..CSV)
|
||||
end
|
||||
do
|
||||
local f = assert(io.open("packed_snaps.csv", "w"))
|
||||
f:write("snapshot,frame\n")
|
||||
for i, fr in ipairs(snapfr) do
|
||||
f:write(string.format("%04d,%d\n", i-1, fr))
|
||||
end
|
||||
f:close()
|
||||
P(string.format("%d snapshots -> tmp/packed_snaps.csv", #snapfr))
|
||||
end
|
||||
P("done")
|
||||
M:exit()
|
||||
end
|
||||
end)
|
||||
if not ok then print("[PK] LUA ERROR: "..tostring(err)); M:exit() end
|
||||
end)
|
||||
Executable
+158
@@ -0,0 +1,158 @@
|
||||
#!/bin/bash
|
||||
# THE PACKED PLAYER, END TO END, OFF A REAL VOLUME. ROADMAP K3.
|
||||
#
|
||||
# tools/bench/packed_run.sh [container.dlxp]
|
||||
#
|
||||
# Four runs of src/player/packed.s, and each answers a different question. They
|
||||
# are separate runs because the questions interfere: the write window has to be
|
||||
# OPEN for the whole transfer and buffer mode blanks the graphics layer, so at
|
||||
# the container's own 12 fps there is no instant at which a complete frame is
|
||||
# displayable and the pixel gate has nothing to sample. Pacing slower opens a
|
||||
# display interval without changing one byte of the transfer. Reporting the
|
||||
# rate off the gate run instead would have been the flattering shortcut, and it
|
||||
# reports the PACE rather than the disc -- the first cut of packed.lua did
|
||||
# exactly that and printed 297 KB/s off a 6 fps run.
|
||||
#
|
||||
# 1. GATE, stealing, paced at half rate: 120 records, 120 snapshots, every one
|
||||
# compared. A packed frame is a LITERAL, so unlike the codec's gate the last
|
||||
# frame audits nothing and all 120 have to be checked (verify_packed.py).
|
||||
# 2. RATE, stealing, FREE-RUNNING: the loop is transfer-bound, so the mean
|
||||
# inter-frame IS the emulated transport's time for a record.
|
||||
# 3. RATE, held, FREE-RUNNING: the same, with the bus held.
|
||||
# 4. CLOCK, held, paced at the container's fps: what holding the bus does to a
|
||||
# frame clock built on counting V-DISP interrupts.
|
||||
#
|
||||
# DLX_PK_GATE_ONLY=1 runs 1 alone. That is what tools/bench/check.sh takes: the
|
||||
# green light's job is to catch a regression in the PLAYER, and runs 2-4 measure
|
||||
# the apparatus rather than gate it -- three more MAME jobs for numbers that
|
||||
# cannot change unless MAME does.
|
||||
#
|
||||
# THE APPARATUS is tools/bench/dma_run.sh's -- `x68000 -exp1 cz6bs1` and a
|
||||
# zero-filled scsiexrom.bin on a private rompath -- and the volume is
|
||||
# tools/bench/mkvol.sh's, which for a DLXP container is the container itself.
|
||||
#
|
||||
# WHAT NO RUN HERE MEASURES: `W`, and any rate a real medium would deliver.
|
||||
# MAME's device models carry no transfer timing (docs/BENCHMARK.md, 42.5). What
|
||||
# is measured is the SHAPE -- one channel start, 193 destinations, 120 times,
|
||||
# on a clock the machine keeps itself, with every frame pixel-exact.
|
||||
set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
DLXP=${1:-tmp/packed_singe.dlxp}
|
||||
NFR=${DLX_PK_NFR:-120}
|
||||
GATE_FPS=${DLX_PK_GATE_FPS:-6}
|
||||
|
||||
bash tools/bench/mkvol.sh "$DLXP"
|
||||
python3 tools/bench/prep_packed.py "$DLXP"
|
||||
# WHICH ORDER THIS CONTAINER USES, read out of the container rather than
|
||||
# assumed. FINDINGS 62.5/63.4 priced palette-first and palette-last at -12.8 dB
|
||||
# for one paint apiece and could not choose between them, so the format records
|
||||
# it (dlxp.py flags bit 1) and BOTH have to pass this gate. The chain assertion
|
||||
# below is the only place the difference is visible from outside the machine,
|
||||
# and hard-coding either order there would turn "K3 ran both" into "K3 ran one
|
||||
# and the other could not have failed".
|
||||
PALLAST=$(sed -n 's/.*palette_last = \([01]\),.*/\1/p' tmp/packed_meta.lua)
|
||||
if [ "$PALLAST" = "1" ]; then
|
||||
CHAIN0='chain\[0\] = MAR \$C08000 MTC 256'
|
||||
ORDER="palette LAST -- the 193rd entry"
|
||||
else
|
||||
CHAIN0='chain\[0\] = MAR \$E82000 MTC 512 ; chain\[1\] = MAR \$C08000 MTC 256'
|
||||
ORDER="palette FIRST -- entry 0, then 192 rows"
|
||||
fi
|
||||
echo " container order: $ORDER"
|
||||
tools/vasm/vasmm68k_mot -Fbin -o tmp/packed.bin src/player/packed.s > /dev/null
|
||||
|
||||
# One run. $1 names the log, the rest are environment.
|
||||
run() {
|
||||
local tag=$1; shift
|
||||
rm -rf "tmp/snap_packed_$tag"; mkdir -p "tmp/snap_packed_$tag"
|
||||
# stdbuf -oL: without it a long MAME run is unobservable until it exits, and a
|
||||
# run that is merely finishing looks exactly like one that is wedged (34.1).
|
||||
( cd tmp && env SDL_VIDEODRIVER=dummy "$@" stdbuf -oL timeout -k 5 900 \
|
||||
mame x68000 -bios ipl10 -exp1 cz6bs1 \
|
||||
-rompath "$HOME/mame/roms;./p4roms" -hard dlxpdisk.chd \
|
||||
-ramsize 2M -video soft -window -sound none -nothrottle -plugins \
|
||||
-autoboot_script ../tools/bench/packed.lua \
|
||||
-snapshot_directory "./snap_packed_$tag" -snapview native \
|
||||
-seconds_to_run "$SECS" > "packed_$tag.log" 2>&1 )
|
||||
grep -aq "^\[PK\] done" "tmp/packed_$tag.log" || {
|
||||
echo "FAIL: the $tag run did not finish -- no completion marker."
|
||||
tail -12 "tmp/packed_$tag.log"; exit 1; }
|
||||
grep -a "^\[PK\]" "tmp/packed_$tag.log" | sed 's/^\[PK\] / /'
|
||||
}
|
||||
fail() { echo "FAIL: $1"; exit 1; }
|
||||
|
||||
echo "--- 1. THE GATE: $NFR records, paced at $GATE_FPS fps, channel stealing ---"
|
||||
SECS=$(( NFR / GATE_FPS + 25 ))
|
||||
run gate DLX_PK_HELD=0 DLX_PK_PACE=1 DLX_PK_FPS=$GATE_FPS DLX_PK_NFR=$NFR
|
||||
cp tmp/packed_snaps.csv tmp/packed_snaps_gate.csv
|
||||
|
||||
# THE ASSERTIONS. Printing a result and gating on it are different things.
|
||||
grep -aq "^\[PK\] FLAG=\$FF" tmp/packed_gate.log || \
|
||||
fail "the player did not reach the end of the scene. FLAG=\$E1 is a CRTC mode
|
||||
the frame clock cannot divide, \$E2 is a transport failure -- and the
|
||||
TRANSPORT FAILED line above names which."
|
||||
grep -aq "array: the 68000 built 193 entries (the container wants 193)" \
|
||||
tmp/packed_gate.log || \
|
||||
fail "the 68000 built a chain of the wrong length. One entry short delivers a
|
||||
picture with its last row missing, which looks like a decode bug and is a
|
||||
layout bug; the container's geometry and the player's arithmetic are two
|
||||
independent statements of one number and they have to agree."
|
||||
grep -aq "$CHAIN0" tmp/packed_gate.log || \
|
||||
fail "the chain does not have the shape this container asks for ($ORDER).
|
||||
The crossing from the palette registers into GVRAM IS the packed frame
|
||||
(FINDINGS 62) -- a palette entry and 192 row entries, one start, the CPU
|
||||
halted throughout -- and an array built the other way round from the
|
||||
record feeding it does not fail: it paints 192 rows of picture into the
|
||||
palette registers and 512 B of palette across the top of the screen."
|
||||
grep -aq "late frames (tick already past at the gate): 0$" tmp/packed_gate.log || \
|
||||
fail "a frame missed its slot in the GATE run, which is paced at half rate on
|
||||
purpose. That is not a rate result -- it means the transfer did not fit in
|
||||
a slot twice as long as the container's, and the pixel comparison below is
|
||||
then sampling frames the player was still overwriting."
|
||||
|
||||
grep -aq "0 frames NOT SAMPLED" tmp/packed_gate.log || \
|
||||
fail "the gate run could not sample every frame: the write window reopened
|
||||
before the snapshot on at least one. At half the container's rate the
|
||||
transfer must fit inside the display interval with room to spare, and if
|
||||
it does not the comparison below is checking frames the player was still
|
||||
overwriting."
|
||||
|
||||
python3 tools/bench/verify_packed.py "$DLXP" --snap tmp/snap_packed_gate \
|
||||
--map tmp/packed_snaps_gate.csv --min-frames "$NFR"
|
||||
|
||||
if [ "${DLX_PK_GATE_ONLY:-0}" = "1" ]; then exit 0; fi
|
||||
|
||||
echo
|
||||
echo "--- 2/3. THE RATE: free-running, both channel configurations ---"
|
||||
SECS=$(( NFR / 8 + 25 ))
|
||||
run free_steal DLX_PK_HELD=0 DLX_PK_PACE=0 DLX_PK_NFR=$NFR
|
||||
run free_held DLX_PK_HELD=1 DLX_PK_PACE=0 DLX_PK_NFR=$NFR
|
||||
for t in free_steal free_held; do
|
||||
grep -aq "FREE-RUNNING, so the mean IS the transport" "tmp/packed_$t.log" || \
|
||||
fail "the $t run did not report a transport time, so there is no rate here
|
||||
to read -- and a rate read off a PACED run is the pace."
|
||||
done
|
||||
|
||||
echo
|
||||
echo "--- 4. THE CLOCK: held, paced at the container's own rate ---"
|
||||
SECS=$(( NFR / 12 + 25 ))
|
||||
run held_paced DLX_PK_HELD=1 DLX_PK_PACE=1 DLX_PK_NFR=$NFR
|
||||
# THE FINDING THIS RUN EXISTS FOR, asserted rather than admired. A held channel
|
||||
# halts the 68000, and the frame clock is an INTERRUPT off V-DISP whose pending
|
||||
# bit is ONE BIT -- so every edge that falls inside a transfer spanning two of
|
||||
# them is an edge the machine can never count. If this ever comes back at zero,
|
||||
# either the transfer got short enough to fit between two rasters or the held
|
||||
# configuration stopped halting the CPU, and both change what the run means.
|
||||
LOST=$(sed -n 's/.*-> \([0-9]*\) edges LOST.*/\1/p' tmp/packed_held_paced.log | head -1)
|
||||
[ -n "$LOST" ] && [ "$LOST" -gt 0 ] || \
|
||||
fail "the held run lost no V-DISP edges (${LOST:-none}). Either the bus is no
|
||||
longer being held for the transfer, or the transfer now fits between two
|
||||
rasters -- and the comparison with the stealing run below is then a
|
||||
comparison of two configurations that do the same thing."
|
||||
LOSTS=$(sed -n 's/.*-> \([0-9]*\) edges LOST.*/\1/p' tmp/packed_gate.log | head -1)
|
||||
echo
|
||||
echo " V-DISP edges lost: $LOST held at 12 fps, $LOSTS stealing at $GATE_FPS fps."
|
||||
echo " A player keeps a clock, reads a stick and feeds ADPCM. Which of the two"
|
||||
echo " configurations can do any of that is a DESIGN question, and it is the"
|
||||
echo " one this run answers; neither figure is W."
|
||||
exit 0
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Everything tools/bench/packed.lua needs to know about a DLXP container.
|
||||
|
||||
python3 tools/bench/prep_packed.py <in.dlxp> -> tmp/packed_meta.lua
|
||||
|
||||
THERE IS NO BLOB TO PREPARE, and that is the whole difference from
|
||||
`prep_stream.py`. The codec's rig has to hand the machine expanded codebooks, a
|
||||
packed palette and a record index, because a DLX record cannot be found or drawn
|
||||
without them; `prep_dlx.py` and `prep_stream.py` exist for that and FINDINGS
|
||||
49.7.5 records what it cost to have two copies of one of those transforms. A
|
||||
packed container carries no such thing: record `i` is at sector 1 + i*97 by
|
||||
geometry and its bytes are already in the order GVRAM wants them (dlxp.py). So
|
||||
this file emits METADATA ONLY -- six numbers the rig would otherwise have to
|
||||
hard-code, every one of them read out of the container's own header.
|
||||
|
||||
The volume is the container itself; tools/bench/mkvol.sh copies it.
|
||||
"""
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "encoder"))
|
||||
from dlxp import DLXP, SECTOR
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit(__doc__)
|
||||
d = DLXP(sys.argv[1])
|
||||
|
||||
# The record's sector count, and the array chain's entry count, DERIVED here and
|
||||
# asserted by the 68000 (PG_ARRN). Two independent statements of one geometry
|
||||
# is the only way a container and a player can be caught disagreeing about it --
|
||||
# a chain one entry short delivers a picture with its last row missing, which
|
||||
# looks like a decode bug and is a layout bug.
|
||||
recs = d.rec_bytes // SECTOR
|
||||
rows = d.H
|
||||
entries = rows + (1 if d.has_palette else 0)
|
||||
|
||||
out = "tmp/packed_meta.lua"
|
||||
with open(out, "w") as fh:
|
||||
fh.write("-- generated by tools/bench/prep_packed.py; do not edit\n")
|
||||
fh.write("return {\n")
|
||||
for k, v in [("W", d.W), ("H", d.H), ("fps", d.fps), ("nframes", d.nframes),
|
||||
("rec_bytes", d.rec_bytes), ("rec_sectors", recs),
|
||||
("pal_bytes", d.pal_bytes), ("pic_bytes", d.pic_bytes),
|
||||
("lba0", d.off_frm // SECTOR),
|
||||
("palette_last", int(d.palette_last)),
|
||||
("has_palette", int(d.has_palette)),
|
||||
("entries", entries)]:
|
||||
fh.write(f" {k} = {v},\n")
|
||||
fh.write("}\n")
|
||||
|
||||
print(f"{sys.argv[1]}: DLXP1 {d.W}x{d.H} {d.fps}fps {d.nframes} frames")
|
||||
print(f" record {d.rec_bytes:,} B = {recs} sectors, palette "
|
||||
f"{'LAST' if d.palette_last else 'FIRST'}, {d.pal_bytes} B")
|
||||
print(f" record i is at LBA {d.off_frm // SECTOR} + i*{recs} -- ARITHMETIC. "
|
||||
f"There is no index in this container and none can be needed.")
|
||||
print(f" the chain the 68000 must build: {entries} entries "
|
||||
f"({rows} rows{' + 1 palette' if d.has_palette else ''})")
|
||||
print(f" wire {d.kbps():.1f} KB/s, FIXED by geometry -> {out}")
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Is EVERY frame the packed player put on screen pixel-exact? ROADMAP K3.
|
||||
|
||||
python3 tools/bench/verify_packed.py <in.dlxp> [--snap tmp/snap_packed]
|
||||
[--map tmp/packed_snaps.csv]
|
||||
[--min-frames N]
|
||||
|
||||
WHY THIS CHECKS ALL OF THEM AND tools/bench/verify_decode.py CHECKS ONE. The
|
||||
codec is temporally recursive: a SKIP block is a claim that the previous frame is
|
||||
still in GVRAM, so the last frame of a sequential run is only correct if every
|
||||
frame before it was, and one comparison audits 120. A packed frame is a
|
||||
LITERAL -- 192 rows of picture and a whole new palette, written over whatever
|
||||
was there. Frame 119 being right says nothing at all about frame 60. The
|
||||
simplification that deleted the ring, the codebooks and the decoder also deleted
|
||||
the gate's free lunch, and this is the bill.
|
||||
|
||||
WHAT IS COMPARED. MAME's own screen, through MAME's own video code: the
|
||||
snapshot is what the display produced out of GVRAM and the palette REGISTERS.
|
||||
Nothing here re-implements the packed interleave -- that is deliberate and it is
|
||||
the same rule tools/bench/gvpack/verify_dlxp.py was built on, because a
|
||||
container round-trips against its own inverse whether or not its byte order is
|
||||
the one the hardware wants. The reference is dlxp.render(i), which is the
|
||||
palette in the record applied to the indices in the record.
|
||||
|
||||
THE LETTERBOX IS CHECKED TOO, and it is not padding. The picture is 192 rows of
|
||||
a 256-row screen; the other 64 rows are STATIC SETUP the 68000 wrote once at
|
||||
scene start (packed.s pg_static) and the channel never touches again. If they
|
||||
were wrong -- or if they decayed as the per-frame palette moved under them --
|
||||
the picture would still be pixel-exact and the screen would not be. Index 255
|
||||
is black in every frame's palette by construction (vq.frame_palette), so this
|
||||
also gates that reservation across all 120 records.
|
||||
"""
|
||||
import argparse, csv, os, sys
|
||||
sys.path.insert(0, "tools/encoder")
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from dlxp import DLXP
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("container")
|
||||
ap.add_argument("--snap", default="tmp/snap_packed")
|
||||
ap.add_argument("--map", default="tmp/packed_snaps.csv")
|
||||
ap.add_argument("--min-frames", type=int, default=1,
|
||||
help="fail if fewer than this many frames were sampled -- a "
|
||||
"run that displayed nothing must not pass as a run with "
|
||||
"no mismatches in it")
|
||||
a = ap.parse_args()
|
||||
|
||||
d = DLXP(a.container)
|
||||
if not d.has_palette:
|
||||
# A --no-palette container leaves the palette registers holding whatever the
|
||||
# scene setup put there, and this rig's player writes none -- so there is no
|
||||
# reference for what the screen should show. Say so rather than compare
|
||||
# against an assumption.
|
||||
sys.exit(f"{a.container} carries no palette; this gate has no reference "
|
||||
f"for what the display should have produced.")
|
||||
|
||||
with open(a.map) as fh:
|
||||
pairs = [(r["snapshot"], int(r["frame"])) for r in csv.DictReader(fh)]
|
||||
if len(pairs) < a.min_frames:
|
||||
print(f"FAIL 0. only {len(pairs)} frames were sampled, --min-frames is "
|
||||
f"{a.min_frames}. A player whose write window never closed displays "
|
||||
f"nothing, and an empty comparison is not a pass.")
|
||||
sys.exit(1)
|
||||
|
||||
SCRH, SCRW = 256, 256
|
||||
YOFF = (SCRH - d.H) // 2
|
||||
fails, checked = [], 0
|
||||
for name, fr in pairs:
|
||||
path = f"{a.snap}/x68000/{name}.png"
|
||||
if not os.path.exists(path):
|
||||
fails.append(f"snapshot {name} (frame {fr}) is missing from {a.snap}")
|
||||
continue
|
||||
s = np.asarray(Image.open(path).convert("RGB")).astype(int)
|
||||
if s.shape[:2] != (2 * SCRH, SCRW):
|
||||
fails.append(f"frame {fr}: geometry {s.shape[1]}x{s.shape[0]}, "
|
||||
f"expected {SCRW}x{2*SCRH}")
|
||||
continue
|
||||
if not all(np.array_equal(s[i], s[i + 1]) for i in range(1, s.shape[0] - 1, 2)):
|
||||
fails.append(f"frame {fr}: double-scan pairing (1,2),(3,4),... broken")
|
||||
continue
|
||||
g = s[0::2]
|
||||
pal = d.palette_rgb(fr)
|
||||
exp = np.empty((SCRH, SCRW, 3), int)
|
||||
exp[:] = pal[255] # the letterbox, and the reservation
|
||||
exp[YOFF:YOFF + d.H] = d.render(fr)
|
||||
checked += 1
|
||||
if np.array_equal(g, exp):
|
||||
continue
|
||||
bad = (g != exp).any(2)
|
||||
by, bx = np.where(bad)
|
||||
inpic = ((by >= YOFF) & (by < YOFF + d.H)).sum()
|
||||
fails.append(f"frame {fr} (snapshot {name}): {bad.sum()} px differ "
|
||||
f"({inpic} in the picture, {bad.sum()-inpic} in the "
|
||||
f"letterbox), first at y={by[0]} x={bx[0]}, maxdiff "
|
||||
f"{abs(g-exp).max()}")
|
||||
|
||||
for f in fails[:12]:
|
||||
print("FAIL " + f)
|
||||
if len(fails) > 12:
|
||||
print(f"FAIL ... and {len(fails)-12} more")
|
||||
if fails:
|
||||
print(f" {checked-len([f for f in fails])} of {len(pairs)} sampled "
|
||||
f"frames compared clean")
|
||||
sys.exit(1)
|
||||
lo, hi = min(f for _, f in pairs), max(f for _, f in pairs)
|
||||
print(f"OK {checked} frames of {a.container} pixel-exact on the emulated "
|
||||
f"68000, frames {lo}..{hi} of {d.nframes}")
|
||||
print(f" every one of them a LITERAL: no decoder, no codebook, no ring. "
|
||||
f"Screen {SCRW}x{SCRH}, picture {d.W}x{d.H} at y={YOFF}, letterbox on "
|
||||
f"the reserved index 255.")
|
||||
print(f" palette {'LAST' if d.palette_last else 'FIRST'} in the record, "
|
||||
f"{d.pal_bytes} B, compared as the DISPLAY renders it (GRB555+I out of "
|
||||
f"the palette registers)")
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The README still for the PACKED player -- ROADMAP K3, FINDINGS 64.
|
||||
|
||||
python3 tools/media/make_packed_media.py [container.dlxp]
|
||||
[--snap tmp/snap_packed_gate] [--map tmp/packed_snaps_gate.csv]
|
||||
[--src tmp/fr_singe] [--frame N] [--out docs/img/packed-player.png]
|
||||
|
||||
Blu-ray source | what the emulated 68000 actually put on screen. The right-hand
|
||||
panel is MAME's own snapshot, de-double-scanned and cropped to the picture -- not
|
||||
a re-render, not `dlxp.render`. It is the same rule the codec's still is built on
|
||||
(tools/media/make_readme_media.py) and it is the only reason the picture is worth
|
||||
printing: an encoder can be checked against its own inverse, and a screen cannot.
|
||||
|
||||
THE FRAME IS CHOSEN, NOT PICKED. --frame defaults to the one whose PSNR against
|
||||
the source is CLOSEST TO THE MEAN over the whole gated window, so the still is
|
||||
representative rather than flattering. The chosen frame and its distance from the
|
||||
mean are printed, so a reader can see it was not the best one.
|
||||
"""
|
||||
import argparse, csv, os, sys
|
||||
sys.path.insert(0, "tools/encoder")
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
from dlxp import DLXP
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("container", nargs="?", default="tmp/packed_singe.dlxp")
|
||||
ap.add_argument("--snap", default="tmp/snap_packed_gate")
|
||||
ap.add_argument("--map", default="tmp/packed_snaps_gate.csv")
|
||||
ap.add_argument("--src", default="tmp/fr_singe")
|
||||
ap.add_argument("--frame", type=int, default=None)
|
||||
ap.add_argument("--out", default="docs/img/packed-player.png")
|
||||
a = ap.parse_args()
|
||||
|
||||
d = DLXP(a.container)
|
||||
SNAP_W, SNAP_H = 256, 512
|
||||
with open(a.map) as fh:
|
||||
shot = {int(r["frame"]): r["snapshot"] for r in csv.DictReader(fh)}
|
||||
if not shot:
|
||||
sys.exit(f"{a.map} is empty -- run tools/bench/packed_run.sh first")
|
||||
|
||||
|
||||
def screen(fr):
|
||||
"""The 256x192 picture out of one MAME native snapshot."""
|
||||
p = f"{a.snap}/x68000/{shot[fr]}.png"
|
||||
s = np.asarray(Image.open(p).convert("RGB"))
|
||||
if s.shape[:2] != (SNAP_H, SNAP_W):
|
||||
sys.exit(f"{p}: expected {SNAP_W}x{SNAP_H}, got {s.shape[1]}x{s.shape[0]}")
|
||||
g = s[0::2] # undo the double scan
|
||||
y = (g.shape[0] - d.H) // 2 # the picture is centred
|
||||
return g[y:y + d.H]
|
||||
|
||||
|
||||
def source(fr):
|
||||
p = f"{a.src}/f{fr+1:04d}.png"
|
||||
if not os.path.exists(p):
|
||||
sys.exit(f"missing {p} -- re-extract the frames the container was built "
|
||||
f"from, or point --src at them")
|
||||
return np.asarray(Image.open(p).convert("RGB"))
|
||||
|
||||
|
||||
def psnr(x, y):
|
||||
e = ((x.astype(float) - y.astype(float)) ** 2).mean()
|
||||
return float("inf") if e == 0 else 10 * np.log10(255.0 ** 2 / e)
|
||||
|
||||
|
||||
frames = sorted(shot)
|
||||
scores = {f: psnr(source(f), screen(f)) for f in frames}
|
||||
mean = float(np.mean(list(scores.values())))
|
||||
if a.frame is None:
|
||||
pick = min(scores, key=lambda f: abs(scores[f] - mean))
|
||||
else:
|
||||
pick = a.frame
|
||||
if pick not in scores:
|
||||
sys.exit(f"frame {pick} was not sampled by that run")
|
||||
|
||||
# THE PANEL IS GATED, not just drawn. A still of the player is a claim that the
|
||||
# player drew it, and the snapshot has to still be pixel-exact against the
|
||||
# container for that claim to hold -- verify_packed.py checks all of them and
|
||||
# this checks the one being printed, so the picture cannot outlive the result.
|
||||
ref = d.render(pick)
|
||||
if not np.array_equal(screen(pick), ref):
|
||||
sys.exit(f"frame {pick} is NOT pixel-exact against {a.container}. The still "
|
||||
f"is not being written: it would be a picture of a failure with a "
|
||||
f"caption saying otherwise.")
|
||||
|
||||
Z, BAR = 2, 22
|
||||
|
||||
|
||||
def captioned(img, text):
|
||||
up = np.repeat(np.repeat(img, Z, 0), Z, 1)
|
||||
out = Image.new("RGB", (up.shape[1], up.shape[0] + BAR), (16, 16, 18))
|
||||
out.paste(Image.fromarray(up), (0, BAR))
|
||||
ImageDraw.Draw(out).text((6, 6), text, fill=(190, 190, 196))
|
||||
return out
|
||||
|
||||
|
||||
left = captioned(source(pick), "Blu-ray source, cropped 256x192")
|
||||
right = captioned(screen(pick),
|
||||
"emulated 68000, MAME's own snapshot, no decoder")
|
||||
out = Image.new("RGB", (left.width + right.width + 8, left.height), (16, 16, 18))
|
||||
out.paste(left, (0, 0))
|
||||
out.paste(right, (left.width + 8, 0))
|
||||
os.makedirs(os.path.dirname(a.out), exist_ok=True)
|
||||
out.save(a.out)
|
||||
print(f"{a.out}: frame {pick} of {d.nframes}, {scores[pick]:.2f} dB against the "
|
||||
f"24-bit source")
|
||||
print(f" chosen as the frame CLOSEST TO THE MEAN ({mean:.2f} dB over "
|
||||
f"{len(frames)} gated frames), {abs(scores[pick]-mean):.3f} dB from it -- "
|
||||
f"best in the window is {max(scores.values()):.2f}, worst "
|
||||
f"{min(scores.values()):.2f}")
|
||||
print(f" and it is pixel-exact against {a.container}, checked before writing")
|
||||
Reference in New Issue
Block a user