tessera_ui/renderer/
app.rs

1use std::{any::TypeId, mem, sync::Arc};
2
3use parking_lot::RwLock;
4use tracing::{error, info, warn};
5use wgpu::{ImageSubresourceRange, TextureFormat};
6use winit::window::Window;
7
8use crate::{
9    ComputeCommand, DrawCommand, Px, PxPosition,
10    compute::resource::ComputeResourceManager,
11    dp::SCALE_FACTOR,
12    px::{PxRect, PxSize},
13    renderer::command::{BarrierRequirement, Command},
14};
15
16use super::{compute::ComputePipelineRegistry, drawer::Drawer};
17
18// Render pass resources for ping-pong operation
19struct PassTarget {
20    texture: wgpu::Texture,
21    view: wgpu::TextureView,
22}
23
24// WGPU context for ping-pong operations
25struct WgpuContext<'a> {
26    encoder: &'a mut wgpu::CommandEncoder,
27    gpu: &'a wgpu::Device,
28    queue: &'a wgpu::Queue,
29    config: &'a wgpu::SurfaceConfiguration,
30}
31
32// Parameters for render_current_pass function
33struct RenderCurrentPassParams<'a> {
34    msaa_view: &'a Option<wgpu::TextureView>,
35    is_first_pass: &'a mut bool,
36    encoder: &'a mut wgpu::CommandEncoder,
37    write_target: &'a PassTarget,
38    commands_in_pass: &'a mut Vec<DrawOrClip>,
39    scene_texture_view: &'a wgpu::TextureView,
40    drawer: &'a mut Drawer,
41    gpu: &'a wgpu::Device,
42    queue: &'a wgpu::Queue,
43    config: &'a wgpu::SurfaceConfiguration,
44    clip_stack: &'a mut Vec<PxRect>,
45}
46
47// Parameters for do_compute function
48struct DoComputeParams<'a> {
49    encoder: &'a mut wgpu::CommandEncoder,
50    commands: Vec<(Box<dyn ComputeCommand>, PxSize, PxPosition)>,
51    compute_pipeline_registry: &'a mut ComputePipelineRegistry,
52    gpu: &'a wgpu::Device,
53    queue: &'a wgpu::Queue,
54    config: &'a wgpu::SurfaceConfiguration,
55    resource_manager: &'a mut ComputeResourceManager,
56    scene_view: &'a wgpu::TextureView,
57    target_a: &'a PassTarget,
58    target_b: &'a PassTarget,
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 PassTarget,
67    compute_target_b: &'a PassTarget,
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    // --- New ping-pong rendering resources ---
92    pass_a: PassTarget,
93    pass_b: PassTarget,
94
95    // --- MSAA resources ---
96    pub sample_count: u32,
97    msaa_texture: Option<wgpu::Texture>,
98    msaa_view: Option<wgpu::TextureView>,
99
100    // --- Compute resources ---
101    compute_target_a: PassTarget,
102    compute_target_b: PassTarget,
103    compute_commands: Vec<(Box<dyn ComputeCommand>, PxSize, PxPosition)>,
104    pub resource_manager: Arc<RwLock<ComputeResourceManager>>,
105}
106
107impl WgpuApp {
108    /// Create a new WGPU app, as the root of Tessera
109    // Small helper functions extracted from `new` to reduce its complexity.
110    //
111    // These helpers keep behavior unchanged but make `new` shorter and easier to analyze.
112    async fn request_adapter_for_surface(
113        instance: &wgpu::Instance,
114        surface: &wgpu::Surface<'_>,
115    ) -> wgpu::Adapter {
116        match instance
117            .request_adapter(&wgpu::RequestAdapterOptions {
118                power_preference: wgpu::PowerPreference::default(),
119                compatible_surface: Some(surface),
120                force_fallback_adapter: false,
121            })
122            .await
123        {
124            Ok(gpu) => gpu,
125            Err(e) => {
126                error!("Failed to find an appropriate adapter: {e:?}");
127                panic!("Failed to find an appropriate adapter: {e:?}");
128            }
129        }
130    }
131
132    async fn request_device_and_queue_for_adapter(
133        adapter: &wgpu::Adapter,
134    ) -> (wgpu::Device, wgpu::Queue) {
135        match adapter
136            .request_device(&wgpu::DeviceDescriptor {
137                required_features: wgpu::Features::empty() | wgpu::Features::CLEAR_TEXTURE,
138                required_limits: if cfg!(target_arch = "wasm32") {
139                    wgpu::Limits::downlevel_webgl2_defaults()
140                } else {
141                    wgpu::Limits::default()
142                },
143                label: None,
144                memory_hints: wgpu::MemoryHints::Performance,
145                trace: wgpu::Trace::Off,
146            })
147            .await
148        {
149            Ok((gpu, queue)) => (gpu, queue),
150            Err(e) => {
151                error!("Failed to create device: {e:?}");
152                panic!("Failed to create device: {e:?}");
153            }
154        }
155    }
156
157    fn make_msaa_resources(
158        gpu: &wgpu::Device,
159        sample_count: u32,
160        config: &wgpu::SurfaceConfiguration,
161    ) -> (Option<wgpu::Texture>, Option<wgpu::TextureView>) {
162        if sample_count > 1 {
163            let texture = gpu.create_texture(&wgpu::TextureDescriptor {
164                label: Some("MSAA Framebuffer"),
165                size: wgpu::Extent3d {
166                    width: config.width,
167                    height: config.height,
168                    depth_or_array_layers: 1,
169                },
170                mip_level_count: 1,
171                sample_count,
172                dimension: wgpu::TextureDimension::D2,
173                format: config.format,
174                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
175                view_formats: &[],
176            });
177            let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
178            (Some(texture), Some(view))
179        } else {
180            (None, None)
181        }
182    }
183    pub(crate) async fn new(window: Arc<Window>, sample_count: u32) -> Self {
184        // Looking for gpus
185        let instance: wgpu::Instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
186            backends: wgpu::Backends::all(),
187            ..Default::default()
188        });
189        // Create a surface
190        let surface = match instance.create_surface(window.clone()) {
191            Ok(surface) => surface,
192            Err(e) => {
193                error!("Failed to create surface: {e:?}");
194                panic!("Failed to create surface: {e:?}");
195            }
196        };
197        // Looking for adapter gpu
198        let adapter = Self::request_adapter_for_surface(&instance, &surface).await;
199        // Create a device and queue
200        let (gpu, queue) = Self::request_device_and_queue_for_adapter(&adapter).await;
201        // Create surface configuration
202        let size = window.inner_size();
203        let caps = surface.get_capabilities(&adapter);
204        // Choose the present mode
205        let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) {
206            // Fifo is the fallback, it is the most compatible and stable
207            wgpu::PresentMode::Fifo
208        } else {
209            // Immediate is the least preferred, it can cause tearing and is not recommended
210            wgpu::PresentMode::Immediate
211        };
212        info!("Using present mode: {present_mode:?}");
213        let config = wgpu::SurfaceConfiguration {
214            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
215            format: caps.formats[0],
216            width: size.width,
217            height: size.height,
218            present_mode,
219            alpha_mode: wgpu::CompositeAlphaMode::Auto,
220            view_formats: vec![],
221            desired_maximum_frame_latency: 2,
222        };
223        surface.configure(&gpu, &config);
224
225        // --- Create MSAA Target ---
226        let (msaa_texture, msaa_view) = Self::make_msaa_resources(&gpu, sample_count, &config);
227
228        // --- Create Pass Targets (A and B and Compute) ---
229        let pass_a = Self::create_pass_target(&gpu, &config, "A");
230        let pass_b = Self::create_pass_target(&gpu, &config, "B");
231        let compute_target_a =
232            Self::create_compute_pass_target(&gpu, &config, TextureFormat::Rgba8Unorm, "Compute A");
233        let compute_target_b =
234            Self::create_compute_pass_target(&gpu, &config, TextureFormat::Rgba8Unorm, "Compute B");
235
236        let drawer = Drawer::new();
237
238        // Set scale factor for dp conversion
239        let scale_factor = window.scale_factor();
240        info!("Window scale factor: {scale_factor}");
241        SCALE_FACTOR
242            .set(RwLock::new(scale_factor))
243            .expect("Failed to set scale factor");
244
245        Self {
246            window,
247            gpu,
248            surface,
249            queue,
250            config,
251            size,
252            size_changed: false,
253            drawer,
254            pass_a,
255            pass_b,
256            compute_pipeline_registry: ComputePipelineRegistry::new(),
257            sample_count,
258            msaa_texture,
259            msaa_view,
260            compute_target_a,
261            compute_target_b,
262            compute_commands: Vec::new(),
263            resource_manager: Arc::new(RwLock::new(ComputeResourceManager::new())),
264        }
265    }
266
267    fn create_pass_target(
268        gpu: &wgpu::Device,
269        config: &wgpu::SurfaceConfiguration,
270        label_suffix: &str,
271    ) -> PassTarget {
272        let label = format!("Pass {label_suffix} Texture");
273        let texture_descriptor = wgpu::TextureDescriptor {
274            label: Some(&label),
275            size: wgpu::Extent3d {
276                width: config.width,
277                height: config.height,
278                depth_or_array_layers: 1,
279            },
280            mip_level_count: 1,
281            sample_count: 1,
282            dimension: wgpu::TextureDimension::D2,
283            // Use surface format for compatibility with final copy operations
284            format: config.format,
285            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
286                | wgpu::TextureUsages::TEXTURE_BINDING
287                | wgpu::TextureUsages::COPY_DST
288                | wgpu::TextureUsages::COPY_SRC,
289            view_formats: &[],
290        };
291        let texture = gpu.create_texture(&texture_descriptor);
292        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
293        PassTarget { texture, view }
294    }
295
296    fn create_compute_pass_target(
297        gpu: &wgpu::Device,
298        config: &wgpu::SurfaceConfiguration,
299        format: TextureFormat,
300        label_suffix: &str,
301    ) -> PassTarget {
302        let label = format!("Compute {label_suffix} Texture");
303        let texture_descriptor = wgpu::TextureDescriptor {
304            label: Some(&label),
305            size: wgpu::Extent3d {
306                width: config.width,
307                height: config.height,
308                depth_or_array_layers: 1,
309            },
310            mip_level_count: 1,
311            sample_count: 1,
312            dimension: wgpu::TextureDimension::D2,
313            format,
314            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
315                | wgpu::TextureUsages::TEXTURE_BINDING
316                | wgpu::TextureUsages::STORAGE_BINDING
317                | wgpu::TextureUsages::COPY_DST
318                | wgpu::TextureUsages::COPY_SRC,
319            view_formats: &[],
320        };
321        let texture = gpu.create_texture(&texture_descriptor);
322        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
323        PassTarget { texture, view }
324    }
325
326    pub fn register_pipelines(&mut self, register_fn: impl FnOnce(&mut Self)) {
327        register_fn(self);
328    }
329
330    /// Resize the surface
331    /// Real resize will be done in the next frame, in [Self::resize_if_needed]
332    pub(crate) fn resize(&mut self, size: winit::dpi::PhysicalSize<u32>) {
333        if self.size == size {
334            return;
335        }
336        self.size = size;
337        self.size_changed = true;
338    }
339
340    /// Get the size of the surface
341    pub(crate) fn size(&self) -> winit::dpi::PhysicalSize<u32> {
342        self.size
343    }
344
345    pub(crate) fn resize_pass_targets_if_needed(&mut self) {
346        if self.size_changed {
347            self.pass_a.texture.destroy();
348            self.pass_b.texture.destroy();
349            self.compute_target_a.texture.destroy();
350            self.compute_target_b.texture.destroy();
351
352            self.pass_a = Self::create_pass_target(&self.gpu, &self.config, "A");
353            self.pass_b = Self::create_pass_target(&self.gpu, &self.config, "B");
354            self.compute_target_a = Self::create_compute_pass_target(
355                &self.gpu,
356                &self.config,
357                TextureFormat::Rgba8Unorm,
358                "Compute A",
359            );
360            self.compute_target_b = Self::create_compute_pass_target(
361                &self.gpu,
362                &self.config,
363                TextureFormat::Rgba8Unorm,
364                "Compute B",
365            );
366
367            if self.sample_count > 1 {
368                if let Some(t) = self.msaa_texture.take() {
369                    t.destroy();
370                }
371                let (msaa_texture, msaa_view) =
372                    Self::make_msaa_resources(&self.gpu, self.sample_count, &self.config);
373                self.msaa_texture = msaa_texture;
374                self.msaa_view = msaa_view;
375            }
376        }
377    }
378
379    /// Resize the surface if needed.
380    pub(crate) fn resize_if_needed(&mut self) -> bool {
381        let result = self.size_changed;
382        if self.size_changed {
383            self.config.width = self.size.width;
384            self.config.height = self.size.height;
385            self.resize_pass_targets_if_needed();
386            self.surface.configure(&self.gpu, &self.config);
387            self.size_changed = false;
388        }
389        result
390    }
391
392    // Helper does ping-pong copy and optional compute; returns an owned TextureView to avoid
393    // holding mutable borrows on pass targets across the caller scope.
394    fn handle_ping_pong_and_compute(
395        context: WgpuContext<'_>,
396        read_target: &mut PassTarget,
397        write_target: &mut PassTarget,
398        compute_resources: ComputeResources<'_>,
399    ) -> wgpu::TextureView {
400        // Swap read/write targets and copy previous pass into the new write target
401        std::mem::swap(read_target, write_target);
402        let texture_size = wgpu::Extent3d {
403            width: context.config.width,
404            height: context.config.height,
405            depth_or_array_layers: 1,
406        };
407        context.encoder.copy_texture_to_texture(
408            read_target.texture.as_image_copy(),
409            write_target.texture.as_image_copy(),
410            texture_size,
411        );
412        // Apply compute commands if any, reusing existing do_compute implementation
413        if !compute_resources.compute_commands.is_empty() {
414            let compute_commands_taken = std::mem::take(compute_resources.compute_commands);
415            Self::do_compute(DoComputeParams {
416                encoder: context.encoder,
417                commands: compute_commands_taken,
418                compute_pipeline_registry: compute_resources.compute_pipeline_registry,
419                gpu: context.gpu,
420                queue: context.queue,
421                config: context.config,
422                resource_manager: compute_resources.resource_manager,
423                scene_view: &read_target.view,
424                target_a: compute_resources.compute_target_a,
425                target_b: compute_resources.compute_target_b,
426            })
427        } else {
428            // Return an owned clone so caller does not keep a borrow on read_target
429            read_target.view.clone()
430        }
431    }
432
433    /// Render the surface using the unified command system.
434    ///
435    /// This method processes a stream of commands (both draw and compute) and renders
436    /// them to the surface using a multi-pass rendering approach with ping-pong buffers.
437    /// Commands that require barriers will trigger texture copies between passes.
438    ///
439    /// # Arguments
440    /// * `commands` - An iterable of (Command, PxSize, PxPosition) tuples representing
441    ///   the rendering operations to perform.
442    ///
443    /// # Returns
444    /// * `Ok(())` if rendering succeeds
445    /// * `Err(wgpu::SurfaceError)` if there are issues with the surface
446    pub(crate) fn render(
447        &mut self,
448        commands: impl IntoIterator<Item = (Command, TypeId, PxSize, PxPosition)>,
449    ) -> Result<(), wgpu::SurfaceError> {
450        // Collect commands into a Vec to allow reordering
451        let commands: Vec<_> = commands.into_iter().collect();
452        // Reorder instructions based on dependencies for better batching optimization
453        let commands = super::reorder::reorder_instructions(commands);
454
455        let output_frame = self.surface.get_current_texture()?;
456        let mut encoder = self
457            .gpu
458            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
459                label: Some("Render Encoder"),
460            });
461
462        let texture_size = wgpu::Extent3d {
463            width: self.config.width,
464            height: self.config.height,
465            depth_or_array_layers: 1,
466        };
467
468        // Initialization
469        let (read_target, write_target) = (&mut self.pass_a, &mut self.pass_b);
470
471        // Clear any existing compute commands
472        if !self.compute_commands.is_empty() {
473            // This is a warning to developers that not all compute commands were used in the last frame.
474            warn!("Not every compute command is used in last frame. This is likely a bug.");
475            self.compute_commands.clear();
476        }
477
478        // Flag for first pass
479        let mut is_first_pass = true;
480
481        // Frame-level begin for all pipelines
482        self.drawer
483            .pipeline_registry
484            .begin_all_frames(&self.gpu, &self.queue, &self.config);
485
486        // Main command processing loop with barrier handling
487        // Use an owned TextureView here to avoid holding mutable borrows across helper calls.
488        let mut scene_texture_view = read_target.view.clone();
489        let mut commands_in_pass: Vec<DrawOrClip> = Vec::new();
490        let mut barrier_draw_rects_in_pass: Vec<PxRect> = Vec::new();
491        let mut clip_stack: Vec<PxRect> = Vec::new();
492
493        for (command, command_type_id, size, start_pos) in commands {
494            let need_new_pass = commands_in_pass
495                .iter()
496                .rev()
497                .find_map(|command| match &command {
498                    DrawOrClip::Draw(cmd) => Some(cmd),
499                    DrawOrClip::Clip(_) => None,
500                })
501                .map(|cmd| {
502                    match (cmd.command.barrier(), command.barrier()) {
503                        (None, Some(_)) => true, // If the last command has no barrier but the next one does, we need a new pass
504                        (Some(_), Some(barrier)) => {
505                            // If both have barriers, we need to check if they are orthogonal
506                            // First extract the last barrier's draw rect
507                            let last_draw_rect =
508                                extract_draw_rect(Some(barrier), size, start_pos, texture_size);
509                            // Then check if the last draw rect is orthogonal to all existing draw rects in this pass
510                            !barrier_draw_rects_in_pass
511                                .iter()
512                                .all(|dr| dr.is_orthogonal(&last_draw_rect)) // We don't need a new pass if the last command's barrier is orthogonal to all existing draw rects
513                        }
514                        (Some(_), None) => false, // If the last command has a barrier but the next one does not, we can continue in the same pass
515                        (None, None) => false, // If both have no barriers, we can continue in the same pass
516                    }
517                })
518                .unwrap_or(false);
519
520            if need_new_pass {
521                // A ping-pong operation is needed if the first command in the pass has a barrier
522                if commands_in_pass
523                    .iter()
524                    .find_map(|command| match &command {
525                        DrawOrClip::Draw(cmd) => Some(cmd),
526                        DrawOrClip::Clip(_) => None,
527                    })
528                    .map(|cmd| cmd.command.barrier().is_some())
529                    .unwrap_or(false)
530                {
531                    // Perform a ping-pong operation (extracted helper)
532                    let final_view_after_compute = Self::handle_ping_pong_and_compute(
533                        WgpuContext {
534                            encoder: &mut encoder,
535                            gpu: &self.gpu,
536                            queue: &self.queue,
537                            config: &self.config,
538                        },
539                        read_target,
540                        write_target,
541                        ComputeResources {
542                            compute_commands: &mut self.compute_commands,
543                            compute_pipeline_registry: &mut self.compute_pipeline_registry,
544                            resource_manager: &mut self.resource_manager.write(),
545                            compute_target_a: &self.compute_target_a,
546                            compute_target_b: &self.compute_target_b,
547                        },
548                    );
549                    scene_texture_view = final_view_after_compute;
550                }
551
552                // Render the current pass before starting a new one
553                render_current_pass(RenderCurrentPassParams {
554                    msaa_view: &self.msaa_view,
555                    is_first_pass: &mut is_first_pass,
556                    encoder: &mut encoder,
557                    write_target,
558                    commands_in_pass: &mut commands_in_pass,
559                    scene_texture_view: &scene_texture_view,
560                    drawer: &mut self.drawer,
561                    gpu: &self.gpu,
562                    queue: &self.queue,
563                    config: &self.config,
564                    clip_stack: &mut clip_stack,
565                });
566                commands_in_pass.clear();
567                barrier_draw_rects_in_pass.clear();
568            }
569
570            match command {
571                Command::Draw(cmd) => {
572                    // Extract the draw rectangle based on the command's barrier, size and position
573                    let draw_rect = extract_draw_rect(cmd.barrier(), size, start_pos, texture_size);
574                    // If the command has a barrier, we need to track the draw rect for orthogonality checks
575                    if cmd.barrier().is_some() {
576                        barrier_draw_rects_in_pass.push(draw_rect);
577                    }
578                    // Add the command to the current pass
579                    commands_in_pass.push(DrawOrClip::Draw(DrawCommandWithMetadata {
580                        command: cmd,
581                        type_id: command_type_id,
582                        size,
583                        start_pos,
584                        draw_rect,
585                    }));
586                }
587                Command::Compute(cmd) => {
588                    // Add the compute command to the current pass
589                    self.compute_commands.push((cmd, size, start_pos));
590                }
591                Command::ClipPush(rect) => {
592                    // Push it into command stack
593                    commands_in_pass.push(DrawOrClip::Clip(ClipOps::Push(rect)));
594                }
595                Command::ClipPop => {
596                    // Push it into command stack
597                    commands_in_pass.push(DrawOrClip::Clip(ClipOps::Pop));
598                }
599            }
600        }
601
602        // After processing all commands, we need to render the last pass if there are any commands left
603        if !commands_in_pass.is_empty() {
604            // A ping-pong operation is needed if the first command in the pass has a barrier
605            if commands_in_pass
606                .iter()
607                .find_map(|command| match &command {
608                    DrawOrClip::Draw(cmd) => Some(cmd),
609                    DrawOrClip::Clip(_) => None,
610                })
611                .map(|cmd| cmd.command.barrier().is_some())
612                .unwrap_or(false)
613            {
614                // Perform a ping-pong operation (extracted helper)
615                let final_view_after_compute = Self::handle_ping_pong_and_compute(
616                    WgpuContext {
617                        encoder: &mut encoder,
618                        gpu: &self.gpu,
619                        queue: &self.queue,
620                        config: &self.config,
621                    },
622                    read_target,
623                    write_target,
624                    ComputeResources {
625                        compute_commands: &mut self.compute_commands,
626                        compute_pipeline_registry: &mut self.compute_pipeline_registry,
627                        resource_manager: &mut self.resource_manager.write(),
628                        compute_target_a: &self.compute_target_a,
629                        compute_target_b: &self.compute_target_b,
630                    },
631                );
632                scene_texture_view = final_view_after_compute;
633            }
634
635            // Render the current pass before starting a new one
636            render_current_pass(RenderCurrentPassParams {
637                msaa_view: &self.msaa_view,
638                is_first_pass: &mut is_first_pass,
639                encoder: &mut encoder,
640                write_target,
641                commands_in_pass: &mut commands_in_pass,
642                scene_texture_view: &scene_texture_view,
643                drawer: &mut self.drawer,
644                gpu: &self.gpu,
645                queue: &self.queue,
646                config: &self.config,
647                clip_stack: &mut clip_stack,
648            });
649            commands_in_pass.clear();
650            barrier_draw_rects_in_pass.clear();
651        }
652
653        // Frame-level end for all pipelines
654        self.drawer
655            .pipeline_registry
656            .end_all_frames(&self.gpu, &self.queue, &self.config);
657
658        // Final copy to surface
659        encoder.copy_texture_to_texture(
660            write_target.texture.as_image_copy(),
661            output_frame.texture.as_image_copy(),
662            texture_size,
663        );
664
665        self.queue.submit(Some(encoder.finish()));
666        output_frame.present();
667
668        Ok(())
669    }
670
671    pub(crate) fn render_dummy(&mut self) -> Result<(), wgpu::SurfaceError> {
672        let output_frame = self.surface.get_current_texture()?;
673        let mut encoder = self
674            .gpu
675            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
676                label: Some("Render Encoder(Dummy)"),
677            });
678
679        encoder.copy_texture_to_texture(
680            self.pass_b.texture.as_image_copy(),
681            output_frame.texture.as_image_copy(),
682            wgpu::Extent3d {
683                width: self.config.width,
684                height: self.config.height,
685                depth_or_array_layers: 1,
686            },
687        );
688
689        self.queue.submit(Some(encoder.finish()));
690        output_frame.present();
691        Ok(())
692    }
693
694    fn do_compute(params: DoComputeParams<'_>) -> wgpu::TextureView {
695        if params.commands.is_empty() {
696            return params.scene_view.clone();
697        }
698
699        let mut read_view = params.scene_view.clone();
700        let (mut write_target, mut read_target) = (params.target_a, params.target_b);
701
702        for (command, size, start_pos) in params.commands {
703            // Ensure the write target is cleared before use
704            params.encoder.clear_texture(
705                &write_target.texture,
706                &ImageSubresourceRange {
707                    aspect: wgpu::TextureAspect::All,
708                    base_mip_level: 0,
709                    mip_level_count: None,
710                    base_array_layer: 0,
711                    array_layer_count: None,
712                },
713            );
714
715            // Create and dispatch the compute pass
716            {
717                let mut cpass = params
718                    .encoder
719                    .begin_compute_pass(&wgpu::ComputePassDescriptor {
720                        label: Some("Compute Pass"),
721                        timestamp_writes: None,
722                    });
723
724                // Get the area of the compute command (reuse extract_draw_rect to avoid duplication)
725                let texture_size = wgpu::Extent3d {
726                    width: params.config.width,
727                    height: params.config.height,
728                    depth_or_array_layers: 1,
729                };
730                let area =
731                    extract_draw_rect(Some(command.barrier()), size, start_pos, texture_size);
732
733                params.compute_pipeline_registry.dispatch_erased(
734                    params.gpu,
735                    params.queue,
736                    params.config,
737                    &mut cpass,
738                    &*command,
739                    params.resource_manager,
740                    area,
741                    &read_view,
742                    &write_target.view,
743                );
744            } // cpass is dropped here, ending the pass
745
746            // The result of this pass is now in write_target.
747            // For the next iteration, this will be our read source.
748            read_view = write_target.view.clone();
749            // Swap targets for the next iteration
750            std::mem::swap(&mut write_target, &mut read_target);
751        }
752
753        // After the loop, the final result is in the `read_view`,
754        // because we swapped one last time at the end of the loop.
755        read_view
756    }
757}
758
759fn compute_padded_rect(
760    size: PxSize,
761    start_pos: PxPosition,
762    top: Px,
763    right: Px,
764    bottom: Px,
765    left: Px,
766    texture_size: wgpu::Extent3d,
767) -> PxRect {
768    let padded_x = (start_pos.x - left).max(Px(0));
769    let padded_y = (start_pos.y - top).max(Px(0));
770    let padded_width = (size.width + left + right).min(Px(texture_size.width as i32 - padded_x.0));
771    let padded_height =
772        (size.height + top + bottom).min(Px(texture_size.height as i32 - padded_y.0));
773    PxRect {
774        x: padded_x,
775        y: padded_y,
776        width: padded_width,
777        height: padded_height,
778    }
779}
780
781fn clamp_rect_to_texture(mut rect: PxRect, texture_size: wgpu::Extent3d) -> PxRect {
782    rect.x = rect.x.positive().min(texture_size.width).into();
783    rect.y = rect.y.positive().min(texture_size.height).into();
784    rect.width = rect
785        .width
786        .positive()
787        .min(texture_size.width - rect.x.positive())
788        .into();
789    rect.height = rect
790        .height
791        .positive()
792        .min(texture_size.height - rect.y.positive())
793        .into();
794    rect
795}
796
797fn extract_draw_rect(
798    barrier: Option<BarrierRequirement>,
799    size: PxSize,
800    start_pos: PxPosition,
801    texture_size: wgpu::Extent3d,
802) -> PxRect {
803    match barrier {
804        Some(BarrierRequirement::Global) => PxRect {
805            x: Px(0),
806            y: Px(0),
807            width: Px(texture_size.width as i32),
808            height: Px(texture_size.height as i32),
809        },
810        Some(BarrierRequirement::PaddedLocal {
811            top,
812            right,
813            bottom,
814            left,
815        }) => compute_padded_rect(size, start_pos, top, right, bottom, left, texture_size),
816        Some(BarrierRequirement::Absolute(rect)) => clamp_rect_to_texture(rect, texture_size),
817        None => {
818            let x = start_pos.x.positive().min(texture_size.width);
819            let y = start_pos.y.positive().min(texture_size.height);
820            let width = size.width.positive().min(texture_size.width - x);
821            let height = size.height.positive().min(texture_size.height - y);
822            PxRect {
823                x: Px::from(x),
824                y: Px::from(y),
825                width: Px::from(width),
826                height: Px::from(height),
827            }
828        }
829    }
830}
831
832fn render_current_pass(params: RenderCurrentPassParams<'_>) {
833    let (view, resolve_target) = if let Some(msaa_view) = params.msaa_view {
834        (msaa_view, Some(&params.write_target.view))
835    } else {
836        (&params.write_target.view, None)
837    };
838
839    let load_ops = if *params.is_first_pass {
840        *params.is_first_pass = false;
841        wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)
842    } else {
843        wgpu::LoadOp::Load
844    };
845
846    let mut rpass = params
847        .encoder
848        .begin_render_pass(&wgpu::RenderPassDescriptor {
849            label: Some("Render Pass"),
850            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
851                view,
852                depth_slice: None,
853                resolve_target,
854                ops: wgpu::Operations {
855                    load: load_ops,
856                    store: wgpu::StoreOp::Store,
857                },
858            })],
859            ..Default::default()
860        });
861
862    params.drawer.begin_pass(
863        params.gpu,
864        params.queue,
865        params.config,
866        &mut rpass,
867        params.scene_texture_view,
868    );
869
870    // Prepare buffered submission state
871    let mut buffer: Vec<(Box<dyn DrawCommand>, PxSize, PxPosition)> = Vec::new();
872    let mut last_command_type_id = None;
873    let mut current_batch_draw_rect: Option<PxRect> = None;
874    for cmd in mem::take(params.commands_in_pass).into_iter() {
875        let cmd = match cmd {
876            DrawOrClip::Clip(clip_ops) => {
877                // Must flush any existing buffered commands before changing clip state
878                if !buffer.is_empty() {
879                    submit_buffered_commands(
880                        &mut rpass,
881                        params.drawer,
882                        params.gpu,
883                        params.queue,
884                        params.config,
885                        &mut buffer,
886                        params.scene_texture_view,
887                        params.clip_stack,
888                        &mut current_batch_draw_rect,
889                    );
890                    last_command_type_id = None; // Reset batch type after flush
891                }
892                // Update clip stack
893                match clip_ops {
894                    ClipOps::Push(rect) => {
895                        params.clip_stack.push(rect);
896                    }
897                    ClipOps::Pop => {
898                        params.clip_stack.pop();
899                    }
900                }
901                // continue to next command
902                continue;
903            }
904            DrawOrClip::Draw(cmd) => cmd, // Proceed with draw commands
905        };
906
907        // If the incoming command cannot be merged into the current batch, flush first.
908        if !can_merge_into_batch(&last_command_type_id, cmd.type_id) && !buffer.is_empty() {
909            submit_buffered_commands(
910                &mut rpass,
911                params.drawer,
912                params.gpu,
913                params.queue,
914                params.config,
915                &mut buffer,
916                params.scene_texture_view,
917                params.clip_stack,
918                &mut current_batch_draw_rect,
919            );
920        }
921
922        // Add the command to the buffer and update the current batch rect (extracted merge helper).
923        buffer.push((cmd.command, cmd.size, cmd.start_pos));
924        last_command_type_id = Some(cmd.type_id);
925        current_batch_draw_rect = Some(merge_batch_rect(current_batch_draw_rect, cmd.draw_rect));
926    }
927
928    // If there are any remaining commands in the buffer, submit them
929    if !buffer.is_empty() {
930        submit_buffered_commands(
931            &mut rpass,
932            params.drawer,
933            params.gpu,
934            params.queue,
935            params.config,
936            &mut buffer,
937            params.scene_texture_view,
938            params.clip_stack,
939            &mut current_batch_draw_rect,
940        );
941    }
942
943    params.drawer.end_pass(
944        params.gpu,
945        params.queue,
946        params.config,
947        &mut rpass,
948        params.scene_texture_view,
949    );
950}
951
952fn submit_buffered_commands(
953    rpass: &mut wgpu::RenderPass<'_>,
954    drawer: &mut Drawer,
955    gpu: &wgpu::Device,
956    queue: &wgpu::Queue,
957    config: &wgpu::SurfaceConfiguration,
958    buffer: &mut Vec<(Box<dyn DrawCommand>, PxSize, PxPosition)>,
959    scene_texture_view: &wgpu::TextureView,
960    clip_stack: &mut [PxRect],
961    current_batch_draw_rect: &mut Option<PxRect>,
962) {
963    // Take the buffered commands and convert to the transient representation expected by drawer.submit
964    let commands = mem::take(buffer);
965    let commands = commands
966        .iter()
967        .map(|(cmd, sz, pos)| (&**cmd, *sz, *pos))
968        .collect::<Vec<_>>();
969
970    // Apply clipping to the current batch rectangle; if nothing remains, abort early.
971    let (current_clip_rect, anything_to_submit) =
972        apply_clip_to_batch_rect(clip_stack, current_batch_draw_rect);
973    if !anything_to_submit {
974        return;
975    }
976
977    let rect = current_batch_draw_rect.unwrap();
978    set_scissor_rect_from_pxrect(rpass, rect);
979
980    drawer.submit(
981        gpu,
982        queue,
983        config,
984        rpass,
985        &commands,
986        scene_texture_view,
987        current_clip_rect,
988    );
989    *current_batch_draw_rect = None;
990}
991
992fn set_scissor_rect_from_pxrect(rpass: &mut wgpu::RenderPass<'_>, rect: PxRect) {
993    rpass.set_scissor_rect(
994        rect.x.positive(),
995        rect.y.positive(),
996        rect.width.positive(),
997        rect.height.positive(),
998    );
999}
1000
1001/// Apply clip_stack to current_batch_draw_rect. Returns false if intersection yields nothing
1002/// (meaning there is nothing to submit), true otherwise.
1003///
1004/// Also returns the current clipping rectangle (if any) for potential use by the caller.
1005fn apply_clip_to_batch_rect(
1006    clip_stack: &[PxRect],
1007    current_batch_draw_rect: &mut Option<PxRect>,
1008) -> (Option<PxRect>, bool) {
1009    if let Some(clipped_rect) = clip_stack.last() {
1010        let Some(current_rect) = current_batch_draw_rect.as_ref() else {
1011            return (Some(*clipped_rect), false);
1012        };
1013        if let Some(final_rect) = current_rect.intersection(clipped_rect) {
1014            *current_batch_draw_rect = Some(final_rect);
1015            return (Some(*clipped_rect), true);
1016        } else {
1017            return (Some(*clipped_rect), false);
1018        }
1019    }
1020    (None, true)
1021}
1022
1023/// Determine whether `next_type_id` (with potential clipping) can be merged into the current batch.
1024/// Equivalent to the negation of the original flush condition:
1025/// merge allowed when last_command_type_id == Some(next_type_id) or last_command_type_id is None.
1026fn can_merge_into_batch(last_command_type_id: &Option<TypeId>, next_type_id: TypeId) -> bool {
1027    match last_command_type_id {
1028        Some(l) => *l == next_type_id,
1029        None => false,
1030    }
1031}
1032
1033/// Merge the existing optional batch rect with a new command rect.
1034fn merge_batch_rect(current: Option<PxRect>, next: PxRect) -> PxRect {
1035    current.map(|dr| dr.union(&next)).unwrap_or(next)
1036}
1037
1038struct DrawCommandWithMetadata {
1039    command: Box<dyn DrawCommand>,
1040    type_id: TypeId,
1041    size: PxSize,
1042    start_pos: PxPosition,
1043    draw_rect: PxRect,
1044}
1045
1046enum DrawOrClip {
1047    Draw(DrawCommandWithMetadata),
1048    Clip(ClipOps),
1049}
1050
1051enum ClipOps {
1052    Push(PxRect),
1053    Pop,
1054}