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:
parent
c9da0f93cf
commit
140c46093c
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue