Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 18 kB View raw
1use crate::gpu::Gpu; 2 3pub const MAX_PLANES: usize = 8; 4 5pub const MAX_STROKE_POINTS: usize = 10; 6 7const STROKE_POINT_SLOTS: usize = MAX_STROKE_POINTS / 2; 8 9#[repr(C, align(16))] 10#[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] 11pub struct ConvexInstance { 12 pub rect_xywh_px: [f32; 4], 13 pub fill_premul_rgba: [f32; 4], 14 pub border_premul_rgba: [f32; 4], 15 pub count_border_pad: [f32; 4], 16 pub planes: [[f32; 4]; MAX_PLANES], 17} 18 19impl ConvexInstance { 20 #[must_use] 21 pub fn new( 22 rect_xywh_px: [f32; 4], 23 fill_premul_rgba: [f32; 4], 24 border_premul_rgba: [f32; 4], 25 border_width_px: f32, 26 planes: &[[f32; 3]], 27 ) -> Option<Self> { 28 if !(1..=MAX_PLANES).contains(&planes.len()) { 29 return None; 30 } 31 let count = u8::try_from(planes.len()).ok()?; 32 let packed = 33 core::array::from_fn(|i| planes.get(i).map_or([0.0; 4], |p| [p[0], p[1], p[2], 0.0])); 34 Some(Self { 35 rect_xywh_px, 36 fill_premul_rgba, 37 border_premul_rgba, 38 count_border_pad: [f32::from(count), border_width_px, 0.0, 0.0], 39 planes: packed, 40 }) 41 } 42} 43 44#[repr(C, align(16))] 45#[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] 46pub struct StrokeInstance { 47 pub rect_xywh_px: [f32; 4], 48 pub color_premul_rgba: [f32; 4], 49 pub count_half_pad: [f32; 4], 50 pub points_px: [[f32; 4]; STROKE_POINT_SLOTS], 51} 52 53impl StrokeInstance { 54 #[must_use] 55 pub fn new( 56 points_px: &[[f32; 2]], 57 color_premul_rgba: [f32; 4], 58 half_width_px: f32, 59 ) -> Option<Self> { 60 if !(2..=MAX_STROKE_POINTS).contains(&points_px.len()) { 61 return None; 62 } 63 let count = u8::try_from(points_px.len()).ok()?; 64 let pad = half_width_px.max(0.0) + 1.0; 65 let (min, max) = points_px.iter().fold( 66 ([f32::INFINITY; 2], [f32::NEG_INFINITY; 2]), 67 |(lo, hi), p| { 68 ( 69 [lo[0].min(p[0]), lo[1].min(p[1])], 70 [hi[0].max(p[0]), hi[1].max(p[1])], 71 ) 72 }, 73 ); 74 let pair = |i: usize| { 75 let at = |j: usize| points_px.get(j).copied().unwrap_or([0.0; 2]); 76 let (a, b) = (at(2 * i), at(2 * i + 1)); 77 [a[0], a[1], b[0], b[1]] 78 }; 79 Some(Self { 80 rect_xywh_px: [ 81 min[0] - pad, 82 min[1] - pad, 83 max[0] - min[0] + 2.0 * pad, 84 max[1] - min[1] + 2.0 * pad, 85 ], 86 color_premul_rgba, 87 count_half_pad: [f32::from(count), half_width_px, 0.0, 0.0], 88 points_px: core::array::from_fn(pair), 89 }) 90 } 91} 92 93#[repr(C, align(16))] 94#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 95struct VectorFrame { 96 viewport_px: [f32; 2], 97 pad: [f32; 2], 98} 99 100const FRAME_SIZE: u64 = core::mem::size_of::<VectorFrame>() as u64; 101const INITIAL_CAP_BYTES: u64 = 256 * 64; 102const VERTS_PER_INSTANCE: u32 = 6; 103 104const CONVEX_ATTRS: [wgpu::VertexAttribute; 12] = wgpu::vertex_attr_array![ 105 0 => Float32x4, 1 => Float32x4, 2 => Float32x4, 3 => Float32x4, 106 4 => Float32x4, 5 => Float32x4, 6 => Float32x4, 7 => Float32x4, 107 8 => Float32x4, 9 => Float32x4, 10 => Float32x4, 11 => Float32x4, 108]; 109 110const STROKE_ATTRS: [wgpu::VertexAttribute; 8] = wgpu::vertex_attr_array![ 111 0 => Float32x4, 1 => Float32x4, 2 => Float32x4, 3 => Float32x4, 112 4 => Float32x4, 5 => Float32x4, 6 => Float32x4, 7 => Float32x4, 113]; 114 115struct ScreenInstancePipeline { 116 device: wgpu::Device, 117 queue: wgpu::Queue, 118 pipeline: wgpu::RenderPipeline, 119 uniform_buffer: wgpu::Buffer, 120 bind_group: wgpu::BindGroup, 121 instances: wgpu::Buffer, 122 capacity_bytes: u64, 123 stride: u64, 124} 125 126impl ScreenInstancePipeline { 127 fn new( 128 gpu: &Gpu, 129 color_format: wgpu::TextureFormat, 130 label: &str, 131 shader_source: &str, 132 attributes: &[wgpu::VertexAttribute], 133 stride: u64, 134 ) -> Self { 135 let device = gpu.device().clone(); 136 let queue = gpu.queue().clone(); 137 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 138 label: Some(label), 139 source: wgpu::ShaderSource::Wgsl(shader_source.into()), 140 }); 141 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 142 label: Some(label), 143 entries: &[wgpu::BindGroupLayoutEntry { 144 binding: 0, 145 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 146 ty: wgpu::BindingType::Buffer { 147 ty: wgpu::BufferBindingType::Uniform, 148 has_dynamic_offset: false, 149 min_binding_size: wgpu::BufferSize::new(FRAME_SIZE), 150 }, 151 count: None, 152 }], 153 }); 154 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 155 label: Some(label), 156 bind_group_layouts: &[Some(&bind_group_layout)], 157 immediate_size: 0, 158 }); 159 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 160 label: Some(label), 161 layout: Some(&pipeline_layout), 162 vertex: wgpu::VertexState { 163 module: &shader, 164 entry_point: Some("vs"), 165 compilation_options: wgpu::PipelineCompilationOptions::default(), 166 buffers: &[wgpu::VertexBufferLayout { 167 array_stride: stride, 168 step_mode: wgpu::VertexStepMode::Instance, 169 attributes, 170 }], 171 }, 172 fragment: Some(wgpu::FragmentState { 173 module: &shader, 174 entry_point: Some("fs"), 175 compilation_options: wgpu::PipelineCompilationOptions::default(), 176 targets: &[Some(wgpu::ColorTargetState { 177 format: color_format, 178 blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), 179 write_mask: wgpu::ColorWrites::ALL, 180 })], 181 }), 182 primitive: wgpu::PrimitiveState { 183 topology: wgpu::PrimitiveTopology::TriangleList, 184 strip_index_format: None, 185 front_face: wgpu::FrontFace::Ccw, 186 cull_mode: None, 187 polygon_mode: wgpu::PolygonMode::Fill, 188 conservative: false, 189 unclipped_depth: false, 190 }, 191 depth_stencil: None, 192 multisample: wgpu::MultisampleState::default(), 193 multiview_mask: None, 194 cache: None, 195 }); 196 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { 197 label: Some(label), 198 size: FRAME_SIZE, 199 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 200 mapped_at_creation: false, 201 }); 202 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 203 label: Some(label), 204 layout: &bind_group_layout, 205 entries: &[wgpu::BindGroupEntry { 206 binding: 0, 207 resource: uniform_buffer.as_entire_binding(), 208 }], 209 }); 210 let instances = create_instance_buffer(&device, INITIAL_CAP_BYTES); 211 Self { 212 device, 213 queue, 214 pipeline, 215 uniform_buffer, 216 bind_group, 217 instances, 218 capacity_bytes: INITIAL_CAP_BYTES, 219 stride, 220 } 221 } 222 223 fn upload(&mut self, viewport_px: [f32; 2], instance_bytes: &[u8]) { 224 let frame = VectorFrame { 225 viewport_px, 226 pad: [0.0, 0.0], 227 }; 228 self.queue 229 .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&frame)); 230 if instance_bytes.is_empty() { 231 return; 232 } 233 let needed = instance_bytes.len() as u64; 234 if needed > self.capacity_bytes { 235 let new_cap = needed.next_power_of_two().max(self.capacity_bytes * 2); 236 self.instances = create_instance_buffer(&self.device, new_cap); 237 self.capacity_bytes = new_cap; 238 } 239 self.queue.write_buffer(&self.instances, 0, instance_bytes); 240 } 241 242 fn draw_range( 243 &self, 244 encoder: &mut wgpu::CommandEncoder, 245 color_view: &wgpu::TextureView, 246 range: core::ops::Range<u32>, 247 ) { 248 if range.start >= range.end { 249 return; 250 } 251 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 252 label: Some("bone-render:vector-pass"), 253 color_attachments: &[Some(wgpu::RenderPassColorAttachment { 254 view: color_view, 255 resolve_target: None, 256 depth_slice: None, 257 ops: wgpu::Operations { 258 load: wgpu::LoadOp::Load, 259 store: wgpu::StoreOp::Store, 260 }, 261 })], 262 depth_stencil_attachment: None, 263 timestamp_writes: None, 264 occlusion_query_set: None, 265 multiview_mask: None, 266 }); 267 pass.set_pipeline(&self.pipeline); 268 pass.set_bind_group(0, &self.bind_group, &[]); 269 let start = u64::from(range.start) * self.stride; 270 let end = u64::from(range.end) * self.stride; 271 pass.set_vertex_buffer(0, self.instances.slice(start..end)); 272 pass.draw(0..VERTS_PER_INSTANCE, 0..(range.end - range.start)); 273 } 274} 275 276fn create_instance_buffer(device: &wgpu::Device, capacity_bytes: u64) -> wgpu::Buffer { 277 device.create_buffer(&wgpu::BufferDescriptor { 278 label: Some("bone-render:vector-instances"), 279 size: capacity_bytes, 280 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, 281 mapped_at_creation: false, 282 }) 283} 284 285pub struct ConvexPolyPipeline(ScreenInstancePipeline); 286 287impl ConvexPolyPipeline { 288 #[must_use] 289 pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self { 290 Self(ScreenInstancePipeline::new( 291 gpu, 292 color_format, 293 "bone-render:convex-poly", 294 include_str!("convex_poly.wgsl"), 295 &CONVEX_ATTRS, 296 core::mem::size_of::<ConvexInstance>() as u64, 297 )) 298 } 299 300 pub fn upload(&mut self, viewport_px: [f32; 2], instances: &[ConvexInstance]) { 301 self.0.upload(viewport_px, bytemuck::cast_slice(instances)); 302 } 303 304 pub fn draw_range( 305 &self, 306 encoder: &mut wgpu::CommandEncoder, 307 color_view: &wgpu::TextureView, 308 range: core::ops::Range<u32>, 309 ) { 310 self.0.draw_range(encoder, color_view, range); 311 } 312} 313 314impl core::fmt::Debug for ConvexPolyPipeline { 315 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 316 f.debug_struct("ConvexPolyPipeline").finish_non_exhaustive() 317 } 318} 319 320pub struct StrokePipeline(ScreenInstancePipeline); 321 322impl StrokePipeline { 323 #[must_use] 324 pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self { 325 Self(ScreenInstancePipeline::new( 326 gpu, 327 color_format, 328 "bone-render:stroke", 329 include_str!("stroke.wgsl"), 330 &STROKE_ATTRS, 331 core::mem::size_of::<StrokeInstance>() as u64, 332 )) 333 } 334 335 pub fn upload(&mut self, viewport_px: [f32; 2], instances: &[StrokeInstance]) { 336 self.0.upload(viewport_px, bytemuck::cast_slice(instances)); 337 } 338 339 pub fn draw_range( 340 &self, 341 encoder: &mut wgpu::CommandEncoder, 342 color_view: &wgpu::TextureView, 343 range: core::ops::Range<u32>, 344 ) { 345 self.0.draw_range(encoder, color_view, range); 346 } 347} 348 349impl core::fmt::Debug for StrokePipeline { 350 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 351 f.debug_struct("StrokePipeline").finish_non_exhaustive() 352 } 353} 354 355#[cfg(test)] 356mod tests { 357 use super::*; 358 use crate::{AdapterPolicy, OffscreenContext, ViewportExtent, ViewportPx}; 359 360 fn clear_black(encoder: &mut wgpu::CommandEncoder, color: &wgpu::TextureView) { 361 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 362 label: Some("vector-test-clear"), 363 color_attachments: &[Some(wgpu::RenderPassColorAttachment { 364 view: color, 365 resolve_target: None, 366 depth_slice: None, 367 ops: wgpu::Operations { 368 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), 369 store: wgpu::StoreOp::Store, 370 }, 371 })], 372 depth_stencil_attachment: None, 373 timestamp_writes: None, 374 occlusion_query_set: None, 375 multiview_mask: None, 376 }); 377 } 378 379 #[test] 380 fn convex_instance_rejects_empty_and_oversized_plane_sets() { 381 let unit = [1.0, 0.0, 0.0]; 382 let rect = [0.0, 0.0, 8.0, 8.0]; 383 let color = [1.0, 0.0, 0.0, 1.0]; 384 assert!(ConvexInstance::new(rect, color, color, 0.0, &[]).is_none()); 385 assert!(ConvexInstance::new(rect, color, color, 0.0, &[unit; MAX_PLANES + 1]).is_none()); 386 assert!(ConvexInstance::new(rect, color, color, 0.0, &[unit; MAX_PLANES]).is_some()); 387 } 388 389 #[test] 390 fn stroke_instance_rejects_too_few_and_too_many_points() { 391 let color = [1.0, 0.0, 0.0, 1.0]; 392 assert!(StrokeInstance::new(&[[0.0, 0.0]], color, 1.0).is_none()); 393 assert!(StrokeInstance::new(&[[0.0, 0.0]; MAX_STROKE_POINTS + 1], color, 1.0).is_none()); 394 assert!(StrokeInstance::new(&[[0.0, 0.0]; MAX_STROKE_POINTS], color, 1.0).is_some()); 395 } 396 397 #[test] 398 fn stroke_instance_bounds_cover_every_point_plus_width_and_aa() { 399 let Some(line) = StrokeInstance::new(&[[10.0, 20.0], [40.0, 5.0]], [0.0; 4], 3.0) else { 400 panic!("two points form a stroke"); 401 }; 402 let expected = [6.0, 1.0, 38.0, 23.0]; 403 line.rect_xywh_px 404 .iter() 405 .zip(expected) 406 .for_each(|(got, want)| { 407 assert!( 408 (got - want).abs() < 1.0e-6, 409 "stroke rect {:?} must match {expected:?}", 410 line.rect_xywh_px, 411 ); 412 }); 413 } 414 415 #[test] 416 fn a_translucent_polyline_joint_blends_once_on_the_gpu() { 417 let extent = ViewportExtent::square(ViewportPx::new(64)); 418 let Ok(ctx) = pollster::block_on(OffscreenContext::new(extent, AdapterPolicy::Platform)) 419 else { 420 return; 421 }; 422 let mut stroke = StrokePipeline::new(ctx.gpu(), ctx.color_format()); 423 let Some(bend) = StrokeInstance::new( 424 &[[8.0, 8.0], [8.0, 56.0], [56.0, 56.0]], 425 [0.0, 0.5, 0.0, 0.5], 426 3.0, 427 ) else { 428 panic!("three points form a stroke"); 429 }; 430 stroke.upload([64.0, 64.0], &[bend]); 431 let Ok(frame) = ctx.render(|encoder, color, _pick| { 432 clear_black(encoder, color); 433 stroke.draw_range(encoder, color, 0..1); 434 }) else { 435 panic!("offscreen render failed"); 436 }; 437 let data = frame.rgba(); 438 let px = |x: u32, y: u32| { 439 let i = ((y * 64 + x) * 4) as usize; 440 [data[i], data[i + 1], data[i + 2], data[i + 3]] 441 }; 442 let joint = px(8, 56); 443 let straight = px(8, 32); 444 assert!( 445 joint[1].abs_diff(straight[1]) <= 8, 446 "the joint must not double-blend, joint {joint:?} vs straight {straight:?}", 447 ); 448 } 449 450 #[test] 451 fn convex_fill_and_stroke_paint_their_pixels_on_the_gpu() { 452 let extent = ViewportExtent::square(ViewportPx::new(64)); 453 let Ok(ctx) = pollster::block_on(OffscreenContext::new(extent, AdapterPolicy::Platform)) 454 else { 455 return; 456 }; 457 let mut convex = ConvexPolyPipeline::new(ctx.gpu(), ctx.color_format()); 458 let mut stroke = StrokePipeline::new(ctx.gpu(), ctx.color_format()); 459 let Some(square) = ConvexInstance::new( 460 [15.0, 15.0, 34.0, 34.0], 461 [1.0, 0.0, 0.0, 1.0], 462 [0.0, 0.0, 0.0, 0.0], 463 0.0, 464 &[ 465 [-1.0, 0.0, -16.0], 466 [1.0, 0.0, 48.0], 467 [0.0, -1.0, -16.0], 468 [0.0, 1.0, 48.0], 469 ], 470 ) else { 471 panic!("four planes are within the cap"); 472 }; 473 let Some(line) = 474 StrokeInstance::new(&[[8.0, 32.0], [56.0, 32.0]], [0.0, 1.0, 0.0, 1.0], 2.0) 475 else { 476 panic!("two points form a stroke"); 477 }; 478 convex.upload([64.0, 64.0], &[square]); 479 stroke.upload([64.0, 64.0], &[line]); 480 let Ok(frame) = ctx.render(|encoder, color, _pick| { 481 clear_black(encoder, color); 482 convex.draw_range(encoder, color, 0..1); 483 stroke.draw_range(encoder, color, 0..1); 484 }) else { 485 panic!("offscreen render failed"); 486 }; 487 let data = frame.rgba(); 488 let px = |x: u32, y: u32| { 489 let i = ((y * 64 + x) * 4) as usize; 490 [data[i], data[i + 1], data[i + 2], data[i + 3]] 491 }; 492 let near = |a: [u8; 4], b: [u8; 4]| a.iter().zip(b).all(|(p, q)| p.abs_diff(q) <= 8); 493 assert!( 494 near(px(24, 24), [255, 0, 0, 255]), 495 "convex interior must paint the fill color, got {:?}", 496 px(24, 24), 497 ); 498 assert!( 499 near(px(52, 32), [0, 255, 0, 255]), 500 "the stroke past the square must paint the stroke color, got {:?}", 501 px(52, 32), 502 ); 503 assert!( 504 near(px(4, 4), [0, 0, 0, 255]), 505 "an empty corner stays the cleared background, got {:?}", 506 px(4, 4), 507 ); 508 } 509}