A 3D browser game has no minimum system requirements. The same link opens on a gaming desktop, an office laptop with integrated graphics, a Chromebook and a five-year-old phone, and the player decides in the first ten seconds whether it runs well. A settings menu with "Low, Medium, High" does not help, because most players never open it. This devlog shows how our balloon game Billowpost chooses its graphics level on its own: a first guess from the GPU's name, a short measurement while the title screen is showing, a resolution that adjusts during play without freezing, and a level remembered for the next visit. The numbers are from our tests on real hardware.
#What the three levels change
Before deciding automatically, decide what each level means. In Billowpost the big costs are the number of pixels, the shadows and the post-processing, so the levels change exactly those:
| Setting | High | Medium | Low |
|---|---|---|---|
| Pixel ratio | up to 2 | up to 1.25, times 0.9 | up to 1, times 0.8 |
| Pixel budget | none | capped | capped lower |
| Sun shadows | 2048 map, redrawn every 3rd frame | 1024 map, every 4th frame | off |
| Post-processing | bloom, colour grade, vignette, 2x multisampling | none, tone mapping only | none |
Two of those ideas are worth copying even without any automatic system. The scenery does not move and the balloon moves a few centimetres per frame, so the shadow map does not need redrawing every frame: switching off shadowMap.autoUpdate and setting needsUpdate every few frames removed the biggest cost on integrated graphics. And on medium and low the scene renders straight to the screen with the renderer's own tone mapping, which saves three full-screen passes and a large half-float render target.
#A first guess from the GPU's name
WebGL can tell you which GPU it runs on. The game creates a throwaway context, reads the renderer string through the WEBGL_debug_renderer_info extension, and releases the context at once:
static gpuName() {
try {
const c = document.createElement('canvas');
const gl = c.getContext('webgl2') ?? c.getContext('webgl');
if (!gl) return '';
const ext = gl.getExtension('WEBGL_debug_renderer_info');
const name = String(ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER));
gl.getExtension('WEBGL_lose_context')?.loseContext();
return name.toLowerCase();
} catch {
return '';
}
}
Then a few regular expressions turn the name into a starting level. Software renderers such as SwiftShader or llvmpipe start on low; so do older phone GPUs and Intel's HD and UHD Graphics, the chips in most office laptops and Chromebooks. Iris, other integrated and mobile GPUs start on medium, and everything else on high. One refinement came from testing: Intel HD and UHD start on medium when the window is about 1366 by 768 or smaller, because there they hold 60 frames a second, and on low only on bigger screens.
Treat the name as a hint, never as the truth. Some browsers report a generic or simplified name to reduce fingerprinting, and an empty name falls back to medium. Whatever the guess, the next step measures.
#Measure on the title screen
The title screen of Billowpost is the game itself: the balloon drifts over the first valley with the menu on top. That makes it a perfect benchmark that the player does not notice. Auto skips the first two seconds, while the scenery is built and shaders compile, then averages three one-second samples of the frame rate. Below 50 frames a second it steps one level down and measures again; otherwise the calibration is done and the level is stored together with the GPU's name:
if (mode === 'title' && !titleCalDone) {
if (++titleSec <= 2) return; // warming up: ignore
titleFps.push(fps);
if (titleFps.length < 3) return;
const avg = titleFps.reduce((a, x) => a + x, 0) / titleFps.length;
titleFps = [];
if (avg < AUTO_SLOW && autoQ !== 'low') {
autoQ = autoQ === 'high' ? 'medium' : 'low';
stage.setQuality(autoQ, w, h);
titleSec = 0; // measure the new level from scratch
} else {
titleCalDone = true;
}
settings.autoLearned = { q: autoQ, gpu: stage.gpu };
writeSave(data);
}
On the next visit the game starts directly at the remembered level, as long as the GPU name still matches. When the name changes, for example on a laptop that switches between integrated and dedicated graphics, the game guesses and measures again.
#During play: change resolution, not scenery
Frame rate changes during play: a busy town, a big cloud bank or another program in the background. Switching quality levels in the middle of play would be the wrong answer, because a new level rebuilds materials, and compiling shaders freezes a frame for a noticeable moment. So during play Auto only changes the render resolution, which is instant:
- after three seconds in a row below 48 frames a second, the render scale drops by 10 percent, down to 0.76 of the level's resolution;
- after eight seconds in a row at 58 or more, it climbs back by about 6 percent, up to full resolution;
- if the scale is already at its floor and play is still slow, a lower level is queued and applied at the next calm moment: the pause menu, the results screen or the title.
The asymmetry is deliberate. Dropping fast and recovering slowly prevents the resolution from pumping up and down every few seconds, which is more distracting than a slightly softer picture. Hidden tabs and manual settings switch the whole mechanism off.
#What the numbers showed
We measure with a script that opens the built game in a headless browser on a real GPU, flies the balloon and logs frames per second for each region and level. On an Intel HD 630, a typical office laptop GPU, low held 59 to 60 frames a second at 1366 by 768 and 53 to 58 at 1920 by 1080. An earlier review on that class of chip found medium at 28 to 31 frames and low at 47 to 57, which is why those chips started on low then.
The most useful finding was not about 3D at all. Profiling showed that the heads-up display, drawn on a 2D canvas, was repainting every frame even when nothing on it had changed, and on weak laptops that turned out to be the real cost. The fix was a key built from everything the display shows, rounded to what the eye can see; the canvas is repainted only when the key changes. After that change, the HD and UHD chips held 60 frames a second on medium at laptop resolutions, and the starting rule above was raised accordingly. Measure before you cut the pretty parts.
#A simpler version for smaller games
Not every game needs all of this. Our arena game Faultwarden uses a lighter rule: it counts frames in four-second windows during play and steps one level down after two slow windows in a row below 42 frames a second, never because of a single hitch. For a game with short rounds and a small scene, that is enough. The parts worth keeping in any version: define what each level changes, judge sustained frame rates rather than single frames, never rebuild during action, remember the result, and always leave a manual setting for players who know better.
For the basics of 3D in the browser see browser games with Three.js and WebGL, and for keeping the rules independent of the frame rate, a fixed-timestep game loop in JavaScript. Billowpost is free and listed as Coming soon in the PN Scripts games catalogue.
#Frequently asked questions
Can a web page find out which GPU a player has?
Usually, through the WEBGL_debug_renderer_info extension of WebGL. Some browsers return a generic or simplified name for privacy, so use it as a starting hint and measure the real frame rate.
Why not switch quality levels during play?
A new level rebuilds materials and compiles shaders, which freezes the game for a moment. Change only the render resolution during play and apply a new level at a pause, a menu or a results screen.
What frame rate should Auto aim for?
We step down below 50 frames a second on the title screen and treat 58 or more as room to spare. Judge averages over several seconds, never a single slow frame.
What costs the most on integrated graphics?
In our measurements: the number of pixels, real-time shadows and full-screen post-processing, and, surprisingly, a 2D interface canvas repainted every frame. Cap the pixel ratio, update shadows every few frames and repaint the interface only when it changes.
Comments
Comments
Be the first to leave a comment.
Leave a comment