Making WebGPU run on every GPU (including ones you can't test)
A native engine gets to assume the machine: pick a target, test on it. Awaken runs in a browser tab, so it runs on everything - a gaming rig, a work laptop's integrated chip, a phone - including hardware with features your own dev machine doesn't have. You are permanently coding a little blind. Here's how you make that survivable, with the real code, because it's all open source now.
Feature detection: ask, don't assume
WebGPU has optional features. Awaken wants a few (compute-driven culling needs indirect-first-instance; desktop wants texture-compression-bc). You detect them opportunistically - filter your wishlist against what the adapter reports, request only the survivors, then read your capabilities back off the device, which is the source of truth for what you actually got:
const wanted = ["indirect-first-instance", "texture-compression-bc", "timestamp-query"]
.filter((f) => adapter.features.has(f));
const device = await adapter.requestDevice(wanted.length ? { requiredFeatures: wanted } : undefined);
const caps = { indirectFirstInstance: device.features.has("indirect-first-instance") /* … */ };Ask for a feature the adapter lacks and requestDevice rejects outright - so filter against the adapter first, always.
The failure mode is worse than a crash
My laptop lacked indirect-first-instance, and the bug was the nasty kind: silent wrong output, not an error. The GPU cull kernel packs each batch's visible objects into a buffer slice and points the draw at it via firstInstance; if the driver ignores firstInstance, every batch reads from instance zero and draws the wrong objects. So the toggle was right there, and it either helped - on your machine - or quietly rendered garbage on mine.
So the capability gates the feature, and the gate falls through cleanly to a path that always works:
flowchart TD
T{"useGpuCull AND
caps.indirectFirstInstance?"}
T -->|yes| C{"scene non-empty?"}
C -->|yes| G["GPU compute cull →
indirect draws"]
C -->|no| CPU
T -->|no| CPU["CPU cull →
instanced draws (always correct)"]The part I'd underline: the CPU path is not dead code. GPU culling is a surgical optimisation of the one main opaque pass - shadows, mesh preview and object-picking always run the CPU path, on every machine. So the fallback is exercised every frame everywhere, which means it can't rot, and a GPU without the fancy feature just gets the same correct picture a hair slower. And the editor toggle greys itself out when the feature's missing, with a tooltip that says why instead of offering a button that lies.
Which GPU did you even get?
Laptops with two GPUs make it worse on purpose: WebGPU has no adapter enumeration - you cannot list the GPUs. The only knob is a hint, powerPreference: "high-performance" | "low-power", which on a hybrid laptop usually maps to discrete vs integrated - usually, because the browser can hand you the integrated chip even when you asked for performance, and never tell you.
So the "GPU picker" probes both preferences and compares what falls out. Which hits a privacy wall - Firefox and Chrome's fingerprint-resistance mode blank the adapter name - so you can't tell one probe from two by name. The fix is to fingerprint the adapter by its capabilities instead:
// Firefox blanks the NAME; a discrete vs integrated GPU still differ in limits/features.
const fp = (a) => [a.limits.maxBufferSize, a.limits.maxComputeWorkgroupSizeX, /* … */,
...[...(a.features ?? [])].sort()].join(",");Two adapters are distinct if their names or fingerprints differ; if names are withheld you fall back to generic labels. And if name and fingerprint match, it's one GPU and the picker hides itself entirely - offering a switch that does nothing is its own kind of lie. Switching GPU forces a full page reload, because a GPUDevice is bound to the adapter that made it and every pipeline and buffer would have to be rebuilt.
📷 Screenshot needed: the Render Settings with the "GPU cull" toggle greyed out + its tooltip, and/or the stats overlay line showing the chosen adapter name + power preference.
And you can't even see the bug
You can't render on hardware you don't own, so how do you trust the cull maths on a GPU you'll never touch? Awaken has a headless WebGPU conformance harness - it boots WebGPU without a display, compiles every WGSL shader, builds every pipeline, dispatches the actual cull kernel on a fixture, and diffs the GPU result against a CPU reference. It even empirically probes spec corners (does @builtin(instance_index) include firstInstance?) by rendering into a 1×1 integer target and reading the pixel back. That harness is how you buy confidence about hardware behaviour when you have no hardware.
All of this is tax - capability checks, a fallback you keep alive, fingerprint hacks - paid so "open the link" works for the person on the mid-range Android, not just the person at my desk. It's less glamorous than the shader you wanted to write. It's also the difference between a demo that runs on my machine and an engine that runs on everyone's.