Add deterministic bench harness (KOBO_BENCH=1)

When KOBO_BENCH=1 is set in the environment, the game becomes a
hands-off benchmark:

  * Skips the intro/menu sequence and pushes st_game directly with
    scene=0, skill=CLASSIC, invulnerable ship, always_fire = constant
    auto-fire, countdown = 0 so "GET READY" auto-pops in 700ms.
  * Seeds pubrand and gamerand from KOBO_BENCH_SEED (default 12345)
    so the map, stress spawn pattern, and enemy AI are identical
    run-to-run.
  * Defaults KOBO_STRESS=1500 unless overridden, so the move pass has
    a steady workload, and KOBO_PROF=1 so the profiler is on.
  * Counts frames in run_game; after KOBO_BENCH_FRAMES (default 1800)
    it dumps prof to stderr and exits cleanly.

Usage from repo root (the cwd matters — relative GFX paths only
resolve from here):
    KOBO_BENCH=1 ./build/kobodl 2>bench-logs/phaseN.log

A single command produces a comparable profiler dump at every phase
of the DOD refactor — no menus to navigate, no keyboard input,
identical workload.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ville Lindholm 2026-06-04 15:22:17 +03:00
parent 869056c5d2
commit 0263be65d4
No known key found for this signature in database
GPG Key ID: 89AE9EAA3B6FDE7C
6 changed files with 133 additions and 4 deletions

View File

@ -160,7 +160,7 @@ set(KOBO_SOURCES
myship.cpp radar.cpp random.cpp scenes.cpp score.cpp screen.cpp
filemap.cpp prefs.cpp cfgform.cpp options.cpp gamestate.cpp
states.cpp form.cpp cfgparse.cpp game.cpp kobo.cpp logger.c
dashboard.cpp sound.cpp prof.cpp
dashboard.cpp sound.cpp prof.cpp bench.cpp
)
add_executable(kobodl ${KOBO_SOURCES})

35
bench.cpp Normal file
View File

@ -0,0 +1,35 @@
#include "bench.h"
#include <stdio.h>
#include <stdlib.h>
int kobo_bench_enabled = 0;
int kobo_bench_frames = 1800;
Uint32 kobo_bench_seed = 12345;
extern "C" void kobo_bench_init_env(void)
{
const char *e = getenv("KOBO_BENCH");
kobo_bench_enabled = (e && *e && *e != '0') ? 1 : 0;
const char *f = getenv("KOBO_BENCH_FRAMES");
if (f && *f) kobo_bench_frames = atoi(f);
const char *s = getenv("KOBO_BENCH_SEED");
if (s && *s) kobo_bench_seed = (Uint32)strtoul(s, NULL, 10);
if (!kobo_bench_enabled) return;
// Default a steady stress workload so the move pass has predictable cost.
if (!getenv("KOBO_STRESS"))
setenv("KOBO_STRESS", "1500", 1);
// Profiler is the whole point of running in bench mode.
if (!getenv("KOBO_PROF"))
setenv("KOBO_PROF", "1", 1);
fprintf(stderr,
"[bench] enabled: frames=%d seed=%u stress=%s prof=%s\n",
kobo_bench_frames,
(unsigned)kobo_bench_seed,
getenv("KOBO_STRESS"),
getenv("KOBO_PROF"));
}

36
bench.h Normal file
View File

@ -0,0 +1,36 @@
// Deterministic, hands-off benchmark mode.
//
// Activated by KOBO_BENCH=1 in the environment. When on, the game:
// * skips the intro/menu sequence and drops straight into stage 0
// * seeds gamerand and pubrand from KOBO_BENCH_SEED (default 12345)
// * enables cmd_indicator (collisions register but neither side takes
// damage — true invulnerability for deterministic workload),
// cmd_cheat (skip score recording), and always_fire (auto-fire)
// * defaults KOBO_STRESS=1500 unless overridden, for a steady workload
// * dumps the profiler table and exits after KOBO_BENCH_FRAMES
// run_game frames (default 1800 = ~60s at the game's tick rate)
//
// Together this makes a single command (`KOBO_BENCH=1 ./kobodl`) produce a
// reproducible profiler dump across phases — no menu navigation, no keyboard
// input, identical RNG and spawn pattern run-to-run.
#ifndef KOBO_BENCH_H
#define KOBO_BENCH_H
#include "sdl_compat.h"
#ifdef __cplusplus
extern "C" {
#endif
extern int kobo_bench_enabled;
extern int kobo_bench_frames;
extern Uint32 kobo_bench_seed;
void kobo_bench_init_env(void);
#ifdef __cplusplus
}
#endif
#endif // KOBO_BENCH_H

