Merge xam-task-schedule-producer/p0-spawn-real-thread (XAMBUG-PRODUCER-001)
This commit is contained in:
@@ -3974,3 +3974,50 @@ likely producer-class candidates for next session:
|
|||||||
4. **GPUBUG-FETCH-PATCH-001**: re-enable the PM4_TYPE0
|
4. **GPUBUG-FETCH-PATCH-001**: re-enable the PM4_TYPE0
|
||||||
fetch-constant patch via a side-channel (GpuCommand variant)
|
fetch-constant patch via a side-channel (GpuCommand variant)
|
||||||
when draws actually start firing — relevant for bloom/blur N+1.
|
when draws actually start firing — relevant for bloom/blur N+1.
|
||||||
|
|
||||||
|
## Producer-hunt session 2026-05-03
|
||||||
|
|
||||||
|
### XAMBUG-PRODUCER-001 — XamTaskSchedule was a no-op stub
|
||||||
|
|
||||||
|
**Status:** fixed. Hypothesis falsified for the parked-waiter set.
|
||||||
|
|
||||||
|
**Site:** `crates/xenia-kernel/src/xam.rs:204` (pre-fix).
|
||||||
|
**Canary parity:** `xenia-canary/src/xenia/kernel/xam/xam_task.cc:43-80`.
|
||||||
|
|
||||||
|
The pre-fix stub allocated a handle, logged it, and returned
|
||||||
|
`STATUS_SUCCESS` — it never spawned a thread. Replaced with a
|
||||||
|
canary-faithful implementation: allocates a `ThreadImage`, allocates
|
||||||
|
a `KernelObject::Thread` handle, and routes through
|
||||||
|
`Scheduler::spawn` with `entry=callback`, `start_context=message_ptr`
|
||||||
|
(canary's third positional `XThread` arg). Stack sized as
|
||||||
|
`max(0x4000, page-aligned 0x10_0000)`.
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- Unit test `xam::tests::xam_task_schedule_spawns_real_thread`
|
||||||
|
confirms the spawned thread's `pc == callback` and `gpr[3] == message_ptr`.
|
||||||
|
- Workspace tests: 561 → 562 green.
|
||||||
|
- `--stable-digest -n 100M` lockstep: `instructions=100000002`
|
||||||
|
unchanged from baseline (interpreter determinism preserved).
|
||||||
|
- `--trace-handles-focus=0x1004,0x100c,0x15e4 -n 500M`: no
|
||||||
|
`kernel.calls{name=XamTaskSchedule}` counter appears — the call
|
||||||
|
site at `0x824a9a10` is **never reached** within 500M
|
||||||
|
instructions. Boot stalls earlier on the parked handles.
|
||||||
|
|
||||||
|
**Outcome:** the 3 focus handles still show
|
||||||
|
`signal_attempts=0 (primary=0, ghost=0)` after 500M instructions.
|
||||||
|
The XAM-task hypothesis is therefore **falsified for this run** —
|
||||||
|
XamTaskSchedule cannot be the missing producer for these specific
|
||||||
|
handles, because Sylpheed's only call site to it isn't reached
|
||||||
|
before the deadlock.
|
||||||
|
|
||||||
|
The fix lands regardless: the stub was a real correctness bug that
|
||||||
|
will manifest the moment the call site is reached (post-deadlock-resolution).
|
||||||
|
|
||||||
|
### Recommended next producer candidate
|
||||||
|
|
||||||
|
`XAudioRegisterRenderDriverClient` (currently a one-shot stub, called
|
||||||
|
once per the metric counter). Audio buffer-complete callbacks are a
|
||||||
|
known signal source on Xbox 360 audio engines; the stub may be
|
||||||
|
hiding the producer for one of the 3 handles. If that lead is also
|
||||||
|
falsified, escalate to file I/O completion (`signal_io_completion_event`
|
||||||
|
already real but possibly mis-routed) or Timer DPC delivery.
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
//! HLE kernel export implementations (xam.xex).
|
//! HLE kernel export implementations (xam.xex).
|
||||||
|
|
||||||
use crate::state::{KernelState, ModuleId};
|
use crate::objects::KernelObject;
|
||||||
|
use crate::state::{GuestMemoryPcr, KernelState, ModuleId};
|
||||||
|
use crate::thread::allocate_thread_image;
|
||||||
use xenia_cpu::PpcContext;
|
use xenia_cpu::PpcContext;
|
||||||
|
use xenia_cpu::scheduler::SpawnParams;
|
||||||
use xenia_memory::{GuestMemory, MemoryAccess};
|
use xenia_memory::{GuestMemory, MemoryAccess};
|
||||||
|
|
||||||
pub fn register_exports(state: &mut KernelState) {
|
pub fn register_exports(state: &mut KernelState) {
|
||||||
@@ -201,10 +204,85 @@ fn xam_loader_terminate_title(ctx: &mut PpcContext, _mem: &GuestMemory, _state:
|
|||||||
|
|
||||||
// ===== Task =====
|
// ===== Task =====
|
||||||
|
|
||||||
fn xam_task_schedule(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
/// `XamTaskSchedule(callback, message, optional_ptr, handle_ptr_out)` —
|
||||||
let handle = state.alloc_handle();
|
/// spawn a guest thread that runs `callback(message)` asynchronously.
|
||||||
tracing::info!("XamTaskSchedule: handle={:#x}", handle);
|
/// Mirrors xenia-canary's `XamTaskSchedule_entry` (xam_task.cc:43-80):
|
||||||
ctx.gpr[3] = 0;
|
/// stack is `max(0x4000, page-aligned default)`, the new thread enters at
|
||||||
|
/// `callback` with `message` in r3, and the resulting thread handle is
|
||||||
|
/// written to `handle_ptr_out`.
|
||||||
|
fn xam_task_schedule(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||||
|
let callback = ctx.gpr[3] as u32;
|
||||||
|
let message_ptr = ctx.gpr[4] as u32;
|
||||||
|
let optional_ptr = ctx.gpr[5] as u32;
|
||||||
|
let handle_ptr = ctx.gpr[6] as u32;
|
||||||
|
let lr = ctx.lr as u32;
|
||||||
|
|
||||||
|
if optional_ptr != 0 {
|
||||||
|
let v1 = mem.read_u32(optional_ptr);
|
||||||
|
let v2 = mem.read_u32(optional_ptr + 4);
|
||||||
|
tracing::info!("XamTaskSchedule: args v1={:#010x} v2={:#010x}", v1, v2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stack_size = std::cmp::max(0x4000u32, (0x10_0000u32 + 0xFFF) & !0xFFF);
|
||||||
|
|
||||||
|
let Some(image) = allocate_thread_image(state, mem, stack_size, 0) else {
|
||||||
|
tracing::error!("XamTaskSchedule: failed to allocate thread image");
|
||||||
|
ctx.gpr[3] = 0xC000_009A; // STATUS_INSUFFICIENT_RESOURCES
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
let tid = state.next_thread_id.fetch_add(1, Ordering::Relaxed);
|
||||||
|
let handle = state.alloc_handle_for(KernelObject::Thread {
|
||||||
|
id: tid,
|
||||||
|
hw_id: None,
|
||||||
|
exit_code: None,
|
||||||
|
waiters: Vec::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let tls_slot_count = state.next_tls_index.load(Ordering::Relaxed);
|
||||||
|
let params = SpawnParams {
|
||||||
|
entry: callback,
|
||||||
|
start_context: message_ptr,
|
||||||
|
stack_base: image.stack_base,
|
||||||
|
stack_size: image.stack_size,
|
||||||
|
pcr_base: image.pcr_base,
|
||||||
|
tls_base: image.tls_base,
|
||||||
|
thread_handle: handle,
|
||||||
|
guest_tid: tid,
|
||||||
|
create_suspended: false,
|
||||||
|
is_initial: false,
|
||||||
|
tls_slot_count,
|
||||||
|
affinity_mask: 0,
|
||||||
|
priority: 0,
|
||||||
|
ideal_processor: None,
|
||||||
|
};
|
||||||
|
match state.scheduler.spawn(params, &mut GuestMemoryPcr(mem)) {
|
||||||
|
Ok(hw_id) => {
|
||||||
|
metrics::counter!("scheduler.spawn.ok").increment(1);
|
||||||
|
if let Some(KernelObject::Thread { hw_id: slot, .. }) = state.objects.get_mut(&handle) {
|
||||||
|
*slot = Some(hw_id);
|
||||||
|
}
|
||||||
|
if handle_ptr != 0 {
|
||||||
|
mem.write_u32(handle_ptr, handle);
|
||||||
|
}
|
||||||
|
state.audit_create(handle, "Thread", lr, "XamTaskSchedule");
|
||||||
|
tracing::info!(
|
||||||
|
"XamTaskSchedule: tid={} handle={:#x} hw={} callback={:#010x} message={:#010x}",
|
||||||
|
tid,
|
||||||
|
handle,
|
||||||
|
hw_id,
|
||||||
|
callback,
|
||||||
|
message_ptr,
|
||||||
|
);
|
||||||
|
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
metrics::counter!("scheduler.spawn.rejected").increment(1);
|
||||||
|
tracing::error!("XamTaskSchedule: no free HW thread slot");
|
||||||
|
ctx.gpr[3] = 0xC000_009A;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Alloc =====
|
// ===== Alloc =====
|
||||||
@@ -326,3 +404,66 @@ fn xget_video_mode(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelS
|
|||||||
}
|
}
|
||||||
ctx.gpr[3] = 0;
|
ctx.gpr[3] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use xenia_memory::page_table::MemoryProtect;
|
||||||
|
|
||||||
|
const SCRATCH_BASE: u32 = 0x4000_0000;
|
||||||
|
|
||||||
|
fn fresh() -> (PpcContext, GuestMemory, KernelState) {
|
||||||
|
let mut mem = GuestMemory::new().expect("memory init");
|
||||||
|
mem.alloc(SCRATCH_BASE, 0x1000, MemoryProtect::READ | MemoryProtect::WRITE)
|
||||||
|
.expect("scratch page must commit");
|
||||||
|
let mut state = KernelState::new();
|
||||||
|
state.install_initial_thread(
|
||||||
|
PpcContext::default(),
|
||||||
|
0x7000_0000,
|
||||||
|
0x10_0000,
|
||||||
|
SCRATCH_BASE + 0x800,
|
||||||
|
SCRATCH_BASE + 0xC00,
|
||||||
|
0xF000_0001,
|
||||||
|
&mut mem,
|
||||||
|
);
|
||||||
|
state.scheduler.begin_slot_visit(0);
|
||||||
|
(PpcContext::default(), mem, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xam_task_schedule_spawns_real_thread() {
|
||||||
|
let (mut ctx, mut mem, mut state) = fresh();
|
||||||
|
|
||||||
|
let callback_pc: u32 = 0x824a_93c8;
|
||||||
|
let message_ptr: u32 = SCRATCH_BASE + 0x100;
|
||||||
|
let handle_out: u32 = SCRATCH_BASE + 0x200;
|
||||||
|
ctx.gpr[3] = callback_pc as u64;
|
||||||
|
ctx.gpr[4] = message_ptr as u64;
|
||||||
|
ctx.gpr[5] = 0;
|
||||||
|
ctx.gpr[6] = handle_out as u64;
|
||||||
|
ctx.lr = 0x824a_9a14;
|
||||||
|
|
||||||
|
xam_task_schedule(&mut ctx, &mut mem, &mut state);
|
||||||
|
|
||||||
|
assert_eq!(ctx.gpr[3], 0, "XamTaskSchedule must return STATUS_SUCCESS");
|
||||||
|
|
||||||
|
let handle = mem.read_u32(handle_out);
|
||||||
|
assert!(handle >= 0x1000, "handle must be allocated, got {:#x}", handle);
|
||||||
|
|
||||||
|
let r = state
|
||||||
|
.scheduler
|
||||||
|
.find_by_handle(handle)
|
||||||
|
.expect("spawned thread must be findable by handle");
|
||||||
|
let new_ctx = state.scheduler.ctx_mut_ref(r);
|
||||||
|
assert_eq!(new_ctx.pc, callback_pc, "entry PC must be the callback");
|
||||||
|
assert_eq!(
|
||||||
|
new_ctx.gpr[3] as u32, message_ptr,
|
||||||
|
"r3 must hold the message pointer"
|
||||||
|
);
|
||||||
|
|
||||||
|
match state.objects.get(&handle) {
|
||||||
|
Some(KernelObject::Thread { hw_id: Some(_), .. }) => {}
|
||||||
|
other => panic!("expected Thread object with hw_id set, got {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user