01 — THE MACHINEWhat Zhaozhou is
Zhaozhou is a console architecture designed for the SuperStation One — a MiSTer-family board built on a Cyclone V FPGA. It is not a miniature PC GPU. The charter puts it plainly: it is “a purpose-built machine for transforming small authored inputs into overwhelming geometric motion.”
The defining idea is an inversion of how detail normally works. On a conventional machine, a model carries its own level of detail: an object is “high poly” or “low poly” as a property of the asset. On Zhaozhou, detail is a budget allocated by projected pixel importance — how much screen a thing actually occupies this frame decides how much geometry it gets. An army of a hundred creatures collapses continuously from full geometry to tiny meaningful moving forms, and nothing about the asset changes.
The second idea is that the ground remembers. Terrain is a live heightfield driven by field programs — small deterministic bytecode programs that deform it — plus a persistent per-patch surface sheet that records scarring. Craters, ridges, trenches and travelling waves are not decals. They are the terrain, and they stay.
What it is for
- Large outdoor battlefields with deformable terrain that permanently records battle damage
- Huge quantities of particles, many represented as actual low-polygon geometry rather than sprites
- Large procedural spells — geometry generated on the fly, not authored
- Two simultaneous local-player cameras and hard 60 Hz presentation
The three frozen display modes
| Mode | 3D render area | Scanout canvas | Purpose |
|---|---|---|---|
| Z60 | 384 × 240 | 384 × 240 | Primary single-player mode |
| Storm | 320 × 240 | 320 × 240 | Maximum overdraw, particles and transparency |
| Duo | 2 × 256 × 192 | 512 × 240 | Two 4:3 views plus 48 scanlines of cheap 2D interface space |
Duo costs almost nothing extra to raster: 98,304 3D pixels against Z60’s 92,160 — 6.7% more. The second camera adds geometry work, but simulation, terrain deformation, creature animation and particle simulation stay shared. Split-screen was designed in from the start rather than bolted on.
The laws that do not bend
- 60 Hz is a contract. A late frame is never partially displayed. The previous complete frame repeats and a deadline fault is recorded.
- The frame rate does not negotiate; form negotiates. LOD, particles, material cost and auxiliary effects degrade coherently before frame timing does.
- No hardware feature exists without an executable reference, counters, captures and differential tests.
- Reserve is a feature. At least 10% fabric and timing headroom is kept until the complete core is stable.
02 — GALLERYWhat the oracle produces
These four images are the reference renderer’s actual output, converted from its RGB565 canvas without regrading, then scaled 3× with nearest-neighbour so 240p reads on a modern display. Each caption says exactly what produced it.
Reminder: reference-oracle renders, not hardware. These came out of
zref::render, a deliberately slow, integer-only, CRC-able C++ implementation with
no floating point anywhere in the render path. Its job is to be the definition of correct, not
to be fast.

Duo — the committed golden frame 512 × 240 displayed · shown at 3×
The full Duo displayed stream: two independent 256×192 player views, plus the 48 border scanlines (24 above, 24 below) that centre them in the 240 active lines. Both views draw the same world with different cameras. Visible: the 360° sky drum, a heightfield island deformed by a terrain field program, a crack-ring scar stamped into the persistent surface sheet, a screen-space marker per view, and additively-blended particle sprites.
Side by side is how it is displayed, not how it is stored. A Duo
frame keeps its two views as contiguous packed blocks — view 0 at slot bytes
[0, 0x18000), view 1 at [0x18000, 0x30000), the blocks stacked —
and the 512-pixel line is assembled at scanout by reading one row from each. That
distinction was a genuine defect: the renderer and the RTL had implemented two incompatible
readings of the same specification, and the packed one won because the scanout fetcher
wants a single linear burst per view. Reading the slot as one 512-wide image instead
produces a garbled, dark frame. Note also that a slot is always allocated
0x3C000 bytes so a mode switch never moves it, while a Duo frame only
occupies 0x30000.
This is not a picture chosen to look good — it is the exact frame the
repository’s render_golden test pins by CRC. The two hashes below are the
committed constants in that test; this render reproduces both, which is what makes the
image evidence rather than decoration.
mode=DUO 512x240 displayed (2 x 256x192 views + 48 black border lines) status=0 commands=14 resource_misses=0 canvas_crc32c=0xD56BB9F4 displayed_crc32c=0x8614BB64 audio_events=2 terrain_velocity_samples=81

