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, usize)>,
61}
62
63impl SceneResidency {
64 pub fn new(scenes: Vec<(AssetId, Vec<Member>)>) -> Self {
69 let mut owner: Vec<(Member, usize, usize)> = scenes
70 .iter()
71 .enumerate()
72 .flat_map(|(set_idx, (_, members))| {
73 members
74 .iter()
75 .enumerate()
76 .map(move |(i, &m)| (m, set_idx, i))
77 })
78 .collect();
79 owner.sort_unstable();
80 let sets = scenes
81 .into_iter()
82 .map(|(scene, members)| SceneSet {
83 scene,
84 pinned: false,
85 resident: vec![false; members.len()],
86 members,
87 })
88 .collect();
89 Self { sets, owner }
90 }
91
92 pub fn is_empty(&self) -> bool {
94 self.sets.is_empty()
95 }
96
97 pub fn all_members(&self) -> impl Iterator<Item = Member> + '_ {
100 self.owner.iter().map(|&(m, ..)| m)
101 }
102
103 pub fn sync_pins(&mut self, pinned: &[AssetId]) -> PinChanges {
107 let mut changes = PinChanges::default();
108 for set in &mut self.sets {
109 let want = pinned.contains(&set.scene);
110 if want == set.pinned {
111 continue;
112 }
113 set.pinned = want;
114 let out = if want {
115 &mut changes.unblocked
116 } else {
117 &mut changes.blocked
118 };
119 out.extend_from_slice(&set.members);
120 }
121 changes
122 }
123
124 pub fn note_resident(&mut self, member: Member, resident: bool) {
127 let Ok(pos) = self.owner.binary_search_by_key(&member, |&(m, ..)| m) else {
128 return;
129 };
130 let (_, set_idx, member_idx) = self.owner[pos];
131 self.sets[set_idx].resident[member_idx] = resident;
132 }
133
134 pub fn state(&self, scene: AssetId) -> Option<SceneLoadState> {
136 self.sets
137 .iter()
138 .find(|s| s.scene == scene)
139 .map(derive_state)
140 }
141
142 pub fn progress(&self, scene: AssetId) -> Option<f32> {
145 self.sets
146 .iter()
147 .find(|s| s.scene == scene)
148 .map(derive_progress)
149 }
150
151 pub fn any_loading(&self) -> bool {
153 self.sets
154 .iter()
155 .any(|s| derive_state(s) == SceneLoadState::Loading)
156 }
157
158 pub fn status(&self) -> Vec<(AssetId, SceneLoadState, f32)> {
160 let mut out = Vec::new();
161 self.status_into(&mut out);
162 out
163 }
164
165 pub fn status_into(&self, out: &mut Vec<(AssetId, SceneLoadState, f32)>) {
168 out.clear();
169 out.extend(
170 self.sets
171 .iter()
172 .map(|s| (s.scene, derive_state(s), derive_progress(s))),
173 );
174 }
175}
176
177fn derive_state(set: &SceneSet) -> SceneLoadState {
178 let all_resident = set.resident.iter().all(|&r| r);
179 let none_resident = set.resident.iter().all(|&r| !r);
180 match (set.pinned, all_resident, none_resident) {
181 (true, true, _) => SceneLoadState::Resident,
182 (true, false, _) => SceneLoadState::Loading,
183 (false, _, true) => SceneLoadState::Unloaded,
184 (false, _, false) => SceneLoadState::Unloading,
185 }
186}
187
188fn derive_progress(set: &SceneSet) -> f32 {
189 if set.members.is_empty() {
190 return 1.0;
191 }
192 let resident = set.resident.iter().filter(|&&r| r).count();
193 resident as f32 / set.members.len() as f32
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 fn residency() -> SceneResidency {
201 SceneResidency::new(vec![
202 (
203 AssetId(1),
204 vec![
205 (CHANNEL_TEXTURE, 0),
206 (CHANNEL_TEXTURE, 1),
207 (CHANNEL_MESH, 5),
208 ],
209 ),
210 (AssetId(2), vec![(CHANNEL_TEXTURE, 2)]),
211 (AssetId(3), vec![]),
212 ])
213 }
214
215 #[test]
216 fn scenes_start_unpinned_and_unloaded() {
217 let r = residency();
218 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Unloaded));
219 assert_eq!(r.state(AssetId(9)), None);
220 assert_eq!(r.progress(AssetId(1)), Some(0.0));
221 }
222
223 #[test]
224 fn first_sync_unblocks_only_the_pinned_scene() {
225 let mut r = residency();
228 let all: Vec<Member> = r.all_members().collect();
229 assert_eq!(all.len(), 4);
230 let changes = r.sync_pins(&[AssetId(1)]);
231 assert_eq!(
232 changes.unblocked,
233 vec![
234 (CHANNEL_TEXTURE, 0),
235 (CHANNEL_TEXTURE, 1),
236 (CHANNEL_MESH, 5)
237 ]
238 );
239 assert!(changes.blocked.is_empty());
240 }
241
242 #[test]
243 fn repeated_sync_is_a_no_op() {
244 let mut r = residency();
245 r.sync_pins(&[AssetId(1)]);
246 assert_eq!(r.sync_pins(&[AssetId(1)]), PinChanges::default());
247 }
248
249 #[test]
250 fn pin_switch_swaps_blocked_and_unblocked() {
251 let mut r = residency();
252 r.sync_pins(&[AssetId(1)]);
253 let changes = r.sync_pins(&[AssetId(2)]);
254 assert_eq!(
255 changes.blocked,
256 vec![
257 (CHANNEL_TEXTURE, 0),
258 (CHANNEL_TEXTURE, 1),
259 (CHANNEL_MESH, 5)
260 ]
261 );
262 assert_eq!(changes.unblocked, vec![(CHANNEL_TEXTURE, 2)]);
263 }
264
265 #[test]
266 fn state_and_progress_follow_residency_notes() {
267 let mut r = residency();
268 r.sync_pins(&[AssetId(1)]);
269 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Loading));
270
271 r.note_resident((CHANNEL_TEXTURE, 0), true);
272 r.note_resident((CHANNEL_TEXTURE, 1), true);
273 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Loading));
274 assert!((r.progress(AssetId(1)).unwrap() - 2.0 / 3.0).abs() < 1e-6);
275
276 r.note_resident((CHANNEL_MESH, 5), true);
277 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Resident));
278 assert_eq!(r.progress(AssetId(1)), Some(1.0));
279
280 r.sync_pins(&[AssetId(2)]);
282 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Unloading));
283 r.note_resident((CHANNEL_TEXTURE, 0), false);
284 r.note_resident((CHANNEL_TEXTURE, 1), false);
285 r.note_resident((CHANNEL_MESH, 5), false);
286 assert_eq!(r.state(AssetId(1)), Some(SceneLoadState::Unloaded));
287 }
288
289 #[test]
290 fn memberless_scene_is_resident_when_pinned() {
291 let mut r = residency();
292 r.sync_pins(&[AssetId(3)]);
293 assert_eq!(r.state(AssetId(3)), Some(SceneLoadState::Resident));
294 assert_eq!(r.progress(AssetId(3)), Some(1.0));
295 }
296
297 #[test]
298 fn unowned_member_notes_are_ignored() {
299 let mut r = residency();
300 r.note_resident((CHANNEL_TEXTURE, 99), true);
301 assert_eq!(r.progress(AssetId(1)), Some(0.0));
302 }
303
304 #[test]
305 fn any_loading_tracks_pinned_scenes_only() {
306 let mut r = residency();
307 assert!(!r.any_loading());
308 r.sync_pins(&[AssetId(1)]);
309 assert!(r.any_loading());
310 r.note_resident((CHANNEL_TEXTURE, 0), true);
311 r.note_resident((CHANNEL_TEXTURE, 1), true);
312 r.note_resident((CHANNEL_MESH, 5), true);
313 assert!(!r.any_loading());
314 r.sync_pins(&[]);
316 assert!(!r.any_loading());
317 }
318
319 #[test]
320 fn status_lists_scenes_in_declaration_order() {
321 let mut r = residency();
322 r.sync_pins(&[AssetId(2)]);
323 r.note_resident((CHANNEL_TEXTURE, 2), true);
324 let status = r.status();
325 assert_eq!(status[0].0, AssetId(1));
326 assert_eq!(status[1], (AssetId(2), SceneLoadState::Resident, 1.0));
327 assert_eq!(status[2], (AssetId(3), SceneLoadState::Unloaded, 1.0));
328 }
329}