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//!         gpu: &wgpu::Device,
88//!         gpu_queue: &wgpu::Queue,
89//!         config: &wgpu::SurfaceConfiguration,
90//!         render_pass: &mut wgpu::RenderPass<'_>,
91//!         command: &RectangleCommand,
92//!         size: PxSize,
93//!         start_pos: PxPosition,
94//!         scene_texture_view: &wgpu::TextureView,
95//!     ) {
96//!         // Update uniforms with command data
97//!         // Set pipeline and draw
98//!         render_pass.set_pipeline(&self.render_pipeline);
99//!         render_pass.set_bind_group(0, &self.bind_group, &[]);
100//!         render_pass.draw(0..6, 0..1); // Draw quad
101//!     }
102//! }
103//!
104//! // 3. Register the pipeline
105//! let mut registry = PipelineRegistry::new();
106//! let rectangle_pipeline = RectanglePipeline::new(&device, &config, sample_count);
107//! registry.register(rectangle_pipeline);
108//! ```
109//!
110//! # Integration with Basic Components
111//!
112//! The `tessera_basic_components` crate demonstrates real-world pipeline implementations:
113//!
114//! - **ShapePipeline**: Renders rounded rectangles, circles, and complex shapes with shadows and ripple effects
115//! - **TextPipeline**: Handles text rendering with font management and glyph caching
116//! - **ImagePipeline**: Displays images with various scaling and filtering options
117//! - **FluidGlassPipeline**: Creates advanced glass effects with distortion and transparency
118//!
119//! These pipelines are registered in `tessera_ui_basic_components::pipelines::register_pipelines()`.
120//!
121//! # Performance Considerations
122//!
123//! - **Batch Similar Commands**: Group similar draw commands to minimize pipeline switches
124//! - **Resource Management**: Reuse buffers and textures when possible
125//! - **Shader Optimization**: Write efficient shaders optimized for your target platforms
126//! - **State Changes**: Minimize render state changes within the draw method
127//!
128//! # Advanced Features
129//!
130//! ## Barrier Requirements
131//!
132//! Some rendering effects need to sample from previously rendered content (e.g., blur effects).
133//! Implement [`DrawCommand::barrier()`] to return [`BarrierRequirement::SampleBackground`]
134//! for such commands.
135//!
136//! ## Multi-Pass Rendering
137//!
138//! Use `begin_pass()` and `end_pass()` for pipelines that require multiple rendering passes
139//! or complex setup/teardown operations.
140//!
141//! ## Scene Texture Access
142//!
143//! The `scene_texture_view` parameter provides access to the current scene texture,
144//! enabling effects that sample from the background or perform post-processing.
145
146use crate::{PxPosition, px::PxSize, renderer::DrawCommand};
147
148/// Core trait for implementing custom graphics rendering pipelines.
149///
150/// This trait defines the interface for rendering pipelines that process specific types
151/// of draw commands. Each pipeline is responsible for setting up GPU resources,
152/// managing render state, and executing the actual drawing operations.
153///
154/// # Type Parameters
155///
156/// * `T` - The specific [`DrawCommand`] type this pipeline can handle
157///
158/// # Lifecycle Methods
159///
160/// The pipeline system provides three lifecycle hooks:
161///
162/// - [`begin_pass()`](Self::begin_pass): Called once at the start of the render pass
163/// - [`draw()`](Self::draw): Called for each command of type `T`
164/// - [`end_pass()`](Self::end_pass): Called once at the end of the render pass
165///
166/// # Implementation Notes
167///
168/// - Only the [`draw()`](Self::draw) method is required; others have default empty implementations
169/// - Pipelines should be stateless between frames when possible
170/// - Resource management should prefer reuse over recreation
171/// - Consider batching multiple commands for better performance
172///
173/// # Example
174///
175/// See the module-level documentation for a complete implementation example.
176#[allow(unused_variables)]
177pub trait DrawablePipeline<T: DrawCommand> {
178    /// Called once at the beginning of the render pass.
179    ///
180    /// Use this method to perform one-time setup operations that apply to all
181    /// draw commands of this type in the current frame. This is ideal for:
182    ///
183    /// - Setting up shared uniform buffers
184    /// - Binding global resources
185    /// - Configuring render state that persists across multiple draw calls
186    ///
187    /// # Parameters
188    ///
189    /// * `gpu` - The WGPU device for creating resources
190    /// * `gpu_queue` - The WGPU queue for submitting commands
191    /// * `config` - Current surface configuration
192    /// * `render_pass` - The active render pass
193    ///
194    /// # Default Implementation
195    ///
196    /// The default implementation does nothing, which is suitable for most pipelines.
197    fn begin_pass(
198        &mut self,
199        gpu: &wgpu::Device,
200        gpu_queue: &wgpu::Queue,
201        config: &wgpu::SurfaceConfiguration,
202        render_pass: &mut wgpu::RenderPass<'_>,
203    ) {
204    }
205
206    /// Called once at the end of the render pass.
207    ///
208    /// Use this method to perform cleanup operations or finalize rendering
209    /// for all draw commands of this type in the current frame. This is useful for:
210    ///
211    /// - Cleaning up temporary resources
212    /// - Finalizing multi-pass rendering operations
213    /// - Submitting batched draw calls
214    ///
215    /// # Parameters
216    ///
217    /// * `gpu` - The WGPU device for creating resources
218    /// * `gpu_queue` - The WGPU queue for submitting commands
219    /// * `config` - Current surface configuration
220    /// * `render_pass` - The active render pass
221    ///
222    /// # Default Implementation
223    ///
224    /// The default implementation does nothing, which is suitable for most pipelines.
225    fn end_pass(
226        &mut self,
227        gpu: &wgpu::Device,
228        gpu_queue: &wgpu::Queue,
229        config: &wgpu::SurfaceConfiguration,
230        render_pass: &mut wgpu::RenderPass<'_>,
231    ) {
232    }
233
234    /// Renders a single draw command.
235    ///
236    /// This is the core method where the actual rendering happens. It's called
237    /// once for each draw command of type `T` that needs to be rendered.
238    ///
239    /// # Parameters
240    ///
241    /// * `gpu` - The WGPU device for creating resources
242    /// * `gpu_queue` - The WGPU queue for submitting commands and updating buffers
243    /// * `config` - Current surface configuration containing format and size information
244    /// * `render_pass` - The active render pass to record draw commands into
245    /// * `command` - The specific draw command to render
246    /// * `size` - The size of the rendering area in pixels
247    /// * `start_pos` - The top-left position where rendering should begin
248    /// * `scene_texture_view` - View of the current scene texture for background sampling
249    ///
250    /// # Implementation Guidelines
251    ///
252    /// - Update any per-command uniforms or push constants
253    /// - Set the appropriate render pipeline
254    /// - Bind necessary resources (textures, buffers, bind groups)
255    /// - Issue draw calls (typically `draw()`, `draw_indexed()`, or `draw_indirect()`)
256    /// - Avoid expensive operations like buffer creation; prefer reusing resources
257    ///
258    /// # Scene Texture Usage
259    ///
260    /// The `scene_texture_view` provides access to the current rendered scene,
261    /// enabling effects that sample from the background. This is commonly used for:
262    ///
263    /// - Blur and post-processing effects
264    /// - Glass and transparency effects
265    /// - Distortion and refraction
266    ///
267    /// # Example
268    ///
269    /// ```rust,ignore
270    /// fn draw(&mut self, gpu: &wgpu::Device, gpu_queue: &wgpu::Queue,
271    ///         config: &wgpu::SurfaceConfiguration, render_pass: &mut wgpu::RenderPass<'_>,
272    ///         command: &MyCommand, size: PxSize, start_pos: PxPosition,
273    ///         scene_texture_view: &wgpu::TextureView) {
274    ///     // Update uniforms with command-specific data
275    ///     let uniforms = MyUniforms {
276    ///         color: command.color,
277    ///         position: [start_pos.x as f32, start_pos.y as f32],
278    ///         size: [size.width as f32, size.height as f32],
279    ///     };
280    ///     gpu_queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
281    ///     
282    ///     // Set pipeline and resources
283    ///     render_pass.set_pipeline(&self.render_pipeline);
284    ///     render_pass.set_bind_group(0, &self.bind_group, &[]);
285    ///     
286    ///     // Draw a quad (two triangles)
287    ///     render_pass.draw(0..6, 0..1);
288    /// }
289    /// ```
290    fn draw(
291        &mut self,
292        gpu: &wgpu::Device,
293        gpu_queue: &wgpu::Queue,
294        config: &wgpu::SurfaceConfiguration,
295        render_pass: &mut wgpu::RenderPass<'_>,
296        command: &T,
297        size: PxSize,
298        start_pos: PxPosition,
299        scene_texture_view: &wgpu::TextureView,
300    );
301}
302
303/// Internal trait for type erasure of drawable pipelines.
304///
305/// This trait enables dynamic dispatch of draw commands to their corresponding pipelines
306/// without knowing the specific command type at compile time. It's used internally by
307/// the [`PipelineRegistry`] and should not be implemented directly by users.
308///
309/// The type erasure is achieved through the [`AsAny`] trait, which allows downcasting
310/// from `&dyn DrawCommand` to concrete command types.
311///
312/// # Implementation Note
313///
314/// This trait is automatically implemented for any type that implements
315/// [`DrawablePipeline<T>`] through the [`DrawablePipelineImpl`] wrapper.
316pub trait ErasedDrawablePipeline {
317    fn begin_pass(
318        &mut self,
319        gpu: &wgpu::Device,
320        gpu_queue: &wgpu::Queue,
321        config: &wgpu::SurfaceConfiguration,
322        render_pass: &mut wgpu::RenderPass<'_>,
323    );
324
325    fn end_pass(
326        &mut self,
327        gpu: &wgpu::Device,
328        gpu_queue: &wgpu::Queue,
329        config: &wgpu::SurfaceConfiguration,
330        render_pass: &mut wgpu::RenderPass<'_>,
331    );
332
333    fn draw_erased(
334        &mut self,
335        gpu: &wgpu::Device,
336        gpu_queue: &wgpu::Queue,
337        config: &wgpu::SurfaceConfiguration,
338        render_pass: &mut wgpu::RenderPass<'_>,
339        command: &dyn DrawCommand,
340        size: PxSize,
341        start_pos: PxPosition,
342        scene_texture_view: &wgpu::TextureView,
343    ) -> bool;
344}
345
346struct DrawablePipelineImpl<T: DrawCommand, P: DrawablePipeline<T>> {
347    pipeline: P,
348    _marker: std::marker::PhantomData<T>,
349}
350
351impl<T: DrawCommand + 'static, P: DrawablePipeline<T> + 'static> ErasedDrawablePipeline
352    for DrawablePipelineImpl<T, P>
353{
354    fn begin_pass(
355        &mut self,
356        gpu: &wgpu::Device,
357        gpu_queue: &wgpu::Queue,
358        config: &wgpu::SurfaceConfiguration,
359        render_pass: &mut wgpu::RenderPass<'_>,
360    ) {
361        self.pipeline
362            .begin_pass(gpu, gpu_queue, config, render_pass);
363    }
364
365    fn end_pass(
366        &mut self,
367        gpu: &wgpu::Device,
368        gpu_queue: &wgpu::Queue,
369        config: &wgpu::SurfaceConfiguration,
370        render_pass: &mut wgpu::RenderPass<'_>,
371    ) {
372        self.pipeline.end_pass(gpu, gpu_queue, config, render_pass);
373    }
374
375    fn draw_erased(
376        &mut self,
377        gpu: &wgpu::Device,
378        gpu_queue: &wgpu::Queue,
379        config: &wgpu::SurfaceConfiguration,
380        render_pass: &mut wgpu::RenderPass<'_>,
381        command: &dyn DrawCommand,
382        size: PxSize,
383        start_pos: PxPosition,
384        scene_texture_view: &wgpu::TextureView,
385    ) -> bool {
386        if let Some(cmd) = command.as_any().downcast_ref::<T>() {
387            self.pipeline.draw(
388                gpu,
389                gpu_queue,
390                config,
391                render_pass,
392                cmd,
393                size,
394                start_pos,
395                scene_texture_view,
396            );
397            true
398        } else {
399            false
400        }
401    }
402}
403
404/// Registry for managing and dispatching drawable pipelines.
405///
406/// The `PipelineRegistry` serves as the central hub for all rendering pipelines in the
407/// Tessera framework. It maintains a collection of registered pipelines and handles
408/// the dispatch of draw commands to their appropriate pipelines.
409///
410/// # Architecture
411///
412/// The registry uses type erasure to store pipelines of different types in a single
413/// collection. When a draw command needs to be rendered, the registry iterates through
414/// all registered pipelines until it finds one that can handle the command type.
415///
416/// # Usage Pattern
417///
418/// 1. Create a new registry
419/// 2. Register all required pipelines during application initialization
420/// 3. The renderer uses the registry to dispatch commands during frame rendering
421///
422/// # Example
423///
424/// ```rust,ignore
425/// use tessera_ui::renderer::drawer::PipelineRegistry;
426///
427/// // Create registry and register pipelines
428/// let mut registry = PipelineRegistry::new();
429/// registry.register(my_shape_pipeline);
430/// registry.register(my_text_pipeline);
431/// registry.register(my_image_pipeline);
432///
433/// // Registry is now ready for use by the renderer
434/// ```
435///
436/// # Performance Considerations
437///
438/// - Pipeline lookup is O(n) where n is the number of registered pipelines
439/// - Register frequently used pipelines first for better average performance
440/// - Consider the order of registration based on command frequency
441pub struct PipelineRegistry {
442    pub(crate) pipelines: Vec<Box<dyn ErasedDrawablePipeline>>,
443}
444
445impl Default for PipelineRegistry {
446    fn default() -> Self {
447        Self::new()
448    }
449}
450
451impl PipelineRegistry {
452    /// Creates a new empty pipeline registry.
453    ///
454    /// # Example
455    ///
456    /// ```rust
457    /// use tessera_ui::renderer::drawer::PipelineRegistry;
458    ///
459    /// let registry = PipelineRegistry::new();
460    /// ```
461    pub fn new() -> Self {
462        Self {
463            pipelines: Vec::new(),
464        }
465    }
466
467    /// Registers a new drawable pipeline for a specific command type.
468    ///
469    /// This method takes ownership of the pipeline and wraps it in a type-erased
470    /// container that can be stored alongside other pipelines of different types.
471    ///
472    /// # Type Parameters
473    ///
474    /// * `T` - The [`DrawCommand`] type this pipeline handles
475    /// * `P` - The pipeline implementation type
476    ///
477    /// # Parameters
478    ///
479    /// * `pipeline` - The pipeline instance to register
480    ///
481    /// # Panics
482    ///
483    /// This method does not panic, but the registry will panic during dispatch
484    /// if no pipeline is found for a given command type.
485    ///
486    /// # Example
487    ///
488    /// ```rust,ignore
489    /// use tessera_ui::renderer::drawer::PipelineRegistry;
490    ///
491    /// let mut registry = PipelineRegistry::new();
492    ///
493    /// // Register a custom pipeline
494    /// let my_pipeline = MyCustomPipeline::new(&device, &config, sample_count);
495    /// registry.register(my_pipeline);
496    ///
497    /// // Register multiple pipelines
498    /// registry.register(ShapePipeline::new(&device, &config, sample_count));
499    /// registry.register(TextPipeline::new(&device, &config, sample_count));
500    /// ```
501    ///
502    /// # Registration Order
503    ///
504    /// The order of registration can affect performance since pipeline lookup
505    /// is performed linearly. Consider registering more frequently used pipelines first.
506    pub fn register<T: DrawCommand + 'static, P: DrawablePipeline<T> + 'static>(
507        &mut self,
508        pipeline: P,
509    ) {
510        let erased = Box::new(DrawablePipelineImpl::<T, P> {
511            pipeline,
512            _marker: std::marker::PhantomData,
513        });
514        self.pipelines.push(erased);
515    }
516
517    pub(crate) fn begin_all_passes(
518        &mut self,
519        gpu: &wgpu::Device,
520        gpu_queue: &wgpu::Queue,
521        config: &wgpu::SurfaceConfiguration,
522        render_pass: &mut wgpu::RenderPass<'_>,
523    ) {
524        for pipeline in self.pipelines.iter_mut() {
525            pipeline.begin_pass(gpu, gpu_queue, config, render_pass);
526        }
527    }
528
529    pub(crate) fn end_all_passes(
530        &mut self,
531        gpu: &wgpu::Device,
532        gpu_queue: &wgpu::Queue,
533        config: &wgpu::SurfaceConfiguration,
534        render_pass: &mut wgpu::RenderPass<'_>,
535    ) {
536        for pipeline in self.pipelines.iter_mut() {
537            pipeline.end_pass(gpu, gpu_queue, config, render_pass);
538        }
539    }
540
541    pub(crate) fn dispatch(
542        &mut self,
543        gpu: &wgpu::Device,
544        gpu_queue: &wgpu::Queue,
545        config: &wgpu::SurfaceConfiguration,
546        render_pass: &mut wgpu::RenderPass<'_>,
547        cmd: &dyn DrawCommand,
548        size: PxSize,
549        start_pos: PxPosition,
550        scene_texture_view: &wgpu::TextureView,
551    ) {
552        for pipeline in self.pipelines.iter_mut() {
553            if pipeline.draw_erased(
554                gpu,
555                gpu_queue,
556                config,
557                render_pass,
558                cmd,
559                size,
560                start_pos,
561                scene_texture_view,
562            ) {
563                return;
564            }
565        }
566
567        panic!(
568            "No pipeline found for command {:?}",
569            std::any::type_name_of_val(cmd)
570        );
571    }
572}