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