Skip to main content

frust_gpu/
pipeline.rs

1//! Render-pipeline variants and the cache that builds them predictably.
2//!
3//! The philosophy is Impeller's: a frame must never be the first place a
4//! pipeline state object gets compiled. Three pieces implement that here.
5//!
6//! - [`RenderPipelineDesc`] is a **value** describing one pipeline variant —
7//!   a [`ShaderId`] plus entry points, vertex layouts, topology, the four
8//!   render-state axes (`blend`, `format`, `sample_count`, `depth`) and an
9//!   optional explicit pipeline layout. Apart from that last, reference-counted
10//!   `wgpu::PipelineLayout` handle it holds no GPU object, so it can be listed
11//!   up front, sent to a worker thread, and hashed.
12//! - [`PipelineCache::warm_up`] takes a list of those descs and builds them on
13//!   a worker thread when the surface is installed, so the variants an app is
14//!   known to need exist before the first frame asks for one. At most one such
15//!   worker is ever alive per cache, and [`PipelineCache::shutdown`] — which
16//!   `Drop` calls — joins it, so no thread holding a clone of the device
17//!   outlives the cache.
18//! - [`PipelineCache::get_or_create`] compiles a variant at most once. If the
19//!   render thread asks for a variant that is still *queued* for the warm-up
20//!   worker, it does not wait its turn: it **steals** the job, builds it
21//!   inline, and marks the queue entry done (Impeller's
22//!   `PipelineCompileQueue::PerformJobEagerly`). If the worker is already
23//!   mid-build on that exact variant, the request waits for it rather than
24//!   compiling a second copy.
25//!
26//! ## The variant key
27//!
28//! Every desc collapses to a `u64` packed from six axes, the same trick as
29//! Impeller's `ContentContextOptions::ToKey`: fixed-width bit fields, one per
30//! axis, so lookup is an integer hash and two descs that differ in any axis
31//! can never collide.
32//!
33//! | Bits | Axis |
34//! |------|------|
35//! | `[0..16)`  | program — shader module + entry points + vertex layouts + topology |
36//! | `[16..28)` | color target format |
37//! | `[28..40)` | blend state (`None` is its own value) |
38//! | `[40..52)` | depth/stencil state (`None` is its own value) |
39//! | `[52..56)` | `log2(sample_count)` |
40//! | `[56..64)` | pipeline layout (`None` — wgpu's derived layout — is its own value) |
41//!
42//! wgpu's render-state types are far too wide to pack literally (a
43//! `BlendState` alone is six enums), so the four wide axes are **interned**:
44//! each distinct value seen gets the next index in a small side table, and the
45//! index is what the bit field holds. That is exact — unlike a truncated
46//! content hash, no two distinct values can ever share a field — at the cost
47//! of a key being meaningful only within one cache instance, which is all it
48//! is ever used for.
49//!
50//! ## The layout axis
51//!
52//! A desc whose `layout` is `None` builds against wgpu's **derived** layout —
53//! the bind-group layout deduced from the bindings each entry point statically
54//! uses. That is the right default, and it is also why a family of programs
55//! specialised from one source needs the explicit axis: a specialisation that
56//! stops reading a binding derives a *different* layout, and a bind group
57//! built against one sibling's layout no longer fits the other. Naming one
58//! explicit `wgpu::PipelineLayout` on every member keeps their bind groups
59//! interchangeable. The layout is a reference-counted handle compared by
60//! identity (wgpu's own `Eq`/`Hash` on it), so two descs naming the same
61//! layout object share the field and two structurally equal but distinct
62//! layout objects do not — which is exactly when wgpu would treat their bind
63//! groups as incompatible too.
64//!
65//! The first axis is the whole *program*, not just the [`ShaderId`], because a
66//! variant that reused one module under a different entry point or vertex
67//! layout would otherwise share a key with its sibling and silently get the
68//! wrong pipeline. Interning the program tuple makes those distinct variants,
69//! which is what Impeller's per-pipeline-type maps achieve structurally.
70//!
71//! A field whose table overflows its width (or a `sample_count` that is not a
72//! power of two, which wgpu would reject anyway) yields no key: that request
73//! falls back to an uncached compile rather than risking a wrong hit. It takes
74//! thousands of distinct blend states to get there, so it is a safety net, not
75//! a path.
76//!
77//! ## The warm-up worker
78//!
79//! One worker at a time, spawned on demand and **exiting when the queue
80//! drains** rather than parking: a surface reinstall calls `warm_up` again,
81//! and the choice keeps a long-lived cache from holding an idle thread (and a
82//! device clone) for the process lifetime. The liveness flag is set and
83//! cleared inside the same critical section that enqueues jobs and claims
84//! them, so a worker on its way out can never be mistaken for one that will
85//! still pick up the job just queued.
86//!
87//! A compile that unwinds marks its variant `JobState::Failed` instead of
88//! leaving it claimable-but-unreachable: the queue entry is already spent, so
89//! a `Pending` job nobody re-queues is a job the worker can never build again
90//! and `queued_variants` would count forever. `Failed` is terminal for the
91//! *queue* only — the next [`PipelineCache::get_or_create`] for that variant
92//! rebuilds it, and a second failure unwinds into that caller rather than
93//! parking it on a build that will never publish. The worker catches the
94//! unwind so one bad variant costs its own pipeline and not the rest of the
95//! queue; an inline build does not, because there the requester is the right
96//! place for the failure to land.
97//!
98//! ## Persisted driver cache
99//!
100//! A [`wgpu::PipelineCache`] — Vulkan-only, seeded from a shell-persisted blob
101//! validated by [`crate::pipeline_cache`] — can be handed to
102//! [`PipelineCache::new`]. Every pipeline built here is then created against
103//! it, so a warm start reuses the driver's previously compiled machine code.
104//! On Metal/DX12 the feature does not exist and `None` is passed; nothing else
105//! changes.
106
107use std::borrow::Cow;
108use std::collections::{HashMap, VecDeque};
109use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
110
111use crate::shader::{ShaderId, ShaderLibrary};
112
113/// Width of the packed key's program field.
114const PROGRAM_BITS: u32 = 16;
115/// Width of the packed key's color-format field.
116const FORMAT_BITS: u32 = 12;
117/// Width of the packed key's blend-state field.
118const BLEND_BITS: u32 = 12;
119/// Width of the packed key's depth/stencil field.
120const DEPTH_BITS: u32 = 12;
121/// Width of the packed key's `log2(sample_count)` field.
122const SAMPLE_BITS: u32 = 4;
123/// Width of the packed key's pipeline-layout field.
124const LAYOUT_BITS: u32 = 8;
125
126const PROGRAM_SHIFT: u32 = 0;
127const FORMAT_SHIFT: u32 = PROGRAM_SHIFT + PROGRAM_BITS;
128const BLEND_SHIFT: u32 = FORMAT_SHIFT + FORMAT_BITS;
129const DEPTH_SHIFT: u32 = BLEND_SHIFT + BLEND_BITS;
130const SAMPLE_SHIFT: u32 = DEPTH_SHIFT + DEPTH_BITS;
131const LAYOUT_SHIFT: u32 = SAMPLE_SHIFT + SAMPLE_BITS;
132
133const _: () = assert!(
134    LAYOUT_SHIFT + LAYOUT_BITS <= u64::BITS,
135    "the packed variant key must fit one u64",
136);
137
138/// One vertex buffer's layout, owned so a [`RenderPipelineDesc`] can be stored,
139/// cloned, hashed and sent to the warm-up worker.
140///
141/// The borrowed `wgpu::VertexBufferLayout` this converts to
142/// ([`VertexLayout::as_wgpu`]) is built only for the duration of the
143/// `create_render_pipeline` call.
144#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
145pub struct VertexLayout {
146    /// Stride in bytes between consecutive elements.
147    pub array_stride: wgpu::BufferAddress,
148    /// Whether the buffer steps per vertex or per instance.
149    pub step_mode: wgpu::VertexStepMode,
150    /// The attributes making up one element.
151    pub attributes: Vec<wgpu::VertexAttribute>,
152}
153
154impl VertexLayout {
155    /// A layout stepped per vertex (the common case) over `attributes`.
156    #[must_use]
157    pub fn per_vertex(
158        array_stride: wgpu::BufferAddress,
159        attributes: Vec<wgpu::VertexAttribute>,
160    ) -> Self {
161        Self {
162            array_stride,
163            step_mode: wgpu::VertexStepMode::Vertex,
164            attributes,
165        }
166    }
167
168    /// The borrowed wgpu view of this layout.
169    #[must_use]
170    pub fn as_wgpu(&self) -> wgpu::VertexBufferLayout<'_> {
171        wgpu::VertexBufferLayout {
172            array_stride: self.array_stride,
173            step_mode: self.step_mode,
174            attributes: &self.attributes,
175        }
176    }
177}
178
179/// One render-pipeline variant, as a plain hashable value.
180///
181/// Build the descs an app needs at start-up, hand them to
182/// [`PipelineCache::warm_up`], and keep them around: a per-frame
183/// [`PipelineCache::get_or_create`] takes one by reference and never allocates
184/// on a hit.
185///
186/// The pipeline's bind-group layout is wgpu's **default layout**, deduced
187/// from the shader module, unless [`Self::layout`] names an explicit one —
188/// see the module docs' *The layout axis* for when a variant set needs that.
189#[derive(Clone, Debug, PartialEq, Eq, Hash)]
190pub struct RenderPipelineDesc {
191    /// The module holding both entry points, from the cache's own library.
192    pub shader: ShaderId,
193    /// Vertex entry point name.
194    pub vs: Cow<'static, str>,
195    /// Fragment entry point name.
196    pub fs: Cow<'static, str>,
197    /// Vertex buffer layouts, in bind order. Empty for a shader that
198    /// generates its own vertices (a fullscreen triangle).
199    pub vertex_layouts: Vec<VertexLayout>,
200    /// Color blending, or `None` for an opaque write.
201    pub blend: Option<wgpu::BlendState>,
202    /// Color target format.
203    pub format: wgpu::TextureFormat,
204    /// MSAA sample count; must be a power of two, `1` for no MSAA.
205    pub sample_count: u32,
206    /// Depth/stencil state, or `None` for a color-only pass.
207    pub depth: Option<wgpu::DepthStencilState>,
208    /// Primitive topology. Strip topologies here are non-indexed only: the
209    /// desc carries no `strip_index_format`.
210    pub topology: wgpu::PrimitiveTopology,
211    /// An explicit pipeline layout, or `None` for wgpu's layout derived from
212    /// the shader module.
213    ///
214    /// Set it when several programs must accept the same bind groups even
215    /// though they do not all read every binding (see the module docs' *The
216    /// layout axis*). Compared and hashed by handle identity.
217    pub layout: Option<wgpu::PipelineLayout>,
218}
219
220impl RenderPipelineDesc {
221    /// A minimal opaque variant: no vertex buffers, no blending, no depth,
222    /// `sample_count: 1`, triangle list. Set the axes that differ with struct
223    /// update syntax.
224    #[must_use]
225    pub fn new(
226        shader: ShaderId,
227        vs: impl Into<Cow<'static, str>>,
228        fs: impl Into<Cow<'static, str>>,
229        format: wgpu::TextureFormat,
230    ) -> Self {
231        Self {
232            shader,
233            vs: vs.into(),
234            fs: fs.into(),
235            vertex_layouts: Vec::new(),
236            blend: None,
237            format,
238            sample_count: 1,
239            depth: None,
240            topology: wgpu::PrimitiveTopology::TriangleList,
241            layout: None,
242        }
243    }
244}
245
246/// The part of a desc that identifies the *program*: everything the packed
247/// key's first field stands for.
248#[derive(Clone, Debug, PartialEq, Eq, Hash)]
249struct ProgramKey {
250    shader: ShaderId,
251    vs: Cow<'static, str>,
252    fs: Cow<'static, str>,
253    vertex_layouts: Vec<VertexLayout>,
254    topology: wgpu::PrimitiveTopology,
255}
256
257impl ProgramKey {
258    fn of(desc: &RenderPipelineDesc) -> Self {
259        Self {
260            shader: desc.shader,
261            vs: desc.vs.clone(),
262            fs: desc.fs.clone(),
263            vertex_layouts: desc.vertex_layouts.clone(),
264            topology: desc.topology,
265        }
266    }
267
268    /// Whether `desc` names this same program, without building a `ProgramKey`
269    /// (and therefore without allocating) — the per-request hot path.
270    fn matches(&self, desc: &RenderPipelineDesc) -> bool {
271        self.shader == desc.shader
272            && self.vs == desc.vs
273            && self.fs == desc.fs
274            && self.topology == desc.topology
275            && self.vertex_layouts == desc.vertex_layouts
276    }
277}
278
279/// Assigns each distinct value of one key axis a dense index.
280///
281/// `None` is returned once the assigned indices would no longer fit the axis's
282/// bit field; the caller then treats the whole key as unavailable.
283#[derive(Debug)]
284struct AxisTable<K> {
285    index: HashMap<K, u64>,
286    bits: u32,
287}
288
289impl<K: std::hash::Hash + Eq + Clone> AxisTable<K> {
290    fn new(bits: u32) -> Self {
291        Self {
292            index: HashMap::new(),
293            bits,
294        }
295    }
296
297    fn intern(&mut self, value: &K) -> Option<u64> {
298        if let Some(existing) = self.index.get(value) {
299            return Some(*existing);
300        }
301        let next = self.index.len() as u64;
302        if next >= 1 << self.bits {
303            return None;
304        }
305        self.index.insert(value.clone(), next);
306        Some(next)
307    }
308}
309
310/// The program axis's own table: a hash index onto stored [`ProgramKey`]s, so
311/// a repeat lookup compares rather than clones.
312#[derive(Debug, Default)]
313struct ProgramTable {
314    /// Program-tuple hash → the indices assigned to programs with that hash.
315    /// A chain longer than one entry means a genuine hash collision, which the
316    /// stored key comparison then resolves.
317    by_hash: HashMap<u64, Vec<u64>>,
318    entries: Vec<ProgramKey>,
319}
320
321impl ProgramTable {
322    fn intern(&mut self, desc: &RenderPipelineDesc) -> Option<u64> {
323        let hash = program_hash(desc);
324        let chain = self.by_hash.entry(hash).or_default();
325        for &candidate in chain.iter() {
326            if self.entries[candidate as usize].matches(desc) {
327                return Some(candidate);
328            }
329        }
330        let next = self.entries.len() as u64;
331        if next >= 1 << PROGRAM_BITS {
332            return None;
333        }
334        chain.push(next);
335        self.entries.push(ProgramKey::of(desc));
336        Some(next)
337    }
338}
339
340/// Hashes a desc's program fields without building a [`ProgramKey`].
341fn program_hash(desc: &RenderPipelineDesc) -> u64 {
342    use std::hash::{Hash, Hasher};
343    let mut hasher = std::collections::hash_map::DefaultHasher::new();
344    desc.shader.hash(&mut hasher);
345    desc.vs.hash(&mut hasher);
346    desc.fs.hash(&mut hasher);
347    desc.vertex_layouts.hash(&mut hasher);
348    desc.topology.hash(&mut hasher);
349    hasher.finish()
350}
351
352/// Packs a [`RenderPipelineDesc`] into the `u64` variant key.
353#[derive(Debug)]
354struct KeyPacker {
355    programs: ProgramTable,
356    formats: AxisTable<wgpu::TextureFormat>,
357    blends: AxisTable<Option<wgpu::BlendState>>,
358    depths: AxisTable<Option<wgpu::DepthStencilState>>,
359    layouts: AxisTable<Option<wgpu::PipelineLayout>>,
360}
361
362impl Default for KeyPacker {
363    fn default() -> Self {
364        Self {
365            programs: ProgramTable::default(),
366            formats: AxisTable::new(FORMAT_BITS),
367            blends: AxisTable::new(BLEND_BITS),
368            depths: AxisTable::new(DEPTH_BITS),
369            layouts: AxisTable::new(LAYOUT_BITS),
370        }
371    }
372}
373
374impl KeyPacker {
375    /// The variant key for `desc`, or `None` if an axis cannot be represented
376    /// (an unusable `sample_count`, or an exhausted interning table).
377    fn key_for(&mut self, desc: &RenderPipelineDesc) -> Option<u64> {
378        // Resolved first so a rejected sample count never leaves a fresh entry
379        // behind in a table.
380        let sample = sample_field(desc.sample_count)?;
381        let program = self.programs.intern(desc)?;
382        let format = self.formats.intern(&desc.format)?;
383        let blend = self.blends.intern(&desc.blend)?;
384        let depth = self.depths.intern(&desc.depth)?;
385        let layout = self.layouts.intern(&desc.layout)?;
386        Some(
387            (program << PROGRAM_SHIFT)
388                | (format << FORMAT_SHIFT)
389                | (blend << BLEND_SHIFT)
390                | (depth << DEPTH_SHIFT)
391                | (sample << SAMPLE_SHIFT)
392                | (layout << LAYOUT_SHIFT),
393        )
394    }
395}
396
397/// `log2(sample_count)`, or `None` for a count wgpu could not use anyway.
398fn sample_field(sample_count: u32) -> Option<u64> {
399    if sample_count == 0 || !sample_count.is_power_of_two() {
400        return None;
401    }
402    let field = u64::from(sample_count.trailing_zeros());
403    (field < 1 << SAMPLE_BITS).then_some(field)
404}
405
406/// Where a queued warm-up job has got to.
407#[derive(Clone, Copy, Debug, PartialEq, Eq)]
408enum JobState {
409    /// Listed by `warm_up`, nobody has started it.
410    Pending,
411    /// Some thread is compiling it right now.
412    Building,
413    /// Built; the pipeline is in `Inner::built`.
414    Done,
415    /// A compile of this variant unwound. Terminal as far as the warm-up
416    /// queue is concerned — its place in the queue is spent, so no worker
417    /// will claim it again — but a later request rebuilds it inline.
418    Failed,
419}
420
421/// One warm-up job: the variant to build and how far along it is.
422#[derive(Debug)]
423struct Job {
424    desc: RenderPipelineDesc,
425    state: JobState,
426}
427
428/// Everything both the render thread and the warm-up worker touch.
429#[derive(Debug)]
430struct Inner<P> {
431    built: HashMap<u64, Arc<P>>,
432    jobs: HashMap<u64, Job>,
433    /// Keys in the order `warm_up` listed them. Entries whose job was stolen
434    /// or finished are skipped when popped.
435    queue: VecDeque<u64>,
436    /// How many compiles have actually run — the "compiled once" evidence.
437    compiles: u64,
438    /// Whether a warm-up worker is draining `queue` right now. Only ever
439    /// written while the lock is held, which is what makes "one worker" a
440    /// fact rather than a race: see the module docs.
441    worker_live: bool,
442    /// Set once the cache is shutting down; a worker claims nothing more, so
443    /// teardown waits for at most the compile already in flight.
444    shutdown: bool,
445    /// Bumped every time liveness is claimed, so a departing worker can tell
446    /// whether the flag it is about to clear is still its own.
447    worker_epoch: u64,
448    /// How many warm-up jobs the worker has finished building.
449    warm_finished: u64,
450}
451
452impl<P> Default for Inner<P> {
453    fn default() -> Self {
454        Self {
455            built: HashMap::new(),
456            jobs: HashMap::new(),
457            queue: VecDeque::new(),
458            compiles: 0,
459            worker_live: false,
460            shutdown: false,
461            worker_epoch: 0,
462            warm_finished: 0,
463        }
464    }
465}
466
467impl<P> Inner<P> {
468    /// Claims the next still-pending job, marking it `Building`.
469    ///
470    /// Returning `None` also clears `worker_live`, in this same critical
471    /// section: a caller that enqueues after this point sees no live worker
472    /// and spawns one, so the job it just listed cannot fall between the
473    /// worker's last look at the queue and its exit.
474    fn claim_next_pending(&mut self) -> Option<(u64, RenderPipelineDesc)> {
475        while !self.shutdown
476            && let Some(key) = self.queue.pop_front()
477        {
478            if let Some(job) = self.jobs.get_mut(&key)
479                && job.state == JobState::Pending
480            {
481                job.state = JobState::Building;
482                return Some((key, job.desc.clone()));
483            }
484        }
485        self.worker_live = false;
486        None
487    }
488}
489
490/// The shared half of a cache: state plus the condition variable a waiter
491/// blocks on while another thread builds the variant it wants.
492#[derive(Debug)]
493struct Shared<P> {
494    inner: Mutex<Inner<P>>,
495    built: Condvar,
496}
497
498impl<P> Default for Shared<P> {
499    fn default() -> Self {
500        Self {
501            inner: Mutex::new(Inner::default()),
502            built: Condvar::new(),
503        }
504    }
505}
506
507/// Locks a mutex, adopting the guard even if a previous holder panicked — this
508/// cache never leaves inconsistent state behind, and a poisoned lock must not
509/// turn a warm-up hiccup into a crash.
510fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
511    mutex.lock().unwrap_or_else(PoisonError::into_inner)
512}
513
514/// Holds a job `Building` for the duration of one compile, and marks it
515/// [`JobState::Failed`] if that compile unwinds — without this a panicking
516/// builder would leave every waiter blocked on a build that will never finish.
517///
518/// `Failed` rather than `Pending` because the job's place in the warm-up queue
519/// is already spent: a `Pending` entry no worker can reach is a phantom that
520/// `queued_variants` would report for the process lifetime. The failure stays
521/// visible to the next requester, which rebuilds the variant inline.
522struct BuildGuard<'a, P> {
523    shared: &'a Shared<P>,
524    key: u64,
525    armed: bool,
526}
527
528impl<'a, P> BuildGuard<'a, P> {
529    fn new(shared: &'a Shared<P>, key: u64) -> Self {
530        Self {
531            shared,
532            key,
533            armed: true,
534        }
535    }
536
537    /// Publishes the finished pipeline, marks the job done, and wakes waiters.
538    fn publish(mut self, pipeline: Arc<P>) {
539        self.armed = false;
540        let mut inner = lock(&self.shared.inner);
541        inner.built.insert(self.key, pipeline);
542        if let Some(job) = inner.jobs.get_mut(&self.key) {
543            job.state = JobState::Done;
544        }
545        inner.compiles += 1;
546        drop(inner);
547        self.shared.built.notify_all();
548    }
549}
550
551/// Clears the worker-liveness flag if a drain leaves by any route other than
552/// the ordinary "queue empty" one, which clears it under the lock itself.
553///
554/// The epoch check is what stops a departing worker from clearing liveness
555/// that a *newer* worker has since claimed — without it, an abnormal exit
556/// racing a fresh `warm_up` could leave two workers on one queue.
557struct LiveWorkerGuard<'a, P> {
558    shared: &'a Shared<P>,
559    epoch: u64,
560}
561
562impl<P> Drop for LiveWorkerGuard<'_, P> {
563    fn drop(&mut self) {
564        let mut inner = lock(&self.shared.inner);
565        if inner.worker_epoch == self.epoch {
566            inner.worker_live = false;
567        }
568    }
569}
570
571impl<P> Drop for BuildGuard<'_, P> {
572    fn drop(&mut self) {
573        if !self.armed {
574            return;
575        }
576        let mut inner = lock(&self.shared.inner);
577        if let Some(job) = inner.jobs.get_mut(&self.key) {
578            job.state = JobState::Failed;
579        }
580        drop(inner);
581        self.shared.built.notify_all();
582    }
583}
584
585/// The device-free half of [`PipelineCache`]: key packing, the warm-up queue,
586/// eager stealing, and the render-thread mirror.
587///
588/// Generic over the compiled artifact so all of that policy is exercised by
589/// host tests with a fake compile function, with no GPU in the loop — the same
590/// split [`crate::caps::TierCaps`] uses to keep tier decisions testable.
591#[derive(Debug)]
592struct VariantCache<P> {
593    shared: Arc<Shared<P>>,
594    /// Render-thread mirror of `Shared::built`, so a steady-state hit takes no
595    /// lock and can hand out a plain reference.
596    local: HashMap<u64, Arc<P>>,
597    /// The most recent uncached compile (see the key-exhaustion note on the
598    /// module docs). Held only so a reference can be returned.
599    overflow: Option<Arc<P>>,
600    keys: KeyPacker,
601    warned_key_space: bool,
602    /// The one warm-up worker, kept so [`VariantCache::shutdown`] can join it.
603    worker: Option<std::thread::JoinHandle<()>>,
604    /// Time the render thread spent inside `resolve` since it was last taken:
605    /// waiting on a `Building` variant, or stealing and building a `Pending`
606    /// or unlisted one inline.
607    #[cfg(feature = "perf-trace")]
608    resolve_wait: std::time::Duration,
609}
610
611impl<P> Default for VariantCache<P> {
612    fn default() -> Self {
613        Self {
614            shared: Arc::new(Shared::default()),
615            local: HashMap::new(),
616            overflow: None,
617            keys: KeyPacker::default(),
618            warned_key_space: false,
619            worker: None,
620            #[cfg(feature = "perf-trace")]
621            resolve_wait: std::time::Duration::ZERO,
622        }
623    }
624}
625
626impl<P> Drop for VariantCache<P> {
627    fn drop(&mut self) {
628        self.shutdown();
629    }
630}
631
632impl<P> VariantCache<P> {
633    /// A handle on the shared state, for a warm-up worker.
634    fn shared(&self) -> Arc<Shared<P>> {
635        Arc::clone(&self.shared)
636    }
637
638    /// Returns the variant for `desc`, compiling it at most once.
639    fn get_or_create<F>(&mut self, desc: &RenderPipelineDesc, compile: F) -> &P
640    where
641        F: Fn(&RenderPipelineDesc) -> P,
642    {
643        let Some(key) = self.keys.key_for(desc) else {
644            if !self.warned_key_space {
645                self.warned_key_space = true;
646                log::error!(
647                    "frust-gpu: pipeline variant key space exhausted (or an unusable \
648                     sample_count {}); this variant is compiled per request instead of cached",
649                    desc.sample_count
650                );
651            }
652            self.overflow = Some(Arc::new(compile(desc)));
653            return self
654                .overflow
655                .as_deref()
656                .expect("the uncached variant was just stored");
657        };
658
659        if !self.local.contains_key(&key) {
660            #[cfg(feature = "perf-trace")]
661            let started = std::time::Instant::now();
662            let built = self.resolve(key, desc, &compile);
663            #[cfg(feature = "perf-trace")]
664            {
665                self.resolve_wait = self.resolve_wait.saturating_add(started.elapsed());
666            }
667            self.local.insert(key, built);
668        }
669        &self.local[&key]
670    }
671
672    /// The shared-state half of `get_or_create`: hit, steal, wait, or build.
673    fn resolve<F>(&self, key: u64, desc: &RenderPipelineDesc, compile: &F) -> Arc<P>
674    where
675        F: Fn(&RenderPipelineDesc) -> P,
676    {
677        let mut inner = lock(&self.shared.inner);
678        loop {
679            if let Some(built) = inner.built.get(&key) {
680                return Arc::clone(built);
681            }
682            match inner.jobs.get(&key).map(|job| job.state) {
683                // The warm-up worker holds this job: wait for its result
684                // rather than compiling a second copy of the same variant.
685                Some(JobState::Building) => {
686                    inner = self
687                        .shared
688                        .built
689                        .wait(inner)
690                        .unwrap_or_else(PoisonError::into_inner);
691                }
692                // Queued but unstarted — steal it and build it inline. Same
693                // for a variant whose last compile unwound: the queue will
694                // never come back to it, so the requester rebuilds it here
695                // (and eats the failure itself if it unwinds again).
696                Some(JobState::Pending | JobState::Failed) => {
697                    if let Some(job) = inner.jobs.get_mut(&key) {
698                        job.state = JobState::Building;
699                    }
700                    drop(inner);
701                    return self.build(key, desc, compile);
702                }
703                // Never queued (or queued, finished, and since dropped):
704                // record it as in-flight so a concurrent request waits.
705                Some(JobState::Done) | None => {
706                    inner.jobs.insert(
707                        key,
708                        Job {
709                            desc: desc.clone(),
710                            state: JobState::Building,
711                        },
712                    );
713                    drop(inner);
714                    return self.build(key, desc, compile);
715                }
716            }
717        }
718    }
719
720    /// Runs one compile for an already-claimed (`Building`) job and publishes it.
721    fn build<F>(&self, key: u64, desc: &RenderPipelineDesc, compile: &F) -> Arc<P>
722    where
723        F: Fn(&RenderPipelineDesc) -> P,
724    {
725        let guard = BuildGuard::new(&self.shared, key);
726        let pipeline = Arc::new(compile(desc));
727        guard.publish(Arc::clone(&pipeline));
728        pipeline
729    }
730
731    /// Records `descs` as warm-up jobs, skipping any variant already built or
732    /// already queued. Compiles nothing itself.
733    ///
734    /// Returns whether the caller must start a worker: `true` only when there
735    /// is pending work and no live worker to take it, in which case the
736    /// liveness flag is claimed here, under the same lock that queued the
737    /// jobs. A cache that is shutting down never asks for one.
738    fn enqueue(&mut self, descs: &[RenderPipelineDesc]) -> bool {
739        let mut inner = lock(&self.shared.inner);
740        for desc in descs {
741            let Some(key) = self.keys.key_for(desc) else {
742                log::error!(
743                    "frust-gpu: warm-up variant has no representable key (sample_count {}); \
744                     it will be compiled on first use instead",
745                    desc.sample_count
746                );
747                continue;
748            };
749            if inner.built.contains_key(&key) || inner.jobs.contains_key(&key) {
750                continue;
751            }
752            inner.jobs.insert(
753                key,
754                Job {
755                    desc: desc.clone(),
756                    state: JobState::Pending,
757                },
758            );
759            inner.queue.push_back(key);
760        }
761        if inner.shutdown || inner.worker_live || inner.queue.is_empty() {
762            return false;
763        }
764        inner.worker_live = true;
765        inner.worker_epoch += 1;
766        true
767    }
768
769    /// Builds every still-pending queued job. This is the warm-up worker's
770    /// whole body; a job the render thread stole in the meantime is skipped.
771    ///
772    /// A compile that unwinds is caught here rather than tearing the worker
773    /// down with the rest of the queue unbuilt: [`BuildGuard`] has already
774    /// marked that one variant failed, and the drain moves on.
775    fn drain_queue<F>(shared: &Arc<Shared<P>>, compile: F)
776    where
777        F: Fn(&RenderPipelineDesc) -> P,
778    {
779        let _live = LiveWorkerGuard {
780            shared,
781            epoch: lock(&shared.inner).worker_epoch,
782        };
783        loop {
784            let claimed = lock(&shared.inner).claim_next_pending();
785            let Some((key, desc)) = claimed else {
786                return;
787            };
788            let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
789                let guard = BuildGuard::new(shared, key);
790                guard.publish(Arc::new(compile(&desc)));
791            }));
792            if built.is_ok() {
793                lock(&shared.inner).warm_finished += 1;
794            }
795            if built.is_err() {
796                log::error!(
797                    "frust-gpu: a warm-up compile panicked; that variant is left to be \
798                     rebuilt on request and the rest of the queue continues"
799                );
800            }
801        }
802    }
803
804    /// Queues `descs` and makes sure exactly one worker is draining them.
805    ///
806    /// `make_compile` is called once per thread that needs one — the worker,
807    /// or the calling thread when the spawn fails and the queue has to be
808    /// drained inline rather than silently not warmed at all.
809    fn warm_up<F>(&mut self, descs: &[RenderPipelineDesc], make_compile: impl Fn() -> F)
810    where
811        P: Send + Sync + 'static,
812        F: Fn(&RenderPipelineDesc) -> P + Send + 'static,
813    {
814        if !self.enqueue(descs) {
815            return;
816        }
817        // The previous worker has already cleared the liveness flag and is on
818        // its way out; joining it here is what keeps "no worker outlives the
819        // cache" true of the handle this cache actually holds.
820        self.join_worker();
821
822        let worker_shared = self.shared();
823        let worker_compile = make_compile();
824        let spawned = std::thread::Builder::new()
825            .name("frust-gpu pipeline warm-up".to_string())
826            .spawn(move || VariantCache::drain_queue(&worker_shared, worker_compile));
827        match spawned {
828            Ok(handle) => self.worker = Some(handle),
829            Err(err) => {
830                log::warn!(
831                    "frust-gpu: could not spawn the pipeline warm-up thread ({err}); \
832                     building the listed variants inline"
833                );
834                VariantCache::drain_queue(&self.shared(), make_compile());
835            }
836        }
837    }
838
839    /// Stops the warm-up worker and waits for it, so nothing this cache
840    /// spawned can still be holding the device once it returns.
841    ///
842    /// Idempotent, and terminal: the shutdown flag stays set, so a later
843    /// `warm_up` queues its variants without starting a worker and they are
844    /// built on request instead.
845    fn shutdown(&mut self) {
846        lock(&self.shared.inner).shutdown = true;
847        self.join_worker();
848    }
849
850    /// Joins the worker if one is recorded, reporting a panic that escaped it.
851    fn join_worker(&mut self) {
852        if let Some(worker) = self.worker.take()
853            && worker.join().is_err()
854        {
855            log::error!("frust-gpu: the pipeline warm-up worker panicked");
856        }
857    }
858
859    /// How many compiles have run since this cache was created.
860    fn compiled_variants(&self) -> u64 {
861        lock(&self.shared.inner).compiles
862    }
863
864    /// How many warm-up jobs the worker has finished building.
865    fn warm_up_finished(&self) -> u64 {
866        lock(&self.shared.inner).warm_finished
867    }
868
869    /// How many warm-up jobs are still waiting to be built.
870    fn queued_variants(&self) -> usize {
871        let inner = lock(&self.shared.inner);
872        inner
873            .jobs
874            .values()
875            .filter(|job| job.state == JobState::Pending)
876            .count()
877    }
878}
879
880/// The engine's render-pipeline cache.
881///
882/// Owns nothing but policy plus the compiled pipelines: the `wgpu::Device` is
883/// passed per call, and the shader modules come from the shared
884/// [`ShaderLibrary`] handed to [`PipelineCache::new`].
885///
886/// See the module docs for the variant key, the warm-up queue and the
887/// eager-steal rule.
888#[derive(Debug)]
889pub struct PipelineCache {
890    core: VariantCache<wgpu::RenderPipeline>,
891    shaders: Arc<ShaderLibrary>,
892    driver_cache: Option<wgpu::PipelineCache>,
893}
894
895impl PipelineCache {
896    /// A cache over `shaders`, optionally seeding every pipeline it builds
897    /// from a persisted `driver_cache`.
898    ///
899    /// `driver_cache` is `None` on every backend but Vulkan; see
900    /// [`crate::pipeline_cache`] for how a persisted blob is validated before
901    /// it becomes one.
902    #[must_use]
903    pub fn new(shaders: Arc<ShaderLibrary>, driver_cache: Option<wgpu::PipelineCache>) -> Self {
904        Self {
905            core: VariantCache::default(),
906            shaders,
907            driver_cache,
908        }
909    }
910
911    /// The library this cache resolves [`ShaderId`]s against.
912    #[must_use]
913    pub fn shaders(&self) -> &ShaderLibrary {
914        &self.shaders
915    }
916
917    /// The pipeline for `desc`, compiling it if this is the first request.
918    ///
919    /// A variant queued for the warm-up worker but not yet started is stolen
920    /// and built inline; one the worker is already building is waited for.
921    /// Either way the variant is compiled exactly once.
922    ///
923    /// # Panics
924    ///
925    /// If `desc.shader` was not minted by this cache's [`ShaderLibrary`]. An
926    /// id is opaque and library-scoped, so that is a wiring bug, not a runtime
927    /// condition.
928    pub fn get_or_create(
929        &mut self,
930        device: &wgpu::Device,
931        desc: &RenderPipelineDesc,
932    ) -> &wgpu::RenderPipeline {
933        let shaders = &self.shaders;
934        let driver_cache = self.driver_cache.as_ref();
935        self.core.get_or_create(desc, |desc| {
936            build_render_pipeline(device, shaders, driver_cache, desc)
937        })
938    }
939
940    /// Queues `descs` and builds them on a worker thread.
941    ///
942    /// Call it once the surface (and therefore the device) exists, with every
943    /// variant the app is known to draw. Returns immediately, and the render
944    /// thread never blocks on the worker — a variant it asks for early is
945    /// stolen out of the queue instead.
946    ///
947    /// Safe to call repeatedly: a surface reinstall enqueues its variants
948    /// against the live worker rather than starting a second one, and a call
949    /// whose variants are all built already starts nothing at all.
950    ///
951    /// If the thread cannot be spawned the queue is drained inline instead, so
952    /// warm-up degrades to a synchronous build rather than silently not
953    /// happening.
954    pub fn warm_up(&mut self, device: &wgpu::Device, descs: &[RenderPipelineDesc]) {
955        let shaders = Arc::clone(&self.shaders);
956        let driver_cache = self.driver_cache.clone();
957        self.core
958            .warm_up(descs, || compiler(device, &shaders, driver_cache.as_ref()));
959    }
960
961    /// Stops warm-up and joins the worker, so no thread holding a clone of the
962    /// device outlives this cache.
963    ///
964    /// [`Drop`] calls this; call it explicitly to order the wait before other
965    /// teardown of your own. It is idempotent, and terminal — afterwards
966    /// [`Self::warm_up`] records its variants without starting a worker, and
967    /// they are compiled on first request.
968    pub fn shutdown(&mut self) {
969        self.core.shutdown();
970    }
971
972    /// How many pipelines this cache has actually compiled.
973    #[must_use]
974    pub fn compiled_variants(&self) -> u64 {
975        self.core.compiled_variants()
976    }
977
978    /// How many warm-up variants are still waiting to be built.
979    #[must_use]
980    pub fn queued_variants(&self) -> usize {
981        self.core.queued_variants()
982    }
983
984    /// How many pipelines the warm-up worker has finished building (variants
985    /// the render thread stole or built itself are not counted).
986    #[must_use]
987    pub fn warm_up_finished(&self) -> u64 {
988        self.core.warm_up_finished()
989    }
990
991    /// Takes the time [`Self::get_or_create`] has spent resolving variants it
992    /// did not already hold — waiting for the worker's in-flight build, or
993    /// building a pending/unlisted one inline — since the last call.
994    ///
995    /// `perf-trace`-only, with the startup span it feeds.
996    #[cfg(feature = "perf-trace")]
997    pub fn take_resolve_wait(&mut self) -> std::time::Duration {
998        std::mem::take(&mut self.core.resolve_wait)
999    }
1000}
1001
1002/// A self-contained compile function for a worker thread: it owns clones of
1003/// the device, library and driver cache, all of which are `Arc`-backed
1004/// handles.
1005fn compiler(
1006    device: &wgpu::Device,
1007    shaders: &Arc<ShaderLibrary>,
1008    driver_cache: Option<&wgpu::PipelineCache>,
1009) -> impl Fn(&RenderPipelineDesc) -> wgpu::RenderPipeline + Send + 'static + use<> {
1010    let device = device.clone();
1011    let shaders = Arc::clone(shaders);
1012    let driver_cache = driver_cache.cloned();
1013    move |desc| build_render_pipeline(&device, &shaders, driver_cache.as_ref(), desc)
1014}
1015
1016/// Creates one `wgpu::RenderPipeline` from a desc — the only place in this
1017/// module that touches the device.
1018fn build_render_pipeline(
1019    device: &wgpu::Device,
1020    shaders: &ShaderLibrary,
1021    driver_cache: Option<&wgpu::PipelineCache>,
1022    desc: &RenderPipelineDesc,
1023) -> wgpu::RenderPipeline {
1024    let name = shaders.name_of(desc.shader).unwrap_or("<unknown shader>");
1025    let module = shaders
1026        .get(desc.shader)
1027        .expect("a RenderPipelineDesc's ShaderId must come from this cache's ShaderLibrary");
1028    // `wgpu::VertexState::buffers` is a slice of `Option`s: a `None` slot
1029    // declares a vertex-buffer index the pipeline leaves unbound. Every layout
1030    // this cache carries is used, so each one is wrapped in `Some` and no gap
1031    // is ever produced.
1032    let layouts: Vec<Option<wgpu::VertexBufferLayout<'_>>> = desc
1033        .vertex_layouts
1034        .iter()
1035        .map(|layout| Some(VertexLayout::as_wgpu(layout)))
1036        .collect();
1037    let label = format!(
1038        "frust-gpu pipeline: {name} [{:?}, msaa x{}]",
1039        desc.format, desc.sample_count
1040    );
1041
1042    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1043        label: Some(&label),
1044        // `None` asks wgpu to derive the layout from the module — see the
1045        // module docs' *The layout axis* for when a desc names one instead.
1046        layout: desc.layout.as_ref(),
1047        vertex: wgpu::VertexState {
1048            module,
1049            entry_point: Some(desc.vs.as_ref()),
1050            compilation_options: wgpu::PipelineCompilationOptions::default(),
1051            buffers: &layouts,
1052        },
1053        primitive: wgpu::PrimitiveState {
1054            topology: desc.topology,
1055            ..Default::default()
1056        },
1057        depth_stencil: desc.depth.clone(),
1058        multisample: wgpu::MultisampleState {
1059            count: desc.sample_count,
1060            mask: !0,
1061            alpha_to_coverage_enabled: false,
1062        },
1063        fragment: Some(wgpu::FragmentState {
1064            module,
1065            entry_point: Some(desc.fs.as_ref()),
1066            compilation_options: wgpu::PipelineCompilationOptions::default(),
1067            targets: &[Some(wgpu::ColorTargetState {
1068                format: desc.format,
1069                blend: desc.blend,
1070                write_mask: wgpu::ColorWrites::ALL,
1071            })],
1072        }),
1073        multiview_mask: None,
1074        cache: driver_cache,
1075    })
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081    use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
1082
1083    /// Stands in for a `wgpu::RenderPipeline` so the queue/steal policy runs
1084    /// with no device: it records which variant it was built from.
1085    #[derive(Debug, PartialEq, Eq)]
1086    struct FakePipeline {
1087        sample_count: u32,
1088        serial: u64,
1089    }
1090
1091    /// A compile function that counts its calls, so "compiled once" is an
1092    /// assertion rather than an inference.
1093    #[derive(Default)]
1094    struct Counter(AtomicU64);
1095
1096    impl Counter {
1097        fn compile(&self, desc: &RenderPipelineDesc) -> FakePipeline {
1098            let serial = self.0.fetch_add(1, Ordering::SeqCst);
1099            FakePipeline {
1100                sample_count: desc.sample_count,
1101                serial,
1102            }
1103        }
1104
1105        fn count(&self) -> u64 {
1106            self.0.load(Ordering::SeqCst)
1107        }
1108    }
1109
1110    fn desc() -> RenderPipelineDesc {
1111        RenderPipelineDesc::new(
1112            ShaderId::from_raw(0),
1113            "vs_main",
1114            "fs_main",
1115            wgpu::TextureFormat::Rgba8Unorm,
1116        )
1117    }
1118
1119    #[test]
1120    fn a_repeated_request_compiles_once() {
1121        let mut cache = VariantCache::<FakePipeline>::default();
1122        let counter = Counter::default();
1123        let desc = desc();
1124
1125        let first = cache.get_or_create(&desc, |d| counter.compile(d)).serial;
1126        for _ in 0..8 {
1127            let again = cache.get_or_create(&desc, |d| counter.compile(d)).serial;
1128            assert_eq!(again, first, "every repeat must return the same pipeline");
1129        }
1130        assert_eq!(counter.count(), 1);
1131        assert_eq!(cache.compiled_variants(), 1);
1132    }
1133
1134    #[test]
1135    fn each_key_axis_is_its_own_variant() {
1136        let base = desc();
1137        let variants = [
1138            RenderPipelineDesc {
1139                blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1140                ..base.clone()
1141            },
1142            RenderPipelineDesc {
1143                format: wgpu::TextureFormat::Bgra8Unorm,
1144                ..base.clone()
1145            },
1146            RenderPipelineDesc {
1147                sample_count: 4,
1148                ..base.clone()
1149            },
1150            RenderPipelineDesc {
1151                depth: Some(wgpu::DepthStencilState {
1152                    format: wgpu::TextureFormat::Depth32Float,
1153                    depth_write_enabled: Some(true),
1154                    depth_compare: Some(wgpu::CompareFunction::Less),
1155                    stencil: wgpu::StencilState::default(),
1156                    bias: wgpu::DepthBiasState::default(),
1157                }),
1158                ..base.clone()
1159            },
1160            RenderPipelineDesc {
1161                shader: ShaderId::from_raw(1),
1162                ..base.clone()
1163            },
1164            // Same shader id, different entry point: the program axis, not a
1165            // silent collision with `base`.
1166            RenderPipelineDesc {
1167                fs: "fs_other".into(),
1168                ..base.clone()
1169            },
1170            // Same shader id and entry points, different vertex layout.
1171            RenderPipelineDesc {
1172                vertex_layouts: vec![VertexLayout::per_vertex(
1173                    16,
1174                    vec![wgpu::VertexAttribute {
1175                        format: wgpu::VertexFormat::Float32x4,
1176                        offset: 0,
1177                        shader_location: 0,
1178                    }],
1179                )],
1180                ..base.clone()
1181            },
1182            RenderPipelineDesc {
1183                topology: wgpu::PrimitiveTopology::LineList,
1184                ..base.clone()
1185            },
1186        ];
1187
1188        let mut packer = KeyPacker::default();
1189        let base_key = packer.key_for(&base).expect("base key");
1190        let mut seen = vec![base_key];
1191        for variant in &variants {
1192            let key = packer.key_for(variant).expect("variant key");
1193            assert!(
1194                !seen.contains(&key),
1195                "{variant:?} must not reuse an existing key"
1196            );
1197            seen.push(key);
1198        }
1199
1200        // And each of them really is a separate compile.
1201        let mut cache = VariantCache::<FakePipeline>::default();
1202        let counter = Counter::default();
1203        cache.get_or_create(&base, |d| counter.compile(d));
1204        for variant in &variants {
1205            cache.get_or_create(variant, |d| counter.compile(d));
1206        }
1207        assert_eq!(counter.count(), 1 + variants.len() as u64);
1208    }
1209
1210    #[test]
1211    fn a_key_is_stable_across_repeated_packing() {
1212        let mut packer = KeyPacker::default();
1213        let desc = desc();
1214        let first = packer.key_for(&desc).expect("key");
1215        for _ in 0..4 {
1216            assert_eq!(packer.key_for(&desc), Some(first));
1217        }
1218    }
1219
1220    #[test]
1221    fn an_unusable_sample_count_has_no_key_and_is_not_cached() {
1222        let mut packer = KeyPacker::default();
1223        assert_eq!(
1224            packer.key_for(&RenderPipelineDesc {
1225                sample_count: 3,
1226                ..desc()
1227            }),
1228            None
1229        );
1230        assert_eq!(
1231            packer.key_for(&RenderPipelineDesc {
1232                sample_count: 0,
1233                ..desc()
1234            }),
1235            None
1236        );
1237
1238        // The uncached fallback still returns a usable pipeline, it just
1239        // recompiles per request.
1240        let mut cache = VariantCache::<FakePipeline>::default();
1241        let counter = Counter::default();
1242        let odd = RenderPipelineDesc {
1243            sample_count: 3,
1244            ..desc()
1245        };
1246        assert_eq!(
1247            cache
1248                .get_or_create(&odd, |d| counter.compile(d))
1249                .sample_count,
1250            3
1251        );
1252        cache.get_or_create(&odd, |d| counter.compile(d));
1253        assert_eq!(counter.count(), 2, "an unkeyable variant is never cached");
1254    }
1255
1256    #[test]
1257    fn draining_the_queue_builds_every_listed_variant_once() {
1258        let mut cache = VariantCache::<FakePipeline>::default();
1259        let counter = Counter::default();
1260        let listed: Vec<_> = [1u32, 2, 4]
1261            .into_iter()
1262            .map(|sample_count| RenderPipelineDesc {
1263                sample_count,
1264                ..desc()
1265            })
1266            .collect();
1267
1268        cache.enqueue(&listed);
1269        assert_eq!(cache.queued_variants(), 3);
1270        VariantCache::drain_queue(&cache.shared(), |d| counter.compile(d));
1271        assert_eq!(counter.count(), 3);
1272        assert_eq!(cache.queued_variants(), 0);
1273
1274        // Every warmed variant is now a hit, not a compile.
1275        for desc in &listed {
1276            cache.get_or_create(desc, |d| counter.compile(d));
1277        }
1278        assert_eq!(counter.count(), 3, "a warmed variant must not recompile");
1279    }
1280
1281    #[test]
1282    fn enqueueing_the_same_variant_twice_queues_one_job() {
1283        let mut cache = VariantCache::<FakePipeline>::default();
1284        let listed = [desc(), desc()];
1285        cache.enqueue(&listed);
1286        cache.enqueue(&listed);
1287        assert_eq!(cache.queued_variants(), 1);
1288    }
1289
1290    #[test]
1291    fn a_render_thread_request_steals_a_queued_job_and_the_queue_skips_it() {
1292        let mut cache = VariantCache::<FakePipeline>::default();
1293        let counter = Counter::default();
1294        let listed: Vec<_> = [1u32, 2, 4]
1295            .into_iter()
1296            .map(|sample_count| RenderPipelineDesc {
1297                sample_count,
1298                ..desc()
1299            })
1300            .collect();
1301        cache.enqueue(&listed);
1302
1303        // The render thread wants the last-listed variant before the worker
1304        // has started: it builds it inline rather than waiting its turn.
1305        let stolen = cache
1306            .get_or_create(&listed[2], |d| counter.compile(d))
1307            .serial;
1308        assert_eq!(stolen, 0, "the steal is the first compile to run");
1309        assert_eq!(counter.count(), 1);
1310        assert_eq!(cache.queued_variants(), 2, "the stolen job is done");
1311
1312        // The worker then builds only what is left — the stolen entry is
1313        // marked done, not rebuilt.
1314        VariantCache::drain_queue(&cache.shared(), |d| counter.compile(d));
1315        assert_eq!(
1316            counter.count(),
1317            3,
1318            "the stolen variant must not compile twice"
1319        );
1320        assert_eq!(
1321            cache
1322                .get_or_create(&listed[2], |d| counter.compile(d))
1323                .serial,
1324            stolen
1325        );
1326        assert_eq!(counter.count(), 3);
1327    }
1328
1329    #[test]
1330    fn a_worker_thread_drain_publishes_to_the_render_thread() {
1331        let mut cache = VariantCache::<FakePipeline>::default();
1332        let listed: Vec<_> = (0..6)
1333            .map(|i| RenderPipelineDesc {
1334                shader: ShaderId::from_raw(i),
1335                ..desc()
1336            })
1337            .collect();
1338        cache.enqueue(&listed);
1339
1340        let shared = cache.shared();
1341        let worker_counter = Arc::new(Counter::default());
1342        let thread_counter = Arc::clone(&worker_counter);
1343        let worker = std::thread::spawn(move || {
1344            VariantCache::drain_queue(&shared, move |d| thread_counter.compile(d));
1345        });
1346        worker.join().expect("the warm-up worker must not panic");
1347
1348        assert_eq!(worker_counter.count(), 6);
1349        assert_eq!(cache.compiled_variants(), 6);
1350
1351        // The render thread now sees the worker's pipelines through the
1352        // shared map; its own compile function is never called.
1353        let render_counter = Counter::default();
1354        for desc in &listed {
1355            cache.get_or_create(desc, |d| render_counter.compile(d));
1356        }
1357        assert_eq!(render_counter.count(), 0);
1358    }
1359
1360    #[test]
1361    fn an_abandoned_build_marks_the_job_failed_rather_than_pending() {
1362        let cache = VariantCache::<FakePipeline>::default();
1363        let shared = cache.shared();
1364        let guard = BuildGuard::new(&shared, 42);
1365        lock(&shared.inner).jobs.insert(
1366            42,
1367            Job {
1368                desc: desc(),
1369                state: JobState::Building,
1370            },
1371        );
1372        // Dropping without publishing is what an unwinding compile does.
1373        drop(guard);
1374        assert_eq!(
1375            lock(&shared.inner).jobs.get(&42).map(|job| job.state),
1376            Some(JobState::Failed),
1377            "an abandoned build must not leave a Pending job no worker can claim"
1378        );
1379    }
1380
1381    /// A compile that panics once, then behaves — the failure mode a driver
1382    /// or a malformed shader produces on exactly one variant.
1383    struct PanicOnce {
1384        counter: Arc<Counter>,
1385        armed: AtomicBool,
1386    }
1387
1388    impl PanicOnce {
1389        fn new(counter: &Arc<Counter>) -> Self {
1390            Self {
1391                counter: Arc::clone(counter),
1392                armed: AtomicBool::new(true),
1393            }
1394        }
1395
1396        fn compile(&self, desc: &RenderPipelineDesc) -> FakePipeline {
1397            assert!(
1398                !self.armed.swap(false, Ordering::SeqCst),
1399                "the fake compile fails its first call"
1400            );
1401            self.counter.compile(desc)
1402        }
1403    }
1404
1405    #[test]
1406    fn a_panicking_warm_up_compile_costs_one_variant_and_not_the_queue() {
1407        let mut cache = VariantCache::<FakePipeline>::default();
1408        let counter = Arc::new(Counter::default());
1409        let failing = PanicOnce::new(&counter);
1410        let listed: Vec<_> = [1u32, 2, 4]
1411            .into_iter()
1412            .map(|sample_count| RenderPipelineDesc {
1413                sample_count,
1414                ..desc()
1415            })
1416            .collect();
1417
1418        cache.enqueue(&listed);
1419        VariantCache::drain_queue(&cache.shared(), |d| failing.compile(d));
1420
1421        // The unwind took its own variant down and nothing else: the drain
1422        // carried on and built the other two.
1423        assert_eq!(counter.count(), 2);
1424        assert_eq!(
1425            cache.queued_variants(),
1426            0,
1427            "a failed job must not be counted as still queued forever"
1428        );
1429
1430        // And the failed variant is rebuilt on request rather than waited on.
1431        assert_eq!(
1432            cache
1433                .get_or_create(&listed[0], |d| counter.compile(d))
1434                .sample_count,
1435            1
1436        );
1437        assert_eq!(counter.count(), 3);
1438
1439        // Everything the worker did build is still a hit.
1440        for desc in &listed {
1441            cache.get_or_create(desc, |d| counter.compile(d));
1442        }
1443        assert_eq!(counter.count(), 3);
1444    }
1445
1446    #[test]
1447    fn a_panicking_inline_build_surfaces_to_the_requester_and_leaves_it_rebuildable() {
1448        let mut cache = VariantCache::<FakePipeline>::default();
1449        let counter = Arc::new(Counter::default());
1450        let failing = PanicOnce::new(&counter);
1451        let desc = desc();
1452        cache.enqueue(std::slice::from_ref(&desc));
1453
1454        // The render thread steals the queued job; its compile unwinds, and
1455        // the failure lands on the requester rather than anywhere silent.
1456        let stolen = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1457            cache.get_or_create(&desc, |d| failing.compile(d));
1458        }));
1459        assert!(
1460            stolen.is_err(),
1461            "an inline compile must not swallow a panic"
1462        );
1463        assert_eq!(counter.count(), 0);
1464
1465        // The next request rebuilds it instead of blocking on a build that
1466        // will never publish.
1467        assert_eq!(cache.get_or_create(&desc, |d| counter.compile(d)).serial, 0);
1468        assert_eq!(counter.count(), 1);
1469        assert_eq!(cache.queued_variants(), 0);
1470    }
1471
1472    /// A latch a fake compile parks on, so a warm-up worker can be held
1473    /// mid-drain while the test makes further `warm_up` calls against the
1474    /// same cache.
1475    #[derive(Default)]
1476    struct Latch {
1477        open: Mutex<bool>,
1478        changed: Condvar,
1479    }
1480
1481    impl Latch {
1482        fn wait(&self) {
1483            let mut open = lock(&self.open);
1484            while !*open {
1485                open = self
1486                    .changed
1487                    .wait(open)
1488                    .unwrap_or_else(PoisonError::into_inner);
1489            }
1490        }
1491
1492        fn open(&self) {
1493            *lock(&self.open) = true;
1494            self.changed.notify_all();
1495        }
1496    }
1497
1498    fn shader_variants(count: u32) -> Vec<RenderPipelineDesc> {
1499        (0..count)
1500            .map(|i| RenderPipelineDesc {
1501                shader: ShaderId::from_raw(i),
1502                ..desc()
1503            })
1504            .collect()
1505    }
1506
1507    #[test]
1508    fn repeated_warm_up_calls_keep_at_most_one_worker() {
1509        let mut cache = VariantCache::<FakePipeline>::default();
1510        let counter = Arc::new(Counter::default());
1511        let latch = Arc::new(Latch::default());
1512        let workers = AtomicU64::new(0);
1513        let listed = shader_variants(6);
1514
1515        // Every worker this cache starts calls the factory exactly once, so
1516        // counting factory calls counts workers.
1517        let make_compile = || {
1518            workers.fetch_add(1, Ordering::SeqCst);
1519            let counter = Arc::clone(&counter);
1520            let latch = Arc::clone(&latch);
1521            let held = AtomicBool::new(false);
1522            move |d: &RenderPipelineDesc| {
1523                if !held.swap(true, Ordering::SeqCst) {
1524                    latch.wait();
1525                }
1526                counter.compile(d)
1527            }
1528        };
1529
1530        // One worker starts on the first listed variant and is held there,
1531        // so it is unambiguously still live for every later call.
1532        cache.warm_up(&listed[..1], make_compile);
1533        for _ in 0..8 {
1534            cache.warm_up(&listed, make_compile);
1535        }
1536        assert_eq!(
1537            workers.load(Ordering::SeqCst),
1538            1,
1539            "a live worker must absorb further warm-up calls, not be joined by more"
1540        );
1541
1542        latch.open();
1543        cache.shutdown();
1544
1545        // Whatever the worker did not reach before shutdown is built on
1546        // request; either way each variant is compiled exactly once.
1547        for desc in &listed {
1548            cache.get_or_create(desc, |d| counter.compile(d));
1549        }
1550        assert_eq!(counter.count(), listed.len() as u64);
1551        assert_eq!(cache.compiled_variants(), listed.len() as u64);
1552    }
1553
1554    #[test]
1555    fn dropping_the_cache_joins_its_worker() {
1556        // Stands in for the `wgpu::Device` handle a real compile function
1557        // clones into the worker: if the thread outlived the cache, this
1558        // clone would still be alive after the drop.
1559        let device = Arc::new(());
1560        let counter = Arc::new(Counter::default());
1561        let listed = shader_variants(4);
1562
1563        let mut cache = VariantCache::<FakePipeline>::default();
1564        cache.warm_up(&listed, || {
1565            let device = Arc::clone(&device);
1566            let counter = Arc::clone(&counter);
1567            move |d: &RenderPipelineDesc| {
1568                let _held = Arc::clone(&device);
1569                counter.compile(d)
1570            }
1571        });
1572        drop(cache);
1573
1574        assert_eq!(
1575            Arc::strong_count(&device),
1576            1,
1577            "no warm-up worker may outlive the cache that started it"
1578        );
1579    }
1580
1581    #[test]
1582    fn warming_up_after_shutdown_starts_no_worker_and_still_builds_on_request() {
1583        let mut cache = VariantCache::<FakePipeline>::default();
1584        let counter = Arc::new(Counter::default());
1585        let workers = AtomicU64::new(0);
1586        let listed = shader_variants(3);
1587
1588        cache.shutdown();
1589        // Idempotent: a second call is a no-op, not a double join.
1590        cache.shutdown();
1591        cache.warm_up(&listed, || {
1592            workers.fetch_add(1, Ordering::SeqCst);
1593            let counter = Arc::clone(&counter);
1594            move |d: &RenderPipelineDesc| counter.compile(d)
1595        });
1596
1597        assert_eq!(workers.load(Ordering::SeqCst), 0);
1598        for desc in &listed {
1599            cache.get_or_create(desc, |d| counter.compile(d));
1600        }
1601        assert_eq!(counter.count(), listed.len() as u64);
1602    }
1603
1604    /// Pops a validation error scope, pumping the device until the pop
1605    /// resolves — the same drain `frust-render`'s shader-effect compile uses,
1606    /// since the pop is a future a plain block-on would park on.
1607    fn drain_error_scope(
1608        device: &wgpu::Device,
1609        scope: wgpu::ErrorScopeGuard,
1610    ) -> Option<wgpu::Error> {
1611        use std::task::{Context, Poll, Waker};
1612
1613        let waker = Waker::noop();
1614        let mut cx = Context::from_waker(waker);
1615        let mut future = std::pin::pin!(scope.pop());
1616        loop {
1617            match future.as_mut().poll(&mut cx) {
1618                Poll::Ready(error) => return error,
1619                Poll::Pending => {
1620                    let _ = device.poll(wgpu::PollType::wait_indefinitely());
1621                }
1622            }
1623        }
1624    }
1625
1626    /// One real pipeline on real hardware: the host tests above all run
1627    /// against a fake compile function, so this is the only case that proves
1628    /// a `RenderPipelineDesc` actually describes a pipeline wgpu accepts —
1629    /// entry points, vertex layout, color target and multisample state
1630    /// included — and that the cache still compiles it exactly once.
1631    #[test]
1632    #[ignore = "requires a GPU (Metal/Vulkan); run locally with `cargo test -p frust-gpu -- --ignored`"]
1633    fn creates_one_real_pipeline() {
1634        const WGSL: &str = r#"
1635@vertex
1636fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
1637    let uv = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
1638    return vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
1639}
1640@fragment
1641fn fs_main() -> @location(0) vec4<f32> {
1642    return vec4<f32>(0.0, 1.0, 0.0, 1.0);
1643}
1644"#;
1645
1646        let (device, _queue) = pollster::block_on(async {
1647            let instance = wgpu::Instance::new(
1648                wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
1649            );
1650            // The environment-aware initializer, so `WGPU_ADAPTER_NAME` picks
1651            // the GPU on a multi-adapter host instead of the run silently
1652            // landing on whichever one enumerates first.
1653            let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, None)
1654                .await
1655                .expect("no compatible GPU adapter");
1656            println!("frust-gpu pipeline test adapter: {:?}", adapter.get_info());
1657            adapter
1658                .request_device(&wgpu::DeviceDescriptor {
1659                    label: Some("frust-gpu pipeline test device"),
1660                    required_features: wgpu::Features::empty(),
1661                    required_limits: wgpu::Limits::default(),
1662                    ..Default::default()
1663                })
1664                .await
1665                .expect("failed to create the device")
1666        });
1667
1668        let mut library = ShaderLibrary::new();
1669        let shader = library.insert_wgsl(&device, "fullscreen-green", WGSL);
1670        let mut cache = PipelineCache::new(Arc::new(library), None);
1671
1672        let desc = RenderPipelineDesc::new(
1673            shader,
1674            "vs_main",
1675            "fs_main",
1676            wgpu::TextureFormat::Rgba8Unorm,
1677        );
1678        let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
1679        for _ in 0..2 {
1680            let _pipeline = cache.get_or_create(&device, &desc);
1681        }
1682        let error = drain_error_scope(&device, scope);
1683
1684        assert!(error.is_none(), "pipeline creation raised {error:?}");
1685        assert_eq!(
1686            cache.compiled_variants(),
1687            1,
1688            "a repeat request must reuse the pipeline"
1689        );
1690
1691        // The explicit-layout axis, on the same device — one device per test
1692        // binary run keeps this suite clear of the driver's teardown races.
1693        explicit_layout_is_its_own_variant_and_shares_bind_groups(&device);
1694    }
1695
1696    /// The explicit-layout axis on real hardware: a desc naming a layout is a
1697    /// variant of its own (never a hit on its derived-layout sibling), naming
1698    /// the *same* layout object again is a hit, and two programs that read
1699    /// different bindings accept one bind group under that shared layout —
1700    /// which is what lets a family of specialised programs share bind groups.
1701    fn explicit_layout_is_its_own_variant_and_shares_bind_groups(device: &wgpu::Device) {
1702        const LAYOUT_WGSL: &str = r#"
1703@group(0) @binding(0) var<uniform> tint: vec4<f32>;
1704@vertex
1705fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
1706    let uv = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
1707    return vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
1708}
1709@fragment
1710fn fs_tinted() -> @location(0) vec4<f32> {
1711    return tint;
1712}
1713@fragment
1714fn fs_plain() -> @location(0) vec4<f32> {
1715    return vec4<f32>(0.0, 1.0, 0.0, 1.0);
1716}
1717"#;
1718
1719        let group = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1720            label: Some("explicit layout test group"),
1721            entries: &[wgpu::BindGroupLayoutEntry {
1722                binding: 0,
1723                visibility: wgpu::ShaderStages::FRAGMENT,
1724                ty: wgpu::BindingType::Buffer {
1725                    ty: wgpu::BufferBindingType::Uniform,
1726                    has_dynamic_offset: false,
1727                    min_binding_size: wgpu::BufferSize::new(16),
1728                },
1729                count: None,
1730            }],
1731        });
1732        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1733            label: Some("explicit layout test"),
1734            bind_group_layouts: &[Some(&group)],
1735            immediate_size: 0,
1736        });
1737
1738        let mut library = ShaderLibrary::new();
1739        let shader = library.insert_wgsl(device, "explicit-layout", LAYOUT_WGSL);
1740        let mut cache = PipelineCache::new(Arc::new(library), None);
1741        let tinted = RenderPipelineDesc::new(
1742            shader,
1743            "vs_main",
1744            "fs_tinted",
1745            wgpu::TextureFormat::Rgba8Unorm,
1746        );
1747        let explicit_tinted = RenderPipelineDesc {
1748            layout: Some(layout.clone()),
1749            ..tinted.clone()
1750        };
1751        // `fs_plain` reads no binding at all, so its derived layout would be
1752        // empty; under the explicit layout it accepts the tinted program's
1753        // group all the same.
1754        let explicit_plain = RenderPipelineDesc {
1755            layout: Some(layout.clone()),
1756            ..RenderPipelineDesc::new(
1757                shader,
1758                "vs_main",
1759                "fs_plain",
1760                wgpu::TextureFormat::Rgba8Unorm,
1761            )
1762        };
1763
1764        let mut packer = KeyPacker::default();
1765        assert_ne!(packer.key_for(&tinted), packer.key_for(&explicit_tinted));
1766        assert_eq!(
1767            packer.key_for(&explicit_tinted),
1768            packer.key_for(&RenderPipelineDesc {
1769                layout: Some(layout.clone()),
1770                ..tinted.clone()
1771            }),
1772            "the same layout object is the same variant"
1773        );
1774
1775        let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
1776        let _ = cache.get_or_create(device, &tinted);
1777        let a = cache.get_or_create(device, &explicit_tinted).clone();
1778        let b = cache.get_or_create(device, &explicit_plain).clone();
1779        let _ = cache.get_or_create(device, &explicit_tinted);
1780        let buffer = device.create_buffer(&wgpu::BufferDescriptor {
1781            label: None,
1782            size: 16,
1783            usage: wgpu::BufferUsages::UNIFORM,
1784            mapped_at_creation: false,
1785        });
1786        // One group, built against the first pipeline's layout, bound under
1787        // both: valid only because both name the explicit layout.
1788        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1789            label: None,
1790            layout: &a.get_bind_group_layout(0),
1791            entries: &[wgpu::BindGroupEntry {
1792                binding: 0,
1793                resource: buffer.as_entire_binding(),
1794            }],
1795        });
1796        let target = device.create_texture(&wgpu::TextureDescriptor {
1797            label: None,
1798            size: wgpu::Extent3d {
1799                width: 4,
1800                height: 4,
1801                depth_or_array_layers: 1,
1802            },
1803            mip_level_count: 1,
1804            sample_count: 1,
1805            dimension: wgpu::TextureDimension::D2,
1806            format: wgpu::TextureFormat::Rgba8Unorm,
1807            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1808            view_formats: &[],
1809        });
1810        let view = target.create_view(&wgpu::TextureViewDescriptor::default());
1811        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
1812        {
1813            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1814                label: None,
1815                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1816                    view: &view,
1817                    depth_slice: None,
1818                    resolve_target: None,
1819                    ops: wgpu::Operations {
1820                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1821                        store: wgpu::StoreOp::Store,
1822                    },
1823                })],
1824                depth_stencil_attachment: None,
1825                timestamp_writes: None,
1826                occlusion_query_set: None,
1827                multiview_mask: None,
1828            });
1829            pass.set_bind_group(0, &bind_group, &[]);
1830            pass.set_pipeline(&a);
1831            pass.draw(0..3, 0..1);
1832            pass.set_pipeline(&b);
1833            pass.draw(0..3, 0..1);
1834        }
1835        drop(encoder.finish());
1836        drop(group);
1837        let error = drain_error_scope(device, scope);
1838        assert!(error.is_none(), "pipeline creation raised {error:?}");
1839        assert_eq!(cache.compiled_variants(), 3);
1840    }
1841
1842    #[test]
1843    fn a_new_desc_derives_its_layout() {
1844        assert!(
1845            desc().layout.is_none(),
1846            "every existing desc keeps wgpu's derived layout unless it names one"
1847        );
1848        // The layout field sits above every other axis, so a derived-layout
1849        // key never reaches it.
1850        let mut packer = KeyPacker::default();
1851        let key = packer.key_for(&desc()).expect("key");
1852        assert_eq!(key >> LAYOUT_SHIFT, 0);
1853    }
1854}