The 68000 decoder draws pixel-exact frames, and does not fit
src/player/decode.s parses DLX1 and decodes straight into GVRAM. Verified pixel-exact over a 120-frame sequential run of the worst sustained window on the disc -- all four block modes, full temporal recursion, so the last frame is only right if all 120 were. In check.sh. It costs a mean of 81.7% of a 12fps frame budget, and 31% of frames exceed 100% (42% at scsi). CPU is now the binding constraint. FINDINGS 28. Three things that were believed and are not true: - The dual-display-path plan of FINDINGS 24.5/25.6 is incoherent. The compose path needs a RAM copy of the previous reconstruction; the direct path's selling point is that it keeps none. Mixing them shows stale pixels on 70 of 120 frames, worst frame 18.8% of the screen. Every coherent repair is dearer than not mixing, and 24.5's two figures were both copies with no decode in either, so there was never a crossover to find. One path ships, and the 96KB reference frame is gone. tools/analysis/10_pathmix_drift.py keeps the counterexample runnable; check.sh asserts it still reproduces. - The four block modes do not cost the same. V1 300, V4 448, RAW 400 cycles against the old model's flat 207.8. V4 is 25% of blocks and 50% of the cycles, and the mode decision charges it bytes it does not charge cycles for. tools/analysis/11_cpu_budget.py reproduces all four frames timed on the 68000 to within 1 point. Hand-derived timings agree to 0.5% on V1. - The container is big-endian but not aligned. Variable-length records laid end to end put frame 1's length field at an odd address, and move.l (a0)+ there is an address error: frame 0 decoded perfectly and then vectored into the IPL for 59 emulated seconds looking like a hang. Found by dumping PC, not by reading the source. Also: an all-V1 frame, the cheapest possible full redraw, is 110.5% of budget. No mode assignment fits a scene cut at 12fps. That one needs a decision, not a measurement. Next: charge cycles in the mode decision and bisect against 833,333 per frame, the way session 6 bisects lam against bytes -- but with no bucket, because a late frame cannot be banked. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
; DLX1 frame decoder for a stock 68000 @ 10MHz -- direct-to-GVRAM, single path.
|
||||
;
|
||||
; WHY ONE PATH. FINDINGS 24.5/25.6 specified two display paths chosen per
|
||||
; frame (compose-in-RAM-then-blit vs decode-direct-to-GVRAM, crossing at 70% of
|
||||
; blocks changed). That plan is incoherent: the compose path assembles a FULL
|
||||
; frame in RAM so its blit can be row-linear, and the pixels it does not decode
|
||||
; this frame -- the SKIP blocks -- can only come from a RAM copy of the previous
|
||||
; reconstruction, which the direct path deliberately never writes. Every direct
|
||||
; frame invalidates the next compose frame's reference. Measured on the Singe
|
||||
; window: 70 of 120 frames display stale pixels, worst frame 18.8% of the
|
||||
; screen. See tools/analysis/10_pathmix_drift.py and FINDINGS 28.
|
||||
;
|
||||
; Every coherent version of the mix is worse than plain direct on the median,
|
||||
; so this decoder implements direct only. It also drops the 96KB RAM reference
|
||||
; frame entirely: the previous frame is already in GVRAM, so SKIP is genuinely
|
||||
; free -- no read, no write, just a pointer advance.
|
||||
;
|
||||
; GEOMETRY (tools/bench/crtc_mode.lua). 256-colour page: one pixel per WORD of
|
||||
; CPU address space, 1024-byte line stride, picture in rows 32..223. A 4x4
|
||||
; block is therefore 4 rows of 8 bytes at a 1024-byte stride, so displacements
|
||||
; 0/1024/2048/3072 all fit a 16-bit offset and one base pointer covers a block.
|
||||
; A block row is 4 picture rows = 4096 bytes; 64 blocks x 8 bytes = 512.
|
||||
;
|
||||
; The HIGH byte of every GVRAM word write is discarded by the hardware (MAME
|
||||
; 0.277 x68k_crtc.cpp:501, gvram_w case 0x0100), which is what makes the RAW
|
||||
; path cheap: it never has to clear the odd bytes it builds.
|
||||
;
|
||||
; CODEBOOKS are pre-expanded to word-per-pixel form by the loader, so the inner
|
||||
; loop movems them straight out with no unpacking:
|
||||
; CB1 entry = 16 words, row-major = 32 bytes (k1=256 -> 8KB)
|
||||
; CB4 entry = 4 words, row-major (2x2) = 8 bytes (k4=256 -> 2KB)
|
||||
; Index scaling is therefore a shift, not a multiply: lsl.w #5 and lsl.w #3.
|
||||
;
|
||||
; REGISTERS are fully committed -- a0 payload, a1 mode header, a2/a3 codebooks,
|
||||
; a4 block cursor, a5 block-row end, a6 block-row base, d0-d7 the 32-byte block
|
||||
; transfer. Nothing survives a block, which is why each block re-reads its mode
|
||||
; from (a1) rather than holding the packed byte in a register. That re-read is
|
||||
; paid only by blocks that are NOT all-SKIP: a header byte of zero clears four
|
||||
; blocks with one tst.b, and SKIP is the median block.
|
||||
;
|
||||
; ALIGNMENT. Frame records are [u32 length][768-byte mode header][payload] laid
|
||||
; end to end, and payload lengths are arbitrary -- so record boundaries land on
|
||||
; odd addresses, and `move.l (a0)+,d0` on an odd address is an ADDRESS ERROR on
|
||||
; a 68000, not a slow read. It killed the first run: frame 0 decoded perfectly,
|
||||
; then the length read for frame 1 at $03220F vectored into the IPL. Being
|
||||
; big-endian is only half of what "the 68000 reads it with a plain move" needs.
|
||||
; This decoder rounds each record start up to 4; the container itself should
|
||||
; carry the padding so a streaming player can DMA records straight into place.
|
||||
; FINDINGS 28.3.
|
||||
|
||||
FLAG = $18000 ; 0 idle / 1 running / $FF done / $EE desync
|
||||
ITER = $18008 ; outer repeat count, written by Lua
|
||||
NFR = $1800C ; frames per pass
|
||||
FPTR = $18010 ; -> first frame record
|
||||
SCR_N = $18014 ; frames remaining this pass
|
||||
SCR_END = $18018 ; expected end of the current payload
|
||||
|
||||
CB1 = $20000 ; expanded 4x4 codebook
|
||||
CB4 = $22000 ; expanded 2x2 codebook
|
||||
|
||||
DST0 = $C08000 ; GVRAM + 32*1024 (first picture row)
|
||||
DSTE = $C38000 ; GVRAM + 224*1024 (one past last)
|
||||
BROW = 4096 ; bytes per block row (4 picture rows)
|
||||
ROWLEN = 512 ; bytes per block row of blocks (64 * 8)
|
||||
MODEB = 768 ; packed mode header, 3072 blocks * 2 bits
|
||||
|
||||
org $10000
|
||||
start:
|
||||
move.l #1,FLAG.l ; timer starts here
|
||||
outer:
|
||||
move.l FPTR.l,a0
|
||||
move.l NFR.l,SCR_N.l
|
||||
frameloop:
|
||||
move.l (a0)+,d0 ; u32 payload length, big-endian
|
||||
lea 0(a0,d0.l),a1
|
||||
move.l a1,SCR_END.l ; where the payload must end
|
||||
move.l a0,a1 ; a1 = packed mode header
|
||||
lea MODEB(a0),a0 ; a0 = payload
|
||||
bsr decode_frame
|
||||
cmpa.l SCR_END.l,a0 ; bitstream desync is silent otherwise
|
||||
bne desync
|
||||
move.l a0,d0 ; next record starts on a 4-byte boundary
|
||||
addq.l #3,d0
|
||||
and.b #$FC,d0
|
||||
move.l d0,a0
|
||||
subq.l #1,SCR_N.l
|
||||
bne frameloop
|
||||
subq.l #1,ITER.l
|
||||
bne outer
|
||||
move.l #$FF,FLAG.l ; timer stops here
|
||||
hold: bra.s hold
|
||||
desync: move.l #$EE,FLAG.l
|
||||
bra.s hold
|
||||
|
||||
; ---------------------------------------------------------------- one block
|
||||
; \1 = right-shift needed to bring this block's 2 mode bits to bits 1-0.
|
||||
BLOCK macro
|
||||
move.b (a1),d0
|
||||
ifne \1
|
||||
lsr.b #\1,d0
|
||||
endc
|
||||
and.w #3,d0
|
||||
beq .sk\@ ; 00 SKIP -- the median block
|
||||
subq.w #1,d0
|
||||
beq .v1\@ ; 01 V1
|
||||
subq.w #1,d0
|
||||
bne .rw\@ ; 11 RAW, else 10 V4
|
||||
|
||||
; -- V4: four 2x2 codewords, sub-block order TL TR BL BR (vq_hybrid.paint)
|
||||
moveq #0,d0
|
||||
move.b (a0)+,d0
|
||||
lsl.w #3,d0
|
||||
movem.l (a3,d0.w),d0-d1
|
||||
move.l d0,(a4)
|
||||
move.l d1,1024(a4)
|
||||
moveq #0,d0
|
||||
move.b (a0)+,d0
|
||||
lsl.w #3,d0
|
||||
movem.l (a3,d0.w),d0-d1
|
||||
move.l d0,4(a4)
|
||||
move.l d1,1028(a4)
|
||||
moveq #0,d0
|
||||
move.b (a0)+,d0
|
||||
lsl.w #3,d0
|
||||
movem.l (a3,d0.w),d0-d1
|
||||
move.l d0,2048(a4)
|
||||
move.l d1,3072(a4)
|
||||
moveq #0,d0
|
||||
move.b (a0)+,d0
|
||||
lsl.w #3,d0
|
||||
movem.l (a3,d0.w),d0-d1
|
||||
move.l d0,2052(a4)
|
||||
move.l d1,3076(a4)
|
||||
bra .sk\@
|
||||
|
||||
; -- V1: one 4x4 codeword, 32 bytes, straight out of the expanded codebook
|
||||
.v1\@:
|
||||
moveq #0,d0
|
||||
move.b (a0)+,d0
|
||||
lsl.w #5,d0
|
||||
movem.l (a2,d0.w),d0-d7 ; EA is resolved before the load
|
||||
movem.l d0-d1,(a4)
|
||||
movem.l d2-d3,1024(a4)
|
||||
movem.l d4-d5,2048(a4)
|
||||
movem.l d6-d7,3072(a4)
|
||||
bra .sk\@
|
||||
|
||||
; -- RAW: 16 literal palette indices. Two indices are assembled into one long
|
||||
; via swap, so each pair of pixels costs one write instead of two; the high
|
||||
; byte of each word is left as zero because the hardware discards it anyway.
|
||||
.rw\@:
|
||||
RAWPAIR 0
|
||||
RAWPAIR 4
|
||||
RAWPAIR 1024
|
||||
RAWPAIR 1028
|
||||
RAWPAIR 2048
|
||||
RAWPAIR 2052
|
||||
RAWPAIR 3072
|
||||
RAWPAIR 3076
|
||||
.sk\@:
|
||||
addq.l #8,a4
|
||||
endm
|
||||
|
||||
RAWPAIR macro
|
||||
moveq #0,d0
|
||||
move.b (a0)+,d0
|
||||
swap d0
|
||||
move.b (a0)+,d0
|
||||
move.l d0,\1(a4)
|
||||
endm
|
||||
|
||||
; ------------------------------------------------------------- one frame
|
||||
; in: a0 = payload, a1 = packed mode header
|
||||
; out: a0 = one past the last payload byte consumed
|
||||
decode_frame:
|
||||
lea CB1,a2
|
||||
lea CB4,a3
|
||||
lea DST0,a6
|
||||
rowloop:
|
||||
move.l a6,a4
|
||||
lea ROWLEN(a6),a5
|
||||
byteloop:
|
||||
tst.b (a1) ; four SKIPs in one test
|
||||
beq allskip
|
||||
BLOCK 6
|
||||
BLOCK 4
|
||||
BLOCK 2
|
||||
BLOCK 0
|
||||
addq.l #1,a1
|
||||
cmpa.l a5,a4
|
||||
bne byteloop
|
||||
bra rowdone
|
||||
allskip:
|
||||
addq.l #1,a1
|
||||
lea 32(a4),a4
|
||||
cmpa.l a5,a4
|
||||
bne byteloop
|
||||
rowdone:
|
||||
lea BROW(a6),a6
|
||||
cmpa.l #DSTE,a6
|
||||
bne rowloop
|
||||
rts
|
||||
Reference in New Issue
Block a user