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