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