Merge remote-tracking branch 'origin/master' into landtmp/493

This commit is contained in:
Matthew Stanley
2026-10-05 13:23:22 -07:00
13 changed files with 638 additions and 5 deletions
+6 -2
View File
@@ -431,9 +431,9 @@ The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` i
## Complete command index (generated)
**334 commands registered** — 321 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
**335 commands registered** — 322 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
63 of 334 have prose above; **271 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
63 of 335 have prose above; **272 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
@@ -506,6 +506,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `dirty_insn_log` | ✓ | | |
| `dirty_ram_stats` | ✓ | | ✓ |
| `dirty_ram_unsupported` | ✓ | | |
| `disasm` | ✓ | | |
| `disc_select` | ✓ | | |
| `disp_ring` | ✓ | | |
| `dispatch_check` | ✓ | | |
@@ -775,3 +776,6 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `xprobe_watch` | ✓ | | |
<!-- END AUTOGENERATED COMMAND INDEX -->
`wtrace_dump pc_lo=<hex> pc_hi=<hex>` filters the recorded producer PC, masked to the physical address (KSEG0/KSEG1/KUSEG alias, like `addr_lo`/`addr_hi`), with inclusive lower and exclusive upper bounds, before limiting results. Omitted bounds match every entry; DMA entries use their recorded initiator, and a DMA whose initiator is unknown (0) never matches an explicit PC bound. Address/frame/order filters still apply.
`disasm addr=<hex> count=<1..256>` inspects aligned guest instructions; default count is 16. Resolved targets use the guest PC. Unknown words are shown verbatim.
@@ -0,0 +1,43 @@
# TCP guest instruction disassembly: source and validation
Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on
`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`.
Exact source commits:
- [85c47e10](https://github.com/FractalEngineer/psxrecomp/commit/85c47e10a2fa6e112205e0e953a89ba120c4b139)
- [d58db909](https://github.com/FractalEngineer/psxrecomp/commit/d58db9092a57cc692d0c9d936f5003fb4b3e0977)
- [c2ed6e55](https://github.com/FractalEngineer/psxrecomp/commit/c2ed6e558f37212c36aedfdfa2c90863883f0aff)
Original author: Yves <calibratedbeats@gmail.com>. The feature commit retains
that exact human author identity. The focused port consolidates the selected
source hunks and corrections. Its commit also cites these sources and carries
the original human contributor's `Co-authored-by: Yves <calibratedbeats@gmail.com>` trailer.
Adapted: The C-compatible bridge to the existing MIPS decoder, bounded TCP handler, build linkage and command docs. Formatting was corrected and covered during reconstruction.
Deliberately excluded: Guest instruction execution/codegen changes, new decoders, game-specific addresses and runtime printf debugging.
The feature was reconstructed on upstream master
`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs,
renderer/input policy and build integration. Validation after reconstruction:
Disassembly operand/target/coprocessor/buffer regressions, debug-tool ON/OFF server compilation and regenerated TCP index.
The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442).
Game-specific integration remains in that repository. Consuming this change
requires a deliberate framework pin update in the game repo and a rebuild.
This is a host/runtime/tool change; it requires no BIOS or game C regeneration.
Historical alpha acceptance used the older framework checkpoint above. It is
separate from current-master branch verification. Fresh game, renderer and
headset checks, where applicable, are recorded in the PR validation notes;
uncertain hardware/runtime behavior must not be inferred from a clean link.
## Current-master integration validation
Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled.
Release disassembly tests passed for arithmetic, multiply/divide, shifts, branch/jump targets, immediates, COP0/COP2, unknown words and bounded buffers. Checks execute with NDEBUG enabled. Full runtime builds passed with debug tools enabled and disabled; the generated command index passes.
Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested.
The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed.
@@ -0,0 +1,41 @@
# Write-trace producer-PC filters: source and validation
Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on
`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`.
Exact source commits:
- [734977c9](https://github.com/FractalEngineer/psxrecomp/commit/734977c93e80789e9210a262daa35a88f41c40d4)
Original author: Yves <calibratedbeats@gmail.com>. The feature commit retains
that exact human author identity. The focused port consolidates the selected
source hunks and corrections. Its commit also cites these sources and carries
the original human contributor's `Co-authored-by: Yves <calibratedbeats@gmail.com>` trailer.
Adapted: Only full-PC low/high query filters and response bounds; recording is unchanged.
Deliberately excluded: The same source commit's OpenXR hand snapshots and pose math; no game memory-map constants.
The feature was reconstructed on upstream master
`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs,
renderer/input policy and build integration. Validation after reconstruction:
Six production-handler wrapped-ring regressions, debug-tool ON/OFF compilation and regenerated TCP index.
The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442).
Game-specific integration remains in that repository. Consuming this change
requires a deliberate framework pin update in the game repo and a rebuild.
This is a host/runtime/tool change; it requires no BIOS or game C regeneration.
Historical alpha acceptance used the older framework checkpoint above. It is
separate from current-master branch verification. Fresh game, renderer and
headset checks, where applicable, are recorded in the PR validation notes;
uncertain hardware/runtime behavior must not be inferred from a clean link.
## Current-master integration validation
Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled.
Six production-handler regressions passed against a wrapped trace ring, covering full PC segments, inclusive/exclusive bounds, filtering before count, combined filters, DMA metadata and response bounds. Full debug-enabled/disabled runtime builds and the generated command index pass.
Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested.
The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed.
+8
View File
@@ -23,6 +23,10 @@ psxrecomp_add_runtime_target(psx-runtime
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
if(BUILD_TESTING)
add_executable(disasm_test tests/test_disasm.cpp src/disasm_shim.cpp
../recompiler/src/mips_decoder.cpp)
target_include_directories(disasm_test PRIVATE include ../recompiler/include)
add_test(NAME disasm_test COMMAND disasm_test)
# Netplay: which BIOS a match boots, and its wire helpers.
add_executable(netplay_bios_settle_test
tests/test_netplay_bios_settle.c
@@ -121,6 +125,10 @@ if(BUILD_TESTING)
set(PSX_GL_READBACK_COMPILER_BIN "${_gl_test_default_bin}" CACHE PATH "GCC/MinGW bin for GL fixture")
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME wtrace_dump_test COMMAND ${CMAKE_COMMAND} -E env
"CC=${CMAKE_C_COMPILER}" "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_wtrace_dump.py")
set_tests_properties(wtrace_dump_test PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME counted_mips_relocations_test COMMAND "${Python3_EXECUTABLE}"
"${PSXRECOMP_ROOT}/tools/tests/test_counted_mips_relocations.py")
add_test(NAME interp_mod_entry_guards_test COMMAND "${Python3_EXECUTABLE}"
+19
View File
@@ -0,0 +1,19 @@
#ifndef PSX_DISASM_H
#define PSX_DISASM_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Format one MIPS R3000 instruction as text into out (NUL-terminated).
* addr is used to resolve branch/jump targets. Returns the number of
* characters written (excluding the NUL), or 0 if out/cap are unusable. */
int psx_disasm_one(uint32_t word, uint32_t addr, char* out, int cap);
#ifdef __cplusplus
}
#endif
#endif /* PSX_DISASM_H */
+4
View File
@@ -94,6 +94,10 @@ void psx_host_write_word(uint32_t addr, uint32_t val);
void psx_host_write_half(uint32_t addr, uint16_t val);
void psx_host_write_byte(uint32_t addr, uint8_t val);
/* Side-effect-free aligned word peek (RAM / scratchpad / BIOS ROM only), for
* the debug path. Returns 0 (and leaves *out alone) for anything else. */
int psx_peek_word(uint32_t addr, uint32_t *out);
#ifdef __cplusplus
}
#endif
+7
View File
@@ -457,6 +457,13 @@ set(PSXRECOMP_RUNTIME_SOURCES
# (sljit Tier-2 in-process JIT backend removed 2026-07-15 — was disabled by
# default since 2026-06-25; gaps fall to the interpreter, gcc/tcc unaffected.)
)
# TCP `disasm` command only (debug_server.c); stripped with the rest of the TCP
# server when PSX_DEBUG_TOOLS is OFF.
if(PSX_DEBUG_TOOLS)
list(APPEND PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/recompiler/src/mips_decoder.cpp
${PSXRECOMP_ROOT}/runtime/src/disasm_shim.cpp)
endif()
# Optional delay-sync netplay (recomp-net). Auto-discovers a sibling checkout
# (…/recomp-net next to the game repo or next to psxrecomp). Override with
+84 -3
View File
@@ -27,6 +27,9 @@
#include "overlay_backend.h"
#include "cpu_state.h"
#include "pgxp.h"
#ifndef PSX_NO_DEBUG_TOOLS
#include "psx_disasm.h"
#endif
#include "dma.h"
#include "gpu.h"
#include "gpu_render.h" /* gr_scale + gr_render_display_hires (screenshot_hires) */
@@ -5185,6 +5188,60 @@ static void handle_get_registers(int id, const char *json)
free(buf);
}
/* disasm: disassemble guest instructions. {"cmd":"disasm","addr":"0x8001121C",
* "count":32}. Reads live guest memory, so it works on EXE code and on
* RAM-installed/overlay code alike. Uses the recompiler's MIPS decoder.
* Reads go through psx_peek_word (main RAM / scratchpad / BIOS ROM only, no
* device reads, no read hooks); a word outside those comes back as
* {"word":null,"text":"<unreadable>"}. Compiled only with the debug tools,
* like the sources it needs (runtime.cmake). */
#ifndef PSX_NO_DEBUG_TOOLS
static void handle_disasm(int id, const char *json)
{
char addr_str[32];
if (!json_get_str(json, "addr", addr_str, sizeof(addr_str))) {
send_err(id, "missing addr"); return;
}
const char *hex = addr_str;
if (hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X')) hex += 2;
char *hex_end = NULL;
unsigned long long addr_val = strtoull(hex, &hex_end, 16);
if (hex_end == hex || *hex_end != '\0' || addr_val > 0xFFFFFFFFull) {
send_err(id, "bad addr"); return;
}
uint32_t addr = (uint32_t)addr_val & ~3u;
int count = json_get_int(json, "count", 16);
if (count < 1) count = 1;
if (count > 256) count = 256;
size_t bufsz = 512u + (size_t)count * 160u;
char *out = (char *)malloc(bufsz);
if (!out) { send_err(id, "oom"); return; }
int pos = snprintf(out, bufsz,
"{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"count\":%d,\"lines\":[",
id, addr, count);
char text[128];
for (int i = 0; i < count; i++) {
uint32_t a = addr + (uint32_t)i * 4u;
uint32_t w = 0;
if (!psx_peek_word(a, &w)) {
pos += snprintf(out + pos, bufsz - (size_t)pos,
"%s{\"addr\":\"0x%08X\",\"word\":null,\"text\":\"<unreadable>\"}",
i ? "," : "", a);
continue;
}
text[0] = '\0';
(void)psx_disasm_one(w, a, text, (int)sizeof(text));
pos += snprintf(out + pos, bufsz - (size_t)pos,
"%s{\"addr\":\"0x%08X\",\"word\":\"0x%08X\",\"text\":\"%s\"}",
i ? "," : "", a, w, text);
}
pos += snprintf(out + pos, bufsz - (size_t)pos, "]}");
debug_server_send_line(out);
free(out);
}
#endif /* !PSX_NO_DEBUG_TOOLS */
static void handle_read_ram(int id, const char *json)
{
char addr_str[32];
@@ -10073,7 +10130,7 @@ static void wtrace_fill_entry(WriteTraceEntry *e, uint64_t seq,
* instead of the stale last-CPU-store PC (which is meaningless mid-DMA). */
if (g_dma_exec_depth > 0) {
e->dma_ch = (int8_t)g_dma_cur_ch;
if (g_dma_initiator_pc) e->pc = g_dma_initiator_pc;
e->pc = g_dma_initiator_pc; /* 0 = unknown; never a stale CPU store PC */
} else {
e->dma_ch = -1;
}
@@ -11680,6 +11737,22 @@ static void handle_wtrace_dump(int id, const char *json)
if (json_get_str(json, "addr_hi", hi_str, sizeof(hi_str)))
filter_hi = hex_to_u32(hi_str) & 0x1FFFFFFFu;
/* Filter the recorded producer PC, segment-masked like the address filter
* (KSEG0/KSEG1/KUSEG alias). DMA entries use the initiator PC stored by
* wtrace_fill_entry; an unknown (0) initiator matches only when no PC
* bound is given. Post-hoc only: recording, fingerprints and guest
* execution are unchanged. pc_lo/pc_hi echo the raw request. */
uint32_t pc_lo = 0, pc_hi = 0xFFFFFFFFu;
int pc_filtered = 0;
/* Default upper bound sits above every masked PC so no entry is excluded. */
uint32_t pc_lo_m = 0, pc_hi_m = 0x20000000u;
if (json_get_str(json, "pc_lo", lo_str, sizeof(lo_str))) {
pc_lo = hex_to_u32(lo_str); pc_lo_m = pc_lo & 0x1FFFFFFFu; pc_filtered = 1;
}
if (json_get_str(json, "pc_hi", hi_str, sizeof(hi_str))) {
pc_hi = hex_to_u32(hi_str); pc_hi_m = pc_hi & 0x1FFFFFFFu; pc_filtered = 1;
}
/* Optional frame-window filter — the "query the ring for the window of
* interest" primitive. Lets a caller reach entries in the MIDDLE of a deep,
* high-traffic ring (which oldest-N / newest-N paging cannot). -1 = unbounded. */
@@ -11703,8 +11776,9 @@ static void handle_wtrace_dump(int id, const char *json)
size_t pos = 0;
uint32_t emitted = 0;
pos += snprintf(buf + pos, BUF_SZ - pos,
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%u,\"entries\":[",
id, (unsigned long long)total, avail);
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%u,"
"\"pc_lo\":\"0x%08X\",\"pc_hi\":\"0x%08X\",\"entries\":[",
id, (unsigned long long)total, avail, pc_lo, pc_hi);
for (uint32_t i = 0; i < avail && emitted < MAX_OUT && pos < BUF_SZ - 256; i++) {
uint32_t idx;
if (newest_first) {
@@ -11715,6 +11789,10 @@ static void handle_wtrace_dump(int id, const char *json)
}
WriteTraceEntry *e = &s_wtrace[idx];
if (e->addr < filter_lo || e->addr >= filter_hi) continue;
if (pc_filtered) {
uint32_t epc = e->pc & 0x1FFFFFFFu;
if (e->pc == 0 || epc < pc_lo_m || epc >= pc_hi_m) continue;
}
if (frame_lo >= 0 && (int)e->frame < frame_lo) continue;
if (frame_hi >= 0 && (int)e->frame > frame_hi) continue;
pos += snprintf(buf + pos, BUF_SZ - pos,
@@ -14789,6 +14867,9 @@ static const CmdEntry s_commands[] = {
{ "c0_history", handle_c0_history },
{ "capture_quads", handle_capture_quads },
{ "get_quads", handle_get_quads },
#ifndef PSX_NO_DEBUG_TOOLS
{ "disasm", handle_disasm },
#endif
{ "gte_state", handle_gte_state },
{ "gte_ring_dump", handle_gte_ring_dump },
{ "gte_intpl_dump", handle_gte_intpl_dump },
+136
View File
@@ -0,0 +1,136 @@
#include "psx_disasm.h"
#include "mips_decoder.h"
#include <cstdio>
#include <cstring>
/* C-linkage bridge so the C debug server can use the C++ MIPS decoder that the
* recompiler already ships (recompiler/src/mips_decoder.cpp). */
/* GTE data registers (COP2 rd index) — psx-spx. */
static const char* const kCop2Data[32] = {
"VXY0", "VZ0", "VXY1", "VZ1", "VXY2", "VZ2", "RGB", "OTZ",
"IR0", "IR1", "IR2", "IR3", "SXY0", "SXY1", "SXY2", "SXYP",
"SZ0", "SZ1", "SZ2", "SZ3", "RGB0", "RGB1", "RGB2", "RES1",
"MAC0", "MAC1", "MAC2", "MAC3", "IRGB", "ORGB", "LZCS", "LZCR"
};
/* GTE control registers (COP2 rd index) — psx-spx. */
static const char* const kCop2Ctrl[32] = {
"RT11RT12", "RT13RT21", "RT22RT23", "RT31RT32", "RT33", "TRX", "TRY", "TRZ",
"L11L12", "L13L21", "L22L23", "L31L32", "L33", "RBK", "GBK", "BBK",
"LR1LR2", "LR3LG1", "LG2LG3", "LB1LB2", "LB3", "RFC", "GFC", "BFC",
"OFX", "OFY", "H", "DQA", "DQB", "ZSF3", "ZSF4", "FLAG"
};
static const char* gte_cmd_name(uint32_t raw) {
switch (raw & 0x3Fu) {
case 0x01: return "RTPS";
case 0x06: return "NCLIP";
case 0x0C: return "OP";
case 0x10: return "DPCS";
case 0x11: return "INTPL";
case 0x12: return "MVMVA";
case 0x13: return "NCDS";
case 0x14: return "CDP";
case 0x16: return "NCDT";
case 0x1B: return "NCCS";
case 0x1C: return "CC";
case 0x1E: return "NCS";
case 0x20: return "NCT";
case 0x28: return "SQR";
case 0x29: return "DCPL";
case 0x2A: return "DPCT";
case 0x2D: return "AVSZ3";
case 0x2E: return "AVSZ4";
case 0x30: return "RTPT";
case 0x3D: return "GPF";
case 0x3E: return "GPL";
case 0x3F: return "NCCT";
default: return "GTE";
}
}
extern "C" int psx_disasm_one(uint32_t word, uint32_t addr, char* out, int cap) {
if (!out || cap <= 0) return 0;
using PSXRecomp::DecodedInstruction;
using PSXRecomp::InstrFormat;
using PSXRecomp::MipsDecoder;
DecodedInstruction d = MipsDecoder::decode(word, addr);
const char* m = d.mnemonic ? d.mnemonic : "???";
auto rn = [](uint8_t r) { return MipsDecoder::register_name(r); };
uint8_t cop = d.rd & 0x1F;
int n;
if (d.format == InstrFormat::UNKNOWN) {
n = std::snprintf(out, (size_t)cap, ".word 0x%08X", word);
} else if (word == 0u) {
n = std::snprintf(out, (size_t)cap, "NOP");
} else if (std::strcmp(m, "JR") == 0) {
n = std::snprintf(out, (size_t)cap, "%-8s %s", m, rn(d.rs));
} else if (std::strcmp(m, "JALR") == 0) {
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s", m, rn(d.rd), rn(d.rs));
} else if (std::strcmp(m, "SLL") == 0 || std::strcmp(m, "SRL") == 0 ||
std::strcmp(m, "SRA") == 0) {
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, %d", m, rn(d.rd), rn(d.rt),
d.shamt);
} else if (std::strcmp(m, "MFHI") == 0 || std::strcmp(m, "MFLO") == 0) {
n = std::snprintf(out, (size_t)cap, "%-8s %s", m, rn(d.rd));
} else if (std::strcmp(m, "MTHI") == 0 || std::strcmp(m, "MTLO") == 0) {
n = std::snprintf(out, (size_t)cap, "%-8s %s", m, rn(d.rs));
} else if (std::strcmp(m, "SYSCALL") == 0 || std::strcmp(m, "BREAK") == 0) {
n = std::snprintf(out, (size_t)cap, "%s", m);
} else if (std::strcmp(m, "MULT") == 0 || std::strcmp(m, "MULTU") == 0 ||
std::strcmp(m, "DIV") == 0 || std::strcmp(m, "DIVU") == 0) {
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s", m, rn(d.rs), rn(d.rt));
} else if (std::strcmp(m, "SLLV") == 0 || std::strcmp(m, "SRLV") == 0 ||
std::strcmp(m, "SRAV") == 0) {
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, %s", m, rn(d.rd), rn(d.rt), rn(d.rs));
} else if (d.format == InstrFormat::COP0) {
if (std::strcmp(m, "MFC0") == 0 || std::strcmp(m, "MTC0") == 0)
n = std::snprintf(out, (size_t)cap, "%-8s %s, $%u", m, rn(d.rt), (unsigned)cop);
else if (std::strcmp(m, "RFE") == 0)
n = std::snprintf(out, (size_t)cap, "%s", m);
else
n = std::snprintf(out, (size_t)cap, ".word 0x%08X", word);
} else if (d.format == InstrFormat::COP2) {
if (std::strcmp(m, "MFC2") == 0 || std::strcmp(m, "MTC2") == 0) {
n = std::snprintf(out, (size_t)cap, "%-8s %s, $%s", m, rn(d.rt),
kCop2Data[cop]);
} else if (std::strcmp(m, "CFC2") == 0 || std::strcmp(m, "CTC2") == 0) {
n = std::snprintf(out, (size_t)cap, "%-8s %s, $%s", m, rn(d.rt),
kCop2Ctrl[cop]);
} else if (d.rs & 0x10) { /* GTE command word */
n = std::snprintf(out, (size_t)cap, "%-8s 0x%07X", gte_cmd_name(d.raw),
d.raw & 0x1FFFFFFu);
} else {
n = std::snprintf(out, (size_t)cap, ".word 0x%08X", word);
}
} else if (d.format == InstrFormat::J) {
n = std::snprintf(out, (size_t)cap, "%-8s 0x%08X", m, d.jump_target);
} else if (d.is_branch && d.opcode != 0x04 && d.opcode != 0x05) {
n = std::snprintf(out, (size_t)cap, "%-8s %s, 0x%08X", m, rn(d.rs), d.branch_target);
} else if (d.is_branch) {
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, 0x%08X", m, rn(d.rs),
rn(d.rt), d.branch_target);
} else if (d.opcode == 0x0F) { /* LUI */
n = std::snprintf(out, (size_t)cap, "%-8s %s, 0x%04X", m, rn(d.rt),
(unsigned)d.uimm16);
} else if (d.is_load || d.is_store) {
const char* reg = (d.opcode == 0x32 || d.opcode == 0x3A) ? kCop2Data[d.rt & 31] : rn(d.rt);
const char* prefix = (d.opcode == 0x32 || d.opcode == 0x3A) ? "$" : "";
n = std::snprintf(out, (size_t)cap, "%-8s %s%s, %d(%s)", m, prefix, reg,
(int)d.imm16, rn(d.rs));
} else if (d.format == InstrFormat::R || d.format == InstrFormat::SPECIAL) {
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, %s", m, rn(d.rd), rn(d.rs),
rn(d.rt));
} else if (d.opcode >= 0x0C && d.opcode <= 0x0E) {
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, 0x%04X", m, rn(d.rt), rn(d.rs), (unsigned)d.uimm16);
} else {
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, %d", m, rn(d.rt), rn(d.rs),
(int)d.imm16);
}
if (n < 0) { out[0] = '\0'; return 0; }
return (n < cap) ? n : (cap - 1);
}
+22
View File
@@ -164,6 +164,28 @@ static inline uint32_t psx_phys_addr_store(uint32_t addr) {
/* Expose RAM pointer for oracle comparison (find_first_divergence). */
uint8_t *memory_get_ram_ptr(void) { return ram; }
uint8_t *memory_get_scratchpad_ptr(void) { return scratchpad; }
/* Side-effect-FREE aligned word peek for the debug/observability path ONLY:
* main RAM (every KUSEG/KSEG0/KSEG1 mirror), scratchpad and BIOS ROM. Never
* touches MMIO, the lockstep/DS read hooks, read watches, or unmapped_fatal.
* Returns 1 and fills *out when addr is backed by one of those; 0 otherwise. */
int psx_peek_word(uint32_t addr, uint32_t *out) {
uint32_t phys, off;
const uint8_t *p;
if (addr >= 0xC0000000u || (addr & 3u)) return 0;
phys = addr & 0x1FFFFFFFu;
if (psx_ram_resolve(addr, 4u, &off)) {
p = &ram[off];
} else if (phys >= 0x1F800000u && phys <= 0x1F8003FCu) {
p = &scratchpad[phys - 0x1F800000u];
} else if (phys >= 0x1FC00000u && phys <= 0x1FC7FFFCu) {
p = &bios_rom[phys - 0x1FC00000u];
} else {
return 0;
}
if (out) memcpy(out, p, sizeof(*out));
return 1;
}
void memory_clear_low_boot_scratch(void) {
memset(ram, 0, 0x10u);
}
+47
View File
@@ -0,0 +1,47 @@
#include "psx_disasm.h"
#include <cstdlib>
#include <cstring>
#include <cstdio>
static void require(bool condition, const char* expression, int line) {
if (!condition) {
std::fprintf(stderr, "FAIL line %d: %s\n", line, expression);
std::exit(EXIT_FAILURE);
}
}
#define CHECK(expr) require((expr), #expr, __LINE__)
static void check(unsigned word, unsigned pc, const char* expected) {
char out[128];
int n=psx_disasm_one(word, pc, out, sizeof(out));
if (std::strcmp(out,expected)) {
std::fprintf(stderr,"%08X: expected [%s], got [%s]\n",word,expected,out);
CHECK(false);
}
CHECK(n == (int)std::strlen(expected));
}
int main() {
check(0,0,"NOP");
check(0x012A4020,0,"ADD $t0, $t1, $t2");
check(0x012A0018,0,"MULT $t1, $t2");
check(0x012A001B,0,"DIVU $t1, $t2");
check(0x012A4004,0,"SLLV $t0, $t2, $t1");
check(0x0521FFFF,0x80001000,"BGEZ $t1, 0x80001000");
check(0x1920FFFE,0x80001000,"BLEZ $t1, 0x80000FFC");
check(0x112AFFFE,0x80001000,"BEQ $t1, $t2, 0x80000FFC");
check(0x08000000,0x8FFFFFFC,"J 0x90000000");
check(0x3128FFFF,0,"ANDI $t0, $t1, 0xFFFF");
check(0x2528FFFF,0,"ADDIU $t0, $t1, -1");
check(0x8D28FFF8,0,"LW $t0, -8($t1)");
check(0xC923FFF8,0,"LWC2 $VZ1, -8($t1)");
check(0x40086000,0,"MFC0 $t0, $12");
check(0x42000010,0,"RFE");
check(0x48086800,0,"MFC2 $t0, $SXY1");
check(0x4848D000,0,"CFC2 $t0, $H");
check(0x4A080001,0,"RTPS 0x0080001");
check(0x48200001,0,".word 0x48200001");
check(0xFC000000,0,".word 0xFC000000");
char tiny[4]={'x','x','x','x'};
CHECK(psx_disasm_one(0,0,tiny,4)==3 && tiny[3]==0);
CHECK(psx_disasm_one(0,0,tiny,1)==0 && tiny[0]==0);
CHECK(psx_disasm_one(0,0,nullptr,4)==0);
tiny[0]='x'; CHECK(psx_disasm_one(0,0,tiny,0)==0 && tiny[0]=='x');
}
+35
View File
@@ -35,6 +35,7 @@ extern void psx_write_word(uint32_t addr, uint32_t val);
extern void psx_write_half(uint32_t addr, uint16_t val);
extern void psx_write_byte(uint32_t addr, uint8_t val);
extern uint8_t *memory_get_ram_ptr(void);
extern uint8_t *memory_get_scratchpad_ptr(void);
/* Stub-side mutation counters (psx_ram_runtime_map_stubs.c). */
extern int g_stub_device_restores;
@@ -130,6 +131,39 @@ static void check_guest_access(int expanded) {
check(psx_read_byte(0x80204005u) == 0x5Au, "byte reads back at its own address");
}
/* ---- psx_peek_word: debug read that must never reach the bus ----------- */
static void check_peek(int expanded) {
uint32_t v = 0xDEADBEEFu;
memset(memory_get_ram_ptr(), 0, PSX_MAIN_RAM_BACKING_BYTES);
psx_write_word(0x80001000u, 0x11223344u);
check(psx_peek_word(0x80001000u, &v) && v == 0x11223344u, "peek: KSEG0 RAM");
check(psx_peek_word(0x00001000u, &v) && v == 0x11223344u, "peek: KUSEG RAM");
check(psx_peek_word(0xA0001000u, &v) && v == 0x11223344u, "peek: KSEG1 RAM");
v = 0;
check(psx_peek_word(0x80201000u, &v) && ((v == 0x11223344u) == !expanded),
"peek: 2nd mirror follows the live geometry");
check(psx_peek_word(0x807FFFFCu, &v), "peek: top of the RAM window");
memory_get_scratchpad_ptr()[0x10] = 0x78; memory_get_scratchpad_ptr()[0x13] = 0x12;
check(psx_peek_word(0x1F800010u, &v) && (v & 0xFF) == 0x78 && (v >> 24) == 0x12,
"peek: scratchpad");
check(psx_peek_word(0x1F8003FCu, &v) && !psx_peek_word(0x1F800400u, &v),
"peek: scratchpad end bound");
check(psx_peek_word(0xBFC00000u, &v) && psx_peek_word(0x1FC7FFFCu, &v) &&
!psx_peek_word(0x1FC80000u, &v), "peek: BIOS ROM bounds");
/* Device / unmapped space is refused WITHOUT touching the bus (the stubbed
* psx_fatal_halt traps if a real access reached unmapped_fatal). */
v = 0xDEADBEEFu;
check(!psx_peek_word(0x1F801044u, &v), "peek: JOY_RX refused");
check(!psx_peek_word(0x1F801810u, &v), "peek: GPUREAD refused");
check(!psx_peek_word(0x1F802000u, &v), "peek: expansion-2 MMIO refused");
check(!psx_peek_word(0x1F000000u, &v), "peek: expansion-1 refused");
check(!psx_peek_word(0x80800000u, &v), "peek: past the RAM window refused");
check(!psx_peek_word(0xFFFE0130u, &v) && !psx_peek_word(0xC0000000u, &v),
"peek: KSEG2 refused");
check(!psx_peek_word(0x80001002u, &v), "peek: unaligned refused");
check(v == 0xDEADBEEFu, "peek: refused reads leave *out untouched");
}
/* ---- 3. a real DMA writing RAM from a guest-chosen MADR --------------- */
#define DMA_BASE 0x1F801080u
static void run_otc(uint32_t madr, uint32_t entries) {
@@ -288,6 +322,7 @@ int main(void) {
for (int expanded = 0; expanded < 2; ++expanded) {
set_geometry(expanded);
check_guest_access(expanded);
check_peek(expanded);
check_dma(expanded);
}
check_savestates();
+186
View File
@@ -0,0 +1,186 @@
"""Execute the production trace-query handler against a wrapped fixture ring.
Compile only the handler and its real JSON helpers, replacing the transport
with stdout. This needs a C compiler, but no game image, SDL or live server.
Exits 77 (ctest SKIP_RETURN_CODE) when no C compiler is available.
"""
import json
import os
from pathlib import Path
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
import unittest
SKIP_EXIT = 77
def matching_brace(source, open_index):
"""Index of the brace closing the one at open_index (skips strings/comments)."""
depth, i, n = 0, open_index, len(source)
while i < n:
c = source[i]
if source.startswith("/*", i):
i = source.index("*/", i + 2) + 1
elif source.startswith("//", i):
i = source.index("\n", i)
elif c in "\"'":
i += 1
while source[i] != c:
i += 2 if source[i] == "\\" else 1
elif c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
return i
i += 1
raise AssertionError("unbalanced braces")
def find_compiler():
"""argv prefix for the C compiler named by CC (default cc), or None."""
setting = os.environ.get("CC", "cc")
if shutil.which(setting):
return [setting]
parts = shlex.split(setting, posix=os.name != "nt")
return parts if parts and shutil.which(parts[0]) else None
class WriteTraceQuery(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = find_compiler()
if compiler is None:
raise unittest.SkipTest("no C compiler (set CC)")
cls.temp = tempfile.TemporaryDirectory()
cls.addClassCleanup(cls.temp.cleanup)
source = (Path(__file__).parents[1] / "src/debug_server.c").read_text(encoding="utf-8")
def function(name):
match = re.search(r"static [^;{}]+\b" + name + r"\([^;{}]*\)\s*\{", source)
if not match:
raise AssertionError(f"production function missing: {name}")
return source[match.start():matching_brace(source, match.end() - 1) + 1]
end = source.index("} WriteTraceEntry;") + len("} WriteTraceEntry;")
start = source.rfind("typedef struct {", 0, end)
harness = """#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define WRITE_TRACE_CAP 8
""" + source[start:end] + """
static WriteTraceEntry ring[WRITE_TRACE_CAP], *s_wtrace=ring;
static uint64_t s_wtrace_seq=10;
static uint32_t s_wtrace_head=2;
typedef struct { uint32_t pc; uint32_t gpr[32]; } CPUState;
static CPUState *debug_cpu_ptr = NULL;
static uint32_t s_frame_count = 0;
uint32_t g_debug_current_func_addr = 0;
uint32_t g_debug_last_store_pc = 0;
int g_dma_exec_depth = 0, g_dma_cur_ch = -1;
uint32_t g_dma_initiator_pc = 0;
static void debug_server_send_line(const char *line) { puts(line); }
static void send_err(int id, const char *error) { (void)id; (void)error; abort(); }
"""
for name in ("json_get_str", "json_get_int", "hex_to_u32", "wtrace_fill_entry",
"handle_wtrace_dump"):
harness += function(name) + "\n"
# seq 7 is a DMA entry with an unknown (0) initiator PC.
harness += """int main(int argc, char **argv) {
if (argc > 4 && !strcmp(argv[1], "fill")) { /* fill <dma_depth> <initiator> <cpu_store_pc> */
WriteTraceEntry e; memset(&e, 0, sizeof e);
g_dma_exec_depth = atoi(argv[2]); g_dma_cur_ch = 2;
g_dma_initiator_pc = (uint32_t)strtoul(argv[3], 0, 16);
g_debug_last_store_pc = (uint32_t)strtoul(argv[4], 0, 16);
wtrace_fill_entry(&e, 1, 0x1000, 0, 1, 4);
printf("{\\"pc\\":%u,\\"dma_ch\\":%d}\\n", e.pc, e.dma_ch);
return 0;
}
const uint32_t pcs[8]={0x80001000,0x80001004,0xA0001000,0x80001008,
0x8000100C,0x00000000,0x80001004,0x00001004};
for (int seq=2; seq<10; ++seq) {
WriteTraceEntry *e=&ring[seq%WRITE_TRACE_CAP];
e->seq=seq; e->pc=pcs[seq-2]; e->addr=0x1000+(seq-2)*4;
e->frame=10+(seq-2)/2; e->width=4; e->dma_ch=seq==5?2:(seq==7?3:-1);
}
handle_wtrace_dump(7,argc>1?argv[1]:"{}");
return 0;
}
"""
path = Path(cls.temp.name)
(path / "query.c").write_text(harness, encoding="utf-8", newline="\n")
cls.exe = path / ("query.exe" if os.name == "nt" else "query")
if Path(compiler[0]).stem.lower() in ("cl", "clang-cl"):
out = ["/nologo", "/Fe:" + str(cls.exe), "/Fo:" + str(path) + os.sep]
else:
out = ["-o", str(cls.exe)]
subprocess.run([*compiler, str(path / "query.c"), *out], check=True, cwd=path)
def query(self, **params):
result = subprocess.check_output([str(self.exe), json.dumps(params)], text=True)
reply = json.loads(result)
self.assertEqual(reply["emitted"], len(reply["entries"]))
return reply
def sequences(self, **params):
return [e["seq"] for e in self.query(**params)["entries"]]
def fill(self, depth, initiator, store_pc):
out = subprocess.check_output(
[str(self.exe), "fill", str(depth), initiator, store_pc], text=True)
return json.loads(out)
def test_fill_entry_producer_pc(self):
self.assertEqual(self.fill(0, "0", "80001234"), {"pc": 0x80001234, "dma_ch": -1})
self.assertEqual(self.fill(1, "80005000", "80001234"), {"pc": 0x80005000, "dma_ch": 2})
# Unknown initiator: the DMA entry must not inherit the stale CPU store PC.
self.assertEqual(self.fill(1, "0", "80001234"), {"pc": 0, "dma_ch": 2})
def test_default_wrapped_order(self):
self.assertEqual(self.sequences(), list(range(2, 10)))
def test_pc_range_exclusive_upper(self):
# seq 9 has KUSEG pc 0x1004, which aliases 0x80001004.
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C"), [3, 5, 8, 9])
def test_segments_alias(self):
self.assertEqual(self.sequences(pc_lo="A0001000", pc_hi="A0001004"), [2, 4])
self.assertEqual(self.sequences(pc_lo="00001000", pc_hi="00001004"), [2, 4])
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="80001008"), [3, 8, 9])
def test_unknown_dma_initiator_never_matches_pc_filter(self):
self.assertIn(7, self.sequences())
self.assertNotIn(7, self.sequences(pc_lo="0", pc_hi="FFFFFFFF"))
self.assertEqual(self.sequences(pc_hi="80001000"), [])
def test_newest_and_count_after_filter(self):
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C", newest=1, count=1), [9])
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C", count=1), [3])
def test_combined_address_frame_filters(self):
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C",
addr_lo="80001008", addr_hi="80001018",
frame_lo=11, frame_hi=12), [5])
def test_empty_ranges(self):
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="80001004"), [])
self.assertEqual(self.sequences(pc_hi="0"), [])
def test_dma_recorded_initiator_and_response_bounds(self):
reply = self.query(pc_lo="80001008", pc_hi="8000100C")
self.assertEqual(reply["pc_lo"], "0x80001008")
self.assertEqual(reply["pc_hi"], "0x8000100C")
self.assertEqual(reply["entries"][0]["dma_ch"], 2)
self.assertEqual(reply["entries"][0]["seq"], 5)
if __name__ == "__main__":
if find_compiler() is None:
print("SKIP: no C compiler (set CC)")
sys.exit(SKIP_EXIT)
unittest.main()