Files
Dragon-s-Lair-X68k/src/player/dmagate.s
T
prosolis 07f36c2af9 Put the palette on the channel, and find one start paints a whole frame
ROADMAP K1, the packed player's one open structural item.  A frame is a picture
AND a palette, and no run in this tree had pointed a DMA channel at the palette
registers.  dmagate.s runs 7-9, gated by dma_run.sh and check.sh:

  7. 512 B off the disc into $E82000, bus held -- byte-exact in 256 register
     words, read back OUT OF the registers by the 68000;
  8. the SAME transfer aimed at RAM -- byte-exact at $2C000, and 256 of 256
     palette words still read the poison the CPU wrote, which is what attributes
     run 7 to the channel's MAR rather than to the readback path;
  9. ONE array-chained start across two kinds of destination -- the palette and
     six picture rows at the 1,024 B line stride, 2,048 B byte-exact.

So a packed frame is one channel start: a 193-entry array, palette first, CPU
halted from the first byte to the last.  The array is scene-constant, because
the packed layout spends both 256-colour pages and there is no page to flip.
What is left on the CPU per frame in the video path is the channel start and the
READ(10) -- no per-frame PAINT, which is not the same claim as no per-frame CPU.

The destination is POISONED first (62.1).  Runs 4-6 wrote into RAM that was zero
and GVRAM that was stale against a record that is mostly pad; "it matches the
disc" was weaker than it read as.  The host counts whether the poison actually
discriminates instead of assuming it: 511 of 512, and the gate refuses under 500.

And it opened a hardware item (62.4, ROADMAP B4).  MAME maps the palette to
palette_device over memory_array, whose write16 is a plain COMBINE_DATA -- RAM
that honours mem_mask, with no handler that could refuse a byte write.  Unlike
GVRAM's 256-colour arm there is nothing here to be wrong about, so the run
bounds the model and not the board.  What a real palette register does with a
byte write is unmeasured.  A negative costs 0.28% of a frame and nothing else.

29_packed_player.py now also prints the two rows with the per-frame palette
charged -- 55.7% of a frame on the chain, 582 KB/s -- alongside the picture-only
figures the codec comparison is quoted against.

check.sh ALL GREEN before (tmp/check_s30_start.log) and after
(tmp/check_s30_end.log).

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-25 07:06:44 -07:00

404 lines
20 KiB
ArmAsm

