1use llimphi_hal::wgpu;
55use vello::peniko::Color;
56
57const MSAA_SAMPLES: u32 = 4;
60
61pub struct GpuPipelines {
75 pub lines: wgpu::RenderPipeline,
76 pub tris: wgpu::RenderPipeline,
77 pub rects: wgpu::RenderPipeline,
78 pub discs: wgpu::RenderPipeline,
83 pub bind_layout: wgpu::BindGroupLayout,
84 composite: wgpu::RenderPipeline,
88 composite_bgl: wgpu::BindGroupLayout,
89 composite_sampler: wgpu::Sampler,
90 color_format: wgpu::TextureFormat,
93}
94
95impl GpuPipelines {
96 pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
100 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
101 label: Some("llimphi-raster-gpu-shader"),
102 source: wgpu::ShaderSource::Wgsl(WGSL.into()),
103 });
104
105 let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
106 label: Some("llimphi-raster-gpu-bgl"),
107 entries: &[wgpu::BindGroupLayoutEntry {
108 binding: 0,
109 visibility: wgpu::ShaderStages::VERTEX,
110 ty: wgpu::BindingType::Buffer {
111 ty: wgpu::BufferBindingType::Uniform,
112 has_dynamic_offset: false,
113 min_binding_size: None,
114 },
115 count: None,
116 }],
117 });
118
119 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
120 label: Some("llimphi-raster-gpu-pl"),
121 bind_group_layouts: &[&bind_layout],
122 push_constant_ranges: &[],
123 });
124
125 let color_targets = [Some(wgpu::ColorTargetState {
126 format: color_format,
127 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
128 write_mask: wgpu::ColorWrites::ALL,
129 })];
130
131 let tris = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
133 label: Some("llimphi-raster-gpu-tris"),
134 layout: Some(&pipeline_layout),
135 vertex: wgpu::VertexState {
136 module: &shader,
137 entry_point: Some("vs_tris"),
138 compilation_options: Default::default(),
139 buffers: &[wgpu::VertexBufferLayout {
140 array_stride: 12,
141 step_mode: wgpu::VertexStepMode::Vertex,
142 attributes: &[
143 wgpu::VertexAttribute {
144 format: wgpu::VertexFormat::Float32x2,
145 offset: 0,
146 shader_location: 0,
147 },
148 wgpu::VertexAttribute {
149 format: wgpu::VertexFormat::Uint32,
150 offset: 8,
151 shader_location: 1,
152 },
153 ],
154 }],
155 },
156 primitive: tri_primitive(),
157 depth_stencil: None,
158 multisample: wgpu::MultisampleState {
159 count: MSAA_SAMPLES,
160 ..Default::default()
161 },
162 fragment: Some(wgpu::FragmentState {
163 module: &shader,
164 entry_point: Some("fs"),
165 compilation_options: Default::default(),
166 targets: &color_targets,
167 }),
168 multiview: None,
169 cache: None,
170 });
171
172 let rects = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
174 label: Some("llimphi-raster-gpu-rects"),
175 layout: Some(&pipeline_layout),
176 vertex: wgpu::VertexState {
177 module: &shader,
178 entry_point: Some("vs_rects"),
179 compilation_options: Default::default(),
180 buffers: &[wgpu::VertexBufferLayout {
181 array_stride: 20,
182 step_mode: wgpu::VertexStepMode::Instance,
183 attributes: &[
184 wgpu::VertexAttribute {
185 format: wgpu::VertexFormat::Float32x2,
186 offset: 0,
187 shader_location: 0,
188 },
189 wgpu::VertexAttribute {
190 format: wgpu::VertexFormat::Float32x2,
191 offset: 8,
192 shader_location: 1,
193 },
194 wgpu::VertexAttribute {
195 format: wgpu::VertexFormat::Uint32,
196 offset: 16,
197 shader_location: 2,
198 },
199 ],
200 }],
201 },
202 primitive: tri_primitive(),
203 depth_stencil: None,
204 multisample: wgpu::MultisampleState {
205 count: MSAA_SAMPLES,
206 ..Default::default()
207 },
208 fragment: Some(wgpu::FragmentState {
209 module: &shader,
210 entry_point: Some("fs"),
211 compilation_options: Default::default(),
212 targets: &color_targets,
213 }),
214 multiview: None,
215 cache: None,
216 });
217
218 let lines = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
222 label: Some("llimphi-raster-gpu-lines"),
223 layout: Some(&pipeline_layout),
224 vertex: wgpu::VertexState {
225 module: &shader,
226 entry_point: Some("vs_lines"),
227 compilation_options: Default::default(),
228 buffers: &[wgpu::VertexBufferLayout {
229 array_stride: 20,
230 step_mode: wgpu::VertexStepMode::Instance,
231 attributes: &[
232 wgpu::VertexAttribute {
233 format: wgpu::VertexFormat::Float32x4,
234 offset: 0,
235 shader_location: 0,
236 },
237 wgpu::VertexAttribute {
238 format: wgpu::VertexFormat::Uint32,
239 offset: 16,
240 shader_location: 1,
241 },
242 ],
243 }],
244 },
245 primitive: tri_primitive(),
246 depth_stencil: None,
247 multisample: wgpu::MultisampleState {
248 count: MSAA_SAMPLES,
249 ..Default::default()
250 },
251 fragment: Some(wgpu::FragmentState {
252 module: &shader,
253 entry_point: Some("fs"),
254 compilation_options: Default::default(),
255 targets: &color_targets,
256 }),
257 multiview: None,
258 cache: None,
259 });
260
261 let discs = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
268 label: Some("llimphi-raster-gpu-discs"),
269 layout: Some(&pipeline_layout),
270 vertex: wgpu::VertexState {
271 module: &shader,
272 entry_point: Some("vs_discs"),
273 compilation_options: Default::default(),
274 buffers: &[wgpu::VertexBufferLayout {
275 array_stride: 20,
276 step_mode: wgpu::VertexStepMode::Instance,
277 attributes: &[
278 wgpu::VertexAttribute {
280 format: wgpu::VertexFormat::Float32x2,
281 offset: 0,
282 shader_location: 0,
283 },
284 wgpu::VertexAttribute {
286 format: wgpu::VertexFormat::Float32x2,
287 offset: 8,
288 shader_location: 1,
289 },
290 wgpu::VertexAttribute {
292 format: wgpu::VertexFormat::Uint32,
293 offset: 16,
294 shader_location: 2,
295 },
296 ],
297 }],
298 },
299 primitive: tri_primitive(),
300 depth_stencil: None,
301 multisample: wgpu::MultisampleState {
302 count: MSAA_SAMPLES,
303 ..Default::default()
304 },
305 fragment: Some(wgpu::FragmentState {
306 module: &shader,
307 entry_point: Some("fs_disc"),
308 compilation_options: Default::default(),
309 targets: &color_targets,
310 }),
311 multiview: None,
312 cache: None,
313 });
314
315 let composite_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
321 label: Some("llimphi-raster-gpu-composite-bgl"),
322 entries: &[
323 wgpu::BindGroupLayoutEntry {
324 binding: 0,
325 visibility: wgpu::ShaderStages::FRAGMENT,
326 ty: wgpu::BindingType::Texture {
327 sample_type: wgpu::TextureSampleType::Float { filterable: true },
328 view_dimension: wgpu::TextureViewDimension::D2,
329 multisampled: false,
330 },
331 count: None,
332 },
333 wgpu::BindGroupLayoutEntry {
334 binding: 1,
335 visibility: wgpu::ShaderStages::FRAGMENT,
336 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
337 count: None,
338 },
339 ],
340 });
341 let composite_pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
342 label: Some("llimphi-raster-gpu-composite-pl"),
343 bind_group_layouts: &[&composite_bgl],
344 push_constant_ranges: &[],
345 });
346 let composite = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
347 label: Some("llimphi-raster-gpu-composite"),
348 layout: Some(&composite_pl),
349 vertex: wgpu::VertexState {
350 module: &shader,
351 entry_point: Some("vs_composite"),
352 compilation_options: Default::default(),
353 buffers: &[],
354 },
355 primitive: wgpu::PrimitiveState::default(),
356 depth_stencil: None,
357 multisample: wgpu::MultisampleState::default(),
358 fragment: Some(wgpu::FragmentState {
359 module: &shader,
360 entry_point: Some("fs_composite"),
361 compilation_options: Default::default(),
362 targets: &[Some(wgpu::ColorTargetState {
363 format: color_format,
364 blend: Some(wgpu::BlendState {
365 color: wgpu::BlendComponent {
366 src_factor: wgpu::BlendFactor::One,
367 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
368 operation: wgpu::BlendOperation::Add,
369 },
370 alpha: wgpu::BlendComponent {
371 src_factor: wgpu::BlendFactor::One,
372 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
373 operation: wgpu::BlendOperation::Add,
374 },
375 }),
376 write_mask: wgpu::ColorWrites::ALL,
377 })],
378 }),
379 multiview: None,
380 cache: None,
381 });
382 let composite_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
383 label: Some("llimphi-raster-gpu-composite-sampler"),
384 ..Default::default()
385 });
386
387 Self {
388 lines,
389 tris,
390 rects,
391 discs,
392 bind_layout,
393 composite,
394 composite_bgl,
395 composite_sampler,
396 color_format,
397 }
398 }
399}
400
401fn tri_primitive() -> wgpu::PrimitiveState {
402 wgpu::PrimitiveState {
403 topology: wgpu::PrimitiveTopology::TriangleList,
404 strip_index_format: None,
405 front_face: wgpu::FrontFace::Ccw,
406 cull_mode: None,
407 unclipped_depth: false,
408 polygon_mode: wgpu::PolygonMode::Fill,
409 conservative: false,
410 }
411}
412
413pub struct GpuBatch<'a> {
415 pipelines: &'a GpuPipelines,
416 line_verts: Vec<u8>,
417 tri_verts: Vec<u8>,
418 rect_insts: Vec<u8>,
419 disc_insts: Vec<u8>,
420 line_width: f32,
421 line_count: u32,
422 tri_vert_count: u32,
423 rect_count: u32,
424 disc_count: u32,
425}
426
427impl<'a> GpuBatch<'a> {
428 pub fn new(pipelines: &'a GpuPipelines) -> Self {
429 Self {
430 pipelines,
431 line_verts: Vec::new(),
432 tri_verts: Vec::new(),
433 rect_insts: Vec::new(),
434 disc_insts: Vec::new(),
435 line_width: 1.0,
436 line_count: 0,
437 tri_vert_count: 0,
438 rect_count: 0,
439 disc_count: 0,
440 }
441 }
442
443 pub fn line_width(&mut self, w: f32) {
447 self.line_width = w;
448 }
449
450 pub fn add_line(&mut self, p0: (f32, f32), p1: (f32, f32), color: Color) {
452 let rgba = pack_rgba(color);
453 self.line_verts.extend_from_slice(&p0.0.to_ne_bytes());
454 self.line_verts.extend_from_slice(&p0.1.to_ne_bytes());
455 self.line_verts.extend_from_slice(&p1.0.to_ne_bytes());
456 self.line_verts.extend_from_slice(&p1.1.to_ne_bytes());
457 self.line_verts.extend_from_slice(&rgba.to_ne_bytes());
458 self.line_count += 1;
459 }
460
461 pub fn add_polyline(&mut self, points: &[(f32, f32)], color: Color) {
464 if points.len() < 2 {
465 return;
466 }
467 for w in points.windows(2) {
468 self.add_line(w[0], w[1], color);
469 }
470 }
471
472 pub fn add_tri(
474 &mut self,
475 a: (f32, f32),
476 b: (f32, f32),
477 c: (f32, f32),
478 ca: Color,
479 cb: Color,
480 cc: Color,
481 ) {
482 self.push_tri_vert(a, ca);
483 self.push_tri_vert(b, cb);
484 self.push_tri_vert(c, cc);
485 }
486
487 fn push_tri_vert(&mut self, p: (f32, f32), color: Color) {
488 let rgba = pack_rgba(color);
489 self.tri_verts.extend_from_slice(&p.0.to_ne_bytes());
490 self.tri_verts.extend_from_slice(&p.1.to_ne_bytes());
491 self.tri_verts.extend_from_slice(&rgba.to_ne_bytes());
492 self.tri_vert_count += 1;
493 }
494
495 pub fn add_tri_list(&mut self, verts: &[(f32, f32)], color: Color) {
499 for &p in verts {
500 self.push_tri_vert(p, color);
501 }
502 }
503
504 pub fn add_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Color) {
507 let rgba = pack_rgba(color);
508 self.rect_insts.extend_from_slice(&x.to_ne_bytes());
509 self.rect_insts.extend_from_slice(&y.to_ne_bytes());
510 self.rect_insts.extend_from_slice(&w.to_ne_bytes());
511 self.rect_insts.extend_from_slice(&h.to_ne_bytes());
512 self.rect_insts.extend_from_slice(&rgba.to_ne_bytes());
513 self.rect_count += 1;
514 }
515
516 pub fn add_disc(&mut self, cx: f32, cy: f32, r: f32, color: Color) {
522 self.push_disc(cx, cy, r, 0.0, color);
523 }
524
525 pub fn add_ring(&mut self, cx: f32, cy: f32, r: f32, stroke: f32, color: Color) {
531 self.push_disc(cx, cy, r, stroke.max(0.0), color);
532 }
533
534 fn push_disc(&mut self, cx: f32, cy: f32, r: f32, stroke: f32, color: Color) {
535 let rgba = pack_rgba(color);
536 self.disc_insts.extend_from_slice(&cx.to_ne_bytes());
537 self.disc_insts.extend_from_slice(&cy.to_ne_bytes());
538 self.disc_insts.extend_from_slice(&r.to_ne_bytes());
539 self.disc_insts.extend_from_slice(&stroke.to_ne_bytes());
540 self.disc_insts.extend_from_slice(&rgba.to_ne_bytes());
541 self.disc_count += 1;
542 }
543
544 pub fn primitive_count(&self) -> u32 {
546 self.line_count + self.rect_count + self.disc_count + self.tri_vert_count / 3
547 }
548
549 pub fn flush(
558 self,
559 device: &wgpu::Device,
560 queue: &wgpu::Queue,
561 encoder: &mut wgpu::CommandEncoder,
562 view: &wgpu::TextureView,
563 viewport: (f32, f32),
564 load_op: wgpu::LoadOp<wgpu::Color>,
565 ) {
566 let total =
567 self.line_count + self.tri_vert_count + self.rect_count + self.disc_count;
568 if total == 0 {
569 return;
570 }
571
572 let u_data = [viewport.0, viewport.1, self.line_width, 0.0];
574 let mut u_bytes = Vec::with_capacity(16);
575 for v in u_data {
576 u_bytes.extend_from_slice(&v.to_ne_bytes());
577 }
578 let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
579 label: Some("llimphi-raster-gpu-u"),
580 size: 16,
581 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
582 mapped_at_creation: false,
583 });
584 queue.write_buffer(&uniforms, 0, &u_bytes);
585
586 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
587 label: Some("llimphi-raster-gpu-bg"),
588 layout: &self.pipelines.bind_layout,
589 entries: &[wgpu::BindGroupEntry {
590 binding: 0,
591 resource: uniforms.as_entire_binding(),
592 }],
593 });
594
595 let lines_buf = (!self.line_verts.is_empty()).then(|| {
597 let b = device.create_buffer(&wgpu::BufferDescriptor {
598 label: Some("llimphi-raster-gpu-lines-buf"),
599 size: self.line_verts.len() as u64,
600 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
601 mapped_at_creation: false,
602 });
603 queue.write_buffer(&b, 0, &self.line_verts);
604 b
605 });
606 let tris_buf = (!self.tri_verts.is_empty()).then(|| {
607 let b = device.create_buffer(&wgpu::BufferDescriptor {
608 label: Some("llimphi-raster-gpu-tris-buf"),
609 size: self.tri_verts.len() as u64,
610 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
611 mapped_at_creation: false,
612 });
613 queue.write_buffer(&b, 0, &self.tri_verts);
614 b
615 });
616 let rects_buf = (!self.rect_insts.is_empty()).then(|| {
617 let b = device.create_buffer(&wgpu::BufferDescriptor {
618 label: Some("llimphi-raster-gpu-rects-buf"),
619 size: self.rect_insts.len() as u64,
620 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
621 mapped_at_creation: false,
622 });
623 queue.write_buffer(&b, 0, &self.rect_insts);
624 b
625 });
626 let discs_buf = (!self.disc_insts.is_empty()).then(|| {
627 let b = device.create_buffer(&wgpu::BufferDescriptor {
628 label: Some("llimphi-raster-gpu-discs-buf"),
629 size: self.disc_insts.len() as u64,
630 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
631 mapped_at_creation: false,
632 });
633 queue.write_buffer(&b, 0, &self.disc_insts);
634 b
635 });
636
637 let tex_w = (viewport.0.round() as u32).max(1);
643 let tex_h = (viewport.1.round() as u32).max(1);
644 let extent = wgpu::Extent3d {
645 width: tex_w,
646 height: tex_h,
647 depth_or_array_layers: 1,
648 };
649 let fmt = self.pipelines.color_format;
650 let msaa_tex = device.create_texture(&wgpu::TextureDescriptor {
652 label: Some("llimphi-raster-gpu-msaa"),
653 size: extent,
654 mip_level_count: 1,
655 sample_count: MSAA_SAMPLES,
656 dimension: wgpu::TextureDimension::D2,
657 format: fmt,
658 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
659 view_formats: &[],
660 });
661 let msaa_view = msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
662 let resolve_tex = device.create_texture(&wgpu::TextureDescriptor {
665 label: Some("llimphi-raster-gpu-resolve"),
666 size: extent,
667 mip_level_count: 1,
668 sample_count: 1,
669 dimension: wgpu::TextureDimension::D2,
670 format: fmt,
671 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
672 | wgpu::TextureUsages::TEXTURE_BINDING,
673 view_formats: &[],
674 });
675 let resolve_view =
676 resolve_tex.create_view(&wgpu::TextureViewDescriptor::default());
677
678 {
683 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
684 label: Some("llimphi-raster-gpu-pass"),
685 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
686 view: &msaa_view,
687 resolve_target: Some(&resolve_view),
688 depth_slice: None,
689 ops: wgpu::Operations {
690 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
691 store: wgpu::StoreOp::Store,
692 },
693 })],
694 depth_stencil_attachment: None,
695 timestamp_writes: None,
696 occlusion_query_set: None,
697 });
698 pass.set_bind_group(0, &bind_group, &[]);
699
700 if let Some(buf) = rects_buf.as_ref() {
703 pass.set_pipeline(&self.pipelines.rects);
704 pass.set_vertex_buffer(0, buf.slice(..));
705 pass.draw(0..6, 0..self.rect_count);
706 }
707 if let Some(buf) = discs_buf.as_ref() {
708 pass.set_pipeline(&self.pipelines.discs);
709 pass.set_vertex_buffer(0, buf.slice(..));
710 pass.draw(0..6, 0..self.disc_count);
711 }
712 if let Some(buf) = tris_buf.as_ref() {
713 pass.set_pipeline(&self.pipelines.tris);
714 pass.set_vertex_buffer(0, buf.slice(..));
715 pass.draw(0..self.tri_vert_count, 0..1);
716 }
717 if let Some(buf) = lines_buf.as_ref() {
718 pass.set_pipeline(&self.pipelines.lines);
719 pass.set_vertex_buffer(0, buf.slice(..));
720 pass.draw(0..6, 0..self.line_count);
721 }
722 }
723
724 let composite_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
732 label: Some("llimphi-raster-gpu-composite-bg"),
733 layout: &self.pipelines.composite_bgl,
734 entries: &[
735 wgpu::BindGroupEntry {
736 binding: 0,
737 resource: wgpu::BindingResource::TextureView(&resolve_view),
738 },
739 wgpu::BindGroupEntry {
740 binding: 1,
741 resource: wgpu::BindingResource::Sampler(
742 &self.pipelines.composite_sampler,
743 ),
744 },
745 ],
746 });
747 let mut cpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
748 label: Some("llimphi-raster-gpu-composite-pass"),
749 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
750 view,
751 resolve_target: None,
752 depth_slice: None,
753 ops: wgpu::Operations {
754 load: load_op,
757 store: wgpu::StoreOp::Store,
758 },
759 })],
760 depth_stencil_attachment: None,
761 timestamp_writes: None,
762 occlusion_query_set: None,
763 });
764 cpass.set_pipeline(&self.pipelines.composite);
765 cpass.set_bind_group(0, &composite_bg, &[]);
766 cpass.draw(0..3, 0..1);
767 }
768}
769
770fn pack_rgba(c: Color) -> u32 {
774 let [r, g, b, a] = c.to_rgba8().to_u8_array();
775 (r as u32) | ((g as u32) << 8) | ((b as u32) << 16) | ((a as u32) << 24)
776}
777
778const WGSL: &str = r#"
779struct Uniforms {
780 viewport: vec2<f32>,
781 line_width: f32,
782 _pad: f32,
783};
784
785@group(0) @binding(0) var<uniform> u: Uniforms;
786
787struct V2F {
788 @builtin(position) pos: vec4<f32>,
789 @location(0) color: vec4<f32>,
790};
791
792fn unpack_rgba(c: u32) -> vec4<f32> {
793 let r = f32( c & 0xFFu) / 255.0;
794 let g = f32((c >> 8u) & 0xFFu) / 255.0;
795 let b = f32((c >> 16u) & 0xFFu) / 255.0;
796 let a = f32((c >> 24u) & 0xFFu) / 255.0;
797 return vec4<f32>(r, g, b, a);
798}
799
800fn px_to_ndc(p: vec2<f32>) -> vec2<f32> {
801 return vec2<f32>(p.x / u.viewport.x * 2.0 - 1.0, 1.0 - p.y / u.viewport.y * 2.0);
802}
803
804// -------- triángulos: 1 vértice = (xy, rgba) --------
805
806@vertex
807fn vs_tris(@location(0) xy: vec2<f32>, @location(1) rgba: u32) -> V2F {
808 var out: V2F;
809 out.pos = vec4<f32>(px_to_ndc(xy), 0.0, 1.0);
810 out.color = unpack_rgba(rgba);
811 return out;
812}
813
814// -------- rects: 1 instancia = (xy, wh, rgba), 6 vértices/quad --------
815
816@vertex
817fn vs_rects(
818 @builtin(vertex_index) vid: u32,
819 @location(0) inst_xy: vec2<f32>,
820 @location(1) inst_wh: vec2<f32>,
821 @location(2) inst_rgba: u32,
822) -> V2F {
823 var corners = array<vec2<f32>, 6>(
824 vec2<f32>(0.0, 0.0),
825 vec2<f32>(1.0, 0.0),
826 vec2<f32>(1.0, 1.0),
827 vec2<f32>(0.0, 0.0),
828 vec2<f32>(1.0, 1.0),
829 vec2<f32>(0.0, 1.0),
830 );
831 let local = corners[vid];
832 let px = inst_xy + local * inst_wh;
833 var out: V2F;
834 out.pos = vec4<f32>(px_to_ndc(px), 0.0, 1.0);
835 out.color = unpack_rgba(inst_rgba);
836 return out;
837}
838
839// -------- líneas: 1 instancia = (p0xy, p1xy, rgba), expandida a quad ----
840
841@vertex
842fn vs_lines(
843 @builtin(vertex_index) vid: u32,
844 @location(0) seg: vec4<f32>,
845 @location(1) rgba: u32,
846) -> V2F {
847 // Quad perpendicular al segmento, grosor uniforme `u.line_width` px.
848 // vid 0..5 mapea a los 6 vértices del quad (2 tris).
849 let p0 = seg.xy;
850 let p1 = seg.zw;
851 let dir = normalize(p1 - p0);
852 let n = vec2<f32>(-dir.y, dir.x);
853 let half_w = u.line_width * 0.5;
854 let offsets = array<vec2<f32>, 6>(
855 vec2<f32>(0.0, -half_w), // p0 -n
856 vec2<f32>(0.0, half_w), // p0 +n
857 vec2<f32>(1.0, half_w), // p1 +n
858 vec2<f32>(0.0, -half_w), // p0 -n
859 vec2<f32>(1.0, half_w), // p1 +n
860 vec2<f32>(1.0, -half_w), // p1 -n
861 );
862 let o = offsets[vid];
863 let along = mix(p0, p1, o.x);
864 let across = n * o.y;
865 let px = along + across;
866 var out: V2F;
867 out.pos = vec4<f32>(px_to_ndc(px), 0.0, 1.0);
868 out.color = unpack_rgba(rgba);
869 return out;
870}
871
872// -------- discos/anillos: 1 instancia = (cxcy, r/stroke, rgba) --------
873//
874// Quad que cubre el disco con 1.5 px de margen (para que el smoothstep
875// del borde no se recorte). El VS pasa al FS la posición local en px
876// relativa al centro; el FS evalúa el SDF del círculo y hace smoothstep
877// sobre `fwidth` → borde antialiased. `stroke > 0` recorta un anillo.
878
879struct DiscV2F {
880 @builtin(position) pos: vec4<f32>,
881 @location(0) color: vec4<f32>,
882 @location(1) local: vec2<f32>, // px relativos al centro
883 @location(2) params: vec2<f32>, // r, stroke (px)
884};
885
886@vertex
887fn vs_discs(
888 @builtin(vertex_index) vid: u32,
889 @location(0) inst_c: vec2<f32>,
890 @location(1) inst_rs: vec2<f32>,
891 @location(2) inst_rgba: u32,
892) -> DiscV2F {
893 var corners = array<vec2<f32>, 6>(
894 vec2<f32>(-1.0, -1.0),
895 vec2<f32>( 1.0, -1.0),
896 vec2<f32>( 1.0, 1.0),
897 vec2<f32>(-1.0, -1.0),
898 vec2<f32>( 1.0, 1.0),
899 vec2<f32>(-1.0, 1.0),
900 );
901 let r = inst_rs.x;
902 let margin = r + 1.5; // 1.5 px de aire para el AA del borde
903 let local = corners[vid] * margin;
904 let px = inst_c + local;
905 var out: DiscV2F;
906 out.pos = vec4<f32>(px_to_ndc(px), 0.0, 1.0);
907 out.color = unpack_rgba(inst_rgba);
908 out.local = local;
909 out.params = inst_rs;
910 return out;
911}
912
913@fragment
914fn fs_disc(in: DiscV2F) -> @location(0) vec4<f32> {
915 let r = in.params.x;
916 let stroke = in.params.y;
917 let dist = length(in.local); // distancia al centro en px
918 // Ancho del filtro AA en px (≈ 1 px en pantalla).
919 let aa = fwidth(dist);
920 // Borde exterior: cobertura 1 dentro de r, 0 fuera de r+aa.
921 var cov = 1.0 - smoothstep(r - aa, r + aa, dist);
922 // Anillo: si hay stroke, recortamos el agujero interior con AA.
923 if (stroke > 0.0) {
924 let inner = max(r - stroke, 0.0);
925 cov = cov * smoothstep(inner - aa, inner + aa, dist);
926 }
927 if (cov <= 0.0) {
928 discard;
929 }
930 return vec4<f32>(in.color.rgb, in.color.a * cov);
931}
932
933@fragment
934fn fs(in: V2F) -> @location(0) vec4<f32> {
935 return in.color;
936}
937
938// -------- composite: blit fullscreen de la scratch resuelta del MSAA ----
939//
940// Triángulo de pantalla completa (3 vértices, sin vertex buffer). Samplea
941// la scratch (alpha **premultiplicado**) y la emite tal cual; el alpha-over
942// real lo hace el BlendState del pipeline (`One, OneMinusSrcAlpha`).
943
944@group(0) @binding(0) var src_tex: texture_2d<f32>;
945@group(0) @binding(1) var src_samp: sampler;
946
947struct CompV2F {
948 @builtin(position) pos: vec4<f32>,
949 @location(0) uv: vec2<f32>,
950};
951
952@vertex
953fn vs_composite(@builtin(vertex_index) vid: u32) -> CompV2F {
954 // Triángulo gigante que cubre el viewport (técnica estándar).
955 var uvs = array<vec2<f32>, 3>(
956 vec2<f32>(0.0, 0.0),
957 vec2<f32>(2.0, 0.0),
958 vec2<f32>(0.0, 2.0),
959 );
960 let uv = uvs[vid];
961 var out: CompV2F;
962 out.pos = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
963 // El framebuffer tiene Y hacia abajo; la textura, hacia arriba en UV.
964 out.uv = vec2<f32>(uv.x, 1.0 - uv.y);
965 return out;
966}
967
968@fragment
969fn fs_composite(in: CompV2F) -> @location(0) vec4<f32> {
970 return textureSample(src_tex, src_samp, in.uv);
971}
972"#;