// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
// Derived from vello_sparse_shaders 0.2.0 (`shaders/filter.wesl`); its
// `import package::helpers::...` lines are resolved by prepending
// `helpers.wgsl`, the blur kernels it calls by prepending `filters_blur.wgsl`,
// and the drop-shadow passes it calls by prepending
// `filters_drop_shadow.wgsl`, at load time (`crate::filters::FILTER`).
//
// One filter pass over one destination page: the vertex stage places the quad
// the pass writes, the fragment stage dispatches on the pass kind and returns
// that texel's filtered colour. A whole Gaussian blur, or a whole shadow-only
// drop shadow, is a sequence of these passes, each its own render pass over
// the page the previous one did not write, planned host-side by
// `crate::filters::blur`/`crate::filters::drop_shadow`.
//
// The blur half of the reference's pass set, plus the offset and colourize
// halves a shadow-only drop shadow needs, are implemented here. Flood (1) and
// the drop-shadow composite that reads a layer's own unfiltered content back
// (7) are still reserved rather than implemented — this engine never
// composites a filter layer's original content back over its shadow (see
// `crate::filters::drop_shadow`'s own doc for why) — so the numbering of
// every pass kind is kept and those two arms can be added later without
// renumbering the wire format; a pass kind this module does not implement can
// never reach it, because the scheduler refuses every filter but a blur or a
// shadow-only drop shadow before a pass is ever planned.
// The texture holding the encoded parameters of every filter in the frame.
@group(0) @binding(0)
var filter_data: texture_2d<u32>;
// The page holding this pass's source, one half of the ping-pong pair.
@group(1) @binding(0)
var source_texture: texture_2d<f32>;
// A bilinear sampler over `source_texture`.
@group(1) @binding(1)
var linear_sampler: sampler;
// Keep every constant and layout below in sync with `crate::filters::blur`.
// Every filter's parameter block is this many bytes, whatever its kind, which
// is what lets one texel offset address any of them. Declared here rather than
// used here: `load_filter_texel` is handed an offset the host already expressed
// in texels, and these are the numbers that offset was computed from.
const FILTER_SIZE_BYTES: u32 = 48u;
const FILTER_SIZE_U32: u32 = FILTER_SIZE_BYTES / 4u;
const TEXELS_PER_FILTER: u32 = FILTER_SIZE_U32 / 4u;
// Pass kind 1 (flood) and 7 (the drop-shadow composite that reads a layer's
// own unfiltered content back) are the reference's; they are reserved rather
// than implemented (see this file's header).
const PASS_COPY: u32 = 0u;
const PASS_OFFSET: u32 = 2u;
const PASS_DOWNSCALE: u32 = 3u;
const PASS_BLUR_H: u32 = 4u;
const PASS_BLUR_V: u32 = 5u;
const PASS_UPSCALE: u32 = 6u;
const PASS_COLORIZE: u32 = 8u;
// Transparent border a decimated pass overdraws around the region it writes.
//
// Half a kernel is as far as a bilinear tap can reach past the region it is
// filtering, so a border of transparent black that wide is what surrounds a
// decimated region with the transparency the next pass's kernel expects there.
// It is a second line rather than the first one: the kernels bound their own
// taps against the source region (`sample_region_bilinear` in
// `filters_blur.wgsl`, `drop_shadow_load_checked` in
// `filters_drop_shadow.wgsl`), so what a tap would have read past the region
// no longer decides the result. The scheduler clears a filter round's whole
// destination page besides.
//
// Keep in sync with `FILTER_ATLAS_PADDING` in `crate::filters::blur`.
const FILTER_ATLAS_PADDING: u32 = 6u;
// The packed header in the first word of a filter's parameter block:
// bits [0:4] filter_type (5 bits)
// bits [5:6] edge_mode (2 bits, blur only; ignored by this module)
// bits [7:10] n_decimations (4 bits, blur only; read host-side only)
// bits [11:12] n_linear_taps (2 bits, blur only)
// bit [13] composite_original (drop shadow only; read host-side only)
// bits [14:31] reserved
// One texel of the filter parameter block starting at `texel_offset`.
fn load_filter_texel(texel_offset: u32, texel_index: u32) -> vec4<u32> {
let w = textureDimensions(filter_data).x;
let flat_index = texel_offset + texel_index;
return textureLoad(filter_data, flat_index_to_texture_coord(flat_index, w), 0);
}
// How many merged bilinear tap pairs per side the blur kernel carries.
fn get_filter_header_n_linear_taps(texel0: vec4<u32>) -> u32 { return (texel0.x >> 11u) & 0x3u; }
// The blur kernel's centre weight.
fn get_blur_center_weight(texel0: vec4<u32>) -> f32 { return bitcast<f32>(texel0.y); }
// The blur kernel's merged tap weights. Assumes `MAX_TAPS_PER_SIDE` is 3.
fn get_blur_linear_weights(texel0: vec4<u32>, texel1: vec4<u32>) -> vec3<f32> {
return vec3<f32>(
bitcast<f32>(texel0.z),
bitcast<f32>(texel0.w),
bitcast<f32>(texel1.x),
);
}
// The blur kernel's merged tap offsets. Assumes `MAX_TAPS_PER_SIDE` is 3.
fn get_blur_linear_offsets(texel1: vec4<u32>) -> vec3<f32> {
return vec3<f32>(
bitcast<f32>(texel1.y),
bitcast<f32>(texel1.z),
bitcast<f32>(texel1.w),
);
}
// Must stay byte-compatible with `crate::filters::blur::FilterInstanceData`.
struct FilterInstanceData {
// Origin of this pass's source region in the source page, packed as u16s.
@location(0)
source_origin: u32,
// Extent of this pass's source region, packed as u16s — the bound every
// kernel holds its taps inside (`sample_region_bilinear`,
// `drop_shadow_load_checked`).
@location(1)
source_size: u32,
// Origin of this pass's destination region in the destination page, packed
// as u16s.
@location(2)
dest_origin: u32,
// Extent of this pass's destination region, packed as u16s.
@location(3)
dest_size: u32,
// Extent of the whole destination page, packed as u16s.
@location(4)
dest_texture_size: u32,
// Texel offset of this filter's parameter block in `filter_data`.
@location(5)
filter_data_offset: u32,
// Extent of the filter layer's unscaled region, packed as u16s.
@location(6)
original_size: u32,
// Which pass of the filter's sequence this instance runs.
@location(7)
filter_pass_kind: u32,
}
struct FilterVertexOutput {
@builtin(position)
position: vec4<f32>,
@location(0) @interpolate(flat)
filter_data_offset: u32,
@location(1) @interpolate(flat)
source_origin: vec2<u32>,
@location(2) @interpolate(flat)
source_size: vec2<u32>,
@location(3) @interpolate(flat)
dest_origin: vec2<u32>,
@location(4) @interpolate(flat)
dest_size: vec2<u32>,
@location(5) @interpolate(flat)
filter_pass_kind: u32,
}
@vertex
fn vs_main(
@builtin(vertex_index) vertex_index: u32,
instance: FilterInstanceData,
) -> FilterVertexOutput {
let source_origin = unpack_u16_pair(instance.source_origin);
let source_size = unpack_u16_pair(instance.source_size);
let dest_origin = unpack_u16_pair(instance.dest_origin);
let dest_size = unpack_u16_pair(instance.dest_size);
let dest_texture_size = vec2<f32>(unpack_u16_pair(instance.dest_texture_size));
let original_size = unpack_u16_pair(instance.original_size);
// A decimated pass writes fewer texels than the pass before it did, so the
// quad is drawn a padding border wider than the region and the fragment
// stage writes transparent black over the difference — bounded by the
// layer's own unscaled extent, past which nothing is ever sampled.
let render_size = min(original_size, dest_size + vec2(FILTER_ATLAS_PADDING));
let corner = quad_corner(vertex_index);
let dest_xy = vec2<f32>(dest_origin) + corner * vec2<f32>(render_size);
var out: FilterVertexOutput;
out.position = vec4<f32>(pixel_to_ndc(dest_xy, dest_texture_size), 0.0, 1.0);
out.filter_data_offset = instance.filter_data_offset;
out.source_origin = source_origin;
out.source_size = source_size;
out.dest_origin = dest_origin;
out.dest_size = dest_size;
out.filter_pass_kind = instance.filter_pass_kind;
return out;
}
// One texel of the source region, addressed relative to its origin.
//
// `rel_coord` is inside the region by construction: the fragment stage returns
// before reaching here for a texel of the padding border.
fn sample_source(source_origin: vec2<u32>, rel_coord: vec2<f32>) -> vec4<f32> {
let source_coord = vec2<u32>(vec2<i32>(source_origin) + vec2<i32>(rel_coord));
return textureLoad(source_texture, source_coord, 0);
}
const HORIZONTAL: vec2<f32> = vec2<f32>(1.0, 0.0);
const VERTICAL: vec2<f32> = vec2<f32>(0.0, 1.0);
@fragment
fn fs_main(
@location(0) @interpolate(flat) filter_data_offset: u32,
@location(1) @interpolate(flat) source_origin: vec2<u32>,
@location(2) @interpolate(flat) source_size: vec2<u32>,
@location(3) @interpolate(flat) dest_origin: vec2<u32>,
@location(4) @interpolate(flat) dest_size: vec2<u32>,
@location(5) @interpolate(flat) filter_pass_kind: u32,
@builtin(position) position: vec4<f32>,
) -> @location(0) vec4<f32> {
let frag_coord = vec2<u32>(position.xy);
let rel_coord = vec2<f32>(frag_coord - dest_origin);
// The padding border the vertex stage added: cleared, not filtered.
if rel_coord.x >= f32(dest_size.x) || rel_coord.y >= f32(dest_size.y) {
return vec4<f32>(0.0);
}
switch filter_pass_kind {
case PASS_COPY: {
return sample_source(source_origin, rel_coord);
}
case PASS_OFFSET: {
let filter_texel2 = load_filter_texel(filter_data_offset, 2u);
let dxdy = get_drop_shadow_offset(filter_texel2);
return offset_drop_shadow(source_texture, source_origin, source_size, rel_coord, dxdy);
}
case PASS_DOWNSCALE: {
return filter_downscale(source_texture, linear_sampler, position, source_origin, source_size, dest_origin);
}
case PASS_BLUR_H: {
let filter_texel0 = load_filter_texel(filter_data_offset, 0u);
let filter_texel1 = load_filter_texel(filter_data_offset, 1u);
return filter_convolve(
source_texture,
linear_sampler,
source_origin,
source_size,
rel_coord,
HORIZONTAL,
get_filter_header_n_linear_taps(filter_texel0),
get_blur_center_weight(filter_texel0),
get_blur_linear_weights(filter_texel0, filter_texel1),
get_blur_linear_offsets(filter_texel1),
);
}
case PASS_BLUR_V: {
let filter_texel0 = load_filter_texel(filter_data_offset, 0u);
let filter_texel1 = load_filter_texel(filter_data_offset, 1u);
return filter_convolve(
source_texture,
linear_sampler,
source_origin,
source_size,
rel_coord,
VERTICAL,
get_filter_header_n_linear_taps(filter_texel0),
get_blur_center_weight(filter_texel0),
get_blur_linear_weights(filter_texel0, filter_texel1),
get_blur_linear_offsets(filter_texel1),
);
}
case PASS_UPSCALE: {
return filter_upscale(source_texture, linear_sampler, position, source_origin, source_size, dest_origin);
}
case PASS_COLORIZE: {
let filter_texel2 = load_filter_texel(filter_data_offset, 2u);
let color = get_drop_shadow_color(filter_texel2);
return colorize_drop_shadow(source_texture, source_origin, rel_coord, color);
}
// A pass kind this module does not implement; unreachable, because the
// scheduler plans none.
default: {
return vec4<f32>(0.0);
}
}
}