diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d0bf61b2a..852b8659c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -325,6 +325,29 @@ foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() +add_test_executable(agc_driver_register_key_tests EXCLUDE_FROM_ALL tests/RegisterKey.cpp) +target_include_directories(agc_driver_register_key_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_driver_register_key_tests PRIVATE + libSceAgcDriver + libc + libkernel +) +set_target_properties(agc_driver_register_key_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_register_key_tests) +if(BUILD_TESTING) + set_target_properties(agc_driver_register_key_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_register_key COMMAND agc_driver_register_key_tests) + set_tests_properties(agc_driver_register_key PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_VERIFY_REGISTER_KEYS=1") + if(WIN32) + set_tests_properties(agc_driver_register_key PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_tests EXCLUDE_FROM_ALL tests/Submit.cpp ) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp new file mode 100644 index 000000000..74a562c8b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP + +#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" + +namespace AgcDriver::DriverDetail { + +std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords = false, bool incremental = true); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index 4e9743773..f270c9451 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,29 @@ public: using mapped_type = std::uint32_t; using value_type = std::pair; + class Mask { + public: + explicit Mask(std::vector bits) : bits(std::move(bits)) {} + bool Contains(std::uint32_t offset) const { return offset / 64 < bits.size() && ((bits[offset / 64] >> (offset % 64)) & 1u) != 0; } + private: + std::vector bits; + }; + + class Reference { + public: + Reference(Registers& owner, std::uint32_t offset) : owner(owner), offset(offset) {} + operator std::uint32_t() const { return owner.values[offset]; } + Reference& operator=(std::uint32_t value) { owner.insert_or_assign(offset, value); return *this; } + Reference& operator=(const Reference& other) { return *this = static_cast(other); } + Reference& operator|=(std::uint32_t value) { return *this = static_cast(*this) | value; } + Reference& operator&=(std::uint32_t value) { return *this = static_cast(*this) & value; } + Reference& operator^=(std::uint32_t value) { return *this = static_cast(*this) ^ value; } + Reference& operator+=(std::uint32_t value) { return *this = static_cast(*this) + value; } + private: + Registers& owner; + std::uint32_t offset; + }; + class const_iterator { public: using iterator_category = std::forward_iterator_tag; @@ -57,6 +81,18 @@ public: using iterator = const_iterator; Registers() = default; + Registers(const Registers&) = default; + Registers& operator=(const Registers&) = default; + Registers(Registers&& other) noexcept : values(std::move(other.values)), present(std::move(other.present)), entries(std::exchange(other.entries, 0)), mask(std::move(other.mask)), fingerprint(std::exchange(other.fingerprint, 0)) {} + Registers& operator=(Registers&& other) noexcept { + if (this == &other) return *this; + values = std::move(other.values); + present = std::move(other.present); + entries = std::exchange(other.entries, 0); + mask = std::move(other.mask); + fingerprint = std::exchange(other.fingerprint, 0); + return *this; + } Registers(std::initializer_list> entries) { for (const auto& [offset, value] : entries) emplace(offset, value); } @@ -83,27 +119,33 @@ public: values.clear(); present.clear(); entries = 0; + fingerprint = 0; } std::pair emplace(std::uint32_t offset, std::uint32_t value) { if (contains(offset)) return {const_iterator{this, offset}, false}; mark(offset) = value; + changeFingerprint(offset, value); return {const_iterator{this, offset}, true}; } std::pair insert_or_assign(std::uint32_t offset, std::uint32_t value) { if (contains(offset)) { + if (values[offset] == value) return {const_iterator{this, offset}, false}; + changeFingerprint(offset, values[offset]); values[offset] = value; + changeFingerprint(offset, value); return {const_iterator{this, offset}, false}; } mark(offset) = value; + changeFingerprint(offset, value); return {const_iterator{this, offset}, true}; } - std::uint32_t& operator[](std::uint32_t offset) { - if (contains(offset)) return values[offset]; - return mark(offset) = 0; + Reference operator[](std::uint32_t offset) { + if (!contains(offset)) emplace(offset, 0); + return {*this, offset}; } - std::uint32_t& at(std::uint32_t offset) { + Reference at(std::uint32_t offset) { if (!contains(offset)) throw std::out_of_range("register is not set"); - return values[offset]; + return {*this, offset}; } const std::uint32_t& at(std::uint32_t offset) const { if (!contains(offset)) throw std::out_of_range("register is not set"); @@ -111,6 +153,7 @@ public: } std::size_t erase(std::uint32_t offset) { if (!contains(offset)) return 0; + changeFingerprint(offset, values[offset]); present[offset / 64] &= ~(std::uint64_t{1} << (offset % 64)); --entries; return 1; @@ -123,8 +166,31 @@ public: } return a == end() && b == other.end(); } + std::uint64_t Fingerprint(const std::shared_ptr& selected) const { + if (mask != selected) { + fingerprint = RecomputeFingerprint(*selected); + mask = selected; + } + return fingerprint; + } + std::uint64_t RecomputeFingerprint(const Mask& selected) const { + std::uint64_t result = 0; + for (const auto [offset, value] : *this) { + if (selected.Contains(offset)) result ^= contribution(offset, value); + } + return result; + } private: + static std::uint64_t contribution(std::uint32_t offset, std::uint32_t value) { + auto hash = ((static_cast(offset) << 32u) | value) + 0x9e3779b97f4a7c15ull; + hash = (hash ^ (hash >> 30u)) * 0xbf58476d1ce4e5b9ull; + hash = (hash ^ (hash >> 27u)) * 0x94d049bb133111ebull; + return hash ^ (hash >> 31u); + } + void changeFingerprint(std::uint32_t offset, std::uint32_t value) { + if (mask != nullptr && mask->Contains(offset)) fingerprint ^= contribution(offset, value); + } static constexpr std::size_t End = ~std::size_t{0}; std::uint32_t& mark(std::uint32_t offset) { if (offset >= values.size()) { @@ -147,6 +213,8 @@ private: std::vector values; std::vector present; std::size_t entries = 0; + mutable std::shared_ptr mask; + mutable std::uint64_t fingerprint = 0; }; inline Registers BuildInitialContextRegisters() { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 3612fb3c3..c19e71439 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" #include #include #include @@ -6,36 +7,69 @@ namespace AgcDriver::DriverDetail { +namespace { + +bool runtimeRegister(std::uint32_t offset) { + return offset - 0x00cu < 32u || offset - 0x08cu < 32u || offset - 0x10cu < 32u || offset == 0x082u || offset == 0x083u || offset == 0x102u || offset == 0x103u; +} + +const auto& registerMasks() { + static const auto masks = [] { + std::array, 3> bits; + for (const auto& range : Graphics::DrawKeyRegisters) { + auto& bank = bits[static_cast(range.bank)]; + bank.resize(std::max(bank.size(), (static_cast(range.first) + range.count + 63) / 64)); + for (auto offset = range.first; offset < range.first + range.count; ++offset) { + if (range.bank == Graphics::RegisterBank::Shader && runtimeRegister(offset)) continue; + bank[offset / 64] |= std::uint64_t{1} << (offset % 64); + } + } + std::array, 3> result; + for (std::size_t i = 0; i < bits.size(); ++i) result[i] = std::make_shared(std::move(bits[i])); + return result; + }(); + return masks; +} + +} + +std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental) { + const auto& masks = registerMasks(); + const auto fingerprint = [&](const Registers& registers, Graphics::RegisterBank bank) { + const auto& mask = masks[static_cast(bank)]; + return incremental ? registers.Fingerprint(mask) : registers.RecomputeFingerprint(*mask); + }; + auto shape = (0xcbf29ce484222325ull ^ fingerprint(queue.context, Graphics::RegisterBank::Context)) * 0x100000001b3ull; + shape = (shape ^ fingerprint(queue.shader, Graphics::RegisterBank::Shader)) * 0x100000001b3ull; + shape = (shape ^ fingerprint(queue.userConfig, Graphics::RegisterBank::UserConfig)) * 0x100000001b3ull; + auto exact = shape; + const auto words = [&](std::uint32_t base, std::uint32_t count) { + for (auto it = queue.shader.lower_bound(base); it != queue.shader.end() && it->first < base + count; ++it) { + exact = (exact ^ it->first) * 0x100000001b3ull; + exact = (exact ^ it->second) * 0x100000001b3ull; + } + }; + for (const auto base : {0x00cu, 0x08cu, 0x10cu}) { + const auto resources = queue.shader.find(base - 1); + const auto count = resources == queue.shader.end() ? 0u : ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); + words(base, allUserWords ? 32u : std::min(count, 32u)); + } + words(0x082u, 2); + words(0x102u, 2); + return exact; +} + + std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial) { static const bool allUserWords = std::getenv("APS5_DRAW_KEY_ALL_USER_WORDS") != nullptr; - std::uint64_t key = 0xcbf29ce484222325ull; + static const bool verify = std::getenv("APS5_VERIFY_REGISTER_KEYS") != nullptr; + auto key = RegisterStateKey(queue, allUserWords); + if (verify && key != RegisterStateKey(queue, allUserWords, false)) throw std::runtime_error("incremental register fingerprint disagrees with full state"); const auto mix = [&](std::uint64_t value) { key ^= value; key *= 0x100000001b3ull; }; - const auto userEnd = [&](std::uint32_t base) { - if (allUserWords) return base + 32u; - const auto resources = queue.shader.find(base - 1); - if (resources == queue.shader.end()) return base; - const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); - return base + std::min(count, 32u); - }; - const std::array, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}}; - const auto unread = [&](std::uint32_t offset) { - return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; }); - }; mix(deviceSerial); - for (const auto& range : Graphics::DrawKeyRegisters) { - const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig; - mix((static_cast(range.bank) << 32u) | range.first); - const auto end = range.first + range.count; - const bool shader = range.bank == Graphics::RegisterBank::Shader; - for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) { - if (shader && unread(it->first)) continue; - mix(it->first); - mix(it->second); - } - } for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) { const auto low = queue.shader.find(base); const auto high = queue.shader.find(base + 1); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index a6c4230d0..78598011e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2378,7 +2378,7 @@ void AlphaToCoverageTests() { Require(AgcDriver::Graphics::DrawRejection(queue, false).find("alpha-to-coverage") != std::string::npos, "the precheck accepted unsupported alpha-to-coverage"); } queue.context[0x2dc] = 0xaa01; - const auto exportFormat = queue.context.at(0x1c5); + const std::uint32_t exportFormat = queue.context.at(0x1c5); for (const auto format : {4u, 5u, 6u, 9u}) { queue.context[0x1c5] = (exportFormat & ~0xfu) | format; Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage rejected an MRT0 export format with a floating-point alpha"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 5c6394789..490ba22a3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -50,8 +51,10 @@ std::uint32_t low(const void* pointer) { return static_cast(reint std::uint32_t high(const void* pointer) { return static_cast(reinterpret_cast(pointer) >> 32u); } void execute(AgcDriver::QueueState& state, const std::vector& packet) { + AgcDriver::DriverDetail::RegisterStateKey(state); AgcDriver::Pm4::Validate(packet, 0); AgcDriver::Pm4::Execute(packet, state); + check(AgcDriver::DriverDetail::RegisterStateKey(state) == AgcDriver::DriverDetail::RegisterStateKey(state, false, false), "PM4 changed registers without updating their fingerprint"); } void testCatalog() { diff --git a/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp b/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp new file mode 100644 index 000000000..448d3455a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp @@ -0,0 +1,140 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include +#include +#include +#include +#include + +using namespace AgcDriver; +using namespace AgcDriver::DriverDetail; + +namespace { + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::uint64_t baselineRegisterStateKey(const QueueState& queue, bool allUserWords = false) { + std::uint64_t key = 0xcbf29ce484222325ull; + const auto mix = [&](std::uint64_t value) { + key ^= value; + key *= 0x100000001b3ull; + }; + const auto userEnd = [&](std::uint32_t base) { + if (allUserWords) return base + 32u; + const auto resources = queue.shader.find(base - 1); + if (resources == queue.shader.end()) return base; + const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); + return base + std::min(count, 32u); + }; + const std::array, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}}; + const auto unread = [&](std::uint32_t offset) { + return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; }); + }; + for (const auto& range : Graphics::DrawKeyRegisters) { + const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig; + mix((static_cast(range.bank) << 32u) | range.first); + const auto end = range.first + range.count; + const bool shader = range.bank == Graphics::RegisterBank::Shader; + for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) { + if (shader && unread(it->first)) continue; + mix(it->first); + mix(it->second); + } + } + return key; +} + +void testFingerprints() { + static_assert(!std::is_convertible_v); + std::mt19937 random(42); + QueueState queue; + auto reference = baselineRegisterStateKey(queue); + auto tracked = RegisterStateKey(queue); + const auto compare = [&] { + const auto expected = baselineRegisterStateKey(queue); + const auto actual = RegisterStateKey(queue); + check(actual == RegisterStateKey(queue, false, false), "incremental fingerprint missed a mutation"); + check(RegisterStateKey(queue, true) == RegisterStateKey(queue, true, false), "all-user fingerprint missed a mutation"); + check((expected == reference) == (actual == tracked), "exact key changed its dependency set"); + reference = expected; + tracked = actual; + }; + for (const auto& range : Graphics::DrawKeyRegisters) { + auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig; + for (auto offset = range.first; offset < range.first + range.count; ++offset) { + bank.erase(offset); compare(); + bank.emplace(offset, 0); compare(); + bank.insert_or_assign(offset, 0); compare(); + bank[offset] = random(); compare(); + bank.at(offset) ^= 1; compare(); + } + } + for (std::uint32_t i = 0; i < 30000; ++i) { + auto& bank = i % 3 == 0 ? queue.context : i % 3 == 1 ? queue.shader : queue.userConfig; + const auto offset = random() % 0x500; + switch (i % 11) { + case 0: bank.erase(offset); break; + case 1: bank.emplace(offset, random()); break; + case 2: bank[offset] |= random(); break; + case 3: bank[offset] &= random(); break; + case 6: bank[offset] += random(); break; + case 4: { + auto retained = bank[offset]; + bank.insert_or_assign(0x1000, random()); + compare(); + retained = random(); + break; + } + case 5: { + auto copy = bank; + copy.insert_or_assign(offset, random()); + compare(); + bank = copy; + break; + } + default: bank.insert_or_assign(offset, random()); break; + } + compare(); + } + queue.shader[0x8b] = 0; compare(); + queue.shader[0x8c] = random(); compare(); + queue.shader[0x8b] = 2; compare(); + queue.shader[0x8b] = 1u << 27u; compare(); + queue.shader.erase(0x8b); compare(); + queue.savedContext = queue.context; + queue.ClearContext(); compare(); + queue.context = *queue.savedContext; compare(); + auto moved = std::move(queue.context); + check(queue.context.empty(), "moved register bank retained its population"); + queue.context.emplace(0x200, 1); compare(); + queue.context = std::move(moved); compare(); + queue.context = queue.context; compare(); + queue.context.clear(); compare(); + queue.context.emplace(0x200, 0); compare(); + queue = QueueState{}; compare(); + Registers registers{{0, 7}, {64, 9}}; + auto low = std::make_shared(std::vector{1}); + auto high = std::make_shared(std::vector{0, 1}); + const auto lowHash = registers.Fingerprint(low); + check(registers.Fingerprint(high) != lowHash && registers.Fingerprint(low) == lowHash, "projection switch reused another mask's hash"); + registers[64] = 8; + check(registers.Fingerprint(low) == lowHash && registers.Fingerprint(high) == registers.RecomputeFingerprint(*high), "projection switch lost a masked write"); +} + + +} + +int main() { + try { + testFingerprints(); + std::puts("Register fingerprint dependencies and mutations passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +}