Files
ue-toolchain/plugins/ue-design-skills/skills/ue-asset-loading-and-memory/SKILL.md
T
ue-toolchain dab3f35079 feat(skills): ship ue-design-skills bundle, licensing and delivery gate
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>
2026-09-05 23:48:55 +07:00

299 lines
12 KiB
Markdown

---
name: ue-asset-loading-and-memory
description: >-
Design or audit asset loading and CPU memory residency in Unreal Engine: the
asset manager and primary asset identifiers, asset bundles and cook rules,
hard versus soft versus weak reference semantics, streamable handle ownership,
synchronous load hitches, startup job sequencing, garbage-collection
reachability, retention leaks and unload paths. Use when reducing load times
or memory, choosing reference types, adding content-pulling definitions,
diagnosing hitches, or investigating objects that are never collected.
---
# UE asset loading and CPU memory
The invariant:
> An asset stays in memory because something reachable **owns** it. Loading is a
> policy decision; residency is an ownership fact. They are set by different code
> and must be reviewed separately.
Measured patterns from a reference product: [patterns](references/patterns.md).
Detection recipes: [failure modes](references/failure-modes.md).
Related skills: `ue-data-driven-architecture`, `ue-modular-gameplay`,
`ue-streaming-and-platform-budgets`, `ue-runtime-allocation-and-caching`.
Method, not architecture — how to check any claim in this bundle before repeating it: `ue-evidence-discipline`.
---
## 1. Separate the five budgets before optimizing anything
| Budget | What it is | Measured by |
|---|---|---|
| A. Disk / cook | package bytes shipped | build size, chunk sizes |
| B. CPU memory | object graph plus bulk data | memory report, object list |
| C. GPU memory | live render resources | platform GPU profiler |
| D. Streaming pool | a **limit**, not consumption | streaming stats |
| E. Net objects | actors, channels, bandwidth | network profiler |
This skill owns A and B. C and D belong to
`ue-streaming-and-platform-budgets`; E to `ue-runtime-allocation-and-caching`.
**The most common analytical error is treating D as B.** A pool setting grants
permission to use memory. It never reports use.
---
## 2. Reference semantics decide load cost
Three mechanisms, routinely conflated:
| Mechanism | Effect |
|---|---|
| reflected property, manual reference collection, a retained streamable handle | **owns** — keeps resident |
| weak pointer, object key | **observes** — keeps nothing |
| soft object or class pointer | **a path, not a reference** |
The third line is where most reasoning fails. **A soft pointer saves nothing by
itself.** Residency is decided by where the loaded result is stored — a soft
reference resolved into a reflected property is a hard reference with extra
steps.
### Measure the transitive closure, not the direct count
Direct reference counts are misleading. What matters is how many packages load
when this one loads. In the audited reference the same architectural layer
spanned three orders of magnitude depending on reference type alone: an archetype
holding hard references pulled over a thousand packages, while a catalog entry
addressing by identifier pulled two.
That single contrast is the argument for treating reference type as
architecture rather than style. Recipe: AL-01.
### Choosing
| Need | Reference |
|---|---|
| address before load; cook and bundle identity | primary asset identifier |
| optional, feature-scoped or async content | soft object or class pointer |
| required whenever the owner is loaded, and small | hard pointer |
| data selects an implementation class | class reference |
| owner-exclusive polymorphic fragment | instanced object |
Rule of thumb: a definition that is a **catalog entry** references by identifier
or softly; a definition that is an **archetype** may reference hard — but its
closure must be measured and budgeted, not assumed.
---
## 3. Bundles express role, not grouping
Declare the load role beside the reference, then request bundles per runtime role
at load time.
Gates:
- every soft reference carries an intentional bundle tag;
- server-irrelevant presentation is client-only;
- prediction-relevant gameplay classes are client and server;
- **a missing bundle tag means "always loaded with the owner"** — decide it, do
not default into it;
- cook rules control budget **A**, never budget **B**.
A label asset with thousands of soft references and no hard references determines
what ships and retains nothing at runtime. Do not read cook rules as residency.
Watch for a declared bundle name that no property ever uses: it is requested at
load time, matches nothing, and looks like a working tier. Recipe: AL-02.
---
## 4. Startup discipline
### Never synchronously load a graph you have not measured
The dominant boot hitch in a data-driven project is one synchronous load whose
hard closure is large. An elegant async bundle pipeline that runs *after* it is
decoration.
- [ ] every synchronous load on the boot path is justified;
- [ ] its transitive closure is measured, not assumed;
- [ ] an async alternative was considered, with an explicit wait state;
- [ ] the loading screen carries a reason string;
- [ ] cancellation is distinguishable from completion.
### Startup jobs must own handles and report truthfully
A progress bar that never moves is a bug, not a cosmetic issue — it hides which
job is slow. Three independent layers can each break it: a macro that never
fills the handle, a throttle comparison with swapped operands, and an empty
progress receiver. Any one of them is enough. Recipe: AL-03.
### Global data assets need bundles too
Soft class pointers on a globally-loaded data asset with **no** bundle tags load
synchronously at first use — typically in the frame of first damage. Tag them and
preload them with the mode. Recipe: AL-04.
---
## 5. Retention: what actually keeps objects alive
Audit six owners explicitly: reflected containers on long-lived subsystems;
manual reference collection in native code; retained streamable handles; delegate
bindings capturing strong references; component and actor ownership chains; class
default objects reached through class references.
### A convenience loader must not default to permanent
The worst shape in this area is a helper that loads an asset and adds it to a
reflected set, with the keep-in-memory parameter **defaulting to true** and no
release API anywhere. Every casual call then synchronously loads *and permanently
roots* the asset. The soft pointer was chosen for deferred loading; the API turns
it into a process-lifetime hard reference.
Required design: the flag is explicit at every call site or defaults to false; a
paired release exists and is used; the retained set is inspectable from a console
command; retention has an owner and a scope. Recipe: AL-05.
### Strong keys pin object graphs — and raw pointers lose them
```text
map<strong actor pointer, strong value> one missed unregister pins the actor,
its meshes, materials, sounds, effects
map<raw actor pointer, non-reflected struct of object pointers>
garbage collection cannot see the
values; the key can dangle
```
Both errors can coexist in one codebase. **Audit direction as well as presence.**
Prefer object keys with explicitly-owned values plus a periodic prune. Recipe:
AL-06.
---
## 6. Unloading must exist before you claim modularity
If a project cannot answer "what is released when a mode ends", its memory
profile only grows. Either implement release, or state plainly that mode changes
require map travel — that is a legitimate scope decision, and leaving it
undefined is not.
The measurement is blunt and takes one command: count unload calls, bundle
releases and explicit collection requests across the whole project. In the
audited reference every one of those counts was **zero**. Recipe: AL-07.
### Avoid collection ping-pong
A forced collection at a transition boundary must not collect what the next
transition immediately needs. Options: keep transition-critical classes rooted,
defer collection past the next load, or do not force it and let the budget drive
it.
---
## 7. Failure handling changes memory behaviour
- a cancelled load must not run the success path;
- asynchronous results must be read, not discarded;
- partially loaded state must be releasable;
- waiting without a timeout converts a stall into a hang;
- a permanent loading screen with no reason string is not error handling.
The sharpest instance: binding the cancel delegate to the **same callback** as
completion. A cancelled load then reports success, downstream code dereferences
unloaded assets, and each of those dereferences is individually silent. Recipes:
AL-08, AL-09.
---
## 8. Review checklist
### References
- [ ] reference type chosen per lifecycle and documented in metadata;
- [ ] transitive hard closure measured for every archetype-level definition;
- [ ] no soft reference resolved into an unbounded reflected cache;
- [ ] bundle tags on all soft references, and every requested bundle is used;
- [ ] cross-plugin edges intentional and acyclic.
### Loading
- [ ] no unmeasured synchronous load on boot or in a hot path;
- [ ] async loads own their handles and can be cancelled;
- [ ] progress reporting is verified to move, not merely implemented;
- [ ] cancellation and failure paths are distinct from success;
- [ ] global data assets are bundled, not lazily loaded at first use.
### Retention
- [ ] every long-lived container has an owner, a bound and a prune policy;
- [ ] strong keys justified; object keys used where identity suffices;
- [ ] non-reflected object pointers eliminated or documented as weak by design;
- [ ] register and unregister pairs verified across teardown paths;
- [ ] a release API exists for anything deliberately kept resident.
---
## 9. Measurement plan
Static first — it is cheap and finds structural problems:
1. build the dependency graph from the asset registry: direct hard and soft
counts, transitive hard closure per definition, cross-plugin edges;
2. rank definitions by closure and compare against their architectural layer;
3. search for synchronous loads on boot and hot paths;
4. inventory retention owners by type.
Runtime, once a packaged build exists: memory reports before and after a mode
cycle; object listings on suspected roots — root-graph evidence, not process
memory; fifty to a hundred spawn and travel cycles looking for monotonic growth;
an allocation profiler for attribution.
### Do not measure residency in the editor
Editor builds commonly load **both** client and server bundles, so an in-editor
memory profile represents neither shipping role. Residency numbers require a
packaged build; use the editor for relative structural comparison only. Recipe:
AL-10.
---
## 10. When to stop
Optimize loading and residency when boot or transition time is a product problem,
a platform ceiling is real and near, growth is monotonic across a session, or a
specific closure is measurably oversized.
Do not restructure references on aesthetics. A fourteen-package closure is not a
problem; a thousand-package synchronous closure on the boot path is. Measure
first, and **keep the measurement** so the next change can be compared against
it.
---
## Provenance
The findings come from a source and dependency-graph audit of Epic's Lyra Starter
Game on Unreal Engine 5.6. The engine's own asset manager and streamable manager
are not part of that project, so every claim about their internals is inferred
from call sites and marked as such rather than presented as measured.
No profiler was run and no timing was captured: this material is structural by
construction, and it deliberately contains no performance numbers. What it
contains is counts — of call sites, of unload paths, of packages in a closure —
which is a different kind of evidence and a more portable one.
Source addresses stay in the research archive. Each entry carries a stable
identifier (`AL-01` and up) resolving back to the audited location there.
## Evidence boundary
One project, one engine version. Binary assets were not read, so the contents of
any specific closure are structural rather than byte-measured. Re-run the recipes
against your own tree; the counts quoted show the shape of a contrast, not a
target.