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