diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index 232d6e151..14e020f13 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 000000000..b5418e50a --- /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 bbc7fe30a..a272659a4 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); +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 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.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(-4); - const std::array 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 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 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 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 b7c173860..6ab8dc4f0 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 2d60b3452..2c5eab0c6 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 9181fc4fb..f03944673 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 c861def8c..7136560e7 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 25b90199f..bbdc6a531 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.