GPU texture downsampling: mip chains today, with reduction pyramids (Hi-Z, min/max) and custom reductions next. WebGPU first, engine-agnostic, zero dependencies. Adapters for three.js and others sit on top.
A WGSL implementation of the ideas in AMD FidelityFX SPD: a 64×64 source tile reduced to six levels per workgroup in workgroup memory, with every level written once. It targets SPD's feature set. See SPEC.md for the full design, parity table and roadmap.
Status: early (milestone 1). Supports
rgba8unormandrgba16float, 2D textures and array layers. Not yet published to npm.
Odd sizes default to 'exact'. Where a level has an odd, non-unit dimension, the next level is re-sampled with normalized-coordinate bilinear filtering. Output matches render-pass and WebGL mipmaps, so you can switch without visual change. A planned 'fast' mode will reduce straight through odd sizes the way SPD does. That means fewer dispatches (2 instead of 7 for 1920×1080) but slightly different edge weights. It is opt-in.
There is no single-dispatch mode. SPD's final levels are written by "the last workgroup to finish", detected with an atomic counter. In WGSL that handoff isn't guaranteed: barriers are workgroup-scoped and atomics are relaxed, so seeing the counter doesn't guarantee seeing other workgroups' results. We built and measured it anyway, and it was also slower. The atomic tail added ~20 µs, versus ~1–2 µs for the dispatch boundary it saves. So chains up to 4096² use two dispatches instead. Details and numbers are in SPEC.md §2.2.
import { Downsampler } from 'downsampler/webgpu';
const adapter = await navigator.gpu.requestAdapter();
// Six levels per dispatch need six storage textures (the default limit is four).
const device = await adapter.requestDevice( { requiredLimits: Downsampler.requiredLimits( adapter ) } );
const downsampler = new Downsampler( device );
// The texture needs TEXTURE_BINDING | STORAGE_BINDING usage.
downsampler.generate( texture ); // encode + submit
downsampler.encode( encoder, texture ); // or record into your own encoder
downsampler.encode( encoder, texture, { baseMipLevel: 2, mipLevelCount: 4 } );three.js (WebGPURenderer, texture allocated with storage usage, e.g. StorageTexture):
import { generateMipmaps } from 'downsampler/three';
if ( ! generateMipmaps( renderer, texture ) ) { /* not allocated or unsupported: use three's own path */ }Native support for all three.js textures is planned as an in-tree three.js option. See SPEC.md §10.
Apple M5 Pro (Metal 3, Chrome 155), GPU ms per full chain, against one render pass per level:
| Size | Format | Render passes | downsampler | Speedup |
|---|---|---|---|---|
| 512² | rgba8unorm | 0.0882 | 0.0098 | 9.0× |
| 1024² | rgba8unorm | 0.1159 | 0.0161 | 7.2× |
| 4096² | rgba8unorm | 0.4501 | 0.3371 | 1.3× |
| 1920×1080 | rgba8unorm | 0.1176 | 0.0374 | 3.1× |
| 512²×6 layers | rgba8unorm | 0.6337 | 0.0236 | 26.8× |
| 1024² | rgba16float | 0.1178 | 0.0263 | 4.5× |
Large textures are memory-bandwidth bound for every method. Other GPUs haven't been measured yet; device reports are welcome (below).
npm install
npm test # build, unit tests, GPU suite (headless Chrome)
npm run test:gpu -- --quick --adapter=swiftshader # software WebGPU, no GPU needed
npm run bench # validation + benchmark
npm run lab # device lab at http://localhost:8080/test/browser/The device lab runs the full validation suite and benchmark on whatever device opens it and produces a JSON report.
MIT