Skip to content

RustyNES v2.6.12 — "Groundwork" (the bitstream was an NROM-only console)

Latest

Choose a tag to compare

@github-actions github-actions released this 02 Sep 13:37
4e77905

RustyNES v2.6.12 "Groundwork"

The bitstream was an NROM-only console.

Rung 7 landed five mapper families and 142 co-simulation gates verify them. The layer that turns that RTL into a bitstream was never told.

The emulation core is unchanged. No chip crate changes, so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction.

The gate

# criterion result
1 the three ports are connected Declared by entity but not connected under nes_top: 3 → 0
2 the cartridge implements at its declared size prg 8 KiB → 256 KiB, chr 8 KiB → 128 KiB
3 it still fits and still closes 0 errors; timing positive at all four corners
4 the port gate fails on the real defect pre-fix emu.svCAUGHT, naming all three pins
5 the warning gate fails three ways new warning, vanished warning, empty report → all CAUGHT
6 the tie-offs change nothing bitstream byte-identical (f0ddb3fa…)

Three inputs, tied to ground

rtl/emu.sv never connected cart_mapper, cart_prg_16k_banks or cart_chr_8k_banks. Quartus said so, three times:

Port Connectivity Checks: "emu:emu|nes_top:u_nes"
  cart_mapper  Input  Warning  Declared by entity but not connected by
                              instance. ... the port will be connected to GND.

In the shipped v2.6.11 bitstream that meant mapper 0 for every cartridge, prg_8k_count = 0 pinning prg_bank_sel to zero and collapsing PRG to an 8 KiB window, and CHR forced to RAM with 8 slots.

memory declared implemented before after
prg 256 KiB 8 KiB 256 KiB
chr 128 KiB 8 KiB 128 KiB
prg_ram 8 KiB 8 KiB 8 KiB
work RAM 2 KiB 2 KiB 2 KiB

The two arrays whose size depends on a tied-off input are the two that collapsed; the two that do not were correct. Block memory 666,061 → 3,680,717 bits, RAM blocks 100 → 468 of 553, and timing still closes at every corner — worst setup +0.225 ns, worst hold +0.105 ns, a better hold margin than v2.6.11's.

emu.sv said it in its own OSD string, honestly, when written at v2.6.6: "Unsupported mapper - this core is NROM only". v2.6.9 added five boards to cart.sv; nothing updated the layer above.

Two of the six titles in v2.6.11's montage are larger than the fitted cartridgeBad Dudes at 128 KiB CHR, Battletoads & Double Dragon at 256 KiB PRG. That release's claim is qualified, not withdrawn: true of the RTL under Verilator, which builds the full array, and not of the bitstream.

Why nothing caught it

  • Simulation cannot. The harness drives those ports itself, and emu.sv is not even in the testbench's file list. All 142 gates exercised a correct cartridge.
  • Quartus did, three times.
  • quartus_clean.py did not, and was right about what it checks. It fails on messages citing a path under rtl/ or tb/; these cite an instance path and no file — 0 of 125 carried one — and they are not in the "0 errors, N warnings" tally either.
  • Verilator's PINMISSING names it exactly — and emu.sv was the one file under rtl/ no lint target reached.

Two gates, both mutated

tb/check_pins.py fails on an unconnected pin of any module this repository declares, scoped by the module we own rather than a pin-name list — sys/ modules legitimately leave dozens open, and 82 such warnings are correctly ignored.

tb/check_warnings.py pins the warning set, not the count, because a warning that is neither attributable nor counted is invisible to every check that reads counts or paths. It fails on a new warning and on a pinned one that stops firing. Every entry carries why it is acceptable; all three originate in sys/, which may not be edited.

Both are wired into make lint and scripts/release-rbf.sh.

Which tie-offs are features

Tying off hps_io's 25 unconnected inputs raised the right question, so it is answered in writing. Each tie-off now says whether it is not applicable or not implemented — the second reading, left implied, is the same defect as the OSD string.

