From bff970af554a1e928d476e55f217562c36262702 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Wed, 7 Oct 2026 20:39:48 +0300 Subject: [PATCH] perf(graphics): retain vertex descriptor decode plans Separate immutable shader semantics from live vertex tables. Coalesce only adjacent referenced words and buffer descriptors, preserving exact read coverage and original semantic ordering. Written with AI assistance. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 + .../include/Driver/Draw/DrawCache.hpp | 3 + .../Execution/src/Driver/Draw/Draw.cpp | 2 +- .../Execution/src/Driver/Draw/DrawDecode.cpp | 1 + .../Graphics/include/ShaderInputState.hpp | 20 ++ .../Graphics/src/ShaderInputState.cpp | 110 +++++--- .../prx/libSceAgcDriver/tests/VertexStage.cpp | 261 ++++++++++++++++++ 7 files changed, 366 insertions(+), 42 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/VertexStage.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 371ccc8c20e..23b723da783 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -380,6 +380,17 @@ target_link_libraries(agc_shared_write_tests PRIVATE libSceAgcDriver libkernel l set_target_properties(agc_shared_write_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shared_write_tests) +add_test_executable(agc_vertex_stage_tests EXCLUDE_FROM_ALL tests/VertexStage.cpp) +target_include_directories(agc_vertex_stage_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +target_link_libraries(agc_vertex_stage_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_vertex_stage_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vertex_stage_tests) +if(BUILD_TESTING) + set_target_properties(agc_vertex_stage_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_vertex_stage COMMAND agc_vertex_stage_tests) + set_tests_properties(agc_vertex_stage PROPERTIES TIMEOUT 30) +endif() + add_test_executable(agc_write_tracking_tests EXCLUDE_FROM_ALL tests/WriteTracking.cpp Execution/src/GuestMemory.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 5fa2834621b..983f8b94dcc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -12,6 +12,8 @@ #include #include +namespace AgcDriver::Graphics { class VertexStagePlan; } + namespace AgcDriver::DriverDetail { struct DrawProgram { @@ -23,6 +25,7 @@ struct DrawProgram { std::shared_ptr snapshot; std::size_t codeOffset = 0; + std::shared_ptr vertexInputs; }; struct DrawDecode { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index dafff997f9b..f3d0a1fc0f4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -144,7 +144,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto& program = programs[i]; if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return; decodeReads[i].clear(); - vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); + vertexInfos[i] = program.vertexInputs->Read(program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207)); }; if (!registerKey) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 590cf275098..50f3d63ead9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -61,6 +61,7 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const roles.reserve(5); const auto append = [&](std::uint32_t base, std::uint8_t type, Stage stage, std::uint32_t resources, std::uint32_t users, Role role) { programs.push_back(prepare(programAddress(base), type, stage, resources, users)); + if (role != Role::GeometryBack && role != Role::Fragment) programs.back().vertexInputs = std::make_shared(programs.back().binary.header, programs.back().binary.headerAddress); roles.push_back(role); }; const auto initializeMerged = [&](DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 6ae3f8ab60b..8683bfbf749 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -19,6 +19,26 @@ struct DecodeRead { std::uint64_t address; std::vector bytes; }; +class VertexStagePlan { +public: + VertexStagePlan(std::span header, std::uint64_t headerAddress); + ShaderRecompiler::ShaderVertexStageInfo Read(std::span userData, std::vector* reads = nullptr, bool staticAbi = false, std::uint32_t hiddenUserWords = 0) const; + +private: + struct Attribute { + std::uint8_t semantic; + std::uint8_t destination; + std::uint8_t components; + }; + struct ReadRun { + std::uint16_t first; + std::uint16_t count; + }; + std::int32_t bufferRegister = -1; + std::int32_t attributeRegister = -1; + std::vector attributes; + std::vector attributeReads; +}; ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads = nullptr, bool staticAbi = false, std::uint32_t hiddenUserWords = 0); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 61cfa6b4204..f2fa1e48045 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -5,6 +5,8 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include +#include +#include #include #include #include @@ -174,12 +176,11 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con }; } -ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi, std::uint32_t hiddenUserWords) { - if (header.empty()) return {}; +VertexStagePlan::VertexStagePlan(std::span header, std::uint64_t headerAddress) { + if (header.empty()) return; if (header.size() < sizeof(Shader)) throw std::runtime_error("AGC graphics: shader header is smaller than the fixed AGC header"); Shader shader; std::memcpy(&shader, header.data(), sizeof(Shader)); - ShaderRecompiler::ShaderVertexStageInfo info{}; if (shader.user_data == nullptr) throw std::runtime_error("AGC graphics: missing AGC user-data header"); constexpr auto userDataBytes = offsetof(ShaderUserData, sharp_resource_count) + sizeof(ShaderUserData{}.sharp_resource_count); const auto userDataHeader = _readHeaderPod(header, headerAddress, shader.user_data, userDataBytes); @@ -187,65 +188,88 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span directOffsets{}; directOffsets.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET); if (userDataHeader.direct_resource_count != 0) _readHeaderArray(header, headerAddress, userDataHeader.direct_resource_offset, userDataHeader.direct_resource_count, directOffsets.data()); - std::int32_t vertexBufferReg = -1; - std::int32_t vertexAttribReg = -1; for (std::uint32_t type = 0; type < userDataHeader.direct_resource_count; ++type) { const auto reg = directOffsets[type]; if (reg == ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET) continue; - if (type == static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)) vertexBufferReg = reg; - if (type == static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexAttribDescTable)) vertexAttribReg = reg; + if (type == static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)) bufferRegister = reg; + if (type == static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexAttribDescTable)) attributeRegister = reg; } - if (vertexAttribReg < 0) return info; - if (vertexBufferReg < 0) throw std::runtime_error("AGC graphics: vertex attribute table requires a vertex buffer table"); - const auto bufferWord = static_cast(vertexBufferReg) + hiddenUserWords; - const auto attribWord = static_cast(vertexAttribReg) + hiddenUserWords; - if (bufferWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain"); - if (attribWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain"); + if (attributeRegister < 0) return; + if (bufferRegister < 0) throw std::runtime_error("AGC graphics: vertex attribute table requires a vertex buffer table"); if (shader.num_input_semantics == 0 || shader.num_input_semantics > ShaderRecompiler::ShaderVertexStageInfo::MaxResources) throw std::runtime_error("AGC graphics: vertex semantic count is outside the supported domain"); if (shader.input_semantics == nullptr) throw std::runtime_error("AGC graphics: missing vertex input semantics"); std::array semantics{}; _readHeaderArray(header, headerAddress, shader.input_semantics, shader.num_input_semantics, semantics.data()); + attributes.reserve(shader.num_input_semantics); + std::array indices; + for (std::uint32_t i = 0; i < shader.num_input_semantics; ++i) { + const auto& semantic = semantics[i]; + if (semantic.static_vb_index == 1 || semantic.static_attribute == 1) throw std::runtime_error("AGC graphics: statically bound vertex attributes are not implemented"); + attributes.push_back({static_cast(semantic.semantic), static_cast(semantic.hardware_mapping), static_cast(semantic.size_in_elements)}); + indices[i] = static_cast(semantic.semantic); + } + const auto sorted = std::span(indices).first(shader.num_input_semantics); + std::ranges::sort(sorted); + for (const auto index : sorted) { + if (!attributeReads.empty() && index <= attributeReads.back().first + attributeReads.back().count) { + auto& run = attributeReads.back(); + run.count = std::max(run.count, index - run.first + 1); + } else attributeReads.push_back({index, 1}); + } +} + +ShaderRecompiler::ShaderVertexStageInfo VertexStagePlan::Read(std::span userData, std::vector* reads, bool staticAbi, std::uint32_t hiddenUserWords) const { + ShaderRecompiler::ShaderVertexStageInfo info{}; + if (attributeRegister < 0) return info; + const auto bufferWord = static_cast(bufferRegister) + hiddenUserWords; + const auto attribWord = static_cast(attributeRegister) + hiddenUserWords; + if (bufferWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain"); + if (attribWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain"); const auto attribTableAddr = static_cast(userData[attribWord]) | (static_cast(userData[attribWord + 1u]) << 32u); const auto bufferTableAddr = static_cast(userData[bufferWord]) | (static_cast(userData[bufferWord + 1u]) << 32u); if (!staticAbi && attribTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex attribute table address"); if (!staticAbi && bufferTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex buffer table address"); info.fetchEmbedded = true; - info.fetchAttribReg = static_cast(vertexAttribReg); - info.fetchBufferReg = static_cast(vertexBufferReg); - for (std::uint32_t i = 0; i < shader.num_input_semantics; ++i) { - const auto& semantic = semantics[i]; - if (semantic.static_vb_index == 1 || semantic.static_attribute == 1) throw std::runtime_error("AGC graphics: statically bound vertex attributes are not implemented"); - if (staticAbi) { + info.fetchAttribReg = static_cast(attributeRegister); + info.fetchBufferReg = static_cast(bufferRegister); + if (staticAbi) { + for (const auto& attribute : attributes) { auto& destination = info.resourcesDst.at(info.resourcesNum++); - destination.registerStart = static_cast(semantic.hardware_mapping); - destination.registersNum = static_cast(semantic.size_in_elements); - destination.attrId = static_cast(semantic.semantic); - continue; + destination.registerStart = attribute.destination; + destination.registersNum = attribute.components; + destination.attrId = attribute.semantic; } - std::array attribWordBytes{}; - const auto attribWordAddress = attribTableAddr + static_cast(semantic.semantic) * 4u; - AgcDriver::GuestMemory::Read(attribWordAddress, attribWordBytes, 4); - if (reads != nullptr) reads->push_back({attribWordAddress, {attribWordBytes.begin(), attribWordBytes.end()}}); - std::uint32_t attribWord; - std::memcpy(&attribWord, attribWordBytes.data(), 4); + return info; + } + const auto readTable = [&](std::uint64_t base, std::uint64_t offset, std::span destination) { + if (offset > std::numeric_limits::max() - base) throw std::runtime_error("AGC graphics: vertex table address overflow"); + const auto address = base + offset; + AgcDriver::GuestMemory::Read(address, destination, 4); + if (reads != nullptr) reads->push_back({address, {destination.begin(), destination.end()}}); + }; + std::array attributeWords; + for (const auto& run : attributeReads) readTable(attribTableAddr, run.first * 4u, std::as_writable_bytes(std::span(attributeWords).subspan(run.first, run.count))); + std::uint32_t buffers = 0; + for (const auto& attribute : attributes) buffers |= 1u << (attributeWords[attribute.semantic] & 0x1fu); + std::array, ShaderRecompiler::ShaderVertexStageInfo::MaxResources> bufferWords; + while (buffers != 0) { + const auto first = std::countr_zero(buffers); + const auto count = std::countr_one(buffers >> first); + readTable(bufferTableAddr, first * 16u, std::as_writable_bytes(std::span(bufferWords).subspan(first, count))); + buffers &= static_cast(~(((std::uint64_t{1} << count) - 1) << first)); + } + for (const auto& attribute : attributes) { + const auto attribWord = attributeWords[attribute.semantic]; const auto index = attribWord & 0x1fu; const auto format = (attribWord >> 5u) & 0x1ffu; const auto offset = (attribWord >> 14u) & 0xfffu; const auto fetchIndex = (attribWord >> 26u) & 0x1u; - if (index >= ShaderRecompiler::ShaderVertexStageInfo::MaxResources) throw std::runtime_error("AGC graphics: vertex buffer index exceeds the supported domain"); - std::array sharpBytes{}; - const auto sharpAddress = bufferTableAddr + static_cast(index) * 16u; - AgcDriver::GuestMemory::Read(sharpAddress, sharpBytes, 4); - if (reads != nullptr) reads->push_back({sharpAddress, {sharpBytes.begin(), sharpBytes.end()}}); - std::array sharp{}; - std::memcpy(sharp.data(), sharpBytes.data(), 16); - if (info.resourcesNum >= ShaderRecompiler::ShaderVertexStageInfo::MaxResources) throw std::runtime_error("AGC graphics: vertex resource count exceeds the supported domain"); auto& resource = info.resources[info.resourcesNum]; auto& destination = info.resourcesDst[info.resourcesNum]; - resource.fields = sharp; - destination.registerStart = static_cast(semantic.hardware_mapping); - destination.registersNum = static_cast(semantic.size_in_elements); - destination.attrId = static_cast(semantic.semantic); + resource.fields = bufferWords[index]; + destination.registerStart = attribute.destination; + destination.registersNum = attribute.components; + destination.attrId = attribute.semantic; destination.fetchIndex = fetchIndex; if (format != 0u) { const auto bufferFormat = format >> 2u; @@ -266,4 +290,8 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi, std::uint32_t hiddenUserWords) { + return VertexStagePlan(header, headerAddress).Read(userData, reads, staticAbi, hiddenUserWords); +} + } diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexStage.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexStage.cpp new file mode 100644 index 00000000000..3234d74f0b4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VertexStage.cpp @@ -0,0 +1,261 @@ +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "SceShaders.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__linux__) +#include +#include +#endif + +namespace { + +using namespace AgcDriver::Graphics; +using Info = ShaderRecompiler::ShaderVertexStageInfo; +constexpr auto bufferType = static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable); +constexpr auto attributeType = static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexAttribDescTable); + +void require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +template void fails(TAction&& action) { + try { action(); } catch (const std::runtime_error&) { return; } + throw std::runtime_error("invalid vertex table was accepted"); +} + +struct Header { + Shader shader{}; + ShaderUserData user{}; + std::array offsets; + std::array semantics{}; + + Header() { + shader.user_data = &user; + shader.input_semantics = semantics.data(); + shader.num_input_semantics = 1; + user.direct_resource_count = offsets.size(); + user.direct_resource_offset = offsets.data(); + offsets.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET); + offsets[bufferType] = 0; + offsets[attributeType] = 2; + semantics[0].size_in_elements = 4; + } + Header(const Header&) = delete; + Header& operator=(const Header&) = delete; + VertexStagePlan plan() const { + return {std::as_bytes(std::span(this, 1)), reinterpret_cast(this)}; + } +}; + +struct Tables { + std::array attributes{}; + std::array, 32> buffers{}; + Tables() { + GuestAllocations::Mutation mutation; + mutation.Add(this, sizeof(*this), true, true, true); + } + ~Tables() { GuestAllocations::Mutation mutation; mutation.Remove(this); } + Tables(const Tables&) = delete; + Tables& operator=(const Tables&) = delete; +}; + +std::array roots(std::uint64_t buffers, std::uint64_t attributes) { + return {static_cast(buffers), static_cast(buffers >> 32), static_cast(attributes), static_cast(attributes >> 32)}; +} + +auto roots(const Tables& tables) { + return roots(reinterpret_cast(tables.buffers.data()), reinterpret_cast(tables.attributes.data())); +} + +void expectBytes(const Header& header, const Tables& tables, std::span reads) { + std::map expected, actual; + const auto add = [](auto& result, const void* pointer, std::size_t bytes) { + const auto* data = static_cast(pointer); + for (std::size_t i = 0; i < bytes; ++i) result[reinterpret_cast(data + i)] = data[i]; + }; + for (unsigned i = 0; i < header.shader.num_input_semantics; ++i) { + const auto* attribute = &tables.attributes[header.semantics[i].semantic]; + add(expected, attribute, 4); + add(expected, tables.buffers[*attribute & 31].data(), 16); + } + for (const auto& read : reads) { + for (std::size_t i = 0; i < read.bytes.size(); ++i) { + require(actual.emplace(read.address + i, read.bytes[i]).second, "descriptor byte was read twice"); + } + } + require(actual == expected, "decode observed unrelated or missing bytes"); +} + +void liveInputs() { + Header header; + header.shader.num_input_semantics = 5; + const std::array ids{255, 2, 1, 2, 128}; + for (unsigned i = 0; i < ids.size(); ++i) { + header.semantics[i].semantic = ids[i]; + header.semantics[i].hardware_mapping = i * 4; + header.semantics[i].size_in_elements = i % 4 + 1; + } + const auto plan = header.plan(); + std::array staticWords{}; + std::vector staticReads; + const auto staticInfo = plan.Read(staticWords, &staticReads, true, 8); + require(staticInfo.resourcesNum == ids.size() && staticReads.empty(), "static ABI touched live tables"); + require(staticInfo.resourcesDst[0].attrId == 255 && staticInfo.fetchAttribReg == 2, "static ABI changed semantics"); + Tables first, second; + std::mt19937 random(18243); + for (unsigned pass = 0; pass < 128; ++pass) { + auto& tables = pass % 2 ? first : second; + for (auto& word : tables.attributes) word = random() % 32; + for (auto& buffer : tables.buffers) for (auto& word : buffer) word = random(); + std::vector reads; + const auto info = plan.Read(roots(tables), &reads); + require(info.resourcesNum == ids.size() && info.fetchEmbedded && info.fetchBufferReg == 0 && info.fetchAttribReg == 2, "vertex metadata changed"); + for (unsigned i = 0; i < ids.size(); ++i) { + require(info.resources[i].fields == tables.buffers[tables.attributes[ids[i]]], "changed descriptor or root was not read"); + const auto& destination = info.resourcesDst[i]; + require(destination.attrId == static_cast(ids[i]) && destination.registerStart == static_cast(i * 4) && destination.registersNum == static_cast(i % 4 + 1), "semantic order or duplicates changed"); + } + expectBytes(header, tables, reads); + } + std::array hidden{}; + const auto direct = roots(first); + std::copy(direct.begin(), direct.end(), hidden.begin() + 8); + const auto hiddenInfo = plan.Read(hidden, nullptr, false, 8); + require(hiddenInfo.resources[0].fields == plan.Read(direct).resources[0].fields, "merged hidden user words changed table roots"); + header.semantics.fill({}); + require(plan.Read(roots(first)).resourcesDst[0].attrId == 255, "plan retained mutable header pointers"); +} + +void fullTableAndFormats() { + Header header; + Tables tables; + header.shader.num_input_semantics = 32; + for (unsigned i = 0; i < 32; ++i) { + header.semantics[i].semantic = 31 - i; + tables.attributes[i] = i; + tables.buffers[i] = {0xfffffff0u, 0x00200001u, 100u + i, 0x12345678u}; + } + const auto plan = header.plan(); + std::vector reads; + auto info = plan.Read(roots(tables), &reads); + require(info.resourcesNum == 32 && reads.size() == 2 && reads[0].bytes.size() == 128 && reads[1].bytes.size() == 512, "full buffer mask did not form two exact reads"); + expectBytes(header, tables, reads); + for (unsigned i = 0; i < 32; ++i) require(info.resources[i].fields == tables.buffers[31 - i], "full table changed result ordering"); + tables.attributes[31] |= ((77u * 4u + 3u) << 5u) | (32u << 14u) | (1u << 26u); + info = plan.Read(roots(tables)); + require(info.resources[0].fields == std::array{0x10u, 0x00200002u, 131u, 0x1234dfacu}, "live format, channels, or address carry was lost"); + require(info.resourcesDst[0].fetchIndex == 1, "live instance fetch index was lost"); +} + +void invalidInputs() { + Header header; + Tables tables; + auto user = roots(tables); + const auto valid = header.plan(); + fails([&] { valid.Read(std::span(user).first(1)); }); + fails([&] { valid.Read(std::span(user).first(3)); }); + fails([&] { valid.Read(roots(0, 1)); }); + fails([&] { valid.Read(roots(1, 0)); }); + header.shader.user_data = nullptr; + fails([&] { header.plan(); }); + header.shader.user_data = reinterpret_cast(std::numeric_limits::max() - 7); + fails([&] { header.plan(); }); + header.shader.user_data = &header.user; + header.user.direct_resource_count = header.offsets.size() + 1; + fails([&] { header.plan(); }); + header.user.direct_resource_count = header.offsets.size(); + header.user.direct_resource_offset = nullptr; + fails([&] { header.plan(); }); + header.user.direct_resource_offset = header.offsets.data(); + header.offsets[bufferType] = ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET; + fails([&] { header.plan(); }); + header.offsets[bufferType] = 0; + for (const auto count : {0u, 33u}) { + header.shader.num_input_semantics = count; + fails([&] { header.plan(); }); + } + header.shader.num_input_semantics = 1; + header.shader.input_semantics = nullptr; + fails([&] { header.plan(); }); + header.shader.input_semantics = header.semantics.data(); + header.semantics[0].static_vb_index = 1; + fails([&] { header.plan(); }); + header.semantics[0].static_vb_index = 0; + header.semantics[0].static_attribute = 1; + fails([&] { header.plan(); }); + header.semantics[0].static_attribute = 0; + header.semantics[0].semantic = 255; + const auto high = header.plan(); + fails([&] { high.Read(roots(1, std::numeric_limits::max() - 3)); }); + header.offsets[attributeType] = ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET; + require(header.plan().Read({}).resourcesNum == 0, "shader without attribute table requires roots"); + require(VertexStagePlan({}, 0).Read({}).resourcesNum == 0, "raw shader requires an AGC header"); +} + +void parallelReads() { + Header header; + const auto plan = header.plan(); + std::atomic okay{true}; + std::array threads; + for (unsigned i = 0; i < threads.size(); ++i) threads[i] = std::thread([&, i] { + Tables tables; + for (unsigned n = 0; n < 256; ++n) { + tables.attributes[0] = (n + i) % 32; + tables.buffers[tables.attributes[0]][2] = n + 1024 * i; + if (plan.Read(roots(tables)).resources[0].fields[2] != n + 1024 * i) okay = false; + } + }); + for (auto& thread : threads) thread.join(); + require(okay, "shared plan mixed concurrent live inputs"); +} + +void guardBoundary() { +#if defined(__linux__) + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + auto* memory = static_cast(mmap(nullptr, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + require(memory != MAP_FAILED, "guard allocation failed"); + require(mprotect(memory + page, page, PROT_NONE) == 0, "guard protection failed"); + { GuestAllocations::Mutation mutation; mutation.Add(memory, page, true, true, true); } + Header header; + const auto plan = header.plan(); + const std::array descriptor{0x1000, 0x00200000, 127, 0}; + std::memcpy(memory + page - 16, descriptor.data(), 16); + std::uint32_t attribute = 31; + std::memcpy(memory, &attribute, 4); + const auto bufferBase = reinterpret_cast(memory + page - 512); + require(plan.Read(roots(bufferBase, reinterpret_cast(memory))).resources[0].fields == descriptor, "last buffer descriptor failed"); + attribute = 0; + std::memcpy(memory + page - 4, &attribute, 4); + std::memcpy(memory, descriptor.data(), 16); + require(plan.Read(roots(reinterpret_cast(memory), reinterpret_cast(memory + page - 4))).resources[0].fields == descriptor, "last attribute word failed"); + { GuestAllocations::Mutation mutation; mutation.Remove(memory); } + munmap(memory, page * 2); +#endif +} + +} + +int main() { + try { + liveInputs(); + fullTableAndFormats(); + invalidInputs(); + parallelReads(); + guardBoundary(); + std::cout << "vertex stage plans passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +}