fix(cd): I2CNTRL FIFO-not-empty status bit is read-only (Dragon's Lair wedge)

Device log on Dragon's Lair (bios boot): after streaming an FMV segment
the game does a read-modify-write of I2CNTRL that carries the set b4
(FIFO-not-empty, read-only status per the BUTCH register doc and MiSTer
butch.v) back in the write data, then flushes the FIFO with a
"read FIFO_DATA; btst #4,I2CNTRL; bne" loop.  We stored the whole
written word and echoed it on reads, so the flush loop never saw the
FIFO go empty: the 68K wedged at $36B2, the GPU parked in its mailbox
wait at $F03270, and video froze right after the play command
(cd_seek_wedge frame=1473, fifo_drains frozen at 62964).

Mask b4 on both the word and byte write paths; reads already compute it
from fifoDataReady.  Same silicon pattern as the existing BUTCH+2
enable/status split.

Also add an I2S_CTRL event to the CD trace ring (fires on data-enable
bit-2 edges) -- the refill loop silently parks while that bit is clear,
and the missing OFF-edge signal is what this diagnosis needed.

Verified: Dragon's Lair now streams five more transfer cycles (seeks
2 -> 10, drains 62k -> 214k) and reaches in-game artwork; new contract
test butch_i2cntrl_status_bit_readonly (18/18 BUTCH tests); full suite
green; c89-lint clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Joseph MattielloandClaude Fable 5 committed 2026-07-15 20:57:29 -04:00
1 parent ba8e1fe5ac
commit f74ce090d7
2 files changed
+71 -2

No files matched your search

