Skip to main content

concinnity_render/
scene_residency.rs

1//! Scene residency bookkeeping: which scenes are pinned (their streamed
2//! content wanted on the GPU), which of each scene's members are currently
3//! resident, and the derived per-scene load state and progress. Pure
4//! bookkeeping against `core` + `alloc` only, like the streaming policy core:
5//! the driver applies pin changes to the stream planners as blocked flags and
6//! reports residency transitions back here.
7
8use crate::ecs::asset_id::AssetId;
9use alloc::vec;
10use alloc::vec::Vec;
11
12/// A streamed item a scene exclusively owns: the driver's channel tag (which
13/// pool the item lives in) plus the pool's item id.
14pub type Member = (u8, u32);
15
16/// Residency channel for textures.
17pub const CHANNEL_TEXTURE: u8 = 0;
18/// Residency channel for meshes.
19pub const CHANNEL_MESH: u8 = 1;
20/// A shader bucket's render pipeline, built on pin and released on unload. The
21/// item id is the bucket (the material's `ShaderHandle` value).
22pub const CHANNEL_SHADER: u8 = 2;
23
24/// Load state of one scene's streamed content, derived from pins + residency.
25#[derive(Clone, Copy, PartialEq, Eq, Debug)]
26pub enum SceneLoadState {
27    /// Unpinned, nothing resident.
28    Unloaded,
29    /// Pinned, some members still loading.
30    Loading,
31    /// Pinned, every member resident (trivially true with no members).
32    Resident,
33    /// Unpinned, members still draining off the GPU.
34    Unloading,
35}
36
37/// Pin changes produced by one `sync_pins` call, as planner blocked-flag
38/// updates the driver must apply.
39#[derive(Debug, Default, PartialEq, Eq)]
40pub struct PinChanges {
41    /// Members whose owning scene became unpinned: block them.
42    pub blocked: Vec<Member>,
43    /// Members whose owning scene became pinned: unblock them.
44    pub unblocked: Vec<Member>,
45}
46
47struct SceneSet {
48    scene: AssetId,
49    pinned: bool,
50    members: Vec<Member>,
51    // Parallel to `members`.
52    resident: Vec<bool>,
53}
54
55/// Per-scene residency pins, one refcount per resource and channel.
56pub struct SceneResidency {
57    sets: Vec<SceneSet>,
58    // (member, set index, index within that set's members), sorted by member
59    // for binary search.
60    owner: Vec<(Member, usize, usize)>,
61}
62
63impl SceneResidency {
64    /// Build from each scene's exclusively-owned members. Every scene starts
65    /// unpinned with nothing resident; the driver blocks every owned member
66    /// ([`all_members`](Self::all_members)) at setup, then the first
67    /// `sync_pins` unblocks the pinned scenes' members.
68    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    /// Whether nothing is pinned.
93    pub fn is_empty(&self) -> bool {
94        self.sets.is_empty()
95    }
96
97    /// Every scene-owned member, for the driver's setup pass: all owned members
98    /// start blocked, matching every scene starting unpinned.
99    pub fn all_members(&self) -> impl Iterator<Item = Member> + '_ {
100        self.owner.iter().map(|&(m, ..)| m)
101    }
102
103    /// Establish the pin set: exactly the scenes in `pinned` are pinned after
104    /// this call. Returns the members whose blocked state must change on the
105    /// stream planners (a no-op sync returns empty changes).
106    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    /// Record a member's residency transition (a completed upload or an applied
125    /// eviction). Members owned by no scene are ignored.
126    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    /// The scene's derived load state, or `None` for an unknown scene.
135    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    /// Fraction of the scene's members resident (1.0 with no members), or
143    /// `None` for an unknown scene.
144    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    /// Whether any pinned scene still has members loading.
152    pub fn any_loading(&self) -> bool {
153        self.sets
154            .iter()
155            .any(|s| derive_state(s) == SceneLoadState::Loading)
156    }
157
158    /// Every scene's `(id, state, progress)`, in declaration order.
159    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    /// `status`, written into `out` (cleared first) so a per-frame poll reuses
166    /// its buffer.
167    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        // Setup blocks every owned member; the first sync then unblocks the
226        // pinned scene's members and leaves the rest blocked (no changes).
227        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        // Unpinning with content still resident drains through Unloading.
281        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        // Unpinning drains through Unloading, which is not a load in flight.
315        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}