From c978d7f5f4e43a7963e9280690e377cf5bf9242f Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Tue, 2 Jun 2026 12:19:19 +0200 Subject: [PATCH] few fixes --- cdrom.jam | 69 +++++++++++++++++++++++++++++++++++++++++-------------- cpu.jam | 6 +++++ gte.jam | 57 ++++++++++++++++++++++++--------------------- mdec.jam | 11 +++++++++ xa.jam | 7 +++++- 5 files changed, 106 insertions(+), 44 deletions(-) diff --git a/cdrom.jam b/cdrom.jam index 867e031..974088d 100644 --- a/cdrom.jam +++ b/cdrom.jam @@ -31,7 +31,7 @@ const { Disc, discRead, discLoaded, discTrackCount, discTrackLba, discTrackNumber, discQuery } = import("disc"); const { xaSectorIsAudio, xaSectorMatchesFilter, xaSectorIsStereo, - xaDecodeSector + xaSectorIs18kHz, xaDecodeSector } = import("xa"); const { spuPushCdSample } = import("spu"); @@ -146,6 +146,10 @@ pub const Cdrom = struct { // sectors so block-N's predictor sees block-(N-1)'s tail. xaLh: [2]i32, xaRh: [2]i32, + // Last resampled OUTPUT sample per channel — the `ls` seed for the next + // sector's XA→44.1kHz rate conversion (psxe xa_prev_left/right_sample). + xaPrevL: i32, + xaPrevR: i32, // Parameter / response FIFOs. Each is a 32-byte ring with u8 // head/tail; the indexes wrap modulo 32 via `& 0x1F`, and the // empty test compares the raw head/tail bytes directly. @@ -173,6 +177,7 @@ pub const Cdrom = struct { dataRidx: 0, dataWidx: 0, xaLh: [0, 0], xaRh: [0, 0], + xaPrevL: 0, xaPrevR: 0, paramFifo: [0; 32], respFifo: [0; 32], dataBuf: [0; 2352], @@ -948,24 +953,54 @@ pub const Cdrom = struct { monoBuf.asMutPtr(), self.xaLh.asMutPtr(), self.xaRh.asMutPtr()); - var k: u32 = 0; - while (k < n) { - var l: i32 = 0; - var r: i32 = 0; - if (stereo) { - l = leftBuf[k]; - r = rightBuf[k]; - } else { - l = monoBuf[k]; - r = monoBuf[k]; + // Resample from the sector's native rate (37800 Hz, or 18900 Hz for an + // f18khz sector) to the SPU's 44100 Hz output rate before pushing. The + // SPU CD FIFO is drained one entry per 44100 Hz output sample, so + // pushing native-rate samples 1:1 plays XA ~17% too fast and overruns + // the FIFO (most samples dropped → warble). Faithful port of psxe + // cdrom_resample_xa_buf (audio.c:52-86): conceptually a 7×-upsample — + // whose `(k+1)/8` integer-divides to 0, making it a sample-and-hold of + // the previous source sample — then decimate by m=f18khz?3:6, yielding + // `resampleCount` samples at 44100 Hz. Output j samples source index + // q-1 where q=(j*m)/7; q==0 uses the previous sector's last output + // sample (psxe's `ls`, carried in xaPrevL/R). n (2016 stereo / 4032 + // mono) bounds the source; q-1 never reaches it for any valid j. + const f18: bool = xaSectorIs18kHz(scratch.asPtr()); + var m: u32 = 6; + if (f18) { m = 3; } + var resampleCount: u32 = 2352; + if (!stereo) { resampleCount = 4704; } + if (f18) { resampleCount = resampleCount * 2; } + const lsL: i32 = self.xaPrevL; + const lsR: i32 = self.xaPrevR; + var outL: i32 = lsL; + var outR: i32 = lsR; + var j: u32 = 0; + while (j < resampleCount) { + const q: u32 = (j * m) / 7; + outL = lsL; + outR = lsR; + if (q != 0) { + var si: u32 = q - 1; + if (si >= n) { si = n - 1; } // n = native sample count (2016/4032) + if (stereo) { + outL = leftBuf[si]; + outR = rightBuf[si]; + } else { + outL = monoBuf[si]; + outR = monoBuf[si]; + } } - if (l < -32768) { l = -32768; } - if (l > 32767) { l = 32767; } - if (r < -32768) { r = -32768; } - if (r > 32767) { r = 32767; } - spuPushCdSample(spu, (l as u32) & 0xFFFF, (r as u32) & 0xFFFF); - k = k + 1; + if (outL < -32768) { outL = -32768; } + if (outL > 32767) { outL = 32767; } + if (outR < -32768) { outR = -32768; } + if (outR > 32767) { outR = 32767; } + spuPushCdSample(spu, (outL as u32) & 0xFFFF, (outR as u32) & 0xFFFF); + j = j + 1; } + // Carry this sector's last output sample as the next sector's ls seed. + self.xaPrevL = outL; + self.xaPrevR = outR; } // Read one sector at `lba` into the dataBuf. Returns 1 on success. diff --git a/cpu.jam b/cpu.jam index 5ea1c7f..293e928 100644 --- a/cpu.jam +++ b/cpu.jam @@ -1208,6 +1208,12 @@ pub fn step(c: mut Cpu, bus: Bus, regs: *mut[] u32, cop0: *mut[] u32) { // (cpu.c psx_cpu_cycle: total_cycles += last_cycles, then return). var irqCyc: u64 = fetchCyc; if ((irqOpc & 0xFE000000) == 0x4A000000) { + // psxe runs DO_PENDING_LOAD as the first statement of this + // GTE-wins branch (cpu.c:369-371), and duckstation flushes the + // load on every exception. Commit the pending delayed load so the + // handler observes the loaded value (without this it commits one + // instruction late, at the handler's first instruction). + commitLoad(c, regs); gteExec(bus.gte.ptr, irqOpc); irqCyc = irqCyc + (gteOpCycles(irqOpc) as u64); } diff --git a/gte.jam b/gte.jam index 10ddc85..166a361 100644 --- a/gte.jam +++ b/gte.jam @@ -135,9 +135,13 @@ pub fn gteDataRead(g: *mut[] u32, idx: u32) u32 { } if (i == 28 || i == 29) { // Re-pack IR1/IR2/IR3 (>> 7, clamped 0..0x1F) into 15-bit RGB. - var r: i64 = (g[9] as i32) as i64 >> 7; - var grn: i64 = (g[10] as i32) as i64 >> 7; - var b: i64 = (g[11] as i32) as i64 >> 7; + // IR is stored zero-extended in the low 16 bits, so sign-extend + // first (s16ToI64) — otherwise a negative IR reads as a large + // positive value and clamps to 0x1F instead of 0 (psxe cpu.c:1290, + // duckstation gte.cpp:343 both give 0 for IR<0). + var r: i64 = s16ToI64(g[9]) >> 7; + var grn: i64 = s16ToI64(g[10]) >> 7; + var b: i64 = s16ToI64(g[11]) >> 7; if (r < 0) { r = 0; } if (r > 0x1F) { r = 0x1F; } if (grn < 0) { grn = 0; } @@ -249,7 +253,7 @@ pub fn gteExec(g: *mut[] u32, opc: u32) { // so RTPS/RTPT/MVMVA all see the same flags. const sf: u32 = ((opc >> 19) & 1) * 12; const lm: u32 = (opc >> 10) & 1; - if (op == 0x01) { gteRtps(g, 0, sf, lm); } // RTPS + if (op == 0x01) { gteRtps(g, 0, sf, lm, 1); } // RTPS if (op == 0x06) { gteNclip(g); } // NCLIP if (op == 0x0C) { gteOp(g, sf, lm); } // OP if (op == 0x12) { gteMvmva(g, opc); } // MVMVA @@ -263,9 +267,9 @@ pub fn gteExec(g: *mut[] u32, opc: u32) { if (op == 0x2D) { gteAvsz(g, 3); } // AVSZ3 if (op == 0x2E) { gteAvsz(g, 4); } // AVSZ4 if (op == 0x30) { // RTPT — 3 successive RTPS - gteRtps(g, 0, sf, lm); - gteRtps(g, 1, sf, lm); - gteRtps(g, 2, sf, lm); + gteRtps(g, 0, sf, lm, 0); + gteRtps(g, 1, sf, lm, 0); + gteRtps(g, 2, sf, lm, 1); // DQ tail only on last vertex } if (op == 0x10) { gteDpcs(g, sf, lm); } // DPCS if (op == 0x11) { gteIntpl(g, sf, lm); } // INTPL @@ -860,7 +864,7 @@ pub fn gteDivide(g: *mut[] u32, h: u32, sz3: u32) u32 { // FIFO. Faithful port of psxe's GTE_RTP_DQ macro including the nested // 44-bit MAC truncation, the clamp_ir_z IR3 path, and the full FLAG // bookkeeping (cpu.c:1728-1748). -pub fn gteRtps(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { +pub fn gteRtps(g: *mut[] u32, vidx: u32, sf: u32, lm: u32, last: u32) { const vSlot: u32 = vidx * 2; // V0 at 0, V1 at 2, V2 at 4 const vxy: u32 = g[vSlot]; const vz: u32 = g[vSlot + 1]; @@ -951,16 +955,21 @@ pub fn gteRtps(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { g[D_MAC2] = (sm2 as u64 & 0xFFFFFFFF) as u32; g[D_MAC3] = (sm3 as u64 & 0xFFFFFFFF) as u32; - // Depth-queue tail (psxe's GTE_RTP_DQ). Strictly only run on the - // last vertex of RTPS/RTPT, but doing it every vertex is harmless - // since the BIOS reads IR0/MAC0 after the final RTP call. - const dqa: i64 = s32ToI64(g[32 + 27]); - const dqb: i64 = s32ToI64(g[32 + 28]); - const mac0: i64 = gteClampMac0(g, dqb + dqa * (div as i64)); - g[D_MAC0] = (mac0 as u64 & 0xFFFFFFFF) as u32; - // IR0 clamps to 0..0x1000 with FLAG bit 12 (gte_clamp_ir0). - const ir0: i64 = gteClampIr0(g, mac0 >> 12); - g[8] = (ir0 as u64 & 0xFFFF) as u32; + // Depth-queue tail (psxe's GTE_RTP_DQ). Only the LAST vertex runs it: + // psxe's RTPT does GTE_RTP(0); GTE_RTP(1); GTE_RTP_DQ(2) (cpu.c:2247-2251) + // and duckstation gates the DQ block on `last` (gte.cpp:885). Running it on + // vertices 0/1 would set spurious FLAG bits 12/15/16 (15/16 feed bit 31), + // so RTPT could report a false error. DQA is s16 (cpu.c:138 / + // gte_types.h:117) — sign-extend the low half, not the full 32 bits. + if (last != 0) { + const dqa: i64 = s16ToI64(g[32 + 27]); + const dqb: i64 = s32ToI64(g[32 + 28]); + const mac0: i64 = gteClampMac0(g, dqb + dqa * (div as i64)); + g[D_MAC0] = (mac0 as u64 & 0xFFFFFFFF) as u32; + // IR0 clamps to 0..0x1000 with FLAG bit 12 (gte_clamp_ir0). + const ir0: i64 = gteClampIr0(g, mac0 >> 12); + g[8] = (ir0 as u64 & 0xFFFF) as u32; + } } // AVSZ3 / AVSZ4 — average Z of the last 3 or 4 SZ FIFO entries, @@ -1109,7 +1118,6 @@ pub fn gteNcds(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { const rc: i64 = (rgbc & 0xFF) as i64; const gc: i64 = ((rgbc >> 8) & 0xFF) as i64; const bc: i64 = ((rgbc >> 16) & 0xFF) as i64; - const code: u32 = (rgbc >> 24) & 0xFF; // Stage 4: far-colour interpolation ir' = clamp((FC<<12) - (C<<4)*IR). // psxe shifts this stage by `sf` too via the same gte_clamp_mac. @@ -1136,13 +1144,10 @@ pub fn gteNcds(g: *mut[] u32, vidx: u32, sf: u32, lm: u32) { g[D_MAC3] = (m3c as u64 & 0xFFFFFFFF) as u32; gteIrTriple(g, m1c, m2c, m3c, lm); - // Stage 6: RGB FIFO push and RGB2 = clamp_rgb(MAC >> 4). - g[20] = g[21]; // RGB0 ← RGB1 - g[21] = g[22]; // RGB1 ← RGB2 - const r2: u32 = clampU8(m1c >> 4); - const g2: u32 = clampU8(m2c >> 4); - const b2: u32 = clampU8(m3c >> 4); - g[22] = r2 | (g2 << 8) | (b2 << 16) | (code << 24); + // Stage 6: RGB FIFO push. Use gtePushRgb (FLAG-aware gte_clamp_rgb) so the + // colour-saturation FLAG bits 19/20/21 are set like psxe (cpu.c:1917-1919) + // and duckstation (gte.cpp:209-224); the old inline clampU8 skipped them. + gtePushRgb(g, m1c, m2c, m3c); } // 8-bit unsigned clamp used by the RGB FIFO writes. diff --git a/mdec.jam b/mdec.jam index 1d28442..ef77ade 100644 --- a/mdec.jam +++ b/mdec.jam @@ -450,6 +450,11 @@ pub const Mdec = struct { self.cmd = val; self.outputReq = 0; self.outputEmpty = 1; + // Clear any stale output count from a prior decode so status + // bit 31 (out-FIFO-empty) and bit 27 (out-request) read correctly + // after a SET_QT/SET_ST that doesn't write output (duckstation + // clears the out FIFO on every command parse, mdec.cpp:433). + self.outputWords = 0; self.outputBit15 = (val >> 25) & 1; self.outputSigned = (val >> 26) & 1; self.outputDepth = (val >> 27) & 3; @@ -494,6 +499,12 @@ pub const Mdec = struct { self.outputReq = 0; self.inputFull = 0; self.outputEmpty = 1; + // Status bit 31 (out-FIFO-empty) is computed from outputWords, + // so it must be cleared here too or a reset issued with + // undrained output would report not-empty (duckstation SoftReset + // clears the out FIFO, mdec.cpp:330-344). + self.outputWords = 0; + self.outputIndex = 0; self.curBlock = 4; } return; diff --git a/xa.jam b/xa.jam index 906b0b9..612ed96 100644 --- a/xa.jam +++ b/xa.jam @@ -103,7 +103,12 @@ pub fn xaDecodeBlock(secBuf: *mut[] u8, idx: u32, blk: u32, nib: u32, dst: *mut[] i32, hist: *mut[] i32) { const hdr: u32 = secBuf[idx + 4 + blk * 2 + nib] as u32; - const shift: i32 = 12 - ((hdr & 0x0F) as i32); + // Reserved ADPCM shift values 13..15 act as 9 on hardware (duckstation + // cdrom.cpp:285-289). psxe's naive `12 - (hdr&0xF)` leaves shift negative + // and the `old << shift` below is then an undefined negative shift; clamp. + var shiftVal: i32 = (hdr & 0x0F) as i32; + if (shiftVal > 12) { shiftVal = 9; } + const shift: i32 = 12 - shiftVal; const filter: u32 = (hdr >> 4) & 3; // psxe masks to 0x30 then >>4 const f0: i32 = xaPosCoef(filter); const f1: i32 = xaNegCoef(filter); -- 2.51.2