diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 262959ff0..3a94e4860 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -448,6 +448,12 @@ target_link_libraries(agc_shader_control_flow_tests PRIVATE shader_recompiler_ag set_target_properties(agc_shader_control_flow_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_control_flow_tests) +add_test_executable(agc_shader_mode_register_tests EXCLUDE_FROM_ALL tests/ShaderModeRegisters.cpp) +target_include_directories(agc_shader_mode_register_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_shader_mode_register_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_shader_mode_register_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_shader_mode_register_tests) + add_test_executable(agc_driver_flip_tests EXCLUDE_FROM_ALL tests/Flip.cpp ) @@ -698,6 +704,12 @@ target_include_directories(agc_driver_prepared_shaders_tests PRIVATE ) if(BUILD_TESTING) + set_target_properties(agc_shader_mode_register_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests) + set_tests_properties(agc_shader_mode_register PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_shader_mode_register PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shader_bindless COMMAND agc_shader_memory_tests --bindless) set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderModeRegisters.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderModeRegisters.cpp new file mode 100644 index 000000000..632181959 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderModeRegisters.cpp @@ -0,0 +1,190 @@ +#include "ControlFlow/GraphBuilder.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "Recompiler.hpp" +#include "Translation/InstructionTranslator.hpp" +#include "BdaAbi.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +IrProgram translate(std::span code, std::optional mode, std::uint32_t waveSize = 32) { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + const auto graph = GraphBuilder{}.Build(decoded); + const ShaderComputeInputInfo compute{}; + TranslateOptions options; + options.stage = ShaderStageKind::Compute; + options.waveSize = waveSize; + options.floatMode = mode; + options.inputInfo.compute = &compute; + return InstructionTranslator{}.Translate(decoded, graph, options); +} + +void expectRegister(const IrProgram& program, std::uint32_t index, std::uint32_t expected) { + const IrValue* value = nullptr; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() != IrOpcode::SetScalarRegister) continue; + const auto reg = instruction->Argument(0)->Register(); + if (reg.bank == RegisterBank::Scalar && reg.index == index) value = instruction->Argument(1); + } + } + require(value && value->HasImmediate() && value->ImmediateU32() == expected, + "MODE read into s" + std::to_string(index) + " did not produce " + std::to_string(expected)); +} + +void expectRejected(std::span code, std::optional mode, std::string_view diagnostic, std::uint32_t pc = 0) { + try { + static_cast(translate(code, mode)); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + require(message.find(diagnostic) != std::string::npos && message.find("at pc " + std::to_string(pc) + " ") != std::string::npos, + "unexpected MODE diagnostic: " + message); + return; + } + throw std::runtime_error("unsupported MODE operation was accepted: " + std::string(diagnostic)); +} + +void verifyReads() { + constexpr std::array code{ + 0xb9140001, 0xb9150801, 0xb9161001, 0xb9171801, 0xb9180041, + 0xb9190841, 0xb91a1041, 0xb91b0081, 0xb91c0881, 0xb91d00c1, 0xbf810000 + }; + constexpr std::array offsets{0, 0, 0, 0, 1, 1, 1, 2, 2, 3}; + constexpr std::array masks{1, 3, 7, 15, 1, 3, 7, 1, 3, 1}; + for (const auto waveSize : {32u, 64u}) { + for (std::uint32_t mode = 0; mode < 256; ++mode) { + const auto program = translate(code, ShaderFloatMode{mode, true, true, true}, waveSize); + for (std::uint32_t index = 0; index < offsets.size(); ++index) { + expectRegister(program, 20 + index, (mode >> offsets[index]) & masks[index]); + } + } + const auto legacy = translate(code, std::nullopt, waveSize); + for (std::uint32_t index = 0; index < offsets.size(); ++index) expectRegister(legacy, 20 + index, 0); + } +} + +void verifyDynamicWrites() { + for (const auto instruction : {0xb9800001u, 0xb9801801u, 0xb9800041u, 0xb9801881u, 0xb980f801u}) { + const std::array code{instruction, 0xbf810000}; + expectRejected(code, std::nullopt, "s_setreg_b32"); + expectRejected(code, ShaderFloatMode{0xc0}, "s_setreg_b32"); + } +} + +void verifyImmediateWrites() { + constexpr std::array instructions{0xba801801, 0xba800801, 0xba800881, 0xba800841}; + constexpr std::array masks{15, 3, 12, 6}; + constexpr std::array offsets{0, 0, 2, 1}; + for (std::uint32_t rounding = 0; rounding < 16; ++rounding) { + for (std::uint32_t value = 0; value < 16; ++value) { + for (std::uint32_t field = 0; field < instructions.size(); ++field) { + const std::array code{instructions[field], 0xfffffff0u | value, 0xb9141801, 0xbf810000}; + const ShaderFloatMode mode{0xc0u | rounding}; + if (((value << offsets[field]) & masks[field]) != (rounding & masks[field])) { + expectRejected(code, mode, "s_setreg_imm32_b32"); + } else { + expectRegister(translate(code, mode), 20, rounding); + } + } + } + } + constexpr std::array full{0xba801801, 0, 0xbf810000}; + static_cast(translate(full, std::nullopt)); + constexpr std::array nonzero{0xba801801, 15, 0xbf810000}; + constexpr std::array denorm{0xba801881, 0, 0xbf810000}; + expectRejected(nonzero, ShaderFloatMode{0xc0}, "s_setreg_imm32_b32"); + expectRejected(denorm, ShaderFloatMode{0xc0}, "s_setreg_imm32_b32"); +} + +void verifyRoundInstruction() { + for (std::uint32_t rounding = 0; rounding < 16; ++rounding) { + for (std::uint32_t value = 0; value < 16; ++value) { + for (const auto upper : {0u, 0x8000u}) { + const std::array code{0xbfa40000u | upper | value, 0xb9141801, 0xbf810000}; + const ShaderFloatMode mode{0xc0u | rounding}; + if (value != rounding) expectRejected(code, mode, "s_round_mode"); + else expectRegister(translate(code, mode), 20, rounding); + } + } + } + constexpr std::array code{0xbfa40000, 0xbf810000}; + static_cast(translate(code, std::nullopt)); + constexpr std::array nonzero{0xbfa40003, 0xbf810000}; + expectRejected(nonzero, ShaderFloatMode{0xc0}, "s_round_mode"); +} + +void verifyUnsupportedReads() { + for (const auto instruction : {0xb9140002u, 0xb9140101u, 0xb91408c1u, 0xb914f801u}) { + const std::array code{instruction, 0xbf810000}; + expectRejected(code, ShaderFloatMode{0xc0}, "s_getreg_b32"); + } +} + +void verifyBranch() { + for (std::uint32_t rounding = 0; rounding < 16; ++rounding) { + const ShaderFloatMode mode{0xc0u | rounding}; + std::array code{0xbf840002, 0xba801801, rounding, 0xb9141801, 0xbf810000}; + expectRegister(translate(code, mode), 20, rounding); + code[2] ^= 1u; + expectRejected(code, mode, "s_setreg_imm32_b32", 4); + } +} + +void verifyRecompilePropagation() { + constexpr std::array code{0xbfa40005, 0xbf810000}; + constexpr std::array capabilities{1, 61}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000, code, 0, {}}; + request.context.waveSize = 64; + request.context.compute = ShaderComputeStageInfo{{1, 1, 1}, 0, {false, false, false}, false, 0}; + request.context.floatMode = ShaderFloatMode{0xc5}; + request.target.vulkanVersion = 0x00403000; + request.target.spirvVersion = 0x00010600; + request.target.subgroupSize = 64; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.maxWorkgroupSize = {1024, 1024, 64}; + request.target.maxWorkgroupInvocations = 1024; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + require(!Recompile(request).spirv.empty(), "declared MODE did not reach full recompilation"); + request.context.floatMode.reset(); + try { + static_cast(Recompile(request)); + } catch (const std::runtime_error& error) { + require(std::string(error.what()).find("s_round_mode") != std::string::npos, "unexpected full recompilation failure"); + return; + } + throw std::runtime_error("full recompilation ignored a runtime MODE change"); +} + +} + +int main() { + int failures = 0; + for (const auto check : {verifyReads, verifyDynamicWrites, verifyImmediateWrites, verifyRoundInstruction, verifyUnsupportedReads, verifyBranch, verifyRecompilePropagation}) { + try { + check(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + ++failures; + } + } + if (failures != 0) return 1; + std::puts("Shader MODE register tests passed"); + return 0; +} diff --git a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp index 7136560e7..ada03c65b 100644 --- a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp @@ -56,6 +56,11 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { scalarSelect64(inst, inst.destination); return true; case RdnaOpcode::SSetregB32: + if ((inst.source1.value & 0x3fu) == 1u) { + throw std::runtime_error("s_setreg_b32 at pc " + std::to_string(inst.programCounter) + " writes MODE from an SGPR: runtime MODE changes are not implemented"); + } + emitControlNop(); + return true; case RdnaOpcode::SVersion: emitControlNop(); return true; @@ -64,8 +69,12 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { const std::uint32_t offset = (field >> 6u) & 0x1fu; const std::uint32_t size = ((field >> 11u) & 0x1fu) + 1u; const std::uint64_t written = static_cast(inst.source0.value) & ((std::uint64_t{1} << size) - 1u); - if ((field & 0x3fu) != 1u || offset + size > 4u || written != 0u) { - throw std::runtime_error("s_setreg_imm32_b32 at pc " + std::to_string(inst.programCounter) + " writes a hardware register field other than round to nearest even in MODE"); + if ((field & 0x3fu) != 1u || offset + size > 4u) { + throw std::runtime_error("s_setreg_imm32_b32 at pc " + std::to_string(inst.programCounter) + " writes a hardware register field other than the MODE rounding fields"); + } + const std::uint32_t initial = floatMode.has_value() ? floatMode->floatMode : 0u; + if (written != ((initial >> offset) & ((1u << size) - 1u))) { + throw std::runtime_error("s_setreg_imm32_b32 at pc " + std::to_string(inst.programCounter) + " changes the initial MODE rounding fields: runtime MODE changes are not implemented"); } emitControlNop(); return true; @@ -77,7 +86,8 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { if ((field & 0x3fu) != 1u || offset + size > 4u) { throw std::runtime_error("s_getreg_b32 at pc " + std::to_string(inst.programCounter) + " reads hardware register " + std::to_string(field & 0x3fu) + " bits " + std::to_string(offset) + ".." + std::to_string(offset + size - 1u) + ": only the MODE round mode fields are modeled"); } - writeRawU32(inst.destination, IrU32(ir.Constant(0u))); + const std::uint32_t initial = floatMode.has_value() ? floatMode->floatMode : 0u; + writeRawU32(inst.destination, IrU32(ir.Constant((initial >> offset) & ((1u << size) - 1u)))); return true; } case RdnaOpcode::SCmovkI32: { @@ -413,12 +423,14 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { case RdnaOpcode::SDecperflevel: emitControlNop(); return true; - case RdnaOpcode::SRoundMode: - if (inst.source0.value != 0u) { - throw std::runtime_error("s_round_mode " + std::to_string(inst.source0.value) + " at pc " + std::to_string(inst.programCounter) + " selects a rounding mode other than round to nearest even"); + case RdnaOpcode::SRoundMode: { + const std::uint32_t initial = floatMode.has_value() ? floatMode->floatMode : 0u; + if ((inst.source0.value & 0xfu) != (initial & 0xfu)) { + throw std::runtime_error("s_round_mode " + std::to_string(inst.source0.value) + " at pc " + std::to_string(inst.programCounter) + " changes the initial MODE rounding fields: runtime MODE changes are not implemented"); } emitControlNop(); return true; + } case RdnaOpcode::SDenormMode: throw std::runtime_error("s_denorm_mode " + std::to_string(inst.source0.value) + " at pc " + std::to_string(inst.programCounter) + ": the recompiler does not model denormal modes"); case RdnaOpcode::SSethalt: diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 1a617546a..83efc2009 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -65,5 +65,15 @@ flowchart LR spv --> vk["libSceAgcDriver/Graphics:
Vulkan pipeline"] ``` +### Shader MODE register access + +`GuestContext::floatMode` carries the initial shader mode through `Recompile` and `TranslateOptions` into each block's `TranslationContext`. `s_getreg_b32` reads confined to MODE bits 0–3 return the selected initial rounding bits in the destination's low bits. Bits 0–1 select f32 rounding; bits 2–3 select f16/f64 rounding. Missing mode metadata retains the legacy round-to-nearest-even assumption. Reading other MODE fields or hardware registers throws. + +An immediate MODE write confined to the rounding fields is accepted only when its selected replacement bits equal the initial state. Source bits above the selected width are ignored. `s_round_mode` similarly compares its low four immediate bits with the initial rounding fields. A state-changing write or an SGPR-sourced MODE write throws with its instruction and program counter. Runtime transitions require control-flow-aware mode tracking and matching arithmetic lowering. Writes to other hardware registers retain their existing behavior and remain technical debt. + +The register layout and instruction contract follow [AMD RDNA2 ISA sections 5.8 and 6.4, Table 24](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna2-shader-instruction-set-architecture.pdf), cross-checked against LLVM 20.1.8's [hardware-register encoding](https://github.com/llvm/llvm-project/blob/87f0227cb60147a26a1eeb4fb06e3b505e9c7261/llvm/lib/Target/AMDGPU/Utils/AMDGPUBaseInfo.h) and [MODE state analysis](https://github.com/llvm/llvm-project/blob/87f0227cb60147a26a1eeb4fb06e3b505e9c7261/llvm/lib/Target/AMDGPU/SIModeRegister.cpp). This contract does not establish arithmetic support for every initial mode or console-specific behavior. + +`agc_shader_mode_register` decodes instruction words, builds the control-flow graph and checks production translation without a Vulkan device. It covers rounding subfield reads, masked constant writes, branches, unsupported runtime changes and diagnostics, plus mode propagation through full recompilation. The requirements remain active in Release; CTest bounds execution to 20 seconds. + - [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. - `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 29cfa2b3a..936720041 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -295,7 +295,7 @@ Throughout the project, every function at every stage either **does exactly what - [8_SRGB and 8_8_SRGB textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. - [Samples and gathers](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures throw on a device that cannot sample R8_SRGB or R8G8_SRGB. - [Bindless image table](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. -- The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. +- [Shader MODE register access](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) reads the initial rounding fields from `GuestContext::floatMode`; missing metadata retains the legacy round-to-nearest-even assumption. Immediate rounding writes are accepted only when they preserve that state. State-changing or SGPR-sourced MODE writes throw until runtime mode tracking and affected arithmetic are implemented. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. - A [flat SRT read](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. - Repeated `s_load`s of one [flat SRT](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) dword through a null or unmapped pointer report one of their pcs, and an `s_load` whose inaccessible dwords are all unused, or only address repeated loads, reports no fault of its own. - A value read through a null or unmapped pointer still throws at record time if a [branch condition, uniform fill or table base](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data, even if the console skips the read.