ITERATE-2.V: scheduler priority aging closes 18-day AUDIT-049 wedge
Priority aging in xenia-cpu/scheduler.rs:pick_runnable
(effective_priority = base + age_bonus(now_round - last_run_round),
capped at +31, AGING_ROUNDS_PER_BONUS=1). Strict-priority was parking
priority=0 threads behind CPU-bound priority=15 audio mixer
(sub_824D1328 guest spinwait at PC=0x824d1404 on CPU5). Aging
eventually picks the starved thread, breaking the producer-consumer
cycle that caused 5-tid wedge at PC=0x824ac578 since AUDIT-049 (10 May).
Cascade observed: tid=13 clean exit; events 121K -> 13M (107x); last
host_ns 767ms -> 51,011ms (66x); 8 new threads spawn; VdSwap 1 -> 2.
Complete two-day iterate sequence (2026-05-27 -> 2026-05-28):
- 2.F: VdSwap drain timeout 900ms -> 1ms (xenia-gpu/handle.rs); 876x
perf win on VdSwap kernel callback
- 2.H: vA0000000 physical heap bucket added (state.rs, exports.rs);
ctx_ptrs now in 0xA0000000-0xBFFFFFFF range matching canary
- 2.L: Phase-A diff harness categorized [return_value mismatch],
[status mismatch], [args_resolved.path mismatch] tags
(tools/diff-events/diff_events.py); closes reading-error #41
(silent test-harness state leak invalidating trace diffs)
- 2.M: always-on exit-thread-state.json sibling to Phase-A JSONL
(event_log.rs + xenia-app/main.rs); closes reading-error #42
(Phase-A blind to blocked-forever waits)
- 2.Q: signal.match kernel instrumentation in NtSetEvent /
NtReleaseSemaphore / KeSetEvent / KeReleaseSemaphore
(exports.rs); emits target_handle + waiter_count + waiter_tids
- 2.T: wake.requested kernel instrumentation in wake_eligible_waiters
(exports.rs); emits target_tid + transition + new_state
- 2.V: scheduler priority aging (xenia-cpu/scheduler.rs) [keystone]
Plus accumulated WIP from earlier May (contention_manifest,
phase_b_snapshot, xam/xaudio enhancements, analysis db, xex loader,
xenia-app main loop, etc.). Audit-runs/ artifacts remain untracked
per project convention.
Tests: 300 xenia-cpu / 227 xenia-kernel / 5 xenia-app / 19 xenia-path
/ 30+ smaller suites -- all PASS, 0 regressions. Determinism preserved
(2x cold runs bit-identical at 13,003,881 events post-2.V).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -242,6 +242,41 @@ enum Commands {
|
||||
/// line). Stdout when omitted.
|
||||
#[arg(long)]
|
||||
lr_trace_out: Option<String>,
|
||||
/// Phase A diff-harness — write schema-v1 JSONL events (kernel
|
||||
/// calls, thread create/exit, handle create/destroy, waits) to
|
||||
/// this path. Empty (default) = disabled, zero overhead.
|
||||
/// Schema: `xenia-rs/audit-runs/phase-a-diff-harness/schema-v1.md`.
|
||||
/// Settable via `XENIA_PHASE_A_EVENT_LOG`.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
phase_a_event_log: Option<PathBuf>,
|
||||
/// Phase B initial-state snapshot — write structured state
|
||||
/// snapshot to `<dir>/ours/` at the moment immediately before
|
||||
/// the first guest PPC instruction of the XEX entry_point.
|
||||
/// Empty (default) = disabled, zero overhead. Settable via
|
||||
/// `XENIA_PHASE_B_SNAPSHOT_DIR`. See
|
||||
/// `xenia-rs/audit-runs/phase-b-state-equivalence/`.
|
||||
#[arg(long, value_name = "DIR")]
|
||||
phase_b_snapshot_dir: Option<PathBuf>,
|
||||
/// Phase B: after writing the snapshot, exit the process
|
||||
/// immediately (`_exit(0)`) so re-runs are byte-deterministic.
|
||||
/// Settable via `XENIA_PHASE_B_SNAPSHOT_AND_EXIT`.
|
||||
#[arg(long)]
|
||||
phase_b_snapshot_and_exit: bool,
|
||||
/// Phase B: in memory.json, populate `section_contents` with raw
|
||||
/// bytes for every committed region. Default false. Settable via
|
||||
/// `XENIA_PHASE_B_DUMP_SECTION_CONTENT`.
|
||||
#[arg(long)]
|
||||
phase_b_dump_section_content: bool,
|
||||
/// review-a Step 1 diagnostic crowbar — when set, synthesises
|
||||
/// the 4 `sub_825070F0` worker spawns once instruction_count
|
||||
/// crosses the configured threshold (default 20M instr,
|
||||
/// override via `XENIA_CROWBAR_TRIGGER_INSTR=N`). **NOT a
|
||||
/// fix**: bypasses the natural-activation gate to test
|
||||
/// whether the workers function correctly IF activated. Off
|
||||
/// by default; settable via `XENIA_CROWBAR_WORKERS=1`. See
|
||||
/// `xenia-rs/audit-runs/review-a-step1-crowbar/`.
|
||||
#[arg(long)]
|
||||
force_spawn_workers: bool,
|
||||
},
|
||||
/// Browse XISO disc image contents
|
||||
Browse {
|
||||
@@ -405,7 +440,45 @@ fn main() -> Result<()> {
|
||||
probe_db,
|
||||
lr_trace,
|
||||
lr_trace_out,
|
||||
} => cmd_exec(
|
||||
phase_a_event_log,
|
||||
phase_b_snapshot_dir,
|
||||
phase_b_snapshot_and_exit,
|
||||
phase_b_dump_section_content,
|
||||
force_spawn_workers,
|
||||
} => {
|
||||
// review-a Step 1: CLI flag → env var so cmd_exec_inner's
|
||||
// existing env-var-driven cvar wire-up picks it up. Avoids
|
||||
// threading two more params through the (already long)
|
||||
// cmd_exec / cmd_exec_inner signatures.
|
||||
if force_spawn_workers {
|
||||
// SAFETY: pre-thread-spawn process startup; no races.
|
||||
unsafe { std::env::set_var("XENIA_CROWBAR_WORKERS", "1"); }
|
||||
}
|
||||
// Resolve the Phase A event-log path: CLI flag wins over env var.
|
||||
// Empty/unset → emitter stays disabled (zero overhead).
|
||||
let phase_a_path: Option<PathBuf> = phase_a_event_log
|
||||
.or_else(|| std::env::var("XENIA_PHASE_A_EVENT_LOG").ok().map(PathBuf::from));
|
||||
xenia_kernel::event_log::init(phase_a_path.as_deref());
|
||||
// Resolve Phase B flags: CLI > env var. Empty/unset = disabled.
|
||||
fn truthy(s: &str) -> bool {
|
||||
let s = s.trim().to_ascii_lowercase();
|
||||
s == "1" || s == "true" || s == "yes"
|
||||
}
|
||||
let phase_b_dir: Option<PathBuf> = phase_b_snapshot_dir
|
||||
.or_else(|| std::env::var("XENIA_PHASE_B_SNAPSHOT_DIR").ok().map(PathBuf::from));
|
||||
let phase_b_exit = phase_b_snapshot_and_exit
|
||||
|| std::env::var("XENIA_PHASE_B_SNAPSHOT_AND_EXIT")
|
||||
.ok()
|
||||
.as_deref()
|
||||
.map(truthy)
|
||||
.unwrap_or(false);
|
||||
let phase_b_dump = phase_b_dump_section_content
|
||||
|| std::env::var("XENIA_PHASE_B_DUMP_SECTION_CONTENT")
|
||||
.ok()
|
||||
.as_deref()
|
||||
.map(truthy)
|
||||
.unwrap_or(false);
|
||||
cmd_exec(
|
||||
&path,
|
||||
max_instructions,
|
||||
ips_limit,
|
||||
@@ -431,7 +504,11 @@ fn main() -> Result<()> {
|
||||
probe_db.as_deref(),
|
||||
lr_trace.as_deref(),
|
||||
lr_trace_out.as_deref(),
|
||||
),
|
||||
phase_b_dir,
|
||||
phase_b_exit,
|
||||
phase_b_dump,
|
||||
)
|
||||
}
|
||||
Commands::Browse { path } => cmd_browse(&path),
|
||||
Commands::Info { path } => cmd_info(&path),
|
||||
Commands::Extract { path, output, db } => cmd_extract(&path, output.as_deref(), db.as_deref()),
|
||||
@@ -662,6 +739,9 @@ fn cmd_exec(
|
||||
probe_db: Option<&str>,
|
||||
lr_trace: Option<&str>,
|
||||
lr_trace_out: Option<&str>,
|
||||
phase_b_snapshot_dir: Option<PathBuf>,
|
||||
phase_b_snapshot_and_exit: bool,
|
||||
phase_b_dump_section_content: bool,
|
||||
) -> Result<()> {
|
||||
cmd_exec_inner(
|
||||
path,
|
||||
@@ -692,6 +772,9 @@ fn cmd_exec(
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
phase_b_snapshot_dir,
|
||||
phase_b_snapshot_and_exit,
|
||||
phase_b_dump_section_content,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -738,6 +821,9 @@ fn cmd_check(
|
||||
out,
|
||||
expect,
|
||||
stable_digest,
|
||||
None, // phase_b_snapshot_dir — never wanted on check goldens
|
||||
false, // phase_b_snapshot_and_exit
|
||||
false, // phase_b_dump_section_content
|
||||
)
|
||||
}
|
||||
|
||||
@@ -770,6 +856,9 @@ fn cmd_exec_inner(
|
||||
digest_out: Option<&str>,
|
||||
digest_expect: Option<&str>,
|
||||
stable_digest: bool,
|
||||
phase_b_snapshot_dir: Option<PathBuf>,
|
||||
phase_b_snapshot_and_exit: bool,
|
||||
phase_b_dump_section_content: bool,
|
||||
) -> Result<()> {
|
||||
let started = Instant::now();
|
||||
let data = load_xex_data(path)?;
|
||||
@@ -840,22 +929,121 @@ fn cmd_exec_inner(
|
||||
info!(thunks = thunk_map.len(), "import thunks mapped");
|
||||
|
||||
// ── Phase 2: CPU initialization per xenia-canary ─────────────────────
|
||||
// Allocate stack (1MB at 0x70000000)
|
||||
//
|
||||
// Stack VA = `0x70000000`, size honors `XEX_HEADER_DEFAULT_STACK_SIZE`
|
||||
// (key `0x00020200`) when present, falling back to 1 MiB. The XEX
|
||||
// header's stack-size value is rounded up to a 4 KiB page boundary
|
||||
// before allocation. NOTE: guard pages are NOT yet allocated — that
|
||||
// would require extending `xenia-memory` with a `NoAccess` protection
|
||||
// flag and platform-layer page-decommit support, deferred to a future
|
||||
// pass. Overflow into adjacent unmapped pages currently silently
|
||||
// drops the write (per `GuestMemory::write_u32`'s `is_mapped` guard).
|
||||
let stack_base = 0x7000_0000u32;
|
||||
let stack_size = 0x10_0000u32;
|
||||
let stack_size = {
|
||||
let from_header = xenia_xex::loader::get_stack_size(&header);
|
||||
let rounded = (from_header + 0xFFF) & !0xFFFu32;
|
||||
rounded.max(0x1_0000) // never less than 64 KiB
|
||||
};
|
||||
mem.alloc(stack_base, stack_size, rw)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to allocate stack: {}", e))?;
|
||||
|
||||
// Allocate PCR (Processor Control Region) and TLS
|
||||
// ── TLS region ────────────────────────────────────────────────────────
|
||||
//
|
||||
// Canary's `XEX_HEADER_TLS_INFO` (key `0x00020104`) describes the title's
|
||||
// TLS template image (`raw_data_address` / `raw_data_size`) and the
|
||||
// number of dynamic slots (`slot_count`, default 1024 per canary's
|
||||
// `kDefaultTlsSlotCount` in `xthread.cc:335`). Layout in guest memory:
|
||||
//
|
||||
// [tls_extended_image (raw_data_size B) | tls_dynamic_slots (slot_count*4 B)]
|
||||
//
|
||||
// The PCR's `tls_ptr` (PCR+0) points at the START of the dynamic-slot
|
||||
// area — i.e. the dynamic slots live IMMEDIATELY AFTER the image bytes.
|
||||
// For ours we keep the historical fixed VA `0x7FFE_0000` but size the
|
||||
// region from the parsed TLS info (clamped to at least 4 KiB). When
|
||||
// the XEX has no TLS info, the block is a 4 KiB zero region — matching
|
||||
// the pre-Phase-2 behavior.
|
||||
let tls_info = xenia_xex::loader::get_tls_info(&header, &data);
|
||||
let tls_raw_data_size = tls_info.map(|t| t.raw_data_size).unwrap_or(0);
|
||||
let tls_slot_count = tls_info.map(|t| t.slot_count).unwrap_or(0).max(1024);
|
||||
let tls_dynamic_bytes = tls_slot_count.saturating_mul(4);
|
||||
let tls_total_bytes = {
|
||||
let needed = tls_raw_data_size.saturating_add(tls_dynamic_bytes);
|
||||
let rounded = (needed + 0xFFF) & !0xFFFu32;
|
||||
rounded.max(0x1000) // never less than 4 KiB
|
||||
};
|
||||
|
||||
let pcr_addr = 0x7FFF_0000u32;
|
||||
let tls_addr = 0x7FFE_0000u32;
|
||||
mem.alloc(pcr_addr, 0x1000, rw)?;
|
||||
mem.alloc(tls_addr, 0x1000, rw)?;
|
||||
mem.alloc(tls_addr, tls_total_bytes, rw)?;
|
||||
|
||||
// Initialize PCR structure
|
||||
mem.write_u32(pcr_addr, tls_addr); // PCR->tls_ptr
|
||||
mem.write_u32(pcr_addr + 0x100, 0x1000); // PCR->current_thread (fake)
|
||||
mem.write_u32(pcr_addr + 0x150, 0); // PCR->dpc_active
|
||||
// Copy the title's TLS template (initial-value image for `__declspec(thread)`
|
||||
// variables) into the head of the TLS region. Canary mirrors this with
|
||||
// `Memory::Copy(tls_static_address_, tls_header->raw_data_address,
|
||||
// tls_header->raw_data_size)` (xthread.cc:357-360). When
|
||||
// `raw_data_size` is zero (no TLS image), the region is left zeroed.
|
||||
if let Some(info) = tls_info {
|
||||
if info.raw_data_size > 0 && info.raw_data_address != 0 {
|
||||
let mut buf = vec![0u8; info.raw_data_size as usize];
|
||||
mem.read_bytes(info.raw_data_address, &mut buf);
|
||||
mem.write_bulk(tls_addr, &buf);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Guest X_KTHREAD struct ────────────────────────────────────────────
|
||||
//
|
||||
// Canary stores a real `X_KTHREAD` in guest memory (`xthread.h:260-`),
|
||||
// and PCR `[+0x100]` (= `prcb_data.current_thread`) holds its VA. Ours
|
||||
// previously wrote the bare host-side handle `0x1000` there, so any
|
||||
// guest pointer-walk through `r13[+0x100]` read garbage. Allocate a
|
||||
// 0x100-byte zero block at a fixed VA `0x7FFD_0000` (just below the
|
||||
// TLS region, in unused address space) and populate the minimum
|
||||
// credible fields:
|
||||
//
|
||||
// +0x00 X_DISPATCH_HEADER:
|
||||
// [+0x00] u8 type = 6 (ThreadObject)
|
||||
// [+0x04] u32 signal_state = 0
|
||||
// [+0x08] X_LIST_ENTRY wait_list { flink, blink } — both self-pointers
|
||||
// +0x5C u32 stack_base (high addr)
|
||||
// +0x60 u32 stack_limit (low addr)
|
||||
// +0x68 u32 tls_address
|
||||
//
|
||||
// The dispatcher-header `type` byte for ThreadObject is `0x06` in the
|
||||
// Vista/Xbox 360 kernel (matches DISPATCHER_HEADER reference at
|
||||
// `xenia-canary/src/xenia/kernel/xobject.h:37-62`); setting it non-zero
|
||||
// is what prevents the worst null-deref class on KTHREAD pointer walks.
|
||||
let kthread_addr = 0x7FFD_0000u32;
|
||||
let kthread_size = 0x1000u32;
|
||||
mem.alloc(kthread_addr, kthread_size, rw)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to allocate X_KTHREAD region: {}", e))?;
|
||||
// X_DISPATCH_HEADER
|
||||
mem.write_u8(kthread_addr, 0x06); // type = ThreadObject
|
||||
mem.write_u32(kthread_addr + 0x04, 0); // signal_state
|
||||
mem.write_u32(kthread_addr + 0x08, kthread_addr + 0x08); // wait_list.flink (self)
|
||||
mem.write_u32(kthread_addr + 0x0C, kthread_addr + 0x08); // wait_list.blink (self)
|
||||
// Stack/TLS pointers (canary X_KTHREAD layout, xthread.h:267-270).
|
||||
mem.write_u32(kthread_addr + 0x5C, stack_base + stack_size); // stack_base = high addr
|
||||
mem.write_u32(kthread_addr + 0x60, stack_base); // stack_limit = low addr
|
||||
mem.write_u32(kthread_addr + 0x68, tls_addr); // tls_address
|
||||
|
||||
// ── PCR initialization ────────────────────────────────────────────────
|
||||
//
|
||||
// Canary `X_KPCR` layout (xthread.h:171-223). Fields ours now populates:
|
||||
// +0x000 tls_ptr — base of dynamic TLS slots
|
||||
// +0x030 pcr_ptr u64 BE — self-reference (PCR base)
|
||||
// +0x070 stack_base_ptr — top of stack (high addr)
|
||||
// +0x074 stack_end_ptr — bottom of stack (low addr)
|
||||
// +0x100 prcb_data.current_thread — VA of the guest X_KTHREAD
|
||||
// +0x150 prcb_data.dpc_active — 0
|
||||
// +0x2A8 prcb — pointer back to prcb_data (= pcr+0x100)
|
||||
// (Skipped: +0x038 `host_stash` — host pointer slot, not applicable to ours.)
|
||||
mem.write_u32(pcr_addr, tls_addr); // tls_ptr
|
||||
mem.write_u64(pcr_addr + 0x030, pcr_addr as u64); // pcr_ptr (self-ref, BE u64)
|
||||
mem.write_u32(pcr_addr + 0x070, stack_base + stack_size); // stack_base_ptr (high)
|
||||
mem.write_u32(pcr_addr + 0x074, stack_base); // stack_end_ptr (low)
|
||||
mem.write_u32(pcr_addr + 0x100, kthread_addr); // prcb_data.current_thread
|
||||
mem.write_u32(pcr_addr + 0x150, 0); // prcb_data.dpc_active
|
||||
mem.write_u32(pcr_addr + 0x2A8, pcr_addr + 0x100); // prcb -> prcb_data
|
||||
|
||||
// Set up CPU context per xenia-canary/cpu/thread_state.cc.
|
||||
//
|
||||
@@ -925,6 +1113,13 @@ fn cmd_exec_inner(
|
||||
let mut kernel = xenia_kernel::KernelState::with_gpu(gpu_backend);
|
||||
kernel.image_base = base;
|
||||
kernel.xex_system_flags = xenia_xex::loader::get_system_flags(&header);
|
||||
// Phase B — install the entry-PC for the snapshot hook's identity
|
||||
// check, plus the cvar-equivalent flags resolved by the caller. When
|
||||
// `phase_b_snapshot_dir` is `None`, the hook short-circuits.
|
||||
kernel.entry_pc = entry;
|
||||
kernel.phase_b_snapshot_dir = phase_b_snapshot_dir.clone();
|
||||
kernel.phase_b_snapshot_and_exit = phase_b_snapshot_and_exit;
|
||||
kernel.phase_b_dump_section_content = phase_b_dump_section_content;
|
||||
// Drain the reverse thunk map into the kernel so `XexGetProcedureAddress`
|
||||
// can resolve ordinals back to callable thunk addresses.
|
||||
for (module, ordinal, addr) in thunk_addr_map.drain(..) {
|
||||
@@ -948,6 +1143,38 @@ fn cmd_exec_inner(
|
||||
});
|
||||
let parallel_active = parallel || parallel_via_env;
|
||||
kernel.parallel_active = parallel_active;
|
||||
|
||||
// Phase D Stage 3 — install a contention-replay manifest if pointed
|
||||
// to via `XENIA_CONTENTION_MANIFEST_PATH`. The manifest is built by
|
||||
// Stage 2's python tool from a Stage-1 cvar-ON canary trace. Unset
|
||||
// = default mode (no replay, identical to pre-Stage-3 behavior).
|
||||
// Errors are non-fatal (log + continue without replay) so a stale
|
||||
// path doesn't brick the run.
|
||||
if let Ok(path) = std::env::var("XENIA_CONTENTION_MANIFEST_PATH") {
|
||||
let trimmed = path.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let p = std::path::PathBuf::from(trimmed);
|
||||
match xenia_kernel::contention_manifest::ContentionManifest::load_from_file(&p) {
|
||||
Ok(m) => {
|
||||
let count = m.initial_count();
|
||||
let arc = std::sync::Arc::new(m);
|
||||
kernel.install_contention_manifest(Some(arc));
|
||||
tracing::info!(
|
||||
"Phase D Stage 3: loaded contention manifest from {:?} ({} entries)",
|
||||
p,
|
||||
count
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Phase D Stage 3: failed to load contention manifest from {:?}: {} — replay disabled",
|
||||
p,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// AUDIT-032: default is `KernelState::xaudio_tick_enabled = true` now
|
||||
// that the dedicated worker eliminates HW-thread hijack regressions.
|
||||
// Treat `--xaudio-tick` / `XENIA_XAUDIO_TICK=...` as an explicit
|
||||
@@ -979,6 +1206,38 @@ fn cmd_exec_inner(
|
||||
"XAudio callback ticker enabled (AUDIT-032 default; toggle via --xaudio-tick / XENIA_XAUDIO_TICK)"
|
||||
);
|
||||
}
|
||||
// review-a Step 1 — `--force-spawn-workers` / `XENIA_CROWBAR_WORKERS=1`.
|
||||
// Diagnostic-only, default-OFF. See
|
||||
// `xenia-rs/audit-runs/review-a-step1-crowbar/`.
|
||||
let crowbar_env_on = std::env::var("XENIA_CROWBAR_WORKERS")
|
||||
.ok()
|
||||
.as_deref()
|
||||
.map(|v| {
|
||||
let v = v.trim().to_ascii_lowercase();
|
||||
v == "1" || v == "true" || v == "yes"
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if crowbar_env_on {
|
||||
kernel.crowbar_workers_enabled = true;
|
||||
}
|
||||
if let Ok(v) = std::env::var("XENIA_CROWBAR_TRIGGER_INSTR") {
|
||||
if let Ok(n) = v.trim().parse::<u64>() {
|
||||
kernel.crowbar_workers_trigger_instr = n;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"XENIA_CROWBAR_TRIGGER_INSTR={:?} — failed to parse as u64; keeping default {}",
|
||||
v,
|
||||
kernel.crowbar_workers_trigger_instr,
|
||||
);
|
||||
}
|
||||
}
|
||||
if kernel.crowbar_workers_enabled && !quiet {
|
||||
tracing::warn!(
|
||||
"review-a CROWBAR enabled: will force-spawn 4 sub_825070F0 workers \
|
||||
at instr={} (NOT a fix — diagnostic only)",
|
||||
kernel.crowbar_workers_trigger_instr,
|
||||
);
|
||||
}
|
||||
if reservations_table || reservations_via_env || parallel_active {
|
||||
kernel.reservations.enable();
|
||||
if !quiet {
|
||||
@@ -1305,14 +1564,47 @@ fn cmd_exec_inner(
|
||||
main_handle,
|
||||
&mut mem,
|
||||
);
|
||||
// Phase C+16: main thread self-reference. Mirrors canary's
|
||||
// `KernelState::LaunchModule` → `thread->Create()` → `RetainHandle()`
|
||||
// at xthread.cc:414 (the "main XThread" also goes through Create()).
|
||||
// Released at LR-sentinel implicit-exit in the prologue/epilogue
|
||||
// path. Without this, ours's main refcount=1 (creator only) vs
|
||||
// canary's 2 (creator + self) — masked at present because the guest
|
||||
// never calls `NtClose` on the main-thread handle, but kept symmetric
|
||||
// to avoid asymmetric `handle.destroy` on shutdown.
|
||||
kernel.retain_handle(main_handle);
|
||||
|
||||
// If the input was a disc image, mount it so the kernel's file I/O
|
||||
// handlers can serve the game's own assets via VFS.
|
||||
if path.to_lowercase().ends_with(".iso") || path.to_lowercase().ends_with(".xiso") {
|
||||
match xenia_vfs::disc_image::DiscImageDevice::open("d", std::path::Path::new(path)) {
|
||||
// Mount the title's content into the VFS so the kernel's file I/O
|
||||
// handlers (`NtCreateFile`, `NtOpenFile`, etc.) can serve game-data
|
||||
// reads. Canary always mounts `game:` + `d:` regardless of input
|
||||
// format (xenia_main.cc:611-651); ours's path normalisation already
|
||||
// strips both prefixes to a single bucket (see
|
||||
// `crate::path::DEVICE_PREFIXES` in xenia-kernel and `is_disc_prefix`
|
||||
// in exports.rs:1725), so a single backing `VfsDevice` covers both.
|
||||
//
|
||||
// Mount logic:
|
||||
// - `.iso` / `.xiso` → `DiscImageDevice`
|
||||
// - directory → `HostPathDevice` rooted at the directory
|
||||
// - bare .xex file → `HostPathDevice` rooted at the file's parent
|
||||
// - STFS / CON / PIRS containers — NOT YET (no reader in ours;
|
||||
// would be 500+ LOC. Deferred to a future pass.)
|
||||
let input_path = std::path::Path::new(path);
|
||||
let lower = path.to_lowercase();
|
||||
if lower.ends_with(".iso") || lower.ends_with(".xiso") {
|
||||
match xenia_vfs::disc_image::DiscImageDevice::open("d", input_path) {
|
||||
Ok(disc) => kernel.vfs = Some(Box::new(disc)),
|
||||
Err(e) => tracing::warn!("Could not mount disc image for VFS: {}", e),
|
||||
}
|
||||
} else if input_path.is_dir() {
|
||||
tracing::info!("VFS: mounting host directory {:?} as game:/d:", input_path);
|
||||
kernel.vfs = Some(Box::new(xenia_vfs::device::HostPathDevice::new("game", input_path)));
|
||||
} else if let Some(parent) = input_path.parent() {
|
||||
// Bare XEX file — mount its containing directory so the title can
|
||||
// reach sibling assets via `game:\<name>`.
|
||||
if !parent.as_os_str().is_empty() {
|
||||
tracing::info!("VFS: mounting XEX parent directory {:?} as game:/d:", parent);
|
||||
kernel.vfs = Some(Box::new(xenia_vfs::device::HostPathDevice::new("game", parent)));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 3: Data export patching (variable imports) ─────────────────
|
||||
@@ -1324,14 +1616,80 @@ fn cmd_exec_inner(
|
||||
kernel.heap_alloc(size, mem).unwrap_or(0)
|
||||
};
|
||||
|
||||
// Helper: allocate a 0x1C-byte X_OBJECT_TYPE descriptor with the
|
||||
// four-char-code `pool_tag` at +0x18 (BE-readable) and write its
|
||||
// guest VA into the import slot. Mirrors canary's
|
||||
// `InitializeKernelGuestGlobals` populating per-type descriptors at
|
||||
// `kernel_state.cc:1538-1615` — the type-tag bytes are non-zero
|
||||
// there, so any guest code that reads the tag-byte field gets the
|
||||
// real FourCC instead of zero.
|
||||
let write_object_type =
|
||||
|addr: u32, pool_tag: u32, mem: &xenia_memory::GuestMemory, kernel: &mut xenia_kernel::KernelState| {
|
||||
let block = kernel.heap_alloc(0x1C, mem).unwrap_or(0);
|
||||
if block != 0 {
|
||||
mem.write_u32(block + 0x18, pool_tag);
|
||||
}
|
||||
mem.write_u32(addr, block);
|
||||
};
|
||||
|
||||
for lib in &header.import_libraries {
|
||||
for imp in &lib.imports {
|
||||
if imp.record_type != 0 { continue; } // Only variable entries
|
||||
let addr = imp.address;
|
||||
match (lib.name.as_str(), imp.ordinal) {
|
||||
// ──── KernelGuestGlobals object-type descriptors ────
|
||||
// 0x1C-byte `X_OBJECT_TYPE` blocks with pool-tag FourCC at
|
||||
// +0x18. Canary populates these via
|
||||
// `InitializeKernelGuestGlobals` (`kernel_state.cc:1511+`);
|
||||
// ours previously left every descriptor as a zero block, so
|
||||
// any guest comparison against the tag-byte signature
|
||||
// diverged. Tags are stored as host-order u32s whose BE
|
||||
// byte-form spells the four-char code; `write_u32` BE-encodes
|
||||
// automatically (see `GuestMemory::write_u32`).
|
||||
("xboxkrnl.exe", 0x000E) => {
|
||||
// ExEventObjectType — pool_tag "EvEv"
|
||||
write_object_type(addr, 0x76657645, &mut mem, &mut kernel);
|
||||
}
|
||||
("xboxkrnl.exe", 0x0012) => {
|
||||
// ExMutantObjectType — pool_tag "Mutu"
|
||||
write_object_type(addr, 0x6174754D, &mut mem, &mut kernel);
|
||||
}
|
||||
("xboxkrnl.exe", 0x0017) => {
|
||||
// ExSemaphoreObjectType — pool_tag "Sema"
|
||||
write_object_type(addr, 0x616D6553, &mut mem, &mut kernel);
|
||||
}
|
||||
("xboxkrnl.exe", 0x001B) => {
|
||||
// ExThreadObjectType — ptr to OBJECT_TYPE descriptor (0x40 bytes)
|
||||
let block = alloc_zero(0x40, &mut mem, &mut kernel);
|
||||
// ExThreadObjectType — pool_tag "Thre"
|
||||
write_object_type(addr, 0x65726854, &mut mem, &mut kernel);
|
||||
}
|
||||
("xboxkrnl.exe", 0x001C) => {
|
||||
// ExTimerObjectType — pool_tag "Time"
|
||||
write_object_type(addr, 0x656D6954, &mut mem, &mut kernel);
|
||||
}
|
||||
("xboxkrnl.exe", 0x0036) => {
|
||||
// IoCompletionObjectType — pool_tag "Comp"
|
||||
write_object_type(addr, 0x706D6F43, &mut mem, &mut kernel);
|
||||
}
|
||||
("xboxkrnl.exe", 0x003A) => {
|
||||
// IoDeviceObjectType — pool_tag "Devi"
|
||||
write_object_type(addr, 0x69766544, &mut mem, &mut kernel);
|
||||
}
|
||||
("xboxkrnl.exe", 0x003E) => {
|
||||
// IoFileObjectType — pool_tag "File"
|
||||
write_object_type(addr, 0x656C6946, &mut mem, &mut kernel);
|
||||
}
|
||||
("xboxkrnl.exe", 0x0106) => {
|
||||
// ObDirectoryObjectType — pool_tag "Dire"
|
||||
write_object_type(addr, 0x65726944, &mut mem, &mut kernel);
|
||||
}
|
||||
("xboxkrnl.exe", 0x0112) => {
|
||||
// ObSymbolicLinkObjectType — pool_tag "Symb"
|
||||
write_object_type(addr, 0x626D7953, &mut mem, &mut kernel);
|
||||
}
|
||||
("xboxkrnl.exe", 0x02DB) => {
|
||||
// UsbdBootEnumerationDoneEvent — 0x10-byte X_KEVENT block,
|
||||
// zero-initialised (signalled=false, type=NotificationEvent).
|
||||
let block = alloc_zero(0x10, &mut mem, &mut kernel);
|
||||
mem.write_u32(addr, block);
|
||||
}
|
||||
("xboxkrnl.exe", 0x0059) => {
|
||||
@@ -1340,16 +1698,51 @@ fn cmd_exec_inner(
|
||||
mem.write_u32(addr, block);
|
||||
}
|
||||
("xboxkrnl.exe", 0x00AD) => {
|
||||
// KeTimeStampBundle — 0x18 block with FILETIME at +0 and
|
||||
// interrupt-time u64 at +0x10. Mirrors the clock used by
|
||||
// KeQuerySystemTime so fast-path readers see consistent values.
|
||||
// KeTimeStampBundle — 0x18-byte `X_TIME_STAMP_BUNDLE`
|
||||
// matching canary's `kernel_state.h:98-104`:
|
||||
// +0x00 u64 interrupt_time (BE, 100-ns ticks since boot)
|
||||
// +0x08 u64 system_time (BE, 100-ns Windows FILETIME)
|
||||
// +0x10 u32 tick_count (BE, monotonic ms since boot)
|
||||
// +0x14 u32 padding
|
||||
// Stash the VA in `KernelState` so the 1 ms host-side
|
||||
// repeating updater spawned later in this file can refresh
|
||||
// the fields — without that updater, polling loops that
|
||||
// wait on `tick_count` to advance hang forever (the
|
||||
// previous "FILETIME at +0 and +0x10" layout never wrote
|
||||
// +0x08 at all and never advanced).
|
||||
let block = alloc_zero(0x18, &mut mem, &mut kernel);
|
||||
if block != 0 {
|
||||
let fake_time: u64 = 132_500_000_000_000_000; // ~2021 FILETIME
|
||||
mem.write_u32(block, (fake_time >> 32) as u32);
|
||||
mem.write_u32(block + 4, fake_time as u32);
|
||||
mem.write_u32(block + 0x10, (fake_time >> 32) as u32);
|
||||
mem.write_u32(block + 0x14, fake_time as u32);
|
||||
// Match ours's existing fixed `KeQueryInterruptTime`
|
||||
// / `KeQuerySystemTime` constants for the initial
|
||||
// sample — the timer thread will overwrite within
|
||||
// ~1 ms, so these values are seen only briefly.
|
||||
mem.write_u64(block, 0x0000_0001_0000_0000); // interrupt_time
|
||||
mem.write_u64(block + 0x08, 132_500_000_000_000_000); // system_time
|
||||
mem.write_u32(block + 0x10, 0); // tick_count
|
||||
mem.write_u32(block + 0x14, 0); // padding
|
||||
kernel.ke_timestamp_bundle_ptr = block;
|
||||
}
|
||||
mem.write_u32(addr, block);
|
||||
}
|
||||
("xboxkrnl.exe", 0x000C) => {
|
||||
// ExConsoleGameRegion — 4-byte u32 = 0xFFFFFFFF (region-free).
|
||||
// Canary writes this at `xboxkrnl_module.cc:144-150`.
|
||||
let block = alloc_zero(4, &mut mem, &mut kernel);
|
||||
if block != 0 {
|
||||
mem.write_u32(block, 0xFFFF_FFFF);
|
||||
}
|
||||
mem.write_u32(addr, block);
|
||||
}
|
||||
("xboxkrnl.exe", 0x0156) => {
|
||||
// XboxHardwareInfo — 16-byte block. Canary
|
||||
// (`xboxkrnl_module.cc:125-142`) writes `[0]=0x20`
|
||||
// (HDD-present flag, bit 5) and `[4]=0x06` (CPU count),
|
||||
// rest zero. Games branch on these for storage- and
|
||||
// SMP-aware code paths.
|
||||
let block = alloc_zero(16, &mut mem, &mut kernel);
|
||||
if block != 0 {
|
||||
mem.write_u8(block, 0x20);
|
||||
mem.write_u8(block + 4, 0x06);
|
||||
}
|
||||
mem.write_u32(addr, block);
|
||||
}
|
||||
@@ -1361,13 +1754,59 @@ fn cmd_exec_inner(
|
||||
mem.write_u16(addr + 6, 0);
|
||||
}
|
||||
("xboxkrnl.exe", 0x0193) => {
|
||||
// XexExecutableModuleHandle -> image base
|
||||
// XexExecutableModuleHandle: keep the pre-existing
|
||||
// `*XexExecutableModuleHandle = base` write (the
|
||||
// game's CRT branches off this slot's value; an
|
||||
// attempt to wire up a proper LDR_DATA_TABLE_ENTRY +
|
||||
// xex_header_base chain at idx=0 short-circuits the
|
||||
// CRT's early RtlImageXexHeaderField probe, causing
|
||||
// Phase A to diverge at idx=0 instead of growing past
|
||||
// 102014 — see Phase C+3 investigation.md). Instead,
|
||||
// allocate a guest-memory copy of the raw XEX header
|
||||
// bytes (mirrors canary `user_module.cc:223-227`'s
|
||||
// `guest_xex_header_`), record its VA in KernelState
|
||||
// for `rtl_image_xex_header_field` to use as a
|
||||
// fallback when the game passes a NULL `xex_header`
|
||||
// arg (which it does here because the LDR walk
|
||||
// through `base` yields PE OptionalHeader bytes, not
|
||||
// a real header pointer).
|
||||
let header_size = header.header_size as usize;
|
||||
if header_size > 0 && header_size <= data.len() {
|
||||
let xex_va = alloc_zero(header.header_size, &mut mem, &mut kernel);
|
||||
if xex_va != 0 {
|
||||
mem.write_bulk(xex_va, &data[0..header_size]);
|
||||
kernel.xex_header_guest_ptr = xex_va;
|
||||
}
|
||||
}
|
||||
mem.write_u32(addr, base);
|
||||
}
|
||||
("xboxkrnl.exe", 0x01AE) => {
|
||||
// ExLoadedCommandLine — ANSI empty string
|
||||
let block = alloc_zero(0x10, &mut mem, &mut kernel);
|
||||
// Block is already zero-initialized by heap_alloc -> empty string.
|
||||
// ExLoadedCommandLine — 1024-byte ANSI buffer.
|
||||
// Canary's default-init path (`xboxkrnl_module.cc:176-194`)
|
||||
// writes the quoted form `"default.xex"` (with literal
|
||||
// ASCII double-quotes) as a placeholder until post-launch
|
||||
// replacement. An empty zero-block silently violates the
|
||||
// CRT contract (any title that scans for the quote
|
||||
// characters sees end-of-string immediately).
|
||||
let block = alloc_zero(1024, &mut mem, &mut kernel);
|
||||
if block != 0 {
|
||||
let cmdline: &[u8] = b"\"default.xex\"\0";
|
||||
mem.write_bulk(block, cmdline);
|
||||
}
|
||||
mem.write_u32(addr, block);
|
||||
}
|
||||
("xboxkrnl.exe", 0x01AF) => {
|
||||
// ExLoadedImageName — 256-byte ANSI buffer. Canary
|
||||
// (`xboxkrnl_module.cc:166-174`,
|
||||
// `kernel_state.cc:486-495`) post-launch fills this with
|
||||
// the executable module path; for ours we write
|
||||
// "default.xex\0" to match canary's pre-launch state.
|
||||
// Size matches canary's `kExLoadedImageNameSize = 256`.
|
||||
let block = alloc_zero(256, &mut mem, &mut kernel);
|
||||
if block != 0 {
|
||||
let imgname: &[u8] = b"default.xex\0";
|
||||
mem.write_bulk(block, imgname);
|
||||
}
|
||||
mem.write_u32(addr, block);
|
||||
}
|
||||
("xboxkrnl.exe", 0x01BE) => {
|
||||
@@ -1501,6 +1940,44 @@ fn cmd_exec_inner(
|
||||
// responsibility per the trait contract.)
|
||||
let mem_arc = std::sync::Arc::new(mem);
|
||||
|
||||
// ── KeTimeStampBundle 1 ms repeating updater ──
|
||||
//
|
||||
// Canary maintains the bundle's `interrupt_time` / `system_time` /
|
||||
// `tick_count` fields via `HighResolutionTimer::CreateRepeating(1 ms,
|
||||
// UpdateKeTimestampBundle)` registered at `kernel_state.cc:1272-1295`.
|
||||
// Without an equivalent host-side ticker, the bundle stays frozen at its
|
||||
// boot-time values and guest polling loops that wait on `tick_count` to
|
||||
// advance hang forever. Spawn a detached thread that wakes every 1 ms,
|
||||
// recomputes the three fields from a monotonic `Instant`, and writes
|
||||
// them BE through the shared `Arc<GuestMemory>`. Cooperative shutdown
|
||||
// via the existing `shutdown_arc` flag — flipped when the dispatch
|
||||
// returns — so test runs don't leak threads. No-op if the patcher
|
||||
// didn't allocate a bundle (the XEX never imported ord 0x00AD).
|
||||
{
|
||||
let ke_bundle_ptr = kernel.ke_timestamp_bundle_ptr;
|
||||
if ke_bundle_ptr != 0 {
|
||||
let mem_for_timer = mem_arc.clone();
|
||||
let shutdown_for_timer = shutdown_arc.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("ke-timestamp-bundle".to_string())
|
||||
.spawn(move || {
|
||||
use xenia_memory::MemoryAccess;
|
||||
let start = std::time::Instant::now();
|
||||
const SYSTEM_TIME_EPOCH: u64 = 132_500_000_000_000_000;
|
||||
while !shutdown_for_timer.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
let elapsed = start.elapsed();
|
||||
let ms = elapsed.as_millis() as u32;
|
||||
let ticks_100ns = (elapsed.as_micros() as u64) * 10;
|
||||
mem_for_timer.write_u64(ke_bundle_ptr, ticks_100ns);
|
||||
mem_for_timer.write_u64(ke_bundle_ptr + 0x08, SYSTEM_TIME_EPOCH + ticks_100ns);
|
||||
mem_for_timer.write_u32(ke_bundle_ptr + 0x10, ms);
|
||||
}
|
||||
})
|
||||
.expect("spawn ke-timestamp-bundle thread");
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn the real GPU worker if the threaded backend was chosen at
|
||||
// kernel-construction time. The handle the kernel already holds
|
||||
// (`GpuBackend::Threaded`) is the CPU-side proxy; the worker owns
|
||||
@@ -1679,6 +2156,11 @@ fn cmd_exec_inner(
|
||||
}
|
||||
print_summary(kernel.scheduler.ctx(0), &debugger, &db_writer, quiet);
|
||||
dump_thread_diagnostic(&kernel, &*mem_arc, quiet);
|
||||
// Iterate 2.M — always-on (reading-error #42). Emits a
|
||||
// sibling JSON next to the Phase-A trace; runs regardless
|
||||
// of --quiet so future "is the wedge moved?" questions
|
||||
// never depend on a manual non-quiet re-run.
|
||||
write_thread_state_dump(&kernel);
|
||||
info!(
|
||||
wall_ms = started.elapsed().as_millis() as u64,
|
||||
instructions = stats.instruction_count,
|
||||
@@ -1896,6 +2378,7 @@ enum RoundCtl {
|
||||
/// asks for shutdown.
|
||||
fn coord_pre_round(
|
||||
kernel: &mut xenia_kernel::KernelState,
|
||||
mem: &xenia_memory::GuestMemory,
|
||||
stats: &ExecStats,
|
||||
max_instructions: Option<u64>,
|
||||
ips_limit: Option<u64>,
|
||||
@@ -1995,6 +2478,12 @@ fn coord_pre_round(
|
||||
try_inject_audio_callback(kernel);
|
||||
}
|
||||
|
||||
// review-a Step 1 — one-shot diagnostic crowbar. No-op when disabled
|
||||
// or already fired. Uses the caller's `&GuestMemory` directly.
|
||||
if kernel.crowbar_workers_enabled && !kernel.crowbar_workers_fired {
|
||||
kernel.try_fire_crowbar_workers(mem, stats.instruction_count);
|
||||
}
|
||||
|
||||
RoundCtl::Continue
|
||||
}
|
||||
|
||||
@@ -2211,6 +2700,21 @@ fn worker_prologue(
|
||||
|
||||
let pc = kernel.scheduler.ctx(hw_id).pc;
|
||||
|
||||
// Phase B snapshot — no-op when `phase_b_snapshot_dir == None`
|
||||
// (zero-cost Option-tag test on the hot path). Fires once on the
|
||||
// entry thread at the moment immediately before its first guest
|
||||
// instruction at entry_pc executes. See
|
||||
// crates/xenia-kernel/src/phase_b_snapshot.rs.
|
||||
if kernel.phase_b_snapshot_dir.is_some() {
|
||||
let current_tid = kernel.scheduler.tid(hw_id).unwrap_or(0);
|
||||
xenia_kernel::phase_b_snapshot::fire_if_entry_thread(
|
||||
kernel,
|
||||
mem,
|
||||
pc,
|
||||
current_tid,
|
||||
);
|
||||
}
|
||||
|
||||
// 0) Diagnostic ctor-probe: if `pc` is in
|
||||
// `kernel.ctor_probe_pcs`, capture live r3/lr/sp + back-chain
|
||||
// and println one record. Read-only; lockstep digest unaffected.
|
||||
@@ -2267,19 +2771,41 @@ fn worker_prologue(
|
||||
cycle = stats.instruction_count,
|
||||
"HW thread returned to LR sentinel — marking exited"
|
||||
);
|
||||
// Phase C+15-α: schema-v1 `thread.exit` event on the implicit
|
||||
// (LR-sentinel) thread-exit path. Symmetric with
|
||||
// `ex_terminate_thread`; canary's `XThread::Execute` ends in
|
||||
// `Exit()` which emits the same event regardless of whether the
|
||||
// guest called `ExTerminateThread` or simply returned.
|
||||
if let (Some(t), true) = (tid, xenia_kernel::event_log::is_enabled()) {
|
||||
let cycle = kernel.scheduler.ctx(hw_id).timebase;
|
||||
xenia_kernel::event_log::emit_thread_exit(t, cycle, 0);
|
||||
}
|
||||
let (_, _exited_tid, handle_opt) = kernel.scheduler.exit_current(0);
|
||||
if let Some(h) = handle_opt
|
||||
&& let Some(xenia_kernel::objects::KernelObject::Thread {
|
||||
if let Some(h) = handle_opt {
|
||||
if let Some(xenia_kernel::objects::KernelObject::Thread {
|
||||
exit_code,
|
||||
waiters,
|
||||
..
|
||||
}) = kernel.objects.get_mut(&h)
|
||||
{
|
||||
*exit_code = Some(0);
|
||||
let to_wake: Vec<xenia_cpu::ThreadRef> = std::mem::take(waiters);
|
||||
for w in to_wake {
|
||||
kernel.scheduler.wake_ref(w);
|
||||
{
|
||||
*exit_code = Some(0);
|
||||
let to_wake: Vec<xenia_cpu::ThreadRef> = std::mem::take(waiters);
|
||||
for w in to_wake {
|
||||
kernel.scheduler.wake_ref(w);
|
||||
}
|
||||
}
|
||||
// Phase C+16: release the thread self-reference (paired with
|
||||
// the retain installed at spawn time by `ex_create_thread` /
|
||||
// `xam_task_schedule`). On the main thread (INITIAL_GUEST_TID)
|
||||
// no retain was installed by `install_initial_thread`, so the
|
||||
// refcount stays at 1 (creator-only). Pre-condition: a
|
||||
// self-retained thread has refcount ≥ 2 at this point; an
|
||||
// un-retained thread (main) has refcount = 1. We unconditionally
|
||||
// call `release_handle` — for main, this destroys it (which is
|
||||
// fine; main is exiting). For workers, this drops the
|
||||
// self-ref; if guest still holds a ref (no NtClose yet) the
|
||||
// object survives; if guest already closed, this destroys.
|
||||
kernel.release_handle(h);
|
||||
}
|
||||
return PrologueOutcome::Continue;
|
||||
}
|
||||
@@ -2329,10 +2855,32 @@ fn worker_prologue(
|
||||
|
||||
// 3) Unmapped PC.
|
||||
if !mem.is_mapped(pc) {
|
||||
// Crowbar v2 — enrich fault with tid/lr/r3 so we can attribute the
|
||||
// fault back to a specific guest thread. Read-only, no behaviour
|
||||
// change. The kernel lock is held by the caller per
|
||||
// run_execution's invariant; tid/ctx lookups are safe.
|
||||
let tid = kernel.scheduler.tid(hw_id);
|
||||
let r = kernel.scheduler.current_ref();
|
||||
let t = kernel.scheduler.thread(r);
|
||||
let lr = t.ctx.lr;
|
||||
let r3 = t.ctx.gpr[3];
|
||||
let r4 = t.ctx.gpr[4];
|
||||
let r29 = t.ctx.gpr[29];
|
||||
let r30 = t.ctx.gpr[30];
|
||||
let r31 = t.ctx.gpr[31];
|
||||
let ctr = t.ctx.ctr;
|
||||
tracing::error!(
|
||||
cycle = stats.instruction_count,
|
||||
pc = format_args!("{:#010x}", pc),
|
||||
hw_id,
|
||||
tid = ?tid,
|
||||
lr = format_args!("{:#010x}", lr),
|
||||
ctr = format_args!("{:#010x}", ctr),
|
||||
r3 = format_args!("{:#010x}", r3),
|
||||
r4 = format_args!("{:#010x}", r4),
|
||||
r29 = format_args!("{:#010x}", r29),
|
||||
r30 = format_args!("{:#010x}", r30),
|
||||
r31 = format_args!("{:#010x}", r31),
|
||||
"FAULT: PC in unmapped memory"
|
||||
);
|
||||
return PrologueOutcome::BreakOuter;
|
||||
@@ -2603,6 +3151,7 @@ fn run_execution(
|
||||
// without duplicating it from the lockstep path.
|
||||
match coord_pre_round(
|
||||
kernel,
|
||||
mem,
|
||||
&stats,
|
||||
max_instructions,
|
||||
ips_limit,
|
||||
@@ -3005,6 +3554,7 @@ fn run_execution_parallel(
|
||||
let s = stats_mtx.lock().expect("stats mutex poisoned");
|
||||
coord_pre_round(
|
||||
&mut *guard,
|
||||
mem,
|
||||
&*s,
|
||||
max_instructions,
|
||||
ips_limit,
|
||||
@@ -3907,6 +4457,131 @@ fn dump_thread_diagnostic(
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate 2.M — always-on structured exit-state dump (reading-error #42).
|
||||
///
|
||||
/// Phase-A's JSONL trace is blind to blocked-forever waits: a wait that
|
||||
/// never returns emits no `kernel.return` event, so a wedge looks identical
|
||||
/// to a clean termination. Iterate 2.J misclassified the wedge that way and
|
||||
/// cost iterate 2.K a re-dispatch to recover. This dumps a machine-readable
|
||||
/// snapshot of every alive thread + the handle/wedge map at exit time,
|
||||
/// regardless of `--quiet`, so every future iterate has the wedge in hand
|
||||
/// alongside the JSONL trace without needing a manual diagnostic re-run.
|
||||
///
|
||||
/// Output: `<phase-A-trace-dir>/exit-thread-state.json` when Phase-A is
|
||||
/// enabled; `./exit-thread-state.json` (CWD) otherwise. Filename is
|
||||
/// predictable — the harness can `glob('**/exit-thread-state.json')`.
|
||||
fn write_thread_state_dump(kernel: &xenia_kernel::KernelState) {
|
||||
use serde_json::{json, Value};
|
||||
use xenia_kernel::objects::KernelObject;
|
||||
let dump_path: std::path::PathBuf = xenia_kernel::event_log::output_path()
|
||||
.and_then(|p| p.parent().map(|d| d.join("exit-thread-state.json")))
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("exit-thread-state.json"));
|
||||
let tid_of = |r: &xenia_cpu::ThreadRef| -> u32 {
|
||||
kernel.scheduler.slots.get(r.hw_id as usize)
|
||||
.and_then(|s| s.runqueue.get(r.idx as usize)).map(|t| t.tid).unwrap_or(0)
|
||||
};
|
||||
// Returns (type_name, signaler_tid_if_known, full json payload).
|
||||
let handle_meta = |h: u32| -> (&'static str, Option<u32>, Value) {
|
||||
let waiters_v = |w: &Vec<xenia_cpu::ThreadRef>| -> Value {
|
||||
json!(w.iter().map(&tid_of).collect::<Vec<_>>())
|
||||
};
|
||||
match kernel.objects.get(&h) {
|
||||
Some(KernelObject::Event { signaled, manual_reset, waiters }) => ("Event", None,
|
||||
json!({"type":"Event","signaled":signaled,"manual_reset":manual_reset,"waiters_tid":waiters_v(waiters)})),
|
||||
Some(KernelObject::Semaphore { count, max, waiters }) => ("Semaphore", None,
|
||||
json!({"type":"Semaphore","count":count,"max":max,"waiters_tid":waiters_v(waiters)})),
|
||||
Some(KernelObject::Thread { id, exit_code, waiters, .. }) => ("Thread", Some(*id),
|
||||
json!({"type":"Thread","thread_id":id,"exited":exit_code.is_some(),"exit_code":exit_code,"signaler_tid_if_known":id,"waiters_tid":waiters_v(waiters)})),
|
||||
Some(KernelObject::Timer { signaled, deadline, waiters, .. }) => ("Timer", None,
|
||||
json!({"type":"Timer","signaled":signaled,"deadline":deadline,"waiters_tid":waiters_v(waiters)})),
|
||||
Some(KernelObject::Mutex { owner, recursion, waiters }) => ("Mutex", None,
|
||||
json!({"type":"Mutex","owner_hw":owner,"recursion":recursion,"waiters_tid":waiters_v(waiters)})),
|
||||
Some(KernelObject::NotifyListener { mask, queue, waiters, .. }) => ("NotifyListener", None,
|
||||
json!({"type":"NotifyListener","mask":format!("{:#x}",mask),"pending":queue.len(),"waiters_tid":waiters_v(waiters)})),
|
||||
Some(KernelObject::File { path, .. }) => ("File", None, json!({"type":"File","path":path})),
|
||||
None => ("unknown", None, json!({"type":"unknown_or_dropped"})),
|
||||
}
|
||||
};
|
||||
let mut alive: Vec<Value> = Vec::new();
|
||||
let mut wedge_map: Vec<Value> = Vec::new();
|
||||
for (hw_id, slot) in kernel.scheduler.slots.iter().enumerate() {
|
||||
for (idx, t) in slot.runqueue.iter().enumerate() {
|
||||
let (state_name, block_payload): (&'static str, Value) = match &t.state {
|
||||
xenia_cpu::HwState::Idle => ("Idle", Value::Null),
|
||||
xenia_cpu::HwState::Ready => ("Ready", Value::Null),
|
||||
xenia_cpu::HwState::Exited(code) => ("Exited", json!({"exit_code":code})),
|
||||
xenia_cpu::HwState::ServicingIrq(_) => ("ServicingIrq", Value::Null),
|
||||
xenia_cpu::HwState::Blocked(reason) => {
|
||||
let body = match reason {
|
||||
xenia_cpu::BlockReason::Suspended => json!({"kind":"Suspended"}),
|
||||
xenia_cpu::BlockReason::DelayUntil(d) => json!({"kind":"DelayUntil","deadline_ns":d}),
|
||||
xenia_cpu::BlockReason::CriticalSection(cs) =>
|
||||
json!({"kind":"CriticalSection","cs_ptr":format!("{:#010x}",cs)}),
|
||||
xenia_cpu::BlockReason::WaitAny { handles, deadline }
|
||||
| xenia_cpu::BlockReason::WaitAll { handles, deadline } => {
|
||||
let kind = if matches!(reason, xenia_cpu::BlockReason::WaitAny{..}) {"WaitAny"} else {"WaitAll"};
|
||||
let hs: Vec<Value> = handles.iter().map(|h| {
|
||||
let (ty, sig_tid, meta) = handle_meta(*h);
|
||||
// Wedge-map: surface every blocked-forever
|
||||
// wait (deadline==None) with a one-line
|
||||
// human summary + structured cross-ref so
|
||||
// future iterates can diff vs canary.
|
||||
if deadline.is_none() {
|
||||
let summary = match ty {
|
||||
"Thread" => format!("tid={} → Thread(id={})", t.tid, sig_tid.unwrap_or(0)),
|
||||
"Event" => format!("tid={} → Event(sig={})", t.tid,
|
||||
meta.get("signaled").and_then(|v|v.as_bool()).unwrap_or(false)),
|
||||
"Semaphore" => format!("tid={} → Semaphore({}/{})", t.tid,
|
||||
meta.get("count").and_then(|v|v.as_i64()).unwrap_or(0),
|
||||
meta.get("max").and_then(|v|v.as_i64()).unwrap_or(0)),
|
||||
_ => format!("tid={} → handle {:#010x} ({})", t.tid, h, ty),
|
||||
};
|
||||
wedge_map.push(json!({
|
||||
"waiter_tid": t.tid,
|
||||
"waiter_pc": format!("{:#010x}", t.ctx.pc),
|
||||
"handle": format!("{:#010x}", h),
|
||||
"handle_type": ty,
|
||||
"signaler_tid_if_known": sig_tid,
|
||||
"summary": summary,
|
||||
}));
|
||||
}
|
||||
json!({"handle":format!("{:#010x}",h),"object":meta})
|
||||
}).collect();
|
||||
json!({"kind":kind,"handles":hs,"deadline_ns_or_inf":deadline})
|
||||
}
|
||||
};
|
||||
("Blocked", body)
|
||||
}
|
||||
};
|
||||
alive.push(json!({
|
||||
"tid": t.tid, "hw_id": hw_id, "idx": idx,
|
||||
"pc": format!("{:#010x}", t.ctx.pc),
|
||||
"lr": format!("{:#010x}", t.ctx.lr as u32),
|
||||
"sp": format!("{:#010x}", t.ctx.gpr[1] as u32),
|
||||
"priority": t.priority,
|
||||
"affinity_mask": format!("{:#04x}", t.affinity_mask),
|
||||
"suspend_count": t.suspend_count,
|
||||
"state": state_name,
|
||||
"block_reason": block_payload,
|
||||
}));
|
||||
}
|
||||
}
|
||||
let dump = json!({
|
||||
"schema_version": 1, "produced_by": "ours", "reason": "exit_dump",
|
||||
"alive_threads": alive, "wedge_map": wedge_map,
|
||||
});
|
||||
match serde_json::to_string_pretty(&dump) {
|
||||
Ok(s) => match std::fs::write(&dump_path, s) {
|
||||
Ok(()) => eprintln!(
|
||||
"exit-thread-state: wrote {} thread(s), {} wedge entr(ies) to {}",
|
||||
alive.len(), wedge_map.len(), dump_path.display(),
|
||||
),
|
||||
Err(e) => eprintln!("exit-thread-state: failed to write {}: {e}", dump_path.display()),
|
||||
},
|
||||
Err(e) => eprintln!("exit-thread-state: failed to serialize: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(skip_all, fields(title))]
|
||||
fn run_with_ui(
|
||||
@@ -3974,6 +4649,8 @@ fn run_with_ui(
|
||||
|
||||
print_summary(kernel.scheduler.ctx(0), &debugger, &db_writer, quiet);
|
||||
dump_thread_diagnostic(&kernel, &mem, quiet);
|
||||
// Iterate 2.M — see cmd_exec_inner path for rationale.
|
||||
write_thread_state_dump(&kernel);
|
||||
info!(
|
||||
wall_ms = started.elapsed().as_millis() as u64,
|
||||
instructions = stats.instruction_count,
|
||||
|
||||
Reference in New Issue
Block a user