From ee0b1109f4daa988ecfd906bd0ca904bdd73129f Mon Sep 17 00:00:00 2001 From: Sanjaysrinivas Date: Thu, 8 Oct 2026 13:14:06 +0200 Subject: [PATCH 1/3] 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. --- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 2 +- .../tests/execution/ScalarStaticCall.cpp | 124 ++++++++++++++ core/libs/tests/ScalarCall.cpp | 156 +++++++++++++----- core/libs/tests/ScalarSwappc.cpp | 2 +- .../ControlFlow/src/GraphBuilder.cpp | 37 +++-- .../src/RdnaInstructionDecoder.cpp | 2 +- .../Translation/src/ScalarInstructions.cpp | 3 +- docs/dev/TechnicalDebt.md | 3 +- 8 files changed, 267 insertions(+), 62 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ScalarStaticCall.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index 232d6e15108..14e020f130e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -285,7 +285,7 @@ void verifyNullSwappc() { verifyLongBranch("backward long branch", LongBranchBack, 0x24u, 0x08u); const std::array call{0xbe8c1f00u, 0xbe8e210cu, 0xbf810000u}; const std::vector, 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"}, diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarStaticCall.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarStaticCall.cpp new file mode 100644 index 00000000000..b5418e50a8c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarStaticCall.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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 Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array 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 CallForward{0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbb080001u, 0xbf820008u, 0x4a0808ffu, 0x13579bdfu, 0x3a0a0affu, 0xa5a5a5a5u, 0x4a0c0f06u, 0x3a0e0effu, 0x5a5a5a5au, 0xbe802008u, 0xe0781000u, 0x80010401u, 0xbf810000u}; +alignas(256) constexpr std::array CallBackward{0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbf820008u, 0x4a0808ffu, 0x13579bdfu, 0x3a0a0affu, 0xa5a5a5a5u, 0x4a0c0f06u, 0x3a0e0effu, 0x5a5a5a5au, 0xbe802008u, 0xbb08fff7u, 0xbf820000u, 0xe0781000u, 0x80010401u, 0xbf810000u}; +alignas(256) constexpr std::array CallAfterEnd{0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbb080003u, 0xe0781000u, 0x80010401u, 0xbf810000u, 0x4a0808ffu, 0x13579bdfu, 0x3a0a0affu, 0xa5a5a5a5u, 0x4a0c0f06u, 0x3a0e0effu, 0x5a5a5a5au, 0xbe802008u, 0xbf810000u}; +alignas(256) constexpr std::array 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 Selectors{0u, 1u, 2u, 3u, 4u, 7u, 0x80000000u, 0xffffffffu}; +constexpr const char* Names[Results] = {"add", "xor", "sum", "xor_second"}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((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 code, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target) { + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(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(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 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(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, 5> variants{SwappcForward, CallForward, CallBackward, CallAfterEnd, MixedNested}; + const std::array 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; + } +} diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index bbc7fe30a21..a272659a45b 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -4,70 +4,148 @@ #include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "ControlFlow/GraphBuilder.hpp" #include +#include +#include +#include #include #include -#include using namespace ShaderRecompiler; -static void Require(bool value) { if (!value) throw std::runtime_error("scalar call regression"); } -static void CheckDecodeAndBranch() { - const std::array code{0xbb040008u}; - const RdnaInstruction instruction = DecodeRdnaSopk(0u, code, 0u); - 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); +static void Require(bool value) { + if (!value) throw std::runtime_error("scalar call regression"); +} + +template +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 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 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 code{0xbf800000u | (op << 16u) | immediate}; + return DecodeRdnaSopp(pc, code, 0u); } -static void CheckNegativeOffset() { - const std::uint32_t word = 0xb8000000u | (0x16u << 23u) | (2u << 16u) | static_cast(-4); - const std::array code{word}; - const RdnaInstruction instruction = DecodeRdnaSopk(16u, code, 0u); +static ControlFlowGraph Build(std::initializer_list 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.destination.reg == 2u); - Require(instruction.branchTarget == 4u); + Require(instruction.family == RdnaInstructionFamily::SOPK); + 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 CheckTranslationThrows() { - const std::array 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 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 CheckProgramGraphBuilderAndTranslationThrows() { - const std::vector code{0xbb040001u, 0xbf810000u, 0xbf810000u}; +static void CheckCallAfterEnd() { + const std::array 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(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(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 code{0xbb080002u, 0xbf810000u, 0xbe8003ffu, 0u, 0xbe802008u, 0xbf810000u, 0xbf810000u}; + static_cast(GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(code))); + }); + ExpectThrow("code span bounds", [] { + const std::array outside{0xbb080010u, 0xbf810000u}; + static_cast(RdnaInstructionDecoder{}.Decode(outside)); + }); + ExpectThrow("code span bounds", [] { + const std::array outside{0xbb08fffeu, 0xbf810000u}; + static_cast(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; } diff --git a/core/libs/tests/ScalarSwappc.cpp b/core/libs/tests/ScalarSwappc.cpp index b7c17386040..6ab8dc4f051 100644 --- a/core/libs/tests/ScalarSwappc.cpp +++ b/core/libs/tests/ScalarSwappc.cpp @@ -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), diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 2d60b3452f9..2c5eab0c666 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -440,25 +440,28 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa std::vector 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 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 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 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 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 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 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 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 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); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp index 9181fc4fb83..f0394467365 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp @@ -160,7 +160,7 @@ void DecodeRdnaProgram(std::span 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(sizeof(std::uint32_t)); if (targetIndex >= code.size()) { throw std::out_of_range("branch target is out of the code span bounds"); diff --git a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp index c861def8cee..7136560e738 100644 --- a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp @@ -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: diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 25b90199fd4..bbdc6a53170 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -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. From 4d2917e63247681225635a59119140012fb85bf0 Mon Sep 17 00:00:00 2001 From: Sanjaysrinivas Date: Thu, 8 Oct 2026 13:20:32 +0200 Subject: [PATCH 2/3] fix(shader): guard scalar call link lifetimes Reject tuple and descriptor overlap, wrong-pair returns, relative SGPR reads, reversed regions and ordinary or conditional fall-through into a callee. Preserve the existing scalar write analysis and require exact return-pair matching. Seven targeted regressions fail before these guards and pass afterward. Seven focused CMake tests and 15 SPIR-V validations pass; the execution matrix passes 3,200 comparisons and detects a one-bit expected-output mutation. --- core/libs/tests/ScalarCall.cpp | 68 +++++++++++++++++-- .../ControlFlow/src/GraphBuilder.cpp | 48 +++++++++++-- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 104 insertions(+), 14 deletions(-) diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index a272659a45b..3058f657724 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -2,6 +2,7 @@ #include "Translation/InstructionTranslator.hpp" #include "RdnaDecoder/RdnaScalarOpDecoder.hpp" #include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "RdnaDecoder/RdnaMemoryOpDecoder.hpp" #include "ControlFlow/GraphBuilder.hpp" #include #include @@ -21,7 +22,9 @@ 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); + if (std::string(error.what()).find(needle) == std::string::npos) { + throw std::runtime_error(std::string("expected ") + needle + ", got " + error.what()); + } return; } throw std::runtime_error(std::string("expected throw containing ") + needle); @@ -137,12 +140,65 @@ static void CheckRejections() { }); } -int main() { +static void CheckGuards(const std::string& only) { + bool checked = false; + const auto check = [&](const char* name, const char* needle, auto fn) { + if (only.empty() || only == name) { + checked = true; + ExpectThrow(needle, fn); + } + }; + check("tuple", "escapes the constant-offset call/return model", [] { + auto load = Sopp(8u, 0u); + load.family = RdnaInstructionFamily::SMEM; + load.op = RdnaOpcode::SLoadDwordx8; + load.dataDwordCount = 8u; + load.destination.kind = RdnaOperandKind::ScalarRegister; + load.destination.reg = 4u; + Build({Call(0u, 8u, 1u), Sopp(4u, 1u), load, Sop1(12u, 0u, 0x20u, 8u)}); + }); + check("wrong_return", "escapes the constant-offset call/return model", [] { + Build({Call(0u, 8u, 1u), Sopp(4u, 1u), Sop1(8u, 0u, 0x20u, 9u)}); + }); + check("fallthrough", "control flow enters the call region", [] { + Build({Call(0u, 8u, 2u), Sopp(4u, 0u), Sopp(8u, 0u), Sopp(12u, 0u), Sop1(16u, 0u, 0x20u, 8u)}); + }); + check("conditional_fallthrough", "control flow enters the call region", [] { + Build({Call(0u, 8u, 2u), Sopp(4u, 2u), Sopp(8u, 4u, 2u), Sopp(12u, 0u), Sop1(16u, 0u, 0x20u, 8u), Sopp(20u, 1u)}); + }); + check("return_before_target", "return precedes the call target", [] { + Build({Call(0u, 8u, 2u), Sopp(4u, 1u), Sop1(8u, 0u, 0x20u, 8u), Sopp(12u, 0u), Sopp(16u, 1u)}); + }); + check("descriptor", "escapes the constant-offset call/return model", [] { + const std::array code{0xe0381000u, 0x80020401u}; + const auto load = DecodeRdnaMubuf(8u, code, 0u); + Require(load.source1.reg == 8u); + Build({Call(0u, 10u, 1u), Sopp(4u, 1u), load, Sop1(16u, 0u, 0x20u, 10u)}); + }); + check("relative_read", "escapes the constant-offset call/return model", [] { + Build({Call(0u, 8u, 1u), Sopp(4u, 1u), Sop1(8u, 0u, 0x2eu, 0u), Sop1(12u, 0u, 0x20u, 8u)}); + }); + if (only.empty()) { + for (const auto link : {0u, 104u}) { + const auto cfg = Build({Call(0u, link, 1u), Sopp(4u, 1u), Sop1(8u, 0u, 0x20u, link)}); + Require(cfg.FindBlockByProgramCounter(0u).terminator.kind == TerminatorKind::Branch); + } + } + Require(checked); +} + +int main(int argc, char** argv) { try { - CheckDecode(); - CheckLink(); - CheckCallAfterEnd(); - CheckRejections(); + if (argc == 1) { + CheckDecode(); + CheckLink(); + CheckCallAfterEnd(); + CheckRejections(); + CheckGuards(""); + } else { + Require(argc == 2); + CheckGuards(argv[1]); + } } catch (const std::exception& error) { std::fprintf(stderr, "scalar call regression: %s\n", error.what()); return 1; diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 2c5eab0c666..1bd3c7776f7 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -392,15 +392,42 @@ bool resolveBoundedJumpTable(const RdnaProgram& program, std::uint32_t index, Bo return resolveDwordJumpTable(program, index, result); } -bool pairOverlapsRegister(const RdnaOperand& operand, std::uint32_t linkRegister) { +bool pairOverlapsRegister(const RdnaOperand& operand, std::uint32_t linkRegister, std::uint32_t count = 1u) { const std::uint32_t slot = scalarIndex(operand); - return slot != NoScalarRegister && (slot == linkRegister || slot == linkRegister + 1u); + return slot != NoScalarRegister && slot <= linkRegister + 1u && linkRegister < slot + count; } bool touchesLinkRegister(const RdnaInstruction& instruction, std::uint32_t linkRegister) { - return pairOverlapsRegister(instruction.destination, linkRegister) || pairOverlapsRegister(instruction.destination2, linkRegister) || - pairOverlapsRegister(instruction.source0, linkRegister) || pairOverlapsRegister(instruction.source1, linkRegister) || - pairOverlapsRegister(instruction.source2, linkRegister) || pairOverlapsRegister(instruction.source3, linkRegister); + if (instruction.op == RdnaOpcode::SMovrelsB32 || instruction.op == RdnaOpcode::SMovrelsB64) return true; + std::uint32_t source0Count = 1u, source1Count = 1u, source2Count = 1u; + switch (instruction.family) { + case RdnaInstructionFamily::SMEM: + source0Count = 2u; + switch (instruction.op) { + case RdnaOpcode::SBufferLoadDword: + case RdnaOpcode::SBufferLoadDwordx2: + case RdnaOpcode::SBufferLoadDwordx4: + case RdnaOpcode::SBufferLoadDwordx8: + case RdnaOpcode::SBufferLoadDwordx16: source0Count = 4u; break; + default: break; + } + break; + case RdnaInstructionFamily::MUBUF: + case RdnaInstructionFamily::MTBUF: source1Count = 4u; break; + case RdnaInstructionFamily::MIMG: + source1Count = instruction.imageR128 ? 4u : 8u; + source2Count = 4u; + break; + case RdnaInstructionFamily::FLAT: + source0Count = 2u; + source1Count = instruction.memorySegment == 2u ? 2u : 1u; + break; + default: break; + } + return writesScalar(instruction, linkRegister) || writesScalar(instruction, linkRegister + 1u) || + pairOverlapsRegister(instruction.destination, linkRegister) || pairOverlapsRegister(instruction.destination2, linkRegister) || + pairOverlapsRegister(instruction.source0, linkRegister, source0Count) || pairOverlapsRegister(instruction.source1, linkRegister, source1Count) || + pairOverlapsRegister(instruction.source2, linkRegister, source2Count) || pairOverlapsRegister(instruction.source3, linkRegister); } bool isFetchCallPrefixOpcode(const RdnaInstruction& instruction) { @@ -490,7 +517,7 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa continue; } const auto& instruction = program.instructions[index]; - if (instruction.op == RdnaOpcode::SSetpcB64 && pairOverlapsRegister(instruction.source0, call.linkRegister)) { + if (instruction.op == RdnaOpcode::SSetpcB64 && isScalar(instruction.source0, call.linkRegister)) { if (returnIndex != InvalidControlFlowId) { 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) + "]"); @@ -515,6 +542,10 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa 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) + "]"); } + if (returnIndex < call.targetIndex) { + throw std::invalid_argument("unclosable scalar call/return pairing: return precedes the call target at program counter " + + toHexString(program.instructions[call.callIndex].programCounter)); + } call.returnIndex = returnIndex; call.returnTargetProgramCounter = instructionEndProgramCounter(program.instructions[call.callIndex]); } @@ -549,6 +580,7 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa const auto& instruction = program.instructions[index]; if (IsDirectBranchOpcode(instruction.op)) { transfers.push_back(Transfer{index, instructionIndexOfProgramCounter(program, instruction.branchTarget)}); + if (IsConditionalBranchOpcode(instruction.op)) transfers.push_back(Transfer{index, index + 1u}); continue; } if (instruction.op == RdnaOpcode::SSetpcB64) { @@ -565,8 +597,10 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa } if (instruction.op == RdnaOpcode::SSwappcB64 || instruction.op == RdnaOpcode::SCallB64) { const SwappcCall* call = findCallAt(calls, index); - transfers.push_back(Transfer{index, call->targetIndex}); + transfers.push_back(Transfer{index, call->fetch ? index + 1u : call->targetIndex}); + continue; } + if (instruction.op != RdnaOpcode::SEndpgm && instruction.op != RdnaOpcode::SCodeEnd) transfers.push_back(Transfer{index, index + 1u}); } for (const auto& call : calls) { if (call.fetch) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index bbdc6a53170..befaeeeca72 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -339,5 +339,5 @@ Throughout the project, every function at every stage either **does exactly what - [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. +- [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 later `s_setpc_b64` return reading that exact pair. Regions must nest or be disjoint; recursion, shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. 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. From 2b2a0c09b4238c188b9487ca9d4ad9fb2e609831 Mon Sep 17 00:00:00 2001 From: Sanjaysrinivas Date: Thu, 8 Oct 2026 13:27:28 +0200 Subject: [PATCH 3/3] test(shader): report effective call targets ComputeTarget(32) can fall back to the default subgroup size. Report both effective sizes and dispatch the third configuration only when it differs, so duplicate runs do not imply wave32 host coverage. Lavapipe exposes subgroup 8 for both requests: seven CMake checks pass, the distinct matrix passes 1,920 comparisons, ten captured modules validate, and the expected-output negative control fails as intended. --- .../tests/execution/ScalarStaticCall.cpp | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarStaticCall.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarStaticCall.cpp index b5418e50a8c..fe8872d9c09 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarStaticCall.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarStaticCall.cpp @@ -103,19 +103,29 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + const auto defaultTarget = device->Target(); + const auto wave32Target = device->ComputeTarget(32); + const bool distinctTarget = defaultTarget.subgroupSize != wave32Target.subgroupSize; + std::printf("static call targets: default subgroup %u, ComputeTarget(32) subgroup %u\n", defaultTarget.subgroupSize, wave32Target.subgroupSize); + std::uint32_t lanes = 0u, dispatches = 0u; const std::array, 5> variants{SwappcForward, CallForward, CallBackward, CallAfterEnd, MixedNested}; const std::array 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)); + Run(*device, code, 32, wave32Target); Check(names[index], 32); - Run(*device, code, 64, device->Target()); + Run(*device, code, 64, defaultTarget); 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]); + if (distinctTarget) { + Run(*device, code, 64, wave32Target); + Check(names[index], 64); + } + const std::uint32_t variantLanes = distinctTarget ? 160u : 96u; + lanes += variantLanes; + dispatches += distinctTarget ? 3u : 2u; + std::printf("passed %s: %u lanes, %u output comparisons\n", names[index], variantLanes, variantLanes * Results); } - std::puts("static call execution passed: 5 variants, 800 lanes, 3200 output comparisons"); + std::printf("static call execution passed: 5 variants, %u dispatches, %u lanes, %u output comparisons\n", dispatches, lanes, lanes * Results); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n';