Put the transport on the 68000, and find PIO costs 87 clocks a byte
ROADMAP P4b. src/player/xfer.i answers src/player/ring.i's XF_* mailbox with a real READ(10) to a real MB89352 in place of tools/bench/stream.lua's modelled transport: 120 records, 4,488,588 B, pixel-exact out of a 256 KB ring, with a real mid-stream seek in a second pass. The tiling is the SAME 18 wraps and 14.7 KB mean hole that 49.4's host producer and 55.4's modelled transport produced -- a third transport, same placement, which is the assertion that ring.i could not tell which side of the seam answered it. What it costs is the finding. tools/bench/xfer_cost.sh subtracts the same 120 frames run twice and gets 87.28 clocks per delivered byte, against the 68000's own cycle table for the loop, which says 87.15 -- 0.2% apart, so the cost is the instruction stream and not MAME's device model, and it is the first number this rig has produced that survives leaving the emulator. That is 391.8% of a 12 fps frame; the machine's own V-DISP clock agrees from the other end at 2.57 fps. Against the ladder, W=5 held is 22.4% of a frame and W=19 is 85.3%, so P4a is worth 4.6x the worst DMA configuration in this tree and 17.5x the best -- where before this session it was worth 9 against 19. W itself did not move by a clock. "UNDERRUNS: 0/120" is vacuous with a synchronous transport, and stream.lua now prints that argument next to the zero: a frame cannot start before its record has landed because the decoder IS the transport. The counter that means something is NO IDLE, 119/120 with a worst overrun of 441 whole ticks. Same class of error as 49.7.2's free-running ring passing at 48 KB. 58.3: a record is not a sector -- 117 of 120 start part way into one, and reading whole sectors into the ring corrupts the neighbours rather than wasting bytes (49.2, no bounds check). scsi.i reads the covering sectors and stores only the window, which is free in PIO and stops being free the moment P4a succeeds. tools/analysis/26_sector_align.py prices the three ways out and sector-aligned records win on both axes: +0.43% wire and zero clocks, against +1.34% and a bounce copy at +5 clk/B. ROADMAP now carries a four-item re-encode bundle and P4a should be attempted against a sector-aligned container. check.sh gains two stages and was ALL GREEN before and after. decode.bin is unchanged at 1,296 B and the same MD5. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"""A record is not a sector: what the mismatch costs, three ways (P4b, 58.3).
|
||||
|
||||
src/player/ring.i asks the transport for a RECORD -- a byte offset into the
|
||||
scene's frame stream and a length, both 4-byte aligned because that is what
|
||||
`move.l (a0)+` needs (28.3) and neither of them a multiple of 512. A SCSI
|
||||
target answers in 512 B BLOCKS. On the gate container 117 of 120 records start
|
||||
part way into a sector, so something has to reconcile the two, and the three
|
||||
ways of doing it are not close.
|
||||
|
||||
WHY IT IS NOT AN IMPLEMENTATION DETAIL. The bytes on either side of a record in
|
||||
the stream belong to OTHER records -- ones the decoder may still be reading --
|
||||
and the block loop walks a0 with no bounds check at all (49.2). So a transport
|
||||
that reads whole sectors straight into the ring does not waste 500 bytes, it
|
||||
CORRUPTS the neighbours, and the symptom is wrong pixels rather than a fault.
|
||||
|
||||
A. WINDOWED PIO. Read the sectors the record lies in, store only the record.
|
||||
src/player/scsi.i does this and it is what FINDINGS 58 measured. It costs
|
||||
nothing in clocks -- the CPU is touching every byte anyway -- and it costs
|
||||
the extra sectors on the wire. It CANNOT be done by a DMAC: a channel
|
||||
writes a contiguous run to a contiguous address and cannot be told to drop
|
||||
the first 300 bytes.
|
||||
B. BOUNCE BUFFER. Let the DMAC write whole sectors somewhere else, then copy
|
||||
the record into the ring. Works under DMA, and costs a copy of every
|
||||
delivered byte -- which is precisely the cost `aligned` was chosen over
|
||||
`split` to avoid (49.3, 19_ring_stream.py).
|
||||
C. SECTOR-ALIGNED RECORDS. Pad each record up to 512 in the container
|
||||
instead of up to 4. Costs bytes on the disc and in every delivery, and
|
||||
nothing else at all; the transport becomes a whole-sector read into the
|
||||
ring with no window and no copy. It is a CONTAINER change -- a re-encode
|
||||
and a re-measurement of every constant fitted to the gate container, which
|
||||
is the class of change ROADMAP already has bundled with P2's other half.
|
||||
|
||||
python3 tools/analysis/26_sector_align.py <in.dlx> [--ring KB]
|
||||
|
||||
No rate is taken and none is needed: every figure here is a fraction of the
|
||||
delivered bytes or a count of clocks, and both are rate-free. What a given
|
||||
delivery rate does with them is 15_bus_occupancy.py's question.
|
||||
"""
|
||||
import sys, os, argparse
|
||||
sys.path.insert(0, "tools/encoder")
|
||||
from dlx import DLX
|
||||
|
||||
SECTOR = 512
|
||||
CPUHZ = 10_000_000
|
||||
# 5.0 clocks/byte, and it is 19_ring_stream.py's constant rather than a new one:
|
||||
# a 68000 `move.l (a0)+,(a1)+` moves 4 bytes in 20 clocks on a 16-bit bus. It
|
||||
# is the OPTIMISTIC figure there and it is the optimistic figure here.
|
||||
COPY_CLK_PER_BYTE = 5.0
|
||||
# The windowed PIO loop in src/player/scsi.i, from the 68000's cycle table:
|
||||
# 12 move.l #SC_PATIENCE,d3 patience reload
|
||||
# 16 move.b SC_SSTS,d0 (xxx).L -> Dn
|
||||
# 10 btst #0,d0
|
||||
# 10 beq.s taken
|
||||
# 20 move.b SC_DREG,(a1)+ (xxx).L -> (An)+
|
||||
# 8 subq.l #1,d7
|
||||
# 10 bne.s taken
|
||||
# FINDINGS 58.2 measured 87.28 clocks per delivered byte against this loop's 86
|
||||
# plus 1.15 for the dropped window bytes -- 0.2% apart, which is what says the
|
||||
# cost is the instruction stream and not MAME's device model.
|
||||
PIO_CLK_PER_BYTE = 86.0
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("container")
|
||||
ap.add_argument("--ring", type=int, default=256, help="ring size in KB")
|
||||
a = ap.parse_args()
|
||||
|
||||
d = DLX(a.container)
|
||||
|
||||
# The disc layout the 68000 walks: [u32 len][body], each record padded up to 4.
|
||||
# Exactly tools/bench/prep_stream.py's, and it is rebuilt here rather than read
|
||||
# from tmp/ so this tool works on any container.
|
||||
off, recs = 0, []
|
||||
for (o, n) in d.frames:
|
||||
ln = 4 + n
|
||||
ln += (-ln) % 4
|
||||
recs.append((off, ln))
|
||||
off += ln
|
||||
payload = sum(ln for _, ln in recs)
|
||||
nfr = len(recs)
|
||||
budget = CPUHZ / d.fps
|
||||
|
||||
print(f"{a.container}: {nfr} records, {payload:,} B, {d.fps} fps")
|
||||
print(f" mean record {payload/nfr:,.0f} B; a {d.fps} fps frame is "
|
||||
f"{budget:,.0f} clocks")
|
||||
aligned0 = sum(1 for o, _ in recs if o % SECTOR == 0)
|
||||
print(f" records that already start on a sector boundary: {aligned0}/{nfr}")
|
||||
print()
|
||||
|
||||
# ---- A. windowed PIO: the sectors the record lies in, and only the record kept
|
||||
wire_a = sum(((o % SECTOR) + ln + SECTOR - 1) // SECTOR for o, ln in recs) * SECTOR
|
||||
drop_a = wire_a - payload
|
||||
print("A. WINDOWED PIO (src/player/scsi.i, what FINDINGS 58 ran)")
|
||||
print(f" wire {wire_a:,} B for {payload:,} B of record "
|
||||
f"= +{100*drop_a/payload:.2f}%")
|
||||
print(f" clocks {PIO_CLK_PER_BYTE:.0f}/B on EVERY byte off the FIFO, "
|
||||
f"dropped ones included:")
|
||||
print(f" {PIO_CLK_PER_BYTE*wire_a/nfr:,.0f} clk/frame "
|
||||
f"= {100*PIO_CLK_PER_BYTE*wire_a/nfr/budget:.0f}% of the frame")
|
||||
print( " and it does not survive the move to the DMAC at all: a channel "
|
||||
"cannot drop bytes.")
|
||||
print()
|
||||
|
||||
# ---- B. bounce buffer: DMA whole sectors elsewhere, copy the record in
|
||||
print("B. BOUNCE BUFFER (whole sectors by DMA, then a copy)")
|
||||
print(f" wire {wire_a:,} B, the same +{100*drop_a/payload:.2f}% -- the "
|
||||
f"command is identical")
|
||||
print(f" clocks {COPY_CLK_PER_BYTE:g}/B of copy on every DELIVERED byte, "
|
||||
f"on top of whatever W the")
|
||||
print(f" channel steals: {COPY_CLK_PER_BYTE*payload/nfr:,.0f} clk/frame "
|
||||
f"= {100*COPY_CLK_PER_BYTE*payload/nfr/budget:.1f}% of the frame")
|
||||
print( " which is the cost `aligned` was chosen over `split` to avoid "
|
||||
"(49.3), arriving")
|
||||
print( " by a different door and on every byte instead of on a wrap.")
|
||||
print()
|
||||
|
||||
# ---- C. sector-aligned records in the container
|
||||
cur, pad = 0, 0
|
||||
for _, ln in recs:
|
||||
if cur % SECTOR:
|
||||
pad += SECTOR - (cur % SECTOR)
|
||||
cur += SECTOR - (cur % SECTOR)
|
||||
cur += ln
|
||||
print("C. SECTOR-ALIGNED RECORDS (a container change; a re-encode)")
|
||||
print(f" wire {cur:,} B for {payload:,} B of record = +{100*pad/payload:.2f}%")
|
||||
print( " clocks ZERO: the read is a whole-sector read straight into the "
|
||||
"ring, no window,")
|
||||
print( " no copy, and the DMAC can do it.")
|
||||
print()
|
||||
|
||||
ringsz = a.ring * 1024
|
||||
print(f" VERDICT, in the currency this project prices delivery in. C is "
|
||||
f"cheaper on the wire")
|
||||
print(f" than A and B by {100*(drop_a-pad)/payload:.2f} points of the payload "
|
||||
f"({drop_a-pad:,} B on this scene),")
|
||||
print(f" and it is the only one of the three a DMA channel can run without a "
|
||||
f"copy. What it")
|
||||
print(f" costs is a container revision and the re-measurement that comes with "
|
||||
f"one.")
|
||||
maxrec = max(ln for _, ln in recs)
|
||||
maxpad = maxrec + (-maxrec) % SECTOR
|
||||
print(f" It also grows the largest record from {maxrec:,} to {maxpad:,} B, "
|
||||
f"which a {a.ring} KB")
|
||||
print(f" ring still holds {ringsz//maxpad} times over.")
|
||||
Reference in New Issue
Block a user