Files
psxrecomp/runtime/src/beetle_main.cpp
T
Matthew Stanley ffc15219fd fix: Bug B hard freeze — TOCTOU underflow in frame pacing slept ~24.7 days
Root cause (freeze dump 1781046069, addr2line-verified): the wall-clock
pacing loop in sdl_vblank_present read the performance counter twice per
iteration — once in the while-condition, once to compute remaining time.
If the frame deadline was crossed between the two reads (a preemption
window), the unsigned subtraction underflowed, *1000 overflowed, and
SDL_Delay received ~2^64/10^4 ms mod 2^32 ≈ 24.7 days at the standard
10 MHz QPC. The app parks in one SDL_Delay with every counter frozen,
wedged between 'i_stat |= VBLANK' (interrupts.c:286) and exception
delivery — which is why dumps showed i_stat=1 pinned. VBLANK was being
ACK'd every frame right up to the wedge in every dump examined; the
'VBLANK-ACK reentry storm' framing was a symptom misread.

- frame_pacing.c/h: shared race-free pacer (single counter read per
  decision, compare-before-subtract, sleep clamped to one period).
  Both call sites switched: main.cpp sdl_vblank_present and
  beetle_main.cpp. Unit test pins the underflow case.
- Vsync self-heal: the 'recovered storm' dump (1781045865) was 8/8
  main-thread samples inside wglSwapBuffers — NV driver blocking ~1.5s
  per present. After 3 presents >250ms, drop driver vsync (we pace to
  59.94 Hz ourselves). Counters surfaced in the freeze dump.
- Watchdog classifier: 'reentry_storm' threshold (5K/sec) was below the
  healthy baseline (~2K reentry blocks/frame × 30-60 fps), mislabeling
  normal play and host stalls — that label misdirected two prior
  investigations. Now normalized per frame (>20K/frame), plus a new
  slow_frames kind (<5 fps) with multi-sample stack capture.

