1use std::collections::{HashMap, HashSet};
11
12use bytemuck::{Pod, Zeroable};
13use glam::{Mat4, Vec2};
14use wgpu::util::DeviceExt;
15
16use crate::application::Application;
17use crate::blend::BlendMode;
18use crate::color::Color;
19use crate::render::blend_pipeline::BlendPipelineMap;
20use crate::render::scene_walk::walk_visible_subtree;
21use crate::scene::graphics::{Fill, Primitive, Stroke};
22use crate::scene::{Node, NodeId, Stage};
23
24const KIND_RECT: u32 = 0;
25const KIND_ELLIPSE: u32 = 1;
26const KIND_ANNULAR_SECTOR: u32 = 2;
27const MODE_FILL: u32 = 0;
28const MODE_OUTLINE: u32 = 1;
29const FILL_SOLID: u32 = 0;
30const FILL_LINEAR: u32 = 1;
31const FILL_RADIAL: u32 = 2;
32
33#[repr(C)]
34#[derive(Clone, Copy, Pod, Zeroable)]
35pub(crate) struct GraphicsInstance {
36 pub model: [[f32; 4]; 4],
37 pub color: [f32; 4],
38 pub color_b: [f32; 4],
39 pub half_extents: [f32; 2],
40 pub radius: f32,
41 pub stroke_width: f32,
42 pub grad_a: [f32; 2],
43 pub grad_b: [f32; 2],
44 pub kind_pack: [u32; 4],
51 pub arc_data: [f32; 4],
54}
55
56const ATTR_LAYOUT: [wgpu::VertexAttribute; 13] = wgpu::vertex_attr_array![
57 0 => Float32x4,
58 1 => Float32x4,
59 2 => Float32x4,
60 3 => Float32x4,
61 4 => Float32x4,
62 5 => Float32x4,
63 6 => Float32x2,
64 7 => Float32,
65 8 => Float32,
66 9 => Float32x2,
67 10 => Float32x2,
68 11 => Uint32x4,
69 12 => Float32x4,
70];
71
72#[repr(C)]
77#[derive(Clone, Copy, Pod, Zeroable)]
78pub(crate) struct PolygonVertex {
79 pub position: [f32; 2],
80 pub color: [f32; 4],
81}
82
83const POLYGON_ATTR_LAYOUT: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
84 0 => Float32x2,
85 1 => Float32x4,
86];
87
88pub(crate) struct GraphicsPipeline {
89 pipelines: BlendPipelineMap,
90 polygon_pipelines: BlendPipelineMap,
91}
92
93impl GraphicsPipeline {
94 pub(crate) fn new(app: &Application, output_format: wgpu::TextureFormat) -> Self {
95 let device = app.device();
96 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
97 label: Some("wisp::graphics_solid"),
98 source: wgpu::ShaderSource::Wgsl(
99 include_str!("../../shaders/graphics_solid.wgsl").into(),
100 ),
101 });
102
103 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
104 label: Some("wisp::graphics pipeline layout"),
105 bind_group_layouts: &[],
106 push_constant_ranges: &[],
107 });
108
109 let pipelines = BlendPipelineMap::new(|blend| {
110 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
111 label: Some("wisp::graphics pipeline"),
112 layout: Some(&layout),
113 vertex: wgpu::VertexState {
114 module: &shader,
115 entry_point: Some("main_vs"),
116 buffers: &[wgpu::VertexBufferLayout {
117 array_stride: std::mem::size_of::<GraphicsInstance>()
118 as wgpu::BufferAddress,
119 step_mode: wgpu::VertexStepMode::Instance,
120 attributes: &ATTR_LAYOUT,
121 }],
122 compilation_options: wgpu::PipelineCompilationOptions::default(),
123 },
124 fragment: Some(wgpu::FragmentState {
125 module: &shader,
126 entry_point: Some("main_fs"),
127 targets: &[Some(wgpu::ColorTargetState {
128 format: output_format,
129 blend: Some(blend),
130 write_mask: wgpu::ColorWrites::ALL,
131 })],
132 compilation_options: wgpu::PipelineCompilationOptions::default(),
133 }),
134 primitive: wgpu::PrimitiveState::default(),
135 depth_stencil: None,
136 multisample: wgpu::MultisampleState::default(),
137 multiview: None,
138 cache: None,
139 })
140 });
141
142 let polygon_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
147 label: Some("wisp::graphics_polygon"),
148 source: wgpu::ShaderSource::Wgsl(
149 include_str!("../../shaders/graphics_polygon.wgsl").into(),
150 ),
151 });
152 let polygon_pipelines = BlendPipelineMap::new(|blend| {
153 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
154 label: Some("wisp::graphics polygon pipeline"),
155 layout: Some(&layout),
156 vertex: wgpu::VertexState {
157 module: &polygon_shader,
158 entry_point: Some("main_vs"),
159 buffers: &[wgpu::VertexBufferLayout {
160 array_stride: std::mem::size_of::<PolygonVertex>() as wgpu::BufferAddress,
161 step_mode: wgpu::VertexStepMode::Vertex,
162 attributes: &POLYGON_ATTR_LAYOUT,
163 }],
164 compilation_options: wgpu::PipelineCompilationOptions::default(),
165 },
166 fragment: Some(wgpu::FragmentState {
167 module: &polygon_shader,
168 entry_point: Some("main_fs"),
169 targets: &[Some(wgpu::ColorTargetState {
170 format: output_format,
171 blend: Some(blend),
172 write_mask: wgpu::ColorWrites::ALL,
173 })],
174 compilation_options: wgpu::PipelineCompilationOptions::default(),
175 }),
176 primitive: wgpu::PrimitiveState::default(),
177 depth_stencil: None,
178 multisample: wgpu::MultisampleState::default(),
179 multiview: None,
180 cache: None,
181 })
182 });
183
184 Self {
185 pipelines,
186 polygon_pipelines,
187 }
188 }
189
190 pub(crate) fn draw_stage(
191 &self,
192 app: &Application,
193 pass: &mut wgpu::RenderPass<'_>,
194 stage: &Stage,
195 ) -> (u32, u32) {
196 self.draw_subtree(app, pass, stage, stage.root(), &HashSet::new())
197 }
198
199 pub(crate) fn draw_subtree(
201 &self,
202 app: &Application,
203 pass: &mut wgpu::RenderPass<'_>,
204 stage: &Stage,
205 start: NodeId,
206 exclude: &HashSet<NodeId>,
207 ) -> (u32, u32) {
208 let CollectedGraphics {
209 instance_groups,
210 polygon_groups,
211 logical_count,
212 } = collect_graphics(stage, start, exclude);
213 let mut draw_calls = 0u32;
214
215 for (mode, instances) in instance_groups {
220 if instances.is_empty() {
221 continue;
222 }
223 let count = u32::try_from(instances.len()).expect("instance count fits in u32");
224 let buffer = app
225 .device()
226 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
227 label: Some("wisp::graphics instances"),
228 contents: bytemuck::cast_slice(&instances),
229 usage: wgpu::BufferUsages::VERTEX,
230 });
231
232 pass.set_pipeline(self.pipelines.get(mode));
233 pass.set_vertex_buffer(0, buffer.slice(..));
234 pass.draw(0..6, 0..count);
235 draw_calls += 1;
236 }
237
238 for (mode, vertices) in polygon_groups {
240 if vertices.is_empty() {
241 continue;
242 }
243 let vertex_count =
244 u32::try_from(vertices.len()).expect("polygon vertex count fits in u32");
245 let buffer = app
246 .device()
247 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
248 label: Some("wisp::graphics polygon vertices"),
249 contents: bytemuck::cast_slice(&vertices),
250 usage: wgpu::BufferUsages::VERTEX,
251 });
252 pass.set_pipeline(self.polygon_pipelines.get(mode));
253 pass.set_vertex_buffer(0, buffer.slice(..));
254 pass.draw(0..vertex_count, 0..1);
255 draw_calls += 1;
256 }
257
258 (draw_calls, logical_count)
259 }
260}
261
262struct CollectedGraphics {
263 instance_groups: Vec<(BlendMode, Vec<GraphicsInstance>)>,
264 polygon_groups: Vec<(BlendMode, Vec<PolygonVertex>)>,
265 logical_count: u32,
266}
267
268fn collect_graphics(stage: &Stage, start: NodeId, exclude: &HashSet<NodeId>) -> CollectedGraphics {
277 let mut instance_grouped: HashMap<BlendMode, Vec<GraphicsInstance>> = HashMap::new();
278 let mut polygon_grouped: HashMap<BlendMode, Vec<PolygonVertex>> = HashMap::new();
279 let mut instance_order: Vec<BlendMode> = Vec::new();
280 let mut polygon_order: Vec<BlendMode> = Vec::new();
281 let mut logical = 0u32;
282 walk_visible_subtree(stage, start, exclude, |_id, node, world| {
283 let container = node.container();
284 if let Node::Graphics(graphics) = node {
285 let mode = container.blend_mode;
286 for primitive in &graphics.primitives {
287 logical = logical.saturating_add(1);
288 if matches!(primitive, Primitive::Polygon { .. }) {
289 let bucket = polygon_grouped.entry(mode).or_insert_with(|| {
290 polygon_order.push(mode);
291 Vec::new()
292 });
293 emit_polygon(primitive, world, container.alpha, bucket);
294 } else {
295 let bucket = instance_grouped.entry(mode).or_insert_with(|| {
296 instance_order.push(mode);
297 Vec::new()
298 });
299 emit_primitive(primitive, world, container.alpha, bucket);
300 }
301 }
302 }
303 });
304 let instance_groups = instance_order
305 .into_iter()
306 .filter_map(|m| instance_grouped.remove(&m).map(|v| (m, v)))
307 .collect();
308 let polygon_groups = polygon_order
309 .into_iter()
310 .filter_map(|m| polygon_grouped.remove(&m).map(|v| (m, v)))
311 .collect();
312 CollectedGraphics {
313 instance_groups,
314 polygon_groups,
315 logical_count: logical,
316 }
317}
318
319#[allow(
320 clippy::too_many_lines,
321 reason = "single dispatch table over Primitive variants — splitting per variant would add indirection without clarity"
322)]
323fn emit_primitive(p: &Primitive, world: Mat4, parent_alpha: f32, out: &mut Vec<GraphicsInstance>) {
324 match *p {
325 Primitive::RoundedRect {
326 rect,
327 radius,
328 fill,
329 stroke,
330 } => {
331 let half = Vec2::new(rect.size.x * 0.5, rect.size.y * 0.5);
332 let center = rect.min + half;
333 let model = world * Mat4::from_translation(glam::Vec3::new(center.x, center.y, 0.0));
334 out.push(rect_instance(
335 model,
336 half,
337 radius,
338 fill,
339 parent_alpha,
340 MODE_FILL,
341 0.0,
342 ));
343 if let Some(s) = stroke {
344 out.push(rect_instance(
345 model,
346 half,
347 radius,
348 Fill::Solid(s.color),
349 parent_alpha,
350 MODE_OUTLINE,
351 s.width,
352 ));
353 }
354 }
355 Primitive::Ellipse {
356 center,
357 radii,
358 fill,
359 stroke,
360 } => {
361 let model = world * Mat4::from_translation(glam::Vec3::new(center.x, center.y, 0.0));
362 out.push(ellipse_instance(
363 model,
364 radii,
365 fill,
366 parent_alpha,
367 MODE_FILL,
368 0.0,
369 ));
370 if let Some(s) = stroke {
371 out.push(ellipse_instance(
372 model,
373 radii,
374 Fill::Solid(s.color),
375 parent_alpha,
376 MODE_OUTLINE,
377 s.width,
378 ));
379 }
380 }
381 Primitive::Line {
382 from,
383 to,
384 width,
385 fill,
386 } => {
387 let delta = to - from;
388 let length = delta.length();
389 if length < f32::EPSILON {
390 return;
391 }
392 let angle = delta.y.atan2(delta.x);
393 let center = (from + to) * 0.5;
394 let translate = Mat4::from_translation(glam::Vec3::new(center.x, center.y, 0.0));
395 let rotate = Mat4::from_rotation_z(angle);
396 let model = world * translate * rotate;
397 let half = Vec2::new(length * 0.5, width * 0.5);
398 out.push(rect_instance(
399 model,
400 half,
401 0.0,
402 fill,
403 parent_alpha,
404 MODE_FILL,
405 0.0,
406 ));
407 }
408 Primitive::Polygon { .. } => {
409 }
414 Primitive::AnnularSector {
415 center,
416 r_inner,
417 r_outer,
418 start_angle,
419 end_angle,
420 fill,
421 stroke,
422 } => {
423 let r_inner = r_inner.max(0.0);
429 let r_outer = r_outer.max(r_inner);
430 let span = (end_angle - start_angle).clamp(0.0, std::f32::consts::TAU);
431 let mid_angle = start_angle + span * 0.5;
432 let half_angle = span * 0.5;
433 let model = world * Mat4::from_translation(glam::Vec3::new(center.x, center.y, 0.0));
434 let half = Vec2::new(r_outer, r_outer);
435 out.push(annular_sector_instance(
436 model,
437 half,
438 r_inner,
439 r_outer,
440 mid_angle,
441 half_angle,
442 fill,
443 parent_alpha,
444 MODE_FILL,
445 0.0,
446 ));
447 if let Some(s) = stroke {
448 out.push(annular_sector_instance(
449 model,
450 half,
451 r_inner,
452 r_outer,
453 mid_angle,
454 half_angle,
455 Fill::Solid(s.color),
456 parent_alpha,
457 MODE_OUTLINE,
458 s.width,
459 ));
460 }
461 }
462 }
463}
464
465fn rect_instance(
466 model: Mat4,
467 half: Vec2,
468 radius: f32,
469 fill: Fill,
470 parent_alpha: f32,
471 mode: u32,
472 stroke_width: f32,
473) -> GraphicsInstance {
474 let resolved = resolve_fill(fill, parent_alpha);
475 GraphicsInstance {
476 model: model.to_cols_array_2d(),
477 color: resolved.color,
478 color_b: resolved.color_b,
479 half_extents: [half.x, half.y],
480 radius,
481 stroke_width,
482 grad_a: resolved.grad_a,
483 grad_b: resolved.grad_b,
484 kind_pack: [KIND_RECT, mode, resolved.fill_kind, 0],
485 arc_data: [0.0; 4],
486 }
487}
488
489fn ellipse_instance(
490 model: Mat4,
491 radii: Vec2,
492 fill: Fill,
493 parent_alpha: f32,
494 mode: u32,
495 stroke_width: f32,
496) -> GraphicsInstance {
497 let resolved = resolve_fill(fill, parent_alpha);
498 GraphicsInstance {
499 model: model.to_cols_array_2d(),
500 color: resolved.color,
501 color_b: resolved.color_b,
502 half_extents: [radii.x, radii.y],
503 radius: 0.0,
504 stroke_width,
505 grad_a: resolved.grad_a,
506 grad_b: resolved.grad_b,
507 kind_pack: [KIND_ELLIPSE, mode, resolved.fill_kind, 0],
508 arc_data: [0.0; 4],
509 }
510}
511
512fn emit_polygon(p: &Primitive, world: Mat4, parent_alpha: f32, out: &mut Vec<PolygonVertex>) {
516 let Primitive::Polygon { vertices, fill } = p else {
517 return;
518 };
519 if vertices.len() < 3 {
520 return;
521 }
522 let color = apply_alpha(
523 match *fill {
524 Fill::Solid(c) => c,
525 Fill::LinearGradient { color_a, .. } | Fill::RadialGradient { color_a, .. } => color_a,
528 },
529 parent_alpha,
530 );
531 let to_clip = |v: Vec2| -> [f32; 2] {
532 let clip = world * glam::Vec4::new(v.x, v.y, 0.0, 1.0);
533 [clip.x, clip.y]
534 };
535 let v0 = to_clip(vertices[0]);
537 for i in 1..vertices.len() - 1 {
538 let vi = to_clip(vertices[i]);
539 let vj = to_clip(vertices[i + 1]);
540 out.push(PolygonVertex {
541 position: v0,
542 color,
543 });
544 out.push(PolygonVertex {
545 position: vi,
546 color,
547 });
548 out.push(PolygonVertex {
549 position: vj,
550 color,
551 });
552 }
553}
554
555#[allow(
556 clippy::too_many_arguments,
557 reason = "annular sector emission inherits the same flat parameter style as rect/ellipse helpers"
558)]
559fn annular_sector_instance(
560 model: Mat4,
561 half: Vec2,
562 r_inner: f32,
563 r_outer: f32,
564 mid_angle: f32,
565 half_angle: f32,
566 fill: Fill,
567 parent_alpha: f32,
568 mode: u32,
569 stroke_width: f32,
570) -> GraphicsInstance {
571 let resolved = resolve_fill(fill, parent_alpha);
572 GraphicsInstance {
573 model: model.to_cols_array_2d(),
574 color: resolved.color,
575 color_b: resolved.color_b,
576 half_extents: [half.x, half.y],
577 radius: 0.0,
578 stroke_width,
579 grad_a: resolved.grad_a,
580 grad_b: resolved.grad_b,
581 kind_pack: [KIND_ANNULAR_SECTOR, mode, resolved.fill_kind, 0],
582 arc_data: [r_inner, r_outer, mid_angle, half_angle],
583 }
584}
585
586struct ResolvedFill {
587 fill_kind: u32,
588 color: [f32; 4],
589 color_b: [f32; 4],
590 grad_a: [f32; 2],
591 grad_b: [f32; 2],
592}
593
594fn resolve_fill(fill: Fill, parent_alpha: f32) -> ResolvedFill {
595 match fill {
596 Fill::Solid(c) => {
597 let arr = apply_alpha(c, parent_alpha);
598 ResolvedFill {
599 fill_kind: FILL_SOLID,
600 color: arr,
601 color_b: arr,
602 grad_a: [0.0, 0.0],
603 grad_b: [0.0, 0.0],
604 }
605 }
606 Fill::LinearGradient {
607 start,
608 end,
609 color_a,
610 color_b,
611 } => ResolvedFill {
612 fill_kind: FILL_LINEAR,
613 color: apply_alpha(color_a, parent_alpha),
614 color_b: apply_alpha(color_b, parent_alpha),
615 grad_a: [start.x, start.y],
616 grad_b: [end.x, end.y],
617 },
618 Fill::RadialGradient {
619 center,
620 radius,
621 color_a,
622 color_b,
623 } => ResolvedFill {
624 fill_kind: FILL_RADIAL,
625 color: apply_alpha(color_a, parent_alpha),
626 color_b: apply_alpha(color_b, parent_alpha),
627 grad_a: [center.x, center.y],
628 grad_b: [radius, 0.0],
629 },
630 }
631}
632
633fn apply_alpha(c: Color, parent_alpha: f32) -> [f32; 4] {
634 [c.r, c.g, c.b, c.a * parent_alpha]
635}
636
637#[allow(dead_code, reason = "re-exported to keep collect_instances readable")]
638const _: Option<Stroke> = None;