1use rustc_hash::FxHashMap;
2
3use valo_dl::BlendMode;
4
5pub const SAMPLE_COUNT: u32 = 4;
10pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24PlusStencil8;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
17pub enum Frag {
18 Solid,
19 Image,
20 ImageMatrix,
22 ImageBlend,
23 Linear,
24 Radial,
25 Sweep,
26 BlendSolid,
28 BlendTexture,
31 RRectBlur,
33 Blur,
35 MaskCombine,
38 DropShadow,
41 MaskComposite,
44 LinearRamp,
46 RadialRamp,
47 SweepRamp,
48 ColorMatrix,
51 ColorBlend,
52 Pattern,
55}
56
57impl Frag {
58 fn entry_point(self) -> &'static str {
59 match self {
60 Frag::Solid => "fs_solid",
61 Frag::Image => "fs_image",
62 Frag::ImageMatrix => "fs_image_matrix",
63 Frag::ImageBlend => "fs_image_blend",
64 Frag::Linear => "fs_linear",
65 Frag::Radial => "fs_radial",
66 Frag::Sweep => "fs_sweep",
67 Frag::BlendSolid => "fs_blend_solid",
68 Frag::BlendTexture => "fs_blend_texture",
69 Frag::RRectBlur => "fs_rrect_blur",
70 Frag::Blur => "fs_blur",
71 Frag::MaskCombine => "fs_mask_combine",
72 Frag::DropShadow => "fs_drop_shadow",
73 Frag::MaskComposite => "fs_mask_composite",
74 Frag::LinearRamp => "fs_linear_ramp",
75 Frag::RadialRamp => "fs_radial_ramp",
76 Frag::SweepRamp => "fs_sweep_ramp",
77 Frag::ColorMatrix => "fs_color_matrix",
78 Frag::ColorBlend => "fs_color_blend",
79 Frag::Pattern => "fs_pattern",
80 }
81 }
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
89pub enum PipelineKind {
90 Draw(Frag),
92 Cover(Frag),
94 OpaqueDraw(Frag),
97 OpaqueCover(Frag),
99 StencilFan { even_odd: bool },
101 ClipCover { difference: bool },
104 Filter(Frag),
107 Strip(Frag),
110 Text { mode: TextMode },
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
116pub enum TextMode {
117 Mask,
119 Sdf,
121 Color,
123}
124
125impl PipelineKind {
126 fn writes_color(self) -> bool {
127 matches!(
128 self,
129 PipelineKind::Draw(_)
130 | PipelineKind::Cover(_)
131 | PipelineKind::OpaqueDraw(_)
132 | PipelineKind::OpaqueCover(_)
133 | PipelineKind::Filter(_)
134 | PipelineKind::Strip(_)
135 | PipelineKind::Text { .. }
136 )
137 }
138
139 fn frag(self) -> Option<Frag> {
140 match self {
141 PipelineKind::Draw(f)
142 | PipelineKind::Cover(f)
143 | PipelineKind::OpaqueDraw(f)
144 | PipelineKind::OpaqueCover(f)
145 | PipelineKind::Filter(f)
146 | PipelineKind::Strip(f) => Some(f),
147 _ => None,
148 }
149 }
150
151 fn replaces_dst(self) -> bool {
155 matches!(
156 self,
157 PipelineKind::OpaqueDraw(_) | PipelineKind::OpaqueCover(_) | PipelineKind::Filter(_)
158 ) || matches!(
159 self.frag(),
160 Some(Frag::BlendSolid) | Some(Frag::BlendTexture)
161 )
162 }
163
164 fn fragment_entry(self) -> &'static str {
165 if let PipelineKind::Text { mode } = self {
166 return match mode {
167 TextMode::Mask => "fs_text",
168 TextMode::Sdf => "fs_text_sdf",
169 TextMode::Color => "fs_text_color",
170 };
171 }
172 self.frag().map_or("fs_solid", Frag::entry_point)
173 }
174
175 pub fn sample_count(self) -> u32 {
177 match self {
178 PipelineKind::Filter(_) => 1,
179 _ => SAMPLE_COUNT,
180 }
181 }
182
183 fn vertex_entry(self) -> &'static str {
184 match self {
185 PipelineKind::StencilFan { .. } | PipelineKind::Strip(_) => "vs_mesh",
186 PipelineKind::Text { .. } => "vs_text",
187 _ => "vs_quad",
188 }
189 }
190
191 fn normalized_blend(self, blend: BlendMode) -> BlendMode {
194 if self.writes_color() && !self.replaces_dst() {
195 blend
196 } else {
197 BlendMode::SrcOver
198 }
199 }
200}
201
202#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
207pub struct PipelineKey {
208 pub format: wgpu::TextureFormat,
209 pub blend: BlendMode,
210 pub kind: PipelineKind,
211}
212
213impl PipelineKey {
214 pub fn new(format: wgpu::TextureFormat, blend: BlendMode, kind: PipelineKind) -> Self {
216 Self {
217 format,
218 blend: kind.normalized_blend(blend),
219 kind,
220 }
221 }
222}
223
224pub struct PipelineCache {
228 shader: wgpu::ShaderModule,
229 plain_layout: wgpu::PipelineLayout,
230 textured_layout: wgpu::PipelineLayout,
231 blend_layout: wgpu::PipelineLayout,
232 texture_bind_layout: wgpu::BindGroupLayout,
233 blend_bind_layout: wgpu::BindGroupLayout,
234 map: FxHashMap<PipelineKey, wgpu::RenderPipeline>,
235}
236
237impl PipelineCache {
238 pub fn new(device: &wgpu::Device, uniforms_layout: &wgpu::BindGroupLayout) -> Self {
240 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
241 label: Some("valo.solid"),
242 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/solid.wgsl").into()),
243 });
244 let texture_bind_layout = texture_bind_group_layout(device);
245 let plain_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
246 label: Some("valo.plain"),
247 bind_group_layouts: &[Some(uniforms_layout)],
248 immediate_size: 0,
249 });
250 let textured_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
251 label: Some("valo.textured"),
252 bind_group_layouts: &[Some(uniforms_layout), Some(&texture_bind_layout)],
253 immediate_size: 0,
254 });
255 let blend_bind_layout = blend_bind_group_layout(device);
256 let blend_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
257 label: Some("valo.blend"),
258 bind_group_layouts: &[Some(uniforms_layout), Some(&blend_bind_layout)],
259 immediate_size: 0,
260 });
261 Self {
262 shader,
263 plain_layout,
264 textured_layout,
265 blend_layout,
266 texture_bind_layout,
267 blend_bind_layout,
268 map: FxHashMap::default(),
269 }
270 }
271
272 pub fn blend_bind_layout(&self) -> &wgpu::BindGroupLayout {
276 &self.blend_bind_layout
277 }
278
279 pub fn texture_bind_layout(&self) -> &wgpu::BindGroupLayout {
283 &self.texture_bind_layout
284 }
285
286 pub fn ensure(&mut self, device: &wgpu::Device, key: PipelineKey) {
288 if !self.map.contains_key(&key) {
289 let pipeline = self.create(device, key);
290 self.map.insert(key, pipeline);
291 }
292 }
293
294 pub fn get(&self, key: &PipelineKey) -> &wgpu::RenderPipeline {
298 &self.map[key]
299 }
300
301 fn create(&self, device: &wgpu::Device, key: PipelineKey) -> wgpu::RenderPipeline {
302 let layout = match key.kind.frag() {
303 _ if matches!(key.kind, PipelineKind::Text { .. }) => &self.textured_layout,
304 Some(Frag::BlendTexture) | Some(Frag::MaskCombine) | Some(Frag::DropShadow) => {
305 &self.blend_layout
306 }
307 Some(Frag::Image)
308 | Some(Frag::ImageMatrix)
309 | Some(Frag::ImageBlend)
310 | Some(Frag::BlendSolid)
311 | Some(Frag::Blur)
312 | Some(Frag::MaskComposite)
313 | Some(Frag::LinearRamp)
314 | Some(Frag::RadialRamp)
315 | Some(Frag::SweepRamp)
316 | Some(Frag::ColorMatrix)
317 | Some(Frag::ColorBlend)
318 | Some(Frag::Pattern) => &self.textured_layout,
319 _ => &self.plain_layout,
320 };
321 let depth_stencil = match key.kind {
322 PipelineKind::Filter(_) => None,
323 kind => Some(depth_stencil(kind)),
324 };
325 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
326 label: Some("valo.solid"),
327 layout: Some(layout),
328 vertex: wgpu::VertexState {
329 module: &self.shader,
330 entry_point: Some(key.kind.vertex_entry()),
331 compilation_options: Default::default(),
332 buffers: vertex_buffers(key.kind),
333 },
334 fragment: Some(wgpu::FragmentState {
335 module: &self.shader,
336 entry_point: Some(key.kind.fragment_entry()),
337 compilation_options: Default::default(),
338 targets: &[Some(color_target(key))],
339 }),
340 primitive: wgpu::PrimitiveState {
341 topology: match key.kind {
342 PipelineKind::Strip(_) => wgpu::PrimitiveTopology::TriangleStrip,
343 _ => wgpu::PrimitiveTopology::TriangleList,
344 },
345 ..Default::default()
346 },
347 depth_stencil,
348 multisample: wgpu::MultisampleState {
349 count: key.kind.sample_count(),
350 ..Default::default()
351 },
352 multiview_mask: None,
353 cache: None,
354 })
355 }
356}
357
358fn texture_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
359 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
360 label: Some("valo.texture"),
361 entries: &[
362 wgpu::BindGroupLayoutEntry {
363 binding: 0,
364 visibility: wgpu::ShaderStages::FRAGMENT,
365 ty: wgpu::BindingType::Texture {
366 sample_type: wgpu::TextureSampleType::Float { filterable: true },
367 view_dimension: wgpu::TextureViewDimension::D2,
368 multisampled: false,
369 },
370 count: None,
371 },
372 wgpu::BindGroupLayoutEntry {
373 binding: 1,
374 visibility: wgpu::ShaderStages::FRAGMENT,
375 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
376 count: None,
377 },
378 ],
379 })
380}
381
382fn blend_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
383 let texture_entry = |binding| wgpu::BindGroupLayoutEntry {
384 binding,
385 visibility: wgpu::ShaderStages::FRAGMENT,
386 ty: wgpu::BindingType::Texture {
387 sample_type: wgpu::TextureSampleType::Float { filterable: true },
388 view_dimension: wgpu::TextureViewDimension::D2,
389 multisampled: false,
390 },
391 count: None,
392 };
393 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
394 label: Some("valo.blend"),
395 entries: &[
396 texture_entry(0), wgpu::BindGroupLayoutEntry {
398 binding: 1,
399 visibility: wgpu::ShaderStages::FRAGMENT,
400 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
401 count: None,
402 },
403 texture_entry(2), ],
405 })
406}
407
408pub fn blur_style_id(style: valo_dl::BlurStyle) -> u32 {
411 match style {
412 valo_dl::BlurStyle::Normal => 0,
413 valo_dl::BlurStyle::Solid => 1,
414 valo_dl::BlurStyle::Inner => 2,
415 valo_dl::BlurStyle::Outer => 3,
416 }
417}
418
419pub fn blend_filter_id(mode: BlendMode) -> u32 {
424 match mode {
425 BlendMode::Clear => 0,
426 BlendMode::Src => 1,
427 BlendMode::Dst => 2,
428 BlendMode::SrcOver => 3,
429 BlendMode::DstOver => 4,
430 BlendMode::SrcIn => 5,
431 BlendMode::DstIn => 6,
432 BlendMode::SrcOut => 7,
433 BlendMode::DstOut => 8,
434 BlendMode::SrcAtop => 9,
435 BlendMode::DstAtop => 10,
436 BlendMode::Xor => 11,
437 BlendMode::Plus => 12,
438 BlendMode::Modulate => 13,
439 BlendMode::Screen => 14,
440 advanced => 15 + advanced_mode_id(advanced),
441 }
442}
443
444pub fn advanced_mode_id(mode: BlendMode) -> u32 {
448 match mode {
449 BlendMode::Multiply => 0,
450 BlendMode::Overlay => 1,
451 BlendMode::Darken => 2,
452 BlendMode::Lighten => 3,
453 BlendMode::ColorDodge => 4,
454 BlendMode::ColorBurn => 5,
455 BlendMode::HardLight => 6,
456 BlendMode::SoftLight => 7,
457 BlendMode::Difference => 8,
458 BlendMode::Exclusion => 9,
459 BlendMode::Hue => 10,
460 BlendMode::Saturation => 11,
461 BlendMode::Color => 12,
462 BlendMode::Luminosity => 13,
463 _ => unreachable!("pipeline-blendable mode routed to advanced path"),
464 }
465}
466
467const MESH_LAYOUT: [Option<wgpu::VertexBufferLayout<'static>>; 1] =
468 [Some(wgpu::VertexBufferLayout {
469 array_stride: 8,
470 step_mode: wgpu::VertexStepMode::Vertex,
471 attributes: &wgpu::vertex_attr_array![0 => Float32x2],
472 })];
473
474const TEXT_LAYOUT: [Option<wgpu::VertexBufferLayout<'static>>; 1] =
475 [Some(wgpu::VertexBufferLayout {
476 array_stride: 16,
477 step_mode: wgpu::VertexStepMode::Vertex,
478 attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
479 })];
480
481fn vertex_buffers(kind: PipelineKind) -> &'static [Option<wgpu::VertexBufferLayout<'static>>] {
482 match kind {
483 PipelineKind::StencilFan { .. } | PipelineKind::Strip(_) => &MESH_LAYOUT,
484 PipelineKind::Text { .. } => &TEXT_LAYOUT,
485 _ => &[],
486 }
487}
488
489fn color_target(key: PipelineKey) -> wgpu::ColorTargetState {
490 let writes_color = key.kind.writes_color();
491 wgpu::ColorTargetState {
492 format: key.format,
493 blend: (writes_color && !key.kind.replaces_dst()).then(|| blend_state(key.blend)),
494 write_mask: if writes_color {
495 wgpu::ColorWrites::ALL
496 } else {
497 wgpu::ColorWrites::empty()
498 },
499 }
500}
501
502fn depth_stencil(kind: PipelineKind) -> wgpu::DepthStencilState {
507 let (depth_write_enabled, depth_compare, stencil) = match kind {
508 PipelineKind::Draw(_) | PipelineKind::Strip(_) => (
509 false,
510 wgpu::CompareFunction::GreaterEqual,
511 face_pair(ALWAYS_KEEP),
512 ),
513 PipelineKind::OpaqueDraw(_) => (
516 true,
517 wgpu::CompareFunction::GreaterEqual,
518 face_pair(ALWAYS_KEEP),
519 ),
520 PipelineKind::OpaqueCover(_) => (
521 true,
522 wgpu::CompareFunction::GreaterEqual,
523 face_pair(wgpu::StencilFaceState {
524 compare: wgpu::CompareFunction::NotEqual,
525 fail_op: wgpu::StencilOperation::Keep,
526 depth_fail_op: wgpu::StencilOperation::Zero,
527 pass_op: wgpu::StencilOperation::Zero,
528 }),
529 ),
530 PipelineKind::Cover(_) => (
534 false,
535 wgpu::CompareFunction::GreaterEqual,
536 face_pair(wgpu::StencilFaceState {
537 compare: wgpu::CompareFunction::NotEqual,
538 fail_op: wgpu::StencilOperation::Keep,
539 depth_fail_op: wgpu::StencilOperation::Zero,
540 pass_op: wgpu::StencilOperation::Zero,
541 }),
542 ),
543 PipelineKind::StencilFan { even_odd } => {
546 let winding = |op| wgpu::StencilFaceState {
547 compare: wgpu::CompareFunction::Always,
548 fail_op: wgpu::StencilOperation::Keep,
549 depth_fail_op: wgpu::StencilOperation::Keep,
550 pass_op: op,
551 };
552 let stencil = if even_odd {
553 face_pair(winding(wgpu::StencilOperation::Invert))
554 } else {
555 wgpu::StencilState {
556 front: winding(wgpu::StencilOperation::IncrementWrap),
557 back: winding(wgpu::StencilOperation::DecrementWrap),
558 read_mask: 0xFF,
559 write_mask: 0xFF,
560 }
561 };
562 (false, wgpu::CompareFunction::Always, stencil)
563 }
564 PipelineKind::Filter(_) => unreachable!("filter passes carry no depth attachment"),
569 PipelineKind::Text { .. } => (
571 false,
572 wgpu::CompareFunction::GreaterEqual,
573 face_pair(ALWAYS_KEEP),
574 ),
575 PipelineKind::ClipCover { difference } => (
576 true,
577 wgpu::CompareFunction::Greater,
578 face_pair(wgpu::StencilFaceState {
579 compare: if difference {
580 wgpu::CompareFunction::NotEqual } else {
582 wgpu::CompareFunction::Equal },
584 fail_op: wgpu::StencilOperation::Zero,
585 depth_fail_op: wgpu::StencilOperation::Zero,
586 pass_op: wgpu::StencilOperation::Zero,
587 }),
588 ),
589 };
590 wgpu::DepthStencilState {
591 format: DEPTH_FORMAT,
592 depth_write_enabled: Some(depth_write_enabled),
593 depth_compare: Some(depth_compare),
594 stencil,
595 bias: Default::default(),
596 }
597}
598
599const ALWAYS_KEEP: wgpu::StencilFaceState = wgpu::StencilFaceState {
600 compare: wgpu::CompareFunction::Always,
601 fail_op: wgpu::StencilOperation::Keep,
602 depth_fail_op: wgpu::StencilOperation::Keep,
603 pass_op: wgpu::StencilOperation::Keep,
604};
605
606fn face_pair(face: wgpu::StencilFaceState) -> wgpu::StencilState {
607 wgpu::StencilState {
608 front: face,
609 back: face,
610 read_mask: 0xFF,
611 write_mask: 0xFF,
612 }
613}
614
615fn blend_state(mode: BlendMode) -> wgpu::BlendState {
619 use wgpu::BlendFactor as F;
620 let (src, dst) = match mode {
621 BlendMode::Clear => (F::Zero, F::Zero),
622 BlendMode::Src => (F::One, F::Zero),
623 BlendMode::Dst => (F::Zero, F::One),
624 BlendMode::SrcOver => (F::One, F::OneMinusSrcAlpha),
625 BlendMode::DstOver => (F::OneMinusDstAlpha, F::One),
626 BlendMode::SrcIn => (F::DstAlpha, F::Zero),
627 BlendMode::DstIn => (F::Zero, F::SrcAlpha),
628 BlendMode::SrcOut => (F::OneMinusDstAlpha, F::Zero),
629 BlendMode::DstOut => (F::Zero, F::OneMinusSrcAlpha),
630 BlendMode::SrcAtop => (F::DstAlpha, F::OneMinusSrcAlpha),
631 BlendMode::DstAtop => (F::OneMinusDstAlpha, F::SrcAlpha),
632 BlendMode::Xor => (F::OneMinusDstAlpha, F::OneMinusSrcAlpha),
633 BlendMode::Plus => (F::One, F::One),
634 BlendMode::Modulate => (F::Zero, F::Src),
635 BlendMode::Screen => (F::One, F::OneMinusSrc),
636 _ => (F::One, F::OneMinusSrcAlpha),
639 };
640 let component = wgpu::BlendComponent {
641 src_factor: src,
642 dst_factor: dst,
643 operation: wgpu::BlendOperation::Add,
644 };
645 wgpu::BlendState {
646 color: component,
647 alpha: component,
648 }
649}