Skip to main content

tessera_ui/renderer/
core.rs

1//! WGPU render core for Tessera frames.
2//!
3//! ## Usage
4//!
5//! Drive frame submission and GPU resource setup for Tessera applications.
6
7use std::{io, sync::Arc, time::Duration};
8
9use winit::window::Window;
10
11use crate::{
12    CompositeCommand, ComputablePipeline, ComputeCommand, DrawCommand, DrawablePipeline, PxSize,
13    compute::resource::ComputeResourceManager,
14    pipeline_cache::save_cache,
15    render_graph::RenderTextureDesc,
16    renderer::{
17        composite::{CompositeContext, CompositePipelineRegistry},
18        external::ExternalTextureRegistry,
19    },
20};
21
22use super::{compute::ComputePipelineRegistry, drawer::Drawer};
23
24mod frame;
25mod init;
26
27struct RenderPipelines {
28    drawer: Drawer,
29    compute_registry: ComputePipelineRegistry,
30    composite_registry: CompositePipelineRegistry,
31}
32
33struct FrameTargets {
34    offscreen: wgpu::TextureView,
35    offscreen_copy: wgpu::TextureView,
36    msaa_texture: Option<wgpu::Texture>,
37    msaa_view: Option<wgpu::TextureView>,
38    sample_count: u32,
39}
40
41/// Timing breakdown for the most recent render call.
42#[derive(Clone, Copy, Debug, Default)]
43pub(crate) struct RenderTimingBreakdown {
44    /// Time spent acquiring the swapchain texture.
45    pub acquire: Duration,
46    /// Time spent building the render pass graph and pass plan.
47    pub build_passes: Duration,
48    /// Time spent encoding GPU commands.
49    pub encode: Duration,
50    /// Time spent submitting command buffers to the queue.
51    pub submit: Duration,
52    /// Time spent presenting the swapchain image.
53    pub present: Duration,
54    /// Total render duration for the frame.
55    pub total: Duration,
56}
57
58struct ComputeState {
59    target_a: wgpu::TextureView,
60    target_b: wgpu::TextureView,
61    resource_manager: ComputeResourceManager,
62}
63
64struct BlitState {
65    pipeline: wgpu::RenderPipeline,
66    pipeline_rgba: wgpu::RenderPipeline,
67    bind_group_layout: wgpu::BindGroupLayout,
68    sampler: wgpu::Sampler,
69    #[cfg(feature = "debug-dirty-overlay")]
70    dirty_overlay_pipeline: wgpu::RenderPipeline,
71}
72
73#[derive(Clone, Copy, PartialEq, Eq, Hash)]
74struct RenderTextureDescKey {
75    size: PxSize,
76    format: wgpu::TextureFormat,
77    sample_count: u32,
78}
79
80impl RenderTextureDescKey {
81    fn from_desc(desc: &RenderTextureDesc, sample_count: u32) -> Self {
82        Self {
83            size: desc.size,
84            format: desc.format,
85            sample_count,
86        }
87    }
88}
89
90struct TextureHandle {
91    view: wgpu::TextureView,
92}
93
94struct LocalTextureSlot {
95    desc: RenderTextureDescKey,
96    front: TextureHandle,
97    back: TextureHandle,
98    msaa_view: Option<wgpu::TextureView>,
99    in_use: bool,
100    last_used_frame: u64,
101}
102
103impl LocalTextureSlot {
104    fn front_view(&self) -> &wgpu::TextureView {
105        &self.front.view
106    }
107
108    fn back_view(&self) -> &wgpu::TextureView {
109        &self.back.view
110    }
111
112    fn swap_front_back(&mut self) {
113        std::mem::swap(&mut self.front, &mut self.back);
114    }
115}
116
117struct LocalTexturePool {
118    slots: Vec<LocalTextureSlot>,
119}
120
121impl LocalTexturePool {
122    const MAX_SLOTS: usize = 16;
123
124    fn new() -> Self {
125        Self { slots: Vec::new() }
126    }
127
128    fn clear(&mut self) {
129        self.slots.clear();
130    }
131
132    fn begin_frame(&mut self, _current_frame: u64) {
133        for slot in &mut self.slots {
134            slot.in_use = false;
135        }
136    }
137
138    fn allocate(
139        &mut self,
140        device: &wgpu::Device,
141        desc: &RenderTextureDesc,
142        sample_count: u32,
143        current_frame: u64,
144    ) -> usize {
145        let key = RenderTextureDescKey::from_desc(desc, sample_count);
146        if let Some((index, slot)) = self
147            .slots
148            .iter_mut()
149            .enumerate()
150            .find(|(_, slot)| slot.desc == key && !slot.in_use)
151        {
152            slot.in_use = true;
153            slot.last_used_frame = current_frame;
154            return index;
155        }
156
157        if self.slots.len() >= Self::MAX_SLOTS
158            && let Some(index) = self.lru_unused_index()
159        {
160            self.slots.swap_remove(index);
161        }
162
163        let front = create_local_texture(device, desc, "Local Front");
164        let back = create_local_texture(device, desc, "Local Back");
165        let msaa_view = if sample_count > 1 {
166            Some(create_msaa_view(device, desc, sample_count))
167        } else {
168            None
169        };
170
171        let slot = LocalTextureSlot {
172            desc: key,
173            front,
174            back,
175            msaa_view,
176            in_use: true,
177            last_used_frame: current_frame,
178        };
179        self.slots.push(slot);
180        self.slots.len() - 1
181    }
182
183    fn lru_unused_index(&self) -> Option<usize> {
184        self.slots
185            .iter()
186            .enumerate()
187            .filter(|(_, slot)| !slot.in_use)
188            .min_by_key(|(_, slot)| slot.last_used_frame)
189            .map(|(index, _)| index)
190    }
191
192    fn slot(&self, index: usize) -> Option<&LocalTextureSlot> {
193        self.slots.get(index)
194    }
195
196    fn slot_mut(&mut self, index: usize) -> Option<&mut LocalTextureSlot> {
197        self.slots.get_mut(index)
198    }
199}
200
201fn create_local_texture(
202    device: &wgpu::Device,
203    desc: &RenderTextureDesc,
204    label: &str,
205) -> TextureHandle {
206    let width = desc.size.width.positive().max(1);
207    let height = desc.size.height.positive().max(1);
208    let texture = device.create_texture(&wgpu::TextureDescriptor {
209        label: Some(label),
210        size: wgpu::Extent3d {
211            width,
212            height,
213            depth_or_array_layers: 1,
214        },
215        mip_level_count: 1,
216        sample_count: 1,
217        dimension: wgpu::TextureDimension::D2,
218        format: desc.format,
219        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
220            | wgpu::TextureUsages::TEXTURE_BINDING
221            | wgpu::TextureUsages::STORAGE_BINDING
222            | wgpu::TextureUsages::COPY_SRC
223            | wgpu::TextureUsages::COPY_DST,
224        view_formats: &[],
225    });
226    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
227    TextureHandle { view }
228}
229
230fn create_msaa_view(
231    device: &wgpu::Device,
232    desc: &RenderTextureDesc,
233    sample_count: u32,
234) -> wgpu::TextureView {
235    let width = desc.size.width.positive().max(1);
236    let height = desc.size.height.positive().max(1);
237    let texture = device.create_texture(&wgpu::TextureDescriptor {
238        label: Some("Local MSAA"),
239        size: wgpu::Extent3d {
240            width,
241            height,
242            depth_or_array_layers: 1,
243        },
244        mip_level_count: 1,
245        sample_count,
246        dimension: wgpu::TextureDimension::D2,
247        format: desc.format,
248        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
249        view_formats: &[],
250    });
251    texture.create_view(&wgpu::TextureViewDescriptor::default())
252}
253
254/// Render core holding device, surface, pipelines, and frame resources.
255pub struct RenderCore {
256    /// Avoiding release the window
257    #[allow(unused)]
258    window: Arc<Window>,
259    /// WGPU instance used to recreate surfaces when the swapchain is lost.
260    instance: wgpu::Instance,
261    /// WGPU device
262    device: wgpu::Device,
263    /// WGPU surface
264    surface: wgpu::Surface<'static>,
265    /// WGPU queue
266    queue: wgpu::Queue,
267    /// WGPU surface configuration
268    config: wgpu::SurfaceConfiguration,
269    /// size of the window
270    size: winit::dpi::PhysicalSize<u32>,
271    /// if size is changed
272    size_changed: bool,
273    /// Draw and compute pipeline registries.
274    pipelines: RenderPipelines,
275
276    /// WGPU pipeline cache for faster pipeline creation when supported.
277    pipeline_cache: Option<wgpu::PipelineCache>,
278    /// Gpu adapter info
279    adapter_info: wgpu::AdapterInfo,
280
281    /// Render target resources for the current frame.
282    targets: FrameTargets,
283    /// Compute resources for ping-pong passes.
284    compute: ComputeState,
285    /// Blit resources for partial copies.
286    blit: BlitState,
287    /// Pool of local textures declared by render graph resources.
288    local_textures: LocalTexturePool,
289    /// Registry of external textures owned by pipelines.
290    external_textures: ExternalTextureRegistry,
291    /// Monotonic frame counter for resource eviction.
292    frame_index: u64,
293    /// Timing breakdown for the last render call.
294    last_render_breakdown: Option<RenderTimingBreakdown>,
295}
296
297/// Shared GPU resources used when creating pipelines.
298pub struct RenderResources<'a> {
299    /// WGPU device used for pipeline creation.
300    pub device: &'a wgpu::Device,
301    /// WGPU queue used by pipelines that upload data.
302    pub queue: &'a wgpu::Queue,
303    /// Surface configuration used for render pipeline setup.
304    pub surface_config: &'a wgpu::SurfaceConfiguration,
305    /// Optional pipeline cache when supported by the adapter.
306    pub pipeline_cache: Option<&'a wgpu::PipelineCache>,
307    /// MSAA sample count for render pipelines.
308    pub sample_count: u32,
309}
310
311impl RenderCore {
312    /// Returns shared GPU resources used for pipeline creation.
313    pub fn resources(&self) -> RenderResources<'_> {
314        RenderResources {
315            device: &self.device,
316            queue: &self.queue,
317            surface_config: &self.config,
318            pipeline_cache: self.pipeline_cache.as_ref(),
319            sample_count: self.targets.sample_count,
320        }
321    }
322
323    /// Returns the current window handle.
324    pub fn window(&self) -> &Window {
325        &self.window
326    }
327
328    /// Returns a cloned window handle for external storage.
329    pub fn window_arc(&self) -> Arc<Window> {
330        self.window.clone()
331    }
332
333    /// Returns the WGPU device.
334    pub fn device(&self) -> &wgpu::Device {
335        &self.device
336    }
337
338    /// Returns the WGPU queue.
339    pub fn queue(&self) -> &wgpu::Queue {
340        &self.queue
341    }
342
343    /// Returns the current surface configuration.
344    pub fn surface_config(&self) -> &wgpu::SurfaceConfiguration {
345        &self.config
346    }
347
348    /// Returns the pipeline cache if available.
349    pub fn pipeline_cache(&self) -> Option<&wgpu::PipelineCache> {
350        self.pipeline_cache.as_ref()
351    }
352
353    /// Returns the configured MSAA sample count.
354    pub fn sample_count(&self) -> u32 {
355        self.targets.sample_count
356    }
357
358    /// Returns the frame-local compute resource manager.
359    pub fn compute_resource_manager_mut(&mut self) -> &mut ComputeResourceManager {
360        &mut self.compute.resource_manager
361    }
362
363    /// Returns the device and compute manager used during record.
364    pub fn record_resources(&mut self) -> (&wgpu::Device, &mut ComputeResourceManager) {
365        (&self.device, &mut self.compute.resource_manager)
366    }
367
368    /// Returns the timing breakdown for the most recent render call.
369    pub(crate) fn last_render_breakdown(&self) -> Option<RenderTimingBreakdown> {
370        self.last_render_breakdown
371    }
372
373    /// Registers a new drawable pipeline for a specific command type.
374    ///
375    /// This method takes ownership of the pipeline and wraps it in a
376    /// type-erased container that can be stored alongside other pipelines of
377    /// different types.
378    pub fn register_draw_pipeline<T, P>(&mut self, pipeline: P)
379    where
380        T: DrawCommand + 'static,
381        P: DrawablePipeline<T> + 'static,
382    {
383        self.pipelines.drawer.pipeline_registry.register(pipeline);
384    }
385
386    /// Registers a new compute pipeline for a specific command type.
387    ///
388    /// This method takes ownership of the pipeline and wraps it in a
389    /// type-erased container that can be stored alongside other pipelines of
390    /// different types.
391    pub fn register_compute_pipeline<T, P>(&mut self, pipeline: P)
392    where
393        T: ComputeCommand + 'static,
394        P: ComputablePipeline<T> + 'static,
395    {
396        self.pipelines.compute_registry.register(pipeline);
397    }
398
399    /// Registers a new composite pipeline for a specific command type.
400    pub fn register_composite_pipeline<T, P>(&mut self, pipeline: P)
401    where
402        T: CompositeCommand + 'static,
403        P: super::composite::CompositePipeline<T> + 'static,
404    {
405        self.pipelines.composite_registry.register(pipeline);
406    }
407
408    pub(crate) fn composite_context_parts(
409        &mut self,
410        frame_size: PxSize,
411        frame_index: u64,
412    ) -> (CompositeContext<'_>, &mut CompositePipelineRegistry) {
413        let RenderCore {
414            device,
415            queue,
416            config,
417            pipeline_cache,
418            targets,
419            pipelines,
420            ..
421        } = self;
422        let resources = RenderResources {
423            device,
424            queue,
425            surface_config: config,
426            pipeline_cache: pipeline_cache.as_ref(),
427            sample_count: targets.sample_count,
428        };
429        let context = CompositeContext {
430            resources,
431            external_textures: self.external_textures.clone(),
432            frame_size,
433            surface_format: config.format,
434            sample_count: targets.sample_count,
435            frame_index,
436        };
437        (context, &mut pipelines.composite_registry)
438    }
439
440    pub(crate) fn save_pipeline_cache(&self) -> io::Result<()> {
441        if let Some(cache) = self.pipeline_cache.as_ref() {
442            save_cache(cache, &self.adapter_info)?;
443        }
444        Ok(())
445    }
446}