Skip to main content

concinnity_asset/
streaming_config.rs

1// Asset-streaming configuration schema.
2
3/// Enables and tunes asset streaming.
4///
5/// When no `StreamingConfig` is declared, streaming is off and every texture and
6/// mesh is loaded up front. When one is present, textures and static mesh
7/// geometry load in gradually after startup: each frame the nearest not-yet-
8/// loaded items are brought in, up to a per-frame budget, prioritised by camera
9/// distance. Once more than the cap would be loaded at once, the farthest are
10/// dropped to make room.
11///
12/// Texture streaming covers the colour and normal-map textures (each capped
13/// independently via `texture_budget` / `texture_cap`). Mesh streaming covers
14/// static geometry; the skybox, rooms, and moving props always stay loaded.
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
16#[serde(default)]
17pub struct StreamingConfig {
18    /// Maximum number of textures whose load is started per frame, applied
19    /// independently to the colour and normal-map pools. A low value spreads the
20    /// cost over more frames.
21    pub texture_budget: u32,
22    /// Maximum number of textures kept loaded at once, applied independently to
23    /// the colour and normal-map pools. When exceeded, the farthest-from-camera
24    /// textures are dropped.
25    pub texture_cap: u32,
26    /// Maximum number of mesh regions whose load is started per frame. A low
27    /// value spreads the cost over more frames.
28    pub mesh_budget: u32,
29    /// Maximum number of meshes kept loaded at once. When exceeded, the
30    /// farthest-from-camera meshes are dropped.
31    pub mesh_cap: u32,
32    /// Resident-texture memory budget in mebibytes, spanning the colour and
33    /// normal-map pools together. Once resident textures exceed it the
34    /// farthest-from-camera ones are dropped, so nearer textures always win the
35    /// space. `0` (the default) derives the budget from the GPU's reported
36    /// memory instead. `texture_cap` still applies as a hard item-count ceiling.
37    pub texture_budget_mb: u32,
38    /// Resident-mesh memory budget in mebibytes. Once resident meshes exceed it
39    /// the farthest-from-camera ones are dropped. `0` (the default) derives the
40    /// budget from the GPU's reported memory instead. `mesh_cap` still applies
41    /// as a hard item-count ceiling.
42    pub mesh_budget_mb: u32,
43}
44
45impl Default for StreamingConfig {
46    fn default() -> Self {
47        Self {
48            texture_budget: 4,
49            texture_cap: 96,
50            mesh_budget: 4,
51            mesh_cap: 4096,
52            texture_budget_mb: 0,
53            mesh_budget_mb: 0,
54        }
55    }
56}
57
58impl StreamingConfig {
59    /// Per-frame texture load budget as a `usize`, floored at 1 so a stray 0
60    /// cannot wedge streaming permanently.
61    pub fn budget(&self) -> usize {
62        (self.texture_budget as usize).max(1)
63    }
64
65    /// Resident-texture cap as a `usize`, floored at 1.
66    pub fn cap(&self) -> usize {
67        (self.texture_cap as usize).max(1)
68    }
69
70    /// Per-frame mesh load budget as a `usize`, floored at 1.
71    pub fn mesh_budget(&self) -> usize {
72        (self.mesh_budget as usize).max(1)
73    }
74
75    /// Resident-mesh cap as a `usize`, floored at 1.
76    pub fn mesh_cap(&self) -> usize {
77        (self.mesh_cap as usize).max(1)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn defaults_stream_a_few_resources_per_frame() {
87        let c = StreamingConfig::default();
88        assert_eq!(c.budget(), 4);
89        assert_eq!(c.cap(), 96);
90        assert_eq!(c.mesh_budget(), 4);
91        assert_eq!(c.mesh_cap(), 4096);
92        // Byte budgets are opt-in; zero leaves the count budgets in charge.
93        assert_eq!(c.texture_budget_mb, 0);
94        assert_eq!(c.mesh_budget_mb, 0);
95    }
96
97    #[test]
98    fn a_zero_budget_or_cap_is_floored_at_one() {
99        // A zero would stall streaming outright, so every accessor keeps at
100        // least one slot rather than trusting the authored number.
101        let c: StreamingConfig = serde_json::from_str(
102            r#"{"texture_budget":0,"texture_cap":0,"mesh_budget":0,"mesh_cap":0}"#,
103        )
104        .unwrap();
105        assert_eq!(c.budget(), 1);
106        assert_eq!(c.cap(), 1);
107        assert_eq!(c.mesh_budget(), 1);
108        assert_eq!(c.mesh_cap(), 1);
109    }
110
111    #[test]
112    fn authored_values_pass_through_and_round_trip_through_postcard() {
113        let c: StreamingConfig = serde_json::from_str(
114            r#"{"texture_budget":8,"texture_cap":256,"mesh_budget":2,"mesh_cap":512,
115                "texture_budget_mb":1024,"mesh_budget_mb":256}"#,
116        )
117        .unwrap();
118        assert_eq!((c.budget(), c.cap()), (8, 256));
119        assert_eq!((c.mesh_budget(), c.mesh_cap()), (2, 512));
120
121        let bytes = postcard::to_allocvec(&c).unwrap();
122        let back: StreamingConfig = postcard::from_bytes(&bytes).unwrap();
123        assert_eq!(back.texture_budget_mb, 1024);
124        assert_eq!(back.mesh_budget_mb, 256);
125        assert_eq!(back.cap(), 256);
126    }
127}