diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 66bb44a0a..1ad0a76d9 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -931,6 +931,14 @@ if(BUILD_TESTING) set_tests_properties(scalar_saveexec PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(scalar_swappc_tests ${LIBS_INCLUDE_DIR}/tests/ScalarSwappc.cpp) + target_link_libraries(scalar_swappc_tests PRIVATE shader_recompiler_agc_driver libc) + configure_windows_unwind(scalar_swappc_tests) + add_test(NAME scalar_swappc COMMAND scalar_swappc_tests) + if(WIN32) + set_tests_properties(scalar_swappc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(scalar_bitcount64_tests ${LIBS_INCLUDE_DIR}/tests/ScalarBitCount64.cpp) target_link_libraries(scalar_bitcount64_tests PRIVATE shader_recompiler_agc_driver libc) configure_windows_unwind(scalar_bitcount64_tests) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index df874dd91..232d6e151 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()}, "unsupported SOP1 opcode 33"}, + {"s_swappc_b64 with a return address", {call.begin(), call.end()}, "unclosable s_swappc_b64 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/tests/ScalarSwappc.cpp b/core/libs/tests/ScalarSwappc.cpp new file mode 100644 index 000000000..b7c173860 --- /dev/null +++ b/core/libs/tests/ScalarSwappc.cpp @@ -0,0 +1,228 @@ +#include "ControlFlow/GraphBuilder.hpp" +#include "IntermediateRepresentation/IrProgram.hpp" +#include "RdnaDecoder/RdnaScalarOpDecoder.hpp" +#include "Translation/TranslationContext.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; +static void Require(bool value) { if (!value) throw std::runtime_error("scalar swappc regression"); } + +template +static void ExpectThrow(const char* needle, Fn fn) { + try { + fn(); + } catch (const std::exception& error) { + if (std::string(error.what()).find(needle) == std::string::npos) { + throw std::runtime_error(std::string("wrong error for \"") + needle + "\": " + error.what()); + } + return; + } + throw std::runtime_error(std::string("expected throw containing \"") + needle + "\""); +} + +static RdnaInstruction Sop1(std::uint32_t pc, std::uint32_t sdst, std::uint32_t opcode, std::uint32_t ssrc0) { + const std::array words{0x80000000u | (0x7du << 23u) | (sdst << 16u) | (opcode << 8u) | ssrc0, 0u}; + return DecodeRdnaSop1(pc, words, 0u); +} + +static RdnaInstruction Sop2Literal(std::uint32_t pc, std::uint32_t sdst, std::uint32_t opcode, std::uint32_t ssrc0, std::uint32_t literal) { + const std::array words{0x80000000u | (opcode << 23u) | (sdst << 16u) | (0xffu << 8u) | ssrc0, literal}; + return DecodeRdnaSop2(pc, words, 0u); +} + +static RdnaInstruction Sopp(std::uint32_t pc, std::uint32_t opcode) { + const std::array words{0x80000000u | (0x7fu << 23u) | (opcode << 16u)}; + return DecodeRdnaSopp(pc, words, 0u); +} + +static RdnaInstruction Sopk(std::uint32_t pc, std::uint32_t opcode, std::uint32_t sdst, std::uint32_t simm16) { + const std::array words{0x80000000u | ((0x60u + opcode) << 23u) | (sdst << 16u) | (simm16 & 0xffffu)}; + return DecodeRdnaSopk(pc, words, 0u); +} + +static RdnaProgram Program(std::initializer_list instructions) { + RdnaProgram program; + program.instructions.assign(instructions); + return program; +} + +static ControlFlowGraph Build(const RdnaProgram& program, const SwappcInfo* swappc = nullptr) { + return GraphBuilder{}.Build(program, swappc); +} + +static void CheckDecodeIdentity() { + const std::array words{0x80000000u | (0x7du << 23u) | (8u << 16u) | (0x21u << 8u) | 4u, 0u}; + const RdnaInstruction instruction = DecodeRdnaSop1(0x24u, words, 0u); + Require(instruction.op == RdnaOpcode::SSwappcB64); + Require(instruction.family == RdnaInstructionFamily::SOP1); + Require(instruction.programCounter == 0x24u); + Require(instruction.dataDwordCount == 2u); + Require(instruction.sourceCount == 1u); + Require(instruction.source0.kind == RdnaOperandKind::ScalarRegister && instruction.source0.reg == 4u); + Require(instruction.destination.kind == RdnaOperandKind::ScalarRegister && instruction.destination.reg == 8u); + + const std::array pcWords{0x80000000u | (0x7du << 23u) | (125u << 16u) | (0x21u << 8u) | 4u, 0u}; + Require(DecodeRdnaSop1(0u, pcWords, 0u).op == RdnaOpcode::SSetpcB64); + + + const RdnaInstruction add = Sop2Literal(0x4u, 4u, 0x00u, 4u, 0x18u); + Require(add.op == RdnaOpcode::SAddU32); + Require(add.wordCount == 2u); +} + +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); + context.TranslateInstruction(instruction); + return ProgramToString(program); +} + +static void CheckLinkModelsNextInstructionAddress() { + const std::array getpcWords{0x80000000u | (0x7du << 23u) | (2u << 16u) | (0x1fu << 8u), 0u}; + const std::array swappcWords{0x80000000u | (0x7du << 23u) | (2u << 16u) | (0x21u << 8u) | 4u, 0u}; + const RdnaInstruction getpc = DecodeRdnaSop1(0x10u, getpcWords, 0u); + const RdnaInstruction swappc = DecodeRdnaSop1(0x10u, swappcWords, 0u); + Require(!TranslateDump(getpc).empty()); + Require(TranslateDump(getpc) == TranslateDump(swappc)); +} + +static void CheckStaticCallRegion() { + const auto program = Program({ + Sop1(0x00u, 4u, 0x1fu, 0u), + Sop2Literal(0x04u, 4u, 0x00u, 4u, 0x18u), + Sop2Literal(0x0cu, 5u, 0x04u, 5u, 0u), + Sop1(0x14u, 8u, 0x21u, 4u), + Sopp(0x18u, 0x01u), + Sopp(0x1cu, 0x00u), + Sop1(0x20u, 0u, 0x20u, 8u), + }); + const auto cfg = Build(program); + Require(cfg.FindBlockByProgramCounter(0x00u).terminator.kind == TerminatorKind::Branch); + Require(cfg.FindBlockByProgramCounter(0x00u).terminator.trueBlock == cfg.FindBlockByProgramCounter(0x1cu).id); + Require(cfg.FindBlockByProgramCounter(0x1cu).terminator.kind == TerminatorKind::Branch); + Require(cfg.FindBlockByProgramCounter(0x1cu).terminator.trueBlock == cfg.FindBlockByProgramCounter(0x18u).id); +} + +static void CheckFetchCallAtEntry() { + SwappcInfo info; + info.fetchCallAllowed = true; + info.userDataBaseRegister = 16u; + info.userDataCount = 8u; + const auto program = Program({ + Sopp(0x00u, 0x00u), + Sop1(0x04u, 2u, 0x21u, 16u), + Sopp(0x08u, 0x01u), + }); + const auto cfg = Build(program, &info); + Require(cfg.hasFetchCall); + Require(cfg.fetchCallProgramCounter == 0x04u); +} + +static void CheckThrows() { + ExpectThrow("computed/data-dependent s_swappc_b64", [] { + Build(Program({ + Sop1(0x00u, 8u, 0x21u, 4u), + Sopp(0x04u, 0x01u), + })); + }); + ExpectThrow("escapes the constant-offset call/return model", [] { + Build(Program({ + Sop1(0x00u, 4u, 0x1fu, 0u), + Sop2Literal(0x04u, 4u, 0x00u, 4u, 0x18u), + Sop2Literal(0x0cu, 5u, 0x04u, 5u, 0u), + Sop1(0x14u, 8u, 0x21u, 4u), + Sop1(0x18u, 8u, 0x03u, 8u), + Sopp(0x1cu, 0x01u), + Sopp(0x20u, 0x00u), + Sop1(0x24u, 0u, 0x20u, 8u), + })); + }); + ExpectThrow("no paired s_setpc_b64 return", [] { + Build(Program({ + Sop1(0x00u, 4u, 0x1fu, 0u), + Sop2Literal(0x04u, 4u, 0x00u, 4u, 0x18u), + Sop2Literal(0x0cu, 5u, 0x04u, 5u, 0u), + Sop1(0x14u, 8u, 0x21u, 4u), + Sopp(0x18u, 0x01u), + Sopp(0x1cu, 0x00u), + })); + }); + ExpectThrow("multiple s_setpc_b64 returns", [] { + Build(Program({ + Sop1(0x00u, 4u, 0x1fu, 0u), + Sop2Literal(0x04u, 4u, 0x00u, 4u, 0x18u), + Sop2Literal(0x0cu, 5u, 0x04u, 5u, 0u), + Sop1(0x14u, 8u, 0x21u, 4u), + Sopp(0x18u, 0x01u), + Sopp(0x1cu, 0x00u), + Sop1(0x20u, 0u, 0x20u, 8u), + Sop1(0x24u, 0u, 0x20u, 8u), + })); + }); + ExpectThrow("is shared with another call", [] { + Build(Program({ + Sop1(0x00u, 4u, 0x1fu, 0u), + Sop2Literal(0x04u, 4u, 0x00u, 4u, 0x18u), + Sop2Literal(0x0cu, 5u, 0x04u, 5u, 0u), + Sop1(0x14u, 8u, 0x21u, 4u), + Sop1(0x18u, 8u, 0x21u, 4u), + Sopp(0x1cu, 0x01u), + Sopp(0x20u, 0x00u), + Sop1(0x24u, 0u, 0x20u, 8u), + })); + }); + ExpectThrow("recursive s_swappc_b64 call", [] { + Build(Program({ + Sop1(0x00u, 4u, 0x1fu, 0u), + Sop2Literal(0x04u, 4u, 0x00u, 4u, 0x10u), + Sop2Literal(0x0cu, 5u, 0x04u, 5u, 0u), + Sop1(0x14u, 8u, 0x21u, 4u), + Sopp(0x18u, 0x01u), + Sop1(0x1cu, 0u, 0x20u, 8u), + })); + }); + ExpectThrow("computed/data-dependent s_swappc_b64", [] { + SwappcInfo info; + info.fetchCallAllowed = true; + info.userDataBaseRegister = 16u; + info.userDataCount = 8u; + Build(Program({ + Sopp(0x00u, 0x00u), + Sop1(0x04u, 2u, 0x21u, 4u), + Sopp(0x08u, 0x01u), + }), &info); + }); + ExpectThrow("must live in the fetch-shader code", [] { + SwappcInfo info; + info.fetchCallAllowed = true; + info.userDataBaseRegister = 16u; + info.userDataCount = 8u; + Build(Program({ + Sopp(0x00u, 0x00u), + Sop1(0x04u, 2u, 0x21u, 16u), + Sopp(0x08u, 0x01u), + Sop1(0x0cu, 0u, 0x20u, 2u), + }), &info); + }); +} + +int main() { + try { + CheckDecodeIdentity(); + CheckLinkModelsNextInstructionAddress(); + CheckStaticCallRegion(); + CheckFetchCallAtEntry(); + CheckThrows(); + } catch (const std::exception& error) { + std::fprintf(stderr, "scalar swappc regression: %s\n", error.what()); + return 1; + } + return 0; +} diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp index 3891e7860..d93905ba9 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp @@ -108,6 +108,8 @@ struct ControlFlowGraph { std::vector codeTableLoadProgramCounters; std::vector codeTableLoads; std::uint32_t entryBlock = InvalidControlFlowId; + bool hasFetchCall = false; + std::uint32_t fetchCallProgramCounter = 0; bool irreducible = false; bool unsupported = false; FailureKind failureKind = FailureKind::None; diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp index 58934a077..bca0da8c9 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp @@ -3,17 +3,34 @@ #include "ControlFlow/ControlFlowGraph.hpp" #include "RdnaDecoder/RdnaProgram.hpp" +#include #include namespace ShaderRecompiler { +struct SwappcInfo { + bool fetchCallAllowed = false; + std::uint32_t userDataBaseRegister = 0; + std::uint32_t userDataCount = 0; +}; + +struct SwappcCall { + std::uint32_t callIndex = 0; + std::uint32_t linkRegister = 0; + bool fetch = false; + std::uint32_t targetIndex = 0; + std::uint32_t targetProgramCounter = 0; + std::uint32_t returnIndex = 0; + std::uint32_t returnTargetProgramCounter = 0; +}; + class GraphBuilder { public: - [[nodiscard]] ControlFlowGraph Build(const RdnaProgram& program) const; + [[nodiscard]] ControlFlowGraph Build(const RdnaProgram& program, const SwappcInfo* swappc = nullptr) const; private: - [[nodiscard]] std::vector splitIntoBlocks(const RdnaProgram& program) const; - void linkBlocks(std::vector& blocks, const RdnaProgram& program) const; + [[nodiscard]] std::vector splitIntoBlocks(const RdnaProgram& program, const std::vector& calls) const; + void linkBlocks(std::vector& blocks, const RdnaProgram& program, const std::vector& calls) const; }; } diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index cfcb917ff..2d60b3452 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -146,7 +146,7 @@ bool resolveLongSetpcTarget(const RdnaProgram& program, std::uint32_t setpcIndex const bool adds = low.op == RdnaOpcode::SAddU32 && high.op == RdnaOpcode::SAddcU32; const bool subtracts = low.op == RdnaOpcode::SSubU32 && high.op == RdnaOpcode::SSubbU32; std::uint32_t lowImmediate = 0, highImmediate = 0; - if (setpc.op != RdnaOpcode::SSetpcB64 || pcRegister == NoScalarRegister || pcRegister % 2u != 0u || pc.op != RdnaOpcode::SGetpcB64 || !isScalar(pc.destination, pcRegister)) return false; + if (!(setpc.op == RdnaOpcode::SSetpcB64 || setpc.op == RdnaOpcode::SSwappcB64) || pcRegister == NoScalarRegister || pcRegister % 2u != 0u || pc.op != RdnaOpcode::SGetpcB64 || !isScalar(pc.destination, pcRegister)) return false; if (adds ? !addsImmediateTo(low, pcRegister, lowImmediate) || !addsImmediateTo(high, pcRegister + 1u, highImmediate) : !subtracts || !subtractsImmediateFrom(low, pcRegister, lowImmediate) || !subtractsImmediateFrom(high, pcRegister + 1u, highImmediate)) return false; const auto offset = (static_cast(highImmediate) << 32u) | lowImmediate; @@ -166,7 +166,7 @@ bool resolveSetpcTarget(const RdnaProgram& program, std::uint32_t setpcIndex, st } const auto& setpc = program.instructions[setpcIndex]; - if (setpc.op != RdnaOpcode::SSetpcB64 || setpc.source0.kind != RdnaOperandKind::ScalarRegister) { + if (!(setpc.op == RdnaOpcode::SSetpcB64 || setpc.op == RdnaOpcode::SSwappcB64) || setpc.source0.kind != RdnaOperandKind::ScalarRegister) { return false; } @@ -392,6 +392,202 @@ bool resolveBoundedJumpTable(const RdnaProgram& program, std::uint32_t index, Bo return resolveDwordJumpTable(program, index, result); } +bool pairOverlapsRegister(const RdnaOperand& operand, std::uint32_t linkRegister) { + const std::uint32_t slot = scalarIndex(operand); + return slot != NoScalarRegister && (slot == linkRegister || slot == linkRegister + 1u); +} + +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); +} + +bool isFetchCallPrefixOpcode(const RdnaInstruction& instruction) { + return IsScalarAluOpcode(instruction.op) || instruction.op == RdnaOpcode::SNop || instruction.op == RdnaOpcode::SWaitcnt || + instruction.op == RdnaOpcode::SWaitcntDepctr || instruction.op == RdnaOpcode::SWaitIdle || instruction.op == RdnaOpcode::SIcacheInv || + instruction.op == RdnaOpcode::SSetprio || instruction.op == RdnaOpcode::SClause; +} + +std::uint32_t instructionIndexOfProgramCounter(const RdnaProgram& program, std::uint32_t programCounter) { + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + if (program.instructions[index].programCounter == programCounter) { + return index; + } + } + return InvalidControlFlowId; +} + +const SwappcCall* findCallAt(const std::vector& calls, std::uint32_t index) { + for (const auto& call : calls) { + if (call.callIndex == index) { + return &call; + } + } + return nullptr; +} + +const SwappcCall* findReturnAt(const std::vector& calls, std::uint32_t index) { + for (const auto& call : calls) { + if (!call.fetch && call.returnIndex == index) { + return &call; + } + } + return nullptr; +} + +std::vector analyzeSwappcCalls(const RdnaProgram& program, const SwappcInfo* swappc) { + 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) { + 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"); + } + 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) + + ": 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); + call.targetIndex = staticTarget ? instructionIndexOfProgramCounter(program, target) : InvalidControlFlowId; + 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 && + targetRegister >= swappc->userDataBaseRegister && targetRegister + 1u < swappc->userDataBaseRegister + swappc->userDataCount && + std::none_of(program.instructions.begin(), program.instructions.end(), [&](const RdnaInstruction& other) { + return writesScalar(other, targetRegister) || writesScalar(other, targetRegister + 1u); + }); + const bool outsideProgram = staticTarget && call.targetIndex == InvalidControlFlowId; + if (swappc != nullptr && swappc->fetchCallAllowed && positional && (userDataPair || outsideProgram)) { + call.fetch = true; + call.returnTargetProgramCounter = instructionEndProgramCounter(instruction); + calls.push_back(call); + continue; + } + if (!staticTarget || call.targetIndex == InvalidControlFlowId) { + throw std::invalid_argument("computed/data-dependent s_swappc_b64 call target at program counter " + toHexString(instruction.programCounter) + " is not statically resolvable"); + } + call.targetProgramCounter = target; + calls.push_back(call); + } + + for (auto& call : calls) { + std::uint32_t returnIndex = InvalidControlFlowId; + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + if (index == call.callIndex) { + continue; + } + 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) + + ": 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 " + + toHexString(program.instructions[call.callIndex].programCounter) + " escapes the constant-offset call/return model (used at program counter " + + toHexString(instruction.programCounter) + ")"); + } + } + if (call.fetch) { + if (returnIndex != InvalidControlFlowId) { + throw std::invalid_argument("unclosable s_swappc_b64 fetch-shader call at program counter " + toHexString(program.instructions[call.callIndex].programCounter) + + ": its s_setpc_b64 return must live in the fetch-shader code"); + } + continue; + } + if (returnIndex == InvalidControlFlowId) { + throw std::invalid_argument("unclosable s_swappc_b64 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; + call.returnTargetProgramCounter = instructionEndProgramCounter(program.instructions[call.callIndex]); + } + + 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) + + ": the call executes inside its own call region"); + } + for (std::size_t second = first + 1u; second < calls.size(); ++second) { + if (calls[first].fetch || calls[second].fetch) { + continue; + } + const bool disjoint = calls[first].returnIndex < calls[second].targetIndex || calls[second].returnIndex < calls[first].targetIndex; + 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) + + " and program counter " + toHexString(program.instructions[calls[second].callIndex].programCounter) + ": call regions interleave"); + } + } + } + + const bool anyStaticCall = std::any_of(calls.begin(), calls.end(), [](const SwappcCall& call) { return !call.fetch; }); + if (anyStaticCall) { + struct Transfer { + std::uint32_t index; + std::uint32_t targetIndex; + }; + std::vector transfers; + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + const auto& instruction = program.instructions[index]; + if (IsDirectBranchOpcode(instruction.op)) { + transfers.push_back(Transfer{index, instructionIndexOfProgramCounter(program, instruction.branchTarget)}); + continue; + } + if (instruction.op == RdnaOpcode::SSetpcB64) { + if (findReturnAt(calls, index) != nullptr) { + continue; + } + 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 " + + toHexString(instruction.programCounter) + " cannot be confined to a call region"); + } + transfers.push_back(Transfer{index, instructionIndexOfProgramCounter(program, target)}); + continue; + } + if (instruction.op == RdnaOpcode::SSwappcB64) { + const SwappcCall* call = findCallAt(calls, index); + transfers.push_back(Transfer{index, call->targetIndex}); + } + } + for (const auto& call : calls) { + if (call.fetch) { + continue; + } + for (const auto& transfer : transfers) { + 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 " + + 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 " + + toHexString(program.instructions[call.callIndex].programCounter) + ": control flow leaves the call region at program counter " + + toHexString(program.instructions[transfer.index].programCounter)); + } + } + } + } + return calls; +} + BranchCondition conditionForOpcode(RdnaOpcode opcode) { switch (opcode) { case RdnaOpcode::SBranch: return BranchCondition::Always; @@ -472,7 +668,7 @@ void pruneUnreachableBlocks(ControlFlowGraph& graph) { } -std::vector GraphBuilder::splitIntoBlocks(const RdnaProgram& program) const { +std::vector GraphBuilder::splitIntoBlocks(const RdnaProgram& program, const std::vector& calls) const { if (program.instructions.empty()) { throw std::invalid_argument("cannot build a control flow graph for an empty program"); } @@ -503,7 +699,9 @@ std::vector GraphBuilder::splitIntoBlocks(const RdnaProgram& program } } else if (instruction.op == RdnaOpcode::SSetpcB64) { std::uint32_t target = 0; - if (!resolveSetpcTarget(program, index, target)) { + if (const SwappcCall* returnCall = findReturnAt(calls, index); returnCall != nullptr) { + target = returnCall->returnTargetProgramCounter; + } else if (!resolveSetpcTarget(program, index, target)) { BoundedJumpTable table; if (!resolveBoundedJumpTable(program, index, table)) throw std::invalid_argument("unsupported dynamic s_setpc_b64 at program counter " + toHexString(instruction.programCounter)); for (const auto tableTarget : table.targets) { @@ -520,6 +718,14 @@ std::vector GraphBuilder::splitIntoBlocks(const RdnaProgram& program if (nextProgramCounter <= endProgramCounter) { labels.insert(nextProgramCounter); } + } else if (instruction.op == RdnaOpcode::SSwappcB64) { + const SwappcCall* call = findCallAt(calls, index); + if (call != nullptr && !call->fetch) { + labels.insert(call->targetProgramCounter); + if (nextProgramCounter <= endProgramCounter) { + labels.insert(nextProgramCounter); + } + } } else if (instruction.op == RdnaOpcode::SEndpgm) { labels.insert(nextProgramCounter); } @@ -550,7 +756,7 @@ std::vector GraphBuilder::splitIntoBlocks(const RdnaProgram& program return blocks; } -void GraphBuilder::linkBlocks(std::vector& blocks, const RdnaProgram& program) const { +void GraphBuilder::linkBlocks(std::vector& blocks, const RdnaProgram& program, const std::vector& calls) const { std::map programCounterToBlock; for (const auto& block : blocks) { programCounterToBlock.emplace(block.startProgramCounter, block.id); @@ -570,7 +776,11 @@ void GraphBuilder::linkBlocks(std::vector& blocks, const RdnaProgram block.terminator.kind = TerminatorKind::Return; } else if (last.op == RdnaOpcode::SSetpcB64) { std::uint32_t target = 0; - if (!resolveSetpcTarget(program, block.instructionEnd - 1u, target)) { + if (const SwappcCall* returnCall = findReturnAt(calls, block.instructionEnd - 1u); returnCall != nullptr) { + block.terminator.kind = TerminatorKind::Branch; + block.terminator.condition = BranchCondition::Always; + block.terminator.trueBlock = programCounterToBlock.at(returnCall->returnTargetProgramCounter); + } else if (!resolveSetpcTarget(program, block.instructionEnd - 1u, target)) { BoundedJumpTable table; if (!resolveBoundedJumpTable(program, block.instructionEnd - 1u, table)) throw std::invalid_argument("unsupported dynamic s_setpc_b64 at program counter " + toHexString(last.programCounter)); block.terminator.kind = TerminatorKind::IndirectBranch; @@ -582,11 +792,18 @@ void GraphBuilder::linkBlocks(std::vector& blocks, const RdnaProgram addUnique(block.successors, programCounterToBlock.at(tableTarget)); } continue; + } else { + if (block.instructionEnd - block.instructionBegin < 4u && resolveLongSetpcTarget(program, block.instructionEnd - 1u, target)) throw std::invalid_argument("long branch at program counter " + toHexString(last.programCounter) + " does not start in its block"); + block.terminator.kind = TerminatorKind::Branch; + block.terminator.condition = BranchCondition::Always; + block.terminator.trueBlock = programCounterToBlock.at(target); } - if (block.instructionEnd - block.instructionBegin < 4u && resolveLongSetpcTarget(program, block.instructionEnd - 1u, target)) throw std::invalid_argument("long branch at program counter " + toHexString(last.programCounter) + " does not start in its block"); + } else if (const SwappcCall* call = findCallAt(calls, block.instructionEnd - 1u); call != nullptr && !call->fetch) { + std::uint32_t ignoredTarget = 0; + if (block.instructionEnd - block.instructionBegin < 4u && resolveLongSetpcTarget(program, block.instructionEnd - 1u, ignoredTarget)) throw std::invalid_argument("long branch at program counter " + toHexString(last.programCounter) + " does not start in its block"); block.terminator.kind = TerminatorKind::Branch; block.terminator.condition = BranchCondition::Always; - block.terminator.trueBlock = programCounterToBlock.at(target); + block.terminator.trueBlock = programCounterToBlock.at(call->targetProgramCounter); } else if (last.op == RdnaOpcode::SBranch) { block.terminator.kind = TerminatorKind::Branch; block.terminator.condition = BranchCondition::Always; @@ -636,10 +853,17 @@ void GraphBuilder::linkBlocks(std::vector& blocks, const RdnaProgram } } -ControlFlowGraph GraphBuilder::Build(const RdnaProgram& program) const { +ControlFlowGraph GraphBuilder::Build(const RdnaProgram& program, const SwappcInfo* swappc) const { + const std::vector calls = analyzeSwappcCalls(program, swappc); ControlFlowGraph graph; - graph.blocks = splitIntoBlocks(program); - linkBlocks(graph.blocks, program); + graph.blocks = splitIntoBlocks(program, calls); + linkBlocks(graph.blocks, program, calls); + for (const auto& call : calls) { + if (call.fetch) { + graph.hasFetchCall = true; + graph.fetchCallProgramCounter = program.instructions[call.callIndex].programCounter; + } + } const auto originalSize = graph.blocks.size(); for (std::uint32_t id = 0; id < originalSize; ++id) { const auto term = graph.blocks[id].terminator; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp index 9e2841c8d..bd54285a7 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp @@ -1043,6 +1043,7 @@ enum class RdnaOpcode : std::uint16_t { Exp, VMulHiI32I24, VMulHiU32U24, + SSwappcB64, SSethalt, SSendmsghalt, SCodeEnd, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp index 6f0cabefe..02abfe5a4 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp @@ -39,6 +39,7 @@ RdnaOpcode decodeSop1Opcode(std::uint32_t opcode) { case 0x1eu: return RdnaOpcode::SBitset1B64; case 0x1fu: return RdnaOpcode::SGetpcB64; case 0x20u: return RdnaOpcode::SSetpcB64; + case 0x21u: return RdnaOpcode::SSwappcB64; case 0x22u: return RdnaOpcode::SRfeB64; case 0x24u: return RdnaOpcode::SAndSaveexecB64; case 0x25u: return RdnaOpcode::SOrSaveexecB64; @@ -262,6 +263,7 @@ std::uint32_t scalarDestinationDwordCount(RdnaOpcode opcode) { case RdnaOpcode::SBitset0B64: case RdnaOpcode::SBitset1B64: case RdnaOpcode::SGetpcB64: + case RdnaOpcode::SSwappcB64: case RdnaOpcode::SAndSaveexecB64: case RdnaOpcode::SOrSaveexecB64: case RdnaOpcode::SXorSaveexecB64: diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index a2870e10f..34432451b 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -105,7 +105,11 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request) { const auto decoded = decoder.Decode(request.shader.code); constexpr GraphBuilder graphBuilder; - auto cfg = graphBuilder.Build(decoded); + SwappcInfo swappcInfo; + swappcInfo.fetchCallAllowed = inputInfo.vertex != nullptr; + swappcInfo.userDataBaseRegister = request.context.userDataBaseRegister; + swappcInfo.userDataCount = static_cast(request.context.userData.size()); + auto cfg = graphBuilder.Build(decoded, &swappcInfo); constexpr Structurizer structurizer; structurizer.Structurize(cfg); diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index cd332977f..4a1cbbae7 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -276,6 +276,7 @@ private: void sTtracedata(); void sInstPrefetch(); void sGetpcB64(const RdnaInstruction& inst); + void sSwappcB64(const RdnaInstruction& inst); void sCselectB32(const RdnaInstruction& inst); void scalarSelect64(const RdnaInstruction& inst, const RdnaOperand& falseSource); void movB32(const RdnaInstruction& inst, bool applyFloatModifiers); diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index bcb207f45..798eaf34c 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -222,6 +222,12 @@ void TranslationContext::sGetpcB64(const RdnaInstruction& inst) { writeU32Pair(inst.destination, extractU64(pc)); } +void TranslationContext::sSwappcB64(const RdnaInstruction& inst) { + IrValue& base = ir.Emit(IrOpcode::GetShaderBase, IrType::U64, {}); + const IrU64 link(ir.Emit(IrOpcode::IAdd64, IrType::U64, {&base, &ir.ConstantU64(static_cast(currentProgramCounter) + 4u)})); + writeU32Pair(inst.destination, extractU64(link)); +} + void TranslationContext::sCselectB32(const RdnaInstruction& inst) { const IrU32 trueValue = readU32(sourceAt(inst, 0u)); const IrU32 falseValue = readU32(sourceAt(inst, 1u)); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index a22d72651..8a478453c 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -90,6 +90,29 @@ bool isCodeTableLoad(const ControlFlowGraph& cfg, std::uint32_t programCounter) return std::find(cfg.codeTableLoadProgramCounters.begin(), cfg.codeTableLoadProgramCounters.end(), programCounter) != cfg.codeTableLoadProgramCounters.end(); } +void translateExternalFetch(TranslationContext& context, const RdnaInstruction& call, const ShaderVertexInputInfo* input) { + if (input == nullptr || input->resourcesNum <= 0) { + throw std::runtime_error("s_swappc_b64 fetch-shader call at program counter " + std::to_string(call.programCounter) + " has no parsed fetch-shader plan to inline"); + } + for (int index = 0; index < input->resourcesNum && index < ShaderVertexInputInfo::MaxResources; ++index) { + const auto& destination = input->resourcesDst[index]; + if (destination.attrId < 0 || destination.registersNum <= 0) { + continue; + } + if (destination.registersNum > 4 || destination.registerStart < 0) { + throw std::runtime_error("external fetch-shader attribute " + std::to_string(destination.attrId) + " writes " + std::to_string(destination.registersNum) + + " registers starting at " + std::to_string(destination.registerStart) + "; the inline model supports one to four"); + } + RdnaInstruction fetch{}; + fetch.op = RdnaOpcode::BufferLoadFormatXyzw; + fetch.programCounter = call.programCounter; + fetch.destination.kind = RdnaOperandKind::VectorRegister; + fetch.destination.reg = static_cast(destination.registerStart); + fetch.formatted = true; + context.TranslateEmbeddedFetch(fetch, static_cast(index), static_cast(destination.registersNum)); + } +} + void includeInstructionVectorRegisters(const RdnaInstruction& instruction, std::uint32_t& vectorLimit) { const auto includeVector = [&vectorLimit](const RdnaOperand& operand, std::uint32_t count = 1u) { if (operand.kind == RdnaOperandKind::VectorRegister) { @@ -431,6 +454,9 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con } } context.TranslateInstruction(instruction); + if (cfg.hasFetchCall && instruction.programCounter == cfg.fetchCallProgramCounter) { + translateExternalFetch(context, instruction, options.inputInfo.vertex); + } } context.AddBranchCondition(cfgBlock, blockInfos[typedIndex]); } diff --git a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp index b9c90f1da..c861def8c 100644 --- a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp @@ -31,6 +31,9 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { 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: + sSwappcB64(inst); + return true; case RdnaOpcode::SSubvectorLoopBegin: sSubvectorLoop(inst, true); return true; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 359b5ac01..1a617546a 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -48,36 +48,22 @@ flowchart LR ## Graphics ```mermaid -flowchart TB - subgraph init["Game initialization"] - direction LR - create["libSceAgc:
create shader,
link stages,
primitive state"] --> reg["libSceAgcDriver:
RegisterShader,
Resolve*Abi"] - end +flowchart LR + game["Game:
command buffers"] --> submit["libSceAgcDriver/Submit
DCB / ACB"] + submit --> pm4["Execution/Pm4:
state, draws,
dispatches"] + pm4 -- "shader + state" --> cache{"Compiled variant
in memory or
on disk?"} + cache -- yes --> vk + cache -- no --> dec subgraph recompiler["core/shader/recompiler"] - direction LR dec["RdnaDecoder"] --> cf["ControlFlow:
graph + structurize"] cf --> tr["Translation:
RDNA to IR"] tr --> opt["Optimization:
SSA, resources,
bindings"] opt --> spv["SpirvBackend:
emit SPIR-V"] end - subgraph frame["Each frame"] - direction LR - game["Game:
command buffers"] --> submit["libSceAgcDriver/Submit
DCB / ACB"] - submit --> pm4["Execution/Pm4:
state, draws,
dispatches"] - pm4 -- "user data +
descriptors" --> inv["Invocation:
specialization constants,
descriptor heaps, BDA"] - inv --> vk["libSceAgcDriver/Graphics:
Vulkan pipeline"] - end - - init -- "code + static ABI" --> disk{"Compiled artifact
on disk?"} - disk -- no --> recompiler - disk -- yes --> art["Prepared artifact:
SPIR-V with no
resource values"] - recompiler --> art - art -- "looked up at
draw / dispatch" --> frame + spv --> vk["libSceAgcDriver/Graphics:
Vulkan pipeline"] ``` -- Shaders are compiled when the game registers them, not when they are first drawn. [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) runs `Driver::RegisterShader` for every shader the game creates, and `Resolve*Abi` when libSceAgc links stages or sets the primitive state. Each one compiles the artifact for that static ABI (stage, wave size, push constant layout, linked stages). -- An artifact does not depend on the resources bound at draw time. The shader reads them at runtime, as on the PS5: buffers, images and samplers from descriptor arrays and typed heaps indexed at runtime, and user data from the shader data block defined in [`RuntimeAbi.hpp`](../../core/shader/recompiler/RuntimeAbi.hpp). -- At a draw or dispatch, the driver looks up the prepared artifact and fails if there is none. Only compute shaders the game never registered are compiled at that point. The draw supplies the invocation: the specialization constants listed in [`PipelineSpecialization.hpp`](../../core/shader/recompiler/PipelineSpecialization.hpp), the descriptors and the push constants. Each distinct set of constants produces one specialized SPIR-V module, kept in memory. -- [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. `ShaderDiskCache` stores the artifacts on disk so later runs skip the recompilation. +- [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. +- `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f4812dd22..25b90199f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -331,7 +331,10 @@ Throughout the project, every function at every stage either **does exactly what - [poll](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [sceFontOpenFontSet](../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, 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. -- [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 +- [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.