GuestMemory::CollectWrites was built on the Windows arena's MEM_WRITE_WATCH and answered 0 on Linux, so every cached copy of guest memory (textures, resident render targets, buffers, DCC key proofs) read as changed at every check: resident targets were stored and re-uploaded at every refresh and unit shadows stayed off. Astro Bot's intro ran at ~3 fps. libc gains GuestWriteWatch, the Linux counterpart of GetWriteWatch. Every guest mapping libkernel makes (MapAligned: fixed, hinted and aligned) is registered with one userfaultfd in asynchronous write-protect mode (UFFD_FEATURE_WP_ASYNC: the kernel lifts a page's protection on its first write without a handler thread, for user and kernel-mode writes alike), and PAGEMAP_SCAN with PM_SCAN_WP_MATCHING reports the pages written since the last scan of a range and protects them again in one call. Unmap forgets the range. Direct memory is mapped from shared backings over its reservation and a fixed remap replaces the mapping, so both register the new mapping afresh. The watch is per mapping: a write through one mapping of shared physical pages is not seen at another, so physical pages mapped at more than one address leave the watch in every mapping of them. A registered mapping reads as written until a collect covers it. The watch keeps that itself instead of relying on how the kernel classifies pages never faulted in: Linux 6.17 reports a range without page tables as not written and leaves it unprotected, 7.2 reports it as written. The first collect of a fresh range reports it written and write-protects it with UFFDIO_WRITEPROTECT, which puts markers on its unpopulated pages (UFFD_FEATURE_WP_UNPOPULATED), so from then on only real writes report. Fallback: the userfaultfd is opened with UFFD_USER_MODE_ONLY where vm.unprivileged_userfaultfd restricts it, and a probe at startup checks the mechanism on scratch pages (a populated and an unpopulated page written after protection report exactly, untouched ones do not). Without it (kernel older than 6.7, no userfaultfd permission, headers without PAGEMAP_SCAN, or APS5_NO_WRITE_WATCH=1) the watch reports unavailable and the tracker stays unwatched exactly as before: CollectWrites answers 0 and callers compare bytes. A range with a page outside the registered mappings also answers 0, and a protection or scan that fails reports its remaining pages as written and answers 0. The driver's tracker keeps its contract and generations. On Linux its block stamps live in 4 GiB leaves over the user address space, made on first stamp, instead of the arena's flat arrays; the collect memo now works on both platforms. GuestMemory::WriteWatched and Watched replace the arena queries in UnitShadowEnabled and ShadowDestinationFor, so unit shadows run on Linux over watched memory and stay off everywhere else. The Windows arena path is unchanged apart from going through the same stamp helpers. Host imports: a driver may lift the write protection of imported pages itself. amdgpu validates the userptr BO behind a VK_EXT_external_memory_host import with a writable HMM fault on every submission after the pages were protected again, so PAGEMAP_SCAN reports them as written with no CPU store, and a resident display image read as written at every present (found by Pacuka on an RX 6650 XT with RADV: the presented image went black). NVIDIA pins the pages once at the import and stays exact from then on. A probe at device setup decides per device: it maps and watches scratch pages, collects them clean, imports them, collects, submits a GPU copy that reads the import, waits, and collects again. Pages written after the submission mean imports resolve write protection: every import from then on takes its range off the watch (GuestMemory::Unwatch) before it is made, so Watched is false for it, CollectWrites answers 0 and callers compare bytes; GPU writes to watched memory keep their MarkWritten stamps. The result is logged once: "[write-watch] host imports resolve write protection: yes/no". A probe that cannot run takes the unwatch side. APS5_WRITE_WATCH_IMPORTS=watch|unwatch overrides the probe (default probe; anything else throws), and APS5_NO_WRITE_WATCH=1 still turns the watch off. On the RTX 4090 the probe reads 0 of 4 pages written after the GPU read (4 after the import itself), so imports stay watched there. The import itself lifts the protection once where imports stay watched: on the RTX 4090 the pinning at vkAllocateMemory reports every imported page as written (4 of 4 in the probe), with no CPU store. Such a report would read as a CPU write over any GPU results pending in the range and drop them. So every import of a watched range is made inside an import window (GuestMemory::ImportWatched), under the tracker lock: the range is collected right before the import, so earlier CPU writes are stamped as usual, and again right after it, and what the window reports is stamped "maybe written". Readers without pending results see it as a change (UnchangedSince fails, ChangedBlocks reports it), so caches re-upload or compare. Decisions about pending GPU results read it as untracked: ChangedBlocks reports BlockMaybeWritten apart from BlockWritten, and the drop rule, the write-back's keep blocks, the alias checks, the unit shadow's stale drop and the whole-image stale checks (through the new WrittenSince, which also answers false for a range off the watch) store the results instead of dropping them. A real CPU store racing an import also races the pending GPU writes to the same memory, which has no defined order on hardware either, so the GPU results winning is acceptable. From the second collect on the range is protected again and watched normally. A range the watch does not cover whole, or a collect of the window that fails, is taken off the watch instead. An unwatched import stays off the watch for the rest of its mapping: a remap registers the range afresh, the import kept over it takes it off again at the next reconcile, and a re-import after an unmap follows the decision. Unwatch invalidates the collect memo, so a collect memoized before the import answers 0. A range that leaves the watch while a storage image holds a generation for it no longer reads as CPU-written: ChangedBlocks now tells whether the range is watched, and the drop rule, the write-back's keep blocks and the alias borrow treat an unwatched range as untracked (stored, never dropped), as a generation of 0 is. Tests: guest_memory checks the watch through libkernel mappings (a CPU store, a read, a kernel write through read(), another thread, a memcpy across pages, an mprotect cycle, a partial munmap and a fixed remap) and over a fresh 2 MiB-aligned mapping without page tables, checks direct memory (watched, an alias takes the shared pages off the watch in both mappings, a fixed direct remap over flexible memory is watched afresh), and names the failing line and the reported pages. The recorder tests allocate watched memory on both platforms, so the UnchangedSince, key proof, close race and unit shadow tests run on Linux, and the storage refresh test gains a watched variant: results kept on the GPU reach a CPU reader, and a plain CPU store into one unit with newer results pending is found by the watch alone. The recorder tests now also run the real import probe, check the decision with the result injected (unwatch: an import leaves the watch, a memoized collect, a partly imported range and the unimported neighbour answer as they should, MarkWritten stamps only watched memory, a remap and a re-import after an unmap follow the decision; watch: the import stays watched and stamped), and check that results pending when their memory leaves the watch are stored rather than dropped. A further test imports the memory of a storage image with results pending in watch mode: the results survive the import window and reach guest memory, a cache generation taken before the import sees a change, the window reads as maybe written and not as a CPU store, and a CPU store after it reads as one again. Unit shadows and the watched storage refresh report themselves as not run when imports are compared.
21 lines
674 B
C++
21 lines
674 B
C++
#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GUESTWRITEWATCH_HPP
|
|
#define CORE_LIBS_PRX_LIBC_INCLUDE_GUESTWRITEWATCH_HPP
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
|
|
namespace GuestWriteWatch {
|
|
|
|
extern "C" {
|
|
|
|
bool GuestWriteWatchAvailable_nid_postfix();
|
|
void GuestWriteWatchRegister_nid_postfix(const void* pointer, std::size_t bytes);
|
|
bool GuestWriteWatchUnregister_nid_postfix(const void* pointer, std::size_t bytes);
|
|
bool GuestWriteWatchCovers_nid_postfix(std::uintptr_t address, std::size_t bytes);
|
|
bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|