fix(shader): decode the scalar aperture sources 235-238 instead of failing the shader

This commit is contained in:
Dextroid17 2026-10-08 21:22:39 -04:00
parent a29ef59219
commit b7e32b3648
7 changed files with 127 additions and 1 deletions

View File

@ -1225,6 +1225,14 @@ if(BUILD_TESTING)
set_tests_properties(scalar_scratch_load PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(aperture_sources_tests ${LIBS_INCLUDE_DIR}/tests/ApertureSources.cpp)
target_link_libraries(aperture_sources_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(aperture_sources_tests)
add_test(NAME aperture_sources COMMAND aperture_sources_tests)
if(WIN32)
set_tests_properties(aperture_sources PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(scalar_call_tests ${LIBS_INCLUDE_DIR}/tests/ScalarCall.cpp)
target_link_libraries(scalar_call_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(scalar_call_tests)

View File

@ -0,0 +1,89 @@
#include "IntermediateRepresentation/IrProgram.hpp"
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
#include "RdnaDecoder/RdnaScalarOpDecoder.hpp"
#include "Translation/TranslationContext.hpp"
#include <array>
#include <cstdio>
#include <stdexcept>
#include <string>
using namespace ShaderRecompiler;
namespace {
struct ApertureSource {
std::uint32_t code;
RdnaOperandKind kind;
const char* name;
};
const std::array<ApertureSource, 4> ApertureSources{{
{235u, RdnaOperandKind::SrcSharedBase, "src_shared_base"},
{236u, RdnaOperandKind::SrcSharedLimit, "src_shared_limit"},
{237u, RdnaOperandKind::SrcPrivateBase, "src_private_base"},
{238u, RdnaOperandKind::SrcPrivateLimit, "src_private_limit"},
}};
void Require(bool value, const std::string& message) {
if (!value) throw std::runtime_error(message);
}
// The refusal must name the source it refused: a bare "not modelled" cannot be told from a decode gap.
template <typename Fn>
void ExpectRefusal(const char* source, Fn translate) {
try {
translate();
} catch (const std::exception& error) {
if (std::string(error.what()).find(source) == std::string::npos) {
throw std::runtime_error(std::string("the refusal does not name ") + source + ": " + error.what());
}
return;
}
throw std::runtime_error(std::string("reading ") + source + " was accepted");
}
// s_mov_b32 s0, <source>: SOP1, format 0x7d, opcode 0x03, the source code in the low byte.
RdnaInstruction ScalarMove(std::uint32_t source) {
const std::array<std::uint32_t, 2> words{0x80000000u | (0x7du << 23u) | (0x03u << 8u) | source, 0u};
return DecodeRdnaSop1(0u, words, 0u);
}
void Translate(const RdnaInstruction& instruction) {
IrProgram program;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder() = {&block};
program.Metadata().blockInfo.resize(1);
TranslationContext context(program, block, 256);
context.TranslateInstruction(instruction);
}
void CheckDecode() {
for (const auto& source : ApertureSources) {
const auto operand = DecodeRdnaScalarSource(source.code, 0u);
Require(operand.kind == source.kind,
"scalar source " + std::to_string(source.code) + " decoded to the wrong operand kind");
Require(RdnaOperandToString(operand) == source.name,
"scalar source " + std::to_string(source.code) + " is named " + RdnaOperandToString(operand));
}
}
// Translating the move reads the source as a value, which is the path a title that carries the
// aperture into an address does not take and one that reads it does.
void CheckReadRefuses() {
for (const auto& source : ApertureSources) {
const RdnaInstruction move = ScalarMove(source.code);
Require(move.op == RdnaOpcode::SMovB32, "the scalar move decoded to another opcode");
Require(move.source0.kind == source.kind, "the scalar move decodes source0 as the wrong operand kind");
ExpectRefusal(source.name, [&] { Translate(move); });
}
}
}
int main() {
CheckDecode();
CheckReadRefuses();
std::puts("aperture sources tests passed");
return 0;
}

View File

@ -53,7 +53,11 @@ enum class RdnaOperandKind {
VccZ,
ExecZ,
M0,
PopsExitingWaveId
PopsExitingWaveId,
SrcSharedBase,
SrcSharedLimit,
SrcPrivateBase,
SrcPrivateLimit
};
enum class RdnaImageDimension : std::uint32_t {

View File

@ -217,6 +217,10 @@ RdnaOperand DecodeRdnaScalarSource(std::uint32_t code, std::uint32_t programCoun
case 125u: operand.kind = RdnaOperandKind::Null; return operand;
case 126u: operand.kind = RdnaOperandKind::ExecLo; return operand;
case 127u: operand.kind = RdnaOperandKind::ExecHi; return operand;
case 235u: operand.kind = RdnaOperandKind::SrcSharedBase; return operand;
case 236u: operand.kind = RdnaOperandKind::SrcSharedLimit; return operand;
case 237u: operand.kind = RdnaOperandKind::SrcPrivateBase; return operand;
case 238u: operand.kind = RdnaOperandKind::SrcPrivateLimit; return operand;
case 239u: operand.kind = RdnaOperandKind::PopsExitingWaveId; return operand;
case 248u:
operand.kind = RdnaOperandKind::FloatInlineConstant;
@ -315,6 +319,10 @@ std::string RdnaOperandToString(const RdnaOperand& operand) {
case RdnaOperandKind::ExecZ: return "execz";
case RdnaOperandKind::M0: return "m0";
case RdnaOperandKind::PopsExitingWaveId: return "pops_exiting_wave_id";
case RdnaOperandKind::SrcSharedBase: return "src_shared_base";
case RdnaOperandKind::SrcSharedLimit: return "src_shared_limit";
case RdnaOperandKind::SrcPrivateBase: return "src_private_base";
case RdnaOperandKind::SrcPrivateLimit: return "src_private_limit";
}
throw std::invalid_argument("unsupported operand kind for string conversion");

View File

@ -436,6 +436,10 @@ RdnaOperand scalarSource(std::uint32_t code) {
case 125u: operand.kind = RdnaOperandKind::Null; return operand;
case 126u: operand.kind = RdnaOperandKind::ExecLo; return operand;
case 127u: operand.kind = RdnaOperandKind::ExecHi; return operand;
case 235u: operand.kind = RdnaOperandKind::SrcSharedBase; return operand;
case 236u: operand.kind = RdnaOperandKind::SrcSharedLimit; return operand;
case 237u: operand.kind = RdnaOperandKind::SrcPrivateBase; return operand;
case 238u: operand.kind = RdnaOperandKind::SrcPrivateLimit; return operand;
case 239u: operand.kind = RdnaOperandKind::PopsExitingWaveId; return operand;
case 248u: operand.kind = RdnaOperandKind::FloatInlineConstant; operand.value = floatBits(0.15915494309189535f); return operand;
case 251u: operand.kind = RdnaOperandKind::VccZ; return operand;

View File

@ -1,7 +1,9 @@
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
#include "Translation/TranslationContext.hpp"
#include <algorithm>
#include <array>
#include <stdexcept>
#include <string>
namespace ShaderRecompiler {
@ -418,6 +420,11 @@ IrU32 TranslationContext::readScalarCode(std::uint32_t code) {
const std::array<IrU32, 2> mask = ballotMask(IrU1(ir.GetExec()));
return mask[code - 126u];
}
case 235u:
case 236u:
case 237u:
case 238u:
throw std::runtime_error("TranslationContext::readScalarCode: the scalar aperture source " + RdnaOperandToString(DecodeRdnaScalarSource(code, 0u)) + " is not modelled");
default: return IrU32(ir.Constant(0u));
}
}
@ -447,6 +454,11 @@ IrU32 TranslationContext::readRawU32(const RdnaOperand& operand) {
const IrU32 zero(ir.Constant(0u));
return IrU32(ir.Select(ir.IEqual(mask.Value(), zero.Value()), ir.Constant(1u), zero.Value()));
}
case RdnaOperandKind::SrcSharedBase:
case RdnaOperandKind::SrcSharedLimit:
case RdnaOperandKind::SrcPrivateBase:
case RdnaOperandKind::SrcPrivateLimit:
throw std::runtime_error("TranslationContext::readRawU32: the scalar aperture source " + RdnaOperandToString(operand) + " is not modelled");
default: throw std::runtime_error("TranslationContext::readRawU32 invalid decoded operand used as a raw U32 source");
}
}

View File

@ -448,6 +448,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Font writing lines](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontCreateWritingLine`, `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics` and `sceFontWritingLineClear` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libFont.cpp): runs are placed left to right, each at the line's advance so far, with no spacing or adjustment. Only horizontal and left-to-right lines, writing attribute 0 and non-null run metrics are modelled; other values, the metrics of an empty line and unknown or null handles throw, since no reference gives their result or error code. `sceFontWritingLineGetOrderingSpace` still throws (KytyPS5's values are placeholders). Lines are allocated on the host, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for.
- [s_swappc_b64](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) writes the link pair with the same model as `s_getpc_b64`: the guest return address `pc + 4` as a byte offset over `GetShaderBase`. Any other use of the link pair (arithmetic, compare, hash, store, or a second `s_swappc_b64` sharing it) throws; constant-offset folding over the link is not modeled. The special encoding `s_swappc_b64` with SDST 125 still decodes as `s_setpc_b64`, which matches the hardware: measured on RDNA2 (gfx1036) in wave32 and wave64, it branches to the source pair and leaves a marker SGPR pair untouched, and with an SGPR destination the link is `PC + 4`.
- The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs.
- The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler) decode to their own operand kinds (scalar source codes 235 to 238, named as in LLVM's `SIRegisterInfo.td`), but their value is not modelled: reading one refuses with the source it refused named, because the wave's shared and private aperture bases and limits are unknown. Both the scalar-code read and the raw-operand read refuse; a shader that only carries the value into an address still prepares, one that reads it does not
- [VertexBufferReadSize](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read.
- [sceImeKeyboardGetResourceId](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard
- [Player review dialog](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) shows nothing: `scePlayerReviewDialogOpen` finishes at once and the result reports that the user canceled, as `sceWebBrowserDialogOpen` does