[Subsystem-fixes] 6 verified ours-vs-canary divergence fixes

From the 2026-06-12 5-subsystem differential audit. All verified against
canary as oracle; 660/660 workspace tests green (655 + 5 new).

1. nt_create_event polarity (exports.rs) — `manual_reset = gpr[5] != 0`
   was INVERTED. Canary xboxkrnl_threading.cc:668 `Initialize(!event_type,..)`
   + xevent.cc:41 (type 0 = NotificationEvent = manual, type 1 = Sync = auto).
   Now `== 0`. Was the dormant 2.AI fix on chore/portable-snapshot, never
   merged. The Ke-path was already correct; only the Nt-path was wrong.

2. 2.AF deadline drain (main.rs coord_pre_round) — expired KeWait/KeDelay
   deadlines never fired under load because advance_to_next_wake_if_due was
   only called in coord_idle_advance (no-Ready-threads path). Added a
   per-round drain loop; covers BOTH lockstep and parallel outer loops since
   both call coord_pre_round. Was the dormant 2.AF fix, never merged.

3. handle slab-recycle ABA guard (state.rs + scheduler.rs) — release_handle_slot
   (my round-34 regression) recycled a closed slot even with a thread still
   parked on it, risking a stale-waiter wake when the slot is re-minted. Added
   Scheduler::any_thread_waiting_on; decline to recycle a still-waited slot.

4. vpkpx pixel-pack (vmx.rs) — wrong field mapping (~100% mismatch). Now
   exact canary ppc_emit_altivec.cc:1795 shift/mask (red 6b out[15:10] from
   w[24:19], green out[9:5] from w[14:10], blue out[4:0] from w[7:3]; no
   fabricated alpha bit). +unit test.

