From 43074a805e80ced15a44984f183dad9516caa5ba Mon Sep 17 00:00:00 2001 From: Nikhil Panchal <37629345+nikhilpanchal1@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:11:00 +0000 Subject: [PATCH 1/2] fix(recompiler): avoid illegal narrow constants during optimization Skip CCP and Simplification for storage-only narrow types while keeping the remaining passes and arithmetic-capable folding. Add compiler and Vulkan readback regressions for default/full/none modes. Tested: Linux/llvmpipe, full CTest with SPIRV-Tools on/off, ASan/UBSan optimizer checks, and cold-cache Vulkan wave32/wave64 readback. Fixes boykopovar/AnyPS5#1276 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 43 ++++ .../tests/NarrowConstantStoreFixture.hpp | 28 +++ .../tests/SpirvNarrowConstantStore.cpp | 108 ++++++++++ .../libSceAgcDriver/tests/SpirvOptimizer.cpp | 199 ++++++++++++++++++ .../tests/execution/ConstantStore.cpp | 100 +++++++++ .../SpirvBackend/src/SpirvOptimizer.cpp | 94 +++++++-- docs/dev/TechnicalDebt.md | 1 + 7 files changed, 559 insertions(+), 14 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/NarrowConstantStoreFixture.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ConstantStore.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 3725ac1ba..6c05e1b72 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -278,6 +278,18 @@ target_link_libraries(agc_spirv_module_tests PRIVATE shader_recompiler_agc_drive set_target_properties(agc_spirv_module_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_spirv_module_tests) +add_test_executable(agc_spirv_narrow_constant_store_tests EXCLUDE_FROM_ALL tests/SpirvNarrowConstantStore.cpp) +target_link_libraries(agc_spirv_narrow_constant_store_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_spirv_narrow_constant_store_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_spirv_narrow_constant_store_tests) +if(ANYPS5_ENABLE_SPIRV_TOOLS) + target_link_libraries(agc_spirv_narrow_constant_store_tests PRIVATE SPIRV-Tools-static) + add_test_executable(agc_spirv_optimizer_tests EXCLUDE_FROM_ALL tests/SpirvOptimizer.cpp) + target_link_libraries(agc_spirv_optimizer_tests PRIVATE shader_recompiler_agc_driver libc SPIRV-Tools-static) + set_target_properties(agc_spirv_optimizer_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_spirv_optimizer_tests) +endif() + add_test_executable(agc_dpp_write_tests EXCLUDE_FROM_ALL tests/DppWrite.cpp) target_link_libraries(agc_dpp_write_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_dpp_write_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -566,6 +578,9 @@ endforeach() if(BUILD_TESTING) foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests ${agcExecutionTests}) + if(agcTest STREQUAL "agc_driver_constant_store_tests") + continue() + endif() string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME ${agcTestName} COMMAND ${agcTest}) @@ -581,6 +596,34 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests) + if(NOT TARGET ${agcTest}) + continue() + endif() + set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF) + string(REGEX REPLACE "_tests$" "" agcTestBaseName "${agcTest}") + foreach(agcSpirvMode IN ITEMS default full none) + if(agcSpirvMode STREQUAL "default") + set(agcTestName "${agcTestBaseName}") + set(agcSpirvEnvironment "--unset=APS5_SPIRV_OPT") + else() + set(agcTestName "${agcTestBaseName}_${agcSpirvMode}") + set(agcSpirvEnvironment "APS5_SPIRV_OPT=${agcSpirvMode}") + endif() + add_test(NAME ${agcTestName} COMMAND ${CMAKE_COMMAND} -E env + ${agcSpirvEnvironment} "$") + set_tests_properties(${agcTestName} PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 60 SKIP_RETURN_CODE 77) + if(agcTest STREQUAL "agc_driver_constant_store_tests") + set_tests_properties(${agcTestName} PROPERTIES RUN_SERIAL TRUE) + endif() + if(WIN32) + set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() endif() option(AGC_BUILD_VISUAL_TEST "Enable the standalone AGC SPIR-V visual test" OFF) diff --git a/core/libs/prx/libSceAgcDriver/tests/NarrowConstantStoreFixture.hpp b/core/libs/prx/libSceAgcDriver/tests/NarrowConstantStoreFixture.hpp new file mode 100644 index 000000000..265dff327 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/NarrowConstantStoreFixture.hpp @@ -0,0 +1,28 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_NARROWCONSTANTSTOREFIXTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_NARROWCONSTANTSTOREFIXTURE_HPP + +#include +#include + +namespace NarrowConstantStoreFixture { + +constexpr std::uint32_t Threads = 8; +constexpr std::uint32_t LaneBytes = 16; +constexpr std::uint8_t Fill = 0xcd; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x7e040280, 0x7e0602ff, 0x00a10200, 0x7e0802ff, 0x0000fec3, + 0xdc608000, 0x00000201, + 0xdc688002, 0x00000401, + 0xdc708008, 0x00000301, + 0xbf810000, +}; + +constexpr std::array ExpectedLane{ + 0x00, Fill, 0xc3, 0xfe, Fill, Fill, Fill, Fill, + 0x00, 0x02, 0xa1, 0x00, Fill, Fill, Fill, Fill, +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp new file mode 100644 index 000000000..daa5ac849 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp @@ -0,0 +1,108 @@ +#include "BdaAbi.hpp" +#include "NarrowConstantStoreFixture.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#if ANYPS5_ENABLE_SPIRV_TOOLS +#include +#endif + +namespace { + +using namespace ShaderRecompiler; + +void Check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckStorageOnlyModule(const RecompileResult& result, std::string_view name) { + const auto& words = result.spirv.Words(); + Check(words.size() >= 5 && words[0] == spv::MagicNumber, std::string(name) + ": missing SPIR-V module"); + bool byteType = false; + bool store = false; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + const auto opcode = words[offset] & spv::OpCodeMask; + Check(count != 0 && count <= words.size() - offset, std::string(name) + ": malformed SPIR-V instruction"); + if (opcode == spv::OpCapability && count == 2) { + Check(words[offset + 1] != spv::CapabilityInt8, std::string(name) + ": introduced unsupported Int8 arithmetic"); + } + if (opcode == spv::OpTypeInt && count == 4 && words[offset + 2] == 8) byteType = true; + if (opcode == spv::OpStore) store = true; + offset += count; + } + Check(byteType && store, std::string(name) + ": lost the byte storage regression fixture"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + std::string diagnostics; + spvtools::SpirvTools validator(SPV_ENV_VULKAN_1_1); + validator.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) { + if (message != nullptr) diagnostics += std::string(message) + '\n'; + }); + const bool valid = validator.Validate(words); + Check(valid, std::string(name) + ": emitted invalid SPIR-V:\n" + diagnostics); +#endif +} + +SpirvTarget StorageOnlyTarget() { + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + }; + static constexpr std::array extensions{ + "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", + }; + return {0x00401000u, 0x00010300u, 64, BdaAbi::Version, capabilities, extensions, false, + {1024, 1024, 64}, 1024, 32768, {}, {}}; +} + +void CheckVertexReproducer() { + alignas(256) static constexpr std::array code{ + 0xbe801f00u, 0x800000ffu, 0x34u, 0xdc7519cbu, 0xb0020010u, 0x10005004u, 0xf4200100u, + 0xfa000000u, 0xbf8cc07fu, 0x7e0049c8u, 0xf80008cfu, 0u, 0xbf810000u, 0x3f800000u, + }; + const std::span words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + RecompileRequest request{}; + request.shader = {ShaderStage::Vertex, reinterpret_cast(code.data()), words, 0, {}}; + request.target = StorageOnlyTarget(); + request.layout = {0, 0, 0, 128}; + request.context.waveSize = 64; + request.context.memory = memory; + request.context.userDataBaseRegister = 8; + request.context.vertex = ShaderVertexStageInfo{}; + request.useCache = false; + CheckStorageOnlyModule(Recompile(request), "issue #1276 vertex"); +} + +void CheckComputeStores(std::uint32_t waveSize) { + const std::span code(NarrowConstantStoreFixture::Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const std::array userData{0x10000u, 0u}; + const ShaderComputeStageInfo compute{{NarrowConstantStoreFixture::Threads, 1, 1}, 0, {false, false, false}, false, 1, {}}; + RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + StorageOnlyTarget(), {0, 0, 0, 128}, std::nullopt, false, + }; + CheckStorageOnlyModule(Recompile(request), "constant stores wave" + std::to_string(waveSize)); +} + +} + +int main() { + try { + CheckVertexReproducer(); + CheckComputeStores(32); + CheckComputeStores(64); + std::puts("storage-only constant store compilation passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp new file mode 100644 index 000000000..25c39567c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -0,0 +1,199 @@ +#include "SpirvBackend/SpirvOptimizer.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; +constexpr std::uint32_t Vulkan11 = 0x00401000u; +constexpr std::uint32_t Spirv13 = 0x00010300u; + +void check(bool condition, const std::string& message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message.c_str()); + ++failures; + } +} + +struct NarrowType { + const char* name; + unsigned width; + bool signedInteger; + bool floating; + const char* arithmeticCapability; + const char* unrelatedCapability; +}; + +constexpr std::array Types{ + NarrowType{"u8", 8, false, false, "Int8", "Int16"}, + NarrowType{"i8", 8, true, false, "Int8", "Int16"}, + NarrowType{"u16", 16, false, false, "Int16", "Int8"}, + NarrowType{"i16", 16, true, false, "Int16", "Int8"}, + NarrowType{"f16", 16, false, true, "Float16", "Int8"}, +}; + +enum class ArithmeticSupport { StorageOnly, Explicit, ImpliedInt8 }; + +std::string moduleSource(const NarrowType& type, bool vector, ArithmeticSupport support) { + const auto width = std::to_string(type.width); + const std::string typeOp = type.floating ? "OpTypeFloat " : "OpTypeInt "; + const std::string signedness = type.floating ? "" : (type.signedInteger ? " 1" : " 0"); + const std::string conversion = type.floating ? "OpFConvert" : (type.signedInteger ? "OpSConvert" : "OpUConvert"); + const std::string wide = type.floating || type.signedInteger ? "%wide" : "%index"; + std::string text = "OpCapability Shader\nOpCapability "; + text += type.width == 8 ? "StorageBuffer8BitAccess\n" : "StorageBuffer16BitAccess\n"; + text += "OpCapability " + std::string(type.unrelatedCapability) + "\n"; + if (support == ArithmeticSupport::Explicit) text += "OpCapability " + std::string(type.arithmeticCapability) + "\n"; + if (support == ArithmeticSupport::ImpliedInt8) { + text += "OpCapability DotProductInput4x8BitKHR\nOpExtension \"SPV_KHR_integer_dot_product\"\n"; + } + text += type.width == 8 ? "OpExtension \"SPV_KHR_8bit_storage\"\n" : "OpExtension \"SPV_KHR_16bit_storage\"\n"; + text += R"(OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 1 1 1 +OpDecorate %block Block +OpMemberDecorate %block 0 Offset 0 +OpDecorate %buffer DescriptorSet 0 +OpDecorate %buffer Binding 0 +%void = OpTypeVoid +%function = OpTypeFunction %void +%index = OpTypeInt 32 0 +%zero = OpConstant %index 0 +)"; + if (wide == "%wide") text += "%wide = " + typeOp + "32" + signedness + "\n"; + text += "%small = " + typeOp + width + signedness + "\n"; + text += "%zeroValue = OpConstant " + wide + " 0\n"; + text += "%value = OpConstant " + wide + " " + std::string(type.floating ? "1.5" : (type.signedInteger ? "-257" : "257")) + "\n"; + if (vector) { + text += "%wideValue = OpTypeVector " + wide + " 2\n%narrow = OpTypeVector %small 2\n"; + text += "%input = OpConstantComposite %wideValue %zeroValue %value\n"; + } + const std::string narrow = vector ? "%narrow" : "%small"; + text += "%block = OpTypeStruct " + narrow + "\n"; + text += "%blockPointer = OpTypePointer StorageBuffer %block\n"; + text += "%pointer = OpTypePointer StorageBuffer " + narrow + "\n"; + text += R"(%buffer = OpVariable %blockPointer StorageBuffer +%main = OpFunction %void None %function +%entry = OpLabel +%address = OpAccessChain %pointer %buffer %zero +%dead = OpIAdd %index %zero %zero +)"; + if (vector) { + text += "%converted = " + conversion + " " + narrow + " %input\n"; + text += "OpStore %address %converted\n"; + } else { + text += "%convertedZero = " + conversion + " " + narrow + " %zeroValue\n"; + text += "OpStore %address %convertedZero\n"; + text += "%converted = " + conversion + " " + narrow + " %value\n"; + text += "OpStore %address %converted\n"; + } + return text + "OpReturn\nOpFunctionEnd\n"; +} + +std::vector assemble(const std::string& source) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + tools.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&, const char* message) { + std::fprintf(stderr, "SPIRV-Tools: %s\n", message); + }); + std::vector words; + check(tools.Assemble(source, &words), "test shader did not assemble"); + return words; +} + +std::size_t opcodeCount(const std::vector& words, spv::Op opcode) { + std::size_t count = 0; + for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> spv::WordCountShift) { + if ((words[offset] & spv::OpCodeMask) == static_cast(opcode)) ++count; + } + return count; +} + +std::set capabilities(const std::vector& words) { + std::set result; + for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> spv::WordCountShift) { + if ((words[offset] & spv::OpCodeMask) == spv::OpCapability) result.insert(words[offset + 1]); + } + return result; +} + +void testNarrowConversions(bool optimizationEnabled) { + spvtools::SpirvTools validator(SPV_ENV_VULKAN_1_1); + for (const auto& type : Types) { + for (const bool vector : {false, true}) { + for (const auto support : {ArithmeticSupport::StorageOnly, ArithmeticSupport::Explicit, ArithmeticSupport::ImpliedInt8}) { + if (support == ArithmeticSupport::ImpliedInt8 && (type.width != 8 || !vector)) continue; + const bool arithmetic = support != ArithmeticSupport::StorageOnly; + const std::string label = std::string(type.name) + (vector ? " vector" : " scalar") + + (support == ArithmeticSupport::ImpliedInt8 ? " implied Int8" : (arithmetic ? " arithmetic" : " storage only")); + const auto input = assemble(moduleSource(type, vector, support)); + if (!validator.Validate(input)) { + check(false, label + ": invalid test input"); + continue; + } + try { + const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false); + if (!validator.Validate(output)) { + check(false, label + ": optimizer returned invalid SPIR-V"); + continue; + } + check(capabilities(output) == capabilities(input), label + ": optimizer changed device capabilities"); + check(opcodeCount(output, spv::OpStore) == (vector ? 1u : 2u), label + ": optimizer lost buffer stores"); + if (!optimizationEnabled) { + check(output == input, label + ": none mode changed the shader"); + } else { + check(opcodeCount(output, spv::OpIAdd) == 0, label + ": optimizer did not remove dead arithmetic"); + if (arithmetic && !type.floating) { + const auto conversion = type.signedInteger ? spv::OpSConvert : spv::OpUConvert; + check(opcodeCount(output, conversion) == 0, label + ": safe constant folding was disabled"); + } + } + } catch (const std::exception& error) { + check(false, label + ": " + error.what()); + } + } + } + } +} + +void expectRejected(const std::vector& words, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, const char* reason) { + try { + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(words, vulkanVersion, spirvVersion, false)); + check(false, std::string("accepted ") + reason); + } catch (const std::exception&) { + } +} + +void testInvalidInputIsRejected() { + const auto valid = assemble(moduleSource(Types.front(), false, ArithmeticSupport::StorageOnly)); + expectRejected(valid, 0x00400000u, Spirv13, "SPIR-V 1.3 with Vulkan 1.0"); + expectRejected(valid, Vulkan11, 0x00010200u, "a module newer than the requested SPIR-V version"); + expectRejected(valid, 0u, Spirv13, "an unsupported Vulkan version"); + auto invalid = moduleSource(Types.front(), false, ArithmeticSupport::StorageOnly); + invalid.insert(invalid.find("%main = OpFunction"), "%illegal = OpConstantNull %small\n"); + expectRejected(assemble(invalid), Vulkan11, Spirv13, "an illegal storage-only narrow constant even when unused"); + auto malformed = valid; + malformed[5] = spv::OpCapability; + expectRejected(malformed, Vulkan11, Spirv13, "a zero-length SPIR-V instruction"); +} + +} + +int main() { + const char* mode = std::getenv("APS5_SPIRV_OPT"); + const bool optimizationEnabled = mode == nullptr || std::string(mode) != "none"; + testNarrowConversions(optimizationEnabled); + testInvalidInputIsRejected(); + if (failures != 0) { + std::fprintf(stderr, "%d optimizer check(s) failed\n", failures); + return 1; + } + std::puts("SPIR-V optimizer checks passed"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ConstantStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ConstantStore.cpp new file mode 100644 index 000000000..c9b16efb5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ConstantStore.cpp @@ -0,0 +1,100 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "../NarrowConstantStoreFixture.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using namespace NarrowConstantStoreFixture; + +constexpr std::size_t BlockBytes = 65536; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "constant store: cannot allocate the guest block"); + Clear(); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + void Clear() const { std::memset(block, Fill, BlockBytes); } + const std::uint8_t* Data() const { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +void Run(AgcDriver::VulkanDevice& device, const GuestBlock& guest, std::uint32_t waveSize) { + using namespace ShaderRecompiler; + guest.Clear(); + const auto address = static_cast(reinterpret_cast(guest.Data())); + const std::array userData{ + static_cast(address), static_cast(address >> 32u), + }; + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1, {}}; + RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), {0, 0, 0, 128}, std::nullopt, false, + }; + const auto result = Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::size_t offset = 0; offset < BlockBytes; ++offset) { + const auto expected = offset < Threads * LaneBytes ? ExpectedLane[offset % LaneBytes] : Fill; + Require(guest.Data()[offset] == expected, "constant store: wave" + std::to_string(waveSize) + " byte " + std::to_string(offset) + " is " + + std::to_string(guest.Data()[offset]) + ", expected " + std::to_string(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const GuestBlock guest; + Run(*device, guest, 32); + Run(*device, guest, 64); + std::puts("constant store Vulkan readback passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp index e29ca3e36..aebbb0715 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp @@ -1,12 +1,49 @@ #include "SpirvBackend/SpirvOptimizer.hpp" #include #include +#include #include #include #include namespace ShaderRecompiler { +namespace { + +bool HasLimitedUseTypes(std::span spirv) { + bool int8 = false, int16 = false, float16 = false; + bool int8Capability = false, int16Capability = false, float16Capability = false; + const bool swapped = spirv[0] != spv::MagicNumber; + const auto wordAt = [spirv, swapped](std::size_t index) { + const auto word = spirv[index]; + return swapped ? (word >> 24u) | ((word >> 8u) & 0x0000ff00u) | + ((word << 8u) & 0x00ff0000u) | (word << 24u) + : word; + }; + for (std::size_t offset = 5; offset < spirv.size(); offset += wordAt(offset) >> 16u) { + switch (wordAt(offset) & 0xffffu) { + case spv::OpCapability: + int8Capability |= wordAt(offset + 1) == spv::CapabilityInt8 || + wordAt(offset + 1) == spv::CapabilityDotProductInput4x8Bit; + int16Capability |= wordAt(offset + 1) == spv::CapabilityInt16; + float16Capability |= wordAt(offset + 1) == spv::CapabilityFloat16; + break; + case spv::OpTypeInt: + int8 |= wordAt(offset + 2) == 8; + int16 |= wordAt(offset + 2) == 16; + break; + case spv::OpTypeFloat: + float16 |= (wordAt(offset) >> 16u) == 3 && wordAt(offset + 2) == 16; + break; + case spv::OpFunction: + return (int8 && !int8Capability) || (int16 && !int16Capability) || (float16 && !float16Capability); + } + } + return (int8 && !int8Capability) || (int16 && !int16Capability) || (float16 && !float16Capability); +} + +} + std::vector ValidateAndOptimizeSpirv(std::span spirv, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, bool allowOffsetTextureOperand) { spv_target_env environment; const auto apiVersion = vulkanVersion & ~0xfffu; @@ -48,10 +85,19 @@ std::vector ValidateAndOptimizeSpirv(std::span ValidateAndOptimizeSpirv(std::span Date: Thu, 8 Oct 2026 00:42:11 +0000 Subject: [PATCH 2/2] fix(recompiler): guard narrow-type scanning bounds Check module headers, instruction lengths and required operands before scanning narrow types. Cover malformed and byte-swapped modules while retaining the native SPIR-V 1.3 conversion matrix. Tested: Linux/llvmpipe CTest three times with SPIRV-Tools on and off, cold-cache Vulkan regressions, and ASan/UBSan in all optimization modes. --- .../libSceAgcDriver/tests/SpirvOptimizer.cpp | 91 ++++++++++++++----- .../SpirvBackend/src/SpirvOptimizer.cpp | 18 +++- 2 files changed, 81 insertions(+), 28 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index 25c39567c..c9f445bbd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -7,12 +7,14 @@ #include #include #include +#include #include namespace { int failures = 0; constexpr std::uint32_t Vulkan11 = 0x00401000u; +constexpr std::uint32_t Spirv11 = 0x00010100u; constexpr std::uint32_t Spirv13 = 0x00010300u; void check(bool condition, const std::string& message) { @@ -55,7 +57,8 @@ std::string moduleSource(const NarrowType& type, bool vector, ArithmeticSupport text += "OpCapability DotProductInput4x8BitKHR\nOpExtension \"SPV_KHR_integer_dot_product\"\n"; } text += type.width == 8 ? "OpExtension \"SPV_KHR_8bit_storage\"\n" : "OpExtension \"SPV_KHR_16bit_storage\"\n"; - text += R"(OpMemoryModel Logical GLSL450 + text += R"(OpExtension "SPV_KHR_storage_buffer_storage_class" +OpMemoryModel Logical GLSL450 OpEntryPoint GLCompute %main "main" OpExecutionMode %main LocalSize 1 1 1 OpDecorate %block Block @@ -123,6 +126,14 @@ std::set capabilities(const std::vector& words) { return result; } +std::vector swapByteOrder(std::vector words) { + for (auto& word : words) { + word = (word >> 24u) | ((word & 0x00ff0000u) >> 8u) | + ((word & 0x0000ff00u) << 8u) | (word << 24u); + } + return words; +} + void testNarrowConversions(bool optimizationEnabled) { spvtools::SpirvTools validator(SPV_ENV_VULKAN_1_1); for (const auto& type : Types) { @@ -130,43 +141,50 @@ void testNarrowConversions(bool optimizationEnabled) { for (const auto support : {ArithmeticSupport::StorageOnly, ArithmeticSupport::Explicit, ArithmeticSupport::ImpliedInt8}) { if (support == ArithmeticSupport::ImpliedInt8 && (type.width != 8 || !vector)) continue; const bool arithmetic = support != ArithmeticSupport::StorageOnly; - const std::string label = std::string(type.name) + (vector ? " vector" : " scalar") + + const std::string name = std::string(type.name) + (vector ? " vector" : " scalar") + (support == ArithmeticSupport::ImpliedInt8 ? " implied Int8" : (arithmetic ? " arithmetic" : " storage only")); - const auto input = assemble(moduleSource(type, vector, support)); - if (!validator.Validate(input)) { - check(false, label + ": invalid test input"); - continue; - } - try { - const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false); - if (!validator.Validate(output)) { - check(false, label + ": optimizer returned invalid SPIR-V"); + auto native = assemble(moduleSource(type, vector, support)); + for (const auto& [version, swapped] : {std::pair{Spirv13, false}, std::pair{Spirv11, false}, std::pair{Spirv11, true}}) { + native[1] = version; + const auto input = swapped ? swapByteOrder(native) : native; + const auto label = name + (version == Spirv13 ? " SPIR-V 1.3" : " SPIR-V 1.1") + (swapped ? " swapped" : " native"); + if (!validator.Validate(input)) { + check(false, label + ": invalid test input"); continue; } - check(capabilities(output) == capabilities(input), label + ": optimizer changed device capabilities"); - check(opcodeCount(output, spv::OpStore) == (vector ? 1u : 2u), label + ": optimizer lost buffer stores"); - if (!optimizationEnabled) { - check(output == input, label + ": none mode changed the shader"); - } else { - check(opcodeCount(output, spv::OpIAdd) == 0, label + ": optimizer did not remove dead arithmetic"); - if (arithmetic && !type.floating) { - const auto conversion = type.signedInteger ? spv::OpSConvert : spv::OpUConvert; - check(opcodeCount(output, conversion) == 0, label + ": safe constant folding was disabled"); + try { + const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false); + if (!validator.Validate(output)) { + check(false, label + ": optimizer returned invalid SPIR-V"); + continue; } + const auto normalized = output.front() == spv::MagicNumber ? output : swapByteOrder(output); + check(capabilities(normalized) == capabilities(native), label + ": optimizer changed device capabilities"); + check(opcodeCount(normalized, spv::OpStore) == (vector ? 1u : 2u), label + ": optimizer lost buffer stores"); + if (!optimizationEnabled) { + check(output == input, label + ": none mode changed the shader"); + } else { + check(opcodeCount(normalized, spv::OpIAdd) == 0, label + ": optimizer did not remove dead arithmetic"); + if (arithmetic && !type.floating) { + const auto conversion = type.signedInteger ? spv::OpSConvert : spv::OpUConvert; + check(opcodeCount(normalized, conversion) == 0, label + ": safe constant folding was disabled"); + } + } + } catch (const std::exception& error) { + check(false, label + ": " + error.what()); } - } catch (const std::exception& error) { - check(false, label + ": " + error.what()); } } } } } -void expectRejected(const std::vector& words, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, const char* reason) { +void expectRejected(const std::vector& words, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, const char* reason, const char* diagnostic = nullptr) { try { static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(words, vulkanVersion, spirvVersion, false)); check(false, std::string("accepted ") + reason); - } catch (const std::exception&) { + } catch (const std::exception& error) { + if (diagnostic != nullptr) check(std::string(error.what()).starts_with(diagnostic), std::string(reason) + ": unexpected rejection: " + error.what()); } } @@ -178,9 +196,32 @@ void testInvalidInputIsRejected() { auto invalid = moduleSource(Types.front(), false, ArithmeticSupport::StorageOnly); invalid.insert(invalid.find("%main = OpFunction"), "%illegal = OpConstantNull %small\n"); expectRejected(assemble(invalid), Vulkan11, Spirv13, "an illegal storage-only narrow constant even when unused"); + const auto rejectMalformed = [](const std::vector& words, const char* reason) { + auto native = words; + if (native.size() > 1) native[1] = Spirv11; + constexpr auto diagnostic = "SPIR-V validation before optimization failed:"; + expectRejected(native, Vulkan11, Spirv13, reason, diagnostic); + expectRejected(swapByteOrder(native), Vulkan11, Spirv13, (std::string(reason) + " (swapped)").c_str(), diagnostic); + }; + for (std::size_t length = 0; length < 5; ++length) { + rejectMalformed(std::vector(valid.begin(), valid.begin() + length), "an incomplete module header"); + } auto malformed = valid; + malformed[0] = 0; + rejectMalformed(malformed, "an invalid module magic"); + malformed = valid; malformed[5] = spv::OpCapability; - expectRejected(malformed, Vulkan11, Spirv13, "a zero-length SPIR-V instruction"); + rejectMalformed(malformed, "a zero-length SPIR-V instruction"); + malformed[5] = (0xffffu << spv::WordCountShift) | spv::OpCapability; + rejectMalformed(malformed, "an instruction extending beyond the module"); + for (const auto& instruction : { + std::vector{(1u << spv::WordCountShift) | spv::OpCapability}, + std::vector{(3u << spv::WordCountShift) | spv::OpTypeInt, 1u, 8u}, + std::vector{(2u << spv::WordCountShift) | spv::OpTypeFloat, 1u}}) { + malformed.assign(valid.begin(), valid.begin() + 5); + malformed.insert(malformed.end(), instruction.begin(), instruction.end()); + rejectMalformed(malformed, "truncated capability or type operands"); + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp index aebbb0715..f03c6d3a5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp @@ -11,6 +11,7 @@ namespace ShaderRecompiler { namespace { bool HasLimitedUseTypes(std::span spirv) { + if (spirv.size() < 5) throw std::runtime_error("SPIRV-Tools: incomplete module header"); bool int8 = false, int16 = false, float16 = false; bool int8Capability = false, int16Capability = false, float16Capability = false; const bool swapped = spirv[0] != spv::MagicNumber; @@ -20,8 +21,18 @@ bool HasLimitedUseTypes(std::span spirv) { ((word << 8u) & 0x00ff0000u) | (word << 24u) : word; }; - for (std::size_t offset = 5; offset < spirv.size(); offset += wordAt(offset) >> 16u) { - switch (wordAt(offset) & 0xffffu) { + if (wordAt(0) != spv::MagicNumber) throw std::runtime_error("SPIRV-Tools: invalid module magic"); + for (std::size_t offset = 5; offset < spirv.size();) { + const auto instruction = wordAt(offset); + const auto wordCount = instruction >> spv::WordCountShift; + const auto opcode = instruction & spv::OpCodeMask; + if (wordCount == 0 || wordCount > spirv.size() - offset || + (opcode == spv::OpCapability && wordCount < 2) || + (opcode == spv::OpTypeInt && wordCount < 4) || + (opcode == spv::OpTypeFloat && wordCount < 3)) { + throw std::runtime_error("SPIRV-Tools: malformed instruction while scanning narrow types"); + } + switch (opcode) { case spv::OpCapability: int8Capability |= wordAt(offset + 1) == spv::CapabilityInt8 || wordAt(offset + 1) == spv::CapabilityDotProductInput4x8Bit; @@ -33,11 +44,12 @@ bool HasLimitedUseTypes(std::span spirv) { int16 |= wordAt(offset + 2) == 16; break; case spv::OpTypeFloat: - float16 |= (wordAt(offset) >> 16u) == 3 && wordAt(offset + 2) == 16; + float16 |= wordCount == 3 && wordAt(offset + 2) == 16; break; case spv::OpFunction: return (int8 && !int8Capability) || (int16 && !int16Capability) || (float16 && !float16Capability); } + offset += wordCount; } return (int8 && !int8Capability) || (int16 && !int16Capability) || (float16 && !float16Capability); }