Skip to content
debugasyncPublic

About

adaptive quality for roblox. measures client frame time and scales the systems you register to hold a target frame rate.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Governor

Adaptive quality for Roblox. It watches the client's frame rate and quietly turns your own systems up or down to hold a target, so the game runs smooth on a cheap phone without the player ever opening a settings menu.

The reason this needs to exist is that Roblox's graphics quality is a single 1-to-10 slider, and it only knows about engine stuff, shadows, textures, render distance. It has no idea your game spawns 200 NPCs or throws a particle storm on every kill, so a Chromebook eats the exact same effect load as a desktop and just chokes on it. You can't even read back what "Automatic" landed on, which means you can't react to it either. Governor comes at it from the other direction: you describe your systems, and it picks the level each one runs at based on what the device can actually keep up with.

how it works

Every frame it drops the delta into a ring buffer, and once a second it pulls the 95th percentile frame time out of it. I use a percentile instead of an average on purpose, because an average smooths right over the stutters, and the stutters are exactly the thing that makes a game feel bad.

From there it's simple. Under target, it drops one tier on the highest-priority system that still has room to fall. Over target with headroom to spare, it raises one tier on the lowest-priority system that still has room to climb. After any change it wipes the samples and sits on its hands for a cooldown, since the old samples were measuring the tier it just replaced and acting on them again is how you get it oscillating forever.

Two things sit on top of that. A single frame slower than PanicFrameTime skips the whole once-a-second cadence and drops immediately, which is what you want when someone detonates half the map and the frame rate falls off a cliff. And when two neighbouring tiers straddle the target, one a hair too slow, one a hair too fast, it would happily ping-pong between them, so after a system gets dropped it's blocked from climbing back for a bit, and if a raise gets immediately undone that block doubles each time. It settles on the lower tier and shuts up. That last part took a couple tries to get right.

use

local Governor = require(game.ReplicatedStorage.Governor)

Governor.Configure({ TargetFrameRate = 60 })

Governor.Register({
    Name = "Particles",
    Priority = 3,
    Tiers = {0, 0.5, 1},
    Apply = function(Value)
        ParticleScale = Value
    end
})

Governor.Register({
    Name = "NpcUpdateRate",
    Priority = 2,
    Tiers = {10, 30, 60},
    Apply = function(Value)
        NpcHertz = Value
    end
})

Governor.Register({
    Name = "Tracers",
    Priority = 1,
    Tiers = {false, true},
    Apply = function(Value)
        TracersEnabled = Value
    end
})

Governor.Prime()
Governor.Start()

Tiers go cheapest first, so index 1 is the floor and the last index is the richest. Apply runs with the tier value and its index every time the tier changes, including once when you register.

Priority decides who moves first. Higher priority gets dropped before lower and restored after it, so put your particles high and your gameplay-critical stuff low.

Prime sets an opening tier off the device, mobile at the floor, console in the middle, desktop up top. It's worth calling because otherwise a phone crawls all the way down from max one interval at a time while the player watches it stutter.

persistence

Snapshot hands you a table of system name to tier index, and Restore takes one back. Save a settled snapshot to a datastore and restore it next join and the client picks up where it left off instead of reconverging from scratch every session.

local Saved = Governor.Snapshot()

Governor.Restore(Saved)

pause

Pause stops the sampling and the decisions, Resume clears the buffer and starts again after a cooldown. Frame time during a loading screen or a cutscene has nothing to do with gameplay, so you don't want it poisoning the measurements.

Governor.Pause()
Governor.Resume()

stability

Once nothing has changed for StableAfter seconds it calls itself stable. OnStable fires when it gets there, OnUnstable when something knocks it back out.

Governor.OnStable(function()
    print("settled at", Governor.GetFrameRate())
end)

manual override

SetTier forces a system to a tier, and Lock takes it out of the automatic pool so your choice sticks. Handy if you're building a real settings menu on top and want the player's picks to win.

Governor.SetTier("Particles", 3)
Governor.Lock("Particles", true)

api

call does
Configure(Options) override config
Register(Definition) add a system, returns it
Unregister(Name) remove a system
Clear() remove every system
Start() begin sampling and deciding
Stop() disconnect
Prime() set opening tiers from device, returns the device kind
Step() run one decision by hand
SetTier(Name, Tier) force a tier
GetTier(Name) returns tier index and value
Lock(Name, Locked) exclude from automatic changes
Snapshot() table of system name to tier index
Restore(Snapshot) apply a snapshot
GetSystems() array of registered names
Each(Callback) iterate name, tier, value, locked
Pause() stop sampling and deciding
Resume() continue after a cooldown
IsPaused() boolean
IsStable() boolean
OnStable(Callback) fires on entering the stable state
OnUnstable(Callback) fires on leaving it
GetFrameRate() current percentile frame rate, nil until enough samples
Stats() table of Rate, MinRate, MaxRate, Changes, Uptime, Idle
OnChanged(Callback) fires on every tier change
Report() printable state summary

Config keys: TargetFrameRate, Interval, Cooldown, Percentile, Headroom, Window, PanicFrameTime, StableAfter, RaiseBackoff, RaiseBackoffMax.

limitations

  • It's client only, since it's measuring render frames. Nothing to measure on the server.
  • It can only move what you register. It can't touch the built-in graphics slider, that property isn't writable from a script, which is half the reason this thing had to exist.
  • It doesn't actually know which system caused a drop, only that the budget got missed. It leans entirely on the Priority numbers you give it to decide what to sacrifice, so if those are wrong it'll drop the wrong thing.
  • One change per interval, except the panic path. So a scene that's way over budget still takes a few seconds to fully climb down.
  • A tier only helps if that system's cost actually changes between tiers. Register something whose tiers all cost the same and it'll get dropped for nothing when the budget's tight.

license

MIT

About

adaptive quality for roblox. measures client frame time and scales the systems you register to hold a target frame rate.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages