Files
PS2Recomp/ps2xRuntime/src/lib/ps2_debug_panel.cpp
T
Ran-j d91d2c1b75 feat: added EE clock Hz
fix: fix MPEG out of sync with new EE refactor
2026-08-07 21:54:33 -03:00

2276 lines
89 KiB
C++

#include "ps2_debug_panel.h"
#include "ps2_runtime.h"
#include "ps2_runtime_macros.h"
#include "ps2_log.h"
#include "runtime/ee_scheduler.h"
#include <unordered_set>
#include "Kernel/Syscalls/Helpers/State.h"
#include "Kernel/Stubs/CD.h"
#include "Kernel/Stubs/MemoryCard.h"
#include "Kernel/Stubs/Pad.h"
#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA)
#include "imgui.h"
#include "rlImGui.h"
#include "raylib.h"
#endif
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <exception>
#include <iomanip>
#include <mutex>
#include <sstream>
#include <string>
#include <vector>
namespace
{
#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA)
const char *threadStatusName(EeThreadStatus status)
{
switch (status)
{
case EeThreadStatus::Running:
return "RUN";
case EeThreadStatus::Ready:
return "READY";
case EeThreadStatus::Waiting:
return "WAIT";
case EeThreadStatus::Suspended:
return "SUSPEND";
case EeThreadStatus::WaitingSuspended:
return "WAITSUSP";
case EeThreadStatus::Dormant:
return "DORMANT";
default:
return "?";
}
}
const char *waitTypeName(EeWaitReason waitType)
{
switch (waitType)
{
case EeWaitReason::None:
return "NONE";
case EeWaitReason::Sleep:
return "SLEEP";
case EeWaitReason::Semaphore:
return "SEMA";
case EeWaitReason::EventFlag:
return "EVENT";
case EeWaitReason::VSync:
return "VSYNC";
case EeWaitReason::External:
return "EXTERNAL";
case EeWaitReason::Mpeg:
return "MPEG";
default:
return "?";
}
}
std::string iopRpcSidName(const ps2x::iop::DebugSnapshot &snapshot, uint32_t sid)
{
for (const ps2x::iop::DebugService &service : snapshot.services)
{
if (std::find(service.sids.begin(), service.sids.end(), sid) !=
service.sids.end())
{
return service.name;
}
}
return {};
}
std::string pressedPadButtons(uint16_t activeLowButtons)
{
struct ButtonName
{
uint16_t mask;
const char *name;
};
static constexpr ButtonName buttons[] = {
{1u << 0, "Select"},
{1u << 1, "L3"},
{1u << 2, "R3"},
{1u << 3, "Start"},
{1u << 4, "Up"},
{1u << 5, "Right"},
{1u << 6, "Down"},
{1u << 7, "Left"},
{1u << 8, "L2"},
{1u << 9, "R2"},
{1u << 10, "L1"},
{1u << 11, "R1"},
{1u << 12, "Triangle"},
{1u << 13, "Circle"},
{1u << 14, "Cross"},
{1u << 15, "Square"},
};
std::string out;
for (const ButtonName &button : buttons)
{
if ((activeLowButtons & button.mask) == 0u)
{
if (!out.empty())
{
out += ", ";
}
out += button.name;
}
}
return out.empty() ? std::string("none") : out;
}
std::string rpcDebugFlags(uint32_t flags)
{
struct FlagName
{
uint32_t mask;
const char *name;
};
static constexpr FlagName names[] = {
{kSifRpcDebugFlagNowait, "nowait"},
{kSifRpcDebugFlagHandledByHle, "hle"},
{kSifRpcDebugFlagCallback, "callback"},
{kSifRpcDebugFlagMissingClient, "bad-client"},
{kSifRpcDebugFlagServerDispatch, "server"},
{kSifRpcDebugFlagUnhandled, "unhandled"},
{kSifRpcDebugFlagFallbackCopy, "fallback-copy"},
{kSifRpcDebugFlagFallbackZero, "fallback-zero"},
};
std::string out;
for (const FlagName &name : names)
{
if ((flags & name.mask) != 0u)
{
if (!out.empty())
{
out += ",";
}
out += name.name;
}
}
return out;
}
std::string rpcPreviewBytes(const uint8_t *bytes, uint32_t count)
{
if (!bytes || count == 0u)
{
return "";
}
std::string out;
char item[4] = {};
for (uint32_t i = 0; i < count; ++i)
{
std::snprintf(item, sizeof(item), "%02X", bytes[i]);
if (!out.empty())
{
out.push_back(' ');
}
out += item;
}
return out;
}
template <typename T>
bool readGuestObjectNoThrow(uint8_t *rdram, uint32_t addr, T &out)
{
if (!rdram || addr == 0u)
{
return false;
}
uint8_t *ptr = getMemPtr(rdram, addr);
if (!ptr)
{
return false;
}
std::memcpy(&out, ptr, sizeof(T));
return true;
}
const char *primTypeName(GSPrimType type)
{
switch (type)
{
case GS_PRIM_POINT:
return "POINT";
case GS_PRIM_LINE:
return "LINE";
case GS_PRIM_LINESTRIP:
return "LINE_STRIP";
case GS_PRIM_TRIANGLE:
return "TRI";
case GS_PRIM_TRISTRIP:
return "TRI_STRIP";
case GS_PRIM_TRIFAN:
return "TRI_FAN";
case GS_PRIM_SPRITE:
return "SPRITE";
default:
return "?";
}
}
const char *gsDebugEventKindName(GSDebugEventKind kind)
{
switch (kind)
{
case GSDebugEventKind::GifTag:
return "GIF";
case GSDebugEventKind::Register:
return "REG";
case GSDebugEventKind::Draw:
return "DRAW";
case GSDebugEventKind::Transfer:
return "XFER";
case GSDebugEventKind::Present:
return "PRESENT";
default:
return "?";
}
}
const char *gifFormatName(uint8_t flg)
{
switch (flg)
{
case GIF_FMT_PACKED:
return "PACKED";
case GIF_FMT_REGLIST:
return "REGLIST";
case GIF_FMT_IMAGE:
return "IMAGE";
case GIF_FMT_DISABLED:
return "DISABLED";
default:
return "?";
}
}
const char *gsRegName(uint8_t reg)
{
switch (reg)
{
case GS_REG_PRIM:
return "PRIM";
case GS_REG_RGBAQ:
return "RGBAQ";
case GS_REG_ST:
return "ST";
case GS_REG_UV:
return "UV";
case GS_REG_XYZF2:
return "XYZF2";
case GS_REG_XYZ2:
return "XYZ2";
case GS_REG_TEX0_1:
return "TEX0_1";
case GS_REG_TEX0_2:
return "TEX0_2";
case GS_REG_CLAMP_1:
return "CLAMP_1";
case GS_REG_CLAMP_2:
return "CLAMP_2";
case GS_REG_XYZF3:
return "XYZF3";
case GS_REG_XYZ3:
return "XYZ3";
case GS_REG_TEX1_1:
return "TEX1_1";
case GS_REG_TEX1_2:
return "TEX1_2";
case GS_REG_TEX2_1:
return "TEX2_1";
case GS_REG_TEX2_2:
return "TEX2_2";
case GS_REG_XYOFFSET_1:
return "XYOFFSET_1";
case GS_REG_XYOFFSET_2:
return "XYOFFSET_2";
case GS_REG_PRMODECONT:
return "PRMODECONT";
case GS_REG_PRMODE:
return "PRMODE";
case GS_REG_TEXCLUT:
return "TEXCLUT";
case GS_REG_TEXA:
return "TEXA";
case GS_REG_SCISSOR_1:
return "SCISSOR_1";
case GS_REG_SCISSOR_2:
return "SCISSOR_2";
case GS_REG_ALPHA_1:
return "ALPHA_1";
case GS_REG_ALPHA_2:
return "ALPHA_2";
case GS_REG_TEST_1:
return "TEST_1";
case GS_REG_TEST_2:
return "TEST_2";
case GS_REG_FBA_1:
return "FBA_1";
case GS_REG_FBA_2:
return "FBA_2";
case GS_REG_FRAME_1:
return "FRAME_1";
case GS_REG_FRAME_2:
return "FRAME_2";
case GS_REG_ZBUF_1:
return "ZBUF_1";
case GS_REG_ZBUF_2:
return "ZBUF_2";
case GS_REG_BITBLTBUF:
return "BITBLTBUF";
case GS_REG_TRXPOS:
return "TRXPOS";
case GS_REG_TRXREG:
return "TRXREG";
case GS_REG_TRXDIR:
return "TRXDIR";
case GS_REG_HWREG:
return "HWREG";
case GS_REG_SIGNAL:
return "SIGNAL";
case GS_REG_FINISH:
return "FINISH";
case GS_REG_LABEL:
return "LABEL";
default:
return "?";
}
}
void textHex32(const char *label, uint32_t value)
{
ImGui::Text("%s: 0x%08X", label, value);
}
void textHex64(const char *label, uint64_t value)
{
ImGui::Text("%s: 0x%016llX", label, static_cast<unsigned long long>(value));
}
uint32_t readGuestU32NoThrow(uint8_t *rdram, uint32_t addr)
{
if (!rdram || addr == 0u)
{
return 0u;
}
uint8_t *ptr = getMemPtr(rdram, addr);
if (!ptr)
{
return 0u;
}
uint32_t value = 0u;
std::memcpy(&value, ptr, sizeof(value));
return value;
}
std::string pathToString(const std::filesystem::path &path)
{
if (path.empty())
{
return std::string("<empty>");
}
return path.string();
}
void textPath(const char *label, const std::filesystem::path &path)
{
const std::string value = pathToString(path);
ImGui::Text("%s: %s", label, value.c_str());
}
std::filesystem::path makeDebugDumpPath(const char *name, uint32_t frameIndex)
{
static uint32_t s_dumpCounter = 0u;
std::filesystem::path dir = std::filesystem::current_path() / "ps2_debug_dumps";
std::filesystem::create_directories(dir);
char fileName[128]{};
std::snprintf(fileName,
sizeof(fileName),
"%s_frame_%08X_%04u.txt",
name,
frameIndex,
++s_dumpCounter);
return dir / fileName;
}
void dumpGsContext(std::ostream &out, const char *label, const GSContext &ctx)
{
out << label << "\n";
out << " FRAME fbp=" << ctx.frame.fbp
<< " fbw=" << static_cast<unsigned int>(ctx.frame.fbw)
<< " psm=0x" << std::hex << static_cast<unsigned int>(ctx.frame.psm)
<< " fbmsk=0x" << ctx.frame.fbmsk << std::dec << "\n";
out << " ZBUF zbp=" << ctx.zbuf.zbp
<< " psm=0x" << std::hex << static_cast<unsigned int>(ctx.zbuf.psm) << std::dec
<< " zmask=" << (ctx.zbuf.zmask ? 1u : 0u) << "\n";
out << " SCISSOR x=" << ctx.scissor.x0 << ".." << ctx.scissor.x1
<< " y=" << ctx.scissor.y0 << ".." << ctx.scissor.y1 << "\n";
out << " TEX0 tbp0=" << ctx.tex0.tbp0
<< " tbw=" << static_cast<unsigned int>(ctx.tex0.tbw)
<< " psm=0x" << std::hex << static_cast<unsigned int>(ctx.tex0.psm) << std::dec
<< " size=" << (1u << ctx.tex0.tw) << "x" << (1u << ctx.tex0.th)
<< " tcc=" << static_cast<unsigned int>(ctx.tex0.tcc)
<< " tfx=" << static_cast<unsigned int>(ctx.tex0.tfx)
<< " cbp=" << ctx.tex0.cbp
<< " cpsm=0x" << std::hex << static_cast<unsigned int>(ctx.tex0.cpsm) << std::dec
<< " csm=" << static_cast<unsigned int>(ctx.tex0.csm)
<< " csa=" << static_cast<unsigned int>(ctx.tex0.csa)
<< " cld=" << static_cast<unsigned int>(ctx.tex0.cld) << "\n";
out << " XYOFFSET ofx=" << ctx.xyoffset.ofx << " ofy=" << ctx.xyoffset.ofy << "\n";
out << " TEST 0x" << std::hex << std::setw(16) << std::setfill('0') << ctx.test << std::setfill(' ') << std::dec
<< " ATE=" << ((ctx.test >> 0) & 1ull)
<< " ATST=" << ((ctx.test >> 1) & 7ull)
<< " AREF=0x" << std::hex << ((ctx.test >> 4) & 0xffull) << std::dec
<< " AFAIL=" << ((ctx.test >> 12) & 3ull)
<< " DATE=" << ((ctx.test >> 14) & 1ull)
<< " DATM=" << ((ctx.test >> 15) & 1ull)
<< " ZTE=" << ((ctx.test >> 16) & 1ull)
<< " ZTST=" << ((ctx.test >> 17) & 3ull) << "\n";
out << " ALPHA 0x" << std::hex << std::setw(16) << std::setfill('0') << ctx.alpha << std::setfill(' ') << std::dec << "\n";
out << " TEX1 0x" << std::hex << std::setw(16) << std::setfill('0') << ctx.tex1 << std::setfill(' ') << std::dec << "\n";
out << " CLAMP 0x" << std::hex << std::setw(16) << std::setfill('0') << ctx.clamp << std::setfill(' ') << std::dec << "\n";
out << " FBA 0x" << std::hex << std::setw(16) << std::setfill('0') << ctx.fba << std::setfill(' ') << std::dec << "\n";
}
bool writeGsDebugDump(const std::filesystem::path &path,
const GSRegisters &regs,
const GSDebugSnapshot &gs,
const std::vector<GSDebugHistoryEntry> &history,
bool currentFrameOnly,
bool drawsOnly,
uint32_t latestFrame,
std::string &error)
{
try
{
std::filesystem::create_directories(path.parent_path());
std::ofstream out(path, std::ios::out | std::ios::trunc);
if (!out)
{
error = "failed to open output file";
return false;
}
out << "PS2 Runtime GS debug dump\n";
out << "path: " << path.string() << "\n";
out << "latestFrame: " << latestFrame << "\n";
out << "filters: currentFrameOnly=" << (currentFrameOnly ? 1u : 0u)
<< " drawsOnly=" << (drawsOnly ? 1u : 0u) << "\n\n";
out << "[GS private registers]\n";
out << std::hex << std::setfill('0');
out << "PMODE 0x" << std::setw(16) << regs.pmode << "\n";
out << "SMODE2 0x" << std::setw(16) << regs.smode2 << "\n";
out << "DISPFB1 0x" << std::setw(16) << regs.dispfb1 << "\n";
out << "DISPLAY1 0x" << std::setw(16) << regs.display1 << "\n";
out << "DISPFB2 0x" << std::setw(16) << regs.dispfb2 << "\n";
out << "DISPLAY2 0x" << std::setw(16) << regs.display2 << "\n";
out << "BGCOLOR 0x" << std::setw(16) << regs.bgcolor << "\n";
out << "CSR 0x" << std::setw(16) << regs.csr.load() << "\n";
out << "IMR 0x" << std::setw(16) << regs.imr << "\n";
out << "BUSDIR 0x" << std::setw(16) << regs.busdir << "\n";
out << "SIGLBLID 0x" << std::setw(16) << regs.siglblid << "\n\n";
out << std::dec << std::setfill(' ');
out << "[GS draw state]\n";
out << "PRIM type=" << primTypeName(gs.prim.type)
<< " iip=" << gs.prim.iip
<< " tme=" << gs.prim.tme
<< " fge=" << gs.prim.fge
<< " abe=" << gs.prim.abe
<< " aa1=" << gs.prim.aa1
<< " fst=" << gs.prim.fst
<< " ctxt=" << gs.prim.ctxt
<< " fix=" << gs.prim.fix << "\n";
out << "TEXA ta0=0x" << std::hex << static_cast<unsigned int>(gs.texa.ta0) << std::dec
<< " aem=" << (gs.texa.aem ? 1u : 0u)
<< " ta1=0x" << std::hex << static_cast<unsigned int>(gs.texa.ta1) << std::dec
<< " TEXCLUT cbw=" << gs.texclut.cbw
<< " cou=" << gs.texclut.cou
<< " cov=" << gs.texclut.cov << "\n";
out << "BITBLT sbp=" << gs.bitbltbuf.sbp
<< " sbw=" << gs.bitbltbuf.sbw
<< " spsm=0x" << std::hex << static_cast<unsigned int>(gs.bitbltbuf.spsm) << std::dec
<< " dbp=" << gs.bitbltbuf.dbp
<< " dbw=" << gs.bitbltbuf.dbw
<< " dpsm=0x" << std::hex << static_cast<unsigned int>(gs.bitbltbuf.dpsm) << std::dec << "\n";
out << "TRXPOS ss=(" << gs.trxpos.ssax << "," << gs.trxpos.ssay << ")"
<< " ds=(" << gs.trxpos.dsax << "," << gs.trxpos.dsay << ")"
<< " dir=" << gs.trxpos.dir
<< " TRXREG=" << gs.trxreg.rrw << "x" << gs.trxreg.rrh
<< " TRXDIR=" << gs.trxdir
<< " copied=" << gs.transferCopiedPixels << "/" << gs.transferTotalPixels
<< " at=(" << gs.transferX << "," << gs.transferY << ")\n";
out << "Host presentation has=" << (gs.hasHostPresentationFrame ? 1u : 0u)
<< " size=" << gs.hostPresentationWidth << "x" << gs.hostPresentationHeight
<< " displayFbp=" << gs.hostPresentationDisplayFbp
<< " sourceFbp=" << gs.hostPresentationSourceFbp
<< " preferred=" << (gs.hostPresentationUsedPreferred ? 1u : 0u)
<< " lastDisplayBaseBytes=0x" << std::hex << gs.lastDisplayBaseBytes << std::dec
<< " localToHostPending=" << gs.localToHostPendingBytes << "\n";
out << "Preferred source has=" << (gs.hasPreferredDisplaySource ? 1u : 0u)
<< " frame fbp=" << gs.preferredDisplaySourceFrame.fbp
<< " fbw=" << gs.preferredDisplaySourceFrame.fbw
<< " psm=0x" << std::hex << static_cast<unsigned int>(gs.preferredDisplaySourceFrame.psm) << std::dec
<< " destFbp=" << gs.preferredDisplayDestFbp << "\n\n";
out << "[GS context state]\n";
dumpGsContext(out, "Context 0", gs.ctx[0]);
dumpGsContext(out, "Context 1", gs.ctx[1]);
out << "\n";
out << "[GS history]\n";
out << "seq\tframe\ttick\tkind\tprim\tctx\tframe\tzbuf\ttest\talpha\ttex0\txy\tz\ta\tdetail\tpresent\n";
for (auto it = history.rbegin(); it != history.rend(); ++it)
{
const GSDebugHistoryEntry &row = *it;
if (currentFrameOnly && row.frameIndex != latestFrame)
{
continue;
}
if (drawsOnly && row.kind != GSDebugEventKind::Draw)
{
continue;
}
out << row.seq << '\t'
<< row.frameIndex << '\t'
<< row.vsyncTick << '\t'
<< gsDebugEventKindName(row.kind) << '\t'
<< primTypeName(row.prim.type) << '\t'
<< (row.prim.ctxt ? 1u : 0u) << '\t'
<< "fbp=" << row.frame.fbp << "/fbw=" << static_cast<unsigned int>(row.frame.fbw) << "/psm=0x" << std::hex << static_cast<unsigned int>(row.frame.psm) << std::dec << '\t'
<< "zbp=" << row.zbuf.zbp << "/psm=0x" << std::hex << static_cast<unsigned int>(row.zbuf.psm) << std::dec << "/m=" << (row.zbuf.zmask ? 1u : 0u) << '\t'
<< "0x" << std::hex << row.test << std::dec << '\t'
<< "0x" << std::hex << row.alpha << std::dec << '\t'
<< "tbp=" << row.tex0.tbp0 << "/psm=0x" << std::hex << static_cast<unsigned int>(row.tex0.psm) << std::dec << "/size=" << (1u << row.tex0.tw) << "x" << (1u << row.tex0.th) << "/cbp=" << row.tex0.cbp << '\t';
if (row.kind == GSDebugEventKind::Draw)
{
out << row.xMin << ".." << row.xMax << "/" << row.yMin << ".." << row.yMax << '\t'
<< row.zMin << ".." << row.zMax << '\t'
<< static_cast<unsigned int>(row.aMin) << ".." << static_cast<unsigned int>(row.aMax) << '\t';
}
else
{
out << "-\t-\t-\t";
}
if (row.kind == GSDebugEventKind::GifTag)
{
out << gifFormatName(row.gifFlg) << " nloop=" << row.gifNloop << " nreg=" << row.gifNreg << " size=" << row.gifSizeBytes;
}
else if (row.kind == GSDebugEventKind::Register)
{
out << gsRegName(row.reg) << "(0x" << std::hex << static_cast<unsigned int>(row.reg) << ")=0x" << row.regValue << std::dec;
}
else if (row.kind == GSDebugEventKind::Transfer)
{
out << "TRXDIR=" << row.trxdir << " " << row.trxreg.rrw << "x" << row.trxreg.rrh
<< " pix=" << row.transferPixels << " sbp=" << row.bitbltbuf.sbp << " dbp=" << row.bitbltbuf.dbp;
}
else
{
out << "-";
}
out << '\t';
if (row.kind == GSDebugEventKind::Present)
{
out << row.width << "x" << row.height
<< " display=" << row.displayFbp
<< " source=" << row.sourceFbp
<< " pref=" << (row.usedPreferred ? 1u : 0u);
}
else
{
out << "-";
}
out << '\n';
}
return true;
}
catch (const std::exception &ex)
{
error = ex.what();
return false;
}
}
void drawGsContext(const char *label, const GSContext &ctx)
{
if (!ImGui::TreeNode(label))
{
return;
}
ImGui::Text("FRAME fbp=%u fbw=%u psm=0x%02X fbmsk=0x%08X",
ctx.frame.fbp,
ctx.frame.fbw,
static_cast<unsigned int>(ctx.frame.psm),
ctx.frame.fbmsk);
ImGui::Text("ZBUF zbp=%u psm=0x%02X zmask=%u",
ctx.zbuf.zbp,
static_cast<unsigned int>(ctx.zbuf.psm),
ctx.zbuf.zmask ? 1u : 0u);
ImGui::Text("SCISSOR x=%u..%u y=%u..%u",
ctx.scissor.x0,
ctx.scissor.x1,
ctx.scissor.y0,
ctx.scissor.y1);
ImGui::Text("TEX0 tbp0=%u tbw=%u psm=0x%02X tw=%u th=%u tcc=%u tfx=%u cbp=%u cpsm=0x%02X csm=%u csa=%u cld=%u",
ctx.tex0.tbp0,
static_cast<unsigned int>(ctx.tex0.tbw),
static_cast<unsigned int>(ctx.tex0.psm),
static_cast<unsigned int>(ctx.tex0.tw),
static_cast<unsigned int>(ctx.tex0.th),
static_cast<unsigned int>(ctx.tex0.tcc),
static_cast<unsigned int>(ctx.tex0.tfx),
ctx.tex0.cbp,
static_cast<unsigned int>(ctx.tex0.cpsm),
static_cast<unsigned int>(ctx.tex0.csm),
static_cast<unsigned int>(ctx.tex0.csa),
static_cast<unsigned int>(ctx.tex0.cld));
ImGui::Text("XYOFFSET ofx=%u ofy=%u", ctx.xyoffset.ofx, ctx.xyoffset.ofy);
textHex64("TEST", ctx.test);
ImGui::SameLine();
ImGui::Text("ATE=%llu ATST=%llu AREF=0x%02llX AFAIL=%llu DATE=%llu DATM=%llu ZTE=%llu ZTST=%llu",
(ctx.test >> 0) & 1ull,
(ctx.test >> 1) & 7ull,
(ctx.test >> 4) & 0xffull,
(ctx.test >> 12) & 3ull,
(ctx.test >> 14) & 1ull,
(ctx.test >> 15) & 1ull,
(ctx.test >> 16) & 1ull,
(ctx.test >> 17) & 3ull);
textHex64("ALPHA", ctx.alpha);
textHex64("TEX1", ctx.tex1);
textHex64("CLAMP", ctx.clamp);
textHex64("FBA", ctx.fba);
ImGui::TreePop();
}
void drawHexDump(uint8_t *rdram, uint32_t startAddr, uint32_t bytes)
{
bytes = std::clamp<uint32_t>(bytes, 16u, 0x1000u);
const uint32_t alignedStart = startAddr & ~0x0fu;
for (uint32_t off = 0u; off < bytes; off += 16u)
{
const uint32_t addr = alignedStart + off;
uint8_t *ptr = getMemPtr(rdram, addr);
if (!ptr)
{
ImGui::Text("%08X: <invalid>", addr);
continue;
}
char hex[16 * 3 + 1]{};
char ascii[17]{};
for (uint32_t i = 0u; i < 16u; ++i)
{
const uint8_t b = ptr[i];
std::snprintf(hex + i * 3, sizeof(hex) - i * 3, "%02X ", b);
ascii[i] = (b >= 32u && b < 127u) ? static_cast<char>(b) : '.';
}
ImGui::Text("%08X: %s |%s|", addr, hex, ascii);
}
}
void drawCpuTab(PS2Runtime &runtime, bool showRegisters)
{
const uint32_t pc = runtime.m_debugPc.load(std::memory_order_relaxed);
const uint32_t ra = runtime.m_debugRa.load(std::memory_order_relaxed);
const uint32_t sp = runtime.m_debugSp.load(std::memory_order_relaxed);
const uint32_t gp = runtime.m_debugGp.load(std::memory_order_relaxed);
ImGui::Text("Runtime: %s", runtime.isStopRequested() ? "stop requested" : "running");
ImGui::Text("EE executor: %s", runtime.eeScheduler().isExecutingGuest() ? "guest" : "scheduler");
ImGui::Separator();
textHex32("PC", pc);
ImGui::SameLine();
textHex32("RA", ra);
textHex32("SP", sp);
ImGui::SameLine();
textHex32("GP", gp);
if (ImGui::Button("Copy PC/RA/SP/GP"))
{
char buf[256]{};
std::snprintf(buf, sizeof(buf), "pc=0x%08X ra=0x%08X sp=0x%08X gp=0x%08X", pc, ra, sp, gp);
ImGui::SetClipboardText(buf);
}
ImGui::TextDisabled("full context snapshot is best-effort while guest is running");
if (showRegisters)
{
const R5900Context &cpu = runtime.cpu();
if (ImGui::BeginTable("gpr", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("Reg");
ImGui::TableSetupColumn("Lo32");
ImGui::TableSetupColumn("Hi32[1]");
ImGui::TableSetupColumn("Hi64[2:3]");
ImGui::TableHeadersRow();
for (int i = 0; i < 32; ++i)
{
const uint32_t w0 = static_cast<uint32_t>(_mm_extract_epi32(cpu.r[i], 0));
const uint32_t w1 = static_cast<uint32_t>(_mm_extract_epi32(cpu.r[i], 1));
const uint32_t w2 = static_cast<uint32_t>(_mm_extract_epi32(cpu.r[i], 2));
const uint32_t w3 = static_cast<uint32_t>(_mm_extract_epi32(cpu.r[i], 3));
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("r%d", i);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", w0);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", w1);
ImGui::TableNextColumn();
ImGui::Text("0x%08X_%08X", w3, w2);
}
ImGui::EndTable();
}
ImGui::SeparatorText("COP0");
ImGui::Text("Status=0x%08X Cause=0x%08X EPC=0x%08X Count=0x%08X Compare=0x%08X BadVAddr=0x%08X",
cpu.cop0_status,
cpu.cop0_cause,
cpu.cop0_epc,
cpu.cop0_count,
cpu.cop0_compare,
cpu.cop0_badvaddr);
}
}
void drawThreadsTab(PS2Runtime &runtime)
{
const EeKernelSnapshot snapshot = runtime.eeScheduler().snapshot();
ImGui::Text("Threads: %zu running=%d", snapshot.threads.size(), snapshot.runningThreadId);
ImGui::Text("EE cycle: %llu slice end: %llu next event: %llu",
static_cast<unsigned long long>(snapshot.eeCycle),
static_cast<unsigned long long>(snapshot.sliceEndCycle),
static_cast<unsigned long long>(snapshot.nextEventCycle));
if (ImGui::BeginTable("threads", 11, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 320)))
{
ImGui::TableSetupColumn("ID");
ImGui::TableSetupColumn("Status");
ImGui::TableSetupColumn("Wait");
ImGui::TableSetupColumn("WaitId");
ImGui::TableSetupColumn("PC");
ImGui::TableSetupColumn("Entry");
ImGui::TableSetupColumn("Stack");
ImGui::TableSetupColumn("GP");
ImGui::TableSetupColumn("Prio");
ImGui::TableSetupColumn("Wake");
ImGui::TableSetupColumn("Susp");
ImGui::TableHeadersRow();
for (const EeThreadSnapshot &row : snapshot.threads)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%d", row.id);
ImGui::TableNextColumn();
ImGui::Text("%s", threadStatusName(row.status));
ImGui::TableNextColumn();
ImGui::Text("%s", waitTypeName(row.waitReason));
ImGui::TableNextColumn();
ImGui::Text("%d", row.waitId);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.pc);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.entry);
ImGui::TableNextColumn();
ImGui::Text("0x%08X/%u", row.stack, row.stackSize);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.gp);
ImGui::TableNextColumn();
ImGui::Text("%d/%d", row.currentPriority, row.initialPriority);
ImGui::TableNextColumn();
ImGui::Text("%u", row.wakeupCount);
ImGui::TableNextColumn();
ImGui::Text("%d", row.suspendCount);
}
ImGui::EndTable();
}
}
void drawKernelTab(PS2Runtime &runtime)
{
const EeKernelSnapshot snapshot = runtime.eeScheduler().snapshot();
ImGui::SeparatorText("Semaphores");
{
if (ImGui::BeginTable("semas", 7, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("ID");
ImGui::TableSetupColumn("Count");
ImGui::TableSetupColumn("Max");
ImGui::TableSetupColumn("Init");
ImGui::TableSetupColumn("Waiters");
ImGui::TableSetupColumn("Attr");
ImGui::TableSetupColumn("Deleted");
ImGui::TableHeadersRow();
for (const EeSemaphoreSnapshot &sema : snapshot.semaphores)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%d", sema.id);
ImGui::TableNextColumn();
ImGui::Text("%d", sema.count);
ImGui::TableNextColumn();
ImGui::Text("%d", sema.maxCount);
ImGui::TableNextColumn();
ImGui::Text("-");
ImGui::TableNextColumn();
ImGui::Text("%u", sema.waiters);
ImGui::TableNextColumn();
ImGui::Text("-");
ImGui::TableNextColumn();
ImGui::Text("0");
}
ImGui::EndTable();
}
}
ImGui::SeparatorText("Event flags");
{
if (ImGui::BeginTable("evf", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("ID");
ImGui::TableSetupColumn("Bits");
ImGui::TableSetupColumn("Init");
ImGui::TableSetupColumn("Waiters");
ImGui::TableSetupColumn("Attr");
ImGui::TableSetupColumn("Deleted");
ImGui::TableHeadersRow();
for (const EeEventFlagSnapshot &evf : snapshot.eventFlags)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%d", evf.id);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", evf.bits);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", evf.initBits);
ImGui::TableNextColumn();
ImGui::Text("%u", evf.waiters);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", evf.attr);
ImGui::TableNextColumn();
ImGui::Text("0");
}
ImGui::EndTable();
}
}
}
void drawIopTab(PS2Runtime &runtime)
{
uint8_t *rdram = runtime.memory().getRDRAM();
const ps2x::iop::DebugSnapshot iopSnapshot = runtime.iopDebugSnapshot();
struct ModuleRow
{
int32_t id = 0;
std::string path;
std::string pathKey;
uint32_t refCount = 0;
bool loaded = false;
};
std::vector<ModuleRow> modules;
uint32_t moduleLogCount = 0;
int32_t nextModuleId = 0;
{
std::lock_guard<std::mutex> lock(g_sif_module_mutex);
modules.reserve(g_sif_modules_by_id.size());
for (const auto &[id, record] : g_sif_modules_by_id)
{
ModuleRow row{};
row.id = id;
row.path = record.path;
row.pathKey = record.pathKey;
row.refCount = record.refCount;
row.loaded = record.loaded;
modules.push_back(std::move(row));
}
moduleLogCount = g_sif_module_log_count;
nextModuleId = g_next_sif_module_id;
}
std::sort(modules.begin(), modules.end(), [](const ModuleRow &a, const ModuleRow &b)
{ return a.id < b.id; });
struct RpcServerRow
{
uint32_t sid = 0;
uint32_t sdPtr = 0;
bool readable = false;
t_SifRpcServerData sd{};
};
struct RpcClientRow
{
uint32_t clientPtr = 0;
bool busy = false;
uint32_t lastRpc = 0;
uint32_t sid = 0;
bool readable = false;
t_SifRpcClientData cd{};
};
std::vector<RpcServerRow> servers;
std::vector<RpcClientRow> clients;
bool rpcInitialized = false;
uint32_t rpcNextId = 0;
uint32_t rpcPacketIndex = 0;
uint32_t rpcServerIndex = 0;
uint32_t rpcActiveQueue = 0;
{
std::lock_guard<std::mutex> lock(g_rpc_mutex);
rpcInitialized = g_rpc_initialized;
rpcNextId = g_rpc_next_id;
rpcPacketIndex = g_rpc_packet_index;
rpcServerIndex = g_rpc_server_index;
rpcActiveQueue = g_rpc_active_queue;
servers.reserve(g_rpc_servers.size());
for (const auto &[sid, state] : g_rpc_servers)
{
RpcServerRow row{};
row.sid = sid;
row.sdPtr = state.sd_ptr;
row.readable = readGuestObjectNoThrow(rdram, state.sd_ptr, row.sd);
servers.push_back(row);
}
clients.reserve(g_rpc_clients.size());
for (const auto &[clientPtr, state] : g_rpc_clients)
{
RpcClientRow row{};
row.clientPtr = clientPtr;
row.busy = state.busy;
row.lastRpc = state.last_rpc;
row.sid = state.sid;
row.readable = readGuestObjectNoThrow(rdram, clientPtr, row.cd);
clients.push_back(row);
}
}
std::sort(servers.begin(), servers.end(), [](const RpcServerRow &a, const RpcServerRow &b)
{ return a.sid < b.sid; });
std::sort(clients.begin(), clients.end(), [](const RpcClientRow &a, const RpcClientRow &b)
{ return a.clientPtr < b.clientPtr; });
auto hasServer = [&](uint32_t sid)
{
return std::any_of(servers.begin(), servers.end(), [sid](const RpcServerRow &row)
{ return row.sid == sid; });
};
ImGui::SeparatorText("IOP / SIF modules");
ImGui::Text("Tracked modules: %zu loaded=%zu nextId=%d moduleLogs=%u",
modules.size(),
static_cast<size_t>(std::count_if(modules.begin(), modules.end(), [](const ModuleRow &row)
{ return row.loaded; })),
nextModuleId,
moduleLogCount);
ImGui::TextDisabled("This table is fed by SifLoadModule/SifLoadModuleBuffer/SifStopModule tracking.");
if (ImGui::BeginTable("iop_modules", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 180)))
{
ImGui::TableSetupColumn("ID");
ImGui::TableSetupColumn("Loaded");
ImGui::TableSetupColumn("Refs");
ImGui::TableSetupColumn("Key");
ImGui::TableSetupColumn("Path");
ImGui::TableHeadersRow();
for (const ModuleRow &row : modules)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%d", row.id);
ImGui::TableNextColumn();
ImGui::Text("%s", row.loaded ? "yes" : "no");
ImGui::TableNextColumn();
ImGui::Text("%u", row.refCount);
ImGui::TableNextColumn();
ImGui::TextUnformatted(row.pathKey.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(row.path.c_str());
}
ImGui::EndTable();
}
ImGui::SeparatorText("ps2xIOP HLE services");
ImGui::Text("Profile: %s provider: %s",
iopSnapshot.activeProfile.empty()
? "<core only>"
: iopSnapshot.activeProfile.c_str(),
iopSnapshot.activeProvider.empty()
? "builtin"
: iopSnapshot.activeProvider.c_str());
if (ImGui::BeginTable("iop_hle_services",
5,
ImGuiTableFlags_Borders |
ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("Service");
ImGui::TableSetupColumn("Layer");
ImGui::TableSetupColumn("SID");
ImGui::TableSetupColumn("EE server");
ImGui::TableSetupColumn("Metrics");
ImGui::TableHeadersRow();
for (const ps2x::iop::DebugService &service : iopSnapshot.services)
{
if (service.sids.empty())
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextUnformatted(service.name.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(service.profileSpecific ? "profile" : "core");
ImGui::TableNextColumn();
ImGui::TextUnformatted("-");
ImGui::TableNextColumn();
ImGui::TextUnformatted("-");
ImGui::TableNextColumn();
ImGui::Text("%zu", service.metrics.size());
continue;
}
for (const uint32_t sid : service.sids)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextUnformatted(service.name.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(service.profileSpecific ? "profile" : "core");
ImGui::TableNextColumn();
ImGui::Text("0x%08X", sid);
ImGui::TableNextColumn();
ImGui::TextUnformatted(hasServer(sid) ? "yes" : "no");
ImGui::TableNextColumn();
if (service.metrics.empty())
{
ImGui::TextUnformatted("-");
}
else
{
std::string metrics;
for (const ps2x::iop::DebugMetric &metric : service.metrics)
{
if (!metrics.empty())
{
metrics += ", ";
}
std::ostringstream value;
value << metric.name << "=";
if (metric.hexadecimal)
{
value << "0x" << std::hex << metric.value;
}
else
{
value << std::dec << metric.value;
}
metrics += value.str();
}
ImGui::TextUnformatted(metrics.c_str());
}
}
}
ImGui::EndTable();
}
for (const std::string &diagnostic : iopSnapshot.diagnostics)
{
ImGui::TextDisabled("%s", diagnostic.c_str());
}
ImGui::SeparatorText("SIF RPC state");
ImGui::Text("initialized=%u servers=%zu clients=%zu nextId=%u packetIndex=%u serverIndex=%u activeQueue=0x%08X",
rpcInitialized ? 1u : 0u,
servers.size(),
clients.size(),
rpcNextId,
rpcPacketIndex,
rpcServerIndex,
rpcActiveQueue);
if (ImGui::BeginTable("rpc_servers", 10, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 180)))
{
ImGui::TableSetupColumn("SID");
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("sd*");
ImGui::TableSetupColumn("func");
ImGui::TableSetupColumn("buf");
ImGui::TableSetupColumn("size");
ImGui::TableSetupColumn("cfunc");
ImGui::TableSetupColumn("queue");
ImGui::TableSetupColumn("recv");
ImGui::TableSetupColumn("rpc#");
ImGui::TableHeadersRow();
for (const RpcServerRow &row : servers)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.sid);
ImGui::TableNextColumn();
const std::string serviceName = iopRpcSidName(iopSnapshot, row.sid);
ImGui::TextUnformatted(serviceName.c_str());
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.sdPtr);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.readable ? row.sd.func : 0u);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.readable ? row.sd.buf : 0u);
ImGui::TableNextColumn();
ImGui::Text("%d", row.readable ? row.sd.size : 0);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.readable ? row.sd.cfunc : 0u);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.readable ? row.sd.base : 0u);
ImGui::TableNextColumn();
ImGui::Text("0x%08X/%d", row.readable ? row.sd.recvbuf : 0u, row.readable ? row.sd.rsize : 0);
ImGui::TableNextColumn();
ImGui::Text("%d", row.readable ? row.sd.rpc_number : 0);
}
ImGui::EndTable();
}
if (ImGui::BeginTable("rpc_clients", 9, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 160)))
{
ImGui::TableSetupColumn("client*");
ImGui::TableSetupColumn("SID");
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Busy");
ImGui::TableSetupColumn("LastRpc");
ImGui::TableSetupColumn("cmd");
ImGui::TableSetupColumn("buf");
ImGui::TableSetupColumn("cbuf");
ImGui::TableSetupColumn("server*");
ImGui::TableHeadersRow();
for (const RpcClientRow &row : clients)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.clientPtr);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.sid);
ImGui::TableNextColumn();
const std::string serviceName = iopRpcSidName(iopSnapshot, row.sid);
ImGui::TextUnformatted(serviceName.c_str());
ImGui::TableNextColumn();
ImGui::Text("%u", row.busy ? 1u : 0u);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.lastRpc);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.readable ? row.cd.command : 0u);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.readable ? row.cd.buf : 0u);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.readable ? row.cd.cbuf : 0u);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.readable ? row.cd.server : 0u);
}
ImGui::EndTable();
}
}
void drawRpcHistoryTab(PS2Runtime &runtime)
{
const ps2x::iop::DebugSnapshot iopSnapshot = runtime.iopDebugSnapshot();
std::vector<SifRpcDebugEvent> events;
uint64_t nextSeq = 0;
{
std::lock_guard<std::mutex> lock(g_rpc_mutex);
nextSeq = g_sif_rpc_debug_next_seq;
events.reserve(kSifRpcDebugHistoryCount);
for (size_t i = 0; i < kSifRpcDebugHistoryCount; ++i)
{
const SifRpcDebugEvent &event = g_sif_rpc_debug_history[i];
if (event.seq != 0u)
{
events.push_back(event);
}
}
}
std::sort(events.begin(), events.end(), [](const SifRpcDebugEvent &a, const SifRpcDebugEvent &b)
{ return a.seq > b.seq; });
ImGui::Text("RPC events: %zu captured, nextSeq=%llu", events.size(), static_cast<unsigned long long>(nextSeq));
ImGui::TextDisabled("Ring buffer fed by SifInitRpc/SifBindRpc/SifRegisterRpc/SifCallRpc.");
static bool onlyCalls = false;
static bool hideBindNoise = false;
ImGui::Checkbox("Only CallRpc", &onlyCalls);
ImGui::SameLine();
ImGui::Checkbox("Hide bind/register/init", &hideBindNoise);
ImGui::SeparatorText("IOP/RPC tracer");
ImGui::TextDisabled("Unhandled rows are RPC calls that no HLE service or EE server callback consumed; the runtime used copy/zero fallback.");
static bool tracerOnlyUnhandled = true;
ImGui::Checkbox("Only unhandled", &tracerOnlyUnhandled);
if (ImGui::BeginTable("iop_rpc_tracer", 9,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable |
ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp,
ImVec2(0, 220)))
{
ImGui::TableSetupColumn("Seq");
ImGui::TableSetupColumn("SID");
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Rpc#");
ImGui::TableSetupColumn("PC");
ImGui::TableSetupColumn("Send");
ImGui::TableSetupColumn("Recv");
ImGui::TableSetupColumn("Send[0..15]");
ImGui::TableSetupColumn("Recv[0..15]");
ImGui::TableHeadersRow();
for (const SifRpcDebugEvent &event : events)
{
const char *op = event.op ? event.op : "";
if (std::strcmp(op, "CallRpc") != 0)
{
continue;
}
const bool unhandled = (event.flags & kSifRpcDebugFlagUnhandled) != 0u;
if (tracerOnlyUnhandled && !unhandled)
{
continue;
}
const std::string sendPreview = rpcPreviewBytes(event.sendPreview, event.sendPreviewSize);
const std::string recvPreview = rpcPreviewBytes(event.recvPreview, event.recvPreviewSize);
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%llu", static_cast<unsigned long long>(event.seq));
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.sid);
ImGui::TableNextColumn();
const std::string serviceName = iopRpcSidName(iopSnapshot, event.sid);
ImGui::TextUnformatted(serviceName.c_str());
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.rpcNum);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.pc);
ImGui::TableNextColumn();
ImGui::Text("0x%08X/%u", event.sendBuf, event.sendSize);
ImGui::TableNextColumn();
ImGui::Text("0x%08X/%u", event.recvBuf, event.recvSize);
ImGui::TableNextColumn();
ImGui::TextUnformatted(sendPreview.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(recvPreview.c_str());
}
ImGui::EndTable();
}
if (ImGui::BeginTable("rpc_history", 20,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable |
ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp,
ImVec2(0, 360)))
{
ImGui::TableSetupColumn("Seq");
ImGui::TableSetupColumn("Op");
ImGui::TableSetupColumn("TID");
ImGui::TableSetupColumn("PC");
ImGui::TableSetupColumn("RA");
ImGui::TableSetupColumn("SID");
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Rpc#");
ImGui::TableSetupColumn("Client");
ImGui::TableSetupColumn("Server");
ImGui::TableSetupColumn("Send");
ImGui::TableSetupColumn("SSize");
ImGui::TableSetupColumn("Recv");
ImGui::TableSetupColumn("RSize");
ImGui::TableSetupColumn("ResultPtr");
ImGui::TableSetupColumn("Mode");
ImGui::TableSetupColumn("EndFunc");
ImGui::TableSetupColumn("EndParam");
ImGui::TableSetupColumn("Sema");
ImGui::TableSetupColumn("Flags");
ImGui::TableHeadersRow();
for (const SifRpcDebugEvent &event : events)
{
const char *op = event.op ? event.op : "";
if (onlyCalls && std::strcmp(op, "CallRpc") != 0)
{
continue;
}
if (hideBindNoise && std::strcmp(op, "CallRpc") != 0)
{
continue;
}
const std::string flags = rpcDebugFlags(event.flags);
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%llu", static_cast<unsigned long long>(event.seq));
ImGui::TableNextColumn();
ImGui::TextUnformatted(op);
ImGui::TableNextColumn();
ImGui::Text("%u", event.threadId);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.pc);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.ra);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.sid);
ImGui::TableNextColumn();
const std::string serviceName = iopRpcSidName(iopSnapshot, event.sid);
ImGui::TextUnformatted(serviceName.c_str());
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.rpcNum);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.clientPtr);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.serverPtr);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.sendBuf);
ImGui::TableNextColumn();
ImGui::Text("%u", event.sendSize);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.recvBuf);
ImGui::TableNextColumn();
ImGui::Text("%u", event.recvSize);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.resultPtr);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.mode);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.endFunc);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.endParam);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", event.semaId);
ImGui::TableNextColumn();
ImGui::TextUnformatted(flags.c_str());
}
ImGui::EndTable();
}
}
void drawPadBytes(const uint8_t *data, size_t size)
{
char bytes[3 * 32 + 1]{};
const size_t count = std::min<size_t>(size, 32u);
for (size_t i = 0; i < count; ++i)
{
std::snprintf(bytes + (i * 3), sizeof(bytes) - (i * 3), "%02X ", data[i]);
}
ImGui::TextUnformatted(bytes);
}
void drawPadTab()
{
const ps2_stubs::PadDebugSnapshot snapshot = ps2_stubs::getPadDebugSnapshot();
ImGui::SeparatorText("PAD global state");
ImGui::Text("override=%u readLogCount=%d", snapshot.overrideEnabled ? 1u : 0u, snapshot.readLogCount);
ImGui::SameLine();
ImGui::Text("override buttons=0x%04X lx=%u ly=%u rx=%u ry=%u",
snapshot.overrideButtons,
snapshot.overrideLx,
snapshot.overrideLy,
snapshot.overrideRx,
snapshot.overrideRy);
ImGui::Text("override pressed: %s", pressedPadButtons(snapshot.overrideButtons).c_str());
if (ImGui::BeginTable("pad_ports", 15, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("Port");
ImGui::TableSetupColumn("Slot");
ImGui::TableSetupColumn("Open");
ImGui::TableSetupColumn("Mode");
ImGui::TableSetupColumn("Pressure");
ImGui::TableSetupColumn("Req");
ImGui::TableSetupColumn("DMA");
ImGui::TableSetupColumn("Read#");
ImGui::TableSetupColumn("ReadAddr");
ImGui::TableSetupColumn("Source");
ImGui::TableSetupColumn("Buttons raw");
ImGui::TableSetupColumn("Pressed");
ImGui::TableSetupColumn("LX/LY");
ImGui::TableSetupColumn("RX/RY");
ImGui::TableSetupColumn("Mask");
ImGui::TableHeadersRow();
for (size_t port = 0; port < ps2_stubs::kPadDebugPortCount; ++port)
{
for (size_t slot = 0; slot < ps2_stubs::kPadDebugSlotCount; ++slot)
{
const ps2_stubs::PadDebugPortSnapshot &row = snapshot.ports[port][slot];
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%zu", port);
ImGui::TableNextColumn();
ImGui::Text("%zu", slot);
ImGui::TableNextColumn();
ImGui::Text("%u", row.open ? 1u : 0u);
ImGui::TableNextColumn();
ImGui::TextUnformatted(row.analogMode ? "analog" : "digital");
ImGui::TableNextColumn();
ImGui::Text("%u", row.pressureEnabled ? 1u : 0u);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.reqState);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.dmaAddr);
ImGui::TableNextColumn();
ImGui::Text("%u", row.readCount);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.lastReadDataAddr);
ImGui::TableNextColumn();
ImGui::TextUnformatted(row.lastUsedOverride ? "override" : (row.lastUsedBackend ? "backend" : "fallback"));
ImGui::TableNextColumn();
ImGui::Text("0x%04X", row.lastButtons);
ImGui::TableNextColumn();
ImGui::TextUnformatted(pressedPadButtons(row.lastButtons).c_str());
ImGui::TableNextColumn();
ImGui::Text("%u/%u", row.lx, row.ly);
ImGui::TableNextColumn();
ImGui::Text("%u/%u", row.rx, row.ry);
ImGui::TableNextColumn();
ImGui::Text("0x%04X", row.buttonMask);
}
}
ImGui::EndTable();
}
ImGui::SeparatorText("Last scePadRead bytes");
for (size_t port = 0; port < ps2_stubs::kPadDebugPortCount; ++port)
{
for (size_t slot = 0; slot < ps2_stubs::kPadDebugSlotCount; ++slot)
{
const ps2_stubs::PadDebugPortSnapshot &row = snapshot.ports[port][slot];
ImGui::Text("port=%zu slot=%zu ok=%u data:", port, slot, row.lastReadOk ? 1u : 0u);
ImGui::SameLine();
drawPadBytes(row.lastData, ps2_stubs::kPadDebugDataSize);
}
}
ImGui::SeparatorText("PS2 active-low button layout");
ImGui::TextUnformatted("byte[2:3] raw is active-low: 0 means pressed, 1 means released.");
ImGui::TextUnformatted("bits: Select,L3,R3,Start,Up,Right,Down,Left,L2,R2,L1,R1,Triangle,Circle,Cross,Square");
}
void drawGsTab(PS2Runtime &runtime)
{
GSRegisters &regs = runtime.memory().gs();
const GSDebugSnapshot gs = runtime.gs().getDebugSnapshot();
ImGui::SeparatorText("GS private registers");
if (ImGui::BeginTable("gspriv", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
auto row64 = [](const char *name, uint64_t value)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextUnformatted(name);
ImGui::TableNextColumn();
ImGui::Text("0x%016llX", static_cast<unsigned long long>(value));
ImGui::TableNextColumn();
ImGui::Text("%llu", static_cast<unsigned long long>(value));
ImGui::TableNextColumn();
ImGui::Text("lo=0x%08X hi=0x%08X", static_cast<uint32_t>(value), static_cast<uint32_t>(value >> 32));
};
ImGui::TableSetupColumn("Reg");
ImGui::TableSetupColumn("Hex64");
ImGui::TableSetupColumn("Dec");
ImGui::TableSetupColumn("Split");
ImGui::TableHeadersRow();
row64("PMODE", regs.pmode);
row64("SMODE2", regs.smode2);
row64("DISPFB1", regs.dispfb1);
row64("DISPLAY1", regs.display1);
row64("DISPFB2", regs.dispfb2);
row64("DISPLAY2", regs.display2);
row64("BGCOLOR", regs.bgcolor);
row64("CSR", regs.csr.load());
row64("IMR", regs.imr);
row64("BUSDIR", regs.busdir);
row64("SIGLBLID", regs.siglblid);
ImGui::EndTable();
}
ImGui::SeparatorText("GS draw state");
ImGui::Text("PRIM type=%s iip=%u tme=%u fge=%u abe=%u aa1=%u fst=%u ctxt=%u fix=%u",
primTypeName(gs.prim.type), gs.prim.iip, gs.prim.tme, gs.prim.fge, gs.prim.abe,
gs.prim.aa1, gs.prim.fst, gs.prim.ctxt, gs.prim.fix);
ImGui::Text("TEXA ta0=0x%02X aem=%u ta1=0x%02X | TEXCLUT cbw=%u cou=%u cov=%u",
gs.texa.ta0, gs.texa.aem ? 1u : 0u, gs.texa.ta1, gs.texclut.cbw, gs.texclut.cou, gs.texclut.cov);
ImGui::Text("BITBLT sbp=%u sbw=%u spsm=0x%02X dbp=%u dbw=%u dpsm=0x%02X",
gs.bitbltbuf.sbp, gs.bitbltbuf.sbw, gs.bitbltbuf.spsm, gs.bitbltbuf.dbp, gs.bitbltbuf.dbw, gs.bitbltbuf.dpsm);
ImGui::Text("TRXPOS ss=(%u,%u) ds=(%u,%u) dir=%u | TRXREG %ux%u TRXDIR=%u copied=%u/%u at=(%u,%u)",
gs.trxpos.ssax, gs.trxpos.ssay, gs.trxpos.dsax, gs.trxpos.dsay, gs.trxpos.dir,
gs.trxreg.rrw, gs.trxreg.rrh, gs.trxdir, gs.transferCopiedPixels, gs.transferTotalPixels,
gs.transferX, gs.transferY);
ImGui::Text("Host presentation: has=%u size=%ux%u displayFbp=%u sourceFbp=%u preferred=%u lastDisplayBaseBytes=0x%08X localToHostPending=%zu",
gs.hasHostPresentationFrame ? 1u : 0u,
gs.hostPresentationWidth,
gs.hostPresentationHeight,
gs.hostPresentationDisplayFbp,
gs.hostPresentationSourceFbp,
gs.hostPresentationUsedPreferred ? 1u : 0u,
gs.lastDisplayBaseBytes,
gs.localToHostPendingBytes);
ImGui::Text("Preferred source: has=%u frame fbp=%u fbw=%u psm=0x%02X destFbp=%u",
gs.hasPreferredDisplaySource ? 1u : 0u,
gs.preferredDisplaySourceFrame.fbp,
gs.preferredDisplaySourceFrame.fbw,
gs.preferredDisplaySourceFrame.psm,
gs.preferredDisplayDestFbp);
drawGsContext("Context 0", gs.ctx[0]);
drawGsContext("Context 1", gs.ctx[1]);
ImGui::SeparatorText("GS history");
bool gsHistoryPaused = runtime.gs().isDebugHistoryPaused();
std::vector<GSDebugHistoryEntry> history = runtime.gs().getDebugHistory();
uint32_t latestFrame = 0u;
if (!history.empty())
{
latestFrame = history.back().frameIndex;
}
static bool s_gsHistoryCurrentFrameOnly = false;
static bool s_gsHistoryDrawsOnly = false;
static std::string s_lastGsDumpPath;
static std::string s_lastGsDumpError;
ImGui::Text("Ring entries: %zu / 512%s", history.size(), gsHistoryPaused ? " (paused)" : "");
ImGui::SameLine();
if (ImGui::Button("Clear GS history"))
{
runtime.gs().clearDebugHistory();
history.clear();
latestFrame = 0u;
}
ImGui::SameLine();
if (ImGui::Checkbox("Pause capture", &gsHistoryPaused))
{
runtime.gs().setDebugHistoryPaused(gsHistoryPaused);
}
ImGui::SameLine();
if (ImGui::Button("Dump GS TXT"))
{
s_lastGsDumpError.clear();
const std::filesystem::path dumpPath = makeDebugDumpPath("gs_history", latestFrame);
if (writeGsDebugDump(dumpPath, regs, gs, history, s_gsHistoryCurrentFrameOnly, s_gsHistoryDrawsOnly, latestFrame, s_lastGsDumpError))
{
s_lastGsDumpPath = dumpPath.string();
ImGui::SetClipboardText(s_lastGsDumpPath.c_str());
}
else
{
s_lastGsDumpPath.clear();
}
}
if (gsHistoryPaused)
{
ImGui::SameLine();
ImGui::TextDisabled("history capture stopped");
}
ImGui::Checkbox("Current frame only", &s_gsHistoryCurrentFrameOnly);
ImGui::SameLine();
ImGui::Checkbox("Draws only", &s_gsHistoryDrawsOnly);
if (!s_lastGsDumpPath.empty())
{
ImGui::TextDisabled("Last dump copied to clipboard: %s", s_lastGsDumpPath.c_str());
}
if (!s_lastGsDumpError.empty())
{
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Dump failed: %s", s_lastGsDumpError.c_str());
}
if (ImGui::BeginTable("gs_history", 16, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX, ImVec2(0, 260)))
{
ImGui::TableSetupColumn("Seq");
ImGui::TableSetupColumn("Frame");
ImGui::TableSetupColumn("Tick");
ImGui::TableSetupColumn("Kind");
ImGui::TableSetupColumn("Prim");
ImGui::TableSetupColumn("Ctx");
ImGui::TableSetupColumn("FBP/PSM");
ImGui::TableSetupColumn("ZBP/PSM");
ImGui::TableSetupColumn("TEST");
ImGui::TableSetupColumn("ALPHA");
ImGui::TableSetupColumn("TEX0");
ImGui::TableSetupColumn("XY");
ImGui::TableSetupColumn("Z");
ImGui::TableSetupColumn("A");
ImGui::TableSetupColumn("GIF/REG/XFER");
ImGui::TableSetupColumn("Present");
ImGui::TableHeadersRow();
for (auto it = history.rbegin(); it != history.rend(); ++it)
{
const GSDebugHistoryEntry &row = *it;
if (s_gsHistoryCurrentFrameOnly && row.frameIndex != latestFrame)
{
continue;
}
if (s_gsHistoryDrawsOnly && row.kind != GSDebugEventKind::Draw)
{
continue;
}
const uint64_t test = row.test;
const uint64_t alpha = row.alpha;
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%llu", static_cast<unsigned long long>(row.seq));
ImGui::TableNextColumn();
ImGui::Text("%u", row.frameIndex);
ImGui::TableNextColumn();
ImGui::Text("%llu", static_cast<unsigned long long>(row.vsyncTick));
ImGui::TableNextColumn();
ImGui::TextUnformatted(gsDebugEventKindName(row.kind));
ImGui::TableNextColumn();
ImGui::Text("%s", primTypeName(row.prim.type));
ImGui::TableNextColumn();
ImGui::Text("%u", row.prim.ctxt ? 1u : 0u);
ImGui::TableNextColumn();
ImGui::Text("%u/%u/0x%02X", row.frame.fbp, row.frame.fbw, row.frame.psm);
ImGui::TableNextColumn();
ImGui::Text("%u/0x%02X m=%u", row.zbuf.zbp, row.zbuf.psm, row.zbuf.zmask ? 1u : 0u);
ImGui::TableNextColumn();
ImGui::Text("ATE=%llu ATST=%llu AREF=%02llX AFAIL=%llu ZTE=%llu ZTST=%llu",
(test >> 0) & 1ull,
(test >> 1) & 7ull,
(test >> 4) & 0xffull,
(test >> 12) & 3ull,
(test >> 16) & 1ull,
(test >> 17) & 3ull);
ImGui::TableNextColumn();
ImGui::Text("0x%016llX", static_cast<unsigned long long>(alpha));
ImGui::TableNextColumn();
ImGui::Text("tbp=%u psm=0x%02X %ux%u cbp=%u cpsm=0x%02X",
row.tex0.tbp0,
row.tex0.psm,
1u << row.tex0.tw,
1u << row.tex0.th,
row.tex0.cbp,
row.tex0.cpsm);
ImGui::TableNextColumn();
if (row.kind == GSDebugEventKind::Draw)
ImGui::Text("%.1f..%.1f / %.1f..%.1f", row.xMin, row.xMax, row.yMin, row.yMax);
else
ImGui::TextUnformatted("-");
ImGui::TableNextColumn();
if (row.kind == GSDebugEventKind::Draw)
ImGui::Text("%.0f..%.0f", row.zMin, row.zMax);
else
ImGui::TextUnformatted("-");
ImGui::TableNextColumn();
if (row.kind == GSDebugEventKind::Draw)
ImGui::Text("%u..%u", row.aMin, row.aMax);
else
ImGui::TextUnformatted("-");
ImGui::TableNextColumn();
if (row.kind == GSDebugEventKind::GifTag)
{
ImGui::Text("%s nloop=%u nreg=%u size=%u", gifFormatName(row.gifFlg), row.gifNloop, row.gifNreg, row.gifSizeBytes);
}
else if (row.kind == GSDebugEventKind::Register)
{
ImGui::Text("%s(0x%02X)=0x%016llX", gsRegName(row.reg), row.reg, static_cast<unsigned long long>(row.regValue));
}
else if (row.kind == GSDebugEventKind::Transfer)
{
ImGui::Text("TRXDIR=%u %ux%u pix=%u sbp=%u dbp=%u", row.trxdir, row.trxreg.rrw, row.trxreg.rrh, row.transferPixels, row.bitbltbuf.sbp, row.bitbltbuf.dbp);
}
else
{
ImGui::TextUnformatted("-");
}
ImGui::TableNextColumn();
if (row.kind == GSDebugEventKind::Present)
{
ImGui::Text("%ux%u display=%u source=%u pref=%u", row.width, row.height, row.displayFbp, row.sourceFbp, row.usedPreferred ? 1u : 0u);
}
else
{
ImGui::TextUnformatted("-");
}
}
ImGui::EndTable();
}
}
void drawFileCdTab(PS2Runtime &runtime)
{
(void)runtime;
const PS2Runtime::IoPaths &ioPaths = PS2Runtime::getIoPaths();
ImGui::SeparatorText("Runtime IO paths");
textPath("ELF", ioPaths.elfPath);
textPath("ELF dir", ioPaths.elfDirectory);
textPath("Host root", ioPaths.hostRoot);
textPath("CD root", ioPaths.cdRoot);
textPath("CD image", ioPaths.cdImage);
textPath("MC root", ioPaths.mcRoot);
bool pathsInitialized = false;
std::filesystem::path hostBase;
std::filesystem::path cdromBase;
std::filesystem::path hostCwd;
std::filesystem::path cdromCwd;
std::string cwdDevice;
{
std::lock_guard<std::mutex> lock(g_ps2_path_mutex);
pathsInitialized = g_ps2_paths_initialized;
hostBase = g_host_base;
cdromBase = g_cdrom_base;
hostCwd = g_host_cwd;
cdromCwd = g_cdrom_cwd;
cwdDevice = g_ps2_cwd_device;
}
ImGui::SeparatorText("PS2 path resolver");
ImGui::Text("initialized=%u cwdDevice=%s", pathsInitialized ? 1u : 0u, cwdDevice.c_str());
textPath("host base", hostBase);
textPath("cdrom base", cdromBase);
textPath("host cwd", hostCwd);
textPath("cdrom cwd", cdromCwd);
struct FdRow
{
int fd = 0;
FILE *file = nullptr;
};
std::vector<FdRow> fds;
{
std::lock_guard<std::mutex> lock(g_fd_mutex);
fds.reserve(g_fileDescriptors.size());
for (const auto &[fd, file] : g_fileDescriptors)
{
fds.push_back(FdRow{fd, file});
}
}
std::sort(fds.begin(), fds.end(), [](const FdRow &a, const FdRow &b)
{ return a.fd < b.fd; });
ImGui::SeparatorText("FileIO descriptors");
ImGui::Text("Open host FILE* descriptors: %zu", fds.size());
if (ImGui::BeginTable("fileio_fds", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable, ImVec2(0, 90)))
{
ImGui::TableSetupColumn("FD");
ImGui::TableSetupColumn("FILE*");
ImGui::TableHeadersRow();
for (const FdRow &row : fds)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%d", row.fd);
ImGui::TableNextColumn();
ImGui::Text("%p", static_cast<void *>(row.file));
}
ImGui::EndTable();
}
const ps2_stubs::CdDebugSnapshot cd = ps2_stubs::getCdDebugSnapshot();
ImGui::SeparatorText("CDVD / sceCd state");
ImGui::Text("initialized=%u lastError=%d mode=0x%08X streamingLbn=0x%08X endLbn=0x%08X nextPseudoLbn=0x%08X",
cd.initialized ? 1u : 0u,
cd.lastError,
cd.mode,
cd.streamingLbn,
cd.streamingEndLbn,
cd.nextPseudoLbn);
ImGui::Text("imageValid=%u imageSize=%llu leafIndexBuilt=%u leafIndex=%zu looseIndex=%zu registeredFiles=%zu",
cd.imageSizeValid ? 1u : 0u,
static_cast<unsigned long long>(cd.imageSizeBytes),
cd.leafIndexBuilt ? 1u : 0u,
cd.leafIndexCount,
cd.loosePathIndexCount,
cd.files.size());
textPath("CD resolved root", cd.cdRoot);
textPath("CD image path", cd.cdImage);
textPath("CD image size path", cd.imageSizePath);
textPath("CD leaf index root", cd.leafIndexRoot);
if (ImGui::BeginTable("cd_files", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 180)))
{
ImGui::TableSetupColumn("Key");
ImGui::TableSetupColumn("Base LBN");
ImGui::TableSetupColumn("End LBN");
ImGui::TableSetupColumn("Sectors");
ImGui::TableSetupColumn("Bytes");
ImGui::TableSetupColumn("Host path");
ImGui::TableHeadersRow();
for (const ps2_stubs::CdDebugFileEntry &row : cd.files)
{
const uint32_t endLbn = row.baseLbn + row.sectors;
const std::string hostPath = pathToString(row.hostPath);
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextUnformatted(row.key.c_str());
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.baseLbn);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", endLbn);
ImGui::TableNextColumn();
ImGui::Text("%u", row.sectors);
ImGui::TableNextColumn();
ImGui::Text("%u", row.sizeBytes);
ImGui::TableNextColumn();
ImGui::TextUnformatted(hostPath.c_str());
}
ImGui::EndTable();
}
const ps2_stubs::MemoryCardDebugSnapshot mc = ps2_stubs::getMemoryCardDebugSnapshot();
ImGui::SeparatorText("Memory card state");
ImGui::Text("lastCmd=0x%X lastResult=%d nextFd=%d cvCursor=%d openFiles=%zu",
mc.lastCmd,
mc.lastResult,
mc.nextFd,
mc.cvFileCursor,
mc.openFiles.size());
if (ImGui::BeginTable("mc_ports", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("Port");
ImGui::TableSetupColumn("Formatted");
ImGui::TableSetupColumn("Current dir");
ImGui::TableSetupColumn("Host root");
ImGui::TableHeadersRow();
for (const ps2_stubs::MemoryCardDebugPort &port : mc.ports)
{
const std::string root = pathToString(port.rootPath);
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%d", port.port);
ImGui::TableNextColumn();
ImGui::Text("%u", port.formatted ? 1u : 0u);
ImGui::TableNextColumn();
ImGui::TextUnformatted(port.currentDir.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(root.c_str());
}
ImGui::EndTable();
}
if (ImGui::BeginTable("mc_open_files", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 120)))
{
ImGui::TableSetupColumn("FD");
ImGui::TableSetupColumn("Port");
ImGui::TableSetupColumn("Host path");
ImGui::TableHeadersRow();
for (const ps2_stubs::MemoryCardDebugOpenFile &row : mc.openFiles)
{
const std::string path = pathToString(row.hostPath);
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%d", row.fd);
ImGui::TableNextColumn();
ImGui::Text("%d", row.port);
ImGui::TableNextColumn();
ImGui::TextUnformatted(path.c_str());
}
ImGui::EndTable();
}
}
void drawIoTab(PS2Runtime &runtime)
{
PS2Memory &mem = runtime.memory();
ImGui::SeparatorText("DMA/VIF/GIF IO registers");
struct RegRow
{
const char *name;
uint32_t addr;
};
static constexpr RegRow rows[] = {
{"D_STAT", 0x1000e010u},
{"VIF0_STAT", 0x10003800u},
{"VIF0_FBRST", 0x10003810u},
{"VIF0_ERR", 0x10003830u},
{"VIF1_STAT", 0x10003c00u},
{"VIF1_FBRST", 0x10003c10u},
{"VIF1_ERR", 0x10003c30u},
{"VIF0_CHCR", 0x10008000u},
{"VIF0_MADR", 0x10008010u},
{"VIF0_QWC", 0x10008020u},
{"VIF0_TADR", 0x10008030u},
{"VIF1_CHCR", 0x10009000u},
{"VIF1_MADR", 0x10009010u},
{"VIF1_QWC", 0x10009020u},
{"VIF1_TADR", 0x10009030u},
{"GIF_CHCR", 0x1000a000u},
{"GIF_MADR", 0x1000a010u},
{"GIF_QWC", 0x1000a020u},
{"GIF_TADR", 0x1000a030u},
{"IPU_FROM_CHCR", 0x1000b000u},
{"IPU_TO_CHCR", 0x1000b400u},
};
if (ImGui::BeginTable("ioregs", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Addr");
ImGui::TableSetupColumn("Value");
ImGui::TableHeadersRow();
for (const auto &row : rows)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextUnformatted(row.name);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", row.addr);
ImGui::TableNextColumn();
ImGui::Text("0x%08X", mem.readIORegister(row.addr));
}
ImGui::EndTable();
}
}
bool writeRuntimeLogDump(const std::filesystem::path &path,
const std::vector<ps2_log::RuntimeLogEntry> &entries,
ImGuiTextFilter *filter,
std::string &error)
{
try
{
std::ofstream out(path, std::ios::out | std::ios::trunc);
if (!out.is_open())
{
error = "failed to open dump path";
return false;
}
out << "PS2 runtime log dump\n";
out << "entries=" << entries.size() << "\n";
out << "filter=" << (filter && filter->IsActive() ? filter->InputBuf : "<none>") << "\n";
out << "ps2_log_txt=" << ps2_log::log_path() << "\n";
out << "\n";
out << "seq\ttext\n";
for (const ps2_log::RuntimeLogEntry &entry : entries)
{
if (filter && filter->IsActive() && !filter->PassFilter(entry.text.c_str()))
{
continue;
}
out << entry.seq << '\t' << entry.text;
if (entry.text.empty() || entry.text.back() != '\n')
{
out << '\n';
}
}
return true;
}
catch (const std::exception &e)
{
error = e.what();
return false;
}
}
void drawLogsTab()
{
static ImGuiTextFilter s_logFilter;
static bool s_autoScroll = true;
static bool s_wrapText = false;
static bool s_latestFirst = false;
static std::string s_lastLogDumpPath;
static std::string s_lastLogDumpError;
bool paused = ps2_log::is_runtime_log_paused();
std::vector<ps2_log::RuntimeLogEntry> entries = ps2_log::snapshot_runtime_log_entries();
ImGui::SeparatorText("Runtime log ring buffer");
ImGui::Text("Entries: %zu / %zu%s", entries.size(), ps2_log::kMaxRuntimeLogEntries, paused ? " (paused)" : "");
ImGui::TextDisabled("Captures RUNTIME_LOG(...) calls. Direct std::cerr/std::cout writes are still console-only unless routed through RUNTIME_LOG.");
ImGui::TextDisabled("Aggressive call trace file: %s", ps2_log::log_path().c_str());
if (ImGui::Button("Clear logs"))
{
ps2_log::clear_runtime_log_entries();
entries.clear();
}
ImGui::SameLine();
if (ImGui::Checkbox("Pause capture", &paused))
{
ps2_log::set_runtime_log_paused(paused);
}
ImGui::SameLine();
ImGui::Checkbox("Auto-scroll", &s_autoScroll);
ImGui::SameLine();
ImGui::Checkbox("Wrap", &s_wrapText);
ImGui::SameLine();
ImGui::Checkbox("Latest first", &s_latestFirst);
s_logFilter.Draw("Filter", 260.0f);
ImGui::SameLine();
if (ImGui::Button("Copy visible"))
{
std::ostringstream out;
auto appendEntry = [&](const ps2_log::RuntimeLogEntry &entry)
{
if (s_logFilter.IsActive() && !s_logFilter.PassFilter(entry.text.c_str()))
{
return;
}
out << entry.seq << '\t' << entry.text;
if (entry.text.empty() || entry.text.back() != '\n')
{
out << '\n';
}
};
if (s_latestFirst)
{
for (auto it = entries.rbegin(); it != entries.rend(); ++it)
{
appendEntry(*it);
}
}
else
{
for (const ps2_log::RuntimeLogEntry &entry : entries)
{
appendEntry(entry);
}
}
const std::string text = out.str();
ImGui::SetClipboardText(text.c_str());
}
ImGui::SameLine();
if (ImGui::Button("Dump logs TXT"))
{
s_lastLogDumpError.clear();
const std::filesystem::path dumpPath = makeDebugDumpPath("runtime_log", 0u);
if (writeRuntimeLogDump(dumpPath, entries, &s_logFilter, s_lastLogDumpError))
{
s_lastLogDumpPath = dumpPath.string();
ImGui::SetClipboardText(s_lastLogDumpPath.c_str());
}
else
{
s_lastLogDumpPath.clear();
}
}
if (!s_lastLogDumpPath.empty())
{
ImGui::TextDisabled("Last dump: %s", s_lastLogDumpPath.c_str());
}
if (!s_lastLogDumpError.empty())
{
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Dump failed: %s", s_lastLogDumpError.c_str());
}
ImGui::Separator();
const ImGuiTableFlags tableFlags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX;
if (ImGui::BeginTable("runtime_logs", 2, tableFlags, ImVec2(0, 430)))
{
ImGui::TableSetupColumn("Seq", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableSetupColumn("Text", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
auto drawEntry = [&](const ps2_log::RuntimeLogEntry &entry)
{
if (s_logFilter.IsActive() && !s_logFilter.PassFilter(entry.text.c_str()))
{
return;
}
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%llu", static_cast<unsigned long long>(entry.seq));
ImGui::TableNextColumn();
if (s_wrapText)
{
ImGui::TextWrapped("%s", entry.text.c_str());
}
else
{
ImGui::TextUnformatted(entry.text.c_str());
}
};
if (s_latestFirst)
{
for (auto it = entries.rbegin(); it != entries.rend(); ++it)
{
drawEntry(*it);
}
}
else
{
for (const ps2_log::RuntimeLogEntry &entry : entries)
{
drawEntry(entry);
}
}
if (s_autoScroll && !s_latestFirst && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 4.0f)
{
ImGui::SetScrollHereY(1.0f);
}
ImGui::EndTable();
}
}
#endif
}
void PS2DebugPanel::initialize()
{
#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA)
if (!m_initialized)
{
rlImGuiSetup(true);
m_initialized = true;
}
#endif
}
void PS2DebugPanel::shutdown()
{
#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA)
if (m_initialized)
{
rlImGuiShutdown();
m_initialized = false;
}
#endif
}
void PS2DebugPanel::draw(PS2Runtime &runtime)
{
#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA)
if (!m_initialized)
{
return;
}
if (IsKeyPressed(KEY_F1))
{
toggleVisible();
}
if (!m_visible)
{
return;
}
rlImGuiBegin();
ImGui::SetNextWindowSize(ImVec2(780.0f, 620.0f), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Runtime Debugger", &m_visible, ImGuiWindowFlags_MenuBar))
{
if (ImGui::BeginMenuBar())
{
ImGui::MenuItem("Registers", nullptr, &m_showRegisters);
ImGui::EndMenuBar();
}
if (ImGui::BeginTabBar("debug-tabs"))
{
if (ImGui::BeginTabItem("CPU"))
{
drawCpuTab(runtime, m_showRegisters);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Threads"))
{
drawThreadsTab(runtime);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Kernel"))
{
drawKernelTab(runtime);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("IOP/SIF"))
{
drawIopTab(runtime);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("RPC History"))
{
drawRpcHistoryTab(runtime);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("PAD"))
{
drawPadTab();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("File/CD"))
{
drawFileCdTab(runtime);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("GS"))
{
drawGsTab(runtime);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("DMA/IO"))
{
drawIoTab(runtime);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Logs"))
{
drawLogsTab();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Memory"))
{
uint32_t bytes = m_memoryBytes;
ImGui::InputScalar("Address", ImGuiDataType_U32, &m_memoryAddress, nullptr, nullptr, "%08X", ImGuiInputTextFlags_CharsHexadecimal);
ImGui::InputScalar("Bytes", ImGuiDataType_U32, &bytes, nullptr, nullptr, "%u");
m_memoryBytes = std::clamp<unsigned int>(bytes, 16u, 0x1000u);
ImGui::SameLine();
if (ImGui::Button("PC"))
m_memoryAddress = runtime.m_debugPc.load(std::memory_order_relaxed);
ImGui::SameLine();
if (ImGui::Button("SP"))
m_memoryAddress = runtime.m_debugSp.load(std::memory_order_relaxed);
ImGui::SameLine();
if (ImGui::Button("GP"))
m_memoryAddress = runtime.m_debugGp.load(std::memory_order_relaxed);
ImGui::Separator();
drawHexDump(runtime.memory().getRDRAM(), m_memoryAddress, m_memoryBytes);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::End();
rlImGuiEnd();
#else
(void)runtime;
#endif
}