From 0e24e980fbf837583b6c46720c97df8b063a4a9a Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 5 Jul 2026 13:29:52 +0200 Subject: [PATCH] [iterate-4C] M3: default connected controller for user 0 (reach the Press A title loop) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sylpheed's boot reaches its front-end TITLE shell (steady frame loop — 2219 swaps over 5B instr; XamResetInactivity/XamEnableInactivityProcessing/ XamUserGetSigninState front-end mgmt; XamInputGetCapabilities polled 8880x) but then STALLS: headless has no HID device, so GetCapabilities returns DEVICE_NOT_CONNECTED and the game loops forever waiting for a controller. It never advances to read input (XamInputGetState / XamInputGetKeystrokeEx = 0). Canary reaches the "Press A" menu because its default input drivers (xinput / winkey) present an always-connected controller. Match that: present a default always-connected, IDLE (no-buttons) virtual controller for user 0 (a console always exposes controller slots; a real gamepad under --ui still supplies actual button state — see xam_input_get_state, which prefers ui.snapshot_gamepad()). Set XENIA_NO_PAD=1 to present no controller (the raw headless no-HID trajectory). MEASURED (headless, this is not inference): with the controller connected the game advances into the interactive input-poll loop — XamInputGetKeystrokeEx 0 -> 2164 (once per frame) XamInputGetState 0 -> 2165 XamInputSetState (rumble) 0 -> 1 i.e. the "Press A" title-screen loop (attract video repeats while it polls for A), exactly the M3 milestone behavior. Visual confirmation of the on-screen "Press A" prompt is the remaining step (--ui + frame readback). GOLDEN RE-BASELINE (deliberate): a connected controller changes the guest trajectory WITHIN the 200M window (the title polls input before 200M), so both sylpheed_n200m.json and sylpheed_n2m.json are re-baselined. Change is proven ISOLATED: XENIA_NO_PAD=1 reproduces the OLD goldens byte-identical at both n. New n200m: instructions 200000203, imports 575647, draws 3208, swaps 910 (was 200000239 / 575447 / 3165 / 895 — small front-end-input increments; RTs/ shaders/textures unchanged). All 6 JIT configs match the new golden byte- identical (interp / JIT / JIT+CHAIN / +REGCACHE / REGION_MAX=1 / budget=1== interp+budget=1); 131 xenia-kernel + 21 xenia-jit tests green. --- .../tests/golden/sylpheed_n200m.json | 8 ++-- crates/xenia-kernel/src/xam.rs | 39 +++++++++++++++---- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/crates/xenia-app/tests/golden/sylpheed_n200m.json b/crates/xenia-app/tests/golden/sylpheed_n200m.json index 4c70278..2b9a5c7 100644 --- a/crates/xenia-app/tests/golden/sylpheed_n200m.json +++ b/crates/xenia-app/tests/golden/sylpheed_n200m.json @@ -1,9 +1,9 @@ { - "instructions": 200000239, - "imports": 575447, + "instructions": 200000203, + "imports": 575647, "unimpl": 0, - "draws": 3165, - "swaps": 895, + "draws": 3208, + "swaps": 910, "unique_render_targets": 2, "shader_blobs_live": 6, "texture_cache_entries": 1 diff --git a/crates/xenia-kernel/src/xam.rs b/crates/xenia-kernel/src/xam.rs index 9950e45..6ad9111 100644 --- a/crates/xenia-kernel/src/xam.rs +++ b/crates/xenia-kernel/src/xam.rs @@ -125,6 +125,29 @@ fn gamepad_key(state: &xenia_hid::GamepadState) -> u128 { u128::from_be_bytes(bytes) } +/// Whether user 0 has a default always-connected, idle (no-buttons) virtual +/// controller. **Default ON** — matches canary, whose default input drivers +/// (xinput / winkey) present a connected pad, so a title screen advances to its +/// "Press A" input-poll state instead of stalling forever on +/// DEVICE_NOT_CONNECTED (a console always exposes controller slots; a real +/// gamepad under `--ui` supplies actual button state, otherwise the pad is idle). +/// Set `XENIA_NO_PAD=1` to present NO controller (reproduces the raw headless +/// no-HID trajectory). +fn virtual_pad_enabled() -> bool { + use std::sync::OnceLock; + static V: OnceLock = OnceLock::new(); + *V.get_or_init(|| { + !std::env::var("XENIA_NO_PAD") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false) + }) +} + +/// True iff user 0 has a controller: a real host gamepad, or the default pad. +fn user_connected(state: &KernelState, user: u32) -> bool { + state.ui.as_ref().is_some_and(|ui| ui.is_connected(user)) || (user == 0 && virtual_pad_enabled()) +} + fn xam_input_get_capabilities( ctx: &mut PpcContext, mem: &GuestMemory, @@ -133,8 +156,7 @@ fn xam_input_get_capabilities( // r3 = user_index, r4 = flags, r5 = out X_INPUT_CAPABILITIES* let user = ctx.gpr[3] as u32; let out_ptr = ctx.gpr[5] as u32; - let connected = state.ui.as_ref().is_some_and(|ui| ui.is_connected(user)); - if !connected { + if !user_connected(state, user) { ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64; return; } @@ -146,15 +168,16 @@ fn xam_input_get_state(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut Kern // r3 = user_index, r4 = flags, r5 = out X_INPUT_STATE* let user = ctx.gpr[3] as u32; let out_ptr = ctx.gpr[5] as u32; - let Some(ui) = state.ui.as_ref() else { - ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64; - return; - }; - if !ui.is_connected(user) { + if !user_connected(state, user) { ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64; return; } - let gamepad = ui.snapshot_gamepad(); + // Real host gamepad if present, else the idle virtual pad (XENIA_VIRTUAL_PAD). + let gamepad = state + .ui + .as_ref() + .map(|ui| ui.snapshot_gamepad()) + .unwrap_or_default(); let key = gamepad_key(&gamepad); if key != state.last_input_bytes { state.input_packet_number = state.input_packet_number.wrapping_add(1);