ROADMAP K1, the packed player's one open structural item. A frame is a picture
AND a palette, and no run in this tree had pointed a DMA channel at the palette
registers. dmagate.s runs 7-9, gated by dma_run.sh and check.sh:
7. 512 B off the disc into $E82000, bus held -- byte-exact in 256 register
words, read back OUT OF the registers by the 68000;
8. the SAME transfer aimed at RAM -- byte-exact at $2C000, and 256 of 256
palette words still read the poison the CPU wrote, which is what attributes
run 7 to the channel's MAR rather than to the readback path;
9. ONE array-chained start across two kinds of destination -- the palette and
six picture rows at the 1,024 B line stride, 2,048 B byte-exact.
So a packed frame is one channel start: a 193-entry array, palette first, CPU
halted from the first byte to the last. The array is scene-constant, because
the packed layout spends both 256-colour pages and there is no page to flip.
What is left on the CPU per frame in the video path is the channel start and the
READ(10) -- no per-frame PAINT, which is not the same claim as no per-frame CPU.
The destination is POISONED first (62.1). Runs 4-6 wrote into RAM that was zero
and GVRAM that was stale against a record that is mostly pad; "it matches the
disc" was weaker than it read as. The host counts whether the poison actually
discriminates instead of assuming it: 511 of 512, and the gate refuses under 500.
And it opened a hardware item (62.4, ROADMAP B4). MAME maps the palette to
palette_device over memory_array, whose write16 is a plain COMBINE_DATA -- RAM
that honours mem_mask, with no handler that could refuse a byte write. Unlike
GVRAM's 256-colour arm there is nothing here to be wrong about, so the run
bounds the model and not the board. What a real palette register does with a
byte write is unmeasured. A negative costs 0.28% of a frame and nothing else.
29_packed_player.py now also prints the two rows with the per-frame palette
charged -- 55.7% of a frame on the chain, 582 KB/s -- alongside the picture-only
figures the codec comparison is quoted against.
check.sh ALL GREEN before (tmp/check_s30_start.log) and after
(tmp/check_s30_end.log).
Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
406 lines
23 KiB
Python
406 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
"""The DECODER-FREE PACKED player, priced against the measured cost model.
|
|
|
|
python3 tools/analysis/29_packed_player.py [container.dlx] [--kbps R]
|
|
|
|
THE QUESTION, and why it is being asked again. FINDINGS 44.7 removed the codec
|
|
and asked what a player that just puts literal frames on screen would cost. It
|
|
answered "it fits the clocks and dies on the medium": 1,152 KB/s and 1.61 GB,
|
|
because 256-colour GVRAM's default write path throws away the high byte of every
|
|
word and a picture byte therefore costs two disc bytes. 46.5/47.1 then found
|
|
the off switch -- CRTC R20 bit 11 -- and 47.2 built the layout and rendered it
|
|
pixel-exactly on both emulators at 1.0 B/pixel. 47.5 re-derived the budget on
|
|
that and withdrew 44.7's conclusion CONDITIONALLY.
|
|
|
|
Everything in 47.5 is arithmetic over a cost model that has since been REPLACED.
|
|
When it was written the transport was an unmeasured `c`; sessions 25b-28 put the
|
|
transport on the 68000 and measured it (58.2: PIO is 87.28 clk/B), put it on the
|
|
DMAC and bounded it (59.2: this machine can run dual-address only, and a
|
|
dual-address byte has a 9 clk/B FLOOR), and re-derived what a frame can afford
|
|
(59.7/60.7: 6.69 clk/B on the gate container). 47.6.1 also filed the CPU paint
|
|
cost as an ASSUMPTION -- "the `movem` shape of the packed writes is an
|
|
assumption", no clock in 47.5 measured.
|
|
|
|
So this tool re-asks 44.7's question with:
|
|
|
|
* the paint MEASURED, not assumed -- tools/bench/blit.s V8 is V1 with 128
|
|
words a row instead of 256, and tools/bench/blit.lua times it next to V1,
|
|
V2 and V3 in the same run, so the packed number is quoted against a
|
|
variant whose value (53.6%) is a session-9 result that has not moved;
|
|
* the transport swept over the SAME `W` ladder 15_bus_occupancy.py uses,
|
|
every rung of it sourced or measured (buscost.py);
|
|
* the audio DMA charged, at the rate the IPL ROM's own channel-3 setup
|
|
implies (21_iplrom_dmac.py) -- 60.x's rule that a budget debits I/O;
|
|
* and the wire and the volume stated for each, because 44.7's answer was
|
|
never about clocks.
|
|
|
|
WHAT IT DOES NOT DO. It does not settle 47.4 -- whether buffer mode BLANKS the
|
|
graphics layer, which MAME asserts and px68k is silent about (48.1), and which
|
|
needs a real board. It PRICES both branches instead, and the blanking section
|
|
is where the measured paint earns its keep: the black interval is the paint, and
|
|
until now the paint was a range read off an unpacked measurement ("~27% to ~54%",
|
|
48.3) rather than a number.
|
|
"""
|
|
import sys, os, re, argparse, csv
|
|
sys.path.insert(0, "tools/encoder")
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import numpy as np
|
|
from dlx import DLX
|
|
import buscost as B
|
|
|
|
CPUHZ = 10e6 # stock X68000, MAME 0.277 x68k.cpp:1133
|
|
GAME_S = 22.8 * 60 # the full-disc survey's runtime (ROADMAP C1)
|
|
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("container", nargs="?", default="tmp/rc_fr_singe_scsi_span.dlx",
|
|
help="the CODEC baseline this is compared against")
|
|
ap.add_argument("--csv", default="tmp/c68k_frames.csv",
|
|
help="per-frame C68K measurement of that container")
|
|
ap.add_argument("--blit-log", default="tmp/blit_v8.log",
|
|
help="tools/bench/blit.lua's log -- where the MEASURED paint "
|
|
"costs are read from. No defaults are compiled in.")
|
|
ap.add_argument("--kbps", type=float, default=None,
|
|
help="a delivery rate to score the wire against. OPTIONAL and "
|
|
"there is no default (FINDINGS 50).")
|
|
a = ap.parse_args()
|
|
|
|
# --- the measured paint, read out of the run's own log ---------------------
|
|
# NOT transcribed into this file. A constant copied out of a log is a constant
|
|
# that stops tracking the log, and this project has been caught by a stale
|
|
# number twice (60.8). If the log is not there the tool refuses rather than
|
|
# substituting a plausible one.
|
|
if not os.path.exists(a.blit_log):
|
|
sys.exit(f"missing {a.blit_log} -- run tools/bench/blit.lua first:\n"
|
|
f" cd tmp && mame x68000 -bios ipl10 -ramsize 2M -video soft "
|
|
f"-window -sound none -nothrottle -plugins \\\n"
|
|
f" -autoboot_script ../tools/bench/blit.lua -seconds_to_run 60")
|
|
blit = {}
|
|
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))
|
|
for v in (1, 2, 3, 4, 8, 9, 10):
|
|
if v not in blit:
|
|
sys.exit(f"{a.blit_log} has no V{v} result -- the summary is incomplete, "
|
|
f"so the run did not finish and nothing here can be quoted.")
|
|
|
|
# --- the codec baseline: the container, and its MEASURED decode ------------
|
|
d = DLX(a.container)
|
|
FPS = d.fps
|
|
FRAME_CLK = CPUHZ / FPS
|
|
meas = {}
|
|
if os.path.exists(a.csv):
|
|
for r in csv.DictReader(open(a.csv)):
|
|
meas[int(r["frame"])] = int(r["cycles"])
|
|
if not meas:
|
|
sys.exit(f"missing {a.csv} -- the codec row's decode term is MEASURED and "
|
|
f"there is no derived stand-in for it.")
|
|
NF = max(meas) + 1
|
|
codec_decode = np.mean([meas[f] for f in range(NF)])
|
|
codec_bpf = sum(d.record_lengths()[:NF]) / NF # the PADDED record (60.7)
|
|
|
|
# --- geometry, which is where the decoder-free rows come from -------------
|
|
W_PX, H_PX = d.W, d.H
|
|
NPX = W_PX * H_PX
|
|
UNPACKED_BPF = NPX * 2 # one pixel per word, high byte discarded
|
|
PACKED_BPF = NPX * 1 # R20 bit 11 + page scroll (47.2, measured)
|
|
|
|
aud_bpf = B.ADPCM_BYTES_PER_S / FPS
|
|
AUD_CLK = aud_bpf * B.ADPCM_CLK_BYTE_BEST # best case, so every row is
|
|
# the optimistic end
|
|
# A device->GVRAM channel cannot walk a 1024-byte line stride inside one
|
|
# transfer: it writes a contiguous run. 192 rows therefore need 192 array-chain
|
|
# entries -- and SESSION 29 RAN THAT, off the disc, through src/player/dma.i's
|
|
# DM_BARV/DM_BTCV: eight rows at the 1024 B stride landed from ONE channel start
|
|
# with the CPU halted throughout (tools/bench/dma_run.sh, `[chain]`). So the
|
|
# MECHANISM is demonstrated and the CPU does not restart the channel per row.
|
|
# The COST is still datasheet arithmetic -- 36 clocks an entry, Fig 4-25 sheet 1,
|
|
# buscost.DMA_CHAIN_CLK -- because MAME's DMAC runs on wall-clock attotimes and
|
|
# cannot be asked what anything costs (42.5).
|
|
CHAIN_CLK = H_PX * B.DMA_CHAIN_CLK
|
|
|
|
print(f"""{a.container}: {NF} frames of {W_PX}x{H_PX} at {FPS:g} fps
|
|
frame slot on a 10 MHz 68000: {FRAME_CLK:,.0f} clocks
|
|
paint costs MEASURED by tools/bench/blit.lua, read from {a.blit_log}:
|
|
V1 unpacked movem blit {blit[1]:>9,} clk {100*blit[1]/FRAME_CLK:5.1f}% (96 KB read + 96 KB write)
|
|
V2 byte-source expansion {blit[2]:>9,} clk {100*blit[2]/FRAME_CLK:5.1f}% (48 KB read + 96 KB write)
|
|
V3 write-only floor {blit[3]:>9,} clk {100*blit[3]/FRAME_CLK:5.1f}% (no source read at all)
|
|
V8 PACKED movem blit {blit[8]:>9,} clk {100*blit[8]/FRAME_CLK:5.1f}% (48 KB read + 48 KB write)
|
|
|
|
V8 is {100*blit[8]/blit[1]:.1f}% of V1 and {100*blit[8]/blit[3]:.1f}% of V3 -- so PACKED PAINT COSTS WHAT THE
|
|
UNPACKED PATH PAYS TO WRITE ALONE, with its source read thrown in free.
|
|
It is not exactly half of V1 because the 192-row loop does not halve with
|
|
the words: per word V1 is {blit[1]/(NPX):.3f} clk and V8 is {blit[8]/(NPX//2):.3f}.""")
|
|
|
|
# --- the architectures ----------------------------------------------------
|
|
# Each is (label, bytes on the wire per frame, CPU clocks per frame that are
|
|
# NOT the transport, and whether the transport lands in GVRAM or in RAM).
|
|
ARCH = [
|
|
("CODEC, CPU-decoded (the shipping design)", codec_bpf, codec_decode, "ring"),
|
|
("free / DMAC device->GVRAM / unpacked", UNPACKED_BPF, CHAIN_CLK, "gvram"),
|
|
("free / DMAC device->GVRAM / PACKED", PACKED_BPF, CHAIN_CLK, "gvram"),
|
|
("free / CPU-painted / unpacked, 2 B/px wire", UNPACKED_BPF, blit[1], "ring"),
|
|
("free / CPU-painted / unpacked, 1 B/px wire", PACKED_BPF, blit[2], "ring"),
|
|
("free / CPU-painted / PACKED", PACKED_BPF, blit[8], "ring"),
|
|
]
|
|
|
|
LADDER = [
|
|
(5.0, "single address, held -- needs a request line (B3)"),
|
|
(9.0, "dual address, held -- the FLOOR (59.2/59.7)"),
|
|
(12.0, "single address, arbitrated"),
|
|
(16.0, "what the ROM programs for SASI, best"),
|
|
(19.0, "what the ROM programs for SASI, worst"),
|
|
(87.28, "PIO -- MEASURED, 58.2"),
|
|
]
|
|
|
|
print("\n" + "=" * 78)
|
|
print("WHAT EACH ARCHITECTURE COSTS A FRAME, over the transport ladder\n")
|
|
print(f" audio DMA is charged in every row at {AUD_CLK:,.0f} clk "
|
|
f"({100*AUD_CLK/FRAME_CLK:.2f}%), best case.\n")
|
|
hdr = f" {'architecture':<44}{'B/frame':>9}" + "".join(f"{f'W={w:g}':>9}" for w, _ in LADDER)
|
|
print(hdr)
|
|
print(" " + "-" * (len(hdr) - 2))
|
|
for label, bpf, cpu, dest in ARCH:
|
|
cells = []
|
|
for w, _ in LADDER:
|
|
tot = bpf * w + cpu + AUD_CLK
|
|
pct = 100 * tot / FRAME_CLK
|
|
cells.append(f"{pct:>8.1f}%" if pct < 1000 else f"{pct:>8.0f}%")
|
|
print(f" {label:<44}{bpf:>9,.0f}" + "".join(cells))
|
|
print(f"""
|
|
100% is the frame deadline. Every cell is CPU work plus transport plus
|
|
best-case audio; none of them overlap, because the 68000 has no cache and a
|
|
two-word prefetch queue that empties at once (buscost.DMA_OVERLAPS = False).
|
|
|
|
THE TWO ROWS THAT MATTER ARE THE FLOOR COLUMN, W=9, because 59.2 found that
|
|
the only configurations this machine can be shown to run are dual-address,
|
|
and a dual-address byte is a 4-clock read of the device plus a 5-clock write
|
|
to memory. Every column left of it is a hardware fact nobody here has.""")
|
|
|
|
# --- the wire, which is what 44.7 actually died on ------------------------
|
|
print("\n" + "=" * 78)
|
|
print("THE WIRE AND THE MEDIUM -- 44.7's real objection\n")
|
|
print(f" {'architecture':<44}{'B/frame':>9}{'KB/s':>9}{'GB for 22.8 min':>18}")
|
|
print(" " + "-" * 78)
|
|
seen = set()
|
|
for label, bpf, cpu, dest in ARCH:
|
|
kbs = bpf * FPS / 1024
|
|
gb = bpf * FPS * GAME_S / 1e9
|
|
print(f" {label:<44}{bpf:>9,.0f}{kbs:>9.1f}{gb:>18.2f}")
|
|
print(f"""
|
|
The codec row is the gate container, which is deliberately the heaviest thing
|
|
the encoder emits (59.7). The default `need` recipe is 267.9 KB/s and E7's
|
|
byte target at the 9 clk/B floor is 327 KB/s (60.7).
|
|
|
|
SO THE PACKED DECODER-FREE PLAYER ASKS FOR {PACKED_BPF*FPS/1024:.0f} KB/s -- {PACKED_BPF*FPS/1024/327:.2f}x E7's target and
|
|
{PACKED_BPF*FPS/1024/(codec_bpf*FPS/1024):.2f}x the gate container -- and it asks for it AT A FIXED RATE. A codec's
|
|
bitrate is a lever; a literal frame's is geometry, and there is no scene in
|
|
the picture that costs less than another.""")
|
|
if a.kbps:
|
|
R = a.kbps * 1024
|
|
print(f"\n against a supplied {a.kbps:g} KB/s:")
|
|
for label, bpf, cpu, dest in ARCH:
|
|
need = bpf * FPS
|
|
print(f" {label:<44}{'FITS' if need <= R else 'SHORT BY '}"
|
|
f"{'' if need <= R else f'{(need-R)/1024:.0f} KB/s'}"
|
|
f" ({need/1024:.0f} KB/s wanted)")
|
|
|
|
# --- 47.4's two branches, priced -----------------------------------------
|
|
print("\n" + "=" * 78)
|
|
print("IF BUFFER MODE BLANKS THE LAYER (47.4 / 48, MAME's reading)\n")
|
|
print(""" R20 bit 11 only has to be SET across the GVRAM writes, so the black
|
|
interval is the paint and not the frame -- and which paint depends on where
|
|
the transport lands. That asymmetry has not been stated before:\n""")
|
|
print(f" {'architecture':<44}{'black interval':>16} {'set for':<14}")
|
|
print(" " + "-" * 78)
|
|
for label, bpf, cpu, dest in ARCH[1:]:
|
|
if dest == "gvram":
|
|
# the channel writes GVRAM, so the bit is set for the whole transfer
|
|
# DMA rungs only: a PIO transport is not a channel writing GVRAM, so
|
|
# 87.28 has no meaning in a device->GVRAM row.
|
|
rows = [bpf * w + CHAIN_CLK for w, _ in LADDER if w < 20]
|
|
span = f"{100*min(rows)/FRAME_CLK:.0f}%..{100*max(rows)/FRAME_CLK:.0f}%"
|
|
note = "the whole DMA"
|
|
else:
|
|
span = f"{100*cpu/FRAME_CLK:.1f}%"
|
|
note = "the blit only"
|
|
print(f" {label:<44}{span:>16} {note:<14}")
|
|
print(f"""
|
|
THE CPU-PAINTED PACKED PATH HAS THE SMALLEST BLACK WINDOW OF ANY OF THEM --
|
|
{100*blit[8]/FRAME_CLK:.1f}% -- because its transport lands in RAM, where bit 11 is irrelevant,
|
|
and only the {blit[8]:,}-clock blit needs the bit set. The DMAC-direct path,
|
|
which is cheaper in clocks at every rung of the ladder, is the one that must
|
|
hold the bit across its whole transfer. Under MAME's reading the cheap
|
|
architecture is the dark one.
|
|
|
|
Both are a strobe at the frame rate over the whole picture, and the packed
|
|
layout has no page to flip to: both 256-colour pages carry picture, which is
|
|
the entire point of it (48.3). {100*blit[8]/FRAME_CLK:.1f}% black at 12 Hz is not a tear.
|
|
|
|
IF PX68K IS RIGHT AND IT DOES NOT BLANK, every number above stands as
|
|
written. Neither emulator is authority and 48.1 is why the prior leans
|
|
MAME's way: MAME asserts the semantic twice and deliberately, px68k's
|
|
display path never reads the bit at all. That is an assertion against a
|
|
silence, not a tie, and it is settled by a board and the two-line probe in
|
|
tools/bench/probe_bit11_blank.lua.""")
|
|
|
|
# --- 47.6.4: does the CODEC survive the packed layout? --------------------
|
|
# Open since session 16 and never touched: "under the packed layout a word spans
|
|
# two columns 128 apart. Whether the existing codec survives that is untouched."
|
|
# There are exactly two ways it could, and blit.s V9 and V10 are them.
|
|
sk_blocks = sk_tot = pair_sk = pair_tot = 0
|
|
for f in range(NF):
|
|
m = d.modes(f).reshape(d.nby, d.nbx)
|
|
L, R = m[:, :d.nbx // 2], m[:, d.nbx // 2:]
|
|
sk_blocks += int((m == 0).sum()); sk_tot += m.size
|
|
pair_sk += int(((L == 0) & (R == 0)).sum()); pair_tot += L.size
|
|
paint_now = 1 - sk_blocks / sk_tot
|
|
paint_pair = 1 - pair_sk / pair_tot
|
|
|
|
print("\n" + "=" * 78)
|
|
print("CAN THE CODEC BE PACKED TOO? -- 47.6.4, open since session 16\n")
|
|
print(f""" A 4x4 block owns four bytes at STRIDE 2 under the packed layout, because
|
|
the high bytes of its four words belong to the block 128 columns away. So a
|
|
block decoder has two options and neither is free:
|
|
|
|
{'V4 block order, UNPACKED (the shipping shape)':<52}{blit[4]:>9,} clk {100*blit[4]/FRAME_CLK:5.1f}%
|
|
{'V9 block order, PACKED, 16 move.b at stride 2':<52}{blit[9]:>9,} clk {100*blit[9]/FRAME_CLK:5.1f}%
|
|
{'V10 block order, PACKED, blocks PAIRED (movem back)':<52}{blit[10]:>9,} clk {100*blit[10]/FRAME_CLK:5.1f}%
|
|
|
|
V9 IS {100*blit[9]/blit[4]-100:.0f}% DEARER THAN V4, not cheaper. Packing buys a block decoder
|
|
nothing on the wire either -- a codeword is already one byte a pixel -- so
|
|
that route buys NOTHING and costs {blit[9]-blit[4]:,} clocks a frame to buy it.
|
|
|
|
V10 halves the paint, and pays for it in the mode map. A pair skips only if
|
|
BOTH its blocks skip, and on this container:
|
|
|
|
SKIP blocks now {100*sk_blocks/sk_tot:5.1f}% painted now {100*paint_now:5.1f}%
|
|
SKIP block PAIRS {100*pair_sk/pair_tot:5.1f}% painted paired {100*paint_pair:5.1f}%
|
|
|
|
So pairing paints {paint_pair/paint_now:.2f}x as many blocks for {blit[10]/blit[4]:.2f}x the paint per block --
|
|
{100*(paint_pair/paint_now)*(blit[10]/blit[4])-100:+.0f}% on the clock, and about {100*(paint_pair/paint_now-1):+.0f}% on the BYTES, because a coded
|
|
block is bytes in the container whether its half of the pair changed or not.
|
|
E7 needs the bytes DOWN {100*(codec_bpf*FPS/1024)/327-100:.0f}%.
|
|
|
|
SO PACKING BELONGS TO THE LITERAL PLAYER AND ONLY TO IT. 47.6.4 is closed:
|
|
the packed layout is not an upgrade the existing codec can take, it is the
|
|
thing you get INSTEAD of the codec.""")
|
|
|
|
# --- the palette, which is where the literal player stops being a compromise --
|
|
# 46.3 measured these while pricing the TEXT PLANE and the 256-colour rows were
|
|
# only there for scale. They answer a question nobody put to them: a literal
|
|
# player has no codebooks, so it is not tied to a scene-wide palette the way the
|
|
# codec is (vq.scene_palette exists BECAUSE codewords are indices into it), and
|
|
# per-frame palettes become legal. Re-run 18_text_plane_16col.py to reproduce.
|
|
PSNR_SHIPPED = 29.19 # docs/STATUS.md, --spans all, c=5, 496.7 KB/s
|
|
PSNR_SCENE_256 = 31.33 # 18_text_plane_16col.py, tmp/fr_singe, 120 frames
|
|
PSNR_FRAME_256 = 34.08 # the same window, per-frame palettes
|
|
PAL_BYTES = 512 # 256 entries x 1 word
|
|
|
|
pal_bpf = PACKED_BPF + PAL_BYTES
|
|
# The palette write, DERIVED from a MEASURED per-word constant: V8 moves a word
|
|
# into GVRAM for blit[8]/(NPX//2) clocks and the palette is 256 consecutive
|
|
# words at $E82000 in the same movem shape.
|
|
pal_clk = 256 * blit[8] / (NPX // 2)
|
|
|
|
print("\n" + "=" * 78)
|
|
print("AND THE PICTURE IS BETTER, WHICH NOBODY HAD ASKED\n")
|
|
print(f""" PSNR against the 24-bit source, 18_text_plane_16col.py over the same
|
|
120-frame window the whole tree is measured on:
|
|
|
|
{'shipping container (the codec, as it ships)':<48}{PSNR_SHIPPED:6.2f} dB
|
|
{'256 colours, SCENE palette -- the codec CEILING':<48}{PSNR_SCENE_256:6.2f} dB
|
|
{'256 colours, PER-FRAME palette':<48}{PSNR_FRAME_256:6.2f} dB
|
|
|
|
THE MIDDLE ROW IS A CEILING AND NOT A RIVAL. Every codeword the codec emits
|
|
is an index INTO the scene palette, so no amount of bitrate takes it past
|
|
{PSNR_SCENE_256:.2f} dB; it spends {codec_bpf*FPS/1024:.0f} KB/s to get within {PSNR_SCENE_256-PSNR_SHIPPED:.2f} dB of it.
|
|
|
|
A LITERAL FRAME HAS NO CODEBOOKS, so the scene palette is not forced on it,
|
|
and the bottom row is what it simply IS -- {PSNR_FRAME_256-PSNR_SHIPPED:+.2f} dB on the shipping
|
|
container and {PSNR_FRAME_256-PSNR_SCENE_256:+.2f} dB past the ceiling the codec cannot cross.
|
|
|
|
WHAT THE PER-FRAME PALETTE COSTS:
|
|
on the wire {PAL_BYTES} B a frame -> {pal_bpf:,} B, {pal_bpf*FPS/1024:.1f} KB/s (+{100*PAL_BYTES/PACKED_BPF:.1f}%)
|
|
in clocks ~{pal_clk:,.0f} ({100*pal_clk/FRAME_CLK:.2f}% of a frame) if the CPU writes it, DERIVED
|
|
from V8's measured {blit[8]/(NPX//2):.3f} clk/word in the same movem shape
|
|
in colours 254, not 256: the packed layout spends index 0 on the
|
|
transparency key and puts black at 255 (47.2,
|
|
prep_frame.py --pack-transparent), against --reserve-black's
|
|
one entry. The tree has already measured a reserved entry at
|
|
0.04 dB (60.3), so this is noise against {PSNR_FRAME_256-PSNR_SHIPPED:+.2f}.
|
|
|
|
SETTLED IN SESSION 30, AND THE ANSWER IS YES (FINDINGS 62): a channel writes
|
|
the palette registers at $E82000 byte-exact, and ONE array-chained start
|
|
crosses from those registers into GVRAM -- so the palette IS a 193rd chain
|
|
entry and the clocks row above is what the CPU pays only if it does the write
|
|
itself. dmagate.s runs 7-9. What that does NOT settle is the board: MAME maps
|
|
the palette to palette_device over memory_array, whose write16 is a plain
|
|
COMBINE_DATA, so there is no handler that could refuse a byte write and the
|
|
model cannot discriminate. ROADMAP B4.
|
|
|
|
AND THE PSNR FIGURES ARE PIL's MEDIANCUT, not this project's own palette
|
|
builder (vq.scene_palette / H.build). The DIRECTION is measured and the
|
|
magnitude is about right; if the packed player gets built, re-derive the
|
|
per-frame number against the builder that will actually ship it.""")
|
|
|
|
# --- the answer ----------------------------------------------------------
|
|
w9 = 9.0
|
|
free_packed_dma = PACKED_BPF * w9 + CHAIN_CLK + AUD_CLK
|
|
free_packed_cpu = PACKED_BPF * w9 + blit[8] + AUD_CLK
|
|
# ... and the same two rows with the PER-FRAME PALETTE actually charged, which
|
|
# is what a player ships. The picture rows above are the comparison against the
|
|
# codec and are left alone so the published 55.2% / 81.6% do not drift; these
|
|
# are the shipping figures. Session 30 (FINDINGS 62) made the DMAC row's
|
|
# version legal: the palette is a 193rd chain ENTRY, so it costs 512 more
|
|
# delivered bytes and one more entry rather than 256 CPU word writes.
|
|
pal_dma = (PACKED_BPF + PAL_BYTES) * w9 + CHAIN_CLK + B.DMA_CHAIN_CLK + AUD_CLK
|
|
pal_cpu = (PACKED_BPF + PAL_BYTES) * w9 + blit[8] + pal_clk + AUD_CLK
|
|
codec_9 = codec_bpf * w9 + codec_decode + AUD_CLK
|
|
print("\n" + "=" * 78)
|
|
print(f"""THE ANSWER, AT THE ONE RUNG THIS MACHINE CAN BE SHOWN TO RUN (W=9)
|
|
|
|
CODEC, gate container {100*codec_9/FRAME_CLK:6.1f}% of the frame -- DOES NOT FIT
|
|
free / DMAC->GVRAM / PACKED {100*free_packed_dma/FRAME_CLK:6.1f}% -- FITS, with {100-100*free_packed_dma/FRAME_CLK:.0f}% to spare
|
|
free / CPU-painted / PACKED {100*free_packed_cpu/FRAME_CLK:6.1f}% -- FITS, with {100-100*free_packed_cpu/FRAME_CLK:.0f}% to spare
|
|
|
|
WITH THE PER-FRAME PALETTE CHARGED, which is what would ship:
|
|
|
|
DMAC-direct, palette on the CHAIN (62) {100*pal_dma/FRAME_CLK:6.1f}% of the frame, {(PACKED_BPF+PAL_BYTES)*FPS/1024:.0f} KB/s
|
|
CPU-painted, palette written by the CPU {100*pal_cpu/FRAME_CLK:6.1f}% of the frame, {(PACKED_BPF+PAL_BYTES)*FPS/1024:.0f} KB/s
|
|
|
|
The palette costs the same on the WIRE either way -- {PAL_BYTES} B a frame,
|
|
+{100*PAL_BYTES/PACKED_BPF:.1f}% -- and the wire is where this design is expensive. The gap
|
|
between the two rows is the PAINT, not the palette.
|
|
|
|
What session 30 bought is smaller than either and is worth stating exactly:
|
|
{pal_clk:,.0f} CPU clocks of palette writing replaced by one more chain entry at
|
|
{B.DMA_CHAIN_CLK} clocks, a net {100*(pal_clk-B.DMA_CHAIN_CLK)/FRAME_CLK:.2f}% of a frame -- plus the structural half,
|
|
which is that the video path then contains no per-frame PAINT at all. The
|
|
CPU still issues the READ(10) and starts the channel, and neither of those
|
|
is priced anywhere in this tree.
|
|
|
|
THE DECODER-FREE PACKED PLAYER FITS THE CLOCK BUDGET THAT THE CODEC MISSES.
|
|
That is not a small correction to 47.5, it is the reverse of the reason the
|
|
codec exists. 44.7 said it in advance and on a different cost model: "the
|
|
codec is not there to save CPU -- it is there to save the wire." The
|
|
measured model agrees, and now says the CPU side is not merely affordable
|
|
but strictly cheaper WITHOUT the codec: at the floor, decoding {codec_bpf:,.0f} bytes
|
|
costs {100*(codec_bpf*w9+codec_decode)/FRAME_CLK:.0f}% of a frame and NOT decoding {PACKED_BPF:,} costs {100*(PACKED_BPF*w9+blit[8])/FRAME_CLK:.0f}%.
|
|
|
|
SO THE QUESTION IS ENTIRELY A MEDIUM QUESTION, and it has two halves:
|
|
|
|
1. {PACKED_BPF*FPS/1024:.0f} KB/s SUSTAINED, with no lever to pull. ROADMAP B1 is
|
|
unmeasured; the 0.7-1.7 MB/s usually quoted for BlueSCSI on an X68000
|
|
is folklore with no published benchmark behind it. {PACKED_BPF*FPS/1024:.0f} KB/s sits
|
|
inside that range, which is exactly why the range has to be measured
|
|
rather than cited. A codec at 327 KB/s survives a slower answer; a
|
|
literal frame does not degrade, it drops.
|
|
2. {PACKED_BPF*FPS*GAME_S/1e9:.2f} GB for the whole game, against the codec's {codec_bpf*FPS*GAME_S/1e9:.2f} GB at the gate
|
|
recipe and ~{327*1024*GAME_S/1e9:.2f} GB at E7's target. That is a packaging fact (C3),
|
|
not a performance one.
|
|
|
|
AND 47.4 STILL SITS OVER ALL OF IT. Everything above assumes the layer is
|
|
visible while it is written. If it is not, the packed player is a {100*blit[8]/FRAME_CLK:.0f}% duty
|
|
strobe at best and there is no version of it that is merely expensive.""")
|