use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
};
use snafu::prelude::*;
use wgsl_rs_ir as ir;
use crate::Source;
#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display(
"Unable to resolve template '{template_name}' for type args {:?}; available templates: \
{:?}",
mangled_type_args,
available
))]
TemplateResolution {
template_name: String,
mangled_type_args: Vec<String>,
available: Vec<String>,
},
#[snafu(display(
"{module}::bind_group_{group}: missing binding resource for declared name {name:?}"
))]
MissingBinding {
module: &'static str,
group: u32,
name: String,
},
#[snafu(display(
"{module}::bind_group_{group}: resource provided for unknown binding name {name:?} \
(declared names: {declared:?})"
))]
UnknownBindingName {
module: &'static str,
group: u32,
name: String,
declared: Vec<String>,
},
#[snafu(display(
"{module}::bind_group_{group}: binding name {name:?} appears more than once in resources"
))]
DuplicateBindingName {
module: &'static str,
group: u32,
name: String,
},
#[snafu(display(
"{module}: no bind group @group({group}) declared; available groups: {available:?}"
))]
NoSuchBindGroup {
module: &'static str,
group: u32,
available: Vec<u32>,
},
}
impl From<crate::SourceError<'_>> for Error {
fn from(err: crate::SourceError<'_>) -> Self {
match err {
crate::SourceError::TemplateWgsl {
uninstantiated_source,
} => Error::TemplateResolution {
template_name: format!("<source {}>", uninstantiated_source.name),
mangled_type_args: uninstantiated_source
.module_type_params
.iter()
.map(|s| (*s).to_string())
.collect(),
available: Vec::new(),
},
crate::SourceError::TemplateNotFound {
template_name,
mangled_type_args,
available_templates,
} => Error::TemplateResolution {
template_name,
mangled_type_args,
available: available_templates,
},
crate::SourceError::AmbiguousTemplate {
template_name,
mangled_type_args,
matching_source_names,
available_templates: _,
} => Error::TemplateResolution {
template_name,
mangled_type_args,
available: matching_source_names,
},
}
}
}
#[derive(Clone, Debug)]
pub struct WgpuLinkage {
pub module_label: &'static str,
ir: ir::Module,
pub bind_groups: BTreeMap<u32, BindGroupInfo>,
bind_group_layouts: HashMap<u32, wgpu::BindGroupLayout>,
pipeline_layout: Option<wgpu::PipelineLayout>,
pub vertex_entries: Vec<EntryPointInfo>,
pub fragment_entries: Vec<EntryPointInfo>,
pub compute_entries: Vec<ComputeEntryInfo>,
pub buffers: Vec<BufferDescriptorInfo>,
}
impl core::fmt::Display for WgpuLinkage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let WgpuLinkage {
module_label,
ir,
bind_groups,
bind_group_layouts,
pipeline_layout,
vertex_entries,
fragment_entries,
compute_entries,
buffers,
} = self;
writeln!(
f,
"linkage for '{module_label}' ({} IR items)",
ir.items.len()
)?;
writeln!(f, " entry points:")?;
if vertex_entries.is_empty() && fragment_entries.is_empty() && compute_entries.is_empty() {
writeln!(f, " (none)")?;
}
for e in vertex_entries {
writeln!(f, " vertex: {}", e.name)?;
}
for e in fragment_entries {
writeln!(f, " fragment: {}", e.name)?;
}
for e in compute_entries {
let (x, y, z) = e.workgroup_size;
writeln!(f, " compute: {} @workgroup_size({x}, {y}, {z})", e.name)?;
}
writeln!(f, " bind groups:")?;
if bind_groups.is_empty() {
writeln!(f, " (none)")?;
}
for (group, info) in bind_groups {
writeln!(f, " @group({group}) {}", info.label)?;
for meta in &info.bindings {
writeln!(
f,
" @binding({}) {} ({})",
meta.binding,
meta.name,
binding_kind_label(&meta.kind)
)?;
}
}
writeln!(f, " buffers:")?;
if buffers.is_empty() {
writeln!(f, " (none)")?;
}
for b in buffers {
let size = if b.size == 0 {
"runtime-sized".to_string()
} else {
format!("{} bytes", b.size)
};
writeln!(
f,
" @group({}) @binding({}): {} ({}, {size}, {ty})",
b.group,
b.binding,
b.binding_name,
buffer_kind_label(&b.kind),
ty = b.ty
)?;
}
write!(
f,
" cache: {} of {} bind group layouts built, pipeline layout {}",
bind_group_layouts.len(),
bind_groups.len(),
if pipeline_layout.is_some() {
"built"
} else {
"not built"
}
)?;
Ok(())
}
}
fn binding_kind_label(kind: &BindingKind) -> &'static str {
match kind {
BindingKind::Uniform => "uniform",
BindingKind::Storage { read_only: true } => "storage, read-only",
BindingKind::Storage { read_only: false } => "storage, read-write",
BindingKind::Sampler { comparison: true } => "comparison sampler",
BindingKind::Sampler { comparison: false } => "sampler",
BindingKind::Texture => "texture",
BindingKind::DepthTexture => "depth texture",
BindingKind::StorageTexture { read_write: true } => "storage texture, read-write",
BindingKind::StorageTexture { read_write: false } => "storage texture",
}
}
fn buffer_kind_label(kind: &BufferKind) -> &'static str {
match kind {
BufferKind::Uniform => "uniform",
BufferKind::Storage { read_only: true } => "storage, read-only",
BufferKind::Storage { read_only: false } => "storage, read-write",
}
}
impl WgpuLinkage {
pub fn bind_group(&self, group: u32) -> Option<&BindGroupInfo> {
self.bind_groups.get(&group)
}
pub fn buffer(&self, name: &str) -> Option<&BufferDescriptorInfo> {
self.buffers.iter().find(|b| b.binding_name == name)
}
pub fn take_buffer(&mut self, name: &str) -> Option<BufferDescriptorInfo> {
let mut info = None;
self.buffers.retain(|bi| {
if bi.binding_name == name {
info = Some(bi.clone());
false
} else {
true
}
});
info
}
pub fn wgsl_source(&self) -> String {
ir::render_module(&self.ir)
}
pub fn shader_module_descriptor(&self) -> wgpu::ShaderModuleDescriptor<'static> {
wgpu::ShaderModuleDescriptor {
label: Some(self.module_label),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(self.wgsl_source())),
}
}
pub fn shader_module(&self, device: &wgpu::Device) -> wgpu::ShaderModule {
device.create_shader_module(self.shader_module_descriptor())
}
pub fn vertex_entry(&self, name: &str) -> Option<&EntryPointInfo> {
self.vertex_entries.iter().find(|e| e.name == name)
}
pub fn fragment_entry(&self, name: &str) -> Option<&EntryPointInfo> {
self.fragment_entries.iter().find(|e| e.name == name)
}
pub fn compute_entry(&self, name: &str) -> Option<&ComputeEntryInfo> {
self.compute_entries.iter().find(|e| e.name == name)
}
pub fn bind_group_layout(
&mut self,
group: u32,
device: &wgpu::Device,
) -> Option<wgpu::BindGroupLayout> {
if let Some(layout) = self.bind_group_layouts.get(&group) {
return Some(layout.clone());
}
let info = self.bind_groups.get(&group)?;
let layout = info.layout(device);
self.bind_group_layouts.insert(group, layout.clone());
Some(layout)
}
pub fn create_bind_group_named<'a>(
&mut self,
group: u32,
device: &wgpu::Device,
resources: &[(&str, wgpu::BindingResource<'a>)],
) -> Result<wgpu::BindGroup, Error> {
if !self.bind_group_layouts.contains_key(&group) {
let Some(info) = self.bind_groups.get(&group) else {
let mut available: Vec<u32> = self.bind_groups.keys().copied().collect();
available.sort_unstable();
return NoSuchBindGroupSnafu {
module: self.module_label,
group,
available,
}
.fail();
};
self.bind_group_layouts.insert(group, info.layout(device));
}
let info = self
.bind_groups
.get(&group)
.expect("just inserted or found");
let layout = self
.bind_group_layouts
.get(&group)
.expect("just inserted or found");
info.create_named(self.module_label, device, layout, resources)
}
pub fn pipeline_layout(
&mut self,
device: &wgpu::Device,
label: Option<&str>,
) -> wgpu::PipelineLayout {
if let Some(layout) = &self.pipeline_layout {
return layout.clone();
}
for (&group, info) in &self.bind_groups {
self.bind_group_layouts
.entry(group)
.or_insert_with(|| info.layout(device));
}
let max_group = self.bind_group_layouts.keys().copied().max().unwrap_or(0);
let mut bg_layouts: Vec<Option<&wgpu::BindGroupLayout>> = (0..=max_group)
.map(|g| self.bind_group_layouts.get(&g))
.collect();
while bg_layouts.last() == Some(&None) {
bg_layouts.pop();
}
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label,
bind_group_layouts: &bg_layouts,
immediate_size: 0,
});
self.pipeline_layout = Some(layout.clone());
layout
}
}
pub trait IrModuleExt {
fn generate_linkage(&self) -> WgpuLinkage;
#[cfg(feature = "validation")]
fn validate(&self) -> Result<(), String>;
}
impl IrModuleExt for wgsl_rs_ir::Module {
fn generate_linkage(&self) -> WgpuLinkage {
analyze_ir_module(self.clone())
}
#[cfg(feature = "validation")]
fn validate(&self) -> Result<(), String> {
crate::validate_wgsl_source(&self.wgsl_source())
}
}
#[derive(Clone, Debug)]
pub struct BindGroupInfo {
pub group: u32,
pub entries: Vec<wgpu::BindGroupLayoutEntry>,
pub bindings: Vec<BindingMeta>,
pub label: String,
}
#[derive(Clone, Debug)]
pub struct BindingMeta {
pub name: String,
pub binding: u32,
pub kind: BindingKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BindingKind {
Uniform,
Storage { read_only: bool },
Sampler { comparison: bool },
Texture,
DepthTexture,
StorageTexture { read_write: bool },
}
impl BindGroupInfo {
pub fn layout_descriptor<'a>(&'a self) -> wgpu::BindGroupLayoutDescriptor<'a> {
wgpu::BindGroupLayoutDescriptor {
label: Some(&self.label),
entries: &self.entries,
}
}
pub fn layout(&self, device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&self.layout_descriptor())
}
pub fn create<'a>(
&self,
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
resources: &[wgpu::BindingResource<'a>],
) -> wgpu::BindGroup {
assert_eq!(
resources.len(),
self.entries.len(),
"{}: expected {} binding resource(s), got {}",
self.label,
self.entries.len(),
resources.len(),
);
let entries: Vec<wgpu::BindGroupEntry> = self
.entries
.iter()
.zip(resources.iter())
.map(|(layout_entry, resource)| wgpu::BindGroupEntry {
binding: layout_entry.binding,
resource: resource.clone(),
})
.collect();
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&self.label),
layout,
entries: &entries,
})
}
pub fn create_named<'a>(
&self,
module_label: &'static str,
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
resources: &[(&str, wgpu::BindingResource<'a>)],
) -> Result<wgpu::BindGroup, Error> {
use std::collections::HashMap;
let mut by_name: HashMap<&str, wgpu::BindingResource<'a>> =
HashMap::with_capacity(resources.len());
for &(name, ref resource) in resources {
if by_name.insert(name, resource.clone()).is_some() {
return DuplicateBindingNameSnafu {
module: module_label,
group: self.group,
name: name.to_string(),
}
.fail();
}
}
for binding in &self.bindings {
if !by_name.contains_key(binding.name.as_str()) {
return MissingBindingSnafu {
module: module_label,
group: self.group,
name: binding.name.clone(),
}
.fail();
}
}
let mut hit: HashMap<&str, ()> = HashMap::with_capacity(by_name.len());
let mut entries: Vec<wgpu::BindGroupEntry> = Vec::with_capacity(self.bindings.len());
for (layout_entry, binding) in self.entries.iter().zip(self.bindings.iter()) {
let resource = by_name
.get(binding.name.as_str())
.expect("checked above")
.clone();
hit.insert(binding.name.as_str(), ());
entries.push(wgpu::BindGroupEntry {
binding: layout_entry.binding,
resource,
});
}
for name in by_name.keys() {
if !hit.contains_key(name) {
let declared: Vec<String> = self.bindings.iter().map(|b| b.name.clone()).collect();
return UnknownBindingNameSnafu {
module: module_label,
group: self.group,
name: (*name).to_string(),
declared,
}
.fail();
}
}
Ok(device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&self.label),
layout,
entries: &entries,
}))
}
}
#[derive(Clone, Debug)]
pub struct EntryPointInfo {
pub name: Cow<'static, str>,
}
impl EntryPointInfo {
pub fn vertex_state<'a>(&'a self, module: &'a wgpu::ShaderModule) -> wgpu::VertexState<'a> {
wgpu::VertexState {
module,
entry_point: Some(&self.name),
buffers: &[],
compilation_options: Default::default(),
}
}
pub fn fragment_state<'a>(
&'a self,
module: &'a wgpu::ShaderModule,
targets: &'a [Option<wgpu::ColorTargetState>],
) -> wgpu::FragmentState<'a> {
wgpu::FragmentState {
module,
entry_point: Some(&self.name),
targets,
compilation_options: Default::default(),
}
}
}
#[derive(Clone, Debug)]
pub struct ComputeEntryInfo {
pub name: Cow<'static, str>,
pub workgroup_size: (u32, u32, u32),
pub label: String,
}
#[derive(Clone, Debug)]
pub struct BufferDescriptorInfo {
pub binding_name: String,
pub group: u32,
pub binding: u32,
pub kind: BufferKind,
pub size: u64,
pub usage: wgpu::BufferUsages,
pub ty: crate::ir::Type,
}
impl BufferDescriptorInfo {
pub fn descriptor(&self) -> wgpu::BufferDescriptor<'_> {
wgpu::BufferDescriptor {
label: Some(&self.binding_name),
size: self.size,
usage: self.usage,
mapped_at_creation: false,
}
}
pub fn create_buffer(&self, device: &wgpu::Device) -> wgpu::Buffer {
device.create_buffer(&self.descriptor())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BufferKind {
Uniform,
Storage { read_only: bool },
}
fn stage_of(fn_attrs: &ir::FnAttrs) -> Option<wgpu::ShaderStages> {
match fn_attrs {
ir::FnAttrs::Vertex => Some(wgpu::ShaderStages::VERTEX),
ir::FnAttrs::Fragment => Some(wgpu::ShaderStages::FRAGMENT),
ir::FnAttrs::Compute { .. } => Some(wgpu::ShaderStages::COMPUTE),
ir::FnAttrs::None => None,
}
}
#[derive(Default)]
struct FnRefs {
idents: std::collections::HashSet<String>,
callees: std::collections::HashSet<String>,
}
fn record_fn_idents(
f: &ir::ItemFn,
name: String,
fn_names: &mut std::collections::HashSet<String>,
fn_refs: &mut std::collections::HashMap<String, FnRefs>,
) {
let mut refs = FnRefs::default();
collect_refs_in_block(&mut refs, &f.block);
fn_names.insert(name.clone());
fn_refs.insert(name, refs);
}
pub fn analyze_ir_module(mut ir_module: wgsl_rs_ir::Module) -> WgpuLinkage {
let module_label = ir_module.name;
ir::deshadow_module(&mut ir_module);
ir::suffix_module(&mut ir_module);
let mut linkage = WgpuLinkage {
module_label,
ir: ir_module,
bind_groups: BTreeMap::new(),
bind_group_layouts: HashMap::new(),
pipeline_layout: None,
vertex_entries: Vec::new(),
fragment_entries: Vec::new(),
compute_entries: Vec::new(),
buffers: Vec::new(),
};
let mut binding_names: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut fn_names: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut fn_refs: std::collections::HashMap<String, FnRefs> = std::collections::HashMap::new();
let mut entry_points: Vec<(String, wgpu::ShaderStages)> = Vec::new();
for item in &linkage.ir.items {
match item {
ir::Item::Uniform(u) => {
binding_names.insert(u.name.clone());
}
ir::Item::Storage(s) => {
binding_names.insert(s.name.clone());
}
ir::Item::Sampler(s) => {
binding_names.insert(s.name.clone());
}
ir::Item::Texture(t) => {
binding_names.insert(t.name.clone());
}
ir::Item::Fn(f) => {
record_fn_idents(f, f.name.to_string(), &mut fn_names, &mut fn_refs);
if let Some(stage) = stage_of(&f.fn_attrs) {
entry_points.push((f.name.to_string(), stage));
}
}
ir::Item::Impl(imp) => {
for impl_item in &imp.items {
let ir::ImplItem::Fn(f) = impl_item else {
continue;
};
let mangled = ir::mangle::mangle(&[&imp.self_ty, &f.name]);
record_fn_idents(f, mangled.clone(), &mut fn_names, &mut fn_refs);
if let Some(stage) = stage_of(&f.fn_attrs) {
entry_points.push((mangled, stage));
}
}
}
_ => {}
}
}
let mut referenced: std::collections::HashMap<String, wgpu::ShaderStages> =
std::collections::HashMap::new();
for (entry_name, stage) in &entry_points {
let mut visited: std::collections::HashSet<&String> = std::collections::HashSet::new();
let mut stack: Vec<&String> = vec![entry_name];
while let Some(fname) = stack.pop() {
if !visited.insert(fname) {
continue;
}
let Some(refs) = fn_refs.get(fname) else {
continue;
};
for name in &refs.idents {
if binding_names.contains(name) {
referenced
.entry(name.clone())
.and_modify(|s| *s |= *stage)
.or_insert(*stage);
}
}
for callee in &refs.callees {
if fn_names.contains(callee) {
stack.push(callee);
}
}
}
}
for item in &linkage.ir.items {
match item {
ir::Item::Uniform(u) => {
let size = type_byte_size_or_zero(&u.ty, &linkage.ir);
let entry = wgpu::BindGroupLayoutEntry {
binding: u.binding,
visibility: visibility_for(&referenced, &u.name),
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
};
linkage
.bind_groups
.entry(u.group)
.or_insert_with(|| BindGroupInfo {
group: u.group,
entries: Vec::new(),
bindings: Vec::new(),
label: format!("{}::bind_group_{}", linkage.module_label, u.group),
})
.entries
.push(entry);
linkage
.bind_groups
.get_mut(&u.group)
.unwrap()
.bindings
.push(BindingMeta {
name: u.name.clone(),
binding: u.binding,
kind: BindingKind::Uniform,
});
linkage.buffers.push(BufferDescriptorInfo {
binding_name: u.name.clone(),
group: u.group,
binding: u.binding,
kind: BufferKind::Uniform,
size,
usage: wgpu::BufferUsages::UNIFORM
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
ty: u.ty.clone(),
});
}
ir::Item::Storage(s) => {
let read_only = matches!(s.access, ir::StorageAccess::Read);
let size = type_byte_size_or_zero(&s.ty, &linkage.ir);
let entry = wgpu::BindGroupLayoutEntry {
binding: s.binding,
visibility: visibility_for(&referenced, &s.name),
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
};
linkage
.bind_groups
.entry(s.group)
.or_insert_with(|| BindGroupInfo {
group: s.group,
entries: Vec::new(),
bindings: Vec::new(),
label: format!("{}::bind_group_{}", linkage.module_label, s.group),
})
.entries
.push(entry);
linkage
.bind_groups
.get_mut(&s.group)
.unwrap()
.bindings
.push(BindingMeta {
name: s.name.clone(),
binding: s.binding,
kind: BindingKind::Storage { read_only },
});
linkage.buffers.push(BufferDescriptorInfo {
binding_name: s.name.clone(),
group: s.group,
binding: s.binding,
kind: BufferKind::Storage { read_only },
size,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
ty: s.ty.clone(),
});
}
ir::Item::Sampler(s) => {
let comparison = matches!(s.ty, ir::Type::SamplerComparison);
let sampler_ty = if comparison {
wgpu::SamplerBindingType::Comparison
} else if !s.filterable {
wgpu::SamplerBindingType::NonFiltering
} else {
wgpu::SamplerBindingType::Filtering
};
let entry = wgpu::BindGroupLayoutEntry {
binding: s.binding,
visibility: visibility_for(&referenced, &s.name),
ty: wgpu::BindingType::Sampler(sampler_ty),
count: None,
};
linkage
.bind_groups
.entry(s.group)
.or_insert_with(|| BindGroupInfo {
group: s.group,
entries: Vec::new(),
bindings: Vec::new(),
label: format!("{}::bind_group_{}", linkage.module_label, s.group),
})
.entries
.push(entry);
linkage
.bind_groups
.get_mut(&s.group)
.unwrap()
.bindings
.push(BindingMeta {
name: s.name.clone(),
binding: s.binding,
kind: BindingKind::Sampler { comparison },
});
}
ir::Item::Texture(t) => {
let (mut entry, kind) = texture_layout_entry(t);
entry.visibility = visibility_for(&referenced, &t.name);
linkage
.bind_groups
.entry(t.group)
.or_insert_with(|| BindGroupInfo {
group: t.group,
entries: Vec::new(),
bindings: Vec::new(),
label: format!("{}::bind_group_{}", linkage.module_label, t.group),
})
.entries
.push(entry);
linkage
.bind_groups
.get_mut(&t.group)
.unwrap()
.bindings
.push(BindingMeta {
name: t.name.clone(),
binding: t.binding,
kind,
});
}
ir::Item::Fn(f) => match &f.fn_attrs {
ir::FnAttrs::Vertex => {
linkage.vertex_entries.push(EntryPointInfo {
name: f.name.clone(),
});
}
ir::FnAttrs::Fragment => {
linkage.fragment_entries.push(EntryPointInfo {
name: f.name.clone(),
});
}
ir::FnAttrs::Compute { workgroup_size } => {
let x = workgroup_size.x;
let y = workgroup_size.y.unwrap_or(1);
let z = workgroup_size.z.unwrap_or(1);
linkage.compute_entries.push(ComputeEntryInfo {
name: f.name.clone(),
workgroup_size: (x, y, z),
label: format!("{}::{}", linkage.module_label, f.name),
});
}
ir::FnAttrs::None => {}
},
ir::Item::Struct(_)
| ir::Item::Impl(_)
| ir::Item::Enum(_)
| ir::Item::Const(_)
| ir::Item::Workgroup(_) => {}
}
}
for bg in linkage.bind_groups.values_mut() {
let mut paired: Vec<_> = bg.entries.drain(..).zip(bg.bindings.drain(..)).collect();
paired.sort_by_key(|(e, _m)| e.binding);
bg.entries = paired.iter().map(|(e, _)| *e).collect();
bg.bindings = paired.into_iter().map(|(_, m)| m).collect();
}
linkage
}
fn visibility_for(
referenced: &std::collections::HashMap<String, wgpu::ShaderStages>,
name: &str,
) -> wgpu::ShaderStages {
referenced
.get(name)
.copied()
.unwrap_or(wgpu::ShaderStages::COMPUTE)
}
fn collect_refs_in_block(refs: &mut FnRefs, block: &ir::Block) {
for stmt in &block.stmts {
collect_refs_in_stmt(refs, stmt);
}
}
fn collect_refs_in_stmt(refs: &mut FnRefs, stmt: &ir::Stmt) {
match stmt {
ir::Stmt::Local(l) => {
if let Some(init) = &l.init {
collect_refs_in_expr(refs, init);
}
}
ir::Stmt::Const(c) => {
collect_refs_in_expr(refs, &c.expr);
}
ir::Stmt::Assignment { lhs, rhs } => {
collect_refs_in_expr(refs, lhs);
collect_refs_in_expr(refs, rhs);
}
ir::Stmt::CompoundAssignment { lhs, rhs, .. } => {
collect_refs_in_expr(refs, lhs);
collect_refs_in_expr(refs, rhs);
}
ir::Stmt::While { condition, body } => {
collect_refs_in_expr(refs, condition);
collect_refs_in_block(refs, body);
}
ir::Stmt::Loop { body } => {
collect_refs_in_block(refs, body);
}
ir::Stmt::Expr { expr, .. } => {
collect_refs_in_expr(refs, expr);
}
ir::Stmt::If(s) => {
collect_refs_in_expr(refs, &s.condition);
collect_refs_in_block(refs, &s.then_block);
if let Some(else_branch) = &s.else_branch {
match else_branch {
ir::ElseBranch::Block(b) => collect_refs_in_block(refs, b),
ir::ElseBranch::If(i) => {
collect_refs_in_expr(refs, &i.condition);
collect_refs_in_block(refs, &i.then_block);
if let Some(else_branch) = &i.else_branch {
match else_branch {
ir::ElseBranch::Block(b) => collect_refs_in_block(refs, b),
ir::ElseBranch::If(_) => {
}
}
}
}
}
}
}
ir::Stmt::Return(Some(e)) => collect_refs_in_expr(refs, e),
ir::Stmt::For(f) => {
collect_refs_in_expr(refs, &f.from);
collect_refs_in_expr(refs, &f.to);
collect_refs_in_block(refs, &f.body);
}
ir::Stmt::Switch(s) => {
collect_refs_in_expr(refs, &s.selector);
for arm in &s.arms {
for sel in &arm.selectors {
match sel {
ir::CaseSelector::Literal(_) => {}
ir::CaseSelector::Expr(e) => collect_refs_in_expr(refs, e),
ir::CaseSelector::Default => {}
}
}
collect_refs_in_block(refs, &arm.body);
}
}
ir::Stmt::Block(b) => collect_refs_in_block(refs, b),
ir::Stmt::SlabCopy {
src,
src_offset,
dest,
dest_offset,
size,
} => {
collect_refs_in_expr(refs, src);
collect_refs_in_expr(refs, src_offset);
collect_refs_in_expr(refs, dest);
collect_refs_in_expr(refs, dest_offset);
collect_refs_in_expr(refs, size);
}
ir::Stmt::Break
| ir::Stmt::Continue
| ir::Stmt::Discard
| ir::Stmt::Return(None)
| ir::Stmt::Macro { .. } => {}
}
}
fn collect_refs_in_expr(refs: &mut FnRefs, expr: &ir::Expr) {
match expr {
ir::Expr::Ident(name) => {
refs.idents.insert(name.clone());
}
ir::Expr::Lit(_) => {}
ir::Expr::Array { elems } => {
for e in elems {
collect_refs_in_expr(refs, e);
}
}
ir::Expr::Paren(e) => collect_refs_in_expr(refs, e),
ir::Expr::Binary { lhs, rhs, .. } => {
collect_refs_in_expr(refs, lhs);
collect_refs_in_expr(refs, rhs);
}
ir::Expr::Unary { expr, .. } => collect_refs_in_expr(refs, expr),
ir::Expr::ArrayIndexing { lhs, index } => {
collect_refs_in_expr(refs, lhs);
collect_refs_in_expr(refs, index);
}
ir::Expr::Swizzle { lhs, params, .. } => {
collect_refs_in_expr(refs, lhs);
if let Some(args) = params {
for a in args {
collect_refs_in_expr(refs, a);
}
}
}
ir::Expr::Cast { lhs, .. } => collect_refs_in_expr(refs, lhs),
ir::Expr::FnCall { path, params, .. } => {
match path {
ir::FnPath::Ident(name) => {
refs.callees.insert(name.clone());
}
ir::FnPath::TypeMethod { ty, method } => {
refs.callees.insert(ir::mangle::mangle(&[ty, method]));
}
}
for p in params {
collect_refs_in_expr(refs, p);
}
}
ir::Expr::Struct { fields, .. } => {
for fv in fields {
collect_refs_in_expr(refs, &fv.expr);
}
}
ir::Expr::FieldAccess { base, .. } => collect_refs_in_expr(refs, base),
ir::Expr::TypePath { .. } => {}
ir::Expr::Reference(e) => collect_refs_in_expr(refs, e),
ir::Expr::ZeroValueArray { len, .. } => collect_refs_in_expr(refs, len),
}
}
pub fn analyze_wgsl_module(wgsl_source: &Source) -> Result<WgpuLinkage, Error> {
if wgsl_source.is_template() {
return Err(crate::SourceError::TemplateWgsl {
uninstantiated_source: wgsl_source,
}
.into());
}
let ir_module = assemble_ir(wgsl_source)?;
Ok(analyze_ir_module(ir_module))
}
pub(crate) fn assemble_ir(wgsl_source: &Source) -> Result<ir::Module, Error> {
let mut visited: std::collections::HashSet<u64> = std::collections::HashSet::new();
let mut seen: std::collections::HashSet<(u64, String, Vec<String>)> =
std::collections::HashSet::new();
let mut items: Vec<ir::Item> = Vec::new();
collect_items(wgsl_source, &mut visited, &mut seen, &mut items, None)?;
Ok(ir::Module {
name: wgsl_source.name,
items,
attrs: Vec::new(),
})
}
fn collect_items(
wgsl_source: &Source,
visited: &mut std::collections::HashSet<u64>,
seen: &mut std::collections::HashSet<(u64, String, Vec<String>)>,
out: &mut Vec<ir::Item>,
subst: Option<&std::collections::HashMap<String, ir::Type>>,
) -> Result<(), Error> {
for m in wgsl_source.imports {
if visited.insert(m.id) {
collect_items(m, visited, seen, out, None)?;
}
}
let mut ir_module = (wgsl_source.ir_constructor)();
if let Some(s) = subst {
ir::substitute_types(&mut ir_module, s);
}
out.extend(ir_module.items);
for inst in wgsl_source.instantiations {
let mangled: Vec<String> = inst
.mangled_type_args
.iter()
.map(|s| (*s).to_string())
.collect();
let type_args = (inst.type_args_constructor)();
collect_instantiation(
inst.modules,
inst.template_name,
&mangled,
&type_args,
out,
seen,
)?;
}
Ok(())
}
fn collect_instantiation(
sources: &[&Source],
template_name: &str,
mangled_type_args: &[String],
type_args: &[ir::Type],
out: &mut Vec<ir::Item>,
seen: &mut std::collections::HashSet<(u64, String, Vec<String>)>,
) -> Result<(), Error> {
let available: Vec<String> = sources
.iter()
.copied()
.flat_map(|m| m.templates.iter().map(|t| t.name.to_string()))
.collect();
let mut matching: Vec<&Source> = sources
.iter()
.copied()
.filter(|m| m.templates.iter().any(|t| t.name == template_name))
.collect();
snafu::ensure!(
!matching.is_empty(),
TemplateResolutionSnafu {
template_name: template_name.to_string(),
mangled_type_args: mangled_type_args.to_vec(),
available,
}
);
if matching.len() > 1 {
let names: Vec<String> = matching.iter().map(|m| m.name.to_string()).collect();
return TemplateResolutionSnafu {
template_name: template_name.to_string(),
mangled_type_args: mangled_type_args.to_vec(),
available: names,
}
.fail();
}
let source = matching
.pop()
.expect("matching is non-empty after the check above");
let Some(template) = source.templates.iter().find(|t| t.name == template_name) else {
unreachable!(
"resolved source '{}' has no template '{}'; available: {:?}",
source.name, template_name, available
);
};
let key = (
source.id,
template_name.to_string(),
mangled_type_args.to_vec(),
);
snafu::ensure!(
seen.insert(key),
TemplateResolutionSnafu {
template_name: template_name.to_owned(),
mangled_type_args: mangled_type_args.to_vec(),
available
}
);
for dep in template.dependencies {
let dep_mangled: Vec<String> = dep
.type_param_mapping
.iter()
.map(|&idx| mangled_type_args[idx].clone())
.collect();
let dep_args: Vec<ir::Type> = dep
.type_param_mapping
.iter()
.map(|&idx| type_args[idx].clone())
.collect();
collect_instantiation(&[source], dep.callee, &dep_mangled, &dep_args, out, seen)?;
}
let mut subst: std::collections::HashMap<String, ir::Type> = std::collections::HashMap::new();
for (param, arg) in template.type_params.iter().zip(type_args.iter()) {
subst.insert((*param).to_string(), arg.clone());
}
let mut items = (template.ir_constructor)();
ir::substitute_items(&mut items, &subst);
let instance_name = if mangled_type_args.is_empty() {
template.name.to_string()
} else {
let mut components: Vec<&str> = Vec::with_capacity(1 + mangled_type_args.len());
components.push(template.name);
for s in mangled_type_args {
components.push(s.as_str());
}
ir::mangle(&components)
};
if instance_name != template.name {
ir::rename_items(&mut items, template.name, &instance_name);
}
out.extend(items);
Ok(())
}
fn texture_layout_entry(item: &ir::ItemTexture) -> (wgpu::BindGroupLayoutEntry, BindingKind) {
let view_dimension = match &item.ty {
ir::Type::Texture { kind, .. } => texture_kind_view_dimension(*kind),
ir::Type::TextureDepth { kind } => texture_depth_view_dimension(*kind),
ir::Type::TextureStorage { kind, .. } => texture_storage_kind_view_dimension(*kind),
_ => unreachable!("ItemTexture validated to be a texture type"),
};
let multisampled = match &item.ty {
ir::Type::Texture { kind, .. } => matches!(kind, ir::TextureKind::TextureMultisampled2D),
ir::Type::TextureDepth { kind } => {
matches!(kind, ir::TextureDepthKind::DepthMultisampled2D)
}
ir::Type::TextureStorage { .. } => false,
_ => unreachable!(),
};
match &item.ty {
ir::Type::Texture { sampled_type, .. } => {
let sample = match sampled_type {
ir::ScalarType::F32 => wgpu::TextureSampleType::Float {
filterable: item.filterable,
},
ir::ScalarType::I32 => wgpu::TextureSampleType::Sint,
ir::ScalarType::U32 => wgpu::TextureSampleType::Uint,
ir::ScalarType::Bool => unreachable!("textures can't be bool-sampled"),
};
(
wgpu::BindGroupLayoutEntry {
binding: item.binding,
visibility: wgpu::ShaderStages::all(),
ty: wgpu::BindingType::Texture {
sample_type: sample,
view_dimension,
multisampled,
},
count: None,
},
BindingKind::Texture,
)
}
ir::Type::TextureDepth { .. } => (
wgpu::BindGroupLayoutEntry {
binding: item.binding,
visibility: wgpu::ShaderStages::all(),
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension,
multisampled,
},
count: None,
},
BindingKind::DepthTexture,
),
ir::Type::TextureStorage { format, access, .. } => {
let wgpu_format = texel_format_to_wgpu(*format);
let wgpu_access = match access {
ir::StorageTextureAccess::Read => wgpu::StorageTextureAccess::ReadOnly,
ir::StorageTextureAccess::Write => wgpu::StorageTextureAccess::WriteOnly,
ir::StorageTextureAccess::ReadWrite => wgpu::StorageTextureAccess::ReadWrite,
};
let read_write = matches!(access, ir::StorageTextureAccess::ReadWrite);
(
wgpu::BindGroupLayoutEntry {
binding: item.binding,
visibility: wgpu::ShaderStages::all(),
ty: wgpu::BindingType::StorageTexture {
access: wgpu_access,
format: wgpu_format,
view_dimension,
},
count: None,
},
BindingKind::StorageTexture { read_write },
)
}
_ => unreachable!(),
}
}
fn texture_storage_kind_view_dimension(kind: ir::TextureStorageKind) -> wgpu::TextureViewDimension {
match kind {
ir::TextureStorageKind::Storage1D => wgpu::TextureViewDimension::D1,
ir::TextureStorageKind::Storage2D => wgpu::TextureViewDimension::D2,
ir::TextureStorageKind::Storage2DArray => wgpu::TextureViewDimension::D2Array,
ir::TextureStorageKind::Storage3D => wgpu::TextureViewDimension::D3,
}
}
fn texel_format_to_wgpu(f: ir::TexelFormat) -> wgpu::TextureFormat {
use ir::TexelFormat::*;
match f {
Rgba8unorm => wgpu::TextureFormat::Rgba8Unorm,
Rgba8snorm => wgpu::TextureFormat::Rgba8Snorm,
Rgba8uint => wgpu::TextureFormat::Rgba8Uint,
Rgba8sint => wgpu::TextureFormat::Rgba8Sint,
Rgba16uint => wgpu::TextureFormat::Rgba16Uint,
Rgba16sint => wgpu::TextureFormat::Rgba16Sint,
Rgba16float => wgpu::TextureFormat::Rgba16Float,
R32uint => wgpu::TextureFormat::R32Uint,
R32sint => wgpu::TextureFormat::R32Sint,
R32float => wgpu::TextureFormat::R32Float,
Rg32uint => wgpu::TextureFormat::Rg32Uint,
Rg32sint => wgpu::TextureFormat::Rg32Sint,
Rg32float => wgpu::TextureFormat::Rg32Float,
Rgba32uint => wgpu::TextureFormat::Rgba32Uint,
Rgba32sint => wgpu::TextureFormat::Rgba32Sint,
Rgba32float => wgpu::TextureFormat::Rgba32Float,
Bgra8unorm => wgpu::TextureFormat::Bgra8Unorm,
Rgba16unorm => wgpu::TextureFormat::Rgba16Unorm,
Rgba16snorm => wgpu::TextureFormat::Rgba16Snorm,
Rg8unorm => wgpu::TextureFormat::Rg8Unorm,
Rg8snorm => wgpu::TextureFormat::Rg8Snorm,
Rg8uint => wgpu::TextureFormat::Rg8Uint,
Rg8sint => wgpu::TextureFormat::Rg8Sint,
Rg16unorm => wgpu::TextureFormat::Rg16Unorm,
Rg16snorm => wgpu::TextureFormat::Rg16Snorm,
Rg16uint => wgpu::TextureFormat::Rg16Uint,
Rg16sint => wgpu::TextureFormat::Rg16Sint,
Rg16float => wgpu::TextureFormat::Rg16Float,
R8unorm => wgpu::TextureFormat::R8Unorm,
R8snorm => wgpu::TextureFormat::R8Snorm,
R8uint => wgpu::TextureFormat::R8Uint,
R8sint => wgpu::TextureFormat::R8Sint,
R16unorm => wgpu::TextureFormat::R16Unorm,
R16snorm => wgpu::TextureFormat::R16Snorm,
R16uint => wgpu::TextureFormat::R16Uint,
R16sint => wgpu::TextureFormat::R16Sint,
R16float => wgpu::TextureFormat::R16Float,
Rgb10a2unorm => wgpu::TextureFormat::Rgb10a2Unorm,
Rgb10a2uint => wgpu::TextureFormat::Rgb10a2Uint,
Rg11b10ufloat => wgpu::TextureFormat::Rg11b10Ufloat,
}
}
fn texture_kind_view_dimension(kind: ir::TextureKind) -> wgpu::TextureViewDimension {
match kind {
ir::TextureKind::Texture1D => wgpu::TextureViewDimension::D1,
ir::TextureKind::Texture2D | ir::TextureKind::TextureMultisampled2D => {
wgpu::TextureViewDimension::D2
}
ir::TextureKind::Texture2DArray => wgpu::TextureViewDimension::D2Array,
ir::TextureKind::Texture3D => wgpu::TextureViewDimension::D3,
ir::TextureKind::TextureCube => wgpu::TextureViewDimension::Cube,
ir::TextureKind::TextureCubeArray => wgpu::TextureViewDimension::CubeArray,
}
}
fn texture_depth_view_dimension(kind: ir::TextureDepthKind) -> wgpu::TextureViewDimension {
match kind {
ir::TextureDepthKind::Depth2D | ir::TextureDepthKind::DepthMultisampled2D => {
wgpu::TextureViewDimension::D2
}
ir::TextureDepthKind::Depth2DArray => wgpu::TextureViewDimension::D2Array,
ir::TextureDepthKind::DepthCube => wgpu::TextureViewDimension::Cube,
ir::TextureDepthKind::DepthCubeArray => wgpu::TextureViewDimension::CubeArray,
}
}
pub fn type_byte_size(ty: &ir::Type, module: &ir::Module) -> u64 {
let layout = type_layout(ty, module);
layout.size
}
fn type_byte_size_or_zero(ty: &ir::Type, module: &ir::Module) -> u64 {
type_byte_size(ty, module)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TypeLayout {
pub size: u64,
pub align: u64,
}
fn align_up(val: u64, align: u64) -> u64 {
if align == 0 {
val
} else {
(val + (align - 1)) & !(align - 1)
}
}
fn type_layout(ty: &ir::Type, module: &ir::Module) -> TypeLayout {
match ty {
ir::Type::Scalar(s) => scalar_layout(*s),
ir::Type::Vector { elements, .. } => vector_layout(*elements),
ir::Type::Matrix { columns, rows, .. } => matrix_layout(*columns, *rows),
ir::Type::Array { elem, len } => {
let inner = type_layout(elem, module);
let n = eval_array_len(len, module).unwrap_or(0);
TypeLayout {
size: (n as u64) * align_up(inner.size, inner.align.max(1)),
align: inner.align,
}
}
ir::Type::RuntimeArray { elem } => {
let inner = type_layout(elem, module);
TypeLayout {
size: 0,
align: inner.align,
}
}
ir::Type::Atomic { elem } => type_layout(elem, module),
ir::Type::Struct { name, type_args } => struct_layout(name, type_args, module),
ir::Type::Ptr { .. } => TypeLayout { size: 0, align: 1 },
ir::Type::Sampler
| ir::Type::SamplerComparison
| ir::Type::Texture { .. }
| ir::Type::TextureDepth { .. }
| ir::Type::TextureStorage { .. }
| ir::Type::TypeParam { .. }
| ir::Type::Phantom { .. }
| ir::Type::AssocType { .. } => TypeLayout { size: 0, align: 1 },
}
}
fn scalar_layout(s: ir::ScalarType) -> TypeLayout {
match s {
ir::ScalarType::F32 | ir::ScalarType::I32 | ir::ScalarType::U32 | ir::ScalarType::Bool => {
TypeLayout { size: 4, align: 4 }
}
}
}
fn vector_layout(elements: u8) -> TypeLayout {
match elements {
2 => TypeLayout { size: 8, align: 8 },
3 => TypeLayout {
size: 12,
align: 16,
},
4 => TypeLayout {
size: 16,
align: 16,
},
_ => TypeLayout { size: 0, align: 1 },
}
}
fn matrix_layout(columns: u8, rows: u8) -> TypeLayout {
let row_vec = vector_layout(rows);
let stride = align_up(row_vec.size, row_vec.align);
TypeLayout {
size: (columns as u64) * stride,
align: row_vec.align,
}
}
fn struct_layout(name: &str, type_args: &[ir::Type], module: &ir::Module) -> TypeLayout {
let Some(s) = find_struct(name, type_args, module) else {
return TypeLayout { size: 0, align: 1 };
};
let mut offset: u64 = 0;
let mut struct_align: u64 = 1;
for field in &s.fields {
let fl = type_layout(&field.ty, module);
struct_align = struct_align.max(fl.align);
offset = align_up(offset, fl.align);
offset += fl.size;
}
let size = align_up(offset, struct_align);
TypeLayout {
size,
align: struct_align,
}
}
fn find_struct<'a>(
name: &str,
_type_args: &[ir::Type],
module: &'a ir::Module,
) -> Option<&'a ir::ItemStruct> {
module.items.iter().find_map(|i| match i {
ir::Item::Struct(s) if s.name == name => Some(s),
_ => None,
})
}
fn eval_array_len(expr: &ir::Expr, module: &ir::Module) -> Option<usize> {
match expr {
ir::Expr::Lit(ir::Lit::Int { digits, .. }) => digits.parse::<usize>().ok(),
ir::Expr::Unary {
op: ir::UnOp::Neg, ..
} => None,
ir::Expr::Ident(name) => {
let val = module.items.iter().find_map(|i| match i {
ir::Item::Const(c) if c.name == *name => Some(eval_const_int(&c.expr, module)),
_ => None,
});
val.flatten()
}
_ => None,
}
}
fn eval_const_int(expr: &ir::Expr, module: &ir::Module) -> Option<usize> {
eval_array_len(expr, module)
}
#[cfg(test)]
mod binding_visibility {
use std::borrow::Cow;
use super::*;
fn f32_ty() -> ir::Type {
ir::Type::Scalar(ir::ScalarType::F32)
}
fn zero() -> ir::Expr {
ir::Expr::Lit(ir::Lit::Float {
text: "0.0".to_string(),
})
}
fn uniform(name: &str, group: u32, binding: u32) -> ir::Item {
ir::Item::Uniform(ir::ItemUniform {
group,
binding,
name: name.to_string(),
ty: f32_ty(),
attrs: Vec::new(),
})
}
fn call(name: &str, params: Vec<ir::Expr>) -> ir::Expr {
ir::Expr::FnCall {
path: ir::FnPath::Ident(name.to_string()),
type_args: Vec::new(),
params,
}
}
fn ident(name: &str) -> ir::Expr {
ir::Expr::Ident(name.to_string())
}
fn fn_item(name: &str, fn_attrs: ir::FnAttrs, stmts: Vec<ir::Stmt>) -> ir::Item {
ir::Item::Fn(ir::ItemFn {
type_params: Vec::new(),
const_params: Vec::new(),
fn_attrs,
name: Cow::Owned(name.to_string()),
inputs: Vec::new(),
return_type: ir::ReturnType::Default,
block: ir::Block { stmts },
attrs: Vec::new(),
})
}
fn returns(expr: ir::Expr) -> ir::Stmt {
ir::Stmt::Return(Some(expr))
}
fn analyze(items: Vec<ir::Item>) -> WgpuLinkage {
analyze_ir_module(ir::Module {
name: "test_module",
items,
attrs: Vec::new(),
})
}
fn visibility_of(linkage: &WgpuLinkage, name: &str) -> wgpu::ShaderStages {
for bind_group in linkage.bind_groups.values() {
if let Some(meta) = bind_group.bindings.iter().find(|b| b.name == name) {
let entry = bind_group
.entries
.iter()
.find(|e| e.binding == meta.binding)
.expect("binding layout entry exists");
return entry.visibility;
}
}
panic!("binding '{name}' not found in any bind group");
}
#[test]
fn direct_use_gets_entry_stage() {
let linkage = analyze(vec![
uniform("MY_UNIFORM", 0, 0),
fn_item(
"vert",
ir::FnAttrs::Vertex,
vec![returns(ident("MY_UNIFORM"))],
),
]);
assert_eq!(
visibility_of(&linkage, "MY_UNIFORM"),
wgpu::ShaderStages::VERTEX
);
}
#[test]
fn helper_only_use_carries_entry_point_stages() {
let linkage = analyze(vec![
uniform("MY_UNIFORM", 0, 0),
fn_item(
"read_uniform",
ir::FnAttrs::None,
vec![returns(ident("MY_UNIFORM"))],
),
fn_item(
"vert",
ir::FnAttrs::Vertex,
vec![returns(call("read_uniform", vec![]))],
),
fn_item(
"frag",
ir::FnAttrs::Fragment,
vec![returns(call("read_uniform", vec![]))],
),
]);
assert_eq!(
visibility_of(&linkage, "MY_UNIFORM"),
wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT
);
}
#[test]
fn frag_only_helper_stays_fragment() {
let linkage = analyze(vec![
uniform("MY_UNIFORM", 0, 0),
fn_item(
"read_uniform",
ir::FnAttrs::None,
vec![returns(ident("MY_UNIFORM"))],
),
fn_item(
"frag",
ir::FnAttrs::Fragment,
vec![returns(call("read_uniform", vec![]))],
),
fn_item("vert", ir::FnAttrs::Vertex, vec![returns(zero())]),
]);
assert_eq!(
visibility_of(&linkage, "MY_UNIFORM"),
wgpu::ShaderStages::FRAGMENT
);
}
#[test]
fn mixed_direct_and_helper_use() {
let linkage = analyze(vec![
uniform("MY_UNIFORM", 0, 0),
fn_item(
"read_uniform",
ir::FnAttrs::None,
vec![returns(ident("MY_UNIFORM"))],
),
fn_item(
"vert",
ir::FnAttrs::Vertex,
vec![returns(ident("MY_UNIFORM"))],
),
fn_item(
"frag",
ir::FnAttrs::Fragment,
vec![returns(call("read_uniform", vec![]))],
),
]);
assert_eq!(
visibility_of(&linkage, "MY_UNIFORM"),
wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT
);
}
#[test]
fn helper_chain_is_transitive() {
let linkage = analyze(vec![
uniform("MY_UNIFORM", 0, 0),
fn_item(
"inner",
ir::FnAttrs::None,
vec![returns(ident("MY_UNIFORM"))],
),
fn_item(
"outer",
ir::FnAttrs::None,
vec![returns(call("inner", vec![]))],
),
fn_item(
"vert",
ir::FnAttrs::Vertex,
vec![returns(call("outer", vec![]))],
),
]);
assert_eq!(
visibility_of(&linkage, "MY_UNIFORM"),
wgpu::ShaderStages::VERTEX
);
}
#[test]
fn unused_binding_defaults_to_compute() {
let linkage = analyze(vec![
uniform("MY_UNIFORM", 0, 0),
fn_item("vert", ir::FnAttrs::Vertex, vec![returns(zero())]),
]);
assert_eq!(
visibility_of(&linkage, "MY_UNIFORM"),
wgpu::ShaderStages::COMPUTE
);
}
#[test]
fn local_shadowing_fn_name_is_not_a_call_edge() {
let linkage = analyze(vec![
uniform("DATA", 0, 0),
fn_item(
"read_uniform",
ir::FnAttrs::None,
vec![returns(ident("DATA"))],
),
fn_item(
"vert",
ir::FnAttrs::Vertex,
vec![
ir::Stmt::Local(ir::Local {
mutable: false,
name: "read_uniform".to_string(),
ty: None,
init: Some(zero()),
}),
returns(ident("read_uniform")),
],
),
]);
assert_eq!(visibility_of(&linkage, "DATA"), wgpu::ShaderStages::COMPUTE);
}
#[test]
fn call_edge_survives_local_shadowing_elsewhere() {
let linkage = analyze(vec![
uniform("DATA", 0, 0),
fn_item(
"read_uniform",
ir::FnAttrs::None,
vec![returns(ident("DATA"))],
),
fn_item(
"vert",
ir::FnAttrs::Vertex,
vec![
ir::Stmt::Local(ir::Local {
mutable: false,
name: "read_uniform".to_string(),
ty: None,
init: Some(zero()),
}),
returns(ident("read_uniform")),
],
),
fn_item(
"frag",
ir::FnAttrs::Fragment,
vec![returns(call("read_uniform", vec![]))],
),
]);
assert_eq!(
visibility_of(&linkage, "DATA"),
wgpu::ShaderStages::FRAGMENT
);
}
#[test]
fn impl_method_helper_is_reachable() {
let method = ir::ItemFn {
type_params: Vec::new(),
const_params: Vec::new(),
fn_attrs: ir::FnAttrs::None,
name: Cow::Owned("read_uniform".to_string()),
inputs: Vec::new(),
return_type: ir::ReturnType::Default,
block: ir::Block {
stmts: vec![returns(ident("MY_UNIFORM"))],
},
attrs: Vec::new(),
};
let linkage = analyze(vec![
uniform("MY_UNIFORM", 0, 0),
ir::Item::Impl(ir::ItemImpl {
type_params: Vec::new(),
const_params: Vec::new(),
self_ty: "Foo".to_string(),
items: vec![ir::ImplItem::Fn(method)],
attrs: Vec::new(),
}),
fn_item(
"vert",
ir::FnAttrs::Vertex,
vec![returns(ir::Expr::FnCall {
path: ir::FnPath::TypeMethod {
ty: "Foo".to_string(),
method: "read_uniform".to_string(),
},
type_args: Vec::new(),
params: Vec::new(),
})],
),
]);
assert_eq!(
visibility_of(&linkage, "MY_UNIFORM"),
wgpu::ShaderStages::VERTEX
);
}
}