4 Commits

Author SHA1 Message Date
MechaCat02
034ec8b47f [iterate-2O] GPU: drain indirect buffers correctly — Sylpheed renders splash (draws 0→78)
Ours' GPU never drained the D3D driver's system command buffer past the first
11-dword indirect buffer, so DRAW_INDX / reg-0x57C-arm packets never executed
and draws stayed 0 (the long-hunted render gate; see UPDATE-18). Runtime tracing
(temporary, removed) showed the guest submits 6 INDIRECT_BUFFER packets at boot
(CP_RB_WPTR 22→37) but ours executed exactly ONE IB and then spun 15.7M packets
inside it. Three coupled command-processor bugs, all corrected to match canary:

1. `sync_with_mmio` applied the primary CP_RB_WPTR to whichever ring was active,
   including an executing indirect buffer — `37 % 11 = 3` clobbered the IB's
   write pointer so its read pointer looped 0→2→5→0 forever and never popped
   back to the primary ring. CP_RB_WPTR governs ONLY the primary ring; while an
   IB executes, the primary is the bottom of the IB stack. Canary executes each
   IB through a separate `RingBuffer reader_` (command_processor.cc), so the
   primary write pointer is structurally inapplicable to an IB.

2. Indirect buffers were treated as circular rings: read wrapped at `size_dwords`
   (`11 % 11 = 0`) and never reached the fixed write pointer, so even without the
   clobber the IB could not terminate. An IB is a fixed *linear* sub-stream; add
   `RingBufferView.indirect` and drain `[0, ib_size)` monotonically, then pop.

3. `is_ready` only checked the active ring, so an IB that now correctly exhausts
   would never get `execute_one` called again to pop back to the primary ring
   (whose WPTR may have advanced). Check the whole IB stack.

Also: the ring was sized `1 << size_log2` bytes (1024 dwords) vs canary's
`1 << (size_log2 + 3)` (8192 dwords) — an 8× undersize that desynced WPTR-wrap
math from the guest. Fixed in `GpuSystem::initialize_ring_buffer` (and the
dead bookkeeping copy in `vd_initialize_ring_buffer`).

Cascade (deterministic; threaded-default backend, byte-identical across runs):
reg 0x57C now written, IB jumps 1→12, packets 15.7M→9,825, and the splash
renders — draws 0→78, shaders 0→3, render_targets 0→2, swaps 2→3 — stable at
50M / 200M / 1B. Boot then reaches a new downstream gate (draws plateau at 78,
interrupts keep climbing → engine alive, not deadlocked).

golden `sylpheed_n50m.json` re-baselined (draws 78). `cargo test --workspace`
green (674; +2 ring_view regression tests). vd_swap's synthetic-swap
short-circuit is now redundant but left untouched (cascade works without
changing it); cleaning it up is a separate follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:06:16 +02:00
MechaCat02
93f60a3ba0 [iterate-2M] PCR+0x10C (PRCB.current_cpu): init per-HW-thread to unwedge spin-barrier
Ours never initialized the PRCB `current_cpu` byte at PCR+0x10C
(prcb_data@0x100 + current_cpu@0xC). Canary sets it from
`GetFakeCpuNumber(affinity)` (xthread.cc:847 `pcr->prcb_data.current_cpu =
cpu_index`), which equals the HW thread id ours already writes at PCR+0x2C.
Left unwritten it read 0 for every thread.

Guest spin-barrier `sub_824D1328` (used by the audio/update pump threads at
entries 0x824D2878 / 0x824D2940, ours tid 9 / tid 10) indexes a per-HW-thread
occupancy byte array via `lbz r11, 268(r13)` then `stbx ..., [array+index]`.
With index 0 for all threads, every thread marked slot 0; the multi-byte
rendezvous signature it then spins on (`ld [obj+0x164]` compared against the
packed per-slot expectation) could never assemble. Both pump threads busied at
pc 0x824d140c/0x824d1410 forever (Ready, 5M+ barrier iterations) and never ran
their `KeSetEvent` loops — so the events they signal (the 21k-per-thread
heartbeat in canary) never fired, starving the downstream worker handshake.

Fix: write `hw_id` to PCR+0x10C alongside PCR+0x2C in both the static thread
image init (thread.rs) and the dynamic PcrWriter (state.rs, used by scheduler
spawn + affinity migration) so the two stay in sync.

Runtime-verified BOTH engines. Post-fix the pump threads escape the barrier
(barrier iterations 5M+ -> 3) and advance into their loop bodies, now correctly
Blocked(WaitAny) at pc 0x824d28d0 / 0x824d29c0 (was spinning at 0x824d140c).
imports at n50M 339,766 -> 451,508; deterministic (two cold runs byte-identical).
draws still 0 (a later, separate render gate). golden re-baselined.
cargo test --workspace: 672 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:08:46 +02:00
MechaCat02
2bdb93e51e [iterate-2K] GPU physical-mirror aliasing: ring/IB/RPtr/resolve read wrong host region
Root cause (physical-mirror aliasing gap → GPU read wrong region → ring never
truly drained → render worker ring-space wait → no frame → no draw):

The Xbox 360 maps its 512 MB of physical DRAM into several virtual mirror
windows differing only in cache policy — bare physical (0x0xxxxxxx),
write-combine (0x4xxxxxxx), and cached 0xA/0xC/0xExxxxxxx — all aliasing
addr & 0x1FFF_FFFF. Ours has one flat membase and `heap_alloc`
(MmAllocatePhysicalMemoryEx) commits physical backing in the 0x4xxxxxxx
window. The guest masks its CP-ring allocation base to bare physical
(0x4adcc000 & 0x1FFFFFFF = 0x0adcc000) before handing it to
VdInitializeRingBuffer, and PM4 INDIRECT_BUFFER / writeback / resolve
pointers are likewise bare-physical. Ours stored those verbatim and read
`membase + 0x0adcc000`, a never-committed zero-filled page — so the GPU
drained ~718k zero PM4 headers, never executed the real Type3/DRAW stream,
and the RPtr writeback landed on a zero page the render worker (tid=8) polls,
freezing it forever.

Fix (GPU/Vd-boundary translation, not memory-layer): add
`physical_to_backing(addr)` deriving the committed backing exactly from
`heap_alloc`'s placement (0x4000_0000 | (addr & 0x1FFF_FFFF), idempotent for
the WC window, flat for non-physical code/stack). Apply it at every point the
GPU/kernel consumes a guest physical address: ring base
(initialize_ring_buffer), RPtr writeback (enable_rptr_writeback), PM4
INDIRECT_BUFFER pointer, WAIT_REG_MEM / COND_WRITE memory poll+write,
REG_TO_MEM / MEM_WRITE / EVENT_WRITE* / LOAD_ALU_CONSTANT / IM_LOAD addresses,
the resolve dest write, and the vd_swap frontbuffer present read. This was
chosen over memory-layer aliasing because the latter re-projects every CPU
load/store and corrupts the guest's flat 0xA/0xC/0xE accesses (it caused an
early PC=0xfffffffc fault).

Two adjacent GPU-backend gates this exposed and also fixed (canary-faithful):
- WaitCmp::from_wait_info was off by one vs canary's MatchValueAndRef
  selector (it decoded wait_info&7==3 as NotEqual instead of Equal),
  inverting the standard CP coherency wait so the GPU parked forever on the
  first INDIRECT_BUFFER. Remapped to 1=Less..7=Always, 0=Never.
- Added MakeCoherent: a WAIT polling COHER_STATUS_HOST clears the status bit
  (mirrors command_processor.cc:801-838) so the coherency handshake resolves.

