pub struct TierConfig {Show 19 fields
pub tier: Tier,
pub grid_scale: GridScale,
pub post_cap: (u32, u32),
pub bloom_levels: u32,
pub attractor_particles: u32,
pub attractor_particles_live_ceiling: u32,
pub attractor_particles_offline_ceiling: u32,
pub attractor_trail_cap: (u32, u32),
pub swarm_particles: usize,
pub emitter_objects: usize,
pub mesh_grid: (u32, u32),
pub max_segments: usize,
pub collage_elements: usize,
pub field_iterations: u32,
pub cellular_radius: u32,
pub cellular_grid: u32,
pub plexus_points: u32,
pub plexus_edges: u32,
pub max_coc_px: u32,
}Expand description
The capacity values a tier sets. Resolved once at renderer construction and read at construction/reconfigure time only — never branched on per frame.
Fields§
§tier: TierWhich tier these values are, so a demotion and the overlay have one thing to read rather than a parallel field to keep in step.
grid_scale: GridScaleThe fraction of the render target both internal grids are drawn at
(GridScale, ADR-0245) — the one value here that is not the tier’s
alone.
The constants below carry GridScale::FULL; the renderer replaces it
with resolve_grid_scale’s answer for its own adapter and pin whenever
it takes a config, so a TierConfig a renderer holds names the scale its
grids were actually built at. It rides here rather than beside the struct
because both grids are built from a TierConfig already, and a rebuild
that took the tier and forgot the scale is then not writable.
post_cap: (u32, u32)Cap on a post stage’s internal grid (ADR-0034), width then height.
The floor value is NFR §12 memory arithmetic, redone for the linear-light
composite (Plan 0045 Phase 3 / ADR-0046). Every intermediate upstream of
the tonemap is now COMPOSITE_FORMAT — 8
bytes/texel, not 4 — so a stage offscreen costs twice what the surface
format would charge, while the trails accumulation
(PingPongField, two textures) was
already float and did not move.
Per chain, both stages live, at this cap (1920x1080, 8 bytes/texel = 16.6 MB a texture):
| buffer | before | after |
|---|---|---|
| trails composited | 8.3 | 16.6 |
| trails accumulation (x2) | 33.2 | 33.2 |
| kaleidoscope source | 8.3 | 16.6 |
| per chain | 50 | 66 |
Plan 0023’s dual-live dissolve holds two whole PostChains, so the peak is
~133 MB rather than ~100. Outside the chains the frame carries one more
surface-sized float buffer beyond the chains — the tonemap’s input, 16.6
MB, the one allocation ADR-0046 genuinely adds — plus the blend’s
snapshot/live pair at 16.6 MB each while a dissolve runs (8.3 at 8-bit),
and ink’s 8.3 MB input, which stays 8-bit because the tonemap hands it
display-referred pixels. Worst case — dual-live, both stages, ink on — is
~191 MB against NFR §12’s ~350 MB soft ceiling, which is mostly driver
floor already.
At the rich cap (2560x1440) the same arithmetic is ~118 MB per chain and ~236 MB dual-live, up from ~88 and ~177 — the trade ADR-0034 priced and declined at floor budgets and the rich tier takes.
This cap is the relief lever if the float chain misses NFR §1 on a floor-tier iGPU: lower it rather than re-fixing the grids (ADR-0046), since bandwidth roughly doubled with the format and the grid policy is shared.
Bloom adds to this only when a preset switches it on (Plan 0045
Phase 4), and it is two allocations, not one. Its pyramid is two
textures per level, each level a quarter of the last, so the pyramid
converges to 2 * (1/4 + 1/16 + …) ≈ 2/3 of one grid-sized texture — ~11 MB
at this cap. On top of that the stage owns its own grid-sized bloom-src
offscreen, a full 16.6 MB at this cap, because a PostStage reads its
input from a texture it owns. So the stage costs 16.6 + ~11 ≈ 28 MB on
top of the ~66 MB per chain above, and ~55 MB in the dual-live worst case —
which is what NFR §12’s table charges and what the ~246 MB worst case there
is computed from. It is charged only against presets that bind
bloom_amount, since an inactive stage builds nothing.
bloom_levels: u32How many levels deep the bloom pyramid goes
(Bloom, ADR-0046).
This is a capacity, not a look: each level doubles the halo’s reach and costs three passes at a quarter of the previous level’s area, so the tail is cheap in pixels and not free in passes. The floor runs four (a halo reaching ~16 of the grid’s texels at the default radius); rich runs six, which is where the widest levels start to matter on a 1440p-class grid.
level_sizes clamps this down on a small render target, so
a value here is an upper bound rather than a promise.
attractor_particles: u32The attractor’s sample budget at REFERENCE_PX — the anchor of the
density law, not the count drawn (ADR-0140).
attractor_budget scales this by target_px / REFERENCE_PX and clamps
it between this value and one of the two ceilings below, so a target at or
under the reference draws exactly this many and a larger one draws more.
What a preset then draws out of that budget is
round(budget * density) (ADR-0069), which is a different and smaller
number again.
State is 48 bytes each (Particle), and the
real ceiling is additive-blend fill rate, which is why the floor value
was described as the number to validate against the 60 fps @ 1080p floor
(ADR-0015 Risks).
This is a sample count and not a brightness: the additive draw divides
its deposit by the active count
(deposit_scale, ADR-0065), so
raising it buys a smoother figure rather than a brighter one. Changing it
changes shot noise and cost; it does not change exposure.
attractor_particles_live_ceiling: u32The largest budget attractor_budget may resolve for a scene drawing
into a live surface — a window, or the plugin’s host surface.
Frame-time bound, and it is the number that keeps the law from spending a
display’s whole budget on sample count. It is also the allocation: the
particle buffer is sized here at construction and never resized, so a
resize changes the active count and rebuilds no GPU resource. That costs
ceiling * 48 B of GPU storage plus the same again for the CPU seed
scatter the scene holds for re-upload, in every window, whether or not
the target is large and whether or not an attractor preset is loaded
(create_all builds every scene up front).
§Where these numbers come from
Measured, not chosen — Plan 0128 Phase 1, at 1920x1080 on
attractor_leviathan, counts interleaved in one process so a throttling
laptop GPU could not read as signal.
Rich: 600 000, four times the anchor. On the midrange-discrete
reference NFR §1 calibrates Rich against, the rule was the largest swept
count whose marginal p99 over today’s anchor stays inside 10 % of the
16.67 ms budget: 600 000 reads +1.454 ms (8.7 %) and the next step up,
1 200 000, reads +4.703 ms (28.2 %).
Floor: its own anchor, so the law is a no-op there. On integrated
hardware — the baseline NFR §1’s floor commitment is about — 1080p at
Floor already sits on the 16.67 ms budget at today’s 50 000 (p99
16.854 ms), and the law’s own 1080p Floor value of 450 000 takes it to
31.942 ms. NFR §1 promises Floor “values exactly the pre-tier engine’s”;
this is what keeps that true at every target size.
attractor_particles_offline_ceiling: u32The same ceiling for a headless render — shot --render, where there
is no present deadline and no governor, and the only bound is memory.
§Where these numbers come from
The bound is the device’s storage-buffer binding limit, not process
memory, because it is reached first: at 48 B a particle, wgpu’s default
max_storage_buffer_binding_size of 134 217 728 B holds 2 796 202
particles, and 5 400 000 — the law’s own unclamped 4K value — fails
outright with Buffer binding 0 range 259200000 exceeds max_*_buffer_binding_size limit 134217728 (Plan 0128 Phase 1).
Rich takes 2 700 000, the largest whole multiple of its anchor under
that wall — 18x, 129.6 MB, 3.4 % of headroom. Floor takes the same
multiple rather than the same number, so a tier still means something
offline: at one shared ceiling --tier floor --render and
--tier rich --render would draw an identical count at 4K.
attractor_trail_cap: (u32, u32)Upper bound on each axis of the attractor’s trail accumulation grid.
The floor is the ceiling Plan 0027/0029 chose for a high-DPI display while keeping the worst case bounded on the iGPU: every frame pays a decay pass plus the full additive instance draw over this grid, so cost scales with its area. Rich lifts it to 4K so a 4K or ultrawide display sizes near 1:1 instead of degrading to a uniform upscale.
swarm_particles: usizeHow many particles the swarm simulates.
Plan 0043 left the floor value at an unmeasured iGPU cost (+0.5 ms per
frame of depth math on the dev box, and not fill rate), which is why the
plans index calls this a live tier candidate rather than a settled
constant. If the on-device floor check misses, this is the lever — on the
floor value, and that routes back through architect.
emitter_objects: usizeHow many objects the emitter’s pool holds (ADR-0057).
Unlike every other count here this is a ceiling on a varying
population, not the population: the emitter spawns and retires, so a
preset’s spawn_rate * lifetime decides how many objects are actually
alive and this decides how many can be. Spawns past it are dropped
rather than queued or allocated for — that is the phase’s whole real-time
hazard — so raising it does not brighten a preset that never reaches it,
and lowering it below one that does thins the shower rather than changing
its motion.
Not an accumulating count in the sense the module docs warn about: each
object is one sprite drawn once per frame, so the light in the frame is
population * brightness and the population is a preset’s own arithmetic.
The tier only says where that arithmetic is cut off.
The floor holds a shipped preset’s shower with room to spare (the emitter family runs a few hundred objects live); rich triples it for the denser looks a discrete GPU can carry. Cheap either way — see NFR §12: the pool and its instance buffer are well under a megabyte at both tiers.
mesh_grid: (u32, u32)Ceiling on the warp mesh’s grid, in cells, width then height (Plan 0100 Phase 1).
A capacity in the strictest sense: the grid is a resolution for the
per-vertex program, not a shape (ADR-0037), so raising it refines the
warp’s spatial detail and changes nothing about what the scene draws. The
vertex count is (x + 1) * (y + 1), and every one of those vertices costs
one evaluation of each [per_vertex] binding on the render thread,
which is what this bounds.
The upper bound either tier may name is the .milk format’s own —
meshx <= 128, meshy <= 96 — so a converted preset’s requested grid is
representable at the top of the range and clamped below it.
warp_mesh::clamp_grid is the one
place the clamp happens, shared by the loader and the scene.
§Where these numbers come from
Measured, not chosen — Plan 0100 Phase 1’s done-when. The rule it set was: raise the grid until one frame of per-vertex evaluation costs more than 1 ms — 6 % of the 16.67 ms NFR §1 commits to at 1080p — and cap the floor one step below.
mesh_cost_by_grid in scenes/warp_mesh/tests.rs is the measurement and
prints the ladder on every run. Taken 2026-08-16 on the development box
(Windows 10, desktop CPU, --release), evaluating a four-binding
[per_vertex] program of the shape a real preset writes — two runs,
agreeing to about 1 %:
grid vertices per frame share of 16.67 ms
16x12 221 0.036 ms 0.2 %
32x24 825 0.129 ms 0.8 %
48x36 1 813 0.280 ms 1.7 %
64x48 3 185 0.488 ms 2.9 % <- Floor
72x54 4 015 0.616 ms 3.7 %
80x60 4 941 0.755 ms 4.5 %
88x66 5 963 0.909 ms 5.5 % <- Rich
96x72 7 081 1.081 ms 6.5 % <- the bar is crossed here
112x84 9 605 1.483 ms 8.9 %
128x96 12 513 1.918 ms 11.5 %The bar is crossed between 88x66 and 96x72, so 88x66 is the
largest grid the rule admits and it is what Rich takes. The format’s own
maximum is therefore refused: at 1.92 ms it is 11.5 % of the frame on
a desktop CPU, which is not a number any tier should spend on one
parameter surface. The grid is lowered because it did not measure clean,
which is the whole of the rule.
Floor sits a step further down than the rule alone would put it, and
deliberately. The rig above is a desktop CPU; NFR §1’s floor tier
targets a ~2015 iGPU-class machine whose single-thread performance this
box does not model, and this is CPU work on the render thread, so a
slower machine pays proportionally more of a budget it is already
struggling to hold. 64x48 is 2.9 % here and leaves room for that
machine to be several times slower before the surface is a problem.
When the floor tier is next exercised on real target hardware this is the constant to re-measure, and the ladder prints exactly what that needs.
max_segments: usizeThe one capacity value here that a preset can see: past it geometry is truncated, and ADR-0007 requires that be surfaced rather than silently cut. So a preset whose mirror pushes over the floor cap reports an overflow at the floor and not at rich — the message is the tier’s most visible edge for the content lane, which is why shipped presets are authored against the floor.
collage_elements: usizeCap on how many flat elements a shape_collage canvas may hold
(ADR-0123).
The one capacity here that bounds a per-pixel loop, which is what
makes it load-bearing rather than a memory number. Every other count in
this struct bounds work paid once per particle, per vertex or per
segment; this one bounds work every fragment pays, so a frame costs
elements x pixels and ADR-0123 prices the bounding-box reject alone —
before anything is drawn — at roughly 6N operations per pixel. The
buffer is irrelevant at any value either tier would take: 64 bytes an
element, so 128 elements is 8 KB.
§Where the floor value comes from
Measured, then decided by a human — Plan 0113 Phases 2 and 3.
core/tests/collage_cost.rs sweeps the count on hardware, prints
the ladder on every run, and its module docs own the readings and
the trap in quoting them. Two tables are not interchangeable:
the pre-roster ladder is what the Phase 3 gate read, and the
post-roster table is what a canvas costs today — the eight-kind
roster made the loop cheaper, because rings, sectors and checker
patches shade far less of their own bounding box than a quad
does.
40 is the reference set’s own top, not a budget line. The
gate was a look judgement and the cost was not the binding
constraint: the user’s working density is 8 to 14 elements,
which on the system as shipped costs 8.2 % of a 60 Hz frame at
eight and 10.7 % at sixteen, and denser canvases were rejected
on sight long before they were rejected on cost. The ceiling is
the densest canvas in ADR-0123’s roster, Kandinsky’s On White
II, counted at just above 40 once its lines and arcs are
included — so this value sits exactly on that canvas, and a
collage_onwhite needing a forty-first element moves this
number rather than being quietly truncated.
Rich is provisional in the sense every RICH value
is — see that constant’s own note.
§It clamps, and it does not yet say so
shape_collage::applied_count holds a bound count to this value
silently, unlike max_segments, which
ADR-0007 requires surface an overflow. That was harmless while the cap
sat far above any authored canvas and is not harmless now that it sits on
one. Recorded as a followup on Plan 0113 rather than fixed there: the
surfaced channel is CapOverflow,
whose context enum is shared with the line scenes, so widening it is an
architect call.
field_iterations: u32The most iterations an analytic_field escape-time preset may follow an
orbit for (ADR-0045, ADR-0180 rule 3).
The one value here that can change a preset’s picture, which is
why it is a cap on what the preset asks for rather than a count the
tier chooses. An escape-time boundary at 48 iterations and at 512 is not
the same image, where a particle cloud at two densities is. So a preset
asks for iterations, this bounds it, and a preset asking within the
Floor value renders identically on both tiers. One that asks for more
is clamped and says so, through the same per-frame overflow the
segment cap surfaces through
(OverflowContext::Iterations),
which the standalone reports on the transition exactly as it reports a
tier demotion.
§Where the numbers come from
Arithmetic, not a measurement. It is a fullscreen per-pixel loop, so
the worst frame is one whose every pixel runs the whole budget: 1080p is
2.07 M pixels, and one step of z^2 + c plus its escape test is about a
dozen floating-point operations, so 64 iterations is ~1.6 GFLOP a frame.
A ~2015 integrated GPU — the baseline NFR §1’s floor is about — peaks at a
few hundred GFLOPS, which puts that frame at single-digit milliseconds at
peak and roughly two to three times that in practice: inside the 16.67 ms
budget with the composite still to pay for. 128 would sit on the budget.
Rich takes the top of iterations’ own declared range.
When the floor tier is next exercised on real target hardware this is a constant to measure, on the heaviest escape-time frame: a zoom whose whole view is the set’s interior.
cellular_radius: u32The widest neighbourhood a cellular larger_than_life preset may ask
for, as a radius in cells (ADR-0045, ADR-0180).
Like field_iterations this is a cap on what
the preset asks for, because a radius is content: the same rule at two
radii grows two different worlds, where a particle count at two densities
is one. A preset asking within the Floor value runs identically on both
tiers.
§Where the numbers come from
Arithmetic, not a measurement. The neighbourhood sum is separated — a
row pass and a column sum — so one generation costs 2 * (2r + 1) + 1
texel reads a cell rather than (2r + 1)^2. At Floor‘s 6 that is 27
reads; on a 512-cell grid at 60 generations a second, 425 M reads a
second, a few percent of what a ~2015 integrated GPU — the baseline NFR
§1’s floor is about — fetches, and the grid and rate both have to sit at
their tops to reach it. Rich takes the top of radius’ declared range,
10: 43 reads, 2.7 G a second at a 1024 grid and the same rate. Unseparated,
radius 10 would be 441 reads a cell — ten times that.
Floor is 6 rather than the whole range because the default rule (at
radius 5) fits under it with one to spare, and a floor
machine pays for every step past it at every generation.
When the floor tier is next exercised on real target hardware this is a constant to measure, at the tier’s own grid cap and 60 generations a second.
cellular_grid: u32The largest [cellular] grid a preset may run on, in cells a side
(ADR-0045).
A cap on content, like cellular_radius,
and it is clamped and announced rather than silently reduced: a
pattern is a fixed number of cells, so a grid clamped from 1024 to 512
draws every glider twice as large. A preset asking within the Floor
value runs identically on both tiers; one asking past it runs on the cap
and says so at load, through the cap overflow the standalone prints
(OverflowContext::Grid).
§Where the numbers come from
Arithmetic, not a measurement, from the same read count
cellular_radius states. Floor’s 512 is the
grid that figure is quoted at — 262 144 cells, 425 M texel reads a
second at Floor’s radius and 60 generations a second — and its state
textures are 4 MB of the ping-pong pair and the row counts. Rich takes
the loader’s own ceiling, 1024: four times the cells, 16 MB.
When the floor tier is next exercised on real target hardware this is
a constant to measure, with cellular_radius, on one frame.
plexus_points: u32The most points a plexus preset draws (ADR-0257), holding its
[plexus] points at load.
A cap on content, clamped and announced at load
(OverflowContext::Points):
fewer points is a sparser network, not the same one cheaper.
The graph is built by testing every pair, so the per-frame cost is
quadratic in this: N (N - 1) / 2 distance checks. Floor’s 600 is
180 thousand checks a frame and Rich’s 1500 is 1.1 million.
§Where the numbers come from
Floor’s three plexus caps were measured together (Plan 0235 Phase
6): a preset at 600 points with links saturating the edge cap, nodes on
and an aperture past the blur cap costs under 4 ms a frame at 1080p on an
integrated GPU, which leaves the frame budget its composite. Rich’s are
the same proportions scaled, not measured.
plexus_edges: u32The most edges a plexus frame draws (ADR-0257): the size of its
instance buffer, and so the fill the graph can cost.
A frame whose graph links more pairs than this draws the first this many
in index order and announces the rest
(OverflowContext::Edges).
Measured with plexus_points.
max_coc_px: u32The largest circle of confusion a 3D primitive is blurred by, as a radius in pixels (ADR-0257).
A blurred stroke costs fill in proportion to how far it is widened, so
this is the lever that keeps a wide-open aperture inside the frame
budget: past it a line stops spreading, and an operator on a lower tier
sees a shallower blur rather than a slower frame — and is told so
(OverflowContext::Blur).
Measured with plexus_points.
Implementations§
Source§impl TierConfig
impl TierConfig
Sourcepub const RICH: Self
pub const RICH: Self
The midrange-discrete tier.
These are provisional multipliers, not measurements. Plan 0044 Phase 4 runs the standalone pinned here on the target GPU at native fullscreen across the heaviest preset of each family and records the frame times; the values that ship are the ones that hold the display rate. A number that misses gets lowered, and no number here is invented upward to look good. Until that phase closes, treat every field below as a starting point.
Sourcepub const fn with_grid_scale(self, scale: GridScale) -> Self
pub const fn with_grid_scale(self, scale: GridScale) -> Self
These values with their grids drawn at scale.
Source§impl TierConfig
impl TierConfig
Sourcepub fn attractor_budget_live(&self, target_px: u32) -> u32
pub fn attractor_budget_live(&self, target_px: u32) -> u32
attractor_budget against this tier’s live ceiling — what a window
and the plugin’s host surface resolve.
Sourcepub fn attractor_budget_offline(&self, target_px: u32) -> u32
pub fn attractor_budget_offline(&self, target_px: u32) -> u32
attractor_budget against this tier’s offline ceiling — what a
headless render resolves, where the bound is memory rather than frame time.
Trait Implementations§
Source§impl Clone for TierConfig
impl Clone for TierConfig
Source§fn clone(&self) -> TierConfig
fn clone(&self) -> TierConfig
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for TierConfig
impl Debug for TierConfig
Source§impl Default for TierConfig
impl Default for TierConfig
Source§impl PartialEq for TierConfig
impl PartialEq for TierConfig
Source§fn eq(&self, other: &TierConfig) -> bool
fn eq(&self, other: &TierConfig) -> bool
self and other values to be equal, and is used by ==.impl Copy for TierConfig
impl Eq for TierConfig
impl StructuralPartialEq for TierConfig
Auto Trait Implementations§
impl Freeze for TierConfig
impl RefUnwindSafe for TierConfig
impl Send for TierConfig
impl Sync for TierConfig
impl Unpin for TierConfig
impl UnsafeUnpin for TierConfig
impl UnwindSafe for TierConfig
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.