Migrating from v1

Mapping 0.x (TensorFlow.js) to the 2.x stateless, WebGPU-only API — construction, execution, properties, and error handling.

v2 is a ground-up rewrite: TensorFlow.js and every WebGL path are gone; the U-Net runs on a WebGPU runtime (by default WebNN fp16 or hand-written WGSL; ORT is opt-in), and all pre/post-processing is WGSL compute on one shared GPUDevice. It is a clean break — the API changed deliberately (see docs/archive/api-v2-spec.md for the rationale): v1 configured runs through instance flags set in the right order; v2 is stateless per call — the instance holds identity only (models, sessions, device), every run is fully described by its arguments.

Requirements changed

v1v2
runtimeTensorFlow.js (WebGL or WebGPU backend)WebGPU only: WebNN / hand-written WGSL by default; onnxruntime-web ≥ 1.27 is an optional peer dependency (denoiser/ort)
browserWebGL2 fallback existedWebGPU required (no fallback)
weights.tza files (/tzas CDN dir)the same .tza files by default (tzaUrl); converted .onnx models (/models) only for denoiser/ort
installnpm i denoisernpm i denoiser
Warning

If you must support non-WebGPU browsers, stay on v1 (0.0.11) for those users — v2 throws DenoiserUnsupportedError where WebGPU/features are missing.

API mapping

Construction

// v1 — sync constructor, backend + context injection, ready-listener
const denoiser = new Denoiser('webgpu', canvasOrDevice);
denoiser.onBackendReady(() => { /* ... */ });

// v2 — async factory; share the device with your renderer
const denoiser = await Denoiser.create({ precision: 'fp16', quality: 'fast', device: renderer.backend.device });

You can no longer hand the denoiser a canvas; hand it a GPUDevice through the device option (with the default runtime), or share denoiser.device with your renderer. Only the opt-in ORT runtime keeps the old constraint: onnxruntime-web creates and owns its GPUDevice (#26107), so with ORT create the denoiser first.

Executing

// v1 — stateful: set modes/inputs, execute, receive via listener
denoiser.inputMode = 'imgData';
denoiser.outputMode = 'imgData';
denoiser.onExecute((result) => draw(result), 'imgData');
await denoiser.execute(colorImg, albedoImg, normalImg);

// v2 — one call, result returned
const img = await denoiser.denoise(colorImg, { albedo: albedoImg, normal: normalImg });
ctx.putImageData(img, 0, 0);

Method-by-method

v1v2
execute(color, albedo, normal)denoise(color, { albedo, normal }) → ImageData
setInputImage(name, img, flipY)pass inputs directly to denoise() (accepts ImageData, img/canvas/video elements, ImageBitmap, or raw {data,width,height})
setInputData(name, f32/u8, w, h)same — the ImgInput union covers raw RGBA8
setInputTexture(name, WebGLTexture)gone (WebGL) → denoiseTextures({ color, albedo, normal }) with GPUTextures
setInputBuffer(name, GPUBuffer)denoiseTextures(...) (textures replaced buffers as the GPU interchange)
setInputTensor(name, tf.Tensor)gone with TFJS
inputMode / outputModegone — the method called is the mode (denoise → ImageData, denoiseToFloat → floats, denoiseTextures → GPUTexture)
setCanvas(canvas) / outputToCanvasgone — putImageData the result, or blit the output texture yourself
onExecute(cb, returnType)on('executed', cb), or just await the call
onProgress(cb)on('progress', cb) or per-call onProgress
onBackendReady(cb)unnecessary — await Denoiser.create()
abort()abort() (unchanged: in-flight call resolves undefined)
dispose()dispose() frees buffers/extra sessions but keeps the device; full teardown is destroyDevice() (⚠ kills the shared device and everything on it)

Properties → per-call options

v1 instance flagv2
qualityDenoiser.create({ quality }), still mutable via denoiser.quality
hdrdenoiseTextures({ hdr: true }) per call
srgbdenoise(img, { srgb }) (default true for image inputs); texture path uses transfer: 'linear' | 'srgb' | 'aces-srgb' for output encoding
flipOutputYflipY (denoise) / outputFlipY (denoiseTextures); input orientation via inputFlipY / auxInputFlipY
height / widthgone — taken from the input
useTiling / tileSize / batchSizeautomatic: whole-frame up to maxRunPixels (create option), tiled above it with batch tiles per run — see Large images & tiling
weightsPath / weightsUrlDenoiser.create({ tzaUrl }) — still .tza weights (weightsUrl now belongs to OrtRuntime, and points at .onnx models)
cleanAux / dirtyAuxautomatic — rasterized aux is treated as clean when both albedo+normal are present
debugging / statsdenoiser.stats (per-stage timings of the last run)

Errors

v2 throws typed errors instead of generic ones: DenoiserUnsupportedError (WebGPU/feature availability) and DenoiserInputError (mismatched sizes, missing aux, bad formats). Full signatures in the API reference.

Weights hosting

v1 served .tza files from a /tzas directory or CDN, and v2's default runtime does too: default CDN (jsDelivr, models-v3), override with tzaUrl. Only the opt-in ORT runtime serves converted .onnx models (rt_ldr.onnx, rt_hdr_alb_nrm_small.fp16.onnx, …), with weightsUrl. Regenerate the ORT models from OIDN .tza weights any time with tools/onnx-convert (Python, no PyTorch).

Output differences to expect

  • v2's output parity vs native OIDN was tightened during the port (PU transfer, autoexposure, normal encoding, no final activation — docs/specs/oidn-color-reference.md), so v2 output is more faithful to upstream OIDN than v1 was — i.e. not bit-identical to v1.
  • fp16 (precision: 'fp16') measures ≈ 53 dB PSNR vs fp32 — visually identical, ~15% faster.

Breaking changes in 2.x: AutoRuntime is the default

During the 2.x alpha the default runtime changed from onnxruntime-web (ORT) to AutoRuntime, and ORT moved behind an explicit import. If you adopted an earlier 2.x build, update like this:

// before: ORT was the default, configured through Denoiser.create
const denoiser = await Denoiser.create({
  precision: 'fp16',
  weightsUrl: '/models',
  wasmPaths: '/ort/',
  splitAux: true,
});

// after, option 1 (recommended): the default AutoRuntime, OIDN .tza weights
const denoiser = await Denoiser.create({
  precision: 'fp16',
  tzaUrl: '/tzas',          // optional, defaults to the jsDelivr models-v3 CDN
  device,                   // optional: run on an existing GPUDevice
});

// after, option 2: keep ORT explicitly (npm i onnxruntime-web)
import { OrtRuntime } from 'denoiser/ort';
const denoiser = await Denoiser.create({
  runtime: new OrtRuntime({ weightsUrl: '/models', wasmPaths: '/ort/', splitAux: true }),
  precision: 'fp16',
});
  • The root denoiser entry no longer re-exports OrtRuntime, OrtSession, DenoiseEngine or Models; import them from denoiser/ort.
  • onnxruntime-web is now an optional peer dependency, installed only for denoiser/ort. The root entry never loads it.
  • weightsUrl, wasmPaths, graphCapture and splitAux are no longer Denoiser.create options; they are OrtRuntime options.
  • New Denoiser.create options: tzaUrl and device.
  • The default runtime reads .tza weights (as v1 did), not .onnx.
  • With the default runtime you no longer have to create the denoiser first to share a device with three.js: create the renderer, then pass its device. Only ORT keeps the "denoiser first" order.
  • Why: ORT computes the *_alb, *_alb_nrm and *_large models wrong, while WGSL and WebNN match native OIDN (see Choosing a runtime).