Align the container to the disc, and find the decoder-free packed player fits

Two sessions, unrecorded until now, committed together because their edits
share files and cannot be split cleanly after the fact.

Session 28 (FINDINGS 60): the container is DLX5 -- every record sector-aligned,
120/120 starting on a boundary where 3/120 did, +0.48% on the wire and zero
clocks -- and the ring's release rounds to RECALN so no pad is stranded.  Two
encoder levers measured and refused: `--spans all` buys +0.19 dB for +67% of
the wire, and joint span/lam selection emits byte-identical containers because
`lam` never leaves its floor on any of 120 frames.

Session 29 (FINDINGS 61): the packed full-frame blit is 27.3% of a 12 fps
frame, a channel fills GVRAM in buffer mode off the disc with the CPU halted,
and it walks the 1,024 B line stride itself through array chaining.  At the
9 clk/B dual-address floor the codec is 110.4% of a frame and a decoder-free
packed literal player is 55.2%, at +4.89 dB -- 2.75 dB past a ceiling the
codec's scene-wide palette cannot cross.  Encoder work is parked; the codec is
kept and not built on.

check.sh is ALL GREEN before and after, plus one new stage that gates the ORDER
of the measured paint costs rather than their values.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 06:54:27 -07:00
parent 8800d8f8c0
commit 1be428c270
28 changed files with 2203 additions and 144 deletions
+23 -1
View File
@@ -29,7 +29,7 @@ end
local MODE = load_mode()
local FLAG, VAR, ITER = 0x18000, 0x18004, 0x18008
local SRCW, SRCB = 0x60000, 0x80000
local SRCW, SRCB, SRCP = 0x60000, 0x80000, 0x90000
local GVRAM, GPAL = 0xC00000, 0xE82000
local CPUHZ = 10000000 -- x68k.cpp:1133, 40_MHz_XTAL/4
local FRAME12 = CPUHZ / 12 -- 833333 cycles at 12 fps
@@ -40,6 +40,16 @@ local PLAN = {
{var=2, iter= 50, name="V2 naive byte-source expansion (move.b/move.w per pixel)"},
{var=3, iter=200, name="V3 write-only floor (no source read at all)"},
{var=4, iter= 60, name="V4 same 96KB of writes, issued in 4x4 BLOCK order (decoder access pattern)"},
-- V8 is V1 with R20 bit 11's packing: 48KB read + 48KB write for the SAME
-- 49,152 pixels. It is the per-frame work of a decoder-free packed player
-- (FINDINGS 44.7 / 46.6 / 47.5), and 47.6.1 filed its `movem` shape as an
-- ASSUMPTION -- this is the measurement that assumption was standing in for.
{var=8, iter=200, name="V8 PACKED movem.l blit (48KB read + 48KB write, same 49,152 pixels as V1)"},
-- V9/V10 are the two ways a BLOCK decoder could survive the packed layout
-- (47.6.4, open since session 16): sixteen move.b at stride 2 per block, or
-- pair the blocks 128 columns apart in the encoder and get V4's movem back.
{var= 9, iter= 40, name="V9 PACKED block order, 16 move.b at stride 2 per 4x4 block"},
{var=10, iter=120, name="V10 PACKED block order, blocks PAIRED so a movem writes whole words"},
}
local code do
@@ -97,6 +107,18 @@ local function setup()
SP:write_u8 (SRCB + y*256 + x, px)
end
end
-- SRCP: the PACKED frame, interleaved the way tools/bench/show_frame256_packed.lua
-- lays it out -- word i of a row is (column i+128) << 8 | (column i), because
-- page 0 is the low byte at screen column i and page 1 the high byte at i+128.
-- Only V8 reads it, and only its SIZE (128 words a row) affects the timing;
-- the interleave is written correctly so the buffer is the real artefact and
-- not a same-sized stand-in.
for y = 0, H-1 do
local row = PIX0 + y*W
for i = 0, (W//2)-1 do
SP:write_u16(SRCP + y*(W//2)*2 + i*2, (B(row+i+W//2) << 8) | B(row+i))
end
end
for i = 1, #code do SP:write_u8(0x10000+i-1, string.byte(code,i)) end
P(string.format("loaded blit.bin=%d bytes, source frame %dx%d at yoff=%d", #code, W, H, YOFF))
end
+146
View File
@@ -104,6 +104,9 @@ ITER = $18008 ; iteration count, written by Lua
SPTR = $1800C ; V5 span stream pointer, written by Lua
SRCW = $60000 ; word-expanded frame 192*512 = 96KB
SRCB = $80000 ; byte-per-pixel frame 192*256 = 48KB
SRCP = $90000 ; PACKED frame 192*256 = 48KB (V8): two picture
; bytes per word, already interleaved by the
; encoder, so the blit is a straight copy
DST0 = $C08000 ; GVRAM + 32*1024 (first picture row)
DSTE = $C38000 ; GVRAM + 224*1024 (one past last)
ROWS = 192 ; picture rows a V5 stream describes
@@ -130,6 +133,12 @@ start:
beq v6
cmp.l #7,d0
beq v7
cmp.l #8,d0
beq v8
cmp.l #9,d0
beq v9
cmp.l #10,d0
beq v10
bra v3
; ---------------------------------------------------------------- V1
@@ -372,5 +381,142 @@ v7fh:
bne v7
bra done
; ---------------------------------------------------------------- V8
; THE PACKED FULL-FRAME BLIT (FINDINGS 46.6/47.2). Identical in shape to V1 --
; a row-linear movem.l chain out of a RAM frame into GVRAM -- and different in
; exactly one thing: a row is 128 WORDS, not 256, because R20 bit 11 lets one
; word carry two picture bytes. 256 = 5*48 + 16, so five 12-register bursts
; and a 4-register tail, against V1's ten and one.
;
; TIMING ONLY, and it does not set bit 11. MAME's gvram_w carries no timing in
; either arm (blit.lua's header), so the bit cannot move a cycle here; what it
; moves is the PICTURE, and the picture is what tools/bench/show_frame256_packed.lua
; and tools/bench/gvpack already verify pixel-exactly. Setting it here would
; make this variant's snapshot right and its measurement no different, and
; would put a display-mode change inside a timing loop for no gain.
;
; The source is PRE-INTERLEAVED by the host, which is the honest half of the
; claim: the packing is an encoder-side transform (46.3's argument for the text
; plane, and the same one here), so the decoder-free player's per-frame work is
; this copy and nothing else. If the interleave had to happen at run time this
; variant would be V2, not V1.
v8: lea SRCP,a0
lea DST0,a1
lea DSTE,a6
v8row: movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,48(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,96(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,144(a1)
movem.l (a0)+,d0-d7/a2-a5
movem.l d0-d7/a2-a5,192(a1)
movem.l (a0)+,d0-d3
movem.l d0-d3,240(a1)
lea 1024(a1),a1
cmpa.l a6,a1
bne v8row
subq.l #1,ITER.l
bne v8
bra done
; ---------------------------------------------------------------- V9
; WHAT THE PACKED LAYOUT COSTS A BLOCK DECODER (FINDINGS 47.6.4, open).
;
; V4 is the access pattern of a decoder that writes 4x4 codewords straight into
; GVRAM: 4 rows of 8 contiguous bytes at a 1024-byte stride, so each row is one
; `movem.l` of two registers. Under the packed layout that pattern is GONE.
; A block at columns x..x+3 owns the LOW bytes of four consecutive words -- four
; bytes at STRIDE 2 -- and the high bytes of those same words belong to the
; block 128 columns away. There is no burst that writes every other byte, so
; the block is sixteen `move.b`s.
;
; V9 does the pair together, low block then high block off one base, so it
; writes every byte it touches and covers the same 49,152 pixels V1/V4/V8 do.
; It is the HONEST version of "keep the codec and pack the screen": the mode
; map is unchanged, SKIP still works per block, and the writes go byte at a
; time. V10 below is the other option, and the comparison is the point.
v9: lea SRCB,a0
lea DST0,a3
lea DSTE,a4
v9brow: move.l a3,a1
lea 256(a3),a5 ; 32 block PAIRS * 8 bytes
v9blk:
move.b (a0)+,(a1)
move.b (a0)+,2(a1)
move.b (a0)+,4(a1)
move.b (a0)+,6(a1)
move.b (a0)+,1024(a1)
move.b (a0)+,1026(a1)
move.b (a0)+,1028(a1)
move.b (a0)+,1030(a1)
move.b (a0)+,2048(a1)
move.b (a0)+,2050(a1)
move.b (a0)+,2052(a1)
move.b (a0)+,2054(a1)
move.b (a0)+,3072(a1)
move.b (a0)+,3074(a1)
move.b (a0)+,3076(a1)
move.b (a0)+,3078(a1)
move.b (a0)+,1(a1)
move.b (a0)+,3(a1)
move.b (a0)+,5(a1)
move.b (a0)+,7(a1)
move.b (a0)+,1025(a1)
move.b (a0)+,1027(a1)
move.b (a0)+,1029(a1)
move.b (a0)+,1031(a1)
move.b (a0)+,2049(a1)
move.b (a0)+,2051(a1)
move.b (a0)+,2053(a1)
move.b (a0)+,2055(a1)
move.b (a0)+,3073(a1)
move.b (a0)+,3075(a1)
move.b (a0)+,3077(a1)
move.b (a0)+,3079(a1)
addq.l #8,a1
cmpa.l a5,a1
bne v9blk
lea 4096(a3),a3
cmpa.l a4,a3
bne v9brow
subq.l #1,ITER.l
bne v9
bra done
; ---------------------------------------------------------------- V10
; THE OTHER OPTION: PAIR THE BLOCKS IN THE ENCODER. If the codec codes the
; block at x and the block at x+128 as ONE unit, the destination is whole words
; again and V4's `movem.l` shape comes straight back -- the same instructions,
; the same 32 bytes of source per unit, and TWICE the pixels, because a word now
; carries two of them. So V10 is V4's inner loop run half as many times.
;
; WHAT IT COSTS IS NOT IN THIS MEASUREMENT. A pair skips only if BOTH of its
; blocks skip, and the two are 128 columns apart with nothing in the picture
; relating them. That is a CONTAINER question -- what fraction of the mode map
; survives pairing -- and 08_mode_map.py has the data to answer it. V10 prices
; the paint; it does not price the SKIPs the pairing loses.
v10: lea SRCP,a0
lea DST0,a3
lea DSTE,a4
v10brow: move.l a3,a1
lea 256(a3),a5 ; 32 block PAIRS * 8 bytes
v10blk: movem.l (a0)+,d0-d7 ; 32 bytes = one PAIR of 4x4 blocks
movem.l d0-d1,(a1)
movem.l d2-d3,1024(a1)
movem.l d4-d5,2048(a1)
movem.l d6-d7,3072(a1)
addq.l #8,a1
cmpa.l a5,a1
bne.s v10blk
lea 4096(a3),a3
cmpa.l a4,a3
bne v10brow
subq.l #1,ITER.l
bne v10
bra done
done: move.l #$FF,FLAG.l ; timer stops here
halt: bra.s halt
+91 -14
View File
@@ -353,7 +353,7 @@ echo "--- session 23: the 68000 fills its own ring (FINDINGS 55) ---"
# rather than faulting (49.2);
# * the host AUDITS every placement against its own index and its own list of
# live records, and refuses the run on the first disagreement;
# * the wrap policy still produces the SAME 18 wraps and 14.7 KB mean hole the
# * the wrap policy still produces the SAME 18 wraps the
# host producer produced in FINDINGS 49.4 -- a third independent
# implementation of `aligned` landing on the same tiling;
# * zero underruns at a two-deep request queue, which is the finding: a
@@ -445,13 +445,17 @@ echo "--- session 26: the ring is filled off a real SCSI volume (FINDINGS 58) --
# * pixel-exact, which is the only test that can see a wrong record: the
# window in scsi.i decides which of a sector's bytes reach the ring, and a
# window off by one byte desyncs the bitstream rather than faulting (49.2);
# * the SAME 18 wraps and 14.7 KB mean hole -- ring.i's placement policy must
# not be able to tell which transport answered it, and this is the assertion
# that says it could not;
# * every record accounted for: 120 READ(10)s, 4,488,588 B into the ring, and
# 4,548,608 B off the disc. The two byte counts differ by 1.34% because a
# record is not a sector, and that gap is a delivery cost (58.3) -- gating
# both numbers means neither can drift silently into the other;
# * the SAME 18 wraps -- ring.i's placement policy must not be able to tell
# which transport answered it, and this is the assertion that says it could
# not. The WRAP COUNT is gated and the mean hole is only reported: DLX5's
# records are up to 511 B longer than DLX4's, so the hole moved (14.7 KB ->
# 13.5 KB) while the tiling did not. Gating a number that the container's
# record lengths move would gate the container, not the policy;
# * every record accounted for: 120 READ(10)s, and the bytes into the ring
# EQUAL to the bytes off the disc -- both read out of the container rather
# than written here. Under DLX4 they differed by 1.34% because a record was
# not a sector (58.3); DLX5 aligns records to sectors and the covering-sector
# read disappears, so the gate is now their IDENTITY;
# * a real mid-stream SEEK with the real transport, in the second pass. This
# is the one path that could not exist before: ring_seek waits for the
# channel to go quiet, and with the transport INSIDE the machine the only
@@ -470,15 +474,34 @@ if command -v chdman > /dev/null; then
| sed "s/^ *//;s/^/ /"
grep -aq "TRANSPORT FAILED" tmp/p4b_check.log && {
echo "FAIL: a record's READ(10) reported an error."; exit 1; }
grep -aq "REAL TRANSPORT: 120 READ(10)s by the 68000, 4488588 B into the ring" \
# THE BYTE COUNTS COME OUT OF THE CONTAINER, not out of this file. They were
# two hardcoded constants fitted to the DLX4 gate container, and session 28's
# re-encode went red on both of them for the right reason -- the container had
# changed and the expectation had not. A gate whose expected value is a
# literal tests the literal.
EXPECT_B=$(python3 -c "
import sys; sys.path.insert(0, 'tools/encoder')
from dlx import DLX
print(sum(DLX('$DLX').record_lengths()))")
grep -aq "REAL TRANSPORT: 120 READ(10)s by the 68000, $EXPECT_B B into the ring" \
tmp/p4b_check.log || {
echo "FAIL: the 68000 did not fetch all 120 records, or did not fetch"
echo " 4,488,588 B of them. A short record is a desync, not a shortfall."
echo " $EXPECT_B B of them. A short record is a desync, not a shortfall."
exit 1; }
grep -aq "SECTOR OVERHEAD: 4548608 B off the disc" tmp/p4b_check.log || {
echo "FAIL: the bytes the DISC moved are no longer 4,548,608. A record is"
echo " not a sector; this is the covering-sector read, and if it moved"
echo " then either the layout or scsi.i's window did. See FINDINGS 58.3."
# DLX5 MAKES THESE THE SAME NUMBER, and that identity IS the finding (59.4,
# and 58.3 option C): a sector-aligned container has no covering-sector read,
# so the disc moves exactly the records and nothing else. Under DLX4 they
# differed by 1.34% and both were gated so neither could drift into the other;
# under DLX5 the gate is that they are EQUAL. If a windowed read ever came
# back -- a container that was not aligned, or a layout that lost the
# alignment -- the disc figure would exceed the ring figure and this goes red.
grep -aq "SECTOR OVERHEAD: $EXPECT_B B off the disc for $EXPECT_B B of record = 0.00%" \
tmp/p4b_check.log || {
echo "FAIL: the disc no longer moves EXACTLY the records. On a sector-"
echo " aligned container (DLX5) there is no covering-sector read at"
echo " all, so these two counts must be the same $EXPECT_B B. If they"
echo " differ, either the container lost its alignment or scsi.i is"
echo " windowing again -- and a DMA channel cannot window (59.4)."
exit 1; }
grep -aq "ring: 18 wraps" tmp/p4b_check.log || {
echo "FAIL: the placement policy tiled this container differently with a"
@@ -574,4 +597,58 @@ else
echo " (git clone --depth 1 https://github.com/icculus/DirkSimple)"
fi
echo "--- session 29: the packed paint, and what it does to the codec (FINDINGS 61) ---"
# tools/bench/blit.s gained V8/V9/V10 -- the packed full-frame blit, and the two
# ways a 4x4 BLOCK decoder could survive the packed layout. 47.6.1 had filed the
# packed paint's `movem` shape as an ASSUMPTION since session 16; this measures
# it, in the same run as V1/V3/V4 so it is quoted against numbers that have not
# moved since session 9.
#
# WHAT IS GATED IS STRUCTURAL, not numeric, for the reason the load stage gives:
# MAME samples these on a 1/56.69 s clock and no cost model in the tree depends
# on their exact value. What DOES depend on them is the ORDER, and the order is
# the whole of FINDINGS 61:
# V8 < V1 packing halves the full-frame literal paint
# V9 > V4 packing makes a BLOCK decoder DEARER, not cheaper
# V10 < V4 unless the blocks are paired, which costs SKIPs instead
# A tree where any of those flipped has a different answer to 44.7 and should
# say so out loud rather than let 29_packed_player.py narrate the old one.
python3 tools/bench/prep_frame.py tmp/fr_00020 tmp/frame256.bin 0 --reserve-black
rm -f tmp/blit_v8.log
( cd tmp && SDL_VIDEODRIVER=dummy stdbuf -oL timeout -k 5 300 mame x68000 \
-bios ipl10 -ramsize 2M -video soft -window -sound none -nothrottle -plugins \
-autoboot_script ../tools/bench/blit.lua -seconds_to_run 120 \
> blit_v8.log 2>&1 )
grep -aq "summary (instruction cycles only" tmp/blit_v8.log || {
echo "FAIL: the blit timing run produced no summary -- it did not finish."
tail -8 tmp/blit_v8.log; exit 1; }
python3 - <<'EOF' || exit 1
import re, sys
v = {}
for line in open("tmp/blit_v8.log", errors="replace"):
m = re.search(r"V(\d+)\s+(\d+) cyc", line)
if m: v[int(m.group(1))] = int(m.group(2))
need = (1, 2, 3, 4, 8, 9, 10)
missing = [n for n in need if n not in v]
if missing: sys.exit(f"FAIL: blit.lua reported no V{missing} -- run incomplete.")
for a, op, b, why in ((8, "<", 1, "packing did not halve the literal paint"),
(9, ">", 4, "packed BLOCK order came out CHEAPER than "
"unpacked -- 61.3's conclusion is inverted"),
(10, "<", 4, "pairing the blocks did not buy back the "
"movem shape")):
ok = v[a] < v[b] if op == "<" else v[a] > v[b]
if not ok:
sys.exit(f"FAIL: V{a} {v[a]:,} is not {op} V{b} {v[b]:,} -- {why}.")
print(f" V1 {v[1]:,} / V8 PACKED {v[8]:,} = {100*v[8]/v[1]:.0f}% -- "
f"and V3, the unpacked WRITE-ONLY floor, is {v[3]:,}")
print(f" V4 {v[4]:,} / V9 packed-block {v[9]:,} = {100*v[9]/v[4]:.0f}% -- "
f"packing costs a BLOCK decoder {100*v[9]/v[4]-100:.0f}%")
print(f" V10 paired blocks {v[10]:,} = {100*v[10]/v[4]:.0f}% of V4, and pairing "
f"is paid for in SKIPs")
EOF
python3 tools/analysis/29_packed_player.py "$DLX" > tmp/packed_player.log 2>&1 \
|| { tail -20 tmp/packed_player.log; exit 1; }
grep -aE "SKIP block PAIRS|free / DMAC->GVRAM / PACKED|^ CODEC, gate" \
tmp/packed_player.log
echo "ALL GREEN"
+87 -8
View File
@@ -25,17 +25,24 @@ local function P(s) print("[DMA] "..s) end
local function T() local t=M.time; return t.seconds + t.attoseconds/1e18 end
local DGFLAG, DGREC, DGREC_SZ = 0x18600, 0x18610, 32
local DGWIN, DGWERR = 0x18680, 0x18684
local DGWIN, DGWERR, DGR20, DGR20N, DGR20C = 0x18700, 0x18704, 0x18708, 0x1870C, 0x18710
local CHROW, CHN, CHBASE = 256, 8, 0xC10000
local R20OF -- filled in after the mailbox addresses are known
local GV = 0xC00000
local DGLBA, DGBLK = 1000, 4
local DST = {0x20000, 0x24000, 0x28000}
local DST = {0x20000, 0x24000, 0x28000, 0xC08000, 0xC0C000, 0xC10000}
local NAME = {"PIO (the path FINDINGS 58 measured)",
"DMA, BUS HELD (DCR $00 burst, OCR $81 max rate)",
"DMA, STEALING (DCR $80 cycle steal, OCR $80 limited)"}
local SHORT = {"pio", "held", "steal"}
"DMA, STEALING (DCR $80 cycle steal, OCR $80 limited)",
"DMA -> GVRAM (bus held, R20 bit 11 = BUFFER MODE) [47.6.2]",
"DMA -> GVRAM (the SAME, bit 11 CLEAR -- NEGATIVE CONTROL)",
"DMA -> GVRAM (ARRAY CHAINED, 8 rows at the 1024 B line stride)"}
local SHORT = {"pio", "held", "steal", "gvram", "masked", "chain"}
local ERRNAME = {[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"}
R20OF = {[3]=DGR20, [4]=DGR20N, [5]=DGR20C}
local DISK = os.getenv("DLX_SCSI_IMG") or "dlxdisk.img"
local code do local f=io.open("dmagate.bin","rb"); code=f:read("a"); f:close() end
@@ -58,7 +65,8 @@ SUB = emu.add_machine_frame_notifier(function()
cpu.state["SP"].value = 0x8000
cpu.state["PC"].value = 0x10000
P(string.format("dmagate.bin=%d B loaded at $10000; reading LBA %d, %d B, "
.."three ways", #code, DGLBA, DGBLK*512))
.."three ways, then once more into GVRAM",
#code, DGLBA, DGBLK*512))
st = "wait"; return
end
if st == "wait" then
@@ -68,7 +76,7 @@ SUB = emu.add_machine_frame_notifier(function()
end
if not want then P("no "..DISK.." to check against"); P("done"); M:exit(); return end
local LEN = DGBLK*512
for i = 0, 2 do
for i = 0, 5 do
local b = DGREC + i*DGREC_SZ
local rc = SP:read_u32(b)
local e = SP:read_u32(b+4)
@@ -83,14 +91,85 @@ SUB = emu.add_machine_frame_notifier(function()
P(string.format(" FAILED: err=%d (%s)", e, ERRNAME[e] or "?"))
else
local bad, first = 0, nil
-- The GVRAM run is read back a WORD at a time and split by hand.
-- SP:read_u8 on $C00000 goes through gvram_r, which in buffer mode
-- returns the whole word; asking for one byte of it would hand back
-- whichever half MAME's address space happens to hand over, and the
-- question here is precisely WHICH HALF each disc byte landed in.
-- Even disc byte -> high half (page 1), odd -> low half (page 0),
-- because the 68000 is big-endian and an even address is the MS byte.
local pg1, pg0, bad_hi, bad_lo = 0, 0, 0, 0
for k = 1, LEN do
if SP:read_u8(DST[i+1]+k-1) ~= string.byte(want, k) then
bad = bad + 1; first = first or (k-1)
local got
if i == 5 then
-- The chained run's destination is not linear: byte k of the
-- transfer is byte k%256 of row k//256, and the rows are a full
-- 1024 B line stride apart. If the channel had ignored the array
-- and run contiguously, every byte past the first row would be
-- in the wrong place and this comparison would say so.
local off = (k-1) % CHROW
local a = CHBASE + ((k-1) // CHROW) * 1024 + (off & ~1)
local w = SP:read_u16(a)
if (off % 2) == 0 then got = (w >> 8) & 0xff; pg1 = pg1 + 1
else got = w & 0xff; pg0 = pg0 + 1 end
elseif i >= 3 then
local w = SP:read_u16(DST[i+1] + ((k-1) & ~1))
if ((k-1) % 2) == 0 then got = (w >> 8) & 0xff; pg1 = pg1 + 1
else got = w & 0xff; pg0 = pg0 + 1 end
else
got = SP:read_u8(DST[i+1]+k-1)
end
if got ~= string.byte(want, k) then
bad = bad + 1; first = first or (k-1)
if ((k-1) % 2) == 0 then bad_hi = bad_hi + 1
else bad_lo = bad_lo + 1 end
end
end
if i >= 3 then
P(string.format(" R20 during the run = $%04X (bit 11 %s); %d bytes "
.."read back as page 1 (high half) and %d as page 0",
SP:read_u32(R20OF[i]),
((SP:read_u32(R20OF[i]) & 0x0800) ~= 0)
and "SET" or "CLEAR",
pg1, pg0))
end
if bad == 0 then
P(string.format(" BYTES OK: %d B from LBA %d match %s byte for byte "
.."[%s]", LEN, DGLBA, DISK, SHORT[i+1]))
if i == 3 then
P(" A CHANNEL FILLS THE PACKED LAYOUT: every disc byte landed in "
.."its own half of a GVRAM word, with the CPU halted -- so a "
.."stream interleaved (right<<8)|left goes from disc to screen "
.."with no CPU in the loop (47.6.2, first half).")
end
if i == 5 then
P(string.format(" THE CHANNEL WALKED THE ARRAY ITSELF: %d rows of "
.."%d B landed at a %d B line stride from ONE start, CPU halted "
.."throughout. A frame is %d such entries; the CPU does not "
.."restart the channel per row.", CHN, CHROW, 1024, 192))
end
if i == 4 then
P(" CONTROL DID NOT FAIL: the masked write path delivered every "
.."byte too, so the run above is not evidence about R20 bit 11.")
end
elseif i == 4 then
-- THE CLAIM IS NOT "half the bytes differ". In masked 256-colour
-- mode gvram_w takes `data & 0x00ff` and ignores mem_mask, so a byte
-- written to an EVEN address is never stored and the high half keeps
-- whatever it held; some of those stale halves match the disc by
-- coincidence, and this record is full of pad, so a lot of them do.
-- The mechanism's signature is WHERE the damage is, not how much:
-- every ODD byte must survive and only EVEN ones may be lost.
P(string.format(" BYTES LOST [masked]: %d of %d differ (first at "
.."+%d) -- %d at EVEN offsets, %d at ODD.",
bad, LEN, first, bad_hi, bad_lo))
if bad_lo == 0 and bad_hi > 0 then
P(string.format(" EXACTLY THE MECHANISM: all %d survivors of the "
.."high half are stale GVRAM that happens to match "
.."(this record is mostly pad); not one of the %d "
.."ODD bytes was harmed. Bit 11 is what carried the "
.."even ones in the run above.", LEN//2 - bad_hi, LEN//2))
end
else
P(string.format(" BYTES WRONG [%s]: %d of %d differ, first at +%d",
SHORT[i+1], bad, LEN, first))
+32
View File
@@ -76,6 +76,38 @@ grep -aq "COC .*CER=\$00 MTC=0 .*(+2048) \[held\]" tmp/dma_run.log || \
fail "the held channel did not report a clean completion of every byte."
grep -aq "COC .*CER=\$00 MTC=0 .*(+2048) \[steal\]" tmp/dma_run.log || \
fail "the stealing channel did not report a clean completion of every byte."
# ---- the GVRAM run and its control (47.6.2). A channel that writes GVRAM in
# buffer mode is the decoder-free packed player's entire per-frame path, and a
# run with no control is 58.3's vacuous "UNDERRUNS: 0/120" again -- the IPL
# leaves R20 = $0B16, bit 11 ALREADY SET, so the first cut of this test could
# not have failed.
grep -aq "BYTES OK: 2048 B from LBA 1000 .*\[gvram\]" tmp/dma_run.log || \
fail "the channel did not fill GVRAM in buffer mode -- a device->GVRAM
transfer is the whole of the decoder-free packed player's frame."
grep -aq "R20 during the run = \$0916 (bit 11 SET)" tmp/dma_run.log || \
fail "the GVRAM run did not run in buffer mode with a KNOWN R20."
grep -aq "R20 during the run = \$0116 (bit 11 CLEAR)" tmp/dma_run.log || \
fail "the negative control did not run with bit 11 clear."
if grep -aq "BYTES OK: 2048 B from LBA 1000 .*\[masked\]" tmp/dma_run.log
then
fail "the MASKED control delivered every byte, so the run above is not a
measurement of R20 bit 11 -- it is a measurement of nothing."
fi
grep -aq "EXACTLY THE MECHANISM" tmp/dma_run.log || \
fail "the masked control lost bytes at ODD offsets too, or lost none at all.
The claim is not a COUNT -- stale GVRAM matches the disc by coincidence
wherever the record is pad -- it is a PLACE: gvram_w's 256-colour arm
drops what the channel wrote to EVEN addresses and stores what it wrote
to odd ones. Damage anywhere else is a different mechanism."
grep -aq "BYTES OK: 2048 B from LBA 1000 .*\[chain\]" tmp/dma_run.log || \
fail "the array-chained run did not put the bytes at the row bases its array
named. A picture row is 256 B of a 1024 B line stride, so a frame is 192
destinations; if the channel cannot walk them the CPU has to restart it
per row and the decoder-free path costs a per-row front end."
grep -aq "THE CHANNEL WALKED THE ARRAY ITSELF" tmp/dma_run.log || \
fail "the chained run did not report walking its own array."
grep -aq "WINDOWED DMA READ REFUSED" tmp/dma_run.log || \
fail "a WINDOWED read through the channel was not refused. 117 of 120 records
start part way into a sector (58.3), and a channel cannot drop the bytes
+15 -1
View File
@@ -31,7 +31,21 @@ open("tmp/dlxdisk.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
chdman createhd -i tmp/dlxdisk.img -o tmp/dlxdisk.chd -ss 512 > /dev/null 2>&1
# -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
# buffer after READ(10) at LBA 0 was byte-for-byte the first 4,096 bytes of
# dlxdisk.chd, starting "MComprHD" -- while `chdman verify` reported both SHA1s
# correct. Uncompressed, the identical image reads byte-exact. The trigger is
# the image's CONTENT: the same 8,768-sector length that works for the DLX4
# volume fails for the DLX5 one, a conventional 16x63 geometry fails too, and
# `-c zlib` alone fails as well. The MAME-side cause is NOT diagnosed; what is
# measured is that compression decides it and uncompressed is sound.
# Costs 4.5 MB in tmp/ against 1.6 MB. DO NOT restore compression to save the
# 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
fi
mkdir -p tmp/p4roms/x68k_cz6bs1
+4 -1
View File
@@ -70,7 +70,10 @@ disk, index = bytearray(), []
for (o, n) in d.frames:
start = len(disk)
disk += n.to_bytes(4, "big") + d.raw[o:o + n]
while len(disk) % 4:
# The container's own alignment rule, not this script's copy of it: DLX5
# pads to 512 so a DMA channel can read whole sectors into the ring, DLX4
# to 4 so `move.l (a0)+` does not take an address error (28.3).
while len(disk) % d.rec_align:
disk += b"\0"
index.append((start, len(disk) - start))
open(a.out + "_disk.bin", "wb").write(bytes(disk))