Skip to main content

besl/vm/
mod.rs

1//! The `vm` module compiles and executes lexed BESL programs for deterministic host-side evaluation.
2
3use std::collections::HashMap;
4
5use crate::lexer::{BindingTypes, Expressions, NodeReference, Nodes, Operators};
6
7mod compiler;
8mod error;
9mod execution;
10mod instruction;
11mod value;
12
13pub use error::VmError;
14use instruction::*;
15use value::*;
16
17/// The `ResourceSlot` struct provides a stable flat lookup key for host resources and VM interface resources.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub struct ResourceSlot {
20	slot: u32,
21	// The kind keeps internal VM namespaces distinct from host resources that use the same numeric slot.
22	kind: ResourceSlotKind,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26enum ResourceSlotKind {
27	Resource,
28	PushConstant,
29	DynamicResource,
30	BuiltinPosition,
31	Input,
32	Output,
33}
34
35impl ResourceSlot {
36	pub const fn new(slot: u32) -> Self {
37		Self {
38			slot,
39			kind: ResourceSlotKind::Resource,
40		}
41	}
42
43	pub const fn slot(&self) -> u32 {
44		self.slot
45	}
46
47	const fn virtual_slot(slot: u32, kind: ResourceSlotKind) -> Self {
48		Self { slot, kind }
49	}
50
51	const fn is_dynamic_resource(&self) -> bool {
52		matches!(self.kind, ResourceSlotKind::DynamicResource)
53	}
54}
55
56const PUSH_CONSTANT_SLOT: ResourceSlot = ResourceSlot::virtual_slot(0, ResourceSlotKind::PushConstant);
57
58pub const fn input_slot(location: u8) -> ResourceSlot {
59	ResourceSlot::virtual_slot(location as u32, ResourceSlotKind::Input)
60}
61
62pub const fn output_slot(location: u8) -> ResourceSlot {
63	ResourceSlot::virtual_slot(location as u32, ResourceSlotKind::Output)
64}
65
66/// Returns the interface slot reserved for the vertex position builtin.
67pub const fn builtin_position_slot() -> ResourceSlot {
68	ResourceSlot::virtual_slot(0, ResourceSlotKind::BuiltinPosition)
69}
70
71fn dynamic_resource_slot(register: usize) -> ResourceSlot {
72	ResourceSlot::virtual_slot(
73		u32::try_from(register).expect(
74			"Invalid VM resource register. The most likely cause is that compilation produced more registers than the flat slot representation can address.",
75		),
76		ResourceSlotKind::DynamicResource,
77	)
78}
79
80/// The `ValueType` enum describes portable BESL values and resource handles used by VM layouts and registers.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub enum ValueType {
83	Bool,
84	U8,
85	U16,
86	U32,
87	I32,
88	F32,
89	Vec2U16,
90	Vec4U16,
91	Vec2I,
92	Vec3U,
93	Vec2U,
94	Vec4U,
95	Vec2F,
96	Vec3F,
97	Vec4F,
98	Mat4F,
99	Mat4x3F,
100	Texture2D,
101	Texture3D,
102	ArrayTexture2D,
103	Struct {
104		name: String,
105		fields: Vec<BufferMemberLayout>,
106		size: usize,
107	},
108}
109
110impl ValueType {
111	pub const fn size(&self) -> usize {
112		match self {
113			ValueType::Bool => 1,
114			ValueType::U8 => 1,
115			ValueType::U16 => 2,
116			ValueType::U32 | ValueType::I32 | ValueType::F32 => 4,
117			ValueType::Vec2U16 => 4,
118			ValueType::Vec4U16 => 8,
119			ValueType::Vec2I => 8,
120			ValueType::Vec2U | ValueType::Vec2F => 8,
121			ValueType::Vec3U => 12,
122			ValueType::Vec4U | ValueType::Vec4F => 16,
123			ValueType::Vec3F => 12,
124			ValueType::Mat4F => 64,
125			ValueType::Mat4x3F => 48,
126			ValueType::Texture2D | ValueType::Texture3D | ValueType::ArrayTexture2D => 0,
127			ValueType::Struct { size, .. } => *size,
128		}
129	}
130
131	fn name(&self) -> &str {
132		match self {
133			ValueType::Bool => "bool",
134			ValueType::U8 => "u8",
135			ValueType::U16 => "u16",
136			ValueType::U32 => "u32",
137			ValueType::I32 => "i32",
138			ValueType::F32 => "f32",
139			ValueType::Vec2U16 => "vec2u16",
140			ValueType::Vec4U16 => "vec4u16",
141			ValueType::Vec2I => "vec2i",
142			ValueType::Vec3U => "vec3u",
143			ValueType::Vec2U => "vec2u",
144			ValueType::Vec4U => "vec4u",
145			ValueType::Vec2F => "vec2f",
146			ValueType::Vec3F => "vec3f",
147			ValueType::Vec4F => "vec4f",
148			ValueType::Mat4F => "mat4f",
149			ValueType::Mat4x3F => "mat4x3f",
150			ValueType::Texture2D => "Texture2D",
151			ValueType::Texture3D => "Texture3D",
152			ValueType::ArrayTexture2D => "ArrayTexture2D",
153			ValueType::Struct { name, .. } => name,
154		}
155	}
156
157	fn field(&self, name: &str) -> Option<&BufferMemberLayout> {
158		match self {
159			ValueType::Struct { fields, .. } => fields.iter().find(|field| field.name() == name),
160			_ => None,
161		}
162	}
163}
164
165/// The `BufferMemberLayout` struct defines how host code addresses one named member in packed VM memory.
166#[derive(Clone, Debug, PartialEq, Eq)]
167pub struct BufferMemberLayout {
168	name: String,
169	offset: usize,
170	value_type: ValueType,
171	count: usize,
172}
173
174impl BufferMemberLayout {
175	pub fn name(&self) -> &str {
176		&self.name
177	}
178
179	pub const fn offset(&self) -> usize {
180		self.offset
181	}
182
183	pub fn value_type(&self) -> &ValueType {
184		&self.value_type
185	}
186
187	pub const fn count(&self) -> usize {
188		self.count
189	}
190
191	fn element_offset(&self, index: usize) -> Result<usize, VmError> {
192		if index >= self.count {
193			return Err(VmError::BufferArrayIndexOutOfBounds {
194				index,
195				count: self.count,
196			});
197		}
198		Ok(self.offset + self.value_type.size() * index)
199	}
200}
201
202/// The `BufferLayout` struct provides the host-visible packed memory contract for one VM buffer binding.
203#[derive(Clone, Debug, PartialEq, Eq)]
204pub struct BufferLayout {
205	members: Vec<BufferMemberLayout>,
206	size: usize,
207}
208
209impl BufferLayout {
210	pub fn members(&self) -> &[BufferMemberLayout] {
211		&self.members
212	}
213
214	pub const fn size(&self) -> usize {
215		self.size
216	}
217
218	fn member(&self, name: &str) -> Option<&BufferMemberLayout> {
219		self.members.iter().find(|member| member.name == name)
220	}
221}
222
223/// The `DescriptorLayout` enum stores the VM resource layout required by one descriptor slot.
224#[derive(Clone, Debug, PartialEq, Eq)]
225pub enum DescriptorLayout {
226	Buffer(BufferLayout),
227	Texture,
228	Image,
229	PushConstant(BufferLayout),
230}
231
232/// The `Buffer` struct provides mutable CPU storage for binding structured host data to a VM invocation.
233#[derive(Debug)]
234pub struct Buffer {
235	layout: BufferLayout,
236	data: Vec<u8>,
237}
238
239impl Buffer {
240	pub fn new(layout: BufferLayout) -> Self {
241		Self {
242			data: vec![0; layout.size()],
243			layout,
244		}
245	}
246
247	pub fn layout(&self) -> &BufferLayout {
248		&self.layout
249	}
250
251	pub fn bytes(&self) -> &[u8] {
252		&self.data
253	}
254
255	/// Reads a VM value from the buffer layout by member name.
256	pub fn read(&self, member_name: &str) -> Result<Value, VmError> {
257		let member = self.member_layout(member_name)?;
258		if member.count() != 1 {
259			return Err(VmError::UnsupportedBufferLayout {
260				message: format!("Array member `{}` requires an element index", member_name),
261			});
262		}
263
264		self.read_value(member.offset, &member.value_type)
265	}
266
267	/// Reads one array element from a VM buffer member.
268	pub fn read_indexed(&self, member_name: &str, index: usize) -> Result<Value, VmError> {
269		let member = self.member_layout(member_name)?;
270		let offset = member.element_offset(index)?;
271		self.read_value(offset, member.value_type())
272	}
273
274	/// Reads one field from a struct-valued VM buffer member.
275	pub fn read_field(&self, member_name: &str, field_name: &str) -> Result<Value, VmError> {
276		let member = self.member_layout(member_name)?;
277		if member.count() != 1 {
278			return Err(VmError::UnsupportedBufferLayout {
279				message: format!("Array member `{}` requires an element index", member_name),
280			});
281		}
282		self.read_indexed_field(member_name, 0, field_name)
283	}
284
285	/// Reads one field from a struct array element in a VM buffer member.
286	pub fn read_indexed_field(&self, member_name: &str, index: usize, field_name: &str) -> Result<Value, VmError> {
287		let member = self.member_layout(member_name)?;
288		let field = member
289			.value_type()
290			.field(field_name)
291			.ok_or_else(|| VmError::UnknownBufferMember {
292				member: format!("{}.{}", member_name, field_name),
293			})?;
294		let offset = member.element_offset(index)? + field.offset();
295		self.read_value(offset, field.value_type())
296	}
297
298	/// Writes a VM value into the buffer layout by member name.
299	pub fn write(&mut self, member_name: &str, value: Value) -> Result<(), VmError> {
300		let (offset, value_type) = {
301			let member = self.member_layout(member_name)?;
302			if member.count() != 1 {
303				return Err(VmError::UnsupportedBufferLayout {
304					message: format!("Array member `{}` requires an element index", member_name),
305				});
306			}
307			(member.offset, member.value_type.clone())
308		};
309
310		self.write_value(offset, &value_type, &value)
311	}
312
313	/// Writes one array element in a VM buffer member.
314	pub fn write_indexed(&mut self, member_name: &str, index: usize, value: Value) -> Result<(), VmError> {
315		let (offset, value_type) = {
316			let member = self.member_layout(member_name)?;
317			(member.element_offset(index)?, member.value_type().clone())
318		};
319		self.write_value(offset, &value_type, &value)
320	}
321
322	/// Writes one field in a struct-valued VM buffer member.
323	pub fn write_field(&mut self, member_name: &str, field_name: &str, value: Value) -> Result<(), VmError> {
324		let member = self.member_layout(member_name)?;
325		if member.count() != 1 {
326			return Err(VmError::UnsupportedBufferLayout {
327				message: format!("Array member `{}` requires an element index", member_name),
328			});
329		}
330		self.write_indexed_field(member_name, 0, field_name, value)
331	}
332
333	/// Writes one field in a struct array element in a VM buffer member.
334	pub fn write_indexed_field(
335		&mut self,
336		member_name: &str,
337		index: usize,
338		field_name: &str,
339		value: Value,
340	) -> Result<(), VmError> {
341		let (offset, value_type) = {
342			let member = self.member_layout(member_name)?;
343			let field = member
344				.value_type()
345				.field(field_name)
346				.ok_or_else(|| VmError::UnknownBufferMember {
347					member: format!("{}.{}", member_name, field_name),
348				})?;
349			(member.element_offset(index)? + field.offset(), field.value_type().clone())
350		};
351		self.write_value(offset, &value_type, &value)
352	}
353
354	/// Reads an `f32` member from the buffer layout by name.
355	pub fn read_f32(&self, member_name: &str) -> Result<f32, VmError> {
356		match self.read(member_name)? {
357			Value::F32(value) => Ok(value),
358			value => Err(VmError::TypeMismatch {
359				expected: "f32".to_string(),
360				found: value.value_type().name().to_string(),
361			}),
362		}
363	}
364
365	fn read_value(&self, offset: usize, value_type: &ValueType) -> Result<Value, VmError> {
366		let bytes = self.read_bytes(offset, value_type.size())?;
367
368		let value = match value_type {
369			ValueType::Bool => Value::Bool(bytes[0] != 0),
370			ValueType::U8 => Value::U8(bytes[0]),
371			ValueType::U16 => Value::U16(u16::from_ne_bytes(bytes.try_into().expect("Invalid u16 byte count"))),
372			ValueType::U32 => Value::U32(u32::from_ne_bytes(bytes.try_into().expect("Invalid u32 byte count"))),
373			ValueType::I32 => Value::I32(i32::from_ne_bytes(bytes.try_into().expect("Invalid i32 byte count"))),
374			ValueType::F32 => Value::F32(f32::from_ne_bytes(bytes.try_into().expect("Invalid f32 byte count"))),
375			ValueType::Vec2U16 => Value::Vec2U16(read_u16_array::<2>(bytes)?),
376			ValueType::Vec4U16 => Value::Vec4U16(read_u16_array::<4>(bytes)?),
377			ValueType::Vec2I => Value::Vec2I(read_i32_array::<2>(bytes)?),
378			ValueType::Vec2U => Value::Vec2U(read_u32_array::<2>(bytes)?),
379			ValueType::Vec3U => Value::Vec3U(read_u32_array::<3>(bytes)?),
380			ValueType::Vec4U => Value::Vec4U(read_u32_array::<4>(bytes)?),
381			ValueType::Vec2F => Value::Vec2F(read_f32_array::<2>(bytes)?),
382			ValueType::Vec3F => Value::Vec3F(read_f32_array::<3>(bytes)?),
383			ValueType::Vec4F => Value::Vec4F(read_f32_array::<4>(bytes)?),
384			ValueType::Mat4F => Value::Mat4F(read_f32_array::<16>(bytes)?),
385			ValueType::Mat4x3F => Value::Mat4x3F(read_f32_array::<12>(bytes)?),
386			ValueType::Texture2D | ValueType::Texture3D | ValueType::ArrayTexture2D => {
387				return Err(VmError::UnsupportedBufferLayout {
388					message: "Resource handles cannot be stored in CPU buffer memory".to_string(),
389				});
390			}
391			ValueType::Struct { fields, .. } => {
392				let mut values = Vec::with_capacity(fields.len());
393				for field in fields {
394					values.push(self.read_value(offset + field.offset(), field.value_type())?);
395				}
396				Value::Struct {
397					value_type: value_type.clone(),
398					fields: values,
399				}
400			}
401		};
402
403		Ok(value)
404	}
405
406	fn write_value(&mut self, offset: usize, value_type: &ValueType, value: &Value) -> Result<(), VmError> {
407		if !value.matches_type(value_type) {
408			return Err(VmError::TypeMismatch {
409				expected: value_type.name().to_string(),
410				found: value.value_type().name().to_string(),
411			});
412		}
413
414		match value {
415			Value::Bool(value) => self.write_bytes(offset, &[u8::from(*value)]),
416			Value::U8(value) => self.write_bytes(offset, &value.to_ne_bytes()),
417			Value::U16(value) => self.write_bytes(offset, &value.to_ne_bytes()),
418			Value::U32(value) => self.write_bytes(offset, &value.to_ne_bytes()),
419			Value::I32(value) => self.write_bytes(offset, &value.to_ne_bytes()),
420			Value::F32(value) => self.write_bytes(offset, &value.to_ne_bytes()),
421			Value::Vec2U16(value) => write_u16_slice(self, offset, value),
422			Value::Vec4U16(value) => write_u16_slice(self, offset, value),
423			Value::Vec2I(value) => write_i32_slice(self, offset, value),
424			Value::Vec2U(value) => write_u32_slice(self, offset, value),
425			Value::Vec3U(value) => write_u32_slice(self, offset, value),
426			Value::Vec4U(value) => write_u32_slice(self, offset, value),
427			Value::Vec2F(value) => write_f32_slice(self, offset, value),
428			Value::Vec3F(value) => write_f32_slice(self, offset, value),
429			Value::Vec4F(value) => write_f32_slice(self, offset, value),
430			Value::Mat4F(value) => write_f32_slice(self, offset, value),
431			Value::Mat4x3F(value) => write_f32_slice(self, offset, value),
432			Value::Resource { .. } => Err(VmError::UnsupportedBufferLayout {
433				message: "Resource handles cannot be written into CPU buffer memory".to_string(),
434			}),
435			Value::Struct { fields, .. } => {
436				let ValueType::Struct {
437					fields: field_layouts, ..
438				} = value_type
439				else {
440					unreachable!("Struct values are validated before writing")
441				};
442				for (field, field_layout) in fields.iter().zip(field_layouts) {
443					self.write_value(offset + field_layout.offset(), field_layout.value_type(), field)?;
444				}
445				Ok(())
446			}
447		}
448	}
449
450	fn read_bytes(&self, offset: usize, size: usize) -> Result<&[u8], VmError> {
451		self.data.get(offset..offset + size).ok_or(VmError::BufferAccessOutOfBounds {
452			offset,
453			size,
454			buffer_size: self.data.len(),
455		})
456	}
457
458	fn write_bytes(&mut self, offset: usize, bytes: &[u8]) -> Result<(), VmError> {
459		let buffer_size = self.data.len();
460		let slice = self
461			.data
462			.get_mut(offset..offset + bytes.len())
463			.ok_or(VmError::BufferAccessOutOfBounds {
464				offset,
465				size: bytes.len(),
466				buffer_size,
467			})?;
468
469		slice.copy_from_slice(bytes);
470
471		Ok(())
472	}
473
474	fn member_layout(&self, member_name: &str) -> Result<&BufferMemberLayout, VmError> {
475		self.layout.member(member_name).ok_or_else(|| VmError::UnknownBufferMember {
476			member: member_name.to_string(),
477		})
478	}
479}
480
481/// The `Texture` struct provides deterministic CPU texels for shader sampling, image access, and atomic assertions.
482#[derive(Debug)]
483pub struct Texture {
484	width: u32,
485	height: u32,
486	depth: u32,
487	texels: Vec<Texel>,
488}
489
490#[derive(Clone, Copy, Debug)]
491enum Texel {
492	Zero,
493	Float([f32; 4]),
494	U32(u32),
495}
496
497impl Texel {
498	const fn kind(self) -> &'static str {
499		match self {
500			Self::Zero => "untyped zero",
501			Self::Float(_) => "float RGBA",
502			Self::U32(_) => "u32",
503		}
504	}
505
506	fn float(self) -> Result<[f32; 4], VmError> {
507		match self {
508			Self::Zero => Ok([0.0; 4]),
509			Self::Float(value) => Ok(value),
510			value => Err(VmError::TextureFormatMismatch {
511				expected: "float RGBA",
512				found: value.kind(),
513			}),
514		}
515	}
516
517	fn u32(self) -> Result<u32, VmError> {
518		match self {
519			Self::Zero => Ok(0),
520			Self::U32(value) => Ok(value),
521			value => Err(VmError::TextureFormatMismatch {
522				expected: "u32",
523				found: value.kind(),
524			}),
525		}
526	}
527}
528
529impl Texture {
530	pub fn new(width: u32, height: u32) -> Result<Self, VmError> {
531		Self::new_3d(width, height, 1)
532	}
533
534	/// Creates a CPU texture with three-dimensional addressing for VM texture tests.
535	pub fn new_3d(width: u32, height: u32, depth: u32) -> Result<Self, VmError> {
536		if width == 0 || height == 0 || depth == 0 {
537			return Err(VmError::InvalidTextureDimensions { width, height, depth });
538		}
539
540		let texel_count = (width as usize)
541			.checked_mul(height as usize)
542			.and_then(|area| area.checked_mul(depth as usize))
543			.ok_or(VmError::TextureTexelCountOverflow { width, height, depth })?;
544		texel_count
545			.checked_mul(std::mem::size_of::<Texel>())
546			.filter(|byte_count| *byte_count <= isize::MAX as usize)
547			.ok_or(VmError::TextureTexelCountOverflow { width, height, depth })?;
548
549		// Fallible reservation keeps hostile or accidental dimensions on the VM error path.
550		let mut texels = Vec::new();
551		texels
552			.try_reserve_exact(texel_count)
553			.map_err(|_| VmError::TextureTexelCountOverflow { width, height, depth })?;
554		texels.resize(texel_count, Texel::Zero);
555		Ok(Self {
556			width,
557			height,
558			depth,
559			texels,
560		})
561	}
562
563	pub fn write(&mut self, coord: [u32; 2], value: [f32; 4]) -> Result<(), VmError> {
564		let index = self.texel_index([coord[0], coord[1], 0])?;
565		self.texels[index] = Texel::Float(value);
566		Ok(())
567	}
568
569	/// Writes one texel in a three-dimensional CPU texture.
570	pub fn write_3d(&mut self, coord: [u32; 3], value: [f32; 4]) -> Result<(), VmError> {
571		let index = self.texel_index(coord)?;
572		self.texels[index] = Texel::Float(value);
573		Ok(())
574	}
575
576	/// Writes one unsigned integer texel for integer image and atomic tests.
577	pub fn write_u32(&mut self, coord: [u32; 2], value: u32) -> Result<(), VmError> {
578		let index = self.texel_index([coord[0], coord[1], 0])?;
579		self.texels[index] = Texel::U32(value);
580		Ok(())
581	}
582
583	/// Fetches one texel without interpolation.
584	pub fn fetch(&self, coord: [u32; 2]) -> Result<Value, VmError> {
585		Ok(Value::Vec4F(self.fetch_texel([coord[0], coord[1], 0])?))
586	}
587
588	/// Fetches one unsigned integer texel without interpolation.
589	pub fn fetch_u32(&self, coord: [u32; 2]) -> Result<Value, VmError> {
590		let index = self.texel_index([coord[0], coord[1], 0])?;
591		Ok(Value::U32(self.texels[index].u32()?))
592	}
593
594	/// Samples one texel using bilinear interpolation in normalized UV space.
595	pub fn sample(&self, uv: [f32; 2]) -> Result<Value, VmError> {
596		let (x0, x1, tx) = normalized_linear_axis(uv[0], self.width);
597		let (y0, y1, ty) = normalized_linear_axis(uv[1], self.height);
598
599		let top = lerp_rgba(self.fetch_texel([x0, y0, 0])?, self.fetch_texel([x1, y0, 0])?, tx);
600		let bottom = lerp_rgba(self.fetch_texel([x0, y1, 0])?, self.fetch_texel([x1, y1, 0])?, tx);
601
602		Ok(Value::Vec4F(lerp_rgba(top, bottom, ty)))
603	}
604
605	/// Samples a three-dimensional texture using trilinear interpolation.
606	pub fn sample_3d(&self, uvw: [f32; 3]) -> Result<Value, VmError> {
607		let x = normalized_linear_axis(uvw[0], self.width);
608		let y = normalized_linear_axis(uvw[1], self.height);
609		let z = normalized_linear_axis(uvw[2], self.depth);
610		let low = [x.0, y.0, z.0];
611		let high = [x.1, y.1, z.1];
612		let factor = [x.2, y.2, z.2];
613		let low_plane = lerp_rgba(
614			lerp_rgba(
615				self.fetch_texel([low[0], low[1], low[2]])?,
616				self.fetch_texel([high[0], low[1], low[2]])?,
617				factor[0],
618			),
619			lerp_rgba(
620				self.fetch_texel([low[0], high[1], low[2]])?,
621				self.fetch_texel([high[0], high[1], low[2]])?,
622				factor[0],
623			),
624			factor[1],
625		);
626		let high_plane = lerp_rgba(
627			lerp_rgba(
628				self.fetch_texel([low[0], low[1], high[2]])?,
629				self.fetch_texel([high[0], low[1], high[2]])?,
630				factor[0],
631			),
632			lerp_rgba(
633				self.fetch_texel([low[0], high[1], high[2]])?,
634				self.fetch_texel([high[0], high[1], high[2]])?,
635				factor[0],
636			),
637			factor[1],
638		);
639		Ok(Value::Vec4F(lerp_rgba(low_plane, high_plane, factor[2])))
640	}
641
642	fn fetch_texel(&self, coord: [u32; 3]) -> Result<[f32; 4], VmError> {
643		let index = self.texel_index(coord)?;
644		self.texels[index].float()
645	}
646
647	fn texel_index(&self, coord: [u32; 3]) -> Result<usize, VmError> {
648		let [x, y, z] = coord;
649		if x >= self.width || y >= self.height || z >= self.depth {
650			return Err(VmError::TextureAccessOutOfBounds {
651				x,
652				y,
653				z,
654				width: self.width,
655				height: self.height,
656				depth: self.depth,
657			});
658		}
659
660		Ok(((z as usize) * self.height as usize + y as usize) * self.width as usize + x as usize)
661	}
662
663	fn contains_2d(&self, coord: [u32; 2]) -> bool {
664		coord[0] < self.width && coord[1] < self.height
665	}
666
667	fn atomic_or(&mut self, coord: [u32; 2], value: u32) -> Result<u32, VmError> {
668		let index = self.texel_index([coord[0], coord[1], 0])?;
669		let previous = self.texels[index].u32()?;
670		let updated = previous | value;
671		self.texels[index] = Texel::U32(updated);
672		Ok(previous)
673	}
674}
675
676enum DescriptorBinding<'a> {
677	Buffer(&'a mut Buffer),
678	Texture(&'a mut Texture),
679	Image(&'a mut Texture),
680}
681
682impl DescriptorBinding<'_> {
683	const fn kind(&self) -> &'static str {
684		match self {
685			Self::Buffer(_) => "buffer",
686			Self::Texture(_) => "texture",
687			Self::Image(_) => "image",
688		}
689	}
690
691	fn type_mismatch(&self, slot: ResourceSlot, expected: &'static str) -> VmError {
692		VmError::DescriptorTypeMismatch {
693			slot,
694			expected,
695			found: self.kind(),
696		}
697	}
698}
699
700/// The `MeshOutputs` struct captures mesh-stage topology and positions for VM assertions.
701#[derive(Clone, Debug, Default, PartialEq)]
702pub struct MeshOutputs {
703	vertex_count: u32,
704	primitive_count: u32,
705	vertex_positions: Vec<[f32; 4]>,
706	triangles: Vec<[u32; 3]>,
707}
708
709impl MeshOutputs {
710	/// Creates an empty capture that can be bound before a mesh shader invocation.
711	pub fn new() -> Self {
712		Self::default()
713	}
714
715	/// Returns the vertex count declared by the most recent mesh invocation.
716	pub const fn vertex_count(&self) -> u32 {
717		self.vertex_count
718	}
719
720	/// Returns the primitive count declared by the most recent mesh invocation.
721	pub const fn primitive_count(&self) -> u32 {
722		self.primitive_count
723	}
724
725	/// Returns one captured mesh vertex position when the shader wrote that declared slot.
726	pub fn vertex_position(&self, index: usize) -> Option<[f32; 4]> {
727		self.vertex_positions.get(index).copied()
728	}
729
730	/// Returns one captured mesh triangle when the shader wrote that declared slot.
731	pub fn triangle(&self, index: usize) -> Option<[u32; 3]> {
732		self.triangles.get(index).copied()
733	}
734
735	/// Prepares mesh output ranges after validating shader-controlled counts.
736	fn set_counts(
737		&mut self,
738		vertex_count: u32,
739		primitive_count: u32,
740		max_vertex_count: u32,
741		max_primitive_count: u32,
742		clear: bool,
743	) -> Result<(), VmError> {
744		if vertex_count > max_vertex_count {
745			return Err(VmError::MeshOutputCountLimitExceeded {
746				kind: "vertex",
747				requested: vertex_count,
748				limit: max_vertex_count,
749			});
750		}
751		if primitive_count > max_primitive_count {
752			return Err(VmError::MeshOutputCountLimitExceeded {
753				kind: "primitive",
754				requested: primitive_count,
755				limit: max_primitive_count,
756			});
757		}
758
759		if clear {
760			self.begin_invocation();
761		}
762		self.vertex_count = vertex_count;
763		self.primitive_count = primitive_count;
764		self.vertex_positions.resize(vertex_count as usize, [0.0; 4]);
765		self.triangles.resize(primitive_count as usize, [0; 3]);
766		Ok(())
767	}
768
769	fn begin_invocation(&mut self) {
770		// The first lane clears the shared capture once; later workgroup lanes retain earlier lane writes.
771		self.vertex_positions.fill([0.0; 4]);
772		self.triangles.fill([0; 3]);
773	}
774}
775
776/// The `TaskOutputs` struct captures task-stage mesh dispatch counts and payload values for VM assertions.
777#[derive(Clone, Debug, Default, PartialEq)]
778pub struct TaskOutputs {
779	mesh_output_count: Option<u32>,
780	payloads: HashMap<String, Vec<Option<Value>>>,
781}
782
783impl TaskOutputs {
784	/// Creates an empty capture that can be bound before a task shader invocation.
785	pub fn new() -> Self {
786		Self::default()
787	}
788
789	/// Returns the mesh workgroup count declared by the task invocation, if it declared one.
790	pub const fn mesh_output_count(&self) -> Option<u32> {
791		self.mesh_output_count
792	}
793
794	/// Returns one task-payload value when the shader wrote the requested declared element.
795	pub fn payload_value(&self, name: &str, index: usize) -> Option<&Value> {
796		self.payloads.get(name)?.get(index)?.as_ref()
797	}
798
799	fn set_mesh_output_count(&mut self, count: u32) {
800		self.mesh_output_count = Some(count);
801		let count = count as usize;
802		for payload in self.payloads.values_mut() {
803			if count < payload.len() {
804				// Values outside the published dispatch range must not survive capture reuse.
805				payload[count..].fill(None);
806			}
807		}
808	}
809
810	/// Clears shader-authored values while retaining the capture's allocated payload storage.
811	fn begin_workgroup(&mut self) {
812		self.mesh_output_count = None;
813		for payload in self.payloads.values_mut() {
814			payload.fill(None);
815		}
816	}
817
818	/// Writes one declared task-payload element while preserving earlier lane writes in the same capture.
819	fn write_payload(&mut self, name: &str, index: usize, count: usize, value: Value) -> Result<(), VmError> {
820		if index >= count {
821			return Err(VmError::TaskPayloadOutputIndexOutOfBounds {
822				name: name.to_string(),
823				index,
824				count,
825			});
826		}
827
828		let payload = if let Some(payload) = self.payloads.get_mut(name) {
829			payload
830		} else {
831			self.payloads.insert(name.to_string(), vec![None; count]);
832			self.payloads
833				.get_mut(name)
834				.expect(
835					"Missing inserted task payload. The most likely cause is that the payload map changed between insertion and lookup.",
836				)
837		};
838		if payload.len() != count {
839			// A capture is scoped to one declared task interface; clear stale values if a caller reuses it with another layout.
840			payload.clear();
841			payload.resize(count, None);
842		}
843		payload[index] = Some(value);
844		Ok(())
845	}
846}
847
848/// The `WorkgroupState` struct provides task-stage invocations with explicitly shared workgroup storage.
849#[derive(Clone, Debug, Default, PartialEq)]
850pub struct WorkgroupState {
851	values: HashMap<String, Option<Value>>,
852}
853
854impl WorkgroupState {
855	/// Creates empty workgroup storage for one VM workgroup fixture.
856	pub fn new() -> Self {
857		Self::default()
858	}
859
860	/// Clears values from the previous workgroup while retaining its names and allocated map storage.
861	fn begin_workgroup(&mut self) {
862		for value in self.values.values_mut() {
863			*value = None;
864		}
865	}
866
867	/// Loads one value initialized by an earlier instruction in the bound workgroup state.
868	fn load(&self, name: &str, value_type: &ValueType) -> Result<Value, VmError> {
869		let value = self
870			.values
871			.get(name)
872			.and_then(Option::as_ref)
873			.ok_or_else(|| VmError::UninitializedWorkgroupValue { name: name.to_string() })?;
874		if !value.matches_type(value_type) {
875			return Err(VmError::TypeMismatch {
876				expected: value_type.name().to_string(),
877				found: value.value_type().name().to_string(),
878			});
879		}
880		Ok(value.clone())
881	}
882
883	/// Replaces one workgroup value after validating the declaration's portable type.
884	fn store(&mut self, name: &str, value_type: &ValueType, value: Value) -> Result<(), VmError> {
885		if !value.matches_type(value_type) {
886			return Err(VmError::TypeMismatch {
887				expected: value_type.name().to_string(),
888				found: value.value_type().name().to_string(),
889			});
890		}
891		if let Some(stored) = self.values.get_mut(name) {
892			*stored = Some(value);
893		} else {
894			self.values.insert(name.to_string(), Some(value));
895		}
896		Ok(())
897	}
898
899	/// Applies the wrapping atomic-u32 addition used by task compaction counters.
900	fn atomic_add_u32(&mut self, name: &str, value: u32) -> Result<u32, VmError> {
901		let stored = self
902			.values
903			.get_mut(name)
904			.and_then(Option::as_mut)
905			.ok_or_else(|| VmError::UninitializedWorkgroupValue { name: name.to_string() })?;
906		let Value::U32(previous) = stored else {
907			return Err(VmError::TypeMismatch {
908				expected: ValueType::U32.name().to_string(),
909				found: stored.value_type().name().to_string(),
910			});
911		};
912		let previous = *previous;
913		*stored = Value::U32(previous.wrapping_add(value));
914		Ok(previous)
915	}
916}
917
918/// The `DescriptorBindings` struct provides invocation-scoped host resources to a compiled BESL program.
919pub struct DescriptorBindings<'a> {
920	bindings: HashMap<ResourceSlot, DescriptorBinding<'a>>,
921	push_constant: Option<&'a mut Buffer>,
922	mesh_outputs: Option<&'a mut MeshOutputs>,
923	task_outputs: Option<&'a mut TaskOutputs>,
924	workgroup_state: Option<&'a mut WorkgroupState>,
925	task_payloads: HashMap<String, Vec<Value>>,
926}
927
928impl<'a> Default for DescriptorBindings<'a> {
929	fn default() -> Self {
930		Self::new()
931	}
932}
933
934impl<'a> DescriptorBindings<'a> {
935	pub fn new() -> Self {
936		Self {
937			bindings: HashMap::new(),
938			push_constant: None,
939			mesh_outputs: None,
940			task_outputs: None,
941			workgroup_state: None,
942			task_payloads: HashMap::new(),
943		}
944	}
945
946	pub fn bind_buffer(&mut self, slot: ResourceSlot, buffer: &'a mut Buffer) {
947		self.bindings.insert(slot, DescriptorBinding::Buffer(buffer));
948	}
949
950	pub fn bind_texture(&mut self, slot: ResourceSlot, texture: &'a mut Texture) {
951		self.bindings.insert(slot, DescriptorBinding::Texture(texture));
952	}
953
954	pub fn bind_image(&mut self, slot: ResourceSlot, image: &'a mut Texture) {
955		self.bindings.insert(slot, DescriptorBinding::Image(image));
956	}
957
958	pub fn bind_push_constant(&mut self, push_constant: &'a mut Buffer) {
959		self.push_constant = Some(push_constant);
960	}
961
962	/// Binds the capture used by mesh output-count, position, and triangle intrinsics.
963	pub fn bind_mesh_outputs(&mut self, mesh_outputs: &'a mut MeshOutputs) {
964		self.mesh_outputs = Some(mesh_outputs);
965	}
966
967	/// Binds the capture used by task payload writes and the task mesh-output-count intrinsic.
968	pub fn bind_task_outputs(&mut self, task_outputs: &'a mut TaskOutputs) {
969		self.task_outputs = Some(task_outputs);
970	}
971
972	/// Binds shared storage for task fixtures executed through the workgroup scheduler.
973	pub fn bind_workgroup_state(&mut self, workgroup_state: &'a mut WorkgroupState) {
974		self.workgroup_state = Some(workgroup_state);
975	}
976
977	/// Binds the authored values produced for one named task-payload array before a mesh-stage invocation.
978	///
979	/// Values are copied into invocation-owned storage so callers may use arrays and other temporary iterators.
980	pub fn bind_task_payload(&mut self, name: impl Into<String>, values: impl IntoIterator<Item = Value>) {
981		self.task_payloads.insert(name.into(), values.into_iter().collect());
982	}
983
984	fn buffer_mut(&mut self, slot: ResourceSlot) -> Result<&mut Buffer, VmError> {
985		let descriptor = self.bindings.get_mut(&slot).ok_or(VmError::UnboundDescriptor { slot })?;
986
987		match descriptor {
988			DescriptorBinding::Buffer(buffer) => Ok(&mut **buffer),
989			descriptor => Err(descriptor.type_mismatch(slot, "buffer")),
990		}
991	}
992
993	fn texture_mut(&mut self, slot: ResourceSlot) -> Result<&mut Texture, VmError> {
994		let descriptor = self.bindings.get_mut(&slot).ok_or(VmError::UnboundDescriptor { slot })?;
995
996		match descriptor {
997			DescriptorBinding::Texture(texture) => Ok(&mut **texture),
998			descriptor => Err(descriptor.type_mismatch(slot, "texture")),
999		}
1000	}
1001
1002	fn image_mut(&mut self, slot: ResourceSlot) -> Result<&mut Texture, VmError> {
1003		let descriptor = self.bindings.get_mut(&slot).ok_or(VmError::UnboundDescriptor { slot })?;
1004
1005		match descriptor {
1006			DescriptorBinding::Image(image) => Ok(&mut **image),
1007			descriptor => Err(descriptor.type_mismatch(slot, "image")),
1008		}
1009	}
1010
1011	fn push_constant_mut(&mut self) -> Result<&mut Buffer, VmError> {
1012		self.push_constant.as_deref_mut().ok_or(VmError::MissingPushConstant)
1013	}
1014
1015	fn mesh_outputs_mut(&mut self) -> Result<&mut MeshOutputs, VmError> {
1016		self.mesh_outputs.as_deref_mut().ok_or(VmError::MissingMeshOutputs)
1017	}
1018
1019	fn task_outputs_mut(&mut self) -> Result<&mut TaskOutputs, VmError> {
1020		self.task_outputs.as_deref_mut().ok_or(VmError::MissingTaskOutputs)
1021	}
1022
1023	fn workgroup_state_mut(&mut self) -> Result<&mut WorkgroupState, VmError> {
1024		self.workgroup_state.as_deref_mut().ok_or(VmError::MissingWorkgroupState)
1025	}
1026
1027	/// Starts a fresh task workgroup without reallocating reusable capture storage.
1028	fn begin_task_workgroup(&mut self) {
1029		if let Some(task_outputs) = self.task_outputs.as_deref_mut() {
1030			task_outputs.begin_workgroup();
1031		}
1032		if let Some(workgroup_state) = self.workgroup_state.as_deref_mut() {
1033			workgroup_state.begin_workgroup();
1034		}
1035	}
1036
1037	fn task_payload_value(&self, name: &str, index: usize) -> Result<Value, VmError> {
1038		let values = self
1039			.task_payloads
1040			.get(name)
1041			.ok_or_else(|| VmError::MissingTaskPayload { name: name.to_string() })?;
1042		values
1043			.get(index)
1044			.cloned()
1045			.ok_or_else(|| VmError::TaskPayloadIndexOutOfBounds {
1046				name: name.to_string(),
1047				index,
1048				count: values.len(),
1049			})
1050	}
1051}
1052
1053/// The `SpecializationValues` struct supplies host-selected values for BESL specialization declarations.
1054#[derive(Clone, Debug, Default)]
1055pub struct SpecializationValues {
1056	values: HashMap<String, Value>,
1057}
1058
1059impl SpecializationValues {
1060	/// Creates an empty specialization map for programs that use only defaults or no specializations.
1061	pub fn new() -> Self {
1062		Self::default()
1063	}
1064
1065	/// Supplies one named specialization value before compiling an executable program.
1066	pub fn set(&mut self, name: impl Into<String>, value: Value) -> Option<Value> {
1067		self.values.insert(name.into(), value)
1068	}
1069
1070	/// Returns a previously supplied specialization value by declaration name.
1071	pub fn get(&self, name: &str) -> Option<&Value> {
1072		self.values.get(name)
1073	}
1074}
1075
1076/// The `ExecutionConfig` struct bounds a VM invocation and supplies its shader-visible thread coordinates.
1077#[derive(Clone, Debug, PartialEq, Eq)]
1078pub struct ExecutionConfig {
1079	instruction_limit: usize,
1080	call_depth_limit: usize,
1081	max_mesh_vertex_count: u32,
1082	max_mesh_primitive_count: u32,
1083	max_task_mesh_output_count: u32,
1084	thread_id: [u32; 2],
1085	thread_idx: u32,
1086	thread_position: u32,
1087	threadgroup_position: u32,
1088}
1089
1090impl Default for ExecutionConfig {
1091	fn default() -> Self {
1092		Self {
1093			instruction_limit: 1_000_000,
1094			call_depth_limit: 64,
1095			max_mesh_vertex_count: 256,
1096			max_mesh_primitive_count: 256,
1097			max_task_mesh_output_count: 256,
1098			thread_id: [0, 0],
1099			thread_idx: 0,
1100			thread_position: 0,
1101			threadgroup_position: 0,
1102		}
1103	}
1104}
1105
1106impl ExecutionConfig {
1107	/// Creates an invocation config with an explicit instruction budget and default coordinates.
1108	pub fn new(instruction_limit: usize) -> Self {
1109		Self {
1110			instruction_limit,
1111			..Self::default()
1112		}
1113	}
1114
1115	/// Returns the maximum number of instructions shared by the invocation's call tree.
1116	pub const fn instruction_limit(&self) -> usize {
1117		self.instruction_limit
1118	}
1119
1120	/// Returns the maximum nested BESL function-call depth.
1121	pub const fn call_depth_limit(&self) -> usize {
1122		self.call_depth_limit
1123	}
1124
1125	/// Returns the maximum vertex count a mesh invocation may request.
1126	pub const fn max_mesh_vertex_count(&self) -> u32 {
1127		self.max_mesh_vertex_count
1128	}
1129
1130	/// Returns the maximum primitive count a mesh invocation may request.
1131	pub const fn max_mesh_primitive_count(&self) -> u32 {
1132		self.max_mesh_primitive_count
1133	}
1134
1135	/// Returns the maximum mesh workgroup count a task invocation may request.
1136	pub const fn max_task_mesh_output_count(&self) -> u32 {
1137		self.max_task_mesh_output_count
1138	}
1139
1140	/// Returns the two-dimensional compute invocation coordinate.
1141	pub const fn thread_id(&self) -> [u32; 2] {
1142		self.thread_id
1143	}
1144
1145	/// Returns the mesh or workgroup-local invocation index.
1146	pub const fn thread_idx(&self) -> u32 {
1147		self.thread_idx
1148	}
1149
1150	/// Returns the task invocation's scalar position in the dispatched grid.
1151	pub const fn thread_position(&self) -> u32 {
1152		self.thread_position
1153	}
1154
1155	/// Returns the mesh workgroup position visible to the shader.
1156	pub const fn threadgroup_position(&self) -> u32 {
1157		self.threadgroup_position
1158	}
1159
1160	/// Selects an explicit nested function-call limit for this invocation.
1161	pub fn with_call_depth_limit(mut self, limit: usize) -> Self {
1162		self.call_depth_limit = limit;
1163		self
1164	}
1165
1166	/// Selects the maximum vertex count accepted from mesh output-count intrinsics.
1167	pub fn with_max_mesh_vertex_count(mut self, limit: u32) -> Self {
1168		self.max_mesh_vertex_count = limit;
1169		self
1170	}
1171
1172	/// Selects the maximum primitive count accepted from mesh output-count intrinsics.
1173	pub fn with_max_mesh_primitive_count(mut self, limit: u32) -> Self {
1174		self.max_mesh_primitive_count = limit;
1175		self
1176	}
1177
1178	/// Selects the maximum mesh workgroup count accepted from task output-count intrinsics.
1179	pub fn with_max_task_mesh_output_count(mut self, limit: u32) -> Self {
1180		self.max_task_mesh_output_count = limit;
1181		self
1182	}
1183
1184	/// Selects the two-dimensional compute invocation coordinate.
1185	pub fn with_thread_id(mut self, thread_id: [u32; 2]) -> Self {
1186		self.thread_id = thread_id;
1187		self
1188	}
1189
1190	/// Selects the mesh or workgroup-local invocation index.
1191	pub fn with_thread_idx(mut self, thread_idx: u32) -> Self {
1192		self.thread_idx = thread_idx;
1193		self
1194	}
1195
1196	/// Selects the task invocation's scalar position in the dispatched grid.
1197	pub fn with_thread_position(mut self, position: u32) -> Self {
1198		self.thread_position = position;
1199		self
1200	}
1201
1202	/// Selects the mesh workgroup position visible to the shader.
1203	pub fn with_threadgroup_position(mut self, position: u32) -> Self {
1204		self.threadgroup_position = position;
1205		self
1206	}
1207}
1208
1209/// The `ExecutionState` struct shares invocation limits and coordinates across nested VM calls.
1210struct ExecutionState<'a> {
1211	config: &'a ExecutionConfig,
1212	remaining_instructions: usize,
1213	call_depth: usize,
1214}
1215
1216impl<'a> ExecutionState<'a> {
1217	fn new(config: &'a ExecutionConfig) -> Self {
1218		Self {
1219			config,
1220			remaining_instructions: config.instruction_limit(),
1221			call_depth: 0,
1222		}
1223	}
1224
1225	fn consume_instruction(&mut self) -> Result<(), VmError> {
1226		if self.remaining_instructions == 0 {
1227			return Err(VmError::InstructionLimitExceeded {
1228				limit: self.config.instruction_limit(),
1229			});
1230		}
1231		self.remaining_instructions -= 1;
1232		Ok(())
1233	}
1234
1235	fn enter_call(&mut self) -> Result<(), VmError> {
1236		if self.call_depth >= self.config.call_depth_limit() {
1237			return Err(VmError::CallDepthLimitExceeded {
1238				limit: self.config.call_depth_limit(),
1239			});
1240		}
1241		self.call_depth += 1;
1242		Ok(())
1243	}
1244
1245	fn leave_call(&mut self) {
1246		self.call_depth -= 1;
1247	}
1248}
1249
1250/// The `ExecutableProgram` struct provides a reusable host-side execution form for one lexed BESL program.
1251pub struct ExecutableProgram {
1252	descriptor_layouts: HashMap<ResourceSlot, DescriptorLayout>,
1253	functions: Vec<ExecutableFunction>,
1254	main_function: usize,
1255}
1256
1257/// The `ExecutableFunction` struct isolates one compiled BESL call target for bounded VM execution.
1258struct ExecutableFunction {
1259	instructions: Vec<Instruction>,
1260	local_types: Vec<ValueType>,
1261	register_count: usize,
1262	parameter_count: usize,
1263	return_type: Option<ValueType>,
1264}
1265
1266impl ExecutableProgram {
1267	/// Compiles a lexed BESL program into a runnable VM program.
1268	#[allow(clippy::mutable_key_type)]
1269	pub fn compile(program: NodeReference) -> Result<Self, VmError> {
1270		Self::compile_with_specializations(program, &SpecializationValues::new())
1271	}
1272
1273	/// Compiles a lexed BESL program using host-provided specialization values.
1274	#[allow(clippy::mutable_key_type)]
1275	pub fn compile_with_specializations(
1276		program: NodeReference,
1277		specializations: &SpecializationValues,
1278	) -> Result<Self, VmError> {
1279		compiler::compile(program, specializations)
1280	}
1281
1282	pub fn descriptor_layout(&self, slot: ResourceSlot) -> Option<&DescriptorLayout> {
1283		self.descriptor_layouts.get(&slot)
1284	}
1285
1286	pub fn buffer_layout(&self, slot: ResourceSlot) -> Option<&BufferLayout> {
1287		match self.descriptor_layouts.get(&slot) {
1288			Some(DescriptorLayout::Buffer(layout)) => Some(layout),
1289			Some(DescriptorLayout::Texture) => None,
1290			Some(DescriptorLayout::Image) => None,
1291			Some(DescriptorLayout::PushConstant(_)) => None,
1292			None => None,
1293		}
1294	}
1295
1296	pub fn push_constant_layout(&self) -> Option<&BufferLayout> {
1297		self.descriptor_layouts.values().find_map(|layout| match layout {
1298			DescriptorLayout::PushConstant(layout) => Some(layout),
1299			_ => None,
1300		})
1301	}
1302
1303	pub fn input_layout(&self, location: u8) -> Option<&BufferLayout> {
1304		self.buffer_layout(input_slot(location))
1305	}
1306
1307	pub fn output_layout(&self, location: u8) -> Option<&BufferLayout> {
1308		self.buffer_layout(output_slot(location))
1309	}
1310
1311	pub fn builtin_position_layout(&self) -> Option<&BufferLayout> {
1312		self.buffer_layout(builtin_position_slot())
1313	}
1314}
1315
1316/// The `Value` enum stores the VM values that can move between registers, locals, and buffers.
1317#[derive(Clone, Debug, PartialEq)]
1318pub enum Value {
1319	Bool(bool),
1320	U8(u8),
1321	U16(u16),
1322	U32(u32),
1323	I32(i32),
1324	F32(f32),
1325	Vec2U16([u16; 2]),
1326	Vec4U16([u16; 4]),
1327	Vec2I([i32; 2]),
1328	Vec2U([u32; 2]),
1329	Vec3U([u32; 3]),
1330	Vec4U([u32; 4]),
1331	Vec2F([f32; 2]),
1332	Vec3F([f32; 3]),
1333	Vec4F([f32; 4]),
1334	Mat4F([f32; 16]),
1335	Mat4x3F([f32; 12]),
1336	Resource { slot: ResourceSlot, value_type: ValueType },
1337	Struct { value_type: ValueType, fields: Vec<Value> },
1338}
1339
1340impl Value {
1341	fn value_type(&self) -> ValueType {
1342		match self {
1343			Value::Bool(_) => ValueType::Bool,
1344			Value::U8(_) => ValueType::U8,
1345			Value::U16(_) => ValueType::U16,
1346			Value::U32(_) => ValueType::U32,
1347			Value::I32(_) => ValueType::I32,
1348			Value::F32(_) => ValueType::F32,
1349			Value::Vec2U16(_) => ValueType::Vec2U16,
1350			Value::Vec4U16(_) => ValueType::Vec4U16,
1351			Value::Vec2I(_) => ValueType::Vec2I,
1352			Value::Vec2U(_) => ValueType::Vec2U,
1353			Value::Vec3U(_) => ValueType::Vec3U,
1354			Value::Vec4U(_) => ValueType::Vec4U,
1355			Value::Vec2F(_) => ValueType::Vec2F,
1356			Value::Vec3F(_) => ValueType::Vec3F,
1357			Value::Vec4F(_) => ValueType::Vec4F,
1358			Value::Mat4F(_) => ValueType::Mat4F,
1359			Value::Mat4x3F(_) => ValueType::Mat4x3F,
1360			Value::Resource { value_type, .. } => value_type.clone(),
1361			Value::Struct { value_type, .. } => value_type.clone(),
1362		}
1363	}
1364
1365	fn matches_type(&self, expected: &ValueType) -> bool {
1366		match (self, expected) {
1367			(
1368				Value::Struct { value_type, fields },
1369				ValueType::Struct {
1370					fields: expected_fields, ..
1371				},
1372			) => {
1373				value_type == expected
1374					&& fields.len() == expected_fields.len()
1375					&& fields
1376						.iter()
1377						.zip(expected_fields)
1378						.all(|(field, expected_field)| field.matches_type(expected_field.value_type()))
1379			}
1380			_ => self.value_type() == *expected,
1381		}
1382	}
1383}
1384
1385#[cfg(test)]
1386mod tests;