Terrain
This file covers planetary terrain: the cube-sphere quadtree and its precision discipline, the editable layer stack, the height-source and pack formats, the per-frame node selection, and the render path that draws a body as one instanced call.
1. Planetary terrain (foundation and vertical slice, phases P0–P2b)
The engine’s ground has always been analytic: PlanetParameters is two colours and a roughness, and
engine/presentation/render/shaders/sky.frag paints a body from noise about its pole. That is
enough to read as a lit sphere from orbit and nothing like enough to stand on, which is why
docs/design/ATMOSPHERE_SYSTEM/README.md §15 records the missing terrain height field as the
blocker for its Phase D. docs/design/SOLAR_SYSTEM_OVERHAUL/README.md is the design that closes it;
engine/domain/terrain/include/SushiEngine/terrain/ is its first phase, and it is host-only — no
Vulkan header sits beneath any of it, because the server, the atmosphere’s nest, and a unit test all
have to be able to ask what the ground is doing without a renderer present.
A body is a cube-sphere quadtree. Six square faces, each subdivided uniformly, gives square
tiles, integer address arithmetic, and no pole singularity — the three properties an equirectangular
grid cannot supply at once. engine/domain/terrain/include/SushiEngine/terrain/tile_address.hpp is
that arithmetic and nothing else: TileAddress (a face, a depth, a cell), parent, child, the
same-face neighbour, a packed key, and the sample layout a tile carries — a 129×129 grid plus a
one-texel apron of the neighbours’ data, so a central-difference normal or a bilinear tap at a
tile’s edge never needs a second tile resident.
engine/domain/terrain/include/SushiEngine/terrain/cube_sphere.hpp is where a tile acquires a
position. Grid coordinates are warped by tan(t·π/4) before projection, which makes the angular
size of a cell nearly uniform across a face instead of varying by about 1.4× between its centre and
its corners, and the warp fixes ±1 exactly so adjacent faces meet. The projection onto a body’s
reference ellipsoid is a componentwise scale of a unit direction, which is the exact surface rather
than an approximation of it, and elevation displaces along the geodetic normal — the datum every
elevation model states its heights against, and on Earth up to 0.19° away from the radial direction.
The same file answers the question that makes single-precision geometry possible at planet scale. A vertex on Earth is 6.37 × 10⁶ m from the body centre and float32 carries about 1.2 × 10⁻⁷ of relative precision, so any planet-space quantity built in float32 arrives about 0.76 m off — against a 0.075 m cell at the deepest addressable level. Subtracting the camera afterwards does not recover it; the error is already in the operand.
normalized_difference never forms the large quantity: given a node centre held in double on the
host and a small offset from the grid, the identity |c| − |g| = (|c|² − |g|²)/(|c| + |g|)
rearranges the difference of the two normalizations into a form with no subtraction of
nearly-equal quantities left in it. The result holds about a micrometre at depth 20, in float32,
with one code path for every depth. Because the ellipsoid scaling is linear it commutes with the
difference, so ellipsoid_point_delta inherits the property rather than re-deriving it.
Ground is editable, and that is a property of the pipeline rather than a feature on top of it. A
TerrainLayer (engine/domain/terrain/include/SushiEngine/terrain/layer_stack.hpp) is a small
authored record — a footprint on the sphere, an operation, a profile — not an edited raster: a
crater is a direction, a radius, and a bowl-and-rim profile; a building pad is a flatten. Records
are bytes rather than megabytes, which is what lets an edit replicate over a network, serialise into
a scene, and be undone. TerrainLayer::order is explicit and unique within a stack, and a duplicate
is refused rather than resolved, so the composed ground is a pure function of the set of layers
and not of the order they arrived in — the property a server and a client need to agree on a
collidable surface.
Where an edit comes from. A stack nothing writes to is a stack that composes nothing, so the
records have an authoring surface:
engine/domain/terrain/include/SushiEngine/terrain/terrain_authoring.hpp declares
ITerrainAuthoring — insert, rewrite, remove, reorder, and the last frame’s selection — and
PlanetTerrain (engine/presentation/render/source/terrain/planet_terrain.*) implements it,
reached from a host through ISceneView::terrain_authoring() and drawn by the editor’s Terrain
window (applications/editor/source/terrain/).
PlanetTerrain owns neither the pack nor the stack. Both live in Terrain::PlanetTerrainStore
(engine/domain/terrain/include/SushiEngine/terrain/planet_terrain_store.hpp), which the world tier
holds so the simulation can read bathymetry from the same instance the renderer draws
(docs/design/OCEAN_AND_WATER_SYSTEM/README.md section 2.1.1); PlanetTerrain forwards the
authoring mutators to it and keeps only the selection and the slot cache.
The interface exists rather than the stack being handed out directly because an edit is two things, not one: the record changes, and every tile already compiled from the old record is now wrong. Each mutator appends the footprint it touched to the store’s journal, and the next frame drains that journal into a queue of resident tiles and re-stages them into the slots they already hold — re-staging rather than evicting, because a resident tile keeps its slot, so no frame in flight has an image pulled out from under a draw it has already queued and no device idle is needed. The queue is drained under the same per-frame upload budget as a streaming miss and ahead of it, since ground that is wrong is worse than ground that is coarse. The stack belongs to the body it was authored against and is dropped when the view travels to another one.
engine/domain/terrain/include/SushiEngine/terrain/height_source.hpp is the seam that keeps real
and invented bodies on one code path: a baked pak, a procedural generator, and a higher-resolution
regional inset are three implementations of IHeightSource, and no consumer learns which one
answered. Its data_depth is the other half of the contract and the more important one — it says
how deep the measurement goes, so the system can always distinguish ground it measured from ground
it invented.
engine/domain/terrain/include/SushiEngine/terrain/height_function.hpp composes the two, and is the
authoritative definition of a body’s ground: the physics collision patches are evaluated from it,
the headless server evaluates it with no renderer, and the tile compile shader that lands with the
render path is a port of it held to a stated tolerance by a conformance test rather than assumed
to agree. It evaluates whole tiles rather than points, deliberately — a point query would invite
exactly the per-column sampling pattern that made the shipped weather non-spatial.
The asset. se planet bake writes one .sushiplanet pack per body per quality tier under
cooked/planet/, and engine/domain/terrain/include/SushiEngine/terrain/pack_format.hpp is the
authority on its layout — the Python writer under cli/sushiengine/services/planet/ is a
transcription of that header, not a shared schema, for the same reason the climatology asset gives:
a schema shared between a tool and an engine header is a third thing to keep in step with both.
PlanetPack::adopt takes bytes and validates all of them before accepting any — magic, version, the
tile geometry the pack was baked for, that the index ascends strictly (which is what licenses the
binary search), and that every payload lies inside the blob at the length its codec requires. A pack
that fails leaves the object unloaded rather than partly loaded, and an unloaded pack is not an
error: the body falls back to the analytic ground, which is what shipped before terrain existed.
Three codecs share that layout, and each is the one before it with a block appended, so a reader
for the newest is a reader for the oldest plus two offsets. Codec 0 stores
TILE_SAMPLE_COUNT elevations as 16-bit fractions of the tile’s own range. Codec 1 adds one
SurfaceClass byte per sample. Codec 2 adds three colour bytes per sample, measured from the
body’s imagery, which makes a tile TILE_SAMPLE_COUNT * 6 bytes; the header carries a colour data
depth beside the elevation one, at byte offset 49, so a consumer can ask how deep the measurement
goes for each. Every payload in one pack is the same length, which is what lets the writer reserve
its index before baking anything and what lets a region be patched in place. The pack version
stays 1 across all three: a codec 0 pack baked before either block existed still loads, and a
reader meeting one reports its colour data depth as zero. cli/tests/test_planet_colour_fidelity.py
holds the colour block honest, comparing every sample of a baked tile against the source raster at
that sample’s own latitude and longitude.
se planet bake --region south,west,north,east re-bakes and patches only the tiles a geographic
box covers, in place, rather than rewriting the whole pack: the box resolves to tile addresses at
the pack’s own height data depth, and each targeted tile is recompiled from the source and
overwritten by cli/sushiengine/services/planet/patch.py’s PackPatcher, a fixed-length rewrite
that cannot change a tile’s payload size or the pack’s index layout. It runs the same landmark
convention audit a full bake runs before patching anything, and re-runs the accuracy audit on the
patched pack afterward — a region re-bake gets the same correctness guarantees a full bake gets,
just narrowed to the box.
A full-pyramid bake can be resumed with se planet bake --resume, so a kill hours into Earth’s
standard tier does not lose that time. PackWriter(resume=True) reopens the destination instead
of truncating it, verifying the header already there matches this bake’s body, tier, depth and
tile count; a per-tile marker beside the pack (cli/sushiengine/services/planet/progress.py)
supplies the four range floats close() needs to reconstruct the index for every tile a killed
run already wrote, since nothing else records them until close() runs. Because a tile’s payload
offset is a pure function of its ordinal, the bytes a killed run left behind are exactly where a
resumed run expects them — a resumed bake is byte-identical to an uninterrupted one, serial or
parallel.
Elevations are 16-bit fractions of a per-tile range rather than floats. That halves the asset and
costs nothing that matters — the quantisation step is the tile’s own relief over 65535 — and it is
what makes the bake’s accuracy claim a number: the bake re-reads its own output and compares
against the source raster, refusing to report success if anything came back further off than that
step. PackHeightSource is the policy over the format: when the quadtree descends past the depth
the pack stores, it resamples the nearest stored ancestor, which is where measurement stops and
where detail synthesis will later begin.
Two disciplines in the baker are worth naming, because both guard against errors that look like success. It reports the depth the source supports rather than the depth it was asked for, so nothing downstream mistakes resampled levels for measurement. And it verifies the raster’s grid convention against known landmarks before baking anything — a longitude read backwards produces a planet that is entirely plausible to look at and wrong everywhere.
Which patches get drawn. engine/domain/terrain/include/SushiEngine/terrain/quadtree.hpp
produces the cut of a body’s quadtree for a frame, and the shape of the algorithm is the
interesting part. It does not descend the tree; it refines it. The six root faces already cover
the body, and selection repeatedly replaces whichever node is furthest over the screen-space error
target with its four children. Because splitting a cut yields a cut, the result covers every point
of the body exactly once at every stage — including the stage a node budget stops it at, which is
what turns “we ran out of budget” into coarser terrain rather than a hole. A recursive descent
cannot make that promise: it commits to refining a subtree before it knows whether it can afford
to emit all of it. Because the heap pops in descending urgency, the cut a budget stops at is the
complete cut of a larger screen error, and the morph bands are laid out against that larger error
rather than the requested one; QuadtreeStatistics::effective_screen_error_pixels reports it. That
is what makes the morph watertight whether or not the budget bound
(docs/design/OCEAN_AND_WATER_SYSTEM/README.md §4.5.1).
Each selected node carries the camera-relative frame the single-precision vertex path consumes and the distance band over which it morphs into its parent, so nothing downstream recomputes either. The selection is host-side and double-precision because a node centre is a planet-scale coordinate and this is the one place in the terrain path allowed to hold one — and because the same cut is what the collision patch set and the builder’s placement queries need, neither of which has a renderer to ask.
Node bounds come from the pack index rather than from tile decodes
(IHeightSource::tile_bounds, a capability with a default): a cull tests far more nodes than it
draws, so a bounding volume that cost a tile read would be more expensive than the drawing it
saves.
Getting it onto the screen. A body is one draw: the visible nodes go into a storage buffer and
TerrainPass (engine/presentation/render/source/passes/terrain_pass.*) issues a single instanced
call of a shared 33×33 lattice, so the host cost of terrain is the selection and nothing else. Set 0
is full at 32 bindings, so terrain owns set 2 and builds its own pipeline layout around the
scene’s sets 0 and 1 — which is also why it binds the bindless heap itself rather than through
SceneLayout: Vulkan set compatibility requires identical push-constant ranges, and terrain’s is
not the mesh path’s. Because engine/presentation/render/shaders/terrain.vert matches
engine/presentation/render/shaders/mesh.vert’s output signature exactly,
engine/presentation/render/shaders/pbr.frag shades terrain without knowing it is terrain, and
terrain inherits the whole lighting, shadowing, IBL and tone-mapping path rather than duplicating
any of it.
Inheriting the shading path means inheriting its whole descriptor set, and that is a sharper
obligation than it sounds. Set 0 is a push-descriptor set, so a pass that writes only the
descriptors its own shaders name leaves the rest undefined — and
engine/presentation/render/shaders/pbr.frag samples them anyway, which costs the device rather
than a pixel. engine/presentation/render/source/passes/shading_set.hpp is therefore one shared
pair: declare_shading_set registers the graph reads, write_shading_set fills the twenty-six
descriptors, and the opaque, transparent and terrain passes all call both. The same obligation
covers the material array: terrain pushes a material per frame rather than naming index zero,
because an index into an array nothing was pushed to reads bytes, not a default.
Heights live in a 2D array image of fixed-size slots, indexed by TileResidency
(engine/domain/terrain/include/SushiEngine/terrain/tile_residency.hpp) — an LRU over anonymous
storage, which is what lets a node whose own tile has not arrived bind a coarser ancestor’s slot
through a scaled UV rectangle and draw a correct, coarser surface instead of nothing. Eviction
spares every slot bound within the frames-in-flight window, because the write and the read are in
different submissions and the frame graph derives barriers within one submission only.
The frame seam is engine/presentation/render/source/terrain/terrain_frame.hpp, and it is the
piece that took the longest to get right. Terrain lives in the body’s fixed frame — where the
elevations were baked and where the ellipsoid sits at the origin — while the scene is anchored at
an observer’s surface point. Environment::planet_body_axes carries the rotation between them,
filled by the ephemeris; the crossing itself (camera position, the shader’s matrix, the frustum
rotated into body-fixed axes) is header-only and Vulkan-free so that its three silent conventions
can be tested without a device. Terrain was also the first consumer able to detect the
prime-meridian frame error described in
the astro coordinate spaces,
for the reason that makes such errors survive: until something has an opinion about where a real
place is, a uniformly rotated planet under a uniformly rotated sky is indistinguishable from a
correct one.
Where the selected cut is fully bound, engine/presentation/render/shaders/sky.frag’s analytic
ellipsoid stands down for that frame (PlanetTerrain::covers_body). The per-pixel depth test is
not sufficient on its own: the reference ellipsoid wins wherever real elevations dig below it,
which on the Moon is every mare. The selection is an exact cover, but binding is not: a node whose
address and every ancestor’s address miss the cache is dropped rather than drawn from noise, so a
body still streaming in can have selected nodes with no resident tile behind them. Standing the
analytic ground down on selection alone — PlanetTerrain::drawing, true as soon as one node binds
— would leave those gaps uncovered; covers_body is the stronger question, true only once nothing
was dropped. A body with no pack keeps the analytic ground.
Terrain writes its depth twice. TerrainDepthPass
(engine/presentation/render/source/passes/terrain_depth_pass.cpp) draws the same geometry
position-only, registered immediately after the depth prepass and before the occlusion, Hi-Z and
GTAO passes, so those three see the ground; the shaded TerrainPass runs late, after the opaque
meshes standing on it, so their finished depth rejects the fullscreen coverage it would otherwise
shade.
What is not here yet: sub-Nyquist detail synthesis. It is a later phase in the design document,
with an exit criterion there rather than in prose. PlanetPack::open (P3/S2,
engine/domain/terrain/include/SushiEngine/terrain/pack_format.hpp) keeps the pack’s index and
reads a tile’s payload from its own offset, so a tile’s pixels no longer require the whole pack
held in memory. The collision patch
set (engine/domain/terrain/include/SushiEngine/terrain/collision_patch.hpp and
engine/domain/terrain/include/SushiEngine/terrain/collision_patch_set.hpp) is built and covered
by tests/unit/test_collision_patch.cpp and tests/unit/test_collision_patch_set.cpp, but those
tests are its only callers: nothing in the engine asks the ground for a collider yet. Layers are
also not persisted: the scene file carries entities, the environment and the sky, and a layer
stack an author builds lives only in the view that drew it, so it is lost on a reload and on a
trip to another body. The record was designed to serialise — that is what makes it a record — and
giving it an owner in the scene is the step that closes the loop.

