Start Here
A Custom Filter Code is a small snippet that GVF imports and applies to videos in real time. You do not need to build a browser extension or edit the userscript manually.
- Open the GVF Filter Library.
- Click Add Custom Filter.
- Select the correct type: SVG, WebGL/GLSL, Canvas 2D or Audio/Speech.
- Paste your code.
- Add a clear label, short description, category and tags.
- Submit or install the filter.
Filter Types
| Type | Best for | Strength | Limitations |
|---|---|---|---|
| SVG Filter | Color, contrast, blur, simple sharpen, glow, creative effects | Works on many pages, often DRM-safe | No real custom pixel loops or AI upscaling |
| WebGL / GLSL | Sharpen, denoise, FSR-like effects, Anime4K-like effects, chroma reconstruction | Most powerful video processing mode | Usually blocked on DRM video; not all mpv shaders work 1:1 |
| Canvas 2D | JavaScript-driven overlays, visual experiments, simple frame effects | Easy to write if you know JS | Slower than GLSL for heavy pixel processing |
| Audio / Speech | Speech presets, gain, EQ, noise suppression logic | Good for voice tools and presets | Not a video image filter |
Label, Description, Tags and Category
Label
The label should be short and clear — it appears in the filter list and in the manager.
Anime Restore Mild Smart Denoise Krig Chroma Restore CAS Sharpen
Description
Explain what the filter does in one sentence.
Edge-aware denoise and mild sharpening for compressed anime and streaming video.
Tags
Use only a few useful tags. Too many tags make search messy.
denoise, sharpen, anime
Category
Use one category that describes the main purpose.
Denoise Sharpening Color Grading Chroma Reconstruction Creative / FX Repair / Utility
SVG Filter Basics
SVG filter code is not a full SVG document. Paste only the inner filter primitives such as <feGaussianBlur> or <feColorMatrix>. GVF wraps them in a <filter> automatically.
Correct
<feColorMatrix type="saturate" values="1.25"/>
Wrong
<svg>
<filter>
<feColorMatrix type="saturate" values="1.25"/>
</filter>
</svg>
SVG Examples
1. Mild Sharpen
<feGaussianBlur stdDeviation="0.45" result="blur"/> <feComposite in="SourceGraphic" in2="blur" operator="arithmetic" k1="0" k2="1.35" k3="-0.35" k4="0"/>
2. Strong Contrast S-Curve
<feComponentTransfer> <feFuncR type="table" tableValues="0 0.04 0.18 0.42 0.68 0.88 1"/> <feFuncG type="table" tableValues="0 0.04 0.18 0.42 0.68 0.88 1"/> <feFuncB type="table" tableValues="0 0.04 0.18 0.42 0.68 0.88 1"/> </feComponentTransfer>
3. Warm White Balance
<feColorMatrix type="matrix" values=" 1.06 0 0 0 0.02 0 1.00 0 0 0 0 0 0.92 0 -0.02 0 0 0 1 0"/>
4. Debanding Dither
<feTurbulence type="fractalNoise" baseFrequency="0.75" numOctaves="1" result="noise"/> <feColorMatrix in="noise" type="saturate" values="0" result="mono"/> <feComposite in="SourceGraphic" in2="mono" operator="arithmetic" k1="0" k2="1" k3="0.012" k4="-0.006"/>
5. Bloom / Glow
<feGaussianBlur stdDeviation="3" result="blur"/> <feBlend in="SourceGraphic" in2="blur" mode="screen"/>
GLSL Basics
GLSL filters process pixels on the GPU using WebGL2. In GVF you write a fragment shader using the built-in variables below. The pipeline is GLSL ES 3.0 (#version 300 es is added automatically).
| Name | Type | Meaning |
|---|---|---|
u_video | sampler2D | The current video frame as a texture |
u_video_raw | sampler2D | Unprocessed source frame (before the filter chain) |
v_uv | vec2 | Current pixel UV coordinate, 0.0 → 1.0 |
u_res | vec2 | Canvas/video resolution in pixels |
u_time | float | Elapsed time in seconds — for animated effects |
u_strength | float | Global strength slider (0–1) |
u_mouse | vec2 | Mouse position in UV space (0–1) |
u_avg_lum | float | Average luma of the current frame |
fragColor | out vec4 | Final output color to write |
Minimal GLSL Filter
void main() {
vec4 color = texture(u_video, v_uv);
fragColor = color;
}
sampler s0 : register(s0), tex2D(), float2, lerp() and saturate() must be converted to GLSL equivalents (texture(), vec2, mix(), clamp(x,0.,1.)).
GLSL Uniforms and Sliders
Add sliders to your filter by placing // @uniform comment lines above your shader code. GVF parses them and builds a live slider UI automatically.
Syntax
// @uniform float u_strength 0.8 0.0 2.0 "Strength"
| Part | Example | Description |
|---|---|---|
| type | float | Currently the only supported type for slider values |
| name | u_strength | Variable name used inside the GLSL shader — must not clash with built-ins |
| default | 0.8 | Initial slider value on first load |
| min | 0.0 | Lowest slider value |
| max | 2.0 | Highest slider value |
| label | "Strength" | Visible label in the UI — use slash for grouping |
Example with slider
// @uniform float u_brightness 1.05 0.5 1.5 "Brightness"
void main() {
vec4 color = texture(u_video, v_uv);
color.rgb *= u_brightness;
fragColor = color;
}
@uniform lines: u_strength, u_layers, u_zoom, u_time, u_mouse, u_res, u_video, u_video_raw, u_avg_lum, u_avg_r, u_avg_g, u_avg_b, u_contrast.
Slider Groups and Checkboxes
Groups
Use a slash in the label string to group sliders visually in the UI:
// @uniform float u_strength 1.0 0.0 2.0 "Sharpen / Strength" // @uniform float u_radius 1.0 0.5 2.0 "Sharpen / Radius" // @uniform float u_mix 0.8 0.0 1.0 "Output / Effect Mix"
This renders as:
SHARPEN Strength ──────●────── Radius ──────●────── OUTPUT Effect Mix ─────●──────
Checkboxes
Sliders with a label containing Enable, Debug, Use, Toggle or Active are rendered as checkboxes. Internally they remain floats:
// @uniform float u_enable 1.0 0.0 1.0 "Denoise / Enable" // @uniform float u_debug 0.0 0.0 1.0 "Debug / Enable"
0.0 = off (unchecked) 1.0 = on (checked)
In GLSL, branch on them like this:
if (u_enable > 0.5) {
// apply effect
}
#if u_enable == 1 — preprocessor directives cannot read runtime uniform values. Always use a runtime if() branch.
GLSL Examples
1. Brightness / Contrast / Saturation
// @uniform float u_brightness 1.02 0.5 1.5 "Color / Brightness"
// @uniform float u_contrast 1.08 0.5 1.8 "Color / Contrast"
// @uniform float u_saturation 1.08 0.0 2.0 "Color / Saturation"
// @uniform float u_mix 1.0 0.0 1.0 "Output / Effect Mix"
float luma(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
void main() {
vec4 src = texture(u_video, v_uv);
vec3 c = src.rgb;
c *= u_brightness;
c = (c - 0.5) * u_contrast + 0.5;
float y = luma(c);
c = mix(vec3(y), c, u_saturation);
fragColor = vec4(mix(src.rgb, clamp(c, 0.0, 1.0), u_mix), src.a);
}
2. Simple Sharpen
// @uniform float u_strength 0.45 0.0 2.0 "Sharpen / Strength"
// @uniform float u_radius 1.0 0.5 2.0 "Sharpen / Radius"
// @uniform float u_mix 0.8 0.0 1.0 "Output / Effect Mix"
vec3 sampleVideo(vec2 uv) {
return texture(u_video, clamp(uv, vec2(0.0), vec2(1.0))).rgb;
}
void main() {
vec2 px = u_radius / u_res;
vec4 src = texture(u_video, v_uv);
vec3 n = sampleVideo(v_uv + vec2(0.0, -px.y));
vec3 s = sampleVideo(v_uv + vec2(0.0, px.y));
vec3 e = sampleVideo(v_uv + vec2( px.x, 0.0));
vec3 w = sampleVideo(v_uv + vec2(-px.x, 0.0));
vec3 blur = (n + s + e + w) * 0.25;
vec3 sharp = src.rgb + (src.rgb - blur) * u_strength;
fragColor = vec4(mix(src.rgb, clamp(sharp, 0.0, 1.0), u_mix), src.a);
}
3. Bilateral Denoise
// @uniform float u_enable 1.0 0.0 1.0 "Denoise / Enable"
// @uniform float u_spatial 1.2 0.1 3.0 "Denoise / Spatial Radius"
// @uniform float u_color 0.12 0.01 0.5 "Denoise / Color Tolerance"
// @uniform float u_mix 0.65 0.0 1.0 "Output / Effect Mix"
void main() {
vec2 px = 1.0 / u_res;
vec4 src = texture(u_video, v_uv);
vec3 center = src.rgb;
vec3 sum = vec3(0.0);
float wSum = 0.0;
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
vec2 o = vec2(float(x), float(y));
vec3 s = texture(u_video, v_uv + o * px).rgb;
float sw = exp(-dot(o, o) / max(2.0 * u_spatial * u_spatial, 0.0001));
float cd = length(s - center);
float cw = exp(-(cd * cd) / max(2.0 * u_color * u_color, 0.0001));
float w = sw * cw;
sum += s * w;
wSum += w;
}
}
vec3 denoised = sum / max(wSum, 0.0001);
if (u_enable < 0.5) denoised = center;
fragColor = vec4(mix(center, denoised, u_mix), src.a);
}
4. Vignette
// @uniform float u_amount 0.35 0.0 1.0 "Vignette / Amount"
// @uniform float u_softness 0.55 0.1 1.5 "Vignette / Softness"
void main() {
vec4 src = texture(u_video, v_uv);
vec2 p = v_uv * 2.0 - 1.0;
float d = dot(p, p);
float vig = smoothstep(1.2, u_softness, d);
fragColor = vec4(mix(src.rgb, src.rgb * vig, u_amount), src.a);
}
5. Animated RGB Shift
// @uniform float u_amount 0.45 0.0 1.0 "RGB Shift / Amount"
// @uniform float u_speed 1.0 0.0 5.0 "RGB Shift / Speed"
// @uniform float u_radius 1.0 0.0 4.0 "RGB Shift / Radius"
void main() {
vec2 px = 1.0 / u_res;
float wave = sin(v_uv.y * u_res.y * 0.04 + u_time * u_speed) * u_radius;
float r = texture(u_video, v_uv + vec2(px.x * wave, 0.0)).r;
float g = texture(u_video, v_uv).g;
float b = texture(u_video, v_uv - vec2(px.x * wave, 0.0)).b;
vec4 src = texture(u_video, v_uv);
fragColor = vec4(mix(src.rgb, vec3(r, g, b), u_amount), src.a);
}
Canvas 2D Basics
Canvas 2D filters are JavaScript snippets that run on every frame via requestAnimationFrame. GVF injects the following variables into your code automatically.
| Name | Type | Meaning |
|---|---|---|
ctx | CanvasRenderingContext2D | The 2D drawing context of the overlay canvas |
canvas | HTMLCanvasElement | The overlay canvas element — use canvas._myState for persistent state |
img | HTMLImageElement | Source image / frame reference |
width | number | Canvas width in pixels (device pixels) |
height | number | Canvas height in pixels (device pixels) |
frame | number | Delta time in milliseconds since last frame — use for physics/animation |
u_mouse | {x, y} | Mouse position in UV space (0–1) |
u_zoom | number | Current zoom level |
Canvas 2D Parameters
Canvas filters support slider and select parameters using comment annotations:
// @param strength 0.8 0.0 2.0 "Strength" // @paramselect mode normal "Mode" normal:Normal,screen:Screen,overlay:Overlay
canvas._myState — this object survives across frames and is unique to each filter instance.
Canvas 2D Examples
1. Simple Draw
ctx.clearRect(0, 0, width, height); ctx.drawImage(img, 0, 0, width, height);
2. CSS Filter Through Canvas
// @param contrast 1.1 0.5 2.0 "Contrast"
// @param saturation 1.15 0.0 2.0 "Saturation"
ctx.clearRect(0, 0, width, height);
ctx.filter = `contrast(${contrast}) saturate(${saturation})`;
ctx.drawImage(img, 0, 0, width, height);
ctx.filter = 'none';
3. Scanline Overlay
// @param opacity 0.18 0.0 0.6 "Opacity"
// @param spacing 4 2 12 "Spacing"
ctx.clearRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
ctx.fillStyle = `rgba(0,0,0,${opacity})`;
for (let y = 0; y < height; y += spacing) {
ctx.fillRect(0, y, width, 1);
}
4. Vignette Overlay
// @param amount 0.45 0.0 1.0 "Amount"
ctx.clearRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
const g = ctx.createRadialGradient(
width / 2, height / 2, Math.min(width, height) * 0.2,
width / 2, height / 2, Math.max(width, height) * 0.65
);
g.addColorStop(0, 'rgba(0,0,0,0)');
g.addColorStop(1, `rgba(0,0,0,${amount})`);
ctx.fillStyle = g;
ctx.fillRect(0, 0, width, height);
Audio / Speech Basics
Audio/Speech filters configure audio processing via Web Audio API snippets or JSON presets. They run through a separate pipeline from the video filters and do not have access to GLSL or Canvas 2D variables.
u_video, v_uv, ctx or any other video/canvas variables.
| Variable | Available in Audio filters |
|---|---|
ctx | Canvas 2D context — not available |
u_video | Video texture — not available |
video | HTMLVideoElement reference — available |
canvas | Overlay canvas — available (for cleanup hooks) |
canvas._gvfAudioCleanup | Function to call on filter teardown — available |
Audio / Speech Examples
1. Speech Enhancer Preset JSON
{
"name": "Speech Enhancer",
"type": "speech",
"gain": 1.15,
"highpassHz": 90,
"presenceBoostDb": 3,
"deEss": 0.25,
"noiseGate": 0.12
}
2. Web Audio EQ Concept
// Web Audio based EQ filter. // GVF provides audioContext and sourceNode via window.__gvfAudio(video). const highpass = audioContext.createBiquadFilter(); highpass.type = 'highpass'; highpass.frequency.value = 90; const presence = audioContext.createBiquadFilter(); presence.type = 'peaking'; presence.frequency.value = 3200; presence.Q.value = 1.0; presence.gain.value = 3; sourceNode.connect(highpass); highpass.connect(presence); presence.connect(audioContext.destination);
3. Noise Gate Preset
{
"name": "Light Noise Gate",
"type": "noise-gate",
"threshold": -42,
"attackMs": 8,
"releaseMs": 120,
"makeupGain": 1.0
}
Recommended Filter Order
Anime / Cartoons
1. Chroma Reconstruction 2. Denoise 3. Upscale / Restore 4. Sharpen 5. Color / LUT 6. Grain (optional)
Real Video / Twitch / YouTube
1. Mild Denoise 2. FSR / RAVU / Restore 3. Very Light Sharpen 4. Contrast / Gamma 5. LUT (optional)
DRM Pages (Netflix, Disney+, etc.)
1. Native GPU video enhancement (e.g. RTX VSR, Intel XeSS) 2. SVG / CSS Contrast 3. SVG / CSS Color 4. LUT-like SVG color matrix
Common Errors and Fixes
input_size undeclared
Cause: mpv/libplacebo shader variable not supported in raw WebGL.
Fix: Use GVF compatibility aliases or rewrite as a single-pass WebGL shader.
macro redefined
Cause: multiple mpv passes define the same macro names.
Fix: Remove duplicate #define blocks or use a GVF single-pass fallback version.
wrong operand types
Cause: mixing int/float or vec2/vec3/vec4 incorrectly.
Wrong: 2 * color Wrong: vec2Value * vec4Value Right: 2.0 * color Right: vec2Value * vec4Value.xy
sampler undeclared
Cause: HLSL shader pasted into a GLSL field.
HLSL: sampler s0 : register(s0); tex2D(s0, uv) GLSL: uniform sampler2D u_video; texture(u_video, v_uv)
unexpected token after conditional expression
Cause: #if trying to branch on a runtime uniform value.
Wrong: #if u_enable == 1 // ... #endif Right: if (u_enable > 0.5) { // ... }
u_contrast / u_strength already declared
Cause: using a built-in uniform name in a @uniform line.
Wrong: // @uniform float u_contrast 1.0 0.5 2.0 "Contrast" Right: // @uniform float u_my_contrast 1.0 0.5 2.0 "Contrast"
Performance Tips
- Use the smallest kernel radius that still looks good — a 3×3 loop is far cheaper than 5×5.
- Avoid 5×5 or 7×7 sample loops on integrated or weak GPUs.
- Do not stack multiple denoise filters — one good bilateral is better than three weak blurs.
- Do not stack multiple sharpen filters — they amplify each other's halos.
- Prefer SVG for simple color and contrast effects — zero GPU overhead.
- Use GLSL only when real multi-pixel sampling is needed.
- On DRM sites, SVG/CSS + native GPU video enhancement (RTX VSR, XeSS, etc.) is the only viable path.
- Use the GLSL Mode selector in the Settings HUD to reduce render rate to 24 fps if the effect doesn't need to be 60 fps.
Good chain
Denoise mild → Restore/Upscale → Sharpen mild → Color
Bad chain
FSR → Anime4K → ACNet → CUT → CAS → Adaptive Sharpen
Quick Templates
SVG Template
<feColorMatrix type="saturate" values="1.2"/>
GLSL Template
// @uniform float u_mix 1.0 0.0 1.0 "Output / Effect Mix"
void main() {
vec4 src = texture(u_video, v_uv);
vec3 fx = src.rgb;
// edit fx here
fragColor = vec4(mix(src.rgb, fx, u_mix), src.a);
}
Canvas 2D Template
// @param opacity 0.5 0.0 1.0 "Opacity"
ctx.clearRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
ctx.fillStyle = `rgba(0,0,0,${opacity})`;
ctx.fillRect(0, 0, width, height);
Audio Preset Template
{
"name": "My Audio Preset",
"type": "speech",
"gain": 1.0,
"noiseGate": 0.1
}