5. VFS GDFX attribute plumbing (vfs/*, exports.rs query fns) — VfsEntry now
   carries the real on-disc attribute byte (GDFX dirent +12, canary
   disc_image_device.cc:136/154) instead of inferring directory-ness from
   path shape. Query exports report the real FILE_ATTRIBUTE_* bits. Candidate
   driver of the XamShowDirtyDiscErrorUI gate. +tests.

6. MmGetPhysicalAddress region-aware mirror (exports.rs) — flat 0x1FFFFFFF
   mask missed canary's +0x1000 host_address_offset for 0xE0000000+ mirror
   (memory.cc:2317). Read-only query; proven byte-identical 50M digest. +test.

Investigated and intentionally NOT changed:
- zero-on-recommit: no-op; ours has no region-reuse path (bump allocators,
  free is a stub).
- 32-bit ALU writeback truncation (PPCBUG-020): documented-deliberate; premise
  (MSR.SF=0) is questionable but flipping it is out of scope here.
- KeSetEvent/NtSetEvent return value: ours returns true previous state
  (hardware-faithful); canary returns constant 1 — NOT an ours bug.

sylpheed_n50m golden will need re-baselining (legit behavior change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-12 14:57:38 +02:00
parent db90ad0f7d
commit b20c99f141
8 changed files with 319 additions and 44 deletions

View File

@@ -696,9 +696,36 @@ fn mm_create_kernel_stack(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut K
}
}
/// Region-aware guest-virtual → physical translation, matching canary's
/// `Memory::GetPhysicalAddress` + `PhysicalHeap::GetPhysicalAddress`
/// (`xenia-canary/src/xenia/memory.cc:528-545` and `:2317-2326`).
///
/// Canary `PhysicalHeap::GetPhysicalAddress`:
/// ```c
/// address -= heap_base_;
/// if (heap_base_ >= 0xE0000000) { address += 0x1000; }
/// return address;
/// ```
/// The three physical heap bases (0xA0000000 / 0xC0000000 / 0xE0000000) all
/// alias the same 512 MB physical window, so `address - heap_base ==
/// address & 0x1FFFFFFF` for each. The only region-specific delta is the
/// `+0x1000` host-address-offset for the 0xE0000000+ 4 KB mirror — see
/// `memory.h:368-372` (`host_address_offset` for `heap_base >= 0xE0000000`).
/// For non-physical / sub-0x1FFFFFFF virtual addresses canary returns the
/// address unchanged, which equals `address & 0x1FFFFFFF` there too.
pub(crate) fn translate_physical_address(virt: u32) -> u32 {
let phys = virt & 0x1FFF_FFFF;
if virt >= 0xE000_0000 {
phys + 0x1000
} else {
phys
}
}
fn mm_get_physical_address(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
// r3 = virtual address -> return physical address
ctx.gpr[3] &= 0x1FFF_FFFF; // Mask to 512MB physical
// r3 = virtual address -> return physical address.
// Region-aware, mirroring canary (see `translate_physical_address`).
ctx.gpr[3] = translate_physical_address(ctx.gpr[3] as u32) as u64;
}
fn mm_query_address_protect(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
@@ -1480,20 +1507,35 @@ fn nt_query_information_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mu
*size
};
// Root-of-device opens (`game:\`, `cache:\`, `partition0`) strip to
// an empty string post-prefix — see `open_vfs_file`'s synth path.
// Games query these as directories (DirectoryObject probe), and
// reporting `Directory=0` makes Sylpheed treat the open as "found a
// non-directory where I expected a directory" and call
// `XamShowDirtyDiscErrorUI`. Canary's `NtQueryInformationFile` pulls
// the real file-system entry's kind; we key on path shape since we
// don't model directory entries.
let is_directory = path.is_empty()
|| path.ends_with('/')
|| path.ends_with(':');
// Snapshot what we need from the handle, then drop the borrow so we can
// re-resolve the path against the VFS for its real attribute byte.
let path = path.clone();
let size = live_size;
let position = *position;
// Pull the REAL GDFX attribute byte (canary `disc_image_device.cc:154`)
// for disc-backed handles by re-resolving the stored path. Root-of-device
// opens (`game:\`, `cache:\`, `partition0`) strip to an empty string and
// synth-stub opens have no VFS entry — for those we fall back to the
// path-shape heuristic. Games query these as directories (DirectoryObject
// probe), and reporting `Directory=0` makes Sylpheed treat the open as
// "found a non-directory where I expected a directory" and call
// `XamShowDirtyDiscErrorUI`.
let vfs_attributes: Option<u32> = if path.is_empty() {
None
} else {
state
.vfs
.as_ref()
.and_then(|vfs| vfs.stat(&path).ok())
.map(|e| e.attributes)
.filter(|&a| a != 0)
};
let is_directory = match vfs_attributes {
Some(a) => (a & 0x10) != 0,
None => path.is_empty() || path.ends_with('/') || path.ends_with(':'),
};
// `FILE_ATTRIBUTE_DIRECTORY` (NT / Xbox) — advertised in
// `FileNetworkOpenInformation.FileAttributes`; Sylpheed's async-I/O
// worker queries with class=34 and the calling code checks this bit
@@ -1532,10 +1574,13 @@ fn nt_query_information_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mu
}
mem.write_u64(file_info + 32, size);
mem.write_u64(file_info + 40, size);
let attrs = if is_directory {
FILE_ATTRIBUTE_DIRECTORY
} else {
FILE_ATTRIBUTE_NORMAL
// Prefer the real GDFX attribute byte; fall back to the
// DIRECTORY/NORMAL split for root-of-device and synth-stub
// handles that have no VFS entry.
let attrs = match vfs_attributes {
Some(a) => a,
None if is_directory => FILE_ATTRIBUTE_DIRECTORY,
None => FILE_ATTRIBUTE_NORMAL,
};
mem.write_u32(file_info + 48, attrs);
mem.write_u32(file_info + 52, 0); // pad
@@ -1738,7 +1783,18 @@ fn nt_query_full_attributes_file(ctx: &mut PpcContext, mem: &GuestMemory, state:
mem.write_u32(out + 28, filetime as u32);
mem.write_u64(out + 32, entry.size);
mem.write_u64(out + 40, entry.size);
let attrs: u32 = if entry.is_directory { 0x10 } else { 0x80 };
// Use the REAL GDFX attribute byte forwarded by the VFS
// (canary `disc_image_device.cc:154`) instead of a
// path-shape guess. Disc rips never carry a 0-attribute
// entry, but guard anyway so a synthesised/legacy entry
// still advertises a sane DIRECTORY/NORMAL split.
let attrs: u32 = if entry.attributes != 0 {
entry.attributes
} else if entry.is_directory {
0x10
} else {
0x80
};
mem.write_u32(out + 48, attrs);
mem.write_u32(out + 52, 0);
}
@@ -1859,6 +1915,7 @@ fn nt_query_directory_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut
is_directory: e.is_directory,
size: e.size,
offset: e.offset,
attributes: e.attributes,
})
})
.collect(),
@@ -1909,7 +1966,12 @@ fn nt_query_directory_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut
mem.write_u64(base + 0x20, 0);
mem.write_u64(base + 0x28, entry.size);
mem.write_u64(base + 0x30, entry.size);
let attrs = if entry.is_directory {
// Real GDFX attribute byte (canary `disc_image_device.cc:154`);
// fall back to the directory/normal split only for legacy entries
// that carry no attribute bits.
let attrs = if entry.attributes != 0 {
entry.attributes
} else if entry.is_directory {
FILE_ATTRIBUTE_DIRECTORY
} else {
FILE_ATTRIBUTE_NORMAL
@@ -1985,9 +2047,21 @@ fn nt_close(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
}
fn nt_create_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
// r3 = handle_ptr, r4 = obj_attrs, r5 = event_type, r6 = initial_state
// r3 = handle_ptr, r4 = obj_attrs, r5 = event_type, r6 = initial_state.
//
// Xenon DISPATCHER_HEADER `Type` (NT convention):
// 0 = NotificationEvent (manual-reset)
// 1 = SynchronizationEvent (auto-reset)
// Canary: `xboxkrnl_threading.cc:668` `ev->Initialize(!event_type, !!initial_state)`
// with `XEvent::Initialize(bool manual_reset, ...)` (xevent.cc:25) and
// `InitializeNative` (xevent.cc:41 `case 0x00: manual_reset_ = true`).
// So `manual_reset = (event_type == 0)`. The Ke-path
// (`ensure_dispatcher_object`) was already correct; the Nt-path here was
// inverted, mis-classifying Sylpheed's per-frame VSync gate (type=1 auto +
// initial=1) as manual-reset+signaled → it stayed signaled forever and
// tid=1's main loop spun ~2800x canary's 60Hz.
let handle_ptr = ctx.gpr[3] as u32;
let manual_reset = ctx.gpr[5] != 0;
let manual_reset = ctx.gpr[5] == 0;
let signaled = ctx.gpr[6] != 0;
let handle = state.alloc_handle_for(KernelObject::Event {
manual_reset,
@@ -4823,12 +4897,14 @@ mod tests {
is_directory: false,
size: 0x1000,
offset: 0,
attributes: 0x81, // NORMAL | READONLY
},
xenia_vfs::VfsEntry {
name: "dat".into(),
is_directory: true,
size: 0,
offset: 0,
attributes: 0x11, // DIRECTORY | READONLY
},
// A grandchild — must NOT appear in root enumeration.
xenia_vfs::VfsEntry {
@@ -4836,6 +4912,7 @@ mod tests {
is_directory: false,
size: 0x2000,
offset: 0,
attributes: 0x81,
},
],
}));
@@ -4862,9 +4939,11 @@ mod tests {
// NextEntryOffset.
let mut cursor: u32 = 0;
let mut names: Vec<String> = Vec::new();
let mut attrs: Vec<u32> = Vec::new();
loop {
let entry_base = buf + cursor;
let name_len = mem.read_u32(entry_base + 0x3C) as usize;
attrs.push(mem.read_u32(entry_base + 0x38));
let mut bytes = Vec::with_capacity(name_len);
for i in 0..name_len as u32 {
bytes.push(mem.read_u8(entry_base + 0x40 + i));
@@ -4877,6 +4956,12 @@ mod tests {
cursor += next;
}
assert_eq!(names, vec!["default.xex", "dat"]);
// The real GDFX attribute byte must be forwarded verbatim: the file
// reports NORMAL|READONLY (no DIRECTORY bit), the directory reports
// DIRECTORY|READONLY.
assert_eq!(attrs, vec![0x81, 0x11]);
assert_eq!(attrs[0] & 0x10, 0, "file must not advertise DIRECTORY");
assert_ne!(attrs[1] & 0x10, 0, "dir must advertise DIRECTORY");
// A second call on the same handle must return NO_MORE_FILES —
// the cursor has advanced past the end.
ctx.gpr[3] = handle as u64;
@@ -6396,4 +6481,23 @@ mod tests {
assert!(resolved.ends_with("etc/foo"));
std::fs::remove_dir_all(&dir).ok();
}
/// `MmGetPhysicalAddress` must be region-aware, matching canary's
/// `PhysicalHeap::GetPhysicalAddress`: the 0xE0000000+ 4 KB mirror gets a
/// `+0x1000` host-address-offset; every other region is a flat
/// `& 0x1FFFFFFF` mask.
#[test]
fn mm_get_physical_address_region_aware() {
// 0xE0000000 mirror: canary `address - heap_base (==addr & 0x1FFFFFFF)`
// then `+ 0x1000`.
assert_eq!(translate_physical_address(0xE000_0000), 0x0000_1000);
assert_eq!(translate_physical_address(0xE000_5000), 0x0000_6000);
assert_eq!(translate_physical_address(0xFFFF_F000), 0x1FFF_F000 + 0x1000);
// 0xA0000000 / 0xC0000000 physical heaps: flat mask, no offset.
assert_eq!(translate_physical_address(0xA000_0000), 0x0000_0000);
assert_eq!(translate_physical_address(0xC012_3000), 0x0012_3000);
// Virtual / already-physical (< 0x20000000): unchanged.
assert_eq!(translate_physical_address(0x0012_3000), 0x0012_3000);
assert_eq!(translate_physical_address(0x4012_3000), 0x0012_3000);
}
}