1use crate::{
20 CopyVarsError, Gpu, RegPipeline,
21 buf::{BufferSizeError, FlexBuffer, buffer_ro, buffer_rw, buffer_uniform},
22 color::{ColorWorkspace as GenericColorWorkspace, ShapeColorBuffers},
23 pixel::{PixelBufferTag, RawDistancePixel},
24 shaders, tag,
25};
26use fidget_core::{render::ImageSize, shape::ShapeVars};
27use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
28
29pub use crate::voxel::effects::ColorError;
30
31const MERGE_SHADER: &str = include_str!("shaders/merge.wgsl");
32const COLOR_SHADER: &str = include_str!("shaders/color.wgsl");
33
34pub type ColorWorkspace = GenericColorWorkspace<ColorConfig>;
36
37fn merge_shader() -> String {
39 MERGE_SHADER.to_owned() + shaders::COMMON + super::DISTANCE_PIXEL_SHADER
40}
41
42fn color_shader(reg_count: u8) -> String {
43 let mut shader_code = shaders::opcode_constants();
44 shader_code += &format!("const REG_COUNT: u32 = {reg_count};");
45 shader_code
46 + COLOR_SHADER
47 + super::TRANSFORM_INPUT
48 + super::DISTANCE_PIXEL_SHADER
49 + shaders::COMMON
50 + shaders::TAPE_INTERPRETER
51 + shaders::DUMMY_STACK
52 + shaders::FLOAT_OPS
53}
54
55#[derive(Debug, thiserror::Error)]
59#[error(
60 "image size mismatch: expected {} × {}, got {} × {}",
61 expected.width(), expected.height(),
62 actual.width(), actual.height(),
63)]
64pub struct ImageSizeMismatch {
65 expected: ImageSize,
66 actual: ImageSize,
67}
68
69#[derive(Debug, thiserror::Error)]
71pub enum MergeError {
72 #[error(transparent)]
74 ImageSizeMismatch(#[from] ImageSizeMismatch),
75
76 #[error(transparent)]
78 OutputSize(BufferSizeError),
79}
80
81tag!(
84 pub PixelDistanceBufferTag,
85 RawDistancePixel,
86 ImageSize,
87 STORAGE | COPY_SRC,
88 "Buffer tag for on-GPU distance images"
89);
90
91tag!(
92 pub PixelColorBufferTag,
93 u32, ImageSize,
95 STORAGE | COPY_SRC,
96 "Buffer tag for on-GPU color images"
97);
98
99pub struct MergeWorkspace {
101 config: wgpu::Buffer,
102 distance: FlexBuffer<PixelDistanceBufferTag>,
103 color: FlexBuffer<PixelColorBufferTag>,
104
105 color_bind_group: std::cell::OnceCell<wgpu::BindGroup>,
106
107 image_count: usize,
109
110 has_color: bool,
115}
116
117impl MergeWorkspace {
118 pub fn reset(&mut self) {
123 self.has_color = false;
124 self.image_count = 0;
125 }
126
127 pub fn has_color(&self) -> bool {
129 self.has_color
130 }
131
132 pub fn output_distance(&self) -> &FlexBuffer<PixelDistanceBufferTag> {
134 &self.distance
135 }
136
137 pub fn output_color(&self) -> Option<&FlexBuffer<PixelColorBufferTag>> {
139 if self.has_color {
140 Some(&self.color)
141 } else {
142 None
143 }
144 }
145}
146
147#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
149#[cfg_attr(test, derive(facet::Facet))]
150#[repr(C)]
151pub(crate) struct MergeConfig {
152 pub image_size: [u32; 2],
154
155 pub remove_nans: u32,
157
158 pub index_base: u32,
162}
163
164pub struct Context {
168 gpu: Gpu,
169
170 merge_bind_group_layout: wgpu::BindGroupLayout,
171 merge_pipeline: wgpu::ComputePipeline,
172
173 color_ctx: ColorContext,
174}
175
176impl Context {
177 pub fn new(gpu: &Gpu) -> Self {
179 let merge_bind_group_layout = gpu.device.create_bind_group_layout(
180 &wgpu::BindGroupLayoutDescriptor {
181 label: None,
182 entries: &[
183 buffer_uniform(0),
184 buffer_ro(1), buffer_rw(2), buffer_rw(3), ],
188 },
189 );
190 let shader_code = merge_shader();
191 let pipeline_layout = gpu.device.create_pipeline_layout(
192 &wgpu::PipelineLayoutDescriptor {
193 label: Some("effects merge pipeline"),
194 bind_group_layouts: &[Some(&merge_bind_group_layout)],
195 immediate_size: 0u32,
196 },
197 );
198 let shader_module =
199 gpu.device
200 .create_shader_module(wgpu::ShaderModuleDescriptor {
201 label: Some("effects merge shader module"),
202 source: wgpu::ShaderSource::Wgsl(shader_code.into()),
203 });
204 let merge_pipeline = gpu.device.create_compute_pipeline(
205 &wgpu::ComputePipelineDescriptor {
206 label: Some("effects merge compute pipeline"),
207 layout: Some(&pipeline_layout),
208 module: &shader_module,
209 entry_point: Some("merge_main"),
210 compilation_options: Default::default(),
211 cache: None,
212 },
213 );
214
215 let color_ctx = ColorContext::new(&gpu.device);
216
217 Self {
218 gpu: gpu.clone(),
219 merge_bind_group_layout,
220 merge_pipeline,
221 color_ctx,
222 }
223 }
224
225 pub fn submit_merge(
231 &self,
232 image: &FlexBuffer<PixelBufferTag>,
233 remove_nans: bool,
234 buf: &mut MergeWorkspace,
235 ) -> Result<(), MergeError> {
236 let size = image.size();
237 if buf.image_count > 0 {
238 let buf_size = buf.distance.size();
239 if size != buf_size {
240 return Err(ImageSizeMismatch {
241 expected: size,
242 actual: buf_size,
243 }
244 .into());
245 }
246 } else {
247 let distance_changed = !matches!(
251 buf.distance
252 .grow_to_fit(&self.gpu.device, size)
253 .map_err(MergeError::OutputSize)?,
254 std::cmp::Ordering::Equal
255 );
256 let color_changed = !matches!(
257 buf.color
258 .grow_to_fit(&self.gpu.device, size)
259 .map_err(MergeError::OutputSize)?,
260 std::cmp::Ordering::Equal
261 );
262 assert_eq!(distance_changed, color_changed, "mismatched buffers");
263 if distance_changed {
264 buf.color_bind_group = Default::default();
265 }
266 }
267 buf.has_color = false;
268 let mut encoder = self.gpu.device.create_command_encoder(
269 &wgpu::CommandEncoderDescriptor {
270 label: Some("merge compute encoder"),
271 },
272 );
273 {
275 let mut compute_pass =
276 encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
277 label: Some("merge compute pass"),
278 timestamp_writes: None, });
280 compute_pass.set_pipeline(&self.merge_pipeline);
281 let cfg = MergeConfig {
282 image_size: [size.width(), size.height()],
283 remove_nans: remove_nans as u32,
284 index_base: buf.image_count as u32,
285 };
286 {
287 let mut writer = self
288 .gpu
289 .queue
290 .write_buffer_with(
291 &buf.config,
292 0,
293 (std::mem::size_of::<MergeConfig>() as u64)
294 .try_into()
295 .unwrap(),
296 )
297 .unwrap();
298 writer.copy_from_slice(cfg.as_bytes());
299 }
300
301 let bg =
302 self.gpu
303 .device
304 .create_bind_group(&wgpu::BindGroupDescriptor {
305 label: Some("merge bind group"),
306 layout: &self.merge_bind_group_layout,
307 entries: &[
308 wgpu::BindGroupEntry {
309 binding: 0,
310 resource: buf.config.as_entire_binding(),
311 },
312 wgpu::BindGroupEntry {
313 binding: 1,
314 resource: image.bind_active(),
315 },
316 wgpu::BindGroupEntry {
317 binding: 2,
318 resource: buf.distance.bind_active(),
319 },
320 wgpu::BindGroupEntry {
321 binding: 3,
322 resource: buf.color.bind_active(),
323 },
324 ],
325 });
326 compute_pass.set_bind_group(0, Some(&bg), &[]);
327 compute_pass.dispatch_workgroups(
328 size.width().div_ceil(8),
329 size.height().div_ceil(8),
330 1,
331 );
332 buf.image_count += 1;
333 }
334 self.gpu.queue.submit(Some(encoder.finish()));
335 Ok(())
336 }
337
338 pub fn merge_workspace(&self) -> MergeWorkspace {
340 let config = self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
341 label: Some("config"),
342 size: std::mem::size_of::<MergeConfig>() as u64,
343 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
344 mapped_at_creation: false,
345 });
346 let distance = FlexBuffer::new(
347 &self.gpu.device,
348 "pixel merge distance",
349 64.into(),
350 )
351 .unwrap();
352 let color =
353 FlexBuffer::new(&self.gpu.device, "pixel merge color", 64.into())
354 .unwrap();
355 MergeWorkspace {
356 config,
357 distance,
358 color,
359 color_bind_group: Default::default(),
360 image_count: 0,
361 has_color: false,
362 }
363 }
364
365 pub fn submit_color(
371 &self,
372 merge: &mut MergeWorkspace,
373 settings: ColorSettings,
374 shape: &ShapeColorBuffers,
375 bufs: &mut ColorWorkspace,
376 ) -> Result<(), ColorError> {
377 self.submit_color_with_vars(
378 merge,
379 settings,
380 shape,
381 bufs,
382 &Default::default(),
383 )
384 }
385
386 pub fn submit_color_with_vars(
392 &self,
393 merge: &mut MergeWorkspace,
394 settings: ColorSettings,
395 shape: &ShapeColorBuffers,
396 bufs: &mut ColorWorkspace,
397 vars: &ShapeVars<f32>,
398 ) -> Result<(), ColorError> {
399 self.color_ctx
400 .submit(merge, settings, shape, bufs, vars, &self.gpu)
401 }
402
403 pub fn color_workspace(&self) -> ColorWorkspace {
405 ColorWorkspace::new(&self.gpu.device)
406 }
407}
408
409#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
416#[cfg_attr(test, derive(facet::Facet))]
417#[repr(C)]
418#[doc(hidden)]
419pub struct ColorConfig {
420 mat: [f32; 12],
422
423 axes: [u32; 3],
427
428 z: f32,
430
431 image_size: [u32; 2],
433
434 only_filled: u32,
436
437 _pad: [u32; 3],
439 }
441
442#[derive(Copy, Clone, Debug)]
444pub struct ColorSettings {
445 pub z: f32,
447
448 pub only_filled: bool,
450
451 pub world_to_model: nalgebra::Matrix3<f32>,
456}
457
458struct ColorContext {
459 config_bind_group_layout: wgpu::BindGroupLayout,
461
462 image_bind_group_layout: wgpu::BindGroupLayout,
464
465 color_pipeline: RegPipeline,
467}
468
469impl ColorContext {
470 pub fn new(device: &wgpu::Device) -> Self {
471 let config_bind_group_layout =
472 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
473 label: Some("color config and shape"),
474 entries: &[buffer_ro(0), buffer_ro(1), buffer_ro(2)],
475 });
476 let image_bind_group_layout =
477 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
478 label: Some("color images"),
479 entries: &[
480 buffer_ro(0), buffer_rw(1), ],
483 });
484
485 let pipeline_layout =
486 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
487 label: Some("color pipeline"),
488 bind_group_layouts: &[
489 Some(&config_bind_group_layout),
490 Some(&image_bind_group_layout),
491 ],
492 immediate_size: 0u32,
493 });
494 let device_ = device.clone();
495 let color_pipeline = RegPipeline::build(Box::new(move |reg_count| {
496 let shader_code = color_shader(reg_count);
497 let shader_module =
498 device_.create_shader_module(wgpu::ShaderModuleDescriptor {
499 label: None,
500 source: wgpu::ShaderSource::Wgsl(shader_code.into()),
501 });
502 device_.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
503 label: Some(&format!("color ({reg_count})")),
504 layout: Some(&pipeline_layout),
505 module: &shader_module,
506 entry_point: Some("color_main"),
507 compilation_options: Default::default(),
508 cache: None,
509 })
510 }));
511
512 Self {
513 config_bind_group_layout,
514 image_bind_group_layout,
515 color_pipeline,
516 }
517 }
518
519 fn submit(
520 &self,
521 image: &mut MergeWorkspace,
522 settings: ColorSettings,
523 shape: &ShapeColorBuffers,
524 bufs: &mut ColorWorkspace,
525 vars: &ShapeVars<f32>,
526 gpu: &Gpu,
527 ) -> Result<(), ColorError> {
528 if image.image_count != shape.shape_count() {
529 return Err(ColorError::BadShapeCount {
530 merge_count: image.image_count,
531 shape_count: shape.shape_count(),
532 });
533 } else if image.has_color {
534 return Err(ColorError::AlreadyHasColor);
535 }
536 let size = image.distance.size();
537 let mat = settings.world_to_model
538 * ImageSize::new(size.width(), size.height()).screen_to_world();
539 let mut mat4 = nalgebra::Matrix4x3::<f32>::identity();
540 mat4.fixed_view_mut::<3, 3>(0, 0).copy_from(&mat);
541
542 bufs.copy_vars(gpu, shape.var_map(), vars)
543 .map_err(|e| match e {
544 CopyVarsError::BufferSize(b) => ColorError::VarBufferSize(b),
545 CopyVarsError::MissingVar(v) => ColorError::MissingVar(v),
546 })?;
547 let config = ColorConfig {
548 mat: mat4.data.as_slice().try_into().unwrap(),
549 axes: shape.axes(),
550 image_size: [size.width(), size.height()],
551 only_filled: settings.only_filled.into(),
552 _pad: [0; 3],
553 z: settings.z,
554 };
555 bufs.copy_config_and_tape(gpu, &config, shape.bytecode())
556 .map_err(ColorError::ConfigBufferSize)?;
557 bufs.copy_shape_starts(gpu, shape.shape_start())
558 .expect("shape starts should always fit if shape bytecode fits");
559
560 let config_bg =
561 bufs.config_bind_group(&gpu.device, &self.config_bind_group_layout);
562
563 let mut encoder = gpu.device.create_command_encoder(
565 &wgpu::CommandEncoderDescriptor { label: None },
566 );
567 {
568 let mut compute_pass =
569 encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
570 label: None,
571 timestamp_writes: None, });
573 compute_pass.set_bind_group(0, config_bg, &[]);
574 let image_bg = image.color_bind_group.get_or_init(|| {
575 gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
576 label: Some("color image bind group"),
577 layout: &self.image_bind_group_layout,
578 entries: &[
579 wgpu::BindGroupEntry {
580 binding: 0,
581 resource: image.distance.bind_active(),
582 },
583 wgpu::BindGroupEntry {
584 binding: 1,
585 resource: image.color.bind_active(),
586 },
587 ],
588 })
589 });
590 compute_pass.set_bind_group(1, image_bg, &[]);
591 compute_pass
592 .set_pipeline(self.color_pipeline.get(shape.reg_count()));
593 compute_pass.dispatch_workgroups(
594 size.width().div_ceil(8),
595 size.height().div_ceil(8),
596 1,
597 );
598 }
599 gpu.queue.submit(Some(encoder.finish()));
600 image.has_color = true;
601 Ok(())
602 }
603}
604
605#[cfg(test)]
608mod test {
609 use super::*;
610
611 #[test]
612 fn compile_merge_shader() {
613 crate::compile_shader(&merge_shader(), "merge");
614 }
615
616 #[test]
617 fn compile_color_shader() {
618 crate::compile_shader(&color_shader(16), "color");
619 }
620
621 #[test]
622 fn merge_config_layout() {
623 crate::test::compare_struct_layout::<MergeConfig>(
624 &merge_shader(),
625 "MergeConfig",
626 );
627 }
628
629 #[test]
630 fn color_config_layout() {
631 crate::test::compare_struct_layout::<ColorConfig>(
632 &color_shader(16),
633 "Config",
634 );
635 }
636}