diff --git a/cue.jam b/cue.jam index 1f591f2..6083a8a 100644 --- a/cue.jam +++ b/cue.jam @@ -18,17 +18,10 @@ // than 99 tracks anyway. Each track stores absolute LBA start/end so the // disc-read path can look up by LBA. -// Internal to cue.jam — the Cue struct is heap-allocated with a custom -// layout that doesn't fit the Box(T) / Vec(T) machinery cleanly (its -// inner Vec fields would still need explicit cleanup because they live -// inside heap bytes, not a stack-tracked container). Keeping the libc -// allocator extern-private here means no other jamstation module sees -// these symbols — defining-module resolution scopes them locally. -extern fn malloc(size: u64) *mut[] u8; -extern fn free(ptr: *mut[] u8); -extern fn memset(dst: *mut[] u8, c: i32, n: u64) *mut[] u8; // 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 (psxe cue.h subset we actually use). pub const TRACK_MODE_UNKNOWN: u32 = 0; @@ -67,19 +60,40 @@ pub const CueTrack = struct { }; pub const Cue = struct { - files: *mut[] CueFile, - tracks: *mut[] CueTrack, + // 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: *mut[] u8, + pathBuf: Vec(u8), pathBufLen: u32, pathBufCap: u32, // Parser scratch state (psxe-style readByte loop). file: File, cur: u32, // current character (sentinel-extended) - rootPath: *mut[] u8, // directory of the .cue, used to resolve BIN paths + rootPath: Vec(u8), // directory of the .cue, used to resolve BIN paths 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). + cfn drop(self: mut Self) { + var i: u32 = 0; + while (i < self.fileCount) { + if (self.files.ptr[i].sectors != 0) { + var f: File = self.files.ptr[i].file; + f.close(); + self.files.ptr[i].sectors = 0; + } + i = i + 1; + } + } }; // ---------- helpers ---------------------------------------------------- @@ -190,36 +204,56 @@ pub fn kwId(buf: *mut[] u8) u32 { // ---------- allocator ------------------------------------------------- -pub fn cueAlloc() *mut[] u8 { - var raw: *mut[] u8 = malloc(96); - memset(raw, 0, 96); - var c: *mut[] Cue = raw as *mut[] Cue; - c[0].files = (malloc((MAX_FILES as u64) * 48) as *mut[] CueFile); - c[0].tracks = (malloc((MAX_TRACKS as u64) * 36) as *mut[] CueTrack); - memset(c[0].files as *mut[] u8, 0, (MAX_FILES as u64) * 48); - memset(c[0].tracks as *mut[] u8, 0, (MAX_TRACKS as u64) * 36); - c[0].pathBufCap = 4096; - c[0].pathBuf = malloc(c[0].pathBufCap as u64); - memset(c[0].pathBuf, 0, c[0].pathBufCap as u64); - c[0].rootPath = malloc(512); - memset(c[0].rootPath, 0, 512); - return raw; -} - -pub fn cueFree(raw: *mut[] u8) { - var c: *mut[] Cue = raw as *mut[] Cue; +// Allocate a Box(Cue) with each inner Vec sized to its MAX_/cap. The +// Vecs are filled-with-zero so the existing index-based writes by +// cueParse (`c.files.ptr[fi].sectors = ...`) land on valid slots — +// matches the old memset-after-malloc semantics. When the owning Bus +// goes out of scope, Box(Disc).drop → @dropInPlace → Disc.cuePtr Box +// drop → @dropInPlace on Cue → Cue's cfn drop closes open files → +// Cue's field-walk drops each Vec → libc free. +// Close every open per-cue-file handle and zero the counters so the +// same Box(Cue) can be repopulated by a fresh cueParse. The Vec backings +// stay live — only the user-visible "we have N tracks/files loaded" +// state is reset. Called from discClose AND from discOpen's parse- +// failure path so partial state never leaks into a subsequent open. +pub fn cueReset(c: *mut[] Cue) { var i: u32 = 0; while (i < c[0].fileCount) { - if (c[0].files[i].sectors != 0) { - c[0].files[i].file.close(); + if (c[0].files.ptr[i].sectors != 0) { + var f: File = c[0].files.ptr[i].file; + f.close(); + c[0].files.ptr[i].sectors = 0; } i = i + 1; } - free(c[0].files as *mut[] u8); - free(c[0].tracks as *mut[] u8); - free(c[0].pathBuf); - free(c[0].rootPath); - free(raw); + c[0].fileCount = 0; + c[0].trackCount = 0; + c[0].pathBufLen = 0; + c[0].rootLen = 0; +} + +pub fn cueAlloc() Box(Cue) { + return Box(Cue).init(Cue { + files: Vec(CueFile).filled( + CueFile { file: File { handle: 0 as *mut[] u8 }, + sectors: 0, startLba: 0, + pathLo: 0, pathHi: 0 }, + MAX_FILES), + tracks: Vec(CueTrack).filled( + CueTrack { number: 0, mode: 0, pregap: 0, + startLba: 0, endLba: 0, + index0: -1, index1: -1, fileIdx: 0 }, + MAX_TRACKS), + fileCount: 0, + trackCount: 0, + pathBuf: Vec(u8).filled(0, 4096), + pathBufLen: 0, + pathBufCap: 4096, + file: File { handle: 0 as *mut[] u8 }, + cur: 0, + rootPath: Vec(u8).filled(0, 512), + rootLen: 0, + }); } // Append a NUL-terminated string into pathBuf, returning the start offset. @@ -229,10 +263,10 @@ pub fn pathBufAppend(raw: *mut[] u8, src: *mut[] u8, len: u32) u32 { if (off + len + 1 >= c[0].pathBufCap) { return 0; } var i: u32 = 0; while (i < len) { - c[0].pathBuf[off + i] = src[i]; + c[0].pathBuf.ptr[off + i] = src[i]; i = i + 1; } - c[0].pathBuf[off + len] = 0; + c[0].pathBuf.ptr[off + len] = 0; c[0].pathBufLen = off + len + 1; return off; } @@ -247,7 +281,7 @@ pub fn cueComputeRanges(raw: *mut[] u8) { var lba: u32 = 150; var fi: u32 = 0; while (fi < c[0].fileCount) { - c[0].files[fi].startLba = lba; + c[0].files.ptr[fi].startLba = lba; // Find tracks for this file and assign ranges. // Single-track file: track spans the whole file. // Multi-track file: each track ends where the next begins. @@ -255,7 +289,7 @@ pub fn cueComputeRanges(raw: *mut[] u8) { var lastTrack: u32 = 0; var ti: u32 = 0; while (ti < c[0].trackCount) { - if (c[0].tracks[ti].fileIdx == fi) { + if (c[0].tracks.ptr[ti].fileIdx == fi) { if (firstTrack == MAX_TRACKS) { firstTrack = ti; } lastTrack = ti; } @@ -266,32 +300,32 @@ pub fn cueComputeRanges(raw: *mut[] u8) { fi = fi + 1; continue; } - const fileSectors: u32 = c[0].files[fi].sectors; + const fileSectors: u32 = c[0].files.ptr[fi].sectors; if (firstTrack == lastTrack) { // Single track: pregap from INDEX 01 if present, then full file. var pregap: u32 = 0; - if (c[0].tracks[firstTrack].index0 != -1 && - c[0].tracks[firstTrack].index1 != -1) { - pregap = c[0].tracks[firstTrack].index1 as u32; + if (c[0].tracks.ptr[firstTrack].index0 != -1 && + c[0].tracks.ptr[firstTrack].index1 != -1) { + pregap = c[0].tracks.ptr[firstTrack].index1 as u32; } - c[0].tracks[firstTrack].pregap = pregap; - c[0].tracks[firstTrack].startLba = lba + pregap; - c[0].tracks[firstTrack].endLba = c[0].tracks[firstTrack].startLba + fileSectors; - lba = c[0].tracks[firstTrack].endLba; + c[0].tracks.ptr[firstTrack].pregap = pregap; + c[0].tracks.ptr[firstTrack].startLba = lba + pregap; + c[0].tracks.ptr[firstTrack].endLba = c[0].tracks.ptr[firstTrack].startLba + fileSectors; + lba = c[0].tracks.ptr[firstTrack].endLba; } else { // Multi-track: each track's start is index1 + 150; ends at next. ti = firstTrack; while (ti <= lastTrack) { - c[0].tracks[ti].startLba = (c[0].tracks[ti].index1 as u32) + 150; + c[0].tracks.ptr[ti].startLba = (c[0].tracks.ptr[ti].index1 as u32) + 150; if (ti == lastTrack) { - c[0].tracks[ti].endLba = lba + fileSectors; + c[0].tracks.ptr[ti].endLba = lba + fileSectors; } else { - c[0].tracks[ti].endLba = - ((c[0].tracks[ti + 1].index1 as u32) + 150) - 1; + c[0].tracks.ptr[ti].endLba = + ((c[0].tracks.ptr[ti + 1].index1 as u32) + 150) - 1; } ti = ti + 1; } - lba = c[0].tracks[lastTrack].endLba; + lba = c[0].tracks.ptr[lastTrack].endLba; } fi = fi + 1; } @@ -307,22 +341,22 @@ pub fn cueGetTrackCount(raw: *mut[] u8) u32 { pub fn cueGetTrackLba(raw: *mut[] u8, track: u32) u32 { var c: *mut[] Cue = raw as *mut[] Cue; if (track == 0) { - return c[0].tracks[c[0].trackCount - 1].endLba; + return c[0].tracks.ptr[c[0].trackCount - 1].endLba; } if (track > c[0].trackCount) { return 0xFFFFFFFF; } - return c[0].tracks[track - 1].startLba; + return c[0].tracks.ptr[track - 1].startLba; } pub fn cueGetTrackNumber(raw: *mut[] u8, lba: u32) u32 { var c: *mut[] Cue = raw as *mut[] Cue; var i: u32 = 0; while (i < c[0].trackCount) { - if (lba >= c[0].tracks[i].startLba && lba < c[0].tracks[i].endLba) { - return c[0].tracks[i].number; + if (lba >= c[0].tracks.ptr[i].startLba && lba < c[0].tracks.ptr[i].endLba) { + return c[0].tracks.ptr[i].number; } i = i + 1; } - if (c[0].trackCount > 0) { return c[0].tracks[c[0].trackCount - 1].number; } + if (c[0].trackCount > 0) { return c[0].tracks.ptr[c[0].trackCount - 1].number; } return 1; } @@ -332,7 +366,7 @@ pub fn cueFindTrack(raw: *mut[] u8, lba: u32) u32 { var c: *mut[] Cue = raw as *mut[] Cue; var i: u32 = 0; while (i < c[0].trackCount) { - if (lba >= c[0].tracks[i].startLba && lba < c[0].tracks[i].endLba) { + if (lba >= c[0].tracks.ptr[i].startLba && lba < c[0].tracks.ptr[i].endLba) { return i; } i = i + 1; @@ -358,20 +392,20 @@ pub fn cueRead(raw: *mut[] u8, lba: u32, buf: *mut[] u8) i32 { const ti: u32 = cueFindTrack(raw, lba); if (ti == c[0].trackCount) { // Past last track — could be pregap or far past end. - if (c[0].trackCount > 0 && lba >= c[0].tracks[c[0].trackCount - 1].endLba) { + if (c[0].trackCount > 0 && lba >= c[0].tracks.ptr[c[0].trackCount - 1].endLba) { return 0; } // Pregap: psxe cue.c:535-536 fills zero + sync at bytes [1..10]. fillPregap(buf); return 1; } - const fi: u32 = c[0].tracks[ti].fileIdx; - const off: i64 = ((lba - c[0].files[fi].startLba) as i64) * (SECTOR as i64); + const fi: u32 = c[0].tracks.ptr[ti].fileIdx; + const off: i64 = ((lba - c[0].files.ptr[fi].startLba) as i64) * (SECTOR as i64); if (off < 0) { fillPregap(buf); return 1; } - var tf: File = c[0].files[fi].file; + var tf: File = c[0].files.ptr[fi].file; tf.seek(off, 0); const got: u64 = tf.read(buf[0..SECTOR]); if (got != SECTOR as u64) { return 0; } @@ -383,12 +417,12 @@ pub fn cueRead(raw: *mut[] u8, lba: u32, buf: *mut[] u8) i32 { pub fn cueQuery(raw: *mut[] u8, lba: u32) u32 { var c: *mut[] Cue = raw as *mut[] Cue; if (c[0].trackCount > 0 && - lba >= c[0].tracks[c[0].trackCount - 1].endLba) { + lba >= c[0].tracks.ptr[c[0].trackCount - 1].endLba) { return 3; } const ti: u32 = cueFindTrack(raw, lba); if (ti == c[0].trackCount) { return 2; } - if (c[0].tracks[ti].mode == TRACK_MODE_AUDIO) { return 1; } + if (c[0].tracks.ptr[ti].mode == TRACK_MODE_AUDIO) { return 1; } return 0; } @@ -433,10 +467,10 @@ pub fn cueParse(raw: *mut[] u8, path: []u8) i32 { } var ri: u32 = 0; while (ri < lastSlash) { - c[0].rootPath[ri] = resolved[ri]; + c[0].rootPath.ptr[ri] = resolved[ri]; ri = ri + 1; } - c[0].rootPath[lastSlash] = 0; + c[0].rootPath.ptr[lastSlash] = 0; c[0].rootLen = lastSlash; skipWs(raw); @@ -482,7 +516,7 @@ pub fn cueParseFile(raw: *mut[] u8) { var ni: u32 = 0; // Prefix with the .cue's directory. while (ni < c[0].rootLen) { - nameBuf[ni] = c[0].rootPath[ni]; + nameBuf[ni] = c[0].rootPath.ptr[ni]; ni = ni + 1; } while (c[0].cur != 0x22 && !c[0].file.eof() && ni < 510) { @@ -506,8 +540,8 @@ pub fn cueParseFile(raw: *mut[] u8) { Some(f) { const sz: u64 = f.size(); const fi: u32 = c[0].fileCount; - c[0].files[fi].file = f; - c[0].files[fi].sectors = (sz as u32) / SECTOR; + c[0].files.ptr[fi].file = f; + c[0].files.ptr[fi].sectors = (sz as u32) / SECTOR; c[0].fileCount = fi + 1; } None { return; } @@ -533,12 +567,12 @@ pub fn cueParseTrack(raw: *mut[] u8) { else if (kw == 12) { mode = TRACK_MODE_AUDIO; } if (c[0].trackCount >= MAX_TRACKS) { return; } const ti: u32 = c[0].trackCount; - c[0].tracks[ti].number = num; - c[0].tracks[ti].mode = mode; - c[0].tracks[ti].pregap = 0; - c[0].tracks[ti].index0 = -1; - c[0].tracks[ti].index1 = -1; - c[0].tracks[ti].fileIdx = c[0].fileCount - 1; + c[0].tracks.ptr[ti].number = num; + c[0].tracks.ptr[ti].mode = mode; + c[0].tracks.ptr[ti].pregap = 0; + c[0].tracks.ptr[ti].index0 = -1; + c[0].tracks.ptr[ti].index1 = -1; + c[0].tracks.ptr[ti].fileIdx = c[0].fileCount - 1; c[0].trackCount = ti + 1; } @@ -552,6 +586,6 @@ pub fn cueParseIndex(raw: *mut[] u8) { skipWs(raw); const msf: u32 = parseMsf(raw); const ti: u32 = c[0].trackCount - 1; - if (i == 0) { c[0].tracks[ti].index0 = msf as i32; } - if (i == 1) { c[0].tracks[ti].index1 = msf as i32; } + if (i == 0) { c[0].tracks.ptr[ti].index0 = msf as i32; } + if (i == 1) { c[0].tracks.ptr[ti].index1 = msf as i32; } } diff --git a/disc.jam b/disc.jam index 576247f..6c1edcd 100644 --- a/disc.jam +++ b/disc.jam @@ -18,7 +18,7 @@ // real discs prefix the data with 2 seconds (150 sectors) of pregap // that's not part of the image file. -const { cueAlloc, cueFree, cueParse, cueRead, cueQuery, +const { Cue, cueAlloc, cueReset, cueParse, cueRead, cueQuery, cueGetTrackCount, cueGetTrackLba, cueGetTrackNumber } = import("cue"); const { Box } = import("std/box"); @@ -35,7 +35,13 @@ pub const Disc = struct { isCue: u8, // 1 if cue-loaded (cuePtr is valid) pad1: u8, pad2: u8, - cuePtr: *mut[] u8, // opaque *mut[] Cue stored as bytes for struct-mirror compat + // Always-allocated Box(Cue). For non-cue discs it sits empty + // (fileCount=0). For cue discs, cueParse writes into it in place. + // Auto-drop chain: Box(Disc).drop → @dropInPlace → field walk → + // Box(Cue).drop → @dropInPlace on Cue → Cue.cfn drop closes files + // → field walk drops each Vec backing. Holding the Box here (vs a + // raw `*mut[] u8`) is what makes the cleanup chain self-sufficient. + cuePtr: Box(Cue), }; // Owning heap-allocated Disc. Bus stores it as a `Box(Disc)` and the @@ -50,17 +56,19 @@ pub fn discAlloc() Box(Disc) { isCue: 0, pad1: 0, pad2: 0, - cuePtr: 0 as *mut[] u8, + cuePtr: cueAlloc(), }); } -// File-handle / cue-buffer cleanup that the auto-drop can't do on its own. -// Box's drop frees the Disc's backing memory; this hook closes any open -// resources the Disc was holding. +// Reset open-file resources before either reopening this Disc with a new +// image or letting the owning Bus go out of scope. For a cue disc we +// invoke cueReset which closes every per-cue-track file handle and +// zeros the file/track counters — the Cue's Vec backings stay live +// (Box(Cue).drop frees them on scope exit). For a raw .bin we close +// the single std.fs.File handle the Disc holds directly. pub fn discClose(d: *mut[] Disc) { if (d[0].isCue != 0) { - cueFree(d[0].cuePtr); - d[0].cuePtr = 0 as *mut[] u8; + cueReset(d[0].cuePtr.ptrMut()); d[0].isCue = 0; } else if (d[0].loaded != 0) { d[0].file.close(); @@ -91,15 +99,19 @@ pub fn pathEndsWithCue(p: []u8) bool { // single-track Mode2/2352 .bin. pub fn discOpen(d: *mut[] Disc, path: []u8) i32 { if (pathEndsWithCue(path)) { - var c: *mut[] u8 = cueAlloc(); - if (cueParse(c, path) != 0) { - cueFree(c); + // Reuse the always-allocated Box(Cue) at d[0].cuePtr — cueParse + // populates it in place, so we don't have to deal with assigning + // a new Box over the old one (jam doesn't auto-drop on field + // overwrite). On parse failure, cueReset clears whatever + // partial state landed before the failure. + var raw: *mut[] u8 = d[0].cuePtr.ptrMut() as *mut[] u8; + if (cueParse(raw, path) != 0) { + cueReset(d[0].cuePtr.ptrMut()); return 0; } - d[0].cuePtr = c; d[0].isCue = 1; d[0].loaded = 1; - const tracks: u32 = cueGetTrackCount(c); + const tracks: u32 = cueGetTrackCount(raw); print("Opened CUE: {path} ({tracks} tracks)\n"); return 1; } @@ -131,7 +143,7 @@ pub fn fillPregapSector(buf: *mut[] u8) { pub fn discRead(d: *mut[] Disc, lba: u32, buf: *mut[] u8) i32 { if (d[0].loaded == 0) { return 0; } if (d[0].isCue != 0) { - return cueRead(d[0].cuePtr, lba, buf); + return cueRead(d[0].cuePtr.ptrMut() as *mut[] u8, lba, buf); } if (lba < LEAD_IN_SECTORS) { fillPregapSector(buf); @@ -156,26 +168,26 @@ pub fn discLoaded(d: *mut[] Disc) i32 { pub fn discTrackCount(d: *mut[] Disc) u32 { if (d[0].loaded == 0) { return 0; } - if (d[0].isCue != 0) { return cueGetTrackCount(d[0].cuePtr); } + if (d[0].isCue != 0) { return cueGetTrackCount(d[0].cuePtr.ptrMut() as *mut[] u8); } return 1; } // Absolute start LBA of `track` (track 0 = lead-out / total). Returns // 0xFFFFFFFF for a track past the end (cue's TS_FAR sentinel). pub fn discTrackLba(d: *mut[] Disc, track: u32) u32 { - if (d[0].isCue != 0) { return cueGetTrackLba(d[0].cuePtr, track); } + if (d[0].isCue != 0) { return cueGetTrackLba(d[0].cuePtr.ptrMut() as *mut[] u8, track); } if (track > 1) { return 0xFFFFFFFF; } return LEAD_IN_SECTORS; } pub fn discTrackNumber(d: *mut[] Disc, lba: u32) u32 { - if (d[0].isCue != 0) { return cueGetTrackNumber(d[0].cuePtr, lba); } + if (d[0].isCue != 0) { return cueGetTrackNumber(d[0].cuePtr.ptrMut() as *mut[] u8, lba); } return 1; } // Track state at `lba`: 0=DATA, 1=AUDIO, 2=PREGAP, 3=FAR (matches cueQuery). pub fn discQuery(d: *mut[] Disc, lba: u32) u32 { - if (d[0].isCue != 0) { return cueQuery(d[0].cuePtr, lba); } + if (d[0].isCue != 0) { return cueQuery(d[0].cuePtr.ptrMut() as *mut[] u8, lba); } if (lba < LEAD_IN_SECTORS) { return 2; } // pregap return 0; // data } diff --git a/main.jam b/main.jam index f77332c..3b7c8c6 100644 --- a/main.jam +++ b/main.jam @@ -23,7 +23,7 @@ const { Cdrom } = import("cdrom"); const { Disc, discAlloc, discOpen } = import("disc"); const { dmaAlloc, dmaTickSpu, dmaUpdate } = import("dma"); const { padAlloc, padUpdate, padSetButtons } = import("pad"); -const { spuAlloc, spuRamAlloc, psone_audio_cb, setU64, spuStepAdsrCpu, +const { spuAlloc, spuRamAlloc, setU64, spuStepAdsrCpu, spuGetSample } = import("spu"); const { Sio1 } = import("sio1"); const { Mdec } = import("mdec"); @@ -692,8 +692,9 @@ fn main() { } } runOneFrame(c, bus, regs.ptr, cop0.ptr, audioDev); - // Audio: nothing to do on the main thread anymore. SDL's audio - // thread is pulling samples from psone_audio_cb at 44.1 kHz. + // Audio: runOneFrame fed the per-frame sample batch to SDL via + // SDL_QueueAudio. SDL drains the queue at the host's 44.1 kHz + // pull rate; no callback path active. var vram: *mut[] u8 = busVramPtr(bus); // Read the current display origin out of the GPU state buffer // (g[63] disp_x, g[64] disp_y — set by GP1 0x05). The BIOS