tessera_ui/renderer/
app.rs

1use std::{mem, sync::Arc};
2
3use log::{error, info, warn};
4use parking_lot::RwLock;
5use wgpu::TextureFormat;
6use winit::window::Window;
7
8use crate::{
9    ComputeCommand, PxPosition, compute::resource::ComputeResourceManager, dp::SCALE_FACTOR,
10    px::PxSize, renderer::command::Command,
11};
12
13use super::{compute::ComputePipelineRegistry, drawer::Drawer};
14
15// Render pass resources for ping-pong operation
16struct PassTarget {
17    texture: wgpu::Texture,
18    view: wgpu::TextureView,
19}
20
21pub struct WgpuApp {
22    /// Avoiding release the window
23    #[allow(unused)]
24    pub window: Arc<Window>,
25    /// WGPU device
26    pub gpu: wgpu::Device,
27    /// WGPU surface
28    surface: wgpu::Surface<'static>,
29    /// WGPU queue
30    pub queue: wgpu::Queue,
31    /// WGPU surface configuration
32    pub config: wgpu::SurfaceConfiguration,
33    /// size of the window
34    size: winit::dpi::PhysicalSize<u32>,
35    /// if size is changed
36    size_changed: bool,
37    /// draw pipelines
38    pub drawer: Drawer,
39    /// compute pipelines
40    pub compute_pipeline_registry: ComputePipelineRegistry,
41
42    // --- New ping-pong rendering resources ---
43    pass_a: PassTarget,
44    pass_b: PassTarget,
45
46    // --- MSAA resources ---
47    pub sample_count: u32,
48    msaa_texture: Option<wgpu::Texture>,
49    msaa_view: Option<wgpu::TextureView>,
50
51    // --- Compute resources ---
52    compute_target_a: PassTarget,
53    compute_target_b: PassTarget,
54    compute_commands: Vec<Box<dyn ComputeCommand>>,
55    pub resource_manager: Arc<RwLock<ComputeResourceManager>>,
56}
57
58impl WgpuApp {
59    /// Create a new WGPU app, as the root of Tessera
60    pub(crate) async fn new(window: Arc<Window>, sample_count: u32) -> Self {
61        // Looking for gpus
62        let instance: wgpu::Instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
63            backends: wgpu::Backends::all(),
64            ..Default::default()
65        });
66        // Create a surface
67        let surface = match instance.create_surface(window.clone()) {
68            Ok(surface) => surface,
69            Err(e) => {
70                error!("Failed to create surface: {e:?}");
71                panic!("Failed to create surface: {e:?}");
72            }
73        };
74        // Looking for adapter gpu
75        let adapter = match instance
76            .request_adapter(&wgpu::RequestAdapterOptions {
77                power_preference: wgpu::PowerPreference::default(),
78                compatible_surface: Some(&surface),
79                force_fallback_adapter: false,
80            })
81            .await
82        {
83            Ok(gpu) => gpu,
84            Err(e) => {
85                error!("Failed to find an appropriate adapter: {e:?}");
86                panic!("Failed to find an appropriate adapter: {e:?}");
87            }
88        };
89        // Create a device and queue
90        let (gpu, queue) = match adapter
91            .request_device(&wgpu::DeviceDescriptor {
92                required_features: wgpu::Features::empty(),
93                // WebGL backend does not support all features
94                required_limits: if cfg!(target_arch = "wasm32") {
95                    wgpu::Limits::downlevel_webgl2_defaults()
96                } else {
97                    wgpu::Limits::default()
98                },
99                label: None,
100                memory_hints: wgpu::MemoryHints::Performance,
101                trace: wgpu::Trace::Off,
102            })
103            .await
104        {
105            Ok((gpu, queue)) => (gpu, queue),
106            Err(e) => {
107                error!("Failed to create device: {e:?}");
108                panic!("Failed to create device: {e:?}");
109            }
110        };
111        // Create surface configuration
112        let size = window.inner_size();
113        let caps = surface.get_capabilities(&adapter);
114        // Choose the present mode
115        let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) {
116            // Fifo is the fallback, it is the most compatible and stable
117            wgpu::PresentMode::Fifo
118        } else {
119            // Immediate is the least preferred, it can cause tearing and is not recommended
120            wgpu::PresentMode::Immediate
121        };
122        info!("Using present mode: {present_mode:?}");
123        let config = wgpu::SurfaceConfiguration {
124            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
125            format: caps.formats[0],
126            width: size.width,
127            height: size.height,
128            present_mode,
129            alpha_mode: caps.alpha_modes[0],
130            view_formats: vec![],
131            desired_maximum_frame_latency: 2,
132        };
133        surface.configure(&gpu, &config);
134
135        // --- Create MSAA Target ---
136        let (msaa_texture, msaa_view) = if sample_count > 1 {
137            let texture = gpu.create_texture(&wgpu::TextureDescriptor {
138                label: Some("MSAA Framebuffer"),
139                size: wgpu::Extent3d {
140                    width: config.width,
141                    height: config.height,
142                    depth_or_array_layers: 1,
143                },
144                mip_level_count: 1,
145                sample_count,
146                dimension: wgpu::TextureDimension::D2,
147                // Use surface format to match pass targets
148                format: config.format,
149                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
150                view_formats: &[],
151            });
152            let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
153            (Some(texture), Some(view))
154        } else {
155            (None, None)
156        };
157
158        // --- Create Pass Targets (A and B and Compute) ---
159        let pass_a = Self::create_pass_target(&gpu, &config, "A");
160        let pass_b = Self::create_pass_target(&gpu, &config, "B");
161        let compute_target_a =
162            Self::create_compute_pass_target(&gpu, &config, TextureFormat::Rgba8Unorm, "Compute A");
163        let compute_target_b =
164            Self::create_compute_pass_target(&gpu, &config, TextureFormat::Rgba8Unorm, "Compute B");
165
166        let drawer = Drawer::new();
167
168        // Set scale factor for dp conversion
169        let scale_factor = window.scale_factor();
170        info!("Window scale factor: {scale_factor}");
171        SCALE_FACTOR
172            .set(RwLock::new(scale_factor))
173            .expect("Failed to set scale factor");
174
175        Self {
176            window,
177            gpu,
178            surface,
179            queue,
180            config,
181            size,
182            size_changed: false,
183            drawer,
184            pass_a,
185            pass_b,
186            compute_pipeline_registry: ComputePipelineRegistry::new(),
187            sample_count,
188            msaa_texture,
189            msaa_view,
190            compute_target_a,
191            compute_target_b,
192            compute_commands: Vec::new(),
193            resource_manager: Arc::new(RwLock::new(ComputeResourceManager::new())),
194        }
195    }
196
197    fn create_pass_target(
198        gpu: &wgpu::Device,
199        config: &wgpu::SurfaceConfiguration,
200        label_suffix: &str,
201    ) -> PassTarget {
202        let label = format!("Pass {label_suffix} Texture");
203        let texture_descriptor = wgpu::TextureDescriptor {
204            label: Some(&label),
205            size: wgpu::Extent3d {
206                width: config.width,
207                height: config.height,
208                depth_or_array_layers: 1,
209            },
210            mip_level_count: 1,
211            sample_count: 1,
212            dimension: wgpu::TextureDimension::D2,
213            // Use surface format for compatibility with final copy operations
214            format: config.format,
215            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
216                | wgpu::TextureUsages::TEXTURE_BINDING
217                | wgpu::TextureUsages::COPY_DST
218                | wgpu::TextureUsages::COPY_SRC,
219            view_formats: &[],
220        };
221        let texture = gpu.create_texture(&texture_descriptor);
222        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
223        PassTarget { texture, view }
224    }
225
226    fn create_compute_pass_target(
227        gpu: &wgpu::Device,
228        config: &wgpu::SurfaceConfiguration,
229        format: TextureFormat,
230        label_suffix: &str,
231    ) -> PassTarget {
232        let label = format!("Compute {label_suffix} Texture");
233        let texture_descriptor = wgpu::TextureDescriptor {
234            label: Some(&label),
235            size: wgpu::Extent3d {
236                width: config.width,
237                height: config.height,
238                depth_or_array_layers: 1,
239            },
240            mip_level_count: 1,
241            sample_count: 1,
242            dimension: wgpu::TextureDimension::D2,
243            format,
244            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
245                | wgpu::TextureUsages::TEXTURE_BINDING
246                | wgpu::TextureUsages::STORAGE_BINDING
247                | wgpu::TextureUsages::COPY_DST
248                | wgpu::TextureUsages::COPY_SRC,
249            view_formats: &[],
250        };
251        let texture = gpu.create_texture(&texture_descriptor);
252        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
253        PassTarget { texture, view }
254    }
255
256    pub fn register_pipelines(&mut self, register_fn: impl FnOnce(&mut Self)) {
257        register_fn(self);
258    }
259
260    /// Resize the surface
261    /// Real resize will be done in the next frame, in [Self::resize_if_needed]
262    pub(crate) fn resize(&mut self, size: winit::dpi::PhysicalSize<u32>) {
263        if self.size == size {
264            return;
265        }
266        self.size = size;
267        self.size_changed = true;
268    }
269
270    /// Get the size of the surface
271    pub(crate) fn size(&self) -> winit::dpi::PhysicalSize<u32> {
272        self.size
273    }
274
275    pub(crate) fn resize_pass_targets_if_needed(&mut self) {
276        if self.size_changed {
277            self.pass_a.texture.destroy();
278            self.pass_b.texture.destroy();
279            self.compute_target_a.texture.destroy();
280            self.compute_target_b.texture.destroy();
281
282            self.pass_a = Self::create_pass_target(&self.gpu, &self.config, "A");
283            self.pass_b = Self::create_pass_target(&self.gpu, &self.config, "B");
284            self.compute_target_a = Self::create_compute_pass_target(
285                &self.gpu,
286                &self.config,
287                TextureFormat::Rgba8Unorm,
288                "Compute A",
289            );
290            self.compute_target_b = Self::create_compute_pass_target(
291                &self.gpu,
292                &self.config,
293                TextureFormat::Rgba8Unorm,
294                "Compute B",
295            );
296
297            if self.sample_count > 1 {
298                if let Some(t) = self.msaa_texture.take() {
299                    t.destroy()
300                }
301                let texture = self.gpu.create_texture(&wgpu::TextureDescriptor {
302                    label: Some("MSAA Framebuffer"),
303                    size: wgpu::Extent3d {
304                        width: self.config.width,
305                        height: self.config.height,
306                        depth_or_array_layers: 1,
307                    },
308                    mip_level_count: 1,
309                    sample_count: self.sample_count,
310                    dimension: wgpu::TextureDimension::D2,
311                    // Use surface format to match pass targets
312                    format: self.config.format,
313                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
314                    view_formats: &[],
315                });
316                let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
317                self.msaa_texture = Some(texture);
318                self.msaa_view = Some(view);
319            }
320        }
321    }
322
323    /// Resize the surface if needed.
324    pub(crate) fn resize_if_needed(&mut self) {
325        if self.size_changed {
326            self.config.width = self.size.width;
327            self.config.height = self.size.height;
328            self.resize_pass_targets_if_needed();
329            self.surface.configure(&self.gpu, &self.config);
330            self.size_changed = false;
331        }
332    }
333
334    /// Render the surface using the unified command system.
335    ///
336    /// This method processes a stream of commands (both draw and compute) and renders
337    /// them to the surface using a multi-pass rendering approach with ping-pong buffers.
338    /// Commands that require barriers will trigger texture copies between passes.
339    ///
340    /// # Arguments
341    /// * `commands` - An iterable of (Command, PxSize, PxPosition) tuples representing
342    ///   the rendering operations to perform.
343    ///
344    /// # Returns
345    /// * `Ok(())` if rendering succeeds
346    /// * `Err(wgpu::SurfaceError)` if there are issues with the surface
347    pub(crate) fn render(
348        &mut self,
349        commands: impl IntoIterator<Item = (Command, PxSize, PxPosition)>,
350    ) -> Result<(), wgpu::SurfaceError> {
351        let output_frame = self.surface.get_current_texture()?;
352        let mut encoder = self
353            .gpu
354            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
355                label: Some("Render Encoder"),
356            });
357
358        let texture_size = wgpu::Extent3d {
359            width: self.config.width,
360            height: self.config.height,
361            depth_or_array_layers: 1,
362        };
363
364        // Initialization
365        let (mut read_target, mut write_target) = (&mut self.pass_a, &mut self.pass_b);
366
367        // Clear any existing compute commands
368        if !self.compute_commands.is_empty() {
369            // This is a warning to developers that not all compute commands were used in the last frame.
370            warn!("Not every compute command is used in last frame. This is likely a bug.");
371            self.compute_commands.clear();
372        }
373
374        // Initial clear pass
375        {
376            let (view, resolve_target) = if let Some(msaa_view) = &self.msaa_view {
377                (msaa_view, Some(&write_target.view))
378            } else {
379                (&write_target.view, None)
380            };
381            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
382                label: Some("Initial Clear Pass"),
383                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
384                    view,
385                    depth_slice: None,
386                    resolve_target,
387                    ops: wgpu::Operations {
388                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
389                        store: wgpu::StoreOp::Store,
390                    },
391                })],
392                ..Default::default()
393            });
394            self.drawer
395                .begin_pass(&self.gpu, &self.queue, &self.config, &mut rpass);
396            self.drawer
397                .end_pass(&self.gpu, &self.queue, &self.config, &mut rpass);
398        }
399
400        // Frame-level begin for all pipelines
401        self.drawer
402            .pipeline_registry
403            .begin_all_frames(&self.gpu, &self.queue, &self.config);
404
405        // Main command processing loop with barrier handling
406        let mut commands_iter = commands.into_iter().peekable();
407        let mut scene_texture_view = &read_target.view;
408        while let Some((command, size, start_pos)) = commands_iter.next() {
409            // Handle barrier requirements by swapping buffers and copying content
410            if command.barrier().is_some() {
411                // Perform a ping-pong operation
412                std::mem::swap(&mut read_target, &mut write_target);
413                encoder.copy_texture_to_texture(
414                    read_target.texture.as_image_copy(),
415                    write_target.texture.as_image_copy(),
416                    texture_size,
417                );
418                // --- Apply compute effect ---
419                let final_view_after_compute = if !self.compute_commands.is_empty() {
420                    let compute_commands = mem::take(&mut self.compute_commands);
421                    Self::do_compute(
422                        &mut encoder,
423                        compute_commands,
424                        &mut self.compute_pipeline_registry,
425                        &self.gpu,
426                        &self.queue,
427                        &self.config,
428                        &mut self.resource_manager.write(),
429                        &read_target.view,
430                        &self.compute_target_a,
431                        &self.compute_target_b,
432                    )
433                } else {
434                    &read_target.view
435                };
436                scene_texture_view = final_view_after_compute;
437            }
438
439            match command {
440                // Process draw commands using the graphics pipeline
441                Command::Draw(command) => {
442                    let (view, resolve_target) = if let Some(msaa_view) = &self.msaa_view {
443                        (msaa_view, Some(&write_target.view))
444                    } else {
445                        (&write_target.view, None)
446                    };
447                    let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
448                        label: Some("Render Pass"),
449                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
450                            view,
451                            depth_slice: None,
452                            resolve_target,
453                            ops: wgpu::Operations {
454                                load: wgpu::LoadOp::Load,
455                                store: wgpu::StoreOp::Store,
456                            },
457                        })],
458                        ..Default::default()
459                    });
460                    self.drawer
461                        .begin_pass(&self.gpu, &self.queue, &self.config, &mut rpass);
462
463                    // Submit the first command
464                    self.drawer.submit(
465                        &self.gpu,
466                        &self.queue,
467                        &self.config,
468                        &mut rpass,
469                        &*command,
470                        size,
471                        start_pos,
472                        scene_texture_view,
473                    );
474
475                    // Batch subsequent draw commands that don't require barriers
476                    while let Some((Command::Draw(command), _, _)) = commands_iter.peek() {
477                        if command.barrier().is_some() {
478                            break; // Break if a barrier is required
479                        }
480                        if let Some((Command::Draw(command), size, start_pos)) =
481                            commands_iter.next()
482                        {
483                            self.drawer.submit(
484                                &self.gpu,
485                                &self.queue,
486                                &self.config,
487                                &mut rpass,
488                                &*command,
489                                size,
490                                start_pos,
491                                scene_texture_view,
492                            );
493                        }
494                    }
495                    self.drawer
496                        .end_pass(&self.gpu, &self.queue, &self.config, &mut rpass);
497                }
498                // Process compute commands using the compute pipeline
499                Command::Compute(command) => {
500                    self.compute_commands.push(command);
501                    // batch subsequent compute commands
502                    while let Some((Command::Compute(_), _, _)) = commands_iter.peek() {
503                        if let Some((Command::Compute(command), _, _)) = commands_iter.next() {
504                            self.compute_commands.push(command);
505                        }
506                    }
507                }
508            }
509        }
510
511        // Frame-level end for all pipelines
512        self.drawer
513            .pipeline_registry
514            .end_all_frames(&self.gpu, &self.queue, &self.config);
515
516        // Final copy to surface
517        encoder.copy_texture_to_texture(
518            write_target.texture.as_image_copy(),
519            output_frame.texture.as_image_copy(),
520            texture_size,
521        );
522
523        self.queue.submit(Some(encoder.finish()));
524        output_frame.present();
525
526        Ok(())
527    }
528
529    fn do_compute<'a>(
530        encoder: &mut wgpu::CommandEncoder,
531        commands: Vec<Box<dyn ComputeCommand>>,
532        compute_pipeline_registry: &mut ComputePipelineRegistry,
533        gpu: &wgpu::Device,
534        queue: &wgpu::Queue,
535        config: &wgpu::SurfaceConfiguration,
536        resource_manager: &mut ComputeResourceManager,
537        // The initial scene content
538        scene_view: &'a wgpu::TextureView,
539        // Ping-pong targets
540        target_a: &'a PassTarget,
541        target_b: &'a PassTarget,
542    ) -> &'a wgpu::TextureView {
543        if commands.is_empty() {
544            return scene_view;
545        }
546
547        let mut read_view = scene_view;
548        let (mut write_target, mut read_target) = (target_a, target_b);
549
550        for command in commands {
551            // Ensure the write target is cleared before use
552            let rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
553                label: Some("Compute Target Clear"),
554                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
555                    view: &write_target.view,
556                    resolve_target: None,
557                    ops: wgpu::Operations {
558                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
559                        store: wgpu::StoreOp::Store,
560                    },
561                    depth_slice: None,
562                })],
563                ..Default::default()
564            });
565            drop(rpass);
566
567            // Create and dispatch the compute pass
568            {
569                let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
570                    label: Some("Compute Pass"),
571                    timestamp_writes: None,
572                });
573
574                compute_pipeline_registry.dispatch_erased(
575                    gpu,
576                    queue,
577                    config,
578                    &mut cpass,
579                    &*command,
580                    resource_manager,
581                    read_view,
582                    &write_target.view,
583                );
584            } // cpass is dropped here, ending the pass
585
586            // The result of this pass is now in write_target.
587            // For the next iteration, this will be our read source.
588            read_view = &write_target.view;
589            // Swap targets for the next iteration
590            std::mem::swap(&mut write_target, &mut read_target);
591        }
592
593        // After the loop, the final result is in the `read_view`,
594        // because we swapped one last time at the end of the loop.
595        read_view
596    }
597}