Phase 0 of the handoff plan, as a marketplace rather than a flat skills/ directory. Content moved out of the LyraResearch archive and depersonalised: addresses stay in the archive, recipes ship. - plugins/ue-design-skills: 17 skills, 232 failure-mode entries, each with the six required fields; catalog.json as the harness-neutral source of truth and .claude-plugin/ as one adapter over it. - _gate: 16 rules, one poisoned fixture per rule, plus surface coverage so a declared file cannot silently miss the line rules. - ADR-0002 (harness-neutral bundle behind a marketplace) and ADR-0003 (split licensing: CC BY-ND 4.0 prose, Apache-2.0 code and metadata). - LICENSE files at both levels, CONTRIBUTING.md, docs/licensing-options.md as the material the licence decision grew from. Verified: gate.py 0 violations; test_gate.py 16/16 rules redden on their fixtures with a clean baseline and 2 root files reaching the line rules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
10 KiB
name, description
| name | description |
|---|---|
| ue-runtime-allocation-and-caching | Design or audit runtime allocation, pooling, caching and replication cost in Unreal Engine: per-frame and per-event churn, object pools, bounded caches and ring buffers, pointers into container storage, strong versus weak retention of spawned components, fast-array and subobject replication, dormancy and relevancy, and dedicated-server exclusions. Use when profiling hitches, investigating memory that grows during a session, effect or UI churn, or deciding how to reduce replicated object count. |
UE runtime allocation and caching
The invariant:
Every cache needs an owner, a bound and an eviction rule. Every pooled object needs a stable handle. Every replication optimization must name which cost it reduces — memory, CPU or bandwidth — because they are not the same.
Measured patterns from a reference product: patterns. Detection recipes: failure modes.
Related skills: ue-asset-loading-and-memory, ue-cosmetics-and-teams,
ue-gameplay-messaging, ue-streaming-and-platform-budgets.
Method, not architecture — how to check any claim in this bundle before repeating it: ue-evidence-discipline.
1. Never store a raw pointer into a container's storage
This is the highest-severity defect class in this skill, and it is the one that looks most harmless:
FPooledList& Pool = PooledMap.FindOrAdd(Key); // address of a map value
LiveEntries.Emplace(Component, &Pool, ReleaseTime); // held across frames
Maps and arrays reallocate on insertion. A later insert invalidates every stored address. The failure is data-dependent: it needs a second key inserted while an earlier entry is still live, so it survives casual testing and appears under load.
A null check does not help. The pointer is not null; it is non-null garbage.
Rules
- store a key or an index, and resolve at the point of use;
- if a stable address is genuinely required, use a container that guarantees stable element addresses, and say so in a comment at the declaration;
- treat "pointer obtained from a container, stored beyond the current scope" as a review blocker, not a style note;
- when you suspect one, the allocator's stomp mode converts a probabilistic corruption into a deterministic crash at the right line.
The same shape appears with lazily-growing statistics maps whose entries are cached by a widget and dereferenced every frame — the map grows a key when a new data source appears, and the widget's pointer dies mid-session. Recipes: RC-01, RC-02.
2. Bound every cache and name its eviction
A cache without a bound is a leak with good intentions. For each one, write down:
owner who invalidates it
key space bounded by what?
capacity a number, or "unbounded" with a justification
eviction time, count, or explicit removal
retention strong or weak
teardown what empties it, and when
Three shapes recur and each is silent:
Read-modify-write growth. Fetch a collection, append one element, write the whole thing back. Per-event cost grows linearly and total work is quadratic over a session — and if nothing ever clears it, the collection is also a leak. Recipe: RC-03.
Re-append without pruning. A component that re-adds every previously spawned effect on each trigger and never removes finished ones. When the arrays are strong references, every one-shot effect is pinned for the whole match. Recipe: RC-04.
Prefilled buffers reported as data. A fixed-capacity sample cache filled with zeroes reports a misleading minimum and average until it wraps. Track the fill count, or exclude the prefix. Recipe: RC-08.
3. Pools need a lifecycle, not just reuse
A pool is correct when acquire and release are symmetric and idempotent; released objects are reset to a known state; there is a high-water policy — grow, cap, or trim on idle; pooled objects remain reachable by the collector while pooled; the pool empties on teardown; and a leaked acquire is detectable rather than silent.
Fixed-size pools must define what happens on exhaustion: block, drop, or grow. Silent drop is acceptable for cosmetic effects only, and it should be counted.
A pool that only grows is not a leak in the technical sense and behaves like one in practice. One burst leaves its peak allocated for the rest of the session.
4. Churn: diff, do not rebuild
A periodic scan that destroys and recreates its descriptor objects every interval allocates, invalidates observers, and hides real changes in noise.
compute the desired set
diff against the current set
add new, remove missing, leave the rest alone
Reserve capacity once when the size is predictable. In hot paths prefer the reset-without-freeing operation over the one that releases the buffer — the latter guarantees a reallocation on the next frame. Do not allocate in paint or per-frame render-thread paths at all. Recipes: RC-05, RC-06, RC-07.
5. Protocol identifiers need width and uniqueness discipline
A batch identifier assigned as the current in-flight count rather than a monotonic sequence produces reuse: A gets 0, B gets 1, A confirms and is removed, C gets 1 again — and a handler that matches the first plausible entry applies the confirmation to the wrong batch.
Compounding it, the same value can be one width at the source, another in the message and a third in storage — which turns a wraparound into a permanent mismatch, and an unbounded pending list into an unbounded leak.
Rules
- identifiers are sequences, never counts;
- one width for the whole path, asserted at every boundary;
- define wrap behaviour explicitly;
- handlers match on identity, not on "the first entry that could be it";
- an unbounded pending list needs a timeout and a drain policy.
Recipe: RC-09.
6. Replication: name the axis you are optimizing
Three different costs, routinely conflated:
| Mechanism | Memory | CPU | Bandwidth |
|---|---|---|---|
| Dedicated-server presentation exclusion | ↓ | ↓ | unchanged |
| Fast-array delta serialization | ↑ per-item key and per-connection state | ↑ delta walk | ↓ |
| Subobject replication | ↓ | ↓ | ↓ |
| Dormancy | — | ↓↓ | ↓ |
| Relevancy and replication graph | — | ↓↓ | ↓ |
Two counterintuitive facts worth internalizing:
- excluding cosmetics on a dedicated server saves memory and CPU and changes bandwidth by zero, because the intent list still replicates;
- fast arrays are a bandwidth optimization that costs memory and CPU.
The genuine object-count reducer is subobject replication: N owned items add zero actors and zero channels.
Before claiming a replication optimization
- state which axis improves and which regresses;
- measure that axis specifically, not "performance";
- confirm the mechanism is actually enabled rather than merely present —
see
ue-multiplayer-authorityfor a measured case where the replication graph ships disabled, dormancy is unused, and a force-update list is declared, read, and never populated; - verify net-mode gating is consistent across presentation systems;
- check replicated history structures for a removal path.
7. Dedicated-server exclusions must be systematic
List every client-only construction and guard it in one reviewable place: cosmetic actors and meshes, widgets and layouts, effect and audio components, preview and capture systems, input and settings objects.
An inconsistent set is worse than none, because server memory profiles become unpredictable — one system guarded while a sibling spawning real actors is not. Audit by searching for the net-mode guard and diffing against the list of presentation systems.
8. Review checklist
Allocation
- no stored pointers into container storage;
- hot paths reset rather than free, with capacity reserved;
- no allocation in paint or render-thread paths;
- periodic work diffs rather than rebuilds.
Caches and pools
- owner, bound, eviction and teardown written down for each;
- key space provably bounded;
- retention strength intentional;
- acquire and release symmetric; exhaustion policy defined;
- prefilled buffers do not distort statistics.
Protocol
- identifiers are sequences with one consistent width;
- pending lists have a timeout and a drain;
- handlers match by identity.
Replication
- the optimization names its axis;
- the mechanism is verified enabled;
- net-mode gating consistent across presentation systems;
- replicated history bounded.
9. Measurement plan
- long-session soak: fifty to a hundred spawn, despawn and transition cycles, looking for monotonic growth rather than an absolute number;
- per-frame allocation counters in the hot scenes;
- the allocator's stomp mode first when a dangling-pointer class is suspected — it is a binary answer, and it costs one run;
- profiler counters for boundedness curves over time, which is the cheapest way to turn "is this cache bounded?" into a graph;
- network statistics and a network profiler for the replication axes, measured separately from each other;
- server and client measured independently; never generalize one to the other.
Report growth curves, not single samples. A cache that is 4 MB after one minute and 40 MB after ten is a defect regardless of where it eventually stops.
Provenance
The patterns and failure modes in references/ come from a source audit of the
runtime, feedback, UI and weapon systems of Epic's Lyra Starter Game on Unreal
Engine 5.6, read as source rather than profiled. No profiler was run, so there
are no timing numbers here by construction — the findings are structural, and the
recipes tell you how to measure them yourself.
Source addresses stay in the research archive that produced this skill. Each
entry carries a stable identifier (RC-01 and up) that resolves back to the
audited location, so any specific claim can be produced on request.
Evidence boundary
One project, one engine version, one workspace. The defect classes are general; the specific instances are evidence, not guarantees about other versions. Re-run the detection recipes against your own tree before acting on any specific claim.