; --------------------------------------------------------------------------- ; packed.s -- THE PLAYER WITH NO DECODER, END TO END, OFF THE DISC. ROADMAP K3. ; ; WHAT THE OTHER FRONT-ENDS IN THIS DIRECTORY DO, AND WHY THIS ONE IS DIFFERENT. ; decode.s parses a DLX record and draws 4x4 blocks; stream.s does the same out ; of a bounded ring that ring.i fills through xfer.i. Both are CODECS: a record ; is a program the 68000 executes against GVRAM, and every cost table in ; docs/FINDINGS.md from 24 to 45 is a cost of executing it. FINDINGS 61 priced ; the alternative and it won: at the 9 clk/B dual-address floor the shipping ; codec is 110.4% of a 12 fps frame and a decoder-free packed literal frame is ; 55.2%. Decoding 37,585 bytes costs more than not decoding 49,152. ; ; So THERE IS NO DECODER BELOW. There is no ring either, and that is the same ; fact rather than a second one: a ring exists because DLX records are ; variable-length and the block loop needs the next one whole and contiguous ; (49.2). A packed record's length is GEOMETRY -- 512 B of palette and 192 rows ; of 256 B, 97 sectors exactly -- so record `i` is at LBA0 + i*97 and there is ; nothing to place, nothing to wrap and nothing to index (tools/encoder/dlxp.py). ; ; WHAT THE 68000 DOES PER FRAME, IN FULL: ; ; 1. wait for its own frame tick (src/player/clock.i, off the CRTC's V-DISP) ; 2. set CRTC R20 bit 11 -- the GVRAM write window ; 3. issue one READ(10) whose DATA IN phase is an HD63450 channel walking a ; 193-entry chain: the palette registers at $E82000, then 192 GVRAM rows a ; 1,024 B line stride apart (FINDINGS 62) ; 4. clear R20 bit 11 ; ; That is the whole video path. There is no per-frame PAINT: between the ; channel start and the channel's COC the 68000 executes nothing at all in the ; held configuration, and in the stealing one it executes only its own wait ; loop. The array is SCENE-CONSTANT -- the packed layout spends both 256-colour ; pages, so there is no page to flip and no destination that changes. ; ; THE TWO THINGS THIS FILE EXISTS TO FIND OUT, neither of which any earlier run ; could ask: ; ; a. DOES A CHAINED TRANSFER RUN BACK TO BACK AT 12 fps? 61.7.2 named this as ; the specific untested thing behind K3's large simplification, and it is ; the reason the frame clock is in here rather than a host tick: a chain ; that has to be restarted 120 times in ten seconds is a different claim ; from one that ran once in a gate. ; b. WHAT DOES HOLDING THE BUS COST THE CLOCK? A held channel halts the ; 68000, and the frame clock is an INTERRUPT off V-DISP. Edges that fall ; while the CPU is halted are not counted twice by the MFP -- the pending ; bit is one bit -- so a transfer long enough to span two V-DISPs makes the ; player's own clock LOSE TIME. Nothing in this project has run a transfer ; and a clock at once before, so nothing could have seen it. CLK_VDISP is ; the machine's count and the host's raster count is the truth; the gate ; compares them rather than trusting either. ; ; BOTH CONFIGURATIONS ARE SELECTABLE (PG_HELD) FOR EXACTLY THAT REASON. 59.3 ; already showed that an auto-requested channel is charged by TIME rather than ; by byte, so "held" and "stealing" are not two speeds of the same thing: held ; is the CPU stopped for as long as the record takes to arrive, and stealing is ; the CPU running against a channel taking its share. A player has to keep a ; clock, read a joystick and feed ADPCM, so which of the two is survivable is a ; design question and not a benchmark. ; ; AND IT IS STILL NOT A RATE. MAME's HD63450 is configured in wall-clock ; attotimes (42.5) and its held mode halts the CPU rather than charging it ; cycles per operand, so nothing below measures `W`. What it measures is ; whether the SHAPE works: one start, 193 destinations, 120 times, on a clock ; the machine keeps itself, with every frame pixel-exact off a real volume. ; --------------------------------------------------------------------------- ; ---- inputs, written by the rig before the CPU is launched PG_FLAG = $18900 ; 0 idle / 1 running / $FF done / $Exx failed PG_NFR = $18904 ; frames in the scene PG_FPS = $18908 ; frame rate the clock is asked for PG_LBA0 = $1890C ; LBA of record 0. A WORD AND NOT A CONSTANT: ; a shipping volume has a filesystem in front of ; the stream, and this is the one number that ; changes when it does (xfer.i says the same). PG_RECS = $18910 ; sectors in a record -- 97, and the container ; is what says so PG_PALL = $18914 ; 1 = the palette is LAST in the record. It is ; a CONTAINER property (dlxp.py flags bit 1) and ; the player reads it rather than assuming it: ; FINDINGS 62.5/63.4 priced both orders at ; -12.8 dB for one paint and could not choose ; between them, so K3 runs both. PG_HELD = $18918 ; 1 = DM_HELD_* (burst, bus held), 0 = DM_STEAL_* PG_PACEON = $1891C ; 1 = obey the frame clock. 0 free-runs, which ; tests the CHAIN without the clock in the way. PG_ITER = $18920 ; passes over the scene; >1 exercises the SEEK, ; which for this container is arithmetic PG_CADF = $18924 ; audio cadence: frames between one lump and the ; next, 0 in a silent container. A DLXP2 puts ; A sectors of ADPCM in front of every F records ; (65.3, dlxp.py) and a player that did not know ; would read the lump AS a record -- 7,168 B of ; audio painted across the top 28 rows of the ; screen, which is a picture, and a gate that ; only looked for errors would pass it. PG_CADA = $18928 ; ...and the sectors in a lump ; ---- outputs PG_SHOWN = $18930 ; frames displayed. Bumped AFTER bit 11 is ; cleared, so a rig that snapshots on a change ; is snapshotting a frame that is on screen. PG_ERR = $18934 ; SC_ERR of the first failed read, 0 = none PG_ERRAT = $18938 ; ...and the frame it failed on PG_LATE = $1893C ; frames that reached the gate with their tick ; already past -- the previous frame overran PG_LATE1 = $18940 ; the first of them PG_LATEM = $18944 ; the worst, in whole ticks PG_VDISP = $18948 ; CLK_VDISP as the machine counted it PG_VD0 = $1894C ; ...and as it stood when frame 0 started, so a ; rig can charge only the frames it ran PG_TSPIN = $18950 ; total DM_SPIN over the run: the CPU's own trips ; round the transfer wait. Held, this is one per ; frame and the CPU did nothing else all scene. PG_GSPIN = $18954 ; total pace-gate polls: what the CPU had LEFT PG_LOSTV = $18958 ; V-DISP edges the machine did not see, summed ; per frame -- see pg_frame PG_ARRN = $1895C ; entries the array was built with (an assertion ; the rig reads back rather than a comment) ; ---- the array chain. 193 x 6 B = 1,158 B, built once at scene setup. ; $1B000 and not $19000: dmagate.s's two arrays live at $19000/$19100 and ; ring.i's disc-offset table at $19400, and a front-end that shares an address ; with another front-end is how DM_USE landed on ring.i's mailbox (dma.i). PG_ARR = $1B000 PG_MAXE = 256 ; the chain cannot be longer than this ; ---- geometry. The container's, not the screen's: the screen is 256x256 and ; the picture is 256x192, so the difference is letterbox and is STATIC SETUP. PG_W = 256 PG_H = 192 PG_ROWB = PG_W/2 ; 128 words = 256 BYTES a row. THE WHOLE POINT: ; 1.0 B/pixel, because bit 11 stops GVRAM ; masking the high byte away and the two ; 256-colour pages are scrolled 384 apart ; (FINDINGS 46.5/47.1, dlxp.py). PG_STRIDE = 1024 ; GVRAM line stride, in bytes PG_PALB = 512 ; 256 GRB555+I words -- and exactly one sector SCRW = 256 SCRH = 256 GVRAM = $C00000 GPAL = $E82000 VC0 = $E82400 VC1 = $E82500 VC2 = $E82600 CONTRAST = $E8E001 ; The graphic scroll registers, named rather than written as CRTC+n*2.l: a ; 256-colour page is assembled from TWO nibble planes with independent scroll ; registers (px68k Grp_DrawLine8 reads scroll sets page*2 and page*2+1), so both ; of a page's registers have to agree or the page tears between its low and high ; nibble. Naming them is what makes the pairing visible at the write. CR_P0X0 = CRTC+12*2 CR_P0Y0 = CRTC+13*2 CR_P0X1 = CRTC+14*2 CR_P0Y1 = CRTC+15*2 CR_P1X0 = CRTC+16*2 CR_P1Y0 = CRTC+17*2 CR_P1X1 = CRTC+18*2 CR_P1Y1 = CRTC+19*2 PG_YOFF = (SCRH-PG_H)/2 ; 32 PG_TOP = GVRAM+PG_YOFF*PG_STRIDE PG_BLACK = $FFFF ; letterbox: index 255 in BOTH bytes. NOT 0 -- ; index 0 is page 1's transparency key and black ; lives at 255 (dlxp.py, vq.frame_palette). ; R20. bit 11 = G-VRAM set to buffer, i.e. the WRITE WINDOW; bits 9-8 = 01 ; 256 colours; bit 4 = 31.5 kHz; bits 3-0 = 256 lines, 256 dots. The two ; values differ in EXACTLY bit 11, which is what makes the window a window. PG_R20D = $0110 ; displaying PG_R20B = $0910 ; the write window open include "src/player/geom.i" org $10000 start: move.l #1,PG_FLAG.l clr.l PG_SHOWN.l clr.l PG_ERR.l move.l #-1,PG_ERRAT.l clr.l PG_LATE.l move.l #-1,PG_LATE1.l clr.l PG_LATEM.l clr.l PG_TSPIN.l clr.l PG_GSPIN.l clr.l PG_LOSTV.l ; ---- 1. the display. THE PLAYER OWNS IT, and that is a change from every ; other rig in this tree: tools/bench/stream.lua and decode.lua call ; crtc_mode.lua's MODE.apply from the host, which is fine for a decoder gate and ; is not a player. A player boots into a machine the IPL left in 768x512 ; 16-colour and has to get to 256x256 256-colour packed by itself, and the ; scroll registers and the priority word are load-bearing rather than cosmetic: ; 47.5 measured VC1 = $0000 putting page 0 on top and blacking the right half of ; the screen. bsr pg_video ; ---- 2. the static half of the picture (FINDINGS 47.2). Words 128..511 of ; every row, and the letterbox rows entire, are written ONCE and never again -- ; which is what makes the per-frame payload exactly the picture. Page 1's ; storage at 384..511 is what the +384 scroll puts under screen columns 0..127, ; and it must read 0 so that the opaque page 0 shows through there. bsr pg_static ; ---- 3. the chain the channel will walk, 193 entries of it (FINDINGS 62). bsr pg_array ; ---- 4. the transport. ONCE, not per frame: scsi_init resets the SPC and ; clears DM_USE, so a per-frame call would quietly put the data phase back on ; the CPU and the run would still deliver every byte -- 87 clocks each (58). bsr scsi_init bsr pg_dmacfg ; ---- 5. the clock. AFTER the SPC, because clk_init lowers the interrupt mask ; to $2500 and the bring-up should not be the first thing running with level 6 ; open. It also CLEARS PACE, so tick 0 is the instant the scene starts. move.l PG_FPS.l,CLK_FPS.l tst.l PG_PACEON.l beq.s .noclk bsr clk_init tst.l CLK_ERR.l beq.s .noclk move.l #$E1,PG_FLAG.l ; the mode is not one HFREQ describes bra pg_hold .noclk: move.l CLK_VDISP.l,PG_VD0.l ; ---- 6. the scene. move.l PG_ITER.l,d6 tst.l d6 bne.s .it moveq #1,d6 .it: pg_pass: moveq #0,d7 ; d7 = frame index within the pass pg_loop: bsr pg_gate ; wait for tick d7 bsr pg_frame ; and paint it tst.l d0 bmi.s pg_failed addq.l #1,d7 move.l PG_NFR.l,d0 cmp.l d0,d7 bcs.s pg_loop ; ---- a pass boundary. For this container a SEEK IS ARITHMETIC: there is no ; ring to discard, no index to walk and no prefill to climb, so the next pass's ; first record is simply LBA0 again. That is the whole of what K3 deletes, and ; it is worth one line of code and four of comment because it is the largest ; simplification in the project (ROADMAP K3) and it does not look like one. subq.l #1,d6 bne.s pg_pass move.l CLK_VDISP.l,PG_VDISP.l bsr clk_stop move.l #$FF,PG_FLAG.l bra.s pg_hold pg_failed: move.l CLK_VDISP.l,PG_VDISP.l bsr clk_stop move.l #$E2,PG_FLAG.l pg_hold: bra.s pg_hold ; ---------------------------------------------------------------- pg_gate ; Frame d7 may not START before tick d7 (the rule is stream.s's, unchanged, and ; deliberately so: the same gate against the same clock is what makes a paced ; packed run comparable with a paced codec run). A frame that arrives with its ; tick ALREADY past did not idle for a single poll, which means the previous ; frame used its whole slot and then some -- that is the underrun this player ; can have, and it is counted rather than absorbed. pg_gate: movem.l d0-d1,-(sp) tst.l PG_PACEON.l beq.s .out move.l CLK_PACE.l,d0 cmp.l d7,d0 bcs.s .wait ; PACE < d7: early, the common case tst.l d7 beq.s .out ; frame 0 starts AT tick 0 by definition tst.l PG_LATE.l bne.s .nf move.l d7,PG_LATE1.l .nf: addq.l #1,PG_LATE.l sub.l d7,d0 ; whole ticks overrun cmp.l PG_LATEM.l,d0 bls.s .out move.l d0,PG_LATEM.l bra.s .out .wait: addq.l #1,PG_GSPIN.l move.l CLK_PACE.l,d0 cmp.l d7,d0 bcs.s .wait .out: movem.l (sp)+,d0-d1 rts ; ---------------------------------------------------------------- pg_frame ; ONE FRAME. Open the write window, hand the record to the channel, close it. ; Returns d0 < 0 on a transport failure. ; ; THE WINDOW IS OPENED ROUND THE TRANSFER AND NOT ROUND THE SCENE, and that is ; not tidiness. R20 bit 11 blanks the graphics layer while it is set (measured, ; tools/bench/crtc_mode.lua) -- 47.4/ROADMAP B2 is exactly the question of ; whether a real board does too -- so a scene-long window would show nothing at ; all. Held round the transfer it is a shutter: the screen is dark for as long ; as the record takes to land and shows a COMPLETE frame the rest of the time. ; That is also why this player cannot tear the way FINDINGS 41's decoder does: ; there is no instant at which a half-written picture is displayable. ; ; CLK_VDISP IS SAMPLED EITHER SIDE OF THE TRANSFER. The difference is the ; V-DISP edges the machine SAW; the raster produced its own number regardless. ; In the held configuration the CPU is stopped for the whole transfer, so any ; edge that falls inside it is one the MFP can only remember once -- and a frame ; clock built on counting them loses time it can never get back. The subtraction ; here is what makes that visible without a host in the loop. pg_frame: movem.l d1-d5/a1,-(sp) move.l CLK_VDISP.l,d5 move.w #PG_R20B,CRTC_R20.l ; the write window opens ; LBA = PG_LBA0 + d7*PG_RECS + (d7/PG_CADF)*PG_CADA. Arithmetic, not a lookup: ; a packed record's length is geometry and an audio lump's is a cadence, so this ; player carries no index of either kind (dlxp.py). The third term is the whole ; cost of DLXP2 on the video path -- a divu and a mulu, once a frame -- and it ; is zero instructions of parsing, because the cadence is two numbers in the ; header and not a table in the stream. move.l d7,d3 move.l PG_RECS.l,d0 mulu d0,d3 ; frames * sectors, both small add.l PG_LBA0.l,d3 move.l PG_CADF.l,d0 beq.s .nocad ; silent container: no lumps to skip move.l d7,d2 divu d0,d2 ; d2.w = frame / F, the lumps passed andi.l #$FFFF,d2 ; divu leaves the REMAINDER in the high ; half, and it is not zero at F=11 move.l PG_CADA.l,d0 mulu d0,d2 add.l d2,d3 .nocad: move.l PG_RECS.l,d4 lea GVRAM,a1 ; IGNORED under chaining -- the channel ; takes MAR from the array's first entry ; -- and passed so that this call site ; reads like every other one in the tree bsr scsi_read move.l d0,d1 move.w #PG_R20D,CRTC_R20.l ; ...and closes. The frame is up. move.l DM_SPIN.l,d0 add.l d0,PG_TSPIN.l move.l CLK_VDISP.l,d0 sub.l d5,d0 ; edges seen across the transfer beq.s .nolost subq.l #1,d0 ; one edge per frame slot is expected; add.l d0,PG_LOSTV.l ; what is counted is the SURPLUS, and a .nolost: ; deficit cannot be seen from in here -- ; the host's raster count is what says ; how many there really were move.l d1,d0 bmi.s .err addq.l #1,PG_SHOWN.l movem.l (sp)+,d1-d5/a1 rts .err: tst.l PG_ERR.l bne.s .err2 move.l SC_ERR.l,PG_ERR.l move.l d7,PG_ERRAT.l .err2: moveq #-1,d0 movem.l (sp)+,d1-d5/a1 rts ; ---------------------------------------------------------------- pg_dmacfg ; The channel's configuration, and the choice between the only two rows of the ; ladder MAME has a code path for (dma.i: no EXREQ wiring, no single-address ; path, and only burst modelled as held). OCR gets CHAIN = %10 on top, which is ; sequential array chaining and is what makes 193 destinations one start. pg_dmacfg: move.l #DM_STEAL_DCR,d0 move.l #DM_STEAL_OCR,d1 tst.l PG_HELD.l beq.s .set move.l #DM_HELD_DCR,d0 move.l #DM_HELD_OCR,d1 .set: move.l d0,DM_DCRV.l ori.l #$08,d1 ; OCR CHAIN = %10, array chain move.l d1,DM_OCRV.l move.l #PG_ARR,DM_BARV.l move.l PG_ARRN.l,DM_BTCV.l move.l #1,DM_USE.l ; AFTER scsi_init, which clears it rts ; ---------------------------------------------------------------- pg_array ; The 193 {u32 MAR, u16 MTC} entries, built once. SCENE-CONSTANT: the packed ; layout spends both 256-colour pages, so there is no page to flip and no ; destination that changes from frame to frame (FINDINGS 62). ; ; THE ORDER IS THE CONTAINER'S. Palette first or 193rd is one paint of ; mismatch either way and 63.4 priced both at -12.8 dB without being able to ; choose; so the format records it (dlxp.py flags bit 1) and this reads it. An ; array built the other way round from the record it is fed would not fail -- it ; would paint 192 rows of picture into the palette registers and 512 B of ; palette across the top two rows of the screen, which is a picture, and a gate ; that only checked for errors would pass it. pg_array: movem.l d0-d2/a0-a1,-(sp) lea PG_ARR,a0 tst.l PG_PALL.l bne.s .rows bsr .pal .rows: lea PG_TOP,a1 move.w #PG_H-1,d0 .r: move.l a1,(a0)+ move.w #PG_ROWB*2,(a0)+ lea PG_STRIDE(a1),a1 dbra d0,.r tst.l PG_PALL.l beq.s .done bsr .pal .done: move.l a0,d0 sub.l #PG_ARR,d0 divu #6,d0 andi.l #$FFFF,d0 move.l d0,PG_ARRN.l movem.l (sp)+,d0-d2/a0-a1 rts .pal: move.l #GPAL,(a0)+ move.w #PG_PALB,(a0)+ rts ; ---------------------------------------------------------------- pg_video ; 256x256, 256 colours, 31.5 kHz, the two pages scrolled 384 apart, page 1 on ; top. Every value here is tools/bench/crtc_mode.lua's, which derives them from ; the dot clocks rather than recalling them; this is the same table in the place ; a player would keep it. pg_video: movem.l d0-d1/a0-a1,-(sp) lea pg_crtc(pc),a0 lea CRTC,a1 moveq #0,d0 .c: move.w (a0)+,d0 bmi.s .cdone move.w (a0)+,d1 move.w d1,0(a1,d0.w) bra.s .c .cdone: move.w #PG_R20D,CRTC_R20.l move.w #$0001,VC0.l ; 256 colours ; PAGE 1 ON TOP. MEASURED (tools/bench/probe_page1.lua): $0000 puts page 0 on ; top, its zeros then cover page 1 and the right half of the screen is black -- ; which was the first failure of the packed layout and does not look like a ; priority bug from the outside. move.w #$0002,VC1.l move.w #$001F,VC2.l ; graphics on, all four pages move.b #15,CONTRAST.l ; the IPL leaves 14, i.e. 7% dark ; The scroll. Both registers of each page, for the reason named at CR_P0X0. ; move.w #0 and not clr.w: CLR on a 68000 READS the destination before writing ; it, and a write-only register read back is a machine-dependent value this code ; has no reason to depend on. move.w #0,CR_P0X0.l move.w #0,CR_P0Y0.l move.w #0,CR_P0X1.l move.w #0,CR_P0Y1.l move.w #384,CR_P1X0.l ; X = 384 = -128 mod 512 move.w #0,CR_P1Y0.l move.w #384,CR_P1X1.l move.w #0,CR_P1Y1.l movem.l (sp)+,d0-d1/a0-a1 rts ; register number * 2, value; -1 ends it pg_crtc: dc.w 0*2,45 ; H total: 46 chars = 368 dots = 31500 Hz dc.w 1*2,5 ; H sync end dc.w 2*2,10 ; H disp begin dc.w 3*2,42 ; H disp end -> 256 dots dc.w 4*2,567 ; V total: 568 lines -> 55.4577 Hz dc.w 5*2,5 ; V sync end dc.w 6*2,40 ; V disp begin dc.w 7*2,552 ; V disp end -> 512 lines, double-scanned dc.w 8*2,27 ; H sync adjust dc.w -1,0 ; ---------------------------------------------------------------- pg_static ; The half of the screen the container does not carry (FINDINGS 47.2). Two ; regions, and they are static for two different reasons: ; ; words 128..511 of EVERY row. Page 1's storage at 384..511 sits under screen ; columns 0..127 after the +384 scroll and must read 0 so the opaque page 0 ; shows through; the rest is off-screen storage. Zero, once. ; the LETTERBOX rows, 0..31 and 224..255. The picture is 192 rows of a ; 256-row screen. $FFFF is index 255 in both bytes, and 255 is black in ; every frame's palette (vq.frame_palette reserves it), so these rows stay ; correct across a per-frame palette without being rewritten. ; ; Written with the WRITE WINDOW OPEN, because that is the only mode in which a ; word write lands both bytes -- outside it gvram_w takes `data & 0x00ff` and ; the high byte is thrown away, which is the whole 2.0 B/pixel problem (46.5). pg_static: movem.l d0-d3/a0-a1,-(sp) move.w #PG_R20B,CRTC_R20.l lea GVRAM,a0 move.w #SCRH-1,d0 ; y moveq #0,d3 .row: movea.l a0,a1 move.w d3,d1 cmp.w #PG_YOFF,d1 bcs.s .letter cmp.w #PG_YOFF+PG_H,d1 bcc.s .letter lea PG_ROWB*2(a1),a1 ; a picture row: leave words 0..127 to move.w #SCRW-PG_ROWB-1,d1 ; the channel, zero 128..511 bra.s .z .letter: move.w #PG_ROWB-1,d1 ; words 0..127 <- black .lb: move.w #PG_BLACK,(a1)+ dbra d1,.lb move.w #SCRW-PG_ROWB-1,d1 ; ...and 128..511 <- 0, as everywhere .z: clr.w (a1)+ dbra d1,.z lea PG_STRIDE(a0),a0 addq.w #1,d3 dbra d0,.row move.w #PG_R20D,CRTC_R20.l movem.l (sp)+,d0-d3/a0-a1 rts include "src/player/clock.i" ; scsi.i includes src/player/dma.i itself, at its foot -- so it is NOT included ; here. The DMAC constants and sc_in_dma come in with the transport that uses ; them, which is also why dmagate.s includes only scsi.i. include "src/player/scsi.i"