1use bytemuck::{Pod, Zeroable};
10use wgpu::util::DeviceExt;
11
12use crate::camera::Camera;
13use crate::error::RenderError;
14use crate::mesh::{EdgeVertex, RenderMesh, Vertex};
15use crate::{RenderOpts, RenderOutput};
16
17#[repr(C)]
19#[derive(Debug, Clone, Copy, Pod, Zeroable)]
20pub struct Globals {
21 pub view_proj: [f32; 16],
23 pub view_dir: [f32; 4],
25 pub ambient: f32,
27 pub selected_id: u32,
29 pub _pad: [f32; 2],
31}
32
33pub const COLOR_FORMAT_OFFSCREEN: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
34pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
35pub const ID_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
36
37#[must_use]
43pub fn probe_adapter() -> Option<String> {
44 let instance = wgpu::Instance::default();
45 candidate_adapters(&instance, None).first().map(|adapter| {
46 let info = adapter.get_info();
47 format!(
48 "{:?} / {} ({:?})",
49 info.backend, info.name, info.device_type
50 )
51 })
52}
53
54fn candidate_adapters(
61 instance: &wgpu::Instance,
62 surface: Option<&wgpu::Surface<'_>>,
63) -> Vec<wgpu::Adapter> {
64 let mut out = Vec::new();
65 if let Ok(adapter) =
66 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
67 power_preference: wgpu::PowerPreference::HighPerformance,
68 force_fallback_adapter: false,
69 compatible_surface: surface,
70 apply_limit_buckets: false,
71 }))
72 {
73 out.push(adapter);
74 }
75 if let Ok(adapter) =
76 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
77 power_preference: wgpu::PowerPreference::LowPower,
78 force_fallback_adapter: true,
79 compatible_surface: surface,
80 apply_limit_buckets: false,
81 }))
82 {
83 out.push(adapter);
84 }
85 out
86}
87
88pub fn acquire_device(
97 instance: &wgpu::Instance,
98 surface: Option<&wgpu::Surface<'_>>,
99) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue), RenderError> {
100 let adapters = candidate_adapters(instance, surface);
101 if adapters.is_empty() {
102 return Err(RenderError::NoAdapter(
103 "request_adapter returned no adapter".into(),
104 ));
105 }
106 let mut last_err = String::new();
107 for adapter in adapters {
108 match pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
109 label: Some("brepkit-render device"),
110 required_features: wgpu::Features::empty(),
111 required_limits: wgpu::Limits::downlevel_defaults(),
112 ..Default::default()
113 })) {
114 Ok((device, queue)) => return Ok((adapter, device, queue)),
115 Err(e) => last_err = e.to_string(),
116 }
117 }
118 Err(RenderError::DeviceRequest(last_err))
119}
120
121pub struct GpuContext {
126 #[cfg_attr(not(feature = "window"), allow(dead_code))]
128 pub adapter: wgpu::Adapter,
129 pub device: wgpu::Device,
130 pub queue: wgpu::Queue,
131}
132
133impl GpuContext {
134 pub fn new() -> Result<Self, RenderError> {
146 let instance = wgpu::Instance::default();
147 Self::with_instance(instance, None)
148 }
149
150 pub fn with_instance(
161 instance: wgpu::Instance,
162 surface: Option<&wgpu::Surface<'_>>,
163 ) -> Result<Self, RenderError> {
164 let (adapter, device, queue) = acquire_device(&instance, surface)?;
165 Ok(Self {
166 adapter,
167 device,
168 queue,
169 })
170 }
171}
172
173pub struct GlobalsBinding {
178 #[cfg_attr(not(feature = "window"), allow(dead_code))]
181 pub buffer: wgpu::Buffer,
182 pub bind_group: wgpu::BindGroup,
183 pub layout: wgpu::BindGroupLayout,
184}
185
186impl GlobalsBinding {
187 pub fn new(device: &wgpu::Device, globals: &Globals) -> Self {
188 let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
189 label: Some("globals"),
190 contents: bytemuck::bytes_of(globals),
191 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
192 });
193 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
194 label: Some("globals layout"),
195 entries: &[wgpu::BindGroupLayoutEntry {
196 binding: 0,
197 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
198 ty: wgpu::BindingType::Buffer {
199 ty: wgpu::BufferBindingType::Uniform,
200 has_dynamic_offset: false,
201 min_binding_size: None,
202 },
203 count: None,
204 }],
205 });
206 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
207 label: Some("globals bind group"),
208 layout: &layout,
209 entries: &[wgpu::BindGroupEntry {
210 binding: 0,
211 resource: buffer.as_entire_binding(),
212 }],
213 });
214 Self {
215 buffer,
216 bind_group,
217 layout,
218 }
219 }
220
221 #[cfg_attr(not(feature = "window"), allow(dead_code))]
223 pub fn upload(&self, queue: &wgpu::Queue, globals: &Globals) {
224 queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(globals));
225 }
226}
227
228pub struct Pipelines {
232 pub mesh: wgpu::RenderPipeline,
233 pub edge: Option<wgpu::RenderPipeline>,
234}
235
236impl Pipelines {
237 pub fn new(
241 device: &wgpu::Device,
242 layout: &wgpu::PipelineLayout,
243 color_format: wgpu::TextureFormat,
244 with_edges: bool,
245 ) -> Self {
246 let mesh_shader = device.create_shader_module(wgpu::include_wgsl!("../shaders/mesh.wgsl"));
247 let color_targets = [
248 Some(wgpu::ColorTargetState {
249 format: color_format,
250 blend: None,
251 write_mask: wgpu::ColorWrites::ALL,
252 }),
253 Some(wgpu::ColorTargetState {
254 format: ID_FORMAT,
255 blend: None,
256 write_mask: wgpu::ColorWrites::ALL,
257 }),
258 ];
259 let mesh = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
260 label: Some("mesh pipeline"),
261 layout: Some(layout),
262 vertex: wgpu::VertexState {
263 module: &mesh_shader,
264 entry_point: Some("vs_main"),
265 buffers: &[Some(wgpu::VertexBufferLayout {
266 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
267 step_mode: wgpu::VertexStepMode::Vertex,
268 attributes: &[
269 wgpu::VertexAttribute {
270 format: wgpu::VertexFormat::Float32x3,
271 offset: 0,
272 shader_location: 0,
273 },
274 wgpu::VertexAttribute {
275 format: wgpu::VertexFormat::Float32x3,
276 offset: 12,
277 shader_location: 1,
278 },
279 wgpu::VertexAttribute {
280 format: wgpu::VertexFormat::Uint32,
281 offset: 24,
282 shader_location: 2,
283 },
284 ],
285 })],
286 compilation_options: wgpu::PipelineCompilationOptions::default(),
287 },
288 primitive: wgpu::PrimitiveState {
289 topology: wgpu::PrimitiveTopology::TriangleList,
290 cull_mode: None,
291 ..Default::default()
292 },
293 depth_stencil: Some(wgpu::DepthStencilState {
294 format: DEPTH_FORMAT,
295 depth_write_enabled: Some(true),
296 depth_compare: Some(wgpu::CompareFunction::Less),
297 stencil: wgpu::StencilState::default(),
298 bias: wgpu::DepthBiasState::default(),
299 }),
300 multisample: wgpu::MultisampleState::default(),
301 fragment: Some(wgpu::FragmentState {
302 module: &mesh_shader,
303 entry_point: Some("fs_main"),
304 targets: &color_targets,
305 compilation_options: wgpu::PipelineCompilationOptions::default(),
306 }),
307 multiview_mask: None,
308 cache: None,
309 });
310
311 let edge = if with_edges {
312 let edge_shader =
313 device.create_shader_module(wgpu::include_wgsl!("../shaders/edge.wgsl"));
314 let edge_color_targets = [
318 Some(wgpu::ColorTargetState {
319 format: color_format,
320 blend: None,
321 write_mask: wgpu::ColorWrites::ALL,
322 }),
323 Some(wgpu::ColorTargetState {
324 format: ID_FORMAT,
325 blend: None,
326 write_mask: wgpu::ColorWrites::empty(),
327 }),
328 ];
329 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
330 label: Some("edge pipeline"),
331 layout: Some(layout),
332 vertex: wgpu::VertexState {
333 module: &edge_shader,
334 entry_point: Some("vs_main"),
335 buffers: &[Some(wgpu::VertexBufferLayout {
336 array_stride: std::mem::size_of::<EdgeVertex>() as wgpu::BufferAddress,
337 step_mode: wgpu::VertexStepMode::Vertex,
338 attributes: &[wgpu::VertexAttribute {
339 format: wgpu::VertexFormat::Float32x3,
340 offset: 0,
341 shader_location: 0,
342 }],
343 })],
344 compilation_options: wgpu::PipelineCompilationOptions::default(),
345 },
346 primitive: wgpu::PrimitiveState {
347 topology: wgpu::PrimitiveTopology::LineList,
348 ..Default::default()
349 },
350 depth_stencil: Some(wgpu::DepthStencilState {
351 format: DEPTH_FORMAT,
352 depth_write_enabled: Some(true),
353 depth_compare: Some(wgpu::CompareFunction::LessEqual),
354 stencil: wgpu::StencilState::default(),
355 bias: wgpu::DepthBiasState::default(),
356 }),
357 multisample: wgpu::MultisampleState::default(),
358 fragment: Some(wgpu::FragmentState {
359 module: &edge_shader,
360 entry_point: Some("fs_main"),
361 targets: &edge_color_targets,
362 compilation_options: wgpu::PipelineCompilationOptions::default(),
363 }),
364 multiview_mask: None,
365 cache: None,
366 });
367 Some(pipeline)
368 } else {
369 None
370 };
371
372 Self { mesh, edge }
373 }
374}
375
376pub struct GeometryBuffers {
378 pub vertex: wgpu::Buffer,
379 pub index: wgpu::Buffer,
380 pub index_count: u32,
381 pub edge: Option<wgpu::Buffer>,
382 pub edge_count: u32,
383}
384
385impl GeometryBuffers {
386 pub fn new(device: &wgpu::Device, mesh: &RenderMesh) -> Self {
387 let vertex = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
388 label: Some("mesh vertices"),
389 contents: bytemuck::cast_slice(&mesh.vertices),
390 usage: wgpu::BufferUsages::VERTEX,
391 });
392 let index = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
393 label: Some("mesh indices"),
394 contents: bytemuck::cast_slice(&mesh.indices),
395 usage: wgpu::BufferUsages::INDEX,
396 });
397 #[allow(clippy::cast_possible_truncation)]
398 let index_count = mesh.indices.len() as u32;
399
400 let (edge, edge_count) = if mesh.edge_vertices.is_empty() {
401 (None, 0)
402 } else {
403 let buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
404 label: Some("edge vertices"),
405 contents: bytemuck::cast_slice(&mesh.edge_vertices),
406 usage: wgpu::BufferUsages::VERTEX,
407 });
408 #[allow(clippy::cast_possible_truncation)]
409 let count = mesh.edge_vertices.len() as u32;
410 (Some(buf), count)
411 };
412
413 Self {
414 vertex,
415 index,
416 index_count,
417 edge,
418 edge_count,
419 }
420 }
421}
422
423pub struct PassTargets<'a> {
425 pub color: &'a wgpu::TextureView,
426 pub id: &'a wgpu::TextureView,
427 pub depth: &'a wgpu::TextureView,
428 pub background: [f32; 4],
429}
430
431pub fn encode_scene(
436 encoder: &mut wgpu::CommandEncoder,
437 pipelines: &Pipelines,
438 globals: &GlobalsBinding,
439 geometry: &GeometryBuffers,
440 targets: &PassTargets<'_>,
441) {
442 let bg = targets.background;
443 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
444 label: Some("mesh + edge pass"),
445 color_attachments: &[
446 Some(wgpu::RenderPassColorAttachment {
447 view: targets.color,
448 depth_slice: None,
449 resolve_target: None,
450 ops: wgpu::Operations {
451 load: wgpu::LoadOp::Clear(wgpu::Color {
452 r: f64::from(bg[0]),
453 g: f64::from(bg[1]),
454 b: f64::from(bg[2]),
455 a: f64::from(bg[3]),
456 }),
457 store: wgpu::StoreOp::Store,
458 },
459 }),
460 Some(wgpu::RenderPassColorAttachment {
461 view: targets.id,
462 depth_slice: None,
463 resolve_target: None,
464 ops: wgpu::Operations {
465 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
467 store: wgpu::StoreOp::Store,
468 },
469 }),
470 ],
471 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
472 view: targets.depth,
473 depth_ops: Some(wgpu::Operations {
474 load: wgpu::LoadOp::Clear(1.0),
475 store: wgpu::StoreOp::Store,
476 }),
477 stencil_ops: None,
478 }),
479 timestamp_writes: None,
480 occlusion_query_set: None,
481 multiview_mask: None,
482 });
483
484 pass.set_bind_group(0, &globals.bind_group, &[]);
485 pass.set_pipeline(&pipelines.mesh);
486 pass.set_vertex_buffer(0, geometry.vertex.slice(..));
487 pass.set_index_buffer(geometry.index.slice(..), wgpu::IndexFormat::Uint32);
488 pass.draw_indexed(0..geometry.index_count, 0, 0..1);
489
490 if let (Some(edge_pipeline), Some(edge_buf)) = (pipelines.edge.as_ref(), geometry.edge.as_ref())
491 {
492 pass.set_pipeline(edge_pipeline);
493 pass.set_vertex_buffer(0, edge_buf.slice(..));
494 pass.draw(0..geometry.edge_count, 0..1);
495 }
496}
497
498pub fn build_globals(cam: &Camera, center: brepkit_math::vec::Point3, ambient: f32) -> Globals {
501 let view_proj = crate::camera::view_proj_rtc(cam, center);
502 let view_dir = cam.view_direction();
503 #[allow(clippy::cast_possible_truncation)]
504 Globals {
505 view_proj,
506 view_dir: [
507 view_dir.x() as f32,
508 view_dir.y() as f32,
509 view_dir.z() as f32,
510 0.0,
511 ],
512 ambient,
513 selected_id: 0,
514 _pad: [0.0; 2],
515 }
516}
517
518#[allow(clippy::too_many_lines)]
528pub fn render(
529 mesh: &RenderMesh,
530 cam: &Camera,
531 opts: &RenderOpts,
532) -> Result<RenderOutput, RenderError> {
533 if opts.width == 0 || opts.height == 0 {
534 return Err(RenderError::InvalidSize {
535 width: opts.width,
536 height: opts.height,
537 });
538 }
539
540 let ctx = GpuContext::new()?;
541 let device = &ctx.device;
542 let queue = &ctx.queue;
543
544 let (width, height) = (opts.width, opts.height);
545 let max = device.limits().max_texture_dimension_2d;
548 if width > max || height > max {
549 return Err(RenderError::SizeTooLarge { width, height, max });
550 }
551 let extent = wgpu::Extent3d {
552 width,
553 height,
554 depth_or_array_layers: 1,
555 };
556
557 let color_tex = device.create_texture(&wgpu::TextureDescriptor {
559 label: Some("color target"),
560 size: extent,
561 mip_level_count: 1,
562 sample_count: 1,
563 dimension: wgpu::TextureDimension::D2,
564 format: COLOR_FORMAT_OFFSCREEN,
565 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
566 view_formats: &[],
567 });
568 let depth_tex = device.create_texture(&wgpu::TextureDescriptor {
569 label: Some("depth target"),
570 size: extent,
571 mip_level_count: 1,
572 sample_count: 1,
573 dimension: wgpu::TextureDimension::D2,
574 format: DEPTH_FORMAT,
575 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
576 view_formats: &[],
577 });
578 let id_tex = device.create_texture(&wgpu::TextureDescriptor {
579 label: Some("id target"),
580 size: extent,
581 mip_level_count: 1,
582 sample_count: 1,
583 dimension: wgpu::TextureDimension::D2,
584 format: ID_FORMAT,
585 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
586 view_formats: &[],
587 });
588 let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
589 let depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
590 let id_view = id_tex.create_view(&wgpu::TextureViewDescriptor::default());
591
592 let globals = build_globals(cam, mesh.center, opts.ambient);
594 let globals_binding = GlobalsBinding::new(device, &globals);
595 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
596 label: Some("pipeline layout"),
597 bind_group_layouts: &[Some(&globals_binding.layout)],
598 immediate_size: 0,
599 });
600 let with_edges = opts.edges && !mesh.edge_vertices.is_empty();
601 let pipelines = Pipelines::new(device, &pipeline_layout, COLOR_FORMAT_OFFSCREEN, with_edges);
602 let geometry = GeometryBuffers::new(device, mesh);
603
604 let color_bpp = 4_u32; let id_bpp = 4_u32; let color_padded_bpr = padded_bytes_per_row(width, color_bpp);
609 let id_padded_bpr = padded_bytes_per_row(width, id_bpp);
610
611 let color_readback = device.create_buffer(&wgpu::BufferDescriptor {
612 label: Some("color readback"),
613 size: u64::from(color_padded_bpr) * u64::from(height),
614 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
615 mapped_at_creation: false,
616 });
617 let id_readback = device.create_buffer(&wgpu::BufferDescriptor {
618 label: Some("id readback"),
619 size: u64::from(id_padded_bpr) * u64::from(height),
620 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
621 mapped_at_creation: false,
622 });
623
624 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
626 label: Some("encoder"),
627 });
628 encode_scene(
629 &mut encoder,
630 &pipelines,
631 &globals_binding,
632 &geometry,
633 &PassTargets {
634 color: &color_view,
635 id: &id_view,
636 depth: &depth_view,
637 background: opts.background,
638 },
639 );
640
641 encoder.copy_texture_to_buffer(
642 wgpu::TexelCopyTextureInfo {
643 texture: &color_tex,
644 mip_level: 0,
645 origin: wgpu::Origin3d::ZERO,
646 aspect: wgpu::TextureAspect::All,
647 },
648 wgpu::TexelCopyBufferInfo {
649 buffer: &color_readback,
650 layout: wgpu::TexelCopyBufferLayout {
651 offset: 0,
652 bytes_per_row: Some(color_padded_bpr),
653 rows_per_image: Some(height),
654 },
655 },
656 extent,
657 );
658 encoder.copy_texture_to_buffer(
659 wgpu::TexelCopyTextureInfo {
660 texture: &id_tex,
661 mip_level: 0,
662 origin: wgpu::Origin3d::ZERO,
663 aspect: wgpu::TextureAspect::All,
664 },
665 wgpu::TexelCopyBufferInfo {
666 buffer: &id_readback,
667 layout: wgpu::TexelCopyBufferLayout {
668 offset: 0,
669 bytes_per_row: Some(id_padded_bpr),
670 rows_per_image: Some(height),
671 },
672 },
673 extent,
674 );
675
676 queue.submit(Some(encoder.finish()));
677
678 let color_bytes = map_and_read(device, &color_readback)?;
680 let id_bytes = map_and_read(device, &id_readback)?;
681
682 let color = unpad_to_rgba(&color_bytes, width, height, color_padded_bpr);
683 let id_buffer = unpad_to_u32(&id_bytes, width, height, id_padded_bpr);
684
685 Ok(RenderOutput {
686 color,
687 id_buffer,
688 width,
689 height,
690 })
691}
692
693pub fn padded_bytes_per_row(width: u32, bytes_per_pixel: u32) -> u32 {
695 let unpadded = width * bytes_per_pixel;
696 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
697 unpadded.div_ceil(align) * align
698}
699
700pub fn map_and_read(device: &wgpu::Device, buffer: &wgpu::Buffer) -> Result<Vec<u8>, RenderError> {
702 use std::sync::mpsc;
703 let (tx, rx) = mpsc::channel();
704 buffer.slice(..).map_async(wgpu::MapMode::Read, move |res| {
705 let _ = tx.send(res);
706 });
707 device
708 .poll(wgpu::PollType::Wait {
709 submission_index: None,
710 timeout: None,
711 })
712 .map_err(|e| RenderError::Poll(e.to_string()))?;
713 match rx.recv() {
714 Ok(Ok(())) => {}
715 Ok(Err(e)) => return Err(RenderError::BufferMap(e.to_string())),
716 Err(e) => return Err(RenderError::BufferMap(e.to_string())),
717 }
718 let data = buffer
719 .slice(..)
720 .get_mapped_range()
721 .map_err(|e| RenderError::BufferMap(e.to_string()))?
722 .to_vec();
723 buffer.unmap();
724 Ok(data)
725}
726
727pub fn unpad_to_rgba(bytes: &[u8], width: u32, height: u32, padded_bpr: u32) -> image::RgbaImage {
729 let row_len = (width * 4) as usize;
730 let mut packed = Vec::with_capacity(row_len * height as usize);
731 for row in 0..height as usize {
732 let start = row * padded_bpr as usize;
733 if let Some(slice) = bytes.get(start..start + row_len) {
734 packed.extend_from_slice(slice);
735 } else {
736 packed.resize(packed.len() + row_len, 0);
737 }
738 }
739 image::RgbaImage::from_raw(width, height, packed)
740 .unwrap_or_else(|| image::RgbaImage::new(width, height))
741}
742
743pub fn unpad_to_u32(bytes: &[u8], width: u32, height: u32, padded_bpr: u32) -> Vec<u32> {
745 let mut out = Vec::with_capacity((width * height) as usize);
746 for row in 0..height as usize {
747 let row_start = row * padded_bpr as usize;
748 for col in 0..width as usize {
749 let off = row_start + col * 4;
750 let v = bytes
751 .get(off..off + 4)
752 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
753 .unwrap_or(0);
754 out.push(v);
755 }
756 }
757 out
758}