Skip to main content

foundry_runtime/
render.rs

1use wgpu::util::DeviceExt;
2
3use crate::scene::*;
4
5/// One corner of a rounded-rect quad. The fragment shader reconstructs the box
6/// from `rect` + `border_radius` and evaluates a rounded-rect SDF per pixel, so
7/// every vertex of a quad carries identical rect/radius/border data.
8#[repr(C)]
9#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
10pub struct QuadVertex {
11    pub position: [f32; 2],
12    pub color: [f32; 4],
13    pub border_color: [f32; 4],
14    pub rect: [f32; 4], // x, y, width, height in pixels
15    pub border_radius: [f32; 4],
16    pub border_width: f32,
17    pub _padding: [f32; 3],
18}
19
20#[repr(C)]
21#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
22struct Viewport {
23    size: [f32; 2],
24    _pad: [f32; 2],
25}
26
27/// Walk the scene graph and build the draw commands (vertex + index buffers)
28/// for every visible node that paints a background or a border.
29///
30/// This is the whole "scene graph -> draw commands" step and is deliberately
31/// free of GPU state so it can be tested without a device or a surface.
32pub fn build_quads(scene: &SceneGraph) -> (Vec<QuadVertex>, Vec<u32>) {
33    let mut vertices = Vec::new();
34    let mut indices = Vec::new();
35    if let Some(root) = scene.root {
36        collect_quads(scene, root, &mut vertices, &mut indices, 0.0, 0.0);
37    }
38    (vertices, indices)
39}
40
41fn collect_quads(
42    scene: &SceneGraph,
43    node_id: NodeId,
44    vertices: &mut Vec<QuadVertex>,
45    indices: &mut Vec<u32>,
46    scroll_x: f32,
47    scroll_y: f32,
48) {
49    let node = scene.get(node_id);
50
51    if node.style.display == Display::None {
52        return;
53    }
54
55    let layout = &node.layout;
56    let x = layout.x - scroll_x;
57    let y = layout.y - scroll_y;
58    let w = layout.width;
59    let h = layout.height;
60
61    if w <= 0.0 || h <= 0.0 {
62        let sx = scroll_x + node.scroll_offset.0;
63        let sy = scroll_y + node.scroll_offset.1;
64        for &child_id in &node.children {
65            collect_quads(scene, child_id, vertices, indices, sx, sy);
66        }
67        return;
68    }
69
70    let bg = node.style.background_color;
71    let has_bg = bg.a > 0.0;
72    let has_border = node.style.border_width.iter().any(|&w| w > 0.0);
73
74    if has_bg || has_border {
75        let base_idx = vertices.len() as u32;
76
77        let color = [bg.r, bg.g, bg.b, bg.a * node.style.opacity];
78        let bc = node.style.border_color;
79        let border_color = [bc.r, bc.g, bc.b, bc.a];
80        let rect = [x, y, w, h];
81        let border_radius = node.style.border_radius;
82        let border_width = node.style.border_width[0];
83
84        let corners = [[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
85
86        for pos in corners {
87            vertices.push(QuadVertex {
88                position: pos,
89                color,
90                border_color,
91                rect,
92                border_radius,
93                border_width,
94                _padding: [0.0; 3],
95            });
96        }
97
98        indices.extend_from_slice(&[
99            base_idx,
100            base_idx + 1,
101            base_idx + 2,
102            base_idx,
103            base_idx + 2,
104            base_idx + 3,
105        ]);
106    }
107
108    let sx = scroll_x + node.scroll_offset.0;
109    let sy = scroll_y + node.scroll_offset.1;
110    for &child_id in &node.children {
111        collect_quads(scene, child_id, vertices, indices, sx, sy);
112    }
113}
114
115/// Pick the framebuffer clear colour: the root background if it is opaque
116/// enough to matter, else the first child (`<body>`) that has one, else white.
117pub fn clear_color(scene: &SceneGraph) -> wgpu::Color {
118    let white = wgpu::Color {
119        r: 1.0,
120        g: 1.0,
121        b: 1.0,
122        a: 1.0,
123    };
124    let to_wgpu = |c: &Color| wgpu::Color {
125        r: c.r as f64,
126        g: c.g as f64,
127        b: c.b as f64,
128        a: c.a as f64,
129    };
130
131    let Some(root) = scene.root else {
132        return white;
133    };
134    let bg = &scene.get(root).style.background_color;
135    if bg.a > 0.0 {
136        return to_wgpu(bg);
137    }
138    for &child in &scene.get(root).children {
139        let cbg = &scene.get(child).style.background_color;
140        if cbg.a > 0.0 {
141            return to_wgpu(cbg);
142        }
143    }
144    white
145}
146
147/// WGSL source for the rounded-rect SDF quad pipeline. Exposed so tests can
148/// compile it against a headless device.
149pub fn quad_shader_source() -> &'static str {
150    QUAD_SHADER
151}
152
153/// Bind group layout for the viewport uniform consumed by the quad shader.
154pub fn create_viewport_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
155    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
156        label: Some("viewport_layout"),
157        entries: &[wgpu::BindGroupLayoutEntry {
158            binding: 0,
159            visibility: wgpu::ShaderStages::VERTEX,
160            ty: wgpu::BindingType::Buffer {
161                ty: wgpu::BufferBindingType::Uniform,
162                has_dynamic_offset: false,
163                min_binding_size: None,
164            },
165            count: None,
166        }],
167    })
168}
169
170/// Build the rounded-rect SDF quad pipeline. Split out of [`Renderer::new`] so
171/// it can be created against a headless device in tests.
172pub fn create_quad_pipeline(
173    device: &wgpu::Device,
174    bind_group_layout: &wgpu::BindGroupLayout,
175    format: wgpu::TextureFormat,
176) -> wgpu::RenderPipeline {
177    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
178        label: Some("quad_shader"),
179        source: wgpu::ShaderSource::Wgsl(QUAD_SHADER.into()),
180    });
181
182    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
183        label: Some("quad_pipeline_layout"),
184        bind_group_layouts: &[Some(bind_group_layout)],
185        ..Default::default()
186    });
187
188    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
189        label: Some("quad_pipeline"),
190        layout: Some(&pipeline_layout),
191        vertex: wgpu::VertexState {
192            module: &shader,
193            entry_point: Some("vs_main"),
194            buffers: &[wgpu::VertexBufferLayout {
195                array_stride: std::mem::size_of::<QuadVertex>() as u64,
196                step_mode: wgpu::VertexStepMode::Vertex,
197                attributes: &wgpu::vertex_attr_array![
198                    0 => Float32x2,  // position
199                    1 => Float32x4,  // color
200                    2 => Float32x4,  // border_color
201                    3 => Float32x4,  // rect
202                    4 => Float32x4,  // border_radius
203                    5 => Float32,    // border_width
204                ],
205            }],
206            compilation_options: Default::default(),
207        },
208        fragment: Some(wgpu::FragmentState {
209            module: &shader,
210            entry_point: Some("fs_main"),
211            targets: &[Some(wgpu::ColorTargetState {
212                format,
213                blend: Some(wgpu::BlendState::ALPHA_BLENDING),
214                write_mask: wgpu::ColorWrites::ALL,
215            })],
216            compilation_options: Default::default(),
217        }),
218        primitive: wgpu::PrimitiveState {
219            topology: wgpu::PrimitiveTopology::TriangleList,
220            ..Default::default()
221        },
222        depth_stencil: None,
223        multisample: wgpu::MultisampleState::default(),
224        multiview_mask: None,
225        cache: None,
226    })
227}
228
229pub struct Renderer {
230    pub device: wgpu::Device,
231    pub queue: wgpu::Queue,
232    surface: wgpu::Surface<'static>,
233    config: wgpu::SurfaceConfiguration,
234    quad_pipeline: wgpu::RenderPipeline,
235    viewport_buffer: wgpu::Buffer,
236    viewport_bind_group: wgpu::BindGroup,
237    viewport: [f32; 2],
238    pub format: wgpu::TextureFormat,
239}
240
241impl Renderer {
242    pub async fn new(window: std::sync::Arc<winit::window::Window>) -> Self {
243        let size = window.inner_size();
244
245        let instance = wgpu::Instance::default();
246
247        let surface = instance.create_surface(window.clone()).unwrap();
248
249        let adapter = instance
250            .request_adapter(&wgpu::RequestAdapterOptions {
251                power_preference: wgpu::PowerPreference::HighPerformance,
252                compatible_surface: Some(&surface),
253                force_fallback_adapter: false,
254            })
255            .await
256            .unwrap();
257
258        let (device, queue) = adapter
259            .request_device(&wgpu::DeviceDescriptor {
260                label: Some("foundry"),
261                required_features: wgpu::Features::empty(),
262                required_limits: wgpu::Limits::default(),
263                memory_hints: wgpu::MemoryHints::Performance,
264                trace: wgpu::Trace::Off,
265                experimental_features: wgpu::ExperimentalFeatures::default(),
266            })
267            .await
268            .unwrap();
269
270        let surface_caps = surface.get_capabilities(&adapter);
271        let format = surface_caps
272            .formats
273            .iter()
274            .find(|f| f.is_srgb())
275            .copied()
276            .unwrap_or(surface_caps.formats[0]);
277
278        let config = wgpu::SurfaceConfiguration {
279            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
280            format,
281            width: size.width.max(1),
282            height: size.height.max(1),
283            present_mode: wgpu::PresentMode::AutoVsync,
284            alpha_mode: surface_caps.alpha_modes[0],
285            view_formats: vec![],
286            desired_maximum_frame_latency: 2,
287        };
288        surface.configure(&device, &config);
289
290        // Viewport uniform buffer
291        let viewport_data = Viewport {
292            size: [size.width as f32, size.height as f32],
293            _pad: [0.0; 2],
294        };
295        let viewport_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
296            label: Some("viewport_uniform"),
297            contents: bytemuck::cast_slice(&[viewport_data]),
298            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
299        });
300
301        let bind_group_layout = create_viewport_bind_group_layout(&device);
302
303        let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
304            label: Some("viewport_bind_group"),
305            layout: &bind_group_layout,
306            entries: &[wgpu::BindGroupEntry {
307                binding: 0,
308                resource: viewport_buffer.as_entire_binding(),
309            }],
310        });
311
312        let quad_pipeline = create_quad_pipeline(&device, &bind_group_layout, format);
313
314        Self {
315            device,
316            queue,
317            surface,
318            config,
319            quad_pipeline,
320            viewport_buffer,
321            viewport_bind_group,
322            viewport: [size.width as f32, size.height as f32],
323            format,
324        }
325    }
326
327    pub fn resize(&mut self, width: u32, height: u32) {
328        if width > 0 && height > 0 {
329            self.config.width = width;
330            self.config.height = height;
331            self.surface.configure(&self.device, &self.config);
332            self.viewport = [width as f32, height as f32];
333
334            let viewport_data = Viewport {
335                size: self.viewport,
336                _pad: [0.0; 2],
337            };
338            self.queue.write_buffer(
339                &self.viewport_buffer,
340                0,
341                bytemuck::cast_slice(&[viewport_data]),
342            );
343        }
344    }
345
346    pub fn viewport_size(&self) -> (f32, f32) {
347        (self.viewport[0], self.viewport[1])
348    }
349
350    pub fn render(
351        &mut self,
352        scene: &SceneGraph,
353        mut text_engine: Option<&mut crate::text::TextEngine>,
354    ) -> Result<(), Box<dyn std::error::Error>> {
355        let output = match self.surface.get_current_texture() {
356            wgpu::CurrentSurfaceTexture::Success(tex)
357            | wgpu::CurrentSurfaceTexture::Suboptimal(tex) => tex,
358            other => {
359                return Err(format!("surface error: {:?}", other).into());
360            }
361        };
362        let view = output.texture.create_view(&Default::default());
363
364        if let Some(te) = text_engine.as_mut() {
365            te.prepare(
366                scene,
367                &self.device,
368                &self.queue,
369                self.config.width,
370                self.config.height,
371            );
372        }
373
374        let (vertices, indices) = build_quads(scene);
375
376        let vertex_buffer = self
377            .device
378            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
379                label: Some("quad_vertices"),
380                contents: bytemuck::cast_slice(&vertices),
381                usage: wgpu::BufferUsages::VERTEX,
382            });
383
384        let index_buffer = self
385            .device
386            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
387                label: Some("quad_indices"),
388                contents: bytemuck::cast_slice(&indices),
389                usage: wgpu::BufferUsages::INDEX,
390            });
391
392        let mut encoder = self
393            .device
394            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
395                label: Some("render_encoder"),
396            });
397
398        // Use root node's background color as clear color (or white fallback)
399        let clear_color = clear_color(scene);
400
401        {
402            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
403                label: Some("main_pass"),
404                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
405                    view: &view,
406                    resolve_target: None,
407                    ops: wgpu::Operations {
408                        load: wgpu::LoadOp::Clear(clear_color),
409                        store: wgpu::StoreOp::Store,
410                    },
411                    depth_slice: None,
412                })],
413                depth_stencil_attachment: None,
414                timestamp_writes: None,
415                occlusion_query_set: None,
416                multiview_mask: None,
417            });
418
419            if !indices.is_empty() {
420                pass.set_pipeline(&self.quad_pipeline);
421                pass.set_bind_group(0, &self.viewport_bind_group, &[]);
422                pass.set_vertex_buffer(0, vertex_buffer.slice(..));
423                pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
424                pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
425            }
426
427            if let Some(te) = text_engine.as_ref() {
428                te.render(&mut pass);
429            }
430        }
431
432        self.queue.submit(std::iter::once(encoder.finish()));
433        output.present();
434
435        Ok(())
436    }
437}
438
439const QUAD_SHADER: &str = r#"
440struct Viewport {
441    size: vec2<f32>,
442    _pad: vec2<f32>,
443};
444
445@group(0) @binding(0)
446var<uniform> viewport: Viewport;
447
448struct VertexInput {
449    @location(0) position: vec2<f32>,
450    @location(1) color: vec4<f32>,
451    @location(2) border_color: vec4<f32>,
452    @location(3) rect: vec4<f32>,
453    @location(4) border_radius: vec4<f32>,
454    @location(5) border_width: f32,
455};
456
457struct VertexOutput {
458    @builtin(position) clip_position: vec4<f32>,
459    @location(0) color: vec4<f32>,
460    @location(1) border_color: vec4<f32>,
461    @location(2) pixel_pos: vec2<f32>,
462    @location(3) rect: vec4<f32>,
463    @location(4) border_radius: vec4<f32>,
464    @location(5) border_width: f32,
465};
466
467@vertex
468fn vs_main(in: VertexInput) -> VertexOutput {
469    var out: VertexOutput;
470    let x = (in.position.x / viewport.size.x) * 2.0 - 1.0;
471    let y = 1.0 - (in.position.y / viewport.size.y) * 2.0;
472    out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
473    out.color = in.color;
474    out.border_color = in.border_color;
475    out.pixel_pos = in.position;
476    out.rect = in.rect;
477    out.border_radius = in.border_radius;
478    out.border_width = in.border_width;
479    return out;
480}
481
482fn rounded_rect_sdf(pixel: vec2<f32>, rect: vec4<f32>, radius: vec4<f32>) -> f32 {
483    let center = vec2<f32>(rect.x + rect.z * 0.5, rect.y + rect.w * 0.5);
484    let half_size = vec2<f32>(rect.z * 0.5, rect.w * 0.5);
485    let p = pixel - center;
486
487    var r: f32;
488    if p.x < 0.0 {
489        if p.y < 0.0 { r = radius.x; }
490        else { r = radius.w; }
491    } else {
492        if p.y < 0.0 { r = radius.y; }
493        else { r = radius.z; }
494    }
495
496    let q = abs(p) - half_size + vec2<f32>(r, r);
497    return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0, 0.0))) - r;
498}
499
500@fragment
501fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
502    let d = rounded_rect_sdf(in.pixel_pos, in.rect, in.border_radius);
503
504    let aa = 1.0;
505    let alpha = 1.0 - smoothstep(-aa, aa, d);
506
507    if alpha < 0.001 {
508        discard;
509    }
510
511    if in.border_width > 0.0 {
512        let inner_d = rounded_rect_sdf(
513            in.pixel_pos,
514            vec4<f32>(
515                in.rect.x + in.border_width,
516                in.rect.y + in.border_width,
517                in.rect.z - in.border_width * 2.0,
518                in.rect.w - in.border_width * 2.0,
519            ),
520            max(in.border_radius - vec4<f32>(in.border_width), vec4<f32>(0.0)),
521        );
522
523        let border_alpha = smoothstep(-aa, aa, inner_d);
524        let color = mix(in.color, in.border_color, border_alpha);
525        return vec4<f32>(color.rgb, color.a * alpha);
526    }
527
528    return vec4<f32>(in.color.rgb, in.color.a * alpha);
529}
530"#;
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use crate::test_gpu::with_headless_device;
536
537    fn node(scene: &mut SceneGraph, tag: &str, rect: (f32, f32, f32, f32)) -> NodeId {
538        let id = scene.add_node(ElementKind::from_tag(tag), tag.to_string());
539        scene.get_mut(id).layout = LayoutRect {
540            x: rect.0,
541            y: rect.1,
542            width: rect.2,
543            height: rect.3,
544        };
545        id
546    }
547
548    fn opaque(scene: &mut SceneGraph, id: NodeId, r: u8, g: u8, b: u8) {
549        scene.get_mut(id).style.background_color = Color::from_rgba(r, g, b, 1.0);
550    }
551
552    // ---- scene graph -> draw commands ----
553
554    #[test]
555    fn a_painted_node_becomes_one_quad_of_four_vertices_and_two_triangles() {
556        let mut scene = SceneGraph::new();
557        let root = node(&mut scene, "div", (10.0, 20.0, 100.0, 50.0));
558        opaque(&mut scene, root, 255, 0, 0);
559
560        let (vertices, indices) = build_quads(&scene);
561
562        assert_eq!(vertices.len(), 4);
563        assert_eq!(indices, vec![0, 1, 2, 0, 2, 3]);
564        // corners are emitted clockwise from the top-left
565        let positions: Vec<[f32; 2]> = vertices.iter().map(|v| v.position).collect();
566        assert_eq!(
567            positions,
568            vec![[10.0, 20.0], [110.0, 20.0], [110.0, 70.0], [10.0, 70.0]]
569        );
570        // every vertex carries the whole rect so the SDF can be evaluated per pixel
571        assert!(vertices.iter().all(|v| v.rect == [10.0, 20.0, 100.0, 50.0]));
572    }
573
574    #[test]
575    fn a_fully_transparent_node_emits_nothing() {
576        let mut scene = SceneGraph::new();
577        node(&mut scene, "div", (0.0, 0.0, 100.0, 100.0));
578
579        let (vertices, indices) = build_quads(&scene);
580
581        assert!(vertices.is_empty());
582        assert!(indices.is_empty());
583    }
584
585    #[test]
586    fn a_transparent_node_with_a_border_still_emits_a_quad() {
587        let mut scene = SceneGraph::new();
588        let root = node(&mut scene, "div", (0.0, 0.0, 100.0, 100.0));
589        scene.get_mut(root).style.border_width = [2.0; 4];
590        scene.get_mut(root).style.border_color = Color::from_rgba(0, 0, 255, 1.0);
591
592        let (vertices, _) = build_quads(&scene);
593
594        assert_eq!(vertices.len(), 4);
595        assert_eq!(vertices[0].border_width, 2.0);
596        assert_eq!(vertices[0].border_color, [0.0, 0.0, 1.0, 1.0]);
597        assert_eq!(vertices[0].color[3], 0.0, "background stays transparent");
598    }
599
600    #[test]
601    fn a_transparent_parent_does_not_hide_its_painted_children() {
602        let mut scene = SceneGraph::new();
603        let root = node(&mut scene, "div", (0.0, 0.0, 200.0, 200.0));
604        let a = node(&mut scene, "div", (0.0, 0.0, 10.0, 10.0));
605        let b = node(&mut scene, "div", (20.0, 0.0, 10.0, 10.0));
606        scene.add_child(root, a);
607        scene.add_child(root, b);
608        opaque(&mut scene, a, 255, 0, 0);
609        opaque(&mut scene, b, 0, 255, 0);
610
611        let (vertices, indices) = build_quads(&scene);
612
613        assert_eq!(vertices.len(), 8);
614        assert_eq!(indices.len(), 12);
615        // indices of the second quad are offset by the first quad's vertices
616        assert_eq!(&indices[6..], &[4, 5, 6, 4, 6, 7]);
617    }
618
619    #[test]
620    fn children_are_drawn_after_their_parent() {
621        let mut scene = SceneGraph::new();
622        let root = node(&mut scene, "div", (0.0, 0.0, 200.0, 200.0));
623        let child = node(&mut scene, "div", (0.0, 0.0, 10.0, 10.0));
624        scene.add_child(root, child);
625        opaque(&mut scene, root, 255, 0, 0);
626        opaque(&mut scene, child, 0, 255, 0);
627
628        let (vertices, _) = build_quads(&scene);
629
630        assert_eq!(vertices[0].color, [1.0, 0.0, 0.0, 1.0]);
631        assert_eq!(vertices[4].color, [0.0, 1.0, 0.0, 1.0]);
632    }
633
634    #[test]
635    fn display_none_removes_the_whole_subtree_from_the_draw_list() {
636        let mut scene = SceneGraph::new();
637        let root = node(&mut scene, "div", (0.0, 0.0, 200.0, 200.0));
638        let hidden = node(&mut scene, "div", (0.0, 0.0, 50.0, 50.0));
639        let inside = node(&mut scene, "div", (0.0, 0.0, 10.0, 10.0));
640        scene.add_child(root, hidden);
641        scene.add_child(hidden, inside);
642        opaque(&mut scene, root, 255, 0, 0);
643        opaque(&mut scene, hidden, 0, 255, 0);
644        opaque(&mut scene, inside, 0, 0, 255);
645        scene.get_mut(hidden).style.display = Display::None;
646
647        let (vertices, _) = build_quads(&scene);
648
649        assert_eq!(vertices.len(), 4, "only the root should be drawn");
650    }
651
652    #[test]
653    fn a_zero_sized_node_is_skipped_but_its_children_are_not() {
654        let mut scene = SceneGraph::new();
655        let root = node(&mut scene, "div", (0.0, 0.0, 0.0, 0.0));
656        let child = node(&mut scene, "div", (5.0, 5.0, 10.0, 10.0));
657        scene.add_child(root, child);
658        opaque(&mut scene, root, 255, 0, 0);
659        opaque(&mut scene, child, 0, 255, 0);
660
661        let (vertices, _) = build_quads(&scene);
662
663        assert_eq!(vertices.len(), 4);
664        assert_eq!(vertices[0].color, [0.0, 1.0, 0.0, 1.0]);
665    }
666
667    #[test]
668    fn opacity_is_folded_into_the_vertex_alpha() {
669        let mut scene = SceneGraph::new();
670        let root = node(&mut scene, "div", (0.0, 0.0, 10.0, 10.0));
671        scene.get_mut(root).style.background_color = Color::from_rgba(255, 0, 0, 0.5);
672        scene.get_mut(root).style.opacity = 0.5;
673
674        let (vertices, _) = build_quads(&scene);
675
676        assert_eq!(vertices[0].color[3], 0.25);
677    }
678
679    #[test]
680    fn color_channels_are_converted_from_bytes_to_unit_floats() {
681        let mut scene = SceneGraph::new();
682        let root = node(&mut scene, "div", (0.0, 0.0, 10.0, 10.0));
683        scene.get_mut(root).style.background_color = Color::from_rgba(255, 128, 0, 1.0);
684
685        let (vertices, _) = build_quads(&scene);
686
687        assert_eq!(vertices[0].color[0], 1.0);
688        assert!((vertices[0].color[1] - 128.0 / 255.0).abs() < 1e-6);
689        assert_eq!(vertices[0].color[2], 0.0);
690    }
691
692    #[test]
693    fn per_corner_border_radii_are_carried_on_every_vertex() {
694        let mut scene = SceneGraph::new();
695        let root = node(&mut scene, "div", (0.0, 0.0, 40.0, 40.0));
696        opaque(&mut scene, root, 255, 0, 0);
697        // top-left, top-right, bottom-right, bottom-left
698        scene.get_mut(root).style.border_radius = [1.0, 2.0, 3.0, 4.0];
699
700        let (vertices, _) = build_quads(&scene);
701
702        assert!(vertices
703            .iter()
704            .all(|v| v.border_radius == [1.0, 2.0, 3.0, 4.0]));
705    }
706
707    #[test]
708    fn a_scroll_offset_shifts_descendants_but_not_the_container() {
709        let mut scene = SceneGraph::new();
710        let root = node(&mut scene, "div", (0.0, 0.0, 100.0, 100.0));
711        let child = node(&mut scene, "div", (0.0, 60.0, 100.0, 20.0));
712        scene.add_child(root, child);
713        opaque(&mut scene, root, 255, 0, 0);
714        opaque(&mut scene, child, 0, 255, 0);
715        scene.get_mut(root).scroll_offset = (0.0, 25.0);
716
717        let (vertices, _) = build_quads(&scene);
718
719        assert_eq!(vertices[0].rect, [0.0, 0.0, 100.0, 100.0]);
720        assert_eq!(vertices[4].rect, [0.0, 35.0, 100.0, 20.0]);
721    }
722
723    #[test]
724    fn the_vertex_layout_stride_matches_the_pipeline_attributes() {
725        // position(8) + color(16) + border_color(16) + rect(16) + radius(16)
726        // + border_width(4) + padding(12) == 88 bytes, 4-byte aligned for wgpu
727        assert_eq!(std::mem::size_of::<QuadVertex>(), 88);
728        assert_eq!(std::mem::align_of::<QuadVertex>(), 4);
729    }
730
731    // ---- clear colour ----
732
733    #[test]
734    fn the_clear_colour_comes_from_the_root_background() {
735        let mut scene = SceneGraph::new();
736        let root = node(&mut scene, "html", (0.0, 0.0, 100.0, 100.0));
737        opaque(&mut scene, root, 255, 0, 0);
738
739        let c = clear_color(&scene);
740
741        assert_eq!((c.r, c.g, c.b, c.a), (1.0, 0.0, 0.0, 1.0));
742    }
743
744    #[test]
745    fn the_clear_colour_falls_back_to_the_body_background() {
746        let mut scene = SceneGraph::new();
747        let root = node(&mut scene, "html", (0.0, 0.0, 100.0, 100.0));
748        let body = node(&mut scene, "body", (0.0, 0.0, 100.0, 100.0));
749        scene.add_child(root, body);
750        opaque(&mut scene, body, 0, 0, 255);
751
752        let c = clear_color(&scene);
753
754        assert_eq!((c.r, c.g, c.b, c.a), (0.0, 0.0, 1.0, 1.0));
755    }
756
757    #[test]
758    fn the_clear_colour_defaults_to_white() {
759        let mut scene = SceneGraph::new();
760        node(&mut scene, "html", (0.0, 0.0, 100.0, 100.0));
761
762        assert_eq!(clear_color(&scene).r, 1.0);
763        assert_eq!(clear_color(&SceneGraph::new()).r, 1.0);
764    }
765
766    // ---- real GPU: the SDF quad pipeline ----
767
768    const TEX: u32 = 64;
769
770    /// Render `scene` into a 64x64 RGBA8 texture with the real quad pipeline
771    /// and return the raw pixels.
772    fn render_to_pixels(device: &wgpu::Device, queue: &wgpu::Queue, scene: &SceneGraph) -> Vec<u8> {
773        let format = wgpu::TextureFormat::Rgba8Unorm;
774        let layout = create_viewport_bind_group_layout(device);
775        let pipeline = create_quad_pipeline(device, &layout, format);
776
777        let viewport: [f32; 4] = [TEX as f32, TEX as f32, 0.0, 0.0];
778        let viewport_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
779            label: None,
780            contents: bytemuck::cast_slice(&viewport),
781            usage: wgpu::BufferUsages::UNIFORM,
782        });
783        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
784            label: None,
785            layout: &layout,
786            entries: &[wgpu::BindGroupEntry {
787                binding: 0,
788                resource: viewport_buffer.as_entire_binding(),
789            }],
790        });
791
792        let (vertices, indices) = build_quads(scene);
793        assert!(!indices.is_empty(), "test scene draws nothing");
794        let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
795            label: None,
796            contents: bytemuck::cast_slice(&vertices),
797            usage: wgpu::BufferUsages::VERTEX,
798        });
799        let ibuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
800            label: None,
801            contents: bytemuck::cast_slice(&indices),
802            usage: wgpu::BufferUsages::INDEX,
803        });
804
805        let texture = device.create_texture(&wgpu::TextureDescriptor {
806            label: None,
807            size: wgpu::Extent3d {
808                width: TEX,
809                height: TEX,
810                depth_or_array_layers: 1,
811            },
812            mip_level_count: 1,
813            sample_count: 1,
814            dimension: wgpu::TextureDimension::D2,
815            format,
816            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
817            view_formats: &[],
818        });
819        let view = texture.create_view(&Default::default());
820
821        // 64 px * 4 bytes == 256, already row-alignment friendly
822        let bytes = (TEX * TEX * 4) as u64;
823        let readback = device.create_buffer(&wgpu::BufferDescriptor {
824            label: None,
825            size: bytes,
826            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
827            mapped_at_creation: false,
828        });
829
830        let mut encoder = device.create_command_encoder(&Default::default());
831        {
832            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
833                label: None,
834                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
835                    view: &view,
836                    resolve_target: None,
837                    ops: wgpu::Operations {
838                        // transparent, so uncovered pixels are unambiguous
839                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
840                        store: wgpu::StoreOp::Store,
841                    },
842                    depth_slice: None,
843                })],
844                depth_stencil_attachment: None,
845                timestamp_writes: None,
846                occlusion_query_set: None,
847                multiview_mask: None,
848            });
849            pass.set_pipeline(&pipeline);
850            pass.set_bind_group(0, &bind_group, &[]);
851            pass.set_vertex_buffer(0, vbuf.slice(..));
852            pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
853            pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
854        }
855        encoder.copy_texture_to_buffer(
856            texture.as_image_copy(),
857            wgpu::TexelCopyBufferInfo {
858                buffer: &readback,
859                layout: wgpu::TexelCopyBufferLayout {
860                    offset: 0,
861                    bytes_per_row: Some(TEX * 4),
862                    rows_per_image: Some(TEX),
863                },
864            },
865            wgpu::Extent3d {
866                width: TEX,
867                height: TEX,
868                depth_or_array_layers: 1,
869            },
870        );
871        queue.submit(Some(encoder.finish()));
872
873        readback.slice(..).map_async(wgpu::MapMode::Read, |_| {});
874        device
875            .poll(wgpu::PollType::wait_indefinitely())
876            .expect("device poll failed");
877        let data = readback.slice(..).get_mapped_range().to_vec();
878        readback.unmap();
879        data
880    }
881
882    fn pixel(data: &[u8], x: u32, y: u32) -> [u8; 4] {
883        let i = ((y * TEX + x) * 4) as usize;
884        [data[i], data[i + 1], data[i + 2], data[i + 3]]
885    }
886
887    #[test]
888    fn the_sdf_shader_fills_a_rounded_rect_and_cuts_its_corners() {
889        with_headless_device(
890            "the_sdf_shader_fills_a_rounded_rect_and_cuts_its_corners",
891            |device, queue| {
892                let mut scene = SceneGraph::new();
893                let root = node(&mut scene, "div", (8.0, 8.0, 48.0, 48.0));
894                opaque(&mut scene, root, 255, 0, 0);
895                scene.get_mut(root).style.border_radius = [16.0; 4];
896
897                let data = render_to_pixels(device, queue, &scene);
898
899                // dead centre is solid fill
900                assert_eq!(pixel(&data, 32, 32), [255, 0, 0, 255]);
901                // middle of the top edge is inside the shape
902                assert_eq!(pixel(&data, 32, 9), [255, 0, 0, 255]);
903                // the rounded corner is cut away: 20.5px from the corner centre
904                // with a 16px radius is outside the antialiasing band
905                assert_eq!(pixel(&data, 9, 9)[3], 0, "top-left corner not rounded");
906                assert_eq!(
907                    pixel(&data, 54, 54)[3],
908                    0,
909                    "bottom-right corner not rounded"
910                );
911                // outside the rect entirely
912                assert_eq!(pixel(&data, 2, 32)[3], 0);
913            },
914        );
915    }
916
917    #[test]
918    fn a_square_quad_keeps_its_corners() {
919        with_headless_device("a_square_quad_keeps_its_corners", |device, queue| {
920            let mut scene = SceneGraph::new();
921            let root = node(&mut scene, "div", (8.0, 8.0, 48.0, 48.0));
922            opaque(&mut scene, root, 0, 255, 0);
923
924            let data = render_to_pixels(device, queue, &scene);
925
926            assert_eq!(pixel(&data, 32, 32), [0, 255, 0, 255]);
927            assert_eq!(pixel(&data, 9, 9), [0, 255, 0, 255]);
928            assert_eq!(pixel(&data, 54, 54), [0, 255, 0, 255]);
929        });
930    }
931
932    #[test]
933    fn the_sdf_shader_paints_the_border_ring_in_the_border_colour() {
934        with_headless_device(
935            "the_sdf_shader_paints_the_border_ring_in_the_border_colour",
936            |device, queue| {
937                let mut scene = SceneGraph::new();
938                let root = node(&mut scene, "div", (8.0, 8.0, 48.0, 48.0));
939                opaque(&mut scene, root, 255, 0, 0);
940                scene.get_mut(root).style.border_width = [6.0; 4];
941                scene.get_mut(root).style.border_color = Color::from_rgba(0, 0, 255, 1.0);
942
943                let data = render_to_pixels(device, queue, &scene);
944
945                // 2px inside the top edge is border, the centre is fill
946                assert_eq!(pixel(&data, 32, 10), [0, 0, 255, 255]);
947                assert_eq!(pixel(&data, 10, 32), [0, 0, 255, 255]);
948                assert_eq!(pixel(&data, 32, 32), [255, 0, 0, 255]);
949            },
950        );
951    }
952
953    #[test]
954    fn the_shader_maps_pixel_space_to_clip_space_with_y_pointing_down() {
955        with_headless_device(
956            "the_shader_maps_pixel_space_to_clip_space_with_y_pointing_down",
957            |device, queue| {
958                let mut scene = SceneGraph::new();
959                // a bar across the top quarter of the surface
960                let root = node(&mut scene, "div", (0.0, 0.0, 64.0, 16.0));
961                opaque(&mut scene, root, 0, 0, 255);
962
963                let data = render_to_pixels(device, queue, &scene);
964
965                assert_eq!(
966                    pixel(&data, 32, 4),
967                    [0, 0, 255, 255],
968                    "top should be painted"
969                );
970                assert_eq!(pixel(&data, 32, 60)[3], 0, "bottom should be untouched");
971            },
972        );
973    }
974}