From bd2c3b8c623c7197c209eec9b447c0744398d650 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 2 Oct 2026 10:24:36 -0700 Subject: [PATCH 1/3] Offline builds: host relay status only with recomp-net linked Co-Authored-By: Claude Opus 5.5 (1M context) --- runtime/src/main.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 7980ad24..8beae404 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -62,7 +62,9 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "psx_netplay_rb.h" #include "psx_selfcheck.h" #include "psx_lobby_client.h" +#if defined(PSX_HAS_RECOMP_NET) #include "recomp_net/host_relay.h" /* RNetHostRelayStatus for the launcher relay line */ +#endif #include "netplay_bios_settle.h" #include "netplay_exit_reason.h" #include "host_time.h" @@ -10951,6 +10953,7 @@ namespace { if (!out || !out_cap) return 0; out[0] = '\0'; if (g_lnch_hosting_lan || g_lnch_joined_lan || !psx_lobby_in_lobby()) return 0; +#if defined(PSX_HAS_RECOMP_NET) RNetHostRelayStatus st; if (!psx_lobby_host_relay_status(&st)) return 0; if (st.role == 1) { @@ -10993,6 +10996,9 @@ namespace { return 1; } return 0; +#else + return 0; /* offline build: recomp-net (and its host relay) is not linked */ +#endif } int ae_np_force_turn_set(void*, int force) { if (g_lnch_hosting_lan || g_lnch_joined_lan) From 03f914be6f0a6aee91520664f0bb4e138886c782 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 2 Oct 2026 10:29:52 -0700 Subject: [PATCH 2/3] Motion kit: ROTATION kind for bare 3x3 rotations Co-Authored-By: Claude Opus 5.5 (1M context) --- runtime/include/render_pass_motion.h | 8 ++++++-- runtime/src/render_pass_motion.c | 19 ++++++++++++++----- runtime/tests/test_render_pass_motion.c | 22 ++++++++++++++++++++++ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/runtime/include/render_pass_motion.h b/runtime/include/render_pass_motion.h index 9bb1ea7b..c28ddc3b 100644 --- a/runtime/include/render_pass_motion.h +++ b/runtime/include/render_pass_motion.h @@ -45,7 +45,11 @@ enum { PSX_MOTION_SVECTOR = 4, /* s16 x, y, z */ /* s16 x, y, z angles, 4096 per turn: each blends along the shorter arc. */ PSX_MOTION_ANGLES = 5, - PSX_MOTION_SCALAR = 6 /* s32 */ + PSX_MOTION_SCALAR = 6, /* s32 */ + /* A bare 3x3 s16 rotation in 4.12 (18 bytes), blended like a MATRIX's; + * pair it with a VECTOR / SVECTOR kind wherever its translation lives + * (skeleton pose records, matrices stored apart from their position). */ + PSX_MOTION_ROTATION = 7 }; typedef struct PSXMotionLimits { @@ -92,7 +96,7 @@ uint32_t psx_motion_prepare(PSXMotionSet* set, const PSXMotionLimits* limits, /* Write every prepared value at t (0 = previous frame, 1 = this frame). */ void psx_motion_apply(const PSXMotionSet* set, double t); /* Blend one prepared value at t into `out` without writing guest RAM: - * a PSXMotionMatrix for the MATRIX kinds, int32_t[3] for VECTOR / SVECTOR / + * a PSXMotionMatrix for the MATRIX kinds and ROTATION (translation 0), int32_t[3] for VECTOR / SVECTOR / * ANGLES, int32_t for SCALAR. Returns 0 when that value does not blend this * frame (out then holds its current value) or was not tracked (out * untouched, returns -1). */ diff --git a/runtime/src/render_pass_motion.c b/runtime/src/render_pass_motion.c index 89d9eeb1..98810907 100644 --- a/runtime/src/render_pass_motion.c +++ b/runtime/src/render_pass_motion.c @@ -108,6 +108,9 @@ static void read_entry(MotionEntry* e) { for (int i = 0; i < 3; i++) e->v[9 + i] = m.t[i]; break; } + case PSX_MOTION_ROTATION: + for (int i = 0; i < 9; i++) e->v[i] = (int16_t)psx_mod_read_half(e->addr + 2u * (uint32_t)i); + break; case PSX_MOTION_VECTOR: for (int i = 0; i < 3; i++) e->v[i] = (int32_t)psx_mod_read_word(e->addr + 4u * (uint32_t)i); break; @@ -123,7 +126,7 @@ static void read_entry(MotionEntry* e) { int psx_motion_track(PSXMotionSet* s, uint32_t kind, uint32_t addr, uint32_t identity) { MotionEntry* e; - if (!s || kind < PSX_MOTION_MATRIX || kind > PSX_MOTION_SCALAR) return 0; + if (!s || kind < PSX_MOTION_MATRIX || kind > PSX_MOTION_ROTATION) return 0; if (!s->began[s->cur] || s->n[s->cur] >= s->cap) return 0; e = &s->cap_buf[s->cur][s->n[s->cur]]; memset(e, 0, sizeof *e); @@ -270,10 +273,11 @@ static int within_limits(const MotionEntry* a, const MotionEntry* b, const PSXMotionLimits* lim, uint32_t ticks) { const double move = lim->move_per_tick > 0 ? lim->move_per_tick * ticks : HUGE_VAL; switch (a->kind) { - case PSX_MOTION_MATRIX: { + case PSX_MOTION_MATRIX: + case PSX_MOTION_ROTATION: { double ang; int16_t ra[9], rb[9]; - if (dist3(&a->v[9], &b->v[9]) > move) return 0; + if (a->kind == PSX_MOTION_MATRIX && dist3(&a->v[9], &b->v[9]) > move) return 0; for (int i = 0; i < 9; i++) { ra[i] = (int16_t)a->v[i]; rb[i] = (int16_t)b->v[i]; } ang = rotation_angle(ra, rb); if (ang >= 0) return ang <= turn_limit(lim->turn_radians); @@ -344,7 +348,8 @@ uint32_t psx_motion_prepare(PSXMotionSet* s, const PSXMotionLimits* lim, /* Blend one pair into `v` (entry layout). */ static void blend_entry(const MotionEntry* a, const MotionEntry* b, double t, int32_t* v) { switch (a->kind) { - case PSX_MOTION_MATRIX: { + case PSX_MOTION_MATRIX: + case PSX_MOTION_ROTATION: { int16_t ra[9], rb[9], ro[9]; for (int i = 0; i < 9; i++) { ra[i] = (int16_t)a->v[i]; rb[i] = (int16_t)b->v[i]; } blend_rotation(ra, rb, t, MOTION_PI * 2, ro); /* limits were applied at prepare */ @@ -388,6 +393,9 @@ static void write_entry(uint8_t kind, uint32_t addr, const int32_t* v) { case PSX_MOTION_SCALAR: psx_mod_write_word(addr, (uint32_t)v[0]); break; + case PSX_MOTION_ROTATION: + for (int i = 0; i < 9; i++) psx_mod_write_half(addr + 2u * (uint32_t)i, (uint16_t)v[i]); + break; } } @@ -405,7 +413,8 @@ void psx_motion_apply(const PSXMotionSet* s, double t) { } static void export_value(uint8_t kind, const int32_t* v, void* out) { - if (kind == PSX_MOTION_MATRIX || kind == PSX_MOTION_MATRIX_TRANSLATION) { + if (kind == PSX_MOTION_MATRIX || kind == PSX_MOTION_MATRIX_TRANSLATION || + kind == PSX_MOTION_ROTATION) { PSXMotionMatrix* m = (PSXMotionMatrix*)out; for (int i = 0; i < 9; i++) m->r[i] = (int16_t)v[i]; for (int i = 0; i < 3; i++) m->t[i] = v[9 + i]; diff --git a/runtime/tests/test_render_pass_motion.c b/runtime/tests/test_render_pass_motion.c index 6098ec78..52f61a50 100644 --- a/runtime/tests/test_render_pass_motion.c +++ b/runtime/tests/test_render_pass_motion.c @@ -164,6 +164,27 @@ static void test_angles_vectors_scalars(void) { psx_motion_set_destroy(s); } +static void test_rotation_only(void) { + PSXMotionSet* s = psx_motion_set_create(4); + PSXMotionLimits lim = {100.0, 0}; + PSXMotionStats st; + /* A bare rotation (as in a pose record) with other data right after it. */ + put_matrix(0xA00, 0.0, 0, 0, 0); + psx_mod_write_word(0xA00 + 0x12, 0xDEADBEEFu); + psx_motion_begin(s, 1); + psx_motion_track(s, PSX_MOTION_ROTATION, 0xA00, 3); + psx_motion_prepare(s, &lim, &st); + put_matrix(0xA00, 1.0, 0, 0, 0); + psx_mod_write_word(0xA00 + 0x12, 0xDEADBEEFu); + psx_motion_begin(s, 2); + psx_motion_track(s, PSX_MOTION_ROTATION, 0xA00, 3); + CHECK(psx_motion_prepare(s, &lim, &st) == 1, "a bare rotation blends"); + psx_motion_apply(s, 0.5); + CHECK(fabs(yaw_of(0xA00) - 0.5) < 0.002, "rotation-only slerps"); + CHECK(psx_mod_read_word(0xA00 + 0x12) == 0xDEADBEEFu, "nothing past the 18 bytes is written"); + psx_motion_set_destroy(s); +} + static int s_fn_calls; static double s_fn_last; static int frame_fn(struct CPUState* cpu, void* user, uint32_t alpha) { @@ -209,6 +230,7 @@ int main(void) { test_matrix_slerp(); test_identity_and_limits(); test_angles_vectors_scalars(); + test_rotation_only(); test_frame_driver(); printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures); return failures ? 1 : 0; From f9cafa73183fce791d035a59ab47fa6b1f9a7405 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 2 Oct 2026 10:32:23 -0700 Subject: [PATCH 3/3] Mod runtime: named plugin counters (psx_mod_counter_add, mod_counters) Same API as the V8:2 branch's b2104464, for every frame-rate plugin. Co-Authored-By: Claude Opus 5.5 (1M context) --- runtime/include/mod_plugins.h | 7 +++++ runtime/src/debug_server.c | 40 ++++++++++++++++++++++++++++ runtime/src/mod_runtime.cpp | 50 +++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 151e6f81..08394d75 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -37,6 +37,13 @@ int psx_mod_function_entry(struct CPUState* cpu, uint32_t address); /* Active function-entry hook count (0 = none). Hot callers test it before the * call, so a run without an active hook pays one load per interpreted entry. */ extern uint32_t g_psx_mod_function_entry_hooks; + +/* Always-on named event counters for trusted plugins (observability, not + * logging): e.g. how often each guard in a hook rejected. `name` should be a + * string literal of the form "."; up to 128 distinct names are + * kept, further names are counted in an overflow bucket. Emulation-thread only. + * TCP: {"cmd":"mod_counters"} lists every counter with its last frame. */ +void psx_mod_counter_add(const char* name, uint32_t delta); /* Entry callbacks can make nested guest calls while retaining host registers. * Save/load and rewind must wait until that host context has returned. */ int psx_mod_function_entry_active(void); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 6c6c4744..ac926631 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -7799,6 +7799,45 @@ static void handle_input_route_status(int id, const char *json) * {"cmd":"ws_hud_mode","tag_rects":0|1} * tag_rects=1 lets TAGGED rect-family prims re-anchor too (Tomba's AP * counter renders through the tagged sprite funnel). */ +/* Named plugin counters (psx_mod_counter_add), always on: + * {"cmd":"mod_counters"} + * -> counters: [{name, count, last_frame}], overflow. */ +static void handle_mod_counters(int id, const char *json) +{ + extern int psx_mod_counters_snapshot(const char **names, uint64_t *counts, + uint64_t *last_frames, int cap, + uint64_t *overflow); + (void)json; + enum { CAP = 128 }; + const char *names[CAP]; + uint64_t counts[CAP], last[CAP], overflow = 0; + int n = psx_mod_counters_snapshot(names, counts, last, CAP, &overflow); + if (n > CAP) n = CAP; + size_t cap = 256u + (size_t)n * 160u; + char *buf = (char *)malloc(cap); + if (!buf) { send_err(id, "alloc failed"); return; } + size_t off = 0; + off += (size_t)snprintf(buf + off, cap - off, + "{\"id\":%d,\"ok\":true,\"frame\":%llu,\"counters\":[", + id, (unsigned long long)s_frame_count); + for (int i = 0; i < n && off < cap; i++) { + char esc[96]; + size_t e = 0; + for (const char *p = names[i]; *p && e + 2 < sizeof esc; p++) + if (*p != '"' && *p != '\\' && (unsigned char)*p >= 0x20) esc[e++] = *p; + esc[e] = '\0'; + off += (size_t)snprintf(buf + off, cap - off, + "%s{\"name\":\"%s\",\"count\":%llu,\"last_frame\":%llu}", + i ? "," : "", esc, (unsigned long long)counts[i], + (unsigned long long)last[i]); + } + if (off < cap) + snprintf(buf + off, cap - off, "],\"overflow\":%llu}", + (unsigned long long)overflow); + debug_server_send_line(buf); + free(buf); +} + static void handle_ws_hud_mode(int id, const char *json) { int v = json_get_int(json, "tag_rects", -1); @@ -14100,6 +14139,7 @@ static const CmdEntry s_commands[] = { { "ws_aspect_cone_site", handle_ws_aspect_cone_site }, { "ws_margin", handle_ws_margin }, { "ws_hud_mode", handle_ws_hud_mode }, + { "mod_counters", handle_mod_counters }, { "kernel_bless", handle_kernel_bless }, { "ws_aspect", handle_ws_aspect }, { "display_aspect", handle_display_aspect }, diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 2c6e4a6a..d89f3fcc 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -54,6 +54,8 @@ extern "C" int fntrace_is_game_started(void); /* Declared in mod_plugins.h; see active_function_entry_hooks() below. */ uint32_t g_psx_mod_function_entry_hooks = 0; +/* Guest vblanks seen by the mod runtime: the plugin counters' time base. */ +static uint64_t g_mod_vblanks = 0; namespace PSXRecompV4 { namespace { @@ -1426,6 +1428,7 @@ extern "C" void mod_runtime_activate_plugins(void) { extern "C" void mod_runtime_on_vblank(void) { using namespace PSXRecompV4; + ++g_mod_vblanks; RuntimeMods& s = state(); if (!s.initialized || !s.plan.ok) return; for (const ModResolution::Plugin& plugin : s.plan.plugins) { @@ -1523,6 +1526,53 @@ extern "C" uint32_t psx_mod_alloc_gpu_dma_memory(uint32_t size, return psx_mod_gpu_dma_memory_alloc(size, alignment); } +namespace { +struct ModCounter { + const char* name = nullptr; /* caller literal; compared by content */ + uint64_t count = 0; + uint64_t last_frame = 0; +}; +constexpr size_t kModCounterCap = 128; +ModCounter g_mod_counters[kModCounterCap]; +size_t g_mod_counter_count = 0; +uint64_t g_mod_counter_overflow = 0; +} // namespace + +extern "C" void psx_mod_counter_add(const char* name, uint32_t delta) { + if (!name || !name[0]) return; + for (size_t i = 0; i < g_mod_counter_count; i++) { + ModCounter& c = g_mod_counters[i]; + if (c.name == name || std::strcmp(c.name, name) == 0) { + c.count += delta; + c.last_frame = g_mod_vblanks; + return; + } + } + if (g_mod_counter_count == kModCounterCap) { + g_mod_counter_overflow += delta; + return; + } + ModCounter& c = g_mod_counters[g_mod_counter_count++]; + c.name = name; + c.count = delta; + c.last_frame = g_mod_vblanks; +} + +/* Debug-server accessor: copies up to `cap` entries; returns the total count + * of distinct counters and writes the overflow bucket. */ +extern "C" int psx_mod_counters_snapshot(const char** names, uint64_t* counts, + uint64_t* last_frames, int cap, + uint64_t* overflow) { + const int n = (int)g_mod_counter_count; + for (int i = 0; i < n && i < cap; i++) { + names[i] = g_mod_counters[i].name; + counts[i] = g_mod_counters[i].count; + last_frames[i] = g_mod_counters[i].last_frame; + } + if (overflow) *overflow = g_mod_counter_overflow; + return n; +} + extern "C" int32_t psx_mod_widescreen_x_margin(void) { return (int32_t)psx_ws_x_margin(); }