From 14fd50644fbd259e264aaacf15b0ca3c9b62816c Mon Sep 17 00:00:00 2001 From: AI Agent Bot Date: Sun, 15 Feb 2026 06:44:49 -0600 Subject: [PATCH 1/9] fix: PSP EBOOT boot crash from kernel-mode partition allocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default feature `kernel-mode` enabled `psp/kernel`, causing the rust-psp global allocator to use partition 1 (kernel memory) for all heap allocations. Since the EBOOT runs in user mode, the kernel partition is inaccessible — every allocation returned ILLEGAL_PARTITION, triggering an OOM panic on the first heap allocation (font atlas). Change default features to empty so the allocator uses partition 2 (user memory). Kernel features remain available via `--features kernel-mode` for real PSP hardware with CFW kernel access. Co-Authored-By: Claude Opus 4.6 --- crates/oasis-backend-psp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/oasis-backend-psp/Cargo.toml b/crates/oasis-backend-psp/Cargo.toml index 09d4f77..7f35af1 100644 --- a/crates/oasis-backend-psp/Cargo.toml +++ b/crates/oasis-backend-psp/Cargo.toml @@ -14,7 +14,7 @@ repository = "https://github.com/AndrewAltimit/oasis-os" authors = ["AndrewAltimit"] [features] -default = ["kernel-mode"] +default = [] kernel-mode = ["psp/kernel", "kernel-volatile", "kernel-me-clock", "kernel-me"] # Granular kernel sub-features: kernel-exception = ["psp/kernel"] # sceKernelRegisterDefaultExceptionHandler (broken on PSP-3000 + 6.20 PRO-C) From 80ce83516994d38c71abff0197af25fc4a9c10f6 Mon Sep 17 00:00:00 2001 From: AI Agent Bot Date: Sun, 15 Feb 2026 06:52:38 -0600 Subject: [PATCH 2/9] fix: reduce PRX codec scan frequency to avoid in-game stutter The codec NID scan + stub extraction was running every 2s for up to 60s (30 attempts), causing periodic stutter in games. Reduce to 3 attempts at 15s intervals (45s total) so only a few hiccups occur before the fallback to loading AV modules directly. Also clarify kernel-mode feature comments per review: kernel-exception is intentionally excluded from the kernel-mode bundle (broken on PSP-3000 + 6.20 PRO-C). Co-Authored-By: Claude Opus 4.6 --- crates/oasis-backend-psp/Cargo.toml | 4 +++- crates/oasis-plugin-psp/src/audio.rs | 11 ++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/oasis-backend-psp/Cargo.toml b/crates/oasis-backend-psp/Cargo.toml index 7f35af1..8f9a6f2 100644 --- a/crates/oasis-backend-psp/Cargo.toml +++ b/crates/oasis-backend-psp/Cargo.toml @@ -15,8 +15,10 @@ authors = ["AndrewAltimit"] [features] default = [] +# kernel-mode bundles all kernel sub-features EXCEPT kernel-exception +# (intentionally excluded: broken on PSP-3000 + 6.20 PRO-C). kernel-mode = ["psp/kernel", "kernel-volatile", "kernel-me-clock", "kernel-me"] -# Granular kernel sub-features: +# Granular kernel sub-features (each enables psp/kernel independently): kernel-exception = ["psp/kernel"] # sceKernelRegisterDefaultExceptionHandler (broken on PSP-3000 + 6.20 PRO-C) kernel-volatile = ["psp/kernel"] # sceKernelVolatileMemTryLock (extra 4MB) kernel-me-clock = ["psp/kernel"] # scePowerGetMeClockFrequency diff --git a/crates/oasis-plugin-psp/src/audio.rs b/crates/oasis-plugin-psp/src/audio.rs index 818bfaa..4beba38 100644 --- a/crates/oasis-plugin-psp/src/audio.rs +++ b/crates/oasis-plugin-psp/src/audio.rs @@ -1282,11 +1282,12 @@ unsafe fn init_audio_drivers() -> bool { // Step 2: Wait for the game to load AVCODEC modules during its own // init, then piggyback on them. This avoids sceUtilityLoadModule - // conflicts. Retry every 2s for up to 60s before falling back to - // loading modules ourselves. + // conflicts. Check every 15s for up to 45s (3 attempts) before + // falling back to loading modules ourselves. Kept infrequent to + // minimise stutter from the NID scan + stub extraction. { let mut attempt = 0u32; - while attempt < 30 { + while attempt < 3 { if unsafe { try_resolve_codec() } { unsafe { core::ptr::write_volatile(&raw mut DECODER_BACKEND, 2); @@ -1304,8 +1305,8 @@ unsafe fn init_audio_drivers() -> bool { return true; } attempt += 1; - if attempt < 30 { - unsafe { psp::sys::sceKernelDelayThread(2_000_000) }; + if attempt < 3 { + unsafe { psp::sys::sceKernelDelayThread(15_000_000) }; } } } From 5e901110b8c8f4d7e8c9d46590069b1e94a5f5e2 Mon Sep 17 00:00:00 2001 From: AI Agent Bot Date: Sun, 15 Feb 2026 07:00:07 -0600 Subject: [PATCH 3/9] fix: load AV codec modules in EBOOT for PRX piggyback, reduce scan frequency The EBOOT never loaded AVCODEC/MPEGBASE/MP3 modules, so the PRX overlay's codec NID scan always failed repeatedly (causing periodic stutter) before falling back to loading modules itself. Now the EBOOT calls sceUtilityLoadModule for all three AV modules during init, so the PRX finds them on its first scan attempt. Also reduce the PRX scan from 30 attempts x 2s to 3 attempts x 15s as a safety net for games that load codecs late. Co-Authored-By: Claude Opus 4.6 --- crates/oasis-backend-psp/src/main.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/oasis-backend-psp/src/main.rs b/crates/oasis-backend-psp/src/main.rs index 2d9c4d6..4fef64e 100644 --- a/crates/oasis-backend-psp/src/main.rs +++ b/crates/oasis-backend-psp/src/main.rs @@ -578,6 +578,16 @@ fn psp_main() { let mut mp_loaded = false; let mut mp_file_name = String::new(); + // Load AV codec modules so the PRX overlay can piggyback on them + // immediately (avoids repeated NID scan stutter). Errors are + // non-fatal -- the EBOOT uses sceMp3 directly via static imports. + unsafe { + use psp::sys::{sceUtilityLoadModule, Module}; + let _ = sceUtilityLoadModule(Module::AvCodec); + let _ = sceUtilityLoadModule(Module::AvMpegBase); + let _ = sceUtilityLoadModule(Module::AvMp3); + } + // Single background worker thread handles both audio and file I/O. let (audio, io) = oasis_backend_psp::spawn_workers(); let mut pv_loading = false; // true while waiting for async texture load From 9f566f342ee3a612e5c81bd695f8a1dc477b4a7a Mon Sep 17 00:00:00 2001 From: AI Agent Bot Date: Sun, 15 Feb 2026 07:10:23 -0600 Subject: [PATCH 4/9] fix: remove sceUtilityLoadModule from EBOOT to avoid codec deadlock Loading AvCodec/AvMpegBase/AvMp3 via sceUtilityLoadModule conflicts with the EBOOT's statically-linked sceMp3 imports -- both resolve to the same kernel codec but through different paths, causing sceMp3Decode to hang when the user plays audio. The PRX overlay handles its own module loading via its fallback path (load_av_modules after 3 scan attempts). With the reduced scan frequency (3x15s) the stutter is minimal. Co-Authored-By: Claude Opus 4.6 --- crates/oasis-backend-psp/src/main.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/oasis-backend-psp/src/main.rs b/crates/oasis-backend-psp/src/main.rs index 4fef64e..2d9c4d6 100644 --- a/crates/oasis-backend-psp/src/main.rs +++ b/crates/oasis-backend-psp/src/main.rs @@ -578,16 +578,6 @@ fn psp_main() { let mut mp_loaded = false; let mut mp_file_name = String::new(); - // Load AV codec modules so the PRX overlay can piggyback on them - // immediately (avoids repeated NID scan stutter). Errors are - // non-fatal -- the EBOOT uses sceMp3 directly via static imports. - unsafe { - use psp::sys::{sceUtilityLoadModule, Module}; - let _ = sceUtilityLoadModule(Module::AvCodec); - let _ = sceUtilityLoadModule(Module::AvMpegBase); - let _ = sceUtilityLoadModule(Module::AvMp3); - } - // Single background worker thread handles both audio and file I/O. let (audio, io) = oasis_backend_psp::spawn_workers(); let mut pv_loading = false; // true while waiting for async texture load From 55ebeb728f128154884b78d92479a80e6d6ca157 Mon Sep 17 00:00:00 2001 From: AI Agent Bot Date: Sun, 15 Feb 2026 07:23:11 -0600 Subject: [PATCH 5/9] fix: replace SpinMutex with bare atomics to prevent priority inversion deadlock On single-core PSP with preemptive scheduling, the audio thread (priority 16/high) preempting the main thread (priority 32/low) while it holds the SpinMutex causes deadlock -- the audio thread spins forever waiting for a lock that the main thread can never release. Replace SharedAudioState/SpinMutex with individual AtomicBool/AtomicU32 fields for lock-free reads and writes with zero contention. Co-Authored-By: Claude Opus 4.6 --- crates/oasis-backend-psp/src/audio.rs | 6 +- crates/oasis-backend-psp/src/threading.rs | 118 ++++++++-------------- 2 files changed, 45 insertions(+), 79 deletions(-) diff --git a/crates/oasis-backend-psp/src/audio.rs b/crates/oasis-backend-psp/src/audio.rs index 05353b0..73a8fb1 100644 --- a/crates/oasis-backend-psp/src/audio.rs +++ b/crates/oasis-backend-psp/src/audio.rs @@ -194,7 +194,7 @@ impl AudioPlayer { /// PSP audio backend that delegates to the audio worker thread. /// /// Stores loaded track data locally and sends it to the audio thread -/// on `play()`. Reads playback state from the shared `SpinMutex` via +/// on `play()`. Reads playback state from shared atomics via /// `AudioHandle`. pub struct PspAudioBackend { audio: AudioHandle, @@ -270,11 +270,11 @@ impl AudioBackend for PspAudioBackend { } fn position_ms(&self) -> u64 { - self.audio.state().position_ms + self.audio.position_ms() } fn duration_ms(&self) -> u64 { - self.audio.state().duration_ms + self.audio.duration_ms() } fn unload_track(&mut self, track: AudioTrackId) -> Result<()> { diff --git a/crates/oasis-backend-psp/src/threading.rs b/crates/oasis-backend-psp/src/threading.rs index 2d75425..3462af3 100644 --- a/crates/oasis-backend-psp/src/threading.rs +++ b/crates/oasis-backend-psp/src/threading.rs @@ -2,11 +2,14 @@ //! //! Uses `psp::thread::ThreadBuilder` for native PSP kernel threads with //! priority tuning. Communication uses lock-free `SpscQueue` for commands -//! and `SpinMutex` for shared state readable from the main thread. +//! and bare atomics for shared state (lock-free to avoid priority inversion +//! on single-core PSP where a high-priority audio thread could starve the +//! main thread if both contend on a spinlock). use std::sync::Arc; -use psp::sync::{SpinMutex, SpscQueue}; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use psp::sync::SpscQueue; use psp::thread::ThreadBuilder; use crate::audio::AudioPlayer; @@ -25,45 +28,17 @@ static IO_CMD_QUEUE: SpscQueue = SpscQueue::new(); static IO_RESP_QUEUE: SpscQueue = SpscQueue::new(); // --------------------------------------------------------------------------- -// Shared audio state (SpinMutex for richer state than bare atomics) +// Shared audio state (lock-free atomics -- no priority inversion) // --------------------------------------------------------------------------- -/// Shared audio state protected by a spinlock. -/// -/// Readable from the main thread and written by the audio thread. -/// PSP is single-core so SpinMutex has near-zero overhead for short -/// critical sections. -static SHARED_AUDIO: SpinMutex = SpinMutex::new(SharedAudioState::new()); - -/// Audio state shared between the audio thread and main thread. -#[derive(Clone)] -pub struct SharedAudioState { - pub playing: bool, - pub paused: bool, - pub sample_rate: u32, - pub bitrate: u32, - pub channels: u32, - pub position_ms: u64, - pub duration_ms: u64, - pub track_name: [u8; 64], - pub track_name_len: usize, -} - -impl SharedAudioState { - const fn new() -> Self { - Self { - playing: false, - paused: false, - sample_rate: 0, - bitrate: 0, - channels: 0, - position_ms: 0, - duration_ms: 0, - track_name: [0u8; 64], - track_name_len: 0, - } - } -} +static AUDIO_PLAYING: AtomicBool = AtomicBool::new(false); +static AUDIO_PAUSED: AtomicBool = AtomicBool::new(false); +static AUDIO_SAMPLE_RATE: AtomicU32 = AtomicU32::new(0); +static AUDIO_BITRATE: AtomicU32 = AtomicU32::new(0); +static AUDIO_CHANNELS: AtomicU32 = AtomicU32::new(0); +// Position/duration stored as u32 milliseconds (max ~49 days, plenty). +static AUDIO_POSITION_MS: AtomicU32 = AtomicU32::new(0); +static AUDIO_DURATION_MS: AtomicU32 = AtomicU32::new(0); // --------------------------------------------------------------------------- // Audio commands @@ -81,7 +56,7 @@ pub enum AudioCmd { Shutdown, } -/// Handle to the background audio thread (reads from SHARED_AUDIO). +/// Handle to the background audio thread (reads shared atomics). pub struct AudioHandle; impl AudioHandle { @@ -90,37 +65,32 @@ impl AudioHandle { let _ = AUDIO_QUEUE.push(cmd); } - /// Snapshot the current audio state (short spinlock hold). - pub fn state(&self) -> SharedAudioState { - SHARED_AUDIO.lock().clone() - } - pub fn is_playing(&self) -> bool { - SHARED_AUDIO.lock().playing + AUDIO_PLAYING.load(Ordering::Relaxed) } pub fn is_paused(&self) -> bool { - SHARED_AUDIO.lock().paused + AUDIO_PAUSED.load(Ordering::Relaxed) } pub fn sample_rate(&self) -> u32 { - SHARED_AUDIO.lock().sample_rate + AUDIO_SAMPLE_RATE.load(Ordering::Relaxed) } pub fn bitrate(&self) -> u32 { - SHARED_AUDIO.lock().bitrate + AUDIO_BITRATE.load(Ordering::Relaxed) } pub fn channels(&self) -> u32 { - SHARED_AUDIO.lock().channels + AUDIO_CHANNELS.load(Ordering::Relaxed) } pub fn position_ms(&self) -> u64 { - SHARED_AUDIO.lock().position_ms + AUDIO_POSITION_MS.load(Ordering::Relaxed) as u64 } pub fn duration_ms(&self) -> u64 { - SHARED_AUDIO.lock().duration_ms + AUDIO_DURATION_MS.load(Ordering::Relaxed) as u64 } } @@ -243,33 +213,32 @@ fn audio_thread_fn() { if player.load_and_play(&path) { publish_audio_state(&player); } else { - SHARED_AUDIO.lock().playing = false; + AUDIO_PLAYING.store(false, Ordering::Relaxed); } }, Some(AudioCmd::LoadAndPlayData(data)) => { if player.load_and_play_data(&data) { publish_audio_state(&player); } else { - SHARED_AUDIO.lock().playing = false; + AUDIO_PLAYING.store(false, Ordering::Relaxed); } }, Some(AudioCmd::Pause) => { if player.is_playing() && !player.is_paused() { player.toggle_pause(); - SHARED_AUDIO.lock().paused = true; + AUDIO_PAUSED.store(true, Ordering::Relaxed); } }, Some(AudioCmd::Resume) => { if player.is_playing() && player.is_paused() { player.toggle_pause(); - SHARED_AUDIO.lock().paused = false; + AUDIO_PAUSED.store(false, Ordering::Relaxed); } }, Some(AudioCmd::Stop) => { player.stop(); - let mut state = SHARED_AUDIO.lock(); - state.playing = false; - state.paused = false; + AUDIO_PLAYING.store(false, Ordering::Relaxed); + AUDIO_PAUSED.store(false, Ordering::Relaxed); }, Some(AudioCmd::SetVolume(v)) => { player.set_volume(v); @@ -281,7 +250,7 @@ fn audio_thread_fn() { }, Some(AudioCmd::Shutdown) => { player.stop(); - SHARED_AUDIO.lock().playing = false; + AUDIO_PLAYING.store(false, Ordering::Relaxed); break; }, None => {}, @@ -290,14 +259,11 @@ fn audio_thread_fn() { if player.is_playing() && !player.is_paused() { // update() contains the blocking sceAudioOutputBlocking call. player.update(); - // Publish position each frame. - { - let mut state = SHARED_AUDIO.lock(); - state.position_ms = player.position_ms(); - state.duration_ms = player.duration_ms(); - } + // Publish position each frame (lock-free atomic stores). + AUDIO_POSITION_MS.store(player.position_ms() as u32, Ordering::Relaxed); + AUDIO_DURATION_MS.store(player.duration_ms() as u32, Ordering::Relaxed); if !player.is_playing() { - SHARED_AUDIO.lock().playing = false; + AUDIO_PLAYING.store(false, Ordering::Relaxed); } } else { // Sleep when idle to avoid spinning. @@ -311,16 +277,16 @@ fn audio_thread_fn() { } } -/// Publish audio player state to the shared spinlock after a load_and_play. +/// Publish audio player state to shared atomics after a load_and_play. fn publish_audio_state(player: &AudioPlayer) { - let mut state = SHARED_AUDIO.lock(); - state.playing = true; - state.paused = false; - state.sample_rate = player.sample_rate; - state.bitrate = player.bitrate; - state.channels = player.channels; - state.position_ms = 0; - state.duration_ms = 0; + AUDIO_SAMPLE_RATE.store(player.sample_rate, Ordering::Relaxed); + AUDIO_BITRATE.store(player.bitrate, Ordering::Relaxed); + AUDIO_CHANNELS.store(player.channels, Ordering::Relaxed); + AUDIO_POSITION_MS.store(0, Ordering::Relaxed); + AUDIO_DURATION_MS.store(0, Ordering::Relaxed); + AUDIO_PAUSED.store(false, Ordering::Relaxed); + // Set playing LAST so readers see consistent metadata first. + AUDIO_PLAYING.store(true, Ordering::Relaxed); } // --------------------------------------------------------------------------- From aa31038a1d5b687bd2dc2dfccec1e20bcbb044e2 Mon Sep 17 00:00:00 2001 From: AI Agent Bot Date: Sun, 15 Feb 2026 09:28:02 -0600 Subject: [PATCH 6/9] fix: JoinHandle leak, lazy AV modules, remove debug logging - Leak JoinHandles with core::mem::forget() to prevent sceKernelTerminateDeleteThread from killing worker threads on drop - Defer sceUtilityLoadModule (AvCodec/AvMpegBase/AvMp3) to first play to avoid ME EDRAM conflict with PRX overlay at boot time - Remove sceMp3InitResource probe from audio.rs (SDK now skips ID3v2) - Remove all file-based debug logging and atomic debug counters - Use sceKernelDelayThread directly for idle sleep (avoids TLS) - Switch psp dependency to local path for ID3v2 skip fix Co-Authored-By: Claude Opus 4.6 --- crates/oasis-backend-psp/Cargo.toml | 2 +- crates/oasis-backend-psp/src/audio.rs | 36 +++++++++++++---------- crates/oasis-backend-psp/src/main.rs | 5 ++++ crates/oasis-backend-psp/src/threading.rs | 33 +++++++++------------ 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/crates/oasis-backend-psp/Cargo.toml b/crates/oasis-backend-psp/Cargo.toml index 8f9a6f2..a4be2f8 100644 --- a/crates/oasis-backend-psp/Cargo.toml +++ b/crates/oasis-backend-psp/Cargo.toml @@ -25,7 +25,7 @@ kernel-me-clock = ["psp/kernel"] # scePowerGetMeClockFrequency kernel-me = ["psp/kernel"] # ME coprocessor (me test command) [dependencies] -psp = { git = "https://github.com/AndrewAltimit/rust-psp", features = ["std"] } +psp = { path = "/home/mikunpc/Documents/repos/rust-psp/psp", features = ["std"] } oasis-core = { path = "../oasis-core" } libm = "0.2" diff --git a/crates/oasis-backend-psp/src/audio.rs b/crates/oasis-backend-psp/src/audio.rs index 73a8fb1..6810bbd 100644 --- a/crates/oasis-backend-psp/src/audio.rs +++ b/crates/oasis-backend-psp/src/audio.rs @@ -13,6 +13,21 @@ use crate::threading::{AudioCmd, AudioHandle, send_audio_cmd}; /// Standard MP3 frame size (MPEG1 Layer 3). const MP3_FRAME_SAMPLES: i32 = 1152; +/// Load AV codec modules once (idempotent). Called lazily on first play +/// to avoid conflicts with the PRX overlay at boot time. +fn load_av_modules_once() { + use core::sync::atomic::{AtomicBool, Ordering}; + static LOADED: AtomicBool = AtomicBool::new(false); + if LOADED.swap(true, Ordering::Relaxed) { + return; // Already loaded. + } + unsafe { + psp::sys::sceUtilityLoadModule(psp::sys::Module::AvCodec); + psp::sys::sceUtilityLoadModule(psp::sys::Module::AvMpegBase); + psp::sys::sceUtilityLoadModule(psp::sys::Module::AvMp3); + } +} + /// MP3 playback engine using the PSP's hardware MP3 decoder. /// /// Uses RAII wrappers from `psp::mp3::Mp3Decoder` and @@ -76,12 +91,13 @@ impl AudioPlayer { return false; } + // Lazily load AV codec modules on first play. This avoids + // conflicts with the PRX overlay's sceAudiocodec at boot time. + load_av_modules_once(); + let decoder = match Mp3Decoder::new(data) { Ok(d) => d, - Err(e) => { - psp::dprintln!("OASIS_OS: Mp3Decoder failed: {:?}", e); - return false; - }, + Err(_) => return false, }; self.sample_rate = decoder.sample_rate(); @@ -98,23 +114,13 @@ impl AudioPlayer { let channel = match AudioChannel::reserve(MP3_FRAME_SAMPLES, fmt) { Ok(ch) => ch, - Err(e) => { - psp::dprintln!("OASIS_OS: AudioChannel::reserve failed: {:?}", e,); - return false; - }, + Err(_) => return false, }; self.decoder = Some(decoder); self.channel = Some(channel); self.playing = true; self.paused = false; - - psp::dprintln!( - "OASIS_OS: MP3 loaded - {}Hz, {}kbps, {}ch", - self.sample_rate, - self.bitrate, - self.channels, - ); true } diff --git a/crates/oasis-backend-psp/src/main.rs b/crates/oasis-backend-psp/src/main.rs index 2d9c4d6..8c3f009 100644 --- a/crates/oasis-backend-psp/src/main.rs +++ b/crates/oasis-backend-psp/src/main.rs @@ -578,6 +578,11 @@ fn psp_main() { let mut mp_loaded = false; let mut mp_file_name = String::new(); + // AV codec modules (AvCodec, AvMpegBase, AvMp3) are loaded lazily + // by the audio thread on first play. Loading them here at startup + // would conflict with the PRX overlay's sceAudiocodec if the PRX + // initialized before the EBOOT was launched. + // Single background worker thread handles both audio and file I/O. let (audio, io) = oasis_backend_psp::spawn_workers(); let mut pv_loading = false; // true while waiting for async texture load diff --git a/crates/oasis-backend-psp/src/threading.rs b/crates/oasis-backend-psp/src/threading.rs index 3462af3..0c543d2 100644 --- a/crates/oasis-backend-psp/src/threading.rs +++ b/crates/oasis-backend-psp/src/threading.rs @@ -164,28 +164,31 @@ impl IoHandle { /// Spawn the background audio and I/O threads. /// -/// Returns handles for audio state and I/O responses. +/// Returns handles for audio state and I/O responses. The `JoinHandle`s +/// are leaked intentionally — the worker threads run for the lifetime of +/// the process, and dropping a `JoinHandle` terminates its thread. pub fn spawn_workers() -> (AudioHandle, IoHandle) { // Audio thread: high priority (16) for low-latency playback. - let audio_result = ThreadBuilder::new(b"oasis_audio\0") + if let Ok(handle) = ThreadBuilder::new(b"oasis_audio\0") .priority(16) .spawn(move || { audio_thread_fn(); 0 - }); - if let Err(e) = &audio_result { - psp::dprintln!("OASIS_OS: Failed to spawn audio thread: {:?}", e); + }) + { + // Leak the JoinHandle so the thread isn't killed on drop. + core::mem::forget(handle); } // I/O thread: normal priority (32) for file operations. - let io_result = ThreadBuilder::new(b"oasis_io\0") + if let Ok(handle) = ThreadBuilder::new(b"oasis_io\0") .priority(32) .spawn(move || { io_thread_fn(); 0 - }); - if let Err(e) = &io_result { - psp::dprintln!("OASIS_OS: Failed to spawn I/O thread: {:?}", e); + }) + { + core::mem::forget(handle); } (AudioHandle, IoHandle) @@ -198,14 +201,9 @@ pub fn spawn_workers() -> (AudioHandle, IoHandle) { /// Dedicated audio thread: MP3 playback + SFX mixing. fn audio_thread_fn() { let mut player = AudioPlayer::new(); - if !player.init() { - psp::dprintln!("OASIS_OS: Audio thread init failed"); - } + player.init(); let mut sfx = SfxEngine::new(); - if sfx.is_none() { - psp::dprintln!("OASIS_OS: SFX engine init failed (non-fatal)"); - } loop { match AUDIO_QUEUE.pop() { @@ -257,17 +255,14 @@ fn audio_thread_fn() { } if player.is_playing() && !player.is_paused() { - // update() contains the blocking sceAudioOutputBlocking call. player.update(); - // Publish position each frame (lock-free atomic stores). AUDIO_POSITION_MS.store(player.position_ms() as u32, Ordering::Relaxed); AUDIO_DURATION_MS.store(player.duration_ms() as u32, Ordering::Relaxed); if !player.is_playing() { AUDIO_PLAYING.store(false, Ordering::Relaxed); } } else { - // Sleep when idle to avoid spinning. - psp::thread::sleep_ms(10); + unsafe { psp::sys::sceKernelDelayThread(10_000) }; } // Pump SFX mixer (separate hardware channel, short blocking). From 374b8be20e986871e09799c20b93e668b5103ee3 Mon Sep 17 00:00:00 2001 From: AI Agent Bot Date: Sun, 15 Feb 2026 12:10:22 -0600 Subject: [PATCH 7/9] feat: streaming sceAudiocodec MP3 player with OASIS detection Replace sceMp3 with sceAudiocodec for EBOOT MP3 playback. The sceMp3 API is unstable on real PSP hardware (PSP-3000 + 6.20 PRO-C), crashing after ~2-3 song switches regardless of handle reuse strategy. New approach streams MP3 from file using a fixed 32KB read buffer with frame-by-frame sceAudiocodec decoding -- zero large heap allocations, matching the PRX plugin's proven pattern. Decoder EDRAM and audio channel are allocated once and reused across all songs. Also adds OASIS OS detection to the PRX plugin so it skips audio codec usage when the EBOOT is running (both share the ME coprocessor's EDRAM). Updates rust-psp dependency from fix/mp3-id3v2-skip branch to main (ID3v2 skip fix, reload APIs, and stability docs now merged upstream). Co-Authored-By: Claude Opus 4.6 --- crates/oasis-backend-psp/Cargo.lock | 2 +- crates/oasis-backend-psp/Cargo.toml | 2 +- crates/oasis-backend-psp/src/audio.rs | 451 ++++++++++++++++++---- crates/oasis-backend-psp/src/threading.rs | 6 +- crates/oasis-plugin-psp/src/audio.rs | 62 ++- 5 files changed, 442 insertions(+), 81 deletions(-) diff --git a/crates/oasis-backend-psp/Cargo.lock b/crates/oasis-backend-psp/Cargo.lock index 723edab..622b9e6 100644 --- a/crates/oasis-backend-psp/Cargo.lock +++ b/crates/oasis-backend-psp/Cargo.lock @@ -748,7 +748,7 @@ dependencies = [ [[package]] name = "psp" version = "0.4.0" -source = "git+https://github.com/AndrewAltimit/rust-psp#edb63cfb7828374fffac218138187b52d0d567f5" +source = "git+https://github.com/AndrewAltimit/rust-psp#680d3d3284ace336219daf05410ad258daa675bf" dependencies = [ "bitflags", "libm", diff --git a/crates/oasis-backend-psp/Cargo.toml b/crates/oasis-backend-psp/Cargo.toml index a4be2f8..8f9a6f2 100644 --- a/crates/oasis-backend-psp/Cargo.toml +++ b/crates/oasis-backend-psp/Cargo.toml @@ -25,7 +25,7 @@ kernel-me-clock = ["psp/kernel"] # scePowerGetMeClockFrequency kernel-me = ["psp/kernel"] # ME coprocessor (me test command) [dependencies] -psp = { path = "/home/mikunpc/Documents/repos/rust-psp/psp", features = ["std"] } +psp = { git = "https://github.com/AndrewAltimit/rust-psp", features = ["std"] } oasis-core = { path = "../oasis-core" } libm = "0.2" diff --git a/crates/oasis-backend-psp/src/audio.rs b/crates/oasis-backend-psp/src/audio.rs index 6810bbd..7dc5a0f 100644 --- a/crates/oasis-backend-psp/src/audio.rs +++ b/crates/oasis-backend-psp/src/audio.rs @@ -1,9 +1,16 @@ -//! Audio playback (MP3 via psp::mp3 + psp::audio) and `AudioBackend` trait. - -use std::sync::Arc; +//! Audio playback (MP3 via sceAudiocodec + psp::audio) and `AudioBackend` trait. +//! +//! Uses the low-level `sceAudiocodec` frame-by-frame decoder instead of the +//! high-level `sceMp3*` API, which crashes on real PSP hardware after ~2-3 +//! handle reuse cycles. +//! +//! MP3 data is **streamed from file** using a small fixed-size read buffer +//! (32 KB) to avoid large heap allocations that cause heap fragmentation +//! and crashes on PSP's limited 24 MB user memory. use psp::audio::{AudioChannel, AudioFormat}; -use psp::mp3::Mp3Decoder; +use psp::audiocodec::{AudiocodecDecoder, CodecType}; +use psp::mp3::{find_sync, skip_id3v2}; use oasis_core::backend::{AudioBackend, AudioTrackId}; use oasis_core::error::{OasisError, Result}; @@ -13,6 +20,10 @@ use crate::threading::{AudioCmd, AudioHandle, send_audio_cmd}; /// Standard MP3 frame size (MPEG1 Layer 3). const MP3_FRAME_SAMPLES: i32 = 1152; +/// Size of the read buffer for streaming MP3 from file. +/// 32 KB is enough for many MP3 frames and avoids large heap allocations. +const READ_BUF_SIZE: usize = 32 * 1024; + /// Load AV codec modules once (idempotent). Called lazily on first play /// to avoid conflicts with the PRX overlay at boot time. fn load_av_modules_once() { @@ -28,26 +39,117 @@ fn load_av_modules_once() { } } -/// MP3 playback engine using the PSP's hardware MP3 decoder. +// --------------------------------------------------------------------------- +// MP3 frame header parsing +// --------------------------------------------------------------------------- + +/// MPEG version bitrate tables (kbps). Index: bitrate_index (1..14). +const BITRATES_V1_L3: [u32; 15] = + [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]; +const BITRATES_V2_L3: [u32; 15] = + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]; + +/// Sample rates by MPEG version. [version_index][srate_index] +const SAMPLE_RATES: [[u32; 3]; 4] = [ + [11025, 12000, 8000], // MPEG 2.5 + [0, 0, 0], // reserved + [22050, 24000, 16000], // MPEG 2 + [44100, 48000, 32000], // MPEG 1 +]; + +/// Parsed MP3 frame header. +struct Mp3FrameHeader { + /// Sample rate in Hz. + sample_rate: u32, + /// Bitrate in kbps. + bitrate: u32, + /// Number of channels (1 or 2). + channels: u32, +} + +/// Parse an MP3 frame header from 4 bytes starting at a sync position. +fn parse_mp3_header(data: &[u8]) -> Option { + if data.len() < 4 { + return None; + } + let b1 = data[1]; + let b2 = data[2]; + let b3 = data[3]; + + let version_bits = (b1 >> 3) & 0x03; + let layer_bits = (b1 >> 1) & 0x03; + let bitrate_idx = (b2 >> 4) & 0x0F; + let srate_idx = (b2 >> 2) & 0x03; + let channel_mode = (b3 >> 6) & 0x03; + + if version_bits == 1 || layer_bits == 0 || bitrate_idx == 0 || bitrate_idx == 15 + || srate_idx == 3 + { + return None; + } + // Only Layer III. + if layer_bits != 1 { + return None; + } + + let is_v1 = version_bits == 3; + let bitrate = if is_v1 { + BITRATES_V1_L3[bitrate_idx as usize] + } else { + BITRATES_V2_L3[bitrate_idx as usize] + }; + let sample_rate = SAMPLE_RATES[version_bits as usize][srate_idx as usize]; + if sample_rate == 0 || bitrate == 0 { + return None; + } + let channels = if channel_mode == 3 { 1 } else { 2 }; + + Some(Mp3FrameHeader { sample_rate, bitrate, channels }) +} + +// --------------------------------------------------------------------------- +// AudioPlayer — streaming sceAudiocodec-based MP3 playback +// --------------------------------------------------------------------------- + +/// MP3 playback engine using sceAudiocodec with file streaming. +/// +/// Instead of loading the entire MP3 into a Vec (which fragments the PSP's +/// 24MB heap after 2-3 songs), this streams from an open file descriptor +/// using a fixed 32KB read buffer — matching the PRX plugin's approach. /// -/// Uses RAII wrappers from `psp::mp3::Mp3Decoder` and -/// `psp::audio::AudioChannel`. Call `load_and_play()` to start, -/// `update()` each frame to pump decoded audio, and `stop()` to halt. +/// The `AudiocodecDecoder` and `AudioChannel` are created once and reused +/// across all songs. pub struct AudioPlayer { - decoder: Option, + decoder: Option, channel: Option, + /// Fixed read buffer for streaming MP3 data from file. + read_buf: Vec, + /// Number of valid bytes in `read_buf`. + buf_valid: usize, + /// Current read position within `read_buf`. + buf_pos: usize, + /// Open file descriptor for the current song (negative = none). + fd: psp::sys::SceUid, + /// Total file size in bytes. + file_size: usize, + /// Bytes read from file so far. + file_pos: usize, playing: bool, paused: bool, - /// Hardware volume (0x0000..=0x8000 range for PSP audio API). + /// Hardware volume (0x0000..=0x8000). hw_volume: i32, - /// Cached MP3 info. + /// PCM decode buffer (stereo: 1152 * 2 samples). + pcm_buf: Vec, + /// Cached MP3 info from first frame header. pub sample_rate: u32, pub bitrate: u32, pub channels: u32, /// Count of decoded MP3 frames (for position tracking). pub frames_decoded: u32, - /// Total file size in bytes (for duration estimation). + /// Total MP3 data size in bytes (for duration estimation). pub data_size: u32, + /// Consecutive decode errors (to bail on corrupt data). + error_count: u32, } impl AudioPlayer { @@ -55,105 +157,300 @@ impl AudioPlayer { Self { decoder: None, channel: None, + read_buf: vec![0u8; READ_BUF_SIZE], + buf_valid: 0, + buf_pos: 0, + fd: psp::sys::SceUid(-1), + file_size: 0, + file_pos: 0, playing: false, paused: false, hw_volume: 0x8000, + pcm_buf: vec![0i16; 1152 * 2], sample_rate: 0, bitrate: 0, channels: 0, frames_decoded: 0, data_size: 0, + error_count: 0, } } - /// Initialize the audio subsystem. - /// - /// With the new SDK, `Mp3Decoder` handles resource init on construction, - /// so this is a no-op kept for API compatibility with the worker thread. + /// Initialize the audio subsystem (no-op, kept for API compat). pub fn init(&mut self) -> bool { true } - /// Load an MP3 file from the Memory Stick and start playback. - pub fn load_and_play(&mut self, path: &str) -> bool { - let data = match psp::io::read_to_vec(path) { - Ok(d) => d, - Err(_) => return false, - }; - self.load_and_play_data(&data) + /// Close the current file if open. + fn close_file(&mut self) { + if self.fd >= psp::sys::SceUid(0) { + unsafe { psp::sys::sceIoClose(self.fd) }; + self.fd = psp::sys::SceUid(-1); + } } - /// Start playback from raw MP3 data already in memory. - pub fn load_and_play_data(&mut self, data: &[u8]) -> bool { - self.stop(); + /// Load an MP3 file from the Memory Stick and start streaming playback. + pub fn load_and_play(&mut self, path: &str) -> bool { + self.playing = false; + self.paused = false; + self.close_file(); + + load_av_modules_once(); - if data.is_empty() { + // Open file. + let mut path_bytes: Vec = path.as_bytes().to_vec(); + path_bytes.push(0); // null-terminate + let fd = unsafe { + psp::sys::sceIoOpen( + path_bytes.as_ptr(), + psp::sys::IoOpenFlags::RD_ONLY, + 0, + ) + }; + if fd < psp::sys::SceUid(0) { return false; } - // Lazily load AV codec modules on first play. This avoids - // conflicts with the PRX overlay's sceAudiocodec at boot time. - load_av_modules_once(); + // Get file size. + let size = unsafe { + psp::sys::sceIoLseek(fd, 0, psp::sys::IoWhence::End) + } as usize; + unsafe { psp::sys::sceIoLseek(fd, 0, psp::sys::IoWhence::Set) }; + if size == 0 { + unsafe { psp::sys::sceIoClose(fd) }; + return false; + } - let decoder = match Mp3Decoder::new(data) { - Ok(d) => d, - Err(_) => return false, + self.fd = fd; + self.file_size = size; + self.file_pos = 0; + self.data_size = size as u32; + + // Initial read into buffer. + let read = unsafe { + psp::sys::sceIoRead( + self.fd, + self.read_buf.as_mut_ptr() as *mut _, + READ_BUF_SIZE as u32, + ) }; + if read <= 0 { + self.close_file(); + return false; + } + self.buf_valid = read as usize; + self.buf_pos = 0; + self.file_pos = self.buf_valid; + + // Skip ID3v2 tag. + let id3_skip = skip_id3v2(&self.read_buf[..self.buf_valid]); + if id3_skip > 0 && id3_skip < self.buf_valid { + self.buf_pos = id3_skip; + } - self.sample_rate = decoder.sample_rate(); - self.bitrate = decoder.bitrate(); - self.channels = decoder.channels() as u32; - self.frames_decoded = 0; - self.data_size = data.len() as u32; + // Parse first frame header for metadata. + let buf_slice = &self.read_buf[self.buf_pos..self.buf_valid]; + if let Some(sync_off) = find_sync(buf_slice, 0) { + if let Some(hdr) = parse_mp3_header(&buf_slice[sync_off..]) { + self.sample_rate = hdr.sample_rate; + self.bitrate = hdr.bitrate; + self.channels = hdr.channels; + } + } - let fmt = if self.channels == 1 { - AudioFormat::Mono - } else { - AudioFormat::Stereo - }; + self.frames_decoded = 0; + self.error_count = 0; + + // Create the audiocodec decoder once, reuse forever. + if self.decoder.is_none() { + match AudiocodecDecoder::new(CodecType::Mp3) { + Ok(dec) => self.decoder = Some(dec), + Err(_) => { + self.close_file(); + return false; + }, + } + } - let channel = match AudioChannel::reserve(MP3_FRAME_SAMPLES, fmt) { - Ok(ch) => ch, - Err(_) => return false, - }; + // Reuse audio channel if we already have one. + if self.channel.is_none() { + match AudioChannel::reserve(MP3_FRAME_SAMPLES, AudioFormat::Stereo) { + Ok(ch) => self.channel = Some(ch), + Err(_) => { + self.close_file(); + return false; + }, + } + } - self.decoder = Some(decoder); - self.channel = Some(channel); self.playing = true; self.paused = false; true } + /// Start playback from in-memory MP3 data. + /// + /// Writes data to a temp file and streams from it, keeping the same + /// streaming architecture. Falls back to the file path approach. + pub fn load_and_play_owned(&mut self, data: Vec) -> bool { + // Write data to a temp file, then stream from it. + // This avoids keeping the full MP3 in heap memory. + let temp_path = "ms0:/PSP/GAME/oasis_os/__temp_audio.mp3"; + let temp_path_c = b"ms0:/PSP/GAME/oasis_os/__temp_audio.mp3\0"; + + let fd = unsafe { + psp::sys::sceIoOpen( + temp_path_c.as_ptr(), + psp::sys::IoOpenFlags::WR_ONLY + | psp::sys::IoOpenFlags::CREAT + | psp::sys::IoOpenFlags::TRUNC, + 0o777, + ) + }; + if fd < psp::sys::SceUid(0) { + return false; + } + let written = unsafe { + psp::sys::sceIoWrite(fd, data.as_ptr() as *const _, data.len()) + }; + unsafe { psp::sys::sceIoClose(fd) }; + + // Free the input data immediately — we don't need it anymore. + drop(data); + + if written <= 0 { + return false; + } + + self.load_and_play(temp_path) + } + + /// Start playback from a borrowed slice (copies data internally). + pub fn load_and_play_data(&mut self, data: &[u8]) -> bool { + self.load_and_play_owned(data.to_vec()) + } + + /// Refill the read buffer: compact consumed data and read more from file. + /// + /// Uses the PRX plugin's compact+stream-refill pattern: when more than + /// half the buffer is consumed, shift remaining data to the front and + /// top up in small chunks (4KB) to avoid blocking audio output. + fn refill_buffer(&mut self) { + // Compact when more than half consumed. + if self.buf_pos > READ_BUF_SIZE / 2 && self.file_pos < self.file_size { + let remaining = self.buf_valid - self.buf_pos; + if remaining > 0 { + // SAFETY: Manual byte-by-byte copy (LLVM memcpy can recurse + // on MIPS). src and dst overlap so we copy forward. + let ptr = self.read_buf.as_mut_ptr(); + for i in 0..remaining { + unsafe { *ptr.add(i) = *ptr.add(self.buf_pos + i) }; + } + } + self.buf_valid = remaining; + self.buf_pos = 0; + } + + // Top up buffer if there's room (small reads to avoid audio stalls). + if self.buf_valid < READ_BUF_SIZE && self.file_pos < self.file_size { + let room = READ_BUF_SIZE - self.buf_valid; + let chunk = room.min(4096); + let read = unsafe { + psp::sys::sceIoRead( + self.fd, + self.read_buf.as_mut_ptr().add(self.buf_valid) as *mut _, + chunk as u32, + ) + }; + if read > 0 { + self.buf_valid += read as usize; + self.file_pos += read as usize; + } + } + } + /// Pump decoded audio to the output channel. Call each frame. pub fn update(&mut self) { if !self.playing || self.paused { return; } - let (Some(decoder), Some(channel)) = (&mut self.decoder, &self.channel) else { + if self.decoder.is_none() || self.channel.is_none() { + return; + } + + // Refill buffer from file (before borrowing decoder/channel). + self.refill_buffer(); + + let avail = self.buf_valid - self.buf_pos; + if avail < 4 { + self.playing = false; + self.close_file(); return; + } + + // Find next MP3 frame sync in the buffer. + let sync_pos = match find_sync(&self.read_buf[..self.buf_valid], self.buf_pos) { + Some(p) => p, + None => { + self.buf_pos = self.buf_valid; + return; + }, }; + self.buf_pos = sync_pos; - match decoder.decode_frame() { - Ok(samples) if !samples.is_empty() => { + if self.buf_valid - self.buf_pos < 8 { + return; + } + + // Decode one frame via sceAudiocodec. + // We use split borrows: decoder, channel, read_buf, and pcm_buf are + // all separate fields, so borrowck allows simultaneous access. + for s in &mut self.pcm_buf { + *s = 0; + } + + let decoder = self.decoder.as_mut().unwrap(); + let buf_pos = self.buf_pos; + let buf_valid = self.buf_valid; + let result = decoder.decode( + &self.read_buf[buf_pos..buf_valid], + &mut self.pcm_buf, + ); + + match result { + Ok(consumed) => { + if consumed == 0 { + self.error_count += 1; + self.buf_pos += 1; + if self.error_count > 100 { + self.playing = false; + self.close_file(); + } + return; + } + self.error_count = 0; + self.buf_pos += consumed; self.frames_decoded += 1; - // output_blocking paces playback to hardware timing. - let _ = channel.output_blocking(self.hw_volume, samples); + let channel = self.channel.as_ref().unwrap(); + let _ = channel.output_blocking(self.hw_volume, &self.pcm_buf); }, - _ => { - // End of stream or decode error. - self.playing = false; + Err(_) => { + self.error_count += 1; + self.buf_pos += 1; + if self.error_count > 100 { + self.playing = false; + self.close_file(); + } }, } } - /// Stop playback and release resources. + /// Stop playback. Decoder and channel are kept alive for reuse. pub fn stop(&mut self) { - // Drop order: channel first (stops hardware output), then decoder. - self.channel = None; - self.decoder = None; self.playing = false; self.paused = false; + self.close_file(); } /// Toggle pause/resume. @@ -180,7 +477,8 @@ impl AudioPlayer { if self.sample_rate == 0 { return 0; } - (self.frames_decoded as u64 * MP3_FRAME_SAMPLES as u64 * 1000) / self.sample_rate as u64 + (self.frames_decoded as u64 * MP3_FRAME_SAMPLES as u64 * 1000) + / self.sample_rate as u64 } /// Estimated total duration in milliseconds (from bitrate + file size). @@ -188,23 +486,28 @@ impl AudioPlayer { if self.bitrate == 0 { return 0; } - // bitrate is in kbps, data_size in bytes. (self.data_size as u64 * 8) / self.bitrate as u64 } } +impl Drop for AudioPlayer { + fn drop(&mut self) { + self.close_file(); + } +} + // --------------------------------------------------------------------------- // AudioBackend trait implementation (delegates to worker thread) // --------------------------------------------------------------------------- /// PSP audio backend that delegates to the audio worker thread. /// -/// Stores loaded track data locally and sends it to the audio thread -/// on `play()`. Reads playback state from shared atomics via -/// `AudioHandle`. +/// Track data is moved (not cloned) to the audio thread on play to +/// minimize peak memory. Only one track's data lives in this struct +/// at a time — previous tracks are freed on load. pub struct PspAudioBackend { audio: AudioHandle, - tracks: Vec>>>, + tracks: Vec>>, current_track: Option, volume: u8, } @@ -228,7 +531,11 @@ impl AudioBackend for PspAudioBackend { fn load_track(&mut self, data: &[u8]) -> Result { let id = self.tracks.len() as u64; - self.tracks.push(Some(Arc::new(data.to_vec()))); + // Free all previous track data to conserve PSP memory. + for slot in &mut self.tracks { + *slot = None; + } + self.tracks.push(Some(data.to_vec())); Ok(AudioTrackId(id)) } @@ -236,12 +543,10 @@ impl AudioBackend for PspAudioBackend { let idx = track.0 as usize; let data = self .tracks - .get(idx) - .and_then(|slot| slot.as_ref()) + .get_mut(idx) + .and_then(|slot| slot.take()) .ok_or_else(|| OasisError::Backend(format!("track {} not loaded", track.0)))?; - // Arc::clone is cheap (ref count bump) -- avoids duplicating the - // entire MP3 buffer on a memory-constrained device. - send_audio_cmd(AudioCmd::LoadAndPlayData(Arc::clone(data))); + send_audio_cmd(AudioCmd::LoadAndPlayData(data)); self.current_track = Some(track.0); Ok(()) } diff --git a/crates/oasis-backend-psp/src/threading.rs b/crates/oasis-backend-psp/src/threading.rs index 0c543d2..46b1eeb 100644 --- a/crates/oasis-backend-psp/src/threading.rs +++ b/crates/oasis-backend-psp/src/threading.rs @@ -6,8 +6,6 @@ //! on single-core PSP where a high-priority audio thread could starve the //! main thread if both contend on a spinlock). -use std::sync::Arc; - use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use psp::sync::SpscQueue; use psp::thread::ThreadBuilder; @@ -47,7 +45,7 @@ static AUDIO_DURATION_MS: AtomicU32 = AtomicU32::new(0); /// Commands for the dedicated audio thread. pub enum AudioCmd { LoadAndPlay(String), - LoadAndPlayData(Arc>), + LoadAndPlayData(Vec), Pause, Resume, Stop, @@ -215,7 +213,7 @@ fn audio_thread_fn() { } }, Some(AudioCmd::LoadAndPlayData(data)) => { - if player.load_and_play_data(&data) { + if player.load_and_play_owned(data) { publish_audio_state(&player); } else { AUDIO_PLAYING.store(false, Ordering::Relaxed); diff --git a/crates/oasis-plugin-psp/src/audio.rs b/crates/oasis-plugin-psp/src/audio.rs index 4beba38..52526fc 100644 --- a/crates/oasis-plugin-psp/src/audio.rs +++ b/crates/oasis-plugin-psp/src/audio.rs @@ -1506,6 +1506,47 @@ unsafe fn set_track_name(path: &[u8]) { } } +// --------------------------------------------------------------------------- +// OASIS OS detection +// --------------------------------------------------------------------------- + +/// Check if the OASIS OS EBOOT is the running application. +/// +/// When running alongside OASIS OS, the PRX must NOT use audio codecs +/// because OASIS OS has its own music player (sceMp3) which shares the +/// ME coprocessor's EDRAM with sceAudiocodec. Both cannot be active +/// simultaneously. +/// +/// Detection: scan the first 1MB of user memory for the "OASIS_OS" +/// string embedded in the EBOOT's SceModuleInfo (generated by +/// `psp::module!("OASIS_OS", ...)`). This works from kernel mode +/// without any kernel APIs (which may not be available on all CFW). +unsafe fn is_oasis_running() -> bool { + let needle = b"OASIS_OS\0"; + let start: u32 = 0x0880_0000; + let end: u32 = 0x0898_0000; // First 1.5MB of user memory + let mut addr = start; + while addr <= end - (needle.len() as u32) { + let mut matched = true; + let mut j = 0usize; + while j < needle.len() { + let byte = unsafe { + core::ptr::read_volatile((addr + j as u32) as *const u8) + }; + if byte != needle[j] { + matched = false; + break; + } + j += 1; + } + if matched { + return true; + } + addr += 4; // word-aligned scan + } + false +} + // --------------------------------------------------------------------------- // Audio thread // --------------------------------------------------------------------------- @@ -1514,8 +1555,25 @@ unsafe extern "C" fn audio_thread_entry( _args: usize, _argp: *mut core::ffi::c_void, ) -> i32 { - // Wait for game to initialize before probing for audio modules. - unsafe { psp::sys::sceKernelDelayThread(3_000_000) }; + // Wait for the host application to load before probing. + // OASIS OS EBOOT takes several seconds to boot (progress screens), + // so we check multiple times with increasing delays to catch it. + { + let delays: [u32; 4] = [3_000_000, 3_000_000, 4_000_000, 5_000_000]; + for (i, &delay) in delays.iter().enumerate() { + unsafe { psp::sys::sceKernelDelayThread(delay) }; + if unsafe { is_oasis_running() } { + crate::debug_log( + b"[OASIS] OASIS_OS detected, skipping PRX audio", + ); + return 0; + } + if i < delays.len() - 1 { + crate::debug_log(b"[OASIS] check: OASIS_OS not found, retrying..."); + } + } + crate::debug_log(b"[OASIS] OASIS_OS not detected after 15s, proceeding"); + } if !unsafe { init_audio_drivers() } { crate::debug_log(b"[OASIS] audio init failed"); From 99c38e69ae762d5b5b57fee117b962d9397ec31c Mon Sep 17 00:00:00 2001 From: AI Review Agent Date: Sun, 15 Feb 2026 12:20:48 -0600 Subject: [PATCH 8/9] fix: address AI review feedback (iteration 1) Automated fix by Claude in response to Gemini/Codex review. Iteration: 1/5 Co-Authored-By: AI Review Agent --- crates/oasis-backend-psp/src/audio.rs | 76 +++++++++++++++++---------- crates/oasis-plugin-psp/src/audio.rs | 2 +- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/crates/oasis-backend-psp/src/audio.rs b/crates/oasis-backend-psp/src/audio.rs index 7dc5a0f..a32d4aa 100644 --- a/crates/oasis-backend-psp/src/audio.rs +++ b/crates/oasis-backend-psp/src/audio.rs @@ -44,10 +44,10 @@ fn load_av_modules_once() { // --------------------------------------------------------------------------- /// MPEG version bitrate tables (kbps). Index: bitrate_index (1..14). -const BITRATES_V1_L3: [u32; 15] = - [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]; -const BITRATES_V2_L3: [u32; 15] = - [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]; +const BITRATES_V1_L3: [u32; 15] = [ + 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, +]; +const BITRATES_V2_L3: [u32; 15] = [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]; /// Sample rates by MPEG version. [version_index][srate_index] const SAMPLE_RATES: [[u32; 3]; 4] = [ @@ -82,7 +82,10 @@ fn parse_mp3_header(data: &[u8]) -> Option { let srate_idx = (b2 >> 2) & 0x03; let channel_mode = (b3 >> 6) & 0x03; - if version_bits == 1 || layer_bits == 0 || bitrate_idx == 0 || bitrate_idx == 15 + if version_bits == 1 + || layer_bits == 0 + || bitrate_idx == 0 + || bitrate_idx == 15 || srate_idx == 3 { return None; @@ -104,7 +107,11 @@ fn parse_mp3_header(data: &[u8]) -> Option { } let channels = if channel_mode == 3 { 1 } else { 2 }; - Some(Mp3FrameHeader { sample_rate, bitrate, channels }) + Some(Mp3FrameHeader { + sample_rate, + bitrate, + channels, + }) } // --------------------------------------------------------------------------- @@ -200,21 +207,14 @@ impl AudioPlayer { // Open file. let mut path_bytes: Vec = path.as_bytes().to_vec(); path_bytes.push(0); // null-terminate - let fd = unsafe { - psp::sys::sceIoOpen( - path_bytes.as_ptr(), - psp::sys::IoOpenFlags::RD_ONLY, - 0, - ) - }; + let fd = + unsafe { psp::sys::sceIoOpen(path_bytes.as_ptr(), psp::sys::IoOpenFlags::RD_ONLY, 0) }; if fd < psp::sys::SceUid(0) { return false; } // Get file size. - let size = unsafe { - psp::sys::sceIoLseek(fd, 0, psp::sys::IoWhence::End) - } as usize; + let size = unsafe { psp::sys::sceIoLseek(fd, 0, psp::sys::IoWhence::End) } as usize; unsafe { psp::sys::sceIoLseek(fd, 0, psp::sys::IoWhence::Set) }; if size == 0 { unsafe { psp::sys::sceIoClose(fd) }; @@ -242,10 +242,36 @@ impl AudioPlayer { self.buf_pos = 0; self.file_pos = self.buf_valid; - // Skip ID3v2 tag. + // Skip ID3v2 tag. If the tag is larger than the read buffer + // (common with embedded album art), seek past it in the file + // and re-read from the first audio frame. let id3_skip = skip_id3v2(&self.read_buf[..self.buf_valid]); - if id3_skip > 0 && id3_skip < self.buf_valid { - self.buf_pos = id3_skip; + if id3_skip > 0 { + // Adjust data_size to exclude the tag for duration estimation. + self.data_size = self.data_size.saturating_sub(id3_skip as u32); + if id3_skip < self.buf_valid { + self.buf_pos = id3_skip; + } else { + // Tag exceeds buffer — seek past it in the file. + unsafe { + psp::sys::sceIoLseek(self.fd, id3_skip as i64, psp::sys::IoWhence::Set); + } + self.file_pos = id3_skip; + let re_read = unsafe { + psp::sys::sceIoRead( + self.fd, + self.read_buf.as_mut_ptr() as *mut _, + READ_BUF_SIZE as u32, + ) + }; + if re_read <= 0 { + self.close_file(); + return false; + } + self.buf_valid = re_read as usize; + self.buf_pos = 0; + self.file_pos = id3_skip + self.buf_valid; + } } // Parse first frame header for metadata. @@ -310,9 +336,7 @@ impl AudioPlayer { if fd < psp::sys::SceUid(0) { return false; } - let written = unsafe { - psp::sys::sceIoWrite(fd, data.as_ptr() as *const _, data.len()) - }; + let written = unsafe { psp::sys::sceIoWrite(fd, data.as_ptr() as *const _, data.len()) }; unsafe { psp::sys::sceIoClose(fd) }; // Free the input data immediately — we don't need it anymore. @@ -413,10 +437,7 @@ impl AudioPlayer { let decoder = self.decoder.as_mut().unwrap(); let buf_pos = self.buf_pos; let buf_valid = self.buf_valid; - let result = decoder.decode( - &self.read_buf[buf_pos..buf_valid], - &mut self.pcm_buf, - ); + let result = decoder.decode(&self.read_buf[buf_pos..buf_valid], &mut self.pcm_buf); match result { Ok(consumed) => { @@ -477,8 +498,7 @@ impl AudioPlayer { if self.sample_rate == 0 { return 0; } - (self.frames_decoded as u64 * MP3_FRAME_SAMPLES as u64 * 1000) - / self.sample_rate as u64 + (self.frames_decoded as u64 * MP3_FRAME_SAMPLES as u64 * 1000) / self.sample_rate as u64 } /// Estimated total duration in milliseconds (from bitrate + file size). diff --git a/crates/oasis-plugin-psp/src/audio.rs b/crates/oasis-plugin-psp/src/audio.rs index 52526fc..8b9c47a 100644 --- a/crates/oasis-plugin-psp/src/audio.rs +++ b/crates/oasis-plugin-psp/src/audio.rs @@ -1282,7 +1282,7 @@ unsafe fn init_audio_drivers() -> bool { // Step 2: Wait for the game to load AVCODEC modules during its own // init, then piggyback on them. This avoids sceUtilityLoadModule - // conflicts. Check every 15s for up to 45s (3 attempts) before + // conflicts. Check every 15s for up to 30s (3 attempts) before // falling back to loading modules ourselves. Kept infrequent to // minimise stutter from the NID scan + stub extraction. { From 39e27fa5c71d7d8f8635e2adf8fea684311632cd Mon Sep 17 00:00:00 2001 From: AI Review Agent Date: Sun, 15 Feb 2026 12:28:14 -0600 Subject: [PATCH 9/9] fix: update stale doc comments in PRX audio detection Co-Authored-By: Claude Opus 4.6 --- crates/oasis-plugin-psp/src/audio.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/oasis-plugin-psp/src/audio.rs b/crates/oasis-plugin-psp/src/audio.rs index 8b9c47a..794a18a 100644 --- a/crates/oasis-plugin-psp/src/audio.rs +++ b/crates/oasis-plugin-psp/src/audio.rs @@ -1513,11 +1513,10 @@ unsafe fn set_track_name(path: &[u8]) { /// Check if the OASIS OS EBOOT is the running application. /// /// When running alongside OASIS OS, the PRX must NOT use audio codecs -/// because OASIS OS has its own music player (sceMp3) which shares the -/// ME coprocessor's EDRAM with sceAudiocodec. Both cannot be active -/// simultaneously. +/// because OASIS OS has its own music player (sceAudiocodec) which shares +/// the ME coprocessor's EDRAM. Both cannot be active simultaneously. /// -/// Detection: scan the first 1MB of user memory for the "OASIS_OS" +/// Detection: scan the first 1.5MB of user memory for the "OASIS_OS" /// string embedded in the EBOOT's SceModuleInfo (generated by /// `psp::module!("OASIS_OS", ...)`). This works from kernel mode /// without any kernel APIs (which may not be available on all CFW).