fix(agc): preserve write tracking for shared host import aliases

This commit is contained in:
boykopovar 2026-10-09 10:59:50 +03:00
parent 0e1d25a8a2
commit 20cfc0130f
3 changed files with 52 additions and 4 deletions

View File

@ -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

View File

@ -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<std::uint8_t*>(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<std::uint8_t*>(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<std::uint32_t> 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<std::uint32_t, 4> 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<std::uint8_t*>(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<std::uint8_t>(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<GuestBlock> block;
std::optional<GuestBlock> watched;
std::optional<GuestBlock> untracked;
std::optional<GuestBlock> 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());

View File

@ -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