From c1fab14ff0cfcfd8beb9f8c57a0e50818e63bc73 Mon Sep 17 00:00:00 2001 From: charlotte Date: Wed, 25 Jun 2025 00:04:40 -0700 Subject: [PATCH 1/9] Add support for `wgpu::PipelineCache` --- .../bevy_core_pipeline/src/upscaling/mod.rs | 3 +- crates/bevy_render/Cargo.toml | 1 + crates/bevy_render/src/render_resource/mod.rs | 2 + .../persistent_pipeline_cache.rs | 342 ++++++++++++++++++ .../src/render_resource/pipeline_cache.rs | 41 ++- 5 files changed, 377 insertions(+), 12 deletions(-) create mode 100644 crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs diff --git a/crates/bevy_core_pipeline/src/upscaling/mod.rs b/crates/bevy_core_pipeline/src/upscaling/mod.rs index 4ce91de393..36146d1039 100644 --- a/crates/bevy_core_pipeline/src/upscaling/mod.rs +++ b/crates/bevy_core_pipeline/src/upscaling/mod.rs @@ -42,6 +42,7 @@ fn prepare_view_upscaling_pipelines( mut pipelines: ResMut>, blit_pipeline: Res, view_targets: Query<(Entity, &ViewTarget, Option<&ExtractedCamera>)>, + persitent_pipeline_cache: Option>, ) { let mut output_textures = >::default(); for (entity, view_target, camera) in view_targets.iter() { @@ -81,7 +82,7 @@ fn prepare_view_upscaling_pipelines( let pipeline = pipelines.specialize(&pipeline_cache, &blit_pipeline, key); // Ensure the pipeline is loaded before continuing the frame to prevent frames without any GPU work submitted - pipeline_cache.block_on_render_pipeline(pipeline); + pipeline_cache.block_on_render_pipeline(pipeline, persitent_pipeline_cache.as_deref()); commands .entity(entity) diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 9ecbbfc744..77499ce120 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -120,6 +120,7 @@ indexmap = { version = "2" } fixedbitset = { version = "0.5" } bitflags = "2" wesl = { version = "0.1.2", optional = true } +dirs = "6.0.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index aecf27173d..803f30c517 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -15,6 +15,7 @@ mod shader; mod storage_buffer; mod texture; mod uniform_buffer; +mod persistent_pipeline_cache; pub use bind_group::*; pub use bind_group_entries::*; @@ -31,6 +32,7 @@ pub use shader::*; pub use storage_buffer::*; pub use texture::*; pub use uniform_buffer::*; +pub use persistent_pipeline_cache::*; // TODO: decide where re-exports should go pub use wgpu::{ diff --git a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs new file mode 100644 index 0000000000..0558ec2ee6 --- /dev/null +++ b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs @@ -0,0 +1,342 @@ +use crate::renderer::RenderAdapterInfo; +use crate::{ExtractSchedule, RenderSystems}; +use bevy_app::{App, Plugin}; +use bevy_ecs::change_detection::{Res, ResMut}; +use bevy_ecs::error::BevyError; +use bevy_ecs::prelude::{not, resource_exists, IntoScheduleConfigs}; +use bevy_ecs::resource::Resource; +use bevy_ecs::system::{Commands, Local}; +use bevy_platform::hash::FixedHasher; +use bevy_render::render_resource::PipelineCache; +use bevy_render::renderer::RenderDevice; +use bevy_render::{Extract, Render}; +use std::fs::OpenOptions; +use std::hash::BuildHasher; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::thread::JoinHandle; +use std::{fs, io}; +use thiserror::Error; +use tracing::{debug, warn}; +use wgpu::hal::PipelineCacheError; +use wgpu::{Backend, PipelineCacheDescriptor}; + +/// Plugin for managing [`wgpu::PipelineCache`] resources across application runs. +/// +/// When pipelines are compiled by [`crate::PipelineCache`], if this plugin is enabled, it will +/// persist the pipeline cache to disk, allowing for faster startup times in subsequent runs. +/// +/// Note: This plugin is currently only supported on the Vulkan backend. +pub struct PersistentPipelineCachePlugin { + /// A unique key for the application, used to identify the cache directory. Should change + /// if the application is updated or if the cache should be invalidated. + pub application_key: &'static str, + /// The directory where the pipeline cache will be stored. + pub data_dir: Option, + /// The eviction policy for the cache. + pub eviction_policy: EvictionPolicy, +} + +impl PersistentPipelineCachePlugin { + /// Creates a new instance of the `PersistentPipelineCachePlugin` with the specified + /// application key. + pub fn new(application_key: &'static str) -> Self { + Self { + application_key, + data_dir: dirs::cache_dir().map(|path| path.join("bevy")), + eviction_policy: EvictionPolicy::Stale, + } + } +} + +impl Plugin for PersistentPipelineCachePlugin { + fn build(&self, _app: &mut App) {} + + fn finish(&self, app: &mut App) { + let Some(data_dir) = &self.data_dir else { + warn!("No data directory specified for PersistentPipelineCachePlugin."); + return; + }; + + if let Some(render_app) = app.get_sub_app_mut(bevy_render::RenderApp) { + let adapter_debug = render_app.world().resource::(); + if adapter_debug.backend != Backend::Vulkan { + warn!("PersistentPipelineCachePlugin is only supported on Vulkan backend.."); + return; + } + render_app + .add_systems( + ExtractSchedule, + extract_persistent_pipeline_cache.run_if( + not(resource_exists::)) + + ) + .add_systems( + Render, + write_persistent_pipeline_cache + .run_if(resource_exists::) + .in_set(RenderSystems::Cleanup), + ); + }; + + app.insert_resource(PersistentPipelineCacheConfig { + application_key: self.application_key, + data_dir: data_dir.clone(), + eviction_policy: self.eviction_policy, + }); + } +} + +pub fn extract_persistent_pipeline_cache( + mut commands: Commands, + persistent_pipeline_cache_config: Extract>>, + adapter_debug: Res, + render_device: Res, +) -> Result<(), BevyError> { + let Some(persistent_pipeline_cache_config) = &*persistent_pipeline_cache_config else { + return Ok(()) + }; + + debug!( + "Extracting persistent pipeline cache with application key: {}", + persistent_pipeline_cache_config.application_key + ); + let cache_path = persistent_pipeline_cache_config + .data_dir + .join(persistent_pipeline_cache_config.application_key); + + match persistent_pipeline_cache_config.eviction_policy { + EvictionPolicy::Always => { + // Evict all existing data + if cache_path.exists() { + fs::remove_dir_all(&cache_path).map_err(PersistentPipelineCacheError::Io)?; + } + } + EvictionPolicy::Stale => { + // Evict all but matching our application key + if cache_path.exists() { + for entry in fs::read_dir(&cache_path).map_err(PersistentPipelineCacheError::Io)? { + // Check if the entry is a directory and doesn't match the cache path + let entry = entry.map_err(PersistentPipelineCacheError::Io)?; + if entry + .file_type() + .map_err(PersistentPipelineCacheError::Io)? + .is_dir() + && entry.file_name() != cache_path + { + fs::remove_dir_all(entry.path()) + .map_err(PersistentPipelineCacheError::Io)?; + debug!("Evicted stale pipeline cache at {:?}", entry.path()); + } + } + } + } + EvictionPolicy::Never => {} + } + + let cache_key = wgpu::util::pipeline_cache_key(&adapter_debug) + .ok_or(PersistentPipelineCacheError::InvalidAdapterKey)?; + let cache_path = cache_path.join(cache_key); + + // Ensure the cache directory exists + if let Some(parent) = cache_path.parent() { + if !parent.exists() { + debug!( + "Creating persistent pipeline cache directory at {:?}", + parent + ); + fs::create_dir_all(parent).map_err(PersistentPipelineCacheError::Io)?; + } + } + + let persistent_pipeline_cache = PersistentPipelineCache::new( + &render_device, + persistent_pipeline_cache_config.application_key, + &cache_path, + )?; + + commands.insert_resource(persistent_pipeline_cache); + Ok(()) +} + +pub fn write_persistent_pipeline_cache( + mut persistent_pipeline_cache: ResMut, + pipeline_cache: Res, + mut pipeline_cache_size: Local, +) -> Result<(), BevyError> { + let cache_size = pipeline_cache.size(); + if cache_size > *pipeline_cache_size { + persistent_pipeline_cache.write()?; + *pipeline_cache_size = cache_size; + } + + Ok(()) +} + +/// Configuration for the persistent pipeline cache. +#[derive(Resource)] +pub struct PersistentPipelineCacheConfig { + /// A unique key for the application, used to identify the cache directory. + pub application_key: &'static str, + /// The directory where the pipeline cache will be stored. + pub data_dir: PathBuf, + /// The eviction policy for the cache. + pub eviction_policy: EvictionPolicy, +} + +/// Resource for managing [`wgpu::PipelineCache`]. +#[derive(Resource)] +pub struct PersistentPipelineCache { + cache: Arc, + write_lock: Arc>, + write_tasks: Vec>>, + cache_path: PathBuf, + data_key: u64, +} + +impl PersistentPipelineCache { + /// Create a new instance of the persistent pipeline cache with the given application key and + /// cache path. + pub fn new( + render_device: &RenderDevice, + app_key: &'static str, + cache_path: &Path, + ) -> Result { + // Get data if the cache file exists + let cache_data = if cache_path.exists() { + let data = fs::read(cache_path).map_err(PersistentPipelineCacheError::Io)?; + debug!( + "Loaded persistent pipeline cache from {:?}, size: {}", + cache_path, + data.len() + ); + Some(data) + } else { + // If the cache file does not exist, create an empty cache + debug!("Creating new persistent pipeline cache at {:?}", cache_path); + None + }; + let cache = unsafe { + render_device + .wgpu_device() + .create_pipeline_cache(&PipelineCacheDescriptor { + data: cache_data.as_deref(), + label: app_key.into(), + fallback: true, + }) + }; + + let data_key = { + let hasher = FixedHasher::default(); + hasher.hash_one(&cache_data) + }; + + Ok(PersistentPipelineCache { + cache: Arc::new(cache), + write_lock: Arc::new(Mutex::new(())), + write_tasks: vec![], + cache_path: cache_path.to_path_buf(), + data_key, + }) + } + + /// Get the cached data if it has changed since the last call. + pub fn get_data(&mut self) -> Option> { + let data = self.cache.get_data(); + let hasher = FixedHasher::default(); + let data_key = hasher.hash_one(&data); + if self.data_key != data_key { + self.data_key = data_key; + return data; + } + + None + } + + /// Write the cached data to disk, if it has changed since the last write. + pub fn write(&mut self) -> Result<(), PersistentPipelineCacheError> { + // Process existing tasks + let mut pending_tasks = vec![]; + let mut error = None; + for task in self.write_tasks.drain(..) { + if task.is_finished() { + match task.join() { + Ok(Ok(())) => { + debug!("Persistent pipeline cache write task completed successfully.") + } + Ok(Err(err)) => { + warn!("Persistent pipeline cache write task failed: {}", err); + error = Some(err); + } + Err(err) => { + warn!("Persistent pipeline cache write task panicked: {:?}", err); + error = Some(PersistentPipelineCacheError::Io(io::Error::new( + io::ErrorKind::Other, + "Persistent pipeline cache write task panicked", + ))); + } + } + } else { + pending_tasks.push(task); + } + } + + if let Some(err) = error { + return Err(err); + } + + if let Some(data) = self.get_data() { + let temp = self.cache_path.with_extension("tmp"); + let cache_path = self.cache_path.clone(); + let lock = self.write_lock.clone(); + let join_handle = std::thread::spawn(move || { + let _guard = lock + .lock() + .or(Err(PersistentPipelineCacheError::LockError))?; + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&temp) + .map_err(PersistentPipelineCacheError::Io)?; + file.write_all(&data) + .map_err(PersistentPipelineCacheError::Io)?; + fs::rename(&temp, &cache_path).map_err(PersistentPipelineCacheError::Io)?; + Ok(()) + }); + self.write_tasks.push(join_handle); + } + + Ok(()) + } + + /// Get the underlying wgpu pipeline cache. + pub fn get_cache(&self) -> Arc { + self.cache.clone() + } +} + +/// Describes the eviction policy for the persistent pipeline cache. +#[derive(Debug, Copy, Clone)] +pub enum EvictionPolicy { + /// Evict all existing data on startup. + Always, + /// Evict all but the data matching the application key on startup. + Stale, + /// Never evict any data. + Never, +} + +/// Error type for persistent pipeline cache operations. +#[derive(Debug, Error)] +#[error("Error while handling persistent pipeline cache")] +pub enum PersistentPipelineCacheError { + #[error(transparent)] + Io(#[from] io::Error), + #[error("Failed to create pipeline cache: {0}")] + DeviceError(#[from] PipelineCacheError), + #[error("Could not create cache key from adapter")] + InvalidAdapterKey, + #[error("Failed to acquire write lock for persistent pipeline cache")] + LockError, +} diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 9d658cf361..f0e15f23ef 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -1,3 +1,4 @@ +use crate::render_resource::persistent_pipeline_cache::PersistentPipelineCache; use crate::{ render_resource::*, renderer::{RenderAdapter, RenderDevice}, @@ -692,9 +693,13 @@ impl PipelineCache { /// Wait for a render pipeline to finish compiling. #[inline] - pub fn block_on_render_pipeline(&mut self, id: CachedRenderPipelineId) { + pub fn block_on_render_pipeline( + &mut self, + id: CachedRenderPipelineId, + persistent_cache: Option<&PersistentPipelineCache>, + ) { if self.pipelines.len() <= id.0 { - self.process_queue(); + self.process_queue(persistent_cache); } let state = &mut self.pipelines[id.0].state; @@ -804,10 +809,12 @@ impl PipelineCache { &mut self, id: CachedPipelineId, descriptor: RenderPipelineDescriptor, + persistent_cache: Option<&PersistentPipelineCache>, ) -> CachedPipelineState { let device = self.device.clone(); let shader_cache = self.shader_cache.clone(); let layout_cache = self.layout_cache.clone(); + let cache = persistent_cache.map(|cache| cache.get_cache()); create_pipeline_task( async move { @@ -877,6 +884,7 @@ impl PipelineCache { zero_initialize_workgroup_memory: descriptor.zero_initialize_workgroup_memory, }; + let descriptor = RawRenderPipelineDescriptor { multiview: None, depth_stencil: descriptor.depth_stencil.clone(), @@ -900,7 +908,7 @@ impl PipelineCache { // TODO: Should this be the same as the vertex compilation options? compilation_options, }), - cache: None, + cache: cache.as_deref(), }; Ok(Pipeline::RenderPipeline( @@ -915,10 +923,12 @@ impl PipelineCache { &mut self, id: CachedPipelineId, descriptor: ComputePipelineDescriptor, + persistent_cache: Option<&PersistentPipelineCache>, ) -> CachedPipelineState { let device = self.device.clone(); let shader_cache = self.shader_cache.clone(); let layout_cache = self.layout_cache.clone(); + let cache = persistent_cache.map(|cache| cache.get_cache()); create_pipeline_task( async move { @@ -959,7 +969,7 @@ impl PipelineCache { zero_initialize_workgroup_memory: descriptor .zero_initialize_workgroup_memory, }, - cache: None, + cache: cache.as_deref(), }; Ok(Pipeline::ComputePipeline( @@ -976,7 +986,7 @@ impl PipelineCache { /// be called manually to force creation at a different time. /// /// [`RenderSystems::Render`]: crate::RenderSystems::Render - pub fn process_queue(&mut self) { + pub fn process_queue(&mut self, persistent_cache: Option<&PersistentPipelineCache>) { let mut waiting_pipelines = mem::take(&mut self.waiting_pipelines); let mut pipelines = mem::take(&mut self.pipelines); @@ -993,21 +1003,26 @@ impl PipelineCache { } for id in waiting_pipelines { - self.process_pipeline(&mut pipelines[id], id); + self.process_pipeline(&mut pipelines[id], id, persistent_cache); } self.pipelines = pipelines; } - fn process_pipeline(&mut self, cached_pipeline: &mut CachedPipeline, id: usize) { + fn process_pipeline( + &mut self, + cached_pipeline: &mut CachedPipeline, + id: usize, + persistent_cache: Option<&PersistentPipelineCache>, + ) { match &mut cached_pipeline.state { CachedPipelineState::Queued => { cached_pipeline.state = match &cached_pipeline.descriptor { PipelineDescriptor::RenderPipelineDescriptor(descriptor) => { - self.start_create_render_pipeline(id, *descriptor.clone()) + self.start_create_render_pipeline(id, *descriptor.clone(), persistent_cache) } PipelineDescriptor::ComputePipelineDescriptor(descriptor) => { - self.start_create_compute_pipeline(id, *descriptor.clone()) + self.start_create_compute_pipeline(id, *descriptor.clone(), persistent_cache) } }; } @@ -1048,8 +1063,8 @@ impl PipelineCache { self.waiting_pipelines.insert(id); } - pub(crate) fn process_pipeline_queue_system(mut cache: ResMut) { - cache.process_queue(); + pub(crate) fn process_pipeline_queue_system(mut cache: ResMut, persistent_pipeline_cache: Option>) { + cache.process_queue(persistent_pipeline_cache.as_deref()); } pub(crate) fn extract_shaders( @@ -1077,6 +1092,10 @@ impl PipelineCache { } } } + + pub fn size(&self) -> usize { + self.pipelines.len() + } } #[cfg(all( From a8aeb584c2163da1009cdb8b0bb58cb3c556f0ef Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Wed, 25 Jun 2025 00:07:29 -0700 Subject: [PATCH 2/9] Format. --- .../persistent_pipeline_cache.rs | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs index 0558ec2ee6..f57515027e 100644 --- a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs @@ -1,26 +1,27 @@ -use crate::renderer::RenderAdapterInfo; -use crate::{ExtractSchedule, RenderSystems}; +use crate::{renderer::RenderAdapterInfo, ExtractSchedule, RenderSystems}; use bevy_app::{App, Plugin}; -use bevy_ecs::change_detection::{Res, ResMut}; -use bevy_ecs::error::BevyError; -use bevy_ecs::prelude::{not, resource_exists, IntoScheduleConfigs}; -use bevy_ecs::resource::Resource; -use bevy_ecs::system::{Commands, Local}; +use bevy_ecs::{ + change_detection::{Res, ResMut}, + error::BevyError, + prelude::{not, resource_exists, IntoScheduleConfigs}, + resource::Resource, + system::{Commands, Local}, +}; use bevy_platform::hash::FixedHasher; -use bevy_render::render_resource::PipelineCache; -use bevy_render::renderer::RenderDevice; -use bevy_render::{Extract, Render}; -use std::fs::OpenOptions; -use std::hash::BuildHasher; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; -use std::thread::JoinHandle; -use std::{fs, io}; +use bevy_render::{render_resource::PipelineCache, renderer::RenderDevice, Extract, Render}; +use std::{ + fs, + fs::OpenOptions, + hash::BuildHasher, + io, + io::Write, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + thread::JoinHandle, +}; use thiserror::Error; use tracing::{debug, warn}; -use wgpu::hal::PipelineCacheError; -use wgpu::{Backend, PipelineCacheDescriptor}; +use wgpu::{hal::PipelineCacheError, Backend, PipelineCacheDescriptor}; /// Plugin for managing [`wgpu::PipelineCache`] resources across application runs. /// @@ -68,9 +69,8 @@ impl Plugin for PersistentPipelineCachePlugin { render_app .add_systems( ExtractSchedule, - extract_persistent_pipeline_cache.run_if( - not(resource_exists::)) - + extract_persistent_pipeline_cache + .run_if(not(resource_exists::)), ) .add_systems( Render, @@ -95,7 +95,7 @@ pub fn extract_persistent_pipeline_cache( render_device: Res, ) -> Result<(), BevyError> { let Some(persistent_pipeline_cache_config) = &*persistent_pipeline_cache_config else { - return Ok(()) + return Ok(()); }; debug!( From 54010745a033fba0497a1009430c303b0d3dfcc0 Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Wed, 25 Jun 2025 00:07:36 -0700 Subject: [PATCH 3/9] Format. --- crates/bevy_render/src/render_resource/mod.rs | 4 ++-- .../bevy_render/src/render_resource/pipeline_cache.rs | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index 803f30c517..af5def9859 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -7,6 +7,7 @@ mod bindless; mod buffer; mod buffer_vec; mod gpu_array_buffer; +mod persistent_pipeline_cache; mod pipeline; mod pipeline_cache; mod pipeline_specializer; @@ -15,7 +16,6 @@ mod shader; mod storage_buffer; mod texture; mod uniform_buffer; -mod persistent_pipeline_cache; pub use bind_group::*; pub use bind_group_entries::*; @@ -25,6 +25,7 @@ pub use bindless::*; pub use buffer::*; pub use buffer_vec::*; pub use gpu_array_buffer::*; +pub use persistent_pipeline_cache::*; pub use pipeline::*; pub use pipeline_cache::*; pub use pipeline_specializer::*; @@ -32,7 +33,6 @@ pub use shader::*; pub use storage_buffer::*; pub use texture::*; pub use uniform_buffer::*; -pub use persistent_pipeline_cache::*; // TODO: decide where re-exports should go pub use wgpu::{ diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index f0e15f23ef..3cc2a0771b 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -884,7 +884,6 @@ impl PipelineCache { zero_initialize_workgroup_memory: descriptor.zero_initialize_workgroup_memory, }; - let descriptor = RawRenderPipelineDescriptor { multiview: None, depth_stencil: descriptor.depth_stencil.clone(), @@ -1021,9 +1020,8 @@ impl PipelineCache { PipelineDescriptor::RenderPipelineDescriptor(descriptor) => { self.start_create_render_pipeline(id, *descriptor.clone(), persistent_cache) } - PipelineDescriptor::ComputePipelineDescriptor(descriptor) => { - self.start_create_compute_pipeline(id, *descriptor.clone(), persistent_cache) - } + PipelineDescriptor::ComputePipelineDescriptor(descriptor) => self + .start_create_compute_pipeline(id, *descriptor.clone(), persistent_cache), }; } @@ -1063,7 +1061,10 @@ impl PipelineCache { self.waiting_pipelines.insert(id); } - pub(crate) fn process_pipeline_queue_system(mut cache: ResMut, persistent_pipeline_cache: Option>) { + pub(crate) fn process_pipeline_queue_system( + mut cache: ResMut, + persistent_pipeline_cache: Option>, + ) { cache.process_queue(persistent_pipeline_cache.as_deref()); } From c42a4fde414ca693f005008811a5867088c0a2b8 Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Wed, 25 Jun 2025 00:16:07 -0700 Subject: [PATCH 4/9] Ci. --- .../src/render_resource/persistent_pipeline_cache.rs | 9 ++++----- crates/bevy_render/src/render_resource/pipeline_cache.rs | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs index f57515027e..e6b656ba65 100644 --- a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs @@ -228,7 +228,7 @@ impl PersistentPipelineCache { }; let data_key = { - let hasher = FixedHasher::default(); + let hasher = FixedHasher; hasher.hash_one(&cache_data) }; @@ -244,7 +244,7 @@ impl PersistentPipelineCache { /// Get the cached data if it has changed since the last call. pub fn get_data(&mut self) -> Option> { let data = self.cache.get_data(); - let hasher = FixedHasher::default(); + let hasher = FixedHasher; let data_key = hasher.hash_one(&data); if self.data_key != data_key { self.data_key = data_key; @@ -263,7 +263,7 @@ impl PersistentPipelineCache { if task.is_finished() { match task.join() { Ok(Ok(())) => { - debug!("Persistent pipeline cache write task completed successfully.") + debug!("Persistent pipeline cache write task completed successfully."); } Ok(Err(err)) => { warn!("Persistent pipeline cache write task failed: {}", err); @@ -271,8 +271,7 @@ impl PersistentPipelineCache { } Err(err) => { warn!("Persistent pipeline cache write task panicked: {:?}", err); - error = Some(PersistentPipelineCacheError::Io(io::Error::new( - io::ErrorKind::Other, + error = Some(PersistentPipelineCacheError::Io(io::Error::other( "Persistent pipeline cache write task panicked", ))); } diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 3cc2a0771b..f7511acbd2 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -814,7 +814,7 @@ impl PipelineCache { let device = self.device.clone(); let shader_cache = self.shader_cache.clone(); let layout_cache = self.layout_cache.clone(); - let cache = persistent_cache.map(|cache| cache.get_cache()); + let cache = persistent_cache.map(PersistentPipelineCache::get_cache); create_pipeline_task( async move { @@ -927,7 +927,7 @@ impl PipelineCache { let device = self.device.clone(); let shader_cache = self.shader_cache.clone(); let layout_cache = self.layout_cache.clone(); - let cache = persistent_cache.map(|cache| cache.get_cache()); + let cache = persistent_cache.map(PersistentPipelineCache::get_cache); create_pipeline_task( async move { From 2a2d018d160bbfc8990d6faa132f3d858c592aec Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Wed, 25 Jun 2025 00:19:26 -0700 Subject: [PATCH 5/9] Ci. --- .../src/render_resource/persistent_pipeline_cache.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs index e6b656ba65..13d8f1d932 100644 --- a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs @@ -1,4 +1,5 @@ use crate::{renderer::RenderAdapterInfo, ExtractSchedule, RenderSystems}; +use alloc::sync::Arc; use bevy_app::{App, Plugin}; use bevy_ecs::{ change_detection::{Res, ResMut}, @@ -9,14 +10,14 @@ use bevy_ecs::{ }; use bevy_platform::hash::FixedHasher; use bevy_render::{render_resource::PipelineCache, renderer::RenderDevice, Extract, Render}; +use core::hash::BuildHasher; use std::{ fs, fs::OpenOptions, - hash::BuildHasher, io, io::Write, path::{Path, PathBuf}, - sync::{Arc, Mutex}, + sync::Mutex, thread::JoinHandle, }; use thiserror::Error; @@ -213,10 +214,10 @@ impl PersistentPipelineCache { ); Some(data) } else { - // If the cache file does not exist, create an empty cache debug!("Creating new persistent pipeline cache at {:?}", cache_path); None }; + // SAFETY: Data was created with a cache key that matches the adapter. let cache = unsafe { render_device .wgpu_device() From ad63a7e7f6cfc2f7c6d763e16298db187aad0ee3 Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Wed, 25 Jun 2025 00:24:32 -0700 Subject: [PATCH 6/9] Ci. --- crates/bevy_core_pipeline/src/upscaling/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_core_pipeline/src/upscaling/mod.rs b/crates/bevy_core_pipeline/src/upscaling/mod.rs index 36146d1039..37ae95e3cb 100644 --- a/crates/bevy_core_pipeline/src/upscaling/mod.rs +++ b/crates/bevy_core_pipeline/src/upscaling/mod.rs @@ -42,7 +42,7 @@ fn prepare_view_upscaling_pipelines( mut pipelines: ResMut>, blit_pipeline: Res, view_targets: Query<(Entity, &ViewTarget, Option<&ExtractedCamera>)>, - persitent_pipeline_cache: Option>, + persistent_pipeline_cache: Option>, ) { let mut output_textures = >::default(); for (entity, view_target, camera) in view_targets.iter() { @@ -82,7 +82,7 @@ fn prepare_view_upscaling_pipelines( let pipeline = pipelines.specialize(&pipeline_cache, &blit_pipeline, key); // Ensure the pipeline is loaded before continuing the frame to prevent frames without any GPU work submitted - pipeline_cache.block_on_render_pipeline(pipeline, persitent_pipeline_cache.as_deref()); + pipeline_cache.block_on_render_pipeline(pipeline, persistent_pipeline_cache.as_deref()); commands .entity(entity) From 77c8cd011924d7ca1b0f2c6c1a8195578c49cf98 Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Wed, 25 Jun 2025 00:35:19 -0700 Subject: [PATCH 7/9] Remove dirs. --- crates/bevy_render/Cargo.toml | 1 - .../persistent_pipeline_cache.rs | 17 ++++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 77499ce120..9ecbbfc744 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -120,7 +120,6 @@ indexmap = { version = "2" } fixedbitset = { version = "0.5" } bitflags = "2" wesl = { version = "0.1.2", optional = true } -dirs = "6.0.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. diff --git a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs index 13d8f1d932..388bd48cff 100644 --- a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs @@ -35,7 +35,7 @@ pub struct PersistentPipelineCachePlugin { /// if the application is updated or if the cache should be invalidated. pub application_key: &'static str, /// The directory where the pipeline cache will be stored. - pub data_dir: Option, + pub data_dir: PathBuf, /// The eviction policy for the cache. pub eviction_policy: EvictionPolicy, } @@ -43,10 +43,10 @@ pub struct PersistentPipelineCachePlugin { impl PersistentPipelineCachePlugin { /// Creates a new instance of the `PersistentPipelineCachePlugin` with the specified /// application key. - pub fn new(application_key: &'static str) -> Self { + pub fn new(application_key: &'static str, data_dir: PathBuf) -> Self { Self { application_key, - data_dir: dirs::cache_dir().map(|path| path.join("bevy")), + data_dir, eviction_policy: EvictionPolicy::Stale, } } @@ -56,10 +56,13 @@ impl Plugin for PersistentPipelineCachePlugin { fn build(&self, _app: &mut App) {} fn finish(&self, app: &mut App) { - let Some(data_dir) = &self.data_dir else { - warn!("No data directory specified for PersistentPipelineCachePlugin."); + if !self.data_dir.exists() || !self.data_dir.is_dir() { + warn!( + "PersistentPipelineCachePlugin data directory does not exist or is not a directory: {:?}", + self.data_dir + ); return; - }; + } if let Some(render_app) = app.get_sub_app_mut(bevy_render::RenderApp) { let adapter_debug = render_app.world().resource::(); @@ -83,7 +86,7 @@ impl Plugin for PersistentPipelineCachePlugin { app.insert_resource(PersistentPipelineCacheConfig { application_key: self.application_key, - data_dir: data_dir.clone(), + data_dir: self.data_dir.clone(), eviction_policy: self.eviction_policy, }); } From 617ce936af73b20fca7e2e4a6498cfc0a6b9595d Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Wed, 25 Jun 2025 01:12:00 -0700 Subject: [PATCH 8/9] Use safe default for new. --- .../src/render_resource/persistent_pipeline_cache.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs index 388bd48cff..2c6fe72ab0 100644 --- a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs @@ -23,6 +23,7 @@ use std::{ use thiserror::Error; use tracing::{debug, warn}; use wgpu::{hal::PipelineCacheError, Backend, PipelineCacheDescriptor}; +use bevy_utils::WgpuWrapper; /// Plugin for managing [`wgpu::PipelineCache`] resources across application runs. /// @@ -47,7 +48,7 @@ impl PersistentPipelineCachePlugin { Self { application_key, data_dir, - eviction_policy: EvictionPolicy::Stale, + eviction_policy: EvictionPolicy::Never, } } } @@ -192,7 +193,7 @@ pub struct PersistentPipelineCacheConfig { /// Resource for managing [`wgpu::PipelineCache`]. #[derive(Resource)] pub struct PersistentPipelineCache { - cache: Arc, + cache: Arc>, write_lock: Arc>, write_tasks: Vec>>, cache_path: PathBuf, @@ -237,7 +238,7 @@ impl PersistentPipelineCache { }; Ok(PersistentPipelineCache { - cache: Arc::new(cache), + cache: Arc::new(WgpuWrapper::new(cache)), write_lock: Arc::new(Mutex::new(())), write_tasks: vec![], cache_path: cache_path.to_path_buf(), @@ -314,7 +315,7 @@ impl PersistentPipelineCache { } /// Get the underlying wgpu pipeline cache. - pub fn get_cache(&self) -> Arc { + pub fn get_cache(&self) -> Arc> { self.cache.clone() } } From 5571acd23cdbe44fb4c8b427b3cbecd3aec721d2 Mon Sep 17 00:00:00 2001 From: Charlotte McElwain Date: Wed, 25 Jun 2025 01:16:58 -0700 Subject: [PATCH 9/9] WgpuWrapper. --- .../src/render_resource/persistent_pipeline_cache.rs | 2 +- crates/bevy_render/src/render_resource/pipeline_cache.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs index 2c6fe72ab0..bca4aa4d81 100644 --- a/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/persistent_pipeline_cache.rs @@ -10,6 +10,7 @@ use bevy_ecs::{ }; use bevy_platform::hash::FixedHasher; use bevy_render::{render_resource::PipelineCache, renderer::RenderDevice, Extract, Render}; +use bevy_utils::WgpuWrapper; use core::hash::BuildHasher; use std::{ fs, @@ -23,7 +24,6 @@ use std::{ use thiserror::Error; use tracing::{debug, warn}; use wgpu::{hal::PipelineCacheError, Backend, PipelineCacheDescriptor}; -use bevy_utils::WgpuWrapper; /// Plugin for managing [`wgpu::PipelineCache`] resources across application runs. /// diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index f7511acbd2..df7f232734 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -907,7 +907,7 @@ impl PipelineCache { // TODO: Should this be the same as the vertex compilation options? compilation_options, }), - cache: cache.as_deref(), + cache: cache.as_deref().map(|v| &**v), }; Ok(Pipeline::RenderPipeline( @@ -968,7 +968,7 @@ impl PipelineCache { zero_initialize_workgroup_memory: descriptor .zero_initialize_workgroup_memory, }, - cache: cache.as_deref(), + cache: cache.as_deref().map(|v| &**v), }; Ok(Pipeline::ComputePipeline(