Skip to main content

soul_terminal_render/
wgpu_backend.rs

1use soul_terminal_core::{Color, RenderCommand};
2use wgpu::util::DeviceExt;
3
4use crate::backend::{RenderBackend, RenderError};
5use crate::glyph::GlyphPipeline;
6use crate::quad::{QuadPipeline, QuadUniforms};
7use crate::texture::TexturePipeline;
8
9/// wgpu-based render backend. Works with WebGL2/WebGPU in browser,
10/// Vulkan/Metal/DX12 natively.
11pub struct WgpuBackend {
12    device: wgpu::Device,
13    queue: wgpu::Queue,
14    surface: wgpu::Surface<'static>,
15    surface_config: wgpu::SurfaceConfiguration,
16    quad_pipeline: QuadPipeline,
17    glyph_pipeline: GlyphPipeline,
18    texture_pipeline: TexturePipeline,
19    width: u32,
20    height: u32,
21    scale_factor: f32,
22    current_texture: Option<wgpu::SurfaceTexture>,
23}
24
25impl WgpuBackend {
26    /// Create a new WgpuBackend from an existing wgpu surface + adapter.
27    pub async fn new(
28        surface: wgpu::Surface<'static>,
29        adapter: &wgpu::Adapter,
30        width: u32,
31        height: u32,
32        scale_factor: f32,
33    ) -> Result<Self, RenderError> {
34        let (device, queue) = adapter
35            .request_device(&wgpu::DeviceDescriptor {
36                label: Some("soul_terminal_device"),
37                required_features: wgpu::Features::empty(),
38                required_limits: wgpu::Limits::downlevel_webgl2_defaults()
39                    .using_resolution(adapter.limits()),
40                memory_hints: wgpu::MemoryHints::default(),
41                trace: wgpu::Trace::Off,
42            })
43            .await
44            .map_err(|e| RenderError::DeviceError(e.to_string()))?;
45
46        let surface_caps = surface.get_capabilities(adapter);
47        let format = surface_caps
48            .formats
49            .iter()
50            .find(|f| f.is_srgb())
51            .copied()
52            .unwrap_or(surface_caps.formats[0]);
53
54        let surface_config = wgpu::SurfaceConfiguration {
55            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
56            format,
57            width,
58            height,
59            present_mode: wgpu::PresentMode::AutoVsync,
60            alpha_mode: surface_caps.alpha_modes[0],
61            view_formats: vec![],
62            desired_maximum_frame_latency: 2,
63        };
64        surface.configure(&device, &surface_config);
65
66        let quad_pipeline = QuadPipeline::new(&device, format);
67        let glyph_pipeline = GlyphPipeline::new(&device, &queue, format);
68        let texture_pipeline = TexturePipeline::new(&device);
69
70        Ok(Self {
71            device,
72            queue,
73            surface,
74            surface_config,
75            quad_pipeline,
76            glyph_pipeline,
77            texture_pipeline,
78            width,
79            height,
80            scale_factor,
81            current_texture: None,
82        })
83    }
84
85    fn process_command(&mut self, cmd: &RenderCommand) {
86        match cmd {
87            RenderCommand::FillRect {
88                rect,
89                color,
90                corner_radius,
91            } => {
92                self.quad_pipeline.push_rect(
93                    rect.x,
94                    rect.y,
95                    rect.width,
96                    rect.height,
97                    color.to_array(),
98                    *corner_radius,
99                );
100            }
101            RenderCommand::DrawBorder { rect, border } => {
102                let w = border.width;
103                let c = border.color.to_array();
104                let r = border.radius;
105                // Top
106                self.quad_pipeline
107                    .push_rect(rect.x, rect.y, rect.width, w, c, r);
108                // Bottom
109                self.quad_pipeline.push_rect(
110                    rect.x,
111                    rect.y + rect.height - w,
112                    rect.width,
113                    w,
114                    c,
115                    r,
116                );
117                // Left
118                self.quad_pipeline
119                    .push_rect(rect.x, rect.y, w, rect.height, c, 0.0);
120                // Right
121                self.quad_pipeline.push_rect(
122                    rect.x + rect.width - w,
123                    rect.y,
124                    w,
125                    rect.height,
126                    c,
127                    0.0,
128                );
129            }
130            RenderCommand::DrawText {
131                text,
132                x,
133                y,
134                color,
135                font_size,
136                bold,
137                italic,
138                monospace,
139            } => {
140                self.glyph_pipeline.push_text(
141                    text,
142                    *x,
143                    *y,
144                    color.to_array(),
145                    *font_size,
146                    *bold,
147                    *italic,
148                    *monospace,
149                );
150            }
151            RenderCommand::DrawShadow {
152                rect,
153                color,
154                offset_x,
155                offset_y,
156                blur_radius,
157                corner_radius,
158            } => {
159                // Approximate shadow with expanded semi-transparent rect
160                let expand = *blur_radius;
161                self.quad_pipeline.push_rect(
162                    rect.x + offset_x - expand,
163                    rect.y + offset_y - expand,
164                    rect.width + expand * 2.0,
165                    rect.height + expand * 2.0,
166                    color.to_array(),
167                    corner_radius + expand,
168                );
169            }
170            RenderCommand::DrawImage {
171                rect: _,
172                texture_id: _,
173            } => {
174                // Image rendering uses texture pipeline — handled separately
175            }
176            RenderCommand::DrawLine {
177                x1,
178                y1,
179                x2,
180                y2,
181                color,
182                width,
183            } => {
184                // Approximate line with a thin rect
185                let dx = x2 - x1;
186                let dy = y2 - y1;
187                let len = (dx * dx + dy * dy).sqrt();
188                if len > 0.0 {
189                    if dy.abs() < 0.001 {
190                        // Horizontal line
191                        self.quad_pipeline
192                            .push_rect(*x1, *y1 - width / 2.0, len, *width, color.to_array(), 0.0);
193                    } else if dx.abs() < 0.001 {
194                        // Vertical line
195                        self.quad_pipeline
196                            .push_rect(*x1 - width / 2.0, *y1, *width, len, color.to_array(), 0.0);
197                    } else {
198                        // Diagonal — just draw a rect (simplified)
199                        self.quad_pipeline
200                            .push_rect(*x1, *y1, dx.abs(), dy.abs(), color.to_array(), 0.0);
201                    }
202                }
203            }
204            RenderCommand::PushClip { .. } | RenderCommand::PopClip => {
205                // Scissor rect management — handled at render pass level
206            }
207        }
208    }
209}
210
211impl RenderBackend for WgpuBackend {
212    fn begin_frame(&mut self, _clear_color: Color) -> Result<(), RenderError> {
213        self.quad_pipeline.clear();
214        self.glyph_pipeline.clear();
215
216        let output = self
217            .surface
218            .get_current_texture()
219            .map_err(|e| RenderError::SurfaceError(e.to_string()))?;
220
221        self.current_texture = Some(output);
222        Ok(())
223    }
224
225    fn submit(&mut self, commands: &[RenderCommand]) -> Result<(), RenderError> {
226        for cmd in commands {
227            self.process_command(cmd);
228        }
229        Ok(())
230    }
231
232    fn present(&mut self) -> Result<(), RenderError> {
233        let output = self
234            .current_texture
235            .take()
236            .ok_or_else(|| RenderError::SurfaceError("no current texture".into()))?;
237
238        let view = output
239            .texture
240            .create_view(&wgpu::TextureViewDescriptor::default());
241
242        // Update uniform buffer with current screen size
243        self.queue.write_buffer(
244            &self.quad_pipeline.uniform_buffer,
245            0,
246            bytemuck::cast_slice(&[QuadUniforms {
247                screen_size: [self.width as f32, self.height as f32],
248                _padding: [0.0; 2],
249            }]),
250        );
251
252        // Create vertex/index buffers for quads
253        let vertex_buffer;
254        let index_buffer;
255        let has_quads = !self.quad_pipeline.vertices.is_empty();
256
257        if has_quads {
258            vertex_buffer =
259                self.device
260                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
261                        label: Some("quad_vertex_buffer"),
262                        contents: bytemuck::cast_slice(&self.quad_pipeline.vertices),
263                        usage: wgpu::BufferUsages::VERTEX,
264                    });
265            index_buffer =
266                self.device
267                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
268                        label: Some("quad_index_buffer"),
269                        contents: bytemuck::cast_slice(&self.quad_pipeline.indices),
270                        usage: wgpu::BufferUsages::INDEX,
271                    });
272        } else {
273            // Dummy — won't be used
274            vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
275                label: None,
276                size: 4,
277                usage: wgpu::BufferUsages::VERTEX,
278                mapped_at_creation: false,
279            });
280            index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
281                label: None,
282                size: 4,
283                usage: wgpu::BufferUsages::INDEX,
284                mapped_at_creation: false,
285            });
286        }
287
288        // Prepare text
289        self.glyph_pipeline
290            .prepare(&self.device, &self.queue, self.width, self.height);
291
292        let mut encoder = self
293            .device
294            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
295                label: Some("soul_terminal_encoder"),
296            });
297
298        {
299            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
300                label: Some("soul_terminal_render_pass"),
301                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
302                    view: &view,
303                    resolve_target: None,
304                    ops: wgpu::Operations {
305                        load: wgpu::LoadOp::Clear(wgpu::Color {
306                            r: 0.04,
307                            g: 0.04,
308                            b: 0.06,
309                            a: 1.0,
310                        }),
311                        store: wgpu::StoreOp::Store,
312                    },
313                })],
314                depth_stencil_attachment: None,
315                timestamp_writes: None,
316                occlusion_query_set: None,
317            });
318
319            // Draw quads
320            if has_quads {
321                render_pass.set_pipeline(&self.quad_pipeline.pipeline);
322                render_pass.set_bind_group(0, &self.quad_pipeline.uniform_bind_group, &[]);
323                render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
324                render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
325                render_pass.draw_indexed(0..self.quad_pipeline.indices.len() as u32, 0, 0..1);
326            }
327
328            // Draw text
329            self.glyph_pipeline.render(&mut render_pass);
330        }
331
332        self.queue.submit(std::iter::once(encoder.finish()));
333        output.present();
334
335        self.glyph_pipeline.trim();
336
337        Ok(())
338    }
339
340    fn resize(&mut self, width: u32, height: u32) {
341        if width > 0 && height > 0 {
342            self.width = width;
343            self.height = height;
344            self.surface_config.width = width;
345            self.surface_config.height = height;
346            self.surface.configure(&self.device, &self.surface_config);
347        }
348    }
349
350    fn load_texture(&mut self, width: u32, height: u32, data: &[u8]) -> Result<u64, RenderError> {
351        self.texture_pipeline
352            .load(&self.device, &self.queue, width, height, data)
353    }
354
355    fn size(&self) -> (u32, u32) {
356        (self.width, self.height)
357    }
358
359    fn scale_factor(&self) -> f32 {
360        self.scale_factor
361    }
362}