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;
12
13#[derive(Copy, Clone, Debug, PartialEq)]
15pub struct LayerTransform {
16 pub position: [f32; 2],
18 pub anchor: [f32; 2],
20 pub scale: [f32; 2],
22 pub rotation_degrees: f32,
24 pub opacity: f32,
26}
27
28#[derive(Copy, Clone, Debug)]
30#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
31pub struct CompositeLayer<'a> {
32 pub texture: &'a wgpu::TextureView,
34 pub visual: LayerTransform,
36 pub blend_mode: BlendMode,
38}
39
40#[derive(Clone, Copy)]
41pub(crate) struct CompositeTarget<'a> {
42 pub(crate) background: Option<Color>,
44 pub(crate) blend_space: crate::BlendSpace,
45 pub(crate) output: &'a wgpu::Texture,
46 pub(crate) output_view: &'a wgpu::TextureView,
47}
48
49impl Renderer {
50 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
59 pub fn render_layers(
60 &self,
61 size: RenderSize,
62 layers: &[CompositeLayer<'_>],
63 ) -> Result<RenderedTexture, RenderError> {
64 let mut graph = RenderGraph::new(size);
65 let mut graph_layers = Vec::with_capacity(layers.len());
66 for layer in layers {
67 graph_layers.push(GraphLayer {
68 source: graph.add_source(SourceView::from_wgpu(layer.texture)),
69 visual: layer.visual,
70 blend_mode: layer.blend_mode,
71 });
72 }
73 let output = graph
74 .add_composite(graph_layers)
75 .map_err(GraphError::into_render_error)?;
76 graph
77 .set_output(output)
78 .map_err(GraphError::into_render_error)?;
79 self.execute_graph(&graph)
80 .map_err(GraphError::into_render_error)
81 }
82
83 #[allow(clippy::too_many_lines)]
92 pub(crate) fn encode_layers_pass(
93 &self,
94 encoder: &mut wgpu::CommandEncoder,
95 size: RenderSize,
96 layers: &[CompositeLayer<'_>],
97 target: CompositeTarget<'_>,
98 uniforms: &mut UniformArenaLease,
99 ) -> Result<u64, RenderError> {
100 let CompositeTarget {
101 background,
102 blend_space,
103 output,
104 output_view,
105 } = target;
106 for layer in layers {
107 validate_visual(layer.visual)?;
108 }
109 let format = self.intermediate_format();
110 let drawn = self.drawn_layers(layers.iter().map(|layer| layer.visual), size);
111 let drawn = drawn
112 .iter()
113 .map(|(index, bounds, tier)| (&layers[*index], *bounds, *tier))
114 .collect::<Vec<_>>();
115 let background = stored_background(background, format);
116 if drawn.is_empty() {
117 clear_target(encoder, output_view, background);
119 return Ok(1);
120 }
121
122 let cached = self.composite_pipeline();
123 let preparation = BatchPreparationContext {
124 device: self.gpu.device(),
125 layout: &cached.bind_group_layout,
126 sampler: &cached.sampler,
127 no_backdrop: &self.empty_input,
128 blend_space,
129 max_filter_samples: self.quality_controls().max_filter_samples,
130 output_size: size,
131 background,
132 };
133 let batches = composite_batches(&drawn, self.composite_batch_layers())
134 .into_iter()
135 .map(|(range, bounds)| (&drawn[range], bounds))
136 .collect::<Vec<_>>();
137 let bounded = |bounds: &PixelRect| under_half(*bounds, size);
138 let ((first, first_bounds), rest) = batches.split_first().expect("a layer is drawn");
139 let first_program = self.batch_program(first);
140 let first = prepare_batch(&preparation, None, first, uniforms);
141 let first_pass = PassTarget {
144 load: wgpu::LoadOp::Clear(stored_color(background)),
145 scissor: bounded(first_bounds).then_some(*first_bounds),
146 };
147 if rest.is_empty() {
148 self.draw_batch(
149 encoder,
150 cached,
151 first_program,
152 &first,
153 output_view,
154 first_pass,
155 );
156 return Ok(1);
157 }
158
159 let scratch = self.textures.acquire(
160 self.gpu.device(),
161 size,
162 format,
163 "alpha2 layer composition scratch",
164 );
165 let textures = [output, scratch.texture()];
166 let views = [output_view, scratch.view()];
167 let mut current = rest.iter().filter(|(_, bounds)| !bounded(bounds)).count() % 2;
169 self.draw_batch(
170 encoder,
171 cached,
172 first_program,
173 &first,
174 views[current],
175 first_pass,
176 );
177 for (batch, bounds) in rest {
178 let other = 1 - current;
179 let program = self.batch_program(batch);
180 if bounded(bounds) {
181 copy_region(encoder, textures[current], textures[other], *bounds);
184 let prepared = prepare_batch(&preparation, Some(views[other]), batch, uniforms);
185 self.draw_batch(
186 encoder,
187 cached,
188 program,
189 &prepared,
190 views[current],
191 PassTarget {
192 load: wgpu::LoadOp::Load,
193 scissor: Some(*bounds),
194 },
195 );
196 } else {
197 let prepared = prepare_batch(&preparation, Some(views[current]), batch, uniforms);
198 self.draw_batch(
199 encoder,
200 cached,
201 program,
202 &prepared,
203 views[other],
204 PassTarget::WHOLE,
205 );
206 current = other;
207 }
208 }
209 debug_assert_eq!(current, 0);
210 Ok(u64::try_from(batches.len()).unwrap_or(u64::MAX))
211 }
212
213 fn batch_program(
215 &self,
216 batch: &[(&CompositeLayer<'_>, PixelRect, LayerTier)],
217 ) -> (CompositeProgram, LayerTier, usize) {
218 let tier = batch[0].2;
219 debug_assert!(batch.iter().all(|(_, _, other)| *other == tier));
220 let program = self.composite_program(tier);
221 debug_assert!(batch.len() == 1 || program == CompositeProgram::Loop);
222 (program, tier, batch.len())
223 }
224
225 fn draw_batch(
226 &self,
227 encoder: &mut wgpu::CommandEncoder,
228 cached: &CachedCompositePipeline,
229 (program, tier, layers): (CompositeProgram, LayerTier, usize),
230 bind_group: &wgpu::BindGroup,
231 target: &wgpu::TextureView,
232 pass: PassTarget,
233 ) {
234 let pipeline = self.composite_program_pipeline(cached, program);
235 #[cfg(test)]
236 {
237 let mut log = self
238 .composite_pipelines
239 .lock()
240 .unwrap_or_else(std::sync::PoisonError::into_inner);
241 log.passes += 1;
242 log.drawn
243 .insert((program, pipeline.clone(), tier, layers > 1));
244 }
245 #[cfg(not(test))]
246 let _ = (tier, layers);
247 encode_batch(encoder, pipeline, bind_group, target, pass);
248 }
249
250 pub(crate) fn batches_small_layers(&self) -> bool {
253 #[cfg(test)]
254 {
255 if self
256 .composite_batch_limit
257 .load(std::sync::atomic::Ordering::Relaxed)
258 > 0
259 {
260 return true;
261 }
262 }
263 !self.gpu.capabilities().software_rasterizer
264 }
265
266 pub(crate) fn composite_program(&self, tier: LayerTier) -> CompositeProgram {
273 if tier == LayerTier::Small && self.batches_small_layers() {
274 CompositeProgram::Loop
275 } else {
276 CompositeProgram::OneLayer
277 }
278 }
279
280 pub(crate) fn composite_batch_layers(&self) -> usize {
286 #[cfg(test)]
287 {
288 let limit = self
289 .composite_batch_limit
290 .load(std::sync::atomic::Ordering::Relaxed);
291 if limit > 0 {
292 return limit.min(COMPOSITE_BATCH_LAYERS);
293 }
294 }
295 if self.batches_small_layers() {
296 COMPOSITE_BATCH_LAYERS
297 } else {
298 1
299 }
300 }
301
302 pub(crate) fn composite_pipeline(&self) -> &CachedCompositePipeline {
304 self.pipelines.composite.get_or_init(|| {
305 let device = self.gpu.device();
306 let bind_group_layout = batch_layout(device);
307 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
308 label: Some("alpha2 layer composite shader"),
309 source: wgpu::ShaderSource::Wgsl(COMPOSITE_WGSL.into()),
310 });
311 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
312 label: Some("alpha2 layer sampler"),
313 address_mode_u: wgpu::AddressMode::ClampToEdge,
314 address_mode_v: wgpu::AddressMode::ClampToEdge,
315 mag_filter: wgpu::FilterMode::Linear,
316 min_filter: wgpu::FilterMode::Linear,
317 ..Default::default()
318 });
319 CachedCompositePipeline {
320 bind_group_layout,
321 sampler,
322 shader,
323 programs: Default::default(),
324 }
325 })
326 }
327
328 pub(crate) fn composite_program_pipeline<'a>(
329 &self,
330 cached: &'a CachedCompositePipeline,
331 program: CompositeProgram,
332 ) -> &'a wgpu::RenderPipeline {
333 let (slot, entry, label) = match program {
334 CompositeProgram::Loop => (0, "fs", "alpha2 small layer composite pipeline"),
335 CompositeProgram::OneLayer => (1, "fs_layer", "alpha2 large layer composite pipeline"),
336 };
337 cached.programs[slot].get_or_init(|| {
338 #[cfg(test)]
339 {
340 self.composite_pipelines
341 .lock()
342 .unwrap_or_else(std::sync::PoisonError::into_inner)
343 .created
344 .push(program);
345 }
346 crate::diagnostics::timed_pipeline_creation(
347 &self.diagnostics,
348 || label.to_owned(),
349 || {
350 composite_pipeline(
351 self.gpu.device(),
352 &cached.bind_group_layout,
353 &cached.shader,
354 self.intermediate_format(),
355 entry,
356 label,
357 )
358 },
359 )
360 })
361 }
362
363 #[cfg_attr(not(test), allow(clippy::unused_self))]
366 pub(crate) fn drawn_layers(
367 &self,
368 visuals: impl Iterator<Item = LayerTransform>,
369 size: RenderSize,
370 ) -> Vec<(usize, PixelRect, LayerTier)> {
371 #[cfg(test)]
372 let full_frame = self
373 .full_frame_layers
374 .load(std::sync::atomic::Ordering::Relaxed);
375 #[cfg(not(test))]
376 let full_frame = false;
377 visuals
378 .enumerate()
379 .filter_map(|(index, visual)| {
380 if is_degenerate(visual) || is_invisible(visual) {
381 return None;
382 }
383 let bounds = layer_bounds(visual, size);
384 let tier = LayerTier::of(bounds.unwrap_or(PixelRect::EMPTY), size);
387 if full_frame {
388 Some((index, PixelRect::whole(size), tier))
389 } else {
390 bounds.map(|bounds| (index, bounds, tier))
391 }
392 })
393 .collect()
394 }
395
396 pub(crate) fn composite_batch_count(
398 &self,
399 visuals: impl Iterator<Item = LayerTransform>,
400 size: RenderSize,
401 ) -> usize {
402 let drawn = self.drawn_layers(visuals, size);
403 composite_batches(&drawn, self.composite_batch_layers()).len()
404 }
405
406 pub(crate) fn warm_composite(&self, layers: usize) {
407 if layers == 0 {
408 return;
409 }
410 let cached = self.composite_pipeline();
411 self.composite_program_pipeline(cached, CompositeProgram::OneLayer);
412 if self.batches_small_layers() {
413 self.composite_program_pipeline(cached, CompositeProgram::Loop);
414 }
415 }
416}
417
418#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
421pub(crate) enum LayerTier {
422 Small,
424 Large,
426}
427
428impl LayerTier {
429 pub(crate) fn of(bounds: PixelRect, size: RenderSize) -> Self {
430 if under_half(bounds, size) {
431 Self::Small
432 } else {
433 Self::Large
434 }
435 }
436}
437
438fn under_half(bounds: PixelRect, size: RenderSize) -> bool {
439 bounds.area() * 2 < PixelRect::whole(size).area()
440}
441
442#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
444pub(crate) enum CompositeProgram {
445 Loop,
447 OneLayer,
449}
450
451#[cfg(test)]
453#[derive(Debug, Default)]
454pub(crate) struct CompositePipelineLog {
455 pub(crate) created: Vec<CompositeProgram>,
456 pub(crate) passes: usize,
457 pub(crate) drawn:
459 std::collections::HashSet<(CompositeProgram, wgpu::RenderPipeline, LayerTier, bool)>,
460}
461
462pub(crate) fn composite_batches<T>(
468 drawn: &[(T, PixelRect, LayerTier)],
469 limit: usize,
470) -> Vec<(std::ops::Range<usize>, PixelRect)> {
471 let mut batches: Vec<(std::ops::Range<usize>, PixelRect)> = Vec::new();
472 for (index, (_, bounds, tier)) in drawn.iter().enumerate() {
473 match batches.last_mut() {
474 Some((range, union))
475 if range.len() < limit
476 && *tier == LayerTier::Small
477 && drawn[range.start].2 == LayerTier::Small =>
478 {
479 range.end = index + 1;
480 *union = union.union(*bounds);
481 }
482 _ => batches.push((index..index + 1, *bounds)),
483 }
484 }
485 batches
486}
487
488#[derive(Copy, Clone, Debug)]
490enum StoredValue {
491 AsIs,
493 Half,
495 Unorm8,
497}
498
499impl StoredValue {
500 fn of(format: wgpu::TextureFormat) -> Self {
501 match format {
502 wgpu::TextureFormat::Rgba16Float => Self::Half,
503 wgpu::TextureFormat::Rgba8Unorm => Self::Unorm8,
504 _ => Self::AsIs,
505 }
506 }
507
508 const fn shader_value(self) -> f64 {
509 match self {
510 Self::AsIs => 0.0,
511 Self::Half => 1.0,
512 Self::Unorm8 => 2.0,
513 }
514 }
515}
516
517struct BatchPreparationContext<'a> {
518 device: &'a wgpu::Device,
519 layout: &'a wgpu::BindGroupLayout,
520 sampler: &'a wgpu::Sampler,
521 no_backdrop: &'a wgpu::TextureView,
523 blend_space: crate::BlendSpace,
524 max_filter_samples: u32,
525 output_size: RenderSize,
526 background: [f32; 4],
527}
528
529fn prepare_batch(
532 context: &BatchPreparationContext<'_>,
533 backdrop: Option<&wgpu::TextureView>,
534 batch: &[(&CompositeLayer<'_>, PixelRect, LayerTier)],
535 uniforms: &mut UniformArenaLease,
536) -> wgpu::BindGroup {
537 debug_assert!(!batch.is_empty() && batch.len() <= COMPOSITE_BATCH_LAYERS);
538 let mut values = [0.0_f32; BATCH_UNIFORM_FLOATS];
539 #[allow(clippy::cast_precision_loss)]
540 let controls = [
541 context.blend_space.shader_value(),
542 context.max_filter_samples as f32,
543 batch.len() as f32,
544 if backdrop.is_some() { 0.0 } else { 1.0 },
545 context.output_size.width as f32,
546 context.output_size.height as f32,
547 0.0,
548 0.0,
549 context.background[0],
550 context.background[1],
551 context.background[2],
552 context.background[3],
553 ];
554 values[..controls.len()].copy_from_slice(&controls);
555 for ((layer, bounds, _), slot) in batch
556 .iter()
557 .zip(values[controls.len()..].as_chunks_mut::<16>().0)
558 {
559 let visual = layer.visual;
560 #[allow(clippy::cast_precision_loss)]
561 slot.copy_from_slice(&[
562 visual.position[0],
563 visual.position[1],
564 visual.anchor[0],
565 visual.anchor[1],
566 visual.scale[0],
567 visual.scale[1],
568 visual.rotation_degrees.to_radians(),
569 visual.opacity,
570 layer.blend_mode.shader_value(),
571 0.0,
572 0.0,
573 0.0,
574 bounds.x as f32,
575 bounds.y as f32,
576 (bounds.x + bounds.width) as f32,
577 (bounds.y + bounds.height) as f32,
578 ]);
579 }
580 let uniform = uniforms.write(&f32_bytes(values));
581 let texture = |slot: usize| {
582 let (layer, _, _) = batch.get(slot).unwrap_or(&batch[0]);
583 layer.texture
584 };
585 let mut entries = (0..COMPOSITE_BATCH_LAYERS)
586 .map(|slot| wgpu::BindGroupEntry {
587 binding: u32::try_from(slot).expect("few layer slots"),
588 resource: wgpu::BindingResource::TextureView(texture(slot)),
589 })
590 .collect::<Vec<_>>();
591 entries.push(wgpu::BindGroupEntry {
592 binding: SAMPLER_BINDING,
593 resource: wgpu::BindingResource::Sampler(context.sampler),
594 });
595 entries.push(wgpu::BindGroupEntry {
596 binding: BACKDROP_BINDING,
597 resource: wgpu::BindingResource::TextureView(backdrop.unwrap_or(context.no_backdrop)),
598 });
599 entries.push(wgpu::BindGroupEntry {
600 binding: UNIFORM_BINDING,
601 resource: uniforms.binding(uniform),
602 });
603 context
604 .device
605 .create_bind_group(&wgpu::BindGroupDescriptor {
606 label: Some("alpha2 layer batch bind group"),
607 layout: context.layout,
608 entries: &entries,
609 })
610}
611
612fn clear_target(encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, stored: [f32; 4]) {
615 let color = stored_color(stored);
616 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
617 label: Some("alpha2 layer clear pass"),
618 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
619 view: target,
620 depth_slice: None,
621 resolve_target: None,
622 ops: wgpu::Operations {
623 load: wgpu::LoadOp::Clear(color),
624 store: wgpu::StoreOp::Store,
625 },
626 })],
627 depth_stencil_attachment: None,
628 timestamp_writes: None,
629 occlusion_query_set: None,
630 multiview_mask: None,
631 });
632}
633
634#[derive(Copy, Clone)]
635struct PassTarget {
636 load: wgpu::LoadOp<wgpu::Color>,
637 scissor: Option<PixelRect>,
639}
640
641impl PassTarget {
642 const WHOLE: Self = Self {
644 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
645 scissor: None,
646 };
647}
648
649fn stored_color(stored: [f32; 4]) -> wgpu::Color {
651 let [r, g, b, a] = stored.map(f64::from);
652 wgpu::Color { r, g, b, a }
653}
654
655fn encode_batch(
657 encoder: &mut wgpu::CommandEncoder,
658 pipeline: &wgpu::RenderPipeline,
659 bind_group: &wgpu::BindGroup,
660 target: &wgpu::TextureView,
661 PassTarget { load, scissor }: PassTarget,
662) {
663 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
664 label: Some("alpha2 layer composition pass"),
665 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
666 view: target,
667 depth_slice: None,
668 resolve_target: None,
669 ops: wgpu::Operations {
670 load,
671 store: wgpu::StoreOp::Store,
672 },
673 })],
674 depth_stencil_attachment: None,
675 timestamp_writes: None,
676 occlusion_query_set: None,
677 multiview_mask: None,
678 });
679 pass.set_pipeline(pipeline);
680 pass.set_bind_group(0, bind_group, &[]);
681 if let Some(scissor) = scissor {
682 pass.set_scissor_rect(scissor.x, scissor.y, scissor.width, scissor.height);
683 }
684 pass.draw(0..3, 0..1);
685}
686
687fn copy_region(
688 encoder: &mut wgpu::CommandEncoder,
689 source: &wgpu::Texture,
690 destination: &wgpu::Texture,
691 region: PixelRect,
692) {
693 let at = |texture| wgpu::TexelCopyTextureInfo {
694 texture,
695 mip_level: 0,
696 origin: wgpu::Origin3d {
697 x: region.x,
698 y: region.y,
699 z: 0,
700 },
701 aspect: wgpu::TextureAspect::All,
702 };
703 encoder.copy_texture_to_texture(
704 at(source),
705 at(destination),
706 wgpu::Extent3d {
707 width: region.width,
708 height: region.height,
709 depth_or_array_layers: 1,
710 },
711 );
712}
713
714#[derive(Copy, Clone, Debug, Eq, PartialEq)]
716pub(crate) struct PixelRect {
717 pub(crate) x: u32,
718 pub(crate) y: u32,
719 pub(crate) width: u32,
720 pub(crate) height: u32,
721}
722
723impl PixelRect {
724 const EMPTY: Self = Self {
726 x: 0,
727 y: 0,
728 width: 0,
729 height: 0,
730 };
731
732 const fn whole(size: RenderSize) -> Self {
733 Self {
734 x: 0,
735 y: 0,
736 width: size.width,
737 height: size.height,
738 }
739 }
740
741 pub(crate) fn area(self) -> u64 {
742 u64::from(self.width) * u64::from(self.height)
743 }
744
745 fn union(self, other: Self) -> Self {
746 let x = self.x.min(other.x);
747 let y = self.y.min(other.y);
748 let right = (self.x + self.width).max(other.x + other.width);
749 let bottom = (self.y + self.height).max(other.y + other.height);
750 Self {
751 x,
752 y,
753 width: right - x,
754 height: bottom - y,
755 }
756 }
757}
758
759const COVERAGE_MARGIN: f64 = 4.0;
762
763pub(crate) fn layer_bounds(visual: LayerTransform, size: RenderSize) -> Option<PixelRect> {
769 let (width, height) = (f64::from(size.width), f64::from(size.height));
770 let (turn_sin, turn_cos) = libm::sincos(f64::from(visual.rotation_degrees).to_radians());
771 let corner = |u: f64, v: f64| {
772 let x = (u - f64::from(visual.anchor[0])) * f64::from(visual.scale[0]) * width;
773 let y = (v - f64::from(visual.anchor[1])) * f64::from(visual.scale[1]) * height;
774 (
775 f64::from(visual.position[0]) * width + turn_cos * x - turn_sin * y,
776 f64::from(visual.position[1]) * height + turn_sin * x + turn_cos * y,
777 )
778 };
779 let corners = [
780 corner(0.0, 0.0),
781 corner(1.0, 0.0),
782 corner(0.0, 1.0),
783 corner(1.0, 1.0),
784 ];
785 let fold = |pick: fn(&(f64, f64)) -> f64, extreme: fn(f64, f64) -> f64, start: f64| {
786 corners.iter().map(pick).fold(start, extreme)
787 };
788 let left = (fold(|c| c.0, f64::min, f64::INFINITY) - COVERAGE_MARGIN)
789 .floor()
790 .max(0.0);
791 let top = (fold(|c| c.1, f64::min, f64::INFINITY) - COVERAGE_MARGIN)
792 .floor()
793 .max(0.0);
794 let right = (fold(|c| c.0, f64::max, f64::NEG_INFINITY) + COVERAGE_MARGIN)
795 .ceil()
796 .min(width);
797 let bottom = (fold(|c| c.1, f64::max, f64::NEG_INFINITY) + COVERAGE_MARGIN)
798 .ceil()
799 .min(height);
800 if !(right > left && bottom > top) {
801 return None;
802 }
803 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
804 Some(PixelRect {
805 x: left as u32,
806 y: top as u32,
807 width: (right - left) as u32,
808 height: (bottom - top) as u32,
809 })
810}
811
812pub(crate) fn stored_background(
819 background: Option<Color>,
820 format: wgpu::TextureFormat,
821) -> [f32; 4] {
822 let Some(color) = background else {
823 return [0.0; 4];
824 };
825 #[allow(clippy::cast_possible_truncation)]
827 let channel = |value: f64| {
828 let value = value as f32;
829 match format {
830 wgpu::TextureFormat::Rgba8Unorm => (value.clamp(0.0, 1.0) * 255.0).round() / 255.0,
831 wgpu::TextureFormat::Rgba16Float => half::f16::from_f32(value).to_f32(),
832 _ => value,
833 }
834 };
835 [
836 channel(color.red),
837 channel(color.green),
838 channel(color.blue),
839 channel(color.alpha),
840 ]
841}
842
843const SAMPLER_BINDING: u32 = 8;
844const BACKDROP_BINDING: u32 = 9;
845const UNIFORM_BINDING: u32 = 10;
846
847fn batch_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
849 let mut entries = (0..COMPOSITE_BATCH_LAYERS)
850 .map(|slot| texture_entry(u32::try_from(slot).expect("few layer slots"), true))
851 .collect::<Vec<_>>();
852 entries.push(wgpu::BindGroupLayoutEntry {
853 binding: SAMPLER_BINDING,
854 visibility: wgpu::ShaderStages::FRAGMENT,
855 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
856 count: None,
857 });
858 entries.push(texture_entry(BACKDROP_BINDING, false));
859 entries.push(wgpu::BindGroupLayoutEntry {
860 binding: UNIFORM_BINDING,
861 visibility: wgpu::ShaderStages::FRAGMENT,
862 ty: batch_uniform_binding(),
863 count: None,
864 });
865 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
866 label: Some("alpha2 layer batch bind group layout"),
867 entries: &entries,
868 })
869}
870
871fn batch_uniform_binding() -> wgpu::BindingType {
874 wgpu::BindingType::Buffer {
875 ty: wgpu::BufferBindingType::Uniform,
876 has_dynamic_offset: false,
877 min_binding_size: std::num::NonZeroU64::new(float_uniform_bytes(BATCH_UNIFORM_FLOATS)),
878 }
879}
880
881fn texture_entry(binding: u32, filterable: bool) -> wgpu::BindGroupLayoutEntry {
882 wgpu::BindGroupLayoutEntry {
883 binding,
884 visibility: wgpu::ShaderStages::FRAGMENT,
885 ty: wgpu::BindingType::Texture {
886 sample_type: wgpu::TextureSampleType::Float { filterable },
887 view_dimension: wgpu::TextureViewDimension::D2,
888 multisampled: false,
889 },
890 count: None,
891 }
892}
893
894fn composite_pipeline(
895 device: &wgpu::Device,
896 layout: &wgpu::BindGroupLayout,
897 shader: &wgpu::ShaderModule,
898 format: wgpu::TextureFormat,
899 fragment_entry: &str,
900 label: &str,
901) -> wgpu::RenderPipeline {
902 let label = Some(label);
903 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
904 label,
905 bind_group_layouts: &[Some(layout)],
906 immediate_size: 0,
907 });
908 let constants = [("STORED_FORMAT", StoredValue::of(format).shader_value())];
909 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
910 label,
911 layout: Some(&pipeline_layout),
912 vertex: wgpu::VertexState {
913 module: shader,
914 entry_point: Some("vs"),
915 buffers: &[],
916 compilation_options: wgpu::PipelineCompilationOptions::default(),
917 },
918 fragment: Some(wgpu::FragmentState {
919 module: shader,
920 entry_point: Some(fragment_entry),
921 targets: &[Some(format.into())],
922 compilation_options: wgpu::PipelineCompilationOptions {
923 constants: &constants,
924 ..wgpu::PipelineCompilationOptions::default()
925 },
926 }),
927 primitive: wgpu::PrimitiveState::default(),
928 depth_stencil: None,
929 multisample: wgpu::MultisampleState::default(),
930 multiview_mask: None,
931 cache: None,
932 })
933}
934
935pub(crate) fn validate_visual(visual: LayerTransform) -> Result<(), RenderError> {
936 let values = [
937 visual.position[0],
938 visual.position[1],
939 visual.anchor[0],
940 visual.anchor[1],
941 visual.scale[0],
942 visual.scale[1],
943 visual.rotation_degrees,
944 visual.opacity,
945 ];
946 if values.into_iter().any(|value| !value.is_finite()) || !(0.0..=1.0).contains(&visual.opacity)
947 {
948 return Err(RenderError::InvalidVisual);
949 }
950 Ok(())
951}
952
953pub(crate) fn is_degenerate(visual: LayerTransform) -> bool {
958 visual.scale[0].abs() <= f32::EPSILON || visual.scale[1].abs() <= f32::EPSILON
959}
960
961pub(crate) fn is_invisible(visual: LayerTransform) -> bool {
965 visual.opacity <= 0.0
966}