; --------------------------------------------------------------------------- ; 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/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 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 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