1use wgpu::util::DeviceExt;
2
3use crate::scene::*;
4
5#[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], 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
27pub 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
115pub 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
147pub fn quad_shader_source() -> &'static str {
150 QUAD_SHADER
151}
152
153pub 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
170pub 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, 1 => Float32x4, 2 => Float32x4, 3 => Float32x4, 4 => Float32x4, 5 => Float32, ],
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 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 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 #[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 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 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 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 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 assert_eq!(std::mem::size_of::<QuadVertex>(), 88);
728 assert_eq!(std::mem::align_of::<QuadVertex>(), 4);
729 }
730
731 #[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 const TEX: u32 = 64;
769
770 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 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 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 assert_eq!(pixel(&data, 32, 32), [255, 0, 0, 255]);
901 assert_eq!(pixel(&data, 32, 9), [255, 0, 0, 255]);
903 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 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 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 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}