xenia-kernel: HLE expansion, scheduler integration, audit + UI bridge
Major HLE buildout in exports.rs: KeInitializeSemaphore now seeds
count/limit, XexGet{Module,Procedure}Address use distinct
HMODULE_XBOXKRNL/HMODULE_XAM pseudo-handles with a reverse
(ModuleId,ordinal)→thunk_addr map, plus sweeping additions across
sync primitives, file I/O, semaphores, events, threads, and
allocator paths needed to advance Sylpheed past VdSwap=2.
New modules:
- thread.rs — ThreadRef + per-thread suspension/wake plumbing
- interrupts.rs — IRQ delivery, pending-IRQ slots, IPI helpers
- path.rs — guest path normalization (D:\\, game:\\, etc.)
- audit.rs — --trace-handles harness backing the handle audit
- ui_bridge.rs — kernel-side endpoint of the xenia-ui bridge
(input snapshots, framebuffer publish handles)
state.rs grows to own the HW-slot scheduler state, the new audit /
UI bridge handles, and the per-handle reverse maps. xam.rs and
objects.rs follow suit for the HLE additions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
195
crates/xenia-kernel/src/audit.rs
Normal file
195
crates/xenia-kernel/src/audit.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
//! Per-handle audit trail for diagnosing HLE sync gaps.
|
||||
//!
|
||||
//! When enabled (via `--trace-handles` / `XENIA_TRACE_HANDLES=1`), the kernel
|
||||
//! records every handle's create/signal/wait/wake events into a bounded
|
||||
//! ring per handle. `dump_thread_diagnostic` (in `xenia-app`) prints the
|
||||
//! trail at end-of-run, which lets a session see *who* signaled (or failed
|
||||
//! to signal) a given handle and *who* parked on it.
|
||||
//!
|
||||
//! The harness is behavior-neutral: when `enabled = false` (the default),
|
||||
//! every record method is an `#[inline]` no-op. When enabled, each record
|
||||
//! costs an O(1) HashMap probe + a `VecDeque::push_back` with a bounded
|
||||
//! `pop_front` to keep memory at ~32 KiB per handle worst case.
|
||||
//!
|
||||
//! See [project_xenia_rs_scheduler.md] note on the latent
|
||||
//! `scheduler.deadlock_recoveries` event during boot — this harness exists
|
||||
//! to identify which kernel API should signal handles
|
||||
//! `0x10FC / 0x1014 / 0x1104 / 0x10DC / 0x10F0` but doesn't.
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
/// Maximum events per category per handle. Bounded so a long-running session
|
||||
/// doesn't OOM if a handle is signaled millions of times.
|
||||
pub const AUDIT_RING_CAPACITY: usize = 32;
|
||||
|
||||
/// One audit record. Captured at the export's call site so `lr` points at
|
||||
/// the guest caller (one instruction past the `bl` to the kernel thunk).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct HandleAuditEntry {
|
||||
/// Per-thread timebase tick at the time of the event. Useful for
|
||||
/// ordering events across threads — same units as
|
||||
/// `Scheduler::ctx(0).timebase`.
|
||||
pub cycle: u64,
|
||||
/// Guest thread id (NOT hw_id — `tid` survives migration).
|
||||
pub tid: u32,
|
||||
/// Caller's LR (the guest pc one past the `bl` to the export).
|
||||
pub lr: u32,
|
||||
/// Stable, kernel-internal label naming the source export. e.g.
|
||||
/// "KeSetEvent", "NtSetEvent", "wake_eligible_waiters".
|
||||
pub source: &'static str,
|
||||
/// Free-form auxiliary data. For signals: previous_state. For waits:
|
||||
/// `(alertable, timeout_ns_or_max)` packed. For wakes: `gpr[3]` set.
|
||||
/// Read by callers as needed.
|
||||
pub aux: u64,
|
||||
}
|
||||
|
||||
/// Per-handle audit trail. Lives in `KernelState::audit.trails`.
|
||||
#[derive(Debug)]
|
||||
pub struct HandleAuditTrail {
|
||||
/// Stable label: "Event/Manual", "Event/Auto", "Semaphore", "Timer/Manual",
|
||||
/// "Timer/Auto", "Mutant", "Thread". Used for filtering in the dump.
|
||||
pub kind: &'static str,
|
||||
/// When/who/where the handle was minted.
|
||||
pub created: HandleAuditEntry,
|
||||
/// Bounded ring of signal events.
|
||||
pub signals: VecDeque<HandleAuditEntry>,
|
||||
/// Bounded ring of wait-entry events (one per `Wait*` call).
|
||||
pub waits: VecDeque<HandleAuditEntry>,
|
||||
/// Bounded ring of wake events (one per scheduler-side wake).
|
||||
pub wakes: VecDeque<HandleAuditEntry>,
|
||||
}
|
||||
|
||||
impl HandleAuditTrail {
|
||||
fn new(kind: &'static str, created: HandleAuditEntry) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
created,
|
||||
signals: VecDeque::with_capacity(AUDIT_RING_CAPACITY),
|
||||
waits: VecDeque::with_capacity(AUDIT_RING_CAPACITY),
|
||||
wakes: VecDeque::with_capacity(AUDIT_RING_CAPACITY),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The audit table itself. Lives on `KernelState`; opt-in via `enabled`.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct HandleAudit {
|
||||
pub trails: HashMap<u32, HandleAuditTrail>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl HandleAudit {
|
||||
/// Push an entry into a bounded ring, dropping the oldest when full.
|
||||
#[inline]
|
||||
fn push_bounded(ring: &mut VecDeque<HandleAuditEntry>, entry: HandleAuditEntry) {
|
||||
if ring.len() == AUDIT_RING_CAPACITY {
|
||||
ring.pop_front();
|
||||
}
|
||||
ring.push_back(entry);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn record_create(&mut self, handle: u32, kind: &'static str, entry: HandleAuditEntry) {
|
||||
if !self.enabled {
|
||||
return;
|
||||
}
|
||||
self.trails
|
||||
.insert(handle, HandleAuditTrail::new(kind, entry));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn record_signal(&mut self, handle: u32, entry: HandleAuditEntry) {
|
||||
if !self.enabled {
|
||||
return;
|
||||
}
|
||||
if let Some(trail) = self.trails.get_mut(&handle) {
|
||||
Self::push_bounded(&mut trail.signals, entry);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn record_wait(&mut self, handle: u32, entry: HandleAuditEntry) {
|
||||
if !self.enabled {
|
||||
return;
|
||||
}
|
||||
if let Some(trail) = self.trails.get_mut(&handle) {
|
||||
Self::push_bounded(&mut trail.waits, entry);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn record_wake(&mut self, handle: u32, entry: HandleAuditEntry) {
|
||||
if !self.enabled {
|
||||
return;
|
||||
}
|
||||
if let Some(trail) = self.trails.get_mut(&handle) {
|
||||
Self::push_bounded(&mut trail.wakes, entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: `(signal_count, wait_count, wake_count)` for a handle.
|
||||
/// Returns `None` if no trail exists.
|
||||
pub fn counts(&self, handle: u32) -> Option<(usize, usize, usize)> {
|
||||
self.trails
|
||||
.get(&handle)
|
||||
.map(|t| (t.signals.len(), t.waits.len(), t.wakes.len()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn entry(cycle: u64, source: &'static str) -> HandleAuditEntry {
|
||||
HandleAuditEntry { cycle, tid: 1, lr: 0x8200_0000, source, aux: 0 }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_audit_is_a_noop() {
|
||||
let mut a = HandleAudit::default();
|
||||
a.record_create(0x1000, "Event/Auto", entry(0, "NtCreateEvent"));
|
||||
a.record_signal(0x1000, entry(1, "NtSetEvent"));
|
||||
assert!(a.trails.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabled_records_create_and_events() {
|
||||
let mut a = HandleAudit { enabled: true, ..HandleAudit::default() };
|
||||
a.record_create(0x1014, "Event/Auto", entry(0, "NtCreateEvent"));
|
||||
a.record_signal(0x1014, entry(10, "NtSetEvent"));
|
||||
a.record_wait(0x1014, entry(5, "NtWaitForSingleObjectEx"));
|
||||
a.record_wake(0x1014, entry(11, "wake_eligible_waiters"));
|
||||
|
||||
let counts = a.counts(0x1014).unwrap();
|
||||
assert_eq!(counts, (1, 1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_for_unknown_handle_is_dropped() {
|
||||
let mut a = HandleAudit { enabled: true, ..HandleAudit::default() };
|
||||
// No `record_create` first → handle has no trail.
|
||||
a.record_signal(0x9999, entry(1, "NtSetEvent"));
|
||||
assert!(a.trails.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_is_bounded_to_capacity() {
|
||||
let mut a = HandleAudit { enabled: true, ..HandleAudit::default() };
|
||||
a.record_create(0x10FC, "Event/Auto", entry(0, "NtCreateEvent"));
|
||||
for i in 0..(AUDIT_RING_CAPACITY * 3) as u64 {
|
||||
a.record_signal(0x10FC, entry(i, "NtSetEvent"));
|
||||
}
|
||||
let trail = &a.trails[&0x10FC];
|
||||
assert_eq!(trail.signals.len(), AUDIT_RING_CAPACITY);
|
||||
// Oldest should have been dropped — the first remaining entry is at
|
||||
// cycle = 2 * AUDIT_RING_CAPACITY (i.e. 64 if capacity = 32).
|
||||
let first = trail.signals.front().unwrap();
|
||||
assert_eq!(first.cycle, (AUDIT_RING_CAPACITY * 2) as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_handle_counts_returns_none() {
|
||||
let a = HandleAudit::default();
|
||||
assert!(a.counts(0x10FC).is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user