Cookbook
Performance tuning
Three dials carry almost all of the cost: how much matter there is, how many device pixels it is drawn into, and how much the engine is allowed to give up when a device cannot keep pace. This page measures each one rather than asserting it.
Shipped · measured at build timedensity — the pool is 130 × density, field-wide
density is a particle-count multiplier over a base of 130. The number to
internalise is that this is a field-wide count, not a per-area one: the pool
does not grow because your field is large, and does not shrink because it is small.
That single fact explains the most common contained-field complaint. A card-sized field still
holds 130 particles spread across the whole volume, so each body gathers less and
--d reads low. The fix is to raise density for a small field, not
lower it — see contained fields.
JS ✓ Swift ✓ Kotlin —
dprCap — the backing-store ceiling
The effective device-pixel ratio is min(devicePixelRatio, dprCap). On a 3× display
an uncapped field fills nine times the pixels of a 1× one for the same CSS box, which is why
the shipped budget caps it at 2. Lower it before you start cutting matter: it costs
sharpness, not behaviour.
// The backing store is the dominant fill cost on a retina display.
// Effective DPR is min(devicePixelRatio, dprCap).
const field = createField(canvas, { host, dprCap: 1.5 });
field.setDprCap(1); // and it is runtime-settable JS ✓ Swift ✓ Kotlin ✓
qualityTier — the degradation ladder
A tier is a coarse, reversible statement of how much the engine may give up: 0 full,
1 effects reduced, 2 minimal, 3 paused. At tier 2 and above the engine
caps the effective DPR and drops the heatmap on its own levers. The
QualityGovernor detects sustained overruns and tells you which tier to ask for;
<field-root> wires that loop for you, and a raw createField
does not.
import { QualityGovernor } from '@fundamental-engine/dom';
// The governor DETECTS sustained overruns; you decide what to do about them.
// <field-root> wires this for you. On a raw createField you own the loop:
const gov = new QualityGovernor(16.67);
function frame(t) {
const tier = gov.feed(t); // 0 full · 1 effects reduced · 2 minimal · 3 paused
if (tier !== null) field.setQualityTier(tier);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame); setQualityTier is
write-only — the handle exposes no property to read the tier back, as the
example below measures. The Swift and Kotlin handles both publish one. The support row is
generated from the parity matrix, so it cannot drift from the ports:
JS — Swift — Kotlin ✓ the read-back property — not on JS Judging a configuration before it ships
inspectBudget(counts) compares a configuration against the shipped budget and
returns one finding per breach — no field required, so it runs in a unit test or a build step.
apps/site/src/lib/cookbook/tuning.ts // PERFORMANCE TUNING — the three dials that actually move the cost.
//
// density the particle-count multiplier. The pool is `130 × density`, rounded — a FIELD-WIDE
// count, NOT a per-area one. (This is why a small contained field reads a low `--d`:
// the same 130 particles are spread across the whole volume, so a card-sized field
// needs a HIGHER density, not a lower one.)
// dprCap the device-pixel-ratio ceiling. Effective DPR is `min(devicePixelRatio, dprCap)`;
// the backing store is the dominant fill cost on a retina display.
// qualityTier the degradation ladder: 0 full, 1 effects reduced, 2 minimal, 3 paused. The engine
// caps DPR and drops the heatmap at tier 2+ on its own levers, reversibly.
//
// `inspectBudget(counts)` judges a CONFIGURATION against the shipped budget before you ship it.
import { createField, headlessHost, seededRng, inspectBudget, DEFAULT_BUDGET } from '@fundamental-engine/core';
export interface TuningResult {
/** the pool at density 1 — the `130 × density` rule, measured. */
particlesAtDensity1: number;
/** …and at 0.25: a quarter of the matter, a quarter of the integration cost. */
particlesAtQuarter: number;
/** the shipped budget's particle ceiling. */
budgetParticles: number;
/** a deliberately over-budget configuration, judged before it ships. */
findings: { field: string; value: number; limit: number; over: number }[];
/** JS takes the tier but exposes NO read-back property for it — `setQualityTier` is write-only
* here, where the Swift and Kotlin handles both publish a `qualityTier` property. */
tierReadableBackOnJs: boolean;
}
function poolAt(density: number): number {
const host = headlessHost({ width: 1200, height: 800 });
const field = createField(undefined as unknown as HTMLCanvasElement, {
host,
render: 'none',
density,
rng: seededRng(29),
});
host.tick();
const n = field.particleCount();
field.destroy();
return n;
}
export function runTuning(): TuningResult {
const particlesAtDensity1 = poolAt(1);
const particlesAtQuarter = poolAt(0.25);
// judge a configuration against the shipped budget — no field required
const findings = inspectBudget({ particles: 900, bodies: 40, dprCap: 3 }).map((f) => ({
field: String(f.field),
value: f.value,
limit: f.limit,
over: f.over,
}));
// a sustained-overrun response: the governor detects, you forward the tier, the engine adapts
const host = headlessHost({ width: 1200, height: 800 });
const field = createField(undefined as unknown as HTMLCanvasElement, {
host,
render: 'none',
density: 1,
dprCap: 2,
rng: seededRng(31),
});
field.setQualityTier(2); // what QualityGovernor.feed() would hand you on a slow device
host.tick();
// Measured, not assumed: the JS handle carries no `qualityTier` property to read it back from.
const tierReadableBackOnJs = 'qualityTier' in field;
field.destroy();
return {
particlesAtDensity1,
particlesAtQuarter,
budgetParticles: DEFAULT_BUDGET.particles,
findings,
tierReadableBackOnJs,
};
} - particlesAtDensity1
- 130
- particlesAtQuarter
- 33
- budgetParticles
- 600
- findings
- [{"field":"particles","value":900,"limit":600,"over":300},{"field":"dprCap","value":3,"limit":2,"over":1}]
- tierReadableBackOnJs
- false
130 particles at density 1 and 33 at 0.25 — the rule, measured on a live pool rather than quoted. The findings array is a real over-budget verdict, and the tier read-back is false on JS.
The order to turn them
- Measure first.
createFieldPerfgives you fps, frame budget, percentiles and dropped frames from your own rAF timestamps. - Cap the pixels.
dprCapcosts sharpness only. - Then cut matter.
densitychanges how the field feels; do it deliberately. - Let the governor handle the bad device. A tier is reversible; a hard cutoff is not.