; Front-end for the HD63450 DATA PHASE (ROADMAP P4a), for the rig.
;
; THE QUESTION. FINDINGS 58 put the transport on the 68000 and priced it: the
; CPU moving every byte itself costs 87.28 clocks per delivered byte, 391.8% of
; a 12 fps frame. Against that, the cheapest DMA row of the ladder is 22.4% and
; the dearest is 85.3%, so everything left before M2 turns on getting the DMAC
; to drive the data phase with the bus HELD. 57.3 is why it cannot be shown by
; watching the data register: with the DMAC's OWN asserted, which it is at idle
; here, MAME cannot tell a CPU-driven byte at $EA0015 from a DMAC-driven one.
;
; THE EVIDENCE THIS GATE PRODUCES, and none of it is a read of $EA0015:
;
; 1. THE SAME BYTES. The same sectors are read three times -- once by the PIO
; path FINDINGS 58 measured, once by the channel with the bus held, once by
; the channel stealing cycles -- and the HOST compares all three against its
; own copy of the image. A transport that returns the wrong bytes without
; saying so is the failure a checksum-free ring cannot survive (49.2).
; 2. THE CPU'S OWN PROGRESS. MTC is sampled by the INSTRUCTION AFTER the one
; that starts the channel. Held, it reads zero: the entire transfer
; happened between two instructions, because the 68000 did not execute in
; between. Stealing, it reads nearly the full count and the CPU then goes
; round its wait loop thousands of times while the bytes arrive. That
; difference IS "the DMAC held the bus", and it is a fact about the CPU.
; 3. THE CHANNEL'S OWN ACCOUNT. CSR, CER, the final MTC and the final MAR:
; the channel says it completed without error, moved every byte, and left
; its memory pointer exactly one transfer-length past where it started.
; 4. THE WINDOW IS REFUSED. A windowed read (58.3: 117 of 120 records start
; part way into a sector) is rejected by the transport rather than silently
; delivering the neighbouring records' bytes into the ring. P4a's
; precondition is stated by the code that has it, not by a comment.
;
; WHAT IT DOES NOT SHOW. Not `W`. Not one clock of it. MAME's DMAC runs on
; wall-clock attotimes (42.5) and its burst mode halts the CPU outright rather
; than costing it cycles per operand, so this gate settles WHICH CONFIGURATION
; WORKS and not what one costs. See src/player/dma.i for the three ways this
; model bounds the question -- no EXREQ wiring, no single-address path, and only
; burst modelled as held.
DGFLAG = $18600 ; 0 idle / 1 done
DGREC = $18800 ; 9 x 32 B: rc, err, mtc0, spin, csr, cer, mtc, mar
; $18800 AND NOT $18610, WHERE THIS LIVED: nine
; records of 32 B run to $188FF, and from $18610
; they would have run over DGWIN at $18700 --
; the window run's own result, which run 10 then
; writes back. A silent overlap between two
; runs' evidence is the kind of thing that makes
; a gate report the wrong run's numbers.
DGREC_SZ = 32
DGWIN = $18700 ; u32 return of the WINDOWED dma read (want -1)
DGWERR = $18704 ; u32 SC_ERR after it (want SCE_WINDOW)
DGR20 = $18708 ; u32 R20 as it stood during run 4
DGR20N = $1870C ; u32 R20 as it stood during run 5 (the control)
DGR20C = $18710 ; u32 R20 as it stood during run 6 (chained)
DGR20P = $18714 ; u32 R20 as it stood during run 9 (palette+rows)
R20_BUF = $0916 ; 256-colour, 31.5 kHz, G-VRAM SET TO BUFFER
R20_MSK = $0116 ; the SAME, bit 11 CLEAR -- the negative control
DGDST4 = $C0C000 ; where the control writes
DGCHA = $19000 ; the array-chain table: 8 x {u32 MAR, u16 MTC}
DGCHN = 8 ; entries
DGCROW = 256 ; bytes an entry carries -- one packed picture row
DGDST5 = $C10000 ; and the 8 row bases, at the 1024 B line stride
CRTC20 = $E80028 ; CRTC R20
DGDST3 = $C08000 ; DMA straight into GVRAM, BUFFER MODE (47.6.2)
DGLBA = 1000 ; a NON-ZERO LBA throughout: a driver that emits
; a malformed LBA field still passes LBA 0
DGBLK = 4 ; 4 x 512 = 2,048 B
DGDST0 = $20000 ; PIO
DGDST1 = $24000 ; DMA, bus held
DGDST2 = $28000 ; DMA, cycle stealing
; ---- runs 7-9: THE PALETTE (ROADMAP K1, FINDINGS 61.9's first open item).
DGPAL = $E82000 ; the GRAPHIC palette: 256 words, GGGGGRRRRRBBBBBI
DGPALN = 256 ; words in it -- and 512 B is exactly ONE sector
DGPBLK = 1 ; so the whole palette is one block off the disc
DGPOIS = $A500 ; the poison: word i = DGPOIS|i. A palette that
; still reads this was not written by anything.
DGDST6 = $2C000 ; run 8's destination: RAM, so the palette is
; left alone and must still read poison
DGDST7 = $C14000 ; run 9's six GVRAM rows, at the 1024 B stride
DGCHA2 = $19100 ; run 9's array: 7 x {u32 MAR, u16 MTC}
DGCHN2 = 7 ; palette 512 B + 6 rows x 256 B = 2,048 B
DGCROW2 = 256
DGPS7 = $1A000 ; the palette as it stood after run 7 ...
DGPS8 = $1A200 ; ... after run 8 (the control: poison) ...
DGPS9 = $1A400 ; ... and after run 9 (chained). SNAPSHOTS, not
; a late read: each run overwrites the previous
; run's palette, so a host that looks once at
; the end sees only the last of the three.
org $10000
start:
clr.l DGFLAG.l
; ---- 1. the PIO path, unchanged, as the reference the other two are measured
; against. It is here so that a DMA failure cannot be confused with a SCSI
; protocol failure: if this one is wrong, nothing below is about the DMAC.
bsr scsi_init
move.l #DGLBA,d3
moveq #DGBLK,d4
lea DGDST0,a1
bsr scsi_read
lea DGREC,a0
bsr dg_save
; ---- 2. the channel, WITH THE BUS HELD
bsr scsi_init
move.l #DM_HELD_DCR,DM_DCRV.l
move.l #DM_HELD_OCR,DM_OCRV.l
move.l #1,DM_USE.l ; after scsi_init, which clears it
move.l #DGLBA,d3
moveq #DGBLK,d4
lea DGDST1,a1
bsr scsi_read
lea DGREC+DGREC_SZ,a0
bsr dg_save
; ---- 3. the channel, STEALING CYCLES. Same bytes, same code, two register
; values different -- which is what makes the comparison a comparison.
bsr scsi_init
move.l #DM_STEAL_DCR,DM_DCRV.l
move.l #DM_STEAL_OCR,DM_OCRV.l
move.l #1,DM_USE.l
move.l #DGLBA,d3
moveq #DGBLK,d4
lea DGDST2,a1
bsr scsi_read
lea DGREC+2*DGREC_SZ,a0
bsr dg_save
; ---- 4. THE CHANNEL WRITING GVRAM, IN BUFFER MODE. 47.6.2: "the DMAC has not
; been near this" -- 44.7 costed a device->GVRAM transfer and 47 built the packed
; layout, and no run in this tree has ever pointed a channel at $C00000. Two
; separate things are being asked at once and both are write-path questions:
;
; a. can a channel write GVRAM AT ALL? Nothing says it cannot -- the DMAC
; writes through the same program address space the CPU does -- but a
; transport that silently drops its writes into a device handler is exactly
; the failure this gate exists to catch, and the SPC's own bytes make a
; better witness than a fill pattern.
; b. does a BYTE-wide channel fill the PACKED layout? 47.1 measured the write
; path with word writes from the CPU. A dual-address channel with an 8-bit
; device port writes BYTES, and MAME's gvram_w in buffer mode passes
; mem_mask straight through -- so an even byte should land in the HIGH half
; of its word (page 1) and an odd byte in the LOW half (page 0). If it
; does, a linear DMA of a stream interleaved (right<<8)|left -- which is
; exactly show_frame256_packed.lua's layout -- fills the screen with no CPU
; in the loop at all.
;
; R20 bit 11 is left SET across the readback on purpose: gvram_r returns the raw
; word in buffer mode and the masked byte outside it, so clearing it first would
; hide half of what is being measured.
;
; R20 IS WRITTEN OUTRIGHT AND NOT OR-ED INTO. The first cut of this run set bit
; 11 on top of whatever the IPL left, and the IPL leaves $0B16 (22.1) -- bit 11
; ALREADY SET, and COL = %11, the 65,536-colour setup, which writes whole words
; with or without the bit. The run passed and proved nothing: it was a test that
; could not fail. Run 5 below is the control that makes this one mean something,
; and the two differ in EXACTLY BIT 11.
bsr scsi_init
move.w #R20_BUF,CRTC20.l
move.l #R20_BUF,DGR20.l
move.l #DM_HELD_DCR,DM_DCRV.l
move.l #DM_HELD_OCR,DM_OCRV.l
move.l #1,DM_USE.l
move.l #DGLBA,d3
moveq #DGBLK,d4
lea DGDST3,a1
bsr scsi_read
lea DGREC+3*DGREC_SZ,a0
bsr dg_save
; ---- 5. THE NEGATIVE CONTROL: the same transfer with bit 11 CLEAR. In masked
; 256-colour mode gvram_w takes `data & 0x00ff` and IGNORES mem_mask, so a byte
; written to an EVEN address -- where the 68000 puts the MS byte, and where every
; other disc byte lands -- contributes nothing and cannot be read back. Half the
; transfer must be lost, and if it is not, run 4 was not measuring the bit.
bsr scsi_init
move.w #R20_MSK,CRTC20.l
move.l #R20_MSK,DGR20N.l
move.l #DM_HELD_DCR,DM_DCRV.l
move.l #DM_HELD_OCR,DM_OCRV.l
move.l #1,DM_USE.l
move.l #DGLBA,d3
moveq #DGBLK,d4
lea DGDST4,a1
bsr scsi_read
lea DGREC+4*DGREC_SZ,a0
bsr dg_save
move.w #R20_BUF,CRTC20.l ; back to buffer mode, so the host reads
; RAW WORDS out of both destinations
; ---- 6. THE ROW STRIDE, WHICH IS THE REAL SHAPE OF THE TRANSFER. Runs 4 and 5
; wrote 2,048 contiguous bytes, and no picture is contiguous: a packed row is
; 256 B of a 1024 B line stride, so a frame is 192 destinations and not one.
; 46.6 said "no stride for a DMAC to skip" about the bytes WITHIN a row and left
; the rows themselves unexamined; a channel cannot skip 768 B any more than it
; could skip the 300 B in front of a record (run 10 below).
;
; The MC68450 answers this with SEQUENTIAL ARRAY CHAINING -- an array of 6-byte
; {u32 MAR, u16 MTC} entries it walks by itself -- and MAME implements it. So
; the question is not whether the CPU can restart the channel 192 times a frame;
; it is whether it has to at all. Eight rows here, out of the same LBA and the
; same 2,048 B, so the ONLY difference from run 4 is where the bytes land.
bsr scsi_init
move.w #R20_BUF,CRTC20.l
move.l #R20_BUF,DGR20C.l
lea DGCHA,a0
lea DGDST5,a1
moveq #DGCHN-1,d5
dg_mkch:
move.l a1,(a0)+ ; MAR: this row's base
move.w #DGCROW,(a0)+ ; MTC: 256 bytes of it
lea 1024(a1),a1 ; the next row is a line stride away
dbra d5,dg_mkch
move.l #DM_HELD_DCR,DM_DCRV.l
move.l #DM_HELD_OCR|$08,DM_OCRV.l ; OCR CHAIN = %10, array chain
move.l #DGCHA,DM_BARV.l
move.l #DGCHN,DM_BTCV.l
move.l #1,DM_USE.l
move.l #DGLBA,d3
moveq #DGBLK,d4
lea DGDST5,a1 ; ignored under chaining; passed so the
; call site reads the same as the others
bsr scsi_read
lea DGREC+5*DGREC_SZ,a0
bsr dg_save
clr.l DM_BARV.l ; and OFF again, so run 7 is unchained
; ---- 7. THE PALETTE. Can a channel write $E82000? (FINDINGS 61.9, ROADMAP
; K1.) Runs 4-6 put the PICTURE on the channel; a packed frame is a picture AND
; a palette, and if the palette registers take a byte-wide DMA the way GVRAM
; does in buffer mode then the palette is a 193rd array-chain entry and ONE
; channel start paints a whole frame -- no per-frame CPU work in the video path
; at all. If they do not, the CPU writes 256 words a frame (61.9 derives that
; at ~2,370 clocks, 0.28% of a frame) and the architecture still stands; this is
; the difference between cheap and free, and it is worth one run to know which.
;
; 512 B IS THE WHOLE PALETTE AND EXACTLY ONE SECTOR, which is why this run reads
; one block where the others read four: a transfer that ran long would write
; $E82200 (the TEXT palette) and then $E82400 (the video controller's own
; registers, priority included), and a probe that reconfigures the video
; controller as a side effect is not a probe.
;
; THE PALETTE IS POISONED FIRST, and that is what stops this being run 4's trap
; a second time. A destination that already holds the right bytes cannot tell a
; channel that wrote them from a channel that did nothing; RAM at $20000 was
; zero and the record is mostly pad, so "it matches" has been a weak claim all
; session. Word i is set to $A500|i, which no 512 B of container matches by
; accident, and the host reports how many of the 512 positions the poison and
; the disc actually differ in rather than assuming all of them.
bsr scsi_init
bsr dg_poison
move.l #DM_HELD_DCR,DM_DCRV.l
move.l #DM_HELD_OCR,DM_OCRV.l
move.l #1,DM_USE.l
move.l #DGLBA,d3
moveq #DGPBLK,d4
lea DGPAL,a1
bsr scsi_read
lea DGPS7,a1
bsr dg_palsnap ; before run 8 overwrites it
lea DGREC+6*DGREC_SZ,a0
bsr dg_save
; ---- 8. THE NEGATIVE CONTROL, and it is an ATTRIBUTION control rather than a
; mechanism one. Run 5's control could point at a mode bit; there is no mode
; bit here, so what has to be excluded is that run 7's palette held the disc's
; bytes for some reason OTHER than the channel having written them there --
; a readback that aliases somewhere else, the SPC's own path touching the
; registers, the poison never having landed.
;
; Same transfer, same channel, same bytes; ONE thing different, the destination
; address. The disc's bytes must appear at $2C000, and the palette must still
; read poison in all 256 words. Two claims from one run, and the second is the
; one that makes run 7 mean anything: it also proves the CPU's poison writes
; reach the registers the host reads back, which is the positive half.
bsr scsi_init
bsr dg_poison
move.l #DM_HELD_DCR,DM_DCRV.l
move.l #DM_HELD_OCR,DM_OCRV.l
move.l #1,DM_USE.l
move.l #DGLBA,d3
moveq #DGPBLK,d4
lea DGDST6,a1
bsr scsi_read
lea DGPS8,a1
bsr dg_palsnap ; must be poison, word for word
lea DGREC+7*DGREC_SZ,a0
bsr dg_save
; ---- 9. THE 193rd ENTRY: one start, the palette AND the picture rows. This is
; the run K1 exists for. Runs 7 and 8 only show that a channel can write the
; palette registers; what the architecture needs is that ONE array chain can
; cross from a hardware register area into GVRAM without the CPU between them,
; because a frame is a palette entry followed by 192 row entries and the whole
; claim is that the CPU starts the channel once.
;
; Seven entries, 2,048 B, out of the same LBA as everything else: 512 B into the
; palette and then six rows of 256 B at the 1024 B line stride. The destination
; regions are of two different KINDS -- device registers and video RAM in buffer
; mode -- which is exactly the crossing that has never been run.
bsr scsi_init
bsr dg_poison
move.w #R20_BUF,CRTC20.l
move.l #R20_BUF,DGR20P.l
lea DGCHA2,a0
move.l #DGPAL,(a0)+ ; entry 0: the palette, a whole sector
move.w #DGPALN*2,(a0)+
lea DGDST7,a1
moveq #DGCHN2-2,d5 ; the remaining six are picture rows
dg_mkch2:
move.l a1,(a0)+
move.w #DGCROW2,(a0)+
lea 1024(a1),a1
dbra d5,dg_mkch2
move.l #DM_HELD_DCR,DM_DCRV.l
move.l #DM_HELD_OCR|$08,DM_OCRV.l ; OCR CHAIN = %10, array chain
move.l #DGCHA2,DM_BARV.l
move.l #DGCHN2,DM_BTCV.l
move.l #1,DM_USE.l
move.l #DGLBA,d3
moveq #DGBLK,d4
lea DGDST7,a1 ; ignored under chaining, as in run 6
bsr scsi_read
lea DGPS9,a1
bsr dg_palsnap
lea DGREC+8*DGREC_SZ,a0
bsr dg_save
clr.l DM_BARV.l ; and OFF again, so run 10 is unchained
; ---- 10. and a WINDOWED read through the channel, which must be REFUSED. This
; is the one test here that is expected to fail, and it has to fail LOUDLY: the
; alternative is a channel writing a whole sector into a ring that has room for
; a record, over the top of records the decoder has not finished with.
bsr scsi_init
move.l #DM_HELD_DCR,DM_DCRV.l
move.l #DM_HELD_OCR,DM_OCRV.l
move.l #1,DM_USE.l
move.l #300,SC_WSKIP.l ; a record that starts 300 B into a sector
move.l #1024,SC_WKEEP.l
move.l #DGLBA,d3
moveq #DGBLK,d4
lea DGDST2,a1
bsr scsi_read_win
move.l d0,DGWIN.l
move.l SC_ERR.l,DGWERR.l
move.l #1,DGFLAG.l
hold: bra.s hold
; ---- one config's result, copied out of the shared reporting words before the
; next run overwrites them. d0 = scsi_read's return; a0 = where it goes.
dg_save:
move.l d0,(a0)+
move.l SC_ERR.l,(a0)+
move.l DM_MTC0.l,(a0)+
move.l DM_SPIN.l,(a0)+
move.l DM_CSRF.l,(a0)+
move.l DM_CERF.l,(a0)+
move.l DM_MTCF.l,(a0)+
move.l DM_MARF.l,(a0)+
; and clear them, so a config that never reached the channel reports
; zeros of its own rather than the previous config's numbers.
clr.l DM_MTC0.l
clr.l DM_SPIN.l
clr.l DM_CSRF.l
clr.l DM_CERF.l
clr.l DM_MTCF.l
clr.l DM_MARF.l
rts
; ---- the poison. Word i of the graphic palette <- $A500|i, written by the
; 68000 itself. Two jobs: a destination that cannot hold the right answer by
; accident, and a known pattern the control run reads back out of the registers
; to show the readback path shows what is actually in them.
dg_poison:
movem.l d0-d2/a0,-(sp)
lea DGPAL,a0
moveq #0,d1 ; i
move.w #DGPALN-1,d0
.p: move.w d1,d2
ori.w #DGPOIS,d2 ; $A500|i, and i never exceeds 255
move.w d2,(a0)+
addq.w #1,d1
dbra d0,.p
movem.l (sp)+,d0-d2/a0
rts
; ---- the palette as it stands, copied to (a1) by the 68000 READING THE
; REGISTERS. A snapshot and not a late host read: each palette run overwrites
; the last one's result, so all three have to be kept while they are true. It
; is also the same shape of evidence 53.3 used for pal_pack -- the words come
; back out of $E82000 rather than out of the buffer they were built in.
dg_palsnap:
movem.l d0/a0-a1,-(sp)
lea DGPAL,a0
move.w #DGPALN-1,d0
.s: move.w (a0)+,(a1)+
dbra d0,.s
movem.l (sp)+,d0/a0-a1
rts
include "src/player/scsi.i"