The GPUDevice

Who owns the device, how to share it with your renderer, and the lifetime rules that follow.

If you're only ever calling denoiser.denoise(image) you can skip this page — device sharing only matters once you want a zero-copy pipeline with your own renderer (three.js, a custom WebGPU engine, etc).

Who creates the device

With the default runtime (AutoRuntime) you choose. Either your renderer creates the device and you hand it to the denoiser through the device option, or the denoiser creates it and you hand it to your renderer:

// renderer first (the clean flow with the default runtime)
const renderer = new THREE.WebGPURenderer({ canvas });
await renderer.init();
const denoiser = await Denoiser.create({
  device: (renderer.backend as { device: GPUDevice }).device,
  precision: 'fp16',
});

// or denoiser first
const denoiser2 = await Denoiser.create({ precision: 'fp16' });
const renderer2 = new THREE.WebGPURenderer({ device: denoiser2.device });

The exception is the opt-in ORT runtime (denoiser/ort): onnxruntime-web creates and owns the GPUDevice and ignores one you try to inject (onnxruntime#26107), so with ORT you must create the Denoiser first and hand its device to your renderer.

Do this and your path tracer, denoiser, and any post-processing all run on one device with zero copies between them — render target in, denoiseTextures(), render target out.

The max-limits patch

WebGPU devices default to fairly small buffer caps (roughly 128–256 MB depending on the limit). A whole-frame U-Net's intermediate activations at any real resolution blow past that easily — and so does a path tracer's own buffers. So when the default runtime creates the device itself (no device option), it requests the adapter's full limits and features instead of WebGPU's conservative defaults. This happens automatically; you don't configure it.

If you pass your own device, it needs those limits too: request them when you create the renderer's device (the path tracer examples do this with a scoped patch of GPUAdapter.requestDevice, patchWebGPUForMaxLimits() in examples/_shared/stack.ts). The ORT runtime creates its own device under a similar scoped patch.

Net effect: the shared device can hold whole-frame U-Net intermediates and your renderer's own pipelines, without you having to hand-tune requiredLimits.

Device lifetime rules

Two rules apply, the first one specific to ORT owning its device:

  1. With ORT, destroyDevice() destroys the shared device. When the last ORT session is released, ORT destroys its device — three.js, canvas contexts, and every other resource on that device die with it (GPUDevice.lost, reason 'destroyed'). dispose() is the safe between-workloads cleanup instead: it frees buffers and extra sessions but deliberately retains one session, so the device survives. Only call destroyDevice() when the entire WebGPU stack is going away. The default runtime does not destroy a device you passed in, but treat destroyDevice() as full teardown anyway.
  2. Model switches are safe. Changing quality or otherwise swapping the active model overlaps creating the new engine with disposing the old one, specifically so the session count never hits zero mid-swap (which would trigger rule 1 accidentally). Don't "optimize" that ordering away if you're working in the internals — it's there to prevent releasing the last session by accident during a routine model switch.
Important

If your whole WebGPU stack unexpectedly dies — canvases go blank, three.js throws device-lost errors — check whether something called destroyDevice() (or, with ORT, let the last session get released). Use dispose() for routine cleanup between workloads instead.

dispose() vs destroyDevice()

keeps device aliveuse for
dispose()yesfreeing buffers/extra sessions between workloads, safe default
destroyDevice()nofull teardown when the whole WebGPU stack is going away

See three.js render targets in/out for the full worked example — unwrapping three.js textures to raw GPUTextures, orientation flags, and the live-loop pacing pattern that goes with a shared device.