Migrating from v1
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
| v1 | v2 | |
|---|---|---|
| runtime | TensorFlow.js (WebGL or WebGPU backend) | WebGPU only: WebNN / hand-written WGSL by default; onnxruntime-web ≥ 1.27 is an optional peer dependency (denoiser/ort) |
| browser | WebGL2 fallback existed | WebGPU required (no fallback) |
| weights | .tza files (/tzas CDN dir) | the same .tza files by default (tzaUrl); converted .onnx models (/models) only for denoiser/ort |
| install | npm i denoiser | npm i denoiser |
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
| v1 | v2 |
|---|---|
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 / outputMode | gone — the method called is the mode (denoise → ImageData, denoiseToFloat → floats, denoiseTextures → GPUTexture) |
setCanvas(canvas) / outputToCanvas | gone — 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 flag | v2 |
|---|---|
quality | Denoiser.create({ quality }), still mutable via denoiser.quality |
hdr | denoiseTextures({ hdr: true }) per call |
srgb | denoise(img, { srgb }) (default true for image inputs); texture path uses transfer: 'linear' | 'srgb' | 'aces-srgb' for output encoding |
flipOutputY | flipY (denoise) / outputFlipY (denoiseTextures); input orientation via inputFlipY / auxInputFlipY |
height / width | gone — taken from the input |
useTiling / tileSize / batchSize | automatic: whole-frame up to maxRunPixels (create option), tiled above it with batch tiles per run — see Large images & tiling |
weightsPath / weightsUrl | Denoiser.create({ tzaUrl }) — still .tza weights (weightsUrl now belongs to OrtRuntime, and points at .onnx models) |
cleanAux / dirtyAux | automatic — rasterized aux is treated as clean when both albedo+normal are present |
debugging / stats | denoiser.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
denoiserentry no longer re-exportsOrtRuntime,OrtSession,DenoiseEngineorModels; import them fromdenoiser/ort. onnxruntime-webis now an optional peer dependency, installed only fordenoiser/ort. The root entry never loads it.weightsUrl,wasmPaths,graphCaptureandsplitAuxare no longerDenoiser.createoptions; they areOrtRuntimeoptions.- New
Denoiser.createoptions:tzaUrlanddevice. - The default runtime reads
.tzaweights (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_nrmand*_largemodels wrong, while WGSL and WebNN match native OIDN (see Choosing a runtime).