Result: the GPU now decodes the real Type3 packets at 0x4adcc000 (ME_INIT,
INDIRECT_BUFFER → real Type0/WAIT_REG_MEM at 0x4adf5080) instead of
zero-headers; RPtr at 0x408619fc advances (0x13, 0x16, … written by the GPU
worker); the frame loop sub_822F1AA8 actively writes the controller at
0x40d09a40 (0x20→0x21→0x23); no fault, full 200M/1B budget runs clean.

draws_seen is still 0: the remaining gate is upstream and separate — the main
frame loop never sets controller bit-28 (frame-ready) at [0x40d09a40] (stalls
at 0x23, the known iterate-2C state-divergence gate), so the guest never
enqueues a render IB; the GPU only ever replays the init IB. This fix
correctly unblocks the GPU ring/IB/RPtr data path (gate-2 GPU backend); the
bit-28 frame-ready gate is the next target.

Stable golden (sylpheed_n50m) unchanged (draws/swaps/RTs/shaders identical at
50M); regenerated twice byte-identical. cargo test --workspace: 672 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:39:57 +02:00
MechaCat02
ed2e0e72fd [iterate-2J] KeTimeStampBundle deterministic tick: fix frozen+mislaid guest clock
The xboxkrnl data export KeTimeStampBundle (ordinal 0x00AD, import slot
0x820007d0 — confirmed via sylpheed.db imports table) was set up with TWO
defects in the import-patch pass:

  1. FROZEN: the block was written once at boot and never updated, so every
     field stayed a constant for the whole run (observed: the guest's clock
     reader sub_824AA830 = [[0x820007d0]+0x10] returned a constant
     0x01d6bc0c from 5M..150M instructions).
  2. WRONG LAYOUT: it stuffed the FILETIME high-dword at +0x10. The canonical
     X_TIME_STAMP_BUNDLE (xenia-canary kernel_state.h) is:
       +0x00 interrupt_time u64 (100ns since boot)
       +0x08 system_time    u64 (FILETIME 100ns since 1601)
       +0x10 tick_count      u32 (milliseconds since boot)
       +0x14 padding
     so [block+0x10] is tick_count in ms, not a FILETIME dword.

Fix (deterministic, no wall-clock):
  * Initialize the block with the correct field layout (tick_count = 0 at
    boot, system_time = FILETIME base, interrupt_time = 0).
  * Store the block VA on KernelState::timestamp_bundle_addr during the
    import patch.
  * Add KernelState::update_timestamp_bundle(mem, clock) and call it every
    round in BOTH the lockstep (run_execution) and parallel
    (run_execution_parallel) outer loops, right where the deterministic
    Scheduler::global_clock is advanced. The clock is the retired-instruction
    monotonic global_clock, so every guest-visible time value stays a pure
    function of guest progress (lockstep byte-reproducible).
  * Cadence: 1 global_clock unit = 100ns (coherent with parse_timeout, which
    divides 100ns timeouts by 100 onto the same basis), so
    INSTRUCTIONS_PER_MS = 10_000. tick_count now advances 0 -> ~4999ms over
    a 50M-instruction window. Also make KeQuerySystemTime read the same
    100ns clock instead of a frozen FILETIME constant.

Verification: tick_count at 0x40002010 now advances (deadline arm at
0x82450d0c stores clock+66 = 0x260,0x269,...,0x51d,... advancing, vs the
frozen 0x01d6bc4e before the fix). Determinism: two cold --stable-digest
runs are byte-identical; the n50m golden is UNCHANGED (the clock-affected
counter is not in the stable digest). 672/672 tests pass.

HONEST CAVEAT — the predicted render cascade did NOT materialize on this
branch. The diagnosed consuming gate at 0x82450b10 (the clock-vs-deadline
compare in the worker-hub channel loop sub_82450A68) is unreachable here:
the loop always branches away at 0x82450b0c ([this+220] >= channel-index),
so the hub already dispatches sub_82450B68 342x in BOTH the frozen and
fixed builds. Guest trajectory (imports 339766@50M / 1738001@200M /
9212446@1B), draws (0), swaps (2) and thread topology (tid14 Ready, not
blocked on 0x109c) are identical frozen-vs-fixed. This commit is therefore
a correct latent-clock-bug fix and determinism-safe prerequisite, NOT the
render unblock. The 0x109c/tid14 starvation premise was not reproduced at
f75bc96; the next gate must be re-localized.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 11:54:44 +02:00
9 changed files with 396 additions and 58 deletions

View File

