use std::sync::Arc;
use rustc_hash::FxHashSet;
use vyre_driver::{BackendError, BackendLayoutClass, BackendLayoutFingerprint, BackendLayoutSlot};
use vyre_emit_naga::program::TrapTag;
use vyre_lower::TRAP_SIDECAR_NAME;
use crate::descriptor_mapping::{
descriptor_bind_group, descriptor_buffer_access, descriptor_memory_kind,
};
use crate::pipeline::element_size_bytes;
#[derive(Clone, Debug)]
pub(crate) struct BufferBindingInfo {
pub group: u32,
pub binding: u32,
pub name: Arc<str>,
pub access: vyre_foundation::ir::BufferAccess,
pub kind: vyre_foundation::ir::MemoryKind,
pub hints: vyre_foundation::ir::MemoryHints,
pub element: vyre_foundation::ir::DataType,
pub count: u32,
pub is_output: bool,
pub preserve_input_contents: bool,
pub internal_trap: bool,
}
pub(crate) fn descriptor_buffer_bindings(
descriptor: &vyre_lower::KernelDescriptor,
public_output_bindings: &FxHashSet<u32>,
explicit_output_bindings: &FxHashSet<u32>,
pipeline_live_out_bindings: &FxHashSet<u32>,
) -> Result<Vec<BufferBindingInfo>, BackendError> {
let mut bindings = Vec::new();
vyre_driver::allocation::try_reserve_vec_to_capacity(
&mut bindings,
descriptor.bindings.slots.len(),
)
.map_err(|source| {
BackendError::new(format!(
"descriptor buffer binding allocation failed for {} slots: {source}. Fix: split the lowered kernel before WGPU pipeline metadata extraction.",
descriptor.bindings.slots.len()
))
})?;
for slot in &descriptor.bindings.slots {
let Some(group) = descriptor_bind_group(slot.memory_class) else {
continue;
};
let access = descriptor_buffer_access(slot.visibility);
let internal_trap = slot.name == TRAP_SIDECAR_NAME;
let is_output = public_output_bindings.contains(&slot.slot) && !internal_trap;
let explicit_output = explicit_output_bindings.contains(&slot.slot);
let pipeline_live_out = pipeline_live_out_bindings.contains(&slot.slot);
let preserve_input_contents = access == vyre_foundation::ir::BufferAccess::ReadWrite
&& !explicit_output
&& !(is_output && pipeline_live_out)
&& !internal_trap;
bindings.push(BufferBindingInfo {
group,
binding: slot.slot,
name: Arc::from(slot.name.as_str()),
access,
kind: descriptor_memory_kind(slot.memory_class),
hints: vyre_foundation::ir::MemoryHints::default(),
element: slot.element_type.clone(),
count: descriptor_element_count(slot.element_count),
is_output,
preserve_input_contents,
internal_trap,
});
}
Ok(bindings)
}
fn descriptor_element_count(element_count: Option<u32>) -> u32 {
match element_count {
Some(count) => count,
None => 0,
}
}
pub(crate) fn bind_group_layout_fingerprint(
bindings: &[BufferBindingInfo],
) -> Result<BackendLayoutFingerprint, BackendError> {
let mut slots = Vec::new();
vyre_driver::allocation::try_reserve_vec_to_capacity(&mut slots, bindings.len()).map_err(|source| {
BackendError::new(format!(
"bind-group layout fingerprint allocation failed for {} bindings: {source}. Fix: split the lowered kernel before WGPU pipeline metadata extraction.",
bindings.len()
))
})?;
for binding in bindings {
let class = match binding.kind {
vyre_foundation::ir::MemoryKind::Uniform | vyre_foundation::ir::MemoryKind::Push => {
BackendLayoutClass::Uniform
}
_ => BackendLayoutClass::Storage,
};
let read_only = matches!(binding.kind, vyre_foundation::ir::MemoryKind::Readonly)
|| matches!(
binding.access,
vyre_foundation::ir::BufferAccess::ReadOnly
| vyre_foundation::ir::BufferAccess::Uniform
);
slots.push(BackendLayoutSlot {
group: binding.group,
binding: binding.binding,
class,
read_only,
element_size: element_size_bytes(&binding.element)?,
});
}
Ok(BackendLayoutFingerprint::new(slots))
}
pub(crate) fn create_bind_group_layouts(
device: &wgpu::Device,
buffer_bindings: &[BufferBindingInfo],
max_group: u32,
) -> Result<Arc<[Arc<wgpu::BindGroupLayout>]>, BackendError> {
let group_count = max_group.checked_add(1).ok_or_else(|| {
BackendError::new(
"bind-group layout count overflowed u32. Fix: lower the maximum bind-group index before WGPU pipeline creation.",
)
})?;
let group_count = usize::try_from(group_count).map_err(|source| {
BackendError::new(format!(
"bind-group layout count cannot fit host usize: {source}. Fix: reduce the maximum bind-group index before WGPU pipeline creation."
))
})?;
let mut layouts: Vec<Arc<wgpu::BindGroupLayout>> = Vec::new();
vyre_driver::allocation::try_reserve_vec_to_capacity(&mut layouts, group_count).map_err(|source| {
BackendError::new(format!(
"bind-group layout vector allocation failed for {group_count} groups: {source}. Fix: split the lowered kernel before WGPU pipeline creation."
))
})?;
for group_index in 0..=max_group {
let group_binding_count = buffer_bindings
.iter()
.filter(|binding| binding.group == group_index)
.count();
let mut entries = Vec::new();
vyre_driver::allocation::try_reserve_vec_to_capacity(&mut entries, group_binding_count).map_err(|source| {
BackendError::new(format!(
"bind-group layout entry allocation failed for group {group_index} with {group_binding_count} bindings: {source}. Fix: split the lowered kernel before WGPU pipeline creation."
))
})?;
for binding in buffer_bindings
.iter()
.filter(|binding| binding.group == group_index)
{
let ty = match binding.kind {
vyre_foundation::ir::MemoryKind::Uniform
| vyre_foundation::ir::MemoryKind::Push => wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
_ => {
let read_only =
matches!(binding.kind, vyre_foundation::ir::MemoryKind::Readonly)
|| matches!(
binding.access,
vyre_foundation::ir::BufferAccess::ReadOnly
| vyre_foundation::ir::BufferAccess::Uniform
);
wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only },
has_dynamic_offset: false,
min_binding_size: None,
}
}
};
entries.push(wgpu::BindGroupLayoutEntry {
binding: binding.binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty,
count: None,
});
}
layouts.push(Arc::new(device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: Some("vyre P-6 bind group layout"),
entries: &entries,
},
)));
}
Ok(layouts.into())
}
pub(crate) fn descriptor_trap_tags(
descriptor: &vyre_lower::KernelDescriptor,
) -> Result<Vec<TrapTag>, BackendError> {
fn recursive_op_count(body: &vyre_lower::KernelBody) -> Result<usize, BackendError> {
let mut count = body.ops.len();
for child in &body.child_bodies {
count = count.checked_add(recursive_op_count(child)?).ok_or_else(|| {
BackendError::new(
"kernel descriptor recursive op count overflowed usize. Fix: split nested kernel bodies before descriptor metadata extraction.",
)
})?;
}
Ok(count)
}
fn walk(
body: &vyre_lower::KernelBody,
seen: &mut FxHashSet<vyre_lower::descriptor::Name>,
out: &mut Vec<TrapTag>,
) -> Result<(), BackendError> {
for op in &body.ops {
if let vyre_lower::KernelOpKind::Trap { tag } = &op.kind {
if seen.insert(tag.clone()) {
let code = out
.len()
.checked_add(1)
.and_then(|value| u32::try_from(value).ok())
.ok_or_else(|| {
BackendError::new(
"kernel descriptor trap tag code overflowed u32. Fix: split trap-tag metadata before pipeline creation.",
)
})?;
out.push(TrapTag {
code,
tag: Arc::from(tag.as_ref()),
});
}
}
}
for child in &body.child_bodies {
walk(child, seen, out)?;
}
Ok(())
}
let op_count = recursive_op_count(&descriptor.body)?;
let mut seen = FxHashSet::default();
vyre_foundation::allocation::try_reserve_hash_set_to_capacity(&mut seen, op_count).map_err(|source| {
BackendError::new(format!(
"trap-tag dedup allocation failed for {op_count} descriptor ops: {source}. Fix: split nested kernel bodies before descriptor metadata extraction."
))
})?;
let mut out = Vec::new();
vyre_driver::allocation::try_reserve_vec_to_capacity(&mut out, op_count).map_err(|source| {
BackendError::new(format!(
"trap-tag output allocation failed for {op_count} descriptor ops: {source}. Fix: split nested kernel bodies before descriptor metadata extraction."
))
})?;
walk(&descriptor.body, &mut seen, &mut out)?;
Ok(out)
}
#[cfg(test)]
mod tests {
#[test]
fn descriptor_metadata_source_has_no_release_path_panic_or_infallible_capacity() {
let source = include_str!("descriptor_metadata.rs");
let production = source
.split("#[cfg(test)]")
.next()
.expect("Fix: descriptor metadata production source must precede tests");
assert!(
!production.contains(concat!("panic", "!("))
&& !production.contains(".expect(")
&& !production.contains("Vec::with_capacity")
&& !production.contains("SmallVec::with_capacity")
&& !production.contains("with_capacity_and_hasher"),
"Fix: WGPU descriptor metadata extraction must reject oversized lowered kernels with BackendError instead of aborting."
);
assert!(
production.contains("try_reserve_vec_to_capacity")
&& production.contains("try_reserve_hash_set_to_capacity")
&& production.contains("Result<Vec<BufferBindingInfo>, BackendError>")
&& production.contains("Result<Arc<[Arc<wgpu::BindGroupLayout>]>, BackendError>")
&& production.contains("Result<Vec<TrapTag>, BackendError>"),
"Fix: WGPU descriptor metadata allocation and overflow paths must stay fallible at the pipeline boundary."
);
}
}