[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

@@ -1184,6 +1184,28 @@ impl Scheduler {
})
}
/// True if any thread is currently `Blocked` on a `WaitAny`/`WaitAll`
/// whose handle set contains `handle`. Used by the handle-slab recycler
/// (AUDIT-059 R34) to avoid an ABA hazard: if a closed handle's slot is
/// returned to the free list while a thread is still parked on it, a
/// later `alloc_handle` could hand the same slot to a NEW object, and a
/// signal on that new object would wake the stale waiter that was
/// waiting on the OLD (closed) object. Canary sidesteps this by keeping
/// the object alive via an object_ref while waiters hold references; we
/// instead simply decline to recycle a still-waited slot (leaking it,
/// matching the pre-R34 bump-only behaviour for that rare case).
pub fn any_thread_waiting_on(&self, handle: u32) -> bool {
self.slots.iter().any(|slot| {
slot.runqueue.iter().any(|t| match &t.state {
HwState::Blocked(BlockReason::WaitAny { handles, .. })
| HwState::Blocked(BlockReason::WaitAll { handles, .. }) => {
handles.contains(&handle)
}
_ => false,
})
})
}
/// Snapshot thread states for diagnostic logging. One entry per live
/// guest thread (Exited are included so post-mortem can see exit codes).
pub fn diagnostic_snapshot(&self) -> Vec<(ThreadRef, Option<u32>, HwState)> {

View File

@@ -293,28 +293,23 @@ pub fn store_vector_right(mem: &dyn MemoryAccess, ea: u32, v: Vec128) {
}
}
// ─── 5-6-5 pixel pack (vpkpx / vupkhpx / vupklpx) ─────────────────────────
// PPC vpkpx takes a 32-bit RGB lane and packs it into a 16-bit 1-5-5-5 pixel.
// vupkhpx / vupklpx reverse the operation.
//
// Format: input 32-bit word holds
// bits 0-6: unused (0)
// bit 7: alpha-select (→ bit 15 of output)
// bits 8-15: R (top 5 bits kept)
// bits 16-23: G (top 5 bits kept)
// bits 24-31: B (top 5 bits kept)
// Output 16-bit word:
// bit 15: A (from input bit 7)
// bits 10-14: R
// bits 5-9: G
// bits 0-4: B
// ─── pixel pack (vpkpx / vupkhpx / vupklpx) ───────────────────────────────
// PPC vpkpx packs each 32-bit lane into a 16-bit 1-5-5-5 pixel.
// Mapping transcribed EXACTLY from xenia-canary
// `ppc_emit_altivec.cc::vkpkx_in_low` (lines 1795-1808):
// tmp1 = (input >> 9) & 0xFC00 // out bits 15:10 = in bits 24:19
// tmp2 = (input >> 6) & 0x3E0 // out bits 9:5 = in bits 14:10
// tmp3 = (input >> 3) & 0x1F // out bits 4:0 = in bits 7:3
// result = tmp1 | tmp2 | tmp3
// This is a pure shift/mask: there is NO standalone alpha select. Output
// bit 15 is simply input bit 24 (the top of the 6-bit field masked by
// 0xFC00) — NOT input bit 7. The red field is 6 bits wide here.
#[inline] pub fn pack_pixel_555(input: u32) -> u16 {
let a = (input >> 7) & 0x1;
let r = (input >> 8) & 0xFF;
let g = (input >> 16) & 0xFF;
let b = (input >> 24) & 0xFF;
((a << 15) | ((r & 0xF8) << 7) | ((g & 0xF8) << 2) | ((b & 0xF8) >> 3)) as u16
let tmp1 = (input >> 9) & 0xFC00;
let tmp2 = (input >> 6) & 0x3E0;
let tmp3 = (input >> 3) & 0x1F;
(tmp1 | tmp2 | tmp3) as u16
}
#[inline] pub fn unpack_pixel_555(input: u16) -> u32 {
@@ -801,9 +796,38 @@ mod tests {
}
#[test]
fn pack_unpack_pixel_555() {
let encoded = pack_pixel_555(0x80_F8_F8_F8);
assert_eq!(encoded & 0x8000, 0x8000);
fn pack_pixel_555_matches_canary() {
// Mapping (canary ppc_emit_altivec.cc::vkpkx_in_low):
// out[15:10] = in[24:19], out[9:5] = in[14:10], out[4:0] = in[7:3]
// Pure shift/mask, NO standalone alpha bit.
// All three colour fields exercised. Expected (hand-computed):
// (0x018844C0 >> 9)&0xFC00 = 0xC400
// (0x018844C0 >> 6)&0x3E0 = 0x100
// (0x018844C0 >> 3)&0x1F = 0x18
// => 0xC518
assert_eq!(pack_pixel_555(0x01_88_44_C0), 0xC518);
// Boundary the audit flagged: low byte 0xF8 has bit 7 set. Canary does
// NOT turn that into output bit 15 (alpha). Output bit 15 = in bit 24,
// which is 0 here => high bit clear. (Old impl wrongly produced 0x8000.)
assert_eq!(pack_pixel_555(0x80_F8_F8_F8), 0x7FFF);
assert_eq!(pack_pixel_555(0x80_F8_F8_F8) & 0x8000, 0);
// Lone source bit 7 (0x80) lands in the blue field, not in bit 15.
assert_eq!(pack_pixel_555(0x00_00_00_80), 0x0010);
// Output bit 15 is sourced from input bit 24, not bit 7.
assert_eq!(pack_pixel_555(0x01_00_00_00), 0x8000);
// Saturated input -> all field bits set.
assert_eq!(pack_pixel_555(0xFF_FF_FF_FF), 0xFFFF);
}
#[test]
fn unpack_pixel_555_roundtrip() {
// vupkhpx/vupklpx are NOTIMPLEMENTED in canary, so unpack_pixel_555 is
// unchanged; just sanity-check the alpha-replicate path still holds.
let w = unpack_pixel_555(0x8000 | (0x1F << 10) | (0x1F << 5) | 0x1F);
assert_eq!(w & 0xFF000000, 0xFF000000);
}