Files
AnyPS5/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp
T
Dextroid17 e920c9e64a fix(agc): decide an edge unit's CPU keep from the pages the unit overlaps
StorageTexture::writeBackLayers keeps the CPU's bytes for an edge unit when StoredOver
reports that the CPU wrote it since the unit's generation. That check read one generation
per 64 KiB block, so a store to another allocation sharing the block counted as a store
over this surface and the whole block was kept, discarding GPU results written into it.

The driver path in the same check already compares range pieces; the CPU path now does
the same. Each collect records the generation of every page it found dirty, and StoredOver
tests only the pages its range overlaps. A block with no page record keeps the previous
whole-block answer.

Measured on an RTX 3080 Ti, main at df16c4c2:

- agc_write_tracking stores at base + Block + 8, outside the queried range but inside the
  shared block. That case fails on main ("a CPU store outside the shared range counts for
  it") and passes here.
- Full suite 408 of 408 (Linux, CachyOS, GCC, Release, BUILD_TESTING=ON).

This does not cover the other half of #865: the report's finding 2, where the stamps fall
on the surface's own pages and the bytes under them are unchanged, still keeps the block.
No title was run here, so the fix is offered for the report's own repro.
2026-10-07 21:30:28 -04:00

137 lines
6.5 KiB
C++

#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/GuestWriteWatch.hpp"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#endif
#include <array>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <string>
namespace {
using namespace AgcDriver::GuestMemory;
constexpr std::size_t Block = 65536;
void Require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
void* AllocateWatched(std::size_t bytes) {
#ifdef _WIN32
void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block);
GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes);
#else
void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched block");
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
const auto aligned = (begin + Block - 1) & ~static_cast<std::uintptr_t>(Block - 1);
if (aligned != begin) munmap(raw, aligned - begin);
if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast<void*>(aligned + bytes), begin + Block - aligned);
void* block = reinterpret_cast<void*>(aligned);
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes);
#endif
Require(Watched(reinterpret_cast<std::uint64_t>(block), bytes), "the test block is not watched");
return block;
}
void CheckSharedBlock() {
void* memory = AllocateWatched(3 * Block);
const auto base = reinterpret_cast<std::uint64_t>(memory);
const auto boundary = base + 2 * Block - 0xb800;
const auto firstBegin = base;
const auto secondEnd = base + 3 * Block;
const auto sharedFirst = boundary - (base + Block);
const auto sharedSecond = base + 2 * Block - boundary;
std::memset(memory, 0x11, 3 * Block);
const auto synced = CollectWrites(base, 3 * Block);
Require(synced != 0, "the test block is not collected");
Require(MarkWritten(firstBegin, static_cast<std::size_t>(boundary - firstBegin)) != 0, "a driver store into watched memory is not stamped");
std::array<std::uint64_t, 2> generations{synced, synced};
std::array<std::uint8_t, 2> changed{};
Require(ChangedBlocks(base + Block, 2 * Block, generations, changed) && changed[0] == BlockWritten && changed[1] == BlockUnchanged, "the shared block does not read as written");
Require(!StoredOver(boundary, static_cast<std::size_t>(sharedSecond), synced), "a store of the neighbour's bytes counts for the second surface's part of the shared block");
Require(StoredOver(base + Block, static_cast<std::size_t>(sharedFirst), synced), "the first surface's own store is not seen over its part of the shared block");
Require(!StoredOver(boundary, static_cast<std::size_t>(secondEnd - boundary), synced), "the second surface reads as stored over");
const auto touched = MarkWritten(boundary + 0x100, 4);
Require(StoredOver(boundary, static_cast<std::size_t>(sharedSecond), synced), "a store over the second surface's bytes is not seen");
Require(!StoredOver(boundary, static_cast<std::size_t>(sharedSecond), touched), "a store older than the generation is seen");
const auto whole = TrackerGeneration();
MarkWritten(base + Block, Block);
Require(StoredOver(boundary, static_cast<std::size_t>(sharedSecond), whole), "a store over the whole block is not seen");
const auto afterWhole = TrackerGeneration();
MarkWritten(base + Block, static_cast<std::size_t>(sharedFirst));
Require(!StoredOver(boundary, static_cast<std::size_t>(sharedSecond), afterWhole), "a partial store after a whole-block store is taken for the whole block");
const auto many = TrackerGeneration();
for (int store = 0; store < 6; ++store) MarkWritten(base + Block + static_cast<std::uint64_t>(store) * 64, 4);
Require(StoredOver(boundary, static_cast<std::size_t>(sharedSecond), many), "forgotten driver stores are taken as missing the range");
const auto beforeCpu = CollectWrites(base, 3 * Block);
static_cast<volatile std::uint8_t*>(memory)[Block + 8] = 0x22;
CollectWritesUncached(base, 3 * Block);
Require(!StoredOver(boundary, static_cast<std::size_t>(sharedSecond), beforeCpu), "a CPU store outside the shared range counts for it");
std::array<std::uint8_t, 64> unwatched{};
const auto outside = reinterpret_cast<std::uint64_t>(unwatched.data());
if (!Watched(outside, unwatched.size())) Require(StoredOver(outside, unwatched.size(), TrackerGeneration()), "an unwatched range reads as not stored over");
}
void CheckOwnStore() {
void* memory = AllocateWatched(2 * Block);
const auto base = reinterpret_cast<std::uint64_t>(memory);
const auto second = base + Block;
std::memset(memory, 0x11, 2 * Block);
const auto synced = CollectWrites(base, 2 * Block);
Require(synced != 0, "the own-store block is not collected");
std::array<std::byte, 8192> bytes{};
bytes.fill(std::byte{0x44});
Write(second, bytes);
const auto stored = TrackerGeneration();
Require(!UnchangedSince(second, bytes.size(), synced), "the driver store is not stamped");
CollectWritesUncached(base, 2 * Block);
Require(UnchangedSince(second, Block, stored), "a walk after a driver store reports the store again as a newer write");
Require(UnchangedSinceCollected(second, bytes.size(), synced), "the driver store's own page faults are stamped as a CPU write");
static_cast<volatile std::uint8_t*>(memory)[Block + 3 * 4096 + 8] = 0x22;
const std::array<std::byte, 64> label{};
Write(second + 3 * 4096 + 64, label);
Require(!UnchangedSinceCollected(second + 3 * 4096, 4096, stored), "a CPU write before a driver store in its page is not stamped as one");
const auto beforeCpu = TrackerGeneration();
static_cast<volatile std::uint8_t*>(memory)[16] = 0x33;
CollectWritesUncached(base, 2 * Block);
Require(!UnchangedSince(base, 64, beforeCpu), "a CPU write after the driver store is not seen");
}
}
int main() {
try {
if (!WriteWatched()) {
std::cout << "no write watching: skipped\n";
return 77;
}
CheckSharedBlock();
CheckOwnStore();
} catch (const std::exception& error) {
std::cerr << "write tracking test failed: " << error.what() << "\n";
return 1;
}
std::cout << "write tracking tests passed\n";
return 0;
}