From 20cfc0130f96a5e3d02df35b0d3ce37259091b18 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Fri, 9 Oct 2026 10:59:50 +0300 Subject: [PATCH] fix(agc): preserve write tracking for shared host import aliases --- .../Graphics/src/GuestBufferMemory.cpp | 5 +- .../tests/execution/DccKeysFirstWrite.cpp | 49 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 3d622a998..92b900e7d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -289,7 +289,7 @@ void decideImportWatch(const Context& context, HostImports& state) { state.watchDevice = context.device; state.unwatchImports = true; state.unwatchDmaBufImports = false; - if (context.hostImportAlignment != 0 && GuestMemory::WriteWatched()) std::fprintf(stderr, "[write-watch] host imports are compared on Windows because driver writes can arrive after the import window\n"); + if (context.hostImportAlignment != 0 && GuestMemory::WriteWatched()) std::fprintf(stderr, "[write-watch] Windows direct host imports use comparisons; separate shared aliases retain guest write tracking\n"); #else const auto request = importWatchRequest(); state.watchDevice = context.device; @@ -412,6 +412,9 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s return step == nullptr; }); entry.unwatched = step == nullptr && (entry.dmaBuf ? state.unwatchDmaBufImports : state.unwatchImports); +#ifdef _WIN32 + if (entry.alias != nullptr) entry.unwatched = false; +#endif if (entry.unwatched) GuestMemory::Unwatch(base, bytes); if (step != nullptr) { #ifdef _WIN32 diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index 57a60b523..c3c4bd5eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -71,15 +71,22 @@ std::uint64_t AddressOf(const void* data) { class GuestBlock { public: - explicit GuestBlock(bool watch) : watched(watch) { + explicit GuestBlock(bool watch, bool shared = false) : watched(watch) { #ifdef _WIN32 if (watched) { block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); - if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); + if (shared) { + Require(block != nullptr, "cannot reserve shared test memory"); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr); + Require(section != nullptr, "cannot create shared test memory"); + GuestArena::GuestArenaMap_nid_postfix(block, BlockBytes, section, 0, PAGE_READWRITE); + CloseHandle(section); + } else if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); } else { block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); } #else + Require(!shared, "shared test memory requires Windows"); if (watched) { constexpr std::uintptr_t alignment = 65536; void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); @@ -184,6 +191,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* block) { std::memset(texels, 0x55, SurfaceBytes); std::memset(keys, 0x00, KeyBytes); std::memset(copied, 0xaa, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); const auto texture = TextureDescriptor(texels, keys); std::vector writeData(16, 0u); std::copy(texture.begin(), texture.end(), writeData.begin() + 4); @@ -385,6 +393,37 @@ void RunUntrackedReuse(AgcDriver::VulkanDevice& device, std::uint8_t* block) { Require(image->Refresh(), "the GPU-edited image was uploaded twice"); } +#ifdef _WIN32 +void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + using namespace AgcDriver::GuestMemory; + const auto address = AddressOf(block); + std::lock_guard gpu(GpuMutex()); + std::memset(block, 0x11, BlockBytes); + const std::array pattern{0x22222222u, 0x22222222u, 0x22222222u, 0x22222222u}; + Require(device.FillBuffer(address, SurfaceBytes, pattern), "shared memory was not imported"); + device.WaitIdle(); + Require(Watched(address, BlockBytes), "importing a separate alias disabled guest write tracking"); + auto* alias = static_cast(GuestArena::GuestArenaMapAlias_nid_postfix(address, BlockBytes)); + const auto beforeAliasWrite = CollectWritesUncached(address, BlockBytes); + alias[SurfaceBytes] = 0x11; + CollectWritesUncached(address, BlockBytes); + Require(UnchangedSince(address + SurfaceBytes, SurfaceBytes, beforeAliasWrite), "a driver alias write dirtied the protected guest view"); + GuestArena::GuestArenaUnmapAlias_nid_postfix(alias); + for (int repeat = 0; repeat < 8; ++repeat) { + const auto before = CollectWritesUncached(address, BlockBytes); + Require(device.FillBuffer(address, SurfaceBytes, pattern), "shared memory import was lost"); + device.WaitIdle(); + CollectWritesUncached(address, BlockBytes); + Require(UnchangedSince(address + SurfaceBytes, SurfaceBytes, before), "GPU access dirtied untouched guest pages"); + const auto current = CollectWritesUncached(address, BlockBytes); + block[SurfaceBytes] = static_cast(repeat); + CollectWritesUncached(address, BlockBytes); + Require(!UnchangedSince(address + SurfaceBytes, SurfaceBytes, current), "CPU write after GPU completion was missed"); + block[SurfaceBytes] = 0x11; + } +} +#endif + } int main() { @@ -392,12 +431,18 @@ int main() { std::optional block; std::optional watched; std::optional untracked; + std::optional shared; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; block.emplace(false); Run(*device, block->Data()); untracked.emplace(false); RunUntrackedReuse(*device, untracked->Data()); +#ifdef _WIN32 + shared.emplace(true, true); + RunSharedImport(*device, shared->Data()); + Run(*device, shared->Data()); +#endif if (AgcDriver::GuestMemory::WriteWatched()) { watched.emplace(true); Run(*device, watched->Data()); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5ba9128a7..b47930fab 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -240,7 +240,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Wide-character conversions](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `wcsrtombs` and [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still pass guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, they misread and miswrite guest strings. The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. -- Windows host-pointer imports are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. [GuestMemory](../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented. +- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented. - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done