Move the loader onto the 68000, and find 5,920 bytes nobody counted
src/player/load.i expands both codebooks to word-per-pixel form and packs the palette to GGGGGRRRRRBBBBBI out of the RAW container header, byte-exact against tools/bench/dlxload.py on both CPU cores. The palette half is gated on words read back out of the palette registers at $E82000, so "the words reached the hardware" is part of what passes. ROADMAP P1 is done; P2's encoder half (a reserved black entry, 23.4) is not, and is a re-encode rather than an edit. A scene change costs 18.96 ms of 68000 time, 22.8% of one 12 fps frame; boot costs 24.70 ms. The scratch tables describe the CRTC, not the scene, so pal_tables is a separate entry point built once at boot -- 5.29 ms off every scene change. The one that moves something: the scene header is 5,920 B that no rate table in this tree included, because it belongs to no frame record. In FINDINGS 51.3's currency it is divided by the surplus pipe - wire, so it is hypersensitive: 138 ms of extra refill climb at 488 KB/s and 1.099 s at 451.4 KB/s, for the same bytes. tools/analysis/22_scene_load.py prices it across explicit rates. Recorded as open: the two CPU cores agree to <3% on every stage but the table build, where they differ by 16.4%. px68k's C68K charges a flat 50 clocks for MULU/MULS (c68kmacro.h:1869) where the 68000 charges 38+2n, which explains 4,608 of the 8,703 clock gap. 4,095 clocks are unexplained. Nothing else in src/player/ multiplies, so no figure in FINDINGS 24-52 is affected. decode.s and stream.s are untouched; decode.bin is still 1,296 B at the same MD5. check.sh gains a stage that gates byte-exactness on both cores and deliberately does not gate the cycle counts -- MAME's clock is 1/55.46 s and a wall timing would make the green light host-sensitive. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
; ---------------------------------------------------------------------------
|
||||
; load.i -- the two LOAD-TIME transforms, on the 68000 itself. ROADMAP P1+P2.
|
||||
;
|
||||
; Until now both of these were done host-side, in tools/bench/dlxload.py, and
|
||||
; the rigs pushed the RESULT into emulated RAM. That was the right call while
|
||||
; the inner loop was the thing being measured -- charging a once-per-scene cost
|
||||
; to the per-frame path would have flattered or damned it for no reason -- but
|
||||
; a player has no host. These are the bytes that replace it.
|
||||
;
|
||||
; The reference is tools/bench/dlxload.py and it stays the reference: this code
|
||||
; is gated BYTE-FOR-BYTE against it (tools/bench/verify_load.py), palette words
|
||||
; and darkest-entry index included. If the two ever disagree, the symptom in a
|
||||
; rig would be wrong colours rather than a crash, which is exactly the class of
|
||||
; bug the split was made to prevent.
|
||||
;
|
||||
; WHAT IT READS. The RAW container as it comes off the disc. The DLX header is
|
||||
; fixed-layout and big-endian (tools/encoder/dlx.py):
|
||||
; +0 magic 'DLX3' +12 k1 u16 +16 off_pal u32
|
||||
; +4 W u16 +14 k4 u16 +20 off_cb1 u32
|
||||
; +6 H u16 +24 off_cb4 u32
|
||||
; +8 fps u16 +28 off_frm u32
|
||||
; +10 nframes u16
|
||||
; The three offsets are container-relative, so every one of them is an add of
|
||||
; the base the loader was handed. Nothing here parses a frame record.
|
||||
;
|
||||
; WHAT IT WRITES. CB1 (8 KB) and CB4 (2 KB) expanded to one WORD per pixel at
|
||||
; the addresses geom.i names, and 256 packed palette words straight into the
|
||||
; graphics palette at $E82000. It also reports the darkest entry, which is what
|
||||
; the letterbox is filled with until the encoder reserves a black one (23.4,
|
||||
; still open).
|
||||
;
|
||||
; WHY WORD-PER-PIXEL. The block loop movems codebook entries straight into
|
||||
; GVRAM with no unpacking, and the high byte of a GVRAM word write is discarded
|
||||
; by the hardware, so the high byte is left zero and never has to be cleared.
|
||||
; It also makes index scaling a shift rather than a multiply (lsl.w #5 / #3).
|
||||
;
|
||||
; SCRATCH. Three tables, built here and dead the moment the palette is packed:
|
||||
; P6TAB 64 B 6-bit level -> the 8-bit value the hardware renders it as
|
||||
; SQTAB 256 B the square of that, so the darkest-entry search has no muls
|
||||
; DTAB 512 B err(v, I=0) - err(v, I=1) per 8-bit channel value, signed
|
||||
; DTAB is what turns P2's per-entry minimum-squared-error choice of the shared
|
||||
; LSB into three table reads and a sign test. Choosing I per entry rather than
|
||||
; fixing it is worth 1.96 dB (FINDINGS 23.3), and it is a per-ENTRY decision
|
||||
; across three channels, so it cannot be folded into a per-channel table alone.
|
||||
; ---------------------------------------------------------------------------
|
||||
|
||||
LFLAG = $18040 ; 0 idle / 1 running / $FF done / $EE bad header
|
||||
LHDR = $18044 ; -> raw container base
|
||||
LDARK = $18048 ; <- index of the darkest palette entry
|
||||
LK1 = $1804C ; <- k1, as the 68000 read it out of the header
|
||||
LK4 = $18050 ; <- k4
|
||||
LMODE = $18054 ; bit0 codebooks, bit1 palette entries,
|
||||
; bit2 the three scratch tables
|
||||
LITER = $18058 ; repeat count, so a 55 Hz host clock can time it
|
||||
|
||||
P6TAB = $19000 ; 64 bytes
|
||||
SQTAB = $19040 ; 64 longs
|
||||
DTAB = $19140 ; 256 words
|
||||
GPAL = $E82000 ; graphics palette, 256 words
|
||||
|
||||
; ---------------------------------------------------------------- do_load
|
||||
; in: a0 = container base, d1 = mode bits: 1 codebooks, 2 palette entries,
|
||||
; 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.
|
||||
do_load:
|
||||
movea.l a0,a5
|
||||
cmpi.l #$444C5833,(a5) ; 'DLX3'
|
||||
bne .bad
|
||||
move.w 12(a5),d0
|
||||
ext.l d0
|
||||
move.l d0,LK1.l
|
||||
move.w 14(a5),d0
|
||||
ext.l d0
|
||||
move.l d0,LK4.l
|
||||
|
||||
btst #2,d1
|
||||
beq.s .notab
|
||||
move.l d1,-(sp)
|
||||
bsr pal_tables
|
||||
move.l (sp)+,d1
|
||||
.notab:
|
||||
btst #0,d1
|
||||
beq.s .nocb
|
||||
moveq #0,d2 ; the count is built as a LONG and the
|
||||
move.w 12(a5),d2 ; high word must not carry junk into it
|
||||
lsl.l #4,d2 ; k1 entries x 16 source bytes
|
||||
movea.l 20(a5),a0
|
||||
adda.l a5,a0
|
||||
lea CB1,a1
|
||||
bsr expand
|
||||
moveq #0,d2
|
||||
move.w 14(a5),d2
|
||||
lsl.l #2,d2 ; k4 entries x 4 source bytes
|
||||
movea.l 24(a5),a0
|
||||
adda.l a5,a0
|
||||
lea CB4,a1
|
||||
bsr expand
|
||||
.nocb:
|
||||
btst #1,d1
|
||||
beq.s .nopal
|
||||
bsr pal_pack
|
||||
.nopal:
|
||||
moveq #0,d0
|
||||
rts
|
||||
.bad: moveq #-1,d0
|
||||
rts
|
||||
|
||||
; ---------------------------------------------------------------- expand
|
||||
; One source byte -> one destination word, high byte zero.
|
||||
; in: a0 src, a1 dst, d2 = source byte count. Always a multiple of 4: CB1 is
|
||||
; k1*16 and CB4 is k4*4, so no remainder case can exist and none is written.
|
||||
; A junk high word here is not a slow path, it is a WRONG one: `lsr.l #2` walks
|
||||
; two of its bits down into the low word and the dbra count comes out long.
|
||||
expand:
|
||||
lsr.l #2,d2
|
||||
subq.l #1,d2 ; k<=256, so the count fits a dbra
|
||||
moveq #0,d0
|
||||
.e1: move.b (a0)+,d0
|
||||
move.w d0,(a1)+
|
||||
move.b (a0)+,d0
|
||||
move.w d0,(a1)+
|
||||
move.b (a0)+,d0
|
||||
move.w d0,(a1)+
|
||||
move.b (a0)+,d0
|
||||
move.w d0,(a1)+
|
||||
dbra d2,.e1
|
||||
rts
|
||||
|
||||
; ---------------------------------------------------------------- pal_tables
|
||||
; The three scratch tables. SCENE-INDEPENDENT, every one of them: they describe
|
||||
; how the CRTC renders a 5-bit channel plus the shared LSB, which is a property
|
||||
; of the machine. A player builds them once at boot and never again, which is
|
||||
; why they are a separate entry point rather than the head of pal_pack -- see
|
||||
; FINDINGS 53.3 for what that is worth.
|
||||
pal_tables:
|
||||
; -- P6TAB[x] = ((x<<2)|(x>>4)) & $FF, and SQTAB[x] = P6TAB[x]^2
|
||||
lea P6TAB,a0
|
||||
lea SQTAB,a1
|
||||
moveq #0,d1
|
||||
.p1: move.w d1,d0
|
||||
lsl.w #2,d0
|
||||
move.w d1,d2
|
||||
lsr.w #4,d2
|
||||
or.w d2,d0
|
||||
andi.w #$FF,d0
|
||||
move.b d0,(a0)+
|
||||
move.w d0,d2
|
||||
mulu d2,d2
|
||||
move.l d2,(a1)+
|
||||
addq.w #1,d1
|
||||
cmpi.w #64,d1
|
||||
bne.s .p1
|
||||
|
||||
; -- DTAB[v] = (render(v,0)-v)^2 - (render(v,1)-v)^2, signed
|
||||
lea P6TAB,a0
|
||||
lea DTAB,a1
|
||||
moveq #0,d1
|
||||
.p2: move.w d1,d2
|
||||
lsr.w #2,d2
|
||||
andi.w #$3E,d2 ; x0 = (v>>3)<<1
|
||||
moveq #0,d3
|
||||
move.b 0(a0,d2.w),d3
|
||||
sub.w d1,d3
|
||||
muls d3,d3
|
||||
moveq #0,d4
|
||||
move.b 1(a0,d2.w),d4
|
||||
sub.w d1,d4
|
||||
muls d4,d4
|
||||
sub.l d4,d3
|
||||
move.w d3,(a1)+
|
||||
addq.w #1,d1
|
||||
cmpi.w #256,d1
|
||||
bne.s .p2
|
||||
rts
|
||||
|
||||
; ---------------------------------------------------------------- pal_pack
|
||||
; 24-bit RGB -> GGGGGRRRRRBBBBBI, the shared LSB chosen per entry by minimum
|
||||
; squared error, written to the palette registers. Identical arithmetic to
|
||||
; dlxload.pack_palette, including its tie-breaks: I stays 0 when the two errors
|
||||
; are equal, and the darkest entry is the FIRST index at the minimum.
|
||||
; in: a5 = container base, and pal_tables already run.
|
||||
pal_pack:
|
||||
movea.l 16(a5),a0
|
||||
adda.l a5,a0 ; -> 256 x RGB888
|
||||
lea GPAL,a1
|
||||
lea DTAB,a2
|
||||
lea SQTAB,a4 ; P6TAB is not needed here: the rendered
|
||||
; value is only ever wanted SQUARED
|
||||
move.l #$7FFFFFFF,d6
|
||||
clr.l LDARK.l
|
||||
moveq #0,d7
|
||||
.p3: moveq #0,d1
|
||||
move.b (a0)+,d1 ; R
|
||||
moveq #0,d2
|
||||
move.b (a0)+,d2 ; G
|
||||
moveq #0,d3
|
||||
move.b (a0)+,d3 ; B
|
||||
move.w d1,d0
|
||||
add.w d0,d0
|
||||
move.w 0(a2,d0.w),d4
|
||||
move.w d2,d0
|
||||
add.w d0,d0
|
||||
add.w 0(a2,d0.w),d4
|
||||
move.w d3,d0
|
||||
add.w d0,d0
|
||||
add.w 0(a2,d0.w),d4 ; sum of err0-err1 over the three
|
||||
moveq #0,d5
|
||||
tst.w d4
|
||||
ble.s .p4
|
||||
moveq #1,d5 ; I=1 only when it is STRICTLY better
|
||||
.p4: lsr.w #3,d1 ; fR
|
||||
lsr.w #3,d2 ; fG
|
||||
lsr.w #3,d3 ; fB
|
||||
move.w d2,d4
|
||||
lsl.w #5,d4
|
||||
or.w d1,d4
|
||||
lsl.w #6,d4 ; (fG<<11)|(fR<<6)
|
||||
move.w d3,d0
|
||||
add.w d0,d0
|
||||
or.w d0,d4
|
||||
or.w d5,d4
|
||||
move.w d4,(a1)+ ; -> the palette register
|
||||
|
||||
add.w d1,d1 ; x = (f<<1)|I, per channel
|
||||
or.w d5,d1
|
||||
add.w d2,d2
|
||||
or.w d5,d2
|
||||
add.w d3,d3
|
||||
or.w d5,d3
|
||||
lsl.w #2,d1 ; SQTAB holds longs
|
||||
move.l 0(a4,d1.w),d0
|
||||
lsl.w #2,d2
|
||||
add.l 0(a4,d2.w),d0
|
||||
lsl.w #2,d3
|
||||
add.l 0(a4,d3.w),d0 ; squared distance from black
|
||||
cmp.l d6,d0
|
||||
bge.s .p5
|
||||
move.l d0,d6
|
||||
move.l d7,LDARK.l ; first index at the minimum wins
|
||||
.p5: addq.w #1,d7
|
||||
cmpi.w #256,d7
|
||||
bne .p3
|
||||
rts
|
||||
@@ -0,0 +1,38 @@
|
||||
; Front-end for the load-time transforms (ROADMAP P1+P2), for the rig.
|
||||
;
|
||||
; It is to load.i what decode.s is to frame.i: a timing and control wrapper that
|
||||
; does nothing the shipping player would not do, so that the bytes being
|
||||
; measured are the bytes that will ship. The player's own boot path will call
|
||||
; do_load once with the mode bits set to 3; this repeats it LITER times so a
|
||||
; host clock with 1/55.46 s granularity can time a job that takes milliseconds,
|
||||
; and splits it by LMODE so the codebook expansion and the palette pack can be
|
||||
; priced apart. A player calls do_load with mode 7 once at boot -- the three
|
||||
; scratch tables describe the machine, not the scene -- and with mode 3 at every
|
||||
; scene change after that.
|
||||
;
|
||||
; Repeating is honest here in a way it would not be for a frame: nothing in
|
||||
; do_load is temporally recursive. Pass n writes exactly what pass n-1 wrote,
|
||||
; over the top of it, out of the same source bytes.
|
||||
|
||||
include "src/player/geom.i"
|
||||
|
||||
org $10000
|
||||
start:
|
||||
move.l LMODE.l,d1
|
||||
move.l LITER.l,d3
|
||||
move.l #1,LFLAG.l ; timer starts here
|
||||
loop:
|
||||
movem.l d1/d3,-(sp)
|
||||
movea.l LHDR.l,a0
|
||||
bsr do_load
|
||||
movem.l (sp)+,d1/d3
|
||||
tst.l d0
|
||||
bne.s bad
|
||||
subq.l #1,d3
|
||||
bne.s loop
|
||||
move.l #$FF,LFLAG.l ; timer stops here
|
||||
hold: bra.s hold
|
||||
bad: move.l #$EE,LFLAG.l
|
||||
bra.s hold
|
||||
|
||||
include "src/player/load.i"
|
||||
Reference in New Issue
Block a user