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
This commit is contained in:
prosolis
2026-08-25 07:06:44 -07:00
parent 1be428c270
commit 07f36c2af9
9 changed files with 664 additions and 32 deletions
+117 -15
View File
@@ -24,25 +24,40 @@ local SP = M.devices[":maincpu"].spaces["program"]
local function P(s) print("[DMA] "..s) end
local function T() local t=M.time; return t.seconds + t.attoseconds/1e18 end
local DGFLAG, DGREC, DGREC_SZ = 0x18600, 0x18610, 32
local DGFLAG, DGREC, DGREC_SZ = 0x18600, 0x18800, 32
local DGWIN, DGWERR, DGR20, DGR20N, DGR20C = 0x18700, 0x18704, 0x18708, 0x1870C, 0x18710
local DGR20P = 0x18714
local CHROW, CHN, CHBASE = 256, 8, 0xC10000
local R20OF -- filled in after the mailbox addresses are known
local GV = 0xC00000
local DGLBA, DGBLK = 1000, 4
local DST = {0x20000, 0x24000, 0x28000, 0xC08000, 0xC0C000, 0xC10000}
-- the palette runs (ROADMAP K1). PS7/PS8/PS9 are the SNAPSHOTS dmagate.s takes
-- by reading $E82000 back with the 68000 after each run; the registers
-- themselves hold only the last of the three by the time the host looks.
local PAL, PALN, PALB, POIS = 0xE82000, 256, 512, 0xA500
local PS7, PS8, PS9 = 0x1A000, 0x1A200, 0x1A400
local CHROW2, CH2BASE, CHN2ROWS = 256, 0xC14000, 6
local DST = {0x20000, 0x24000, 0x28000, 0xC08000, 0xC0C000, 0xC10000,
PAL, 0x2C000, CH2BASE}
local NAME = {"PIO (the path FINDINGS 58 measured)",
"DMA, BUS HELD (DCR $00 burst, OCR $81 max rate)",
"DMA, STEALING (DCR $80 cycle steal, OCR $80 limited)",
"DMA -> GVRAM (bus held, R20 bit 11 = BUFFER MODE) [47.6.2]",
"DMA -> GVRAM (the SAME, bit 11 CLEAR -- NEGATIVE CONTROL)",
"DMA -> GVRAM (ARRAY CHAINED, 8 rows at the 1024 B line stride)"}
local SHORT = {"pio", "held", "steal", "gvram", "masked", "chain"}
"DMA -> GVRAM (ARRAY CHAINED, 8 rows at the 1024 B line stride)",
"DMA -> PALETTE (bus held, 512 B into $E82000) [K1, 61.9]",
"DMA -> RAM (the SAME read aimed elsewhere -- NEGATIVE CONTROL:"
.." the palette must still read poison)",
"DMA -> PALETTE + SIX ROWS (ONE array-chained start across two"
.." kinds of destination)"}
local SHORT = {"pio", "held", "steal", "gvram", "masked", "chain",
"pal", "palctl", "palchain"}
local LENOF = {[6]=PALB, [7]=PALB} -- everything else is DGBLK*512
local ERRNAME = {[0]="OK", "SELECTION TIMEOUT -- no target answered",
"UNEXPECTED PHASE", "POLL TIMEOUT -- a phase never arrived",
"NON-ZERO SCSI STATUS",
"WINDOWED READ REFUSED -- a channel cannot drop bytes"}
R20OF = {[3]=DGR20, [4]=DGR20N, [5]=DGR20C}
R20OF = {[3]=DGR20, [4]=DGR20N, [5]=DGR20C, [8]=DGR20P}
local DISK = os.getenv("DLX_SCSI_IMG") or "dlxdisk.img"
local code do local f=io.open("dmagate.bin","rb"); code=f:read("a"); f:close() end
@@ -75,8 +90,8 @@ SUB = emu.add_machine_frame_notifier(function()
return
end
if not want then P("no "..DISK.." to check against"); P("done"); M:exit(); return end
local LEN = DGBLK*512
for i = 0, 5 do
for i = 0, 8 do
local LEN = LENOF[i] or DGBLK*512
local b = DGREC + i*DGREC_SZ
local rc = SP:read_u32(b)
local e = SP:read_u32(b+4)
@@ -101,21 +116,43 @@ SUB = emu.add_machine_frame_notifier(function()
local pg1, pg0, bad_hi, bad_lo = 0, 0, 0, 0
for k = 1, LEN do
local got
-- `a` is set for every destination that has to be read a WORD at a
-- time and split by hand -- GVRAM in buffer mode, and the palette
-- snapshots, whose words are what the 68000 read back out of
-- $E82000. Where it stays nil the destination is plain RAM.
local off, a
if i == 5 then
-- The chained run's destination is not linear: byte k of the
-- transfer is byte k%256 of row k//256, and the rows are a full
-- 1024 B line stride apart. If the channel had ignored the array
-- and run contiguously, every byte past the first row would be
-- in the wrong place and this comparison would say so.
local off = (k-1) % CHROW
local a = CHBASE + ((k-1) // CHROW) * 1024 + (off & ~1)
off = (k-1) % CHROW
a = CHBASE + ((k-1) // CHROW) * 1024 + (off & ~1)
elseif i == 6 then
off = (k-1) % 2
a = PS7 + ((k-1) & ~1)
elseif i == 8 then
-- ONE transfer across two kinds of destination: the first sector
-- is the palette, the rest is six picture rows at the line
-- stride. The split is the array's, and this walks it the same
-- way the channel was told to.
if k <= PALB then
off = (k-1) % 2
a = PS9 + ((k-1) & ~1)
else
local idx = k - PALB - 1
off = idx % 2
a = CH2BASE + (idx // CHROW2) * 1024 + ((idx % CHROW2) & ~1)
end
elseif i >= 3 and i ~= 7 then
off = (k-1) % 2
a = DST[i+1] + ((k-1) & ~1)
end
if a then
local w = SP:read_u16(a)
if (off % 2) == 0 then got = (w >> 8) & 0xff; pg1 = pg1 + 1
else got = w & 0xff; pg0 = pg0 + 1 end
elseif i >= 3 then
local w = SP:read_u16(DST[i+1] + ((k-1) & ~1))
if ((k-1) % 2) == 0 then got = (w >> 8) & 0xff; pg1 = pg1 + 1
else got = w & 0xff; pg0 = pg0 + 1 end
else
got = SP:read_u8(DST[i+1]+k-1)
end
@@ -125,14 +162,35 @@ SUB = emu.add_machine_frame_notifier(function()
else bad_lo = bad_lo + 1 end
end
end
if i >= 3 then
if R20OF[i] then
P(string.format(" R20 during the run = $%04X (bit 11 %s); %d bytes "
.."read back as page 1 (high half) and %d as page 0",
.."read back out of the HIGH half of a destination "
.."word and %d out of the LOW half",
SP:read_u32(R20OF[i]),
((SP:read_u32(R20OF[i]) & 0x0800) ~= 0)
and "SET" or "CLEAR",
pg1, pg0))
end
-- THE PALETTE RUNS' OWN VACUITY CHECK. Run 7's destination was
-- poisoned by the 68000 first, so "it matches the disc" cannot be
-- satisfied by a channel that did nothing -- but only if the poison
-- and the disc actually differ everywhere they are compared. That is
-- a property of THIS record and is counted rather than assumed.
if i == 6 then
local diff = 0
for j = 0, PALN-1 do
local w = (POIS | j) & 0xffff
if ((w >> 8) & 0xff) ~= string.byte(want, 2*j+1) then diff = diff + 1 end
if (w & 0xff) ~= string.byte(want, 2*j+2) then diff = diff + 1 end
end
P(string.format(" PALETTE POISON IS A DISCRIMINATOR: %d of %d "
.."positions differ from the disc's bytes -- a "
.."channel that wrote nothing could not have passed "
.."in those.", diff, PALB))
P(string.format(" %d bytes read back out of the HIGH half of a "
.."palette word (G and the top of R) and %d out of "
.."the LOW half", pg1, pg0))
end
if bad == 0 then
P(string.format(" BYTES OK: %d B from LBA %d match %s byte for byte "
.."[%s]", LEN, DGLBA, DISK, SHORT[i+1]))
@@ -152,6 +210,26 @@ SUB = emu.add_machine_frame_notifier(function()
P(" CONTROL DID NOT FAIL: the masked write path delivered every "
.."byte too, so the run above is not evidence about R20 bit 11.")
end
if i == 6 then
P(" A CHANNEL WRITES THE PALETTE REGISTERS: 512 B off the disc "
.."became 256 palette words, read back OUT OF $E82000 by the "
.."68000 itself, with the CPU halted for the transfer. Each "
.."disc byte landed in its own half of a register word, so a "
.."per-frame palette needs no CPU (61.9, ROADMAP K1).")
end
if i == 8 then
P(string.format(" ONE START PAINTED THE PALETTE AND %d ROWS: a "
.."single array-chained transfer crossed from device registers "
.."at $%06X into GVRAM at $%06X, %d B in %d entries, CPU halted "
.."throughout. A frame is that shape with %d row entries "
.."instead of %d.", CHN2ROWS, PAL, CH2BASE, LEN, CHN2ROWS+1,
192, CHN2ROWS))
end
elseif i == 6 or i == 8 then
P(string.format(" PALETTE WRONG [%s]: %d of %d differ, first at "
.."+%d -- %d at EVEN offsets (the HIGH half of a "
.."word), %d at ODD.",
SHORT[i+1], bad, LEN, first, bad_hi, bad_lo))
elseif i == 4 then
-- THE CLAIM IS NOT "half the bytes differ". In masked 256-colour
-- mode gvram_w takes `data & 0x00ff` and ignores mem_mask, so a byte
@@ -175,6 +253,30 @@ SUB = emu.add_machine_frame_notifier(function()
SHORT[i+1], bad, LEN, first))
end
end
if i == 7 then
-- THE ATTRIBUTION CONTROL'S SECOND CLAIM, and the one that makes run
-- 7 mean something: the same transfer aimed 20 KB away leaves the
-- palette exactly as the 68000 poisoned it. If this comes back with
-- the disc's bytes in it, something other than the channel's MAR
-- decides what reaches $E82000 and run 7 measured that instead.
local stale, first_s = 0, nil
for j = 0, PALN-1 do
if SP:read_u16(PS8 + 2*j) ~= ((POIS | j) & 0xffff) then
stale = stale + 1; first_s = first_s or j
end
end
if stale == 0 then
P(string.format(" PALETTE UNTOUCHED BY THE CONTROL: %d of %d words "
.."still read the poison the 68000 wrote, so the "
.."bytes in run 7 got there because the channel's "
.."MAR pointed at $%06X.", PALN, PALN, PAL))
else
P(string.format(" CONTROL DID NOT FAIL [palctl]: %d of %d palette "
.."words are no longer poison (first at entry %d) "
.."-- the palette changed without a channel aimed "
.."at it.", stale, PALN, first_s))
end
end
if i > 0 then
-- THE DISCRIMINATOR. MTC as the instruction after START saw it, and
-- the number of times the CPU went round its own wait loop.