+51 -2
View File
@@ -335,12 +335,17 @@ enum
CD_TRACE_FIFO_FILL, /* fifoDataReady transitioned to true */
CD_TRACE_FIFO_DRAIN, /* FIFO_DRAIN_READS reached, fifoDataReady cleared */
CD_TRACE_STOP, /* $0200 (STOP) command processed */
CD_TRACE_HLE_READ /* HLE CD_read (see jagcd_hle.c) -- carries LBA */
CD_TRACE_HLE_READ, /* HLE CD_read (see jagcd_hle.c) -- carries LBA */
CD_TRACE_I2S_CTRL /* I2CNTRL data-enable (bit 2) transition; value =
new low word. The FIFO refill loop silently
parks while this bit is clear, so an OFF edge
with no later ON edge explains every "drains
frozen" wedge that isn't a STOP. */
};
static const char * const cdTraceKindName[] = {
"DSA_TX", "DSA_RX", "SEEK_START", "SEEK_DONE",
"FIFO_FILL", "FIFO_DRAIN", "STOP", "HLE_READ"
"FIFO_FILL", "FIFO_DRAIN", "STOP", "HLE_READ", "I2S_CTRL"
};
typedef struct
@@ -374,6 +379,11 @@ static uint32_t cdSeekStartCount = 0;
static uint32_t cdSeekDoneCount = 0;
static uint32_t cdFifoDrainCount = 0;
/* Last-observed I2CNTRL data-enable state (bit 2 of the low byte), so
* writes that don't change it stay out of the trace ring. -1 = unknown
* (traces the first write after reset regardless of value). */
static int cdTraceLastI2SEnable = -1;
/* First (boot-relevant) seek target LBA, captured post-redirect the first
* time a non-redundant $12xx seek runs after reset. 0xFFFFFFFF = none. */
static uint32_t diag_firstSeekBlock = 0xFFFFFFFFu;
@@ -416,6 +426,22 @@ static void CDTracePush(uint16_t kind, uint16_t value, uint32_t blk)
cdTraceCount++;
}
/* Trace I2CNTRL data-enable (bit 2) edges. Called after any store that
* touches the I2CNTRL low byte (word or byte path -- the GPU ISR uses
* 32-bit stores that arrive as two word writes, but byte stores exist). */
static void CDTraceI2SWrite(void)
{
int enable = (cdRam[I2CNTRL + 3] & 0x04) ? 1 : 0;
if (enable != cdTraceLastI2SEnable)
{
CDTracePush(CD_TRACE_I2S_CTRL,
(uint16_t)((cdRam[I2CNTRL + 2] << 8) | cdRam[I2CNTRL + 3]),
block);
cdTraceLastI2SEnable = enable;
}
}
/* Public wrapper for jagcd_hle.c -- the HLE CD_read path performs a
* synchronous seek+transfer in one call (no separate seek-delay state
* machine like the real-BIOS path), so a single HLE_READ event carrying
@@ -500,6 +526,7 @@ void CDROMReset(void)
dsaQueueHead = 0;
dsaQueueTail = 0;
dsaQueueCount = 0;
cdTraceLastI2SEnable = -1;
diag_butchExecCalls = 0;
diag_fifoIRQsFired = 0;
@@ -1100,7 +1127,13 @@ TOC: 2 10 00 b 00:00:00 00 54:26:17 <-- Track #11
void CDROMWriteByte(uint32_t offset, uint8_t data, uint32_t who/*=UNKNOWN*/)
{
offset &= 0xFF;
/* I2CNTRL bit 4 (FIFO-not-empty) is read-only status, computed from
* FIFO state on read -- see CDROMWriteWord below. */
if (offset == I2CNTRL + 3)
data &= ~0x10;
cdRam[offset] = data;
if (offset == I2CNTRL + 3)
CDTraceI2SWrite();
}
void CDROMWriteWord(uint32_t offset, uint16_t data, uint32_t who/*=UNKNOWN*/)
@@ -1123,8 +1156,24 @@ void CDROMWriteWord(uint32_t offset, uint16_t data, uint32_t who/*=UNKNOWN*/)
return;
}
/* I2CNTRL bit 4 (FIFO-not-empty) is read-only status ("When read: b4 -
* FIFO state is not empty if 1" -- BUTCH register doc above), computed
* from fifoDataReady in the read handler. Games do read-modify-write
* cycles on I2CNTRL, so a set status bit rides along in the write data;
* storing it made the bit stick at 1 forever. Device-traced on Dragon's
* Lair (bios): end-of-transfer writes $0011 (RMW carrying b4), then
* drains the FIFO with a "read FIFO_DATA; btst #4,I2CNTRL; bne" flush
* loop that can never see empty -- 68K wedges, GPU parks in its mailbox
* wait, video freezes after the play command. Same silicon pattern as
* the BUTCH+2 enable/status split above. */
if (offset == I2CNTRL + 2)
data &= ~0x0010;
SET16(cdRam, offset, data);
if (offset == I2CNTRL + 2)
CDTraceI2SWrite();
if (offset < UNKNOWN) // Don't log EEPROM bus writes ($2C/$2E) — too noisy
CD_LOG("WriteWord offset=0x%02X data=0x%04X [PC=$%06X]\n", offset, data, m68k_get_reg(NULL, M68K_REG_PC));
+20
View File
@@ -281,6 +281,25 @@ TEST(butch_int_fifo_requires_both_bits)
* $70nn echo is silently replaced by the $17nn mode status; the game polls
* DS_DATA repeatedly for its echo and wedges (cd_seek_wedge, GPU spinning).
* Real hardware queues each drive response in the DSA RX path. */
TEST(butch_i2cntrl_status_bit_readonly)
{
uint16_t readback;
core.CDROMReset();
/* Games do read-modify-write cycles on I2CNTRL, so a set FIFO-not-empty
* status bit (b4, read-only per butch.v) rides along in the write data.
* Device-traced on Dragon's Lair (bios): end-of-transfer writes $0011,
* then flushes the FIFO with a "btst #4; bne" loop. If the written b4
* is stored and echoed, the loop never sees FIFO-empty and the 68K
* wedges. With no FIFO data pending, b4 must read back 0 regardless
* of what was written. */
core.CDROMWriteWord(BUTCH_I2CNTRL + 2,
BUTCH_I2S_DRIVE | BUTCH_I2S_FIFONEMPTY, CALLER_M68K);
readback = core.CDROMReadWord(BUTCH_I2CNTRL + 2, CALLER_M68K);
CHECK_EQ(readback & BUTCH_I2S_FIFONEMPTY, 0);
/* the writable bits still stick */
CHECK_EQ(readback & BUTCH_I2S_DRIVE, BUTCH_I2S_DRIVE);
}
TEST(butch_dsa_back_to_back_responses)
{
bool (*openImage)(const char *);
@@ -435,6 +454,7 @@ int main(int argc, char *argv[])
RUN_TEST(butch_dsa_command_write);
RUN_TEST(butch_dsa_read_toc_command);
RUN_TEST(butch_dsa_get_status_command);
RUN_TEST(butch_i2cntrl_status_bit_readonly);
RUN_TEST(butch_dsa_back_to_back_responses);
/* FIFO */