webgpu-video-shaders
v0.1.0
Published
Video processing algorithms as WGSL shader generators — libplacebo ports + originals
Maintainers
Readme
webgpu-video-shaders
libplacebo's video processing algorithms as WGSL shader generators. Zero dependencies. Pure TypeScript. Works with any WebGPU pipeline.
This is a faithful 1:1 port of libplacebo (the rendering engine behind mpv) to WebGPU/WGSL. Every public shader function is ported, every algorithm matches the original C code.
Install
npm install webgpu-video-shadersQuick Start
import { createDeband } from 'webgpu-video-shaders/libplacebo';
const deband = createDeband({ iterations: 2, threshold: 4, grain: 6 });
// deband.fn contains a WGSL function: fn deband(tex, coord, dims) -> vec4f
// Paste it into your shader and call it:
const shader = /* wgsl */ `
@group(0) @binding(0) var inputTex: texture_2d<f32>;
@group(0) @binding(1) var outputTex: texture_storage_2d<rgba8unorm, write>;
${deband.fn}
@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) id: vec3u) {
let dims = textureDimensions(inputTex);
if (id.x >= dims.x || id.y >= dims.y) { return; }
let result = deband(inputTex, vec2i(id.xy), vec2i(dims));
textureStore(outputTex, vec2i(id.xy), result);
}
`;Imports
Import from specific barrels for tree shaking:
// libplacebo ports (faithful 1:1 translations)
import { createDeband, createToneMap } from 'webgpu-video-shaders/libplacebo';
// Original implementations (not from libplacebo)
import { createVignette, createSharpen } from 'webgpu-video-shaders/original';
// Shared types
import type { ShaderFunction, ShaderResult } from 'webgpu-video-shaders/core';
// Or import everything (no tree shaking)
import { createDeband, createVignette } from 'webgpu-video-shaders';Two-Tier API
Tier 1: Composable Functions
Returns a WGSL fn string. You wire it into your own shader with your own bindings. Works with any WebGPU pipeline.
import { createDeband, createToneMap, createGrain } from 'webgpu-video-shaders/libplacebo';
const deband = createDeband({ iterations: 1, threshold: 3 });
const tonemap = createToneMap({ method: 'hable', srcPeakNits: 1000 });
const grain = createGrain({ amount: 4 });
// Compose multiple effects in a single shader (one dispatch, no intermediate textures):
const shader = /* wgsl */ `
@group(0) @binding(0) var inputTex: texture_2d<f32>;
@group(0) @binding(1) var outputTex: texture_storage_2d<rgba16float, write>;
${deband.fn}
${tonemap.fn}
${grain.fn}
@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) id: vec3u) {
let dims = textureDimensions(inputTex);
if (id.x >= dims.x || id.y >= dims.y) { return; }
let coord = vec2i(id.xy);
var color = ${deband.fnName}(inputTex, coord, vec2i(dims));
color = ${tonemap.fnName}(color);
color = ${grain.fnName}(color, coord);
textureStore(outputTex, coord, color);
}
`;Tier 2: Complete Shaders
Returns a full WGSL compute shader string ready for device.createShaderModule(). Includes bindings, workgroup size, everything.
import { createDebandShader } from 'webgpu-video-shaders/libplacebo';
const { wgsl, extraBindings, description } = createDebandShader({
iterations: 2, threshold: 4, grain: 6
});
const module = device.createShaderModule({ code: wgsl });Usage with Browser WebGPU
import { createToneMap, createDeband } from 'webgpu-video-shaders/libplacebo';
const deband = createDeband({ iterations: 1 });
const tonemap = createToneMap({ method: 'bt2390', srcPeakNits: 1000, dstPeakNits: 203 });
const shaderCode = /* wgsl */ `
@group(0) @binding(0) var inputTex: texture_2d<f32>;
@group(0) @binding(1) var outputTex: texture_storage_2d<rgba16float, write>;
${deband.fn}
${tonemap.fn}
@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) id: vec3u) {
let dims = textureDimensions(inputTex);
if (id.x >= dims.x || id.y >= dims.y) { return; }
let coord = vec2i(id.xy);
var color = ${deband.fnName}(inputTex, coord, vec2i(dims));
color = ${tonemap.fnName}(color);
textureStore(outputTex, coord, color);
}
`;
// Standard WebGPU pipeline setup
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: { module: device.createShaderModule({ code: shaderCode }), entryPoint: 'main' },
});
// Create textures, bind group, dispatch...
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: inputTexture.createView() },
{ binding: 1, resource: outputTexture.createView() },
],
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(Math.ceil(width / 16), Math.ceil(height / 16));
pass.end();
device.queue.submit([encoder.finish()]);Usage with TypeGPU
import { createDeband } from 'webgpu-video-shaders/libplacebo';
import tgpu from 'typegpu';
const root = await tgpu.init();
const deband = createDeband({ iterations: 2, threshold: 4 });
// TypeGPU supports raw WGSL via tgpu.resolve
const shaderCode = /* wgsl */ `
@group(0) @binding(0) var inputTex: texture_2d<f32>;
@group(0) @binding(1) var outputTex: texture_storage_2d<rgba8unorm, write>;
${deband.fn}
@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) id: vec3u) {
let dims = textureDimensions(inputTex);
if (id.x >= dims.x || id.y >= dims.y) { return; }
let result = ${deband.fnName}(inputTex, vec2i(id.xy), vec2i(dims));
textureStore(outputTex, vec2i(id.xy), result);
}
`;
const module = root.device.createShaderModule({ code: shaderCode });Usage with React Native Skia (useVideo)
import { useVideo } from '@shopify/react-native-skia';
import { createConeDistort } from 'webgpu-video-shaders/libplacebo';
// Generate shader at module scope (runs once)
const cone = createConeDistort({ type: 'deuteranopia', severity: 1.0 });
const COLORBLIND_SHADER = /* wgsl */ `
@group(0) @binding(0) var inputTex: texture_2d<f32>;
@group(0) @binding(1) var outputTex: texture_storage_2d<rgba16float, write>;
${cone.fn}
@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) id: vec3u) {
let dims = textureDimensions(inputTex);
if (id.x >= dims.x || id.y >= dims.y) { return; }
let color = textureLoad(inputTex, vec2i(id.xy), 0);
textureStore(outputTex, vec2i(id.xy), ${cone.fnName}(color));
}
`;
// In your component, useVideo provides frames as SkImage textures.
// With Skia Graphite (SK_GRAPHITE=1), navigator.gpu is available via Dawn,
// and you can run compute shaders on the GPU texture backing the SkImage.
function VideoPlayer() {
const { currentFrame } = useVideo('video.mp4');
// ... render currentFrame via <Canvas> + <Image>, process via navigator.gpu
}LUT-Based HDR Pipeline
For production HDR tone mapping, use the LUT-based pipeline that matches libplacebo's architecture:
import {
createPeakDetectShader,
createToneMapLutGenPipelineShader,
createToneMapLutApplyShader,
} from 'webgpu-video-shaders/libplacebo';
// Pass 1: Peak detection (measures scene luminance)
const peakDetect = createPeakDetectShader({ histogramBins: 64 });
// Pass 2: LUT generation (reads peak stats, generates/caches 1D tone map LUT)
const lutGen = createToneMapLutGenPipelineShader({
method: 'bt2390',
dstPeakNits: 203,
});
// Pass 3: LUT application (reads LUT, applies per-pixel in IPT space)
const lutApply = createToneMapLutApplyShader({
lutSize: 256,
srcPeakNits: 1000,
srcTransfer: 'hlg', // Apple Log → linear
dstTransfer: 'srgb', // → sRGB for display
srcPrimaries: 'bt2020', // BT.2020 → BT.709
dstPrimaries: 'bt709',
});
// The LUT gen shader caches the peak value — if scene brightness hasn't
// changed since last frame, LUT regeneration is skipped automatically.What's Included
libplacebo Ports (webgpu-video-shaders/libplacebo)
Every public shader function from libplacebo is ported:
Color Pipeline
createColorMap— linearize/delinearize (17 transfer functions: sRGB, PQ, HLG, BT.1886, gamma variants, V-Log, S-Log, ProPhoto, ST428, scRGB) + gamut conversion with Bradford chromatic adaptationcreateColorMapShader— fullpl_shader_color_map_exorchestrator (linearize → IPT → tone map → gamut map → delinearize with contrast recovery and clipping diagnostics)createDecodeColor/createEncodeColor— YCbCr decode/encode (BT.601/709/2020, BT.2020-CL, ICtCp PQ/HLG, Dolby Vision, XYZ, chroma location, subsampling)createDoviReshape— Dolby Vision polynomial + MMR reshapingcreateConeDistort— color vision deficiency simulation (8 types, CAT16 matrix)createAlpha— premultiply/unpremultiplycreateSigmoidize/createUnsigmoidize— anti-ringing sigmoid for upscaling
Tone Mapping (12 curves)
createToneMap— clip, bt2390, bt2446a, st2094_40, st2094_10, spline, reinhard, mobius, hable, gamma, linear, linear_lightcreateToneMapLutGenShader/createToneMapLutApplyShader— GPU LUT-based pipeline with caching
Gamut Mapping (10 methods)
createGamutMap— clip, perceptual, softclip, relative, saturation, absolute, desaturate, darken, highlight, linear (operates in IPTPQc4 space)createGamutMapLutGenShader/createGamutMapLutApplyShader— 3D LUT pipeline
Peak Detection
createPeakDetectShader— 12-slice sharding, PQ-biased histogram, IIR temporal smoothing, scene-change detection, percentile-based peak measurement
Sampling
createUpscale— nearest, bilinear, bicubic, hermite, gaussiancreatePolarSampleShader— EWA sampling (ewa_lanczos, ewa_jinc, etc.)createOrthoSampleShader— separable 2-pass scalingcreateOversample— spatial oversamplingcreateFilterKernel— 21 filter weight functions (sinc, jinc, kaiser, mitchell, etc.)createDistortLpShader— affine geometric distortion
Dithering
createDither— ordered (16×16), white noise, blue noise (LUT), ordered LUT, with temporal rotation and gamma-corrected pathcreateErrorDiffusionShader— GPU error diffusion (Floyd-Steinberg, Sierra Lite, Burkes, Stucki, Atkinson, Sierra 2/3)generateBayerMatrix/generateBlueNoise— CPU-side LUT generators
Other
createDeband— flash3kyuu debanding with PCG3D PRNGcreateGrain— film grain with vec3 independent PRNGcreateExtractFeaturesShader— IPT intensity extraction for contrast recovery
Original (webgpu-video-shaders/original)
createLanczos— windowed sinc resampling with anti-ringingcreateGaussianBlur— precomputed kernel blurcreateSharpen— unsharp mask / CAS adaptive sharpeningcreateVignette— optical vignettingcreateDistort— barrel/pincushion/chromatic aberrationcreateDeinterlace— bob/weave/yadif
Source
All libplacebo ports reference the original C source by file and line number. The source is haasn/libplacebo (LGPL-2.1+).
License
LGPL-2.1-or-later (matching libplacebo)
