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
- Large outdoor battlefields whose terrain permanently records damage
- Huge quantities of particles, many as real low-polygon geometry rather than flat sprites
- Large procedural spells: geometry generated on the fly, not authored
- Two-player split-screen at 60 frames per second
The three 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 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
- A late frame is never partially shown. The previous complete frame repeats and the miss is recorded.
- When a frame gets expensive, form gives way first. Detail, particles and effects degrade before frame timing does.
- No hardware feature exists without an executable reference and tests that compare the two.
- Reserve is a feature. At least 10% of the chip is kept free until the complete core is stable.
02 · GALLERYWhat the reference renderer produces
These images are the reference renderer's actual output, converted straight from its 16-bit canvas without regrading, then scaled 3× with no smoothing so 240p reads on a modern display. Each caption says exactly what produced it.

Duo: the committed golden frame 512 × 240 displayed · shown at 3×
The full two-player picture: two independent 256×192 views of the same world from different cameras, centred by black border bands. Visible: the 360° sky, a heightfield island deformed by a terrain program, a crack-ring scar stamped into the ground's persistent damage record, a marker sigil per view, and additively-blended particles.
This frame earned its place by catching a real bug. The renderer and the hardware design had implemented two incompatible readings of how a split-screen frame is stored in memory; read it the wrong way and you get a garbled, dark image. The stored layout was ratified, both sides now agree, and this exact frame is pinned in the test suite by checksum. The two hashes below are the committed constants, and this render reproduces both.
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: drum, cap, clouds and sun 384 × 240 (Z60) · shown at 3×
A dusk sky rendered on its own. The backdrop is a real 360° cylinder of 48 gradient columns, closed by a zenith cap above and an under-plane below, with a scrolling cloud sheet and an additive sun over the top: 1,686 triangles, rendered like any other geometry.
Thin cracks used to run through this image. They were a genuine defect in the rasterizer's fill rule: a pixel landing exactly on the edge shared by two triangles was claimed by neither, so the background peeked through the seam. The fix is pinned by a test that sweeps a shared edge through all 256 subpixel positions demanding no holes and no double-drawn pixels.
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 terrain story in one frame. The island is a 161 × 161 grid of
height values: 51,200 triangles at 15 cm spacing. Over it run six applications of
one compiled terrain program, crater_ring, each with its own centre, radius
and strength. Those are the overlapping crater rims, computed live rather than sculpted
by an artist. Two more commands stamp crack rings into the island's persistent damage
sheet, which darkens the shading where the ground was hurt.
The scar outlives the frame that made it: the damage sheet persists, so a later frame with no stamp command renders identically.
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 terrain program, visualised 256 × 256 data map · shown at 3×
This one is a data visualisation, not a scene. It is
the raw height output of the crater_ring program, sampled on a grid and
false-coloured from dark (zero) to bright (maximum). No camera, no lighting, just the
number the program returns at each point of the ground.
Terrain programs are compiled bytecode, not shaders: a compiler emits a small program image, the image is checksummed, and exactly one interpreter evaluates it, the same interpreter the renderer calls when it deforms terrain. The interpreter exists twice, in C++ and TypeScript, and tests hold the two byte-identical.
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 loops are the reference renderer's actual frame sequences, encoded with each subject's own exact colour palette and verified by decoding the file back and comparing every frame byte-for-byte against the source. What the loop shows is what the renderer drew: no re-compression, no dithering, no interpolation.
Development-state renders. The terrain renderer has just moved to a dual-heightfield island format: islands now have a modelled top and bottom surface, so holes can be punched clean through (the last loop below). The first three loops were captured before that change and still reproduce bit-for-bit under it; their pinned checksums did not move. Surfaces are still flat-shaded solid colour: the texturing stage is specified but deliberately not built yet.

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 terrain program,
wave_pool, expresses a travelling radial wave using only the instruction
set the hardware will freeze. The wave is exactly periodic by construction, so the loop
closes without a seam. Every frame is a full evaluation of the program over the whole
island, then the ordinary render. Nothing here is a canned animation.
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 exactly 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×
Sacrifice's most famous moment, mechanised. One program,
impact_wave, drives the whole event: the strike dents the ground, a ring
wave expands outward, the centre rebounds through a damped wobble, and everything
returns exactly to rest. Ballistic debris and a decaying screen shake ride along. 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: the same compiled
impact_wave program applied three times with different centres and start
times, stacking in command order. As each wave settles, a crack ring is stamped into the
island's persistent damage sheet, and the darkened scars stay. The
sequence holds at the end so the accumulated scarring reads.
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%

Breach: a hole punched clean through 384 × 240 (Z60) · 64-frame sequence · 320 m island · shown at 3×
The new island format doing the thing it exists for. This island has real thickness: a top surface and a modelled underside, five metres thick at the rim and about twenty-seven at the heart. A crater digs down through it; when the ground gets thin enough, cells give way entirely and simply cease. No top, no bottom, sky straight through the island, debris falling out of the world. Because neighbouring cells share corners, the ground sags toward the hole before dropping; the crumble comes from the geometry itself, not from an animation.
Where Sacrifice dressed every hole with a fixed fake curtain wall, these walls end at the island's actual underside. 246 cells give way across twelve frames, every one a discrete, exactly-reproducible event.
subject=terrain-breach 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=0 stamps=0 debris=8 shake_frames=10 palette=105 unique colours (palette-exact by construction, <=256 enforced) 320 m dual-heightfield island (2.0 m pitch = 25 Island-Patch pages of ground, one Phase-3 envelope patch), modelled keel 5..27 m thick; 36-step bake ramp digs a 30 m pframe_0000_crc32c=0xCA2BB028 gif: 64 frames at 20 fps, 110,275 B, palette 105 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 0.9% of pixels, max 16.0%, min 0.0%
Untouched native-resolution copies of the stills live under
renders/native/. A dated, append-only diary of renders (what the
machine looked like then, beside what it looks like now) will join this gallery. A subject
only enters the diary once it is definitive for its phase; with terrain mid-migration and
texturing unbuilt, nothing qualifies yet, so the diary is still empty.
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.
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.
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.
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.
- Board truth. Real memory timings, measured bandwidth, clock stability, input latency. The file meant to hold these measurements does not exist, by design. No probe result has ever been simulated or assumed.
- The MiSTer platform layer is not vendored yet, which blocks the clock, reset and clock-domain-crossing blocks.
- Per-block resource figures and timing closure. The synthesis toolchain is open locally, but the real chip is unknown until a board is probed, so no such claim is made.
- The top three maturity rungs, for all 73 hardware blocks.
- The long soak tests: eight and twenty-four hours of combined stress on real silicon. Not runnable, and explicitly not simulated.
And what simply is not built yet
- The Wound Lab demo is barely begun. The charter specifies a full scene: an island, two player wizards, three creature species, four spells. What exists is its first thin slice, the Phase-2 gate demo: two controllers moving markers on the composed machine. The sigils in the renders above are 8×8 test patterns standing in for wizards.
- The hardware rasterizer is not started. Its five blocks are specification-only and its RTL directory is empty. Every image on this page came from the software renderer. That is Phase 4 and 5 work.
- No geometry frontend, no creature system, no particle hardware, 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 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.