2.1 Startup and the frontends¶
Everything that happens before the first emulated instruction lives in
src/main.cpp. It parses the command line, resolves an SD-card image, builds
an EmulatorConfig, picks one of the three frontends, hands it the config and
runs it. There is no second entry point: the GUI, the SDL frontend and the
headless runner all reach the machine through that same function, which is what
keeps them from drifting apart.
Parsing: the option table¶
Arguments are dispatched from cli::OPTIONS in src/core/cli_options.h, a
constexpr array whose rows are { name, arity, doc-status, id }. The parsing
loop looks each argument up with cli::find(), consumes as many following
values as the row's arity says, and switches on opt->id.
Holding the flag set as data rather than as a chain of string comparisons is not a style preference — it buys three separate guarantees:
- The parser physically cannot accept a flag that is not in the table. And
because
CMakeLists.txtsets-Werror=switch, a row added to the table with no matching arm in theOptIdswitch is a compile error rather than a flag that silently does nothing. cli_options_testcan enumerate the table and diff it against the OPTIONS section of the man page in both directions. An implemented flag that nobody documented and a documented flag that nobody implemented are therefore equally hard failures ofmake cli-check.- Where an exception is genuinely wanted, it is declared on the row itself
instead of being hidden in the checker. There is exactly one:
--sd-card, markedDoc::UndocumentedAlias, a back-compatible spelling that keeps working but is deliberately not advertised.
Two argument shapes bypass the table lookup, both on purpose.
--log-level=VALUE is the one inline-value form and is matched by prefix
before the lookup happens, because scripts using that spelling predate the
table. And a bare argument that does not begin with - is taken as the program
to load, so jnext game.tap does what you would expect. Anything that does
begin with - and is not in the table is an error rather than a filename,
because a mistyped flag must never be quietly swallowed as one.
Before the frontend exists¶
Two things happen in the gap between parsing and constructing a frontend.
First, in a Qt build on a non-headless run, AppConfig is loaded from
~/.jnext/jnext.conf, early enough that a saved SD-card path can seed the
resolution step below. CLI values always win: a saved value is consulted
only where the corresponding flag was absent. Headless runs do not read the
configuration file at all, because a preference saved on a developer's machine
must never be able to change the outcome of an automated run.
Second, the SD-card image is resolved by sdcard::provision_sd_card() in
src/core/sdcard_provisioner.h. It tries an explicit --sdcard first, then
the image cached under ~/.jnext/sdcard/, and failing both it offers to
download and patch the canonical distribution image. Headless asks on the
terminal; the GUI spins up a temporary QApplication for the dialogs and tears
it down again before QtApp builds its own. If no image can be resolved,
startup fails outright — every ROM except the FPGA boot ROM is read out of that
image, exactly as on real hardware, so there is no machine to run without one.
Choosing a frontend¶
The choice comes down to a single runtime condition wrapped around a single compile-time arm:
if (headless) {
HeadlessApp app; result = configure_and_run(app);
} else {
#ifdef ENABLE_QT_UI
QtApp app; result = configure_and_run(app);
#else
SdlApp app; result = configure_and_run(app);
#endif
}
configure_and_run is a generic lambda, and that is what lets a single body
drive three classes that share no base class. They implement the same informal
interface — set_config, init, run, shutdown, emulator, exit_code —
and the handful of steps that genuinely belong to only one of them are guarded
with if constexpr on the deduced type. --delayed-snapshot wiring is
HeadlessApp-only that way, and some GUI plumbing is QtApp-only.
QtApp(src/gui/qt_app.h) owns aQApplicationand aMainWindowwhose central widget is anEmulatorWidget. AQTimerdrives the machine, paced fromEmulator::frame_period_ms()and the current speed multiplier. SDL is still linked into this build, but only to get audio out.SdlApp(src/platform/sdl_app.h) owns anSdlDisplay, anSdlInputand anSdlAudio, and runs its own event and pacing loop.HeadlessApp(src/platform/headless_app.h) owns nothing but theEmulator. Itsrun()is a barerun_frame()loop with no sleep in it.
Compile-time arms¶
| Option | Default | Effect |
|---|---|---|
ENABLE_QT_UI |
OFF in CMakeLists.txt; ON in every make target that builds a usable binary |
Compiles src/gui, links jnext_gui, selects QtApp over SdlApp. |
ENABLE_DEBUGGER |
ON |
Compiles src/debugger (the Qt panels). Turning it off removes the debugger UI entirely; src/debug, the pure-C++ backend it sits on, is always built. |
ENABLE_TESTS |
ON |
Builds the unit suites and registers them with CTest. |
The mismatch on the first row catches people out and is worth knowing: a raw
cmake invocation with no options produces an SDL-only binary, which is why
every Makefile target that is meant to yield a usable build passes
-DENABLE_QT_UI=ON explicitly. See
5.2 Build configurations.
What headless actually skips¶
--headless is a runtime flag understood by either build, not a separate
binary. It opens no window and no audio device, registers no host input
dispatchers, and never sleeps, so the frame loop advances as fast as the host
can manage. Everything else is the same machine doing the same work: the same
Emulator, the same rendering, and the same audio synthesis unless --silent
is given. That equivalence is what makes headless useful for measurement, and
it is why --benchmark is headless-only — run under a GUI frontend it would be
measuring the frame timer rather than the emulator.
Headless is also where JNEXT's deterministic automation lives. The
--delayed-screenshot, --delayed-snapshot, --delayed-keypress and
--delayed-automatic-exit family lets a script boot the machine, wait a
defined amount of emulated time, press keys and capture the screen without any
human present — which is how the project's own screenshot regression suite
drives the emulator. The delays are counted in emulated frames rather than
wall-clock seconds precisely so that a loaded machine produces the same bytes
as an idle one. And a capture that was requested but never taken makes the
process exit non-zero rather than quietly writing nothing, so a broken test run
cannot pass by producing no output. See 2.5 and
4.3 The regression suite.