Troubleshooting / FAQ
"WebGPU unavailable" / DenoiserUnsupportedError on create
Denoiser.create() probes for an adapter up front (so you get a clear error
instead of a cryptic failure deeper in). If none is available, it throws:
denoiser 2.x requires WebGPU (no adapter available). See browser support:
Chrome/Edge stable, Safari 26+. For WebGL environments use denoiser 0.x (v1).
Current support: Chrome/Edge stable, Safari 26+, Firefox (Windows
stable, other platforms in progress). This is a hard requirement — v2 has no
WebGL fallback. If you need to support older/unsupported browsers, stay on
v1 (0.0.11); see Migrating from v1 for what changed.
Black output
Two usual causes:
- Device mismatch. Your renderer/pipelines and the denoiser ended up on two
different
GPUDevices, so textures from one can't be used by the other. With the default runtime, pass your device in (Denoiser.create({ device })) or build ondenoiser.device. With the opt-in ORT runtime, which always creates its own device (#26107), create the denoiser first, then build your renderer/pipelines ondenoiser.device— see Custom engines and three.js render targets. - Reading before the work is done. Forgetting to
awaitthedenoise*()call, or (three.js) reading a freshly-created caller-owned output texture beforerenderer.initTexture()has materialized its GPU resource — see the pitfalls checklist.
Upside-down output
Orientation is three independent flags, because color/aux/output can each have a different convention — see the flipY family:
inputFlipY— thecolorinput. WebGPU compute-written targets (e.g. a path tracer'sStorageTextureoutput) are bottom-up →true. Rasterized targets are top-down →false.auxInputFlipY—albedo/normal, defaults toinputFlipY; set it separately if your G-buffer pass has the opposite convention from color.outputFlipY— flips the result.
If the image is upside down, toggle these before looking anywhere else.
"My aux output is speckled" (ORT only)
This only applies to the opt-in ORT runtime (denoiser/ort); the default runtime
does not have this bug. On ORT it is a known onnxruntime-web WebGPU execution-provider bug (not a
denoiser defect — confirmed with a standalone vanilla-ORT repro, unrelated to
our code), on the first convolution that reduces a >3-channel input. The
OrtRuntime splitAux option (default on) works around it by computing that first
conv ourselves in WGSL and running the rest of the graph on WebGPU normally —
verified to restore native quality.
You'll see speckled aux output if:
-
splitAux: falsewas set explicitly — the workaround is off, so the upstream bug is unmasked. Only do this if you have a specific reason to. -
You're self-hosting an old
models-v1weights set without the<model>.tail.onnx+<model>.enc0.binsplit-graph artifactssplitAuxneeds. The runtime falls back to the plain (speckled) model with a one-time console warning in this case, rather than failing outright:Denoiser: splitAux artifacts unavailable for <model>, aux will speckle (ORT-web WebGPU Conv bug). Host <name>.tail.onnx + <name>.enc0.bin next to the model, or set splitAux:false to silence.Mirror the current ORT weights tag (
models-v2or later) if you self-host — see Models & weights for hosting details.
"My aux textures are silently ignored"
If you pass albedo/normal keys whose value resolves to undefined (e.g.
a three.js texture unwrap that failed and returned undefined, but you still
passed the key), the denoiser degrades to the color-only model rather than
throwing — but it warns once per input so this doesn't fail silently forever:
Denoiser: albedo texture could not be unwrapped — falling back to color-only model
If you see this warning, check whatever produced the texture — most commonly a stale/destroyed render target reference (re-resolve it fresh each frame; see the render-targets pitfalls checklist) or a G-buffer pass that hasn't rendered yet.
CORS / hosting errors
- Default (
tzaUrlunset) works out of the box — the.tzaweights are served from jsDelivr's GitHub endpoint (cdn.jsdelivr.net/gh/...), which sendsaccess-control-allow-origin: *. (The ORT runtime'sweightsUrldefault works the same way.) - GitHub Releases assets have no CORS support — verified dead end, don't
point
tzaUrl(orweightsUrl) at a Releases download URL.release-assets.githubusercontent.comdoesn't sendaccess-control-allow-origineven on a plainGETwith anOriginheader, so browserfetch()fails. - For production, host the
.tzafiles yourself (copy them into your own static assets/CDN and settzaUrl; for ORT, the.onnxfiles andweightsUrl) rather than depending on someone else's default. See Models & weights for the full hosting comparison.
Device-lost after dispose() / destroyDevice()
These two are not the same:
dispose()— safe between workloads. Frees per-image buffers and extra model sessions but keeps one session alive on purpose, so the shared device survives.destroyDevice()— full teardown. With ORT, releasing the last session destroys the sharedGPUDevice— your renderer, canvas contexts, and every other resource built on it die with it (GPUDevice.lost, reason'destroyed'). Only call this when the entire WebGPU stack is going away (e.g. tearing down the whole app/page).
If you're seeing an unexpected device-lost event, search for a
destroyDevice() call (or code that lets the denoiser's session count hit
zero) rather than dispose() — dispose() cannot destroy the device.
fp16 unavailable
fp16 (precision: 'fp16') needs the WebGPU shader-f16 feature. Behavior
differs by which API you use:
Denoiser.create({ precision: 'fp16' })— falls back automatically: if session creation fails becauseshader-f16is missing, it retries once at fp32 and logsDenoiser: fp16 unavailable, falling back to fp32. You don't need to handle this yourself.- The lower-level
DenoiseEngine.create()— no fallback. It throwsDenoiserUnsupportedError: Denoiser: fp16 needs the shader-f16 WebGPU feature (unavailable on this device). Only relevant if you're bypassingDenoiserand drivingDenoiseEnginedirectly.
See the API reference for the full option/error surface.