Initial commit

This commit is contained in:
nmzik
2026-07-16 10:21:41 +02:00
commit 147188b143
427 changed files with 205923 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+631
View File
@@ -0,0 +1,631 @@
#include "graphics/host_gpu/pageManager.h"
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#undef min
#undef max
#endif
namespace {
using Libs::Graphics::GpuAccess;
using Libs::Graphics::PageFaultAccess;
using Libs::Graphics::PageManager;
void Check(bool value, const char *text) {
if (!value) {
std::fprintf(stderr, "PageManagerTests: failed: %s\n", text);
std::abort();
}
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool IsWritable(const void *address) {
MEMORY_BASIC_INFORMATION info{};
Check(VirtualQuery(address, &info, sizeof(info)) != 0, "VirtualQuery failed");
return info.Protect == PAGE_READWRITE;
}
uint32_t Protection(const void *address) {
MEMORY_BASIC_INFORMATION info{};
Check(VirtualQuery(address, &info, sizeof(info)) != 0, "VirtualQuery failed");
return info.Protect;
}
struct FaultContext {
PageManager *manager = nullptr;
bool result = true;
bool reenter = false;
uint64_t reenter_address = 0;
bool block = false;
std::atomic_uint32_t calls{0};
std::atomic_bool entered{false};
std::atomic_bool release{false};
};
std::atomic<PageManager *> g_native_fault_manager{nullptr};
std::atomic_bool g_delay_native_fault{false};
std::atomic_bool g_native_fault_entered{false};
std::atomic_bool g_release_native_fault{false};
LONG CALLBACK NativeFaultHandler(EXCEPTION_POINTERS *exception) {
if (exception == nullptr || exception->ExceptionRecord == nullptr ||
exception->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) {
return EXCEPTION_CONTINUE_SEARCH;
}
const auto operation = exception->ExceptionRecord->ExceptionInformation[0];
const auto access = operation == 0 ? PageFaultAccess::Read
: operation == 1 ? PageFaultAccess::Write
: operation == 8 ? PageFaultAccess::Execute
: PageFaultAccess::Unknown;
auto *manager = g_native_fault_manager.load(std::memory_order_acquire);
if (manager == nullptr) {
return EXCEPTION_CONTINUE_SEARCH;
}
if (g_delay_native_fault.load(std::memory_order_acquire)) {
g_native_fault_entered.store(true, std::memory_order_release);
while (!g_release_native_fault.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
}
return manager->HandleFault(
access, exception->ExceptionRecord->ExceptionInformation[1])
? EXCEPTION_CONTINUE_EXECUTION
: EXCEPTION_CONTINUE_SEARCH;
}
bool InvalidateFault(void *context, Libs::Graphics::PageFaultAccess, uint64_t vaddr, uint64_t size,
Libs::Graphics::PageFaultPhase phase) noexcept {
auto *fault = static_cast<FaultContext *>(context);
Check(fault != nullptr && fault->manager != nullptr, "invalid fault context");
if (phase != Libs::Graphics::PageFaultPhase::Invalidate) {
return true;
}
fault->calls.fetch_add(1, std::memory_order_relaxed);
if (fault->reenter) {
const auto address =
fault->reenter_address != 0 ? fault->reenter_address : vaddr;
(void)fault->manager->HandleFault(PageFaultAccess::Write, address);
}
if (fault->block) {
fault->entered.store(true, std::memory_order_release);
while (!fault->release.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
}
(void)size;
return fault->result;
}
uint8_t *Allocate(uint64_t size, uint32_t protection = PAGE_READWRITE) {
constexpr uintptr_t test_address = 0x0000000200010000ull;
auto *memory = static_cast<uint8_t *>(
VirtualAlloc(reinterpret_cast<void *>(test_address), size,
MEM_RESERVE | MEM_COMMIT, protection));
Check(memory == reinterpret_cast<void *>(test_address),
"fixed low VirtualAlloc failed");
return memory;
}
void TestWatchFaultAndUnwatch() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size * 2);
manager.OnGpuMap(reinterpret_cast<uint64_t>(memory), page_size * 2);
manager.UpdatePageWatchers(true, reinterpret_cast<uint64_t>(memory),
page_size);
Check(manager.IsTracked(reinterpret_cast<uint64_t>(memory)) &&
!IsWritable(memory),
"watch did not protect the page");
Check(manager.HandleFault(PageFaultAccess::Write,
reinterpret_cast<uint64_t>(memory + 32)),
"tracked write fault was not handled");
Check(!manager.IsTracked(reinterpret_cast<uint64_t>(memory)) &&
IsWritable(memory),
"fault invalidation did not remove the watcher");
Check(manager.HandleFault(PageFaultAccess::Write,
reinterpret_cast<uint64_t>(memory)),
"single delayed write fault was not coalesced");
Check(manager.HandleFault(PageFaultAccess::Write,
reinterpret_cast<uint64_t>(memory)),
"second delayed write fault was not coalesced");
manager.OnGpuUnmap(reinterpret_cast<uint64_t>(memory), page_size * 2);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestSharedWatcherFault() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size);
manager.UpdatePageWatchers(true, address + 8, 32);
manager.UpdatePageWatchers(true, address + 128, 64);
Check(manager.HandleFault(PageFaultAccess::Write, address + 16),
"shared-watcher fault was not handled");
Check(!manager.IsTracked(address) && IsWritable(memory),
"fault callback did not clear every shared watcher");
manager.OnGpuUnmap(address, page_size);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestReadWriteWatcherFault() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size);
manager.UpdatePageWatchers(true, address, page_size,
Libs::Graphics::PageWatchMode::Write);
manager.UpdatePageWatchers(true, address, page_size,
Libs::Graphics::PageWatchMode::ReadWrite);
manager.UpdatePageWatchers(false, address, page_size,
Libs::Graphics::PageWatchMode::Write);
Check(Protection(memory) == PAGE_NOACCESS,
"read/write watcher did not install no-access protection");
Check(manager.HandleFault(PageFaultAccess::Read, address + 8),
"tracked read fault was not handled");
Check(Protection(memory) == PAGE_READWRITE && !manager.IsTracked(address),
"read fault did not release every watcher");
manager.OnGpuUnmap(address, page_size);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestPermittedMappedLateFaultsResume() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size);
manager.UpdatePageWatchers(true, address, page_size);
manager.UpdatePageWatchers(false, address, page_size);
Check(manager.HandleFault(PageFaultAccess::Write, address),
"first delayed mapped write was not accepted");
Check(manager.HandleFault(PageFaultAccess::Write, address),
"second delayed mapped write was not accepted");
Check(manager.HandleFault(PageFaultAccess::Read, address),
"delayed mapped read was not accepted on readable backing");
DWORD old_protection = 0;
Check(VirtualProtect(memory, page_size, PAGE_READONLY, &old_protection) != 0 &&
old_protection == PAGE_READWRITE,
"failed to prepare intentional read-only protection");
Check(!manager.HandleFault(PageFaultAccess::Write, address),
"intentional read-only mapping accepted a write fault");
Check(VirtualProtect(memory, page_size, PAGE_READWRITE, &old_protection) != 0,
"failed to restore writable protection");
manager.OnGpuUnmap(address, page_size);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestPartialMappingUnmapPreservesTokens() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size);
manager.OnGpuMap(address + 8, 16);
manager.UpdatePageWatchers(true, address, page_size,
Libs::Graphics::PageWatchMode::ReadWrite);
manager.UpdatePageWatchers(false, address, page_size,
Libs::Graphics::PageWatchMode::ReadWrite);
manager.OnGpuUnmap(address + 8, 16);
Check(manager.HandleFault(PageFaultAccess::Read, address),
"partial mapping unmap erased delayed read ownership");
Check(manager.HandleFault(PageFaultAccess::Write, address),
"partial mapping unmap erased delayed write ownership");
manager.OnGpuUnmap(address, page_size);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestNativeDelayedReadAfterModeDowngrade() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
memory[0] = 0x6d;
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size);
manager.UpdatePageWatchers(true, address, page_size);
manager.UpdatePageWatchers(true, address, page_size,
Libs::Graphics::PageWatchMode::ReadWrite);
manager.UpdatePageWatchers(false, address, page_size);
Check(Protection(memory) == PAGE_NOACCESS,
"read/write ownership did not install no-access protection");
void *handler = AddVectoredExceptionHandler(1, NativeFaultHandler);
Check(handler != nullptr, "AddVectoredExceptionHandler failed");
Check(g_native_fault_manager.exchange(&manager, std::memory_order_acq_rel) ==
nullptr,
"native fault manager already installed");
g_native_fault_entered.store(false, std::memory_order_release);
g_release_native_fault.store(false, std::memory_order_release);
g_delay_native_fault.store(true, std::memory_order_release);
uint8_t value = 0;
std::thread reader(
[&] { value = *static_cast<volatile uint8_t *>(memory); });
while (!g_native_fault_entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
manager.UpdatePageWatchers(true, address, page_size);
manager.UpdatePageWatchers(false, address, page_size,
Libs::Graphics::PageWatchMode::ReadWrite);
Check(Protection(memory) == PAGE_READONLY,
"mode downgrade did not restore readable protection");
manager.UpdatePageWatchers(true, address, page_size);
context.block = true;
bool write_handled = false;
std::thread writer([&] {
write_handled = manager.HandleFault(PageFaultAccess::Write, address);
});
while (!context.entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
g_release_native_fault.store(true, std::memory_order_release);
reader.join();
context.release.store(true, std::memory_order_release);
writer.join();
g_delay_native_fault.store(false, std::memory_order_release);
Check(g_native_fault_manager.exchange(nullptr, std::memory_order_acq_rel) ==
&manager,
"native fault manager publication changed");
Check(RemoveVectoredExceptionHandler(handler) != 0,
"RemoveVectoredExceptionHandler failed");
Check(value == 0x6d && write_handled &&
context.calls.load(std::memory_order_relaxed) == 1,
"delayed read was not coalesced across write ownership/resolution");
manager.OnGpuUnmap(address, page_size);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestDelayedFaultAfterExplicitUnwatch() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size);
manager.UpdatePageWatchers(true, address, page_size);
// Models a store that already raised an AV before another thread published
// explicit CPU dirtiness and removed the watcher.
manager.UpdatePageWatchers(false, address, page_size);
Check(manager.HandleFault(PageFaultAccess::Write, address),
"delayed watched write was not accepted after explicit unwatch");
Check(manager.HandleFault(PageFaultAccess::Write, address) &&
context.calls.load(std::memory_order_relaxed) == 0,
"second explicit-unwatch fault was not coalesced or dispatched invalidation");
manager.OnGpuUnmap(address, page_size);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestNativeAccessViolation() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size);
manager.UpdatePageWatchers(true, address, page_size);
void *handler = AddVectoredExceptionHandler(1, NativeFaultHandler);
Check(handler != nullptr, "AddVectoredExceptionHandler failed");
Check(g_native_fault_manager.exchange(&manager, std::memory_order_acq_rel) ==
nullptr,
"native fault manager already installed");
*static_cast<volatile uint8_t *>(memory) = 0x5a;
Check(g_native_fault_manager.exchange(nullptr, std::memory_order_acq_rel) ==
&manager,
"native fault manager publication changed");
Check(RemoveVectoredExceptionHandler(handler) != 0,
"RemoveVectoredExceptionHandler failed");
Check(memory[0] == 0x5a && !manager.IsTracked(address) &&
context.calls.load(std::memory_order_relaxed) == 1,
"native access violation did not invalidate and resume the store");
manager.OnGpuUnmap(address, page_size);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestInvalidLateWriteTokenIsConsumed() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size);
manager.UpdatePageWatchers(true, address, page_size);
Check(manager.HandleFault(PageFaultAccess::Write, address),
"initial write fault was not handled");
DWORD old_protection = 0;
Check(VirtualProtect(memory, page_size, PAGE_READONLY, &old_protection) !=
0 &&
old_protection == PAGE_READWRITE,
"failed to create invalid late-write protection state");
Check(!manager.HandleFault(PageFaultAccess::Write, address) &&
!manager.HandleFault(PageFaultAccess::Write, address),
"invalid late-write token was accepted or retained");
Check(VirtualProtect(memory, page_size, PAGE_READWRITE, &old_protection) != 0,
"failed to restore test protection");
manager.OnGpuUnmap(address, page_size);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestCrossRegionRange() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
constexpr uint64_t region_size = 4ull * 1024ull * 1024ull;
auto *memory = Allocate(region_size * 2);
const auto base = reinterpret_cast<uint64_t>(memory);
const auto boundary = (base + region_size - 1) & ~(region_size - 1);
Check(boundary >= base + page_size &&
boundary + page_size <= base + region_size * 2,
"test allocation does not contain a region boundary");
manager.OnGpuMap(base, region_size * 2);
manager.UpdatePageWatchers(true, boundary - page_size, page_size * 2);
Check(!IsWritable(reinterpret_cast<void *>(boundary - page_size)) &&
!IsWritable(reinterpret_cast<void *>(boundary)),
"cross-region watch did not protect both pages");
manager.UpdatePageWatchers(false, boundary - page_size, page_size * 2);
Check(IsWritable(reinterpret_cast<void *>(boundary - page_size)) &&
IsWritable(reinterpret_cast<void *>(boundary)),
"cross-region unwatch did not restore both pages");
manager.OnGpuUnmap(base, region_size * 2);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
[[noreturn]] void RunDeathCase(const char *name) {
FaultContext context;
auto manager = std::make_unique<PageManager>(InvalidateFault, &context);
context.manager = manager.get();
const auto page_size = manager->GetPageSize();
if (std::strcmp(name, "invalid-range") == 0) {
manager->UpdatePageWatchers(true, (1ull << 40u) - 1, 2);
} else if (std::strcmp(name, "unknown-untrack") == 0) {
manager->UpdatePageWatchers(false, 0x1000, page_size);
} else {
const bool two_pages = std::strcmp(name, "cross-reentrant") == 0;
auto *memory = Allocate(
two_pages ? page_size * 2 : page_size,
std::strcmp(name, "protection") == 0 ? PAGE_READONLY : PAGE_READWRITE);
const auto address = reinterpret_cast<uint64_t>(memory);
manager->OnGpuMap(address, two_pages ? page_size * 2 : page_size);
manager->UpdatePageWatchers(true, address, page_size);
if (two_pages) {
manager->UpdatePageWatchers(true, address + page_size, page_size);
}
if (std::strcmp(name, "destructor-watch") == 0) {
manager.reset();
} else if (std::strcmp(name, "non-write") == 0) {
(void)manager->HandleFault(PageFaultAccess::Read, address);
} else if (std::strcmp(name, "callback-false") == 0) {
context.result = false;
(void)manager->HandleFault(PageFaultAccess::Write, address);
} else if (std::strcmp(name, "reentrant") == 0) {
context.reenter = true;
(void)manager->HandleFault(PageFaultAccess::Write, address);
} else if (std::strcmp(name, "cross-reentrant") == 0) {
context.reenter = true;
context.reenter_address = address + page_size;
(void)manager->HandleFault(PageFaultAccess::Write, address);
} else if (std::strcmp(name, "concurrent-non-write") == 0) {
context.block = true;
std::thread first(
[&] { (void)manager->HandleFault(PageFaultAccess::Write, address); });
while (!context.entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
(void)manager->HandleFault(PageFaultAccess::Read, address);
first.join();
} else if (std::strcmp(name, "watched-unmap") == 0) {
manager->OnGpuUnmap(address, page_size);
} else if (std::strcmp(name, "protection") != 0) {
std::_Exit(0x7f);
}
}
std::_Exit(0x7f);
}
void CheckDeathCase(const char *name) {
char path[MAX_PATH]{};
Check(GetModuleFileNameA(nullptr, path, MAX_PATH) != 0,
"GetModuleFileName failed");
std::string command = std::string("\"") + path + "\" --death " + name;
std::vector<char> mutable_command(command.begin(), command.end());
mutable_command.push_back('\0');
STARTUPINFOA startup{sizeof(startup)};
PROCESS_INFORMATION process{};
Check(CreateProcessA(nullptr, mutable_command.data(), nullptr, nullptr, FALSE,
CREATE_NO_WINDOW, nullptr, nullptr, &startup,
&process) != 0,
"CreateProcess failed");
Check(WaitForSingleObject(process.hProcess, 10000) == WAIT_OBJECT_0,
"death test timed out");
DWORD exit_code = 0;
Check(
GetExitCodeProcess(process.hProcess, &exit_code) != 0 &&
(exit_code == 322 || exit_code == EXCEPTION_NONCONTINUABLE_EXCEPTION),
"death case did not use the PageManager fatal exit");
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
}
void TestFatalPaths() {
for (const char *name :
{"invalid-range", "unknown-untrack", "destructor-watch", "non-write",
"callback-false", "reentrant", "cross-reentrant",
"concurrent-non-write", "watched-unmap", "protection"}) {
CheckDeathCase(name);
}
}
void TestConcurrentFault() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
context.block = true;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size);
manager.UpdatePageWatchers(true, address, page_size);
bool first_result = false;
bool second_result = false;
std::thread first([&] {
first_result = manager.HandleFault(PageFaultAccess::Write, address);
});
while (!context.entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
std::thread second([&] {
second_result = manager.HandleFault(PageFaultAccess::Write, address);
});
context.release.store(true, std::memory_order_release);
first.join();
second.join();
Check(first_result && second_result &&
context.calls.load(std::memory_order_relaxed) == 1,
"concurrent faults dispatched invalidation more than once");
manager.OnGpuUnmap(address, page_size);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestExternalDirtyTransferDuringResolution() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
context.block = true;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size);
manager.UpdatePageWatchers(true, address, page_size);
bool handled = false;
std::thread fault(
[&] { handled = manager.HandleFault(PageFaultAccess::Write, address); });
while (!context.entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
manager.UpdatePageWatchers(false, address, page_size);
context.release.store(true, std::memory_order_release);
fault.join();
Check(handled && !manager.IsTracked(address) && IsWritable(memory),
"external dirty transfer did not satisfy active resolution");
manager.OnGpuUnmap(address, page_size);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestMappingDoesNotRequireCpuWriteAccess() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
DWORD old_protection = 0;
Check(VirtualProtect(memory, page_size, PAGE_NOACCESS, &old_protection) != 0 &&
old_protection == PAGE_READWRITE,
"failed to prepare CPU-inaccessible mapping");
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size);
Check(manager.IsMapped(address, page_size),
"CPU-inaccessible committed range was not GPU mapped");
manager.OnGpuUnmap(address, page_size);
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestGpuAccessPermissions() {
FaultContext context;
PageManager manager(InvalidateFault, &context);
context.manager = &manager;
const auto page_size = manager.GetPageSize();
auto *memory = Allocate(page_size);
const auto address = reinterpret_cast<uint64_t>(memory);
manager.OnGpuMap(address, page_size, GpuAccess::Read);
Check(manager.HasGpuAccess(address, page_size, GpuAccess::Read) &&
!manager.HasGpuAccess(address, page_size, GpuAccess::Write),
"read-only GPU mapping granted write access");
manager.OnGpuMap(address, page_size, GpuAccess::Write);
Check(manager.HasGpuAccess(address, page_size, GpuAccess::ReadWrite),
"overlapping GPU mappings did not combine permissions");
manager.OnGpuUnmap(address, page_size, GpuAccess::Read);
Check(!manager.HasGpuAccess(address, page_size, GpuAccess::Read) &&
manager.HasGpuAccess(address, page_size, GpuAccess::Write),
"GPU read unmap removed the wrong permission");
manager.OnGpuUnmap(address, page_size, GpuAccess::Write);
Check(!manager.IsMapped(address, page_size),
"GPU permission mappings were not fully balanced");
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
#endif
} // namespace
int main(int argc, char **argv) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (argc == 3 && std::strcmp(argv[1], "--death") == 0) {
RunDeathCase(argv[2]);
}
TestWatchFaultAndUnwatch();
TestSharedWatcherFault();
TestReadWriteWatcherFault();
TestPermittedMappedLateFaultsResume();
TestPartialMappingUnmapPreservesTokens();
TestNativeDelayedReadAfterModeDowngrade();
TestDelayedFaultAfterExplicitUnwatch();
TestNativeAccessViolation();
TestInvalidLateWriteTokenIsConsumed();
TestCrossRegionRange();
TestConcurrentFault();
TestExternalDirtyTransferDuringResolution();
TestMappingDoesNotRequireCpuWriteAccess();
TestGpuAccessPermissions();
TestFatalPaths();
std::puts("PageManagerTests: all cases passed");
return 0;
#else
(void)argc;
(void)argv;
std::fputs("PageManagerTests: unsupported platform\n", stderr);
return 1;
#endif
}
+244
View File
@@ -0,0 +1,244 @@
#include "graphics/host_gpu/renderer/resourceMutex.h"
#include <atomic>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#undef min
#undef max
#endif
namespace {
using Libs::Graphics::ResourceMutex;
void Check(bool value, const char* text) {
if (!value) {
std::fprintf(stderr, "ResourceMutexTests: failed: %s\n", text);
std::abort();
}
}
void YieldMany() {
for (uint32_t i = 0; i < 4096; i++) {
std::this_thread::yield();
}
}
void TestFaultBlocksPublisher() {
ResourceMutex mutex;
std::atomic_bool publisher_started {false};
std::atomic_bool publisher_entered {false};
std::thread publisher;
{
ResourceMutex::FaultScope fault(mutex);
publisher = std::thread([&] {
publisher_started.store(true, std::memory_order_release);
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
});
while (!publisher_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered during active fault transaction");
}
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after fault transaction");
}
void TestFaultDrainsExistingOwner() {
ResourceMutex mutex;
std::unique_lock owner(mutex);
std::atomic_bool fault_started {false};
std::atomic_bool fault_entered {false};
std::atomic_bool release_fault {false};
std::atomic_bool publisher_started {false};
std::atomic_bool publisher_entered {false};
std::thread fault([&] {
fault_started.store(true, std::memory_order_release);
ResourceMutex::FaultScope scope(mutex);
fault_entered.store(true, std::memory_order_release);
while (!release_fault.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
});
while (!fault_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!fault_entered.load(std::memory_order_acquire),
"fault transaction did not wait for existing owner");
owner.unlock();
while (!fault_entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
std::thread publisher([&] {
publisher_started.store(true, std::memory_order_release);
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
});
while (!publisher_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered before drained fault completed");
release_fault.store(true, std::memory_order_release);
fault.join();
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after drained fault");
}
void TestFaultScopesSerialize() {
ResourceMutex mutex;
std::atomic_bool first_entered {false};
std::atomic_bool second_started {false};
std::atomic_bool second_entered {false};
std::atomic_bool release_first {false};
std::atomic_bool release_second {false};
std::atomic_bool publisher_entered {false};
std::thread first([&] {
ResourceMutex::FaultScope scope(mutex);
first_entered.store(true, std::memory_order_release);
while (!release_first.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
});
while (!first_entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
std::thread second([&] {
second_started.store(true, std::memory_order_release);
ResourceMutex::FaultScope scope(mutex);
second_entered.store(true, std::memory_order_release);
while (!release_second.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
});
while (!second_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!second_entered.load(std::memory_order_acquire),
"second fault transaction entered before first completed");
release_first.store(true, std::memory_order_release);
while (!second_entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
std::thread publisher([&] {
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
});
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered during second fault transaction");
release_second.store(true, std::memory_order_release);
first.join();
second.join();
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after serialized faults");
}
void TestPreownedFaultKeepsResourceTransaction() {
ResourceMutex mutex;
std::atomic_bool publisher_started {false};
std::atomic_bool publisher_entered {false};
std::thread publisher;
std::unique_lock owner(mutex);
{
ResourceMutex::FaultScope fault(mutex);
publisher = std::thread([&] {
publisher_started.store(true, std::memory_order_release);
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
});
while (!publisher_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered during preowned fault transaction");
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"preowned fault released its outer resource transaction");
owner.unlock();
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after outer transaction");
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
[[noreturn]] void RunDeathCase(const char* name) {
ResourceMutex mutex;
if (std::strcmp(name, "recursive-lock") == 0) {
std::lock_guard first(mutex);
std::lock_guard second(mutex);
} else if (std::strcmp(name, "nested-fault") == 0) {
ResourceMutex::FaultScope first(mutex);
ResourceMutex::FaultScope second(mutex);
}
std::_Exit(0x7f);
}
void CheckDeathCase(const char* name) {
char path[MAX_PATH] {};
Check(GetModuleFileNameA(nullptr, path, MAX_PATH) != 0, "GetModuleFileName failed");
std::string command = std::string("\"") + path + "\" --death " + name;
std::vector<char> mutable_command(command.begin(), command.end());
mutable_command.push_back('\0');
STARTUPINFOA startup {sizeof(startup)};
PROCESS_INFORMATION process {};
Check(CreateProcessA(nullptr, mutable_command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
nullptr, nullptr, &startup, &process) != 0,
"CreateProcess failed");
const auto wait = WaitForSingleObject(process.hProcess, 10000);
if (wait != WAIT_OBJECT_0) {
TerminateProcess(process.hProcess, 0x7e);
}
Check(wait == WAIT_OBJECT_0, "ResourceMutex death test timed out");
DWORD exit_code = 0;
Check(GetExitCodeProcess(process.hProcess, &exit_code) != 0 && exit_code == 321,
"ResourceMutex death path used the wrong exit");
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
}
#endif
} // namespace
int main(int argc, char** argv) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (argc == 3 && std::strcmp(argv[1], "--death") == 0) {
RunDeathCase(argv[2]);
}
#else
(void)argc;
(void)argv;
#endif
TestFaultBlocksPublisher();
TestFaultDrainsExistingOwner();
TestFaultScopesSerialize();
TestPreownedFaultKeepsResourceTransaction();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
CheckDeathCase("recursive-lock");
CheckDeathCase("nested-fault");
#endif
std::puts("ResourceMutexTests: all cases passed");
return 0;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+107
View File
@@ -0,0 +1,107 @@
#include "graphics/shader/shader.h"
#include "graphics/shader/recompiler/ResourceMaterialization.h"
#include "graphics/shader/recompiler/ShaderIR.h"
#include <array>
#include <cstdio>
#include <cstdlib>
#include <memory>
namespace {
void Check(bool value, const char* text) {
if (!value) {
std::fprintf(stderr, "ShaderStageRuntimeTests: failed: %s\n", text);
std::abort();
}
}
std::shared_ptr<const Libs::Graphics::ShaderRecompiler::IR::Program> SrtProgram(uint64_t address) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
program.srt_plan_complete = true;
program.srt_patching_complete = true;
program.resource_tracking_complete = true;
program.provenance.values.resize(6);
program.provenance.values[0].op = ScalarValueOp::Undefined;
program.provenance.values[1].op = ScalarValueOp::Unknown;
program.provenance.values[2].op = ScalarValueOp::Constant;
program.provenance.values[2].imm = static_cast<uint32_t>(address);
program.provenance.values[3].op = ScalarValueOp::Constant;
program.provenance.values[3].imm = static_cast<uint32_t>(address >> 32u);
program.provenance.values[4].op = ScalarValueOp::Constant;
program.provenance.values[5].op = ScalarValueOp::ReadConst;
program.provenance.values[5].args = {2, 3, 4};
program.srt.reads.push_back({5, 0, 0x40});
return std::make_shared<const Program>(std::move(program));
}
std::shared_ptr<const Libs::Graphics::ShaderRecompiler::IR::Program> UnbasedFlatProgram() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
program.srt_plan_complete = true;
program.srt_patching_complete = true;
program.resource_tracking_complete = true;
AddressResource address;
address.source = ScalarProvenance::Unknown;
address.first_use_pc = 0x80;
address.kind = ResourceKind::Flat;
program.info.addresses.push_back(address);
return std::make_shared<const Program>(std::move(program));
}
void TestFailurePreservesPriorStage() {
using namespace Libs::Graphics;
auto cached_program = SrtProgram(0x1001);
auto prior_program = std::make_shared<const ShaderRecompiler::IR::Program>();
auto prior_resources = std::make_shared<const ShaderRecompiler::IR::ResourceSnapshot>();
ShaderStageRuntime stage {prior_program, prior_resources};
const std::array<uint32_t, 1> user_data = {};
std::string error;
Check(!ShaderMaterializeStageRuntime(cached_program, user_data, 0, &stage, &error) &&
error.find("failed at 0x0000000000001000") != std::string::npos,
"unmapped cache ReadConst did not fail through the checked production reader");
Check(stage.program == prior_program && stage.resources == prior_resources,
"failed cache rematerialization replaced the prior stage snapshot");
}
void TestMappedSrtUsesCheckedReaderByDefault() {
using namespace Libs::Graphics;
const uint32_t dword = 0x12345678;
auto cached_program = SrtProgram(reinterpret_cast<uint64_t>(&dword));
ShaderStageRuntime stage;
std::string error;
Check(ShaderMaterializeStageRuntime(cached_program, {}, 0, &stage, &error), error.c_str());
Check(stage.program == cached_program && stage.resources != nullptr,
"cache rematerialization did not publish the mapped stage");
Check(stage.resources->flattened_srt.size() == 1 &&
stage.resources->flattened_srt[0] == dword,
"cache rematerialization did not use the checked reader by default");
}
void TestUnbasedFlatCacheHitFailsClosed() {
using namespace Libs::Graphics;
auto cached_program = UnbasedFlatProgram();
auto prior_program = std::make_shared<const ShaderRecompiler::IR::Program>();
auto prior_resources = std::make_shared<const ShaderRecompiler::IR::ResourceSnapshot>();
ShaderStageRuntime stage {prior_program, prior_resources};
std::string error;
Check(!ShaderMaterializeStageRuntime(cached_program, {}, 0, &stage, &error) &&
error.find("requires runtime guest-address translation") != std::string::npos,
"unbased FLAT cache hit did not fail without a runtime translator");
Check(stage.program == prior_program && stage.resources == prior_resources,
"unbased FLAT cache rejection replaced the prior stage snapshot");
}
} // namespace
int main() {
TestFailurePreservesPriorStage();
TestMappedSrtUsesCheckedReaderByDefault();
TestUnbasedFlatCacheHitFailsClosed();
std::puts("ShaderStageRuntimeTests: all cases passed");
return 0;
}
+152
View File
@@ -0,0 +1,152 @@
#include "graphics/shader/shaderVertexMetadata.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#undef min
#undef max
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX
#include <sys/mman.h>
#include <unistd.h>
#endif
namespace {
using namespace Libs::Graphics;
void Check(bool value, const char* text) {
if (!value) {
std::fprintf(stderr, "ShaderVertexMetadataTests: failed: %s\n", text);
std::abort();
}
}
struct Fixture {
std::array<uint16_t, static_cast<size_t>(AgcDirectResourceType::Last) + 1> offsets {};
ShaderUserData user_data {};
ShaderSemantic semantic {};
ShaderMappedData mapped {};
Fixture() {
offsets.fill(AGC_ILLEGAL_DIRECT_OFFSET);
offsets[static_cast<size_t>(AgcDirectResourceType::PtrVertexBufferTable)] = 2;
offsets[static_cast<size_t>(AgcDirectResourceType::PtrVertexAttribDescTable)] = 4;
user_data.direct_resource_offset = offsets.data();
user_data.direct_resource_count = static_cast<uint16_t>(offsets.size());
mapped.user_data = &user_data;
mapped.input_semantics = &semantic;
mapped.num_input_semantics = 1;
}
};
void CheckRejected(const ShaderMappedData& data, const char* text) {
ShaderVertexMetadata output;
output.vertex_buffer_reg = 37;
output.vertex_attrib_reg = 41;
output.input_semantics_count = 7;
std::string error;
Check(!ShaderReadVertexMetadata(data, 64, &output, &error), text);
Check(!error.empty(), "metadata rejection omitted its diagnostic");
Check(output.vertex_buffer_reg == 37 && output.vertex_attrib_reg == 41 &&
output.input_semantics_count == 7,
"metadata rejection changed the prior output");
}
void TestValidAndHostileMetadata() {
Fixture fixture;
ShaderVertexMetadata output;
std::string error;
Check(ShaderReadVertexMetadata(fixture.mapped, 64, &output, &error),
"valid AGC vertex metadata was rejected");
Check(output.vertex_buffer_reg == 2 && output.vertex_attrib_reg == 4 &&
output.input_semantics_count == 1,
"valid AGC vertex metadata was decoded incorrectly");
auto unreadable = fixture.mapped;
unreadable.user_data = reinterpret_cast<ShaderUserData*>(uintptr_t {1});
CheckRejected(unreadable, "unreadable AGC user-data header was accepted");
Fixture excessive_resources;
excessive_resources.user_data.direct_resource_count =
static_cast<uint16_t>(excessive_resources.offsets.size() + 1);
CheckRejected(excessive_resources.mapped, "excessive direct-resource count was accepted");
Fixture excessive_semantics;
excessive_semantics.mapped.num_input_semantics = ShaderVertexInputInfo::RES_MAX + 1;
CheckRejected(excessive_semantics.mapped, "excessive vertex semantic count was accepted");
Fixture excessive_register;
excessive_register.offsets[
static_cast<size_t>(AgcDirectResourceType::PtrVertexBufferTable)] = 63;
CheckRejected(excessive_register.mapped, "out-of-domain vertex table SGPR was accepted");
Fixture bad_semantics;
bad_semantics.mapped.input_semantics =
reinterpret_cast<ShaderSemantic*>(uintptr_t {1});
CheckRejected(bad_semantics.mapped, "unreadable vertex semantic array was accepted");
}
void TestCrossPageMetadata() {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
SYSTEM_INFO system_info {};
GetSystemInfo(&system_info);
const auto page_size = static_cast<uint64_t>(system_info.dwPageSize);
auto* pages = static_cast<uint8_t*>(
VirtualAlloc(nullptr, page_size * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
Check(pages != nullptr, "failed to allocate metadata test pages");
DWORD old_protect = 0;
Check(VirtualProtect(pages + page_size, page_size, PAGE_NOACCESS, &old_protect) != 0,
"failed to protect metadata test page");
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX
const auto native_page_size = sysconf(_SC_PAGESIZE);
Check(native_page_size > 0, "failed to query page size");
const auto page_size = static_cast<uint64_t>(native_page_size);
auto* pages = static_cast<uint8_t*>(
mmap(nullptr, page_size * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
Check(pages != MAP_FAILED, "failed to allocate metadata test pages");
Check(mprotect(pages + page_size, page_size, PROT_NONE) == 0,
"failed to protect metadata test page");
#else
return;
#endif
Fixture fixture;
auto header_crossing = fixture.mapped;
header_crossing.user_data = reinterpret_cast<ShaderUserData*>(
pages + page_size - sizeof(ShaderUserData) + 1);
CheckRejected(header_crossing, "cross-page AGC user-data header was accepted");
auto offsets_crossing = fixture.mapped;
fixture.user_data.direct_resource_offset =
reinterpret_cast<uint16_t*>(pages + page_size - sizeof(uint16_t));
CheckRejected(offsets_crossing, "cross-page direct-resource offsets were accepted");
fixture.user_data.direct_resource_offset = fixture.offsets.data();
auto semantics_crossing = fixture.mapped;
semantics_crossing.input_semantics =
reinterpret_cast<ShaderSemantic*>(pages + page_size - sizeof(uint16_t));
CheckRejected(semantics_crossing, "cross-page vertex semantics were accepted");
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
Check(VirtualFree(pages, 0, MEM_RELEASE) != 0, "failed to free metadata test pages");
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX
Check(munmap(pages, page_size * 2) == 0, "failed to free metadata test pages");
#endif
}
} // namespace
int main() {
TestValidAndHostileMetadata();
TestCrossPageMetadata();
std::puts("ShaderVertexMetadataTests: all cases passed");
return 0;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff