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}