Verified: test_frame_pacing ALL PASS; live Tomba paced at 59.962 fps
over 20s (single-process measurement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 20:09:25 -07:00

178 lines
5.8 KiB
C++

/* beetle_main.cpp — psx-beetle entry point.
* Standalone harness around Beetle PSX libretro core. SDL window for
* Beetle's framebuffer, keyboard input, TCP debug server on port 4380. */
#include <SDL.h>
#include "frame_pacing.h"
#include <cstdio>
#include <cstring>
#include <cstdlib>
extern "C" {
int beetle_init(const char* bios_path);
int beetle_init_with_disc(const char* bios_path, const char* disc_path);
void beetle_shutdown(void);
void beetle_run_frame(uint16_t pad1_buttons);
int beetle_get_framebuffer(uint32_t **out_pixels, unsigned *out_w, unsigned *out_h);
void beetle_debug_server_init(int port);
void beetle_debug_server_poll(void);
void beetle_debug_server_shutdown(void);
int beetle_debug_server_get_input_override(void);
}
#define PAD_SELECT (1 << 0)
#define PAD_START (1 << 3)
#define PAD_UP (1 << 4)
#define PAD_RIGHT (1 << 5)
#define PAD_DOWN (1 << 6)
#define PAD_LEFT (1 << 7)
#define PAD_L2 (1 << 8)
#define PAD_R2 (1 << 9)
#define PAD_L1 (1 << 10)
#define PAD_R1 (1 << 11)
#define PAD_TRIANGLE (1 << 12)
#define PAD_CIRCLE (1 << 13)
#define PAD_CROSS (1 << 14)
#define PAD_SQUARE (1 << 15)
static uint16_t pad_from_keyboard(void) {
const Uint8* keys = SDL_GetKeyboardState(NULL);
uint16_t b = 0xFFFF;
if (keys[SDL_SCANCODE_UP]) b &= ~PAD_UP;
if (keys[SDL_SCANCODE_DOWN]) b &= ~PAD_DOWN;
if (keys[SDL_SCANCODE_LEFT]) b &= ~PAD_LEFT;
if (keys[SDL_SCANCODE_RIGHT]) b &= ~PAD_RIGHT;
if (keys[SDL_SCANCODE_RETURN]) b &= ~PAD_START;
if (keys[SDL_SCANCODE_RSHIFT]) b &= ~PAD_SELECT;
if (keys[SDL_SCANCODE_X]) b &= ~PAD_CROSS;
if (keys[SDL_SCANCODE_Z]) b &= ~PAD_SQUARE;
if (keys[SDL_SCANCODE_S]) b &= ~PAD_CIRCLE;
if (keys[SDL_SCANCODE_A]) b &= ~PAD_TRIANGLE;
if (keys[SDL_SCANCODE_Q]) b &= ~PAD_L1;
if (keys[SDL_SCANCODE_W]) b &= ~PAD_R1;
if (keys[SDL_SCANCODE_E]) b &= ~PAD_L2;
if (keys[SDL_SCANCODE_R]) b &= ~PAD_R2;
return b;
}
int main(int argc, char** argv) {
std::setvbuf(stdout, nullptr, _IOLBF, 0);
std::setvbuf(stderr, nullptr, _IOLBF, 0);
const char* bios_path = "bios/SCPH1001.BIN";
const char* disc_path = NULL;
for (int i = 1; i < argc; i++) {
if (!std::strcmp(argv[i], "--disc") && i + 1 < argc) {
disc_path = argv[++i];
} else if (argv[i][0] != '-') {
bios_path = argv[i];
}
}
std::fprintf(stdout, "psx-beetle: loading BIOS from %s\n", bios_path);
if (disc_path) {
std::fprintf(stdout, "psx-beetle: loading disc from %s\n", disc_path);
}
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
return 1;
}
SDL_Window* win = SDL_CreateWindow(
"psx-beetle — Beetle PSX",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
640, 480,
SDL_WINDOW_SHOWN);
if (!win) {
std::fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError());
return 1;
}
/* OpenGL renderer — see main.cpp note. Software renderer (GDI) hangs
* the SDL main thread under heavy emulation load after the FMV-speed
* fix raised cycle throughput. OpenGL avoids the GDI path. */
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl");
SDL_Renderer* ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);
if (!ren) {
std::fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError());
return 1;
}
SDL_Texture* tex = SDL_CreateTexture(
ren, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 640, 512);
if (!tex) {
std::fprintf(stderr, "SDL_CreateTexture failed: %s\n", SDL_GetError());
return 1;
}
if (beetle_init_with_disc(bios_path, disc_path) != 0) {
std::fprintf(stderr, "beetle_init failed\n");
return 1;
}
#ifndef PSX_NO_DEBUG_TOOLS
#ifdef DEFAULT_DEBUG_PORT
beetle_debug_server_init(DEFAULT_DEBUG_PORT);
#else
beetle_debug_server_init(4380);
#endif
#endif
static uint32_t pixels[640 * 512];
for (;;) {
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
if (ev.type == SDL_QUIT) {
#ifndef PSX_NO_DEBUG_TOOLS
beetle_debug_server_shutdown();
#endif
beetle_shutdown();
SDL_DestroyTexture(tex);
SDL_DestroyRenderer(ren);
SDL_DestroyWindow(win);
SDL_Quit();
return 0;
}
}
#ifndef PSX_NO_DEBUG_TOOLS
beetle_debug_server_poll();
int override = beetle_debug_server_get_input_override();
#else
int override = -1;
#endif
uint16_t pad = (override >= 0) ? (uint16_t)override : pad_from_keyboard();
beetle_run_frame(pad);
uint32_t* fb = nullptr;
unsigned w = 0, h = 0;
if (beetle_get_framebuffer(&fb, &w, &h) && fb && w && h) {
if (w > 640) w = 640;
if (h > 512) h = 512;
for (unsigned y = 0; y < h; y++) {
for (unsigned x = 0; x < w; x++) {
pixels[y * w + x] = fb[y * w + x] | 0xFF000000u;
}
}
SDL_UpdateTexture(tex, NULL, pixels, (int)(w * sizeof(uint32_t)));
SDL_Rect dst = { 0, 0, 640, 480 };
SDL_RenderClear(ren);
SDL_RenderCopy(ren, tex, NULL, &dst);
SDL_RenderPresent(ren);
}
/* Wall-clock pacing to PSX-native 59.94 Hz for apples-to-apples
* comparison with psx-runtime. Press TAB on the Beetle window to
* sustain unlocked rate (turbo). */
const Uint8* keys = SDL_GetKeyboardState(NULL);
if (keys && keys[SDL_SCANCODE_TAB]) continue;
constexpr double FRAME_MS = 1000.0 / 59.94;
static FramePacer pacer = { 0 };
frame_pacer_wait(&pacer, FRAME_MS);
}
}