GPU Rendering

Presentar uses WebGPU for hardware-accelerated rendering, achieving 60fps performance for complex UIs.

Architecture

┌─────────────────────────────────────────────────────────────┐
│  Widget Tree                                                 │
│  └── paint() → RecordingCanvas                              │
├─────────────────────────────────────────────────────────────┤
│  Draw Commands                                               │
│  └── Batch by type (rects, circles, text)                   │
├─────────────────────────────────────────────────────────────┤
│  Instance Buffer                                             │
│  └── [pos, size, color, corner_radius, shape_type]          │
├─────────────────────────────────────────────────────────────┤
│  WGSL Shader                                                 │
│  └── SDF-based rendering with anti-aliasing                 │
├─────────────────────────────────────────────────────────────┤
│  WebGPU Pipeline                                             │
│  └── Instanced draw call → Framebuffer                      │
└─────────────────────────────────────────────────────────────┘

Pipeline Stages

1. Draw Command Collection

// Widget paints to RecordingCanvas
fn paint(&self, canvas: &mut RecordingCanvas) {
    canvas.fill_rect(self.bounds, self.color);
    canvas.draw_text(&self.label, position, style);
}

2. Command Batching

// Commands batched by type for efficient rendering
struct RenderBatch {
    instances: Vec<Instance>,
    texture: Option<TextureHandle>,
}

// Single draw call for many primitives
let rect_batch = batch_rects(&draw_commands);  // 100 rects → 1 call
let text_batch = batch_text(&draw_commands);   // 50 glyphs → 1 call

3. Instance Buffer Upload

pub struct Instance {
    pub pos: [f32; 2],           // Screen position
    pub size: [f32; 2],          // Width, height
    pub color: [f32; 4],         // RGBA
    pub corner_radius: f32,      // Border radius
    pub shape_type: u32,         // 0=rect, 1=circle, 2=text
}

// Upload to GPU
queue.write_buffer(&instance_buffer, 0, bytemuck::cast_slice(&instances));

4. Shader Execution

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    // SDF-based shape with anti-aliased edges
    let d = sdf_rounded_rect(local_pos, half_size, corner_radius);
    let alpha = 1.0 - smoothstep(-1.0, 1.0, d);
    return vec4<f32>(in.color.rgb, in.color.a * alpha);
}

WebGPU Resources

Configuration

pub struct WebGpuConfig {
    pub canvas_id: String,
    pub power_preference: PowerPreference,
    pub present_mode: PresentMode,
    pub max_instances: usize,
    pub glyph_atlas_size: u32,
}

impl Default for WebGpuConfig {
    fn default() -> Self {
        Self {
            canvas_id: "canvas".to_string(),
            power_preference: PowerPreference::HighPerformance,
            present_mode: PresentMode::Fifo,
            max_instances: 10_000,
            glyph_atlas_size: 1024,
        }
    }
}

Resource Management

pub struct GpuResources {
    device: Device,
    queue: Queue,
    surface: Surface,
    pipeline: RenderPipeline,
    uniform_buffer: Buffer,
    instance_buffer: Buffer,
    glyph_atlas: Texture,
    glyph_sampler: Sampler,
}

impl GpuResources {
    pub fn render_instances(&self, instances: &[Instance]) {
        // Single instanced draw call
        render_pass.draw(0..6, 0..instances.len() as u32);
    }
}

Text Rendering

Glyph Cache

pub struct GlyphCache {
    atlas: Texture,
    regions: HashMap<GlyphKey, AtlasRegion>,
    next_position: (u32, u32),
    row_height: u32,
}

#[derive(Hash, Eq, PartialEq)]
pub struct GlyphKey {
    pub codepoint: char,
    pub font_size: u16,
    pub font_id: u16,
}

pub struct AtlasRegion {
    pub u: f32,
    pub v: f32,
    pub width: f32,
    pub height: f32,
}

Text Layout

pub struct TextLayout {
    pub glyphs: Vec<PositionedGlyph>,
    pub bounds: Rect,
    pub baseline: f32,
}

pub fn layout_text(
    text: &str,
    font: &Font,
    size: f32,
    max_width: Option<f32>,
) -> TextLayout {
    // Use fontdue for glyph metrics
    // Position glyphs with kerning
    // Handle word wrapping
}

Performance Characteristics

