Replaces the "Synthesized empty file" stub for cache:/* paths with a
real host-FS HostPathDevice-style mount. Each KernelState gets a fresh
per-process tmpdir under /tmp/xenia-rs-cache-<pid>-<id>/ which is
cleared on init for lockstep determinism (mirrors canary's
xenia_main.cc:649 RegisterSymbolicLink("cache:", "\\CACHE") +
HostPathDevice in xenia-canary/src/xenia/vfs/devices/host_path_device.cc).
NtCreateFile now honours create_disposition for cache: paths:
FILE_OPEN -> NOT_FOUND if missing
FILE_CREATE -> NAME_COLLISION if present
FILE_OPEN_IF -> open or create
FILE_OVERWRITE_IF -> create or truncate
FILE_OVERWRITE -> NOT_FOUND if missing, else truncate
FILE_SUPERSEDE -> create or truncate
NtReadFile / NtWriteFile / NtSetInformationFile (XFileEndOfFileInformation)
/ NtQueryInformationFile / NtQueryFullAttributesFile route through
std::fs against the per-handle host_path; non-cache paths keep their
legacy semantics (read-only disc image, synth-empty stubs).
Verified by audit-037 cascade:
- sub_82459D18 (cache-miss restore): 0 fires (was firing constantly)
- sub_8245D230 (resize/zero-fill): 0 fires (was firing constantly)
- 105+ real cache-file writes per 500M run; 4+ MB of game data persisting
to disk per boot; cache:/recent, cache:/access, cache:/d4ea*.tmp, etc.
- Lockstep deterministic at instructions=100000004 / imports=987485
across 3+ reruns (digest shifted as expected; goldens re-baselined).
- swaps=2 plateau still in place; cluster L1 unactivated. Cascade
dimension D (cluster activation) — UNKNOWN, no L1 fires.
Tests 640 -> 645 (+5 cache-specific unit tests; full workspace green).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
113 lines
4.4 KiB
Rust
113 lines
4.4 KiB
Rust
//! Kernel object tracking for HLE.
|
|
|
|
use std::collections::VecDeque;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use xenia_cpu::ThreadRef;
|
|
|
|
/// 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)]
|
|
pub enum KernelObject {
|
|
Event {
|
|
manual_reset: bool,
|
|
signaled: bool,
|
|
/// Guest threads parked on this event.
|
|
waiters: Vec<ThreadRef>,
|
|
},
|
|
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>,
|
|
/// AUDIT-038 — when `Some`, this file is backed by a real host-FS
|
|
/// path (the cache: persistent VFS) rather than the in-memory
|
|
/// `data` buffer. NtReadFile / NtWriteFile / NtSetInformationFile
|
|
/// route through `std::fs` against this path. Mirrors canary's
|
|
/// `HostPathDevice` (xenia-canary/src/xenia/vfs/devices/
|
|
/// host_path_device.cc) which symlinks `cache:` → `\CACHE`.
|
|
/// `None` for disc-VFS reads, root-of-device opens, and synth
|
|
/// stubs (those keep the in-memory zero-byte semantics).
|
|
host_path: Option<PathBuf>,
|
|
},
|
|
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>,
|
|
},
|
|
NotifyListener {
|
|
mask: u64,
|
|
max_version: u32,
|
|
queue: VecDeque<(u32, 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, .. }
|
|
| KernelObject::NotifyListener { waiters, .. } => Some(waiters),
|
|
KernelObject::File { .. } => None,
|
|
}
|
|
}
|
|
}
|