FPGA console · development diary

Zhaozhou

A 240p-class, fixed-function 3D console being designed for an FPGA. Its detail goes where the camera is actually looking, its terrain permanently records battle damage, and its signature game is an action-RTS in the lineage of Sacrifice.

What you are looking at. Every image on this page comes from the project's reference renderer: a deliberately slow, integer-only C++ program that defines what the hardware must eventually produce, pixel for pixel. There is no FPGA board in this project yet, and the rendering hardware is not written. The parts of the machine that do exist as circuit designs are already held to this renderer: in simulation, the assembled console reproduces its output byte for byte, checked every frame. These images are the target the silicon will be tested against.

01 · THE MACHINEWhat Zhaozhou is

Zhaozhou is a console architecture aimed at the SuperStation One, a MiSTer-family board built around a Cyclone V FPGA. It is not a miniature PC graphics card. The project charter calls it “a purpose-built machine for transforming small authored inputs into overwhelming geometric motion.”

The defining idea inverts how detail normally works. On a conventional machine, a model carries its own level of detail: an asset is “high poly” or “low poly” by nature. On Zhaozhou, detail is a budget, allocated by how much screen a thing actually occupies this frame. An army of a hundred creatures collapses smoothly from full geometry down to tiny meaningful moving forms as it recedes, and nothing about the assets changes.

The second idea is that the ground remembers. Terrain is a live surface deformed by small deterministic programs. Craters, ridges, trenches and travelling waves are not painted-on effects; they are the terrain itself, and they stay.

What it is for

The three display modes

Mode3D render areaScanout canvasPurpose
Z60384 × 240384 × 240Primary single-player mode
Storm320 × 240320 × 240Maximum overdraw, particles and transparency
Duo2 × 256 × 192512 × 240Two 4:3 views plus a strip of cheap 2D interface space

Split-screen was designed in from the start, not bolted on: Duo draws only 6.7% more 3D pixels than the single-player mode (98,304 against 92,160), and the simulation, terrain deformation and particles are shared between the two views.

The rules that do not bend

03 · STATUSWhere the project actually is

Every number below was read out of the repository at a pinned commit. Nothing is rounded up, and the zeroes are as load-bearing as the rest.

88
blocks in the design ledger
78/78
fast test gate, zero failures (+1 honest skip)
205,026
checks in the 10,000-frame soak, zero failures
0
blocks synthesized or above
0
FPGA boards in this project

The maturity ladder

The design is split into 88 blocks: 73 hardware, 15 software. Every block climbs a seven-rung ladder one rung at a time, and each advance must cite evidence that a validator re-checks against git history. Nothing is on the top three rungs, because those require a physical board, and there is none.

SPECIFIED68
REFERENCE_COMPLETE3
UNIT_VERIFIED3
RTL_VERIFIED14
SYNTHESIZED0
INTEGRATED0
HARDWARE_PROVEN0

The fourteen blocks at RTL_VERIFIED are the whole console shell: command scheduling and DMA, the memory arbiter, guard and bridge, the video pipeline, input, audio, and the debug counters and per-frame checksums, each verified in simulation against its C++ reference. Six further blocks are marked blocked on hardware; the ledger tool refuses to advance them until a board exists, no matter what evidence is offered.

Phase 2 closed: the console runs as one machine

Until this week, every verified block had only ever been tested alone. Phase 2 composed them into one running machine: publish a sealed frame packet and the console fetches it, schedules it, copies the pixels, swaps buffers, scans out, and reports a per-frame checksum and counters, with no test scaffolding in the datapath. In the gate demo, two controllers move markers across a split-screen world through the real input path, and every one of 1,202 displayed frames matched the independent C++ reference over every byte. A randomized 10,000-frame soak (two and a half hours of simulated play) passed 205,026 checks with zero failures.

Composing the blocks revealed defects no per-block test could see: a data seam that silently carried only an eighth of each memory write, while its test compared exactly the eighth that survived; a deadlock between two blocks that are each correct alone; and both framebuffers sharing one DRAM bank, which shredded the display under real combined traffic. All three are fixed and pinned by tests.

The gate also measured that copying a finished frame off the machine for checking takes just under two frames, so debug captures run at 30 Hz. That number is the cost of the debug copy, not how fast the console draws.

Phases

The charter defines fourteen phases, each with a hard acceptance gate. Two have written, ratified gate reports.

