Put the ring on the 68000, and find the disc stops whenever the player is not asking
ROADMAP P5. The loader moved in session 21 and the frame clock in 22; the ring producer was the last policy living outside the machine. src/player/ring.i does `aligned` placement, the descriptor ring, a prefill, 51.2's slack rule and a seek, and the host keeps only the transport. It needed a container change. `aligned` asks whether the next record fits before the end of the ring -- a length asked BEFORE the record is fetched -- and every reader in this tree answered that by walking the frame stream, which is exactly what a player streaming off a disc cannot do. DLX4 carries nframes u16 record lengths in the scene header. Frame payloads are byte-identical to the DLX3 encode, so no fitted constant moves; the scene header goes 5,920 to 6,164 B. The producer reproduces the host's tiling exactly: 18 wraps, 14.7 KB mean hole, pixel-exact, a third independent implementation of the same policy. What it exposed is bigger than the item. A channel only moves bytes while it has a request and only the CPU can issue one, so the disc stands still between records by an amount the PLAYER sets, not the medium -- and no host-filled run could see it. At 488 KB/s in a 256 KB ring a one-deep request queue gives away 6.8% of the pipe and underruns 59 of 120 frames; two-deep gives away 3.4% and underruns none. The container's whole surplus over the wire is 8.7%, so the player's own loop was spending most of the slack a branch point saves up. Prefill is the weaker lever: six records of it still leaves 24 underruns. Three silent bugs are recorded in FINDINGS 55.7 -- all produced wrong pixels or a desync rather than a fault -- plus a rig one: MAME renders a screen line by line, so snapshotting the frame the decoder finished in captures a tear that reads exactly like a decoder bug. check.sh gains the machine-owned ring and a seek with the decode after it. decode.bin is unchanged at 1,296 B and a host-filled run executes none of the new code, so every FINDINGS 49/51 figure stands. ALL GREEN before and after. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
+13
-2
@@ -63,11 +63,22 @@ GPAL = $E82000 ; graphics palette, 256 words
|
||||
; 4 the scratch tables. A player builds the tables ONCE at boot (they
|
||||
; describe the hardware's colour rendering and nothing about the scene) and
|
||||
; then loads each scene with 3.
|
||||
; out: d0 = 0 ok, -1 not a DLX3 container. a0-a4 clobbered, a5 = base.
|
||||
; out: d0 = 0 ok, -1 not a DLX3/DLX4 container. a0-a4 clobbered, a5 = base.
|
||||
;
|
||||
; The magic is accepted as 'DLX' plus a version byte of '3' OR ABOVE rather than
|
||||
; as one constant. DLX4 (ROADMAP P5) adds the per-record index and a fifth
|
||||
; header offset at +32; every field this routine reads is at its DLX3 place, so
|
||||
; the transforms are version-independent and the check should be too. A version
|
||||
; this loader has never seen is still refused -- '3' or above, not "anything
|
||||
; that begins DLX".
|
||||
do_load:
|
||||
movea.l a0,a5
|
||||
cmpi.l #$444C5833,(a5) ; 'DLX3'
|
||||
move.l (a5),d0
|
||||
andi.l #$FFFFFF00,d0
|
||||
cmpi.l #$444C5800,d0 ; 'DLX'
|
||||
bne .bad
|
||||
cmpi.b #'3',3(a5) ; ... version 3 or above
|
||||
bcs .bad
|
||||
move.w 12(a5),d0
|
||||
ext.l d0
|
||||
move.l d0,LK1.l
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
; ---------------------------------------------------------------- ring.i
|
||||
; The RING PRODUCER, on the 68000. ROADMAP P5.
|
||||
;
|
||||
; WHAT MOVED. FINDINGS 49 and 51 measured a ring that a HOST filled:
|
||||
; tools/bench/stream.lua held the record index, decided where each record went,
|
||||
; wrote the descriptor and advertised it. The 68000 only ever consumed. That
|
||||
; is the same shape session 21 found in the loader and session 22 in the frame
|
||||
; clock -- a policy living outside the machine that has to run inside it -- and
|
||||
; it is the last one in the delivery path. A player has no host to place its
|
||||
; records.
|
||||
;
|
||||
; So the placement policy is here now, and the host keeps only the part that is
|
||||
; genuinely not the CPU's: moving bytes off a disc at a rate. What the rig
|
||||
; supplies is a TRANSPORT, one request at a time, which is what a single SPC and
|
||||
; one DMAC channel are (FINDINGS 52.5); what this file supplies is every
|
||||
; decision about WHICH record, WHERE in the ring, and WHEN it is safe to start.
|
||||
;
|
||||
; THE POLICY IS `aligned`, and it is the same one 19_ring_stream.py scored and
|
||||
; 49.3 chose: never start a record that will not finish before the end of the
|
||||
; ring; leave the hole, restart at the base. It costs a mean hole of 5.7% of
|
||||
; the ring on the gate container and ZERO clocks in the block loop, against
|
||||
; `split`'s 3.64% of every frame budget forever.
|
||||
;
|
||||
; WHY IT NEEDS AN INDEX, and why that is a container change (DLX4). `aligned`
|
||||
; asks "does the NEXT record fit before the end of the ring", which is a
|
||||
; question about a record's length asked BEFORE it is fetched. Every reader in
|
||||
; this tree learned record lengths by walking the frame stream -- reading each
|
||||
; record's length word to find the next -- and that is exactly what a player
|
||||
; streaming off a disc cannot do: the length word of record i+1 is one of the
|
||||
; bytes it has not fetched yet. DLX4 puts nframes u16 longword-counts in the
|
||||
; scene header for this, and the same table gives a branch point the disc
|
||||
; address of an arbitrary record without reading what lies between (`ring_seek`).
|
||||
;
|
||||
; THE HANDSHAKE WITH THE DECODER IS UNCHANGED, deliberately. FR_HEAD/FR_TAIL/
|
||||
; DESC[] are the same words src/player/stream.s already reads, written in the
|
||||
; same order, so the decoder cannot tell a host-filled ring from a self-filled
|
||||
; one -- which is what makes the self-filled run a test of THIS file and not of
|
||||
; a new rig. Two monotonic counters, single reader, single writer, no atomics.
|
||||
;
|
||||
; THE TRANSPORT MAILBOX, XF_QD outstanding requests:
|
||||
; XF_SLOT[seq & 1] what to fetch, where to put it, how much, and which
|
||||
; record it is
|
||||
; XF_GO requests issued -- bumped LAST, after the slot
|
||||
; XF_ACK requests completed, in order, by the transport
|
||||
; In the player XF_* is an MB89352 command and a DMAC channel; here it is
|
||||
; tools/bench/stream.lua delivering at a modelled rate. Either way the CPU
|
||||
; issues and polls, and the bytes arrive on somebody else's time.
|
||||
;
|
||||
; AND THAT IS WHERE THE COST IS. The channel only moves bytes while a request
|
||||
; is outstanding, and only the CPU can issue the next one. Between the
|
||||
; completion of record i and the issue of record i+1 the disc is IDLE, and the
|
||||
; length of that gap is a property of the PLAYER's loop, not of the medium.
|
||||
; `ring_poll` is therefore called from the pace wait -- the idle the frame clock
|
||||
; already creates -- rather than once a frame: once a frame would cap the fill
|
||||
; at one record per slot, which is the wire rate exactly, and a ring that can
|
||||
; only just keep up can never accumulate the slack a branch point spends
|
||||
; (51.3). A frame that uses its whole slot does not merely present late
|
||||
; (54.4); it stops the disc for a frame time. The rig counts that gap.
|
||||
|
||||
; ---- transport mailbox. TWO REQUEST SLOTS, and the depth is a knob.
|
||||
; A channel only moves bytes while it has a request, and only the CPU can give
|
||||
; it one. With ONE slot the disc stands still from the moment a transfer
|
||||
; completes until the player next polls -- and a player polls in its idle, which
|
||||
; is the end of a frame slot, so the gap is up to a whole frame's decode. With
|
||||
; TWO the next request is already queued when the current one lands and the
|
||||
; channel need never stop. XF_QD selects which, so the cost of the first is
|
||||
; measurable against the second in one rig rather than argued about.
|
||||
XF_SLOT = $18300 ; 2 x 16 B: u32 disc offset, u32 destination,
|
||||
; u32 length, u32 record index
|
||||
XF_SLSZ = 16
|
||||
XF_SLM = 1 ; slot = sequence & XF_SLM
|
||||
XF_GO = $18320 ; u32 requests ISSUED, written by the 68000
|
||||
XF_ACK = $18324 ; u32 requests COMPLETED, written by transport
|
||||
XF_QD = $18328 ; u32 queue depth, 1 or 2 (input)
|
||||
|
||||
; ---- producer state
|
||||
RINGOWN = $1832C ; 1 = the 68000 owns placement (this file)
|
||||
RNG_B = $18330 ; ring base address
|
||||
RNG_SZ = $18334 ; ring size in bytes
|
||||
IDX_B = $18338 ; base of the DLX4 record index, nframes u16
|
||||
RQ_NEXT = $1833C ; next record to REQUEST
|
||||
WCUR = $18340 ; write cursor, a ring OFFSET
|
||||
RCUR = $18344 ; read cursor: ring offset of the oldest record
|
||||
; the decoder has not finished with
|
||||
RTAILN = $18348 ; records RCUR has stepped over; chases FR_TAIL
|
||||
NRETIRE = $1834C ; requests this file has published; chases XF_ACK
|
||||
DOFF = $18350 ; running disc offset of record RQ_NEXT
|
||||
; ---- instruments. None of these is read by the policy.
|
||||
N_HOLE = $18354 ; wraps that left a hole
|
||||
N_HOLEB = $18358 ; total bytes in those holes
|
||||
N_FULL = $1835C ; polls that refused for SPACE (ring-bound)
|
||||
N_POLL = $18360 ; ring_poll calls
|
||||
N_ISSUE = $18364 ; requests issued
|
||||
SLK_MIN = $18368 ; least slack seen at a frame start, in records
|
||||
SLK_AT = $1836C ; and the frame it was seen at
|
||||
PF_REC = $18370 ; prefill target, in whole records (input)
|
||||
PF_DONE = $18374 ; records resident when the prefill released
|
||||
N_SEEK = $18378 ; ring_seek calls
|
||||
SK_WAIT = $1837C ; polls spent waiting for the channel to go
|
||||
; quiet before the last seek could start
|
||||
ROFF = $19400 ; u32 per record: disc offset, built at load.
|
||||
; The seek half of the index -- a running sum
|
||||
; is enough to PLAY, but a branch point needs
|
||||
; record j's address without summing to it.
|
||||
ROFFMAX = 1024 ; entries; $19400..$1A400, below CB1 at $20000
|
||||
|
||||
RPOLLMAX = 4000000 ; ring_poll calls with no progress before the
|
||||
; producer is declared wedged
|
||||
|
||||
; ---------------------------------------------------------------- ring_init
|
||||
; in: IDX_B, RNG_B, RNG_SZ, NFR set by the caller.
|
||||
; out: d0 = 0 ok, -1 the index is longer than ROFF can hold. Builds the disc
|
||||
; offset table and leaves the ring empty at record 0.
|
||||
; Clobbers d0-d2/a0-a1.
|
||||
ring_init:
|
||||
move.l NFR.l,d0
|
||||
cmpi.l #ROFFMAX,d0
|
||||
bhi .toobig
|
||||
movea.l IDX_B.l,a0
|
||||
lea ROFF.l,a1
|
||||
moveq #0,d1 ; running disc offset
|
||||
move.l d0,d2
|
||||
beq.s .noidx
|
||||
.sum: move.l d1,(a1)+
|
||||
moveq #0,d0
|
||||
move.w (a0)+,d0 ; longwords in this padded record
|
||||
lsl.l #2,d0
|
||||
add.l d0,d1
|
||||
subq.l #1,d2
|
||||
bne.s .sum
|
||||
.noidx:
|
||||
clr.l NRETIRE.l
|
||||
clr.l N_HOLE.l
|
||||
clr.l N_HOLEB.l
|
||||
clr.l N_FULL.l
|
||||
clr.l N_POLL.l
|
||||
clr.l N_ISSUE.l
|
||||
clr.l N_SEEK.l
|
||||
clr.l SK_WAIT.l
|
||||
clr.l XF_GO.l
|
||||
clr.l XF_ACK.l
|
||||
move.l #$7FFFFFFF,SLK_MIN.l
|
||||
move.l #-1,SLK_AT.l
|
||||
moveq #0,d0
|
||||
bsr ring_seek ; a scene starts with a seek to record 0
|
||||
moveq #0,d0
|
||||
rts
|
||||
.toobig:
|
||||
moveq #-1,d0
|
||||
rts
|
||||
|
||||
; ---------------------------------------------------------------- ring_seek
|
||||
; in: d0 = record index to play from.
|
||||
; out: the ring is empty, the cursors are at its base, and the next request
|
||||
; will be for record d0. FR_HEAD is reset; the CALLER must reset FR_TAIL
|
||||
; (it is the decoder's word, and this file never writes the decoder's).
|
||||
;
|
||||
; A SEEK CANNOT START WHILE THE CHANNEL IS BUSY. An outstanding request is
|
||||
; bytes already on their way to an address this routine is about to declare
|
||||
; free, so it is waited out and thrown away rather than cancelled -- a real
|
||||
; SPC would need the transfer aborted and the bus handed back before a new
|
||||
; command, and waiting is the version of that a rig can be honest about. What
|
||||
; it costs is up to one record's delivery time, charged to the seek, and
|
||||
; SK_WAIT counts the polls it took.
|
||||
; Clobbers d0-d2/a0.
|
||||
ring_seek:
|
||||
movem.l d0-d2/a0,-(sp)
|
||||
addq.l #1,N_SEEK.l
|
||||
clr.l SK_WAIT.l
|
||||
.wait: move.l XF_ACK.l,d1
|
||||
cmp.l XF_GO.l,d1
|
||||
beq.s .quiet
|
||||
addq.l #1,SK_WAIT.l
|
||||
bra.s .wait
|
||||
.quiet:
|
||||
move.l XF_GO.l,NRETIRE.l ; whatever landed belongs to the scene
|
||||
; we came FROM, and is discarded
|
||||
move.l d0,RQ_NEXT.l
|
||||
lsl.l #2,d0
|
||||
lea ROFF.l,a0
|
||||
move.l (a0,d0.l),DOFF.l ; the index's second job: record j's
|
||||
; disc address without reading to it
|
||||
clr.l WCUR.l
|
||||
clr.l RCUR.l
|
||||
clr.l RTAILN.l
|
||||
clr.l FR_HEAD.l
|
||||
movem.l (sp)+,d0-d2/a0
|
||||
rts
|
||||
|
||||
; ---------------------------------------------------------------- ring_poll
|
||||
; Advance the producer by at most one step: retire a completed request, catch
|
||||
; the read cursor up with the decoder, and issue the next request if one fits.
|
||||
; Preserves every register -- it is called from inside the decoder's wait loops
|
||||
; and must be invisible to them.
|
||||
ring_poll:
|
||||
movem.l d0-d3/a0-a1,-(sp)
|
||||
addq.l #1,N_POLL.l
|
||||
|
||||
; ---- 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
|
||||
; src/player/stream.s reads them the other way round.
|
||||
move.l NRETIRE.l,d3
|
||||
.retire:
|
||||
cmp.l XF_ACK.l,d3
|
||||
bcc.s .retired ; d3 >= XF_ACK: nothing new has landed
|
||||
move.l d3,d0
|
||||
and.l #XF_SLM,d0
|
||||
lsl.l #4,d0 ; * XF_SLSZ
|
||||
lea XF_SLOT.l,a1
|
||||
adda.l d0,a1 ; a1 = the completed request's slot
|
||||
move.l 12(a1),d0 ; its record index
|
||||
lsl.l #2,d0
|
||||
and.w #DESCM,d0
|
||||
lea DESC.l,a0
|
||||
move.l 4(a1),(a0,d0.w) ; its destination -> the descriptor
|
||||
addq.l #1,FR_HEAD.l ; ...advertised only after the address
|
||||
addq.l #1,d3
|
||||
bra.s .retire
|
||||
.retired:
|
||||
move.l d3,NRETIRE.l
|
||||
|
||||
; ---- 2. catch the read cursor up. The decoder publishes FR_TAIL and nothing
|
||||
; else the producer needs: RCUR walks the SAME placement rule the writer
|
||||
; used, so it steps over the holes in exactly the places they were left.
|
||||
; That is what makes the free space a single circular gap rather than a
|
||||
; list of live records -- the host producer kept a list because it could
|
||||
; afford to.
|
||||
; THE RULE MUST BE THE WRITER'S, APPLIED TO THE SAME RECORD. Stepping
|
||||
; the reader past record i lands on the END of record i, which is where
|
||||
; record i+1 went only if i+1 FITTED there -- and if it did not, the writer
|
||||
; put it at the ring base and left a hole. So the wrap is decided by the
|
||||
; length of the record being stepped ONTO, exactly as the placement was.
|
||||
;
|
||||
; Deciding it with the wrong record's length was a real bug and not a
|
||||
; conservative one: it left RCUR pointing into the hole, and one more
|
||||
; retirement then pushed it past the end of the ring and wrapped it to a
|
||||
; low address unrelated to any record. The live span computed from that is
|
||||
; SHORTER than the truth, so the producer places on top of a record the
|
||||
; decoder has not finished, and the symptom is a bitstream desync -- the
|
||||
; decoder's a0 walking off the end of a record that changed underneath it.
|
||||
move.l RTAILN.l,d1
|
||||
.catch: cmp.l FR_TAIL.l,d1
|
||||
bcc.s .caught
|
||||
movea.l IDX_B.l,a0
|
||||
move.l d1,d0
|
||||
add.l d0,d0
|
||||
moveq #0,d2
|
||||
move.w (a0,d0.l),d2
|
||||
lsl.l #2,d2 ; length of the record being retired
|
||||
move.l RCUR.l,d0
|
||||
add.l d2,d0 ; d0 = one past its end
|
||||
addq.l #1,d1
|
||||
cmp.l NFR.l,d1
|
||||
bcc.s .last ; nothing follows it in this scene
|
||||
move.l d1,d2
|
||||
add.l d2,d2
|
||||
moveq #0,d3
|
||||
move.w (a0,d2.l),d3
|
||||
lsl.l #2,d3 ; length of the record after it
|
||||
add.l d0,d3
|
||||
cmp.l RNG_SZ.l,d3
|
||||
bls.s .last
|
||||
moveq #0,d0 ; it did not fit: the writer restarted
|
||||
; at the base, so the reader does too
|
||||
.last:
|
||||
move.l d0,RCUR.l
|
||||
bra.s .catch
|
||||
.caught:
|
||||
move.l d1,RTAILN.l
|
||||
|
||||
; ---- 3. issue, if there is anything left and it fits.
|
||||
move.l RQ_NEXT.l,d1
|
||||
cmp.l NFR.l,d1
|
||||
bcc .out ; whole scene requested
|
||||
; ---- MEASURED AGAINST WHAT HAS BEEN RETIRED, NOT WHAT HAS BEEN ACKED, and
|
||||
; the difference is a slot. A request's slot stays in use until this file
|
||||
; has read the record index and destination out of it -- which happens in
|
||||
; step 1 above, one poll later than the ack at the earliest. Gating on
|
||||
; XF_ACK let the CPU write a slot whose descriptor had not been published
|
||||
; yet: the transport had finished the transfer, the retire loop then read
|
||||
; the OVERWRITTEN slot, and DESC for that frame stayed zero. The decoder
|
||||
; duly decoded address zero and reported a bitstream desync.
|
||||
move.l XF_GO.l,d0
|
||||
sub.l NRETIRE.l,d0 ; slots still spoken for
|
||||
cmp.l XF_QD.l,d0
|
||||
bcc .out ; the queue is as deep as it may go
|
||||
move.l d1,d0
|
||||
add.l d0,d0
|
||||
movea.l IDX_B.l,a0
|
||||
moveq #0,d2
|
||||
move.w (a0,d0.l),d2
|
||||
lsl.l #2,d2 ; d2 = length to place
|
||||
; ---- WHERE IT GOES, AND WHETHER IT MAY. The live bytes are the circular
|
||||
; interval [RCUR, WCUR) -- oldest record the decoder has not finished with,
|
||||
; up to the write cursor -- so the FREE bytes are its complement, and a
|
||||
; record has to fit in ONE piece of it because the block loop reads with a
|
||||
; monotonically increasing a0 (49.2).
|
||||
;
|
||||
; There are three shapes and they are not symmetric, which is the trap:
|
||||
; empty the whole ring is free
|
||||
; RCUR <= WCUR live is one run; free is [WCUR, SZ) THEN [0, RCUR),
|
||||
; so a record that will not fit before the end may
|
||||
; restart at the base -- this is `aligned`, and the
|
||||
; skipped bytes are the hole
|
||||
; RCUR > WCUR LIVE is the one that wraps; free is only [WCUR, RCUR)
|
||||
; and the ring base is NOT ours -- a record that will
|
||||
; not fit must simply wait
|
||||
; Deciding the wrap from `WCUR + len > SZ` alone, before knowing which
|
||||
; shape it is, was the second bug in this file: in the third shape it
|
||||
; restarted at a base that was live and overwrote records the decoder had
|
||||
; not read, and the symptom was a bitstream desync rather than a fault.
|
||||
move.l WCUR.l,d3 ; d3 = candidate offset
|
||||
moveq #0,d0 ; d0 = hole bytes, if any
|
||||
move.l RQ_NEXT.l,d1
|
||||
cmp.l RTAILN.l,d1
|
||||
beq.s .isempty
|
||||
move.l RCUR.l,d1
|
||||
cmp.l d3,d1
|
||||
beq .full ; RCUR == WCUR and not empty: the ring
|
||||
; is completely full
|
||||
bhi.s .freehi
|
||||
; RCUR < WCUR: free is [WCUR, SZ) then [0, RCUR).
|
||||
move.l d3,d0
|
||||
add.l d2,d0
|
||||
cmp.l RNG_SZ.l,d0
|
||||
bls.s .nohole2 ; fits before the end of the ring
|
||||
move.l RCUR.l,d0
|
||||
cmp.l d2,d0
|
||||
bcs .full ; it will not fit at the base either
|
||||
move.l RNG_SZ.l,d0
|
||||
sub.l d3,d0 ; the hole `aligned` is about to leave.
|
||||
; Charged here, where the record is
|
||||
; actually PLACED, and not where the
|
||||
; wrap is decided: charging it at the
|
||||
; decision counts one hole per retry
|
||||
; while the decoder still owns the base,
|
||||
; which is every poll of a fast pipe,
|
||||
; and reported 105 wraps where there
|
||||
; are 18.
|
||||
moveq #0,d3
|
||||
bra.s .place
|
||||
.nohole2:
|
||||
moveq #0,d0
|
||||
bra.s .place
|
||||
.freehi:
|
||||
; RCUR > WCUR: the LIVE span wraps, so the only free run is [WCUR, RCUR).
|
||||
move.l d3,d0
|
||||
add.l d2,d0
|
||||
cmp.l RCUR.l,d0
|
||||
bhi .full
|
||||
moveq #0,d0
|
||||
bra.s .place
|
||||
.isempty:
|
||||
; Nothing live, so the whole ring is free and the live span is about to start
|
||||
; here. Moving RCUR is what keeps the invariant true across a drained ring;
|
||||
; without it the reader's cursor would still point at the last consumed record.
|
||||
move.l d3,d0
|
||||
add.l d2,d0
|
||||
cmp.l RNG_SZ.l,d0
|
||||
bls.s .enohole
|
||||
move.l RNG_SZ.l,d0
|
||||
sub.l d3,d0
|
||||
moveq #0,d3
|
||||
bra.s .esetr
|
||||
.enohole:
|
||||
moveq #0,d0
|
||||
.esetr:
|
||||
move.l d3,RCUR.l
|
||||
.place:
|
||||
tst.l d0
|
||||
beq.s .nohole
|
||||
addq.l #1,N_HOLE.l
|
||||
add.l d0,N_HOLEB.l
|
||||
.nohole:
|
||||
move.l XF_GO.l,d1
|
||||
and.l #XF_SLM,d1
|
||||
lsl.l #4,d1 ; * XF_SLSZ
|
||||
lea XF_SLOT.l,a1
|
||||
adda.l d1,a1
|
||||
move.l DOFF.l,(a1)
|
||||
move.l RNG_B.l,d0
|
||||
add.l d3,d0
|
||||
move.l d0,4(a1)
|
||||
move.l d2,8(a1)
|
||||
move.l RQ_NEXT.l,12(a1)
|
||||
add.l d2,d3
|
||||
move.l d3,WCUR.l
|
||||
add.l d2,DOFF.l
|
||||
addq.l #1,RQ_NEXT.l
|
||||
addq.l #1,N_ISSUE.l
|
||||
addq.l #1,XF_GO.l ; LAST: the three words above must be
|
||||
; visible before the request is
|
||||
bra.s .out
|
||||
.full:
|
||||
addq.l #1,N_FULL.l
|
||||
.out:
|
||||
movem.l (sp)+,d0-d3/a0-a1
|
||||
rts
|
||||
|
||||
; ---------------------------------------------------------------- ring_prefill
|
||||
; Fill until PF_REC whole records are resident, then return. THE POLICY, not a
|
||||
; convenience: the decoder must not be released at slack 1, because 51.2
|
||||
; measured that n resident records buy n-1 frame times of stall -- the last one
|
||||
; is spent covering the pipe's restart. Releasing at 1 therefore starts a scene
|
||||
; with a stall budget of zero, and the first hiccup is an underrun.
|
||||
;
|
||||
; It is also the ONLY place a player can buy lookahead cheaply. 51.3: slack is
|
||||
; accumulated out of `pipe - wire` over seconds of play, so a scene that starts
|
||||
; empty climbs for 4.83 s at 488 KB/s before it can afford a branch. Bytes
|
||||
; bought here are bought before the frame clock starts and cost nothing but the
|
||||
; wait -- which is the one moment in a scene when the decoder has nothing else
|
||||
; to do anyway.
|
||||
;
|
||||
; out: d0 = 0 ok, -1 the transport never delivered. PF_DONE = records resident.
|
||||
ring_prefill:
|
||||
movem.l d1-d2,-(sp)
|
||||
moveq #0,d1
|
||||
.loop: bsr ring_poll
|
||||
move.l FR_HEAD.l,d0
|
||||
sub.l FR_TAIL.l,d0
|
||||
cmp.l PF_REC.l,d0
|
||||
bcc.s .done
|
||||
addq.l #1,d1
|
||||
cmp.l #RPOLLMAX,d1
|
||||
bcs.s .loop
|
||||
movem.l (sp)+,d1-d2
|
||||
moveq #-1,d0
|
||||
rts
|
||||
.done: move.l d0,PF_DONE.l
|
||||
movem.l (sp)+,d1-d2
|
||||
moveq #0,d0
|
||||
rts
|
||||
|
||||
; ---------------------------------------------------------------- ring_slack
|
||||
; out: d0 = whole records resident and unconsumed.
|
||||
;
|
||||
; This is the number FINDINGS 51 spent a session establishing the meaning of,
|
||||
; and it is only worth what it is worth when the decoder is PACED: free-running,
|
||||
; the decoder outruns any pipe and the ring never backs up, so the difference is
|
||||
; a statement about earliness (49.7.2).
|
||||
ring_slack:
|
||||
move.l FR_HEAD.l,d0
|
||||
sub.l FR_TAIL.l,d0
|
||||
rts
|
||||
|
||||
; ---------------------------------------------------------------- ring_may_seek
|
||||
; in: d0 = the stall a branch would cost, in whole frame times.
|
||||
; out: d0 = 0 the ring can cover it, -1 it cannot. Z set on ok.
|
||||
;
|
||||
; THE RULE, from 51.2, measured and not assumed: n resident records buy n-1
|
||||
; frame times, because the record due immediately after the pipe restarts is
|
||||
; still arriving when its slot opens. A design that reads the resident count
|
||||
; as its stall budget is over by one record every time.
|
||||
;
|
||||
; What a player does with a `no` is not this file's business -- delay the
|
||||
; branch, take the outcome that needs no seek, or accept a late present -- but
|
||||
; it must be able to ASK, and until now the answer only existed in the rig's
|
||||
; log.
|
||||
ring_may_seek:
|
||||
move.l d1,-(sp)
|
||||
move.l FR_HEAD.l,d1
|
||||
sub.l FR_TAIL.l,d1
|
||||
beq.s .no ; nothing resident: the subtraction
|
||||
; below would wrap to $FFFFFFFF and an
|
||||
; unsigned compare would then answer YES
|
||||
; to any request, from an empty ring
|
||||
subq.l #1,d1 ; the restart record is not spendable
|
||||
cmp.l d0,d1
|
||||
bcs.s .no
|
||||
move.l (sp)+,d1
|
||||
moveq #0,d0
|
||||
rts
|
||||
.no: move.l (sp)+,d1
|
||||
moveq #-1,d0
|
||||
rts
|
||||
|
||||
; ---------------------------------------------------------------- ring_mark
|
||||
; Sample the slack at a frame boundary, for the instruments only.
|
||||
; in: d0 = frame number. Clobbers nothing.
|
||||
ring_mark:
|
||||
movem.l d0-d1,-(sp)
|
||||
move.l FR_HEAD.l,d1
|
||||
sub.l FR_TAIL.l,d1
|
||||
cmp.l SLK_MIN.l,d1
|
||||
bcc.s .out
|
||||
move.l d1,SLK_MIN.l
|
||||
move.l d0,SLK_AT.l
|
||||
.out: movem.l (sp)+,d0-d1
|
||||
rts
|
||||
+84
-2
@@ -94,6 +94,9 @@ CLKON = $1803C ; 1 = the 68000 paces ITSELF: src/player/clock.i
|
||||
; the host writing it. Needs PACEON=1; the gate
|
||||
; below cannot tell the two apart and must not.
|
||||
DESC = $18100 ; DESCN x u32, record base addresses
|
||||
; The producer's own words -- RINGOWN, the transport mailbox and its
|
||||
; instruments -- are in src/player/ring.i, at $18300 and up, clear of DESC's
|
||||
; 256 bytes.
|
||||
|
||||
DESCN = 64 ; power of two; the index is masked, not compared
|
||||
DESCM = (DESCN-1)*4 ; mask for a BYTE offset into DESC
|
||||
@@ -104,6 +107,18 @@ SPINMAX = 2000000 ; polls with no progress before giving up
|
||||
|
||||
org $10000
|
||||
start:
|
||||
; ---- the ring producer, if this run is asking the 68000 to fill its own ring
|
||||
; (ROADMAP P5, src/player/ring.i). It goes FIRST because it only builds tables
|
||||
; and touches no hardware: a run that cannot build them should not have armed an
|
||||
; interrupt source first.
|
||||
tst.l RINGOWN.l
|
||||
beq.s noring
|
||||
bsr ring_init
|
||||
tst.l d0
|
||||
bpl.s noring
|
||||
move.l #$E3,FLAG.l ; the record index is longer than ROFF
|
||||
bra hold
|
||||
noring:
|
||||
; ---- the frame clock, if this run is asking the 68000 to keep its own time.
|
||||
; It goes here rather than inside the frame loop because clk_init CLEARS PACE:
|
||||
; tick 0 has to be the instant the decoder was released, exactly as it is when
|
||||
@@ -125,7 +140,38 @@ outer:
|
||||
clr.l LATEFR.l
|
||||
clr.l LATEMAX.l
|
||||
move.l #-1,LATE1ST.l
|
||||
; ---- SEEK AND PREFILL. Every pass starts with a seek to record 0 -- which on
|
||||
; the first pass is just "start of scene" and on any later one is a REAL seek:
|
||||
; the channel has to go quiet, the ring is declared empty, and the whole
|
||||
; lookahead 51.3 says takes seconds of play to accumulate is thrown away and
|
||||
; rebuilt from the prefill up. That is the branch point rehearsed with the one
|
||||
; thing a rig can check afterwards -- the decode has to still be pixel-exact.
|
||||
;
|
||||
; The clock is REBASED here rather than at clk_init, because tick 0 must be the
|
||||
; instant the decoder is released and the prefill happens before that. Under a
|
||||
; host-written PACE this word belongs to the host, so it is only touched when
|
||||
; the 68000 is keeping its own time; ITER>1 therefore needs CLKON.
|
||||
tst.l RINGOWN.l
|
||||
beq.s noseek
|
||||
moveq #0,d0
|
||||
bsr ring_seek
|
||||
bsr ring_prefill
|
||||
tst.l d0
|
||||
bpl.s .pfok
|
||||
move.l #$E1,FLAG.l ; the transport never delivered
|
||||
bra hold
|
||||
.pfok:
|
||||
tst.l CLKON.l
|
||||
beq.s noseek
|
||||
clr.l PACE.l ; at most one tick is lost to a V-DISP
|
||||
; landing between the ISR and here
|
||||
noseek:
|
||||
frameloop:
|
||||
tst.l RINGOWN.l
|
||||
beq.s nomark
|
||||
move.l FR_TAIL.l,d0
|
||||
bsr ring_mark
|
||||
nomark:
|
||||
; ---- PACE GATE (FINDINGS 49.7.2, and it is the whole point of this session).
|
||||
; Free-running, this loop asks for record i the instant it finishes record i-1,
|
||||
; so it outruns any finite pipe, the ring NEVER backs up, and the producer's
|
||||
@@ -170,9 +216,9 @@ frameloop:
|
||||
beq.s nopace
|
||||
move.l PACE.l,d0
|
||||
cmp.l FR_TAIL.l,d0 ; PACE < FR_TAIL: the slot has not come
|
||||
bcs.s pacewait ; round yet, so this frame is EARLY
|
||||
bcs.s pacesel ; round yet, so this frame is EARLY
|
||||
tst.l FR_TAIL.l
|
||||
beq.s pacewait ; frame 0 starts AT tick 0 by definition
|
||||
beq.s pacesel ; frame 0 starts AT tick 0 by definition
|
||||
tst.l LATEFR.l
|
||||
bne.s .nf1
|
||||
move.l FR_TAIL.l,LATE1ST.l
|
||||
@@ -182,6 +228,25 @@ frameloop:
|
||||
cmp.l LATEMAX.l,d0 ; slot but with nothing left of it
|
||||
bls.s pacewait
|
||||
move.l d0,LATEMAX.l
|
||||
pacesel:
|
||||
; Both branches above -- the early frame and frame 0 -- come here rather than
|
||||
; jumping straight into the legacy wait, because the early frame is the COMMON
|
||||
; case and it is the one with idle in it. Routing it past this test was a real
|
||||
; bug and not a tidy-up: the producer then only ever ran from the record wait,
|
||||
; about once a frame, and the disc spent most of the scene stopped.
|
||||
tst.l RINGOWN.l
|
||||
beq.s pacewait
|
||||
; ---- THE IDLE IS WHERE THE DISC RUNS. ring_poll retires the completed
|
||||
; request and issues the next one, and this loop is the only place in a paced
|
||||
; player with time to spare. Polling once a FRAME instead would cap the fill at
|
||||
; one record per slot -- the wire rate exactly -- and a ring that can only keep
|
||||
; up never accumulates the slack a branch point spends (51.3).
|
||||
pacewaitR:
|
||||
bsr ring_poll
|
||||
move.l PACE.l,d0
|
||||
cmp.l FR_TAIL.l,d0
|
||||
bcs.s pacewaitR
|
||||
bra.s nopace
|
||||
pacewait:
|
||||
move.l PACE.l,d0
|
||||
cmp.l FR_TAIL.l,d0 ; d0 - FR_TAIL; carry = tick not reached
|
||||
@@ -191,6 +256,22 @@ nopace:
|
||||
; d1 counts polls for this frame; a nonzero d1 on exit means the frame stalled.
|
||||
moveq #0,d1
|
||||
move.l FR_TAIL.l,d2
|
||||
tst.l RINGOWN.l
|
||||
beq.s waitrec
|
||||
; ---- the same wait, with the producer inside it. Here d1 counts POLLS rather
|
||||
; than spins, so STALLS still means "frames that had to wait" and SPINS is not
|
||||
; comparable with a host-filled run's. A frame that waits here is a real
|
||||
; underrun either way: paced, its slot has already opened.
|
||||
waitrecR:
|
||||
bsr ring_poll
|
||||
move.l FR_HEAD.l,d0
|
||||
cmp.l d2,d0
|
||||
bhi gotrec
|
||||
addq.l #1,d1
|
||||
cmp.l #SPINMAX,d1
|
||||
bcs.s waitrecR
|
||||
move.l #$E1,FLAG.l
|
||||
bra hold
|
||||
waitrec:
|
||||
move.l FR_HEAD.l,d0
|
||||
cmp.l d2,d0
|
||||
@@ -253,3 +334,4 @@ desync: move.l #$EE,FLAG.l
|
||||
|
||||
include "src/player/frame.i"
|
||||
include "src/player/clock.i"
|
||||
include "src/player/ring.i"
|
||||
|
||||
Reference in New Issue
Block a user