From fbb05f4621b1122dd3f705ab65821a437865f294 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Fri, 5 Jun 2026 13:10:22 +0200 Subject: [PATCH] rewrite to match --- cdrom.jam | 123 ++++-------------------------------------------------- cpu.jam | 47 +++------------------ cue.jam | 68 ++++++++++-------------------- 3 files changed, 38 insertions(+), 200 deletions(-) diff --git a/cdrom.jam b/cdrom.jam index 43a586f..e747f88 100644 --- a/cdrom.jam +++ b/cdrom.jam @@ -233,9 +233,6 @@ pub const Cdrom = struct { self.respW = 0; } - // Status byte: SPINDLE always, plus - // READ while the sector pump is active, SHELLOPEN if no disc, - // IDERROR if mode bit 4 is set. pub fn getStat(self: Self) u32 { var s: u32 = CD_STAT_SPINDLE; if (self.readOngoing != 0) { s = s | CD_STAT_READ; } @@ -244,16 +241,10 @@ pub const Cdrom = struct { return s; } - // register I/O - - // Status register at offset 0 (always, regardless of bank). pub fn readStatus(self: Self) u32 { var r: u32 = (self.index as u32) & 0x3; // bit 2 (ADPBUSY): XA-ADPCM streaming active. Set when a ReadN/ReadS // runs in XA mode. - // (Brave Fencer's FMV polls this; jam hardcoding 0 made the game - // branch away from the decode path - the SECOND trace-diff divergence, - // instr ~368.37M.) if (self.xaPlaying != 0) { r = r | 0x04; } if (self.paramEmpty()) { r = r | 0x08; } if (!self.paramFull()) { r = r | 0x10; } @@ -308,14 +299,6 @@ pub const Cdrom = struct { var delay: u32 = CD_DELAY_FR; if ((val & 0xFF) == CDL_INIT) { delay = CD_DELAY_INIT_FR; } self.delay = delay; - // A command write does NOT raise BUSYSTS: `state` was just set - // to TX_RESP1 two lines above, so any "if reading, mark busy" - // check sees TX_RESP1, not READ, and never fires. We must not - // assert busy either: doing so on the mid-read GetlocL/Getstat - // polls that the BFM/Suikoden FMV loader spams makes the game - // see the drive busy when it should be idle, so it re-reads the - // resource sectors forever and never triggers the MDEC decode - // (FMV black-screen). } // Push parameter byte. 2 { self.pushParam(val); } @@ -344,16 +327,11 @@ pub const Cdrom = struct { self.write8(off + 3, (val >> 24) & 0xFF); } - // state-machine tick - - // Handle the command response - dispatch the queued command - // after the TX_RESP1 delay elapses. pub fn executeCommand(self: mut Self, disc: *mut[] Disc, ic: *mut[] u32, cop0: *mut[] u32) { const cmd: u32 = self.pendingCmd as u32; self.busy = 0; - // Response-1 prechecks, before dispatch: // (1) disc-required commands with no disc -> INT5(11h,80h), // (3) wrong parameter count -> INT5(03h,20h), unknown command -> // INT5(03h,40h). (Version checks are omitted - jam models a @@ -412,14 +390,9 @@ pub const Cdrom = struct { self.errorOut(CD_STAT_SPINDLE, CD_ERR_INVALID_SUBFUNC); return; } - // Single-INT3 status commands. if (cmd == CDL_GETSTAT || cmd == CDL_MUTE || cmd == CDL_DEMUTE || cmd == CDL_SETMODE || cmd == CDL_RESET || cmd == CDL_SETLOC) { if (cmd == CDL_SETMODE) { - // SetMode: a 1x<->2x speed change - // costs a big ~650ms drive resync, charged to the next read. - // FMV setup (SetMode double-speed -> ReadN) relies on it; - // without it the first STR sector arrives too early. const prevSpeed: u32 = self.mode as u32 & MODE_SPEED; self.mode = self.popParam() as u8; if (prevSpeed != (self.mode as u32 & MODE_SPEED)) { @@ -430,9 +403,6 @@ pub const Cdrom = struct { const m: u32 = self.popParam(); const s: u32 = self.popParam(); const f: u32 = self.popParam(); - // Validate the MSF before accepting: - // BCD-valid nibbles, seconds < 0x60, frame < 0x75. Invalid - // MSF -> INT5(stat, 0x10) instead of an INT3 success. const bcdOk: bool = ((m & 0x0F) <= 9) && ((m >> 4) <= 9) && ((s & 0x0F) <= 9) && ((s >> 4) <= 9) && ((f & 0x0F) <= 9) && ((f >> 4) <= 9); @@ -448,7 +418,6 @@ pub const Cdrom = struct { self.ifr = 3; self.pushResp(self.getStat()); if (cmd == CDL_SETMODE) { - // SetMode hard-resets the state machine to IDLE. self.state = CD_STATE_IDLE as u8; self.prevState = CD_STATE_IDLE as u8; self.readOngoing = 0; @@ -458,7 +427,6 @@ pub const Cdrom = struct { self.state = self.prevState; return; } - // CdlPause: two-phase. INT3+stat, then delayed INT2+stat. if (cmd == CDL_PAUSE) { self.ifr = 3; self.pushResp(self.getStat()); @@ -478,10 +446,6 @@ pub const Cdrom = struct { self.ifr = 3; self.pushResp(self.getStat()); self.state = CD_STATE_TX_RESP2 as u8; - // Init's 2nd-response delay is - // CD_DELAY_1MS (33869), NOT CD_DELAY_INIT_FR (81102). The 47233-cyc - // gap made jam's Init INT2 fire late, slipping the CD IRQ at ~164.8M - // (the next trace divergence after the GPU-acc/-ffast-math one). self.delay = CD_DELAY_1MS; return; } @@ -489,11 +453,6 @@ pub const Cdrom = struct { self.ifr = 3; self.pushResp(self.getStat()); self.state = CD_STATE_TX_RESP2 as u8; - // seek precision is set in resp2 on success, not - // here in resp1. SeekL's 2nd-response delay is - // CD_DELAY_1MS (NOT FR) - the FR=50401 vs 1MS=33869 gap (16532 - // cyc) made jam's SeekL INT2 fire ~8266 instrs late, the root of - // the FMV CD-INT divergence at ~102.7M. self.delay = CD_DELAY_1MS; return; } @@ -515,8 +474,6 @@ pub const Cdrom = struct { self.state = CD_STATE_READ as u8; self.prevState = CD_STATE_READ as u8; self.readOngoing = 1; - // A read in XA-ADPCM mode marks the drive - // as XA-streaming (status bit2 ADPBUSY). The game polls this. if ((self.mode as u32 & MODE_XA_ADPCM) != 0) { self.xaPlaying = 1; } self.delay = self.readDelay(); return; @@ -551,9 +508,6 @@ pub const Cdrom = struct { } if (cmd == CDL_GETLOCP) { self.ifr = 3; - // GetLocP: track/index from - // the disc; subtract the 25-sector seek slop FIRST, then derive - // both the relative (within-track) and absolute MSF from it. const lbaRaw: u32 = self.lba; const track: u32 = discTrackNumber(disc, lbaRaw); const trackLba: u32 = discTrackLba(disc, track); @@ -583,7 +537,6 @@ pub const Cdrom = struct { return; } if (cmd == CDL_GETTN) { - // GetTN: stat, first-track = 1 (raw), last = BCD(track count). self.ifr = 3; self.pushResp(self.getStat()); self.pushResp(1); @@ -597,8 +550,6 @@ pub const Cdrom = struct { self.errorOut(CD_STAT_SPINDLE, CD_ERR_INVALID_SUBFUNC); return; } - // GetTD: look up the track's absolute LBA -> - // MM:SS; INT5 if past the last track (TS_FAR = 0xFFFFFFFF here). const track: u32 = (bcd & 0x0F) + ((bcd >> 4) & 0x0F) * 10; const f: u32 = discTrackLba(disc, track); if (f == 0xFFFFFFFF) { @@ -642,7 +593,7 @@ pub const Cdrom = struct { self.ifr = 3; self.pushResp(self.getStat()); self.state = CD_STATE_TX_RESP2 as u8; - self.delay = CD_DELAY_1MS; // SeekP: 1MS, not FR + self.delay = CD_DELAY_1MS; return; } @@ -696,8 +647,6 @@ pub const Cdrom = struct { return CD_DELAY_READ_SS; } - // Second-phase response for two-stage commands (Init / GetID / - // Seek / Pause). pub fn executeResp2(self: mut Self, disc: *mut[] Disc) { const cmd: u32 = self.pendingCmd as u32; if (cmd == CDL_INIT) { @@ -707,10 +656,6 @@ pub const Cdrom = struct { return; } if (cmd == CDL_SEEKL) { - // Query the seek TARGET (pendingLba) - // FIRST; TS_FAR -> INVALID_SUBFUNC, TS_AUDIO -> SEEK_FAILED, and - // the seek is NOT committed on error. On success commit and - // set seek precision = 1. const tsl: u32 = discQuery(disc, self.pendingLba); if (tsl == 3) { self.errorOut(CD_STAT_SPINDLE | CD_STAT_SEEKERROR, @@ -734,9 +679,6 @@ pub const Cdrom = struct { return; } if (cmd == CDL_SEEKP) { - // SeekP: INT2(stat), commit - // the seek, seek precision = 0. No seek-error check (SeekP is - // the audio-seek command and may land on CDDA tracks). self.ifr = 2; self.pushResp(self.getStat()); self.processSetloc(); @@ -749,8 +691,6 @@ pub const Cdrom = struct { return; } if (cmd == CDL_STOP) { - // Stop: rewind to LBA 150 and - // pause, then INT2(stat). self.pendingLba = 150; self.processSetloc(); self.readOngoing = 0; @@ -762,7 +702,6 @@ pub const Cdrom = struct { return; } if (cmd == CDL_MOTORON || cmd == CDL_SETSESSION || cmd == CDL_READTOC) { - // Each sends a second INT2(stat). self.ifr = 2; self.pushResp(self.getStat()); self.state = CD_STATE_IDLE as u8; @@ -826,7 +765,6 @@ pub const Cdrom = struct { self.busy = 0; } - // Per-scanline tick. Advances the response/read state machine. pub fn update(self: mut Self, disc: *mut[] Disc, spu: *mut[] u8, ic: *mut[] u32, cop0: *mut[] u32, cyc: u32) { var delay: u32 = self.delay; @@ -837,15 +775,6 @@ pub const Cdrom = struct { } if (self.state == CD_STATE_IDLE as u8) { return; } if (self.state == CD_STATE_PLAY as u8) { return; } - // Hold off until the game acks the previous INT. Re-assert the CPU - // IRQ each tick while it is pending AND enabled: if IER bit 0 was - // clear when the sector fired (so no IRQ reached the CPU) and the - // game enables it afterwards, this delivers the pending INT1 instead - // of wedging the drive forever with an unacked, never-delivered INT. - // All four reference emulators guarantee a queued INT1 is eventually - // delivered (DuckStation cdrom.cpp:1806 "there isn't much that can - // stop an INT1 once it's been queued"). Idempotent: irqRaise only ORs - // I_STAT, so re-raising an already-set bit each tick is harmless. if ((self.ifr as u32 & 0x1F) != 0) { if ((self.ifr as u32 & self.ier as u32 & 0x1F) != 0) { irqRaise(ic, cop0, IC_CDROM); @@ -855,11 +784,9 @@ pub const Cdrom = struct { } if (self.state == CD_STATE_TX_RESP1 as u8) { self.executeCommand(disc, ic, cop0); - // Switching to READ after a non-read - // command in TX_RESP1 incurs a delay. if (self.state == CD_STATE_READ as u8) { self.processSetloc(); - // First sector after a command uses the ongoing-read delay + // first sector after a command uses the ongoing-read delay // (= readDelay + 4ms). The speed-switch resync delay is // computed but then overwritten by this ongoing-read delay, // so it never applies; adding pendingSpeedSwitch (~650ms once) @@ -871,11 +798,7 @@ pub const Cdrom = struct { self.executeResp2(disc); if (self.state == CD_STATE_READ as u8) { self.processSetloc(); - // First sector after a command uses the ongoing-read delay - // (= readDelay + 4ms). The speed-switch resync delay is - // computed but then overwritten by this ongoing-read delay, - // so it never applies; adding pendingSpeedSwitch (~650ms once) - // delayed the first FMV sector. Drop it; just consume it. + // same self.delay = self.readDelay() + 4 * 33869; self.pendingSpeedSwitch = 0; } @@ -890,8 +813,6 @@ pub const Cdrom = struct { pub fn handleRead(self: mut Self, disc: *mut[] Disc, spu: *mut[] u8) { self.processSetloc(); if (self.hasDisc == 0) { - // Report no-disc as SHELLOPEN (0x10), giving INT5(11h,80h) - // - not SPINDLE (which would give 03h,80h). self.errorOut(CD_STAT_SHELLOPEN, CD_ERR_NO_DISC); return; } @@ -901,20 +822,11 @@ pub const Cdrom = struct { CD_ERR_INVALID_SUBFUNC); return; } - // XA-ADPCM audio sectors are decoded to the SPU, but we STILL deliver - // the sector via INT1 + the data FIFO. INT1 fires for EVERY data-track - // sector regardless of submode (XA is decoded on a separate parallel - // path), as on real hardware - // when no audio filter is set. Suppressing audio-sector INT1s here - // (the previous "filter" behaviour, ported from Avocado's filtered - // model) desynced games that do SOFTWARE de-interleaving - they expect - // one interrupt per sector to track stream position, so dropping the - // audio INT1s stalled FMV streaming. submode is at sector offset 0x12 - // (bit 2 = Audio, bit 6 = Real-time). const submode: u32 = self.dataBuf[18] as u32; const isXaAudio: bool = (submode & 0x04) != 0 && (submode & 0x40) != 0; if ((self.mode as u32 & MODE_XA_ADPCM) != 0 && isXaAudio) { - self.tryDecodeXa(spu); // SPU CD-audio; fall through to deliver INT1 + // SPU CD-audio; fall through to deliver INT1 + self.tryDecodeXa(spu); } var rIdx: u32 = 24; @@ -929,15 +841,15 @@ pub const Cdrom = struct { self.ifr = 1; self.pushResp(self.getStat()); - self.pendingLba = lba + 1; // advance deferred via pendingLba; lba promoted on the next processSetloc (so GetLocL/P returns the current sector, not the next) + self.pendingLba = lba + 1; self.delay = self.readDelay(); } pub fn tryDecodeXa(self: mut Self, spu: *mut[] u8) { + // TODO: Revist this // Copy the current sector into a scratch buffer because the XA // helpers want a *mut[] u8 / *const[] u8 they can stride - // through. Same shape as the old code; only the source is now - // `self.dataBuf` instead of `d[C_DATA_BUF + i]`. + // through. var scratch: [2352]u8 = [0; 2352]; var i: u32 = 0; while (i < 2352) { @@ -961,18 +873,6 @@ pub const Cdrom = struct { monoBuf.asMutPtr(), self.xaLh.asMutPtr(), self.xaRh.asMutPtr()); - // 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). The resampler is - // 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 (the `ls` seed, 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; } @@ -990,7 +890,7 @@ pub const Cdrom = struct { outR = lsR; if (q != 0) { var si: u32 = q - 1; - if (si >= n) { si = n - 1; } // n = native sample count (2016/4032) + if (si >= n) { si = n - 1; } if (stereo) { outL = leftBuf[si]; outR = rightBuf[si]; @@ -1011,12 +911,10 @@ pub const Cdrom = struct { self.xaPrevR = outR; } - // Read one sector at `lba` into the dataBuf. Returns 1 on success. pub fn discReadAt(self: Self, disc: *mut[] Disc, lba: u32) i32 { return discRead(disc, lba, self.dataBuf.asMutPtr()); } - // Read one byte from the data FIFO (the 0x1F801802 register). pub fn readData(self: mut Self) u32 { if (self.dataReq == 0) { return 0; } var rIdx: u32 = self.dataRidx; @@ -1028,8 +926,6 @@ pub const Cdrom = struct { } }; -// free helpers (no state required) - // Binary-to-BCD encoder. Inputs 0..99. pub fn bcdEnc(v: u32) u32 { const lo: u32 = v % 10; @@ -1043,7 +939,6 @@ pub fn bcdValid(b: u32) bool { } // CD-ROM controller version-ID table, 4 bytes per version. Index 1 = C0A. -// `version` is unused for v1 - would index a table for other versions. pub fn versionByteForIdx(version: u32, n: u32) u32 { if (n == 0) { return 0x94; } if (n == 1) { return 0x09; } diff --git a/cpu.jam b/cpu.jam index e6cb1b1..e818e67 100644 --- a/cpu.jam +++ b/cpu.jam @@ -1133,7 +1133,6 @@ pub fn cpuIrqPending(cop0: *mut[] u32) bool { return (sr & cause & 0x0000FF00) != 0; } -// One CPU cycle: latch PC, fetch, advance, dispatch. pub fn step(c: mut Cpu, bus: Bus, regs: *mut[] u32, cop0: *mut[] u32) { c.savedPc = c.pc; c.delaySlot = c.branch; @@ -1143,20 +1142,17 @@ pub fn step(c: mut Cpu, bus: Bus, regs: *mut[] u32, cop0: *mut[] u32) { if ((c.savedPc & 3) != 0) { cop0Write(cop0, C0_BADVADDR, c.savedPc); raiseException(c, cop0, CAUSE_ADEL); - // Stop here: the PC is misaligned, so we cannot fetch this cycle. + // stop! + // the PC is misaligned, so we cannot fetch this cycle. // raiseException has already redirected PC to the exception vector; - // the next cycle fetches from there. Mirrors the IBE branch below - // and every reference emulator (mednafen `goto OpDone`, DuckStation - // FlushPipeline). Without the return we'd fall through and fetch from - // the misaligned/vector address, double-faulting or double-executing. + // the next cycle fetches from there. return; } + // this was verified by ps1-tests cpu/code-in-io // Instruction fetch is allowed from cached RAM/BIOS and from a few // IO regions that happen to respond as if they're memory (SPU and - // DMA register banks). Everything else - scratchpad, MDEC, IRQ - // controller, GPU, CDROM, etc. - triggers Bus Error on Instruction - // (excode 6). Verified by ps1-tests cpu/code-in-io. + // DMA register banks). const pcPhys: u32 = c.savedPc & 0x1FFFFFFF; const inRam: bool = pcPhys < 0x00800000; const inBios: bool = pcPhys >= 0x1FC00000 && pcPhys < 0x1FC80000; @@ -1168,36 +1164,11 @@ pub fn step(c: mut Cpu, bus: Bus, regs: *mut[] u32, cop0: *mut[] u32) { return; } - // Take an external interrupt before issuing the next instruction - - // checked here after fetch and - // before dispatch. CAUSE_INT has code 0 (already in CAUSE bits 6..2 - // when raised), so we just trip the exception vector. - // - // `return` after the exception so the current cycle does NOT - // dispatch the vector's first instruction - that happens on the - // next cycle. Without the return, every IRQ - // handler would run 1 instruction ahead. - - // - // Fetch-access cost (bus access cycles): - // reading an opcode from the BIOS ROM costs 18 cycles (BIOS bus - // delay=18); RAM/scratchpad/IO cost 0. This is the dominant - // per-instruction timing difference from a flat model - BIOS code, - // where the CD/IO polling loops live, advances device clocks ~10× - // faster per instruction. var fetchCyc: u64 = 0; if (inBios) { fetchCyc = 18; } if (cpuIrqPending(cop0)) { - // Hardware quirk: a COP2 (GTE) math op "wins" over a pending IRQ - - // it executes before the interrupt is taken, and because EPC - // points back at it, it re-runs after the handler returns. - // Handle this by executing the op here when the - // instruction about to run (at c.pc == savedPc) is a GTE math - // op: primary 0x12 with the COP2 command bit set (0x4A......). const irqOpc: u32 = busRead32(bus, c.pc); - // Charge fetch + (GTE op cycles if the op won); execute() does - // not run on an IRQ-taken cycle, so there is no +2 base here. var irqCyc: u64 = fetchCyc; if ((irqOpc & 0xFE000000) == 0x4A000000) { // Commit the pending delayed load as the first statement of this @@ -1220,9 +1191,6 @@ pub fn step(c: mut Cpu, bus: Bus, regs: *mut[] u32, cop0: *mut[] u32) { c.pc = c.nextPc; c.nextPc = c.nextPc + 4; - // Total cost = fetch cycles + instruction cycles. Most - // instructions cost 2; the COP2 math path - // below adds the remainder of its GTE op cost on top of this base. c.cycles = c.cycles + fetchCyc + 2; dispatch(c, opc, bus, regs, cop0); @@ -1240,8 +1208,7 @@ pub fn run(c: mut Cpu, bus: Bus, regs: *mut[] u32, cop0: *mut[] u32, commitLoad(c, regs); } -// BIOS A/B-table hooks - +// TODO: Revisit // Emit one BIOS console byte. std/fmt.print's `{b}` interpolation on a // `[]u8` slice dispatches to a raw byte-write (same `@emitWriteBytes` // path the literal-string `print("...")` form uses, write(1, ...) under @@ -1286,8 +1253,6 @@ pub fn biosHook(c: mut Cpu, bus: Bus, regs: *mut[] u32) { } } -// MIPS instruction encoders (for hand-assembling tests) - pub fn encR(op: u32, rs: u32, rt: u32, rd: u32, sa: u32, fn_: u32) u32 { return ((op & 0x3F) << 26) | ((rs & 0x1F) << 21) diff --git a/cue.jam b/cue.jam index 800d778..d65bf8b 100644 --- a/cue.jam +++ b/cue.jam @@ -1,7 +1,5 @@ -// CUE-sheet parser. -// -// A .cue text file describes how one or more .bin files map to CD-ROM -// tracks. The minimum we need to parse is: +// .cue text file describes how one or more .bin files map to CD-ROM +// tracks. // // FILE "track.bin" BINARY // TRACK 01 MODE2/2352 @@ -12,18 +10,11 @@ // Each TRACK has a mode (MODE1/2048, MODE1/2352, MODE2/2336, MODE2/2352, // AUDIO, etc.) and one or two INDEX entries (00 = pregap start, 01 = // track data start, MSF format). -// -// We use fixed-size tables (max 16 files, 99 tracks) instead of -// linked lists - Jam's stdlib doesn't ship one and a CD can't have more -// than 99 tracks anyway. Each track stores absolute LBA start/end so the -// disc-read path can look up by LBA. -// All file I/O goes through std.fs - no raw libc file ABI in this module. const { File, exists, canonicalize } = import("std/fs"); const { Vec } = import("std/collections"); const { Box } = import("std/box"); -// Track-mode enum (subset we actually use). pub const TRACK_MODE_UNKNOWN: u32 = 0; pub const TRACK_MODE_AUDIO: u32 = 1; pub const TRACK_MODE_MODE1_2048: u32 = 2; @@ -35,54 +26,42 @@ pub const MAX_FILES: u32 = 16; pub const MAX_TRACKS: u32 = 99; pub const SECTOR: u32 = 2352; -// Per-file metadata. `file` is the libc FILE* (kept open while the CUE -// is live); `startLba` is the absolute LBA where this file's first -// sector lands. -// Internal types - not exported (other modules treat Cue as an opaque -// `*mut[] u8` blob and use the cueXxx functions to interact). pub const CueFile = struct { - file: File, // std.fs.File handle - sectors: u32, // file_size / SECTOR - startLba: u32, // LBA where this file's sector 0 lands - pathLo: u32, // path stored separately, see Cue.pathBuf + file: File, + sectors: u32, + // LBA where this file's sector 0 lands + startLba: u32, + pathLo: u32, pathHi: u32, }; pub const CueTrack = struct { number: u32, - mode: u32, // TRACK_MODE_* + mode: u32, pregap: u32, - startLba: u32, // absolute LBA, inclusive - endLba: u32, // absolute LBA, exclusive - index0: i32, // -1 if not specified + startLba: u32, + endLba: u32, + index0: i32, index1: i32, - fileIdx: u32, // which entry in files[] + fileIdx: u32, }; pub const Cue = struct { - // Vec instead of raw `*mut[] T` so each backing buffer auto-drops - // when the enclosing Box(Cue) is destroyed (Box.drop -> @dropInPlace - // -> field walk -> Vec.drop). Cue's own `cfn drop` is the iterate-and- - // close-files cleanup that Vec can't synthesize for a File handle. files: Vec(CueFile), tracks: Vec(CueTrack), fileCount: u32, trackCount: u32, - // Concatenated path strings, NUL-terminated; pathLo/pathHi index into here. pathBuf: Vec(u8), pathBufLen: u32, pathBufCap: u32, - // Parser scratch state (readByte loop). file: File, - cur: u32, // current character (sentinel-extended) - rootPath: Vec(u8), // directory of the .cue, used to resolve BIN paths + // current character + cur: u32, + // directory of the .cue + rootPath: Vec(u8), rootLen: u32, - // Close every open per-file handle before the Vec backings get - // freed by the auto-drop. The Vec drop itself doesn't know how to - // close a File - that's our responsibility. The user-side cleanup - // path is `cueClose(c)`; this `cfn drop` is the safety net for any - // path that bypasses cueClose (e.g. test teardown). + // TODO: File in std will apply it's own drop so we don't need this cfn drop(self: mut Self) { var i: u32 = 0; while (i < self.fileCount) { @@ -96,12 +75,12 @@ pub const Cue = struct { } }; -// helpers - pub fn isAlpha(c: u32) bool { - if (c >= 0x41 && c <= 0x5A) { return true; } - if (c >= 0x61 && c <= 0x7A) { return true; } - return false; + match (c) { + 0x41..=0x5A { return true; } // A-Z + 0x61..=0x7A { return true; } // a-z + _ { return false; } + } } pub fn isDigit(c: u32) bool { @@ -122,12 +101,11 @@ pub fn strEqAscii(a: *mut[] u8, b: *const[] u8, n: u32) bool { return true; } -// Parse "MM:SS:FF" into an LBA (75 frames per second, 60 sec per min). pub fn parseMsf(raw: *mut[] u8) u32 { var c: *mut[] Cue = raw as *mut[] Cue; if (!isDigit(c[0].cur)) { return 0; } const m: u32 = parseNumber(raw); - if (c[0].cur != 0x3A) { return 0; } // ':' + if (c[0].cur != 0x3A) { return 0; } c[0].cur = c[0].file.readByte() as u32 & 0xFF; const s: u32 = parseNumber(raw); if (c[0].cur != 0x3A) { return 0; } -- 2.51.2