Skip to content

Perf/DOD: flat depth-sorted hierarchy world-matrix propagation pass (follow-up to #442) #499

Closed

Description

@drsnuggles8

Problem

Follow-up to #442. Slice 1 (cached local world matrix + self-validating dirty cache on TransformComponent::GetTransform()) shipped on feature/transform-world-matrix-cache. This issue tracks slice 2: the flat, depth-sorted hierarchy propagation pass that #442's proposal also called for.

While implementing slice 1 it was confirmed that the engine currently renders and queries every entity with its local transform — there is no parent→child world-matrix composition anywhere in Scene.cpp (no GetWorldTransform, no parent-chain walk in the render/submission paths). So this is net-new feature work, not merely an optimization of an existing on-demand walk.

Proposed approach

  • Add a derived world-matrix cache (a parallel cache component, or a mutable world field alongside the local cache) that is not serialized — same runtime-only discipline as slice 1's local cache.
  • Once per frame, iterate entities in depth order over a flat array (not recursive per-entity pointer chasing over RelationshipComponent links): world[child] = world[parent] * local[child], roots first.
  • Mark children dirty when a parent's local transform changes; only re-propagate dirty subtrees.
  • Point world-transform consumers (rendering, physics sync, audio positioning, culling, AI) at the cached world matrix instead of the local GetTransform().

Acceptance criteria

  • Parented entities compose their parent's transform into their rendered/queried world matrix (a visible behavior change — children currently ignore parent transforms).
  • World-matrix reads are O(1) cache hits within a frame; a deep-hierarchy profiling scene shows reduced transform-rebuild time.
  • No change to serialized data / scene round-trip (world matrices stay derived-only).
  • Visual verification per CLAUDE.md: screenshot a parented hierarchy from multiple angles, confirm children follow parents.

Notes

  • Depends on nothing now that slice 1 has landed; slice 1's local-matrix cache is the natural per-entity local[] input for the propagation.
  • Because it changes what parented entities render as, this is higher-risk than slice 1 and needs the rendering visual-verification loop, not just CPU/contract tests.

Score

capability: 5
craft: 2
stability: 3
decay: 2
effort: 5
confidence: 0.7
learning: 3
fun: 3
kano: performance
blocked_by: []
blocks: []

Rated per issue-scoring · score = confidence × (capability + craft + stability + decay) / effort, derived by the picker.

Activity

  1. added
    featureNew feature or request
    performancePerformance / data-oriented optimization
    on Jul 1, 2026
  2. coderabbitai commented on Jul 1, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #100 - Feature/rendercommand_sorting [closed]
    #199 - Add Quality Tiering System and Refactor Transform Component [closed]
    #467 - feat(codegen): SceneSerializer blocks for AssetHandle/UUID-field components (#451) [merged]
    #468 - feat(physics): wire Jolt HeightFieldShape into terrain collision (#428) [merged]
    #483 - feat(net): generalize snapshot interpolation/reconciliation to a per-component policy registry (#462) [merged]


    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  3. drsnuggles8 commented on Jul 5, 2026

    @drsnuggles8
    OwnerAuthor

    Slice 1 shipped on feature/flat-world-matrix-propagation (PR #562).

    What shipped — flat depth-sorted world-matrix propagation pass

    • Scene::PropagateWorldTransforms(): a flat BFS from root entities over RelationshipComponent, composing parent-chain world matrices in one linear sweep per tick (parents always written before children, not recursive per-entity pointer chasing). Result lands in a new runtime-only WorldTransformComponent (excluded from the scene-copy tuple / save-games / auto-serialization — same *StateComponent exclusion pattern used elsewhere).
    • Confirmed while implementing: there was genuinely no parent→child composition anywhere before this — every consumer read TransformComponent::GetTransform() (local space) directly. This is net-new behavior (as Perf/DOD: cache world matrix on TransformComponent (dirty flag) + flat hierarchy update pass #442 flagged), and it's now a visible, verified change: a rotated parent's child/grandchild render diagonally offset along the parent's rotated axis instead of ignoring it.
    • Wired into all three per-tick update paths (SimulateRuntimeStep, OnUpdateSimulation, OnUpdateEditor), and migrated the primary render/debug-gizmo submission paths (2D sprites/circles/text, static + animated mesh, models, tile renderer, camera frustum + AABB gizmos) to read the composed world transform.
    • Entity::GetWorldTransform() / Scene::GetWorldTransform(entt::entity) accessors, falling back to local transform if the pass hasn't run yet for an entity.
    • Tests: 6 new WorldTransformPropagationTest Functional tests (deep chains, rotation composition, re-parenting, un-parenting, dangling/destroyed-parent safety) + a new opt-in-assert perf benchmark (100k flat entities, 100k across 1000×100 chains) confirming linear scaling. Full suite green.

    Deferred — the rest of the original proposal

    • Physics-sync / audio / particle / culling / terrain / foliage / water producers still read local transforms only — not yet migrated to the composed world transform.
    • No incremental per-subtree dirty tracking: the BFS traversal order is recomputed fresh every tick rather than cached and invalidated on hierarchy change. Still O(N) and correctness-safe, just not maximally optimized — a natural follow-up once the remaining consumers are migrated.

    Leaving this issue open until those two follow-ups land; the acceptance criteria that matter most for a first slice (parented entities visibly compose parent transforms, verified with screenshots; O(N) linear-sweep propagation) are satisfied.

  4. drsnuggles8 commented on Jul 6, 2026

    @drsnuggles8
    OwnerAuthor

    Resolved by #562 (merged 2026-07-05): Scene::PropagateWorldTransforms() adds the flat, depth-sorted BFS world-matrix propagation this issue asked for, wired into all three per-tick update paths, with 6 Functional tests + a 100k-entity benchmark and visual verification (rotated-parent composition confirmed in editor). All acceptance criteria met. Flagging for closure.

  5. drsnuggles8 commented on Jul 6, 2026

    @drsnuggles8
    OwnerAuthor

    Appears resolved by #562 (merged 2026-07-05): PR #562 ("Perf/DOD: flat depth-sorted hierarchy world-matrix propagation pass") — title matches this issue exactly and implements the proposed flat depth-order world-matrix propagation pass as slice 2 following #442's slice 1. Flagging for closure.

  6. drsnuggles8 commented on Jul 6, 2026

    @drsnuggles8
    OwnerAuthor

    Closing — confirmed resolved by #562 (merged 2026-07-05).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or requestperformancePerformance / data-oriented optimization

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions