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