Path: blob/main/crates/bevy_render/src/render_resource/pipeline_cache.rs
6596 views
use crate::{1render_resource::*,2renderer::{RenderAdapter, RenderDevice, WgpuWrapper},3Extract,4};5use alloc::{borrow::Cow, sync::Arc};6use bevy_asset::{AssetEvent, AssetId, Assets, Handle};7use bevy_ecs::{8event::EventReader,9resource::Resource,10system::{Res, ResMut},11};12use bevy_platform::collections::{HashMap, HashSet};13use bevy_shader::{14CachedPipelineId, PipelineCacheError, Shader, ShaderCache, ShaderCacheSource, ShaderDefVal,15ValidateShader,16};17use bevy_tasks::Task;18use bevy_utils::default;19use core::{future::Future, hash::Hash, mem};20use std::sync::{Mutex, PoisonError};21use tracing::error;22use wgpu::{PipelineCompilationOptions, VertexBufferLayout as RawVertexBufferLayout};2324/// A descriptor for a [`Pipeline`].25///26/// Used to store a heterogenous collection of render and compute pipeline descriptors together.27#[derive(Debug)]28pub enum PipelineDescriptor {29RenderPipelineDescriptor(Box<RenderPipelineDescriptor>),30ComputePipelineDescriptor(Box<ComputePipelineDescriptor>),31}3233/// A pipeline defining the data layout and shader logic for a specific GPU task.34///35/// Used to store a heterogenous collection of render and compute pipelines together.36#[derive(Debug)]37pub enum Pipeline {38RenderPipeline(RenderPipeline),39ComputePipeline(ComputePipeline),40}4142/// Index of a cached render pipeline in a [`PipelineCache`].43#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]44pub struct CachedRenderPipelineId(CachedPipelineId);4546impl CachedRenderPipelineId {47/// An invalid cached render pipeline index, often used to initialize a variable.48pub const INVALID: Self = CachedRenderPipelineId(usize::MAX);4950#[inline]51pub fn id(&self) -> usize {52self.053}54}5556/// Index of a cached compute pipeline in a [`PipelineCache`].57#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]58pub struct CachedComputePipelineId(CachedPipelineId);5960impl CachedComputePipelineId {61/// An invalid cached compute pipeline index, often used to initialize a variable.62pub const INVALID: Self = CachedComputePipelineId(usize::MAX);6364#[inline]65pub fn id(&self) -> usize {66self.067}68}6970pub struct CachedPipeline {71pub descriptor: PipelineDescriptor,72pub state: CachedPipelineState,73}7475/// State of a cached pipeline inserted into a [`PipelineCache`].76#[cfg_attr(77not(target_arch = "wasm32"),78expect(79clippy::large_enum_variant,80reason = "See https://github.com/bevyengine/bevy/issues/19220"81)82)]83#[derive(Debug)]84pub enum CachedPipelineState {85/// The pipeline GPU object is queued for creation.86Queued,87/// The pipeline GPU object is being created.88Creating(Task<Result<Pipeline, PipelineCacheError>>),89/// The pipeline GPU object was created successfully and is available (allocated on the GPU).90Ok(Pipeline),91/// An error occurred while trying to create the pipeline GPU object.92Err(PipelineCacheError),93}9495impl CachedPipelineState {96/// Convenience method to "unwrap" a pipeline state into its underlying GPU object.97///98/// # Returns99///100/// The method returns the allocated pipeline GPU object.101///102/// # Panics103///104/// This method panics if the pipeline GPU object is not available, either because it is105/// pending creation or because an error occurred while attempting to create GPU object.106pub fn unwrap(&self) -> &Pipeline {107match self {108CachedPipelineState::Ok(pipeline) => pipeline,109CachedPipelineState::Queued => {110panic!("Pipeline has not been compiled yet. It is still in the 'Queued' state.")111}112CachedPipelineState::Creating(..) => {113panic!("Pipeline has not been compiled yet. It is still in the 'Creating' state.")114}115CachedPipelineState::Err(err) => panic!("{}", err),116}117}118}119120type LayoutCacheKey = (Vec<BindGroupLayoutId>, Vec<PushConstantRange>);121#[derive(Default)]122struct LayoutCache {123layouts: HashMap<LayoutCacheKey, Arc<WgpuWrapper<PipelineLayout>>>,124}125126impl LayoutCache {127fn get(128&mut self,129render_device: &RenderDevice,130bind_group_layouts: &[BindGroupLayout],131push_constant_ranges: Vec<PushConstantRange>,132) -> Arc<WgpuWrapper<PipelineLayout>> {133let bind_group_ids = bind_group_layouts.iter().map(BindGroupLayout::id).collect();134self.layouts135.entry((bind_group_ids, push_constant_ranges))136.or_insert_with_key(|(_, push_constant_ranges)| {137let bind_group_layouts = bind_group_layouts138.iter()139.map(BindGroupLayout::value)140.collect::<Vec<_>>();141Arc::new(WgpuWrapper::new(render_device.create_pipeline_layout(142&PipelineLayoutDescriptor {143bind_group_layouts: &bind_group_layouts,144push_constant_ranges,145..default()146},147)))148})149.clone()150}151}152153#[expect(154clippy::result_large_err,155reason = "See https://github.com/bevyengine/bevy/issues/19220"156)]157fn load_module(158render_device: &RenderDevice,159shader_source: ShaderCacheSource,160validate_shader: &ValidateShader,161) -> Result<WgpuWrapper<ShaderModule>, PipelineCacheError> {162let shader_source = match shader_source {163#[cfg(feature = "shader_format_spirv")]164ShaderCacheSource::SpirV(data) => wgpu::util::make_spirv(data),165#[cfg(not(feature = "shader_format_spirv"))]166ShaderCacheSource::SpirV(_) => {167unimplemented!("Enable feature \"shader_format_spirv\" to use SPIR-V shaders")168}169ShaderCacheSource::Wgsl(src) => ShaderSource::Wgsl(Cow::Owned(src)),170#[cfg(not(feature = "decoupled_naga"))]171ShaderCacheSource::Naga(src) => ShaderSource::Naga(Cow::Owned(src)),172};173let module_descriptor = ShaderModuleDescriptor {174label: None,175source: shader_source,176};177178render_device179.wgpu_device()180.push_error_scope(wgpu::ErrorFilter::Validation);181182let shader_module = WgpuWrapper::new(match validate_shader {183ValidateShader::Enabled => {184render_device.create_and_validate_shader_module(module_descriptor)185}186// SAFETY: we are interfacing with shader code, which may contain undefined behavior,187// such as indexing out of bounds.188// The checks required are prohibitively expensive and a poor default for game engines.189ValidateShader::Disabled => unsafe {190render_device.create_shader_module(module_descriptor)191},192});193194let error = render_device.wgpu_device().pop_error_scope();195196// `now_or_never` will return Some if the future is ready and None otherwise.197// On native platforms, wgpu will yield the error immediately while on wasm it may take longer since the browser APIs are asynchronous.198// So to keep the complexity of the ShaderCache low, we will only catch this error early on native platforms,199// and on wasm the error will be handled by wgpu and crash the application.200if let Some(Some(wgpu::Error::Validation { description, .. })) =201bevy_tasks::futures::now_or_never(error)202{203return Err(PipelineCacheError::CreateShaderModule(description));204}205206Ok(shader_module)207}208209/// Cache for render and compute pipelines.210///211/// The cache stores existing render and compute pipelines allocated on the GPU, as well as212/// pending creation. Pipelines inserted into the cache are identified by a unique ID, which213/// can be used to retrieve the actual GPU object once it's ready. The creation of the GPU214/// pipeline object is deferred to the [`RenderSystems::Render`] step, just before the render215/// graph starts being processed, as this requires access to the GPU.216///217/// Note that the cache does not perform automatic deduplication of identical pipelines. It is218/// up to the user not to insert the same pipeline twice to avoid wasting GPU resources.219///220/// [`RenderSystems::Render`]: crate::RenderSystems::Render221#[derive(Resource)]222pub struct PipelineCache {223layout_cache: Arc<Mutex<LayoutCache>>,224shader_cache: Arc<Mutex<ShaderCache<WgpuWrapper<ShaderModule>, RenderDevice>>>,225device: RenderDevice,226pipelines: Vec<CachedPipeline>,227waiting_pipelines: HashSet<CachedPipelineId>,228new_pipelines: Mutex<Vec<CachedPipeline>>,229global_shader_defs: Vec<ShaderDefVal>,230/// If `true`, disables asynchronous pipeline compilation.231/// This has no effect on macOS, wasm, or without the `multi_threaded` feature.232synchronous_pipeline_compilation: bool,233}234235impl PipelineCache {236/// Returns an iterator over the pipelines in the pipeline cache.237pub fn pipelines(&self) -> impl Iterator<Item = &CachedPipeline> {238self.pipelines.iter()239}240241/// Returns a iterator of the IDs of all currently waiting pipelines.242pub fn waiting_pipelines(&self) -> impl Iterator<Item = CachedPipelineId> + '_ {243self.waiting_pipelines.iter().copied()244}245246/// Create a new pipeline cache associated with the given render device.247pub fn new(248device: RenderDevice,249render_adapter: RenderAdapter,250synchronous_pipeline_compilation: bool,251) -> Self {252let mut global_shader_defs = Vec::new();253#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]254{255global_shader_defs.push("NO_ARRAY_TEXTURES_SUPPORT".into());256global_shader_defs.push("NO_CUBE_ARRAY_TEXTURES_SUPPORT".into());257global_shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into());258}259260if cfg!(target_abi = "sim") {261global_shader_defs.push("NO_CUBE_ARRAY_TEXTURES_SUPPORT".into());262}263264global_shader_defs.push(ShaderDefVal::UInt(265String::from("AVAILABLE_STORAGE_BUFFER_BINDINGS"),266device.limits().max_storage_buffers_per_shader_stage,267));268269Self {270shader_cache: Arc::new(Mutex::new(ShaderCache::new(271device.features(),272render_adapter.get_downlevel_capabilities().flags,273load_module,274))),275device,276layout_cache: default(),277waiting_pipelines: default(),278new_pipelines: default(),279pipelines: default(),280global_shader_defs,281synchronous_pipeline_compilation,282}283}284285/// Get the state of a cached render pipeline.286///287/// See [`PipelineCache::queue_render_pipeline()`].288#[inline]289pub fn get_render_pipeline_state(&self, id: CachedRenderPipelineId) -> &CachedPipelineState {290// If the pipeline id isn't in `pipelines`, it's queued in `new_pipelines`291self.pipelines292.get(id.0)293.map_or(&CachedPipelineState::Queued, |pipeline| &pipeline.state)294}295296/// Get the state of a cached compute pipeline.297///298/// See [`PipelineCache::queue_compute_pipeline()`].299#[inline]300pub fn get_compute_pipeline_state(&self, id: CachedComputePipelineId) -> &CachedPipelineState {301// If the pipeline id isn't in `pipelines`, it's queued in `new_pipelines`302self.pipelines303.get(id.0)304.map_or(&CachedPipelineState::Queued, |pipeline| &pipeline.state)305}306307/// Get the render pipeline descriptor a cached render pipeline was inserted from.308///309/// See [`PipelineCache::queue_render_pipeline()`].310///311/// **Note**: Be careful calling this method. It will panic if called with a pipeline that312/// has been queued but has not yet been processed by [`PipelineCache::process_queue()`].313#[inline]314pub fn get_render_pipeline_descriptor(315&self,316id: CachedRenderPipelineId,317) -> &RenderPipelineDescriptor {318match &self.pipelines[id.0].descriptor {319PipelineDescriptor::RenderPipelineDescriptor(descriptor) => descriptor,320PipelineDescriptor::ComputePipelineDescriptor(_) => unreachable!(),321}322}323324/// Get the compute pipeline descriptor a cached render pipeline was inserted from.325///326/// See [`PipelineCache::queue_compute_pipeline()`].327///328/// **Note**: Be careful calling this method. It will panic if called with a pipeline that329/// has been queued but has not yet been processed by [`PipelineCache::process_queue()`].330#[inline]331pub fn get_compute_pipeline_descriptor(332&self,333id: CachedComputePipelineId,334) -> &ComputePipelineDescriptor {335match &self.pipelines[id.0].descriptor {336PipelineDescriptor::RenderPipelineDescriptor(_) => unreachable!(),337PipelineDescriptor::ComputePipelineDescriptor(descriptor) => descriptor,338}339}340341/// Try to retrieve a render pipeline GPU object from a cached ID.342///343/// # Returns344///345/// This method returns a successfully created render pipeline if any, or `None` if the pipeline346/// was not created yet or if there was an error during creation. You can check the actual creation347/// state with [`PipelineCache::get_render_pipeline_state()`].348#[inline]349pub fn get_render_pipeline(&self, id: CachedRenderPipelineId) -> Option<&RenderPipeline> {350if let CachedPipelineState::Ok(Pipeline::RenderPipeline(pipeline)) =351&self.pipelines.get(id.0)?.state352{353Some(pipeline)354} else {355None356}357}358359/// Wait for a render pipeline to finish compiling.360#[inline]361pub fn block_on_render_pipeline(&mut self, id: CachedRenderPipelineId) {362if self.pipelines.len() <= id.0 {363self.process_queue();364}365366let state = &mut self.pipelines[id.0].state;367if let CachedPipelineState::Creating(task) = state {368*state = match bevy_tasks::block_on(task) {369Ok(p) => CachedPipelineState::Ok(p),370Err(e) => CachedPipelineState::Err(e),371};372}373}374375/// Try to retrieve a compute pipeline GPU object from a cached ID.376///377/// # Returns378///379/// This method returns a successfully created compute pipeline if any, or `None` if the pipeline380/// was not created yet or if there was an error during creation. You can check the actual creation381/// state with [`PipelineCache::get_compute_pipeline_state()`].382#[inline]383pub fn get_compute_pipeline(&self, id: CachedComputePipelineId) -> Option<&ComputePipeline> {384if let CachedPipelineState::Ok(Pipeline::ComputePipeline(pipeline)) =385&self.pipelines.get(id.0)?.state386{387Some(pipeline)388} else {389None390}391}392393/// Insert a render pipeline into the cache, and queue its creation.394///395/// The pipeline is always inserted and queued for creation. There is no attempt to deduplicate it with396/// an already cached pipeline.397///398/// # Returns399///400/// This method returns the unique render shader ID of the cached pipeline, which can be used to query401/// the caching state with [`get_render_pipeline_state()`] and to retrieve the created GPU pipeline once402/// it's ready with [`get_render_pipeline()`].403///404/// [`get_render_pipeline_state()`]: PipelineCache::get_render_pipeline_state405/// [`get_render_pipeline()`]: PipelineCache::get_render_pipeline406pub fn queue_render_pipeline(407&self,408descriptor: RenderPipelineDescriptor,409) -> CachedRenderPipelineId {410let mut new_pipelines = self411.new_pipelines412.lock()413.unwrap_or_else(PoisonError::into_inner);414let id = CachedRenderPipelineId(self.pipelines.len() + new_pipelines.len());415new_pipelines.push(CachedPipeline {416descriptor: PipelineDescriptor::RenderPipelineDescriptor(Box::new(descriptor)),417state: CachedPipelineState::Queued,418});419id420}421422/// Insert a compute pipeline into the cache, and queue its creation.423///424/// The pipeline is always inserted and queued for creation. There is no attempt to deduplicate it with425/// an already cached pipeline.426///427/// # Returns428///429/// This method returns the unique compute shader ID of the cached pipeline, which can be used to query430/// the caching state with [`get_compute_pipeline_state()`] and to retrieve the created GPU pipeline once431/// it's ready with [`get_compute_pipeline()`].432///433/// [`get_compute_pipeline_state()`]: PipelineCache::get_compute_pipeline_state434/// [`get_compute_pipeline()`]: PipelineCache::get_compute_pipeline435pub fn queue_compute_pipeline(436&self,437descriptor: ComputePipelineDescriptor,438) -> CachedComputePipelineId {439let mut new_pipelines = self440.new_pipelines441.lock()442.unwrap_or_else(PoisonError::into_inner);443let id = CachedComputePipelineId(self.pipelines.len() + new_pipelines.len());444new_pipelines.push(CachedPipeline {445descriptor: PipelineDescriptor::ComputePipelineDescriptor(Box::new(descriptor)),446state: CachedPipelineState::Queued,447});448id449}450451fn set_shader(&mut self, id: AssetId<Shader>, shader: Shader) {452let mut shader_cache = self.shader_cache.lock().unwrap();453let pipelines_to_queue = shader_cache.set_shader(id, shader);454for cached_pipeline in pipelines_to_queue {455self.pipelines[cached_pipeline].state = CachedPipelineState::Queued;456self.waiting_pipelines.insert(cached_pipeline);457}458}459460fn remove_shader(&mut self, shader: AssetId<Shader>) {461let mut shader_cache = self.shader_cache.lock().unwrap();462let pipelines_to_queue = shader_cache.remove(shader);463for cached_pipeline in pipelines_to_queue {464self.pipelines[cached_pipeline].state = CachedPipelineState::Queued;465self.waiting_pipelines.insert(cached_pipeline);466}467}468469fn start_create_render_pipeline(470&mut self,471id: CachedPipelineId,472descriptor: RenderPipelineDescriptor,473) -> CachedPipelineState {474let device = self.device.clone();475let shader_cache = self.shader_cache.clone();476let layout_cache = self.layout_cache.clone();477478create_pipeline_task(479async move {480let mut shader_cache = shader_cache.lock().unwrap();481let mut layout_cache = layout_cache.lock().unwrap();482483let vertex_module = match shader_cache.get(484&device,485id,486descriptor.vertex.shader.id(),487&descriptor.vertex.shader_defs,488) {489Ok(module) => module,490Err(err) => return Err(err),491};492493let fragment_module = match &descriptor.fragment {494Some(fragment) => {495match shader_cache.get(496&device,497id,498fragment.shader.id(),499&fragment.shader_defs,500) {501Ok(module) => Some(module),502Err(err) => return Err(err),503}504}505None => None,506};507508let layout =509if descriptor.layout.is_empty() && descriptor.push_constant_ranges.is_empty() {510None511} else {512Some(layout_cache.get(513&device,514&descriptor.layout,515descriptor.push_constant_ranges.to_vec(),516))517};518519drop((shader_cache, layout_cache));520521let vertex_buffer_layouts = descriptor522.vertex523.buffers524.iter()525.map(|layout| RawVertexBufferLayout {526array_stride: layout.array_stride,527attributes: &layout.attributes,528step_mode: layout.step_mode,529})530.collect::<Vec<_>>();531532let fragment_data = descriptor.fragment.as_ref().map(|fragment| {533(534fragment_module.unwrap(),535fragment.entry_point.as_deref(),536fragment.targets.as_slice(),537)538});539540// TODO: Expose the rest of this somehow541let compilation_options = PipelineCompilationOptions {542constants: &[],543zero_initialize_workgroup_memory: descriptor.zero_initialize_workgroup_memory,544};545546let descriptor = RawRenderPipelineDescriptor {547multiview: None,548depth_stencil: descriptor.depth_stencil.clone(),549label: descriptor.label.as_deref(),550layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }),551multisample: descriptor.multisample,552primitive: descriptor.primitive,553vertex: RawVertexState {554buffers: &vertex_buffer_layouts,555entry_point: descriptor.vertex.entry_point.as_deref(),556module: &vertex_module,557// TODO: Should this be the same as the fragment compilation options?558compilation_options: compilation_options.clone(),559},560fragment: fragment_data561.as_ref()562.map(|(module, entry_point, targets)| RawFragmentState {563entry_point: entry_point.as_deref(),564module,565targets,566// TODO: Should this be the same as the vertex compilation options?567compilation_options,568}),569cache: None,570};571572Ok(Pipeline::RenderPipeline(573device.create_render_pipeline(&descriptor),574))575},576self.synchronous_pipeline_compilation,577)578}579580fn start_create_compute_pipeline(581&mut self,582id: CachedPipelineId,583descriptor: ComputePipelineDescriptor,584) -> CachedPipelineState {585let device = self.device.clone();586let shader_cache = self.shader_cache.clone();587let layout_cache = self.layout_cache.clone();588589create_pipeline_task(590async move {591let mut shader_cache = shader_cache.lock().unwrap();592let mut layout_cache = layout_cache.lock().unwrap();593594let compute_module = match shader_cache.get(595&device,596id,597descriptor.shader.id(),598&descriptor.shader_defs,599) {600Ok(module) => module,601Err(err) => return Err(err),602};603604let layout =605if descriptor.layout.is_empty() && descriptor.push_constant_ranges.is_empty() {606None607} else {608Some(layout_cache.get(609&device,610&descriptor.layout,611descriptor.push_constant_ranges.to_vec(),612))613};614615drop((shader_cache, layout_cache));616617let descriptor = RawComputePipelineDescriptor {618label: descriptor.label.as_deref(),619layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }),620module: &compute_module,621entry_point: descriptor.entry_point.as_deref(),622// TODO: Expose the rest of this somehow623compilation_options: PipelineCompilationOptions {624constants: &[],625zero_initialize_workgroup_memory: descriptor626.zero_initialize_workgroup_memory,627},628cache: None,629};630631Ok(Pipeline::ComputePipeline(632device.create_compute_pipeline(&descriptor),633))634},635self.synchronous_pipeline_compilation,636)637}638639/// Process the pipeline queue and create all pending pipelines if possible.640///641/// This is generally called automatically during the [`RenderSystems::Render`] step, but can642/// be called manually to force creation at a different time.643///644/// [`RenderSystems::Render`]: crate::RenderSystems::Render645pub fn process_queue(&mut self) {646let mut waiting_pipelines = mem::take(&mut self.waiting_pipelines);647let mut pipelines = mem::take(&mut self.pipelines);648649{650let mut new_pipelines = self651.new_pipelines652.lock()653.unwrap_or_else(PoisonError::into_inner);654for new_pipeline in new_pipelines.drain(..) {655let id = pipelines.len();656pipelines.push(new_pipeline);657waiting_pipelines.insert(id);658}659}660661for id in waiting_pipelines {662self.process_pipeline(&mut pipelines[id], id);663}664665self.pipelines = pipelines;666}667668fn process_pipeline(&mut self, cached_pipeline: &mut CachedPipeline, id: usize) {669match &mut cached_pipeline.state {670CachedPipelineState::Queued => {671cached_pipeline.state = match &cached_pipeline.descriptor {672PipelineDescriptor::RenderPipelineDescriptor(descriptor) => {673self.start_create_render_pipeline(id, *descriptor.clone())674}675PipelineDescriptor::ComputePipelineDescriptor(descriptor) => {676self.start_create_compute_pipeline(id, *descriptor.clone())677}678};679}680681CachedPipelineState::Creating(task) => match bevy_tasks::futures::check_ready(task) {682Some(Ok(pipeline)) => {683cached_pipeline.state = CachedPipelineState::Ok(pipeline);684return;685}686Some(Err(err)) => cached_pipeline.state = CachedPipelineState::Err(err),687_ => (),688},689690CachedPipelineState::Err(err) => match err {691// Retry692PipelineCacheError::ShaderNotLoaded(_)693| PipelineCacheError::ShaderImportNotYetAvailable => {694cached_pipeline.state = CachedPipelineState::Queued;695}696697// Shader could not be processed ... retrying won't help698PipelineCacheError::ProcessShaderError(err) => {699let error_detail =700err.emit_to_string(&self.shader_cache.lock().unwrap().composer);701if std::env::var("VERBOSE_SHADER_ERROR")702.is_ok_and(|v| !(v.is_empty() || v == "0" || v == "false"))703{704error!("{}", pipeline_error_context(cached_pipeline));705}706error!("failed to process shader error:\n{}", error_detail);707return;708}709PipelineCacheError::CreateShaderModule(description) => {710error!("failed to create shader module: {}", description);711return;712}713},714715CachedPipelineState::Ok(_) => return,716}717718// Retry719self.waiting_pipelines.insert(id);720}721722pub(crate) fn process_pipeline_queue_system(mut cache: ResMut<Self>) {723cache.process_queue();724}725726pub(crate) fn extract_shaders(727mut cache: ResMut<Self>,728shaders: Extract<Res<Assets<Shader>>>,729mut events: Extract<EventReader<AssetEvent<Shader>>>,730) {731for event in events.read() {732#[expect(733clippy::match_same_arms,734reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon."735)]736match event {737// PERF: Instead of blocking waiting for the shader cache lock, try again next frame if the lock is currently held738AssetEvent::Added { id } | AssetEvent::Modified { id } => {739if let Some(shader) = shaders.get(*id) {740let mut shader = shader.clone();741shader.shader_defs.extend(cache.global_shader_defs.clone());742743cache.set_shader(*id, shader);744}745}746AssetEvent::Removed { id } => cache.remove_shader(*id),747AssetEvent::Unused { .. } => {}748AssetEvent::LoadedWithDependencies { .. } => {749// TODO: handle this750}751}752}753}754}755756fn pipeline_error_context(cached_pipeline: &CachedPipeline) -> String {757fn format(758shader: &Handle<Shader>,759entry: &Option<Cow<'static, str>>,760shader_defs: &[ShaderDefVal],761) -> String {762let source = match shader.path() {763Some(path) => path.path().to_string_lossy().to_string(),764None => String::new(),765};766let entry = match entry {767Some(entry) => entry.to_string(),768None => String::new(),769};770let shader_defs = shader_defs771.iter()772.flat_map(|def| match def {773ShaderDefVal::Bool(k, v) if *v => Some(k.to_string()),774ShaderDefVal::Int(k, v) => Some(format!("{k} = {v}")),775ShaderDefVal::UInt(k, v) => Some(format!("{k} = {v}")),776_ => None,777})778.collect::<Vec<_>>()779.join(", ");780format!("{source}:{entry}\nshader defs: {shader_defs}")781}782match &cached_pipeline.descriptor {783PipelineDescriptor::RenderPipelineDescriptor(desc) => {784let vert = &desc.vertex;785let vert_str = format(&vert.shader, &vert.entry_point, &vert.shader_defs);786let Some(frag) = desc.fragment.as_ref() else {787return vert_str;788};789let frag_str = format(&frag.shader, &frag.entry_point, &frag.shader_defs);790format!("vertex {vert_str}\nfragment {frag_str}")791}792PipelineDescriptor::ComputePipelineDescriptor(desc) => {793format(&desc.shader, &desc.entry_point, &desc.shader_defs)794}795}796}797798#[cfg(all(799not(target_arch = "wasm32"),800not(target_os = "macos"),801feature = "multi_threaded"802))]803fn create_pipeline_task(804task: impl Future<Output = Result<Pipeline, PipelineCacheError>> + Send + 'static,805sync: bool,806) -> CachedPipelineState {807if !sync {808return CachedPipelineState::Creating(bevy_tasks::AsyncComputeTaskPool::get().spawn(task));809}810811match bevy_tasks::block_on(task) {812Ok(pipeline) => CachedPipelineState::Ok(pipeline),813Err(err) => CachedPipelineState::Err(err),814}815}816817#[cfg(any(818target_arch = "wasm32",819target_os = "macos",820not(feature = "multi_threaded")821))]822fn create_pipeline_task(823task: impl Future<Output = Result<Pipeline, PipelineCacheError>> + Send + 'static,824_sync: bool,825) -> CachedPipelineState {826match bevy_tasks::block_on(task) {827Ok(pipeline) => CachedPipelineState::Ok(pipeline),828Err(err) => CachedPipelineState::Err(err),829}830}831832833