Compare commits
7 Commits
iterate-2I
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad9c8e4cb8 | ||
|
|
873c197ff1 | ||
|
|
1ae472bd2b | ||
|
|
034ec8b47f | ||
|
|
93f60a3ba0 | ||
|
|
2bdb93e51e | ||
|
|
ed2e0e72fd |
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -1528,8 +1540,19 @@ fn cmd_exec_inner(
|
|||||||
mem.write_u32(addr, block);
|
mem.write_u32(addr, block);
|
||||||
}
|
}
|
||||||
("xboxkrnl.exe", 0x01BE) => {
|
("xboxkrnl.exe", 0x01BE) => {
|
||||||
// VdGlobalDevice — passed through to Vd* shims. Write 0.
|
// VdGlobalDevice — a *pointer to* a global D3D-device cell.
|
||||||
mem.write_u32(addr, 0);
|
// Mirror xenia-canary RegisterVideoExports (xboxkrnl_video.cc:
|
||||||
|
// 557-564): allocate a 4-byte cell, point the import slot at
|
||||||
|
// it, and zero the cell. The guest's graphics init then stores
|
||||||
|
// its device object INTO the cell (e.g. sub_824C6DC0 @
|
||||||
|
// 0x824C6F18 `stw r31, 0([0x82000750])`), and the swap-complete
|
||||||
|
// callback sub_824CE2B8 reads it back via the two-level
|
||||||
|
// `[[VdGlobalDevice]+0]+15160` to bump the swap counter (clock
|
||||||
|
// B). Writing 0 directly here (the old behaviour) made that
|
||||||
|
// store land at address 0 and the swap counter never advance —
|
||||||
|
// freezing the title-loop's per-frame manager update.
|
||||||
|
let cell = alloc_zero(0x4, &mut mem, &mut kernel);
|
||||||
|
mem.write_u32(addr, cell);
|
||||||
}
|
}
|
||||||
("xboxkrnl.exe", 0x01C0) => {
|
("xboxkrnl.exe", 0x01C0) => {
|
||||||
// VdGpuClockInMHz
|
// VdGpuClockInMHz
|
||||||
@@ -2852,6 +2875,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 +3325,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
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"instructions": 50000000,
|
"instructions": 50000014,
|
||||||
"imports": 339766,
|
"imports": 178937,
|
||||||
"unimpl": 0,
|
"unimpl": 0,
|
||||||
"draws": 0,
|
"draws": 78,
|
||||||
"swaps": 2,
|
"swaps": 4,
|
||||||
"unique_render_targets": 0,
|
"unique_render_targets": 2,
|
||||||
"shader_blobs_live": 0,
|
"shader_blobs_live": 3,
|
||||||
"texture_cache_entries": 0
|
"texture_cache_entries": 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,16 @@ fn run_oracle(label: &str, max_instr: u64, golden_rel: &str) {
|
|||||||
&iso,
|
&iso,
|
||||||
"-n",
|
"-n",
|
||||||
&max_instr_str,
|
&max_instr_str,
|
||||||
|
// Pin the inline (single-threaded) GPU backend. The default
|
||||||
|
// threaded backend drains the ring on a separate host thread,
|
||||||
|
// so the exact instruction at which a CP interrupt is queued —
|
||||||
|
// and therefore when the guest's swap-complete ISR callback runs
|
||||||
|
// (iterate-2S armed it via SCRATCH_REG writeback) — varies run to
|
||||||
|
// run. Inline draining is instruction-count-deterministic, which
|
||||||
|
// is what a regression golden needs. (The threaded path is the
|
||||||
|
// documented "GPU thread race" the stable-digest already warns
|
||||||
|
// about.)
|
||||||
|
"--gpu-inline",
|
||||||
"--stable-digest",
|
"--stable-digest",
|
||||||
"--expect",
|
"--expect",
|
||||||
&golden_str,
|
&golden_str,
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -642,10 +726,13 @@ impl GpuSystem {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
});
|
});
|
||||||
self.pending_interrupts.push(PendingInterrupt {
|
// iterate-2T: do NOT raise a CP swap-complete interrupt here. Canary's
|
||||||
source: InterruptSource::Swap,
|
// `VdSwap`/PM4_XE_SWAP path raises no interrupt; swap-complete CP
|
||||||
cpu_mask: 0x1,
|
// interrupts come ONLY from in-stream `PM4_INTERRUPT` packets, which
|
||||||
});
|
// are naturally ordered after D3D has armed the swap-callback slot.
|
||||||
|
// Synthesizing one out of band (as we did pre-2T) delivered a CP
|
||||||
|
// interrupt while the slot still held the `0xBADF00D` placeholder,
|
||||||
|
// tripping the graphics ISR's "Unanticipated CPU_INTERRUPT" assert.
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
frame = self.swap_counter,
|
frame = self.swap_counter,
|
||||||
fb = format_args!("{frontbuffer_phys:#010x}"),
|
fb = format_args!("{frontbuffer_phys:#010x}"),
|
||||||
@@ -657,9 +744,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 +774,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 +827,58 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CP scratch-register memory writeback, mirroring canary's
|
||||||
|
/// `CommandProcessor::HandleSpecialRegisterWrite`
|
||||||
|
/// (`command_processor.cc:545-552`). Every register write runs through
|
||||||
|
/// here; when the target is one of the eight `SCRATCH_REG{n}`
|
||||||
|
/// (`0x0578..=0x057F`) **and** the matching bit in `SCRATCH_UMSK` is set,
|
||||||
|
/// the value is also written (big-endian, as `mem.write_u32` already
|
||||||
|
/// stores) to `SCRATCH_ADDR + n*4` in guest physical memory.
|
||||||
|
///
|
||||||
|
/// Sylpheed arms its CP swap-complete interrupt callback through this
|
||||||
|
/// path: it programs `SCRATCH_ADDR` to the GPU command-block descriptor
|
||||||
|
/// (`[gfx+10772]`, runtime `0x0b1d5000`), `SCRATCH_UMSK` bit 4, then a
|
||||||
|
/// Type-0 write of the callback PC `0x824ce2b8` into `SCRATCH_REG4`
|
||||||
|
/// (`0x057C`). The writeback lands it at descriptor+16 (`0x4b1d5010`),
|
||||||
|
/// which the graphics ISR (`sub_824BE9A0`) reads via `[[gfx+10772]+16]`
|
||||||
|
/// and `bcctrl`s to fire the swap-complete callback. Without this
|
||||||
|
/// writeback the slot stayed NULL, the ISR skipped the callback, the
|
||||||
|
/// swap counter never advanced, and the title's per-frame manager
|
||||||
|
/// re-fired once then plateaued.
|
||||||
|
fn scratch_register_writeback(&self, mem: &dyn MemoryAccess, index: u32, value: u32) {
|
||||||
|
if !(reg::SCRATCH_REG0..=reg::SCRATCH_REG7).contains(&index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let scratch_reg = index - reg::SCRATCH_REG0;
|
||||||
|
let umsk = self.register_file.read(reg::SCRATCH_UMSK);
|
||||||
|
if (1u32 << scratch_reg) & umsk == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let scratch_addr = self.register_file.read(reg::SCRATCH_ADDR);
|
||||||
|
let mem_addr = physical_to_backing(scratch_addr.wrapping_add(scratch_reg * 4));
|
||||||
|
mem.write_u32(mem_addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
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(
|
||||||
@@ -748,6 +903,7 @@ impl GpuSystem {
|
|||||||
let value = mem.read_u32(dword_addr);
|
let value = mem.read_u32(dword_addr);
|
||||||
let target = if write_one { base_index } else { base_index + i };
|
let target = if write_one { base_index } else { base_index + i };
|
||||||
self.register_file.write(target, value);
|
self.register_file.write(target, value);
|
||||||
|
self.scratch_register_writeback(mem, target, value);
|
||||||
}
|
}
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
base = format_args!("{base_index:#x}"),
|
base = format_args!("{base_index:#x}"),
|
||||||
@@ -770,6 +926,8 @@ impl GpuSystem {
|
|||||||
let b = mem.read_u32(b_addr);
|
let b = mem.read_u32(b_addr);
|
||||||
self.register_file.write(reg_index_1, a);
|
self.register_file.write(reg_index_1, a);
|
||||||
self.register_file.write(reg_index_2, b);
|
self.register_file.write(reg_index_2, b);
|
||||||
|
self.scratch_register_writeback(mem, reg_index_1, a);
|
||||||
|
self.scratch_register_writeback(mem, reg_index_2, b);
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
r1 = format_args!("{reg_index_1:#x}"),
|
r1 = format_args!("{reg_index_1:#x}"),
|
||||||
r2 = format_args!("{reg_index_2:#x}"),
|
r2 = format_args!("{reg_index_2:#x}"),
|
||||||
@@ -816,7 +974,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 +992,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 +1018,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 +1030,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 +1079,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 +1091,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 +1107,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 +1117,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 +1136,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 +1164,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 +1294,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 +1326,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));
|
||||||
@@ -1378,6 +1549,17 @@ pub mod reg {
|
|||||||
/// `XE_GPU_REG_COHER_STATUS_HOST` — coherency bits
|
/// `XE_GPU_REG_COHER_STATUS_HOST` — coherency bits
|
||||||
/// (Canary `register_table.inc:530`).
|
/// (Canary `register_table.inc:530`).
|
||||||
pub const COHER_STATUS_HOST: u32 = 0x0A31;
|
pub const COHER_STATUS_HOST: u32 = 0x0A31;
|
||||||
|
/// `XE_GPU_REG_SCRATCH_UMSK` — bitmask of which `SCRATCH_REG{n}` writes are
|
||||||
|
/// mirrored to memory (Canary `register_table.inc:139`).
|
||||||
|
pub const SCRATCH_UMSK: u32 = 0x01DC;
|
||||||
|
/// `XE_GPU_REG_SCRATCH_ADDR` — base physical address of the scratch
|
||||||
|
/// writeback block (Canary `register_table.inc:141`).
|
||||||
|
pub const SCRATCH_ADDR: u32 = 0x01DD;
|
||||||
|
/// `XE_GPU_REG_SCRATCH_REG0` — first of 8 CP scratch registers
|
||||||
|
/// (`0x0578..=0x057F`, Canary `register_table.inc:331-338`).
|
||||||
|
pub const SCRATCH_REG0: u32 = 0x0578;
|
||||||
|
/// `XE_GPU_REG_SCRATCH_REG7` — last CP scratch register.
|
||||||
|
pub const SCRATCH_REG7: u32 = 0x057F;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 32-bit FNV-1a over a u32 seed + a slice of u32s. Used to derive a
|
/// 32-bit FNV-1a over a u32 seed + a slice of u32s. Used to derive a
|
||||||
@@ -1468,6 +1650,38 @@ mod tests {
|
|||||||
assert_eq!(gpu.register_file.read(0x101), 0xCAFE_BABE);
|
assert_eq!(gpu.register_file.read(0x101), 0xCAFE_BABE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scratch_reg_write_mirrors_to_memory_when_umsk_enabled() {
|
||||||
|
// Mirrors Sylpheed's CP swap-callback arming: SCRATCH_ADDR points at a
|
||||||
|
// descriptor, SCRATCH_UMSK enables bit 4, and a Type-0 write of the
|
||||||
|
// callback PC into SCRATCH_REG4 (0x57C) must land at SCRATCH_ADDR + 16.
|
||||||
|
let mut gpu = GpuSystem::new();
|
||||||
|
let mut mem = build_mem();
|
||||||
|
gpu.initialize_ring_buffer(0x4000_0000, 10);
|
||||||
|
// Program SCRATCH_ADDR = 0x4000_1000 (physical-mirror identity), and
|
||||||
|
// SCRATCH_UMSK = bit 4 only (so SCRATCH_REG4 mirrors, REG3 does not).
|
||||||
|
gpu.register_file.write(reg::SCRATCH_ADDR, 0x4000_1000);
|
||||||
|
gpu.register_file.write(reg::SCRATCH_UMSK, 1 << 4);
|
||||||
|
// Type0 write run: base = SCRATCH_REG3 (0x57B), count = 2 → writes
|
||||||
|
// 0x11111111 → SCRATCH_REG3 (UMSK bit 3 clear), 0x824CE2B8 →
|
||||||
|
// SCRATCH_REG4 (UMSK bit 4 set → mirrored to ADDR + 4*4 = +16).
|
||||||
|
const SCRATCH_REG3: u32 = 0x057B;
|
||||||
|
let hdr = (1u32 << 16) | SCRATCH_REG3;
|
||||||
|
mem.write_u32(0x4000_0000, hdr);
|
||||||
|
mem.write_u32(0x4000_0004, 0x1111_1111);
|
||||||
|
mem.write_u32(0x4000_0008, 0x824C_E2B8);
|
||||||
|
gpu.extend_write_ptr(3);
|
||||||
|
assert!(matches!(gpu.execute_one(&mut mem), ExecOutcome::Stepped { .. }));
|
||||||
|
// SCRATCH_REG3 (bit 3 clear) must NOT mirror; SCRATCH_REG4 (bit 4 set)
|
||||||
|
// must mirror to SCRATCH_ADDR + 16.
|
||||||
|
assert_eq!(mem.read_u32(0x4000_1000 + 12), 0, "reg3 must not mirror");
|
||||||
|
assert_eq!(
|
||||||
|
mem.read_u32(0x4000_1000 + 16),
|
||||||
|
0x824C_E2B8,
|
||||||
|
"reg4 must mirror to SCRATCH_ADDR+16"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wait_reg_mem_blocks_then_unblocks_when_mem_changes() {
|
fn wait_reg_mem_blocks_then_unblocks_when_mem_changes() {
|
||||||
let mut gpu = GpuSystem::new();
|
let mut gpu = GpuSystem::new();
|
||||||
@@ -1477,8 +1691,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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,13 +84,19 @@ 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;
|
||||||
}
|
}
|
||||||
self.read_offset_dwords =
|
if self.indirect {
|
||||||
(self.read_offset_dwords + dwords) % self.size_dwords;
|
self.read_offset_dwords = self.read_offset_dwords.saturating_add(dwords);
|
||||||
|
} else {
|
||||||
|
self.read_offset_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
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2989,52 +2999,82 @@ fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
|||||||
// xboxkrnl_video.cc:479. Currently skipped (see below).
|
// xboxkrnl_video.cc:479. Currently skipped (see below).
|
||||||
let _ = fetch_dwords; // silence unused — will be live again under the deferred path
|
let _ = fetch_dwords; // silence unused — will be live again under the deferred path
|
||||||
|
|
||||||
// The original M2b path zero-filled buffer_ptr (in the system command
|
// iterate-2T: mirror xenia-canary `VdSwap_entry` (xboxkrnl_video.cc:518-548)
|
||||||
// buffer) and bumped WPTR by 64 to expose the game's own ring writes.
|
// FAITHFULLY. The game reserves 64 dwords (256 bytes) in the primary ring
|
||||||
// Keep that untouched — the game still expects buffer_ptr to be a
|
// at `buffer_ptr`; canary writes a `PM4_TYPE0(SHADER_CONSTANT_FETCH_00_0)`
|
||||||
// skippable scratch area, and the bump still exposes any game-batched
|
// fetch-constant patch followed by `PM4_TYPE3(PM4_XE_SWAP)`, then pads with
|
||||||
// PM4 packets for the drain.
|
// NOPs. We do the same, then bump WPTR by 64 so the drain consumes the
|
||||||
|
// PM4_XE_SWAP **in command-stream order** — i.e. AFTER any in-stream
|
||||||
|
// callback-arming Type-0 writes the game already queued.
|
||||||
|
//
|
||||||
|
// Why this matters (the iterate-2T root): the previous M2b short-circuit
|
||||||
|
// called `notify_xe_swap` directly from the HLE, which synthesized a CP
|
||||||
|
// swap-complete interrupt OUT OF BAND. When that interrupt reached the
|
||||||
|
// graphics ISR (`sub_824BE9A0`) before D3D had armed its swap-callback
|
||||||
|
// slot (`[gfx+10772]+16` still the `0xBADF00D` placeholder), the ISR hit
|
||||||
|
// its "ERR[D3D]: Unanticipated CPU_INTERRUPT. Sign of a corrupt command
|
||||||
|
// buffer?" assert (`twi` at 0x824BE9DC). Routing the swap through the ring
|
||||||
|
// packet keeps the interrupt naturally ordered after arming, matching
|
||||||
|
// canary (whose VdSwap raises NO interrupt itself; swap-complete CP
|
||||||
|
// interrupts come only from in-stream `PM4_INTERRUPT` packets).
|
||||||
if buffer_ptr != 0 {
|
if buffer_ptr != 0 {
|
||||||
for i in 0..64u32 {
|
let mut off = 0u32;
|
||||||
mem.write_u32(buffer_ptr + i * 4, xenia_gpu::pm4::make_packet_type2());
|
let mut put = |i: &mut u32, v: u32| {
|
||||||
|
mem.write_u32(buffer_ptr + *i * 4, v);
|
||||||
|
*i += 1;
|
||||||
|
};
|
||||||
|
// PM4_TYPE0 fetch-constant slot-0 patch (6 dwords payload). The
|
||||||
|
// base_address field is patched to the physical frontbuffer so the
|
||||||
|
// bloom/blur "sample frame N for frame N+1" path reads the right page.
|
||||||
|
let mut patched = fetch_dwords;
|
||||||
|
patched[1] = (patched[1] & 0x0000_0FFF) | ((frontbuffer_addr >> 12) << 12);
|
||||||
|
put(
|
||||||
|
&mut off,
|
||||||
|
xenia_gpu::pm4::make_packet_type0(
|
||||||
|
xenia_gpu::gpu_system::CONST_BASE_FETCH as u16,
|
||||||
|
6,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for d in patched {
|
||||||
|
put(&mut off, d);
|
||||||
|
}
|
||||||
|
// PM4_TYPE3(PM4_XE_SWAP, 4 dwords): signature, frontbuffer_phys, w, h.
|
||||||
|
put(
|
||||||
|
&mut off,
|
||||||
|
xenia_gpu::pm4::make_packet_type3(xenia_gpu::pm4::PM4_XE_SWAP, 4),
|
||||||
|
);
|
||||||
|
put(&mut off, xenia_gpu::pm4::SWAP_SIGNATURE);
|
||||||
|
put(&mut off, frontbuffer_addr);
|
||||||
|
put(&mut off, width);
|
||||||
|
put(&mut off, height);
|
||||||
|
// Pad the remainder with NOP (Type-2) packets.
|
||||||
|
while off < 64 {
|
||||||
|
put(&mut off, xenia_gpu::pm4::make_packet_type2());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.gpu.extend_write_ptr_by(64);
|
state.gpu.extend_write_ptr_by(64);
|
||||||
|
|
||||||
// GPUBUG-DRAIN-001: notify the swap directly.
|
// Drain the ring; the PM4_XE_SWAP we just queued (and any in-stream
|
||||||
//
|
// PM4_INTERRUPT) executes in order. The PM4_XE_SWAP handler calls
|
||||||
// Per xenia-canary `VdSwap_entry` (xboxkrnl_video.cc:438-521), the
|
// `notify_xe_swap` for host swap bookkeeping; no synthetic interrupt is
|
||||||
// textbook approach is to inject `PM4_TYPE0(SHADER_CONSTANT_FETCH_00_0)`
|
// raised (see `notify_xe_swap`).
|
||||||
// (fetch-constant slot-0 patch for the Sylpheed bloom/blur "frame N+1"
|
|
||||||
// sample) followed by `PM4_TYPE3(PM4_XE_SWAP)` directly into the
|
|
||||||
// primary ring at WPTR, then let the natural drain consume them.
|
|
||||||
//
|
|
||||||
// That works in **pure lockstep** (drain runs at every kernel callback
|
|
||||||
// boundary, ring has at most a few hundred packets pending). It
|
|
||||||
// **does not** work under `--parallel` (CPU + GPU ring contention) —
|
|
||||||
// observed empirically: vd_swap's `drain_to_current_wptr` consumes
|
|
||||||
// 8-10 million game-batched IB packets in the 900 ms inline-deadline
|
|
||||||
// window without reaching our tail-injected PM4_XE_SWAP. Under
|
|
||||||
// threaded backend the worker has the same deadline. Either:
|
|
||||||
// (a) the safety-net direct notify (below) fires and gets the swap
|
|
||||||
// counted — but if the worker *eventually* drains past our
|
|
||||||
// injected packet later it would double-count,
|
|
||||||
// (b) we extend the deadline so far that vd_swap blocks for many
|
|
||||||
// seconds — unreasonable for a kernel callback.
|
|
||||||
//
|
|
||||||
// Skip the ring injection unconditionally and post `notify_xe_swap`
|
|
||||||
// directly. The drain still runs (game packets execute as normal).
|
|
||||||
// **Trade-off**: the slot-0 fetch-constant patch is deferred —
|
|
||||||
// tracked as GPUBUG-FETCH-PATCH-001. Sylpheed currently has draws=0,
|
|
||||||
// so a stale slot 0 has no observable effect.
|
|
||||||
let drained = state.gpu.drain_to_current_wptr(mem);
|
let drained = state.gpu.drain_to_current_wptr(mem);
|
||||||
tracing::debug!(drained, "VdSwap: drained PM4 packets");
|
tracing::debug!(drained, "VdSwap: drained PM4 packets");
|
||||||
|
|
||||||
// Direct swap notification. Inline mode bumps `swaps_seen`
|
// Safety net: if the drain did NOT reach our PM4_XE_SWAP this call (e.g.
|
||||||
// synchronously; threaded mode posts a `GpuCommand::NotifyXeSwap`
|
// an undersized inline deadline left game-batched packets pending), still
|
||||||
// and the worker bumps it asynchronously.
|
// bump the host swap counter so the UI present + swap stats stay live.
|
||||||
|
// Skip when the in-stream PM4_XE_SWAP already recorded this frontbuffer
|
||||||
|
// (avoids double-counting). This path does NOT raise a CP interrupt.
|
||||||
if frontbuffer_addr != 0 && width > 0 && height > 0 {
|
if frontbuffer_addr != 0 && width > 0 && height > 0 {
|
||||||
state.gpu.notify_xe_swap(frontbuffer_addr, width, height);
|
let already_swapped = state
|
||||||
|
.gpu
|
||||||
|
.as_inline_mut()
|
||||||
|
.map(|g| g.last_swap.map(|s| s.frontbuffer_phys) == Some(frontbuffer_addr))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !already_swapped {
|
||||||
|
state.gpu.notify_xe_swap(frontbuffer_addr, width, height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The remaining vd_swap work (UI publish: shader blobs, constants,
|
// The remaining vd_swap work (UI publish: shader blobs, constants,
|
||||||
@@ -3161,13 +3201,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user