DOTS//CORE local · not synced

m06 · Transforms · reading · 10 min

The hierarchy — when entity data stops being independent

The property you are about to give up

Five modules have rested on one quiet assumption, and this lesson is where it breaks.

Module 1 said entities in a chunk are independent: entity 7’s data does not depend on entity 6’s. Module 4 turned that into parallelism — if entities are independent, a job can process them in any order, on any core, and the result is identical. Order-independence is why chunk iteration parallelizes at all. Every performance claim in this course so far inherits from it.

A hierarchy destroys that property, deliberately and locally, and it is worth being precise about how.

Attach a turret to a tank. The turret’s LocalTransform now means relative to the tank. To know where the turret is in the world you must first know where the tank is in the world. The turret’s world position is no longer a function of the turret’s data alone — it is a function of the turret’s data and its parent’s result.

That is a dependency. And a set of dependencies over entities is a graph.

What the graph forces

Once you accept that framing, the entire transform system’s behaviour follows without needing to be told.

You cannot compute a child before its parent. The child’s world transform literally requires the parent’s world transform as input. So the work has an ordering constraint, and the constraint is the graph’s edges.

The ordering is by depth, not by chunk. All roots can be computed first (they depend on nothing). Then everything at depth 1, which now has its inputs. Then depth 2. This is a breadth-first sweep down the tree, and it’s forced — no clever chunk layout removes it, because the dependency is between entities, not between chunks.

Within a depth level, independence returns. Two entities at depth 1 with different parents don’t depend on each other at all. So each level parallelizes freely; it’s only between levels that you need a barrier. The shape is: pass over the roots, sync, pass over depth 1, sync, pass over depth 2.

cache line · 64 B fetched 24 B useful · 38% of the bandwidth you paid for a depth level as a work unit: entities at the same depth are mutually independent, so the level parallelizes — the barrier is only between levels, one per depth of the tree

That last point is the useful one, and notice it is the same shape as Module 4’s dependency chains: independent work parallelizes, dependent work serializes, and the cost of a dependency is a sync point. A transform hierarchy is a dependency chain expressed in data instead of in job handles. A tree of depth 3 costs you three sequential passes. A tree of depth 12 costs twelve, and each one is a barrier where cores idle waiting for the level above.

Depth is the cost. Not entity count — depth. Ten thousand entities parented flat to one root is three passes’ worth of structure and parallelizes beautifully. Two hundred entities in a chain twelve deep is twelve barriers to move two hundred things. This is the single most actionable fact in the module.

Why parent and child are both stored

The graph must be represented in components, and here the design does something that looks redundant until you price the alternative.

The child carries a Parent component holding its parent’s Entity. That’s the edge, stored once, pointing up. It’s the minimal representation and it is what you author.

But the transform system needs to walk down — roots first, then their children. With only upward links, finding a parent’s children means scanning every entity in the world and testing whether its Parent matches. Per parent. That’s quadratic, per frame, and it’s unacceptable.

So the system also maintains a Child buffer on the parent: a dynamic buffer of the entities beneath it. Now the downward walk is a traversal — start at a root, read its children, recurse — proportional to the number of entities in the tree rather than to the square of the world.

Two representations of one relationship, which means they can disagree, which means something must keep them in sync. That something is a system, and it is the reason parenting is not instantaneous.

The density cost nobody mentions

There is a second cost, and it’s the one Module 1 taught you to look for.

An entity with a parent carries: LocalTransform (32 bytes), LocalToWorld (64 bytes, the composed matrix — next lesson), Parent (8 bytes), and if it has children, a Child buffer. A root entity with no children carries LocalTransform and LocalToWorld and nothing else.

More importantly, Parent is a component, so it changes the archetype. Parented and unparented entities of otherwise identical composition live in different chunks. Parenting an entity at runtime is a structural change — the entity moves chunks, with everything Module 1 said that costs.

So a hierarchy costs you three separate things, and they’re worth naming separately because they’re paid at different times: archetype fragmentation (paid in chunk density, permanently), structural change on reparent (paid per reparent), and a sync barrier per depth level (paid every frame, forever).

When to use one anyway

Given that bill, the discipline follows. A hierarchy is correct when the relationship is genuinely rigid and genuinely needed: a turret that must stay bolted to a tank, a hand attached to an arm, a UI element pinned to a panel. There, the alternative is worse — you’d be recomputing the relationship manually in a system, which is the same work without the engine’s parallelism.

It’s wrong when it’s used for organization. Grouping a thousand props under an empty “Props” root because it tidies the hierarchy adds a depth level and a parent component to a thousand entities in exchange for nothing — they don’t move with it, and there’s no spatial relationship to maintain. That’s the authoring model’s habit leaking into the runtime model, and Module 5 established that those models are optimized against each other.

What this buys you

You can now explain why a parent link converts independent entity data into a dependency graph, why that forces a top-down depth-ordered sweep with a barrier per level, why the relationship is stored twice and what that costs in timeliness, and why depth rather than entity count is the number to watch. Next: LocalToWorld — the composed result, why it’s stored rather than recomputed, and who is allowed to write it.

m06.l02