use valo_dl::{
ColorFilter, FocalCircle, GradientStop, Image, MaskBlur, MaskKind, Paint, Sampling, Shader,
SpreadMode, TileMode, MAX_GRADIENT_STOPS,
};
use valo_geometry::{Color, Matrix, Point, Rect};
use crate::frame::Bindings;
use crate::pipelines::{AdvancedBlend, Blend, Frag};
use crate::shader_abi::{
blur_style_id, conical, gamma, mask_kind_id, slot, spread_id, UniformRecord,
};
use super::emit::StepEmitter;
use super::filter_output::Merge;
use super::gaussian::{BlurPass, DownsampleTaps};
use super::snapshot::Snapshot;
pub(super) struct Shading {
pub frag: Frag,
pub record: UniformRecord,
pub bindings: Bindings,
pub opaque: bool,
}
impl Shading {
pub fn none() -> Self {
Self::of(
Frag::Solid,
UniformRecord::tinted([0.0; 4]),
Bindings::Plain,
)
}
pub fn solid(tint: [f32; 4]) -> Self {
Self::of(Frag::Solid, UniformRecord::tinted(tint), Bindings::Plain)
}
pub fn rrect_blur(
rect: &Rect,
radii: [f32; 4],
color: Color,
blur: MaskBlur,
alpha: f32,
) -> Self {
let mut record = UniformRecord::tinted(scaled_premul(color, alpha));
record.set(slot::GEOM, [rect.x, rect.y, rect.right(), rect.bottom()]);
record.set(
slot::MISC,
[blur.sigma.max(0.05), blur_style_id(blur.style), 0.0, 0.0],
);
record.set(slot::RADII, radii);
Self::of(Frag::RRectBlur, record, Bindings::Plain)
}
pub fn glyphs(frag: Frag, tint: [f32; 4], page: wgpu::BindGroup) -> Self {
Self::of(frag, UniformRecord::tinted(tint), Bindings::Textured(page))
}
pub fn colour_glyphs(
page: wgpu::BindGroup,
filter: Option<ColorFilter>,
alpha: f32,
group_alpha: f32,
) -> Self {
let Some(filter) = filter else {
return Self::glyphs(Frag::GlyphColor, alpha_tint(alpha * group_alpha), page);
};
let mut record = UniformRecord::tinted(alpha_tint(group_alpha));
let frag = encode_color_filter(&mut record, filter, alpha).glyph_fragment();
Self::of(frag, record, Bindings::Textured(page))
}
fn of(frag: Frag, record: UniformRecord, bindings: Bindings) -> Self {
Self {
frag,
record,
bindings,
opaque: false,
}
}
}
impl StepEmitter<'_> {
pub fn paint_shading(&mut self, paint: &Paint, alpha: f32) -> Shading {
let mut record = UniformRecord::tinted(tinted(paint, alpha));
let (frag, bindings) = match PaintSource::of(paint) {
PaintSource::Solid => (Frag::Solid, Bindings::Plain),
PaintSource::Pattern {
image,
sampling,
local,
} => {
fill_pattern_payload(&mut record, image, sampling, local);
(
Frag::Pattern,
Bindings::Textured(self.image_bind(image, sampling)),
)
}
PaintSource::Gradient(gradient) if gradient.stops.len() > MAX_GRADIENT_STOPS => {
let (ramp, texels) = self.ramp(gradient.stops);
fill_gradient_payload(&mut record, &gradient, Some(texels));
(gradient.ramp_frag(), Bindings::Textured(ramp))
}
PaintSource::Gradient(gradient) => {
fill_gradient_payload(&mut record, &gradient, None);
(gradient.frag(), Bindings::Plain)
}
};
Shading {
opaque: alpha >= 1.0 && is_opaque_paint(paint),
..Shading::of(frag, record, bindings)
}
}
pub fn image_shading(
&mut self,
image: &Image,
src: &Rect,
dst: &Rect,
sampling: Sampling,
color_filter: Option<ColorFilter>,
alpha: f32,
) -> Shading {
let mut record = UniformRecord::tinted(alpha_tint(alpha));
record.set(slot::GEOM, image_uv(image, src, dst));
record.set(slot::DECAL, decal_flags(sampling));
let frag = match color_filter {
None => Frag::Image,
Some(filter) => encode_color_filter(&mut record, filter, 1.0).image_fragment(),
};
Shading::of(
frag,
record,
Bindings::Textured(self.image_bind(image, sampling)),
)
}
pub fn texture_shading(&self, snapshot: &Snapshot, alpha: f32) -> Shading {
let mut record = UniformRecord::tinted(alpha_tint(alpha));
record.set(slot::GEOM, snapshot.placement.uv_mapping());
Shading::of(Frag::Image, record, self.textured(&snapshot.view))
}
pub fn raster_shading(&self, view: &wgpu::TextureView, sampled: &Rect, alpha: f32) -> Shading {
let mut record = UniformRecord::tinted(alpha_tint(alpha));
record.set(slot::GEOM, full_rect_uv(sampled));
Shading::of(Frag::Image, record, self.textured(view))
}
pub fn recoloured_shading(&self, input: &Snapshot, filter: ColorFilter, alpha: f32) -> Shading {
let mut record = UniformRecord::tinted([1.0; 4]);
record.set(slot::GEOM, input.placement.uv_mapping());
let frag = encode_color_filter(&mut record, filter, alpha).image_fragment();
Shading::of(frag, record, self.textured(&input.view))
}
pub fn merged_shading(
&self,
first: &Snapshot,
second: &Snapshot,
merge: Merge,
alpha: f32,
) -> Shading {
let mut record = UniformRecord::tinted(alpha_tint(alpha));
let frag = encode_merge(
&mut record,
first.placement.uv_mapping(),
second.placement.uv_mapping(),
merge,
);
let bindings = Bindings::Textured(self.blend_bind(&first.view, &second.view));
Shading::of(frag, record, bindings)
}
pub fn mask_composite_shading(&self, mask: &Snapshot, kind: MaskKind, alpha: f32) -> Shading {
let mut record = UniformRecord::tinted(alpha_tint(alpha));
record.set(slot::GEOM, mask.placement.uv_mapping());
record.set(slot::MISC, [mask_kind_id(kind), 0.0, 0.0, 0.0]);
Shading::of(Frag::MaskComposite, record, self.textured(&mask.view))
}
pub fn blended_solid_shading(
&self,
color: Color,
alpha: f32,
mode: AdvancedBlend,
destination: &wgpu::TextureView,
size: [u32; 2],
) -> Shading {
let mut record = UniformRecord::tinted(scaled_premul(color, alpha));
set_destination_read(&mut record, mode, size);
Shading::of(Frag::BlendSolid, record, self.textured(destination))
}
pub fn blended_texture_shading(
&self,
source: &Snapshot,
alpha: f32,
mode: AdvancedBlend,
destination: &wgpu::TextureView,
size: [u32; 2],
) -> Shading {
let mut record = UniformRecord::tinted(alpha_tint(alpha));
record.set(slot::GEOM, source.placement.uv_mapping());
set_destination_read(&mut record, mode, size);
let bindings = Bindings::Textured(self.blend_bind(destination, &source.view));
Shading::of(Frag::BlendTexture, record, bindings)
}
pub fn resample_shading(&self, source: &Snapshot, region: &Rect) -> Shading {
let mut record = UniformRecord::tinted([1.0; 4]);
let texel = source.placement.size.map(|size| 1.0 / size as f32);
let uv = source.placement.region_uv_mapping(region);
encode_downsample(
&mut record,
uv,
DownsampleKernel::ONE_TAP,
texel,
None,
false,
);
Shading::of(Frag::Downsample, record, self.textured(&source.view))
}
pub fn recolour_pass_shading(
&self,
source: &Snapshot,
region: &Rect,
filter: ColorFilter,
input_alpha: f32,
) -> Shading {
let mut record = UniformRecord::tinted([1.0; 4]);
record.set(slot::GEOM, source.placement.region_uv_mapping(region));
let frag = encode_color_filter(&mut record, filter, input_alpha).pass_fragment();
Shading::of(frag, record, self.textured(&source.view))
}
pub fn merge_pass_shading(
&self,
first: &Snapshot,
second: &Snapshot,
region: &Rect,
merge: Merge,
) -> Shading {
let mut record = UniformRecord::tinted([1.0; 4]);
let frag = encode_merge(
&mut record,
first.placement.region_uv_mapping(region),
second.placement.region_uv_mapping(region),
merge,
);
let bindings = Bindings::Textured(self.blend_bind(&first.view, &second.view));
Shading::of(frag, record, bindings)
}
pub fn bake_shading(&self, source: &Image, filter: ColorFilter) -> Shading {
let size = source.size();
let mut record = UniformRecord::tinted([1.0; 4]);
record.set(
slot::GEOM,
[1.0 / size[0] as f32, 1.0 / size[1] as f32, 0.0, 0.0],
);
let frag = encode_color_filter(&mut record, filter, 1.0).pass_fragment();
Shading::of(frag, record, self.textured(source.view()))
}
pub fn downsample_shading(&self, taps: &DownsampleTaps, input: &wgpu::TextureView) -> Shading {
let mut record = UniformRecord::tinted([1.0; 4]);
let edges = taps.edges.map(|edges| edges.0);
let decal = taps.tile_mode == TileMode::Decal;
encode_downsample(
&mut record,
taps.uv,
taps.kernel,
taps.texel,
edges.as_ref(),
decal,
);
let bindings = Bindings::Textured(self.tiled_texture_bind(input, taps.tile_mode));
Shading::of(Frag::Downsample, record, bindings)
}
pub fn blur_pass_shading(
&mut self,
pass: &BlurPass,
input: &wgpu::TextureView,
size: [u32; 2],
) -> Shading {
let mut record = UniformRecord::tinted([1.0; 4]);
let texel = size.map(|size| 1.0 / size as f32);
record.set(slot::GEOM, [texel[0], texel[1], 0.0, 0.0]);
record.set(
slot::MISC,
[
pass.sample_count() as f32,
f32::from(pass.divide_by_alpha),
0.0,
0.0,
],
);
let bindings = Bindings::Blurred {
texture: self.texture_bind(input),
kernel: self.alloc_kernel(&pass.kernel_uniform()),
};
Shading::of(Frag::Blur, record, bindings)
}
fn textured(&self, view: &wgpu::TextureView) -> Bindings {
Bindings::Textured(self.texture_bind(view))
}
}
fn set_destination_read(record: &mut UniformRecord, mode: AdvancedBlend, size: [u32; 2]) {
record.set(
slot::MISC,
[mode.id() as f32, 0.0, size[0] as f32, size[1] as f32],
);
}
pub(super) fn tinted(paint: &Paint, extra: f32) -> [f32; 4] {
if paint.shader.is_none() {
scaled_premul(paint.color, extra)
} else {
alpha_tint(paint.color.a * extra)
}
}
pub(super) fn scaled_premul(color: Color, alpha: f32) -> [f32; 4] {
let [r, g, b, a] = color.premultiplied();
[r * alpha, g * alpha, b * alpha, a * alpha]
}
pub(super) fn alpha_tint(a: f32) -> [f32; 4] {
[a, a, a, a]
}
fn full_rect_uv(rect: &Rect) -> [f32; 4] {
let sx = 1.0 / rect.width;
let sy = 1.0 / rect.height;
[sx, sy, -rect.x * sx, -rect.y * sy]
}
enum PaintSource<'a> {
Solid,
Pattern {
image: &'a Image,
sampling: Sampling,
local: &'a Matrix,
},
Gradient(Gradient<'a>),
}
impl<'a> PaintSource<'a> {
fn of(paint: &'a Paint) -> Self {
let Some(shader) = &paint.shader else {
return PaintSource::Solid;
};
let (kind, stops, local) = match shader {
Shader::Image {
image,
sampling,
local,
} => {
return PaintSource::Pattern {
image,
sampling: *sampling,
local,
}
}
Shader::Linear {
start,
end,
stops,
spread,
local,
} => (
GradientKind::Linear {
start: *start,
end: *end,
spread: *spread,
},
stops,
local,
),
Shader::Radial {
center,
radius,
focus,
stops,
spread,
local,
} => (
GradientKind::Radial {
center: *center,
radius: *radius,
focus: *focus,
spread: *spread,
},
stops,
local,
),
Shader::Sweep {
center,
start_angle,
stops,
local,
} => (
GradientKind::Sweep {
center: *center,
start_angle: *start_angle,
},
stops,
local,
),
};
PaintSource::Gradient(Gradient { kind, stops, local })
}
}
struct Gradient<'a> {
kind: GradientKind,
stops: &'a [GradientStop],
local: &'a Matrix,
}
enum GradientKind {
Linear {
start: Point,
end: Point,
spread: SpreadMode,
},
Radial {
center: Point,
radius: f32,
focus: Option<FocalCircle>,
spread: SpreadMode,
},
Sweep { center: Point, start_angle: f32 },
}
impl Gradient<'_> {
fn frag(&self) -> Frag {
match self.kind {
GradientKind::Linear { .. } => Frag::Linear,
GradientKind::Radial { .. } => Frag::Radial,
GradientKind::Sweep { .. } => Frag::Sweep,
}
}
fn ramp_frag(&self) -> Frag {
match self.kind {
GradientKind::Linear { .. } => Frag::LinearRamp,
GradientKind::Radial { .. } => Frag::RadialRamp,
GradientKind::Sweep { .. } => Frag::SweepRamp,
}
}
}
fn is_opaque_paint(paint: &Paint) -> bool {
paint.mask_blur.is_none()
&& paint.effective_image_filter().is_none()
&& paint.color.a >= 1.0
&& paint.shader.as_ref().is_none_or(shader_opaque)
}
fn shader_opaque(shader: &Shader) -> bool {
if let Shader::Radial {
center,
focus: Some(circle),
..
} = shader
{
if circle.radius > 0.0 || circle.center != *center {
return false;
}
}
let stops = match shader {
Shader::Linear { stops, .. }
| Shader::Radial { stops, .. }
| Shader::Sweep { stops, .. } => stops,
Shader::Image { .. } => return false,
};
stops.iter().all(|stop| stop.color.a >= 1.0)
}
#[derive(Clone, Copy)]
enum EncodedColorFilter {
Matrix,
Blend,
Gamma,
}
impl EncodedColorFilter {
fn pass_fragment(self) -> Frag {
match self {
EncodedColorFilter::Matrix => Frag::ColorMatrix,
EncodedColorFilter::Blend => Frag::ColorBlend,
EncodedColorFilter::Gamma => Frag::ColorGamma,
}
}
fn image_fragment(self) -> Frag {
match self {
EncodedColorFilter::Matrix => Frag::ImageMatrix,
EncodedColorFilter::Blend => Frag::ImageBlend,
EncodedColorFilter::Gamma => Frag::ImageGamma,
}
}
fn glyph_fragment(self) -> Frag {
match self {
EncodedColorFilter::Matrix => Frag::GlyphColorMatrix,
EncodedColorFilter::Blend => Frag::GlyphColorBlend,
EncodedColorFilter::Gamma => Frag::GlyphColorGamma,
}
}
}
fn encode_color_filter(
record: &mut UniformRecord,
filter: ColorFilter,
input_alpha: f32,
) -> EncodedColorFilter {
match filter {
ColorFilter::Matrix(matrix) => {
for row in 0..4 {
let start = row * 5;
record.set(
slot::COLOR_MATRIX + row,
[
matrix[start],
matrix[start + 1],
matrix[start + 2],
matrix[start + 3],
],
);
}
record.set(
slot::COLOR_MATRIX + 4,
[matrix[4], matrix[9], matrix[14], matrix[19]],
);
record.set(slot::MISC, [0.0, 0.0, 0.0, input_alpha]);
EncodedColorFilter::Matrix
}
ColorFilter::Blend(color, mode) => {
record.set(slot::COLOR_MATRIX, color.premultiplied());
let id = Blend::of(mode).filter_id() as f32;
record.set(slot::MISC, [id, 0.0, 0.0, input_alpha]);
EncodedColorFilter::Blend
}
ColorFilter::LinearToSrgbGamma => encode_gamma(record, gamma::LINEAR_TO_SRGB, input_alpha),
ColorFilter::SrgbToLinearGamma => encode_gamma(record, gamma::SRGB_TO_LINEAR, input_alpha),
}
}
fn encode_gamma(
record: &mut UniformRecord,
direction: f32,
input_alpha: f32,
) -> EncodedColorFilter {
record.set(slot::MISC, [direction, 0.0, 0.0, input_alpha]);
EncodedColorFilter::Gamma
}
fn encode_merge(
record: &mut UniformRecord,
first_uv: [f32; 4],
second_uv: [f32; 4],
merge: Merge,
) -> Frag {
let (frag, switch) = match merge {
Merge::DropShadow => (Frag::DropShadow, 0.0),
Merge::BlurStyle(style) => (Frag::MaskCombine, blur_style_id(style)),
};
record.set(slot::GEOM, first_uv);
record.set(slot::SECOND_UV, second_uv);
record.set(slot::MISC, [switch, 0.0, 0.0, 0.0]);
frag
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct DownsampleKernel {
pub edge: f32,
pub ratio: f32,
}
impl DownsampleKernel {
pub const ONE_TAP: Self = Self {
edge: 0.0,
ratio: 1.0,
};
}
fn encode_downsample(
record: &mut UniformRecord,
uv: [f32; 4],
kernel: DownsampleKernel,
texel: [f32; 2],
bounds: Option<&[[f32; 4]; 4]>,
decal: bool,
) {
record.set(slot::GEOM, uv);
record.set(slot::MISC, [kernel.edge, kernel.ratio, texel[0], texel[1]]);
if let Some(lines) = bounds {
for (index, line) in lines.iter().enumerate() {
record.set(slot::DOWNSAMPLE_EDGES + index, *line);
}
}
record.set(
slot::DOWNSAMPLE_MODES,
[f32::from(bounds.is_some()), f32::from(decal), 0.0, 0.0],
);
}
fn fill_gradient_payload(
record: &mut UniformRecord,
gradient: &Gradient,
ramp_texels: Option<u32>,
) {
let (geom, angle, misc_w, spread) = match gradient.kind {
GradientKind::Linear { start, end, spread } => {
([start.x, start.y, end.x, end.y], 0.0, 0.0, spread)
}
GradientKind::Radial {
center,
radius,
focus,
spread,
} => {
let f = focus.map_or(center, |circle| circle.center);
([center.x, center.y, radius, f.x], 0.0, f.y, spread)
}
GradientKind::Sweep {
center,
start_angle,
} => (
[center.x, center.y, 0.0, 0.0],
start_angle,
0.0,
SpreadMode::Pad,
),
};
record.set(slot::GEOM, geom);
let mut inverse = gradient
.local
.invert()
.unwrap_or(Matrix::from_affine(0.0, 0.0, 0.0, 0.0, 0.0, 0.0));
let setup = match gradient.kind {
GradientKind::Radial {
center,
radius,
focus,
..
} => ConicalSetup::solve(center, radius, focus),
_ => ConicalSetup::UNUSED,
};
if let Some(focal_map) = setup.focal_map {
inverse = focal_map.then(&inverse);
}
record.set(slot::CONICAL, setup.constants);
record.set(slot::CONICAL_FLAGS, setup.flags);
let [a, b, c, d, tx, ty] = inverse.to_affine();
record.set(slot::LOCAL, [a, b, c, d]);
record.set(slot::LOCAL + 1, [tx, ty, 0.0, 0.0]);
let count = gradient.stops.len().min(MAX_GRADIENT_STOPS);
let count_lane = match ramp_texels {
Some(texels) => texels as f32,
None => count as f32,
};
record.set(slot::MISC, [count_lane, angle, spread_id(spread), misc_w]);
if ramp_texels.is_some() {
return;
}
let mut offsets = [0.0f32; MAX_GRADIENT_STOPS];
for (i, stop) in gradient.stops.iter().take(count).enumerate() {
offsets[i] = stop.offset;
record.set(slot::COLORS + i, stop.color.components());
}
record.set(
slot::OFFSETS,
[offsets[0], offsets[1], offsets[2], offsets[3]],
);
record.set(
slot::OFFSETS + 1,
[offsets[4], offsets[5], offsets[6], offsets[7]],
);
}
fn fill_pattern_payload(
record: &mut UniformRecord,
image: &Image,
sampling: Sampling,
local: &Matrix,
) {
let size = image.size();
record.set(
slot::GEOM,
[1.0 / size[0] as f32, 1.0 / size[1] as f32, 0.0, 0.0],
);
record.set(slot::DECAL, decal_flags(sampling));
let inverse = local
.invert()
.unwrap_or(Matrix::from_affine(0.0, 0.0, 0.0, 0.0, 0.0, 0.0));
let [a, b, c, d, tx, ty] = inverse.to_affine();
record.set(slot::LOCAL, [a, b, c, d]);
record.set(slot::LOCAL + 1, [tx, ty, 0.0, 0.0]);
}
fn image_uv(image: &Image, src: &Rect, dst: &Rect) -> [f32; 4] {
let (tw, th) = (image.width(), image.height());
let sx = src.width / (dst.width * tw);
let sy = src.height / (dst.height * th);
[sx, sy, src.x / tw - dst.x * sx, src.y / th - dst.y * sy]
}
fn decal_flags(sampling: Sampling) -> [f32; 4] {
[
f32::from(sampling.tile_x == TileMode::Decal),
f32::from(sampling.tile_y == TileMode::Decal),
0.0,
0.0,
]
}
struct ConicalSetup {
constants: [f32; 4],
flags: [f32; 4],
focal_map: Option<Matrix>,
}
impl ConicalSetup {
const UNUSED: Self = Self {
constants: [conical::CONCENTRIC, 0.0, 0.0, 0.0],
flags: [0.0; 4],
focal_map: None,
};
fn solve(center: Point, radius: f32, focus: Option<FocalCircle>) -> Self {
const CASE_EPSILON: f32 = 1.0e-3;
const NEARLY_ZERO: f32 = 1.0 / (1 << 12) as f32;
let start = focus.unwrap_or(FocalCircle::point(center));
let separation = (center.x - start.center.x).hypot(center.y - start.center.y);
if separation < CASE_EPSILON {
if (radius - start.radius).abs() < CASE_EPSILON {
return Self {
constants: [conical::EMPTY, 0.0, 0.0, 0.0],
..Self::UNUSED
};
}
return Self {
constants: [conical::CONCENTRIC, start.radius, radius, 0.0],
flags: [0.0; 4],
focal_map: None,
};
}
if (radius - start.radius).abs() < CASE_EPSILON {
let radius_in_unit_space = start.radius / separation;
return Self {
constants: [
conical::STRIP,
radius_in_unit_space * radius_in_unit_space,
0.0,
0.0,
],
flags: [0.0; 4],
focal_map: Some(map_to_unit_x(start.center, center)),
};
}
let (mut first, mut second) = (start.center, center);
let mut focal = start.radius / (start.radius - radius);
let is_swapped = (focal - 1.0).abs() < NEARLY_ZERO;
if is_swapped {
std::mem::swap(&mut first, &mut second);
focal = 0.0f32;
}
let focal_center = Point::new(
first.x * (1.0 - focal) + second.x * focal,
first.y * (1.0 - focal) + second.y * focal,
);
let radius_in_unit_space = (radius - start.radius).abs() / separation;
let is_focal_on_circle = (radius_in_unit_space - 1.0).abs() < NEARLY_ZERO;
let span = (1.0 - focal).abs();
let (scale_x, scale_y) = if is_focal_on_circle {
(span * 0.5, span * 0.5)
} else {
let squared = radius_in_unit_space * radius_in_unit_space;
(
span * radius_in_unit_space / (squared - 1.0),
span / (squared - 1.0).abs().sqrt(),
)
};
let is_well_behaved = !is_focal_on_circle && radius_in_unit_space > 1.0;
Self {
constants: [
conical::GENERAL,
radius_in_unit_space,
focal,
(1.0 - focal).signum(),
],
flags: [
is_swapped as u32 as f32,
is_focal_on_circle as u32 as f32,
is_well_behaved as u32 as f32,
0.0,
],
focal_map: Some(scale_after(
map_to_unit_x(focal_center, second),
scale_x,
scale_y,
)),
}
}
}
fn map_to_unit_x(from: Point, to: Point) -> Matrix {
let (dx, dy) = (to.x - from.x, to.y - from.y);
let length = dx.hypot(dy);
let (ux, uy) = (dx / length, dy / length);
Matrix::from_affine(
ux / length,
-uy / length,
uy / length,
ux / length,
-(ux * from.x + uy * from.y) / length,
(uy * from.x - ux * from.y) / length,
)
}
fn scale_after(matrix: Matrix, x: f32, y: f32) -> Matrix {
let [a, b, c, d, tx, ty] = matrix.to_affine();
Matrix::from_affine(a * x, b * y, c * x, d * y, tx * x, ty * y)
}