Not implemented, and one costs users data:

  • T-MISTER-SAVE — battery-backed save RAM. cart.sv already has 8 KiB of PRG-RAM and nothing persists it, so every MMC1/MMC3 battery game — Zelda, Final Fantasy, Kirby's Adventure, Crystalis — loses its saves at power-off.
  • T-MISTER-4PLAYER Four Score · T-MISTER-PADDLE the Arkanoid Vaus controller, for a game in our own montage · T-MISTER-ZAPPER the light gun, which the oracle already implements · T-MISTER-SDRAM-SZ, needed the moment the SDRAM controller lands · T-MISTER-DIRECTVIDEO for the analog board · keyboard, menumask and runtime PAL switching.

Not applicable: rumble, mouse, RTC, UART, rotated video.

Annotating the nine was the finding

None land here — and after each was given a blocker and an unblock condition, that stopped being a scoping choice and became a measurement. Not one of the nine is blocked on effort. Four are deferred to v2.8+ by the approved plan, three cannot be verified by anything in this repository, and two wait on subsystems that do not exist. The list is the rung-6 agenda, not a backlog: "close these tickets" resolves to attach a board, not write more RTL.

T-MISTER-SAVE was attempted, and the attempt is what established this. Its ticket said the implementation was unblocked and only the verification was rung 6. That split does not survive contact with sys/hps_io.sv:

  • there is no OSD-close signal to flush on — this sys/ exposes no OSD_STATUS, and the nearest available signal, buttons, is already the reset button in emu.sv's own reset expression, so the obvious trigger is a plausible-looking wrong one;
  • the NVRAM route is not self-containedhps_io.sv:152 says it in its own port comment: ioctl_upload_req // must be supported on HPS side for specific core;
  • neither route is reachable by any gate heresd_* and ioctl_* both terminate in hps_io, which the testbench does not instantiate.

So a save path written now would ship with zero evidence, which is this project's oldest defect in its purest form. The attempt is preserved as a patch rather than committed, and cart.sv deliberately carries no save port: dead infrastructure for a feature that cannot land is worse than none, and check_pins.py would have had to be lied to in order to accept it.

One correction rides along: the nine tickets were first filed under a heading reading "cases where the DUT is measurably more accurate than RustyNES itself". None of them is — that heading belongs to T-ORACLE-001 alone. Corrected rather than quietly moved, because a ticket is read by its heading.

The new gate had the defect it was written to close

Review found it before it shipped, and it reproduced on the first try: both an empty capture and a Verilator run that died early printed

check_pins: 0 PINMISSING warning(s) examined; 16 module(s) declared under rtl/
PASS: no module under rtl/ has an unconnected pin.

An absent result reading as a clean one — this repository's oldest defect, sitting inside the gate written to close the previous instance of it. The script already failed closed on an empty module set; it did not fail closed on empty input.

The || true in the recipe has to stay, and it is why the hole existed: two of sys/'s modules are Quartus megafunctions (Hq2x, altera_pll) with no source Verilator can see, so a clean lint of the top level always exits non-zero on MODMISSING. The exit status carries no information — which is exactly what makes a died-early run indistinguishable from a clean one. So the discrimination moved into the script, as three controls:

input result
empty file exit 2, "lint output is empty"
unexpected %Error exit 2, "the lint did not complete"
allowed errors, 0 pins exit 2, "no PINMISSING warnings at all"
summary count mismatch exit 2, "counted 3 error(s); 1 were expected"
the real lint PASS, 57 PINMISSING warnings examined

Four mutations, four different controls. The third is the one worth keeping: sys/ produces 57 PINMISSING warnings on every healthy run, so zero is not "a clean design", it is "the lint never reached the design" — a non-zero control, which this project keeps having to relearn.

