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), sorted by member for binary search.
59    owner: Vec<(Member, usize)>,
60}
61
62impl SceneResidency {
63    /// Build from each scene's exclusively-owned members. Every scene starts
64    /// unpinned with nothing resident; the driver blocks every owned member
65    /// ([`all_members`](Self::all_members)) at setup, then the first
66    /// `sync_pins` unblocks the pinned scenes' members.
67    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    /// Whether nothing is pinned.
87    pub fn is_empty(&self) -> bool {
88        self.sets.is_empty()
89    }
90
91    /// Every scene-owned member, for the driver's setup pass: all owned members
92    /// start blocked, matching every scene starting unpinned.
93    pub fn all_members(&self) -> impl Iterator<Item = Member> + '_ {
94        self.owner.iter().map(|&(m, _)| m)
95    }
96
97    /// Establish the pin set: exactly the scenes in `pinned` are pinned after
98    /// this call. Returns the members whose blocked state must change on the
99    /// stream planners (a no-op sync returns empty changes).
100    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    /// Record a member's residency transition (a completed upload or an applied
119    /// eviction). Members owned by no scene are ignored.
120    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    /// The scene's derived load state, or `None` for an unknown scene.
131    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    /// Fraction of the scene's members resident (1.0 with no members), or
139    /// `None` for an unknown scene.
140    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    /// Whether any pinned scene still has members loading.
148    pub fn any_loading(&self) -> bool {
149        self.sets
150            .iter()
151            .any(|s| derive_state(s) == SceneLoadState::Loading)
152    }
153
154    /// Every scene's `(id, state, progress)`, in declaration order.
155    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    /// `status`, written into `out` (cleared first) so a per-frame poll reuses
162    /// its buffer.
163    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        // Setup blocks every owned member; the first sync then unblocks the
222        // pinned scene's members and leaves the rest blocked (no changes).
223        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        // Unpinning with content still resident drains through Unloading.
277        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        // Unpinning drains through Unloading, which is not a load in flight.
311        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}