Skip to content

Add a headless, fixed-step simulation mode for batch experiments - #7897

Open
BTawaifi wants to merge 3 commits into
scp-fs2open:masterfrom
BTawaifi:feature/headless-sim
Open

BTawaifi wants to merge 3 commits into
scp-fs2open:masterfrom
BTawaifi:feature/headless-sim

Conversation

@BTawaifi

Copy link
Copy Markdown

Split out of #7874, now on top of #7834. Works best with the afterburner cooldown fix (#7895) merged first, since afterburner cooldowns otherwise run on the real clock while headless runs simulate many seconds per real second.

An opt-in mode for running single-player missions unattended and fast, for balancing, regression and AI experiments. Nothing changes unless -headless is passed.

Option Effect
-headless No window, renderer, audio, speech, joystick, HUD/radar or movies; the stub renderer is used, model and collision data still load. Frames run as fast as the CPU allows on a mission clock that advances only by simulated time (wall-clock APIs are unchanged). Implies -noninteractive and -nomovies.
-fixed-dt <s> Authoritative step, default 1/30, range [1/65536, 0.25].
-max-sim-time <s> End the run after this much simulated time (at most 32767, the fixed-point limit).
-sim-result <path> Write a JSON result at shutdown: mission, seed, simulated and wall seconds, why the run ended (time_limit, player_died, start_failed, mission_exit) and the state of each mission goal.

The fast clock is not a separate option: on its own it would freeze menu timers and break multiplayer timing.

What runs: the normal authoritative frame (game_frame(), game_simulation_frame()), so objects, AI, weapons, collisions, goals and scripting all run. On Frame callbacks normally fire from gr_flip(); they are split out as gr_process_frame_callbacks() and run once per simulated frame. Keyboard and joystick input is not read, but read_player_controls() still runs, so scripted (Lua) flight control, player physics and the player warp-out sequence work.

Unattended flow: with -start_mission, the fiction and command briefing are skipped, a red-alert mission goes through the red alert screen as its Continue button does, the default loadout is committed through commit_pressed() (as for no-briefing missions), and the debriefing is left by quitting. A failed commit ends the run (start_failed). The death popup cannot be answered, so the player's death ends the run (player_died). Other modal popups are logged and answered with the abort answer, unless a script handles On Dialog Frame.

Second commit: under -noninteractive, Lua errors are now printed to stderr before the exception ends the process, as Error() already does. Release builds write no log, so before this a Lua error (e.g. a syntax error in a script table) ended an unattended run with no indication why.

Without -headless the additions are single boolean checks on the normal path (timer read, frame timing, radar_plot_object, popup(), the player-controls block).

Testing (retail sm1-01, Release unless noted):

Known limits: single player only; without -start_mission (or a script driving the menus) the engine waits at the main hall; long runs with many AI ships can still diverge for equal seeds.

New command-line options:
  -headless          no window, renderer, audio, speech, joystick, HUD/radar or
                     movies; implies -noninteractive and -nomovies. Graphics
                     runs on the stub renderer, and model geometry/collision
                     data still load. Frames run as fast as the CPU allows, on
                     a mission clock that advances only by simulated time
                     (wall-clock APIs are unchanged for profiling and UI
                     deadlines). The clock starts at one hour and never moves
                     backwards, so timestamps never collide with the "never"
                     and "immediate" values.
  -fixed-dt <s>      authoritative step in seconds (default 1/30, range
                     [1/65536, 0.25]).
  -max-sim-time <s>  end the run after this much simulated time (at most
                     32767; larger values overflow the fixed-point time type and
                     are rejected).
  -sim-result <path> write a JSON result (mission, seed, simulated and wall
                     seconds, why it stopped, and the state of each mission
                     goal) when the engine shuts down.

The unthrottled clock is not a separate option: on its own it would freeze menu
timers and break multiplayer timing, so it only comes with -headless.

Script frame callbacks (On Frame) normally run from gr_flip(); they are split
out as gr_process_frame_callbacks() so headless runs still execute them once per
simulated frame. Camera and photo-mode interpolation are skipped (presentation
only). Keyboard and joystick input is not read, but read_player_controls() still
runs, so scripted (Lua) flight control, player physics and the player warp-out
sequence work as usual.

Unattended single-player runs: -start_mission normally stops at the fiction,
command briefing, red alert and briefing screens and at the debriefing, waiting
for a player. Headless runs skip the fiction and command briefing, enter a
red-alert mission through the red alert screen as its Continue button does, and
otherwise go to the briefing state and take the default loadout through
commit_pressed() (the same call the engine makes for missions flagged
no-briefing). The debriefing is left by quitting. The death popup cannot be
answered, so once the player's ship has blown up the run ends. The result's
"termination" is time_limit, start_failed (the default loadout could not be
committed), player_died, or mission_exit.

Modal popups cannot be answered in a headless run, so popup() logs the text and
returns the abort answer instead of waiting (unless a script handles On Dialog
Frame). Without this a first launch of a mod with no settings file, or any
loadout-commit failure, hangs until the caller's timeout.
With -noninteractive, Error() and Warning() already print their text to stderr
before they abort or return, so the reason is visible without a dialog. A Lua
error only threw LuaErrorException, which ends the process as an unhandled
exception, and release builds write no log, so an unattended run that hit a Lua
error (for example a syntax error in a script table) gave no indication why.
The Lua error text and traceback now go to stderr first, as for Error().
Use nullptr for the new -headless parameter, and mark the two range checks
that are written as !(in range) on purpose: the suggested De Morgan form
(< min || > max) would accept NaN.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant