soul-terminal-render 0.1.0

GPU rendering backend for soul-terminal (wgpu)
Documentation
use soul_terminal_core::{Color, RenderCommand};
use wgpu::util::DeviceExt;

use crate::backend::{RenderBackend, RenderError};
use crate::glyph::GlyphPipeline;
use crate::quad::{QuadPipeline, QuadUniforms};
use crate::texture::TexturePipeline;

/// wgpu-based render backend. Works with WebGL2/WebGPU in browser,
/// Vulkan/Metal/DX12 natively.
pub struct WgpuBackend {
    device: wgpu::Device,
    queue: wgpu::Queue,
    surface: wgpu::Surface<'static>,
    surface_config: wgpu::SurfaceConfiguration,
    quad_pipeline: QuadPipeline,
    glyph_pipeline: GlyphPipeline,
    texture_pipeline: TexturePipeline,
    width: u32,
    height: u32,
    scale_factor: f32,
    current_texture: Option<wgpu::SurfaceTexture>,
}

impl WgpuBackend {
    /// Create a new WgpuBackend from an existing wgpu surface + adapter.
    pub async fn new(
        surface: wgpu::Surface<'static>,
        adapter: &wgpu::Adapter,
        width: u32,
        height: u32,
        scale_factor: f32,
    ) -> Result<Self, RenderError> {
        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor {
                label: Some("soul_terminal_device"),
                required_features: wgpu::Features::empty(),
                required_limits: wgpu::Limits::downlevel_webgl2_defaults()
                    .using_resolution(adapter.limits()),
                memory_hints: wgpu::MemoryHints::default(),
                trace: wgpu::Trace::Off,
            })
            .await
            .map_err(|e| RenderError::DeviceError(e.to_string()))?;

        let surface_caps = surface.get_capabilities(adapter);
        let format = surface_caps
            .formats
            .iter()
            .find(|f| f.is_srgb())
            .copied()
            .unwrap_or(surface_caps.formats[0]);

        let surface_config = wgpu::SurfaceConfiguration {
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            format,
            width,
            height,
            present_mode: wgpu::PresentMode::AutoVsync,
            alpha_mode: surface_caps.alpha_modes[0],
            view_formats: vec![],
            desired_maximum_frame_latency: 2,
        };
        surface.configure(&device, &surface_config);

        let quad_pipeline = QuadPipeline::new(&device, format);
        let glyph_pipeline = GlyphPipeline::new(&device, &queue, format);
        let texture_pipeline = TexturePipeline::new(&device);

        Ok(Self {
            device,
            queue,
            surface,
            surface_config,
            quad_pipeline,
            glyph_pipeline,
            texture_pipeline,
            width,
            height,
            scale_factor,
            current_texture: None,
        })
    }

    fn process_command(&mut self, cmd: &RenderCommand) {
        match cmd {
            RenderCommand::FillRect {
                rect,
                color,
                corner_radius,
            } => {
                self.quad_pipeline.push_rect(
                    rect.x,
                    rect.y,
                    rect.width,
                    rect.height,
                    color.to_array(),
                    *corner_radius,
                );
            }
            RenderCommand::DrawBorder { rect, border } => {
                let w = border.width;
                let c = border.color.to_array();
                let r = border.radius;
                // Top
                self.quad_pipeline
                    .push_rect(rect.x, rect.y, rect.width, w, c, r);
                // Bottom
                self.quad_pipeline.push_rect(
                    rect.x,
                    rect.y + rect.height - w,
                    rect.width,
                    w,
                    c,
                    r,
                );
                // Left
                self.quad_pipeline
                    .push_rect(rect.x, rect.y, w, rect.height, c, 0.0);
                // Right
                self.quad_pipeline.push_rect(
                    rect.x + rect.width - w,
                    rect.y,
                    w,
                    rect.height,
                    c,
                    0.0,
                );
            }
            RenderCommand::DrawText {
                text,
                x,
                y,
                color,
                font_size,
                bold,
                italic,
                monospace,
            } => {
                self.glyph_pipeline.push_text(
                    text,
                    *x,
                    *y,
                    color.to_array(),
                    *font_size,
                    *bold,
                    *italic,
                    *monospace,
                );
            }
            RenderCommand::DrawShadow {
                rect,
                color,
                offset_x,
                offset_y,
                blur_radius,
                corner_radius,
            } => {
                // Approximate shadow with expanded semi-transparent rect
                let expand = *blur_radius;
                self.quad_pipeline.push_rect(
                    rect.x + offset_x - expand,
                    rect.y + offset_y - expand,
                    rect.width + expand * 2.0,
                    rect.height + expand * 2.0,
                    color.to_array(),
                    corner_radius + expand,
                );
            }
            RenderCommand::DrawImage {
                rect: _,
                texture_id: _,
            } => {
                // Image rendering uses texture pipeline — handled separately
            }
            RenderCommand::DrawLine {
                x1,
                y1,
                x2,
                y2,
                color,
                width,
            } => {
                // Approximate line with a thin rect
                let dx = x2 - x1;
                let dy = y2 - y1;
                let len = (dx * dx + dy * dy).sqrt();
                if len > 0.0 {
                    if dy.abs() < 0.001 {
                        // Horizontal line
                        self.quad_pipeline
                            .push_rect(*x1, *y1 - width / 2.0, len, *width, color.to_array(), 0.0);
                    } else if dx.abs() < 0.001 {
                        // Vertical line
                        self.quad_pipeline
                            .push_rect(*x1 - width / 2.0, *y1, *width, len, color.to_array(), 0.0);
                    } else {
                        // Diagonal — just draw a rect (simplified)
                        self.quad_pipeline
                            .push_rect(*x1, *y1, dx.abs(), dy.abs(), color.to_array(), 0.0);
                    }
                }
            }
            RenderCommand::PushClip { .. } | RenderCommand::PopClip => {
                // Scissor rect management — handled at render pass level
            }
        }
    }
}

