tessera_ui/renderer/
app.rs

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