Sky — the drum, cap, clouds and sun 384 × 240 (Z60) · shown at 3×
A dusk sky set rendered on its own. The backdrop is a real 360° drum: a 48-column cylinder of two vertical gradient bands, closed by a 16-triangle zenith cap above and a 10240 × 10240 under-plane below, with a scrolling cloud sheet and an additively-blended sun quad over the top. 1,686 primitives, emitted once and rendered per view.
The thin cracks that used to run along this image are gone. They were a real defect in the fill rule — a strict comparison where the specification pinned a non-strict one, so a pixel exactly on a shared edge was claimed by neither triangle and the background showed through the seam. It is fixed, and pinned by a test that sweeps a shared edge through all 256 subpixel positions demanding no holes and no double-writes. This sky now measures zero isolated-pixel anomalies.
One known software-preview deviation remains: the band pass is double-sided and does not depth-test, so the far wall of the drum can show through the near one. The inside-facing winding is already recorded in the emitter for the RTL freeze.
mode=Z60 384x240 single view, DrawSky only status=0 canvas_crc32c=0xD3044EC2 sky primitives emitted=1686 (1536 drum bands + 16 zenith cap + 2 under-plane + 128 cloud + 4 sun) tick=1234 (cloud scroll) drum_yaw=0x1800 rot_proj: scale 18, pitch cos 17 / sin 4 (rotation-only, validated)

Island — deformation and damage 384 × 240 (Z60) · shown at 3×
The whole terrain story in one frame. A 161 × 161
authored heightfield patch is the island: 25,600 cells, 51,200 triangles,
at 0.15 m grid spacing over a ±12 m envelope. Over it run
six stacked applications of crater_ring — one real compiled
earth field program, submitted six times with different centres, radii and amplitudes, each
with its own footprint — whose height output is added to every column inside that
footprint. Those are the overlapping crater rims, computed live rather than authored. Two
SurfaceStamp records then write crack rings into the patch’s persistent
64 × 64 surface sheet, which darkens the shading where the ground was hurt. The terrain
pass decided 18,092 of the frame’s 92,160 pixels.
This render was previously capped at 25 × 25 cells and 1 metre spacing, and not by choice. The heightfield shading normal was built with a rescale that quantised it to whole world-units squared, so below about a metre every component rounded to zero, a zero-magnitude guard fired, and the entire patch shaded solid black. That defect is fixed — the normal is now an exact 64-bit cross product with an integer inverse-square-root normalize — and the density cap went with it. Sub-metre relief is what you are looking at.
The scar outlives the frame that made it: the sheet is per-patch and persistent, so a later frame with no stamp command renders identically. That is the “terrain records battle damage” claim, made mechanical.
mode=Z60 384x240 single view status=0 commands=17 resource_misses=0 canvas_crc32c=0x6DD1E610 patch=161x161 heightfield, +-12 m envelope, 8 m peak, 0.150 m grid spacing (sub-metre) geometry: 25600 cells -> 51200 triangles; terrain decided 18092 of 92160 pixels (coverage after overdraw, not fragment invocations) TerrainField program=crater_ring hash=0x484ADD8D (28 instrs), 6 applications stacked main crater: centre (0,0) r_in 3 m r_out 7 m amplitude 4.0 m; tick 19 of 100 -> phase 0.190 SurfaceStamp: 2 crack rings, tag 1 - outer radius 7 m width 4 m strength 0xC000, inner radius 2.5 m width 1 m strength 0x9000 terrain_velocity_samples=32114