impl RenderBackend for WgpuBackend {
    fn begin_frame(&mut self, _clear_color: Color) -> Result<(), RenderError> {
        self.quad_pipeline.clear();
        self.glyph_pipeline.clear();

        let output = self
            .surface
            .get_current_texture()
            .map_err(|e| RenderError::SurfaceError(e.to_string()))?;

        self.current_texture = Some(output);
        Ok(())
    }

    fn submit(&mut self, commands: &[RenderCommand]) -> Result<(), RenderError> {
        for cmd in commands {
            self.process_command(cmd);
        }
        Ok(())
    }

    fn present(&mut self) -> Result<(), RenderError> {
        let output = self
            .current_texture
            .take()
            .ok_or_else(|| RenderError::SurfaceError("no current texture".into()))?;

        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());

        // Update uniform buffer with current screen size
        self.queue.write_buffer(
            &self.quad_pipeline.uniform_buffer,
            0,
            bytemuck::cast_slice(&[QuadUniforms {
                screen_size: [self.width as f32, self.height as f32],
                _padding: [0.0; 2],
            }]),
        );

        // Create vertex/index buffers for quads
        let vertex_buffer;
        let index_buffer;
        let has_quads = !self.quad_pipeline.vertices.is_empty();

        if has_quads {
            vertex_buffer =
                self.device
                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                        label: Some("quad_vertex_buffer"),
                        contents: bytemuck::cast_slice(&self.quad_pipeline.vertices),
                        usage: wgpu::BufferUsages::VERTEX,
                    });
            index_buffer =
                self.device
                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                        label: Some("quad_index_buffer"),
                        contents: bytemuck::cast_slice(&self.quad_pipeline.indices),
                        usage: wgpu::BufferUsages::INDEX,
                    });
        } else {
            // Dummy — won't be used
            vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
                label: None,
                size: 4,
                usage: wgpu::BufferUsages::VERTEX,
                mapped_at_creation: false,
            });
            index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
                label: None,
                size: 4,
                usage: wgpu::BufferUsages::INDEX,
                mapped_at_creation: false,
            });
        }

        // Prepare text
        self.glyph_pipeline
            .prepare(&self.device, &self.queue, self.width, self.height);

        let mut encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("soul_terminal_encoder"),
            });

        {
            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("soul_terminal_render_pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color {
                            r: 0.04,
                            g: 0.04,
                            b: 0.06,
                            a: 1.0,
                        }),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
            });

            // Draw quads
            if has_quads {
                render_pass.set_pipeline(&self.quad_pipeline.pipeline);
                render_pass.set_bind_group(0, &self.quad_pipeline.uniform_bind_group, &[]);
                render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
                render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
                render_pass.draw_indexed(0..self.quad_pipeline.indices.len() as u32, 0, 0..1);
            }

            // Draw text
            self.glyph_pipeline.render(&mut render_pass);
        }

        self.queue.submit(std::iter::once(encoder.finish()));
        output.present();

        self.glyph_pipeline.trim();

        Ok(())
    }

    fn resize(&mut self, width: u32, height: u32) {
        if width > 0 && height > 0 {
            self.width = width;
            self.height = height;
            self.surface_config.width = width;
            self.surface_config.height = height;
            self.surface.configure(&self.device, &self.surface_config);
        }
    }

    fn load_texture(&mut self, width: u32, height: u32, data: &[u8]) -> Result<u64, RenderError> {
        self.texture_pipeline
            .load(&self.device, &self.queue, width, height, data)
    }

    fn size(&self) -> (u32, u32) {
        (self.width, self.height)
    }

    fn scale_factor(&self) -> f32 {
        self.scale_factor
    }
}