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