Put the container on the chip, and find the held bus costs 463 times the seam

ROADMAP P6c, FINDINGS 68. 78,125 B of a DLXP2's audio out of channel 3,
sample-exact, while the video channel fetches records off the same disc.

The two pieces 67.6 said were missing: the lump buffer (pg_afill/pg_afetch,
three slots and the minimum is unmeasured) and 67.2's remainder accumulator
(pg_apay). The capture prices what the accumulator avoided at 1.26 s of
lip-sync over the game, against 67.2's predicted 1.25.

The finding is the third piece, which nothing had named: the MSM6258 has no
starvation state, so the gap between a channel counting out and the next arm
is a held nibble pair driving the predictor. Stealing, the seam is 0.51 ms
over ten seconds because dma.i's new DM_HOOK services the chip from inside
the transfer wait -- 250,000 of 250,240 looks. Held, the 68000 is halted and
gets 369: every one of the ten lump boundaries has a seam, worst 72.8 ms,
2.31% of the audio. Identical bytes, different sound. 64.3 reaching the audio.

Two bugs, and no counter in the player could see either. Clearing DM_BARV does
not unchain a channel -- OCR bits 3-2 are what it obeys -- and the symptom is
POLL TIMEOUT on the lump and every record after it. And the refill ran one lump
ahead of its ring and overwrote the buffer the channel was reading: 11 of 11
armed, 11 fetched, no starve, and the sound wrong from 0.2 s in. Which is why
the gate is a WAV: verify_packed_audio.py walks the stream one delivered byte
at a time, because MAME's okim6258 resets the nibble select on every write and
a byte is two nibbles only 99.994% of the time.