View File

@ -62,6 +62,7 @@ extern "C" {
#include "myship.h"
#include "enemies.h"
#include "prof.h"
#include "bench.h"
#define MAX_FPS_RESULTS 64
@ -1388,12 +1389,40 @@ int KOBO_main::open()
ct_engine.render_highlight = kobo_render_highlight;
wdash->mode(DASHBOARD_GAME);
wradar->mode(RM_NOISE);
// Bench mode overrides flags that downstream init reads (always_fire is
// captured by gamecontrol.init at call-time, so it has to be set before).
if(kobo_bench_enabled)
{
// cmd_indicator is "collision testing mode" — collisions still
// register sounds but neither side takes damage, so the ship
// can't die and the bench workload doesn't drift across runs.
// (cmd_cheat is *unlimited lives*, not invulnerability.)
prefs->cmd_indicator = 1;
prefs->cmd_cheat = 1; // skip score recording on exit
prefs->always_fire = 1; // constant auto-fire
prefs->countdown = 0; // auto-pop the "GET READY" wait
}
if(kobo_bench_enabled)
pubrand.init(kobo_bench_seed);
else
pubrand.init();
init_js(prefs);
gamecontrol.init(prefs->always_fire);
manage.init();
if(kobo_bench_enabled)
{
// Hands-off bench: jump straight into stage 0, no menu navigation.
scorefile.profile()->skill = SKILL_CLASSIC;
manage.set_scene_num(0);
gsm.push(&st_game);
}
else
{
gsm.push(&st_intro_title);
}
return 0;
}
@ -2055,6 +2084,7 @@ int main(int argc, char *argv[])
signal(SIGTERM, breakhandler);
signal(SIGINT, breakhandler);
kobo_bench_init_env();
prof_init();
SDL_Init(0);

View File

@ -51,6 +51,7 @@
#include "audio.h"
#include "random.h"
#include "prof.h"
#include "bench.h"
#define GIGA 1000000000
@ -350,6 +351,9 @@ void _manage::init_resources_to_play(int newship)
next_state_out = 0;
next_state_next = 0;
if(kobo_bench_enabled)
gamerand.init(kobo_bench_seed); // deterministic map + spawn pattern
else
gamerand.init();
game_seed = gamerand.get_seed();
enemies.init();
@ -608,6 +612,29 @@ void _manage::run_game()
update();
++hi.playtime;
prof_frame_tick();
// Bench harness: stop the program after a fixed number of frames so a
// single command produces a comparable profiler dump across phases.
// Push SDL_EVENT_QUIT instead of calling exit() — exit() runs the
// atexit handler which tries to shut down SDL audio synchronously and
// hangs on macOS. Going through the engine's own quit path tears down
// in the right order.
if(kobo_bench_enabled && (int)hi.playtime >= kobo_bench_frames)
{
static int triggered = 0;
if(!triggered)
{
triggered = 1;
fprintf(stderr,
"[bench] reached %d frames; dumping profile and quitting\n",
kobo_bench_frames);
prof_dump(stderr);
SDL_Event ev;
SDL_zero(ev);
ev.type = SDL_EVENT_QUIT;
SDL_PushEvent(&ev);
}
}
}
// Top up the enemy pool with rocks until we hit the target population.

View File

@ -95,6 +95,7 @@ class _manage
static void regenerate();
static void select_scene(int scene, int redraw_map = 1);
static int scene() { return scene_num; }
static void set_scene_num(int n) { scene_num = n; } // for bench harness
static void abort();
static void freeze_abort();
static int aborted() { return exit_manage; }