From be617d89e6920ccf257d55e945b8bdd4c2ebd72b Mon Sep 17 00:00:00 2001 From: AI Agent Bot Date: Tue, 17 Feb 2026 04:23:26 -0600 Subject: [PATCH 1/2] feat: add connect_dialog() for WiFi via netconf utility Add psp::net::connect_dialog() which shows the PSP's built-in network configuration dialog (sceUtilityNetconfDialog) for WiFi connection. This is the standard approach used by PSP games and homebrew, and works correctly on both real hardware and PPSSPP. Also: - Derive PartialEq/Eq on ApctlState for connection state checks - Expose DIALOG_LIST and make_netconf_common as pub(crate) for reuse by the netconf dialog loop Co-Authored-By: Claude Opus 4.6 --- psp/src/dialog.rs | 11 ++++- psp/src/net.rs | 103 +++++++++++++++++++++++++++++++++++++++++++++ psp/src/sys/net.rs | 2 +- 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/psp/src/dialog.rs b/psp/src/dialog.rs index d6ed0fa..22d0f30 100644 --- a/psp/src/dialog.rs +++ b/psp/src/dialog.rs @@ -78,8 +78,15 @@ const MAX_DIALOG_ITERATIONS: u32 = 600; /// Small display list for utility dialog GU frames (16KB, 16-byte aligned). #[repr(C, align(16))] -struct Align16(T); -static mut DIALOG_LIST: Align16<[u8; 0x4000]> = Align16([0u8; 0x4000]); +pub(crate) struct Align16(pub T); +/// Shared display list for all utility dialog loops. Only accessed from +/// the main thread, never concurrently. +pub(crate) static mut DIALOG_LIST: Align16<[u8; 0x4000]> = Align16([0u8; 0x4000]); + +/// Create a `UtilityDialogCommon` for the netconf dialog. +pub(crate) fn make_netconf_common(size: u32) -> UtilityDialogCommon { + make_common(size) +} fn run_dialog(params: &mut UtilityMsgDialogParams) -> Result { let ret = diff --git a/psp/src/net.rs b/psp/src/net.rs index 065673b..b1e1aae 100644 --- a/psp/src/net.rs +++ b/psp/src/net.rs @@ -149,6 +149,109 @@ pub fn connect_ap_timeout(config_index: i32, timeout_ms: u32) -> Result<(), NetE Err(NetError(-1)) } +/// Connect to WiFi using the PSP's built-in network configuration dialog. +/// +/// Shows a system dialog that lets the user select a stored WiFi profile +/// and manages the entire connection flow (scan, auth, DHCP). This is the +/// most compatible way to establish internet connectivity — it works on +/// both real PSP hardware and PPSSPP. +/// +/// The dialog renders into the framebuffer, so the caller's GU display +/// list is closed and re-opened automatically. +/// +/// Returns `Ok(())` when connected, or `Err` if the user cancelled or +/// the dialog failed. +pub fn connect_dialog() -> Result<(), NetError> { + // Check if we're already connected. + let mut state = sys::ApctlState::Disconnected; + let ret = unsafe { sys::sceNetApctlGetState(&mut state) }; + if ret >= 0 && state == sys::ApctlState::GotIp { + return Ok(()); + } + + let mut data = sys::UtilityNetconfData { + base: crate::dialog::make_netconf_common( + core::mem::size_of::() as u32, + ), + action: sys::UtilityNetconfAction::ConnectAP, + adhocparam: core::ptr::null_mut(), + hotspot: 0, + hotspot_connected: 0, + wifisp: 0, + }; + + let ret = unsafe { sys::sceUtilityNetconfInitStart(&mut data) }; + if ret < 0 { + return Err(NetError(ret)); + } + + // Close the caller's open GU display list. + unsafe { + sys::sceGuFinish(); + sys::sceGuSync(sys::GuSyncMode::Finish, sys::GuSyncBehavior::Wait); + } + + // Dialog rendering loop (up to ~30 seconds at 60 fps). + for _ in 0..1800 { + let status = unsafe { sys::sceUtilityNetconfGetStatus() }; + if status == 0 || status < 0 { + break; + } + + // Provide a GU frame for the dialog background. + // SAFETY: DIALOG_LIST is shared with dialog.rs but both run on + // the main thread and never overlap. + unsafe { + sys::sceGuStart( + sys::GuContextType::Direct, + &raw mut crate::dialog::DIALOG_LIST as *mut core::ffi::c_void, + ); + sys::sceGuClearColor(0xff00_0000); + sys::sceGuClear(sys::ClearBuffer::COLOR_BUFFER_BIT); + sys::sceGuFinish(); + sys::sceGuSync(sys::GuSyncMode::Finish, sys::GuSyncBehavior::Wait); + } + + match status { + 2 => { unsafe { sys::sceUtilityNetconfUpdate(1); } }, + 3 => { unsafe { sys::sceUtilityNetconfShutdownStart(); } }, + _ => {}, + } + + unsafe { + sys::sceDisplayWaitVblankStart(); + sys::sceGuSwapBuffers(); + } + } + + // Drain shutdown if needed. + for _ in 0..120 { + let s = unsafe { sys::sceUtilityNetconfGetStatus() }; + match s { + 3 => unsafe { + sys::sceUtilityNetconfShutdownStart(); + sys::sceDisplayWaitVblankStart(); + }, + 4 => unsafe { + sys::sceDisplayWaitVblankStart(); + }, + _ => break, + } + } + + // Verify we actually got connected. + let mut state = sys::ApctlState::Disconnected; + let ret = unsafe { sys::sceNetApctlGetState(&mut state) }; + if ret < 0 { + return Err(NetError(ret)); + } + if state != sys::ApctlState::GotIp { + return Err(NetError(-1)); + } + + Ok(()) +} + /// Disconnect from the current access point. pub fn disconnect_ap() -> Result<(), NetError> { let ret = unsafe { sys::sceNetApctlDisconnect() }; diff --git a/psp/src/sys/net.rs b/psp/src/sys/net.rs index fe356ae..02b816c 100644 --- a/psp/src/sys/net.rs +++ b/psp/src/sys/net.rs @@ -1225,7 +1225,7 @@ psp_extern! { } #[repr(u32)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ApctlState { Disconnected, Scanning, From 49e9c76b6be7f747b077fe178b46784dde0dab80 Mon Sep 17 00:00:00 2001 From: AI Review Agent Date: Wed, 18 Feb 2026 02:12:20 -0600 Subject: [PATCH 2/2] fix: correct doc comment, error propagation, and shutdown drain loop - Fix inaccurate doc claiming GU display list is "re-opened automatically" (it is only finished; caller must re-open manually) - Propagate negative status from sceUtilityNetconfGetStatus as NetError instead of silently breaking and returning generic error - Call sceUtilityNetconfShutdownStart once in drain loop instead of repeatedly every iteration (same fix applied to dialog.rs for consistency) Co-Authored-By: Claude Opus 4.6 --- psp/src/dialog.rs | 26 ++++++++++++++++---------- psp/src/net.rs | 45 +++++++++++++++++++++++++++++---------------- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/psp/src/dialog.rs b/psp/src/dialog.rs index 22d0f30..c5b8fbc 100644 --- a/psp/src/dialog.rs +++ b/psp/src/dialog.rs @@ -153,17 +153,23 @@ fn run_dialog(params: &mut UtilityMsgDialogParams) -> Result unsafe { - crate::sys::sceUtilityMsgDialogShutdownStart(); - crate::sys::sceDisplayWaitVblankStart(); - }, - 4 => unsafe { + // Call ShutdownStart once if still in QUIT state, then wait for + // the status to drain to NONE (0). + let s = unsafe { crate::sys::sceUtilityMsgDialogGetStatus() }; + if s == 3 { + unsafe { + crate::sys::sceUtilityMsgDialogShutdownStart(); + } + } + if s == 3 || s == 4 { + for _ in 0..120 { + let s = unsafe { crate::sys::sceUtilityMsgDialogGetStatus() }; + if s != 3 && s != 4 { + break; + } + unsafe { crate::sys::sceDisplayWaitVblankStart(); - }, - _ => break, + } } } diff --git a/psp/src/net.rs b/psp/src/net.rs index b1e1aae..4674a6f 100644 --- a/psp/src/net.rs +++ b/psp/src/net.rs @@ -157,7 +157,8 @@ pub fn connect_ap_timeout(config_index: i32, timeout_ms: u32) -> Result<(), NetE /// both real PSP hardware and PPSSPP. /// /// The dialog renders into the framebuffer, so the caller's GU display -/// list is closed and re-opened automatically. +/// list is finished before the dialog runs. The caller must re-open a +/// new display list with `sceGuStart` after this function returns. /// /// Returns `Ok(())` when connected, or `Err` if the user cancelled or /// the dialog failed. @@ -171,7 +172,7 @@ pub fn connect_dialog() -> Result<(), NetError> { let mut data = sys::UtilityNetconfData { base: crate::dialog::make_netconf_common( - core::mem::size_of::() as u32, + core::mem::size_of::() as u32 ), action: sys::UtilityNetconfAction::ConnectAP, adhocparam: core::ptr::null_mut(), @@ -194,7 +195,10 @@ pub fn connect_dialog() -> Result<(), NetError> { // Dialog rendering loop (up to ~30 seconds at 60 fps). for _ in 0..1800 { let status = unsafe { sys::sceUtilityNetconfGetStatus() }; - if status == 0 || status < 0 { + if status < 0 { + return Err(NetError(status)); + } + if status == 0 { break; } @@ -213,8 +217,12 @@ pub fn connect_dialog() -> Result<(), NetError> { } match status { - 2 => { unsafe { sys::sceUtilityNetconfUpdate(1); } }, - 3 => { unsafe { sys::sceUtilityNetconfShutdownStart(); } }, + 2 => unsafe { + sys::sceUtilityNetconfUpdate(1); + }, + 3 => unsafe { + sys::sceUtilityNetconfShutdownStart(); + }, _ => {}, } @@ -224,18 +232,23 @@ pub fn connect_dialog() -> Result<(), NetError> { } } - // Drain shutdown if needed. - for _ in 0..120 { - let s = unsafe { sys::sceUtilityNetconfGetStatus() }; - match s { - 3 => unsafe { - sys::sceUtilityNetconfShutdownStart(); - sys::sceDisplayWaitVblankStart(); - }, - 4 => unsafe { + // Drain shutdown if needed. Call ShutdownStart once if still in + // QUIT state (3), then wait for FINISHED (4) to drain to NONE (0). + let s = unsafe { sys::sceUtilityNetconfGetStatus() }; + if s == 3 { + unsafe { + sys::sceUtilityNetconfShutdownStart(); + } + } + if s == 3 || s == 4 { + for _ in 0..120 { + let s = unsafe { sys::sceUtilityNetconfGetStatus() }; + if s != 3 && s != 4 { + break; + } + unsafe { sys::sceDisplayWaitVblankStart(); - }, - _ => break, + } } }