Merge pull request #1343 from nikhilpanchal1/fix/recompiler-storage-only-constants

fix(recompiler): avoid illegal narrow constants during optimization
This commit is contained in:
Adrià 2026-10-08 13:28:34 +02:00 committed by GitHub
commit f0cc6750db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 612 additions and 14 deletions

View File

@ -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:$<TARGET_FILE_DIR:libc>")
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} "$<TARGET_FILE:${agcTest}>")
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:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
endforeach()
endif()
option(AGC_BUILD_VISUAL_TEST "Enable the standalone AGC SPIR-V visual test" OFF)

View File

@ -0,0 +1,28 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_NARROWCONSTANTSTOREFIXTURE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_NARROWCONSTANTSTOREFIXTURE_HPP
#include <array>
#include <cstdint>
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<std::uint32_t, 13> Code{
0x34020084, 0x7e040280, 0x7e0602ff, 0x00a10200, 0x7e0802ff, 0x0000fec3,
0xdc608000, 0x00000201,
0xdc688002, 0x00000401,
0xdc708008, 0x00000301,
0xbf810000,
};
constexpr std::array<std::uint8_t, LaneBytes> ExpectedLane{
0x00, Fill, 0xc3, 0xfe, Fill, Fill, Fill, Fill,
0x00, 0x02, 0xa1, 0x00, Fill, Fill, Fill, Fill,
};
}
#endif

View File

@ -0,0 +1,108 @@
#include "BdaAbi.hpp"
#include "NarrowConstantStoreFixture.hpp"
#include "Recompiler.hpp"
#include <array>
#include <cstdio>
#include <exception>
#include <span>
#include <spirv/unified1/spirv.hpp>
#include <stdexcept>
#include <string>
#include <string_view>
#if ANYPS5_ENABLE_SPIRV_TOOLS
#include <spirv-tools/libspirv.hpp>
#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<std::uint32_t, 3> capabilities{
spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess,
};
static constexpr std::array<std::string_view, 4> 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<std::uint32_t, 14> code{
0xbe801f00u, 0x800000ffu, 0x34u, 0xdc7519cbu, 0xb0020010u, 0x10005004u, 0xf4200100u,
0xfa000000u, 0xbf8cc07fu, 0x7e0049c8u, 0xf80008cfu, 0u, 0xbf810000u, 0x3f800000u,
};
const std::span<const std::uint32_t> words(code);
const std::array<MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(words)}}};
RecompileRequest request{};
request.shader = {ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(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<const std::uint32_t> code(NarrowConstantStoreFixture::Code);
const std::array<MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const std::array<std::uint32_t, 2> userData{0x10000u, 0u};
const ShaderComputeStageInfo compute{{NarrowConstantStoreFixture::Threads, 1, 1}, 0, {false, false, false}, false, 1, {}};
RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(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;
}
}

View File

@ -0,0 +1,240 @@
#include "SpirvBackend/SpirvOptimizer.hpp"
#include <spirv-tools/libspirv.hpp>
#include <spirv/unified1/spirv.hpp>
#include <array>
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <set>
#include <string>
#include <utility>
#include <vector>
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) {
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"(OpExtension "SPV_KHR_storage_buffer_storage_class"
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<std::uint32_t> 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<std::uint32_t> words;
check(tools.Assemble(source, &words), "test shader did not assemble");
return words;
}
std::size_t opcodeCount(const std::vector<std::uint32_t>& 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<std::uint32_t>(opcode)) ++count;
}
return count;
}
std::set<std::uint32_t> capabilities(const std::vector<std::uint32_t>& words) {
std::set<std::uint32_t> 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;
}
std::vector<std::uint32_t> swapByteOrder(std::vector<std::uint32_t> 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) {
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 name = std::string(type.name) + (vector ? " vector" : " scalar") +
(support == ArithmeticSupport::ImpliedInt8 ? " implied Int8" : (arithmetic ? " arithmetic" : " storage only"));
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;
}
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());
}
}
}
}
}
}
void expectRejected(const std::vector<std::uint32_t>& words, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, const char* reason, const char* diagnostic = nullptr) {
try {
static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(words, vulkanVersion, spirvVersion, false));
check(false, std::string("accepted ") + reason);
} catch (const std::exception& error) {
if (diagnostic != nullptr) check(std::string(error.what()).starts_with(diagnostic), std::string(reason) + ": unexpected rejection: " + error.what());
}
}
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");
const auto rejectMalformed = [](const std::vector<std::uint32_t>& 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<std::uint32_t>(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;
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<std::uint32_t>{(1u << spv::WordCountShift) | spv::OpCapability},
std::vector<std::uint32_t>{(3u << spv::WordCountShift) | spv::OpTypeInt, 1u, 8u},
std::vector<std::uint32_t>{(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");
}
}
}
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;
}

View File

@ -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 <windows.h>
#endif
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <span>
#include <string>
namespace {
using AgcDriver::Graphics::Require;
using namespace NarrowConstantStoreFixture;
constexpr std::size_t BlockBytes = 65536;
class GuestBlock {
public:
GuestBlock() {
#ifdef _WIN32
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
#else
block = static_cast<std::uint8_t*>(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<std::uint64_t>(reinterpret_cast<std::uintptr_t>(guest.Data()));
const std::array<std::uint32_t, 2> userData{
static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u),
};
const std::span<const std::uint32_t> code(Code);
const std::array<MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1, {}};
RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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;
}
}

View File

