Getting Started
This tutorial climbs the tess concept ladder in the order the pieces
compose: shapes, schemas, worlds, writes, pathfinding, topology, the
schedule loop, and the render bridge. Each stage links the maintained
architecture note and a runnable example. Every example is a
self-checking binary built by the examples and dev presets (see the
contributor guide).
The pieces form one dirty-driven data flow. Callers own the queue, schedule, agent storage, and presentation state; tess connects those objects without owning an engine loop.
API used
Worlds: tess::Shape,
tess::FieldSchema, tess::World.
Composition: tess::OperationBatch,
tess::astar_path,
tess::RegionGraphT,
tess::Schedule, and
tess::DeltaFrame.
flowchart TB
accTitle: End-to-end simulation data flow
accDescr: Queued edits update the world and dirty metadata, which drives topology, paths, movement, and versioned render deltas.
Caller["Game systems"] --> Queue["OperationBatch"]
Queue --> Exec["Plan and execute"]
Exec --> World["World fields"]
Exec --> Dirty["Dirty masks, bounds, and content versions"]
Dirty --> Topology["OnDirty topology task"]
Dirty --> Paths["Path-agent replanning"]
Topology --> Paths
Paths --> Move["Validate and commit movement"]
Move --> World
Move --> Dirty
Dirty --> Collector["DeltaCollector"]
Collector --> Frame["Versioned DeltaFrame"]
Frame --> Renderer["Consumer-owned presentation state"]
Consume the library per the installation guide. CMake users can
use an installed package, FetchContent, or add_subdirectory and link
tess::tess; compiler-only consumers can extract the portable headers asset
and add its include directory. Then include the pathfinding facade:
1. Shape: the compile-time world model
A tess::Shape fixes the world and chunk dimensions at compile time.
Chunk dimensions must be powers of two that evenly divide the world
dimensions; both are tess::Extent3 values.
One model covers 2D (z defaults to 1), vertical cross-sections (y = 1),
and full 3D - degenerate axes cost nothing. Tess stores canonical signed
tess::Coord3 coordinates; ordinary top-down APIs also accept a
tess::Coord2 and lift it to the z = 0 plane.
- Architecture:
architecture/shape.md - Example:
examples/ant_farm_vertical.cc(a degenerate-axis x-z world)
2. FieldSchema: what each tile stores
Fields are declared with empty tag types plus a stored value type, and
collected into a tess::FieldSchema. Tags are type-level names: they
never exist at runtime.
struct PassableTag {};
struct CostTag {};
struct ConstructionTag {};
using Schema = tess::FieldSchema<tess::Field<PassableTag, std::uint8_t>,
tess::Field<CostTag, std::uint32_t>,
tess::Field<ConstructionTag, std::uint8_t>>;
Storage is struct-of-arrays per chunk: each field is a contiguous span per chunk page, which is what the block kernels and path queries iterate.
- Architecture:
architecture/storage.md
3. World: residency policies
A world binds a shape and schema to a residency policy:
using World = tess::AlwaysResidentWorld<Shape, Schema>;
using WeightedMovement =
tess::movement::PositiveCostFieldMovement<PassableTag, CostTag>;
AlwaysResidentWorld keeps every chunk allocated - the simplest choice
and the right default for small or dense worlds. SparseResidentWorld
(see tess/storage/sparse_world.h) materializes chunks on demand under
a byte-budgeted residency manager for large or mostly-empty worlds.
Construct the world inside the application lifecycle so allocation failures
can be reported at its error boundary. Zero-initialized fields mean a fresh
world is fully impassable for the identity movement class below: mark
tiles passable before pathing.
- Architecture:
architecture/storage.md - Example:
examples/sparse_stream.cc(budget-bounded residency and theIndeterminatestream-and-retry flow)
4. Writing tiles: direct access vs queued operations
For setup and single-threaded code, write fields directly:
For bulk setup in a dense world, world.fill_field<PassableTag>(1) assigns
one value to every tile. Like direct field() writes, it does not implicitly
mark chunks dirty or advance their content versions. Simulation-time edits
should instead go through queued operations: a
tess::OperationBatch collects declared edits (domain, touched fields, dirty
mask, write policy), tess::plan_operations validates them into a
conflict-checked plan, and tess::execute_plan runs the writes through
chunk views. The declared write policy (for example
WritePolicy::UniquePerChunk) is what later licenses parallel
execution, and the dirty mask is what drives incremental topology
updates and render deltas downstream.
Queued operations also report back: result channels
(tess/ops/result_channel.h) give each system deterministic, typed
per-operation completion records, drained once per frame.
- Architecture:
architecture/queued-operations.md - Example:
examples/queued_path.cc(the smallest end-to-end queued edit plus A* query)
5. Pathfinding: A*, movement classes, weighted routing
The basic query needs only a passability field and reusable scratch:
tess::PathScratch scratch;
const auto result = tess::astar_path<World, PassableTag>(
world, tess::PathRequest{start, goal}, scratch);
Check result.status == tess::PathStatus::Found; result.cost is the
step count and result.path is a tess::PathView - a non-owning span
of Coord3 that borrows scratch and is invalidated by the next query
that reuses it.
Richer rules live in movement classes, which combine passability predicates and cost sources over schema fields:
using Walker = tess::movement::MovementClass<
tess::movement::AllOf<
tess::movement::Field<PassableTag>,
tess::movement::Not<tess::movement::Field<ConstructionTag>>>,
tess::movement::FieldCost<CostTag>>;
tess::weighted_astar_path consumes cost fields. When many agents path
at once, pick by workload shape: agents sharing a goal set on unit-cost
terrain reuse one distance-field product (see
tess/path/field_product_cache.h), weighted per-tick batches amortize
repeated goals through tess::weighted_path_batch (all-distinct goals
fall back to per-request A*), and repeated identical routes on an unchanged
map are served by the route cache via tess::cached_astar_path. The
pathfinding note maps each workload shape to its
API.
- Architecture:
architecture/path.md - Example:
examples/path_agents.cc(a multi-agent tick loop with replanning)
6. Topology: the region graph and the precheck
A per-movement-class region graph summarizes connectivity so that definitively unreachable queries are rejected without expanding the grid:
tess::LocalTopologyScratch scratch;
tess::RegionGraph graph;
tess::build_region_graph<World, Walker>(world, scratch, graph);
const auto verdict = tess::precheck_path<Walker>(
graph, world, tess::PathRequest{start, goal}, precheck_scratch);
The class (or tag) given to precheck_path must match the one the graph
was built for; a mismatch reports GraphStale and falls back to A*.
Only PrecheckStatus::Unreachable proves failure; every other verdict
means "inconclusive, run A*", so the precheck can never turn a solvable
query into a wrong failure. After edits, tess::update_region_graph
refreshes only the dirty chunks. Transition providers such as
tess::StairTransitions extend a class's connectivity across z-levels.
- Architecture:
architecture/topology.md - Example:
examples/stairs_3d.cc(two z-levels joined by a stair, with the precheck agreeing before and after demolition)
7. The Schedule: composing a frame
The scheduling and render layers use the broader simulation facade:
tess::Schedule runs tasks in fixed phases (PreUpdate, Topology,
Movement, ...) with per-task cadences: every tick, every N ticks, or
Cadence::on_dirty(mask) to run exactly when matching edits landed.
tess::AutoExecTask wraps the queued-operation pipeline (plan, execute,
ack results) as a schedule task, and tess::run_schedule_frame drives
the whole thing under a fixed-step clock:
tess::Schedule schedule;
schedule.add_task(
{"build", tess::SimPhase::PreUpdate, tess::Cadence::every_tick()},
build_task);
schedule.add_task({"topology", tess::SimPhase::Topology,
tess::Cadence::on_dirty(kTerrainDirty)},
topology_task);
schedule.seal();
tess::SimClock clock;
tess::FixedStepAccumulator accumulator(20, 8);
tess::run_schedule_frame(schedule, clock, accumulator, 1.0 / 20.0,
tess::SimTimeControl{tess::SimSpeed::Speed1x});
Schedule tasks themselves run serially; the selectable parallel phase
executor (see tess/ops/phase_executor.h) parallelizes the planned,
write-policy-compatible queued operations a task submits. The worker pool
is the production parallel backend; the scoped-thread executor is a
address-stable per-dispatch alternative. Whether the pool pays off depends on how much
work each chunk does — see
performance, which
publishes measured four-worker figures and the crossover below which the
pool loses. Declaring an honest WritePolicy on each operation is what
licenses parallel execution, with no changes to operation code.
- Architecture:
architecture/simulation.md - Example:
examples/colony_2d.cc(the flagship composition: queued construction, OnDirty topology rebuild, movement-class agents, and render deltas in one loop)
8. The render bridge: versioned DeltaFrames
Render consumers never walk the world. A tess::DeltaCollector gathers
dirty-driven tile deltas and publishes immutable, versioned
DeltaFrames that a consumer applies to its own shadow state:
Frame versions let a consumer detect gaps and request resynchronization.
The frame borrows collector storage: its spans are valid until the next
publish() or reserve(), so apply it or copy what you need before
publishing again. The header is a value and stays valid regardless.
- Architecture:
architecture/simulation.md - Archived design: render delta presentation bridge TDD
- Example:
examples/render_delta_consumer.cc(a standalone consumer rebuilding a shadow grid)
Where next
- The decision guide — once the concepts are familiar and you need to choose between residency policies, write paths, and path strategies for a real workload.
- ECS integration by concepts, with independently gated EnTT and Flecs
adapters and a deliberately custom micro-ECS example:
architecture/ecs.md,examples/custom_ecs_min.cc,examples/entt_pawns.cc,examples/flecs_pawns.cc. - Compile-gated diagnostics, tracing, and the ImGui panels:
architecture/diagnostics.md. - Benchmarks, thresholds, and the trend snapshot:
performance.md.