1.3 The rules that shape the code¶
If you read one page of this guide before touching the code, make it this one.
JNEXT holds a handful of project-wide invariants that are not visible from any single file. You cannot discover them by reading the function you are about to change, and each of them exists for the same reason: the failure it prevents already happened once, and nobody noticed at the time. The practical consequence is that breaking one of these rules rarely produces an error message about the rule. It produces a red test run somewhere else entirely, about something that looks unrelated to your change. Five minutes here saves that afternoon.
The VHDL is the specification¶
The authoritative description of the hardware is not a manual or a wiki page.
It is the VHDL source of the official ZX Spectrum Next FPGA core — the
cores/zxnext/src/ tree of the FPGA repository, which is expected to be
checked out beside this one. When JNEXT and the documentation disagree about
what the machine does, the VHDL decides.
Calling it "authoritative" is not a slogan. In practice it means three specific things.
Expected values come from the VHDL, never from the current code. This is
the rule most easily broken with good intentions. A test written by reading
Layer2::render_scanline and asserting what it currently produces proves only
that the function still does what it did yesterday — it will happily pin a bug
in place and then defend it. The expected value has to come from the hardware
description instead. doc/testing/UNIT-TEST-PLAN-EXECUTION.md is the process
document for all of this, and it is mandatory reading before you write or
rewrite a subsystem's test plan.
Fixes are faithful, not convenient. When a defect could be closed either by
a targeted hack or by transcribing what the VHDL actually does, the
transcription wins, even when it is the larger job. The comment blocks
scattered through the source that quote signal names and line numbers —
zxnext.vhd:7103, zxula_timing.vhd:577 — are the record of those decisions,
and they exist so the next reader can check the claim rather than take it on
faith.
Citations are machine-checked. Those references are not decorative.
doc/testing/TRACEABILITY-MATRIX.md is a generated table mapping plan row →
test ID → VHDL citation → test location, and its generator validates every one
of those citations against the real FPGA source tree, so a reference that has
been typo'd or points at a file that no longer exists is caught rather than
published. See 4.4 Traceability.
Some documents are generated, committed, and gated¶
Four outputs in the repository are produced by a tool and then checked into git anyway, so that a fresh clone can read them with no toolchain installed at all:
| Output | Source | Regenerate with |
|---|---|---|
doc/man/jnext.1 and USAGE.md |
doc/man/jnext.1.md |
make docs-man |
doc/user-guide/ |
src/doc/user-guide/ |
make docs-userguide |
doc/developer-guide/ and src/doc/developer-guide/img/*.svg |
src/doc/developer-guide/ and diagrams/*.dot |
make docs-devguide |
doc/testing/TRACEABILITY-MATRIX.md |
the test sources and plan docs | regenerated by make unit-test |
Committing a generated file has an obvious hazard: the copy in git can stop
matching the source it came from, and a stale copy is a silent lie that no
reviewer can see by reading the diff. So the project closes that hole
mechanically — make docs-check is a prerequisite of both make unit-test
and make regression, which means a stale generated document fails the test
run itself rather than waiting to be noticed. What that asks of you is simple:
edit the source, re-render, and commit both halves in the same change.
On a machine without pandoc, mkdocs or graphviz, or with a different version of them than the committed render was made with, the relevant part of the check fails — locally and in CI alike. A check that quietly skips is indistinguishable from a check that passed, and the project has a rule for that: a SKIP is a failure of the gate.
It is worth being precise about what this gate does and does not establish.
docs-check proves the outputs match jnext.1.md. It does not prove that
jnext.1.md describes the command line the program actually parses — that is
make cli-check, and even then only for the flag set. The prose in the man
page's narrative sections is checked by nobody at all. See
4.5 The documentation and CLI gates.
The user guide's Qt screenshots are generated, but not gated¶
src/doc/user-guide/img/*.png are pictures of the running program, and
make docs-userguide only copies them — it does not produce them, and nothing
compares them against the UI they claim to show. docs-check proves the
rendered HTML matches the markdown; it cannot see inside a PNG. So a screenshot
can contradict the prose directly beneath it and every gate stays green. That
has happened: a panel gained a column and a master switch, the guide described both, and the
picture above the description showed neither; a panel's one-line header was replaced by
labelled counters and a diagram while its picture kept the old header.
Every picture of a Qt widget in the guide is now regenerated by a make target:
$ make docs-screenshots # then: make docs-userguide, and commit both copies
It builds tools/docshot/docshot.cpp — a small program against
jnext_debugger and jnext_gui — boots a real Next from the SD image, brings
the real DebuggerWindow up through the production path
(DebuggerManager::set_enabled(true), then the same on_pause() /
refresh_panels() the GUI calls), renders that window under
QT_QPA_PLATFORM=offscreen, and crops each panel out of the render using
the panel's own geometry. Nothing is reparented, resized or drawn by hand, so
no image can show a layout the window does not actually have, and every panel
crop lines up with debugger-window.png by construction. Read that file's
header before changing it: it explains where the machine is stopped and why,
and the sprite/Copper/AY fixture the last three images need (NextZXOS at its
welcome screen drives none of those three, so those panels would otherwise be
pictures of an empty table).
It covers the twelve debugger-*.png and
gui-main-window.png and preferences-startup.png — the real MainWindow
with the booted framebuffer pushed into its viewport, and the real
PreferencesDialog on its Startup tab. Those two were excluded at first for
reasons that turned out to be wrong: MainWindow needs no audio device (QtApp
owns that path, and main_window_accel_test already builds the real window
offscreen), and the Preferences capture is handed a default-constructed
AppConfigData under the same redirected JNEXT_CONFIG_DIR that isolates the
debugger geometry, so it cannot publish anyone's jnext.conf. Meanwhile both
had gone stale the same way the debugger images had — gui-main-window.png
predated the Soft Reset toolbar button by four days and showed three icons
where the product builds four.
It also covers the five debugger-source*.png of the user guide's
Source Debugger section. They are Group 4, last: it loads a committed z88dk
fixture (test/fixtures/z88dk, from the working directory or --fixtures DIR)
through the production load and sidecar path (emulator_apply_load, then
Debugger::load_program_sidecars), sets a file:line breakpoint through
BreakpointModel::add_source_line, continues until the machine stops, and fails
loudly if the Source tab does not read the expected position. The other
images come out byte-identical with the group present or absent.
It has nothing to do with the images that are emulator output —
boot-splash, machine-*, layers-* and the rest — which jnext already
renders itself with --delayed-screenshot.
Every toolbar button in gui-main-window.png draws its icon. The icons are
the monochrome SVG set in src/gui/icons/, compiled into the binary and
rendered by jnext itself (src/toolbar_icons/, see 1.2),
so the capture depends on no desktop icon theme. Until that set existed, the
Screenshot button took its icon from QIcon::fromTheme("camera-photo"), which
the offscreen platform cannot resolve, and the picture showed the word
"Screenshot".
It is not a prerequisite of anything, and there is no staleness gate on these
images. Both are deliberate. The target boots a machine and needs a Qt build
and an offscreen platform, which is not something an ordinary build or test run
should do. And a byte-comparison gate would fail on every Qt, fontconfig or
freetype update, and on an SD image or NextZXOS update, none of which is
staleness — the same version-gap problem that makes docs-man-check and
docs-userguide-check need a fingerprinted reference renderer (and fail when it
differs), except that here there is no single renderer to fingerprint. A gate that cries wolf on
a routine package upgrade would be turned off within a week, and then the
images would be ungated and believed to be gated.
What the target does give you is a structural check, every time it runs: it fails loudly if a panel it is told to capture no longer exists, if a named tab is gone, or if the panel area scrolls at the capture size (which would slice every crop at the wrong offset). So a change that moves the UI out from under the guide is reported the moment anyone regenerates.
So the rule stays a rule, not an automation: if you change a panel the guide
shows, run make docs-screenshots and make docs-userguide in the same
change, look at the images, and commit both copies. Three consecutive runs on
one machine produce byte-identical files, so a diff in git status after a
regeneration means something really changed.
The command line is data, not control flow¶
src/core/cli_options.h holds every accepted flag as a constexpr table of
{ name, arity, doc-status, id }, and src/main.cpp dispatches from it with a
switch over cli::OptId compiled with -Werror=switch. Adding a flag to the
table and forgetting to write its parser arm is therefore a compile error, not
a flag that exists and silently does nothing. Because the flag set is now a
value the program can enumerate, cli_options_test can diff it against the man
page's OPTIONS section in both directions at once — undocumented flags and
documented-but-absent flags are both failures.
The shape of that is a direct response to what came before it. The flag set
used to exist only as a 47-arm if chain, which nothing could enumerate;
because nothing could enumerate it, nothing could compare it to the
documentation, and the documentation drifted twice while every gate stayed
green. Deliberate exceptions live in the table — there is exactly one, the
undocumented --sd-card back-compat alias — and never in a checker's exclusion
list, because an exclusion list is invisible from the code it excuses.
The same goes for the settings. Every Preferences setting is a row of
config_keys::KEYS and is therefore settable with --set section.key=value;
a persisted field with no row fails AC-94, and a row with no arm in
main.cpp's switch does not compile. See 4.5.
Test suites are declared, and a missing test is loud¶
test/unit-tests.conf names every unit suite together with the exact number
of rows it must report. test/run-unit-tests.sh refuses to run at all if that
manifest and the suites CMake registered disagree in either direction, and fails
the run if a suite reports any other count — including a lower one, which is
the case that matters. The regression suite carries the same contract in
test/00regression/regression_tests.conf and functional_tests.conf, with one
extra witness on top: every checked-in reference PNG must have an entry in the
conf, so deleting lines from the conf cannot quietly shrink the suite.
The practical effect is that adding or removing a test row means editing a number in a manifest, by hand. That is not friction to be engineered away — it is the mechanism. The number is the project's claim about how much it tests, so it should only ever change because someone decided to change it. Before this existed, three suites vanished from the counts in a single day and each was found by accident; a green result is only as trustworthy as its denominator. 4.2 Declared suites and pinned counts has the detail.
The core does not know about the frontend¶
Emulator can be constructed and run with no window, no audio device and no
host input at all. That is what makes headless testing and the unit suites
possible in the first place, and it is maintained by a simple rule: nothing
under src/core, src/cpu, src/memory, src/video, src/audio, src/port
or src/peripheral includes SDL or Qt. When the core genuinely does need to
tell a host something — a joystick source changed, a rewind restored the input
state — it goes through a std::function whose signature mentions no SDL or Qt
type.
This is the one rule in the list that no gate will catch. Adding such an include compiles perfectly well today; it just makes the core unbuildable in some configuration, on somebody else's machine, long after you wrote it.
Build configurations and the ENABLE_QT_UI / ENABLE_DEBUGGER arms are
covered in 5.2 Build configurations;
branch, worktree and review discipline is in
6.2 Branches, worktrees and review.