crater_ring — a field program’s height lane 256 × 256 data map · shown at 3×
This one is a data visualisation, not a rendered scene.
It is the crater_ring earth program’s height output lane, evaluated
by the field interpreter on a regular grid across its footprint and false-coloured on a
linear ramp from zero (dark) to the maximum (bright). No camera, no lighting, no
perspective — just the number the program returns at each point.
Field programs are compiled bytecode, not shaders: a builder in
TypeScript emits a .zprog image, the image is hashed and CRC’d, and exactly
one interpreter evaluates it — the same one the renderer calls when it deforms terrain.
The C++ and TypeScript interpreters are held byte-identical by differential tests.
crater_ring earth field program, hash=0x484ADD8D, 28 instructions grid=256x256 over x,z in [-10 m, +10 m]; height output lane only params: p0/p1 centre (0,0), p2 r_in 3 m, p3 r_out 7 m, p4 amplitude 4 m phase=0.190 (age 19 of 100 ticks) height range: 0.0000 m .. 2.3227 m (fx16 raw 0 .. 152220) saturating evaluations: 0 of 65536
In motion — the deformation reel
Deformation is the one thing a still cannot show. These three loops are the reference oracle's actual frame sequences, encoded palette-exactly: each subject's frames share a single ≤256-colour palette of their own, the GIF is encoded against that palette with dithering off, and the encode is verified by decoding the GIF back and comparing every frame byte-for-byte against the captured canvases. What the loop shows is what the oracle rendered — no re-quantisation, no dithering, no interpolation.
Development-state renders. These run on today's single-heightfield terrain renderer. The dual-heightfield island format — true thickness, undercuts, holes punched clean through — is specified and frozen, and the renderer move is the next terrain wave. These loops will be regenerated and promoted when that lands; what you see here is where the machine is now.

Membrane — a travelling wave 384 × 240 (Z60) · 64-frame loop · shown at 3×
The signature behaviour: ground acting as a rubbery
membrane, not dirt with a hole punched in it. A new earth field program,
wave_pool, expresses a travelling radial wave —
amp · env(d) · sin(k·d − n·phase) — entirely inside the frozen field-IR
instruction set: the SIN op consumes its lane as a wrapping angle16 turn, so with an
integer cycle count n the field is exactly periodic and the captured loop
closes seamlessly. No new opcode was needed.
Every frame is a full evaluation of the program over the 161 × 161 patch through the one field interpreter, then the ordinary terrain pass. Nothing here is a vertex animation or a baked sequence — it is the deformation pipeline, ticked.
subject=terrain-wave mode=Z60 384x240 single view, reference oracle (zref::render) frames=64 step=8 ticks (frame f: tick = f*step) programs: wave_pool hash=0x8BDCEB63 (27 instrs), impact_wave hash=0x82F5F4E4 (30 instrs) field_apps=1 stamps=0 debris=0 shake_frames=0 palette=98 unique colours (palette-exact by construction, <=256 enforced) travelling radial wave, n=2 integer cycles: the loop closes seamlessly sequence_crc32c=0xDAA6AE41 gif: 64 frames at 20 fps, 466,986 B, palette 98 of 256, paletteuse=dither=none (no palettegen) encode verified: decoded GIF matches every source frame byte-for-byte (CRC-32C cross-checked) inter-frame change: median 8.0% of pixels, max 8.8%, min 7.2%

Impact — strike, wave, rebound, settle 384 × 240 (Z60) · 80-frame loop · shown at 3×
The donor's most famous moment, mechanised. A second field program,
impact_wave, drives the whole event: the strike dents the membrane, an
annular wave expands outward (a wavelet curve evaluated over the moving
coordinate d − speed·phase), the centre rebounds through a
damped oscillation, and everything returns exactly to rest — the wave is the point, and
the ground behaves like rubber throughout. Ballistic debris particles and a decaying
screen shake ride along as command-stream garnish.
The loop starts and ends at rest, so the restart reads as the next strike landing.
subject=terrain-impact mode=Z60 384x240 single view, reference oracle (zref::render) frames=80 step=8 ticks (frame f: tick = f*step) programs: wave_pool hash=0x8BDCEB63 (27 instrs), impact_wave hash=0x82F5F4E4 (30 instrs) field_apps=1 stamps=0 debris=8 shake_frames=10 palette=107 unique colours (palette-exact by construction, <=256 enforced) strike -> expanding annular wave -> centre rebound -> settle; debris + screen shake (erupt-style garnish); starts and ends at rest sequence_crc32c=0xD846D69C gif: 80 frames at 20 fps, 297,823 B, palette 107 of 256, paletteuse=dither=none (no palettegen) encode verified: decoded GIF matches every source frame byte-for-byte (CRC-32C cross-checked) inter-frame change: median 4.3% of pixels, max 11.0%, min 1.1%

