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:
2026-09-05 23:48:55 +07:00
parent 80ea7b03a9
commit ecd87ac96d
67 changed files with 21630 additions and 0 deletions
@@ -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.