Skip to main content

tessera_ui/renderer/drawer/
pipeline.rs

1//! Custom drawing pipelines.
2//!
3//! ## Usage
4//!
5//! Implement [`DrawablePipeline`] for a command and register it with [`PipelineRegistry`].
6
7use std::{any::TypeId, collections::HashMap};
8
9use smallvec::SmallVec;
10
11use crate::{
12    px::{PxRect, PxSize},
13    render_graph::DrawGeometry,
14    renderer::DrawCommand,
15};
16
17/// Resources available throughout one frame.
18pub struct FrameContext<'a> {
19    /// Renderer-owned resource creation and uploads.
20    pub graphics: &'a crate::GraphicsDevice,
21    /// The WGPU device.
22    pub device: &'a wgpu::Device,
23    /// The WGPU queue.
24    pub queue: &'a wgpu::Queue,
25    /// The current surface configuration.
26    pub config: &'a wgpu::SurfaceConfiguration,
27}
28
29/// A batch of drawing commands within an active render pass.
30pub struct DrawContext<'a, 'b, 'c, T> {
31    /// Renderer-owned resource creation and uploads.
32    pub graphics: &'a crate::GraphicsDevice,
33    /// The WGPU device.
34    pub device: &'a wgpu::Device,
35    /// The WGPU queue.
36    pub queue: &'a wgpu::Queue,
37    /// The current surface configuration.
38    pub config: &'a wgpu::SurfaceConfiguration,
39    /// Target texture size for the current pass.
40    pub target_size: PxSize,
41    /// The active render pass encoder.
42    pub render_pass: &'a mut wgpu::RenderPass<'b>,
43    /// The draw commands to be processed.
44    pub commands: &'c [(&'c T, DrawGeometry)],
45    /// Scene pixels sampled by commands that declare a sample region.
46    pub scene_texture_view: &'a wgpu::TextureView,
47    /// Window rectangle of the valid pixels in `scene_texture_view`, whose
48    /// first texel is at the rectangle's origin. The texture can be larger.
49    pub scene_rect: PxRect,
50    /// An optional clipping rectangle for the draw call.
51    pub clip_rect: Option<PxRect>,
52}
53
54/// Type-erased context used for dispatching draw pipelines.
55pub struct ErasedDrawContext<'a, 'b> {
56    /// Renderer-owned resource creation and uploads.
57    pub graphics: &'a crate::GraphicsDevice,
58    /// WGPU device used for pipeline resource access.
59    pub device: &'a wgpu::Device,
60    /// WGPU queue used for submissions.
61    pub queue: &'a wgpu::Queue,
62    /// Current surface configuration for the render target.
63    pub config: &'a wgpu::SurfaceConfiguration,
64    /// Target texture size for the current pass.
65    pub target_size: PxSize,
66    /// Active render pass that receives draw calls.
67    pub render_pass: &'a mut wgpu::RenderPass<'b>,
68    /// Scene pixels sampled by commands that declare a sample region.
69    pub scene_texture_view: &'a wgpu::TextureView,
70    /// Window rectangle of the valid pixels in `scene_texture_view`, whose
71    /// first texel is at the rectangle's origin. The texture can be larger.
72    pub scene_rect: PxRect,
73    /// Optional clipping rectangle applied to the submission.
74    pub clip_rect: Option<PxRect>,
75}
76
77/// Draws commands of type `T`.
78///
79/// Each frame calls `begin_frame`, then `draw` for each batch; `end_frame` runs
80/// after all passes and before submission.
81#[allow(unused_variables)]
82pub trait DrawablePipeline<T: DrawCommand> {
83    /// Prepares resources once per frame, before render passes.
84    fn begin_frame(&mut self, context: &FrameContext<'_>) {}
85
86    /// Renders the command batch. Honor `clip_rect` when setting scissor bounds.
87    /// The scene texture is available for commands that declare background sampling.
88    fn draw(&mut self, context: &mut DrawContext<'_, '_, '_, T>);
89
90    /// Finalizes the frame after all passes and before GPU submission.
91    fn end_frame(&mut self, context: &FrameContext<'_>) {}
92}
93
94pub(crate) trait ErasedDrawablePipeline {
95    /// Called at the beginning of a frame to prepare pipeline resources.
96    fn begin_frame(&mut self, context: &FrameContext<'_>);
97    /// Called at the end of a frame for cleanup or readback.
98    fn end_frame(&mut self, context: &FrameContext<'_>);
99    /// Draws a batch of commands with type-erased dispatch.
100    fn draw_erased(
101        &mut self,
102        context: ErasedDrawContext<'_, '_>,
103        commands: &[(&dyn DrawCommand, DrawGeometry)],
104    ) -> bool;
105}
106
107struct DrawablePipelineImpl<T: DrawCommand, P: DrawablePipeline<T>> {
108    pipeline: P,
109    _marker: std::marker::PhantomData<T>,
110}
111
112impl<T: DrawCommand + 'static, P: DrawablePipeline<T> + 'static> ErasedDrawablePipeline
113    for DrawablePipelineImpl<T, P>
114{
115    fn begin_frame(&mut self, context: &FrameContext<'_>) {
116        self.pipeline.begin_frame(context);
117    }
118
119    fn end_frame(&mut self, context: &FrameContext<'_>) {
120        self.pipeline.end_frame(context);
121    }
122
123    fn draw_erased(
124        &mut self,
125        context: ErasedDrawContext<'_, '_>,
126        commands: &[(&dyn DrawCommand, DrawGeometry)],
127    ) -> bool {
128        let timing = tracing::enabled!(tracing::Level::DEBUG).then(crate::time::Instant::now);
129        if commands.is_empty() {
130            return true;
131        }
132
133        let ErasedDrawContext {
134            graphics,
135            device,
136            queue,
137            config,
138            target_size,
139            render_pass,
140            scene_texture_view,
141            scene_rect,
142            clip_rect,
143        } = context;
144
145        if commands[0].0.is::<T>() {
146            let typed_commands: SmallVec<[(&T, DrawGeometry); 16]> = commands
147                .iter()
148                .map(|(cmd, geometry)| {
149                    (
150                        cmd.downcast_ref::<T>().expect(
151                            "FATAL: A command in a batch has a different type than the first one.",
152                        ),
153                        *geometry,
154                    )
155                })
156                .collect();
157
158            self.pipeline.draw(&mut DrawContext {
159                graphics,
160                device,
161                queue,
162                config,
163                target_size,
164                render_pass,
165                commands: &typed_commands,
166                scene_texture_view,
167                scene_rect,
168                clip_rect,
169            });
170            if let Some(timing) = timing {
171                tracing::debug!(
172                    pipeline = std::any::type_name::<P>(),
173                    phase = "draw",
174                    commands = commands.len(),
175                    elapsed_us = timing.elapsed().as_micros(),
176                    frame_nanos = crate::current_frame_nanos(),
177                    "pipeline_cpu"
178                );
179            }
180            true
181        } else {
182            false
183        }
184    }
185}
186
187/// Registered drawing pipelines, selected by command type.
188#[derive(Default)]
189pub struct PipelineRegistry {
190    pub(crate) pipelines: HashMap<TypeId, Box<dyn ErasedDrawablePipeline>>,
191}
192
193impl PipelineRegistry {
194    /// Creates an empty pipeline registry.
195    pub fn new() -> Self {
196        Self::default()
197    }
198
199    /// Registers a pipeline for its command type, replacing any prior registration.
200    pub fn register<T: DrawCommand + 'static, P: DrawablePipeline<T> + 'static>(
201        &mut self,
202        pipeline: P,
203    ) {
204        let erased = Box::new(DrawablePipelineImpl::<T, P> {
205            pipeline,
206            _marker: std::marker::PhantomData,
207        });
208        self.pipelines.insert(TypeId::of::<T>(), erased);
209    }
210
211    pub(crate) fn begin_all_frames(
212        &mut self,
213        device: &wgpu::Device,
214        queue: &wgpu::Queue,
215        config: &wgpu::SurfaceConfiguration,
216        graphics: &crate::GraphicsDevice,
217    ) {
218        graphics.begin_frame();
219        for pipeline in self.pipelines.values_mut() {
220            pipeline.begin_frame(&FrameContext {
221                graphics,
222                device,
223                queue,
224                config,
225            });
226        }
227    }
228
229    pub(crate) fn end_all_frames(
230        &mut self,
231        device: &wgpu::Device,
232        queue: &wgpu::Queue,
233        config: &wgpu::SurfaceConfiguration,
234        graphics: &crate::GraphicsDevice,
235    ) {
236        for pipeline in self.pipelines.values_mut() {
237            pipeline.end_frame(&FrameContext {
238                graphics,
239                device,
240                queue,
241                config,
242            });
243        }
244    }
245
246    pub(crate) fn dispatch(
247        &mut self,
248        context: ErasedDrawContext<'_, '_>,
249        commands: &[(&dyn DrawCommand, DrawGeometry)],
250    ) {
251        if commands.is_empty() {
252            return;
253        }
254
255        let command_type_id = commands[0].0.as_any().type_id();
256        if let Some(pipeline) = self.pipelines.get_mut(&command_type_id) {
257            if !pipeline.draw_erased(context, commands) {
258                panic!(
259                    "FATAL: A command in a batch has a different type than the first one. This should not happen."
260                )
261            }
262        } else {
263            panic!(
264                "No pipeline found for command {:?}",
265                std::any::type_name_of_val(commands[0].0)
266            );
267        }
268    }
269}