diff --git a/src/graphics/shader/shaderCallTrace.cpp b/src/graphics/shader/shaderCallTrace.cpp new file mode 100644 index 000000000..9d5a0aee9 --- /dev/null +++ b/src/graphics/shader/shaderCallTrace.cpp @@ -0,0 +1,384 @@ +#include "graphics/shader/shaderCallTrace.h" + +#include "common/logging/log.h" +#include "common/magicEnum.h" +#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h" +#include "kernel/memory.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace Libs::Graphics { + +namespace { + +namespace Decoder = ShaderRecompiler::Decoder; +using Decoder::Opcode; +using Decoder::OperandKind; + +constexpr uint32_t MaxDepth = 12; + +constexpr uint32_t Sop1Prefix = 0x17Du; +constexpr uint32_t Sop1SwappcB64 = 0x21u; +constexpr uint32_t Sop1SetpcB64 = 0x20u; +constexpr uint32_t SCodeEnd = 0xbf9f0000u; +constexpr uint32_t SBranch = 0xbf820000u; +constexpr uint32_t SEndpgm = 0xbf810000u; + +struct SwappcSite { + uint32_t pc = 0; + uint32_t sdst = 0; + uint32_t ssrc = 0; +}; + +std::vector FindSwappcSites(std::span code) { + std::vector sites; + for (uint32_t index = 0; index < code.size(); index++) { + const auto word = code[index]; + if ((word >> 23u) != Sop1Prefix || ((word >> 8u) & 0xffu) != Sop1SwappcB64) { + continue; + } + sites.push_back({index * 4u, (word >> 16u) & 0x7fu, word & 0xffu}); + } + return sites; +} + +bool ScalarCode(const Decoder::Operand& operand, uint32_t& code) { + if (operand.kind != OperandKind::Sgpr) { + return false; + } + code = operand.reg; + return true; +} + +bool WritesScalar(const Decoder::Instruction& inst, uint32_t reg, uint32_t& base, + uint32_t& dwords) { + uint32_t dst = 0; + if (!ScalarCode(inst.dst, dst)) { + return false; + } + uint32_t width = 1; + switch (inst.opcode) { + case Opcode::S_LOAD_DWORD: + case Opcode::S_BUFFER_LOAD_DWORD: width = 1; break; + case Opcode::S_LOAD_DWORDX2: + case Opcode::S_BUFFER_LOAD_DWORDX2: width = 2; break; + case Opcode::S_LOAD_DWORDX4: + case Opcode::S_BUFFER_LOAD_DWORDX4: width = 4; break; + case Opcode::S_LOAD_DWORDX8: + case Opcode::S_BUFFER_LOAD_DWORDX8: width = 8; break; + case Opcode::S_LOAD_DWORDX16: + case Opcode::S_BUFFER_LOAD_DWORDX16: width = 16; break; + case Opcode::S_MOV_B32: width = 1; break; + case Opcode::S_MOV_B64: width = 2; break; + default: width = 1; break; + } + if (reg < dst || reg >= dst + width) { + return false; + } + base = dst; + dwords = width; + return true; +} + +bool IsLoad(Opcode opcode) { + switch (opcode) { + case Opcode::S_LOAD_DWORD: + case Opcode::S_LOAD_DWORDX2: + case Opcode::S_LOAD_DWORDX4: + case Opcode::S_LOAD_DWORDX8: + case Opcode::S_LOAD_DWORDX16: + case Opcode::S_BUFFER_LOAD_DWORD: + case Opcode::S_BUFFER_LOAD_DWORDX2: + case Opcode::S_BUFFER_LOAD_DWORDX4: + case Opcode::S_BUFFER_LOAD_DWORDX8: + case Opcode::S_BUFFER_LOAD_DWORDX16: return true; + default: return false; + } +} + +bool IsBufferLoad(Opcode opcode) { + switch (opcode) { + case Opcode::S_BUFFER_LOAD_DWORD: + case Opcode::S_BUFFER_LOAD_DWORDX2: + case Opcode::S_BUFFER_LOAD_DWORDX4: + case Opcode::S_BUFFER_LOAD_DWORDX8: + case Opcode::S_BUFFER_LOAD_DWORDX16: return true; + default: return false; + } +} + +class ChainWalker { +public: + ChainWalker(const Decoder::Program& program, std::span user_data) + : m_program(program), m_user_data(user_data) {} + + std::optional Scalar(uint32_t reg, uint32_t before_pc, uint32_t depth) { + if (depth > MaxDepth) { + Note(depth, fmt::format("s{} - giving up, chain deeper than {}", reg, MaxDepth)); + return std::nullopt; + } + const auto* writer = FindWriter(reg, before_pc); + if (writer == nullptr) { + if (reg < m_user_data.size()) { + return m_user_data[reg]; + } + Note(depth, fmt::format("s{} - no writer and outside the {} user-data words", reg, + m_user_data.size())); + return std::nullopt; + } + uint32_t base = 0; + uint32_t dwords = 0; + WritesScalar(*writer, reg, base, dwords); + + if (writer->opcode == Opcode::S_MOV_B32 || writer->opcode == Opcode::S_MOV_B64) { + uint32_t src = 0; + if (ScalarCode(writer->src0, src)) { + return Scalar(src + (reg - base), writer->pc, depth + 1); + } + if (writer->src0.kind == OperandKind::LiteralConstant || + writer->src0.kind == OperandKind::IntegerInlineConstant) { + return writer->src0.value; + } + Note(depth, fmt::format("s{} <- s_mov from an operand kind this walker does not read", + reg)); + return std::nullopt; + } + + if (!IsLoad(writer->opcode)) { + Note(depth, fmt::format("s{} <- {} at pc 0x{:08x}, not a load or move", reg, + magic_enum::enum_name(writer->opcode), writer->pc)); + return std::nullopt; + } + + uint32_t sbase = 0; + if (!ScalarCode(writer->src0, sbase)) { + Note(depth, fmt::format("s{} <- load whose base is not an SGPR", reg)); + return std::nullopt; + } + + uint64_t address = 0; + if (IsBufferLoad(writer->opcode)) { + const auto d0 = Scalar(sbase, writer->pc, depth + 1); + const auto d1 = Scalar(sbase + 1u, writer->pc, depth + 1); + if (!d0 || !d1) { + return std::nullopt; + } + address = static_cast(*d0) | + (static_cast(*d1 & 0xffffu) << 32u); + Note(depth, fmt::format("s{} <- s_buffer_load [V# s{} base=0x{:012x}] + 0x{:x}", reg, + sbase, address, writer->offset)); + if (std::find(m_bases.begin(), m_bases.end(), address) == m_bases.end()) { + m_bases.push_back(address); + } + } else { + const auto lo = Scalar(sbase, writer->pc, depth + 1); + const auto hi = Scalar(sbase + 1u, writer->pc, depth + 1); + if (!lo || !hi) { + return std::nullopt; + } + address = static_cast(*lo) | (static_cast(*hi) << 32u); + Note(depth, fmt::format("s{} <- s_load [s[{}:{}] = 0x{:012x}] + 0x{:x}", reg, sbase, + sbase + 1u, address, writer->offset)); + } + + address += writer->offset; + address += static_cast(reg - base) * 4u; + + uint32_t value = 0; + if (!Libs::LibKernel::Memory::TryReadBacking(address, &value, sizeof(value))) { + Note(depth, fmt::format("s{} - guest read at 0x{:012x} failed (unmapped)", reg, + address)); + return std::nullopt; + } + return value; + } + + [[nodiscard]] const std::vector& Notes() const { return m_notes; } + [[nodiscard]] const std::vector& Bases() const { return m_bases; } + +private: + const Decoder::Instruction* FindWriter(uint32_t reg, uint32_t before_pc) const { + const Decoder::Instruction* found = nullptr; + for (const auto& inst: m_program.instructions) { + if (inst.pc >= before_pc) { + break; + } + uint32_t base = 0; + uint32_t dwords = 0; + if (WritesScalar(inst, reg, base, dwords)) { + found = &inst; + } + } + return found; + } + + void Note(uint32_t depth, std::string text) { + m_notes.push_back(std::string(depth * 2u, ' ') + std::move(text)); + } + + std::vector m_bases; + const Decoder::Program& m_program; + std::span m_user_data; + std::vector m_notes; +}; + +} // namespace + +std::vector ResolveIndirectCalls(std::span code, + std::span user_data, + uint64_t shader_addr) { + std::vector resolved; + const auto sites = FindSwappcSites(code); + if (sites.empty()) { + return resolved; + } + + Decoder::Program program {}; + Decoder::DecodeProgram(code, program); + if (program.instructions.empty()) { + return resolved; + } + + for (const auto& site: sites) { + IndirectCallSite out; + out.pc = site.pc; + out.target_sgpr = site.ssrc; + + ChainWalker walker(program, user_data); + const auto lo = walker.Scalar(site.ssrc, site.pc, 0); + const auto hi = walker.Scalar(site.ssrc + 1u, site.pc, 0); + if (lo && hi) { + out.handler = static_cast(*lo) | (static_cast(*hi) << 32u); + } + + resolved.push_back(out); + } + return resolved; +} + +std::span TrimToCode(std::span code, uint32_t return_sgpr) { + for (size_t word = 0; word < code.size(); word++) { + if (code[word] == SCodeEnd) { + return code.first(word); + } + } + for (size_t word = code.size(); word-- > 0;) { + if (code[word] == SEndpgm) { + return code.first(word + 1u); + } + } + if (return_sgpr != UINT32_MAX) { + for (size_t word = code.size(); word-- > 0;) { + const auto candidate = code[word]; + if ((candidate >> 23u) == Sop1Prefix && ((candidate >> 8u) & 0xffu) == Sop1SetpcB64 && + (candidate & 0xffu) == return_sgpr) { + return code.first(word + 1u); + } + } + } + return code; +} + +bool EndsWithReturn(std::span code, uint32_t target_sgpr) { + if (code.empty()) { + return false; + } + const auto last = code.back(); + return (last >> 23u) == Sop1Prefix && ((last >> 8u) & 0xffu) == Sop1SetpcB64 && + (last & 0xffu) == target_sgpr; +} + +bool IsBranchOpcode(Opcode opcode) { + switch (opcode) { + case Opcode::S_BRANCH: + case Opcode::S_CBRANCH_SCC0: + case Opcode::S_CBRANCH_SCC1: + case Opcode::S_CBRANCH_VCCZ: + case Opcode::S_CBRANCH_VCCNZ: + case Opcode::S_CBRANCH_EXECZ: + case Opcode::S_CBRANCH_EXECNZ: return true; + default: return false; + } +} + +std::vector SpliceIndirectCalls(std::span code, + std::span sites, + std::span> handlers) { + if (sites.size() != handlers.size() || sites.empty()) { + return {}; + } + + const auto base = TrimToCode(code); + + Decoder::Program program {}; + Decoder::DecodeProgram(base, program); + if (program.instructions.empty()) { + return {}; + } + + struct Insertion { + uint32_t word = 0; // dword index of the S_SWAPPC_B64 being replaced + std::span body; // callee code without its trailing return + }; + std::vector insertions; + for (size_t index = 0; index < sites.size(); index++) { + const auto& site = sites[index]; + const auto handler = handlers[index]; + if (site.handler == 0 || handler.empty() || !EndsWithReturn(handler, site.target_sgpr)) { + return {}; + } + const auto word = site.pc / 4u; + if (word >= base.size()) { + return {}; + } + insertions.push_back({word, handler.first(handler.size() - 1u)}); + } + std::sort(insertions.begin(), insertions.end(), + [](const Insertion& lhs, const Insertion& rhs) { return lhs.word < rhs.word; }); + + std::vector remap(base.size(), 0); + std::vector out; + out.reserve(base.size() + 512u); + size_t next = 0; + for (const auto& insertion: insertions) { + for (; next < insertion.word; next++) { + remap[next] = static_cast(out.size()); + out.push_back(base[next]); + } + remap[insertion.word] = static_cast(out.size()); + out.insert(out.end(), insertion.body.begin(), insertion.body.end()); + next = insertion.word + 1u; // the call itself is gone + } + for (; next < base.size(); next++) { + remap[next] = static_cast(out.size()); + out.push_back(base[next]); + } + + for (const auto& inst: program.instructions) { + if (!IsBranchOpcode(inst.opcode)) { + continue; + } + const auto from = inst.pc / 4u; + const auto to = inst.branch_target / 4u; + if (from >= base.size() || to >= base.size()) { + return {}; + } + const auto new_from = remap[from]; + const auto new_to = remap[to]; + const auto delta = static_cast(new_to) - static_cast(new_from) - 1; + if (delta < INT16_MIN || delta > INT16_MAX || new_from >= out.size()) { + return {}; + } + out[new_from] = (out[new_from] & 0xffff0000u) | + static_cast(static_cast(delta)); + } + return out; +} + +} // namespace Libs::Graphics diff --git a/src/graphics/shader/shaderCallTrace.h b/src/graphics/shader/shaderCallTrace.h new file mode 100644 index 000000000..d0d4c5951 --- /dev/null +++ b/src/graphics/shader/shaderCallTrace.h @@ -0,0 +1,31 @@ +#ifndef EMULATOR_SRC_GRAPHICS_SHADER_SHADERCALLTRACE_H_ +#define EMULATOR_SRC_GRAPHICS_SHADER_SHADERCALLTRACE_H_ + +#include +#include +#include + +namespace Libs::Graphics { + +struct IndirectCallSite { + uint32_t pc = 0; // byte offset of the S_SWAPPC_B64 within the caller + uint32_t target_sgpr = 0; // SGPR pair holding the target, and the return address afterwards + uint64_t handler = 0; // resolved callee address; 0 when it could not be resolved +}; + +std::vector ResolveIndirectCalls(std::span code, + std::span user_data, + uint64_t shader_addr); + +std::span TrimToCode(std::span code, + uint32_t return_sgpr = UINT32_MAX); + +bool EndsWithReturn(std::span code, uint32_t target_sgpr); + +std::vector SpliceIndirectCalls(std::span code, + std::span sites, + std::span> handlers); + +} // namespace Libs::Graphics + +#endif