fix(shader): guard runtime MODE changes

Read MODE rounding fields from the declared initial mode. Accept only
constant writes that preserve it; diagnose unsupported runtime changes.
Document the supported contract and add decoder-to-recompiler regressions.

Refs #1668, #981
This commit is contained in:
Sanjaysrinivas 2026-10-09 00:01:06 +02:00 committed by Adrià Franch
parent c9da0f93cf
commit 140c46093c
5 changed files with 231 additions and 7 deletions

View File

@ -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:$<TARGET_FILE_DIR:libc>")
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)

View File

@ -0,0 +1,190 @@
#include "ControlFlow/GraphBuilder.hpp"
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
#include "Recompiler.hpp"
#include "Translation/InstructionTranslator.hpp"
#include "BdaAbi.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
namespace {
using namespace ShaderRecompiler;
void require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
IrProgram translate(std::span<const std::uint32_t> code, std::optional<ShaderFloatMode> 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<const std::uint32_t> code, std::optional<ShaderFloatMode> mode, std::string_view diagnostic, std::uint32_t pc = 0) {
try {
static_cast<void>(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<std::uint32_t, 11> code{
0xb9140001, 0xb9150801, 0xb9161001, 0xb9171801, 0xb9180041,
0xb9190841, 0xb91a1041, 0xb91b0081, 0xb91c0881, 0xb91d00c1, 0xbf810000
};
constexpr std::array<std::uint32_t, 10> offsets{0, 0, 0, 0, 1, 1, 1, 2, 2, 3};
constexpr std::array<std::uint32_t, 10> 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<std::uint32_t, 2> code{instruction, 0xbf810000};
expectRejected(code, std::nullopt, "s_setreg_b32");
expectRejected(code, ShaderFloatMode{0xc0}, "s_setreg_b32");
}
}
void verifyImmediateWrites() {
constexpr std::array<std::uint32_t, 4> instructions{0xba801801, 0xba800801, 0xba800881, 0xba800841};
constexpr std::array<std::uint32_t, 4> masks{15, 3, 12, 6};
constexpr std::array<std::uint32_t, 4> 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<std::uint32_t, 4> 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<std::uint32_t, 3> full{0xba801801, 0, 0xbf810000};
static_cast<void>(translate(full, std::nullopt));
constexpr std::array<std::uint32_t, 3> nonzero{0xba801801, 15, 0xbf810000};
constexpr std::array<std::uint32_t, 3> 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<std::uint32_t, 3> 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<std::uint32_t, 2> code{0xbfa40000, 0xbf810000};
static_cast<void>(translate(code, std::nullopt));
constexpr std::array<std::uint32_t, 2> nonzero{0xbfa40003, 0xbf810000};
expectRejected(nonzero, ShaderFloatMode{0xc0}, "s_round_mode");
}
void verifyUnsupportedReads() {
for (const auto instruction : {0xb9140002u, 0xb9140101u, 0xb91408c1u, 0xb914f801u}) {
const std::array<std::uint32_t, 2> 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<std::uint32_t, 5> 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<std::uint32_t, 2> code{0xbfa40005, 0xbf810000};
constexpr std::array<std::uint32_t, 2> 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<void>(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;
}

View File

@ -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<std::uint64_t>(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:

View File

@ -65,5 +65,15 @@ flowchart LR
spv --> vk["libSceAgcDriver/Graphics:<br/>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.

View File

@ -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.