Progressive enhancement means shipping a working baseline first, then layering on richer visuals only while the device can actually afford them. This simulator drives that decision with a real control loop instead of a fixed device whitelist.
Each simulated frame:
raw = tierCost[tier] * stress + noise
ema = alpha*raw + (1-alpha)*ema (alpha = 0.2)
budget = 1000 / targetFPS
If ema > budget: overCount++, underCount = 0
Else if ema < 0.7*budget: underCount++, overCount = 0
Else: (dead zone — no change)
If overCount >= window: tier = max(tier-1, 0); overCount = 0
If underCount >= 2*window: tier = min(tier+1, 4); underCount = 0
- EMA smoothing — a single slow frame doesn't trigger a downgrade; only a sustained trend does, which is what real frame-time monitors (Android's
Choreographer, iOS's CADisplayLink) do before reacting.
- Asymmetric hysteresis — downgrading needs window consecutive over-budget frames, upgrading needs 2×window consecutive comfortably-under-budget frames. Dropping quality should feel instant when the device struggles; raising it again should be cautious, or the tier oscillates every few seconds (visible if you set the window very low).
- Device stress slider — models background CPU contention, thermal state or an underpowered device; the throttle button injects a temporary ~3× cost spike, like the OS momentarily stealing the core for a GC pause or another app.
- Ladder — five real enhancement tiers, from a plain-text baseline up to full particle/lighting effects, each with a real relative render cost. The 3D scene mirrors the state machine: shells of instanced particles appear or disappear as the tier changes, never the other way around.
Real-world relevance: this is the same shape of logic behind Android's PowerManager thermal API, WebGL "quality preset" auto-detection, and React Native's frame-drop-driven animation throttling — measure, smooth, and only then decide, with hysteresis to avoid visibly flickering between quality levels.