diff --git a/main.jam b/main.jam index 055f2cb..ebf0b8d 100644 --- a/main.jam +++ b/main.jam @@ -622,6 +622,14 @@ fn main() { var lastPoly: u32 = 0; const IDLE_TRIGGER: u32 = 180; // ~3s at 60 fps const MIN_POLY_DRAWN: u32 = 200; // boot animation + shell minimum + // Frame-rate limiter: pace to the PSX's native ~59.94 Hz, independent of + // the host display refresh. The emulated frame is already ~565K cycles + // (33.8688 MHz / 59.94); without wall-clock pacing the VSync-only present + // runs the whole machine at the monitor's rate (2x on a 120 Hz ProMotion + // panel) and overruns/drops the audio queue. The f64 accumulator avoids + // the drift an integer-ms target would introduce. + const FRAME_MS: f64 = 1000.0 / 59.94; + var nextFrameMs: f64 = SDL_GetTicks() as f64; while (!quit) { // Game-EXE side-load. Two trigger paths: // 1. BIOS jumps to 0x80030000 (real-disc boot handoff). @@ -682,6 +690,20 @@ fn main() { // write); doing it once per frame is enough since controller // polling runs at VBlank cadence in the BIOS. padSetButtons(bus.pad.ptr, buttons[0]); + + // Pace this frame to ~59.94 Hz. The VSync present above already + // blocked to a host vblank; pad the rest of the frame so the loop + // runs at the PSX rate rather than the (possibly 120 Hz) panel rate. + nextFrameMs = nextFrameMs + FRAME_MS; + const nowMs: u32 = SDL_GetTicks(); + if ((nowMs as f64) < nextFrameMs) { + SDL_Delay((nextFrameMs - (nowMs as f64)) as u32); + } else { + // Fell behind (slow frame, or a 60 Hz host where VSync already + // paced us) — resync so we never burst-catch-up faster than + // real time. + nextFrameMs = nowMs as f64; + } } // Persist memcard slot 1 to disk before freeing the bus. psxe does diff --git a/mdec.jam b/mdec.jam index ad57711..2700192 100644 --- a/mdec.jam +++ b/mdec.jam @@ -386,11 +386,19 @@ pub const Mdec = struct { if (self.outputBit15 != 0) { st = st | (1 << 23); } if (self.outputSigned != 0) { st = st | (1 << 24); } st = st | ((self.outputDepth & 3) << 25); - if (self.outputReq != 0) { st = st | (1 << 27); } - if (self.inputReq != 0) { st = st | (1 << 28); } + // Request bits 27/28 and Data-Out FIFO Empty (31) are LEVELS + // recomputed here, NOT latched: bit 27 = DMA1 enabled AND output + // has data; bit 28 = DMA0 enabled AND more input wanted; bit 31 = + // output drained. Matches nocash psx-spx, DuckStation + // (`enable_dma_out && !out_fifo.empty()`) and Avocado. psxe latches + // outputReq at the final decode word, so a game that enables DMA1 + // AFTER the decode reads bit 27 = 0 forever and never DMAs the MDEC + // output back out -- the frame decodes but is never read (FMV skip). + if (self.enableDma1 != 0 && self.outputWords > 0) { st = st | (1 << 27); } + if (self.enableDma0 != 0 && self.wordsRem > 0) { st = st | (1 << 28); } if (self.busy != 0) { st = st | (1 << 29); } if (self.inputFull != 0) { st = st | (1 << 30); } - if (self.outputEmpty != 0) { st = st | (1 << 31); } + if (self.outputWords == 0) { st = st | (1 << 31); } return st; } return 0;