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