@@ -1497,16 +1497,28 @@ fn cmd_exec_inner(
mem.write_u32(addr, block); mem.write_u32(addr, block);
} }
("xboxkrnl.exe", 0x00AD) => { ("xboxkrnl.exe", 0x00AD) => {
// KeTimeStampBundle — 0x18 block with FILETIME at +0 and // KeTimeStampBundle — X_TIME_STAMP_BUNDLE (canary layout,
// interrupt-time u64 at +0x10. Mirrors the clock used by // kernel_state.h): +0x00 interrupt_time u64, +0x08
// KeQuerySystemTime so fast-path readers see consistent values. // system_time u64 (FILETIME 100ns), +0x10 tick_count u32
// (milliseconds since boot), +0x14 padding. The guest's
// worker-hub channel-dispatch loop (sub_82450A68 @
// 0x82450b10) polls [block+0x10] (tick_count) and gates
// dispatch on a `tick_count + 66` (ms) deadline. The block
// MUST be ticked over the run or that deadline never
// elapses (tid14 0x109c starvation gate). Initialize to a
// zero-uptime base; KernelState::update_timestamp_bundle
// ticks it every round from the deterministic global_clock.
let block = alloc_zero(0x18, &mut mem, &mut kernel); let block = alloc_zero(0x18, &mut mem, &mut kernel);
if block != 0 { if block != 0 {
let fake_time: u64 = 132_500_000_000_000_000; // ~2021 FILETIME // FILETIME base (~2021) so system_time is plausible.
mem.write_u32(block, (fake_time >> 32) as u32); let fake_time: u64 = 132_500_000_000_000_000;
mem.write_u32(block + 4, fake_time as u32); mem.write_u32(block, 0); // interrupt_time hi
mem.write_u32(block + 0x10, (fake_time >> 32) as u32); mem.write_u32(block + 4, 0); // interrupt_time lo
mem.write_u32(block + 0x14, fake_time as u32); mem.write_u32(block + 0x08, (fake_time >> 32) as u32); // system_time hi
mem.write_u32(block + 0x0C, fake_time as u32); // system_time lo
mem.write_u32(block + 0x10, 0); // tick_count (ms) = 0 at boot
mem.write_u32(block + 0x14, 0); // padding
kernel.timestamp_bundle_addr = block;
} }
mem.write_u32(addr, block); mem.write_u32(addr, block);
} }
@@ -2852,6 +2864,12 @@ fn run_execution(
kernel kernel
.scheduler .scheduler
.advance_global_clock_to(stats.instruction_count); .advance_global_clock_to(stats.instruction_count);
// ITERATE-2J — tick the KeTimeStampBundle (ordinal 0x00AD) from the
// same deterministic clock so the guest's worker-hub tick_count
// deadline gate (`[block+0x10] + 66` ms) actually elapses. Without
// this the block is frozen at boot and the hub spins forever,
// starving tid14 on event 0x109c.
kernel.update_timestamp_bundle(mem, kernel.scheduler.global_clock());
kernel.fire_due_silph_autosignals(stats.instruction_count); kernel.fire_due_silph_autosignals(stats.instruction_count);
dispatch_graphics_interrupts( dispatch_graphics_interrupts(
kernel, kernel,
@@ -3296,6 +3314,16 @@ fn run_execution_parallel(
guard.fire_due_silph_autosignals(s.instruction_count); guard.fire_due_silph_autosignals(s.instruction_count);
} }
// ITERATE-2J — tick the KeTimeStampBundle (ordinal 0x00AD) from
// the parallel-mode coherent global_clock (summed per-block
// retired instructions). Same fix as the lockstep loop: keeps the
// guest's worker-hub tick_count deadline gate advancing so it
// dispatches channel-3 and unblocks tid14 on event 0x109c.
{
let clock = guard.scheduler.global_clock();
guard.update_timestamp_bundle(mem, clock);
}
// Iterate-2.BE — host-driven synchronous ISR dispatch. // Iterate-2.BE — host-driven synchronous ISR dispatch.
// Runs under the kernel lock while workers are still parked // Runs under the kernel lock while workers are still parked
// at the phaser B2 barrier (the coordinator hasn't published // at the phaser B2 barrier (the coordinator hasn't published

View File

@@ -1,10 +1,10 @@
{ {
"instructions": 50000000, "instructions": 50000001,
"imports": 339766, "imports": 451499,
"unimpl": 0, "unimpl": 0,
"draws": 0, "draws": 78,
"swaps": 2, "swaps": 3,
"unique_render_targets": 0, "unique_render_targets": 2,
"shader_blobs_live": 0, "shader_blobs_live": 3,
"texture_cache_entries": 0 "texture_cache_entries": 0
} }

View File

@@ -28,6 +28,56 @@ use crate::primitive::{self, ProcessedPrimitive};
use crate::register_file::RegisterFile; use crate::register_file::RegisterFile;
use crate::ring_view::RingBufferView; use crate::ring_view::RingBufferView;
/// The guest-virtual window that physical allocations are committed into.
/// `xenia-kernel`'s `heap_alloc` bumps its cursor through `0x4000_0000..=
/// 0x6FFF_FFFF` and commits the host backing for `MmAllocatePhysicalMemoryEx`
/// there, so this write-combine mirror is the canonical home of physical DRAM.
/// Keep in sync with `KernelState::heap_cursor`'s initial value.
pub const PHYSICAL_BACKING_BASE: u32 = 0x4000_0000;
/// Re-project a guest *physical* address — as handed to the Vd/GPU ABI and
/// embedded in PM4 pointers (`INDIRECT_BUFFER`, `WAIT_REG_MEM`-memory,
/// `MEM_WRITE`, `EVENT_WRITE*`, `IM_LOAD`, …) — onto the guest-virtual window
/// where its host backing is actually committed.
///
/// The Xbox 360 maps its 512 MB of physical DRAM into several virtual mirror
/// windows that differ only in cache policy: bare physical (`0x0xxxxxxx`),
/// write-combine (`0x4xxxxxxx`), and the cached `0xA/0xC/0xExxxxxxx` mirrors —
/// all aliasing `addr & 0x1FFF_FFFF`. On real hardware (and in xenia-canary
/// via overlapping `mmap`s) these are literally the same bytes.
///
/// Ours has a single flat `membase` and `MmAllocatePhysicalMemoryEx` commits
/// physical backing in the write-combine `0x4xxxxxxx` window. The guest then
/// masks its allocation base to *bare physical* before passing it to
/// `VdInitializeRingBuffer` / `VdEnableRingBufferRPtrWriteBack`, and PM4
/// pointers are likewise bare-physical. A flat `membase + phys` access
/// therefore hits a never-committed, zero-filled page instead of the committed
/// `0x4xxxxxxx` backing — so the GPU decoded zero PM4 headers and never ran
/// the real command stream.
///
/// Projecting any physical-mirror address back onto the `0x4xxxxxxx` window
/// lands on the page `heap_alloc` actually backed, regardless of which mirror
/// the guest used (idempotent for `0x4xxxxxxx` itself). The projection is
/// derived from `heap_alloc`'s placement, not a guess — if that window ever
/// moves, `PHYSICAL_BACKING_BASE` must move with it.
///
/// This is deliberately applied only at the GPU/Vd boundary (where addresses
/// arrive in their bare-physical form), NOT on the CPU's flat load/store path:
/// the guest CPU already accesses its allocations through the `0x4xxxxxxx`
/// base, and non-physical guest-virtual addresses (image `0x82xxxxxx`, stacks
/// `0x7xxxxxxx`) must stay flat.
#[inline]
pub fn physical_to_backing(addr: u32) -> u32 {
match addr {
0x0000_0000..=0x1FFF_FFFF
| 0x4000_0000..=0x4FFF_FFFF
| 0xA000_0000..=0xBFFF_FFFF
| 0xC000_0000..=0xDFFF_FFFF
| 0xE000_0000..=0xFFFF_FFFF => PHYSICAL_BACKING_BASE | (addr & 0x1FFF_FFFF),
_ => addr,
}
}
/// Cached Xenos microcode blob, produced by `PM4_IM_LOAD*` packets. /// Cached Xenos microcode blob, produced by `PM4_IM_LOAD*` packets.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ShaderBlob { pub struct ShaderBlob {
@@ -58,21 +108,37 @@ pub enum WaitCmp {
GreaterEq, GreaterEq,
/// value > ref /// value > ref
Greater, Greater,
/// Always — caller wants to sleep regardless. /// Always — caller wants to sleep regardless (selector bit 7).
Always, Always,
/// Never matches — `wait_info & 7 == 0` selects bit 0 of canary's
/// selector word, which is always zero.
Never,
} }
impl WaitCmp { impl WaitCmp {
/// Interpret the lower 3 bits of `wait_info` per canary's `MatchValueAndRef`. /// Interpret the lower 3 bits of `wait_info` per canary's `MatchValueAndRef`
/// (`pm4_command_processor_implement.h:685-696`). Canary forms a selector
/// `((value<ref)<<1) | ((value<=ref)<<2) | ((value==ref)<<3) |
/// ((value!=ref)<<4) | ((value>=ref)<<5) | ((value>ref)<<6) | (1<<7)` and
/// evaluates `(selector >> (wait_info & 7)) & 1`. So the index is the bit
/// position: 1=Less, 2=LessEq, 3=Equal, 4=NotEqual, 5=GreaterEq,
/// 6=Greater, 7=always-true, 0=never (bit 0 is always clear).
///
/// GPUBUG: the prior mapping was off by one (it started at `0 => Less`),
/// so `wait_info & 7 == 3` decoded as `NotEqual` instead of `Equal`. That
/// inverted the standard CP coherency wait
/// (`WAIT_REG_MEM COHER_STATUS_HOST, Equal 0`): the GPU parked forever on
/// the first INDIRECT_BUFFER and never reached any draw.
pub fn from_wait_info(wait_info: u32) -> Self { pub fn from_wait_info(wait_info: u32) -> Self {
match wait_info & 0x7 { match wait_info & 0x7 {
0 => WaitCmp::Less, 1 => WaitCmp::Less,
1 => WaitCmp::LessEq, 2 => WaitCmp::LessEq,
2 => WaitCmp::Equal, 3 => WaitCmp::Equal,
3 => WaitCmp::NotEqual, 4 => WaitCmp::NotEqual,
4 => WaitCmp::GreaterEq, 5 => WaitCmp::GreaterEq,
5 => WaitCmp::Greater, 6 => WaitCmp::Greater,
_ => WaitCmp::Always, 7 => WaitCmp::Always,
_ => WaitCmp::Never,
} }
} }
@@ -85,6 +151,7 @@ impl WaitCmp {
WaitCmp::GreaterEq => value >= reference, WaitCmp::GreaterEq => value >= reference,
WaitCmp::Greater => value > reference, WaitCmp::Greater => value > reference,
WaitCmp::Always => true, WaitCmp::Always => true,
WaitCmp::Never => false,
} }
} }
} }
@@ -536,14 +603,21 @@ impl GpuSystem {
/// Release. /// Release.
pub fn sync_with_mmio(&mut self) { pub fn sync_with_mmio(&mut self) {
let wptr_dwords = self.mmio.cp_rb_wptr.load(Ordering::Acquire); let wptr_dwords = self.mmio.cp_rb_wptr.load(Ordering::Acquire);
if wptr_dwords != self.ring.write_offset_dwords && self.ring.size_dwords != 0 { // CP_RB_WPTR governs ONLY the primary ring. While an indirect buffer
self.ring.write_offset_dwords = wptr_dwords % self.ring.size_dwords; // is executing, the active `self.ring` is a fixed linear sub-stream
// and the primary ring is saved at the bottom of the IB stack —
// applying the (primary) write pointer to the IB would corrupt its
// extent (e.g. `wptr % ib_size`) and strand the GPU mid-buffer.
let primary = self.ib_stack.first_mut().unwrap_or(&mut self.ring);
if wptr_dwords != primary.write_offset_dwords && primary.size_dwords != 0 {
primary.write_offset_dwords = wptr_dwords % primary.size_dwords;
} }
// Mirror our read pointer (Release pairs with any guest-side let primary_rptr = primary.read_offset_dwords;
// Mirror the *primary* read pointer (Release pairs with any guest-side
// Acquire-load of CP_RB_RPTR for ring writeback bookkeeping). // Acquire-load of CP_RB_RPTR for ring writeback bookkeeping).
self.mmio self.mmio
.cp_rb_rptr .cp_rb_rptr
.store(self.ring.read_offset_dwords, Ordering::Release); .store(primary_rptr, Ordering::Release);
} }
/// True iff `execute_one` is expected to make progress without blocking. /// True iff `execute_one` is expected to make progress without blocking.
@@ -551,7 +625,11 @@ impl GpuSystem {
if let Some(block) = &self.pending_block { if let Some(block) = &self.pending_block {
return block.is_satisfied(mem, &self.register_file); return block.is_satisfied(mem, &self.register_file);
} }
self.ring.has_pending() // Pending work may be in the active ring OR in a saved caller ring
// further down the IB stack (an exhausted IB still needs `execute_one`
// to pop back and resume the primary ring, whose WPTR may have since
// advanced).
self.ring.has_pending() || self.ib_stack.iter().any(|r| r.has_pending())
} }
/// Execute exactly one PM4 packet. Returns [`ExecOutcome::Idle`] when /// Execute exactly one PM4 packet. Returns [`ExecOutcome::Idle`] when
@@ -561,6 +639,12 @@ impl GpuSystem {
pub fn execute_one(&mut self, mem: &dyn MemoryAccess) -> ExecOutcome { pub fn execute_one(&mut self, mem: &dyn MemoryAccess) -> ExecOutcome {
// 0) If currently parked, probe the condition and either wake up or stay blocked. // 0) If currently parked, probe the condition and either wake up or stay blocked.
if let Some(block) = self.pending_block.clone() { if let Some(block) = self.pending_block.clone() {
// Re-service the CP coherency handshake on each probe so a
// COHER_STATUS_HOST wait can clear (canary does this in its WAIT
// loop body, not just at entry).
if let GpuBlock::WaitRegMem { poll_addr, is_memory: false, .. } = &block {
self.make_coherent(*poll_addr);
}
if block.is_satisfied(mem, &self.register_file) { if block.is_satisfied(mem, &self.register_file) {
tracing::debug!(?block, "gpu: wait satisfied — resuming"); tracing::debug!(?block, "gpu: wait satisfied — resuming");
self.pending_block = None; self.pending_block = None;
@@ -657,9 +741,21 @@ impl GpuSystem {
/// Called by `VdInitializeRingBuffer` to give us the primary ring. /// Called by `VdInitializeRingBuffer` to give us the primary ring.
pub fn initialize_ring_buffer(&mut self, base: u32, size_log2: u32) { pub fn initialize_ring_buffer(&mut self, base: u32, size_log2: u32) {
let size_bytes = 1u32 << size_log2.min(31); // Canary `CommandProcessor::InitializeRingBuffer` (command_processor.cc:
// 436): `primary_buffer_size_ = 1 << (size_log2 + 3)` *bytes*. The
// `VdInitializeRingBuffer` `r4` argument is log2(size-in-quadwords),
// so the byte size is `1 << (size_log2 + 3)` (× 8 bytes/quadword), i.e.
// `1 << (size_log2 + 1)` dwords. (Sylpheed passes size_log2=12 →
// 32768 bytes / 8192 dwords; the previous `1 << size_log2` undersized
// the ring 8× and desynced WPTR wrap math from the guest.)
let size_bytes = 1u32 << size_log2.saturating_add(3).min(31);
// The guest hands us a bare *physical* ring base; project it onto the
// committed backing window so ring reads hit real PM4 packets (see
// `physical_to_backing`).
let base = physical_to_backing(base);
self.ring.base = base; self.ring.base = base;
self.ring.size_dwords = size_bytes / 4; self.ring.size_dwords = size_bytes / 4;
self.ring.indirect = false;
self.ring.read_offset_dwords = 0; self.ring.read_offset_dwords = 0;
// `write_offset` is driven by the guest — start at 0 so the ring // `write_offset` is driven by the guest — start at 0 so the ring
// appears empty until MMIO writes advance it. // appears empty until MMIO writes advance it.
@@ -675,6 +771,10 @@ impl GpuSystem {
/// Called by `VdEnableRingBufferRPtrWriteBack` to record where the guest /// Called by `VdEnableRingBufferRPtrWriteBack` to record where the guest
/// expects us to mirror `read_offset_dwords`. /// expects us to mirror `read_offset_dwords`.
pub fn enable_rptr_writeback(&mut self, addr: u32, block_log2: u32) { pub fn enable_rptr_writeback(&mut self, addr: u32, block_log2: u32) {
// The guest registers a bare *physical* writeback address and polls
// the same allocation through its `0x4xxxxxxx` base; project so our
// RPtr store lands on the page the guest actually reads.
let addr = physical_to_backing(addr);
self.ring.rptr_writeback_addr = addr; self.ring.rptr_writeback_addr = addr;
self.ring.rptr_writeback_block_dwords = 1u32 << block_log2.min(31); self.ring.rptr_writeback_block_dwords = 1u32 << block_log2.min(31);
tracing::info!( tracing::info!(
@@ -724,6 +824,26 @@ impl GpuSystem {
/// upstream packet effects (memory writes, register file updates /// upstream packet effects (memory writes, register file updates
/// the guest reads via subsequent MMIO) happen-before the /// the guest reads via subsequent MMIO) happen-before the
/// CPU-visible RPTR bump. /// CPU-visible RPTR bump.
/// Service a CP coherency request, mirroring canary's
/// `CommandProcessor::MakeCoherent` (`command_processor.cc:801-838`).
///
/// The guest requests a vertex/texture-cache flush by writing
/// `COHER_STATUS_HOST` with its status bit (bit 31) set, then spins on a
/// `WAIT_REG_MEM COHER_STATUS_HOST, Equal 0`. We have no host cache to
/// flush (memory is shared, coherency is implicit), so completing the
/// request is simply clearing the register — which lets the wait satisfy.
/// No-op unless `poll_addr` is `COHER_STATUS_HOST` and its status bit is
/// set, so it is safe to call on every coherency-register WAIT probe.
fn make_coherent(&mut self, poll_addr: u32) {
if poll_addr != reg::COHER_STATUS_HOST {
return;
}
let status = self.register_file.read(reg::COHER_STATUS_HOST);
if status & 0x8000_0000 != 0 {
self.register_file.write(reg::COHER_STATUS_HOST, 0);
}
}
fn writeback_read_ptr(&mut self, mem: &dyn MemoryAccess) { fn writeback_read_ptr(&mut self, mem: &dyn MemoryAccess) {
if self.ring.rptr_writeback_addr != 0 && self.ring.is_initialized() { if self.ring.rptr_writeback_addr != 0 && self.ring.is_initialized() {
mem.write_u32_fence( mem.write_u32_fence(
@@ -816,7 +936,9 @@ impl GpuSystem {
} }
pm4::PM4_INDIRECT_BUFFER | pm4::PM4_INDIRECT_BUFFER_PFD => { pm4::PM4_INDIRECT_BUFFER | pm4::PM4_INDIRECT_BUFFER_PFD => {
self.stats.indirect_buffer_jumps += 1; self.stats.indirect_buffer_jumps += 1;
let ib_ptr = self.read_payload(mem, 1); // The IB pointer is a guest *physical* address — project it
// onto the committed backing window (see `physical_to_backing`).
let ib_ptr = physical_to_backing(self.read_payload(mem, 1));
let ib_size = self.read_payload(mem, 2); let ib_size = self.read_payload(mem, 2);
// Advance past the IB header + payload before recursing so // Advance past the IB header + payload before recursing so
// the return location is correct. // the return location is correct.
@@ -832,6 +954,10 @@ impl GpuSystem {
write_offset_dwords: ib_size, // IB is fully-written at jump time write_offset_dwords: ib_size, // IB is fully-written at jump time
rptr_writeback_addr: 0, rptr_writeback_addr: 0,
rptr_writeback_block_dwords: 0, rptr_writeback_block_dwords: 0,
// Linear sub-stream: drain [0, ib_size) then pop. Never
// wraps, and `sync_with_mmio`'s CP_RB_WPTR must not touch
// it (canary executes IBs through a separate reader).
indirect: true,
}; };
tracing::debug!( tracing::debug!(
ib_ptr = format_args!("{ib_ptr:#010x}"), ib_ptr = format_args!("{ib_ptr:#010x}"),
@@ -854,7 +980,8 @@ impl GpuSystem {
let is_memory = (wait_info & 0x10) != 0; let is_memory = (wait_info & 0x10) != 0;
let cmp = WaitCmp::from_wait_info(wait_info); let cmp = WaitCmp::from_wait_info(wait_info);
let poll_addr = if is_memory { let poll_addr = if is_memory {
poll_addr_raw & !3 // Physical memory poll address → committed backing.
physical_to_backing(poll_addr_raw & !3)
} else { } else {
poll_addr_raw poll_addr_raw
}; };
@@ -865,6 +992,12 @@ impl GpuSystem {
mask, mask,
cmp, cmp,
}; };
// A WAIT polling COHER_STATUS_HOST is the CP coherency
// handshake: service it now so the status bit clears (see
// `make_coherent`), exactly as canary does in its WAIT loop.
if !is_memory {
self.make_coherent(poll_addr);
}
if block.is_satisfied(mem, &self.register_file) { if block.is_satisfied(mem, &self.register_file) {
// Condition already true; proceed past this packet. // Condition already true; proceed past this packet.
tracing::trace!(?block, "gpu: WAIT_REG_MEM immediately satisfied"); tracing::trace!(?block, "gpu: WAIT_REG_MEM immediately satisfied");
@@ -908,7 +1041,7 @@ impl GpuSystem {
pm4::PM4_REG_TO_MEM => { pm4::PM4_REG_TO_MEM => {
// payload[0] = reg_index, payload[1] = mem addr // payload[0] = reg_index, payload[1] = mem addr
let reg_index = self.read_payload(mem, 1) & 0x1FFF; let reg_index = self.read_payload(mem, 1) & 0x1FFF;
let dst = self.read_payload(mem, 2) & !3; let dst = physical_to_backing(self.read_payload(mem, 2) & !3);
let value = self.register_file.read(reg_index); let value = self.register_file.read(reg_index);
mem.write_u32(dst, value); mem.write_u32(dst, value);
tracing::trace!( tracing::trace!(
@@ -920,7 +1053,7 @@ impl GpuSystem {
} }
pm4::PM4_MEM_WRITE => { pm4::PM4_MEM_WRITE => {
// payload[0] = dst, payload[1..=count-1] = values // payload[0] = dst, payload[1..=count-1] = values
let mut dst = self.read_payload(mem, 1) & !3; let mut dst = physical_to_backing(self.read_payload(mem, 1) & !3);
for i in 2..=count { for i in 2..=count {
let val = self.read_payload(mem, i); let val = self.read_payload(mem, i);
mem.write_u32(dst, val); mem.write_u32(dst, val);
@@ -936,7 +1069,7 @@ impl GpuSystem {
let mask = self.read_payload(mem, 4); let mask = self.read_payload(mem, 4);
let is_memory = (wait_info & 0x10) != 0; let is_memory = (wait_info & 0x10) != 0;
let cmp = WaitCmp::from_wait_info(wait_info); let cmp = WaitCmp::from_wait_info(wait_info);
let poll_addr = if is_memory { poll_raw & !3 } else { poll_raw }; let poll_addr = if is_memory { physical_to_backing(poll_raw & !3) } else { poll_raw };
let cur_raw = if is_memory { let cur_raw = if is_memory {
mem.read_u32(poll_addr) mem.read_u32(poll_addr)
} else { } else {
@@ -946,7 +1079,7 @@ impl GpuSystem {
let write_addr = self.read_payload(mem, 5); let write_addr = self.read_payload(mem, 5);
let write_data = self.read_payload(mem, 6); let write_data = self.read_payload(mem, 6);
if (wait_info & 0x100) != 0 { if (wait_info & 0x100) != 0 {
mem.write_u32(write_addr & !3, write_data); mem.write_u32(physical_to_backing(write_addr & !3), write_data);
} else { } else {
self.register_file self.register_file
.write(write_addr & 0x1FFF, write_data); .write(write_addr & 0x1FFF, write_data);
@@ -965,7 +1098,7 @@ impl GpuSystem {
// payload[0] = initiator (bit 31: write counter, else write `value`) // payload[0] = initiator (bit 31: write counter, else write `value`)
// payload[1] = address, payload[2] = value // payload[1] = address, payload[2] = value
let initiator = self.read_payload(mem, 1); let initiator = self.read_payload(mem, 1);
let address = self.read_payload(mem, 2); let address = physical_to_backing(self.read_payload(mem, 2));
let value = self.read_payload(mem, 3); let value = self.read_payload(mem, 3);
self.register_file self.register_file
.write(reg::VGT_EVENT_INITIATOR, initiator & 0x3F); .write(reg::VGT_EVENT_INITIATOR, initiator & 0x3F);
@@ -993,7 +1126,7 @@ impl GpuSystem {
// payload[0] = initiator, [1] = address. Writes 6 u16 extents // payload[0] = initiator, [1] = address. Writes 6 u16 extents
// (min/max x/y/z) — we're not tracking scissors yet, so write zeros. // (min/max x/y/z) — we're not tracking scissors yet, so write zeros.
let initiator = self.read_payload(mem, 1); let initiator = self.read_payload(mem, 1);
let address = self.read_payload(mem, 2) & !3; let address = physical_to_backing(self.read_payload(mem, 2) & !3);
self.register_file self.register_file
.write(reg::VGT_EVENT_INITIATOR, initiator & 0x3F); .write(reg::VGT_EVENT_INITIATOR, initiator & 0x3F);
self.handle_event_initiator(initiator & 0x3F, mem); self.handle_event_initiator(initiator & 0x3F, mem);
@@ -1123,7 +1256,7 @@ impl GpuSystem {
} }
pm4::PM4_LOAD_ALU_CONSTANT => { pm4::PM4_LOAD_ALU_CONSTANT => {
// payload[0] = source mem addr, [1] = offset_type, [2] = size_dwords // payload[0] = source mem addr, [1] = offset_type, [2] = size_dwords
let src = self.read_payload(mem, 1) & !3; let src = physical_to_backing(self.read_payload(mem, 1) & !3);
let offset_type = self.read_payload(mem, 2); let offset_type = self.read_payload(mem, 2);
let size_dwords = self.read_payload(mem, 3); let size_dwords = self.read_payload(mem, 3);
let index = offset_type & 0x7FF; let index = offset_type & 0x7FF;
@@ -1155,7 +1288,7 @@ impl GpuSystem {
} }
v v
} else { } else {
let addr = self.read_payload(mem, 1) & !3; let addr = physical_to_backing(self.read_payload(mem, 1) & !3);
let mut v = Vec::with_capacity(size_dwords as usize); let mut v = Vec::with_capacity(size_dwords as usize);
for i in 0..size_dwords { for i in 0..size_dwords {
v.push(mem.read_u32(addr + i * 4)); v.push(mem.read_u32(addr + i * 4));
@@ -1477,8 +1610,9 @@ mod tests {
// header // header
let hdr = (3u32 << 30) | ((5u32 - 1) << 16) | ((pm4::PM4_WAIT_REG_MEM as u32) << 8); let hdr = (3u32 << 30) | ((5u32 - 1) << 16) | ((pm4::PM4_WAIT_REG_MEM as u32) << 8);
mem.write_u32(0x4000_0000, hdr); mem.write_u32(0x4000_0000, hdr);
// wait_info: is_memory=1 (bit 4), cmp=equal (bits 2:0 = 2) // wait_info: is_memory=1 (bit 4), cmp=equal (bits 2:0 = 3, per canary's
mem.write_u32(0x4000_0004, 0x12); // MatchValueAndRef selector: 1=Less, 2=LessEq, 3=Equal, …).
mem.write_u32(0x4000_0004, 0x13);
mem.write_u32(0x4000_0008, 0x4000_1000); mem.write_u32(0x4000_0008, 0x4000_1000);
mem.write_u32(0x4000_000C, 0x42); mem.write_u32(0x4000_000C, 0x42);
mem.write_u32(0x4000_0010, 0xFFFF_FFFF); mem.write_u32(0x4000_0010, 0xFFFF_FFFF);

View File

@@ -34,7 +34,7 @@ pub mod xenos_constants;
pub use gpu_system::{ pub use gpu_system::{
ExecOutcome, GpuBlock, GpuMmio, GpuStats, GpuSystem, InterruptSource, PendingInterrupt, ExecOutcome, GpuBlock, GpuMmio, GpuStats, GpuSystem, InterruptSource, PendingInterrupt,
ShaderBlob, SwapNotification, WaitCmp, PHYSICAL_BACKING_BASE, ShaderBlob, SwapNotification, WaitCmp, physical_to_backing,
}; };
pub use handle::{ pub use handle::{
DrainReply, GpuBackend, GpuCommand, GpuDigestSnapshot, GpuHandle, GpuWorker, DrainReply, GpuBackend, GpuCommand, GpuDigestSnapshot, GpuHandle, GpuWorker,

View File

@@ -364,7 +364,11 @@ pub fn copy_to_memory(
// Destination coordinates are 0-based against `dest_base` — the // Destination coordinates are 0-based against `dest_base` — the
// base already points at the top-left of the copy rectangle. // base already points at the top-left of the copy rectangle.
let dst_off = tiled_2d_offset(dx, dy, pitch_aligned, bpp_log2); let dst_off = tiled_2d_offset(dx, dy, pitch_aligned, bpp_log2);
let dst_addr = info.dest_base.wrapping_add(dst_off); // `dest_base` is a bare guest *physical* address; project onto the
// committed backing window so resolved pixels land where the guest
// (and `vd_swap`'s frontbuffer read) actually see them.
let dst_addr =
crate::gpu_system::physical_to_backing(info.dest_base.wrapping_add(dst_off));
if info.source_is_64bpp { if info.source_is_64bpp {
let (lo, hi) = match single_sample_idx { let (lo, hi) = match single_sample_idx {

View File

@@ -32,6 +32,16 @@ pub struct RingBufferView {
/// `VdEnableRingBufferRPtrWriteBack`). We always write back eagerly, so /// `VdEnableRingBufferRPtrWriteBack`). We always write back eagerly, so
/// we don't actually use this for scheduling — kept for observability. /// we don't actually use this for scheduling — kept for observability.
pub rptr_writeback_block_dwords: u32, pub rptr_writeback_block_dwords: u32,
/// True for an indirect-buffer (`INDIRECT_BUFFER`) view. An IB is a fixed
/// *linear* sub-stream, not a circular ring: it is fully written when the
/// GPU jumps to it, so the read pointer advances monotonically from `0` to
/// `size_dwords` and then the buffer is exhausted (the caller ring is
/// popped). It must NOT wrap, and the primary `CP_RB_WPTR` must not be
/// applied to it. Mirrors canary `ExecuteIndirectBuffer`, which executes
/// the IB through a separate `RingBuffer reader_` and restores the primary
/// reader afterward (command_processor.cc). Circular (primary-ring)
/// semantics are used when this is `false`.
pub indirect: bool,
} }
impl RingBufferView { impl RingBufferView {
@@ -46,7 +56,16 @@ impl RingBufferView {
/// True if there is pending unread data to consume. /// True if there is pending unread data to consume.
pub fn has_pending(&self) -> bool { pub fn has_pending(&self) -> bool {
self.is_initialized() && self.read_offset_dwords != self.write_offset_dwords if !self.is_initialized() {
return false;
}
if self.indirect {
// Linear sub-stream: exhausted once the read pointer reaches the
// (fixed) write pointer. Never wraps.
self.read_offset_dwords < self.write_offset_dwords
} else {
self.read_offset_dwords != self.write_offset_dwords
}
} }
/// Number of dwords we can consume without wrapping past the write ptr. /// Number of dwords we can consume without wrapping past the write ptr.
@@ -54,7 +73,10 @@ impl RingBufferView {
if !self.is_initialized() { if !self.is_initialized() {
return 0; return 0;
} }
if self.write_offset_dwords >= self.read_offset_dwords { if self.indirect {
self.write_offset_dwords
.saturating_sub(self.read_offset_dwords)
} else if self.write_offset_dwords >= self.read_offset_dwords {
self.write_offset_dwords - self.read_offset_dwords self.write_offset_dwords - self.read_offset_dwords
} else { } else {
// write has wrapped — we can read up to the end of the ring. // write has wrapped — we can read up to the end of the ring.
@@ -62,14 +84,20 @@ impl RingBufferView {
} }
} }
/// Advance the read pointer by `dwords`, wrapping at `size_dwords`. /// Advance the read pointer by `dwords`. Circular rings wrap at
/// `size_dwords`; an indirect buffer advances linearly (no wrap) so it
/// terminates exactly at its fixed write pointer.
pub fn advance_read(&mut self, dwords: u32) { pub fn advance_read(&mut self, dwords: u32) {
if self.size_dwords == 0 { if self.size_dwords == 0 {
return; return;
} }
if self.indirect {
self.read_offset_dwords = self.read_offset_dwords.saturating_add(dwords);
} else {
self.read_offset_dwords = self.read_offset_dwords =
(self.read_offset_dwords + dwords) % self.size_dwords; (self.read_offset_dwords + dwords) % self.size_dwords;
} }
}
/// Guest address for the dword at relative offset `i` from the current /// Guest address for the dword at relative offset `i` from the current
/// read pointer. `None` if uninitialized. /// read pointer. `None` if uninitialized.
@@ -77,7 +105,11 @@ impl RingBufferView {
if !self.is_initialized() { if !self.is_initialized() {
return None; return None;
} }
let off = (self.read_offset_dwords + offset_dwords) % self.size_dwords; let off = if self.indirect {
self.read_offset_dwords.saturating_add(offset_dwords)
} else {
(self.read_offset_dwords + offset_dwords) % self.size_dwords
};
Some(self.base.wrapping_add(off.wrapping_mul(4))) Some(self.base.wrapping_add(off.wrapping_mul(4)))
} }
} }
@@ -120,4 +152,52 @@ mod tests {
assert_eq!(v.addr_at_offset(1), Some(0x4000_0000)); assert_eq!(v.addr_at_offset(1), Some(0x4000_0000));
assert_eq!(v.addr_at_offset(2), Some(0x4000_0004)); assert_eq!(v.addr_at_offset(2), Some(0x4000_0004));
} }
#[test]
fn indirect_buffer_drains_linearly_and_terminates() {
// An indirect buffer is a fixed linear sub-stream: read advances from
// 0 to `size_dwords` and then is exhausted — it must NOT wrap back to
// 0 (which previously caused an infinite re-read of a system command
// buffer; iterate-2O). write_offset == size, exactly as the
// INDIRECT_BUFFER handler sets it.
let mut ib = RingBufferView {
base: 0x4adf_5080,
size_dwords: 11,
read_offset_dwords: 0,
write_offset_dwords: 11,
rptr_writeback_addr: 0,
rptr_writeback_block_dwords: 0,
indirect: true,
};
assert!(ib.has_pending());
// Drain the exact packet layout observed for Sylpheed's init IB:
// 2 + 3 + 6 dwords = 11.
ib.advance_read(2);
assert!(ib.has_pending());
ib.advance_read(3);
assert!(ib.has_pending());
ib.advance_read(6); // reaches 11 == write
assert_eq!(ib.read_offset_dwords, 11);
assert!(
!ib.has_pending(),
"indirect buffer must terminate at write ptr, not wrap to 0"
);
// addr_at_offset must not modulo-wrap for an indirect buffer.
ib.read_offset_dwords = 9;
assert_eq!(ib.addr_at_offset(1), Some(0x4adf_5080 + 10 * 4));
}
#[test]
fn indirect_flag_does_not_affect_circular_ring() {
// Sanity: a circular (primary) ring still wraps as before.
let mut v = RingBufferView::new();
v.base = 0x4adc_c000;
v.size_dwords = 8192;
v.read_offset_dwords = 8190;
v.write_offset_dwords = 2;
assert!(v.has_pending());
v.advance_read(4); // (8190 + 4) % 8192 = 2
assert_eq!(v.read_offset_dwords, 2);
assert!(!v.has_pending());
}
} }

View File

@@ -486,12 +486,20 @@ fn ke_query_performance_frequency(ctx: &mut PpcContext, _mem: &GuestMemory, _sta
ctx.gpr[3] = 50_000_000; // 50 MHz ctx.gpr[3] = 50_000_000; // 50 MHz
} }
fn ke_query_system_time(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) { fn ke_query_system_time(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
let time_ptr = ctx.gpr[3] as u32; let time_ptr = ctx.gpr[3] as u32;
if time_ptr != 0 { if time_ptr != 0 {
let fake_time: u64 = 132_500_000_000_000_000; // ~2021 FILETIME // ITERATE-2J — advance with the same deterministic clock the
mem.write_u32(time_ptr, (fake_time >> 32) as u32); // KeTimeStampBundle uses (1 global_clock unit ≈ 100 ns) so a guest
mem.write_u32(time_ptr + 4, fake_time as u32); // that polls KeQuerySystemTime for elapsed time also sees forward
// progress instead of a frozen constant. FILETIME base (~2021) +
// 100-ns-unit clock.
const FILETIME_BASE: u64 = 132_500_000_000_000_000;
let hw_id = state.scheduler.current_hw_id().unwrap_or(0);
let now = state.now_basis_at(hw_id);
let system_time = FILETIME_BASE.wrapping_add(now);
mem.write_u32(time_ptr, (system_time >> 32) as u32);
mem.write_u32(time_ptr + 4, system_time as u32);
} }
} }
@@ -2875,10 +2883,12 @@ fn vd_initialize_ring_buffer(ctx: &mut PpcContext, _mem: &GuestMemory, state: &m
// packets directly into ring memory at the current WPTR (the GPU // packets directly into ring memory at the current WPTR (the GPU
// backend lives on a worker thread under `--gpu-thread` so we can't // backend lives on a worker thread under `--gpu-thread` so we can't
// read its `ring.base` from the kernel side without a channel hop). // read its `ring.base` from the kernel side without a channel hop).
// Per canary: size_log2 is log2(size in BYTES), so size in dwords = // Per canary `CommandProcessor::InitializeRingBuffer`: the ring is
// 2^size_log2 / 4 = 1 << (size_log2 - 2). // `1 << (size_log2 + 3)` bytes = `1 << (size_log2 + 1)` dwords (`r4` is
// log2 of the size in quadwords). Kept in sync with
// `GpuSystem::initialize_ring_buffer`. (Currently bookkeeping-only.)
state.ring_base = ptr; state.ring_base = ptr;
state.ring_size_dwords = if size_log2 >= 2 { 1u32 << (size_log2 - 2) } else { 0 }; state.ring_size_dwords = 1u32 << (size_log2 + 1);
ctx.gpr[3] = 0; ctx.gpr[3] = 0;
} }
@@ -3161,13 +3171,18 @@ fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
// safer to cap the read at the known total size to avoid OOB. // safer to cap the read at the known total size to avoid OOB.
let mut tiled = Vec::with_capacity(total_tiled_bytes); let mut tiled = Vec::with_capacity(total_tiled_bytes);
let mut ok = true; let mut ok = true;
// The frontbuffer is a guest *physical* address; project onto the
// committed backing window (see `xenia_gpu::physical_to_backing`)
// so the present reads the pixels the GPU resolved, not a stale /
// zero mirror page.
let fb_backing = xenia_gpu::physical_to_backing(swap.frontbuffer_phys);
for i in 0..total_tiled_bytes { for i in 0..total_tiled_bytes {
// read_u8 is cheap — the VirtualMemory handler returns 0 // read_u8 is cheap — the VirtualMemory handler returns 0
// for unmapped pages so we get a recognisable dark frame // for unmapped pages so we get a recognisable dark frame
// rather than a crash if the address turned out bogus. // rather than a crash if the address turned out bogus.
let addr = swap.frontbuffer_phys.wrapping_add(i as u32); let addr = fb_backing.wrapping_add(i as u32);
tiled.push(mem.read_u8(addr)); tiled.push(mem.read_u8(addr));
if addr < swap.frontbuffer_phys { if addr < fb_backing {
ok = false; ok = false;
break; break;
} }

View File

@@ -17,6 +17,16 @@ impl PcrWriter for GuestMemoryPcr<'_> {
// `GuestMemory::write_u32` takes `&self` post-M2 trait flip; the // `GuestMemory::write_u32` takes `&self` post-M2 trait flip; the
// wrapping `&'a GuestMemory` is sufficient. // wrapping `&'a GuestMemory` is sufficient.
self.0.write_u32(pcr_base + 0x2C, hw_id as u32); self.0.write_u32(pcr_base + 0x2C, hw_id as u32);
// PRCB.current_cpu byte at PCR+0x10C (prcb_data@0x100 + current_cpu@0xC).
// Canary writes `GetFakeCpuNumber(affinity)` here (xthread.cc:847
// `pcr->prcb_data.current_cpu = cpu_index`), which equals the HW thread
// id we already compute. Guest spin-barriers (e.g. sub_824D1328, used by
// the audio/update pump threads at entries 0x824D2878/0x824D2940) index a
// per-HW-thread occupancy array by `lbz r11, 268(r13)` = this byte. Left
// unwritten it stayed 0 for every thread, so all threads collided on
// slot 0 and the multi-thread rendezvous signature never assembled —
// the pump threads spun forever and never fired their KeSetEvent loops.
self.0.write_u8(pcr_base + 0x10C, hw_id);
} }
} }
@@ -354,6 +364,16 @@ pub struct KernelState {
/// [`Self::fire_due_silph_autosignals`] on the first visit where /// [`Self::fire_due_silph_autosignals`] on the first visit where
/// the pending queue is non-empty but no entry is due yet. /// the pending queue is non-empty but no entry is due yet.
pub silph_autosignal_diag_logged: bool, pub silph_autosignal_diag_logged: bool,
/// ITERATE-2J — guest VA of the `KeTimeStampBundle` block (xboxkrnl
/// data export ordinal 0x00AD). Set during the import-patch pass in
/// `xenia-app`. Zero until then. The guest's worker-hub channel
/// dispatch loop polls `[block+0x10]` (`tick_count`, milliseconds) and
/// gates dispatch on a `tick_count + 66` deadline; if the block is
/// never re-written that deadline never elapses and the hub spins
/// forever (the tid14 0x109c starvation gate). The run loop ticks this
/// block every round from the deterministic `global_clock` via
/// [`Self::update_timestamp_bundle`].
pub timestamp_bundle_addr: u32,
} }
/// ITERATE-2C Phase D — one queued auto-signal. `deadline_cycle` is /// ITERATE-2C Phase D — one queued auto-signal. `deadline_cycle` is
@@ -444,6 +464,7 @@ impl KernelState {
silph_autosignal_pending: Vec::new(), silph_autosignal_pending: Vec::new(),
last_cycle_hint: 0, last_cycle_hint: 0,
silph_autosignal_diag_logged: false, silph_autosignal_diag_logged: false,
timestamp_bundle_addr: 0,
}; };
crate::exports::register_exports(&mut state); crate::exports::register_exports(&mut state);
crate::xam::register_exports(&mut state); crate::xam::register_exports(&mut state);
@@ -862,6 +883,57 @@ impl KernelState {
self.last_cycle_hint = now_cycle; self.last_cycle_hint = now_cycle;
} }
/// ITERATE-2J — tick the `KeTimeStampBundle` block (xboxkrnl ordinal
/// 0x00AD) from the deterministic monotonic clock so the guest sees a
/// clock that *advances*.
///
/// `clock` is the scheduler's `global_clock` — a pure function of
/// retired guest instructions (see [`Self::now_basis_at`] /
/// `Scheduler::global_clock`). Lockstep floors it up to
/// `stats.instruction_count` each round; parallel sums per-block
/// retired counts. Using it (rather than wall-clock) keeps every
/// guest-visible time value a deterministic function of guest progress,
/// so lockstep stays byte-reproducible.
///
/// ## Cadence
/// The existing kernel time math (`parse_timeout` in `exports.rs`)
/// already treats **1 `global_clock` unit ≈ 100 ns**: it converts a
/// signed 100-ns `LARGE_INTEGER` timeout to a deadline by dividing the
/// magnitude by 100 and adding it to `now` (= `global_clock`). To stay
/// coherent with that, this method uses the same scale:
///
/// * `interrupt_time` / `system_time` (100-ns units): `clock` (with a
/// FILETIME epoch base added to `system_time`).
/// * `tick_count` (milliseconds): `clock / INSTRUCTIONS_PER_MS` where
/// `INSTRUCTIONS_PER_MS = 10_000` (10_000 × 100 ns = 1 ms).
///
/// At 10_000 clock-units/ms, the guest's `tick_count + 66` ms hub
/// deadline elapses by ~660_000 retired instructions — very early in a
/// ~1 B-instruction boot — while a 16 ms `KeWait` timeout
/// (`parse_timeout`: 160_000 units) still resolves to 16 ms of
/// tick_count, so no timeout collapses to "instant". The two readers
/// share one scale.
pub fn update_timestamp_bundle(&self, mem: &GuestMemory, clock: u64) {
let block = self.timestamp_bundle_addr;
if block == 0 {
return;
}
const INSTRUCTIONS_PER_MS: u64 = 10_000;
// FILETIME epoch base (~2021) so `system_time` is a plausible
// absolute wall-clock; matches the constant used by
// `ke_query_system_time`. interrupt_time is "since boot" so it
// starts at the clock origin (no epoch offset).
const FILETIME_BASE: u64 = 132_500_000_000_000_000;
let interrupt_time: u64 = clock;
let system_time: u64 = FILETIME_BASE.wrapping_add(clock);
let tick_count: u32 = (clock / INSTRUCTIONS_PER_MS) as u32;
// BE writes (write_u64/write_u32 use to_be_bytes) — guest is BE.
mem.write_u64(block, interrupt_time); // +0x00 interrupt_time
mem.write_u64(block + 0x08, system_time); // +0x08 system_time
mem.write_u32(block + 0x10, tick_count); // +0x10 tick_count (ms)
mem.write_u32(block + 0x14, 0); // +0x14 padding
}
/// ITERATE-2C Phase D — register a freshly-allocated event for /// ITERATE-2C Phase D — register a freshly-allocated event for
/// auto-signal after the configured delay, **iff** the creating /// auto-signal after the configured delay, **iff** the creating
/// thread matches the silph::UImpl tid=13 chain that wedges in /// thread matches the silph::UImpl tid=13 chain that wedges in

View File

@@ -57,6 +57,11 @@ pub fn allocate_thread_image(
mem.write_u32(pcr_base, tls_base); mem.write_u32(pcr_base, tls_base);
mem.write_u32(pcr_base + 0x2C, hw_thread_id as u32); mem.write_u32(pcr_base + 0x2C, hw_thread_id as u32);
mem.write_u32(pcr_base + 0x100, 0x1000); mem.write_u32(pcr_base + 0x100, 0x1000);
// +0x10C prcb_data.current_cpu — canary `pcr->prcb_data.current_cpu`
// (PRCB@0x100 + current_cpu@0xC). Guest spin-barriers index a
// per-HW-thread slot array by `lbz r11, 268(r13)` = this byte; it
// must equal the HW thread id (== PCR+0x2C). See state.rs PcrWriter.
mem.write_u8(pcr_base + 0x10C, hw_thread_id);
mem.write_u32(pcr_base + 0x150, 0); mem.write_u32(pcr_base + 0x150, 0);
Some(ThreadImage { Some(ThreadImage {