Something went wrong. Try again.
A Playstation One (PS1) emulator fully written in Jam
Something went wrong. Try again.
jamstation disc.jam
4.9 kB · 177 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178// SPDX-License-Identifier: MIT// Copyright (c) 2026 Raphael Amorim
// 1. Raw .bin (Mode2/2352): single-file image, one data track. The// historical layout for our test discs and most plain dumps.// 2. .cue + .bin(s): multi-track support via the CUE parser in// cue.jam. Required for games shipping CDDA tracks after the// data track (Brave Fencer Musashi, Wipeout, etc.).//// LBA convention follows the PSX BIOS:// LBA = 150 -> start of track 1 data (00:02:00 on the disc)// LBA = 150 + N -> Nth sector of the data track in the .bin file
const { Cue, createCue, cueReset, cueParse, cueRead, cueQuery, cueGetTrackCount, cueGetTrackLba, cueGetTrackNumber } = import("cue");
const { Box } = import("std/box");const { File, exists } = import("std/fs");const { print } = import("std/fmt");
const SECTOR_SIZE: u32 = 2352;const LEAD_IN_SECTORS: u32 = 150;
pub const Disc = struct { file: File, loaded: u8, isCue: u8, pad1: u8, pad2: u8, cuePtr: Box(Cue),};
pub fn createDisc() Box(Disc) { return Box(Disc).init(Disc { file: File { handle: 0 as *mut[] u8 }, loaded: 0, isCue: 0, pad1: 0, pad2: 0, cuePtr: createCue(), });}
pub fn discClose(d: *mut[] Disc) { if (d[0].isCue != 0) { cueReset(d[0].cuePtr.ptrMut()); d[0].isCue = 0; } else if (d[0].loaded != 0) { d[0].file.close(); } d[0].loaded = 0;}
pub fn pathEndsWithCue(p: []u8) bool { var n: u32 = 0; while (p.ptr[n] != 0) { n = n + 1; } if (n < 4) { return false; } const a: u32 = p.ptr[n - 4] as u32; const b: u32 = p.ptr[n - 3] as u32; const cC: u32 = p.ptr[n - 2] as u32; const e: u32 = p.ptr[n - 1] as u32; // ".cue" or ".CUE" if (a == 0x2E && (b == 0x63 || b == 0x43) && (cC == 0x75 || cC == 0x55) && (e == 0x65 || e == 0x45)) { return true; } return false;}
pub fn discOpen(d: *mut[] Disc, path: []u8) i32 { if (pathEndsWithCue(path)) { // TODO: Revisit // 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].isCue = 1; d[0].loaded = 1; const tracks: u32 = cueGetTrackCount(raw); print("Opened CUE: {path} ({tracks} tracks)\n"); return 1; } match (File.open(path)) { Some(f) { d[0].file = f; d[0].loaded = 1; print("Opened disc image: {path}\n"); return 1; } None { return 0; } } return 0;}
pub fn fillPregapSector(buf: *mut[] u8) { var i: u32 = 0; while (i < SECTOR_SIZE) { buf[i] = 0; i = i + 1; } i = 1; while (i < 11) { buf[i] = 0xFF; i = i + 1; }}
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.ptrMut() as *mut[] u8, lba, buf); } if (lba < LEAD_IN_SECTORS) { fillPregapSector(buf); return 1; } const off: i64 = ((lba - LEAD_IN_SECTORS) as i64) * (SECTOR_SIZE as i64); var f: File = d[0].file; f.seek(off, 0); const got: u64 = f.read(buf[0..SECTOR_SIZE]); if (got != SECTOR_SIZE as u64) { return 0; } return 1;}
pub fn discLoaded(d: *mut[] Disc) i32 { if (d[0].loaded == 0) { return 0; } return 1;}
pub fn discTrackCount(d: *mut[] Disc) u32 { if (d[0].loaded == 0) { return 0; } 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.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.ptrMut() as *mut[] u8, lba); } return 1;}
// Track state at `lba`: 0=DATA, 1=AUDIO, 2=PREGAP, 3=FARpub fn discQuery(d: *mut[] Disc, lba: u32) u32 { if (d[0].isCue != 0) { return cueQuery(d[0].cuePtr.ptrMut() as *mut[] u8, lba); } if (lba < LEAD_IN_SECTORS) { return 2; } return 0;}