diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index b3b18b7..4bbe2af 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -188,6 +188,16 @@ jobs: permissions: contents: write steps: + - name: Pre-checkout cleanup + run: | + for item in outputs target/psp-std-sysroot psp_output_file.log .git/index.lock; do + if [ -d "$item" ] || [ -f "$item" ]; then + docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \ + "rm -rf /workspace/$item" 2>/dev/null || \ + sudo rm -rf "$item" 2>/dev/null || true + fi + done + - name: Checkout uses: actions/checkout@v4 with: diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 7cfdcbc..58205e0 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -137,6 +137,16 @@ jobs: outputs: status: ${{ steps.review.outputs.status }} steps: + - name: Pre-checkout cleanup + run: | + for item in outputs target/psp-std-sysroot psp_output_file.log .git/index.lock; do + if [ -d "$item" ] || [ -f "$item" ]; then + docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \ + "rm -rf /workspace/$item" 2>/dev/null || \ + sudo rm -rf "$item" 2>/dev/null || true + fi + done + - name: Checkout uses: actions/checkout@v4 with: @@ -224,6 +234,16 @@ jobs: outputs: status: ${{ steps.review.outputs.status }} steps: + - name: Pre-checkout cleanup + run: | + for item in outputs target/psp-std-sysroot psp_output_file.log .git/index.lock; do + if [ -d "$item" ] || [ -f "$item" ]; then + docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \ + "rm -rf /workspace/$item" 2>/dev/null || \ + sudo rm -rf "$item" 2>/dev/null || true + fi + done + - name: Checkout uses: actions/checkout@v4 with: @@ -301,6 +321,16 @@ jobs: GITHUB_TOKEN: ${{ secrets.AGENT_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} steps: + - name: Pre-checkout cleanup + run: | + for item in outputs target/psp-std-sysroot psp_output_file.log .git/index.lock; do + if [ -d "$item" ] || [ -f "$item" ]; then + docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \ + "rm -rf /workspace/$item" 2>/dev/null || \ + sudo rm -rf "$item" 2>/dev/null || true + fi + done + - name: Checkout uses: actions/checkout@v4 with: @@ -380,6 +410,16 @@ jobs: GITHUB_TOKEN: ${{ secrets.AGENT_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} steps: + - name: Pre-checkout cleanup + run: | + for item in outputs target/psp-std-sysroot psp_output_file.log .git/index.lock; do + if [ -d "$item" ] || [ -f "$item" ]; then + docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \ + "rm -rf /workspace/$item" 2>/dev/null || \ + sudo rm -rf "$item" 2>/dev/null || true + fi + done + - name: Checkout uses: actions/checkout@v4 with: diff --git a/Cargo.lock b/Cargo.lock index 320c2cc..8987a48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -468,6 +468,13 @@ dependencies = [ "psp", ] +[[package]] +name = "psp-config-save-example" +version = "0.1.0" +dependencies = [ + "psp", +] + [[package]] name = "psp-cube-example" version = "0.1.0" @@ -512,6 +519,20 @@ dependencies = [ "psp", ] +[[package]] +name = "psp-http-client-example" +version = "0.1.0" +dependencies = [ + "psp", +] + +[[package]] +name = "psp-input-analog-example" +version = "0.1.0" +dependencies = [ + "psp", +] + [[package]] name = "psp-msg-dialog" version = "0.1.0" @@ -519,6 +540,20 @@ dependencies = [ "psp", ] +[[package]] +name = "psp-net-http-example" +version = "0.1.0" +dependencies = [ + "psp", +] + +[[package]] +name = "psp-osk-input-example" +version = "0.1.0" +dependencies = [ + "psp", +] + [[package]] name = "psp-paint-mode" version = "0.1.0" @@ -545,6 +580,20 @@ dependencies = [ "ratatui", ] +[[package]] +name = "psp-rtc-sysinfo-example" +version = "0.1.0" +dependencies = [ + "psp", +] + +[[package]] +name = "psp-savedata-example" +version = "0.1.0" +dependencies = [ + "psp", +] + [[package]] name = "psp-screenshot-example" version = "0.1.0" @@ -552,6 +601,20 @@ dependencies = [ "psp", ] +[[package]] +name = "psp-system-font-example" +version = "0.1.0" +dependencies = [ + "psp", +] + +[[package]] +name = "psp-thread-sync-example" +version = "0.1.0" +dependencies = [ + "psp", +] + [[package]] name = "psp-time-example" version = "0.1.0" @@ -559,6 +622,13 @@ dependencies = [ "psp", ] +[[package]] +name = "psp-timer-alarm-example" +version = "0.1.0" +dependencies = [ + "psp", +] + [[package]] name = "psp-vfpu-addition-example" version = "0.1.0" diff --git a/README.md b/README.md index 6e63807..7d89356 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The upstream project is maintained at a low cadence (3-5 commits/year, mostly ni psp::module!("sample_module", 1, 1); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); psp::dprintln!("Hello PSP from rust!"); } ``` @@ -51,7 +51,88 @@ The `psp` crate provides ~825 syscall bindings covering every major PSP subsyste | **Kernel-only** | `sircs` | 1 | Infrared remote control (SIRCS protocol) | | **Kernel-only** | `codec` | 10 | Hardware video/audio codec control | -### High-Level Utilities +### Platform SDK + +36+ high-level modules providing safe, idiomatic Rust APIs with RAII resource management over PSP syscalls. + +#### System & Lifecycle + +| Module | Key API | Description | +|--------|---------|-------------| +| `psp::callback` | `setup_exit_callback()` | Register exit callback (spawns handler thread) | +| `psp::power` | `get_clock()`, `set_clock()`, `battery_info()` | CPU/bus clock control, battery status, AC detection | +| `psp::display` | `wait_vblank()`, `set_framebuf()` | VBlank sync, framebuffer management | +| `psp::time` | `Instant`, `Duration`, `FrameTimer` | Microsecond timing, frame rate measurement | +| `psp::timer` | `Alarm`, `VTimer` | One-shot alarms (closure-based), virtual timers | +| `psp::dialog` | `message_dialog()`, `confirm_dialog()` | System message/confirmation/error dialogs | +| `psp::system_param` | `language()`, `nickname()`, `timezone_offset()` | System parameter queries (language, date/time format, etc.) | +| `psp::rtc` | `Tick`, `format_rfc3339()`, `day_of_week()` | Extended RTC: tick arithmetic, RFC 3339, UTC/local conversion | + +#### Threading & Sync + +| Module | Key API | Description | +|--------|---------|-------------| +| `psp::thread` | `spawn()`, `JoinHandle`, `sleep_ms()` | Thread creation with closure trampolines, join/sleep | +| `psp::sync` | `SpinMutex`, `SpinRwLock`, `Semaphore`, `EventFlag` | Spinlocks, kernel semaphores, event flags, SPSC queue | + +#### Input + +| Module | Key API | Description | +|--------|---------|-------------| +| `psp::input` | `Controller`, `analog_x_f32()`, `is_pressed()` | Button press/release detection, analog deadzone normalization | +| `psp::osk` | `text_input()`, `OskBuilder` | On-screen keyboard for user text input (UTF-16 handling) | + +#### File I/O & Config + +| Module | Key API | Description | +|--------|---------|-------------| +| `psp::io` | `File`, `ReadDir`, `read_to_vec()`, `write_bytes()` | RAII file handles, directory iteration, convenience I/O | +| `psp::config` | `Config`, `save()`, `load()` | Key-value store with binary RCFG format (bool/i32/f32/str) | +| `psp::savedata` | `Savedata`, `save()`, `load()` | PSP system save/load dialog with auto-save/auto-load modes | + +#### Audio + +| Module | Key API | Description | +|--------|---------|-------------| +| `psp::audio` | `AudioChannel`, `output_blocking()` | RAII audio channels with PCM output | +| `psp::audio_mixer` | `Mixer`, `Channel` | Multi-channel PCM software mixer | +| `psp::mp3` | `Mp3Decoder`, `decode_frame()` | Hardware-accelerated MP3 decoding to PCM samples | + +#### Graphics & Rendering + +| Module | Key API | Description | +|--------|---------|-------------| +| `psp::framebuffer` | `DoubleBuffer`, `LayerCompositor` | Double-buffered framebuffer, dirty-rect tracking | +| `psp::gu_ext` | `setup_2d()`, `SpriteBatch`, `GuStateSnapshot` | 2D rendering helpers, sprite batching, GU state save/restore | +| `psp::simd` | `Vec4`, `Mat4` | VFPU-accelerated vector/matrix math, easing, color ops | +| `psp::image` | `decode_jpeg()`, `decode_bmp()`, `load_image()` | Hardware JPEG decode, BMP 24/32-bit decode, auto-detect | +| `psp::font` | `FontLib`, `Font`, `FontRenderer` | System PGF font loading, VRAM glyph atlas rendering | + +#### Networking + +| Module | Key API | Description | +|--------|---------|-------------| +| `psp::net` | `TcpStream`, `UdpSocket`, `connect_ap()` | WiFi connect, TCP/UDP sockets (RAII), DNS resolution | +| `psp::http` | `HttpClient`, `get()`, `post()`, `RequestBuilder` | HTTP client with RAII template/connection/request lifecycle | +| `psp::wlan` | `status()`, `is_available()` | WLAN module status query | + +#### Hardware & Memory + +| Module | Key API | Description | +|--------|---------|-------------| +| `psp::dma` | `memcpy_dma()`, `vram_blit_dma()` | DMA memory copy and VRAM blitting | +| `psp::cache` | `CachedPtr`, `UncachedPtr` | Cache-aware pointers, dcache flush/invalidate helpers | +| `psp::mem` | `Partition2Alloc`, `Partition3Alloc` | Typed partition memory allocators | +| `psp::usb` | `UsbStorageMode`, `is_connected()` | USB bus control, mass storage mode (RAII) | + +#### Kernel-Only (requires `--features kernel`) + +| Module | Key API | Description | +|--------|---------|-------------| +| `psp::me` | `MeExecutor`, `me_boot()` | Media Engine coprocessor boot/task management | +| `psp::hw` | `hw_read32()`, `hw_write32()`, `Register` | Memory-mapped hardware register I/O | + +#### Standalone Utilities | Module | Description | |--------|-------------| @@ -61,8 +142,6 @@ The `psp` crate provides ~825 syscall bindings covering every major PSP subsyste | `psp::benchmark()` | Cycle-accurate benchmarking via RTC | | `psp::math` | VFPU-accelerated `sinf`/`cosf`, full libm math library | | `psp::vfpu!()` | Inline VFPU (Vector FPU) assembly macros | -| `psp::hw` | Memory-mapped hardware register I/O (kernel mode) | -| `psp::me` | Media Engine coprocessor boot/task management (kernel mode) | | `psp::dprintln!()` | Thread-safe debug printing via `SpinMutex` | ## Features @@ -78,12 +157,12 @@ The `psp` crate provides ~825 syscall bindings covering every major PSP subsyste | Example | APIs Demonstrated | Description | |---------|-------------------|-------------| -| `hello-world` | `dprintln!`, `enable_home_button` | Minimal PSP program | +| `hello-world` | `dprintln!`, `psp::callback` | Minimal PSP program | | `cube` | `sceGu*`, `sceGum*`, VRAM alloc | Rotating 3D cube with lighting | | `rainbow` | `sceGu*`, vertex colors | Animated color gradient | | `gu-background` | `sceGu*`, VRAM alloc | Clear screen with solid color | | `gu-debug-print` | `sceGu*`, debug font | On-screen debug text via GU | -| `clock-speed` | `scePower*` | Read/set CPU and bus clock speeds | +| `clock-speed` | `psp::power` | Read/set CPU and bus clock speeds | | `time` | `sceRtc*` | Read and display real-time clock | | `wlan` | `sceWlan*` | Query WLAN module status | | `msg-dialog` | `sceUtility*` | System message dialog | @@ -95,9 +174,19 @@ The `psp` crate provides ~825 syscall bindings covering every major PSP subsyste | `vfpu-context-switching` | `vfpu!()`, threads | VFPU context save/restore across threads | | `rust-std-hello-world` | `String`, `Vec`, `std` | Standard library on PSP | | `kernel-mode` | `module_kernel!()`, NAND, volatile mem | Kernel-mode APIs (requires CFW) | -| `file-io` | `sceIoOpen/Write/Read/Close` | File write and read-back | +| `file-io` | `psp::io` | File write and read-back | | `screenshot` | `screenshot_bmp()`, `sceIoWrite` | Capture framebuffer to BMP file | -| `audio-tone` | `sceAudioChReserve`, `sceAudioOutputBlocking` | Generate and play a sine wave | +| `audio-tone` | `psp::audio::AudioChannel` | Generate and play a sine wave | +| `config-save` | `psp::config`, `psp::io` | Save and load key-value settings | +| `input-analog` | `psp::input`, `psp::display` | Controller input with analog deadzone | +| `net-http` | `psp::net`, `psp::wlan` | Low-level raw TCP HTTP request | +| `http-client` | `psp::http`, `psp::net` | High-level HTTP GET with HttpClient | +| `savedata` | `psp::savedata`, `sceGu*` | Save and load game data via system dialog | +| `osk-input` | `psp::osk`, `sceGu*` | On-screen keyboard text input | +| `rtc-sysinfo` | `psp::rtc`, `psp::system_param` | RTC date/time and system settings | +| `system-font` | `psp::font`, `psp::gu_ext` | Render text using PSP system fonts | +| `thread-sync` | `psp::thread`, `psp::sync` | Spawn threads sharing a SpinMutex counter | +| `timer-alarm` | `psp::timer` | One-shot alarm and virtual timer | ## Kernel Mode @@ -115,7 +204,7 @@ psp = { git = "https://github.com/AndrewAltimit/rust-psp", features = ["kernel"] psp::module_kernel!("MyKernelApp", 1, 0); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); unsafe { let me_freq = psp::sys::scePowerGetMeClockFrequency(); psp::dprintln!("ME clock: {}MHz", me_freq); @@ -158,7 +247,7 @@ This fork adds experimental `std` support for PSP, allowing use of `String`, `Ve psp::module!("rust_std_hello_world", 1, 1); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); let greeting = String::from("Hello from std!"); psp::dprintln!("{}", greeting); @@ -234,7 +323,7 @@ In your `main.rs` file, set up a basic skeleton: psp::module!("sample_module", 1, 0); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); psp::dprintln!("Hello PSP from rust!"); } ``` @@ -441,7 +530,7 @@ Tagging a commit with `v*` (e.g., `v0.1.0`) triggers a release build: ``` rust-psp/ -+-- psp/ # Core PSP crate (sceGu, sceCtrl, sys bindings, vram_alloc) ++-- psp/ # Core PSP crate (~825 syscall bindings + 30 SDK modules) +-- cargo-psp/ # Build tool: cross-compile + prxgen + pack-pbp -> EBOOT.PBP +-- rust-std-src/ # PSP PAL overlay for std support (merged with rust-src at build time) +-- examples/ # Sample programs (hello-world, cube, gu-background, etc.) diff --git a/cargo-psp/src/main.rs b/cargo-psp/src/main.rs index 47803a2..a14d85d 100644 --- a/cargo-psp/src/main.rs +++ b/cargo-psp/src/main.rs @@ -529,9 +529,15 @@ fn main() -> Result<()> { .arg(build_std_flag) .arg("--target") .arg(&target_arg) - .arg("--message-format=json-render-diagnostics") - .args(args) - .stdout(Stdio::piped()); + .arg("--message-format=json-render-diagnostics"); + + // Newer nightlies (post Jan 2026) destabilized custom JSON target specs + // and require -Zjson-target-spec when using a .json target path. + if build_std { + build_cmd.arg("-Z").arg("json-target-spec"); + } + + build_cmd.args(args).stdout(Stdio::piped()); if build_std { // __CARGO_TESTS_ONLY_SRC_ROOT must point to the workspace root diff --git a/examples/audio-tone/src/main.rs b/examples/audio-tone/src/main.rs index d7752bf..e918466 100644 --- a/examples/audio-tone/src/main.rs +++ b/examples/audio-tone/src/main.rs @@ -2,12 +2,9 @@ #![no_main] use core::f32::consts::PI; -use core::ffi::c_void; -use psp::sys::{ - AUDIO_NEXT_CHANNEL, AUDIO_VOLUME_MAX, AudioFormat, audio_sample_align, sceAudioChRelease, - sceAudioChReserve, sceAudioOutputBlocking, -}; +use psp::audio::{AudioChannel, AudioFormat}; +use psp::sys::AUDIO_VOLUME_MAX; psp::module!("audio_tone_example", 1, 1); @@ -17,31 +14,24 @@ const SAMPLE_COUNT: i32 = 1024; const PLAY_SECONDS: u32 = 3; fn psp_main() { - psp::enable_home_button(); - - // Reserve an audio channel (stereo, 1024 samples per buffer). - let channel = unsafe { - sceAudioChReserve( - AUDIO_NEXT_CHANNEL, - audio_sample_align(SAMPLE_COUNT), - AudioFormat::Stereo, - ) + psp::callback::setup_exit_callback().unwrap(); + + let channel = match AudioChannel::reserve(SAMPLE_COUNT, AudioFormat::Stereo) { + Ok(ch) => ch, + Err(e) => { + psp::dprintln!("Failed to reserve audio channel: {:?}", e); + return; + }, }; - if channel < 0 { - psp::dprintln!("Failed to reserve audio channel: {}", channel); - return; - } - psp::dprintln!( "Playing {}Hz tone for {}s on channel {}", TONE_HZ, PLAY_SECONDS, - channel + channel.channel_id() ); - // Generate and play sine wave buffers. - let aligned_count = audio_sample_align(SAMPLE_COUNT) as usize; + let aligned_count = channel.sample_count() as usize; let mut buf = [0i16; 2048]; // stereo pairs: 1024 * 2 let mut phase: f32 = 0.0; let phase_inc = 2.0 * PI * TONE_HZ / SAMPLE_RATE; @@ -58,15 +48,12 @@ fn psp_main() { } } - unsafe { - sceAudioOutputBlocking( - channel, - AUDIO_VOLUME_MAX as i32, - buf.as_mut_ptr() as *mut c_void, - ); + if let Err(e) = channel.output_blocking(AUDIO_VOLUME_MAX as i32, &buf) { + psp::dprintln!("Audio output error: {:?}", e); + return; } } - unsafe { sceAudioChRelease(channel) }; + // Channel is released on drop psp::dprintln!("Audio playback complete"); } diff --git a/examples/clock-speed/src/main.rs b/examples/clock-speed/src/main.rs index 6b8b8c1..97d9aa1 100644 --- a/examples/clock-speed/src/main.rs +++ b/examples/clock-speed/src/main.rs @@ -4,20 +4,21 @@ psp::module!("sample_clock_speed", 1, 1); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); - unsafe { - let cpu = psp::sys::scePowerGetCpuClockFrequency(); - let bus = psp::sys::scePowerGetBusClockFrequency(); + let clock = psp::power::get_clock(); + psp::dprintln!("PSP is operating at {}/{}MHz", clock.cpu_mhz, clock.bus_mhz); + psp::dprintln!("Setting clock speed to maximum..."); - psp::dprintln!("PSP is operating at {}/{}MHz", cpu, bus); - psp::dprintln!("Setting clock speed to maximum..."); - - psp::sys::scePowerSetClockFrequency(333, 333, 166); - - let cpu = psp::sys::scePowerGetCpuClockFrequency(); - let bus = psp::sys::scePowerGetBusClockFrequency(); - - psp::dprintln!("PSP is now operating at {}/{}MHz", cpu, bus); + match psp::power::set_clock_frequency(333, 166, 333) { + Ok(()) => { + let clock = psp::power::get_clock(); + psp::dprintln!( + "PSP is now operating at {}/{}MHz", + clock.cpu_mhz, + clock.bus_mhz + ); + }, + Err(e) => psp::dprintln!("Failed to set clock: {:?}", e), } } diff --git a/examples/config-save/Cargo.toml b/examples/config-save/Cargo.toml new file mode 100644 index 0000000..527247a --- /dev/null +++ b/examples/config-save/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "psp-config-save-example" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +psp = { path = "../../psp" } diff --git a/examples/config-save/src/main.rs b/examples/config-save/src/main.rs new file mode 100644 index 0000000..37b532f --- /dev/null +++ b/examples/config-save/src/main.rs @@ -0,0 +1,51 @@ +//! Save and load key-value settings using the Config module. + +#![no_std] +#![no_main] + +use psp::config::{Config, ConfigValue}; + +psp::module!("config_save_example", 1, 1); + +fn psp_main() { + psp::callback::setup_exit_callback().unwrap(); + + // Create a config and populate it. + let mut cfg = Config::new(); + cfg.set("fullscreen", ConfigValue::Bool(true)); + cfg.set("volume", ConfigValue::I32(80)); + cfg.set("gamma", ConfigValue::F32(1.2)); + cfg.set("player_name", ConfigValue::Str("PSP_User".into())); + + psp::dprintln!("Created config with {} entries", cfg.len()); + + // Save to file. + let path = "host0:/test_config.rcfg"; + match cfg.save(path) { + Ok(()) => psp::dprintln!("Saved config to {}", path), + Err(e) => { + psp::dprintln!("Failed to save: {:?}", e); + return; + }, + } + + // Load it back. + let loaded = match Config::load(path) { + Ok(c) => c, + Err(e) => { + psp::dprintln!("Failed to load: {:?}", e); + return; + }, + }; + + psp::dprintln!("Loaded {} entries:", loaded.len()); + if let Some(v) = loaded.get_bool("fullscreen") { + psp::dprintln!(" fullscreen = {}", v); + } + if let Some(v) = loaded.get_i32("volume") { + psp::dprintln!(" volume = {}", v); + } + if let Some(v) = loaded.get_str("player_name") { + psp::dprintln!(" player_name = {}", v); + } +} diff --git a/examples/cube/src/main.rs b/examples/cube/src/main.rs index 21ece02..1c38975 100644 --- a/examples/cube/src/main.rs +++ b/examples/cube/src/main.rs @@ -291,7 +291,7 @@ fn psp_main() { } unsafe fn psp_main_inner() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); let allocator = get_vram_allocator().unwrap(); let fbp0 = allocator diff --git a/examples/embedded-graphics/src/main.rs b/examples/embedded-graphics/src/main.rs index 013d41d..80c6b68 100644 --- a/examples/embedded-graphics/src/main.rs +++ b/examples/embedded-graphics/src/main.rs @@ -14,7 +14,7 @@ use psp::embedded_graphics::Framebuffer; psp::module!("sample_emb_gfx", 1, 1); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); let mut disp = Framebuffer::new(); let style = PrimitiveStyleBuilder::new() diff --git a/examples/file-io/src/main.rs b/examples/file-io/src/main.rs index a258565..a947ef4 100644 --- a/examples/file-io/src/main.rs +++ b/examples/file-io/src/main.rs @@ -1,53 +1,27 @@ #![no_std] #![no_main] -use core::ffi::c_void; - -use psp::sys::{self, IoOpenFlags, IoPermissions, SceUid}; - psp::module!("file_io_example", 1, 1); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); - let path = b"host0:/test_output.txt\0"; + let path = "host0:/test_output.txt"; let message = b"Hello from rust-psp file I/O!"; // Write a message to a file. - let fd: SceUid = unsafe { - sys::sceIoOpen( - path.as_ptr(), - IoOpenFlags::WR_ONLY | IoOpenFlags::CREAT | IoOpenFlags::TRUNC, - 0o644 as IoPermissions, - ) - }; - - if fd.0 < 0 { - psp::dprintln!("Failed to open file for writing: {}", fd.0); + if let Err(e) = psp::io::write_bytes(path, message) { + psp::dprintln!("Failed to write file: {:?}", e); return; } - - let written = unsafe { sys::sceIoWrite(fd, message.as_ptr() as *const c_void, message.len()) }; - psp::dprintln!("Wrote {} bytes", written); - unsafe { sys::sceIoClose(fd) }; + psp::dprintln!("Wrote {} bytes", message.len()); // Read the file back. - let fd: SceUid = - unsafe { sys::sceIoOpen(path.as_ptr(), IoOpenFlags::RD_ONLY, 0 as IoPermissions) }; - - if fd.0 < 0 { - psp::dprintln!("Failed to open file for reading: {}", fd.0); - return; - } - - let mut buf = [0u8; 128]; - let read = unsafe { sys::sceIoRead(fd, buf.as_mut_ptr() as *mut c_void, buf.len() as u32) }; - unsafe { sys::sceIoClose(fd) }; - - if read > 0 { - let text = core::str::from_utf8(&buf[..read as usize]).unwrap_or(""); - psp::dprintln!("Read back: {}", text); - } else { - psp::dprintln!("Failed to read file: {}", read); + match psp::io::read_to_vec(path) { + Ok(data) => { + let text = core::str::from_utf8(&data).unwrap_or(""); + psp::dprintln!("Read back: {}", text); + }, + Err(e) => psp::dprintln!("Failed to read file: {:?}", e), } } diff --git a/examples/fontdue-scrolltext/src/main.rs b/examples/fontdue-scrolltext/src/main.rs index 872282f..b1595fb 100644 --- a/examples/fontdue-scrolltext/src/main.rs +++ b/examples/fontdue-scrolltext/src/main.rs @@ -29,7 +29,7 @@ const BUF_WIDTH: usize = 64; const BUF_HEIGHT: usize = 64; fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); // Set up buffers let mut allocator = get_vram_allocator().unwrap(); diff --git a/examples/gu-background/src/main.rs b/examples/gu-background/src/main.rs index 2d12984..687238b 100644 --- a/examples/gu-background/src/main.rs +++ b/examples/gu-background/src/main.rs @@ -13,7 +13,7 @@ psp::module!("sample_gu_background", 1, 1); static mut LIST: psp::Align16<[u32; 0x40000]> = psp::Align16([0; 0x40000]); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); let allocator = get_vram_allocator().unwrap(); let fbp0 = allocator diff --git a/examples/gu-debug-print/src/main.rs b/examples/gu-debug-print/src/main.rs index e328d33..dbdd1a1 100644 --- a/examples/gu-debug-print/src/main.rs +++ b/examples/gu-debug-print/src/main.rs @@ -14,7 +14,7 @@ psp::module!("sample_gu_debug", 1, 1); static mut LIST: psp::Align16<[u32; 0x40000]> = psp::Align16([0; 0x40000]); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); let allocator = get_vram_allocator().unwrap(); let fbp0 = allocator diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs index 3767896..d9f89aa 100644 --- a/examples/hello-world/src/main.rs +++ b/examples/hello-world/src/main.rs @@ -4,6 +4,6 @@ psp::module!("sample_module", 1, 1); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); psp::dprint!("Hello PSP from rust!"); } diff --git a/examples/http-client/Cargo.toml b/examples/http-client/Cargo.toml new file mode 100644 index 0000000..1003c1a --- /dev/null +++ b/examples/http-client/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "psp-http-client-example" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +psp = { path = "../../psp" } diff --git a/examples/http-client/src/main.rs b/examples/http-client/src/main.rs new file mode 100644 index 0000000..4032440 --- /dev/null +++ b/examples/http-client/src/main.rs @@ -0,0 +1,62 @@ +//! High-level HTTP GET using psp::http::HttpClient. +//! +//! Requires a real PSP with WiFi configured in network settings slot 1. +//! Will not work in PPSSPP emulator. + +#![no_std] +#![no_main] + +use psp::http::HttpClient; +use psp::net; + +psp::module!("http_client_example", 1, 1); + +fn psp_main() { + psp::callback::setup_exit_callback().unwrap(); + + // Initialize networking subsystem (256 KiB pool). + if let Err(e) = net::init(256 * 1024) { + psp::dprintln!("net::init failed: {:?}", e); + return; + } + + // Connect to WiFi access point (slot 1). + psp::dprintln!("Connecting to WiFi..."); + if let Err(e) = net::connect_ap(1) { + psp::dprintln!("connect_ap failed: {:?}", e); + net::term(); + return; + } + psp::dprintln!("WiFi connected."); + + // Create an HTTP client (initializes sceHttp subsystem). + let client = match HttpClient::new() { + Ok(c) => c, + Err(e) => { + psp::dprintln!("HttpClient::new failed: {:?}", e); + net::term(); + return; + }, + }; + + // Perform a GET request (URL must be null-terminated). + psp::dprintln!("Fetching http://example.com/ ..."); + match client.get(b"http://example.com/\0") { + Ok(resp) => { + psp::dprintln!("Status: {}", resp.status_code); + if let Some(len) = resp.content_length { + psp::dprintln!("Content-Length: {}", len); + } + // Print first 256 bytes of the body as text. + let preview_len = resp.body.len().min(256); + let text = core::str::from_utf8(&resp.body[..preview_len]).unwrap_or(""); + psp::dprintln!("Body preview:\n{}", text); + }, + Err(e) => psp::dprintln!("GET failed: {:?}", e), + } + + // Client cleans up sceHttp on drop. + drop(client); + net::term(); + psp::dprintln!("Done."); +} diff --git a/examples/input-analog/Cargo.toml b/examples/input-analog/Cargo.toml new file mode 100644 index 0000000..c8b7cc9 --- /dev/null +++ b/examples/input-analog/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "psp-input-analog-example" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +psp = { path = "../../psp" } diff --git a/examples/input-analog/src/main.rs b/examples/input-analog/src/main.rs new file mode 100644 index 0000000..3495ed0 --- /dev/null +++ b/examples/input-analog/src/main.rs @@ -0,0 +1,45 @@ +//! Controller input with analog deadzone normalization. + +#![no_std] +#![no_main] + +use psp::input::{self, Controller}; +use psp::sys::CtrlButtons; + +psp::module!("input_analog_example", 1, 1); + +const DEADZONE: f32 = 0.2; + +fn psp_main() { + psp::callback::setup_exit_callback().unwrap(); + input::enable_analog(); + + let mut ctrl = Controller::new(); + + psp::dprintln!("Move the analog stick or press CROSS. START exits."); + + loop { + ctrl.update(); + + if ctrl.is_pressed(CtrlButtons::START) { + psp::dprintln!("START pressed, exiting."); + break; + } + + if ctrl.is_pressed(CtrlButtons::CROSS) { + psp::dprintln!("CROSS pressed!"); + } + + let x = ctrl.analog_x_f32(DEADZONE); + let y = ctrl.analog_y_f32(DEADZONE); + + if x != 0.0 || y != 0.0 { + // Scale to integer display since PSP debug print has no float formatting + let xi = (x * 100.0) as i32; + let yi = (y * 100.0) as i32; + psp::dprintln!("Analog: x={} y={} (x100)", xi, yi); + } + + psp::display::wait_vblank(); + } +} diff --git a/examples/kernel-mode/src/main.rs b/examples/kernel-mode/src/main.rs index 3a016b8..c75d3fe 100644 --- a/examples/kernel-mode/src/main.rs +++ b/examples/kernel-mode/src/main.rs @@ -10,7 +10,7 @@ psp::module_kernel!("KernelDemo", 1, 0); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); // Demonstrate kernel-only features unsafe { diff --git a/examples/msg-dialog/src/main.rs b/examples/msg-dialog/src/main.rs index dbf6575..465b365 100644 --- a/examples/msg-dialog/src/main.rs +++ b/examples/msg-dialog/src/main.rs @@ -3,9 +3,7 @@ use psp::sys::{ self, DepthFunc, DisplayPixelFormat, FrontFaceDirection, GuContextType, GuState, - GuSyncBehavior, GuSyncMode, ShadingModel, SystemParamLanguage, UtilityDialogButtonAccept, - UtilityDialogCommon, UtilityMsgDialogMode, UtilityMsgDialogOption, UtilityMsgDialogParams, - UtilityMsgDialogPressed, + GuSyncBehavior, GuSyncMode, ShadingModel, }; use core::ffi::c_void; @@ -49,58 +47,17 @@ unsafe fn setup_gu() { } fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); unsafe { setup_gu(); } - let dialog_size = core::mem::size_of::(); - let base = UtilityDialogCommon { - size: dialog_size as u32, - language: SystemParamLanguage::English, - button_accept: UtilityDialogButtonAccept::Cross, // X to accept - graphics_thread: 0x11, // magic number stolen from pspsdk example - access_thread: 0x13, - font_thread: 0x12, - sound_thread: 0x10, - result: 0, - reserved: [0i32; 4], - }; - - let mut msg: [u8; 512] = [0u8; 512]; - msg[..40].copy_from_slice(b"Hello from a Rust-created PSP Msg Dialog"); - - let mut msg_dialog = UtilityMsgDialogParams { - base, - unknown: 0, - mode: UtilityMsgDialogMode::Text, - error_value: 0, - message: msg, - options: UtilityMsgDialogOption::TEXT, - button_pressed: UtilityMsgDialogPressed::Unknown1, - }; - - unsafe { - sys::sceUtilityMsgDialogInitStart(&mut msg_dialog as *mut UtilityMsgDialogParams); + match psp::dialog::message_dialog("Hello from a Rust-created PSP Msg Dialog") { + Ok(result) => psp::dprintln!("Dialog result: {:?}", result), + Err(e) => psp::dprintln!("Dialog error: {:?}", e), } - loop { - let status = unsafe { sys::sceUtilityMsgDialogGetStatus() }; - match status { - 2 => unsafe { sys::sceUtilityMsgDialogUpdate(1) }, - 3 => unsafe { sys::sceUtilityMsgDialogShutdownStart() }, - 0 => break, - _ => (), - } - unsafe { - sys::sceGuStart(GuContextType::Direct, &raw mut LIST as *mut c_void); - sys::sceGuFinish(); - sys::sceGuSync(GuSyncMode::Finish, sys::GuSyncBehavior::Wait); - sys::sceDisplayWaitVblankStart(); - sys::sceGuSwapBuffers(); - } - } unsafe { sys::sceKernelExitGame(); } diff --git a/examples/net-http/Cargo.toml b/examples/net-http/Cargo.toml new file mode 100644 index 0000000..eadb549 --- /dev/null +++ b/examples/net-http/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "psp-net-http-example" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +psp = { path = "../../psp" } diff --git a/examples/net-http/src/main.rs b/examples/net-http/src/main.rs new file mode 100644 index 0000000..0072a59 --- /dev/null +++ b/examples/net-http/src/main.rs @@ -0,0 +1,74 @@ +//! Connect to WiFi and fetch an HTTP response. +//! +//! Requires a real PSP with WiFi configured in network settings slot 1. +//! Will not work in PPSSPP emulator. + +#![no_std] +#![no_main] + +use psp::net::{self, TcpStream}; + +psp::module!("net_http_example", 1, 1); + +fn psp_main() { + psp::callback::setup_exit_callback().unwrap(); + + // Initialize networking subsystem (256 KiB pool). + if let Err(e) = net::init(256 * 1024) { + psp::dprintln!("net::init failed: {:?}", e); + return; + } + + // Connect to WiFi access point (slot 1). + psp::dprintln!("Connecting to WiFi..."); + if let Err(e) = net::connect_ap(1) { + psp::dprintln!("connect_ap failed: {:?}", e); + net::term(); + return; + } + psp::dprintln!("WiFi connected."); + + // Resolve hostname. + let host = b"example.com\0"; + let addr = match net::resolve_hostname(host) { + Ok(a) => a, + Err(e) => { + psp::dprintln!("DNS resolve failed: {:?}", e); + net::term(); + return; + }, + }; + + // TCP connect to port 80. + let stream = match TcpStream::connect(addr, 80) { + Ok(s) => s, + Err(e) => { + psp::dprintln!("TCP connect failed: {:?}", e); + net::term(); + return; + }, + }; + + // Send HTTP GET request. + let request = b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n"; + if let Err(e) = stream.write(request) { + psp::dprintln!("write failed: {:?}", e); + net::term(); + return; + } + + // Read and print response (first 512 bytes). + let mut buf = [0u8; 512]; + match stream.read(&mut buf) { + Ok(n) => { + let text = core::str::from_utf8(&buf[..n]).unwrap_or(""); + psp::dprintln!("Response ({} bytes):\n{}", n, text); + }, + Err(e) => psp::dprintln!("read failed: {:?}", e), + } + + // Stream closed on drop, then terminate networking. + drop(stream); + net::term(); + psp::dprintln!("Done."); +} diff --git a/examples/osk-input/Cargo.toml b/examples/osk-input/Cargo.toml new file mode 100644 index 0000000..b032fb7 --- /dev/null +++ b/examples/osk-input/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "psp-osk-input-example" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +psp = { path = "../../psp" } diff --git a/examples/osk-input/src/main.rs b/examples/osk-input/src/main.rs new file mode 100644 index 0000000..0cb226b --- /dev/null +++ b/examples/osk-input/src/main.rs @@ -0,0 +1,69 @@ +//! On-screen keyboard text input using psp::osk. +//! +//! Demonstrates both the convenience function and the builder pattern. +//! The OSK renders via the GE, so GU must be initialized first. + +#![no_std] +#![no_main] + +use core::ffi::c_void; +use psp::osk::{self, OskBuilder}; +use psp::sys::{ + self, DisplayPixelFormat, FrontFaceDirection, GuContextType, GuState, GuSyncBehavior, + GuSyncMode, ShadingModel, +}; + +psp::module!("osk_input_example", 1, 1); + +static mut LIST: psp::Align16<[u32; 262144]> = psp::Align16([0; 262144]); + +unsafe fn setup_gu() { + sys::sceGuInit(); + sys::sceGuStart(GuContextType::Direct, &raw mut LIST as *mut c_void); + sys::sceGuDrawBuffer(DisplayPixelFormat::Psm8888, core::ptr::null_mut(), 512); + sys::sceGuDispBuffer(480, 272, 0x88000 as *mut c_void, 512); + sys::sceGuDepthBuffer(0x110000 as *mut c_void, 512); + sys::sceGuOffset(2048 - 240, 2048 - 136); + sys::sceGuViewport(2048, 2048, 480, 272); + sys::sceGuScissor(0, 0, 480, 272); + sys::sceGuEnable(GuState::ScissorTest); + sys::sceGuFrontFace(FrontFaceDirection::Clockwise); + sys::sceGuShadeModel(ShadingModel::Smooth); + sys::sceGuEnable(GuState::CullFace); + sys::sceGuFinish(); + sys::sceGuSync(GuSyncMode::Finish, GuSyncBehavior::Wait); + sys::sceDisplayWaitVblankStart(); + sys::sceGuDisplay(true); +} + +fn psp_main() { + psp::callback::setup_exit_callback().unwrap(); + + unsafe { + setup_gu(); + } + + // Simple convenience function: prompt + max chars. + psp::dprintln!("Opening simple text input..."); + match osk::text_input("Enter your name:", 32) { + Ok(Some(text)) => psp::dprintln!("Hello, {}!", text), + Ok(None) => psp::dprintln!("Input cancelled."), + Err(e) => psp::dprintln!("OSK error: {:?}", e), + } + + // Builder pattern for more control. + psp::dprintln!("Opening builder-based input..."); + match OskBuilder::new("What is your favorite color?") + .max_chars(24) + .initial_text("blue") + .show() + { + Ok(Some(text)) => psp::dprintln!("Favorite color: {}", text), + Ok(None) => psp::dprintln!("Input cancelled."), + Err(e) => psp::dprintln!("OSK error: {:?}", e), + } + + unsafe { + sys::sceKernelExitGame(); + } +} diff --git a/examples/paint-mode/src/main.rs b/examples/paint-mode/src/main.rs index 8bd253c..1cab8f1 100644 --- a/examples/paint-mode/src/main.rs +++ b/examples/paint-mode/src/main.rs @@ -15,7 +15,7 @@ use psp::{SCREEN_HEIGHT, SCREEN_WIDTH}; psp::module!("Paint Mode Example", 0, 1); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); let disp = &mut Framebuffer::new(); let mut cur_size = 1; diff --git a/examples/rainbow/src/main.rs b/examples/rainbow/src/main.rs index cfa1e8f..1288a31 100644 --- a/examples/rainbow/src/main.rs +++ b/examples/rainbow/src/main.rs @@ -9,7 +9,7 @@ psp::module!("sample_module", 1, 1); static mut VRAM: *mut u32 = 0x4000_0000 as *mut u32; fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); unsafe { sys::sceDisplaySetMode( sys::DisplayMode::Lcd, diff --git a/examples/ratatui/src/main.rs b/examples/ratatui/src/main.rs index 5850b42..f839214 100644 --- a/examples/ratatui/src/main.rs +++ b/examples/ratatui/src/main.rs @@ -15,7 +15,7 @@ use psp::embedded_graphics::Framebuffer; psp::module!("ratatui_example", 1, 1); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); let mut disp = Framebuffer::new(); let backend = EmbeddedBackend::new(&mut disp, EmbeddedBackendConfig::default()); diff --git a/examples/rtc-sysinfo/Cargo.toml b/examples/rtc-sysinfo/Cargo.toml new file mode 100644 index 0000000..221c8e3 --- /dev/null +++ b/examples/rtc-sysinfo/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "psp-rtc-sysinfo-example" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +psp = { path = "../../psp" } diff --git a/examples/rtc-sysinfo/src/main.rs b/examples/rtc-sysinfo/src/main.rs new file mode 100644 index 0000000..04d473a --- /dev/null +++ b/examples/rtc-sysinfo/src/main.rs @@ -0,0 +1,123 @@ +//! RTC and system parameter queries using psp::rtc and psp::system_param. +//! +//! Demonstrates reading system settings (language, nickname, timezone, +//! date/time format) and using the extended RTC API for tick arithmetic, +//! day-of-week, leap year checks, and RFC 3339 formatting. + +#![no_std] +#![no_main] + +use psp::rtc::{self, Tick}; +use psp::system_param; + +psp::module!("rtc_sysinfo_example", 1, 1); + +const DAY_NAMES: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; + +fn psp_main() { + psp::callback::setup_exit_callback().unwrap(); + + // --- System parameters --- + psp::dprintln!("=== System Parameters ==="); + + match system_param::language() { + Ok(lang) => psp::dprintln!("Language: {:?}", lang), + Err(e) => psp::dprintln!("Language error: {:?}", e), + } + + match system_param::nickname() { + Ok(buf) => { + let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + let name = core::str::from_utf8(&buf[..end]).unwrap_or(""); + psp::dprintln!("Nickname: {}", name); + }, + Err(e) => psp::dprintln!("Nickname error: {:?}", e), + } + + match system_param::timezone_offset() { + Ok(tz) => { + let hours = tz / 60; + let mins = (tz % 60).abs(); + psp::dprintln!("Timezone: UTC{:+}:{:02}", hours, mins); + }, + Err(e) => psp::dprintln!("Timezone error: {:?}", e), + } + + match system_param::date_format() { + Ok(fmt) => psp::dprintln!("Date format: {:?}", fmt), + Err(e) => psp::dprintln!("Date format error: {:?}", e), + } + + match system_param::time_format() { + Ok(fmt) => psp::dprintln!("Time format: {:?}", fmt), + Err(e) => psp::dprintln!("Time format error: {:?}", e), + } + + match system_param::daylight_saving() { + Ok(dst) => psp::dprintln!("Daylight saving: {}", if dst { "on" } else { "off" }), + Err(e) => psp::dprintln!("DST error: {:?}", e), + } + + // --- RTC operations --- + psp::dprintln!("\n=== RTC ==="); + + let now = match Tick::now() { + Ok(t) => t, + Err(e) => { + psp::dprintln!("Tick::now() failed: {:?}", e); + return; + }, + }; + + // Current date/time. + if let Ok(dt) = now.to_datetime() { + psp::dprintln!( + "Now: {:04}-{:02}-{:02} {:02}:{:02}:{:02}", + dt.year(), + dt.month(), + dt.day(), + dt.hour(), + dt.minute(), + dt.second() + ); + + // Day of week. + let dow = rtc::day_of_week(dt.year() as i32, dt.month() as i32, dt.day() as i32); + if (0..7).contains(&dow) { + psp::dprintln!("Day of week: {}", DAY_NAMES[dow as usize]); + } + + // Leap year check. + let year = dt.year() as i32; + psp::dprintln!( + "{} is{}a leap year", + year, + if rtc::is_leap_year(year) { + " " + } else { + " not " + } + ); + } + + // Tick arithmetic: add 3 hours. + if let Ok(later) = now.add_hours(3) { + if let Ok(dt) = later.to_datetime() { + psp::dprintln!( + "+3 hours: {:02}:{:02}:{:02}", + dt.hour(), + dt.minute(), + dt.second() + ); + } + } + + // RFC 3339 local time formatting. + if let Ok(buf) = rtc::format_rfc3339_local(&now) { + let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + let s = core::str::from_utf8(&buf[..end]).unwrap_or(""); + psp::dprintln!("RFC 3339 local: {}", s); + } + + psp::dprintln!("Done."); +} diff --git a/examples/rust-std-hello-world/src/main.rs b/examples/rust-std-hello-world/src/main.rs index 752d769..f218494 100644 --- a/examples/rust-std-hello-world/src/main.rs +++ b/examples/rust-std-hello-world/src/main.rs @@ -4,7 +4,7 @@ psp::module!("rust_std_hello_world", 1, 1); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); let yeet = String::from("Yeeteth! I am inside a String!"); psp::dprintln!("{}", yeet); diff --git a/examples/savedata/Cargo.toml b/examples/savedata/Cargo.toml new file mode 100644 index 0000000..25e2b07 --- /dev/null +++ b/examples/savedata/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "psp-savedata-example" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +psp = { path = "../../psp" } diff --git a/examples/savedata/src/main.rs b/examples/savedata/src/main.rs new file mode 100644 index 0000000..0d7b9ff --- /dev/null +++ b/examples/savedata/src/main.rs @@ -0,0 +1,77 @@ +//! Save and load game data using psp::savedata. +//! +//! The savedata utility renders via the GE, so GU must be initialized +//! before calling save/load. + +#![no_std] +#![no_main] + +use core::ffi::c_void; +use psp::savedata::Savedata; +use psp::sys::{ + self, DisplayPixelFormat, FrontFaceDirection, GuContextType, GuState, GuSyncBehavior, + GuSyncMode, ShadingModel, +}; + +psp::module!("savedata_example", 1, 1); + +static mut LIST: psp::Align16<[u32; 262144]> = psp::Align16([0; 262144]); + +unsafe fn setup_gu() { + sys::sceGuInit(); + sys::sceGuStart(GuContextType::Direct, &raw mut LIST as *mut c_void); + sys::sceGuDrawBuffer(DisplayPixelFormat::Psm8888, core::ptr::null_mut(), 512); + sys::sceGuDispBuffer(480, 272, 0x88000 as *mut c_void, 512); + sys::sceGuDepthBuffer(0x110000 as *mut c_void, 512); + sys::sceGuOffset(2048 - 240, 2048 - 136); + sys::sceGuViewport(2048, 2048, 480, 272); + sys::sceGuScissor(0, 0, 480, 272); + sys::sceGuEnable(GuState::ScissorTest); + sys::sceGuFrontFace(FrontFaceDirection::Clockwise); + sys::sceGuShadeModel(ShadingModel::Smooth); + sys::sceGuEnable(GuState::CullFace); + sys::sceGuFinish(); + sys::sceGuSync(GuSyncMode::Finish, GuSyncBehavior::Wait); + sys::sceDisplayWaitVblankStart(); + sys::sceGuDisplay(true); +} + +fn psp_main() { + psp::callback::setup_exit_callback().unwrap(); + + unsafe { + setup_gu(); + } + + // Save some data. + let save_data = b"Hello from rust-psp savedata!"; + let game_name = b"RUSTPSP00000\0"; + let save_name = b"SAVE0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + + psp::dprintln!("Saving {} bytes...", save_data.len()); + match Savedata::new(game_name) + .title("Rust PSP Save") + .detail("Example save data") + .save(save_name, save_data) + { + Ok(()) => psp::dprintln!("Save successful!"), + Err(e) => { + psp::dprintln!("Save failed: {:?}", e); + return; + }, + } + + // Load it back. + psp::dprintln!("Loading..."); + match Savedata::new(game_name).load(save_name, 1024) { + Ok(data) => { + let text = core::str::from_utf8(&data).unwrap_or(""); + psp::dprintln!("Loaded {} bytes: {}", data.len(), text); + }, + Err(e) => psp::dprintln!("Load failed: {:?}", e), + } + + unsafe { + sys::sceKernelExitGame(); + } +} diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 0b6ffd1..098763c 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -12,7 +12,7 @@ psp::module!("screenshot_example", 1, 1); static mut LIST: psp::Align16<[u32; 0x40000]> = psp::Align16([0; 0x40000]); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); let allocator = get_vram_allocator().unwrap(); let fbp0 = allocator diff --git a/examples/system-font/Cargo.toml b/examples/system-font/Cargo.toml new file mode 100644 index 0000000..6c40b37 --- /dev/null +++ b/examples/system-font/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "psp-system-font-example" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +psp = { path = "../../psp" } diff --git a/examples/system-font/src/main.rs b/examples/system-font/src/main.rs new file mode 100644 index 0000000..81cce3d --- /dev/null +++ b/examples/system-font/src/main.rs @@ -0,0 +1,112 @@ +//! Render text using PSP system fonts via the FontRenderer API. + +#![no_std] +#![no_main] + +use core::ffi::c_void; + +use psp::font::{FontLib, FontRenderer}; +use psp::sys::font::{SceFontFamilyCode, SceFontLanguageCode, SceFontStyleCode}; +use psp::sys::{ + self, ClearBuffer, DisplayPixelFormat, GuContextType, GuState, GuSyncBehavior, GuSyncMode, + TexturePixelFormat, +}; +use psp::vram_alloc::get_vram_allocator; +use psp::{BUF_WIDTH, SCREEN_HEIGHT, SCREEN_WIDTH}; + +psp::module!("system_font_example", 1, 1); + +static mut LIST: psp::Align16<[u32; 0x40000]> = psp::Align16([0; 0x40000]); + +fn psp_main() { + psp::callback::setup_exit_callback().unwrap(); + + // Allocate VRAM for framebuffers and depth. + let allocator = get_vram_allocator().unwrap(); + let fbp0 = allocator + .alloc_texture_pixels(BUF_WIDTH, SCREEN_HEIGHT, TexturePixelFormat::Psm8888) + .unwrap() + .as_mut_ptr_from_zero(); + let fbp1 = allocator + .alloc_texture_pixels(BUF_WIDTH, SCREEN_HEIGHT, TexturePixelFormat::Psm8888) + .unwrap() + .as_mut_ptr_from_zero(); + let zbp = allocator + .alloc_texture_pixels(BUF_WIDTH, SCREEN_HEIGHT, TexturePixelFormat::Psm4444) + .unwrap() + .as_mut_ptr_from_zero(); + + // Allocate 512x512 T8 atlas in VRAM for font glyphs. + let atlas_vram = allocator + .alloc_texture_pixels(512, 512, TexturePixelFormat::PsmT8) + .unwrap() + .as_mut_ptr_direct(); + + // Initialize GU. + unsafe { + sys::sceGuInit(); + sys::sceGuStart(GuContextType::Direct, &raw mut LIST as *mut c_void); + sys::sceGuDrawBuffer(DisplayPixelFormat::Psm8888, fbp0 as _, BUF_WIDTH as i32); + sys::sceGuDispBuffer( + SCREEN_WIDTH as i32, + SCREEN_HEIGHT as i32, + fbp1 as _, + BUF_WIDTH as i32, + ); + sys::sceGuDepthBuffer(zbp as _, BUF_WIDTH as i32); + sys::sceGuOffset(2048 - (SCREEN_WIDTH / 2), 2048 - (SCREEN_HEIGHT / 2)); + sys::sceGuViewport(2048, 2048, SCREEN_WIDTH as i32, SCREEN_HEIGHT as i32); + sys::sceGuDepthRange(65535, 0); + sys::sceGuScissor(0, 0, SCREEN_WIDTH as i32, SCREEN_HEIGHT as i32); + sys::sceGuEnable(GuState::ScissorTest); + sys::sceGuFinish(); + sys::sceGuSync(GuSyncMode::Finish, GuSyncBehavior::Wait); + sys::sceDisplayWaitVblankStart(); + sys::sceGuDisplay(true); + } + + // Open a system font. + let fontlib = match FontLib::new(4) { + Ok(fl) => fl, + Err(e) => { + psp::dprintln!("FontLib::new failed: {:?}", e); + return; + }, + }; + + let font = match fontlib.find_optimum( + SceFontFamilyCode::SansSerif, + SceFontStyleCode::Regular, + SceFontLanguageCode::Latin, + ) { + Ok(f) => f, + Err(e) => { + psp::dprintln!("find_optimum failed: {:?}", e); + return; + }, + }; + + let mut renderer = FontRenderer::new(&font, atlas_vram, 16.0); + + // Render loop. + unsafe { + loop { + sys::sceGuStart(GuContextType::Direct, &raw mut LIST as *mut c_void); + sys::sceGuClearColor(0xff442200); + sys::sceGuClear(ClearBuffer::COLOR_BUFFER_BIT); + + psp::gu_ext::setup_2d(); + + renderer.draw_text(20.0, 30.0, 0xffffffff, "Hello from system fonts!"); + renderer.draw_text(20.0, 60.0, 0xff00ffff, "rust-psp FontRenderer"); + renderer.draw_text(20.0, 90.0, 0xff88ff88, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); + renderer.draw_text(20.0, 120.0, 0xffff8888, "0123456789 !@#$%^&*()"); + renderer.flush(); + + sys::sceGuFinish(); + sys::sceGuSync(GuSyncMode::Finish, GuSyncBehavior::Wait); + sys::sceDisplayWaitVblankStart(); + sys::sceGuSwapBuffers(); + } + } +} diff --git a/examples/thread-sync/Cargo.toml b/examples/thread-sync/Cargo.toml new file mode 100644 index 0000000..2844006 --- /dev/null +++ b/examples/thread-sync/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "psp-thread-sync-example" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +psp = { path = "../../psp" } diff --git a/examples/thread-sync/src/main.rs b/examples/thread-sync/src/main.rs new file mode 100644 index 0000000..4f1a0b3 --- /dev/null +++ b/examples/thread-sync/src/main.rs @@ -0,0 +1,58 @@ +//! Spawn threads sharing a SpinMutex counter. + +#![no_std] +#![no_main] + +use psp::sync::SpinMutex; +use psp::thread; + +psp::module!("thread_sync_example", 1, 1); + +static COUNTER: SpinMutex = SpinMutex::new(0); + +const THREAD_COUNT: usize = 4; +const INCREMENTS: u32 = 100; + +fn psp_main() { + psp::callback::setup_exit_callback().unwrap(); + + psp::dprintln!( + "Spawning {} threads, each incrementing {} times", + THREAD_COUNT, + INCREMENTS + ); + + let mut handles = [const { None }; THREAD_COUNT]; + let names: [&[u8]; THREAD_COUNT] = [b"worker_0\0", b"worker_1\0", b"worker_2\0", b"worker_3\0"]; + + for i in 0..THREAD_COUNT { + match thread::spawn(names[i], || { + for _ in 0..INCREMENTS { + *COUNTER.lock() += 1; + } + 0 + }) { + Ok(h) => handles[i] = Some(h), + Err(e) => { + psp::dprintln!("Failed to spawn thread {}: {:?}", i, e); + return; + }, + } + } + + for (i, slot) in handles.into_iter().enumerate() { + if let Some(h) = slot { + match h.join() { + Ok(code) => psp::dprintln!("Thread {} exited with code {}", i, code), + Err(e) => psp::dprintln!("Thread {} join failed: {:?}", i, e), + } + } + } + + let total = *COUNTER.lock(); + psp::dprintln!( + "Final counter value: {} (expected {})", + total, + THREAD_COUNT as u32 * INCREMENTS + ); +} diff --git a/examples/time/src/main.rs b/examples/time/src/main.rs index 699f83e..3095e1f 100644 --- a/examples/time/src/main.rs +++ b/examples/time/src/main.rs @@ -1,27 +1,20 @@ #![no_main] #![no_std] -use core::mem::MaybeUninit; - psp::module!("sample_time", 1, 1); fn psp_main() { - psp::enable_home_button(); - - unsafe { - let mut tick = 0; - psp::sys::sceRtcGetCurrentTick(&mut tick); - - // Convert the tick to an instance of `ScePspDateTime` - let mut date = MaybeUninit::uninit(); - psp::sys::sceRtcSetTick(date.as_mut_ptr(), &tick); - let date = date.assume_init(); + psp::callback::setup_exit_callback().unwrap(); - psp::dprintln!( - "Current time is {:02}:{:02}:{:02} UTC", - date.hour, - date.minutes, - date.seconds - ); + match psp::time::DateTime::now() { + Ok(now) => { + psp::dprintln!( + "Current time is {:02}:{:02}:{:02}", + now.hour(), + now.minute(), + now.second() + ); + }, + Err(e) => psp::dprintln!("Failed to get time: {:?}", e), } } diff --git a/examples/timer-alarm/Cargo.toml b/examples/timer-alarm/Cargo.toml new file mode 100644 index 0000000..b910718 --- /dev/null +++ b/examples/timer-alarm/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "psp-timer-alarm-example" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +psp = { path = "../../psp" } diff --git a/examples/timer-alarm/src/main.rs b/examples/timer-alarm/src/main.rs new file mode 100644 index 0000000..218cc30 --- /dev/null +++ b/examples/timer-alarm/src/main.rs @@ -0,0 +1,51 @@ +//! One-shot alarm and virtual timer demonstration. + +#![no_std] +#![no_main] + +use psp::thread; +use psp::timer::{Alarm, VTimer}; + +psp::module!("timer_alarm_example", 1, 1); + +fn psp_main() { + psp::callback::setup_exit_callback().unwrap(); + + // One-shot alarm: fires after 2 seconds. + psp::dprintln!("Setting alarm for 2 seconds..."); + let _alarm = match Alarm::after_micros(2_000_000, || { + psp::dprintln!("Alarm fired!"); + }) { + Ok(a) => a, + Err(e) => { + psp::dprintln!("Failed to create alarm: {:?}", e); + return; + }, + }; + + // Virtual timer: start and read elapsed time. + let vtimer = match VTimer::new(b"demo_vtimer\0") { + Ok(v) => v, + Err(e) => { + psp::dprintln!("Failed to create VTimer: {:?}", e); + return; + }, + }; + + if let Err(e) = vtimer.start() { + psp::dprintln!("Failed to start VTimer: {:?}", e); + return; + } + + // Wait 3 seconds so the alarm fires and the timer accumulates. + thread::sleep_ms(3000); + + let elapsed = vtimer.time_us(); + psp::dprintln!("VTimer elapsed: {} us (~3s expected)", elapsed); + + if let Err(e) = vtimer.stop() { + psp::dprintln!("Failed to stop VTimer: {:?}", e); + } + + psp::dprintln!("Timer demo complete."); +} diff --git a/examples/vfpu-addition/src/main.rs b/examples/vfpu-addition/src/main.rs index dab9b53..311c8cb 100644 --- a/examples/vfpu-addition/src/main.rs +++ b/examples/vfpu-addition/src/main.rs @@ -49,7 +49,7 @@ fn vfpu_add(a: i32, b: i32) -> i32 { } fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); psp::dprintln!("Testing VFPU..."); psp::dprintln!("VFPU 123 + 4 = {}", vfpu_add(123, 4)); } diff --git a/examples/vfpu-context-switching/src/main.rs b/examples/vfpu-context-switching/src/main.rs index 3b452f9..82935ad 100644 --- a/examples/vfpu-context-switching/src/main.rs +++ b/examples/vfpu-context-switching/src/main.rs @@ -7,7 +7,7 @@ use psp::sys::vfpu_context::{Context, MatrixSet}; psp::module!("vfpu_context_test", 1, 1); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); psp::dprintln!("Testing VFPU context switcher..."); let mut context = Context::new(); diff --git a/examples/wlan/src/main.rs b/examples/wlan/src/main.rs index 5bdbc8d..c05811c 100644 --- a/examples/wlan/src/main.rs +++ b/examples/wlan/src/main.rs @@ -1,6 +1,6 @@ //! This example only demonstrates functionality regarding the WLAN chip. It is -//! not a networking example. You might want to look into `sceNet*` functions -//! for actual network access. +//! not a networking example. You might want to look into `psp::net` for actual +//! network access. #![no_std] #![no_main] @@ -8,26 +8,20 @@ psp::module!("sample_wlan", 1, 1); fn psp_main() { - psp::enable_home_button(); + psp::callback::setup_exit_callback().unwrap(); - unsafe { - let wlan_power = psp::sys::sceWlanDevIsPowerOn() == 1; - let wlan_switch = psp::sys::sceWlanGetSwitchState() == 1; + let status = psp::wlan::status(); - let mut buf = [0; 8]; - psp::sys::sceWlanGetEtherAddr(&mut buf[0]); - - psp::dprintln!( - "WLAN switch enabled: {}, WLAN active: {}, \ - MAC address: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", - wlan_power, - wlan_switch, - buf[0], - buf[1], - buf[2], - buf[3], - buf[4], - buf[5], - ); - } + psp::dprintln!( + "WLAN switch enabled: {}, WLAN active: {}, \ + MAC address: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + status.power_on, + status.switch_on, + status.mac_address[0], + status.mac_address[1], + status.mac_address[2], + status.mac_address[3], + status.mac_address[4], + status.mac_address[5], + ); } diff --git a/psp/src/audio.rs b/psp/src/audio.rs new file mode 100644 index 0000000..c3ebaaf --- /dev/null +++ b/psp/src/audio.rs @@ -0,0 +1,195 @@ +//! Audio channel management with RAII for the PSP. +//! +//! Provides [`AudioChannel`] for reserving, outputting to, and +//! automatically releasing PSP hardware audio channels. +//! +//! # Example +//! +//! ```ignore +//! use psp::audio::{AudioChannel, AudioFormat}; +//! +//! let ch = AudioChannel::reserve(1024, AudioFormat::Stereo).unwrap(); +//! ch.output_blocking(0x8000, &pcm_buf).unwrap(); +//! // Channel is released on drop. +//! ``` + +use core::ffi::c_void; +use core::marker::PhantomData; + +/// Audio output format. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AudioFormat { + /// Stereo interleaved (L, R, L, R, ...). + Stereo, + /// Mono output. + Mono, +} + +impl AudioFormat { + fn to_sys(self) -> crate::sys::AudioFormat { + match self { + AudioFormat::Stereo => crate::sys::AudioFormat::Stereo, + AudioFormat::Mono => crate::sys::AudioFormat::Mono, + } + } + + /// Number of i16 elements per sample (2 for stereo, 1 for mono). + fn channels(self) -> usize { + match self { + AudioFormat::Stereo => 2, + AudioFormat::Mono => 1, + } + } +} + +/// Error from an audio operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct AudioError(pub i32); + +impl core::fmt::Debug for AudioError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "AudioError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for AudioError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "audio error {:#010x}", self.0 as u32) + } +} + +/// Align a sample count to the PSP hardware requirement (multiple of 64). +pub fn align_sample_count(count: i32) -> i32 { + crate::sys::audio_sample_align(count) +} + +/// An RAII handle to a reserved PSP hardware audio channel. +/// +/// Audio data is output via [`output_blocking`](Self::output_blocking) or +/// [`output_blocking_panning`](Self::output_blocking_panning). The channel +/// is automatically released when dropped. +pub struct AudioChannel { + channel: i32, + sample_count: i32, + format: AudioFormat, + _marker: PhantomData<*const ()>, // !Send + !Sync +} + +impl AudioChannel { + /// Reserve a hardware audio channel. + /// + /// `sample_count` is automatically aligned to a multiple of 64. + /// Pass `AudioFormat::Stereo` or `AudioFormat::Mono`. + /// + /// Returns the channel handle, or an error if no channels are available. + pub fn reserve(sample_count: i32, format: AudioFormat) -> Result { + let aligned = align_sample_count(sample_count); + let ch = unsafe { + crate::sys::sceAudioChReserve(crate::sys::AUDIO_NEXT_CHANNEL, aligned, format.to_sys()) + }; + if ch < 0 { + return Err(AudioError(ch)); + } + Ok(Self { + channel: ch, + sample_count: aligned, + format, + _marker: PhantomData, + }) + } + + /// Output PCM audio data, blocking until the hardware buffer is free. + /// + /// `volume` ranges from 0 to 0x8000 (max). + /// `buf` must contain at least `sample_count * channels` i16 values + /// (stereo: 2 per sample, mono: 1 per sample). + /// + /// Returns [`AudioError`] if `buf` is too short or the hardware call fails. + pub fn output_blocking(&self, volume: i32, buf: &[i16]) -> Result<(), AudioError> { + let required = self.sample_count as usize * self.format.channels(); + if buf.len() < required { + return Err(AudioError(-1)); + } + let ret = unsafe { + crate::sys::sceAudioOutputBlocking(self.channel, volume, buf.as_ptr() as *mut c_void) + }; + if ret < 0 { + Err(AudioError(ret)) + } else { + Ok(()) + } + } + + /// Output PCM audio with separate left/right volume, blocking. + /// + /// `vol_left` and `vol_right` range from 0 to 0x8000. + /// `buf` must contain at least `sample_count * channels` i16 values. + /// + /// Returns [`AudioError`] if `buf` is too short or the hardware call fails. + pub fn output_blocking_panning( + &self, + vol_left: i32, + vol_right: i32, + buf: &[i16], + ) -> Result<(), AudioError> { + let required = self.sample_count as usize * self.format.channels(); + if buf.len() < required { + return Err(AudioError(-1)); + } + let ret = unsafe { + crate::sys::sceAudioOutputPannedBlocking( + self.channel, + vol_left, + vol_right, + buf.as_ptr() as *mut c_void, + ) + }; + if ret < 0 { + Err(AudioError(ret)) + } else { + Ok(()) + } + } + + /// Change the sample count for this channel. + /// + /// The new count is automatically aligned to a multiple of 64. + pub fn set_sample_count(&mut self, count: i32) -> Result<(), AudioError> { + let aligned = align_sample_count(count); + let ret = unsafe { crate::sys::sceAudioSetChannelDataLen(self.channel, aligned) }; + if ret < 0 { + Err(AudioError(ret)) + } else { + self.sample_count = aligned; + Ok(()) + } + } + + /// Get the number of samples remaining to be played. + pub fn remaining_samples(&self) -> i32 { + unsafe { crate::sys::sceAudioGetChannelRestLen(self.channel) } + } + + /// Get the raw channel number. + pub fn channel_id(&self) -> i32 { + self.channel + } + + /// Get the current sample count per buffer. + pub fn sample_count(&self) -> i32 { + self.sample_count + } + + /// Get the audio format (stereo or mono). + pub fn format(&self) -> AudioFormat { + self.format + } +} + +impl Drop for AudioChannel { + fn drop(&mut self) { + unsafe { + crate::sys::sceAudioChRelease(self.channel); + } + } +} diff --git a/psp/src/audio_mixer.rs b/psp/src/audio_mixer.rs new file mode 100644 index 0000000..41570d9 --- /dev/null +++ b/psp/src/audio_mixer.rs @@ -0,0 +1,449 @@ +//! Audio mixing engine for the PSP. +//! +//! Provides a multi-channel PCM audio mixer that can run on the main CPU +//! or (in kernel mode) offload mixing to the Media Engine. The mixer +//! accepts PCM streams from multiple sources, handles volume, panning, +//! and fade in/out, and writes mixed output to the PSP audio hardware. +//! +//! # Architecture +//! +//! The mixer uses a double-buffered approach: +//! 1. The main CPU submits audio data to channels +//! 2. The mixing callback reads all active channels, mixes them, and +//! writes to the output buffer +//! 3. The output buffer is submitted to the PSP audio hardware via +//! `sceAudioOutputBlocking` +//! +//! # Example +//! +//! ```ignore +//! use psp::audio_mixer::{Mixer, ChannelConfig}; +//! +//! let mut mixer = Mixer::new(1024).unwrap(); +//! +//! let ch = mixer.alloc_channel(ChannelConfig { +//! volume_left: 0x6000, +//! volume_right: 0x6000, +//! ..Default::default() +//! }).unwrap(); +//! +//! mixer.submit_samples(ch, &pcm_data); +//! mixer.start(); +//! ``` + +use crate::sync::SpinMutex; +use core::sync::atomic::{AtomicI32, AtomicU32, Ordering}; + +/// Maximum number of mixer channels. +pub const MAX_CHANNELS: usize = 8; + +/// Default sample count per audio output call (must be 64-aligned). +pub const DEFAULT_SAMPLE_COUNT: i32 = 1024; + +/// Channel state. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum ChannelState { + /// Channel is free and can be allocated. + Free = 0, + /// Channel is allocated but has no data queued. + Idle = 1, + /// Channel is actively playing audio. + Playing = 2, + /// Channel is fading out and will become idle when done. + FadingOut = 3, +} + +/// Configuration for a mixer channel. +#[derive(Debug, Clone, Copy)] +pub struct ChannelConfig { + /// Left channel volume (0..=0x8000). + pub volume_left: i32, + /// Right channel volume (0..=0x8000). + pub volume_right: i32, + /// Whether to loop when the buffer runs out. + pub looping: bool, +} + +impl Default for ChannelConfig { + fn default() -> Self { + Self { + volume_left: 0x8000, + volume_right: 0x8000, + looping: false, + } + } +} + +/// A handle to a mixer channel. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChannelHandle(pub u8); + +/// Full-volume value for the fade multiplier. +const FADE_MAX: i32 = 256; + +/// Fixed-point fractional bits for fade arithmetic (16.16). +const FADE_FP_SHIFT: i32 = 16; + +/// Full volume in fixed-point representation (`256 << 16`). +const FADE_MAX_FP: i32 = FADE_MAX << FADE_FP_SHIFT; + +/// Per-channel state stored in the mixer. +struct Channel { + state: ChannelState, + config: ChannelConfig, + /// PCM sample buffer (interleaved stereo i16: L, R, L, R, ...) + buffer: &'static [i16], + /// Current read position in the buffer (in samples, not bytes). + position: usize, + /// Fade volume multiplier in 16.16 fixed-point (0..=FADE_MAX_FP). + fade_level: i32, + /// Fade step per output frame in 16.16 fixed-point (negative = fade out). + fade_step: i32, +} + +impl Channel { + const fn new() -> Self { + Self { + state: ChannelState::Free, + config: ChannelConfig { + volume_left: 0x8000, + volume_right: 0x8000, + looping: false, + }, + buffer: &[], + position: 0, + fade_level: FADE_MAX_FP, + fade_step: 0, + } + } +} + +/// Multi-channel PCM audio mixer. +/// +/// Manages up to [`MAX_CHANNELS`] concurrent audio streams and mixes +/// them into a single stereo output buffer for the PSP audio hardware. +pub struct Mixer { + channels: SpinMutex<[Channel; MAX_CHANNELS]>, + /// Number of samples per output call (64-aligned). + sample_count: i32, + /// Hardware channel ID from sceAudioChReserve. + hw_channel: AtomicI32, + /// Master volume (0..=0x8000). + master_volume: AtomicU32, +} + +// SAFETY: Mixer uses internal synchronization (SpinMutex + atomics). +unsafe impl Sync for Mixer {} +unsafe impl Send for Mixer {} + +/// Error type for mixer operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MixerError { + /// No free channels available. + NoFreeChannels, + /// The specified channel handle is invalid. + InvalidChannel, + /// The PSP audio hardware returned an error. + AudioError(i32), + /// The mixer is already running. + AlreadyRunning, +} + +impl Mixer { + /// Create a new mixer with the given sample count per output call. + /// + /// `sample_count` must be between 64 and 65472, aligned to 64. + /// Use [`DEFAULT_SAMPLE_COUNT`] (1024) for a good balance of + /// latency and efficiency. + pub fn new(sample_count: i32) -> Result { + let sample_count = crate::sys::audio_sample_align(sample_count); + + Ok(Self { + channels: SpinMutex::new([const { Channel::new() }; MAX_CHANNELS]), + sample_count, + hw_channel: AtomicI32::new(-1), + master_volume: AtomicU32::new(0x8000), + }) + } + + /// Allocate a mixer channel with the given configuration. + /// + /// Returns a [`ChannelHandle`] for submitting samples and controlling + /// the channel. + pub fn alloc_channel(&self, config: ChannelConfig) -> Result { + let mut channels = self.channels.lock(); + for (i, ch) in channels.iter_mut().enumerate() { + if ch.state == ChannelState::Free { + ch.state = ChannelState::Idle; + ch.config = config; + ch.buffer = &[]; + ch.position = 0; + ch.fade_level = FADE_MAX_FP; + ch.fade_step = 0; + return Ok(ChannelHandle(i as u8)); + } + } + Err(MixerError::NoFreeChannels) + } + + /// Free a mixer channel. + pub fn free_channel(&self, handle: ChannelHandle) -> Result<(), MixerError> { + let mut channels = self.channels.lock(); + let ch = channels + .get_mut(handle.0 as usize) + .ok_or(MixerError::InvalidChannel)?; + ch.state = ChannelState::Free; + ch.buffer = &[]; + ch.position = 0; + Ok(()) + } + + /// Submit PCM samples to a channel. + /// + /// `samples` must be interleaved stereo i16 data (L, R, L, R, ...). + /// The buffer must live for at least as long as the channel is playing + /// (use `'static` lifetime or ensure it's pinned). + /// + /// # Safety + /// + /// The caller must ensure `samples` remains valid for the lifetime of + /// playback. Passing stack-allocated data will cause use-after-free. + pub unsafe fn submit_samples( + &self, + handle: ChannelHandle, + samples: &'static [i16], + ) -> Result<(), MixerError> { + // Buffer must have an even number of elements (interleaved stereo pairs). + if samples.len() % 2 != 0 { + return Err(MixerError::AudioError(-1)); + } + let mut channels = self.channels.lock(); + let ch = channels + .get_mut(handle.0 as usize) + .ok_or(MixerError::InvalidChannel)?; + if ch.state == ChannelState::Free { + return Err(MixerError::InvalidChannel); + } + ch.buffer = samples; + ch.position = 0; + ch.state = ChannelState::Playing; + Ok(()) + } + + /// Set the volume for a channel. + pub fn set_channel_volume( + &self, + handle: ChannelHandle, + left: i32, + right: i32, + ) -> Result<(), MixerError> { + let mut channels = self.channels.lock(); + let ch = channels + .get_mut(handle.0 as usize) + .ok_or(MixerError::InvalidChannel)?; + ch.config.volume_left = left; + ch.config.volume_right = right; + Ok(()) + } + + /// Start a fade-out on a channel. + /// + /// `frames` is the number of output frames over which to fade. + /// After the fade completes, the channel transitions to `Idle`. + pub fn fade_out(&self, handle: ChannelHandle, frames: u16) -> Result<(), MixerError> { + let mut channels = self.channels.lock(); + let ch = channels + .get_mut(handle.0 as usize) + .ok_or(MixerError::InvalidChannel)?; + if frames == 0 { + ch.fade_level = 0; + ch.state = ChannelState::Idle; + } else { + ch.fade_step = -(FADE_MAX_FP / frames as i32); + ch.state = ChannelState::FadingOut; + } + Ok(()) + } + + /// Start a fade-in on a channel. + pub fn fade_in(&self, handle: ChannelHandle, frames: u16) -> Result<(), MixerError> { + let mut channels = self.channels.lock(); + let ch = channels + .get_mut(handle.0 as usize) + .ok_or(MixerError::InvalidChannel)?; + if frames == 0 { + ch.fade_level = FADE_MAX_FP; + } else { + ch.fade_level = 0; + ch.fade_step = FADE_MAX_FP / frames as i32; + } + Ok(()) + } + + /// Set master volume (0..=0x8000). + pub fn set_master_volume(&self, volume: u32) { + self.master_volume + .store(volume.min(0x8000), Ordering::Relaxed); + } + + /// Get master volume. + pub fn master_volume(&self) -> u32 { + self.master_volume.load(Ordering::Relaxed) + } + + /// Mix all active channels into the output buffer. + /// + /// `output` must have space for `sample_count * 2` i16 values + /// (interleaved stereo). + pub fn mix_into(&self, output: &mut [i16]) { + // Clear the output buffer + for sample in output.iter_mut() { + *sample = 0; + } + + let master_vol = self.master_volume.load(Ordering::Relaxed) as i32; + let mut channels = self.channels.lock(); + + for ch in channels.iter_mut() { + if ch.state != ChannelState::Playing && ch.state != ChannelState::FadingOut { + continue; + } + + if ch.buffer.is_empty() { + ch.state = ChannelState::Idle; + continue; + } + + let vol_l = ch.config.volume_left; + let vol_r = ch.config.volume_right; + let fade = ch.fade_level >> FADE_FP_SHIFT; + + // Mix this channel's samples into the output + let stereo_samples = output.len() / 2; + for i in 0..stereo_samples { + let mut buf_pos = ch.position * 2; // stereo pairs + + if buf_pos + 1 >= ch.buffer.len() { + if ch.config.looping { + ch.position = 0; + buf_pos = 0; + } else { + ch.state = ChannelState::Idle; + break; + } + } + + let src_l = ch.buffer[buf_pos] as i32; + let src_r = ch.buffer[buf_pos + 1] as i32; + + // Apply channel volume, fade, and master volume. + // Use i64 intermediates to prevent overflow when + // src ~ 32000 and vol = 0x8000. + let mixed_l = (src_l as i64 * vol_l as i64 / 0x8000 * fade as i64 / 256 + * master_vol as i64 + / 0x8000) + .clamp(i16::MIN as i64, i16::MAX as i64) as i16; + let mixed_r = (src_r as i64 * vol_r as i64 / 0x8000 * fade as i64 / 256 + * master_vol as i64 + / 0x8000) + .clamp(i16::MIN as i64, i16::MAX as i64) as i16; + + // Saturating add to output + let out_idx = i * 2; + output[out_idx] = output[out_idx].saturating_add(mixed_l); + output[out_idx + 1] = output[out_idx + 1].saturating_add(mixed_r); + + ch.position += 1; + } + + // Update fade + if ch.state == ChannelState::FadingOut { + let new_fade = ch.fade_level + ch.fade_step; + if new_fade <= 0 { + ch.fade_level = 0; + ch.state = ChannelState::Idle; + } else { + ch.fade_level = new_fade; + } + } else if ch.fade_step > 0 { + let new_fade = ch.fade_level + ch.fade_step; + if new_fade >= FADE_MAX_FP { + ch.fade_level = FADE_MAX_FP; + ch.fade_step = 0; + } else { + ch.fade_level = new_fade; + } + } + } + } + + /// Reserve a hardware audio channel. + /// + /// Must be called before [`output_blocking`](Self::output_blocking). + pub fn reserve_hw_channel(&self) -> Result<(), MixerError> { + let ch = unsafe { + crate::sys::sceAudioChReserve( + crate::sys::AUDIO_NEXT_CHANNEL, + self.sample_count, + crate::sys::AudioFormat::Stereo, + ) + }; + if ch < 0 { + return Err(MixerError::AudioError(ch)); + } + self.hw_channel.store(ch, Ordering::Release); + Ok(()) + } + + /// Release the hardware audio channel. + pub fn release_hw_channel(&self) { + let ch = self.hw_channel.swap(-1, Ordering::AcqRel); + if ch >= 0 { + unsafe { + crate::sys::sceAudioChRelease(ch); + } + } + } + + /// Output the given buffer to the audio hardware (blocking). + /// + /// The buffer must contain at least `sample_count * 2` i16 samples + /// (interleaved stereo). Returns [`MixerError::AudioError`] if the + /// buffer is too small. This call blocks until the hardware is + /// ready for the next buffer. + pub fn output_blocking(&self, buffer: &[i16]) -> Result<(), MixerError> { + let required = self.sample_count as usize * 2; // stereo + if buffer.len() < required { + return Err(MixerError::AudioError(-1)); + } + let ch = self.hw_channel.load(Ordering::Acquire); + if ch < 0 { + return Err(MixerError::AudioError(-1)); + } + let ret = unsafe { + crate::sys::sceAudioOutputPannedBlocking( + ch, + 0x8000, // full left + 0x8000, // full right + buffer.as_ptr() as *mut core::ffi::c_void, + ) + }; + if ret < 0 { + Err(MixerError::AudioError(ret)) + } else { + Ok(()) + } + } + + /// Get the configured sample count per output call. + pub fn sample_count(&self) -> i32 { + self.sample_count + } +} + +impl Drop for Mixer { + fn drop(&mut self) { + self.release_hw_channel(); + } +} diff --git a/psp/src/cache.rs b/psp/src/cache.rs new file mode 100644 index 0000000..53f36f1 --- /dev/null +++ b/psp/src/cache.rs @@ -0,0 +1,289 @@ +//! Type-safe cache control for PSP memory. +//! +//! The PSP uses a MIPS R4000 CPU with separate instruction and data caches. +//! When sharing memory between the main CPU and the Media Engine (ME), or +//! when using DMA, the data cache must be explicitly managed. +//! +//! This module provides pointer wrapper types that enforce cache coherency +//! at the type level — passing a cached pointer where an uncached one is +//! required becomes a compile error instead of runtime corruption. +//! +//! # Address Model +//! +//! On the PSP, physical addresses can be accessed through two virtual +//! address windows: +//! +//! - **Cached** (`0x0000_0000..0x3FFF_FFFF`): Normal access through the +//! CPU data cache. Fast for repeated access, but the ME and DMA +//! controller cannot see cached data. +//! +//! - **Uncached** (`0x4000_0000..0x7FFF_FFFF`): Bypasses the CPU data +//! cache. Every read/write goes directly to RAM. Required for all +//! ME-shared memory and DMA source/destination addresses. +//! +//! The conversion between cached and uncached is done by setting or +//! clearing bit 30 of the address. + +use core::ffi::c_void; +use core::marker::PhantomData; + +/// Bitmask to convert a cached address to uncached (set bit 30). +pub const UNCACHED_MASK: u32 = 0x4000_0000; + +// ── CachedPtr ─────────────────────────────────────────────────────── + +/// A pointer to data in the CPU's cached address space. +/// +/// This is a zero-cost wrapper that tags a raw pointer as "cached." +/// To share this data with the ME or DMA, you must explicitly convert +/// it via [`flush_to_uncached`](CachedPtr::flush_to_uncached), which +/// writes back the data cache and returns an [`UncachedPtr`]. +#[derive(Copy, Clone)] +pub struct CachedPtr { + ptr: *mut T, + _marker: PhantomData, +} + +impl CachedPtr { + /// Wrap a raw pointer as a `CachedPtr`. + /// + /// # Safety + /// + /// `ptr` must be in the cached address range (`< 0x4000_0000` or the + /// cached KSEG0 equivalent). The pointer must be valid for the + /// intended access pattern. + pub unsafe fn new(ptr: *mut T) -> Self { + Self { + ptr, + _marker: PhantomData, + } + } + + /// Get the raw cached pointer. + pub fn as_ptr(&self) -> *mut T { + self.ptr + } + + /// Flush the data cache for this region and return an uncached pointer. + /// + /// This writes back all dirty cache lines covering `[ptr, ptr+size)`, + /// then invalidates them so subsequent cached reads will fetch fresh + /// data from RAM. Returns an [`UncachedPtr`] to the same physical + /// memory, accessed through the uncached window. + /// + /// # Safety + /// + /// - The memory region `[ptr, ptr+size)` must be valid. + /// - `size` must cover the full extent of data to be shared. + /// - Caller must ensure the uncached pointer is not used concurrently + /// with cached writes to the same region. + pub unsafe fn flush_to_uncached(&self, size: u32) -> UncachedPtr { + unsafe { + crate::sys::sceKernelDcacheWritebackInvalidateRange(self.ptr as *const c_void, size); + } + UncachedPtr { + ptr: (self.ptr as u32 | UNCACHED_MASK) as *mut T, + _marker: PhantomData, + } + } + + /// Flush the entire data cache and return an uncached pointer. + /// + /// Prefer [`flush_to_uncached`](CachedPtr::flush_to_uncached) with a + /// size for better performance. This is a convenience for when the + /// exact size is unknown. + /// + /// # Safety + /// + /// Same as `flush_to_uncached`. + pub unsafe fn flush_all_to_uncached(&self) -> UncachedPtr { + unsafe { + crate::sys::sceKernelDcacheWritebackInvalidateAll(); + } + UncachedPtr { + ptr: (self.ptr as u32 | UNCACHED_MASK) as *mut T, + _marker: PhantomData, + } + } +} + +impl core::fmt::Debug for CachedPtr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "CachedPtr({:p})", self.ptr) + } +} + +// ── UncachedPtr ───────────────────────────────────────────────────── + +/// A pointer to data in the uncached address space. +/// +/// All ME-shared memory and DMA addresses must use uncached pointers. +/// To read DMA'd data through the cache (for performance), convert back +/// via [`invalidate_to_cached`](UncachedPtr::invalidate_to_cached). +#[derive(Copy, Clone)] +pub struct UncachedPtr { + ptr: *mut T, + _marker: PhantomData, +} + +impl UncachedPtr { + /// Wrap a raw pointer as an `UncachedPtr`. + /// + /// # Safety + /// + /// `ptr` must be in the uncached address range (bit 30 set, i.e. + /// `>= 0x4000_0000`). The pointer must be valid for the intended + /// access pattern. + pub unsafe fn new(ptr: *mut T) -> Self { + Self { + ptr, + _marker: PhantomData, + } + } + + /// Create an `UncachedPtr` from a cached address by setting the + /// uncached bit. Does **not** flush the cache — use this only when + /// you know the cache is already clean or you're writing new data. + /// + /// # Safety + /// + /// The caller must ensure cache coherency is maintained. + pub unsafe fn from_cached_addr(cached_ptr: *mut T) -> Self { + Self { + ptr: (cached_ptr as u32 | UNCACHED_MASK) as *mut T, + _marker: PhantomData, + } + } + + /// Get the raw uncached pointer. + pub fn as_ptr(&self) -> *mut T { + self.ptr + } + + /// Read the value using volatile access (appropriate for uncached memory). + /// + /// # Safety + /// + /// The pointer must be valid and properly aligned. No concurrent + /// writes may be in progress. + pub unsafe fn read_volatile(&self) -> T { + unsafe { core::ptr::read_volatile(self.ptr) } + } + + /// Write a value using volatile access (appropriate for uncached memory). + /// + /// # Safety + /// + /// The pointer must be valid and properly aligned. No concurrent + /// reads/writes may be in progress. + pub unsafe fn write_volatile(&self, val: T) { + unsafe { core::ptr::write_volatile(self.ptr, val) } + } + + /// Invalidate the data cache for this region and return a cached pointer. + /// + /// After DMA or ME has written to this uncached region, call this to + /// invalidate stale cache lines so that subsequent cached reads will + /// fetch the fresh data from RAM. + /// + /// # Safety + /// + /// - The memory region `[ptr, ptr+size)` must be valid. + /// - The DMA/ME write must have completed before calling this. + /// - `size` must cover the full extent of data that was modified. + pub unsafe fn invalidate_to_cached(&self, size: u32) -> CachedPtr { + let cached_addr = (self.ptr as u32 & !UNCACHED_MASK) as *mut T; + unsafe { + crate::sys::sceKernelDcacheInvalidateRange(cached_addr as *const c_void, size); + } + CachedPtr { + ptr: cached_addr, + _marker: PhantomData, + } + } + + /// Get the corresponding cached address without invalidating. + /// + /// # Safety + /// + /// The caller must manually ensure cache coherency. + pub unsafe fn to_cached_addr(&self) -> *mut T { + (self.ptr as u32 & !UNCACHED_MASK) as *mut T + } +} + +impl core::fmt::Debug for UncachedPtr { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "UncachedPtr({:p})", self.ptr) + } +} + +// ── Convenience Functions ─────────────────────────────────────────── + +/// Write back the entire data cache to memory. +/// +/// After this call, all cached writes are visible in RAM and can be +/// seen by the ME or DMA controller. +pub fn dcache_writeback_all() { + unsafe { + crate::sys::sceKernelDcacheWritebackAll(); + } +} + +/// Write back and invalidate the entire data cache. +/// +/// Writes back all dirty lines and then invalidates all cache entries. +/// This is the safest (but slowest) way to ensure cache coherency. +pub fn dcache_writeback_invalidate_all() { + unsafe { + crate::sys::sceKernelDcacheWritebackInvalidateAll(); + } +} + +/// Write back a range of the data cache to memory. +/// +/// # Safety +/// +/// `ptr` and `size` must describe a valid memory region. +pub unsafe fn dcache_writeback_range(ptr: *const c_void, size: u32) { + unsafe { + crate::sys::sceKernelDcacheWritebackRange(ptr, size); + } +} + +/// Write back and invalidate a range of the data cache. +/// +/// # Safety +/// +/// `ptr` and `size` must describe a valid memory region. +pub unsafe fn dcache_writeback_invalidate_range(ptr: *const c_void, size: u32) { + unsafe { + crate::sys::sceKernelDcacheWritebackInvalidateRange(ptr, size); + } +} + +/// Invalidate a range of the data cache (discard cached data). +/// +/// Use this after DMA or ME has written to a memory region to ensure +/// subsequent cached reads see the fresh data. +/// +/// # Safety +/// +/// - `ptr` and `size` must describe a valid memory region. +/// - Any dirty cache lines in this range will be **discarded**, not +/// written back. Ensure no pending cached writes exist in this range. +pub unsafe fn dcache_invalidate_range(ptr: *const c_void, size: u32) { + unsafe { + crate::sys::sceKernelDcacheInvalidateRange(ptr, size); + } +} + +/// Invalidate the entire instruction cache. +/// +/// Required after writing code to memory (e.g., for ME task code +/// placed in ME-accessible memory). +pub fn icache_invalidate_all() { + unsafe { + crate::sys::sceKernelIcacheInvalidateAll(); + } +} diff --git a/psp/src/callback.rs b/psp/src/callback.rs new file mode 100644 index 0000000..2680522 --- /dev/null +++ b/psp/src/callback.rs @@ -0,0 +1,112 @@ +//! System callback management for the PSP. +//! +//! The most common use is handling the Home button: when the user presses +//! Home, the PSP invokes the registered exit callback. Without one, the +//! Home button does nothing. +//! +//! # Example +//! +//! ```ignore +//! fn psp_main() { +//! psp::callback::setup_exit_callback().unwrap(); +//! // ... main loop ... +//! } +//! ``` + +use core::ffi::c_void; +use core::ptr; + +use crate::sys::{ + SceUid, ThreadAttributes, sceKernelCreateCallback, sceKernelRegisterExitCallback, +}; + +/// Error from a callback operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct CallbackError(pub i32); + +impl core::fmt::Debug for CallbackError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "CallbackError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for CallbackError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "callback error {:#010x}", self.0 as u32) + } +} + +/// Set up the standard exit callback. +/// +/// Spawns a background thread that sleeps with callback processing +/// enabled. When the Home button is pressed, `sceKernelExitGame()` +/// is called, cleanly exiting the application. +/// +/// Call this once at the start of your program. Equivalent to the +/// boilerplate found in most PSPSDK examples. +pub fn setup_exit_callback() -> Result<(), CallbackError> { + unsafe extern "C" fn exit_callback(_arg1: i32, _arg2: i32, _arg: *mut c_void) -> i32 { + unsafe { crate::sys::sceKernelExitGame() }; + 0 + } + + unsafe extern "C" fn exit_thread(_args: usize, _argp: *mut c_void) -> i32 { + let cbid = unsafe { + sceKernelCreateCallback(b"exit_callback\0".as_ptr(), exit_callback, ptr::null_mut()) + }; + if cbid.0 >= 0 { + unsafe { sceKernelRegisterExitCallback(cbid) }; + } + unsafe { crate::sys::sceKernelSleepThreadCB() }; + 0 + } + + let thid = unsafe { + crate::sys::sceKernelCreateThread( + b"exit_thread\0".as_ptr(), + exit_thread, + crate::DEFAULT_THREAD_PRIORITY, + 4096, + ThreadAttributes::empty(), + ptr::null_mut(), + ) + }; + + if thid.0 < 0 { + return Err(CallbackError(thid.0)); + } + + let ret = unsafe { crate::sys::sceKernelStartThread(thid, 0, ptr::null_mut()) }; + if ret < 0 { + unsafe { crate::sys::sceKernelDeleteThread(thid) }; + return Err(CallbackError(ret)); + } + + Ok(()) +} + +/// Register a custom exit callback function. +/// +/// The handler is invoked when the user presses the Home button. +/// Unlike [`setup_exit_callback`], this does **not** spawn a callback +/// thread — you must already have a thread sleeping with +/// `sceKernelSleepThreadCB` for the callback to fire. +/// +/// Returns the callback UID on success. +pub fn register_exit_callback( + handler: unsafe extern "C" fn(i32, i32, *mut c_void) -> i32, +) -> Result { + let cbid = unsafe { sceKernelCreateCallback(b"exit_cb\0".as_ptr(), handler, ptr::null_mut()) }; + + if cbid.0 < 0 { + return Err(CallbackError(cbid.0)); + } + + let ret = unsafe { sceKernelRegisterExitCallback(cbid) }; + if ret < 0 { + unsafe { crate::sys::sceKernelDeleteCallback(cbid) }; + return Err(CallbackError(ret)); + } + + Ok(cbid) +} diff --git a/psp/src/config.rs b/psp/src/config.rs new file mode 100644 index 0000000..2055533 --- /dev/null +++ b/psp/src/config.rs @@ -0,0 +1,375 @@ +//! Configuration persistence for the PSP. +//! +//! Stores key-value pairs in a compact binary format and reads/writes +//! them via [`crate::io`]. Suitable for saving game settings or +//! application preferences to the Memory Stick. +//! +//! # Binary Format +//! +//! ```text +//! Magic: b"RCFG" (4 bytes) +//! Version: 1 (u16 LE) +//! Count: N (u16 LE) +//! Entry[N]: +//! key_len: u8 +//! key: [u8; key_len] +//! value_type: u8 (0=Bool, 1=I32, 2=U32, 3=F32, 4=Str, 5=Bytes) +//! value_len: u16 LE +//! value: [u8; value_len] +//! ``` + +use alloc::string::String; +use alloc::vec::Vec; + +const MAGIC: &[u8; 4] = b"RCFG"; +const VERSION: u16 = 1; +const MAX_FILE_SIZE: usize = 64 * 1024; + +/// Error from a config operation. +pub enum ConfigError { + /// I/O error reading or writing the file. + Io(crate::io::IoError), + /// The file has an invalid format or unsupported version. + InvalidFormat, + /// The requested key was not found. + KeyNotFound, + /// The serialized config exceeds the maximum size. + TooLarge, + /// A key exceeds 255 bytes. + KeyTooLong, +} + +impl core::fmt::Debug for ConfigError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Io(e) => write!(f, "ConfigError::Io({e:?})"), + Self::InvalidFormat => write!(f, "ConfigError::InvalidFormat"), + Self::KeyNotFound => write!(f, "ConfigError::KeyNotFound"), + Self::TooLarge => write!(f, "ConfigError::TooLarge"), + Self::KeyTooLong => write!(f, "ConfigError::KeyTooLong"), + } + } +} + +impl core::fmt::Display for ConfigError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Io(e) => write!(f, "config I/O error: {e}"), + Self::InvalidFormat => write!(f, "invalid config format"), + Self::KeyNotFound => write!(f, "config key not found"), + Self::TooLarge => write!(f, "config file too large"), + Self::KeyTooLong => write!(f, "config key too long"), + } + } +} + +impl From for ConfigError { + fn from(e: crate::io::IoError) -> Self { + Self::Io(e) + } +} + +/// A configuration value. +#[derive(Clone)] +pub enum ConfigValue { + Bool(bool), + I32(i32), + U32(u32), + F32(f32), + Str(String), + Bytes(Vec), +} + +impl core::fmt::Debug for ConfigValue { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Bool(v) => write!(f, "Bool({v})"), + Self::I32(v) => write!(f, "I32({v})"), + Self::U32(v) => write!(f, "U32({v})"), + Self::F32(v) => write!(f, "F32({v})"), + Self::Str(v) => write!(f, "Str({v:?})"), + Self::Bytes(v) => write!(f, "Bytes(len={})", v.len()), + } + } +} + +const TYPE_BOOL: u8 = 0; +const TYPE_I32: u8 = 1; +const TYPE_U32: u8 = 2; +const TYPE_F32: u8 = 3; +const TYPE_STR: u8 = 4; +const TYPE_BYTES: u8 = 5; + +/// Key-value configuration store. +pub struct Config { + entries: Vec<(String, ConfigValue)>, +} + +impl Config { + /// Create an empty configuration. + pub fn new() -> Self { + Self { + entries: Vec::new(), + } + } + + /// Load a configuration from a file. + pub fn load(path: &str) -> Result { + let data = crate::io::read_to_vec(path)?; + if data.len() > MAX_FILE_SIZE { + return Err(ConfigError::TooLarge); + } + Self::deserialize(&data) + } + + /// Save the configuration to a file. + pub fn save(&self, path: &str) -> Result<(), ConfigError> { + let data = self.serialize()?; + crate::io::write_bytes(path, &data)?; + Ok(()) + } + + /// Get a value by key. + pub fn get(&self, key: &str) -> Option<&ConfigValue> { + self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v) + } + + /// Set a value for a key. Overwrites if the key already exists. + pub fn set(&mut self, key: &str, value: ConfigValue) { + if let Some(entry) = self.entries.iter_mut().find(|(k, _)| k == key) { + entry.1 = value; + } else { + self.entries.push((String::from(key), value)); + } + } + + /// Remove a key and return its value. + pub fn remove(&mut self, key: &str) -> Option { + let idx = self.entries.iter().position(|(k, _)| k == key)?; + Some(self.entries.remove(idx).1) + } + + /// Get a value as `i32`. + pub fn get_i32(&self, key: &str) -> Option { + match self.get(key)? { + ConfigValue::I32(v) => Some(*v), + _ => None, + } + } + + /// Get a value as `u32`. + pub fn get_u32(&self, key: &str) -> Option { + match self.get(key)? { + ConfigValue::U32(v) => Some(*v), + _ => None, + } + } + + /// Get a value as `f32`. + pub fn get_f32(&self, key: &str) -> Option { + match self.get(key)? { + ConfigValue::F32(v) => Some(*v), + _ => None, + } + } + + /// Get a value as `bool`. + pub fn get_bool(&self, key: &str) -> Option { + match self.get(key)? { + ConfigValue::Bool(v) => Some(*v), + _ => None, + } + } + + /// Get a value as `&str`. + pub fn get_str(&self, key: &str) -> Option<&str> { + match self.get(key)? { + ConfigValue::Str(v) => Some(v.as_str()), + _ => None, + } + } + + /// Iterate over all entries. + pub fn iter(&self) -> impl Iterator { + self.entries.iter().map(|(k, v)| (k.as_str(), v)) + } + + /// Number of entries. + pub fn len(&self) -> usize { + self.entries.len() + } + + /// Whether the config is empty. + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + fn serialize(&self) -> Result, ConfigError> { + if self.entries.len() > u16::MAX as usize { + return Err(ConfigError::TooLarge); + } + + let mut buf = Vec::new(); + buf.extend_from_slice(MAGIC); + buf.extend_from_slice(&VERSION.to_le_bytes()); + buf.extend_from_slice(&(self.entries.len() as u16).to_le_bytes()); + + for (key, value) in &self.entries { + let key_bytes = key.as_bytes(); + if key_bytes.len() > 255 { + return Err(ConfigError::KeyTooLong); + } + buf.push(key_bytes.len() as u8); + buf.extend_from_slice(key_bytes); + + match value { + ConfigValue::Bool(v) => { + buf.push(TYPE_BOOL); + buf.extend_from_slice(&1u16.to_le_bytes()); + buf.push(if *v { 1 } else { 0 }); + }, + ConfigValue::I32(v) => { + buf.push(TYPE_I32); + buf.extend_from_slice(&4u16.to_le_bytes()); + buf.extend_from_slice(&v.to_le_bytes()); + }, + ConfigValue::U32(v) => { + buf.push(TYPE_U32); + buf.extend_from_slice(&4u16.to_le_bytes()); + buf.extend_from_slice(&v.to_le_bytes()); + }, + ConfigValue::F32(v) => { + buf.push(TYPE_F32); + buf.extend_from_slice(&4u16.to_le_bytes()); + buf.extend_from_slice(&v.to_le_bytes()); + }, + ConfigValue::Str(v) => { + let bytes = v.as_bytes(); + if bytes.len() > u16::MAX as usize { + return Err(ConfigError::TooLarge); + } + buf.push(TYPE_STR); + buf.extend_from_slice(&(bytes.len() as u16).to_le_bytes()); + buf.extend_from_slice(bytes); + }, + ConfigValue::Bytes(v) => { + if v.len() > u16::MAX as usize { + return Err(ConfigError::TooLarge); + } + buf.push(TYPE_BYTES); + buf.extend_from_slice(&(v.len() as u16).to_le_bytes()); + buf.extend_from_slice(v); + }, + } + } + + if buf.len() > MAX_FILE_SIZE { + return Err(ConfigError::TooLarge); + } + Ok(buf) + } + + fn deserialize(data: &[u8]) -> Result { + if data.len() < 8 { + return Err(ConfigError::InvalidFormat); + } + if &data[0..4] != MAGIC { + return Err(ConfigError::InvalidFormat); + } + let version = u16::from_le_bytes([data[4], data[5]]); + if version != VERSION { + return Err(ConfigError::InvalidFormat); + } + let count = u16::from_le_bytes([data[6], data[7]]) as usize; + + let mut entries = Vec::with_capacity(count); + let mut pos = 8; + + for _ in 0..count { + if pos >= data.len() { + return Err(ConfigError::InvalidFormat); + } + let key_len = data[pos] as usize; + pos += 1; + if pos + key_len > data.len() { + return Err(ConfigError::InvalidFormat); + } + let key = core::str::from_utf8(&data[pos..pos + key_len]) + .map_err(|_| ConfigError::InvalidFormat)?; + pos += key_len; + + if pos + 3 > data.len() { + return Err(ConfigError::InvalidFormat); + } + let value_type = data[pos]; + pos += 1; + let value_len = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize; + pos += 2; + + if pos + value_len > data.len() { + return Err(ConfigError::InvalidFormat); + } + let value_data = &data[pos..pos + value_len]; + pos += value_len; + + let value = match value_type { + TYPE_BOOL => { + if value_len != 1 { + return Err(ConfigError::InvalidFormat); + } + ConfigValue::Bool(value_data[0] != 0) + }, + TYPE_I32 => { + if value_len != 4 { + return Err(ConfigError::InvalidFormat); + } + ConfigValue::I32(i32::from_le_bytes([ + value_data[0], + value_data[1], + value_data[2], + value_data[3], + ])) + }, + TYPE_U32 => { + if value_len != 4 { + return Err(ConfigError::InvalidFormat); + } + ConfigValue::U32(u32::from_le_bytes([ + value_data[0], + value_data[1], + value_data[2], + value_data[3], + ])) + }, + TYPE_F32 => { + if value_len != 4 { + return Err(ConfigError::InvalidFormat); + } + ConfigValue::F32(f32::from_le_bytes([ + value_data[0], + value_data[1], + value_data[2], + value_data[3], + ])) + }, + TYPE_STR => { + let s = + core::str::from_utf8(value_data).map_err(|_| ConfigError::InvalidFormat)?; + ConfigValue::Str(String::from(s)) + }, + TYPE_BYTES => ConfigValue::Bytes(Vec::from(value_data)), + _ => return Err(ConfigError::InvalidFormat), + }; + + entries.push((String::from(key), value)); + } + + Ok(Self { entries }) + } +} + +impl Default for Config { + fn default() -> Self { + Self::new() + } +} diff --git a/psp/src/debug.rs b/psp/src/debug.rs index 7a1420e..f29b74b 100644 --- a/psp/src/debug.rs +++ b/psp/src/debug.rs @@ -4,10 +4,10 @@ //! //! Thread-safe: access to the character buffer is protected by a spinlock. +use crate::sync::SpinMutex; use crate::sys; -use core::cell::UnsafeCell; use core::fmt; -use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; +use core::sync::atomic::{AtomicPtr, Ordering}; /// Like `println!`, but prints to the PSP screen. #[macro_export] @@ -29,67 +29,6 @@ macro_rules! dprint { }} } -/// A simple spinlock for single-core environments (PSP MIPS R4000). -/// -/// Uses `AtomicBool` with acquire/release ordering. On the single-core PSP -/// this prevents compiler reordering; on multi-core it would provide proper -/// synchronization too. -struct SpinMutex { - locked: AtomicBool, - data: UnsafeCell, -} - -// SAFETY: SpinMutex provides exclusive access via the atomic lock. -// PSP is single-core, so the spinlock prevents re-entrant access from -// interrupt handlers or coroutines that might call dprintln!. -unsafe impl Sync for SpinMutex {} -unsafe impl Send for SpinMutex {} - -impl SpinMutex { - const fn new(val: T) -> Self { - Self { - locked: AtomicBool::new(false), - data: UnsafeCell::new(val), - } - } - - fn lock(&self) -> SpinGuard<'_, T> { - while self - .locked - .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed) - .is_err() - { - core::hint::spin_loop(); - } - SpinGuard { mutex: self } - } -} - -struct SpinGuard<'a, T> { - mutex: &'a SpinMutex, -} - -impl core::ops::Deref for SpinGuard<'_, T> { - type Target = T; - fn deref(&self) -> &T { - // SAFETY: We hold the lock. - unsafe { &*self.mutex.data.get() } - } -} - -impl core::ops::DerefMut for SpinGuard<'_, T> { - fn deref_mut(&mut self) -> &mut T { - // SAFETY: We hold the lock exclusively. - unsafe { &mut *self.mutex.data.get() } - } -} - -impl Drop for SpinGuard<'_, T> { - fn drop(&mut self) { - self.mutex.locked.store(false, Ordering::Release); - } -} - static CHARS: SpinMutex = SpinMutex::new(CharBuffer::new()); /// Update the screen. diff --git a/psp/src/dialog.rs b/psp/src/dialog.rs new file mode 100644 index 0000000..243be31 --- /dev/null +++ b/psp/src/dialog.rs @@ -0,0 +1,206 @@ +//! System dialog wrappers for the PSP. +//! +//! Provides simple blocking functions for the PSP's built-in message +//! dialogs, hiding the Init→Update→GetStatus→Shutdown state machine. +//! +//! # Example +//! +//! ```ignore +//! use psp::dialog; +//! +//! let result = dialog::message_dialog("Hello from Rust!").unwrap(); +//! if result == dialog::DialogResult::Confirm { +//! // User pressed OK +//! } +//! ``` + +use crate::sys::{ + SystemParamLanguage, UtilityDialogButtonAccept, UtilityDialogCommon, UtilityMsgDialogMode, + UtilityMsgDialogOption, UtilityMsgDialogParams, UtilityMsgDialogPressed, +}; + +/// Result of a dialog interaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DialogResult { + /// User confirmed (pressed OK / Yes). + Confirm, + /// User cancelled (pressed No). + Cancel, + /// User closed the dialog (pressed Back). + Closed, +} + +/// Error from a dialog operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct DialogError(pub i32); + +impl core::fmt::Debug for DialogError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "DialogError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for DialogError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "dialog error {:#010x}", self.0 as u32) + } +} + +/// Standard thread priorities for utility dialogs (from PSPSDK convention). +const GRAPHICS_THREAD: i32 = 0x11; +const ACCESS_THREAD: i32 = 0x13; +const FONT_THREAD: i32 = 0x12; +const SOUND_THREAD: i32 = 0x10; + +fn make_common(size: u32) -> UtilityDialogCommon { + UtilityDialogCommon { + size, + language: SystemParamLanguage::English, + button_accept: UtilityDialogButtonAccept::Cross, + graphics_thread: GRAPHICS_THREAD, + access_thread: ACCESS_THREAD, + font_thread: FONT_THREAD, + sound_thread: SOUND_THREAD, + result: 0, + reserved: [0i32; 4], + } +} + +fn make_message_buf(message: &str) -> [u8; 512] { + let mut msg = [0u8; 512]; + let len = message.len().min(511); + msg[..len].copy_from_slice(&message.as_bytes()[..len]); + msg +} + +/// Maximum iterations for dialog polling (~10 seconds at 60 fps). +const MAX_DIALOG_ITERATIONS: u32 = 600; + +fn run_dialog(params: &mut UtilityMsgDialogParams) -> Result { + let ret = + unsafe { crate::sys::sceUtilityMsgDialogInitStart(params as *mut UtilityMsgDialogParams) }; + if ret < 0 { + return Err(DialogError(ret)); + } + + for _ in 0..MAX_DIALOG_ITERATIONS { + let status = unsafe { crate::sys::sceUtilityMsgDialogGetStatus() }; + match status { + 2 => unsafe { crate::sys::sceUtilityMsgDialogUpdate(1) }, + 3 => unsafe { crate::sys::sceUtilityMsgDialogShutdownStart() }, + 0 => break, + _ => {}, + } + unsafe { + crate::sys::sceDisplayWaitVblankStart(); + } + } + + Ok(match params.button_pressed { + UtilityMsgDialogPressed::Yes => DialogResult::Confirm, + UtilityMsgDialogPressed::No => DialogResult::Cancel, + UtilityMsgDialogPressed::Back => DialogResult::Closed, + UtilityMsgDialogPressed::Unknown1 => DialogResult::Confirm, + }) +} + +/// Show a blocking message dialog with an OK button. +pub fn message_dialog(message: &str) -> Result { + let mut params = UtilityMsgDialogParams { + base: make_common(core::mem::size_of::() as u32), + unknown: 0, + mode: UtilityMsgDialogMode::Text, + error_value: 0, + message: make_message_buf(message), + options: UtilityMsgDialogOption::TEXT, + button_pressed: UtilityMsgDialogPressed::Unknown1, + }; + run_dialog(&mut params) +} + +/// Show a blocking Yes/No confirmation dialog. +pub fn confirm_dialog(message: &str) -> Result { + let mut params = UtilityMsgDialogParams { + base: make_common(core::mem::size_of::() as u32), + unknown: 0, + mode: UtilityMsgDialogMode::Text, + error_value: 0, + message: make_message_buf(message), + options: UtilityMsgDialogOption::TEXT | UtilityMsgDialogOption::YES_NO_BUTTONS, + button_pressed: UtilityMsgDialogPressed::Unknown1, + }; + run_dialog(&mut params) +} + +/// Show a blocking error code dialog. +pub fn error_dialog(error_code: u32) -> Result { + let mut params = UtilityMsgDialogParams { + base: make_common(core::mem::size_of::() as u32), + unknown: 0, + mode: UtilityMsgDialogMode::Error, + error_value: error_code, + message: [0u8; 512], + options: UtilityMsgDialogOption::ERROR, + button_pressed: UtilityMsgDialogPressed::Unknown1, + }; + run_dialog(&mut params) +} + +/// Builder for customized message dialogs. +pub struct MessageDialogBuilder { + message: [u8; 512], + mode: UtilityMsgDialogMode, + options: UtilityMsgDialogOption, + language: SystemParamLanguage, + error_value: u32, +} + +impl MessageDialogBuilder { + /// Create a new builder for a text message dialog. + pub fn new(message: &str) -> Self { + Self { + message: make_message_buf(message), + mode: UtilityMsgDialogMode::Text, + options: UtilityMsgDialogOption::TEXT, + language: SystemParamLanguage::English, + error_value: 0, + } + } + + /// Set the dialog language. + pub fn language(mut self, lang: SystemParamLanguage) -> Self { + self.language = lang; + self + } + + /// Enable Yes/No buttons instead of just OK. + pub fn yes_no(mut self) -> Self { + self.options |= UtilityMsgDialogOption::YES_NO_BUTTONS; + self + } + + /// Set dialog to error mode with the given error code. + pub fn error_mode(mut self, code: u32) -> Self { + self.mode = UtilityMsgDialogMode::Error; + self.options = UtilityMsgDialogOption::ERROR; + self.error_value = code; + self + } + + /// Show the dialog and block until the user responds. + pub fn show(self) -> Result { + let mut base = make_common(core::mem::size_of::() as u32); + base.language = self.language; + + let mut params = UtilityMsgDialogParams { + base, + unknown: 0, + mode: self.mode, + error_value: self.error_value, + message: self.message, + options: self.options, + button_pressed: UtilityMsgDialogPressed::Unknown1, + }; + run_dialog(&mut params) + } +} diff --git a/psp/src/display.rs b/psp/src/display.rs new file mode 100644 index 0000000..dc237d4 --- /dev/null +++ b/psp/src/display.rs @@ -0,0 +1,79 @@ +//! Display and vblank synchronization for the PSP. +//! +//! Wraps the common `sceDisplay*` syscalls into ergonomic functions. +//! Every graphics application needs vblank sync — this module removes +//! the need to call raw syscalls directly. + +use core::ffi::c_void; + +use crate::sys::{DisplayPixelFormat, DisplaySetBufSync}; + +/// Information about the current framebuffer configuration. +pub struct FrameBufInfo { + /// Pointer to the top-left pixel of the framebuffer. + pub top_addr: *mut u8, + /// Buffer width in pixels (power of 2, typically 512). + pub buf_width: usize, + /// Pixel format of the framebuffer. + pub pixel_format: DisplayPixelFormat, +} + +/// Wait for the current vblank period to end. +pub fn wait_vblank() { + unsafe { + crate::sys::sceDisplayWaitVblank(); + } +} + +/// Wait for the next vblank period to start. +pub fn wait_vblank_start() { + unsafe { + crate::sys::sceDisplayWaitVblankStart(); + } +} + +/// Get the number of vertical blank pulses since the system started. +pub fn vblank_count() -> u32 { + unsafe { crate::sys::sceDisplayGetVcount() } +} + +/// Check if the display is currently in the vertical blank period. +pub fn is_vblank() -> bool { + unsafe { crate::sys::sceDisplayIsVblank() != 0 } +} + +/// Set the framebuffer displayed on screen. +/// +/// # Safety +/// +/// `buf_ptr` must point to a valid framebuffer in VRAM with the +/// correct format and stride. +pub unsafe fn set_framebuf( + buf_ptr: *const u8, + buf_width: usize, + fmt: DisplayPixelFormat, + sync: DisplaySetBufSync, +) { + unsafe { + crate::sys::sceDisplaySetFrameBuf(buf_ptr, buf_width, fmt, sync); + } +} + +/// Get the current framebuffer configuration. +/// +/// `sync` selects which buffer info to retrieve: the currently +/// displayed buffer ([`Immediate`](DisplaySetBufSync::Immediate)) or the +/// one queued for next frame ([`NextFrame`](DisplaySetBufSync::NextFrame)). +pub fn get_framebuf(sync: DisplaySetBufSync) -> FrameBufInfo { + let mut top_addr: *mut c_void = core::ptr::null_mut(); + let mut buf_width: usize = 0; + let mut pixel_format = DisplayPixelFormat::Psm8888; + unsafe { + crate::sys::sceDisplayGetFrameBuf(&mut top_addr, &mut buf_width, &mut pixel_format, sync); + } + FrameBufInfo { + top_addr: top_addr as *mut u8, + buf_width, + pixel_format, + } +} diff --git a/psp/src/dma.rs b/psp/src/dma.rs new file mode 100644 index 0000000..8b6ae9b --- /dev/null +++ b/psp/src/dma.rs @@ -0,0 +1,127 @@ +//! DMA (Direct Memory Access) transfer abstractions. +//! +//! The PSP's DMA controller can perform memory-to-memory transfers +//! independently of the CPU, freeing it for other work. This module +//! provides a safe, ergonomic API over the raw DMA syscalls. +//! +//! # Features +//! +//! - [`DmaResult`] handle for cache management after transfer +//! - [`memcpy_dma`] for bulk memory copies +//! - [`vram_blit_dma`] for efficient VRAM writes +//! - Automatic cache management on completion +//! +//! # Note +//! +//! The PSP's `sceDmacMemcpy` syscall is **synchronous** — it blocks +//! until the DMA transfer completes. The API returns a [`DmaResult`] +//! handle for post-transfer cache invalidation. + +use core::ffi::c_void; + +/// Error type for DMA operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DmaError { + /// The PSP kernel returned an error code. + KernelError(i32), + /// Invalid parameter (null pointer, zero size, etc.). + InvalidParam, +} + +/// Result of a completed DMA transfer. +/// +/// Since `sceDmacMemcpy` is synchronous, the transfer is already +/// complete when this handle is returned. Use [`invalidate_cache`](Self::invalidate_cache) +/// to invalidate the destination cache region so CPU reads see the +/// DMA'd data through cached access. +pub struct DmaResult { + dst: *mut u8, + size: u32, +} + +impl DmaResult { + /// Invalidate the destination cache region so CPU reads see the + /// DMA'd data. + /// + /// Returns a raw pointer to the destination for convenience. + pub fn invalidate_cache(&self) -> *mut u8 { + unsafe { + crate::sys::sceKernelDcacheInvalidateRange(self.dst as *const c_void, self.size); + } + self.dst + } + + /// Get the destination pointer. + pub fn dst(&self) -> *mut u8 { + self.dst + } + + /// Get the transfer size in bytes. + pub fn size(&self) -> u32 { + self.size + } +} + +/// Perform a DMA memory copy. +/// +/// Copies `len` bytes from `src` to `dst` using the PSP's DMA controller. +/// The call blocks until the transfer completes. +/// +/// The source region's cache is written back before the transfer begins. +/// Call [`DmaResult::invalidate_cache`] on the result to read the +/// destination through cached access. +/// +/// # Safety +/// +/// - `dst` must be valid for `len` bytes of writes. +/// - `src` must be valid for `len` bytes of reads. +/// - The source and destination regions must not overlap. +/// - `len` must be > 0. +pub unsafe fn memcpy_dma(dst: *mut u8, src: *const u8, len: u32) -> Result { + if dst.is_null() || src.is_null() || len == 0 { + return Err(DmaError::InvalidParam); + } + + // Flush source region from cache so DMA reads correct data + unsafe { + crate::sys::sceKernelDcacheWritebackRange(src as *const c_void, len); + } + + // Use the kernel DMA memcpy syscall (synchronous — blocks until done) + let ret = unsafe { crate::sys::sceDmacMemcpy(dst as *mut c_void, src as *const c_void, len) }; + + if ret < 0 { + return Err(DmaError::KernelError(ret)); + } + + Ok(DmaResult { dst, size: len }) +} + +/// DMA blit data into VRAM. +/// +/// Copies `src` data into VRAM at the given byte offset. The VRAM +/// base address is at `0x0400_0000` (uncached: `0x4400_0000`). +/// +/// This is useful for uploading textures, framebuffer updates, or +/// any bulk VRAM write that benefits from DMA rather than CPU loops. +/// +/// # Safety +/// +/// - `src` must be valid for `src.len()` bytes of reads. +/// - `vram_offset + src.len()` must not exceed VRAM size (2 MiB). +pub unsafe fn vram_blit_dma(vram_offset: usize, src: &[u8]) -> Result { + const VRAM_BASE: u32 = 0x0400_0000; + const VRAM_SIZE: usize = 2 * 1024 * 1024; + + if src.is_empty() { + return Err(DmaError::InvalidParam); + } + + if vram_offset + src.len() > VRAM_SIZE { + return Err(DmaError::InvalidParam); + } + + let dst = (VRAM_BASE as usize + vram_offset) as *mut u8; + + unsafe { memcpy_dma(dst, src.as_ptr(), src.len() as u32) } +} diff --git a/psp/src/font.rs b/psp/src/font.rs new file mode 100644 index 0000000..0068554 --- /dev/null +++ b/psp/src/font.rs @@ -0,0 +1,682 @@ +//! Font rendering with VRAM glyph atlas. +//! +//! Three-layer architecture: +//! - [`FontLib`]: Library instance (one per app). RAII. +//! - [`Font`]: Open PGF font handle. RAII. +//! - [`FontRenderer`]: High-level text renderer with glyph atlas caching +//! and sprite-batched drawing via [`crate::gu_ext::SpriteBatch`]. + +use alloc::vec::Vec; +use core::alloc::Layout; +use core::ffi::c_void; + +use crate::sys::{ + SceFontCharInfo, SceFontErrorCode, SceFontFamilyCode, SceFontGlyphImage, SceFontInfo, + SceFontLanguageCode, SceFontNewLibParams, SceFontPixelFormatCode, SceFontStyle, + SceFontStyleCode, sceFontClose, sceFontDoneLib, sceFontFindOptimumFont, + sceFontGetCharGlyphImage, sceFontGetCharInfo, sceFontGetFontInfo, sceFontGetNumFontList, + sceFontNewLib, sceFontOpen, +}; + +/// Error from a font operation. +pub enum FontError { + /// SCE error code from a font syscall. + Sce(i32), + /// Font library error code. + Lib(SceFontErrorCode), + /// Font not found. + NotFound, + /// Font library not initialized. + NotInitialized, +} + +impl core::fmt::Debug for FontError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Sce(e) => write!(f, "FontError::Sce({e:#010x})"), + Self::Lib(e) => write!(f, "FontError::Lib({e:?})"), + Self::NotFound => write!(f, "FontError::NotFound"), + Self::NotInitialized => write!(f, "FontError::NotInitialized"), + } + } +} + +impl core::fmt::Display for FontError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Sce(e) => write!(f, "font error {e:#010x}"), + Self::Lib(e) => write!(f, "font library error {e:?}"), + Self::NotFound => write!(f, "font not found"), + Self::NotInitialized => write!(f, "font library not initialized"), + } + } +} + +// ── Alloc callbacks for SceFontNewLibParams ────────────────────────── + +extern "C" fn font_alloc(_user: *mut c_void, size: usize) -> *mut c_void { + let total = size + 16; + let Ok(layout) = Layout::from_size_align(total, 16) else { + return core::ptr::null_mut(); + }; + let ptr = unsafe { alloc::alloc::alloc(layout) }; + if ptr.is_null() { + return core::ptr::null_mut(); + } + unsafe { (ptr as *mut usize).write(total) }; + unsafe { ptr.add(16) as *mut c_void } +} + +extern "C" fn font_free(_user: *mut c_void, ptr: *mut c_void) { + if ptr.is_null() { + return; + } + let real_ptr = unsafe { (ptr as *mut u8).sub(16) }; + let total = unsafe { (real_ptr as *mut usize).read() }; + if let Ok(layout) = Layout::from_size_align(total, 16) { + unsafe { alloc::alloc::dealloc(real_ptr, layout) }; + } +} + +// ── FontLib ────────────────────────────────────────────────────────── + +/// Font library instance. One per application. +/// +/// Calls `sceFontDoneLib` on drop. +pub struct FontLib { + handle: u32, +} + +impl FontLib { + /// Initialize the font library. + /// + /// `max_fonts` is the maximum number of fonts that can be open simultaneously. + pub fn new(max_fonts: u32) -> Result { + let params = SceFontNewLibParams { + user_data_addr: 0, + num_fonts: max_fonts, + cache_data: 0, + alloc_func: Some(font_alloc), + free_func: Some(font_free), + open_func: None, + close_func: None, + read_func: None, + seek_func: None, + error_func: None, + io_finish_func: None, + }; + + let mut error = SceFontErrorCode::Success; + let handle = unsafe { sceFontNewLib(¶ms, &mut error) }; + + if handle == 0 { + return Err(FontError::Lib(error)); + } + + Ok(Self { handle }) + } + + /// Open a font by index. + pub fn open(&self, index: u32) -> Result { + let mut error = SceFontErrorCode::Success; + let font_handle = unsafe { sceFontOpen(self.handle, index, 0, &mut error) }; + if font_handle == 0 { + return Err(FontError::Lib(error)); + } + Ok(Font { + handle: font_handle, + _lib_handle: self.handle, + }) + } + + /// Find and open the best matching font. + pub fn find_optimum( + &self, + family: SceFontFamilyCode, + style: SceFontStyleCode, + language: SceFontLanguageCode, + ) -> Result { + let mut font_style: SceFontStyle = unsafe { core::mem::zeroed() }; + font_style.font_family = family; + font_style.font_style = style; + font_style.font_language = language; + // Default resolution. + font_style.font_h = 0.0; + font_style.font_v = 0.0; + font_style.font_h_res = 128.0; + font_style.font_v_res = 128.0; + + let mut error = SceFontErrorCode::Success; + let index = unsafe { sceFontFindOptimumFont(self.handle, &font_style, &mut error) }; + if index < 0 { + return Err(FontError::NotFound); + } + + self.open(index as u32) + } + + /// Get the number of fonts available in the library. + pub fn font_count(&self) -> Result { + let mut error = SceFontErrorCode::Success; + let count = unsafe { sceFontGetNumFontList(self.handle, &mut error) }; + if count < 0 { + Err(FontError::Sce(count)) + } else { + Ok(count) + } + } +} + +impl Drop for FontLib { + fn drop(&mut self) { + unsafe { sceFontDoneLib(self.handle) }; + } +} + +// ── Font ───────────────────────────────────────────────────────────── + +/// An open PGF font handle. +/// +/// Calls `sceFontClose` on drop. +pub struct Font { + handle: u32, + _lib_handle: u32, +} + +impl Font { + /// Get character metrics without rendering. + pub fn char_info(&self, c: char) -> Result { + let mut info: SceFontCharInfo = unsafe { core::mem::zeroed() }; + let ret = unsafe { sceFontGetCharInfo(self.handle, c as u32, &mut info) }; + if ret < 0 { + return Err(FontError::Sce(ret)); + } + Ok(GlyphMetrics { + width: info.bitmap_width, + height: info.bitmap_height, + bearing_x: sfp26_to_f32(info.sfp26_bearing_hx), + bearing_y: sfp26_to_f32(info.sfp26_bearing_hy), + advance_x: sfp26_to_f32(info.sfp26_advance_h), + advance_y: sfp26_to_f32(info.sfp26_advance_v), + }) + } + + /// Get font-level information. + pub fn info(&self) -> Result { + let mut info: SceFontInfo = unsafe { core::mem::zeroed() }; + let ret = unsafe { sceFontGetFontInfo(self.handle, &mut info) }; + if ret < 0 { + Err(FontError::Sce(ret)) + } else { + Ok(info) + } + } + + /// Render a glyph into a buffer in Format8 (8-bit alpha). + /// + /// `buf` must be at least `buf_width * buf_height` bytes. + /// Returns the glyph metrics on success. + pub fn render_glyph( + &self, + c: char, + buf: &mut [u8], + buf_width: u16, + buf_height: u16, + ) -> Result { + let metrics = self.char_info(c)?; + + if metrics.width == 0 || metrics.height == 0 { + return Ok(metrics); + } + + // Clear the target region. + for b in buf + .iter_mut() + .take((buf_width as usize) * (buf_height as usize)) + { + *b = 0; + } + + let mut glyph_image = SceFontGlyphImage { + pixel_format: SceFontPixelFormatCode::Format8, + x_pos_64: 0, + y_pos_64: 0, + buf_width, + buf_height, + bytes_per_line: buf_width, + pad: 0, + buffer_ptr: buf.as_mut_ptr() as u32, + }; + + let ret = unsafe { sceFontGetCharGlyphImage(self.handle, c as u32, &mut glyph_image) }; + if ret < 0 { + return Err(FontError::Sce(ret)); + } + + Ok(metrics) + } + + /// Get the raw font handle for direct syscall use. + pub fn handle(&self) -> u32 { + self.handle + } +} + +impl Drop for Font { + fn drop(&mut self) { + unsafe { sceFontClose(self.handle) }; + } +} + +// ── GlyphMetrics ───────────────────────────────────────────────────── + +/// Metrics for a single glyph. +#[derive(Debug, Clone, Copy, Default)] +pub struct GlyphMetrics { + pub width: u32, + pub height: u32, + pub bearing_x: f32, + pub bearing_y: f32, + pub advance_x: f32, + pub advance_y: f32, +} + +/// Convert a 26.6 fixed-point value to f32. +fn sfp26_to_f32(v: i32) -> f32 { + v as f32 / 64.0 +} + +// ── Glyph Atlas ────────────────────────────────────────────────────── + +struct AtlasRow { + y: u32, + height: u32, + x_cursor: u32, + lru_stamp: u32, +} + +struct CachedGlyph { + char_code: u32, + atlas_x: u32, + atlas_y: u32, + atlas_w: u32, + atlas_h: u32, + metrics: GlyphMetrics, + row_idx: usize, +} + +struct GlyphAtlas { + vram_ptr: *mut u8, + width: u32, + height: u32, + rows: Vec, + cache: Vec, + lru_counter: u32, + y_cursor: u32, +} + +impl GlyphAtlas { + fn new(vram_ptr: *mut u8, width: u32, height: u32) -> Self { + Self { + vram_ptr, + width, + height, + rows: Vec::new(), + cache: Vec::new(), + lru_counter: 0, + y_cursor: 0, + } + } + + fn find_cached(&mut self, char_code: u32) -> Option<&CachedGlyph> { + let stamp = self.lru_counter; + for entry in &mut self.cache { + if entry.char_code == char_code { + // Update LRU stamp on the row. + if let Some(row) = self.rows.get_mut(entry.row_idx) { + row.lru_stamp = stamp; + } + // Return a reference — we need to reborrow from self.cache. + break; + } + } + // Re-search to satisfy borrow checker. + self.cache.iter().find(|e| e.char_code == char_code) + } + + fn insert( + &mut self, + char_code: u32, + glyph_w: u32, + glyph_h: u32, + metrics: GlyphMetrics, + staging: &[u8], + staging_width: u32, + ) -> Option<&CachedGlyph> { + self.lru_counter += 1; + let stamp = self.lru_counter; + + // Try to fit in an existing row. + let mut fit_row = None; + for (i, row) in self.rows.iter().enumerate() { + if row.height >= glyph_h && row.x_cursor + glyph_w <= self.width { + fit_row = Some(i); + break; + } + } + + // No existing row fits — try to add a new row. + if fit_row.is_none() { + if self.y_cursor + glyph_h <= self.height { + let idx = self.rows.len(); + self.rows.push(AtlasRow { + y: self.y_cursor, + height: glyph_h, + x_cursor: 0, + lru_stamp: stamp, + }); + self.y_cursor += glyph_h; + fit_row = Some(idx); + } + } + + // Still no room — evict the LRU row that can fit the glyph height. + if fit_row.is_none() { + if let Some((evict_idx, _)) = self + .rows + .iter() + .enumerate() + .filter(|(_, r)| r.height >= glyph_h) + .min_by_key(|(_, r)| r.lru_stamp) + { + // Remove all cached glyphs in this row. + self.cache.retain(|g| g.row_idx != evict_idx); + let row = &mut self.rows[evict_idx]; + row.x_cursor = 0; + // Keep the original row height to avoid overwriting adjacent rows. + row.lru_stamp = stamp; + fit_row = Some(evict_idx); + } + } + + let row_idx = fit_row?; + let row = &mut self.rows[row_idx]; + let atlas_x = row.x_cursor; + let atlas_y = row.y; + row.x_cursor += glyph_w; + row.lru_stamp = stamp; + + // Copy staging buffer to VRAM atlas. + for sy in 0..glyph_h { + let src_off = (sy * staging_width) as usize; + let dst_off = ((atlas_y + sy) * self.width + atlas_x) as usize; + let len = glyph_w as usize; + if src_off + len <= staging.len() { + unsafe { + core::ptr::copy_nonoverlapping( + staging.as_ptr().add(src_off), + self.vram_ptr.add(dst_off), + len, + ); + } + } + } + + self.cache.push(CachedGlyph { + char_code, + atlas_x, + atlas_y, + atlas_w: glyph_w, + atlas_h: glyph_h, + metrics, + row_idx, + }); + + self.cache.last() + } + + fn clear(&mut self) { + self.rows.clear(); + self.cache.clear(); + self.y_cursor = 0; + self.lru_counter = 0; + } +} + +// ── FontRenderer ───────────────────────────────────────────────────── + +/// High-level text renderer with VRAM glyph atlas and sprite batching. +/// +/// Renders glyphs to a PsmT8 atlas in VRAM on cache miss, then draws +/// them as textured sprites via [`crate::gu_ext::SpriteBatch`]. +pub struct FontRenderer<'a> { + font: &'a Font, + atlas: GlyphAtlas, + batch: crate::gu_ext::SpriteBatch, + font_size: f32, + staging: Vec, +} + +/// CLUT for PsmT8: maps index i to RGBA(0xFF, 0xFF, 0xFF, i). +/// 256 entries × 4 bytes = 1024 bytes. Must be 16-byte aligned. +#[repr(align(16))] +struct ClutTable([u32; 256]); + +static ALPHA_CLUT: ClutTable = { + let mut table = [0u32; 256]; + let mut i = 0u32; + while i < 256 { + // ABGR format: 0xAABBGGRR where AA=i, BB=FF, GG=FF, RR=FF + table[i as usize] = (i << 24) | 0x00FFFFFF; + i += 1; + } + ClutTable(table) +}; + +const ATLAS_WIDTH: u32 = 512; +const ATLAS_HEIGHT: u32 = 512; +const MAX_STAGING_SIZE: usize = 128 * 128; // Largest single glyph staging buffer. + +impl<'a> FontRenderer<'a> { + /// Create a font renderer. + /// + /// `atlas_vram` must point to at least `512 * 512` bytes of VRAM + /// (allocated via `vram_alloc`). `font_size` is used for scaling + /// (currently informational — PSP system fonts have fixed pixel sizes). + pub fn new(font: &'a Font, atlas_vram: *mut u8, font_size: f32) -> Self { + Self { + font, + atlas: GlyphAtlas::new(atlas_vram, ATLAS_WIDTH, ATLAS_HEIGHT), + batch: crate::gu_ext::SpriteBatch::new(256), + font_size, + staging: alloc::vec![0u8; MAX_STAGING_SIZE], + } + } + + /// Queue text for drawing at `(x, y)` with the given color (ABGR). + /// + /// Renders glyphs to the atlas on cache miss. Characters that fail + /// to render are silently skipped. + pub fn draw_text(&mut self, x: f32, y: f32, color: u32, text: &str) { + let mut cursor_x = x; + + for c in text.chars() { + if c == ' ' { + // Use advance of space character or fallback. + if let Ok(metrics) = self.font.char_info(c) { + cursor_x += metrics.advance_x; + } else { + cursor_x += self.font_size * 0.5; + } + continue; + } + + let char_code = c as u32; + + // Check cache first. + if let Some(cached) = self.atlas.find_cached(char_code) { + let gx = cursor_x + cached.metrics.bearing_x; + let gy = y - cached.metrics.bearing_y; + let u0 = cached.atlas_x as f32; + let v0 = cached.atlas_y as f32; + let u1 = (cached.atlas_x + cached.atlas_w) as f32; + let v1 = (cached.atlas_y + cached.atlas_h) as f32; + self.batch.draw_rect( + gx, + gy, + cached.atlas_w as f32, + cached.atlas_h as f32, + u0, + v0, + u1, + v1, + color, + ); + cursor_x += cached.metrics.advance_x; + continue; + } + + // Cache miss — render glyph. + let Ok(metrics) = self.font.char_info(c) else { + continue; + }; + + if metrics.width == 0 || metrics.height == 0 { + cursor_x += metrics.advance_x; + continue; + } + + let gw = metrics.width; + let gh = metrics.height; + let staging_size = (gw * gh) as usize; + if staging_size > self.staging.len() { + self.staging.resize(staging_size, 0); + } + + // Clear staging buffer. + for b in self.staging[..staging_size].iter_mut() { + *b = 0; + } + + let mut glyph_image = SceFontGlyphImage { + pixel_format: SceFontPixelFormatCode::Format8, + x_pos_64: 0, + y_pos_64: 0, + buf_width: gw as u16, + buf_height: gh as u16, + bytes_per_line: gw as u16, + pad: 0, + buffer_ptr: self.staging.as_mut_ptr() as u32, + }; + + let ret = + unsafe { sceFontGetCharGlyphImage(self.font.handle, char_code, &mut glyph_image) }; + if ret < 0 { + cursor_x += metrics.advance_x; + continue; + } + + // Insert into atlas. + if let Some(cached) = self.atlas.insert( + char_code, + gw, + gh, + metrics, + &self.staging[..staging_size], + gw, + ) { + let gx = cursor_x + cached.metrics.bearing_x; + let gy = y - cached.metrics.bearing_y; + let u0 = cached.atlas_x as f32; + let v0 = cached.atlas_y as f32; + let u1 = (cached.atlas_x + cached.atlas_w) as f32; + let v1 = (cached.atlas_y + cached.atlas_h) as f32; + self.batch.draw_rect( + gx, + gy, + cached.atlas_w as f32, + cached.atlas_h as f32, + u0, + v0, + u1, + v1, + color, + ); + } + + cursor_x += metrics.advance_x; + } + } + + /// Measure the width of a string in pixels without drawing. + pub fn measure_text(&self, text: &str) -> f32 { + let mut width = 0.0f32; + for c in text.chars() { + if let Ok(metrics) = self.font.char_info(c) { + width += metrics.advance_x; + } + } + width + } + + /// Get the line height in pixels. + pub fn line_height(&self) -> f32 { + if let Ok(info) = self.font.info() { + info.max_glyph_height_f + } else { + self.font_size + } + } + + /// Submit all queued glyph sprites to the GU. + /// + /// Sets up the CLUT and texture state for the PsmT8 atlas, then + /// flushes the sprite batch. + /// + /// # Safety + /// + /// Must be called within an active GU display list. + pub unsafe fn flush(&mut self) { + if self.batch.count() == 0 { + return; + } + + unsafe { + // Set up CLUT: alpha-ramp lookup table. + crate::sys::sceGuClutMode(crate::sys::ClutPixelFormat::Psm8888, 0, 0xFF, 0); + crate::sys::sceGuClutLoad(256 / 8, ALPHA_CLUT.0.as_ptr() as *const c_void); + + // Bind atlas texture as PsmT8. + crate::sys::sceGuTexMode(crate::sys::TexturePixelFormat::PsmT8, 0, 0, 0); + crate::sys::sceGuTexImage( + crate::sys::MipmapLevel::None, + ATLAS_WIDTH as i32, + ATLAS_HEIGHT as i32, + ATLAS_WIDTH as i32, + self.atlas.vram_ptr as *const c_void, + ); + + // Modulate: vertex color * texture alpha. + crate::sys::sceGuTexFunc( + crate::sys::TextureEffect::Modulate, + crate::sys::TextureColorComponent::Rgba, + ); + + self.batch.flush(); + } + } + + /// Clear the atlas, forcing all glyphs to be re-rendered. + pub fn clear_atlas(&mut self) { + self.atlas.clear(); + } + + /// Change the font size (clears the atlas). + /// + /// Note: PSP system fonts have fixed pixel sizes. This value is used + /// for spacing calculations when the font doesn't report metrics. + pub fn set_size(&mut self, size: f32) { + self.font_size = size; + self.atlas.clear(); + } +} diff --git a/psp/src/framebuffer.rs b/psp/src/framebuffer.rs new file mode 100644 index 0000000..f8baeaf --- /dev/null +++ b/psp/src/framebuffer.rs @@ -0,0 +1,419 @@ +//! Framebuffer management and layer compositing for the PSP display. +//! +//! Provides higher-level abstractions over the raw VRAM pointers and +//! display syscalls: +//! +//! - [`DoubleBuffer`]: Vsync-aware page flipping with two framebuffers +//! - [`DirtyRect`]: Track modified regions to minimize VRAM writes +//! - [`LayerCompositor`]: Compose multiple layers (background, content, +//! overlay) with DMA-driven blits +//! +//! # PSP Display Model +//! +//! The PSP has 2 MiB of VRAM at physical address `0x0400_0000`. The +//! display controller reads from a configurable base address within VRAM. +//! At 32bpp (PSM8888) with a 512-pixel stride, one framebuffer is +//! `512 * 272 * 4 = 557,056` bytes (~544 KiB). Two framebuffers fit +//! comfortably in VRAM with room for textures. + +use crate::sys::{DisplayPixelFormat, DisplaySetBufSync}; + +/// PSP screen width in pixels. +pub const SCREEN_WIDTH: u32 = 480; +/// PSP screen height in pixels. +pub const SCREEN_HEIGHT: u32 = 272; +/// Framebuffer stride in pixels (power-of-two padded). +pub const BUF_WIDTH: u32 = 512; + +/// Bytes per pixel for each display pixel format. +pub const fn bytes_per_pixel(fmt: DisplayPixelFormat) -> u32 { + match fmt { + DisplayPixelFormat::Psm5650 | DisplayPixelFormat::Psm5551 | DisplayPixelFormat::Psm4444 => { + 2 + }, + DisplayPixelFormat::Psm8888 => 4, + } +} + +/// Size of one framebuffer in bytes. +pub const fn framebuffer_size(fmt: DisplayPixelFormat) -> u32 { + BUF_WIDTH * SCREEN_HEIGHT * bytes_per_pixel(fmt) +} + +// ── DoubleBuffer ──────────────────────────────────────────────────── + +/// Double-buffered framebuffer manager with vsync-aware page flipping. +/// +/// Maintains two framebuffers in VRAM. While the display controller shows +/// one buffer, the application draws into the other. On swap, the display +/// pointer is updated to the newly drawn buffer (optionally synced to +/// vsync to avoid tearing). +/// +/// # Example +/// +/// ```ignore +/// use psp::framebuffer::DoubleBuffer; +/// use psp::sys::DisplayPixelFormat; +/// +/// let mut db = DoubleBuffer::new(DisplayPixelFormat::Psm8888, true); +/// db.init(); +/// +/// loop { +/// let buf = db.draw_buffer(); +/// // ... draw into buf ... +/// db.swap(); +/// } +/// ``` +pub struct DoubleBuffer { + /// VRAM offsets for the two buffers (relative to VRAM base). + offsets: [u32; 2], + /// Which buffer is currently being displayed (0 or 1). + display_buf: u8, + /// Pixel format. + format: DisplayPixelFormat, + /// Whether to sync swaps to vsync. + vsync: bool, +} + +impl DoubleBuffer { + /// Create a new double buffer manager. + /// + /// `vsync`: If true, `swap()` waits for vertical blank before + /// switching buffers, preventing tearing. + /// + /// # Important + /// + /// You **must** call [`init()`](Self::init) before using the double + /// buffer. Without it, the display mode is not configured and you + /// will get a black screen with no error. + pub fn new(format: DisplayPixelFormat, vsync: bool) -> Self { + let fb_size = framebuffer_size(format); + Self { + offsets: [0, fb_size], + display_buf: 0, + format, + vsync, + } + } + + /// Initialize the display mode and set the first framebuffer. + pub fn init(&self) { + unsafe { + crate::sys::sceDisplaySetMode( + crate::sys::DisplayMode::Lcd, + SCREEN_WIDTH as usize, + SCREEN_HEIGHT as usize, + ); + self.set_display_buffer(self.display_buf); + } + } + + /// Get a mutable pointer to the draw buffer (the one NOT being displayed). + /// + /// Returns a pointer to uncached VRAM suitable for direct pixel writes. + pub fn draw_buffer(&self) -> *mut u8 { + let draw_idx = 1 - self.display_buf; + self.vram_ptr(draw_idx) + } + + /// Get a pointer to the display buffer (the one currently shown). + pub fn display_buffer(&self) -> *const u8 { + self.vram_ptr(self.display_buf) as *const u8 + } + + /// Get the VRAM offset of the draw buffer. + pub fn draw_buffer_offset(&self) -> u32 { + let draw_idx = 1 - self.display_buf; + self.offsets[draw_idx as usize] + } + + /// Swap the draw and display buffers. + /// + /// If vsync is enabled, this blocks until the next vertical blank + /// before switching the display pointer. + pub fn swap(&mut self) { + if self.vsync { + unsafe { + crate::sys::sceDisplayWaitVblankStart(); + } + } + + // Switch to showing the draw buffer + self.display_buf = 1 - self.display_buf; + + unsafe { + self.set_display_buffer(self.display_buf); + } + } + + /// Get the pixel format. + pub fn format(&self) -> DisplayPixelFormat { + self.format + } + + /// Enable or disable vsync. + pub fn set_vsync(&mut self, vsync: bool) { + self.vsync = vsync; + } + + /// Get a raw pointer to a VRAM buffer. + fn vram_ptr(&self, idx: u8) -> *mut u8 { + const VRAM_UNCACHED: u32 = 0x4400_0000; + (VRAM_UNCACHED + self.offsets[idx as usize]) as *mut u8 + } + + /// Set the display controller to show the given buffer index. + unsafe fn set_display_buffer(&self, idx: u8) { + let sync = if self.vsync { + DisplaySetBufSync::NextFrame + } else { + DisplaySetBufSync::Immediate + }; + unsafe { + crate::sys::sceDisplaySetFrameBuf( + self.vram_ptr(idx) as *const u8, + BUF_WIDTH as usize, + self.format, + sync, + ); + } + } +} + +// ── DirtyRect ─────────────────────────────────────────────────────── + +/// A dirty-rectangle tracker. +/// +/// Tracks which rectangular regions of the framebuffer have been modified, +/// so you can minimize the amount of data copied (e.g., via DMA) when +/// updating the display. +/// +/// # Example +/// +/// ```ignore +/// use psp::framebuffer::DirtyRect; +/// +/// let mut dirty = DirtyRect::new(); +/// dirty.mark(10, 20, 100, 50); // Mark region (10,20)-(110,70) as dirty +/// dirty.mark(200, 100, 80, 30); +/// +/// if let Some((x, y, w, h)) = dirty.bounds() { +/// // Copy only the bounding rectangle of all dirty regions +/// blit_region(x, y, w, h); +/// } +/// +/// dirty.clear(); +/// ``` +pub struct DirtyRect { + min_x: u32, + min_y: u32, + max_x: u32, + max_y: u32, + dirty: bool, +} + +impl DirtyRect { + /// Create a new (clean) dirty-rect tracker. + pub const fn new() -> Self { + Self { + min_x: u32::MAX, + min_y: u32::MAX, + max_x: 0, + max_y: 0, + dirty: false, + } + } + + /// Mark a rectangular region as dirty. + pub fn mark(&mut self, x: u32, y: u32, width: u32, height: u32) { + self.dirty = true; + self.min_x = self.min_x.min(x); + self.min_y = self.min_y.min(y); + self.max_x = self.max_x.max(x + width); + self.max_y = self.max_y.max(y + height); + } + + /// Mark the entire screen as dirty. + pub fn mark_all(&mut self) { + self.mark(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + } + + /// Get the bounding rectangle of all dirty regions. + /// + /// Returns `Some((x, y, width, height))` if any region is dirty, + /// or `None` if everything is clean. + pub fn bounds(&self) -> Option<(u32, u32, u32, u32)> { + if !self.dirty { + return None; + } + let x = self.min_x.min(SCREEN_WIDTH); + let y = self.min_y.min(SCREEN_HEIGHT); + let max_x = self.max_x.min(SCREEN_WIDTH); + let max_y = self.max_y.min(SCREEN_HEIGHT); + if max_x <= x || max_y <= y { + return None; + } + Some((x, y, max_x - x, max_y - y)) + } + + /// Clear all dirty flags. + pub fn clear(&mut self) { + self.min_x = u32::MAX; + self.min_y = u32::MAX; + self.max_x = 0; + self.max_y = 0; + self.dirty = false; + } + + /// Check if any region is dirty. + pub fn is_dirty(&self) -> bool { + self.dirty + } +} + +impl Default for DirtyRect { + fn default() -> Self { + Self::new() + } +} + +// ── LayerCompositor ───────────────────────────────────────────────── + +/// Layer index for the compositor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Layer { + /// Background layer (drawn first, behind everything). + Background = 0, + /// Content layer (main application content). + Content = 1, + /// Overlay layer (drawn last, on top of everything — notifications, HUD). + Overlay = 2, +} + +/// Number of compositing layers. +pub const NUM_LAYERS: usize = 3; + +/// A simple layer-based framebuffer compositor. +/// +/// Maintains separate offscreen buffers for background, content, and +/// overlay layers. On each frame, the compositor blits the dirty +/// portions of each layer into the final framebuffer in order. +/// +/// # Memory Layout +/// +/// Each layer gets its own buffer. For PSM8888 at 512x272, each buffer +/// is ~544 KiB. Three layers plus two display buffers require ~2.7 MiB, +/// which exceeds VRAM (2 MiB). Therefore, layer buffers are allocated in +/// main RAM and blitted to VRAM. +/// +/// # Example +/// +/// ```ignore +/// use psp::framebuffer::{LayerCompositor, Layer, DoubleBuffer}; +/// use psp::sys::DisplayPixelFormat; +/// +/// let db = DoubleBuffer::new(DisplayPixelFormat::Psm8888, true); +/// let mut comp = LayerCompositor::new(DisplayPixelFormat::Psm8888); +/// +/// // Draw into layers +/// let bg = comp.layer_buffer(Layer::Background); +/// // ... draw background ... +/// comp.mark_dirty(Layer::Background, 0, 0, 480, 272); +/// +/// // Composite to the draw buffer +/// comp.composite_to(db.draw_buffer()); +/// ``` +pub struct LayerCompositor { + format: DisplayPixelFormat, + dirty: [DirtyRect; NUM_LAYERS], + /// Whether each layer is enabled. + enabled: [bool; NUM_LAYERS], +} + +impl LayerCompositor { + /// Create a new layer compositor. + pub fn new(format: DisplayPixelFormat) -> Self { + Self { + format, + dirty: [DirtyRect::new(), DirtyRect::new(), DirtyRect::new()], + enabled: [true, true, true], + } + } + + /// Mark a rectangular region of a layer as dirty. + pub fn mark_dirty(&mut self, layer: Layer, x: u32, y: u32, w: u32, h: u32) { + self.dirty[layer as usize].mark(x, y, w, h); + } + + /// Mark an entire layer as dirty. + pub fn mark_layer_dirty(&mut self, layer: Layer) { + self.dirty[layer as usize].mark_all(); + } + + /// Enable or disable a layer. + pub fn set_layer_enabled(&mut self, layer: Layer, enabled: bool) { + self.enabled[layer as usize] = enabled; + } + + /// Check if a layer is enabled. + pub fn is_layer_enabled(&self, layer: Layer) -> bool { + self.enabled[layer as usize] + } + + /// Composite all dirty layer regions into the output buffer. + /// + /// Layers are drawn in order: Background, Content, Overlay. Only + /// the dirty bounding rectangle of each layer is copied. + /// + /// # Safety + /// + /// - `output` must point to a valid framebuffer of the correct format. + /// - `layer_buffers` must contain valid pointers for each enabled layer. + pub unsafe fn composite_to( + &mut self, + output: *mut u8, + layer_buffers: &[*const u8; NUM_LAYERS], + ) { + let bpp = bytes_per_pixel(self.format); + let stride = BUF_WIDTH * bpp; + + for i in 0..NUM_LAYERS { + if !self.enabled[i] { + continue; + } + + if let Some((x, y, w, h)) = self.dirty[i].bounds() { + let src = layer_buffers[i]; + // Blit the dirty region row by row + for row in y..y + h { + let src_offset = (row * stride + x * bpp) as usize; + let dst_offset = src_offset; + let row_bytes = (w * bpp) as usize; + + unsafe { + core::ptr::copy_nonoverlapping( + src.add(src_offset), + output.add(dst_offset), + row_bytes, + ); + } + } + + self.dirty[i].clear(); + } + } + } + + /// Clear all dirty flags for all layers. + pub fn clear_all_dirty(&mut self) { + for d in &mut self.dirty { + d.clear(); + } + } + + /// Get the pixel format. + pub fn format(&self) -> DisplayPixelFormat { + self.format + } +} diff --git a/psp/src/gu_ext.rs b/psp/src/gu_ext.rs new file mode 100644 index 0000000..a6c78f4 --- /dev/null +++ b/psp/src/gu_ext.rs @@ -0,0 +1,192 @@ +//! GU rendering extensions for 2D sprite batching. +//! +//! Provides state snapshot/restore, 2D setup helpers, and a sprite batcher +//! that draws textured quads efficiently using `GuPrimitive::Sprites`. + +use crate::sys::{ + BlendFactor, BlendOp, GuState, MatrixMode, VertexType, sceGuBlendFunc, sceGuDisable, + sceGuEnable, sceGuGetAllStatus, sceGuSetAllStatus, sceGumLoadIdentity, sceGumMatrixMode, + sceGumOrtho, +}; + +/// Snapshot of all 22 GU boolean states. +/// +/// Only covers the states toggled by `sceGuEnable`/`sceGuDisable`. +/// Other state (blend func, texture mode, scissor) must be saved manually. +pub struct GuStateSnapshot { + bits: i32, +} + +impl GuStateSnapshot { + /// Capture the current GU boolean state. + pub fn capture() -> Self { + Self { + bits: unsafe { sceGuGetAllStatus() }, + } + } + + /// Restore the captured state. + pub fn restore(&self) { + unsafe { sceGuSetAllStatus(self.bits) }; + } +} + +/// Set up GU for 2D rendering. +/// +/// Configures an orthographic projection from (0,0) to (480,272), disables +/// depth testing, and enables texture mapping and alpha blending. +/// +/// # Safety +/// +/// Must be called within an active GU display list. +pub unsafe fn setup_2d() { + unsafe { + sceGumMatrixMode(MatrixMode::Projection); + sceGumLoadIdentity(); + sceGumOrtho(0.0, 480.0, 272.0, 0.0, -1.0, 1.0); + + sceGumMatrixMode(MatrixMode::View); + sceGumLoadIdentity(); + + sceGumMatrixMode(MatrixMode::Model); + sceGumLoadIdentity(); + + sceGuDisable(GuState::DepthTest); + sceGuEnable(GuState::Texture2D); + sceGuEnable(GuState::Blend); + sceGuBlendFunc( + BlendOp::Add, + BlendFactor::SrcAlpha, + BlendFactor::OneMinusSrcAlpha, + 0, + 0, + ); + } +} + +/// 2D sprite vertex: texture coords + color + position. +/// +/// Layout matches `SPRITE_VERTEX_TYPE` for use with `GuPrimitive::Sprites`. +#[repr(C, align(4))] +#[derive(Clone, Copy)] +pub struct SpriteVertex { + pub u: f32, + pub v: f32, + pub color: u32, + pub x: f32, + pub y: f32, + pub z: f32, +} + +/// Vertex type flags for [`SpriteVertex`]. +pub const SPRITE_VERTEX_TYPE: VertexType = VertexType::from_bits_truncate( + VertexType::TEXTURE_32BITF.bits() + | VertexType::COLOR_8888.bits() + | VertexType::VERTEX_32BITF.bits() + | VertexType::TRANSFORM_2D.bits(), +); + +/// Batches textured quads for efficient 2D rendering. +/// +/// Each sprite is a pair of vertices (top-left, bottom-right) drawn with +/// `GuPrimitive::Sprites`. Call [`flush`](SpriteBatch::flush) to submit +/// all queued sprites in a single draw call. +#[cfg(not(feature = "stub-only"))] +pub struct SpriteBatch { + vertices: alloc::vec::Vec, +} + +#[cfg(not(feature = "stub-only"))] +impl SpriteBatch { + /// Create a new sprite batch with capacity for `max_sprites` sprites. + /// + /// Each sprite uses 2 vertices, so this allocates `max_sprites * 2` entries. + pub fn new(max_sprites: usize) -> Self { + Self { + vertices: alloc::vec::Vec::with_capacity(max_sprites * 2), + } + } + + /// Add a textured rectangle. + /// + /// `(x, y)` is the top-left corner, `(w, h)` is the size. + /// `(u0, v0)` to `(u1, v1)` are texture coordinates. + /// `color` is ABGR format (0xAABBGGRR). + pub fn draw_rect( + &mut self, + x: f32, + y: f32, + w: f32, + h: f32, + u0: f32, + v0: f32, + u1: f32, + v1: f32, + color: u32, + ) { + self.vertices.push(SpriteVertex { + u: u0, + v: v0, + color, + x, + y, + z: 0.0, + }); + self.vertices.push(SpriteVertex { + u: u1, + v: v1, + color, + x: x + w, + y: y + h, + z: 0.0, + }); + } + + /// Add an untextured colored rectangle. + /// + /// Texture coordinates are set to 0; bind a 1x1 white texture or + /// disable texturing before flushing. + pub fn draw_colored_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: u32) { + self.draw_rect(x, y, w, h, 0.0, 0.0, 0.0, 0.0, color); + } + + /// Number of sprites currently queued. + pub fn count(&self) -> usize { + self.vertices.len() / 2 + } + + /// Discard all queued sprites. + pub fn clear(&mut self) { + self.vertices.clear(); + } + + /// Submit all queued sprites to the GU and clear the batch. + /// + /// # Safety + /// + /// Must be called within an active GU display list with an appropriate + /// texture bound (for textured sprites). + pub unsafe fn flush(&mut self) { + use crate::sys::{GuPrimitive, sceGuDrawArray, sceKernelDcacheWritebackRange}; + use core::ffi::c_void; + + if self.vertices.is_empty() { + return; + } + unsafe { + // Flush the CPU data cache so the GE can see the vertex data. + sceKernelDcacheWritebackRange( + self.vertices.as_ptr() as *const c_void, + (self.vertices.len() * core::mem::size_of::()) as u32, + ); + sceGuDrawArray( + GuPrimitive::Sprites, + SPRITE_VERTEX_TYPE, + self.vertices.len() as i32, + core::ptr::null::(), + self.vertices.as_ptr() as *const c_void, + ); + } + self.vertices.clear(); + } +} diff --git a/psp/src/http.rs b/psp/src/http.rs new file mode 100644 index 0000000..7bd77fa --- /dev/null +++ b/psp/src/http.rs @@ -0,0 +1,254 @@ +//! HTTP client for the PSP. +//! +//! Wraps `sceHttp*` syscalls into a safe, RAII-managed HTTP client with +//! template/connection/request lifecycle management. +//! +//! # Example +//! +//! ```ignore +//! use psp::http::HttpClient; +//! +//! let client = HttpClient::new().unwrap(); +//! let response = client.get(b"http://example.com/\0").unwrap(); +//! psp::dprintln!("Status: {}", response.status_code); +//! psp::dprintln!("Body: {} bytes", response.body.len()); +//! ``` + +use alloc::vec::Vec; +use core::ffi::c_void; + +use crate::sys; + +/// Error from an HTTP operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct HttpError(pub i32); + +impl core::fmt::Debug for HttpError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "HttpError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for HttpError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "http error {:#010x}", self.0 as u32) + } +} + +/// An HTTP client with RAII resource management. +/// +/// Manages the sceHttp subsystem initialization and template lifecycle. +/// All connections and requests created through this client are cleaned +/// up on drop. +pub struct HttpClient { + template_id: i32, +} + +impl HttpClient { + /// Initialize the HTTP subsystem and create a client. + /// + /// Calls `sceHttpInit` and creates a default template. + pub fn new() -> Result { + let ret = unsafe { sys::sceHttpInit(0x20000) }; + if ret < 0 { + return Err(HttpError(ret)); + } + + let template_id = unsafe { + sys::sceHttpCreateTemplate( + b"rust-psp/1.0\0".as_ptr() as *mut u8, + 1, // HTTP/1.1 + 0, + ) + }; + if template_id < 0 { + unsafe { sys::sceHttpEnd() }; + return Err(HttpError(template_id)); + } + + // Enable redirects by default. + unsafe { sys::sceHttpEnableRedirect(template_id) }; + + Ok(Self { template_id }) + } + + /// Perform an HTTP GET request. + /// + /// `url` must be a null-terminated byte string. + pub fn get(&self, url: &[u8]) -> Result { + RequestBuilder::new(self, sys::HttpMethod::Get, url).send() + } + + /// Perform an HTTP POST request. + /// + /// `url` must be a null-terminated byte string. + pub fn post(&self, url: &[u8], body: &[u8]) -> Result { + RequestBuilder::new(self, sys::HttpMethod::Post, url) + .body(body) + .send() + } + + /// Create a request builder for more control. + pub fn request<'a>(&'a self, method: sys::HttpMethod, url: &'a [u8]) -> RequestBuilder<'a> { + RequestBuilder::new(self, method, url) + } + + /// Get the template ID for advanced use. + pub fn template_id(&self) -> i32 { + self.template_id + } +} + +impl Drop for HttpClient { + fn drop(&mut self) { + unsafe { + sys::sceHttpDeleteTemplate(self.template_id); + sys::sceHttpEnd(); + } + } +} + +/// An HTTP response. +pub struct Response { + /// HTTP status code (e.g., 200, 404). + pub status_code: u16, + /// Content length if provided by the server, or `None`. + pub content_length: Option, + /// Response body. + pub body: Vec, +} + +/// Builder for HTTP requests. +pub struct RequestBuilder<'a> { + client: &'a HttpClient, + method: sys::HttpMethod, + url: &'a [u8], + body: Option<&'a [u8]>, + timeout_ms: Option, +} + +impl<'a> RequestBuilder<'a> { + fn new(client: &'a HttpClient, method: sys::HttpMethod, url: &'a [u8]) -> Self { + Self { + client, + method, + url, + body: None, + timeout_ms: None, + } + } + + /// Set the request body (for POST/PUT). + pub fn body(mut self, body: &'a [u8]) -> Self { + self.body = Some(body); + self + } + + /// Set the request timeout in milliseconds. + pub fn timeout(mut self, ms: u32) -> Self { + self.timeout_ms = Some(ms); + self + } + + /// Send the request and return the response. + pub fn send(self) -> Result { + // Validate null termination — the SCE HTTP syscalls expect C strings. + if self.url.last() != Some(&0) { + return Err(HttpError(-1)); + } + + let content_length = self.body.map(|b| b.len() as u64).unwrap_or(0); + + // Create connection + request using URL-based APIs. + let conn_id = unsafe { + sys::sceHttpCreateConnectionWithURL(self.client.template_id, self.url.as_ptr(), 0) + }; + if conn_id < 0 { + return Err(HttpError(conn_id)); + } + + let req_id = unsafe { + sys::sceHttpCreateRequestWithURL( + conn_id, + self.method, + self.url.as_ptr() as *mut u8, + content_length, + ) + }; + if req_id < 0 { + unsafe { sys::sceHttpDeleteConnection(conn_id) }; + return Err(HttpError(req_id)); + } + + // Apply timeout if set. + if let Some(ms) = self.timeout_ms { + unsafe { + sys::sceHttpSetConnectTimeOut(req_id, ms * 1000); + sys::sceHttpSetRecvTimeOut(req_id, ms * 1000); + sys::sceHttpSetSendTimeOut(req_id, ms * 1000); + } + } + + // Send the request. + let (data_ptr, data_size) = match self.body { + Some(b) => (b.as_ptr() as *mut c_void, b.len() as u32), + None => (core::ptr::null_mut(), 0), + }; + let ret = unsafe { sys::sceHttpSendRequest(req_id, data_ptr, data_size) }; + if ret < 0 { + unsafe { + sys::sceHttpDeleteRequest(req_id); + sys::sceHttpDeleteConnection(conn_id); + } + return Err(HttpError(ret)); + } + + // Get status code. + let mut status_code: i32 = 0; + let ret = unsafe { sys::sceHttpGetStatusCode(req_id, &mut status_code) }; + if ret < 0 { + unsafe { + sys::sceHttpDeleteRequest(req_id); + sys::sceHttpDeleteConnection(conn_id); + } + return Err(HttpError(ret)); + } + + // Get content length. + let mut cl: u64 = 0; + let cl_ret = unsafe { sys::sceHttpGetContentLength(req_id, &mut cl) }; + let content_length = if cl_ret >= 0 { Some(cl) } else { None }; + + // Read body. + let mut body = Vec::new(); + let mut buf = [0u8; 4096]; + loop { + let n = unsafe { + sys::sceHttpReadData(req_id, buf.as_mut_ptr() as *mut c_void, buf.len() as u32) + }; + if n < 0 { + unsafe { + sys::sceHttpDeleteRequest(req_id); + sys::sceHttpDeleteConnection(conn_id); + } + return Err(HttpError(n)); + } + if n == 0 { + break; + } + body.extend_from_slice(&buf[..n as usize]); + } + + // Cleanup. + unsafe { + sys::sceHttpDeleteRequest(req_id); + sys::sceHttpDeleteConnection(conn_id); + } + + Ok(Response { + status_code: status_code as u16, + content_length, + body, + }) + } +} diff --git a/psp/src/image.rs b/psp/src/image.rs new file mode 100644 index 0000000..c5d0095 --- /dev/null +++ b/psp/src/image.rs @@ -0,0 +1,229 @@ +//! Image decoding for the PSP. +//! +//! Supports hardware-accelerated JPEG decoding via `sceJpeg*` and +//! software BMP decoding for uncompressed 24/32-bit bitmaps. + +use alloc::vec::Vec; +use core::ffi::c_void; + +/// Pixel format of decoded image data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PixelFormat { + /// 4 bytes per pixel: R, G, B, A. + Rgba8888, + /// 3 bytes per pixel: R, G, B. + Rgb888, +} + +/// A decoded image in memory. +pub struct DecodedImage { + pub width: u32, + pub height: u32, + pub format: PixelFormat, + pub data: Vec, +} + +/// Error from an image operation. +pub enum ImageError { + /// Could not determine image format from magic bytes. + UnknownFormat, + /// Hardware JPEG decode error (SCE error code). + JpegError(i32), + /// BMP parsing error. + InvalidBmp(&'static str), + /// I/O error loading from file. + Io(crate::io::IoError), +} + +impl core::fmt::Debug for ImageError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::UnknownFormat => write!(f, "ImageError::UnknownFormat"), + Self::JpegError(e) => write!(f, "ImageError::JpegError({e:#010x})"), + Self::InvalidBmp(msg) => write!(f, "ImageError::InvalidBmp({msg:?})"), + Self::Io(e) => write!(f, "ImageError::Io({e:?})"), + } + } +} + +impl core::fmt::Display for ImageError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::UnknownFormat => write!(f, "unknown image format"), + Self::JpegError(e) => write!(f, "JPEG decode error {e:#010x}"), + Self::InvalidBmp(msg) => write!(f, "invalid BMP: {msg}"), + Self::Io(e) => write!(f, "image I/O error: {e}"), + } + } +} + +impl From for ImageError { + fn from(e: crate::io::IoError) -> Self { + Self::Io(e) + } +} + +/// Auto-detect format from magic bytes and decode. +pub fn decode(data: &[u8]) -> Result { + if data.len() >= 2 { + if data[0] == 0xFF && data[1] == 0xD8 { + // JPEG: use 1024x1024 as default max size. + return decode_jpeg(data, 1024, 1024); + } + if data[0] == b'B' && data[1] == b'M' { + return decode_bmp(data); + } + } + Err(ImageError::UnknownFormat) +} + +/// Decode a JPEG image using PSP hardware. +/// +/// `max_width` and `max_height` specify the maximum output dimensions. +/// The JPEG must fit within these bounds. +pub fn decode_jpeg( + data: &[u8], + max_width: i32, + max_height: i32, +) -> Result { + let ret = unsafe { crate::sys::sceJpegInitMJpeg() }; + if ret < 0 { + return Err(ImageError::JpegError(ret)); + } + + let ret = unsafe { crate::sys::sceJpegCreateMJpeg(max_width, max_height) }; + if ret < 0 { + unsafe { crate::sys::sceJpegFinishMJpeg() }; + return Err(ImageError::JpegError(ret)); + } + + let buf_size = (max_width as usize) * (max_height as usize) * 4; + let mut output = alloc::vec![0u8; buf_size]; + + let ret = unsafe { + crate::sys::sceJpegDecodeMJpeg( + data.as_ptr() as *mut u8, + data.len(), + output.as_mut_ptr() as *mut c_void, + 0, + ) + }; + + unsafe { + crate::sys::sceJpegDeleteMJpeg(); + crate::sys::sceJpegFinishMJpeg(); + } + + if ret < 0 { + return Err(ImageError::JpegError(ret)); + } + + let width = ((ret >> 16) & 0xFFFF) as u32; + let height = (ret & 0xFFFF) as u32; + output.truncate((width * height * 4) as usize); + + Ok(DecodedImage { + width, + height, + format: PixelFormat::Rgba8888, + data: output, + }) +} + +/// Decode an uncompressed 24-bit or 32-bit BMP. +pub fn decode_bmp(data: &[u8]) -> Result { + if data.len() < 54 { + return Err(ImageError::InvalidBmp("file too small")); + } + if data[0] != b'B' || data[1] != b'M' { + return Err(ImageError::InvalidBmp("bad magic")); + } + + let data_offset = read_u32_le(data, 10) as usize; + let dib_size = read_u32_le(data, 14); + if dib_size < 40 { + return Err(ImageError::InvalidBmp("unsupported DIB header")); + } + + let width = read_i32_le(data, 18); + let height_raw = read_i32_le(data, 22); + let top_down = height_raw < 0; + let height = if top_down { -height_raw } else { height_raw }; + if width <= 0 || height <= 0 { + return Err(ImageError::InvalidBmp("invalid dimensions")); + } + let width = width as u32; + let height = height as u32; + + let bpp = read_u16_le(data, 28); + let compression = read_u32_le(data, 30); + if compression != 0 { + return Err(ImageError::InvalidBmp("compressed BMPs not supported")); + } + + let (format, out_bpp) = match bpp { + 24 => (PixelFormat::Rgb888, 3u32), + 32 => (PixelFormat::Rgba8888, 4u32), + _ => return Err(ImageError::InvalidBmp("only 24/32-bit supported")), + }; + + let row_stride = ((width * (bpp as u32) + 31) / 32) * 4; + let mut output = alloc::vec![0u8; (width * height * out_bpp) as usize]; + + for y in 0..height { + let src_y = if top_down { y } else { height - 1 - y }; + let src_offset = data_offset + (src_y * row_stride) as usize; + let dst_offset = (y * width * out_bpp) as usize; + + if src_offset + (width * (bpp as u32 / 8)) as usize > data.len() { + return Err(ImageError::InvalidBmp("unexpected end of data")); + } + + for x in 0..width { + let si = src_offset + (x * (bpp as u32 / 8)) as usize; + let di = dst_offset + (x * out_bpp) as usize; + // BMP stores BGR; convert to RGB. + output[di] = data[si + 2]; // R + output[di + 1] = data[si + 1]; // G + output[di + 2] = data[si]; // B + if bpp == 32 { + output[di + 3] = data[si + 3]; // A + } + } + } + + Ok(DecodedImage { + width, + height, + format, + data: output, + }) +} + +/// Load an image from a file path (auto-detect format). +pub fn load(path: &str) -> Result { + let data = crate::io::read_to_vec(path)?; + decode(&data) +} + +fn read_u16_le(data: &[u8], offset: usize) -> u16 { + u16::from_le_bytes([data[offset], data[offset + 1]]) +} + +fn read_u32_le(data: &[u8], offset: usize) -> u32 { + u32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]) +} + +fn read_i32_le(data: &[u8], offset: usize) -> i32 { + i32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]) +} diff --git a/psp/src/input.rs b/psp/src/input.rs new file mode 100644 index 0000000..2382a90 --- /dev/null +++ b/psp/src/input.rs @@ -0,0 +1,145 @@ +//! Controller input with state change detection. +//! +//! Wraps `sceCtrlReadBufferPositive` with a high-level [`Controller`] that +//! tracks previous/current state for press/release detection and provides +//! normalized analog stick values with deadzone support. +//! +//! # Example +//! +//! ```ignore +//! use psp::input::Controller; +//! use psp::sys::CtrlButtons; +//! +//! psp::input::enable_analog(); +//! let mut ctrl = Controller::new(); +//! +//! loop { +//! ctrl.update(); +//! if ctrl.is_pressed(CtrlButtons::CROSS) { +//! // just pressed this frame +//! } +//! let x = ctrl.analog_x_f32(0.2); +//! // x is -1.0..1.0 with 20% deadzone +//! } +//! ``` + +use crate::sys::{CtrlButtons, CtrlMode, SceCtrlData, sceCtrlReadBufferPositive}; + +/// Initialize analog input mode. +/// +/// Call this once at startup before reading the analog stick. +/// Sets the sampling cycle to 0 (default) and mode to Analog. +pub fn enable_analog() { + unsafe { + crate::sys::sceCtrlSetSamplingCycle(0); + crate::sys::sceCtrlSetSamplingMode(CtrlMode::Analog); + } +} + +/// High-level controller input with state change detection. +/// +/// Call [`update()`](Self::update) once per frame to refresh the state, +/// then query buttons and analog stick. +pub struct Controller { + current: SceCtrlData, + previous: SceCtrlData, +} + +impl Controller { + /// Create a new controller with zeroed initial state. + pub fn new() -> Self { + Self { + current: SceCtrlData::default(), + previous: SceCtrlData::default(), + } + } + + /// Read the current controller state. + /// + /// Must be called once per frame for press/release detection to work. + pub fn update(&mut self) { + self.previous = self.current; + unsafe { + sceCtrlReadBufferPositive(&mut self.current, 1); + } + } + + /// Returns `true` if the button is currently held down. + pub fn is_held(&self, button: CtrlButtons) -> bool { + self.current.buttons.contains(button) + } + + /// Returns `true` if the button was just pressed this frame. + /// + /// (Down now, was not down last frame.) + pub fn is_pressed(&self, button: CtrlButtons) -> bool { + self.current.buttons.contains(button) && !self.previous.buttons.contains(button) + } + + /// Returns `true` if the button was just released this frame. + /// + /// (Not down now, was down last frame.) + pub fn is_released(&self, button: CtrlButtons) -> bool { + !self.current.buttons.contains(button) && self.previous.buttons.contains(button) + } + + /// Raw analog stick X value (0..=255, 128 is center). + pub fn analog_x(&self) -> u8 { + self.current.lx + } + + /// Raw analog stick Y value (0..=255, 128 is center). + pub fn analog_y(&self) -> u8 { + self.current.ly + } + + /// Normalized analog X in -1.0..=1.0 with deadzone. + /// + /// `deadzone` is the fraction of travel to ignore (e.g. 0.2 = 20%). + /// Returns 0.0 if within the deadzone. + pub fn analog_x_f32(&self, deadzone: f32) -> f32 { + normalize_axis(self.current.lx, deadzone) + } + + /// Normalized analog Y in -1.0..=1.0 with deadzone. + pub fn analog_y_f32(&self, deadzone: f32) -> f32 { + normalize_axis(self.current.ly, deadzone) + } + + /// Access the raw current controller data. + pub fn raw(&self) -> &SceCtrlData { + &self.current + } + + /// Access the raw previous-frame controller data. + pub fn raw_previous(&self) -> &SceCtrlData { + &self.previous + } +} + +impl Default for Controller { + fn default() -> Self { + Self::new() + } +} + +/// Normalize a raw 0..=255 axis value to -1.0..=1.0 with deadzone. +fn normalize_axis(raw: u8, deadzone: f32) -> f32 { + // Map 0..255 to -1.0..1.0 (128 is center) + let normalized = (raw as f32 - 128.0) / 127.0; + let abs = if normalized < 0.0 { + -normalized + } else { + normalized + }; + if abs < deadzone { + 0.0 + } else { + // Remap so the edge of the deadzone maps to 0.0 + let sign = if normalized < 0.0 { -1.0 } else { 1.0 }; + let remapped = (abs - deadzone) / (1.0 - deadzone); + // Clamp to 1.0 (raw=0 or raw=255 can slightly exceed 1.0) + let clamped = if remapped > 1.0 { 1.0 } else { remapped }; + sign * clamped + } +} diff --git a/psp/src/io.rs b/psp/src/io.rs new file mode 100644 index 0000000..807d9fe --- /dev/null +++ b/psp/src/io.rs @@ -0,0 +1,348 @@ +//! File I/O abstractions for the PSP. +//! +//! Wraps the raw `sceIo*` syscalls with RAII file handles, directory +//! iterators, and convenience functions for common operations. +//! +//! # Example +//! +//! ```ignore +//! use psp::io::{File, IoOpenFlags}; +//! +//! // Write a file +//! let mut f = File::create("ms0:/data/save.bin").unwrap(); +//! f.write(b"hello").unwrap(); +//! +//! // Read a file +//! let mut f = File::open("ms0:/data/save.bin", IoOpenFlags::RD_ONLY).unwrap(); +//! let mut buf = [0u8; 64]; +//! let n = f.read(&mut buf).unwrap(); +//! ``` + +use crate::sys::{ + IoOpenFlags, IoWhence, SceIoDirent, SceIoStat, SceUid, sceIoClose, sceIoDclose, sceIoDopen, + sceIoDread, sceIoGetstat, sceIoLseek, sceIoMkdir, sceIoOpen, sceIoRead, sceIoRemove, + sceIoRename, sceIoRmdir, sceIoWrite, +}; +use core::ffi::c_void; +use core::marker::PhantomData; + +// ── IoError ───────────────────────────────────────────────────────── + +/// Error from a PSP I/O operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct IoError(pub i32); + +impl IoError { + /// The raw SCE error code. + pub fn code(self) -> i32 { + self.0 + } +} + +impl core::fmt::Debug for IoError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "IoError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for IoError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "I/O error {:#010x}", self.0 as u32) + } +} + +// ── Helpers ───────────────────────────────────────────────────────── + +/// Maximum path length (including null terminator) that fits on the stack. +const MAX_PATH: usize = 256; + +/// Copy a `&str` into a stack buffer with a null terminator. +/// +/// Returns `Err` if the path is too long. +fn path_to_cstr(path: &str, buf: &mut [u8; MAX_PATH]) -> Result<(), IoError> { + let bytes = path.as_bytes(); + if bytes.len() >= MAX_PATH { + // SCE_KERNEL_ERROR_NAMETOOLONG = 0x8001005B + return Err(IoError(0x8001_005Bu32 as i32)); + } + buf[..bytes.len()].copy_from_slice(bytes); + buf[bytes.len()] = 0; + Ok(()) +} + +// ── File ──────────────────────────────────────────────────────────── + +/// An open file descriptor with RAII cleanup. +/// +/// The file is automatically closed when this value is dropped. +/// `File` is `!Send + !Sync` because PSP file descriptors are not thread-safe. +pub struct File { + fd: SceUid, + // Make File !Send + !Sync (raw pointers are neither). + _marker: PhantomData<*const ()>, +} + +impl File { + /// Open a file with the given flags. + /// + /// `path` is a PSP path, e.g. `"ms0:/data/file.txt"`. + pub fn open(path: &str, flags: IoOpenFlags) -> Result { + let mut buf = [0u8; MAX_PATH]; + path_to_cstr(path, &mut buf)?; + let fd = unsafe { sceIoOpen(buf.as_ptr(), flags, 0o777) }; + if fd.0 < 0 { + Err(IoError(fd.0)) + } else { + Ok(Self { + fd, + _marker: PhantomData, + }) + } + } + + /// Create a file for writing (create + truncate + write-only). + pub fn create(path: &str) -> Result { + Self::open( + path, + IoOpenFlags::WR_ONLY | IoOpenFlags::CREAT | IoOpenFlags::TRUNC, + ) + } + + /// Read bytes into `buf`. Returns the number of bytes read. + pub fn read(&self, buf: &mut [u8]) -> Result { + let ret = unsafe { sceIoRead(self.fd, buf.as_mut_ptr() as *mut c_void, buf.len() as u32) }; + if ret < 0 { + Err(IoError(ret)) + } else { + Ok(ret as usize) + } + } + + /// Write bytes from `buf`. Returns the number of bytes written. + pub fn write(&self, buf: &[u8]) -> Result { + let ret = unsafe { sceIoWrite(self.fd, buf.as_ptr() as *const c_void, buf.len()) }; + if ret < 0 { + Err(IoError(ret)) + } else { + Ok(ret as usize) + } + } + + /// Read until `buf` is full or EOF is reached. + /// + /// Returns the total number of bytes read. + pub fn read_all(&self, buf: &mut [u8]) -> Result { + let mut total = 0; + while total < buf.len() { + let n = self.read(&mut buf[total..])?; + if n == 0 { + break; // EOF + } + total += n; + } + Ok(total) + } + + /// Seek to a position in the file. + /// + /// Returns the new absolute position. + pub fn seek(&self, offset: i64, whence: IoWhence) -> Result { + let pos = unsafe { sceIoLseek(self.fd, offset, whence) }; + if pos < 0 { + Err(IoError(pos as i32)) + } else { + Ok(pos) + } + } + + /// Get the size of the file in bytes. + pub fn size(&self) -> Result { + let old = self.seek(0, IoWhence::Cur)?; + let end = self.seek(0, IoWhence::End)?; + self.seek(old, IoWhence::Set)?; + Ok(end) + } + + /// Get the underlying file descriptor. + pub fn fd(&self) -> SceUid { + self.fd + } +} + +impl Drop for File { + fn drop(&mut self) { + unsafe { + sceIoClose(self.fd); + } + } +} + +// ── ReadDir ───────────────────────────────────────────────────────── + +/// A directory entry returned by [`ReadDir`]. +pub struct DirEntry { + /// The raw directory entry from the PSP OS. + pub dirent: SceIoDirent, +} + +impl DirEntry { + /// File name as a byte slice (null-terminated in the raw struct). + pub fn name(&self) -> &[u8] { + let name = &self.dirent.d_name; + let len = name.iter().position(|&b| b == 0).unwrap_or(name.len()); + &name[..len] + } + + /// File status. + pub fn stat(&self) -> &SceIoStat { + &self.dirent.d_stat + } + + /// Returns `true` if this entry is a directory. + pub fn is_dir(&self) -> bool { + use crate::sys::IoStatMode; + self.dirent.d_stat.st_mode.contains(IoStatMode::IFDIR) + } + + /// Returns `true` if this entry is a regular file. + pub fn is_file(&self) -> bool { + use crate::sys::IoStatMode; + self.dirent.d_stat.st_mode.contains(IoStatMode::IFREG) + } +} + +/// An iterator over directory entries. +/// +/// Created by [`read_dir()`]. Automatically closes the directory +/// handle on drop. +pub struct ReadDir { + fd: SceUid, + done: bool, + _marker: PhantomData<*const ()>, +} + +impl Iterator for ReadDir { + type Item = Result; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + + // SAFETY: SceIoDirent is repr(C) and zeroed is a valid initial state. + let mut dirent: SceIoDirent = unsafe { core::mem::zeroed() }; + let ret = unsafe { sceIoDread(self.fd, &mut dirent) }; + + if ret < 0 { + self.done = true; + Some(Err(IoError(ret))) + } else if ret == 0 { + self.done = true; + None + } else { + Some(Ok(DirEntry { dirent })) + } + } +} + +impl Drop for ReadDir { + fn drop(&mut self) { + unsafe { + sceIoDclose(self.fd); + } + } +} + +/// Open a directory for iteration. +/// +/// # Example +/// +/// ```ignore +/// for entry in psp::io::read_dir("ms0:/PSP/GAME").unwrap() { +/// let entry = entry.unwrap(); +/// psp::dprintln!("{}", core::str::from_utf8(entry.name()).unwrap_or("?")); +/// } +/// ``` +pub fn read_dir(path: &str) -> Result { + let mut buf = [0u8; MAX_PATH]; + path_to_cstr(path, &mut buf)?; + let fd = unsafe { sceIoDopen(buf.as_ptr()) }; + if fd.0 < 0 { + Err(IoError(fd.0)) + } else { + Ok(ReadDir { + fd, + done: false, + _marker: PhantomData, + }) + } +} + +// ── Convenience functions ─────────────────────────────────────────── + +/// Read an entire file into a `Vec`. +#[cfg(not(feature = "stub-only"))] +pub fn read_to_vec(path: &str) -> Result, IoError> { + let f = File::open(path, IoOpenFlags::RD_ONLY)?; + let size = f.size()? as usize; + let mut data = alloc::vec![0u8; size]; + f.read_all(&mut data)?; + Ok(data) +} + +/// Write bytes to a file (create/truncate). +pub fn write_bytes(path: &str, data: &[u8]) -> Result<(), IoError> { + let f = File::create(path)?; + let mut written = 0; + while written < data.len() { + let n = f.write(&data[written..])?; + if n == 0 { + return Err(IoError(-1)); + } + written += n; + } + Ok(()) +} + +/// Get file status without opening the file. +pub fn stat(path: &str) -> Result { + let mut buf = [0u8; MAX_PATH]; + path_to_cstr(path, &mut buf)?; + let mut st: SceIoStat = unsafe { core::mem::zeroed() }; + let ret = unsafe { sceIoGetstat(buf.as_ptr(), &mut st) }; + if ret < 0 { Err(IoError(ret)) } else { Ok(st) } +} + +/// Create a directory. +pub fn create_dir(path: &str) -> Result<(), IoError> { + let mut buf = [0u8; MAX_PATH]; + path_to_cstr(path, &mut buf)?; + let ret = unsafe { sceIoMkdir(buf.as_ptr(), 0o777) }; + if ret < 0 { Err(IoError(ret)) } else { Ok(()) } +} + +/// Remove a file. +pub fn remove_file(path: &str) -> Result<(), IoError> { + let mut buf = [0u8; MAX_PATH]; + path_to_cstr(path, &mut buf)?; + let ret = unsafe { sceIoRemove(buf.as_ptr()) }; + if ret < 0 { Err(IoError(ret)) } else { Ok(()) } +} + +/// Remove a directory (must be empty). +pub fn remove_dir(path: &str) -> Result<(), IoError> { + let mut buf = [0u8; MAX_PATH]; + path_to_cstr(path, &mut buf)?; + let ret = unsafe { sceIoRmdir(buf.as_ptr()) }; + if ret < 0 { Err(IoError(ret)) } else { Ok(()) } +} + +/// Rename a file or directory. +pub fn rename(from: &str, to: &str) -> Result<(), IoError> { + let mut from_buf = [0u8; MAX_PATH]; + let mut to_buf = [0u8; MAX_PATH]; + path_to_cstr(from, &mut from_buf)?; + path_to_cstr(to, &mut to_buf)?; + let ret = unsafe { sceIoRename(from_buf.as_ptr(), to_buf.as_ptr()) }; + if ret < 0 { Err(IoError(ret)) } else { Ok(()) } +} diff --git a/psp/src/lib.rs b/psp/src/lib.rs index 58b2467..0b50996 100644 --- a/psp/src/lib.rs +++ b/psp/src/lib.rs @@ -39,17 +39,58 @@ pub mod debug; #[macro_use] mod vfpu; +pub mod audio; +pub mod audio_mixer; +pub mod cache; +#[cfg(not(feature = "stub-only"))] +pub mod callback; +#[cfg(not(feature = "stub-only"))] +pub mod config; +pub mod dialog; +pub mod display; +pub mod dma; mod eabi; +#[cfg(not(feature = "stub-only"))] +pub mod font; +pub mod framebuffer; +pub mod gu_ext; +#[cfg(not(feature = "stub-only"))] +pub mod http; #[cfg(feature = "kernel")] pub mod hw; +#[cfg(not(feature = "stub-only"))] +pub mod image; +pub mod input; +pub mod io; pub mod math; #[cfg(feature = "kernel")] pub mod me; +pub mod mem; +#[cfg(not(feature = "stub-only"))] +pub mod mp3; +#[cfg(not(feature = "stub-only"))] +pub mod net; +#[cfg(not(feature = "stub-only"))] +pub mod osk; +pub mod power; +pub mod rtc; +#[cfg(not(feature = "stub-only"))] +pub mod savedata; +pub mod simd; +pub mod sync; pub mod sys; +pub mod system_param; #[cfg(not(feature = "stub-only"))] pub mod test_runner; #[cfg(not(feature = "stub-only"))] +pub mod thread; +pub mod time; +#[cfg(not(feature = "stub-only"))] +pub mod timer; +pub mod usb; +#[cfg(not(feature = "stub-only"))] pub mod vram_alloc; +pub mod wlan; #[cfg(not(feature = "stub-only"))] mod alloc_impl; @@ -318,6 +359,7 @@ macro_rules! __module_impl { /// This API does not have destructor support yet. You can manually set up an /// exit callback if you need more control -- see the source code of this /// function. +#[deprecated(note = "Use psp::callback::setup_exit_callback() instead")] pub fn enable_home_button() { use core::{ffi::c_void, ptr}; use sys::ThreadAttributes; diff --git a/psp/src/me.rs b/psp/src/me.rs index d4d5883..4a984ab 100644 --- a/psp/src/me.rs +++ b/psp/src/me.rs @@ -16,6 +16,22 @@ //! addresses (OR'd with `0x4000_0000`). The ME cannot access cached main //! RAM coherently. //! +//! # High-Level API +//! +//! The [`MeExecutor`] provides a safe, high-level task submission API that +//! handles uncached memory allocation, shared synchronization state, and +//! cache management internally. +//! +//! ```ignore +//! use psp::me::MeExecutor; +//! +//! unsafe extern "C" fn my_task(arg: i32) -> i32 { arg * 2 } +//! +//! let mut executor = MeExecutor::new(4096).unwrap(); +//! let handle = unsafe { executor.submit(my_task, 21) }; +//! let result = executor.wait(&handle); // returns 42 +//! ``` +//! //! # Kernel Mode Required //! //! All functions in this module require `feature = "kernel"` and the module @@ -146,3 +162,244 @@ pub unsafe fn me_alloc(size: u32, name: *const u8) -> Result<(*mut u8, crate::sy Ok((uncached_ptr, block_id)) } + +// ── MeExecutor ────────────────────────────────────────────────────── + +/// Status values for ME task slots, stored in uncached shared memory. +#[cfg(feature = "kernel")] +mod status { + /// Slot is available for a new task. + pub const IDLE: u32 = 0; + /// Task has been submitted and is running on the ME. + pub const RUNNING: u32 = 1; + /// Task has completed; result is available. + pub const DONE: u32 = 2; +} + +/// Shared state between the main CPU and ME for a single task. +/// +/// This struct lives in uncached memory. The ME writes `status` and +/// `result` when the task completes; the main CPU reads them. +/// +/// `real_task` and `real_arg` are written by the main CPU before booting +/// the ME. The ME wrapper reads them from here rather than from +/// `boot_params`, avoiding a race where `boot_params` would need to be +/// written twice. +#[cfg(feature = "kernel")] +#[repr(C, align(64))] +struct MeSharedState { + /// Task status (see [`status`] module). + status: u32, + /// Task return value (valid when `status == DONE`). + result: i32, + /// The actual user task, stored separately from boot_params. + real_task: MeTask, + /// The actual user argument, stored separately from boot_params. + real_arg: i32, + /// Boot parameters for the ME (always points to the wrapper). + boot_params: MeBootParams, +} + +/// An opaque handle to a submitted ME task. +/// +/// Use with [`MeExecutor::poll`] or [`MeExecutor::wait`] to retrieve +/// the result. +#[cfg(feature = "kernel")] +#[derive(Debug, Clone, Copy)] +pub struct MeHandle { + /// Index into the shared state — currently always 0 since the ME + /// can only run one task at a time. + _slot: u32, +} + +/// High-level Media Engine task executor. +/// +/// Manages uncached memory allocation, ME boot parameters, and +/// synchronization internally. Submit tasks with [`submit`](Self::submit), +/// then poll or wait for results. +/// +/// # Example +/// +/// ```ignore +/// use psp::me::MeExecutor; +/// +/// unsafe extern "C" fn double(arg: i32) -> i32 { arg * 2 } +/// +/// let mut executor = MeExecutor::new(4096).unwrap(); +/// let handle = unsafe { executor.submit(double, 21) }; +/// assert_eq!(executor.wait(&handle), 42); +/// ``` +#[cfg(feature = "kernel")] +pub struct MeExecutor { + /// Pointer to the shared state in uncached memory. + shared: *mut MeSharedState, + /// Block ID for the shared state allocation. + shared_block: crate::sys::SceUid, + /// Pointer to the ME stack in uncached memory. + stack_base: *mut u8, + /// Block ID for the stack allocation. + stack_block: crate::sys::SceUid, + /// Size of the ME stack. + stack_size: u32, +} + +#[cfg(feature = "kernel")] +impl MeExecutor { + /// Create a new `MeExecutor` with the given ME stack size. + /// + /// Allocates shared state and stack memory in ME-accessible partition 3. + /// `stack_size` should be at least 4096 bytes for most tasks. + /// + /// # Errors + /// + /// Returns the PSP error code if memory allocation fails. + pub fn new(stack_size: u32) -> Result { + let shared_size = core::mem::size_of::() as u32; + + // SAFETY: Kernel mode is required. We allocate from partition 3. + let (shared_ptr, shared_block) = + unsafe { me_alloc(shared_size, b"MeExecState\0".as_ptr()) }?; + let shared = shared_ptr as *mut MeSharedState; + + let (stack_base, stack_block) = + match unsafe { me_alloc(stack_size, b"MeExecStack\0".as_ptr()) } { + Ok(v) => v, + Err(e) => { + // Clean up the shared state allocation + unsafe { + crate::sys::sceKernelFreePartitionMemory(shared_block); + } + return Err(e); + }, + }; + + // Initialize shared state to idle + // SAFETY: shared is a valid uncached pointer. + unsafe { + core::ptr::write_volatile(&raw mut (*shared).status, status::IDLE); + core::ptr::write_volatile(&raw mut (*shared).result, 0); + } + + Ok(Self { + shared, + shared_block, + stack_base, + stack_block, + stack_size, + }) + } + + /// Submit a task to the Media Engine. + /// + /// The ME will execute `task(arg)` on its own core. Use the returned + /// [`MeHandle`] with [`poll`](Self::poll) or [`wait`](Self::wait) to + /// retrieve the result. + /// + /// # Safety + /// + /// - Only one task can run at a time. Calling `submit` while a + /// previous task is still running is undefined behavior. + /// - `task` must be safe to execute on the ME core (no syscalls, + /// no cached memory access, no floating-point context sharing). + /// - The caller must be in kernel mode. + #[cfg(all(target_os = "psp", feature = "kernel"))] + pub unsafe fn submit(&mut self, task: MeTask, arg: i32) -> MeHandle { + // Wrapper that reads the real task from shared state, executes it, + // then writes the result and status. The ME cannot call PSP syscalls, + // so the wrapper writes directly to the uncached shared state. + unsafe extern "C" fn me_wrapper(shared_addr: i32) -> i32 { + let shared = shared_addr as *mut MeSharedState; + let task: MeTask = core::ptr::read_volatile(&raw const (*shared).real_task); + let arg = core::ptr::read_volatile(&raw const (*shared).real_arg); + + let result = task(arg); + + // Write result and mark as done (uncached memory, visible immediately) + core::ptr::write_volatile(&raw mut (*shared).result, result); + core::ptr::write_volatile(&raw mut (*shared).status, status::DONE); + + result + } + + // Stack grows downward — point to the top + let stack_top = self.stack_base.add(self.stack_size as usize); + + // Write the real task and arg to dedicated fields first + unsafe { + core::ptr::write_volatile(&raw mut (*self.shared).status, status::RUNNING); + core::ptr::write_volatile(&raw mut (*self.shared).real_task, task); + core::ptr::write_volatile(&raw mut (*self.shared).real_arg, arg); + } + + // Write boot_params once with the wrapper — no second write needed + unsafe { + core::ptr::write_volatile( + &raw mut (*self.shared).boot_params, + MeBootParams { + task: me_wrapper, + arg: self.shared as i32, + stack_top, + }, + ); + } + + // Boot the ME + // SAFETY: All params are in uncached memory, kernel mode is required + unsafe { + me_boot(&(*self.shared).boot_params); + } + + MeHandle { _slot: 0 } + } + + /// Poll for task completion without blocking. + /// + /// Returns `Some(result)` if the task has completed, `None` if it's + /// still running. + pub fn poll(&self, _handle: &MeHandle) -> Option { + // SAFETY: Reading from uncached memory — volatile access + let st = unsafe { core::ptr::read_volatile(&raw const (*self.shared).status) }; + if st == status::DONE { + let result = unsafe { core::ptr::read_volatile(&raw const (*self.shared).result) }; + Some(result) + } else { + None + } + } + + /// Block until the task completes and return its result. + pub fn wait(&self, handle: &MeHandle) -> i32 { + loop { + if let Some(result) = self.poll(handle) { + return result; + } + core::hint::spin_loop(); + } + } + + /// Check if the executor is idle (no task running). + pub fn is_idle(&self) -> bool { + let st = unsafe { core::ptr::read_volatile(&raw const (*self.shared).status) }; + st != status::RUNNING + } + + /// Reset the executor state to idle. + /// + /// Call this after retrieving a result to allow submitting new tasks. + pub fn reset(&mut self) { + unsafe { + core::ptr::write_volatile(&raw mut (*self.shared).status, status::IDLE); + } + } +} + +#[cfg(feature = "kernel")] +impl Drop for MeExecutor { + fn drop(&mut self) { + // SAFETY: We own these allocations + unsafe { + crate::sys::sceKernelFreePartitionMemory(self.stack_block); + crate::sys::sceKernelFreePartitionMemory(self.shared_block); + } + } +} diff --git a/psp/src/mem.rs b/psp/src/mem.rs new file mode 100644 index 0000000..84dcd47 --- /dev/null +++ b/psp/src/mem.rs @@ -0,0 +1,265 @@ +//! Typed memory partition allocators for the PSP. +//! +//! The PSP has multiple memory partitions with different access +//! characteristics. Passing a pointer from the wrong partition to +//! hardware (e.g., giving a main-RAM pointer to the ME) causes silent +//! corruption. This module provides typed allocators that make partition +//! misuse a compile-time error. +//! +//! # Partitions +//! +//! | Partition | ID | Access | Use Case | +//! |-----------|----|---------------|-----------------------------------| +//! | User | 2 | Main CPU only | General-purpose allocations | +//! | ME Kernel | 3 | CPU + ME | Shared state, ME task stacks | +//! | ME User | 7 | CPU + ME | ME-accessible user memory | +//! +//! # Kernel Mode Required +//! +//! Partitions 1, 3-5, 8-12 require kernel mode. Partition 2 is available +//! in user mode. + +use crate::sys::{ + SceSysMemBlockTypes, SceSysMemPartitionId, SceUid, sceKernelAllocPartitionMemory, + sceKernelFreePartitionMemory, sceKernelGetBlockHeadAddr, +}; +use core::marker::PhantomData; + +/// Marker trait for a memory partition. +/// +/// Sealed — cannot be implemented outside this module. +pub trait Partition: sealed::Sealed { + /// The PSP partition ID. + const ID: SceSysMemPartitionId; + /// Human-readable name for debug output. + const NAME: &'static str; +} + +mod sealed { + pub trait Sealed {} + impl Sealed for super::UserPartition {} + impl Sealed for super::MePartition {} +} + +/// Marker type for user-mode partition 2 (main RAM, CPU only). +pub struct UserPartition; +impl Partition for UserPartition { + const ID: SceSysMemPartitionId = SceSysMemPartitionId::SceKernelPrimaryUserPartition; + const NAME: &'static str = "User"; +} + +/// Marker type for ME kernel partition 3 (ME-accessible, kernel only). +pub struct MePartition; +impl Partition for MePartition { + const ID: SceSysMemPartitionId = SceSysMemPartitionId::SceKernelOtherKernelPartition1; + const NAME: &'static str = "ME"; +} + +/// A typed allocation from a specific memory partition. +/// +/// The partition type parameter `P` ensures at compile time that you +/// cannot pass a `PartitionAlloc` where a +/// `PartitionAlloc` is expected. +/// +/// # Example +/// +/// ```ignore +/// use psp::mem::{PartitionAlloc, UserPartition, MePartition}; +/// +/// // User-mode allocation +/// let user_buf = PartitionAlloc::::new( +/// [0u8; 1024], b"mybuf\0" +/// ).unwrap(); +/// +/// // ME-accessible allocation (kernel mode required) +/// let me_buf = PartitionAlloc::::new( +/// 0u32, b"mebuf\0" +/// ).unwrap(); +/// ``` +pub struct PartitionAlloc { + ptr: *mut T, + block_id: SceUid, + /// Whether the value at `ptr` has been initialized and needs dropping. + /// Set to `false` for `new_uninit()` allocations to prevent UB from + /// calling `drop_in_place` on uninitialized memory. + initialized: bool, + _partition: PhantomData

, +} + +/// Convenience alias for user-mode partition 2 allocations. +pub type Partition2Alloc = PartitionAlloc; + +/// Convenience alias for ME kernel partition 3 allocations. +#[cfg(feature = "kernel")] +pub type Partition3Alloc = PartitionAlloc; + +impl PartitionAlloc { + /// Allocate memory in partition `P` and initialize it with `val`. + /// + /// `name` must be a null-terminated byte string used by the kernel + /// for identification (e.g., `b"myalloc\0"`). + /// + /// # Errors + /// + /// Returns the negative PSP error code if allocation fails. + pub fn new(val: T, name: &[u8]) -> Result { + let size = core::mem::size_of::().max(1) as u32; + + let block_id = unsafe { + sceKernelAllocPartitionMemory( + P::ID, + name.as_ptr(), + SceSysMemBlockTypes::Low, + size, + core::ptr::null_mut(), + ) + }; + + if block_id.0 < 0 { + return Err(block_id.0); + } + + let ptr = unsafe { sceKernelGetBlockHeadAddr(block_id) } as *mut T; + + // SAFETY: ptr is valid and properly sized + unsafe { + core::ptr::write(ptr, val); + } + + Ok(Self { + ptr, + block_id, + initialized: true, + _partition: PhantomData, + }) + } + + /// Allocate uninitialized memory in partition `P`. + /// + /// # Safety + /// + /// The caller must initialize the memory before reading from it. + pub unsafe fn new_uninit(size: u32, name: &[u8]) -> Result { + let block_id = unsafe { + sceKernelAllocPartitionMemory( + P::ID, + name.as_ptr(), + SceSysMemBlockTypes::Low, + size, + core::ptr::null_mut(), + ) + }; + + if block_id.0 < 0 { + return Err(block_id.0); + } + + let ptr = unsafe { sceKernelGetBlockHeadAddr(block_id) } as *mut T; + + Ok(Self { + ptr, + block_id, + initialized: false, + _partition: PhantomData, + }) + } + + /// Mark the allocation as initialized. + /// + /// After calling this, `Drop` will call `drop_in_place` on the value. + /// Call this after writing a valid `T` into the allocation. + /// + /// # Safety + /// + /// The caller must have written a valid, initialized `T` to the pointer. + pub unsafe fn assume_init(&mut self) { + self.initialized = true; + } + + /// Get a raw pointer to the allocated memory. + pub fn as_ptr(&self) -> *const T { + self.ptr + } + + /// Get a mutable raw pointer to the allocated memory. + pub fn as_mut_ptr(&mut self) -> *mut T { + self.ptr + } + + /// Get a reference to the allocated value. + /// + /// # Safety + /// + /// The caller must ensure no concurrent mutable access exists. + pub unsafe fn as_ref(&self) -> &T { + unsafe { &*self.ptr } + } + + /// Get a mutable reference to the allocated value. + /// + /// # Safety + /// + /// The caller must ensure exclusive access. + pub unsafe fn as_mut(&mut self) -> &mut T { + unsafe { &mut *self.ptr } + } + + /// Get the kernel block ID (for manual management). + pub fn block_id(&self) -> SceUid { + self.block_id + } +} + +// ME partition allocations can convert to uncached pointers +#[cfg(feature = "kernel")] +impl PartitionAlloc { + /// Get an uncached pointer to this ME-accessible allocation. + /// + /// The ME requires uncached addresses. This method ORs the pointer + /// with `0x4000_0000` to bypass the CPU data cache. + pub fn as_uncached_ptr(&self) -> *mut T { + crate::me::to_uncached(self.ptr) + } +} + +// SAFETY: PartitionAlloc owns its allocation and can be sent across threads. +unsafe impl Send for PartitionAlloc {} + +impl Drop for PartitionAlloc { + fn drop(&mut self) { + unsafe { + // Only drop the value if it was initialized (prevents UB for new_uninit). + if self.initialized { + core::ptr::drop_in_place(self.ptr); + } + sceKernelFreePartitionMemory(self.block_id); + } + } +} + +impl core::fmt::Debug for PartitionAlloc { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PartitionAlloc") + .field("partition", &P::NAME) + .field("ptr", &self.ptr) + .field("block_id", &self.block_id) + .finish() + } +} + +/// Allocate a byte buffer in user partition 2. +/// +/// Convenience function for the common case of allocating a byte buffer. +pub fn alloc_user_bytes(size: u32, name: &[u8]) -> Result, i32> { + // SAFETY: Byte buffers don't need initialization + unsafe { PartitionAlloc::::new_uninit(size, name) } +} + +/// Allocate a byte buffer in ME kernel partition 3. +/// +/// Convenience function for kernel-mode ME-accessible buffer allocation. +#[cfg(feature = "kernel")] +pub fn alloc_me_bytes(size: u32, name: &[u8]) -> Result, i32> { + // SAFETY: Byte buffers don't need initialization + unsafe { PartitionAlloc::::new_uninit(size, name) } +} diff --git a/psp/src/mp3.rs b/psp/src/mp3.rs new file mode 100644 index 0000000..769fc69 --- /dev/null +++ b/psp/src/mp3.rs @@ -0,0 +1,230 @@ +//! MP3 decoder for the PSP. +//! +//! Wraps the hardware-accelerated `sceMp3*` syscalls for decoding MP3 +//! audio data into PCM samples suitable for playback via [`crate::audio`]. +//! +//! # Example +//! +//! ```ignore +//! use psp::mp3::Mp3Decoder; +//! +//! let data = psp::io::read_to_vec("ms0:/music/song.mp3").unwrap(); +//! let mut decoder = Mp3Decoder::new(&data).unwrap(); +//! +//! while let Ok(samples) = decoder.decode_frame() { +//! if samples.is_empty() { break; } +//! // Feed samples to psp::audio::AudioChannel +//! } +//! ``` + +use crate::sys; +use alloc::vec::Vec; +use core::ffi::c_void; + +/// Error from an MP3 operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Mp3Error(pub i32); + +impl core::fmt::Debug for Mp3Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Mp3Error({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for Mp3Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "mp3 error {:#010x}", self.0 as u32) + } +} + +/// MP3 decoder with RAII resource management. +/// +/// Decodes MP3 data using the PSP's hardware decoder. The MP3 data is +/// provided as a byte slice and must remain valid for the decoder's lifetime. +pub struct Mp3Decoder { + handle: sys::Mp3Handle, + /// MP3 source data (kept alive for the duration of decoding). + _data: Vec, + /// Internal stream buffer used by the MP3 decoder. + mp3_buf: Vec, + /// Internal PCM output buffer. + pcm_buf: Vec, + /// Whether we've finished feeding data. + eof: bool, +} + +/// Size of the internal MP3 stream buffer. +const MP3_BUF_SIZE: usize = 8 * 1024; +/// Size of the internal PCM output buffer (max output per decode call). +const PCM_BUF_SIZE: usize = 4608; // 1152 samples * 2 channels * 2 bytes (as i16 count) + +impl Mp3Decoder { + /// Create a decoder from in-memory MP3 data. + /// + /// Initializes the MP3 resource subsystem, reserves a handle, and + /// feeds the initial data to the decoder. + pub fn new(data: &[u8]) -> Result { + let ret = unsafe { sys::sceMp3InitResource() }; + if ret < 0 { + return Err(Mp3Error(ret)); + } + + let owned_data = Vec::from(data); + let mut mp3_buf = alloc::vec![0u8; MP3_BUF_SIZE]; + let mut pcm_buf = alloc::vec![0i16; PCM_BUF_SIZE]; + + let mut init_arg = sys::SceMp3InitArg { + mp3_stream_start: 0, + unk1: 0, + mp3_stream_end: owned_data.len() as u32, + unk2: 0, + mp3_buf: mp3_buf.as_mut_ptr() as *mut c_void, + mp3_buf_size: MP3_BUF_SIZE as i32, + pcm_buf: pcm_buf.as_mut_ptr() as *mut c_void, + pcm_buf_size: (PCM_BUF_SIZE * 2) as i32, // in bytes + }; + + let handle_id = unsafe { sys::sceMp3ReserveMp3Handle(&mut init_arg) }; + if handle_id < 0 { + unsafe { sys::sceMp3TermResource() }; + return Err(Mp3Error(handle_id)); + } + let handle = sys::Mp3Handle(handle_id); + + let mut decoder = Self { + handle, + _data: owned_data, + mp3_buf, + pcm_buf, + eof: false, + }; + + // Feed initial data. + decoder.feed_data()?; + + // Initialize the decoder. + let ret = unsafe { sys::sceMp3Init(handle) }; + if ret < 0 { + unsafe { + sys::sceMp3ReleaseMp3Handle(handle); + sys::sceMp3TermResource(); + } + return Err(Mp3Error(ret)); + } + + Ok(decoder) + } + + /// Decode the next frame of MP3 data. + /// + /// Returns a slice of interleaved stereo i16 PCM samples. + /// Returns an empty slice when decoding is complete. + pub fn decode_frame(&mut self) -> Result<&[i16], Mp3Error> { + // Feed more data if the decoder needs it. + if !self.eof && unsafe { sys::sceMp3CheckStreamDataNeeded(self.handle) } > 0 { + self.feed_data()?; + } + + let mut out_ptr: *mut i16 = core::ptr::null_mut(); + let ret = unsafe { sys::sceMp3Decode(self.handle, &mut out_ptr) }; + if ret < 0 { + // Negative values other than "no more data" are errors. + // sceMp3Decode returns 0 when no more data. + return Err(Mp3Error(ret)); + } + if ret == 0 || out_ptr.is_null() { + return Ok(&[]); + } + + // ret is the number of bytes decoded. + let sample_count = ret as usize / 2; // i16 samples + Ok(unsafe { core::slice::from_raw_parts(out_ptr, sample_count) }) + } + + /// Get the sampling rate of the MP3 stream. + pub fn sample_rate(&self) -> u32 { + let ret = unsafe { sys::sceMp3GetSamplingRate(self.handle) }; + if ret < 0 { 0 } else { ret as u32 } + } + + /// Get the number of channels (1 = mono, 2 = stereo). + pub fn channels(&self) -> u8 { + let ret = unsafe { sys::sceMp3GetMp3ChannelNum(self.handle) }; + if ret < 0 { 0 } else { ret as u8 } + } + + /// Get the bitrate in kbps. + pub fn bitrate(&self) -> u32 { + let ret = unsafe { sys::sceMp3GetBitRate(self.handle) }; + if ret < 0 { 0 } else { ret as u32 } + } + + /// Set the number of times to loop. -1 = infinite, 0 = no loop. + pub fn set_loop(&mut self, count: i32) { + unsafe { sys::sceMp3SetLoopNum(self.handle, count) }; + } + + /// Reset playback position to the beginning. + pub fn reset(&mut self) -> Result<(), Mp3Error> { + let ret = unsafe { sys::sceMp3ResetPlayPosition(self.handle) }; + if ret < 0 { Err(Mp3Error(ret)) } else { Ok(()) } + } + + /// Feed data from the source buffer into the decoder's stream buffer. + fn feed_data(&mut self) -> Result<(), Mp3Error> { + let mut dst_ptr: *mut u8 = core::ptr::null_mut(); + let mut to_write: i32 = 0; + let mut src_pos: i32 = 0; + + let ret = unsafe { + sys::sceMp3GetInfoToAddStreamData( + self.handle, + &mut dst_ptr, + &mut to_write, + &mut src_pos, + ) + }; + if ret < 0 { + return Err(Mp3Error(ret)); + } + + if to_write <= 0 || dst_ptr.is_null() { + self.eof = true; + return Ok(()); + } + + let src_offset = src_pos as usize; + let available = self._data.len().saturating_sub(src_offset); + let copy_len = (to_write as usize).min(available); + + if copy_len == 0 { + self.eof = true; + let _ = unsafe { sys::sceMp3NotifyAddStreamData(self.handle, 0) }; + return Ok(()); + } + + unsafe { + core::ptr::copy_nonoverlapping(self._data.as_ptr().add(src_offset), dst_ptr, copy_len); + } + + let ret = unsafe { sys::sceMp3NotifyAddStreamData(self.handle, copy_len as i32) }; + if ret < 0 { + return Err(Mp3Error(ret)); + } + + if src_offset + copy_len >= self._data.len() { + self.eof = true; + } + + Ok(()) + } +} + +impl Drop for Mp3Decoder { + fn drop(&mut self) { + unsafe { + sys::sceMp3ReleaseMp3Handle(self.handle); + sys::sceMp3TermResource(); + } + } +} diff --git a/psp/src/net.rs b/psp/src/net.rs new file mode 100644 index 0000000..065673b --- /dev/null +++ b/psp/src/net.rs @@ -0,0 +1,375 @@ +//! Network sockets and WiFi access for the PSP. +//! +//! Provides RAII wrappers around the PSP's networking stack: access +//! point connection, DNS resolution, and TCP/UDP sockets. +//! +//! # Initialization +//! +//! Before using any networking, call [`init`] to set up the network +//! subsystem. Call [`term`] when done. Connect to a WiFi access point +//! with [`connect_ap`]. +//! +//! # Example +//! +//! ```ignore +//! use psp::net; +//! +//! net::init(0x20000).unwrap(); +//! net::connect_ap(1).unwrap(); +//! +//! let ip = net::get_ip_address().unwrap(); +//! psp::dprintln!("IP: {}", core::str::from_utf8(&ip).unwrap_or("?")); +//! +//! let mut stream = net::TcpStream::connect(net::Ipv4Addr([93, 184, 216, 34]), 80).unwrap(); +//! stream.write(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n").unwrap(); +//! +//! let mut buf = [0u8; 1024]; +//! let n = stream.read(&mut buf).unwrap(); +//! ``` + +use core::ffi::c_void; +use core::marker::PhantomData; + +use crate::sys; + +/// Error from a network operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct NetError(pub i32); + +impl core::fmt::Debug for NetError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "NetError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for NetError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "net error {:#010x}", self.0 as u32) + } +} + +/// An IPv4 address in network byte order. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Ipv4Addr(pub [u8; 4]); + +impl Ipv4Addr { + /// Convert to a `u32` in network byte order (big-endian). + pub fn to_u32_be(self) -> u32 { + u32::from_be_bytes(self.0) + } +} + +/// Initialize the PSP network subsystem. +/// +/// `pool_size` is the memory pool size for the networking stack. +/// A typical value is `0x20000` (128 KiB). +pub fn init(pool_size: u32) -> Result<(), NetError> { + let ret = unsafe { sys::sceNetInit(pool_size as i32, 0x20, 0x1000, 0x20, 0x1000) }; + if ret < 0 { + return Err(NetError(ret)); + } + + let ret = unsafe { sys::sceNetInetInit() }; + if ret < 0 { + unsafe { sys::sceNetTerm() }; + return Err(NetError(ret)); + } + + let ret = unsafe { sys::sceNetResolverInit() }; + if ret < 0 { + unsafe { + sys::sceNetInetTerm(); + sys::sceNetTerm(); + } + return Err(NetError(ret)); + } + + let ret = unsafe { sys::sceNetApctlInit(0x1600, 42) }; + if ret < 0 { + unsafe { + sys::sceNetResolverTerm(); + sys::sceNetInetTerm(); + sys::sceNetTerm(); + } + return Err(NetError(ret)); + } + + Ok(()) +} + +/// Terminate the network subsystem. +/// +/// Call when networking is no longer needed. +pub fn term() { + unsafe { + sys::sceNetApctlTerm(); + sys::sceNetResolverTerm(); + sys::sceNetInetTerm(); + sys::sceNetTerm(); + } +} + +/// Connect to a WiFi access point using a stored PSP network config slot. +/// +/// `config_index` is 1-based (matches the PSP's Network Settings list). +/// Blocks until the connection is established or fails. +/// Uses a default timeout of 30 seconds. +pub fn connect_ap(config_index: i32) -> Result<(), NetError> { + connect_ap_timeout(config_index, 30_000) +} + +/// Connect to a WiFi access point with a custom timeout. +/// +/// `config_index` is 1-based (matches the PSP's Network Settings list). +/// `timeout_ms` is the maximum time to wait in milliseconds. +pub fn connect_ap_timeout(config_index: i32, timeout_ms: u32) -> Result<(), NetError> { + let ret = unsafe { sys::sceNetApctlConnect(config_index) }; + if ret < 0 { + return Err(NetError(ret)); + } + + // Poll until we get an IP, hit an error, or time out. + let max_iterations = timeout_ms / 50; + for _ in 0..max_iterations { + let mut state = sys::ApctlState::Disconnected; + let ret = unsafe { sys::sceNetApctlGetState(&mut state) }; + if ret < 0 { + return Err(NetError(ret)); + } + match state { + sys::ApctlState::GotIp => return Ok(()), + sys::ApctlState::Disconnected => return Err(NetError(-1)), + _ => {}, + } + crate::thread::sleep_ms(50); + } + + // Timed out — disconnect and return error. + let _ = unsafe { sys::sceNetApctlDisconnect() }; + Err(NetError(-1)) +} + +/// Disconnect from the current access point. +pub fn disconnect_ap() -> Result<(), NetError> { + let ret = unsafe { sys::sceNetApctlDisconnect() }; + if ret < 0 { Err(NetError(ret)) } else { Ok(()) } +} + +/// Get the IP address assigned to the WLAN interface. +/// +/// Returns a null-terminated string in a 16-byte buffer (e.g. `"192.168.1.42\0"`). +pub fn get_ip_address() -> Result<[u8; 16], NetError> { + let mut info: sys::SceNetApctlInfo = unsafe { core::mem::zeroed() }; + let ret = unsafe { sys::sceNetApctlGetInfo(sys::ApctlInfo::Ip, &mut info) }; + if ret < 0 { + return Err(NetError(ret)); + } + // IP is stored as a string in the `name` field of the union + let mut out = [0u8; 16]; + let src = unsafe { &info.name[..16] }; + out.copy_from_slice(src); + Ok(out) +} + +/// Resolve a hostname to an IPv4 address. +/// +/// `hostname` must be a null-terminated byte string. +pub fn resolve_hostname(hostname: &[u8]) -> Result { + let mut rid: i32 = 0; + let mut buf = [0u8; 1024]; + + let ret = unsafe { + sys::sceNetResolverCreate(&mut rid, buf.as_mut_ptr() as *mut c_void, buf.len() as u32) + }; + if ret < 0 { + return Err(NetError(ret)); + } + + let mut addr = sys::in_addr(0); + let ret = unsafe { sys::sceNetResolverStartNtoA(rid, hostname.as_ptr(), &mut addr, 5, 3) }; + unsafe { sys::sceNetResolverDelete(rid) }; + + if ret < 0 { + return Err(NetError(ret)); + } + + Ok(Ipv4Addr(addr.0.to_be_bytes())) +} + +fn make_sockaddr_in(addr: Ipv4Addr, port: u16) -> sys::sockaddr { + let mut sa = sys::sockaddr { + sa_len: 16, + sa_family: 2, // AF_INET + sa_data: [0u8; 14], + }; + // sockaddr_in layout: family(2) + port(2, big-endian) + addr(4, big-endian) + pad(8) + let port_be = port.to_be_bytes(); + sa.sa_data[0] = port_be[0]; + sa.sa_data[1] = port_be[1]; + sa.sa_data[2] = addr.0[0]; + sa.sa_data[3] = addr.0[1]; + sa.sa_data[4] = addr.0[2]; + sa.sa_data[5] = addr.0[3]; + sa +} + +// ── TcpStream ────────────────────────────────────────────────────── + +/// A TCP stream with RAII socket management. +pub struct TcpStream { + fd: i32, + _marker: PhantomData<*const ()>, // !Send + !Sync +} + +impl TcpStream { + /// Connect to a remote TCP endpoint. + pub fn connect(addr: Ipv4Addr, port: u16) -> Result { + // AF_INET=2, SOCK_STREAM=1, protocol=0 + let fd = unsafe { sys::sceNetInetSocket(2, 1, 0) }; + if fd < 0 { + return Err(NetError(unsafe { sys::sceNetInetGetErrno() })); + } + + let sa = make_sockaddr_in(addr, port); + let ret = unsafe { + sys::sceNetInetConnect(fd, &sa, core::mem::size_of::() as u32) + }; + if ret < 0 { + let errno = unsafe { sys::sceNetInetGetErrno() }; + unsafe { sys::sceNetInetClose(fd) }; + return Err(NetError(errno)); + } + + Ok(Self { + fd, + _marker: PhantomData, + }) + } + + /// Read data from the stream. + /// + /// Returns the number of bytes read. Returns 0 at EOF. + pub fn read(&self, buf: &mut [u8]) -> Result { + let ret = + unsafe { sys::sceNetInetRecv(self.fd, buf.as_mut_ptr() as *mut c_void, buf.len(), 0) }; + if ret < 0 { + Err(NetError(unsafe { sys::sceNetInetGetErrno() })) + } else { + Ok(ret as usize) + } + } + + /// Write data to the stream. + /// + /// Returns the number of bytes written. + pub fn write(&self, buf: &[u8]) -> Result { + let ret = + unsafe { sys::sceNetInetSend(self.fd, buf.as_ptr() as *const c_void, buf.len(), 0) }; + if ret < 0 { + Err(NetError(unsafe { sys::sceNetInetGetErrno() })) + } else { + Ok(ret as usize) + } + } +} + +impl Drop for TcpStream { + fn drop(&mut self) { + unsafe { + sys::sceNetInetClose(self.fd); + } + } +} + +// ── UdpSocket ────────────────────────────────────────────────────── + +/// A UDP socket with RAII management. +pub struct UdpSocket { + fd: i32, + _marker: PhantomData<*const ()>, // !Send + !Sync +} + +impl UdpSocket { + /// Create a UDP socket bound to the given port. + /// + /// Pass `0` to let the OS choose an ephemeral port. + pub fn bind(port: u16) -> Result { + // AF_INET=2, SOCK_DGRAM=2, protocol=0 + let fd = unsafe { sys::sceNetInetSocket(2, 2, 0) }; + if fd < 0 { + return Err(NetError(unsafe { sys::sceNetInetGetErrno() })); + } + + let sa = make_sockaddr_in(Ipv4Addr([0, 0, 0, 0]), port); + let ret = + unsafe { sys::sceNetInetBind(fd, &sa, core::mem::size_of::() as u32) }; + if ret < 0 { + let errno = unsafe { sys::sceNetInetGetErrno() }; + unsafe { sys::sceNetInetClose(fd) }; + return Err(NetError(errno)); + } + + Ok(Self { + fd, + _marker: PhantomData, + }) + } + + /// Send data to a remote UDP endpoint. + pub fn send_to(&self, buf: &[u8], addr: Ipv4Addr, port: u16) -> Result { + let sa = make_sockaddr_in(addr, port); + let ret = unsafe { + sys::sceNetInetSendto( + self.fd, + buf.as_ptr() as *const c_void, + buf.len(), + 0, + &sa, + core::mem::size_of::() as u32, + ) + }; + if ret < 0 { + Err(NetError(unsafe { sys::sceNetInetGetErrno() })) + } else { + Ok(ret as usize) + } + } + + /// Receive data from any remote endpoint. + /// + /// Returns `(bytes_read, sender_addr, sender_port)`. + pub fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, Ipv4Addr, u16), NetError> { + let mut sa = sys::sockaddr { + sa_len: 16, + sa_family: 2, + sa_data: [0u8; 14], + }; + let mut sa_len = core::mem::size_of::() as u32; + + let ret = unsafe { + sys::sceNetInetRecvfrom( + self.fd, + buf.as_mut_ptr() as *mut c_void, + buf.len(), + 0, + &mut sa, + &mut sa_len, + ) + }; + if ret < 0 { + return Err(NetError(unsafe { sys::sceNetInetGetErrno() })); + } + + let port = u16::from_be_bytes([sa.sa_data[0], sa.sa_data[1]]); + let addr = Ipv4Addr([sa.sa_data[2], sa.sa_data[3], sa.sa_data[4], sa.sa_data[5]]); + Ok((ret as usize, addr, port)) + } +} + +impl Drop for UdpSocket { + fn drop(&mut self) { + unsafe { + sys::sceNetInetClose(self.fd); + } + } +} diff --git a/psp/src/osk.rs b/psp/src/osk.rs new file mode 100644 index 0000000..0d765ad --- /dev/null +++ b/psp/src/osk.rs @@ -0,0 +1,189 @@ +//! On-Screen Keyboard (OSK) for text input on the PSP. +//! +//! Wraps `sceUtilityOsk*` to provide a safe API for displaying the +//! system keyboard and capturing user text input. +//! +//! # Example +//! +//! ```ignore +//! use psp::osk; +//! +//! if let Ok(Some(text)) = osk::text_input("Enter your name:", 32) { +//! psp::dprintln!("Hello, {}!", text); +//! } +//! ``` + +use alloc::string::String; +use alloc::vec::Vec; + +use crate::sys::{ + SceUtilityOskData, SceUtilityOskInputLanguage, SceUtilityOskInputType, SceUtilityOskParams, + SceUtilityOskResult, SystemParamLanguage, UtilityDialogButtonAccept, UtilityDialogCommon, +}; + +/// Error from an OSK operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct OskError(pub i32); + +impl core::fmt::Debug for OskError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "OskError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for OskError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "osk error {:#010x}", self.0 as u32) + } +} + +/// Standard thread priorities for utility dialogs. +const GRAPHICS_THREAD: i32 = 0x11; +const ACCESS_THREAD: i32 = 0x13; +const FONT_THREAD: i32 = 0x12; +const SOUND_THREAD: i32 = 0x10; + +/// Maximum iterations for OSK polling (~30 seconds at 60 fps). +const MAX_OSK_ITERATIONS: u32 = 1800; + +fn make_common(size: u32) -> UtilityDialogCommon { + UtilityDialogCommon { + size, + language: SystemParamLanguage::English, + button_accept: UtilityDialogButtonAccept::Cross, + graphics_thread: GRAPHICS_THREAD, + access_thread: ACCESS_THREAD, + font_thread: FONT_THREAD, + sound_thread: SOUND_THREAD, + result: 0, + reserved: [0i32; 4], + } +} + +/// Show a simple text input dialog and return the entered text. +/// +/// Returns `Ok(Some(text))` if the user confirmed, `Ok(None)` if cancelled, +/// or `Err` on failure. +pub fn text_input(prompt: &str, max_chars: usize) -> Result, OskError> { + OskBuilder::new(prompt).max_chars(max_chars).show() +} + +/// Builder for customized OSK dialogs. +pub struct OskBuilder { + prompt_utf16: Vec, + initial_utf16: Vec, + max_chars: usize, + input_type: SceUtilityOskInputType, + language: SceUtilityOskInputLanguage, +} + +impl OskBuilder { + /// Create a new OSK builder with the given prompt text. + pub fn new(prompt: &str) -> Self { + Self { + prompt_utf16: str_to_utf16(prompt), + initial_utf16: alloc::vec![0u16], + max_chars: 128, + input_type: SceUtilityOskInputType::All, + language: SceUtilityOskInputLanguage::Default, + } + } + + /// Set the maximum number of characters the user can enter. + pub fn max_chars(mut self, max: usize) -> Self { + self.max_chars = max; + self + } + + /// Set initial text in the input field. + pub fn initial_text(mut self, text: &str) -> Self { + self.initial_utf16 = str_to_utf16(text); + self + } + + /// Set the input language. + pub fn language(mut self, lang: SceUtilityOskInputLanguage) -> Self { + self.language = lang; + self + } + + /// Set the input type (filter what characters are allowed). + pub fn input_type(mut self, input_type: SceUtilityOskInputType) -> Self { + self.input_type = input_type; + self + } + + /// Show the OSK dialog and block until the user responds. + /// + /// Returns `Ok(Some(text))` if the user confirmed input, + /// `Ok(None)` if cancelled, or `Err` on failure. + pub fn show(mut self) -> Result, OskError> { + let mut output_buf = alloc::vec![0u16; self.max_chars + 1]; + + let mut osk_data = SceUtilityOskData { + unk_00: 0, + unk_04: 0, + language: self.language, + unk_12: 0, + inputtype: self.input_type, + lines: 1, + unk_24: 0, + desc: self.prompt_utf16.as_mut_ptr(), + intext: self.initial_utf16.as_mut_ptr(), + outtextlength: output_buf.len() as i32, + outtext: output_buf.as_mut_ptr(), + result: SceUtilityOskResult::Unchanged, + outtextlimit: self.max_chars as i32, + }; + + let mut params = SceUtilityOskParams { + base: make_common(core::mem::size_of::() as u32), + datacount: 1, + data: &mut osk_data, + state: crate::sys::PspUtilityDialogState::None, + unk_60: 0, + }; + + let ret = + unsafe { crate::sys::sceUtilityOskInitStart(&mut params as *mut SceUtilityOskParams) }; + if ret < 0 { + return Err(OskError(ret)); + } + + for _ in 0..MAX_OSK_ITERATIONS { + let status = unsafe { crate::sys::sceUtilityOskGetStatus() }; + match status { + 2 => { + unsafe { crate::sys::sceUtilityOskUpdate(1) }; + }, + 3 => { + unsafe { crate::sys::sceUtilityOskShutdownStart() }; + }, + 0 => break, + _ => {}, + } + unsafe { crate::sys::sceDisplayWaitVblankStart() }; + } + + match osk_data.result { + SceUtilityOskResult::Changed => { + let text = utf16_to_string(&output_buf); + Ok(Some(text)) + }, + _ => Ok(None), + } + } +} + +/// Convert a &str to a null-terminated UTF-16 Vec. +fn str_to_utf16(s: &str) -> Vec { + let mut buf: Vec = s.encode_utf16().collect(); + buf.push(0); + buf +} + +/// Convert a null-terminated UTF-16 buffer to a String. +fn utf16_to_string(buf: &[u16]) -> String { + let end = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); + String::from_utf16_lossy(&buf[..end]) +} diff --git a/psp/src/power.rs b/psp/src/power.rs new file mode 100644 index 0000000..d02239b --- /dev/null +++ b/psp/src/power.rs @@ -0,0 +1,205 @@ +//! Power and clock management for the PSP. +//! +//! Provides clock speed control, battery monitoring, AC power detection, +//! power event callbacks, and idle-timer control. Wraps `scePower*` +//! syscalls into safe, ergonomic functions. + +/// CPU and bus clock frequencies in MHz. +#[derive(Debug, Clone, Copy)] +pub struct ClockFrequency { + pub cpu_mhz: i32, + pub bus_mhz: i32, +} + +/// Error from a power operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PowerError(pub i32); + +impl core::fmt::Debug for PowerError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "PowerError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for PowerError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "power error {:#010x}", self.0 as u32) + } +} + +/// Battery status information. +#[derive(Debug, Clone, Copy)] +pub struct BatteryInfo { + /// Whether the battery is currently charging. + pub is_charging: bool, + /// Whether a battery is physically present. + pub is_present: bool, + /// Whether the battery level is low. + pub is_low: bool, + /// Battery charge percentage (0-100), or -1 on error. + pub percent: i32, + /// Estimated remaining battery life in minutes, or -1 on error. + pub lifetime_minutes: i32, + /// Battery voltage in millivolts. + pub voltage_mv: i32, + /// Battery temperature (units depend on PSP firmware). + pub temperature: i32, +} + +/// Get the current CPU and bus clock frequencies. +pub fn get_clock() -> ClockFrequency { + ClockFrequency { + cpu_mhz: unsafe { crate::sys::scePowerGetCpuClockFrequency() }, + bus_mhz: unsafe { crate::sys::scePowerGetBusClockFrequency() }, + } +} + +/// Set the CPU and bus clock frequencies. +/// +/// `cpu_mhz`: 1-333, `bus_mhz`: 1-166. +/// The PLL frequency is set equal to `cpu_mhz`. +/// +/// Returns the new clock frequencies on success. +pub fn set_clock(cpu_mhz: i32, bus_mhz: i32) -> Result { + let ret = unsafe { crate::sys::scePowerSetClockFrequency(cpu_mhz, cpu_mhz, bus_mhz) }; + if ret < 0 { + return Err(PowerError(ret)); + } + Ok(get_clock()) +} + +/// Set CPU, bus, and GPU clock frequencies independently. +/// +/// `cpu`: 1-333, `bus`: 1-166, `gpu` (PLL): 19-333. +/// Constraints: `cpu <= gpu`, `bus*2 <= gpu`. +pub fn set_clock_frequency(cpu: i32, bus: i32, gpu: i32) -> Result<(), PowerError> { + let ret = unsafe { crate::sys::scePowerSetClockFrequency(gpu, cpu, bus) }; + if ret < 0 { + Err(PowerError(ret)) + } else { + Ok(()) + } +} + +/// Query battery status in a single call. +pub fn battery_info() -> BatteryInfo { + BatteryInfo { + is_charging: unsafe { crate::sys::scePowerIsBatteryCharging() } == 1, + is_present: unsafe { crate::sys::scePowerIsBatteryExist() } == 1, + is_low: unsafe { crate::sys::scePowerIsLowBattery() } == 1, + percent: unsafe { crate::sys::scePowerGetBatteryLifePercent() }, + lifetime_minutes: unsafe { crate::sys::scePowerGetBatteryLifeTime() }, + voltage_mv: unsafe { crate::sys::scePowerGetBatteryVolt() }, + temperature: unsafe { crate::sys::scePowerGetBatteryTemp() }, + } +} + +/// Check if the PSP is running on AC (mains) power. +pub fn is_ac_power() -> bool { + (unsafe { crate::sys::scePowerIsPowerOnline() }) == 1 +} + +// ── Power event callbacks ──────────────────────────────────────────── + +/// Register a power event callback. +/// +/// Spawns a callback thread that sleeps with callback processing enabled. +/// The `handler` is called when power events occur (suspend, resume, AC +/// state changes, battery level changes, etc.). +/// +/// The handler signature matches `sceKernelCreateCallback`'s expected +/// callback: `fn(count: i32, power_info: i32, common: *mut c_void) -> i32`. +/// The `power_info` parameter contains [`crate::sys::PowerInfo`] flags. +/// +/// Returns a handle that unregisters the callback on drop. +#[cfg(not(feature = "stub-only"))] +pub fn on_power_event( + handler: unsafe extern "C" fn(i32, i32, *mut core::ffi::c_void) -> i32, +) -> Result { + use core::ffi::c_void; + + let cbid = unsafe { + crate::sys::sceKernelCreateCallback(b"power_cb\0".as_ptr(), handler, core::ptr::null_mut()) + }; + if cbid.0 < 0 { + return Err(PowerError(cbid.0)); + } + + let slot = unsafe { crate::sys::scePowerRegisterCallback(-1, cbid) }; + if slot < 0 { + return Err(PowerError(slot)); + } + + // Spawn a thread that sleeps with CB processing enabled. + unsafe extern "C" fn sleep_thread(_args: usize, _argp: *mut c_void) -> i32 { + unsafe { crate::sys::sceKernelSleepThreadCB() }; + 0 + } + + let thid = unsafe { + crate::sys::sceKernelCreateThread( + b"power_cb_thread\0".as_ptr(), + sleep_thread, + crate::DEFAULT_THREAD_PRIORITY, + 4096, + crate::sys::ThreadAttributes::empty(), + core::ptr::null_mut(), + ) + }; + if thid.0 < 0 { + unsafe { + crate::sys::scePowerUnregisterCallback(slot); + crate::sys::sceKernelDeleteCallback(cbid); + } + return Err(PowerError(thid.0)); + } + + let ret = unsafe { crate::sys::sceKernelStartThread(thid, 0, core::ptr::null_mut()) }; + if ret < 0 { + unsafe { + crate::sys::scePowerUnregisterCallback(slot); + crate::sys::sceKernelDeleteThread(thid); + crate::sys::sceKernelDeleteCallback(cbid); + } + return Err(PowerError(ret)); + } + + Ok(PowerCallbackHandle { + slot, + cb_id: cbid, + thread_id: thid, + }) +} + +/// RAII handle for a registered power callback. +/// +/// Unregisters the callback and terminates the background thread on drop. +#[cfg(not(feature = "stub-only"))] +pub struct PowerCallbackHandle { + slot: i32, + cb_id: crate::sys::SceUid, + thread_id: crate::sys::SceUid, +} + +#[cfg(not(feature = "stub-only"))] +impl Drop for PowerCallbackHandle { + fn drop(&mut self) { + unsafe { + crate::sys::scePowerUnregisterCallback(self.slot); + crate::sys::sceKernelTerminateDeleteThread(self.thread_id); + crate::sys::sceKernelDeleteCallback(self.cb_id); + } + } +} + +/// Reset the idle timer to prevent the PSP from auto-sleeping. +/// +/// Call this once per frame in your main loop. +pub fn prevent_sleep() { + unsafe { crate::sys::scePowerTick(crate::sys::PowerTick::All) }; +} + +/// Reset the display idle timer to prevent the screen from turning off. +pub fn prevent_display_off() { + unsafe { crate::sys::scePowerTick(crate::sys::PowerTick::Display) }; +} diff --git a/psp/src/rtc.rs b/psp/src/rtc.rs new file mode 100644 index 0000000..4fce3fb --- /dev/null +++ b/psp/src/rtc.rs @@ -0,0 +1,241 @@ +//! Extended real-time clock operations for the PSP. +//! +//! Provides tick arithmetic, date validation, RFC 3339 formatting/parsing, +//! and UTC/local time conversion. Builds on the basic types in [`crate::time`]. +//! +//! # Example +//! +//! ```ignore +//! use psp::rtc::Tick; +//! +//! let now = Tick::now().unwrap(); +//! let later = now.add_seconds(60).unwrap(); +//! let dt = later.to_datetime().unwrap(); +//! psp::dprintln!("{}-{:02}-{:02}", dt.year(), dt.month(), dt.day()); +//! ``` + +use crate::sys; +use crate::time::DateTime; + +/// Error from an RTC operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct RtcError(pub i32); + +impl core::fmt::Debug for RtcError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "RtcError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for RtcError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "rtc error {:#010x}", self.0 as u32) + } +} + +/// A raw RTC tick value (microseconds since epoch). +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Tick(pub u64); + +impl Tick { + /// Get the current tick. + pub fn now() -> Result { + let mut tick: u64 = 0; + let ret = unsafe { sys::sceRtcGetCurrentTick(&mut tick) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Self(tick)) + } + } + + /// Add microseconds. + pub fn add_micros(self, us: u64) -> Result { + let mut dest: u64 = 0; + let ret = unsafe { sys::sceRtcTickAddMicroseconds(&mut dest, &self.0, us) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Self(dest)) + } + } + + /// Add seconds. + pub fn add_seconds(self, secs: u64) -> Result { + let mut dest: u64 = 0; + let ret = unsafe { sys::sceRtcTickAddSeconds(&mut dest, &self.0, secs) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Self(dest)) + } + } + + /// Add minutes. + pub fn add_minutes(self, mins: u64) -> Result { + let mut dest: u64 = 0; + let ret = unsafe { sys::sceRtcTickAddMinutes(&mut dest, &self.0, mins) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Self(dest)) + } + } + + /// Add hours. + pub fn add_hours(self, hours: i32) -> Result { + let mut dest: u64 = 0; + let ret = unsafe { sys::sceRtcTickAddHours(&mut dest, &self.0, hours) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Self(dest)) + } + } + + /// Add days. + pub fn add_days(self, days: i32) -> Result { + let mut dest: u64 = 0; + let ret = unsafe { sys::sceRtcTickAddDays(&mut dest, &self.0, days) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Self(dest)) + } + } + + /// Add weeks. + pub fn add_weeks(self, weeks: i32) -> Result { + let mut dest: u64 = 0; + let ret = unsafe { sys::sceRtcTickAddWeeks(&mut dest, &self.0, weeks) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Self(dest)) + } + } + + /// Add months. + pub fn add_months(self, months: i32) -> Result { + let mut dest: u64 = 0; + let ret = unsafe { sys::sceRtcTickAddMonths(&mut dest, &self.0, months) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Self(dest)) + } + } + + /// Add years. + pub fn add_years(self, years: i32) -> Result { + let mut dest: u64 = 0; + let ret = unsafe { sys::sceRtcTickAddYears(&mut dest, &self.0, years) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Self(dest)) + } + } + + /// Convert this tick to a [`DateTime`]. + pub fn to_datetime(self) -> Result { + let mut dt = sys::ScePspDateTime::default(); + let ret = unsafe { sys::sceRtcSetTick(&mut dt, &self.0) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(DateTime::from_raw(dt)) + } + } + + /// Compare two ticks. Returns -1, 0, or 1. + pub fn compare(self, other: Tick) -> i32 { + unsafe { sys::sceRtcCompareTick(&self.0, &other.0) } + } +} + +/// Convert a [`DateTime`] to a [`Tick`]. +pub fn datetime_to_tick(dt: &DateTime) -> Result { + let mut tick: u64 = 0; + let ret = unsafe { sys::sceRtcGetTick(dt.as_raw(), &mut tick) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Tick(tick)) + } +} + +/// Format a tick as an RFC 3339 string. +/// +/// Returns a null-terminated string in a 32-byte buffer. +/// `tz_minutes` is the timezone offset from UTC in minutes. +pub fn format_rfc3339(tick: &Tick, tz_minutes: i32) -> Result<[u8; 32], RtcError> { + let mut buf = [0u8; 32]; + let ret = unsafe { sys::sceRtcFormatRFC3339(buf.as_mut_ptr(), &tick.0, tz_minutes) }; + if ret < 0 { Err(RtcError(ret)) } else { Ok(buf) } +} + +/// Format a tick as an RFC 3339 string using local time. +pub fn format_rfc3339_local(tick: &Tick) -> Result<[u8; 32], RtcError> { + let mut buf = [0u8; 32]; + let ret = unsafe { sys::sceRtcFormatRFC3339LocalTime(buf.as_mut_ptr(), &tick.0) }; + if ret < 0 { Err(RtcError(ret)) } else { Ok(buf) } +} + +/// Parse an RFC 3339 date string into a tick. +/// +/// `s` must be a null-terminated byte string. +pub fn parse_rfc3339(s: &[u8]) -> Result { + let mut tick: u64 = 0; + let ret = unsafe { sys::sceRtcParseRFC3339(&mut tick, s.as_ptr()) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Tick(tick)) + } +} + +/// Convert a UTC tick to local time. +pub fn to_local(utc_tick: &Tick) -> Result { + let mut local: u64 = 0; + let ret = unsafe { sys::sceRtcConvertUtcToLocalTime(&utc_tick.0, &mut local) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Tick(local)) + } +} + +/// Convert a local-time tick to UTC. +pub fn to_utc(local_tick: &Tick) -> Result { + let mut utc: u64 = 0; + let ret = unsafe { sys::sceRtcConvertLocalTimeToUTC(&local_tick.0, &mut utc) }; + if ret < 0 { + Err(RtcError(ret)) + } else { + Ok(Tick(utc)) + } +} + +/// Get the number of days in the given month (1-12). +pub fn days_in_month(year: i32, month: i32) -> i32 { + unsafe { sys::sceRtcGetDaysInMonth(year, month) } +} + +/// Get the day of week (0=Monday, 6=Sunday). +pub fn day_of_week(year: i32, month: i32, day: i32) -> i32 { + unsafe { sys::sceRtcGetDayOfWeek(year, month, day) } +} + +/// Check if the given year is a leap year. +pub fn is_leap_year(year: i32) -> bool { + (unsafe { sys::sceRtcIsLeapYear(year) }) != 0 +} + +/// Validate a DateTime's fields. +/// +/// Returns `Ok(())` if valid, or `Err` with the error code. +pub fn check_valid(dt: &DateTime) -> Result<(), RtcError> { + let ret = unsafe { sys::sceRtcCheckValid(dt.as_raw()) }; + if ret < 0 { Err(RtcError(ret)) } else { Ok(()) } +} diff --git a/psp/src/savedata.rs b/psp/src/savedata.rs new file mode 100644 index 0000000..9271d21 --- /dev/null +++ b/psp/src/savedata.rs @@ -0,0 +1,189 @@ +//! Savedata utility for the PSP. +//! +//! Wraps `sceUtilitySavedata*` to provide a safe, builder-pattern API +//! for saving and loading game data via the PSP's standard save dialog. +//! +//! # Example +//! +//! ```ignore +//! use psp::savedata::Savedata; +//! +//! // Save +//! let data = b"hello world"; +//! Savedata::new(b"MYAPP00000\0\0\0") +//! .title("My Save") +//! .save(b"SAVE0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", data) +//! .unwrap(); +//! +//! // Load +//! let loaded = Savedata::new(b"MYAPP00000\0\0\0") +//! .load(b"SAVE0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 1024) +//! .unwrap(); +//! ``` + +use alloc::vec::Vec; +use core::ffi::c_void; + +use crate::sys::{ + SceUtilitySavedataParam, SystemParamLanguage, UtilityDialogButtonAccept, UtilityDialogCommon, + UtilitySavedataFocus, UtilitySavedataMode, UtilitySavedataSFOParam, +}; + +/// Error from a savedata operation. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct SavedataError(pub i32); + +impl core::fmt::Debug for SavedataError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "SavedataError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for SavedataError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "savedata error {:#010x}", self.0 as u32) + } +} + +/// Standard thread priorities for utility dialogs. +const GRAPHICS_THREAD: i32 = 0x11; +const ACCESS_THREAD: i32 = 0x13; +const FONT_THREAD: i32 = 0x12; +const SOUND_THREAD: i32 = 0x10; + +/// Maximum iterations for savedata polling (~30 seconds at 60 fps). +const MAX_SAVEDATA_ITERATIONS: u32 = 1800; + +fn make_common() -> UtilityDialogCommon { + UtilityDialogCommon { + size: core::mem::size_of::() as u32, + language: SystemParamLanguage::English, + button_accept: UtilityDialogButtonAccept::Cross, + graphics_thread: GRAPHICS_THREAD, + access_thread: ACCESS_THREAD, + font_thread: FONT_THREAD, + sound_thread: SOUND_THREAD, + result: 0, + reserved: [0i32; 4], + } +} + +/// Builder for savedata operations. +pub struct Savedata { + game_name: [u8; 13], + title: [u8; 128], + detail: [u8; 1024], +} + +impl Savedata { + /// Create a new savedata builder. + /// + /// `game_name` must be exactly 13 bytes (e.g., `b"MYAPP00000\0\0\0"`), + /// matching the game's product code registered with SCE. + pub fn new(game_name: &[u8; 13]) -> Self { + Self { + game_name: *game_name, + title: [0u8; 128], + detail: [0u8; 1024], + } + } + + /// Set the save title (shown in the save dialog). + pub fn title(mut self, title: &str) -> Self { + let len = title.len().min(127); + self.title[..len].copy_from_slice(&title.as_bytes()[..len]); + self + } + + /// Set the save detail text (shown in the save dialog). + pub fn detail(mut self, detail: &str) -> Self { + let len = detail.len().min(1023); + self.detail[..len].copy_from_slice(&detail.as_bytes()[..len]); + self + } + + /// Save data to the specified save slot. + /// + /// `save_name` must be exactly 20 bytes (null-padded). + /// `data` is the raw bytes to save. + pub fn save(&self, save_name: &[u8; 20], data: &[u8]) -> Result<(), SavedataError> { + let mut data_buf = Vec::from(data); + + let mut sfo = UtilitySavedataSFOParam { + title: self.title, + savedata_title: [0u8; 128], + detail: self.detail, + parental_level: 0, + unknown: [0u8; 3], + }; + + let mut params: SceUtilitySavedataParam = unsafe { core::mem::zeroed() }; + params.base = make_common(); + params.mode = UtilitySavedataMode::AutoSave; + params.game_name = self.game_name; + params.save_name = *save_name; + params.file_name = *b"DATA.BIN\0\0\0\0\0"; + params.data_buf = data_buf.as_mut_ptr() as *mut c_void; + params.data_buf_size = data_buf.len(); + params.data_size = data_buf.len(); + params.sfo_param = sfo; + params.focus = UtilitySavedataFocus::Latest; + + self.run_savedata(&mut params) + } + + /// Load data from the specified save slot. + /// + /// `save_name` must be exactly 20 bytes (null-padded). + /// `max_size` is the maximum expected data size. + pub fn load(&self, save_name: &[u8; 20], max_size: usize) -> Result, SavedataError> { + let mut data_buf = alloc::vec![0u8; max_size]; + + let mut params: SceUtilitySavedataParam = unsafe { core::mem::zeroed() }; + params.base = make_common(); + params.mode = UtilitySavedataMode::AutoLoad; + params.game_name = self.game_name; + params.save_name = *save_name; + params.file_name = *b"DATA.BIN\0\0\0\0\0"; + params.data_buf = data_buf.as_mut_ptr() as *mut c_void; + params.data_buf_size = data_buf.len(); + params.data_size = 0; + params.focus = UtilitySavedataFocus::Latest; + + self.run_savedata(&mut params)?; + + let actual_size = params.data_size.min(max_size); + data_buf.truncate(actual_size); + Ok(data_buf) + } + + fn run_savedata(&self, params: &mut SceUtilitySavedataParam) -> Result<(), SavedataError> { + let ret = unsafe { + crate::sys::sceUtilitySavedataInitStart(params as *mut SceUtilitySavedataParam) + }; + if ret < 0 { + return Err(SavedataError(ret)); + } + + for _ in 0..MAX_SAVEDATA_ITERATIONS { + let status = unsafe { crate::sys::sceUtilitySavedataGetStatus() }; + match status { + 2 => { + unsafe { crate::sys::sceUtilitySavedataUpdate(1) }; + }, + 3 => { + unsafe { crate::sys::sceUtilitySavedataShutdownStart() }; + }, + 0 => break, + _ => {}, + } + unsafe { crate::sys::sceDisplayWaitVblankStart() }; + } + + if params.base.result < 0 { + return Err(SavedataError(params.base.result)); + } + + Ok(()) + } +} diff --git a/psp/src/simd.rs b/psp/src/simd.rs new file mode 100644 index 0000000..047a46a --- /dev/null +++ b/psp/src/simd.rs @@ -0,0 +1,595 @@ +//! VFPU-accelerated SIMD math library for PSP. +//! +//! The PSP's Vector Floating Point Unit (VFPU) provides hardware-accelerated +//! operations on vectors (2/3/4 component) and 4x4 matrices. This module +//! exposes ready-to-use math functions built on top of the raw `vfpu_asm!` +//! macro. +//! +//! # Categories +//! +//! - **Vector operations**: lerp, dot product, normalize, cross product +//! - **Matrix operations**: multiply, transpose, transform +//! - **Color operations**: RGBA blending, HSV↔RGB conversion +//! - **Easing functions**: Quadratic, cubic, spring-damped interpolation + +// ── Vector Types ──────────────────────────────────────────────────── + +/// A 4-component f32 vector, 16-byte aligned for VFPU register loads. +#[repr(C, align(16))] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Vec4(pub [f32; 4]); + +impl Vec4 { + pub const ZERO: Self = Self([0.0, 0.0, 0.0, 0.0]); + pub const ONE: Self = Self([1.0, 1.0, 1.0, 1.0]); + + pub const fn new(x: f32, y: f32, z: f32, w: f32) -> Self { + Self([x, y, z, w]) + } + + pub fn x(&self) -> f32 { + self.0[0] + } + pub fn y(&self) -> f32 { + self.0[1] + } + pub fn z(&self) -> f32 { + self.0[2] + } + pub fn w(&self) -> f32 { + self.0[3] + } +} + +/// A 4x4 f32 matrix, 16-byte aligned for VFPU matrix loads. +/// Stored in column-major order (matches OpenGL and GU conventions). +#[repr(C, align(16))] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Mat4(pub [[f32; 4]; 4]); + +impl Mat4 { + pub const IDENTITY: Self = Self([ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ]); + + pub const ZERO: Self = Self([[0.0; 4]; 4]); +} + +// ── Vector Operations ─────────────────────────────────────────────── + +/// Linearly interpolate between two Vec4 values. +/// +/// `t = 0.0` returns `a`, `t = 1.0` returns `b`. +/// Uses VFPU for all four components simultaneously. +pub fn vec4_lerp(a: &Vec4, b: &Vec4, t: f32) -> Vec4 { + let mut out = Vec4::ZERO; + let a_ptr = a.0.as_ptr(); + let b_ptr = b.0.as_ptr(); + let t_bits = t.to_bits(); + let out_ptr = out.0.as_mut_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({a_ptr})", + "lv.q C010, 0({b_ptr})", + "mtv {t_bits}, S020", + // out = a + t * (b - a) + "vsub.q C010, C010, C000", // C010 = b - a + "vscl.q C010, C010, S020", // C010 = t * (b - a) + "vadd.q C000, C000, C010", // C000 = a + t * (b - a) + "sv.q C000, 0({out_ptr})", + a_ptr = in(reg) a_ptr, + b_ptr = in(reg) b_ptr, + t_bits = in(reg) t_bits, + out_ptr = in(reg) out_ptr, + options(nostack), + ); + } + out +} + +/// Compute the dot product of two Vec4 values. +pub fn vec4_dot(a: &Vec4, b: &Vec4) -> f32 { + let result: f32; + let a_ptr = a.0.as_ptr(); + let b_ptr = b.0.as_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({a_ptr})", + "lv.q C010, 0({b_ptr})", + "vdot.q S020, C000, C010", + "mfv {tmp}, S020", + "mtc1 {tmp}, {fout}", + "nop", + a_ptr = in(reg) a_ptr, + b_ptr = in(reg) b_ptr, + tmp = out(reg) _, + fout = out(freg) result, + options(nostack), + ); + } + result +} + +/// Normalize a Vec4 (make unit length). +/// +/// Behavior is undefined for zero-length vectors (may return NaN or infinity). +pub fn vec4_normalize(v: &Vec4) -> Vec4 { + let mut out = Vec4::ZERO; + let v_ptr = v.0.as_ptr(); + let out_ptr = out.0.as_mut_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({v_ptr})", + "vdot.q S010, C000, C000", // S010 = dot(v, v) + "vrsq.s S010, S010", // S010 = 1/sqrt(dot) + "vscl.q C000, C000, S010", // scale by 1/length + "sv.q C000, 0({out_ptr})", + v_ptr = in(reg) v_ptr, + out_ptr = in(reg) out_ptr, + options(nostack), + ); + } + out +} + +/// Add two Vec4 values component-wise. +pub fn vec4_add(a: &Vec4, b: &Vec4) -> Vec4 { + let mut out = Vec4::ZERO; + let a_ptr = a.0.as_ptr(); + let b_ptr = b.0.as_ptr(); + let out_ptr = out.0.as_mut_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({a_ptr})", + "lv.q C010, 0({b_ptr})", + "vadd.q C000, C000, C010", + "sv.q C000, 0({out_ptr})", + a_ptr = in(reg) a_ptr, + b_ptr = in(reg) b_ptr, + out_ptr = in(reg) out_ptr, + options(nostack), + ); + } + out +} + +/// Subtract two Vec4 values component-wise (a - b). +pub fn vec4_sub(a: &Vec4, b: &Vec4) -> Vec4 { + let mut out = Vec4::ZERO; + let a_ptr = a.0.as_ptr(); + let b_ptr = b.0.as_ptr(); + let out_ptr = out.0.as_mut_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({a_ptr})", + "lv.q C010, 0({b_ptr})", + "vsub.q C000, C000, C010", + "sv.q C000, 0({out_ptr})", + a_ptr = in(reg) a_ptr, + b_ptr = in(reg) b_ptr, + out_ptr = in(reg) out_ptr, + options(nostack), + ); + } + out +} + +/// Scale a Vec4 by a scalar value. +pub fn vec4_scale(v: &Vec4, s: f32) -> Vec4 { + let mut out = Vec4::ZERO; + let v_ptr = v.0.as_ptr(); + let s_bits = s.to_bits(); + let out_ptr = out.0.as_mut_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({v_ptr})", + "mtv {s_bits}, S010", + "vscl.q C000, C000, S010", + "sv.q C000, 0({out_ptr})", + v_ptr = in(reg) v_ptr, + s_bits = in(reg) s_bits, + out_ptr = in(reg) out_ptr, + options(nostack), + ); + } + out +} + +/// Compute the length (magnitude) of a Vec4. +pub fn vec4_length(v: &Vec4) -> f32 { + let result: f32; + let v_ptr = v.0.as_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({v_ptr})", + "vdot.q S010, C000, C000", + "vsqrt.s S010, S010", + "mfv {tmp}, S010", + "mtc1 {tmp}, {fout}", + "nop", + v_ptr = in(reg) v_ptr, + tmp = out(reg) _, + fout = out(freg) result, + options(nostack), + ); + } + result +} + +/// Compute the distance between two Vec4 points. +pub fn vec4_distance(a: &Vec4, b: &Vec4) -> f32 { + let result: f32; + let a_ptr = a.0.as_ptr(); + let b_ptr = b.0.as_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({a_ptr})", + "lv.q C010, 0({b_ptr})", + "vsub.q C000, C000, C010", + "vdot.q S020, C000, C000", + "vsqrt.s S020, S020", + "mfv {tmp}, S020", + "mtc1 {tmp}, {fout}", + "nop", + a_ptr = in(reg) a_ptr, + b_ptr = in(reg) b_ptr, + tmp = out(reg) _, + fout = out(freg) result, + options(nostack), + ); + } + result +} + +/// Compute the cross product of two 3D vectors (w component set to 0). +pub fn vec3_cross(a: &Vec4, b: &Vec4) -> Vec4 { + let mut out = Vec4::ZERO; + let a_ptr = a.0.as_ptr(); + let b_ptr = b.0.as_ptr(); + let out_ptr = out.0.as_mut_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({a_ptr})", + "lv.q C010, 0({b_ptr})", + "vcrsp.t C020, C000, C010", + "vzero.s S023", // w = 0 + "sv.q C020, 0({out_ptr})", + a_ptr = in(reg) a_ptr, + b_ptr = in(reg) b_ptr, + out_ptr = in(reg) out_ptr, + options(nostack), + ); + } + out +} + +// ── Matrix Operations ─────────────────────────────────────────────── + +/// Multiply two 4x4 matrices. +/// +/// Returns `a * b` (in column-major order, matching OpenGL conventions). +pub fn mat4_multiply(a: &Mat4, b: &Mat4) -> Mat4 { + let mut out = Mat4::ZERO; + let a_ptr = a.0.as_ptr(); + let b_ptr = b.0.as_ptr(); + let out_ptr = out.0.as_mut_ptr(); + unsafe { + vfpu_asm!( + // Load matrix A into M000-M003 (columns 0-3) + "lv.q C000, 0({a_ptr})", + "lv.q C010, 16({a_ptr})", + "lv.q C020, 32({a_ptr})", + "lv.q C030, 48({a_ptr})", + // Load matrix B into M100-M103 + "lv.q C100, 0({b_ptr})", + "lv.q C110, 16({b_ptr})", + "lv.q C120, 32({b_ptr})", + "lv.q C130, 48({b_ptr})", + // Multiply: M200 = M000 * M100 + "vmmul.q M200, M000, M100", + // Store result + "sv.q C200, 0({out_ptr})", + "sv.q C210, 16({out_ptr})", + "sv.q C220, 32({out_ptr})", + "sv.q C230, 48({out_ptr})", + a_ptr = in(reg) a_ptr, + b_ptr = in(reg) b_ptr, + out_ptr = in(reg) out_ptr, + options(nostack), + ); + } + out +} + +/// Transpose a 4x4 matrix. +pub fn mat4_transpose(m: &Mat4) -> Mat4 { + let mut out = Mat4::ZERO; + let m_ptr = m.0.as_ptr(); + let out_ptr = out.0.as_mut_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({m_ptr})", + "lv.q C010, 16({m_ptr})", + "lv.q C020, 32({m_ptr})", + "lv.q C030, 48({m_ptr})", + // Transpose: rows become columns + "sv.q R000, 0({out_ptr})", + "sv.q R001, 16({out_ptr})", + "sv.q R002, 32({out_ptr})", + "sv.q R003, 48({out_ptr})", + m_ptr = in(reg) m_ptr, + out_ptr = in(reg) out_ptr, + options(nostack), + ); + } + out +} + +/// Transform a Vec4 by a Mat4 (matrix * vector). +pub fn mat4_transform(m: &Mat4, v: &Vec4) -> Vec4 { + let mut out = Vec4::ZERO; + let m_ptr = m.0.as_ptr(); + let v_ptr = v.0.as_ptr(); + let out_ptr = out.0.as_mut_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({m_ptr})", + "lv.q C010, 16({m_ptr})", + "lv.q C020, 32({m_ptr})", + "lv.q C030, 48({m_ptr})", + "lv.q C100, 0({v_ptr})", + "vtfm4.q C110, M000, C100", + "sv.q C110, 0({out_ptr})", + m_ptr = in(reg) m_ptr, + v_ptr = in(reg) v_ptr, + out_ptr = in(reg) out_ptr, + options(nostack), + ); + } + out +} + +/// Load the 4x4 identity matrix. +pub fn mat4_identity() -> Mat4 { + let mut out = Mat4::ZERO; + let out_ptr = out.0.as_mut_ptr(); + unsafe { + vfpu_asm!( + "vmidt.q M000", + "sv.q C000, 0({out_ptr})", + "sv.q C010, 16({out_ptr})", + "sv.q C020, 32({out_ptr})", + "sv.q C030, 48({out_ptr})", + out_ptr = in(reg) out_ptr, + options(nostack), + ); + } + out +} + +// ── Color Operations ──────────────────────────────────────────────── + +/// Blend two RGBA colors using alpha blending. +/// +/// Colors are in `[R, G, B, A]` format with components in `0.0..=1.0`. +/// Uses standard "over" compositing: `result = src * src.a + dst * (1 - src.a)`. +pub fn color_blend_rgba(src: &Vec4, dst: &Vec4) -> Vec4 { + let mut out = Vec4::ZERO; + let src_ptr = src.0.as_ptr(); + let dst_ptr = dst.0.as_ptr(); + let out_ptr = out.0.as_mut_ptr(); + unsafe { + vfpu_asm!( + "lv.q C000, 0({src_ptr})", // C000 = src RGBA + "lv.q C010, 0({dst_ptr})", // C010 = dst RGBA + // Extract src alpha and compute (1 - src.a) + "vscl.q C020, C000, S003", // C020 = src * src.a + "vone.s S030", // S030 = 1.0 + "vsub.s S030, S030, S003", // S030 = 1 - src.a + "vscl.q C010, C010, S030", // C010 = dst * (1 - src.a) + "vadd.q C000, C020, C010", // C000 = src*a + dst*(1-a) + "sv.q C000, 0({out_ptr})", + src_ptr = in(reg) src_ptr, + dst_ptr = in(reg) dst_ptr, + out_ptr = in(reg) out_ptr, + options(nostack), + ); + } + out +} + +/// Convert HSV color to RGB. +/// +/// Input: `[H, S, V, A]` where H is in `0.0..=360.0`, S/V/A in `0.0..=1.0`. +/// Output: `[R, G, B, A]` with components in `0.0..=1.0`. +pub fn color_hsv_to_rgb(hsv: &Vec4) -> Vec4 { + let h = hsv.0[0]; + let s = hsv.0[1]; + let v = hsv.0[2]; + let a = hsv.0[3]; + + if s <= 0.0 { + return Vec4::new(v, v, v, a); + } + + let hh = if h >= 360.0 { 0.0 } else { h / 60.0 }; + let i = hh as u32; + let ff = hh - i as f32; + let p = v * (1.0 - s); + let q = v * (1.0 - s * ff); + let t = v * (1.0 - s * (1.0 - ff)); + + match i { + 0 => Vec4::new(v, t, p, a), + 1 => Vec4::new(q, v, p, a), + 2 => Vec4::new(p, v, t, a), + 3 => Vec4::new(p, q, v, a), + 4 => Vec4::new(t, p, v, a), + _ => Vec4::new(v, p, q, a), + } +} + +/// Convert RGB color to HSV. +/// +/// Input: `[R, G, B, A]` with components in `0.0..=1.0`. +/// Output: `[H, S, V, A]` where H is in `0.0..=360.0`. +pub fn color_rgb_to_hsv(rgb: &Vec4) -> Vec4 { + let r = rgb.0[0]; + let g = rgb.0[1]; + let b = rgb.0[2]; + let a = rgb.0[3]; + + let max = if r > g { + if r > b { r } else { b } + } else if g > b { + g + } else { + b + }; + let min = if r < g { + if r < b { r } else { b } + } else if g < b { + g + } else { + b + }; + let delta = max - min; + + let v = max; + let s = if max > 0.0 { delta / max } else { 0.0 }; + + let h = if delta < 0.00001 { + 0.0 + } else if max == r { + 60.0 * (((g - b) / delta) % 6.0) + } else if max == g { + 60.0 * (((b - r) / delta) + 2.0) + } else { + 60.0 * (((r - g) / delta) + 4.0) + }; + + let h = if h < 0.0 { h + 360.0 } else { h }; + + Vec4::new(h, s, v, a) +} + +// ── Easing Functions ──────────────────────────────────────────────── + +/// Quadratic ease-in-out. +/// +/// `t` is in `0.0..=1.0`. Returns a smoothly accelerating/decelerating value. +pub fn ease_in_out_quad(t: f32) -> f32 { + if t < 0.5 { + 2.0 * t * t + } else { + let t2 = -2.0 * t + 2.0; + 1.0 - t2 * t2 / 2.0 + } +} + +/// Cubic ease-in-out. +pub fn ease_in_out_cubic(t: f32) -> f32 { + if t < 0.5 { + 4.0 * t * t * t + } else { + let t2 = -2.0 * t + 2.0; + 1.0 - t2 * t2 * t2 / 2.0 + } +} + +/// Quadratic ease-in (accelerating from zero). +pub fn ease_in_quad(t: f32) -> f32 { + t * t +} + +/// Quadratic ease-out (decelerating to zero). +pub fn ease_out_quad(t: f32) -> f32 { + 1.0 - (1.0 - t) * (1.0 - t) +} + +/// Cubic ease-in. +pub fn ease_in_cubic(t: f32) -> f32 { + t * t * t +} + +/// Cubic ease-out. +pub fn ease_out_cubic(t: f32) -> f32 { + let t2 = 1.0 - t; + 1.0 - t2 * t2 * t2 +} + +/// Spring-damped interpolation. +/// +/// Simulates a damped spring system. Good for "bouncy" UI animations. +/// +/// - `t`: Progress `0.0..=1.0` +/// - `damping`: Damping factor (higher = less bounce). Typical: `0.5..0.8` +/// - `frequency`: Oscillation frequency. Typical: `8.0..15.0` +pub fn spring_damped(t: f32, damping: f32, frequency: f32) -> f32 { + if t <= 0.0 { + return 0.0; + } + if t >= 1.0 { + return 1.0; + } + + // Damped harmonic oscillator: 1 - e^(-d*t) * cos(f*t) + let decay = libm::expf(-damping * t * 10.0); + let angle = frequency * t * core::f32::consts::PI; + let oscillation = libm::cosf(angle); + 1.0 - decay * oscillation +} + +/// Smoothstep (Hermite interpolation). +/// +/// `t` is clamped to `0.0..=1.0`. Returns a smooth S-curve. +pub fn smoothstep(t: f32) -> f32 { + let t = if t < 0.0 { + 0.0 + } else if t > 1.0 { + 1.0 + } else { + t + }; + t * t * (3.0 - 2.0 * t) +} + +/// Smoother step (Ken Perlin's improved smoothstep). +pub fn smootherstep(t: f32) -> f32 { + let t = if t < 0.0 { + 0.0 + } else if t > 1.0 { + 1.0 + } else { + t + }; + t * t * t * (t * (t * 6.0 - 15.0) + 10.0) +} + +// ── Utility ───────────────────────────────────────────────────────── + +/// Clamp a float to a range. +pub fn clampf(val: f32, min: f32, max: f32) -> f32 { + if val < min { + min + } else if val > max { + max + } else { + val + } +} + +/// Remap a value from one range to another. +/// +/// Returns `out_min` when `in_max == in_min` (degenerate input range). +pub fn remapf(val: f32, in_min: f32, in_max: f32, out_min: f32, out_max: f32) -> f32 { + let range = in_max - in_min; + if range == 0.0 { + return out_min; + } + let t = (val - in_min) / range; + out_min + t * (out_max - out_min) +} diff --git a/psp/src/sync.rs b/psp/src/sync.rs new file mode 100644 index 0000000..d93995c --- /dev/null +++ b/psp/src/sync.rs @@ -0,0 +1,751 @@ +//! Synchronization primitives for the PSP. +//! +//! The PSP is a single-core MIPS R4000 processor, so these primitives use +//! atomic operations primarily to prevent re-entrant access from interrupt +//! handlers and to provide proper compiler ordering barriers. +//! +//! # Primitives +//! +//! - [`SpinMutex`]: Exclusive-access spinlock (extracted from `debug.rs`) +//! - [`SpinRwLock`]: Reader-writer spinlock for shared-read / exclusive-write +//! - [`SpscQueue`]: Lock-free single-producer single-consumer ring buffer +//! - [`UncachedBox`]: Heap-allocated box in uncached (ME-accessible) memory + +use core::cell::UnsafeCell; +use core::mem::MaybeUninit; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + +// ── SpinMutex ─────────────────────────────────────────────────────── + +/// A simple spinlock for single-core environments (PSP MIPS R4000). +/// +/// Uses `AtomicBool` with acquire/release ordering. On the single-core PSP +/// this prevents compiler reordering; on multi-core it would provide proper +/// synchronization too. +/// +/// # Example +/// +/// ```ignore +/// use psp::sync::SpinMutex; +/// +/// static COUNTER: SpinMutex = SpinMutex::new(0); +/// +/// let mut guard = COUNTER.lock(); +/// *guard += 1; +/// ``` +pub struct SpinMutex { + locked: AtomicBool, + data: UnsafeCell, +} + +// SAFETY: SpinMutex provides exclusive access via the atomic lock. +// PSP is single-core, so the spinlock prevents re-entrant access from +// interrupt handlers or coroutines. +unsafe impl Sync for SpinMutex {} +unsafe impl Send for SpinMutex {} + +impl SpinMutex { + /// Create a new `SpinMutex` wrapping `val`. + pub const fn new(val: T) -> Self { + Self { + locked: AtomicBool::new(false), + data: UnsafeCell::new(val), + } + } + + /// Acquire the lock, spinning until it becomes available. + /// + /// Returns a RAII guard that releases the lock on drop. + pub fn lock(&self) -> SpinGuard<'_, T> { + while self + .locked + .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_err() + { + core::hint::spin_loop(); + } + SpinGuard { mutex: self } + } + + /// Try to acquire the lock without spinning. + /// + /// Returns `None` if the lock is already held. + pub fn try_lock(&self) -> Option> { + if self + .locked + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + { + Some(SpinGuard { mutex: self }) + } else { + None + } + } +} + +/// RAII guard for [`SpinMutex`]. Releases the lock when dropped. +pub struct SpinGuard<'a, T> { + mutex: &'a SpinMutex, +} + +impl core::ops::Deref for SpinGuard<'_, T> { + type Target = T; + fn deref(&self) -> &T { + // SAFETY: We hold the lock. + unsafe { &*self.mutex.data.get() } + } +} + +impl core::ops::DerefMut for SpinGuard<'_, T> { + fn deref_mut(&mut self) -> &mut T { + // SAFETY: We hold the lock exclusively. + unsafe { &mut *self.mutex.data.get() } + } +} + +impl Drop for SpinGuard<'_, T> { + fn drop(&mut self) { + self.mutex.locked.store(false, Ordering::Release); + } +} + +// ── SpinRwLock ────────────────────────────────────────────────────── + +/// A reader-writer spinlock. +/// +/// Allows multiple concurrent readers or one exclusive writer. +/// Useful for the "UI reads state while IO writes" pattern. +/// +/// The state is encoded in a single `AtomicU32`: +/// - `0` = unlocked +/// - `WRITER_BIT` set = write-locked +/// - Otherwise, the value is the reader count +/// +/// # Example +/// +/// ```ignore +/// use psp::sync::SpinRwLock; +/// +/// static STATE: SpinRwLock = SpinRwLock::new(GameState::default()); +/// +/// // Reader (UI thread): +/// let guard = STATE.read(); +/// draw_ui(&*guard); +/// +/// // Writer (IO thread): +/// let mut guard = STATE.write(); +/// guard.score += 10; +/// ``` +pub struct SpinRwLock { + /// 0 = unlocked, WRITER_BIT = write-locked, else reader count + state: AtomicU32, + data: UnsafeCell, +} + +const WRITER_BIT: u32 = 1 << 31; + +// SAFETY: SpinRwLock provides reader/writer exclusion via atomic state. +unsafe impl Send for SpinRwLock {} +unsafe impl Sync for SpinRwLock {} + +impl SpinRwLock { + /// Create a new `SpinRwLock` wrapping `val`. + pub const fn new(val: T) -> Self { + Self { + state: AtomicU32::new(0), + data: UnsafeCell::new(val), + } + } + + /// Acquire a read lock, spinning until no writer holds the lock. + pub fn read(&self) -> ReadGuard<'_, T> { + loop { + let s = self.state.load(Ordering::Relaxed); + // Cannot acquire read lock while a writer holds it + if s & WRITER_BIT != 0 { + core::hint::spin_loop(); + continue; + } + // Try to increment the reader count + if self + .state + .compare_exchange_weak(s, s + 1, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + { + return ReadGuard { lock: self }; + } + core::hint::spin_loop(); + } + } + + /// Try to acquire a read lock without spinning. + pub fn try_read(&self) -> Option> { + let s = self.state.load(Ordering::Relaxed); + if s & WRITER_BIT != 0 { + return None; + } + if self + .state + .compare_exchange(s, s + 1, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + { + Some(ReadGuard { lock: self }) + } else { + None + } + } + + /// Acquire a write lock, spinning until all readers and writers release. + pub fn write(&self) -> WriteGuard<'_, T> { + loop { + if self + .state + .compare_exchange_weak(0, WRITER_BIT, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + { + return WriteGuard { lock: self }; + } + core::hint::spin_loop(); + } + } + + /// Try to acquire a write lock without spinning. + pub fn try_write(&self) -> Option> { + if self + .state + .compare_exchange(0, WRITER_BIT, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + { + Some(WriteGuard { lock: self }) + } else { + None + } + } +} + +/// RAII read guard for [`SpinRwLock`]. +pub struct ReadGuard<'a, T> { + lock: &'a SpinRwLock, +} + +impl core::ops::Deref for ReadGuard<'_, T> { + type Target = T; + fn deref(&self) -> &T { + // SAFETY: Read lock is held; no writer can exist. + unsafe { &*self.lock.data.get() } + } +} + +impl Drop for ReadGuard<'_, T> { + fn drop(&mut self) { + self.lock.state.fetch_sub(1, Ordering::Release); + } +} + +/// RAII write guard for [`SpinRwLock`]. +pub struct WriteGuard<'a, T> { + lock: &'a SpinRwLock, +} + +impl core::ops::Deref for WriteGuard<'_, T> { + type Target = T; + fn deref(&self) -> &T { + // SAFETY: Write lock is held exclusively. + unsafe { &*self.lock.data.get() } + } +} + +impl core::ops::DerefMut for WriteGuard<'_, T> { + fn deref_mut(&mut self) -> &mut T { + // SAFETY: Write lock is held exclusively. + unsafe { &mut *self.lock.data.get() } + } +} + +impl Drop for WriteGuard<'_, T> { + fn drop(&mut self) { + self.lock.state.store(0, Ordering::Release); + } +} + +// ── SPSC Ring Buffer ──────────────────────────────────────────────── + +/// A lock-free single-producer single-consumer (SPSC) ring buffer. +/// +/// This is the fundamental primitive for ME↔CPU and IO-thread↔UI-thread +/// communication. One thread pushes, one thread pops — no locks needed. +/// +/// `N` must be a power of two for efficient modular indexing. +/// +/// # Example +/// +/// ```ignore +/// use psp::sync::SpscQueue; +/// +/// static QUEUE: SpscQueue = SpscQueue::new(); +/// +/// // Producer thread: +/// QUEUE.push(42); +/// +/// // Consumer thread: +/// if let Some(val) = QUEUE.pop() { +/// assert_eq!(val, 42); +/// } +/// ``` +pub struct SpscQueue { + head: AtomicU32, + tail: AtomicU32, + buf: UnsafeCell<[MaybeUninit; N]>, +} + +// SAFETY: Only one producer and one consumer are expected. +// The atomic head/tail provide the necessary synchronization. +unsafe impl Send for SpscQueue {} +unsafe impl Sync for SpscQueue {} + +impl SpscQueue { + const _ASSERT_POWER_OF_TWO: () = assert!( + N > 0 && (N & (N - 1)) == 0, + "SpscQueue capacity must be a power of two" + ); + + /// Create a new empty `SpscQueue`. + pub const fn new() -> Self { + // Trigger the compile-time assertion + #[allow(clippy::let_unit_value)] + let _ = Self::_ASSERT_POWER_OF_TWO; + + // SAFETY: An array of MaybeUninit doesn't require initialization + let buf = unsafe { MaybeUninit::<[MaybeUninit; N]>::uninit().assume_init() }; + Self { + head: AtomicU32::new(0), + tail: AtomicU32::new(0), + buf: UnsafeCell::new(buf), + } + } + + const MASK: u32 = (N - 1) as u32; + + /// Push a value into the queue. + /// + /// Returns `Err(val)` if the queue is full. + pub fn push(&self, val: T) -> Result<(), T> { + let tail = self.tail.load(Ordering::Relaxed); + let head = self.head.load(Ordering::Acquire); + + if tail.wrapping_sub(head) >= N as u32 { + return Err(val); + } + + let idx = (tail & Self::MASK) as usize; + // SAFETY: We are the sole producer, and we've verified there's space. + unsafe { + let slot = &mut (*self.buf.get())[idx]; + slot.write(val); + } + + self.tail.store(tail.wrapping_add(1), Ordering::Release); + Ok(()) + } + + /// Pop a value from the queue. + /// + /// Returns `None` if the queue is empty. + pub fn pop(&self) -> Option { + let head = self.head.load(Ordering::Relaxed); + let tail = self.tail.load(Ordering::Acquire); + + if head == tail { + return None; + } + + let idx = (head & Self::MASK) as usize; + // SAFETY: We are the sole consumer, and we've verified there's data. + let val = unsafe { + let slot = &(*self.buf.get())[idx]; + slot.assume_init_read() + }; + + self.head.store(head.wrapping_add(1), Ordering::Release); + Some(val) + } + + /// Returns `true` if the queue is empty. + pub fn is_empty(&self) -> bool { + let head = self.head.load(Ordering::Relaxed); + let tail = self.tail.load(Ordering::Acquire); + head == tail + } + + /// Returns the number of items currently in the queue. + pub fn len(&self) -> u32 { + let head = self.head.load(Ordering::Relaxed); + let tail = self.tail.load(Ordering::Acquire); + tail.wrapping_sub(head) + } + + /// Returns the total capacity of the queue. + pub const fn capacity(&self) -> usize { + N + } +} + +impl Drop for SpscQueue { + fn drop(&mut self) { + // Drop all remaining items in the queue. + while self.pop().is_some() {} + } +} + +// ── UncachedBox ───────────────────────────────────────────────────── + +/// A heap-allocated box in uncached (partition 3) memory, suitable for +/// sharing data with the Media Engine. +/// +/// The ME cannot access cached main RAM coherently — all shared memory must +/// use uncached addresses (OR'd with `0x4000_0000`). `UncachedBox` +/// allocates from ME-accessible partition 3 and returns an uncached pointer. +/// +/// `UncachedBox` is `Send` but not `Sync`: it enforces the "one writer" +/// model. Pass it to the ME thread or use it from one side at a time with +/// explicit synchronization. +/// +/// # Kernel Mode Required +/// +/// This type requires `feature = "kernel"` because partition 3 is only +/// accessible in kernel mode. +/// +/// # Example +/// +/// ```ignore +/// use psp::sync::UncachedBox; +/// +/// let shared = UncachedBox::new(0u32).unwrap(); +/// // Pass `shared` to ME task... +/// ``` +#[cfg(feature = "kernel")] +pub struct UncachedBox { + ptr: *mut T, + block_id: crate::sys::SceUid, +} + +// SAFETY: UncachedBox owns its data and can be sent across threads. +// Not Sync — enforces "one writer" model for ME-shared data. +#[cfg(feature = "kernel")] +unsafe impl Send for UncachedBox {} + +#[cfg(feature = "kernel")] +impl UncachedBox { + /// Allocate an `UncachedBox` in ME-accessible partition 3. + /// + /// The value is written to uncached memory. Returns an error with the + /// PSP error code if allocation fails. + pub fn new(val: T) -> Result { + let size = core::mem::size_of::().max(1) as u32; + // SAFETY: Kernel mode is required; we allocate from partition 3. + let (ptr, block_id) = unsafe { crate::me::me_alloc(size, b"UncachedBox\0".as_ptr()) }?; + let typed_ptr = ptr as *mut T; + + // SAFETY: ptr is valid uncached memory of sufficient size. + unsafe { + core::ptr::write_volatile(typed_ptr, val); + } + + Ok(Self { + ptr: typed_ptr, + block_id, + }) + } + + /// Get a raw pointer to the uncached data. + pub fn as_ptr(&self) -> *const T { + self.ptr + } + + /// Get a mutable raw pointer to the uncached data. + pub fn as_mut_ptr(&mut self) -> *mut T { + self.ptr + } + + /// Read the value using volatile access (appropriate for uncached memory). + /// + /// # Safety + /// + /// The caller must ensure no concurrent writes are in progress (e.g., + /// the ME is not currently modifying this data). + pub unsafe fn read_volatile(&self) -> T { + unsafe { core::ptr::read_volatile(self.ptr) } + } + + /// Write a value using volatile access (appropriate for uncached memory). + /// + /// # Safety + /// + /// The caller must ensure no concurrent reads/writes are in progress. + pub unsafe fn write_volatile(&mut self, val: T) { + unsafe { core::ptr::write_volatile(self.ptr, val) } + } +} + +#[cfg(feature = "kernel")] +impl Drop for UncachedBox { + fn drop(&mut self) { + unsafe { + // Drop the inner value before freeing the memory. + core::ptr::drop_in_place(self.ptr); + crate::sys::sceKernelFreePartitionMemory(self.block_id); + } + } +} + +#[cfg(feature = "kernel")] +impl core::fmt::Debug for UncachedBox { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + // SAFETY: Debug access — caller should ensure no concurrent ME writes + let val = unsafe { core::ptr::read_volatile(self.ptr) }; + f.debug_struct("UncachedBox") + .field("value", &val) + .field("ptr", &self.ptr) + .finish() + } +} + +// ── SyncError ─────────────────────────────────────────────────────── + +/// Error from a PSP synchronization operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct SyncError(pub i32); + +impl SyncError { + pub fn code(self) -> i32 { + self.0 + } +} + +impl core::fmt::Debug for SyncError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "SyncError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for SyncError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "sync error {:#010x}", self.0 as u32) + } +} + +// ── Semaphore ─────────────────────────────────────────────────────── + +/// A kernel semaphore with RAII cleanup. +/// +/// Provides blocking, non-blocking, and timed wait operations backed by +/// `sceKernelCreateSema` / `sceKernelDeleteSema`. +/// +/// # Example +/// +/// ```ignore +/// use psp::sync::Semaphore; +/// +/// let sem = Semaphore::new(b"MySema\0", 0, 1).unwrap(); +/// // In producer: sem.signal(1); +/// // In consumer: sem.wait(); +/// ``` +pub struct Semaphore { + id: crate::sys::SceUid, +} + +// SAFETY: PSP kernel semaphores are designed for cross-thread use. +unsafe impl Send for Semaphore {} +unsafe impl Sync for Semaphore {} + +impl Semaphore { + /// Create a new kernel semaphore. + /// + /// - `name`: null-terminated name (e.g. `b"MySema\0"`) + /// - `init_count`: initial semaphore count + /// - `max_count`: maximum semaphore count + pub fn new(name: &[u8], init_count: i32, max_count: i32) -> Result { + debug_assert!(name.last() == Some(&0), "name must be null-terminated"); + let id = unsafe { + crate::sys::sceKernelCreateSema( + name.as_ptr(), + 0, // default attributes + init_count, + max_count, + core::ptr::null_mut(), + ) + }; + if id.0 < 0 { + Err(SyncError(id.0)) + } else { + Ok(Self { id }) + } + } + + /// Wait (block) until the semaphore count is >= 1, then decrement. + pub fn wait(&self) -> Result<(), SyncError> { + let ret = unsafe { crate::sys::sceKernelWaitSema(self.id, 1, core::ptr::null_mut()) }; + if ret < 0 { Err(SyncError(ret)) } else { Ok(()) } + } + + /// Wait with a timeout in microseconds. + /// + /// Returns `Err` on timeout or other error. + pub fn wait_timeout(&self, us: u32) -> Result<(), SyncError> { + let mut timeout = us; + let ret = unsafe { crate::sys::sceKernelWaitSema(self.id, 1, &mut timeout) }; + if ret < 0 { Err(SyncError(ret)) } else { Ok(()) } + } + + /// Try to decrement the semaphore without blocking. + /// + /// Returns `Err` if the count is zero. + pub fn try_wait(&self) -> Result<(), SyncError> { + let ret = unsafe { crate::sys::sceKernelPollSema(self.id, 1) }; + if ret < 0 { Err(SyncError(ret)) } else { Ok(()) } + } + + /// Increment the semaphore count by `count`. + pub fn signal(&self, count: i32) -> Result<(), SyncError> { + let ret = unsafe { crate::sys::sceKernelSignalSema(self.id, count) }; + if ret < 0 { Err(SyncError(ret)) } else { Ok(()) } + } + + /// Get the kernel UID. + pub fn id(&self) -> crate::sys::SceUid { + self.id + } +} + +impl Drop for Semaphore { + fn drop(&mut self) { + unsafe { + crate::sys::sceKernelDeleteSema(self.id); + } + } +} + +// ── EventFlag ─────────────────────────────────────────────────────── + +/// A kernel event flag with RAII cleanup. +/// +/// Provides a bitmask-based synchronization primitive backed by +/// `sceKernelCreateEventFlag` / `sceKernelDeleteEventFlag`. +/// +/// # Example +/// +/// ```ignore +/// use psp::sync::EventFlag; +/// use psp::sys::{EventFlagAttributes, EventFlagWaitTypes}; +/// +/// let flag = EventFlag::new(b"MyFlag\0", EventFlagAttributes::empty(), 0).unwrap(); +/// // In producer: flag.set(0x01); +/// // In consumer: flag.wait(0x01, EventFlagWaitTypes::OR | EventFlagWaitTypes::CLEAR); +/// ``` +pub struct EventFlag { + id: crate::sys::SceUid, +} + +// SAFETY: PSP kernel event flags are designed for cross-thread use. +unsafe impl Send for EventFlag {} +unsafe impl Sync for EventFlag {} + +impl EventFlag { + /// Create a new kernel event flag. + /// + /// - `name`: null-terminated name + /// - `attr`: attributes (e.g. `EventFlagAttributes::WAIT_MULTIPLE`) + /// - `init_pattern`: initial bit pattern + pub fn new( + name: &[u8], + attr: crate::sys::EventFlagAttributes, + init_pattern: u32, + ) -> Result { + debug_assert!(name.last() == Some(&0), "name must be null-terminated"); + let id = unsafe { + crate::sys::sceKernelCreateEventFlag( + name.as_ptr(), + attr, + init_pattern as i32, + core::ptr::null_mut(), + ) + }; + if id.0 < 0 { + Err(SyncError(id.0)) + } else { + Ok(Self { id }) + } + } + + /// Wait for bits matching `pattern` according to `wait_type`. + /// + /// Returns the bit pattern that was matched. + pub fn wait( + &self, + pattern: u32, + wait_type: crate::sys::EventFlagWaitTypes, + ) -> Result { + let mut out_bits: u32 = 0; + let ret = unsafe { + crate::sys::sceKernelWaitEventFlag( + self.id, + pattern, + wait_type, + &mut out_bits, + core::ptr::null_mut(), + ) + }; + if ret < 0 { + Err(SyncError(ret)) + } else { + Ok(out_bits) + } + } + + /// Set bits in the event flag. + pub fn set(&self, bits: u32) -> Result<(), SyncError> { + let ret = unsafe { crate::sys::sceKernelSetEventFlag(self.id, bits) }; + if ret < 0 { Err(SyncError(ret)) } else { Ok(()) } + } + + /// Clear bits in the event flag. + /// + /// Bits that are 1 in `bits` are *kept*; bits that are 0 are cleared. + /// (This matches the PSP kernel semantics: the flag is AND'd with `bits`.) + pub fn clear(&self, bits: u32) -> Result<(), SyncError> { + let ret = unsafe { crate::sys::sceKernelClearEventFlag(self.id, bits) }; + if ret < 0 { Err(SyncError(ret)) } else { Ok(()) } + } + + /// Poll for matching bits without blocking. + /// + /// Returns the matched bit pattern, or `Err` if no match. + pub fn poll( + &self, + pattern: u32, + wait_type: crate::sys::EventFlagWaitTypes, + ) -> Result { + let mut out_bits: u32 = 0; + let ret = unsafe { + crate::sys::sceKernelPollEventFlag(self.id, pattern, wait_type, &mut out_bits) + }; + if ret < 0 { + Err(SyncError(ret)) + } else { + Ok(out_bits) + } + } + + /// Get the kernel UID. + pub fn id(&self) -> crate::sys::SceUid { + self.id + } +} + +impl Drop for EventFlag { + fn drop(&mut self) { + unsafe { + crate::sys::sceKernelDeleteEventFlag(self.id); + } + } +} diff --git a/psp/src/sys/kernel/mod.rs b/psp/src/sys/kernel/mod.rs index 5787372..a422c74 100644 --- a/psp/src/sys/kernel/mod.rs +++ b/psp/src/sys/kernel/mod.rs @@ -498,6 +498,54 @@ psp_extern! { ) -> i32; } +psp_extern! { + #![name = "sceDmac"] + #![flags = 0x4001] + #![version = (0x00, 0x00)] + + #[psp(0x617F3FE6)] + /// Perform a DMA memory copy (blocking). + /// + /// Copies `size` bytes from `src` to `dst` using the DMA controller. + /// This call blocks until the transfer is complete. + /// + /// # Parameters + /// + /// - `dst`: Destination address. + /// - `src`: Source address. + /// - `size`: Number of bytes to copy. + /// + /// # Return Value + /// + /// 0 on success, < 0 on error. + pub fn sceDmacMemcpy( + dst: *mut c_void, + src: *const c_void, + size: u32, + ) -> i32; + + #[psp(0xD97F94D8)] + /// Try to perform a DMA memory copy (non-blocking attempt). + /// + /// Like `sceDmacMemcpy`, but returns immediately with an error if + /// the DMA controller is busy. + /// + /// # Parameters + /// + /// - `dst`: Destination address. + /// - `src`: Source address. + /// - `size`: Number of bytes to copy. + /// + /// # Return Value + /// + /// 0 on success, < 0 on error (including busy). + pub fn sceDmacTryMemcpy( + dst: *mut c_void, + src: *const c_void, + size: u32, + ) -> i32; +} + #[repr(packed, C)] pub struct IntrHandlerOptionParam { size: i32, //+00 diff --git a/psp/src/system_param.rs b/psp/src/system_param.rs new file mode 100644 index 0000000..ed19ecd --- /dev/null +++ b/psp/src/system_param.rs @@ -0,0 +1,88 @@ +//! System parameter queries for the PSP. +//! +//! Read system-level settings like language, nickname, date/time format, +//! timezone, and daylight saving status. These are configured by the user +//! in the PSP's System Settings menu. +//! +//! # Example +//! +//! ```ignore +//! use psp::system_param; +//! +//! let lang = system_param::language(); +//! let tz = system_param::timezone_offset(); +//! psp::dprintln!("Language: {:?}, TZ offset: {} min", lang, tz); +//! ``` + +use crate::sys::{ + SystemParamDateFormat, SystemParamDaylightSavings, SystemParamId, SystemParamLanguage, + SystemParamTimeFormat, sceUtilityGetSystemParamInt, sceUtilityGetSystemParamString, +}; + +/// Error from a system parameter operation. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct ParamError(pub i32); + +impl core::fmt::Debug for ParamError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "ParamError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for ParamError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "system param error {:#010x}", self.0 as u32) + } +} + +fn get_int(id: SystemParamId) -> Result { + let mut value: i32 = 0; + let ret = unsafe { sceUtilityGetSystemParamInt(id, &mut value) }; + if ret < 0 { + Err(ParamError(ret)) + } else { + Ok(value) + } +} + +/// Get the system language setting. +pub fn language() -> Result { + let val = get_int(SystemParamId::Language)?; + SystemParamLanguage::try_from(val as u32).map_err(|_| ParamError(val)) +} + +/// Get the user's nickname (up to 128 bytes, null-terminated). +pub fn nickname() -> Result<[u8; 128], ParamError> { + let mut buf = [0u8; 128]; + let ret = unsafe { + sceUtilityGetSystemParamString(SystemParamId::StringNickname, buf.as_mut_ptr(), 128) + }; + if ret < 0 { + Err(ParamError(ret)) + } else { + Ok(buf) + } +} + +/// Get the date format preference. +pub fn date_format() -> Result { + let val = get_int(SystemParamId::DateFormat)?; + SystemParamDateFormat::try_from(val as u32).map_err(|_| ParamError(val)) +} + +/// Get the time format preference (12-hour or 24-hour). +pub fn time_format() -> Result { + let val = get_int(SystemParamId::TimeFormat)?; + SystemParamTimeFormat::try_from(val as u32).map_err(|_| ParamError(val)) +} + +/// Get the timezone offset in minutes from UTC. +pub fn timezone_offset() -> Result { + get_int(SystemParamId::Timezone) +} + +/// Check if daylight saving time is enabled. +pub fn daylight_saving() -> Result { + let val = get_int(SystemParamId::DaylightSavings)?; + Ok(val == SystemParamDaylightSavings::Dst as i32) +} diff --git a/psp/src/thread.rs b/psp/src/thread.rs new file mode 100644 index 0000000..573becf --- /dev/null +++ b/psp/src/thread.rs @@ -0,0 +1,328 @@ +//! Thread spawning and management for the PSP. +//! +//! Provides a closure-based [`spawn()`] function and [`JoinHandle`] for +//! waiting on thread completion, similar to `std::thread` but tailored +//! to the PSP's threading model. +//! +//! # Example +//! +//! ```ignore +//! use psp::thread; +//! +//! let handle = thread::spawn(b"worker\0", || { +//! // do background work +//! 42 +//! }).unwrap(); +//! +//! let result = handle.join().unwrap(); +//! assert_eq!(result, 42); +//! ``` + +use crate::sys::{ + SceUid, ThreadAttributes, sceKernelCreateThread, sceKernelDelayThread, sceKernelDeleteThread, + sceKernelGetThreadExitStatus, sceKernelGetThreadId, sceKernelSleepThread, sceKernelStartThread, + sceKernelTerminateDeleteThread, sceKernelWaitThreadEnd, +}; +use alloc::boxed::Box; +use core::ffi::c_void; +use core::sync::atomic::{AtomicBool, Ordering}; + +// ── ThreadError ───────────────────────────────────────────────────── + +/// Error from a PSP thread operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct ThreadError(pub i32); + +impl ThreadError { + pub fn code(self) -> i32 { + self.0 + } +} + +impl core::fmt::Debug for ThreadError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "ThreadError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for ThreadError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "thread error {:#010x}", self.0 as u32) + } +} + +// ── ThreadBuilder ─────────────────────────────────────────────────── + +/// Builder for configuring and spawning threads. +/// +/// # Example +/// +/// ```ignore +/// use psp::thread::ThreadBuilder; +/// use psp::sys::ThreadAttributes; +/// +/// let handle = ThreadBuilder::new(b"my_thread\0") +/// .priority(48) +/// .stack_size(64 * 1024) +/// .attributes(ThreadAttributes::USER | ThreadAttributes::VFPU) +/// .spawn(|| 0) +/// .unwrap(); +/// ``` +pub struct ThreadBuilder { + name: &'static [u8], + priority: i32, + stack_size: i32, + attributes: ThreadAttributes, +} + +impl ThreadBuilder { + /// Create a new builder. `name` must be a null-terminated byte string. + pub fn new(name: &'static [u8]) -> Self { + Self { + name, + priority: 32, + stack_size: 64 * 1024, + attributes: ThreadAttributes::USER | ThreadAttributes::VFPU, + } + } + + /// Set the initial thread priority (lower = higher priority). + pub fn priority(mut self, prio: i32) -> Self { + self.priority = prio; + self + } + + /// Set the thread stack size in bytes. + pub fn stack_size(mut self, size: i32) -> Self { + self.stack_size = size; + self + } + + /// Set thread attributes. + pub fn attributes(mut self, attr: ThreadAttributes) -> Self { + self.attributes = attr; + self + } + + /// Spawn the thread, running `f` on it. + /// + /// The closure must be `Send + 'static` because it runs on a different + /// thread. It returns an `i32` which becomes the thread's exit status. + pub fn spawn i32 + Send + 'static>( + self, + f: F, + ) -> Result { + spawn_inner( + self.name, + self.priority, + self.stack_size, + self.attributes, + f, + ) + } +} + +// ── ThreadPayload ─────────────────────────────────────────────────── + +/// Shared state between the trampoline and `JoinHandle` to prevent +/// double-free of the closure when a thread finishes between the +/// zero-timeout wait check and `sceKernelTerminateDeleteThread` in Drop. +struct ThreadPayload { + closure: Option i32 + Send + 'static>>, + /// Set to `true` by the trampoline after consuming the closure. + consumed: AtomicBool, +} + +// ── spawn ─────────────────────────────────────────────────────────── + +/// Spawn a thread with default settings. +/// +/// Equivalent to `ThreadBuilder::new(name).spawn(f)`. +/// +/// - Priority: 32 +/// - Stack size: 64 KiB +/// - Attributes: USER | VFPU +pub fn spawn i32 + Send + 'static>( + name: &'static [u8], + f: F, +) -> Result { + ThreadBuilder::new(name).spawn(f) +} + +/// Internal spawn implementation. +fn spawn_inner i32 + Send + 'static>( + name: &'static [u8], + priority: i32, + stack_size: i32, + attributes: ThreadAttributes, + f: F, +) -> Result { + // Validate null termination — the PSP kernel expects a C string. + // Without this check, safe code could cause out-of-bounds reads. + if name.last() != Some(&0) { + return Err(ThreadError(-1)); + } + + // Box the closure into a ThreadPayload with an atomic flag. + let payload = Box::into_raw(Box::new(ThreadPayload { + closure: Some(Box::new(f)), + consumed: AtomicBool::new(false), + })); + + let thid = unsafe { + sceKernelCreateThread( + name.as_ptr(), + trampoline, + priority, + stack_size, + attributes, + core::ptr::null_mut(), + ) + }; + + if thid.0 < 0 { + // Thread creation failed — reclaim the payload. + unsafe { + drop(Box::from_raw(payload)); + } + return Err(ThreadError(thid.0)); + } + + // Start the thread, passing the payload pointer as the argument. + let ret = unsafe { + sceKernelStartThread( + thid, + core::mem::size_of::<*mut c_void>(), + &payload as *const _ as *mut c_void, + ) + }; + + if ret < 0 { + // Start failed — clean up the thread and payload. + unsafe { + sceKernelDeleteThread(thid); + drop(Box::from_raw(payload)); + } + return Err(ThreadError(ret)); + } + + Ok(JoinHandle { + thid, + joined: false, + payload, + }) +} + +/// C-callable trampoline that runs the boxed closure. +/// +/// The PSP passes `argp` pointing to a buffer containing the raw pointer +/// to our `ThreadPayload`. The payload holds the closure and an atomic +/// flag that we set after consuming the closure, preventing the +/// `JoinHandle::drop` from double-freeing it. +/// +/// Panics are caught with `catch_unwind` to prevent unwinding across the +/// `extern "C"` boundary, which would abort the process. +unsafe extern "C" fn trampoline(_args: usize, argp: *mut c_void) -> i32 { + // `argp` points to a buffer containing a pointer to ThreadPayload. + let ptr_to_payload = argp as *const *mut ThreadPayload; + let payload = unsafe { &mut **ptr_to_payload }; + // Take the closure out of the payload. + let closure = payload.closure.take().unwrap(); + // Mark as consumed BEFORE running, so Drop won't try to free it + // even if the thread is terminated mid-execution. + payload.consumed.store(true, Ordering::Release); + match crate::catch_unwind(core::panic::AssertUnwindSafe(closure)) { + Ok(code) => code, + Err(_) => -0x7FFF_FFFF, // panic sentinel + } +} + +// ── JoinHandle ────────────────────────────────────────────────────── + +/// A handle to a spawned thread. +/// +/// Can be used to wait for the thread to finish. If dropped without +/// calling [`join()`](Self::join), the thread is terminated and deleted. +pub struct JoinHandle { + thid: SceUid, + joined: bool, + /// Shared payload containing the closure and a "consumed" flag. + /// The trampoline sets `consumed` after taking the closure, so + /// Drop can safely check whether it needs to free the closure. + payload: *mut ThreadPayload, +} + +// SAFETY: The payload pointer is only accessed after the thread is +// terminated (in drop) or after it has finished (in join). The handle +// itself can safely be sent to another thread. +unsafe impl Send for JoinHandle {} + +impl JoinHandle { + /// Block until the thread exits and return its exit status. + pub fn join(mut self) -> Result { + let ret = unsafe { sceKernelWaitThreadEnd(self.thid, core::ptr::null_mut()) }; + if ret < 0 { + return Err(ThreadError(ret)); + } + self.joined = true; + // Retrieve the actual thread exit status. + let exit_status = unsafe { sceKernelGetThreadExitStatus(self.thid) }; + let del = unsafe { sceKernelDeleteThread(self.thid) }; + // Free the payload (closure was already consumed by trampoline). + unsafe { drop(Box::from_raw(self.payload)) }; + self.payload = core::ptr::null_mut(); + if del < 0 { + return Err(ThreadError(del)); + } + Ok(exit_status) + } + + /// Get the thread's kernel UID. + pub fn id(&self) -> SceUid { + self.thid + } +} + +impl Drop for JoinHandle { + fn drop(&mut self) { + if self.joined || self.payload.is_null() { + return; + } + // Forcibly terminate and delete the thread. This is synchronous: + // after it returns the thread is dead. + unsafe { sceKernelTerminateDeleteThread(self.thid) }; + // Check the atomic flag to determine if the trampoline already + // consumed the closure. This prevents a double-free race where + // the thread finishes between the wait-check and terminate. + let payload = unsafe { Box::from_raw(self.payload) }; + if payload.consumed.load(Ordering::Acquire) { + // Trampoline already took the closure — nothing more to free. + // The payload Box itself is freed when `payload` drops here. + } + // If !consumed, the closure is still in payload.closure and will + // be dropped when `payload` drops here. + } +} + +// ── Free functions ────────────────────────────────────────────────── + +/// Sleep the current thread for `ms` milliseconds. +pub fn sleep_ms(ms: u32) { + let us = (ms as u64 * 1000).min(u32::MAX as u64) as u32; + unsafe { + sceKernelDelayThread(us); + } +} + +/// Put the current thread to sleep (woken by `sceKernelWakeupThread`). +pub fn sleep_thread() { + unsafe { + sceKernelSleepThread(); + } +} + +/// Get the UID of the current thread. +pub fn current_thread_id() -> SceUid { + let id = unsafe { sceKernelGetThreadId() }; + SceUid(id) +} diff --git a/psp/src/time.rs b/psp/src/time.rs new file mode 100644 index 0000000..4375bfc --- /dev/null +++ b/psp/src/time.rs @@ -0,0 +1,245 @@ +//! Time and clock abstractions for the PSP. +//! +//! Provides monotonic timing ([`Instant`], [`Duration`]), wall-clock +//! date/time ([`DateTime`]), and a frame-rate tracker ([`FrameTimer`]). +//! +//! # Example +//! +//! ```ignore +//! use psp::time::{Instant, FrameTimer}; +//! +//! let start = Instant::now(); +//! // ... do work ... +//! let elapsed = start.elapsed(); +//! psp::dprintln!("took {} ms", elapsed.as_millis()); +//! +//! let mut timer = FrameTimer::new(); +//! loop { +//! let dt = timer.tick(); +//! // dt is seconds since last frame +//! } +//! ``` + +/// Error type for time operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TimeError(pub i32); + +impl core::fmt::Display for TimeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "TimeError({:#010x})", self.0 as u32) + } +} + +// ── Duration ──────────────────────────────────────────────────────── + +/// A span of time in microseconds. +/// +/// The PSP's tick counter runs at 1 MHz, so microseconds are the native +/// resolution. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] +pub struct Duration { + micros: u64, +} + +impl Duration { + /// Zero duration. + pub const ZERO: Self = Self { micros: 0 }; + + /// Create a duration from microseconds. + pub const fn from_micros(us: u64) -> Self { + Self { micros: us } + } + + /// Create a duration from milliseconds. + pub const fn from_millis(ms: u64) -> Self { + Self { micros: ms * 1000 } + } + + /// Create a duration from whole seconds. + pub const fn from_secs(s: u64) -> Self { + Self { + micros: s * 1_000_000, + } + } + + /// Return the total number of microseconds. + pub const fn as_micros(&self) -> u64 { + self.micros + } + + /// Return the total number of whole milliseconds. + pub const fn as_millis(&self) -> u64 { + self.micros / 1000 + } + + /// Return the duration as fractional seconds. + pub fn as_secs_f32(&self) -> f32 { + self.micros as f32 / 1_000_000.0 + } +} + +// ── Instant ───────────────────────────────────────────────────────── + +/// A monotonic timestamp from the PSP's tick counter. +/// +/// Created via [`Instant::now()`]. Useful for measuring elapsed time +/// without wall-clock concerns (daylight saving, NTP adjustments, etc.). +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Instant { + tick: u64, +} + +impl Instant { + /// Capture the current tick counter. + pub fn now() -> Self { + let mut tick: u64 = 0; + unsafe { + crate::sys::sceRtcGetCurrentTick(&mut tick); + } + Self { tick } + } + + /// Time elapsed since this instant was captured. + pub fn elapsed(&self) -> Duration { + let now = Self::now(); + self.duration_to(now) + } + + /// Duration from `self` to a later instant. + /// + /// If `later` is actually earlier (e.g. due to wrapping), returns + /// `Duration::ZERO`. + pub fn duration_since(&self, earlier: Instant) -> Duration { + earlier.duration_to(*self) + } + + /// Raw tick value. + pub fn as_ticks(&self) -> u64 { + self.tick + } + + fn duration_to(self, later: Instant) -> Duration { + let ticks = later.tick.saturating_sub(self.tick); + // The PSP tick resolution is always 1,000,000 (1 MHz). + // Use a constant to avoid a syscall on every timing measurement. + const TICK_RESOLUTION: u64 = 1_000_000; + let micros = ticks * 1_000_000 / TICK_RESOLUTION; + Duration::from_micros(micros) + } +} + +// ── DateTime ──────────────────────────────────────────────────────── + +/// Wall-clock date and time from the PSP's RTC. +#[derive(Debug, Clone, Copy)] +pub struct DateTime { + inner: crate::sys::ScePspDateTime, +} + +impl DateTime { + /// Create a DateTime from a raw `ScePspDateTime`. + pub fn from_raw(raw: crate::sys::ScePspDateTime) -> Self { + Self { inner: raw } + } + + /// Get a reference to the underlying `ScePspDateTime`. + pub fn as_raw(&self) -> &crate::sys::ScePspDateTime { + &self.inner + } + + /// Get the current local date and time. + pub fn now() -> Result { + let mut dt = crate::sys::ScePspDateTime::default(); + let ret = unsafe { crate::sys::sceRtcGetCurrentClockLocalTime(&mut dt) }; + if ret < 0 { + Err(TimeError(ret)) + } else { + Ok(Self { inner: dt }) + } + } + + pub fn year(&self) -> u16 { + self.inner.year + } + pub fn month(&self) -> u16 { + self.inner.month + } + pub fn day(&self) -> u16 { + self.inner.day + } + pub fn hour(&self) -> u16 { + self.inner.hour + } + pub fn minute(&self) -> u16 { + self.inner.minutes + } + pub fn second(&self) -> u16 { + self.inner.seconds + } + pub fn microsecond(&self) -> u32 { + self.inner.microseconds + } +} + +// ── FrameTimer ────────────────────────────────────────────────────── + +/// Tracks frame timing for game loops. +/// +/// Call [`tick()`](Self::tick) once per frame to get the delta time in +/// seconds. [`fps()`](Self::fps) returns the estimated frames per second +/// based on the most recent delta. +/// +/// # Example +/// +/// ```ignore +/// let mut timer = FrameTimer::new(); +/// loop { +/// let dt = timer.tick(); +/// update_game(dt); +/// render(); +/// } +/// ``` +pub struct FrameTimer { + last: Instant, + delta: f32, +} + +impl FrameTimer { + /// Create a new `FrameTimer` starting from now. + pub fn new() -> Self { + Self { + last: Instant::now(), + delta: 1.0 / 60.0, // assume 60 FPS initially + } + } + + /// Advance one frame and return the delta time in seconds. + pub fn tick(&mut self) -> f32 { + let now = Instant::now(); + self.delta = self.last.duration_to(now).as_secs_f32(); + self.last = now; + self.delta + } + + /// Estimated frames per second based on the last delta. + /// + /// Returns `f32::INFINITY` if the last delta was zero. + pub fn fps(&self) -> f32 { + if self.delta > 0.0 { + 1.0 / self.delta + } else { + f32::INFINITY + } + } + + /// The delta time from the most recent `tick()` call, in seconds. + pub fn last_delta(&self) -> f32 { + self.delta + } +} + +impl Default for FrameTimer { + fn default() -> Self { + Self::new() + } +} diff --git a/psp/src/timer.rs b/psp/src/timer.rs new file mode 100644 index 0000000..33ea220 --- /dev/null +++ b/psp/src/timer.rs @@ -0,0 +1,315 @@ +//! Timer and alarm abstractions for the PSP. +//! +//! Provides one-shot alarms with closure support and virtual timers +//! with RAII cleanup. + +use crate::sys::{SceKernelVTimerHandlerWide, SceUid}; +use core::ffi::c_void; +use core::sync::atomic::{AtomicU8, Ordering}; + +/// Error from a timer operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct TimerError(pub i32); + +impl core::fmt::Debug for TimerError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "TimerError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for TimerError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "timer error {:#010x}", self.0 as u32) + } +} + +// ── Alarm ──────────────────────────────────────────────────────────── + +/// Alarm lifecycle states. Atomically tracks ownership of AlarmData. +const ALARM_PENDING: u8 = 0; +const ALARM_FIRED: u8 = 1; +const ALARM_CANCELLED: u8 = 2; + +struct AlarmData { + state: AtomicU8, + /// Function pointer + opaque argument for the callback. + /// Using a function pointer instead of `Box` avoids + /// heap allocation/deallocation in interrupt context. + handler: Option, +} + +struct AlarmHandler { + /// Calls the closure and frees its memory. + call: unsafe fn(*mut c_void), + /// Drops the closure without calling it (for cancellation). + drop_fn: unsafe fn(*mut c_void), + /// Raw pointer to the boxed closure. + arg: *mut c_void, +} + +// SAFETY: The *mut c_void in handler is a raw pointer to a Send type +// (the user's closure, boxed and leaked). AlarmData is only accessed +// through atomic state coordination. +unsafe impl Send for AlarmData {} +unsafe impl Sync for AlarmData {} + +/// One-shot alarm that fires a callback after a delay. +/// +/// The alarm is automatically cancelled on drop if it hasn't fired yet. +/// The callback runs in interrupt context — it must not allocate, sleep, +/// or take locks. Use a function pointer + opaque argument pattern. +/// +/// For closures, use [`after_micros`](Self::after_micros) which boxes the +/// closure on creation and frees it outside interrupt context. +pub struct Alarm { + id: SceUid, + data: *mut AlarmData, +} + +// Alarm is Send because it only holds an SceUid and a pointer whose +// ownership is transferred. The closure itself is Send. +unsafe impl Send for Alarm {} + +impl Alarm { + /// Schedule `f` to run after `delay_us` microseconds. + /// + /// The closure is boxed at creation time. The interrupt trampoline only + /// calls the closure and sets a flag — deallocation happens in `Drop` + /// or `cancel()`, never in interrupt context. + pub fn after_micros( + delay_us: u32, + f: F, + ) -> Result { + // Box the closure and leak it as a raw pointer. + let closure_ptr = alloc::boxed::Box::into_raw(alloc::boxed::Box::new(f)); + + /// Typed trampoline that calls and frees the closure. + unsafe fn call_closure(arg: *mut c_void) { + let closure = unsafe { alloc::boxed::Box::from_raw(arg as *mut F) }; + closure(); + } + + /// Drop the closure without calling it. + unsafe fn drop_closure(arg: *mut c_void) { + let _ = unsafe { alloc::boxed::Box::from_raw(arg as *mut F) }; + } + + let data = alloc::boxed::Box::into_raw(alloc::boxed::Box::new(AlarmData { + state: AtomicU8::new(ALARM_PENDING), + handler: Some(AlarmHandler { + call: call_closure::, + drop_fn: drop_closure::, + arg: closure_ptr as *mut c_void, + }), + })); + + let id = unsafe { + crate::sys::sceKernelSetAlarm(delay_us, alarm_trampoline, data as *mut c_void) + }; + + if id.0 < 0 { + // Failed — reclaim both the AlarmData and the closure. + unsafe { free_alarm_data(data) }; + return Err(TimerError(id.0)); + } + + Ok(Alarm { id, data }) + } + + /// Cancel the alarm explicitly. + /// + /// Returns `Ok(())` if cancelled before firing, or `Err` if + /// the alarm already fired or another error occurred. + pub fn cancel(self) -> Result<(), TimerError> { + let ret = unsafe { crate::sys::sceKernelCancelAlarm(self.id) }; + + let data = unsafe { &*self.data }; + // Try to claim ownership via atomic state transition. + let prev = data.state.compare_exchange( + ALARM_PENDING, + ALARM_CANCELLED, + Ordering::AcqRel, + Ordering::Acquire, + ); + + if prev.is_ok() { + // We won the race — free the data and the closure. + unsafe { free_alarm_data(self.data) }; + } + // If prev == FIRED, the trampoline already ran the callback. + // The trampoline does NOT free AlarmData, so we still free it, + // but the handler is already None. + if prev == Err(ALARM_FIRED) { + unsafe { + let _ = alloc::boxed::Box::from_raw(self.data); + } + } + + // Prevent Drop from running. + core::mem::forget(self); + if ret < 0 { + Err(TimerError(ret)) + } else { + Ok(()) + } + } +} + +impl Drop for Alarm { + fn drop(&mut self) { + let _ = unsafe { crate::sys::sceKernelCancelAlarm(self.id) }; + + let data = unsafe { &*self.data }; + let prev = data.state.compare_exchange( + ALARM_PENDING, + ALARM_CANCELLED, + Ordering::AcqRel, + Ordering::Acquire, + ); + + if prev.is_ok() { + // We won — free the data and un-called closure. + unsafe { free_alarm_data(self.data) }; + } else { + // Trampoline already fired — handler was consumed, just free AlarmData. + unsafe { + let _ = alloc::boxed::Box::from_raw(self.data); + } + } + } +} + +/// Free an AlarmData and its closure (if still present). +/// +/// # Safety +/// +/// `ptr` must be a valid `*mut AlarmData` from `Box::into_raw`. +unsafe fn free_alarm_data(ptr: *mut AlarmData) { + let mut ad = unsafe { *alloc::boxed::Box::from_raw(ptr) }; + if let Some(handler) = ad.handler.take() { + // The closure was never called — drop it without calling. + unsafe { (handler.drop_fn)(handler.arg) }; + } +} + +/// Interrupt-context trampoline for alarm callbacks. +/// +/// Atomically transitions state to FIRED, then calls the handler. +/// Does NOT deallocate — deallocation happens in Drop/cancel. +unsafe extern "C" fn alarm_trampoline(common: *mut c_void) -> u32 { + let data = unsafe { &*(common as *mut AlarmData) }; + + // Try to claim the handler. + let prev = data.state.compare_exchange( + ALARM_PENDING, + ALARM_FIRED, + Ordering::AcqRel, + Ordering::Acquire, + ); + + if prev.is_ok() { + // We won the race — execute the handler. + // SAFETY: We're the only accessor after winning the CAS. + let data_mut = unsafe { &mut *(common as *mut AlarmData) }; + if let Some(handler) = data_mut.handler.take() { + // call() both invokes and frees the closure. + unsafe { (handler.call)(handler.arg) }; + } + } + + 0 // Don't reschedule. +} + +// ── VTimer ─────────────────────────────────────────────────────────── + +/// Virtual timer with RAII cleanup. +/// +/// The timer is deleted on drop. Any registered handler is cancelled first. +pub struct VTimer { + id: SceUid, +} + +impl VTimer { + /// Create a new virtual timer. + /// + /// `name` must be a null-terminated byte string. + pub fn new(name: &[u8]) -> Result { + debug_assert!(name.last() == Some(&0), "name must be null-terminated"); + let id = unsafe { crate::sys::sceKernelCreateVTimer(name.as_ptr(), core::ptr::null_mut()) }; + if id.0 < 0 { + Err(TimerError(id.0)) + } else { + Ok(Self { id }) + } + } + + /// Start the timer. + pub fn start(&self) -> Result<(), TimerError> { + let ret = unsafe { crate::sys::sceKernelStartVTimer(self.id) }; + if ret < 0 { + Err(TimerError(ret)) + } else { + Ok(()) + } + } + + /// Stop the timer. + pub fn stop(&self) -> Result<(), TimerError> { + let ret = unsafe { crate::sys::sceKernelStopVTimer(self.id) }; + if ret < 0 { + Err(TimerError(ret)) + } else { + Ok(()) + } + } + + /// Set a wide (64-bit) timer handler. + /// + /// The handler runs in interrupt context. Return non-zero to reschedule, + /// 0 to stop. + /// + /// # Safety + /// + /// `handler` must be a valid function pointer. `common` must remain valid + /// for the lifetime of the handler registration. + pub unsafe fn set_handler_wide( + &self, + delay_us: i64, + handler: SceKernelVTimerHandlerWide, + common: *mut c_void, + ) -> Result<(), TimerError> { + let ret = unsafe { + crate::sys::sceKernelSetVTimerHandlerWide(self.id, delay_us, handler, common) + }; + if ret < 0 { + Err(TimerError(ret)) + } else { + Ok(()) + } + } + + /// Cancel the current handler. + pub fn cancel_handler(&self) -> Result<(), TimerError> { + let ret = unsafe { crate::sys::sceKernelCancelVTimerHandler(self.id) }; + if ret < 0 { + Err(TimerError(ret)) + } else { + Ok(()) + } + } + + /// Get the current timer time in microseconds. + pub fn time_us(&self) -> i64 { + unsafe { crate::sys::sceKernelGetVTimerTimeWide(self.id) } + } +} + +impl Drop for VTimer { + fn drop(&mut self) { + unsafe { + let _ = crate::sys::sceKernelCancelVTimerHandler(self.id); + let _ = crate::sys::sceKernelStopVTimer(self.id); + let _ = crate::sys::sceKernelDeleteVTimer(self.id); + } + } +} diff --git a/psp/src/usb.rs b/psp/src/usb.rs new file mode 100644 index 0000000..358fbfc --- /dev/null +++ b/psp/src/usb.rs @@ -0,0 +1,127 @@ +//! USB management for the PSP. +//! +//! Provides bus driver control and an RAII handle for USB mass storage mode. +//! When [`UsbStorageMode`] is dropped, the storage driver is deactivated +//! and stopped automatically. + +use crate::sys::UsbState; +use core::ffi::c_void; + +/// Error from a USB operation, wrapping the raw SCE error code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct UsbError(pub i32); + +impl core::fmt::Debug for UsbError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "UsbError({:#010x})", self.0 as u32) + } +} + +impl core::fmt::Display for UsbError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "USB error {:#010x}", self.0 as u32) + } +} + +/// Memory Stick storage mode product ID. +pub const USB_STOR_PID: u32 = 0x1c8; + +/// Start the USB bus driver. Required before any USB mode. +pub fn start_bus() -> Result<(), UsbError> { + let ret = unsafe { + crate::sys::sceUsbStart( + b"USBBusDriver\0".as_ptr(), + 0, + core::ptr::null_mut::(), + ) + }; + if ret < 0 { Err(UsbError(ret)) } else { Ok(()) } +} + +/// Stop the USB bus driver. +pub fn stop_bus() -> Result<(), UsbError> { + let ret = unsafe { + crate::sys::sceUsbStop( + b"USBBusDriver\0".as_ptr(), + 0, + core::ptr::null_mut::(), + ) + }; + if ret < 0 { Err(UsbError(ret)) } else { Ok(()) } +} + +/// Get current USB state flags. +pub fn state() -> UsbState { + unsafe { crate::sys::sceUsbGetState() } +} + +/// Check if a USB cable is physically connected. +pub fn is_connected() -> bool { + state().contains(UsbState::CONNECTED) +} + +/// Check if the USB connection is fully established (host mounted). +pub fn is_established() -> bool { + state().contains(UsbState::ESTABLISHED) +} + +/// RAII handle for USB storage mode. +/// +/// When dropped, deactivates USB and stops the storage driver. +pub struct UsbStorageMode { + _private: (), +} + +impl UsbStorageMode { + /// Enter USB storage mode. + /// + /// Starts the USBStor_Driver and activates with PID 0x1c8. The PSP + /// appears as a mass storage device to the host. + /// + /// The USB bus driver must be started first via [`start_bus`]. + pub fn activate() -> Result { + let ret = unsafe { + crate::sys::sceUsbStart( + b"USBStor_Driver\0".as_ptr(), + 0, + core::ptr::null_mut::(), + ) + }; + if ret < 0 { + return Err(UsbError(ret)); + } + + let ret = unsafe { crate::sys::sceUsbActivate(USB_STOR_PID) }; + if ret < 0 { + // Clean up: stop the driver we just started. + unsafe { + crate::sys::sceUsbStop( + b"USBStor_Driver\0".as_ptr(), + 0, + core::ptr::null_mut::(), + ); + } + return Err(UsbError(ret)); + } + + Ok(Self { _private: () }) + } + + /// Check if the USB storage is mounted by the host. + pub fn is_mounted(&self) -> bool { + is_established() + } +} + +impl Drop for UsbStorageMode { + fn drop(&mut self) { + unsafe { + crate::sys::sceUsbDeactivate(USB_STOR_PID); + crate::sys::sceUsbStop( + b"USBStor_Driver\0".as_ptr(), + 0, + core::ptr::null_mut::(), + ); + } + } +} diff --git a/psp/src/wlan.rs b/psp/src/wlan.rs new file mode 100644 index 0000000..d93d798 --- /dev/null +++ b/psp/src/wlan.rs @@ -0,0 +1,38 @@ +//! WiFi hardware status for the PSP. +//! +//! Provides a simple API to query WLAN chip state and MAC address. +//! This module does **not** provide networking — see [`crate::net`] for +//! TCP/UDP sockets and access point connections. + +/// WLAN hardware status. +pub struct WlanStatus { + /// Whether the WLAN chip is powered on. + pub power_on: bool, + /// Whether the physical WLAN switch is in the ON position. + pub switch_on: bool, + /// The 6-byte Ethernet (MAC) address of the WLAN interface. + pub mac_address: [u8; 6], +} + +/// Query the current WLAN hardware status. +/// +/// Returns power state, switch state, and MAC address in one call. +pub fn status() -> WlanStatus { + let power_on = unsafe { crate::sys::sceWlanDevIsPowerOn() } == 1; + let switch_on = unsafe { crate::sys::sceWlanGetSwitchState() } == 1; + let mut buf = [0u8; 8]; + unsafe { crate::sys::sceWlanGetEtherAddr(buf.as_mut_ptr()) }; + let mut mac_address = [0u8; 6]; + mac_address.copy_from_slice(&buf[..6]); + WlanStatus { + power_on, + switch_on, + mac_address, + } +} + +/// Check if WLAN is available (powered on and switch enabled). +pub fn is_available() -> bool { + let s = status(); + s.power_on && s.switch_on +}