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:
+23
-1
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user