pub struct TierConfig {
pub tier: Tier,
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,
}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.
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.
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.
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.