use rustc_hash::FxHashMap;
use valo_dl::BlendMode;
pub const SAMPLE_COUNT: u32 = 4;
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24PlusStencil8;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Frag {
Solid,
Image,
ImageMatrix,
ImageBlend,
Linear,
Radial,
Sweep,
BlendSolid,
BlendTexture,
RRectBlur,
Blur,
MaskCombine,
DropShadow,
MaskComposite,
LinearRamp,
RadialRamp,
SweepRamp,
ColorMatrix,
ColorBlend,
Pattern,
}
impl Frag {
fn entry_point(self) -> &'static str {
match self {
Frag::Solid => "fs_solid",
Frag::Image => "fs_image",
Frag::ImageMatrix => "fs_image_matrix",
Frag::ImageBlend => "fs_image_blend",
Frag::Linear => "fs_linear",
Frag::Radial => "fs_radial",
Frag::Sweep => "fs_sweep",
Frag::BlendSolid => "fs_blend_solid",
Frag::BlendTexture => "fs_blend_texture",
Frag::RRectBlur => "fs_rrect_blur",
Frag::Blur => "fs_blur",
Frag::MaskCombine => "fs_mask_combine",
Frag::DropShadow => "fs_drop_shadow",
Frag::MaskComposite => "fs_mask_composite",
Frag::LinearRamp => "fs_linear_ramp",
Frag::RadialRamp => "fs_radial_ramp",
Frag::SweepRamp => "fs_sweep_ramp",
Frag::ColorMatrix => "fs_color_matrix",
Frag::ColorBlend => "fs_color_blend",
Frag::Pattern => "fs_pattern",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PipelineKind {
Draw(Frag),
Cover(Frag),
OpaqueDraw(Frag),
OpaqueCover(Frag),
StencilFan { even_odd: bool },
ClipCover { difference: bool },
Filter(Frag),
Strip(Frag),
Text { mode: TextMode },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TextMode {
Mask,
Sdf,
Color,
}
impl PipelineKind {
fn writes_color(self) -> bool {
matches!(
self,
PipelineKind::Draw(_)
| PipelineKind::Cover(_)
| PipelineKind::OpaqueDraw(_)
| PipelineKind::OpaqueCover(_)
| PipelineKind::Filter(_)
| PipelineKind::Strip(_)
| PipelineKind::Text { .. }
)
}
fn frag(self) -> Option<Frag> {
match self {
PipelineKind::Draw(f)
| PipelineKind::Cover(f)
| PipelineKind::OpaqueDraw(f)
| PipelineKind::OpaqueCover(f)
| PipelineKind::Filter(f)
| PipelineKind::Strip(f) => Some(f),
_ => None,
}
}
fn replaces_dst(self) -> bool {
matches!(
self,
PipelineKind::OpaqueDraw(_) | PipelineKind::OpaqueCover(_) | PipelineKind::Filter(_)
) || matches!(
self.frag(),
Some(Frag::BlendSolid) | Some(Frag::BlendTexture)
)
}
fn fragment_entry(self) -> &'static str {
if let PipelineKind::Text { mode } = self {
return match mode {
TextMode::Mask => "fs_text",
TextMode::Sdf => "fs_text_sdf",
TextMode::Color => "fs_text_color",
};
}
self.frag().map_or("fs_solid", Frag::entry_point)
}
pub fn sample_count(self) -> u32 {
match self {
PipelineKind::Filter(_) => 1,
_ => SAMPLE_COUNT,
}
}
fn vertex_entry(self) -> &'static str {
match self {
PipelineKind::StencilFan { .. } | PipelineKind::Strip(_) => "vs_mesh",
PipelineKind::Text { .. } => "vs_text",
_ => "vs_quad",
}
}
fn normalized_blend(self, blend: BlendMode) -> BlendMode {
if self.writes_color() && !self.replaces_dst() {
blend
} else {
BlendMode::SrcOver
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PipelineKey {
pub format: wgpu::TextureFormat,
pub blend: BlendMode,
pub kind: PipelineKind,
}
impl PipelineKey {
pub fn new(format: wgpu::TextureFormat, blend: BlendMode, kind: PipelineKind) -> Self {
Self {
format,
blend: kind.normalized_blend(blend),
kind,
}
}
}
pub struct PipelineCache {
shader: wgpu::ShaderModule,
plain_layout: wgpu::PipelineLayout,
textured_layout: wgpu::PipelineLayout,
blend_layout: wgpu::PipelineLayout,
texture_bind_layout: wgpu::BindGroupLayout,
blend_bind_layout: wgpu::BindGroupLayout,
map: FxHashMap<PipelineKey, wgpu::RenderPipeline>,
}
impl PipelineCache {
pub fn new(device: &wgpu::Device, uniforms_layout: &wgpu::BindGroupLayout) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("valo.solid"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/solid.wgsl").into()),
});
let texture_bind_layout = texture_bind_group_layout(device);
let plain_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("valo.plain"),
bind_group_layouts: &[Some(uniforms_layout)],
immediate_size: 0,
});
let textured_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("valo.textured"),
bind_group_layouts: &[Some(uniforms_layout), Some(&texture_bind_layout)],
immediate_size: 0,
});
let blend_bind_layout = blend_bind_group_layout(device);
let blend_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("valo.blend"),
bind_group_layouts: &[Some(uniforms_layout), Some(&blend_bind_layout)],
immediate_size: 0,
});
Self {
shader,
plain_layout,
textured_layout,
blend_layout,
texture_bind_layout,
blend_bind_layout,
map: FxHashMap::default(),
}
}
pub fn blend_bind_layout(&self) -> &wgpu::BindGroupLayout {
&self.blend_bind_layout
}
pub fn texture_bind_layout(&self) -> &wgpu::BindGroupLayout {
&self.texture_bind_layout
}
pub fn ensure(&mut self, device: &wgpu::Device, key: PipelineKey) {
if !self.map.contains_key(&key) {
let pipeline = self.create(device, key);
self.map.insert(key, pipeline);
}
}
pub fn get(&self, key: &PipelineKey) -> &wgpu::RenderPipeline {
&self.map[key]
}
fn create(&self, device: &wgpu::Device, key: PipelineKey) -> wgpu::RenderPipeline {
let layout = match key.kind.frag() {
_ if matches!(key.kind, PipelineKind::Text { .. }) => &self.textured_layout,
Some(Frag::BlendTexture) | Some(Frag::MaskCombine) | Some(Frag::DropShadow) => {
&self.blend_layout
}
Some(Frag::Image)
| Some(Frag::ImageMatrix)
| Some(Frag::ImageBlend)
| Some(Frag::BlendSolid)
| Some(Frag::Blur)
| Some(Frag::MaskComposite)
| Some(Frag::LinearRamp)
| Some(Frag::RadialRamp)
| Some(Frag::SweepRamp)
| Some(Frag::ColorMatrix)
| Some(Frag::ColorBlend)
| Some(Frag::Pattern) => &self.textured_layout,
_ => &self.plain_layout,
};
let depth_stencil = match key.kind {
PipelineKind::Filter(_) => None,
kind => Some(depth_stencil(kind)),
};
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("valo.solid"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: &self.shader,
entry_point: Some(key.kind.vertex_entry()),
compilation_options: Default::default(),
buffers: vertex_buffers(key.kind),
},
fragment: Some(wgpu::FragmentState {
module: &self.shader,
entry_point: Some(key.kind.fragment_entry()),
compilation_options: Default::default(),
targets: &[Some(color_target(key))],
}),
primitive: wgpu::PrimitiveState {
topology: match key.kind {
PipelineKind::Strip(_) => wgpu::PrimitiveTopology::TriangleStrip,
_ => wgpu::PrimitiveTopology::TriangleList,
},
..Default::default()
},
depth_stencil,
multisample: wgpu::MultisampleState {
count: key.kind.sample_count(),
..Default::default()
},
multiview_mask: None,
cache: None,
})
}
}
fn texture_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("valo.texture"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
})
}
fn blend_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
let texture_entry = |binding| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
};
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("valo.blend"),
entries: &[
texture_entry(0), wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
texture_entry(2), ],
})
}
pub fn blur_style_id(style: valo_dl::BlurStyle) -> u32 {
match style {
valo_dl::BlurStyle::Normal => 0,
valo_dl::BlurStyle::Solid => 1,
valo_dl::BlurStyle::Inner => 2,
valo_dl::BlurStyle::Outer => 3,
}
}
pub fn blend_filter_id(mode: BlendMode) -> u32 {
match mode {
BlendMode::Clear => 0,
BlendMode::Src => 1,
BlendMode::Dst => 2,
BlendMode::SrcOver => 3,
BlendMode::DstOver => 4,
BlendMode::SrcIn => 5,
BlendMode::DstIn => 6,
BlendMode::SrcOut => 7,
BlendMode::DstOut => 8,
BlendMode::SrcAtop => 9,
BlendMode::DstAtop => 10,
BlendMode::Xor => 11,
BlendMode::Plus => 12,
BlendMode::Modulate => 13,
BlendMode::Screen => 14,
advanced => 15 + advanced_mode_id(advanced),
}
}
pub fn advanced_mode_id(mode: BlendMode) -> u32 {
match mode {
BlendMode::Multiply => 0,
BlendMode::Overlay => 1,
BlendMode::Darken => 2,
BlendMode::Lighten => 3,
BlendMode::ColorDodge => 4,
BlendMode::ColorBurn => 5,
BlendMode::HardLight => 6,
BlendMode::SoftLight => 7,
BlendMode::Difference => 8,
BlendMode::Exclusion => 9,
BlendMode::Hue => 10,
BlendMode::Saturation => 11,
BlendMode::Color => 12,
BlendMode::Luminosity => 13,
_ => unreachable!("pipeline-blendable mode routed to advanced path"),
}
}
const MESH_LAYOUT: [Option<wgpu::VertexBufferLayout<'static>>; 1] =
[Some(wgpu::VertexBufferLayout {
array_stride: 8,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![0 => Float32x2],
})];
const TEXT_LAYOUT: [Option<wgpu::VertexBufferLayout<'static>>; 1] =
[Some(wgpu::VertexBufferLayout {
array_stride: 16,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
})];
fn vertex_buffers(kind: PipelineKind) -> &'static [Option<wgpu::VertexBufferLayout<'static>>] {
match kind {
PipelineKind::StencilFan { .. } | PipelineKind::Strip(_) => &MESH_LAYOUT,
PipelineKind::Text { .. } => &TEXT_LAYOUT,
_ => &[],
}
}
fn color_target(key: PipelineKey) -> wgpu::ColorTargetState {
let writes_color = key.kind.writes_color();
wgpu::ColorTargetState {
format: key.format,
blend: (writes_color && !key.kind.replaces_dst()).then(|| blend_state(key.blend)),
write_mask: if writes_color {
wgpu::ColorWrites::ALL
} else {
wgpu::ColorWrites::empty()
},
}
}
fn depth_stencil(kind: PipelineKind) -> wgpu::DepthStencilState {
let (depth_write_enabled, depth_compare, stencil) = match kind {
PipelineKind::Draw(_) | PipelineKind::Strip(_) => (
false,
wgpu::CompareFunction::GreaterEqual,
face_pair(ALWAYS_KEEP),
),
PipelineKind::OpaqueDraw(_) => (
true,
wgpu::CompareFunction::GreaterEqual,
face_pair(ALWAYS_KEEP),
),
PipelineKind::OpaqueCover(_) => (
true,
wgpu::CompareFunction::GreaterEqual,
face_pair(wgpu::StencilFaceState {
compare: wgpu::CompareFunction::NotEqual,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Zero,
pass_op: wgpu::StencilOperation::Zero,
}),
),
PipelineKind::Cover(_) => (
false,
wgpu::CompareFunction::GreaterEqual,
face_pair(wgpu::StencilFaceState {
compare: wgpu::CompareFunction::NotEqual,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Zero,
pass_op: wgpu::StencilOperation::Zero,
}),
),
PipelineKind::StencilFan { even_odd } => {
let winding = |op| wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: op,
};
let stencil = if even_odd {
face_pair(winding(wgpu::StencilOperation::Invert))
} else {
wgpu::StencilState {
front: winding(wgpu::StencilOperation::IncrementWrap),
back: winding(wgpu::StencilOperation::DecrementWrap),
read_mask: 0xFF,
write_mask: 0xFF,
}
};
(false, wgpu::CompareFunction::Always, stencil)
}
PipelineKind::Filter(_) => unreachable!("filter passes carry no depth attachment"),
PipelineKind::Text { .. } => (
false,
wgpu::CompareFunction::GreaterEqual,
face_pair(ALWAYS_KEEP),
),
PipelineKind::ClipCover { difference } => (
true,
wgpu::CompareFunction::Greater,
face_pair(wgpu::StencilFaceState {
compare: if difference {
wgpu::CompareFunction::NotEqual } else {
wgpu::CompareFunction::Equal },
fail_op: wgpu::StencilOperation::Zero,
depth_fail_op: wgpu::StencilOperation::Zero,
pass_op: wgpu::StencilOperation::Zero,
}),
),
};
wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(depth_write_enabled),
depth_compare: Some(depth_compare),
stencil,
bias: Default::default(),
}
}
const ALWAYS_KEEP: wgpu::StencilFaceState = wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Keep,
};
fn face_pair(face: wgpu::StencilFaceState) -> wgpu::StencilState {
wgpu::StencilState {
front: face,
back: face,
read_mask: 0xFF,
write_mask: 0xFF,
}
}
fn blend_state(mode: BlendMode) -> wgpu::BlendState {
use wgpu::BlendFactor as F;
let (src, dst) = match mode {
BlendMode::Clear => (F::Zero, F::Zero),
BlendMode::Src => (F::One, F::Zero),
BlendMode::Dst => (F::Zero, F::One),
BlendMode::SrcOver => (F::One, F::OneMinusSrcAlpha),
BlendMode::DstOver => (F::OneMinusDstAlpha, F::One),
BlendMode::SrcIn => (F::DstAlpha, F::Zero),
BlendMode::DstIn => (F::Zero, F::SrcAlpha),
BlendMode::SrcOut => (F::OneMinusDstAlpha, F::Zero),
BlendMode::DstOut => (F::Zero, F::OneMinusSrcAlpha),
BlendMode::SrcAtop => (F::DstAlpha, F::OneMinusSrcAlpha),
BlendMode::DstAtop => (F::OneMinusDstAlpha, F::SrcAlpha),
BlendMode::Xor => (F::OneMinusDstAlpha, F::OneMinusSrcAlpha),
BlendMode::Plus => (F::One, F::One),
BlendMode::Modulate => (F::Zero, F::Src),
BlendMode::Screen => (F::One, F::OneMinusSrc),
_ => (F::One, F::OneMinusSrcAlpha),
};
let component = wgpu::BlendComponent {
src_factor: src,
dst_factor: dst,
operation: wgpu::BlendOperation::Add,
};
wgpu::BlendState {
color: component,
alpha: component,
}
}