1use std::any::TypeId;
2use std::borrow::Cow;
3use std::collections::{HashMap, HashSet};
4use std::hash::Hash;
5use std::num::NonZeroU64;
6#[cfg(feature = "winit-surface")]
7use std::panic::{AssertUnwindSafe, catch_unwind};
8use std::sync::{Arc, Mutex, Weak};
9
10use repose_core::color::{ChromaSiting, ColorInfo, PixelFormat};
11use repose_core::{
12 Brush, FontStyle, GradientStops, ImageAlignment, ImageFilter, ImageFit, ImagePaintStyle,
13 ImageSourceRect, MAX_GRADIENT_STOPS, Scene, SceneNode, StrokeCap, TextPaintStyle, Transform,
14 Vec2,
15};
16#[cfg(feature = "winit-surface")]
17use repose_core::{GlyphRasterConfig, PresentModePref, RenderBackend, request_frame};
18#[cfg(feature = "winit-surface")]
19use wgpu::Instance;
20
21mod slug;
22
23fn align_up(value: u64, alignment: u64) -> anyhow::Result<u64> {
24 if !alignment.is_power_of_two() {
25 anyhow::bail!("alignment must be a power of two");
26 }
27 value
28 .checked_add(alignment - 1)
29 .map(|v| v & !(alignment - 1))
30 .ok_or_else(|| anyhow::anyhow!("alignment overflow"))
31}
32
33fn mesh_uniform_stride(size: u64, base: u64) -> u64 {
41 let rounded = size.saturating_add(base.saturating_sub(1)) / base * base;
42 rounded.next_power_of_two().max(base)
43}
44
45mod commands;
46pub use commands::RenderCommandFailure;
47pub use commands::RenderCommandReport;
48pub use commands::apply_render_commands;
49
50pub mod offscreen;
51
52mod callback;
53pub use callback::{
54 Callback, CallbackRenderPass, CallbackResources, ScreenDescriptor, WgpuCallback,
55};
56
57mod depth_composite;
58pub use depth_composite::{BlitRenderPass, DepthComposite};
59
60#[derive(Clone)]
61struct UploadRing {
62 buf: wgpu::Buffer,
63 cap: u64,
64 head: u64,
65 usage: wgpu::BufferUsages,
66}
67
68impl UploadRing {
69 fn new(device: &wgpu::Device, label: &str, cap: u64, usage: wgpu::BufferUsages) -> Self {
70 let max = device.limits().max_buffer_size;
71 let cap = cap.max(4).min(max).min(MAX_UPLOAD_RING_BYTES).max(4);
72 let buf = device.create_buffer(&wgpu::BufferDescriptor {
73 label: Some(label),
74 size: cap,
75 usage,
76 mapped_at_creation: false,
77 });
78 Self {
79 buf,
80 cap,
81 head: 0,
82 usage,
83 }
84 }
85
86 fn reset(&mut self) {
87 self.head = 0;
88 }
89
90 fn grow_to_fit(
91 &mut self,
92 device: &wgpu::Device,
93 encoder: &mut wgpu::CommandEncoder,
94 needed: u64,
95 ) -> anyhow::Result<()> {
96 if needed == 0 {
97 return Ok(());
98 }
99 let start = align_up(self.head, 4)?;
100 let end = start
101 .checked_add(needed)
102 .ok_or_else(|| anyhow::anyhow!("upload ring offset overflow"))?;
103 if end <= self.cap {
104 return Ok(());
105 }
106 let required = align_up(end, 4)?;
107 let doubled = self.cap.checked_mul(2).unwrap_or(0);
108 let new_cap = required.max(doubled).max(256);
109 let new_cap = align_up(new_cap, 4)?;
110 if new_cap > device.limits().max_buffer_size || new_cap > MAX_UPLOAD_RING_BYTES {
111 anyhow::bail!("upload ring exceeds resource budget");
112 }
113 if !self.usage.contains(wgpu::BufferUsages::COPY_SRC) {
114 anyhow::bail!("upload ring growth requires COPY_SRC");
115 }
116 let old = self.buf.clone();
117 self.buf = device.create_buffer(&wgpu::BufferDescriptor {
118 label: Some("upload ring (grown)"),
119 size: new_cap,
120 usage: self.usage,
121 mapped_at_creation: false,
122 });
123 self.cap = new_cap;
124 if self.head > 0 {
125 let copy_size = align_up(self.head, 4)?;
126 if copy_size > old.size() {
127 anyhow::bail!("upload ring old data exceeds old buffer");
128 }
129 encoder.copy_buffer_to_buffer(&old, 0, &self.buf, 0, copy_size);
130 }
131 Ok(())
132 }
133
134 fn alloc_write(&mut self, queue: &wgpu::Queue, bytes: &[u8]) -> anyhow::Result<u64> {
135 let len = u64::try_from(bytes.len())
136 .map_err(|_| anyhow::anyhow!("upload byte length exceeds u64"))?;
137 let start = align_up(self.head, 4)?;
138 let end = start
139 .checked_add(len)
140 .ok_or_else(|| anyhow::anyhow!("upload ring offset overflow"))?;
141 if end > self.cap {
142 anyhow::bail!(
143 "upload ring overflow: start={start} len={len} cap={}",
144 self.cap
145 );
146 }
147 queue.write_buffer(&self.buf, start, bytes);
148 self.head = end;
149 Ok(start)
150 }
151}
152
153struct InstancedPipe<I: bytemuck::Pod> {
154 ring: UploadRing,
155 _marker: std::marker::PhantomData<I>,
156}
157
158impl<I: bytemuck::Pod> InstancedPipe<I> {
159 fn new(ring: UploadRing) -> Self {
160 Self {
161 ring,
162 _marker: std::marker::PhantomData,
163 }
164 }
165
166 fn upload(
167 &mut self,
168 device: &wgpu::Device,
169 queue: &wgpu::Queue,
170 encoder: &mut wgpu::CommandEncoder,
171 data: &[I],
172 ) -> Option<(u64, u32)> {
173 if data.is_empty() {
174 return None;
175 }
176 let count = u32::try_from(data.len()).ok()?;
177 let bytes = bytemuck::cast_slice(data);
178 let byte_len = u64::try_from(bytes.len()).ok()?;
179 if byte_len == 0 || byte_len > device.limits().max_buffer_size {
180 return None;
181 }
182 self.ring
183 .grow_to_fit(device, encoder, byte_len)
184 .map_err(|error| log::error!("{error:#}"))
185 .ok()?;
186 let off = self
187 .ring
188 .alloc_write(queue, bytes)
189 .map_err(|error| log::error!("{error:#}"))
190 .ok()?;
191 Some((off, count))
192 }
193
194 fn reset(&mut self) {
195 self.ring.reset();
196 }
197}
198
199#[repr(C)]
200#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
201struct Globals {
202 ndc_to_px: [f32; 2],
203 _pad: [f32; 2],
204}
205
206fn make_globals(target_w: f32, target_h: f32) -> Globals {
207 Globals {
208 ndc_to_px: [target_w * 0.5, target_h * 0.5],
209 _pad: [0.0, 0.0],
210 }
211}
212
213pub struct WgpuSceneRenderer {
214 pub device: wgpu::Device,
215 pub queue: wgpu::Queue,
216 pub output_format: wgpu::TextureFormat,
217 surface_format: wgpu::TextureFormat,
219 pub output_width: u32,
220 pub output_height: u32,
221 pub pixels_per_point: f32,
223
224 surface_pipes: Pipelines,
227 working_space_pipes: Pipelines,
228 layer_pipes: Pipelines,
229 working_space_layer_pipes: Pipelines,
230
231 rects: InstancedPipe<RectInstance>,
233 borders: InstancedPipe<BorderInstance>,
234 ellipses: InstancedPipe<EllipseInstance>,
235 ellipse_borders: InstancedPipe<EllipseBorderInstance>,
236 arcs: InstancedPipe<ArcInstance>,
237 glyph_mask: InstancedPipe<GlyphInstance>,
238 glyph_color: InstancedPipe<GlyphInstance>,
239
240 image_bind_layout_rgba: wgpu::BindGroupLayout,
242 image_bind_layout_nv12: wgpu::BindGroupLayout,
243 image_sampler: wgpu::Sampler,
244 image_sampler_nearest: wgpu::Sampler,
245 image_sampler_repeat: wgpu::Sampler,
246 image_sampler_repeat_nearest: wgpu::Sampler,
247 layer_sampler: wgpu::Sampler,
248 layer_sampler_linear: wgpu::Sampler,
249
250 blur_ring: UploadRing,
252
253 text_bind_layout: wgpu::BindGroupLayout,
254
255 clip_ring: UploadRing,
257
258 projective_ring: UploadRing,
261
262 blend_ring: UploadRing,
264
265 slug_enabled: bool,
267 slug_ring: UploadRing,
268 slug_cache: slug::GlyphSlugCache,
269 slug_draw_cache: HashMap<SlugDrawKey, SlugDrawCacheEntry>,
270 slug_draw_cache_bytes_total: u64,
271
272 nv12: InstancedPipe<Nv12Instance>,
274
275 mesh_cache: HashMap<MeshCacheKey, MeshCacheEntry>,
276 mesh_cache_bytes_total: u64,
277 mesh_uniform_buf: wgpu::Buffer,
278 mesh_bind: wgpu::BindGroup,
279 mesh_uniform_head: u64,
280 mesh_uniform_alignment: u64,
281 mesh_uniform_cap: u64,
282
283 flatten_layer_ids: Vec<u32>,
287
288 blur_scratch_ids: HashMap<(u32, u32), u32>,
293 blur_scratch_next: u32,
294
295 blend_snapshots: std::collections::HashMap<u32, BlendSnapshot>,
299 blend_snapshot_pool: HashMap<BlendSnapshotKey, BlendSnapshot>,
300 blend_copies: Vec<BlendCopy>,
304
305 msaa_samples: u32,
306 working_space_msaa_samples: u32,
307
308 depth_stencil_tex: wgpu::Texture,
310 depth_stencil_view: wgpu::TextureView,
311
312 msaa_tex: Option<wgpu::Texture>,
314 msaa_view: Option<wgpu::TextureView>,
315 ws_msaa_tex: Option<wgpu::Texture>,
316 ws_msaa_view: Option<wgpu::TextureView>,
317 surface_resolve_tex: Option<wgpu::Texture>,
318 surface_resolve_view: Option<wgpu::TextureView>,
319 surface_resolve_bytes: u64,
320 msaa_bytes: u64,
321 ws_msaa_bytes: u64,
322 depth_stencil_bytes: u64,
323 working_space_bytes: u64,
324 gpu_budget_bytes: u64,
325
326 globals_buf: wgpu::Buffer,
327 globals_bind: wgpu::BindGroup,
328 globals_staging: wgpu::Buffer,
329 globals_staging_cap: u64,
330
331 atlas_mask: AtlasA8,
333 atlas_color: AtlasRGBA,
334 atlas_mask_bind: wgpu::BindGroup,
335 atlas_color_bind: wgpu::BindGroup,
336 atlas_generation: u64,
337 atlas_mask_pending: AtlasPending,
338 atlas_color_pending: AtlasPending,
339 atlas_last_compact_frame: u64,
340 atlas_mask_failures: HashSet<repose_text::CacheKey>,
341 atlas_color_failures: HashSet<repose_text::CacheKey>,
342 atlas_mask_deferred: HashSet<repose_text::CacheKey>,
345 atlas_color_deferred: HashSet<repose_text::CacheKey>,
346
347 glyph_outline_cache: HashMap<repose_text::CacheKey, GlyphOutlineEntry>,
348 legacy_shaped_text_cache: HashMap<TextShapeKey, TextShapeCacheEntry>,
349
350 next_image_handle: u64,
352 images: HashMap<u64, ImageTex>,
353 retained: HashMap<u64, RetainedImage>,
354 retained_bytes_total: u64,
355
356 next_coverage_handle: u64,
359 coverages: HashMap<u64, CoverageTex>,
360
361 frame_index: u64,
363 cache_touch_clock: u64,
364 image_bytes_total: u64,
365 image_evict_after_frames: u64,
366 image_budget_bytes: u64,
367
368 layer_pool: HashMap<u32, LayerTarget>,
371 producer_layer_ids: HashSet<u32>,
372 layer_bytes_total: u64,
373 transient_layer_bytes_total: u64,
374
375 working_space: bool,
379 ws_tex: Option<wgpu::Texture>,
380 ws_view: Option<wgpu::TextureView>,
381 ws_bind: Option<wgpu::BindGroup>,
382 display_pipeline: Option<wgpu::RenderPipeline>,
383 display_layout: Option<wgpu::BindGroupLayout>,
384
385 pub callback_resources: CallbackResources,
386 callback_scoped_resources: HashMap<CallbackScopeKey, CallbackResources>,
387 callback_scope_uses: HashMap<CallbackScopeKey, CallbackScopeUse>,
388 callback_scope_payloads: HashMap<CallbackScopeKey, Weak<Callback>>,
389 callback_scope_clock: u64,
390 blend_snapshot_bytes_total: u64,
391 frame_active: bool,
392 last_render_error: Option<String>,
393}
394
395pub struct WgpuSurfaceBackend {
396 #[cfg(feature = "winit-surface")]
397 instance: Option<wgpu::Instance>,
398 #[cfg(feature = "winit-surface")]
399 window: Option<Arc<winit::window::Window>>,
400 pub surface: Option<wgpu::Surface<'static>>,
401 pub surface_config: Option<wgpu::SurfaceConfiguration>,
402 #[cfg_attr(not(feature = "winit-surface"), allow(dead_code))]
403 pending_reconfigure: bool,
404 pub renderer: WgpuSceneRenderer,
405 device_lost: Arc<Mutex<Option<(wgpu::DeviceLostReason, String)>>>,
406}
407
408impl std::ops::Deref for WgpuSurfaceBackend {
409 type Target = WgpuSceneRenderer;
410 fn deref(&self) -> &Self::Target {
411 &self.renderer
412 }
413}
414impl std::ops::DerefMut for WgpuSurfaceBackend {
415 fn deref_mut(&mut self) -> &mut Self::Target {
416 &mut self.renderer
417 }
418}
419
420#[cfg(feature = "winit-surface")]
421pub type WgpuBackend = WgpuSurfaceBackend;
422
423pub fn clamp_output_size(width: u32, height: u32, max: u32) -> (u32, u32) {
433 if width == 0 || height == 0 {
434 return (0, 0);
435 }
436 let max = max.max(1);
437 if width <= max && height <= max {
438 return (width, height);
439 }
440 let scale = max as f64 / width.max(height) as f64;
441 let clamped = |v: u32| ((v as f64 * scale).round() as u32).clamp(1, max);
442 (clamped(width), clamped(height))
443}
444
445fn blur_sigma(radius: f32) -> f32 {
448 (radius.max(0.0) * 0.577_350_3 + 0.5).max(0.0)
449}
450
451fn blur_pad(radius: f32) -> f32 {
454 (blur_sigma(radius) * 3.0).ceil().max(0.0)
455}
456
457const BLUR_SCRATCH_ID_BASE: u32 = 0xE000_0000;
460
461const BLUR_SLOT_CONTENT: u32 = 0;
463const BLUR_SLOT_SHADOW: u32 = 1;
467
468impl Drop for WgpuSceneRenderer {
469 fn drop(&mut self) {
470 let _ = self.device.poll(wgpu::PollType::Poll);
471 #[cfg(not(target_arch = "wasm32"))]
472 {
473 let _ = self.device.poll(wgpu::PollType::Wait {
474 submission_index: None,
475 timeout: Some(web_time::Duration::from_millis(100)),
476 });
477 }
478 }
479}
480
481#[derive(Clone)]
482struct LayerTarget {
483 texture: wgpu::Texture,
484 view: wgpu::TextureView,
485 bind: wgpu::BindGroup,
486 bind_linear: wgpu::BindGroup,
487 depth_stencil_view: wgpu::TextureView,
488 width: u32,
489 height: u32,
490 format: wgpu::TextureFormat,
491 bytes: u64,
492 rect_px: (f32, f32, f32, f32),
493 transient: bool,
494}
495
496#[derive(Clone, Copy, PartialEq, Eq, Hash)]
500struct BlendSnapshotKey {
501 format: wgpu::TextureFormat,
502 width: u32,
503 height: u32,
504}
505
506#[derive(Clone)]
507struct BlendSnapshot {
508 key: BlendSnapshotKey,
509 texture: wgpu::Texture,
510 bind: wgpu::BindGroup,
511 bytes: u64,
512}
513
514struct BlendCopy {
515 pass_id: u64,
516 blend_id: u32,
517 target: PassTarget,
518 region: repose_core::Rect,
519}
520
521#[derive(Clone)]
522enum ActiveClip {
523 Rect {
524 off: u64,
525 cnt: u32,
526 rect: repose_core::Rect,
527 radii: [f32; 4],
528 scissor: (u32, u32, u32, u32),
533 difference: bool,
534 applied: bool,
535 blocked: bool,
536 },
537 Vector {
538 mesh: Option<MeshGpu>,
539 source: Arc<repose_core::VectorMeshData>,
540 uoff: u64,
541 affine: [f32; 6],
542 scissor: (u32, u32, u32, u32),
543 difference: bool,
544 applied: bool,
545 blocked: bool,
546 },
547}
548
549impl ActiveClip {
550 fn difference(&self) -> bool {
551 match self {
552 Self::Rect { difference, .. } | Self::Vector { difference, .. } => *difference,
553 }
554 }
555
556 fn blocked(&self) -> bool {
557 match self {
558 Self::Rect { blocked, .. } | Self::Vector { blocked, .. } => *blocked,
559 }
560 }
561}
562
563#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
565enum PassTarget {
566 Surface,
567 Layer(u32),
568}
569
570#[derive(Clone, Debug, PartialEq, Eq, Hash)]
571struct CallbackIdentity {
572 type_id: TypeId,
573 key: Option<Arc<str>>,
576 reusable: bool,
577}
578
579#[derive(Clone, Debug, PartialEq, Eq, Hash)]
591struct CallbackScopeKey {
592 callback: CallbackIdentity,
593 target: PassTarget,
594 target_format: wgpu::TextureFormat,
595 sample_count: u32,
596}
597
598#[derive(Clone, Copy)]
599struct CallbackScopeUse {
600 tick: u64,
601 frame: u64,
602}
603
604fn callback_identity(callback: &Callback, fallback: usize) -> CallbackIdentity {
605 match callback.0.resource_key() {
606 Some(key) => CallbackIdentity {
607 type_id: callback.0.resource_type_id(),
608 key: Some(Arc::from(key)),
609 reusable: true,
610 },
611 None => CallbackIdentity {
612 type_id: TypeId::of::<Callback>(),
613 key: Some(Arc::from(fallback.to_string())),
614 reusable: false,
615 },
616 }
617}
618
619fn callback_scope_key(
620 callback: &Callback,
621 fallback: usize,
622 target: PassTarget,
623 descriptor: &ScreenDescriptor,
624) -> CallbackScopeKey {
625 CallbackScopeKey {
626 callback: callback_identity(callback, fallback),
627 target,
628 target_format: descriptor.target_format,
629 sample_count: descriptor.sample_count,
630 }
631}
632
633const FIXED_FUNCTION_BLENDS: [repose_core::BlendMode; 5] = [
638 repose_core::BlendMode::Add,
639 repose_core::BlendMode::Multiply,
640 repose_core::BlendMode::Screen,
641 repose_core::BlendMode::Darken,
642 repose_core::BlendMode::Lighten,
643];
644
645fn fixed_function_blend(mode: repose_core::BlendMode) -> Option<wgpu::BlendState> {
649 let premult_alpha = wgpu::BlendComponent::OVER;
650 let additive = wgpu::BlendComponent {
651 src_factor: wgpu::BlendFactor::One,
652 dst_factor: wgpu::BlendFactor::One,
653 operation: wgpu::BlendOperation::Add,
654 };
655 Some(match mode {
656 repose_core::BlendMode::Add => wgpu::BlendState {
657 color: additive,
658 alpha: additive,
659 },
660 repose_core::BlendMode::Multiply => wgpu::BlendState {
661 color: wgpu::BlendComponent {
662 src_factor: wgpu::BlendFactor::Dst,
663 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
664 operation: wgpu::BlendOperation::Add,
665 },
666 alpha: premult_alpha,
667 },
668 repose_core::BlendMode::Screen => wgpu::BlendState {
669 color: wgpu::BlendComponent {
670 src_factor: wgpu::BlendFactor::One,
671 dst_factor: wgpu::BlendFactor::OneMinusSrc,
672 operation: wgpu::BlendOperation::Add,
673 },
674 alpha: premult_alpha,
675 },
676 repose_core::BlendMode::Darken => wgpu::BlendState {
677 color: wgpu::BlendComponent {
678 src_factor: wgpu::BlendFactor::One,
679 dst_factor: wgpu::BlendFactor::One,
680 operation: wgpu::BlendOperation::Min,
681 },
682 alpha: premult_alpha,
683 },
684 repose_core::BlendMode::Lighten => wgpu::BlendState {
685 color: wgpu::BlendComponent {
686 src_factor: wgpu::BlendFactor::One,
687 dst_factor: wgpu::BlendFactor::One,
688 operation: wgpu::BlendOperation::Max,
689 },
690 alpha: premult_alpha,
691 },
692 _ => return None,
693 })
694}
695
696#[derive(Clone)]
701struct Pipelines {
702 rects: wgpu::RenderPipeline,
703 borders: wgpu::RenderPipeline,
704 ellipses: wgpu::RenderPipeline,
705 ellipse_borders: wgpu::RenderPipeline,
706 arcs: wgpu::RenderPipeline,
707 text_mask: wgpu::RenderPipeline,
708 text_color: wgpu::RenderPipeline,
709 image_rgba: wgpu::RenderPipeline,
710 image_blends: [wgpu::RenderPipeline; FIXED_FUNCTION_BLENDS.len()],
714 coverage: wgpu::RenderPipeline,
718 image_nv12: wgpu::RenderPipeline,
719 image_nv12_blends: [wgpu::RenderPipeline; FIXED_FUNCTION_BLENDS.len()],
722 blur: wgpu::RenderPipeline,
723 blur_content: wgpu::RenderPipeline,
724 clip_bin: wgpu::RenderPipeline,
725 clip_dec: wgpu::RenderPipeline,
726 slug: Option<wgpu::RenderPipeline>,
727 mesh: wgpu::RenderPipeline,
731 mesh_overlay: wgpu::RenderPipeline,
734 mesh_blends: [wgpu::RenderPipeline; FIXED_FUNCTION_BLENDS.len()],
739 blend_layer: wgpu::RenderPipeline,
743 mesh_clip_inc: wgpu::RenderPipeline,
745 mesh_clip_dec: wgpu::RenderPipeline,
747 projective_layer: wgpu::RenderPipeline,
751}
752
753fn vattrs(spec: &[(u32, u64, wgpu::VertexFormat)]) -> Vec<wgpu::VertexAttribute> {
757 spec.iter()
758 .map(|&(shader_location, offset, format)| wgpu::VertexAttribute {
759 shader_location,
760 offset,
761 format,
762 })
763 .collect()
764}
765
766macro_rules! field_offset {
769 ($ty:ty, $field:ident) => {
770 std::mem::offset_of!($ty, $field) as u64
771 };
772}
773
774impl Pipelines {
775 fn create(
776 device: &wgpu::Device,
777 format: wgpu::TextureFormat,
778 sample_count: u32,
779 globals_layout: &wgpu::BindGroupLayout,
780 text_bind_layout: &wgpu::BindGroupLayout,
781 image_bind_layout_nv12: &wgpu::BindGroupLayout,
782 clip_pipeline_layout: &wgpu::PipelineLayout,
783 stencil_for_content: &wgpu::DepthStencilState,
784 stencil_for_clip_inc: &wgpu::DepthStencilState,
785 stencil_for_clip_dec: &wgpu::DepthStencilState,
786 clip_vertex_layout: &wgpu::VertexBufferLayout,
787 mesh_bind_layout: &wgpu::BindGroupLayout,
788 ) -> Self {
789 let msaa_state = wgpu::MultisampleState {
790 count: sample_count,
791 mask: !0,
792 alpha_to_coverage_enabled: false,
793 };
794
795 macro_rules! make_content_pipeline {
796 ($name:ident, $shader:literal, $inst_type:ty, $attrs:expr) => {
797 let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
798 label: Some(concat!($shader, ".wgsl")),
799 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(concat!(
800 "shaders/", $shader, ".wgsl"
801 )))),
802 });
803 let pipeline_layout =
804 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
805 label: Some(concat!($shader, " pipeline layout")),
806 bind_group_layouts: &[Some(globals_layout)],
807 immediate_size: 0,
808 });
809 let $name = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
810 label: Some(concat!($shader, " pipeline")),
811 layout: Some(&pipeline_layout),
812 vertex: wgpu::VertexState {
813 module: &shader_module,
814 entry_point: Some("vs_main"),
815 buffers: &[Some(wgpu::VertexBufferLayout {
816 array_stride: std::mem::size_of::<$inst_type>() as u64,
817 step_mode: wgpu::VertexStepMode::Instance,
818 attributes: $attrs,
819 })],
820 compilation_options: wgpu::PipelineCompilationOptions::default(),
821 },
822 fragment: Some(wgpu::FragmentState {
823 module: &shader_module,
824 entry_point: Some("fs_main"),
825 targets: &[Some(wgpu::ColorTargetState {
826 format,
827 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
828 write_mask: wgpu::ColorWrites::ALL,
829 })],
830 compilation_options: wgpu::PipelineCompilationOptions::default(),
831 }),
832 primitive: wgpu::PrimitiveState::default(),
833 depth_stencil: Some(stencil_for_content.clone()),
834 multisample: msaa_state,
835 multiview_mask: None,
836 cache: None,
837 });
838 };
839 }
840
841 let rect_attrs = vattrs(&[
842 (
843 0,
844 field_offset!(RectInstance, xywh),
845 wgpu::VertexFormat::Float32x4,
846 ),
847 (
848 1,
849 field_offset!(RectInstance, radii),
850 wgpu::VertexFormat::Float32x4,
851 ),
852 (
853 2,
854 field_offset!(RectInstance, color0),
855 wgpu::VertexFormat::Float32x4,
856 ),
857 (
858 3,
859 field_offset!(RectInstance, color1),
860 wgpu::VertexFormat::Float32x4,
861 ),
862 (
863 4,
864 field_offset!(RectInstance, fwd_mat),
865 wgpu::VertexFormat::Float32x4,
866 ),
867 (
868 5,
869 field_offset!(RectInstance, grad),
870 wgpu::VertexFormat::Float32x4,
871 ),
872 (
873 6,
874 field_offset!(RectInstance, bits),
875 wgpu::VertexFormat::Uint32,
876 ),
877 ]);
878 let border_attrs = vattrs(&[
879 (
880 0,
881 field_offset!(BorderInstance, xywh),
882 wgpu::VertexFormat::Float32x4,
883 ),
884 (
885 1,
886 field_offset!(BorderInstance, radii),
887 wgpu::VertexFormat::Float32x4,
888 ),
889 (
890 2,
891 field_offset!(BorderInstance, color0),
892 wgpu::VertexFormat::Float32x4,
893 ),
894 (
895 3,
896 field_offset!(BorderInstance, color1),
897 wgpu::VertexFormat::Float32x4,
898 ),
899 (
900 4,
901 field_offset!(BorderInstance, fwd_mat),
902 wgpu::VertexFormat::Float32x4,
903 ),
904 (
905 5,
906 field_offset!(BorderInstance, grad),
907 wgpu::VertexFormat::Float32x4,
908 ),
909 (
910 6,
911 field_offset!(BorderInstance, bits),
912 wgpu::VertexFormat::Uint32,
913 ),
914 (
915 7,
916 field_offset!(BorderInstance, stroke),
917 wgpu::VertexFormat::Float32,
918 ),
919 ]);
920 let ellipse_attrs = vattrs(&[
921 (
922 0,
923 field_offset!(EllipseInstance, xywh),
924 wgpu::VertexFormat::Float32x4,
925 ),
926 (
927 1,
928 field_offset!(EllipseInstance, color0),
929 wgpu::VertexFormat::Float32x4,
930 ),
931 (
932 2,
933 field_offset!(EllipseInstance, color1),
934 wgpu::VertexFormat::Float32x4,
935 ),
936 (
937 3,
938 field_offset!(EllipseInstance, fwd_mat),
939 wgpu::VertexFormat::Float32x4,
940 ),
941 (
942 4,
943 field_offset!(EllipseInstance, grad),
944 wgpu::VertexFormat::Float32x4,
945 ),
946 (
947 5,
948 field_offset!(EllipseInstance, bits),
949 wgpu::VertexFormat::Uint32,
950 ),
951 ]);
952 let ellipse_border_attrs = vattrs(&[
953 (
954 0,
955 field_offset!(EllipseBorderInstance, xywh),
956 wgpu::VertexFormat::Float32x4,
957 ),
958 (
959 1,
960 field_offset!(EllipseBorderInstance, color0),
961 wgpu::VertexFormat::Float32x4,
962 ),
963 (
964 2,
965 field_offset!(EllipseBorderInstance, color1),
966 wgpu::VertexFormat::Float32x4,
967 ),
968 (
969 3,
970 field_offset!(EllipseBorderInstance, fwd_mat),
971 wgpu::VertexFormat::Float32x4,
972 ),
973 (
974 4,
975 field_offset!(EllipseBorderInstance, grad),
976 wgpu::VertexFormat::Float32x4,
977 ),
978 (
979 5,
980 field_offset!(EllipseBorderInstance, bits),
981 wgpu::VertexFormat::Uint32,
982 ),
983 (
984 6,
985 field_offset!(EllipseBorderInstance, stroke),
986 wgpu::VertexFormat::Float32,
987 ),
988 (
989 7,
990 field_offset!(EllipseBorderInstance, pad),
991 wgpu::VertexFormat::Float32,
992 ),
993 ]);
994
995 make_content_pipeline!(rects, "rect", RectInstance, &rect_attrs);
996 make_content_pipeline!(borders, "border", BorderInstance, &border_attrs);
997 make_content_pipeline!(ellipses, "ellipse", EllipseInstance, &ellipse_attrs);
998 make_content_pipeline!(
999 ellipse_borders,
1000 "ellipse_border",
1001 EllipseBorderInstance,
1002 &ellipse_border_attrs
1003 );
1004
1005 let arc_attrs = vattrs(&[
1006 (
1007 0,
1008 field_offset!(ArcInstance, xywh),
1009 wgpu::VertexFormat::Float32x4,
1010 ),
1011 (
1012 1,
1013 field_offset!(ArcInstance, color0),
1014 wgpu::VertexFormat::Float32x4,
1015 ),
1016 (
1017 2,
1018 field_offset!(ArcInstance, color1),
1019 wgpu::VertexFormat::Float32x4,
1020 ),
1021 (
1022 3,
1023 field_offset!(ArcInstance, fwd_mat),
1024 wgpu::VertexFormat::Float32x4,
1025 ),
1026 (
1027 4,
1028 field_offset!(ArcInstance, grad),
1029 wgpu::VertexFormat::Float32x4,
1030 ),
1031 (
1032 5,
1033 field_offset!(ArcInstance, bits),
1034 wgpu::VertexFormat::Uint32,
1035 ),
1036 (
1037 6,
1038 field_offset!(ArcInstance, start_angle),
1039 wgpu::VertexFormat::Float32,
1040 ),
1041 (
1042 7,
1043 field_offset!(ArcInstance, sweep_angle),
1044 wgpu::VertexFormat::Float32,
1045 ),
1046 (
1047 8,
1048 field_offset!(ArcInstance, stroke),
1049 wgpu::VertexFormat::Float32,
1050 ),
1051 (
1052 9,
1053 field_offset!(ArcInstance, pad),
1054 wgpu::VertexFormat::Float32,
1055 ),
1056 ]);
1057
1058 make_content_pipeline!(arcs, "arc", ArcInstance, &arc_attrs);
1059
1060 let text_mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1062 label: Some("text.wgsl"),
1063 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/text.wgsl"))),
1064 });
1065 let text_color_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1067 label: Some("text_color.wgsl"),
1068 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1069 "shaders/text_color.wgsl"
1070 ))),
1071 });
1072 let text_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1073 label: Some("text pipeline layout"),
1074 bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
1075 immediate_size: 0,
1076 });
1077 let glyph_vertex = wgpu::VertexBufferLayout {
1078 array_stride: std::mem::size_of::<GlyphInstance>() as u64,
1079 step_mode: wgpu::VertexStepMode::Instance,
1080 attributes: &[
1081 wgpu::VertexAttribute {
1082 shader_location: 0,
1083 offset: 0,
1084 format: wgpu::VertexFormat::Float32x4,
1085 },
1086 wgpu::VertexAttribute {
1087 shader_location: 1,
1088 offset: 16,
1089 format: wgpu::VertexFormat::Float32x4,
1090 },
1091 wgpu::VertexAttribute {
1092 shader_location: 2,
1093 offset: 32,
1094 format: wgpu::VertexFormat::Float32x4,
1095 },
1096 wgpu::VertexAttribute {
1097 shader_location: 3,
1098 offset: 48,
1099 format: wgpu::VertexFormat::Float32x4,
1100 },
1101 ],
1102 };
1103 let text_mask = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1104 label: Some("text pipeline (mask)"),
1105 layout: Some(&text_pipeline_layout),
1106 vertex: wgpu::VertexState {
1107 module: &text_mask_shader,
1108 entry_point: Some("vs_main"),
1109 buffers: &[Some(glyph_vertex.clone())],
1110 compilation_options: wgpu::PipelineCompilationOptions::default(),
1111 },
1112 fragment: Some(wgpu::FragmentState {
1113 module: &text_mask_shader,
1114 entry_point: Some("fs_main"),
1115 targets: &[Some(wgpu::ColorTargetState {
1116 format,
1117 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1118 write_mask: wgpu::ColorWrites::ALL,
1119 })],
1120 compilation_options: wgpu::PipelineCompilationOptions::default(),
1121 }),
1122 primitive: wgpu::PrimitiveState::default(),
1123 depth_stencil: Some(stencil_for_content.clone()),
1124 multisample: msaa_state,
1125 multiview_mask: None,
1126 cache: None,
1127 });
1128 let text_color_pipeline =
1129 |label: &'static str, blend: wgpu::BlendState| -> wgpu::RenderPipeline {
1130 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1131 label: Some(label),
1132 layout: Some(&text_pipeline_layout),
1133 vertex: wgpu::VertexState {
1134 module: &text_color_shader,
1135 entry_point: Some("vs_main"),
1136 buffers: &[Some(glyph_vertex.clone())],
1137 compilation_options: wgpu::PipelineCompilationOptions::default(),
1138 },
1139 fragment: Some(wgpu::FragmentState {
1140 module: &text_color_shader,
1141 entry_point: Some("fs_main"),
1142 targets: &[Some(wgpu::ColorTargetState {
1143 format,
1144 blend: Some(blend),
1145 write_mask: wgpu::ColorWrites::ALL,
1146 })],
1147 compilation_options: wgpu::PipelineCompilationOptions::default(),
1148 }),
1149 primitive: wgpu::PrimitiveState::default(),
1150 depth_stencil: Some(stencil_for_content.clone()),
1151 multisample: msaa_state,
1152 multiview_mask: None,
1153 cache: None,
1154 })
1155 };
1156 let text_color = text_color_pipeline(
1157 "text pipeline (color)",
1158 wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
1159 );
1160 let image_rgba = text_color.clone();
1162 let image_blends: [wgpu::RenderPipeline; FIXED_FUNCTION_BLENDS.len()] =
1165 std::array::from_fn(|index| {
1166 let blend = fixed_function_blend(FIXED_FUNCTION_BLENDS[index])
1167 .unwrap_or(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING);
1168 text_color_pipeline("image pipeline (blend)", blend)
1169 });
1170
1171 let coverage_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1175 label: Some("coverage.wgsl"),
1176 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/coverage.wgsl"))),
1177 });
1178 let coverage = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1179 label: Some("coverage pipeline (tinted a8)"),
1180 layout: Some(&text_pipeline_layout),
1181 vertex: wgpu::VertexState {
1182 module: &coverage_shader,
1183 entry_point: Some("vs_main"),
1184 buffers: &[Some(glyph_vertex.clone())],
1185 compilation_options: wgpu::PipelineCompilationOptions::default(),
1186 },
1187 fragment: Some(wgpu::FragmentState {
1188 module: &coverage_shader,
1189 entry_point: Some("fs_main"),
1190 targets: &[Some(wgpu::ColorTargetState {
1191 format,
1192 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1193 write_mask: wgpu::ColorWrites::ALL,
1194 })],
1195 compilation_options: wgpu::PipelineCompilationOptions::default(),
1196 }),
1197 primitive: wgpu::PrimitiveState::default(),
1198 depth_stencil: Some(stencil_for_content.clone()),
1199 multisample: msaa_state,
1200 multiview_mask: None,
1201 cache: None,
1202 });
1203
1204 let blur_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1205 label: Some("blur_separable.wgsl"),
1206 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1207 "shaders/blur_separable.wgsl"
1208 ))),
1209 });
1210 let blur_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1211 label: Some("blur pipeline layout"),
1212 bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
1213 immediate_size: 0,
1214 });
1215 let blur_layout = wgpu::VertexBufferLayout {
1216 array_stride: std::mem::size_of::<BlurInstance>() as u64,
1217 step_mode: wgpu::VertexStepMode::Instance,
1218 attributes: &[
1219 wgpu::VertexAttribute {
1220 shader_location: 0,
1221 offset: 0,
1222 format: wgpu::VertexFormat::Float32x4,
1223 },
1224 wgpu::VertexAttribute {
1225 shader_location: 1,
1226 offset: 16,
1227 format: wgpu::VertexFormat::Float32x4,
1228 },
1229 wgpu::VertexAttribute {
1230 shader_location: 2,
1231 offset: 32,
1232 format: wgpu::VertexFormat::Float32x4,
1233 },
1234 wgpu::VertexAttribute {
1235 shader_location: 3,
1236 offset: 48,
1237 format: wgpu::VertexFormat::Float32x2,
1238 },
1239 wgpu::VertexAttribute {
1240 shader_location: 4,
1241 offset: 56,
1242 format: wgpu::VertexFormat::Float32x4,
1243 },
1244 wgpu::VertexAttribute {
1245 shader_location: 5,
1246 offset: 72,
1247 format: wgpu::VertexFormat::Uint32,
1248 },
1249 wgpu::VertexAttribute {
1250 shader_location: 6,
1251 offset: 76,
1252 format: wgpu::VertexFormat::Uint32,
1253 },
1254 wgpu::VertexAttribute {
1255 shader_location: 7,
1256 offset: 80,
1257 format: wgpu::VertexFormat::Float32x4,
1258 },
1259 ],
1260 };
1261 let blur = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1262 label: Some("blur pipeline"),
1263 layout: Some(&blur_pipeline_layout),
1264 vertex: wgpu::VertexState {
1265 module: &blur_shader,
1266 entry_point: Some("vs_main"),
1267 buffers: &[Some(blur_layout.clone())],
1268 compilation_options: wgpu::PipelineCompilationOptions::default(),
1269 },
1270 fragment: Some(wgpu::FragmentState {
1271 module: &blur_shader,
1272 entry_point: Some("fs_alpha"),
1273 targets: &[Some(wgpu::ColorTargetState {
1274 format,
1275 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1276 write_mask: wgpu::ColorWrites::ALL,
1277 })],
1278 compilation_options: wgpu::PipelineCompilationOptions::default(),
1279 }),
1280 primitive: wgpu::PrimitiveState::default(),
1281 depth_stencil: Some(stencil_for_content.clone()),
1282 multisample: msaa_state,
1283 multiview_mask: None,
1284 cache: None,
1285 });
1286
1287 let blur_content = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1288 label: Some("blur content pipeline"),
1289 layout: Some(&blur_pipeline_layout),
1290 vertex: wgpu::VertexState {
1291 module: &blur_shader,
1292 entry_point: Some("vs_main"),
1293 buffers: &[Some(blur_layout)],
1294 compilation_options: wgpu::PipelineCompilationOptions::default(),
1295 },
1296 fragment: Some(wgpu::FragmentState {
1297 module: &blur_shader,
1298 entry_point: Some("fs_color"),
1299 targets: &[Some(wgpu::ColorTargetState {
1300 format,
1301 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1302 write_mask: wgpu::ColorWrites::ALL,
1303 })],
1304 compilation_options: wgpu::PipelineCompilationOptions::default(),
1305 }),
1306 primitive: wgpu::PrimitiveState::default(),
1307 depth_stencil: Some(stencil_for_content.clone()),
1308 multisample: msaa_state,
1309 multiview_mask: None,
1310 cache: None,
1311 });
1312
1313 let image_nv12_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1315 label: Some("image_nv12.wgsl"),
1316 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1317 "shaders/image_nv12.wgsl"
1318 ))),
1319 });
1320 let image_nv12_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1321 label: Some("image nv12 pipeline layout"),
1322 bind_group_layouts: &[Some(globals_layout), Some(image_bind_layout_nv12)],
1323 immediate_size: 0,
1324 });
1325 let nv12_vertex_layout = wgpu::VertexBufferLayout {
1326 array_stride: std::mem::size_of::<Nv12Instance>() as u64,
1327 step_mode: wgpu::VertexStepMode::Instance,
1328 attributes: &[
1329 wgpu::VertexAttribute {
1330 shader_location: 0,
1331 offset: 0,
1332 format: wgpu::VertexFormat::Float32x4,
1333 },
1334 wgpu::VertexAttribute {
1335 shader_location: 1,
1336 offset: 16,
1337 format: wgpu::VertexFormat::Float32x4,
1338 },
1339 wgpu::VertexAttribute {
1340 shader_location: 2,
1341 offset: 32,
1342 format: wgpu::VertexFormat::Float32x4,
1343 },
1344 wgpu::VertexAttribute {
1345 shader_location: 3,
1346 offset: 48,
1347 format: wgpu::VertexFormat::Float32,
1348 },
1349 wgpu::VertexAttribute {
1350 shader_location: 4,
1351 offset: 52,
1352 format: wgpu::VertexFormat::Float32,
1353 },
1354 wgpu::VertexAttribute {
1355 shader_location: 5,
1356 offset: 56,
1357 format: wgpu::VertexFormat::Float32x4,
1358 },
1359 ],
1360 };
1361 let nv12_pipeline =
1362 |label: &'static str, blend: wgpu::BlendState| -> wgpu::RenderPipeline {
1363 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1364 label: Some(label),
1365 layout: Some(&image_nv12_layout),
1366 vertex: wgpu::VertexState {
1367 module: &image_nv12_shader,
1368 entry_point: Some("vs_main"),
1369 buffers: &[Some(nv12_vertex_layout.clone())],
1370 compilation_options: wgpu::PipelineCompilationOptions::default(),
1371 },
1372 fragment: Some(wgpu::FragmentState {
1373 module: &image_nv12_shader,
1374 entry_point: Some("fs_main"),
1375 targets: &[Some(wgpu::ColorTargetState {
1376 format,
1377 blend: Some(blend),
1378 write_mask: wgpu::ColorWrites::ALL,
1379 })],
1380 compilation_options: wgpu::PipelineCompilationOptions::default(),
1381 }),
1382 primitive: wgpu::PrimitiveState::default(),
1383 depth_stencil: Some(stencil_for_content.clone()),
1384 multisample: msaa_state,
1385 multiview_mask: None,
1386 cache: None,
1387 })
1388 };
1389 let image_nv12 = nv12_pipeline(
1390 "image nv12 pipeline",
1391 wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
1392 );
1393 let image_nv12_blends: [wgpu::RenderPipeline; FIXED_FUNCTION_BLENDS.len()] =
1394 std::array::from_fn(|index| {
1395 let blend = fixed_function_blend(FIXED_FUNCTION_BLENDS[index])
1396 .unwrap_or(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING);
1397 nv12_pipeline("image nv12 pipeline (blend)", blend)
1398 });
1399
1400 let clip_color_target = wgpu::ColorTargetState {
1401 format,
1402 blend: None,
1403 write_mask: wgpu::ColorWrites::empty(),
1404 };
1405
1406 let clip_shader_bin = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1408 label: Some("clip_round_rect_bin.wgsl"),
1409 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1410 "shaders/clip_round_rect_bin.wgsl"
1411 ))),
1412 });
1413 let clip_bin = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1414 label: Some("clip pipeline (bin)"),
1415 layout: Some(clip_pipeline_layout),
1416 vertex: wgpu::VertexState {
1417 module: &clip_shader_bin,
1418 entry_point: Some("vs_main"),
1419 buffers: &[Some(clip_vertex_layout.clone())],
1420 compilation_options: wgpu::PipelineCompilationOptions::default(),
1421 },
1422 fragment: Some(wgpu::FragmentState {
1423 module: &clip_shader_bin,
1424 entry_point: Some("fs_main"),
1425 targets: &[Some(clip_color_target.clone())],
1426 compilation_options: wgpu::PipelineCompilationOptions::default(),
1427 }),
1428 primitive: wgpu::PrimitiveState::default(),
1429 depth_stencil: Some(stencil_for_clip_inc.clone()),
1430 multisample: wgpu::MultisampleState {
1431 count: sample_count,
1432 mask: !0,
1433 alpha_to_coverage_enabled: false,
1434 },
1435 multiview_mask: None,
1436 cache: None,
1437 });
1438 let clip_dec = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1439 label: Some("clip pipeline (dec)"),
1440 layout: Some(clip_pipeline_layout),
1441 vertex: wgpu::VertexState {
1442 module: &clip_shader_bin,
1443 entry_point: Some("vs_main"),
1444 buffers: &[Some(clip_vertex_layout.clone())],
1445 compilation_options: wgpu::PipelineCompilationOptions::default(),
1446 },
1447 fragment: Some(wgpu::FragmentState {
1448 module: &clip_shader_bin,
1449 entry_point: Some("fs_main"),
1450 targets: &[Some(clip_color_target.clone())],
1451 compilation_options: wgpu::PipelineCompilationOptions::default(),
1452 }),
1453 primitive: wgpu::PrimitiveState::default(),
1454 depth_stencil: Some(stencil_for_clip_dec.clone()),
1455 multisample: wgpu::MultisampleState {
1456 count: sample_count,
1457 mask: !0,
1458 alpha_to_coverage_enabled: false,
1459 },
1460 multiview_mask: None,
1461 cache: None,
1462 });
1463
1464 let slug = Some(slug::create_pipeline(
1465 device,
1466 format,
1467 sample_count,
1468 stencil_for_content,
1469 ));
1470
1471 let mesh_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1473 label: Some("mesh.wgsl"),
1474 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/mesh.wgsl"))),
1475 });
1476 let mesh_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1477 label: Some("mesh pipeline layout"),
1478 bind_group_layouts: &[Some(globals_layout), Some(mesh_bind_layout)],
1479 immediate_size: 0,
1480 });
1481 let mesh_vertex_layout = wgpu::VertexBufferLayout {
1482 array_stride: std::mem::size_of::<MeshVertex>() as u64,
1483 step_mode: wgpu::VertexStepMode::Vertex,
1484 attributes: &[
1485 wgpu::VertexAttribute {
1486 shader_location: 0,
1487 offset: 0,
1488 format: wgpu::VertexFormat::Float32x2,
1489 },
1490 wgpu::VertexAttribute {
1491 shader_location: 1,
1492 offset: 8,
1493 format: wgpu::VertexFormat::Float32x4,
1494 },
1495 wgpu::VertexAttribute {
1496 shader_location: 2,
1497 offset: 24,
1498 format: wgpu::VertexFormat::Float32x2,
1499 },
1500 ],
1501 };
1502 let make_mesh_pipeline =
1503 |label: &str, depth: &wgpu::DepthStencilState, color: &wgpu::ColorTargetState| {
1504 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1505 label: Some(label),
1506 layout: Some(&mesh_pipeline_layout),
1507 vertex: wgpu::VertexState {
1508 module: &mesh_shader,
1509 entry_point: Some("vs_main"),
1510 buffers: &[Some(mesh_vertex_layout.clone())],
1511 compilation_options: wgpu::PipelineCompilationOptions::default(),
1512 },
1513 fragment: Some(wgpu::FragmentState {
1514 module: &mesh_shader,
1515 entry_point: Some("fs_main"),
1516 targets: &[Some(color.clone())],
1517 compilation_options: wgpu::PipelineCompilationOptions::default(),
1518 }),
1519 primitive: wgpu::PrimitiveState {
1520 topology: wgpu::PrimitiveTopology::TriangleList,
1521 ..Default::default()
1522 },
1523 depth_stencil: Some(depth.clone()),
1524 multisample: msaa_state,
1525 multiview_mask: None,
1526 cache: None,
1527 })
1528 };
1529 let mesh_color_target = wgpu::ColorTargetState {
1530 format,
1531 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1532 write_mask: wgpu::ColorWrites::ALL,
1533 };
1534 let mut stencil_for_mesh = stencil_for_content.clone();
1535 stencil_for_mesh.stencil.front.compare = wgpu::CompareFunction::Equal;
1536 stencil_for_mesh.stencil.back.compare = wgpu::CompareFunction::Equal;
1537 let mut stencil_for_overlay = stencil_for_content.clone();
1538 stencil_for_overlay.stencil.front.compare = wgpu::CompareFunction::LessEqual;
1539 stencil_for_overlay.stencil.back.compare = wgpu::CompareFunction::LessEqual;
1540 let mesh = make_mesh_pipeline("mesh pipeline", &stencil_for_mesh, &mesh_color_target);
1541 let mesh_overlay = make_mesh_pipeline(
1542 "mesh overlay pipeline",
1543 &stencil_for_overlay,
1544 &mesh_color_target,
1545 );
1546 let mesh_clip_inc = make_mesh_pipeline(
1547 "mesh clip (inc) pipeline",
1548 stencil_for_clip_inc,
1549 &clip_color_target,
1550 );
1551 let mesh_clip_dec = make_mesh_pipeline(
1552 "mesh clip (dec) pipeline",
1553 stencil_for_clip_dec,
1554 &clip_color_target,
1555 );
1556 let mesh_blend_target = |blend: wgpu::BlendState| wgpu::ColorTargetState {
1557 format,
1558 blend: Some(blend),
1559 write_mask: wgpu::ColorWrites::ALL,
1560 };
1561 let mesh_blends: [wgpu::RenderPipeline; FIXED_FUNCTION_BLENDS.len()] =
1562 std::array::from_fn(|index| {
1563 let mode = FIXED_FUNCTION_BLENDS[index];
1564 let blend = fixed_function_blend(mode).unwrap_or(wgpu::BlendState::REPLACE);
1565 make_mesh_pipeline(
1566 match mode {
1567 repose_core::BlendMode::Add => "mesh pipeline (add)",
1568 repose_core::BlendMode::Multiply => "mesh pipeline (multiply)",
1569 repose_core::BlendMode::Screen => "mesh pipeline (screen)",
1570 repose_core::BlendMode::Darken => "mesh pipeline (darken)",
1571 _ => "mesh pipeline (lighten)",
1572 },
1573 &stencil_for_mesh,
1574 &mesh_blend_target(blend),
1575 )
1576 });
1577 let projective_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1582 label: Some("projective_layer.wgsl"),
1583 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1584 "shaders/projective_layer.wgsl"
1585 ))),
1586 });
1587 let projective_pipeline_layout =
1588 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1589 label: Some("projective layer pipeline layout"),
1590 bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
1591 immediate_size: 0,
1592 });
1593 let projective_vertex_layout = wgpu::VertexBufferLayout {
1594 array_stride: std::mem::size_of::<ProjectiveInstance>() as u64,
1595 step_mode: wgpu::VertexStepMode::Instance,
1596 attributes: &[
1597 wgpu::VertexAttribute {
1598 shader_location: 0,
1599 offset: 0,
1600 format: wgpu::VertexFormat::Float32x2,
1601 },
1602 wgpu::VertexAttribute {
1603 shader_location: 1,
1604 offset: 8,
1605 format: wgpu::VertexFormat::Float32x2,
1606 },
1607 wgpu::VertexAttribute {
1608 shader_location: 2,
1609 offset: 16,
1610 format: wgpu::VertexFormat::Float32x2,
1611 },
1612 wgpu::VertexAttribute {
1613 shader_location: 3,
1614 offset: 24,
1615 format: wgpu::VertexFormat::Float32x2,
1616 },
1617 wgpu::VertexAttribute {
1618 shader_location: 4,
1619 offset: 32,
1620 format: wgpu::VertexFormat::Float32x4,
1621 },
1622 wgpu::VertexAttribute {
1623 shader_location: 5,
1624 offset: 48,
1625 format: wgpu::VertexFormat::Float32x4,
1626 },
1627 wgpu::VertexAttribute {
1628 shader_location: 6,
1629 offset: 64,
1630 format: wgpu::VertexFormat::Float32,
1631 },
1632 ],
1633 };
1634 let projective_layer = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1635 label: Some("projective layer composite pipeline"),
1636 layout: Some(&projective_pipeline_layout),
1637 vertex: wgpu::VertexState {
1638 module: &projective_shader,
1639 entry_point: Some("vs_main"),
1640 buffers: &[Some(projective_vertex_layout)],
1641 compilation_options: wgpu::PipelineCompilationOptions::default(),
1642 },
1643 fragment: Some(wgpu::FragmentState {
1644 module: &projective_shader,
1645 entry_point: Some("fs_main"),
1646 targets: &[Some(wgpu::ColorTargetState {
1647 format,
1648 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1649 write_mask: wgpu::ColorWrites::ALL,
1650 })],
1651 compilation_options: wgpu::PipelineCompilationOptions::default(),
1652 }),
1653 primitive: wgpu::PrimitiveState::default(),
1654 depth_stencil: Some(stencil_for_content.clone()),
1655 multisample: msaa_state,
1656 multiview_mask: None,
1657 cache: None,
1658 });
1659
1660 let blend_layer_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1661 label: Some("blend_layer.wgsl"),
1662 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
1663 "shaders/blend_layer.wgsl"
1664 ))),
1665 });
1666 let blend_layer_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1667 label: Some("blend layer pipeline layout"),
1668 bind_group_layouts: &[
1669 Some(globals_layout),
1670 Some(text_bind_layout),
1671 Some(text_bind_layout),
1672 ],
1673 immediate_size: 0,
1674 });
1675 let blend_vertex_layout = wgpu::VertexBufferLayout {
1676 array_stride: std::mem::size_of::<BlendInstance>() as u64,
1677 step_mode: wgpu::VertexStepMode::Instance,
1678 attributes: &[
1679 wgpu::VertexAttribute {
1680 shader_location: 0,
1681 offset: 0,
1682 format: wgpu::VertexFormat::Float32x4,
1683 },
1684 wgpu::VertexAttribute {
1685 shader_location: 1,
1686 offset: 16,
1687 format: wgpu::VertexFormat::Float32x4,
1688 },
1689 wgpu::VertexAttribute {
1690 shader_location: 2,
1691 offset: 32,
1692 format: wgpu::VertexFormat::Float32x4,
1693 },
1694 wgpu::VertexAttribute {
1695 shader_location: 3,
1696 offset: 48,
1697 format: wgpu::VertexFormat::Float32x4,
1698 },
1699 wgpu::VertexAttribute {
1700 shader_location: 4,
1701 offset: 64,
1702 format: wgpu::VertexFormat::Uint32,
1703 },
1704 ],
1705 };
1706 let blend_layer = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1707 label: Some("blend layer pipeline"),
1708 layout: Some(&blend_layer_layout),
1709 vertex: wgpu::VertexState {
1710 module: &blend_layer_shader,
1711 entry_point: Some("vs_main"),
1712 buffers: &[Some(blend_vertex_layout)],
1713 compilation_options: wgpu::PipelineCompilationOptions::default(),
1714 },
1715 fragment: Some(wgpu::FragmentState {
1716 module: &blend_layer_shader,
1717 entry_point: Some("fs_main"),
1718 targets: &[Some(wgpu::ColorTargetState {
1719 format,
1720 blend: None,
1721 write_mask: wgpu::ColorWrites::ALL,
1722 })],
1723 compilation_options: wgpu::PipelineCompilationOptions::default(),
1724 }),
1725 primitive: wgpu::PrimitiveState::default(),
1726 depth_stencil: Some(stencil_for_content.clone()),
1727 multisample: msaa_state,
1728 multiview_mask: None,
1729 cache: None,
1730 });
1731
1732 Self {
1733 rects,
1734 borders,
1735 ellipses,
1736 ellipse_borders,
1737 arcs,
1738 text_mask,
1739 text_color,
1740 image_rgba,
1741 image_blends,
1742 image_nv12,
1743 image_nv12_blends,
1744 coverage,
1745 blur,
1746 blur_content,
1747 clip_bin,
1748 clip_dec,
1749 slug,
1750 mesh,
1751 mesh_blends,
1752 blend_layer,
1753 mesh_overlay,
1754 mesh_clip_inc,
1755 mesh_clip_dec,
1756 projective_layer,
1757 }
1758 }
1759}
1760
1761fn affine_aabb(transform: &Transform, rect: &repose_core::Rect) -> repose_core::Rect {
1764 let m = transform.linear();
1765 let (tx, ty) = (transform.translate_x, transform.translate_y);
1766 let corners = [
1767 (rect.x, rect.y),
1768 (rect.x + rect.w, rect.y),
1769 (rect.x, rect.y + rect.h),
1770 (rect.x + rect.w, rect.y + rect.h),
1771 ];
1772 let mut min_x = f32::INFINITY;
1773 let mut min_y = f32::INFINITY;
1774 let mut max_x = f32::NEG_INFINITY;
1775 let mut max_y = f32::NEG_INFINITY;
1776 for (x, y) in corners {
1777 let wx = m[0] * x + m[1] * y + tx;
1778 let wy = m[2] * x + m[3] * y + ty;
1779 min_x = min_x.min(wx);
1780 min_y = min_y.min(wy);
1781 max_x = max_x.max(wx);
1782 max_y = max_y.max(wy);
1783 }
1784 repose_core::Rect {
1785 x: min_x,
1786 y: min_y,
1787 w: (max_x - min_x).max(0.0),
1788 h: (max_y - min_y).max(0.0),
1789 }
1790}
1791
1792fn rect_to_instance_ndc(
1797 rect: repose_core::Rect,
1798 transform: &Transform,
1799 fb_w: f32,
1800 fb_h: f32,
1801) -> ([f32; 4], [f32; 4]) {
1802 let cx = rect.x + rect.w * 0.5;
1803 let cy = rect.y + rect.h * 0.5;
1804
1805 let m = transform.linear();
1806 let tx = m[0] * cx + m[1] * cy + transform.translate_x;
1807 let ty = m[2] * cx + m[3] * cy + transform.translate_y;
1808
1809 let ndc_cx = (tx / fb_w) * 2.0 - 1.0;
1810 let ndc_cy = 1.0 - (ty / fb_h) * 2.0;
1811 let ndc_w = (rect.w * transform.scale_x / fb_w) * 2.0;
1813 let ndc_h = (rect.h * transform.scale_y / fb_h) * 2.0;
1814
1815 ([ndc_cx, ndc_cy, ndc_w, ndc_h], forward_rs_mat(transform))
1816}
1817
1818fn forward_rs_mat(transform: &Transform) -> [f32; 4] {
1822 let c = transform.rotate.cos();
1823 let s = transform.rotate.sin();
1824 let (hx, hy) = (transform.shear_x, transform.shear_y);
1825 let m = [c - s * hy, c * hx - s, s + c * hy, s * hx + c];
1826 if (m[0] * m[3] - m[1] * m[2]).abs() < 1e-6 {
1827 return [1.0, 0.0, 0.0, 1.0];
1828 }
1829 m
1830}
1831
1832struct Pass {
1834 id: u64,
1835 target: PassTarget,
1836 initial_scissor: (u32, u32, u32, u32),
1838 active_scissor: Option<(u32, u32, u32, u32)>,
1841 clear_color: Option<[f32; 4]>,
1844 cmds: Vec<Cmd>,
1845}
1846
1847struct LayerState {
1848 layer_id: u32,
1849 parent_target: PassTarget,
1850 parent_scissors: Vec<repose_core::Rect>,
1851 parent_root_clip: repose_core::Rect,
1852 parent_size: (f32, f32),
1853 parent_transform: Transform,
1854 parent_scissor: Option<(u32, u32, u32, u32)>,
1855 parent_clips: Vec<ActiveClip>,
1856 alpha: f32,
1857 blur: (f32, f32),
1858 rectangle_edge: bool,
1859}
1860
1861struct FlattenRecord {
1865 stack_len: usize,
1868 layer_id: u32,
1869 map: [f32; 9],
1872 layer_rect: repose_core::Rect,
1874 saved_scissor_stack: Vec<repose_core::Rect>,
1875 saved_root: repose_core::Rect,
1876 saved_size: (f32, f32),
1877 saved_active_scissor: Option<(u32, u32, u32, u32)>,
1878 saved_clips: Vec<ActiveClip>,
1879}
1880
1881const FLATTEN_ID_BASE: u32 = 0xF000_0000;
1885const MAX_GRAPHICS_LAYERS: usize = 128;
1886const MAX_GRAPHICS_LAYER_BYTES: u64 = 256 * 1024 * 1024;
1887const MAX_TRANSIENT_GRAPHICS_LAYERS: usize = 64;
1888const MAX_TRANSIENT_GRAPHICS_LAYER_BYTES: u64 = 128 * 1024 * 1024;
1889const STENCIL_BASE: u32 = 128;
1890const STENCIL_MAX_DEPTH: u32 = 127;
1891
1892#[allow(non_snake_case)]
1893enum Cmd {
1894 ClipPush {
1895 off: u64,
1896 cnt: u32,
1897 scissor: (u32, u32, u32, u32),
1898 difference: bool,
1899 applied: bool,
1900 },
1901 ClipPop {
1902 off: u64,
1903 cnt: u32,
1904 scissor: (u32, u32, u32, u32),
1906 stencil_scissor: (u32, u32, u32, u32),
1909 difference: bool,
1910 applied: bool,
1911 },
1912 Rect {
1913 off: u64,
1914 cnt: u32,
1915 },
1916 Border {
1917 off: u64,
1918 cnt: u32,
1919 },
1920 Ellipse {
1921 off: u64,
1922 cnt: u32,
1923 },
1924 EllipseBorder {
1925 off: u64,
1926 cnt: u32,
1927 },
1928 Arc {
1929 off: u64,
1930 cnt: u32,
1931 },
1932 GlyphsMask {
1933 off: u64,
1934 cnt: u32,
1935 },
1936 GlyphsColor {
1937 off: u64,
1938 cnt: u32,
1939 },
1940 GlyphsVector {
1941 off: u64,
1942 cnt: u32,
1943 },
1944 ImageRgba {
1945 off: u64,
1946 cnt: u32,
1947 handle: u64,
1948 filter: ImageFilter,
1949 tile: bool,
1951 blend: repose_core::BlendMode,
1953 },
1954 Coverage {
1958 off: u64,
1959 cnt: u32,
1960 handle: u64,
1961 },
1962 ImageNv12 {
1963 off: u64,
1964 cnt: u32,
1965 handle: u64,
1966 filter: ImageFilter,
1967 blend: repose_core::BlendMode,
1969 },
1970 CompositeLayer {
1974 off: u64,
1975 cnt: u32,
1976 layer_id: u32,
1977 },
1978 CompositeShadow {
1984 off: u64,
1985 cnt: u32,
1986 layer_id: u32,
1987 },
1988 CompositeBlur {
1991 off: u64,
1992 cnt: u32,
1993 layer_id: u32,
1994 },
1995 CompositeProjective {
2000 off: u64,
2001 cnt: u32,
2002 layer_id: u32,
2003 },
2004 VectorMesh {
2006 mesh: MeshGpu,
2007 uoff: u64,
2008 blend: repose_core::BlendMode,
2009 },
2010 BlendLayer {
2017 off: u64,
2018 cnt: u32,
2019 src_layer: u32,
2020 dst_layer: Option<u32>,
2021 parent: PassTarget,
2022 scissor: (u32, u32, u32, u32),
2023 },
2024 VectorOverlay {
2026 mesh: MeshGpu,
2027 uoff: u64,
2028 },
2029 VectorClipPush {
2034 mesh: MeshGpu,
2035 uoff: u64,
2036 scissor: (u32, u32, u32, u32),
2037 difference: bool,
2038 applied: bool,
2039 },
2040 VectorClipPop {
2042 mesh: MeshGpu,
2043 uoff: u64,
2044 scissor: (u32, u32, u32, u32),
2045 stencil_scissor: (u32, u32, u32, u32),
2046 difference: bool,
2047 applied: bool,
2048 },
2049 Callback {
2050 rect: repose_core::Rect,
2051 clip_rect: repose_core::Rect,
2052 scissor: (u32, u32, u32, u32),
2053 restore_scissor: (u32, u32, u32, u32),
2054 callback_id: usize,
2055 payload: repose_core::PaintCallbackPayload,
2056 },
2057}
2058
2059struct CoverageTex {
2064 #[allow(dead_code)]
2066 tex: wgpu::Texture,
2067 bind: wgpu::BindGroup,
2068 w: u32,
2069 h: u32,
2070 last_used_frame: u64,
2071 bytes: u64,
2072}
2073
2074struct ImageBinds {
2075 linear: wgpu::BindGroup,
2076 nearest: wgpu::BindGroup,
2077 tile_linear: wgpu::BindGroup,
2079 tile_nearest: wgpu::BindGroup,
2080}
2081
2082impl ImageBinds {
2083 fn get(&self, filter: ImageFilter, tile: bool) -> &wgpu::BindGroup {
2084 match (filter, tile) {
2085 (ImageFilter::Linear, false) => &self.linear,
2086 (ImageFilter::Nearest, false) => &self.nearest,
2087 (ImageFilter::Linear, true) => &self.tile_linear,
2088 (ImageFilter::Nearest, true) => &self.tile_nearest,
2089 }
2090 }
2091}
2092
2093enum ImageTex {
2094 Rgba {
2095 tex: wgpu::Texture,
2096 binds: ImageBinds,
2097 w: u32,
2098 h: u32,
2099 format: wgpu::TextureFormat,
2100 last_used_frame: u64,
2101 bytes: u64,
2102 },
2103 User {
2105 binds: ImageBinds,
2106 w: u32,
2107 h: u32,
2108 last_used_frame: u64,
2109 bytes: u64,
2110 },
2111 Nv12 {
2112 tex_y: wgpu::Texture,
2113 tex_uv: wgpu::Texture,
2114 binds: ImageBinds,
2115 yuv_buf: wgpu::Buffer,
2116 w: u32,
2117 h: u32,
2118 pixel_format: PixelFormat,
2119 fourcc: Option<u32>,
2120 color_info: ColorInfo,
2121 external: bool,
2122 last_used_frame: u64,
2123 bytes: u64,
2124 },
2125}
2126
2127impl ImageTex {
2128 fn evictable(&self) -> bool {
2129 match self {
2130 Self::Rgba { .. } => true,
2131 Self::User { .. } => false,
2132 Self::Nv12 { external, .. } => !*external,
2133 }
2134 }
2135}
2136
2137#[derive(Clone)]
2138struct RetainedImage {
2139 w: u32,
2140 h: u32,
2141 format: wgpu::TextureFormat,
2142 rgba: Vec<u8>,
2143 last_used_frame: u64,
2144}
2145
2146struct ResolvedImageSource {
2147 width: f32,
2148 height: f32,
2149 uv_min: [f32; 2],
2150 uv_max: [f32; 2],
2151}
2152
2153fn resolve_image_source(
2154 source_rect: Option<ImageSourceRect>,
2155 image_width: u32,
2156 image_height: u32,
2157) -> Option<ResolvedImageSource> {
2158 if image_width == 0 || image_height == 0 {
2159 return None;
2160 }
2161 let Some(source) = source_rect else {
2162 return Some(ResolvedImageSource {
2163 width: image_width as f32,
2164 height: image_height as f32,
2165 uv_min: [0.0, 0.0],
2166 uv_max: [1.0, 1.0],
2167 });
2168 };
2169 if source.is_empty() {
2170 return None;
2171 }
2172 let right = source.x.checked_add(source.width)?;
2173 let bottom = source.y.checked_add(source.height)?;
2174 if right > image_width || bottom > image_height {
2175 return None;
2176 }
2177 Some(ResolvedImageSource {
2178 width: source.width as f32,
2179 height: source.height as f32,
2180 uv_min: [
2181 source.x as f32 / image_width as f32,
2182 source.y as f32 / image_height as f32,
2183 ],
2184 uv_max: [
2185 right as f32 / image_width as f32,
2186 bottom as f32 / image_height as f32,
2187 ],
2188 })
2189}
2190
2191fn image_fit_geometry(
2200 rect: repose_core::Rect,
2201 fit: ImageFit,
2202 alignment: ImageAlignment,
2203 source: &ResolvedImageSource,
2204) -> Option<(repose_core::Rect, [f32; 4])> {
2205 let src_w = source.width;
2206 let src_h = source.height;
2207 let dst_w = rect.w.max(0.0);
2208 let dst_h = rect.h.max(0.0);
2209 if src_w <= 0.0 || src_h <= 0.0 || dst_w <= 0.0 || dst_h <= 0.0 {
2210 return None;
2211 }
2212
2213 let slack = |content: f32, extent: f32| match alignment {
2216 ImageAlignment::Center => (extent - content) * 0.5,
2217 ImageAlignment::Begin => 0.0,
2218 ImageAlignment::End => extent - content,
2219 };
2220
2221 let tiled = matches!(fit, ImageFit::Tile);
2224 let (scale_x, scale_y) = fit.scale_factor((src_w, src_h), (dst_w, dst_h));
2225 let drawn = if tiled {
2226 repose_core::Rect {
2227 x: rect.x,
2228 y: rect.y,
2229 w: dst_w,
2230 h: dst_h,
2231 }
2232 } else {
2233 let (w, h) = (src_w * scale_x, src_h * scale_y);
2234 repose_core::Rect {
2235 x: rect.x + slack(w, dst_w),
2236 y: rect.y + slack(h, dst_h),
2237 w,
2238 h,
2239 }
2240 };
2241
2242 let uv_min = source.uv_min;
2243 let uv_max = source.uv_max;
2244 let repeats = if tiled {
2245 [dst_w / src_w, dst_h / src_h]
2246 } else {
2247 [1.0, 1.0]
2248 };
2249 let mut uv = [
2250 uv_min[0],
2251 uv_max[1],
2252 uv_min[0] + (uv_max[0] - uv_min[0]) * repeats[0],
2253 uv_max[1] - (uv_max[1] - uv_min[1]) * repeats[1],
2254 ];
2255
2256 let clipped = intersect(drawn, rect);
2257 if clipped.w <= 0.0 || clipped.h <= 0.0 {
2258 return None;
2259 }
2260 let du = uv[2] - uv[0];
2261 let dv = uv[3] - uv[1];
2262 uv = [
2263 uv[0] + du * (clipped.x - drawn.x) / drawn.w,
2264 uv[1] + dv * (clipped.y - drawn.y) / drawn.h,
2265 uv[0] + du * (clipped.x + clipped.w - drawn.x) / drawn.w,
2266 uv[1] + dv * (clipped.y + clipped.h - drawn.y) / drawn.h,
2267 ];
2268 Some((clipped, uv))
2269}
2270
2271struct AtlasA8 {
2272 tex: wgpu::Texture,
2273 view: wgpu::TextureView,
2274 sampler: wgpu::Sampler,
2275 size: u32,
2276 next_x: u32,
2277 next_y: u32,
2278 row_h: u32,
2279 map: HashMap<repose_text::CacheKey, GlyphInfo>,
2280 used_area: u64,
2283}
2284
2285struct AtlasRGBA {
2286 tex: wgpu::Texture,
2287 view: wgpu::TextureView,
2288 sampler: wgpu::Sampler,
2289 size: u32,
2290 next_x: u32,
2291 next_y: u32,
2292 row_h: u32,
2293 map: HashMap<repose_text::CacheKey, GlyphInfo>,
2294 used_area: u64,
2295}
2296
2297#[derive(Default, Clone, Copy)]
2301struct AtlasPending {
2302 grow: bool,
2304 compact: bool,
2307}
2308
2309#[derive(Clone, Copy)]
2310struct GlyphInfo {
2311 u0: f32,
2312 v0: f32,
2313 u1: f32,
2314 v1: f32,
2315 w: f32,
2316 h: f32,
2317 last_touch: u64,
2321 last_used_frame: u64,
2322}
2323
2324#[derive(Clone, Hash, PartialEq, Eq)]
2325struct TextShapeKey {
2326 text: Arc<str>,
2327 px_bits: u32,
2328 line_height_bits: u32,
2329 font_family: Option<&'static str>,
2330 font_weight: u16,
2331 font_style: u8,
2332 letter_spacing_bits: u32,
2333 variation: Option<Arc<str>>,
2334 font_generation: u64,
2335}
2336
2337struct TextShapeCacheEntry {
2338 glyphs: Arc<[repose_text::ShapedGlyph]>,
2339 last_used_frame: u64,
2340}
2341
2342#[repr(C)]
2347#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2348struct RectInstance {
2349 xywh: [f32; 4],
2350 radii: [f32; 4],
2351 color0: [f32; 4],
2352 color1: [f32; 4],
2353 fwd_mat: [f32; 4],
2354 grad: [f32; 4],
2355 bits: u32,
2356 _pad: [f32; 3],
2357}
2358
2359#[repr(C)]
2360#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2361struct BorderInstance {
2362 xywh: [f32; 4],
2363 radii: [f32; 4],
2364 color0: [f32; 4],
2365 color1: [f32; 4],
2366 fwd_mat: [f32; 4],
2367 grad: [f32; 4],
2368 bits: u32,
2369 stroke: f32,
2370 _pad: [f32; 2],
2371}
2372
2373#[repr(C)]
2374#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2375struct EllipseInstance {
2376 xywh: [f32; 4],
2377 color0: [f32; 4],
2378 color1: [f32; 4],
2379 fwd_mat: [f32; 4],
2380 grad: [f32; 4],
2381 bits: u32,
2382 _pad: [f32; 1],
2383}
2384
2385#[repr(C)]
2386#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2387struct EllipseBorderInstance {
2388 xywh: [f32; 4],
2389 color0: [f32; 4],
2390 color1: [f32; 4],
2391 fwd_mat: [f32; 4],
2392 grad: [f32; 4],
2393 bits: u32,
2394 stroke: f32,
2395 pad: f32,
2396 _pad: f32,
2397}
2398
2399#[repr(C)]
2400#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2401struct ArcInstance {
2402 xywh: [f32; 4],
2403 color0: [f32; 4],
2404 color1: [f32; 4],
2405 fwd_mat: [f32; 4],
2406 grad: [f32; 4],
2407 bits: u32,
2408 start_angle: f32,
2409 sweep_angle: f32,
2410 stroke: f32,
2411 pad: f32,
2412 _pad: f32,
2413}
2414
2415#[repr(C)]
2416#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2417struct GlyphInstance {
2418 xywh: [f32; 4],
2419 uv: [f32; 4],
2420 color: [f32; 4],
2421 fwd_mat: [f32; 4],
2422}
2423
2424#[repr(C)]
2425#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2426struct BlurInstance {
2427 xywh: [f32; 4],
2428 uv: [f32; 4],
2429 color: [f32; 4],
2430 sigma_px: [f32; 2],
2431 fwd_mat: [f32; 4],
2432 axis: u32,
2433 edge_mode: u32,
2434 shape: [f32; 4],
2438}
2439
2440#[repr(C)]
2446#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2447struct ProjectiveInstance {
2448 c0: [f32; 2],
2449 c1: [f32; 2],
2450 c2: [f32; 2],
2451 c3: [f32; 2],
2452 uv: [f32; 4],
2453 w: [f32; 4],
2454 alpha: f32,
2455 _pad: [f32; 3],
2456}
2457
2458#[repr(C)]
2461#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2462struct YuvTransformRaw {
2463 row0: [f32; 4],
2464 row1: [f32; 4],
2465 row2: [f32; 4],
2466 b: [f32; 4],
2467 transfer: [f32; 4],
2468}
2469
2470fn make_yuv_transform_raw(color_info: ColorInfo, pixel_format: PixelFormat) -> YuvTransformRaw {
2471 let yuv = color_info.to_yuv_transform();
2472 let sample_scale = match pixel_format {
2473 PixelFormat::P010 => 65535.0 / (64.0 * 1020.0),
2474 _ => 1.0,
2475 };
2476 let transfer_mode = match color_info.transfer {
2477 repose_core::color::Transfer::Srgb => 0.0,
2478 repose_core::color::Transfer::Bt709 => 1.0,
2479 repose_core::color::Transfer::Linear => 2.0,
2480 repose_core::color::Transfer::Pq => 3.0,
2481 repose_core::color::Transfer::Hlg => 4.0,
2482 };
2483 YuvTransformRaw {
2484 row0: [yuv.m[0][0], yuv.m[0][1], yuv.m[0][2], 0.0],
2485 row1: [yuv.m[1][0], yuv.m[1][1], yuv.m[1][2], 0.0],
2486 row2: [yuv.m[2][0], yuv.m[2][1], yuv.m[2][2], 0.0],
2487 b: [yuv.b[0], yuv.b[1], yuv.b[2], sample_scale],
2488 transfer: [transfer_mode, 0.0, 0.0, 0.0],
2489 }
2490}
2491
2492#[repr(C)]
2493#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2494struct Nv12Instance {
2495 xywh: [f32; 4],
2496 uv: [f32; 4],
2497 color: [f32; 4],
2498 uv_x_offset: f32,
2499 uv_y_offset: f32,
2500 fwd_mat: [f32; 4],
2501}
2502
2503#[repr(C)]
2504#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2505struct ClipInstance {
2506 xywh: [f32; 4],
2507 radii: [f32; 4],
2508 fwd_mat: [f32; 4],
2509}
2510
2511#[repr(C)]
2514#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2515struct BlendInstance {
2516 xywh: [f32; 4],
2517 uv: [f32; 4],
2518 color: [f32; 4],
2519 fwd_mat: [f32; 4],
2520 mode: u32,
2521 _pad: [f32; 3],
2522}
2523
2524#[repr(C)]
2525#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2526struct MeshVertex {
2527 pos: [f32; 2],
2528 color: [f32; 4],
2529 uv: [f32; 2],
2530}
2531
2532#[derive(Clone)]
2533struct MeshGpu {
2534 vertices: wgpu::Buffer,
2535 indices: wgpu::Buffer,
2536 vertex_count: u32,
2537 index_count: u32,
2538 bytes: u64,
2539}
2540
2541#[derive(Clone, Copy, PartialEq, Eq, Hash)]
2542struct MeshCacheKey {
2543 source: usize,
2544 index: usize,
2545 kind: u8,
2546 vertex_len: usize,
2547 index_len: usize,
2548}
2549
2550#[derive(Clone)]
2551enum MeshOwner {
2552 Object(Arc<repose_core::VectorMeshData>),
2553 Overlay(Arc<[repose_core::VectorMeshData]>),
2554}
2555
2556struct MeshCacheEntry {
2557 owner: MeshOwner,
2558 gpu: MeshGpu,
2559 last_used_frame: u64,
2560 last_touch_frame: u64,
2561}
2562
2563#[derive(Clone, PartialEq, Eq, Hash)]
2564struct SlugDrawKey {
2565 cache_key: repose_text::CacheKey,
2566 fill: bool,
2567 stroke: Option<slug::StrokeTessKey>,
2570 transform: [u32; 11],
2571 origin: [u32; 2],
2572 px_bits: u32,
2573 target: [u32; 2],
2574 color: [u32; 4],
2575}
2576
2577struct SlugDrawCacheEntry {
2578 vertices: Arc<[slug::TessVertex]>,
2579 bytes: u64,
2580 last_used_frame: u64,
2581 last_touch_frame: u64,
2582}
2583
2584struct GlyphOutlineEntry {
2588 commands: Option<Arc<[repose_text::Command]>>,
2589 last_touch: u64,
2590}
2591
2592fn evict_atlas_lru(
2597 map: &mut HashMap<repose_text::CacheKey, GlyphInfo>,
2598 used_area: &mut u64,
2599 frame_index: u64,
2600 target_area: u64,
2601) -> bool {
2602 let mut victims: Vec<(u64, repose_text::CacheKey)> = map
2603 .iter()
2604 .filter(|(_, info)| info.last_used_frame != frame_index)
2605 .map(|(key, info)| (info.last_touch, key.clone()))
2606 .collect();
2607 if victims.is_empty() {
2608 return false;
2609 }
2610 victims.sort_unstable_by_key(|victim| victim.0);
2612 let mut evicted = false;
2613 for (_, key) in victims {
2614 if *used_area <= target_area || map.len() <= 1 {
2615 break;
2616 }
2617 if let Some(info) = map.remove(&key) {
2618 *used_area = used_area.saturating_sub(glyph_cell_area(&info));
2619 evicted = true;
2620 }
2621 }
2622 evicted
2623}
2624
2625fn glyph_cell_area(info: &GlyphInfo) -> u64 {
2628 (info.w as u64).saturating_mul(info.h as u64)
2629}
2630
2631fn defer_atlas_glyph(deferred: &mut HashSet<repose_text::CacheKey>, key: repose_text::CacheKey) {
2635 if deferred.len() >= MAX_GLYPH_ATLAS_ENTRIES {
2636 deferred.clear();
2637 }
2638 deferred.insert(key);
2639}
2640
2641fn slug_draw_cache_entry_bytes(vertices: usize) -> u64 {
2645 let per_entry = (std::mem::size_of::<SlugDrawKey>()
2646 + std::mem::size_of::<SlugDrawCacheEntry>()
2647 + std::mem::size_of::<(SlugDrawKey, SlugDrawCacheEntry)>()) as u64;
2648 u64::try_from(vertices)
2649 .ok()
2650 .and_then(|count| count.checked_mul(std::mem::size_of::<slug::TessVertex>() as u64))
2651 .and_then(|payload| payload.checked_add(per_entry))
2652 .unwrap_or(u64::MAX)
2653}
2654
2655#[repr(C)]
2662#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2663struct MeshUniform {
2664 m0: [f32; 4],
2665 m1: [f32; 4],
2666 paint: [u32; 4],
2667 color0: [f32; 4],
2668 color1: [f32; 4],
2669 grad_start: [f32; 2],
2670 _p3: [f32; 2],
2671 grad_end: [f32; 2],
2672 _p4: [f32; 2],
2673 stop_colors: [[f32; 4]; MAX_GRADIENT_STOPS],
2674 stop_offsets: [[f32; 4]; MAX_GRADIENT_STOPS / 4],
2675}
2676
2677const MESH_UNIFORM_BYTES: usize = 272;
2681const _: () = assert!(std::mem::size_of::<MeshUniform>() == MESH_UNIFORM_BYTES);
2682
2683const MESH_UNIFORM_CAP: u64 = 4 * 1024 * 1024;
2684const MAX_RETAINED_IMAGE_BYTES: u64 = 256 * 1024 * 1024;
2685const MAX_RETAINED_IMAGES: usize = 512;
2686const MAX_BLEND_SNAPSHOT_BYTES: u64 = 64 * 1024 * 1024;
2687const MAX_BLEND_SNAPSHOT_POOL_ENTRIES: usize = 64;
2688const MAX_GLYPH_ATLAS_ENTRIES: usize = 32768;
2689const MAX_GLYPH_ATLAS_SIZE: u32 = 4096;
2692const ATLAS_COMPACT_KEEP_NUM: u64 = 3;
2696const ATLAS_COMPACT_KEEP_DEN: u64 = 4;
2697const ATLAS_COMPACT_MIN_FRAME_INTERVAL: u64 = 30;
2701const MAX_GPU_RESOURCE_BYTES: u64 = 1024 * 1024 * 1024;
2702const MAX_UPLOAD_RING_BYTES: u64 = 64 * 1024 * 1024;
2703const MAX_MESH_CACHE_BYTES: u64 = 256 * 1024 * 1024;
2704const MAX_MESH_CACHE_ENTRIES: usize = 512;
2705const MAX_SLUG_DRAW_CACHE_ENTRIES: usize = 2048;
2706const MAX_SLUG_DRAW_CACHE_BYTES: u64 = 32 * 1024 * 1024;
2707const MAX_LEGACY_SHAPED_TEXT_CACHE_ENTRIES: usize = 256;
2708const MAX_GLYPH_OUTLINE_CACHE_ENTRIES: usize = 4096;
2709const MAX_IMAGE_DECODE_BYTES: u64 = 64 * 1024 * 1024;
2710const MAX_CALLBACK_SCOPES: usize = 256;
2711const NV12_FOURCC: u32 = 0x3231_564e;
2712const P010_FOURCC: u32 = 0x3031_3050;
2713
2714fn canonical_fourcc(format: PixelFormat) -> u32 {
2715 match format {
2716 PixelFormat::P010 => P010_FOURCC,
2717 _ => NV12_FOURCC,
2718 }
2719}
2720
2721impl MeshUniform {
2722 fn identity() -> Self {
2723 Self {
2724 m0: [1.0, 0.0, 0.0, 0.0],
2725 m1: [0.0, 1.0, 0.0, 0.0],
2726 paint: [0; 4],
2727 color0: [0.0; 4],
2728 color1: [0.0; 4],
2729 grad_start: [0.0; 2],
2730 _p3: [0.0; 2],
2731 grad_end: [0.0; 2],
2732 _p4: [0.0; 2],
2733 stop_colors: [[0.0; 4]; MAX_GRADIENT_STOPS],
2734 stop_offsets: [[0.0; 4]; MAX_GRADIENT_STOPS / 4],
2735 }
2736 }
2737}
2738
2739type PackedStops = (
2741 [[f32; 4]; MAX_GRADIENT_STOPS],
2742 [[f32; 4]; MAX_GRADIENT_STOPS / 4],
2743 u32,
2744);
2745
2746fn pack_gradient_stops(stops: &GradientStops) -> PackedStops {
2752 let mut stop_colors = [[0.0f32; 4]; MAX_GRADIENT_STOPS];
2753 let mut flat = [0.0f32; MAX_GRADIENT_STOPS];
2754 let mut count = 0u32;
2755 let mut prev = 0.0f32;
2756 for ((color, offset), stop) in stop_colors
2757 .iter_mut()
2758 .zip(flat.iter_mut())
2759 .zip(stops.as_slice())
2760 {
2761 let clamped = stop.offset.clamp(0.0, 1.0).max(prev);
2762 prev = clamped;
2763 *color = stop.color.to_linear();
2764 *offset = clamped;
2765 count += 1;
2766 }
2767 let mut stop_offsets = [[0.0f32; 4]; MAX_GRADIENT_STOPS / 4];
2768 for (dst, src) in stop_offsets.iter_mut().zip(flat.as_chunks::<4>().0) {
2769 *dst = *src;
2770 }
2771 (stop_colors, stop_offsets, count)
2772}
2773
2774fn mesh_uniform_from_paint(affine: [f32; 6], paint: &repose_core::PaintDesc) -> MeshUniform {
2775 let no_stops = GradientStops::default();
2776 let (paint_type, paint_kind, stops, color0, color1, grad_start, grad_end) = match paint {
2777 repose_core::PaintDesc::Solid => {
2778 (0u32, 0u32, no_stops, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2])
2779 }
2780 repose_core::PaintDesc::Linear {
2781 start,
2782 end,
2783 start_color,
2784 end_color,
2785 } => (
2786 1u32,
2787 0u32,
2788 no_stops,
2789 start_color.to_linear(),
2790 end_color.to_linear(),
2791 [start.x, start.y],
2792 [end.x, end.y],
2793 ),
2794 repose_core::PaintDesc::Radial {
2795 center,
2796 radius,
2797 start_color,
2798 end_color,
2799 } => (
2800 1u32,
2801 1u32,
2802 no_stops,
2803 start_color.to_linear(),
2804 end_color.to_linear(),
2805 [center.x, center.y],
2806 [radius.max(0.0), 0.0],
2807 ),
2808 repose_core::PaintDesc::Sweep {
2809 center,
2810 start_color,
2811 end_color,
2812 } => (
2813 1u32,
2814 2u32,
2815 no_stops,
2816 start_color.to_linear(),
2817 end_color.to_linear(),
2818 [center.x, center.y],
2819 [0.0, 0.0],
2820 ),
2821 repose_core::PaintDesc::LinearStops { start, end, stops } => (
2822 1u32,
2823 3u32,
2824 *stops,
2825 [0.0; 4],
2826 [0.0; 4],
2827 [start.x, start.y],
2828 [end.x, end.y],
2829 ),
2830 repose_core::PaintDesc::RadialStops {
2831 center,
2832 radius,
2833 stops,
2834 } => (
2835 1u32,
2836 4u32,
2837 *stops,
2838 [0.0; 4],
2839 [0.0; 4],
2840 [center.x, center.y],
2841 [radius.max(0.0), 0.0],
2842 ),
2843 _ => (0u32, 0u32, no_stops, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
2844 };
2845 let (stop_colors, stop_offsets, count) = pack_gradient_stops(&stops);
2846 MeshUniform {
2847 m0: [affine[0], affine[1], affine[2], 0.0],
2848 m1: [affine[3], affine[4], affine[5], 0.0],
2849 paint: [paint_type, paint_kind, count, 0],
2850 color0,
2851 color1,
2852 grad_start,
2853 _p3: [0.0; 2],
2854 grad_end,
2855 _p4: [0.0; 2],
2856 stop_colors,
2857 stop_offsets,
2858 }
2859}
2860
2861fn combine_mesh_affine(current: &Transform, mesh: [f32; 6]) -> [f32; 6] {
2862 let cm = current.linear();
2863 let (cm00, cm01, cm10, cm11) = (cm[0], cm[1], cm[2], cm[3]);
2864 let mm00 = mesh[0];
2865 let mm01 = mesh[1];
2866 let mm10 = mesh[2];
2867 let mm11 = mesh[3];
2868 let mtx = mesh[4];
2869 let mty = mesh[5];
2870 let r00 = cm00 * mm00 + cm01 * mm10;
2871 let r01 = cm00 * mm01 + cm01 * mm11;
2872 let r10 = cm10 * mm00 + cm11 * mm10;
2873 let r11 = cm10 * mm01 + cm11 * mm11;
2874 let tx = cm00 * mtx + cm01 * mty + current.translate_x;
2875 let ty = cm10 * mtx + cm11 * mty + current.translate_y;
2876 [r00, r01, tx, r10, r11, ty]
2879}
2880
2881fn mesh_aabb(mesh: &repose_core::VectorMeshData, affine: [f32; 6]) -> repose_core::Rect {
2882 let mut min_x = f32::INFINITY;
2883 let mut min_y = f32::INFINITY;
2884 let mut max_x = f32::NEG_INFINITY;
2885 let mut max_y = f32::NEG_INFINITY;
2886 for v in mesh.vertices.iter() {
2887 let x = affine[0] * v.pos[0] + affine[1] * v.pos[1] + affine[2];
2888 let y = affine[3] * v.pos[0] + affine[4] * v.pos[1] + affine[5];
2889 min_x = min_x.min(x);
2890 min_y = min_y.min(y);
2891 max_x = max_x.max(x);
2892 max_y = max_y.max(y);
2893 }
2894 let w = (max_x - min_x).max(0.0);
2895 let h = (max_y - min_y).max(0.0);
2896 if !min_x.is_finite() || !min_y.is_finite() {
2897 return repose_core::Rect {
2898 x: 0.0,
2899 y: 0.0,
2900 w: 0.0,
2901 h: 0.0,
2902 };
2903 }
2904 repose_core::Rect {
2905 x: min_x,
2906 y: min_y,
2907 w,
2908 h,
2909 }
2910}
2911
2912fn quantize_glyph_px(px: f32) -> f32 {
2913 if !px.is_finite() || px <= 0.0 {
2914 return 1.0;
2915 }
2916 ((px * 4.0).round() / 4.0).max(0.25)
2917}
2918
2919fn swash_to_a8_coverage(content: repose_text::SwashContent, data: &[u8]) -> Option<Vec<u8>> {
2920 match content {
2921 repose_text::SwashContent::Mask => Some(data.to_vec()),
2922 repose_text::SwashContent::SubpixelMask => {
2923 let mut out = Vec::with_capacity(data.len() / 4);
2924 for px in data.as_chunks::<4>().0 {
2925 let r = px[0];
2926 let g = px[1];
2927 let b = px[2];
2928 out.push(r.max(g).max(b));
2929 }
2930 Some(out)
2931 }
2932 repose_text::SwashContent::Color => None,
2933 }
2934}
2935
2936impl WgpuSceneRenderer {
2937 pub fn from_device(
2948 device: wgpu::Device,
2949 queue: wgpu::Queue,
2950 output_format: wgpu::TextureFormat,
2951 msaa_samples: u32,
2952 ) -> anyhow::Result<Self> {
2953 validate_sample_count("msaa_samples", msaa_samples)?;
2954 Self::from_device_with_working_space_msaa(
2955 device,
2956 queue,
2957 output_format,
2958 msaa_samples,
2959 msaa_samples,
2960 )
2961 }
2962
2963 pub fn try_from_device_with_adapter(
2967 adapter: &wgpu::Adapter,
2968 device: wgpu::Device,
2969 queue: wgpu::Queue,
2970 output_format: wgpu::TextureFormat,
2971 requested_msaa: u32,
2972 ) -> anyhow::Result<Self> {
2973 let msaa = pick_surface_msaa(adapter, output_format, requested_msaa);
2974 validate_render_target_format(adapter, output_format, msaa)?;
2975 let working_space_msaa =
2976 pick_surface_msaa_for_mode(adapter, wgpu::TextureFormat::Rgba16Float, msaa, true);
2977 Self::from_device_with_working_space_msaa(
2978 device,
2979 queue,
2980 output_format,
2981 msaa,
2982 working_space_msaa,
2983 )
2984 }
2985
2986 pub fn with_surface_format(
2990 mut self,
2991 surface_format: wgpu::TextureFormat,
2992 ) -> anyhow::Result<Self> {
2993 if surface_format != self.output_format
2994 && surface_format.add_srgb_suffix() != self.output_format
2995 {
2996 anyhow::bail!(
2997 "surface format {surface_format:?} cannot be reinterpreted as {:?}",
2998 self.output_format
2999 );
3000 }
3001 if self.surface_format != surface_format {
3002 self.surface_format = surface_format;
3003 self.recreate_msaa_and_depth_stencil();
3004 }
3005 Ok(self)
3006 }
3007
3008 pub fn from_device_with_working_space_msaa(
3009 device: wgpu::Device,
3010 queue: wgpu::Queue,
3011 output_format: wgpu::TextureFormat,
3012 msaa_samples: u32,
3013 working_space_msaa_samples: u32,
3014 ) -> anyhow::Result<Self> {
3015 validate_sample_count("msaa_samples", msaa_samples)?;
3016 validate_sample_count("working_space_msaa_samples", working_space_msaa_samples)?;
3017 let msaa_samples = msaa_samples.max(1);
3018 let working_space_msaa_samples = working_space_msaa_samples.max(1);
3019 let mesh_uniform_size = std::mem::size_of::<MeshUniform>() as u64;
3020 let base_mesh_alignment =
3021 u64::from(device.limits().min_uniform_buffer_offset_alignment).max(4);
3022 let mesh_uniform_alignment = mesh_uniform_stride(mesh_uniform_size, base_mesh_alignment);
3023 let mesh_uniform_cap = align_up(
3024 MESH_UNIFORM_CAP
3025 .min(device.limits().max_buffer_size)
3026 .max(mesh_uniform_alignment),
3027 base_mesh_alignment,
3028 )
3029 .unwrap_or(mesh_uniform_alignment);
3030 let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3031 label: Some("globals layout"),
3032 entries: &[wgpu::BindGroupLayoutEntry {
3033 binding: 0,
3034 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
3035 ty: wgpu::BindingType::Buffer {
3036 ty: wgpu::BufferBindingType::Uniform,
3037 has_dynamic_offset: false,
3038 min_binding_size: None,
3039 },
3040 count: None,
3041 }],
3042 });
3043
3044 let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
3045 label: Some("globals buf"),
3046 size: std::mem::size_of::<Globals>() as u64,
3047 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3048 mapped_at_creation: false,
3049 });
3050
3051 let globals_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
3052 label: Some("globals bind"),
3053 layout: &globals_layout,
3054 entries: &[wgpu::BindGroupEntry {
3055 binding: 0,
3056 resource: globals_buf.as_entire_binding(),
3057 }],
3058 });
3059 let globals_staging_cap =
3060 (64 * std::mem::size_of::<Globals>() as u64).max(std::mem::size_of::<Globals>() as u64);
3061 let globals_staging = device.create_buffer(&wgpu::BufferDescriptor {
3062 label: Some("globals staging"),
3063 size: globals_staging_cap,
3064 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
3065 mapped_at_creation: false,
3066 });
3067
3068 let ds_format = wgpu::TextureFormat::Depth24PlusStencil8;
3069
3070 let stencil_for_content = wgpu::DepthStencilState {
3071 format: ds_format,
3072 depth_write_enabled: Some(false),
3073 depth_compare: Some(wgpu::CompareFunction::Always),
3074 stencil: wgpu::StencilState {
3075 front: wgpu::StencilFaceState {
3076 compare: wgpu::CompareFunction::Equal,
3083 fail_op: wgpu::StencilOperation::Keep,
3084 depth_fail_op: wgpu::StencilOperation::Keep,
3085 pass_op: wgpu::StencilOperation::Keep,
3086 },
3087 back: wgpu::StencilFaceState {
3088 compare: wgpu::CompareFunction::Equal,
3089 fail_op: wgpu::StencilOperation::Keep,
3090 depth_fail_op: wgpu::StencilOperation::Keep,
3091 pass_op: wgpu::StencilOperation::Keep,
3092 },
3093 read_mask: 0xFF,
3094 write_mask: 0x00,
3095 },
3096 bias: wgpu::DepthBiasState::default(),
3097 };
3098
3099 let stencil_for_clip_inc = wgpu::DepthStencilState {
3100 format: ds_format,
3101 depth_write_enabled: Some(false),
3102 depth_compare: Some(wgpu::CompareFunction::Always),
3103 stencil: wgpu::StencilState {
3104 front: wgpu::StencilFaceState {
3105 compare: wgpu::CompareFunction::Always,
3106 fail_op: wgpu::StencilOperation::Keep,
3107 depth_fail_op: wgpu::StencilOperation::Keep,
3108 pass_op: wgpu::StencilOperation::IncrementClamp,
3109 },
3110 back: wgpu::StencilFaceState {
3111 compare: wgpu::CompareFunction::Always,
3112 fail_op: wgpu::StencilOperation::Keep,
3113 depth_fail_op: wgpu::StencilOperation::Keep,
3114 pass_op: wgpu::StencilOperation::IncrementClamp,
3115 },
3116 read_mask: 0xFF,
3117 write_mask: 0xFF,
3118 },
3119 bias: wgpu::DepthBiasState::default(),
3120 };
3121
3122 let stencil_for_clip_dec = wgpu::DepthStencilState {
3123 format: ds_format,
3124 depth_write_enabled: Some(false),
3125 depth_compare: Some(wgpu::CompareFunction::Always),
3126 stencil: wgpu::StencilState {
3127 front: wgpu::StencilFaceState {
3128 compare: wgpu::CompareFunction::Always,
3129 fail_op: wgpu::StencilOperation::Keep,
3130 depth_fail_op: wgpu::StencilOperation::Keep,
3131 pass_op: wgpu::StencilOperation::DecrementClamp,
3132 },
3133 back: wgpu::StencilFaceState {
3134 compare: wgpu::CompareFunction::Always,
3135 fail_op: wgpu::StencilOperation::Keep,
3136 depth_fail_op: wgpu::StencilOperation::Keep,
3137 pass_op: wgpu::StencilOperation::DecrementClamp,
3138 },
3139 read_mask: 0xFF,
3140 write_mask: 0xFF,
3141 },
3142 bias: wgpu::DepthBiasState::default(),
3143 };
3144
3145 let _multisample_state = wgpu::MultisampleState {
3146 count: msaa_samples,
3147 mask: !0,
3148 alpha_to_coverage_enabled: false,
3149 };
3150
3151 let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
3155 label: Some("image/text sampler"),
3156 address_mode_u: wgpu::AddressMode::ClampToEdge,
3157 address_mode_v: wgpu::AddressMode::ClampToEdge,
3158 mag_filter: wgpu::FilterMode::Linear,
3159 min_filter: wgpu::FilterMode::Linear,
3160 mipmap_filter: wgpu::MipmapFilterMode::Linear,
3161 ..Default::default()
3162 });
3163 let image_sampler_nearest = device.create_sampler(&wgpu::SamplerDescriptor {
3164 label: Some("image nearest sampler"),
3165 address_mode_u: wgpu::AddressMode::ClampToEdge,
3166 address_mode_v: wgpu::AddressMode::ClampToEdge,
3167 mag_filter: wgpu::FilterMode::Nearest,
3168 min_filter: wgpu::FilterMode::Nearest,
3169 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
3170 ..Default::default()
3171 });
3172
3173 let image_sampler_repeat = device.create_sampler(&wgpu::SamplerDescriptor {
3176 label: Some("image repeat sampler"),
3177 address_mode_u: wgpu::AddressMode::Repeat,
3178 address_mode_v: wgpu::AddressMode::Repeat,
3179 mag_filter: wgpu::FilterMode::Linear,
3180 min_filter: wgpu::FilterMode::Linear,
3181 mipmap_filter: wgpu::MipmapFilterMode::Linear,
3182 ..Default::default()
3183 });
3184 let image_sampler_repeat_nearest = device.create_sampler(&wgpu::SamplerDescriptor {
3185 label: Some("image repeat nearest sampler"),
3186 address_mode_u: wgpu::AddressMode::Repeat,
3187 address_mode_v: wgpu::AddressMode::Repeat,
3188 mag_filter: wgpu::FilterMode::Nearest,
3189 min_filter: wgpu::FilterMode::Nearest,
3190 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
3191 ..Default::default()
3192 });
3193
3194 let layer_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
3196 label: Some("layer nearest sampler"),
3197 address_mode_u: wgpu::AddressMode::ClampToEdge,
3198 address_mode_v: wgpu::AddressMode::ClampToEdge,
3199 mag_filter: wgpu::FilterMode::Nearest,
3200 min_filter: wgpu::FilterMode::Nearest,
3201 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
3202 ..Default::default()
3203 });
3204
3205 let layer_sampler_linear = device.create_sampler(&wgpu::SamplerDescriptor {
3208 label: Some("layer linear sampler"),
3209 address_mode_u: wgpu::AddressMode::ClampToEdge,
3210 address_mode_v: wgpu::AddressMode::ClampToEdge,
3211 mag_filter: wgpu::FilterMode::Linear,
3212 min_filter: wgpu::FilterMode::Linear,
3213 mipmap_filter: wgpu::MipmapFilterMode::Linear,
3214 ..Default::default()
3215 });
3216
3217 let text_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3219 label: Some("text/rgba bind layout"),
3220 entries: &[
3221 wgpu::BindGroupLayoutEntry {
3222 binding: 0,
3223 visibility: wgpu::ShaderStages::FRAGMENT,
3224 ty: wgpu::BindingType::Texture {
3225 multisampled: false,
3226 view_dimension: wgpu::TextureViewDimension::D2,
3227 sample_type: wgpu::TextureSampleType::Float { filterable: true },
3228 },
3229 count: None,
3230 },
3231 wgpu::BindGroupLayoutEntry {
3232 binding: 1,
3233 visibility: wgpu::ShaderStages::FRAGMENT,
3234 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
3235 count: None,
3236 },
3237 ],
3238 });
3239 let image_bind_layout_rgba = text_bind_layout.clone();
3241
3242 let image_bind_layout_nv12 =
3244 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3245 label: Some("image bind layout nv12"),
3246 entries: &[
3247 wgpu::BindGroupLayoutEntry {
3249 binding: 0,
3250 visibility: wgpu::ShaderStages::FRAGMENT,
3251 ty: wgpu::BindingType::Texture {
3252 multisampled: false,
3253 view_dimension: wgpu::TextureViewDimension::D2,
3254 sample_type: wgpu::TextureSampleType::Float { filterable: true },
3255 },
3256 count: None,
3257 },
3258 wgpu::BindGroupLayoutEntry {
3260 binding: 1,
3261 visibility: wgpu::ShaderStages::FRAGMENT,
3262 ty: wgpu::BindingType::Texture {
3263 multisampled: false,
3264 view_dimension: wgpu::TextureViewDimension::D2,
3265 sample_type: wgpu::TextureSampleType::Float { filterable: true },
3266 },
3267 count: None,
3268 },
3269 wgpu::BindGroupLayoutEntry {
3271 binding: 2,
3272 visibility: wgpu::ShaderStages::FRAGMENT,
3273 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
3274 count: None,
3275 },
3276 wgpu::BindGroupLayoutEntry {
3278 binding: 3,
3279 visibility: wgpu::ShaderStages::FRAGMENT,
3280 ty: wgpu::BindingType::Buffer {
3281 ty: wgpu::BufferBindingType::Uniform,
3282 has_dynamic_offset: false,
3283 min_binding_size: None,
3284 },
3285 count: None,
3286 },
3287 ],
3288 });
3289
3290 let clip_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3292 label: Some("clip pipeline layout"),
3293 bind_group_layouts: &[Some(&globals_layout)],
3294 immediate_size: 0,
3295 });
3296 let clip_vertex_layout = wgpu::VertexBufferLayout {
3297 array_stride: std::mem::size_of::<ClipInstance>() as u64,
3298 step_mode: wgpu::VertexStepMode::Instance,
3299 attributes: &[
3300 wgpu::VertexAttribute {
3301 shader_location: 0,
3302 offset: 0,
3303 format: wgpu::VertexFormat::Float32x4,
3304 },
3305 wgpu::VertexAttribute {
3306 shader_location: 1,
3307 offset: 16,
3308 format: wgpu::VertexFormat::Float32x4,
3309 },
3310 wgpu::VertexAttribute {
3311 shader_location: 2,
3312 offset: 32,
3313 format: wgpu::VertexFormat::Float32x4,
3314 },
3315 ],
3316 };
3317 let mesh_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3319 label: Some("mesh uniform layout"),
3320 entries: &[wgpu::BindGroupLayoutEntry {
3321 binding: 0,
3322 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
3323 ty: wgpu::BindingType::Buffer {
3324 ty: wgpu::BufferBindingType::Uniform,
3325 has_dynamic_offset: true,
3326 min_binding_size: NonZeroU64::new(mesh_uniform_size),
3327 },
3328 count: None,
3329 }],
3330 });
3331 let mesh_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
3332 label: Some("mesh uniform buffer"),
3333 size: mesh_uniform_cap,
3334 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3335 mapped_at_creation: false,
3336 });
3337 let mesh_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
3338 label: Some("mesh uniform bind"),
3339 layout: &mesh_bind_layout,
3340 entries: &[wgpu::BindGroupEntry {
3341 binding: 0,
3342 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3343 buffer: &mesh_uniform_buf,
3344 offset: 0,
3345 size: NonZeroU64::new(mesh_uniform_size),
3346 }),
3347 }],
3348 });
3349
3350 let surface_pipes = Pipelines::create(
3353 &device,
3354 output_format,
3355 msaa_samples,
3356 &globals_layout,
3357 &text_bind_layout,
3358 &image_bind_layout_nv12,
3359 &clip_pipeline_layout,
3360 &stencil_for_content,
3361 &stencil_for_clip_inc,
3362 &stencil_for_clip_dec,
3363 &clip_vertex_layout,
3364 &mesh_bind_layout,
3365 );
3366 let working_space_pipes = Pipelines::create(
3367 &device,
3368 wgpu::TextureFormat::Rgba16Float,
3369 working_space_msaa_samples,
3370 &globals_layout,
3371 &text_bind_layout,
3372 &image_bind_layout_nv12,
3373 &clip_pipeline_layout,
3374 &stencil_for_content,
3375 &stencil_for_clip_inc,
3376 &stencil_for_clip_dec,
3377 &clip_vertex_layout,
3378 &mesh_bind_layout,
3379 );
3380 let layer_pipes = if msaa_samples == 1 {
3381 surface_pipes.clone()
3382 } else {
3383 Pipelines::create(
3384 &device,
3385 output_format,
3386 1,
3387 &globals_layout,
3388 &text_bind_layout,
3389 &image_bind_layout_nv12,
3390 &clip_pipeline_layout,
3391 &stencil_for_content,
3392 &stencil_for_clip_inc,
3393 &stencil_for_clip_dec,
3394 &clip_vertex_layout,
3395 &mesh_bind_layout,
3396 )
3397 };
3398 let working_space_layer_pipes = if working_space_msaa_samples == 1 {
3399 working_space_pipes.clone()
3400 } else {
3401 Pipelines::create(
3402 &device,
3403 wgpu::TextureFormat::Rgba16Float,
3404 1,
3405 &globals_layout,
3406 &text_bind_layout,
3407 &image_bind_layout_nv12,
3408 &clip_pipeline_layout,
3409 &stencil_for_content,
3410 &stencil_for_clip_inc,
3411 &stencil_for_clip_dec,
3412 &clip_vertex_layout,
3413 &mesh_bind_layout,
3414 )
3415 };
3416
3417 let slug_enabled = true;
3419
3420 let blur_ring = UploadRing::new(
3422 &device,
3423 "blur ring",
3424 1024 * 1024,
3425 wgpu::BufferUsages::VERTEX
3426 | wgpu::BufferUsages::COPY_DST
3427 | wgpu::BufferUsages::COPY_SRC,
3428 );
3429
3430 let atlas_mask = init_atlas_mask(&device);
3432 let atlas_color = init_atlas_color(&device);
3433 let atlas_mask_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
3434 label: Some("atlas mask bind"),
3435 layout: &text_bind_layout,
3436 entries: &[
3437 wgpu::BindGroupEntry {
3438 binding: 0,
3439 resource: wgpu::BindingResource::TextureView(&atlas_mask.view),
3440 },
3441 wgpu::BindGroupEntry {
3442 binding: 1,
3443 resource: wgpu::BindingResource::Sampler(&atlas_mask.sampler),
3444 },
3445 ],
3446 });
3447 let atlas_color_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
3448 label: Some("atlas color bind"),
3449 layout: &text_bind_layout,
3450 entries: &[
3451 wgpu::BindGroupEntry {
3452 binding: 0,
3453 resource: wgpu::BindingResource::TextureView(&atlas_color.view),
3454 },
3455 wgpu::BindGroupEntry {
3456 binding: 1,
3457 resource: wgpu::BindingResource::Sampler(&atlas_color.sampler),
3458 },
3459 ],
3460 });
3461
3462 let ring_rect = UploadRing::new(
3464 &device,
3465 "ring rect",
3466 1 << 20,
3467 wgpu::BufferUsages::VERTEX
3468 | wgpu::BufferUsages::COPY_DST
3469 | wgpu::BufferUsages::COPY_SRC,
3470 );
3471 let ring_border = UploadRing::new(
3472 &device,
3473 "ring border",
3474 1 << 20,
3475 wgpu::BufferUsages::VERTEX
3476 | wgpu::BufferUsages::COPY_DST
3477 | wgpu::BufferUsages::COPY_SRC,
3478 );
3479 let ring_ellipse = UploadRing::new(
3480 &device,
3481 "ring ellipse",
3482 1 << 20,
3483 wgpu::BufferUsages::VERTEX
3484 | wgpu::BufferUsages::COPY_DST
3485 | wgpu::BufferUsages::COPY_SRC,
3486 );
3487 let ring_ellipse_border = UploadRing::new(
3488 &device,
3489 "ring ellipse border",
3490 1 << 20,
3491 wgpu::BufferUsages::VERTEX
3492 | wgpu::BufferUsages::COPY_DST
3493 | wgpu::BufferUsages::COPY_SRC,
3494 );
3495 let ring_arc = UploadRing::new(
3496 &device,
3497 "ring arc",
3498 1 << 20,
3499 wgpu::BufferUsages::VERTEX
3500 | wgpu::BufferUsages::COPY_DST
3501 | wgpu::BufferUsages::COPY_SRC,
3502 );
3503 let ring_glyph_mask = UploadRing::new(
3504 &device,
3505 "ring glyph mask",
3506 1 << 20,
3507 wgpu::BufferUsages::VERTEX
3508 | wgpu::BufferUsages::COPY_DST
3509 | wgpu::BufferUsages::COPY_SRC,
3510 );
3511 let ring_glyph_color = UploadRing::new(
3512 &device,
3513 "ring glyph color",
3514 1 << 20,
3515 wgpu::BufferUsages::VERTEX
3516 | wgpu::BufferUsages::COPY_DST
3517 | wgpu::BufferUsages::COPY_SRC,
3518 );
3519 let ring_slug = UploadRing::new(
3520 &device,
3521 "ring slug",
3522 1 << 22,
3523 wgpu::BufferUsages::VERTEX
3524 | wgpu::BufferUsages::COPY_DST
3525 | wgpu::BufferUsages::COPY_SRC,
3526 );
3527 let ring_clip = UploadRing::new(
3528 &device,
3529 "ring clip",
3530 1 << 16,
3531 wgpu::BufferUsages::VERTEX
3532 | wgpu::BufferUsages::COPY_DST
3533 | wgpu::BufferUsages::COPY_SRC,
3534 );
3535 let blend_ring = UploadRing::new(
3536 &device,
3537 "ring blend",
3538 1 << 16,
3539 wgpu::BufferUsages::VERTEX
3540 | wgpu::BufferUsages::COPY_DST
3541 | wgpu::BufferUsages::COPY_SRC,
3542 );
3543 let ring_projective = UploadRing::new(
3544 &device,
3545 "ring projective",
3546 1 << 16,
3547 wgpu::BufferUsages::VERTEX
3548 | wgpu::BufferUsages::COPY_DST
3549 | wgpu::BufferUsages::COPY_SRC,
3550 );
3551 let ring_nv12 = UploadRing::new(
3552 &device,
3553 "ring nv12",
3554 1 << 20,
3555 wgpu::BufferUsages::VERTEX
3556 | wgpu::BufferUsages::COPY_DST
3557 | wgpu::BufferUsages::COPY_SRC,
3558 );
3559 let depth_stencil_tex = device.create_texture(&wgpu::TextureDescriptor {
3561 label: Some("temp ds"),
3562 size: wgpu::Extent3d {
3563 width: 1,
3564 height: 1,
3565 depth_or_array_layers: 1,
3566 },
3567 mip_level_count: 1,
3568 sample_count: 1,
3569 dimension: wgpu::TextureDimension::D2,
3570 format: wgpu::TextureFormat::Depth24PlusStencil8,
3571 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
3572 view_formats: &[],
3573 });
3574 let depth_stencil_view =
3575 depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
3576
3577 let mut renderer = WgpuSceneRenderer {
3578 device,
3579 queue,
3580 output_format,
3581 surface_format: output_format,
3582 output_width: 0,
3583 output_height: 0,
3584 pixels_per_point: 1.0,
3585
3586 surface_pipes,
3587 working_space_pipes,
3588 layer_pipes,
3589 working_space_layer_pipes,
3590
3591 rects: InstancedPipe::new(ring_rect),
3592 borders: InstancedPipe::new(ring_border),
3593 ellipses: InstancedPipe::new(ring_ellipse),
3594 ellipse_borders: InstancedPipe::new(ring_ellipse_border),
3595 arcs: InstancedPipe::new(ring_arc),
3596 glyph_mask: InstancedPipe::new(ring_glyph_mask),
3597 glyph_color: InstancedPipe::new(ring_glyph_color),
3598
3599 text_bind_layout,
3600
3601 image_bind_layout_rgba,
3602 image_bind_layout_nv12,
3603 image_sampler,
3604 image_sampler_nearest,
3605 image_sampler_repeat,
3606 image_sampler_repeat_nearest,
3607 layer_sampler,
3608 layer_sampler_linear,
3609
3610 blur_ring,
3611
3612 slug_enabled,
3613 slug_ring: ring_slug,
3614 slug_cache: slug::GlyphSlugCache::new(),
3615 slug_draw_cache: HashMap::new(),
3616 slug_draw_cache_bytes_total: 0,
3617
3618 clip_ring: ring_clip,
3619
3620 nv12: InstancedPipe::new(ring_nv12),
3621
3622 mesh_cache: HashMap::new(),
3623 mesh_cache_bytes_total: 0,
3624 mesh_uniform_buf,
3625 mesh_bind,
3626 mesh_uniform_head: 0,
3627 mesh_uniform_alignment,
3628 mesh_uniform_cap,
3629
3630 projective_ring: ring_projective,
3631 blend_ring,
3632 flatten_layer_ids: Vec::new(),
3633 blur_scratch_ids: HashMap::new(),
3634 blur_scratch_next: 0,
3635 blend_snapshots: std::collections::HashMap::new(),
3636 blend_snapshot_pool: HashMap::new(),
3637 blend_copies: Vec::new(),
3638
3639 msaa_samples,
3640 working_space_msaa_samples,
3641 depth_stencil_tex,
3642 depth_stencil_view,
3643 msaa_tex: None,
3644 msaa_view: None,
3645 ws_msaa_tex: None,
3646 ws_msaa_view: None,
3647 surface_resolve_tex: None,
3648 surface_resolve_view: None,
3649 surface_resolve_bytes: 0,
3650 msaa_bytes: 0,
3651 ws_msaa_bytes: 0,
3652 depth_stencil_bytes: 0,
3653 working_space_bytes: 0,
3654 gpu_budget_bytes: MAX_GPU_RESOURCE_BYTES,
3655 globals_bind,
3656 globals_buf,
3657 globals_staging,
3658 globals_staging_cap,
3659
3660 atlas_mask,
3661 atlas_color,
3662 atlas_mask_bind,
3663 atlas_color_bind,
3664 atlas_generation: repose_text::font_generation(),
3665 atlas_mask_pending: AtlasPending::default(),
3666 atlas_color_pending: AtlasPending::default(),
3667 atlas_last_compact_frame: 0,
3668 atlas_mask_failures: HashSet::new(),
3669 atlas_color_failures: HashSet::new(),
3670 atlas_mask_deferred: HashSet::new(),
3671 atlas_color_deferred: HashSet::new(),
3672 glyph_outline_cache: HashMap::new(),
3673 legacy_shaped_text_cache: HashMap::new(),
3674
3675 next_image_handle: 1,
3676 images: HashMap::new(),
3677 retained: HashMap::new(),
3678 retained_bytes_total: 0,
3679
3680 next_coverage_handle: 1,
3681 coverages: HashMap::new(),
3682
3683 frame_index: 0,
3684 cache_touch_clock: 0,
3685 image_bytes_total: 0,
3686 image_evict_after_frames: 600, image_budget_bytes: 512 * 1024 * 1024, layer_pool: HashMap::new(),
3689 producer_layer_ids: HashSet::new(),
3690 layer_bytes_total: 0,
3691 transient_layer_bytes_total: 0,
3692
3693 working_space: false,
3694 ws_tex: None,
3695 ws_view: None,
3696 ws_bind: None,
3697 display_pipeline: None,
3698 display_layout: None,
3699
3700 callback_resources: CallbackResources::default(),
3701 callback_scoped_resources: HashMap::new(),
3702 callback_scope_uses: HashMap::new(),
3703 callback_scope_payloads: HashMap::new(),
3704 callback_scope_clock: 0,
3705 blend_snapshot_bytes_total: 0,
3706 frame_active: false,
3707 last_render_error: None,
3708 };
3709
3710 renderer.recreate_msaa_and_depth_stencil();
3711 Ok(renderer)
3712 }
3713}
3714
3715#[cfg(not(target_arch = "wasm32"))]
3719fn backend_by_name(name: &str) -> Option<wgpu::Backend> {
3720 match name {
3721 "gl" | "opengl" | "opengles" | "webgl" => Some(wgpu::Backend::Gl),
3722 "webgpu" | "browser-webgpu" => Some(wgpu::Backend::BrowserWebGpu),
3723 "metal" => Some(wgpu::Backend::Metal),
3724 "vulkan" => Some(wgpu::Backend::Vulkan),
3725 "dx12" | "d3d12" => Some(wgpu::Backend::Dx12),
3726 _ => None,
3727 }
3728}
3729
3730#[cfg(not(target_arch = "wasm32"))]
3734pub fn parse_backends(spec: &str) -> Option<wgpu::Backends> {
3735 let mut bits = wgpu::Backends::empty();
3736 for name in spec.split([',', ' ', '+']).filter(|n| !n.is_empty()) {
3737 match backend_by_name(&name.to_ascii_lowercase()) {
3738 Some(backend) => bits |= wgpu::Backends::from(backend),
3739 None => log::warn!(
3740 "REPOSE_WGPU_BACKENDS: ignoring unknown backend {name:?}; \
3741 expected one of noop, vulkan, metal, dx12, gl, webgpu"
3742 ),
3743 }
3744 }
3745 (!bits.is_empty()).then_some(bits)
3746}
3747
3748#[cfg(feature = "winit-surface")]
3749pub(crate) async fn surface_instance(
3750 display: Option<winit::event_loop::OwnedDisplayHandle>,
3751) -> (Instance, wgpu::Backends) {
3752 let mut desc = match display {
3757 Some(handle) => wgpu::InstanceDescriptor::new_with_display_handle(Box::new(handle)),
3758 None => wgpu::InstanceDescriptor::new_without_display_handle(),
3759 };
3760 if cfg!(target_arch = "wasm32") {
3761 desc.backends = wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL;
3762 }
3763 #[cfg(not(target_arch = "wasm32"))]
3764 if let Ok(spec) = std::env::var("REPOSE_WGPU_BACKENDS")
3765 && let Some(bits) = parse_backends(&spec)
3766 {
3767 log::info!("REPOSE_WGPU_BACKENDS={spec} -> {bits:?}");
3768 desc.backends = bits;
3769 }
3770
3771 let backends = desc.backends;
3772 let instance = if cfg!(target_arch = "wasm32") {
3773 wgpu::util::new_instance_with_webgpu_detection(desc).await
3774 } else {
3775 wgpu::Instance::new(desc)
3776 };
3777 (instance, backends)
3778}
3779
3780impl WgpuSurfaceBackend {
3781 #[cfg(feature = "winit-surface")]
3782 pub async fn new_async(
3783 window: Arc<winit::window::Window>,
3784 ) -> anyhow::Result<WgpuSurfaceBackend> {
3785 Self::new_async_with_options(window, None, 4, PresentModePref::Auto).await
3786 }
3787
3788 #[cfg(feature = "winit-surface")]
3791 pub async fn new_async_with_msaa(
3792 window: Arc<winit::window::Window>,
3793 msaa_samples: u32,
3794 ) -> anyhow::Result<WgpuSurfaceBackend> {
3795 Self::new_async_with_options(window, None, msaa_samples, PresentModePref::Auto).await
3796 }
3797
3798 #[cfg(feature = "winit-surface")]
3805 pub async fn new_async_with_options(
3806 window: Arc<winit::window::Window>,
3807 display: Option<winit::event_loop::OwnedDisplayHandle>,
3808 msaa_samples: u32,
3809 present_mode: PresentModePref,
3810 ) -> anyhow::Result<WgpuSurfaceBackend> {
3811 let (instance, backends) = surface_instance(display).await;
3812
3813 let surface = instance.create_surface(window.clone())?;
3814
3815 let adapter = instance
3816 .request_adapter(&wgpu::RequestAdapterOptions {
3817 power_preference: wgpu::PowerPreference::HighPerformance,
3818 compatible_surface: Some(&surface),
3819 force_fallback_adapter: false,
3820 apply_limit_buckets: false,
3821 })
3822 .await
3823 .map_err(|e| {
3824 anyhow::anyhow!("no adapter for this window (enabled backends {backends:?}): {e:?}")
3825 })?;
3826 log::info!(
3827 "wgpu adapter: {} backend={:?} driver={:?} type={:?}",
3828 adapter.get_info().name,
3829 adapter.get_info().backend,
3830 adapter.get_info().driver,
3831 adapter.get_info().device_type,
3832 );
3833
3834 let limits = adapter.limits();
3835
3836 let features = {
3837 let available = adapter.features();
3838 let mut features = wgpu::Features::empty();
3839 if available.contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM) {
3840 features |= wgpu::Features::TEXTURE_FORMAT_16BIT_NORM;
3841 }
3842 #[cfg(target_os = "linux")]
3843 {
3844 if available.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_FD) {
3845 features |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_FD;
3846 }
3847 if available.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF) {
3848 features |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF;
3849 }
3850 }
3851 features
3852 };
3853
3854 let (device, queue) = adapter
3855 .request_device(&wgpu::DeviceDescriptor {
3856 label: Some("repose-rs device"),
3857 required_features: features,
3858 required_limits: limits,
3859 experimental_features: wgpu::ExperimentalFeatures::disabled(),
3860 memory_hints: wgpu::MemoryHints::default(),
3861 trace: wgpu::Trace::Off,
3862 })
3863 .await
3864 .map_err(|e| anyhow::anyhow!("request_device failed: {e:?}"))?;
3865
3866 let device_lost = Arc::new(Mutex::new(None));
3867 {
3868 let device_lost = device_lost.clone();
3869 device.set_device_lost_callback(move |reason, message| {
3870 log::error!("wgpu device lost ({reason:?}): {message}");
3871 if let Ok(mut slot) = device_lost.lock() {
3872 *slot = Some((reason, message));
3873 }
3874 });
3875 }
3876
3877 let size = window.inner_size();
3878
3879 let caps = surface.get_capabilities(&adapter);
3880
3881 let Some(format) = caps.formats.first().copied() else {
3882 anyhow::bail!(
3883 "surface exposes no presentable formats (adapter {})",
3884 adapter.get_info().name
3885 );
3886 };
3887
3888 let (format, view_format) = if cfg!(target_arch = "wasm32")
3889 && adapter
3890 .get_downlevel_capabilities()
3891 .flags
3892 .contains(wgpu::DownlevelFlags::SURFACE_VIEW_FORMATS)
3893 {
3894 let non_srgb = caps
3895 .formats
3896 .iter()
3897 .copied()
3898 .find(|f| !f.is_srgb())
3899 .unwrap_or(format);
3900 (non_srgb, Some(non_srgb.add_srgb_suffix()))
3901 } else if cfg!(target_arch = "wasm32") {
3902 let fmt = caps
3903 .formats
3904 .iter()
3905 .copied()
3906 .find(|f| f.is_srgb())
3907 .unwrap_or(format);
3908 (fmt, None)
3909 } else {
3910 let fmt = caps
3911 .formats
3912 .iter()
3913 .copied()
3914 .find(|f| f.is_srgb())
3915 .unwrap_or(format);
3916 (fmt, None)
3917 };
3918
3919 let present_mode = pick_present_mode(&caps, present_mode);
3920 let alpha_mode = *caps
3921 .alpha_modes
3922 .first()
3923 .ok_or_else(|| anyhow::anyhow!("surface exposes no alpha modes"))?;
3924
3925 let render_format = view_format.unwrap_or(format);
3926 let msaa_samples = pick_surface_msaa(&adapter, render_format, msaa_samples);
3927 let mut renderer = WgpuSceneRenderer::try_from_device_with_adapter(
3928 &adapter,
3929 device,
3930 queue,
3931 render_format,
3932 msaa_samples,
3933 )?
3934 .with_surface_format(format)?;
3935 let (fw, fh) = clamp_output_size(
3936 size.width,
3937 size.height,
3938 renderer.device.limits().max_texture_dimension_2d,
3939 );
3940 renderer.resize(fw, fh);
3941
3942 let view_formats = view_format.into_iter().collect::<Vec<_>>();
3943
3944 let wanted_usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC;
3952 let surface_usage = wanted_usage & caps.usages;
3953 if surface_usage != wanted_usage {
3954 log::warn!(
3955 "surface {format:?} cannot be used as {:?}; swapchain-sourced blend snapshots \
3956 are skipped on this backend",
3957 wanted_usage - surface_usage
3958 );
3959 }
3960 let config = wgpu::SurfaceConfiguration {
3961 usage: surface_usage,
3962 format,
3963 width: fw.max(1),
3964 height: fh.max(1),
3965 present_mode,
3966 alpha_mode,
3967 color_space: wgpu::SurfaceColorSpace::Auto,
3968 view_formats,
3969 desired_maximum_frame_latency: 1,
3970 };
3971 surface.configure(&renderer.device, &config);
3972
3973 Ok(WgpuSurfaceBackend {
3974 #[cfg(feature = "winit-surface")]
3975 instance: Some(instance),
3976 #[cfg(feature = "winit-surface")]
3977 window: Some(window),
3978 surface: Some(surface),
3979 surface_config: Some(config),
3980 pending_reconfigure: false,
3981 renderer,
3982 device_lost,
3983 })
3984 }
3985
3986 #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
3987 pub fn new(window: Arc<winit::window::Window>) -> anyhow::Result<WgpuSurfaceBackend> {
3988 pollster::block_on(Self::new_async(window))
3989 }
3990
3991 #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
3992 pub fn new_with_msaa(
3993 window: Arc<winit::window::Window>,
3994 msaa_samples: u32,
3995 ) -> anyhow::Result<WgpuSurfaceBackend> {
3996 pollster::block_on(Self::new_async_with_msaa(window, msaa_samples))
3997 }
3998
3999 #[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
4000 pub fn new_with_options(
4001 window: Arc<winit::window::Window>,
4002 display: Option<winit::event_loop::OwnedDisplayHandle>,
4003 msaa_samples: u32,
4004 present_mode: PresentModePref,
4005 ) -> anyhow::Result<WgpuSurfaceBackend> {
4006 pollster::block_on(Self::new_async_with_options(
4007 window,
4008 display,
4009 msaa_samples,
4010 present_mode,
4011 ))
4012 }
4013
4014 #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
4015 pub fn new(_window: Arc<winit::window::Window>) -> anyhow::Result<WgpuSurfaceBackend> {
4016 anyhow::bail!("Use WgpuSurfaceBackend::new_async(window).await on wasm32")
4017 }
4018
4019 #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
4020 pub fn new_with_msaa(
4021 _window: Arc<winit::window::Window>,
4022 _msaa_samples: u32,
4023 ) -> anyhow::Result<WgpuSurfaceBackend> {
4024 anyhow::bail!("Use WgpuSurfaceBackend::new_async_with_msaa(window, msaa).await on wasm32")
4025 }
4026
4027 #[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
4028 pub fn new_with_options(
4029 _window: Arc<winit::window::Window>,
4030 _display: Option<winit::event_loop::OwnedDisplayHandle>,
4031 _msaa_samples: u32,
4032 _present_mode: PresentModePref,
4033 ) -> anyhow::Result<WgpuSurfaceBackend> {
4034 anyhow::bail!(
4035 "Use WgpuSurfaceBackend::new_async_with_options(window, display, msaa, mode).await on wasm32"
4036 )
4037 }
4038
4039 pub fn take_device_lost(&self) -> Option<String> {
4044 let (reason, message) = self.device_lost.lock().ok()?.take()?;
4045 (reason != wgpu::DeviceLostReason::Destroyed).then_some(message)
4046 }
4047}
4048
4049#[cfg(feature = "winit-surface")]
4052fn pick_present_mode(caps: &wgpu::SurfaceCapabilities, pref: PresentModePref) -> wgpu::PresentMode {
4053 let auto = || {
4054 caps.present_modes
4055 .iter()
4056 .copied()
4057 .find(|m| *m == wgpu::PresentMode::Fifo)
4058 .or_else(|| {
4059 caps.present_modes
4060 .iter()
4061 .copied()
4062 .find(|m| *m == wgpu::PresentMode::Mailbox)
4063 })
4064 .unwrap_or(wgpu::PresentMode::Immediate)
4065 };
4066 match pref {
4067 PresentModePref::Auto => auto(),
4068 PresentModePref::Fifo if caps.present_modes.contains(&wgpu::PresentMode::Fifo) => {
4069 wgpu::PresentMode::Fifo
4070 }
4071 PresentModePref::Mailbox if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) => {
4072 wgpu::PresentMode::Mailbox
4073 }
4074 PresentModePref::Immediate
4075 if caps.present_modes.contains(&wgpu::PresentMode::Immediate) =>
4076 {
4077 wgpu::PresentMode::Immediate
4078 }
4079 _ => auto(),
4080 }
4081}
4082
4083const VALID_SAMPLE_COUNTS: [u32; 5] = [1, 2, 4, 8, 16];
4086
4087fn validate_sample_count(label: &str, samples: u32) -> anyhow::Result<()> {
4088 if !VALID_SAMPLE_COUNTS.contains(&samples) {
4089 anyhow::bail!(
4090 "{label} must be one of {VALID_SAMPLE_COUNTS:?} (wgpu-supported sample counts), got {samples}"
4091 );
4092 }
4093 Ok(())
4094}
4095
4096fn validate_render_target_format(
4102 adapter: &wgpu::Adapter,
4103 output_format: wgpu::TextureFormat,
4104 samples: u32,
4105) -> anyhow::Result<()> {
4106 let features = adapter.get_texture_format_features(output_format);
4107 let flags = features.flags;
4108 for (needed, what) in [
4109 (wgpu::TextureUsages::RENDER_ATTACHMENT, "render attachment"),
4110 (wgpu::TextureUsages::TEXTURE_BINDING, "texture binding"),
4111 ] {
4112 if !features.allowed_usages.contains(needed) {
4113 anyhow::bail!("{output_format:?} cannot be used as a {what}");
4114 }
4115 }
4116 if !flags.contains(wgpu::TextureFormatFeatureFlags::BLENDABLE) {
4117 anyhow::bail!("{output_format:?} cannot be blended into");
4118 }
4119 if samples > 1 && !flags.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE) {
4120 anyhow::bail!(
4121 "{output_format:?} cannot resolve the requested {samples}x multisampled attachment"
4122 );
4123 }
4124 let ws = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba16Float);
4126 if !ws
4127 .allowed_usages
4128 .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
4129 || !ws
4130 .allowed_usages
4131 .contains(wgpu::TextureUsages::TEXTURE_BINDING)
4132 {
4133 anyhow::bail!(
4134 "{:?} cannot be used for linear working-space rendering",
4135 wgpu::TextureFormat::Rgba16Float
4136 );
4137 }
4138 Ok(())
4139}
4140
4141pub fn pick_surface_msaa(
4144 adapter: &wgpu::Adapter,
4145 format: wgpu::TextureFormat,
4146 requested: u32,
4147) -> u32 {
4148 pick_surface_msaa_for_mode(adapter, format, requested, false)
4149}
4150
4151pub fn pick_surface_msaa_for_mode(
4152 adapter: &wgpu::Adapter,
4153 format: wgpu::TextureFormat,
4154 requested: u32,
4155 working_space: bool,
4156) -> u32 {
4157 let requested = requested.max(1);
4158 let color_feat = adapter.get_texture_format_features(format);
4159 let working_space_feat = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba16Float);
4160 let depth_feat = adapter.get_texture_format_features(wgpu::TextureFormat::Depth24PlusStencil8);
4161 let supported = |n: u32| {
4162 color_feat
4163 .allowed_usages
4164 .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
4165 && color_feat.flags.sample_count_supported(n)
4166 && (!working_space
4167 || (working_space_feat
4168 .allowed_usages
4169 .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
4170 && working_space_feat.flags.sample_count_supported(n)
4171 && (n == 1
4172 || working_space_feat
4173 .flags
4174 .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE))))
4175 && (n == 1
4176 || color_feat
4177 .flags
4178 .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE))
4179 && depth_feat
4180 .allowed_usages
4181 .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
4182 && depth_feat.flags.sample_count_supported(n)
4183 };
4184 let mut candidates = vec![requested];
4185 for n in [8, 4, 2, 1] {
4186 if n < requested {
4187 candidates.push(n);
4188 }
4189 }
4190 let chosen = candidates.into_iter().find(|&n| supported(n)).unwrap_or(1);
4191 if chosen != requested {
4192 log::info!("requested MSAA x{requested}, using x{chosen}");
4193 }
4194 chosen
4195}
4196
4197impl WgpuSceneRenderer {
4198 pub fn set_image_from_bytes(
4201 &mut self,
4202 handle: u64,
4203 data: &[u8],
4204 srgb: bool,
4205 ) -> anyhow::Result<()> {
4206 validate_image_handle(handle)?;
4207 let max_dimension = self.device.limits().max_texture_dimension_2d;
4208 let (w, h, rgba) = decode_image_rgba8(data, max_dimension)?;
4209 self.set_image_rgba8(handle, w, h, &rgba, srgb)?;
4210 repose_core::set_image_intrinsic_size(handle, w, h);
4213 Ok(())
4214 }
4215
4216 pub fn set_image_rgba8(
4217 &mut self,
4218 handle: u64,
4219 w: u32,
4220 h: u32,
4221 rgba: &[u8],
4222 srgb: bool,
4223 ) -> anyhow::Result<()> {
4224 validate_image_handle(handle)?;
4225 validate_texture_dimensions(&self.device, w, h)?;
4226 let expected = checked_image_bytes_usize(w, h, 4)?;
4227 if rgba.len() < expected {
4228 return Err(anyhow::anyhow!(
4229 "RGBA buffer too small: {} < {}",
4230 rgba.len(),
4231 expected
4232 ));
4233 }
4234
4235 let format = if srgb {
4236 wgpu::TextureFormat::Rgba8UnormSrgb
4237 } else {
4238 wgpu::TextureFormat::Rgba8Unorm
4239 };
4240
4241 let needs_recreate = match self.images.get(&handle) {
4242 Some(ImageTex::Rgba {
4243 w: cw,
4244 h: ch,
4245 format: cf,
4246 ..
4247 }) => *cw != w || *ch != h || *cf != format,
4248 _ => true,
4249 };
4250
4251 if needs_recreate {
4252 self.remove_image(handle);
4253
4254 let (tex, binds) = self.create_rgba_tex(w, h, format);
4255 let bytes = checked_image_bytes(w, h, 4)?;
4256 self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
4257
4258 self.images.insert(
4259 handle,
4260 ImageTex::Rgba {
4261 tex,
4262 binds,
4263 w,
4264 h,
4265 format,
4266 last_used_frame: self.frame_index,
4267 bytes,
4268 },
4269 );
4270 }
4271
4272 if let Some(old) = self.retained.remove(&handle) {
4273 self.retained_bytes_total = self
4274 .retained_bytes_total
4275 .saturating_sub(old.rgba.len() as u64);
4276 }
4277 self.retained_bytes_total = self.retained_bytes_total.saturating_add(expected as u64);
4278 self.retained.insert(
4279 handle,
4280 RetainedImage {
4281 w,
4282 h,
4283 format,
4284 rgba: rgba[..expected].to_vec(),
4285 last_used_frame: self.frame_index,
4286 },
4287 );
4288 self.evict_retained_excess();
4289
4290 let tex = match self.images.get(&handle) {
4291 Some(ImageTex::Rgba { tex, .. }) => tex,
4292 _ => unreachable!(),
4293 };
4294
4295 self.queue.write_texture(
4296 wgpu::TexelCopyTextureInfo {
4297 texture: tex,
4298 mip_level: 0,
4299 origin: wgpu::Origin3d::ZERO,
4300 aspect: wgpu::TextureAspect::All,
4301 },
4302 &rgba[..expected],
4303 wgpu::TexelCopyBufferLayout {
4304 offset: 0,
4305 bytes_per_row: Some(4 * w),
4306 rows_per_image: Some(h),
4307 },
4308 wgpu::Extent3d {
4309 width: w,
4310 height: h,
4311 depth_or_array_layers: 1,
4312 },
4313 );
4314
4315 self.evict_budget_excess();
4317
4318 Ok(())
4319 }
4320
4321 fn create_image_binds(
4324 &self,
4325 view: &wgpu::TextureView,
4326 label: &str,
4327 sampler: Option<&wgpu::Sampler>,
4328 ) -> ImageBinds {
4329 let make = |name: &str, sampler: &wgpu::Sampler| {
4330 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4331 label: Some(name),
4332 layout: &self.image_bind_layout_rgba,
4333 entries: &[
4334 wgpu::BindGroupEntry {
4335 binding: 0,
4336 resource: wgpu::BindingResource::TextureView(view),
4337 },
4338 wgpu::BindGroupEntry {
4339 binding: 1,
4340 resource: wgpu::BindingResource::Sampler(sampler),
4341 },
4342 ],
4343 })
4344 };
4345 match sampler {
4346 Some(sampler) => {
4347 let bind = make(label, sampler);
4348 ImageBinds {
4349 linear: make(label, sampler),
4350 nearest: bind.clone(),
4351 tile_linear: bind.clone(),
4352 tile_nearest: bind,
4353 }
4354 }
4355 None => ImageBinds {
4356 linear: make(label, &self.image_sampler),
4357 nearest: make(label, &self.image_sampler_nearest),
4358 tile_linear: make(label, &self.image_sampler_repeat),
4359 tile_nearest: make(label, &self.image_sampler_repeat_nearest),
4360 },
4361 }
4362 }
4363
4364 fn create_nv12_binds(
4365 &self,
4366 view_y: &wgpu::TextureView,
4367 view_uv: &wgpu::TextureView,
4368 yuv_buf: &wgpu::Buffer,
4369 label: &str,
4370 ) -> ImageBinds {
4371 let make = |name: &str, sampler: &wgpu::Sampler| {
4372 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4373 label: Some(name),
4374 layout: &self.image_bind_layout_nv12,
4375 entries: &[
4376 wgpu::BindGroupEntry {
4377 binding: 0,
4378 resource: wgpu::BindingResource::TextureView(view_y),
4379 },
4380 wgpu::BindGroupEntry {
4381 binding: 1,
4382 resource: wgpu::BindingResource::TextureView(view_uv),
4383 },
4384 wgpu::BindGroupEntry {
4385 binding: 2,
4386 resource: wgpu::BindingResource::Sampler(sampler),
4387 },
4388 wgpu::BindGroupEntry {
4389 binding: 3,
4390 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
4391 buffer: yuv_buf,
4392 offset: 0,
4393 size: None,
4394 }),
4395 },
4396 ],
4397 })
4398 };
4399 ImageBinds {
4400 linear: make(label, &self.image_sampler),
4401 nearest: make(label, &self.image_sampler_nearest),
4402 tile_linear: make(label, &self.image_sampler_repeat),
4403 tile_nearest: make(label, &self.image_sampler_repeat_nearest),
4404 }
4405 }
4406
4407 fn create_rgba_tex(
4408 &self,
4409 w: u32,
4410 h: u32,
4411 format: wgpu::TextureFormat,
4412 ) -> (wgpu::Texture, ImageBinds) {
4413 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
4414 label: Some("user image rgba"),
4415 size: wgpu::Extent3d {
4416 width: w,
4417 height: h,
4418 depth_or_array_layers: 1,
4419 },
4420 mip_level_count: 1,
4421 sample_count: 1,
4422 dimension: wgpu::TextureDimension::D2,
4423 format,
4424 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4425 view_formats: &[],
4426 });
4427 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
4428 let binds = self.create_image_binds(&view, "image bind rgba", None);
4429 (tex, binds)
4430 }
4431
4432 fn validate_native_texture_view(
4434 &self,
4435 view: &wgpu::TextureView,
4436 width: u32,
4437 height: u32,
4438 ) -> anyhow::Result<()> {
4439 if width == 0 || height == 0 {
4440 anyhow::bail!("native texture dimensions must be non-zero");
4441 }
4442 let texture = view.texture();
4443 if texture.sample_count() != 1
4444 || texture.dimension() != wgpu::TextureDimension::D2
4445 || texture.depth_or_array_layers() != 1
4446 || texture.mip_level_count() == 0
4447 {
4448 anyhow::bail!("native texture must be a single-sample 2D texture with mip levels");
4449 }
4450 if width > self.device.limits().max_texture_dimension_2d
4451 || height > self.device.limits().max_texture_dimension_2d
4452 {
4453 anyhow::bail!("native texture dimensions exceed the device limit");
4454 }
4455 if !texture
4456 .usage()
4457 .contains(wgpu::TextureUsages::TEXTURE_BINDING)
4458 {
4459 anyhow::bail!("native texture lacks TEXTURE_BINDING usage");
4460 }
4461 if !native_format_supported(texture.format(), self.device.features()) {
4462 anyhow::bail!(
4463 "native texture format {:?} is not a filterable alpha color format",
4464 texture.format()
4465 );
4466 }
4467 if texture.width() < width || texture.height() < height {
4468 anyhow::bail!(
4469 "native texture is {}x{}, smaller than requested {width}x{height}",
4470 texture.width(),
4471 texture.height()
4472 );
4473 }
4474 Ok(())
4475 }
4476
4477 pub fn register_native_texture(
4478 &mut self,
4479 view: &wgpu::TextureView,
4480 width: u32,
4481 height: u32,
4482 ) -> u64 {
4483 if let Err(error) = self.validate_native_texture_view(view, width, height) {
4484 log::warn!("register_native_texture: {error:#}");
4485 return 0;
4486 }
4487 let handle = self.next_image_handle;
4488 self.next_image_handle += 1;
4489 let binds = self.create_image_binds(view, "user native image", None);
4490 self.images.insert(
4491 handle,
4492 ImageTex::User {
4493 binds,
4494 w: width,
4495 h: height,
4496 last_used_frame: self.frame_index,
4497 bytes: 0,
4498 },
4499 );
4500 handle
4501 }
4502
4503 pub fn register_native_texture_with_sampler(
4505 &mut self,
4506 view: &wgpu::TextureView,
4507 sampler_desc: wgpu::SamplerDescriptor<'_>,
4508 width: u32,
4509 height: u32,
4510 ) -> u64 {
4511 if let Err(error) = self.validate_native_texture_view(view, width, height) {
4512 log::warn!("register_native_texture: {error:#}");
4513 return 0;
4514 }
4515 if let Err(error) = validate_sampler_descriptor(&self.device, &sampler_desc) {
4516 log::warn!("register_native_texture: {error:#}");
4517 return 0;
4518 }
4519 let handle = self.next_image_handle;
4520 self.next_image_handle += 1;
4521 let sampler = self.device.create_sampler(&sampler_desc);
4522 let binds = self.create_image_binds(view, "user native image sampleropts", Some(&sampler));
4523 self.images.insert(
4524 handle,
4525 ImageTex::User {
4526 binds,
4527 w: width,
4528 h: height,
4529 last_used_frame: self.frame_index,
4530 bytes: 0,
4531 },
4532 );
4533 handle
4534 }
4535
4536 pub fn update_native_texture(&mut self, handle: u64, view: &wgpu::TextureView) {
4538 if handle == 0 {
4539 log::warn!("update_native_texture: reserved handle");
4540 return;
4541 }
4542 let Some((w, h)) = self.images.get(&handle).and_then(|entry| match entry {
4543 ImageTex::User { w, h, .. } => Some((*w, *h)),
4544 _ => None,
4545 }) else {
4546 log::warn!("update_native_texture: handle {handle} is not a native image");
4547 return;
4548 };
4549 if let Err(error) = self.validate_native_texture_view(view, w, h) {
4550 log::warn!("update_native_texture: {error:#}");
4551 return;
4552 }
4553 let binds = self.create_image_binds(view, "user native image update", None);
4554 if let Some(entry) = self.images.get_mut(&handle) {
4555 *entry = ImageTex::User {
4556 binds,
4557 w,
4558 h,
4559 last_used_frame: self.frame_index,
4560 bytes: 0,
4561 };
4562 }
4563 }
4564
4565 pub fn set_image_nv12(
4566 &mut self,
4567 handle: u64,
4568 w: u32,
4569 h: u32,
4570 y: &[u8],
4571 uv: &[u8],
4572 color_info: ColorInfo,
4573 ) -> anyhow::Result<()> {
4574 validate_image_handle(handle)?;
4575 validate_texture_dimensions(&self.device, w, h)?;
4576 if self
4577 .images
4578 .get(&handle)
4579 .is_some_and(|image| matches!(image, ImageTex::Nv12 { external: true, .. }))
4580 {
4581 anyhow::bail!("cannot overwrite an external DMA-BUF image");
4582 }
4583 let y_expected = checked_image_bytes_usize(w, h, 1)?;
4584 let uv_w = w.div_ceil(2);
4585 let uv_h = h.div_ceil(2);
4586 let uv_expected = checked_image_bytes_usize(uv_w, uv_h, 2)?;
4587
4588 if y.len() < y_expected {
4589 return Err(anyhow::anyhow!("Y plane too small"));
4590 }
4591 if uv.len() < uv_expected {
4592 return Err(anyhow::anyhow!("UV plane too small"));
4593 }
4594
4595 let needs_recreate = match self.images.get(&handle) {
4596 Some(ImageTex::Nv12 {
4597 w: ww,
4598 h: hh,
4599 pixel_format,
4600 ..
4601 }) => *ww != w || *hh != h || *pixel_format != PixelFormat::Nv12,
4602 _ => true,
4603 };
4604
4605 let yuv_raw = make_yuv_transform_raw(color_info, PixelFormat::Nv12);
4607
4608 if needs_recreate {
4609 self.remove_image(handle);
4610
4611 let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
4612 label: Some("nv12 Y"),
4613 size: wgpu::Extent3d {
4614 width: w,
4615 height: h,
4616 depth_or_array_layers: 1,
4617 },
4618 mip_level_count: 1,
4619 sample_count: 1,
4620 dimension: wgpu::TextureDimension::D2,
4621 format: wgpu::TextureFormat::R8Unorm,
4622 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4623 view_formats: &[],
4624 });
4625 let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
4626
4627 let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
4628 label: Some("nv12 UV"),
4629 size: wgpu::Extent3d {
4630 width: uv_w,
4631 height: uv_h,
4632 depth_or_array_layers: 1,
4633 },
4634 mip_level_count: 1,
4635 sample_count: 1,
4636 dimension: wgpu::TextureDimension::D2,
4637 format: wgpu::TextureFormat::Rg8Unorm,
4638 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4639 view_formats: &[],
4640 });
4641 let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
4642
4643 let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
4645 label: Some("nv12 yuv transform"),
4646 size: std::mem::size_of::<YuvTransformRaw>() as u64,
4647 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4648 mapped_at_creation: false,
4649 });
4650
4651 self.queue
4653 .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
4654
4655 let binds = self.create_nv12_binds(&view_y, &view_uv, &yuv_buf, "nv12 bind");
4656
4657 let bytes = checked_image_bytes(w, h, 1)?
4658 .checked_add(checked_image_bytes(uv_w, uv_h, 2)?)
4659 .and_then(|bytes| bytes.checked_add(std::mem::size_of::<YuvTransformRaw>() as u64))
4660 .ok_or_else(|| anyhow::anyhow!("NV12 image byte size overflow"))?;
4661 self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
4662
4663 self.images.insert(
4664 handle,
4665 ImageTex::Nv12 {
4666 tex_y,
4667 tex_uv,
4668 binds,
4669 yuv_buf,
4670 w,
4671 h,
4672 pixel_format: PixelFormat::Nv12,
4673 fourcc: Some(NV12_FOURCC),
4674 color_info,
4675 external: false,
4676 last_used_frame: self.frame_index,
4677 bytes,
4678 },
4679 );
4680 } else {
4681 if let Some(ImageTex::Nv12 {
4683 yuv_buf,
4684 color_info: stored_color,
4685 last_used_frame,
4686 ..
4687 }) = self.images.get_mut(&handle)
4688 {
4689 *stored_color = color_info;
4690 *last_used_frame = self.frame_index;
4691 self.queue
4692 .write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
4693 }
4694 }
4695
4696 let (tex_y, tex_uv, _binds) = match self.images.get(&handle) {
4697 Some(ImageTex::Nv12 {
4698 tex_y,
4699 tex_uv,
4700 binds,
4701 ..
4702 }) => (tex_y, tex_uv, binds),
4703 _ => return Err(anyhow::anyhow!("Handle is not NV12")),
4704 };
4705
4706 self.queue.write_texture(
4707 wgpu::TexelCopyTextureInfo {
4708 texture: tex_y,
4709 mip_level: 0,
4710 origin: wgpu::Origin3d::ZERO,
4711 aspect: wgpu::TextureAspect::All,
4712 },
4713 &y[..y_expected],
4714 wgpu::TexelCopyBufferLayout {
4715 offset: 0,
4716 bytes_per_row: Some(w),
4717 rows_per_image: Some(h),
4718 },
4719 wgpu::Extent3d {
4720 width: w,
4721 height: h,
4722 depth_or_array_layers: 1,
4723 },
4724 );
4725
4726 self.queue.write_texture(
4727 wgpu::TexelCopyTextureInfo {
4728 texture: tex_uv,
4729 mip_level: 0,
4730 origin: wgpu::Origin3d::ZERO,
4731 aspect: wgpu::TextureAspect::All,
4732 },
4733 &uv[..uv_expected],
4734 wgpu::TexelCopyBufferLayout {
4735 offset: 0,
4736 bytes_per_row: Some(2 * uv_w),
4737 rows_per_image: Some(uv_h),
4738 },
4739 wgpu::Extent3d {
4740 width: uv_w,
4741 height: uv_h,
4742 depth_or_array_layers: 1,
4743 },
4744 );
4745
4746 self.evict_budget_excess();
4747 Ok(())
4748 }
4749
4750 pub fn set_image_planes(
4751 &mut self,
4752 handle: u64,
4753 w: u32,
4754 h: u32,
4755 pixel_format: PixelFormat,
4756 planes: &[&[u8]],
4757 color_info: ColorInfo,
4758 ) -> anyhow::Result<()> {
4759 validate_image_handle(handle)?;
4760 if planes.len() != pixel_format.num_planes() as usize {
4761 anyhow::bail!(
4762 "{} requires {} planes, got {}",
4763 match pixel_format {
4764 PixelFormat::Nv12 => "NV12",
4765 PixelFormat::P010 => "P010",
4766 PixelFormat::I420 => "I420",
4767 PixelFormat::I444 => "I444",
4768 PixelFormat::Rgba => "RGBA",
4769 },
4770 pixel_format.num_planes(),
4771 planes.len()
4772 );
4773 }
4774 match pixel_format {
4775 PixelFormat::Nv12 => {
4776 let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
4777 let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
4778 self.set_image_nv12(handle, w, h, y, uv, color_info)
4779 }
4780 PixelFormat::P010 => {
4781 let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
4782 let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
4783 self.set_image_p010(handle, w, h, y, uv, color_info)
4784 }
4785 PixelFormat::I420 | PixelFormat::I444 => Err(anyhow::anyhow!(
4786 "I420/I444 not implemented and unlikely -> cheap to convert to NV12 (better for the GPU too)"
4787 )),
4788 PixelFormat::Rgba => {
4789 let rgba = planes
4790 .first()
4791 .ok_or(anyhow::anyhow!("missing RGBA plane"))?;
4792 self.set_image_rgba8(handle, w, h, rgba, false)
4793 }
4794 }
4795 }
4796
4797 fn set_image_p010(
4798 &mut self,
4799 handle: u64,
4800 w: u32,
4801 h: u32,
4802 y: &[u8],
4803 uv: &[u8],
4804 color_info: ColorInfo,
4805 ) -> anyhow::Result<()> {
4806 validate_image_handle(handle)?;
4807 validate_texture_dimensions(&self.device, w, h)?;
4808 if self
4809 .images
4810 .get(&handle)
4811 .is_some_and(|image| matches!(image, ImageTex::Nv12 { external: true, .. }))
4812 {
4813 anyhow::bail!("cannot overwrite an external DMA-BUF image");
4814 }
4815 if !self
4816 .device
4817 .features()
4818 .contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
4819 {
4820 anyhow::bail!("P010 requires TEXTURE_FORMAT_16BIT_NORM");
4821 }
4822 let uv_w = w.div_ceil(2);
4823 let uv_h = h.div_ceil(2);
4824
4825 let y_expected = checked_image_bytes_usize(w, h, 2)?;
4826 let uv_expected = checked_image_bytes_usize(uv_w, uv_h, 4)?;
4827
4828 if y.len() < y_expected {
4829 return Err(anyhow::anyhow!("P010 Y plane too small"));
4830 }
4831 if uv.len() < uv_expected {
4832 return Err(anyhow::anyhow!("P010 UV plane too small"));
4833 }
4834
4835 let needs_recreate = match self.images.get(&handle) {
4839 Some(ImageTex::Nv12 {
4840 w: ww,
4841 h: hh,
4842 pixel_format,
4843 ..
4844 }) => *ww != w || *hh != h || *pixel_format != PixelFormat::P010,
4845 _ => true,
4846 };
4847
4848 let yuv_raw = make_yuv_transform_raw(color_info, PixelFormat::P010);
4849
4850 if needs_recreate {
4851 self.remove_image(handle);
4852
4853 let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
4854 label: Some("p010 Y"),
4855 size: wgpu::Extent3d {
4856 width: w,
4857 height: h,
4858 depth_or_array_layers: 1,
4859 },
4860 mip_level_count: 1,
4861 sample_count: 1,
4862 dimension: wgpu::TextureDimension::D2,
4863 format: wgpu::TextureFormat::R16Unorm,
4864 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4865 view_formats: &[],
4866 });
4867 let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
4868
4869 let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
4870 label: Some("p010 UV"),
4871 size: wgpu::Extent3d {
4872 width: uv_w,
4873 height: uv_h,
4874 depth_or_array_layers: 1,
4875 },
4876 mip_level_count: 1,
4877 sample_count: 1,
4878 dimension: wgpu::TextureDimension::D2,
4879 format: wgpu::TextureFormat::Rg16Unorm,
4880 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4881 view_formats: &[],
4882 });
4883 let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
4884
4885 let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
4886 label: Some("p010 yuv transform"),
4887 size: std::mem::size_of::<YuvTransformRaw>() as u64,
4888 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4889 mapped_at_creation: false,
4890 });
4891 self.queue
4892 .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
4893
4894 let binds = self.create_nv12_binds(&view_y, &view_uv, &yuv_buf, "p010 bind");
4895
4896 let bytes = checked_image_bytes(w, h, 2)?
4897 .checked_add(checked_image_bytes(uv_w, uv_h, 4)?)
4898 .and_then(|bytes| bytes.checked_add(std::mem::size_of::<YuvTransformRaw>() as u64))
4899 .ok_or_else(|| anyhow::anyhow!("P010 image byte size overflow"))?;
4900 self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
4901
4902 self.images.insert(
4903 handle,
4904 ImageTex::Nv12 {
4905 tex_y,
4906 tex_uv,
4907 binds,
4908 yuv_buf,
4909 w,
4910 h,
4911 pixel_format: PixelFormat::P010,
4912 fourcc: Some(P010_FOURCC),
4913 color_info,
4914 external: false,
4915 last_used_frame: self.frame_index,
4916 bytes,
4917 },
4918 );
4919 } else {
4920 if let Some(ImageTex::Nv12 {
4921 yuv_buf,
4922 color_info: stored_color,
4923 last_used_frame,
4924 ..
4925 }) = self.images.get_mut(&handle)
4926 {
4927 *stored_color = color_info;
4928 *last_used_frame = self.frame_index;
4929 self.queue
4930 .write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
4931 }
4932 }
4933
4934 let (tex_y, tex_uv, _binds) = match self.images.get(&handle) {
4935 Some(ImageTex::Nv12 {
4936 tex_y,
4937 tex_uv,
4938 binds,
4939 ..
4940 }) => (tex_y, tex_uv, binds),
4941 _ => return Err(anyhow::anyhow!("Handle is not P010/NV12")),
4942 };
4943
4944 self.queue.write_texture(
4945 wgpu::TexelCopyTextureInfo {
4946 texture: tex_y,
4947 mip_level: 0,
4948 origin: wgpu::Origin3d::ZERO,
4949 aspect: wgpu::TextureAspect::All,
4950 },
4951 &y[..y_expected],
4952 wgpu::TexelCopyBufferLayout {
4953 offset: 0,
4954 bytes_per_row: Some(
4955 w.checked_mul(2)
4956 .ok_or_else(|| anyhow::anyhow!("P010 row size overflow"))?,
4957 ),
4958 rows_per_image: Some(h),
4959 },
4960 wgpu::Extent3d {
4961 width: w,
4962 height: h,
4963 depth_or_array_layers: 1,
4964 },
4965 );
4966 self.queue.write_texture(
4967 wgpu::TexelCopyTextureInfo {
4968 texture: tex_uv,
4969 mip_level: 0,
4970 origin: wgpu::Origin3d::ZERO,
4971 aspect: wgpu::TextureAspect::All,
4972 },
4973 &uv[..uv_expected],
4974 wgpu::TexelCopyBufferLayout {
4975 offset: 0,
4976 bytes_per_row: Some(
4977 uv_w.checked_mul(4)
4978 .ok_or_else(|| anyhow::anyhow!("P010 UV row size overflow"))?,
4979 ),
4980 rows_per_image: Some(uv_h),
4981 },
4982 wgpu::Extent3d {
4983 width: uv_w,
4984 height: uv_h,
4985 depth_or_array_layers: 1,
4986 },
4987 );
4988
4989 self.evict_budget_excess();
4990 Ok(())
4991 }
4992
4993 #[cfg(target_os = "linux")]
4994 pub fn set_image_dmabuf(
4995 &mut self,
4996 handle: u64,
4997 w: u32,
4998 h: u32,
4999 fds: Vec<std::os::unix::io::OwnedFd>,
5000 modifier: u64,
5001 strides: Vec<u32>,
5002 offsets: Vec<u64>,
5003 color_info: ColorInfo,
5004 ) -> anyhow::Result<()> {
5005 self.set_image_dmabuf_fourcc(
5006 handle,
5007 w,
5008 h,
5009 fds,
5010 NV12_FOURCC,
5011 modifier,
5012 strides,
5013 offsets,
5014 color_info,
5015 )
5016 }
5017
5018 #[cfg(target_os = "linux")]
5019 pub fn set_image_dmabuf_fourcc(
5020 &mut self,
5021 handle: u64,
5022 w: u32,
5023 h: u32,
5024 fds: Vec<std::os::unix::io::OwnedFd>,
5025 fourcc: u32,
5026 modifier: u64,
5027 strides: Vec<u32>,
5028 offsets: Vec<u64>,
5029 color_info: ColorInfo,
5030 ) -> anyhow::Result<()> {
5031 validate_image_handle(handle)?;
5032 checked_image_bytes(w, h, 1)?;
5033 if w == 0
5034 || h == 0
5035 || w > self.device.limits().max_texture_dimension_2d
5036 || h > self.device.limits().max_texture_dimension_2d
5037 {
5038 anyhow::bail!("DMA-BUF dimensions are outside the device texture limit");
5039 }
5040 if modifier == u64::MAX {
5041 anyhow::bail!("DMA-BUF modifier is invalid");
5042 }
5043 if fds.len() != 2 || strides.len() != 2 || offsets.len() != 2 {
5044 anyhow::bail!(
5045 "DMA-BUF import requires two fds, strides, and offsets (got {}, {}, {})",
5046 fds.len(),
5047 strides.len(),
5048 offsets.len()
5049 );
5050 }
5051 let pixel_format = match fourcc {
5052 0x3231_564e | 0x4e56_3132 => PixelFormat::Nv12,
5053 0x3031_3050 | 0x5030_3130 => PixelFormat::P010,
5054 _ => anyhow::bail!("unsupported DMA-BUF fourcc 0x{fourcc:08x}"),
5055 };
5056 if !self
5057 .device
5058 .features()
5059 .contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF)
5060 {
5061 anyhow::bail!("DMA-BUF import requires VULKAN_EXTERNAL_MEMORY_DMA_BUF");
5062 }
5063 if pixel_format == PixelFormat::P010
5064 && !self
5065 .device
5066 .features()
5067 .contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
5068 {
5069 anyhow::bail!("P010 DMA-BUF requires TEXTURE_FORMAT_16BIT_NORM");
5070 }
5071 let bytes_per_pixel = if pixel_format == PixelFormat::P010 {
5072 2
5073 } else {
5074 1
5075 };
5076 let uv_w = w.div_ceil(2);
5077 let uv_h = h.div_ceil(2);
5078 let y_file_size =
5079 validate_dmabuf_plane(&fds[0], offsets[0], strides[0], w, h, bytes_per_pixel)?;
5080 let uv_file_size = validate_dmabuf_plane(
5081 &fds[1],
5082 offsets[1],
5083 strides[1],
5084 uv_w,
5085 uv_h,
5086 bytes_per_pixel * 2,
5087 )?;
5088 let yuv_raw = make_yuv_transform_raw(color_info, pixel_format);
5089 let (y_format, uv_format) = if pixel_format == PixelFormat::P010 {
5090 (
5091 wgpu::TextureFormat::R16Unorm,
5092 wgpu::TextureFormat::Rg16Unorm,
5093 )
5094 } else {
5095 (wgpu::TextureFormat::R8Unorm, wgpu::TextureFormat::Rg8Unorm)
5096 };
5097 let hal_y_desc = wgpu::hal::TextureDescriptor {
5098 label: Some("dmabuf y"),
5099 size: wgpu::Extent3d {
5100 width: w,
5101 height: h,
5102 depth_or_array_layers: 1,
5103 },
5104 mip_level_count: 1,
5105 sample_count: 1,
5106 dimension: wgpu::TextureDimension::D2,
5107 format: y_format,
5108 usage: wgpu::wgt::TextureUses::RESOURCE,
5109 memory_flags: wgpu::hal::MemoryFlags::empty(),
5110 view_formats: vec![],
5111 };
5112 let hal_uv_desc = wgpu::hal::TextureDescriptor {
5113 label: Some("dmabuf uv"),
5114 size: wgpu::Extent3d {
5115 width: uv_w,
5116 height: uv_h,
5117 depth_or_array_layers: 1,
5118 },
5119 mip_level_count: 1,
5120 sample_count: 1,
5121 dimension: wgpu::TextureDimension::D2,
5122 format: uv_format,
5123 usage: wgpu::wgt::TextureUses::RESOURCE,
5124 memory_flags: wgpu::hal::MemoryFlags::empty(),
5125 view_formats: vec![],
5126 };
5127 let wgpu_y_desc = wgpu::TextureDescriptor {
5128 label: Some("dmabuf y"),
5129 size: hal_y_desc.size,
5130 mip_level_count: 1,
5131 sample_count: 1,
5132 dimension: wgpu::TextureDimension::D2,
5133 format: y_format,
5134 usage: wgpu::TextureUsages::TEXTURE_BINDING,
5135 view_formats: &[],
5136 };
5137 let wgpu_uv_desc = wgpu::TextureDescriptor {
5138 label: Some("dmabuf uv"),
5139 size: hal_uv_desc.size,
5140 mip_level_count: 1,
5141 sample_count: 1,
5142 dimension: wgpu::TextureDimension::D2,
5143 format: uv_format,
5144 usage: wgpu::TextureUsages::TEXTURE_BINDING,
5145 view_formats: &[],
5146 };
5147
5148 let (tex_y, view_y, tex_uv, view_uv) = unsafe {
5149 let hal_guard = self
5150 .device
5151 .as_hal::<wgpu::hal::vulkan::Api>()
5152 .ok_or_else(|| anyhow::anyhow!("device is not Vulkan"))?;
5153 let mut fds = fds.into_iter();
5154 let y_fd = fds.next().expect("validated fd count");
5155 let uv_fd = fds.next().expect("validated fd count");
5156 let y_texture = hal_guard.texture_from_dmabuf_fd(
5157 y_fd,
5158 &hal_y_desc,
5159 modifier,
5160 strides[0] as u64,
5161 offsets[0],
5162 );
5163 let y_texture =
5164 y_texture.map_err(|error| anyhow::anyhow!("import Y DMA-BUF: {error:?}"))?;
5165 let uv_texture = hal_guard.texture_from_dmabuf_fd(
5166 uv_fd,
5167 &hal_uv_desc,
5168 modifier,
5169 strides[1] as u64,
5170 offsets[1],
5171 );
5172 let uv_texture =
5173 uv_texture.map_err(|error| anyhow::anyhow!("import UV DMA-BUF: {error:?}"))?;
5174 drop(hal_guard);
5175 let tex_y = self
5176 .device
5177 .create_texture_from_hal::<wgpu::hal::vulkan::Api>(
5178 y_texture,
5179 &wgpu_y_desc,
5180 wgpu::wgt::TextureUses::UNINITIALIZED,
5181 );
5182 let tex_uv = self
5183 .device
5184 .create_texture_from_hal::<wgpu::hal::vulkan::Api>(
5185 uv_texture,
5186 &wgpu_uv_desc,
5187 wgpu::wgt::TextureUses::UNINITIALIZED,
5188 );
5189 let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
5190 let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
5191 (tex_y, view_y, tex_uv, view_uv)
5192 };
5193
5194 let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
5195 label: Some("dmabuf yuv transform"),
5196 size: std::mem::size_of::<YuvTransformRaw>() as u64,
5197 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
5198 mapped_at_creation: false,
5199 });
5200 self.queue
5201 .write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
5202 let binds = self.create_nv12_binds(&view_y, &view_uv, &yuv_buf, "dmabuf yuv bind");
5203 let bytes = y_file_size
5204 .checked_add(uv_file_size)
5205 .and_then(|value| value.checked_add(std::mem::size_of::<YuvTransformRaw>() as u64))
5206 .ok_or_else(|| anyhow::anyhow!("DMA-BUF byte size overflow"))?;
5207 self.evict_budget_excess();
5208 let replaced_bytes = self
5209 .images
5210 .get(&handle)
5211 .map(|image| match image {
5212 ImageTex::Rgba { bytes, .. }
5213 | ImageTex::Nv12 { bytes, .. }
5214 | ImageTex::User { bytes, .. } => *bytes,
5215 })
5216 .unwrap_or(0);
5217 let projected = self
5218 .image_bytes_total
5219 .saturating_sub(replaced_bytes)
5220 .checked_add(bytes)
5221 .ok_or_else(|| anyhow::anyhow!("DMA-BUF image budget overflow"))?;
5222 if projected > self.image_budget_bytes {
5223 anyhow::bail!("DMA-BUF image budget exceeded");
5224 }
5225 self.remove_image(handle);
5226 self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
5227 self.images.insert(
5228 handle,
5229 ImageTex::Nv12 {
5230 tex_y,
5231 tex_uv,
5232 binds,
5233 yuv_buf,
5234 w,
5235 h,
5236 pixel_format,
5237 fourcc: Some(canonical_fourcc(pixel_format)),
5238 color_info,
5239 external: true,
5240 last_used_frame: self.frame_index,
5241 bytes,
5242 },
5243 );
5244 self.evict_budget_excess();
5245 Ok(())
5246 }
5247
5248 pub fn image_fourcc(&self, handle: u64) -> Option<u32> {
5249 match self.images.get(&handle) {
5250 Some(ImageTex::Nv12 { fourcc, .. }) => *fourcc,
5251 _ => None,
5252 }
5253 }
5254
5255 pub fn remove_image(&mut self, handle: u64) {
5256 if let Some(img) = self.images.remove(&handle) {
5257 let b = match &img {
5258 ImageTex::Rgba { bytes, .. } => *bytes,
5259 ImageTex::Nv12 { bytes, .. } => *bytes,
5260 ImageTex::User { bytes, .. } => *bytes,
5261 };
5262 self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
5263 }
5264 if let Some(retained) = self.retained.remove(&handle) {
5265 self.retained_bytes_total = self
5266 .retained_bytes_total
5267 .saturating_sub(retained.rgba.len() as u64);
5268 }
5269 }
5270
5271 fn evict_image_gpu(&mut self, handle: u64) -> u64 {
5272 let Some(img) = self.images.remove(&handle) else {
5273 return 0;
5274 };
5275 let b = match &img {
5276 ImageTex::Rgba { bytes, .. } => *bytes,
5277 ImageTex::Nv12 { bytes, .. } => *bytes,
5278 ImageTex::User { bytes, .. } => *bytes,
5279 };
5280 self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
5281 b
5282 }
5283
5284 fn revive_retained_image(&mut self, handle: u64) -> anyhow::Result<bool> {
5285 if self.images.contains_key(&handle) {
5286 return Ok(true);
5287 }
5288 let Some(r) = self.retained.get_mut(&handle) else {
5289 return Ok(false);
5290 };
5291 r.last_used_frame = self.frame_index;
5292 let r = r.clone();
5293 let (tex, binds) = self.create_rgba_tex(r.w, r.h, r.format);
5294
5295 self.queue.write_texture(
5296 wgpu::TexelCopyTextureInfo {
5297 texture: &tex,
5298 mip_level: 0,
5299 origin: wgpu::Origin3d::ZERO,
5300 aspect: wgpu::TextureAspect::All,
5301 },
5302 &r.rgba,
5303 wgpu::TexelCopyBufferLayout {
5304 offset: 0,
5305 bytes_per_row: Some(4 * r.w),
5306 rows_per_image: Some(r.h),
5307 },
5308 wgpu::Extent3d {
5309 width: r.w,
5310 height: r.h,
5311 depth_or_array_layers: 1,
5312 },
5313 );
5314
5315 let bytes = checked_image_bytes(r.w, r.h, 4)?;
5316 self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
5317 self.images.insert(
5318 handle,
5319 ImageTex::Rgba {
5320 tex,
5321 binds,
5322 w: r.w,
5323 h: r.h,
5324 format: r.format,
5325 last_used_frame: self.frame_index,
5326 bytes,
5327 },
5328 );
5329 Ok(true)
5330 }
5331
5332 fn resolve_image_for_draw(&mut self, handle: u64) -> Option<(u32, u32, bool)> {
5333 if let Some(retained) = self.retained.get_mut(&handle) {
5334 retained.last_used_frame = self.frame_index;
5335 }
5336 if let Some(t) = self.images.get_mut(&handle) {
5337 return match t {
5338 ImageTex::Rgba {
5339 w,
5340 h,
5341 last_used_frame,
5342 ..
5343 } => {
5344 *last_used_frame = self.frame_index;
5345 Some((*w, *h, false))
5346 }
5347 ImageTex::User {
5348 w,
5349 h,
5350 last_used_frame,
5351 ..
5352 } => {
5353 *last_used_frame = self.frame_index;
5354 Some((*w, *h, false))
5355 }
5356 ImageTex::Nv12 {
5357 w,
5358 h,
5359 last_used_frame,
5360 ..
5361 } => {
5362 *last_used_frame = self.frame_index;
5363 Some((*w, *h, true))
5364 }
5365 };
5366 }
5367 if self.revive_retained_image(handle).unwrap_or(false)
5368 && let Some(ImageTex::Rgba {
5369 w,
5370 h,
5371 last_used_frame,
5372 ..
5373 }) = self.images.get_mut(&handle)
5374 {
5375 *last_used_frame = self.frame_index;
5376 return Some((*w, *h, false));
5377 }
5378 None
5379 }
5380
5381 pub fn register_image_from_bytes(&mut self, data: &[u8], srgb: bool) -> u64 {
5383 let handle = self.next_image_handle;
5384 self.next_image_handle += 1;
5385 if let Err(e) = self.set_image_from_bytes(handle, data, srgb) {
5386 log::error!("Failed to register image: {e}");
5387 }
5388 handle
5389 }
5390
5391 pub fn register_image_rgba8(&mut self, w: u32, h: u32, rgba: &[u8], srgb: bool) -> u64 {
5396 let handle = self.next_image_handle;
5397 self.next_image_handle += 1;
5398 if let Err(e) = self.set_image_rgba8(handle, w, h, rgba, srgb) {
5399 log::error!("Failed to register image: {e}");
5400 }
5401 handle
5402 }
5403
5404 pub fn register_coverage_a8(&mut self, w: u32, h: u32, coverage: &[u8]) -> u64 {
5410 if validate_texture_dimensions(&self.device, w, h).is_err() {
5411 log::error!("Coverage dimensions are outside the device limit");
5412 return 0;
5413 }
5414 let Ok(expected_bytes) = checked_image_bytes(w, h, 1) else {
5415 log::error!("Coverage dimensions overflow");
5416 return 0;
5417 };
5418 let Ok(expected) = usize::try_from(expected_bytes) else {
5419 log::error!("Coverage dimensions exceed addressable memory");
5420 return 0;
5421 };
5422 if coverage.len() < expected {
5423 log::error!("Coverage buffer too small: {} < {expected}", coverage.len());
5424 return 0;
5425 }
5426 let handle = self.next_coverage_handle;
5427 self.next_coverage_handle += 1;
5428
5429 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5430 label: Some("coverage tile a8"),
5431 size: wgpu::Extent3d {
5432 width: w,
5433 height: h,
5434 depth_or_array_layers: 1,
5435 },
5436 mip_level_count: 1,
5437 sample_count: 1,
5438 dimension: wgpu::TextureDimension::D2,
5439 format: wgpu::TextureFormat::R8Unorm,
5440 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
5441 view_formats: &[],
5442 });
5443 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
5444 let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
5445 label: Some("coverage bind a8"),
5446 layout: &self.image_bind_layout_rgba,
5447 entries: &[
5448 wgpu::BindGroupEntry {
5449 binding: 0,
5450 resource: wgpu::BindingResource::TextureView(&view),
5451 },
5452 wgpu::BindGroupEntry {
5453 binding: 1,
5454 resource: wgpu::BindingResource::Sampler(&self.image_sampler),
5455 },
5456 ],
5457 });
5458 self.queue.write_texture(
5459 wgpu::TexelCopyTextureInfo {
5460 texture: &tex,
5461 mip_level: 0,
5462 origin: wgpu::Origin3d::ZERO,
5463 aspect: wgpu::TextureAspect::All,
5464 },
5465 &coverage[..expected],
5466 wgpu::TexelCopyBufferLayout {
5467 offset: 0,
5468 bytes_per_row: Some(w),
5469 rows_per_image: Some(h),
5470 },
5471 wgpu::Extent3d {
5472 width: w,
5473 height: h,
5474 depth_or_array_layers: 1,
5475 },
5476 );
5477 let bytes = checked_image_bytes(w, h, 1).unwrap_or(u64::MAX);
5478 self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
5479 self.coverages.insert(
5480 handle,
5481 CoverageTex {
5482 tex,
5483 bind,
5484 w,
5485 h,
5486 last_used_frame: self.frame_index,
5487 bytes,
5488 },
5489 );
5490 self.evict_budget_excess();
5491 handle
5492 }
5493
5494 pub fn remove_coverage(&mut self, handle: u64) {
5496 if let Some(tile) = self.coverages.remove(&handle) {
5497 self.image_bytes_total = self.image_bytes_total.saturating_sub(tile.bytes);
5498 }
5499 }
5500
5501 pub fn coverage_dimensions(&mut self, handle: u64) -> Option<(u32, u32)> {
5504 if let Some(tile) = self.coverages.get_mut(&handle) {
5505 tile.last_used_frame = self.frame_index;
5506 return Some((tile.w, tile.h));
5507 }
5508 None
5509 }
5510
5511 fn evict_retained_excess(&mut self) {
5512 let mut candidates: Vec<(u64, u64, u64)> = self
5513 .retained
5514 .iter()
5515 .map(|(handle, image)| (*handle, image.last_used_frame, image.rgba.len() as u64))
5516 .collect();
5517 candidates.sort_by_key(|candidate| candidate.1);
5518 for (handle, _last_used, bytes) in candidates {
5519 if self.retained.len() <= MAX_RETAINED_IMAGES
5520 && self.retained_bytes_total <= MAX_RETAINED_IMAGE_BYTES
5521 {
5522 break;
5523 }
5524 if let Some(image) = self.retained.remove(&handle) {
5525 self.retained_bytes_total = self
5526 .retained_bytes_total
5527 .saturating_sub(bytes.min(image.rgba.len() as u64));
5528 }
5529 }
5530 }
5531
5532 fn evict_unused_images(&mut self) {
5533 let now = self.frame_index;
5534 let evict_after = self.image_evict_after_frames;
5535
5536 let mut to_evict = Vec::new();
5539 for (h, t) in self.images.iter() {
5540 if !t.evictable() {
5541 continue;
5542 }
5543 let last = match t {
5544 ImageTex::Rgba {
5545 last_used_frame, ..
5546 } => *last_used_frame,
5547 ImageTex::User {
5548 last_used_frame, ..
5549 } => *last_used_frame,
5550 ImageTex::Nv12 {
5551 last_used_frame, ..
5552 } => *last_used_frame,
5553 };
5554 if now.saturating_sub(last) > evict_after {
5555 to_evict.push(*h);
5556 }
5557 }
5558 for h in to_evict {
5559 if self.retained.contains_key(&h) {
5560 self.evict_image_gpu(h);
5561 } else {
5562 self.remove_image(h);
5563 }
5564 }
5565
5566 let mut stale = Vec::new();
5568 for (h, t) in self.coverages.iter() {
5569 if now.saturating_sub(t.last_used_frame) > evict_after {
5570 stale.push(*h);
5571 }
5572 }
5573 for h in stale {
5574 self.remove_coverage(h);
5575 }
5576
5577 self.evict_budget_excess();
5578 }
5579
5580 fn next_cache_touch(&mut self) -> u64 {
5581 let touch = self.cache_touch_clock;
5582 self.cache_touch_clock = self.cache_touch_clock.wrapping_add(1);
5583 touch
5584 }
5585
5586 fn gpu_bytes_total(&self) -> u64 {
5587 self.image_bytes_total
5588 .saturating_add(self.layer_bytes_total)
5589 .saturating_add(self.blend_snapshot_bytes_total)
5590 .saturating_add(self.mesh_cache_bytes_total)
5591 .saturating_add(self.working_space_bytes)
5592 .saturating_add(self.surface_resolve_bytes)
5593 .saturating_add(self.msaa_bytes)
5594 .saturating_add(self.ws_msaa_bytes)
5595 .saturating_add(self.depth_stencil_bytes)
5596 }
5597
5598 fn budget_exceeded(&self) -> bool {
5599 self.image_bytes_total > self.image_budget_bytes
5600 || self.gpu_bytes_total() > self.gpu_budget_bytes
5601 }
5602
5603 fn evict_budget_excess(&mut self) {
5604 let image_over_budget = self.image_bytes_total > self.image_budget_bytes;
5605 let gpu_over_budget = self.gpu_bytes_total() > self.gpu_budget_bytes;
5606 if !image_over_budget && !gpu_over_budget {
5607 return;
5608 }
5609 let mut coverage_candidates: Vec<(u64, u64)> = self
5610 .coverages
5611 .iter()
5612 .map(|(handle, tile)| (*handle, tile.last_used_frame))
5613 .collect();
5614 coverage_candidates.sort_by_key(|candidate| candidate.1);
5615 let now = self.frame_index;
5616 for (handle, last_used) in coverage_candidates {
5617 if !self.budget_exceeded() {
5618 break;
5619 }
5620 if last_used == now {
5621 continue;
5622 }
5623 self.remove_coverage(handle);
5624 }
5625 if !self.budget_exceeded() {
5626 return;
5627 }
5628 let mut candidates: Vec<(u64, u64, u64)> = self
5630 .images
5631 .iter()
5632 .filter_map(|(h, t)| {
5633 if !t.evictable() {
5634 return None;
5635 }
5636 let (last, bytes) = match t {
5637 ImageTex::Rgba {
5638 last_used_frame,
5639 bytes,
5640 ..
5641 } => (*last_used_frame, *bytes),
5642 ImageTex::User {
5643 last_used_frame,
5644 bytes,
5645 ..
5646 } => (*last_used_frame, *bytes),
5647 ImageTex::Nv12 {
5648 last_used_frame,
5649 bytes,
5650 ..
5651 } => (*last_used_frame, *bytes),
5652 };
5653 Some((*h, last, bytes))
5654 })
5655 .collect();
5656
5657 candidates.sort_by_key(|k| k.1);
5659
5660 for (h, last, _bytes) in candidates {
5661 if !self.budget_exceeded() {
5662 break;
5663 }
5664 if last == now {
5665 continue;
5666 }
5667 if self.retained.contains_key(&h) {
5668 self.evict_image_gpu(h);
5669 } else {
5670 self.remove_image(h);
5671 }
5672 }
5673 while self.gpu_bytes_total() > self.gpu_budget_bytes {
5674 if !self.evict_mesh_cache() {
5675 break;
5676 }
5677 }
5678 if self.gpu_bytes_total() > self.gpu_budget_bytes {
5679 log::warn!(
5680 "renderer GPU resource budget exceeded: {} > {}",
5681 self.gpu_bytes_total(),
5682 self.gpu_budget_bytes
5683 );
5684 }
5685 }
5686
5687 pub fn set_pixels_per_point(&mut self, ppp: f32) {
5689 if ppp.is_finite() {
5690 self.pixels_per_point = ppp.clamp(0.5, 8.0);
5691 }
5692 }
5693
5694 pub fn set_working_space(&mut self, enabled: bool) {
5698 if enabled == self.working_space {
5699 return;
5700 }
5701 self.working_space = enabled;
5702 if enabled {
5703 self.ensure_display_pipeline();
5704 self.recreate_msaa_and_depth_stencil();
5705 self.recreate_working_space_texture();
5706 } else {
5707 self.ws_tex = None;
5708 self.ws_view = None;
5709 self.ws_bind = None;
5710 self.ws_msaa_tex = None;
5711 self.ws_msaa_view = None;
5712 self.surface_resolve_tex = None;
5713 self.surface_resolve_view = None;
5714 self.surface_resolve_bytes = 0;
5715 self.working_space_bytes = 0;
5716 self.recreate_msaa_and_depth_stencil();
5717 }
5718 self.evict_budget_excess();
5719 }
5720
5721 fn ensure_display_pipeline(&mut self) {
5722 if self.display_pipeline.is_some() {
5723 return;
5724 }
5725
5726 let layout = self
5727 .device
5728 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
5729 label: Some("display transform layout"),
5730 entries: &[
5731 wgpu::BindGroupLayoutEntry {
5732 binding: 0,
5733 visibility: wgpu::ShaderStages::FRAGMENT,
5734 ty: wgpu::BindingType::Texture {
5735 multisampled: false,
5736 view_dimension: wgpu::TextureViewDimension::D2,
5737 sample_type: wgpu::TextureSampleType::Float { filterable: true },
5738 },
5739 count: None,
5740 },
5741 wgpu::BindGroupLayoutEntry {
5742 binding: 1,
5743 visibility: wgpu::ShaderStages::FRAGMENT,
5744 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
5745 count: None,
5746 },
5747 ],
5748 });
5749 self.display_layout = Some(layout);
5750
5751 let display_source: Cow<'static, str> = if self.output_format.is_srgb() {
5752 Cow::Borrowed(include_str!("shaders/display_transform_passthrough.wgsl"))
5753 } else {
5754 Cow::Borrowed(include_str!("shaders/display_transform.wgsl"))
5755 };
5756 let shader = self
5757 .device
5758 .create_shader_module(wgpu::ShaderModuleDescriptor {
5759 label: Some("display transform"),
5760 source: wgpu::ShaderSource::Wgsl(display_source),
5761 });
5762
5763 let pipeline_layout = self
5764 .device
5765 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
5766 label: Some("display transform pipeline layout"),
5767 bind_group_layouts: &[None, self.display_layout.as_ref()],
5768 immediate_size: 0,
5769 });
5770
5771 let pipeline = self
5772 .device
5773 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
5774 label: Some("display transform pipeline"),
5775 layout: Some(&pipeline_layout),
5776 vertex: wgpu::VertexState {
5777 module: &shader,
5778 entry_point: Some("vs_main"),
5779 buffers: &[],
5780 compilation_options: wgpu::PipelineCompilationOptions::default(),
5781 },
5782 fragment: Some(wgpu::FragmentState {
5783 module: &shader,
5784 entry_point: Some("fs_main"),
5785 targets: &[Some(wgpu::ColorTargetState {
5786 format: self.output_format,
5787 blend: None,
5788 write_mask: wgpu::ColorWrites::ALL,
5789 })],
5790 compilation_options: wgpu::PipelineCompilationOptions::default(),
5791 }),
5792 primitive: wgpu::PrimitiveState::default(),
5793 depth_stencil: None,
5794 multisample: wgpu::MultisampleState::default(),
5795 multiview_mask: None,
5796 cache: None,
5797 });
5798 self.display_pipeline = Some(pipeline);
5799 }
5800
5801 pub fn resize(&mut self, width: u32, height: u32) {
5806 let (width, height) =
5807 clamp_output_size(width, height, self.device.limits().max_texture_dimension_2d);
5808 if self.output_width == width && self.output_height == height {
5809 return;
5810 }
5811 self.output_width = width;
5812 self.output_height = height;
5813 if width == 0 || height == 0 {
5814 self.msaa_tex = None;
5815 self.msaa_view = None;
5816 self.ws_msaa_tex = None;
5817 self.ws_msaa_view = None;
5818 self.surface_resolve_tex = None;
5819 self.surface_resolve_view = None;
5820 self.surface_resolve_bytes = 0;
5821 self.msaa_bytes = 0;
5822 self.ws_msaa_bytes = 0;
5823 self.depth_stencil_bytes = 0;
5824 self.working_space_bytes = 0;
5825 self.ws_tex = None;
5826 self.ws_view = None;
5827 self.ws_bind = None;
5828 self.evict_budget_excess();
5829 return;
5830 }
5831 self.recreate_msaa_and_depth_stencil();
5832 self.recreate_working_space_texture();
5833 self.evict_budget_excess();
5834 }
5835
5836 fn recreate_working_space_texture(&mut self) {
5837 if !self.working_space || self.output_width == 0 || self.output_height == 0 {
5838 return;
5839 }
5840 self.working_space_bytes = 0;
5841 let w = self
5842 .output_width
5843 .clamp(1, self.device.limits().max_texture_dimension_2d);
5844 let h = self
5845 .output_height
5846 .clamp(1, self.device.limits().max_texture_dimension_2d);
5847
5848 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5849 label: Some("working space"),
5850 size: wgpu::Extent3d {
5851 width: w,
5852 height: h,
5853 depth_or_array_layers: 1,
5854 },
5855 mip_level_count: 1,
5856 sample_count: 1,
5857 dimension: wgpu::TextureDimension::D2,
5858 format: wgpu::TextureFormat::Rgba16Float,
5859 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
5860 | wgpu::TextureUsages::TEXTURE_BINDING
5861 | wgpu::TextureUsages::COPY_SRC
5862 | wgpu::TextureUsages::COPY_DST,
5863 view_formats: &[],
5864 });
5865 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
5866
5867 let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
5868 label: Some("working space bind"),
5869 layout: self.display_layout.as_ref().unwrap(),
5870 entries: &[
5871 wgpu::BindGroupEntry {
5872 binding: 0,
5873 resource: wgpu::BindingResource::TextureView(&view),
5874 },
5875 wgpu::BindGroupEntry {
5876 binding: 1,
5877 resource: wgpu::BindingResource::Sampler(&self.image_sampler),
5878 },
5879 ],
5880 });
5881
5882 self.working_space_bytes =
5883 texture_storage_bytes(wgpu::TextureFormat::Rgba16Float, w, h).unwrap_or(0);
5884 self.ws_tex = Some(tex);
5885 self.ws_view = Some(view);
5886 self.ws_bind = Some(bind);
5887 }
5888
5889 fn recreate_msaa_and_depth_stencil(&mut self) {
5890 if self.output_width == 0 || self.output_height == 0 {
5891 self.msaa_tex = None;
5892 self.msaa_view = None;
5893 self.ws_msaa_tex = None;
5894 self.ws_msaa_view = None;
5895 self.surface_resolve_tex = None;
5896 self.surface_resolve_view = None;
5897 self.surface_resolve_bytes = 0;
5898 self.msaa_bytes = 0;
5899 self.ws_msaa_bytes = 0;
5900 self.depth_stencil_bytes = 0;
5901 return;
5902 }
5903 self.msaa_bytes = 0;
5904 self.ws_msaa_bytes = 0;
5905 self.depth_stencil_bytes = 0;
5906 let aliased_surface = self.surface_format != self.output_format;
5907 let view_format_list = [self.output_format];
5908 let surface_view_formats: &[wgpu::TextureFormat] = if aliased_surface {
5909 &view_format_list
5910 } else {
5911 &[]
5912 };
5913 let surface_view = if aliased_surface {
5914 wgpu::TextureViewDescriptor {
5915 format: Some(self.output_format),
5916 ..Default::default()
5917 }
5918 } else {
5919 wgpu::TextureViewDescriptor::default()
5920 };
5921 if self.msaa_samples > 1 && !self.working_space {
5922 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5923 label: Some("msaa color"),
5924 size: wgpu::Extent3d {
5925 width: self.output_width.max(1),
5926 height: self.output_height.max(1),
5927 depth_or_array_layers: 1,
5928 },
5929 mip_level_count: 1,
5930 sample_count: self.msaa_samples,
5931 dimension: wgpu::TextureDimension::D2,
5932 format: self.surface_format,
5933 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
5934 view_formats: surface_view_formats,
5935 });
5936 let view = tex.create_view(&surface_view);
5937 self.msaa_bytes = texture_storage_bytes_with_samples(
5938 self.surface_format,
5939 self.output_width.max(1),
5940 self.output_height.max(1),
5941 self.msaa_samples,
5942 )
5943 .unwrap_or(0);
5944 self.msaa_tex = Some(tex);
5945 self.msaa_view = Some(view);
5946 } else {
5947 self.msaa_tex = None;
5948 self.msaa_view = None;
5949 }
5950 if self.working_space && self.working_space_msaa_samples > 1 {
5951 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5952 label: Some("working space msaa color"),
5953 size: wgpu::Extent3d {
5954 width: self.output_width.max(1),
5955 height: self.output_height.max(1),
5956 depth_or_array_layers: 1,
5957 },
5958 mip_level_count: 1,
5959 sample_count: self.working_space_msaa_samples,
5960 dimension: wgpu::TextureDimension::D2,
5961 format: wgpu::TextureFormat::Rgba16Float,
5962 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
5963 view_formats: &[],
5964 });
5965 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
5966 self.ws_msaa_bytes = texture_storage_bytes_with_samples(
5967 wgpu::TextureFormat::Rgba16Float,
5968 self.output_width.max(1),
5969 self.output_height.max(1),
5970 self.working_space_msaa_samples,
5971 )
5972 .unwrap_or(0);
5973 self.ws_msaa_tex = Some(tex);
5974 self.ws_msaa_view = Some(view);
5975 } else {
5976 self.ws_msaa_tex = None;
5977 self.ws_msaa_view = None;
5978 }
5979 self.surface_resolve_tex = None;
5980 self.surface_resolve_view = None;
5981 self.surface_resolve_bytes = 0;
5982 if self.msaa_samples > 1 && !self.working_space {
5983 let width = self.output_width.max(1);
5984 let height = self.output_height.max(1);
5985 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
5986 label: Some("surface resolve"),
5987 size: wgpu::Extent3d {
5988 width,
5989 height,
5990 depth_or_array_layers: 1,
5991 },
5992 mip_level_count: 1,
5993 sample_count: 1,
5994 dimension: wgpu::TextureDimension::D2,
5995 format: self.surface_format,
5996 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
5997 | wgpu::TextureUsages::TEXTURE_BINDING
5998 | wgpu::TextureUsages::COPY_SRC,
5999 view_formats: surface_view_formats,
6000 });
6001 let view = tex.create_view(&surface_view);
6002 self.surface_resolve_bytes =
6003 texture_storage_bytes(self.surface_format, width, height).unwrap_or(0);
6004 self.surface_resolve_tex = Some(tex);
6005 self.surface_resolve_view = Some(view);
6006 }
6007
6008 self.depth_stencil_bytes = texture_storage_bytes_with_samples(
6009 wgpu::TextureFormat::Depth24PlusStencil8,
6010 self.output_width.max(1),
6011 self.output_height.max(1),
6012 self.active_surface_msaa_samples(),
6013 )
6014 .unwrap_or(0);
6015 self.depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
6016 label: Some("depth-stencil (stencil clips)"),
6017 size: wgpu::Extent3d {
6018 width: self.output_width.max(1),
6019 height: self.output_height.max(1),
6020 depth_or_array_layers: 1,
6021 },
6022 mip_level_count: 1,
6023 sample_count: self.active_surface_msaa_samples(),
6024 dimension: wgpu::TextureDimension::D2,
6025 format: wgpu::TextureFormat::Depth24PlusStencil8,
6026 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
6027 view_formats: &[],
6028 });
6029 self.depth_stencil_view = self
6030 .depth_stencil_tex
6031 .create_view(&wgpu::TextureViewDescriptor::default());
6032 }
6033
6034 fn active_surface_msaa_samples(&self) -> u32 {
6035 if self.working_space {
6036 self.working_space_msaa_samples
6037 } else {
6038 self.msaa_samples
6039 }
6040 }
6041
6042 fn layer_target_format(&self) -> wgpu::TextureFormat {
6043 if self.working_space {
6044 wgpu::TextureFormat::Rgba16Float
6045 } else {
6046 self.output_format
6047 }
6048 }
6049
6050 fn get_or_create_layer(
6051 &mut self,
6052 layer_id: u32,
6053 width: u32,
6054 height: u32,
6055 rect: repose_core::Rect,
6056 transient: bool,
6057 ) -> bool {
6058 if width == 0
6059 || height == 0
6060 || width > self.device.limits().max_texture_dimension_2d
6061 || height > self.device.limits().max_texture_dimension_2d
6062 {
6063 return false;
6064 }
6065 let needs_alloc = match self.layer_pool.get(&layer_id) {
6066 Some(lt) => {
6067 lt.width != width || lt.height != height || lt.format != self.layer_target_format()
6068 }
6069 None => true,
6070 };
6071 if !needs_alloc {
6072 if let Some(lt) = self.layer_pool.get_mut(&layer_id) {
6073 lt.rect_px = (rect.x, rect.y, rect.w, rect.h);
6074 }
6075 return true;
6076 }
6077 let color_bytes =
6078 texture_storage_bytes(self.layer_target_format(), width.max(1), height.max(1))
6079 .unwrap_or(MAX_GRAPHICS_LAYER_BYTES + 1);
6080 let depth_bytes = checked_image_bytes(width.max(1), height.max(1), 4)
6081 .unwrap_or(MAX_GRAPHICS_LAYER_BYTES + 1);
6082 let bytes = color_bytes.saturating_add(depth_bytes);
6083 let old = self.layer_pool.get(&layer_id);
6084 let old_bytes = old.map_or(0, |layer| layer.bytes);
6085 let old_transient_bytes =
6086 old.map_or(0, |layer| if layer.transient { layer.bytes } else { 0 });
6087 let transient_count = self
6088 .layer_pool
6089 .values()
6090 .filter(|layer| layer.transient)
6091 .count();
6092 let transient_count = transient_count
6093 .saturating_sub(usize::from(old.is_some_and(|layer| layer.transient)))
6094 .saturating_add(usize::from(transient));
6095 let count = self.layer_pool.len() + usize::from(!self.layer_pool.contains_key(&layer_id));
6096 let total = self
6097 .layer_bytes_total
6098 .saturating_sub(old_bytes)
6099 .saturating_add(bytes);
6100 let projected_gpu = self
6101 .gpu_bytes_total()
6102 .saturating_sub(old_bytes)
6103 .saturating_add(bytes);
6104 let projected_transient = self
6105 .transient_layer_bytes_total
6106 .saturating_sub(old_transient_bytes)
6107 .saturating_add(if transient { bytes } else { 0 });
6108 if count > MAX_GRAPHICS_LAYERS
6109 || (transient
6110 && (transient_count > MAX_TRANSIENT_GRAPHICS_LAYERS
6111 || projected_transient > MAX_TRANSIENT_GRAPHICS_LAYER_BYTES))
6112 || bytes > MAX_GRAPHICS_LAYER_BYTES
6113 || total > MAX_GRAPHICS_LAYER_BYTES
6114 || projected_gpu > self.gpu_budget_bytes
6115 {
6116 log::warn!("graphics layer budget exhausted; layer {layer_id} skipped");
6117 if let Some(old) = self.layer_pool.remove(&layer_id) {
6118 self.layer_bytes_total = self.layer_bytes_total.saturating_sub(old.bytes);
6119 if old.transient {
6120 self.transient_layer_bytes_total =
6121 self.transient_layer_bytes_total.saturating_sub(old.bytes);
6122 }
6123 }
6124 return false;
6125 }
6126 if let Some(old) = self.layer_pool.remove(&layer_id) {
6127 self.layer_bytes_total = self.layer_bytes_total.saturating_sub(old.bytes);
6128 if old.transient {
6129 self.transient_layer_bytes_total =
6130 self.transient_layer_bytes_total.saturating_sub(old.bytes);
6131 }
6132 }
6133 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
6134 label: Some("graphics layer"),
6135 size: wgpu::Extent3d {
6136 width: width.max(1),
6137 height: height.max(1),
6138 depth_or_array_layers: 1,
6139 },
6140 mip_level_count: 1,
6141 sample_count: 1,
6142 dimension: wgpu::TextureDimension::D2,
6143 format: self.layer_target_format(),
6144 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
6145 | wgpu::TextureUsages::TEXTURE_BINDING
6146 | wgpu::TextureUsages::COPY_SRC,
6147 view_formats: &[],
6148 });
6149 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
6150 let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
6151 label: Some("layer bind"),
6152 layout: &self.image_bind_layout_rgba,
6153 entries: &[
6154 wgpu::BindGroupEntry {
6155 binding: 0,
6156 resource: wgpu::BindingResource::TextureView(&view),
6157 },
6158 wgpu::BindGroupEntry {
6159 binding: 1,
6160 resource: wgpu::BindingResource::Sampler(&self.layer_sampler),
6161 },
6162 ],
6163 });
6164 let bind_linear = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
6165 label: Some("layer bind linear"),
6166 layout: &self.image_bind_layout_rgba,
6167 entries: &[
6168 wgpu::BindGroupEntry {
6169 binding: 0,
6170 resource: wgpu::BindingResource::TextureView(&view),
6171 },
6172 wgpu::BindGroupEntry {
6173 binding: 1,
6174 resource: wgpu::BindingResource::Sampler(&self.layer_sampler_linear),
6175 },
6176 ],
6177 });
6178 let depth_stencil_view = self.create_layer_depth_stencil(width, height);
6179 self.layer_pool.insert(
6180 layer_id,
6181 LayerTarget {
6182 texture: tex,
6183 view,
6184 bind,
6185 bind_linear,
6186 depth_stencil_view,
6187 width,
6188 height,
6189 format: self.layer_target_format(),
6190 bytes,
6191 rect_px: (rect.x, rect.y, rect.w, rect.h),
6192 transient,
6193 },
6194 );
6195 self.layer_bytes_total = self.layer_bytes_total.saturating_add(bytes);
6196 if transient {
6197 self.transient_layer_bytes_total =
6198 self.transient_layer_bytes_total.saturating_add(bytes);
6199 }
6200 true
6201 }
6202
6203 fn acquire_blur_scratch(&mut self, layer_id: u32, slot: u32, pad: (f32, f32)) -> Option<u32> {
6213 let src = self.layer_pool.get(&layer_id)?;
6214 let width_f = (src.width as f32 + pad.0 * 2.0).ceil();
6215 let height_f = (src.height as f32 + pad.1 * 2.0).ceil();
6216 let max_dim = self.device.limits().max_texture_dimension_2d as f32;
6217 if !width_f.is_finite()
6218 || !height_f.is_finite()
6219 || width_f < 1.0
6220 || height_f < 1.0
6221 || width_f > max_dim
6222 || height_f > max_dim
6223 {
6224 log::warn!("blur scratch size {width_f}x{height_f} is invalid");
6225 return None;
6226 }
6227 let next = self.blur_scratch_next;
6228 let scratch_id = match self.blur_scratch_ids.get(&(layer_id, slot)) {
6229 Some(&id) => id,
6230 None => {
6231 let id = BLUR_SCRATCH_ID_BASE.wrapping_add(next);
6232 self.blur_scratch_next = next.wrapping_add(1);
6233 self.blur_scratch_ids.insert((layer_id, slot), id);
6234 id
6235 }
6236 };
6237 let allocated = self.get_or_create_layer(
6238 scratch_id,
6239 width_f as u32,
6240 height_f as u32,
6241 repose_core::Rect {
6242 x: 0.0,
6243 y: 0.0,
6244 w: width_f,
6245 h: height_f,
6246 },
6247 true,
6248 );
6249 allocated.then_some(scratch_id)
6250 }
6251
6252 #[allow(clippy::too_many_arguments)]
6271 fn emit_separable_blur(
6272 &mut self,
6273 source_layer: u32,
6274 slot: u32,
6275 sigma: [f32; 2],
6276 pad: (f32, f32),
6277 edge_mode: u32,
6278 alpha_only: bool,
6279 shape: [f32; 4],
6280 passes: &mut Vec<Pass>,
6281 current_pass: &mut Pass,
6282 next_pass_id: &mut u64,
6283 encoder: &mut wgpu::CommandEncoder,
6284 ) -> Option<u32> {
6285 let scratch_id = self.acquire_blur_scratch(source_layer, slot, pad)?;
6286 let (scratch_w, scratch_h, source_w, source_h) = {
6287 let scratch = self.layer_pool.get(&scratch_id)?;
6288 let source = self.layer_pool.get(&source_layer)?;
6289 (
6290 scratch.width,
6291 scratch.height,
6292 source.width as f32,
6293 source.height as f32,
6294 )
6295 };
6296 let uv = [
6302 -pad.0 / source_w,
6303 -pad.1 / source_h,
6304 1.0 + pad.0 / source_w,
6305 1.0 + pad.1 / source_h,
6306 ];
6307 let ndc = [-1.0, -1.0, 2.0, 2.0];
6308 let inst = BlurInstance {
6309 xywh: [ndc[0] + ndc[2] * 0.5, ndc[1] + ndc[3] * 0.5, ndc[2], ndc[3]],
6310 uv,
6311 color: [1.0, 1.0, 1.0, 1.0],
6312 sigma_px: sigma,
6313 fwd_mat: [1.0, 0.0, 0.0, 1.0],
6314 axis: 0,
6315 edge_mode,
6316 shape,
6317 };
6318 let cmd = match self.upload_blur_instance(&inst, encoder) {
6319 Some(off) if alpha_only => Cmd::CompositeShadow {
6320 off,
6321 cnt: 1,
6322 layer_id: source_layer,
6323 },
6324 Some(off) => Cmd::CompositeBlur {
6325 off,
6326 cnt: 1,
6327 layer_id: source_layer,
6328 },
6329 None => return None,
6330 };
6331 let target = current_pass.target;
6332 let scissor = current_pass.active_scissor;
6333 let resumed_pass_id = *next_pass_id;
6334 *next_pass_id += 1;
6335 let saved = std::mem::replace(
6336 current_pass,
6337 Pass {
6338 id: resumed_pass_id,
6339 target,
6340 initial_scissor: scissor.unwrap_or((0, 0, 1, 1)),
6341 active_scissor: scissor,
6342 clear_color: None,
6343 cmds: Vec::new(),
6344 },
6345 );
6346 passes.push(saved);
6347 let scratch_pass_id = *next_pass_id;
6348 *next_pass_id += 1;
6349 passes.push(Pass {
6350 id: scratch_pass_id,
6351 target: PassTarget::Layer(scratch_id),
6352 initial_scissor: (0, 0, scratch_w, scratch_h),
6353 active_scissor: Some((0, 0, scratch_w, scratch_h)),
6354 clear_color: Some([0.0, 0.0, 0.0, 0.0]),
6355 cmds: vec![cmd],
6356 });
6357 Some(scratch_id)
6358 }
6359
6360 fn upload_blur_instance(
6361 &mut self,
6362 inst: &BlurInstance,
6363 encoder: &mut wgpu::CommandEncoder,
6364 ) -> Option<u64> {
6365 self.blur_ring
6366 .grow_to_fit(
6367 &self.device,
6368 encoder,
6369 std::mem::size_of::<BlurInstance>() as u64,
6370 )
6371 .ok()?;
6372 self.blur_ring
6373 .alloc_write(&self.queue, bytemuck::bytes_of(inst))
6374 .ok()
6375 }
6376 fn create_layer_depth_stencil(&self, width: u32, height: u32) -> wgpu::TextureView {
6377 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
6378 label: Some("layer depth-stencil"),
6379 size: wgpu::Extent3d {
6380 width: width.max(1),
6381 height: height.max(1),
6382 depth_or_array_layers: 1,
6383 },
6384 mip_level_count: 1,
6385 sample_count: 1,
6386 dimension: wgpu::TextureDimension::D2,
6387 format: wgpu::TextureFormat::Depth24PlusStencil8,
6388 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
6389 view_formats: &[],
6390 });
6391 tex.create_view(&wgpu::TextureViewDescriptor::default())
6392 }
6393
6394 fn ensure_globals_staging(&mut self, pass_count: usize) -> bool {
6395 let size = std::mem::size_of::<Globals>() as u64;
6396 let needed = match (pass_count.max(1) as u64).checked_mul(size) {
6397 Some(needed) => needed,
6398 None => return false,
6399 };
6400 if needed <= self.globals_staging_cap {
6401 return true;
6402 }
6403 let cap = needed
6404 .next_power_of_two()
6405 .min(self.device.limits().max_buffer_size);
6406 if cap < needed {
6407 return false;
6408 }
6409 self.globals_staging = self.device.create_buffer(&wgpu::BufferDescriptor {
6410 label: Some("globals staging"),
6411 size: cap,
6412 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
6413 mapped_at_creation: false,
6414 });
6415 self.globals_staging_cap = cap;
6416 true
6417 }
6418
6419 fn reset_atlas_storage(&mut self) {
6420 self.atlas_mask = init_atlas_mask(&self.device);
6421 self.atlas_color = init_atlas_color(&self.device);
6422 self.atlas_mask_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
6423 label: Some("atlas mask bind"),
6424 layout: &self.text_bind_layout,
6425 entries: &[
6426 wgpu::BindGroupEntry {
6427 binding: 0,
6428 resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
6429 },
6430 wgpu::BindGroupEntry {
6431 binding: 1,
6432 resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
6433 },
6434 ],
6435 });
6436 self.atlas_color_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
6437 label: Some("atlas color bind"),
6438 layout: &self.text_bind_layout,
6439 entries: &[
6440 wgpu::BindGroupEntry {
6441 binding: 0,
6442 resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
6443 },
6444 wgpu::BindGroupEntry {
6445 binding: 1,
6446 resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
6447 },
6448 ],
6449 });
6450 self.atlas_mask_pending = AtlasPending::default();
6451 self.atlas_color_pending = AtlasPending::default();
6452 self.atlas_mask_deferred.clear();
6453 self.atlas_color_deferred.clear();
6454 }
6455
6456 fn apply_atlas_pending(&mut self) {
6461 if self.atlas_mask_pending.grow {
6462 self.atlas_mask_pending.grow = false;
6463 let size = (self.atlas_mask.size * 2).min(MAX_GLYPH_ATLAS_SIZE);
6464 self.repack_mask(size);
6465 }
6466 if self.atlas_color_pending.grow {
6467 self.atlas_color_pending.grow = false;
6468 let size = (self.atlas_color.size * 2).min(MAX_GLYPH_ATLAS_SIZE);
6469 self.repack_color(size);
6470 }
6471 if self.frame_index.wrapping_sub(self.atlas_last_compact_frame)
6472 < ATLAS_COMPACT_MIN_FRAME_INTERVAL
6473 {
6474 return;
6475 }
6476 if self.atlas_mask_pending.compact && self.compact_atlas_mask() {
6479 self.atlas_mask_pending.compact = false;
6480 }
6481 if self.atlas_color_pending.compact && self.compact_atlas_color() {
6482 self.atlas_color_pending.compact = false;
6483 }
6484 }
6485
6486 fn compact_atlas_mask(&mut self) -> bool {
6491 let target = self.atlas_mask.used_area / ATLAS_COMPACT_KEEP_DEN * ATLAS_COMPACT_KEEP_NUM;
6492 if !evict_atlas_lru(
6493 &mut self.atlas_mask.map,
6494 &mut self.atlas_mask.used_area,
6495 self.frame_index,
6496 target,
6497 ) {
6498 return false;
6499 }
6500 let size = self.atlas_mask.size;
6501 self.repack_mask(size);
6502 self.atlas_last_compact_frame = self.frame_index;
6503 true
6504 }
6505
6506 fn compact_atlas_color(&mut self) -> bool {
6507 let target = self.atlas_color.used_area / ATLAS_COMPACT_KEEP_DEN * ATLAS_COMPACT_KEEP_NUM;
6508 if !evict_atlas_lru(
6509 &mut self.atlas_color.map,
6510 &mut self.atlas_color.used_area,
6511 self.frame_index,
6512 target,
6513 ) {
6514 return false;
6515 }
6516 let size = self.atlas_color.size;
6517 self.repack_color(size);
6518 self.atlas_last_compact_frame = self.frame_index;
6519 true
6520 }
6521
6522 fn repack_mask(&mut self, new_size: u32) {
6526 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
6527 label: Some("glyph atlas A8 (repacked)"),
6528 size: wgpu::Extent3d {
6529 width: new_size,
6530 height: new_size,
6531 depth_or_array_layers: 1,
6532 },
6533 mip_level_count: 1,
6534 sample_count: 1,
6535 dimension: wgpu::TextureDimension::D2,
6536 format: wgpu::TextureFormat::R8Unorm,
6537 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
6538 view_formats: &[],
6539 });
6540 self.atlas_mask.tex = tex;
6541 self.atlas_mask.view = self
6542 .atlas_mask
6543 .tex
6544 .create_view(&wgpu::TextureViewDescriptor::default());
6545 self.atlas_mask.size = new_size;
6546 self.atlas_mask.next_x = 1;
6547 self.atlas_mask.next_y = 1;
6548 self.atlas_mask.row_h = 0;
6549 self.atlas_mask.used_area = 0;
6550 self.atlas_mask_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
6551 label: Some("atlas mask bind"),
6552 layout: &self.text_bind_layout,
6553 entries: &[
6554 wgpu::BindGroupEntry {
6555 binding: 0,
6556 resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
6557 },
6558 wgpu::BindGroupEntry {
6559 binding: 1,
6560 resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
6561 },
6562 ],
6563 });
6564 let survivors: Vec<_> = self.atlas_mask.map.drain().collect();
6565 self.atlas_mask_deferred.clear();
6566 for (key, previous) in survivors {
6567 let _ = self.upload_glyph_mask(key.clone(), f32::from_bits(key.font_size_bits));
6568 if let Some(info) = self.atlas_mask.map.get_mut(&key) {
6569 info.last_touch = previous.last_touch;
6570 info.last_used_frame = previous.last_used_frame;
6571 }
6572 }
6573 }
6574
6575 fn repack_color(&mut self, new_size: u32) {
6577 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
6578 label: Some("glyph atlas RGBA (repacked)"),
6579 size: wgpu::Extent3d {
6580 width: new_size,
6581 height: new_size,
6582 depth_or_array_layers: 1,
6583 },
6584 mip_level_count: 1,
6585 sample_count: 1,
6586 dimension: wgpu::TextureDimension::D2,
6587 format: wgpu::TextureFormat::Rgba8UnormSrgb,
6588 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
6589 view_formats: &[],
6590 });
6591 self.atlas_color.tex = tex;
6592 self.atlas_color.view = self
6593 .atlas_color
6594 .tex
6595 .create_view(&wgpu::TextureViewDescriptor::default());
6596 self.atlas_color.size = new_size;
6597 self.atlas_color.next_x = 1;
6598 self.atlas_color.next_y = 1;
6599 self.atlas_color.row_h = 0;
6600 self.atlas_color.used_area = 0;
6601 self.atlas_color_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
6602 label: Some("atlas color bind"),
6603 layout: &self.text_bind_layout,
6604 entries: &[
6605 wgpu::BindGroupEntry {
6606 binding: 0,
6607 resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
6608 },
6609 wgpu::BindGroupEntry {
6610 binding: 1,
6611 resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
6612 },
6613 ],
6614 });
6615 let survivors: Vec<_> = self.atlas_color.map.drain().collect();
6616 self.atlas_color_deferred.clear();
6617 for (key, previous) in survivors {
6618 let _ = self.upload_glyph_color(key.clone(), f32::from_bits(key.font_size_bits));
6619 if let Some(info) = self.atlas_color.map.get_mut(&key) {
6620 info.last_touch = previous.last_touch;
6621 info.last_used_frame = previous.last_used_frame;
6622 }
6623 }
6624 }
6625
6626 fn sync_text_caches(&mut self) {
6627 let generation = repose_text::font_generation();
6628 if generation == self.atlas_generation {
6629 return;
6630 }
6631 self.atlas_generation = generation;
6632 self.reset_atlas_storage();
6633 self.atlas_mask_failures.clear();
6634 self.atlas_color_failures.clear();
6635 self.glyph_outline_cache.clear();
6636 self.legacy_shaped_text_cache.clear();
6637 self.slug_draw_cache.clear();
6638 self.slug_draw_cache_bytes_total = 0;
6639 self.slug_cache = slug::GlyphSlugCache::new();
6640 }
6641
6642 fn shape_text_cached(
6643 &mut self,
6644 text: &Arc<str>,
6645 px: f32,
6646 line_height_ratio: f32,
6647 font_family: Option<&'static str>,
6648 font_weight: u16,
6649 font_style: u8,
6650 letter_spacing: f32,
6651 variation: Option<&Arc<str>>,
6652 ) -> Arc<repose_text::SharedShapedText> {
6653 repose_text::shape_line_cached(
6654 text.as_ref(),
6655 px,
6656 line_height_ratio,
6657 font_family,
6658 font_weight,
6659 font_style,
6660 letter_spacing,
6661 variation.map(AsRef::as_ref),
6662 )
6663 }
6664
6665 fn legacy_shape_text_cached(
6666 &mut self,
6667 text: &Arc<str>,
6668 px: f32,
6669 line_height_ratio: f32,
6670 font_family: Option<&'static str>,
6671 font_weight: u16,
6672 font_style: u8,
6673 letter_spacing: f32,
6674 variation: Option<&Arc<str>>,
6675 ) -> Arc<[repose_text::ShapedGlyph]> {
6676 let key = TextShapeKey {
6677 text: text.clone(),
6678 px_bits: px.to_bits(),
6679 line_height_bits: line_height_ratio.to_bits(),
6680 font_family,
6681 font_weight,
6682 font_style,
6683 letter_spacing_bits: letter_spacing.to_bits(),
6684 variation: variation.cloned(),
6685 font_generation: self.atlas_generation,
6686 };
6687 if let Some(entry) = self.legacy_shaped_text_cache.get_mut(&key) {
6688 entry.last_used_frame = self.frame_index;
6689 return entry.glyphs.clone();
6690 }
6691 let glyphs: Arc<[repose_text::ShapedGlyph]> = Arc::from(repose_text::shape_line(
6692 text.as_ref(),
6693 px,
6694 line_height_ratio,
6695 font_family,
6696 font_weight,
6697 font_style,
6698 letter_spacing,
6699 variation.map(AsRef::as_ref),
6700 ));
6701 self.legacy_shaped_text_cache.insert(
6702 key,
6703 TextShapeCacheEntry {
6704 glyphs: glyphs.clone(),
6705 last_used_frame: self.frame_index,
6706 },
6707 );
6708 while self.legacy_shaped_text_cache.len() > MAX_LEGACY_SHAPED_TEXT_CACHE_ENTRIES {
6709 let Some(victim) = self
6710 .legacy_shaped_text_cache
6711 .iter()
6712 .min_by_key(|(_, entry)| entry.last_used_frame)
6713 .map(|(key, _)| key.clone())
6714 else {
6715 break;
6716 };
6717 self.legacy_shaped_text_cache.remove(&victim);
6718 }
6719 glyphs
6720 }
6721
6722 fn slug_draw_key(
6723 cache_key: &repose_text::CacheKey,
6724 fill: bool,
6725 stroke_key: Option<&slug::StrokeTessKey>,
6726 transform: &Transform,
6727 origin: (f32, f32),
6728 px: f32,
6729 target: (f32, f32),
6730 color: [f32; 4],
6731 ) -> SlugDrawKey {
6732 let linear = transform.linear();
6733 SlugDrawKey {
6734 cache_key: cache_key.clone(),
6735 fill,
6736 stroke: stroke_key.cloned(),
6737 transform: [
6738 linear[0].to_bits(),
6739 linear[1].to_bits(),
6740 linear[2].to_bits(),
6741 linear[3].to_bits(),
6742 transform.scale_x.to_bits(),
6743 transform.scale_y.to_bits(),
6744 transform.translate_x.to_bits(),
6745 transform.translate_y.to_bits(),
6746 transform.rotate.to_bits(),
6747 transform.shear_x.to_bits(),
6748 transform.shear_y.to_bits(),
6749 ],
6750 origin: [origin.0.to_bits(), origin.1.to_bits()],
6751 px_bits: px.to_bits(),
6752 target: [target.0.to_bits(), target.1.to_bits()],
6753 color: color.map(f32::to_bits),
6754 }
6755 }
6756
6757 fn evict_slug_draw_cache(&mut self) -> bool {
6758 let victim = self
6759 .slug_draw_cache
6760 .iter()
6761 .filter(|(_, entry)| entry.last_used_frame != self.frame_index)
6762 .min_by_key(|(_, entry)| entry.last_touch_frame)
6763 .map(|(key, _)| key.clone());
6764 let Some(victim) = victim else {
6765 return false;
6766 };
6767 let Some(entry) = self.slug_draw_cache.remove(&victim) else {
6768 return false;
6769 };
6770 self.slug_draw_cache_bytes_total =
6771 self.slug_draw_cache_bytes_total.saturating_sub(entry.bytes);
6772 true
6773 }
6774
6775 fn reserve_slug_draw_cache(&mut self, bytes: u64) -> bool {
6776 while self.slug_draw_cache.len() >= MAX_SLUG_DRAW_CACHE_ENTRIES
6777 || self.slug_draw_cache_bytes_total.saturating_add(bytes) > MAX_SLUG_DRAW_CACHE_BYTES
6778 {
6779 if !self.evict_slug_draw_cache() {
6780 return false;
6781 }
6782 }
6783 true
6784 }
6785
6786 fn get_cached_slug_vertices(&mut self, key: &SlugDrawKey) -> Option<Arc<[slug::TessVertex]>> {
6787 let touch = self.next_cache_touch();
6788 self.slug_draw_cache.get_mut(key).map(|entry| {
6789 entry.last_used_frame = self.frame_index;
6790 entry.last_touch_frame = touch;
6791 entry.vertices.clone()
6792 })
6793 }
6794
6795 fn cached_slug_vertices(
6796 &mut self,
6797 key: SlugDrawKey,
6798 vertices: Vec<slug::TessVertex>,
6799 ) -> Arc<[slug::TessVertex]> {
6800 let touch = self.next_cache_touch();
6801 if let Some(entry) = self.slug_draw_cache.get_mut(&key) {
6802 entry.last_used_frame = self.frame_index;
6803 entry.last_touch_frame = touch;
6804 return entry.vertices.clone();
6805 }
6806 let bytes = slug_draw_cache_entry_bytes(vertices.len());
6807 if bytes > MAX_SLUG_DRAW_CACHE_BYTES || !self.reserve_slug_draw_cache(bytes) {
6808 return Arc::from(vertices);
6809 }
6810 let touch = self.next_cache_touch();
6811 let vertices: Arc<[slug::TessVertex]> = Arc::from(vertices);
6812 self.slug_draw_cache.insert(
6813 key,
6814 SlugDrawCacheEntry {
6815 vertices: vertices.clone(),
6816 bytes,
6817 last_used_frame: self.frame_index,
6818 last_touch_frame: touch,
6819 },
6820 );
6821 self.slug_draw_cache_bytes_total = self.slug_draw_cache_bytes_total.saturating_add(bytes);
6822 vertices
6823 }
6824
6825 fn cached_glyph_outline(
6826 &mut self,
6827 cache_key: &repose_text::CacheKey,
6828 ) -> Option<Arc<[repose_text::Command]>> {
6829 let touch = self.next_cache_touch();
6830 if let Some(entry) = self.glyph_outline_cache.get_mut(cache_key) {
6831 entry.last_touch = touch;
6832 return entry.commands.clone();
6833 }
6834 let commands = repose_text::extract_outline_commands(cache_key).map(Arc::from);
6835 if self.glyph_outline_cache.len() >= MAX_GLYPH_OUTLINE_CACHE_ENTRIES {
6836 self.trim_glyph_outline_cache();
6837 }
6838 self.glyph_outline_cache.insert(
6839 cache_key.clone(),
6840 GlyphOutlineEntry {
6841 commands: commands.clone(),
6842 last_touch: touch,
6843 },
6844 );
6845 commands
6846 }
6847
6848 fn trim_glyph_outline_cache(&mut self) {
6851 let target = (MAX_GLYPH_OUTLINE_CACHE_ENTRIES / ATLAS_COMPACT_KEEP_DEN as usize)
6852 * ATLAS_COMPACT_KEEP_NUM as usize;
6853 let mut victims: Vec<(u64, repose_text::CacheKey)> = self
6854 .glyph_outline_cache
6855 .iter()
6856 .map(|(key, entry)| (entry.last_touch, key.clone()))
6857 .collect();
6858 victims.sort_unstable_by_key(|victim| victim.0);
6859 for (_, key) in victims {
6860 if self.glyph_outline_cache.len() <= target {
6861 break;
6862 }
6863 self.glyph_outline_cache.remove(&key);
6864 }
6865 }
6866
6867 fn atlas_bind_group_mask(&self) -> wgpu::BindGroup {
6868 self.atlas_mask_bind.clone()
6869 }
6870
6871 fn atlas_bind_group_color(&self) -> wgpu::BindGroup {
6872 self.atlas_color_bind.clone()
6873 }
6874
6875 fn mark_atlas_mask_failure(&mut self, key: repose_text::CacheKey) {
6876 if self.atlas_mask_failures.len() >= MAX_GLYPH_ATLAS_ENTRIES {
6877 self.atlas_mask_failures.clear();
6878 }
6879 self.atlas_mask_failures.insert(key);
6880 }
6881
6882 fn mark_atlas_color_failure(&mut self, key: repose_text::CacheKey) {
6883 if self.atlas_color_failures.len() >= MAX_GLYPH_ATLAS_ENTRIES {
6884 self.atlas_color_failures.clear();
6885 }
6886 self.atlas_color_failures.insert(key);
6887 }
6888
6889 fn upload_glyph_mask(
6890 &mut self,
6891 cache_key: repose_text::CacheKey,
6892 px: f32,
6893 ) -> Option<GlyphInfo> {
6894 let px = quantize_glyph_px(px);
6895 let keyp = repose_text::CacheKey {
6896 font_size_bits: px.to_bits(),
6897 ..cache_key
6898 };
6899 let touch = self.next_cache_touch();
6900 if let Some(info) = self.atlas_mask.map.get_mut(&keyp) {
6901 info.last_touch = touch;
6902 info.last_used_frame = self.frame_index;
6903 return Some(*info);
6904 }
6905 if self.atlas_mask_failures.contains(&keyp) || self.atlas_mask_deferred.contains(&keyp) {
6906 return None;
6907 }
6908
6909 let Some(gb) = repose_text::rasterize_cache_key(&keyp) else {
6910 self.mark_atlas_mask_failure(keyp);
6911 return None;
6912 };
6913 if gb.w == 0 || gb.h == 0 || gb.data.is_empty() {
6914 self.mark_atlas_mask_failure(keyp);
6915 return None;
6916 }
6917 let Some(coverage) = swash_to_a8_coverage(gb.content, &gb.data) else {
6918 self.mark_atlas_mask_failure(keyp);
6919 return None;
6920 };
6921
6922 let w = gb.w.max(1);
6923 let h = gb.h.max(1);
6924 let expected = checked_image_bytes_usize(w, h, 1).ok()?;
6925 if coverage.len() < expected {
6926 self.mark_atlas_mask_failure(keyp);
6927 return None;
6928 }
6929 if w >= MAX_GLYPH_ATLAS_SIZE || h >= MAX_GLYPH_ATLAS_SIZE {
6930 self.mark_atlas_mask_failure(keyp);
6931 return None;
6932 }
6933 if !self.alloc_or_schedule_mask(w, h) {
6934 defer_atlas_glyph(&mut self.atlas_mask_deferred, keyp);
6935 return None;
6936 }
6937 let x = self.atlas_mask.next_x;
6938 let y = self.atlas_mask.next_y;
6939 self.atlas_mask.next_x += w + 1;
6940 self.atlas_mask.row_h = self.atlas_mask.row_h.max(h + 1);
6941
6942 let layout = wgpu::TexelCopyBufferLayout {
6943 offset: 0,
6944 bytes_per_row: Some(w),
6945 rows_per_image: Some(h),
6946 };
6947 let size = wgpu::Extent3d {
6948 width: w,
6949 height: h,
6950 depth_or_array_layers: 1,
6951 };
6952 self.queue.write_texture(
6953 wgpu::TexelCopyTextureInfoBase {
6954 texture: &self.atlas_mask.tex,
6955 mip_level: 0,
6956 origin: wgpu::Origin3d { x, y, z: 0 },
6957 aspect: wgpu::TextureAspect::All,
6958 },
6959 &coverage,
6960 layout,
6961 size,
6962 );
6963
6964 let info = GlyphInfo {
6965 u0: x as f32 / self.atlas_mask.size as f32,
6966 v0: y as f32 / self.atlas_mask.size as f32,
6967 u1: (x + w) as f32 / self.atlas_mask.size as f32,
6968 v1: (y + h) as f32 / self.atlas_mask.size as f32,
6969 w: w as f32,
6970 h: h as f32,
6971 last_touch: touch,
6972 last_used_frame: self.frame_index,
6973 };
6974 self.atlas_mask.used_area = self
6975 .atlas_mask
6976 .used_area
6977 .saturating_add(u64::from(w).saturating_mul(u64::from(h)));
6978 self.atlas_mask.map.insert(keyp, info);
6979 Some(info)
6980 }
6981
6982 fn upload_glyph_color(
6983 &mut self,
6984 cache_key: repose_text::CacheKey,
6985 px: f32,
6986 ) -> Option<GlyphInfo> {
6987 let px = quantize_glyph_px(px);
6988 let keyp = repose_text::CacheKey {
6989 font_size_bits: px.to_bits(),
6990 ..cache_key
6991 };
6992 let touch = self.next_cache_touch();
6993 if let Some(info) = self.atlas_color.map.get_mut(&keyp) {
6994 info.last_touch = touch;
6995 info.last_used_frame = self.frame_index;
6996 return Some(*info);
6997 }
6998 if self.atlas_color_failures.contains(&keyp) || self.atlas_color_deferred.contains(&keyp) {
6999 return None;
7000 }
7001 let Some(gb) = repose_text::rasterize_cache_key(&keyp) else {
7002 self.mark_atlas_color_failure(keyp);
7003 return None;
7004 };
7005 if !matches!(gb.content, repose_text::SwashContent::Color) {
7006 self.mark_atlas_color_failure(keyp);
7007 return None;
7008 }
7009 let w = gb.w.max(1);
7010 let h = gb.h.max(1);
7011 let expected = checked_image_bytes_usize(w, h, 4).ok()?;
7012 if gb.data.len() < expected {
7013 self.mark_atlas_color_failure(keyp);
7014 return None;
7015 }
7016 if w >= MAX_GLYPH_ATLAS_SIZE || h >= MAX_GLYPH_ATLAS_SIZE {
7017 self.mark_atlas_color_failure(keyp);
7018 return None;
7019 }
7020 if !self.alloc_or_schedule_color(w, h) {
7021 defer_atlas_glyph(&mut self.atlas_color_deferred, keyp);
7022 return None;
7023 }
7024 let x = self.atlas_color.next_x;
7025 let y = self.atlas_color.next_y;
7026 self.atlas_color.next_x += w + 1;
7027 self.atlas_color.row_h = self.atlas_color.row_h.max(h + 1);
7028
7029 let layout = wgpu::TexelCopyBufferLayout {
7030 offset: 0,
7031 bytes_per_row: Some(w * 4),
7032 rows_per_image: Some(h),
7033 };
7034 let size = wgpu::Extent3d {
7035 width: w,
7036 height: h,
7037 depth_or_array_layers: 1,
7038 };
7039 self.queue.write_texture(
7040 wgpu::TexelCopyTextureInfoBase {
7041 texture: &self.atlas_color.tex,
7042 mip_level: 0,
7043 origin: wgpu::Origin3d { x, y, z: 0 },
7044 aspect: wgpu::TextureAspect::All,
7045 },
7046 &gb.data,
7047 layout,
7048 size,
7049 );
7050 let info = GlyphInfo {
7051 u0: x as f32 / self.atlas_color.size as f32,
7052 v0: y as f32 / self.atlas_color.size as f32,
7053 u1: (x + w) as f32 / self.atlas_color.size as f32,
7054 v1: (y + h) as f32 / self.atlas_color.size as f32,
7055 w: w as f32,
7056 h: h as f32,
7057 last_touch: touch,
7058 last_used_frame: self.frame_index,
7059 };
7060 self.atlas_color.used_area = self
7061 .atlas_color
7062 .used_area
7063 .saturating_add(u64::from(w).saturating_mul(u64::from(h)));
7064 self.atlas_color.map.insert(keyp, info);
7065 Some(info)
7066 }
7067
7068 fn alloc_space_mask(&mut self, w: u32, h: u32) -> bool {
7069 let Some(x_end) = self
7070 .atlas_mask
7071 .next_x
7072 .checked_add(w)
7073 .and_then(|v| v.checked_add(1))
7074 else {
7075 return false;
7076 };
7077 if x_end >= self.atlas_mask.size {
7078 self.atlas_mask.next_x = 1;
7079 let Some(next_y) = self
7080 .atlas_mask
7081 .next_y
7082 .checked_add(self.atlas_mask.row_h)
7083 .and_then(|v| v.checked_add(1))
7084 else {
7085 return false;
7086 };
7087 self.atlas_mask.next_y = next_y;
7088 self.atlas_mask.row_h = 0;
7089 }
7090 let Some(y_end) = self
7091 .atlas_mask
7092 .next_y
7093 .checked_add(h)
7094 .and_then(|v| v.checked_add(1))
7095 else {
7096 return false;
7097 };
7098 y_end < self.atlas_mask.size
7099 }
7100
7101 fn alloc_space_color(&mut self, w: u32, h: u32) -> bool {
7102 let Some(x_end) = self
7103 .atlas_color
7104 .next_x
7105 .checked_add(w)
7106 .and_then(|v| v.checked_add(1))
7107 else {
7108 return false;
7109 };
7110 if x_end >= self.atlas_color.size {
7111 self.atlas_color.next_x = 1;
7112 let Some(next_y) = self
7113 .atlas_color
7114 .next_y
7115 .checked_add(self.atlas_color.row_h)
7116 .and_then(|v| v.checked_add(1))
7117 else {
7118 return false;
7119 };
7120 self.atlas_color.next_y = next_y;
7121 self.atlas_color.row_h = 0;
7122 }
7123 let Some(y_end) = self
7124 .atlas_color
7125 .next_y
7126 .checked_add(h)
7127 .and_then(|v| v.checked_add(1))
7128 else {
7129 return false;
7130 };
7131 y_end < self.atlas_color.size
7132 }
7133
7134 fn alloc_or_schedule_mask(&mut self, w: u32, h: u32) -> bool {
7139 if self.atlas_mask.map.len() >= MAX_GLYPH_ATLAS_ENTRIES
7140 || self
7141 .atlas_mask
7142 .used_area
7143 .saturating_add(u64::from(w) * u64::from(h))
7144 >= self.atlas_mask.size as u64 * self.atlas_mask.size as u64
7145 {
7146 self.atlas_mask_pending.compact = true;
7147 return false;
7148 }
7149 if self.alloc_space_mask(w, h) {
7150 return true;
7151 }
7152 if self.atlas_mask.size < MAX_GLYPH_ATLAS_SIZE {
7153 self.atlas_mask_pending.grow = true;
7154 } else {
7155 self.atlas_mask_pending.compact = true;
7156 }
7157 false
7158 }
7159
7160 fn alloc_or_schedule_color(&mut self, w: u32, h: u32) -> bool {
7162 if self.atlas_color.map.len() >= MAX_GLYPH_ATLAS_ENTRIES
7163 || self
7164 .atlas_color
7165 .used_area
7166 .saturating_add(u64::from(w) * u64::from(h))
7167 >= self.atlas_color.size as u64 * self.atlas_color.size as u64
7168 {
7169 self.atlas_color_pending.compact = true;
7170 return false;
7171 }
7172 if self.alloc_space_color(w, h) {
7173 return true;
7174 }
7175 if self.atlas_color.size < MAX_GLYPH_ATLAS_SIZE {
7176 self.atlas_color_pending.grow = true;
7177 } else {
7178 self.atlas_color_pending.compact = true;
7179 }
7180 false
7181 }
7182}
7183
7184fn pack_shape_bits(brush_type: u32, grad_kind: u32, tile_mode: u32, cap: u32) -> u32 {
7187 debug_assert!(brush_type < 4 && grad_kind < 4 && tile_mode < 4 && cap < 4);
7188 (brush_type & 3) | ((grad_kind & 3) << 2) | ((tile_mode & 3) << 4) | ((cap & 3) << 6)
7189}
7190
7191fn brush_to_shape_fields(
7205 brush: &Brush,
7206 rect: &repose_core::Rect,
7207 transform: &Transform,
7208) -> (u32, u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2], u32) {
7209 let to_local = |p: Vec2| {
7210 let m = transform.linear();
7211 let det = m[0] * m[3] - m[1] * m[2];
7212 if det.abs() < 1e-12 {
7213 return [p.x, p.y];
7214 }
7215 [
7216 (m[3] * p.x - m[1] * p.y) / det,
7217 (-m[2] * p.x + m[0] * p.y) / det,
7218 ]
7219 };
7220 match brush {
7221 Brush::Solid(c) => (
7222 0u32,
7223 0u32,
7224 c.to_linear(),
7225 [0.0; 4],
7226 [0.0; 2],
7227 [0.0; 2],
7228 0u32,
7229 ),
7230 Brush::Linear {
7231 start,
7232 end,
7233 start_color,
7234 end_color,
7235 } => (
7236 1u32,
7237 0u32,
7238 start_color.to_linear(),
7239 end_color.to_linear(),
7240 to_local(*start),
7241 to_local(*end),
7242 0u32,
7243 ),
7244 Brush::LinearNormalized {
7245 start,
7246 end,
7247 start_color,
7248 end_color,
7249 } => (
7250 1u32,
7251 0u32,
7252 start_color.to_linear(),
7253 end_color.to_linear(),
7254 to_local(Vec2 {
7255 x: start.x * rect.w,
7256 y: start.y * rect.h,
7257 }),
7258 to_local(Vec2 {
7259 x: end.x * rect.w,
7260 y: end.y * rect.h,
7261 }),
7262 0u32,
7263 ),
7264 Brush::Radial {
7265 center,
7266 radius,
7267 start_color,
7268 end_color,
7269 } => (
7270 1u32,
7271 1u32,
7272 start_color.to_linear(),
7273 end_color.to_linear(),
7274 to_local(*center),
7275 [radius.max(0.0), 0.0],
7276 0u32,
7277 ),
7278 Brush::Sweep {
7279 center,
7280 start_color,
7281 end_color,
7282 } => (
7283 1u32,
7284 2u32,
7285 start_color.to_linear(),
7286 end_color.to_linear(),
7287 to_local(*center),
7288 [0.0, 0.0],
7289 0u32,
7290 ),
7291 _ => (0u32, 0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2], 0u32),
7292 }
7293}
7294
7295fn brush_to_instance_fields(brush: &Brush) -> (u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2]) {
7296 match brush {
7297 Brush::Solid(c) => (
7298 0u32,
7299 c.to_linear(),
7300 [0.0, 0.0, 0.0, 0.0],
7301 [0.0, 0.0],
7302 [0.0, 1.0],
7303 ),
7304 Brush::Linear {
7305 start,
7306 end,
7307 start_color,
7308 end_color,
7309 } => (
7310 1u32,
7311 start_color.to_linear(),
7312 end_color.to_linear(),
7313 [start.x, start.y],
7314 [end.x, end.y],
7315 ),
7316 Brush::LinearNormalized {
7317 start,
7318 end,
7319 start_color,
7320 end_color,
7321 } => (
7322 1u32,
7323 start_color.to_linear(),
7324 end_color.to_linear(),
7325 [start.x, start.y],
7326 [end.x, end.y],
7327 ),
7328 Brush::Radial { start_color, .. } => (
7329 0u32,
7330 start_color.to_linear(),
7331 [0.0, 0.0, 0.0, 0.0],
7332 [0.0, 0.0],
7333 [0.0, 1.0],
7334 ),
7335 Brush::Sweep { start_color, .. } => (
7336 0u32,
7337 start_color.to_linear(),
7338 [0.0, 0.0, 0.0, 0.0],
7339 [0.0, 0.0],
7340 [0.0, 1.0],
7341 ),
7342 _ => (0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
7343 }
7344}
7345
7346#[allow(dead_code)]
7349fn brush_to_solid_color(brush: &Brush) -> [f32; 4] {
7350 match brush {
7351 Brush::Solid(c) => c.to_linear(),
7352 Brush::Linear { start_color, .. } => start_color.to_linear(),
7353 Brush::LinearNormalized { start_color, .. } => start_color.to_linear(),
7354 Brush::Radial { start_color, .. } => start_color.to_linear(),
7355 Brush::Sweep { start_color, .. } => start_color.to_linear(),
7356 _ => [0.0; 4],
7357 }
7358}
7359
7360fn init_atlas_mask(device: &wgpu::Device) -> AtlasA8 {
7361 let size = 1024u32;
7362 let tex = device.create_texture(&wgpu::TextureDescriptor {
7363 label: Some("glyph atlas A8"),
7364 size: wgpu::Extent3d {
7365 width: size,
7366 height: size,
7367 depth_or_array_layers: 1,
7368 },
7369 mip_level_count: 1,
7370 sample_count: 1,
7371 dimension: wgpu::TextureDimension::D2,
7372 format: wgpu::TextureFormat::R8Unorm,
7373 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
7374 view_formats: &[],
7375 });
7376 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
7377 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
7378 label: Some("glyph atlas sampler A8"),
7379 address_mode_u: wgpu::AddressMode::ClampToEdge,
7380 address_mode_v: wgpu::AddressMode::ClampToEdge,
7381 address_mode_w: wgpu::AddressMode::ClampToEdge,
7382 mag_filter: wgpu::FilterMode::Linear,
7383 min_filter: wgpu::FilterMode::Linear,
7384 mipmap_filter: wgpu::MipmapFilterMode::Linear,
7385 ..Default::default()
7386 });
7387
7388 AtlasA8 {
7389 tex,
7390 view,
7391 sampler,
7392 size,
7393 next_x: 1,
7394 next_y: 1,
7395 row_h: 0,
7396 map: HashMap::new(),
7397 used_area: 0,
7398 }
7399}
7400
7401fn init_atlas_color(device: &wgpu::Device) -> AtlasRGBA {
7402 let size = 1024u32;
7403 let tex = device.create_texture(&wgpu::TextureDescriptor {
7404 label: Some("glyph atlas RGBA"),
7405 size: wgpu::Extent3d {
7406 width: size,
7407 height: size,
7408 depth_or_array_layers: 1,
7409 },
7410 mip_level_count: 1,
7411 sample_count: 1,
7412 dimension: wgpu::TextureDimension::D2,
7413 format: wgpu::TextureFormat::Rgba8UnormSrgb,
7414 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
7415 view_formats: &[],
7416 });
7417 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
7418 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
7419 label: Some("glyph atlas sampler RGBA"),
7420 address_mode_u: wgpu::AddressMode::ClampToEdge,
7421 address_mode_v: wgpu::AddressMode::ClampToEdge,
7422 address_mode_w: wgpu::AddressMode::ClampToEdge,
7423 mag_filter: wgpu::FilterMode::Linear,
7424 min_filter: wgpu::FilterMode::Linear,
7425 mipmap_filter: wgpu::MipmapFilterMode::Linear,
7426 ..Default::default()
7427 });
7428 AtlasRGBA {
7429 tex,
7430 view,
7431 sampler,
7432 size,
7433 next_x: 1,
7434 next_y: 1,
7435 row_h: 0,
7436 map: HashMap::new(),
7437 used_area: 0,
7438 }
7439}
7440
7441#[cfg(feature = "winit-surface")]
7442impl WgpuSurfaceBackend {
7443 pub fn take_surface(&mut self) -> Option<wgpu::Surface<'static>> {
7446 self.surface.take()
7447 }
7448
7449 pub fn recreate_surface(&mut self, window: &Arc<winit::window::Window>) -> anyhow::Result<()> {
7453 self.window = Some(window.clone());
7454 let size = window.inner_size();
7455 if size.width == 0 || size.height == 0 {
7456 self.surface = None;
7457 self.pending_reconfigure = false;
7458 self.renderer.resize(0, 0);
7459 anyhow::bail!("window has zero size; surface recreation deferred");
7460 }
7461 let Some(instance) = self.instance.as_ref() else {
7462 anyhow::bail!("no wgpu instance retained; cannot recreate surface")
7463 };
7464 let Some(config) = self.surface_config.as_mut() else {
7465 anyhow::bail!("no surface config retained; cannot recreate surface")
7466 };
7467 let (width, height) = clamp_output_size(
7468 size.width,
7469 size.height,
7470 self.renderer.device.limits().max_texture_dimension_2d,
7471 );
7472 config.width = width;
7473 config.height = height;
7474 let surface = instance.create_surface(window.clone())?;
7475 surface.configure(&self.renderer.device, config);
7476 self.surface = Some(surface);
7477 self.pending_reconfigure = false;
7478 self.renderer.resize(width, height);
7479 Ok(())
7480 }
7481}
7482
7483#[cfg(feature = "winit-surface")]
7484impl RenderBackend for WgpuSurfaceBackend {
7485 fn configure_surface(&mut self, width: u32, height: u32) -> (u32, u32) {
7486 if width == 0 || height == 0 {
7487 self.renderer.resize(0, 0);
7488 self.pending_reconfigure = true;
7489 return (0, 0);
7490 }
7491 let (width, height) = clamp_output_size(
7492 width,
7493 height,
7494 self.renderer.device.limits().max_texture_dimension_2d,
7495 );
7496 if self.renderer.output_width == width && self.renderer.output_height == height {
7497 if let Some(ref mut config) = self.surface_config {
7498 config.width = width;
7499 config.height = height;
7500 }
7501 return (width, height);
7502 }
7503 if let Some(ref mut config) = self.surface_config {
7504 config.width = width;
7505 config.height = height;
7506 }
7507 self.renderer.resize(width, height);
7508 if let (Some(surface), Some(config)) = (self.surface.as_ref(), self.surface_config.as_ref())
7509 {
7510 surface.configure(&self.renderer.device, config);
7511 }
7512 (width, height)
7513 }
7514
7515 fn frame(&mut self, scene: &Scene, _glyph_cfg: GlyphRasterConfig) -> bool {
7516 if self.pending_reconfigure {
7517 if let (Some(surface), Some(config)) =
7518 (self.surface.as_ref(), self.surface_config.as_ref())
7519 {
7520 surface.configure(&self.renderer.device, config);
7521 }
7522 self.pending_reconfigure = false;
7523 }
7524
7525 if self.renderer.output_width == 0 || self.renderer.output_height == 0 {
7526 request_frame();
7527 return false;
7528 }
7529 if let Err(error) = self.renderer.begin_frame() {
7530 log::warn!("begin renderer frame: {error:#}");
7531 request_frame();
7532 return false;
7533 }
7534
7535 let Some(surface) = self.surface.as_ref() else {
7536 self.renderer.end_frame();
7537 request_frame();
7538 return false;
7539 };
7540 let frame = match surface.get_current_texture() {
7541 wgpu::CurrentSurfaceTexture::Success(f) => f,
7542 wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
7543 self.pending_reconfigure = true;
7544 f
7545 }
7546 other @ (wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost) => {
7547 self.surface = None;
7548 let result = self
7549 .window
7550 .clone()
7551 .ok_or_else(|| anyhow::anyhow!("no window retained for surface recreation"))
7552 .and_then(|window| self.recreate_surface(&window));
7553 if let Err(error) = result {
7554 log::warn!("surface {other:?}; recreation deferred: {error:#}");
7555 }
7556 self.renderer.end_frame();
7557 request_frame();
7558 return false;
7559 }
7560 other => {
7561 match other {
7562 wgpu::CurrentSurfaceTexture::Validation => {
7563 log::warn!("surface {other:?}; reconfiguring next frame");
7564 self.pending_reconfigure = true;
7565 }
7566 wgpu::CurrentSurfaceTexture::Timeout
7567 | wgpu::CurrentSurfaceTexture::Occluded => {
7568 log::debug!("surface {other:?}; retrying next frame");
7569 }
7570 _ => {}
7571 }
7572 self.renderer.end_frame();
7573 request_frame();
7574 return false;
7575 }
7576 };
7577
7578 let (swap_w, swap_h) = (frame.texture.width(), frame.texture.height());
7587 if swap_w != self.renderer.output_width || swap_h != self.renderer.output_height {
7588 log::debug!(
7589 "swapchain {swap_w}x{swap_h} != renderer {}x{}; resyncing",
7590 self.renderer.output_width,
7591 self.renderer.output_height
7592 );
7593 drop(frame);
7594 if let Some(config) = self.surface_config.as_mut() {
7595 config.width = swap_w;
7596 config.height = swap_h;
7597 }
7598 self.renderer.resize(swap_w, swap_h);
7599 self.pending_reconfigure = true;
7600 self.renderer.end_frame();
7601 request_frame();
7602 return false;
7603 }
7604
7605 let swap_view = if let Some(view_format) = self
7606 .surface_config
7607 .as_ref()
7608 .and_then(|c| c.view_formats.iter().find(|f| f.is_srgb()).copied())
7609 {
7610 frame.texture.create_view(&wgpu::TextureViewDescriptor {
7611 format: Some(view_format),
7612 ..Default::default()
7613 })
7614 } else {
7615 frame
7616 .texture
7617 .create_view(&wgpu::TextureViewDescriptor::default())
7618 };
7619 let mut encoder =
7620 self.renderer
7621 .device
7622 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
7623 label: Some("frame encoder"),
7624 });
7625
7626 self.renderer.render_scene_to_encoder_with_texture(
7627 scene,
7628 &mut encoder,
7629 &swap_view,
7630 Some(&frame.texture),
7631 None,
7632 );
7633
7634 {
7637 let _reset = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
7638 label: Some("webgl color_mask reset before present"),
7639 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7640 view: &swap_view,
7641 resolve_target: None,
7642 ops: wgpu::Operations {
7643 load: wgpu::LoadOp::Load,
7644 store: wgpu::StoreOp::Store,
7645 },
7646 depth_slice: None,
7647 })],
7648 depth_stencil_attachment: None,
7649 timestamp_writes: None,
7650 occlusion_query_set: None,
7651 multiview_mask: None,
7652 });
7653 }
7654
7655 self.renderer
7656 .queue
7657 .submit(std::iter::once(encoder.finish()));
7658 if let Err(e) = catch_unwind(AssertUnwindSafe(|| self.renderer.queue.present(frame))) {
7659 log::warn!("queue.present panicked: {:?}", e);
7660 self.renderer.end_frame();
7661 request_frame();
7662 return false;
7663 }
7664 self.renderer.end_frame();
7665 true
7666 }
7667}
7668
7669impl WgpuSceneRenderer {
7670 #[allow(clippy::too_many_arguments)]
7682 fn replay_active_clips(
7683 &mut self,
7684 clips: &[ActiveClip],
7685 parent_transform: &Transform,
7686 origin: (f32, f32),
7687 target_size: (f32, f32),
7688 pass: &mut Pass,
7689 encoder: &mut wgpu::CommandEncoder,
7690 ) -> Vec<ActiveClip> {
7691 let mut current = repose_core::Rect {
7692 x: 0.0,
7693 y: 0.0,
7694 w: target_size.0,
7695 h: target_size.1,
7696 };
7697 let mut replayed = Vec::with_capacity(clips.len());
7698 for (index, clip) in clips.iter().enumerate() {
7699 let has_inverse = clips[..index].iter().any(ActiveClip::difference);
7700 let mut blocked = clip.blocked();
7701 if has_inverse && !blocked {
7702 log::error!(
7703 "unsupported clip nesting: a Difference clip cannot contain another clip"
7704 );
7705 blocked = true;
7706 }
7707 let difference = clip.difference();
7708 let mut command = None;
7709 match clip {
7710 ActiveClip::Rect {
7711 off: _,
7712 cnt: _,
7713 rect,
7714 radii,
7715 ..
7716 } => {
7717 let local_rect = parent_rect_to_layer_rect(*rect, parent_transform, origin)
7718 .unwrap_or(repose_core::Rect {
7719 x: 0.0,
7720 y: 0.0,
7721 w: 0.0,
7722 h: 0.0,
7723 });
7724 let local_radii =
7725 parent_radii_to_layer_radii(*radii, parent_transform).unwrap_or([0.0; 4]);
7726 let next = if difference {
7727 current
7728 } else {
7729 intersect(current, local_rect)
7730 };
7731 let scissor = rect_to_scissor(
7732 next,
7733 target_size.0.max(1.0) as u32,
7734 target_size.1.max(1.0) as u32,
7735 );
7736 let has_area = local_rect.x.is_finite()
7737 && local_rect.y.is_finite()
7738 && local_rect.w.is_finite()
7739 && local_rect.h.is_finite()
7740 && local_rect.w > 0.0
7741 && local_rect.h > 0.0;
7742 if blocked || !has_area || (!difference && scissor.2 == 0) {
7743 blocked = blocked || !difference;
7744 } else {
7745 let ndc = rect_to_ndc(local_rect, target_size.0, target_size.1);
7746 let instance = ClipInstance {
7747 xywh: [ndc[0] + ndc[2] * 0.5, ndc[1] + ndc[3] * 0.5, ndc[2], ndc[3]],
7748 radii: local_radii,
7749 fwd_mat: [1.0, 0.0, 0.0, 1.0],
7750 };
7751 let bytes = bytemuck::bytes_of(&instance);
7752 if self
7753 .clip_ring
7754 .grow_to_fit(
7755 &self.device,
7756 encoder,
7757 u64::try_from(bytes.len()).unwrap_or(u64::MAX),
7758 )
7759 .is_ok()
7760 && let Ok(new_off) = self.clip_ring.alloc_write(&self.queue, bytes)
7761 {
7762 command = Some(Cmd::ClipPush {
7763 off: new_off,
7764 cnt: 1,
7765 scissor,
7766 difference,
7767 applied: true,
7768 });
7769 replayed.push(ActiveClip::Rect {
7770 off: new_off,
7771 cnt: 1,
7772 rect: local_rect,
7773 radii: local_radii,
7774 scissor,
7775 difference,
7776 applied: true,
7777 blocked: false,
7778 });
7779 } else {
7780 blocked = blocked || !difference;
7781 }
7782 }
7783 current = next;
7784 }
7785 ActiveClip::Vector {
7786 mesh,
7787 source,
7788 affine,
7789 ..
7790 } => {
7791 let local_affine =
7792 parent_affine_to_layer_affine(*affine, parent_transform, origin)
7793 .unwrap_or([0.0; 6]);
7794 let aabb = mesh_aabb(source, local_affine);
7795 let next = if difference {
7796 current
7797 } else {
7798 intersect(current, aabb)
7799 };
7800 let scissor = rect_to_scissor(
7801 next,
7802 target_size.0.max(1.0) as u32,
7803 target_size.1.max(1.0) as u32,
7804 );
7805 let has_area = aabb.x.is_finite()
7806 && aabb.y.is_finite()
7807 && aabb.w.is_finite()
7808 && aabb.h.is_finite()
7809 && aabb.w > 0.0
7810 && aabb.h > 0.0
7811 && mesh
7812 .as_ref()
7813 .is_some_and(|mesh| mesh.vertex_count > 0 && mesh.index_count > 0);
7814 if blocked || !has_area || (!difference && scissor.2 == 0) {
7815 blocked = blocked || !difference;
7816 } else if let Some(mesh_gpu) = mesh.clone()
7817 && let Some(uoff) = self.alloc_mesh_uniform(mesh_uniform_from_paint(
7818 local_affine,
7819 &repose_core::PaintDesc::Solid,
7820 ))
7821 {
7822 command = Some(Cmd::VectorClipPush {
7823 mesh: mesh_gpu.clone(),
7824 uoff,
7825 scissor,
7826 difference,
7827 applied: true,
7828 });
7829 replayed.push(ActiveClip::Vector {
7830 mesh: Some(mesh_gpu),
7831 source: source.clone(),
7832 uoff,
7833 affine: local_affine,
7834 scissor,
7835 difference,
7836 applied: true,
7837 blocked: false,
7838 });
7839 } else {
7840 blocked = blocked || !difference;
7841 }
7842 current = next;
7843 }
7844 }
7845 if blocked {
7846 current = repose_core::Rect {
7847 x: 0.0,
7848 y: 0.0,
7849 w: 0.0,
7850 h: 0.0,
7851 };
7852 }
7853 if command.is_none() {
7854 replayed.push(match clip {
7855 ActiveClip::Rect {
7856 rect,
7857 radii,
7858 scissor,
7859 difference,
7860 ..
7861 } => ActiveClip::Rect {
7862 off: 0,
7863 cnt: 0,
7864 rect: *rect,
7865 radii: *radii,
7866 scissor: *scissor,
7867 difference: *difference,
7868 applied: false,
7869 blocked,
7870 },
7871 ActiveClip::Vector {
7872 mesh,
7873 source,
7874 affine,
7875 scissor,
7876 difference,
7877 ..
7878 } => ActiveClip::Vector {
7879 mesh: mesh.clone(),
7880 source: source.clone(),
7881 uoff: 0,
7882 affine: parent_affine_to_layer_affine(*affine, parent_transform, origin)
7883 .unwrap_or([0.0; 6]),
7884 scissor: *scissor,
7885 difference: *difference,
7886 applied: false,
7887 blocked,
7888 },
7889 });
7890 }
7891 if let Some(command) = command {
7892 pass.cmds.push(command);
7893 }
7894 pass.active_scissor = if blocked {
7895 None
7896 } else {
7897 let scissor = rect_to_scissor(
7898 current,
7899 target_size.0.max(1.0) as u32,
7900 target_size.1.max(1.0) as u32,
7901 );
7902 (scissor.2 > 0 && scissor.3 > 0).then_some(scissor)
7903 };
7904 }
7905 replayed
7906 }
7907
7908 #[allow(clippy::too_many_arguments)]
7909 fn push_perspective_layer(
7910 &mut self,
7911 node: Transform,
7912 top: Transform,
7913 transform_stack: &mut Vec<Transform>,
7914 scissor_stack: &mut Vec<repose_core::Rect>,
7915 root_clip_rect: &mut repose_core::Rect,
7916 current_target_size: &mut (f32, f32),
7917 current_pass: &mut Pass,
7918 active_clips: &mut Vec<ActiveClip>,
7919 passes: &mut Vec<Pass>,
7920 target_stack: &mut Vec<PassTarget>,
7921 flatten_stack: &mut Vec<FlattenRecord>,
7922 id_head: &mut u32,
7923 pass_id_head: &mut u64,
7924 ids_used: &mut Vec<u32>,
7925 encoder: &mut wgpu::CommandEncoder,
7926 ) {
7927 let map =
7931 Transform::compose_projective(&top.projective_matrix(), &node.projective_matrix());
7932 let scr = scissor_stack.last().copied().unwrap_or(*root_clip_rect);
7933 let max_dimension = self.device.limits().max_texture_dimension_2d as f32;
7934 if !scr.w.is_finite()
7935 || !scr.h.is_finite()
7936 || scr.w < 1.0
7937 || scr.h < 1.0
7938 || scr.w > max_dimension
7939 || scr.h > max_dimension
7940 {
7941 return;
7942 }
7943 let w = scr.w.ceil();
7944 let h = scr.h.ceil();
7945 let layer_rect = repose_core::Rect {
7946 x: scr.x,
7947 y: scr.y,
7948 w,
7949 h,
7950 };
7951 if ids_used.len() >= MAX_TRANSIENT_GRAPHICS_LAYERS {
7952 return;
7953 }
7954 let layer_id = *id_head;
7955 *id_head = id_head.wrapping_add(1);
7956 ids_used.push(layer_id);
7957
7958 let stack_len = transform_stack.len();
7959 transform_stack.push(top);
7963 transform_stack.push(Transform::translate(-layer_rect.x, -layer_rect.y));
7964
7965 let saved_scissor_stack = std::mem::replace(
7966 scissor_stack,
7967 vec![repose_core::Rect {
7968 x: 0.0,
7969 y: 0.0,
7970 w,
7971 h,
7972 }],
7973 );
7974 let saved_root = std::mem::replace(
7975 root_clip_rect,
7976 repose_core::Rect {
7977 x: 0.0,
7978 y: 0.0,
7979 w,
7980 h,
7981 },
7982 );
7983 let saved_size = std::mem::replace(current_target_size, (w, h));
7984 let prev_target = current_pass.target;
7985 let saved_active_scissor = current_pass.active_scissor;
7986 let saved_clips = std::mem::take(active_clips);
7987 let layer_pass_id = *pass_id_head;
7988 *pass_id_head += 1;
7989 let mut layer_pass = Pass {
7990 id: layer_pass_id,
7991 target: PassTarget::Layer(layer_id),
7992 initial_scissor: (0, 0, w as u32, h as u32),
7993 active_scissor: Some((0, 0, w as u32, h as u32)),
7994 clear_color: Some([0.0, 0.0, 0.0, 0.0]),
7995 cmds: Vec::new(),
7996 };
7997 let layer_clips = self.replay_active_clips(
7998 &saved_clips,
7999 &top,
8000 (layer_rect.x, layer_rect.y),
8001 (w, h),
8002 &mut layer_pass,
8003 encoder,
8004 );
8005 *active_clips = layer_clips;
8006 let saved = std::mem::replace(current_pass, layer_pass);
8007 passes.push(saved);
8008 target_stack.push(prev_target);
8009 self.get_or_create_layer(layer_id, w as u32, h as u32, layer_rect, true);
8010 *current_target_size = (w, h);
8011 flatten_stack.push(FlattenRecord {
8012 stack_len,
8013 layer_id,
8014 map,
8015 layer_rect,
8016 saved_scissor_stack,
8017 saved_root,
8018 saved_size,
8019 saved_active_scissor,
8020 saved_clips,
8021 });
8022 }
8023
8024 #[allow(clippy::too_many_arguments)]
8027 fn pop_perspective_layer(
8028 &mut self,
8029 rec: FlattenRecord,
8030 scissor_stack: &mut Vec<repose_core::Rect>,
8031 root_clip_rect: &mut repose_core::Rect,
8032 current_target_size: &mut (f32, f32),
8033 current_pass: &mut Pass,
8034 active_clips: &mut Vec<ActiveClip>,
8035 passes: &mut Vec<Pass>,
8036 target_stack: &mut Vec<PassTarget>,
8037 pass_id_head: &mut u64,
8038 encoder: &mut wgpu::CommandEncoder,
8039 ) {
8040 *scissor_stack = rec.saved_scissor_stack;
8041 *root_clip_rect = rec.saved_root;
8042 *current_target_size = rec.saved_size;
8043 *active_clips = rec.saved_clips;
8044 let resumed_pass_id = *pass_id_head;
8045 *pass_id_head += 1;
8046 let saved = std::mem::replace(
8047 current_pass,
8048 Pass {
8049 id: resumed_pass_id,
8050 target: target_stack.pop().unwrap_or(PassTarget::Surface),
8051 initial_scissor: rec.saved_active_scissor.unwrap_or((0, 0, 1, 1)),
8052 active_scissor: rec.saved_active_scissor,
8053 clear_color: None,
8054 cmds: Vec::new(),
8055 },
8056 );
8057 passes.push(saved);
8058
8059 let (tw, th) = rec.saved_size;
8063 let r = rec.layer_rect;
8064 let corners = [
8065 (r.x, r.y),
8066 (r.x + r.w, r.y),
8067 (r.x + r.w, r.y + r.h),
8068 (r.x, r.y + r.h),
8069 ];
8070 let mut ndc = [[0.0f32; 2]; 4];
8071 let mut ws = [1.0f32; 4];
8072 let mut all_behind = true;
8073 for (i, (x, y)) in corners.iter().enumerate() {
8074 let w_raw = rec.map[6] * x + rec.map[7] * y + rec.map[8];
8075 let w = if w_raw.abs() < 1e-6 {
8076 if w_raw < 0.0 { -1e-6 } else { 1e-6 }
8077 } else {
8078 w_raw
8079 };
8080 if w > 0.0 {
8081 all_behind = false;
8082 }
8083 let px = (rec.map[0] * x + rec.map[1] * y + rec.map[2]) / w;
8084 let py = (rec.map[3] * x + rec.map[4] * y + rec.map[5]) / w;
8085 ndc[i] = [px / tw * 2.0 - 1.0, 1.0 - py / th * 2.0];
8086 ws[i] = w;
8087 }
8088 if all_behind {
8089 return;
8092 }
8093 let Some(layer) = self.layer_pool.get(&rec.layer_id) else {
8094 return;
8095 };
8096 let uv_u1 = layer.rect_px.2 / layer.width.max(1) as f32;
8097 let uv_v1 = layer.rect_px.3 / layer.height.max(1) as f32;
8098 let inst = ProjectiveInstance {
8099 c0: ndc[0],
8100 c1: ndc[1],
8101 c2: ndc[2],
8102 c3: ndc[3],
8103 uv: [0.0, 0.0, uv_u1, uv_v1],
8104 w: ws,
8105 alpha: 1.0,
8106 _pad: [0.0; 3],
8107 };
8108 if self
8109 .projective_ring
8110 .grow_to_fit(
8111 &self.device,
8112 encoder,
8113 std::mem::size_of::<ProjectiveInstance>() as u64,
8114 )
8115 .is_err()
8116 {
8117 return;
8118 }
8119 let bytes = bytemuck::bytes_of(&inst);
8120 let Ok(off) = self.projective_ring.alloc_write(&self.queue, bytes) else {
8121 return;
8122 };
8123 current_pass.cmds.push(Cmd::CompositeProjective {
8124 off,
8125 cnt: 1,
8126 layer_id: rec.layer_id,
8127 });
8128 }
8129
8130 fn mesh_cache_key(mesh: &Arc<repose_core::VectorMeshData>) -> MeshCacheKey {
8131 MeshCacheKey {
8132 source: Arc::as_ptr(mesh) as usize,
8133 index: 0,
8134 kind: 1,
8135 vertex_len: mesh.vertices.len(),
8136 index_len: mesh.indices.len(),
8137 }
8138 }
8139
8140 fn overlay_mesh_cache_key(
8141 meshes: &Arc<[repose_core::VectorMeshData]>,
8142 index: usize,
8143 ) -> MeshCacheKey {
8144 let mesh = &meshes[index];
8145 MeshCacheKey {
8146 source: meshes.as_ptr() as usize,
8147 index,
8148 kind: 2,
8149 vertex_len: mesh.vertices.len(),
8150 index_len: mesh.indices.len(),
8151 }
8152 }
8153
8154 fn mesh_owner_matches(owner: &MeshOwner, other: &MeshOwner) -> bool {
8155 match (owner, other) {
8156 (MeshOwner::Object(a), MeshOwner::Object(b)) => Arc::ptr_eq(a, b),
8157 (MeshOwner::Overlay(a), MeshOwner::Overlay(b)) => Arc::ptr_eq(a, b),
8158 _ => false,
8159 }
8160 }
8161
8162 fn evict_mesh_cache(&mut self) -> bool {
8163 let victim = self
8164 .mesh_cache
8165 .iter()
8166 .filter(|(_, entry)| entry.last_used_frame != self.frame_index)
8167 .min_by_key(|(_, entry)| entry.last_touch_frame)
8168 .map(|(key, _)| *key);
8169 let Some(victim) = victim else {
8170 return false;
8171 };
8172 let Some(entry) = self.mesh_cache.remove(&victim) else {
8173 return false;
8174 };
8175 self.mesh_cache_bytes_total = self.mesh_cache_bytes_total.saturating_sub(entry.gpu.bytes);
8176 true
8177 }
8178
8179 fn reserve_mesh_cache(&mut self, bytes: u64) -> bool {
8180 while self.mesh_cache.len() >= MAX_MESH_CACHE_ENTRIES
8181 || self.mesh_cache_bytes_total.saturating_add(bytes) > MAX_MESH_CACHE_BYTES
8182 {
8183 if !self.evict_mesh_cache() {
8184 return false;
8185 }
8186 }
8187 true
8188 }
8189
8190 fn reserve_mesh_gpu_budget(&mut self, bytes: u64) -> bool {
8191 while self.gpu_bytes_total().saturating_add(bytes) > self.gpu_budget_bytes {
8192 if !self.evict_mesh_cache() {
8193 return false;
8194 }
8195 }
8196 true
8197 }
8198
8199 fn upload_mesh_geometry_with_owner(
8200 &mut self,
8201 mesh: &repose_core::VectorMeshData,
8202 key: MeshCacheKey,
8203 owner: MeshOwner,
8204 ) -> Option<MeshGpu> {
8205 let touch = self.next_cache_touch();
8206 let cached = self
8207 .mesh_cache
8208 .get(&key)
8209 .filter(|entry| Self::mesh_owner_matches(&entry.owner, &owner))
8210 .map(|entry| entry.gpu.clone());
8211 if let Some(gpu) = cached {
8212 if let Some(entry) = self.mesh_cache.get_mut(&key) {
8213 entry.last_used_frame = self.frame_index;
8214 entry.last_touch_frame = touch;
8215 }
8216 return Some(gpu);
8217 }
8218 if let Some(entry) = self.mesh_cache.remove(&key) {
8219 self.mesh_cache_bytes_total =
8220 self.mesh_cache_bytes_total.saturating_sub(entry.gpu.bytes);
8221 }
8222 if mesh.vertices.is_empty() || mesh.indices.is_empty() {
8223 log::warn!("vector mesh has no geometry");
8224 return None;
8225 }
8226 if !mesh.indices.len().is_multiple_of(3) {
8227 log::warn!("vector mesh index count is not a triangle list");
8228 return None;
8229 }
8230 let vertex_count = u32::try_from(mesh.vertices.len()).ok()?;
8231 let index_count = u32::try_from(mesh.indices.len()).ok()?;
8232 if mesh.indices.iter().any(|&index| index >= vertex_count) {
8233 log::warn!("vector mesh index is outside its vertex buffer");
8234 return None;
8235 }
8236 if mesh.vertices.iter().any(|vertex| {
8237 vertex.pos.iter().any(|value| !value.is_finite())
8238 || vertex.color.iter().any(|value| !value.is_finite())
8239 || vertex.uv.iter().any(|value| !value.is_finite())
8240 }) {
8241 log::warn!("vector mesh contains non-finite vertex data");
8242 return None;
8243 }
8244 let verts: Vec<MeshVertex> = mesh
8245 .vertices
8246 .iter()
8247 .map(|v| MeshVertex {
8248 pos: v.pos,
8249 color: v.color,
8250 uv: v.uv,
8251 })
8252 .collect();
8253 let vbytes = bytemuck::cast_slice(&verts);
8254 let ibytes = bytemuck::cast_slice(&mesh.indices[..]);
8255 let bytes = u64::try_from(vbytes.len())
8256 .ok()?
8257 .checked_add(u64::try_from(ibytes.len()).ok()?)?;
8258 let max_buffer_size = self.device.limits().max_buffer_size;
8259 if vbytes.len() as u64 > max_buffer_size || ibytes.len() as u64 > max_buffer_size {
8260 return None;
8261 }
8262 let cacheable = bytes <= MAX_MESH_CACHE_BYTES
8263 && self.reserve_mesh_cache(bytes)
8264 && self.reserve_mesh_gpu_budget(bytes);
8265 let vertices = self.device.create_buffer(&wgpu::BufferDescriptor {
8266 label: Some("vector mesh vertices"),
8267 size: vbytes.len().max(4) as u64,
8268 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
8269 mapped_at_creation: false,
8270 });
8271 let indices = self.device.create_buffer(&wgpu::BufferDescriptor {
8272 label: Some("vector mesh indices"),
8273 size: ibytes.len().max(4) as u64,
8274 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
8275 mapped_at_creation: false,
8276 });
8277 self.queue.write_buffer(&vertices, 0, vbytes);
8278 self.queue.write_buffer(&indices, 0, ibytes);
8279 let gpu = MeshGpu {
8280 vertices,
8281 indices,
8282 vertex_count,
8283 index_count,
8284 bytes,
8285 };
8286 if cacheable {
8287 let touch = self.next_cache_touch();
8288 self.mesh_cache.insert(
8289 key,
8290 MeshCacheEntry {
8291 owner,
8292 gpu: gpu.clone(),
8293 last_used_frame: self.frame_index,
8294 last_touch_frame: touch,
8295 },
8296 );
8297 self.mesh_cache_bytes_total = self.mesh_cache_bytes_total.saturating_add(bytes);
8298 }
8299 Some(gpu)
8300 }
8301
8302 fn upload_mesh_geometry(&mut self, mesh: &Arc<repose_core::VectorMeshData>) -> Option<MeshGpu> {
8303 self.upload_mesh_geometry_with_owner(
8304 mesh,
8305 Self::mesh_cache_key(mesh),
8306 MeshOwner::Object(mesh.clone()),
8307 )
8308 }
8309
8310 fn upload_overlay_mesh(
8311 &mut self,
8312 meshes: &Arc<[repose_core::VectorMeshData]>,
8313 index: usize,
8314 ) -> Option<MeshGpu> {
8315 self.upload_mesh_geometry_with_owner(
8316 &meshes[index],
8317 Self::overlay_mesh_cache_key(meshes, index),
8318 MeshOwner::Overlay(meshes.clone()),
8319 )
8320 }
8321
8322 fn alloc_mesh_uniform(&mut self, u: MeshUniform) -> Option<u64> {
8323 let size = std::mem::size_of::<MeshUniform>() as u64;
8324 let next = self
8325 .mesh_uniform_head
8326 .checked_add(self.mesh_uniform_alignment)?;
8327 if next > self.mesh_uniform_cap || next > u64::from(u32::MAX) {
8328 log::warn!("mesh uniform buffer exhausted; remaining meshes skipped");
8329 return None;
8330 }
8331 let slot = self.mesh_uniform_head;
8332 if slot.checked_add(size)? > self.mesh_uniform_cap {
8333 return None;
8334 }
8335 self.queue
8336 .write_buffer(&self.mesh_uniform_buf, slot, bytemuck::bytes_of(&u));
8337 self.mesh_uniform_head = next;
8338 Some(slot)
8339 }
8340
8341 #[allow(clippy::too_many_arguments)]
8358 fn emit_isolated_blend_source(
8359 &mut self,
8360 bounds: repose_core::Rect,
8361 blend: repose_core::BlendMode,
8362 current_transform: &repose_core::Transform,
8363 current_pass: &mut Pass,
8364 active_clips: &[ActiveClip],
8365 passes: &mut Vec<Pass>,
8366 id_head: &mut u32,
8367 pass_id_head: &mut u64,
8368 ids_used: &mut Vec<u32>,
8369 scissor: (u32, u32, u32, u32),
8370 encoder: &mut wgpu::CommandEncoder,
8371 fb_w: f32,
8372 fb_h: f32,
8373 emit_source: impl FnOnce(
8374 &mut Self,
8375 &mut Pass,
8376 &repose_core::Transform,
8377 f32,
8378 f32,
8379 &mut wgpu::CommandEncoder,
8380 ),
8381 ) {
8382 if scissor.2 == 0 || scissor.3 == 0 || active_clips.iter().any(ActiveClip::blocked) {
8383 return;
8384 }
8385 let parent_target = current_pass.target;
8386 let (parent_w, parent_h) = match parent_target {
8387 PassTarget::Surface => (fb_w, fb_h),
8388 PassTarget::Layer(id) => match self.layer_pool.get(&id) {
8389 Some(layer) => (layer.width as f32, layer.height as f32),
8390 None => return,
8391 },
8392 };
8393 let local_rect = intersect(
8394 bounds,
8395 repose_core::Rect {
8396 x: 0.0,
8397 y: 0.0,
8398 w: parent_w,
8399 h: parent_h,
8400 },
8401 );
8402 if !local_rect.x.is_finite()
8403 || !local_rect.y.is_finite()
8404 || !local_rect.w.is_finite()
8405 || !local_rect.h.is_finite()
8406 || local_rect.w <= 0.0
8407 || local_rect.h <= 0.0
8408 || local_rect.w > self.device.limits().max_texture_dimension_2d as f32
8409 || local_rect.h > self.device.limits().max_texture_dimension_2d as f32
8410 {
8411 return;
8412 }
8413 let layer_rect = repose_core::Rect {
8414 x: local_rect.x.floor(),
8415 y: local_rect.y.floor(),
8416 w: (local_rect.x + local_rect.w).ceil() - local_rect.x.floor(),
8417 h: (local_rect.y + local_rect.h).ceil() - local_rect.y.floor(),
8418 };
8419 let w = layer_rect.w;
8420 let h = layer_rect.h;
8421 let local_rect = layer_rect;
8422 if ids_used.len() >= MAX_TRANSIENT_GRAPHICS_LAYERS {
8423 return;
8424 }
8425 let layer_id = *id_head;
8426 *id_head = id_head.wrapping_add(1);
8427 ids_used.push(layer_id);
8428
8429 let snapshot_format = match parent_target {
8433 PassTarget::Surface if self.working_space => wgpu::TextureFormat::Rgba16Float,
8434 PassTarget::Surface => self.output_format,
8435 PassTarget::Layer(_) => self.layer_target_format(),
8436 };
8437 if !self.alloc_blend_snapshot(layer_id, w as u32, h as u32, snapshot_format) {
8438 log::warn!("blend snapshot budget exhausted; blend skipped");
8439 return;
8440 }
8441 let source_pass_id = *pass_id_head;
8442 *pass_id_head += 1;
8443 let resumed_pass_id = *pass_id_head;
8444 *pass_id_head += 1;
8445 self.blend_copies.push(BlendCopy {
8446 pass_id: resumed_pass_id,
8447 blend_id: layer_id,
8448 target: parent_target,
8449 region: layer_rect,
8450 });
8451
8452 let mut layer_pass = Pass {
8456 id: source_pass_id,
8457 target: PassTarget::Layer(layer_id),
8458 initial_scissor: (0, 0, w as u32, h as u32),
8459 active_scissor: Some((0, 0, w as u32, h as u32)),
8460 clear_color: Some([0.0, 0.0, 0.0, 0.0]),
8461 cmds: Vec::new(),
8462 };
8463 self.replay_active_clips(
8464 active_clips,
8465 current_transform,
8466 (local_rect.x, local_rect.y),
8467 (w, h),
8468 &mut layer_pass,
8469 encoder,
8470 );
8471 self.get_or_create_layer(layer_id, w as u32, h as u32, layer_rect, true);
8472 let shift = repose_core::Transform::translate(-local_rect.x, -local_rect.y);
8473 let local = current_transform.combine(&shift);
8474 emit_source(self, &mut layer_pass, &local, w, h, encoder);
8475 let saved = std::mem::replace(
8476 current_pass,
8477 Pass {
8478 id: resumed_pass_id,
8479 target: parent_target,
8480 initial_scissor: scissor,
8481 active_scissor: Some(scissor),
8482 clear_color: None,
8483 cmds: Vec::new(),
8484 },
8485 );
8486 passes.push(layer_pass);
8487 passes.push(saved);
8488
8489 let ndc = {
8493 let cx = local_rect.x + local_rect.w * 0.5;
8494 let cy = local_rect.y + local_rect.h * 0.5;
8495 let ndc_cx = (cx / parent_w) * 2.0 - 1.0;
8496 let ndc_cy = 1.0 - (cy / parent_h) * 2.0;
8497 let ndc_w = (local_rect.w / parent_w) * 2.0;
8498 let ndc_h = (local_rect.h / parent_h) * 2.0;
8499 [ndc_cx, ndc_cy, ndc_w, ndc_h]
8500 };
8501 let inst = BlendInstance {
8502 xywh: ndc,
8503 uv: [0.0, 0.0, 1.0, 1.0],
8504 color: [1.0, 1.0, 1.0, 1.0],
8505 fwd_mat: [1.0, 0.0, 0.0, 1.0],
8506 mode: blend.shader_mode(),
8507 _pad: [0.0; 3],
8508 };
8509 if self
8510 .blend_ring
8511 .grow_to_fit(
8512 &self.device,
8513 encoder,
8514 std::mem::size_of::<BlendInstance>() as u64,
8515 )
8516 .is_err()
8517 {
8518 return;
8519 }
8520 let bytes = bytemuck::bytes_of(&inst);
8521 let Ok(off) = self.blend_ring.alloc_write(&self.queue, bytes) else {
8522 return;
8523 };
8524 current_pass.cmds.push(Cmd::BlendLayer {
8525 off,
8526 cnt: 1,
8527 src_layer: layer_id,
8528 dst_layer: Some(layer_id),
8529 parent: parent_target,
8530 scissor,
8531 });
8532 }
8533
8534 #[allow(clippy::too_many_arguments)]
8538 fn emit_isolated_blend(
8539 &mut self,
8540 mesh: std::sync::Arc<repose_core::VectorMeshData>,
8541 transform: [f32; 6],
8542 paint: repose_core::PaintDesc,
8543 blend: repose_core::BlendMode,
8544 current_transform: &repose_core::Transform,
8545 current_pass: &mut Pass,
8546 active_clips: &[ActiveClip],
8547 passes: &mut Vec<Pass>,
8548 id_head: &mut u32,
8549 pass_id_head: &mut u64,
8550 ids_used: &mut Vec<u32>,
8551 scissor: (u32, u32, u32, u32),
8552 encoder: &mut wgpu::CommandEncoder,
8553 fb_w: f32,
8554 fb_h: f32,
8555 ) {
8556 let affine = combine_mesh_affine(current_transform, transform);
8557 let bounds = mesh_aabb(&mesh, affine);
8558 self.emit_isolated_blend_source(
8559 bounds,
8560 blend,
8561 current_transform,
8562 current_pass,
8563 active_clips,
8564 passes,
8565 id_head,
8566 pass_id_head,
8567 ids_used,
8568 scissor,
8569 encoder,
8570 fb_w,
8571 fb_h,
8572 |slf, pass, local, _w, _h, _encoder| {
8573 slf.emit_vector_mesh(
8574 local,
8575 &mesh,
8576 transform,
8577 &paint,
8578 repose_core::BlendMode::Alpha,
8579 &mut pass.cmds,
8580 );
8581 },
8582 );
8583 }
8584
8585 #[allow(clippy::too_many_arguments)]
8591 fn emit_isolated_image(
8592 &mut self,
8593 draw_rect: repose_core::Rect,
8594 handle: u64,
8595 uv_rect: [f32; 4],
8596 tint: repose_core::Color,
8597 tile: bool,
8598 filter: ImageFilter,
8599 blend: repose_core::BlendMode,
8600 current_transform: &repose_core::Transform,
8601 current_pass: &mut Pass,
8602 active_clips: &[ActiveClip],
8603 passes: &mut Vec<Pass>,
8604 id_head: &mut u32,
8605 pass_id_head: &mut u64,
8606 ids_used: &mut Vec<u32>,
8607 scissor: (u32, u32, u32, u32),
8608 encoder: &mut wgpu::CommandEncoder,
8609 fb_w: f32,
8610 fb_h: f32,
8611 ) {
8612 let bounds = affine_aabb(current_transform, &draw_rect);
8613 self.emit_isolated_blend_source(
8614 bounds,
8615 blend,
8616 current_transform,
8617 current_pass,
8618 active_clips,
8619 passes,
8620 id_head,
8621 pass_id_head,
8622 ids_used,
8623 scissor,
8624 encoder,
8625 fb_w,
8626 fb_h,
8627 |slf, pass, local, w, h, encoder| {
8628 let (ndc_center, fwd_mat) = rect_to_instance_ndc(draw_rect, local, w, h);
8629 let inst = GlyphInstance {
8630 xywh: ndc_center,
8631 uv: uv_rect,
8632 color: tint.to_linear(),
8633 fwd_mat,
8634 };
8635 if let Some((off, _)) =
8636 slf.glyph_color
8637 .upload(&slf.device, &slf.queue, encoder, &[inst])
8638 {
8639 pass.cmds.push(Cmd::ImageRgba {
8640 off,
8641 cnt: 1,
8642 handle,
8643 filter,
8644 tile,
8645 blend: repose_core::BlendMode::Alpha,
8646 });
8647 }
8648 },
8649 );
8650 }
8651
8652 fn recycle_blend_snapshot(&mut self, snapshot: BlendSnapshot) {
8653 let key = snapshot.key;
8654 if let Some(old) = self.blend_snapshot_pool.insert(key, snapshot) {
8655 self.blend_snapshot_bytes_total =
8656 self.blend_snapshot_bytes_total.saturating_sub(old.bytes);
8657 }
8658 while self.blend_snapshot_bytes_total > MAX_BLEND_SNAPSHOT_BYTES
8659 || self.blend_snapshot_pool.len() > MAX_BLEND_SNAPSHOT_POOL_ENTRIES
8660 {
8661 let Some(victim) = self.blend_snapshot_pool.keys().next().copied() else {
8662 break;
8663 };
8664 if let Some(snapshot) = self.blend_snapshot_pool.remove(&victim) {
8665 self.blend_snapshot_bytes_total = self
8666 .blend_snapshot_bytes_total
8667 .saturating_sub(snapshot.bytes);
8668 }
8669 }
8670 }
8671
8672 fn recycle_blend_snapshots(&mut self) {
8673 for (_, snapshot) in std::mem::take(&mut self.blend_snapshots) {
8674 self.recycle_blend_snapshot(snapshot);
8675 }
8676 }
8677
8678 fn alloc_blend_snapshot(
8679 &mut self,
8680 layer_id: u32,
8681 w: u32,
8682 h: u32,
8683 format: wgpu::TextureFormat,
8684 ) -> bool {
8685 let key = BlendSnapshotKey {
8686 format,
8687 width: w,
8688 height: h,
8689 };
8690 if self
8691 .blend_snapshots
8692 .get(&layer_id)
8693 .is_some_and(|snapshot| snapshot.key == key)
8694 {
8695 return true;
8696 }
8697 if w == 0 || h == 0 {
8698 return false;
8699 }
8700 if let Some(old) = self.blend_snapshots.remove(&layer_id) {
8701 self.recycle_blend_snapshot(old);
8702 }
8703 if let Some(snapshot) = self.blend_snapshot_pool.remove(&key) {
8704 self.blend_snapshots.insert(layer_id, snapshot);
8705 return true;
8706 }
8707 let Ok(bytes) = texture_storage_bytes(format, w, h) else {
8708 return false;
8709 };
8710 if bytes > MAX_BLEND_SNAPSHOT_BYTES {
8711 return false;
8712 }
8713 while self.blend_snapshot_bytes_total.saturating_add(bytes) > MAX_BLEND_SNAPSHOT_BYTES
8714 || self.gpu_bytes_total().saturating_add(bytes) > self.gpu_budget_bytes
8715 {
8716 let Some(victim) = self.blend_snapshot_pool.keys().next().copied() else {
8717 return false;
8718 };
8719 let Some(snapshot) = self.blend_snapshot_pool.remove(&victim) else {
8720 return false;
8721 };
8722 self.blend_snapshot_bytes_total = self
8723 .blend_snapshot_bytes_total
8724 .saturating_sub(snapshot.bytes);
8725 }
8726 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
8727 label: Some("blend backdrop snapshot"),
8728 size: wgpu::Extent3d {
8729 width: w,
8730 height: h,
8731 depth_or_array_layers: 1,
8732 },
8733 mip_level_count: 1,
8734 sample_count: 1,
8735 dimension: wgpu::TextureDimension::D2,
8736 format,
8737 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
8738 view_formats: &[],
8739 });
8740 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
8741 let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
8742 label: Some("blend snapshot bind"),
8743 layout: &self.image_bind_layout_rgba,
8744 entries: &[
8745 wgpu::BindGroupEntry {
8746 binding: 0,
8747 resource: wgpu::BindingResource::TextureView(&view),
8748 },
8749 wgpu::BindGroupEntry {
8750 binding: 1,
8751 resource: wgpu::BindingResource::Sampler(&self.layer_sampler),
8752 },
8753 ],
8754 });
8755 self.blend_snapshot_bytes_total = self.blend_snapshot_bytes_total.saturating_add(bytes);
8756 self.blend_snapshots.insert(
8757 layer_id,
8758 BlendSnapshot {
8759 key,
8760 texture: tex,
8761 bind,
8762 bytes,
8763 },
8764 );
8765 true
8766 }
8767
8768 fn remove_blend_snapshot(&mut self, layer_id: u32) {
8769 if let Some(snapshot) = self.blend_snapshots.remove(&layer_id) {
8770 self.recycle_blend_snapshot(snapshot);
8771 }
8772 }
8773
8774 fn blend_snapshot_texture(&self, layer_id: u32) -> Option<wgpu::Texture> {
8775 self.blend_snapshots
8776 .get(&layer_id)
8777 .map(|s| s.texture.clone())
8778 }
8779
8780 #[allow(clippy::too_many_arguments)]
8781 fn emit_vector_mesh(
8782 &mut self,
8783 current_transform: &Transform,
8784 mesh: &Arc<repose_core::VectorMeshData>,
8785 transform: [f32; 6],
8786 paint: &repose_core::PaintDesc,
8787 blend: repose_core::BlendMode,
8788 cmds: &mut Vec<Cmd>,
8789 ) {
8790 let affine = combine_mesh_affine(current_transform, transform);
8791 let Some(mesh_gpu) = self.upload_mesh_geometry(mesh) else {
8792 return;
8793 };
8794 let Some(uoff) = self.alloc_mesh_uniform(mesh_uniform_from_paint(affine, paint)) else {
8795 return;
8796 };
8797 cmds.push(Cmd::VectorMesh {
8798 mesh: mesh_gpu,
8799 uoff,
8800 blend,
8801 });
8802 }
8803
8804 fn touch_callback_scope(&mut self, scope: CallbackScopeKey) {
8805 self.callback_scope_clock = self.callback_scope_clock.wrapping_add(1);
8806 let tick = self.callback_scope_clock;
8807 self.callback_scope_uses.insert(
8808 scope,
8809 CallbackScopeUse {
8810 tick,
8811 frame: self.frame_index,
8812 },
8813 );
8814 }
8815
8816 fn remove_callback_scope(&mut self, scope: &CallbackScopeKey) {
8817 self.callback_scoped_resources.remove(scope);
8818 self.callback_scope_uses.remove(scope);
8819 self.callback_scope_payloads.remove(scope);
8820 }
8821
8822 fn evict_callback_scope(&mut self, protected: &HashSet<CallbackScopeKey>) -> bool {
8823 let candidate = self
8824 .callback_scoped_resources
8825 .keys()
8826 .filter(|scope| !protected.contains(*scope))
8827 .min_by_key(|scope| {
8828 self.callback_scope_uses
8829 .get(scope)
8830 .map(|usage| usage.tick)
8831 .unwrap_or(0)
8832 })
8833 .cloned();
8834 let Some(scope) = candidate else {
8835 return false;
8836 };
8837 self.remove_callback_scope(&scope);
8838 true
8839 }
8840
8841 fn prune_callback_scopes(&mut self) {
8842 let current_frame = self.frame_index;
8843 let mut dead: Vec<(CallbackScopeKey, u64)> = self
8844 .callback_scope_uses
8845 .iter()
8846 .filter_map(|(scope, usage)| {
8847 let payload_dead = self
8848 .callback_scope_payloads
8849 .get(scope)
8850 .is_none_or(|weak| weak.upgrade().is_none());
8851 (payload_dead && !scope.callback.reusable).then(|| (scope.clone(), usage.tick))
8852 })
8853 .collect();
8854 dead.sort_by_key(|(_, tick)| *tick);
8855 for (scope, _) in dead {
8856 self.remove_callback_scope(&scope);
8857 }
8858
8859 if self.callback_scoped_resources.len() <= MAX_CALLBACK_SCOPES {
8860 return;
8861 }
8862 let mut inactive: Vec<(CallbackScopeKey, u64)> = self
8863 .callback_scope_uses
8864 .iter()
8865 .filter(|(_, usage)| usage.frame != current_frame)
8866 .map(|(scope, usage)| (scope.clone(), usage.tick))
8867 .collect();
8868 inactive.sort_by_key(|(_, tick)| *tick);
8869 for (scope, _) in inactive {
8870 if self.callback_scoped_resources.len() <= MAX_CALLBACK_SCOPES {
8871 break;
8872 }
8873 self.remove_callback_scope(&scope);
8874 }
8875 }
8876
8877 pub fn begin_frame(&mut self) -> anyhow::Result<()> {
8878 if self.frame_active {
8879 self.last_render_error = Some("renderer frame is already active".into());
8880 anyhow::bail!("renderer frame is already active");
8881 }
8882 self.last_render_error = None;
8883 self.frame_active = true;
8884 self.frame_index = self.frame_index.wrapping_add(1);
8885 self.apply_atlas_pending();
8886 self.slug_cache.next_frame();
8887 if let Some(composite) = self.callback_resources.get_mut::<DepthComposite>() {
8888 composite.begin_frame();
8889 }
8890 for resources in self.callback_scoped_resources.values_mut() {
8891 if let Some(composite) = resources.get_mut::<DepthComposite>() {
8892 composite.begin_frame();
8893 }
8894 }
8895 Ok(())
8896 }
8897
8898 pub fn end_frame(&mut self) {
8899 if !self.frame_active {
8900 return;
8901 }
8902 if let Some(composite) = self.callback_resources.get_mut::<DepthComposite>() {
8903 composite.end_frame();
8904 }
8905 for resources in self.callback_scoped_resources.values_mut() {
8906 if let Some(composite) = resources.get_mut::<DepthComposite>() {
8907 composite.end_frame();
8908 }
8909 }
8910 self.prune_callback_scopes();
8911 self.evict_unused_images();
8912 self.frame_active = false;
8913 }
8914
8915 pub fn last_render_error(&self) -> Option<&str> {
8916 self.last_render_error.as_deref()
8917 }
8918
8919 pub fn render_scene_to_encoder(
8920 &mut self,
8921 scene: &Scene,
8922 encoder: &mut wgpu::CommandEncoder,
8923 target_view: &wgpu::TextureView,
8924 clear_color_override: Option<[f64; 4]>,
8925 ) {
8926 self.render_scene_to_encoder_with_texture(
8927 scene,
8928 encoder,
8929 target_view,
8930 None,
8931 clear_color_override,
8932 )
8933 }
8934
8935 pub fn render_scene_to_encoder_with_texture(
8941 &mut self,
8942 scene: &Scene,
8943 encoder: &mut wgpu::CommandEncoder,
8944 target_view: &wgpu::TextureView,
8945 target_texture: Option<&wgpu::Texture>,
8946 clear_color_override: Option<[f64; 4]>,
8947 ) {
8948 if self.output_width == 0 || self.output_height == 0 {
8949 return;
8950 }
8951 self.sync_text_caches();
8952 fn visible(aabb: repose_core::Rect, clip: repose_core::Rect) -> bool {
8953 if !aabb.x.is_finite()
8954 || !aabb.y.is_finite()
8955 || !aabb.w.is_finite()
8956 || !aabb.h.is_finite()
8957 || aabb.w <= 0.0
8958 || aabb.h <= 0.0
8959 {
8960 return false;
8961 }
8962 let aabb = repose_core::Rect {
8963 x: aabb.x - 1.0,
8964 y: aabb.y - 1.0,
8965 w: aabb.w + 2.0,
8966 h: aabb.h + 2.0,
8967 };
8968 let clipped = intersect(aabb, clip);
8969 clipped.x.is_finite()
8970 && clipped.y.is_finite()
8971 && clipped.w.is_finite()
8972 && clipped.h.is_finite()
8973 && clipped.w > 0.0
8974 && clipped.h > 0.0
8975 }
8976
8977 fn to_ndc(x: f32, y: f32, w: f32, h: f32, fb_w: f32, fb_h: f32) -> [f32; 4] {
8978 let x0 = (x / fb_w) * 2.0 - 1.0;
8979 let y0 = 1.0 - (y / fb_h) * 2.0;
8980 let x1 = ((x + w) / fb_w) * 2.0 - 1.0;
8981 let y1 = 1.0 - ((y + h) / fb_h) * 2.0;
8982 let min_x = x0.min(x1);
8983 let min_y = y0.min(y1);
8984 let w_ndc = (x1 - x0).abs();
8985 let h_ndc = (y1 - y0).abs();
8986 [min_x, min_y, w_ndc, h_ndc]
8987 }
8988
8989 fn to_scissor(r: &repose_core::Rect, fb_w: u32, fb_h: u32) -> (u32, u32, u32, u32) {
8990 if fb_w == 0
8991 || fb_h == 0
8992 || !r.x.is_finite()
8993 || !r.y.is_finite()
8994 || !r.w.is_finite()
8995 || !r.h.is_finite()
8996 || r.w <= 0.0
8997 || r.h <= 0.0
8998 {
8999 return (0, 0, 0, 0);
9000 }
9001 let x0 = r.x.ceil().max(0.0).min(fb_w as f32);
9006 let y0 = r.y.ceil().max(0.0).min(fb_h as f32);
9007 let x1 = (r.x + r.w).floor().max(x0).min(fb_w as f32);
9008 let y1 = (r.y + r.h).floor().max(y0).min(fb_h as f32);
9009 if x1 <= x0 || y1 <= y0 {
9010 return (0, 0, 0, 0);
9011 }
9012 (x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32)
9013 }
9014
9015 let fb_w = self.output_width as f32;
9016 let fb_h = self.output_height as f32;
9017
9018 let mut passes: Vec<Pass> = Vec::with_capacity(1);
9019 let clear_color = clear_color_override
9020 .map(|color| {
9021 let alpha = color[3];
9022 [color[0] * alpha, color[1] * alpha, color[2] * alpha, alpha]
9023 })
9024 .unwrap_or_else(|| {
9025 let color = scene.clear_color.to_linear();
9026 let alpha = color[3] as f64;
9027 [
9028 color[0] as f64 * alpha,
9029 color[1] as f64 * alpha,
9030 color[2] as f64 * alpha,
9031 alpha,
9032 ]
9033 });
9034 let mut next_pass_id = 0u64;
9035 let mut current_pass: Pass = Pass {
9036 id: next_pass_id,
9037 target: PassTarget::Surface,
9038 initial_scissor: (0, 0, self.output_width, self.output_height),
9039 active_scissor: Some((0, 0, self.output_width, self.output_height)),
9040 clear_color: Some([
9041 clear_color[0] as f32,
9042 clear_color[1] as f32,
9043 clear_color[2] as f32,
9044 clear_color[3] as f32,
9045 ]),
9046 cmds: Vec::with_capacity(scene.nodes.len()),
9047 };
9048 next_pass_id += 1;
9049 let mut target_stack: Vec<PassTarget> = Vec::new();
9050 let mut layer_stack: Vec<LayerState> = Vec::new();
9051 let mut current_target_size: (f32, f32) = (fb_w, fb_h);
9052
9053 struct Batch {
9054 rects: Vec<RectInstance>,
9055 borders: Vec<BorderInstance>,
9056 ellipses: Vec<EllipseInstance>,
9057 e_borders: Vec<EllipseBorderInstance>,
9058 arcs: Vec<ArcInstance>,
9059 masks: Vec<GlyphInstance>,
9060 colors: Vec<GlyphInstance>,
9061 }
9062
9063 impl Batch {
9064 fn new() -> Self {
9065 Self {
9066 rects: vec![],
9067 borders: vec![],
9068 ellipses: vec![],
9069 e_borders: vec![],
9070 arcs: vec![],
9071 masks: vec![],
9072 colors: vec![],
9073 }
9074 }
9075
9076 fn is_empty(&self) -> bool {
9077 self.rects.is_empty()
9078 && self.borders.is_empty()
9079 && self.ellipses.is_empty()
9080 && self.e_borders.is_empty()
9081 && self.arcs.is_empty()
9082 && self.masks.is_empty()
9083 && self.colors.is_empty()
9084 }
9085
9086 fn flush(
9087 &mut self,
9088 pipes: (
9089 &mut InstancedPipe<RectInstance>,
9090 &mut InstancedPipe<BorderInstance>,
9091 &mut InstancedPipe<EllipseInstance>,
9092 &mut InstancedPipe<EllipseBorderInstance>,
9093 &mut InstancedPipe<ArcInstance>,
9094 ),
9095 glyph_pipes: (
9096 &mut InstancedPipe<GlyphInstance>,
9097 &mut InstancedPipe<GlyphInstance>,
9098 ),
9099 device: &wgpu::Device,
9100 queue: &wgpu::Queue,
9101 encoder: &mut wgpu::CommandEncoder,
9102 cmds: &mut Vec<Cmd>,
9103 ) {
9104 let (rects, borders, ellipses, e_borders, arcs) = pipes;
9105 let (masks, colors) = glyph_pipes;
9106
9107 macro_rules! flush_one {
9108 ($buf:ident, $pipe:expr, $variant:ident) => {
9109 if !self.$buf.is_empty() {
9110 if let Some((off, cnt)) =
9111 $pipe.upload(device, queue, encoder, &self.$buf)
9112 {
9113 cmds.push(Cmd::$variant { off, cnt });
9114 }
9115 self.$buf.clear();
9116 }
9117 };
9118 }
9119
9120 flush_one!(rects, rects, Rect);
9121 flush_one!(borders, borders, Border);
9122 flush_one!(ellipses, ellipses, Ellipse);
9123 flush_one!(e_borders, e_borders, EllipseBorder);
9124 flush_one!(arcs, arcs, Arc);
9125 flush_one!(masks, masks, GlyphsMask);
9126 flush_one!(colors, colors, GlyphsColor);
9127 }
9128 }
9129
9130 self.rects.reset();
9131 self.borders.reset();
9132 self.ellipses.reset();
9133 self.ellipse_borders.reset();
9134 self.arcs.reset();
9135 self.glyph_mask.reset();
9136 self.glyph_color.reset();
9137 self.clip_ring.reset();
9138 self.blur_ring.reset();
9139 self.nv12.reset();
9140
9141 self.slug_ring.reset();
9142 self.mesh_uniform_head = 0;
9143 self.projective_ring.reset();
9144 self.blend_ring.reset();
9145 for id in self.flatten_layer_ids.drain(..) {
9146 if let Some(layer) = self.layer_pool.remove(&id) {
9147 self.layer_bytes_total = self.layer_bytes_total.saturating_sub(layer.bytes);
9148 if layer.transient {
9149 self.transient_layer_bytes_total =
9150 self.transient_layer_bytes_total.saturating_sub(layer.bytes);
9151 }
9152 }
9153 }
9154 self.producer_layer_ids.clear();
9155 self.recycle_blend_snapshots();
9156 self.blend_copies.clear();
9157 let mut batch = Batch::new();
9158 let mut image_run: Option<(
9161 u64,
9162 ImageFilter,
9163 bool,
9164 repose_core::BlendMode,
9165 Vec<GlyphInstance>,
9166 )> = None;
9167 let mut nv12_run: Option<(u64, ImageFilter, repose_core::BlendMode, Vec<Nv12Instance>)> =
9168 None;
9169 let mut slug_verts_local: Vec<slug::TessVertex> = Vec::new();
9170 let mut transform_stack: Vec<Transform> = vec![Transform::identity()];
9171 let mut flatten_stack: Vec<FlattenRecord> = Vec::new();
9172 let mut flatten_id_head: u32 = FLATTEN_ID_BASE;
9173 let mut flatten_ids_used: Vec<u32> = Vec::new();
9174 let mut scissor_stack: Vec<repose_core::Rect> = Vec::with_capacity(8);
9175 let mut active_clips: Vec<ActiveClip> = Vec::with_capacity(8);
9176 let mut root_clip_rect = repose_core::Rect {
9177 x: 0.0,
9178 y: 0.0,
9179 w: fb_w,
9180 h: fb_h,
9181 };
9182
9183 let mut current_prim: Option<&'static str> = None;
9184
9185 macro_rules! flush_if_prim_changed {
9186 ($prim:literal, $pipe:expr) => {
9187 if current_prim != Some($prim) {
9188 flush_batch!();
9189 current_prim = Some($prim);
9190 }
9191 };
9192 }
9193
9194 macro_rules! flush_image_runs {
9195 () => {{
9196 if let Some((handle, filter, tile, blend, instances)) = image_run.take()
9197 && let Some((off, cnt)) =
9198 self.glyph_color
9199 .upload(&self.device, &self.queue, encoder, &instances)
9200 {
9201 current_pass.cmds.push(Cmd::ImageRgba {
9202 off,
9203 cnt,
9204 handle,
9205 filter,
9206 tile,
9207 blend,
9208 });
9209 }
9210 if let Some((handle, filter, blend, instances)) = nv12_run.take()
9211 && let Some((off, cnt)) =
9212 self.nv12
9213 .upload(&self.device, &self.queue, encoder, &instances)
9214 {
9215 current_pass.cmds.push(Cmd::ImageNv12 {
9216 off,
9217 cnt,
9218 handle,
9219 filter,
9220 blend,
9221 });
9222 }
9223 }};
9224 }
9225
9226 macro_rules! flush_primitives {
9227 () => {{
9228 if !batch.is_empty() {
9229 batch.flush(
9230 (
9231 &mut self.rects,
9232 &mut self.borders,
9233 &mut self.ellipses,
9234 &mut self.ellipse_borders,
9235 &mut self.arcs,
9236 ),
9237 (&mut self.glyph_mask, &mut self.glyph_color),
9238 &self.device,
9239 &self.queue,
9240 encoder,
9241 &mut current_pass.cmds,
9242 )
9243 }
9244 }};
9245 }
9246
9247 macro_rules! flush_batch {
9248 () => {{
9249 flush_image_runs!();
9250 flush_primitives!();
9251 }};
9252 }
9253 let mut pending_layer_composite: Option<Cmd> = None;
9254 let shadow_layer_ids: HashSet<u32> = scene
9255 .nodes
9256 .iter()
9257 .filter_map(|node| match node {
9258 SceneNode::CompositeShadow { layer_id, .. } => Some(*layer_id),
9259 _ => None,
9260 })
9261 .collect();
9262 macro_rules! flush_pending_layer {
9263 () => {
9264 if let Some(command) = pending_layer_composite.take() {
9265 current_pass.cmds.push(command);
9266 }
9267 };
9268 }
9269 for node in &scene.nodes {
9270 let t_identity = Transform::identity();
9271 let current_transform = transform_stack.last().unwrap_or(&t_identity);
9272
9273 let image_spec = match node {
9277 SceneNode::Image {
9278 rect,
9279 handle,
9280 tint,
9281 style,
9282 } => Some((*rect, *handle, *tint, *style, repose_core::BlendMode::Alpha)),
9283 SceneNode::ImageBlended {
9284 rect,
9285 handle,
9286 tint,
9287 style,
9288 blend,
9289 } => Some((*rect, *handle, *tint, *style, *blend)),
9290 _ => None,
9291 };
9292 if let Some((rect, handle, tint, style, blend)) = image_spec {
9293 let ImagePaintStyle {
9294 fit,
9295 filter,
9296 source_rect,
9297 alignment,
9298 } = style;
9299 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9300 if !visible(affine_aabb(current_transform, &rect), clip) {
9301 continue;
9302 }
9303 flush_batch!();
9304 let (img_w, img_h, is_nv12) = match self.resolve_image_for_draw(handle) {
9305 Some(wh) => wh,
9306 None => {
9307 log::warn!("Image handle {} not found", handle);
9308 continue;
9309 }
9310 };
9311 let Some(source) = resolve_image_source(source_rect, img_w, img_h) else {
9312 log::warn!(
9313 "Image handle {} has invalid source rect {:?}",
9314 handle,
9315 source_rect
9316 );
9317 continue;
9318 };
9319 let Some((draw_rect, uv_rect)) = image_fit_geometry(rect, fit, alignment, &source)
9320 else {
9321 continue;
9322 };
9323 let tile = matches!(fit, ImageFit::Tile);
9324 let translucent_fixed_blend = matches!(
9328 blend,
9329 repose_core::BlendMode::Add
9330 | repose_core::BlendMode::Multiply
9331 | repose_core::BlendMode::Screen
9332 | repose_core::BlendMode::Darken
9333 | repose_core::BlendMode::Lighten
9334 ) && tint.3 < 255;
9335 if blend.needs_isolation() || translucent_fixed_blend {
9336 if is_nv12 {
9337 log::warn!("blend on an NV12 image is not supported");
9340 } else {
9341 let blend_scissor = to_scissor(
9342 &scissor_stack.last().copied().unwrap_or(root_clip_rect),
9343 current_target_size.0 as u32,
9344 current_target_size.1 as u32,
9345 );
9346 self.emit_isolated_image(
9347 draw_rect,
9348 handle,
9349 uv_rect,
9350 tint,
9351 tile,
9352 filter,
9353 blend,
9354 current_transform,
9355 &mut current_pass,
9356 &active_clips,
9357 &mut passes,
9358 &mut flatten_id_head,
9359 &mut next_pass_id,
9360 &mut flatten_ids_used,
9361 blend_scissor,
9362 encoder,
9363 fb_w,
9364 fb_h,
9365 );
9366 }
9367 continue;
9368 }
9369 let (ndc_center, fwd_mat) = rect_to_instance_ndc(
9370 draw_rect,
9371 current_transform,
9372 current_target_size.0,
9373 current_target_size.1,
9374 );
9375 if is_nv12 {
9376 if image_run.is_some() {
9377 flush_image_runs!();
9378 }
9379 let (uv_x_offset, uv_y_offset) =
9380 if let Some(ImageTex::Nv12 {
9381 w, h, color_info, ..
9382 }) = self.images.get(&handle)
9383 {
9384 let chroma_w = w.div_ceil(2).max(1) as f32;
9385 let chroma_h = h.div_ceil(2).max(1) as f32;
9386 match color_info.chroma_siting {
9387 ChromaSiting::Center => (0.0, 0.0),
9388 ChromaSiting::Left => (-0.5 / chroma_w, 0.0),
9389 ChromaSiting::TopLeft => (-0.5 / chroma_w, -0.5 / chroma_h),
9390 }
9391 } else {
9392 (0.0, 0.0)
9393 };
9394 let inst = Nv12Instance {
9395 xywh: ndc_center,
9396 uv: uv_rect,
9397 color: tint.to_linear(),
9398 uv_x_offset,
9399 uv_y_offset,
9400 fwd_mat,
9401 };
9402 if nv12_run
9403 .as_ref()
9404 .is_some_and(|(run_handle, run_filter, run_blend, _)| {
9405 *run_handle != handle || *run_filter != filter || *run_blend != blend
9406 })
9407 {
9408 flush_image_runs!();
9409 }
9410 match &mut nv12_run {
9411 Some((run_handle, run_filter, run_blend, instances))
9412 if *run_handle == handle
9413 && *run_filter == filter
9414 && *run_blend == blend =>
9415 {
9416 instances.push(inst)
9417 }
9418 _ => nv12_run = Some((handle, filter, blend, vec![inst])),
9419 }
9420 } else {
9421 if nv12_run.is_some() {
9422 flush_image_runs!();
9423 }
9424 let inst = GlyphInstance {
9425 xywh: ndc_center,
9426 uv: uv_rect,
9427 color: tint.to_linear(),
9428 fwd_mat,
9429 };
9430 if image_run.as_ref().is_some_and(
9431 |(run_handle, run_filter, run_tile, run_blend, _)| {
9432 *run_handle != handle
9433 || *run_filter != filter
9434 || *run_tile != tile
9435 || *run_blend != blend
9436 },
9437 ) {
9438 flush_image_runs!();
9439 }
9440 match &mut image_run {
9441 Some((run_handle, run_filter, run_tile, run_blend, instances))
9442 if *run_handle == handle
9443 && *run_filter == filter
9444 && *run_tile == tile
9445 && *run_blend == blend =>
9446 {
9447 instances.push(inst)
9448 }
9449 _ => image_run = Some((handle, filter, tile, blend, vec![inst])),
9450 }
9451 }
9452 continue;
9453 }
9454
9455 match node {
9456 SceneNode::Rect {
9457 rect,
9458 brush,
9459 radius,
9460 } => {
9461 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9462 if !visible(affine_aabb(current_transform, rect), clip) {
9463 continue;
9464 }
9465 flush_if_prim_changed!("rect", &self.rects);
9466 let (ndc, fwd_mat) = rect_to_instance_ndc(
9467 *rect,
9468 current_transform,
9469 current_target_size.0,
9470 current_target_size.1,
9471 );
9472 let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
9473 brush_to_shape_fields(brush, rect, current_transform);
9474 batch.rects.push(RectInstance {
9475 xywh: ndc,
9476 radii: radius.map(|r| r.0),
9477 color0,
9478 color1,
9479 fwd_mat,
9480 grad: [grad_p0[0], grad_p0[1], grad_p1[0], grad_p1[1]],
9481 bits: pack_shape_bits(brush_type, grad_kind, tile_mode, 0),
9482 _pad: [0.0; 3],
9483 });
9484 }
9485 SceneNode::Border {
9486 rect,
9487 brush,
9488 width,
9489 radius,
9490 } => {
9491 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9492 if !visible(affine_aabb(current_transform, rect), clip) {
9493 continue;
9494 }
9495 flush_if_prim_changed!("border", &self.borders);
9496 let (ndc, fwd_mat) = rect_to_instance_ndc(
9497 *rect,
9498 current_transform,
9499 current_target_size.0,
9500 current_target_size.1,
9501 );
9502 let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
9503 brush_to_shape_fields(brush, rect, current_transform);
9504 batch.borders.push(BorderInstance {
9505 xywh: ndc,
9506 radii: radius.map(|r| r.0),
9507 color0,
9508 color1,
9509 fwd_mat,
9510 grad: [grad_p0[0], grad_p0[1], grad_p1[0], grad_p1[1]],
9511 bits: pack_shape_bits(brush_type, grad_kind, tile_mode, 0),
9512 stroke: width.0,
9513 _pad: [0.0; 2],
9514 });
9515 }
9516 SceneNode::Ellipse { rect, brush } => {
9517 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9518 if !visible(affine_aabb(current_transform, rect), clip) {
9519 continue;
9520 }
9521 flush_if_prim_changed!("ellipse", &self.ellipses);
9522 let (ndc, fwd_mat) = rect_to_instance_ndc(
9523 *rect,
9524 current_transform,
9525 current_target_size.0,
9526 current_target_size.1,
9527 );
9528 let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
9529 brush_to_shape_fields(brush, rect, current_transform);
9530 batch.ellipses.push(EllipseInstance {
9531 xywh: ndc,
9532 color0,
9533 color1,
9534 fwd_mat,
9535 grad: [grad_p0[0], grad_p0[1], grad_p1[0], grad_p1[1]],
9536 bits: pack_shape_bits(brush_type, grad_kind, tile_mode, 0),
9537 _pad: [0.0; 1],
9538 });
9539 }
9540 SceneNode::EllipseBorder { rect, brush, width } => {
9541 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9542 let cull_pad = width.0.abs() * 0.5 + 2.0;
9543 let cull_rect = repose_core::Rect {
9544 x: rect.x - cull_pad,
9545 y: rect.y - cull_pad,
9546 w: rect.w + cull_pad * 2.0,
9547 h: rect.h + cull_pad * 2.0,
9548 };
9549 if !visible(affine_aabb(current_transform, &cull_rect), clip) {
9550 continue;
9551 }
9552 flush_if_prim_changed!("ellipse_border", &self.ellipse_borders);
9553 let (ndc, fwd_mat) = rect_to_instance_ndc(
9554 *rect,
9555 current_transform,
9556 current_target_size.0,
9557 current_target_size.1,
9558 );
9559 let pad_px = width.0 * 0.5 + 2.0;
9560 let pad = (pad_px / current_target_size.0.max(1.0)) * 2.0;
9561 let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
9562 brush_to_shape_fields(brush, rect, current_transform);
9563 batch.e_borders.push(EllipseBorderInstance {
9564 xywh: ndc,
9565 color0,
9566 color1,
9567 fwd_mat,
9568 grad: [grad_p0[0], grad_p0[1], grad_p1[0], grad_p1[1]],
9569 bits: pack_shape_bits(brush_type, grad_kind, tile_mode, 0),
9570 stroke: width.0,
9571 pad,
9572 _pad: 0.0,
9573 });
9574 }
9575 SceneNode::Arc {
9576 rect,
9577 start_angle,
9578 sweep_angle,
9579 stroke_width,
9580 brush,
9581 cap,
9582 } => {
9583 if !start_angle.is_finite()
9584 || !sweep_angle.is_finite()
9585 || !stroke_width.0.is_finite()
9586 || !rect.x.is_finite()
9587 || !rect.y.is_finite()
9588 || !rect.w.is_finite()
9589 || !rect.h.is_finite()
9590 || rect.w <= 0.0
9591 || rect.h <= 0.0
9592 || stroke_width.0 <= 0.0
9593 || sweep_angle.abs() <= 1e-6
9594 {
9595 continue;
9596 }
9597 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9598 let cull_pad = stroke_width.0.abs() * 0.5 + 2.0;
9599 let cull_rect = repose_core::Rect {
9600 x: rect.x - cull_pad,
9601 y: rect.y - cull_pad,
9602 w: rect.w + cull_pad * 2.0,
9603 h: rect.h + cull_pad * 2.0,
9604 };
9605 if !visible(affine_aabb(current_transform, &cull_rect), clip) {
9606 continue;
9607 }
9608 flush_if_prim_changed!("arc", &self.arcs);
9609 let (ndc, fwd_mat) = rect_to_instance_ndc(
9610 *rect,
9611 current_transform,
9612 current_target_size.0.max(1.0),
9613 current_target_size.1.max(1.0),
9614 );
9615 let pad_px = stroke_width.0 * 0.5 + 2.0;
9616 let pad = (pad_px / current_target_size.0.max(1.0)) * 2.0;
9617 let cap_val = match cap {
9618 StrokeCap::Butt => 0.0,
9619 StrokeCap::Round => 1.0,
9620 StrokeCap::Square => 2.0,
9621 };
9622 let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
9623 brush_to_shape_fields(brush, rect, current_transform);
9624 let (start, sweep) = if *sweep_angle < 0.0 {
9625 (*start_angle + *sweep_angle, -*sweep_angle)
9626 } else {
9627 (*start_angle, *sweep_angle)
9628 };
9629 batch.arcs.push(ArcInstance {
9630 xywh: ndc,
9631 color0,
9632 color1,
9633 fwd_mat,
9634 grad: [grad_p0[0], grad_p0[1], grad_p1[0], grad_p1[1]],
9635 bits: pack_shape_bits(brush_type, grad_kind, tile_mode, cap_val as u32),
9636 start_angle: start,
9637 sweep_angle: sweep,
9638 stroke: stroke_width.0,
9639 pad,
9640 _pad: 0.0,
9641 });
9642 }
9643 SceneNode::Text {
9644 rect,
9645 text,
9646 color,
9647 size,
9648 style,
9649 } => {
9650 let TextPaintStyle {
9651 font_family,
9652 font_weight,
9653 font_style,
9654 text_decoration,
9655 letter_spacing,
9656 font_variation_settings,
9657 baseline_shift,
9658 draw_style,
9659 ..
9660 } = style;
9661 let px = size.0;
9662 if !px.is_finite()
9663 || px <= 0.0
9664 || !rect.x.is_finite()
9665 || !rect.y.is_finite()
9666 || !rect.w.is_finite()
9667 || !rect.h.is_finite()
9668 {
9669 continue;
9670 }
9671 let margin = px * 4.0 + letter_spacing.0.abs() * 2.0;
9672 let text_bounds = repose_core::Rect {
9673 x: rect.x - margin,
9674 y: rect.y - margin,
9675 w: rect.w + margin * 2.0,
9676 h: rect.h + margin * 2.0,
9677 };
9678 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
9679 if !visible(affine_aabb(current_transform, &text_bounds), clip) {
9680 continue;
9681 }
9682 flush_batch!(); let lh_ratio = rect.h / px;
9685 let fw = font_weight.0;
9686 let fs = if *font_style == FontStyle::Italic {
9687 1
9688 } else {
9689 0
9690 };
9691 let shaped = self.shape_text_cached(
9692 text,
9693 px,
9694 lh_ratio,
9695 *font_family,
9696 fw,
9697 fs,
9698 letter_spacing.0,
9699 font_variation_settings.as_ref(),
9700 );
9701 let baseline_y = shaped.glyphs.first().map(|g| rect.y + g.y);
9702
9703 let fwd = forward_rs_mat(current_transform);
9704 let has_linear = fwd != [1.0, 0.0, 0.0, 1.0];
9705
9706 let lin = current_transform.linear();
9707 let tr_x = current_transform.translate_x;
9708 let tr_y = current_transform.translate_y;
9709
9710 let make_glyph_instance =
9711 |gx: f32, gy: f32, gw: f32, gh: f32| -> ([f32; 4], [f32; 4]) {
9712 if has_linear {
9713 let gc_x = gx + gw * 0.5;
9714 let gc_y = gy + gh * 0.5;
9715 let wc_x = lin[0] * gc_x + lin[1] * gc_y + tr_x;
9716 let wc_y = lin[2] * gc_x + lin[3] * gc_y + tr_y;
9717 let ww = gw * current_transform.scale_x;
9718 let wh = gh * current_transform.scale_y;
9719 let ex = fwd[0].abs() * ww * 0.5 + fwd[1].abs() * wh * 0.5;
9720 let ey = fwd[2].abs() * ww * 0.5 + fwd[3].abs() * wh * 0.5;
9721 let ndc_tl = to_ndc(
9722 wc_x - ex,
9723 wc_y - ey,
9724 ex * 2.0,
9725 ey * 2.0,
9726 current_target_size.0,
9727 current_target_size.1,
9728 );
9729 let ndc = [
9730 ndc_tl[0] + ndc_tl[2] * 0.5,
9731 ndc_tl[1] + ndc_tl[3] * 0.5,
9732 ndc_tl[2],
9733 ndc_tl[3],
9734 ];
9735 (ndc, fwd)
9736 } else {
9737 let (sx, sy) = if current_transform.scale_x == 1.0
9738 && current_transform.scale_y == 1.0
9739 {
9740 (gx.round(), gy.round())
9741 } else {
9742 (gx, gy)
9743 };
9744 rect_to_instance_ndc(
9745 repose_core::Rect {
9746 x: sx,
9747 y: sy,
9748 w: gw,
9749 h: gh,
9750 },
9751 current_transform,
9752 current_target_size.0,
9753 current_target_size.1,
9754 )
9755 }
9756 };
9757
9758 let baseline_shift_y: f32 = px * baseline_shift.0;
9759
9760 let (
9761 draws_fill,
9762 is_stroke,
9763 stroke_width,
9764 stroke_cap,
9765 stroke_join,
9766 stroke_miter,
9767 stroke_path_effect,
9768 stroke_color,
9769 ) = match draw_style {
9770 repose_core::DrawStyle::Stroke {
9771 width,
9772 cap,
9773 join,
9774 miter,
9775 path_effect,
9776 } => (
9777 false,
9778 true,
9779 *width,
9780 *cap,
9781 *join,
9782 *miter,
9783 path_effect.clone(),
9784 None,
9785 ),
9786 repose_core::DrawStyle::FillAndStroke {
9787 width,
9788 cap,
9789 join,
9790 miter,
9791 path_effect,
9792 outline_color,
9793 } => (
9794 true,
9795 true,
9796 *width,
9797 *cap,
9798 *join,
9799 *miter,
9800 path_effect.clone(),
9801 outline_color.map(|c| {
9805 c.with_alpha_f32(
9806 f32::from(c.3) * f32::from(color.3) / (255.0 * 255.0),
9807 )
9808 }),
9809 ),
9810 _ => (
9811 true,
9812 false,
9813 0.0,
9814 repose_core::StrokeCap::Butt,
9815 repose_core::StrokeJoin::Miter,
9816 4.0,
9817 None,
9818 None,
9819 ),
9820 };
9821 let outline_under = stroke_color.is_some();
9825 let stroke_tess_key = if is_stroke {
9826 Some(slug::StrokeTessKey::new(
9827 stroke_width,
9828 stroke_cap,
9829 stroke_join,
9830 stroke_miter,
9831 &stroke_path_effect,
9832 ))
9833 } else {
9834 None
9835 };
9836
9837 let mut legacy_shaped = None;
9838 for (glyph_index, sg) in shaped.glyphs.iter().enumerate() {
9839 if self.slug_enabled {
9840 let ck = &sg.cache_key;
9841 let color_linear = color.to_linear();
9842 let stroke_color_linear = stroke_color.unwrap_or(*color).to_linear();
9843 let glyph_pos = (rect.x + sg.x, rect.y + sg.y + baseline_shift_y);
9844 let fill_key = draws_fill.then(|| {
9845 Self::slug_draw_key(
9846 ck,
9847 true,
9848 None,
9849 current_transform,
9850 glyph_pos,
9851 px,
9852 current_target_size,
9853 color_linear,
9854 )
9855 });
9856 let stroke_key = is_stroke.then(|| {
9857 Self::slug_draw_key(
9858 ck,
9859 false,
9860 stroke_tess_key.as_ref(),
9861 current_transform,
9862 glyph_pos,
9863 px,
9864 current_target_size,
9865 stroke_color_linear,
9866 )
9867 });
9868
9869 let fill_cache = fill_key
9872 .as_ref()
9873 .and_then(|k| self.get_cached_slug_vertices(k));
9874 let stroke_cache = stroke_key
9875 .as_ref()
9876 .and_then(|k| self.get_cached_slug_vertices(k));
9877 let fill_miss = fill_key.is_some() && fill_cache.is_none();
9878 let stroke_miss = stroke_key.is_some() && stroke_cache.is_none();
9879 if (fill_key.is_some() || stroke_key.is_some())
9880 && !fill_miss
9881 && !stroke_miss
9882 {
9883 let mut push = |cached: &Option<Arc<[slug::TessVertex]>>| {
9884 if let Some(vertices) = cached {
9885 slug_verts_local.extend_from_slice(vertices);
9886 }
9887 };
9888 if outline_under {
9889 push(&stroke_cache);
9890 push(&fill_cache);
9891 } else {
9892 push(&fill_cache);
9893 push(&stroke_cache);
9894 }
9895 continue;
9896 }
9897
9898 let outline = self.cached_glyph_outline(ck);
9899 if let Some(commands) = outline.as_ref() {
9900 let font_size_px = f32::from_bits(ck.font_size_bits);
9901 if draws_fill {
9902 self.slug_cache.get_or_insert(
9903 ck.clone(),
9904 font_size_px,
9905 commands.as_ref(),
9906 );
9907 }
9908 if is_stroke {
9909 self.slug_cache.get_or_insert_stroke(
9910 ck.clone(),
9911 font_size_px,
9912 commands.as_ref(),
9913 stroke_width,
9914 stroke_cap,
9915 stroke_join,
9916 stroke_miter,
9917 &stroke_path_effect,
9918 );
9919 }
9920 }
9921
9922 if let Some(entry) = self.slug_cache.get(ck) {
9923 let ox = rect.x + sg.x;
9924 let oy = rect.y + sg.y + baseline_shift_y;
9925 let tf = |x: f32, y: f32| -> (f32, f32) {
9926 if has_linear {
9927 (
9928 lin[0] * x + lin[1] * y + current_transform.translate_x,
9929 lin[2] * x + lin[3] * y + current_transform.translate_y,
9930 )
9931 } else {
9932 (
9933 x * current_transform.scale_x
9934 + current_transform.translate_x,
9935 y * current_transform.scale_y
9936 + current_transform.translate_y,
9937 )
9938 }
9939 };
9940 let tw = current_target_size.0;
9941 let th = current_target_size.1;
9942 let fill_vertices = if draws_fill && fill_cache.is_none() {
9943 entry.fill_vertices.clone().unwrap_or_default()
9944 } else {
9945 Vec::new()
9946 };
9947 let stroke_vertices = if stroke_cache.is_none() {
9948 stroke_tess_key
9949 .as_ref()
9950 .and_then(|key| entry.stroke_variants.get(key))
9951 .map(|variant| variant.vertices.clone())
9952 .unwrap_or_default()
9953 } else {
9954 Vec::new()
9955 };
9956 let mut emit =
9957 |verts: &Vec<[f32; 2]>,
9958 key: &Option<SlugDrawKey>,
9959 cached: &Option<Arc<[slug::TessVertex]>>,
9960 vertex_color: [f32; 4]| {
9961 if let Some(vertices) = cached {
9962 slug_verts_local.extend_from_slice(vertices);
9963 return;
9964 }
9965 let Some(key) = key.clone() else {
9966 return;
9967 };
9968 let transformed: Vec<slug::TessVertex> = verts
9969 .iter()
9970 .map(|v| {
9971 let (sx, sy) = tf(ox + v[0] * px, oy - v[1] * px);
9972 slug::TessVertex {
9973 ndc_pos: [
9974 sx / tw * 2.0 - 1.0,
9975 -(sy / th) * 2.0 + 1.0,
9976 ],
9977 color: vertex_color,
9978 }
9979 })
9980 .collect();
9981 let vertices = self.cached_slug_vertices(key, transformed);
9982 slug_verts_local.extend_from_slice(&vertices);
9983 };
9984 if outline_under {
9988 emit(
9989 &stroke_vertices,
9990 &stroke_key,
9991 &stroke_cache,
9992 stroke_color_linear,
9993 );
9994 emit(&fill_vertices, &fill_key, &fill_cache, color_linear);
9995 } else {
9996 emit(&fill_vertices, &fill_key, &fill_cache, color_linear);
9997 emit(
9998 &stroke_vertices,
9999 &stroke_key,
10000 &stroke_cache,
10001 stroke_color_linear,
10002 );
10003 }
10004 continue;
10005 }
10006 }
10007
10008 if !draws_fill {
10009 continue;
10010 }
10011 if legacy_shaped.is_none() {
10012 legacy_shaped = Some(self.legacy_shape_text_cached(
10013 text,
10014 px,
10015 lh_ratio,
10016 *font_family,
10017 fw,
10018 fs,
10019 letter_spacing.0,
10020 font_variation_settings.as_ref(),
10021 ));
10022 }
10023 let Some(legacy_glyph) = legacy_shaped
10024 .as_ref()
10025 .and_then(|glyphs| glyphs.get(glyph_index))
10026 else {
10027 continue;
10028 };
10029 let gx = rect.x + legacy_glyph.x + legacy_glyph.bearing_x;
10030 let gy =
10031 rect.y + legacy_glyph.y - legacy_glyph.bearing_y + baseline_shift_y;
10032 if let Some(info) =
10033 self.upload_glyph_color(legacy_glyph.cache_key.clone(), legacy_glyph.px)
10034 {
10035 let (ndc, fwd_mat) = make_glyph_instance(gx, gy, info.w, info.h);
10036 batch.colors.push(GlyphInstance {
10037 xywh: ndc,
10038 uv: [info.u0, info.v1, info.u1, info.v0],
10039 color: color.to_linear(),
10040 fwd_mat,
10041 });
10042 } else if let Some(info) =
10043 self.upload_glyph_mask(legacy_glyph.cache_key.clone(), legacy_glyph.px)
10044 {
10045 let (ndc, fwd_mat) = make_glyph_instance(gx, gy, info.w, info.h);
10046 batch.masks.push(GlyphInstance {
10047 xywh: ndc,
10048 uv: [info.u0, info.v1, info.u1, info.v0],
10049 color: color.to_linear(),
10050 fwd_mat,
10051 });
10052 }
10053 }
10054
10055 if !slug_verts_local.is_empty() {
10057 let bytes = bytemuck::cast_slice(&slug_verts_local);
10058 let byte_len = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
10059 let Ok(count) = u32::try_from(slug_verts_local.len()) else {
10060 slug_verts_local.clear();
10061 continue;
10062 };
10063 if self
10064 .slug_ring
10065 .grow_to_fit(&self.device, encoder, byte_len)
10066 .is_ok()
10067 && let Ok(off) = self.slug_ring.alloc_write(&self.queue, bytes)
10068 {
10069 current_pass
10070 .cmds
10071 .push(Cmd::GlyphsVector { off, cnt: count });
10072 }
10073 slug_verts_local.clear();
10074 }
10075
10076 if (text_decoration.underline || text_decoration.strikethrough)
10078 && let Some(baseline_y) = baseline_y
10079 {
10080 flush_batch!();
10081 current_prim = Some("rect");
10082 let deco_color = text_decoration.color.unwrap_or(*color);
10083 let thickness = (px * 0.07).max(1.0);
10084
10085 if text_decoration.underline {
10086 let dy = baseline_y + px * 0.1;
10087 let (ndc, fwd_mat) = rect_to_instance_ndc(
10088 repose_core::Rect {
10089 x: rect.x,
10090 y: dy,
10091 w: rect.w,
10092 h: thickness,
10093 },
10094 current_transform,
10095 current_target_size.0,
10096 current_target_size.1,
10097 );
10098 batch.rects.push(RectInstance {
10099 xywh: ndc,
10100 radii: [0.0; 4],
10101 color0: deco_color.to_linear(),
10102 color1: [0.0; 4],
10103 fwd_mat,
10104 grad: [0.0; 4],
10105 bits: 0,
10106 _pad: [0.0; 3],
10107 });
10108 }
10109 if text_decoration.strikethrough {
10110 let sy = baseline_y - px * 0.3;
10111 let (ndc, fwd_mat) = rect_to_instance_ndc(
10112 repose_core::Rect {
10113 x: rect.x,
10114 y: sy,
10115 w: rect.w,
10116 h: thickness,
10117 },
10118 current_transform,
10119 current_target_size.0,
10120 current_target_size.1,
10121 );
10122 batch.rects.push(RectInstance {
10123 xywh: ndc,
10124 radii: [0.0; 4],
10125 color0: deco_color.to_linear(),
10126 color1: [0.0; 4],
10127 fwd_mat,
10128 grad: [0.0; 4],
10129 bits: 0,
10130 _pad: [0.0; 3],
10131 });
10132 }
10133 }
10134 }
10135 SceneNode::Coverage {
10136 rect,
10137 handle,
10138 color,
10139 } => {
10140 flush_batch!();
10141 let Some((tile_w, tile_h)) = self.coverage_dimensions(*handle) else {
10144 log::warn!("Coverage handle {handle} not found");
10145 continue;
10146 };
10147 let draw_rect = repose_core::Rect {
10150 x: rect.x,
10151 y: rect.y,
10152 w: tile_w as f32,
10153 h: tile_h as f32,
10154 };
10155 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10156 if !visible(affine_aabb(current_transform, &draw_rect), clip) {
10157 continue;
10158 }
10159 let (ndc_center, fwd_mat) = rect_to_instance_ndc(
10160 draw_rect,
10161 current_transform,
10162 current_target_size.0,
10163 current_target_size.1,
10164 );
10165 let inst = GlyphInstance {
10166 xywh: ndc_center,
10167 uv: [0.0, 1.0, 1.0, 0.0],
10168 color: color.to_linear(),
10169 fwd_mat,
10170 };
10171 if let Some((off, _)) =
10172 self.glyph_color
10173 .upload(&self.device, &self.queue, encoder, &[inst])
10174 {
10175 current_pass.cmds.push(Cmd::Coverage {
10176 off,
10177 cnt: 1,
10178 handle: *handle,
10179 });
10180 }
10181 }
10182 SceneNode::PushClip { rect, radius, op } => {
10183 flush_batch!();
10184
10185 let is_diff = matches!(op, repose_core::ClipOp::Difference);
10186 let t_identity = Transform::identity();
10187 let current_transform = transform_stack.last().unwrap_or(&t_identity);
10188 let transformed = affine_aabb(current_transform, rect);
10189 let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10190 let next_scissor = if is_diff {
10191 top
10192 } else {
10193 intersect(top, transformed)
10194 };
10195 scissor_stack.push(next_scissor);
10196 let scissor = to_scissor(
10197 &next_scissor,
10198 current_target_size.0 as u32,
10199 current_target_size.1 as u32,
10200 );
10201 let has_inverse = active_clips.iter().any(ActiveClip::difference);
10202 let mut blocked =
10203 current_pass.active_scissor.is_none() || (has_inverse && !is_diff);
10204 if has_inverse && is_diff {
10205 log::error!(
10206 "unsupported clip nesting: Difference cannot contain a Difference clip"
10207 );
10208 blocked = true;
10209 }
10210 let has_area = transformed.x.is_finite()
10211 && transformed.y.is_finite()
10212 && transformed.w.is_finite()
10213 && transformed.h.is_finite()
10214 && transformed.w > 0.0
10215 && transformed.h > 0.0;
10216 let mut applied = false;
10217 let mut off = 0;
10218 if !blocked && has_area && (is_diff || scissor.2 > 0 && scissor.3 > 0) {
10219 let clip_ndc_tl = to_ndc(
10220 transformed.x,
10221 transformed.y,
10222 transformed.w,
10223 transformed.h,
10224 current_target_size.0,
10225 current_target_size.1,
10226 );
10227 let inst = ClipInstance {
10228 xywh: [
10229 clip_ndc_tl[0] + clip_ndc_tl[2] * 0.5,
10230 clip_ndc_tl[1] + clip_ndc_tl[3] * 0.5,
10231 clip_ndc_tl[2],
10232 clip_ndc_tl[3],
10233 ],
10234 radii: radius.map(|r| r.0),
10235 fwd_mat: [1.0, 0.0, 0.0, 1.0],
10236 };
10237 let bytes = bytemuck::bytes_of(&inst);
10238 if self
10239 .clip_ring
10240 .grow_to_fit(
10241 &self.device,
10242 encoder,
10243 u64::try_from(bytes.len()).unwrap_or(u64::MAX),
10244 )
10245 .is_ok()
10246 && let Ok(new_off) = self.clip_ring.alloc_write(&self.queue, bytes)
10247 {
10248 off = new_off;
10249 applied = true;
10250 current_pass.cmds.push(Cmd::ClipPush {
10251 off,
10252 cnt: 1,
10253 scissor,
10254 difference: is_diff,
10255 applied,
10256 });
10257 } else {
10258 blocked = !is_diff;
10259 }
10260 } else if !is_diff {
10261 blocked = true;
10262 }
10263 current_pass.active_scissor = if blocked {
10264 None
10265 } else {
10266 (scissor.2 > 0 && scissor.3 > 0).then_some(scissor)
10267 };
10268 active_clips.push(ActiveClip::Rect {
10269 off,
10270 cnt: u32::from(applied),
10271 rect: transformed,
10272 radii: radius.map(|r| r.0),
10273 scissor,
10274 difference: is_diff,
10275 applied,
10276 blocked,
10277 });
10278 }
10279 SceneNode::PopClip => {
10280 flush_batch!();
10281
10282 if scissor_stack.is_empty() || active_clips.is_empty() {
10283 log::error!("PopClip does not match an active clip");
10284 } else {
10285 scissor_stack.pop();
10286 }
10287 let clip = active_clips.pop();
10288 let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10289 let scissor = to_scissor(
10290 &top,
10291 current_target_size.0 as u32,
10292 current_target_size.1 as u32,
10293 );
10294 current_pass.active_scissor =
10295 (scissor.2 > 0 && scissor.3 > 0).then_some(scissor);
10296 match clip {
10297 Some(ActiveClip::Rect {
10298 off,
10299 cnt,
10300 scissor: stencil_scissor,
10301 difference,
10302 applied,
10303 ..
10304 }) if applied && cnt > 0 => {
10305 current_pass.cmds.push(Cmd::ClipPop {
10306 off,
10307 cnt,
10308 scissor,
10309 stencil_scissor,
10310 difference,
10311 applied: true,
10312 });
10313 }
10314 Some(ActiveClip::Vector { .. }) => {
10315 log::error!("PopClip matched a vector clip");
10316 }
10317 None => {}
10318 _ => {}
10319 }
10320 }
10321 SceneNode::Shadow {
10322 rect,
10323 radius,
10324 elevation: _,
10325 color,
10326 } => {
10327 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10328 if !visible(affine_aabb(current_transform, rect), clip) {
10329 continue;
10330 }
10331 flush_if_prim_changed!("rect", &self.rects);
10332 let (ndc, fwd_mat) = rect_to_instance_ndc(
10333 *rect,
10334 current_transform,
10335 current_target_size.0,
10336 current_target_size.1,
10337 );
10338 let (_brush_type, color0, _color1, _grad_p0, _grad_p1) =
10339 brush_to_instance_fields(&Brush::Solid(*color));
10340 batch.rects.push(RectInstance {
10341 xywh: ndc,
10342 radii: radius.map(|r| r.0),
10343 color0,
10344 color1: [0.0; 4],
10345 fwd_mat,
10346 grad: [0.0; 4],
10347 bits: 0,
10348 _pad: [0.0; 3],
10349 });
10350 }
10351 SceneNode::PushTransform { transform } => {
10352 flush_batch!(); if transform.has_perspective() {
10354 let top = *transform_stack.last().unwrap_or(&t_identity);
10359 self.push_perspective_layer(
10360 *transform,
10361 top,
10362 &mut transform_stack,
10363 &mut scissor_stack,
10364 &mut root_clip_rect,
10365 &mut current_target_size,
10366 &mut current_pass,
10367 &mut active_clips,
10368 &mut passes,
10369 &mut target_stack,
10370 &mut flatten_stack,
10371 &mut flatten_id_head,
10372 &mut next_pass_id,
10373 &mut flatten_ids_used,
10374 encoder,
10375 );
10376 } else {
10377 let combined = current_transform.combine(transform);
10378 transform_stack.push(combined);
10379 }
10380 }
10381 SceneNode::PopTransform => {
10382 flush_batch!(); if let Some(rec) = flatten_stack.last() {
10384 if transform_stack.len() == rec.stack_len + 2 {
10388 let rec = flatten_stack.pop().expect("checked above");
10389 transform_stack.pop();
10390 transform_stack.pop();
10391 self.pop_perspective_layer(
10392 rec,
10393 &mut scissor_stack,
10394 &mut root_clip_rect,
10395 &mut current_target_size,
10396 &mut current_pass,
10397 &mut active_clips,
10398 &mut passes,
10399 &mut target_stack,
10400 &mut next_pass_id,
10401 encoder,
10402 );
10403 continue;
10404 }
10405 }
10406 transform_stack.pop();
10407 }
10408 SceneNode::BeginLayer {
10409 rect,
10410 layer_id,
10411 alpha,
10412 blur_radius_x,
10413 blur_radius_y,
10414 rectangle_edge,
10415 } => {
10416 flush_batch!();
10417 let width_f = rect.w.round();
10418 let height_f = rect.h.round();
10419 let max_dimension = self.device.limits().max_texture_dimension_2d as f32;
10420 if !width_f.is_finite()
10421 || !height_f.is_finite()
10422 || width_f < 1.0
10423 || height_f < 1.0
10424 || width_f > max_dimension
10425 || height_f > max_dimension
10426 {
10427 log::warn!("BeginLayer dimensions are invalid");
10428 continue;
10429 }
10430 let width = width_f as u32;
10431 let height = height_f as u32;
10432 let parent_scissors =
10433 std::mem::replace(&mut scissor_stack, Vec::with_capacity(8));
10434 let parent_root_clip = std::mem::replace(
10435 &mut root_clip_rect,
10436 repose_core::Rect {
10437 x: 0.0,
10438 y: 0.0,
10439 w: width as f32,
10440 h: height as f32,
10441 },
10442 );
10443 scissor_stack.push(root_clip_rect);
10444 let layer_pass_id = next_pass_id;
10445 next_pass_id += 1;
10446 let parent_clips = std::mem::take(&mut active_clips);
10447 let mut layer_pass = Pass {
10448 id: layer_pass_id,
10449 target: PassTarget::Layer(*layer_id),
10450 initial_scissor: (0, 0, width, height),
10451 active_scissor: Some((0, 0, width, height)),
10452 clear_color: Some([0.0, 0.0, 0.0, 0.0]),
10453 cmds: Vec::new(),
10454 };
10455 let parent_transform = *current_transform;
10456 active_clips = self.replay_active_clips(
10457 &parent_clips,
10458 &parent_transform,
10459 (rect.x, rect.y),
10460 (width as f32, height as f32),
10461 &mut layer_pass,
10462 encoder,
10463 );
10464 let saved = std::mem::replace(&mut current_pass, layer_pass);
10465 if let Some(top) = transform_stack.last_mut() {
10466 *top = Transform::identity();
10467 }
10468 layer_stack.push(LayerState {
10469 layer_id: *layer_id,
10470 parent_target: saved.target,
10471 parent_scissors,
10472 parent_root_clip,
10473 parent_size: current_target_size,
10474 parent_transform,
10475 parent_scissor: saved.active_scissor,
10476 parent_clips,
10477 alpha: *alpha,
10478 blur: (blur_radius_x.0, blur_radius_y.0),
10479 rectangle_edge: *rectangle_edge,
10480 });
10481 passes.push(saved);
10482 if self.get_or_create_layer(*layer_id, width, height, *rect, false) {
10483 self.producer_layer_ids.insert(*layer_id);
10484 }
10485 current_target_size = (width as f32, height as f32);
10486 }
10487 SceneNode::EndLayer { layer_id } => {
10488 flush_batch!();
10489 if layer_stack.last().map(|state| state.layer_id) != Some(*layer_id) {
10490 log::warn!("EndLayer {} does not match active layer", layer_id);
10491 continue;
10492 }
10493 let state = layer_stack.pop().expect("checked above");
10494 scissor_stack = state.parent_scissors;
10495 root_clip_rect = state.parent_root_clip;
10496 active_clips = state.parent_clips;
10497 if let Some(top) = transform_stack.last_mut() {
10498 *top = state.parent_transform;
10499 }
10500 current_target_size = state.parent_size;
10501 let resumed_pass_id = next_pass_id;
10502 next_pass_id += 1;
10503 let saved = std::mem::replace(
10504 &mut current_pass,
10505 Pass {
10506 id: resumed_pass_id,
10507 target: state.parent_target,
10508 initial_scissor: state.parent_scissor.unwrap_or((0, 0, 1, 1)),
10509 active_scissor: state.parent_scissor,
10510 clear_color: None,
10511 cmds: Vec::new(),
10512 },
10513 );
10514 passes.push(saved);
10515 let Some(layer) = self.layer_pool.get(layer_id).cloned() else {
10516 continue;
10517 };
10518 let layer_rect = repose_core::Rect {
10519 x: layer.rect_px.0,
10520 y: layer.rect_px.1,
10521 w: layer.rect_px.2,
10522 h: layer.rect_px.3,
10523 };
10524 let local_layer_rect = affine_aabb(&state.parent_transform, &layer_rect);
10525 let (parent_width, parent_height) = state.parent_size;
10526 let parent_clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10527 let composite_rect = if state.blur.0 > 0.0 || state.blur.1 > 0.0 {
10528 let pad_x = blur_pad(state.blur.0);
10529 let pad_y = blur_pad(state.blur.1);
10530 repose_core::Rect {
10531 x: local_layer_rect.x - pad_x,
10532 y: local_layer_rect.y - pad_y,
10533 w: local_layer_rect.w + pad_x * 2.0,
10534 h: local_layer_rect.h + pad_y * 2.0,
10535 }
10536 } else {
10537 local_layer_rect
10538 };
10539 let shadow_follows = shadow_layer_ids.contains(layer_id);
10540 if !visible(composite_rect, parent_clip) {
10541 continue;
10542 }
10543 let mut composite = None;
10544 if state.blur.0 > 0.0 || state.blur.1 > 0.0 {
10545 flush_batch!();
10546 let pad_x = blur_pad(state.blur.0);
10547 let pad_y = blur_pad(state.blur.1);
10548 let rect = repose_core::Rect {
10549 x: local_layer_rect.x - pad_x,
10550 y: local_layer_rect.y - pad_y,
10551 w: local_layer_rect.w + pad_x * 2.0,
10552 h: local_layer_rect.h + pad_y * 2.0,
10553 };
10554 let ndc =
10555 to_ndc(rect.x, rect.y, rect.w, rect.h, parent_width, parent_height);
10556 let edge_mode = if state.rectangle_edge { 0 } else { 1 };
10557 let sigma = [blur_sigma(state.blur.0), blur_sigma(state.blur.1)];
10558 if let Some(scratch_id) = self.emit_separable_blur(
10559 *layer_id,
10560 BLUR_SLOT_CONTENT,
10561 sigma,
10562 (pad_x, pad_y),
10563 edge_mode,
10564 false,
10565 [0.0; 4],
10566 &mut passes,
10567 &mut current_pass,
10568 &mut next_pass_id,
10569 encoder,
10570 ) {
10571 let inst = BlurInstance {
10572 xywh: [
10573 ndc[0] + ndc[2] * 0.5,
10574 ndc[1] + ndc[3] * 0.5,
10575 ndc[2],
10576 ndc[3],
10577 ],
10578 uv: [0.0, 0.0, 1.0, 1.0],
10579 color: [1.0, 1.0, 1.0, state.alpha],
10580 sigma_px: sigma,
10581 fwd_mat: [1.0, 0.0, 0.0, 1.0],
10582 axis: 1,
10583 edge_mode,
10584 shape: [0.0; 4],
10585 };
10586 if let Some(off) = self.upload_blur_instance(&inst, encoder) {
10587 composite = Some(Cmd::CompositeBlur {
10588 off,
10589 cnt: 1,
10590 layer_id: scratch_id,
10591 });
10592 }
10593 }
10594 }
10595 if composite.is_none() {
10598 let ndc = to_ndc(
10599 local_layer_rect.x,
10600 local_layer_rect.y,
10601 local_layer_rect.w,
10602 local_layer_rect.h,
10603 parent_width,
10604 parent_height,
10605 );
10606 let inst = GlyphInstance {
10607 xywh: [ndc[0] + ndc[2] * 0.5, ndc[1] + ndc[3] * 0.5, ndc[2], ndc[3]],
10608 uv: [0.0, 1.0, 1.0, 0.0],
10609 color: [1.0, 1.0, 1.0, state.alpha],
10610 fwd_mat: [1.0, 0.0, 0.0, 1.0],
10611 };
10612 composite = self
10613 .glyph_color
10614 .upload(&self.device, &self.queue, encoder, &[inst])
10615 .map(|(off, cnt)| Cmd::CompositeLayer {
10616 off,
10617 cnt,
10618 layer_id: *layer_id,
10619 });
10620 }
10621 if let Some(command) = composite {
10622 if shadow_follows {
10623 pending_layer_composite = Some(command);
10624 } else {
10625 current_pass.cmds.push(command);
10626 }
10627 }
10628 }
10629 SceneNode::CompositeShadow {
10630 layer_id,
10631 blur_px,
10632 offset_px,
10633 color,
10634 shape_px,
10635 } => {
10636 let sigma = blur_sigma(blur_px.0);
10637 let pad = blur_pad(blur_px.0);
10638 let layer = self.layer_pool.get(layer_id).cloned();
10639 if let Some(layer) = layer {
10640 let layer_rect = repose_core::Rect {
10641 x: layer.rect_px.0,
10642 y: layer.rect_px.1,
10643 w: layer.rect_px.2,
10644 h: layer.rect_px.3,
10645 };
10646 let local_layer_rect =
10647 affine_aabb(transform_stack.last().unwrap_or(&t_identity), &layer_rect);
10648 let sx = local_layer_rect.x + offset_px.0.0 - pad;
10649 let sy = local_layer_rect.y + offset_px.1.0 - pad;
10650 let sw = local_layer_rect.w + pad * 2.0;
10651 let sh = local_layer_rect.h + pad * 2.0;
10652 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10653 if !visible(
10654 repose_core::Rect {
10655 x: sx,
10656 y: sy,
10657 w: sw,
10658 h: sh,
10659 },
10660 clip,
10661 ) {
10662 flush_pending_layer!();
10663 continue;
10664 }
10665 flush_batch!();
10666 let shadow_color = color.to_linear();
10667 let max_r = 0.5 * layer.rect_px.2.min(layer.rect_px.3).max(0.0);
10668 let shape = shape_px.map(|r| r.0.clamp(0.0, max_r));
10669 let scratch_id = self.emit_separable_blur(
10673 *layer_id,
10674 BLUR_SLOT_SHADOW,
10675 [sigma, sigma],
10676 (pad, pad),
10677 1,
10678 true,
10679 shape,
10680 &mut passes,
10681 &mut current_pass,
10682 &mut next_pass_id,
10683 encoder,
10684 );
10685 if let Some(scratch_id) = scratch_id {
10686 let ndc_tl = to_ndc(
10687 sx,
10688 sy,
10689 sw,
10690 sh,
10691 current_target_size.0,
10692 current_target_size.1,
10693 );
10694 let inst = BlurInstance {
10695 xywh: [
10696 ndc_tl[0] + ndc_tl[2] * 0.5,
10697 ndc_tl[1] + ndc_tl[3] * 0.5,
10698 ndc_tl[2],
10699 ndc_tl[3],
10700 ],
10701 uv: [0.0, 0.0, 1.0, 1.0],
10702 color: shadow_color,
10703 sigma_px: [sigma, sigma],
10704 fwd_mat: [1.0, 0.0, 0.0, 1.0],
10705 axis: 1,
10706 edge_mode: 1,
10707 shape: [0.0; 4],
10708 };
10709 if let Some(off) = self.upload_blur_instance(&inst, encoder) {
10710 current_pass.cmds.push(Cmd::CompositeShadow {
10711 off,
10712 cnt: 1,
10713 layer_id: scratch_id,
10714 });
10715 }
10716 }
10717 }
10718 flush_pending_layer!();
10719 }
10720 SceneNode::VectorMesh {
10721 mesh,
10722 transform,
10723 paint,
10724 clip: _,
10725 blend,
10726 } => {
10727 let affine = combine_mesh_affine(current_transform, *transform);
10728 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10729 if !visible(mesh_aabb(mesh, affine), clip) {
10730 continue;
10731 }
10732 flush_batch!();
10733 let paint_alpha = match paint {
10734 repose_core::PaintDesc::Linear {
10735 start_color,
10736 end_color,
10737 ..
10738 }
10739 | repose_core::PaintDesc::Radial {
10740 start_color,
10741 end_color,
10742 ..
10743 }
10744 | repose_core::PaintDesc::Sweep {
10745 start_color,
10746 end_color,
10747 ..
10748 } => Some(start_color.3.min(end_color.3) as f32 / 255.0),
10749 repose_core::PaintDesc::LinearStops { stops, .. }
10750 | repose_core::PaintDesc::RadialStops { stops, .. } => {
10751 Some(stops.min_alpha())
10752 }
10753 _ => None,
10754 };
10755 let translucent_fixed_blend =
10756 matches!(
10757 blend,
10758 repose_core::BlendMode::Add
10759 | repose_core::BlendMode::Multiply
10760 | repose_core::BlendMode::Screen
10761 | repose_core::BlendMode::Darken
10762 | repose_core::BlendMode::Lighten
10763 ) && (mesh.vertices.iter().any(|vertex| vertex.color[3] < 0.9999)
10764 || paint_alpha.is_some_and(|alpha| alpha < 0.9999));
10765 if blend.needs_isolation() || translucent_fixed_blend {
10766 let blend_scissor = to_scissor(
10767 &scissor_stack.last().copied().unwrap_or(root_clip_rect),
10768 current_target_size.0 as u32,
10769 current_target_size.1 as u32,
10770 );
10771 self.emit_isolated_blend(
10772 mesh.clone(),
10773 *transform,
10774 *paint,
10775 *blend,
10776 current_transform,
10777 &mut current_pass,
10778 &active_clips,
10779 &mut passes,
10780 &mut flatten_id_head,
10781 &mut next_pass_id,
10782 &mut flatten_ids_used,
10783 blend_scissor,
10784 encoder,
10785 fb_w,
10786 fb_h,
10787 );
10788 } else {
10789 let t_identity = Transform::identity();
10790 let current_transform = transform_stack.last().unwrap_or(&t_identity);
10791 self.emit_vector_mesh(
10792 current_transform,
10793 mesh,
10794 *transform,
10795 paint,
10796 *blend,
10797 &mut current_pass.cmds,
10798 );
10799 }
10800 }
10801 SceneNode::VectorOverlay { meshes } => {
10802 let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10803 let has_visible = meshes
10804 .iter()
10805 .any(|mesh| visible(mesh_aabb(mesh, [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]), clip));
10806 if !has_visible {
10807 continue;
10808 }
10809 flush_batch!();
10810 for (index, m) in meshes.iter().enumerate() {
10811 if !visible(mesh_aabb(m, [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]), clip) {
10812 continue;
10813 }
10814 let Some(mesh_gpu) = self.upload_overlay_mesh(meshes, index) else {
10815 continue;
10816 };
10817 let Some(uoff) = self.alloc_mesh_uniform(MeshUniform::identity()) else {
10818 continue;
10819 };
10820 current_pass.cmds.push(Cmd::VectorOverlay {
10821 mesh: mesh_gpu,
10822 uoff,
10823 });
10824 }
10825 }
10826 SceneNode::PushVectorClip { mesh, op } => {
10827 flush_batch!();
10828 let difference = matches!(op, repose_core::ClipOp::Difference);
10829 let t_identity = Transform::identity();
10830 let current_transform = transform_stack.last().unwrap_or(&t_identity);
10831 let affine =
10832 combine_mesh_affine(current_transform, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
10833 let aabb = mesh_aabb(mesh, affine);
10834 let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10835 let next = if difference {
10836 top
10837 } else {
10838 intersect(top, aabb)
10839 };
10840 scissor_stack.push(next);
10841 let scissor = to_scissor(
10842 &next,
10843 current_target_size.0 as u32,
10844 current_target_size.1 as u32,
10845 );
10846 let has_inverse = active_clips.iter().any(ActiveClip::difference);
10847 let mut blocked = current_pass.active_scissor.is_none() || has_inverse;
10848 if has_inverse {
10849 log::error!(
10850 "unsupported clip nesting: a Difference clip cannot contain another vector clip"
10851 );
10852 }
10853 let has_area = aabb.x.is_finite()
10854 && aabb.y.is_finite()
10855 && aabb.w.is_finite()
10856 && aabb.h.is_finite()
10857 && aabb.w > 0.0
10858 && aabb.h > 0.0;
10859 let mut applied = false;
10860 let mut geometry = None;
10861 let mut uoff = 0;
10862 if !blocked
10863 && has_area
10864 && (if difference {
10865 visible(aabb, top)
10866 } else {
10867 scissor.2 > 0 && scissor.3 > 0
10868 })
10869 {
10870 if let Some(mesh_gpu) = self.upload_mesh_geometry(mesh)
10871 && let Some(slot) = self.alloc_mesh_uniform(mesh_uniform_from_paint(
10872 affine,
10873 &repose_core::PaintDesc::Solid,
10874 ))
10875 {
10876 geometry = Some(mesh_gpu.clone());
10877 uoff = slot;
10878 applied = true;
10879 current_pass.cmds.push(Cmd::VectorClipPush {
10880 mesh: mesh_gpu,
10881 uoff,
10882 scissor,
10883 difference,
10884 applied,
10885 });
10886 } else {
10887 blocked = !difference;
10888 }
10889 } else if !difference {
10890 blocked = true;
10891 }
10892 current_pass.active_scissor = if blocked {
10893 None
10894 } else {
10895 (scissor.2 > 0 && scissor.3 > 0).then_some(scissor)
10896 };
10897 active_clips.push(ActiveClip::Vector {
10898 mesh: geometry,
10899 source: mesh.clone(),
10900 uoff,
10901 affine,
10902 scissor,
10903 difference,
10904 applied,
10905 blocked,
10906 });
10907 }
10908 SceneNode::PopVectorClip => {
10909 flush_batch!();
10910 if scissor_stack.is_empty() || active_clips.is_empty() {
10911 log::error!("PopVectorClip does not match an active clip");
10912 } else {
10913 scissor_stack.pop();
10914 }
10915 let clip = active_clips.pop();
10916 let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10917 let scissor = to_scissor(
10918 &top,
10919 current_target_size.0 as u32,
10920 current_target_size.1 as u32,
10921 );
10922 current_pass.active_scissor =
10923 (scissor.2 > 0 && scissor.3 > 0).then_some(scissor);
10924 match clip {
10925 Some(ActiveClip::Vector {
10926 mesh: Some(mesh),
10927 uoff,
10928 scissor: stencil_scissor,
10929 difference,
10930 applied,
10931 ..
10932 }) if applied && mesh.vertex_count > 0 && mesh.index_count > 0 => {
10933 current_pass.cmds.push(Cmd::VectorClipPop {
10934 mesh,
10935 uoff,
10936 scissor,
10937 stencil_scissor,
10938 difference,
10939 applied: true,
10940 });
10941 }
10942 Some(ActiveClip::Rect { .. }) => {
10943 log::error!("PopVectorClip matched a rounded-rect clip");
10944 }
10945 None => {}
10946 _ => {}
10947 }
10948 }
10949 SceneNode::Callback { rect, payload } => {
10950 let t = transform_stack
10951 .last()
10952 .copied()
10953 .unwrap_or(Transform::identity());
10954 let transformed = affine_aabb(&t, rect);
10955 let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
10956 let clip_rect = intersect(transformed, top);
10957 let scissor = to_scissor(
10958 &clip_rect,
10959 current_target_size.0 as u32,
10960 current_target_size.1 as u32,
10961 );
10962 let restore_scissor = to_scissor(
10963 &top,
10964 current_target_size.0 as u32,
10965 current_target_size.1 as u32,
10966 );
10967 if current_pass.active_scissor.is_none()
10968 || !visible(transformed, top)
10969 || scissor.2 == 0
10970 || scissor.3 == 0
10971 {
10972 continue;
10973 }
10974 flush_batch!();
10975 current_pass.cmds.push(Cmd::Callback {
10976 rect: transformed,
10977 clip_rect,
10978 scissor,
10979 restore_scissor,
10980 callback_id: Arc::as_ptr(payload) as *const () as usize,
10981 payload: payload.clone(),
10982 });
10983 }
10984 _ => {}
10985 }
10986 }
10987
10988 flush_pending_layer!();
10989 flush_batch!();
10990 passes.push(current_pass);
10991
10992 {
10993 let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
10994 let mut prepare_list: Vec<(usize, Arc<Callback>)> = Vec::new();
10995 for pass in &passes {
10996 for command in &pass.cmds {
10997 if let Cmd::Callback { payload, .. } = command
10998 && payload.downcast_ref::<Callback>().is_some()
10999 {
11000 let ptr = Arc::as_ptr(payload) as *const () as usize;
11001 if seen.insert(ptr)
11002 && let Ok(cb_arc) = payload.clone().downcast::<Callback>()
11003 {
11004 prepare_list.push((ptr, cb_arc));
11005 }
11006 }
11007 }
11008 }
11009 if !prepare_list.is_empty() {
11010 let mut callback_targets: HashMap<usize, Vec<(PassTarget, ScreenDescriptor)>> =
11011 HashMap::new();
11012 for pass in &passes {
11013 let (size, target_format, sample_count) = match pass.target {
11014 PassTarget::Surface => (
11015 [self.output_width, self.output_height],
11016 if self.working_space {
11017 wgpu::TextureFormat::Rgba16Float
11018 } else {
11019 self.output_format
11020 },
11021 self.active_surface_msaa_samples(),
11022 ),
11023 PassTarget::Layer(layer_id) => {
11024 let layer = self.layer_pool.get(&layer_id);
11025 (
11026 layer.map_or([self.output_width, self.output_height], |layer| {
11027 [layer.width, layer.height]
11028 }),
11029 self.layer_target_format(),
11030 1,
11031 )
11032 }
11033 };
11034 for command in &pass.cmds {
11035 if let Cmd::Callback { payload, .. } = command {
11036 let descriptor = ScreenDescriptor {
11037 size_in_pixels: size,
11038 pixels_per_point: self.pixels_per_point,
11039 target_format,
11040 sample_count,
11041 };
11042 let targets = callback_targets
11043 .entry(Arc::as_ptr(payload) as *const () as usize)
11044 .or_default();
11045 if !targets.iter().any(|(target, target_desc)| {
11046 *target == pass.target
11047 && target_desc.target_format == descriptor.target_format
11048 && target_desc.sample_count == descriptor.sample_count
11049 }) {
11050 targets.push((pass.target, descriptor));
11051 }
11052 }
11053 }
11054 }
11055
11056 let default_descriptor = ScreenDescriptor {
11057 size_in_pixels: [self.output_width, self.output_height],
11058 pixels_per_point: self.pixels_per_point,
11059 target_format: if self.working_space {
11060 wgpu::TextureFormat::Rgba16Float
11061 } else {
11062 self.output_format
11063 },
11064 sample_count: self.active_surface_msaa_samples(),
11065 };
11066 let default_descriptors = [(PassTarget::Surface, default_descriptor)];
11067 let mut prepare_encoder =
11068 self.device
11069 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
11070 label: Some("callback prepare"),
11071 });
11072 let mut finish_encoder =
11073 self.device
11074 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
11075 label: Some("callback finish prepare"),
11076 });
11077 let mut active_callback_scopes = HashSet::new();
11078 for (key, cb) in &prepare_list {
11079 let descriptors = callback_targets
11080 .get(key)
11081 .map(Vec::as_slice)
11082 .unwrap_or(&default_descriptors);
11083 for &(target, screen_desc) in descriptors {
11084 let scope = callback_scope_key(cb, *key, target, &screen_desc);
11085 active_callback_scopes.insert(scope.clone());
11086 if !scope.callback.reusable
11087 && self
11088 .callback_scope_payloads
11089 .get(&scope)
11090 .is_some_and(|payload| payload.upgrade().is_none())
11091 {
11092 self.remove_callback_scope(&scope);
11093 }
11094 let payload: Weak<Callback> = Arc::downgrade(cb);
11095 self.callback_scope_payloads.insert(scope, payload);
11096 }
11097 }
11098 let mut prepare_buffers = Vec::new();
11099 let mut finish_buffers = Vec::new();
11100 for (key, cb) in &prepare_list {
11101 let descriptors = callback_targets
11102 .get(key)
11103 .map(Vec::as_slice)
11104 .unwrap_or(&default_descriptors);
11105 for &(target, screen_desc) in descriptors {
11106 let scope = callback_scope_key(cb, *key, target, &screen_desc);
11107 self.touch_callback_scope(scope.clone());
11108 if !self.callback_scoped_resources.contains_key(&scope)
11109 && self.callback_scoped_resources.len() >= MAX_CALLBACK_SCOPES
11110 {
11111 self.evict_callback_scope(&active_callback_scopes);
11112 }
11113 let resources = self.callback_scoped_resources.entry(scope).or_default();
11114 prepare_buffers.extend(cb.0.prepare(
11115 &self.device,
11116 &self.queue,
11117 &mut prepare_encoder,
11118 &screen_desc,
11119 resources,
11120 ));
11121 }
11122 }
11123 for (key, cb) in &prepare_list {
11124 let descriptors = callback_targets
11125 .get(key)
11126 .map(Vec::as_slice)
11127 .unwrap_or(&default_descriptors);
11128 for &(target, screen_desc) in descriptors {
11129 let scope = callback_scope_key(cb, *key, target, &screen_desc);
11130 self.touch_callback_scope(scope.clone());
11131 if let Some(resources) = self.callback_scoped_resources.get_mut(&scope) {
11132 finish_buffers.extend(cb.0.finish_prepare(
11133 &self.device,
11134 &self.queue,
11135 &mut finish_encoder,
11136 &screen_desc,
11137 resources,
11138 ));
11139 }
11140 }
11141 }
11142 let mut callback_buffers =
11143 Vec::with_capacity(2 + prepare_buffers.len() + finish_buffers.len());
11144 callback_buffers.push(prepare_encoder.finish());
11145 callback_buffers.extend(prepare_buffers);
11146 callback_buffers.push(finish_encoder.finish());
11147 callback_buffers.extend(finish_buffers);
11148 self.queue.submit(callback_buffers);
11149 }
11150 }
11151
11152 let globals_bytes = std::mem::size_of::<Globals>() as u64;
11153 if !self.ensure_globals_staging(passes.len()) {
11154 return;
11155 }
11156 for (i, pass) in passes.iter().enumerate() {
11157 let (target_w, target_h) = match pass.target {
11158 PassTarget::Surface => (fb_w, fb_h),
11159 PassTarget::Layer(layer_id) => {
11160 let lt = self.layer_pool.get(&layer_id);
11161 (
11162 lt.map_or(fb_w, |l| l.width as f32),
11163 lt.map_or(fb_h, |l| l.height as f32),
11164 )
11165 }
11166 };
11167 self.queue.write_buffer(
11168 &self.globals_staging,
11169 (i as u64) * globals_bytes,
11170 bytemuck::bytes_of(&make_globals(target_w, target_h)),
11171 );
11172 }
11173
11174 let bind_mask = self.atlas_bind_group_mask();
11175 let bind_color = self.atlas_bind_group_color();
11176 let mut clip_depth: u32 = 0;
11177 let mut clip_depth_stack: Vec<u32> = Vec::new();
11178
11179 let snapshot_source = target_texture.cloned();
11180 let last_surface_pass = passes
11181 .iter()
11182 .rposition(|pass| matches!(pass.target, PassTarget::Surface));
11183 for (pass_index, pass) in std::mem::take(&mut passes).into_iter().enumerate() {
11184 let needs_snapshot = pass
11192 .cmds
11193 .iter()
11194 .any(|c| matches!(c, Cmd::BlendLayer { .. }));
11195 if needs_snapshot {
11196 let copies = std::mem::take(&mut self.blend_copies);
11197 let has_surface_copy = copies.iter().any(|copy| copy.target == PassTarget::Surface);
11198 let mut resolved_surface = None;
11199 if has_surface_copy
11200 && !self.working_space
11201 && let (Some(msaa_view), Some(resolve_view)) =
11202 (self.msaa_view.as_ref(), self.surface_resolve_view.as_ref())
11203 {
11204 let resolve_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
11205 label: Some("surface blend resolve"),
11206 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11207 view: msaa_view,
11208 resolve_target: Some(resolve_view),
11209 ops: wgpu::Operations {
11210 load: wgpu::LoadOp::Load,
11211 store: wgpu::StoreOp::Store,
11212 },
11213 depth_slice: None,
11214 })],
11215 depth_stencil_attachment: None,
11216 timestamp_writes: None,
11217 occlusion_query_set: None,
11218 multiview_mask: None,
11219 });
11220 drop(resolve_pass);
11221 resolved_surface = self.surface_resolve_tex.clone();
11222 }
11223 let mut remaining = Vec::with_capacity(copies.len());
11224 for copy in copies {
11225 if copy.pass_id != pass.id {
11226 remaining.push(copy);
11227 continue;
11228 }
11229 let BlendCopy {
11230 blend_id,
11231 target,
11232 region,
11233 ..
11234 } = copy;
11235 let (src_tex, tw, th) = match target {
11236 PassTarget::Layer(parent_id) => match self.layer_pool.get(&parent_id) {
11237 Some(lt) => (lt.texture.clone(), lt.width, lt.height),
11238 None => continue,
11239 },
11240 PassTarget::Surface => {
11241 let texture = if self.working_space {
11242 self.ws_tex.clone()
11243 } else if self.msaa_view.is_some() {
11244 resolved_surface.clone()
11245 } else {
11246 snapshot_source.clone()
11247 };
11248 match texture {
11249 Some(texture) => (texture, self.output_width, self.output_height),
11250 None => {
11251 self.remove_blend_snapshot(blend_id);
11252 continue;
11253 }
11254 }
11255 }
11256 };
11257 let Some(dst_tex) = self.blend_snapshot_texture(blend_id) else {
11258 continue;
11259 };
11260 if !src_tex.usage().contains(wgpu::TextureUsages::COPY_SRC)
11261 || !dst_tex.usage().contains(wgpu::TextureUsages::COPY_DST)
11262 {
11263 self.remove_blend_snapshot(blend_id);
11264 continue;
11265 }
11266 let Some((sx, sy, cw, ch)) = checked_copy_region(region, tw, th) else {
11267 self.remove_blend_snapshot(blend_id);
11268 continue;
11269 };
11270 encoder.copy_texture_to_texture(
11271 wgpu::TexelCopyTextureInfo {
11272 texture: &src_tex,
11273 mip_level: 0,
11274 origin: wgpu::Origin3d { x: sx, y: sy, z: 0 },
11275 aspect: wgpu::TextureAspect::All,
11276 },
11277 wgpu::TexelCopyTextureInfo {
11278 texture: &dst_tex,
11279 mip_level: 0,
11280 origin: wgpu::Origin3d::ZERO,
11281 aspect: wgpu::TextureAspect::All,
11282 },
11283 wgpu::Extent3d {
11284 width: cw,
11285 height: ch,
11286 depth_or_array_layers: 1,
11287 },
11288 );
11289 }
11290 self.blend_copies = remaining;
11291 }
11292 let (color_view, resolve_target, depth_stencil_view, is_layer, working_target) =
11293 match pass.target {
11294 PassTarget::Surface => {
11295 let swap_view = target_view.clone();
11296 let use_ws = self.working_space && self.ws_view.is_some();
11297 let (color, resolve) = if use_ws {
11298 let ws_view = self.ws_view.as_ref().unwrap();
11299 if let Some(msaa_view) = &self.ws_msaa_view {
11300 (msaa_view.clone(), Some(ws_view.clone()))
11301 } else {
11302 (ws_view.clone(), None)
11303 }
11304 } else if let Some(msaa_view) = &self.msaa_view {
11305 let resolve =
11310 (Some(pass_index) == last_surface_pass).then(|| swap_view.clone());
11311 (msaa_view.clone(), resolve)
11312 } else {
11313 (swap_view, None)
11314 };
11315 (
11316 color,
11317 resolve,
11318 self.depth_stencil_view.clone(),
11319 false,
11320 use_ws,
11321 )
11322 }
11323 PassTarget::Layer(layer_id) => {
11324 if let Some(lt) = self.layer_pool.get(&layer_id) {
11325 (
11326 lt.view.clone(),
11327 None,
11328 lt.depth_stencil_view.clone(),
11329 true,
11330 false,
11331 )
11332 } else {
11333 log::warn!("missing layer target {layer_id}");
11334 continue;
11335 }
11336 }
11337 };
11338
11339 encoder.copy_buffer_to_buffer(
11340 &self.globals_staging,
11341 (pass_index as u64) * globals_bytes,
11342 &self.globals_buf,
11343 0,
11344 globals_bytes,
11345 );
11346
11347 if is_layer {
11348 clip_depth_stack.push(clip_depth);
11349 clip_depth = 0;
11350 }
11351
11352 let (tw, th) = match pass.target {
11353 PassTarget::Surface => (self.output_width, self.output_height),
11354 PassTarget::Layer(layer_id) => self
11355 .layer_pool
11356 .get(&layer_id)
11357 .map(|l| (l.width, l.height))
11358 .unwrap_or((self.output_width, self.output_height)),
11359 };
11360 let initial_scissor = clamp_scissor(
11361 pass.initial_scissor.0,
11362 pass.initial_scissor.1,
11363 pass.initial_scissor.2,
11364 pass.initial_scissor.3,
11365 tw,
11366 th,
11367 );
11368 let mut active_scissor = pass
11369 .active_scissor
11370 .map(|scissor| clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th));
11371 if active_scissor.is_some_and(|scissor| scissor.2 == 0 || scissor.3 == 0) {
11372 active_scissor = None;
11373 }
11374
11375 let pipes: &Pipelines = if is_layer {
11376 if self.working_space {
11377 &self.working_space_layer_pipes
11378 } else {
11379 &self.layer_pipes
11380 }
11381 } else if working_target {
11382 &self.working_space_pipes
11383 } else {
11384 &self.surface_pipes
11385 };
11386
11387 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
11388 label: Some("pass"),
11389 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11390 view: &color_view,
11391 resolve_target: resolve_target.as_ref(),
11392 ops: wgpu::Operations {
11393 load: match pass.clear_color {
11394 Some(c) => wgpu::LoadOp::Clear(wgpu::Color {
11395 r: c[0] as f64,
11396 g: c[1] as f64,
11397 b: c[2] as f64,
11398 a: c[3] as f64,
11399 }),
11400 None => wgpu::LoadOp::Load,
11401 },
11402 store: wgpu::StoreOp::Store,
11403 },
11404 depth_slice: None,
11405 })],
11406 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
11407 view: &depth_stencil_view,
11408 depth_ops: None,
11409 stencil_ops: Some(wgpu::Operations {
11410 load: if pass.clear_color.is_some() {
11411 wgpu::LoadOp::Clear(STENCIL_BASE)
11412 } else {
11413 wgpu::LoadOp::Load
11414 },
11415 store: wgpu::StoreOp::Store,
11416 }),
11417 }),
11418 timestamp_writes: None,
11419 occlusion_query_set: None,
11420 multiview_mask: None,
11421 });
11422
11423 rpass.set_bind_group(0, &self.globals_bind, &[]);
11424 rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
11425 let initial_draw_scissor = if initial_scissor.2 == 0 || initial_scissor.3 == 0 {
11426 (0, 0, 1, 1)
11427 } else {
11428 initial_scissor
11429 };
11430 rpass.set_scissor_rect(
11431 initial_draw_scissor.0,
11432 initial_draw_scissor.1,
11433 initial_draw_scissor.2,
11434 initial_draw_scissor.3,
11435 );
11436
11437 macro_rules! draw_simple {
11438 ($pipeline:expr, $ring:expr, $inst:ty, $off:ident, $n:ident) => {{
11439 rpass.set_pipeline($pipeline);
11440 let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
11441 rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
11442 rpass.draw(0..6, 0..$n);
11443 }};
11444 }
11445
11446 macro_rules! draw_with_bind {
11447 ($pipeline:expr, $ring:expr, $inst:ty, $bind:expr, $off:ident, $n:ident) => {{
11448 rpass.set_pipeline($pipeline);
11449 rpass.set_bind_group(1, $bind, &[]);
11450 let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
11451 rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
11452 rpass.draw(0..6, 0..$n);
11453 }};
11454 }
11455
11456 macro_rules! draw_indexed_mesh {
11457 ($pipeline:expr, $uoff:ident, $mesh:ident) => {{
11458 rpass.set_pipeline($pipeline);
11459 rpass.set_bind_group(1, &self.mesh_bind, &[$uoff as u32]);
11460 let vbytes =
11461 ($mesh.vertex_count as u64) * std::mem::size_of::<MeshVertex>() as u64;
11462 rpass.set_vertex_buffer(0, $mesh.vertices.slice(0..vbytes));
11463 let ibytes = ($mesh.index_count as u64) * std::mem::size_of::<u32>() as u64;
11464 rpass.set_index_buffer(
11465 $mesh.indices.slice(0..ibytes),
11466 wgpu::IndexFormat::Uint32,
11467 );
11468 rpass.draw_indexed(0..$mesh.index_count, 0, 0..1);
11469 }};
11470 }
11471
11472 for cmd in pass.cmds {
11473 if active_scissor.is_none()
11474 && !matches!(
11475 &cmd,
11476 Cmd::ClipPush { .. }
11477 | Cmd::ClipPop { .. }
11478 | Cmd::VectorClipPush { .. }
11479 | Cmd::VectorClipPop { .. }
11480 )
11481 {
11482 continue;
11483 }
11484 match cmd {
11485 Cmd::ClipPush {
11486 off,
11487 cnt: n,
11488 scissor,
11489 difference,
11490 applied,
11491 } => {
11492 let scissor =
11493 clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
11494 let usable = applied && scissor.2 > 0 && scissor.3 > 0;
11495 if usable {
11496 rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11497 active_scissor = Some(scissor);
11498 rpass.set_pipeline(if difference {
11499 &pipes.clip_dec
11500 } else {
11501 &pipes.clip_bin
11502 });
11503
11504 let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
11505 rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
11506 rpass.draw(0..6, 0..n);
11507 if !difference {
11508 clip_depth = (clip_depth + 1).min(STENCIL_MAX_DEPTH);
11509 }
11510 } else {
11511 active_scissor = None;
11512 }
11513 rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
11514 }
11515
11516 Cmd::ClipPop {
11517 off,
11518 cnt: n,
11519 scissor,
11520 stencil_scissor,
11521 difference,
11522 applied,
11523 } => {
11524 let scissor =
11525 clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
11526 let dec = clamp_scissor(
11527 stencil_scissor.0,
11528 stencil_scissor.1,
11529 stencil_scissor.2,
11530 stencil_scissor.3,
11531 tw,
11532 th,
11533 );
11534 if scissor.2 > 0 && scissor.3 > 0 {
11535 rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11536 active_scissor = Some(scissor);
11537 } else {
11538 active_scissor = None;
11539 }
11540 if applied && n > 0 && dec.2 > 0 && dec.3 > 0 {
11541 rpass.set_scissor_rect(dec.0, dec.1, dec.2, dec.3);
11542 rpass.set_pipeline(if difference {
11543 &pipes.clip_bin
11544 } else {
11545 &pipes.clip_dec
11546 });
11547 let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
11548 rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
11549 rpass.draw(0..6, 0..n);
11550 if scissor.2 > 0 && scissor.3 > 0 {
11551 rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11552 }
11553 if !difference {
11554 clip_depth = clip_depth.saturating_sub(1);
11555 }
11556 }
11557 rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
11558 }
11559
11560 Cmd::Rect { off, cnt: n } => {
11561 draw_simple!(&pipes.rects, self.rects.ring, RectInstance, off, n);
11562 }
11563
11564 Cmd::Border { off, cnt: n } => {
11565 draw_simple!(&pipes.borders, self.borders.ring, BorderInstance, off, n);
11566 }
11567
11568 Cmd::GlyphsMask { off, cnt: n } => {
11569 draw_with_bind!(
11570 &pipes.text_mask,
11571 self.glyph_mask.ring,
11572 GlyphInstance,
11573 &bind_mask,
11574 off,
11575 n
11576 );
11577 }
11578
11579 Cmd::GlyphsColor { off, cnt: n } => {
11580 draw_with_bind!(
11581 &pipes.text_color,
11582 self.glyph_color.ring,
11583 GlyphInstance,
11584 &bind_color,
11585 off,
11586 n
11587 );
11588 }
11589
11590 Cmd::GlyphsVector { off, cnt: n } => {
11591 if let Some(slug_pipe) = pipes.slug.as_ref() {
11592 rpass.set_pipeline(slug_pipe);
11593 let bytes = (n as u64) * std::mem::size_of::<slug::TessVertex>() as u64;
11594 rpass.set_vertex_buffer(0, self.slug_ring.buf.slice(off..off + bytes));
11595 rpass.draw(0..n, 0..1);
11596 }
11597 }
11598
11599 Cmd::ImageRgba {
11600 off,
11601 cnt: n,
11602 handle,
11603 filter,
11604 tile,
11605 blend,
11606 } => {
11607 let bind_opt = match self.images.get(&handle) {
11608 Some(ImageTex::Rgba { binds, .. }) => Some(binds.get(filter, tile)),
11609 Some(ImageTex::User { binds, .. }) => Some(binds.get(filter, tile)),
11610 _ => None,
11611 };
11612 if let Some(bind) = bind_opt {
11613 let pipe = match FIXED_FUNCTION_BLENDS.iter().position(|m| *m == blend)
11614 {
11615 Some(index) => &pipes.image_blends[index],
11616 None => &pipes.image_rgba,
11617 };
11618 draw_with_bind!(
11619 pipe,
11620 self.glyph_color.ring,
11621 GlyphInstance,
11622 bind,
11623 off,
11624 n
11625 );
11626 }
11627 }
11628 Cmd::Coverage {
11629 off,
11630 cnt: n,
11631 handle,
11632 } => {
11633 if let Some(tile) = self.coverages.get(&handle) {
11634 draw_with_bind!(
11635 &pipes.coverage,
11636 self.glyph_color.ring,
11637 GlyphInstance,
11638 &tile.bind,
11639 off,
11640 n
11641 );
11642 }
11643 }
11644
11645 Cmd::ImageNv12 {
11646 off,
11647 cnt: n,
11648 handle,
11649 filter,
11650 blend,
11651 } => {
11652 if let Some(ImageTex::Nv12 { binds, .. }) = self.images.get(&handle) {
11653 let pipe = match FIXED_FUNCTION_BLENDS.iter().position(|m| *m == blend)
11654 {
11655 Some(index) => &pipes.image_nv12_blends[index],
11656 None => &pipes.image_nv12,
11657 };
11658 draw_with_bind!(
11659 pipe,
11660 self.nv12.ring,
11661 Nv12Instance,
11662 binds.get(filter, false),
11663 off,
11664 n
11665 );
11666 }
11667 }
11668
11669 Cmd::Ellipse { off, cnt: n } => {
11670 draw_simple!(&pipes.ellipses, self.ellipses.ring, EllipseInstance, off, n);
11671 }
11672
11673 Cmd::EllipseBorder { off, cnt: n } => {
11674 draw_simple!(
11675 &pipes.ellipse_borders,
11676 self.ellipse_borders.ring,
11677 EllipseBorderInstance,
11678 off,
11679 n
11680 );
11681 }
11682
11683 Cmd::Arc { off, cnt: n } => {
11684 draw_simple!(&pipes.arcs, self.arcs.ring, ArcInstance, off, n);
11685 }
11686
11687 Cmd::CompositeLayer {
11688 off,
11689 cnt: n,
11690 layer_id,
11691 } => {
11692 if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
11693 draw_with_bind!(
11694 &pipes.image_rgba,
11695 self.glyph_color.ring,
11696 GlyphInstance,
11697 <.bind,
11698 off,
11699 n
11700 );
11701 }
11702 }
11703 Cmd::CompositeShadow {
11704 off,
11705 cnt: n,
11706 layer_id,
11707 } => {
11708 if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
11709 draw_with_bind!(
11710 &pipes.blur,
11711 self.blur_ring,
11712 BlurInstance,
11713 <.bind_linear,
11714 off,
11715 n
11716 );
11717 }
11718 }
11719 Cmd::CompositeBlur {
11720 off,
11721 cnt: n,
11722 layer_id,
11723 } => {
11724 if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
11725 draw_with_bind!(
11726 &pipes.blur_content,
11727 self.blur_ring,
11728 BlurInstance,
11729 <.bind_linear,
11730 off,
11731 n
11732 );
11733 }
11734 }
11735 Cmd::CompositeProjective {
11736 off,
11737 cnt: n,
11738 layer_id,
11739 } => {
11740 if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
11741 draw_with_bind!(
11745 &pipes.projective_layer,
11746 self.projective_ring,
11747 ProjectiveInstance,
11748 <.bind,
11749 off,
11750 n
11751 );
11752 }
11753 }
11754
11755 Cmd::BlendLayer {
11756 off,
11757 cnt: n,
11758 src_layer,
11759 dst_layer,
11760 parent,
11761 scissor,
11762 } => {
11763 let src = self.layer_pool.get(&src_layer).cloned();
11764 let dst = dst_layer.and_then(|id| self.blend_snapshots.get(&id).cloned());
11771 let in_parent = match parent {
11772 PassTarget::Surface => !is_layer,
11773 PassTarget::Layer(id) => {
11774 matches!(pass.target, PassTarget::Layer(pid) if pid == id)
11775 }
11776 };
11777 if let (Some(src_lt), Some(dst_snap)) = (src, dst)
11778 && in_parent
11779 {
11780 let bytes = (n as u64) * std::mem::size_of::<BlendInstance>() as u64;
11781 rpass.set_pipeline(&pipes.blend_layer);
11782 let scissor =
11783 clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
11784 if scissor.2 > 0 && scissor.3 > 0 {
11785 rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11786 rpass.set_bind_group(0, &self.globals_bind, &[]);
11787 rpass.set_bind_group(1, &src_lt.bind, &[]);
11788 rpass.set_bind_group(2, &dst_snap.bind, &[]);
11789 rpass.set_vertex_buffer(
11790 0,
11791 self.blend_ring.buf.slice(off..off + bytes),
11792 );
11793 rpass.draw(0..6, 0..n);
11794 }
11795 }
11796 }
11797
11798 Cmd::VectorMesh { mesh, uoff, blend } => {
11799 let pipe = match FIXED_FUNCTION_BLENDS.iter().position(|m| *m == blend) {
11800 Some(index) => &pipes.mesh_blends[index],
11801 None => &pipes.mesh,
11802 };
11803 draw_indexed_mesh!(pipe, uoff, mesh);
11804 }
11805
11806 Cmd::VectorOverlay { mesh, uoff } => {
11807 rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
11808 draw_indexed_mesh!(&pipes.mesh_overlay, uoff, mesh);
11809 }
11810
11811 Cmd::VectorClipPush {
11812 mesh,
11813 uoff,
11814 scissor,
11815 difference,
11816 applied,
11817 } => {
11818 let scissor =
11819 clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
11820 let usable = applied && scissor.2 > 0 && scissor.3 > 0;
11821 if usable {
11822 rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11823 active_scissor = Some(scissor);
11824 let pipe = if difference {
11825 &pipes.mesh_clip_dec
11826 } else {
11827 &pipes.mesh_clip_inc
11828 };
11829 draw_indexed_mesh!(pipe, uoff, mesh);
11830 if !difference {
11831 clip_depth = (clip_depth + 1).min(STENCIL_MAX_DEPTH);
11832 }
11833 } else {
11834 active_scissor = None;
11835 }
11836 rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
11837 }
11838
11839 Cmd::VectorClipPop {
11840 mesh,
11841 uoff,
11842 scissor,
11843 stencil_scissor,
11844 difference,
11845 applied,
11846 } => {
11847 let scissor =
11848 clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
11849 let dec = clamp_scissor(
11850 stencil_scissor.0,
11851 stencil_scissor.1,
11852 stencil_scissor.2,
11853 stencil_scissor.3,
11854 tw,
11855 th,
11856 );
11857 if scissor.2 > 0 && scissor.3 > 0 {
11858 rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11859 active_scissor = Some(scissor);
11860 } else {
11861 active_scissor = None;
11862 }
11863 if applied && dec.2 > 0 && dec.3 > 0 {
11864 rpass.set_scissor_rect(dec.0, dec.1, dec.2, dec.3);
11865 let pipe = if difference {
11866 &pipes.mesh_clip_inc
11867 } else {
11868 &pipes.mesh_clip_dec
11869 };
11870 draw_indexed_mesh!(pipe, uoff, mesh);
11871 if scissor.2 > 0 && scissor.3 > 0 {
11872 rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11873 }
11874 if !difference {
11875 clip_depth = clip_depth.saturating_sub(1);
11876 }
11877 }
11878 rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
11879 }
11880
11881 Cmd::Callback {
11882 rect,
11883 clip_rect,
11884 scissor,
11885 restore_scissor,
11886 callback_id,
11887 payload,
11888 } => {
11889 if let Some(cb) = payload.downcast_ref::<Callback>() {
11890 let vp_x = rect.x.ceil().max(0.0);
11896 let vp_y = rect.y.ceil().max(0.0);
11897 let vp_w = ((rect.x + rect.w).floor() - vp_x).max(1.0);
11898 let vp_h = ((rect.y + rect.h).floor() - vp_y).max(1.0);
11899 if vp_w > 0.0 && vp_h > 0.0 && clip_rect.w > 0.0 && clip_rect.h > 0.0 {
11900 rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
11901 rpass.set_viewport(vp_x, vp_y, vp_w, vp_h, 0.0, 1.0);
11902 let info = repose_core::PaintCallbackInfo {
11903 viewport: rect,
11904 clip_rect,
11905 pixels_per_point: self.pixels_per_point,
11906 screen_size_px: [tw, th],
11907 };
11908 let descriptor = ScreenDescriptor {
11909 size_in_pixels: [tw, th],
11910 pixels_per_point: self.pixels_per_point,
11911 target_format: if is_layer {
11912 self.layer_target_format()
11913 } else if working_target {
11914 wgpu::TextureFormat::Rgba16Float
11915 } else {
11916 self.output_format
11917 },
11918 sample_count: if is_layer {
11919 1
11920 } else {
11921 self.active_surface_msaa_samples()
11922 },
11923 };
11924 let scope =
11925 callback_scope_key(cb, callback_id, pass.target, &descriptor);
11926 let resources = self
11927 .callback_scoped_resources
11928 .get(&scope)
11929 .unwrap_or(&self.callback_resources);
11930 let outcome = {
11937 let mut callback_pass = CallbackRenderPass::new(
11938 &mut rpass,
11939 descriptor.target_format,
11940 descriptor.sample_count,
11941 );
11942 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
11943 cb.0.paint(info, &mut callback_pass, resources)
11944 }))
11945 };
11946 if outcome.is_err() {
11947 log::warn!("paint callback panicked; state restored");
11948 }
11949 rpass.set_viewport(0.0, 0.0, tw as f32, th as f32, 0.0, 1.0);
11950 let restore = if restore_scissor.2 > 0 && restore_scissor.3 > 0 {
11951 restore_scissor
11952 } else {
11953 (0, 0, 1, 1)
11954 };
11955 rpass.set_scissor_rect(restore.0, restore.1, restore.2, restore.3);
11956 rpass.set_bind_group(0, &self.globals_bind, &[]);
11957 rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
11958 }
11959 } else {
11960 log::warn!("Unknown paint callback payload");
11961 }
11962 }
11963 }
11964 }
11965 if is_layer {
11966 clip_depth = clip_depth_stack.pop().unwrap_or(0);
11967 }
11968 }
11969
11970 let transient_ids: HashSet<u32> = flatten_ids_used.iter().copied().collect();
11971 let producer_ids = std::mem::take(&mut self.producer_layer_ids);
11972 self.blur_scratch_ids
11976 .retain(|&(source, _), _| producer_ids.contains(&source));
11977 let live_scratch: HashSet<u32> = self.blur_scratch_ids.values().copied().collect();
11978 let stale_layers: Vec<u32> = self
11979 .layer_pool
11980 .keys()
11981 .filter_map(|id| {
11982 (!producer_ids.contains(id)
11983 && !transient_ids.contains(id)
11984 && !live_scratch.contains(id))
11985 .then_some(*id)
11986 })
11987 .collect();
11988 for id in stale_layers {
11989 if let Some(layer) = self.layer_pool.remove(&id) {
11990 self.layer_bytes_total = self.layer_bytes_total.saturating_sub(layer.bytes);
11991 if layer.transient {
11992 self.transient_layer_bytes_total =
11993 self.transient_layer_bytes_total.saturating_sub(layer.bytes);
11994 }
11995 }
11996 }
11997 self.flatten_layer_ids = flatten_ids_used;
11998
11999 if self.working_space
12001 && let (Some(_ws_view), Some(ws_bind), Some(display_pipeline)) =
12002 (&self.ws_view, &self.ws_bind, &self.display_pipeline)
12003 {
12004 let swap_view = target_view.clone();
12005 let mut display_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
12006 label: Some("display transform"),
12007 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
12008 view: &swap_view,
12009 resolve_target: None,
12010 ops: wgpu::Operations {
12011 load: wgpu::LoadOp::Load,
12012 store: wgpu::StoreOp::Store,
12013 },
12014 depth_slice: None,
12015 })],
12016 depth_stencil_attachment: None,
12017 timestamp_writes: None,
12018 occlusion_query_set: None,
12019 multiview_mask: None,
12020 });
12021 display_pass.set_pipeline(display_pipeline);
12022 display_pass.set_bind_group(1, ws_bind, &[]);
12023 display_pass.draw(0..3, 0..1);
12024 }
12025 self.recycle_blend_snapshots();
12026 }
12027
12028 pub fn render_to_view(
12032 &mut self,
12033 scene: &Scene,
12034 encoder: &mut wgpu::CommandEncoder,
12035 target_view: &wgpu::TextureView,
12036 width: u32,
12037 height: u32,
12038 clear_color: Option<[f64; 4]>,
12039 ) {
12040 self.resize(width, height);
12041
12042 if let Err(error) = self.begin_frame() {
12043 log::error!("render_to_view: {error:#}");
12044 return;
12045 }
12046
12047 if self.output_width == 0 || self.output_height == 0 {
12048 self.end_frame();
12049 return;
12050 }
12051
12052 self.render_scene_to_encoder(scene, encoder, target_view, clear_color);
12053 self.end_frame();
12054 }
12055}
12056
12057#[cfg(target_os = "linux")]
12058fn validate_dmabuf_plane(
12059 fd: &std::os::unix::io::OwnedFd,
12060 offset: u64,
12061 stride: u32,
12062 width: u32,
12063 height: u32,
12064 bytes_per_pixel: u32,
12065) -> anyhow::Result<u64> {
12066 if width == 0 || height == 0 || bytes_per_pixel == 0 {
12067 anyhow::bail!("DMA-BUF plane dimensions and bytes per pixel must be non-zero");
12068 }
12069 if !offset.is_multiple_of(4) || !stride.is_multiple_of(4) {
12070 anyhow::bail!("DMA-BUF offset and stride must be 4-byte aligned");
12071 }
12072 let row_bytes = (width as u64)
12073 .checked_mul(bytes_per_pixel as u64)
12074 .ok_or_else(|| anyhow::anyhow!("DMA-BUF row size overflow"))?;
12075 if (stride as u64) < row_bytes {
12076 anyhow::bail!(
12077 "DMA-BUF stride {} is smaller than row size {row_bytes}",
12078 stride
12079 );
12080 }
12081 let span = (height as u64 - 1)
12082 .checked_mul(stride as u64)
12083 .and_then(|value| value.checked_add(row_bytes))
12084 .ok_or_else(|| anyhow::anyhow!("DMA-BUF plane size overflow"))?;
12085 let file = std::fs::File::from(
12086 fd.try_clone()
12087 .map_err(|error| anyhow::anyhow!("duplicate DMA-BUF fd: {error}"))?,
12088 );
12089 let file_size = file
12090 .metadata()
12091 .map_err(|error| anyhow::anyhow!("stat DMA-BUF fd: {error}"))?
12092 .len();
12093 if offset > file_size || span > file_size - offset {
12094 anyhow::bail!("DMA-BUF plane offset/size {offset}+{span} exceeds file size {file_size}");
12095 }
12096 Ok(file_size)
12097}
12098
12099fn is_filterable_color_format(format: wgpu::TextureFormat) -> bool {
12100 matches!(
12101 format,
12102 wgpu::TextureFormat::Rgba8Unorm
12103 | wgpu::TextureFormat::Rgba8UnormSrgb
12104 | wgpu::TextureFormat::Bgra8Unorm
12105 | wgpu::TextureFormat::Bgra8UnormSrgb
12106 | wgpu::TextureFormat::Rgba16Unorm
12107 | wgpu::TextureFormat::Rgba16Float
12108 | wgpu::TextureFormat::Rgba32Float
12109 | wgpu::TextureFormat::Rgb10a2Unorm
12110 )
12111}
12112
12113fn native_format_supported(format: wgpu::TextureFormat, features: wgpu::Features) -> bool {
12114 if !is_filterable_color_format(format) {
12115 return false;
12116 }
12117 match format {
12118 wgpu::TextureFormat::Rgba16Unorm => {
12119 features.contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
12120 }
12121 wgpu::TextureFormat::Rgba32Float => features.contains(wgpu::Features::FLOAT32_FILTERABLE),
12122 _ => true,
12123 }
12124}
12125
12126fn validate_sampler_descriptor(
12127 device: &wgpu::Device,
12128 sampler: &wgpu::SamplerDescriptor<'_>,
12129) -> anyhow::Result<()> {
12130 if sampler.compare.is_some()
12131 || !matches!(
12132 sampler.mag_filter,
12133 wgpu::FilterMode::Nearest | wgpu::FilterMode::Linear
12134 )
12135 || !matches!(
12136 sampler.min_filter,
12137 wgpu::FilterMode::Nearest | wgpu::FilterMode::Linear
12138 )
12139 || sampler.anisotropy_clamp == 0
12140 || !sampler.lod_min_clamp.is_finite()
12141 || !sampler.lod_max_clamp.is_finite()
12142 || sampler.lod_min_clamp < 0.0
12143 || sampler.lod_max_clamp < sampler.lod_min_clamp
12144 {
12145 anyhow::bail!("native sampler must be non-comparison linear filtering with finite LODs");
12146 }
12147 if (matches!(sampler.address_mode_u, wgpu::AddressMode::ClampToBorder)
12148 || matches!(sampler.address_mode_v, wgpu::AddressMode::ClampToBorder)
12149 || matches!(sampler.address_mode_w, wgpu::AddressMode::ClampToBorder))
12150 && !device
12151 .features()
12152 .contains(wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER)
12153 {
12154 anyhow::bail!("native sampler uses unsupported clamp-to-border addressing");
12155 }
12156 Ok(())
12157}
12158
12159fn validate_texture_dimensions(
12160 device: &wgpu::Device,
12161 width: u32,
12162 height: u32,
12163) -> anyhow::Result<()> {
12164 if width == 0
12165 || height == 0
12166 || width > device.limits().max_texture_dimension_2d
12167 || height > device.limits().max_texture_dimension_2d
12168 {
12169 anyhow::bail!("texture dimensions are outside the device limit");
12170 }
12171 Ok(())
12172}
12173
12174fn validate_image_handle(handle: u64) -> anyhow::Result<()> {
12175 if handle == 0 {
12176 anyhow::bail!("image handle 0 is reserved");
12177 }
12178 Ok(())
12179}
12180
12181fn decode_limits(max_dimension: u32) -> image::Limits {
12186 let mut limits = image::Limits::default();
12187 limits.max_image_width = Some(max_dimension);
12188 limits.max_image_height = Some(max_dimension);
12189 limits.max_alloc = Some(MAX_IMAGE_DECODE_BYTES);
12190 limits
12191}
12192
12193fn decode_image_rgba8(data: &[u8], max_dimension: u32) -> anyhow::Result<(u32, u32, Vec<u8>)> {
12197 let format = image::guess_format(data)?;
12198 let (w, h) = {
12199 let mut probe = image::ImageReader::with_format(std::io::Cursor::new(data), format);
12200 probe.limits(decode_limits(max_dimension));
12201 probe.into_dimensions()?
12202 };
12203 validate_image_decode_budget(w, h, max_dimension)?;
12204 let mut reader = image::ImageReader::with_format(std::io::Cursor::new(data), format);
12205 reader.limits(decode_limits(max_dimension));
12206 let rgba = reader.decode()?.into_rgba8();
12207 Ok((w, h, rgba.into_raw()))
12208}
12209
12210fn validate_image_decode_budget(w: u32, h: u32, max_dimension: u32) -> anyhow::Result<()> {
12211 if w == 0 || h == 0 {
12212 anyhow::bail!("encoded image has zero dimensions");
12213 }
12214 if w > max_dimension || h > max_dimension {
12215 anyhow::bail!("encoded image {w}x{h} exceeds the device texture limit {max_dimension}");
12216 }
12217 let bytes = checked_image_bytes(w, h, 4)?;
12218 if bytes > MAX_IMAGE_DECODE_BYTES {
12219 anyhow::bail!(
12220 "encoded image {w}x{h} needs {bytes} bytes to decode, over the {MAX_IMAGE_DECODE_BYTES} byte limit"
12221 );
12222 }
12223 Ok(())
12224}
12225
12226fn checked_image_bytes(w: u32, h: u32, bytes_per_pixel: u32) -> anyhow::Result<u64> {
12227 if w == 0 || h == 0 || bytes_per_pixel == 0 {
12228 anyhow::bail!("image dimensions and bytes per pixel must be non-zero");
12229 }
12230 (w as u64)
12231 .checked_mul(h as u64)
12232 .and_then(|size| size.checked_mul(bytes_per_pixel as u64))
12233 .ok_or_else(|| anyhow::anyhow!("image byte size overflow"))
12234}
12235
12236fn texture_storage_bytes(
12237 format: wgpu::TextureFormat,
12238 width: u32,
12239 height: u32,
12240) -> anyhow::Result<u64> {
12241 if width == 0 || height == 0 {
12242 anyhow::bail!("texture dimensions must be non-zero");
12243 }
12244 let bytes = format.theoretical_memory_footprint(wgpu::Extent3d {
12245 width,
12246 height,
12247 depth_or_array_layers: 1,
12248 });
12249 if bytes == 0 {
12250 anyhow::bail!("texture format has no storage size");
12251 }
12252 Ok(bytes)
12253}
12254
12255fn texture_storage_bytes_with_samples(
12256 format: wgpu::TextureFormat,
12257 width: u32,
12258 height: u32,
12259 sample_count: u32,
12260) -> anyhow::Result<u64> {
12261 texture_storage_bytes(format, width, height)?
12262 .checked_mul(u64::from(sample_count.max(1)))
12263 .ok_or_else(|| anyhow::anyhow!("multisample texture size overflow"))
12264}
12265
12266fn checked_image_bytes_usize(w: u32, h: u32, bytes_per_pixel: u32) -> anyhow::Result<usize> {
12267 usize::try_from(checked_image_bytes(w, h, bytes_per_pixel)?)
12268 .map_err(|_| anyhow::anyhow!("image byte size exceeds addressable memory"))
12269}
12270
12271fn clamp_scissor(x: u32, y: u32, w: u32, h: u32, tw: u32, th: u32) -> (u32, u32, u32, u32) {
12272 if w == 0 || h == 0 || tw == 0 || th == 0 {
12273 return (0, 0, 0, 0);
12274 }
12275 let x = x.min(tw - 1);
12276 let y = y.min(th - 1);
12277 let w = w.min(tw - x);
12278 let h = h.min(th - y);
12279 (x, y, w, h)
12280}
12281
12282fn rect_to_scissor(r: repose_core::Rect, width: u32, height: u32) -> (u32, u32, u32, u32) {
12283 if width == 0
12284 || height == 0
12285 || !r.x.is_finite()
12286 || !r.y.is_finite()
12287 || !r.w.is_finite()
12288 || !r.h.is_finite()
12289 || r.w <= 0.0
12290 || r.h <= 0.0
12291 {
12292 return (0, 0, 0, 0);
12293 }
12294 let x0 = r.x.ceil().max(0.0).min(width as f32);
12297 let y0 = r.y.ceil().max(0.0).min(height as f32);
12298 let x1 = (r.x + r.w).floor().max(x0).min(width as f32);
12299 let y1 = (r.y + r.h).floor().max(y0).min(height as f32);
12300 if x1 <= x0 || y1 <= y0 {
12301 return (0, 0, 0, 0);
12302 }
12303 (x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32)
12304}
12305
12306fn rect_to_ndc(r: repose_core::Rect, width: f32, height: f32) -> [f32; 4] {
12307 if width <= 0.0 || height <= 0.0 || !r.x.is_finite() || !r.y.is_finite() {
12308 return [0.0, 0.0, 0.0, 0.0];
12309 }
12310 let x0 = r.x / width * 2.0 - 1.0;
12311 let y0 = 1.0 - r.y / height * 2.0;
12312 let x1 = (r.x + r.w) / width * 2.0 - 1.0;
12313 let y1 = 1.0 - (r.y + r.h) / height * 2.0;
12314 [x0.min(x1), y0.min(y1), (x1 - x0).abs(), (y1 - y0).abs()]
12315}
12316
12317fn parent_rect_to_layer_rect(
12318 rect: repose_core::Rect,
12319 parent_transform: &Transform,
12320 origin: (f32, f32),
12321) -> Option<repose_core::Rect> {
12322 let inverse = parent_transform.inverse_linear()?;
12323 let tx = parent_transform.translate_x;
12324 let ty = parent_transform.translate_y;
12325 let corners = [
12326 (rect.x, rect.y),
12327 (rect.x + rect.w, rect.y),
12328 (rect.x, rect.y + rect.h),
12329 (rect.x + rect.w, rect.y + rect.h),
12330 ];
12331 let mut min_x = f32::INFINITY;
12332 let mut min_y = f32::INFINITY;
12333 let mut max_x = f32::NEG_INFINITY;
12334 let mut max_y = f32::NEG_INFINITY;
12335 for (x, y) in corners {
12336 let local_x = x - tx;
12337 let local_y = y - ty;
12338 let transformed_x = inverse[0] * local_x + inverse[1] * local_y;
12339 let transformed_y = inverse[2] * local_x + inverse[3] * local_y;
12340 min_x = min_x.min(transformed_x);
12341 min_y = min_y.min(transformed_y);
12342 max_x = max_x.max(transformed_x);
12343 max_y = max_y.max(transformed_y);
12344 }
12345 let result = repose_core::Rect {
12346 x: min_x - origin.0,
12347 y: min_y - origin.1,
12348 w: (max_x - min_x).max(0.0),
12349 h: (max_y - min_y).max(0.0),
12350 };
12351 (result.x.is_finite() && result.y.is_finite() && result.w.is_finite() && result.h.is_finite())
12352 .then_some(result)
12353}
12354
12355fn parent_radii_to_layer_radii(radii: [f32; 4], parent_transform: &Transform) -> Option<[f32; 4]> {
12356 let m = parent_transform.linear();
12357 let sx = (m[0] * m[0] + m[2] * m[2]).sqrt();
12358 let sy = (m[1] * m[1] + m[3] * m[3]).sqrt();
12359 (sx.is_finite() && sy.is_finite() && sx > 1e-6 && sy > 1e-6)
12360 .then(|| [radii[0] / sx, radii[1] / sy, radii[2] / sx, radii[3] / sy])
12361}
12362
12363fn parent_affine_to_layer_affine(
12364 affine: [f32; 6],
12365 parent_transform: &Transform,
12366 origin: (f32, f32),
12367) -> Option<[f32; 6]> {
12368 let inverse = parent_transform.inverse_linear()?;
12369 let tx = affine[4] - parent_transform.translate_x;
12370 let ty = affine[5] - parent_transform.translate_y;
12371 Some([
12372 inverse[0] * affine[0] + inverse[1] * affine[2],
12373 inverse[0] * affine[1] + inverse[1] * affine[3],
12374 inverse[2] * affine[0] + inverse[3] * affine[2],
12375 inverse[2] * affine[1] + inverse[3] * affine[3],
12376 inverse[0] * tx + inverse[1] * ty - origin.0,
12377 inverse[2] * tx + inverse[3] * ty - origin.1,
12378 ])
12379}
12380
12381fn checked_copy_region(
12382 region: repose_core::Rect,
12383 target_width: u32,
12384 target_height: u32,
12385) -> Option<(u32, u32, u32, u32)> {
12386 if target_width == 0
12387 || target_height == 0
12388 || !region.x.is_finite()
12389 || !region.y.is_finite()
12390 || !region.w.is_finite()
12391 || !region.h.is_finite()
12392 || region.w <= 0.0
12393 || region.h <= 0.0
12394 {
12395 return None;
12396 }
12397 let x0 = region.x.floor().max(0.0).min(target_width as f32) as u32;
12398 let y0 = region.y.floor().max(0.0).min(target_height as f32) as u32;
12399 let x1 = (region.x + region.w)
12400 .ceil()
12401 .max(x0 as f32)
12402 .min(target_width as f32) as u32;
12403 let y1 = (region.y + region.h)
12404 .ceil()
12405 .max(y0 as f32)
12406 .min(target_height as f32) as u32;
12407 (x1 > x0 && y1 > y0).then_some((x0, y0, x1 - x0, y1 - y0))
12408}
12409
12410fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> repose_core::Rect {
12411 let x0 = a.x.max(b.x);
12412 let y0 = a.y.max(b.y);
12413 let x1 = (a.x + a.w).min(b.x + b.w);
12414 let y1 = (a.y + a.h).min(b.y + b.h);
12415 repose_core::Rect {
12416 x: x0,
12417 y: y0,
12418 w: (x1 - x0).max(0.0),
12419 h: (y1 - y0).max(0.0),
12420 }
12421}
12422
12423#[cfg(test)]
12424mod tests {
12425 use super::*;
12426
12427 #[test]
12428 fn image_source_rect_uses_texel_edges() {
12429 let source = resolve_image_source(Some(ImageSourceRect::new(2, 1, 3, 2)), 10, 8)
12430 .expect("source should be valid");
12431 assert_eq!(source.uv_min, [0.2, 0.125]);
12432 assert_eq!(source.uv_max, [0.5, 0.375]);
12433 assert_eq!(source.width, 3.0);
12434 assert_eq!(source.height, 2.0);
12435 assert!(resolve_image_source(Some(ImageSourceRect::new(8, 0, 3, 1)), 10, 8).is_none());
12436 assert!(resolve_image_source(Some(ImageSourceRect::new(0, 0, 0, 1)), 10, 8).is_none());
12437 }
12438
12439 #[test]
12440 fn image_fit_stays_inside_selected_source() {
12441 let source = resolve_image_source(Some(ImageSourceRect::new(2, 1, 4, 2)), 10, 8)
12442 .expect("source should be valid");
12443 let rect = repose_core::Rect {
12444 x: 20.0,
12445 y: 30.0,
12446 w: 8.0,
12447 h: 8.0,
12448 };
12449 let (_, uv) = image_fit_geometry(rect, ImageFit::Cover, ImageAlignment::Center, &source)
12450 .expect("cover geometry should be valid");
12451 assert!(uv[0] >= source.uv_min[0] && uv[2] <= source.uv_max[0]);
12452 assert!(uv[1] >= source.uv_min[1] && uv[3] <= source.uv_max[1]);
12453 }
12454
12455 #[test]
12456 fn ring_alignment_rejects_bad_input() {
12457 assert_eq!(align_up(0, 4).unwrap(), 0);
12458 assert_eq!(align_up(1, 4).unwrap(), 4);
12459 assert!(align_up(1, 3).is_err());
12460 assert!(align_up(u64::MAX, 4).is_err());
12461 }
12462
12463 #[test]
12466 fn mesh_uniform_stride_is_always_a_power_of_two() {
12467 for base in [4u64, 16, 32, 64, 128, 256, 512] {
12468 for size in [
12469 std::mem::size_of::<MeshUniform>() as u64,
12470 96,
12471 272,
12472 288,
12473 1024,
12474 ] {
12475 let stride = mesh_uniform_stride(size, base);
12476 assert!(
12477 stride.is_power_of_two(),
12478 "stride {stride} for size {size} base {base} is not a power of two"
12479 );
12480 assert_eq!(stride % base, 0, "stride {stride} misaligned to {base}");
12481 assert!(stride >= size, "stride {stride} below size {size}");
12482 assert!(
12483 align_up(size, stride).is_ok(),
12484 "capacity alignment rejects stride {stride}"
12485 );
12486 }
12487 }
12488 }
12489
12490 fn wgsl_type_size(module: &naga::Module, ty: naga::Handle<naga::Type>) -> u32 {
12492 match &module.types[ty].inner {
12493 naga::TypeInner::Scalar(_) | naga::TypeInner::Atomic(_) => 4,
12494 naga::TypeInner::Vector { size, .. } => 4 * *size as u32,
12495 naga::TypeInner::Array {
12496 size: naga::ArraySize::Constant(count),
12497 stride,
12498 ..
12499 } => stride * count.get(),
12500 _ => 0,
12501 }
12502 }
12503
12504 #[test]
12509 fn mesh_uniform_matches_the_wgsl_member_layout() {
12510 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/src/shaders/mesh.wgsl");
12511 let module = naga::front::wgsl::parse_str(&std::fs::read_to_string(path).expect("read"))
12512 .expect("mesh.wgsl parses");
12513 let wgsl = module
12514 .types
12515 .iter()
12516 .filter(|(_, ty)| ty.name.as_deref() == Some("MeshUniform"))
12517 .find_map(|(_, ty)| match &ty.inner {
12518 naga::TypeInner::Struct { members, .. } => Some(members.as_slice()),
12519 _ => None,
12520 })
12521 .expect("mesh.wgsl declares MeshUniform");
12522 let rust = [
12523 ("m0", std::mem::offset_of!(MeshUniform, m0)),
12524 ("m1", std::mem::offset_of!(MeshUniform, m1)),
12525 ("paint", std::mem::offset_of!(MeshUniform, paint)),
12526 ("color0", std::mem::offset_of!(MeshUniform, color0)),
12527 ("color1", std::mem::offset_of!(MeshUniform, color1)),
12528 ("grad_start", std::mem::offset_of!(MeshUniform, grad_start)),
12529 ("grad_end", std::mem::offset_of!(MeshUniform, grad_end)),
12530 (
12531 "stop_colors",
12532 std::mem::offset_of!(MeshUniform, stop_colors),
12533 ),
12534 (
12535 "stop_offsets",
12536 std::mem::offset_of!(MeshUniform, stop_offsets),
12537 ),
12538 ];
12539 assert_eq!(wgsl.len(), rust.len(), "MeshUniform member count");
12540 for (member, (name, offset)) in wgsl.iter().zip(rust) {
12541 assert_eq!(member.name.as_deref(), Some(name), "member order");
12542 assert_eq!(member.offset as usize, offset, "offset of {name}");
12543 }
12544 let last = wgsl.last().expect("non-empty");
12545 let wgsl_size = last.offset as usize + wgsl_type_size(&module, last.ty) as usize;
12546 assert_eq!(wgsl_size, std::mem::size_of::<MeshUniform>());
12547 assert_eq!(wgsl_size, MESH_UNIFORM_BYTES);
12548 }
12549
12550 #[test]
12554 fn gradient_stops_pack_clamped_and_monotonic() {
12555 let stops = GradientStops::from_slice(&[
12556 repose_core::GradientStop {
12557 offset: 0.25,
12558 color: repose_core::Color::from_rgba(255, 0, 0, 255),
12559 },
12560 repose_core::GradientStop {
12561 offset: f32::NAN,
12562 color: repose_core::Color::from_rgba(0, 255, 0, 255),
12563 },
12564 repose_core::GradientStop {
12565 offset: -2.0,
12566 color: repose_core::Color::from_rgba(0, 0, 255, 255),
12567 },
12568 repose_core::GradientStop {
12569 offset: 9.0,
12570 color: repose_core::Color::from_rgba(255, 255, 255, 255),
12571 },
12572 ]);
12573 let (colors, offsets, count) = pack_gradient_stops(&stops);
12574 assert_eq!(count, 4);
12575 assert_eq!(offsets, [[0.25, 0.25, 0.25, 1.0], [0.0, 0.0, 0.0, 0.0]]);
12576 assert_eq!(colors[0], [1.0, 0.0, 0.0, 1.0]);
12577 assert_eq!(colors[1], [0.0, 1.0, 0.0, 1.0]);
12578 assert_eq!(colors[2], [0.0, 0.0, 1.0, 1.0]);
12579 assert_eq!(colors[3], [1.0, 1.0, 1.0, 1.0]);
12580 assert_eq!(
12581 pack_gradient_stops(&GradientStops::default()).2,
12582 0,
12583 "an empty table paints nothing"
12584 );
12585 }
12586
12587 #[test]
12592 fn atlas_eviction_spares_the_frame_in_flight() {
12593 fn key(glyph_id: u32) -> repose_text::CacheKey {
12594 repose_text::CacheKey {
12595 font_id: 1,
12596 glyph_id,
12597 font_size_bits: 16.0f32.to_bits(),
12598 variation: None,
12599 }
12600 }
12601 let mut map: HashMap<repose_text::CacheKey, GlyphInfo> = HashMap::new();
12602 for (glyph_id, last_touch, last_used_frame) in
12603 [(1u32, 10u64, 9u64), (2, 20, 7), (3, 30, 7), (4, 40, 7)]
12604 {
12605 map.insert(
12606 key(glyph_id),
12607 GlyphInfo {
12608 u0: 0.0,
12609 v0: 0.0,
12610 u1: 0.1,
12611 v1: 0.1,
12612 w: 10.0,
12613 h: 10.0,
12614 last_touch,
12615 last_used_frame,
12616 },
12617 );
12618 }
12619 let mut used_area = 400;
12620 assert!(evict_atlas_lru(&mut map, &mut used_area, 9, 250));
12621 assert_eq!(used_area, 200);
12622 assert!(map.contains_key(&key(1)), "in-flight glyph was evicted");
12623 assert!(!map.contains_key(&key(2)) && !map.contains_key(&key(3)));
12624 assert!(map.contains_key(&key(4)));
12625 }
12626
12627 #[test]
12628 fn atlas_eviction_refuses_to_empty_the_atlas() {
12629 let only = repose_text::CacheKey {
12630 font_id: 1,
12631 glyph_id: 1,
12632 font_size_bits: 16.0f32.to_bits(),
12633 variation: None,
12634 };
12635 let mut map: HashMap<repose_text::CacheKey, GlyphInfo> = HashMap::new();
12636 map.insert(
12637 only,
12638 GlyphInfo {
12639 u0: 0.0,
12640 v0: 0.0,
12641 u1: 0.1,
12642 v1: 0.1,
12643 w: 10.0,
12644 h: 10.0,
12645 last_touch: 1,
12646 last_used_frame: 1,
12647 },
12648 );
12649 let mut used_area = 100;
12650 assert!(!evict_atlas_lru(&mut map, &mut used_area, 2, 0));
12651 assert_eq!(map.len(), 1);
12652 assert_eq!(used_area, 100);
12653 }
12654
12655 #[test]
12656 fn empty_and_negative_rects_are_suppressed() {
12657 assert_eq!(
12658 rect_to_scissor(
12659 repose_core::Rect {
12660 x: 0.0,
12661 y: 0.0,
12662 w: 0.0,
12663 h: 4.0,
12664 },
12665 16,
12666 16,
12667 ),
12668 (0, 0, 0, 0)
12669 );
12670 assert_eq!(
12671 rect_to_scissor(
12672 repose_core::Rect {
12673 x: 0.0,
12674 y: 0.0,
12675 w: -1.0,
12676 h: 4.0,
12677 },
12678 16,
12679 16,
12680 ),
12681 (0, 0, 0, 0)
12682 );
12683 }
12684
12685 #[test]
12686 fn copy_region_is_checked() {
12687 assert_eq!(
12688 checked_copy_region(
12689 repose_core::Rect {
12690 x: 2.25,
12691 y: 3.5,
12692 w: 4.0,
12693 h: 5.0,
12694 },
12695 16,
12696 16,
12697 ),
12698 Some((2, 3, 5, 6))
12699 );
12700 assert_eq!(
12701 checked_copy_region(
12702 repose_core::Rect {
12703 x: 20.0,
12704 y: 0.0,
12705 w: 2.0,
12706 h: 2.0,
12707 },
12708 16,
12709 16,
12710 ),
12711 None
12712 );
12713 }
12714
12715 #[test]
12716 fn decode_budget_rejects_oversized_encoded_images() {
12717 assert!(validate_image_decode_budget(64, 64, 8192).is_ok());
12718 assert!(validate_image_decode_budget(0, 64, 8192).is_err());
12719 assert!(validate_image_decode_budget(64, 0, 8192).is_err());
12720 assert!(validate_image_decode_budget(8193, 1, 8192).is_err());
12721 assert!(validate_image_decode_budget(1, 8193, 8192).is_err());
12722 assert!(validate_image_decode_budget(8192, 4096, 8192).is_err());
12724 }
12725
12726 #[test]
12727 fn only_wgpu_supported_sample_counts_are_accepted() {
12728 for samples in [1, 2, 4, 8, 16] {
12729 assert!(validate_sample_count("msaa_samples", samples).is_ok());
12730 }
12731 for samples in [0, 3, 5, 7, 9, 17, 32, u32::MAX] {
12732 assert!(
12733 validate_sample_count("msaa_samples", samples).is_err(),
12734 "{samples} must be rejected"
12735 );
12736 }
12737 }
12738
12739 #[test]
12740 fn p010_uses_ten_bit_sample_range() {
12741 let raw = make_yuv_transform_raw(ColorInfo::default(), PixelFormat::P010);
12742 let expected = 65535.0 / (64.0 * 1020.0);
12743 assert!((raw.b[3] - expected).abs() < 1e-6);
12744 assert_eq!(canonical_fourcc(PixelFormat::P010), P010_FOURCC);
12745 assert_eq!(canonical_fourcc(PixelFormat::Nv12), NV12_FOURCC);
12746 assert_eq!(
12747 make_yuv_transform_raw(
12748 ColorInfo {
12749 transfer: repose_core::color::Transfer::Pq,
12750 ..ColorInfo::default()
12751 },
12752 PixelFormat::Nv12,
12753 )
12754 .transfer[0],
12755 3.0
12756 );
12757 assert_eq!(
12758 make_yuv_transform_raw(
12759 ColorInfo {
12760 transfer: repose_core::color::Transfer::Hlg,
12761 ..ColorInfo::default()
12762 },
12763 PixelFormat::Nv12,
12764 )
12765 .transfer[0],
12766 4.0
12767 );
12768 }
12769}