use crate::pipeline::{
BlendComponent, BlendFactor, BlendOperation, BlendState, ColorTargetState,
PipelineKey, RenderPipelineDescriptor, VertexAttribute, VertexBufferLayout, VertexFormat,
};
use crate::shader::builtin;
use crate::texture::TextureFormat;
use skia_rs_paint::{BlendMode, Paint, Style};
use skia_rs_paint::shader::{
LinearGradient, RadialGradient, Shader, ShaderKind, ShaderRef, SweepGradient,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BuiltinShader {
SolidColor,
LinearGradient,
RadialGradient,
SweepGradient,
Image,
Unsupported,
}
impl BuiltinShader {
pub fn wgsl_sources(self) -> (&'static str, &'static str) {
match self {
Self::SolidColor => (builtin::SOLID_COLOR_VS, builtin::SOLID_COLOR_FS),
Self::LinearGradient => (builtin::GRADIENT_VS, builtin::LINEAR_GRADIENT_FS),
Self::RadialGradient => (builtin::GRADIENT_VS, builtin::RADIAL_GRADIENT_FS),
Self::SweepGradient => (builtin::GRADIENT_VS, builtin::LINEAR_GRADIENT_FS),
Self::Image => (builtin::TEXTURED_VS, builtin::TEXTURED_FS),
Self::Unsupported => (builtin::SOLID_COLOR_VS, builtin::SOLID_COLOR_FS),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlendClass {
PorterDuff,
Advanced,
}
pub fn blend_mode_to_state(mode: BlendMode) -> (BlendState, BlendClass) {
let pma_alpha = BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
};
let (color, class) = match mode {
BlendMode::Clear => (
BlendComponent {
src_factor: BlendFactor::Zero,
dst_factor: BlendFactor::Zero,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::Src => (
BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::Zero,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::Dst => (
BlendComponent {
src_factor: BlendFactor::Zero,
dst_factor: BlendFactor::One,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::SrcOver => (
BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::DstOver => (
BlendComponent {
src_factor: BlendFactor::OneMinusDstAlpha,
dst_factor: BlendFactor::One,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::SrcIn => (
BlendComponent {
src_factor: BlendFactor::DstAlpha,
dst_factor: BlendFactor::Zero,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::DstIn => (
BlendComponent {
src_factor: BlendFactor::Zero,
dst_factor: BlendFactor::SrcAlpha,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::SrcOut => (
BlendComponent {
src_factor: BlendFactor::OneMinusDstAlpha,
dst_factor: BlendFactor::Zero,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::DstOut => (
BlendComponent {
src_factor: BlendFactor::Zero,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::SrcATop => (
BlendComponent {
src_factor: BlendFactor::DstAlpha,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::DstATop => (
BlendComponent {
src_factor: BlendFactor::OneMinusDstAlpha,
dst_factor: BlendFactor::SrcAlpha,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::Xor => (
BlendComponent {
src_factor: BlendFactor::OneMinusDstAlpha,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::Plus => (
BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::One,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::Modulate => (
BlendComponent {
src_factor: BlendFactor::Zero,
dst_factor: BlendFactor::Src,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::Screen => (
BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::OneMinusSrc,
operation: BlendOperation::Add,
},
BlendClass::PorterDuff,
),
BlendMode::Overlay
| BlendMode::Darken
| BlendMode::Lighten
| BlendMode::ColorDodge
| BlendMode::ColorBurn
| BlendMode::HardLight
| BlendMode::SoftLight
| BlendMode::Difference
| BlendMode::Exclusion
| BlendMode::Multiply
| BlendMode::Hue
| BlendMode::Saturation
| BlendMode::Color
| BlendMode::Luminosity => (
BlendComponent {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
BlendClass::Advanced,
),
};
(BlendState { color, alpha: pma_alpha }, class)
}
#[derive(Debug, Clone)]
pub struct PipelineSelection {
pub shader: BuiltinShader,
pub blend: BlendState,
pub blend_class: BlendClass,
pub style: Style,
}
#[derive(Debug, Clone)]
pub struct PaintUniforms {
pub bytes: Vec<u8>,
}
impl PaintUniforms {
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct PaintPlan {
pub selection: PipelineSelection,
pub uniforms: PaintUniforms,
pub pipeline_key: PipelineKey,
}
pub fn paint_to_pipeline(paint: &Paint) -> PaintPlan {
let (blend, blend_class) = blend_mode_to_state(paint.blend_mode());
let (shader_kind, uniforms) = classify_paint_shader(paint);
let selection = PipelineSelection {
shader: shader_kind,
blend,
blend_class,
style: paint.style(),
};
let descriptor = build_pipeline_descriptor(&selection);
let pipeline_key = PipelineKey::from_descriptor(&descriptor);
PaintPlan {
selection,
uniforms,
pipeline_key,
}
}
pub fn build_pipeline_descriptor(selection: &PipelineSelection) -> RenderPipelineDescriptor {
build_pipeline_descriptor_with_target(selection, TextureFormat::Rgba8Unorm)
}
pub fn build_pipeline_descriptor_with_target(
selection: &PipelineSelection,
target: TextureFormat,
) -> RenderPipelineDescriptor {
let (vs, fs) = selection.shader.wgsl_sources();
let vertex_layout = VertexBufferLayout {
stride: 16,
step_mode: crate::pipeline::VertexStepMode::Vertex,
attributes: vec![
VertexAttribute {
location: 0,
offset: 0,
format: VertexFormat::Float32x2,
},
VertexAttribute {
location: 1,
offset: 8,
format: VertexFormat::Float32x2,
},
],
};
let color_target = ColorTargetState {
format: target,
blend: Some(selection.blend),
write_mask: crate::pipeline::ColorWriteMask::ALL,
};
RenderPipelineDescriptor::new(vs, fs)
.with_vertex_buffer(vertex_layout)
.with_color_target(color_target)
}
fn classify_paint_shader(paint: &Paint) -> (BuiltinShader, PaintUniforms) {
match paint.shader() {
None => {
let c = paint.color();
(
BuiltinShader::SolidColor,
pack_solid_color(c.r, c.g, c.b, c.a),
)
}
Some(shader_ref) => classify_shader_ref(shader_ref, paint),
}
}
fn classify_shader_ref(shader_ref: &ShaderRef, paint: &Paint) -> (BuiltinShader, PaintUniforms) {
match shader_ref.shader_kind() {
ShaderKind::Color => {
let c = paint.color();
(
BuiltinShader::SolidColor,
pack_solid_color(c.r, c.g, c.b, c.a),
)
}
ShaderKind::LinearGradient => {
(
BuiltinShader::LinearGradient,
pack_linear_gradient_from_shader(shader_ref.as_ref(), paint),
)
}
ShaderKind::RadialGradient => (
BuiltinShader::RadialGradient,
pack_radial_gradient_from_shader(shader_ref.as_ref(), paint),
),
ShaderKind::SweepGradient => (
BuiltinShader::SweepGradient,
pack_sweep_gradient_from_shader(shader_ref.as_ref(), paint),
),
ShaderKind::Image => {
let c = paint.color();
(BuiltinShader::Image, pack_solid_color(c.r, c.g, c.b, c.a))
}
_ => {
let c = paint.color();
(
BuiltinShader::Unsupported,
pack_solid_color(c.r, c.g, c.b, c.a),
)
}
}
}
fn pack_solid_color(r: f32, g: f32, b: f32, a: f32) -> PaintUniforms {
let mut bytes = Vec::with_capacity(16);
bytes.extend_from_slice(&r.to_le_bytes());
bytes.extend_from_slice(&g.to_le_bytes());
bytes.extend_from_slice(&b.to_le_bytes());
bytes.extend_from_slice(&a.to_le_bytes());
PaintUniforms { bytes }
}
fn pack_linear_gradient(
start: [f32; 2],
end: [f32; 2],
color0: [f32; 4],
color1: [f32; 4],
) -> PaintUniforms {
let mut bytes = Vec::with_capacity(48);
bytes.extend_from_slice(&start[0].to_le_bytes());
bytes.extend_from_slice(&start[1].to_le_bytes());
bytes.extend_from_slice(&end[0].to_le_bytes());
bytes.extend_from_slice(&end[1].to_le_bytes());
for c in color0 {
bytes.extend_from_slice(&c.to_le_bytes());
}
for c in color1 {
bytes.extend_from_slice(&c.to_le_bytes());
}
PaintUniforms { bytes }
}
fn pack_radial_gradient(
center: [f32; 2],
radius: f32,
color0: [f32; 4],
color1: [f32; 4],
) -> PaintUniforms {
let mut bytes = Vec::with_capacity(48);
bytes.extend_from_slice(¢er[0].to_le_bytes());
bytes.extend_from_slice(¢er[1].to_le_bytes());
bytes.extend_from_slice(&radius.to_le_bytes());
bytes.extend_from_slice(&0.0f32.to_le_bytes()); for c in color0 {
bytes.extend_from_slice(&c.to_le_bytes());
}
for c in color1 {
bytes.extend_from_slice(&c.to_le_bytes());
}
PaintUniforms { bytes }
}
fn pack_linear_gradient_from_shader(shader: &dyn Shader, paint: &Paint) -> PaintUniforms {
let (start, end, c0, c1) = sample_linear_endpoints(shader);
let c0 = apply_paint_alpha(c0, paint);
let c1 = apply_paint_alpha(c1, paint);
pack_linear_gradient(start, end, c0, c1)
}
fn pack_radial_gradient_from_shader(shader: &dyn Shader, paint: &Paint) -> PaintUniforms {
let (center, radius, c0, c1) = sample_radial_endpoints(shader);
let c0 = apply_paint_alpha(c0, paint);
let c1 = apply_paint_alpha(c1, paint);
pack_radial_gradient(center, radius, c0, c1)
}
fn pack_sweep_gradient_from_shader(shader: &dyn Shader, paint: &Paint) -> PaintUniforms {
let (start, end, c0, c1) = sample_sweep_as_linear(shader);
let c0 = apply_paint_alpha(c0, paint);
let c1 = apply_paint_alpha(c1, paint);
pack_linear_gradient(start, end, c0, c1)
}
fn sample_linear_endpoints(shader: &dyn Shader) -> ([f32; 2], [f32; 2], [f32; 4], [f32; 4]) {
if let Some(g) = as_linear_gradient(shader) {
let start = [g.start().x, g.start().y];
let end = [g.end().x, g.end().y];
let colors = g.colors();
let c0 = colors.first().copied().unwrap_or_default();
let c1 = colors.last().copied().unwrap_or_default();
return (start, end, [c0.r, c0.g, c0.b, c0.a], [c1.r, c1.g, c1.b, c1.a]);
}
let s = shader.sample(0.0, 0.0);
let e = shader.sample(1.0, 0.0);
(
[0.0, 0.0],
[1.0, 0.0],
[s.r, s.g, s.b, s.a],
[e.r, e.g, e.b, e.a],
)
}
fn sample_radial_endpoints(shader: &dyn Shader) -> ([f32; 2], f32, [f32; 4], [f32; 4]) {
if let Some(g) = as_radial_gradient(shader) {
let center = [g.center().x, g.center().y];
let radius = g.radius();
let colors = g.colors();
let c0 = colors.first().copied().unwrap_or_default();
let c1 = colors.last().copied().unwrap_or_default();
return (center, radius, [c0.r, c0.g, c0.b, c0.a], [c1.r, c1.g, c1.b, c1.a]);
}
let s = shader.sample(0.0, 0.0);
let e = shader.sample(1.0, 0.0);
([0.0, 0.0], 1.0, [s.r, s.g, s.b, s.a], [e.r, e.g, e.b, e.a])
}
fn sample_sweep_as_linear(shader: &dyn Shader) -> ([f32; 2], [f32; 2], [f32; 4], [f32; 4]) {
if let Some(g) = as_sweep_gradient(shader) {
let center = [g.center().x, g.center().y];
let end = [center[0] + 1.0, center[1]];
let colors = g.colors();
let c0 = colors.first().copied().unwrap_or_default();
let c1 = colors.last().copied().unwrap_or_default();
return (center, end, [c0.r, c0.g, c0.b, c0.a], [c1.r, c1.g, c1.b, c1.a]);
}
let s = shader.sample(0.0, 0.0);
let e = shader.sample(1.0, 0.0);
(
[0.0, 0.0],
[1.0, 0.0],
[s.r, s.g, s.b, s.a],
[e.r, e.g, e.b, e.a],
)
}
fn apply_paint_alpha(color: [f32; 4], paint: &Paint) -> [f32; 4] {
let a = paint.alpha();
[color[0], color[1], color[2], color[3] * a]
}
fn as_linear_gradient(shader: &dyn Shader) -> Option<&LinearGradient> {
if shader.shader_kind() != ShaderKind::LinearGradient {
return None;
}
let _ = shader;
None
}
fn as_radial_gradient(shader: &dyn Shader) -> Option<&RadialGradient> {
if shader.shader_kind() != ShaderKind::RadialGradient {
return None;
}
let _ = shader;
None
}
fn as_sweep_gradient(shader: &dyn Shader) -> Option<&SweepGradient> {
if shader.shader_kind() != ShaderKind::SweepGradient {
return None;
}
let _ = shader;
None
}
#[cfg(test)]
mod tests {
use super::*;
use skia_rs_core::Color4f;
use skia_rs_paint::shader::{ColorShader, LinearGradient as LG, RadialGradient as RG, TileMode};
use skia_rs_paint::Paint;
use skia_rs_core::Point;
use std::sync::Arc;
#[test]
fn test_solid_color_paint_plan() {
let mut paint = Paint::new();
paint.set_color(Color4f::new(1.0, 0.5, 0.25, 1.0));
paint.set_blend_mode(BlendMode::SrcOver);
let plan = paint_to_pipeline(&paint);
assert_eq!(plan.selection.shader, BuiltinShader::SolidColor);
assert_eq!(plan.selection.blend_class, BlendClass::PorterDuff);
assert_eq!(plan.uniforms.len(), 16); let r = f32::from_le_bytes(plan.uniforms.bytes[0..4].try_into().unwrap());
assert!((r - 1.0).abs() < 1e-6);
}
#[test]
fn test_blend_mode_to_state_porterduff() {
let (state, class) = blend_mode_to_state(BlendMode::SrcOver);
assert_eq!(class, BlendClass::PorterDuff);
assert_eq!(state.color.src_factor, BlendFactor::One);
assert_eq!(state.color.dst_factor, BlendFactor::OneMinusSrcAlpha);
let (state, class) = blend_mode_to_state(BlendMode::Plus);
assert_eq!(class, BlendClass::PorterDuff);
assert_eq!(state.color.src_factor, BlendFactor::One);
assert_eq!(state.color.dst_factor, BlendFactor::One);
let (_state, class) = blend_mode_to_state(BlendMode::Clear);
assert_eq!(class, BlendClass::PorterDuff);
}
#[test]
fn test_blend_mode_to_state_advanced() {
for mode in [
BlendMode::Overlay,
BlendMode::Multiply,
BlendMode::HardLight,
BlendMode::Hue,
BlendMode::Luminosity,
] {
let (_state, class) = blend_mode_to_state(mode);
assert_eq!(class, BlendClass::Advanced, "mode {:?}", mode);
}
}
#[test]
fn test_linear_gradient_paint_plan() {
let gradient = LG::new(
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
vec![
Color4f::new(1.0, 0.0, 0.0, 1.0),
Color4f::new(0.0, 0.0, 1.0, 1.0),
],
None,
TileMode::Clamp,
);
let mut paint = Paint::new();
paint.set_shader(Some(Arc::new(gradient)));
let plan = paint_to_pipeline(&paint);
assert_eq!(plan.selection.shader, BuiltinShader::LinearGradient);
assert_eq!(plan.uniforms.len(), 48);
}
#[test]
fn test_radial_gradient_paint_plan() {
let gradient = RG::new(
Point::new(0.5, 0.5),
1.0,
vec![
Color4f::new(1.0, 1.0, 1.0, 1.0),
Color4f::new(0.0, 0.0, 0.0, 1.0),
],
None,
TileMode::Clamp,
);
let mut paint = Paint::new();
paint.set_shader(Some(Arc::new(gradient)));
let plan = paint_to_pipeline(&paint);
assert_eq!(plan.selection.shader, BuiltinShader::RadialGradient);
assert_eq!(plan.uniforms.len(), 48);
}
#[test]
fn test_pipeline_key_stable_across_calls() {
let mut paint = Paint::new();
paint.set_color(Color4f::new(1.0, 0.0, 0.0, 1.0));
let a = paint_to_pipeline(&paint).pipeline_key;
let b = paint_to_pipeline(&paint).pipeline_key;
assert_eq!(a, b, "identical paints must produce identical pipeline keys");
}
#[test]
fn test_pipeline_key_differs_for_different_shaders() {
let mut solid = Paint::new();
solid.set_color(Color4f::new(1.0, 0.0, 0.0, 1.0));
let mut lg = Paint::new();
let gradient = LG::new(
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
vec![Color4f::new(1.0, 0.0, 0.0, 1.0), Color4f::new(0.0, 0.0, 1.0, 1.0)],
None,
TileMode::Clamp,
);
lg.set_shader(Some(Arc::new(gradient)));
let key_a = paint_to_pipeline(&solid).pipeline_key;
let key_b = paint_to_pipeline(&lg).pipeline_key;
assert_ne!(key_a, key_b);
}
#[test]
fn test_color_shader_maps_to_solid() {
let shader = ColorShader::new(Color4f::new(0.25, 0.5, 0.75, 1.0));
let mut paint = Paint::new();
paint.set_color(Color4f::new(0.25, 0.5, 0.75, 1.0));
paint.set_shader(Some(Arc::new(shader)));
let plan = paint_to_pipeline(&paint);
assert_eq!(plan.selection.shader, BuiltinShader::SolidColor);
}
#[test]
fn test_pipeline_descriptor_has_correct_layout() {
let mut paint = Paint::new();
paint.set_color(Color4f::new(1.0, 0.0, 0.0, 1.0));
let plan = paint_to_pipeline(&paint);
let desc = build_pipeline_descriptor(&plan.selection);
assert_eq!(desc.vertex_buffers.len(), 1);
assert_eq!(desc.vertex_buffers[0].stride, 16);
assert_eq!(desc.vertex_buffers[0].attributes.len(), 2);
assert_eq!(desc.color_targets.len(), 1);
assert!(desc.color_targets[0].blend.is_some());
}
#[test]
fn test_paint_alpha_affects_uniforms() {
let gradient = LG::new(
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
vec![Color4f::new(1.0, 1.0, 1.0, 1.0), Color4f::new(1.0, 1.0, 1.0, 1.0)],
None,
TileMode::Clamp,
);
let mut paint = Paint::new();
paint.set_shader(Some(Arc::new(gradient)));
paint.set_alpha(0.5);
let plan = paint_to_pipeline(&paint);
let alpha_bytes = &plan.uniforms.bytes[28..32];
let alpha = f32::from_le_bytes(alpha_bytes.try_into().unwrap());
assert!((alpha - 0.5).abs() < 1e-6, "paint alpha should fold into uniform color");
}
#[test]
fn test_stroke_style_preserved() {
let mut paint = Paint::new();
paint.set_style(Style::Stroke);
let plan = paint_to_pipeline(&paint);
assert_eq!(plan.selection.style, Style::Stroke);
}
}