Skip to main content

unlit_wgpu/
specialize.rs

1//! Variant caching for pipeline-like values.
2//!
3//! Three types carry it:
4//!
5//! - a [PipelineVariant] is the complete, hashable identity of one compiled
6//!   value and builds the [PipelineDescriptor] it compiles from,
7//! - [Variants] caches the [SpecializedPipeline]s that result, handing the same
8//!   index back the next time a variant is asked for, and
9//! - a [PipelineDescriptor] is the blueprint itself: owned wgpu state that
10//!   compiles into a pipeline of type `P`.
11//!
12//! A variant is the cache key, so it must be injective: equal variants build
13//! equal descriptors, and draws whose descriptors differ resolve to different
14//! variants. Because a variant carries everything its descriptor depends on,
15//! the cache is one map -- no canonical form, no second level.
16//!
17//! There is no full-descriptor cache. A [RenderPipelineDesc] cannot be hashed
18//! -- its compilation constants hold f64 -- and memoizing on a small key is
19//! the point of the type: a family has finitely many variants, and a lookup is
20//! cheaper than comparing whole descriptors. Cached variants are never
21//! evicted, matching the bounded-variant assumption.
22//!
23//! [PipelineDescriptor] is generic over the pipeline it compiles, so one cache
24//! serves a render pipeline and a compute pipeline alike: the descriptor is the
25//! only thing that knows which of the two it builds.
26
27use core::hash::Hash;
28use std::sync::Arc;
29
30use arrayvec::ArrayVec;
31use hashbrown::HashMap;
32use smallvec::SmallVec;
33
34use crate::render_attachments::RenderAttachments;
35use crate::scene::MAX_VERTEX_BUFFERS;
36use crate::util::Hashed;
37
38/// A blueprint that compiles into a pipeline of type `P`.
39///
40/// A [PipelineVariant] builds one of these and [Variants] caches the result.
41/// Not comparable or hashable: it may hold wgpu handles and `f64` constants,
42/// which is why the variant -- not the blueprint -- is the cache key.
43///
44/// Deliberately not `Send`/`Sync`: a descriptor holds wgpu handles, which are
45/// not thread-safe on the web. A cache and the device it creates against live
46/// together and never cross threads, so requiring it would only rule the web
47/// backend out.
48pub trait PipelineDescriptor<P>: Clone {
49    /// Compile this blueprint into a pipeline.
50    fn create(&self, device: &wgpu::Device) -> P;
51}
52
53/// A compiled pipeline together with the descriptor it came from.
54///
55/// This is what a cache hands out: the pipeline to bind, and the descriptor it
56/// was built from, which the renderer reads to discover the bind-group layouts
57/// and vertex streams the pipeline expects. A compiled pipeline does not expose
58/// those, so the blueprint is kept beside it rather than re-derived.
59#[derive(Clone, Debug)]
60pub struct SpecializedPipeline<P, D> {
61    /// The compiled pipeline.
62    pub pipeline: P,
63    /// The descriptor it was compiled from.
64    pub descriptor: D,
65}
66
67impl<P, D: PipelineDescriptor<P>> SpecializedPipeline<P, D> {
68    /// Compile `descriptor` into a pipeline.
69    pub fn create(device: &wgpu::Device, descriptor: D) -> Self {
70        Self {
71            pipeline: descriptor.create(device),
72            descriptor,
73        }
74    }
75
76    /// The descriptor this pipeline was compiled from.
77    pub fn descriptor(&self) -> &D {
78        &self.descriptor
79    }
80}
81
82/// The complete, hashable identity of one compiled pipeline, and the recipe
83/// for its blueprint.
84///
85/// This is the cache key, so it carries exactly the information the descriptor
86/// depends on -- no more, no less -- and must be injective: equal variants
87/// build equal descriptors, and draws whose descriptors differ resolve to
88/// different variants. Equal keys then share one compiled pipeline, which is
89/// why [Variants] needs only a single map.
90///
91/// A variant may be its own descriptor (`type Descriptor = Self`), as the
92/// built-in lit family does, or describe a larger one it does not own.
93pub trait PipelineVariant<P>: Clone + Hash + Eq + 'static {
94    /// The blueprint this variant compiles.
95    type Descriptor: PipelineDescriptor<P>;
96
97    /// Build this variant's blueprint. Called only on a cache miss.
98    ///
99    /// `device` is there for variants whose blueprint depends on device
100    /// capabilities; the cache itself is built for one device.
101    fn descriptor(&self, device: &wgpu::Device) -> Self::Descriptor;
102}
103
104/// A cache of the [SpecializedPipeline]s one family compiles. At most one
105/// pipeline is created per variant.
106///
107/// The variants are stored in creation order; [Self::specialize] returns their
108/// index. The cache never evicts: a family has finitely many variants, so one
109/// that specializes on the mesh's vertex layout, say, holds one pipeline per
110/// distinct layout it has seen.
111pub struct Variants<P, V: PipelineVariant<P>> {
112    /// The device variants are created on.
113    device: wgpu::Device,
114    /// The cache: variant to index into `variants`.
115    keys: HashMap<V, u32>,
116    /// The created variants, indexed by the returned index.
117    variants: Vec<SpecializedPipeline<P, V::Descriptor>>,
118}
119
120impl<P, V: PipelineVariant<P>> Variants<P, V> {
121    /// Create an empty cache.
122    pub fn new(device: &wgpu::Device) -> Self {
123        Self {
124            device: device.clone(),
125            keys: HashMap::new(),
126            variants: Vec::new(),
127        }
128    }
129
130    /// The index of the variant's pipeline, creating it on first use.
131    ///
132    /// Creates at most one pipeline per variant; the blueprint is built only
133    /// on a cache miss.
134    pub fn specialize(&mut self, variant: V) -> u32 {
135        if let Some(&index) = self.keys.get(&variant) {
136            return index;
137        }
138
139        let descriptor = variant.descriptor(&self.device);
140        let specialized = SpecializedPipeline::create(&self.device, descriptor);
141        let index = u32::try_from(self.variants.len()).expect("variant count fits in u32");
142        self.variants.push(specialized);
143        self.keys.insert(variant, index);
144        index
145    }
146
147    /// The variant at the given index.
148    ///
149    /// # Panics
150    /// If the index was not returned by [Self::specialize].
151    pub fn get(&self, index: u32) -> &SpecializedPipeline<P, V::Descriptor> {
152        &self.variants[index as usize]
153    }
154}
155
156/// The attributes of one vertex-buffer layout.
157///
158/// A vertex buffer carries a handful of attributes in practice, so the inline
159/// capacity keeps the ordinary layout free of a heap allocation.
160pub type VertexAttributes = SmallVec<[wgpu::VertexAttribute; 8]>;
161
162/// An owned vertex-buffer layout. The attributes are owned rather than
163/// borrowed, so the layout outlives the descriptor it was read from.
164///
165/// Eq + Hash (beyond the descriptor's own PartialEq) so it can be part of a
166/// specializer key, which is what lets a family re-specialize when a mesh's
167/// vertex layout changes.
168#[derive(Clone, Debug, PartialEq, Eq, Hash)]
169pub struct VertexBufferLayoutDesc {
170    /// The stride, in bytes, between elements of this buffer.
171    pub array_stride: wgpu::BufferAddress,
172    /// How often this vertex buffer is stepped forward.
173    pub step_mode: wgpu::VertexStepMode,
174    /// The attributes that make up one element of this buffer.
175    pub attributes: VertexAttributes,
176}
177
178impl VertexBufferLayoutDesc {
179    /// Own a borrowed layout, cloning its attributes.
180    pub fn from_wgpu(layout: &wgpu::VertexBufferLayout<'_>) -> Self {
181        Self {
182            array_stride: layout.array_stride,
183            step_mode: layout.step_mode,
184            attributes: layout.attributes.into(),
185        }
186    }
187
188    /// Borrow this layout as a wgpu one, for the duration of a pipeline
189    /// creation.
190    pub(crate) fn as_wgpu(&self) -> wgpu::VertexBufferLayout<'_> {
191        wgpu::VertexBufferLayout {
192            array_stride: self.array_stride,
193            step_mode: self.step_mode,
194            attributes: &self.attributes,
195        }
196    }
197}
198
199/// A mesh's vertex-buffer layout, slot by slot, shared rather than copied.
200///
201/// The layout is part of the key a family specializes on, and that key is built
202/// and hashed once per visible entity per frame. A mesh's layout never changes
203/// after upload, so it is interned behind an [`Arc`] with its hash taken once by
204/// [`Hashed`]: cloning a key copies a pointer, hashing it writes one word, and
205/// comparing two keys settles on that word before it looks at an attribute.
206///
207/// Equality is by the buffers themselves — the shared-allocation fast path
208/// aside — so two independently uploaded meshes with the same layout are one
209/// key; equal layouts always carry equal hashes, which is what lets [`Hash`] be
210/// the stored word.
211#[derive(Clone, Debug, PartialEq, Eq, Hash)]
212pub struct VertexLayout {
213    /// The interned buffers and their hash.
214    ///
215    /// [`Hashed`] sits inside the [`Arc`] rather than outside it: hashing a
216    /// `Hashed<Arc<_>>` would forward through the `Arc` to the buffers and walk
217    /// every attribute, which is the cost this type exists to avoid.
218    inner: Arc<Hashed<ArrayVec<(u32, VertexBufferLayoutDesc), MAX_VERTEX_BUFFERS>>>,
219}
220
221impl VertexLayout {
222    /// Intern `buffers`, slot by slot.
223    ///
224    /// # Panics
225    ///
226    /// If more than [MAX_VERTEX_BUFFERS] buffers are given: a draw cannot bind
227    /// more than that.
228    pub fn new(buffers: impl IntoIterator<Item = (u32, VertexBufferLayoutDesc)>) -> Self {
229        let buffers: ArrayVec<_, MAX_VERTEX_BUFFERS> = buffers.into_iter().collect();
230        Self {
231            inner: Arc::new(Hashed::new(buffers)),
232        }
233    }
234}
235
236impl Default for VertexLayout {
237    /// A layout naming no buffer.
238    fn default() -> Self {
239        Self::new([])
240    }
241}
242
243impl core::ops::Deref for VertexLayout {
244    type Target = [(u32, VertexBufferLayoutDesc)];
245
246    fn deref(&self) -> &Self::Target {
247        &self.inner
248    }
249}
250
251/// Owned pipeline compilation options.
252#[derive(Clone, Debug, PartialEq)]
253pub struct CompilationOptions {
254    /// Values of the shader's pipeline-overridable constants.
255    pub constants: Vec<(String, f64)>,
256    /// Whether workgroup-scoped memory is zero-initialized for this stage.
257    pub zero_initialize_workgroup_memory: bool,
258}
259
260impl CompilationOptions {
261    /// Own a borrowed set of compilation options.
262    fn from_wgpu(options: &wgpu::PipelineCompilationOptions<'_>) -> Self {
263        Self {
264            constants: options
265                .constants
266                .iter()
267                .map(|(name, value)| ((*name).to_owned(), *value))
268                .collect(),
269            zero_initialize_workgroup_memory: options.zero_initialize_workgroup_memory,
270        }
271    }
272}
273
274impl Default for CompilationOptions {
275    fn default() -> Self {
276        Self {
277            constants: Vec::new(),
278            zero_initialize_workgroup_memory: true,
279        }
280    }
281}
282
283/// Owned vertex stage of a [RenderPipelineDesc].
284#[derive(Clone, Debug, PartialEq)]
285pub struct VertexStateDesc {
286    /// The compiled shader module for this stage.
287    pub module: wgpu::ShaderModule,
288    /// The name of the entry point to use, or the module's only one.
289    pub entry_point: Option<String>,
290    /// Advanced options for when this stage is compiled.
291    pub compilation_options: CompilationOptions,
292    /// The vertex buffers this stage reads.
293    pub buffers: Vec<Option<VertexBufferLayoutDesc>>,
294}
295
296impl VertexStateDesc {
297    /// Own a borrowed vertex state.
298    fn from_wgpu(state: &wgpu::VertexState<'_>) -> Self {
299        Self {
300            module: state.module.clone(),
301            entry_point: state.entry_point.map(str::to_owned),
302            compilation_options: CompilationOptions::from_wgpu(&state.compilation_options),
303            buffers: state
304                .buffers
305                .iter()
306                .map(|buffer| buffer.as_ref().map(VertexBufferLayoutDesc::from_wgpu))
307                .collect(),
308        }
309    }
310}
311
312/// Owned fragment stage of a [RenderPipelineDesc].
313#[derive(Clone, Debug, PartialEq)]
314pub struct FragmentStateDesc {
315    /// The compiled shader module for this stage.
316    pub module: wgpu::ShaderModule,
317    /// The name of the entry point to use, or the module's only one.
318    pub entry_point: Option<String>,
319    /// Advanced options for when this stage is compiled.
320    pub compilation_options: CompilationOptions,
321    /// The color targets this stage writes.
322    pub targets: Vec<Option<wgpu::ColorTargetState>>,
323}
324
325impl FragmentStateDesc {
326    /// Own a borrowed fragment state.
327    fn from_wgpu(state: &wgpu::FragmentState<'_>) -> Self {
328        Self {
329            module: state.module.clone(),
330            entry_point: state.entry_point.map(str::to_owned),
331            compilation_options: CompilationOptions::from_wgpu(&state.compilation_options),
332            targets: state.targets.to_vec(),
333        }
334    }
335}
336
337/// An owned mirror of wgpu's render-pipeline descriptor, with every borrowed
338/// slice owned so it can outlive the descriptor it was read from.
339///
340/// A specializer rewrites these fields in place, and a
341/// [SpecializedPipeline] stores the result alongside the pipeline it compiles
342/// into.
343#[derive(Clone, Debug, PartialEq)]
344pub struct RenderPipelineDesc {
345    /// Debug label of the pipeline.
346    pub label: Option<String>,
347    /// The bind group layouts this pipeline uses.
348    pub layout: Option<wgpu::PipelineLayout>,
349    /// The vertex stage.
350    pub vertex: VertexStateDesc,
351    /// Primitive assembly and rasterization state.
352    pub primitive: wgpu::PrimitiveState,
353    /// Depth-stencil state, if the pass has a depth attachment.
354    pub depth_stencil: Option<wgpu::DepthStencilState>,
355    /// Multisampling state.
356    pub multisample: wgpu::MultisampleState,
357    /// The fragment stage, if the pipeline writes color.
358    pub fragment: Option<FragmentStateDesc>,
359    /// The multiview mask, if the pipeline renders with multiview.
360    pub multiview_mask: Option<core::num::NonZeroU32>,
361    /// The pipeline cache to compile against.
362    pub cache: Option<wgpu::PipelineCache>,
363}
364
365impl RenderPipelineDesc {
366    /// Own a borrowed render-pipeline descriptor, cloning every slice and
367    /// handle.
368    pub fn from_wgpu(descriptor: &wgpu::RenderPipelineDescriptor<'_>) -> Self {
369        Self {
370            label: descriptor.label.map(str::to_owned),
371            layout: descriptor.layout.cloned(),
372            vertex: VertexStateDesc::from_wgpu(&descriptor.vertex),
373            primitive: descriptor.primitive,
374            depth_stencil: descriptor.depth_stencil.clone(),
375            multisample: descriptor.multisample,
376            fragment: descriptor
377                .fragment
378                .as_ref()
379                .map(FragmentStateDesc::from_wgpu),
380            multiview_mask: descriptor.multiview_mask,
381            cache: descriptor.cache.cloned(),
382        }
383    }
384}
385
386impl PipelineDescriptor<wgpu::RenderPipeline> for RenderPipelineDesc {
387    /// Create the pipeline this descriptor describes.
388    ///
389    /// The borrowed wgpu descriptor is assembled and consumed inside this
390    /// function, so the owned vecs it borrows never outlive the call.
391    fn create(&self, device: &wgpu::Device) -> wgpu::RenderPipeline {
392        let buffers: Vec<Option<wgpu::VertexBufferLayout<'_>>> = self
393            .vertex
394            .buffers
395            .iter()
396            .map(|buffer| buffer.as_ref().map(VertexBufferLayoutDesc::as_wgpu))
397            .collect();
398        let vertex_constants: Vec<(&str, f64)> = self
399            .vertex
400            .compilation_options
401            .constants
402            .iter()
403            .map(|(name, value)| (name.as_str(), *value))
404            .collect();
405        let fragment_constants: Vec<(&str, f64)> = self
406            .fragment
407            .as_ref()
408            .map(|fragment| {
409                fragment
410                    .compilation_options
411                    .constants
412                    .iter()
413                    .map(|(name, value)| (name.as_str(), *value))
414                    .collect()
415            })
416            .unwrap_or_default();
417
418        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
419            label: self.label.as_deref(),
420            layout: self.layout.as_ref(),
421            vertex: wgpu::VertexState {
422                module: &self.vertex.module,
423                entry_point: self.vertex.entry_point.as_deref(),
424                compilation_options: wgpu::PipelineCompilationOptions {
425                    constants: &vertex_constants,
426                    zero_initialize_workgroup_memory: self
427                        .vertex
428                        .compilation_options
429                        .zero_initialize_workgroup_memory,
430                },
431                buffers: &buffers,
432            },
433            primitive: self.primitive,
434            depth_stencil: self.depth_stencil.clone(),
435            multisample: self.multisample,
436            fragment: self.fragment.as_ref().map(|fragment| wgpu::FragmentState {
437                module: &fragment.module,
438                entry_point: fragment.entry_point.as_deref(),
439                compilation_options: wgpu::PipelineCompilationOptions {
440                    constants: &fragment_constants,
441                    zero_initialize_workgroup_memory: fragment
442                        .compilation_options
443                        .zero_initialize_workgroup_memory,
444                },
445                targets: &fragment.targets,
446            }),
447            multiview_mask: self.multiview_mask,
448            cache: self.cache.as_ref(),
449        })
450    }
451}
452
453/// The render target a pipeline is specialized for.
454///
455/// Width and height are excluded: a pipeline does not depend on the size of
456/// the target it draws into.
457#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
458pub struct SurfaceKey {
459    /// The format of the color attachment.
460    pub color_format: wgpu::TextureFormat,
461    /// The format of the depth-stencil attachment, if the pass has one.
462    pub depth_stencil_format: Option<wgpu::TextureFormat>,
463    /// The sample count the pass renders with.
464    pub sample_count: u32,
465}
466
467impl SurfaceKey {
468    /// The surface a set of attachments describes.
469    ///
470    /// The color format and sample count are read from the same attachment
471    /// path, so they stay consistent: the MSAA view when one is set, the color
472    /// view otherwise. A depth-only attachment set has no color view, so its
473    /// key reports Rgba8UnormSrgb as the color format; a depth-only pipeline
474    /// is the caller's to build.
475    pub fn from_attachments(attachments: &RenderAttachments) -> Self {
476        Self {
477            color_format: attachments
478                .color_format()
479                .unwrap_or(wgpu::TextureFormat::Rgba8UnormSrgb),
480            depth_stencil_format: attachments.depth_stencil_format(),
481            sample_count: attachments.sample_count(),
482        }
483    }
484}
485
486/// A descriptor whose pipeline is only valid for one render target.
487///
488/// A pipeline's color format, sample count and depth-stencil format must match
489/// the attachments its pass renders into, so a descriptor that names them has
490/// to be rewritten when the target changes. Implementing this lets a variant
491/// apply a [SurfaceKey] to the blueprint it builds, whichever kind of pipeline
492/// it compiles; the framework itself neither knows nor caches the target.
493pub trait SurfaceTarget {
494    /// Rewrite this descriptor's target-dependent fields for `surface`.
495    fn set_surface(&mut self, surface: SurfaceKey);
496}
497
498impl SurfaceTarget for RenderPipelineDesc {
499    /// The color format and sample count reach every fragment target, and the
500    /// depth format the depth-stencil state.
501    ///
502    /// A target with no depth attachment yields a descriptor with no depth
503    /// state: `wgpu` rejects a pipeline that declares one against a pass that
504    /// has none, so the state follows the target rather than keeping a stale
505    /// format. A target that has one keeps whatever state the descriptor
506    /// carried — the comparison, the write mask — and only takes the format.
507    fn set_surface(&mut self, surface: SurfaceKey) {
508        self.multisample.count = surface.sample_count;
509        if let Some(fragment) = &mut self.fragment {
510            for target in fragment.targets.iter_mut().flatten() {
511                target.format = surface.color_format;
512            }
513        }
514        match surface.depth_stencil_format {
515            Some(format) => {
516                if let Some(depth_stencil) = &mut self.depth_stencil {
517                    depth_stencil.format = format;
518                }
519            }
520            None => self.depth_stencil = None,
521        }
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use std::collections::hash_map::DefaultHasher;
528    use std::hash::Hasher;
529
530    use super::*;
531
532    /// A blueprint a variant builds.
533    #[derive(Clone, Debug, PartialEq)]
534    struct TestDescriptor {
535        /// The value every variant starts from.
536        base: u32,
537        /// The value the variant's id becomes.
538        rewritten: u32,
539    }
540
541    /// A pipeline the descriptor compiles into. The noop backend makes a
542    /// stand-in value cheap, and the descriptor is what the tests assert on.
543    type TestPipeline = u64;
544
545    impl PipelineDescriptor<TestPipeline> for TestDescriptor {
546        fn create(&self, _device: &wgpu::Device) -> TestPipeline {
547            u64::from(self.rewritten)
548        }
549    }
550
551    /// A variant whose id is exactly what reaches the descriptor.
552    #[derive(Clone, Debug, PartialEq, Eq, Hash)]
553    struct TestVariant {
554        id: u32,
555    }
556
557    impl PipelineVariant<TestPipeline> for TestVariant {
558        type Descriptor = TestDescriptor;
559
560        fn descriptor(&self, _device: &wgpu::Device) -> TestDescriptor {
561            TestDescriptor {
562                base: 7,
563                rewritten: self.id,
564            }
565        }
566    }
567
568    fn variants() -> Variants<TestPipeline, TestVariant> {
569        let (device, _queue) = crate::util::test::noop_device();
570        Variants::new(&device)
571    }
572
573    /// How many variants the cache has created.
574    fn created<P, V: PipelineVariant<P>>(variants: &Variants<P, V>) -> usize {
575        variants.variants.len()
576    }
577
578    #[test]
579    fn variant_is_created_once_per_variant() {
580        let mut variants = variants();
581
582        let first = variants.specialize(TestVariant { id: 1 });
583        let again = variants.specialize(TestVariant { id: 1 });
584        assert_eq!(first, again, "the same variant reuses its pipeline");
585        assert_eq!(created(&variants), 1);
586
587        let second = variants.specialize(TestVariant { id: 2 });
588        assert_ne!(first, second, "a different variant gets a new pipeline");
589        assert_eq!(created(&variants), 2);
590    }
591
592    #[test]
593    fn each_variant_keeps_its_own_descriptor() {
594        let mut variants = variants();
595
596        let first = variants.specialize(TestVariant { id: 1 });
597        let second = variants.specialize(TestVariant { id: 2 });
598
599        // The blueprint is built from the variant alone, so one miss cannot
600        // leak into another.
601        assert_eq!(
602            variants.get(first).descriptor(),
603            &TestDescriptor {
604                base: 7,
605                rewritten: 1,
606            }
607        );
608        assert_eq!(
609            variants.get(second).descriptor(),
610            &TestDescriptor {
611                base: 7,
612                rewritten: 2,
613            }
614        );
615    }
616
617    const MINIMAL_WGSL: &str = "\
618@vertex
619fn vs_main() -> @builtin(position) vec4<f32> {
620    return vec4<f32>(0.0, 0.0, 0.0, 1.0);
621}
622
623@fragment
624fn fs_main() -> @location(0) vec4<f32> {
625    return vec4<f32>(1.0);
626}
627";
628
629    /// The compute shader the pipeline-agnostic cache test compiles.
630    const COMPUTE_WGSL: &str = "\
631@compute @workgroup_size(1)
632fn main() {
633}
634";
635
636    /// A pipeline descriptor borrowing its module and targets.
637    fn source_descriptor<'a>(
638        module: &'a wgpu::ShaderModule,
639        targets: &'a [Option<wgpu::ColorTargetState>],
640    ) -> wgpu::RenderPipelineDescriptor<'a> {
641        wgpu::RenderPipelineDescriptor {
642            label: Some("test"),
643            layout: None,
644            vertex: wgpu::VertexState {
645                module,
646                entry_point: Some("vs_main"),
647                compilation_options: Default::default(),
648                buffers: &[],
649            },
650            primitive: Default::default(),
651            depth_stencil: None,
652            multisample: Default::default(),
653            fragment: Some(wgpu::FragmentState {
654                module,
655                entry_point: Some("fs_main"),
656                compilation_options: Default::default(),
657                targets,
658            }),
659            multiview_mask: None,
660            cache: None,
661        }
662    }
663
664    #[test]
665    fn render_pipeline_desc_survives_its_source() {
666        let (device, _queue) = crate::util::test::noop_device();
667        let module = crate::util::shader_module(&device, MINIMAL_WGSL, "test::shader");
668
669        let owned = {
670            let targets = [Some(wgpu::ColorTargetState {
671                format: wgpu::TextureFormat::Rgba8UnormSrgb,
672                blend: None,
673                write_mask: wgpu::ColorWrites::ALL,
674            })];
675            let source = source_descriptor(&module, &targets);
676            RenderPipelineDesc::from_wgpu(&source)
677        };
678
679        // The source descriptor and the slices it borrowed are gone; the owned
680        // descriptor still creates a pipeline.
681        let _pipeline = owned.create(&device);
682    }
683
684    #[test]
685    fn vertex_buffer_layout_desc_round_trips() {
686        let attributes = [
687            wgpu::VertexAttribute {
688                format: wgpu::VertexFormat::Float32x3,
689                offset: 0,
690                shader_location: 0,
691            },
692            wgpu::VertexAttribute {
693                format: wgpu::VertexFormat::Unorm8x4,
694                offset: 12,
695                shader_location: 1,
696            },
697        ];
698        let layout = wgpu::VertexBufferLayout {
699            array_stride: 16,
700            step_mode: wgpu::VertexStepMode::Vertex,
701            attributes: &attributes,
702        };
703
704        let owned = VertexBufferLayoutDesc::from_wgpu(&layout);
705        let round_tripped = owned.as_wgpu();
706
707        assert_eq!(round_tripped.array_stride, layout.array_stride);
708        assert_eq!(round_tripped.step_mode, layout.step_mode);
709        assert_eq!(round_tripped.attributes, layout.attributes);
710    }
711
712    /// The pipeline type parameter is what makes the cache pipeline-agnostic:
713    /// a descriptor that compiles a compute pipeline caches through the same
714    /// [Variants] a render descriptor does.
715    #[test]
716    fn a_compute_pipeline_variant_is_cached() {
717        /// A blueprint for a compute pipeline, which knows only a workgroup
718        /// size.
719        #[derive(Clone, Debug, PartialEq)]
720        struct ComputeDesc {
721            workgroup: u32,
722        }
723
724        impl PipelineDescriptor<wgpu::ComputePipeline> for ComputeDesc {
725            fn create(&self, device: &wgpu::Device) -> wgpu::ComputePipeline {
726                let module = crate::util::shader_module(device, COMPUTE_WGSL, "test::compute");
727                let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
728                    label: Some("test::compute::layout"),
729                    bind_group_layouts: &[],
730                    immediate_size: 0,
731                });
732                let _ = self.workgroup;
733                device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
734                    label: Some("test::compute"),
735                    layout: Some(&layout),
736                    module: &module,
737                    entry_point: Some("main"),
738                    compilation_options: Default::default(),
739                    cache: None,
740                })
741            }
742        }
743
744        /// A variant whose workgroup size is exactly what reaches the
745        /// descriptor.
746        #[derive(Clone, Debug, PartialEq, Eq, Hash)]
747        struct WorkgroupVariant {
748            workgroup: u32,
749        }
750
751        impl PipelineVariant<wgpu::ComputePipeline> for WorkgroupVariant {
752            type Descriptor = ComputeDesc;
753
754            fn descriptor(&self, _device: &wgpu::Device) -> ComputeDesc {
755                ComputeDesc {
756                    workgroup: self.workgroup,
757                }
758            }
759        }
760
761        let (device, _queue) = crate::util::test::noop_device();
762        let mut variants = Variants::new(&device);
763
764        let first = variants.specialize(WorkgroupVariant { workgroup: 8 });
765        assert_eq!(
766            variants.specialize(WorkgroupVariant { workgroup: 8 }),
767            first,
768            "one variant per workgroup size"
769        );
770        assert_ne!(
771            variants.specialize(WorkgroupVariant { workgroup: 16 }),
772            first
773        );
774        assert_eq!(created(&variants), 2);
775    }
776
777    fn layout(array_stride: u64) -> VertexBufferLayoutDesc {
778        VertexBufferLayoutDesc {
779            array_stride,
780            step_mode: wgpu::VertexStepMode::Vertex,
781            attributes: VertexAttributes::new(),
782        }
783    }
784
785    fn hash_of(layout: &VertexLayout) -> u64 {
786        let mut hasher = DefaultHasher::new();
787        layout.hash(&mut hasher);
788        hasher.finish()
789    }
790
791    #[test]
792    fn layouts_with_equal_buffers_are_equal_and_hash_alike() {
793        let one = VertexLayout::new([(0, layout(12)), (1, layout(24))]);
794        let other = VertexLayout::new([(0, layout(12)), (1, layout(24))]);
795
796        assert_eq!(one, other, "separately built layouts compare equal");
797        assert_eq!(
798            hash_of(&one),
799            hash_of(&other),
800            "equal layouts must hash alike, since the hash is precomputed"
801        );
802    }
803
804    #[test]
805    fn layouts_that_differ_are_not_equal() {
806        let one = VertexLayout::new([(0, layout(12))]);
807        let other = VertexLayout::new([(0, layout(16))]);
808
809        assert_ne!(one, other);
810    }
811
812    /// The hash is stored, so it is only correct if it was computed over the
813    /// buffers: an empty layout must not collide with a populated one by
814    /// construction.
815    #[test]
816    fn an_empty_layout_is_not_equal_to_a_populated_one() {
817        assert_ne!(
818            VertexLayout::default(),
819            VertexLayout::new([(0, layout(12))])
820        );
821    }
822}