fix(memory): XMODBUG-002 — write_bulk bumps page_versions for touched pages
`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) <noreply@anthropic.com>
This commit is contained in:
@@ -318,11 +318,33 @@ impl GuestMemory {
|
|||||||
/// different threads. Used by the XEX loader (init, single-thread)
|
/// different threads. Used by the XEX loader (init, single-thread)
|
||||||
/// and `NtReadFile` (mid-execution; the file's destination buffer is
|
/// and `NtReadFile` (mid-execution; the file's destination buffer is
|
||||||
/// guest-thread-private by construction).
|
/// 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]) {
|
pub fn write_bulk(&self, addr: u32, buf: &[u8]) {
|
||||||
let ptr = self.translate_virtual_mut(addr);
|
let ptr = self.translate_virtual_mut(addr);
|
||||||
unsafe {
|
unsafe {
|
||||||
std::ptr::copy_nonoverlapping(buf.as_ptr(), ptr, buf.len());
|
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
|
/// Check if a guest address has been allocated/committed. Acquire load
|
||||||
|
|||||||
Reference in New Issue
Block a user