1use std::borrow::Cow;
108use std::collections::{HashMap, VecDeque};
109use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
110
111use crate::shader::{ShaderId, ShaderLibrary};
112
113const PROGRAM_BITS: u32 = 16;
115const FORMAT_BITS: u32 = 12;
117const BLEND_BITS: u32 = 12;
119const DEPTH_BITS: u32 = 12;
121const SAMPLE_BITS: u32 = 4;
123const LAYOUT_BITS: u32 = 8;
125
126const PROGRAM_SHIFT: u32 = 0;
127const FORMAT_SHIFT: u32 = PROGRAM_SHIFT + PROGRAM_BITS;
128const BLEND_SHIFT: u32 = FORMAT_SHIFT + FORMAT_BITS;
129const DEPTH_SHIFT: u32 = BLEND_SHIFT + BLEND_BITS;
130const SAMPLE_SHIFT: u32 = DEPTH_SHIFT + DEPTH_BITS;
131const LAYOUT_SHIFT: u32 = SAMPLE_SHIFT + SAMPLE_BITS;
132
133const _: () = assert!(
134 LAYOUT_SHIFT + LAYOUT_BITS <= u64::BITS,
135 "the packed variant key must fit one u64",
136);
137
138#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
145pub struct VertexLayout {
146 pub array_stride: wgpu::BufferAddress,
148 pub step_mode: wgpu::VertexStepMode,
150 pub attributes: Vec<wgpu::VertexAttribute>,
152}
153
154impl VertexLayout {
155 #[must_use]
157 pub fn per_vertex(
158 array_stride: wgpu::BufferAddress,
159 attributes: Vec<wgpu::VertexAttribute>,
160 ) -> Self {
161 Self {
162 array_stride,
163 step_mode: wgpu::VertexStepMode::Vertex,
164 attributes,
165 }
166 }
167
168 #[must_use]
170 pub fn as_wgpu(&self) -> wgpu::VertexBufferLayout<'_> {
171 wgpu::VertexBufferLayout {
172 array_stride: self.array_stride,
173 step_mode: self.step_mode,
174 attributes: &self.attributes,
175 }
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq, Hash)]
190pub struct RenderPipelineDesc {
191 pub shader: ShaderId,
193 pub vs: Cow<'static, str>,
195 pub fs: Cow<'static, str>,
197 pub vertex_layouts: Vec<VertexLayout>,
200 pub blend: Option<wgpu::BlendState>,
202 pub format: wgpu::TextureFormat,
204 pub sample_count: u32,
206 pub depth: Option<wgpu::DepthStencilState>,
208 pub topology: wgpu::PrimitiveTopology,
211 pub layout: Option<wgpu::PipelineLayout>,
218}
219
220impl RenderPipelineDesc {
221 #[must_use]
225 pub fn new(
226 shader: ShaderId,
227 vs: impl Into<Cow<'static, str>>,
228 fs: impl Into<Cow<'static, str>>,
229 format: wgpu::TextureFormat,
230 ) -> Self {
231 Self {
232 shader,
233 vs: vs.into(),
234 fs: fs.into(),
235 vertex_layouts: Vec::new(),
236 blend: None,
237 format,
238 sample_count: 1,
239 depth: None,
240 topology: wgpu::PrimitiveTopology::TriangleList,
241 layout: None,
242 }
243 }
244}
245
246#[derive(Clone, Debug, PartialEq, Eq, Hash)]
249struct ProgramKey {
250 shader: ShaderId,
251 vs: Cow<'static, str>,
252 fs: Cow<'static, str>,
253 vertex_layouts: Vec<VertexLayout>,
254 topology: wgpu::PrimitiveTopology,
255}
256
257impl ProgramKey {
258 fn of(desc: &RenderPipelineDesc) -> Self {
259 Self {
260 shader: desc.shader,
261 vs: desc.vs.clone(),
262 fs: desc.fs.clone(),
263 vertex_layouts: desc.vertex_layouts.clone(),
264 topology: desc.topology,
265 }
266 }
267
268 fn matches(&self, desc: &RenderPipelineDesc) -> bool {
271 self.shader == desc.shader
272 && self.vs == desc.vs
273 && self.fs == desc.fs
274 && self.topology == desc.topology
275 && self.vertex_layouts == desc.vertex_layouts
276 }
277}
278
279#[derive(Debug)]
284struct AxisTable<K> {
285 index: HashMap<K, u64>,
286 bits: u32,
287}
288
289impl<K: std::hash::Hash + Eq + Clone> AxisTable<K> {
290 fn new(bits: u32) -> Self {
291 Self {
292 index: HashMap::new(),
293 bits,
294 }
295 }
296
297 fn intern(&mut self, value: &K) -> Option<u64> {
298 if let Some(existing) = self.index.get(value) {
299 return Some(*existing);
300 }
301 let next = self.index.len() as u64;
302 if next >= 1 << self.bits {
303 return None;
304 }
305 self.index.insert(value.clone(), next);
306 Some(next)
307 }
308}
309
310#[derive(Debug, Default)]
313struct ProgramTable {
314 by_hash: HashMap<u64, Vec<u64>>,
318 entries: Vec<ProgramKey>,
319}
320
321impl ProgramTable {
322 fn intern(&mut self, desc: &RenderPipelineDesc) -> Option<u64> {
323 let hash = program_hash(desc);
324 let chain = self.by_hash.entry(hash).or_default();
325 for &candidate in chain.iter() {
326 if self.entries[candidate as usize].matches(desc) {
327 return Some(candidate);
328 }
329 }
330 let next = self.entries.len() as u64;
331 if next >= 1 << PROGRAM_BITS {
332 return None;
333 }
334 chain.push(next);
335 self.entries.push(ProgramKey::of(desc));
336 Some(next)
337 }
338}
339
340fn program_hash(desc: &RenderPipelineDesc) -> u64 {
342 use std::hash::{Hash, Hasher};
343 let mut hasher = std::collections::hash_map::DefaultHasher::new();
344 desc.shader.hash(&mut hasher);
345 desc.vs.hash(&mut hasher);
346 desc.fs.hash(&mut hasher);
347 desc.vertex_layouts.hash(&mut hasher);
348 desc.topology.hash(&mut hasher);
349 hasher.finish()
350}
351
352#[derive(Debug)]
354struct KeyPacker {
355 programs: ProgramTable,
356 formats: AxisTable<wgpu::TextureFormat>,
357 blends: AxisTable<Option<wgpu::BlendState>>,
358 depths: AxisTable<Option<wgpu::DepthStencilState>>,
359 layouts: AxisTable<Option<wgpu::PipelineLayout>>,
360}
361
362impl Default for KeyPacker {
363 fn default() -> Self {
364 Self {
365 programs: ProgramTable::default(),
366 formats: AxisTable::new(FORMAT_BITS),
367 blends: AxisTable::new(BLEND_BITS),
368 depths: AxisTable::new(DEPTH_BITS),
369 layouts: AxisTable::new(LAYOUT_BITS),
370 }
371 }
372}
373
374impl KeyPacker {
375 fn key_for(&mut self, desc: &RenderPipelineDesc) -> Option<u64> {
378 let sample = sample_field(desc.sample_count)?;
381 let program = self.programs.intern(desc)?;
382 let format = self.formats.intern(&desc.format)?;
383 let blend = self.blends.intern(&desc.blend)?;
384 let depth = self.depths.intern(&desc.depth)?;
385 let layout = self.layouts.intern(&desc.layout)?;
386 Some(
387 (program << PROGRAM_SHIFT)
388 | (format << FORMAT_SHIFT)
389 | (blend << BLEND_SHIFT)
390 | (depth << DEPTH_SHIFT)
391 | (sample << SAMPLE_SHIFT)
392 | (layout << LAYOUT_SHIFT),
393 )
394 }
395}
396
397fn sample_field(sample_count: u32) -> Option<u64> {
399 if sample_count == 0 || !sample_count.is_power_of_two() {
400 return None;
401 }
402 let field = u64::from(sample_count.trailing_zeros());
403 (field < 1 << SAMPLE_BITS).then_some(field)
404}
405
406#[derive(Clone, Copy, Debug, PartialEq, Eq)]
408enum JobState {
409 Pending,
411 Building,
413 Done,
415 Failed,
419}
420
421#[derive(Debug)]
423struct Job {
424 desc: RenderPipelineDesc,
425 state: JobState,
426}
427
428#[derive(Debug)]
430struct Inner<P> {
431 built: HashMap<u64, Arc<P>>,
432 jobs: HashMap<u64, Job>,
433 queue: VecDeque<u64>,
436 compiles: u64,
438 worker_live: bool,
442 shutdown: bool,
445 worker_epoch: u64,
448 warm_finished: u64,
450}
451
452impl<P> Default for Inner<P> {
453 fn default() -> Self {
454 Self {
455 built: HashMap::new(),
456 jobs: HashMap::new(),
457 queue: VecDeque::new(),
458 compiles: 0,
459 worker_live: false,
460 shutdown: false,
461 worker_epoch: 0,
462 warm_finished: 0,
463 }
464 }
465}
466
467impl<P> Inner<P> {
468 fn claim_next_pending(&mut self) -> Option<(u64, RenderPipelineDesc)> {
475 while !self.shutdown
476 && let Some(key) = self.queue.pop_front()
477 {
478 if let Some(job) = self.jobs.get_mut(&key)
479 && job.state == JobState::Pending
480 {
481 job.state = JobState::Building;
482 return Some((key, job.desc.clone()));
483 }
484 }
485 self.worker_live = false;
486 None
487 }
488}
489
490#[derive(Debug)]
493struct Shared<P> {
494 inner: Mutex<Inner<P>>,
495 built: Condvar,
496}
497
498impl<P> Default for Shared<P> {
499 fn default() -> Self {
500 Self {
501 inner: Mutex::new(Inner::default()),
502 built: Condvar::new(),
503 }
504 }
505}
506
507fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
511 mutex.lock().unwrap_or_else(PoisonError::into_inner)
512}
513
514struct BuildGuard<'a, P> {
523 shared: &'a Shared<P>,
524 key: u64,
525 armed: bool,
526}
527
528impl<'a, P> BuildGuard<'a, P> {
529 fn new(shared: &'a Shared<P>, key: u64) -> Self {
530 Self {
531 shared,
532 key,
533 armed: true,
534 }
535 }
536
537 fn publish(mut self, pipeline: Arc<P>) {
539 self.armed = false;
540 let mut inner = lock(&self.shared.inner);
541 inner.built.insert(self.key, pipeline);
542 if let Some(job) = inner.jobs.get_mut(&self.key) {
543 job.state = JobState::Done;
544 }
545 inner.compiles += 1;
546 drop(inner);
547 self.shared.built.notify_all();
548 }
549}
550
551struct LiveWorkerGuard<'a, P> {
558 shared: &'a Shared<P>,
559 epoch: u64,
560}
561
562impl<P> Drop for LiveWorkerGuard<'_, P> {
563 fn drop(&mut self) {
564 let mut inner = lock(&self.shared.inner);
565 if inner.worker_epoch == self.epoch {
566 inner.worker_live = false;
567 }
568 }
569}
570
571impl<P> Drop for BuildGuard<'_, P> {
572 fn drop(&mut self) {
573 if !self.armed {
574 return;
575 }
576 let mut inner = lock(&self.shared.inner);
577 if let Some(job) = inner.jobs.get_mut(&self.key) {
578 job.state = JobState::Failed;
579 }
580 drop(inner);
581 self.shared.built.notify_all();
582 }
583}
584
585#[derive(Debug)]
592struct VariantCache<P> {
593 shared: Arc<Shared<P>>,
594 local: HashMap<u64, Arc<P>>,
597 overflow: Option<Arc<P>>,
600 keys: KeyPacker,
601 warned_key_space: bool,
602 worker: Option<std::thread::JoinHandle<()>>,
604 #[cfg(feature = "perf-trace")]
608 resolve_wait: std::time::Duration,
609}
610
611impl<P> Default for VariantCache<P> {
612 fn default() -> Self {
613 Self {
614 shared: Arc::new(Shared::default()),
615 local: HashMap::new(),
616 overflow: None,
617 keys: KeyPacker::default(),
618 warned_key_space: false,
619 worker: None,
620 #[cfg(feature = "perf-trace")]
621 resolve_wait: std::time::Duration::ZERO,
622 }
623 }
624}
625
626impl<P> Drop for VariantCache<P> {
627 fn drop(&mut self) {
628 self.shutdown();
629 }
630}
631
632impl<P> VariantCache<P> {
633 fn shared(&self) -> Arc<Shared<P>> {
635 Arc::clone(&self.shared)
636 }
637
638 fn get_or_create<F>(&mut self, desc: &RenderPipelineDesc, compile: F) -> &P
640 where
641 F: Fn(&RenderPipelineDesc) -> P,
642 {
643 let Some(key) = self.keys.key_for(desc) else {
644 if !self.warned_key_space {
645 self.warned_key_space = true;
646 log::error!(
647 "frust-gpu: pipeline variant key space exhausted (or an unusable \
648 sample_count {}); this variant is compiled per request instead of cached",
649 desc.sample_count
650 );
651 }
652 self.overflow = Some(Arc::new(compile(desc)));
653 return self
654 .overflow
655 .as_deref()
656 .expect("the uncached variant was just stored");
657 };
658
659 if !self.local.contains_key(&key) {
660 #[cfg(feature = "perf-trace")]
661 let started = std::time::Instant::now();
662 let built = self.resolve(key, desc, &compile);
663 #[cfg(feature = "perf-trace")]
664 {
665 self.resolve_wait = self.resolve_wait.saturating_add(started.elapsed());
666 }
667 self.local.insert(key, built);
668 }
669 &self.local[&key]
670 }
671
672 fn resolve<F>(&self, key: u64, desc: &RenderPipelineDesc, compile: &F) -> Arc<P>
674 where
675 F: Fn(&RenderPipelineDesc) -> P,
676 {
677 let mut inner = lock(&self.shared.inner);
678 loop {
679 if let Some(built) = inner.built.get(&key) {
680 return Arc::clone(built);
681 }
682 match inner.jobs.get(&key).map(|job| job.state) {
683 Some(JobState::Building) => {
686 inner = self
687 .shared
688 .built
689 .wait(inner)
690 .unwrap_or_else(PoisonError::into_inner);
691 }
692 Some(JobState::Pending | JobState::Failed) => {
697 if let Some(job) = inner.jobs.get_mut(&key) {
698 job.state = JobState::Building;
699 }
700 drop(inner);
701 return self.build(key, desc, compile);
702 }
703 Some(JobState::Done) | None => {
706 inner.jobs.insert(
707 key,
708 Job {
709 desc: desc.clone(),
710 state: JobState::Building,
711 },
712 );
713 drop(inner);
714 return self.build(key, desc, compile);
715 }
716 }
717 }
718 }
719
720 fn build<F>(&self, key: u64, desc: &RenderPipelineDesc, compile: &F) -> Arc<P>
722 where
723 F: Fn(&RenderPipelineDesc) -> P,
724 {
725 let guard = BuildGuard::new(&self.shared, key);
726 let pipeline = Arc::new(compile(desc));
727 guard.publish(Arc::clone(&pipeline));
728 pipeline
729 }
730
731 fn enqueue(&mut self, descs: &[RenderPipelineDesc]) -> bool {
739 let mut inner = lock(&self.shared.inner);
740 for desc in descs {
741 let Some(key) = self.keys.key_for(desc) else {
742 log::error!(
743 "frust-gpu: warm-up variant has no representable key (sample_count {}); \
744 it will be compiled on first use instead",
745 desc.sample_count
746 );
747 continue;
748 };
749 if inner.built.contains_key(&key) || inner.jobs.contains_key(&key) {
750 continue;
751 }
752 inner.jobs.insert(
753 key,
754 Job {
755 desc: desc.clone(),
756 state: JobState::Pending,
757 },
758 );
759 inner.queue.push_back(key);
760 }
761 if inner.shutdown || inner.worker_live || inner.queue.is_empty() {
762 return false;
763 }
764 inner.worker_live = true;
765 inner.worker_epoch += 1;
766 true
767 }
768
769 fn drain_queue<F>(shared: &Arc<Shared<P>>, compile: F)
776 where
777 F: Fn(&RenderPipelineDesc) -> P,
778 {
779 let _live = LiveWorkerGuard {
780 shared,
781 epoch: lock(&shared.inner).worker_epoch,
782 };
783 loop {
784 let claimed = lock(&shared.inner).claim_next_pending();
785 let Some((key, desc)) = claimed else {
786 return;
787 };
788 let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
789 let guard = BuildGuard::new(shared, key);
790 guard.publish(Arc::new(compile(&desc)));
791 }));
792 if built.is_ok() {
793 lock(&shared.inner).warm_finished += 1;
794 }
795 if built.is_err() {
796 log::error!(
797 "frust-gpu: a warm-up compile panicked; that variant is left to be \
798 rebuilt on request and the rest of the queue continues"
799 );
800 }
801 }
802 }
803
804 fn warm_up<F>(&mut self, descs: &[RenderPipelineDesc], make_compile: impl Fn() -> F)
810 where
811 P: Send + Sync + 'static,
812 F: Fn(&RenderPipelineDesc) -> P + Send + 'static,
813 {
814 if !self.enqueue(descs) {
815 return;
816 }
817 self.join_worker();
821
822 let worker_shared = self.shared();
823 let worker_compile = make_compile();
824 let spawned = std::thread::Builder::new()
825 .name("frust-gpu pipeline warm-up".to_string())
826 .spawn(move || VariantCache::drain_queue(&worker_shared, worker_compile));
827 match spawned {
828 Ok(handle) => self.worker = Some(handle),
829 Err(err) => {
830 log::warn!(
831 "frust-gpu: could not spawn the pipeline warm-up thread ({err}); \
832 building the listed variants inline"
833 );
834 VariantCache::drain_queue(&self.shared(), make_compile());
835 }
836 }
837 }
838
839 fn shutdown(&mut self) {
846 lock(&self.shared.inner).shutdown = true;
847 self.join_worker();
848 }
849
850 fn join_worker(&mut self) {
852 if let Some(worker) = self.worker.take()
853 && worker.join().is_err()
854 {
855 log::error!("frust-gpu: the pipeline warm-up worker panicked");
856 }
857 }
858
859 fn compiled_variants(&self) -> u64 {
861 lock(&self.shared.inner).compiles
862 }
863
864 fn warm_up_finished(&self) -> u64 {
866 lock(&self.shared.inner).warm_finished
867 }
868
869 fn queued_variants(&self) -> usize {
871 let inner = lock(&self.shared.inner);
872 inner
873 .jobs
874 .values()
875 .filter(|job| job.state == JobState::Pending)
876 .count()
877 }
878}
879
880#[derive(Debug)]
889pub struct PipelineCache {
890 core: VariantCache<wgpu::RenderPipeline>,
891 shaders: Arc<ShaderLibrary>,
892 driver_cache: Option<wgpu::PipelineCache>,
893}
894
895impl PipelineCache {
896 #[must_use]
903 pub fn new(shaders: Arc<ShaderLibrary>, driver_cache: Option<wgpu::PipelineCache>) -> Self {
904 Self {
905 core: VariantCache::default(),
906 shaders,
907 driver_cache,
908 }
909 }
910
911 #[must_use]
913 pub fn shaders(&self) -> &ShaderLibrary {
914 &self.shaders
915 }
916
917 pub fn get_or_create(
929 &mut self,
930 device: &wgpu::Device,
931 desc: &RenderPipelineDesc,
932 ) -> &wgpu::RenderPipeline {
933 let shaders = &self.shaders;
934 let driver_cache = self.driver_cache.as_ref();
935 self.core.get_or_create(desc, |desc| {
936 build_render_pipeline(device, shaders, driver_cache, desc)
937 })
938 }
939
940 pub fn warm_up(&mut self, device: &wgpu::Device, descs: &[RenderPipelineDesc]) {
955 let shaders = Arc::clone(&self.shaders);
956 let driver_cache = self.driver_cache.clone();
957 self.core
958 .warm_up(descs, || compiler(device, &shaders, driver_cache.as_ref()));
959 }
960
961 pub fn shutdown(&mut self) {
969 self.core.shutdown();
970 }
971
972 #[must_use]
974 pub fn compiled_variants(&self) -> u64 {
975 self.core.compiled_variants()
976 }
977
978 #[must_use]
980 pub fn queued_variants(&self) -> usize {
981 self.core.queued_variants()
982 }
983
984 #[must_use]
987 pub fn warm_up_finished(&self) -> u64 {
988 self.core.warm_up_finished()
989 }
990
991 #[cfg(feature = "perf-trace")]
997 pub fn take_resolve_wait(&mut self) -> std::time::Duration {
998 std::mem::take(&mut self.core.resolve_wait)
999 }
1000}
1001
1002fn compiler(
1006 device: &wgpu::Device,
1007 shaders: &Arc<ShaderLibrary>,
1008 driver_cache: Option<&wgpu::PipelineCache>,
1009) -> impl Fn(&RenderPipelineDesc) -> wgpu::RenderPipeline + Send + 'static + use<> {
1010 let device = device.clone();
1011 let shaders = Arc::clone(shaders);
1012 let driver_cache = driver_cache.cloned();
1013 move |desc| build_render_pipeline(&device, &shaders, driver_cache.as_ref(), desc)
1014}
1015
1016fn build_render_pipeline(
1019 device: &wgpu::Device,
1020 shaders: &ShaderLibrary,
1021 driver_cache: Option<&wgpu::PipelineCache>,
1022 desc: &RenderPipelineDesc,
1023) -> wgpu::RenderPipeline {
1024 let name = shaders.name_of(desc.shader).unwrap_or("<unknown shader>");
1025 let module = shaders
1026 .get(desc.shader)
1027 .expect("a RenderPipelineDesc's ShaderId must come from this cache's ShaderLibrary");
1028 let layouts: Vec<Option<wgpu::VertexBufferLayout<'_>>> = desc
1033 .vertex_layouts
1034 .iter()
1035 .map(|layout| Some(VertexLayout::as_wgpu(layout)))
1036 .collect();
1037 let label = format!(
1038 "frust-gpu pipeline: {name} [{:?}, msaa x{}]",
1039 desc.format, desc.sample_count
1040 );
1041
1042 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1043 label: Some(&label),
1044 layout: desc.layout.as_ref(),
1047 vertex: wgpu::VertexState {
1048 module,
1049 entry_point: Some(desc.vs.as_ref()),
1050 compilation_options: wgpu::PipelineCompilationOptions::default(),
1051 buffers: &layouts,
1052 },
1053 primitive: wgpu::PrimitiveState {
1054 topology: desc.topology,
1055 ..Default::default()
1056 },
1057 depth_stencil: desc.depth.clone(),
1058 multisample: wgpu::MultisampleState {
1059 count: desc.sample_count,
1060 mask: !0,
1061 alpha_to_coverage_enabled: false,
1062 },
1063 fragment: Some(wgpu::FragmentState {
1064 module,
1065 entry_point: Some(desc.fs.as_ref()),
1066 compilation_options: wgpu::PipelineCompilationOptions::default(),
1067 targets: &[Some(wgpu::ColorTargetState {
1068 format: desc.format,
1069 blend: desc.blend,
1070 write_mask: wgpu::ColorWrites::ALL,
1071 })],
1072 }),
1073 multiview_mask: None,
1074 cache: driver_cache,
1075 })
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use super::*;
1081 use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
1082
1083 #[derive(Debug, PartialEq, Eq)]
1086 struct FakePipeline {
1087 sample_count: u32,
1088 serial: u64,
1089 }
1090
1091 #[derive(Default)]
1094 struct Counter(AtomicU64);
1095
1096 impl Counter {
1097 fn compile(&self, desc: &RenderPipelineDesc) -> FakePipeline {
1098 let serial = self.0.fetch_add(1, Ordering::SeqCst);
1099 FakePipeline {
1100 sample_count: desc.sample_count,
1101 serial,
1102 }
1103 }
1104
1105 fn count(&self) -> u64 {
1106 self.0.load(Ordering::SeqCst)
1107 }
1108 }
1109
1110 fn desc() -> RenderPipelineDesc {
1111 RenderPipelineDesc::new(
1112 ShaderId::from_raw(0),
1113 "vs_main",
1114 "fs_main",
1115 wgpu::TextureFormat::Rgba8Unorm,
1116 )
1117 }
1118
1119 #[test]
1120 fn a_repeated_request_compiles_once() {
1121 let mut cache = VariantCache::<FakePipeline>::default();
1122 let counter = Counter::default();
1123 let desc = desc();
1124
1125 let first = cache.get_or_create(&desc, |d| counter.compile(d)).serial;
1126 for _ in 0..8 {
1127 let again = cache.get_or_create(&desc, |d| counter.compile(d)).serial;
1128 assert_eq!(again, first, "every repeat must return the same pipeline");
1129 }
1130 assert_eq!(counter.count(), 1);
1131 assert_eq!(cache.compiled_variants(), 1);
1132 }
1133
1134 #[test]
1135 fn each_key_axis_is_its_own_variant() {
1136 let base = desc();
1137 let variants = [
1138 RenderPipelineDesc {
1139 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1140 ..base.clone()
1141 },
1142 RenderPipelineDesc {
1143 format: wgpu::TextureFormat::Bgra8Unorm,
1144 ..base.clone()
1145 },
1146 RenderPipelineDesc {
1147 sample_count: 4,
1148 ..base.clone()
1149 },
1150 RenderPipelineDesc {
1151 depth: Some(wgpu::DepthStencilState {
1152 format: wgpu::TextureFormat::Depth32Float,
1153 depth_write_enabled: Some(true),
1154 depth_compare: Some(wgpu::CompareFunction::Less),
1155 stencil: wgpu::StencilState::default(),
1156 bias: wgpu::DepthBiasState::default(),
1157 }),
1158 ..base.clone()
1159 },
1160 RenderPipelineDesc {
1161 shader: ShaderId::from_raw(1),
1162 ..base.clone()
1163 },
1164 RenderPipelineDesc {
1167 fs: "fs_other".into(),
1168 ..base.clone()
1169 },
1170 RenderPipelineDesc {
1172 vertex_layouts: vec![VertexLayout::per_vertex(
1173 16,
1174 vec![wgpu::VertexAttribute {
1175 format: wgpu::VertexFormat::Float32x4,
1176 offset: 0,
1177 shader_location: 0,
1178 }],
1179 )],
1180 ..base.clone()
1181 },
1182 RenderPipelineDesc {
1183 topology: wgpu::PrimitiveTopology::LineList,
1184 ..base.clone()
1185 },
1186 ];
1187
1188 let mut packer = KeyPacker::default();
1189 let base_key = packer.key_for(&base).expect("base key");
1190 let mut seen = vec![base_key];
1191 for variant in &variants {
1192 let key = packer.key_for(variant).expect("variant key");
1193 assert!(
1194 !seen.contains(&key),
1195 "{variant:?} must not reuse an existing key"
1196 );
1197 seen.push(key);
1198 }
1199
1200 let mut cache = VariantCache::<FakePipeline>::default();
1202 let counter = Counter::default();
1203 cache.get_or_create(&base, |d| counter.compile(d));
1204 for variant in &variants {
1205 cache.get_or_create(variant, |d| counter.compile(d));
1206 }
1207 assert_eq!(counter.count(), 1 + variants.len() as u64);
1208 }
1209
1210 #[test]
1211 fn a_key_is_stable_across_repeated_packing() {
1212 let mut packer = KeyPacker::default();
1213 let desc = desc();
1214 let first = packer.key_for(&desc).expect("key");
1215 for _ in 0..4 {
1216 assert_eq!(packer.key_for(&desc), Some(first));
1217 }
1218 }
1219
1220 #[test]
1221 fn an_unusable_sample_count_has_no_key_and_is_not_cached() {
1222 let mut packer = KeyPacker::default();
1223 assert_eq!(
1224 packer.key_for(&RenderPipelineDesc {
1225 sample_count: 3,
1226 ..desc()
1227 }),
1228 None
1229 );
1230 assert_eq!(
1231 packer.key_for(&RenderPipelineDesc {
1232 sample_count: 0,
1233 ..desc()
1234 }),
1235 None
1236 );
1237
1238 let mut cache = VariantCache::<FakePipeline>::default();
1241 let counter = Counter::default();
1242 let odd = RenderPipelineDesc {
1243 sample_count: 3,
1244 ..desc()
1245 };
1246 assert_eq!(
1247 cache
1248 .get_or_create(&odd, |d| counter.compile(d))
1249 .sample_count,
1250 3
1251 );
1252 cache.get_or_create(&odd, |d| counter.compile(d));
1253 assert_eq!(counter.count(), 2, "an unkeyable variant is never cached");
1254 }
1255
1256 #[test]
1257 fn draining_the_queue_builds_every_listed_variant_once() {
1258 let mut cache = VariantCache::<FakePipeline>::default();
1259 let counter = Counter::default();
1260 let listed: Vec<_> = [1u32, 2, 4]
1261 .into_iter()
1262 .map(|sample_count| RenderPipelineDesc {
1263 sample_count,
1264 ..desc()
1265 })
1266 .collect();
1267
1268 cache.enqueue(&listed);
1269 assert_eq!(cache.queued_variants(), 3);
1270 VariantCache::drain_queue(&cache.shared(), |d| counter.compile(d));
1271 assert_eq!(counter.count(), 3);
1272 assert_eq!(cache.queued_variants(), 0);
1273
1274 for desc in &listed {
1276 cache.get_or_create(desc, |d| counter.compile(d));
1277 }
1278 assert_eq!(counter.count(), 3, "a warmed variant must not recompile");
1279 }
1280
1281 #[test]
1282 fn enqueueing_the_same_variant_twice_queues_one_job() {
1283 let mut cache = VariantCache::<FakePipeline>::default();
1284 let listed = [desc(), desc()];
1285 cache.enqueue(&listed);
1286 cache.enqueue(&listed);
1287 assert_eq!(cache.queued_variants(), 1);
1288 }
1289
1290 #[test]
1291 fn a_render_thread_request_steals_a_queued_job_and_the_queue_skips_it() {
1292 let mut cache = VariantCache::<FakePipeline>::default();
1293 let counter = Counter::default();
1294 let listed: Vec<_> = [1u32, 2, 4]
1295 .into_iter()
1296 .map(|sample_count| RenderPipelineDesc {
1297 sample_count,
1298 ..desc()
1299 })
1300 .collect();
1301 cache.enqueue(&listed);
1302
1303 let stolen = cache
1306 .get_or_create(&listed[2], |d| counter.compile(d))
1307 .serial;
1308 assert_eq!(stolen, 0, "the steal is the first compile to run");
1309 assert_eq!(counter.count(), 1);
1310 assert_eq!(cache.queued_variants(), 2, "the stolen job is done");
1311
1312 VariantCache::drain_queue(&cache.shared(), |d| counter.compile(d));
1315 assert_eq!(
1316 counter.count(),
1317 3,
1318 "the stolen variant must not compile twice"
1319 );
1320 assert_eq!(
1321 cache
1322 .get_or_create(&listed[2], |d| counter.compile(d))
1323 .serial,
1324 stolen
1325 );
1326 assert_eq!(counter.count(), 3);
1327 }
1328
1329 #[test]
1330 fn a_worker_thread_drain_publishes_to_the_render_thread() {
1331 let mut cache = VariantCache::<FakePipeline>::default();
1332 let listed: Vec<_> = (0..6)
1333 .map(|i| RenderPipelineDesc {
1334 shader: ShaderId::from_raw(i),
1335 ..desc()
1336 })
1337 .collect();
1338 cache.enqueue(&listed);
1339
1340 let shared = cache.shared();
1341 let worker_counter = Arc::new(Counter::default());
1342 let thread_counter = Arc::clone(&worker_counter);
1343 let worker = std::thread::spawn(move || {
1344 VariantCache::drain_queue(&shared, move |d| thread_counter.compile(d));
1345 });
1346 worker.join().expect("the warm-up worker must not panic");
1347
1348 assert_eq!(worker_counter.count(), 6);
1349 assert_eq!(cache.compiled_variants(), 6);
1350
1351 let render_counter = Counter::default();
1354 for desc in &listed {
1355 cache.get_or_create(desc, |d| render_counter.compile(d));
1356 }
1357 assert_eq!(render_counter.count(), 0);
1358 }
1359
1360 #[test]
1361 fn an_abandoned_build_marks_the_job_failed_rather_than_pending() {
1362 let cache = VariantCache::<FakePipeline>::default();
1363 let shared = cache.shared();
1364 let guard = BuildGuard::new(&shared, 42);
1365 lock(&shared.inner).jobs.insert(
1366 42,
1367 Job {
1368 desc: desc(),
1369 state: JobState::Building,
1370 },
1371 );
1372 drop(guard);
1374 assert_eq!(
1375 lock(&shared.inner).jobs.get(&42).map(|job| job.state),
1376 Some(JobState::Failed),
1377 "an abandoned build must not leave a Pending job no worker can claim"
1378 );
1379 }
1380
1381 struct PanicOnce {
1384 counter: Arc<Counter>,
1385 armed: AtomicBool,
1386 }
1387
1388 impl PanicOnce {
1389 fn new(counter: &Arc<Counter>) -> Self {
1390 Self {
1391 counter: Arc::clone(counter),
1392 armed: AtomicBool::new(true),
1393 }
1394 }
1395
1396 fn compile(&self, desc: &RenderPipelineDesc) -> FakePipeline {
1397 assert!(
1398 !self.armed.swap(false, Ordering::SeqCst),
1399 "the fake compile fails its first call"
1400 );
1401 self.counter.compile(desc)
1402 }
1403 }
1404
1405 #[test]
1406 fn a_panicking_warm_up_compile_costs_one_variant_and_not_the_queue() {
1407 let mut cache = VariantCache::<FakePipeline>::default();
1408 let counter = Arc::new(Counter::default());
1409 let failing = PanicOnce::new(&counter);
1410 let listed: Vec<_> = [1u32, 2, 4]
1411 .into_iter()
1412 .map(|sample_count| RenderPipelineDesc {
1413 sample_count,
1414 ..desc()
1415 })
1416 .collect();
1417
1418 cache.enqueue(&listed);
1419 VariantCache::drain_queue(&cache.shared(), |d| failing.compile(d));
1420
1421 assert_eq!(counter.count(), 2);
1424 assert_eq!(
1425 cache.queued_variants(),
1426 0,
1427 "a failed job must not be counted as still queued forever"
1428 );
1429
1430 assert_eq!(
1432 cache
1433 .get_or_create(&listed[0], |d| counter.compile(d))
1434 .sample_count,
1435 1
1436 );
1437 assert_eq!(counter.count(), 3);
1438
1439 for desc in &listed {
1441 cache.get_or_create(desc, |d| counter.compile(d));
1442 }
1443 assert_eq!(counter.count(), 3);
1444 }
1445
1446 #[test]
1447 fn a_panicking_inline_build_surfaces_to_the_requester_and_leaves_it_rebuildable() {
1448 let mut cache = VariantCache::<FakePipeline>::default();
1449 let counter = Arc::new(Counter::default());
1450 let failing = PanicOnce::new(&counter);
1451 let desc = desc();
1452 cache.enqueue(std::slice::from_ref(&desc));
1453
1454 let stolen = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1457 cache.get_or_create(&desc, |d| failing.compile(d));
1458 }));
1459 assert!(
1460 stolen.is_err(),
1461 "an inline compile must not swallow a panic"
1462 );
1463 assert_eq!(counter.count(), 0);
1464
1465 assert_eq!(cache.get_or_create(&desc, |d| counter.compile(d)).serial, 0);
1468 assert_eq!(counter.count(), 1);
1469 assert_eq!(cache.queued_variants(), 0);
1470 }
1471
1472 #[derive(Default)]
1476 struct Latch {
1477 open: Mutex<bool>,
1478 changed: Condvar,
1479 }
1480
1481 impl Latch {
1482 fn wait(&self) {
1483 let mut open = lock(&self.open);
1484 while !*open {
1485 open = self
1486 .changed
1487 .wait(open)
1488 .unwrap_or_else(PoisonError::into_inner);
1489 }
1490 }
1491
1492 fn open(&self) {
1493 *lock(&self.open) = true;
1494 self.changed.notify_all();
1495 }
1496 }
1497
1498 fn shader_variants(count: u32) -> Vec<RenderPipelineDesc> {
1499 (0..count)
1500 .map(|i| RenderPipelineDesc {
1501 shader: ShaderId::from_raw(i),
1502 ..desc()
1503 })
1504 .collect()
1505 }
1506
1507 #[test]
1508 fn repeated_warm_up_calls_keep_at_most_one_worker() {
1509 let mut cache = VariantCache::<FakePipeline>::default();
1510 let counter = Arc::new(Counter::default());
1511 let latch = Arc::new(Latch::default());
1512 let workers = AtomicU64::new(0);
1513 let listed = shader_variants(6);
1514
1515 let make_compile = || {
1518 workers.fetch_add(1, Ordering::SeqCst);
1519 let counter = Arc::clone(&counter);
1520 let latch = Arc::clone(&latch);
1521 let held = AtomicBool::new(false);
1522 move |d: &RenderPipelineDesc| {
1523 if !held.swap(true, Ordering::SeqCst) {
1524 latch.wait();
1525 }
1526 counter.compile(d)
1527 }
1528 };
1529
1530 cache.warm_up(&listed[..1], make_compile);
1533 for _ in 0..8 {
1534 cache.warm_up(&listed, make_compile);
1535 }
1536 assert_eq!(
1537 workers.load(Ordering::SeqCst),
1538 1,
1539 "a live worker must absorb further warm-up calls, not be joined by more"
1540 );
1541
1542 latch.open();
1543 cache.shutdown();
1544
1545 for desc in &listed {
1548 cache.get_or_create(desc, |d| counter.compile(d));
1549 }
1550 assert_eq!(counter.count(), listed.len() as u64);
1551 assert_eq!(cache.compiled_variants(), listed.len() as u64);
1552 }
1553
1554 #[test]
1555 fn dropping_the_cache_joins_its_worker() {
1556 let device = Arc::new(());
1560 let counter = Arc::new(Counter::default());
1561 let listed = shader_variants(4);
1562
1563 let mut cache = VariantCache::<FakePipeline>::default();
1564 cache.warm_up(&listed, || {
1565 let device = Arc::clone(&device);
1566 let counter = Arc::clone(&counter);
1567 move |d: &RenderPipelineDesc| {
1568 let _held = Arc::clone(&device);
1569 counter.compile(d)
1570 }
1571 });
1572 drop(cache);
1573
1574 assert_eq!(
1575 Arc::strong_count(&device),
1576 1,
1577 "no warm-up worker may outlive the cache that started it"
1578 );
1579 }
1580
1581 #[test]
1582 fn warming_up_after_shutdown_starts_no_worker_and_still_builds_on_request() {
1583 let mut cache = VariantCache::<FakePipeline>::default();
1584 let counter = Arc::new(Counter::default());
1585 let workers = AtomicU64::new(0);
1586 let listed = shader_variants(3);
1587
1588 cache.shutdown();
1589 cache.shutdown();
1591 cache.warm_up(&listed, || {
1592 workers.fetch_add(1, Ordering::SeqCst);
1593 let counter = Arc::clone(&counter);
1594 move |d: &RenderPipelineDesc| counter.compile(d)
1595 });
1596
1597 assert_eq!(workers.load(Ordering::SeqCst), 0);
1598 for desc in &listed {
1599 cache.get_or_create(desc, |d| counter.compile(d));
1600 }
1601 assert_eq!(counter.count(), listed.len() as u64);
1602 }
1603
1604 fn drain_error_scope(
1608 device: &wgpu::Device,
1609 scope: wgpu::ErrorScopeGuard,
1610 ) -> Option<wgpu::Error> {
1611 use std::task::{Context, Poll, Waker};
1612
1613 let waker = Waker::noop();
1614 let mut cx = Context::from_waker(waker);
1615 let mut future = std::pin::pin!(scope.pop());
1616 loop {
1617 match future.as_mut().poll(&mut cx) {
1618 Poll::Ready(error) => return error,
1619 Poll::Pending => {
1620 let _ = device.poll(wgpu::PollType::wait_indefinitely());
1621 }
1622 }
1623 }
1624 }
1625
1626 #[test]
1632 #[ignore = "requires a GPU (Metal/Vulkan); run locally with `cargo test -p frust-gpu -- --ignored`"]
1633 fn creates_one_real_pipeline() {
1634 const WGSL: &str = r#"
1635@vertex
1636fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
1637 let uv = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
1638 return vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
1639}
1640@fragment
1641fn fs_main() -> @location(0) vec4<f32> {
1642 return vec4<f32>(0.0, 1.0, 0.0, 1.0);
1643}
1644"#;
1645
1646 let (device, _queue) = pollster::block_on(async {
1647 let instance = wgpu::Instance::new(
1648 wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
1649 );
1650 let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, None)
1654 .await
1655 .expect("no compatible GPU adapter");
1656 println!("frust-gpu pipeline test adapter: {:?}", adapter.get_info());
1657 adapter
1658 .request_device(&wgpu::DeviceDescriptor {
1659 label: Some("frust-gpu pipeline test device"),
1660 required_features: wgpu::Features::empty(),
1661 required_limits: wgpu::Limits::default(),
1662 ..Default::default()
1663 })
1664 .await
1665 .expect("failed to create the device")
1666 });
1667
1668 let mut library = ShaderLibrary::new();
1669 let shader = library.insert_wgsl(&device, "fullscreen-green", WGSL);
1670 let mut cache = PipelineCache::new(Arc::new(library), None);
1671
1672 let desc = RenderPipelineDesc::new(
1673 shader,
1674 "vs_main",
1675 "fs_main",
1676 wgpu::TextureFormat::Rgba8Unorm,
1677 );
1678 let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
1679 for _ in 0..2 {
1680 let _pipeline = cache.get_or_create(&device, &desc);
1681 }
1682 let error = drain_error_scope(&device, scope);
1683
1684 assert!(error.is_none(), "pipeline creation raised {error:?}");
1685 assert_eq!(
1686 cache.compiled_variants(),
1687 1,
1688 "a repeat request must reuse the pipeline"
1689 );
1690
1691 explicit_layout_is_its_own_variant_and_shares_bind_groups(&device);
1694 }
1695
1696 fn explicit_layout_is_its_own_variant_and_shares_bind_groups(device: &wgpu::Device) {
1702 const LAYOUT_WGSL: &str = r#"
1703@group(0) @binding(0) var<uniform> tint: vec4<f32>;
1704@vertex
1705fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
1706 let uv = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
1707 return vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
1708}
1709@fragment
1710fn fs_tinted() -> @location(0) vec4<f32> {
1711 return tint;
1712}
1713@fragment
1714fn fs_plain() -> @location(0) vec4<f32> {
1715 return vec4<f32>(0.0, 1.0, 0.0, 1.0);
1716}
1717"#;
1718
1719 let group = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1720 label: Some("explicit layout test group"),
1721 entries: &[wgpu::BindGroupLayoutEntry {
1722 binding: 0,
1723 visibility: wgpu::ShaderStages::FRAGMENT,
1724 ty: wgpu::BindingType::Buffer {
1725 ty: wgpu::BufferBindingType::Uniform,
1726 has_dynamic_offset: false,
1727 min_binding_size: wgpu::BufferSize::new(16),
1728 },
1729 count: None,
1730 }],
1731 });
1732 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1733 label: Some("explicit layout test"),
1734 bind_group_layouts: &[Some(&group)],
1735 immediate_size: 0,
1736 });
1737
1738 let mut library = ShaderLibrary::new();
1739 let shader = library.insert_wgsl(device, "explicit-layout", LAYOUT_WGSL);
1740 let mut cache = PipelineCache::new(Arc::new(library), None);
1741 let tinted = RenderPipelineDesc::new(
1742 shader,
1743 "vs_main",
1744 "fs_tinted",
1745 wgpu::TextureFormat::Rgba8Unorm,
1746 );
1747 let explicit_tinted = RenderPipelineDesc {
1748 layout: Some(layout.clone()),
1749 ..tinted.clone()
1750 };
1751 let explicit_plain = RenderPipelineDesc {
1755 layout: Some(layout.clone()),
1756 ..RenderPipelineDesc::new(
1757 shader,
1758 "vs_main",
1759 "fs_plain",
1760 wgpu::TextureFormat::Rgba8Unorm,
1761 )
1762 };
1763
1764 let mut packer = KeyPacker::default();
1765 assert_ne!(packer.key_for(&tinted), packer.key_for(&explicit_tinted));
1766 assert_eq!(
1767 packer.key_for(&explicit_tinted),
1768 packer.key_for(&RenderPipelineDesc {
1769 layout: Some(layout.clone()),
1770 ..tinted.clone()
1771 }),
1772 "the same layout object is the same variant"
1773 );
1774
1775 let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
1776 let _ = cache.get_or_create(device, &tinted);
1777 let a = cache.get_or_create(device, &explicit_tinted).clone();
1778 let b = cache.get_or_create(device, &explicit_plain).clone();
1779 let _ = cache.get_or_create(device, &explicit_tinted);
1780 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
1781 label: None,
1782 size: 16,
1783 usage: wgpu::BufferUsages::UNIFORM,
1784 mapped_at_creation: false,
1785 });
1786 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1789 label: None,
1790 layout: &a.get_bind_group_layout(0),
1791 entries: &[wgpu::BindGroupEntry {
1792 binding: 0,
1793 resource: buffer.as_entire_binding(),
1794 }],
1795 });
1796 let target = device.create_texture(&wgpu::TextureDescriptor {
1797 label: None,
1798 size: wgpu::Extent3d {
1799 width: 4,
1800 height: 4,
1801 depth_or_array_layers: 1,
1802 },
1803 mip_level_count: 1,
1804 sample_count: 1,
1805 dimension: wgpu::TextureDimension::D2,
1806 format: wgpu::TextureFormat::Rgba8Unorm,
1807 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1808 view_formats: &[],
1809 });
1810 let view = target.create_view(&wgpu::TextureViewDescriptor::default());
1811 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
1812 {
1813 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1814 label: None,
1815 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1816 view: &view,
1817 depth_slice: None,
1818 resolve_target: None,
1819 ops: wgpu::Operations {
1820 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1821 store: wgpu::StoreOp::Store,
1822 },
1823 })],
1824 depth_stencil_attachment: None,
1825 timestamp_writes: None,
1826 occlusion_query_set: None,
1827 multiview_mask: None,
1828 });
1829 pass.set_bind_group(0, &bind_group, &[]);
1830 pass.set_pipeline(&a);
1831 pass.draw(0..3, 0..1);
1832 pass.set_pipeline(&b);
1833 pass.draw(0..3, 0..1);
1834 }
1835 drop(encoder.finish());
1836 drop(group);
1837 let error = drain_error_scope(device, scope);
1838 assert!(error.is_none(), "pipeline creation raised {error:?}");
1839 assert_eq!(cache.compiled_variants(), 3);
1840 }
1841
1842 #[test]
1843 fn a_new_desc_derives_its_layout() {
1844 assert!(
1845 desc().layout.is_none(),
1846 "every existing desc keeps wgpu's derived layout unless it names one"
1847 );
1848 let mut packer = KeyPacker::default();
1851 let key = packer.key_for(&desc()).expect("key");
1852 assert_eq!(key >> LAYOUT_SHIFT, 0);
1853 }
1854}