0Board truth: probe the real hardware before freezing any numberBLOCKED · no board
1Specification, Form IR and oracle skeleton6 of 7 gates green
2Console shell: the composed machine, double-buffered scanoutGATED · 2026-08-16
3Software console and minimal Form languagein progress
4First exact tile and triangle: the hardware rasterizer beginsnot started
5Opaque textured 3D corenot started
6Static Mantle and visible surface damagenot started
7Live terrain deformationnot started
8Geometry frontend and The Measurenot started
9Creatures and Transform Loomnot started
10Myriad polygon stormnot started
11Primitive Forge and final 2D effectsnot started
12Production language and asset pipelinenot started
13Closure: timing, soak, cartridge and release flownot started

The one outstanding Phase-1 gate (a tiny program in the console's own language parses, type-checks and compiles) was deferred into Phase 3, in writing, and Phase 3 is now underway.

Tests and proofs

The fast gate every change must pass currently runs 78 tests with zero failures (one test, a code-formatting check, skips on machines without the tool and is counted here as a skip, not a pass). Beyond it: long randomized nightly soaks, dedicated memory and video lanes, and 216 compiler test cases. The codebase is roughly 53,000 lines (26,600 C++, 16,900 TypeScript, 9,500 SystemVerilog), excluding generated code.

Ten properties are registered for formal proof, which is mathematical model-checking rather than testing. One of them is worth retelling. The memory arbiter's fairness bound was frozen at 40 cycles and cited as evidence for a full development wave, but the proof had never actually been run. The moment it was made real, 40 failed. The bound was re-derived from the actual burst arithmetic and then proven exactly tight: the proof fails at 51 cycles and passes at 52, so 52 is the true worst case rather than a number that happened to pass. The old formula stays in the spec, struck through, so the error remains visible. One property, the memory-refresh bound, is not established: its full proof does not finish on this machine, so it is excluded from the automated lane and recorded as unfinished.

First synthesis

Synthesis has been run exactly once, and not on the console: a 52-line checksum probe went through Quartus to open the toolchain lane (about 51 logic elements, 96 registers, zero errors, on a provisional Cyclone V part; the real device is Phase-0 board truth). No console block records a measured resource figure, and none is SYNTHESIZED. There is no timing-closure data and no bandwidth data; those directories exist and are empty on purpose.

04 · BLOCKEDWhat needs physical hardware

The repository keeps a written list of everything it cannot honestly do without a board, and refuses to guess any of it.

And what simply is not built yet

05 · METHODHow it is being built

The interesting part of this project is not the renderer. It is the rule that nothing is allowed to exist as hardware until it already exists as something testable.

Every hardware block must have, before any circuit is written: an executable reference implementation, directed tests, randomized tests that compare the circuit against that reference, and formal proofs where a property can be stated. The command format is generated from a single definition into C++, TypeScript and SystemVerilog, and the three are held byte-identical by a test that fails on any drift. There is no floating-point math anywhere in the render path; the software renderer is integer-only precisely so hardware can match it exactly.

The part that is easy to get wrong

Comparing two implementations catches disagreement. It cannot catch a mistake they share. Adversarial review rounds, reading the code against the frozen specification, have repeatedly found real bugs while the entire test suite was green:

A 65,536× amplification in a curve instruction. Both the C++ and TypeScript interpreters made the identical mistake, so the test comparing them agreed perfectly, on the wrong answer.

An invisible sun. A brightness conversion overflowed so that full brightness wrapped around to zero: any sun above a third of maximum energy drew exactly nothing.

A sky that silently did not exist. A shift bug collapsed all 48 columns of the sky cylinder to a single angle, turning every one of its 1,536 triangles into zero-area slivers the rasterizer dropped. The tests counted primitives and checked coordinates; none of them ever asked whether a pixel had been drawn. The fix ships with an assertion that does: zero sky pixels before, 371 after.

Formal proofs that had never run. Two blocks carried "formally proven" badges resting on proofs the tool had never once elaborated; they passed vacuously. When the lane was made real, both failed immediately, and fixing one of them surfaced a genuine hole in the memory protection. The rule that came out of it: a proof may not be cited as evidence unless the tool has recorded a passing run in which the assertions were demonstrably reachable.

The lesson the project keeps re-learning, and keeps writing down: a green test suite is evidence about the tests, not about the machine. Counting triangles is not the same as checking that a pixel was written, and a proof that has never run is not a proof.