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 myship.cpp radar.cpp random.cpp scenes.cpp score.cpp screen.cpp
filemap.cpp prefs.cpp cfgform.cpp options.cpp gamestate.cpp filemap.cpp prefs.cpp cfgform.cpp options.cpp gamestate.cpp
states.cpp form.cpp cfgparse.cpp game.cpp kobo.cpp logger.c 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}) 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 "myship.h"
#include "enemies.h" #include "enemies.h"
#include "prof.h" #include "prof.h"
#include "bench.h"
#define MAX_FPS_RESULTS 64 #define MAX_FPS_RESULTS 64
@ -1388,12 +1389,40 @@ int KOBO_main::open()
ct_engine.render_highlight = kobo_render_highlight; ct_engine.render_highlight = kobo_render_highlight;
wdash->mode(DASHBOARD_GAME); wdash->mode(DASHBOARD_GAME);
wradar->mode(RM_NOISE); 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); init_js(prefs);
gamecontrol.init(prefs->always_fire); gamecontrol.init(prefs->always_fire);
manage.init(); 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; return 0;
} }
@ -2055,6 +2084,7 @@ int main(int argc, char *argv[])
signal(SIGTERM, breakhandler); signal(SIGTERM, breakhandler);
signal(SIGINT, breakhandler); signal(SIGINT, breakhandler);
kobo_bench_init_env();
prof_init(); prof_init();
SDL_Init(0); SDL_Init(0);

View File

@ -51,6 +51,7 @@
#include "audio.h" #include "audio.h"
#include "random.h" #include "random.h"
#include "prof.h" #include "prof.h"
#include "bench.h"
#define GIGA 1000000000 #define GIGA 1000000000
@ -350,7 +351,10 @@ void _manage::init_resources_to_play(int newship)
next_state_out = 0; next_state_out = 0;
next_state_next = 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(); game_seed = gamerand.get_seed();
enemies.init(); enemies.init();
myship.init(); myship.init();
@ -608,6 +612,29 @@ void _manage::run_game()
update(); update();
++hi.playtime; ++hi.playtime;
prof_frame_tick(); 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. // 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 regenerate();
static void select_scene(int scene, int redraw_map = 1); static void select_scene(int scene, int redraw_map = 1);
static int scene() { return scene_num; } static int scene() { return scene_num; }
static void set_scene_num(int n) { scene_num = n; } // for bench harness
static void abort(); static void abort();
static void freeze_abort(); static void freeze_abort();
static int aborted() { return exit_manage; } static int aborted() { return exit_manage; }