Skip to main content

vyre_driver/
binding.rs

1//! Backend-neutral binding-plan construction for VYRE programs.
2
3use smallvec::SmallVec;
4use std::sync::Arc;
5use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, MemoryKind, Program};
6
7use crate::BackendError;
8
9/// Host/device binding role assigned to one VYRE buffer.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum BindingRole {
12    /// Host input copied to a read-only device buffer.
13    Input,
14    /// Device output read back after dispatch.
15    Output,
16    /// Host input copied to a read-write device buffer and read back later.
17    InputOutput,
18    /// Uniform-style read-only input.
19    Uniform,
20    /// Workgroup-local memory declared in target code.
21    Shared,
22    /// Persistent memory handle managed by runtime ingest APIs.
23    Persistent,
24}
25
26/// One validated binding descriptor.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Binding {
29    /// VYRE buffer name.
30    pub name: Arc<str>,
31    /// VYRE binding number.
32    pub binding: u32,
33    /// Original buffer index in `Program::buffers`.
34    pub buffer_index: usize,
35    /// Host/device role for launch.
36    pub role: BindingRole,
37    /// Element size in bytes when statically known.
38    pub element_size: usize,
39    /// Preferred byte alignment for backend allocation/upload planning.
40    ///
41    /// This optimization contract is derived from `BufferDecl::hints` and the
42    /// scalar element size. It does not change program semantics; concrete
43    /// drivers use it to choose buffer allocation and launch paths without
44    /// rewalking the IR.
45    pub preferred_alignment: usize,
46    /// Declared or input-derived element count. Zero means runtime-sized.
47    pub element_count: u32,
48    /// Static byte count when known.
49    pub static_byte_len: Option<usize>,
50    /// Index in the caller's input slice, if this binding consumes input.
51    pub input_index: Option<usize>,
52    /// Index in the backend output vector, if this binding is observed output.
53    pub output_index: Option<usize>,
54}
55
56/// Deterministic ABI plan for a VYRE program.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct BindingPlan {
59    /// Ordered binding descriptors, sorted by VYRE binding number.
60    pub bindings: Vec<Binding>,
61    /// Original program buffer indices that consume host inputs.
62    pub input_indices: Vec<usize>,
63    /// Original program buffer indices that produce host outputs.
64    pub output_indices: Vec<usize>,
65    /// Original program buffer indices that are workgroup-local.
66    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    /// Build a binding plan from a VYRE program without host input checks.
99    ///
100    /// # Errors
101    ///
102    /// Returns when memory/access combinations or static byte sizing cannot be
103    /// represented by a concrete backend ABI.
104    pub fn build(program: &Program) -> Result<Self, BackendError> {
105        Self::build_inner(program, InputLengths::None, false)
106    }
107
108    /// Build and validate a binding plan from a VYRE program.
109    ///
110    /// # Errors
111    ///
112    /// Returns when input count, input byte lengths, buffer alignment, or
113    /// memory/access combinations do not match the backend ABI contract.
114    pub fn from_program(program: &Program, inputs: &[Vec<u8>]) -> Result<Self, BackendError> {
115        Self::build_inner(program, InputLengths::Owned(inputs), true)
116    }
117
118    /// Build and validate a binding plan from borrowed input buffers.
119    ///
120    /// # Errors
121    ///
122    /// Returns when input count, input byte lengths, buffer alignment, or
123    /// memory/access combinations do not match the backend ABI contract.
124    pub fn from_borrowed_inputs(program: &Program, inputs: &[&[u8]]) -> Result<Self, BackendError> {
125        Self::build_inner(program, InputLengths::Borrowed(inputs), true)
126    }
127
128    /// Build and validate a binding plan from backend-resident input byte lengths.
129    ///
130    /// # Errors
131    ///
132    /// Returns when resident input counts are wrong or byte lengths are
133    /// smaller than the program ABI requires.
134    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    /// Verifies backend-resident input byte lengths satisfy this binding plan.
142    ///
143    /// # Errors
144    ///
145    /// Returns when the caller supplies the wrong number of resident inputs or
146    /// a resident input is smaller than the buffer declaration captured in
147    /// this plan.
148    pub fn validate_input_byte_lengths(&self, input_lengths: &[usize]) -> Result<(), BackendError> {
149        self.validate_input_lengths(InputLengths::Lengths(input_lengths))
150    }
151
152    /// Verifies dynamic input slices match the expected plan.
153    ///
154    /// # Errors
155    ///
156    /// Returns when the caller supplies the wrong number of inputs or an input
157    /// length violates the buffer declaration.
158    pub fn validate_inputs(&self, inputs: &[Vec<u8>]) -> Result<(), BackendError> {
159        self.validate_input_lengths(InputLengths::Owned(inputs))
160    }
161
162    /// Verifies borrowed dynamic input slices match the expected plan.
163    ///
164    /// # Errors
165    ///
166    /// Returns when the caller supplies the wrong number of inputs or an input
167    /// length violates the buffer declaration.
168    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/// Element count a runtime-sized buffer takes from the bytes the caller supplied.
501///
502/// A buffer declared without `.with_count(n)` has `count == 0`, which the IR
503/// defines as runtime-sized: its element count is whatever the caller passed for
504/// it on this dispatch. This function is the ONE rule for deriving that count,
505/// `byte_len` divided by the element width, sub-byte element types included.
506///
507/// The CPU reference, CUDA, and WGPU all size runtime-sized writable buffers
508/// through this rule, so a countless declaration reads back the same element
509/// count on every backend. A second copy of this arithmetic is how the three
510/// paths drifted into returning 4 words, 1 word, and 0 words for one program.
511///
512/// Returns `None` when the element type has no fixed width or the count does not
513/// fit `u32`.
514#[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// ---------------------------------------------------------------------------
624// N7 binding-set merging across consecutive dispatches
625// ---------------------------------------------------------------------------
626
627/// Stable fingerprint of a binding set's *layout*  -  the parts that
628/// determine whether two `BindingPlan`s can share a backend bind
629/// group layout / descriptor set.
630///
631/// Two plans with the same [`BindingSetFingerprint`] can reuse the
632/// same `portable::BindGroupLayout` or native descriptor set across
633/// consecutive dispatches, skipping the layout-rebind cost. The
634/// hot-path perf snapshot puts binding rebind at ~20% of warm
635/// dispatch time on attention/softmax/reduce shapes.
636///
637/// Layout (this fingerprint) is distinct from contents (which
638/// `program_vsa_fingerprint` covers)  -  two dispatches of the same
639/// kernel on different input buffers share a layout fingerprint but
640/// differ in their content fingerprint.
641#[derive(Debug, Clone, PartialEq, Eq, Hash)]
642pub struct BindingSetFingerprint {
643    /// Per-binding layout slot: `(binding_index, role, element_size)`.
644    /// Ordered by `binding_index` for deterministic equality.
645    pub slots: Vec<(u32, BindingRole, usize)>,
646}
647
648impl BindingSetFingerprint {
649    /// Derive the layout fingerprint from a `BindingPlan`. Stable
650    /// across runs and across machines (no random salts).
651    #[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/// True when two binding plans can share a backend bind group
664/// layout / descriptor set. This is the N7 merge predicate; a
665/// driver maintains a cache keyed by [`BindingSetFingerprint`] and
666/// reuses the cached layout when this returns `true`.
667#[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/// Backend-neutral descriptor/bind-group layout slot.
673///
674/// Concrete drivers own target-specific object creation, but the
675/// fingerprint used to decide whether a descriptor layout is reusable is
676/// shared here so portable/native/secondary do not grow separate cache-key rules.
677#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
678pub struct BackendLayoutSlot {
679    /// Target descriptor group/set.
680    pub group: u32,
681    /// Binding index inside the descriptor group/set.
682    pub binding: u32,
683    /// Descriptor memory class.
684    pub class: BackendLayoutClass,
685    /// Whether storage descriptors are read-only.
686    pub read_only: bool,
687    /// Element size in bytes when statically known.
688    pub element_size: usize,
689}
690
691/// Backend-neutral descriptor memory class.
692#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
693pub enum BackendLayoutClass {
694    /// Read-write or read-only storage buffer.
695    Storage,
696    /// Uniform/constant buffer.
697    Uniform,
698}
699
700/// Stable descriptor-layout fingerprint for backend object caches.
701#[derive(Debug, Clone, PartialEq, Eq, Hash)]
702pub struct BackendLayoutFingerprint {
703    /// Canonical slots sorted by `(group, binding)`.
704    pub slots: Vec<BackendLayoutSlot>,
705}
706
707impl BackendLayoutFingerprint {
708    /// Build a deterministic fingerprint from unsorted layout slots.
709    #[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        // Same binding shape (slot 0 ReadOnly, slot 1 output, both
735        // U32), different element_count. Layout fingerprint must match;
736        // content fingerprint will not.
737        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        // Three bindings instead of two  -  must NOT share layout.
750        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}