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
| Operation | CPU Only | GPU Accelerated | Speedup |
|---|---|---|---|
| 1000 rectangles | 5ms | 0.5ms | 10x |
| 100 text glyphs | 10ms | 1ms | 10x |
| Full frame (complex UI) | 15ms | 2ms | 7.5x |
| 10000 rectangles | 50ms | 1ms | 50x |
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
- Minimize state changes - Batch similar primitives together
- Use texture atlases - Single bind for all glyphs
- Prefer SDF shapes - Resolution-independent, GPU-friendly
- Sort transparent objects - Back-to-front for correct blending
- 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);
}