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>
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user