1#![warn(missing_docs)]
65
66use fidget_bytecode::{Bytecode, ReservedRegister};
67use fidget_core::{
68 eval::Function,
69 shape::{MissingVar, ShapeVars},
70 var::{Var, VarMap},
71 vm::VmShape,
72};
73use fidget_raster::RenderSize;
74
75use heck::ToShoutySnakeCase;
76use zerocopy::{FromBytes, Immutable, IntoBytes};
77
78pub mod buf;
79pub mod color;
80pub mod pixel;
81pub mod voxel;
82
83pub use wgpu;
85
86pub(crate) mod shaders {
87 use super::*;
88
89 pub const COMMON: &str = include_str!("shaders/common.wgsl");
90 pub const DUMMY_STACK: &str = include_str!("shaders/dummy_stack.wgsl");
91 pub const FLOAT_OPS: &str = include_str!("shaders/float_ops.wgsl");
92 pub const GRAD_OPS: &str = include_str!("shaders/grad_ops.wgsl");
93 pub const INTERVAL_OPS: &str = include_str!("shaders/interval_ops.wgsl");
94 pub const STACK: &str = include_str!("shaders/stack.wgsl");
95 pub const TAPE_INTERPRETER: &str =
96 include_str!("shaders/tape_interpreter.wgsl");
97 pub const TAPE_SIMPLIFY: &str = include_str!("shaders/tape_simplify.wgsl");
98
99 pub fn opcode_constants() -> String {
101 let mut out = String::new();
102 for (op, i) in fidget_bytecode::iter_ops() {
103 out += &format!(
104 "const OP_{}: u32 = {i};\n",
105 op.to_shouty_snake_case()
106 );
107 }
108 out
109 }
110}
111
112pub(crate) const TAPE_DATA_CAPACITY: usize = 8 * 1024 * 1024; #[repr(C)]
116pub(crate) struct TapeWord {
117 op: u32,
118 imm: u32,
119}
120
121#[derive(Debug, thiserror::Error)]
125pub enum InitError {
126 #[error(transparent)]
128 Adapter(#[from] wgpu::RequestAdapterError),
129
130 #[error(transparent)]
132 Device(#[from] wgpu::RequestDeviceError),
133}
134
135#[derive(Clone)]
137pub struct Gpu {
138 pub device: wgpu::Device,
140 pub queue: wgpu::Queue,
142}
143
144impl Gpu {
145 pub async fn init() -> Result<Gpu, InitError> {
154 let instance = wgpu::Instance::default();
155 let adapter = instance
156 .request_adapter(&wgpu::RequestAdapterOptions {
157 power_preference: wgpu::PowerPreference::HighPerformance,
158 ..wgpu::RequestAdapterOptions::default()
159 })
160 .await?;
161 let (device, queue) = adapter
162 .request_device(&wgpu::DeviceDescriptor {
163 required_features: wgpu::Features::TIMESTAMP_QUERY,
164 ..wgpu::DeviceDescriptor::default()
165 })
166 .await?;
167 Ok(Gpu { device, queue })
168 }
169
170 #[doc(hidden)]
174 pub async fn init_basic() -> Result<Gpu, InitError> {
175 let instance = wgpu::Instance::default();
176 let adapter = instance
177 .request_adapter(&wgpu::RequestAdapterOptions::default())
178 .await?;
179 let (device, queue) = adapter
180 .request_device(&wgpu::DeviceDescriptor::default())
181 .await?;
182 Ok(Gpu { device, queue })
183 }
184
185 pub fn read_buffer_for<T>(
190 &self,
191 buf: &buf::FlexBuffer<T>,
192 ) -> buf::ReadBuffer<T>
193 where
194 T: buf::BufferTag,
195 {
196 buf::ReadBuffer::new(
197 &self.device,
198 format!("{} (read)", buf.name()),
199 buf.size(),
200 )
201 .expect("buf.size should always be a valid size for ReadBuffer::new")
202 }
203
204 pub fn read_buffer<T>(&self, name: &str) -> buf::ReadBuffer<T>
212 where
213 T: buf::BufferTag,
214 T::S: TryFrom<u32>,
215 {
216 let Ok(size) = 64u32.try_into() else {
217 panic!("could not build size");
218 };
219 buf::ReadBuffer::new(&self.device, name, size)
220 .expect("64 should always be a valid size for ReadBuffer::new")
221 }
222
223 #[cfg(not(target_arch = "wasm32"))]
234 pub fn read_vec<T: buf::BufferTag>(
235 &self,
236 buf: &buf::FlexBuffer<T>,
237 ) -> Vec<T::T>
238 where
239 T::T: FromBytes + Immutable + Clone + Copy,
240 {
241 let mut scratch = self.read_buffer_for(buf);
242 self.copy(buf, &mut scratch);
243 let data = self.map(&mut scratch);
244 data.to_vec()
245 }
246
247 pub fn copy<A>(
251 &self,
252 src: &buf::FlexBuffer<A>,
253 dst: &mut buf::ReadBuffer<A>,
254 ) where
255 A: buf::BufferTag,
256 {
257 let mut encoder = self.device.create_command_encoder(
258 &wgpu::CommandEncoderDescriptor {
259 label: Some("read_buffer"),
260 },
261 );
262 self.encode_copy(src, dst, &mut encoder);
263 self.queue.submit(Some(encoder.finish()));
264 }
265
266 pub fn encode_copy<A>(
270 &self,
271 src: &buf::FlexBuffer<A>,
272 dst: &mut buf::ReadBuffer<A>,
273 encoder: &mut wgpu::CommandEncoder,
274 ) where
275 A: buf::BufferTag,
276 {
277 dst.grow_to_fit(&self.device, src.size())
278 .expect("dst buffer should be resizable to match src buffer");
279 encoder.copy_buffer_to_buffer(
280 src.data(),
281 0,
282 dst.data(),
283 0,
284 src.size_bytes(),
285 );
286 }
287
288 #[cfg(any(not(target_arch = "wasm32"), doc))]
290 pub fn map<'a, T>(
291 &self,
292 image: &'a mut buf::ReadBuffer<T>,
293 ) -> buf::MappedBuffer<'a, T>
294 where
295 T: buf::BufferTag,
296 T::T: Immutable + FromBytes + Copy,
297 {
298 pollster::block_on(self.map_async(image))
299 }
300
301 pub async fn map_async<'a, T>(
308 &self,
309 data: &'a mut buf::ReadBuffer<T>,
310 ) -> buf::MappedBuffer<'a, T>
311 where
312 T: buf::BufferTag,
313 T::T: Immutable + FromBytes + Copy,
314 {
315 let (tx, rx) = flume::bounded(1);
316 let slice = data.map_async(move |_| tx.send(()).unwrap());
317 self.device
318 .poll(wgpu::PollType::wait_indefinitely())
319 .unwrap();
320 rx.recv_async().await.unwrap();
321 buf::MappedBuffer::new(data, slice)
322 }
323
324 #[cfg(any(not(target_arch = "wasm32"), doc))]
326 pub fn map_image<'a, T>(
327 &self,
328 data: &'a mut buf::ReadBuffer<T>,
329 ) -> buf::MappedImage<'a, T>
330 where
331 T: buf::BufferTag,
332 T::T: Immutable + FromBytes + Copy,
333 T::S: RenderSize,
334 {
335 pollster::block_on(self.map_image_async(data))
336 }
337
338 pub async fn map_image_async<'a, T>(
345 &self,
346 data: &'a mut buf::ReadBuffer<T>,
347 ) -> buf::MappedImage<'a, T>
348 where
349 T: buf::BufferTag,
350 T::T: Immutable + FromBytes + Copy,
351 T::S: RenderSize,
352 {
353 self.map_async(data).await.into()
354 }
355}
356
357pub(crate) struct RegPipeline {
361 thunk: Box<dyn Fn(u8) -> wgpu::ComputePipeline>,
362 cache:
363 [std::cell::OnceCell<wgpu::ComputePipeline>; REG_PIPELINE_SIZES.len()],
364}
365
366const REG_PIPELINE_SIZES: &[u8] = &[8, 16, 32, 64, 128, 192, 255];
367
368impl RegPipeline {
369 pub fn build(builder: Box<dyn Fn(u8) -> wgpu::ComputePipeline>) -> Self {
370 Self {
371 thunk: builder,
372 cache: std::array::from_fn(|_| Default::default()),
373 }
374 }
375
376 pub fn get(&self, reg_count: u8) -> &wgpu::ComputePipeline {
381 let (i, r) = REG_PIPELINE_SIZES
382 .iter()
383 .enumerate()
384 .find(|(_i, r)| reg_count <= **r)
385 .expect("bytecode tape cannot use more than 255 registers");
386 self.cache[i].get_or_init(|| (*self.thunk)(*r))
387 }
388}
389
390pub struct RenderShape {
398 shape: VmShape,
400 bytecode: Bytecode,
402}
403
404#[derive(Debug, thiserror::Error)]
406pub enum RenderShapeError {
407 #[error(
409 "shape bytecode is {0} tape words (8 bytes each), which exceeds \
410 buffer capacity of {TAPE_DATA_CAPACITY} tape words"
411 )]
412 TooLong(usize),
413 #[error(transparent)]
415 RegisterError(#[from] ReservedRegister),
416}
417
418impl RenderShape {
419 pub fn new(shape: &VmShape) -> Result<Self, RenderShapeError> {
421 let bytecode = Bytecode::new(shape.inner().data())?;
423 if bytecode.len() / 2 > TAPE_DATA_CAPACITY {
424 return Err(RenderShapeError::TooLong(bytecode.len() / 2));
425 }
426
427 Ok(Self {
428 shape: shape.clone(),
429 bytecode,
430 })
431 }
432
433 fn axes(&self) -> [u32; 3] {
435 let vars = self.shape.inner().vars();
436 [Var::X, Var::Y, Var::Z]
437 .map(|a| vars.get(&a).map(|v| v as u32).unwrap_or(u32::MAX))
438 }
439
440 fn copy_vars(
445 &self,
446 gpu: &Gpu,
447 vars: &ShapeVars<f32>,
448 buf: &mut buf::FlexBuffer<voxel::VarsBufferTag>,
449 ) -> Result<CopyVarsChanged, CopyVarsError> {
450 copy_vars(gpu, self.shape.inner().vars(), vars, buf)
451 }
452}
453
454pub(crate) fn copy_vars(
455 gpu: &Gpu,
456 vs: &VarMap,
457 vars: &ShapeVars<f32>,
458 buf: &mut buf::FlexBuffer<voxel::VarsBufferTag>,
459) -> Result<CopyVarsChanged, CopyVarsError> {
460 let mut changed = CopyVarsChanged::BufferUnchanged;
461 if vs.has_free_vars() {
462 for (v, _i) in vs.iter() {
464 match v {
465 Var::X | Var::Y | Var::Z => (),
466 Var::V(vi) => {
467 if vars.get(vi).is_none() {
468 return Err(MissingVar { var: vi }.into());
469 };
470 }
471 }
472 }
473 let r = buf.grow_to_fit(&gpu.device, vs.len())?;
478 if !matches!(r, std::cmp::Ordering::Equal) {
479 changed = CopyVarsChanged::BufferChanged;
480 }
481 let mut writer = gpu
482 .queue
483 .write_buffer_with(
484 buf.data(),
485 0,
486 ((vs.len() * std::mem::size_of::<f32>()) as u64)
487 .try_into()
488 .unwrap(),
489 )
490 .unwrap();
491 for (v, i) in vs.iter() {
492 match v {
493 Var::X | Var::Y | Var::Z => (),
494 Var::V(vi) => {
495 let value = vars.get(vi).unwrap(); let offset = i * std::mem::size_of::<f32>();
497 writer
498 .slice(offset..offset + 4)
499 .copy_from_slice(value.as_bytes());
500 }
501 }
502 }
503 }
504 Ok(changed)
505}
506
507#[derive(thiserror::Error, Debug)]
508enum CopyVarsError {
509 #[error(transparent)]
510 BufferSize(#[from] buf::BufferSizeError),
511 #[error(transparent)]
512 MissingVar(#[from] MissingVar),
513}
514
515#[must_use]
516#[derive(Copy, Clone, Debug)]
517enum CopyVarsChanged {
518 BufferChanged,
519 BufferUnchanged,
520}
521
522#[cfg(test)]
526fn compile_shader(src: &str, desc: &str) {
527 let mut v = naga::valid::Validator::new(
528 naga::valid::ValidationFlags::all(),
529 naga::valid::Capabilities::all(),
530 );
531
532 let m = naga::front::wgsl::parse_str(src).unwrap_or_else(|e| {
535 if let Some(i) = e.location(src) {
536 let pos = i.offset as usize..(i.offset + i.length) as usize;
537 panic!(
538 "shader compilation failed\n{src}\n{}",
539 e.emit_to_string_with_path(&src[pos], desc)
540 );
541 } else {
542 panic!(
543 "shader compilation failed\n{src}\n{}",
544 e.emit_to_string(desc)
545 );
546 }
547 });
548 if let Err(e) = v.validate(&m) {
549 let (pos, desc) = e.spans().next().unwrap();
550 panic!(
551 "shader compilation failed\n{src}\n{}",
552 e.emit_to_string_with_path(&src[pos.to_range().unwrap()], desc)
553 );
554 }
555}
556
557#[cfg(test)]
558mod test {
559 use super::*;
560 use std::collections::HashSet;
561
562 #[test]
563 fn shader_has_all_ops() {
564 for (op, _) in fidget_bytecode::iter_ops() {
565 let op = format!("OP_{}", op.to_shouty_snake_case());
566 assert!(
567 shaders::TAPE_INTERPRETER.contains(&op),
568 "tape interpreter is missing {op}"
569 );
570 assert!(
571 shaders::TAPE_SIMPLIFY.contains(&op),
572 "tape simplification is missing {op}"
573 );
574 }
575 }
576
577 pub(crate) fn compare_struct_layout<T: facet::Facet<'static>>(
578 shader: &str,
579 struct_name: &str,
580 ) {
581 let module = naga::front::wgsl::parse_str(shader).expect("valid WGSL");
582 let (members, span) = module
583 .types
584 .iter()
585 .find_map(|(_, ty)| {
586 if ty.name.as_deref() == Some(struct_name)
587 && let naga::TypeInner::Struct { members, span } = &ty.inner
588 {
589 Some((members, *span))
590 } else {
591 None
592 }
593 })
594 .expect("could not find struct");
595
596 let dynamic_array_offset = members.last().and_then(|m| {
599 let ty = &module.types[m.ty];
600 let naga::TypeInner::Array {
601 base: _,
602 size: naga::ir::ArraySize::Dynamic,
603 stride: _,
604 } = &ty.inner
605 else {
606 return None;
607 };
608 Some(m.offset)
609 });
610 if let Some(dynamic_array_offset) = dynamic_array_offset {
611 assert_eq!(
612 dynamic_array_offset as usize,
613 std::mem::size_of::<T>(),
614 "dynamic array offset is incorrect; invalid last member size?"
615 );
616 } else {
617 assert_eq!(
618 span as usize,
619 std::mem::size_of::<T>(),
620 "overall size is incorrect"
621 );
622 }
623
624 let facet::Type::User(facet::UserType::Struct(shape)) = T::SHAPE.ty
625 else {
626 panic!("must build a struct");
627 };
628
629 let mut shape_field_names = HashSet::new();
631 for field in shape.fields {
632 let field_name = field.name;
633 shape_field_names.insert(field_name);
634 let wgsl_member = members
635 .iter()
636 .find(|m| m.name.as_deref() == Some(field_name))
637 .unwrap_or_else(|| {
638 panic!("field `{field_name}` missing in WGSL struct")
639 });
640 assert_eq!(
641 wgsl_member.offset as usize, field.offset,
642 "offset mismatch for field `{field_name}`"
643 );
644 assert_eq!(
645 module.types[wgsl_member.ty].inner.size(module.to_ctx())
646 as usize,
647 field.shape().layout.sized_layout().unwrap().size(),
648 "size mismatch for field `{field_name}`"
649 );
650 }
651 let slice_len = if dynamic_array_offset.is_some() {
652 members.len() - 1
653 } else {
654 members.len()
655 };
656 for m in &members[..slice_len] {
657 let field_name =
658 m.name.as_ref().expect("cannot check unnamed WGSL fields");
659 assert!(
660 shape_field_names.contains(field_name.as_str()),
661 "field `{field_name}` missing in Rust struct"
662 );
663 }
664 }
665}