Skip to main content

frust_engine/
config.rs

1use std::sync::OnceLock;
2
3/// Parse an environment flag as enabled/disabled, checking compile-time or runtime value.
4///
5/// Mirrors the pattern in `frust-render::context`, supporting both compile-time
6/// (`option_env!`) and runtime (`std::env::var`) checks. Runtime overrides
7/// compile-time.
8fn env_flag_enabled(compile_time: Option<&str>, runtime: Option<String>) -> bool {
9    if let Some(val) = runtime {
10        val.eq_ignore_ascii_case("1") || val.eq_ignore_ascii_case("true")
11    } else if let Some(val) = compile_time {
12        val.eq_ignore_ascii_case("1") || val.eq_ignore_ascii_case("true")
13    } else {
14        false
15    }
16}
17
18/// The precedence-resolved value of a string-or-number-valued `FRUST_ENGINE_*`
19/// env knob, mirroring `frust-render::context`'s `env_str` combinator: a
20/// non-empty runtime (`std::env::var`) value wins even when a compile-time
21/// (`option_env!`) value is also set; an empty (`""`) runtime value is
22/// treated as unset and falls through to the compile-time half; `None` when
23/// neither half carries a non-empty value. This is the same
24/// compile-time-or-runtime shape `env_flag_enabled` uses for booleans, kept
25/// as its own combinator because `max_texture_size`/`atlas_size` need the
26/// raw string (to parse) rather than a bool.
27fn env_str(compile_time: Option<&'static str>, runtime: Option<String>) -> Option<String> {
28    fn non_empty(value: Option<String>) -> Option<String> {
29        value.filter(|v| !v.is_empty())
30    }
31    non_empty(runtime).or_else(|| non_empty(compile_time.map(str::to_string)))
32}
33
34/// Parses a raw `FRUST_ENGINE_MAX_TEX` value (already resolved by
35/// [`env_str`]) into the maximum texture edge length. An unset, empty, or
36/// unparsable value falls back to `0` (no limit).
37fn parse_max_texture_size(raw: Option<String>) -> u32 {
38    raw.and_then(|s| s.parse().ok()).unwrap_or(0)
39}
40
41/// Parses a raw `FRUST_ENGINE_ATLAS_SIZE` value (already resolved by
42/// [`env_str`]), e.g. `"2048x2048"`, into `(width, height)`. Returns `None`
43/// if unset or not exactly two `x`-separated integers.
44fn parse_atlas_size(raw: Option<String>) -> Option<(u32, u32)> {
45    raw.and_then(|s| {
46        let parts: Vec<&str> = s.split('x').collect();
47        if parts.len() == 2 {
48            let w = parts[0].parse::<u32>().ok()?;
49            let h = parts[1].parse::<u32>().ok()?;
50            Some((w, h))
51        } else {
52            None
53        }
54    })
55}
56
57/// Whether depth textures are disabled via `FRUST_ENGINE_NO_DEPTH`.
58/// Cached: read once per process.
59pub fn depth_disabled() -> bool {
60    static DISABLED: OnceLock<bool> = OnceLock::new();
61    *DISABLED.get_or_init(|| {
62        env_flag_enabled(
63            option_env!("FRUST_ENGINE_NO_DEPTH"),
64            std::env::var("FRUST_ENGINE_NO_DEPTH").ok(),
65        )
66    })
67}
68
69/// Whether atlas allocation is disabled via `FRUST_ENGINE_NO_ATLAS`.
70/// Cached: read once per process.
71///
72/// Consulted by [`crate::cache::images::ImageResidency`]: when set, every
73/// image resolution answers a logged skip instead of touching the atlas.
74pub fn atlas_disabled() -> bool {
75    static DISABLED: OnceLock<bool> = OnceLock::new();
76    *DISABLED.get_or_init(|| {
77        env_flag_enabled(
78            option_env!("FRUST_ENGINE_NO_ATLAS"),
79            std::env::var("FRUST_ENGINE_NO_ATLAS").ok(),
80        )
81    })
82}
83
84/// Whether offscreen fragment-shader effects are disabled via
85/// `FRUST_ENGINE_NO_SHADER_EFFECTS`. Cached: read once per process.
86///
87/// Consulted by [`crate::effects::shader_quad`] and by the compiler's
88/// [`frust_scene::Command::ShaderQuad`] lowering: when set, no user fragment
89/// program is compiled and no shader quad is drawn — the frame renders
90/// everything else and reports the skip once at warning level. The escape
91/// hatch for a driver that miscompiles a user shader, where the alternative
92/// is losing the whole application rather than one effect.
93pub fn shader_effects_disabled() -> bool {
94    static DISABLED: OnceLock<bool> = OnceLock::new();
95    *DISABLED.get_or_init(|| {
96        env_flag_enabled(
97            option_env!("FRUST_ENGINE_NO_SHADER_EFFECTS"),
98            std::env::var("FRUST_ENGINE_NO_SHADER_EFFECTS").ok(),
99        )
100    })
101}
102
103/// Whether layer caching is disabled via `FRUST_ENGINE_NO_LAYERS`.
104/// Cached: read once per process.
105///
106/// Reserved: parsed and cached, consulted by nothing yet; wired when its
107/// subsystem lands.
108pub fn layers_disabled() -> bool {
109    static DISABLED: OnceLock<bool> = OnceLock::new();
110    *DISABLED.get_or_init(|| {
111        env_flag_enabled(
112            option_env!("FRUST_ENGINE_NO_LAYERS"),
113            std::env::var("FRUST_ENGINE_NO_LAYERS").ok(),
114        )
115    })
116}
117
118/// Whether resource pooling is disabled via `FRUST_ENGINE_NO_POOL`.
119/// Cached: read once per process.
120///
121/// Reserved: parsed and cached, consulted by nothing yet; wired when its
122/// subsystem lands.
123pub fn pool_disabled() -> bool {
124    static DISABLED: OnceLock<bool> = OnceLock::new();
125    *DISABLED.get_or_init(|| {
126        env_flag_enabled(
127            option_env!("FRUST_ENGINE_NO_POOL"),
128            std::env::var("FRUST_ENGINE_NO_POOL").ok(),
129        )
130    })
131}
132
133/// Maximum texture resolution (each axis) via `FRUST_ENGINE_MAX_TEX=<n>`.
134/// Parsed as an integer; defaults to 0 (no limit). Cached: read once per process.
135///
136/// Reserved: parsed and cached, consulted by nothing yet; wired when its
137/// subsystem lands.
138pub fn max_texture_size() -> u32 {
139    static MAX_SIZE: OnceLock<u32> = OnceLock::new();
140    *MAX_SIZE.get_or_init(|| {
141        parse_max_texture_size(env_str(
142            option_env!("FRUST_ENGINE_MAX_TEX"),
143            std::env::var("FRUST_ENGINE_MAX_TEX").ok(),
144        ))
145    })
146}
147
148/// Atlas size via `FRUST_ENGINE_ATLAS_SIZE=<WxH>` (e.g., "2048x2048").
149/// Parsed as `(width, height)` or returns `None` if not set or invalid.
150/// Cached: read once per process.
151///
152/// Consulted by [`crate::cache::images::AtlasBudget`]: when set, it overrides
153/// the capability-chosen per-layer atlas extent (runtime wins).
154pub fn atlas_size() -> Option<(u32, u32)> {
155    static ATLAS_SIZE: OnceLock<Option<(u32, u32)>> = OnceLock::new();
156    *ATLAS_SIZE.get_or_init(|| {
157        parse_atlas_size(env_str(
158            option_env!("FRUST_ENGINE_ATLAS_SIZE"),
159            std::env::var("FRUST_ENGINE_ATLAS_SIZE").ok(),
160        ))
161    })
162}
163
164/// Downlevel mode via `FRUST_ENGINE_DOWNLEVEL=1`.
165/// Cached: read once per process.
166pub fn downlevel_mode() -> bool {
167    static DOWNLEVEL: OnceLock<bool> = OnceLock::new();
168    *DOWNLEVEL.get_or_init(|| {
169        env_flag_enabled(
170            option_env!("FRUST_ENGINE_DOWNLEVEL"),
171            std::env::var("FRUST_ENGINE_DOWNLEVEL").ok(),
172        )
173    })
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_env_flag_enabled() {
182        assert!(env_flag_enabled(Some("1"), None));
183        assert!(env_flag_enabled(Some("true"), None));
184        assert!(env_flag_enabled(Some("TRUE"), None));
185        assert!(!env_flag_enabled(Some("0"), None));
186        assert!(!env_flag_enabled(Some("false"), None));
187        assert!(!env_flag_enabled(None, None));
188    }
189
190    #[test]
191    fn test_runtime_overrides_compile_time() {
192        assert!(env_flag_enabled(Some("0"), Some("1".to_string())));
193        assert!(!env_flag_enabled(Some("1"), Some("0".to_string())));
194    }
195
196    #[test]
197    fn test_env_str_unset_is_none() {
198        assert_eq!(env_str(None, None), None);
199    }
200
201    #[test]
202    fn test_env_str_runtime_wins_over_compile_time() {
203        assert_eq!(
204            env_str(Some("1024"), Some("2048".to_string())),
205            Some("2048".to_string())
206        );
207    }
208
209    #[test]
210    fn test_env_str_empty_runtime_falls_through_to_compile_time() {
211        assert_eq!(
212            env_str(Some("1024"), Some(String::new())),
213            Some("1024".to_string())
214        );
215    }
216
217    #[test]
218    fn test_env_str_compile_time_only() {
219        assert_eq!(env_str(Some("1024"), None), Some("1024".to_string()));
220    }
221
222    #[test]
223    fn test_env_str_runtime_only() {
224        assert_eq!(
225            env_str(None, Some("2048".to_string())),
226            Some("2048".to_string())
227        );
228    }
229
230    /// The production reader itself, on the value the process was started
231    /// with: unset in the gate, so the switch is off and shader effects run.
232    /// Reading it here also pins that the reader parses and caches without
233    /// panicking, which is what the frame path depends on.
234    #[test]
235    fn shader_effects_are_enabled_unless_the_switch_is_set() {
236        assert!(!shader_effects_disabled());
237    }
238
239    /// The switch's own parsing rules, exercised through the combinator every
240    /// `FRUST_ENGINE_*` boolean shares — including the runtime-wins
241    /// precedence, which is what lets a compile-time-disabled build be
242    /// re-enabled at launch.
243    #[test]
244    fn shader_effects_switch_parses_like_every_other_engine_flag() {
245        assert!(env_flag_enabled(None, Some("1".to_string())));
246        assert!(env_flag_enabled(None, Some("TRUE".to_string())));
247        assert!(!env_flag_enabled(None, Some("0".to_string())));
248        assert!(!env_flag_enabled(None, Some(String::new())));
249        // Runtime wins in both directions.
250        assert!(env_flag_enabled(Some("0"), Some("1".to_string())));
251        assert!(!env_flag_enabled(Some("1"), Some("0".to_string())));
252    }
253
254    #[test]
255    fn test_max_texture_size_parsing() {
256        assert_eq!(parse_max_texture_size(Some("1024".to_string())), 1024);
257        assert_eq!(parse_max_texture_size(Some("0".to_string())), 0);
258        assert_eq!(parse_max_texture_size(Some("invalid".to_string())), 0);
259        assert_eq!(parse_max_texture_size(None), 0);
260    }
261
262    #[test]
263    fn test_atlas_size_parsing() {
264        assert_eq!(
265            parse_atlas_size(Some("2048x2048".to_string())),
266            Some((2048, 2048))
267        );
268        assert_eq!(
269            parse_atlas_size(Some("1024x512".to_string())),
270            Some((1024, 512))
271        );
272        assert_eq!(parse_atlas_size(Some("2048".to_string())), None);
273        assert_eq!(parse_atlas_size(Some("2048x2048x2048".to_string())), None);
274        assert_eq!(parse_atlas_size(None), None);
275    }
276}