tessera_ui/renderer/
app.rs

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