tessera_ui/renderer/drawer/
pipeline.rs

1//! Graphics rendering pipeline system for Tessera UI framework.
2//!
3//! This module provides the core infrastructure for pluggable graphics rendering pipelines
4//! in Tessera. The design philosophy emphasizes flexibility and extensibility, allowing
5//! developers to create custom rendering effects without being constrained by built-in
6//! drawing primitives.
7//!
8//! # Architecture Overview
9//!
10//! The pipeline system uses a trait-based approach with type erasure to support dynamic
11//! dispatch of rendering commands. Each pipeline is responsible for rendering a specific
12//! type of draw command, such as shapes, text, images, or custom visual effects.
13//!
14//! ## Key Components
15//!
16//! - [`DrawablePipeline<T>`]: The main trait for implementing custom rendering pipelines
17//! - [`PipelineRegistry`]: Manages and dispatches commands to registered pipelines
18//!
19//! # Design Philosophy
20//!
21//! Unlike traditional UI frameworks that provide built-in "brush" or drawing primitives,
22//! Tessera treats shaders as first-class citizens. This approach offers several advantages:
23//!
24//! - **Modern GPU Utilization**: Leverages WGPU and WGSL for efficient, cross-platform rendering
25//! - **Advanced Visual Effects**: Enables complex effects like neumorphic design, lighting,
26//!   shadows, reflections, and bloom that are difficult to achieve with traditional approaches
27//! - **Flexibility**: Custom shaders allow for unlimited creative possibilities
28//! - **Performance**: Direct GPU programming eliminates abstraction overhead
29//!
30//! # Pipeline Lifecycle
31//!
32//! Each pipeline follows a three-phase lifecycle during rendering:
33//!
34//! 1. **Begin Pass**: Setup phase for initializing pipeline-specific resources
35//! 2. **Draw**: Main rendering phase where commands are processed
36//! 3. **End Pass**: Cleanup phase for finalizing rendering operations
37//!
38//! # Implementation Guide
39//!
40//! ## Creating a Custom Pipeline
41//!
42//! To create a custom rendering pipeline:
43//!
44//! 1. Define your draw command struct implementing [`DrawCommand`]
45//! 2. Create a pipeline struct implementing [`DrawablePipeline<YourCommand>`]
46//! 3. Register the pipeline with [`PipelineRegistry::register`]
47//!
48//! # Integration with Basic Components
49//!
50//! The `tessera_basic_components` crate demonstrates real-world pipeline implementations:
51//!
52//! - **ShapePipeline**: Renders rounded rectangles, circles, and complex shapes with shadows and ripple effects
53//! - **TextPipeline**: Handles text rendering with font management and glyph caching
54//! - **ImagePipeline**: Displays images with various scaling and filtering options
55//! - **FluidGlassPipeline**: Creates advanced glass effects with distortion and transparency
56//!
57//! These pipelines are registered in `tessera_ui_basic_components::pipelines::register_pipelines()`.
58//!
59//! # Performance Considerations
60//!
61//! - **Batch Similar Commands**: Group similar draw commands to minimize pipeline switches
62//! - **Resource Management**: Reuse buffers and textures when possible
63//! - **Shader Optimization**: Write efficient shaders optimized for your target platforms
64//! - **State Changes**: Minimize render state changes within the draw method
65//!
66//! # Advanced Features
67//!
68//! ## Barrier Requirements
69//!
70//! Some rendering effects need to sample from previously rendered content (e.g., blur effects).
71//! Implement [`DrawCommand::barrier()`] to return `SampleBackground` requirements for such commands.
72//!
73//! ## Multi-Pass Rendering
74//!
75//! Use `begin_pass()` and `end_pass()` for pipelines that require multiple rendering passes
76//! or complex setup/teardown operations.
77//!
78//! ## Scene Texture Access
79//!
80//! The `scene_texture_view` parameter provides access to the current scene texture,
81//! enabling effects that sample from the background or perform post-processing.
82
83use std::{any::TypeId, collections::HashMap};
84
85use crate::{
86    px::{PxPosition, PxRect, PxSize},
87    renderer::DrawCommand,
88};
89
90/// Provides context for operations that occur once per frame.
91///
92/// This struct bundles essential WGPU resources and configuration that are relevant
93/// for the entire rendering frame, but are not specific to a single render pass.
94pub struct FrameContext<'a> {
95    /// The WGPU device.
96    pub device: &'a wgpu::Device,
97    /// The WGPU queue.
98    pub queue: &'a wgpu::Queue,
99    /// The current surface configuration.
100    pub config: &'a wgpu::SurfaceConfiguration,
101}
102
103/// Provides context for operations within a single render pass.
104///
105/// This struct bundles WGPU resources and configuration specific to a render pass,
106/// including the active render pass encoder and the scene texture view for sampling.
107pub struct PassContext<'a, 'b> {
108    /// The WGPU device.
109    pub device: &'a wgpu::Device,
110    /// The WGPU queue.
111    pub queue: &'a wgpu::Queue,
112    /// The current surface configuration.
113    pub config: &'a wgpu::SurfaceConfiguration,
114    /// The active render pass encoder.
115    pub render_pass: &'a mut wgpu::RenderPass<'b>,
116    /// A view of the current scene texture.
117    pub scene_texture_view: &'a wgpu::TextureView,
118}
119
120/// Provides comprehensive context for drawing operations within a render pass.
121///
122/// This struct extends `PassContext` with information specific to individual draw calls,
123/// including the commands to be rendered and an optional clipping rectangle.
124///
125/// # Type Parameters
126///
127/// * `T` - The specific [`DrawCommand`] type being processed.
128///
129/// # Fields
130///
131/// * `device` - The WGPU device, used for creating and managing GPU resources.
132/// * `queue` - The WGPU queue, used for submitting command buffers and writing buffer data.
133/// * `config` - The current surface configuration, providing information like format and dimensions.
134/// * `render_pass` - The active `wgpu::RenderPass` encoder, used to record rendering commands.
135/// * `commands` - A slice of tuples, each containing a draw command, its size, and its position.
136/// * `scene_texture_view` - A view of the current scene texture, useful for effects that sample from the background.
137/// * `clip_rect` - An optional rectangle defining the clipping area for the draw call.
138pub struct DrawContext<'a, 'b, 'c, T> {
139    /// The WGPU device.
140    pub device: &'a wgpu::Device,
141    /// The WGPU queue.
142    pub queue: &'a wgpu::Queue,
143    /// The current surface configuration.
144    pub config: &'a wgpu::SurfaceConfiguration,
145    /// The active render pass encoder.
146    pub render_pass: &'a mut wgpu::RenderPass<'b>,
147    /// The draw commands to be processed.
148    pub commands: &'c [(&'c T, PxSize, PxPosition)],
149    /// A view of the current scene texture.
150    pub scene_texture_view: &'a wgpu::TextureView,
151    /// An optional clipping rectangle for the draw call.
152    pub clip_rect: Option<PxRect>,
153}
154
155/// Type-erased context used for dispatching draw pipelines.
156pub struct ErasedDrawContext<'a, 'b> {
157    /// WGPU device used for pipeline resource access.
158    pub device: &'a wgpu::Device,
159    /// WGPU queue used for submissions.
160    pub queue: &'a wgpu::Queue,
161    /// Current surface configuration for the render target.
162    pub config: &'a wgpu::SurfaceConfiguration,
163    /// Active render pass that receives draw calls.
164    pub render_pass: &'a mut wgpu::RenderPass<'b>,
165    /// Scene texture view available for sampling.
166    pub scene_texture_view: &'a wgpu::TextureView,
167    /// Optional clipping rectangle applied to the submission.
168    pub clip_rect: Option<PxRect>,
169}
170
171/// Core trait for implementing custom graphics rendering pipelines.
172///
173/// This trait defines the interface for rendering pipelines that process specific types
174/// of draw commands. Each pipeline is responsible for setting up GPU resources,
175/// managing render state, and executing the actual drawing operations.
176///
177/// # Type Parameters
178///
179/// * `T` - The specific [`DrawCommand`] type this pipeline can handle
180///
181/// # Lifecycle Methods
182///
183/// The pipeline system provides five lifecycle hooks, executed in the following order:
184///
185/// 1. [`begin_frame()`](Self::begin_frame): Called once at the start of a new frame, before any render passes.
186/// 2. [`begin_pass()`](Self::begin_pass): Called at the start of each render pass that involves this pipeline.
187/// 3. [`draw()`](Self::draw): Called for each command of type `T` within a render pass.
188/// 4. [`end_pass()`](Self::end_pass): Called at the end of each render pass that involved this pipeline.
189/// 5. [`end_frame()`](Self::end_frame): Called once at the end of the frame, after all render passes are complete.
190///
191/// Typically, `begin_pass`, `draw`, and `end_pass` are used for the core rendering logic within a pass,
192/// while `begin_frame` and `end_frame` are used for setup and teardown that spans the entire frame.
193///
194/// # Implementation Notes
195///
196/// - Only the [`draw()`](Self::draw) method is required; others have default empty implementations.
197/// - Pipelines should be stateless between frames when possible
198/// - Resource management should prefer reuse over recreation
199/// - Consider batching multiple commands for better performance
200///
201/// # Example
202///
203/// See the module-level documentation for a complete implementation example.
204#[allow(unused_variables)]
205pub trait DrawablePipeline<T: DrawCommand> {
206    /// Called once at the beginning of the frame, before any render passes.
207    ///
208    /// This method is the first hook in the pipeline's frame lifecycle. It's invoked
209    /// after a new `CommandEncoder` has been created but before any rendering occurs.
210    /// It's ideal for per-frame setup that is not tied to a specific `wgpu::RenderPass`.
211    ///
212    /// Since this method is called outside a render pass, it cannot be used for drawing
213    /// commands. However, it can be used for operations like:
214    ///
215    /// - Updating frame-global uniform buffers (e.g., with time or resolution data)
216    ///   using [`wgpu::Queue::write_buffer`].
217    /// - Preparing or resizing buffers that will be used throughout the frame.
218    /// - Performing CPU-side calculations needed for the frame.
219    ///
220    /// # Parameters
221    ///
222    /// * `context` - The context for the frame.
223    ///
224    /// # Default Implementation
225    ///
226    /// The default implementation does nothing.
227    fn begin_frame(&mut self, context: &FrameContext<'_>) {}
228
229    /// Called once at the beginning of the render pass.
230    ///
231    /// Use this method to perform one-time setup operations that apply to all
232    /// draw commands of this type in the current frame. This is ideal for:
233    ///
234    /// - Setting up shared uniform buffers
235    /// - Binding global resources
236    /// - Configuring render state that persists across multiple draw calls
237    ///
238    /// # Parameters
239    ///
240    /// * `context` - The context for the render pass.
241    ///
242    /// # Default Implementation
243    ///
244    /// The default implementation does nothing, which is suitable for most pipelines.
245    fn begin_pass(&mut self, context: &mut PassContext<'_, '_>) {}
246
247    /// Renders a batch of draw commands.
248    ///
249    /// This is the core method where the actual rendering happens. It's called
250    /// once for a batch of draw commands of type `T` that need to be rendered.
251    ///
252    /// # Parameters
253    ///
254    /// * `context` - The context for drawing, including the render pass and commands.
255    ///
256    /// # Implementation Guidelines
257    ///
258    /// - Iterate over the `context.commands` slice to process each command.
259    /// - Update buffers (e.g., instance buffers, storage buffers) with data from the command batch.
260    /// - Set the appropriate render pipeline.
261    /// - Bind necessary resources (textures, buffers, bind groups).
262    /// - Issue one or more draw calls (e.g., an instanced draw call) to render the entire batch.
263    /// - If `context.clip_rect` is `Some`, use `context.render_pass.set_scissor_rect()` to clip rendering.
264    /// - Avoid expensive operations like buffer creation; prefer reusing and updating existing resources.
265    ///
266    /// # Scene Texture Usage
267    ///
268    /// The `context.scene_texture_view` provides access to the current rendered scene,
269    /// enabling effects that sample from the background.
270    fn draw(&mut self, context: &mut DrawContext<'_, '_, '_, T>);
271
272    /// Called once at the end of the render pass.
273    ///
274    /// Use this method to perform cleanup operations or finalize rendering
275    /// for all draw commands of this type in the current frame. This is useful for:
276    ///
277    /// - Cleaning up temporary resources
278    /// - Finalizing multi-pass rendering operations
279    /// - Submitting batched draw calls
280    ///
281    /// # Parameters
282    ///
283    /// * `context` - The context for the render pass.
284    ///
285    /// # Default Implementation
286    ///
287    /// The default implementation does nothing, which is suitable for most pipelines.
288    fn end_pass(&mut self, context: &mut PassContext<'_, '_>) {}
289
290    /// Called once at the end of the frame, after all render passes are complete.
291    ///
292    /// This method is the final hook in the pipeline's frame lifecycle. It's invoked
293    /// after all `begin_pass`, `draw`, and `end_pass` calls for the frame have
294    /// completed, but before the frame's command buffer is submitted to the GPU.
295    ///
296    /// It's suitable for frame-level cleanup or finalization tasks, such as:
297    ///
298    /// - Reading data back from the GPU (though this can be slow and should be used sparingly).
299    /// - Cleaning up temporary resources created in `begin_frame`.
300    /// - Preparing data for the next frame.
301    ///
302    /// # Parameters
303    ///
304    /// * `context` - The context for the frame.
305    ///
306    /// # Default Implementation
307    ///
308    /// The default implementation does nothing.
309    fn end_frame(&mut self, context: &FrameContext<'_>) {}
310}
311
312/// Internal trait for type erasure of drawable pipelines.
313///
314/// This trait enables dynamic dispatch of draw commands to their corresponding pipelines
315/// without knowing the specific command type at compile time. It's used internally by
316/// the [`PipelineRegistry`] and should not be implemented directly by users.
317///
318/// The type erasure is achieved through the [`AsAny`] trait, which allows downcasting
319/// from `&dyn DrawCommand` to concrete command types.
320///
321/// # Implementation Note
322///
323/// This trait is automatically implemented for any type that implements
324/// [`DrawablePipeline<T>`] through the [`DrawablePipelineImpl`] wrapper.
325pub(crate) trait ErasedDrawablePipeline {
326    /// Called at the beginning of a frame to prepare pipeline resources.
327    fn begin_frame(&mut self, context: &FrameContext<'_>);
328    /// Called at the end of a frame for cleanup or readback.
329    fn end_frame(&mut self, context: &FrameContext<'_>);
330    /// Invoked before a render pass starts to bind shared pass state.
331    fn begin_pass(&mut self, context: &mut PassContext<'_, '_>);
332    /// Invoked after a render pass ends to finalize pass-level resources.
333    fn end_pass(&mut self, context: &mut PassContext<'_, '_>);
334    /// Draws a batch of commands with type-erased dispatch.
335    fn draw_erased(
336        &mut self,
337        context: ErasedDrawContext<'_, '_>,
338        commands: &[(&dyn DrawCommand, PxSize, PxPosition)],
339    ) -> bool;
340}
341
342struct DrawablePipelineImpl<T: DrawCommand, P: DrawablePipeline<T>> {
343    pipeline: P,
344    _marker: std::marker::PhantomData<T>,
345}
346
347impl<T: DrawCommand + 'static, P: DrawablePipeline<T> + 'static> ErasedDrawablePipeline
348    for DrawablePipelineImpl<T, P>
349{
350    fn begin_frame(&mut self, context: &FrameContext<'_>) {
351        self.pipeline.begin_frame(context);
352    }
353
354    fn end_frame(&mut self, context: &FrameContext<'_>) {
355        self.pipeline.end_frame(context);
356    }
357
358    fn begin_pass(&mut self, context: &mut PassContext<'_, '_>) {
359        self.pipeline.begin_pass(context);
360    }
361
362    fn end_pass(&mut self, context: &mut PassContext<'_, '_>) {
363        self.pipeline.end_pass(context);
364    }
365
366    fn draw_erased(
367        &mut self,
368        context: ErasedDrawContext<'_, '_>,
369        commands: &[(&dyn DrawCommand, PxSize, PxPosition)],
370    ) -> bool {
371        if commands.is_empty() {
372            return true;
373        }
374
375        let ErasedDrawContext {
376            device,
377            queue,
378            config,
379            render_pass,
380            scene_texture_view,
381            clip_rect,
382        } = context;
383
384        if commands[0].0.as_any().is::<T>() {
385            let typed_commands: Vec<(&T, PxSize, PxPosition)> = commands
386                .iter()
387                .map(|(cmd, size, pos)| {
388                    (
389                        cmd.as_any().downcast_ref::<T>().expect(
390                            "FATAL: A command in a batch has a different type than the first one.",
391                        ),
392                        *size,
393                        *pos,
394                    )
395                })
396                .collect();
397
398            self.pipeline.draw(&mut DrawContext {
399                device,
400                queue,
401                config,
402                render_pass,
403                commands: &typed_commands,
404                scene_texture_view,
405                clip_rect,
406            });
407            true
408        } else {
409            false
410        }
411    }
412}
413
414/// Registry for managing and dispatching drawable pipelines.
415///
416/// The `PipelineRegistry` serves as the central hub for all rendering pipelines in the
417/// Tessera framework. It maintains a collection of registered pipelines and handles
418/// the dispatch of draw commands to their appropriate pipelines.
419///
420/// # Architecture
421///
422/// The registry uses type erasure to store pipelines of different types in a single
423/// collection. When a draw command needs to be rendered, the registry iterates through
424/// all registered pipelines until it finds one that can handle the command type.
425///
426/// # Usage Pattern
427///
428/// 1. Create a new registry
429/// 2. Register all required pipelines during application initialization
430/// 3. The renderer uses the registry to dispatch commands during frame rendering
431pub struct PipelineRegistry {
432    pub(crate) pipelines: HashMap<TypeId, Box<dyn ErasedDrawablePipeline>>,
433}
434
435impl Default for PipelineRegistry {
436    fn default() -> Self {
437        Self::new()
438    }
439}
440
441impl PipelineRegistry {
442    /// Creates a new empty pipeline registry.
443    ///
444    /// # Example
445    ///
446    /// ```
447    /// use tessera_ui::renderer::drawer::PipelineRegistry;
448    ///
449    /// let registry = PipelineRegistry::new();
450    /// ```
451    pub fn new() -> Self {
452        Self {
453            pipelines: HashMap::new(),
454        }
455    }
456
457    /// Registers a new drawable pipeline for a specific command type.
458    ///
459    /// This method takes ownership of the pipeline and wraps it in a type-erased
460    /// container that can be stored alongside other pipelines of different types.
461    ///
462    /// # Type Parameters
463    ///
464    /// * `T` - The [`DrawCommand`] type this pipeline handles
465    /// * `P` - The pipeline implementation type
466    ///
467    /// # Parameters
468    ///
469    /// * `pipeline` - The pipeline instance to register
470    ///
471    /// # Panics
472    ///
473    /// This method does not panic, but the registry will panic during dispatch
474    /// if no pipeline is found for a given command type.
475    pub fn register<T: DrawCommand + 'static, P: DrawablePipeline<T> + 'static>(
476        &mut self,
477        pipeline: P,
478    ) {
479        let erased = Box::new(DrawablePipelineImpl::<T, P> {
480            pipeline,
481            _marker: std::marker::PhantomData,
482        });
483        self.pipelines.insert(TypeId::of::<T>(), erased);
484    }
485
486    pub(crate) fn begin_all_passes(
487        &mut self,
488        device: &wgpu::Device,
489        queue: &wgpu::Queue,
490        config: &wgpu::SurfaceConfiguration,
491        render_pass: &mut wgpu::RenderPass<'_>,
492        scene_texture_view: &wgpu::TextureView,
493    ) {
494        for pipeline in self.pipelines.values_mut() {
495            pipeline.begin_pass(&mut PassContext {
496                device,
497                queue,
498                config,
499                render_pass,
500                scene_texture_view,
501            });
502        }
503    }
504
505    pub(crate) fn end_all_passes(
506        &mut self,
507        device: &wgpu::Device,
508        queue: &wgpu::Queue,
509        config: &wgpu::SurfaceConfiguration,
510        render_pass: &mut wgpu::RenderPass<'_>,
511        scene_texture_view: &wgpu::TextureView,
512    ) {
513        for pipeline in self.pipelines.values_mut() {
514            pipeline.end_pass(&mut PassContext {
515                device,
516                queue,
517                config,
518                render_pass,
519                scene_texture_view,
520            });
521        }
522    }
523
524    pub(crate) fn begin_all_frames(
525        &mut self,
526        device: &wgpu::Device,
527        queue: &wgpu::Queue,
528        config: &wgpu::SurfaceConfiguration,
529    ) {
530        for pipeline in self.pipelines.values_mut() {
531            pipeline.begin_frame(&FrameContext {
532                device,
533                queue,
534                config,
535            });
536        }
537    }
538
539    pub(crate) fn end_all_frames(
540        &mut self,
541        device: &wgpu::Device,
542        queue: &wgpu::Queue,
543        config: &wgpu::SurfaceConfiguration,
544    ) {
545        for pipeline in self.pipelines.values_mut() {
546            pipeline.end_frame(&FrameContext {
547                device,
548                queue,
549                config,
550            });
551        }
552    }
553
554    pub(crate) fn dispatch(
555        &mut self,
556        context: ErasedDrawContext<'_, '_>,
557        commands: &[(&dyn DrawCommand, PxSize, PxPosition)],
558    ) {
559        if commands.is_empty() {
560            return;
561        }
562
563        let command_type_id = commands[0].0.as_any().type_id();
564        if let Some(pipeline) = self.pipelines.get_mut(&command_type_id) {
565            if !pipeline.draw_erased(context, commands) {
566                panic!(
567                    "FATAL: A command in a batch has a different type than the first one. This should not happen."
568                )
569            }
570        } else {
571            panic!(
572                "No pipeline found for command {:?}",
573                std::any::type_name_of_val(commands[0].0)
574            );
575        }
576    }
577}