From 0263be65d418dca0ad13176314b95d538340aeff Mon Sep 17 00:00:00 2001 From: Ville Lindholm Date: Thu, 4 Jun 2026 15:22:17 +0300 Subject: [PATCH] Add deterministic bench harness (KOBO_BENCH=1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CMakeLists.txt | 2 +- bench.cpp | 35 +++++++++++++++++++++++++++++++++++ bench.h | 36 ++++++++++++++++++++++++++++++++++++ kobo.cpp | 34 ++++++++++++++++++++++++++++++++-- manage.cpp | 29 ++++++++++++++++++++++++++++- manage.h | 1 + 6 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 bench.cpp create mode 100644 bench.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 10aa884..19424e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/bench.cpp b/bench.cpp new file mode 100644 index 0000000..75822d1 --- /dev/null +++ b/bench.cpp @@ -0,0 +1,35 @@ +#include "bench.h" +#include +#include + +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")); +} diff --git a/bench.h b/bench.h new file mode 100644 index 0000000..491ec5e --- /dev/null +++ b/bench.h @@ -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 diff --git a/kobo.cpp b/kobo.cpp index 780d682..0f06a28 100644 --- a/kobo.cpp +++ b/kobo.cpp @@ -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); - pubrand.init(); + + // 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(); - gsm.push(&st_intro_title); + 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); diff --git a/manage.cpp b/manage.cpp index 851f712..512b344 100644 --- a/manage.cpp +++ b/manage.cpp @@ -51,6 +51,7 @@ #include "audio.h" #include "random.h" #include "prof.h" +#include "bench.h" #define GIGA 1000000000 @@ -350,7 +351,10 @@ void _manage::init_resources_to_play(int newship) next_state_out = 0; next_state_next = 0; - gamerand.init(); + if(kobo_bench_enabled) + gamerand.init(kobo_bench_seed); // deterministic map + spawn pattern + else + gamerand.init(); game_seed = gamerand.get_seed(); enemies.init(); myship.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. diff --git a/manage.h b/manage.h index c71776b..fef0cd5 100644 --- a/manage.h +++ b/manage.h @@ -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; }