The current milestone is a bootable, playable, performance-characterized Neo Geo cartridge. It runs the upstream translated C game core on the MC68000, uses Neo Geo graphics hardware directly, reads active-low Neo Geo controller registers directly, and packages a cartridge from a user-owned SMB dump.
Completed:
- Pure C target; no C++ runtime or desktop framework in the Neo Geo link.
- MC68000 code generation (
-m68000 -mlra) with-O3, LTO, and measured inlining of the translated core's hot instruction helpers. - A guarded semantic direct-C enemy graphics path for normal Goomba, piranha plant, Lakitu, and spiny states. It emits the three OAM rows directly and falls back to the generated instruction-equivalent handler for every other identity, state, or global mode.
- A guarded direct-C movement path for normal Goombas and the normal and egg states of Spinies. It preserves the original fixed-point movement, emulated stack effects, registers, and flags, with the complete generated state machine retained as the fallback.
- A folded enemy-relative-position path guarded by the exact caller and helper shapes. It performs the source-ordered zero-page reads, coordinate writes, subtraction, and register restoration directly while retaining the generic helper chain for every other object class.
- Direct background, OAM sprite, palette, FIX HUD, and input backends.
- Two 33-strip background banks with generation-tracked columns, sparse changed-row uploads, and one or two sticky chain drivers per hidden bank.
- Double-buffered background and OAM state, bounded FIX/palette phases, and an independent next-VBlank SCB3 reveal.
- Local, asset-clean CHR-to-C-ROM/S-ROM converter with unit tests.
- Full P/M/V/S/C ROM packaging and ngdevkit-gngeo boot.
- Byte-for-byte reproducible cartridge checker using two isolated builds.
- Deterministic recording validation, compact replay emission, and a strict local FM2 input-log importer.
- A separate FM2-driven cartridge, sequential 32-stage/final-victory tracker, raw debugger mailbox, and bounded isolated runner for progression evidence.
- A passing hardware-direction-safe full-game replay through all 32 stages and a 60-frame stable final-victory condition.
- Measured FM2 source-frame bootstrap/area-load timing, versioned replay accounting, and exact FCEUX-versus-C state transcript tooling.
- Restored carry semantics for every translated assembly jump dispatcher, with generated-code and gameplay-state regression tests.
- Neutralized impossible opposite keyboard directions before they can corrupt the original game's single-stick direction state.
- Exact native-reference and port measurements for ordinary and enemy-heavy scenes, including dedicated crowd regression windows.
- Integer-only NES APU-to-YM2610 SSG bridge with changed-register coalescing, acknowledged MC68000/Z80 transport, and a custom M1 sound driver.
- Per-channel software pulse sweep with exact target overflow muting, divider/reload ordering, and pulse-1/pulse-2 negate asymmetry.
- Host regressions for pitch, sweep, envelope, mixer, noise, write ordering, and transport retry plus an enforced Z80 ROM/RAM/stack linker-map budget.
- Automated architecture, translated-core reachability, forbidden-symbol, and work-RAM guards.
Not completed:
- Full-fidelity audio synthesis. The current native SSG bridge intentionally approximates source waveforms and omits several APU behaviors.
- Physical retest of the corrected background/FIX handoff on MV1C hardware, plus broader AES and flash-cartridge validation.
- Cycle/scanline-accurate PPU behavior. This port intentionally follows the upstream frame-at-a-time timing model.
- Exact NES left-edge masking in every fine-scroll case.
- One game tick per stock-clock VBlank throughout the enemy-heavy regression windows.
- Save state or memory-card support.
The desktop PPU turns every NES frame into a 256x240 RGB bitmap. That is a poor fit for the Neo Geo's roughly 64 KiB user work-RAM window and would also spend the 12 MHz 68000 on work already handled by the sprite hardware.
The port converts each NES 8x8 2bpp CHR tile in two ways at build time:
- C-ROM: pixels are expanded 2x to a 16x16 4bpp Neo Geo tile. Single-tile
OAM uses SCB2
$077e; background chains use$077f. Both select the same eight in-tile rows, while$077eavoids a ninth-row GnGeo DDA artifact on one-tile objects. - S-ROM: tile zero remains transparent for BIOS FIX-map clears. The original 8x8 pixels are encoded directly for FIX tiles 1 through 512.
The 2x expansion followed by the in-tile SCB2 shrink map is a lossless
sampling pair: the hardware selects one pixel from each duplicated 2x2 block.
A ROM-free
regression locks that transform and the zoom word. Color indices are also
preserved. tools/gen_neogeo_palette.py converts the named FCEUX 2.2.1
reference profile into the nearest representable Neo Geo color words, with at
most four 8-bit levels of per-channel error. See
visual fidelity for the exact geometry and color gates.
The game also reads 314 bytes at CHR $1ec0-$1ff9 to construct its title
nametable. Cartridge builds generate a small read-only C array for that
window from the user-supplied ROM. The normal ROM-less verification ELF links
a zero stub instead, and the Makefile uses distinct ELF outputs so one mode
cannot accidentally reuse the other's link product.
At runtime:
- 33 vertical SCB strips cover the scrolling 256-pixel background, including
the incoming fine-scroll column. Each strip uses SCB3 height 33 to select
the LSPC's 32-tile full-height mode; HUD-visible playfields are padded by
three transparent physical rows so whole-chain
$7fshrink preserves the intended 8-pixel row geometry. - Each hidden circular strip bank keeps its own world-column/generation cache. Fine scrolling only changes one or two chain-driver SCB4 words, and crossing an eight-pixel boundary normally rebuilds one entering strip in that bank instead of all 33.
- The stationary three-row SMB status bar uses FIX tiles.
- NES OAM entries use one-tile Neo Geo sprites.
- OAM priority-behind-background sprites are below the background strips; normal sprites are above them.
- Lower NES OAM indices receive higher Neo Geo sprite numbers so their priority remains in front.
- OAM is evaluated from entry zero upward and every in-range entry is sent to the Neo Geo in the same priority order. The port intentionally does not emulate the source hardware's eight-sprites-per-scanline dropout.
- Opaque FIX tiles mask the 32-pixel side borders, producing a centered 256x224 viewport with the usual eight-pixel top/bottom overscan crop.
The Neo Geo global backdrop supplies NES background color zero. Because C-ROM pen zero is transparent, a behind-background sprite naturally appears through transparent NES background pixels and disappears under opaque ones.
Palette RAM word zero is not used for that visible color: it is the LSPC's
analog black reference and is fixed to $8000 in both physical palette banks.
The independently addressed backdrop register retains the requested NES
universal background color.
The physical hardware layout is:
| Purpose | Hardware slots |
|---|---|
| Behind-background OAM bank A | 64 |
| Behind-background OAM bank B | 64 |
| Scrolling background bank A | 33 |
| Scrolling background bank B | 33 |
| Front OAM bank A | 64 |
| Front OAM bank B | 64 |
| Total | 322 of 381 |
The complete background and OAM scene is double-buffered. Each background bank forms one circular sticky chain, split into two chains only when its ring wraps around physical slot zero. SCB1 tiles, sticky follower words, and SCB4 positions are built only in the hidden bank. Its prospective one or two driver words are explicitly cleared first, preventing stale sticky state from chaining across a bank boundary.
The next OAM bank's SCB1 and SCB4 state is prepared while its SCB3 entries are hidden. The renderer stages its live SCB3 words in work RAM, then hides only the old bank's active entries and reveals only the new bank's in-range/live entries. SCB2 zoom values are initialized once for all 322 slots. Hidden-bank SCB1/SCB4 data is written directly instead of compared against a work-RAM OAM cache. Each live OAM SCB3 word uses an independent address/data pair so every 8x8 component remains independent on the physical LSPC bus.
The renderer waits for a display interrupt observed after hidden-set construction finishes. If construction crosses an earlier VBlank, it waits for the following one rather than writing live state during active display. The wait uses an atomic 16-bit signal while separate 32-bit counters retain long-running cadence evidence.
Background work may span display periods because it cannot affect the visible bank. Palette changes and at most 32 sparse FIX entries are committed in one fresh VBlank under a conservative 20,000-cycle accounting ceiling. Additional FIX entries continue in later chunks. Small measured palette/FIX deltas may share the old/new background-and-OAM SCB3 swap only when their combined worst-case accounting leaves at least 1,024 modeled cycles below the same ceiling; larger live changes complete first. The render generation advances only after the atomic reveal.
Every LSPC address, data, and modifier write is emitted as an absolute-long
move.w to $3c0000, $3c0002, or $3c0004. Each instruction takes 16
cycles, satisfying both the LSPC's minimum data-write interval and its
data-to-new-address interval without relying on optimizer-sensitive
address-register loops. Final-ELF verification rejects known-pointer indirect
stores, direct non-word/odd-address stores, and a missing absolute-long
register class. The current linked image has 30 address, 36 data, and 12
modifier writes, all in the required form.
The following table belongs to the older, pre-audio, double-background
renderer milestone ELF (SHA-256
8a3894ea0cf378d33eee451893fdce76c53e051f71252d6b3926a56fe2fda7d0)
and is retained as historical evidence only. It was audited instruction by
instruction with these worst-case MC68000 cycle bounds, including a successful
VBlank-poll iteration:
| Live-update path | Worst-case cycles |
|---|---|
| Clean palette, five changed HUD cells, and maximum SCB swap | 24,952 |
| All 51 palette words changed, no pending HUD scan, and maximum SCB swap | 24,842 |
| Split phase 1: all palette words plus all 96 HUD cells | 22,782 |
| Split phase 2: maximum SCB swap | 18,344 |
Those historical paths stayed below the 25,000-cycle post-handler working ceiling. A raw NTSC VBlank is approximately 30,720 68000 cycles, so the ceiling reserves more than 5,700 cycles for interrupt/BIOS work, recognition latency, and hardware wait-state uncertainty. The current implementation instead uses the 20,000-cycle phase policy described above, reserving a larger margin for BIOS work and hardware wait-state uncertainty.
An exact 67,677-record NTSC reference run was measured with FCEUX 2.2.1 and
cross-checked against FCEUX 2.6.5. On all 57,812 live gameplay frames
(OperMode == 1 and task 3), the NMI count advanced once, the game's
FrameCounter advanced once, and FCEUX reported no lagged frame. The full run
contained 53 lag markers, all during boot or transitions; 46 were in
game-mode area-loading task 1. Mean CPU time was 29,780.500007 cycles per
video frame, approximately 60.0988 Hz.
Therefore native crowd pressure does not throttle gameplay logic in the measured run. It causes first-eight-per-scanline sprite dropout and flicker. The port instead sends every in-range OAM entry to the substantially larger Neo Geo sprite engine: the target is one translated game tick per hardware VBlank without intentionally reproducing that flicker. The nominal Neo Geo and source NTSC refresh rates are slightly different, so this target prioritizes stable hardware pacing over fractional double-tick jitter.
Enemy/object-buffer occupancy and source PPU rejection were:
| Active enemy slots | Live frames | Frames with rejected OAM entries |
|---|---|---|
| 0 | 5,683 | 0 |
| 1 | 6,713 | 1 |
| 2 | 19,713 | 3 |
| 3 | 13,179 | 131 |
| 4 | 7,902 | 487 |
| 5 | 4,530 | 386 |
| 6 | 92 | 0 |
Six active object slots do not imply overflow: the source hardware limit is eight sprites intersecting one scanline, not six objects anywhere on screen.
The zero-based FM2 crowd regression windows are:
| Source frames | Stage | Reference behavior |
|---|---|---|
3707..3712 |
1-2 | First early overflow onset |
3739..3751 |
1-2 | Five active slots and continuous rotating dropout |
19780..19809 |
3-2 | 30 consecutive overflow frames |
30384..30488 |
4-3 | 105 consecutive overflow frames |
32587..32620 |
4-4 | 34 consecutive overflow frames |
At frames 3739 through 3742, the rejected whole OAM-entry indices rotate as
[56,57], [36,37], [58,59], and [56,57]. Every one of those frames
still advances exactly one native game tick.
The committed persistent-background baseline was measured at the stock emulated MC68000 clock with host pacing, sound, VSync, and overclock disabled. Each interval followed a 240-frame complete renderer/audio warmup, and three independent launches reproduced the early values exactly:
| Inclusive source window | Native ticks / frames | Port ticks / VBlanks |
|---|---|---|
3435..3554 |
120 / 120 | 120 / 226 |
11806..11925 |
120 / 120 | 120 / 225 |
3707..3751 |
45 / 45 | 45 / 90 |
3739..3751 |
13 / 13 | 13 / 26 |
Those ratios are the explicit optimization gate for the old software-culling baseline. A light-load 120/120 cadence smoke does not override them. The renderer screenshot at source frame 3742 was identical across all three baseline runs.
The cacheless all-sprite renderer was then measured over the exact
3435..3554 World 1-2 pipe window:
| Renderer configuration | Game ticks / VBlanks | Missed VBlanks |
|---|---|---|
| Persistent-background baseline with software culling | 120 / 226 | 106 |
| All in-range sprites with the old OAM cache | 120 / 203 | 83 |
| Cacheless all-sprite renderer with batched SCB3 runs | 120 / 177 | 57 |
| Previous persistent renderer plus flattened collision scan | 120 / 174 | 54 |
The endpoint screenshot was byte-identical in all four runs, and the translated game/RAM endpoint matched. That previous integrated result removed 52 of the baseline's 106 missed display periods in this window. With the established 53-hold source schedule, the CSV header and all 67,677 non-comment state rows also matched byte-for-byte between baseline and all-sprite builds; the complete files differ only in scheduling metadata. The new policy deliberately restores sprites that the source PPU would reject on overflow frames while leaving the translated gameplay state unchanged.
The current double-background hardware-safety renderer was also exercised in MAME 0.264 with the same deterministic active World 1-1 input schedule. The frame-720-through-1080 window produced 360 game ticks in 360 VBlanks with no counter jump or stalled palette/FIX phase, and an independent fresh-state run repeated that result exactly. An intermediate safety build that always split two-word palette animation updates produced only 332 ticks; counting actual palette writes and sharing that bounded update recovered all 28 ticks without raising the 20,000-cycle policy ceiling. This ordinary-scene result does not replace the enemy-heavy replay gates or physical hardware validation.
A field-major SCB1/SCB4 prototype reduced the modeled OAM register-store count from 25,895 to 20,762 over this window but regressed real stock-clock cadence to 120 / 197. It was rejected and is not present in the renderer. No assembly replacement has been accepted without a measured stock-clock win.
Native hardware behavior is documented by the NES PPU frame timing, sprite evaluation, and OAM references; the target display rate is documented by the Neo Geo development wiki.
The Neo Geo link replaces the desktop PCM mixer with a compact native bridge. The translated game still performs its normal APU register writes; the bridge shadows the relevant channel state and derives YM2610 SSG and ADPCM-B registers on ordinary game/audio frames plus bounded native-hardware catch-up periods:
| Source voice | Native target |
|---|---|
| Pulse 1 | SSG tone A |
| Pulse 2 | SSG tone B |
| Triangle | Variable-rate looping ADPCM-B |
| Noise | SSG noise C |
Only changed target registers are sent. Pulse periods use 8.8 fixed-point multiplication and shifts, while a 16-entry integer table maps noise periods. Triangle timer writes calculate ADPCM-B Delta-N from a 64-sample period; division therefore occurs only when a note timer byte changes, never in the 60 Hz emission loop. The bridge adds no runtime PCM buffers or floating point.
Pulse amplitude also uses a 16-entry integer curve. The source pulse mixer compresses its 0..15 range, while each fixed SSG step is approximately 3 dB; direct passthrough therefore exaggerated the gap between music around 4..8 and jump/fire effects at 14..15. The curve retains target level 8 for source level 8, maps 14/15 to 10, and leaves noise independent. This keeps capture headroom while reducing pulse-effect peaks by up to five SSG steps.
Writes to the two source pulse-sweep registers are retained as compact MC68000 state. Each 60 Hz bridge step clocks both sweep units twice, applies the source divider/reload order, continuously mutes invalid targets, and distinguishes pulse 1's one's-complement negate from pulse 2's two's- complement negate. Hardware length counters also receive two half-frame clocks; envelopes and the triangle linear counter receive four quarter-frame clocks. Source halt/control bits, disabled length loads, immediate disable clears, linear reload, and triangle timers zero through two are modeled. The resulting state still passes through the changed-register coalescer and generic Z80 command path.
The renderer may occasionally consume more than one display period.
audio_cadence.c detects that with an atomic 16-bit VBlank snapshot and clocks
only the native bridge units for the missed periods before the next ordinary
game frame. It leaves the newest period for that normal frame, handles counter
wrap, and caps catch-up at four steps; older debt increments an exported
diagnostic instead of forming a transport/recovery spiral. It intentionally
does not call the translated SoundEngine() out of band: that routine consumes
queues and changes EventMusicBuffer, which end-of-level gameplay also reads.
Music-note and software-effect duration counters therefore remain tied to
completed game frames.
The YM2610 is driven by the Z80 rather than directly by the MC68000. Each
13-bit register/value payload uses two commands from the 122-symbol
$06-$7f alphabet. The payload is split into base-121 quotient/remainder
digits and each digit is rotated relative to the preceding symbol. Adjacent
commands therefore always differ, including across packet boundaries, so an
old echoed acknowledgement cannot satisfy the next wait. Bit 7 remains
exclusive to acknowledgements. The Z80's 64-entry FIFO preserves order,
decodes the pair before the deferred YM write, and a full 18-register initial
flush uses 36 commands.
Command 3 resets the sound driver without an acknowledgement. The MC68000
allows eight game frames for startup and then sends one of two alternating
ready pings. A transport timeout invalidates the changed-register cache and
restarts this sequence. Two reset retries are allowed; a third consecutive
failure disables audio transport instead of hanging gameplay. The Z80 commit
handler calls nullsound's ym2610_write_port_a, retaining its required YM2610
delays and interrupt-safe port restoration.
The triangle sample is generated from source code rather than stored as a
copyrighted asset. A 64-point, DC-centered, +/-10,000 PCM triangle is repeated
2,048 times and deterministically encoded into the first 64 KiB of V1. The
remaining 448 KiB are zero-reserved. Independent decoding measures
-10,078..10,081, 47.534 PCM units of RMS error, and a repeat-reset seam one
PCM unit from the ideal adjacent slope. Its fixed V1 SHA-256 is checked before
normal and replay packaging. The bridge sets start block $0000, inclusive
stop block $00ff, stereo pan, volume $70, and START|REPEAT. It changes
Delta-N without retriggering while notes remain audible.
This is a major fidelity improvement, not a claim of source-chip exactness:
- SSG tones have a fixed 50-percent duty cycle, so pulse duty is not retained.
- The source short-noise mode bit is retained, but the SSG cannot reproduce that alternate sequence and slow noise periods saturate at 31.
- Direct DAC/DMC mixer bias is not modeled.
- Sweep control is advanced with two half-frame clocks, but changed SSG tone periods are emitted at the next 60 Hz bridge boundary rather than at a source-chip sub-frame instant.
- Music-note and software-effect duration sequencing remains game-frame-bound; only native hardware units receive missed-display-period catch-up.
- Frame-sequencer units are aggregated at the 60 Hz bridge boundary rather than exact source-chip sub-frame instants.
- The startup handshake proves that the command FIFO accepts input after the fixed delay; it is not a processed-ready response from the driver main loop.
- Physical hardware still needs to confirm the cartridge board exposes V1 to the YM2610 ADPCM-B bus and that the emulator-tuned level transfers cleanly.
The custom sound driver is packaged as a 128 KiB M1 region and the generated
triangle as a 512 KiB V1 region. Normal and rendered-replay GnGeo commands
enable sound explicitly.
REPLAY_FAST=1 skips both hardware rendering and apu_step_frame(), so the
fast all-stage lane remains a core-progression test rather than audio
evidence.
GnGeo's remote-debug mode forcibly disables its sound/Z80 path, even when
--sound is requested. The debugger cadence and replay gates therefore
cannot observe audio transport. tools/probe_neogeo_audio.py launches a
separate normal-mode instance with fixed stock-clock and 60 Hz host-pacing
flags (--autoframeskip --sleepidle --no-vsync), an isolated
home/configuration and X display, explicit debugger disablement, active
gameplay input, and SDL's disk-audio driver. Signal detection is separate from
evidence: the hashed PCM interval starts only after gameplay activation is
accepted and contains exactly the requested number of audio frames. Finite
argument ceilings, per-command timeouts, an overall deadline, and a
child-process file-size limit bound the run while it is active. The probe
rejects empty or silent signed-16-bit stereo, records cartridge, GnGeo-data,
PCM-segment, and screenshot hashes in result.json, and deletes the raw PCM
after a successful check by default. A failed probe retains its bounded raw
capture with the logs for diagnosis.
The cartridge itself advances from the Neo Geo VBlank signal. On physical
hardware that signal is periodic, but the host emulator still has to throttle
its emulated frame loop. In the installed GnGeo revision, the wait for the
next 60 Hz deadline is inside frame_skip(), guarded by the
autoframeskip option. Consequently, --no-autoframeskip removes wall-clock
throttling entirely: gameplay speeds up when the host is idle and slows down
when it is busy even though both emulated CPU clock adjustments remain zero.
The interactive Make targets and normal-mode audio probe therefore use
--autoframeskip --sleepidle --no-vsync --68kclock=0 --z80clock=0.
Automatic frame skipping keeps game time at 60 Hz and may omit a host draw
only if the machine falls behind; --sleepidle avoids a busy wait, and
--no-vsync avoids adding a second display-dependent limiter. The debugger
cadence and replay tools use the opposite policy intentionally: their
unpaced, sound-disabled emulator validates guest work per emulated VBlank and
can finish deterministic evidence faster than real time. That evidence must
not be described as a wall-clock-speed measurement.
The current Z80 linker map reports:
| Measurement | Bytes |
|---|---|
| Fixed Z80 code | 10,965 |
| Z80 static data | 1,945 |
| Data-to-stack headroom | 100 |
tools/check_neogeo_sound_driver.py runs as part of verify. It rejects
missing or inconsistent CODE/DATA summaries, code that does not start at
$0000 or extends beyond the fixed $8000 window, data that does not start
at $f800 or reaches the $fffd stack start, and less than 64 bytes of stack
headroom. It also verifies the exact 128-entry packet command dispatch,
ready-ping state reset, decoder instructions, base-121 lookup table, and
exactly three bytes of driver-owned mutable DATA. Its parser and rejection
paths have Python unit coverage.
tools/test_neogeo_apu_bridge.c verifies initial mute, changed-register
coalescing, A4 and general period conversion, positive and negative sweep
targets, per-channel negate behavior, divider/reload cadence, sweep overflow
muting, envelope decay, hardware length/halt semantics, triangle linear reload
and low-timer muting, master disable/re-enable behavior, independent
ADPCM-B/noise registers, representative noise periods, coarse-before-fine
tone updates, the exact flagpole sweep start/first update, normalized
music/effect pulse levels, and dirty-register retry after a transport failure.
tools/test_neogeo_audio_cadence.c verifies no-op/single/multiple display
periods, 16-bit wrap, a catch-up step that crosses another VBlank, and the
bounded debt-drop path.
tools/test_gen_neogeo_triangle_vrom.py independently decodes the waveform
and fixes its dimensions, error bound, seam, padding, and SHA-256. These host
tests and emulator-oriented packaging checks do not replace listening tests
or electrical/timing validation on physical AES/MVS-compatible hardware.
make -C platform/neogeo verify currently reports:
| Measurement | Bytes |
|---|---|
| MC68000 text + read-only data | 182,394 |
Initialized work RAM (.data) |
36 |
Zeroed work RAM (.bss) |
14,176 |
| Static user work RAM total | 14,212 |
User-RAM limit below $10f300 |
62,208 |
| Remaining stack/heap headroom | 47,996 |
For comparison, the unmodified desktop link's measured BSS was 545,556 bytes. Most of that was its RGB framebuffer, opacity mask, decoded-tile cache, audio buffers, save-state storage, and runtime CHR copy. None of those symbols is present in the Neo Geo ELF.
The verifier fails if:
- the ELF is not MC68000;
- the actual translated
main, programdata,ram, ornametablehas been optimized out; - the ROM image is implausibly small;
- static user work RAM exceeds 48 KiB;
- ngdevkit startup BSS/data write envelopes leave cartridge work RAM or are not restored by the initialized guard block;
- the cartridge NGH is not the project-selected packed-BCD
$2026; - final linked code contains a known-pointer indirect or a direct non-word/odd-address LSPC register write;
- both physical palette banks are not initialized with
$8000at word zero; - a memory-card API or direct BIOS/control/raw card write is linked;
- framebuffer, CHR-copy, desktop, or software-float symbols are linked.
The game implements no save or memory-card path. The final-ELF audit rejects
ngdevkit memory-card APIs, the BIOS card vectors and parameter block, card
unlock/bank controls, and direct writes anywhere in the mirrored
$800000..$bfffff card window. The standard zero-size backup-data header
symbols are metadata, not a card operation.
The 48 KiB guard is intentionally below the linker limit, preserving more than 13 KiB for stack and future runtime state even if the port grows.
tools/gen_neogeo_assets.py accepts a raw iNES ROM or a ZIP containing
exactly one .nes member. It:
- verifies SHA-1
ea343f4e445a9050d4b4fbac2c77d0693b1d0922; - validates mapper 0, vertical mirroring, 2x16 KiB PRG, and 1x8 KiB CHR;
- reads the CHR bank only;
- generates 512 C-ROM and 512 S-ROM NES tiles plus transparent/solid helper tiles;
- emits the 314-byte CHR-resident title nametable payload as an ignored C translation unit for the cartridge-only ELF;
- pads C1/C2/S1 to cartridge sizes; and
- records a manifest confirming that zero source PRG bytes were written.
The Makefile then builds the one-megabyte native P1, custom Z80 sound-driver
M1, generated triangle V1, and full C/S regions. make cart packages those
full regions as the authoritative smbneo.zip and the byte-equivalent
single-file smbneo.neo; make hardware-cart is an explicit alias for the
six-ROM physical-cartridge layout. The NeoSD image has a 4 KiB header followed
by native P/S/M/V data and byte-interleaved C1/C2 data. It is created with
ngdevkit's packer and checked by an independent project validator.
The canonical build also generates gngeo_data.zip with one custom
rom/smbneo.drv entry. A validator checks the title, full region sizes,
filenames, destinations, and CRCs against the native ROM files. The ordinary
make run path therefore launches the project as smbneo, not through a
donor database entry. The cartridge header also uses the project-specific
packed-BCD NGH value 0x2026 instead of ngdevkit's generic
default. Both the command-line and browser packers reject non-BCD identifiers.
make mame-cart pairs smbneo.zip with a generated
build/mame/hash/neogeo.xml containing the unique smbneo software entry,
the visible title Super Mario Bros. Neo, full region sizes, generated
hashes, and explicit MAME loading semantics. MAME therefore never needs to
describe the canonical build as another game.
make compat-cart separately generates the optional puzzledp.zip profile
for fixed-database frontends. It retains the first 512 KiB of P1 and first
1 MiB of each C ROM only after proving that every omitted byte is native
FF/zero padding. The six expected driver CRCs are reached by changing only
the last four bytes of verified padding tails; P1 remains at offset zero for
load16_word_swap. Exact loader paths and the separate neogeo.zip
requirement are documented in
EMULATOR_COMPATIBILITY.md.
A shared V1 target encodes the 64 KiB ADPCM-B loop, pads the image to 512 KiB,
and checks SHA-256
c52017058a226a44506a5d94fc1f692b42fe818302761da64d4f7adc5e5928a7.
Normal and replay recipes copy and recheck that identical artifact before
packaging, so a correctly sized stale or zero-filled V1 cannot survive an
incremental build. All derived assets live below the ignored
platform/neogeo/build/ directory.
For the renderer/replay milestone before native audio, two isolated builds
produced identical
1 MiB P regions with SHA-256
d8ea97f3e05846467d298e9287bbf49567cc2799c8d16d8cf2aeac1153046b50
and identical 105,220-byte cartridge ZIPs with SHA-256
352e4e0272a59d10e7f52014196ed2a35dac5a90a5a08e56ac1a58f3818b98fb.
tools/check_reproducible_cart.py performs two complete cartridge builds in
different owned temporary directories. It validates the P/C1/C2/S/M/V region
sizes, the asset-clean manifest, the exact optional compatibility profile,
the full native contents of smbneo.zip, every header and payload byte in
smbneo.neo, the generated custom GnGeo driver, and the unique canonical
MAME software-list semantics. It requires byte-for-byte equality of every
region, both cartridge ZIPs, both NeoSD images, gngeo_data.zip, and the
generated neogeo.xml. It never cleans or writes the normal
platform/neogeo/build/ directory.
tools/rec_tool.py retains the input movie's exact frame count, source hash,
initial reset command, and RAM-initialization provenance. Movies from
FCEUX 2.2.1
without a RAMInitOption are labeled as legacy option 0, matching that
version's deterministic 00 00 00 00 ff ff ff ff power-on pattern; explicit
zero-fill option 2 is also supported. Fill-FF, random, malformed, or ambiguous
initialization is rejected instead of being silently imported.
An FM2 row is a source video-frame record, not a promise that the game CPU
received an NMI on that row. The original reset path also spends several video
frames before its first game NMI, while the translated Start() routine runs
synchronously. An exact FCEUX 2.2.1 comparison measured the resulting adapter:
- translated row 0 aligns with reference/FM2 row 7;
- the first seven FM2 inputs are retained without advancing the C core; and
- one no-NMI input hold is inserted on each continuous game-mode, area-initialization task-0 entry.
The replay cartridge defaults are therefore
REPLAY_BOOTSTRAP_FRAMES=7 and REPLAY_AREA_INIT_HOLD_FRAMES=1. They are
compile-time parameters rather than hidden constants. Debugger mailbox version
4 reports both parameters, the number of area holds actually consumed, and the
number of core frames actually advanced. The host runner rejects internally
inconsistent frame, tail, bootstrap, hold, or core-advance accounting before
classifying a pass.
The cartridge adapter is intentionally small. For instruction-level drift
diagnosis, tools/core_state_trace.py uses the reference emulator's explicit
lag markers instead of inferring them from already-divergent C state. FCEUX
marks the row after an extra source boundary as lagged, so the tool holds the
immediately preceding source row. It consumes one optional reference
lookahead row to make the final hold decision.
The original 6502 program also contains a few indexed table reads whose valid
indices extend into physically adjacent tables or instruction bytes. Packing
only declared .db data into C silently changed those reads. The
.rom_fallthrough lowering directive now retains such bytes as physical
storage without pretending they enlarge the logical table. Generator and
native regressions cover the fireball direction byte, full-byte bubble
scratch index, firebar mirror index, and flying-enemy random windows.
A bounded comparison can be produced with local ROM and movie files:
# Print, but do not execute, the exact reference environment and argv.
python3 tools/core_state_trace.py fceux-command \
--fceux /path/to/fceux-2.2.1 \
--rom /path/to/owned/smb.nes \
--fm2 /path/to/no-opposite-warpless.fm2 \
--output /tmp/smb-reference-000000-004101.csv \
--frames 4102
# After executing that printed reference command:
python3 tools/core_state_trace.py emit-translated \
--fm2 /path/to/no-opposite-warpless.fm2 \
--output /tmp/smb-translated-000007-004100.csv \
--input-frame-offset 7 \
--frames 4094 \
--hold-schedule-reference /tmp/smb-reference-000000-004101.csv
python3 tools/core_state_trace.py compare \
--translated /tmp/smb-translated-000007-004100.csv \
--reference /tmp/smb-reference-000000-004101.csv \
--reference-frame-offset 7 \
--skip-scheduled-holds \
--result-json /tmp/smb-state-comparison.jsonEvery output path is create-only: the Python command, Lua extractor, and optional native RAM dump refuse to overwrite an existing file. A complete CSV carries an explicit schema, contiguous frame numbers, frame semantics, source metadata, and a final completion marker. The pass domain compares controller input, OAM, and selected persistent gameplay fields. Whole-RAM, zero-page, stack, and work-buffer hashes remain diagnostic because the static C translation deliberately does not model the instruction stack and retains some transient buffers differently. Reference traces, RAM dumps, ROMs, and FM2 files are external evidence and are not tracked.
The printed reference command records the emulator executable's SHA-256 and
runs it through exec-fceux-verified. That wrapper reopens and hashes the
binary immediately before executing the same verified file descriptor, so a
later pathname replacement cannot be mislabeled as the measured build. The
Lua side requires and embeds the matching label and digest. If a streamed
write itself fails, its exclusively created partial file is deliberately
retained; the missing completion marker makes it invalid evidence and avoids
unsafe pathname cleanup.
FM2 is a useful interchange format here because its text input log contains
one RLDUTSBA controller record per emulated frame. The importer accepts
UTF-8 or legacy single-byte metadata, but requires every input record to be
strict ASCII and validates the exact game checksum, startup mode, controller
ports, emulator commands, frame count, and raw file hash.
Four external movies provide complementary regression lanes:
| Lane | Source and authors | Local FM2 records | Intended gate |
|---|---|---|---|
| Published warpless | HappyLee and Mars608, 18:36.78 | 67,117 | Definitive published all-32-stage core oracle |
| No-opposite warpless user file | yizhihongzhunan, 18:36.877 RTA | 67,677 | Preferred stock-controller-expressible all-stage oracle |
| Published warp run | HappyLee, 04:57.31 | 17,868 | Famous short synchronization smoke; intentionally cannot pass the sequential-stage gate |
| No-opposite warp user file | zdoroviy_antony, 17,882 frames | 17,882 | Short stock-controller-expressible synchronization smoke |
The published warpless movie is the strongest provenance/reference run and
was replayed on an original console, but its documented 6-2 route includes
simultaneous L+D+R. The no-opposite warpless user file is therefore the
better input for the Neo Geo joystick policy. It is still a frame-perfect
tool-assisted movie, not evidence of ordinary human execution.
The exact locally inspected downloads had these SHA-256 values:
| Movie | Raw FM2 SHA-256 |
|---|---|
| Published warpless | a9a3403b639cd30bd06d721ebb44449555ac64f979d95b3ce1c77642bc4ba423 |
| No-opposite warpless | c9afd9d1d6ee7abbeacf1ae32a74cd26fae2b42109815bf7bbbdccc253111f9b |
| Published warp run | 66f28af696f95f642ae962829d51a4d7071ee3255cf39b277c84c6f3ff6e191b |
| No-opposite warp run | fc5fde2e256b3a6c3765d317a648099537ff860bdd890160efd61104f11dcff4 |
TASVideos asks callers to link to the publication/user-file page instead of
hotlinking the download. The repository follows that rule and does not
contain any of these movies. Download a text .fm2 manually from its page,
retain author attribution, and build the local gate with:
make -C platform/neogeo replay-cart \
SMB_ROM="/path/to/smb.zip" \
REPLAY_FM2="/path/to/no-opposite-warpless.fm2" \
REPLAY_FAST=1 REPLAY_HARDWARE_PLAYABLE=1
python3 tools/run_neogeo_replay_gate.py \
--68k-overclock 10000 \
--timeout 2400
# Direct-renderer endurance lane. This target defaults to stock MC68000
# timing and a conservative 7,200-second host deadline.
make -C platform/neogeo replay-rendered-evidence \
SMB_ROM="/path/to/smb.zip" \
REPLAY_FM2="/path/to/no-opposite-warpless.fm2" \
REPLAY_HARDWARE_PLAYABLE=1 \
REPLAY_EVIDENCE_DIR="/tmp/smb-neogeo-rendered-evidence"The rendered lane requires scrot and the Python Pillow package. The
documented Make target checks both dependencies, selects the rendered build,
and enforces the hardware-playable/no-opposite input policy before beginning
the long emulator run. REPLAY_EVIDENCE_DIR should name a new external
directory; a path below /tmp keeps ROM-derived captures and replay evidence
outside the repository.
The generated header stores compact uint16_t durations and uint8_t
controller states rather than 67,000 padded structures. It also embeds the raw
FM2 SHA-256, canonical imported-recording SHA-256, exact input frame count,
initial command, RAM initialization option/seed, direction policy, and
opposite-direction count. The ignored gate build has its own ELF, map,
cartridge, and asset directory, so it cannot contaminate the playable build.
REPLAY_FAST=1 skips both the hardware renderer and apu_step_frame() and is
appropriate for the translated-core progression gate. Omitting it exercises
normal rendering and emulated VBlank accounting but is not a substitute for
the separate guest VBlank-budget measurement or an interactively paced run.
The runner accepts a result only
when it reaches the pass trap with both 32-stage masks complete. Failure,
incomplete playback, invalid mailbox data, debugger/emulator exit, occupied
debug port, and timeout all remain non-passing and produce a bounded
result.json plus logs. A cartridge-side checkpoint exposes an intermediate
mailbox every 1,800 frames; the debugger therefore does not need to stop and
round-trip on every frame.
The rendered-evidence lane adds two distinct traps per newly entered stage. The first records the immediate transition state. The second fires after two additional calls have completed the direct renderer and its VBlank swap. Its version-4 mailbox identifies the rendered build and records the direct renderer game-frame count, VBlank count, configured settle interval, and 16-bit uploaded/presented render generations. Before a rendered pass is accepted, the host requires:
- a hardware-playable FM2 with zero opposite-direction transitions;
- exactly 32 ordered transition/settled pairs with prefix entry/completion masks and matching world/level coordinates;
- renderer game frames equal to translated core frames at every checkpoint;
- at least one VBlank per rendered frame, with an exact two-rendered-frame source/core/game-frame and modulo-65,536 render-generation delta between each transition and settled pair;
- equal uploaded and presented generations at every screenshot-bearing trap;
- 32 valid diagnostic transition PNGs plus 32 non-blank settled-stage PNGs, with every settled playfield distinct from its paired transition and no two consecutive settled stages pixel-identical; and
- a valid terminal PNG tied to the existing 60-frame stable victory state.
The synchronous screenshot helper writes through a temporary PNG, validates its signature, dimensions, and per-file size bound, then atomically publishes it. The correctness boundary is inside the cartridge: after uploading the live sprite/FIX/palette state, the renderer increments a 16-bit generation; the following VBlank callback latches it as presented, and screenshot traps wait for equality without advancing the translated core or renderer frame. Those two shared words and one 16-bit callback copy also exist in normal cartridges and are included in the measured 14,176-byte BSS above.
Immediately before invoking scrot, the host also applies a bounded display
settling allowance: 50 milliseconds by default, configurable from 0 through
0.25 seconds with --display-settle-seconds. This does not establish frame
identity; it only lets the already-issued SDL/X11 presentation reach the X
window while the debugger is stopped. The selected allowance is validated and
recorded in result.json and adds no cartridge work or memory. The final
manifest records both file and centered 320x224 pixel hashes.
It also records a playfield-only hash below the 32-pixel HUD region and
requires the terminal playfield to differ from the settled 8-4 entrance.
The evidence directory also contains immutable, hashed snapshots of the
exercised artifacts and the runner/capture/debugger provenance needed to bind
the result to what actually ran. result.json has a hard 128 KiB bound.
Repository-local generated builds remain ignored; ROM-derived screenshots and
the evidence snapshot instead stay in the explicitly selected external
directory. GnGeo debugger mode disables its Z80/audio execution, so this lane
proves direct-renderer endurance plus progression, not audio, pixel-perfect
equivalence to the source console, or operation on physical hardware.
On 2026-07-24, the preferred no-opposite 67,677-frame warpless movie passed the fast cartridge gate through every stage and the final victory state:
| Measurement | Terminal value |
|---|---|
| Cartridge frame | 68,631 |
| Source replay tail frame | 954 |
| Stages entered | 32 (0xffffffff) |
| Stages completed | 32 (0xffffffff) |
| Stable victory frames | 60 |
| Bootstrap frames skipped | 7 |
| Area-initialization holds | 46 |
| Translated core frames advanced | 68,579 |
| Opposite-direction transitions | 0 |
The terminal mailbox had valid version-3 metadata, identified the
rendering-disabled fast build, retained the exact source frame count and
hardware-playable direction policy, passed all host-side accounting checks,
and reached the dedicated pass trap. The bounded 6,816-byte result.json had
SHA-256
223bd9f04b6f4b3842dc0c6a880e307622007a526347185bcba8d4b84afed393.
The exercised replay ELF, cartridge ZIP, and P region had SHA-256 values
559fa54a5896c754dbc12da2ed1e33895a5cecfd0b9b0a96c7534b2ec7727e67,
b65b03858dcc7e108dcef89869534c95630dc52311f869933a1a2869c86484f4,
and
409b8234902d48387d8edd08430925d1a71992be6071dfa1c6f64752468ce04f,
respectively.
The same movie also completed the direct-renderer lane with the stock emulated MC68000 cycle budget under the earlier version-3 protocol. That run remains evidence of full-game renderer endurance and ordered progression, but its screenshots were captured one host presentation behind their mailbox state. The following figures are therefore retained as historical endurance measurements, not as current state-bound image evidence:
| Measurement | Terminal value |
|---|---|
| Cartridge frame | 68,631 |
| Source replay tail frame | 954 |
| Stages entered | 32 (0xffffffff) |
| Stages completed | 32 (0xffffffff) |
| Translated core frames advanced | 68,579 |
| Direct-renderer game frames | 68,579 |
| VBlanks | 101,178 |
| Transition captures | 32 |
| Two-frame settled captures | 32 |
| Stable-victory captures | 1 |
| Opposite-direction transitions | 0 |
All 32 ordered transition/settled pairs had exact source/core/renderer frame
deltas of two and a VBlank delta of seven. The 65 decoded PNGs and all nine
immutable provenance artifacts passed the then-current version-3 validator.
The bounded 78,356-byte result.json had SHA-256
7a67f2fb11bd646e7f160c3b8522ba853be819bcaff96b4bff899a90d1ebfd65.
The exercised rendered ELF and cartridge had SHA-256 values
d4d87d31520f72aafcfe27f3e59c32419a682f4498e92a4f3a0b9fad559c62d7
and
09f21de1da091bf871b9cd931cd5389123c7f5da1ece6c7e94c23f214afea77d,
respectively.
The corrected version-4 presentation fence then passed a bounded first-stage
regression. Its transition mailbox reported source/core/render generation
550/543/543 and its settled mailbox reported 552/545/545. Uploaded and
presented generations were equal at both traps, the two-frame modulo-65,536
delta passed, and the settled PNG visibly contained the player sprite that
the stale version-3 image omitted. The corrected PNG has SHA-256
6966034a78426eea51d7b3e162af2e95970d8f32e480a1fa0548232e79429e36.
The bounded run intentionally timed out after collecting that pair, so a new
32-stage version-4 rendered pass remains outstanding.
The fast lane is a translated-core progression proof. The historical rendered run adds stock-clock renderer endurance; the version-4 smoke proves the new state-bound presentation mechanism at the first stage. Neither is a pixel-perfect source-console, audio, or physical-hardware claim. The normal rendered cartridge is covered separately by the stock-clock cadence probes, and the corrected renderer still awaits its follow-up MV1C run.
The milestone is checked with:
# Uses MAME's LSPC implementation and captures title/gameplay frames.
make mame-capture SMB_ROM="/path/to/smb.zip"
# Includes isolated MoonBit lowering/transpiler tests, generated-C comparison,
# native audio bridge tests, and MC68000 plus Z80 link/map guards.
make -C platform/neogeo verify
make -C platform/neogeo cart \
SMB_ROM="/path/to/smb.zip"
make -C platform/neogeo hardware-cart \
SMB_ROM="/path/to/smb.zip"
make -C platform/neogeo mame-cart \
SMB_ROM="/path/to/smb.zip"
python3 tools/check_reproducible_cart.py \
--rom "/path/to/smb.zip"
# Must run without GnGeo debugger mode so its Z80/audio path executes.
python3 tools/probe_neogeo_audio.py \
--evidence-dir /tmp/smb-neogeo-audio-evidence
python3 tools/rec_tool.py validate rec/warpless.rec \
--expect-end-frame 7987 \
--expect-transition-count 509
python3 tools/measure_neogeo_cadence.py \
--warmup-vblanks 120 \
--sample-vblanks 120 \
--assert-zero-missed
python3 tools/measure_neogeo_cadence.py \
--active-motion \
--warmup-vblanks 300 \
--sample-vblanks 120 \
--assert-zero-missed
make -C platform/neogeo replay-cart \
SMB_ROM="/path/to/smb.zip" \
REPLAY_FM2="/path/to/no-opposite-warpless.fm2" \
REPLAY_FAST=1 REPLAY_HARDWARE_PLAYABLE=1
python3 tools/run_neogeo_replay_gate.py \
--68k-overclock 10000 --timeout 2400The current verify result includes a passing native audio bridge regression
and Z80 map-checker regression. It also links the custom sound driver and
reports 10,965 bytes of Z80 fixed code, 1,945 bytes of static data, and 100
bytes of data-to-stack headroom. Both cartridge profiles package that driver
as M1 and the hash-checked generated triangle as V1; the reproducibility lane
checks the native regions and both final archives. The M1 image and linker map
are grouped build outputs;
changes to the nullsound library or included command helper trigger a relink,
and the map checker runs on every verification or cartridge invocation.
The cadence probe owns an isolated X display and process groups, verifies
that the fixed debugger listener belongs to its launched emulator, and has a
finite sampling deadline. It disables host pacing so its counters measure
whether one guest game frame fits each emulated VBlank budget rather than how
fast the host happens to run. Its result.json records that policy plus the
ELF, cartridge, and GnGeo-data SHA-256 values together with the full
timing/sample arguments, so the measurements remain tied to the binaries that
were exercised.
The supplied ZIP contained one 40,976-byte iNES file with the supported SHA-1. The generated canonical hardware cartridge loaded all P/M/V/S/C regions in ngdevkit-gngeo, initialized the AES BIOS and 68000, and produced stable 640x448 captures (2x scale) showing:
- the large centered title panel and complete one/two-player menu;
- the centered/cropped 256x224 viewport;
- the fixed
MARIO / WORLD / TIMEHUD; - sky, cloud, mountain, bush, and ground background tiles;
- Mario and other OAM sprites; and
- live palette/scroll updates across successive frames.
The earlier bounded light-load probe completed 89 game frames during its first 120 emulated display periods while caches warmed, followed by 120/120. An active-input smoke completed 269/300 during warmup and then 120/120. These remain useful build and ordinary-scene checks, but they predate the exact enemy-heavy windows above and must not be generalized into a full-game one-tick-per-VBlank claim. All cadence probes measure guest work per emulated VBlank rather than host wall-clock rate; interactive and audio runs exercise the separate real-time pacing policy.
Emulator captures and generated ROMs are verification artifacts only and are not tracked. MAME reproduced the two failures visible in the initial MV1C footage: a tiled FIX screen during BIOS handoff and truncated background strips that left the ground missing and extended black columns below solid tiles. Reserving transparent FIX tile zero and using the LSPC 32-tile full-height chain removed both failures in the same MAME lane.
A follow-up MV1C run then exposed an alternating Goomba pose with swapped but unmirrored 8x8 halves. The source animation deliberately swaps each half and requests horizontal mirroring, so the recorded shape identifies the LSPC flip phase. The cartridge now stores normal, horizontal, vertical, and combined orientations in separate C-ROM banks; OAM tile selection uses those banks and leaves hardware flip bits clear. A clean canonical package booted through the generated MAME software list, and 201 consecutive gameplay captures covered both Goomba walk poses without malformed halves. Pixel-level host tests verify every pre-oriented bank. The result still requires confirmation on the reporting MV1C setup.
The normal-mode audio probe establishes non-silent emulator PCM from active gameplay in addition to the sound-state, linkage, and packaging gates. It does not establish subjective fidelity, electrical timing, or operation on physical hardware.
- Reach one tick per stock-clock VBlank in every enemy-heavy regression window while rendering every in-range OAM entry.
- Retest the pre-oriented sprite build on the reporting MV1C, then broaden audio/video testing to other AES/MVS-compatible hardware, confirm the ADPCM-B V1 bus mapping, and tune output level plus visible-area offsets.
- Evaluate pulse-duty and short-noise approximations without consuming the remaining Z80 stack margin.
- Tighten the remaining fine-scroll left-edge masking cases.
- Extend the ROM-free pixel-transform and palette checks to more composite sprite poses without tracking game-derived captures.
- Add memory-card or save-state support.