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}