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:
@@ -8,6 +8,10 @@ license.workspace = true
|
|||||||
xenia-types = { workspace = true }
|
xenia-types = { workspace = true }
|
||||||
xenia-memory = { workspace = true }
|
xenia-memory = { workspace = true }
|
||||||
xenia-cpu = { workspace = true }
|
xenia-cpu = { workspace = true }
|
||||||
|
xenia-vfs = { workspace = true }
|
||||||
|
xenia-hid = { workspace = true }
|
||||||
|
xenia-gpu = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
metrics = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|||||||
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
424
crates/xenia-kernel/src/interrupts.rs
Normal file
424
crates/xenia-kernel/src/interrupts.rs
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
//! Graphics interrupt + synthetic v-sync bookkeeping (P6).
|
||||||
|
//!
|
||||||
|
//! The Xbox 360 graphics driver calls `VdSetGraphicsInterruptCallback` to
|
||||||
|
//! register a single per-process callback that the OS invokes on:
|
||||||
|
//!
|
||||||
|
//! 1. **V-sync** — at 60 Hz; source code 0 (`INTERRUPT_SOURCE_VSYNC`).
|
||||||
|
//! 2. **Command-processor interrupt** — when `PM4_INTERRUPT` fires from the
|
||||||
|
//! guest-issued command stream; source code 1 (`INTERRUPT_SOURCE_CP`).
|
||||||
|
//!
|
||||||
|
//! Canary's [xboxkrnl_video.cc:303-310](xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_video.cc#L303-L310)
|
||||||
|
//! dispatches the callback on HW thread 0. We follow the same convention.
|
||||||
|
//!
|
||||||
|
//! The delivery model is cooperative: we inject the callback entry into HW
|
||||||
|
//! thread 0 at the top of a scheduler round when it's safe (not mid-export,
|
||||||
|
//! not already inside another interrupt). When the callback returns to
|
||||||
|
//! [`LR_HALT_SENTINEL`] the main loop restores the saved [`PpcContext`]
|
||||||
|
//! fields and the HW thread picks up where it left off.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use xenia_cpu::context::{CrField, PpcContext};
|
||||||
|
use xenia_cpu::ThreadRef;
|
||||||
|
|
||||||
|
pub const INTERRUPT_SOURCE_VSYNC: u32 = 0;
|
||||||
|
pub const INTERRUPT_SOURCE_CP: u32 = 1;
|
||||||
|
|
||||||
|
/// Guest-registered V-sync / graphics-interrupt callback (from
|
||||||
|
/// `VdSetGraphicsInterruptCallback`).
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct GraphicsInterruptCallback {
|
||||||
|
pub callback_pc: u32,
|
||||||
|
pub user_data: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot of the fields we mutate when diverting a HW thread into an
|
||||||
|
/// interrupt callback. Restored when the callback returns to
|
||||||
|
/// `LR_HALT_SENTINEL`.
|
||||||
|
///
|
||||||
|
/// We save **all PPC volatile registers** (r0, r2–r12) plus `r1` (SP),
|
||||||
|
/// `pc`, `lr`, `ctr`, and `cr`. Non-volatile regs (r13–r31) are preserved
|
||||||
|
/// by the callback's own `__savegprlr_N` prologue/epilogue per the PPC
|
||||||
|
/// ELF ABI, so they don't need stashing here.
|
||||||
|
///
|
||||||
|
/// **SP (`gpr[1]`) is included because the injector decrements it by
|
||||||
|
/// [`CALLBACK_STACK_PAD`] before the callback runs** — see that constant's
|
||||||
|
/// docs for why. Without this, the callback's `__savegprlr_N` prologue
|
||||||
|
/// overwrites the interrupted function's own stack-saved LR (which lives
|
||||||
|
/// at `[r1 - 8]`), and when the interrupted function later tries to
|
||||||
|
/// return, `bclr` jumps to `LR_HALT_SENTINEL` and the thread exits
|
||||||
|
/// prematurely.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct SavedCallbackCtx {
|
||||||
|
pub pc: u32,
|
||||||
|
pub lr: u64,
|
||||||
|
pub ctr: u64,
|
||||||
|
/// All PPC volatile GPRs (r0, r2–r12) plus r1 (SP) in index order.
|
||||||
|
/// Index 0 = r0, 1 = r1, 2 = r2, …, 12 = r12. Index 13..32 unused.
|
||||||
|
pub gprs: [u64; 13],
|
||||||
|
pub cr: [CrField; 8],
|
||||||
|
pub source: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bytes the injector reserves below the interrupted thread's SP before
|
||||||
|
/// running the ISR callback. Matches Canary's
|
||||||
|
/// [`Processor::Execute`](../../../../xenia-canary/src/xenia/cpu/processor.cc#L383)
|
||||||
|
/// which decrements `r[1]` by `64 + 112 = 176` before
|
||||||
|
/// `function->Call(...)` and restores afterwards. The pad must be larger
|
||||||
|
/// than any plausible sum of `__savegprlr_N`'s save-area (up to 64 B for
|
||||||
|
/// r25-r31 + 8 B for LR) plus the callback's own `stwu r1,-N(r1)` frame
|
||||||
|
/// (the Sylpheed vsync ISR uses 128 B).
|
||||||
|
///
|
||||||
|
/// Pre-fix: the ISR's `__savegprlr_25` stored the callback's saved LR
|
||||||
|
/// (= `LR_HALT_SENTINEL`, from injection) at `[r1 - 8]` — exactly where
|
||||||
|
/// the interrupted thread's current `bl`-saved LR lived. The
|
||||||
|
/// interrupted function's return site got stomped with `SENTINEL`, so
|
||||||
|
/// `__restgprlr_N -> bclr` jumped to the halt sentinel and the thread
|
||||||
|
/// exited through the wrong path. Manifested in Sylpheed as tid=5
|
||||||
|
/// (producer for the render queue) terminating at cycle 7.5M, starving
|
||||||
|
/// both `0x10fc` (main's completion wait) and the PKEVENT that tid=6
|
||||||
|
/// polls — no second `VdSwap`, no first pixel.
|
||||||
|
pub const CALLBACK_STACK_PAD: u32 = 64 + 112;
|
||||||
|
|
||||||
|
impl SavedCallbackCtx {
|
||||||
|
pub fn capture(ctx: &PpcContext, source: u32) -> Self {
|
||||||
|
let mut gprs = [0u64; 13];
|
||||||
|
for i in 0..13 {
|
||||||
|
gprs[i] = ctx.gpr[i];
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
pc: ctx.pc,
|
||||||
|
lr: ctx.lr,
|
||||||
|
ctr: ctx.ctr,
|
||||||
|
gprs,
|
||||||
|
cr: ctx.cr,
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore(self, ctx: &mut PpcContext) {
|
||||||
|
ctx.pc = self.pc;
|
||||||
|
ctx.lr = self.lr;
|
||||||
|
ctx.ctr = self.ctr;
|
||||||
|
for i in 0..13 {
|
||||||
|
ctx.gpr[i] = self.gprs[i];
|
||||||
|
}
|
||||||
|
ctx.cr = self.cr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maximum pending sources held in the FIFO queue before new ones are
|
||||||
|
/// dropped. Four is enough to absorb a short burst (a few v-syncs arriving
|
||||||
|
/// while HW 0 is mid-callback from a prior one) without letting runaway
|
||||||
|
/// delivery swamp the guest.
|
||||||
|
pub const INTERRUPT_QUEUE_CAP: usize = 4;
|
||||||
|
|
||||||
|
/// All interrupt bookkeeping — single field on `KernelState`.
|
||||||
|
///
|
||||||
|
/// **First-Pixels M2 (2026-04-20)** — changed from a single-slot
|
||||||
|
/// `pending_source: Option<u32>` coalesce to a bounded FIFO so bursts
|
||||||
|
/// don't drop silently, and dropped `VSYNC_INSTR_PERIOD` from 500k to
|
||||||
|
/// 150k so cadence approximates 60 Hz at the current ~10 MIPS interpreter
|
||||||
|
/// throughput. Combined with the `HwState::ServicingIrq` variant added to
|
||||||
|
/// `xenia-cpu::scheduler`, interrupts can now be delivered even when HW 0
|
||||||
|
/// is `Blocked(WaitAny)` — the injector stashes the block into the new
|
||||||
|
/// variant and the restore path re-blocks when the callback returns,
|
||||||
|
/// unless a `wake()` during the callback resolved the wait.
|
||||||
|
/// M2.5 — per-slot pending-IRQ bitmask. Each `AtomicU8` holds one bit per
|
||||||
|
/// interrupt source (currently 2 sources: VSYNC=bit 0, CP=bit 1) destined
|
||||||
|
/// for that specific HW slot. Used by the M3 parallel path: T_main (or
|
||||||
|
/// the GPU thread) sets a bit Release on the target slot's atomic; the
|
||||||
|
/// target T_cpu_i checks the bit Acquire at its quantum boundary and
|
||||||
|
/// self-injects without taking another thread's slot lock.
|
||||||
|
///
|
||||||
|
/// The 6-element fixed-size array mirrors `xenia_cpu::scheduler::HW_THREAD_COUNT`.
|
||||||
|
pub type PendingLocalIrq = [std::sync::atomic::AtomicU8;
|
||||||
|
xenia_cpu::scheduler::HW_THREAD_COUNT];
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct InterruptState {
|
||||||
|
/// Registered callback (set by `VdSetGraphicsInterruptCallback`).
|
||||||
|
pub callback: Option<GraphicsInterruptCallback>,
|
||||||
|
/// Bounded FIFO of pending interrupt sources awaiting injection.
|
||||||
|
/// Push-back on queue, pop-front on inject. Over-cap pushes drop.
|
||||||
|
pub pending: VecDeque<u32>,
|
||||||
|
/// When `Some`, some HW thread is currently running a callback; on
|
||||||
|
/// return-to-sentinel we restore this and clear the flag.
|
||||||
|
pub saved: Option<SavedCallbackCtx>,
|
||||||
|
/// Which guest thread the current callback was injected into.
|
||||||
|
/// Required because we no longer anchor delivery to HW 0 — any
|
||||||
|
/// non-Exited thread is a valid target. Meaningful only while
|
||||||
|
/// `saved.is_some()`. Stored as a `ThreadRef` so per-slot
|
||||||
|
/// runqueues don't get ambiguous addressing.
|
||||||
|
pub injected_ref: Option<ThreadRef>,
|
||||||
|
/// Monotonic count of delivered interrupts.
|
||||||
|
pub delivered: u64,
|
||||||
|
/// Dropped interrupts (callback unset, queue full, or thread
|
||||||
|
/// exited/idle at inject time).
|
||||||
|
pub dropped: u64,
|
||||||
|
/// Instruction-count accumulator for the synthetic v-sync ticker. At
|
||||||
|
/// `VSYNC_INSTR_PERIOD` the main loop pushes an `INTERRUPT_SOURCE_VSYNC`
|
||||||
|
/// onto `pending` and resets.
|
||||||
|
pub vsync_accumulator: u64,
|
||||||
|
/// Last observed instruction count — `tick_vsync` diffs against
|
||||||
|
/// this to advance `vsync_accumulator`.
|
||||||
|
pub last_instr_count: u64,
|
||||||
|
/// M2.5 — per-slot pending-IRQ bits. Set by the producer (M3's
|
||||||
|
/// IRQ-routing logic on `T_main`) with `Release`; consumed by the
|
||||||
|
/// target T_cpu_i with `Acquire` at quantum boundary. Unused under
|
||||||
|
/// the lockstep path (M2's single-host-thread model still uses
|
||||||
|
/// `pending` + `try_inject_graphics_interrupt`); the field is wired
|
||||||
|
/// here so M3's per-HW-thread path is a flag flip, not a refactor.
|
||||||
|
pub pending_local_irq: PendingLocalIrq,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many guest instructions correspond to one synthetic v-sync.
|
||||||
|
///
|
||||||
|
/// Targets **~60 Hz at the post-Tier-3 interpreter throughput (~10 MIPS)**:
|
||||||
|
/// 10e6 instr/s ÷ 60 Hz ≈ 167k — we use 150k to give a small cushion.
|
||||||
|
/// Before M2 this was 500k (~20 Hz), which was enough for games that
|
||||||
|
/// don't gate anything on v-sync but not enough for titles like Sylpheed
|
||||||
|
/// whose main loop waits on the v-sync callback to signal an event every
|
||||||
|
/// frame.
|
||||||
|
pub const VSYNC_INSTR_PERIOD: u64 = 150_000;
|
||||||
|
|
||||||
|
impl InterruptState {
|
||||||
|
/// Record a new callback registration.
|
||||||
|
pub fn set_callback(&mut self, callback_pc: u32, user_data: u32) {
|
||||||
|
self.callback = Some(GraphicsInterruptCallback {
|
||||||
|
callback_pc,
|
||||||
|
user_data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queue an interrupt for the next safe injection point.
|
||||||
|
pub fn queue_interrupt(&mut self, source: u32) {
|
||||||
|
if self.callback.is_none() {
|
||||||
|
self.dropped += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.pending.len() >= INTERRUPT_QUEUE_CAP {
|
||||||
|
self.dropped += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.pending.push_back(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Peek at the next pending source without removing it.
|
||||||
|
pub fn peek_next(&self) -> Option<u32> {
|
||||||
|
self.pending.front().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop the next pending source (called by the injector after it has
|
||||||
|
/// committed to dispatching it).
|
||||||
|
pub fn take_next(&mut self) -> Option<u32> {
|
||||||
|
self.pending.pop_front()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance the v-sync accumulator by the delta since the last call.
|
||||||
|
/// Returns `true` if a new v-sync was queued.
|
||||||
|
pub fn tick_vsync(&mut self, current_instr_count: u64) -> bool {
|
||||||
|
let delta = current_instr_count.saturating_sub(self.last_instr_count);
|
||||||
|
self.last_instr_count = current_instr_count;
|
||||||
|
self.vsync_accumulator = self.vsync_accumulator.saturating_add(delta);
|
||||||
|
if self.vsync_accumulator < VSYNC_INSTR_PERIOD {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Multiple periods may have elapsed in a single tick call if a
|
||||||
|
// large instruction delta went by (e.g. a long export). Drain
|
||||||
|
// the accumulator fully so we don't lag behind.
|
||||||
|
let periods = self.vsync_accumulator / VSYNC_INSTR_PERIOD;
|
||||||
|
self.vsync_accumulator %= VSYNC_INSTR_PERIOD;
|
||||||
|
for _ in 0..periods {
|
||||||
|
self.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is HW thread 0 currently in a callback?
|
||||||
|
pub fn is_in_callback(&self) -> bool {
|
||||||
|
self.saved.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn queue_interrupt_drops_without_callback() {
|
||||||
|
let mut s = InterruptState::default();
|
||||||
|
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||||
|
assert_eq!(s.dropped, 1);
|
||||||
|
assert!(s.pending.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn queue_interrupt_fifo_preserves_order() {
|
||||||
|
let mut s = InterruptState::default();
|
||||||
|
s.set_callback(0x1000, 0xAB);
|
||||||
|
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||||
|
s.queue_interrupt(INTERRUPT_SOURCE_CP);
|
||||||
|
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||||
|
assert_eq!(s.dropped, 0);
|
||||||
|
// FIFO: take_next hands them out in push order.
|
||||||
|
assert_eq!(s.take_next(), Some(INTERRUPT_SOURCE_VSYNC));
|
||||||
|
assert_eq!(s.take_next(), Some(INTERRUPT_SOURCE_CP));
|
||||||
|
assert_eq!(s.take_next(), Some(INTERRUPT_SOURCE_VSYNC));
|
||||||
|
assert_eq!(s.take_next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn queue_interrupt_caps_at_queue_size() {
|
||||||
|
let mut s = InterruptState::default();
|
||||||
|
s.set_callback(0x1000, 0xAB);
|
||||||
|
for _ in 0..INTERRUPT_QUEUE_CAP {
|
||||||
|
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||||
|
}
|
||||||
|
// Over-cap: drops rather than evicting the oldest.
|
||||||
|
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||||
|
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||||
|
assert_eq!(s.dropped, 2);
|
||||||
|
assert_eq!(s.pending.len(), INTERRUPT_QUEUE_CAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tick_vsync_fires_at_new_150k_threshold() {
|
||||||
|
let mut s = InterruptState::default();
|
||||||
|
s.set_callback(0x1000, 0xAB);
|
||||||
|
assert_eq!(VSYNC_INSTR_PERIOD, 150_000);
|
||||||
|
assert!(!s.tick_vsync(VSYNC_INSTR_PERIOD - 1));
|
||||||
|
assert!(s.pending.is_empty());
|
||||||
|
assert!(s.tick_vsync(VSYNC_INSTR_PERIOD));
|
||||||
|
assert_eq!(s.peek_next(), Some(INTERRUPT_SOURCE_VSYNC));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tick_vsync_drains_multiple_periods_in_one_call() {
|
||||||
|
// Long kernel export → big instr delta → multiple v-syncs must
|
||||||
|
// be delivered, not lost.
|
||||||
|
let mut s = InterruptState::default();
|
||||||
|
s.set_callback(0x1000, 0xAB);
|
||||||
|
assert!(s.tick_vsync(VSYNC_INSTR_PERIOD * 3 + 10));
|
||||||
|
assert_eq!(s.pending.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates what the main loop does: inject, execute guest code up
|
||||||
|
/// to the sentinel, restore. Uses a single-instruction `bclr` callback
|
||||||
|
/// — the interpreter sees `pc == callback_pc`, steps, and the blr
|
||||||
|
/// instruction writes `lr` into `pc`, which equals `LR_HALT_SENTINEL`
|
||||||
|
/// → main loop detects and triggers restore.
|
||||||
|
#[test]
|
||||||
|
fn inject_restore_roundtrip_smoke() {
|
||||||
|
let mut ctx = PpcContext::new();
|
||||||
|
ctx.pc = 0x1000_0000;
|
||||||
|
ctx.lr = 0xCAFE_BABE;
|
||||||
|
ctx.gpr[3] = 0x1234;
|
||||||
|
ctx.gpr[4] = 0x5678;
|
||||||
|
|
||||||
|
let mut s = InterruptState::default();
|
||||||
|
s.set_callback(0x2000_0000, 0xDEAD);
|
||||||
|
|
||||||
|
// Simulate main loop inject: save ctx fields, divert pc/lr/r3/r4.
|
||||||
|
let saved = SavedCallbackCtx::capture(&ctx, INTERRUPT_SOURCE_VSYNC);
|
||||||
|
s.saved = Some(saved);
|
||||||
|
ctx.pc = 0x2000_0000;
|
||||||
|
ctx.lr = xenia_cpu::context::LR_HALT_SENTINEL;
|
||||||
|
ctx.gpr[3] = INTERRUPT_SOURCE_VSYNC as u64;
|
||||||
|
ctx.gpr[4] = 0xDEAD;
|
||||||
|
assert!(s.is_in_callback());
|
||||||
|
|
||||||
|
// Guest callback "runs" to the sentinel — simulate by writing
|
||||||
|
// pc = lr (what `blr` would do).
|
||||||
|
ctx.pc = ctx.lr as u32;
|
||||||
|
|
||||||
|
// Main loop detects pc == LR_HALT_SENTINEL while in_callback:
|
||||||
|
let saved = s.saved.take().unwrap();
|
||||||
|
saved.restore(&mut ctx);
|
||||||
|
s.delivered += 1;
|
||||||
|
|
||||||
|
assert_eq!(ctx.pc, 0x1000_0000);
|
||||||
|
assert_eq!(ctx.lr, 0xCAFE_BABE);
|
||||||
|
assert_eq!(ctx.gpr[3], 0x1234);
|
||||||
|
assert_eq!(ctx.gpr[4], 0x5678);
|
||||||
|
assert!(!s.is_in_callback());
|
||||||
|
assert_eq!(s.delivered, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn saved_ctx_roundtrip() {
|
||||||
|
let mut ctx = PpcContext::new();
|
||||||
|
ctx.pc = 0x11223344;
|
||||||
|
ctx.lr = 0xDEADBEEF;
|
||||||
|
ctx.gpr[3] = 0xAAAA;
|
||||||
|
ctx.gpr[4] = 0xBBBB;
|
||||||
|
let saved = SavedCallbackCtx::capture(&ctx, INTERRUPT_SOURCE_VSYNC);
|
||||||
|
ctx.pc = 0;
|
||||||
|
ctx.lr = 0;
|
||||||
|
ctx.gpr[3] = 0;
|
||||||
|
ctx.gpr[4] = 0;
|
||||||
|
saved.restore(&mut ctx);
|
||||||
|
assert_eq!(ctx.pc, 0x11223344);
|
||||||
|
assert_eq!(ctx.lr, 0xDEADBEEF);
|
||||||
|
assert_eq!(ctx.gpr[3], 0xAAAA);
|
||||||
|
assert_eq!(ctx.gpr[4], 0xBBBB);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full volatile-GPR + SP roundtrip. Regression test for the
|
||||||
|
/// 2026-04-24 IRQ-injection fix: the ISR callback's prologue clobbers
|
||||||
|
/// `[r1 - 8]` on the interrupted thread's stack unless the injector
|
||||||
|
/// pre-decrements SP by [`CALLBACK_STACK_PAD`] and the saved ctx puts
|
||||||
|
/// SP (and the rest of the PPC volatile set) back on return.
|
||||||
|
#[test]
|
||||||
|
fn saved_ctx_covers_sp_and_all_volatile_gprs() {
|
||||||
|
let mut ctx = PpcContext::new();
|
||||||
|
ctx.pc = 0xAAAA_BBBB;
|
||||||
|
ctx.lr = 0x1111_2222;
|
||||||
|
ctx.ctr = 0x3333_4444;
|
||||||
|
for i in 0..13 {
|
||||||
|
ctx.gpr[i] = 0x1000 + i as u64;
|
||||||
|
}
|
||||||
|
// r13..r31 are non-volatile and should survive the callback's own
|
||||||
|
// save/restore — the saved ctx deliberately does NOT cover them.
|
||||||
|
for i in 13..32 {
|
||||||
|
ctx.gpr[i] = 0xDEAD_0000 + i as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
let saved = SavedCallbackCtx::capture(&ctx, INTERRUPT_SOURCE_VSYNC);
|
||||||
|
|
||||||
|
// Simulate injector: flip pc/lr/r1/r3/r4 (what the real injector
|
||||||
|
// actually does — see try_inject_graphics_interrupt in main.rs).
|
||||||
|
ctx.pc = 0xCAFE;
|
||||||
|
ctx.lr = xenia_cpu::context::LR_HALT_SENTINEL;
|
||||||
|
ctx.gpr[1] = ctx.gpr[1].wrapping_sub(CALLBACK_STACK_PAD as u64);
|
||||||
|
ctx.gpr[3] = INTERRUPT_SOURCE_VSYNC as u64;
|
||||||
|
ctx.gpr[4] = 0xBEEF;
|
||||||
|
// Simulate callback clobbering a few volatile regs that aren't
|
||||||
|
// part of the "obviously diverted" set.
|
||||||
|
ctx.gpr[0] = 0xFEED_FACE;
|
||||||
|
ctx.gpr[7] = 0x9999;
|
||||||
|
ctx.gpr[12] = 0xABCD;
|
||||||
|
|
||||||
|
saved.restore(&mut ctx);
|
||||||
|
|
||||||
|
// All volatile GPRs restored to pre-injection.
|
||||||
|
for i in 0..13 {
|
||||||
|
assert_eq!(
|
||||||
|
ctx.gpr[i],
|
||||||
|
0x1000 + i as u64,
|
||||||
|
"volatile r{} clobbered by callback was not restored",
|
||||||
|
i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// SP specifically back to the pre-pad value.
|
||||||
|
assert_eq!(ctx.gpr[1], 0x1001, "SP must be restored to pre-injection");
|
||||||
|
// Non-volatile regs were never captured; they stay as the callback
|
||||||
|
// left them (here, untouched because we didn't modify 13..32).
|
||||||
|
for i in 13..32 {
|
||||||
|
assert_eq!(ctx.gpr[i], 0xDEAD_0000 + i as u64);
|
||||||
|
}
|
||||||
|
assert_eq!(ctx.pc, 0xAAAA_BBBB);
|
||||||
|
assert_eq!(ctx.lr, 0x1111_2222);
|
||||||
|
assert_eq!(ctx.ctr, 0x3333_4444);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,17 @@
|
|||||||
|
pub mod audit;
|
||||||
pub mod exports;
|
pub mod exports;
|
||||||
|
pub mod interrupts;
|
||||||
pub mod objects;
|
pub mod objects;
|
||||||
|
pub mod path;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
pub mod thread;
|
||||||
|
pub mod ui_bridge;
|
||||||
pub mod xam;
|
pub mod xam;
|
||||||
|
|
||||||
|
pub use interrupts::{
|
||||||
|
GraphicsInterruptCallback, InterruptState, SavedCallbackCtx, INTERRUPT_SOURCE_CP,
|
||||||
|
INTERRUPT_SOURCE_VSYNC, VSYNC_INSTR_PERIOD,
|
||||||
|
};
|
||||||
pub use state::{KernelState, ModuleId};
|
pub use state::{KernelState, ModuleId};
|
||||||
|
pub use thread::{allocate_thread_image, ThreadImage};
|
||||||
|
pub use ui_bridge::{SwapInfo, UiBridge};
|
||||||
|
|||||||
@@ -1,12 +1,94 @@
|
|||||||
//! Kernel object tracking for HLE.
|
//! Kernel object tracking for HLE.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use xenia_cpu::ThreadRef;
|
||||||
|
|
||||||
/// Kernel object types tracked by handle.
|
/// Kernel object types tracked by handle.
|
||||||
|
///
|
||||||
|
/// Sync variants (`Event`, `Semaphore`, `Mutex`, `Thread`) carry an in-place
|
||||||
|
/// waiter list so wait/set/release sites keep invariants local — dropping the
|
||||||
|
/// object implicitly drops its waiters. Waiters are stored as `ThreadRef`
|
||||||
|
/// (post-Axis-1) — a bare `hw_id: u8` would have been ambiguous under per-slot
|
||||||
|
/// runqueues where multiple guest threads share one HW slot.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum KernelObject {
|
pub enum KernelObject {
|
||||||
Event { manual_reset: bool, signaled: bool },
|
Event {
|
||||||
Semaphore { count: i32, max: i32 },
|
manual_reset: bool,
|
||||||
File { path: String },
|
signaled: bool,
|
||||||
Thread { id: u32 },
|
/// Guest threads parked on this event.
|
||||||
Timer,
|
waiters: Vec<ThreadRef>,
|
||||||
Mutex,
|
},
|
||||||
|
Semaphore {
|
||||||
|
count: i32,
|
||||||
|
max: i32,
|
||||||
|
waiters: Vec<ThreadRef>,
|
||||||
|
},
|
||||||
|
File {
|
||||||
|
/// Normalized VFS path (e.g. "default.xex", "media/shared/foo.pkg").
|
||||||
|
path: String,
|
||||||
|
/// Full file size in bytes.
|
||||||
|
size: u64,
|
||||||
|
/// Current read/write cursor.
|
||||||
|
position: u64,
|
||||||
|
/// Whole-file buffer — VFS reads the entire file up front so
|
||||||
|
/// subsequent NtReadFile calls are O(1) slice copies.
|
||||||
|
/// `Arc<Vec<u8>>` so duplicate handles could share backing storage.
|
||||||
|
data: Arc<Vec<u8>>,
|
||||||
|
/// Directory-enumeration cursor consumed by `NtQueryDirectoryFile`.
|
||||||
|
/// `None` before the first call; `Some(N)` = next VFS entry index
|
||||||
|
/// to emit. Reset to `Some(0)` when the guest passes
|
||||||
|
/// `restart_scan=1`. Unused on non-directory files.
|
||||||
|
dir_enum_pos: Option<usize>,
|
||||||
|
},
|
||||||
|
Thread {
|
||||||
|
id: u32,
|
||||||
|
/// HW thread slot currently running this guest thread (None once exited
|
||||||
|
/// — `exit_code` becomes Some).
|
||||||
|
hw_id: Option<u8>,
|
||||||
|
/// None while the thread is running; populated on ExTerminateThread
|
||||||
|
/// or halt-sentinel return.
|
||||||
|
exit_code: Option<u32>,
|
||||||
|
/// Guest threads parked in KeWaitForSingleObject on this thread handle.
|
||||||
|
waiters: Vec<ThreadRef>,
|
||||||
|
},
|
||||||
|
Timer {
|
||||||
|
/// Xbox 360 timer_type 0 = NotificationTimer (manual-reset),
|
||||||
|
/// 1 = SynchronizationTimer (auto-reset). Same shape as Event.
|
||||||
|
manual_reset: bool,
|
||||||
|
signaled: bool,
|
||||||
|
/// Absolute tick-space deadline; None when disarmed.
|
||||||
|
deadline: Option<u64>,
|
||||||
|
/// Period in ticks (same units as `deadline`); 0 = one-shot.
|
||||||
|
period_ticks: u64,
|
||||||
|
/// Original ms value (canary's SetTimer keeps it for diagnostics).
|
||||||
|
period_ms: u32,
|
||||||
|
/// APC routine (deferred — see `timer_apc` warn in nt_set_timer_ex).
|
||||||
|
callback_routine: u32,
|
||||||
|
callback_arg: u32,
|
||||||
|
waiters: Vec<ThreadRef>,
|
||||||
|
},
|
||||||
|
Mutex {
|
||||||
|
/// HW thread id currently holding the mutex; None when free.
|
||||||
|
owner: Option<u8>,
|
||||||
|
recursion: u32,
|
||||||
|
waiters: Vec<ThreadRef>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KernelObject {
|
||||||
|
/// Returns the per-object waiter list for the 5 sync variants (Event,
|
||||||
|
/// Semaphore, Thread, Timer, Mutex) and `None` for `File`. Used by
|
||||||
|
/// deadline-expiry scrub in `KernelState::handle_timeout_wake` so a
|
||||||
|
/// timed-out waiter isn't left stranded in a handle's waiters list.
|
||||||
|
pub fn waiters_mut(&mut self) -> Option<&mut Vec<ThreadRef>> {
|
||||||
|
match self {
|
||||||
|
KernelObject::Event { waiters, .. }
|
||||||
|
| KernelObject::Semaphore { waiters, .. }
|
||||||
|
| KernelObject::Thread { waiters, .. }
|
||||||
|
| KernelObject::Timer { waiters, .. }
|
||||||
|
| KernelObject::Mutex { waiters, .. } => Some(waiters),
|
||||||
|
KernelObject::File { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
crates/xenia-kernel/src/path.rs
Normal file
139
crates/xenia-kernel/src/path.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
//! Path normalization for kernel file I/O.
|
||||||
|
//!
|
||||||
|
//! Guests pass file paths inside an `OBJECT_ATTRIBUTES` struct that points at
|
||||||
|
//! an `ANSI_STRING` descriptor. Those paths come in several Xbox-flavored
|
||||||
|
//! forms — NT device paths (`\Device\Cdrom0\...`), drive letters (`D:\...`,
|
||||||
|
//! `d:\...`), or symbolic link prefixes (`game:\...`). We strip whichever
|
||||||
|
//! prefix applies and return a plain slash-separated path relative to the
|
||||||
|
//! mounted VFS root, so `VfsDevice::read_file` can look it up directly.
|
||||||
|
|
||||||
|
use xenia_memory::{GuestMemory, MemoryAccess};
|
||||||
|
|
||||||
|
/// Xbox `ANSI_STRING`:
|
||||||
|
/// u16 Length
|
||||||
|
/// u16 MaximumLength
|
||||||
|
/// u32 Buffer (guest pointer)
|
||||||
|
fn read_ansi_string(mem: &GuestMemory, ptr: u32) -> Option<String> {
|
||||||
|
if ptr == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let length = mem.read_u16(ptr) as u32;
|
||||||
|
let buffer = mem.read_u32(ptr + 4);
|
||||||
|
if buffer == 0 || length == 0 {
|
||||||
|
return Some(String::new());
|
||||||
|
}
|
||||||
|
let mut out = String::with_capacity(length as usize);
|
||||||
|
for i in 0..length {
|
||||||
|
let c = mem.read_u8(buffer + i);
|
||||||
|
if c == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.push(c as char);
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Xbox `OBJECT_ATTRIBUTES`:
|
||||||
|
/// u32 RootDirectory (handle)
|
||||||
|
/// u32 Name (pointer to ANSI_STRING)
|
||||||
|
/// u32 Attributes
|
||||||
|
fn read_object_attributes_name(mem: &GuestMemory, obj_attrs_ptr: u32) -> Option<String> {
|
||||||
|
if obj_attrs_ptr == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name_ptr = mem.read_u32(obj_attrs_ptr + 4);
|
||||||
|
read_ansi_string(mem, name_ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Known Xbox device prefixes that need to be stripped before looking a path
|
||||||
|
/// up in the VFS. The list mirrors the symbolic links xenia-canary sets up
|
||||||
|
/// at boot (see `xboxkrnl_io.cc`). Case-insensitive matching.
|
||||||
|
const DEVICE_PREFIXES: &[&str] = &[
|
||||||
|
"\\Device\\Cdrom0\\",
|
||||||
|
"\\Device\\Harddisk0\\Partition1\\",
|
||||||
|
"\\Device\\Harddisk0\\Partition0\\",
|
||||||
|
"\\Device\\Harddisk0\\",
|
||||||
|
"\\Device\\Mu0\\",
|
||||||
|
"\\Device\\Mu1\\",
|
||||||
|
"\\Device\\Mass0\\",
|
||||||
|
"\\Device\\Mass1\\",
|
||||||
|
"\\Device\\Mass2\\",
|
||||||
|
"\\SystemRoot\\",
|
||||||
|
"\\??\\",
|
||||||
|
"game:\\",
|
||||||
|
"d:\\",
|
||||||
|
"D:\\",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Strip any Xbox device prefix and normalize backslashes to forward slashes.
|
||||||
|
/// Returns the path relative to the VFS root.
|
||||||
|
pub fn normalize_path(raw: &str) -> String {
|
||||||
|
let mut s = raw.trim().to_string();
|
||||||
|
|
||||||
|
// Case-insensitive prefix strip.
|
||||||
|
let lowered = s.to_ascii_lowercase();
|
||||||
|
for prefix in DEVICE_PREFIXES {
|
||||||
|
let pl = prefix.to_ascii_lowercase();
|
||||||
|
if lowered.starts_with(&pl) {
|
||||||
|
s = s[pl.len()..].to_string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop any leading slash/backslash that survived prefix stripping.
|
||||||
|
while s.starts_with('\\') || s.starts_with('/') {
|
||||||
|
s.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonical form: forward slashes.
|
||||||
|
s.replace('\\', "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: read the OBJECT_ATTRIBUTES struct at `obj_attrs_ptr` and
|
||||||
|
/// return a normalized VFS path. Returns `None` if the struct pointer or its
|
||||||
|
/// inner name pointer is null.
|
||||||
|
pub fn object_attributes_to_vfs_path(mem: &GuestMemory, obj_attrs_ptr: u32) -> Option<String> {
|
||||||
|
let raw = read_object_attributes_name(mem, obj_attrs_ptr)?;
|
||||||
|
if raw.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(normalize_path(&raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strips_device_cdrom() {
|
||||||
|
assert_eq!(normalize_path("\\Device\\Cdrom0\\default.xex"), "default.xex");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strips_drive_letter_lowercase() {
|
||||||
|
assert_eq!(normalize_path("d:\\media\\shared\\foo.pkg"), "media/shared/foo.pkg");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strips_drive_letter_uppercase() {
|
||||||
|
assert_eq!(normalize_path("D:\\default.xex"), "default.xex");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strips_game_prefix() {
|
||||||
|
assert_eq!(normalize_path("game:\\data\\whatever.bin"), "data/whatever.bin");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserves_relative_path() {
|
||||||
|
assert_eq!(normalize_path("scripts/init.lua"), "scripts/init.lua");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_partition1() {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_path("\\Device\\Harddisk0\\Partition1\\content\\abc.sav"),
|
||||||
|
"content/abc.sav"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,35 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use xenia_cpu::PpcContext;
|
use xenia_cpu::scheduler::{PcrWriter, Scheduler};
|
||||||
use xenia_memory::GuestMemory;
|
use xenia_cpu::{PpcContext, ThreadRef};
|
||||||
|
use xenia_memory::{GuestMemory, MemoryAccess};
|
||||||
|
use xenia_vfs::VfsDevice;
|
||||||
|
|
||||||
|
use crate::audit::{HandleAudit, HandleAuditEntry};
|
||||||
use crate::objects::KernelObject;
|
use crate::objects::KernelObject;
|
||||||
|
use crate::ui_bridge::UiBridge;
|
||||||
|
|
||||||
|
/// Adapter: write PCR+0x2C on guest memory. Lets `Scheduler::spawn` and
|
||||||
|
/// Axis 4's migration call through without `xenia-cpu` depending on the
|
||||||
|
/// memory crate.
|
||||||
|
pub struct GuestMemoryPcr<'a>(pub &'a GuestMemory);
|
||||||
|
impl PcrWriter for GuestMemoryPcr<'_> {
|
||||||
|
fn write_pcr_id(&mut self, pcr_base: u32, hw_id: u8) {
|
||||||
|
// `GuestMemory::write_u32` takes `&self` post-M2 trait flip; the
|
||||||
|
// wrapping `&'a GuestMemory` is sufficient.
|
||||||
|
self.0.write_u32(pcr_base + 0x2C, hw_id as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Function signature for HLE kernel exports.
|
/// Function signature for HLE kernel exports.
|
||||||
pub type KernelExportFn = fn(&mut PpcContext, &mut GuestMemory, &mut KernelState);
|
///
|
||||||
|
/// The first argument is the **currently running** HW thread's `PpcContext`,
|
||||||
|
/// which the caller has temporarily moved out of the scheduler slot to avoid
|
||||||
|
/// aliasing. Exports that only touch register/GPR state use `ctx` directly;
|
||||||
|
/// exports that need scheduler state (spawn/park/wake/tls/etc.) reach
|
||||||
|
/// through `state.scheduler` — note that `state.scheduler.hw_threads[current]`
|
||||||
|
/// holds a placeholder `PpcContext` for the duration of the call, not the
|
||||||
|
/// live one passed as `ctx`.
|
||||||
|
pub type KernelExportFn = fn(&mut PpcContext, &GuestMemory, &mut KernelState);
|
||||||
|
|
||||||
/// Module identifier for kernel exports.
|
/// Module identifier for kernel exports.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
@@ -15,45 +39,174 @@ pub enum ModuleId {
|
|||||||
Xbdm,
|
Xbdm,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pseudo-`HMODULE` values returned by `XexGetModuleHandle` and accepted by
|
||||||
|
/// `XexGetProcedureAddress`. Distinct from real loaded-image bases
|
||||||
|
/// (>=0x82000000) and from kernel handles (0x1000+, allocated by
|
||||||
|
/// `alloc_handle`). The 0xFFFE_xxxx prefix is unused by both guest segments
|
||||||
|
/// and our handle allocator.
|
||||||
|
pub const HMODULE_XBOXKRNL: u32 = 0xFFFE_0001;
|
||||||
|
pub const HMODULE_XAM: u32 = 0xFFFE_0002;
|
||||||
|
|
||||||
/// Central kernel state tracking all guest OS state.
|
/// Central kernel state tracking all guest OS state.
|
||||||
pub struct KernelState {
|
pub struct KernelState {
|
||||||
exports: HashMap<(ModuleId, u32), (&'static str, KernelExportFn)>,
|
exports: HashMap<(ModuleId, u32), (&'static str, KernelExportFn)>,
|
||||||
next_handle: u32,
|
/// M2.4: bump allocator for kernel handles. `AtomicU32` so concurrent
|
||||||
pub tls_slots: HashMap<u32, u64>,
|
/// HLE calls under M3 can `fetch_add` without a lock. `Relaxed` is
|
||||||
next_tls_index: u32,
|
/// fine — the allocated value is a fresh ID with no prior payload to
|
||||||
|
/// publish; observers (the kernel object table) are guarded by
|
||||||
|
/// their own synchronization.
|
||||||
|
next_handle: std::sync::atomic::AtomicU32,
|
||||||
|
/// Scheduler managing all emulated HW threads + their per-slot
|
||||||
|
/// runqueues. Starts empty — the app installs the initial guest thread
|
||||||
|
/// on slot 0 via `KernelState::install_initial_thread` once it has the
|
||||||
|
/// entry address.
|
||||||
|
pub scheduler: Scheduler,
|
||||||
|
/// TLS slot allocator — index counter only. Per-thread *values* live on
|
||||||
|
/// `GuestThread::tls_values` (see scheduler). M2.4: `AtomicU32`.
|
||||||
|
pub next_tls_index: std::sync::atomic::AtomicU32,
|
||||||
|
/// Critical-section waiter map: guest `cs_ptr` → guest threads parked
|
||||||
|
/// on it. Critical sections are in guest memory (not kernel objects),
|
||||||
|
/// so their waiter list lives here rather than on an object.
|
||||||
|
pub cs_waiters: HashMap<u32, Vec<ThreadRef>>,
|
||||||
/// Kernel object table: handle → object
|
/// Kernel object table: handle → object
|
||||||
pub objects: HashMap<u32, KernelObject>,
|
pub objects: HashMap<u32, KernelObject>,
|
||||||
/// Bump allocator for guest heap (NtAllocateVirtualMemory etc.)
|
/// Bump allocator for guest heap (NtAllocateVirtualMemory etc.).
|
||||||
pub heap_cursor: u32,
|
/// M2.4: `AtomicU32` for lock-free concurrent allocation.
|
||||||
/// Stack allocator cursor for MmCreateKernelStack
|
pub heap_cursor: std::sync::atomic::AtomicU32,
|
||||||
pub stack_cursor: u32,
|
/// Stack allocator cursor for MmCreateKernelStack. M2.4: atomic.
|
||||||
|
pub stack_cursor: std::sync::atomic::AtomicU32,
|
||||||
/// GPU command buffer address (set by VdGetSystemCommandBuffer)
|
/// GPU command buffer address (set by VdGetSystemCommandBuffer)
|
||||||
pub gpu_command_buffer: u32,
|
pub gpu_command_buffer: u32,
|
||||||
|
/// GPU backend. M1.4: was `xenia_gpu::GpuSystem` directly, now a
|
||||||
|
/// [`xenia_gpu::GpuBackend`] enum so the kernel can hold either an
|
||||||
|
/// inline `GpuSystem` (synchronous, default) or a `GpuHandle` proxy
|
||||||
|
/// pointing at a worker thread (`--gpu-thread`). Forwarding methods
|
||||||
|
/// on the enum keep call sites in [`crate::exports`] terse.
|
||||||
|
pub gpu: xenia_gpu::GpuBackend,
|
||||||
|
/// Monotonic packet number returned by `XamInputGetState`. Games detect
|
||||||
|
/// input changes by watching this increment.
|
||||||
|
pub input_packet_number: u32,
|
||||||
|
/// Previous gamepad snapshot; `input_packet_number` only advances when
|
||||||
|
/// the state bytes actually change, matching host XInput semantics.
|
||||||
|
pub last_input_bytes: u128,
|
||||||
/// Image base of the loaded XEX (for XexExecutableModuleHandle etc.)
|
/// Image base of the loaded XEX (for XexExecutableModuleHandle etc.)
|
||||||
pub image_base: u32,
|
pub image_base: u32,
|
||||||
/// Next thread ID
|
/// Next thread ID. M2.4: atomic.
|
||||||
pub next_thread_id: u32,
|
pub next_thread_id: std::sync::atomic::AtomicU32,
|
||||||
|
/// Virtual file system for NtCreateFile/NtReadFile/etc. The app mounts
|
||||||
|
/// the disc image or host directory into this slot; file I/O handlers
|
||||||
|
/// route all reads through it.
|
||||||
|
pub vfs: Option<Box<dyn VfsDevice>>,
|
||||||
|
/// Bridge to the host UI. `None` when running headless. Installed by
|
||||||
|
/// `cmd_exec` when the user passes `--ui`.
|
||||||
|
pub ui: Option<UiBridge>,
|
||||||
|
/// P6 — graphics interrupt + synthetic v-sync bookkeeping. Registers
|
||||||
|
/// the callback set by `VdSetGraphicsInterruptCallback` and tracks
|
||||||
|
/// the paused-context snapshot while HW thread 0 is running it.
|
||||||
|
pub interrupts: crate::interrupts::InterruptState,
|
||||||
|
/// Per-handle refcount. Since `NtDuplicateObject` aliases (returns the
|
||||||
|
/// source handle value as the "new" handle rather than minting a fresh
|
||||||
|
/// id), a single handle commonly has multiple logical references. This
|
||||||
|
/// map tracks that count so a stray `NtClose` on one reference doesn't
|
||||||
|
/// destroy the object while another reference is still live. Canary's
|
||||||
|
/// `ObjectTable::ReleaseHandle` (object_table.cc:189) is the parity
|
||||||
|
/// reference. Initialized to 1 in `alloc_handle_for`; incremented in
|
||||||
|
/// `nt_duplicate_object` when `DUPLICATE_CLOSE_SOURCE` is absent;
|
||||||
|
/// decremented in `nt_close` which drops the underlying object only
|
||||||
|
/// when the count reaches zero.
|
||||||
|
pub handle_refcount: HashMap<u32, u32>,
|
||||||
|
/// Pending timer expirations — `(deadline, handle)` sorted ascending by
|
||||||
|
/// deadline. Pushed by `arm_timer`, popped by `fire_due_timers`. Kept in
|
||||||
|
/// lockstep with the per-`Timer` object's `deadline` field via the
|
||||||
|
/// `arm_timer`/`disarm_timer` helpers. See the plan's step 3/6 for the
|
||||||
|
/// design rationale — timer deadlines coexist with
|
||||||
|
/// `Scheduler::timed_waits` but track a different class (signaled object
|
||||||
|
/// fires, not thread wake-ups).
|
||||||
|
pub pending_timer_fires: Vec<(u64, u32)>,
|
||||||
|
/// Per-handle signal/wait/wake audit trail. Default `enabled=false` →
|
||||||
|
/// every record method is a no-op. Flip via `--trace-handles`/
|
||||||
|
/// `XENIA_TRACE_HANDLES` to diagnose missing-signal deadlocks (handles
|
||||||
|
/// 0x10FC / 0x1014 / 0x1104 / 0x10DC / 0x10F0 specifically). See
|
||||||
|
/// [`crate::audit`] for layout.
|
||||||
|
pub audit: HandleAudit,
|
||||||
|
/// M2.2 — banked reservation table for `lwarx`/`stwcx.` under M3's
|
||||||
|
/// per-HW-thread parallelism. Always allocated. Consulted by the
|
||||||
|
/// interpreter when `reservations.is_enabled()` is true; otherwise
|
||||||
|
/// the legacy per-`PpcContext` fields drive observable behavior.
|
||||||
|
/// Settable via `--reservations-table` / `XENIA_RESERVATIONS_TABLE=1`
|
||||||
|
/// for golden verification, or implicitly under `--parallel`.
|
||||||
|
/// See [`xenia_cpu::ReservationTable`] for the concurrency model.
|
||||||
|
pub reservations: std::sync::Arc<xenia_cpu::ReservationTable>,
|
||||||
|
/// Map from `(module, ordinal)` to the guest-side import-thunk address
|
||||||
|
/// resolved at load time. Reverse of `xenia-app/src/main.rs`'s
|
||||||
|
/// `thunk_map`. Populated from xenia-app's Phase 1 (record_type==1
|
||||||
|
/// only). Used by `xex_get_procedure_address` to resolve ordinals back
|
||||||
|
/// to callable thunks.
|
||||||
|
thunks_by_ordinal: HashMap<(ModuleId, u16), u32>,
|
||||||
|
/// First-Pixels diagnostic latch. Set the first time
|
||||||
|
/// `RtlRaiseException` fires with code `0xE06D7363` (MSVC C++ throw)
|
||||||
|
/// so the deep stack-walk + `runtime_error` decode in
|
||||||
|
/// `rtl_raise_exception` only emits once per run, regardless of how
|
||||||
|
/// many subsequent throws fire. Reset on each fresh process start.
|
||||||
|
pub cxx_throw_logged: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KernelState {
|
impl KernelState {
|
||||||
pub fn new() -> Self {
|
/// Construct a kernel with the supplied GPU backend.
|
||||||
|
///
|
||||||
|
/// The caller (typically `cmd_exec_inner`) decides whether to install
|
||||||
|
/// an inline backend (default) or a threaded one (`--gpu-thread`).
|
||||||
|
/// Most existing call sites build via [`Self::new`], which defaults to
|
||||||
|
/// an inline backend; the threaded constructor lives at
|
||||||
|
/// [`Self::with_gpu`].
|
||||||
|
pub fn with_gpu(gpu: xenia_gpu::GpuBackend) -> Self {
|
||||||
|
// Scheduler starts empty; the app installs the initial thread on
|
||||||
|
// slot 0 via `install_initial_thread` right after construction.
|
||||||
|
let mut scheduler = Scheduler::new();
|
||||||
|
use std::sync::atomic::AtomicU32;
|
||||||
|
let reservations = std::sync::Arc::new(xenia_cpu::ReservationTable::new());
|
||||||
|
// M3.7 — wire the reservation table to the scheduler so
|
||||||
|
// `spawn`/`install_initial_thread` populate every PpcContext's
|
||||||
|
// `reservation_table` clone. The table is `disabled` by
|
||||||
|
// default; `--reservations-table` / `XENIA_RESERVATIONS_TABLE`
|
||||||
|
// / M3 spawn flip it on.
|
||||||
|
scheduler.set_reservation_table(Some(reservations.clone()));
|
||||||
let mut state = Self {
|
let mut state = Self {
|
||||||
exports: HashMap::new(),
|
exports: HashMap::new(),
|
||||||
next_handle: 0x1000,
|
next_handle: AtomicU32::new(0x1000),
|
||||||
tls_slots: HashMap::new(),
|
scheduler,
|
||||||
next_tls_index: 0,
|
next_tls_index: AtomicU32::new(0),
|
||||||
|
cs_waiters: HashMap::new(),
|
||||||
objects: HashMap::new(),
|
objects: HashMap::new(),
|
||||||
heap_cursor: 0x4000_0000, // Start of user heap region
|
heap_cursor: AtomicU32::new(0x4000_0000), // Start of user heap region
|
||||||
stack_cursor: 0x7100_0000, // Above main stack
|
stack_cursor: AtomicU32::new(0x7100_0000), // Above main stack
|
||||||
gpu_command_buffer: 0,
|
gpu_command_buffer: 0,
|
||||||
|
gpu,
|
||||||
|
input_packet_number: 0,
|
||||||
|
last_input_bytes: 0,
|
||||||
image_base: 0,
|
image_base: 0,
|
||||||
next_thread_id: 1,
|
next_thread_id: AtomicU32::new(1),
|
||||||
|
vfs: None,
|
||||||
|
ui: None,
|
||||||
|
interrupts: crate::interrupts::InterruptState::default(),
|
||||||
|
handle_refcount: HashMap::new(),
|
||||||
|
pending_timer_fires: Vec::new(),
|
||||||
|
audit: HandleAudit::default(),
|
||||||
|
reservations,
|
||||||
|
thunks_by_ordinal: HashMap::new(),
|
||||||
|
cxx_throw_logged: false,
|
||||||
};
|
};
|
||||||
crate::exports::register_exports(&mut state);
|
crate::exports::register_exports(&mut state);
|
||||||
crate::xam::register_exports(&mut state);
|
crate::xam::register_exports(&mut state);
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default constructor — installs an inline `GpuSystem`. Kept for
|
||||||
|
/// callers that don't (yet) thread a `GpuBackend` choice through.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::with_gpu(xenia_gpu::GpuBackend::Inline(xenia_gpu::GpuSystem::new()))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn register_export(
|
pub fn register_export(
|
||||||
&mut self,
|
&mut self,
|
||||||
module: ModuleId,
|
module: ModuleId,
|
||||||
@@ -64,31 +217,159 @@ impl KernelState {
|
|||||||
self.exports.insert((module, ordinal), (name, func));
|
self.exports.insert((module, ordinal), (name, func));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Record an import-thunk address resolved at load time. Called once
|
||||||
|
/// per `record_type==1` import in xenia-app's Phase 1. Idempotent: a
|
||||||
|
/// duplicate ordinal overwrites (later wins; in practice the loader
|
||||||
|
/// emits each ordinal once per module).
|
||||||
|
pub fn register_thunk(&mut self, module: ModuleId, ordinal: u16, address: u32) {
|
||||||
|
self.thunks_by_ordinal.insert((module, ordinal), address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a `(module, ordinal)` to its registered thunk address.
|
||||||
|
pub fn resolve_thunk(&self, module: ModuleId, ordinal: u16) -> Option<u32> {
|
||||||
|
self.thunks_by_ordinal.get(&(module, ordinal)).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a pseudo-`HMODULE` (as returned by `XexGetModuleHandle`) back
|
||||||
|
/// to its `ModuleId`. Returns `None` for unknown handles, including
|
||||||
|
/// the loaded XEX's `image_base` (which is *not* a kernel module).
|
||||||
|
pub fn module_id_from_hmodule(&self, handle: u32) -> Option<ModuleId> {
|
||||||
|
match handle {
|
||||||
|
HMODULE_XBOXKRNL => Some(ModuleId::Xboxkrnl),
|
||||||
|
HMODULE_XAM => Some(ModuleId::Xam),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatch a kernel export on the current HW thread. Uses `mem::replace`
|
||||||
|
/// to temporarily move the active `PpcContext` out of its scheduler slot,
|
||||||
|
/// so the export function can receive `&mut ctx` while also getting
|
||||||
|
/// `&mut self` (which contains the scheduler). Without this, the export
|
||||||
|
/// signature would have to avoid aliasing via a bundle struct — see the
|
||||||
|
/// approved plan's ExportCtx section for the alternative we rejected.
|
||||||
|
///
|
||||||
|
/// While the export runs, `scheduler.hw_threads[current_hw_id].ctx` holds
|
||||||
|
/// a freshly-constructed placeholder. Exports that reach through
|
||||||
|
/// `state.scheduler` must not touch the current slot's `ctx` field.
|
||||||
|
///
|
||||||
|
/// **Perf note (First-Pixels M1):** this function fires ~250K/s on
|
||||||
|
/// Sylpheed (1 import per 40 guest instructions). A former
|
||||||
|
/// `#[tracing::instrument]` attribute + two `tracing::info!` call
|
||||||
|
/// sites made up ~28% of `run_execution` wall time on a post-Tier-3
|
||||||
|
/// profile — most of it in `tracing::span::Span::new` +
|
||||||
|
/// `Layered::new_span` + `ErrorLayer::on_new_span`. The span was at
|
||||||
|
/// `level = "debug"` but the span **construction** happened
|
||||||
|
/// unconditionally; only the emit was level-gated. Removing the
|
||||||
|
/// attribute + the two `info!` lines recovers the overhead without
|
||||||
|
/// losing any observability — the `metrics::counter!("kernel.calls",
|
||||||
|
/// "name" => name)` below still tracks per-export counts, and
|
||||||
|
/// unimplemented lookups still emit a `warn!`.
|
||||||
pub fn call_export(
|
pub fn call_export(
|
||||||
&mut self,
|
&mut self,
|
||||||
module: ModuleId,
|
module: ModuleId,
|
||||||
ordinal: u32,
|
ordinal: u32,
|
||||||
ctx: &mut PpcContext,
|
mem: &GuestMemory,
|
||||||
mem: &mut GuestMemory,
|
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some(&(name, func)) = self.exports.get(&(module, ordinal)) {
|
// The thread whose ctx we're swapping out must be addressed by
|
||||||
tracing::info!(
|
// `ThreadRef`, not `hw_id` — under per-slot runqueues a bare
|
||||||
"Kernel call: {:?}:{:#x} ({}) args=[{:#x}, {:#x}, {:#x}, {:#x}]",
|
// `hw_id` alone can't distinguish multiple threads on the same
|
||||||
module, ordinal, name,
|
// slot, and Axis 4 migration can change the slot underneath us.
|
||||||
ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6]
|
let r = self
|
||||||
|
.scheduler
|
||||||
|
.current
|
||||||
|
.expect("call_export: no current thread");
|
||||||
|
let mut ctx = std::mem::replace(
|
||||||
|
self.scheduler.ctx_mut_ref(r),
|
||||||
|
PpcContext::new(),
|
||||||
);
|
);
|
||||||
func(ctx, mem, self);
|
|
||||||
tracing::info!(" -> returned {:#x}", ctx.gpr[3]);
|
let result = if let Some(&(name, func)) = self.exports.get(&(module, ordinal)) {
|
||||||
|
metrics::counter!("kernel.calls", "name" => name).increment(1);
|
||||||
|
tracing::trace!(target: "probe_calls", "hw={} call={} r3={:#x} r4={:#x} r5={:#x} lr={:#x}",
|
||||||
|
r.hw_id, name, ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.lr);
|
||||||
|
func(&mut ctx, mem, self);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
metrics::counter!("kernel.unimplemented").increment(1);
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Unimplemented kernel export: {:?}:{:#x}",
|
module = ?module,
|
||||||
module, ordinal
|
ordinal = format_args!("{:#x}", ordinal),
|
||||||
|
"unimplemented kernel export"
|
||||||
);
|
);
|
||||||
// Return 0 (STATUS_SUCCESS) by default for unimplemented calls
|
|
||||||
ctx.gpr[3] = 0;
|
ctx.gpr[3] = 0;
|
||||||
false
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restore the (possibly mutated) ctx by ThreadRef. Axis 4
|
||||||
|
// self-migration (KeSetAffinityThread(NtCurrentThread, ...))
|
||||||
|
// updates `scheduler.current` in place; re-read here so we
|
||||||
|
// restore onto the thread's new slot, not its old one.
|
||||||
|
let final_ref = self.scheduler.current.unwrap_or(r);
|
||||||
|
*self.scheduler.ctx_mut_ref(final_ref) = ctx;
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Axis 4: `KeSetAffinityThread` orchestration. Drives the scheduler's
|
||||||
|
/// migration and fixes up every `ThreadRef` held outside the
|
||||||
|
/// scheduler (kernel object waiter lists, critical-section waiters,
|
||||||
|
/// `interrupts.injected_ref`). Returns the previous mask.
|
||||||
|
pub fn set_affinity(&mut self, handle: u32, new_mask: u8, mem: &GuestMemory) -> u8 {
|
||||||
|
let Some(r) = self.scheduler.find_by_handle(handle) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let (old_mask, _new_ref, fixup) = self.scheduler.set_affinity_ref(
|
||||||
|
r,
|
||||||
|
new_mask,
|
||||||
|
&mut GuestMemoryPcr(mem),
|
||||||
|
);
|
||||||
|
if let Some(fx) = fixup {
|
||||||
|
use crate::objects::KernelObject;
|
||||||
|
for obj in self.objects.values_mut() {
|
||||||
|
match obj {
|
||||||
|
KernelObject::Event { waiters, .. }
|
||||||
|
| KernelObject::Semaphore { waiters, .. }
|
||||||
|
| KernelObject::Thread { waiters, .. }
|
||||||
|
| KernelObject::Mutex { waiters, .. } => {
|
||||||
|
for w in waiters.iter_mut() {
|
||||||
|
fx.apply(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for list in self.cs_waiters.values_mut() {
|
||||||
|
for w in list.iter_mut() {
|
||||||
|
fx.apply(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ref mut ir) = self.interrupts.injected_ref {
|
||||||
|
fx.apply(ir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
old_mask
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install the initial (main) guest thread on HW slot 0. Called once at
|
||||||
|
/// startup after the app allocates the main stack/PCR/TLS blocks.
|
||||||
|
pub fn install_initial_thread(
|
||||||
|
&mut self,
|
||||||
|
ctx: PpcContext,
|
||||||
|
stack_base: u32,
|
||||||
|
stack_size: u32,
|
||||||
|
pcr_base: u32,
|
||||||
|
tls_base: u32,
|
||||||
|
thread_handle: u32,
|
||||||
|
mem: &GuestMemory,
|
||||||
|
) {
|
||||||
|
self.scheduler.install_initial_thread(
|
||||||
|
ctx,
|
||||||
|
stack_base,
|
||||||
|
stack_size,
|
||||||
|
pcr_base,
|
||||||
|
tls_base,
|
||||||
|
thread_handle,
|
||||||
|
&mut GuestMemoryPcr(mem),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn export_name(&self, module: ModuleId, ordinal: u32) -> Option<&'static str> {
|
pub fn export_name(&self, module: ModuleId, ordinal: u32) -> Option<&'static str> {
|
||||||
@@ -96,60 +377,261 @@ impl KernelState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn alloc_handle(&mut self) -> u32 {
|
pub fn alloc_handle(&mut self) -> u32 {
|
||||||
let h = self.next_handle;
|
// M2.4: lock-free fetch_add. Relaxed is sufficient — IDs are
|
||||||
self.next_handle += 4;
|
// opaque tokens; no payload is sequenced against the counter.
|
||||||
h
|
self.next_handle
|
||||||
|
.fetch_add(4, std::sync::atomic::Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn alloc_handle_for(&mut self, obj: KernelObject) -> u32 {
|
pub fn alloc_handle_for(&mut self, obj: KernelObject) -> u32 {
|
||||||
let h = self.alloc_handle();
|
let h = self.alloc_handle();
|
||||||
self.objects.insert(h, obj);
|
self.objects.insert(h, obj);
|
||||||
|
// Each fresh handle starts with one logical reference (the creator).
|
||||||
|
// `NtDuplicateObject` bumps this; `NtClose` decrements; the object is
|
||||||
|
// only dropped when the count reaches zero. See `nt_close` for the
|
||||||
|
// aliased-handle rationale.
|
||||||
|
self.handle_refcount.insert(h, 1);
|
||||||
h
|
h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Handle audit hooks =====
|
||||||
|
//
|
||||||
|
// These are no-ops when `audit.enabled == false`, so call sites can
|
||||||
|
// unconditionally invoke them without a hot-path branch in release builds
|
||||||
|
// (the `inline` `if !enabled return` short-circuits before any work).
|
||||||
|
|
||||||
|
/// Build a [`HandleAuditEntry`] describing the *current* call-site —
|
||||||
|
/// captures cycle (slot-0 timebase), current `tid`, and `lr` from the
|
||||||
|
/// passed `PpcContext`.
|
||||||
|
fn audit_entry(&self, lr: u32, source: &'static str, aux: u64) -> HandleAuditEntry {
|
||||||
|
let hw_id = self.scheduler.current_hw_id().unwrap_or(0);
|
||||||
|
let cycle = self.scheduler.ctx(hw_id).timebase;
|
||||||
|
let tid = self.scheduler.tid(hw_id).unwrap_or(0);
|
||||||
|
HandleAuditEntry { cycle, tid, lr, source, aux }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record the creation of a fresh handle. `kind` is one of the stable
|
||||||
|
/// labels documented on [`crate::audit::HandleAuditTrail::kind`].
|
||||||
|
pub fn audit_create(&mut self, handle: u32, kind: &'static str, lr: u32, source: &'static str) {
|
||||||
|
if !self.audit.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entry = self.audit_entry(lr, source, 0);
|
||||||
|
self.audit.record_create(handle, kind, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record a Set/Pulse/Release/etc. call against a handle. `aux` is the
|
||||||
|
/// previous signal state (or per-export-specific data).
|
||||||
|
pub fn audit_signal(&mut self, handle: u32, lr: u32, source: &'static str, aux: u64) {
|
||||||
|
if !self.audit.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entry = self.audit_entry(lr, source, aux);
|
||||||
|
self.audit.record_signal(handle, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record a `Wait*` call against a handle. `aux` packs `(alertable as u64)
|
||||||
|
/// | (timeout_kind << 8)` etc. — schema is informal; the dump just prints
|
||||||
|
/// it.
|
||||||
|
pub fn audit_wait(&mut self, handle: u32, lr: u32, source: &'static str, aux: u64) {
|
||||||
|
if !self.audit.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entry = self.audit_entry(lr, source, aux);
|
||||||
|
self.audit.record_wait(handle, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record a wake event (called from `wake_eligible_waiters`). `aux`
|
||||||
|
/// is the status code stamped into the woken thread's `gpr[3]`.
|
||||||
|
pub fn audit_wake(&mut self, handle: u32, lr: u32, source: &'static str, aux: u64) {
|
||||||
|
if !self.audit.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entry = self.audit_entry(lr, source, aux);
|
||||||
|
self.audit.record_wake(handle, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a TLS slot for the currently running HW thread.
|
||||||
pub fn tls_get(&self, index: u32) -> u64 {
|
pub fn tls_get(&self, index: u32) -> u64 {
|
||||||
self.tls_slots.get(&index).copied().unwrap_or(0)
|
self.scheduler.tls_get(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write a TLS slot for the currently running HW thread.
|
||||||
pub fn tls_set(&mut self, index: u32, value: u64) {
|
pub fn tls_set(&mut self, index: u32, value: u64) {
|
||||||
self.tls_slots.insert(index, value);
|
self.scheduler.tls_set(index, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allocate a new global TLS slot index. Grows every HW thread's
|
||||||
|
/// `tls_values` array to match.
|
||||||
pub fn tls_alloc(&mut self) -> u32 {
|
pub fn tls_alloc(&mut self) -> u32 {
|
||||||
let idx = self.next_tls_index;
|
use std::sync::atomic::Ordering;
|
||||||
self.next_tls_index += 1;
|
// M2.4: atomic bump. The Scheduler::tls_grow_to call still needs
|
||||||
|
// a coherent post-bump value, so we read the new size from the
|
||||||
|
// fetch_add return.
|
||||||
|
let idx = self.next_tls_index.fetch_add(1, Ordering::Relaxed);
|
||||||
|
let new_size = idx + 1;
|
||||||
|
self.scheduler.tls_grow_to(new_size as usize);
|
||||||
idx
|
idx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allocate guest memory from the heap bump allocator.
|
/// Allocate guest memory from the heap bump allocator.
|
||||||
/// Returns the base address of the allocated region.
|
/// Returns the base address of the allocated region.
|
||||||
pub fn heap_alloc(&mut self, size: u32, mem: &mut GuestMemory) -> Option<u32> {
|
pub fn heap_alloc(&mut self, size: u32, mem: &GuestMemory) -> Option<u32> {
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
let aligned_size = (size + 0xFFF) & !0xFFF; // Page-align
|
let aligned_size = (size + 0xFFF) & !0xFFF; // Page-align
|
||||||
let base = self.heap_cursor;
|
// M2.4: atomic bump, then verify post-bump invariants. If the
|
||||||
if base.checked_add(aligned_size).is_none() || base + aligned_size > 0x6FFF_FFFF {
|
// bump pushed us past the heap-region ceiling, the cursor stays
|
||||||
|
// advanced — subsequent allocations also fail, matching the
|
||||||
|
// pre-M2 sequential semantics. We don't try to "undo" the bump
|
||||||
|
// because that opens a CAS-loop race for marginal benefit (a
|
||||||
|
// failing alloc near the limit is already game-over).
|
||||||
|
let base = self.heap_cursor.fetch_add(aligned_size, Ordering::Relaxed);
|
||||||
|
let new_top = base.checked_add(aligned_size)?;
|
||||||
|
if new_top > 0x6FFF_FFFF {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let protect = xenia_memory::page_table::MemoryProtect::READ
|
let protect = xenia_memory::page_table::MemoryProtect::READ
|
||||||
| xenia_memory::page_table::MemoryProtect::WRITE;
|
| xenia_memory::page_table::MemoryProtect::WRITE;
|
||||||
if mem.alloc(base, aligned_size, protect).is_err() {
|
mem.alloc(base, aligned_size, protect).ok()?;
|
||||||
return None;
|
|
||||||
}
|
|
||||||
self.heap_cursor += aligned_size;
|
|
||||||
Some(base)
|
Some(base)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allocate a kernel stack.
|
/// Allocate a kernel stack.
|
||||||
pub fn stack_alloc(&mut self, size: u32, mem: &mut GuestMemory) -> Option<u32> {
|
pub fn stack_alloc(&mut self, size: u32, mem: &GuestMemory) -> Option<u32> {
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
let aligned_size = (size + 0xFFF) & !0xFFF;
|
let aligned_size = (size + 0xFFF) & !0xFFF;
|
||||||
let base = self.stack_cursor;
|
let base = self.stack_cursor.fetch_add(aligned_size, Ordering::Relaxed);
|
||||||
let protect = xenia_memory::page_table::MemoryProtect::READ
|
let protect = xenia_memory::page_table::MemoryProtect::READ
|
||||||
| xenia_memory::page_table::MemoryProtect::WRITE;
|
| xenia_memory::page_table::MemoryProtect::WRITE;
|
||||||
if mem.alloc(base, aligned_size, protect).is_err() {
|
mem.alloc(base, aligned_size, protect).ok()?;
|
||||||
return None;
|
|
||||||
}
|
|
||||||
self.stack_cursor += aligned_size;
|
|
||||||
Some(base + aligned_size) // Return top of stack
|
Some(base + aligned_size) // Return top of stack
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Timer subsystem =====
|
||||||
|
|
||||||
|
/// Idempotent arm — removes any prior entry for `handle`, then inserts
|
||||||
|
/// the new `(deadline, handle)` pair and re-sorts by deadline ascending.
|
||||||
|
/// The per-`Timer` object's `deadline` field must be set separately by
|
||||||
|
/// the caller (see `NtSetTimerEx` in exports.rs) — this helper only
|
||||||
|
/// manages the central pending-fires list so `fire_due_timers` has a
|
||||||
|
/// sorted head to peek.
|
||||||
|
pub fn arm_timer(&mut self, handle: u32, deadline: u64) {
|
||||||
|
self.pending_timer_fires.retain(|&(_, h)| h != handle);
|
||||||
|
self.pending_timer_fires.push((deadline, handle));
|
||||||
|
self.pending_timer_fires.sort_by_key(|&(d, _)| d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Idempotent disarm — strip any entry for `handle`. Safe to call
|
||||||
|
/// regardless of prior state; `NtClose`, `NtCancelTimer`, and the
|
||||||
|
/// periodic-rearm guard all invoke this.
|
||||||
|
pub fn disarm_timer(&mut self, handle: u32) {
|
||||||
|
self.pending_timer_fires.retain(|&(_, h)| h != handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Peek the earliest pending timer deadline. Paired with
|
||||||
|
/// `Scheduler::earliest_wait_deadline` by the main loop's "advance to
|
||||||
|
/// next event" coordination — the earlier of the two drives
|
||||||
|
/// `advance_all_timebases_to`.
|
||||||
|
pub fn earliest_timer_deadline(&self) -> Option<u64> {
|
||||||
|
self.pending_timer_fires.first().map(|&(d, _)| d)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fire every timer whose deadline is `<= now` (derived from slot 0's
|
||||||
|
/// timebase, matching `parse_timeout`'s "current thread" fallback).
|
||||||
|
/// For each fire: mark the timer `signaled=true`, clear its
|
||||||
|
/// `deadline`, rearm if periodic, then wake eligible waiters via
|
||||||
|
/// `exports::wake_eligible_waiters`. Returns `true` iff any timer
|
||||||
|
/// fired — the caller uses this to decide whether the scheduler round
|
||||||
|
/// needs a follow-up `advance_to_next_wake_if_due` step.
|
||||||
|
pub fn fire_due_timers(&mut self) -> bool {
|
||||||
|
let now = self.scheduler.ctx(0).timebase;
|
||||||
|
let mut fired = false;
|
||||||
|
loop {
|
||||||
|
let Some(&(deadline, handle)) = self.pending_timer_fires.first() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if deadline > now {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.pending_timer_fires.remove(0);
|
||||||
|
// Mark signaled + capture period before any rearm so we don't
|
||||||
|
// double-borrow the object while calling wake_eligible_waiters.
|
||||||
|
let periodic_next =
|
||||||
|
if let Some(KernelObject::Timer {
|
||||||
|
signaled,
|
||||||
|
deadline: obj_deadline,
|
||||||
|
period_ticks,
|
||||||
|
..
|
||||||
|
}) = self.objects.get_mut(&handle)
|
||||||
|
{
|
||||||
|
*signaled = true;
|
||||||
|
*obj_deadline = None;
|
||||||
|
if *period_ticks > 0 {
|
||||||
|
Some(now + *period_ticks)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Closed handle — its entry lingered because disarm on
|
||||||
|
// NtClose was missed, OR fire_due_timers picked up a
|
||||||
|
// race. Skip silently; nothing to wake.
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(next) = periodic_next {
|
||||||
|
if let Some(KernelObject::Timer { deadline, .. }) =
|
||||||
|
self.objects.get_mut(&handle)
|
||||||
|
{
|
||||||
|
*deadline = Some(next);
|
||||||
|
}
|
||||||
|
self.arm_timer(handle, next);
|
||||||
|
}
|
||||||
|
crate::exports::wake_eligible_waiters(self, handle);
|
||||||
|
fired = true;
|
||||||
|
}
|
||||||
|
fired
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle deadline-expiry cleanup for a thread whose wait timed out.
|
||||||
|
/// Called by the main loop right after `Scheduler::advance_to_next_wake`
|
||||||
|
/// returns a `Some((ref, reason))`. Stamps `STATUS_TIMEOUT` into the
|
||||||
|
/// woken thread's `gpr[3]` and scrubs its `ThreadRef` out of any
|
||||||
|
/// handle's waiter list so a later signal can't consume the
|
||||||
|
/// auto-reset slot into a stale waiter.
|
||||||
|
///
|
||||||
|
/// `BlockReason::DelayUntil` is a pure sleep and expects
|
||||||
|
/// `STATUS_SUCCESS` — the default pre-populated value in
|
||||||
|
/// `ke_delay_execution_thread` — so we leave `gpr[3]` alone for it.
|
||||||
|
pub fn handle_timeout_wake(
|
||||||
|
&mut self,
|
||||||
|
r: ThreadRef,
|
||||||
|
reason: xenia_cpu::scheduler::BlockReason,
|
||||||
|
) {
|
||||||
|
use xenia_cpu::scheduler::BlockReason;
|
||||||
|
const STATUS_TIMEOUT: u64 = 0x0000_0102;
|
||||||
|
match reason {
|
||||||
|
BlockReason::WaitAny { handles, .. } | BlockReason::WaitAll { handles, .. } => {
|
||||||
|
self.scheduler.ctx_mut_ref(r).gpr[3] = STATUS_TIMEOUT;
|
||||||
|
for h in handles {
|
||||||
|
if let Some(obj) = self.objects.get_mut(&h) {
|
||||||
|
if let Some(waiters) = obj.waiters_mut() {
|
||||||
|
waiters.retain(|&w| w != r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BlockReason::DelayUntil(_) => {
|
||||||
|
// Pure sleep → default STATUS_SUCCESS is correct; no handles
|
||||||
|
// to scrub.
|
||||||
|
}
|
||||||
|
BlockReason::CriticalSection(cs_ptr) => {
|
||||||
|
self.scheduler.ctx_mut_ref(r).gpr[3] = STATUS_TIMEOUT;
|
||||||
|
if let Some(list) = self.cs_waiters.get_mut(&cs_ptr) {
|
||||||
|
list.retain(|&w| w != r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BlockReason::Suspended => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for KernelState {
|
impl Default for KernelState {
|
||||||
@@ -157,3 +639,89 @@ impl Default for KernelState {
|
|||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use xenia_memory::GuestMemory;
|
||||||
|
|
||||||
|
/// Ten consecutive `heap_alloc(0x14)` calls must return distinct
|
||||||
|
/// page-aligned addresses. A previous bug had kernel exports passing 0 as
|
||||||
|
/// `size`, causing the bump allocator to return the same address every
|
||||||
|
/// time — 10 "allocations" that all aliased 0x40105000 and silently
|
||||||
|
/// corrupted the guest's static-constructor state.
|
||||||
|
#[test]
|
||||||
|
fn heap_alloc_advances_for_nonzero_size() {
|
||||||
|
let mut mem = GuestMemory::new().expect("memory init");
|
||||||
|
let mut state = KernelState::new();
|
||||||
|
let mut seen = Vec::new();
|
||||||
|
for _ in 0..10 {
|
||||||
|
let addr = state
|
||||||
|
.heap_alloc(0x14, &mut mem)
|
||||||
|
.expect("heap must have room for 0x14 bytes");
|
||||||
|
assert_eq!(addr & 0xFFF, 0, "heap returns page-aligned addresses");
|
||||||
|
assert!(!seen.contains(&addr), "heap returned duplicate address {addr:#x}");
|
||||||
|
seen.push(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `heap_alloc(0)` must not advance the cursor (it has nothing to do).
|
||||||
|
/// The kernel exports that previously hit this path did so because they
|
||||||
|
/// read the wrong argument register; guarded at the export boundary now.
|
||||||
|
#[test]
|
||||||
|
fn heap_alloc_zero_is_noop_in_cursor() {
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
let mem = GuestMemory::new().expect("memory init");
|
||||||
|
let mut state = KernelState::new();
|
||||||
|
let before = state.heap_cursor.load(Ordering::Relaxed);
|
||||||
|
let _ = state.heap_alloc(0, &mem);
|
||||||
|
let after = state.heap_cursor.load(Ordering::Relaxed);
|
||||||
|
assert_eq!(before, after, "zero-size alloc must not advance heap cursor");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// M2.4: concurrent handle allocations must produce distinct values.
|
||||||
|
/// Ten threads each allocate 100 handles via `alloc_handle`; the union
|
||||||
|
/// must contain exactly 1000 distinct values, and the maximum equals
|
||||||
|
/// `0x1000 + 4 * (1000 - 1)` (ascending step is 4 per the kernel
|
||||||
|
/// allocator's policy).
|
||||||
|
#[test]
|
||||||
|
fn concurrent_alloc_handle_distinct() {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
|
||||||
|
// Use a free-standing AtomicU32 mirroring `next_handle`'s semantics;
|
||||||
|
// we can't easily share `&mut KernelState` across threads. The
|
||||||
|
// production code uses the same `fetch_add(4, Relaxed)` recipe.
|
||||||
|
let counter = std::sync::Arc::new(AtomicU32::new(0x1000));
|
||||||
|
let collected: std::sync::Arc<Mutex<HashSet<u32>>> =
|
||||||
|
std::sync::Arc::new(Mutex::new(HashSet::new()));
|
||||||
|
|
||||||
|
let mut handles = Vec::new();
|
||||||
|
for _ in 0..10 {
|
||||||
|
let c = counter.clone();
|
||||||
|
let s = collected.clone();
|
||||||
|
handles.push(std::thread::spawn(move || {
|
||||||
|
let mut local = Vec::with_capacity(100);
|
||||||
|
for _ in 0..100 {
|
||||||
|
local.push(c.fetch_add(4, Ordering::Relaxed));
|
||||||
|
}
|
||||||
|
let mut g = s.lock().unwrap();
|
||||||
|
for v in local {
|
||||||
|
g.insert(v);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for h in handles {
|
||||||
|
h.join().unwrap();
|
||||||
|
}
|
||||||
|
let set = collected.lock().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
set.len(),
|
||||||
|
1000,
|
||||||
|
"expected 1000 distinct handles, got {}",
|
||||||
|
set.len()
|
||||||
|
);
|
||||||
|
assert!(set.iter().all(|h| (h - 0x1000) % 4 == 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
68
crates/xenia-kernel/src/thread.rs
Normal file
68
crates/xenia-kernel/src/thread.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
//! Guest-thread image allocation — shared by the initial thread setup in
|
||||||
|
//! `xenia-app/src/main.rs` and `ExCreateThread`. Stack, PCR, and TLS blocks
|
||||||
|
//! all come from the existing kernel bump allocators so layout is consistent.
|
||||||
|
|
||||||
|
use xenia_memory::{GuestMemory, MemoryAccess};
|
||||||
|
|
||||||
|
use crate::state::KernelState;
|
||||||
|
|
||||||
|
/// Addresses the caller passes to `Scheduler::spawn` / the initial-thread
|
||||||
|
/// setup. Matches xenia-canary's per-thread allocations: a stack, a PCR, and
|
||||||
|
/// a TLS block.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct ThreadImage {
|
||||||
|
pub stack_base: u32,
|
||||||
|
pub stack_size: u32,
|
||||||
|
pub pcr_base: u32,
|
||||||
|
pub tls_base: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate stack + PCR + TLS for one guest thread and initialize the PCR
|
||||||
|
/// fields that games read in their thread prolog.
|
||||||
|
///
|
||||||
|
/// - Stack comes from `KernelState::stack_alloc` (bump allocator at
|
||||||
|
/// 0x7100_0000 upward). The returned base is the *bottom*; callers
|
||||||
|
/// compute SP as `base + size`.
|
||||||
|
/// - PCR and TLS are fixed 4 KiB pages allocated via `heap_alloc` so they
|
||||||
|
/// land in the user heap region together with other kernel metadata.
|
||||||
|
/// - `hw_thread_id` is written at PCR+0x2C so `KeGetCurrentProcessorNumber`-
|
||||||
|
/// style reads from r13 resolve correctly even though we never register
|
||||||
|
/// that export.
|
||||||
|
pub fn allocate_thread_image(
|
||||||
|
kernel: &mut KernelState,
|
||||||
|
mem: &GuestMemory,
|
||||||
|
stack_size: u32,
|
||||||
|
hw_thread_id: u8,
|
||||||
|
) -> Option<ThreadImage> {
|
||||||
|
// Round stack size to a page and give games a minimum that matches
|
||||||
|
// xenia-canary's 16 MiB default when callers request 0 (common for
|
||||||
|
// ExCreateThread when the caller lets the kernel pick).
|
||||||
|
let stack_size = if stack_size == 0 {
|
||||||
|
0x10_0000
|
||||||
|
} else {
|
||||||
|
(stack_size + 0xFFF) & !0xFFF
|
||||||
|
};
|
||||||
|
// stack_alloc returns top-of-stack; we need the base.
|
||||||
|
let stack_top = kernel.stack_alloc(stack_size, mem)?;
|
||||||
|
let stack_base = stack_top - stack_size;
|
||||||
|
|
||||||
|
let pcr_base = kernel.heap_alloc(0x1000, mem)?;
|
||||||
|
let tls_base = kernel.heap_alloc(0x1000, mem)?;
|
||||||
|
|
||||||
|
// PCR layout (canary xboxkrnl/xboxkrnl_module.cc, simplified):
|
||||||
|
// +0x000 tls_ptr → TLS block base
|
||||||
|
// +0x02C current_processor_id → HW thread id (0..5)
|
||||||
|
// +0x100 current_thread → placeholder non-zero tag
|
||||||
|
// +0x150 dpc_active → 0 (no DPC queued)
|
||||||
|
mem.write_u32(pcr_base, tls_base);
|
||||||
|
mem.write_u32(pcr_base + 0x2C, hw_thread_id as u32);
|
||||||
|
mem.write_u32(pcr_base + 0x100, 0x1000);
|
||||||
|
mem.write_u32(pcr_base + 0x150, 0);
|
||||||
|
|
||||||
|
Some(ThreadImage {
|
||||||
|
stack_base,
|
||||||
|
stack_size,
|
||||||
|
pcr_base,
|
||||||
|
tls_base,
|
||||||
|
})
|
||||||
|
}
|
||||||
185
crates/xenia-kernel/src/ui_bridge.rs
Normal file
185
crates/xenia-kernel/src/ui_bridge.rs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
//! Bridge between the kernel (CPU-thread side) and a host UI (main-thread side).
|
||||||
|
//!
|
||||||
|
//! The kernel side needs to:
|
||||||
|
//! - snapshot the latest host gamepad each time a guest calls
|
||||||
|
//! `XamInputGetState`, and
|
||||||
|
//! - signal the UI when the guest calls `VdSwap` so the UI can upload the
|
||||||
|
//! guest's frontbuffer to a wgpu texture and present it.
|
||||||
|
//!
|
||||||
|
//! Both directions are expressed as trait-object closures so that `xenia-kernel`
|
||||||
|
//! does not have to depend on winit/wgpu/gilrs. The [`UiBridge`] is installed
|
||||||
|
//! on [`KernelState::ui`] by `cmd_exec` when `--ui` is passed.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicU64};
|
||||||
|
|
||||||
|
use xenia_gpu::texture_cache::TextureKey;
|
||||||
|
use xenia_gpu::xenos_constants::XenosConstantsBlock;
|
||||||
|
use xenia_hid::GamepadState;
|
||||||
|
use xenia_memory::MemoryAccess;
|
||||||
|
|
||||||
|
/// Information surfaced to the UI each time the guest presents a frame.
|
||||||
|
///
|
||||||
|
/// Fields mirror the seven "interesting" arguments to `VdSwap` in
|
||||||
|
/// `xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_video.cc`: the raw
|
||||||
|
/// frontbuffer pointer, its dimensions, and the format/color-space enum values
|
||||||
|
/// the guest passed through.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct SwapInfo {
|
||||||
|
/// Guest physical/virtual address of the frontbuffer to present.
|
||||||
|
pub frontbuffer_addr: u32,
|
||||||
|
/// Width in pixels as reported by the guest.
|
||||||
|
pub width: u32,
|
||||||
|
/// Height in pixels as reported by the guest.
|
||||||
|
pub height: u32,
|
||||||
|
/// Xenos texture format enum (the guest passes a pointer; we dereference
|
||||||
|
/// it here). 0 means "unknown / guest passed a null pointer".
|
||||||
|
pub texture_format: u32,
|
||||||
|
/// Color-space enum (sRGB / BT.709 / …).
|
||||||
|
pub color_space: u32,
|
||||||
|
/// Monotonically increasing frame counter maintained by the kernel; useful
|
||||||
|
/// for HUD display and deduping.
|
||||||
|
pub frame_index: u64,
|
||||||
|
/// Total PM4 `DRAW_INDX*` packets the GPU has captured since boot.
|
||||||
|
/// Surfaced so the UI HUD can show progress even before the full
|
||||||
|
/// uber-shader pipeline is wired in.
|
||||||
|
pub draws_total: u64,
|
||||||
|
/// Total PM4 packets executed, across all opcodes — useful signal for
|
||||||
|
/// "is the GPU actually getting anything at all to consume?".
|
||||||
|
pub packets_total: u64,
|
||||||
|
/// Most-recent draw's Xenos primitive-type code (0 = none yet).
|
||||||
|
pub last_draw_prim: u32,
|
||||||
|
/// Most-recent draw's vertex count.
|
||||||
|
pub last_draw_vertex_count: u32,
|
||||||
|
/// Indirect-buffer jumps so far (useful "is the game driving the ring
|
||||||
|
/// buffer through IBs?" signal).
|
||||||
|
pub indirect_buffer_jumps: u64,
|
||||||
|
/// WAIT_REG_MEM stalls observed on the GPU slot.
|
||||||
|
pub wait_reg_mem_blocks: u64,
|
||||||
|
/// Summed CPU instruction count across all 6 HW threads. Mirrors the
|
||||||
|
/// `cycle_count` field each `PpcContext` maintains; gives the HUD a live
|
||||||
|
/// "how far has the guest run?" readout.
|
||||||
|
pub instructions_total: u64,
|
||||||
|
/// Active VS shader blob key at the most recent DRAW_INDX* (0 = none).
|
||||||
|
/// P3b: the UI uses this to index into `handles.shader_blobs` so the
|
||||||
|
/// Xenos uber-shader interpreter can upload the matching microcode.
|
||||||
|
pub vs_blob_key: u32,
|
||||||
|
/// Active PS shader blob key at the most recent DRAW_INDX*.
|
||||||
|
pub ps_blob_key: u32,
|
||||||
|
/// P4: total EDRAM→memory resolves fired since boot (TILE_FLUSH
|
||||||
|
/// events). Non-zero means the game is committing pixels.
|
||||||
|
pub resolves_total: u64,
|
||||||
|
/// Subset of `resolves_total` whose byte-copy path succeeded and wrote
|
||||||
|
/// at least one sample into guest memory.
|
||||||
|
pub resolves_copied_total: u64,
|
||||||
|
/// Subset of `resolves_total` that were skipped by the byte-copy path
|
||||||
|
/// due to an unsupported format / MSAA mode / 3D destination.
|
||||||
|
pub resolves_skipped_total: u64,
|
||||||
|
/// P4: unique RT keys seen (from the GPU's internal render-target
|
||||||
|
/// cache). Grows as the game exercises new RT footprints.
|
||||||
|
pub unique_render_targets: u64,
|
||||||
|
/// P6: total graphics-interrupt callbacks delivered (v-sync + CP).
|
||||||
|
/// Non-zero means `VdSetGraphicsInterruptCallback` has been wired end
|
||||||
|
/// to end and callbacks are actually running.
|
||||||
|
pub interrupts_delivered: u64,
|
||||||
|
/// P6: graphics-interrupts queued but dropped (callback unset,
|
||||||
|
/// thread 0 blocked, or already inside another callback).
|
||||||
|
pub interrupts_dropped: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles the kernel uses to talk to a running host UI.
|
||||||
|
///
|
||||||
|
/// None of the closures are allowed to block for long — they are called from
|
||||||
|
/// the CPU interpreter thread on the hot path.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UiBridge {
|
||||||
|
/// Snapshot the host gamepad. Called from `XamInputGetState`.
|
||||||
|
pub gamepad: Arc<dyn Fn() -> GamepadState + Send + Sync>,
|
||||||
|
/// Report that the guest completed a frame. The closure gets the swap
|
||||||
|
/// metadata plus a borrow of guest memory so it can copy the frontbuffer
|
||||||
|
/// bytes into a UI-owned staging buffer before returning. Called from
|
||||||
|
/// `VdSwap` on the CPU thread.
|
||||||
|
pub post_swap: Arc<dyn Fn(SwapInfo, &dyn MemoryAccess) + Send + Sync>,
|
||||||
|
/// Indicates the UI wants the CPU loop to stop. Checked periodically by
|
||||||
|
/// the interpreter loop.
|
||||||
|
pub shutdown: Arc<AtomicBool>,
|
||||||
|
/// Set to `true` when a gamepad is present. `XamInputGetState` returns
|
||||||
|
/// `ERROR_DEVICE_NOT_CONNECTED` when this is `false`.
|
||||||
|
pub gamepad_connected: Arc<AtomicBool>,
|
||||||
|
/// Live CPU instruction counter mirror. The app's run loop publishes
|
||||||
|
/// the sum of `ctx.cycle_count` across HW threads here every ~8k
|
||||||
|
/// instructions so the HUD can report progress between VdSwap events.
|
||||||
|
pub instructions_counter: Arc<AtomicU64>,
|
||||||
|
/// P3b asset publish: `vd_swap` snapshots the GPU's `shader_blobs` and
|
||||||
|
/// constants register region and feeds them to the UI so the Xenos
|
||||||
|
/// uber-shader interpreter has the microcode + constants needed to
|
||||||
|
/// execute the guest draw. Split from `post_swap` so the asset wire
|
||||||
|
/// stays optional — if the UI doesn't need them (headless mode) the
|
||||||
|
/// closure is a no-op.
|
||||||
|
pub publish_xenos_assets:
|
||||||
|
Arc<dyn Fn(HashMap<u32, Vec<u32>>, XenosConstantsBlock) + Send + Sync>,
|
||||||
|
/// P4 frontbuffer publish: at each `VdSwap`, the kernel CPU-side
|
||||||
|
/// detiles the guest frontbuffer (k_8_8_8_8 Tiled2D) into a linear
|
||||||
|
/// RGBA8 buffer and hands it to the UI. The closure receives
|
||||||
|
/// `(width, height, bytes)` — the UI uploads it as a texture.
|
||||||
|
pub publish_frontbuffer:
|
||||||
|
Arc<dyn Fn(u32, u32, Vec<u8>) + Send + Sync>,
|
||||||
|
/// P5 primary texture publish: at each `VdSwap`, the kernel thread
|
||||||
|
/// decodes the PS shader's primary-texture fetch constant (slot 0
|
||||||
|
/// for now) and hands the decoded linear bytes + key to the UI so
|
||||||
|
/// the xenos pipeline can bind a real texture at `@group(1)`.
|
||||||
|
/// Receives `(TextureKey, bytes)`; when `None` is sent the UI
|
||||||
|
/// reverts to its magenta stub.
|
||||||
|
pub publish_texture:
|
||||||
|
Arc<dyn Fn(Option<(TextureKey, Vec<u8>)>) + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiBridge {
|
||||||
|
/// Snapshot input state (user 0 only; higher indices are unconnected).
|
||||||
|
pub fn snapshot_gamepad(&self) -> GamepadState {
|
||||||
|
(self.gamepad)()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True iff a gamepad is connected for user 0.
|
||||||
|
pub fn is_connected(&self, user_index: u32) -> bool {
|
||||||
|
user_index == 0
|
||||||
|
&& self
|
||||||
|
.gamepad_connected
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a swap event to the UI thread.
|
||||||
|
pub fn notify_swap(&self, info: SwapInfo, mem: &dyn MemoryAccess) {
|
||||||
|
(self.post_swap)(info, mem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot current shader blobs + constants and hand them to the UI.
|
||||||
|
/// Call from `vd_swap` so the UI has the matching assets for every
|
||||||
|
/// draw captured in this frame.
|
||||||
|
pub fn publish_assets(
|
||||||
|
&self,
|
||||||
|
blobs: HashMap<u32, Vec<u32>>,
|
||||||
|
constants: XenosConstantsBlock,
|
||||||
|
) {
|
||||||
|
(self.publish_xenos_assets)(blobs, constants);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True iff the UI asked for shutdown.
|
||||||
|
pub fn should_shutdown(&self) -> bool {
|
||||||
|
self.shutdown.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hand a detiled frontbuffer frame to the UI. Called at most once per
|
||||||
|
/// `VdSwap`. `bytes` must be `width * height * 4` bytes in
|
||||||
|
/// `Rgba8Unorm` order (the UI pipeline's expected layout).
|
||||||
|
pub fn publish_frontbuffer(&self, width: u32, height: u32, bytes: Vec<u8>) {
|
||||||
|
(self.publish_frontbuffer)(width, height, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hand one decoded guest texture to the UI. `Some` = update the bound
|
||||||
|
/// slot; `None` = revert to the magenta stub.
|
||||||
|
pub fn publish_texture(&self, tex: Option<(TextureKey, Vec<u8>)>) {
|
||||||
|
(self.publish_texture)(tex);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,10 +12,10 @@ pub fn register_exports(state: &mut KernelState) {
|
|||||||
state.register_export(Xam, 0x02, "NetDll_WSACleanup", stub_success);
|
state.register_export(Xam, 0x02, "NetDll_WSACleanup", stub_success);
|
||||||
|
|
||||||
// Input
|
// Input
|
||||||
state.register_export(Xam, 0x0190, "XamInputGetCapabilities", xam_input_not_connected);
|
state.register_export(Xam, 0x0190, "XamInputGetCapabilities", xam_input_get_capabilities);
|
||||||
state.register_export(Xam, 0x0191, "XamInputGetState", xam_input_not_connected);
|
state.register_export(Xam, 0x0191, "XamInputGetState", xam_input_get_state);
|
||||||
state.register_export(Xam, 0x0192, "XamInputSetState", xam_input_not_connected);
|
state.register_export(Xam, 0x0192, "XamInputSetState", xam_input_set_state);
|
||||||
state.register_export(Xam, 0x0198, "XamInputGetKeystrokeEx", xam_input_not_connected);
|
state.register_export(Xam, 0x0198, "XamInputGetKeystrokeEx", xam_input_get_keystroke);
|
||||||
|
|
||||||
// Inactivity
|
// Inactivity
|
||||||
state.register_export(Xam, 0x01A0, "XamEnableInactivityProcessing", stub_success);
|
state.register_export(Xam, 0x01A0, "XamEnableInactivityProcessing", stub_success);
|
||||||
@@ -94,39 +94,114 @@ pub fn register_exports(state: &mut KernelState) {
|
|||||||
|
|
||||||
// ===== Generic stubs =====
|
// ===== Generic stubs =====
|
||||||
|
|
||||||
fn stub_success(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn stub_success(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
ctx.gpr[3] = 0;
|
ctx.gpr[3] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stub_return_zero(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn stub_return_zero(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
ctx.gpr[3] = 0;
|
ctx.gpr[3] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stub_error_no_more_files(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn stub_error_no_more_files(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
ctx.gpr[3] = 0x12; // ERROR_NO_MORE_FILES
|
ctx.gpr[3] = 0x12; // ERROR_NO_MORE_FILES
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Input =====
|
// ===== Input =====
|
||||||
|
|
||||||
fn xam_input_not_connected(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
/// Helper: pack a `GamepadState` into a 12-byte key used to detect input
|
||||||
ctx.gpr[3] = 0x48F; // ERROR_DEVICE_NOT_CONNECTED
|
/// changes. Cheap to compare across frames.
|
||||||
|
fn gamepad_key(state: &xenia_hid::GamepadState) -> u128 {
|
||||||
|
let mut bytes = [0u8; 16];
|
||||||
|
bytes[0..2].copy_from_slice(&state.buttons.to_be_bytes());
|
||||||
|
bytes[2] = state.left_trigger;
|
||||||
|
bytes[3] = state.right_trigger;
|
||||||
|
bytes[4..6].copy_from_slice(&state.left_stick_x.to_be_bytes());
|
||||||
|
bytes[6..8].copy_from_slice(&state.left_stick_y.to_be_bytes());
|
||||||
|
bytes[8..10].copy_from_slice(&state.right_stick_x.to_be_bytes());
|
||||||
|
bytes[10..12].copy_from_slice(&state.right_stick_y.to_be_bytes());
|
||||||
|
u128::from_be_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xam_input_get_capabilities(
|
||||||
|
ctx: &mut PpcContext,
|
||||||
|
mem: &GuestMemory,
|
||||||
|
state: &mut KernelState,
|
||||||
|
) {
|
||||||
|
// 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 {
|
||||||
|
ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xenia_hid::write_input_capabilities(mem, out_ptr);
|
||||||
|
ctx.gpr[3] = xenia_hid::errors::SUCCESS as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xam_input_get_state(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||||
|
// 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) {
|
||||||
|
ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let gamepad = ui.snapshot_gamepad();
|
||||||
|
let key = gamepad_key(&gamepad);
|
||||||
|
if key != state.last_input_bytes {
|
||||||
|
state.input_packet_number = state.input_packet_number.wrapping_add(1);
|
||||||
|
state.last_input_bytes = key;
|
||||||
|
}
|
||||||
|
xenia_hid::write_input_state(mem, out_ptr, state.input_packet_number, &gamepad);
|
||||||
|
ctx.gpr[3] = xenia_hid::errors::SUCCESS as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xam_input_set_state(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||||||
|
// r3 = user_index, r4 = flags, r5 = X_INPUT_VIBRATION*
|
||||||
|
// Rumble is out of scope for Phase 1; we accept the call and return
|
||||||
|
// success so games don't retry in a tight loop, but we never actually
|
||||||
|
// shake anything.
|
||||||
|
let user = ctx.gpr[3] as u32;
|
||||||
|
let connected = state.ui.as_ref().is_some_and(|ui| ui.is_connected(user));
|
||||||
|
if !connected {
|
||||||
|
ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.gpr[3] = xenia_hid::errors::SUCCESS as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xam_input_get_keystroke(
|
||||||
|
ctx: &mut PpcContext,
|
||||||
|
_mem: &GuestMemory,
|
||||||
|
_state: &mut KernelState,
|
||||||
|
) {
|
||||||
|
// No keyboard input in Phase 1 — always "queue empty". Games that only
|
||||||
|
// use the gamepad ignore this return code; those that drive text entry
|
||||||
|
// through the keystroke queue simply get a permanently empty queue, which
|
||||||
|
// manifests as no virtual-keyboard input — acceptable for minimal UI.
|
||||||
|
ctx.gpr[3] = xenia_hid::errors::EMPTY as u64;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Loader =====
|
// ===== Loader =====
|
||||||
|
|
||||||
fn xam_loader_launch_title(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn xam_loader_launch_title(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
tracing::warn!("XamLoaderLaunchTitle called");
|
tracing::warn!("XamLoaderLaunchTitle called");
|
||||||
ctx.gpr[3] = 0;
|
ctx.gpr[3] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xam_loader_terminate_title(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn xam_loader_terminate_title(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
tracing::warn!("XamLoaderTerminateTitle called");
|
tracing::warn!("XamLoaderTerminateTitle called");
|
||||||
ctx.gpr[3] = 0;
|
ctx.gpr[3] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Task =====
|
// ===== Task =====
|
||||||
|
|
||||||
fn xam_task_schedule(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
fn xam_task_schedule(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||||||
let handle = state.alloc_handle();
|
let handle = state.alloc_handle();
|
||||||
tracing::info!("XamTaskSchedule: handle={:#x}", handle);
|
tracing::info!("XamTaskSchedule: handle={:#x}", handle);
|
||||||
ctx.gpr[3] = 0;
|
ctx.gpr[3] = 0;
|
||||||
@@ -134,7 +209,7 @@ fn xam_task_schedule(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut K
|
|||||||
|
|
||||||
// ===== Alloc =====
|
// ===== Alloc =====
|
||||||
|
|
||||||
fn xam_alloc(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
fn xam_alloc(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||||
// r3 = flags, r4 = size, r5 = out_ptr_ptr
|
// r3 = flags, r4 = size, r5 = out_ptr_ptr
|
||||||
let size = ctx.gpr[4] as u32;
|
let size = ctx.gpr[4] as u32;
|
||||||
let out_ptr = ctx.gpr[5] as u32;
|
let out_ptr = ctx.gpr[5] as u32;
|
||||||
@@ -154,7 +229,7 @@ fn xam_alloc(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelStat
|
|||||||
|
|
||||||
// ===== User =====
|
// ===== User =====
|
||||||
|
|
||||||
fn xam_user_get_xuid(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn xam_user_get_xuid(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
// r3 = user_index, r4 = xuid_ptr
|
// r3 = user_index, r4 = xuid_ptr
|
||||||
let xuid_ptr = ctx.gpr[4] as u32;
|
let xuid_ptr = ctx.gpr[4] as u32;
|
||||||
if xuid_ptr != 0 {
|
if xuid_ptr != 0 {
|
||||||
@@ -163,7 +238,7 @@ fn xam_user_get_xuid(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut K
|
|||||||
ctx.gpr[3] = 0;
|
ctx.gpr[3] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xam_user_get_name(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn xam_user_get_name(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
// r3 = user_index, r4 = buffer, r5 = buffer_size
|
// r3 = user_index, r4 = buffer, r5 = buffer_size
|
||||||
let buffer = ctx.gpr[4] as u32;
|
let buffer = ctx.gpr[4] as u32;
|
||||||
if buffer != 0 {
|
if buffer != 0 {
|
||||||
@@ -172,14 +247,14 @@ fn xam_user_get_name(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut K
|
|||||||
ctx.gpr[3] = 0;
|
ctx.gpr[3] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xam_user_read_profile_settings(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn xam_user_read_profile_settings(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
// Return error — no profile
|
// Return error — no profile
|
||||||
ctx.gpr[3] = 0x0000_048B; // ERROR_NOT_FOUND
|
ctx.gpr[3] = 0x0000_048B; // ERROR_NOT_FOUND
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== System =====
|
// ===== System =====
|
||||||
|
|
||||||
fn xam_get_execution_id(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
fn xam_get_execution_id(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||||
// r3 = execution_id_ptr_ptr — write pointer to execution info
|
// r3 = execution_id_ptr_ptr — write pointer to execution info
|
||||||
let ptr_ptr = ctx.gpr[3] as u32;
|
let ptr_ptr = ctx.gpr[3] as u32;
|
||||||
if ptr_ptr != 0 {
|
if ptr_ptr != 0 {
|
||||||
@@ -197,25 +272,25 @@ fn xam_get_execution_id(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut
|
|||||||
ctx.gpr[3] = 0;
|
ctx.gpr[3] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xam_get_system_version(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn xam_get_system_version(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
ctx.gpr[3] = 0x2000_0000; // System version
|
ctx.gpr[3] = 0x2000_0000; // System version
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Notify =====
|
// ===== Notify =====
|
||||||
|
|
||||||
fn xam_notify_create_listener(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
fn xam_notify_create_listener(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||||||
let handle = state.alloc_handle();
|
let handle = state.alloc_handle();
|
||||||
ctx.gpr[3] = handle as u64;
|
ctx.gpr[3] = handle as u64;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xnotify_get_next(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn xnotify_get_next(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
// r3 = handle, r4 = id_ptr, r5 = param_ptr
|
// r3 = handle, r4 = id_ptr, r5 = param_ptr
|
||||||
ctx.gpr[3] = 0; // FALSE (no notifications)
|
ctx.gpr[3] = 0; // FALSE (no notifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Session =====
|
// ===== Session =====
|
||||||
|
|
||||||
fn xam_session_create_handle(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
fn xam_session_create_handle(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||||
// r3 = handle_ptr
|
// r3 = handle_ptr
|
||||||
let handle_ptr = ctx.gpr[3] as u32;
|
let handle_ptr = ctx.gpr[3] as u32;
|
||||||
let handle = state.alloc_handle();
|
let handle = state.alloc_handle();
|
||||||
@@ -227,19 +302,19 @@ fn xam_session_create_handle(ctx: &mut PpcContext, mem: &mut GuestMemory, state:
|
|||||||
|
|
||||||
// ===== Locale =====
|
// ===== Locale =====
|
||||||
|
|
||||||
fn xget_avpack(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn xget_avpack(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
ctx.gpr[3] = 0x16; // HDMI
|
ctx.gpr[3] = 0x16; // HDMI
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xget_game_region(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn xget_game_region(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
ctx.gpr[3] = 0xFF; // All regions
|
ctx.gpr[3] = 0xFF; // All regions
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xget_language(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn xget_language(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
ctx.gpr[3] = 1; // English
|
ctx.gpr[3] = 1; // English
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xget_video_mode(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
fn xget_video_mode(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) {
|
||||||
// r3 = video_mode_ptr
|
// r3 = video_mode_ptr
|
||||||
let ptr = ctx.gpr[3] as u32;
|
let ptr = ctx.gpr[3] as u32;
|
||||||
if ptr != 0 {
|
if ptr != 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user