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.""")
|
||||
Reference in New Issue
Block a user