1use crate::ecs::asset_id::AssetId;
9use alloc::vec;
10use alloc::vec::Vec;
11
12pub type Member = (u8, u32);
15
16pub const CHANNEL_TEXTURE: u8 = 0;
18pub const CHANNEL_MESH: u8 = 1;
20pub const CHANNEL_SHADER: u8 = 2;
23
24#[derive(Clone, Copy, PartialEq, Eq, Debug)]
26pub enum SceneLoadState {
27 Unloaded,
29 Loading,
31 Resident,
33 Unloading,
35}
36
37#[derive(Debug, Default, PartialEq, Eq)]
40pub struct PinChanges {
41 pub blocked: Vec<Member>,
43 pub unblocked: Vec<Member>,
45}
46
47struct SceneSet {
48 scene: AssetId,
49 pinned: bool,
50 members: Vec<Member>,
51 resident: Vec<bool>,
53}
54
55pub struct SceneResidency {
57 sets: Vec<SceneSet>,
58 owner: Vec<(Member, usize)>,
60}
61
62impl SceneResidency {
63 pub fn new(scenes: Vec<(AssetId, Vec<Member>)>) -> Self {
68 let mut owner: Vec<(Member, usize)> = scenes
69 .iter()
70 .enumerate()
71 .flat_map(|(set_idx, (_, members))| members.iter().map(move |&m| (m, set_idx)))
72 .collect();
73 owner.sort_unstable();
74 let sets = scenes
75 .into_iter()
76 .map(|(scene, members)| SceneSet {
77 scene,
78 pinned: false,
79 resident: vec![false; members.len()],
80 members,
81 })
82 .collect();
83 Self { sets, owner }
84 }
85
86 pub fn is_empty(&self) -> bool {
88 self.sets.is_empty()
89 }
90
91 pub fn all_members(&self) -> impl Iterator<Item = Member> + '_ {
94 self.owner.iter().map(|&(m, _)| m)
95 }
96
97 pub fn sync_pins(&mut self, pinned: &[AssetId]) -> PinChanges {
101 let mut changes = PinChanges::default();
102 for set in &mut self.sets {
103 let want = pinned.contains(&set.scene);
104 if want == set.pinned {
105 continue;
106 }
107 set.pinned = want;
108 let out = if want {
109 &mut changes.unblocked
110 } else {
111 &mut changes.blocked
112 };
113 out.extend_from_slice(&set.members);
114 }
115 changes
116 }
117
118 pub fn note_resident(&mut self, member: Member, resident: bool) {
121 let Ok(pos) = self.owner.binary_search_by_key(&member, |&(m, _)| m) else {
122 return;
123 };
124 let set = &mut self.sets[self.owner[pos].1];
125 if let Some(i) = set.members.iter().position(|&m| m == member) {
126 set.resident[i] = resident;
127 }
128 }
129
130 pub fn state(&self, scene: AssetId) -> Option<SceneLoadState> {
132 self.sets
133 .iter()
134 .find(|s| s.scene == scene)
135 .map(derive_state)
136 }
137
138 pub fn progress(&self, scene: AssetId) -> Option<f32> {
141 self.sets
142 .iter()
143 .find(|s| s.scene == scene)
144 .map(derive_progress)
145 }
146
147 pub fn any_loading(&self) -> bool {
149 self.sets
150 .iter()
151 .any(|s| derive_state(s) == SceneLoadState::Loading)
152 }
153
154 pub fn status(&self) -> Vec<(AssetId, SceneLoadState, f32)> {
156 let mut out = Vec::new();
157 self.status_into(&mut out);
158 out
159 }
160
161 pub fn status_into(&self, out: &mut Vec<(AssetId, SceneLoadState, f32)>) {
164 out.clear();
165 out.extend(
166 self.sets
167 .iter()
168 .map(|s| (s.scene, derive_state(s), derive_progress(s))),
169 );
170 }
171}
172
173fn derive_state(set: &SceneSet) -> SceneLoadState {
174 let all_resident = set.resident.iter().all(|&r| r);
175 let none_resident = set.resident.iter().all(|&r| !r);
176 match (set.pinned, all_resident, none_resident) {
177 (true, true, _) => SceneLoadState::Resident,
178 (true, false, _) => SceneLoadState::Loading,
179 (false, _, true) => SceneLoadState::Unloaded,
180 (false, _, false) => SceneLoadState::Unloading,
181 }
182}
183
184fn derive_progress(set: &SceneSet) -> f32 {
185 if set.members.is_empty() {
186 return 1.0;
187 }
188 let resident = set.resident.iter().filter(|&&r| r).count();
189 resident as f32 / set.members.len() as f32
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 fn residency() -> SceneResidency {
197 SceneResidency::new(vec![
198 (
199 AssetId(1),
200 vec![
201 (CHANNEL_TEXTURE, 0),
202 (CHANNEL_TEXTURE, 1),
203 (CHANNEL_MESH, 5),
204 ],
205 ),
206 (AssetId(2), vec![(CHANNEL_TEXTURE, 2)]),
207 (AssetId(3), vec![]),
208 ])
209 }
210
211 #[test]
212 fn scenes_start_unpinned_and_unloaded() {
213 let r = residency();
214 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Unloaded));
215 assert_eq!(r.state(AssetId(9)), None);
216 assert_eq!(r.progress(AssetId(1)), Some(0.0));
217 }
218
219 #[test]
220 fn first_sync_unblocks_only_the_pinned_scene() {
221 let mut r = residency();
224 let all: Vec<Member> = r.all_members().collect();
225 assert_eq!(all.len(), 4);
226 let changes = r.sync_pins(&[AssetId(1)]);
227 assert_eq!(
228 changes.unblocked,
229 vec![
230 (CHANNEL_TEXTURE, 0),
231 (CHANNEL_TEXTURE, 1),
232 (CHANNEL_MESH, 5)
233 ]
234 );
235 assert!(changes.blocked.is_empty());
236 }
237
238 #[test]
239 fn repeated_sync_is_a_no_op() {
240 let mut r = residency();
241 r.sync_pins(&[AssetId(1)]);
242 assert_eq!(r.sync_pins(&[AssetId(1)]), PinChanges::default());
243 }
244
245 #[test]
246 fn pin_switch_swaps_blocked_and_unblocked() {
247 let mut r = residency();
248 r.sync_pins(&[AssetId(1)]);
249 let changes = r.sync_pins(&[AssetId(2)]);
250 assert_eq!(
251 changes.blocked,
252 vec![
253 (CHANNEL_TEXTURE, 0),
254 (CHANNEL_TEXTURE, 1),
255 (CHANNEL_MESH, 5)
256 ]
257 );
258 assert_eq!(changes.unblocked, vec![(CHANNEL_TEXTURE, 2)]);
259 }
260
261 #[test]
262 fn state_and_progress_follow_residency_notes() {
263 let mut r = residency();
264 r.sync_pins(&[AssetId(1)]);
265 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Loading));
266
267 r.note_resident((CHANNEL_TEXTURE, 0), true);
268 r.note_resident((CHANNEL_TEXTURE, 1), true);
269 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Loading));
270 assert!((r.progress(AssetId(1)).unwrap() - 2.0 / 3.0).abs() < 1e-6);
271
272 r.note_resident((CHANNEL_MESH, 5), true);
273 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Resident));
274 assert_eq!(r.progress(AssetId(1)), Some(1.0));
275
276 r.sync_pins(&[AssetId(2)]);
278 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Unloading));
279 r.note_resident((CHANNEL_TEXTURE, 0), false);
280 r.note_resident((CHANNEL_TEXTURE, 1), false);
281 r.note_resident((CHANNEL_MESH, 5), false);
282 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Unloaded));
283 }
284
285 #[test]
286 fn memberless_scene_is_resident_when_pinned() {
287 let mut r = residency();
288 r.sync_pins(&[AssetId(3)]);
289 assert_eq!(r.state(AssetId(3)), Some(SceneLoadState::Resident));
290 assert_eq!(r.progress(AssetId(3)), Some(1.0));
291 }
292
293 #[test]
294 fn unowned_member_notes_are_ignored() {
295 let mut r = residency();
296 r.note_resident((CHANNEL_TEXTURE, 99), true);
297 assert_eq!(r.progress(AssetId(1)), Some(0.0));
298 }
299
300 #[test]
301 fn any_loading_tracks_pinned_scenes_only() {
302 let mut r = residency();
303 assert!(!r.any_loading());
304 r.sync_pins(&[AssetId(1)]);
305 assert!(r.any_loading());
306 r.note_resident((CHANNEL_TEXTURE, 0), true);
307 r.note_resident((CHANNEL_TEXTURE, 1), true);
308 r.note_resident((CHANNEL_MESH, 5), true);
309 assert!(!r.any_loading());
310 r.sync_pins(&[]);
312 assert!(!r.any_loading());
313 }
314
315 #[test]
316 fn status_lists_scenes_in_declaration_order() {
317 let mut r = residency();
318 r.sync_pins(&[AssetId(2)]);
319 r.note_resident((CHANNEL_TEXTURE, 2), true);
320 let status = r.status();
321 assert_eq!(status[0].0, AssetId(1));
322 assert_eq!(status[1], (AssetId(2), SceneLoadState::Resident, 1.0));
323 assert_eq!(status[2], (AssetId(3), SceneLoadState::Unloaded, 1.0));
324 }
325}