1use 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
144const PROGRAM_BITS: u32 = 16;
146const FORMAT_BITS: u32 = 12;
148const BLEND_BITS: u32 = 12;
150const DEPTH_BITS: u32 = 12;
152const SAMPLE_BITS: u32 = 4;
154const 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#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
176pub struct VertexLayout {
177 pub array_stride: wgpu::BufferAddress,
179 pub step_mode: wgpu::VertexStepMode,
181 pub attributes: Vec<wgpu::VertexAttribute>,
183}
184
185impl VertexLayout {
186 #[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 #[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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
221pub struct RenderPipelineDesc {
222 pub shader: ShaderId,
224 pub vs: Cow<'static, str>,
226 pub fs: Cow<'static, str>,
228 pub vertex_layouts: Vec<VertexLayout>,
231 pub blend: Option<wgpu::BlendState>,
233 pub format: wgpu::TextureFormat,
235 pub sample_count: u32,
237 pub depth: Option<wgpu::DepthStencilState>,
239 pub topology: wgpu::PrimitiveTopology,
242 pub layout: Option<wgpu::PipelineLayout>,
249}
250
251impl RenderPipelineDesc {
252 #[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#[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 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#[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#[derive(Debug, Default)]
344struct ProgramTable {
345 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
371fn 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#[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 fn key_for(&mut self, desc: &RenderPipelineDesc) -> Option<u64> {
409 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
428fn 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
439enum JobState {
440 Pending,
442 Building,
444 Done,
446 Failed,
450}
451
452#[derive(Debug)]
454struct Job {
455 desc: RenderPipelineDesc,
456 state: JobState,
457}
458
459#[derive(Debug)]
461struct Inner<P> {
462 built: HashMap<u64, Arc<P>>,
463 jobs: HashMap<u64, Job>,
464 queue: VecDeque<u64>,
467 compiles: u64,
469 worker_live: bool,
473 shutdown: bool,
476 worker_epoch: u64,
479 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 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 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 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#[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
558fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
562 mutex.lock().unwrap_or_else(PoisonError::into_inner)
563}
564
565struct 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 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
602struct 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#[derive(Debug)]
643struct VariantCache<P> {
644 shared: Arc<Shared<P>>,
645 local: HashMap<u64, Arc<P>>,
648 overflow: Option<Arc<P>>,
651 keys: KeyPacker,
652 warned_key_space: bool,
653 worker: Option<std::thread::JoinHandle<()>>,
655 #[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 fn shared(&self) -> Arc<Shared<P>> {
686 Arc::clone(&self.shared)
687 }
688
689 fn is_shut_down(&self) -> bool {
691 lock(&self.shared.inner).shutdown
692 }
693
694 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 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 Some(JobState::Building) => {
742 inner = self
743 .shared
744 .built
745 .wait(inner)
746 .unwrap_or_else(PoisonError::into_inner);
747 }
748 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 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 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 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 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 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 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 fn shutdown(&mut self) {
902 lock(&self.shared.inner).shutdown = true;
903 self.join_worker();
904 }
905
906 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 fn compiled_variants(&self) -> u64 {
917 lock(&self.shared.inner).compiles
918 }
919
920 fn warm_up_finished(&self) -> u64 {
922 lock(&self.shared.inner).warm_finished
923 }
924
925 fn warm_up_done(&self) -> bool {
928 lock(&self.shared.inner).warm_up_settled()
929 }
930
931 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 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#[derive(Debug)]
979pub struct PipelineCache {
980 core: VariantCache<wgpu::RenderPipeline>,
981 shaders: Arc<ShaderLibrary>,
982 driver_cache: Option<wgpu::PipelineCache>,
983 shader_workers: ShaderWorkers,
986}
987
988impl PipelineCache {
989 #[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 #[must_use]
1007 pub fn shaders(&self) -> &ShaderLibrary {
1008 &self.shaders
1009 }
1010
1011 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 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 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 pub fn shutdown(&mut self) {
1131 self.core.shutdown();
1132 self.shader_workers.join_all();
1133 }
1134
1135 #[must_use]
1137 pub fn compiled_variants(&self) -> u64 {
1138 self.core.compiled_variants()
1139 }
1140
1141 #[must_use]
1143 pub fn queued_variants(&self) -> usize {
1144 self.core.queued_variants()
1145 }
1146
1147 #[must_use]
1150 pub fn warm_up_finished(&self) -> u64 {
1151 self.core.warm_up_finished()
1152 }
1153
1154 #[must_use]
1159 pub fn warm_up_done(&self) -> bool {
1160 self.core.warm_up_done()
1161 }
1162
1163 pub fn wait_warm_up(&mut self, timeout: Duration) -> bool {
1178 self.core.wait_warm_up(timeout)
1179 }
1180
1181 #[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
1193fn shader_thread_count(modules: usize, parallelism: usize) -> usize {
1198 modules.min(parallelism.saturating_sub(1)).max(1)
1199}
1200
1201#[derive(Debug, Default)]
1205struct ShaderWorkers(Vec<std::thread::JoinHandle<()>>);
1206
1207impl ShaderWorkers {
1208 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 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
1240fn 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
1254fn 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 let module = shaders
1266 .module(device, desc.shader)
1267 .expect("a RenderPipelineDesc's ShaderId must come from this cache's ShaderLibrary");
1268 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 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 #[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 #[derive(Debug, PartialEq, Eq)]
1350 struct FakePipeline {
1351 sample_count: u32,
1352 serial: u64,
1353 }
1354
1355 #[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 RenderPipelineDesc {
1431 fs: "fs_other".into(),
1432 ..base.clone()
1433 },
1434 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[derive(Default)]
1740 struct Latch {
1741 open: Mutex<bool>,
1742 changed: Condvar,
1743 }
1744
1745 const LATCH_WAIT: Duration = Duration::from_secs(30);
1748
1749 impl Latch {
1750 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 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 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 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 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 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 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 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 let _release = OpenOnDrop(Arc::clone(&latch));
1919 let listed = shader_variants(5);
1920
1921 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 entered.wait();
1936
1937 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 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 for desc in &listed {
1971 cache.get_or_create(desc, |d| counter.compile(d));
1972 }
1973 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 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 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 #[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 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 explicit_layout_is_its_own_variant_and_shares_bind_groups(&device);
2095 }
2096
2097 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 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 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 let mut packer = KeyPacker::default();
2252 let key = packer.key_for(&desc()).expect("key");
2253 assert_eq!(key >> LAYOUT_SHIFT, 0);
2254 }
2255
2256 #[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}