You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implements ADR 0012 — read it first; this issue is the worklist, the ADR is the reasoning.
Why
An architecture review measured the ported UE stdlib (Containers/ 22,213, Memory/ 8,390, Templates/ 5,848, Algo/ 1,268 — 37,719 lines) as serving two consumers, with std::string outnumbering FString206:1 and ~7,000 lines unreachable, and recommended deleting it.
ADR 0012 decides the opposite. Those ratios describe a migration that stopped one file in, not a library nobody wanted. FString exists solely because TArray relocates bitwise and libstdc++'s std::string does not survive that (OloEngine/src/OloEngine/Renderer/MeshSource.h:48-52) — deleting the port removes that symptom while preserving the condition that produced it. Committing to one library removes the boundary.
Scope
Bounded by a distinction the codebase had not previously named:
Engine-owned data — the engine allocates, owns and iterates it → adopts UE containers.
Binding surface — its shape is dictated from outside, or a generated binding crosses it → keeps std::. Concretely: entt, yaml-cpp, sol2, Mono, ImGui, Jolt, spdlog, and any ECS component field a generated binding marshals.
The test is "does something outside the engine dictate this type?", not "is this hot?".
Read before touching anything
Violations pass silently on MSVC and abort only under libstdc++. MSVC's std::string keeps no SSO self-pointer; libstdc++'s does. Every relocation mistake in this work is green on the primary dev toolchain and red on the Linux CI / GPU runner. Local green does not imply CI green for any task below. Step 1 exists to move that to compile time — do it first.
Worklist
Steps 1 and 2 are independent and can run in parallel. Everything from 3 onward is a chain.
1. Mechanise the reference-stability audit
Set CMAKE_EXPORT_COMPILE_COMMANDS on the clangcl preset (Ninja supports it natively; there is no compile_commands.json today and no .clang-tidy)
Write a clang-query matcher for references/pointers bound to a mapped value whose lifetime crosses a mutation. clang-query and clang-tidy are already installed at C:\Program Files\LLVM\bin
Validate the matcher against a known-safe and a known-unsafe hand-written case before trusting its output
2. Flip TIsTriviallyRelocatable to opt-in + hard error
The trait already exists (OloEngine/src/OloEngine/Templates/UnrealTypeTraits.h:703) and is already asserted by all six containers (Array.h:586, CompactSet.h:327, Deque.h:164, SparseArray.h:703, transitively Map.h / Set.h). It would not have caught any of the four cases this migration creates, because:
it defaults to Value = true for every type and is not recursive over members — TArray<std::string> is caught, TArray<StructContainingAString> is silently true;
it is OLO_STATIC_ASSERT_WARN, a [[deprecated]]-backed warning, not a gate.
Default becomes std::is_trivially_copyable_v<T>
Explicit true specialisations for FString (already at String.h:701 — required, it owns a heap buffer), TArray, TMap, Ref, and the other known-relocatable types
OLO_STATIC_ASSERT_WARN becomes a hard static_assert
Must build clean across all six containers, not just TArray
Add opt-in specialisations until green — expect ~15–20, concentrated in Task/ and the async asset system (TArray<FCallback>, TArray<Tasks::TTask<Ref<Asset>>>, TArray<TRefCountPtr<FThread>>, TArray<LowLevelTasks::FTask>), i.e. internal plumbing rather than the data this is for
No existing TArray<T> violates the trait today (Submesh already uses FString; BoneInfluence / BoneInfo / Vertex / BVHNode / BVHTriangle are POD), so this is free now and gets more expensive with every conversion made before it.
3. Make the OloHeaderTool classifier testable — prerequisite, not optional
tools/OloHeaderTool/main.cpp is 4,717 lines of hand-rolled text parsing with zero tests; every function is static inside a main.cpp and therefore unlinkable from a test binary.
Lift parse + classify into a linkable module: source text in, ComponentSerInfo out; main.cpp becomes the file-I/O adapter
Add tests, including a regression test for the parser fix at main.cpp:1934 (an inline method body swallowing an intervening private: label) — found empirically, never tested
A missing emit is loud (ComponentSerializerCoverageTest.cpp:188 fails with the component named). A wrong emit is caught only if a round-trip test happens to construct that component. That gap is why this is a prerequisite.
4. Teach the classifier TArray and FString
YAML emit path
Binary emit path
Recursive nested-struct path (needed for UIDropdownOption inside its component)
Rebuild GenerateBindings and diff the generated .inl — per CLAUDE.md's diff-the-.inl discipline
5. Convert the four blocking element types to FString
These four are what unblock all fourteen component vectors without converting any component's own string field:
Type
Fields
Home
Material
m_Name (Material.h:478)
Renderer
FoliageLayer
Name, MeshPath, AlbedoPath
Terrain
DialogueChoice
Text, Condition
Dialogue
UIDropdownOption
m_Label
Scene/UI
Each currently makes its enclosing std::vector unconvertible — TArray<UIDropdownOption> is exactly the free(): invalid pointer abort documented at Containers/String.h:26-33.
6. Convert the fourteen component std::vector fields to TArray
Nine were already safe (glm::vec3 x2, UUID, AssetHandle, u8, TerrainLayerRule, OffMeshLink, Ref<Texture2D>, ParticleSystem); the other four unblock via step 5.
7. Convert the four subsystems' engine-owned data
In order, following the element types outward: Renderer → Terrain → Dialogue → Scene/UI.
Run the step-1 matcher over each subsystem's maps before converting them
TMap / TSet are in scope but gated on the audit: std::unordered_map guarantees references to mapped values survive insertion, TMap does not — TSparseArray gives stable indices, not stable addresses (SparseArray.h:14, :731). &map[key] across an insert is fine with one and UB with the other, and unlike relocation there is no compile-time guard available. 727 unordered_map / set declarations exist in production.
8. Stop and measure
Cost per KLOC, defects found, defects the trait and audit missed
Decide whether to continue beyond these four subsystems — do not continue by momentum
Explicitly out of scope
No container is deleted, including the unreachable ones (~5,000 lines: TSparseSet 1,805, LinkedList 784, Queue 341, Algo 1,268, MemoryView 300 + 380 test, Memory orphans ~490). Accepted cost of committing to the port's shape. Note TSparseSet stays unreachable by construction — Set.h:42 hardcodes OLO_USE_COMPACT_SET_AS_DEFAULT 1, so TSetisTCompactSet.
No conversion at third-party seams.FString's implicit-in / explicit-out asymmetry is designed to sit there.
Component std::string fields. Binding surface.
Related
ADR 0012 — the decision and its five rejected alternatives
ADR 0004 — untouched and constrains this: Memory/LockFreeList.cpp's allocator is first-touched from a scheduler worker freeing a Jolt job; not a deletion candidate either
docs/analysis/dead-code.md — partly superseded for these four directories; its include-graph method cannot observe compiled-but-never-instantiated templates
Reversal conditions
Any one is sufficient cause to re-open (ADR 0012 section 7):
Step 8's measurement comes back materially worse than projected
The clang-query matcher cannot reach an acceptable false-negative rate → drop TMap adoption, keep the vector/string work
P2996 ships and makes the classifier work moot before step 4
A relocation hazard class emerges the trait cannot express (e.g. a type that registers this externally survives is_trivially_copyable_v and still cannot be relocated)
The section 0 ratios alone are not grounds to re-open — they are the starting condition the ADR exists to explain.
Implements ADR 0012 — read it first; this issue is the worklist, the ADR is the reasoning.
Why
An architecture review measured the ported UE stdlib (
Containers/22,213,Memory/8,390,Templates/5,848,Algo/1,268 — 37,719 lines) as serving two consumers, withstd::stringoutnumberingFString206:1 and ~7,000 lines unreachable, and recommended deleting it.ADR 0012 decides the opposite. Those ratios describe a migration that stopped one file in, not a library nobody wanted.
FStringexists solely becauseTArrayrelocates bitwise and libstdc++'sstd::stringdoes not survive that (OloEngine/src/OloEngine/Renderer/MeshSource.h:48-52) — deleting the port removes that symptom while preserving the condition that produced it. Committing to one library removes the boundary.Scope
Bounded by a distinction the codebase had not previously named:
std::. Concretely:entt,yaml-cpp,sol2, Mono, ImGui, Jolt,spdlog, and any ECS component field a generated binding marshals.The test is "does something outside the engine dictate this type?", not "is this hot?".
Read before touching anything
Violations pass silently on MSVC and abort only under libstdc++. MSVC's
std::stringkeeps no SSO self-pointer; libstdc++'s does. Every relocation mistake in this work is green on the primary dev toolchain and red on the Linux CI / GPU runner. Local green does not imply CI green for any task below. Step 1 exists to move that to compile time — do it first.Worklist
Steps 1 and 2 are independent and can run in parallel. Everything from 3 onward is a chain.
1. Mechanise the reference-stability audit
CMAKE_EXPORT_COMPILE_COMMANDSon theclangclpreset (Ninja supports it natively; there is nocompile_commands.jsontoday and no.clang-tidy)clang-querymatcher for references/pointers bound to a mapped value whose lifetime crosses a mutation.clang-queryandclang-tidyare already installed atC:\Program Files\LLVM\bin2. Flip
TIsTriviallyRelocatableto opt-in + hard errorThe trait already exists (
OloEngine/src/OloEngine/Templates/UnrealTypeTraits.h:703) and is already asserted by all six containers (Array.h:586,CompactSet.h:327,Deque.h:164,SparseArray.h:703, transitivelyMap.h/Set.h). It would not have caught any of the four cases this migration creates, because:Value = truefor every type and is not recursive over members —TArray<std::string>is caught,TArray<StructContainingAString>is silentlytrue;OLO_STATIC_ASSERT_WARN, a[[deprecated]]-backed warning, not a gate.std::is_trivially_copyable_v<T>truespecialisations forFString(already atString.h:701— required, it owns a heap buffer),TArray,TMap,Ref, and the other known-relocatable typesOLO_STATIC_ASSERT_WARNbecomes a hardstatic_assertTArrayTask/and the async asset system (TArray<FCallback>,TArray<Tasks::TTask<Ref<Asset>>>,TArray<TRefCountPtr<FThread>>,TArray<LowLevelTasks::FTask>), i.e. internal plumbing rather than the data this is forNo existing
TArray<T>violates the trait today (Submeshalready usesFString;BoneInfluence/BoneInfo/Vertex/BVHNode/BVHTriangleare POD), so this is free now and gets more expensive with every conversion made before it.3. Make the OloHeaderTool classifier testable — prerequisite, not optional
tools/OloHeaderTool/main.cppis 4,717 lines of hand-rolled text parsing with zero tests; every function isstaticinside amain.cppand therefore unlinkable from a test binary.ComponentSerInfoout;main.cppbecomes the file-I/O adaptermain.cpp:1934(an inline method body swallowing an interveningprivate:label) — found empirically, never testedA missing emit is loud (
ComponentSerializerCoverageTest.cpp:188fails with the component named). A wrong emit is caught only if a round-trip test happens to construct that component. That gap is why this is a prerequisite.4. Teach the classifier
TArrayandFStringUIDropdownOptioninside its component)GenerateBindingsand diff the generated.inl— per CLAUDE.md's diff-the-.inldiscipline5. Convert the four blocking element types to
FStringThese four are what unblock all fourteen component vectors without converting any component's own string field:
Materialm_Name(Material.h:478)FoliageLayerName,MeshPath,AlbedoPathDialogueChoiceText,ConditionUIDropdownOptionm_LabelEach currently makes its enclosing
std::vectorunconvertible —TArray<UIDropdownOption>is exactly thefree(): invalid pointerabort documented atContainers/String.h:26-33.6. Convert the fourteen component
std::vectorfields toTArrayComponents.hholds 48std::string, 14std::vector, 15std::unordered_*. Vectors convert;std::stringfields staystd::string(five generated consumers cross them: scene YAML, binary sidecar, save-games, MCP field registry, C#/Lua).Nine were already safe (
glm::vec3x2,UUID,AssetHandle,u8,TerrainLayerRule,OffMeshLink,Ref<Texture2D>,ParticleSystem); the other four unblock via step 5.7. Convert the four subsystems' engine-owned data
In order, following the element types outward: Renderer → Terrain → Dialogue → Scene/UI.
TMap/TSetare in scope but gated on the audit:std::unordered_mapguarantees references to mapped values survive insertion,TMapdoes not —TSparseArraygives stable indices, not stable addresses (SparseArray.h:14,:731).&map[key]across an insert is fine with one and UB with the other, and unlike relocation there is no compile-time guard available. 727unordered_map/setdeclarations exist in production.8. Stop and measure
Explicitly out of scope
TSparseSet1,805,LinkedList784,Queue341,Algo1,268,MemoryView300 + 380 test, Memory orphans ~490). Accepted cost of committing to the port's shape. NoteTSparseSetstays unreachable by construction —Set.h:42hardcodesOLO_USE_COMPACT_SET_AS_DEFAULT 1, soTSetisTCompactSet.FString's implicit-in / explicit-out asymmetry is designed to sit there.std::stringfields. Binding surface.Related
Memory/LockFreeList.cpp's allocator is first-touched from a scheduler worker freeing a Jolt job; not a deletion candidate eitherdocs/analysis/dead-code.md— partly superseded for these four directories; its include-graph method cannot observe compiled-but-never-instantiated templatesReversal conditions
Any one is sufficient cause to re-open (ADR 0012 section 7):
clang-querymatcher cannot reach an acceptable false-negative rate → dropTMapadoption, keep the vector/string workthisexternally survivesis_trivially_copyable_vand still cannot be relocated)The section 0 ratios alone are not grounds to re-open — they are the starting condition the ADR exists to explain.