Skip to main content

bevy_cache/
save_queue.rs

1use bevy::prelude::*;
2use bevy::reflect::serde::ReflectSerializer;
3
4use std::time::Duration;
5
6use crate::config::CacheConfig;
7use crate::error::CacheError;
8use crate::manifest::CacheManifest;
9
10/// A pending save entry — either reflected data awaiting serialization,
11/// or a reader ready to write.
12pub(crate) enum PendingPayload {
13    /// A reflected value to be serialized via [`ReflectSerializer`] → RON.
14    Reflect {
15        value: Box<dyn Reflect>,
16        extension: String,
17    },
18    /// Pre-serialized data provided as a [`Read`]er.
19    Bytes {
20        reader: Box<dyn std::io::Read + Send + Sync + 'static>,
21        extension: String,
22    },
23}
24
25pub(crate) struct PendingSaveEntry {
26    pub key: String,
27    pub max_age: Option<Duration>,
28    pub payload: PendingPayload,
29}
30
31/// Resource holding queued asset-save requests.
32///
33/// Assets can be enqueued in two ways:
34///
35/// 1. [`enqueue_reflect`](Self::enqueue_reflect) — pass any value that
36///    implements [`Reflect`]. The value will be serialized to RON via
37///    Bevy's [`ReflectSerializer`] using the [`AppTypeRegistry`].
38///    The type **must** be registered in the type registry (via
39///    `app.register_type::<T>()`) and should have `ReflectSerialize`
40///    type data (derived automatically for types that implement both
41///    `Reflect` and `serde::Serialize`).
42///
43/// 2. [`enqueue`](Self::enqueue) — pass any [`Read`]er containing
44///    pre-serialized data and a file extension. No reflection or registry
45///    required. Use [`std::io::Cursor`] in tests or for in-memory data.
46///
47/// Enqueued entries are processed by [`process_pending_saves`] which runs
48/// each frame in `PostUpdate`.
49#[derive(Resource, Default)]
50pub struct CacheQueue {
51    pub(crate) queue: Vec<PendingSaveEntry>,
52}
53
54impl CacheQueue {
55    /// Returns `true` when there are no pending save requests.
56    pub fn is_empty(&self) -> bool {
57        self.queue.is_empty()
58    }
59
60    /// Returns the number of pending save requests.
61    pub fn len(&self) -> usize {
62        self.queue.len()
63    }
64
65    /// Enqueue a reflected value for caching.
66    ///
67    /// The value will be serialized to RON using Bevy's
68    /// [`ReflectSerializer`] during [`process_pending_saves`].
69    /// The type must be registered in the [`AppTypeRegistry`].
70    ///
71    /// # Example
72    /// ```rust,ignore
73    /// #[derive(Asset, TypePath, Reflect, serde::Serialize)]
74    /// #[reflect(Serialize)]
75    /// struct LevelData { tiles: Vec<u32> }
76    ///
77    /// fn cache_level(
78    ///     mut pending: ResMut<CacheQueue>,
79    ///     assets: Res<Assets<LevelData>>,
80    ///     handle: Res<MyLevelHandle>,
81    /// ) {
82    ///     if let Some(level) = assets.get(&handle.0) {
83    ///         pending.enqueue_reflect(
84    ///             Box::new(level.clone()),
85    ///             "level_01",
86    ///             "ron",
87    ///             None,
88    ///         );
89    ///     }
90    /// }
91    /// ```
92    pub fn enqueue_reflect(
93        &mut self,
94        value: Box<dyn Reflect>,
95        key: impl Into<String>,
96        extension: impl Into<String>,
97        max_age: Option<Duration>,
98    ) {
99        self.queue.push(PendingSaveEntry {
100            key: key.into(),
101            max_age,
102            payload: PendingPayload::Reflect {
103                value,
104                extension: extension.into(),
105            },
106        });
107    }
108
109    /// Enqueue a [`Read`]er for caching.
110    ///
111    /// The caller is responsible for serialization; the data is streamed
112    /// as-is to `{key}.{extension}` in the cache directory without buffering
113    /// the entire payload in memory.
114    ///
115    /// Use [`std::io::Cursor`] to wrap in-memory buffers:
116    ///
117    /// # Example
118    /// ```rust,ignore
119    /// fn cache_screenshot(mut pending: ResMut<CacheQueue>) {
120    ///     let png_bytes: Vec<u8> = capture_screenshot();
121    ///     pending.enqueue("scene_01", "png", std::io::Cursor::new(png_bytes), None);
122    /// }
123    /// ```
124    pub fn enqueue<R: std::io::Read + Send + Sync + 'static>(
125        &mut self,
126        key: impl Into<String>,
127        extension: impl Into<String>,
128        reader: R,
129        max_age: Option<Duration>,
130    ) {
131        self.queue.push(PendingSaveEntry {
132            key: key.into(),
133            max_age,
134            payload: PendingPayload::Bytes {
135                reader: Box::new(reader),
136                extension: extension.into(),
137            },
138        });
139    }
140}
141
142/// Serialize a reflected value to RON bytes using the type registry.
143fn serialize_reflect(
144    value: &dyn Reflect,
145    registry: &AppTypeRegistry,
146) -> Result<Vec<u8>, CacheError> {
147    let registry = registry.read();
148    let serializer = ReflectSerializer::new(value.as_partial_reflect(), &registry);
149    let pretty = ron::ser::PrettyConfig::default();
150    let serialized =
151        ron::ser::to_string_pretty(&serializer, pretty).map_err(CacheError::RonSerialize)?;
152    Ok(serialized.into_bytes())
153}
154
155/// System that processes the pending save queue each frame.
156/// Reflected values are serialized via [`ReflectSerializer`], then all
157/// entries are written to the cache directory through [`CacheManifest::store`].
158pub fn process_pending_saves(world: &mut World) {
159    let queue = {
160        let mut pending = world
161            .get_resource_mut::<CacheQueue>()
162            .expect("CacheQueue resource should exist");
163        std::mem::take(&mut pending.queue)
164    };
165
166    if queue.is_empty() {
167        return;
168    }
169
170    let config = world
171        .get_resource::<CacheConfig>()
172        .expect("CacheConfig resource should exist")
173        .clone();
174
175    let registry = world
176        .get_resource::<AppTypeRegistry>()
177        .expect("AppTypeRegistry resource should exist")
178        .clone();
179
180    for entry in queue {
181        let PendingSaveEntry { key, max_age, payload } = entry;
182        let result = match payload {
183            PendingPayload::Reflect { value, extension } => {
184                serialize_reflect(value.as_ref(), &registry)
185                    .and_then(|bytes| {
186                        let mut manifest = world
187                            .get_resource_mut::<CacheManifest>()
188                            .expect("CacheManifest resource should exist");
189                        manifest.store(
190                            &config,
191                            &key,
192                            &extension,
193                            std::io::Cursor::new(bytes),
194                            max_age,
195                        )
196                    })
197            }
198            PendingPayload::Bytes { mut reader, extension } => {
199                let mut manifest = world
200                    .get_resource_mut::<CacheManifest>()
201                    .expect("CacheManifest resource should exist");
202                manifest.store(
203                    &config,
204                    &key,
205                    &extension,
206                    &mut *reader,
207                    max_age,
208                )
209            }
210        };
211
212        match result {
213            Ok(()) => {
214                tracing::debug!("Cached asset '{key}'");
215            }
216            Err(e) => {
217                tracing::error!("Failed to cache asset '{key}': {e}");
218            }
219        }
220    }
221}