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}