spirt/qptr/
layout.rs

1// FIXME(eddyb) layouts are a bit tricky: this recomputes them from several passes.
2
3use crate::qptr::shapes;
4use crate::{
5    AddrSpace, Attr, Const, ConstKind, Context, Diag, FxIndexMap, Type, TypeKind, TypeOrConst, spv,
6};
7use itertools::Either;
8use smallvec::SmallVec;
9use std::cell::RefCell;
10use std::cmp::Ordering;
11use std::num::NonZeroU32;
12use std::ops::Range;
13use std::rc::Rc;
14
15/// Various toggles for layout-related behavior that is not unambiguous from the
16/// SPIR-V alone, or involves intermediary illegal SPIR-V (during legalization).
17//
18// FIXME(eddyb) use proper newtypes (and log2 for align!).
19pub struct LayoutConfig {
20    pub ignore_legacy_align: bool,
21    pub min_aggregate_legacy_align: u32,
22
23    /// Assumed size and alignment for `OpTypeBool`, even if unusable
24    /// with externally-visible concrete memory (i.e. buffers).
25    ///
26    /// This is only useful for accurate handling of illegal SPIR-V relying on
27    /// e.g. pointer casts, and as such defaults to `(1, 1)`, to merely ensure
28    /// unique offsets and guarantee `qptr::lift` can tell fields apart.
29    //
30    // FIXME(eddyb) might be nice to default to an "offsets/sizes are abstract"
31    // mode, which disallows reinterpretation on the basis that the precise
32    // offsets/sizes may not match between types (but that's its own nightmare).
33    pub abstract_bool_size_align: (u32, u32),
34
35    /// Assumed size and alignment for logical `OpTypePointer`s, even if unusable
36    /// with externally-visible concrete memory (i.e. buffers).
37    ///
38    /// This is only useful for accurate handling of illegal SPIR-V relying on
39    /// e.g. pointer casts, and as such defaults to `(1, 1)`, to merely ensure
40    /// unique offsets and guarantee `qptr::lift` can tell fields apart.
41    //
42    // FIXME(eddyb) might be nice to default to an "offsets/sizes are abstract"
43    // mode, which disallows reinterpretation on the basis that the precise
44    // offsets/sizes may not match between types (but that's its own nightmare).
45    pub logical_ptr_size_align: (u32, u32),
46}
47
48impl LayoutConfig {
49    pub const VULKAN_SCALAR_LAYOUT: Self = Self {
50        ignore_legacy_align: true,
51        min_aggregate_legacy_align: 1,
52
53        abstract_bool_size_align: (1, 1),
54        logical_ptr_size_align: (1, 1),
55    };
56    pub const VULKAN_STANDARD_LAYOUT: Self =
57        Self { ignore_legacy_align: false, ..Self::VULKAN_SCALAR_LAYOUT };
58    // FIXME(eddyb) is this even useful? (all the storage classes that have any
59    // kind of alignment requirements, require explicit offsets)
60    pub const VULKAN_EXTENDED_ALIGN_UBO_LAYOUT: Self =
61        Self { min_aggregate_legacy_align: 16, ..Self::VULKAN_STANDARD_LAYOUT };
62}
63
64pub(super) struct LayoutError(pub(super) Diag);
65
66#[derive(Clone)]
67pub(super) enum TypeLayout {
68    Handle(HandleLayout),
69    HandleArray(HandleLayout, Option<NonZeroU32>),
70
71    // FIXME(eddyb) unify terminology around "concrete"/"memory"/"untyped (data)".
72    Concrete(Rc<MemTypeLayout>),
73}
74
75// NOTE(eddyb) `Handle` is parameterized over the `Buffer` layout.
76pub(super) type HandleLayout = shapes::Handle<Rc<MemTypeLayout>>;
77
78pub(super) struct MemTypeLayout {
79    pub(super) original_type: Type,
80    pub(super) mem_layout: shapes::MaybeDynMemLayout,
81    pub(super) components: Components,
82}
83
84// FIXME(eddyb) use proper newtypes for byte sizes.
85pub(super) enum Components {
86    Scalar,
87
88    /// Vector and array elements (all of them having the same `elem` layout).
89    Elements {
90        stride: NonZeroU32,
91        elem: Rc<MemTypeLayout>,
92        fixed_len: Option<NonZeroU32>,
93    },
94
95    Fields {
96        // FIXME(eddyb) should these be fused? (but `u32` is smaller than `Rc`)
97        offsets: SmallVec<[u32; 4]>,
98        layouts: SmallVec<[Rc<MemTypeLayout>; 4]>,
99    },
100}
101
102impl Components {
103    /// Return all components (by index), which completely contain `offset_range`.
104    ///
105    /// If `offset_range` is zero-sized (`offset_range.start == offset_range.end`),
106    /// this can return multiple components, with at most one ever being non-ZST.
107    //
108    // FIXME(eddyb) be more aggressive in pruning ZSTs so this can be simpler.
109    pub(super) fn find_components_containing(
110        &self,
111        // FIXME(eddyb) consider renaming such offset ranges to "extent".
112        offset_range: Range<u32>,
113    ) -> impl Iterator<Item = usize> + '_ {
114        match self {
115            Components::Scalar => Either::Left(None.into_iter()),
116            Components::Elements { stride, elem, fixed_len } => {
117                Either::Left(
118                    Some(offset_range.start / stride.get())
119                        .and_then(|elem_idx| {
120                            let elem_idx_vs_len = fixed_len
121                                .map_or(Ordering::Less, |fixed_len| elem_idx.cmp(&fixed_len.get()));
122                            let elem_size = match elem_idx_vs_len {
123                                Ordering::Less => elem.mem_layout.fixed_base.size,
124
125                                // HACK(eddyb) this allows one-past-the-end pointers.
126                                Ordering::Equal => 0,
127
128                                Ordering::Greater => return None,
129                            };
130                            let elem_start = elem_idx * stride.get();
131                            Some((elem_idx, elem_start..elem_start.checked_add(elem_size)?))
132                        })
133                        .filter(|(_, elem_range)| offset_range.end <= elem_range.end)
134                        .and_then(|(elem_idx, _)| usize::try_from(elem_idx).ok())
135                        .into_iter(),
136                )
137            }
138            // FIXME(eddyb) this is inefficient, we should be doing binary search
139            // on offsets if they're ordered (with an optional `BTreeMap<offset, idx>`?)
140            // - ideally this needs an abstraction tho, some kind of "binary-searchable array"?
141            Components::Fields { offsets, layouts } => Either::Right(
142                offsets
143                    .iter()
144                    .zip(layouts)
145                    .map(|(&field_offset, field)| {
146                        // HACK(eddyb) really need a maybe-open-ended range type.
147                        if field.mem_layout.dyn_unit_stride.is_some() {
148                            Err(field_offset..)
149                        } else {
150                            Ok(field_offset
151                                ..field_offset
152                                    .checked_add(field.mem_layout.fixed_base.size)
153                                    .unwrap())
154                        }
155                    })
156                    .enumerate()
157                    .filter(move |(_, field_range)| match field_range {
158                        Ok(field_range) => {
159                            field_range.start <= offset_range.start
160                                && offset_range.end <= field_range.end
161                        }
162                        Err(field_range) => field_range.start <= offset_range.start,
163                    })
164                    .map(|(field_idx, _)| field_idx),
165            ),
166        }
167    }
168}
169
170/// Context for computing `TypeLayout`s from `Type`s (with caching).
171pub(super) struct LayoutCache<'a> {
172    cx: Rc<Context>,
173    wk: &'static spv::spec::WellKnown,
174
175    config: &'a LayoutConfig,
176
177    cache: RefCell<FxIndexMap<Type, TypeLayout>>,
178}
179
180impl<'a> LayoutCache<'a> {
181    pub(super) fn new(cx: Rc<Context>, config: &'a LayoutConfig) -> Self {
182        Self { cx, wk: &spv::spec::Spec::get().well_known, config, cache: Default::default() }
183    }
184
185    // FIXME(eddyb) properly distinguish between zero-extension and sign-extension.
186    fn const_as_u32(&self, ct: Const) -> Option<u32> {
187        if let ConstKind::SpvInst { spv_inst_and_const_inputs } = &self.cx[ct].kind {
188            let (spv_inst, _const_inputs) = &**spv_inst_and_const_inputs;
189            if spv_inst.opcode == self.wk.OpConstant && spv_inst.imms.len() == 1 {
190                match spv_inst.imms[..] {
191                    [spv::Imm::Short(_, x)] => return Some(x),
192                    _ => unreachable!(),
193                }
194            }
195        }
196        None
197    }
198
199    /// Attempt to compute a `TypeLayout` for a given (SPIR-V) `Type`.
200    pub(super) fn layout_of(&self, ty: Type) -> Result<TypeLayout, LayoutError> {
201        if let Some(cached) = self.cache.borrow().get(&ty).cloned() {
202            return Ok(cached);
203        }
204
205        let cx = &self.cx;
206        let wk = self.wk;
207
208        let ty_def = &cx[ty];
209        let (spv_inst, type_and_const_inputs) = match &ty_def.kind {
210            // FIXME(eddyb) treat `QPtr`s as scalars.
211            TypeKind::QPtr => {
212                return Err(LayoutError(Diag::bug(
213                    ["`layout_of(qptr)` (already lowered?)".into()],
214                )));
215            }
216            TypeKind::SpvInst { spv_inst, type_and_const_inputs } => {
217                (spv_inst, type_and_const_inputs)
218            }
219            TypeKind::SpvStringLiteralForExtInst => {
220                return Err(LayoutError(Diag::bug([
221                    "`layout_of(type_of(OpString<\"...\">))`".into()
222                ])));
223            }
224        };
225
226        let scalar_with_size_and_align = |(size, align)| {
227            TypeLayout::Concrete(Rc::new(MemTypeLayout {
228                original_type: ty,
229                mem_layout: shapes::MaybeDynMemLayout {
230                    fixed_base: shapes::MemLayout { align, legacy_align: align, size },
231                    dyn_unit_stride: None,
232                },
233                components: Components::Scalar,
234            }))
235        };
236        let scalar = |width: u32| {
237            assert!(width.is_power_of_two());
238            let size = width / 8;
239            assert_eq!(size * 8, width);
240            scalar_with_size_and_align((size, size))
241        };
242        let align_to = |size: u32, align: u32| {
243            assert!(align.is_power_of_two() && align > 0);
244            Ok(size.checked_add(align - 1).ok_or_else(|| {
245                LayoutError(Diag::bug([
246                    format!("`align_to({size}, {align})` overflowed `u32`").into()
247                ]))
248            })? & !(align - 1))
249        };
250        // HACK(eddyb) named arguments for the `array` closure.
251        struct ArrayParams {
252            fixed_len: Option<u32>,
253            known_stride: Option<u32>,
254            min_legacy_align: u32,
255            legacy_align_multiplier: u32,
256        }
257        let array = |elem_type: Type,
258                     ArrayParams {
259                         fixed_len,
260                         known_stride,
261                         min_legacy_align,
262                         legacy_align_multiplier,
263                     }| {
264            let fixed_len = fixed_len
265                .map(|x| {
266                    NonZeroU32::new(x).ok_or_else(|| {
267                        LayoutError(Diag::err(["SPIR-V disallows arrays of `0` length".into()]))
268                    })
269                })
270                .transpose()?;
271            match self.layout_of(elem_type)? {
272                TypeLayout::Handle(handle) => Ok(TypeLayout::HandleArray(handle, fixed_len)),
273                TypeLayout::HandleArray(..) => Err(LayoutError(Diag::err([
274                    "handle array `".into(),
275                    elem_type.into(),
276                    "` cannot be further wrapped in an array".into(),
277                ]))),
278                TypeLayout::Concrete(elem) => {
279                    if elem.mem_layout.dyn_unit_stride.is_some() {
280                        return Err(LayoutError(Diag::err([
281                            "dynamically sized type `".into(),
282                            elem_type.into(),
283                            "` cannot be further wrapped in an array".into(),
284                        ])));
285                    }
286                    let stride = match known_stride {
287                        Some(stride) => stride,
288                        None => {
289                            let shapes::MemLayout { align, legacy_align, size } =
290                                elem.mem_layout.fixed_base;
291                            let (stride, legacy_stride) =
292                                (align_to(size, align)?, align_to(size, legacy_align)?);
293
294                            // FIXME(eddyb) this whole ambiguity mechanism is strange and
295                            // maybe unnecessary? (all the storage classes that have any
296                            // kind of alignment requirements, require explicit offsets)
297                            if !self.config.ignore_legacy_align && stride != legacy_stride {
298                                return Err(LayoutError(Diag::bug([format!(
299                                    "ambiguous stride: \
300                                    {stride} (scalar) vs {legacy_stride} (legacy), \
301                                     due to alignment differences \
302                                     ({align} scalar vs {legacy_align} legacy)",
303                                )
304                                .into()])));
305                            }
306                            stride
307                        }
308                    };
309                    let stride = NonZeroU32::new(stride).ok_or_else(|| {
310                        LayoutError(Diag::err(["SPIR-V disallows arrays of `0` stride".into()]))
311                    })?;
312                    Ok(TypeLayout::Concrete(Rc::new(MemTypeLayout {
313                        original_type: ty,
314                        mem_layout: shapes::MaybeDynMemLayout {
315                            fixed_base: shapes::MemLayout {
316                                align: elem.mem_layout.fixed_base.align,
317                                legacy_align: elem
318                                    .mem_layout
319                                    .fixed_base
320                                    .legacy_align
321                                    .checked_mul(legacy_align_multiplier)
322                                    .unwrap()
323                                    .max(min_legacy_align),
324                                size: fixed_len
325                                    .map(|len| {
326                                        stride.checked_mul(len).ok_or_else(|| {
327                                            LayoutError(Diag::bug([format!(
328                                                "`{stride} * {len}` overflowed `u32`"
329                                            )
330                                            .into()]))
331                                        })
332                                    })
333                                    .transpose()?
334                                    .map_or(0, |size| size.get()),
335                            },
336                            dyn_unit_stride: if fixed_len.is_none() { Some(stride) } else { None },
337                        },
338                        components: Components::Elements { stride, elem, fixed_len },
339                    })))
340                }
341            }
342        };
343        let short_imm_at = |i| match spv_inst.imms[i] {
344            spv::Imm::Short(_, x) => x,
345            _ => unreachable!(),
346        };
347
348        // FIXME(eddyb) !!! what if... types had a min/max size and then...
349        // that would allow surrounding offsets to limit their size... but... ugh...
350        // ugh this doesn't make any sense. maybe if the front-end specifies
351        // offsets with "abstract types", it must configure `qptr::layout`?
352        let layout = if spv_inst.opcode == wk.OpTypeBool {
353            // FIXME(eddyb) make this properly abstract instead of only configurable.
354            scalar_with_size_and_align(self.config.abstract_bool_size_align)
355        } else if spv_inst.opcode == wk.OpTypePointer {
356            // FIXME(eddyb) make this properly abstract instead of only configurable.
357            // FIXME(eddyb) categorize `OpTypePointer` by storage class and split on
358            // logical vs physical here.
359            scalar_with_size_and_align(self.config.logical_ptr_size_align)
360        } else if [wk.OpTypeInt, wk.OpTypeFloat].contains(&spv_inst.opcode) {
361            scalar(short_imm_at(0))
362        } else if [wk.OpTypeVector, wk.OpTypeMatrix].contains(&spv_inst.opcode) {
363            let len = short_imm_at(0);
364            let (min_legacy_align, legacy_align_multiplier) = if spv_inst.opcode == wk.OpTypeVector
365            {
366                // NOTE(eddyb) this is specifically Vulkan "base alignment".
367                (1, if len <= 2 { 2 } else { 4 })
368            } else {
369                (self.config.min_aggregate_legacy_align, 1)
370            };
371            // NOTE(eddyb) `RowMajor` is disallowed on `OpTypeStruct` members below.
372            array(
373                match type_and_const_inputs[..] {
374                    [TypeOrConst::Type(elem_type)] => elem_type,
375                    _ => unreachable!(),
376                },
377                ArrayParams {
378                    fixed_len: Some(len),
379                    known_stride: None,
380                    min_legacy_align,
381                    legacy_align_multiplier,
382                },
383            )?
384        } else if [wk.OpTypeArray, wk.OpTypeRuntimeArray].contains(&spv_inst.opcode) {
385            let len = type_and_const_inputs
386                .get(1)
387                .map(|&len| {
388                    let len = match len {
389                        TypeOrConst::Const(len) => len,
390                        TypeOrConst::Type(_) => unreachable!(),
391                    };
392                    self.const_as_u32(len).ok_or_else(|| {
393                        LayoutError(Diag::bug(
394                            ["specialization constants not supported yet".into()],
395                        ))
396                    })
397                })
398                .transpose()?;
399            let mut stride_decoration = None;
400            for attr in &cx[ty_def.attrs].attrs {
401                match attr {
402                    Attr::SpvAnnotation(attr_spv_inst)
403                        if attr_spv_inst.opcode == wk.OpDecorate
404                            && attr_spv_inst.imms[0]
405                                == spv::Imm::Short(wk.Decoration, wk.ArrayStride) =>
406                    {
407                        stride_decoration = Some(match attr_spv_inst.imms[1] {
408                            spv::Imm::Short(_, x) => x,
409                            _ => unreachable!(),
410                        });
411                        break;
412                    }
413                    _ => {}
414                }
415            }
416            array(
417                match type_and_const_inputs[0] {
418                    TypeOrConst::Type(elem_type) => elem_type,
419                    TypeOrConst::Const(_) => unreachable!(),
420                },
421                ArrayParams {
422                    fixed_len: len,
423                    known_stride: stride_decoration,
424                    min_legacy_align: self.config.min_aggregate_legacy_align,
425                    legacy_align_multiplier: 1,
426                },
427            )?
428        } else if spv_inst.opcode == wk.OpTypeStruct {
429            let field_layouts: SmallVec<[_; 4]> = type_and_const_inputs
430                .iter()
431                .map(|&ty_or_ct| match ty_or_ct {
432                    TypeOrConst::Type(field_type) => field_type,
433                    TypeOrConst::Const(_) => unreachable!(),
434                })
435                .map(|field_type| match self.layout_of(field_type)? {
436                    TypeLayout::Handle(_) | TypeLayout::HandleArray(..) => {
437                        Err(LayoutError(Diag::bug([
438                            "handles cannot be placed in a struct field".into()
439                        ])))
440                    }
441                    TypeLayout::Concrete(field_layout) => Ok(field_layout),
442                })
443                .collect::<Result<_, _>>()?;
444
445            let mut field_offsets: SmallVec<[_; 4]> = SmallVec::with_capacity(field_layouts.len());
446            for attr in &cx[ty_def.attrs].attrs {
447                match attr {
448                    Attr::SpvAnnotation(attr_spv_inst)
449                        if attr_spv_inst.opcode == wk.OpMemberDecorate
450                            && attr_spv_inst.imms[1]
451                                == spv::Imm::Short(wk.Decoration, wk.RowMajor) =>
452                    {
453                        return Err(LayoutError(Diag::bug([
454                            "`RowMajor` matrix types unsupported".into(),
455                        ])));
456                    }
457                    Attr::SpvAnnotation(attr_spv_inst)
458                        if attr_spv_inst.opcode == wk.OpMemberDecorate
459                            && attr_spv_inst.imms[1]
460                                == spv::Imm::Short(wk.Decoration, wk.Offset) =>
461                    {
462                        let (field_idx, field_offset) = match attr_spv_inst.imms[..] {
463                            [spv::Imm::Short(_, idx), _, spv::Imm::Short(_, offset)] => {
464                                (idx, offset)
465                            }
466                            _ => unreachable!(),
467                        };
468                        let field_idx = usize::try_from(field_idx).unwrap();
469                        match field_idx.cmp(&field_offsets.len()) {
470                            Ordering::Less => {
471                                return Err(LayoutError(Diag::bug([
472                                    "a struct field cannot have more than one explicit offset"
473                                        .into(),
474                                ])));
475                            }
476                            Ordering::Greater => {
477                                return Err(LayoutError(Diag::bug([
478                                    "structs with explicit offsets must provide them for all fields"
479                                        .into(),
480                                ])));
481                            }
482                            Ordering::Equal => {
483                                field_offsets.push(field_offset);
484                            }
485                        }
486                    }
487                    _ => {}
488                }
489            }
490            let mut mem_layout = shapes::MaybeDynMemLayout {
491                fixed_base: shapes::MemLayout {
492                    align: 1,
493                    legacy_align: self.config.min_aggregate_legacy_align,
494                    size: 0,
495                },
496                dyn_unit_stride: None,
497            };
498            if !field_offsets.is_empty() {
499                if field_offsets.len() != field_layouts.len() {
500                    return Err(LayoutError(Diag::bug([
501                        "structs with explicit offsets must provide them for all fields".into(),
502                    ])));
503                }
504
505                // HACK(eddyb) this treats the struct more like an union, but
506                // it shold nevertheless work (the other approach would be to
507                // search for the "last field (in offset order)", and/or iterate
508                // all fields in offset order, to validate the lack of overlap),
509                // and also "last field (in offset order)" approaches would still
510                // have to look at all the fields in order to compute alignment.
511                for (&field_offset, field_layout) in field_offsets.iter().zip(&field_layouts) {
512                    let field = field_layout.mem_layout;
513
514                    mem_layout.fixed_base.align =
515                        mem_layout.fixed_base.align.max(field.fixed_base.align);
516                    mem_layout.fixed_base.legacy_align =
517                        mem_layout.fixed_base.legacy_align.max(field.fixed_base.legacy_align);
518                    mem_layout.fixed_base.size = mem_layout.fixed_base.size.max(
519                        field_offset.checked_add(field.fixed_base.size).ok_or_else(|| {
520                            LayoutError(Diag::bug([format!(
521                                "`{} + {}` overflowed `u32`",
522                                field_offset, field.fixed_base.size
523                            )
524                            .into()]))
525                        })?,
526                    );
527
528                    // FIXME(eddyb) validate sized-vs-unsized fields, too.
529                    if let Some(field_dyn_unit_stride) = field.dyn_unit_stride {
530                        if mem_layout.dyn_unit_stride.is_some() {
531                            return Err(LayoutError(Diag::bug([
532                                "only one field of a struct can have a dynamically sized type"
533                                    .into(),
534                            ])));
535                        }
536                        mem_layout.dyn_unit_stride = Some(field_dyn_unit_stride);
537                    }
538                }
539            } else {
540                for field_layout in &field_layouts {
541                    if mem_layout.dyn_unit_stride.is_some() {
542                        return Err(LayoutError(Diag::bug([
543                            "only the last field of a struct can have a dynamically sized type"
544                                .into(),
545                        ])));
546                    }
547
548                    let field = field_layout.mem_layout;
549
550                    let (offset, legacy_offset) = (
551                        align_to(mem_layout.fixed_base.size, field.fixed_base.align)?,
552                        align_to(mem_layout.fixed_base.size, field.fixed_base.legacy_align)?,
553                    );
554                    // FIXME(eddyb) this whole ambiguity mechanism is strange and
555                    // maybe unnecessary? (all the storage classes that have any
556                    // kind of alignment requirements, require explicit offsets)
557                    if !self.config.ignore_legacy_align && offset != legacy_offset {
558                        return Err(LayoutError(Diag::bug([format!(
559                            "ambiguous offset: {offset} (scalar) vs {legacy_offset} (legacy), \
560                             due to alignment differences ({} scalar vs {} legacy)",
561                            field.fixed_base.align, field.fixed_base.legacy_align
562                        )
563                        .into()])));
564                    }
565
566                    field_offsets.push(offset);
567
568                    mem_layout.fixed_base.align =
569                        mem_layout.fixed_base.align.max(field.fixed_base.align);
570                    mem_layout.fixed_base.legacy_align =
571                        mem_layout.fixed_base.legacy_align.max(field.fixed_base.legacy_align);
572                    mem_layout.fixed_base.size =
573                        offset.checked_add(field.fixed_base.size).ok_or_else(|| {
574                            LayoutError(Diag::bug([format!(
575                                "`{} + {}` overflowed `u32`",
576                                offset, field.fixed_base.size
577                            )
578                            .into()]))
579                        })?;
580
581                    assert!(mem_layout.dyn_unit_stride.is_none());
582                    mem_layout.dyn_unit_stride = field.dyn_unit_stride;
583                }
584            }
585            // FIXME(eddyb) how should the fixed base be aligned in unsized structs?
586            if mem_layout.dyn_unit_stride.is_none() {
587                mem_layout.fixed_base.size =
588                    align_to(mem_layout.fixed_base.size, mem_layout.fixed_base.align)?;
589            }
590
591            let concrete = Rc::new(MemTypeLayout {
592                original_type: ty,
593                mem_layout,
594                components: Components::Fields { offsets: field_offsets, layouts: field_layouts },
595            });
596            let mut is_interface_block = false;
597            for attr in &cx[ty_def.attrs].attrs {
598                match attr {
599                    Attr::SpvAnnotation(attr_spv_inst)
600                        if attr_spv_inst.opcode == wk.OpDecorate
601                            && attr_spv_inst.imms[0]
602                                == spv::Imm::Short(wk.Decoration, wk.Block) =>
603                    {
604                        is_interface_block = true;
605                        break;
606                    }
607                    _ => {}
608                }
609            }
610            // FIXME(eddyb) not all "interface blocks" imply buffers, so this may
611            // need to be ignored based on the SPIR-V storage class of a `GlobalVar`.
612            //
613            // FIXME(eddyb) but the lowering of operations on pointers depend on
614            // whether the pointer is to a buffer or a data type - without the
615            // way Rust-GPU uses `Generic`, it should at least be possible to
616            // determine from the pointer type itself, at the op lowering time,
617            // but with storage class inference this isn't knowable...
618            //
619            // OTOH, Rust-GPU doesn't really use `Block` outside of buffers, so
620            // it's plausible there could be `qptr` customization options which
621            // Rust-GPU uses to unambiguously communicate its (mis)use of SPIR-V
622            // (long-term it should probably have different Rust types per
623            // storage class, or even represent buffers as Rust pointers?)
624            if is_interface_block {
625                // HACK(eddyb) we need an `AddrSpace` but it's not known yet.
626                TypeLayout::Handle(shapes::Handle::Buffer(AddrSpace::Handles, concrete))
627            } else {
628                TypeLayout::Concrete(concrete)
629            }
630        } else if [
631            wk.OpTypeImage,
632            wk.OpTypeSampler,
633            wk.OpTypeSampledImage,
634            wk.OpTypeAccelerationStructureKHR,
635        ]
636        .contains(&spv_inst.opcode)
637        {
638            TypeLayout::Handle(shapes::Handle::Opaque(ty))
639        } else {
640            return Err(LayoutError(Diag::bug([format!(
641                "unknown/unsupported SPIR-V type `{}`",
642                spv_inst.opcode.name()
643            )
644            .into()])));
645        };
646        self.cache.borrow_mut().insert(ty, layout.clone());
647        Ok(layout)
648    }
649}