Hosts and rendering
How a game reaches a window or a browser, how the GPU is negotiated, and what
the renderer can actually draw: sprites, shapes, text, 2D lighting, tilemaps
and tile volumes, 3D meshes and models, and the world presentation stack.
docs/rendering-frame-pipeline.md, docs/rendering-surface.md and
docs/rendering-color.md are the contracts.
Each entry links its tests and the projects that exercise it. Surface marks match the row in parity where one is named: โ works and is exercised ยท ๐ก a useful slice, with the gap named ยท โ absent ยท โ not applicable.
Hosts and GPU
Native desktop
One Game loop runs windowed on desktop, in a browser and headless, with failures reaching the host whole.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Native desktop
Tested by: crates/sindri-desktop/src/host/app.rs, crates/sindri-platform/tests/game_loop.rs
Exercised in: games/platformer, games/orbital-baked
Evidence
Game receives start, fixed_update, update, and stop, each with a
FrameContext carrying the world, the input state, and frame time, and each
able to fail in a way that reaches the host rather than being swallowed. The
same loop runs windowed on a desktop, in a browser through the same winit
event loop and canvas attachment, and headless in a test.
DesktopApp supplies the window, event loop, and input translation once, so
an application does not rewrite them. Target-specific code is confined to the
crates that must have it. Application failures retain their complete
std::error::Error source chain when logged natively or announced through the
browser's sindri:failed event, rather than exposing only the outer host
error.
Browser / WASM
The engine runs in Chromium over WebGPU with the same pictures and the same Decay behaviour as native.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Browser / WASM
Tested by: scripts/browser/smoke.mjs, crates/sindri-export/tests/exports_a_project.rs
Exercised in: examples/cube, games/platformer
See: docs/browser.md
Evidence
The cube example draws the same picture in Chromium as it does natively, in
the same colours, over WebGPU, and games are playable there with Decay
executing on the browser target. scripts/browser/smoke.mjs checks that a page
starts the engine and fails when it does not.
GPU negotiation and swapchain policy
Shared adapter/device negotiation with conservative limits, one policy for every swapchain outcome, and enforced sRGB.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ
Tested by: crates/sindri-gpu/src/surface/tests.rs
Exercised in: examples/triangle, examples/cube
See: docs/rendering-surface.md
Evidence
Adapter, device, and queue negotiation is shared, with conservative cross-target limits, resource labels, and errors that name what failed. Swapchain acquisition has seven outcomes and one policy applied to all of them, so hosts do not re-derive when to skip, reconfigure, or recreate. Surfaces must negotiate an sRGB format or fail loudly. Headless adapter initialisation is proven in CI on software Vulkan.
Frame and scene extraction
Extraction, preparation and ordered passes
Frames are extracted from the world, validated and ordered deterministically by stage, layer and insertion.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ
Tested by: crates/sindri-render/src/frame.rs, crates/sindri-scene/tests/extraction/passes.rs, crates/sindri-scene/tests/extraction/textures.rs, crates/sindri-scene/tests/extraction/assets.rs
Exercised in: games/platformer
See: docs/rendering-frame-pipeline.md, docs/scene-extraction.md
Evidence
A frame goes through extraction, preparation, and rendering. Preparation
validates and orders passes deterministically by stage, then layer, then
insertion order; stage order is Opaque3d, Transparent2d, Overlay.
Gameplay writes to the world and nothing tells the renderer. SceneExtractor
derives an ordered frame from whatever the world currently holds, using the
built-in camera, mesh, sprite, tilemap, animation, and text schemas. No scene
needs hand-written extraction code.
Textures bind by reference: a scene names textures/badge.png, the renderer
knows only a TextureId, and one place knows both. An unbound reference draws
a magenta checker and is reported by name rather than failing the frame or
silently reusing the last texture.
Cameras, depth, cube primitive, inline textured surface mesh
Perspective and orthographic cameras with tested projection maths, depth, and deterministic offscreen PNG capture.
Engine ๐ก ยท Editor ๐ก ยท Decay โ ยท Proof ๐ก โ parity row Cameras, depth, cube primitive, inline textured surface mesh
Tested by: crates/sindri-render/src/camera.rs, crates/sindri-render/src/offscreen.rs, crates/sindri-scene/tests/extraction/cameras.rs
Exercised in: examples/cube
See: docs/cameras.md, docs/rendering-color.md
Evidence
What can be drawn includes a triangle, a coloured cube, and a textured cube with depth testing. The frame clears once before anything draws, so a scene of only sprites has a depth buffer to test against. Perspective and orthographic cameras exist with tested projection maths, and the zero-to-one depth range and Y-up convention are chosen in one place rather than at each call site. Colour space is enforced by a shared constant, and the offscreen capture verifies authored colours actually survive to the pixels. Offscreen rendering produces a deterministic PNG, which CI uploads.
Sprites and shapes
Sprites, sheets, UVs, layers, blending
Textured sprites in screen or world space, batched per space, layer and texture, cut by named sheet parts.
Engine โ ยท Editor โ ยท Decay ๐ก ยท Proof โ โ parity row Sprites, sheets, UVs, layers, blending
Tested by: crates/sindri-gpu/tests/sprite_batches_are_independent.rs, crates/sindri-gpu/tests/sprite_uv_rect.rs, crates/sindri-gpu/tests/world_sprites.rs, crates/sindri-gpu/tests/sprite_textures_follow_their_registry.rs, crates/sindri-render/src/sprite_batch/tests.rs, crates/sindri-scene/tests/extraction/sprites.rs, crates/sindri-core/src/sheet/tests.rs
Exercised in: games/platformer, games/orbital-baked
Evidence
Textured sprites carry tint, anchor, and layer. A sprite is either screen-anchored, which is the default and cannot be occluded by the world, or in the world, drawn through the world camera by its full transform and hidden by opaque geometry in front of it. Sprites batch per space, layer, and texture, with a measured five-to-one draw-call reduction. Each batch draws with its own camera and its own instances, from buffers of its own; a GPU test draws two batches at two scales into one frame and reads back the pixels that only come out right when they stayed separate.
A sliced image says how it is cut, once. A sheet document beside a
texture โ textures/tiles.png is sliced by textures/tiles.sheet, at a
derived ID nothing has to declare โ names the parts of it, either as a grid or
as explicit rects. A scene then writes textures/tiles.png#floor, and the #
that had always been rejected inside an asset ID is what carries the name,
because it was reserved so a fragment could not leak into a path. Names rather
than indices, so a re-slice that moves a cell does not silently change what a
scene draws. Rects are checked once, where a sheet binds, and ride on the
instance so every part of one sheet stays in one draw call โ a GPU test reads
the pixels back to prove the shader honours it. A name no loaded sheet places
draws the whole image and is reported by unresolved_sprites, the same rule an
unbound texture follows.
Advanced sprite colour transform
Per-instance RGBA multipliers and offsets, sample * tint * multiply + offset, without breaking batches.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Advanced sprite colour transform
Tested by: crates/sindri-scene/tests/extraction/sprites.rs
Exercised in: games/orbital-baked
See: docs/color-transform-design.md
Evidence
A sprite may carry independent RGBA multipliers and offsets evaluated as
sample * tint * multiply + offset, travelling per instance so two sprites
recoloured differently still share a draw call. The identity is
multiply = [1, 1, 1, 1] and offset = [0, 0, 0, 0], so a sprite authored
before the transform existed draws exactly as it did. A transform that is not
finite is refused at extraction rather than drawn โ the reachable case is
narrowing, since 1e39 is an ordinary f64 and an infinity once it is an
f32 โ while a finite value outside zero to one is left alone for the render
target to clip. The editor draws the two quadruples as labelled RGBA rows inside
an advanced section, collapsed until the scene has authored one and resettable
to the identity. Orbital's mine blast flashes white-hot through the colour
offset, which a tint cannot do, and cools within a few frames.
Procedural shapes
Rectangles, ellipses, grids, regular polygons and up-to-eight-point hulls evaluated crisply in the shape shader.
Engine โ ยท Editor ๐ก ยท Decay โ ยท Proof โ โ parity row Procedural shapes
Tested by: crates/sindri-render/src/shape/mod.rs, crates/sindri-decay/tests/a_script_authors_shape.rs, games/orbital-baked/tests/every_boss_is_its_own.rs
Exercised in: games/orbital-baked
Evidence
Rectangles, ellipses, grids, and regular polygons are evaluated in the shape shader. A polygon may also carry up to eight explicit 2D vertices; those points travel in the same instanced payload and the shader measures against the actual authored edges, so an irregular hull remains crisp without becoming a texture. When no points are authored, regular polygons keep their existing path. The eight-point cap keeps the per-instance vertex attributes inside Sindri's conservative WebGPU limits. Orbital Last Stand exercises the capability with the Strider's exact six-point hull.
Custom shaders / materials
A sprite can be drawn through a project .wgsl function, validated with naga by every host and hot-reloaded in the editor.
Engine ๐ก ยท Editor โ ยท Decay โ ยท Proof ๐ก โ parity row Custom shaders / materials
Tested by: crates/sindri-render/src/sprite_shader/tests.rs, crates/sindri-gpu/tests/sprite_shaders.rs, crates/sindri-scene/tests/extraction/sprite_shaders.rs, game/tests/the_sprite_shader_demo_works.rs
Exercised in: examples/sprite-shaders
Limits:
- Shaders reach
sindri.spriteonly, with one texture and no material asset.
Evidence
sindri.sprite names a .wgsl asset in shader and carries four
shader_params. The file defines
fn sprite_shader(input: SpriteShaderInput) -> vec4<f32>, handed the pixel as
it would draw unshaded, its UVs and the sprite's UV rect, its world position,
the four parameters and the scene's clock, with sample_sprite and
sprite_texel_size to read the texture again; the engine supplies the vertex
stage and pipeline around it. Parameters ride on the instance, so sprites
sharing a texture and a shader share a draw. Every host validates a shader with
naga as it binds it: the editor loads, watches and hot-reloads the ones a scene
names, sindri-player reads them natively and from the export manifest, and
sindri-export refuses one that does not compile. A failure names the file and
the line in the project's own numbering and the sprite falls back to the
built-in look. Decay writes this.sprite.shader_params.{x,y,z,w}.
examples/sprite-shaders draws an outline, a dissolve, a hit flash and a wave.
Sprite masking / stencil
sindri.sprite_mask lets sprites draw only inside or outside a soft alpha shape, without a stencil or broken batches.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Sprite masking / stencil
Tested by: crates/sindri-gpu/tests/sprite_masks.rs, crates/sindri-scene/tests/extraction/sprite_masks.rs, game/tests/the_sprite_mask_demo_works.rs
Exercised in: examples/sprite-masks
See: docs/sprite-masks.md
Limits:
- World sprites only.
Evidence
sindri.sprite_mask places a picture whose alpha is a shape, and a sprite with
mask: inside or outside and the mask's name draws only within or beyond it,
softly where the mask is soft. The mask is batch state read in the fragment
stage, so masked sprites still share draws and nothing needs a stencil.
examples/sprite-masks reveals a meadow and a hidden gem through a wandering
keyhole.
Nine-slice sprites
A sliced sprite or UI image keeps its sheet-recorded border and stretches only the middle, in one draw.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Nine-slice sprites
Tested by: crates/sindri-scene/tests/extraction/nine_slice.rs, games/platformer/tests/sliced_panels.rs
Exercised in: games/platformer
See: docs/nine-slice.md
Evidence
A sprite or UI image drawn sliced keeps the border its sheet records, in
pixels by sprite name, and stretches only the middle: nine quads in the image's
own space, sharing one draw. border_scale sets how big a border pixel is
drawn. The sheet slicer drags the border's guides on the image. The
platformer's HUD frame and signboard are one 24-pixel panel.
Sprite animation
Sprite frame clips, timing, loop state
Clips of named sheet parts, played as runtime state and chosen by scripts without dirtying the scene.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Sprite frame clips, timing, loop state
Tested by: crates/sindri-scene/src/animation/tests.rs, crates/sindri-scene/tests/extraction/animation.rs, crates/sindri-decay/tests/a_script_plays_a_clip.rs, games/low-tide/tests/a_crawler_looks_alive.rs
Exercised in: games/low-tide, games/platformer
Evidence
sindri.animation.sprite holds clips of sprite names and which one plays,
SpriteAnimations holds where each sprite has got to, and the cursor holds a
name rather than a rect so playback does not depend on where anything sits in
an image. Playback is runtime state, so watching an animation run does not
rewrite the scene it came from.
Animation.play, stop and restart name one of the clips the scene
authored; is_finished, frame and clip read where playback has got to;
set_speed scales it. Choosing is authored state and writes the component, so
it lives in the world and undoes and saves like anything else, while the cursor
is derived and is only read โ which is what keeps a script driving an
animation from dirtying the scene. play is idempotent so that the natural way
to write it (if moving { play("walk") }, named on every frame) runs the clip
instead of pinning it to frame one; restart is the separate thing to want.
An animated sprite whose reference names no part of its sheet draws the first frame of the clip it is playing rather than the whole sheet, so a scene shows a pose before anything has run it โ in a game's opening frame, in an offscreen capture, and in the editor outside play mode. Once a clip is running it decides alone: a frame that resolves to nothing draws the whole image rather than the resting pose, because a plausible picture of the wrong moment is the failure that hides.
Low Tide exercises speed-controlled clips on children of a moving deck: its two tread belts roll with actual movement, pivot in opposite directions, hold their frame while flooded and resume after the ebb.
Clip authoring and preview
The Sprite sheet tab slices an image on the canvas, names cells, and the animation inspector builds and previews clips.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Clip authoring and preview
Tested by: editor/src/slicer/tests.rs
Exercised in: games/platformer
Evidence
Selecting a texture opens it in the Sprite sheet tab, a top-row tab beside the Scene and Game views, where the image fills the canvas pixel-sharp on a checkerboard, zooms about the pointer with the wheel (5% to 6400%), pans with the middle or right button, and fits back with Fit. Each cell is outlined on the picture, named cells are labelled once there is room, and with nothing open the tab lists every image in the project. The slice itself is the inspector's while an image is open: columns, rows, margin and spacing are drags, so a packed sheet with gutters can be cut and not only one that divides edge to edge. A cell is named by clicking it, and the panel lists the cells that have names rather than a field per cell, so a 16x16 atlas is as workable as a four-frame strip. Saving writes the sidecar, and the browser then lists the sprites underneath the image, collapsed until asked for. A sprite's animation inspector creates, renames, and removes clips from those named cells, orders frames, edits timing and looping, chooses the runtime clip, and previews it without changing scene state.
Text
World-space text
sindri.text places a label in world units over the sprites of its layer.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row World-space text
Tested by: crates/sindri-scene/tests/extraction/world_text.rs, crates/sindri-render/tests/text_measurement.rs, games/orbital-baked/tests/damage_numbers.rs
Exercised in: games/orbital-baked
See: docs/world-text.md
Evidence
sindri.text is the UI label's fields in world units, placed at its entity and
drawn over the sprites of its layer. Decay writes the words with Ui.set_text
and Ui.set_number and reaches this.world_text.color. Orbital's landed shots
float their damage.
Rich text
With rich: true, [b], [i] and [color=#rrggbb] tags in UI or world text are spans.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Rich text
Tested by: crates/sindri-render/src/text/rich.rs, crates/sindri-scene/tests/extraction/text.rs
Exercised in: games/orbital-baked
Evidence
With rich: true, [b], [i] and [color=#rrggbb] tags in either kind of
text are spans. Orbital's critical hits float their damage in bold gold.
2D lighting
2D lights and shadows
Ambient plus point lights, normal maps and polar-depth shadows from occluders and tilemaps, for sprites, tiles and maps.
Engine โ ยท Editor ๐ก ยท Decay โ ยท Proof โ โ parity row 2D lights and shadows
Tested by: crates/sindri-render/src/light2d/tests.rs, crates/sindri-gpu/tests/sprite_lights.rs, crates/sindri-scene/tests/extraction/lights_2d.rs, games/platformer/tests/cave_lights.rs, games/low-tide/tests/the_basin_is_lit.rs
Exercised in: games/platformer, games/low-tide
See: docs/2d-lights.md
Limits:
- A frame carries the 16 lights nearest the camera.
Evidence
With lighting_2d on in sindri.environment, world sprites, tilemaps, flecks
and world shapes are multiplied by the light reaching them: an ambient color
plus every sindri.light2d point light within its radius, faded by its
falloff. A sprite opts out with unlit, additive shapes are light themselves,
and the UI is never lit. A sprite or tilemap may name a normal_map laid out
like its texture; a light's height then decides how squarely it meets each
texel, so a low light rakes across the relief. A light with shadows on is
blocked by sindri.light2d.occluder boxes, polygons and the solid cells of a
tilemap, through one-dimensional polar depth maps built on the CPU each frame. An optional five-sample area light produces approximate soft shadows; Lamplighter uses it for its lanterns.
Decay writes a light's color, intensity, radius, falloff and height. The Scene
view draws light radii and occluder outlines with handles. The platformer's
cave is the proof. A voxel world viewed as a map is lit too, and its
normal_maps name a normal map for each texture its squares draw from: Low
Tide's Basin is lit by a sun that follows the tide from noon through a golden
hour to a moonlit flood, and its crawler carries lamps and shadow-casting
headlights.
Normal map studio
The Normal map tab derives a sprite's normal map and previews it under the game's own 2D lighting.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ
Tested by: crates/sindri-assets/src/normal_map/tests.rs, tools/normal-map-web/src/lib.rs, editor/src/normal_studio/tests.rs, editor/src/native/normal_studio/scene/tests.rs, games/platformer/tests/studio_normal_map.rs
Exercised in: games/platformer
See: docs/normal-map-studio.md
Evidence
The responsive browser studio uses the same generation library and saves PNG
and settings files; its lighting preview is approximate. The editor Normal map
tab derives a sprite's normal_map from its brightness or a
painted height map, keeping its size, alpha and pixel edges, with each sheet
cell kept apart, and material presets as starting values. The preview is the
game's own 2D lighting rather than an imitation: the studio's surface, light
and ambient are a sindri.sprite, a sindri.light2d and a
sindri.environment in a world of its own, extracted and drawn as the Scene
view draws, and a test requires the frame to equal the one a scene using the
saved map extracts. It shows the map per channel, splits it against the sprite
lit without a map, and orbits a plane in 3D. Saving writes the map and a
.normalmap of the settings, so the map is made again from changed art; the
platformer's hero is lit through one made this way.
Tilemaps and tile volumes
Tilemaps (ortho + iso, overhang)
One entity draws a grid of sheet tiles into the same batches as loose sprites, painted in the Scene view.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Tilemaps (ortho + iso, overhang)
Tested by: crates/sindri-scene/tests/extraction/tilemap.rs, editor/src/tilemap/tests.rs
Exercised in: games/platformer, games/low-tide
Limits:
- Screen-space tilemaps render and edit as data, but direct painting is limited to world space.
Evidence
A tilemap is a grid of tiles drawn from one entity: sindri.tilemap carries
the map's grid, a palette of sprite names, and a flat array of cells indexing
that palette, with null where the map is empty. Its cells become instances in
the same batches loose sprites use, so a prop sorts among the floor rather than
behind it. Columns and rows lay out orthogonally or isometrically, and placing a
tile and finding the tile under a point are inverses on every cell of both.
Selecting a world-space tilemap shows the slices from its texture's sheet as a visual palette, lets its grid be resized while preserving the overlap, and turns primary drag in the Scene view into an undoable paint or erase stroke. The hovered cell is found by inverting the exact camera matrix used to render that viewport, so an orthographic or isometric map does not need separate picking approximations. The ordinary component fields still edit projection, tile size, tint, render layer, and space.
Autotiling / rule tiles
A sheet declares 16-edge, 47-blob or 16-corner terrains; painting one redraws every tile that depends on the cell or corner.
Engine โ ยท Editor ๐ก ยท Decay โ ยท Proof โ โ parity row Autotiling / rule tiles
Tested by: crates/sindri-core/src/terrain.rs, crates/sindri-scene/src/terrain_paint_tests.rs, crates/sindri-scene/src/terrain_corners_tests.rs, crates/sindri-decay/tests/a_script_paints_terrain.rs, crates/sindri-decay/tests/a_script_paints_corners.rs, games/lamplighter/tests/the_ground_is_painted.rs
Exercised in: games/platformer, games/lamplighter
See: docs/terrains.md
Limits:
- Corners terrains are proven only by Lamplighter's one ground terrain; the platformer proves edge and blob terrains.
- The editor brush does not offer a corners terrain.
- No random variants, probabilities or custom rules, and no Tiled map import.
Evidence
A sheet can declare terrains: the sixteen edges or forty-seven blob
sprites a cell draws for each pattern of same-terrain neighbors. The tilemap
brush and Grid.set_terrain paint a terrain and redraw every neighbor whose
picture depends on the cell, erasing included. The platformer's level is
painted with its ground terrain.
A corners terrain is sixteen sprites chosen by which of a tile's four corners
are the terrain, the layout of a Wang or Tiled corner set. It is painted by
corner: Grid.set_terrain_corner sets or clears the corner at a grid line's
crossing and rewrites the up to four tiles that meet there, each reading its
own corners back from the sprite it already shows. This was found by trying to
use the first tileset Pixel Lab generated for Lamplighter, which is laid out
this way. Lamplighter's ground is that tileset: scripts/ground.decay lays its
dirt path corner by corner, and a test reads the tiles back.
Stackable tile volumes
Sparse (column, row, level) cells from a .tileset, resolved into only the faces nothing covers, in either projection.
Engine โ ยท Editor ๐ก ยท Decay ๐ก ยท Proof โ โ parity row Stackable tile volumes
Tested by: crates/sindri-core/src/tileset/tests.rs, crates/sindri-scene/tests/extraction/tile_volume.rs, crates/sindri-scene/tests/extraction/baked_volume.rs, crates/sindri-scene/src/tile_chunk.rs, editor/src/tile_volume/tests.rs
Limits:
- No game in the repository uses a tile volume since Causeway moved onto the voxel world.
Evidence
sindri.tile_grid carries the geometry โ columns, rows, cell size,
projection, and the screen step one level costs โ and sindri.tile_volume
carries sparse cells at integer (column, row, level) coordinates naming tiles
in a reusable .tileset asset. Empty is absence rather than a reserved ID, so
an empty sky costs nothing to store. A tile says what its faces look like,
whether it hides a neighbour's shared face, whether it is solid, and how much of
its cell it fills; the renderer resolves each occupied cell into only the faces
nothing covers, so a column two blocks taller than its neighbour exposes two
side faces without a cliff case and removing a lower block opens a real hole.
A tile can be shorter than its cell. height is a fraction of a cell
measured from its floor, defaulting to one. A half is a slab. The value is
logical: it decides what the tile hides and how high something standing on it
stands, and says nothing about the art. Anything taller than one cell is
refused when the asset decodes, because occlusion and occupancy both assume a
cell's contents stay inside it. A neighbour hides a side face only when it is at
least as tall as the face it abuts, and a top face survives a block placed
above it unless the tile fills its cell.
Both projections are real. orthogonal or isometric decides where a cell
sits, which of its faces a view can see, and what "behind" means. One tile set
serves both: faces a projection cannot see are left undrawn. Depth belongs to a
cell's column rather than to each drawn face, so a tower keeps the depth of the
column it stands in.
Runtime tile volumes have one engine-owned chunk unit. A chunk is 16ร16
columns, addressed with Euclidean coordinates and held sparsely by
TileChunkStore; materializing it produces the existing scene component.
A volume is resolved once, not every frame. It is resolved into faces once
and replayed, with only the depth each cell sorts at measured again. World
gives each entity a revision, bumped whenever anything takes a mutable borrow
of it, and TextureBindings and TileSetBindings each carry a generation,
handed out process-wide; a volume keeps the ones it was built from, so editing
its cells, moving it, changing its grid, rebinding its tile set and re-cutting
its atlas each show up on the next frame. baked_volume.rs is one test per
input: each edits exactly one of them and fails if the volume goes on drawing
what it drew before.
A script reaches one cell at a time. Grid.block(volume, column, row,
level) answers with the tile a cell holds and Grid.set_block writes one,
with the empty string meaning absence in both directions. A write naming a tile
the volume's own tile set does not define is refused at the call.
Navigation over tile volume surfaces
A volume's walkable surface becomes grid walls by max_step, and Grid.walkable asks the same question from a script.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ
Tested by: crates/sindri-scene/tests/navigation/volume.rs, crates/sindri-decay/tests/a_script_asks_what_is_walkable.rs, crates/sindri-scene/tests/extraction/standing_on_ground.rs
Limits:
Grid.walkableis about a cell, not an edge, so a script-driven walker does not honourmax_step.- Separate climb and drop limits wait on directional edges.
Evidence
A volume is three-dimensional and grid navigation is not, so the bridge is a
surface: for each column, the height of the ground something walking across
it stands on โ the top of its highest solid cell, which is a fraction rather
than a level because a slab's top genuinely sits half a cell lower than a
block's. A column holding nothing solid has no surface at all: it is a hole.
Every edge between two columns is then either a step something can take or one
it cannot, and sindri-grid already refuses to cross a blocked edge โ so the
volume becomes walls rather than the pathfinder learning about levels.
sindri.grid.navigation carries max_step, one cell by default.
A grid still carrying sindri.tilemap navigates by that flat map alone, so
dropping a volume beside an existing floor never closes ground the game already
walks on. Only WorldGridNavigation::from_world_with_tile_sets can answer at
all, because which cells are solid and how tall they are is a question only the
tile set answers.
Grid.walkable(floor, x, y) answers whether a walker can stand where a point
falls, from the walkable surface the pathfinder walks, choosing the cell by
sindri_scene::nearest_cell, the same rule a walker's own placement reads
from. Off the grid, water, decoration and empty columns all answer the same
way. An authored occupant rests on whatever supports it; a walker stands only
on what is walkable.
World presentation
Camera follow / confine / shake
Orthographic 2D camera confinement keeps the view within level edges and can look ahead in portrait as the target moves.
Engine โ ยท Editor ๐ก ยท Decay ๐ก ยท Proof โ โ parity row Camera follow / confine / shake
Tested by: crates/sindri-scene/src/components/camera_confine.rs, games/platformer/tests/the_shell.rs
Exercised in: games/platformer
See: docs/cameras.md
Limits:
- Viewport confinement assumes an unrotated, unscaled 2D camera and an untransformed scene root; shake can cross the bounds.
3D directional + ambient lighting
sindri.environment and a directional sindri.light shade textured 3D geometry, with one bounded shadow map.
Engine ๐ก ยท Editor ๐ก ยท Decay ๐ก ยท Proof ๐ก โ parity row 3D directional + ambient lighting
Tested by: crates/sindri-gpu/tests/sunlight_faces.rs, crates/sindri-decay/tests/lights3d.rs
Exercised in: games/voxel-lab, examples/lighting3d, examples/daynight
See: docs/world-presentation-plan.md
Limits:
- Sky, environment profiles/volumes, weather, water, full material assets and cascaded shadows remain future slices; authored fog and post effects reach the shared project host.
Evidence
sindri.environment is authored scene state for background, ambient fill,
directional shadows, voxel contact-depth ambient occlusion, and an ordered
world post stack. The sun is a sindri.light entity (kind directional) aimed
by its rotation along local -Z and drawn and aimed in the Scene view;
SceneExtractor::lighting combines it with the environment's ambient into
renderer-owned WorldLighting, tolerating an invalid light in the editor;
textured cubes, authored meshes, and cached voxel meshes share that model in
editor and browser rendering. The default renderer lighting preserves the
previous unlit appearance for scenes without an environment. Voxel Lab is the
acceptance surface. Directional shadows use one bounded depth map with authored
coverage, 256โ2048 resolution, and bias controls; both transient world meshes
and persistent voxel meshes cast and receive them.
this.light and Entity.light expose color, intensity, range, spotlight
half-angles and independent boolean enabled state from the shared typed surface.
Setters validate full candidates before mutation, including f32 narrowing and
cross-field cone bounds; failed writes retain payloads and whole colors are
atomic. Reads honor omitted defaults; Color alpha stays one. The enabled flag
changes illumination without deactivating the entity or stopping its scripts.
examples/lighting3d is feature proof for Explorer's planned lighting track:
its checked script animates parented points/spots and the sun, reaches lit/dark
Session goals and exercises cube, inline and imported geometry. Completion,
hover, generated docs and typed batch checks share the same metadata. Native,
real WebGPU and editor Play controls are recorded in 3d-lights.md. In-tree
game adoption and local shadows remain
open; no new uniform or pass cost is introduced.
Voxel ambient occlusion / contact depth
Voxel meshes carry deterministic corner AO controlled by the environment's contact-depth strength.
Engine ๐ก ยท Editor ๐ก ยท Decay โ ยท Proof ๐ก โ parity row Voxel ambient occlusion / contact depth
Tested by: crates/sindri-voxel/src/mesh_tests.rs
Exercised in: games/voxel-lab
Limits:
- Screen-space AO for arbitrary geometry is absent.
Post-processing stack
Exposure, tone mapping, contrast, saturation, bloom and vignette in one ordered world post path; overlay/UI stays crisp.
Engine ๐ก ยท Editor ๐ก ยท Decay โ ยท Proof โ โ parity row Post-processing stack
Tested by: crates/sindri-render/src/bloom/mod.rs
Exercised in: games/voxel-lab, game
Limits:
- LUT grading and advanced cinematic effects remain future slices.
Authored fog/AO/post stack in shipped project hosts
The shared native/browser player and the offscreen project renderer apply an environment's fog, occlusion, grade and bloom as the editor does
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Authored fog/AO/post stack in shipped project hosts
Tested by: game/tests/the_game_holds_together.rs, crates/sindri-decay/tests/environment.rs
Exercised in: game
See: docs/3d-foundation-review.md
Evidence
SceneExtractor::world_settings gathers an environment's light, shadows, fog,
occlusion strength and grade into one WorldSettings, and
TexturedCubeRenderer::configure applies them, so the shared native/browser
player and the offscreen project renderer light a frame as the editor does. A
frame with post-processing goes through encode_lit_frame and its bloom.
Causeway ships tone mapping, grade, vignette and bloom on its lanterns and
beacon this way. Decay reaches the same settings while a game runs:
this.environment.fog, .post_process and .bloom read and write each field,
checked as the scene's own values are, and Causeway eases every island's fog,
grade and bloom from one mood to the next, flashing them for lightning.
3D models
glTF / model import
Self-contained binary GLB decodes to reusable geometry, hierarchy, materials and textures, and draws natively and in the browser.
Engine ๐ก ยท Editor โ ยท Decay โ ยท Proof โ โ parity row glTF / model import
Tested by: game/tests/the_game_holds_together.rs, crates/sindri-assets/src/decode/model/tests.rs, crates/sindri-render/src/model/tests.rs, crates/sindri-gpu/tests/imported_models.rs, crates/sindri-scene/tests/extraction/models.rs, crates/sindri-export/tests/imported_models.rs, crates/sindri/tests/imported_model.rs
Exercised in: game, examples/model-collision
Limits:
- Full model authoring, animation and skinning remain deferred.
- Causeway draws only static, generated models; no flagship imports artist-authored animated GLB yet.
Evidence
ModelAssetDecoder parses self-contained binary GLB into reusable CPU geometry,
node local matrices/children, materials and embedded base-color textures. The
tiny deterministic fixture exercises both index widths, multiple primitives,
mesh reuse, non-indexed triangles and malformed/unsupported diagnostics.
AssetLoader reuse is covered. A separate renderer resource and GPU path retain
normal/UV geometry, hierarchy, textures and materials, composing node and entity
matrices with inverse-transpose normals. GPU readback coverage checks colors,
depth and upload reuse; renderer resource tests check malformed data. Scene
ModelComponent references validate asset paths and extract shared resources
with entity world transforms. Disabled models are discovered without drawing;
missing bindings name the asset. The public facade converts decoded models
without cross-coupling foundational crates. The editor catalogue assigns the
registered component an existing icon/family. Render and static-collision asset
fields now declare model metadata, use a GLB-only project picker and complete
Add Component with an actual model. Independent widget/component history scopes
keep the two selections separate; native review verifies Undo/Redo, save/reopen
and Play with an alternate wall. Broader model authoring remains deferred. Model manifests and export retain original GLB bytes and discover
scene/prefab
references, including inactive entities. Native project capture and the WebGPU
project host prepare/bind models once through the existing asset queue and
apply scene lighting. The exact Low Tide cutaway crawler was visually inspected
in both runtimes at 1200 ร 1000, correctly oriented and visibly recognizable.
Exported bytes match the source hash. Full editor authoring,
animation and skinning remain deferred. This external proof does not claim
the repository's in-tree game completion surface. Model Collision Lab now also
has native player-window visual proof on X11/software Vulkan, after editor asset
selection and save/reopen; the imported wall blocks keyboard movement and the
walker renders on the goal pad.
Causeway draws every island, figure and building as a generated GLB with
emissive materials, lit by the sun and point lights.
See imported models.
Not yet
Font fallback and richer spans
There is no font fallback, and rich text has colour, bold and italic spans only.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ
Skeletal animation
Imported models are static: glTF animation and skinning, and further generated primitives, are open.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Skeletal animation
Tile volume terrain tools
Terrain-name painting, automatic supporting strata, side-face picking and slice views for tile volumes remain; see docs/tile-system-2.md.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ
3D foundation
3D orbit / third-person camera
sindri.camera.orbit and explicit Camera.orbit controls provide world-space follow, aiming, smoothing and 3D ray pull-in; Camera.perspective_fov changes FOV
Engine ๐ก ยท Editor ๐ก ยท Decay ๐ก ยท Proof ๐ก โ parity row 3D orbit / third-person camera
Tested by: crates/sindri-scene/tests/orbit_cameras.rs, crates/sindri-runtime/tests/an_orbit_camera_project_plays.rs
Exercised in: examples/orbit
See: docs/cameras.md
Evidence
examples/orbit is an authored feature project with a walker, raised goal and
blocking wall. Decay changes the perspective FOV and orbit angles; the shared
runtime follows, aims, pulls in against the wall and recovers after the wall
is disabled. Its project test opens, compiles and runs to the goal. Native and
exported Chromium captures were visually inspected before and after recovery;
the browser fetched the exported assets and reported the scripted goal without
runtime or GPU errors. The flat-color example produces fourteen screenshot
colors, so its local smoke used an eight-color minimum plus blue-walker and
gold-goal pixel checks instead of the generic sixteen-color blank-frame gate. Scene
regressions cover immediate pull-in, smoothed recovery, sensor/layer/self
filtering, rotated and scaled parents, Z locks, invalid settings and payload
preservation. Editor Add Component now creates Camera Orbit from a real active
spatial target with a stable ID; a Camera is required. Invalid numeric or target
edits are refused before commands, retaining the previous component. Unit tests
exercise defaults, self/child/inactive/UI/unidentified exclusions, validation,
Undo/Redo and save/reopen. Native review removes
the camera script from a scratch Orbit Camera Lab, authors orbit in the inspector
and reaches the walker goal with engine-owned pull-in/recovery. Selected Scene
view guides show the world-space focus, requested position/yaw circle and
actual camera pose during pull-in/recovery, with near-plane clipping and no
picking or world mutations. Parented target/owner, live poses, invalid targets,
overflow and a camera behind the Scene eye have regressions. Collision uses one
sight-line ray and synchronized obstacle poses; camera-volume sweeps and
Explorer's game/browser proof remain pending. Pointer lock has separate native,
browser and editor feature-lab evidence. This is new general camera
capability for Explorer in slice 6 of docs/3d-update.md.
Orbit Lab also exercises existing transform scale through normal keyboard controls: 1 narrows the wall, 2 restores it and R resumes automatic removal. Manual edits reactivate and retain the barrier. A synchronized center ray must hit it before an outside ray can verify the narrow/wide state, preventing an inactive/missing collider from counting as scale proof. Session regression observes reactivation, repeated edits, orbit recovery/pull-in and the goal; Chromium drives the same controls and captures both widths. Native scale review verifies matching mesh/collider guides, Undo/Redo, save/reopen, Play queries/goal and Stop restoring the saved scale. This is feature adoption of the existing general 3D scale capability; Explorer game proof remains open.
Shared standard material factors
One numeric MaterialFactors type and GGX shader serve opt-in cubes, inline/cached geometry, voxel owners and imported primitives
Engine ๐ก ยท Editor ๐ก ยท Decay โ ยท Proof ๐ก โ parity row Shared standard material factors
Tested by: crates/sindri-gpu/tests/standard_materials.rs
Exercised in: examples/lighting3d
See: docs/rendering-materials.md
Evidence
Shared MaterialFactors and direct GGX now shade opt-in cubes, inline/cached
meshes, voxel roots and imported primitives. sindri.material authors linear
base color, metallic/roughness and emission, with checked ranges and per-entity
factor replacement. GPU comparisons, zero-light emission, mask-alpha and cache
reuse, scene undo and voxel residency regressions, native/browser swatches and
native editor edit/Undo/Redo/save/reopen review exercise the numeric foundation.
Core GLB emissive factors survive decoding/preparation and render without scene
overrides. Decoder/facade/resource tests, zero-light GPU readback, override
restoration, masked-shadow invariance and native/browser imported GLB controls
exercise this addition. Absent overrides preserve legacy/imported looks. Material
texture-bearing assets, unified alpha, emissive maps and Explorer game adoption remain
open; see the material contract.
Authored textured geometry normals
Cubes have outward face normals; inline surfaces accept validated optional XYZ normals and cached render vertices carry them
Engine ๐ก ยท Editor โ ยท Decay โ ยท Proof ๐ก โ parity row Authored textured geometry normals
Tested by: crates/sindri-gpu/tests/standard_materials/normals.rs
See: docs/rendering-materials.md
Evidence
Cubes carry outward face normals, while inline mesh documents and cached render vertices can supply object-space normals for smooth lighting. Checked components reject malformed vectors/counts; absent normals retain flat shading. GPU comparisons with imported geometry exercise nonuniform scale, rotation and reflection; world color/shadow pipelines correct reflected front-face selection. Scene edit/history tests and native/browser smooth/flat ellipsoid controls prove the feature. Textured vertices grow from 24 to 36 bytes; per-draw uniforms and cache residency are unchanged. Editor authoring review, Decay access and Explorer adoption remain open. See normal semantics.
Lighting
One authored ambient contribution plus one directional world light shade textured 3D geometry and voxel terrain in editor viewports and Voxel Lab, with one bounded directional shadow map
Engine ๐ก ยท Editor ๐ก ยท Decay ๐ก ยท Proof ๐ก โ parity row Lighting
Tested by: game/tests/a_3d_lighting_project_plays.rs
Exercised in: examples/lighting3d
See: docs/3d-lights.md
Evidence
this.light and Entity.light expose color, intensity, range, spotlight
half-angles and independent boolean enabled state from the shared typed surface.
Setters validate full candidates before mutation, including f32 narrowing and
cross-field cone bounds; failed writes retain payloads and whole colors are
atomic. Reads honor omitted defaults; Color alpha stays one. The enabled flag
changes illumination without deactivating the entity or stopping its scripts.
examples/lighting3d is feature proof for Explorer's planned lighting track:
its checked script animates parented points/spots and the sun, reaches lit/dark
Session goals and exercises cube, inline and imported geometry. Completion,
hover, generated docs and typed batch checks share the same metadata. Native,
real WebGPU and editor Play controls are recorded in 3d-lights.md. In-tree
game adoption and local shadows remain
open; no new uniform or pass cost is introduced.
3D point and spot lights
Strongest-eight active points/spots, world-unit range/falloff, feathered half-angle cones and composed parent positions/directions share Lambert/GGX across meshes, voxels and imported models
Engine ๐ก ยท Editor ๐ก ยท Decay ๐ก ยท Proof ๐ก โ parity row 3D point and spot lights
Tested by: crates/sindri-gpu/tests/standard_materials/points.rs, crates/sindri-gpu/tests/standard_materials/spots.rs
Exercised in: examples/lighting3d
See: docs/3d-lights.md
Evidence
sindri.light now supports point lights with world-unit range. The strongest
eight active points shade primitive, inline/cached/voxel and imported geometry
through shared Lambert/GGX point lighting. Positions follow composed live
transforms and inactive ancestors suppress the light. Editor creation uses one
checked transaction and selected world-range circles; studio lighting omits
authored points. Native/browser feature controls and GPU/scene regressions are
described in the 3D light contract. This is Explorer feature
proof, with no in-tree game adoption yet; local shadows are absent.
Typed runtime light properties are described below. Points now share the
spotlight records
described below. The fixed uniform cost is 512 additional
bytes per color draw; shared buffers and spatial light assignment remain open.
Explorer's planned lighting track now has unshadowed spotlights as feature proof;
no in-tree game uses them yet. sindri.light.kind = "spot" uses composed world
position/forward with validated inner/outer half-angles in radians. Points and
spots share eight strongest-intensity slots, range falloff and Lambert/GGX
shading across world/voxel meshes and imported models. Cone strength feathers
with smoothstep between angle cosines. Color uniforms are now 880/944 bytes for
textured/model draws (eight 64-byte light records, plus the model silhouette
color); shadow uniforms, passes, writes
and persistent geometry uploads are unchanged. The editor creates down-facing
spots in one undo step and paints clipped selected inner/outer range cones.
Native/browser aimed/away controls and Scene/GPU/guide regressions provide
feature evidence. Local shadows and Explorer adoption remain open; runtime light
controls and mounted-light feature evidence are described below and in 3d-lights.md.
Runtime ambient and background controls
Typed entity environment access shares authored defaults and validates full candidates atomically
Engine ๐ก ยท Editor ๐ก ยท Decay ๐ก ยท Proof ๐ก โ parity row Runtime ambient and background controls
Tested by: crates/sindri-decay/tests/environment.rs
Exercised in: examples/lighting3d
See: docs/runtime-environment.md
Evidence
Typed this.environment / entity.environment address an existing authored
environment. Ambient intensity, RGB Color and RGBA background reads honor
defaults; writes validate the full payload before mutation and preserve unrelated
settings. Whole Color writes are atomic, fractional numbers stay fractional,
ambient alpha stays one, and stale/missing targets are runtime errors.
Lighting Lab extends its first two phases with cool/warm ambient and clear-color
changes, restoring original settings at the ready goal. Shared Session tests
observe the resulting lighting and frame clear; native/browser captures and
editor Play exercise rendering. This is feature proof for Explorer/Low Tide 3D's
planned lighting track, with in-tree game adoption and fog/post controls open.
Day/Night Lab below exercises the project clock and mounted-light integration. No renderer layout, resource lifetime or dependency changes.
See runtime-environment.md for the contract and validation evidence.
Scripted day/night cycle
Day/Night Lab opens, compiles and plays two Decay clock configurations through sunrise/midday/sunset/night and a continuing goal
Engine ๐ก ยท Editor ๐ก ยท Decay ๐ก ยท Proof ๐ก โ parity row Scripted day/night cycle
Tested by: game/tests/a_daynight_project_plays.rs
Exercised in: examples/daynight
See: docs/daynight.md
Evidence
examples/daynight is feature proof for Explorer and external Low Tide 3D's
planned lighting track. Its Decay-owned clock smoothly changes sun brightness,
color and rotation, ambient light and background through four phases. A moving
GLB carries a point and spotlight; night illumination works while the sun is
disabled. Real-project Session tests exercise two clock configurations, all
phases, composed night positions, continued motion after the goal, frozen hours
and independent night controls. Native/browser images isolate moving sun
shadows on imported ground with fixed model poses, and local illumination at
midnight. Native editor Play runs the same scene. See daynight.md for measured
evidence. No new host API, renderer path, dependency or engine time service;
in-tree game adoption and fog/post controls remain open.
Retained inline mesh geometry
Immutable u32 resources share shadow/color buffers and survive independent draw-state changes
Engine ๐ก ยท Editor ๐ก ยท Decay โ ยท Proof ๐ก โ parity row Retained inline mesh geometry
Tested by: crates/sindri-core/src/component/decoded.rs, crates/sindri-scene/tests/extraction/retained_meshes.rs, crates/sindri-gpu/tests/standard_materials/retained.rs, crates/sindri-gpu/tests/standard_materials/normals.rs, game/tests/a_3d_benchmark_project_plays.rs
Exercised in: examples/benchmark3d, examples/lighting3d
Evidence
Scene extraction reuses immutable geometry until its positions, UVs, normals, indices or atlas rectangle change. Per-renderer weak caches upload once across shadow/color draws and retire unused resources; older frames own their revision. Checked edits/undo, lifecycle, real u32 drawing and shared shading have regressions. Native/editor benchmark reports record zero settled geometry uploads with unchanged draws/passes. Release browser exports exercise real u32 geometry, shadows, smooth normals and live lighting. This is feature proof for the planned Explorer/Low Tide 3D track; in-tree game adoption, instancing, deduplication and culling remain open.
Authored material sidedness
Inherit, single and double face visibility share color/shadow policy across world geometry
Engine ๐ก ยท Editor ๐ก ยท Decay โ ยท Proof ๐ก โ parity row Authored material sidedness
Tested by: crates/sindri-gpu/tests/standard_materials/sidedness.rs, crates/sindri-gpu/tests/imported_models/sidedness.rs, crates/sindri-scene/tests/extraction/material.rs, crates/sindri-scene/tests/extraction/voxel_world.rs, game/tests/a_3d_lighting_project_plays.rs
Exercised in: examples/lighting3d
See: docs/rendering-materials.md
Evidence
The material component carries a checked sidedness choice independently of numeric factors. Inherit preserves imported primitive settings; other geometry is single-sided. Explicit single/double choices govern both color and shadows, including reflected transforms and lighting on visible backs. Policy edits retain geometry and voxel meshes. Lighting Lab carries a back-facing orange panel and a single-sided native/WebGPU control. Shared texture bindings, alpha unification and Explorer game adoption remain open.
Shared render material profiles
Typed profile assets share numeric material factors and face visibility with explicit local overrides
Engine ๐ก ยท Editor ๐ก ยท Decay โ ยท Proof ๐ก โ parity row Shared render material profiles
Tested by: crates/sindri-scene/src/render_material/tests.rs, crates/sindri-scene/tests/extraction/material_profiles.rs, crates/sindri-scene/tests/extraction/voxel_world.rs, crates/sindri-export/tests/render_materials.rs, editor/src/scripts/tests.rs, editor/src/profile.rs, game/tests/a_3d_lighting_project_plays.rs
Exercised in: examples/lighting3d
See: docs/rendering-materials.md, docs/profiles.md
Evidence
Engine-owned render_material profiles carry checked numeric factors and sidedness. References use the existing asynchronous profile pipeline and export dependency walk, including inactive entities, later scenes and expanded prefabs. Explicit local overrides are independent of profile values. Invalid editor reloads retain last-valid values; extraction snapshots values without replacing geometry. Lighting Lab shares three profiles across six objects and carries inline-equivalence and shared-edit controls, exercised in native and real Chromium/WebGPU captures. Native editor review covers creation, selection/history, validated Save/correction, reload, reopen and Play. Material texture ownership, unified alpha and in-tree game adoption remain open.
3D rendering
Imported-model occlusion silhouettes
Optional muted model silhouettes keep Causeway units visible behind elevated terrain.
Engine โ ยท Editor โ ยท Decay โ ยท Proof โ โ parity row Imported-model occlusion silhouettes
Tested by: crates/sindri-scene/tests/extraction/models.rs, crates/sindri-gpu/tests/imported_models/occlusion.rs
Exercised in: game
Evidence
sindri.model.occluded_color opts into a depth-tested flat RGBA pass. Opaque
non-subject geometry fills depth first, then silhouettes, then normally lit
subjects. Shared geometry, material facing and cutouts are preserved. No
subject self-occlusion; opted-in subjects do not reveal each other. Filled
silhouettes may accumulate alpha where primitives overlap; transparency can
cover them. JSON authoring only; editor color interaction is not proven.