1use smallvec::SmallVec;
4use std::sync::Arc;
5use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, MemoryKind, Program};
6
7use crate::BackendError;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum BindingRole {
12 Input,
14 Output,
16 InputOutput,
18 Uniform,
20 Shared,
22 Persistent,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Binding {
29 pub name: Arc<str>,
31 pub binding: u32,
33 pub buffer_index: usize,
35 pub role: BindingRole,
37 pub element_size: usize,
39 pub preferred_alignment: usize,
46 pub element_count: u32,
48 pub static_byte_len: Option<usize>,
50 pub input_index: Option<usize>,
52 pub output_index: Option<usize>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct BindingPlan {
59 pub bindings: Vec<Binding>,
61 pub input_indices: Vec<usize>,
63 pub output_indices: Vec<usize>,
65 pub shared_indices: Vec<usize>,
67}
68
69#[derive(Clone, Copy)]
70enum InputLengths<'a> {
71 None,
72 Owned(&'a [Vec<u8>]),
73 Borrowed(&'a [&'a [u8]]),
74 Lengths(&'a [usize]),
75}
76
77impl InputLengths<'_> {
78 fn len(self) -> usize {
79 match self {
80 Self::None => 0,
81 Self::Owned(inputs) => inputs.len(),
82 Self::Borrowed(inputs) => inputs.len(),
83 Self::Lengths(lengths) => lengths.len(),
84 }
85 }
86
87 fn get(self, index: usize) -> Option<usize> {
88 match self {
89 Self::None => None,
90 Self::Owned(inputs) => inputs.get(index).map(Vec::len),
91 Self::Borrowed(inputs) => inputs.get(index).map(|input| input.len()),
92 Self::Lengths(lengths) => lengths.get(index).copied(),
93 }
94 }
95}
96
97impl BindingPlan {
98 pub fn build(program: &Program) -> Result<Self, BackendError> {
105 Self::build_inner(program, InputLengths::None, false)
106 }
107
108 pub fn from_program(program: &Program, inputs: &[Vec<u8>]) -> Result<Self, BackendError> {
115 Self::build_inner(program, InputLengths::Owned(inputs), true)
116 }
117
118 pub fn from_borrowed_inputs(program: &Program, inputs: &[&[u8]]) -> Result<Self, BackendError> {
125 Self::build_inner(program, InputLengths::Borrowed(inputs), true)
126 }
127
128 pub fn from_input_lengths(
135 program: &Program,
136 input_lengths: &[usize],
137 ) -> Result<Self, BackendError> {
138 Self::build_inner(program, InputLengths::Lengths(input_lengths), true)
139 }
140
141 pub fn validate_input_byte_lengths(&self, input_lengths: &[usize]) -> Result<(), BackendError> {
149 self.validate_input_lengths(InputLengths::Lengths(input_lengths))
150 }
151
152 pub fn validate_inputs(&self, inputs: &[Vec<u8>]) -> Result<(), BackendError> {
159 self.validate_input_lengths(InputLengths::Owned(inputs))
160 }
161
162 pub fn validate_borrowed_inputs(&self, inputs: &[&[u8]]) -> Result<(), BackendError> {
169 self.validate_input_lengths(InputLengths::Borrowed(inputs))
170 }
171
172 fn validate_input_lengths(&self, input_lens: InputLengths<'_>) -> Result<(), BackendError> {
173 if input_lens.len() != self.input_indices.len() {
174 return Err(BackendError::InvalidProgram {
175 fix: format!(
176 "Fix: dispatch expected {} input buffer(s) from Program declarations but received {}.",
177 self.input_indices.len(),
178 input_lens.len()
179 ),
180 });
181 }
182
183 for binding in &self.bindings {
184 if let Some(input_index) = binding.input_index {
185 let byte_len = input_lens.get(input_index).ok_or_else(|| {
186 BackendError::InvalidProgram {
187 fix: format!(
188 "Fix: dispatch input index {input_index} for `{}` was missing after input-count validation.",
189 binding.name
190 ),
191 }
192 })?;
193 validate_input_len(
194 binding,
195 byte_len,
196 !matches!(input_lens, InputLengths::Lengths(_)),
197 )?;
198 }
199 }
200 Ok(())
201 }
202
203 fn build_inner(
204 program: &Program,
205 input_lens: InputLengths<'_>,
206 validate_inputs_now: bool,
207 ) -> Result<Self, BackendError> {
208 let mut ordered = SmallVec::<[(usize, &BufferDecl); 16]>::new();
209 vyre_foundation::allocation::try_reserve_smallvec_to_capacity(
210 &mut ordered,
211 program.buffers().len(),
212 )
213 .map_err(|error| {
214 BackendError::InvalidProgram {
215 fix: format!(
216 "Fix: binding-plan construction could not reserve {} ordered buffer slot(s): {error}. Split the program buffers or construct a smaller pipeline.",
217 program.buffers().len()
218 ),
219 }
220 })?;
221 let buffer_count = program.buffers().len();
222 ordered.extend(program.buffers().iter().enumerate());
223 ordered.sort_by_key(|(_, buffer)| buffer.binding());
224
225 let mut bindings = Vec::new();
226 crate::allocation::try_reserve_vec_to_capacity(&mut bindings, ordered.len()).map_err(
227 |error| BackendError::InvalidProgram {
228 fix: format!(
229 "Fix: binding-plan construction could not reserve {} binding descriptor(s): {error}. Split the program buffers or construct a smaller pipeline.",
230 ordered.len()
231 ),
232 },
233 )?;
234 let (input_slot_count, output_slot_count, shared_slot_count) =
235 binding_role_counts(&ordered)?;
236 let mut logical_input_slots = Vec::new();
237 crate::allocation::try_reserve_vec_to_capacity(&mut logical_input_slots, buffer_count)
238 .map_err(|error| BackendError::InvalidProgram {
239 fix: format!(
240 "Fix: binding-plan construction could not reserve {buffer_count} logical input slot(s): {error}. Split the program buffers or construct a smaller pipeline.",
241 ),
242 })?;
243 logical_input_slots.resize(buffer_count, None);
244 let mut logical_output_slots = Vec::new();
245 crate::allocation::try_reserve_vec_to_capacity(&mut logical_output_slots, buffer_count)
246 .map_err(|error| BackendError::InvalidProgram {
247 fix: format!(
248 "Fix: binding-plan construction could not reserve {buffer_count} logical output slot(s): {error}. Split the program buffers or construct a smaller pipeline.",
249 ),
250 })?;
251 logical_output_slots.resize(buffer_count, None);
252 let mut input_indices = SmallVec::<[usize; 8]>::new();
253 let mut output_indices = SmallVec::<[usize; 8]>::new();
254 let mut shared_indices = SmallVec::<[usize; 4]>::new();
255 vyre_foundation::allocation::try_reserve_smallvec_to_capacity(
256 &mut input_indices,
257 input_slot_count,
258 )
259 .map_err(|error| {
260 BackendError::InvalidProgram {
261 fix: format!(
262 "Fix: binding-plan construction could not reserve {input_slot_count} input index slot(s): {error}. Split the program buffers or construct a smaller pipeline."
263 ),
264 }
265 })?;
266 vyre_foundation::allocation::try_reserve_smallvec_to_capacity(
267 &mut output_indices,
268 output_slot_count,
269 )
270 .map_err(|error| {
271 BackendError::InvalidProgram {
272 fix: format!(
273 "Fix: binding-plan construction could not reserve {output_slot_count} output index slot(s): {error}. Split the program buffers or construct a smaller pipeline."
274 ),
275 }
276 })?;
277 vyre_foundation::allocation::try_reserve_smallvec_to_capacity(
278 &mut shared_indices,
279 shared_slot_count,
280 )
281 .map_err(|error| {
282 BackendError::InvalidProgram {
283 fix: format!(
284 "Fix: binding-plan construction could not reserve {shared_slot_count} shared index slot(s): {error}. Split the program buffers or construct a smaller pipeline."
285 ),
286 }
287 })?;
288
289 for (buffer_index, buffer) in program.buffers().iter().enumerate() {
290 let role = role_for_buffer(buffer)?;
291 if matches!(
292 role,
293 BindingRole::Input | BindingRole::InputOutput | BindingRole::Uniform
294 ) {
295 let index = input_indices.len();
296 input_indices.push(buffer_index);
297 logical_input_slots[buffer_index] = Some(index);
298 }
299 if matches!(role, BindingRole::Output | BindingRole::InputOutput)
300 || buffer.pipeline_live_out
301 {
302 let index = output_indices.len();
303 output_indices.push(buffer_index);
304 logical_output_slots[buffer_index] = Some(index);
305 }
306 if role == BindingRole::Shared {
307 shared_indices.push(buffer_index);
308 }
309 }
310
311 for (buffer_index, buffer) in ordered {
312 let role = role_for_buffer(buffer)?;
313 let consumes_input = matches!(
314 role,
315 BindingRole::Input | BindingRole::InputOutput | BindingRole::Uniform
316 );
317 let produces_output = matches!(role, BindingRole::Output | BindingRole::InputOutput);
318 buffer
319 .element()
320 .validate_layout()
321 .map_err(|error| BackendError::InvalidProgram {
322 fix: format!(
323 "Fix: binding `{}` has malformed data-type layout metadata: {error}",
324 buffer.name()
325 ),
326 })?;
327 let element_size = buffer.element().min_bytes();
328 let static_byte_len = static_byte_len(buffer)?;
329 let preferred_alignment = preferred_alignment(buffer, element_size)?;
330
331 let input_index = if consumes_input {
332 Some(logical_input_slots
333 .get(buffer_index)
334 .copied()
335 .flatten()
336 .ok_or_else(|| BackendError::InvalidProgram {
337 fix: format!(
338 "Fix: binding `{}` consumes input but no logical input slot was assigned. Rebuild BindingPlan from Program::buffers order before launch.",
339 buffer.name()
340 ),
341 })?)
342 } else {
343 None
344 };
345 let output_index = if produces_output || buffer.pipeline_live_out {
346 Some(logical_output_slots
347 .get(buffer_index)
348 .copied()
349 .flatten()
350 .ok_or_else(|| BackendError::InvalidProgram {
351 fix: format!(
352 "Fix: binding `{}` produces output but no logical output slot was assigned. Rebuild BindingPlan from Program::buffers order before readback.",
353 buffer.name()
354 ),
355 })?)
356 } else {
357 None
358 };
359 let element_count = if buffer.count() == 0 {
360 input_index
361 .and_then(|index| input_lens.get(index))
362 .and_then(|byte_len| {
363 dynamic_element_count_from_bytes(&buffer.element, byte_len)
364 })
365 .unwrap_or(0)
366 } else {
367 buffer.count()
368 };
369
370 bindings.push(Binding {
371 name: Arc::clone(&buffer.name),
372 binding: buffer.binding(),
373 buffer_index,
374 role,
375 element_size,
376 preferred_alignment,
377 element_count,
378 static_byte_len,
379 input_index,
380 output_index,
381 });
382 }
383
384 let plan = Self {
385 bindings,
386 input_indices: input_indices.into_vec(),
387 output_indices: output_indices.into_vec(),
388 shared_indices: shared_indices.into_vec(),
389 };
390
391 if validate_inputs_now {
392 plan.validate_input_lengths(input_lens)?;
393 }
394
395 Ok(plan)
396 }
397}
398
399fn binding_role_counts(
400 ordered: &SmallVec<[(usize, &BufferDecl); 16]>,
401) -> Result<(usize, usize, usize), BackendError> {
402 ordered
403 .iter()
404 .try_fold((0usize, 0usize, 0usize), |(inputs, outputs, shared), (_, buffer)| {
405 let role = role_for_buffer(buffer)?;
406 let next_inputs = inputs
407 .checked_add(usize::from(matches!(
408 role,
409 BindingRole::Input | BindingRole::InputOutput | BindingRole::Uniform
410 )))
411 .ok_or_else(|| BackendError::InvalidProgram {
412 fix: "Fix: binding-plan input role count overflowed usize. Split the program buffers before binding-plan construction.".to_string(),
413 })?;
414 let next_outputs = outputs
415 .checked_add(usize::from(
416 matches!(role, BindingRole::Output | BindingRole::InputOutput)
417 || buffer.pipeline_live_out,
418 ))
419 .ok_or_else(|| BackendError::InvalidProgram {
420 fix: "Fix: binding-plan output role count overflowed usize. Split the program buffers before binding-plan construction.".to_string(),
421 })?;
422 let next_shared = shared
423 .checked_add(usize::from(role == BindingRole::Shared))
424 .ok_or_else(|| BackendError::InvalidProgram {
425 fix: "Fix: binding-plan shared role count overflowed usize. Split the program buffers before binding-plan construction.".to_string(),
426 })?;
427 Ok((next_inputs, next_outputs, next_shared))
428 })
429}
430
431fn role_for_buffer(buffer: &BufferDecl) -> Result<BindingRole, BackendError> {
432 if buffer.kind() == MemoryKind::Shared || buffer.access() == BufferAccess::Workgroup {
433 return Ok(BindingRole::Shared);
434 }
435 if buffer.kind() == MemoryKind::Persistent {
436 return Ok(BindingRole::Persistent);
437 }
438 if buffer.is_output || buffer.pipeline_live_out {
439 return Ok(BindingRole::Output);
440 }
441 match buffer.access() {
442 BufferAccess::ReadOnly => Ok(BindingRole::Input),
443 BufferAccess::ReadWrite => Ok(BindingRole::InputOutput),
444 BufferAccess::WriteOnly => Ok(BindingRole::Output),
445 BufferAccess::Uniform => Ok(BindingRole::Uniform),
446 BufferAccess::Workgroup => Ok(BindingRole::Shared),
447 _ => Err(BackendError::InvalidProgram {
448 fix: format!(
449 "Fix: binding `{}` uses an unknown BufferAccess variant; update vyre-driver binding role mapping.",
450 buffer.name()
451 ),
452 }),
453 }
454}
455
456fn preferred_alignment(buffer: &BufferDecl, element_size: usize) -> Result<usize, BackendError> {
457 let hinted = usize::try_from(buffer.hints().preferred_alignment).map_err(|_| {
458 BackendError::InvalidProgram {
459 fix: format!(
460 "Fix: binding `{}` preferred_alignment does not fit usize on this target.",
461 buffer.name()
462 ),
463 }
464 })?;
465 if hinted != 0 && !hinted.is_power_of_two() {
466 return Err(BackendError::InvalidProgram {
467 fix: format!(
468 "Fix: binding `{}` preferred_alignment={} is not a power of two. Use 0 or a power-of-two byte alignment.",
469 buffer.name(),
470 hinted
471 ),
472 });
473 }
474 Ok(hinted.max(element_size.max(1)))
475}
476
477fn static_byte_len(buffer: &BufferDecl) -> Result<Option<usize>, BackendError> {
478 let bytes = buffer
479 .static_byte_len()
480 .map_err(|error| BackendError::InvalidProgram {
481 fix: format!(
482 "Fix: binding `{}` static byte length could not be computed: {error}",
483 buffer.name(),
484 ),
485 })?;
486 if buffer.count() == 0 {
487 return Ok(None);
488 }
489 bytes
490 .map(Some)
491 .ok_or_else(|| BackendError::InvalidProgram {
492 fix: format!(
493 "Fix: binding `{}` declares {} elements of a runtime-sized data type; use a byte-addressed buffer contract or a fixed-width element type.",
494 buffer.name(),
495 buffer.count()
496 ),
497 })
498}
499
500#[must_use]
515pub fn dynamic_element_count_from_bytes(element: &DataType, byte_len: usize) -> Option<u32> {
516 if let Some(bits) = element.bit_width() {
517 let total_bits = byte_len.checked_mul(8)?;
518 return u32::try_from(total_bits / bits).ok();
519 }
520 element
521 .size_bytes()
522 .and_then(|element_size| byte_len.checked_div(element_size))
523 .and_then(|count| u32::try_from(count).ok())
524}
525
526fn validate_input_len(
527 binding: &Binding,
528 input_len: usize,
529 strict_static_input_len: bool,
530) -> Result<(), BackendError> {
531 if binding.element_size > 1 && input_len % binding.element_size != 0 {
532 return Err(BackendError::InvalidProgram {
533 fix: format!(
534 "Fix: input `{}` has {} bytes, which is not aligned to its {}-byte element size.",
535 binding.name, input_len, binding.element_size
536 ),
537 });
538 }
539 if let Some(expected) = binding.static_byte_len {
540 if strict_static_input_len && input_len != expected {
541 return Err(BackendError::InvalidProgram {
542 fix: format!(
543 "Fix: input `{}` expected {expected} bytes from its static buffer declaration but received {} bytes.",
544 binding.name,
545 input_len
546 ),
547 });
548 }
549 if !strict_static_input_len && input_len < expected {
550 return Err(BackendError::InvalidProgram {
551 fix: format!(
552 "Fix: resident input `{}` expected at least {expected} bytes from its static buffer declaration but received {} bytes.",
553 binding.name, input_len
554 ),
555 });
556 }
557 }
558 Ok(())
559}
560
561#[cfg(test)]
562mod exact_length_tests {
563 use super::*;
564 use vyre_foundation::ir::DataType;
565
566 fn static_u32_input_program(count: u32) -> Program {
567 Program::wrapped(
568 vec![BufferDecl::read("input", 0, DataType::U32).with_count(count)],
569 [1, 1, 1],
570 Vec::new(),
571 )
572 }
573
574 #[test]
575 fn static_host_inputs_are_exact_while_resident_inputs_may_be_larger() {
576 let program = static_u32_input_program(2);
577 let short = vec![0u8; 4];
578 let exact = vec![0u8; 8];
579 let oversized = vec![0u8; 12];
580
581 let owned_err = BindingPlan::from_program(&program, &[short.clone()])
582 .expect_err("owned static input length must be exact");
583 assert!(owned_err.to_string().contains("expected 8 bytes"));
584 assert!(BindingPlan::from_program(&program, &[exact.clone()]).is_ok());
585 let owned_oversized_err = BindingPlan::from_program(&program, &[oversized.clone()])
586 .expect_err("owned static input length must remain exact");
587 assert!(owned_oversized_err.to_string().contains("expected 8 bytes"));
588
589 let borrowed_short = [short.as_slice()];
590 let borrowed_err = BindingPlan::from_borrowed_inputs(&program, &borrowed_short)
591 .expect_err("borrowed static input length must be exact");
592 assert!(borrowed_err.to_string().contains("expected 8 bytes"));
593 let borrowed_oversized = [oversized.as_slice()];
594 let borrowed_oversized_err =
595 BindingPlan::from_borrowed_inputs(&program, &borrowed_oversized)
596 .expect_err("borrowed static input length must remain exact");
597 assert!(borrowed_oversized_err
598 .to_string()
599 .contains("expected 8 bytes"));
600
601 let resident_err = BindingPlan::from_input_lengths(&program, &[4])
602 .expect_err("resident static input length must not be smaller than the ABI");
603 assert!(resident_err.to_string().contains("at least 8 bytes"));
604 let resident_exact = BindingPlan::from_input_lengths(&program, &[8])
605 .expect("resident input equal to the ABI size should validate");
606 assert_eq!(resident_exact.bindings[0].element_count, 2);
607 let resident_oversized = BindingPlan::from_input_lengths(&program, &[12])
608 .expect("resident input larger than the ABI size should validate");
609 assert_eq!(resident_oversized.bindings[0].element_count, 2);
610 }
611
612 #[test]
613 fn dynamic_input_length_sets_runtime_element_count() {
614 let program = static_u32_input_program(0);
615 let plan = BindingPlan::from_program(&program, &[vec![0u8; 12]])
616 .expect("Fix: reject bindings without known element width; do not dispatch un-sized dynamic inputs - dynamic input byte length should define element count");
617
618 assert_eq!(plan.bindings[0].element_count, 3);
619 assert_eq!(plan.bindings[0].static_byte_len, None);
620 }
621}
622
623#[derive(Debug, Clone, PartialEq, Eq, Hash)]
642pub struct BindingSetFingerprint {
643 pub slots: Vec<(u32, BindingRole, usize)>,
646}
647
648impl BindingSetFingerprint {
649 #[must_use]
652 pub fn from_plan(plan: &BindingPlan) -> Self {
653 let mut slots: Vec<(u32, BindingRole, usize)> = plan
654 .bindings
655 .iter()
656 .map(|b| (b.binding, b.role, b.element_size))
657 .collect();
658 slots.sort_by_key(|(idx, _, _)| *idx);
659 Self { slots }
660 }
661}
662
663#[must_use]
668pub fn binding_plans_share_layout(a: &BindingPlan, b: &BindingPlan) -> bool {
669 BindingSetFingerprint::from_plan(a) == BindingSetFingerprint::from_plan(b)
670}
671
672#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
678pub struct BackendLayoutSlot {
679 pub group: u32,
681 pub binding: u32,
683 pub class: BackendLayoutClass,
685 pub read_only: bool,
687 pub element_size: usize,
689}
690
691#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
693pub enum BackendLayoutClass {
694 Storage,
696 Uniform,
698}
699
700#[derive(Debug, Clone, PartialEq, Eq, Hash)]
702pub struct BackendLayoutFingerprint {
703 pub slots: Vec<BackendLayoutSlot>,
705}
706
707impl BackendLayoutFingerprint {
708 #[must_use]
710 pub fn new(mut slots: Vec<BackendLayoutSlot>) -> Self {
711 slots.sort_by_key(|slot| (slot.group, slot.binding));
712 Self { slots }
713 }
714}
715
716#[cfg(test)]
717mod n7_tests {
718 use super::*;
719 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Program};
720
721 fn add_one_program() -> Program {
722 Program::wrapped(
723 vec![
724 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
725 .with_count(16),
726 BufferDecl::output("out", 1, DataType::U32).with_count(16),
727 ],
728 [16, 1, 1],
729 vec![],
730 )
731 }
732
733 fn add_one_program_different_input_count() -> Program {
734 Program::wrapped(
738 vec![
739 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
740 .with_count(64),
741 BufferDecl::output("out", 1, DataType::U32).with_count(64),
742 ],
743 [16, 1, 1],
744 vec![],
745 )
746 }
747
748 fn different_layout_program() -> Program {
749 Program::wrapped(
751 vec![
752 BufferDecl::storage("a", 0, BufferAccess::ReadOnly, DataType::U32).with_count(16),
753 BufferDecl::storage("b", 1, BufferAccess::ReadOnly, DataType::U32).with_count(16),
754 BufferDecl::output("out", 2, DataType::U32).with_count(16),
755 ],
756 [16, 1, 1],
757 vec![],
758 )
759 }
760
761 #[test]
762 fn same_layout_with_different_element_counts_shares_fingerprint() {
763 let a = BindingPlan::build(&add_one_program()).unwrap();
764 let b = BindingPlan::build(&add_one_program_different_input_count()).unwrap();
765 assert!(
766 binding_plans_share_layout(&a, &b),
767 "plans with same (binding, role, element_size) tuples must share layout"
768 );
769 }
770
771 #[test]
772 fn different_binding_count_does_not_share_layout() {
773 let a = BindingPlan::build(&add_one_program()).unwrap();
774 let b = BindingPlan::build(&different_layout_program()).unwrap();
775 assert!(
776 !binding_plans_share_layout(&a, &b),
777 "plans with different binding count must not share layout"
778 );
779 }
780
781 #[test]
782 fn fingerprint_is_stable_across_repeated_builds() {
783 let a = BindingPlan::build(&add_one_program()).unwrap();
784 let b = BindingPlan::build(&add_one_program()).unwrap();
785 assert_eq!(
786 BindingSetFingerprint::from_plan(&a),
787 BindingSetFingerprint::from_plan(&b),
788 "repeated build of the same Program must produce identical fingerprints"
789 );
790 }
791
792 #[test]
793 fn fingerprint_slots_are_sorted_by_binding_index() {
794 let plan = BindingPlan::build(&add_one_program()).unwrap();
795 let fp = BindingSetFingerprint::from_plan(&plan);
796 let indices: Vec<u32> = fp.slots.iter().map(|(i, _, _)| *i).collect();
797 assert_eq!(indices, [0, 1], "slots must be sorted by binding index");
798 }
799
800 #[test]
801 fn backend_layout_fingerprint_sorts_slots() {
802 let a = BackendLayoutFingerprint::new(vec![
803 BackendLayoutSlot {
804 group: 1,
805 binding: 4,
806 class: BackendLayoutClass::Storage,
807 read_only: false,
808 element_size: 4,
809 },
810 BackendLayoutSlot {
811 group: 0,
812 binding: 1,
813 class: BackendLayoutClass::Uniform,
814 read_only: true,
815 element_size: 4,
816 },
817 ]);
818 let b = BackendLayoutFingerprint::new(vec![
819 BackendLayoutSlot {
820 group: 0,
821 binding: 1,
822 class: BackendLayoutClass::Uniform,
823 read_only: true,
824 element_size: 4,
825 },
826 BackendLayoutSlot {
827 group: 1,
828 binding: 4,
829 class: BackendLayoutClass::Storage,
830 read_only: false,
831 element_size: 4,
832 },
833 ]);
834 assert_eq!(a, b);
835 }
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841 use vyre_foundation::ir::{CacheLocality, DataType, MemoryHints};
842
843 #[test]
844 fn binding_plan_carries_alignment_hints() {
845 let program = Program::wrapped(
846 vec![BufferDecl::output("out", 0, DataType::U32)
847 .with_count(16)
848 .with_hints(MemoryHints {
849 coalesce_axis: Some(0),
850 preferred_alignment: 64,
851 cache_locality: CacheLocality::Streaming,
852 })],
853 [64, 1, 1],
854 vec![],
855 );
856 let plan = BindingPlan::build(&program).expect("Fix: alignment hint should build");
857 assert_eq!(plan.bindings[0].preferred_alignment, 64);
858 }
859
860 #[test]
861 fn binding_plan_keeps_logical_slots_when_binding_numbers_are_reordered() {
862 let program = Program::wrapped(
863 vec![
864 BufferDecl::read("declared_first_high_binding", 9, DataType::U32),
865 BufferDecl::output("declared_output_first_high_binding", 8, DataType::U32)
866 .with_count(1),
867 BufferDecl::read("declared_second_low_binding", 0, DataType::U32),
868 BufferDecl::output("declared_output_second_low_binding", 1, DataType::U32)
869 .with_count(1),
870 ],
871 [1, 1, 1],
872 vec![],
873 );
874 let inputs = [vec![0u8; 12], vec![0u8; 8]];
875
876 let plan = BindingPlan::from_program(&program, &inputs)
877 .expect("Fix: binding plan must accept logical input order before descriptor sorting");
878
879 assert_eq!(
880 plan.bindings
881 .iter()
882 .map(|binding| binding.binding)
883 .collect::<Vec<_>>(),
884 [0, 1, 8, 9],
885 "descriptor ABI must remain sorted by VYRE binding number"
886 );
887 assert_eq!(
888 plan.input_indices,
889 [0, 2],
890 "caller input slots must follow Program::buffers declaration order"
891 );
892 assert_eq!(
893 plan.output_indices,
894 [1, 3],
895 "backend output slots must follow Program::buffers declaration order"
896 );
897
898 let high_input = plan
899 .bindings
900 .iter()
901 .find(|binding| binding.binding == 9)
902 .expect("high binding input descriptor must exist");
903 assert_eq!(high_input.input_index, Some(0));
904 assert_eq!(high_input.element_count, 3);
905
906 let low_input = plan
907 .bindings
908 .iter()
909 .find(|binding| binding.binding == 0)
910 .expect("low binding input descriptor must exist");
911 assert_eq!(low_input.input_index, Some(1));
912 assert_eq!(low_input.element_count, 2);
913
914 let high_output = plan
915 .bindings
916 .iter()
917 .find(|binding| binding.binding == 8)
918 .expect("high binding output descriptor must exist");
919 assert_eq!(high_output.output_index, Some(0));
920
921 let low_output = plan
922 .bindings
923 .iter()
924 .find(|binding| binding.binding == 1)
925 .expect("low binding output descriptor must exist");
926 assert_eq!(low_output.output_index, Some(1));
927 }
928
929 #[test]
930 fn binding_plan_rejects_non_power_of_two_alignment_hint() {
931 let program = Program::wrapped(
932 vec![BufferDecl::output("out", 0, DataType::U32)
933 .with_count(16)
934 .with_hints(MemoryHints {
935 coalesce_axis: None,
936 preferred_alignment: 48,
937 cache_locality: CacheLocality::Temporal,
938 })],
939 [64, 1, 1],
940 vec![],
941 );
942 let err = BindingPlan::build(&program).expect_err("bad alignment must fail");
943 assert!(format!("{err}").contains("preferred_alignment=48"));
944 }
945
946 #[test]
947 fn binding_plan_alignment_defaults_to_element_size() {
948 let program = Program::wrapped(
949 vec![BufferDecl::output("out", 0, DataType::U32).with_count(16)],
950 [64, 1, 1],
951 vec![],
952 );
953 let plan = BindingPlan::build(&program).expect("Fix: default alignment should build");
954 assert_eq!(plan.bindings[0].preferred_alignment, 4);
955 }
956
957 #[test]
958 fn binding_plan_uses_packed_static_byte_len_for_subbyte_elements() {
959 let program = Program::wrapped(
960 vec![
961 BufferDecl::storage("packed_i4", 0, BufferAccess::ReadOnly, DataType::I4)
962 .with_count(3),
963 ],
964 [1, 1, 1],
965 vec![],
966 );
967 let plan =
968 BindingPlan::build(&program).expect("Fix: packed I4 binding layout should build");
969
970 assert_eq!(plan.bindings[0].element_size, 1);
971 assert_eq!(plan.bindings[0].static_byte_len, Some(2));
972 }
973
974 #[test]
975 fn binding_plan_validates_packed_static_input_lengths() {
976 let program = Program::wrapped(
977 vec![
978 BufferDecl::storage("packed_i4", 0, BufferAccess::ReadOnly, DataType::I4)
979 .with_count(3),
980 ],
981 [1, 1, 1],
982 vec![],
983 );
984 let plan = BindingPlan::from_input_lengths(&program, &[2])
985 .expect("Fix: packed I4 input should accept the exact packed byte count");
986
987 plan.validate_input_byte_lengths(&[2])
988 .expect("Fix: cached packed I4 input length should remain valid");
989 plan.validate_input_byte_lengths(&[3])
990 .expect("Fix: resident packed I4 input may be larger than its static ABI byte count");
991 let error = plan
992 .validate_input_byte_lengths(&[1])
993 .expect_err("undersized resident byte length must not satisfy packed I4 contract");
994 assert!(
995 format!("{error}").contains("at least 2 bytes"),
996 "Fix: packed resident byte mismatch must be explicit: {error}"
997 );
998 }
999
1000 #[test]
1001 fn binding_plan_rejects_malformed_data_type_layouts() {
1002 let program = Program::wrapped(
1003 vec![BufferDecl::output(
1004 "bad_vec",
1005 0,
1006 DataType::Vec {
1007 element: Box::new(DataType::U32),
1008 count: 0,
1009 },
1010 )
1011 .with_count(1)],
1012 [1, 1, 1],
1013 vec![],
1014 );
1015
1016 let error = BindingPlan::build(&program)
1017 .expect_err("zero-lane vector layout must not enter binding planning");
1018 assert!(
1019 format!("{error}").contains("Vec count must be > 0"),
1020 "Fix: malformed data-type layout diagnostics must survive binding planning: {error}"
1021 );
1022 }
1023
1024 #[test]
1025 fn binding_plan_validates_cached_resident_input_lengths() {
1026 let program = Program::wrapped(
1027 vec![
1028 BufferDecl::read("in", 0, DataType::U32).with_count(4),
1029 BufferDecl::output("out", 1, DataType::U32).with_count(4),
1030 ],
1031 [4, 1, 1],
1032 vec![],
1033 );
1034 let plan = BindingPlan::from_input_lengths(&program, &[16])
1035 .expect("Fix: resident input length should match the declared u32[4] input");
1036
1037 plan.validate_input_byte_lengths(&[16])
1038 .expect("Fix: cached resident plan should accept the same input byte length");
1039 plan.validate_input_byte_lengths(&[20])
1040 .expect("Fix: cached resident plan should accept a larger reused allocation");
1041 let error = plan
1042 .validate_input_byte_lengths(&[12])
1043 .expect_err("cached resident plan must reject stale pipeline shape reuse");
1044 assert!(
1045 format!("{error}").contains("at least 16 bytes"),
1046 "wrong resident input length must produce an actionable size mismatch: {error}"
1047 );
1048 }
1049}