Control 2 needed a second pass, found by running the real lint against the first version: %Error: Exiting due to 2 error(s) is itself a %Error line and names no module, so an allowlist over every %Error rejected a healthy run. A coded error is a cause; the bare summary is a summary of them.

And the control's first CI run found something worse than the report. It failed — and the successful run immediately before it, with the control absent, says why:

check_pins: 0 PINMISSING warning(s) examined; 16 module(s) declared under rtl/
PASS: no module under rtl/ has an unconnected pin.

The gate had been completely inert on CI since the moment it was added, in this very release, while reporting PASS on every run. Locally the identical command examines 57. So it was not merely capable of a false pass — it was delivering one, every run, for its whole life.

Both causes are version skew between CI's Verilator 5.020 and the 5.050 here, and neither direction is a superset of the other:

line 5.050 5.020 effect
error %Error-MODMISSING: bare %Error: allowlist saw 0 against a summary of 2 — a healthy run refused
warning Instance has missing pin Cell has missing pin PIN_RE matched nothing — zero examined, PASS reported

Both wordings are accepted now, and CI reports 57 examined where it reported 0. This is the second time this project has assumed the newer Verilator is the louder one; the first was a width cast 5.020 flags and 5.050 accepts. The message text is not a contract — the warning code is.

And the gate now carries a self-test whose verdict the release script refuses to trust without. scripts/check_timing.py already worked that way; check_pins.py earned it. Six fixtures live in the file — both Verilator message formats and all four failure modes — and the judging logic is extracted so the self-test drives the real implementation rather than a copy of it, because a test that reimplements its subject agrees with itself forever. Reintroducing either real defect fails it, each on the 5.020 fixture specifically.

The non-zero control now diagnoses itself: when it fires it prints every distinct Verilator message code in the capture and the capture's line count, so a wording change names itself on the run that breaks it rather than costing another round-trip through CI.

One further review finding is recorded as what it is rather than upgraded: release-rbf.sh fed check_warnings.py only the map report. Both are passed now — but today the two invocations give the identical answer, because all three warnings appear in the map report and the Fitter merely re-emits one. That closes a latent gap, and was not a live defect. The numbers were checked before the change was written.

The bump script deletes a release, and its own gate caught it

bump_release.py classifies each release anchor by the shape of the text around it, then demotes the outgoing release into that shape. One shape is PERIOD — a period right after the codename — and its rule reads "the statement ends at the codename: nothing describes the release, so there is nothing to demote."

That is true of one of this repository's two PERIOD anchors and false of the other:

OVERVIEW.md   ... **v2.6.11 "Exposure"**. The never-tagged v2.4.0 "Concordance" ...
SECURITY.md   ... **v2.6.11 "Exposure"**. Built on **v2.6.10 "Inference"** and ...

In OVERVIEW.md the sentence genuinely ends. In SECURITY.md the period is a separator before a lineage chain, so "swap and stop" replaced the head and left v2.6.11 nowhere at all — not stale, absent. That is the defect v2.6.11 itself found and fixed in the documents that were not this one, recurring one release later in the one that was missed.

Split into PERIOD and PERIOD_CHAIN, with four selftests — the script's own comment records that PERIOD and DATED_CODE "were added without selftests and review caught it", so a fifth shape without one would be the same defect a third time. Collapsing the classifier back fails three of them, including the one that names the defect outright: period_chain erased the outgoing release.

Two more anchors needed hand correction, and the v2.6.11 chain gates named both — a chain in ROADMAP.md whose first link had become v2.6.10, and one in to-dos/ROADMAP.md ending "v2.6.11, the current release". The gates print the file and the remedy, which is the difference between a release that ships a broken lineage and one that does not: a_release_line_chain_does_not_skip_a_release is one release old and has now caught a real skip in each of its two releases.

Not closed

Rung 6. No board is attached, confirmed by checking. The SDRAM controller is still unwritten — connecting the ports makes the declared 392 KiB real, which fits on-chip; it does not make a 512 KiB MMC3 game fit.