Skip to content

DeviceBench

DeviceBench answers “how fast is this device” during the loading screen so quality stops being one-size. Three time-boxed Luau workloads measure the client on three axes — Compute, Churn, Resume — each expressed as a continuous 0.25..4 multiplier against a mid-range reference (1.0). Decimals are valid quality values; there are no tier cliffs. The defining decision: budgets are funded per axis — each knob scales by the workload that pays its cost, so a device that benched 3.4× on churn but 1.2× on compute gets near-max particle budgets while bone chains stay modest.

Everything here is a client measurement. Results drive cosmetics only and never touch server authority.

Each axis runs a small chunked workload in a time box and reports operations per second:

Axis Workload What it funds
Compute 4,000-iteration Vector3/math loop per chunk — simulation-ish throughput Bone chains, bone cull distance, projectile visuals
Churn 400 four-field table allocations per chunk — allocation pressure and GC cost Particle budgets, VFX density — instance/VFX spawning
Resume 200 coroutine create/resume pairs per chunk — how quickly the client’s threads actually switch Audio channels — scheduling, mixing, concurrent fades

Time-boxed, not fixed-work: run splits BudgetMs (default 90) evenly across the three workloads and each runs as many chunks as fit in its slice. A weak device runs fewer chunks in the same wall time instead of hitching longer — the bench costs ~90ms on every device.

Each measured rate divides by its reference rate to give the axis multiplier, clamped to 0.25..4. The Score is the raw geometric mean of the three ratios (unclamped); Quality is the score clamped into the band. The geometric mean tempers a lopsided device: 8× on one axis with 1× on the other two scores ∛8 = 2, not 3.3.

Low/Medium/High/Ultra remain as a coarse ladder over the continuous scale, mainly for pick():

Tier Quality threshold As a uniform quality (profile("Tier"))
Low >= 0 0.5
Medium >= 0.6 1
High >= 1.2 1.5
Ultra >= 2 2.5

The first run() caches its Result module-wide; every later call returns it (Force = true re-measures). That is what lets consumers call DeviceBench.profile() lazily without re-benching — and why the bench should run once, deliberately, during the loading screen rather than implicitly at first use.

Pair with Preload: asset warming is network-bound, the bench uses the idle CPU.

-- Client, during the loading screen
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Kernel = require(ReplicatedStorage.ChloeKernel).boot()
local DeviceBench = require(ReplicatedStorage.ChloeKernel.DeviceBench)
local Bench = DeviceBench.run({ BudgetMs = 90, Bus = Kernel.Bus })
print(Bench.Quality) -- continuous 0.25..4, decimals allowed (1 = mid-range reference)
print(Bench.Axes) -- e.g. { Compute = 2.1, Churn = 3.4, Resume = 1.6 }
print(Bench.Tier, Bench.Platform) -- "High", "Desktop"

run is synchronous for BudgetMs — it never yields unless you also pass Frames = N, which additionally samples N Heartbeats for a median FrameMs.

profile() turns axes into concrete budgets. Pass nothing for the benched device, a Result, a plain quality number (profile(1.35) is a real point, not a rounded tier), or a tier name:

local Quality = DeviceBench.profile() -- computed from the benched axes
Emitter.Rate = BaseRate * Quality.VfxDensity
Lighting.GlobalShadows = Quality.ShadowsEnabled
local Handcrafted = DeviceBench.profile(1.35) -- exact quality point
local Coarse = DeviceBench.profile("High") -- uniform 1.5 on every axis
-- Log-mapped: min at 0.25, midpoint at 1.0, max at 4.0 — every doubling of
-- device quality buys the same slice of the range
local ViewDistance = DeviceBench.scale(80, 500)
-- Tier-keyed choice; sparse tables fall to the nearest tier below, then above
local MaxRagdolls = DeviceBench.pick({ Low = 4, High = 16 })
DeviceBench.governor({
TargetFps = 60,
Bus = Kernel.Bus, -- publishes Device.QualityChanged(quality, profile)
OnChange = function(quality, profile)
applyQuality(profile)
end,
})

The governor connects to Heartbeat and watches p95 frame times in 1-second windows. Sustained misses (p95 above 1.05 × the frame target for 3s) multiply the effective quality by 0.8; sustained headroom (p95 below 0.7 × the target for 10s) multiplies it by 1.15 — granular nudges, not tier jumps. One hitch is not a quality change, and the asymmetric timers prevent flapping: the band between breach and headroom resets both. The effective quality is clamped between the floor (0.25) and the benched quality — a lucky frame window can never promote past measured capability. Governor profiles scale the benched axes together, so axis ratios survive: a churn-strong device dialed down still favors its particles.

