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:
ue-toolchain
2026-09-05 23:48:55 +07:00
parent 9e2194c298
commit dab3f35079
67 changed files with 21630 additions and 0 deletions
@@ -0,0 +1,291 @@
---
name: ue-streaming-and-platform-budgets
description: >-
Design or audit GPU and streaming budgets in Unreal Engine: texture, mesh and
virtual texture streaming, streaming pool sizing, texture LOD groups,
scalability buckets, device profiles and console-variable priority, world
partition and level instance lifetime, and the boundary between disk, CPU
memory and video memory. Use when targeting a platform memory ceiling, tuning
quality tiers, adding a background or preview level, or investigating why a
quality setting has no effect.
---
# UE streaming and platform budgets
The invariant:
> A streaming pool is a **permission**, not a measurement. A quality setting is a
> **request**, not an outcome. Both are arbitrated by device profiles and
> console-variable priority before anything reaches the GPU.
Measured patterns from a reference product: [patterns](references/patterns.md).
Detection recipes: [failure modes](references/failure-modes.md).
Related skills: `ue-asset-loading-and-memory`, `ue-game-settings-architecture`,
`ue-runtime-allocation-and-caching`.
Method, not architecture — how to check any claim in this bundle before repeating it: `ue-evidence-discipline`.
---
## 1. Four budgets, four instruments
| Budget | What it physically is | Instrument |
|---|---|---|
| **A. Disk / cook** | bytes in the shipped package | build and chunk sizes |
| **B. CPU memory** | object graph plus bulk data in system RAM | memory report, object listing |
| **C. Video memory** | live render resources: textures, buffers, render-target pool, virtual-texture pool, shadow pages, ray-tracing structures | platform GPU profiler |
| **D. Streaming pool** | an accounting **ceiling** the streamer tries to stay under | streaming stat |
Four rules that prevent most wrong conclusions:
- **A is not D.** Clamping a texture group's maximum size shrinks what is cooked.
The pool is unchanged.
- **D is not C.** The pool covers streamable mips only. Render targets, virtual
texture pools, shadow pages and acceleration structures are in C and invisible
to the streaming stat.
- **B is not C.** A loaded texture object need not have any resident mips.
- **Never report a pool size as memory used.** It is the single most common
error in this area, and it makes every downstream number meaningless.
---
## 2. Console-variable priority is the real arbiter
Effective value is not "last write wins"; sources are ranked. In practice:
```text
command line > console > device profile > system ini > project ini
> game setting > scalability > constructor
```
with one exception that matters enormously: a device profile applies variables
whose names begin with the scalability prefix at **scalability** priority, and
everything else at **device-profile** priority.
### The consequence
A value set by a device profile **cannot be changed** by anything the
scalability system does afterwards — which includes every in-game quality
setting. The same setting is therefore mutable on one tier and frozen on
another, with nothing in the UI explaining the difference. Recipe: ST-01.
### Gates
- [ ] every quality-affecting variable has a documented intended priority source;
- [ ] prefixed and unprefixed names are used deliberately, not by habit;
- [ ] per-tier overrides are complete across tiers, or partial with a comment
saying why;
- [ ] the settings UI can express "clamped by platform" rather than silently
failing;
- [ ] requested and effective values are both observable at runtime.
**The one command that settles it:** dump variables with their source. Anything
else is inference.
---
## 3. Pool sizing, and its detachment from real memory
Two knobs interact: the pool size, and whether the pool is clamped by actual
video memory.
The trap is structural rather than accidental: **the highest quality buckets
commonly disable the video-memory clamp**, and automatic detection promotes
hardware to the top bucket on a compute benchmark that does not consider memory
size at all. A fast card with modest memory therefore lands in the bucket where
the pool budget is largest and least constrained. Recipe: ST-02.
Rules:
- decide per platform whether the pool is derived from memory or fixed;
- if fixed, write down the assumed memory floor;
- benchmark thresholds must consider memory, not only throughput;
- test the top bucket on a low-memory card, not only on the reference machine.
### Sharing the pool
If mesh streaming is enabled and no separate mesh pool is configured, geometry
and textures compete inside one budget. That is a legitimate choice; it is not a
legitimate surprise. Check both values on every platform. Recipe: ST-03.
---
## 4. Texture groups are the per-content lever
Scalability moves everything at once; groups shape individual content classes.
Use them to encode intent: hero content larger, world content platform-scaled,
UI unstreamed and sized exactly, debug content excluded from cook.
Gates: every group has a stated purpose; mobile tiers override group caps rather
than only global quality; no content relies on a group it does not belong to;
streaming-exempt content is deliberate and small.
Watch for group settings that describe a delivery mechanism the project does not
have — optional mip levels configured with no chunk installation, for instance.
Configuration that cannot take effect is worse than absent configuration,
because it reads as a decision. Recipe: ST-08.
---
## 5. Build-level decisions that look like runtime knobs
Some streaming variables are read-only: their value is fixed at startup and
influences what is cooked. Changing them at runtime, or from a device profile,
does nothing and says nothing.
Before treating any streaming variable as tunable, check its flags. And never
A/B a read-only variable mid-session and compare the numbers — you are comparing
one configuration with itself. Recipe: ST-04.
Virtual textures belong in the same category. They change residency
characteristics fundamentally, their pool is separate from the streaming budget,
and a pool that does not grow on over-subscription produces thrashing rather
than an allocation. Enabling them for some content and not other content makes
budget comparisons invalid.
---
## 6. Device profiles: reachability before content
A profile hierarchy usually encodes platform base → tier → device. Three
failures recur, and all three are silent:
**A section that is never selected.** Defining a profile section does not make it
reachable; something must map to it. A tier section absent from the registry is
configuration that reviewers read as live. Recipe: ST-05.
**Headers that contradict assignment.** Comment banners grouping devices under a
tier label, while the devices beneath resolve to a different base profile. The
comment is documentation; the base profile is behaviour. Recipe: ST-06.
**An override list that no configuration populates.** A user-facing profile
variant list declared in code and set in no file means the selection logic
composes an empty name, the override is never applied, and the preset UI never
appears. Recipe: ST-07.
Gates: every profile section is reachable from the registry; tier assignment is
asserted by a dump, not read from comments; unknown hardware has a documented
fallback; every override hook has a real caller and real data.
---
## 7. Requested versus effective quality
A settings screen showing the user's request while a device profile clamps the
result is lying by omission. Model it explicitly:
```text
requested quality → platform clamp → effective quality → applied variables
```
Expose both, and give a reason when they differ.
### Unit-test every clamp at its range boundaries
The measured defect worth guarding against: a compatibility check comparing a
quality **level** (0–3) against a resolution **percentage** (default 100), so the
constraint can never fire. Its sibling function used the correct accessor, which
is what makes this findable — and what makes it invisible to review, since both
lines read identically. Recipe: ST-09.
Also check every normalisation for a zero denominator. A remap between two
ranges divides by their difference; a configuration that makes those equal is
reachable from data. Recipe: ST-10.
---
## 8. World content lifetime
### Level instances, not second worlds
Background and preview content is usually a level instanced into the **current**
world, not a second world. The cost is duplicated level content in one scene:
actors, components, primitives and their render resources.
Measure by scene composition — actor and primitive counts, visible material slots
— not by counting world contexts. "There is only one world" is true and
irrelevant.
### Ownership rules
Every streamed-in instance has one owner and an unload path; async handles are
retained and cancellable; failure surfaces a reason rather than a permanent wait;
teardown runs on travel, mode change and error.
### Hidden render targets
Scene-capture preview systems allocate render targets per capture object and may
keep rendering state resident between captures. Budget them explicitly: count ×
resolution × format, and verify release. A subsystem with no callers that still
instantiates per world and installs a ticker is paying for a feature nobody uses
— unused systems must decline creation.
### If runtime layer state is never set, it is not runtime streaming
Shipping editor-authored initial state is a valid choice. Describing it as
dynamic streaming is not, and a team that believes it has a reference
implementation will look for one that does not exist.
---
## 9. Review checklist
- [ ] Four budgets separated in every claim and every report.
- [ ] Pool settings never presented as consumption.
- [ ] Variable priority documented per quality knob, verified by a source dump.
- [ ] Tier overrides complete, or partial with a stated reason.
- [ ] Memory-clamp policy explicit per platform.
- [ ] Mesh and texture pool split decided per platform.
- [ ] Read-only variables identified before anyone tries to tune them.
- [ ] Every profile section reachable; assignment asserted, not commented.
- [ ] Requested and effective quality both visible, with a reason on divergence.
- [ ] Clamp logic unit-tested at both ends of its real range.
- [ ] Level instances owned, released and counted.
- [ ] Preview and capture render targets budgeted and released.
- [ ] Unused subsystems decline creation.
---
## 10. Measurement plan
Static first — cheap, and finds the structural problems:
1. Dump effective variables **with their source** per device profile, and diff
against intent.
2. Verify every profile section is reachable from the registry.
3. List texture groups and their per-tier caps.
4. Search for runtime streaming and layer-state calls to confirm what is
actually dynamic.
Runtime, on target hardware, in a packaged build:
- streaming stat for pool budget versus used and over-budget;
- platform GPU profiler for real video memory — the streaming stat cannot see
most of it;
- **fixed camera poses** for any comparison; view-dependent metrics are otherwise
noise;
- one run per tier on real devices, not emulated profiles alone;
- scene composition counts for level-instance cost.
**Do not compare numbers taken at different camera poses, different tiers, or
between editor and packaged builds.** Those comparisons look quantitative and
mean nothing.
---
## Provenance
The findings come from a configuration and source audit of Epic's Lyra Starter
Game on Unreal Engine 5.6, cross-read against the engine's own scalability and
device-profile configuration. Source addresses stay in the research archive that
produced this skill; each entry carries a stable identifier (`ST-01`, `ST-02`, …)
resolving back to the audited location there.
## Evidence boundary
Static configuration analysis only: no profiler was run, no device was measured,
and no number in the failure modes is a memory measurement. Engine defaults
quoted describe one engine version. The recipes are written to be re-run against
your own project and engine, which is the only way any of these values become
true for you.
@@ -0,0 +1,388 @@
# Failure modes: streaming and platform budgets
Ten ways a platform budget is configured, reviewed, approved and inert.
The shared property here is different from the rest of this bundle, and it is the
reason this file exists: **configuration cannot fail.** A line in an ini file
that names an unreachable section, a variable set at a priority nothing can
override, a clamp that compares two different units — none of these produce an
error, a warning, or a log line. They produce a game that runs, on hardware that
was budgeted for, using numbers nobody set.
A second property: **the instrument lies more often than the code.** Most wrong
conclusions in this area come from reading a pool setting as consumption, or a
scalability level as an outcome. Several recipes below check the instrument
rather than the project.
Recipes use `rg` from a project root and were executed against the audited
project while this file was written. Runtime recipes name the console command,
because static analysis genuinely cannot answer some of these.
---
## Priority and arbitration
### ST-01 - A value set at a priority nothing can override
**Mechanism.** A device profile sets a streaming variable. Device-profile
priority outranks scalability priority, so every later quality change — including
every in-game setting — is silently discarded for that variable.
**Why it is silent.** Both halves work exactly as designed. The profile applies
its value; the settings system applies its value; the higher priority wins and
nobody is told. The setting's UI shows the user's choice, which is stored
correctly and simply has no effect.
**Why the obvious check misses it.** Reading either file shows a sensible value.
Reading both shows two sensible values with no visible conflict, because priority
is not expressed in the files at all — it is a property of the mechanism that
applied them. And the same setting **does** work on a tier where the profile is
silent, so "it works on my device" is true and useless.
**Symptom.** A quality setting that works on some hardware and not on other
hardware, with no pattern the player or the tester can see.
**Detect.** Ask the running game who set the value — this cannot be done
statically:
```text
DumpCVars # every variable with its SetBy source
r.Streaming.PoolSize # prints the value and its source
```
Then find the static side and compare coverage:
```bash
rg -n "r\.Streaming\.PoolSize" Config/
rg -n "^\[TextureQuality@" Config/ | wc -l
```
In the audited project the pool is set in exactly two places — the mid tier of
each mobile platform — and the project declares **no** per-tier texture-quality
sections at all, so every other tier inherits the engine's values. One setting,
opposite behaviour across tiers.
**Guardrail.** Document the intended source per quality variable, and assert it
in a startup dump on each platform. If a value must be immutable, say so in the
settings UI rather than accepting the write and dropping it.
---
### ST-02 - The top quality bucket detaches the pool from real memory
**Mechanism.** The highest quality buckets disable the clamp that limits the
streaming pool to available video memory, while automatic detection promotes
hardware into those buckets on a compute benchmark that never considers memory
size.
**Why it is silent.** Both decisions are defensible alone. Disabling the clamp on
the top bucket assumes a machine with plenty of memory; a compute benchmark is a
reasonable proxy for GPU class. The combination targets exactly the hardware the
assumption is wrong about: a fast chip with modest memory.
**Why the obvious check misses it.** The clamp lives in engine configuration and
the thresholds live in project configuration. Each is reviewed by a different
person, in a different file, at a different time. Neither file mentions the
other.
**Symptom.** Stutter and driver-level eviction on mid-range cards, at the quality
tier automatic detection chose for them. Reproduces on hardware the team does not
own, because the team's machines have enough memory for the assumption to hold.
**Detect.** Read the thresholds, then read what the top bucket does to the clamp:
```bash
rg -n "PerfIndexThresholds_TextureQuality" Config/
rg -n "LimitPoolSizeToVRAM|r\.Streaming\.PoolSize" \
"<engine>/Engine/Config/BaseScalability.ini"
```
Confirm on hardware: set the quality level manually and watch the pool.
```text
sg.TextureQuality 3
stat streaming # pool size, required pool, over budget
```
**Guardrail.** Include a memory term in the promotion thresholds, or keep the
clamp enabled on the top bucket. Test the top bucket on a low-memory card before
shipping the thresholds.
---
### ST-03 - Meshes and textures share a budget nobody split
**Mechanism.** Mesh streaming is enabled, and the separate mesh pool is left at
its default, which means one shared pool.
**Why it is silent.** Sharing is a supported configuration and works. Textures and
geometry simply compete inside one ceiling, and the streamer resolves the
competition by lowering mips — which looks like a texture problem.
**Why the obvious check misses it.** Two separate settings, one enabled in a
renderer section and one absent from every platform except mobile. Nothing
connects them, and the absent one has a default whose meaning ("share") is only
documented in the engine's variable description.
**Symptom.** Texture quality degrading on scenes with heavy geometry, on
platforms where nobody configured a mesh budget.
**Detect.** Check both values on every platform, and check where each was set:
```bash
rg -n "r\.MeshStreaming" Config/
rg -n "r\.Streaming\.PoolSizeForMeshes" Config/
```
In the audited project mesh streaming is enabled globally in the renderer
settings while the mesh pool is configured only for mobile — so desktop and
console share one pool by default.
**Guardrail.** Set both explicitly per platform, even when the value equals the
default. An explicit default is a decision; an absent one is an assumption.
---
### ST-04 - A read-only variable treated as a runtime knob
**Mechanism.** Several streaming variables are marked read-only: their value is
captured at startup and influences what is cooked. Setting them later — from a
console, a device profile, or a settings screen — is accepted and ignored.
**Why it is silent.** The write succeeds. Nothing rejects it, nothing warns, and
the variable reads back as unchanged, which is easy to miss when you are looking
at frame time rather than at the variable.
**Why the obvious check misses it.** The variable has a name, a value and a
description like any other. Its flags are in engine source, and nothing in the
project's configuration indicates the difference.
**Symptom.** An A/B comparison of a "setting" that produces two identical
configurations, and therefore a confident conclusion drawn from noise.
**Detect.** Check the flags before tuning anything:
```bash
rg -n -B2 -A6 "r\.MeshStreaming|CoarseMeshStreaming" \
"<engine>/Engine/Source/Runtime/Engine/Private/ContentStreaming.cpp" \
| rg "ECVF_ReadOnly|ECVF_Scalability"
```
At runtime, the same question is one command: print the variable and confirm the
value changed after you set it.
**Guardrail.** Maintain a short list of the build-level streaming decisions in
your project, next to the platform matrix. Anything on that list is changed in a
build, never in a session, and never compared mid-session.
---
## Reachability
### ST-05 - A profile section that nothing can select
**Mechanism.** A device profile section is defined with a full set of values, and
no registry entry or matching rule leads to it.
**Why it is silent.** The file parses, the section is well formed, and the values
inside it are correct. It is simply never chosen, so it produces no behaviour to
be wrong.
**Why the obvious check misses it.** Reviewers read sections, not the registry.
A tier section that sits beside three reachable siblings looks exactly like them
— same shape, same variables, same care — and the difference is a missing line in
a list at the top of the file, or an absent mapping in engine configuration.
**Symptom.** A platform that never reaches its top tier, with per-tier values
that were reviewed, approved and never applied.
**Detect.** Diff declared profiles against defined sections:
```bash
rg -n "DeviceProfileNameAndTypes" Config/DefaultDeviceProfiles.ini
rg -n "^\[\w+ DeviceProfile\]" Config/DefaultDeviceProfiles.ini
```
Every defined section must be either declared here or present in the engine's
base profiles. In the audited project one platform's top tier is defined in full
and appears in neither list — every value in it is unreachable.
Confirm on hardware, which is the only authoritative answer:
```text
dumpdeviceprofile # the active profile and its variables
```
**Guardrail.** Assert reachability in a test that parses both lists. A profile
that cannot be selected should fail the build, not be discovered on a device.
---
### ST-06 - Section headers that contradict the assignment
**Mechanism.** Comment banners group device sections under tier labels. The
sections beneath name a different base profile.
**Why it is silent.** Comments do not execute. The devices get the tier their
base profile names, which is a real and consistent assignment — just not the one
the banner announces.
**Why the obvious check misses it.** The banner is the fastest way to read a
600-line configuration file, and it is the wrong instrument. Checking means
reading the base profile line of every device section, which is exactly the work
the banner appears to save.
**Symptom.** Device tier assumptions in planning documents that do not match
runtime. Performance reports grouped by a tier the devices are not in.
**Detect.** Ignore banners; extract the pairs:
```bash
rg -n -A2 "^\[\w+ DeviceProfile\]" Config/DefaultDeviceProfiles.ini \
| rg "DeviceProfile\]|BaseProfileName"
```
Read the output as a table. In the audited project several devices sitting under
a high-tier banner resolve to the low tier.
**Guardrail.** Generate the device-to-tier table from configuration rather than
maintaining banners, and put the generated table in the performance
documentation.
---
### ST-07 - An override list that no configuration populates
**Mechanism.** A platform settings object declares a list of user-facing profile
variants. Selection logic composes a profile name from that list. No
configuration file ever sets it.
**Why it is silent.** The composition handles an empty list gracefully: the
resulting name is empty, no matching profile is found, and the override is not
applied. The engine's startup selection stays in effect, which is correct
behaviour and looks like intent.
**Why the obvious check misses it.** The code is complete and reads well —
fallback chains, refresh-rate filtering, logging. Confirming the absence means
searching every platform's configuration for a property that is never mentioned
in the source that consumes it.
**Symptom.** A console-style quality preset UI that never appears, because it is
only added when the variant list has more than one entry. The feature is
"implemented" and has never run.
**Detect.** Separate reads from writes:
```bash
P='UserFacingDeviceProfileOptions'
rg -n "\b$P\b" Source/ Config/ Platforms/
```
Hits only in a declaration and in read sites — with nothing in any configuration
file — is the finding. In the audited project the property is read in three
places and set in none.
**Guardrail.** For every data-driven selection list, add a startup log naming its
size, and treat zero as a warning. The logging line already exists in most such
systems; make someone read it.
---
## Configuration that cannot take effect
### ST-08 - Settings for a delivery mechanism the project does not have
**Mechanism.** Texture groups configure optional mip levels, which are cooked
into a separate chunk. The project has no chunk installation and does not acquire
missing chunks on load.
**Why it is silent.** The settings are valid, the cook honours them, and nothing
at runtime asks for the chunk that would contain the extra mips. Everything
behaves as if the settings were absent.
**Why the obvious check misses it.** The group configuration is reviewed as a
block of texture settings, where the value is plausible. The dependency on a
delivery mechanism is two systems away, in packaging configuration nobody reads
during a texture pass.
**Symptom.** A team believes it has a high-resolution optional tier. It does not,
and the belief survives because nothing contradicts it.
**Detect.** Check the consumer of the mechanism, not the producer:
```bash
rg -n "OptionalMaxLODSize|OptionalLODBias" Config/
rg -n "bShouldAcquireMissingChunksOnLoad|bBuildHttpChunkInstallData" Config/
```
Optional mips configured while chunk acquisition is disabled is the finding.
**Guardrail.** Configuration that depends on a delivery mechanism gets a comment
naming that mechanism, and a check in the packaging review. Better: remove it
until the mechanism exists.
---
## Clamps
### ST-09 - A clamp comparing two different units
**Mechanism.** A compatibility check obtains a **resolution percentage** and
compares it against a **quality level**. The percentage defaults to one hundred
and the level ranges to three, so the comparison is always satisfied.
**Why it is silent.** Both values are integers, both accessors exist, and the
comparison is well typed. The constraint simply never binds, which is
indistinguishable from a constraint that is never violated.
**Why the obvious check misses it.** The line reads correctly. The two accessor
names differ by one word, the sibling function two hundred lines away uses the
correct one, and nothing about either line suggests a unit mismatch. This is a
type error that the type system cannot express.
**Symptom.** A frame-rate and quality compatibility rule that never fires, so a
platform can select a combination the rule exists to prevent.
**Detect.** Find clamps and check the accessor against the value being compared:
```bash
rg -n -B4 -A4 "GetApplicable\w*Limit\(" Source/ | rg "Quality|Resolution|Limit"
```
Read each hit for unit agreement. Then unit-test every clamp at both ends of its
real range — the test that would have caught this takes three lines.
**Guardrail.** Give the two ranges distinct types, or name the accessors so a
mismatch is visible in one line. Test each clamp with a value that must be
rejected; a clamp with no failing test case has never been shown to work.
---
### ST-10 - A remap with a reachable zero denominator
**Mechanism.** A normalisation maps a value between two ranges by dividing by the
difference between a maximum and a minimum. Nothing guarantees they differ.
**Why it is silent.** For every configuration anyone has tried, they differ. The
division is correct and the result is correct.
**Why the obvious check misses it.** The formula is standard and reads as
obviously right. Reachability of the degenerate case depends on data — a device
profile setting a limit equal to the scale minimum — and data is not reviewed with
the arithmetic.
**Symptom.** An infinity or a not-a-number propagating into a quality value on
one device configuration, producing behaviour that is hard to attribute and
impossible to reproduce elsewhere.
**Detect.** Find range normalisations and check their guards:
```bash
rg -n -B3 -A1 "\) / \(\w+ - \w+\)|/ \(Max\w+ - Min\w+\)" Source/
```
Any division by a difference with no preceding equality check is the finding.
**Guardrail.** Guard the denominator and decide the degenerate result
deliberately. Then check whether data can actually produce it — and if it can,
validate the data too.
@@ -0,0 +1,202 @@
# Patterns: platform budgets in a measured reference product
A worked reading of one shipped platform-budget configuration: scalability
buckets, device profile tiers, streaming pools and the code that applies them.
This file is almost entirely about **configuration**, which makes it unlike the
others in this bundle in one important way: none of the findings below are bugs
in the usual sense. Every value is valid, every file parses, and the game runs.
What the audit produced is a list of places where the configuration says
something the runtime does not do — and the only reason any of it is findable is
that someone read the engine's own configuration alongside the project's.
Markers: **[measured]** — read in project or engine configuration and source;
**[derived]** — conclusion from measured facts; **[open]** — requires hardware.
**No number here is a memory measurement.** Nothing was profiled. Everything is
a declared budget, which is exactly the distinction the skill is about.
---
## 1. The single most useful table in the audit
What the project declares about the texture streaming pool, per mobile tier
**[measured]**:
| Tier | Pool declared by the project | Effective source |
|---|---:|---|
| Low | — | engine scalability bucket |
| **Mid** | **85 MB** | **device profile** |
| High | — | engine scalability bucket |
| Epic | — | engine scalability bucket |
The project sets the pool in exactly two lines — the mid tier of each mobile
platform — and declares **no** per-tier texture-quality sections of its own
**[measured]**. Every other tier inherits engine values, which on this engine
version run from several hundred megabytes to a gigabyte, with the
video-memory clamp disabled at the top **[measured]**.
**[derived]** Two consequences, and the second is the one that matters:
1. The tier with the smallest declared budget is the only tier the project
actually configured. The rest are engine defaults that nobody chose.
2. Because a device profile applies at a higher priority than scalability, the
mid tier's pool **cannot be changed by the in-game texture setting**, while
on every other tier the same setting works. One control, opposite behaviour,
no explanation anywhere in the UI.
That asymmetry is the reason ST-01 leads the failure modes. It is not a mistake
in either file; it is a mistake that only exists between them.
---
## 2. Priority, in one paragraph you can act on
Device profiles apply variables whose names carry the scalability prefix at
**scalability** priority, and every other variable at **device-profile**
priority — which outranks everything the settings system does **[measured, from
engine source]**.
**[derived]** So the same file produces two completely different mutability
outcomes depending on a naming convention:
- a prefixed quality bucket set in a profile is a **default** the player can
override;
- an unprefixed variable set in a profile is a **lock**.
Nothing in the ini syntax distinguishes them. The audited project uses both
forms, mostly correctly and once consequentially (§1).
**The practical rule:** you cannot determine effective configuration by reading
files. Dump the variables with their source on the target device. That command is
the only authoritative answer, and it takes ten seconds.
---
## 3. Automatic detection promotes on the wrong axis
Promotion thresholds are expressed against a GPU performance index, for every
quality group, with **no memory term anywhere** **[measured]**. The texture group
reaches its top level at a low index value, and that top level is the one where
the engine disables the video-memory clamp on the pool **[measured]**.
**[derived]** A fast chip with modest memory therefore lands in the bucket that
grants the largest pool and removes the guard against it. The two decisions were
made in different files by different reasoning, and each is defensible alone.
The project's own comment on the thresholds is honest about their intent — tuning
so a specific generation of hardware lands in a specific mix **[measured]** — and
that intent is about throughput, which is exactly the axis that misses this.
Recipe: ST-02.
---
## 4. Dead configuration, and how much of it there is
Four independent instances, all silent, all reviewed at some point:
| Finding **[measured]** | What it means |
|---|---|
| A top-tier device profile section defined in full, absent from the profile registry and from the engine's base profiles | Every value in it is unreachable |
| Comment banners grouping devices under tier labels that contradict the base profile named two lines below | The documentation and the behaviour disagree; several devices sit under the wrong banner |
| A user-facing profile variant list read in three places and set in no configuration file | The profile override never applies; the console preset UI never appears |
| Optional mip levels configured in every mobile texture group, with chunk acquisition disabled | A high-resolution optional tier that cannot be delivered |
**[derived]** The common structure is worth naming, because it generalises far
beyond streaming: **configuration that names a mechanism the project does not
have.** It reads as a decision, survives review indefinitely, and produces
nothing. It is the configuration-file form of the stub described in
`ue-reference-project-adoption`.
Recipes: ST-05, ST-06, ST-07, ST-08.
---
## 5. Two arithmetic defects in the clamp layer
**A comparison between a percentage and a level.** A frame-rate compatibility
check obtains a resolution-quality limit — a percentage defaulting to one hundred
— and compares it against an overall quality level in the range zero to three
**[measured]**. The comparison is always satisfied, so the constraint never
binds.
**[derived]** What makes this findable rather than invisible is that the sibling
function two hundred lines away uses the correct accessor for the same purpose
**[measured]**. One of the two is wrong, and the codebase contains its own
counterexample.
**A remap with no guard on its denominator.** A normalisation between two
resolution ranges divides by the difference between a maximum and a minimum, with
no check that they differ, and the degenerate case is reachable from device
profile data **[measured]**.
Recipes: ST-09, ST-10. Both are three-line fixes and both would be caught by one
unit test each — which is the argument for testing clamps at their range
boundaries rather than at their typical values.
---
## 6. What the project got right
Worth stating, because the list above is long and the configuration is not bad:
1. **Cook-time budget levers are used deliberately.** Mobile texture groups clamp
the maximum size sixteen-fold on a side, animation key stripping is enabled,
and unused detail-mode components and emitters are pruned at cook
**[measured]**. These are budget-A decisions, correctly separated from
runtime budgets.
2. **Foliage density was moved between scalability groups deliberately**, with the
old group's keys removed and a different curve applied in the new one
**[measured]**. That is a real understanding of how the groups compose.
3. **Per-device resolution overrides on specific hardware** rather than a blanket
platform value **[measured]**.
4. **Dynamic resolution configured for one mobile platform** with an explicit
comment stating the target frame rate and output resolution **[measured]** —
the kind of comment that makes a number reviewable.
5. **A rich set of runtime diagnostics** already wired: the profile selection
logs its inputs and its chosen result, frame pacing logs its target, quality
clamping logs both the before and after values **[measured]**. Someone
arriving on a device has the answers available.
**[derived]** Point 5 deserves emphasis. Most of the findings in this file are
detectable in ten seconds on a device by reading log lines the project already
prints. The failure is not instrumentation; it is that nobody read the output.
---
## 7. What to copy, and what to check first
Copy: the four-budget separation as a documentation convention; cook-time levers
used per platform; the diagnostic logging around profile selection and clamping;
per-device overrides for outlier hardware.
Check first, in this order, on your own project:
1. **Dump variables with their source** on each target. Everything else is
inference. (ST-01)
2. **Diff declared profiles against defined sections.** One command. (ST-05)
3. **Separate reads from writes** for every data-driven selection list. (ST-07)
4. **Unit-test every clamp** at both ends of its range. (ST-09, ST-10)
5. **Check both pool values** — texture and mesh — on every platform. (ST-03)
**[derived]** Steps 1 and 2 take minutes and would have found the two largest
findings in this file. That ratio is the argument for doing configuration audits
at all.
---
## Provenance
Measured against Epic's Lyra Starter Game on Unreal Engine 5.6, read as project
configuration and source, cross-referenced against the engine's own scalability
and device-profile configuration and streaming source in the same installation.
Source addresses and engine paths stay in the research archive that produced this
skill; each `ST-` identifier resolves back to the audited location there.
## Evidence boundary
**Nothing here was profiled.** No device was measured, no memory figure is an
observation, and every quantity is a declared budget or a configured threshold.
Engine values describe one engine version and will drift. The recipes exist
precisely because these numbers must be re-derived on your project and your
hardware before they mean anything.