Compare commits
10 Commits
iterate-2J
...
iterate-2Z
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f5d5cf5f7 | ||
|
|
2f55d1fd7d | ||
|
|
a91f4c550b | ||
|
|
66bd805726 | ||
|
|
ad9c8e4cb8 | ||
|
|
873c197ff1 | ||
|
|
1ae472bd2b | ||
|
|
034ec8b47f | ||
|
|
93f60a3ba0 | ||
|
|
2bdb93e51e |
@@ -1540,8 +1540,19 @@ fn cmd_exec_inner(
|
||||
mem.write_u32(addr, block);
|
||||
}
|
||||
("xboxkrnl.exe", 0x01BE) => {
|
||||
// VdGlobalDevice — passed through to Vd* shims. Write 0.
|
||||
mem.write_u32(addr, 0);
|
||||
// VdGlobalDevice — a *pointer to* a global D3D-device cell.
|
||||
// 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) => {
|
||||
// VdGpuClockInMHz
|
||||
@@ -2327,10 +2338,22 @@ fn coord_post_round(
|
||||
}
|
||||
|
||||
if kernel.gpu.has_pending_interrupts() {
|
||||
for _pi in kernel.gpu.take_pending_interrupts() {
|
||||
for pi in kernel.gpu.take_pending_interrupts() {
|
||||
// Canary `ExecutePacketType3_INTERRUPT` dispatches the callback
|
||||
// once per set bit of `cpu_mask` with that bit's index as the
|
||||
// target CPU (`DispatchInterruptCallback(1, n)`). The guest's
|
||||
// swap-acknowledge fence stores `cpu_mask`, and the ISR clears
|
||||
// `1 << current_cpu` from it — so the ISR must run impersonating
|
||||
// the masked CPU or the fence never reaches 0. Sylpheed uses a
|
||||
// single-bit mask (`0x4` → CPU 2); take the lowest set bit.
|
||||
let cpu = if pi.cpu_mask == 0 {
|
||||
xenia_kernel::interrupts::VSYNC_TARGET_CPU
|
||||
} else {
|
||||
pi.cpu_mask.trailing_zeros().min(5) as u8
|
||||
};
|
||||
kernel
|
||||
.interrupts
|
||||
.queue_interrupt(xenia_kernel::INTERRUPT_SOURCE_CP);
|
||||
.queue_interrupt(xenia_kernel::INTERRUPT_SOURCE_CP, cpu);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3534,7 +3557,17 @@ fn dispatch_graphics_interrupts(
|
||||
None
|
||||
};
|
||||
|
||||
/// X_KPCR offset of `prcb_data.current_cpu` (canary `xthread.cc`
|
||||
/// `SetActiveCpu` → `pcr.prcb_data.current_cpu`). The guest graphics
|
||||
/// ISR reads it via `lbz r10, 268(r13)` to decide which per-CPU bit of
|
||||
/// the swap-acknowledge fence to clear.
|
||||
const PCR_CURRENT_CPU_OFF: u32 = 268;
|
||||
|
||||
while let Some(source) = kernel.interrupts.peek_next() {
|
||||
let target_cpu = kernel
|
||||
.interrupts
|
||||
.peek_next_cpu()
|
||||
.unwrap_or(xenia_kernel::interrupts::VSYNC_TARGET_CPU);
|
||||
// Victim selection: Ready first, then Blocked (canary's
|
||||
// `XThread::GetCurrentThread()` analog — any live thread will
|
||||
// do for borrowing context). Skip Idle/Exited/ServicingIrq.
|
||||
@@ -3604,6 +3637,19 @@ fn dispatch_graphics_interrupts(
|
||||
saved
|
||||
};
|
||||
|
||||
// Impersonate the interrupt's target CPU on the borrowed thread's
|
||||
// PCR, mirroring canary `EmulateCPInterruptDPC` →
|
||||
// `XThread::SetActiveCpu(cpu)`. The guest swap-complete ISR clears
|
||||
// `1 << [pcr.current_cpu]` from the per-present swap-acknowledge
|
||||
// fence; if it runs on the wrong CPU it clears the wrong bit and
|
||||
// the GPU's trailing `WAIT_REG_MEM` on that fence never releases —
|
||||
// stranding the present/title loop. Save/restore so borrowing a
|
||||
// thread doesn't permanently rewrite its processor number.
|
||||
let pcr_addr = (kernel.scheduler.ctx_mut_ref(target_ref).gpr[13] as u32)
|
||||
.wrapping_add(PCR_CURRENT_CPU_OFF);
|
||||
let saved_cpu = mem.read_u8(pcr_addr);
|
||||
mem.write_u8(pcr_addr, target_cpu);
|
||||
|
||||
// Stash the previous `scheduler.current` (call_export reaches
|
||||
// it; imports the ISR calls must dispatch on the borrowed
|
||||
// thread). Restore on the way out.
|
||||
@@ -3696,6 +3742,7 @@ fn dispatch_graphics_interrupts(
|
||||
|
||||
// Restore the borrowed context.
|
||||
saved.restore(kernel.scheduler.ctx_mut_ref(target_ref));
|
||||
mem.write_u8(pcr_addr, saved_cpu);
|
||||
kernel.scheduler.current = prev_current;
|
||||
kernel.interrupts.delivered += 1;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"instructions": 50000000,
|
||||
"imports": 339766,
|
||||
"instructions": 50000014,
|
||||
"imports": 352251,
|
||||
"unimpl": 0,
|
||||
"draws": 0,
|
||||
"swaps": 2,
|
||||
"unique_render_targets": 0,
|
||||
"shader_blobs_live": 0,
|
||||
"draws": 718,
|
||||
"swaps": 147,
|
||||
"unique_render_targets": 2,
|
||||
"shader_blobs_live": 6,
|
||||
"texture_cache_entries": 0
|
||||
}
|
||||
|
||||
@@ -57,6 +57,16 @@ fn run_oracle(label: &str, max_instr: u64, golden_rel: &str) {
|
||||
&iso,
|
||||
"-n",
|
||||
&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",
|
||||
"--expect",
|
||||
&golden_str,
|
||||
|
||||
@@ -28,6 +28,80 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Max guest page-version over the `[base, base+len)` span, walking 4 KiB
|
||||
/// pages via the `MemoryAccess` trait's `page_version`.
|
||||
///
|
||||
/// The concrete heap exposes an inherent `max_page_version(base, len)`, but
|
||||
/// the draw handler only holds `&dyn MemoryAccess` (which carries the coarser
|
||||
/// `page_version(addr)` accessor). This is byte-equivalent to
|
||||
/// `heap::max_page_version` and stays a pure function of the per-page write
|
||||
/// counters (no wall-clock), so texture-decode timing remains deterministic.
|
||||
fn span_max_version(mem: &dyn MemoryAccess, base: u32, len: u32) -> u64 {
|
||||
const PAGE: u32 = 0x1000;
|
||||
let last = base.saturating_add(len.saturating_sub(1));
|
||||
let mut page = base & !(PAGE - 1);
|
||||
let last_page = last & !(PAGE - 1);
|
||||
let mut max = 0u64;
|
||||
loop {
|
||||
max = max.max(mem.page_version(page));
|
||||
if page >= last_page {
|
||||
break;
|
||||
}
|
||||
page = page.wrapping_add(PAGE);
|
||||
}
|
||||
max
|
||||
}
|
||||
|
||||
/// Cached Xenos microcode blob, produced by `PM4_IM_LOAD*` packets.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShaderBlob {
|
||||
@@ -58,21 +132,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)<<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 {
|
||||
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 +175,7 @@ impl WaitCmp {
|
||||
WaitCmp::GreaterEq => value >= reference,
|
||||
WaitCmp::Greater => value > reference,
|
||||
WaitCmp::Always => true,
|
||||
WaitCmp::Never => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,6 +424,12 @@ pub struct GpuSystem {
|
||||
/// on every texture-fetch resolution; the UI thread sees the decoded
|
||||
/// bytes via `UiBridge::publish_texture`.
|
||||
pub texture_cache: crate::texture_cache::TextureCache,
|
||||
/// P5b: textures decoded at the most recent `PM4_DRAW_INDX*`, keyed off
|
||||
/// the *active* pixel shader's real `tfetch` fetch-constant slots (not a
|
||||
/// hardcoded slot). `vd_swap` publishes the first of these to the UI so
|
||||
/// the replay binds the texture the draw actually samples. Cleared and
|
||||
/// repopulated each draw; empty when the active PS issues no `tfetch`.
|
||||
pub last_draw_textures: Vec<(crate::texture_cache::TextureKey, Vec<u8>)>,
|
||||
/// 10 MiB shadow of the Xenos EDRAM. Written by clear-resolves and
|
||||
/// (future) host-render-target readback; read by the resolve byte-copy
|
||||
/// path that writes tiled pixels into guest memory. Allocated once at
|
||||
@@ -364,6 +461,7 @@ impl GpuSystem {
|
||||
rt_cache: crate::render_target_cache::RenderTargetCache::new(),
|
||||
last_resolve: None,
|
||||
texture_cache: crate::texture_cache::TextureCache::new(),
|
||||
last_draw_textures: Vec::new(),
|
||||
edram: crate::edram::ShadowEdram::new(),
|
||||
}
|
||||
}
|
||||
@@ -536,14 +634,21 @@ impl GpuSystem {
|
||||
/// Release.
|
||||
pub fn sync_with_mmio(&mut self) {
|
||||
let wptr_dwords = self.mmio.cp_rb_wptr.load(Ordering::Acquire);
|
||||
if wptr_dwords != self.ring.write_offset_dwords && self.ring.size_dwords != 0 {
|
||||
self.ring.write_offset_dwords = wptr_dwords % self.ring.size_dwords;
|
||||
// CP_RB_WPTR governs ONLY the primary ring. While an indirect buffer
|
||||
// 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).
|
||||
self.mmio
|
||||
.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.
|
||||
@@ -551,7 +656,11 @@ impl GpuSystem {
|
||||
if let Some(block) = &self.pending_block {
|
||||
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
|
||||
@@ -561,6 +670,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;
|
||||
@@ -642,10 +757,13 @@ impl GpuSystem {
|
||||
width,
|
||||
height,
|
||||
});
|
||||
self.pending_interrupts.push(PendingInterrupt {
|
||||
source: InterruptSource::Swap,
|
||||
cpu_mask: 0x1,
|
||||
});
|
||||
// iterate-2T: do NOT raise a CP swap-complete interrupt here. Canary's
|
||||
// `VdSwap`/PM4_XE_SWAP path raises no interrupt; swap-complete CP
|
||||
// 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!(
|
||||
frame = self.swap_counter,
|
||||
fb = format_args!("{frontbuffer_phys:#010x}"),
|
||||
@@ -657,9 +775,21 @@ 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);
|
||||
// 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.size_dwords = size_bytes / 4;
|
||||
self.ring.indirect = false;
|
||||
self.ring.read_offset_dwords = 0;
|
||||
// `write_offset` is driven by the guest — start at 0 so the ring
|
||||
// appears empty until MMIO writes advance it.
|
||||
@@ -675,6 +805,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 +858,58 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
if self.ring.rptr_writeback_addr != 0 && self.ring.is_initialized() {
|
||||
mem.write_u32_fence(
|
||||
@@ -748,6 +934,7 @@ impl GpuSystem {
|
||||
let value = mem.read_u32(dword_addr);
|
||||
let target = if write_one { base_index } else { base_index + i };
|
||||
self.register_file.write(target, value);
|
||||
self.scratch_register_writeback(mem, target, value);
|
||||
}
|
||||
tracing::trace!(
|
||||
base = format_args!("{base_index:#x}"),
|
||||
@@ -770,6 +957,8 @@ impl GpuSystem {
|
||||
let b = mem.read_u32(b_addr);
|
||||
self.register_file.write(reg_index_1, a);
|
||||
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!(
|
||||
r1 = format_args!("{reg_index_1:#x}"),
|
||||
r2 = format_args!("{reg_index_2:#x}"),
|
||||
@@ -816,7 +1005,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.
|
||||
@@ -832,6 +1023,10 @@ impl GpuSystem {
|
||||
write_offset_dwords: ib_size, // IB is fully-written at jump time
|
||||
rptr_writeback_addr: 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!(
|
||||
ib_ptr = format_args!("{ib_ptr:#010x}"),
|
||||
@@ -854,7 +1049,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 +1061,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 +1110,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 +1122,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 +1138,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 +1148,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 +1167,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 +1195,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);
|
||||
@@ -1094,6 +1296,60 @@ impl GpuSystem {
|
||||
);
|
||||
self.last_draw = Some(ds);
|
||||
self.last_primitive = Some(processed);
|
||||
|
||||
// P5b: decode the textures the *active pixel shader* actually
|
||||
// samples. Parse the bound PS, collect its `tfetch`
|
||||
// fetch-constant slots, read each 6-dword fetch constant from
|
||||
// the register file, and decode+cache it. `vd_swap` publishes
|
||||
// the result. Empty for flat (no-tfetch) shaders — the
|
||||
// dominant case on Sylpheed's current splash, where this stays
|
||||
// inert until the textured logo draw is reached.
|
||||
self.last_draw_textures.clear();
|
||||
if let Some(ps_key) = self.active_ps_key {
|
||||
// Collect slots under an immutable borrow of `shader_blobs`,
|
||||
// then drop it before mutating `texture_cache`.
|
||||
let slots: Vec<u8> = match self.shader_blobs.get(&ps_key) {
|
||||
Some(blob) => {
|
||||
let parsed = crate::ucode::parse_shader(&blob.dwords);
|
||||
crate::shader_metrics::tfetch_slots(&parsed)
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
for slot in slots {
|
||||
let mut fetch6 = [0u32; 6];
|
||||
for (k, w) in fetch6.iter_mut().enumerate() {
|
||||
*w = self
|
||||
.register_file
|
||||
.read(CONST_BASE_FETCH + slot as u32 * 6 + k as u32);
|
||||
}
|
||||
let Some(key) = crate::texture_cache::decode_fetch_constant(fetch6) else {
|
||||
continue;
|
||||
};
|
||||
let bi = key.format.block_info();
|
||||
let span_bytes = (key.pitch_texels as u32)
|
||||
* (key.height as u32)
|
||||
* (bi.bytes_per_block as u32)
|
||||
/ (bi.block_w as u32);
|
||||
let version = span_max_version(mem, key.base_address, span_bytes.max(4));
|
||||
match self.texture_cache.ensure_cached(key, version, mem) {
|
||||
Ok(entry) => {
|
||||
self.last_draw_textures.push((entry.key, entry.bytes.clone()));
|
||||
metrics::counter!(
|
||||
"gpu.texture.decode",
|
||||
"fmt" => format!("{:?}", key.format),
|
||||
)
|
||||
.increment(1);
|
||||
}
|
||||
Err(e) => {
|
||||
metrics::counter!(
|
||||
"gpu.texture.reject",
|
||||
"reason" => format!("{e:?}"),
|
||||
)
|
||||
.increment(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pm4::PM4_SET_CONSTANT | pm4::PM4_SET_SHADER_CONSTANTS => {
|
||||
// payload[0] = offset_type — bits[10:0] index, bits[23:16] type
|
||||
@@ -1123,7 +1379,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 +1411,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));
|
||||
@@ -1373,11 +1629,31 @@ pub mod reg {
|
||||
/// `XE_GPU_REG_D1MODE_VBLANK_VLINE_STATUS` (Canary register_table.inc:1126).
|
||||
/// Bit 0 = VBLANK_INT_OCCURRED.
|
||||
pub const D1MODE_VBLANK_VLINE_STATUS: u32 = 0x1951;
|
||||
/// `XE_GPU_REG_D1MODE_VIEWPORT_SIZE` / `AVIVO_D1MODE_VIEWPORT_SIZE`
|
||||
/// (Canary `register_table.inc:1134`). Packs the active display resolution
|
||||
/// as `(width << 16) | height` with 12-bit fields. The guest's
|
||||
/// swap-complete interrupt callback (`sub_824CE2B8`) divides by the low
|
||||
/// 12 bits (`height`) as a refresh-pacing term, so a 0 read makes its
|
||||
/// `twi` divide-by-zero guard trap and abort the ISR before it clears the
|
||||
/// swap-acknowledge fence. Canary returns the constant below from
|
||||
/// `GraphicsSystem::ReadRegister` (graphics_system.cc:311).
|
||||
pub const D1MODE_VIEWPORT_SIZE: u32 = 0x1961;
|
||||
/// `XE_GPU_REG_VGT_EVENT_INITIATOR` — set by EVENT_WRITE.
|
||||
pub const VGT_EVENT_INITIATOR: u32 = 0x21F9;
|
||||
/// `XE_GPU_REG_COHER_STATUS_HOST` — coherency bits
|
||||
/// (Canary `register_table.inc:530`).
|
||||
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
|
||||
@@ -1468,6 +1744,38 @@ mod tests {
|
||||
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]
|
||||
fn wait_reg_mem_blocks_then_unblocks_when_mem_changes() {
|
||||
let mut gpu = GpuSystem::new();
|
||||
@@ -1477,8 +1785,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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -58,6 +58,15 @@ pub fn build_region(mmio: &GpuMmio) -> MmioRegion {
|
||||
reg::D1MODE_VBLANK_VLINE_STATUS => {
|
||||
read_vblank_status.load(Ordering::Relaxed)
|
||||
}
|
||||
// AVIVO_D1MODE_VIEWPORT_SIZE: the active display resolution
|
||||
// (1280x720) packed as `(width << 16) | height`. Canary
|
||||
// serves this constant from `GraphicsSystem::ReadRegister`
|
||||
// (graphics_system.cc:311). The guest swap-complete interrupt
|
||||
// callback divides by the low 12 bits (`height = 0x2D0`); a 0
|
||||
// read trips its `twi` divide-guard and aborts the ISR before
|
||||
// it acknowledges the per-present swap fence — which strands
|
||||
// the present/title loop. Mirror canary exactly.
|
||||
reg::D1MODE_VIEWPORT_SIZE => 0x0500_02D0,
|
||||
_ => {
|
||||
tracing::trace!(
|
||||
reg = format_args!("{reg_index:#x}"),
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
//! rectangles) we rewrite indices on the CPU side so the host just sees a
|
||||
//! triangle list. Ground truth: `xenia-canary/src/xenia/gpu/primitive_processor.h/cc`.
|
||||
//!
|
||||
//! P3 scope: only the shapes Sylpheed's UI + early gameplay paths need
|
||||
//! (list, strip, fan). Rectangle + quad expansions are stubs logged via
|
||||
//! `tracing::warn!` for later.
|
||||
//! Scope: list, strip, fan, quad, and rectangle expansions are all handled
|
||||
//! (rectangles via CPU triangle-list rewrite — see `expand_rectangles`).
|
||||
|
||||
use crate::draw_state::{IndexSize, PrimitiveType};
|
||||
|
||||
@@ -138,18 +137,43 @@ fn expand_quads(indices: Option<&[u32]>, vertex_count: u32) -> ProcessedPrimitiv
|
||||
}
|
||||
|
||||
/// Rectangle lists: a Xenos-specific primitive where each group of 3
|
||||
/// vertices defines a right-angle rectangle by its three non-repeated
|
||||
/// corners (the 4th is derived). The uber-shader doesn't support this yet;
|
||||
/// the ucode translator will emulate it as a geometry-stage fake. For P3
|
||||
/// we emit an empty draw.
|
||||
fn expand_rectangles(_indices: Option<&[u32]>, _vertex_count: u32) -> ProcessedPrimitive {
|
||||
tracing::warn!("gpu: rectangle list primitive not yet implemented (P3 stub)");
|
||||
metrics::counter!("gpu.primitive.rejected", "reason" => "rectangle_list").increment(1);
|
||||
/// vertices defines a rectangle; the 4th corner is extrapolated as
|
||||
/// `v3 = v0 + v2 - v1` (parallelogram completion). Canary expands this in a
|
||||
/// host vertex-shader variant (`kRectangleListAsTriangleStrip`,
|
||||
/// `primitive_processor.cc:389-456`): a 4-vertex triangle strip per rect with
|
||||
/// the 4th corner synthesized *in the VS* from the host-vertex index.
|
||||
///
|
||||
/// Our replay pipeline has no host-VS corner synthesis (and the procedural
|
||||
/// `vs_main` does not consume `rewritten_indices` yet), so we mirror the
|
||||
/// `expand_quads`/`expand_fan` CPU idiom and emit the 3 real vertices of each
|
||||
/// rect as one triangle list `(v0,v1,v2)` — the visible lower half of the
|
||||
/// rect. This un-rejects the draw and gives a faithful `host_vertex_count`.
|
||||
///
|
||||
/// TODO: once `vs_main` does real vertex fetch + interpolation, upgrade to the
|
||||
/// full quad — 6 indices `[v0,v1,v2, v2,v1,v3]` with a synthesized `v3` corner
|
||||
/// — mirroring canary's `kRectangleListAsTriangleStrip`.
|
||||
fn expand_rectangles(indices: Option<&[u32]>, vertex_count: u32) -> ProcessedPrimitive {
|
||||
let rect_count = vertex_count / 3;
|
||||
let mut out = Vec::with_capacity(3 * rect_count as usize);
|
||||
let get = |i: u32| -> u32 {
|
||||
match indices {
|
||||
Some(buf) => buf[i as usize],
|
||||
None => i,
|
||||
}
|
||||
};
|
||||
for r in 0..rect_count {
|
||||
let base = r * 3;
|
||||
out.push(get(base));
|
||||
out.push(get(base + 1));
|
||||
out.push(get(base + 2));
|
||||
}
|
||||
let host_vertex_count = out.len() as u32;
|
||||
metrics::counter!("gpu.primitive.expanded", "shape" => "rectangle_list").increment(1);
|
||||
ProcessedPrimitive {
|
||||
topology: HostTopology::TriangleList,
|
||||
rewritten_indices: Some(Vec::new()),
|
||||
host_vertex_count: 0,
|
||||
rejected: true,
|
||||
rewritten_indices: Some(out),
|
||||
host_vertex_count,
|
||||
rejected: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +237,17 @@ mod tests {
|
||||
assert_eq!(idx, vec![0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rectangle_list_expansion() {
|
||||
// 2 rects (6 verts) → one triangle (v0,v1,v2) per rect, not rejected.
|
||||
let p = process(PrimitiveType::RectangleList, 6, None);
|
||||
let idx = p.rewritten_indices.unwrap();
|
||||
assert_eq!(idx, vec![0, 1, 2, 3, 4, 5]);
|
||||
assert_eq!(p.topology, HostTopology::TriangleList);
|
||||
assert_eq!(p.host_vertex_count, 6);
|
||||
assert!(!p.rejected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widen_u16_indices_big_endian() {
|
||||
// 3 indices [1, 2, 0x1234] in BE u16.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -32,6 +32,16 @@ pub struct RingBufferView {
|
||||
/// `VdEnableRingBufferRPtrWriteBack`). We always write back eagerly, so
|
||||
/// we don't actually use this for scheduling — kept for observability.
|
||||
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 {
|
||||
@@ -46,7 +56,16 @@ impl RingBufferView {
|
||||
|
||||
/// True if there is pending unread data to consume.
|
||||
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.
|
||||
@@ -54,7 +73,10 @@ impl RingBufferView {
|
||||
if !self.is_initialized() {
|
||||
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
|
||||
} else {
|
||||
// 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) {
|
||||
if self.size_dwords == 0 {
|
||||
return;
|
||||
}
|
||||
self.read_offset_dwords =
|
||||
(self.read_offset_dwords + dwords) % self.size_dwords;
|
||||
if self.indirect {
|
||||
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
|
||||
@@ -77,7 +105,11 @@ impl RingBufferView {
|
||||
if !self.is_initialized() {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
@@ -120,4 +152,52 @@ mod tests {
|
||||
assert_eq!(v.addr_at_offset(1), Some(0x4000_0000));
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,49 @@ pub fn emit_for(parsed: &ParsedShader, stage: &'static str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect the unique texture-fetch-constant slot indices a shader samples.
|
||||
///
|
||||
/// Walks the same exec-clause / sequence-bitmap path as [`emit_for`] but only
|
||||
/// extracts `TextureFetch.fetch_const` slots, deduplicated and in first-seen
|
||||
/// order. The GPU draw handler uses this to decide which fetch constants to
|
||||
/// decode + cache at draw time (keyed off the *active* pixel shader's real
|
||||
/// `tfetch` instructions rather than a hardcoded slot).
|
||||
pub fn tfetch_slots(parsed: &ParsedShader) -> Vec<u8> {
|
||||
let mut slots: Vec<u8> = Vec::new();
|
||||
for clause in &parsed.cf {
|
||||
if let ControlFlowInstruction::Exec {
|
||||
address,
|
||||
count,
|
||||
sequence,
|
||||
..
|
||||
} = clause
|
||||
{
|
||||
for i in 0..(*count as usize) {
|
||||
let base = (*address as usize + i) * 3;
|
||||
if base + 2 >= parsed.instructions.len() {
|
||||
break;
|
||||
}
|
||||
// sequence bit layout: 2 bits per triple, hi bit = is-fetch.
|
||||
let is_fetch = ((sequence >> (i * 2 + 1)) & 1) != 0;
|
||||
if !is_fetch {
|
||||
continue;
|
||||
}
|
||||
let words = [
|
||||
parsed.instructions[base],
|
||||
parsed.instructions[base + 1],
|
||||
parsed.instructions[base + 2],
|
||||
];
|
||||
if let FetchInstruction::Texture(tf) = decode_fetch(words) {
|
||||
if !slots.contains(&tf.fetch_const) {
|
||||
slots.push(tf.fetch_const);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
slots
|
||||
}
|
||||
|
||||
fn mark_feature(buf: &mut Vec<&'static str>, name: &'static str) {
|
||||
if !buf.contains(&name) {
|
||||
buf.push(name);
|
||||
@@ -298,6 +341,46 @@ mod tests {
|
||||
emit_for(&shader, "vs");
|
||||
}
|
||||
|
||||
/// `tfetch_slots` should extract the fetch-constant slot of a texture
|
||||
/// fetch (and dedup), and return empty for a flat ALU-only shader.
|
||||
#[test]
|
||||
fn tfetch_slots_extracts_texture_fetch_constants() {
|
||||
// word0: opcode TEXTURE_FETCH (0x01) in low 5 bits, fetch_const=3 in
|
||||
// bits[9:5] → 0x01 | (3 << 5) = 0x61.
|
||||
let tfetch_w0: u32 = 0x01 | (3u32 << 5);
|
||||
let shader = ParsedShader {
|
||||
cf: vec![
|
||||
ControlFlowInstruction::Exec {
|
||||
address: 0,
|
||||
count: 2,
|
||||
// triple 0 is a fetch (hi bit of its 2-bit field set),
|
||||
// triple 1 is ALU. is_fetch = (sequence >> (i*2+1)) & 1.
|
||||
sequence: 0b00_10,
|
||||
is_end: false,
|
||||
predicated: false,
|
||||
predicate_condition: false,
|
||||
},
|
||||
ControlFlowInstruction::Exit,
|
||||
],
|
||||
instructions: vec![tfetch_w0, 0, 0, /* ALU triple */ 0, 0, 0],
|
||||
};
|
||||
assert_eq!(tfetch_slots(&shader), vec![3]);
|
||||
|
||||
// Flat shader: no fetch bits → no slots.
|
||||
let flat = ParsedShader {
|
||||
cf: vec![ControlFlowInstruction::Exec {
|
||||
address: 0,
|
||||
count: 1,
|
||||
sequence: 0,
|
||||
is_end: false,
|
||||
predicated: false,
|
||||
predicate_condition: false,
|
||||
}],
|
||||
instructions: vec![0, 0, 0],
|
||||
};
|
||||
assert!(tfetch_slots(&flat).is_empty());
|
||||
}
|
||||
|
||||
/// P8: a shader containing `LoopStart` should mark `cf_loop` as used
|
||||
/// so the HUD can surface which deferred feature a game triggers.
|
||||
#[test]
|
||||
|
||||
@@ -1652,6 +1652,79 @@ fn nt_set_information_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut
|
||||
return;
|
||||
}
|
||||
|
||||
// XFileRenameInformation (10): move the backing file to a new path.
|
||||
// Sylpheed's asset-cache decompresses each packed resource to a staging
|
||||
// `cache:\<hash><tail>.tmp` then renames it into its final nested path
|
||||
// `cache:\<hash>\<dir>\<file>`. Without an actual host-FS rename the
|
||||
// nested target stays empty, the later read-back of the decompressed
|
||||
// asset (e.g. the title logo texture `\69d8e45c\e\534ffea`) misses, and
|
||||
// the logo never loads. Mirror canary `xboxkrnl_io_info.cc:226`
|
||||
// (`X_FILE_RENAME_INFORMATION{ replace_existing@0, root_dir_handle@4,
|
||||
// ansi_string@8 }` → `file->Rename(TranslateAnsiPath(ansi_string))`).
|
||||
if info_class == 10 {
|
||||
// Read the target path from the embedded ANSI_STRING at info_ptr+8.
|
||||
let target_raw = match crate::path::read_ansi_string(mem, info_ptr + 8) {
|
||||
Some(s) if !s.is_empty() => s,
|
||||
_ => {
|
||||
const STATUS_OBJECT_NAME_INVALID: u64 = 0xC000_0033;
|
||||
ctx.gpr[3] = STATUS_OBJECT_NAME_INVALID;
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Resolve the destination against the host cache backing dir. We only
|
||||
// support renames within the writable `cache:` mount (the only place
|
||||
// a guest can create files); disc/synth entries are read-only.
|
||||
let new_host = state.resolve_cache_path(&target_raw);
|
||||
// Current backing host path of the handle.
|
||||
let old_host = match state.objects.get(&handle) {
|
||||
Some(KernelObject::File { host_path: Some(hp), .. }) => Some(hp.clone()),
|
||||
Some(KernelObject::File { .. }) => None,
|
||||
_ => {
|
||||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let status: u64 = match (old_host, new_host) {
|
||||
(Some(old), Some(new)) => {
|
||||
if let Some(parent) = new.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
match std::fs::rename(&old, &new) {
|
||||
Ok(()) => {
|
||||
// Update the handle so subsequent I/O targets the new
|
||||
// host path + guest path.
|
||||
if let Some(KernelObject::File { path, host_path, .. }) =
|
||||
state.objects.get_mut(&handle)
|
||||
{
|
||||
*path = crate::path::normalize_path(&target_raw);
|
||||
*host_path = Some(new.clone());
|
||||
}
|
||||
tracing::info!(
|
||||
"NtSetInformationFile rename cache {:?} -> {:?} ({:?})",
|
||||
old, new, target_raw
|
||||
);
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"NtSetInformationFile rename {:?} -> {:?} failed: {}",
|
||||
old, new, e
|
||||
);
|
||||
STATUS_UNSUCCESSFUL
|
||||
}
|
||||
}
|
||||
}
|
||||
// Non-cache (read-only VFS) source/target: acknowledge without a
|
||||
// host move, matching the prior permissive behaviour.
|
||||
_ => STATUS_SUCCESS,
|
||||
};
|
||||
if iosb_ptr != 0 {
|
||||
write_io_status_block(mem, iosb_ptr, status as u32, info_length);
|
||||
}
|
||||
ctx.gpr[3] = status;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle lookup.
|
||||
let Some(KernelObject::File { size, position, host_path, .. }) = state.objects.get_mut(&handle) else {
|
||||
ctx.gpr[3] = STATUS_INVALID_HANDLE;
|
||||
@@ -2883,10 +2956,12 @@ fn vd_initialize_ring_buffer(ctx: &mut PpcContext, _mem: &GuestMemory, state: &m
|
||||
// packets directly into ring memory at the current WPTR (the GPU
|
||||
// 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).
|
||||
// Per canary: size_log2 is log2(size in BYTES), so size in dwords =
|
||||
// 2^size_log2 / 4 = 1 << (size_log2 - 2).
|
||||
// Per canary `CommandProcessor::InitializeRingBuffer`: the ring is
|
||||
// `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_size_dwords = if size_log2 >= 2 { 1u32 << (size_log2 - 2) } else { 0 };
|
||||
state.ring_size_dwords = 1u32 << (size_log2 + 1);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
@@ -2997,52 +3072,86 @@ fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||
// xboxkrnl_video.cc:479. Currently skipped (see below).
|
||||
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
|
||||
// buffer) and bumped WPTR by 64 to expose the game's own ring writes.
|
||||
// Keep that untouched — the game still expects buffer_ptr to be a
|
||||
// skippable scratch area, and the bump still exposes any game-batched
|
||||
// PM4 packets for the drain.
|
||||
// iterate-2V: mirror xenia-canary `VdSwap_entry` (xboxkrnl_video.cc:518-548)
|
||||
// FAITHFULLY. The game reserves 64 dwords (256 bytes) in the primary ring
|
||||
// at `buffer_ptr`; canary writes a `PM4_TYPE0(SHADER_CONSTANT_FETCH_00_0)`
|
||||
// fetch-constant patch followed by `PM4_TYPE3(PM4_XE_SWAP)`, then pads with
|
||||
// NOPs — and **NEVER touches `CP_RB_WPTR`**. The game advances the primary
|
||||
// ring write-pointer itself via its own doorbell once it has finished
|
||||
// populating the reserved slot, so VdSwap only fills the bytes.
|
||||
//
|
||||
// iterate-2V FIX (the bug this removes): a prior revision bumped the
|
||||
// primary ring `CP_RB_WPTR` out-of-band here (`extend_write_ptr_by(64)`).
|
||||
// But `buffer_ptr` (~0x4add6efc) is NOT inside the primary ring (base
|
||||
// ~0x4adcd000, 8192 dwords) — it lives ~10k dwords past it, in the
|
||||
// renderer indirect-buffer region. The bogus WPTR bump pushed the GPU
|
||||
// read-pointer PAST the guest's real write-pointer, the drain treated the
|
||||
// overshoot as a circular wrap, and **re-executed the splash's draw
|
||||
// indirect-buffers ~2×** — inflating draws to 78 (real splash ≈ 28; 12
|
||||
// INDIRECT_BUFFERs vs the real 6). Canary's `VdSwap_entry` writes the
|
||||
// block and returns; the swap-complete CP interrupt comes only from the
|
||||
// game's own in-stream `PM4_INTERRUPT` packets, never from VdSwap.
|
||||
if buffer_ptr != 0 {
|
||||
for i in 0..64u32 {
|
||||
mem.write_u32(buffer_ptr + i * 4, xenia_gpu::pm4::make_packet_type2());
|
||||
let mut off = 0u32;
|
||||
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);
|
||||
// NOTE: We deliberately do NOT bump `CP_RB_WPTR` here (see the iterate-2V
|
||||
// comment above). The drain below consumes only the packets the game has
|
||||
// legitimately advanced the write-pointer over.
|
||||
|
||||
// GPUBUG-DRAIN-001: notify the swap directly.
|
||||
//
|
||||
// Per xenia-canary `VdSwap_entry` (xboxkrnl_video.cc:438-521), the
|
||||
// textbook approach is to inject `PM4_TYPE0(SHADER_CONSTANT_FETCH_00_0)`
|
||||
// (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.
|
||||
// Drain the ring up to whatever the game has actually submitted; any
|
||||
// in-stream `PM4_INTERRUPT` / draw packets execute in order. The
|
||||
// reserved-slot PM4_XE_SWAP is consumed by the GPU only once the game
|
||||
// advances its own doorbell over it. The swap-counter safety net below
|
||||
// keeps host swap bookkeeping live in the meantime.
|
||||
let drained = state.gpu.drain_to_current_wptr(mem);
|
||||
tracing::debug!(drained, "VdSwap: drained PM4 packets");
|
||||
|
||||
// Direct swap notification. Inline mode bumps `swaps_seen`
|
||||
// synchronously; threaded mode posts a `GpuCommand::NotifyXeSwap`
|
||||
// and the worker bumps it asynchronously.
|
||||
// Safety net: if the drain did NOT reach our PM4_XE_SWAP this call (e.g.
|
||||
// an undersized inline deadline left game-batched packets pending), still
|
||||
// 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 {
|
||||
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,
|
||||
@@ -3080,27 +3189,27 @@ fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||
);
|
||||
ui.publish_assets(blobs, constants);
|
||||
|
||||
// P5: try to decode the primary texture (fetch constant slot 0).
|
||||
// Slot 0 is the convention most games use for their main bound
|
||||
// texture at draw time; full N-slot binding waits for P6+. If the
|
||||
// slot is unset or the format isn't supported (magenta stub kicks
|
||||
// in host-side), we skip.
|
||||
//
|
||||
// Texture fetch constants live at `CONST_BASE_FETCH + slot*6` in
|
||||
// the register file; we read the 6 dwords, decode the key, hit
|
||||
// the CPU cache (with page-version freshness), and clone the
|
||||
// decoded bytes across the bridge.
|
||||
const TEX_SLOT: u32 = 0;
|
||||
let mut fetch6 = [0u32; 6];
|
||||
for (i, slot) in fetch6.iter_mut().enumerate() {
|
||||
*slot = gpu_inline
|
||||
.register_file
|
||||
.read(xenia_gpu::gpu_system::CONST_BASE_FETCH + TEX_SLOT * 6 + i as u32);
|
||||
}
|
||||
let published = if let Some(key) = xenia_gpu::texture_cache::decode_fetch_constant(fetch6)
|
||||
{
|
||||
// Span over the entire tiled texture footprint to pick the
|
||||
// max page version covering it.
|
||||
// P5b: publish the texture the last draw's *active pixel shader*
|
||||
// actually sampled. The GPU draw handler decodes the PS's real
|
||||
// `tfetch` fetch-constant slots into `last_draw_textures`; we publish
|
||||
// the first (the UI binds a single texture today). When the last draw
|
||||
// used a flat (no-tfetch) shader the list is empty, so we fall back to
|
||||
// the legacy slot-0 probe to preserve behavior on flat-only frames.
|
||||
let published = gpu_inline.last_draw_textures.first().cloned().or_else(|| {
|
||||
// Fallback: probe fetch constant slot 0 directly. Texture fetch
|
||||
// constants live at `CONST_BASE_FETCH + slot*6` in the register
|
||||
// file; read 6 dwords, decode the key, hit the CPU cache with
|
||||
// page-version freshness, clone the bytes across the bridge.
|
||||
const TEX_SLOT: u32 = 0;
|
||||
let mut fetch6 = [0u32; 6];
|
||||
for (i, slot) in fetch6.iter_mut().enumerate() {
|
||||
*slot = gpu_inline
|
||||
.register_file
|
||||
.read(xenia_gpu::gpu_system::CONST_BASE_FETCH + TEX_SLOT * 6 + i as u32);
|
||||
}
|
||||
let key = xenia_gpu::texture_cache::decode_fetch_constant(fetch6)?;
|
||||
// Span over the entire tiled texture footprint to pick the max
|
||||
// page version covering it.
|
||||
let bi = key.format.block_info();
|
||||
let span_bytes = (key.pitch_texels as u32)
|
||||
* (key.height as u32)
|
||||
@@ -3118,9 +3227,7 @@ fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
});
|
||||
metrics::gauge!("gpu.texture_cache.entries")
|
||||
.set(gpu_inline.texture_cache.len() as f64);
|
||||
ui.publish_texture(published);
|
||||
@@ -3169,13 +3276,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;
|
||||
}
|
||||
@@ -5542,6 +5654,67 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// `NtSetInformationFile` class 10 (`XFileRenameInformation`) must move
|
||||
/// the backing host file to the new `cache:` path and update the handle.
|
||||
/// Mirrors Sylpheed's asset-cache `.tmp` → `\<hash>\<dir>\<file>` move;
|
||||
/// without it the nested target stays empty and the decompressed asset
|
||||
/// (logo texture) never reads back. Faithful to canary `file->Rename`.
|
||||
#[test]
|
||||
fn nt_set_information_file_rename_moves_cache_file() {
|
||||
let (mut ctx, mut mem, mut state) = fresh();
|
||||
// Real temp cache root + a staging `.tmp` file with known bytes.
|
||||
let root = std::env::temp_dir().join(format!("xenia-rs-rename-test-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let old_host = root.join("69d8e45ce534ffea.tmp");
|
||||
std::fs::write(&old_host, b"LOGOTEX!").unwrap();
|
||||
state.cache_root = Some(root.clone());
|
||||
// Open handle whose backing host_path is the staging file.
|
||||
let handle = state.alloc_handle_for(KernelObject::File {
|
||||
path: "69d8e45ce534ffea.tmp".to_string(),
|
||||
size: 8,
|
||||
position: 0,
|
||||
data: Arc::new(Vec::new()),
|
||||
dir_enum_pos: None,
|
||||
host_path: Some(old_host.clone()),
|
||||
});
|
||||
// X_FILE_RENAME_INFORMATION { replace@0, root_dir@4, ANSI_STRING@8 }.
|
||||
// ANSI_STRING { len u16, max u16, buf u32 } at info_ptr+8; buffer holds
|
||||
// the target path "cache:\69d8e45c\e\534ffea".
|
||||
let info_ptr = SCRATCH_BASE + 0x100;
|
||||
let str_buf = SCRATCH_BASE + 0x200;
|
||||
let target = b"cache:\\69d8e45c\\e\\534ffea";
|
||||
for (i, b) in target.iter().enumerate() {
|
||||
mem.write_u8(str_buf + i as u32, *b);
|
||||
}
|
||||
mem.write_u32(info_ptr, 0); // replace_existing
|
||||
mem.write_u32(info_ptr + 4, 0); // root_dir_handle
|
||||
mem.write_u16(info_ptr + 8, target.len() as u16); // ANSI_STRING.Length
|
||||
mem.write_u16(info_ptr + 10, target.len() as u16); // MaximumLength
|
||||
mem.write_u32(info_ptr + 12, str_buf); // Buffer
|
||||
let iosb_ptr = SCRATCH_BASE + 0x140;
|
||||
ctx.gpr[3] = handle as u64;
|
||||
ctx.gpr[4] = iosb_ptr as u64;
|
||||
ctx.gpr[5] = info_ptr as u64;
|
||||
ctx.gpr[6] = 16;
|
||||
ctx.gpr[7] = 10; // XFileRenameInformation
|
||||
nt_set_information_file(&mut ctx, &mut mem, &mut state);
|
||||
assert_eq!(ctx.gpr[3], STATUS_SUCCESS);
|
||||
// Staging file gone; nested target exists with the same bytes.
|
||||
let new_host = root.join("69d8e45c").join("e").join("534ffea");
|
||||
assert!(!old_host.exists(), "staging .tmp should be moved away");
|
||||
assert_eq!(std::fs::read(&new_host).unwrap(), b"LOGOTEX!");
|
||||
// Handle now points at the new host + guest path.
|
||||
match state.objects.get(&handle) {
|
||||
Some(KernelObject::File { host_path: Some(hp), path, .. }) => {
|
||||
assert_eq!(hp, &new_host);
|
||||
assert_eq!(path, "cache:/69d8e45c/e/534ffea");
|
||||
}
|
||||
_ => panic!("file handle lost or host_path missing"),
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
/// Read-only VFS — truncating to a different size must fail with
|
||||
/// `STATUS_UNSUCCESSFUL`, matching Canary's error path when
|
||||
/// `file->SetLength(...)` can't honour the request.
|
||||
|
||||
@@ -30,6 +30,12 @@ use xenia_cpu::ThreadRef;
|
||||
pub const INTERRUPT_SOURCE_VSYNC: u32 = 0;
|
||||
pub const INTERRUPT_SOURCE_CP: u32 = 1;
|
||||
|
||||
/// The processor the graphics ISR impersonates for a v-sync interrupt.
|
||||
/// Canary hard-codes this: `MarkVblank` → `DispatchInterruptCallback(0, 2)`
|
||||
/// (graphics_system.cc:478). CP interrupts instead use the bit index of the
|
||||
/// `PM4_INTERRUPT` `cpu_mask`.
|
||||
pub const VSYNC_TARGET_CPU: u8 = 2;
|
||||
|
||||
/// Guest-registered V-sync / graphics-interrupt callback (from
|
||||
/// `VdSetGraphicsInterruptCallback`).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -145,9 +151,16 @@ pub type PendingLocalIrq = [std::sync::atomic::AtomicU8;
|
||||
pub struct InterruptState {
|
||||
/// Registered callback (set by `VdSetGraphicsInterruptCallback`).
|
||||
pub callback: Option<GraphicsInterruptCallback>,
|
||||
/// Bounded FIFO of pending interrupt sources awaiting injection.
|
||||
/// Push-back on queue, pop-front on inject. Over-cap pushes drop.
|
||||
pub pending: VecDeque<u32>,
|
||||
/// Bounded FIFO of pending interrupts awaiting injection, as
|
||||
/// `(source, target_cpu)`. Push-back on queue, pop-front on inject.
|
||||
/// Over-cap pushes drop. `target_cpu` is the processor the graphics
|
||||
/// ISR must impersonate (canary `XThread::SetActiveCpu` / the
|
||||
/// `DispatchInterruptCallback(source, cpu)` argument): the bit index
|
||||
/// of the CP `PM4_INTERRUPT` `cpu_mask` for source=1, and a fixed `2`
|
||||
/// for vsync (canary `DispatchInterruptCallback(0, 2)`). The ISR reads
|
||||
/// it from the PCR (`[r13+268]`) to clear the matching per-CPU bit of
|
||||
/// the swap-acknowledge fence.
|
||||
pub pending: VecDeque<(u32, u8)>,
|
||||
/// When `Some`, some HW thread is currently running a callback; on
|
||||
/// return-to-sentinel we restore this and clear the flag.
|
||||
pub saved: Option<SavedCallbackCtx>,
|
||||
@@ -211,8 +224,9 @@ impl InterruptState {
|
||||
});
|
||||
}
|
||||
|
||||
/// Queue an interrupt for the next safe injection point.
|
||||
pub fn queue_interrupt(&mut self, source: u32) {
|
||||
/// Queue an interrupt for the next safe injection point. `cpu` is the
|
||||
/// processor the ISR must impersonate (see `pending`).
|
||||
pub fn queue_interrupt(&mut self, source: u32, cpu: u8) {
|
||||
if self.callback.is_none() {
|
||||
self.dropped += 1;
|
||||
return;
|
||||
@@ -221,18 +235,23 @@ impl InterruptState {
|
||||
self.dropped += 1;
|
||||
return;
|
||||
}
|
||||
self.pending.push_back(source);
|
||||
self.pending.push_back((source, cpu));
|
||||
}
|
||||
|
||||
/// Peek at the next pending source without removing it.
|
||||
pub fn peek_next(&self) -> Option<u32> {
|
||||
self.pending.front().copied()
|
||||
self.pending.front().map(|&(source, _)| source)
|
||||
}
|
||||
|
||||
/// Peek at the target CPU of the next pending interrupt.
|
||||
pub fn peek_next_cpu(&self) -> Option<u8> {
|
||||
self.pending.front().map(|&(_, cpu)| cpu)
|
||||
}
|
||||
|
||||
/// Pop the next pending source (called by the injector after it has
|
||||
/// committed to dispatching it).
|
||||
pub fn take_next(&mut self) -> Option<u32> {
|
||||
self.pending.pop_front()
|
||||
self.pending.pop_front().map(|(source, _)| source)
|
||||
}
|
||||
|
||||
/// **Legacy** — instruction-count v-sync ticker. Kept for unit tests
|
||||
@@ -249,7 +268,7 @@ impl InterruptState {
|
||||
let periods = self.vsync_accumulator / VSYNC_INSTR_PERIOD;
|
||||
self.vsync_accumulator %= VSYNC_INSTR_PERIOD;
|
||||
for _ in 0..periods {
|
||||
self.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||
self.queue_interrupt(INTERRUPT_SOURCE_VSYNC, VSYNC_TARGET_CPU);
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -288,7 +307,7 @@ impl InterruptState {
|
||||
self.last_vsync_instant = Some(anchor + advance);
|
||||
let to_queue = (periods as usize).min(INTERRUPT_QUEUE_CAP);
|
||||
for _ in 0..to_queue {
|
||||
self.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||
self.queue_interrupt(INTERRUPT_SOURCE_VSYNC, VSYNC_TARGET_CPU);
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -306,7 +325,7 @@ mod tests {
|
||||
#[test]
|
||||
fn queue_interrupt_drops_without_callback() {
|
||||
let mut s = InterruptState::default();
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC, VSYNC_TARGET_CPU);
|
||||
assert_eq!(s.dropped, 1);
|
||||
assert!(s.pending.is_empty());
|
||||
}
|
||||
@@ -315,9 +334,9 @@ mod tests {
|
||||
fn queue_interrupt_fifo_preserves_order() {
|
||||
let mut s = InterruptState::default();
|
||||
s.set_callback(0x1000, 0xAB);
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_CP);
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC, VSYNC_TARGET_CPU);
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_CP, 2);
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC, VSYNC_TARGET_CPU);
|
||||
assert_eq!(s.dropped, 0);
|
||||
// FIFO: take_next hands them out in push order.
|
||||
assert_eq!(s.take_next(), Some(INTERRUPT_SOURCE_VSYNC));
|
||||
@@ -331,11 +350,11 @@ mod tests {
|
||||
let mut s = InterruptState::default();
|
||||
s.set_callback(0x1000, 0xAB);
|
||||
for _ in 0..INTERRUPT_QUEUE_CAP {
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC, VSYNC_TARGET_CPU);
|
||||
}
|
||||
// Over-cap: drops rather than evicting the oldest.
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC);
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC, VSYNC_TARGET_CPU);
|
||||
s.queue_interrupt(INTERRUPT_SOURCE_VSYNC, VSYNC_TARGET_CPU);
|
||||
assert_eq!(s.dropped, 2);
|
||||
assert_eq!(s.pending.len(), INTERRUPT_QUEUE_CAP);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use xenia_memory::{GuestMemory, MemoryAccess};
|
||||
/// u16 Length
|
||||
/// u16 MaximumLength
|
||||
/// u32 Buffer (guest pointer)
|
||||
fn read_ansi_string(mem: &GuestMemory, ptr: u32) -> Option<String> {
|
||||
pub fn read_ansi_string(mem: &GuestMemory, ptr: u32) -> Option<String> {
|
||||
if ptr == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@ impl PcrWriter for GuestMemoryPcr<'_> {
|
||||
// `GuestMemory::write_u32` takes `&self` post-M2 trait flip; the
|
||||
// wrapping `&'a GuestMemory` is sufficient.
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,11 @@ pub fn allocate_thread_image(
|
||||
mem.write_u32(pcr_base, tls_base);
|
||||
mem.write_u32(pcr_base + 0x2C, hw_thread_id as u32);
|
||||
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);
|
||||
|
||||
Some(ThreadImage {
|
||||
|
||||
Reference in New Issue
Block a user