Skip to main content

foundry_runtime/
render.rs

1use wgpu::util::DeviceExt;
2
3use crate::scene::*;
4
5#[repr(C)]
6#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
7struct QuadVertex {
8    position: [f32; 2],
9    color: [f32; 4],
10    border_color: [f32; 4],
11    rect: [f32; 4], // x, y, width, height in pixels
12    border_radius: [f32; 4],
13    border_width: f32,
14    _padding: [f32; 3],
15}
16
17#[repr(C)]
18#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
19struct Viewport {
20    size: [f32; 2],
21    _pad: [f32; 2],
22}
23
24pub struct Renderer {
25    pub device: wgpu::Device,
26    pub queue: wgpu::Queue,
27    surface: wgpu::Surface<'static>,
28    config: wgpu::SurfaceConfiguration,
29    quad_pipeline: wgpu::RenderPipeline,
30    viewport_buffer: wgpu::Buffer,
31    viewport_bind_group: wgpu::BindGroup,
32    viewport: [f32; 2],
33    pub format: wgpu::TextureFormat,
34}
35
36impl Renderer {
37    pub async fn new(window: std::sync::Arc<winit::window::Window>) -> Self {
38        let size = window.inner_size();
39
40        let instance = wgpu::Instance::default();
41
42        let surface = instance.create_surface(window.clone()).unwrap();
43
44        let adapter = instance
45            .request_adapter(&wgpu::RequestAdapterOptions {
46                power_preference: wgpu::PowerPreference::HighPerformance,
47                compatible_surface: Some(&surface),
48                force_fallback_adapter: false,
49            })
50            .await
51            .unwrap();
52
53        let (device, queue) = adapter
54            .request_device(&wgpu::DeviceDescriptor {
55                label: Some("foundry"),
56                required_features: wgpu::Features::empty(),
57                required_limits: wgpu::Limits::default(),
58                memory_hints: wgpu::MemoryHints::Performance,
59                trace: wgpu::Trace::Off,
60                experimental_features: wgpu::ExperimentalFeatures::default(),
61            })
62            .await
63            .unwrap();
64
65        let surface_caps = surface.get_capabilities(&adapter);
66        let format = surface_caps
67            .formats
68            .iter()
69            .find(|f| f.is_srgb())
70            .copied()
71            .unwrap_or(surface_caps.formats[0]);
72
73        let config = wgpu::SurfaceConfiguration {
74            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
75            format,
76            width: size.width.max(1),
77            height: size.height.max(1),
78            present_mode: wgpu::PresentMode::AutoVsync,
79            alpha_mode: surface_caps.alpha_modes[0],
80            view_formats: vec![],
81            desired_maximum_frame_latency: 2,
82        };
83        surface.configure(&device, &config);
84
85        // Viewport uniform buffer
86        let viewport_data = Viewport {
87            size: [size.width as f32, size.height as f32],
88            _pad: [0.0; 2],
89        };
90        let viewport_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
91            label: Some("viewport_uniform"),
92            contents: bytemuck::cast_slice(&[viewport_data]),
93            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
94        });
95
96        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
97            label: Some("viewport_layout"),
98            entries: &[wgpu::BindGroupLayoutEntry {
99                binding: 0,
100                visibility: wgpu::ShaderStages::VERTEX,
101                ty: wgpu::BindingType::Buffer {
102                    ty: wgpu::BufferBindingType::Uniform,
103                    has_dynamic_offset: false,
104                    min_binding_size: None,
105                },
106                count: None,
107            }],
108        });
109
110        let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
111            label: Some("viewport_bind_group"),
112            layout: &bind_group_layout,
113            entries: &[wgpu::BindGroupEntry {
114                binding: 0,
115                resource: viewport_buffer.as_entire_binding(),
116            }],
117        });
118
119        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
120            label: Some("quad_shader"),
121            source: wgpu::ShaderSource::Wgsl(QUAD_SHADER.into()),
122        });
123
124        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
125            label: Some("quad_pipeline_layout"),
126            bind_group_layouts: &[Some(&bind_group_layout)],
127            ..Default::default()
128        });
129
130        let quad_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
131            label: Some("quad_pipeline"),
132            layout: Some(&pipeline_layout),
133            vertex: wgpu::VertexState {
134                module: &shader,
135                entry_point: Some("vs_main"),
136                buffers: &[wgpu::VertexBufferLayout {
137                    array_stride: std::mem::size_of::<QuadVertex>() as u64,
138                    step_mode: wgpu::VertexStepMode::Vertex,
139                    attributes: &wgpu::vertex_attr_array![
140                        0 => Float32x2,  // position
141                        1 => Float32x4,  // color
142                        2 => Float32x4,  // border_color
143                        3 => Float32x4,  // rect
144                        4 => Float32x4,  // border_radius
145                        5 => Float32,    // border_width
146                    ],
147                }],
148                compilation_options: Default::default(),
149            },
150            fragment: Some(wgpu::FragmentState {
151                module: &shader,
152                entry_point: Some("fs_main"),
153                targets: &[Some(wgpu::ColorTargetState {
154                    format,
155                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
156                    write_mask: wgpu::ColorWrites::ALL,
157                })],
158                compilation_options: Default::default(),
159            }),
160            primitive: wgpu::PrimitiveState {
161                topology: wgpu::PrimitiveTopology::TriangleList,
162                ..Default::default()
163            },
164            depth_stencil: None,
165            multisample: wgpu::MultisampleState::default(),
166            multiview_mask: None,
167            cache: None,
168        });
169
170        Self {
171            device,
172            queue,
173            surface,
174            config,
175            quad_pipeline,
176            viewport_buffer,
177            viewport_bind_group,
178            viewport: [size.width as f32, size.height as f32],
179            format,
180        }
181    }
182
183    pub fn resize(&mut self, width: u32, height: u32) {
184        if width > 0 && height > 0 {
185            self.config.width = width;
186            self.config.height = height;
187            self.surface.configure(&self.device, &self.config);
188            self.viewport = [width as f32, height as f32];
189
190            let viewport_data = Viewport {
191                size: self.viewport,
192                _pad: [0.0; 2],
193            };
194            self.queue.write_buffer(
195                &self.viewport_buffer,
196                0,
197                bytemuck::cast_slice(&[viewport_data]),
198            );
199        }
200    }
201
202    pub fn viewport_size(&self) -> (f32, f32) {
203        (self.viewport[0], self.viewport[1])
204    }
205
206    pub fn render(
207        &mut self,
208        scene: &SceneGraph,
209        mut text_engine: Option<&mut crate::text::TextEngine>,
210    ) -> Result<(), Box<dyn std::error::Error>> {
211        let output = match self.surface.get_current_texture() {
212            wgpu::CurrentSurfaceTexture::Success(tex)
213            | wgpu::CurrentSurfaceTexture::Suboptimal(tex) => tex,
214            other => {
215                return Err(format!("surface error: {:?}", other).into());
216            }
217        };
218        let view = output.texture.create_view(&Default::default());
219
220        if let Some(te) = text_engine.as_mut() {
221            te.prepare(
222                scene,
223                &self.device,
224                &self.queue,
225                self.config.width,
226                self.config.height,
227            );
228        }
229
230        let mut vertices = Vec::new();
231        let mut indices: Vec<u32> = Vec::new();
232
233        if let Some(root) = scene.root {
234            self.collect_quads(scene, root, &mut vertices, &mut indices, 0.0, 0.0);
235        }
236
237        let vertex_buffer = self
238            .device
239            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
240                label: Some("quad_vertices"),
241                contents: bytemuck::cast_slice(&vertices),
242                usage: wgpu::BufferUsages::VERTEX,
243            });
244
245        let index_buffer = self
246            .device
247            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
248                label: Some("quad_indices"),
249                contents: bytemuck::cast_slice(&indices),
250                usage: wgpu::BufferUsages::INDEX,
251            });
252
253        let mut encoder = self
254            .device
255            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
256                label: Some("render_encoder"),
257            });
258
259        // Use root node's background color as clear color (or white fallback)
260        let clear_color = scene
261            .root
262            .map(|r| {
263                let bg = &scene.get(r).style.background_color;
264                if bg.a > 0.0 {
265                    return wgpu::Color {
266                        r: bg.r as f64,
267                        g: bg.g as f64,
268                        b: bg.b as f64,
269                        a: bg.a as f64,
270                    };
271                }
272                // Check body (first child of root)
273                for &child in &scene.get(r).children {
274                    let cbg = &scene.get(child).style.background_color;
275                    if cbg.a > 0.0 {
276                        return wgpu::Color {
277                            r: cbg.r as f64,
278                            g: cbg.g as f64,
279                            b: cbg.b as f64,
280                            a: cbg.a as f64,
281                        };
282                    }
283                }
284                wgpu::Color {
285                    r: 1.0,
286                    g: 1.0,
287                    b: 1.0,
288                    a: 1.0,
289                }
290            })
291            .unwrap_or(wgpu::Color {
292                r: 1.0,
293                g: 1.0,
294                b: 1.0,
295                a: 1.0,
296            });
297
298        {
299            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
300                label: Some("main_pass"),
301                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
302                    view: &view,
303                    resolve_target: None,
304                    ops: wgpu::Operations {
305                        load: wgpu::LoadOp::Clear(clear_color),
306                        store: wgpu::StoreOp::Store,
307                    },
308                    depth_slice: None,
309                })],
310                depth_stencil_attachment: None,
311                timestamp_writes: None,
312                occlusion_query_set: None,
313                multiview_mask: None,
314            });
315
316            if !indices.is_empty() {
317                pass.set_pipeline(&self.quad_pipeline);
318                pass.set_bind_group(0, &self.viewport_bind_group, &[]);
319                pass.set_vertex_buffer(0, vertex_buffer.slice(..));
320                pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
321                pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
322            }
323
324            if let Some(te) = text_engine.as_ref() {
325                te.render(&mut pass);
326            }
327        }
328
329        self.queue.submit(std::iter::once(encoder.finish()));
330        output.present();
331
332        Ok(())
333    }
334
335    fn collect_quads(
336        &self,
337        scene: &SceneGraph,
338        node_id: NodeId,
339        vertices: &mut Vec<QuadVertex>,
340        indices: &mut Vec<u32>,
341        scroll_x: f32,
342        scroll_y: f32,
343    ) {
344        let node = scene.get(node_id);
345
346        if node.style.display == Display::None {
347            return;
348        }
349
350        let layout = &node.layout;
351        let x = layout.x - scroll_x;
352        let y = layout.y - scroll_y;
353        let w = layout.width;
354        let h = layout.height;
355
356        if w <= 0.0 || h <= 0.0 {
357            let sx = scroll_x + node.scroll_offset.0;
358            let sy = scroll_y + node.scroll_offset.1;
359            for &child_id in &node.children {
360                self.collect_quads(scene, child_id, vertices, indices, sx, sy);
361            }
362            return;
363        }
364
365        let bg = node.style.background_color;
366        let has_bg = bg.a > 0.0;
367        let has_border = node.style.border_width.iter().any(|&w| w > 0.0);
368
369        if has_bg || has_border {
370            let base_idx = vertices.len() as u32;
371
372            let color = [bg.r, bg.g, bg.b, bg.a * node.style.opacity];
373            let bc = node.style.border_color;
374            let border_color = [bc.r, bc.g, bc.b, bc.a];
375            let rect = [x, y, w, h];
376            let border_radius = node.style.border_radius;
377            let border_width = node.style.border_width[0];
378
379            let corners = [[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
380
381            for pos in corners {
382                vertices.push(QuadVertex {
383                    position: pos,
384                    color,
385                    border_color,
386                    rect,
387                    border_radius,
388                    border_width,
389                    _padding: [0.0; 3],
390                });
391            }
392
393            indices.extend_from_slice(&[
394                base_idx,
395                base_idx + 1,
396                base_idx + 2,
397                base_idx,
398                base_idx + 2,
399                base_idx + 3,
400            ]);
401        }
402
403        let sx = scroll_x + node.scroll_offset.0;
404        let sy = scroll_y + node.scroll_offset.1;
405        for &child_id in &node.children {
406            self.collect_quads(scene, child_id, vertices, indices, sx, sy);
407        }
408    }
409}
410
411const QUAD_SHADER: &str = r#"
412struct Viewport {
413    size: vec2<f32>,
414    _pad: vec2<f32>,
415};
416
417@group(0) @binding(0)
418var<uniform> viewport: Viewport;
419
420struct VertexInput {
421    @location(0) position: vec2<f32>,
422    @location(1) color: vec4<f32>,
423    @location(2) border_color: vec4<f32>,
424    @location(3) rect: vec4<f32>,
425    @location(4) border_radius: vec4<f32>,
426    @location(5) border_width: f32,
427};
428
429struct VertexOutput {
430    @builtin(position) clip_position: vec4<f32>,
431    @location(0) color: vec4<f32>,
432    @location(1) border_color: vec4<f32>,
433    @location(2) pixel_pos: vec2<f32>,
434    @location(3) rect: vec4<f32>,
435    @location(4) border_radius: vec4<f32>,
436    @location(5) border_width: f32,
437};
438
439@vertex
440fn vs_main(in: VertexInput) -> VertexOutput {
441    var out: VertexOutput;
442    let x = (in.position.x / viewport.size.x) * 2.0 - 1.0;
443    let y = 1.0 - (in.position.y / viewport.size.y) * 2.0;
444    out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
445    out.color = in.color;
446    out.border_color = in.border_color;
447    out.pixel_pos = in.position;
448    out.rect = in.rect;
449    out.border_radius = in.border_radius;
450    out.border_width = in.border_width;
451    return out;
452}
453
454fn rounded_rect_sdf(pixel: vec2<f32>, rect: vec4<f32>, radius: vec4<f32>) -> f32 {
455    let center = vec2<f32>(rect.x + rect.z * 0.5, rect.y + rect.w * 0.5);
456    let half_size = vec2<f32>(rect.z * 0.5, rect.w * 0.5);
457    let p = pixel - center;
458
459    var r: f32;
460    if p.x < 0.0 {
461        if p.y < 0.0 { r = radius.x; }
462        else { r = radius.w; }
463    } else {
464        if p.y < 0.0 { r = radius.y; }
465        else { r = radius.z; }
466    }
467
468    let q = abs(p) - half_size + vec2<f32>(r, r);
469    return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0, 0.0))) - r;
470}
471
472@fragment
473fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
474    let d = rounded_rect_sdf(in.pixel_pos, in.rect, in.border_radius);
475
476    let aa = 1.0;
477    let alpha = 1.0 - smoothstep(-aa, aa, d);
478
479    if alpha < 0.001 {
480        discard;
481    }
482
483    if in.border_width > 0.0 {
484        let inner_d = rounded_rect_sdf(
485            in.pixel_pos,
486            vec4<f32>(
487                in.rect.x + in.border_width,
488                in.rect.y + in.border_width,
489                in.rect.z - in.border_width * 2.0,
490                in.rect.w - in.border_width * 2.0,
491            ),
492            max(in.border_radius - vec4<f32>(in.border_width), vec4<f32>(0.0)),
493        );
494
495        let border_alpha = smoothstep(-aa, aa, inner_d);
496        let color = mix(in.color, in.border_color, border_alpha);
497        return vec4<f32>(color.rgb, color.a * alpha);
498    }
499
500    return vec4<f32>(in.color.rgb, in.color.a * alpha);
501}
502"#;