@ -1,12 +1,61 @@
#include "SpirvBackend/SpirvOptimizer.hpp"
#include <spirv-tools/libspirv.hpp>
#include <spirv-tools/optimizer.hpp>
#include <spirv/unified1/spirv.hpp>
#include <cstdlib>
#include <stdexcept>
#include <string>
namespace ShaderRecompiler {
namespace {
bool HasLimitedUseTypes(std::span<const std::uint32_t> 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;
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;
};
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;
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 |= 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);
}
}
std::vector<std::uint32_t> ValidateAndOptimizeSpirv(std::span<const std::uint32_t> spirv, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, bool allowOffsetTextureOperand) {
spv_target_env environment;
const auto apiVersion = vulkanVersion & ~0xfffu;
@ -48,10 +97,19 @@ std::vector<std::uint32_t> ValidateAndOptimizeSpirv(std::span<const std::uint32_
// access. Exhaustive inlining multiplies module size by ~10x and optimization time by ~20x, so the
// default pipeline keeps the performance passes that work per function and leaves inlining to the driver.
// APS5_SPIRV_OPT=full restores the SPIRV-Tools performance pipeline, =none skips optimization.
if (mode == nullptr || std::string(mode) != "full") {
const bool allowFolding = !HasLimitedUseTypes(spirv);
const bool full = mode != nullptr && std::string(mode) == "full";
if (full && allowFolding) {
optimizer.RegisterPerformancePasses(true);
} else {
optimizer.RegisterPass(spvtools::CreateWrapOpKillPass())
.RegisterPass(spvtools::CreateDeadBranchElimPass())
.RegisterPass(spvtools::CreateAggressiveDCEPass(true))
.RegisterPass(spvtools::CreateDeadBranchElimPass());
if (full) {
optimizer.RegisterPass(spvtools::CreateMergeReturnPass())
.RegisterPass(spvtools::CreateInlineExhaustivePass())
.RegisterPass(spvtools::CreateEliminateDeadFunctionsPass());
}
optimizer.RegisterPass(spvtools::CreateAggressiveDCEPass(true))
.RegisterPass(spvtools::CreatePrivateToLocalPass())
.RegisterPass(spvtools::CreateLocalSingleBlockLoadStoreElimPass())
.RegisterPass(spvtools::CreateLocalSingleStoreElimPass())
@ -62,17 +120,37 @@ std::vector<std::uint32_t> ValidateAndOptimizeSpirv(std::span<const std::uint32_
.RegisterPass(spvtools::CreateLocalSingleStoreElimPass())
.RegisterPass(spvtools::CreateAggressiveDCEPass(true))
.RegisterPass(spvtools::CreateLocalMultiStoreElimPass())
.RegisterPass(spvtools::CreateAggressiveDCEPass(true))
.RegisterPass(spvtools::CreateCCPPass())
.RegisterPass(spvtools::CreateAggressiveDCEPass(true))
.RegisterPass(spvtools::CreateDeadBranchElimPass())
.RegisterPass(spvtools::CreateRedundancyEliminationPass())
.RegisterPass(spvtools::CreateSimplificationPass())
.RegisterPass(spvtools::CreateAggressiveDCEPass(true))
.RegisterPass(spvtools::CreateBlockMergePass())
.RegisterPass(spvtools::CreateSimplificationPass());
} else {
optimizer.RegisterPerformancePasses(true);
.RegisterPass(spvtools::CreateAggressiveDCEPass(true));
if (allowFolding) optimizer.RegisterPass(spvtools::CreateCCPPass());
optimizer.RegisterPass(spvtools::CreateAggressiveDCEPass(true));
if (full) optimizer.RegisterPass(spvtools::CreateLoopUnrollPass(true));
optimizer.RegisterPass(spvtools::CreateDeadBranchElimPass())
.RegisterPass(spvtools::CreateRedundancyEliminationPass());
if (full) optimizer.RegisterPass(spvtools::CreateCombineAccessChainsPass());
if (allowFolding) optimizer.RegisterPass(spvtools::CreateSimplificationPass());
if (full) {
optimizer.RegisterPass(spvtools::CreateScalarReplacementPass(0))
.RegisterPass(spvtools::CreateLocalAccessChainConvertPass())
.RegisterPass(spvtools::CreateLocalSingleBlockLoadStoreElimPass())
.RegisterPass(spvtools::CreateLocalSingleStoreElimPass())
.RegisterPass(spvtools::CreateAggressiveDCEPass(true))
.RegisterPass(spvtools::CreateSSARewritePass())
.RegisterPass(spvtools::CreateAggressiveDCEPass(true))
.RegisterPass(spvtools::CreateVectorDCEPass())
.RegisterPass(spvtools::CreateDeadInsertElimPass())
.RegisterPass(spvtools::CreateDeadBranchElimPass())
.RegisterPass(spvtools::CreateIfConversionPass())
.RegisterPass(spvtools::CreateCopyPropagateArraysPass())
.RegisterPass(spvtools::CreateReduceLoadSizePass());
}
optimizer.RegisterPass(spvtools::CreateAggressiveDCEPass(true))
.RegisterPass(spvtools::CreateBlockMergePass());
if (full) {
optimizer.RegisterPass(spvtools::CreateRedundancyEliminationPass())
.RegisterPass(spvtools::CreateDeadBranchElimPass())
.RegisterPass(spvtools::CreateBlockMergePass());
}
if (allowFolding) optimizer.RegisterPass(spvtools::CreateSimplificationPass());
}
// Validating after every pass costs more than the passes on large shaders; the result is validated below.
optimizer.SetValidateAfterAll(false);

View File

@ -231,6 +231,7 @@ Throughout the project, every function at every stage either **does exactly what
- [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw
- [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown
- [Wide-character conversions](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `wcsrtombs` and [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still pass guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, they misread and miswrite guest strings. The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits
- [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe.
- [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported
- [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries
- [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done