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