Scars — damage that accrues 384 × 240 (Z60) · 96-frame sequence · shown at 3×
Three strikes land in sequence. Each one is another application of the
same compiled impact_wave program with its own centre and start tick — the
composed-lattice law: field applications stack in command order over the same patch. As
each membrane wave passes and settles, a SurfaceStamp writes a crack ring
into the patch's persistent 64 × 64 surface sheet, and the darkened scars
stay — the terrain records battle damage, mechanically. The sequence
holds at the end so the accrued scarring reads; the restart is the sequence replaying.
subject=terrain-scars mode=Z60 384x240 single view, reference oracle (zref::render) frames=96 step=8 ticks (frame f: tick = f*step) programs: wave_pool hash=0x8BDCEB63 (27 instrs), impact_wave hash=0x82F5F4E4 (30 instrs) field_apps=3 stamps=6 debris=0 shake_frames=0 palette=99 unique colours (palette-exact by construction, <=256 enforced) three strikes in sequence; surface-sheet scars persist and accrue (the loop restart is the sequence replaying) sequence_crc32c=0x67FA0C1F gif: 96 frames at 20 fps, 300,500 B, palette 99 of 256, paletteuse=dither=none (no palettegen) encode verified: decoded GIF matches every source frame byte-for-byte (CRC-32C cross-checked) inter-frame change: median 4.9% of pixels, max 7.4%, min 0.0%
Native-resolution copies of all still images are published alongside the scaled
ones under renders/native/, in case you want the untouched pixels. Each animated
subject also keeps a representative still beside its loop.
03 — STATUSWhere the project actually is
Every number below was read out of the repository. Nothing here is rounded up, and the zeroes are as load-bearing as the rest.
The maturity ladder
Every block advances one rung at a time, and each advance must be justified by pinned evidence that a validator checks against git history. The ladder has seven rungs. The project stands on rung four, for six blocks out of eighty-seven.
Of the 87 blocks, 72 are FPGA/RTL blocks and 15 are software. The six at
RTL_VERIFIED are the memory and I/O spine — the VRAM arbiter, the HPS bridge, the
memory guard, the input snapshot and rumble paths, and the audio FIFO — verified in simulation
against their C++ references, with formal properties where the property is expressible.
Six further blocks carry blocked_on: hardware
(SYS.PLL, SYS.RESET, SYS.CDC, MEM.SDRAM,
SW.TOOLS.REPORT, SW.TOOLS.BOARDPROBE). The ledger validator refuses
to advance them past SPECIFIED no matter what evidence is offered, until the
hardware lane opens. That is a mechanically enforced honesty rule, not a convention.
Tests
| Suite | Count | What it covers |
|---|---|---|
| ctest, total registered | 58 | the whole C++/RTL lane |
· labelled fast | 48 | 48/48, zero failures — the gate every change must pass |
· labelled nightly | 44 | long randomized soaks |
· labelled lint | 12 | RTL and package lint gates |
· labelled formal | 5 | bounded model checking (see below) |
| TypeScript, compiler workspace | 216 | 216/216 as recorded 2026-08-15; table-driven, so the case count exceeds the 69 literal call sites across 9 files |
SystemVerilog files under fpga/rtl | 12 | 9 modules + 3 packages |
| Code, excluding generated and vendored output | ≈ 37,200 | 17,400 C++ · 15,900 TypeScript · 3,850 SystemVerilog |
One of the 48 fast tests, the clang-format gate, skips cleanly on
this machine because clang-format is not installed. ctest counts that as a pass;
this page counts it as a skip and says so.
Formal proofs
Five properties are registered for bounded model checking. Two are now genuinely established, and the distinction is one this project learned the hard way.
| Property | State |
|---|---|
MEM.GUARD no-escape | Proven. BMC depth 30, with a mandatory cover task; all seven covers reachable. Mutation-verified: removing the scanout bound, the blit lower bound, the blit_span clamp or the length check each makes it fail. |
AUDIO.FIFO bounds | Proven, and mutation-verified — though the mutation run was manual and left no re-runnable artifact. |
MEM.VRAM.ARBITER liveness | Fails, deliberately and honestly. The frozen bound of 40 cycles budgeted one burst per client turn, but a 64-byte request is four bursts. It fails at 40 and passes at 60, 90 and 120, so the true worst case is somewhere in 41–60. The assertion is left in the tree, failing, rather than quietly relaxed. |
MEM.SDRAM refresh bound | Not established. The cover passes; the BMC run is unfinished and excluded from the automated lane. |
INPUT.SNAPSHOT atomicity | Registered on the nightly formal lane. |
Because that liveness property does not currently hold,
MEM.VRAM.ARBITER sits at RTL_VERIFIED while citing a proof that
fails. The ratification record says the block drops a rung to UNIT_VERIFIED if the
corrected bound cannot be proven. That is an open item, and it is listed here rather than
waited out.
What the reviews caught
Adversarial review is the part of this project worth writing down. A consolidated review found three critical and five major defects, and an earlier one found a sixth of the same character. Every one of them was found while the entire test suite was green.
- A
SPLINEopcode that violated its frozen specification by a factor of 216 — and did so identically in both the C++ and the TypeScript interpreter, so the differential test that compared them agreed perfectly. - A sun that was invisible. Its centre alpha was computed as
(a+128)>>8on a value clamped to0x10000, so full brightness wrapped 256 → 0. Any sun energy above about one third rendered nothing at all. - A sky whose 48 drum columns collapsed to a single angle: a turn fraction was shifted right by 16, so all 1,536 band triangles landed on nine distinct X coordinates, every one of them zero-area. The bands never rendered.
- An
fx_sinthat read one entry past its 257-entry table, on inputs as ordinary asfx_cos(0). - A fill rule that dropped shared-edge pixels on both sides of the edge — a strict comparison where the specification pinned a non-strict one, so neither triangle claimed the seam. Measured on a directed case: ten holes before, zero after.
- A formal proof that had never once been elaborated. Its assertions were implications on a signal a driver defect made unreachable, so it passed vacuously. When the lane was made real it failed immediately — and this had happened twice, on the same lane, to two different blocks.
- An arbiter liveness bound that fails when actually proven, described above.
Making the memory-guard proof real also surfaced a genuine escape: the
blit_span field was unclamped, so blit_base + blit_span could wrap 32
bits and the wrapped window admitted writes far outside the map. All of the above are fixed and
pinned to commits. The reason they are on this page is that a green suite is evidence about the
tests, not about the design.
Phases
The charter defines fourteen phases, 0 through 13, each with a hard acceptance gate. One phase has a written gate report. Substantial merged work exists beyond it — memory, input, audio, the Form language frontend, the whole software renderer these images come from — but no gate report covers it, so it is not claimed as gated.
The one outstanding Phase-1 gate is “one tiny Form program parses, type-checks and lowers to deterministic C++”, deliberately deferred to Phase 3 by the build plan. The report that records this says so itself, in writing.
First synthesis
Synthesis has been run exactly once, and not on the console.
| Metric | Result |
|---|---|
| Toolchain | Quartus Prime 17.0.2 Lite |
| Device (provisional) | Cyclone V 5CSEBA6U23I7 |
| ALMs | ≈ 51 |
| Registers | 96 |
| Pins | 92 |
| DSP blocks | 0 |
| M10K blocks | 0 |
| Errors | 0 |
Three caveats, all of which matter more than the numbers. The design under synthesis is the
ABI probe — a 52-line streaming CRC-32C engine over the generated ABI package
— not the console. The part is provisional: the real device
ordering code is Phase-0 board truth and is not yet known. And the numbers live in a commit
message rather than a parsed report, because the report parser is deliberately untested until
it has a real Quartus fitter report to eat. Consequently no block records a measured resource
figure, and no block 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. This is that list.
- Board truth. SDRAM memtest, real timing constants, sequential and strided
bandwidth, burst latency versus length, clock-tree stability, input latency. The file
reports/board_truth.jsondoes not exist — by design. No probe result has ever been simulated or assumed, so the SDRAM timing constants remain explicitly cycle-approximate. - The MiSTer framework. The upstream
sys/directory is not vendored yet, which blocks the PLL, reset and clock-domain-crossing blocks. - Synthesis and timing closure. Fitter reports, resource figures per block, and any claim that timing closes with margin.
- The top three maturity rungs.
SYNTHESIZED,INTEGRATEDandHARDWARE_PROVENare unreachable for all 72 RTL blocks until the hardware lane opens. - The soak tests. The eight-hour simultaneous video/input/audio/memory stress and the twenty-four-hour memory stress. Not runnable, and explicitly not simulated.
And what simply is not built yet
- The Wound Lab demo does not exist. The charter specifies it in detail — one floating island, two player wizards, two 256×192 cameras, three creature species, twelve units per player, and four spells (Raise, Break, Wave, Shatter). None of it is implemented; the directory holds a placeholder file and nothing else. The marker sigils in the renders above are 8×8 test patterns standing in for wizards, not wizards.
- The RTL rasterizer is not started. All five raster blocks — tile store,
edge walker, early-Z, fragment and resolve — are contract-only at
SPECIFIED, and the raster RTL directory is empty. The rasterizer that produced every image on this page is the software one, in C++. That is Phase 4 and 5 work. - No geometry frontend, no creature system, no particle RTL, no compositor. Those are Phases 8 through 11.
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 RTL is written: an executable reference implementation, directed tests, randomized differential tests against that reference, and formal properties where a property can be stated. A block advances one rung at a time and each advance is pinned to evidence a validator re-checks against git history. Six blocks are blocked on hardware and no amount of evidence can move them — the tool enforces it.
The command ABI is generated from a single interface definition into C++, TypeScript and SystemVerilog, and the three are held byte-identical by a conformance test that fails on any drift. The field-program interpreter exists twice, in C++ and TypeScript, and a fuzz-parity test compares them on generated corpora. Frame packets are sealed, validated fail-safe, and replayed; canvases are pinned by CRC-32C. There are no floating-point operations anywhere in the render path — the software renderer is integer-only precisely so that hardware can match it exactly rather than approximately.
The part that is easy to get wrong
Differential testing catches disagreement, not shared error. Adversarial review rounds — reading the code against the frozen specification, hunting for defects the suite cannot see — have found real bugs while the entire test suite was green:
A 216 amplification in the spline operation. Both
interpreters computed the final Catmull-Rom term as v · 215 instead
of v / 2. Every non-knot input saturated to the maximum representable value.
Spline knots evaluate with v = 0, which hid it — and the C++/TypeScript
differential test compared two implementations that shared the identical mistake, so it
could not possibly have caught it.
A sky-geometry collapse that made the sky invisible. The drum column angle was computed with a shift that is zero for every column, so all 48 columns shared one angle. All 1,536 band triangles degenerated to vertical lines and the rasterizer dropped every one of them at the zero-area check. The 360° skybox silently did not exist, while the layer-census, UV and scroll tests all stayed green — they counted primitives and checked texture coordinates without ever asking whether a pixel had been drawn. The fix came with an assertion that pins the geometry itself: zero band pixels before, 371 after.
An invisible sun, in the same review. A hand-rolled fixed-point-to-byte narrowing produced 256, which truncates to 0 in a byte — so for any sun energy above one third, the additive quad contributed exactly nothing. The commit message notes this is the same defect class as an earlier finding, reintroduced by writing a second implementation of a frozen conversion instead of calling the one that already existed.
Also caught: a fill rule that dropped every shared-edge pixel on both sides of a seam, a sine table read one entry past its end, and world-space markers that grew with distance instead of shrinking. Each fix ships with a test that fails if the bug is reintroduced.
The lesson the project keeps re-learning, and keeps writing down: a green suite is evidence about the tests, not about the machine. Counting primitives is not the same as checking that a pixel was written.