From 780e854c2f516f97a694d34a69a76140acfd8316 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 3 May 2026 14:30:22 +0200 Subject: [PATCH] =?UTF-8?q?fix(memory):=20XMODBUG-002=20=E2=80=94=20write?= =?UTF-8?q?=5Fbulk=20bumps=20page=5Fversions=20for=20touched=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `GuestMemory::write_bulk` did the bulk copy via raw `copy_nonoverlapping` without bumping page_versions for any of the pages it touched. The per-byte `write_u8/u16/u32` methods all bump page_versions after their store; downstream caches (texture cache, shader cache) Acquire-load the slot to invalidate stale entries on guest writes. Without the bulk bump, a caller like `NtReadFile` writing a texture/shader resource into guest memory would leave any cache that had already keyed on the prior version handing back stale decoded bytes. After the copy, walk every page the write touched and bump it. Cheap: the typical bulk write spans a few pages (NtReadFile uses 64-128 KB chunks → 16-32 pages). Reservation-table invalidation for `lwarx`/`stwcx.` (XMODBUG-001's sibling) is NOT addressed here — the reservation table lives on KernelState, not GuestMemory, and plumbing it through requires a wider change. Callers that bulk-write code-bearing or atomic-bearing memory should call `kernel.reservations.invalidate_for_write(addr)` themselves; XEX-loader and NtReadFile are doing data-bearing writes that don't intersect lwarx targets, so this is acceptable for now. Verification at -n 100M lockstep: swaps: 2 → 2 (unchanged) draws: 0 → 0 texture_cache_entries: 0 → 0 (Sylpheed hasn't issued IM_LOAD yet — the bump is silent until a cache keys on a touched page, which won't happen until Phase F2/F3 unblocks the resource-loader workers) packets: ~59M (within noise) Tests: 16 memory pass. Closes XMODBUG-002 (P1). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/xenia-memory/src/heap.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/xenia-memory/src/heap.rs b/crates/xenia-memory/src/heap.rs index 18cd16f..c951a8d 100644 --- a/crates/xenia-memory/src/heap.rs +++ b/crates/xenia-memory/src/heap.rs @@ -318,11 +318,33 @@ impl GuestMemory { /// different threads. Used by the XEX loader (init, single-thread) /// and `NtReadFile` (mid-execution; the file's destination buffer is /// guest-thread-private by construction). + /// + /// XMODBUG-002: bumps `page_versions` for every page the write + /// touches. Pre-fix, callers like `NtReadFile` could rewrite a page + /// containing texture or shader bytes that a downstream cache had + /// already keyed on the prior version — the cache would happily + /// hand back the stale decoded bytes. The per-byte `write_*` methods + /// already bump the version after their store; this is the bulk + /// equivalent. Reservation-table invalidation for `lwarx`/`stwcx.` + /// remains the caller's responsibility (the table isn't reachable + /// from `GuestMemory` without a wider plumbing change). pub fn write_bulk(&self, addr: u32, buf: &[u8]) { let ptr = self.translate_virtual_mut(addr); unsafe { std::ptr::copy_nonoverlapping(buf.as_ptr(), ptr, buf.len()); } + if buf.is_empty() { + return; + } + let last_byte = addr.saturating_add(buf.len() as u32).saturating_sub(1); + let first_page = addr / PAGE_SIZE; + let last_page = last_byte / PAGE_SIZE; + for page in first_page..=last_page { + // Use the page-aligned address; bump_page_version computes + // the slot index by `addr / PAGE_SIZE` so any address within + // the page works. + self.bump_page_version(page * PAGE_SIZE); + } } /// Check if a guest address has been allocated/committed. Acquire load