[AUDIT-059 R-D2] Phase D auto-signal POC confirms audit-049 wedge diagnosis
Hook NtCreateEvent for the silph::UImpl tid=13 chain (entry=0x821748F0, start_context=0x4024a840, frame-1 LR=0x821CB15C inside sub_821CB030+0x128) and auto-signal the resulting handle after XENIA_SILPH_UI_AUTOSIGNAL_DELAY instructions. Env-gated; default off. SR4 verdict B (partial unwedge): - handle 0x1078 signal_attempts 0->1 - tid=13 Blocked(WaitAny[0x1078]) -> Ready pc=0x824a9108 - ExCreateThread 10 -> 12 (new silph::UImpl tid=14, worker tid=15) - New downstream wedges 0x1084 + 0x1088 - cxx_throw runtime_error on tid=5 inside R26 dispatcher (BST not-registered instance lhs=0x715a7af0) - VdSwap stays 1; no draws (POC is diagnostic, not final fix) Confirms Phase C diagnosis end-to-end. The real signaler must (a) drive NtSetEvent on the silph KEVENT AND (b) register the dispatcher's BST instance upstream; this POC only does (a). Reading-error class #20: ctx.lr at kernel export entry is the thunk wrapper's return slot, NOT the guest caller's post-bl PC. Walk back-chain 1 step to get frames[1].lr. Reading-error class #21: --parallel and lockstep have SEPARATE outer loops in main.rs (run_execution_parallel line 2928 vs run_execution line 2706). Per-round hooks must be wired in BOTH paths. Files: - crates/xenia-cpu/src/scheduler.rs: GuestThread.start_entry/start_context fields + spawn() population + current_thread_entry_and_ctx() helper - crates/xenia-kernel/src/state.rs: AutoSignalPending struct, env-parsed silph_autosignal_delay, pending Vec, last_cycle_hint, set_now_cycle_hint, maybe_register_silph_autosignal (walks back-chain), fire_due_silph_autosignals - crates/xenia-kernel/src/exports.rs: hook in nt_create_event - crates/xenia-app/src/main.rs: fire-site + cycle hint in both outer loops - audit-runs/audit-059-handle-disambiguation/round-D2-autosignal-poc/FINDINGS.md Tests 655/655 green. Default behavior byte-identical when env unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2001,6 +2001,9 @@ fn nt_create_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelSt
|
||||
mem,
|
||||
"NtCreateEvent",
|
||||
);
|
||||
// ITERATE-2C Phase D — audit-049 auto-signal POC. Env-gated; no-op
|
||||
// when `XENIA_SILPH_UI_AUTOSIGNAL_DELAY` is unset.
|
||||
state.maybe_register_silph_autosignal(handle, ctx, mem);
|
||||
if handle_ptr != 0 {
|
||||
mem.write_u32(handle_ptr, handle);
|
||||
}
|
||||
|
||||
@@ -325,6 +325,32 @@ pub struct KernelState {
|
||||
pub silph_synth_handles: [Option<u32>; 4],
|
||||
/// AUDIT-2.BF — `ThreadRef` cache for the 4 synthetic workers.
|
||||
pub silph_synth_refs: [Option<xenia_cpu::ThreadRef>; 4],
|
||||
/// ITERATE-2C Phase D — auto-signal delay for silph::UImpl
|
||||
/// `NtCreateEvent` calls (see [`Self::maybe_register_silph_autosignal`]).
|
||||
/// `None` = feature disabled; populated once from
|
||||
/// `XENIA_SILPH_UI_AUTOSIGNAL_DELAY=<u64>` at construction.
|
||||
pub silph_autosignal_delay: Option<u64>,
|
||||
/// ITERATE-2C Phase D — pending auto-signal queue. Drained each
|
||||
/// outer round by [`Self::fire_due_silph_autosignals`].
|
||||
pub silph_autosignal_pending: Vec<AutoSignalPending>,
|
||||
/// ITERATE-2C Phase D — most recent `stats.instruction_count`
|
||||
/// deposited by the scheduler loop (see
|
||||
/// [`Self::set_now_cycle_hint`]). Used by
|
||||
/// [`Self::maybe_register_silph_autosignal`] to compute absolute
|
||||
/// deadlines, since `nt_create_event` doesn't see `ExecStats`.
|
||||
pub last_cycle_hint: u64,
|
||||
/// ITERATE-2C Phase D — one-shot diagnostic latch. Flipped by
|
||||
/// [`Self::fire_due_silph_autosignals`] on the first visit where
|
||||
/// the pending queue is non-empty but no entry is due yet.
|
||||
pub silph_autosignal_diag_logged: bool,
|
||||
}
|
||||
|
||||
/// ITERATE-2C Phase D — one queued auto-signal. `deadline_cycle` is
|
||||
/// absolute (cycle hint at register time + configured delay).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct AutoSignalPending {
|
||||
pub handle: u32,
|
||||
pub deadline_cycle: u64,
|
||||
}
|
||||
|
||||
impl KernelState {
|
||||
@@ -400,6 +426,12 @@ impl KernelState {
|
||||
silph_synth_ctx: 0,
|
||||
silph_synth_handles: [None; 4],
|
||||
silph_synth_refs: [None; 4],
|
||||
silph_autosignal_delay: std::env::var("XENIA_SILPH_UI_AUTOSIGNAL_DELAY")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u64>().ok()),
|
||||
silph_autosignal_pending: Vec::new(),
|
||||
last_cycle_hint: 0,
|
||||
silph_autosignal_diag_logged: false,
|
||||
};
|
||||
crate::exports::register_exports(&mut state);
|
||||
crate::xam::register_exports(&mut state);
|
||||
@@ -800,6 +832,122 @@ impl KernelState {
|
||||
self.audit.record_wake(handle, entry);
|
||||
}
|
||||
|
||||
/// ITERATE-2C Phase D — deposit the latest scheduler instruction
|
||||
/// count so `nt_create_event` can compute absolute auto-signal
|
||||
/// deadlines. Called once per outer round from the app's
|
||||
/// `coord_pre_round` site. No-op when the feature env is unset.
|
||||
pub fn set_now_cycle_hint(&mut self, now_cycle: u64) {
|
||||
self.last_cycle_hint = now_cycle;
|
||||
}
|
||||
|
||||
/// ITERATE-2C Phase D — register a freshly-allocated event for
|
||||
/// auto-signal after the configured delay, **iff** the creating
|
||||
/// thread matches the silph::UImpl tid=13 chain that wedges in
|
||||
/// audit-049. Filter:
|
||||
///
|
||||
/// * Env `XENIA_SILPH_UI_AUTOSIGNAL_DELAY` set (= delay non-None)
|
||||
/// * Frame-1 LR (the guest caller's post-bl PC, walked one step up
|
||||
/// from the live thunk-wrapper frame) is in
|
||||
/// `[0x821CB15C, 0x821CB160]` — this is the `NtCreateEvent` call
|
||||
/// site inside `sub_821CB030+0x128`. The live `ctx.lr` is the
|
||||
/// thunk wrapper's return slot (e.g. `0x824a9f6c`), so we walk
|
||||
/// one back-chain step to reach the actual guest caller.
|
||||
/// * Creating thread's `start_entry == 0x821748F0` (silph trampoline)
|
||||
/// * Creating thread's `start_context == 0x4024a840`
|
||||
///
|
||||
/// On match, the handle is queued with `deadline = last_cycle_hint +
|
||||
/// delay`. Drained by [`Self::fire_due_silph_autosignals`] from the
|
||||
/// outer scheduler loop.
|
||||
pub fn maybe_register_silph_autosignal(
|
||||
&mut self,
|
||||
handle: u32,
|
||||
ctx: &PpcContext,
|
||||
mem: &GuestMemory,
|
||||
) {
|
||||
let Some(delay) = self.silph_autosignal_delay else {
|
||||
return;
|
||||
};
|
||||
let Some((entry, start_ctx)) = self.scheduler.current_thread_entry_and_ctx() else {
|
||||
return;
|
||||
};
|
||||
if entry != 0x821748F0 || start_ctx != 0x4024_a840 {
|
||||
return;
|
||||
}
|
||||
let frames = walk_guest_back_chain(ctx.gpr[1] as u32, ctx.lr as u32, mem, 2);
|
||||
let caller_lr = match frames.get(1) {
|
||||
Some((_, lr)) => *lr,
|
||||
None => return,
|
||||
};
|
||||
if !(0x821CB15C..=0x821CB160).contains(&caller_lr) {
|
||||
return;
|
||||
}
|
||||
let deadline = self.last_cycle_hint.saturating_add(delay);
|
||||
self.silph_autosignal_pending
|
||||
.push(AutoSignalPending { handle, deadline_cycle: deadline });
|
||||
tracing::info!(
|
||||
"silph autosignal: scheduled handle={:#x} caller_lr={:#x} for cycle {} (now={}, delay={})",
|
||||
handle,
|
||||
caller_lr,
|
||||
deadline,
|
||||
self.last_cycle_hint,
|
||||
delay,
|
||||
);
|
||||
}
|
||||
|
||||
/// ITERATE-2C Phase D — drain pending entries whose deadline has
|
||||
/// passed. Each fires by setting `Event { signaled = true }` and
|
||||
/// invoking the existing `wake_eligible_waiters` to release blocked
|
||||
/// waiters. No-op when the queue is empty (the common case).
|
||||
pub fn fire_due_silph_autosignals(&mut self, now_cycle: u64) {
|
||||
if self.silph_autosignal_pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
let any_due = self
|
||||
.silph_autosignal_pending
|
||||
.iter()
|
||||
.any(|p| p.deadline_cycle <= now_cycle);
|
||||
if !any_due {
|
||||
// Diagnostic for the Phase D POC: log first time we visit
|
||||
// with a non-empty queue but nothing due yet.
|
||||
if !self.silph_autosignal_diag_logged {
|
||||
self.silph_autosignal_diag_logged = true;
|
||||
if let Some(first) = self.silph_autosignal_pending.first() {
|
||||
tracing::info!(
|
||||
"silph autosignal: tick (first visit, none due) now={} pending={} first_deadline={}",
|
||||
now_cycle,
|
||||
self.silph_autosignal_pending.len(),
|
||||
first.deadline_cycle,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut i = 0;
|
||||
while i < self.silph_autosignal_pending.len() {
|
||||
if self.silph_autosignal_pending[i].deadline_cycle <= now_cycle {
|
||||
let p = self.silph_autosignal_pending.swap_remove(i);
|
||||
let prev = match self.objects.get_mut(&p.handle) {
|
||||
Some(KernelObject::Event { signaled, .. }) => {
|
||||
let was = *signaled;
|
||||
*signaled = true;
|
||||
Some(was)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
tracing::info!(
|
||||
"silph autosignal: firing handle={:#x} prev_signaled={:?} at cycle {}",
|
||||
p.handle,
|
||||
prev,
|
||||
now_cycle,
|
||||
);
|
||||
self.audit_signal(p.handle, 0, "silph_autosignal", prev.unwrap_or(false) as u64);
|
||||
crate::exports::wake_eligible_waiters(self, p.handle);
|
||||
// do not advance i — swap_remove pulled a new entry into i
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Diagnostic. If the live PC for HW slot `hw_id` is in
|
||||
/// `self.ctor_probe_pcs`, emit a single `CTOR-PROBE` line with
|
||||
/// the current cycle, tid, hw_id, sp, r3, lr, plus an 8-frame
|
||||
|
||||
Reference in New Issue
Block a user