Framework client modules default from the profile when unconfigured. Every default is an option you can override:

Consumer Knob Default from
BonePhysics MaxDistance (cull) profile().BoneDistance
BonePhysics MaxChains profile().BoneChains
AudioKit MaxChannels profile().AudioChannels
ProjectileClient MaxVisuals profile().ProjectileVisuals
Dissolve MaxPoints scale(500, 5000)

The caps are purely cosmetic — a culled bone chain freezes at its pose, an over-budget sound is the least important one playing. Projectiles add a fairness rule on top: MaxVisuals only decides who gets the full visual (trails, particles); overflow renders as a minimal pooled tracer, and a shot on course to pass within ThreatRadius (default 15 studs) of the local character always renders full. No quality level ever hides an incoming round from its target.

Member Description
DeviceBench.run(options?) → Result Runs (or returns the cached) benchmark. Synchronous for BudgetMs; Frames > 0 additionally yields that many Heartbeats for a median frame-time sample
DeviceBench.axes(measures) → Axes Pure: per-axis multipliers from raw rates, clamped 0.25..4
DeviceBench.score(measures) → (score, tier) Pure: unclamped geometric mean of the reference ratios, plus its tier

RunOptions:

Option Default Description
BudgetMs 90 Total bench time, split evenly across the three workloads
Frames 0 Heartbeat frames to sample for FrameMs (yields); 0 skips
Force false Re-measure instead of returning the cached result
Clock os.clock Injectable for specs
Bus nil Publishes Device.Benchmarked(result)

Result: Score (raw, unclamped), Quality (clamped 0.25..4), Axes { Compute, Churn, Resume }, Tier, ComputePerSecond, ChurnPerSecond, ResumePerSecond, Platform ("Desktop" | "Mobile" | "Console" | "Unknown" — console via ten-foot interface, mobile via touch-without-keyboard), FrameMs? (median, only when Frames were sampled).

Member Description
DeviceBench.profile(value?) → Profile Budgets from a Result (per-axis), a 0.25..4 number (uniform), a tier name (uniform), or nil (benched device — runs the bench if never run). Returns a fresh table every call
DeviceBench.quality(result?) → number The overall clamped multiplier for the benched (or given) device
DeviceBench.scale(min, max, result?) → number Log2-mapped knob: 0.25 → min, 1.0 → midpoint, 4.0 → max; every doubling buys the same slice
DeviceBench.pick(byTier, result?) → T? Tier-keyed choice; sparse tables fall to the nearest tier below, then above
DeviceBench.tier(result?) → string The coarse tier name

Profile — the 1.0 base each axis multiplies, and the formula per knob:

Field Base at 1.0 Funded by Notes
Quality all Geometric mean of the (clamped) axes
ViewDistance 200 studs overall Rounded
VfxDensity 0.6 Churn 0..1 emit-rate scalar, capped at 1
ParticleBudget 150 Churn Max concurrent particles
ShadowsEnabled overall true at overall >= 0.9
PostFx overall Bloom/DoF/atmosphere extras; true at overall >= 1.4
BoneChains 10 Compute Minimum 1
BoneDistance 80 studs Compute BonePhysics cull default
AudioChannels 24 Resume Minimum 4
ProjectileVisuals 40 Compute Minimum 8; caps full cosmetics only (fairness rule above)
Member Description
DeviceBench.governor(options?) → Governor Starts holding the frame target (Heartbeat-connected unless Manual)
governor.Quality / governor.Tier Current effective quality and its coarse tier
governor:profile() → Profile Budgets at the effective quality — benched axes scaled together, ratios preserved
governor:sample(dt) Manual mode: feed frame deltas yourself
governor:stop() Disconnects the Heartbeat connection

GovernorOptions:

Option Default Description
TargetFps 60 The frame target the governor holds
Result cached/auto-run bench The capability ceiling
Manual false No Heartbeat connection; drive sample(dt) yourself
Clock os.clock Injectable for specs
Bus nil Publishes Device.QualityChanged(quality, profile)
OnChange nil (quality, profile) on every shift

Pacing constants: 1s evaluation windows on p95; breach at > 1.05 × frame target sustained 3s× 0.8; headroom at < 0.7 × target sustained 10s× 1.15; clamped to [0.25, benched quality].

Topic Payload Fired
Device.Benchmarked result: Result run() completed, when a Bus was passed
Device.QualityChanged quality: number, profile: Profile The governor shifted the effective quality, when a Bus was passed