Skip to content

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.

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:

#include <tess/pathfinding.h>

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.

using Shape = tess::Shape<tess::Extent3{32, 32}, tess::Extent3{8, 8}>;

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.

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 the Indeterminate stream-and-retry flow)

4. Writing tiles: direct access vs queued operations

For setup and single-threaded code, write fields directly:

world.field<PassableTag>(tess::Coord2{4, 2}) = 1;

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.

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:

#include <tess/simulation.h>

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:

tess::collect_tile_deltas(deltas, world, kTerrainDirty);
const auto frame = deltas.publish();

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.

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.