diff --git a/crates/xenia-gpu/src/gpu_system.rs b/crates/xenia-gpu/src/gpu_system.rs index 8ae97d0..bf27114 100644 --- a/crates/xenia-gpu/src/gpu_system.rs +++ b/crates/xenia-gpu/src/gpu_system.rs @@ -28,6 +28,56 @@ use crate::primitive::{self, ProcessedPrimitive}; use crate::register_file::RegisterFile; 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. #[derive(Debug, Clone)] pub struct ShaderBlob { @@ -58,21 +108,37 @@ pub enum WaitCmp { GreaterEq, /// value > ref Greater, - /// Always — caller wants to sleep regardless. + /// Always — caller wants to sleep regardless (selector bit 7). Always, + /// Never matches — `wait_info & 7 == 0` selects bit 0 of canary's + /// selector word, which is always zero. + Never, } 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)<<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 { match wait_info & 0x7 { - 0 => WaitCmp::Less, - 1 => WaitCmp::LessEq, - 2 => WaitCmp::Equal, - 3 => WaitCmp::NotEqual, - 4 => WaitCmp::GreaterEq, - 5 => WaitCmp::Greater, - _ => WaitCmp::Always, + 1 => WaitCmp::Less, + 2 => WaitCmp::LessEq, + 3 => WaitCmp::Equal, + 4 => WaitCmp::NotEqual, + 5 => WaitCmp::GreaterEq, + 6 => WaitCmp::Greater, + 7 => WaitCmp::Always, + _ => WaitCmp::Never, } } @@ -85,6 +151,7 @@ impl WaitCmp { WaitCmp::GreaterEq => value >= reference, WaitCmp::Greater => value > reference, WaitCmp::Always => true, + WaitCmp::Never => false, } } } @@ -561,6 +628,12 @@ impl GpuSystem { pub fn execute_one(&mut self, mem: &dyn MemoryAccess) -> ExecOutcome { // 0) If currently parked, probe the condition and either wake up or stay blocked. 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) { tracing::debug!(?block, "gpu: wait satisfied — resuming"); self.pending_block = None; @@ -658,6 +731,10 @@ impl GpuSystem { /// Called by `VdInitializeRingBuffer` to give us the primary ring. pub fn initialize_ring_buffer(&mut self, base: u32, size_log2: u32) { let size_bytes = 1u32 << size_log2.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.size_dwords = size_bytes / 4; self.ring.read_offset_dwords = 0; @@ -675,6 +752,10 @@ impl GpuSystem { /// Called by `VdEnableRingBufferRPtrWriteBack` to record where the guest /// expects us to mirror `read_offset_dwords`. 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_block_dwords = 1u32 << block_log2.min(31); tracing::info!( @@ -724,6 +805,26 @@ impl GpuSystem { /// upstream packet effects (memory writes, register file updates /// the guest reads via subsequent MMIO) happen-before the /// 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) { if self.ring.rptr_writeback_addr != 0 && self.ring.is_initialized() { mem.write_u32_fence( @@ -816,7 +917,9 @@ impl GpuSystem { } pm4::PM4_INDIRECT_BUFFER | pm4::PM4_INDIRECT_BUFFER_PFD => { 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); // Advance past the IB header + payload before recursing so // the return location is correct. @@ -854,7 +957,8 @@ impl GpuSystem { let is_memory = (wait_info & 0x10) != 0; let cmp = WaitCmp::from_wait_info(wait_info); let poll_addr = if is_memory { - poll_addr_raw & !3 + // Physical memory poll address → committed backing. + physical_to_backing(poll_addr_raw & !3) } else { poll_addr_raw }; @@ -865,6 +969,12 @@ impl GpuSystem { mask, 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) { // Condition already true; proceed past this packet. tracing::trace!(?block, "gpu: WAIT_REG_MEM immediately satisfied"); @@ -908,7 +1018,7 @@ impl GpuSystem { pm4::PM4_REG_TO_MEM => { // payload[0] = reg_index, payload[1] = mem addr 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); mem.write_u32(dst, value); tracing::trace!( @@ -920,7 +1030,7 @@ impl GpuSystem { } pm4::PM4_MEM_WRITE => { // 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 { let val = self.read_payload(mem, i); mem.write_u32(dst, val); @@ -936,7 +1046,7 @@ impl GpuSystem { let mask = self.read_payload(mem, 4); let is_memory = (wait_info & 0x10) != 0; 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 { mem.read_u32(poll_addr) } else { @@ -946,7 +1056,7 @@ impl GpuSystem { let write_addr = self.read_payload(mem, 5); let write_data = self.read_payload(mem, 6); 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 { self.register_file .write(write_addr & 0x1FFF, write_data); @@ -965,7 +1075,7 @@ impl GpuSystem { // payload[0] = initiator (bit 31: write counter, else write `value`) // payload[1] = address, payload[2] = value 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); self.register_file .write(reg::VGT_EVENT_INITIATOR, initiator & 0x3F); @@ -993,7 +1103,7 @@ impl GpuSystem { // payload[0] = initiator, [1] = address. Writes 6 u16 extents // (min/max x/y/z) — we're not tracking scissors yet, so write zeros. 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 .write(reg::VGT_EVENT_INITIATOR, initiator & 0x3F); self.handle_event_initiator(initiator & 0x3F, mem); @@ -1123,7 +1233,7 @@ impl GpuSystem { } pm4::PM4_LOAD_ALU_CONSTANT => { // 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 size_dwords = self.read_payload(mem, 3); let index = offset_type & 0x7FF; @@ -1155,7 +1265,7 @@ impl GpuSystem { } v } 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); for i in 0..size_dwords { v.push(mem.read_u32(addr + i * 4)); @@ -1477,8 +1587,9 @@ mod tests { // header let hdr = (3u32 << 30) | ((5u32 - 1) << 16) | ((pm4::PM4_WAIT_REG_MEM as u32) << 8); mem.write_u32(0x4000_0000, hdr); - // wait_info: is_memory=1 (bit 4), cmp=equal (bits 2:0 = 2) - mem.write_u32(0x4000_0004, 0x12); + // wait_info: is_memory=1 (bit 4), cmp=equal (bits 2:0 = 3, per canary's + // 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_000C, 0x42); mem.write_u32(0x4000_0010, 0xFFFF_FFFF); diff --git a/crates/xenia-gpu/src/lib.rs b/crates/xenia-gpu/src/lib.rs index 74f906f..a7539d9 100644 --- a/crates/xenia-gpu/src/lib.rs +++ b/crates/xenia-gpu/src/lib.rs @@ -34,7 +34,7 @@ pub mod xenos_constants; pub use gpu_system::{ ExecOutcome, GpuBlock, GpuMmio, GpuStats, GpuSystem, InterruptSource, PendingInterrupt, - ShaderBlob, SwapNotification, WaitCmp, + PHYSICAL_BACKING_BASE, ShaderBlob, SwapNotification, WaitCmp, physical_to_backing, }; pub use handle::{ DrainReply, GpuBackend, GpuCommand, GpuDigestSnapshot, GpuHandle, GpuWorker, diff --git a/crates/xenia-gpu/src/resolve.rs b/crates/xenia-gpu/src/resolve.rs index e81bfe0..da62e34 100644 --- a/crates/xenia-gpu/src/resolve.rs +++ b/crates/xenia-gpu/src/resolve.rs @@ -364,7 +364,11 @@ pub fn copy_to_memory( // Destination coordinates are 0-based against `dest_base` — the // 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_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 { let (lo, hi) = match single_sample_idx { diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs index d7d7e74..e118e57 100644 --- a/crates/xenia-kernel/src/exports.rs +++ b/crates/xenia-kernel/src/exports.rs @@ -3169,13 +3169,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. let mut tiled = Vec::with_capacity(total_tiled_bytes); 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 { // read_u8 is cheap — the VirtualMemory handler returns 0 // for unmapped pages so we get a recognisable dark frame // 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)); - if addr < swap.frontbuffer_phys { + if addr < fb_backing { ok = false; break; }