check.sh ALL GREEN before (tmp/check_s36_start.log) and after
(tmp/check_s36_end.log), with the new stage.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 13:11:37 -07:00
parent e3778f62b0
commit 191f2b47bb
13 changed files with 1413 additions and 20 deletions
+107 -1
View File
@@ -68,11 +68,19 @@ local META = loadfile("packed_meta.lua")()
local PG_FLAG, PG_NFR, PG_FPS, PG_LBA0 = 0x18900, 0x18904, 0x18908, 0x1890C
local PG_RECS, PG_PALL, PG_HELD = 0x18910, 0x18914, 0x18918
local PG_PACEON, PG_ITER = 0x1891C, 0x18920
local PG_CADF, PG_CADA = 0x18924, 0x18928
local PG_CADF, PG_CADA, PG_AUDON = 0x18924, 0x18928, 0x1892C
local PG_SHOWN, PG_ERR, PG_ERRAT = 0x18930, 0x18934, 0x18938
local PG_LATE, PG_LATE1, PG_LATEM = 0x1893C, 0x18940, 0x18944
local PG_VDISP, PG_VD0, PG_TSPIN = 0x18948, 0x1894C, 0x18950
local PG_GSPIN, PG_LOSTV, PG_ARRN = 0x18954, 0x18958, 0x1895C
-- ROADMAP P6c: the audio path's inputs and its own account of what it did.
local PG_AFPS, PG_AHZ, PG_ALBA0 = 0x18960, 0x18964, 0x18968
local PG_NLUMP, PG_ABYTES, PG_APRE = 0x1896C, 0x18970, 0x18974
local PG_AARM, PG_AFET, PG_ABYT = 0x18980, 0x18984, 0x18988
local PG_ADRY, PG_ASEAM, PG_ASRV = 0x1898C, 0x18990, 0x18994
local PG_ACSR, PG_ACER, PG_ALATE = 0x18998, 0x1899C, 0x189A0
local PG_AMTC0, PG_AK, PG_AKF, PG_AACC = 0x189A4, 0x189B0, 0x189B4, 0x189B8
local PG_AFERR, PG_AFERA = 0x189A8, 0x189AC
local PG_ARR = 0x1B000
-- src/player/clock.i and src/player/scsi.i, read for diagnosis only.
local CLK_PACE, CLK_VDISP, CLK_ERR = 0x18034, 0x18064, 0x1806C
@@ -85,6 +93,13 @@ local ITERS = tonumber(os.getenv("DLX_PK_ITER") or "") or 1
local NFR = tonumber(os.getenv("DLX_PK_NFR") or "") or META.nframes
local FPS = tonumber(os.getenv("DLX_PK_FPS") or "") or META.fps
local CSV = os.getenv("DLX_PK_CSV")
-- ROADMAP P6c. OFF by default and it is not a convenience: a run with the chip
-- silent is the CONTROL this one is read against, and every gate that existed
-- before session 36 is that control. DLX_PK_APRE is the lumps fetched before
-- frame 0 -- see src/player/packed.s on why the answer is not 1.
local AUDIO = (os.getenv("DLX_PK_AUD") or "0") == "1"
local APRE = tonumber(os.getenv("DLX_PK_APRE") or "") or 2
local AJSON = os.getenv("DLX_PK_AJSON")
local SNAP_DELAY = 2
local SCERRNAME = {[0]="OK", "SELECTION TIMEOUT -- no target answered",
@@ -114,6 +129,16 @@ local function setup()
SP:write_u32(PG_CADF, META.cad_f or 0)
SP:write_u32(PG_CADA, META.cad_a or 0)
SP:write_u32(PG_SHOWN, 0)
-- P6c. AUDON is separate from the cadence on purpose: the LBA arithmetic has
-- to skip the lumps whether or not a chip is being fed, and a run that skips
-- them without playing them is the control this one is measured against.
SP:write_u32(PG_AUDON, (AUDIO and (META.has_audio or 0) == 1) and 1 or 0)
SP:write_u32(PG_AFPS, META.fps) -- the CONTAINER's, NOT the pace
SP:write_u32(PG_AHZ, META.aud_hz or 0)
SP:write_u32(PG_ALBA0, META.lba_aud or 0)
SP:write_u32(PG_NLUMP, META.n_lumps or 0)
SP:write_u32(PG_ABYTES, META.aud_bytes or 0)
SP:write_u32(PG_APRE, APRE)
P(string.format("packed.bin=%d B, %dx%d %d fps, %d of %d frames, %d passes",
#code, META.W, META.H, META.fps, NFR, META.nframes, ITERS))
local cad = ""
@@ -130,6 +155,19 @@ local function setup()
.."whole cost of it on the video path",
META.aud_bytes, META.aud_hz, META.n_lumps, META.cad_a, META.cad_f))
end
if AUDIO and (META.has_audio or 0) == 1 then
P(string.format("AUDIO ON: lump k at LBA %d + k*%d, payload 11*%d/24 B a "
.."group -- the PAYLOAD and not the %d B lump (FINDINGS "
.."67.2). Decoder from the header: %s/%s, %d-bit clamp, "
.."accumulator %d at PLAY. Prefill %d lumps of %d.",
META.lba_aud, META.cad_f*META.rec_sectors + META.cad_a,
META.aud_hz, META.cad_a*512, META.aud_variant,
META.aud_order, META.aud_bits, META.aud_init,
APRE, META.n_lumps))
elseif (META.has_audio or 0) == 1 then
P("audio present in the container and NOT played -- this is the silent "
.."control (DLX_PK_AUD=1 plays it)")
end
P(string.format("channel: %s, %s",
HELD and "BUS HELD (burst, max rate)" or "CYCLE STEALING",
PACED and ("SELF-PACED at "..FPS.." fps off V-DISP"
@@ -268,6 +306,74 @@ SUB = emu.add_machine_frame_notifier(function()
P(string.format("CPU: %d trips round the TRANSFER wait in total, %d round "
.."the PACE gate", SP:read_u32(PG_TSPIN),
SP:read_u32(PG_GSPIN)))
if AUDIO and (META.has_audio or 0) == 1 then
local armed, fet = SP:read_u32(PG_AARM), SP:read_u32(PG_AFET)
local byt, dry = SP:read_u32(PG_ABYT), SP:read_u32(PG_ADRY)
local seam, srv = SP:read_u32(PG_ASEAM), SP:read_u32(PG_ASRV)
local late, acc = SP:read_u32(PG_ALATE), SP:read_u32(PG_AACC)
P(string.format("AUDIO: %d of %d lumps armed, %d fetched, %d B of "
.."payload handed to the chip (the stream is %d B)",
armed, META.n_lumps, fet, byt, META.aud_bytes))
-- THE PADDING, CHARGED. A player that fed the chip the whole lump
-- would have handed it n_lumps*A*512 B; the difference is the drift
-- FINDINGS 67.2 priced at 1.25 s over the game, and printing both
-- numbers is the only way the accumulator is visible from outside.
-- FULL groups only. The last lump is short when the scene's frame
-- count is not a multiple of F, and averaging that in reports the
-- scene's TAIL as though it were the cadence -- which is a different
-- number from the drift and looks like a worse one. The percentage
-- lives in verify_packed_audio.py, which knows each lump's payload.
local nfull = META.n_lumps
if NFR % META.cad_f ~= 0 then nfull = nfull - 1 end
local lumpb = nfull * META.cad_a * 512
P(string.format(" %d whole groups: %d B of lump space for the "
.."payload the accumulator asked for. The whole-lump "
.."player feeds the chip that space, and the excess is "
.."DRIFT and not waste (67.2). Accumulator left at "
.."%d/%d.", nfull, lumpb, acc, 2*META.fps))
P(string.format(" service: %d calls, %d found the channel "
.."counted out, %d of those had NO lump ready (a "
.."STARVE -- the chip replays its last byte)",
srv, seam, dry))
P(string.format(" re-arms with MTC still non-zero: %d (bytes "
.."fetched and never played; 0 is the correct value)",
late))
local ferr = SP:read_u32(PG_AFERR)
if ferr ~= 0 then
P(string.format(" A LUMP FETCH FAILED on lump %d: %s -- the "
.."picture is unaffected and the sound is gone, which "
.."is why this has its own error word",
SP:read_u32(PG_AFERA), SCERRNAME[ferr] or ("code "..ferr)))
end
P(string.format(" channel 3 at the end: CSR=$%02X CER=$%02X, "
.."MTC one instruction after the first START = %d",
SP:read_u32(PG_ACSR), SP:read_u32(PG_ACER),
SP:read_u32(PG_AMTC0)))
-- WHAT HOLDING THE BUS COSTS A SECOND CONSUMER, and it is this line.
-- Stealing, pg_aserv runs from inside dma.i's transfer wait as well as
-- twice a frame; held, the 68000 is HALTED for the whole transfer and
-- the two frame-loop calls are all it gets. The ratio is the audio's
-- half of FINDINGS 64.3.
P(string.format(" -> %.1f service calls per frame shown. %s",
shown > 0 and srv/shown or 0,
HELD and ("BUS HELD: the 68000 is halted for the whole "
.."transfer, so DM_HOOK never runs and this is the "
.."two frame-loop calls and nothing else.")
or ("CYCLE STEALING: DM_HOOK ran from inside the "
.."transfer wait, which is where a 68000 driving this "
.."video path has any time at all.")))
if AJSON then
local f = io.open(AJSON, "w")
f:write(string.format('{"armed":%d,"fetched":%d,"bytes":%d,'
..'"starve":%d,"seam":%d,"serv":%d,"late":%d,"acc":%d,'
..'"csr":%d,"cer":%d,"held":%s,"fps":%d,"shown":%d}\n',
armed, fet, byt, dry, seam, srv, late, acc,
SP:read_u32(PG_ACSR), SP:read_u32(PG_ACER),
HELD and "true" or "false", FPS, shown))
f:close()
P("audio counters -> "..AJSON)
end
end
local open = 0
for _,v in ipairs(r20seen) do open = open + v end
P(string.format("WRITE WINDOW OPEN on %d of %d host frames (%.1f%%) -- "