1use crate::camera::Camera;
19use crate::color::{solid_color_srgb, srgb_to_linear};
20use crate::scene::{RenderScene, SolidDisplay};
21use crate::style::{Emphasis, EmphasisState, FaceColorMode, RenderSettings, Rgba};
22use std::collections::{HashMap, HashSet};
23use wgpu::util::DeviceExt;
24
25pub const SAMPLES: u32 = 4;
27pub const COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
30const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
31
32const EDGE_NUDGE: f32 = 2e-4;
35const LINE_ART_PLACED_OUTLINE_PX: f32 = 1.5;
37const LINE_ART_ADDING_OUTLINE_PX: f32 = 2.5;
38const LINE_ART_OUTLINE_DEPTH: f32 = 2e-3;
41
42#[repr(C)]
43#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
44struct Globals {
45 view_proj: [[f32; 4]; 4],
46 viewport: [f32; 4],
47 forward: [f32; 4],
48}
49
50#[repr(C)]
53#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
54struct ModelParams {
55 model: [f32; 16],
56 normal: [f32; 16],
57}
58
59const IDENTITY4: [f32; 16] = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0];
60
61impl ModelParams {
62 fn of(pose: Option<&[f32; 16]>) -> Self {
63 let Some(m) = pose else { return Self { model: IDENTITY4, normal: IDENTITY4 } };
64 let a = |row: usize, col: usize| m[col * 4 + row] as f64;
67 let cof = |r0: usize, r1: usize, c0: usize, c1: usize| a(r0, c0) * a(r1, c1) - a(r0, c1) * a(r1, c0);
68 let det = a(0, 0) * cof(1, 2, 1, 2) - a(0, 1) * cof(1, 2, 0, 2) + a(0, 2) * cof(1, 2, 0, 1);
69 let mut normal = IDENTITY4;
70 if det.abs() > 1e-30 {
71 let c = [
73 [cof(1, 2, 1, 2), -cof(1, 2, 0, 2), cof(1, 2, 0, 1)],
74 [-cof(0, 2, 1, 2), cof(0, 2, 0, 2), -cof(0, 2, 0, 1)],
75 [cof(0, 1, 1, 2), -cof(0, 1, 0, 2), cof(0, 1, 0, 1)],
76 ];
77 for row in 0..3 {
78 for col in 0..3 {
79 normal[col * 4 + row] = (c[row][col] / det) as f32;
80 }
81 }
82 }
83 Self { model: *m, normal }
84 }
85}
86
87struct ModelArena {
92 buf: wgpu::Buffer,
93 bind: wgpu::BindGroup,
94 capacity: u32,
95 data: Vec<u8>,
96 free: Vec<u32>,
97 next: u32,
98 dirty: Option<(usize, usize)>,
100 fresh: bool,
102}
103
104const MODEL_STRIDE: u64 = 256;
106
107fn model_binding(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, buf: &wgpu::Buffer, label: &str) -> wgpu::BindGroup {
108 device.create_bind_group(&wgpu::BindGroupDescriptor {
109 label: Some(label),
110 layout,
111 entries: &[wgpu::BindGroupEntry {
112 binding: 0,
113 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
114 buffer: buf,
115 offset: 0,
116 size: std::num::NonZeroU64::new(std::mem::size_of::<ModelParams>() as u64),
117 }),
118 }],
119 })
120}
121
122fn model_buffer(device: &wgpu::Device, slots: u32, label: &str) -> wgpu::Buffer {
123 device.create_buffer(&wgpu::BufferDescriptor {
124 label: Some(label),
125 size: slots as u64 * MODEL_STRIDE,
126 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
127 mapped_at_creation: false,
128 })
129}
130
131impl ModelArena {
132 fn new(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, capacity: u32) -> Self {
133 let capacity = capacity.max(16);
134 let buf = model_buffer(device, capacity, "solid poses");
135 let bind = model_binding(device, layout, &buf, "solid poses");
136 Self { buf, bind, capacity, data: vec![0; capacity as usize * MODEL_STRIDE as usize], free: Vec::new(), next: 0, dirty: None, fresh: true }
137 }
138
139 fn alloc(&mut self) -> u32 {
140 self.free.pop().unwrap_or_else(|| {
141 self.next += 1;
142 self.next - 1
143 })
144 }
145
146 fn release(&mut self, slot: u32) {
147 self.free.push(slot);
148 }
149
150 fn set(&mut self, slot: u32, pose: Option<&[f32; 16]>) {
151 let need = (slot as usize + 1) * MODEL_STRIDE as usize;
152 if self.data.len() < need {
153 self.data.resize(need.next_power_of_two(), 0);
154 }
155 let at = slot as usize * MODEL_STRIDE as usize;
156 let params = ModelParams::of(pose);
157 let bytes = bytemuck::bytes_of(¶ms);
158 self.data[at..at + bytes.len()].copy_from_slice(bytes);
159 let (lo, hi) = self.dirty.unwrap_or((at, at + bytes.len()));
160 self.dirty = Some((lo.min(at), hi.max(at + bytes.len())));
161 }
162
163 fn offset(slot: u32) -> u32 {
164 (slot as u64 * MODEL_STRIDE) as u32
165 }
166
167 fn flush(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, layout: &wgpu::BindGroupLayout) {
169 let slots = (self.data.len() / MODEL_STRIDE as usize) as u32;
170 if slots > self.capacity {
171 self.capacity = slots;
172 self.buf = model_buffer(device, slots, "solid poses");
173 self.bind = model_binding(device, layout, &self.buf, "solid poses");
174 self.fresh = true;
175 }
176 if std::mem::take(&mut self.fresh) {
177 queue.write_buffer(&self.buf, 0, &self.data[..self.capacity as usize * MODEL_STRIDE as usize]);
178 } else if let Some((lo, hi)) = self.dirty {
179 let hi = hi.next_multiple_of(4);
180 queue.write_buffer(&self.buf, lo as u64, &self.data[lo..hi]);
181 }
182 self.dirty = None;
183 }
184}
185
186struct IdentityModel {
189 _buf: wgpu::Buffer,
190 bind: wgpu::BindGroup,
191}
192
193impl IdentityModel {
194 fn new(device: &wgpu::Device, queue: &wgpu::Queue, layout: &wgpu::BindGroupLayout) -> Self {
195 let buf = model_buffer(device, 1, "identity pose");
196 queue.write_buffer(&buf, 0, bytemuck::bytes_of(&ModelParams::of(None)));
197 let bind = model_binding(device, layout, &buf, "identity pose");
198 Self { _buf: buf, bind }
199 }
200}
201
202#[repr(C)]
203#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
204struct StyleParams {
205 color: [f32; 4],
206 params: [f32; 4],
207}
208
209#[repr(C)]
210#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
211struct MeshVertex {
212 position: [f32; 3],
213 normal: [f32; 3],
214}
215
216#[repr(C)]
217#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
218struct EdgeInstance {
219 p0: [f32; 3],
220 p1: [f32; 3],
221}
222
223#[repr(C)]
224#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
225struct PointInstance {
226 center: [f32; 3],
227}
228
229#[repr(C)]
231#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
232struct OverlayLineInstance {
233 p0: [f32; 3],
234 p1: [f32; 3],
235 color: [f32; 4],
236}
237
238#[repr(C)]
240#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
241struct OverlayTriVertex {
242 position: [f32; 3],
243 normal: [f32; 3],
244 color: [f32; 4],
245}
246
247struct StyleBuf {
249 buf: wgpu::Buffer,
250 bind: wgpu::BindGroup,
251}
252
253impl StyleBuf {
254 fn new(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, label: &str) -> Self {
255 let buf = device.create_buffer(&wgpu::BufferDescriptor {
256 label: Some(label),
257 size: std::mem::size_of::<StyleParams>() as u64,
258 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
259 mapped_at_creation: false,
260 });
261 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
262 label: Some(label),
263 layout,
264 entries: &[wgpu::BindGroupEntry {
265 binding: 0,
266 resource: buf.as_entire_binding(),
267 }],
268 });
269 Self { buf, bind }
270 }
271
272 fn write(&self, queue: &wgpu::Queue, color: Rgba, params: [f32; 4]) {
273 queue.write_buffer(&self.buf, 0, bytemuck::bytes_of(&StyleParams { color, params }));
274 }
275}
276
277struct GlobalStyles {
279 face_selected: StyleBuf,
280 face_hovered: StyleBuf,
281 edge_base: StyleBuf,
282 edge_selected: StyleBuf,
283 edge_hovered: StyleBuf,
284 edge_hidden: StyleBuf,
285 edge_aux: StyleBuf,
287 boundary: StyleBuf,
288 point_base: StyleBuf,
289 point_selected: StyleBuf,
290 point_hovered: StyleBuf,
291 axis_x: StyleBuf,
292 axis_y: StyleBuf,
293 axis_z: StyleBuf,
294 overlay_line: StyleBuf,
296 line_art_placed_face: StyleBuf,
300 line_art_adding_face: StyleBuf,
301 line_art_placed_edge: StyleBuf,
302 line_art_adding_edge: StyleBuf,
303 line_art_placed_outline: StyleBuf,
304 line_art_adding_outline: StyleBuf,
305}
306
307struct FaceRange {
308 first_index: u32,
309 index_count: u32,
310 wire_first: u32,
315 wire_count: u32,
316 style: Option<u32>,
320}
321
322#[derive(Default)]
332struct EdgeHasher(u64);
333
334impl std::hash::Hasher for EdgeHasher {
335 fn write(&mut self, bytes: &[u8]) {
336 for &byte in bytes {
337 self.write_u32(u32::from(byte));
338 }
339 }
340
341 fn write_u32(&mut self, value: u32) {
342 self.0 = (self.0 ^ u64::from(value)).wrapping_mul(0x9E37_79B9_7F4A_7C15);
345 }
346
347 fn finish(&self) -> u64 {
348 self.0 ^ (self.0 >> 32)
349 }
350}
351
352type BuildEdgeHasher = std::hash::BuildHasherDefault<EdgeHasher>;
353
354struct EdgeRange {
355 first_instance: u32,
356 instance_count: u32,
357}
358
359struct GpuSolid {
360 revision: u64,
361 shape: u64,
365 vertex_count: u32,
366 vertex_buf: wgpu::Buffer,
367 index_buf: wgpu::Buffer,
368 wire_index_buf: wgpu::Buffer,
370 wire_index_count: u32,
371 faces: Vec<FaceRange>,
372 edge_buf: Option<wgpu::Buffer>,
373 edges: Vec<EdgeRange>,
374 edge_instances: u32,
375 occludable_edge_instances: u32,
380 point_buf: Option<wgpu::Buffer>,
381 point_count: u32,
382 base_style: StyleBuf,
384 face_styles: Vec<StyleBuf>,
389 boundary: Option<BoundaryBuf>,
392 model_slot: u32,
395 holds_shape: bool,
399}
400
401struct BoundaryBuf {
402 emphasis_generation: u64,
403 revision: u64,
404 buf: Option<wgpu::Buffer>,
405 count: u32,
406}
407
408pub const WORK_PART_CONTEXT_RGBA: [f32; 4] = [0.45, 0.55, 0.65, 0.22];
412
413#[derive(Default)]
416pub struct GpuScene {
417 solids: HashMap<String, GpuSolid>,
418 order: Vec<String>,
420 settings_generation: u64,
421 axis_buf: Option<wgpu::Buffer>,
422 uploads: u64,
425 reposes: u64,
427 pose_writes: u64,
429 models: Option<ModelArena>,
431}
432
433impl GpuScene {
434 pub fn upload_count(&self) -> u64 {
437 self.uploads
438 }
439
440 pub fn repose_count(&self) -> u64 {
443 self.reposes
444 }
445
446 pub fn pose_write_count(&self) -> u64 {
449 self.pose_writes
450 }
451}
452
453struct CachedTargets {
454 width: u32,
455 height: u32,
456 msaa_view: wgpu::TextureView,
457 depth_view: wgpu::TextureView,
458}
459
460pub struct FrameParams<'a> {
462 pub camera: &'a Camera,
463 pub width: u32,
465 pub height: u32,
466 pub dpr: f32,
468 pub settings: &'a RenderSettings,
469 pub emphasis: &'a Emphasis,
470 pub world_per_pixel: f64,
472 pub overlay: Option<&'a crate::widgets::WidgetOverlay>,
475}
476
477#[cfg(not(target_arch = "wasm32"))]
482pub fn create_headless_device() -> Result<(wgpu::Device, wgpu::Queue, String), String> {
483 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
484 let want = std::env::var("BREP_RENDER_ADAPTER").ok();
485 let adapter = if let Some(want) = &want {
486 let want_lower = want.to_lowercase();
487 pollster::block_on(instance.enumerate_adapters(wgpu::Backends::all()))
488 .into_iter()
489 .find(|adapter| adapter.get_info().name.to_lowercase().contains(&want_lower))
490 .ok_or_else(|| format!("no adapter matching BREP_RENDER_ADAPTER={want}"))?
491 } else {
492 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
493 power_preference: wgpu::PowerPreference::HighPerformance,
494 compatible_surface: None,
495 force_fallback_adapter: false,
496 }))
497 .map_err(|error| format!("no wgpu adapter available: {error}"))?
498 };
499 let info = adapter.get_info();
500 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
501 label: Some("brep-render"),
502 ..Default::default()
503 }))
504 .map_err(|error| format!("wgpu device request failed: {error}"))?;
505 Ok((device, queue, format!("{} ({:?})", info.name, info.backend)))
506}
507
508pub struct RenderCore {
510 pub device: wgpu::Device,
511 pub queue: wgpu::Queue,
512 format: wgpu::TextureFormat,
513 globals_buf: wgpu::Buffer,
514 globals_bind: wgpu::BindGroup,
515 style_layout: wgpu::BindGroupLayout,
516 model_layout: wgpu::BindGroupLayout,
519 identity_model: IdentityModel,
522 mesh_pipeline: wgpu::RenderPipeline,
523 hull_pipeline: wgpu::RenderPipeline,
525 context_pipeline: wgpu::RenderPipeline,
526 context_style: StyleBuf,
527 context_selected: StyleBuf,
530 context_hovered: StyleBuf,
531 wire_pipeline: wgpu::RenderPipeline,
532 edge_visible_pipeline: wgpu::RenderPipeline,
533 edge_hidden_pipeline: wgpu::RenderPipeline,
534 point_pipeline: wgpu::RenderPipeline,
535 overlay_tri_pipeline: wgpu::RenderPipeline,
538 overlay_tri_nodepth_pipeline: wgpu::RenderPipeline,
544 overlay_line_pipeline: wgpu::RenderPipeline,
545 vc_globals_buf: wgpu::Buffer,
547 vc_globals_bind: wgpu::BindGroup,
548 styles: GlobalStyles,
549 targets: Option<CachedTargets>,
550}
551
552#[cfg(not(target_arch = "wasm32"))]
554fn encode_png(rgb: &[u8], width: u32, height: u32) -> Result<Vec<u8>, String> {
555 brep_gizmos::raster::encode_png(rgb, width, height, png::ColorType::Rgb)
556}
557
558mod pipelines;
559mod scene_sync;
560mod frame;