Simulation Integration MVP
The current simulation integration layer lives under include/tess/sim/ and
is exported by tess/tess.h. It provides the first colony-sim-facing bridge
over storage, queued operations, path requests, movement validation, and render
deltas.
Public Surface
Movement
MovementIntentrecords one adjacent tile move fromfromtotoplus an optionalMovementVersionCheck.MovementVersionCheckcarries optional expectedfrom/tochunk versions andfrom/totopology versions. Unset fields are not checked.MovementStatusreportsMoved, invalid endpoints (InvalidFrom,InvalidTo,NotAdjacent), blocked endpoints (BlockedFrom,BlockedTo),OccupiedorReserveddestinations, and staleStaleVersion/StaleTopologyversion guards.MovementResultreturns the status plus the echoedfrom/tocoordinates.is_transient_movement_failure(status)classifies failures: blocked, occupied, reserved, and stale statuses are transient (the world can legitimately change under a routed agent; re-path and retry), while invalid endpoints and non-adjacent steps indicate a caller bug and are terminal.MovementFailureCountsaggregates failures intoinvalid,blocked,occupied,reserved,stale_version, andstale_topologybuckets;record_movement_failure(counts, status)maps each non-Movedstatus into its bucket.movement_versions_match(world, intent)checks only the optional version guards (chunk versions first, then topology versions) and returnsMovedwhen every set guard matches. It resolves both endpoints unchecked, so callers must validate coordinates first.validate_movement_intent<World, ClassOrTag, OccupancyTag, ReservationTag>(world, intent)checks shape bounds, six-axis adjacency (manhattan_distance == 1), passability of both endpoints, destination occupancy, destination reservation, and the optional version guards, in that order, without mutating the world. The second template argument is a movement class OR a raw passable tag, normalized exactly as inastar_path, so plan and commit share one vocabulary: every step A* accepted for a class validates for that same class. Validation checks the class's PASSABILITY predicate only -- entry cost is a search concern, and commit staying more permissive than the weighted search is the deliberate legacy asymmetry (a cost field dropping to zero after planning blocks re-planning, not an already-planned adjacent step). Classes wanting cost folded into commit passability too should useWalkableCostField, whose predicate already includesNotZero<CostTag>. The from- and to-tiles may live on different pages; each endpoint's predicate is evaluated on its own resolved page.commit_movement_intent<World, ClassOrTag, OccupancyTag, ReservationTag>( world, intent, dirty_mask)validates the same intent, clears source occupancy, sets destination occupancy, clears destination reservation, and marks source and destination tiles dirty whendirty_maskis nonzero.
Render Deltas
RenderTileDeltarecords a changed tile coordinate, chunk key, local tile id, matching dirty flags, and chunk version.collect_render_tile_deltas(world, dirty_mask, out)appends one delta per dirty tile in each matching chunk dirty bound. On a dense world it scans every chunk; on a sparse world it scans only the resident set (a non-resident chunk holds no data and cannot be dirty, so this misses no delta and never reads a non-resident slot or runs a fullchunk_countscan).render_tile_deltas(world, dirty_mask)returns an owning vector of render deltas for simple consumers.clear_render_delta_dirty(world, dirty_mask)clears the render-relevant dirty bits after a presentation layer has consumed them; it iterates the resident set on a sparse world.
The RenderTileDelta family above is the legacy per-tile seam; new
consumers should use the versioned DeltaFrame bridge below.
DeltaFrame Render Bridge (M11)
The versioned frame protocol in sim/delta_frame.h. Tile deltas are
invalidation records, not value payloads: the consumer re-reads the
current world for covered tiles at apply time, which is idempotent and
convergent. Chunk dirty metadata is already a cross-tick coalescer
(flags OR, bounds union), so tiles are collected once per published
frame through the lost-update-safe observe/clear-observed protocol.
stateDiagram-v2
accTitle: DeltaFrame application and resynchronization
accDescr: A consumer starts from a complete baseline, follows a continuous version chain, and returns to baseline recovery after a gap or truncation.
[*] --> Uninitialized
Uninitialized --> Synced: complete baseline, adopt to_version
Uninitialized --> NeedsBaseline: delta or truncated frame
Synced --> NeedsBaseline: version gap or truncation
NeedsBaseline --> Synced: complete baseline, adopt to_version
note right of Synced: Apply matching deltas and adopt to_version
note right of NeedsBaseline: Reject deltas until a complete baseline arrives
RenderVersionis the monotonic frame-chain version. Collectors start at 1; value 0 is reserved for a consumer that has never applied a frame, so a fresh consumer can only start from a baseline.TileChunkDeltais one chunk's record: matching dirty flags, the chunk-clipped bounds, and eithertile_countper-tile entries starting atfirst_tileinframe.tiles, ortile_count == 0meaning box-granular (repaint every tile inbounds).chunk_versionis debugging only -- clears do not bump it and sparse reloads reset it. Chunk records are the only entry point; consumers never iterateframe.tilesdirectly.TileDeltais one changed tile (coordinate, local tile id, flags).EntityDeltaKind/EntityDeltarecord entity motion and lifecycle:Moved(coalescible), and the barriersTeleported,Spawned,Despawned,Parked,Placed.from == tofor spawns/places; parks and despawns carry the released tile.last_tickstamps the last coalesced commit so renderers can tell moved-this-frame from resting. Coalesced records are not a serializable per-record sequence: a coalesced move sits at its first commit's position, so consumers key presentation by entity and check tile exclusivity only at frame end.DeltaFrameHeadercarriesfrom_version/to_version(equal on empty frames; +1 on state-carrying ones), the folded tick range and count, the uniondirty_mask, and thebaseline/truncatedflags. Truncation (capacity overflow or a hardclear()) is a structural gap: entity loss is unrecoverable by the version chain.DeltaFrameis an immutable view into collector-owned storage, valid until the next mutating collector call;empty()ignores overlays.delta_frame_applicable(header, consumer)is the consumer's apply gate: truncated frames never apply -- not even baselines, because a baseline that overflowed chunk storage covers only part of the world (size baseline consumers' chunk capacity to the whole world); un-truncated baselines always apply (adoptto_version, re-snapshot entity presentation); otherwise the chain must match exactly withconsumer.value != 0.PathOverlayDeltais one agent's remaining route this frame (frame.overlay_nodes[first_node .. first_node + node_count)). Overlays are stateless, full-replacement decorations: every applied frame replaces the consumer's whole overlay set (possibly with the empty set), no create/update/remove lifecycle exists, they never affect version semantics orempty(), and overflowing overlay storage drops the overlay (counted inoverlay_truncations), never the frame. Nodes are copies, valid for the frame's lifetime.DeltaCollectorOptionssets the per-chunksparse_tile_threshold(records per-tile up to it, box-granular above; 0 = always box) andcoalesce_moves(fold consecutive moves last-writer-wins; disable for motion-interpolating renderers so each step spans one tile).DeltaCollectorStatscounts published frames, baselines, record kinds, coalesced moves, and truncations, cumulatively.DeltaCollectoraccumulates records and publishes frames:reservesizes every buffer once (steady state never allocates; records past capacity are dropped and flagged, never grown mid-frame);begin_tickstamps subsequent records;record_move/record_teleport/record_spawn/record_despawn/record_park/record_placefeed entity deltas (the ECS pipeline hook and lifecycle intents call these on success only);append_chunk_record/append_tile_record/pending_tile_count/note_collected_mask/mark_baseline_pendingare the collection seams;publish()seals the frame, bumping the version iff it carries state and dropping pending entity records on baselines (consumers re-snapshot entities on every baseline apply);clear()hard-resets pending state and poisons the stream -- the next publish is forced truncated unless it is a baseline, and a world swap isclear()followed by a full baseline collection. The collector must be the sole clearing owner of every dirty bit it collects; shareddirty_boundsacross flag owners only widens boxes (conservative).collect_baseline(collector, world, dirty_mask)is the full-scope resync: one box record covering every chunk (dense) or resident chunk (sparse), pending Dirty records dropped as superseded, the mask's dirty bits plainly cleared, and the pending frame marked baseline (which also drops pending entity records at publish). Scoped baselines deliberately do not exist -- a partial baseline that adopts the frame version would permanently lose out-of-scope invalidations from a gap.collect_path_overlays(collector, runtime, agents, handles[, selection])stages the remaining route (path.suffix(path_index)) of every Following agent, gated onhas_goal && status == Foundbefore touching a ticket (which provably avoids the runtime's stale-ticket assert and the value-zero cleared-ticket alias). Ordering contract: lifecycle intents run before the tick, overlays collect after it; an intent squeezed between tick and collection leaves that agent's overlay one frame stale while entity deltas stay correct.collect_tile_deltas(collector, world, dirty_mask)observes, records, and clears (observed-generation-safe: a racing mark leaves the bits set for a harmless duplicate next frame) every dirty chunk under the mask; dense worlds scan chunk metadata, sparse worlds scan the resident set. Per chunk it emits per-tile records up to the threshold and a clipped box record otherwise, degrading to a box record when tile storage cannot hold a chunk. Sparse residency change records are deferred until a sparse render consumer exists; reloads reset chunk metadata, so such consumers must treat them as baseline triggers.
Path-Agent Batch Helpers
PathAgentStatestores an agent's position, goal,PathTicket, path index, lastPathStatus,PathAgentPhase, active-goal flag, andblocked_retriescount.PathAgentPhaseis the agent lifecycle, decoupled from the lastPathStatus:Idle(no goal or arrived),NeedsPath(goal assigned, no route yet),Following(walking aFoundroute),Blocked(transient failure; re-paths until the retry budget runs out), andUnreachable(terminal until a new goal is assigned).set_path_agent_goal(agent, goal)arms the lifecycle (NeedsPath, retry count reset);clear_path_agent_goal(agent)returns the agent toIdle.PathAgentFrameStatscounts submitted, completed, found, invalid-start, invalid-goal, no-path, and indeterminate results plusprecheck_ruled_out, advanced steps, arrivals, blocked waits, and aMovementFailureCounts.precheck_ruled_outis the number of agents whose goal an optional topology precheck proved unreachable before A* (a subset ofno_path; see the path runtime'sprecheck_ruled_out).add_path_agent_stats(lhs, rhs)accumulates two frames;record_path_agent_status(stats, status)buckets one path result.submit_path_agents(agents, runtime)starts a new runtime request set and submits one request per agent with an active goal, skippingUnreachableagents and clearing agents that already stand on their goal (counted as arrived).apply_path_agent_results(agents, runtime)copies ticketed results back:FoundentersFollowingand resets the retry count; planner failures enterBlockedso the tick driver's retry budget governs them.advance_path_agents(agents, runtime, max_steps)walks agents with aFoundresult up tomax_stepsnodes along runtime-owned paths without touching world fields.advance_path_agents_with_movement<World, ClassOrTag, OccupancyTag, ReservationTag>(world, agents, runtime, max_steps, movement_dirty_mask)commits each step throughcommit_movement_intent(no version guards), validating with the same movement class the plan used. A transient failure leaves theFoundroute intact, moves the agent toBlocked, consumes one retry, and counts a blocked wait; a structural failure is terminalUnreachable. Arrival clears the goal and counts an arrival. An observer overload appendson_commit(agent_index, from, to), invoked once per successful commit (after position/occupancy update, before arrival handling) and never on a failed validation, so external tile-to-entity mirrors updated inside the callback stay synchronized with the occupancy field by construction (the M10 ECS adapter's hook point).process_unit_path_agents<World, ClassOrTag>(...)andprocess_weighted_path_agents<World, Class, MaxCost>(...)(plus the legacy<World, PassableTag, CostTag, MaxCost>overload) run submit, runtime processing (cached unit or weighted batch), and result application as one synchronous pass. Both take an optional trailingconst RegionGraphT<World::residency_type>*(defaultnullptr) that they forward to the runtime's precheck gate; when supplied, goals the region graph proves unreachable are resolved without A* and surfaced inPathAgentFrameStats::precheck_ruled_out.
Path-Agent Tick
SimClockholds the current tick;advance_sim_tick(clock)increments and returns it.PathAgentTickStateowns the clock, the WORLD-scopedpathing_dirtyflag, and the per-agent retained routes (PathAgentRoutes, index-paired with the agents span).mark_pathing_dirty(state)requests a full replan of every agent on the next tick (required after world edits); the three-argumentset_path_agent_goal(state, agent, goal)arms a goal as agent-scoped dirt -- only that agent replans (the drivers submit withPathSubmitScope::NeedsOnly), everyone else keeps their retained route (per-agent pathing dirt; pre-split, one re-arm replanned the whole batch every tick).PathAgentTickOptionscarriesmax_stepsper tick, the runtimePathRuntimeCachePolicy, andmax_blocked_retries(default 8).PathAgentTickStatsreports the tick value, whether paths were processed, separate pathing and movementPathAgentFrameStats, and therepaths_requested/repath_exhaustedcounts.prepare_path_agent_processing(agents, options, stats)scans agents ahead of path processing:NeedsPathagents (goals assigned through eitherset_path_agent_goaloverload) request processing with no manual dirty mark, andBlockedagents consume one re-path attempt per processed tick until the retry budget runs out, at which point they turn terminallyUnreachablewithPathStatus::NoPath.tick_unit_path_agents<World, ClassOrTag>(...),tick_weighted_path_agents<World, Class, MaxCost>(...),tick_unit_path_agents_with_movement<World, ClassOrTag, OccupancyTag, ReservationTag>(...), andtick_weighted_path_agents_with_movement<...>(the weighted forms keep their legacy<World, PassableTag, CostTag, MaxCost[, ...]>overloads) advance the clock, re-process paths whenpathing_dirtyis set or any agent requested processing, then advance agents — either freely or through movement commits with the suppliedmovement_dirty_mask. In the class forms one movement class drives pathing, the precheck, and commit validation, so plan and commit provably agree per class. Each accepts an optional trailingconst RegionGraphT<World::residency_type>*(defaultnullptr) forwarded to the runtime precheck gate, so a caller that maintains a region graph can skip A* for goals proven unreachable.
Schedule
include/tess/sim/schedule.h is the M5 schedule: ordered phases of
type-erased tasks driven by cadences that are pure functions of the fixed
SimClock tick counter and per-task pending dirty masks. The schedule never
touches a world -- dirty bits are FED to it by task results and
notify_dirty -- so the no-hidden-full-world-scans rule holds by
construction. Type erasure is a function pointer plus a context pointer;
world-typed work lives in task objects the caller owns and registers by
reference. ScheduleTaskFn is the raw erased function-pointer form for callers
that do not use the object-reference overload.
flowchart TB
accTitle: Fixed schedule phase order
accDescr: Every simulation tick traverses the complete phase list in declaration order and tasks retain registration order within a phase.
Early["Input → PreUpdate → AI"]
Agent["Pathing → Movement → Commit"]
Derived["Topology → Fields → Background"]
Output["RenderDelta → Diagnostics"]
Early --> Agent --> Derived --> Output
Dirty results are merged immediately into every matching OnDirty task.
flowchart TB
accTitle: Same-tick and next-tick dirty propagation
accDescr: A later matching task consumes dirty bits in the current tick, while an already-run task consumes them on the next tick.
Produced["Task returns dirty_mask"] --> Remaining{"Target phase<br/>still ahead?"}
Remaining -->|Yes| SameTick["Run later OnDirty task<br/>this tick"]
Remaining -->|No| NextTick["Run matching task<br/>next tick"]
SimPhaseis the fixed phase list, executed in declaration order each tick; tasks run in registration order within a phase.SimClock(hoisted intotime.h; the path-agent tick shares it) is the authoritative fixed-tick counter every cadence derives from.Cadenceselectsevery_tick(),every_ticks(n)(exact: the countdown advances once perrun_tick, even while the task is disabled, so re-enabling never shifts the lockstep phase; a due-while-disabled tick is counted as skipped),on_dirty(mask)(fires iff bits of the task's OWN mask are pending; firing consumes only those bits, so producers' same-tick marks re-arm it for the next tick),background(budget), andmanual().CadenceKindis the stored discriminator for those five cadence forms.ScheduleTaskDesccombines a phase and cadence;ScheduleTaskContextsupplies the current clock and background budget toScheduleTaskFn;ScheduleTaskResultreturns dirty bits, completed items, and backlog state; andScheduleTaskStatsexposes cumulative run, skip, and item counts.BackgroundBudgetis deliberately items-only: a due background task is offeredmax_itemsunits per run and reportsitems_doneplusmore_workto continue next tick. A wall-clock valve would make tick outcomes nondeterministic; it returns with its first real consumer.Schedule::add_task(desc, task)registers a caller-owned task object (or a raw fn-pointer + context);seal()freezes registration;request_run(id)arms any task for the next tick (the Manual trigger and the Background initial trigger);notify_dirty(mask)merges external dirty bits (frame-owner thread only; never from an op callback -- worker-side dirty flows exclusively through the task-result mask);run_tick(clock)advances the clock and dispatches, returningScheduleTickStats;task_stats(id)reports per-task counters.- A task result's
dirty_maskmerges into every task's pending mask immediately: later-phase OnDirty tasks fire in the SAME tick, earlier-phase tasks the next tick. - Allocation contract:
reserve_tasks+ registration happen at setup;run_tick,notify_dirty, andrequest_runnever allocate afterseal()(pinned by test). run_schedule_frame(schedule, clock, accumulator, real_delta_seconds, control)is the frame-to-ticks bridge: it consumes real frame time through theFixedStepAccumulator(honoringSimSpeedand the per-frame tick cap) and runs the schedule once per granted fixed tick, returning aScheduleFrameSummary(ticks, alpha, dropped seconds, last tick's stats). Cadences therefore count FIXED TICKS, never frames: an EveryN task at 4x fires four times as often in real time and exactly as often in sim time, and a backlogged frame advances every cadence through each granted tick.
Auto-Exec
include/tess/sim/auto_exec.h closes M5's auto-exec gap: AutoExecTask
<World, Policy, Ack, ChunkFn> is one schedule task running the whole
queued-ops pipeline -- plan, parallel phase planning, execution (serial or
worker pool, chosen per phase by an operation-count threshold), per-phase
dirty apply, and ack drain -- over a caller-owned FrameOps queue. Both the
queue and the task's result channel are cleared together at the end of every
successful run (the paired-clear discipline), and the run's dirty_mask union
feeds the schedule so OnDirty tasks in later phases fire the same tick. A
planning or kernel exception preserves the caller-owned queue for inspection
or replacement while the exception path clears transient result slots, so old
completions cannot leak into a later run. Earlier writes may already have
executed, so blindly retrying that queue is unsafe; chunk callbacks should not
throw.
- Policy uniformity is PRE-VALIDATED (
AutoExecStatus::PolicyMismatchexecutes nothing; asserted in debug), which makes runtime aborts unreachable -- serial and pool execution therefore can never diverge on partially-applied plans, and the serial == pool golden compares whole worlds, chunk metadata, and drained ack sequences byte-for-byte. - Dirty records are merged after EACH phase: the partitioned scratch is re-prepared per phase, so a single post-loop merge would silently drop every phase's dirty but the last (pinned by a write-then-read phase-split test).
Policymust be ReadOnly or UniquePerChunk (the parallel phase planner's set) and the world dense (merge_planned_dirtyis AlwaysResident-only).- Planning reuses a task-owned
ExecutionReport, and the result channel and phase scratch retain capacity. After callers reserve or warm those buffers, the synchronous planning/execution path is allocation-free for payload types whose default construction and assignment are allocation-free. - Result hooks have a
noexceptfunction-pointer contract. The queue is cleared before draining, so follow-up operations enqueued by a hook survive for the next run. AutoExecRunStats(last_run()) reports status, planned/rejected ops, executed chunks, merged dirty chunks, drained acks, and phase/pool-phase counts between ticks.
Scheduler
SimSchedulerStateowns the scheduler-adjacent state currently needed by the path-agent tick layer.SimSchedulerOptionsconfigures which dirty masks should trigger path replanning, which dirty masks should produce render deltas, path-agent tick options, whether render dirty bits should be cleared after collection, and which movement commits should mark dirty tiles.SimSchedulerStatsreports one tick: the tick value, whether operations were planned (planned_ops) and executed (executed_ops), the queuedExecutionReportandPlannedExecutionResult, the variant'sPathAgentTickStats, and the number of render deltas appended this tick (render_delta_count).run_queued_operations<World, Policy>(world, ops, fn)is the shared plan-then-execute step: it plans the frame's operations, returns without executing when validation fails, and otherwise executes the plan through the serial block bridge and reports whether execution completed.tick_unit_scheduler<World, PassableTag, Policy>(...)executes planned queued operations through the existing serial block bridge, marks pathing dirty when planned work dirtied configured pathing fields, ticks unit-cost path agents, and emits render deltas.tick_unit_movement_scheduler<World, PassableTag, OccupancyTag, ReservationTag, Policy>(...)runs the same sequence and commits agent movement throughcommit_movement_intent, marking moved-agent chunks dirty with the configured movement dirty mask.tick_weighted_scheduler<World, PassableTag, CostTag, MaxCost, Policy>(...)runs the same sequence through the weighted path-agent batch tick.tick_weighted_movement_scheduler<World, PassableTag, CostTag, MaxCost, OccupancyTag, ReservationTag, Policy>(...)combines the weighted batch tick with movement commits and the movement dirty mask.
All four scheduler variants share one internal tick sequence
(detail::tick_scheduler_core); they differ only in the path-agent tick
they run. When queued operations fail planning, the tick reports
planned_ops without executed_ops, leaves the world untouched, and still
ticks path agents.
Fixed-Step Time
SimSpeedisPaused,Speed1x,Speed2x, orSpeed4x;SimTimeControlcarries the current speed.sim_speed_multiplier(speed)returns the integer multiplier (0/1/2/4) andeffective_tps(base_tps, speed)returns the multiplied tick rate, saturating at thestd::uint32_tmaximum.FixedStepAccumulator(base_tps, max_ticks_per_frame)converts variable real frame deltas into whole simulation ticks.consume(delta, control)banks speed-scaled time (paused, zero-tps, and zero-cap configurations produce no ticks; NaN and negative deltas contribute nothing) and returns aFixedStepFrame.FixedStepFramereports theticksto run this frame, the interpolationalpha(fraction of one step still banked, clamped to [0, 1]), anddropped_seconds— sim-time seconds discarded because the frame hitmax_ticks_per_framewith more than one step of backlog remaining. When the tick cap is hit, backlog beyond one step is dropped instead of banked: retained debt would force max-tick catch-up frames or an unrecoverable spiral, while one step of carry preserves alpha continuity. Sim time slows instead, and a nonzerodropped_secondsmeans the simulation is running behind real time.
The scheduler does not consume FixedStepAccumulator itself; callers use
it to decide how many scheduler ticks to run in one rendered frame.
Behavior
The scheduler is deterministic and synchronous. It does not own worker threads, async handles, or an event loop. Callers still own entity storage, game-specific job logic, AI decisions, UI state, and content rules.
The intended per-frame order for current consumers is:
- Enqueue field edits in
FrameOpswith accurateFieldAccessDescmasks. - Call a scheduler tick with a callback that applies each planned chunk view.
- Let the scheduler mark pathing dirty when executed operations dirtied configured movement-relevant fields.
- Let the path-agent tick submit/process active requests only when dirty.
- Consume render deltas from dirty chunk bounds instead of full snapshots.
- Commit accepted movement intents through
commit_movement_intentwhen a game system needs occupancy and reservation validation.
The PathAgentPhase lifecycle ties the layers together. Assigning a goal
arms NeedsPath, which requests path processing on the next tick even
without a world edit. Planner failures and transient movement failures both
land in Blocked; each processed tick consumes one of
max_blocked_retries until the agent either finds a route (Following,
retries reset) or exhausts the budget and turns terminally Unreachable.
Structural movement failures (invalid endpoints, non-adjacent steps) skip
the retry budget entirely. Only a new goal re-arms an Unreachable agent.
Clearing a goal returns any active lifecycle state to Idle; those equivalent
edges are omitted from the diagram to keep the failure paths legible.
stateDiagram-v2
accTitle: Path-agent lifecycle
accDescr: Goals arm pathfinding; transient failures retry through Blocked, while structural failures or exhausted retries remain Unreachable.
[*] --> Idle
Idle --> NeedsPath: assign goal
NeedsPath --> Following: path found
NeedsPath --> Blocked: planning fails
Following --> Idle: arrive
Following --> Blocked: transient move failure
Following --> Unreachable: structural move failure
Blocked --> Following: retry finds path
Blocked --> Unreachable: retry budget exhausted
Unreachable --> NeedsPath: assign new goal
MovementIntent version guards are opt-in. They are useful when an external
system collected path or move intents before queued world edits were applied.
If a stored expected chunk version or topology version no longer matches, the
move fails with StaleVersion or StaleTopology before occupancy changes are
committed. The scheduler's own movement ticks submit intents without version
guards; their steps are validated against live world state instead.
Render deltas are based on current chunk dirty bounds. If multiple dirty masks share a chunk, the current dirty bound is the union maintained by storage. A caller that needs per-field exact rectangles should keep its own field-level presentation data or drain deltas before broadening the chunk dirty bound with unrelated edits. Collection clips each chunk's dirty bounds to the chunk's own world-space box before visiting tiles, so bounds that span chunk borders or leave the shape emit deltas only for tiles the chunk owns.
Deliberate Limits
This slice is still a synchronous MVP. It does not implement async execution, worker scheduling, persistent queued kernels, result channels, ECS storage adapters, local avoidance, multi-agent collision resolution, permission layers, doors, vertical transition policies, topology-aware route planning, or region-selective path cache invalidation.
Movement validation currently uses a boolean-like passability field plus boolean-like occupancy and reservation fields. Weighted terrain remains part of path selection, not movement commit validation. Games with doors, factions, construction phases, vehicles, or multi-tile entities should layer those rules around this narrow helper until the movement vocabulary is expanded.