ECS for Control — Outlook
Where the ECS-for-control evaluation goes next: declaring the attributions, deriving the schedule, and the store as one object.
Overview
The concepts document sets out the architecture; the variants exercise a slice of it in working code; the lessons record what building them taught. This document looks forward. It starts from the parts the evaluation deliberately left unmechanised, and sketches the work that would close them: a further round of evaluation programs, and a follow-up to the concepts document once the open questions have answers.
What the Evaluation Left Open
The programs are faithful to the concepts, with the machine-readable half of the story stated but not built. Several lessons end at the same edge:
-
the producer and consumer roles are annotations – comments in each variant's
Componentsmodule – not declarations a tool can read; -
the schedule is hand-written: the Systems run in the order they are registered in
addSystems, an order the programmer works out and maintains by hand; -
access rests on discipline: the alias workaround stands in for a checked write path, so the declared roles are enforced by review, not by the compiler;
-
the storage model is the simplest thing that worked – one
POINTER TO RECORDper Component, allocated singly – chosen to get the concepts running, not settled on its merits.
These are not defects to patch one by one. They turn out to be one problem seen from four sides, and the next round addresses them together.
Deriving the Schedule
Every addSystems body in the variants carries the same comment: that this order will one day be derived, not written. The information to derive it already exists – each Component type has a single producer and its consumers – but only as prose. The first move is to make those roles declarations the machine can read: each System states the Component types it produces and consumes, in a form the compiler and the kernel can check.
Once the roles are declared rather than described, three things the concepts document promised become real, all before the first tick:
-
the run order is derived from the producer→consumer graph – a topological sort of the Systems over the Components they share;
-
a second producer for a Component type, or a dependency cycle, is rejected at construction, not left to review;
-
a Component with no consumer is flagged – the anti-cruft check the two-sided definition test of Control System and Controlled System asks for, turned from a manual audit into a build-time one.
One design point settled early, and it is worth stating because it is easy to get wrong. Deriving order from data means there is no need for a precedence primitive – no "runs-before" token a System could set to force an ordering. Where two Systems must run in a fixed order within a tick, that order already follows from a data edge between them: the producer runs first because the consumer reads what it wrote. Where there is no data edge, there is no order to impose – the Systems are independent, and a delayed edge (consumed a tick later) lets them run in either order, or at different rates. Order that does not follow from data is order without a reason. If an ordering seems necessary and no Component carries it, the missing dependency is the thing to model – reify the edge as a Component the second System consumes – not a scheduling hint to bolt on beside the store.
An Enforceable, Checkpointable Store
How the attributions are best declared turns out to depend on how the store is stored – which is why the schedule and the storage model are one question, not two. Two objectives fix the direction, both left open by the pointer-per-Component store these programs use:
-
Enforceable access. The producer and consumer roles should be checked by the compiler, not upheld by discipline – a System able to write only what it produces and read only what it consumes, with the alias workaround gone. The roles become part of the store's type, not a comment beside it.
-
A checkpointable store. The store's control state should be capturable as a whole – snapshotted, compared, serialised in one operation – so recovery and monitoring get a coherent picture for next to nothing. That asks for the state to be free of internal pointers, held as a value rather than a structure to walk.
Both interact with how the Components are physically stored, which is why the declared attributions, the derived schedule, and the storage model are treated as one coupled step in the next round, not three independent ones. How access is made checkable and the state made pointer-free is the design work of that round; here the objectives are what matter, and enough to set its direction.
How the instances themselves are grouped and addressed – the fixed per-Component arrays of these programs are the simplest choice – is a question in its own right, and the one where cross-cutting Systems come in.
Cross-Cutting Systems and the Capability Query
The concepts document makes a System that cuts across the machine one of the architecture's headline properties – a thermal-limit System over every joint, cutting across the structure without being built into it. That form already works here: a System sweeps the instances of a Component type, whatever entities carry them. What it does not yet reach is the cross-kind case – one System serving entities of different kinds (a motor, a valve, a heater) by a capability they share, without being coded to the list of kinds.
The current store handles that only in a degenerate, hand-built form: each entity kind keeps its fixed Component combination in its own pools, and a System is bound to a pool by construction. To serve several kinds it would have to iterate each kind's pool – coupled to the full enumeration of kinds, and edited whenever one is added. The archetype extension generalises this: each entity carries a mask of the Component types it holds, and a System queries by capability – "every entity whose Components include {X}" – receiving every match across all kinds, and any future kind bearing X, with no edit. The System becomes closed against the set of entity kinds.
Taken in the static form the no-surprise discipline requires – masks fixed at creation, the query matched once before the first tick, no runtime migration – this adds a query capability, not a dynamic structure: runtime still only reads and writes Component values over pre-matched sets. The dynamic, gain-and-lose-Components form that game engines use is excluded, and with it the cache-locality motivation behind it – beside the point at control-system entity counts on an M-class core. The reason to want archetypes here is composability, not speed.
A capability query earns its place wherever a concern cuts across the machine rather than living inside one entity kind – and control systems are full of these. Periodic logging, telemetry, health and diagnostics, operating-mode and mission coordination: each is a System that surveys or drives entities by what they have in common, not by what kind they are. These programs already show the pattern – the report to the terminal surveys the LEDs by a shared property, their reportable state, and emits it busy-wait-free through the adaptation layer. With a single entity kind that survey stays within reach of the fixed pools; add a second reportable kind – a sensor, a mode indicator – and the capability query is what lets the one reporter serve both, and every later kind that is reportable, without being rewritten. Console output, logging, and monitoring are all this same shape: one System over "every entity with capability X".
Recovery is the most demanding member of that family, and the reason the store was chosen in the first place. A fault monitor, a liveness checker, a recovery coordinator are cross-cutting by nature – they survey the system across kinds. The store already holds the whole state at a tick boundary, a coherent snapshot by construction; the capability query is what makes that global view addressable, letting a recovery System ask directly for "every faulted entity", "every safety-critical entity", "every entity gone stale", and act on the answer whatever the kinds. That is where the Motivation's recoverable-state picture and the machinery to act on it meet, and where this evaluation joins the framework's error-handling work.
Proportionately, as ever: fixed pools stay adequate while the entity kinds are few and no System cuts across them; the query earns its place with the first that does – often a reporter or a monitor well before any fault handler.
Toward a Framework
These remain evaluation programs, not a framework: implemented to work, duplicated per variant, their sizes hard-coded. Turning the settled parts into reusable pieces is a further step, and it is gated on questions still open – where program-specific sizes (buffers, rings) should live, the storage model above, and the declared-attribution mechanism.
The intent is to extend the RTK framework, not to replace it: ECS as an additional way to build a program on the existing base, offered where its guarantees – a recoverable store, a derived schedule – are worth having, and left aside where they are not. Recovery itself, the motivation the architecture was chosen for, comes later still: it needs the checkpointable store above, and it belongs with the framework's error-handling work rather than with this evaluation.
The Next Artefacts
Two things follow directly:
-
a further set of evaluation programs – a sibling to
EcsControlBase– exercising the declared attributions, the derived schedule, the value-section store, and a first cross-cutting System keyed on a capability across entity kinds, together with the checks that prove them: a second producer rejected, a cycle caught, a dead Component flagged, each shown by perturbing a working program; -
a follow-up to the concepts document: the parts it currently marks described but not yet mechanised move to the past tense, and the lessons' open storage question gets its answer.
Beyond these lie the larger themes the concepts document raises but the programs have not touched – partitioning across cores and rates, and recovery built on the store. Those are the horizon, not the next step.
Last updated: 13 July 2026