● S4E WebGPU wasm32 OPFS

Settings

A settings screen with nothing behind it. Move a fader, press SAVE, close the tab — and the value is still there when you come back, kept in this origin's own private filesystem by the same code that writes a config file on a desktop.

options — crcbl
Starting…
Click the canvas first. A canvas has to hold keyboard focus to receive keys, and browsers will not start audio until you have interacted with the page. Clicking away pauses the game and lets go of whatever key was held — no platform sends the release for a key that was down when focus left.
Touch reaches the engine. A contact arrives as an ordinary pointer, and the first one is also the gesture a browser waits for before it will start audio. Whether this demo can be played with one is in its controls below — not all of them can. Switching tabs pauses the game and lets go of whatever was held: no platform sends the release for an input that was down when focus left.

controls

The panel is the demo, so it is never dismissed.

Click the canvas first — a canvas has to hold focus to receive keys, and letting go of it pauses the loop.

The F3 panel has a settings file section: the file as it stands, one row per key, in the file's own tables — so a key none of these rows owns is still there to read.

SAVE says where it went. RESET puts every bus back to 100%, takes the frame cap off, puts the anisotropy back to the engine's own and the render scale back to the whole frame, switches every effect on, and leaves the file alone until you save it — a reset you did not mean to keep is one reload away from being undone.

why a screen with no game under it

Because a setting that does not survive a restart is not a setting.

The engine has had a layered settings stack for a long time: an on-disk TOML file under the player's own config directory, layers that override each other in a defined order, and readers for every key the renderer and the mixer understand. What it had never had was an application writing one. The only writer was a command line, so the code that keeps a player's choices had been exercised by everything except a player.

This sample is that player. The six faders are the audio buses — master, music, effects, interface, voice and ambience — and each is a real key in the file, written the moment you move it and read back the next time the page opens.

They are not linear. A fader is a square law, so the middle of the groove is a quarter of the amplitude rather than half of it, which is roughly how a halving sounds. The percentage beside each row is the gain that reaches the file, not the position of the handle.

And you can hear them. Three of the six buses carry something: a looping tone on music, a tick once a second on effects, and a click on the interface bus every time a fader passes a detent. They are told apart by behaviour rather than by timbre, so the bus you are pulling down is the one you can identify by ear — and master takes all three with it. Voice and ambience carry nothing and their rows say (silent), because a control that does nothing is worse than one that admits it.

QUALITY, FRAME CAP, ANISOTROPY, ANTIALIASING, RENDER SCALE and the six effect switches are the [engine.video] keys on the screen and the only rows here that do not apply as you change them: the loop takes its frame ceiling once, when it is built, and a renderer takes its anisotropy, its antialiasing, its scale and its effect set when it opens over a scene — and this screen draws none. QUALITY sits above the rest and writes a whole tier at once, so the rows below it move together and then say custom the moment you disagree with one of them. So each row says (next start) until a reload has come up on the value you chose — which is the same honesty the silent buses get, applied to time instead of to sound. FRAME CAP goes one further: a ceiling above the rate this build asked for reads held to the rate it actually runs at, since a settings file may only hold a game down and never lift it.

what is actually running

The native sample, with the storage swapped underneath it.

Start-up is polled across several frames, because requestDevice is a promise and the main thread cannot block on it. The frame loop is driven by requestAnimationFrame out here rather than by a loop inside the engine. And the clock is the browser's, because wasm32-unknown-unknown has no Instant.

The fourth difference is this demo's subject. A browser tab has no config directory, so the same call that resolves ~/.config/options/ on Linux resolves the Origin Private File System here, and the save is queued rather than written — the page drains the queue every frame. Where no store is installed at all, the screen says so instead of pretending: a settings screen that silently forgets is the worst version of this bug.

What is not here yet is the rest of the video half — display mode, resolution, present mode — and the requested-versus-resolved display that makes a device's downward-only clamp visible instead of confusing. Those are written down in the repository's backlog rather than implied by an empty row.

Open the console for the engine's log. The wasm module imports nothing, not even console.log, so log lines are queued inside wasm and drained by the page each frame. crcbl.audio(), crcbl.saves() and crcbl.logLevel(4) are there too.