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