Skip to content

perf(scene): reserve EnTT storage + dedup AnimatedModel loads on deserialize#584

Merged
drsnuggles8 merged 1 commit into
masterfrom
feature/scene-load-model-sharing
Jul 7, 2026
Merged

perf(scene): reserve EnTT storage + dedup AnimatedModel loads on deserialize#584
drsnuggles8 merged 1 commit into
masterfrom
feature/scene-load-model-sharing

Conversation

@drsnuggles8

@drsnuggles8 drsnuggles8 commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Cheap-wins slice of #525 ("Scene: YAML load takes 24-59s at 50k-100k entities"), scoped to the two structural fixes identified without needing a binary scene format or runtime spawner rewrite:

  • Registry/component reserve: SceneSerializer::DeserializeFromYAML and DeserializeAdditive now reserve the EnTT entity pool plus the IDComponent/TransformComponent/TagComponent storages up front, using the known entity count from the YAML Entities sequence, instead of letting every pool reallocate repeatedly as entities stream in.
  • AnimatedModel dedup cache: SceneSerializer now caches Ref<AnimatedModel> per resolved absolute source path for the lifetime of one Deserialize()/DeserializeFromYAML() call. A crowd of entities sharing one rigged-character source file (the common case for NPCs/enemies) now pays Assimp import + GPU-buffer allocation once instead of once per entity. Per-entity playback state (AnimationStateComponent) stays independent — verified by a new regression test.

Results

Measured with the perf-stress battery (docs/guides/perf-stress-scenes.md), scene-open time:

scene entities before after speedup
anim_crowd_5000 5,003 56.9 s 2.2 s ~26x
anim_crowd_10000 10,003 >120s (perf-battery tool timeout) 4.3 s >28x
ecs_static_100000 100,002 >120s (timeout) 20.0 s >6x

ecs_static_100000 has no AnimationStateComponent at all, isolating the reserve-only win; the anim_crowd_* scenes get both fixes stacked.

Test plan

  • New AnimatedModelDedupTest.cpp — two entities sharing one glTF source path share the same MeshSource/Skeleton Ref after deserialize, while independently-set AnimationStateComponent playback scalars (m_CurrentTime, m_IsPlaying) survive un-aliased. Log evidence confirms only one Assimp import fires for the shared path.
  • Full OloEngine-Tests suite (4198 tests) passes, 0 failures, 0 regressions.
  • Perf-stress battery before/after comparison (above).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Scene loading now avoids reloading the same animated asset multiple times, improving load times and reducing memory use for scenes with repeated characters.
    • Animated entities keep their individual playback state after reloading, so one entity’s animation changes no longer affect another’s.
  • Tests

    • Added coverage for animated scene reloads to verify shared asset reuse and independent per-entity animation state.

…ene deserialize (#525)

Two cheap wins for scene YAML load time, measured with the perf-stress
battery (docs/guides/perf-stress-scenes.md):

- Reserve the entity pool and the 3 always-added component storages
  (IDComponent/TransformComponent/TagComponent) up front in both
  Deserialize and DeserializeAdditive, using the known entity count
  from the YAML, so none of them reallocate mid-load.
- Cache Ref<AnimatedModel> loads per resolved source path within a
  single SceneSerializer instance, so a crowd of entities sharing one
  rigged-character source file only pays Assimp import + GPU buffer
  allocation once instead of once per entity. Per-entity playback
  state (AnimationStateComponent) stays independent — verified by a
  new regression test (AnimatedModelDedupTest.cpp) that shares one
  glTF across two entities and confirms the model/skeleton Refs are
  shared while playback scalars are not.

Scene-open-time results (anim_crowd/ecs_static perf-stress scenes):
  anim_crowd_5000  (5,003 entities):   56.9s -> 2.2s   (~26x)
  anim_crowd_10000 (10,003 entities): >120s -> 4.3s   (>28x)
  ecs_static_100000 (100,002 entities): >120s -> 20.0s (>6x)

Full OloEngine-Tests suite (4198 tests) passes with no regressions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

SceneSerializer now caches Ref<AnimatedModel> instances by source file path during entity deserialization to avoid redundant loads, threading the cache through DeserializeEntityComponents and DeserializeEntity. Scene deserialization also pre-reserves EnTT registry storage for large loads. A new integration test validates the dedup behavior.

Changes

Animated Model Dedup and Scene Loading

Layer / File(s) Summary
Cache member and header updates
OloEngine/src/OloEngine/Scene/SceneSerializer.h
Adds AnimatedModel.h, <string>, <unordered_map> includes and a private m_AnimatedModelCache member keyed by resolved source path.
Cache threading through deserialization flow
OloEngine/src/OloEngine/Scene/SceneSerializer.cpp
DeserializeEntityComponents gains an animatedModelCache parameter; AnimationStateComponent loading checks the cache before creating a new AnimatedModel; DeserializeEntity passes m_AnimatedModelCache through.
EnTT registry pre-sizing for scene load
OloEngine/src/OloEngine/Scene/SceneSerializer.cpp
DeserializeFromYAML and DeserializeAdditive reserve storage for entities and key components (IDComponent, TransformComponent, TagComponent) ahead of load.
Dedup integration test
OloEngine/tests/Rendering/AnimatedModelDedupTest.cpp, OloEngine/tests/CMakeLists.txt
New GPU-dependent test creates two entities sharing an animation source path but distinct playback state, serializes/deserializes them, and asserts shared mesh/skeleton refs with independently restored and mutable playback state; test source is registered in CMakeLists.txt.

Sequence Diagram(s)

sequenceDiagram
  participant DeserializeEntity
  participant DeserializeEntityComponents
  participant AnimatedModelCache
  participant AnimatedModel

  DeserializeEntity->>DeserializeEntityComponents: pass m_AnimatedModelCache
  DeserializeEntityComponents->>AnimatedModelCache: lookup by SourceFilePath
  alt cached
    AnimatedModelCache-->>DeserializeEntityComponents: return existing Ref
  else not cached
    DeserializeEntityComponents->>AnimatedModel: Create(SourceFilePath)
    DeserializeEntityComponents->>AnimatedModelCache: insert new Ref
  end
  DeserializeEntityComponents-->>DeserializeEntity: entity with shared model, independent playback state
Loading

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@drsnuggles8

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@sonarqubecloud

sonarqubecloud Bot commented Jul 7, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
E Maintainability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@drsnuggles8 drsnuggles8 merged commit 8560179 into master Jul 7, 2026
8 of 10 checks passed
@drsnuggles8 drsnuggles8 deleted the feature/scene-load-model-sharing branch July 7, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant