1use crate::{
2 BlendMode, Color, GraphError, GraphLayer, RenderError, RenderGraph, RenderSize,
3 RenderedTexture, Renderer, SourceView,
4 pipeline::CachedCompositePipeline,
5 uniform::{BATCH_UNIFORM_FLOATS, UniformArenaLease, f32_bytes, float_uniform_bytes},
6};
7use alpha2_shaders::COMPOSITE_WGSL;
8
9pub(crate) const COMPOSITE_BATCH_LAYERS: usize = 8;
14
15#[derive(Copy, Clone, Debug, PartialEq)]
17pub struct LayerTransform {
18 pub position: [f32; 2],
20 pub anchor: [f32; 2],
22 pub scale: [f32; 2],
24 pub rotation_degrees: f32,
26 pub opacity: f32,
28}
29
30#[derive(Copy, Clone, Debug)]
32#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
33pub struct CompositeLayer<'a> {
34 pub texture: &'a wgpu::TextureView,
36 pub visual: LayerTransform,
38 pub blend_mode: BlendMode,
40}
41
42#[derive(Clone, Copy)]
44pub(crate) struct CompositeTarget<'a> {
45 pub(crate) background: Option<Color>,
47 pub(crate) blend_space: crate::BlendSpace,
49 pub(crate) output: &'a wgpu::Texture,
51 pub(crate) output_view: &'a wgpu::TextureView,
53}
54
55impl Renderer {
56 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
66 pub fn render_layers(
67 &self,
68 size: RenderSize,
69 layers: &[CompositeLayer<'_>],
70 ) -> Result<RenderedTexture, RenderError> {
71 let mut graph = RenderGraph::new(size);
72 let mut graph_layers = Vec::with_capacity(layers.len());
73 for layer in layers {
74 graph_layers.push(GraphLayer {
75 source: graph.add_source(SourceView::from_wgpu(layer.texture)),
76 visual: layer.visual,
77 blend_mode: layer.blend_mode,
78 });
79 }
80 let output = graph
81 .add_composite(graph_layers)
82 .map_err(GraphError::into_render_error)?;
83 graph
84 .set_output(output)
85 .map_err(GraphError::into_render_error)?;
86 self.execute_graph(&graph)
87 .map_err(GraphError::into_render_error)
88 }
89
90 #[allow(clippy::too_many_lines)]
112 pub(crate) fn encode_layers_pass(
113 &self,
114 encoder: &mut wgpu::CommandEncoder,
115 size: RenderSize,
116 layers: &[CompositeLayer<'_>],
117 target: CompositeTarget<'_>,
118 uniforms: &mut UniformArenaLease,
119 ) -> Result<u64, RenderError> {
120 let CompositeTarget {
121 background,
122 blend_space,
123 output,
124 output_view,
125 } = target;
126 for layer in layers {
127 validate_visual(layer.visual)?;
128 }
129 let format = self.intermediate_format();
130 let drawn = self.drawn_layers(layers.iter().map(|layer| layer.visual), size);
131 let drawn = drawn
132 .iter()
133 .map(|(index, bounds, tier)| (&layers[*index], *bounds, *tier))
134 .collect::<Vec<_>>();
135 let background = stored_background(background, format);
136 if drawn.is_empty() {
137 clear_target(encoder, output_view, background);
140 return Ok(1);
141 }
142
143 let cached = self.composite_pipeline();
144 let preparation = BatchPreparationContext {
145 device: self.gpu.device(),
146 layout: &cached.bind_group_layout,
147 sampler: &cached.sampler,
148 no_backdrop: &self.empty_input,
149 blend_space,
150 max_filter_samples: self.quality_controls().max_filter_samples,
151 output_size: size,
152 background,
153 };
154 let batches = composite_batches(&drawn, self.composite_batch_layers())
155 .into_iter()
156 .map(|(range, bounds)| (&drawn[range], bounds))
157 .collect::<Vec<_>>();
158 let bounded = |bounds: &PixelRect| under_half(*bounds, size);
159 let ((first, first_bounds), rest) = batches.split_first().expect("a layer is drawn");
160 let first_program = self.batch_program(first);
161 let first = prepare_batch(&preparation, None, first, uniforms);
162 let first_pass = PassTarget {
165 load: wgpu::LoadOp::Clear(stored_color(background)),
166 scissor: bounded(first_bounds).then_some(*first_bounds),
167 };
168 if rest.is_empty() {
169 self.draw_batch(
170 encoder,
171 cached,
172 first_program,
173 &first,
174 output_view,
175 first_pass,
176 );
177 return Ok(1);
178 }
179
180 let scratch = self.textures.acquire(
181 self.gpu.device(),
182 size,
183 format,
184 "alpha2 layer composition scratch",
185 );
186 let textures = [output, scratch.texture()];
187 let views = [output_view, scratch.view()];
188 let mut current = rest.iter().filter(|(_, bounds)| !bounded(bounds)).count() % 2;
190 self.draw_batch(
191 encoder,
192 cached,
193 first_program,
194 &first,
195 views[current],
196 first_pass,
197 );
198 for (batch, bounds) in rest {
199 let other = 1 - current;
200 let program = self.batch_program(batch);
201 if bounded(bounds) {
202 copy_region(encoder, textures[current], textures[other], *bounds);
205 let prepared = prepare_batch(&preparation, Some(views[other]), batch, uniforms);
206 self.draw_batch(
207 encoder,
208 cached,
209 program,
210 &prepared,
211 views[current],
212 PassTarget {
213 load: wgpu::LoadOp::Load,
214 scissor: Some(*bounds),
215 },
216 );
217 } else {
218 let prepared = prepare_batch(&preparation, Some(views[current]), batch, uniforms);
219 self.draw_batch(
220 encoder,
221 cached,
222 program,
223 &prepared,
224 views[other],
225 PassTarget::WHOLE,
226 );
227 current = other;
228 }
229 }
230 debug_assert_eq!(current, 0);
231 Ok(u64::try_from(batches.len()).unwrap_or(u64::MAX))
232 }
233
234 fn batch_program(
237 &self,
238 batch: &[(&CompositeLayer<'_>, PixelRect, LayerTier)],
239 ) -> (CompositeProgram, LayerTier, usize) {
240 let tier = batch[0].2;
241 debug_assert!(batch.iter().all(|(_, _, other)| *other == tier));
242 let program = self.composite_program(tier);
243 debug_assert!(batch.len() == 1 || program == CompositeProgram::Loop);
244 (program, tier, batch.len())
245 }
246
247 fn draw_batch(
249 &self,
250 encoder: &mut wgpu::CommandEncoder,
251 cached: &CachedCompositePipeline,
252 (program, tier, layers): (CompositeProgram, LayerTier, usize),
253 bind_group: &wgpu::BindGroup,
254 target: &wgpu::TextureView,
255 pass: PassTarget,
256 ) {
257 let pipeline = self.composite_program_pipeline(cached, program);
258 #[cfg(test)]
259 {
260 let mut log = self
261 .composite_pipelines
262 .lock()
263 .unwrap_or_else(std::sync::PoisonError::into_inner);
264 log.passes += 1;
265 log.drawn
266 .insert((program, pipeline.clone(), tier, layers > 1));
267 }
268 #[cfg(not(test))]
269 let _ = (tier, layers);
270 encode_batch(encoder, pipeline, bind_group, target, pass);
271 }
272
273 pub(crate) fn batches_small_layers(&self) -> bool {
278 #[cfg(test)]
279 {
280 if self
281 .composite_batch_limit
282 .load(std::sync::atomic::Ordering::Relaxed)
283 > 0
284 {
285 return true;
286 }
287 }
288 !self.gpu.capabilities().software_rasterizer
289 }
290
291 pub(crate) fn composite_program(&self, tier: LayerTier) -> CompositeProgram {
304 if tier == LayerTier::Small && self.batches_small_layers() {
305 CompositeProgram::Loop
306 } else {
307 CompositeProgram::OneLayer
308 }
309 }
310
311 pub(crate) fn composite_batch_layers(&self) -> usize {
322 #[cfg(test)]
323 {
324 let limit = self
325 .composite_batch_limit
326 .load(std::sync::atomic::Ordering::Relaxed);
327 if limit > 0 {
328 return limit.min(COMPOSITE_BATCH_LAYERS);
329 }
330 }
331 if self.batches_small_layers() {
332 COMPOSITE_BATCH_LAYERS
333 } else {
334 1
335 }
336 }
337
338 pub(crate) fn composite_pipeline(&self) -> &CachedCompositePipeline {
341 self.pipelines.composite.get_or_init(|| {
342 let device = self.gpu.device();
343 let bind_group_layout = batch_layout(device);
344 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
345 label: Some("alpha2 layer composite shader"),
346 source: wgpu::ShaderSource::Wgsl(COMPOSITE_WGSL.into()),
347 });
348 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
349 label: Some("alpha2 layer sampler"),
350 address_mode_u: wgpu::AddressMode::ClampToEdge,
351 address_mode_v: wgpu::AddressMode::ClampToEdge,
352 mag_filter: wgpu::FilterMode::Linear,
353 min_filter: wgpu::FilterMode::Linear,
354 ..Default::default()
355 });
356 CachedCompositePipeline {
357 bind_group_layout,
358 sampler,
359 shader,
360 programs: Default::default(),
361 }
362 })
363 }
364
365 pub(crate) fn composite_program_pipeline<'a>(
367 &self,
368 cached: &'a CachedCompositePipeline,
369 program: CompositeProgram,
370 ) -> &'a wgpu::RenderPipeline {
371 let (slot, entry, label) = match program {
372 CompositeProgram::Loop => (0, "fs", "alpha2 small layer composite pipeline"),
373 CompositeProgram::OneLayer => (1, "fs_layer", "alpha2 large layer composite pipeline"),
374 };
375 cached.programs[slot].get_or_init(|| {
376 #[cfg(test)]
377 {
378 self.composite_pipelines
379 .lock()
380 .unwrap_or_else(std::sync::PoisonError::into_inner)
381 .created
382 .push(program);
383 }
384 composite_pipeline(
385 self.gpu.device(),
386 &cached.bind_group_layout,
387 &cached.shader,
388 self.intermediate_format(),
389 entry,
390 label,
391 )
392 })
393 }
394
395 #[cfg_attr(not(test), allow(clippy::unused_self))]
398 pub(crate) fn drawn_layers(
399 &self,
400 visuals: impl Iterator<Item = LayerTransform>,
401 size: RenderSize,
402 ) -> Vec<(usize, PixelRect, LayerTier)> {
403 #[cfg(test)]
404 let full_frame = self
405 .full_frame_layers
406 .load(std::sync::atomic::Ordering::Relaxed);
407 #[cfg(not(test))]
408 let full_frame = false;
409 visuals
410 .enumerate()
411 .filter_map(|(index, visual)| {
412 if is_degenerate(visual) || is_invisible(visual) {
413 return None;
414 }
415 let bounds = layer_bounds(visual, size);
416 let tier = LayerTier::of(bounds.unwrap_or(PixelRect::EMPTY), size);
419 if full_frame {
420 Some((index, PixelRect::whole(size), tier))
421 } else {
422 bounds.map(|bounds| (index, bounds, tier))
423 }
424 })
425 .collect()
426 }
427
428 pub(crate) fn composite_batch_count(
431 &self,
432 visuals: impl Iterator<Item = LayerTransform>,
433 size: RenderSize,
434 ) -> usize {
435 let drawn = self.drawn_layers(visuals, size);
436 composite_batches(&drawn, self.composite_batch_layers()).len()
437 }
438
439 pub(crate) fn warm_composite(&self, layers: usize) {
441 if layers == 0 {
442 return;
443 }
444 let cached = self.composite_pipeline();
445 self.composite_program_pipeline(cached, CompositeProgram::OneLayer);
446 if self.batches_small_layers() {
447 self.composite_program_pipeline(cached, CompositeProgram::Loop);
448 }
449 }
450}
451
452#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
456pub(crate) enum LayerTier {
457 Small,
459 Large,
461}
462
463impl LayerTier {
464 pub(crate) fn of(bounds: PixelRect, size: RenderSize) -> Self {
466 if under_half(bounds, size) {
467 Self::Small
468 } else {
469 Self::Large
470 }
471 }
472}
473
474fn under_half(bounds: PixelRect, size: RenderSize) -> bool {
477 bounds.area() * 2 < PixelRect::whole(size).area()
478}
479
480#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
483pub(crate) enum CompositeProgram {
484 Loop,
486 OneLayer,
488}
489
490#[cfg(test)]
493#[derive(Debug, Default)]
494pub(crate) struct CompositePipelineLog {
495 pub(crate) created: Vec<CompositeProgram>,
497 pub(crate) passes: usize,
499 pub(crate) drawn:
502 std::collections::HashSet<(CompositeProgram, wgpu::RenderPipeline, LayerTier, bool)>,
503}
504
505pub(crate) fn composite_batches<T>(
514 drawn: &[(T, PixelRect, LayerTier)],
515 limit: usize,
516) -> Vec<(std::ops::Range<usize>, PixelRect)> {
517 let mut batches: Vec<(std::ops::Range<usize>, PixelRect)> = Vec::new();
518 for (index, (_, bounds, tier)) in drawn.iter().enumerate() {
519 match batches.last_mut() {
520 Some((range, union))
521 if range.len() < limit
522 && *tier == LayerTier::Small
523 && drawn[range.start].2 == LayerTier::Small =>
524 {
525 range.end = index + 1;
526 *union = union.union(*bounds);
527 }
528 _ => batches.push((index..index + 1, *bounds)),
529 }
530 }
531 batches
532}
533
534#[derive(Copy, Clone, Debug)]
538enum StoredValue {
539 AsIs,
541 Half,
543 Unorm8,
545}
546
547impl StoredValue {
548 fn of(format: wgpu::TextureFormat) -> Self {
549 match format {
550 wgpu::TextureFormat::Rgba16Float => Self::Half,
551 wgpu::TextureFormat::Rgba8Unorm => Self::Unorm8,
552 _ => Self::AsIs,
553 }
554 }
555
556 const fn shader_value(self) -> f64 {
557 match self {
558 Self::AsIs => 0.0,
559 Self::Half => 1.0,
560 Self::Unorm8 => 2.0,
561 }
562 }
563}
564
565struct BatchPreparationContext<'a> {
566 device: &'a wgpu::Device,
567 layout: &'a wgpu::BindGroupLayout,
568 sampler: &'a wgpu::Sampler,
569 no_backdrop: &'a wgpu::TextureView,
572 blend_space: crate::BlendSpace,
573 max_filter_samples: u32,
574 output_size: RenderSize,
575 background: [f32; 4],
577}
578
579fn prepare_batch(
583 context: &BatchPreparationContext<'_>,
584 backdrop: Option<&wgpu::TextureView>,
585 batch: &[(&CompositeLayer<'_>, PixelRect, LayerTier)],
586 uniforms: &mut UniformArenaLease,
587) -> wgpu::BindGroup {
588 debug_assert!(!batch.is_empty() && batch.len() <= COMPOSITE_BATCH_LAYERS);
589 let mut values = [0.0_f32; BATCH_UNIFORM_FLOATS];
590 #[allow(clippy::cast_precision_loss)]
591 let controls = [
592 context.blend_space.shader_value(),
593 context.max_filter_samples as f32,
594 batch.len() as f32,
595 if backdrop.is_some() { 0.0 } else { 1.0 },
596 context.output_size.width as f32,
597 context.output_size.height as f32,
598 0.0,
599 0.0,
600 context.background[0],
601 context.background[1],
602 context.background[2],
603 context.background[3],
604 ];
605 values[..controls.len()].copy_from_slice(&controls);
606 for ((layer, bounds, _), slot) in batch
607 .iter()
608 .zip(values[controls.len()..].as_chunks_mut::<16>().0)
609 {
610 let visual = layer.visual;
611 #[allow(clippy::cast_precision_loss)]
612 slot.copy_from_slice(&[
613 visual.position[0],
614 visual.position[1],
615 visual.anchor[0],
616 visual.anchor[1],
617 visual.scale[0],
618 visual.scale[1],
619 visual.rotation_degrees.to_radians(),
620 visual.opacity,
621 layer.blend_mode.shader_value(),
622 0.0,
623 0.0,
624 0.0,
625 bounds.x as f32,
626 bounds.y as f32,
627 (bounds.x + bounds.width) as f32,
628 (bounds.y + bounds.height) as f32,
629 ]);
630 }
631 let uniform = uniforms.write(&f32_bytes(values));
632 let texture = |slot: usize| {
633 let (layer, _, _) = batch.get(slot).unwrap_or(&batch[0]);
634 layer.texture
635 };
636 let mut entries = (0..COMPOSITE_BATCH_LAYERS)
637 .map(|slot| wgpu::BindGroupEntry {
638 binding: u32::try_from(slot).expect("few layer slots"),
639 resource: wgpu::BindingResource::TextureView(texture(slot)),
640 })
641 .collect::<Vec<_>>();
642 entries.push(wgpu::BindGroupEntry {
643 binding: SAMPLER_BINDING,
644 resource: wgpu::BindingResource::Sampler(context.sampler),
645 });
646 entries.push(wgpu::BindGroupEntry {
647 binding: BACKDROP_BINDING,
648 resource: wgpu::BindingResource::TextureView(backdrop.unwrap_or(context.no_backdrop)),
649 });
650 entries.push(wgpu::BindGroupEntry {
651 binding: UNIFORM_BINDING,
652 resource: uniforms.binding(uniform),
653 });
654 context
655 .device
656 .create_bind_group(&wgpu::BindGroupDescriptor {
657 label: Some("alpha2 layer batch bind group"),
658 layout: context.layout,
659 entries: &entries,
660 })
661}
662
663fn clear_target(encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, stored: [f32; 4]) {
667 let color = stored_color(stored);
668 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
669 label: Some("alpha2 layer clear pass"),
670 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
671 view: target,
672 depth_slice: None,
673 resolve_target: None,
674 ops: wgpu::Operations {
675 load: wgpu::LoadOp::Clear(color),
676 store: wgpu::StoreOp::Store,
677 },
678 })],
679 depth_stencil_attachment: None,
680 timestamp_writes: None,
681 occlusion_query_set: None,
682 multiview_mask: None,
683 });
684}
685
686#[derive(Copy, Clone)]
688struct PassTarget {
689 load: wgpu::LoadOp<wgpu::Color>,
691 scissor: Option<PixelRect>,
693}
694
695impl PassTarget {
696 const WHOLE: Self = Self {
698 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
699 scissor: None,
700 };
701}
702
703fn stored_color(stored: [f32; 4]) -> wgpu::Color {
706 let [r, g, b, a] = stored.map(f64::from);
707 wgpu::Color { r, g, b, a }
708}
709
710fn encode_batch(
713 encoder: &mut wgpu::CommandEncoder,
714 pipeline: &wgpu::RenderPipeline,
715 bind_group: &wgpu::BindGroup,
716 target: &wgpu::TextureView,
717 PassTarget { load, scissor }: PassTarget,
718) {
719 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
720 label: Some("alpha2 layer composition pass"),
721 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
722 view: target,
723 depth_slice: None,
724 resolve_target: None,
725 ops: wgpu::Operations {
726 load,
727 store: wgpu::StoreOp::Store,
728 },
729 })],
730 depth_stencil_attachment: None,
731 timestamp_writes: None,
732 occlusion_query_set: None,
733 multiview_mask: None,
734 });
735 pass.set_pipeline(pipeline);
736 pass.set_bind_group(0, bind_group, &[]);
737 if let Some(scissor) = scissor {
738 pass.set_scissor_rect(scissor.x, scissor.y, scissor.width, scissor.height);
739 }
740 pass.draw(0..3, 0..1);
741}
742
743fn copy_region(
745 encoder: &mut wgpu::CommandEncoder,
746 source: &wgpu::Texture,
747 destination: &wgpu::Texture,
748 region: PixelRect,
749) {
750 let at = |texture| wgpu::TexelCopyTextureInfo {
751 texture,
752 mip_level: 0,
753 origin: wgpu::Origin3d {
754 x: region.x,
755 y: region.y,
756 z: 0,
757 },
758 aspect: wgpu::TextureAspect::All,
759 };
760 encoder.copy_texture_to_texture(
761 at(source),
762 at(destination),
763 wgpu::Extent3d {
764 width: region.width,
765 height: region.height,
766 depth_or_array_layers: 1,
767 },
768 );
769}
770
771#[derive(Copy, Clone, Debug, Eq, PartialEq)]
773pub(crate) struct PixelRect {
774 pub(crate) x: u32,
775 pub(crate) y: u32,
776 pub(crate) width: u32,
777 pub(crate) height: u32,
778}
779
780impl PixelRect {
781 const EMPTY: Self = Self {
783 x: 0,
784 y: 0,
785 width: 0,
786 height: 0,
787 };
788
789 const fn whole(size: RenderSize) -> Self {
790 Self {
791 x: 0,
792 y: 0,
793 width: size.width,
794 height: size.height,
795 }
796 }
797
798 pub(crate) fn area(self) -> u64 {
799 u64::from(self.width) * u64::from(self.height)
800 }
801
802 fn union(self, other: Self) -> Self {
804 let x = self.x.min(other.x);
805 let y = self.y.min(other.y);
806 let right = (self.x + self.width).max(other.x + other.width);
807 let bottom = (self.y + self.height).max(other.y + other.height);
808 Self {
809 x,
810 y,
811 width: right - x,
812 height: bottom - y,
813 }
814 }
815}
816
817const COVERAGE_MARGIN: f64 = 4.0;
821
822pub(crate) fn layer_bounds(visual: LayerTransform, size: RenderSize) -> Option<PixelRect> {
831 let (width, height) = (f64::from(size.width), f64::from(size.height));
832 let (turn_sin, turn_cos) = f64::from(visual.rotation_degrees).to_radians().sin_cos();
833 let corner = |u: f64, v: f64| {
834 let x = (u - f64::from(visual.anchor[0])) * f64::from(visual.scale[0]) * width;
835 let y = (v - f64::from(visual.anchor[1])) * f64::from(visual.scale[1]) * height;
836 (
837 f64::from(visual.position[0]) * width + turn_cos * x - turn_sin * y,
838 f64::from(visual.position[1]) * height + turn_sin * x + turn_cos * y,
839 )
840 };
841 let corners = [
842 corner(0.0, 0.0),
843 corner(1.0, 0.0),
844 corner(0.0, 1.0),
845 corner(1.0, 1.0),
846 ];
847 let fold = |pick: fn(&(f64, f64)) -> f64, extreme: fn(f64, f64) -> f64, start: f64| {
848 corners.iter().map(pick).fold(start, extreme)
849 };
850 let left = (fold(|c| c.0, f64::min, f64::INFINITY) - COVERAGE_MARGIN)
851 .floor()
852 .max(0.0);
853 let top = (fold(|c| c.1, f64::min, f64::INFINITY) - COVERAGE_MARGIN)
854 .floor()
855 .max(0.0);
856 let right = (fold(|c| c.0, f64::max, f64::NEG_INFINITY) + COVERAGE_MARGIN)
857 .ceil()
858 .min(width);
859 let bottom = (fold(|c| c.1, f64::max, f64::NEG_INFINITY) + COVERAGE_MARGIN)
860 .ceil()
861 .min(height);
862 if !(right > left && bottom > top) {
863 return None;
864 }
865 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
866 Some(PixelRect {
867 x: left as u32,
868 y: top as u32,
869 width: (right - left) as u32,
870 height: (bottom - top) as u32,
871 })
872}
873
874fn stored_background(background: Option<Color>, format: wgpu::TextureFormat) -> [f32; 4] {
886 let Some(color) = background else {
887 return [0.0; 4];
888 };
889 #[allow(clippy::cast_possible_truncation)]
891 let channel = |value: f64| {
892 let value = value as f32;
893 match format {
894 wgpu::TextureFormat::Rgba8Unorm => (value.clamp(0.0, 1.0) * 255.0).round() / 255.0,
895 wgpu::TextureFormat::Rgba16Float => half::f16::from_f32(value).to_f32(),
896 _ => value,
897 }
898 };
899 [
900 channel(color.red),
901 channel(color.green),
902 channel(color.blue),
903 channel(color.alpha),
904 ]
905}
906
907const SAMPLER_BINDING: u32 = 8;
908const BACKDROP_BINDING: u32 = 9;
909const UNIFORM_BINDING: u32 = 10;
910
911fn batch_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
914 let mut entries = (0..COMPOSITE_BATCH_LAYERS)
915 .map(|slot| texture_entry(u32::try_from(slot).expect("few layer slots"), true))
916 .collect::<Vec<_>>();
917 entries.push(wgpu::BindGroupLayoutEntry {
918 binding: SAMPLER_BINDING,
919 visibility: wgpu::ShaderStages::FRAGMENT,
920 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
921 count: None,
922 });
923 entries.push(texture_entry(BACKDROP_BINDING, false));
924 entries.push(wgpu::BindGroupLayoutEntry {
925 binding: UNIFORM_BINDING,
926 visibility: wgpu::ShaderStages::FRAGMENT,
927 ty: batch_uniform_binding(),
928 count: None,
929 });
930 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
931 label: Some("alpha2 layer batch bind group layout"),
932 entries: &entries,
933 })
934}
935
936fn batch_uniform_binding() -> wgpu::BindingType {
940 wgpu::BindingType::Buffer {
941 ty: wgpu::BufferBindingType::Uniform,
942 has_dynamic_offset: false,
943 min_binding_size: std::num::NonZeroU64::new(float_uniform_bytes(BATCH_UNIFORM_FLOATS)),
944 }
945}
946
947fn texture_entry(binding: u32, filterable: bool) -> wgpu::BindGroupLayoutEntry {
948 wgpu::BindGroupLayoutEntry {
949 binding,
950 visibility: wgpu::ShaderStages::FRAGMENT,
951 ty: wgpu::BindingType::Texture {
952 sample_type: wgpu::TextureSampleType::Float { filterable },
953 view_dimension: wgpu::TextureViewDimension::D2,
954 multisampled: false,
955 },
956 count: None,
957 }
958}
959
960fn composite_pipeline(
961 device: &wgpu::Device,
962 layout: &wgpu::BindGroupLayout,
963 shader: &wgpu::ShaderModule,
964 format: wgpu::TextureFormat,
965 fragment_entry: &str,
966 label: &str,
967) -> wgpu::RenderPipeline {
968 let label = Some(label);
969 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
970 label,
971 bind_group_layouts: &[Some(layout)],
972 immediate_size: 0,
973 });
974 let constants = [("STORED_FORMAT", StoredValue::of(format).shader_value())];
975 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
976 label,
977 layout: Some(&pipeline_layout),
978 vertex: wgpu::VertexState {
979 module: shader,
980 entry_point: Some("vs"),
981 buffers: &[],
982 compilation_options: wgpu::PipelineCompilationOptions::default(),
983 },
984 fragment: Some(wgpu::FragmentState {
985 module: shader,
986 entry_point: Some(fragment_entry),
987 targets: &[Some(format.into())],
988 compilation_options: wgpu::PipelineCompilationOptions {
989 constants: &constants,
990 ..wgpu::PipelineCompilationOptions::default()
991 },
992 }),
993 primitive: wgpu::PrimitiveState::default(),
994 depth_stencil: None,
995 multisample: wgpu::MultisampleState::default(),
996 multiview_mask: None,
997 cache: None,
998 })
999}
1000
1001fn validate_visual(visual: LayerTransform) -> Result<(), RenderError> {
1002 let values = [
1003 visual.position[0],
1004 visual.position[1],
1005 visual.anchor[0],
1006 visual.anchor[1],
1007 visual.scale[0],
1008 visual.scale[1],
1009 visual.rotation_degrees,
1010 visual.opacity,
1011 ];
1012 if values.into_iter().any(|value| !value.is_finite()) || !(0.0..=1.0).contains(&visual.opacity)
1013 {
1014 return Err(RenderError::InvalidVisual);
1015 }
1016 Ok(())
1017}
1018
1019fn is_degenerate(visual: LayerTransform) -> bool {
1025 visual.scale[0].abs() <= f32::EPSILON || visual.scale[1].abs() <= f32::EPSILON
1026}
1027
1028fn is_invisible(visual: LayerTransform) -> bool {
1036 visual.opacity <= 0.0
1037}