Merge pull request #1343 from nikhilpanchal1/fix/recompiler-storage-only-constants
fix(recompiler): avoid illegal narrow constants during optimization
This commit is contained in:
commit
f0cc6750db
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue