mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Always-on host CPU sampler ring (TCP host_profile)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
#ifndef PSX_HOST_SAMPLER_H
|
||||
#define PSX_HOST_SAMPLER_H
|
||||
/* Always-on host CPU sampler for the emulation thread (debug-tools builds).
|
||||
* See host_sampler.c; TCP host_profile; tools/host_profile.py. */
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HOST_SAMPLER_CAP 65536u /* ~65 s at 1 kHz */
|
||||
|
||||
typedef struct HostSample {
|
||||
uint64_t rva; /* instruction pointer minus the executable's base */
|
||||
uint32_t frame; /* guest frame (s_frame_count) */
|
||||
uint32_t in_pass; /* 1 while a render pass ran */
|
||||
} HostSample;
|
||||
|
||||
/* Start sampling the calling thread (the emulation thread). Idempotent. */
|
||||
void host_sampler_start(void);
|
||||
int host_sampler_supported(void);
|
||||
/* Total samples recorded; the ring holds the newest HOST_SAMPLER_CAP. */
|
||||
uint64_t host_sampler_seq(void);
|
||||
uint64_t host_sampler_image_base(void);
|
||||
/* Sample `seq`, if it is still in the ring. */
|
||||
int host_sampler_get(uint64_t seq, HostSample *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -369,6 +369,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/sio.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/memcard.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/debug_server.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/host_sampler.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/debug_trace_ranges.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/dirty_ram_interp.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/game_dispatch_compat.c
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#endif
|
||||
#include <time.h>
|
||||
#include "debug_server.h"
|
||||
#include "host_sampler.h"
|
||||
#include "psx_video_timing.h"
|
||||
#include "psx_bss.h"
|
||||
#include "nd_intro_ot.h"
|
||||
@@ -5323,6 +5324,68 @@ static void handle_geom_correction(int id, const char *json)
|
||||
(unsigned long long)ps.word_no_z);
|
||||
}
|
||||
|
||||
/* host_profile — histogram of the always-on host CPU sampler (host_sampler.c)
|
||||
* over a window of the ring: {"cmd":"host_profile","since":SEQ} or
|
||||
* {"cmd":"host_profile","frames":N} (the last N guest frames), optional
|
||||
* "pass":1 (only samples inside render passes) / "pass":0 (outside), "top":K.
|
||||
* Addresses are executable-relative; tools/host_profile.py names them. Take
|
||||
* "seq" from one reply as "since" of the next to profile what ran between. */
|
||||
static void handle_host_profile(int id, const char *json)
|
||||
{
|
||||
enum { MAXB = 4096 };
|
||||
static uint64_t rvas[MAXB];
|
||||
static uint32_t counts[MAXB];
|
||||
const uint64_t seq = host_sampler_seq();
|
||||
const uint64_t oldest = seq > HOST_SAMPLER_CAP ? seq - HOST_SAMPLER_CAP : 0;
|
||||
int since = json_get_int(json, "since", -1);
|
||||
const int frames = json_get_int(json, "frames", -1);
|
||||
const int pass = json_get_int(json, "pass", -1);
|
||||
int top = json_get_int(json, "top", 60);
|
||||
if (top < 1) top = 1;
|
||||
if (top > 400) top = 400;
|
||||
uint64_t from = since >= 0 ? (uint64_t)since : oldest;
|
||||
if (from < oldest) from = oldest;
|
||||
uint32_t frame_lo = 0;
|
||||
if (frames > 0) frame_lo = (uint32_t)s_frame_count - (uint32_t)frames;
|
||||
int nb = 0;
|
||||
uint64_t total = 0, dropped = 0;
|
||||
for (uint64_t i = from; i < seq; i++) {
|
||||
HostSample smp;
|
||||
if (!host_sampler_get(i, &smp)) continue;
|
||||
if (frames > 0 && (int32_t)(smp.frame - frame_lo) < 0) continue;
|
||||
if (pass >= 0 && (int)smp.in_pass != pass) continue;
|
||||
total++;
|
||||
int b = 0;
|
||||
while (b < nb && rvas[b] != smp.rva) b++;
|
||||
if (b == nb) {
|
||||
if (nb == MAXB) { dropped++; continue; }
|
||||
rvas[nb] = smp.rva; counts[nb] = 0; nb++;
|
||||
}
|
||||
counts[b]++;
|
||||
}
|
||||
size_t cap = 256 + (size_t)top * 48;
|
||||
char *buf = (char *)malloc(cap);
|
||||
if (!buf) { send_err(id, "oom"); return; }
|
||||
size_t n = (size_t)snprintf(buf, cap,
|
||||
"{\"id\":%d,\"ok\":true,\"supported\":%d,\"seq\":%llu,"
|
||||
"\"image_base\":\"0x%llX\",\"samples\":%llu,\"unbinned\":%llu,\"top\":[",
|
||||
id, host_sampler_supported(), (unsigned long long)seq,
|
||||
(unsigned long long)host_sampler_image_base(),
|
||||
(unsigned long long)total, (unsigned long long)dropped);
|
||||
for (int k = 0; k < top; k++) {
|
||||
int best = -1;
|
||||
for (int b = 0; b < nb; b++)
|
||||
if (counts[b] && (best < 0 || counts[b] > counts[best])) best = b;
|
||||
if (best < 0) break;
|
||||
n += (size_t)snprintf(buf + n, cap - n, "%s[\"0x%llX\",%u]", k ? "," : "",
|
||||
(unsigned long long)rvas[best], counts[best]);
|
||||
counts[best] = 0;
|
||||
}
|
||||
snprintf(buf + n, cap - n, "]}");
|
||||
send_fmt("%s", buf);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* pgxp_shadow — read PGXP shadow slots: {"cmd":"pgxp_shadow","addr":A,
|
||||
* "count":N} walks N guest words from A (RAM / scratchpad); "space":"gpr" or
|
||||
* "gte" with "index"/"count" reads register shadows instead. Each slot: the
|
||||
@@ -14459,6 +14522,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "geom_correction", handle_geom_correction },
|
||||
{ "pgxp", handle_pgxp },
|
||||
{ "pgxp_shadow", handle_pgxp_shadow },
|
||||
{ "host_profile", handle_host_profile },
|
||||
{ "pgxp_miss_ring", handle_pgxp_miss_ring },
|
||||
{ "ws_aspect_cone_site", handle_ws_aspect_cone_site },
|
||||
{ "ws_margin", handle_ws_margin },
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/* host_sampler.c - always-on host CPU sampler for the emulation thread
|
||||
* (debug-tools builds; TCP host_profile, tools/host_profile.py).
|
||||
*
|
||||
* A background thread samples the emulation thread's instruction pointer
|
||||
* every millisecond into a ring, tagged with the guest frame and whether a
|
||||
* render pass was running. The ring covers the last ~65 seconds, so a probe
|
||||
* asks for a window that already happened instead of arming a profiler
|
||||
* before a workload. Addresses are stored relative to the executable's load
|
||||
* base; tools/host_profile.py maps them to functions with the image's own
|
||||
* symbol table (nm), so no debug info is needed.
|
||||
*
|
||||
* Windows: SuspendThread / GetThreadContext / ResumeThread from a sampler
|
||||
* thread. POSIX: the process CPU-time timer (SIGPROF) delivered to the
|
||||
* emulation thread, which reads its own interrupted PC from the ucontext.
|
||||
* Other hosts report the sampler as unsupported. */
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
|
||||
#if defined(__linux__) && !defined(_GNU_SOURCE)
|
||||
#define _GNU_SOURCE 1 /* REG_RIP, dladdr */
|
||||
#endif
|
||||
#include "host_sampler.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
extern int g_psx_render_pass_active;
|
||||
extern uint64_t s_frame_count;
|
||||
|
||||
static HostSample s_ring[HOST_SAMPLER_CAP];
|
||||
static volatile uint64_t s_seq;
|
||||
static uintptr_t s_image_base;
|
||||
static int s_supported;
|
||||
|
||||
static void record(uintptr_t pc) {
|
||||
const uint64_t n = s_seq;
|
||||
HostSample *e = &s_ring[n % HOST_SAMPLER_CAP];
|
||||
e->rva = (pc >= s_image_base) ? (uint64_t)(pc - s_image_base) : 0;
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
e->in_pass = g_psx_render_pass_active ? 1u : 0u;
|
||||
s_seq = n + 1;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
|
||||
static HANDLE s_target;
|
||||
|
||||
#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
|
||||
#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
|
||||
#endif
|
||||
|
||||
static DWORD WINAPI sampler_main(LPVOID arg) {
|
||||
(void)arg;
|
||||
/* A 1 ms period needs a high-resolution timer; Sleep(1) would follow
|
||||
* whatever timer resolution the process last asked for. */
|
||||
HANDLE timer = CreateWaitableTimerExW(NULL, NULL,
|
||||
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
|
||||
TIMER_ALL_ACCESS);
|
||||
if (timer) {
|
||||
LARGE_INTEGER due;
|
||||
due.QuadPart = -10000; /* 1 ms, relative */
|
||||
if (!SetWaitableTimer(timer, &due, 1, NULL, NULL, FALSE)) {
|
||||
CloseHandle(timer);
|
||||
timer = NULL;
|
||||
}
|
||||
}
|
||||
for (;;) {
|
||||
if (timer) WaitForSingleObject(timer, INFINITE);
|
||||
else Sleep(1);
|
||||
if (SuspendThread(s_target) == (DWORD)-1) continue;
|
||||
CONTEXT ctx;
|
||||
memset(&ctx, 0, sizeof ctx);
|
||||
ctx.ContextFlags = CONTEXT_CONTROL;
|
||||
if (GetThreadContext(s_target, &ctx)) {
|
||||
#if defined(_M_X64) || defined(__x86_64__)
|
||||
record((uintptr_t)ctx.Rip);
|
||||
#elif defined(_M_ARM64) || defined(__aarch64__)
|
||||
record((uintptr_t)ctx.Pc);
|
||||
#endif
|
||||
}
|
||||
ResumeThread(s_target);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void host_sampler_start(void) {
|
||||
if (s_supported) return;
|
||||
s_image_base = (uintptr_t)GetModuleHandleW(NULL);
|
||||
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
|
||||
GetCurrentProcess(), &s_target,
|
||||
THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT |
|
||||
THREAD_QUERY_INFORMATION,
|
||||
FALSE, 0))
|
||||
return;
|
||||
HANDLE t = CreateThread(NULL, 0, sampler_main, NULL, 0, NULL);
|
||||
if (!t) return;
|
||||
SetThreadPriority(t, THREAD_PRIORITY_TIME_CRITICAL);
|
||||
CloseHandle(t);
|
||||
s_supported = 1;
|
||||
}
|
||||
|
||||
#elif defined(__linux__) || defined(__APPLE__)
|
||||
#include <signal.h>
|
||||
#include <sys/time.h>
|
||||
#include <ucontext.h>
|
||||
#include <pthread.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
static pthread_t s_thread;
|
||||
|
||||
static void on_prof(int sig, siginfo_t *si, void *uc_) {
|
||||
(void)sig; (void)si;
|
||||
if (!pthread_equal(pthread_self(), s_thread)) return;
|
||||
ucontext_t *uc = (ucontext_t *)uc_;
|
||||
#if defined(__linux__) && defined(__x86_64__)
|
||||
record((uintptr_t)uc->uc_mcontext.gregs[REG_RIP]);
|
||||
#elif defined(__linux__) && defined(__aarch64__)
|
||||
record((uintptr_t)uc->uc_mcontext.pc);
|
||||
#elif defined(__APPLE__) && defined(__x86_64__)
|
||||
record((uintptr_t)uc->uc_mcontext->__ss.__rip);
|
||||
#elif defined(__APPLE__) && defined(__aarch64__)
|
||||
record((uintptr_t)uc->uc_mcontext->__ss.__pc);
|
||||
#else
|
||||
(void)uc;
|
||||
#endif
|
||||
}
|
||||
|
||||
void host_sampler_start(void) {
|
||||
if (s_supported) return;
|
||||
Dl_info info;
|
||||
if (dladdr((void *)&host_sampler_start, &info) && info.dli_fbase)
|
||||
s_image_base = (uintptr_t)info.dli_fbase;
|
||||
s_thread = pthread_self();
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_sigaction = on_prof;
|
||||
sa.sa_flags = SA_SIGINFO | SA_RESTART;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
if (sigaction(SIGPROF, &sa, NULL) != 0) return;
|
||||
struct itimerval it;
|
||||
it.it_interval.tv_sec = 0;
|
||||
it.it_interval.tv_usec = 1000;
|
||||
it.it_value = it.it_interval;
|
||||
if (setitimer(ITIMER_PROF, &it, NULL) != 0) return;
|
||||
s_supported = 1;
|
||||
}
|
||||
|
||||
#else
|
||||
void host_sampler_start(void) {}
|
||||
#endif
|
||||
|
||||
int host_sampler_supported(void) { return s_supported; }
|
||||
|
||||
uint64_t host_sampler_seq(void) { return s_seq; }
|
||||
|
||||
uint64_t host_sampler_image_base(void) { return (uint64_t)s_image_base; }
|
||||
|
||||
int host_sampler_get(uint64_t seq, HostSample *out) {
|
||||
const uint64_t now = s_seq;
|
||||
if (seq >= now || now - seq > HOST_SAMPLER_CAP) return 0;
|
||||
*out = s_ring[seq % HOST_SAMPLER_CAP];
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endif /* !PSX_NO_DEBUG_TOOLS */
|
||||
@@ -85,6 +85,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#include "recomp_audio_drc.h"
|
||||
#include "memcard.h"
|
||||
#include "debug_server.h"
|
||||
#include "host_sampler.h"
|
||||
#include "crash_trace.h"
|
||||
#include "freeze_heartbeat.h"
|
||||
#include "config_loader.h"
|
||||
@@ -16210,6 +16211,7 @@ session_reboot:
|
||||
std::atexit(game_options_save_now);
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
debug_server_init(debug_port);
|
||||
host_sampler_start(); /* this is the emulation thread */
|
||||
#else
|
||||
(void)debug_port;
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user