mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 12:47:58 -10:00
fix(cd): let game-installed CD drivers own the transfer; fix HLE progress cadence (IS2 match-load checksum)
Iron Soldier 2's match load rejected every HLE CD_read delivery and retried forever (68K looping $6AAA-$6AD2, wedge at frame ~331). Three distinct defects, each pinned byte-level: 1. Jump-table hijack of a game-owned driver (the decisive fix). IS2's loader copies a complete BIOS-workalike CD driver over the $3000-$3DFF jump table, replacing our $4E75 HLE stubs with real code (e.g. $303C: `bra` into its own CD_read, $304E: a CD_poll that reads the write pointer straight from [$3074] -> GPU data area). The HLE hooks kept firing at those PCs anyway, so the HLE stream and the driver's native DSA-seek + GPU-ISR FIFO transfer double-drove the same buffer. The native path is exactly what BIOS mode runs -- and it works. JaguarCDHLEHook now steps aside whenever the entry PC no longer holds our $4E75 stub. 2. Byte-granular progress reporting broke incremental checksummers. The game sums the buffer *as it polls* (ADD.L (A2)+,D0 against the advancing pointer) and never re-reads. Reporting dest+written at byte granularity let a partial long expose up to 3 not-yet-written bytes; the running sum went stale and the final compare at $6AD4 (expected D5=$D76563EE, from the match table at $6B88+16*idx+8) missed even though the completed buffer was byte-exact vs the disc (verified: full-buffer sum of the delivered 273360 bytes from LBA 224643+1846 == $D76563EE). Stream progress is now reported with the real GPU ISR cadence: dest-4 advancing in whole 32-byte FIFO batches, never past delivered data. 3. BUTCH FIFO feed vs a concurrent HLE stream. The driver's own seek ($1031/$1139/$120C -> LBA 224637) re-armed the FIFO mid-stream and its GPU ISR (gpupc $F03F32) drained stale audio words to its live write pointer, stomping 13 bytes at the checksum base ($8C029-$8C035: RAM held 97480102... where the disc holds 7441BAC2..., first divergence at stream offset 100037 of a 273360-byte transfer that was otherwise byte-identical). BUTCH now holds FIFO-ready off while an HLE stream is active, and an accepted HLE data read stops the CDDA play/FIFO feed like the real CD_read's seek does. Also: the BUTCH edge tracker now matches GPUSetIRQLine's assert-drop condition (halted OR single-step-parked GPU captures no edges) via GPUCanCaptureIRQ(), so an edge dropped during a SINGLE_STEP handshake window is not marked consumed. Validation: - IS2 HLE repro (save state + press B) runs 1800 frames clean; the match visibly loads and plays (in-cockpit gameplay at frame 1800, audio RMS ~1218). Was: exit 42 at frame 331. - IS2 BIOS repro still exits 0 (900/1800 frames). - cd_boot_matrix (3000 frames): no backward stage moves; forward: IS2 hle 0/1->1/1, Battle Morph bios pc_escape->GAME_CODE, Baldies hle 0/1->1/1. - Battle Morph wedge probe 1200 frames clean, both blitter paths. - make TEST_EXPORTS=1 test: 0 failed. c89-lint clean on all touched files. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
8675138374
commit
73438bae78
5 files changed
+118
-19
No files matched your search
+60
-15
@@ -20,6 +20,7 @@
|
||||
#include <string.h>
|
||||
#include "cdintf.h"
|
||||
#include "jagcd_boot.h"
|
||||
#include "jagcd_hle.h"
|
||||
#include "log.h"
|
||||
#include "gpu.h"
|
||||
#include "dsp.h"
|
||||
@@ -324,6 +325,22 @@ static uint32_t dsaQueueCount = 0;
|
||||
#define DSA_RESPONSE_DELAY_TICKS 4
|
||||
static int32_t dsaResponseDelay = 0;
|
||||
|
||||
/* While an HLE CD_read stream is delivering into the destination buffer,
|
||||
* BUTCH must not flag FIFO data ready: the HLE stream *is* the transfer,
|
||||
* and the game's GPU CD ISR would otherwise drain unframed FIFO words
|
||||
* into the same buffer at its live write pointer, corrupting freshly
|
||||
* streamed bytes. Iron Soldier 2's loader issues its own DSA seek
|
||||
* ($10/$11/$12) to the data track in parallel with the BIOS CD_read
|
||||
* call; the seek-done path re-armed the FIFO mid-stream and the ISR
|
||||
* stomped 13 bytes at the match checksum base -> checksum mismatch ->
|
||||
* infinite retry. Once the stream completes the ISR's write pointer is
|
||||
* parked past the end address, so post-stream FIFO drains are harmless
|
||||
* (and some drivers rely on them to see the drive "playing"). */
|
||||
static bool FIFOFeedAllowed(void)
|
||||
{
|
||||
return !JaguarCDHLEStreamActive();
|
||||
}
|
||||
|
||||
static void DSAQueuePush(uint16_t response)
|
||||
{
|
||||
if (dsaQueueCount < DSA_QUEUE_SIZE)
|
||||
@@ -586,6 +603,27 @@ void CDROMHLESetAudioPlaying(int playing)
|
||||
CD_LOG("HLE audio playing=%d (block=%u)\n", playing, block);
|
||||
}
|
||||
|
||||
/* An HLE data CD_read models the real BIOS CD_read's DSA seek to the
|
||||
* data track: any in-progress audio play ends there, and the BUTCH FIFO
|
||||
* stops feeding the GPU CD ISR (the HLE stream delivers the data
|
||||
* itself, so the ISR has nothing to drain). Leaving the CDDA feed
|
||||
* running double-drives the transfer: the game's GPU CD ISR keeps
|
||||
* firing on FIFO-ready edges and drains stale audio words to its live
|
||||
* write pointer inside the destination buffer, stomping freshly
|
||||
* streamed data (Iron Soldier 2 match load: the ISR corrupted 13 bytes
|
||||
* at the checksum base -> checksum mismatch -> infinite retry loop).
|
||||
* Audio resumes when the game calls CD_I2S_enable (hle_post_read_lba)
|
||||
* or issues a bit-31 just-seek CD_read, exactly as on hardware. */
|
||||
void CDROMHLEDataReadBegin(void)
|
||||
{
|
||||
if (!haveCDGoodness)
|
||||
return;
|
||||
cdPlaying = false;
|
||||
fifoDataReady = false;
|
||||
fifoReadCount = 0;
|
||||
CD_LOG("HLE data read: audio play + FIFO feed stopped\n");
|
||||
}
|
||||
|
||||
/* Park the drive head at `lba`, paused, with the data-path framing the
|
||||
* seek handler establishes (cdBufPtr=2: BUTCH's one-word capture skew —
|
||||
* see the $12xx Goto Frame handler). Used by the BIOS boot-stub
|
||||
@@ -797,7 +835,7 @@ void BUTCHExec(uint32_t cycles)
|
||||
cdPlaying = true;
|
||||
{
|
||||
bool i2sDataEnabled = (cdRam[I2CNTRL + 3] & 0x04) != 0;
|
||||
if (i2sDataEnabled)
|
||||
if (i2sDataEnabled && FIFOFeedAllowed())
|
||||
{
|
||||
fifoDataReady = true;
|
||||
fifoReadCount = 0;
|
||||
@@ -825,14 +863,14 @@ void BUTCHExec(uint32_t cycles)
|
||||
{
|
||||
bool i2sDataEnabled = (cdRam[I2CNTRL + 3] & 0x04) != 0;
|
||||
fifoFillDelay--;
|
||||
if (fifoFillDelay == 0 && cdPlaying && i2sDataEnabled)
|
||||
if (fifoFillDelay == 0 && cdPlaying && i2sDataEnabled && FIFOFeedAllowed())
|
||||
{
|
||||
fifoDataReady = true;
|
||||
fifoReadCount = 0;
|
||||
CDTracePush(CD_TRACE_FIFO_FILL, 0, block);
|
||||
CD_LOG("BUTCHExec: FIFO half-full — ready for GPU ISR\n");
|
||||
}
|
||||
else if (fifoFillDelay == 0 && cdPlaying && !i2sDataEnabled)
|
||||
else if (fifoFillDelay == 0 && cdPlaying && (!i2sDataEnabled || !FIFOFeedAllowed()))
|
||||
{
|
||||
fifoFillDelay = 1; // Retry next tick
|
||||
}
|
||||
@@ -956,14 +994,17 @@ void BUTCHExec(uint32_t cycles)
|
||||
m68k_set_irq(2);
|
||||
}
|
||||
}
|
||||
/* The edge is only "consumed" if a RUNNING GPU could capture it —
|
||||
* a halted GPU's interrupt logic is not clocked (GPUSetIRQLine
|
||||
* drops asserts while stopped). Keeping the tracker false across
|
||||
* a stopped window means the still-high line re-asserts on the
|
||||
* first tick after the 68K restarts the GPU; marking it true there
|
||||
* would swallow the edge forever and stall the transfer (the
|
||||
* press-B@550/650 variant of the Hover Strike lockup). */
|
||||
cdPrevShouldIRQ = shouldIRQ && GPUIsRunning();
|
||||
/* The edge is only "consumed" if a GPU that can capture IRQs saw
|
||||
* it — a halted OR single-step-parked GPU's interrupt logic is not
|
||||
* clocked (GPUSetIRQLine drops asserts in both states). Keeping
|
||||
* the tracker false across such a window means the still-high line
|
||||
* re-asserts on the first tick after the 68K lets the GPU run;
|
||||
* marking it true there would swallow the edge forever and stall
|
||||
* the transfer (the press-B@550/650 variant of the Hover Strike
|
||||
* lockup; Iron Soldier 2's post-load engine bring-up drives the
|
||||
* GPU through a SINGLE_STEP handshake and lost the FIFO-ready
|
||||
* edge the same way — the streaming ISR never got its first IRQ). */
|
||||
cdPrevShouldIRQ = shouldIRQ && GPUCanCaptureIRQ();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1060,7 +1101,7 @@ uint16_t CDROMReadWord(uint32_t offset, uint32_t who/*=UNKNOWN*/)
|
||||
// at seek completion in BUTCHExec. Re-assert in case STOP
|
||||
// cleared them between seek completion and this read.
|
||||
cdPlaying = true;
|
||||
if (!fifoDataReady)
|
||||
if (!fifoDataReady && FIFOFeedAllowed())
|
||||
{
|
||||
fifoDataReady = true;
|
||||
fifoReadCount = 0;
|
||||
@@ -1078,7 +1119,8 @@ uint16_t CDROMReadWord(uint32_t offset, uint32_t who/*=UNKNOWN*/)
|
||||
{
|
||||
data = 0x0100 | (cdCmd & 0xFF); // Echo: $01nn -> $01nn (Found)
|
||||
cdPlaying = true;
|
||||
fifoDataReady = true;
|
||||
if (FIFOFeedAllowed())
|
||||
fifoDataReady = true;
|
||||
CD_LOG("Play Title response consumed — playback and FIFO now active\n");
|
||||
}
|
||||
else if ((cdCmd & 0xFF00) == 0x0200) // Stop CD
|
||||
@@ -1144,8 +1186,11 @@ TOC: 2 10 00 b 00:00:00 00 54:26:17 <-- Track #11
|
||||
{
|
||||
data = 0x0100; // Found (seek complete)
|
||||
cdPlaying = true;
|
||||
fifoDataReady = true;
|
||||
fifoReadCount = 0;
|
||||
if (FIFOFeedAllowed())
|
||||
{
|
||||
fifoDataReady = true;
|
||||
fifoReadCount = 0;
|
||||
}
|
||||
CD_LOG("Seek response $0100 consumed (direct) — cdPlaying=true\n");
|
||||
}
|
||||
else if ((cdCmd & 0xFF00) == 0x1400) // Read "full" session TOC
|
||||
|
||||
@@ -69,6 +69,11 @@ void CDTraceHLERead(uint32_t lba, uint16_t byteCountTrunc);
|
||||
* pause/resume the stream. */
|
||||
void CDROMHLEStartAudio(uint32_t lba);
|
||||
void CDROMHLESetAudioPlaying(int playing);
|
||||
/* Stop the CDDA play + BUTCH FIFO feed when an HLE data CD_read is
|
||||
* armed (models the BIOS CD_read's seek away from the audio position;
|
||||
* prevents the game's GPU CD ISR from draining stale FIFO words into
|
||||
* the destination buffer while the HLE stream delivers the data). */
|
||||
void CDROMHLEDataReadBegin(void);
|
||||
|
||||
/* Read-only accessor for crash_detect.c's cd_seek_wedge watchdog. Any
|
||||
* pointer may be NULL. seekStarts/seekDones count genuine (non-redundant)
|
||||
|
||||
+41
-4
@@ -390,6 +390,15 @@ static void HLEHandleCDRead(void)
|
||||
return;
|
||||
}
|
||||
|
||||
/* Accepted data read: the real CD_read's DSA seek ends any running
|
||||
* audio play before data flows. Stop the CDDA/FIFO feed for the
|
||||
* duration of this transfer — otherwise the game's GPU CD ISR keeps
|
||||
* draining stale FIFO audio words to its live write pointer inside
|
||||
* the destination buffer, corrupting the freshly streamed data
|
||||
* (Iron Soldier 2 match-load checksum stomp). Audio restarts via
|
||||
* CD_I2S_enable / bit-31 just-seek, as on hardware. */
|
||||
CDROMHLEDataReadBegin();
|
||||
|
||||
/* Clear the DSP completion flag so polling code sees a 0 -> $FFFFFFFF
|
||||
* transition once the transfer finishes. Real hardware: the GPU CD ISR
|
||||
* writes $FFFFFFFF here when its write pointer reaches the end address. */
|
||||
@@ -912,10 +921,22 @@ void JaguarCDHLEStreamTick(void)
|
||||
}
|
||||
|
||||
/* Advance the write pointer the game's poll loop watches — at the
|
||||
* base latched when this read was armed (see statusBase). */
|
||||
* base latched when this read was armed (see statusBase).
|
||||
*
|
||||
* Report it with the real GPU ISR's cadence: the pre-decremented
|
||||
* pointer (dest-4) advancing in whole 32-byte FIFO batches, and
|
||||
* never past the bytes actually delivered. Games read this pointer
|
||||
* straight out of the GPU data area (IS2's driver CD_poll at $304E
|
||||
* does `movea.l ($3074).l,a0; movea.l (a0),a0`) and checksum the
|
||||
* buffer INCREMENTALLY against it as it advances. A byte-granular
|
||||
* `dest+written` value exposed partial longs whose tail bytes had
|
||||
* not been written yet — the running sum consumed up to 3 stale
|
||||
* bytes, never re-read them, and the final compare missed (Iron
|
||||
* Soldier 2 match-load retry loop). */
|
||||
if (hleStream.statusBase != 0)
|
||||
GPUWriteLong(hleStream.statusBase + 0,
|
||||
hleStream.dest + hleStream.written, 0);
|
||||
hleStream.dest - 4 + ((hleStream.written + 4) / 32) * 32,
|
||||
0);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -965,10 +986,13 @@ static void HLEHandleCDPoll(void)
|
||||
* take a sequencing branch. A0=0 fails both completion idioms. */
|
||||
if (hleStream.active)
|
||||
{
|
||||
/* Transfer still streaming — report the partial position.
|
||||
/* Transfer still streaming — report the partial position with
|
||||
* the same 32-byte-batch quantization as the GPU-data-area
|
||||
* pointer (see JaguarCDHLEStreamTick): never past the bytes
|
||||
* actually delivered, so incremental checksummers stay exact.
|
||||
* A1 (the [+8] error status) stays 0: boot stubs abort on
|
||||
* nonzero A1, it is NOT a progress counter. */
|
||||
a0_val = hleStream.dest - 4 + hleStream.written;
|
||||
a0_val = hleStream.dest - 4 + ((hleStream.written + 4) / 32) * 32;
|
||||
a1_val = 0;
|
||||
}
|
||||
else if (hle_read_pending)
|
||||
@@ -1269,6 +1293,19 @@ bool JaguarCDHLEHook(uint32_t pc)
|
||||
if (pc < BIOS_JUMPTABLE_BASE || pc > 0x00306B)
|
||||
return false;
|
||||
|
||||
/* Step aside for games that install their own BIOS-workalike CD
|
||||
* driver over the jump table (Iron Soldier 2's loader copies a full
|
||||
* driver to $3000+, replacing our $4E75 stubs with real code). Such
|
||||
* a driver performs the complete transfer protocol natively — DSA
|
||||
* seek, GPU CD ISR, FIFO drain — against the BUTCH emulation, which
|
||||
* is exactly the path BIOS mode runs (and it works). Intercepting
|
||||
* on top of it double-drives the transfer: the HLE stream and the
|
||||
* driver's ISR write the same buffer, and the HLE CD_poll fights the
|
||||
* driver's own register returns. Hook only while OUR stub is still
|
||||
* what executes at the entry PC. */
|
||||
if (GET16(jaguarMainRAM, pc) != 0x4E75)
|
||||
return false;
|
||||
|
||||
switch (pc)
|
||||
{
|
||||
case JT_CD_READ:
|
||||
|
||||
@@ -842,6 +842,13 @@ int GPUIsRunning(void)
|
||||
return GPU_RUNNING ? 1 : 0;
|
||||
}
|
||||
|
||||
/* See gpu.h: matches the assert-drop condition in GPUSetIRQLine (a
|
||||
* stopped or single-step-parked core samples no interrupt lines). */
|
||||
int GPUCanCaptureIRQ(void)
|
||||
{
|
||||
return (GPU_RUNNING && !(gpu_control & 0x08)) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* Is the Object Processor interrupt (IRQ3) enabled in G_FLAGS? Used by
|
||||
* OPProcessList to decide whether a GPU object's inline release-wait can
|
||||
* ever be serviced — if the game never enabled IRQ3, waiting is futile. */
|
||||
|
||||
@@ -35,6 +35,11 @@ void GPUCPUINTCallback(void);
|
||||
void GPUResetStats(void);
|
||||
uint32_t GPUReadPC(void);
|
||||
int GPUIsRunning(void);
|
||||
/* True when the RISC core cannot capture interrupt asserts: stopped
|
||||
* (GPUGO clear) or parked in the SINGLE_STEP coprocessor barrier.
|
||||
* Mirrors the drop condition in GPUSetIRQLine/GPUHandleIRQs so edge
|
||||
* trackers (BUTCH) don't mark a dropped assert as consumed. */
|
||||
int GPUCanCaptureIRQ(void);
|
||||
int GPUOPInterruptEnabled(void);
|
||||
void GPUDumpState(const char *tag);
|
||||
|
||||
|
||||
Reference in new issue
Block a user