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:
@@ -171,6 +171,17 @@ ring_seek:
|
||||
cmp.l XF_GO.l,d1
|
||||
beq.s .quiet
|
||||
addq.l #1,SK_WAIT.l
|
||||
bsr xf_service ; the transport hook, and here it is not
|
||||
; optional: with the transport INSIDE
|
||||
; the machine (src/player/xfer.i) the
|
||||
; only thing that can retire the
|
||||
; outstanding request is this loop, so
|
||||
; without it a seek issued with a
|
||||
; request in flight spins forever. A
|
||||
; host transport retired it on its own
|
||||
; time, which is exactly the kind of
|
||||
; difference the seam exists to hide and
|
||||
; this one it could not.
|
||||
bra.s .wait
|
||||
.quiet:
|
||||
move.l XF_GO.l,NRETIRE.l ; whatever landed belongs to the scene
|
||||
@@ -195,6 +206,12 @@ ring_seek:
|
||||
ring_poll:
|
||||
movem.l d0-d3/a0-a1,-(sp)
|
||||
addq.l #1,N_POLL.l
|
||||
; ---- 0. the TRANSPORT, if it lives in this machine. src/player/xfer.i answers
|
||||
; at most one outstanding request per call and preserves every register;
|
||||
; with XF_SCSI = 0 it is a tst and a branch, and the host is the transport
|
||||
; exactly as it was in FINDINGS 55. It goes BEFORE the retire step so that
|
||||
; a transfer completed here is published in the same poll.
|
||||
bsr xf_service
|
||||
|
||||
; ---- 1. retire. The descriptor is written BEFORE the count that advertises
|
||||
; it, which is the same order tools/bench/stream.lua used and the reason
|
||||
|
||||
+121
-5
@@ -132,6 +132,33 @@ SC_PH = $18208 ; u32 phase we were in when it went wron
|
||||
SC_CDB = $18210 ; 12 B command block, built here
|
||||
SC_MSG = $1821C ; 4 B message byte, either direction
|
||||
|
||||
; ---- THE RECORD WINDOW, and why a transport needs one. ROADMAP P4b.
|
||||
; src/player/ring.i asks for a RECORD: a byte offset into the scene's frame
|
||||
; stream and a length, both of them 4-byte aligned and neither of them a
|
||||
; multiple of 512. A SCSI target deals in BLOCKS. On the gate container 117
|
||||
; of 120 records start part way into a sector, so a transport that reads only
|
||||
; whole sectors delivers the record plus up to 511 bytes in front of it and up
|
||||
; to 511 behind, and those neighbouring bytes belong to records the decoder may
|
||||
; still be reading -- the block loop walks a0 with no bounds check (49.2), so
|
||||
; landing them in the ring is a corruption, not a waste.
|
||||
;
|
||||
; IN PIO THE FIX IS FREE, and that is the only reason this is affordable here:
|
||||
; the CPU is already touching every byte, so it simply does not STORE the ones
|
||||
; outside the window. SC_WSKIP bytes are pulled from the FIFO and dropped, the
|
||||
; next SC_WKEEP are stored, the rest are pulled and dropped. Three loops rather
|
||||
; than one steered loop, deliberately: the middle one is then byte-for-byte as
|
||||
; tight as the un-windowed sc_in_pio, so the per-byte cost this rig reports is
|
||||
; the transport's and not the window's.
|
||||
;
|
||||
; UNDER A DMAC IT IS NOT FREE, and that is P4a's problem arriving early. A
|
||||
; channel writes a contiguous run to a contiguous address; it cannot be told to
|
||||
; drop the first 300 bytes. So when the data phase moves to the HD63450 the
|
||||
; choice is a bounce buffer plus a copy of every byte (the cost `aligned` was
|
||||
; chosen to avoid, 49.3) or sector-aligned records in the container -- which is
|
||||
; a re-encode. 57 measured which is cheaper; see FINDINGS 58.3.
|
||||
SC_WSKIP = $18220 ; u32 bytes to drop before the window
|
||||
SC_WKEEP = $18224 ; u32 bytes of window to store
|
||||
|
||||
; ---- a TRACE, because a SCSI bring-up cannot be debugged from one error code.
|
||||
; Four registers at each interesting instant: SSTS, PSNS, INTS, SERR. MAME's
|
||||
; SCMD_CMD_TRANSFER is a NO-OP unless SSTS_INIT_CONNECTED is set -- it `break`s
|
||||
@@ -395,6 +422,84 @@ sip3: move.b SC_DREG,(a1)+
|
||||
move.l (sp)+,d0
|
||||
rts
|
||||
|
||||
; ---------------------------------------------------------------- sc_in_data
|
||||
; Receive d1 bytes in phase d2, storing only the WINDOW: drop SC_WSKIP, store
|
||||
; SC_WKEEP at (a1), drop whatever is left. This is the DATA IN path; STATUS and
|
||||
; MESSAGE IN keep sc_in_pio, which is one byte and has no window.
|
||||
;
|
||||
; THREE LOOPS, NOT ONE STEERED LOOP. A single loop with a `which third am I in`
|
||||
; test per byte would cost ~20 clocks on every byte of every record, and the
|
||||
; number this rig exists to produce is the transport's per-byte cost -- so the
|
||||
; middle loop is byte-for-byte sc_in_pio's and the window is paid for once at
|
||||
; each boundary instead of once per byte.
|
||||
;
|
||||
; A SPLIT DATA PHASE WOULD RE-SKIP. The counters are re-read from memory on
|
||||
; every entry, so a target that broke one READ(10) across two DATA IN phases
|
||||
; would drop the head of the second phase as well. This one does not split --
|
||||
; the same limitation sc_in_pio's caller already carries -- and the fix is the
|
||||
; same one: d5 has to become what each phase actually delivered.
|
||||
sc_in_data:
|
||||
movem.l d6-d7,-(sp)
|
||||
move.b d2,SC_PCTL
|
||||
move.l d1,d0
|
||||
bsr sc_settc
|
||||
move.b #SCMD_XFER,SC_SCMD
|
||||
move.l #7,SC_TAG.l ; 7 = TRANSFER issued for an IN phase
|
||||
bsr sc_snap
|
||||
move.l SC_WSKIP.l,d6
|
||||
move.l SC_WKEEP.l,d7
|
||||
sub.l d6,d1
|
||||
sub.l d7,d1 ; d1 = trailing bytes to drop
|
||||
tst.l d6
|
||||
beq.s .keep
|
||||
.drop1: move.l #SC_PATIENCE,d3
|
||||
.dw1: move.b SC_SSTS,d0
|
||||
btst #0,d0 ; DREG EMPTY -- wait for a byte
|
||||
beq.s .dg1
|
||||
subq.l #1,d3
|
||||
bne.s .dw1
|
||||
bra .tmo
|
||||
.dg1: tst.b SC_DREG ; popped and thrown away
|
||||
subq.l #1,d6
|
||||
bne.s .drop1
|
||||
.keep: tst.l d7
|
||||
beq.s .tail
|
||||
.keep1: move.l #SC_PATIENCE,d3
|
||||
.kw1: move.b SC_SSTS,d0
|
||||
btst #0,d0
|
||||
beq.s .kg1
|
||||
subq.l #1,d3
|
||||
bne.s .kw1
|
||||
bra .tmo
|
||||
.kg1: move.b SC_DREG,(a1)+
|
||||
subq.l #1,d7
|
||||
bne.s .keep1
|
||||
.tail: tst.l d1
|
||||
beq.s .fin
|
||||
.tail1: move.l #SC_PATIENCE,d3
|
||||
.tw1: move.b SC_SSTS,d0
|
||||
btst #0,d0
|
||||
beq.s .tg1
|
||||
subq.l #1,d3
|
||||
bne.s .tw1
|
||||
bra.s .tmo
|
||||
.tg1: tst.b SC_DREG
|
||||
subq.l #1,d1
|
||||
bne.s .tail1
|
||||
.fin: movem.l (sp)+,d6-d7
|
||||
move.l #8,SC_TAG.l ; 8 = every byte taken from the FIFO
|
||||
bsr sc_snap
|
||||
bsr sc_xferend
|
||||
move.l d0,-(sp)
|
||||
move.l #9,SC_TAG.l
|
||||
bsr sc_snap
|
||||
move.l (sp)+,d0
|
||||
rts
|
||||
.tmo: movem.l (sp)+,d6-d7
|
||||
move.l #SCE_TIMEOUT,SC_ERR.l
|
||||
moveq #-1,d0
|
||||
rts
|
||||
|
||||
; ---------------------------------------------------------------- scsi_read
|
||||
; READ(10) of d4 blocks from LBA d3 into (a1). READ(10) rather than READ(6)
|
||||
; because a 21-bit LBA and a 256-block ceiling are limits this container will
|
||||
@@ -410,6 +515,21 @@ sip3: move.b SC_DREG,(a1)+
|
||||
; finds is both shorter and correct, and it is what the target is entitled to.
|
||||
scsi_read:
|
||||
movem.l d3-d5/a1,-(sp)
|
||||
move.l d4,d5
|
||||
lsl.l #8,d5
|
||||
lsl.l #1,d5 ; blocks * 512
|
||||
clr.l SC_WSKIP.l ; no window: keep the whole transfer
|
||||
move.l d5,SC_WKEEP.l
|
||||
bra.s scr_body
|
||||
; ---- the same read, delivering only SC_WSKIP..SC_WSKIP+SC_WKEEP of it. The
|
||||
; caller sets the two words; everything else is identical, which is the point --
|
||||
; a windowed read and a whole one must not be able to differ in the protocol.
|
||||
scsi_read_win:
|
||||
movem.l d3-d5/a1,-(sp)
|
||||
move.l d4,d5
|
||||
lsl.l #8,d5
|
||||
lsl.l #1,d5
|
||||
scr_body:
|
||||
; ---- the command block, built before anything is on the bus
|
||||
lea SC_CDB.l,a0
|
||||
move.b #$28,(a0)+ ; READ(10)
|
||||
@@ -429,10 +549,6 @@ scsi_read:
|
||||
move.b d0,(a0)+ ; would have emitted bits 31..24/23..16
|
||||
move.b d4,(a0)+ ; of a count that lives in 15..0.
|
||||
clr.b (a0)+
|
||||
; ---- d5 = bytes of data still expected
|
||||
move.l d4,d5
|
||||
lsl.l #8,d5
|
||||
lsl.l #1,d5 ; blocks * 512
|
||||
clr.l SC_STAT.l
|
||||
bsr sc_select
|
||||
tst.l d0
|
||||
@@ -482,7 +598,7 @@ scr_din:
|
||||
; from sp as d3,d4,d5,a1 -- a1 is at 12.
|
||||
move.l d5,d1
|
||||
moveq #PH_DATAIN,d2
|
||||
bsr sc_in_pio
|
||||
bsr sc_in_data
|
||||
tst.l d0
|
||||
bmi scr_out
|
||||
bra scr_ph
|
||||
|
||||
@@ -113,6 +113,11 @@ start:
|
||||
; interrupt source first.
|
||||
tst.l RINGOWN.l
|
||||
beq.s noring
|
||||
; ---- the transport, before the producer that will ask it for something.
|
||||
; ring_init ends in a ring_seek and a seek WAITS for the channel to go quiet, so
|
||||
; the thing that makes the channel quiet has to exist first. With XF_SCSI = 0
|
||||
; this brings up nothing and the host is the transport (FINDINGS 55).
|
||||
bsr xf_init
|
||||
bsr ring_init
|
||||
tst.l d0
|
||||
bpl.s noring
|
||||
@@ -335,3 +340,5 @@ desync: move.l #$EE,FLAG.l
|
||||
include "src/player/frame.i"
|
||||
include "src/player/clock.i"
|
||||
include "src/player/ring.i"
|
||||
include "src/player/scsi.i"
|
||||
include "src/player/xfer.i"
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
; ---------------------------------------------------------------- xfer.i
|
||||
; THE TRANSPORT BEHIND THE MAILBOX. ROADMAP P4b.
|
||||
;
|
||||
; src/player/ring.i has always ended at a seam: it decides which record to
|
||||
; fetch, where in the ring to put it and when that is safe, writes those four
|
||||
; words into XF_SLOT and bumps XF_GO, and then polls XF_ACK. On the other side
|
||||
; of that seam, until now, was tools/bench/stream.lua -- a host moving bytes at
|
||||
; a MODELLED rate, with XF_ACK synthesised out of emulated time. A player has
|
||||
; no host. This file is the other side: XF_GO is answered by src/player/scsi.i
|
||||
; issuing a real READ(10) to a real MB89352, and XF_ACK is a word the 68000
|
||||
; bumps when the bytes have landed.
|
||||
;
|
||||
; NOTHING ABOVE THE SEAM CHANGED, and that is deliberate for the same reason it
|
||||
; was in sessions 22 and 23: ring.i cannot tell which transport answered it, so
|
||||
; a green run here is a test of THIS file rather than of a new producer. The
|
||||
; two hooks in ring.i (one in ring_poll, one in ring_seek's quiet-wait) are the
|
||||
; whole of the change on that side, and with XF_SCSI = 0 they are a tst and a
|
||||
; branch.
|
||||
;
|
||||
; IT IS SYNCHRONOUS, AND THAT IS NOT A SHORTCUT -- IT IS THE FINDING. The
|
||||
; modelled transport overlapped: a request issued at time t landed at t + len/
|
||||
; rate while the 68000 got on with decoding, which is what a DMAC channel does.
|
||||
; Here the CPU moves every byte itself through $EA0015 (57.3: a PIO write to
|
||||
; that address is discarded, so even "PIO" runs the SPC in DMA mode with the CPU
|
||||
; standing in for the channel), so `bsr xf_service` does not start a transfer,
|
||||
; it PERFORMS one. A two-deep request queue therefore buys nothing at all: the
|
||||
; second slot is filled and drained by the same instruction stream that would
|
||||
; have been decoding. FINDINGS 55.3's whole result -- that a one-deep queue
|
||||
; gives away 6.8% of the pipe -- is about a transport that runs in parallel with
|
||||
; the CPU, and this one does not.
|
||||
;
|
||||
; So what this file is FOR is not to be the shipping transport. It is to make
|
||||
; the shipping transport's cost measurable: the same 120 pixel-exact frames,
|
||||
; delivered by the machine off a real volume, with the CPU cost of doing it
|
||||
; charged where a rate model cannot hide it. P4a -- the HD63450 holding the bus
|
||||
; -- is what makes the transfer overlap again, and until it exists this is the
|
||||
; honest floor.
|
||||
|
||||
; ---- state. Above src/player/ring.i's instruments (last: SK_WAIT at $1837C)
|
||||
; and below the disc-offset table at $19400.
|
||||
XF_SCSI = $18380 ; 1 = the 68000 is the transport (input)
|
||||
XS_LBA0 = $18384 ; LBA of byte 0 of the scene's frame stream
|
||||
XS_NXFER = $18388 ; transfers completed
|
||||
XS_NBYTE = $1838C ; record bytes delivered into the ring
|
||||
XS_NWIRE = $18390 ; bytes actually read off the disc, sectors and
|
||||
; all -- the two differ and 58.3 is why
|
||||
XS_ERR = $18394 ; SC_ERR of the FIRST failure, 0 = none
|
||||
XS_ERRAT = $18398 ; ...and the request index it failed on
|
||||
|
||||
; ---------------------------------------------------------------- xf_init
|
||||
; Clears the instruments and brings the SPC up, if this run has one. Called
|
||||
; before ring_init, because ring_init ends in a ring_seek and a seek waits on
|
||||
; the transport.
|
||||
xf_init:
|
||||
clr.l XS_NXFER.l
|
||||
clr.l XS_NBYTE.l
|
||||
clr.l XS_NWIRE.l
|
||||
clr.l XS_ERR.l
|
||||
move.l #-1,XS_ERRAT.l
|
||||
tst.l XF_SCSI.l
|
||||
beq.s .out
|
||||
bsr scsi_init
|
||||
.out: rts
|
||||
|
||||
; ---------------------------------------------------------------- xf_service
|
||||
; Answer at most ONE outstanding request, then return. Preserves every
|
||||
; register: it is called from inside ring_poll, which is itself called from
|
||||
; inside the decoder's wait loops and must be invisible to them.
|
||||
;
|
||||
; ONE PER CALL, not "drain the queue". ring_poll retires exactly one completed
|
||||
; request per call as well, and a transport that answered both queued requests
|
||||
; in one visit would hand the retire loop two acks it can only take one poll at
|
||||
; a time -- which is legal, but it also means the decoder's wait loop would
|
||||
; disappear for two record times instead of one. One per call keeps the two
|
||||
; sides stepping at the same rate.
|
||||
;
|
||||
; A RECORD IS NOT A SECTOR, and this is where that is dealt with. ring.i asks
|
||||
; for a byte offset and a length; the target answers in 512 B blocks. So the
|
||||
; command covers the sectors the record lies in, and SC_WSKIP/SC_WKEEP tell
|
||||
; src/player/scsi.i's DATA IN loop which of those bytes to store. The ones
|
||||
; outside the window are pulled from the FIFO and dropped -- they are NOT
|
||||
; written past the ends of the destination, because the bytes on either side of
|
||||
; a record in the stream belong to records the decoder may still be reading and
|
||||
; the block loop has no bounds check (49.2).
|
||||
;
|
||||
; XS_NWIRE counts what the disc actually moved and XS_NBYTE what the ring got.
|
||||
; They are not the same number and the gap is a delivery cost, not an accounting
|
||||
; detail: it is bytes on the wire that no frame contains.
|
||||
xf_service:
|
||||
tst.l XF_SCSI.l
|
||||
beq.s .idle
|
||||
move.l XF_ACK.l,d0
|
||||
cmp.l XF_GO.l,d0
|
||||
bcs.s .work ; XF_ACK < XF_GO: something outstanding
|
||||
.idle: rts
|
||||
.work:
|
||||
movem.l d0-d7/a0-a2,-(sp)
|
||||
move.l XF_ACK.l,d0
|
||||
move.l d0,d1
|
||||
and.l #XF_SLM,d1
|
||||
lsl.l #4,d1 ; * XF_SLSZ
|
||||
lea XF_SLOT.l,a0
|
||||
adda.l d1,a0
|
||||
move.l (a0),d1 ; disc byte offset within the stream
|
||||
movea.l 4(a0),a1 ; destination in the ring
|
||||
move.l 8(a0),d2 ; length
|
||||
; ---- sector arithmetic
|
||||
move.l d1,d3
|
||||
and.l #511,d3 ; bytes of the first sector to drop
|
||||
move.l d1,d4
|
||||
lsr.l #8,d4
|
||||
lsr.l #1,d4 ; sector index within the stream
|
||||
add.l XS_LBA0.l,d4 ; ...and where the stream begins
|
||||
move.l d3,d5
|
||||
add.l d2,d5
|
||||
addi.l #511,d5
|
||||
lsr.l #8,d5
|
||||
lsr.l #1,d5 ; sectors the record lies in
|
||||
move.l d3,SC_WSKIP.l
|
||||
move.l d2,SC_WKEEP.l
|
||||
add.l d2,XS_NBYTE.l
|
||||
move.l d5,d0
|
||||
lsl.l #8,d0
|
||||
lsl.l #1,d0
|
||||
add.l d0,XS_NWIRE.l
|
||||
move.l d4,d3 ; d3 = LBA
|
||||
move.l d5,d4 ; d4 = blocks
|
||||
bsr scsi_read_win
|
||||
tst.l d0
|
||||
bmi.s .err
|
||||
addq.l #1,XS_NXFER.l
|
||||
addq.l #1,XF_ACK.l ; LAST: the bytes are all in the ring
|
||||
; before the request is called done
|
||||
movem.l (sp)+,d0-d7/a0-a2
|
||||
rts
|
||||
; ---- a failed read is NOT acked. The record never becomes resident, the
|
||||
; decoder spins out in waitrec and reports a stalled producer, and XS_ERR says
|
||||
; which request failed and why. Acking a failed transfer would publish a
|
||||
; descriptor for a buffer full of whatever was there before -- and the decoder
|
||||
; would find a plausible-looking length word in it and desync somewhere else
|
||||
; entirely.
|
||||
.err:
|
||||
tst.l XS_ERR.l
|
||||
bne.s .err2
|
||||
move.l SC_ERR.l,XS_ERR.l
|
||||
move.l XF_ACK.l,XS_ERRAT.l
|
||||
.err2: movem.l (sp)+,d0-d7/a0-a2
|
||||
rts
|
||||
Reference in New Issue
Block a user