#include "prx/libSceAgc/Misc/include/ShaderFusion.hpp" #include "prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include "prx/libc/include/Shutdown.hpp" #include #include #include #include #include #include #include namespace { constexpr std::uint32_t SCodeEnd = 0xbf9f0000u; constexpr std::uint32_t SNop = 0xbf800000u; constexpr std::uint32_t SSetpcS6 = 0xbe802006u; constexpr std::uint32_t SSwappcNullS6 = 0xbefd2106u; constexpr std::uint32_t SMovS0 = 0xbe800380u; constexpr std::uint32_t VMovV0 = 0x7e000280u; constexpr std::uint32_t SEndpgm = 0xbf810000u; constexpr int InvalidShaderHalves = static_cast(0x8a6c0008u); constexpr std::uint32_t GsStage = 1u << 22u; constexpr std::uint32_t HsStage = 1u << 21u; void check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } template void expectFailure(TAction action) { try { action(); } catch (const std::exception& error) { check(error.what()[0] != '\0', "empty exception message"); return; } throw std::runtime_error("expected an exception"); } std::uint32_t word(const void* memory, std::size_t index) { std::uint32_t value = 0; std::memcpy(&value, static_cast(memory) + index * 4, sizeof(value)); return value; } const ShaderRegister* find(const Shader& shader, std::uint32_t offset) { for (std::uint32_t i = 0; i < shader.num_sh_registers; ++i) { if (shader.sh_registers[i].offset == offset) return &shader.sh_registers[i]; } return nullptr; } struct Halves { std::vector frontCode; std::array backCode{SMovS0, SEndpgm, SCodeEnd}; std::array frontSh{{{0x080u, 0x1234u}, {0x08Au, (2u << 6u) | 3u}, {0x08Bu, (6u << 1u) | (1u << 7u)}, {0x0B2u, 0x55u}}}; std::array frontCx{{{0x29Bu, 7u}, {0x2D5u, 0x24u}}}; std::array backSh{{{0x088u, 0u}, {0x089u, 0u}, {0x08Au, (1u << 24u) | (1u << 6u) | 5u}, {0x08Bu, (2u << 1u) | (1u << 7u) | (1u << 15u)}, {0x0C8u, 0u}, {0x0C9u, 0u}}}; std::array backCx{{{0x29Bu, 2u}}}; std::array inputs{}; std::array outputs{}; std::array direct{3u, 9u}; std::array sharps{}; ShaderUserData userData{}; ShaderSpecialRegs frontSpecials{}; ShaderSpecialRegs backSpecials{}; Shader front{}; Shader back{}; explicit Halves(std::uint32_t terminator = SSetpcS6) { frontCode = {SMovS0, VMovV0, terminator, SCodeEnd, SCodeEnd}; const auto codeBytes = static_cast(frontCode.size() * 4); std::array magic{}; std::memcpy(magic.data(), "barefoot", 8); frontCode.insert(frontCode.end(), {magic[0], magic[1], 0xabcdu, 0u, 1u, codeBytes, 0u}); inputs[0].semantic = 1; inputs[1].semantic = 2; outputs[0].semantic = 3; sharps[0].offset_dw = 12; userData.direct_resource_offset = direct.data(); userData.direct_resource_count = static_cast(direct.size()); userData.sharp_resource_offset[1] = sharps.data(); userData.sharp_resource_count[1] = 1; userData.eud_size_dw = 4; frontSpecials.vgt_shader_stages_en.value = GsStage; backSpecials.vgt_shader_stages_en.value = GsStage; backSpecials.ge_cntl.value = 0x77u; front.file_header = ShaderRegs::SHADER_FILE_HEADER_MAGIC; front.version = ShaderRegs::SHADER_VERSION; front.code = frontCode.data(); front.shader_size = static_cast(frontCode.size() * 4); front.header_size = sizeof(Shader); front.sh_registers = frontSh.data(); front.num_sh_registers = static_cast(frontSh.size()); front.cx_registers = frontCx.data(); front.num_cx_registers = static_cast(frontCx.size()); front.input_semantics = inputs.data(); front.num_input_semantics = static_cast(inputs.size()); front.specials = &frontSpecials; front.scratch_size_dw_per_thread = 16; front.type = static_cast(ShaderRegs::ShaderBinaryType::GsFront); back.file_header = ShaderRegs::SHADER_FILE_HEADER_MAGIC; back.version = ShaderRegs::SHADER_VERSION; back.code = backCode.data(); back.shader_size = static_cast(backCode.size() * 4); back.header_size = sizeof(Shader); back.sh_registers = backSh.data(); back.num_sh_registers = static_cast(backSh.size()); back.cx_registers = backCx.data(); back.num_cx_registers = static_cast(backCx.size()); back.output_semantics = outputs.data(); back.num_output_semantics = static_cast(outputs.size()); back.user_data = &userData; back.specials = &backSpecials; back.special_sizes_bytes = sizeof(ShaderSpecialRegs); back.scratch_size_dw_per_thread = 8; back.type = static_cast(ShaderRegs::ShaderBinaryType::GsBack); } }; std::uint8_t* aligned(std::vector& storage, std::size_t alignment) { const auto address = reinterpret_cast(storage.data()); return storage.data() + ((alignment - address % alignment) % alignment); } void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) { Halves halves(terminator); SizeAlign size{}; check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "fused size query failed"); check(size.m_align == 8, "fused shaders are not 256-byte aligned"); check(size.m_size > 256 + 255 + halves.back.shader_size + sizeof(Shader), "fused size does not cover the alignment padding, code and header"); std::vector storage(size.m_size + 512, 0xcd); auto* scratch = aligned(storage, 256) + misalignment; Shader fused{}; check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, scratch) == 0, "fusion failed"); for (std::size_t i = 0; i < 16; ++i) check(scratch[size.m_size + i] == 0xcd, "fusion wrote past the reported size"); auto* memory = scratch + (256 - misalignment) % 256; const auto base = reinterpret_cast(memory); const auto scratchBase = reinterpret_cast(scratch); check(fused.type == static_cast(ShaderRegs::ShaderBinaryType::Gs), "fused shader is not a GS"); check(fused.code == memory && fused.shader_size == 256 + halves.back.shader_size, "fused code placement is wrong"); check(word(memory, 0) == SMovS0 && word(memory, 1) == VMovV0, "front program was not copied"); for (std::size_t i = 2; i < 64; ++i) check(word(memory, i) == SNop, "front program does not fall through to the back half"); check(std::memcmp(memory + 256, halves.backCode.data(), halves.back.shader_size) == 0, "back program was not copied"); const auto* backLo = find(fused, 0x088u); const auto* frontLo = find(fused, 0x0C8u); check(backLo != nullptr && backLo->value == static_cast((base + 256) >> 8u), "back program address was not patched"); check(frontLo != nullptr && frontLo->value == static_cast(base >> 8u), "fused program address was not patched"); check(find(fused, 0x08Au)->value == ((1u << 24u) | (2u << 6u) | 5u), "RSRC1 register counts were not merged"); check(find(fused, 0x08Bu)->value == ((6u << 1u) | (1u << 7u) | (1u << 15u)), "RSRC2 user SGPRs were not taken from the front half"); check(find(fused, 0x0B2u) != nullptr && find(fused, 0x0B2u)->value == 0x55u, "front-only SH register was lost"); check(find(fused, 0x080u) == nullptr, "front program checksum was kept"); check(fused.num_sh_registers == 7 && fused.num_cx_registers == 2, "fused register counts are wrong"); check(fused.cx_registers[0].offset == 0x29Bu && fused.cx_registers[0].value == 2u && fused.cx_registers[1].offset == 0x2D5u, "context registers were not merged"); check(fused.num_input_semantics == 2 && fused.input_semantics[1].semantic == 2, "front inputs were not kept"); check(fused.num_output_semantics == 1 && fused.output_semantics[0].semantic == 3, "back outputs were not kept"); check(fused.scratch_size_dw_per_thread == 16, "scratch size is not the larger half's"); check(fused.specials != nullptr && fused.specials != halves.back.specials && fused.specials->ge_cntl.value == 0x77u, "back specials were not copied"); const auto inside = [&](const void* pointer) { const auto address = reinterpret_cast(pointer); return address >= scratchBase && address < scratchBase + size.m_size; }; check(inside(fused.sh_registers) && inside(fused.cx_registers) && inside(fused.specials) && inside(fused.input_semantics) && inside(fused.output_semantics), "fused tables are not in the fused memory"); check(inside(fused.user_data) && inside(fused.user_data->direct_resource_offset) && inside(fused.user_data->sharp_resource_offset[1]), "fused user data is not in the fused memory"); check(fused.user_data->direct_resource_count == 2 && fused.user_data->direct_resource_offset[1] == 9u && fused.user_data->sharp_resource_offset[1][0].offset_dw == 12 && fused.user_data->sharp_resource_offset[0] == nullptr && fused.user_data->eud_size_dw == 4, "fused user data is wrong"); const auto headerOffset = static_cast(size.m_size) - 255 - fused.header_size; check(headerOffset % alignof(Shader) == 0 && headerOffset >= fused.shader_size, "fused header is misplaced"); const auto* header = reinterpret_cast(memory + headerOffset); check(header->code == fused.code && header->sh_registers == fused.sh_registers && header->user_data == fused.user_data && header->header_size == fused.header_size && header->shader_size == fused.shader_size && header->type == fused.type && header->num_sh_registers == fused.num_sh_registers, "fused memory has no self-contained header"); } void testRejections() { Halves halves; SizeAlign size{}; Shader fused{}; expectFailure([&] { sceAgcGetFusedShaderSize_0080(nullptr, &halves.front, &halves.back); }); expectFailure([&] { sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, nullptr); }); { Halves swapped; check(sceAgcGetFusedShaderSize_0080(&size, &swapped.back, &swapped.front) == InvalidShaderHalves, "swapped halves were accepted by the size query"); check(sceAgcFuseShaderHalves_0200(&fused, &swapped.back, &swapped.front, nullptr) == InvalidShaderHalves, "swapped halves were fused"); } { Halves stages; stages.backSpecials.vgt_shader_stages_en.value = 0; std::vector storage(4096); check(sceAgcFuseShaderHalves_0200(&fused, &stages.front, &stages.back, storage.data()) == InvalidShaderHalves, "halves with different GS stages were fused"); } { Halves noJump; noJump.frontCode[2] = SNop; expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &noJump.front, &noJump.back); }); } { Halves noTrailer; noTrailer.frontCode[5] = 0; expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &noTrailer.front, &noTrailer.back); }); } { Halves hardware; hardware.frontSh[2].value |= 1u << 16u; expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &hardware.front, &hardware.back); }); } { Halves constants; constants.back.embedded_constant_buffer_size_dqw = 1; expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &constants.front, &constants.back); }); } } void testHullHalves() { std::array frontCode{SEndpgm, SCodeEnd}; std::array backCode{SEndpgm, SCodeEnd}; std::array frontSh{{{0x100u, 0x11u}, {0x100u, 0x22u}, {0x10Au, 7u}, {0x10Bu, 6u << 1u}}}; std::array backSh{{{0x100u, 0u}, {0x100u, 0u}, {0x10Au, 3u}, {0x10Bu, 2u << 1u}, {0x148u, 0u}, {0x149u, 0xab00u}}}; ShaderSpecialRegs specials{}; specials.vgt_shader_stages_en.value = HsStage; Shader front{}; front.code = frontCode.data(); front.sh_registers = frontSh.data(); front.num_sh_registers = static_cast(frontSh.size()); front.specials = &specials; front.type = static_cast(ShaderRegs::ShaderBinaryType::HsFront); Shader back{}; back.code = backCode.data(); back.sh_registers = backSh.data(); back.num_sh_registers = static_cast(backSh.size()); back.specials = &specials; back.type = static_cast(ShaderRegs::ShaderBinaryType::HsBack); SizeAlign size{}; check(sceAgcGetFusedShaderSize_0080(&size, &front, &back) == 0 && size.m_size == backSh.size() * sizeof(ShaderRegister) && size.m_align == 4, "hull size query changed"); std::vector scratch(backSh.size()); Shader fused{}; check(sceAgcFuseShaderHalves_0200(&fused, &front, &back, scratch.data()) == 0, "hull fusion failed"); check(fused.type == static_cast(ShaderRegs::ShaderBinaryType::Hs) && fused.code == back.code && fused.sh_registers == scratch.data() && fused.user_data == nullptr, "hull halves were not fused at the register level"); const auto address = reinterpret_cast(front.code); check(scratch[0].value == 0x11u && scratch[1].value == 0x22u, "hull checksums were not taken from the front half"); check(scratch[2].value == 7u && scratch[3].value == (6u << 1u), "hull resource registers were not merged"); check(scratch[4].value == static_cast(address >> 8u) && scratch[5].value == (0xab00u | static_cast((address >> 40u) & 0xffu)), "hull local program address does not point at the front half"); check(backSh[4].value == 0u, "hull fusion wrote the back half's registers"); } } int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; testFusion(0); testFusion(0xa0); testFusion(4); testFusion(0, SSwappcNullS6); testRejections(); testHullHalves(); LibcRunShutdown_nid_postfix(); std::puts("AGC shader fusion tests passed"); return 0; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); try { LibcRunShutdown_nid_postfix(); } catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } return 1; } }