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::{any::TypeId, io, sync::Arc, time::Duration};
8
9use rustc_hash::FxHashMap;
10use winit::window::Window;
11
12use crate::{
13    ComputablePipeline, ComputeCommand, DrawCommand, DrawablePipeline, pipeline_cache::save_cache,
14};
15
16use super::{compute::ComputePipelineRegistry, drawer::Drawer};
17
18mod clip;
19mod frame;
20#[cfg(all(feature = "profiling", not(target_arch = "wasm32")))]
21mod gpu_timing;
22mod init;
23
24struct RenderPipelines {
25    clipper: clip::ShapeClipper,
26    drawer: Drawer,
27    compute_registry: ComputePipelineRegistry,
28}
29
30#[derive(Clone, Copy, Eq, Hash, PartialEq)]
31enum PipelineKind {
32    Draw,
33    Compute,
34}
35
36type PipelineFactory = Box<dyn FnOnce(&mut RenderCore) + Send>;
37
38struct FrameTargets {
39    offscreen: wgpu::TextureView,
40    // Only surface-less cores render into a copy for readback.
41    offscreen_copy: Option<wgpu::TextureView>,
42    msaa_texture: Option<wgpu::Texture>,
43    msaa_view: Option<wgpu::TextureView>,
44    stencil: wgpu::TextureView,
45    sample_count: u32,
46}
47
48/// Timing breakdown for the most recent render call.
49#[derive(Clone, Copy, Debug, Default)]
50pub(crate) struct RenderTimingBreakdown {
51    /// Time spent acquiring the swapchain texture.
52    pub acquire: Duration,
53    /// Time spent building the render pass graph and pass plan.
54    pub build_passes: Duration,
55    /// Time spent encoding GPU commands.
56    pub encode: Duration,
57    /// Time spent submitting command buffers to the queue.
58    pub submit: Duration,
59    /// Time spent presenting the swapchain image.
60    pub present: Duration,
61    /// Total render duration for the frame.
62    pub total: Duration,
63}
64
65/// Error returned when reading back an offscreen frame fails.
66#[derive(Clone, Debug)]
67pub enum OffscreenReadbackError {
68    /// The offscreen target has a zero width or height.
69    EmptyTarget,
70    /// The GPU device poll failed while waiting for the readback buffer.
71    Poll(String),
72    /// Mapping the readback buffer failed.
73    Map(String),
74}
75
76impl std::fmt::Display for OffscreenReadbackError {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            Self::EmptyTarget => write!(f, "offscreen render target has zero size"),
80            Self::Poll(err) => write!(f, "device poll failed during offscreen readback: {err}"),
81            Self::Map(err) => write!(f, "failed to map offscreen readback buffer: {err}"),
82        }
83    }
84}
85
86impl std::error::Error for OffscreenReadbackError {}
87
88struct BlitState {
89    clear_pipeline: wgpu::RenderPipeline,
90    present_pipeline: wgpu::RenderPipeline,
91    pipeline_rgba: wgpu::RenderPipeline,
92    bind_group_layout: wgpu::BindGroupLayout,
93    sampler: wgpu::Sampler,
94    #[cfg(feature = "debug-dirty-overlay")]
95    dirty_overlay_pipeline: wgpu::RenderPipeline,
96}
97
98/// Render core holding device, surface, pipelines, and frame resources.
99pub struct RenderCore {
100    graphics: crate::GraphicsDevice,
101    #[allow(unused)]
102    window: Option<Arc<Window>>,
103    pub(crate) instance: wgpu::Instance,
104    device: wgpu::Device,
105    surface: Option<wgpu::Surface<'static>>,
106    queue: wgpu::Queue,
107    config: wgpu::SurfaceConfiguration,
108    draw_config: wgpu::SurfaceConfiguration,
109    size: winit::dpi::PhysicalSize<u32>,
110    size_changed: bool,
111    pipelines: RenderPipelines,
112    pipeline_factories: FxHashMap<(PipelineKind, TypeId), PipelineFactory>,
113
114    pipeline_cache: Option<wgpu::PipelineCache>,
115    adapter_info: wgpu::AdapterInfo,
116
117    targets: FrameTargets,
118    blit: BlitState,
119    frame_index: u64,
120    previous_graph: Option<crate::RenderGraph>,
121    last_render_breakdown: Option<RenderTimingBreakdown>,
122    #[cfg(all(feature = "profiling", not(target_arch = "wasm32")))]
123    gpu_timing: Option<gpu_timing::GpuTimingProbe>,
124}
125
126/// Shared GPU resources used when creating pipelines.
127pub struct RenderResources<'a> {
128    /// Renderer-owned resource creation, uploads, and work observations.
129    pub graphics: &'a crate::GraphicsDevice,
130    /// WGPU device used for pipeline creation.
131    pub device: &'a wgpu::Device,
132    /// WGPU queue used by pipelines that upload data.
133    pub queue: &'a wgpu::Queue,
134    /// Configuration of the color targets used by render pipelines.
135    pub surface_config: &'a wgpu::SurfaceConfiguration,
136    /// Optional pipeline cache when supported by the adapter.
137    pub pipeline_cache: Option<&'a wgpu::PipelineCache>,
138    /// MSAA sample count for render pipelines.
139    pub sample_count: u32,
140}
141
142impl RenderCore {
143    /// Returns shared GPU resources used for pipeline creation.
144    pub fn resources(&self) -> RenderResources<'_> {
145        RenderResources {
146            graphics: &self.graphics,
147            device: &self.device,
148            queue: &self.queue,
149            surface_config: &self.draw_config,
150            pipeline_cache: self.pipeline_cache.as_ref(),
151            sample_count: self.targets.sample_count,
152        }
153    }
154
155    /// Returns the current window handle.
156    ///
157    /// Panics if this core was created for offscreen rendering; check
158    /// [`RenderCore::has_surface`] first.
159    pub fn window(&self) -> &Window {
160        self.window
161            .as_deref()
162            .expect("RenderCore has no window in offscreen mode")
163    }
164
165    /// Returns a cloned window handle for external storage.
166    ///
167    /// Panics if this core was created for offscreen rendering.
168    pub fn window_arc(&self) -> Arc<Window> {
169        self.window
170            .clone()
171            .expect("RenderCore has no window in offscreen mode")
172    }
173
174    /// Returns whether this core renders into a window surface.
175    ///
176    /// Offscreen cores return `false`.
177    pub fn has_surface(&self) -> bool {
178        self.surface.is_some()
179    }
180
181    /// Returns the WGPU device.
182    pub fn device(&self) -> &wgpu::Device {
183        &self.device
184    }
185
186    /// Returns the WGPU queue.
187    pub fn queue(&self) -> &wgpu::Queue {
188        &self.queue
189    }
190
191    /// Returns the current surface configuration.
192    pub fn surface_config(&self) -> &wgpu::SurfaceConfiguration {
193        &self.config
194    }
195
196    /// Returns the pipeline cache if available.
197    pub fn pipeline_cache(&self) -> Option<&wgpu::PipelineCache> {
198        self.pipeline_cache.as_ref()
199    }
200
201    /// Returns the configured MSAA sample count.
202    pub fn sample_count(&self) -> u32 {
203        self.targets.sample_count
204    }
205
206    /// Returns the timing breakdown for the most recent render call.
207    pub(crate) fn last_render_breakdown(&self) -> Option<RenderTimingBreakdown> {
208        self.last_render_breakdown
209    }
210
211    /// Registers a new drawable pipeline for a specific command type.
212    pub fn register_draw_pipeline<T, P>(&mut self, pipeline: P)
213    where
214        T: DrawCommand + 'static,
215        P: DrawablePipeline<T> + 'static,
216    {
217        self.pipeline_factories
218            .remove(&(PipelineKind::Draw, TypeId::of::<T>()));
219        self.pipelines.drawer.pipeline_registry.register(pipeline);
220    }
221
222    /// Registers a new compute pipeline for a specific command type.
223    pub fn register_compute_pipeline<T, P>(&mut self, pipeline: P)
224    where
225        T: ComputeCommand + 'static,
226        P: ComputablePipeline<T> + 'static,
227    {
228        self.pipeline_factories
229            .remove(&(PipelineKind::Compute, TypeId::of::<T>()));
230        self.pipelines.compute_registry.register(pipeline);
231    }
232
233    pub(crate) fn register_draw_pipeline_factory<T, P>(
234        &mut self,
235        factory: impl FnOnce(RenderResources<'_>) -> P + Send + 'static,
236    ) where
237        T: DrawCommand + 'static,
238        P: DrawablePipeline<T> + 'static,
239    {
240        let type_id = TypeId::of::<T>();
241        self.pipelines
242            .drawer
243            .pipeline_registry
244            .pipelines
245            .remove(&type_id);
246        self.pipeline_factories.insert(
247            (PipelineKind::Draw, type_id),
248            Box::new(move |core| {
249                let pipeline = factory(core.resources());
250                core.register_draw_pipeline(pipeline);
251            }),
252        );
253    }
254
255    pub(crate) fn register_compute_pipeline_factory<T, P>(
256        &mut self,
257        factory: impl FnOnce(RenderResources<'_>) -> P + Send + 'static,
258    ) where
259        T: ComputeCommand + 'static,
260        P: ComputablePipeline<T> + 'static,
261    {
262        let type_id = TypeId::of::<T>();
263        self.pipelines.compute_registry.remove(type_id);
264        self.pipeline_factories.insert(
265            (PipelineKind::Compute, type_id),
266            Box::new(move |core| {
267                let pipeline = factory(core.resources());
268                core.register_compute_pipeline(pipeline);
269            }),
270        );
271    }
272
273    pub(crate) fn save_pipeline_cache(&self) -> io::Result<()> {
274        if let Some(cache) = self.pipeline_cache.as_ref() {
275            save_cache(cache, &self.adapter_info)?;
276        }
277        Ok(())
278    }
279}