tessera_ui/renderer/
app.rs

1use std::{any::TypeId, io, mem, sync::Arc};
2
3use parking_lot::RwLock;
4use smallvec::SmallVec;
5use tracing::{error, info, warn};
6use wgpu::TextureFormat;
7use winit::window::Window;
8
9use crate::{
10    ComputablePipeline, ComputeCommand, DrawCommand, DrawablePipeline, Px, PxPosition,
11    compute::resource::ComputeResourceManager,
12    dp::SCALE_FACTOR,
13    pipeline_cache::{initialize_cache, save_cache},
14    px::{PxRect, PxSize},
15    renderer::command::{AsAny, BarrierRequirement, Command},
16};
17
18use super::{
19    compute::{ComputePipelineRegistry, ErasedComputeBatchItem},
20    drawer::Drawer,
21};
22
23// WGPU context for ping-pong operations
24struct WgpuContext<'a> {
25    encoder: &'a mut wgpu::CommandEncoder,
26    gpu: &'a wgpu::Device,
27    queue: &'a wgpu::Queue,
28    config: &'a wgpu::SurfaceConfiguration,
29}
30
31// Parameters for render_current_pass function
32struct RenderCurrentPassParams<'a> {
33    msaa_view: &'a Option<wgpu::TextureView>,
34    is_first_pass: &'a mut bool,
35    encoder: &'a mut wgpu::CommandEncoder,
36    write_target: &'a wgpu::TextureView,
37    commands_in_pass: &'a mut SmallVec<[DrawOrClip; 32]>,
38    scene_texture_view: &'a wgpu::TextureView,
39    drawer: &'a mut Drawer,
40    gpu: &'a wgpu::Device,
41    queue: &'a wgpu::Queue,
42    config: &'a wgpu::SurfaceConfiguration,
43    clip_stack: &'a mut SmallVec<[PxRect; 16]>,
44}
45
46// Parameters for do_compute function
47struct DoComputeParams<'a> {
48    encoder: &'a mut wgpu::CommandEncoder,
49    commands: Vec<PendingComputeCommand>,
50    compute_pipeline_registry: &'a mut ComputePipelineRegistry,
51    gpu: &'a wgpu::Device,
52    queue: &'a wgpu::Queue,
53    config: &'a wgpu::SurfaceConfiguration,
54    resource_manager: &'a mut ComputeResourceManager,
55    scene_view: &'a wgpu::TextureView,
56    target_a: &'a wgpu::TextureView,
57    target_b: &'a wgpu::TextureView,
58    blit_bind_group_layout: &'a wgpu::BindGroupLayout,
59    blit_sampler: &'a wgpu::Sampler,
60    compute_blit_pipeline: &'a wgpu::RenderPipeline,
61}
62
63// Compute resources for ping-pong operations
64struct ComputeResources<'a> {
65    compute_pipeline_registry: &'a mut ComputePipelineRegistry,
66    resource_manager: &'a mut ComputeResourceManager,
67    compute_target_a: &'a wgpu::TextureView,
68    compute_target_b: &'a wgpu::TextureView,
69}
70
71struct PendingComputeCommand {
72    command: Box<dyn ComputeCommand>,
73    size: PxSize,
74    start_pos: PxPosition,
75    target_rect: PxRect,
76    sampling_rect: PxRect,
77}
78
79pub struct WgpuApp {
80    /// Avoiding release the window
81    #[allow(unused)]
82    pub window: Arc<Window>,
83    /// WGPU device
84    pub gpu: wgpu::Device,
85    /// WGPU surface
86    surface: wgpu::Surface<'static>,
87    /// WGPU queue
88    pub queue: wgpu::Queue,
89    /// WGPU surface configuration
90    pub config: wgpu::SurfaceConfiguration,
91    /// size of the window
92    size: winit::dpi::PhysicalSize<u32>,
93    /// if size is changed
94    size_changed: bool,
95    /// draw pipelines
96    pub drawer: Drawer,
97    /// compute pipelines
98    pub compute_pipeline_registry: ComputePipelineRegistry,
99
100    /// Wgpu cache, if available
101    pub pipeline_cache: Option<wgpu::PipelineCache>,
102    /// Gpu adapter info
103    adapter_info: wgpu::AdapterInfo,
104
105    // Offscreen rendering resources
106    offscreen_texture: wgpu::TextureView,
107
108    // MSAA resources
109    pub sample_count: u32,
110    msaa_texture: Option<wgpu::Texture>,
111    msaa_view: Option<wgpu::TextureView>,
112
113    // Compute resources
114    compute_target_a: wgpu::TextureView,
115    compute_target_b: wgpu::TextureView,
116    compute_commands: Vec<PendingComputeCommand>,
117    pub resource_manager: Arc<RwLock<ComputeResourceManager>>,
118
119    // Blit resources for partial copies
120    blit_pipeline: wgpu::RenderPipeline,
121    blit_bind_group_layout: wgpu::BindGroupLayout,
122    blit_sampler: wgpu::Sampler,
123    compute_blit_pipeline: wgpu::RenderPipeline,
124}
125
126impl WgpuApp {
127    // Small helper functions extracted from `new` to reduce its complexity.
128    //
129    // These helpers keep behavior unchanged but make `new` shorter and easier to analyze.
130    async fn request_adapter_for_surface(
131        instance: &wgpu::Instance,
132        surface: &wgpu::Surface<'_>,
133    ) -> wgpu::Adapter {
134        match instance
135            .request_adapter(&wgpu::RequestAdapterOptions {
136                power_preference: wgpu::PowerPreference::default(),
137                compatible_surface: Some(surface),
138                force_fallback_adapter: false,
139            })
140            .await
141        {
142            Ok(gpu) => gpu,
143            Err(e) => {
144                error!("Failed to find an appropriate adapter: {e:?}");
145                panic!("Failed to find an appropriate adapter: {e:?}");
146            }
147        }
148    }
149
150    async fn request_device_and_queue_for_adapter(
151        adapter: &wgpu::Adapter,
152    ) -> (wgpu::Device, wgpu::Queue) {
153        match adapter
154            .request_device(&wgpu::DeviceDescriptor {
155                required_features: wgpu::Features::empty()
156                    | wgpu::Features::CLEAR_TEXTURE
157                    | wgpu::Features::PIPELINE_CACHE,
158                required_limits: if cfg!(target_arch = "wasm32") {
159                    wgpu::Limits::downlevel_webgl2_defaults()
160                } else {
161                    wgpu::Limits::default()
162                },
163                label: None,
164                memory_hints: wgpu::MemoryHints::MemoryUsage,
165                trace: wgpu::Trace::Off,
166                experimental_features: wgpu::ExperimentalFeatures::default(),
167            })
168            .await
169        {
170            Ok((gpu, queue)) => (gpu, queue),
171            Err(e) => {
172                error!("Failed to create device: {e:?}");
173                panic!("Failed to create device: {e:?}");
174            }
175        }
176    }
177
178    fn make_msaa_resources(
179        gpu: &wgpu::Device,
180        sample_count: u32,
181        config: &wgpu::SurfaceConfiguration,
182    ) -> (Option<wgpu::Texture>, Option<wgpu::TextureView>) {
183        if sample_count > 1 {
184            let texture = gpu.create_texture(&wgpu::TextureDescriptor {
185                label: Some("MSAA Framebuffer"),
186                size: wgpu::Extent3d {
187                    width: config.width,
188                    height: config.height,
189                    depth_or_array_layers: 1,
190                },
191                mip_level_count: 1,
192                sample_count,
193                dimension: wgpu::TextureDimension::D2,
194                format: config.format,
195                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
196                view_formats: &[],
197            });
198            let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
199            (Some(texture), Some(view))
200        } else {
201            (None, None)
202        }
203    }
204
205    /// Create a new WGPU app, as the root of Tessera
206    pub(crate) async fn new(window: Arc<Window>, sample_count: u32) -> Self {
207        // Looking for gpus
208        let instance: wgpu::Instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
209            backends: wgpu::Backends::all(),
210            ..Default::default()
211        });
212        // Create a surface
213        let surface = match instance.create_surface(window.clone()) {
214            Ok(surface) => surface,
215            Err(e) => {
216                error!("Failed to create surface: {e:?}");
217                panic!("Failed to create surface: {e:?}");
218            }
219        };
220        // Looking for adapter gpu
221        let adapter = Self::request_adapter_for_surface(&instance, &surface).await;
222        // Create a device and queue
223        let (gpu, queue) = Self::request_device_and_queue_for_adapter(&adapter).await;
224        // Create surface configuration
225        let size = window.inner_size();
226        let caps = surface.get_capabilities(&adapter);
227        // Choose the present mode
228        let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) {
229            // Fifo is the fallback, it is the most compatible and stable
230            wgpu::PresentMode::Fifo
231        } else {
232            // Immediate is the least preferred, it can cause tearing and is not recommended
233            wgpu::PresentMode::Immediate
234        };
235        info!("Using present mode: {present_mode:?}");
236        let config = wgpu::SurfaceConfiguration {
237            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
238            format: caps.formats[0],
239            width: size.width,
240            height: size.height,
241            present_mode,
242            alpha_mode: wgpu::CompositeAlphaMode::Auto,
243            view_formats: vec![],
244            desired_maximum_frame_latency: 2,
245        };
246        surface.configure(&gpu, &config);
247
248        // Create pipeline cache if supported
249        let pipeline_cache = initialize_cache(&gpu, &adapter.get_info());
250
251        // Create MSAA Target
252        let (msaa_texture, msaa_view) = Self::make_msaa_resources(&gpu, sample_count, &config);
253
254        // Create Pass Targets (Offscreen and Compute)
255        let offscreen_texture = Self::create_pass_target(&gpu, &config, "Offscreen");
256        let compute_target_a =
257            Self::create_compute_pass_target(&gpu, &config, TextureFormat::Rgba8Unorm, "Compute A");
258        let compute_target_b =
259            Self::create_compute_pass_target(&gpu, &config, TextureFormat::Rgba8Unorm, "Compute B");
260
261        let drawer = Drawer::new();
262
263        // Set scale factor for dp conversion
264        let scale_factor = window.scale_factor();
265        info!("Window scale factor: {scale_factor}");
266        let _ = SCALE_FACTOR.set(RwLock::new(scale_factor));
267
268        // Create blit pipeline resources
269        let blit_shader = gpu.create_shader_module(wgpu::include_wgsl!("shaders/blit.wgsl"));
270        let blit_sampler = gpu.create_sampler(&wgpu::SamplerDescriptor::default());
271        let blit_bind_group_layout =
272            gpu.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
273                label: Some("Blit Bind Group Layout"),
274                entries: &[
275                    wgpu::BindGroupLayoutEntry {
276                        binding: 0,
277                        visibility: wgpu::ShaderStages::FRAGMENT,
278                        ty: wgpu::BindingType::Texture {
279                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
280                            view_dimension: wgpu::TextureViewDimension::D2,
281                            multisampled: false,
282                        },
283                        count: None,
284                    },
285                    wgpu::BindGroupLayoutEntry {
286                        binding: 1,
287                        visibility: wgpu::ShaderStages::FRAGMENT,
288                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
289                        count: None,
290                    },
291                ],
292            });
293
294        let blit_pipeline_layout = gpu.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
295            label: Some("Blit Pipeline Layout"),
296            bind_group_layouts: &[&blit_bind_group_layout],
297            push_constant_ranges: &[],
298        });
299
300        let blit_pipeline = gpu.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
301            label: Some("Blit Pipeline"),
302            layout: Some(&blit_pipeline_layout),
303            vertex: wgpu::VertexState {
304                module: &blit_shader,
305                entry_point: Some("vs_main"),
306                buffers: &[],
307                compilation_options: Default::default(),
308            },
309            fragment: Some(wgpu::FragmentState {
310                module: &blit_shader,
311                entry_point: Some("fs_main"),
312                targets: &[Some(config.format.into())],
313                compilation_options: Default::default(),
314            }),
315            primitive: wgpu::PrimitiveState::default(),
316            depth_stencil: None,
317            multisample: wgpu::MultisampleState::default(),
318            multiview: None,
319            cache: pipeline_cache.as_ref(),
320        });
321
322        let compute_blit_pipeline = gpu.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
323            label: Some("Compute Copy Pipeline"),
324            layout: Some(&blit_pipeline_layout),
325            vertex: wgpu::VertexState {
326                module: &blit_shader,
327                entry_point: Some("vs_main"),
328                buffers: &[],
329                compilation_options: Default::default(),
330            },
331            fragment: Some(wgpu::FragmentState {
332                module: &blit_shader,
333                entry_point: Some("fs_main"),
334                targets: &[Some(TextureFormat::Rgba8Unorm.into())],
335                compilation_options: Default::default(),
336            }),
337            primitive: wgpu::PrimitiveState::default(),
338            depth_stencil: None,
339            multisample: wgpu::MultisampleState::default(),
340            multiview: None,
341            cache: pipeline_cache.as_ref(),
342        });
343
344        Self {
345            window,
346            gpu,
347            surface,
348            queue,
349            config,
350            size,
351            size_changed: false,
352            drawer,
353            offscreen_texture,
354            compute_pipeline_registry: ComputePipelineRegistry::new(),
355            pipeline_cache,
356            adapter_info: adapter.get_info(),
357            sample_count,
358            msaa_texture,
359            msaa_view,
360            compute_target_a,
361            compute_target_b,
362            compute_commands: Vec::new(),
363            resource_manager: Arc::new(RwLock::new(ComputeResourceManager::new())),
364            blit_pipeline,
365            blit_bind_group_layout,
366            blit_sampler,
367            compute_blit_pipeline,
368        }
369    }
370
371    /// Registers a new drawable pipeline for a specific command type.
372    ///
373    /// This method takes ownership of the pipeline and wraps it in a type-erased container that can be stored alongside other pipelines of different types.
374    pub fn register_draw_pipeline<T, P>(&mut self, pipeline: P)
375    where
376        T: DrawCommand + 'static,
377        P: DrawablePipeline<T> + 'static,
378    {
379        self.drawer.pipeline_registry.register(pipeline);
380    }
381
382    /// Registers a new compute pipeline for a specific command type.
383    ///
384    /// This method takes ownership of the pipeline and wraps it in a type-erased container that can be stored alongside other pipelines of different types.
385    pub fn register_compute_pipeline<T, P>(&mut self, pipeline: P)
386    where
387        T: ComputeCommand + 'static,
388        P: ComputablePipeline<T> + 'static,
389    {
390        self.compute_pipeline_registry.register(pipeline);
391    }
392
393    fn create_pass_target(
394        gpu: &wgpu::Device,
395        config: &wgpu::SurfaceConfiguration,
396        label_suffix: &str,
397    ) -> wgpu::TextureView {
398        let label = format!("Pass {label_suffix} Texture");
399        let texture_descriptor = wgpu::TextureDescriptor {
400            label: Some(&label),
401            size: wgpu::Extent3d {
402                width: config.width,
403                height: config.height,
404                depth_or_array_layers: 1,
405            },
406            mip_level_count: 1,
407            sample_count: 1,
408            dimension: wgpu::TextureDimension::D2,
409            // Use surface format for compatibility with final copy operations
410            format: config.format,
411            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
412                | wgpu::TextureUsages::TEXTURE_BINDING
413                | wgpu::TextureUsages::COPY_DST
414                | wgpu::TextureUsages::COPY_SRC,
415            view_formats: &[],
416        };
417        let texture = gpu.create_texture(&texture_descriptor);
418        texture.create_view(&wgpu::TextureViewDescriptor::default())
419    }
420
421    fn create_compute_pass_target(
422        gpu: &wgpu::Device,
423        config: &wgpu::SurfaceConfiguration,
424        format: TextureFormat,
425        label_suffix: &str,
426    ) -> wgpu::TextureView {
427        let label = format!("Compute {label_suffix} Texture");
428        let texture_descriptor = wgpu::TextureDescriptor {
429            label: Some(&label),
430            size: wgpu::Extent3d {
431                width: config.width,
432                height: config.height,
433                depth_or_array_layers: 1,
434            },
435            mip_level_count: 1,
436            sample_count: 1,
437            dimension: wgpu::TextureDimension::D2,
438            format,
439            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
440                | wgpu::TextureUsages::TEXTURE_BINDING
441                | wgpu::TextureUsages::STORAGE_BINDING
442                | wgpu::TextureUsages::COPY_DST
443                | wgpu::TextureUsages::COPY_SRC,
444            view_formats: &[],
445        };
446        let texture = gpu.create_texture(&texture_descriptor);
447        texture.create_view(&wgpu::TextureViewDescriptor::default())
448    }
449
450    pub fn register_pipelines(&mut self, register_fn: impl FnOnce(&mut Self)) {
451        register_fn(self);
452    }
453
454    /// Resize the surface
455    /// Real resize will be done in the next frame, in [Self::resize_if_needed]
456    pub(crate) fn resize(&mut self, size: winit::dpi::PhysicalSize<u32>) {
457        if self.size == size {
458            return;
459        }
460        self.size = size;
461        self.size_changed = true;
462    }
463
464    /// Get the size of the surface
465    pub(crate) fn size(&self) -> winit::dpi::PhysicalSize<u32> {
466        self.size
467    }
468
469    pub(crate) fn resize_surface(&mut self) {
470        if self.size.width > 0 && self.size.height > 0 {
471            self.config.width = self.size.width;
472            self.config.height = self.size.height;
473            self.surface.configure(&self.gpu, &self.config);
474            self.rebuild_pass_targets();
475        }
476    }
477
478    pub(crate) fn rebuild_pass_targets(&mut self) {
479        self.offscreen_texture.texture().destroy();
480        self.compute_target_a.texture().destroy();
481        self.compute_target_b.texture().destroy();
482
483        self.offscreen_texture = Self::create_pass_target(&self.gpu, &self.config, "Offscreen");
484        self.compute_target_a = Self::create_compute_pass_target(
485            &self.gpu,
486            &self.config,
487            TextureFormat::Rgba8Unorm,
488            "Compute A",
489        );
490        self.compute_target_b = Self::create_compute_pass_target(
491            &self.gpu,
492            &self.config,
493            TextureFormat::Rgba8Unorm,
494            "Compute B",
495        );
496
497        if self.sample_count > 1 {
498            if let Some(t) = self.msaa_texture.take() {
499                t.destroy();
500            }
501            let (msaa_texture, msaa_view) =
502                Self::make_msaa_resources(&self.gpu, self.sample_count, &self.config);
503            self.msaa_texture = msaa_texture;
504            self.msaa_view = msaa_view;
505        }
506    }
507
508    /// Resize the surface if needed.
509    pub(crate) fn resize_if_needed(&mut self) -> bool {
510        let result = self.size_changed;
511        if self.size_changed {
512            self.resize_surface();
513            self.size_changed = false;
514        }
515        result
516    }
517
518    // Helper does offscreen copy and optional compute; returns an owned TextureView to avoid
519    // holding mutable borrows on pass targets across the caller scope.
520    fn handle_offscreen_and_compute(
521        context: WgpuContext<'_>,
522        offscreen_texture: &mut wgpu::TextureView,
523        output_texture: &mut wgpu::TextureView,
524        compute_commands: Vec<PendingComputeCommand>,
525        compute_resources: ComputeResources<'_>,
526        copy_rect: PxRect,
527        blit_bind_group_layout: &wgpu::BindGroupLayout,
528        blit_sampler: &wgpu::Sampler,
529        blit_pipeline: &wgpu::RenderPipeline,
530        compute_blit_pipeline: &wgpu::RenderPipeline,
531    ) -> wgpu::TextureView {
532        let blit_bind_group = context.gpu.create_bind_group(&wgpu::BindGroupDescriptor {
533            layout: blit_bind_group_layout,
534            entries: &[
535                wgpu::BindGroupEntry {
536                    binding: 0,
537                    resource: wgpu::BindingResource::TextureView(output_texture),
538                },
539                wgpu::BindGroupEntry {
540                    binding: 1,
541                    resource: wgpu::BindingResource::Sampler(blit_sampler),
542                },
543            ],
544            label: Some("Blit Bind Group"),
545        });
546
547        let mut rpass = context
548            .encoder
549            .begin_render_pass(&wgpu::RenderPassDescriptor {
550                label: Some("Blit Pass"),
551                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
552                    view: offscreen_texture,
553                    resolve_target: None,
554                    ops: wgpu::Operations {
555                        load: wgpu::LoadOp::Load,
556                        store: wgpu::StoreOp::Store,
557                    },
558                    depth_slice: None,
559                })],
560                depth_stencil_attachment: None,
561                ..Default::default()
562            });
563
564        rpass.set_pipeline(blit_pipeline);
565        rpass.set_bind_group(0, &blit_bind_group, &[]);
566        // Set a scissor rect to ensure we only write to the required region.
567        rpass.set_scissor_rect(
568            copy_rect.x.0.max(0) as u32,
569            copy_rect.y.0.max(0) as u32,
570            copy_rect.width.0.max(0) as u32,
571            copy_rect.height.0.max(0) as u32,
572        );
573        // Draw a single triangle that covers the whole screen. The scissor rect clips it.
574        rpass.draw(0..3, 0..1);
575
576        drop(rpass); // End the blit pass
577
578        // Apply compute commands if any, reusing existing do_compute implementation
579        if !compute_commands.is_empty() {
580            Self::do_compute(DoComputeParams {
581                encoder: context.encoder,
582                commands: compute_commands,
583                compute_pipeline_registry: compute_resources.compute_pipeline_registry,
584                gpu: context.gpu,
585                queue: context.queue,
586                config: context.config,
587                resource_manager: compute_resources.resource_manager,
588                scene_view: offscreen_texture,
589                target_a: compute_resources.compute_target_a,
590                target_b: compute_resources.compute_target_b,
591                blit_bind_group_layout,
592                blit_sampler,
593                compute_blit_pipeline,
594            })
595        } else {
596            // Return an owned clone so caller does not keep a borrow on read_target
597            offscreen_texture.clone()
598        }
599    }
600
601    /// Render the surface using the unified command system.
602    ///
603    /// This method processes a stream of commands (both draw and compute) and renders
604    /// them to the surface using a multi-pass rendering approach with offscreen texture.
605    /// Commands that require barriers will trigger texture copies between passes.
606    ///
607    /// # Arguments
608    ///
609    /// * `commands` - An iterable of (Command, PxSize, PxPosition) tuples representing
610    ///   the rendering operations to perform.
611    ///
612    /// # Returns
613    ///
614    /// * `Ok(())` if rendering succeeds
615    /// * `Err(wgpu::SurfaceError)` if there are issues with the surface
616    pub(crate) fn render(
617        &mut self,
618        commands: impl IntoIterator<Item = (Command, TypeId, PxSize, PxPosition)>,
619    ) -> Result<(), wgpu::SurfaceError> {
620        // Collect commands into a Vec to allow reordering
621        let commands: Vec<_> = commands.into_iter().collect();
622        // Reorder instructions based on dependencies for better batching optimization
623        let commands = super::reorder::reorder_instructions(commands);
624
625        let output_frame = self.surface.get_current_texture()?;
626        let mut encoder = self
627            .gpu
628            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
629                label: Some("Render Encoder"),
630            });
631
632        let texture_size = wgpu::Extent3d {
633            width: self.config.width,
634            height: self.config.height,
635            depth_or_array_layers: 1,
636        };
637
638        // Clear any existing compute commands
639        if !self.compute_commands.is_empty() {
640            // This is a warning to developers that not all compute commands were used in the last frame.
641            warn!("Not every compute command is used in last frame. This is likely a bug.");
642            self.compute_commands.clear();
643        }
644
645        // Flag for first pass
646        let mut is_first_pass = true;
647
648        // Frame-level begin for all pipelines
649        self.drawer
650            .pipeline_registry
651            .begin_all_frames(&self.gpu, &self.queue, &self.config);
652
653        let mut scene_texture_view = self.offscreen_texture.clone();
654        let mut commands_in_pass: SmallVec<[DrawOrClip; 32]> = SmallVec::new();
655        let mut sampling_rects_in_pass: SmallVec<[PxRect; 16]> = SmallVec::new();
656        let mut clip_stack: SmallVec<[PxRect; 16]> = SmallVec::new();
657
658        let mut output_view = output_frame
659            .texture
660            .create_view(&wgpu::TextureViewDescriptor::default());
661
662        for (command, command_type_id, size, start_pos) in commands {
663            let need_new_pass = commands_in_pass
664                .iter()
665                .rev()
666                .find_map(|command| match &command {
667                    DrawOrClip::Draw(cmd) => Some(cmd),
668                    DrawOrClip::Clip(_) => None,
669                })
670                .map(|cmd| match (cmd.command.barrier(), command.barrier()) {
671                    (None, Some(_)) => true,
672                    (Some(_), Some(barrier)) => {
673                        let last_draw_rect =
674                            extract_sampling_rect(Some(barrier), size, start_pos, texture_size);
675                        !sampling_rects_in_pass
676                            .iter()
677                            .all(|dr| dr.is_orthogonal(&last_draw_rect))
678                    }
679                    (Some(_), None) => false,
680                    (None, None) => false,
681                })
682                .unwrap_or(false);
683
684            if need_new_pass {
685                let mut draw_target_rects: SmallVec<[PxRect; 8]> = SmallVec::new();
686                for rect in commands_in_pass.iter().filter_map(|command| match command {
687                    DrawOrClip::Draw(cmd) if cmd.command.barrier().is_some() => Some(cmd.draw_rect),
688                    _ => None,
689                }) {
690                    draw_target_rects.push(rect);
691                }
692
693                if !draw_target_rects.is_empty() {
694                    let compute_to_run = self.take_compute_commands_for_rects(&draw_target_rects);
695
696                    let mut copy_rects = sampling_rects_in_pass.clone();
697                    for pending in &compute_to_run {
698                        copy_rects.push(pending.sampling_rect);
699                    }
700
701                    if !copy_rects.is_empty() {
702                        let mut combined_rect = copy_rects[0];
703                        for rect in copy_rects.iter().skip(1) {
704                            combined_rect = combined_rect.union(rect);
705                        }
706
707                        let final_view_after_compute = Self::handle_offscreen_and_compute(
708                            WgpuContext {
709                                encoder: &mut encoder,
710                                gpu: &self.gpu,
711                                queue: &self.queue,
712                                config: &self.config,
713                            },
714                            &mut self.offscreen_texture,
715                            &mut output_view,
716                            compute_to_run,
717                            ComputeResources {
718                                compute_pipeline_registry: &mut self.compute_pipeline_registry,
719                                resource_manager: &mut self.resource_manager.write(),
720                                compute_target_a: &self.compute_target_a,
721                                compute_target_b: &self.compute_target_b,
722                            },
723                            combined_rect,
724                            &self.blit_bind_group_layout,
725                            &self.blit_sampler,
726                            &self.blit_pipeline,
727                            &self.compute_blit_pipeline,
728                        );
729                        scene_texture_view = final_view_after_compute;
730                    }
731                }
732
733                render_current_pass(RenderCurrentPassParams {
734                    msaa_view: &self.msaa_view,
735                    is_first_pass: &mut is_first_pass,
736                    encoder: &mut encoder,
737                    write_target: &output_view,
738                    commands_in_pass: &mut commands_in_pass,
739                    scene_texture_view: &scene_texture_view,
740                    drawer: &mut self.drawer,
741                    gpu: &self.gpu,
742                    queue: &self.queue,
743                    config: &self.config,
744                    clip_stack: &mut clip_stack,
745                });
746                commands_in_pass.clear();
747                sampling_rects_in_pass.clear();
748            }
749
750            match command {
751                Command::Draw(cmd) => {
752                    // Compute sampling area for copy and target rect for drawing
753                    if let Some(barrier) = cmd.barrier() {
754                        let sampling_rect =
755                            extract_sampling_rect(Some(barrier), size, start_pos, texture_size);
756                        sampling_rects_in_pass.push(sampling_rect);
757                    }
758                    let draw_rect = extract_target_rect(size, start_pos, texture_size);
759                    // Add the command to the current pass
760                    commands_in_pass.push(DrawOrClip::Draw(DrawCommandWithMetadata {
761                        command: cmd,
762                        type_id: command_type_id,
763                        size,
764                        start_pos,
765                        draw_rect,
766                    }));
767                }
768                Command::Compute(cmd) => {
769                    let barrier = cmd.barrier();
770                    let sampling_rect =
771                        extract_sampling_rect(Some(barrier), size, start_pos, texture_size);
772                    let target_rect = extract_target_rect(size, start_pos, texture_size);
773                    // Add the compute command to the pending list
774                    self.compute_commands.push(PendingComputeCommand {
775                        command: cmd,
776                        size,
777                        start_pos,
778                        target_rect,
779                        sampling_rect,
780                    });
781                }
782                Command::ClipPush(rect) => {
783                    // Push it into command stack
784                    commands_in_pass.push(DrawOrClip::Clip(ClipOps::Push(rect)));
785                }
786                Command::ClipPop => {
787                    // Push it into command stack
788                    commands_in_pass.push(DrawOrClip::Clip(ClipOps::Pop));
789                }
790            }
791        }
792
793        // After processing all commands, we need to render the last pass if there are any commands left
794        if !commands_in_pass.is_empty() {
795            let mut draw_target_rects: SmallVec<[PxRect; 8]> = SmallVec::new();
796            for rect in commands_in_pass.iter().filter_map(|command| match command {
797                DrawOrClip::Draw(cmd) if cmd.command.barrier().is_some() => Some(cmd.draw_rect),
798                _ => None,
799            }) {
800                draw_target_rects.push(rect);
801            }
802
803            if !draw_target_rects.is_empty() {
804                let compute_to_run = self.take_compute_commands_for_rects(&draw_target_rects);
805
806                let mut copy_rects = sampling_rects_in_pass.clone();
807                for pending in &compute_to_run {
808                    copy_rects.push(pending.sampling_rect);
809                }
810
811                if !copy_rects.is_empty() {
812                    let mut combined_rect = copy_rects[0];
813                    for rect in copy_rects.iter().skip(1) {
814                        combined_rect = combined_rect.union(rect);
815                    }
816
817                    let final_view_after_compute = Self::handle_offscreen_and_compute(
818                        WgpuContext {
819                            encoder: &mut encoder,
820                            gpu: &self.gpu,
821                            queue: &self.queue,
822                            config: &self.config,
823                        },
824                        &mut self.offscreen_texture,
825                        &mut output_view,
826                        compute_to_run,
827                        ComputeResources {
828                            compute_pipeline_registry: &mut self.compute_pipeline_registry,
829                            resource_manager: &mut self.resource_manager.write(),
830                            compute_target_a: &self.compute_target_a,
831                            compute_target_b: &self.compute_target_b,
832                        },
833                        combined_rect,
834                        &self.blit_bind_group_layout,
835                        &self.blit_sampler,
836                        &self.blit_pipeline,
837                        &self.compute_blit_pipeline,
838                    );
839                    scene_texture_view = final_view_after_compute;
840                }
841            }
842
843            // Render the current pass before starting a new one
844            render_current_pass(RenderCurrentPassParams {
845                msaa_view: &self.msaa_view,
846                is_first_pass: &mut is_first_pass,
847                encoder: &mut encoder,
848                write_target: &output_view,
849                commands_in_pass: &mut commands_in_pass,
850                scene_texture_view: &scene_texture_view,
851                drawer: &mut self.drawer,
852                gpu: &self.gpu,
853                queue: &self.queue,
854                config: &self.config,
855                clip_stack: &mut clip_stack,
856            });
857            commands_in_pass.clear();
858            sampling_rects_in_pass.clear();
859        }
860
861        if !self.compute_commands.is_empty() {
862            warn!(
863                "{} compute command(s) were not matched with draw commands in this frame",
864                self.compute_commands.len()
865            );
866            self.compute_commands.clear();
867        }
868
869        // Frame-level end for all pipelines
870        self.drawer
871            .pipeline_registry
872            .end_all_frames(&self.gpu, &self.queue, &self.config);
873
874        self.queue.submit(Some(encoder.finish()));
875        output_frame.present();
876
877        Ok(())
878    }
879
880    fn take_compute_commands_for_rects(
881        &mut self,
882        target_rects: &[PxRect],
883    ) -> Vec<PendingComputeCommand> {
884        if target_rects.is_empty() {
885            return Vec::new();
886        }
887
888        let mut taken = Vec::new();
889        let mut remaining = Vec::with_capacity(self.compute_commands.len());
890
891        for pending in self.compute_commands.drain(..) {
892            if target_rects.iter().any(|rect| rect == &pending.target_rect) {
893                taken.push(pending);
894            } else {
895                remaining.push(pending);
896            }
897        }
898
899        self.compute_commands = remaining;
900        taken
901    }
902
903    fn do_compute(params: DoComputeParams<'_>) -> wgpu::TextureView {
904        if params.commands.is_empty() {
905            return params.scene_view.clone();
906        }
907
908        let texture_size = wgpu::Extent3d {
909            width: params.config.width,
910            height: params.config.height,
911            depth_or_array_layers: 1,
912        };
913
914        Self::blit_to_view(
915            params.encoder,
916            params.gpu,
917            params.scene_view,
918            params.target_a,
919            params.blit_bind_group_layout,
920            params.blit_sampler,
921            params.compute_blit_pipeline,
922        );
923
924        let mut read_view = params.target_a.clone();
925        let mut write_target = params.target_b;
926        let mut read_target = params.target_a;
927
928        let commands = &params.commands;
929        let mut index = 0;
930        while index < commands.len() {
931            let command = &commands[index];
932            let type_id = AsAny::as_any(&*command.command).type_id();
933
934            let mut batch_items: SmallVec<[ErasedComputeBatchItem<'_>; 8]> = SmallVec::new();
935            let mut batch_sampling_rects: SmallVec<[PxRect; 8]> = SmallVec::new();
936            let mut cursor = index;
937
938            while cursor < commands.len() {
939                let candidate = &commands[cursor];
940                if AsAny::as_any(&*candidate.command).type_id() != type_id {
941                    break;
942                }
943
944                let sampling_area = candidate.sampling_rect;
945
946                if batch_sampling_rects
947                    .iter()
948                    .any(|existing| rects_overlap(*existing, sampling_area))
949                {
950                    break;
951                }
952
953                batch_sampling_rects.push(sampling_area);
954                batch_items.push(ErasedComputeBatchItem {
955                    command: &*candidate.command,
956                    size: candidate.size,
957                    position: candidate.start_pos,
958                    target_area: candidate.target_rect,
959                });
960                cursor += 1;
961            }
962
963            if batch_items.is_empty() {
964                batch_sampling_rects.push(command.sampling_rect);
965                batch_items.push(ErasedComputeBatchItem {
966                    command: &*command.command,
967                    size: command.size,
968                    position: command.start_pos,
969                    target_area: command.target_rect,
970                });
971                cursor = index + 1;
972            }
973
974            params.encoder.copy_texture_to_texture(
975                read_view.texture().as_image_copy(),
976                write_target.texture().as_image_copy(),
977                texture_size,
978            );
979
980            {
981                let mut cpass = params
982                    .encoder
983                    .begin_compute_pass(&wgpu::ComputePassDescriptor {
984                        label: Some("Compute Pass"),
985                        timestamp_writes: None,
986                    });
987
988                params.compute_pipeline_registry.dispatch_erased(
989                    params.gpu,
990                    params.queue,
991                    params.config,
992                    &mut cpass,
993                    &batch_items,
994                    params.resource_manager,
995                    &read_view,
996                    write_target,
997                );
998            }
999
1000            read_view = write_target.clone();
1001            std::mem::swap(&mut write_target, &mut read_target);
1002            index = cursor;
1003        }
1004
1005        // After the loop, the final result is in the `read_view`,
1006        // because we swapped one last time at the end of the loop.
1007        read_view
1008    }
1009
1010    fn blit_to_view(
1011        encoder: &mut wgpu::CommandEncoder,
1012        device: &wgpu::Device,
1013        source: &wgpu::TextureView,
1014        target: &wgpu::TextureView,
1015        bind_group_layout: &wgpu::BindGroupLayout,
1016        sampler: &wgpu::Sampler,
1017        pipeline: &wgpu::RenderPipeline,
1018    ) {
1019        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1020            layout: bind_group_layout,
1021            entries: &[
1022                wgpu::BindGroupEntry {
1023                    binding: 0,
1024                    resource: wgpu::BindingResource::TextureView(source),
1025                },
1026                wgpu::BindGroupEntry {
1027                    binding: 1,
1028                    resource: wgpu::BindingResource::Sampler(sampler),
1029                },
1030            ],
1031            label: Some("Compute Copy Bind Group"),
1032        });
1033
1034        let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1035            label: Some("Compute Copy Pass"),
1036            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1037                view: target,
1038                resolve_target: None,
1039                depth_slice: None,
1040                ops: wgpu::Operations {
1041                    load: wgpu::LoadOp::Load,
1042                    store: wgpu::StoreOp::Store,
1043                },
1044            })],
1045            depth_stencil_attachment: None,
1046            ..Default::default()
1047        });
1048
1049        rpass.set_pipeline(pipeline);
1050        rpass.set_bind_group(0, &bind_group, &[]);
1051        rpass.draw(0..3, 0..1);
1052    }
1053
1054    pub(crate) fn save_pipeline_cache(&self) -> io::Result<()> {
1055        if let Some(cache) = self.pipeline_cache.as_ref() {
1056            save_cache(cache, &self.adapter_info)?;
1057        }
1058        Ok(())
1059    }
1060}
1061
1062fn rects_overlap(a: PxRect, b: PxRect) -> bool {
1063    let a_left = a.x.0;
1064    let a_top = a.y.0;
1065    let a_right = a_left + a.width.0;
1066    let a_bottom = a_top + a.height.0;
1067
1068    let b_left = b.x.0;
1069    let b_top = b.y.0;
1070    let b_right = b_left + b.width.0;
1071    let b_bottom = b_top + b.height.0;
1072
1073    !(a_right <= b_left || b_right <= a_left || a_bottom <= b_top || b_bottom <= a_top)
1074}
1075
1076fn compute_padded_rect(
1077    size: PxSize,
1078    start_pos: PxPosition,
1079    top: Px,
1080    right: Px,
1081    bottom: Px,
1082    left: Px,
1083    texture_size: wgpu::Extent3d,
1084) -> PxRect {
1085    let padded_x = (start_pos.x - left).max(Px(0));
1086    let padded_y = (start_pos.y - top).max(Px(0));
1087    let padded_width = (size.width + left + right).min(Px(texture_size.width as i32 - padded_x.0));
1088    let padded_height =
1089        (size.height + top + bottom).min(Px(texture_size.height as i32 - padded_y.0));
1090    PxRect {
1091        x: padded_x,
1092        y: padded_y,
1093        width: padded_width,
1094        height: padded_height,
1095    }
1096}
1097
1098fn clamp_rect_to_texture(mut rect: PxRect, texture_size: wgpu::Extent3d) -> PxRect {
1099    rect.x = rect.x.positive().min(texture_size.width).into();
1100    rect.y = rect.y.positive().min(texture_size.height).into();
1101    rect.width = rect
1102        .width
1103        .positive()
1104        .min(texture_size.width - rect.x.positive())
1105        .into();
1106    rect.height = rect
1107        .height
1108        .positive()
1109        .min(texture_size.height - rect.y.positive())
1110        .into();
1111    rect
1112}
1113
1114fn extract_sampling_rect(
1115    barrier: Option<BarrierRequirement>,
1116    size: PxSize,
1117    start_pos: PxPosition,
1118    texture_size: wgpu::Extent3d,
1119) -> PxRect {
1120    match barrier {
1121        Some(BarrierRequirement::Global) => PxRect {
1122            x: Px(0),
1123            y: Px(0),
1124            width: Px(texture_size.width as i32),
1125            height: Px(texture_size.height as i32),
1126        },
1127        Some(BarrierRequirement::PaddedLocal(sampling)) => {
1128            // For actual rendering/compute, use the sampling padding
1129            compute_padded_rect(
1130                size,
1131                start_pos,
1132                sampling.top,
1133                sampling.right,
1134                sampling.bottom,
1135                sampling.left,
1136                texture_size,
1137            )
1138        }
1139        Some(BarrierRequirement::Absolute(rect)) => clamp_rect_to_texture(rect, texture_size),
1140        None => extract_target_rect(size, start_pos, texture_size),
1141    }
1142}
1143
1144fn extract_target_rect(
1145    size: PxSize,
1146    start_pos: PxPosition,
1147    texture_size: wgpu::Extent3d,
1148) -> PxRect {
1149    let x = start_pos.x.positive().min(texture_size.width);
1150    let y = start_pos.y.positive().min(texture_size.height);
1151    let width = size.width.positive().min(texture_size.width - x);
1152    let height = size.height.positive().min(texture_size.height - y);
1153    PxRect {
1154        x: Px::from(x),
1155        y: Px::from(y),
1156        width: Px::from(width),
1157        height: Px::from(height),
1158    }
1159}
1160
1161fn render_current_pass(params: RenderCurrentPassParams<'_>) {
1162    let (view, resolve_target) = if let Some(msaa_view) = params.msaa_view {
1163        (msaa_view, Some(params.write_target))
1164    } else {
1165        (params.write_target, None)
1166    };
1167
1168    let load_ops = if *params.is_first_pass {
1169        *params.is_first_pass = false;
1170        wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)
1171    } else {
1172        wgpu::LoadOp::Load
1173    };
1174
1175    let mut rpass = params
1176        .encoder
1177        .begin_render_pass(&wgpu::RenderPassDescriptor {
1178            label: Some("Render Pass"),
1179            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1180                view,
1181                depth_slice: None,
1182                resolve_target,
1183                ops: wgpu::Operations {
1184                    load: load_ops,
1185                    store: wgpu::StoreOp::Store,
1186                },
1187            })],
1188            ..Default::default()
1189        });
1190
1191    params.drawer.begin_pass(
1192        params.gpu,
1193        params.queue,
1194        params.config,
1195        &mut rpass,
1196        params.scene_texture_view,
1197    );
1198
1199    // Prepare buffered submission state
1200    let mut buffer: Vec<(Box<dyn DrawCommand>, PxSize, PxPosition)> = Vec::new();
1201    let mut last_command_type_id = None;
1202    let mut current_batch_draw_rect: Option<PxRect> = None;
1203    for cmd in mem::take(params.commands_in_pass).into_iter() {
1204        let cmd = match cmd {
1205            DrawOrClip::Clip(clip_ops) => {
1206                // Must flush any existing buffered commands before changing clip state
1207                if !buffer.is_empty() {
1208                    submit_buffered_commands(
1209                        &mut rpass,
1210                        params.drawer,
1211                        params.gpu,
1212                        params.queue,
1213                        params.config,
1214                        &mut buffer,
1215                        params.scene_texture_view,
1216                        params.clip_stack,
1217                        &mut current_batch_draw_rect,
1218                    );
1219                    last_command_type_id = None; // Reset batch type after flush
1220                }
1221                // Update clip stack
1222                match clip_ops {
1223                    ClipOps::Push(rect) => {
1224                        params.clip_stack.push(rect);
1225                    }
1226                    ClipOps::Pop => {
1227                        params.clip_stack.pop();
1228                    }
1229                }
1230                // continue to next command
1231                continue;
1232            }
1233            DrawOrClip::Draw(cmd) => cmd, // Proceed with draw commands
1234        };
1235
1236        // If the incoming command cannot be merged into the current batch, flush first.
1237        if !can_merge_into_batch(&last_command_type_id, cmd.type_id) && !buffer.is_empty() {
1238            submit_buffered_commands(
1239                &mut rpass,
1240                params.drawer,
1241                params.gpu,
1242                params.queue,
1243                params.config,
1244                &mut buffer,
1245                params.scene_texture_view,
1246                params.clip_stack.as_slice(),
1247                &mut current_batch_draw_rect,
1248            );
1249        }
1250
1251        // Add the command to the buffer and update the current batch rect (extracted merge helper).
1252        buffer.push((cmd.command, cmd.size, cmd.start_pos));
1253        last_command_type_id = Some(cmd.type_id);
1254        current_batch_draw_rect = Some(merge_batch_rect(current_batch_draw_rect, cmd.draw_rect));
1255    }
1256
1257    // If there are any remaining commands in the buffer, submit them
1258    if !buffer.is_empty() {
1259        submit_buffered_commands(
1260            &mut rpass,
1261            params.drawer,
1262            params.gpu,
1263            params.queue,
1264            params.config,
1265            &mut buffer,
1266            params.scene_texture_view,
1267            params.clip_stack.as_slice(),
1268            &mut current_batch_draw_rect,
1269        );
1270    }
1271
1272    params.drawer.end_pass(
1273        params.gpu,
1274        params.queue,
1275        params.config,
1276        &mut rpass,
1277        params.scene_texture_view,
1278    );
1279}
1280
1281fn submit_buffered_commands(
1282    rpass: &mut wgpu::RenderPass<'_>,
1283    drawer: &mut Drawer,
1284    gpu: &wgpu::Device,
1285    queue: &wgpu::Queue,
1286    config: &wgpu::SurfaceConfiguration,
1287    buffer: &mut Vec<(Box<dyn DrawCommand>, PxSize, PxPosition)>,
1288    scene_texture_view: &wgpu::TextureView,
1289    clip_stack: &[PxRect],
1290    current_batch_draw_rect: &mut Option<PxRect>,
1291) {
1292    // Take the buffered commands and convert to the transient representation expected by drawer.submit
1293    let commands = mem::take(buffer);
1294    let commands = commands
1295        .iter()
1296        .map(|(cmd, sz, pos)| (&**cmd, *sz, *pos))
1297        .collect::<Vec<_>>();
1298
1299    // Apply clipping to the current batch rectangle; if nothing remains, abort early.
1300    let (current_clip_rect, anything_to_submit) =
1301        apply_clip_to_batch_rect(clip_stack, current_batch_draw_rect);
1302    if !anything_to_submit {
1303        return;
1304    }
1305
1306    let rect = current_batch_draw_rect.unwrap();
1307    set_scissor_rect_from_pxrect(rpass, rect);
1308
1309    drawer.submit(
1310        gpu,
1311        queue,
1312        config,
1313        rpass,
1314        &commands,
1315        scene_texture_view,
1316        current_clip_rect,
1317    );
1318    *current_batch_draw_rect = None;
1319}
1320
1321fn set_scissor_rect_from_pxrect(rpass: &mut wgpu::RenderPass<'_>, rect: PxRect) {
1322    rpass.set_scissor_rect(
1323        rect.x.positive(),
1324        rect.y.positive(),
1325        rect.width.positive(),
1326        rect.height.positive(),
1327    );
1328}
1329
1330/// Apply clip_stack to current_batch_draw_rect. Returns false if intersection yields nothing
1331/// (meaning there is nothing to submit), true otherwise.
1332///
1333/// Also returns the current clipping rectangle (if any) for potential use by the caller.
1334fn apply_clip_to_batch_rect(
1335    clip_stack: &[PxRect],
1336    current_batch_draw_rect: &mut Option<PxRect>,
1337) -> (Option<PxRect>, bool) {
1338    if let Some(clipped_rect) = clip_stack.last() {
1339        let Some(current_rect) = current_batch_draw_rect.as_ref() else {
1340            return (Some(*clipped_rect), false);
1341        };
1342        if let Some(final_rect) = current_rect.intersection(clipped_rect) {
1343            *current_batch_draw_rect = Some(final_rect);
1344            return (Some(*clipped_rect), true);
1345        }
1346        return (Some(*clipped_rect), false);
1347    }
1348    (None, true)
1349}
1350
1351/// Determine whether `next_type_id` (with potential clipping) can be merged into the current batch.
1352/// Equivalent to the negation of the original flush condition:
1353/// merge allowed when last_command_type_id == Some(next_type_id) or last_command_type_id is None.
1354fn can_merge_into_batch(last_command_type_id: &Option<TypeId>, next_type_id: TypeId) -> bool {
1355    match last_command_type_id {
1356        Some(l) => *l == next_type_id,
1357        None => true,
1358    }
1359}
1360
1361/// Merge the existing optional batch rect with a new command rect.
1362fn merge_batch_rect(current: Option<PxRect>, next: PxRect) -> PxRect {
1363    current.map(|dr| dr.union(&next)).unwrap_or(next)
1364}
1365
1366struct DrawCommandWithMetadata {
1367    command: Box<dyn DrawCommand>,
1368    type_id: TypeId,
1369    size: PxSize,
1370    start_pos: PxPosition,
1371    draw_rect: PxRect,
1372}
1373
1374enum DrawOrClip {
1375    Draw(DrawCommandWithMetadata),
1376    Clip(ClipOps),
1377}
1378
1379enum ClipOps {
1380    Push(PxRect),
1381    Pop,
1382}