feat(shader): execute bounded S_CALL_B64

Reuse the prerequisite call-region analysis for signed relative calls, including callees after s_endpgm. Lower the saved link as shader base plus PC+4 and reject unsupported targets and call shapes.

Add decoder, IR, CFG and Vulkan execution regressions. Seven focused CMake tests pass; five layouts cover 15 dispatches and 3,200 output comparisons on lavapipe. Record ISA sources and native RDNA2/PS5 limits in TechnicalDebt.
This commit is contained in:
Sanjaysrinivas 2026-10-08 13:14:06 +02:00
parent 4ddbf0497d
commit ee0b1109f4
8 changed files with 268 additions and 63 deletions

View File

@ -285,7 +285,7 @@ void verifyNullSwappc() {
verifyLongBranch("backward long branch", LongBranchBack, 0x24u, 0x08u);
const std::array<std::uint32_t, 3> call{0xbe8c1f00u, 0xbe8e210cu, 0xbf810000u};
const std::vector<std::tuple<std::string_view, std::vector<std::uint32_t>, std::string_view>> refused{
{"s_swappc_b64 with a return address", {call.begin(), call.end()}, "unclosable s_swappc_b64 call/return pairing"},
{"s_swappc_b64 with a return address", {call.begin(), call.end()}, "unclosable scalar call/return pairing"},
{"index rewritten after the bound", patched(DwordTableGetpcFirst, 5, {0xbe9203ffu, 0x00000040u}), "unsupported dynamic s_setpc_b64"},
{"index scaled for two-dword entries", patched(DwordTableGetpcFirst, 7, {0x8f128312u}), "unsupported dynamic s_setpc_b64"},
{"entry rewritten after the load", patched(DwordTableGetpcFirst, 11, {0xbe8e0380u}), "unsupported dynamic s_setpc_b64"},

View File

@ -0,0 +1,124 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t MaxThreads = 64;
constexpr std::uint32_t Inputs = 4;
constexpr std::uint32_t Results = 4;
alignas(256) std::array<std::uint32_t, MaxThreads * Inputs> Input{};
alignas(256) std::array<std::uint32_t, MaxThreads * Results> Output{};
alignas(256) constexpr std::array<std::uint32_t, 21> SwappcForward{0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbe8a1f00u, 0x800aff0au, 0x00000014u, 0x820b800bu, 0xbe88210au, 0xbf820008u, 0x4a0808ffu, 0x13579bdfu, 0x3a0a0affu, 0xa5a5a5a5u, 0x4a0c0f06u, 0x3a0e0effu, 0x5a5a5a5au, 0xbe802008u, 0xe0781000u, 0x80010401u, 0xbf810000u};
alignas(256) constexpr std::array<std::uint32_t, 17> CallForward{0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbb080001u, 0xbf820008u, 0x4a0808ffu, 0x13579bdfu, 0x3a0a0affu, 0xa5a5a5a5u, 0x4a0c0f06u, 0x3a0e0effu, 0x5a5a5a5au, 0xbe802008u, 0xe0781000u, 0x80010401u, 0xbf810000u};
alignas(256) constexpr std::array<std::uint32_t, 18> CallBackward{0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbf820008u, 0x4a0808ffu, 0x13579bdfu, 0x3a0a0affu, 0xa5a5a5a5u, 0x4a0c0f06u, 0x3a0e0effu, 0x5a5a5a5au, 0xbe802008u, 0xbb08fff7u, 0xbf820000u, 0xe0781000u, 0x80010401u, 0xbf810000u};
alignas(256) constexpr std::array<std::uint32_t, 17> CallAfterEnd{0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbb080003u, 0xe0781000u, 0x80010401u, 0xbf810000u, 0x4a0808ffu, 0x13579bdfu, 0x3a0a0affu, 0xa5a5a5a5u, 0x4a0c0f06u, 0x3a0e0effu, 0x5a5a5a5au, 0xbe802008u, 0xbf810000u};
alignas(256) constexpr std::array<std::uint32_t, 24> MixedNested{0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbe8a1f00u, 0x800aff0au, 0x00000014u, 0x820b800bu, 0xbe88210au, 0xbf82000bu, 0x4a0808ffu, 0x13579bdfu, 0xbb0c0001u, 0xbf820006u, 0x3a0a0affu, 0xa5a5a5a5u, 0x4a0c0f06u, 0x3a0e0effu, 0x5a5a5a5au, 0xbe80200cu, 0xbe802008u, 0xe0781000u, 0x80010401u, 0xbf810000u};
constexpr std::array<std::uint32_t, 8> Selectors{0u, 1u, 2u, 3u, 4u, 7u, 0x80000000u, 0xffffffffu};
constexpr const char* Names[Results] = {"add", "xor", "sum", "xor_second"};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}
std::uint32_t Expected(std::uint32_t lane, std::uint32_t result) {
const auto selector = Input[lane * Inputs];
const auto value = Input[lane * Inputs + 1u];
switch (result) {
case 0: return selector + 0x13579bdfu;
case 1: return value ^ 0xa5a5a5a5u;
case 2: return Input[lane * Inputs + 2u] + Input[lane * Inputs + 3u];
default: return Input[lane * Inputs + 3u] ^ 0x5a5a5a5au;
}
}
ShaderRecompiler::RecompileResult Compile(std::span<const std::uint32_t> code, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target) {
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{waveSize, 1, 1}, 0u, {false, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory},
target,
{0, 0, 0, 128}
};
request.useCache = false;
return ShaderRecompiler::Recompile(request);
}
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target) {
for (std::uint32_t tid = 0; tid < MaxThreads; ++tid) {
Input[tid * Inputs] = Selectors[tid % Selectors.size()];
Input[tid * Inputs + 1u] = 0x9e3779b9u * (tid + 1u);
Input[tid * Inputs + 2u] = 0xffffffffu - tid;
Input[tid * Inputs + 3u] = 17u * tid + 1u;
}
Output.fill(0xdeadbeefu);
const auto result = Compile(code, waveSize, target);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
}
void Check(const char* variant, std::uint32_t lanes) {
for (std::uint32_t tid = 0; tid < lanes; ++tid) {
for (std::uint32_t index = 0; index < Results; ++index) {
const auto actual = Output[tid * Results + index];
const auto expected = Expected(tid, index);
Require(actual == expected, std::string(variant) + " wave" + std::to_string(lanes) + ": lane " + std::to_string(tid) + " " + Names[index] + " is " + Hex(actual) + ", expected " + Hex(expected));
}
}
for (std::uint32_t index = lanes * Results; index < Output.size(); ++index) {
Require(Output[index] == 0xdeadbeefu, std::string(variant) + ": inactive lane wrote output");
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
const std::array<std::span<const std::uint32_t>, 5> variants{SwappcForward, CallForward, CallBackward, CallAfterEnd, MixedNested};
const std::array<const char*, 5> names{"SWAPPC forward", "CALL forward", "CALL backward", "CALL after endpgm", "mixed nested calls"};
for (std::size_t index = 0; index < variants.size(); ++index) {
const auto code = variants[index];
Run(*device, code, 32, device->ComputeTarget(32));
Check(names[index], 32);
Run(*device, code, 64, device->Target());
Check(names[index], 64);
Run(*device, code, 64, device->ComputeTarget(32));
Check(names[index], 64);
std::printf("passed %s: 160 lanes, 640 output comparisons\n", names[index]);
}
std::puts("static call execution passed: 5 variants, 800 lanes, 3200 output comparisons");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -4,70 +4,148 @@
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
#include "ControlFlow/GraphBuilder.hpp"
#include <array>
#include <cstdio>
#include <initializer_list>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
using namespace ShaderRecompiler;
static void Require(bool value) { if (!value) throw std::runtime_error("scalar call regression"); }
static void CheckDecodeAndBranch() {
const std::array<std::uint32_t, 1> code{0xbb040008u};
const RdnaInstruction instruction = DecodeRdnaSopk(0u, code, 0u);
static void Require(bool value) {
if (!value) throw std::runtime_error("scalar call regression");
}
template <typename TFunction>
static void ExpectThrow(const char* needle, TFunction fn) {
try {
fn();
} catch (const std::exception& error) {
Require(std::string(error.what()).find(needle) != std::string::npos);
return;
}
throw std::runtime_error(std::string("expected throw containing ") + needle);
}
static RdnaInstruction Call(std::uint32_t pc, std::uint32_t link, std::uint32_t immediate) {
const std::array<std::uint32_t, 1> code{0xbb000000u | (link << 16u) | (immediate & 0xffffu)};
return DecodeRdnaSopk(pc, code, 0u);
}
static RdnaInstruction Sop1(std::uint32_t pc, std::uint32_t destination, std::uint32_t op, std::uint32_t source) {
const std::array<std::uint32_t, 1> code{0xbe800000u | (destination << 16u) | (op << 8u) | source};
return DecodeRdnaSop1(pc, code, 0u);
}
static RdnaInstruction Sopp(std::uint32_t pc, std::uint32_t op, std::uint32_t immediate = 0u) {
const std::array<std::uint32_t, 1> code{0xbf800000u | (op << 16u) | immediate};
return DecodeRdnaSopp(pc, code, 0u);
}
static ControlFlowGraph Build(std::initializer_list<RdnaInstruction> instructions) {
RdnaProgram program;
program.instructions.assign(instructions);
return GraphBuilder{}.Build(program);
}
static void CheckDecode() {
const auto instruction = Call(0x40u, 8u, 7u);
Require(instruction.op == RdnaOpcode::SCallB64);
Require(instruction.family == RdnaInstructionFamily::SOPK);
Require(instruction.destination.kind == RdnaOperandKind::ScalarRegister);
Require(instruction.destination.reg == 4u);
Require(instruction.dataDwordCount == 2u);
Require(instruction.branchTarget == 36u);
Require(instruction.destination.kind == RdnaOperandKind::ScalarRegister && instruction.destination.reg == 8u);
Require(instruction.dataDwordCount == 2u && instruction.wordCount == 1u);
Require(instruction.branchTarget == 0x60u);
Require(Call(0x40u, 8u, 0xfff9u).branchTarget == 0x28u);
Require(Call(0x20000u, 8u, 0x8000u).branchTarget == 4u);
Require(Call(0u, 8u, 0x7fffu).branchTarget == 0x20000u);
}
static void CheckNegativeOffset() {
const std::uint32_t word = 0xb8000000u | (0x16u << 23u) | (2u << 16u) | static_cast<std::uint16_t>(-4);
const std::array<std::uint32_t, 1> code{word};
const RdnaInstruction instruction = DecodeRdnaSopk(16u, code, 0u);
Require(instruction.op == RdnaOpcode::SCallB64);
Require(instruction.destination.reg == 2u);
Require(instruction.branchTarget == 4u);
}
static void CheckTranslationThrows() {
const std::array<std::uint32_t, 1> code{0xbb040008u};
const RdnaInstruction instruction = DecodeRdnaSopk(0u, code, 0u);
static std::string TranslateDump(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);
bool threw = false;
try {
context.TranslateInstruction(instruction);
} catch (const std::runtime_error&) {
threw = true;
}
Require(threw);
context.TranslateInstruction(instruction);
return ProgramToString(program);
}
static void CheckProgramGraphBuilderAndTranslationThrows() {
const std::vector<std::uint32_t> code{0xbb040001u, 0xbf810000u, 0xbf810000u};
static void CheckLink() {
const auto dump = TranslateDump(Call(0x50u, 8u, 2u));
Require(dump.find("0x0000000000000054") != std::string::npos);
Require(dump == TranslateDump(Sop1(0x50u, 8u, 0x1fu, 0u)));
}
static void CheckCallAfterEnd() {
const std::array<std::uint32_t, 5> code{0xbb080001u, 0xbf810000u, 0xbf800000u, 0xbe802008u, 0xbf810000u};
const auto decoded = RdnaInstructionDecoder{}.Decode(code);
Require(decoded.instructions.size() == code.size());
const auto cfg = GraphBuilder{}.Build(decoded);
const auto& caller = cfg.FindBlockByProgramCounter(0u);
const auto& callee = cfg.FindBlockByProgramCounter(8u);
Require(caller.terminator.kind == TerminatorKind::Branch && caller.terminator.trueBlock == callee.id);
Require(callee.terminator.kind == TerminatorKind::Branch && callee.terminator.trueBlock == cfg.FindBlockByProgramCounter(4u).id);
ShaderComputeInputInfo computeInfo{};
TranslateOptions options{};
options.stage = ShaderStageKind::Compute;
options.inputInfo.compute = &computeInfo;
bool threw = false;
try {
static_cast<void>(InstructionTranslator{}.Translate(decoded, cfg, options));
} catch (const std::runtime_error& err) {
threw = std::string(err.what()).find("s_call_b64 at pc 0 is not implemented") != std::string::npos;
static_cast<void>(InstructionTranslator{}.Translate(decoded, cfg, options));
}
static void CheckRejections() {
ExpectThrow("invalid instruction boundary", [] {
Build({Call(0u, 8u, 16u), Sopp(4u, 1u)});
});
ExpectThrow("invalid instruction boundary", [] {
const std::array<std::uint32_t, 7> code{0xbb080002u, 0xbf810000u, 0xbe8003ffu, 0u, 0xbe802008u, 0xbf810000u, 0xbf810000u};
static_cast<void>(GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(code)));
});
ExpectThrow("code span bounds", [] {
const std::array<std::uint32_t, 2> outside{0xbb080010u, 0xbf810000u};
static_cast<void>(RdnaInstructionDecoder{}.Decode(outside));
});
ExpectThrow("code span bounds", [] {
const std::array<std::uint32_t, 2> outside{0xbb08fffeu, 0xbf810000u};
static_cast<void>(RdnaInstructionDecoder{}.Decode(outside));
});
ExpectThrow("no paired s_setpc_b64 return", [] {
Build({Call(0u, 8u, 0u), Sopp(4u, 1u)});
});
for (const auto link : {9u, 105u, 106u, 125u}) {
ExpectThrow("ordinary aligned scalar register pair", [=] {
Build({Call(0u, link, 0u), Sopp(4u, 1u)});
});
}
Require(threw);
ExpectThrow("escapes the constant-offset call/return model", [] {
Build({Call(0u, 8u, 1u), Sopp(4u, 1u), Sop1(8u, 8u, 3u, 128u), Sop1(12u, 0u, 0x20u, 8u)});
});
ExpectThrow("recursive scalar call", [] {
Build({Call(0u, 8u, 0xffffu), Sop1(4u, 0u, 0x20u, 8u), Sopp(8u, 1u)});
});
ExpectThrow("is shared with another call", [] {
Build({Call(0u, 8u, 2u), Call(4u, 8u, 1u), Sopp(8u, 1u), Sop1(12u, 0u, 0x20u, 8u)});
});
ExpectThrow("multiple s_setpc_b64 returns", [] {
Build({Call(0u, 8u, 1u), Sopp(4u, 1u), Sop1(8u, 0u, 0x20u, 8u), Sop1(12u, 0u, 0x20u, 8u)});
});
ExpectThrow("control flow enters the call region", [] {
Build({Call(0u, 8u, 2u), Sopp(4u, 2u, 1u), Sopp(8u, 1u), Sopp(12u, 0u), Sop1(16u, 0u, 0x20u, 8u)});
});
ExpectThrow("control flow leaves the call region", [] {
Build({Call(0u, 8u, 1u), Sopp(4u, 1u), Sopp(8u, 2u, 2u), Sop1(12u, 0u, 0x20u, 8u), Sopp(16u, 1u), Sopp(20u, 1u)});
});
}
int main() {
CheckDecodeAndBranch();
CheckNegativeOffset();
CheckTranslationThrows();
CheckProgramGraphBuilderAndTranslationThrows();
try {
CheckDecode();
CheckLink();
CheckCallAfterEnd();
CheckRejections();
} catch (const std::exception& error) {
std::fprintf(stderr, "scalar call regression: %s\n", error.what());
return 1;
}
return 0;
}

View File

@ -178,7 +178,7 @@ static void CheckThrows() {
Sop1(0x24u, 0u, 0x20u, 8u),
}));
});
ExpectThrow("recursive s_swappc_b64 call", [] {
ExpectThrow("recursive scalar call", [] {
Build(Program({
Sop1(0x00u, 4u, 0x1fu, 0u),
Sop2Literal(0x04u, 4u, 0x00u, 4u, 0x10u),

View File

@ -440,25 +440,28 @@ std::vector<SwappcCall> analyzeSwappcCalls(const RdnaProgram& program, const Swa
std::vector<SwappcCall> calls;
for (std::uint32_t index = 0; index < program.instructions.size(); ++index) {
const auto& instruction = program.instructions[index];
if (instruction.op != RdnaOpcode::SSwappcB64) {
if (instruction.op != RdnaOpcode::SSwappcB64 && instruction.op != RdnaOpcode::SCallB64) {
continue;
}
const std::uint32_t linkRegister = scalarIndex(instruction.destination);
if (linkRegister == NoScalarRegister) {
throw std::invalid_argument("unsupported s_swappc_b64 link register at program counter " + toHexString(instruction.programCounter) + ": the link must be a scalar register pair");
if (linkRegister == NoScalarRegister || linkRegister % 2u != 0u || linkRegister > 104u) {
throw std::invalid_argument("unsupported scalar call link register at program counter " + toHexString(instruction.programCounter) + ": the link must be an ordinary aligned scalar register pair");
}
SwappcCall call;
call.callIndex = index;
call.linkRegister = linkRegister;
for (const auto& other : calls) {
if (call.linkRegister <= other.linkRegister + 1u && other.linkRegister <= call.linkRegister + 1u) {
throw std::invalid_argument("unclosable s_swappc_b64 call/return pairing at program counter " + toHexString(instruction.programCounter) +
throw std::invalid_argument("unclosable scalar call/return pairing at program counter " + toHexString(instruction.programCounter) +
": link register s[" + std::to_string(call.linkRegister) + "] is shared with another call");
}
}
std::uint32_t target = 0;
const bool staticTarget = resolveSetpcTarget(program, index, target);
std::uint32_t target = instruction.branchTarget;
const bool staticTarget = instruction.op == RdnaOpcode::SCallB64 || resolveSetpcTarget(program, index, target);
call.targetIndex = staticTarget ? instructionIndexOfProgramCounter(program, target) : InvalidControlFlowId;
if (instruction.op == RdnaOpcode::SCallB64 && call.targetIndex == InvalidControlFlowId) {
throw std::invalid_argument("s_call_b64 at program counter " + toHexString(instruction.programCounter) + " targets invalid instruction boundary " + toHexString(target));
}
const bool positional = std::all_of(program.instructions.begin(), program.instructions.begin() + index, isFetchCallPrefixOpcode);
const std::uint32_t targetRegister = scalarIndex(instruction.source0);
const bool userDataPair = swappc != nullptr && targetRegister != NoScalarRegister &&
@ -467,7 +470,7 @@ std::vector<SwappcCall> analyzeSwappcCalls(const RdnaProgram& program, const Swa
return writesScalar(other, targetRegister) || writesScalar(other, targetRegister + 1u);
});
const bool outsideProgram = staticTarget && call.targetIndex == InvalidControlFlowId;
if (swappc != nullptr && swappc->fetchCallAllowed && positional && (userDataPair || outsideProgram)) {
if (instruction.op == RdnaOpcode::SSwappcB64 && swappc != nullptr && swappc->fetchCallAllowed && positional && (userDataPair || outsideProgram)) {
call.fetch = true;
call.returnTargetProgramCounter = instructionEndProgramCounter(instruction);
calls.push_back(call);
@ -489,14 +492,14 @@ std::vector<SwappcCall> analyzeSwappcCalls(const RdnaProgram& program, const Swa
const auto& instruction = program.instructions[index];
if (instruction.op == RdnaOpcode::SSetpcB64 && pairOverlapsRegister(instruction.source0, call.linkRegister)) {
if (returnIndex != InvalidControlFlowId) {
throw std::invalid_argument("unclosable s_swappc_b64 call/return pairing at program counter " + toHexString(program.instructions[call.callIndex].programCounter) +
throw std::invalid_argument("unclosable scalar call/return pairing at program counter " + toHexString(program.instructions[call.callIndex].programCounter) +
": multiple s_setpc_b64 returns read link register s[" + std::to_string(call.linkRegister) + "]");
}
returnIndex = index;
continue;
}
if (touchesLinkRegister(instruction, call.linkRegister)) {
throw std::invalid_argument("s_swappc_b64 link register s[" + std::to_string(call.linkRegister) + "] written at program counter " +
throw std::invalid_argument("scalar call link register s[" + std::to_string(call.linkRegister) + "] written at program counter " +
toHexString(program.instructions[call.callIndex].programCounter) + " escapes the constant-offset call/return model (used at program counter " +
toHexString(instruction.programCounter) + ")");
}
@ -509,7 +512,7 @@ std::vector<SwappcCall> analyzeSwappcCalls(const RdnaProgram& program, const Swa
continue;
}
if (returnIndex == InvalidControlFlowId) {
throw std::invalid_argument("unclosable s_swappc_b64 call/return pairing at program counter " + toHexString(program.instructions[call.callIndex].programCounter) +
throw std::invalid_argument("unclosable scalar call/return pairing at program counter " + toHexString(program.instructions[call.callIndex].programCounter) +
": no paired s_setpc_b64 return reads link register s[" + std::to_string(call.linkRegister) + "]");
}
call.returnIndex = returnIndex;
@ -518,7 +521,7 @@ std::vector<SwappcCall> analyzeSwappcCalls(const RdnaProgram& program, const Swa
for (std::size_t first = 0; first < calls.size(); ++first) {
if (!calls[first].fetch && calls[first].callIndex >= calls[first].targetIndex && calls[first].callIndex <= calls[first].returnIndex) {
throw std::invalid_argument("recursive s_swappc_b64 call at program counter " + toHexString(program.instructions[calls[first].callIndex].programCounter) +
throw std::invalid_argument("recursive scalar call at program counter " + toHexString(program.instructions[calls[first].callIndex].programCounter) +
": the call executes inside its own call region");
}
for (std::size_t second = first + 1u; second < calls.size(); ++second) {
@ -529,7 +532,7 @@ std::vector<SwappcCall> analyzeSwappcCalls(const RdnaProgram& program, const Swa
const bool firstContainsSecond = calls[first].targetIndex <= calls[second].targetIndex && calls[second].returnIndex <= calls[first].returnIndex;
const bool secondContainsFirst = calls[second].targetIndex <= calls[first].targetIndex && calls[first].returnIndex <= calls[second].returnIndex;
if (!disjoint && !firstContainsSecond && !secondContainsFirst) {
throw std::invalid_argument("recursive s_swappc_b64 calls at program counter " + toHexString(program.instructions[calls[first].callIndex].programCounter) +
throw std::invalid_argument("recursive scalar calls at program counter " + toHexString(program.instructions[calls[first].callIndex].programCounter) +
" and program counter " + toHexString(program.instructions[calls[second].callIndex].programCounter) + ": call regions interleave");
}
}
@ -554,13 +557,13 @@ std::vector<SwappcCall> analyzeSwappcCalls(const RdnaProgram& program, const Swa
}
std::uint32_t target = 0;
if (!resolveSetpcTarget(program, index, target)) {
throw std::invalid_argument("unclosable s_swappc_b64 call region in a program with calls: dynamic s_setpc_b64 at program counter " +
throw std::invalid_argument("unclosable scalar call region in a program with calls: dynamic s_setpc_b64 at program counter " +
toHexString(instruction.programCounter) + " cannot be confined to a call region");
}
transfers.push_back(Transfer{index, instructionIndexOfProgramCounter(program, target)});
continue;
}
if (instruction.op == RdnaOpcode::SSwappcB64) {
if (instruction.op == RdnaOpcode::SSwappcB64 || instruction.op == RdnaOpcode::SCallB64) {
const SwappcCall* call = findCallAt(calls, index);
transfers.push_back(Transfer{index, call->targetIndex});
}
@ -573,12 +576,12 @@ std::vector<SwappcCall> analyzeSwappcCalls(const RdnaProgram& program, const Swa
const bool sourceInside = transfer.index >= call.targetIndex && transfer.index <= call.returnIndex;
const bool targetInside = transfer.targetIndex != InvalidControlFlowId && transfer.targetIndex >= call.targetIndex && transfer.targetIndex <= call.returnIndex;
if (!sourceInside && targetInside && transfer.index != call.callIndex) {
throw std::invalid_argument("unclosable s_swappc_b64 call/return pairing at program counter " +
throw std::invalid_argument("unclosable scalar call/return pairing at program counter " +
toHexString(program.instructions[call.callIndex].programCounter) + ": control flow enters the call region from program counter " +
toHexString(program.instructions[transfer.index].programCounter));
}
if (sourceInside && !targetInside && transfer.index != call.returnIndex) {
throw std::invalid_argument("unclosable s_swappc_b64 call/return pairing at program counter " +
throw std::invalid_argument("unclosable scalar call/return pairing at program counter " +
toHexString(program.instructions[call.callIndex].programCounter) + ": control flow leaves the call region at program counter " +
toHexString(program.instructions[transfer.index].programCounter));
}
@ -718,7 +721,7 @@ std::vector<BasicBlock> GraphBuilder::splitIntoBlocks(const RdnaProgram& program
if (nextProgramCounter <= endProgramCounter) {
labels.insert(nextProgramCounter);
}
} else if (instruction.op == RdnaOpcode::SSwappcB64) {
} else if (instruction.op == RdnaOpcode::SSwappcB64 || instruction.op == RdnaOpcode::SCallB64) {
const SwappcCall* call = findCallAt(calls, index);
if (call != nullptr && !call->fetch) {
labels.insert(call->targetProgramCounter);

View File

@ -160,7 +160,7 @@ void DecodeRdnaProgram(std::span<const std::uint32_t> code, RdnaProgram& program
const RdnaInstruction& instruction = program.instructions.back();
wordIndex += instruction.wordCount;
if (IsDirectBranchOpcode(instruction.op)) {
if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64) {
const std::uint32_t targetIndex = instruction.branchTarget / static_cast<std::uint32_t>(sizeof(std::uint32_t));
if (targetIndex >= code.size()) {
throw std::out_of_range("branch target is out of the code span bounds");

View File

@ -29,9 +29,8 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) {
return true;
case RdnaOpcode::SSetpcB64:
return true;
case RdnaOpcode::SCallB64:
throw std::runtime_error("s_call_b64 at pc " + std::to_string(inst.programCounter) + " is not implemented");
case RdnaOpcode::SSwappcB64:
case RdnaOpcode::SCallB64:
sSwappcB64(inst);
return true;
case RdnaOpcode::SSubvectorLoopBegin:

View File

@ -334,9 +334,10 @@ 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`, keeping the existing decode quirk.
- 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.
- A [static s_swappc_b64 call region](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) requires exactly one call per link pair and exactly one `s_setpc_b64` return reading it; the return transfers to the call's `pc + 4` block through the existing `s_setpc_b64` path. Call regions must nest or be disjoint and cannot be entered or left by any other control transfer; recursion, interleaved regions, multiple or no returns, and a dynamic `s_setpc_b64` (including a jump table) in a program that also has `s_swappc_b64` all throw.
- [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
- [NP reachability callbacks](../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable.
- [Temporary app-content storage](../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw.
- [Guest path resolution](../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied.
- [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require one call per ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and one `s_setpc_b64` return. Regions must nest or be disjoint; recursion, shared links, escaping link values, invalid targets, interleaved regions, cross-region branches and dynamic `s_setpc_b64` in a program with calls throw. Arbitrary indirect calls and general call frames are not implemented.
- [s_call_b64](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Native RDNA2/PS5 execution and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation.