tessera_ui/renderer/
app.rs

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