Skip to main content

bevy_render/render_resource/
pipeline_cache.rs

1use bevy_material::descriptor::{
2    BindGroupLayoutDescriptor, CachedComputePipelineId, CachedRenderPipelineId,
3    ComputePipelineDescriptor, PipelineDescriptor, RenderPipelineDescriptor,
4};
5
6use crate::{
7    render_resource::*,
8    renderer::{RenderAdapter, RenderDevice, WgpuWrapper},
9    Extract,
10};
11use alloc::{borrow::Cow, sync::Arc};
12use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
13use bevy_ecs::{
14    message::MessageReader,
15    resource::Resource,
16    system::{Res, ResMut},
17};
18use bevy_log::error;
19use bevy_platform::collections::{HashMap, HashSet};
20use bevy_shader::{
21    CachedPipelineId, Shader, ShaderCache, ShaderCacheError, ShaderCacheSource, ShaderDefVal,
22    ValidateShader,
23};
24use bevy_tasks::Task;
25use bevy_utils::default;
26use core::{future::Future, mem};
27use std::sync::{Mutex, PoisonError};
28use wgpu::{PipelineCompilationOptions, VertexBufferLayout as RawVertexBufferLayout};
29
30/// A pipeline defining the data layout and shader logic for a specific GPU task.
31///
32/// Used to store a heterogenous collection of render and compute pipelines together.
33#[derive(Debug)]
34pub enum Pipeline {
35    RenderPipeline(RenderPipeline),
36    ComputePipeline(ComputePipeline),
37}
38
39pub struct CachedPipeline {
40    pub descriptor: PipelineDescriptor,
41    pub state: CachedPipelineState,
42}
43
44/// State of a cached pipeline inserted into a [`PipelineCache`].
45#[derive(Debug)]
46pub enum CachedPipelineState {
47    /// The pipeline GPU object is queued for creation.
48    Queued,
49    /// The pipeline GPU object is being created.
50    Creating(Task<Result<Pipeline, ShaderCacheError>>),
51    /// The pipeline GPU object was created successfully and is available (allocated on the GPU).
52    Ok(Pipeline),
53    /// An error occurred while trying to create the pipeline GPU object.
54    Err(ShaderCacheError),
55}
56
57impl CachedPipelineState {
58    /// Convenience method to "unwrap" a pipeline state into its underlying GPU object.
59    ///
60    /// # Returns
61    ///
62    /// The method returns the allocated pipeline GPU object.
63    ///
64    /// # Panics
65    ///
66    /// This method panics if the pipeline GPU object is not available, either because it is
67    /// pending creation or because an error occurred while attempting to create GPU object.
68    pub fn unwrap(&self) -> &Pipeline {
69        match self {
70            CachedPipelineState::Ok(pipeline) => pipeline,
71            CachedPipelineState::Queued => {
72                panic!("Pipeline has not been compiled yet. It is still in the 'Queued' state.")
73            }
74            CachedPipelineState::Creating(..) => {
75                panic!("Pipeline has not been compiled yet. It is still in the 'Creating' state.")
76            }
77            CachedPipelineState::Err(err) => panic!("{}", err),
78        }
79    }
80}
81
82type ImmediateSize = u32;
83type LayoutCacheKey = (Vec<BindGroupLayoutId>, ImmediateSize);
84#[derive(Default)]
85struct LayoutCache {
86    layouts: HashMap<LayoutCacheKey, Arc<WgpuWrapper<PipelineLayout>>>,
87}
88
89impl LayoutCache {
90    fn get(
91        &mut self,
92        render_device: &RenderDevice,
93        bind_group_layouts: &[BindGroupLayout],
94        immediate_size: u32,
95    ) -> Arc<WgpuWrapper<PipelineLayout>> {
96        let bind_group_ids = bind_group_layouts.iter().map(BindGroupLayout::id).collect();
97        self.layouts
98            .entry((bind_group_ids, immediate_size))
99            .or_insert_with_key(|(_, immediate_size)| {
100                let bind_group_layouts = bind_group_layouts
101                    .iter()
102                    .map(BindGroupLayout::value)
103                    .map(Some)
104                    .collect::<Vec<_>>();
105                Arc::new(WgpuWrapper::new(render_device.create_pipeline_layout(
106                    &PipelineLayoutDescriptor {
107                        bind_group_layouts: &bind_group_layouts,
108                        immediate_size: *immediate_size,
109                        ..default()
110                    },
111                )))
112            })
113            .clone()
114    }
115}
116
117fn load_module(
118    render_device: &RenderDevice,
119    shader_source: ShaderCacheSource,
120    validate_shader: &ValidateShader,
121) -> Result<WgpuWrapper<ShaderModule>, ShaderCacheError> {
122    let shader_source = match shader_source {
123        #[cfg(feature = "shader_format_spirv")]
124        ShaderCacheSource::SpirV(data) => wgpu::util::make_spirv(data),
125        #[cfg(not(feature = "shader_format_spirv"))]
126        ShaderCacheSource::SpirV(_) => {
127            unimplemented!("Enable feature \"shader_format_spirv\" to use SPIR-V shaders")
128        }
129        ShaderCacheSource::Wgsl(src) => ShaderSource::Wgsl(Cow::Owned(src)),
130        #[cfg(not(feature = "decoupled_naga"))]
131        ShaderCacheSource::Naga(src) => ShaderSource::Naga(Cow::Owned(src)),
132    };
133    let module_descriptor = ShaderModuleDescriptor {
134        label: None,
135        source: shader_source,
136    };
137
138    let scope = render_device
139        .wgpu_device()
140        .push_error_scope(wgpu::ErrorFilter::Validation);
141
142    let shader_module = WgpuWrapper::new(match validate_shader {
143        ValidateShader::Enabled => {
144            render_device.create_and_validate_shader_module(module_descriptor)
145        }
146        // SAFETY: we are interfacing with shader code, which may contain undefined behavior,
147        // such as indexing out of bounds.
148        // The checks required are prohibitively expensive and a poor default for game engines.
149        ValidateShader::Disabled => unsafe {
150            render_device.create_shader_module(module_descriptor)
151        },
152    });
153
154    let error = scope.pop();
155
156    // `now_or_never` will return Some if the future is ready and None otherwise.
157    // On native platforms, wgpu will yield the error immediately while on wasm it may take longer since the browser APIs are asynchronous.
158    // So to keep the complexity of the ShaderCache low, we will only catch this error early on native platforms,
159    // and on wasm the error will be handled by wgpu and crash the application.
160    if let Some(Some(wgpu::Error::Validation { description, .. })) =
161        bevy_tasks::futures::now_or_never(error)
162    {
163        return Err(ShaderCacheError::CreateShaderModule(description));
164    }
165
166    Ok(shader_module)
167}
168
169#[derive(Default)]
170struct BindGroupLayoutCache {
171    bgls: HashMap<BindGroupLayoutDescriptor, BindGroupLayout>,
172}
173
174impl BindGroupLayoutCache {
175    fn get(
176        &mut self,
177        render_device: &RenderDevice,
178        descriptor: BindGroupLayoutDescriptor,
179    ) -> BindGroupLayout {
180        self.bgls
181            .entry(descriptor)
182            .or_insert_with_key(|descriptor| {
183                render_device
184                    .create_bind_group_layout(descriptor.label.as_ref(), &descriptor.entries)
185            })
186            .clone()
187    }
188}
189
190/// Cache for render and compute pipelines.
191///
192/// The cache stores existing render and compute pipelines allocated on the GPU, as well as
193/// pending creation. Pipelines inserted into the cache are identified by a unique ID, which
194/// can be used to retrieve the actual GPU object once it's ready. The creation of the GPU
195/// pipeline object is deferred to the [`RenderSystems::Render`] step, just before the render
196/// graph starts being processed, as this requires access to the GPU.
197///
198/// Note that the cache does not perform automatic deduplication of identical pipelines. It is
199/// up to the user not to insert the same pipeline twice to avoid wasting GPU resources.
200///
201/// [`RenderSystems::Render`]: crate::RenderSystems::Render
202#[derive(Resource)]
203pub struct PipelineCache {
204    layout_cache: Arc<Mutex<LayoutCache>>,
205    bindgroup_layout_cache: Arc<Mutex<BindGroupLayoutCache>>,
206    shader_cache: Arc<Mutex<ShaderCache<WgpuWrapper<ShaderModule>, RenderDevice>>>,
207    device: RenderDevice,
208    pipelines: Vec<CachedPipeline>,
209    waiting_pipelines: HashSet<CachedPipelineId>,
210    new_pipelines: Mutex<Vec<CachedPipeline>>,
211    global_shader_defs: Vec<ShaderDefVal>,
212    /// If `true`, disables asynchronous pipeline compilation.
213    /// This has no effect on macOS, wasm, or without the `multi_threaded` feature.
214    pub(crate) synchronous_pipeline_compilation: bool,
215    /// If `true`, the shader cache needs to be repopulated from the main world's `Assets<Shader>`.
216    needs_shader_reload: bool,
217}
218
219impl PipelineCache {
220    /// Returns an iterator over the pipelines in the pipeline cache.
221    pub fn pipelines(&self) -> impl Iterator<Item = &CachedPipeline> {
222        self.pipelines.iter()
223    }
224
225    /// Returns a iterator of the IDs of all currently waiting pipelines.
226    pub fn waiting_pipelines(&self) -> impl Iterator<Item = CachedPipelineId> + '_ {
227        self.waiting_pipelines.iter().copied()
228    }
229
230    /// Create a new pipeline cache associated with the given render device.
231    pub fn new(
232        device: RenderDevice,
233        render_adapter: RenderAdapter,
234        synchronous_pipeline_compilation: bool,
235    ) -> Self {
236        let mut global_shader_defs = Vec::new();
237        #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
238        {
239            global_shader_defs.push("NO_ARRAY_TEXTURES_SUPPORT".into());
240            global_shader_defs.push("NO_CUBE_ARRAY_TEXTURES_SUPPORT".into());
241            global_shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into());
242        }
243
244        if cfg!(target_abi = "sim") {
245            global_shader_defs.push("NO_CUBE_ARRAY_TEXTURES_SUPPORT".into());
246        }
247
248        global_shader_defs.push(ShaderDefVal::UInt(
249            String::from("AVAILABLE_STORAGE_BUFFER_BINDINGS"),
250            device.limits().max_storage_buffers_per_shader_stage,
251        ));
252
253        Self {
254            shader_cache: Arc::new(Mutex::new(ShaderCache::new(
255                device.clone(),
256                device.features(),
257                render_adapter.get_downlevel_capabilities().flags,
258                load_module,
259            ))),
260            device,
261            layout_cache: default(),
262            bindgroup_layout_cache: default(),
263            waiting_pipelines: default(),
264            new_pipelines: default(),
265            pipelines: default(),
266            global_shader_defs,
267            synchronous_pipeline_compilation,
268            needs_shader_reload: true,
269        }
270    }
271
272    /// Get the state of a cached render pipeline.
273    ///
274    /// See [`PipelineCache::queue_render_pipeline()`].
275    #[inline]
276    pub fn get_render_pipeline_state(&self, id: CachedRenderPipelineId) -> &CachedPipelineState {
277        // If the pipeline id isn't in `pipelines`, it's queued in `new_pipelines`
278        self.pipelines
279            .get(id.id())
280            .map_or(&CachedPipelineState::Queued, |pipeline| &pipeline.state)
281    }
282
283    /// Get the state of a cached compute pipeline.
284    ///
285    /// See [`PipelineCache::queue_compute_pipeline()`].
286    #[inline]
287    pub fn get_compute_pipeline_state(&self, id: CachedComputePipelineId) -> &CachedPipelineState {
288        // If the pipeline id isn't in `pipelines`, it's queued in `new_pipelines`
289        self.pipelines
290            .get(id.id())
291            .map_or(&CachedPipelineState::Queued, |pipeline| &pipeline.state)
292    }
293
294    /// Get the render pipeline descriptor a cached render pipeline was inserted from.
295    ///
296    /// See [`PipelineCache::queue_render_pipeline()`].
297    ///
298    /// **Note**: Be careful calling this method. It will panic if called with a pipeline that
299    /// has been queued but has not yet been processed by [`PipelineCache::process_queue()`].
300    #[inline]
301    pub fn get_render_pipeline_descriptor(
302        &self,
303        id: CachedRenderPipelineId,
304    ) -> &RenderPipelineDescriptor {
305        match &self.pipelines[id.id()].descriptor {
306            PipelineDescriptor::RenderPipelineDescriptor(descriptor) => descriptor,
307            PipelineDescriptor::ComputePipelineDescriptor(_) => unreachable!(),
308        }
309    }
310
311    /// Get the compute pipeline descriptor a cached render pipeline was inserted from.
312    ///
313    /// See [`PipelineCache::queue_compute_pipeline()`].
314    ///
315    /// **Note**: Be careful calling this method. It will panic if called with a pipeline that
316    /// has been queued but has not yet been processed by [`PipelineCache::process_queue()`].
317    #[inline]
318    pub fn get_compute_pipeline_descriptor(
319        &self,
320        id: CachedComputePipelineId,
321    ) -> &ComputePipelineDescriptor {
322        match &self.pipelines[id.id()].descriptor {
323            PipelineDescriptor::RenderPipelineDescriptor(_) => unreachable!(),
324            PipelineDescriptor::ComputePipelineDescriptor(descriptor) => descriptor,
325        }
326    }
327
328    /// Try to retrieve a render pipeline GPU object from a cached ID.
329    ///
330    /// # Returns
331    ///
332    /// This method returns a successfully created render pipeline if any, or `None` if the pipeline
333    /// was not created yet or if there was an error during creation. You can check the actual creation
334    /// state with [`PipelineCache::get_render_pipeline_state()`].
335    #[inline]
336    pub fn get_render_pipeline(&self, id: CachedRenderPipelineId) -> Option<&RenderPipeline> {
337        if let CachedPipelineState::Ok(Pipeline::RenderPipeline(pipeline)) =
338            &self.pipelines.get(id.id())?.state
339        {
340            Some(pipeline)
341        } else {
342            None
343        }
344    }
345
346    /// Wait for a render pipeline to finish compiling.
347    #[inline]
348    pub fn block_on_render_pipeline(&mut self, id: CachedRenderPipelineId) {
349        if self.pipelines.len() <= id.id() {
350            self.process_queue();
351        }
352
353        let state = &mut self.pipelines[id.id()].state;
354        if let CachedPipelineState::Creating(task) = state {
355            *state = match bevy_tasks::block_on(task) {
356                Ok(p) => CachedPipelineState::Ok(p),
357                Err(e) => CachedPipelineState::Err(e),
358            };
359        }
360    }
361
362    /// Try to retrieve a compute pipeline GPU object from a cached ID.
363    ///
364    /// # Returns
365    ///
366    /// This method returns a successfully created compute pipeline if any, or `None` if the pipeline
367    /// was not created yet or if there was an error during creation. You can check the actual creation
368    /// state with [`PipelineCache::get_compute_pipeline_state()`].
369    #[inline]
370    pub fn get_compute_pipeline(&self, id: CachedComputePipelineId) -> Option<&ComputePipeline> {
371        if let CachedPipelineState::Ok(Pipeline::ComputePipeline(pipeline)) =
372            &self.pipelines.get(id.id())?.state
373        {
374            Some(pipeline)
375        } else {
376            None
377        }
378    }
379
380    /// Insert a render pipeline into the cache, and queue its creation.
381    ///
382    /// The pipeline is always inserted and queued for creation. There is no attempt to deduplicate it with
383    /// an already cached pipeline.
384    ///
385    /// # Returns
386    ///
387    /// This method returns the unique render shader ID of the cached pipeline, which can be used to query
388    /// the caching state with [`get_render_pipeline_state()`] and to retrieve the created GPU pipeline once
389    /// it's ready with [`get_render_pipeline()`].
390    ///
391    /// [`get_render_pipeline_state()`]: PipelineCache::get_render_pipeline_state
392    /// [`get_render_pipeline()`]: PipelineCache::get_render_pipeline
393    pub fn queue_render_pipeline(
394        &self,
395        descriptor: RenderPipelineDescriptor,
396    ) -> CachedRenderPipelineId {
397        let mut new_pipelines = self
398            .new_pipelines
399            .lock()
400            .unwrap_or_else(PoisonError::into_inner);
401        let id = CachedRenderPipelineId::new(self.pipelines.len() + new_pipelines.len());
402        new_pipelines.push(CachedPipeline {
403            descriptor: PipelineDescriptor::RenderPipelineDescriptor(Box::new(descriptor)),
404            state: CachedPipelineState::Queued,
405        });
406        id
407    }
408
409    /// Insert a compute pipeline into the cache, and queue its creation.
410    ///
411    /// The pipeline is always inserted and queued for creation. There is no attempt to deduplicate it with
412    /// an already cached pipeline.
413    ///
414    /// # Returns
415    ///
416    /// This method returns the unique compute shader ID of the cached pipeline, which can be used to query
417    /// the caching state with [`get_compute_pipeline_state()`] and to retrieve the created GPU pipeline once
418    /// it's ready with [`get_compute_pipeline()`].
419    ///
420    /// [`get_compute_pipeline_state()`]: PipelineCache::get_compute_pipeline_state
421    /// [`get_compute_pipeline()`]: PipelineCache::get_compute_pipeline
422    pub fn queue_compute_pipeline(
423        &self,
424        descriptor: ComputePipelineDescriptor,
425    ) -> CachedComputePipelineId {
426        let mut new_pipelines = self
427            .new_pipelines
428            .lock()
429            .unwrap_or_else(PoisonError::into_inner);
430        let id = CachedComputePipelineId::new(self.pipelines.len() + new_pipelines.len());
431        new_pipelines.push(CachedPipeline {
432            descriptor: PipelineDescriptor::ComputePipelineDescriptor(Box::new(descriptor)),
433            state: CachedPipelineState::Queued,
434        });
435        id
436    }
437
438    pub fn get_bind_group_layout(
439        &self,
440        bind_group_layout_descriptor: &BindGroupLayoutDescriptor,
441    ) -> BindGroupLayout {
442        self.bindgroup_layout_cache
443            .lock()
444            .unwrap()
445            .get(&self.device, bind_group_layout_descriptor.clone())
446    }
447
448    /// Inserts a [`Shader`] into this cache with the provided [`AssetId`].
449    pub fn set_shader(&mut self, id: AssetId<Shader>, shader: Shader) {
450        let mut shader_cache = self.shader_cache.lock().unwrap();
451        let pipelines_to_queue = shader_cache.set_shader(id, shader);
452        for cached_pipeline in pipelines_to_queue {
453            self.pipelines[cached_pipeline].state = CachedPipelineState::Queued;
454            self.waiting_pipelines.insert(cached_pipeline);
455        }
456    }
457
458    /// Removes a [`Shader`] from this cache if it exists.
459    pub fn remove_shader(&mut self, shader: AssetId<Shader>) {
460        let mut shader_cache = self.shader_cache.lock().unwrap();
461        let pipelines_to_queue = shader_cache.remove(shader);
462        for cached_pipeline in pipelines_to_queue {
463            self.pipelines[cached_pipeline].state = CachedPipelineState::Queued;
464            self.waiting_pipelines.insert(cached_pipeline);
465        }
466    }
467
468    fn start_create_render_pipeline(
469        &mut self,
470        id: CachedPipelineId,
471        descriptor: RenderPipelineDescriptor,
472    ) -> CachedPipelineState {
473        let device = self.device.clone();
474        let shader_cache = self.shader_cache.clone();
475        let layout_cache = self.layout_cache.clone();
476        let mut bindgroup_layout_cache = self.bindgroup_layout_cache.lock().unwrap();
477        let bind_group_layout = descriptor
478            .layout
479            .iter()
480            .map(|bind_group_layout_descriptor| {
481                bindgroup_layout_cache.get(&self.device, bind_group_layout_descriptor.clone())
482            })
483            .collect::<Vec<_>>();
484
485        create_pipeline_task(
486            async move {
487                let mut shader_cache = shader_cache.lock().unwrap();
488                let mut layout_cache = layout_cache.lock().unwrap();
489
490                let vertex_module = match shader_cache.get(
491                    id,
492                    descriptor.vertex.shader.id(),
493                    &descriptor.vertex.shader_defs,
494                ) {
495                    Ok(module) => module,
496                    Err(err) => return Err(err),
497                };
498
499                let fragment_module = match &descriptor.fragment {
500                    Some(fragment) => {
501                        match shader_cache.get(id, fragment.shader.id(), &fragment.shader_defs) {
502                            Ok(module) => Some(module),
503                            Err(err) => return Err(err),
504                        }
505                    }
506                    None => None,
507                };
508
509                let layout = if descriptor.layout.is_empty() && descriptor.immediate_size == 0 {
510                    None
511                } else {
512                    Some(layout_cache.get(&device, &bind_group_layout, descriptor.immediate_size))
513                };
514
515                drop((shader_cache, layout_cache));
516
517                let vertex_buffer_layouts = descriptor
518                    .vertex
519                    .buffers
520                    .iter()
521                    .map(|layout| RawVertexBufferLayout {
522                        array_stride: layout.array_stride,
523                        attributes: &layout.attributes,
524                        step_mode: layout.step_mode,
525                    })
526                    .collect::<Vec<_>>();
527
528                let fragment_data = descriptor.fragment.as_ref().map(|fragment| {
529                    (
530                        fragment_module.unwrap(),
531                        fragment.entry_point.as_deref(),
532                        fragment.targets.as_slice(),
533                    )
534                });
535
536                // TODO: Expose the rest of this somehow
537                let compilation_options = PipelineCompilationOptions {
538                    constants: &[],
539                    zero_initialize_workgroup_memory: descriptor.zero_initialize_workgroup_memory,
540                };
541
542                let descriptor = RawRenderPipelineDescriptor {
543                    multiview_mask: None,
544                    depth_stencil: descriptor.depth_stencil.clone(),
545                    label: descriptor.label.as_deref(),
546                    layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }),
547                    multisample: descriptor.multisample,
548                    primitive: descriptor.primitive,
549                    vertex: RawVertexState {
550                        buffers: &vertex_buffer_layouts,
551                        entry_point: descriptor.vertex.entry_point.as_deref(),
552                        module: &vertex_module,
553                        // TODO: Should this be the same as the fragment compilation options?
554                        compilation_options: compilation_options.clone(),
555                    },
556                    fragment: fragment_data
557                        .as_ref()
558                        .map(|(module, entry_point, targets)| RawFragmentState {
559                            entry_point: entry_point.as_deref(),
560                            module,
561                            targets,
562                            // TODO: Should this be the same as the vertex compilation options?
563                            compilation_options,
564                        }),
565                    cache: None,
566                };
567
568                Ok(Pipeline::RenderPipeline(
569                    device.create_render_pipeline(&descriptor),
570                ))
571            },
572            self.synchronous_pipeline_compilation,
573        )
574    }
575
576    fn start_create_compute_pipeline(
577        &mut self,
578        id: CachedPipelineId,
579        descriptor: ComputePipelineDescriptor,
580    ) -> CachedPipelineState {
581        let device = self.device.clone();
582        let shader_cache = self.shader_cache.clone();
583        let layout_cache = self.layout_cache.clone();
584        let mut bindgroup_layout_cache = self.bindgroup_layout_cache.lock().unwrap();
585        let bind_group_layout = descriptor
586            .layout
587            .iter()
588            .map(|bind_group_layout_descriptor| {
589                bindgroup_layout_cache.get(&self.device, bind_group_layout_descriptor.clone())
590            })
591            .collect::<Vec<_>>();
592
593        create_pipeline_task(
594            async move {
595                let mut shader_cache = shader_cache.lock().unwrap();
596                let mut layout_cache = layout_cache.lock().unwrap();
597
598                let compute_module =
599                    match shader_cache.get(id, descriptor.shader.id(), &descriptor.shader_defs) {
600                        Ok(module) => module,
601                        Err(err) => return Err(err),
602                    };
603
604                let layout = if descriptor.layout.is_empty() && descriptor.immediate_size == 0 {
605                    None
606                } else {
607                    Some(layout_cache.get(&device, &bind_group_layout, descriptor.immediate_size))
608                };
609
610                drop((shader_cache, layout_cache));
611
612                let descriptor = RawComputePipelineDescriptor {
613                    label: descriptor.label.as_deref(),
614                    layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }),
615                    module: &compute_module,
616                    entry_point: descriptor.entry_point.as_deref(),
617                    // TODO: Expose the rest of this somehow
618                    compilation_options: PipelineCompilationOptions {
619                        constants: &[],
620                        zero_initialize_workgroup_memory: descriptor
621                            .zero_initialize_workgroup_memory,
622                    },
623                    cache: None,
624                };
625
626                Ok(Pipeline::ComputePipeline(
627                    device.create_compute_pipeline(&descriptor),
628                ))
629            },
630            self.synchronous_pipeline_compilation,
631        )
632    }
633
634    /// Process the pipeline queue and create all pending pipelines if possible.
635    ///
636    /// This is generally called automatically during the [`RenderSystems::Render`] step, but can
637    /// be called manually to force creation at a different time.
638    ///
639    /// [`RenderSystems::Render`]: crate::RenderSystems::Render
640    pub fn process_queue(&mut self) {
641        let mut waiting_pipelines = mem::take(&mut self.waiting_pipelines);
642        let mut pipelines = mem::take(&mut self.pipelines);
643
644        {
645            let mut new_pipelines = self
646                .new_pipelines
647                .lock()
648                .unwrap_or_else(PoisonError::into_inner);
649            for new_pipeline in new_pipelines.drain(..) {
650                let id = pipelines.len();
651                pipelines.push(new_pipeline);
652                waiting_pipelines.insert(id);
653            }
654        }
655
656        for id in waiting_pipelines {
657            self.process_pipeline(&mut pipelines[id], id);
658        }
659
660        self.pipelines = pipelines;
661    }
662
663    fn process_pipeline(&mut self, cached_pipeline: &mut CachedPipeline, id: usize) {
664        match &mut cached_pipeline.state {
665            CachedPipelineState::Queued => {
666                cached_pipeline.state = match &cached_pipeline.descriptor {
667                    PipelineDescriptor::RenderPipelineDescriptor(descriptor) => {
668                        self.start_create_render_pipeline(id, *descriptor.clone())
669                    }
670                    PipelineDescriptor::ComputePipelineDescriptor(descriptor) => {
671                        self.start_create_compute_pipeline(id, *descriptor.clone())
672                    }
673                };
674            }
675
676            CachedPipelineState::Creating(task) => match bevy_tasks::futures::check_ready(task) {
677                Some(Ok(pipeline)) => {
678                    cached_pipeline.state = CachedPipelineState::Ok(pipeline);
679                    return;
680                }
681                Some(Err(err)) => cached_pipeline.state = CachedPipelineState::Err(err),
682                _ => (),
683            },
684
685            CachedPipelineState::Err(err) => match err {
686                // Retry
687                ShaderCacheError::ShaderNotLoaded(_)
688                | ShaderCacheError::ShaderImportNotYetAvailable => {
689                    cached_pipeline.state = CachedPipelineState::Queued;
690                }
691
692                // Shader could not be processed ... retrying won't help
693                ShaderCacheError::ProcessShaderError(err) => {
694                    let error_detail =
695                        err.emit_to_string(&self.shader_cache.lock().unwrap().composer);
696                    if std::env::var("VERBOSE_SHADER_ERROR")
697                        .is_ok_and(|v| !(v.is_empty() || v == "0" || v == "false"))
698                    {
699                        error!("{}", pipeline_error_context(cached_pipeline));
700                    }
701                    error!("failed to process shader error:\n{}", error_detail);
702                    return;
703                }
704                ShaderCacheError::CreateShaderModule(description) => {
705                    error!("failed to create shader module: {}", description);
706                    return;
707                }
708            },
709
710            CachedPipelineState::Ok(_) => return,
711        }
712
713        // Retry
714        self.waiting_pipelines.insert(id);
715    }
716
717    pub(crate) fn process_pipeline_queue_system(mut cache: ResMut<Self>) {
718        cache.process_queue();
719    }
720
721    pub(crate) fn extract_shaders(
722        mut cache: ResMut<Self>,
723        shaders: Extract<Res<Assets<Shader>>>,
724        mut events: Extract<MessageReader<AssetEvent<Shader>>>,
725    ) {
726        if cache.needs_shader_reload {
727            cache.needs_shader_reload = false;
728            for (id, shader) in shaders.iter() {
729                let mut shader = shader.clone();
730                shader.shader_defs.extend(cache.global_shader_defs.clone());
731                cache.set_shader(id, shader);
732            }
733            // Drain events so we don't double-process shaders we just loaded.
734            for _ in events.read() {}
735            return;
736        }
737
738        for event in events.read() {
739            #[expect(
740                clippy::match_same_arms,
741                reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon."
742            )]
743            match event {
744                // PERF: Instead of blocking waiting for the shader cache lock, try again next frame if the lock is currently held
745                AssetEvent::Added { id } | AssetEvent::Modified { id } => {
746                    if let Some(shader) = shaders.get(*id) {
747                        let mut shader = shader.clone();
748                        shader.shader_defs.extend(cache.global_shader_defs.clone());
749
750                        cache.set_shader(*id, shader);
751                    }
752                }
753                AssetEvent::Removed { id } => cache.remove_shader(*id),
754                AssetEvent::Unused { .. } => {}
755                AssetEvent::LoadedWithDependencies { .. } => {
756                    // TODO: handle this
757                }
758            }
759        }
760    }
761}
762
763fn pipeline_error_context(cached_pipeline: &CachedPipeline) -> String {
764    fn format(
765        shader: &Handle<Shader>,
766        entry: &Option<Cow<'static, str>>,
767        shader_defs: &[ShaderDefVal],
768    ) -> String {
769        let source = match shader.path() {
770            Some(path) => path.path().to_string_lossy().to_string(),
771            None => String::new(),
772        };
773        let entry = match entry {
774            Some(entry) => entry.to_string(),
775            None => String::new(),
776        };
777        let shader_defs = shader_defs
778            .iter()
779            .flat_map(|def| match def {
780                ShaderDefVal::Bool(k, v) if *v => Some(k.to_string()),
781                ShaderDefVal::Int(k, v) => Some(format!("{k} = {v}")),
782                ShaderDefVal::UInt(k, v) => Some(format!("{k} = {v}")),
783                _ => None,
784            })
785            .collect::<Vec<_>>()
786            .join(", ");
787        format!("{source}:{entry}\nshader defs: {shader_defs}")
788    }
789    match &cached_pipeline.descriptor {
790        PipelineDescriptor::RenderPipelineDescriptor(desc) => {
791            let vert = &desc.vertex;
792            let vert_str = format(&vert.shader, &vert.entry_point, &vert.shader_defs);
793            let Some(frag) = desc.fragment.as_ref() else {
794                return vert_str;
795            };
796            let frag_str = format(&frag.shader, &frag.entry_point, &frag.shader_defs);
797            format!("vertex {vert_str}\nfragment {frag_str}")
798        }
799        PipelineDescriptor::ComputePipelineDescriptor(desc) => {
800            format(&desc.shader, &desc.entry_point, &desc.shader_defs)
801        }
802    }
803}
804
805#[cfg(all(
806    not(target_arch = "wasm32"),
807    not(target_os = "macos"),
808    feature = "multi_threaded"
809))]
810fn create_pipeline_task(
811    task: impl Future<Output = Result<Pipeline, ShaderCacheError>> + Send + 'static,
812    sync: bool,
813) -> CachedPipelineState {
814    if !sync {
815        return CachedPipelineState::Creating(bevy_tasks::AsyncComputeTaskPool::get().spawn(task));
816    }
817
818    match bevy_tasks::block_on(task) {
819        Ok(pipeline) => CachedPipelineState::Ok(pipeline),
820        Err(err) => CachedPipelineState::Err(err),
821    }
822}
823
824#[cfg(any(
825    target_arch = "wasm32",
826    target_os = "macos",
827    not(feature = "multi_threaded")
828))]
829fn create_pipeline_task(
830    task: impl Future<Output = Result<Pipeline, ShaderCacheError>> + Send + 'static,
831    _sync: bool,
832) -> CachedPipelineState {
833    match bevy_tasks::block_on(task) {
834        Ok(pipeline) => CachedPipelineState::Ok(pipeline),
835        Err(err) => CachedPipelineState::Err(err),
836    }
837}