fix(agc): compare Windows host imports after late driver writes
This commit is contained in:
parent
b3fcd6d6c0
commit
838d02a88c
|
|
@ -203,6 +203,10 @@ set_target_properties(libSceAgcDriver PROPERTIES CXX_EXTENSIONS OFF)
|
|||
configure_windows_unwind(libSceAgcDriver)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(agc_write_watch_coverage_tests tests/WriteWatchCoverage.cpp)
|
||||
target_include_directories(agc_write_watch_coverage_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
add_test(NAME agc_write_watch_coverage COMMAND agc_write_watch_coverage_tests)
|
||||
|
||||
add_test_executable(agc_profile_output_tests tests/ProfileOutput.cpp)
|
||||
target_include_directories(agc_profile_output_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_profile_output_tests PRIVATE libc)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WRITEWATCHCOVERAGE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WRITEWATCHCOVERAGE_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver::GuestMemory {
|
||||
|
||||
class WriteWatchCoverage {
|
||||
public:
|
||||
void Initialize(std::uint64_t address, std::size_t bytes) {
|
||||
base = address;
|
||||
size = bytes;
|
||||
excluded.assign(bytes / BlockBytes + (bytes % BlockBytes != 0), 0);
|
||||
}
|
||||
|
||||
bool Covers(std::uint64_t address, std::size_t bytes) const {
|
||||
if (bytes == 0 || bytes > size || address < base || address - base > size - bytes) return false;
|
||||
const auto first = (address - base) / BlockBytes;
|
||||
const auto last = (address - base + bytes - 1) / BlockBytes;
|
||||
return std::find(excluded.begin() + first, excluded.begin() + last + 1, 1) == excluded.begin() + last + 1;
|
||||
}
|
||||
|
||||
bool Exclude(std::uint64_t address, std::size_t bytes) {
|
||||
if (bytes == 0 || size == 0) return false;
|
||||
if (address < base) {
|
||||
if (base - address >= bytes) return false;
|
||||
bytes -= base - address;
|
||||
address = base;
|
||||
}
|
||||
const auto offset = address - base;
|
||||
if (offset >= size) return false;
|
||||
bytes = static_cast<std::size_t>(std::min<std::uint64_t>(bytes, size - offset));
|
||||
const auto first = offset / BlockBytes;
|
||||
const auto last = (offset + bytes - 1) / BlockBytes;
|
||||
bool changed = false;
|
||||
for (auto block = first; block <= last; ++block) {
|
||||
changed = changed || excluded[block] == 0;
|
||||
excluded[block] = 1;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::size_t BlockBytes = 65536;
|
||||
std::uint64_t base = 0;
|
||||
std::size_t size = 0;
|
||||
std::vector<std::uint8_t> excluded;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp"
|
||||
#include "ThreadOwned.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
|
|
@ -762,6 +763,7 @@ struct WriteTracker {
|
|||
// records in the same block (the title's per-job slots are 0x20 apart) must not count.
|
||||
std::vector<std::uint32_t> cpuBlocks;
|
||||
std::vector<std::uint32_t> writtenBlocks;
|
||||
WriteWatchCoverage coverage;
|
||||
#else
|
||||
static constexpr std::size_t LeafBlocks = std::size_t{1} << 16;
|
||||
static constexpr std::size_t LeafCount = std::size_t{1} << 15;
|
||||
|
|
@ -805,6 +807,7 @@ struct WriteTracker {
|
|||
blocks.assign(size / WriteBlockBytes + 1, 0);
|
||||
cpuBlocks.assign(size / WriteBlockBytes + 1, 0);
|
||||
writtenBlocks.assign(size / WriteBlockBytes + 1, 0);
|
||||
coverage.Initialize(base, size);
|
||||
pages.resize(1u << 16);
|
||||
#else
|
||||
watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix();
|
||||
|
|
@ -814,7 +817,7 @@ struct WriteTracker {
|
|||
|
||||
bool covers(std::uint64_t address, std::size_t bytes) const {
|
||||
#ifdef _WIN32
|
||||
return address >= base && address - base <= size - bytes;
|
||||
return coverage.Covers(address, bytes);
|
||||
#else
|
||||
return address + bytes <= LeafCount * LeafBlocks * WriteBlockBytes && GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(static_cast<std::uintptr_t>(address), bytes);
|
||||
#endif
|
||||
|
|
@ -1118,11 +1121,10 @@ std::uint64_t CollectEpochBumps() {
|
|||
|
||||
namespace {
|
||||
|
||||
void unwatchLocked(const WriteTracker& tracker, std::uint64_t address, std::size_t bytes) {
|
||||
void unwatchLocked(WriteTracker& tracker, std::uint64_t address, std::size_t bytes) {
|
||||
if (!tracker.watched) return;
|
||||
#ifdef _WIN32
|
||||
static_cast<void>(address);
|
||||
static_cast<void>(bytes);
|
||||
if (tracker.coverage.Exclude(address, bytes)) unwatchSerial.fetch_add(1, std::memory_order_release);
|
||||
#else
|
||||
if (GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast<const void*>(address), bytes)) unwatchSerial.fetch_add(1, std::memory_order_release);
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -285,8 +285,9 @@ void decideImportWatch(const Context& context, HostImports& state) {
|
|||
if (state.watchDevice == context.device) return;
|
||||
#ifdef _WIN32
|
||||
state.watchDevice = context.device;
|
||||
state.unwatchImports = false;
|
||||
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");
|
||||
#else
|
||||
const auto request = importWatchRequest();
|
||||
state.watchDevice = context.device;
|
||||
|
|
|
|||
|
|
@ -1172,6 +1172,10 @@ void drawSnapshotReuseTests(const Device& device, Recorder& recorder) {
|
|||
std::cout << "host import of the watched block refused: draw snapshot reuse not tested\n";
|
||||
return;
|
||||
}
|
||||
if (!AgcDriver::GuestMemory::Watched(address, bytes)) {
|
||||
std::cout << "host imports are compared, not watched: draw snapshot reuse not tested\n";
|
||||
return;
|
||||
}
|
||||
const auto element = address + 4096;
|
||||
constexpr std::size_t elementBytes = 1024;
|
||||
ShaderRecompiler::RecompileResult program;
|
||||
|
|
@ -1688,8 +1692,8 @@ void importWatchTests(const Device& device) {
|
|||
using namespace AgcDriver::GuestMemory;
|
||||
const auto& context = device.GetContext();
|
||||
#ifdef _WIN32
|
||||
static_cast<void>(context);
|
||||
std::cout << "import watch decisions: Linux write watch only\n";
|
||||
Require(PrepareImportWatch(context) == ImportWatch::Unwatch, "Windows host imports stayed watched by default");
|
||||
std::cout << "import watch decisions: Windows host imports use comparisons\n";
|
||||
#else
|
||||
if (context.hostImportAlignment == 0 || !WriteWatched()) {
|
||||
std::cout << "host imports or write watching unavailable: import watch decisions not tested\n";
|
||||
|
|
|
|||
|
|
@ -117,6 +117,30 @@ void CheckOwnStore() {
|
|||
CollectWritesUncached(base, 2 * Block);
|
||||
Require(!UnchangedSince(base, 64, beforeCpu), "a CPU write after the driver store is not seen");
|
||||
}
|
||||
|
||||
void CheckUnwatch() {
|
||||
void* memory = AllocateWatched(3 * Block);
|
||||
const auto base = reinterpret_cast<std::uint64_t>(memory);
|
||||
const auto imported = base + Block;
|
||||
std::memset(memory, 0x11, 3 * Block);
|
||||
BumpCollectEpoch();
|
||||
const auto before = CollectWrites(base, 3 * Block);
|
||||
Require(before != 0 && UnchangedSince(imported, Block, before), "the import range was not initially watched");
|
||||
Unwatch(imported, Block);
|
||||
Require(!Watched(imported, Block) && !UnchangedSince(imported, Block, before), "unwatch kept trusting import stamps");
|
||||
Require(CollectWrites(base, 3 * Block) == 0 && CollectWritesUncached(imported, Block) == 0, "unwatch did not invalidate a cached collect");
|
||||
Require(Watched(base, Block) && Watched(base + 2 * Block, Block), "unwatch disabled unrelated memory");
|
||||
Require(MarkWritten(imported, Block) == 0, "an unwatched import still produces trusted stamps");
|
||||
std::array<std::uint64_t, 1> generations{before};
|
||||
std::array<std::uint8_t, 1> changed{};
|
||||
Require(!ChangedBlocks(imported, Block, generations, changed), "an unwatched import still uses block stamps");
|
||||
Unwatch(imported, Block);
|
||||
const auto adjacent = CollectWrites(base, Block);
|
||||
Require(adjacent != 0, "unwatch broke the adjacent block's collect");
|
||||
static_cast<volatile std::uint8_t*>(memory)[0] = 0x33;
|
||||
CollectWritesUncached(base, Block);
|
||||
Require(!UnchangedSince(base, Block, adjacent), "unwatch hid a CPU edit in unrelated memory");
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
|
|
@ -127,6 +151,7 @@ int main() {
|
|||
}
|
||||
CheckSharedBlock();
|
||||
CheckOwnStore();
|
||||
CheckUnwatch();
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "write tracking test failed: " << error.what() << "\n";
|
||||
return 1;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp"
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
using AgcDriver::GuestMemory::WriteWatchCoverage;
|
||||
constexpr std::uint64_t base = 0x200000000;
|
||||
constexpr std::size_t block = 65536;
|
||||
WriteWatchCoverage coverage;
|
||||
require(!coverage.Covers(base, 1), "an uninitialized arena is watched");
|
||||
coverage.Initialize(base, 4 * block + 1);
|
||||
require(coverage.Covers(base, 4 * block + 1), "the initialized arena is not watched");
|
||||
require(!coverage.Covers(base - 1, 1) && !coverage.Covers(base + 4 * block + 1, 1), "outside memory is watched");
|
||||
require(!coverage.Covers(base, 0) && !coverage.Covers(base, std::numeric_limits<std::size_t>::max()), "an empty or overflowing range is watched");
|
||||
require(coverage.Exclude(base + block + 4096, 1), "an imported page was not excluded");
|
||||
require(!coverage.Covers(base + block, block), "the imported block is still watched");
|
||||
require(!coverage.Covers(base + block - 1, 2), "a range crossing into the import is watched");
|
||||
require(coverage.Covers(base, block) && coverage.Covers(base + 2 * block, 2 * block + 1), "unrelated blocks lost write watching");
|
||||
require(!coverage.Exclude(base + block, block), "excluding an imported block again reports a change");
|
||||
require(coverage.Exclude(base + 3 * block - 1, 2), "an import across a block boundary was not excluded");
|
||||
require(!coverage.Covers(base + 2 * block, block) && !coverage.Covers(base + 3 * block, block), "a boundary import left one block watched");
|
||||
require(coverage.Covers(base, block) && coverage.Covers(base + 4 * block, 1), "a boundary import excluded adjacent blocks");
|
||||
require(!coverage.Exclude(base - block, block) && !coverage.Exclude(base + 5 * block, block), "an outside import changed coverage");
|
||||
require(coverage.Exclude(base + 4 * block, std::numeric_limits<std::size_t>::max()), "the last partial block was not excluded");
|
||||
require(!coverage.Covers(base + 4 * block, 1), "the last partial block is still watched");
|
||||
coverage.Initialize(base, 2 * block);
|
||||
require(coverage.Exclude(base - 1, 2) && !coverage.Covers(base, block), "an import crossing the arena start was not clipped");
|
||||
require(coverage.Covers(base + block, block), "clipping an import excluded the rest of the arena");
|
||||
std::cout << "write watch coverage passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,8 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0
|
||||
- [sceFontGlyphDefineAttribute](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal`
|
||||
|
||||
- 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 for the process lifetime, including after that allocation is reused. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for those blocks; restoring tracking safely after import retirement is unimplemented.
|
||||
|
||||
### Unknown function info
|
||||
|
||||
- [sceHmd2Initialize](../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the error the console returns when the PS VR2 runtime is unavailable is unknown; it returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016). PPSA01317 passes a pointer to 16 zeroed bytes and only tests the result for non-zero
|
||||
|
|
|
|||
Loading…
Reference in New Issue