OperationCPU OnlyGPU AcceleratedSpeedup
1000 rectangles5ms0.5ms10x
100 text glyphs10ms1ms10x
Full frame (complex UI)15ms2ms7.5x
10000 rectangles50ms1ms50x

Instanced Rendering

// Vertex buffer: unit quad
const QUAD_VERTICES: &[Vertex] = &[
    Vertex { position: [-1.0, -1.0], uv: [0.0, 0.0] },
    Vertex { position: [ 1.0, -1.0], uv: [1.0, 0.0] },
    Vertex { position: [ 1.0,  1.0], uv: [1.0, 1.0] },
    Vertex { position: [-1.0, -1.0], uv: [0.0, 0.0] },
    Vertex { position: [ 1.0,  1.0], uv: [1.0, 1.0] },
    Vertex { position: [-1.0,  1.0], uv: [0.0, 1.0] },
];

// Each instance transforms the quad
render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
render_pass.draw(0..6, 0..instance_count);

Canvas2D Fallback

For browsers without WebGPU support:

pub struct Canvas2dRenderer {
    context: CanvasRenderingContext2d,
}

impl Canvas2dRenderer {
    pub fn render(&self, commands: &[DrawCommand]) {
        for cmd in commands {
            match cmd {
                DrawCommand::FillRect { rect, color } => {
                    self.context.set_fill_style(&color.to_css());
                    self.context.fill_rect(
                        rect.x.into(),
                        rect.y.into(),
                        rect.width.into(),
                        rect.height.into(),
                    );
                }
                // ... other commands
            }
        }
    }
}

Software Rendering (Testing)

#[cfg(test)]
pub struct SoftwareRenderer {
    buffer: Vec<u32>,
    width: u32,
    height: u32,
}

impl SoftwareRenderer {
    pub fn new(width: u32, height: u32) -> Self {
        Self {
            buffer: vec![0; (width * height) as usize],
            width,
            height,
        }
    }

    pub fn pixel_at(&self, x: u32, y: u32) -> u32 {
        self.buffer[(y * self.width + x) as usize]
    }
}

Best Practices

  1. Minimize state changes - Batch similar primitives together
  2. Use texture atlases - Single bind for all glyphs
  3. Prefer SDF shapes - Resolution-independent, GPU-friendly
  4. Sort transparent objects - Back-to-front for correct blending
  5. Reuse buffers - Resize rather than reallocate

Verified Test

#[test]
fn test_gpu_rendering_batching() {
    // Batching reduces draw calls
    struct Batch {
        commands: Vec<DrawCommand>,
    }

    impl Batch {
        fn new() -> Self {
            Self { commands: vec![] }
        }

        fn add(&mut self, cmd: DrawCommand) {
            self.commands.push(cmd);
        }

        fn draw_call_count(&self) -> usize {
            // Group by shape type
            let mut types = std::collections::HashSet::new();
            for cmd in &self.commands {
                types.insert(cmd.shape_type());
            }
            types.len()
        }
    }

    #[derive(Clone)]
    enum DrawCommand {
        Rect,
        Circle,
        Text,
    }

    impl DrawCommand {
        fn shape_type(&self) -> u32 {
            match self {
                Self::Rect => 0,
                Self::Circle => 1,
                Self::Text => 2,
            }
        }
    }

    let mut batch = Batch::new();

    // Add 100 rects - should be 1 draw call
    for _ in 0..100 {
        batch.add(DrawCommand::Rect);
    }
    assert_eq!(batch.draw_call_count(), 1);

    // Add circles - now 2 draw calls
    batch.add(DrawCommand::Circle);
    assert_eq!(batch.draw_call_count(), 2);
}

#[test]
fn test_instance_buffer_layout() {
    // Instance struct layout for GPU
    #[repr(C)]
    struct Instance {
        pos: [f32; 2],
        size: [f32; 2],
        color: [f32; 4],
        corner_radius: f32,
        shape_type: u32,
    }

    // Verify alignment (important for GPU buffers)
    assert_eq!(std::mem::size_of::<Instance>(), 40);
    assert_eq!(std::mem::align_of::<Instance>(), 4);

    let instance = Instance {
        pos: [100.0, 200.0],
        size: [50.0, 30.0],
        color: [1.0, 0.0, 0.0, 1.0],
        corner_radius: 5.0,
        shape_type: 0,
    };

    assert_eq!(instance.pos, [100.0, 200.0]);
    assert_eq!(instance.shape_type, 0);
}