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/// Parse a default-on environment flag — a kill switch: enabled unless the
19/// resolved value is `0` or `false` (any case).
20///
21/// The same compile-time-or-runtime precedence as [`env_flag_enabled`],
22/// resolved through [`env_str`] so an empty runtime value falls through to
23/// the compile-time half rather than reading as off.
24fn env_flag_default_on(compile_time: Option<&'static str>, runtime: Option<String>) -> bool {
25    env_str(compile_time, runtime)
26        .is_none_or(|val| !(val.eq_ignore_ascii_case("0") || val.eq_ignore_ascii_case("false")))
27}
28
29/// The precedence-resolved value of a string-or-number-valued `FRUST_ENGINE_*`
30/// env knob, mirroring `frust-render::context`'s `env_str` combinator: a
31/// non-empty runtime (`std::env::var`) value wins even when a compile-time
32/// (`option_env!`) value is also set; an empty (`""`) runtime value is
33/// treated as unset and falls through to the compile-time half; `None` when
34/// neither half carries a non-empty value. This is the same
35/// compile-time-or-runtime shape `env_flag_enabled` uses for booleans, kept
36/// as its own combinator because `max_texture_size`/`atlas_size` need the
37/// raw string (to parse) rather than a bool.
38fn env_str(compile_time: Option<&'static str>, runtime: Option<String>) -> Option<String> {
39    fn non_empty(value: Option<String>) -> Option<String> {
40        value.filter(|v| !v.is_empty())
41    }
42    non_empty(runtime).or_else(|| non_empty(compile_time.map(str::to_string)))
43}
44
45/// Parses a raw `FRUST_ENGINE_MAX_TEX` value (already resolved by
46/// [`env_str`]) into the maximum texture edge length. An unset, empty, or
47/// unparsable value falls back to `0` (no limit).
48fn parse_max_texture_size(raw: Option<String>) -> u32 {
49    raw.and_then(|s| s.parse().ok()).unwrap_or(0)
50}
51
52/// Parses a raw `FRUST_ENGINE_ATLAS_SIZE` value (already resolved by
53/// [`env_str`]), e.g. `"2048x2048"`, into `(width, height)`. Returns `None`
54/// if unset or not exactly two `x`-separated integers.
55fn parse_atlas_size(raw: Option<String>) -> Option<(u32, u32)> {
56    raw.and_then(|s| {
57        let parts: Vec<&str> = s.split('x').collect();
58        if parts.len() == 2 {
59            let w = parts[0].parse::<u32>().ok()?;
60            let h = parts[1].parse::<u32>().ok()?;
61            Some((w, h))
62        } else {
63            None
64        }
65    })
66}
67
68/// Whether depth textures are disabled via `FRUST_ENGINE_NO_DEPTH`.
69/// Cached: read once per process.
70pub fn depth_disabled() -> bool {
71    static DISABLED: OnceLock<bool> = OnceLock::new();
72    *DISABLED.get_or_init(|| {
73        env_flag_enabled(
74            option_env!("FRUST_ENGINE_NO_DEPTH"),
75            std::env::var("FRUST_ENGINE_NO_DEPTH").ok(),
76        )
77    })
78}
79
80/// Whether every strip run is forced through the whole (Uber) strip program
81/// via `FRUST_ENGINE_UBER_PROGRAM`. Cached: read once per process.
82///
83/// The kill switch over paint-class specialisation
84/// ([`crate::gpu::strips::PaintClass`]): when set, no pass segment is split
85/// into class runs, no class program is warmed up or built, and every strip
86/// pass draws exactly as the engine did before the classes existed — one
87/// pipeline per pass, the whole `strip.wgsl` program (of the family
88/// [`image_sampler_enabled`] picks). The escape hatch for
89/// a driver that miscompiles a specialisation, and the off-arm of every
90/// before/after measurement of it. `EngineRenderer::set_uber_program` is its
91/// programmatic counterpart.
92pub fn uber_program_forced() -> bool {
93    static FORCED: OnceLock<bool> = OnceLock::new();
94    *FORCED.get_or_init(|| {
95        env_flag_enabled(
96            option_env!("FRUST_ENGINE_UBER_PROGRAM"),
97            std::env::var("FRUST_ENGINE_UBER_PROGRAM").ok(),
98        )
99    })
100}
101
102/// Whether the frame's surface opening stays three passes of its own via
103/// `FRUST_ENGINE_NO_PASS_MERGE`. Cached: read once per process.
104///
105/// The kill switch over pass merging: by default a frame clears its surface,
106/// draws its opaque strips and draws its first surface round's strips in ONE
107/// render pass (see `crate::renderer`'s *The frame's passes*), which on a
108/// tile-based GPU saves a whole-framebuffer load and store per pass folded
109/// away. When set, the clear and the opaque strips are recorded as passes of
110/// their own ahead of every round, and every surface round in a pass of its
111/// own — the off-arm of every before/after measurement of it, and the escape hatch for
112/// a driver that mishandles a pipeline switch inside a cleared pass.
113/// `EngineRenderer::set_pass_merge` is its programmatic counterpart.
114pub fn pass_merge_disabled() -> bool {
115    static DISABLED: OnceLock<bool> = OnceLock::new();
116    *DISABLED.get_or_init(|| {
117        env_flag_enabled(
118            option_env!("FRUST_ENGINE_NO_PASS_MERGE"),
119            std::env::var("FRUST_ENGINE_NO_PASS_MERGE").ok(),
120        )
121    })
122}
123
124/// Whether Medium-quality atlas images are filtered through the hardware
125/// sampler — on unless `FRUST_ENGINE_IMAGE_SAMPLER=0` (or `false`) says
126/// otherwise. Cached: read once per process.
127///
128/// On, the renderer compiles the image-sampling strip programs from their
129/// `IMAGE_SAMPLER` specialisation (see [`crate::gpu::shader_src`]): a bilinear
130/// atlas image is read with one filtered `textureSampleLevel` instead of four
131/// `textureLoad` taps, at the filtering precision of the hardware rather than
132/// in full `f32` — within the image tolerance the CPU-oracle gate already
133/// holds, at about three quarters of the GPU cost on a magnified image stream.
134/// The kill switch (`0`) compiles the manual family instead: no strip layout
135/// declares a sampler and every program is the four-tap one — pixel for pixel
136/// what the engine drew before the sampler existed — which is the escape hatch
137/// for a driver that filters wrongly, and the off-arm of every before/after
138/// measurement of it. `EngineRenderer::with_image_sampler` is its
139/// programmatic counterpart.
140pub fn image_sampler_enabled() -> bool {
141    static ENABLED: OnceLock<bool> = OnceLock::new();
142    *ENABLED.get_or_init(|| {
143        env_flag_default_on(
144            option_env!("FRUST_ENGINE_IMAGE_SAMPLER"),
145            std::env::var("FRUST_ENGINE_IMAGE_SAMPLER").ok(),
146        )
147    })
148}
149
150/// The shortest class run worth a pipeline switch of its own, via
151/// `FRUST_ENGINE_RUN_SPLIT_MIN=<n>`; [`crate::gpu::strips::RUN_SPLIT_MIN`]
152/// when unset or unparsable. Cached: read once per process.
153///
154/// A diagnostic knob for re-measuring the threshold on a device (a build-time
155/// `--define` reaches it through `option_env!`); see
156/// [`crate::gpu::strips::split_class_runs`] for what it decides.
157pub fn run_split_min() -> u32 {
158    static MIN: OnceLock<u32> = OnceLock::new();
159    *MIN.get_or_init(|| {
160        parse_run_split_min(env_str(
161            option_env!("FRUST_ENGINE_RUN_SPLIT_MIN"),
162            std::env::var("FRUST_ENGINE_RUN_SPLIT_MIN").ok(),
163        ))
164    })
165}
166
167/// Parses a raw `FRUST_ENGINE_RUN_SPLIT_MIN` value (already resolved by
168/// [`env_str`]); an unset or unparsable value is the measured default.
169fn parse_run_split_min(raw: Option<String>) -> u32 {
170    raw.and_then(|s| s.trim().parse().ok())
171        .unwrap_or(crate::gpu::strips::RUN_SPLIT_MIN)
172}
173
174/// Whether atlas allocation is disabled via `FRUST_ENGINE_NO_ATLAS`.
175/// Cached: read once per process.
176///
177/// Consulted by [`crate::cache::images::ImageResidency`]: when set, every
178/// image resolution answers a logged skip instead of touching the atlas.
179pub fn atlas_disabled() -> bool {
180    static DISABLED: OnceLock<bool> = OnceLock::new();
181    *DISABLED.get_or_init(|| {
182        env_flag_enabled(
183            option_env!("FRUST_ENGINE_NO_ATLAS"),
184            std::env::var("FRUST_ENGINE_NO_ATLAS").ok(),
185        )
186    })
187}
188
189/// Whether glyph quads are disabled via `FRUST_ENGINE_NO_GLYPH_QUADS`.
190/// Cached: read once per process.
191///
192/// Consulted when a [`crate::compile::SceneCompiler`] is built: with it set, a
193/// glyph run resolved entirely out of the glyph atlas is lowered the way every
194/// atlas glyph was before glyph quads existed — one image record per glyph and
195/// coverage strips generated from the glyph's rectangle — instead of one
196/// record per run and one rect instance per glyph. The kill switch for a
197/// device whose glyph quads disagree with that lowering;
198/// [`crate::compile::SceneCompiler::set_glyph_quads`] is the per-compiler
199/// counterpart a caller comparing the two arms uses.
200pub fn glyph_quads_disabled() -> bool {
201    static DISABLED: OnceLock<bool> = OnceLock::new();
202    *DISABLED.get_or_init(|| {
203        env_flag_enabled(
204            option_env!("FRUST_ENGINE_NO_GLYPH_QUADS"),
205            std::env::var("FRUST_ENGINE_NO_GLYPH_QUADS").ok(),
206        )
207    })
208}
209
210/// Whether parallel strip generation is disabled via
211/// `FRUST_ENGINE_NO_PARALLEL`. Cached: read once per process.
212///
213/// Consulted when a [`crate::compile::SceneCompiler`] is built: with it set,
214/// the compiler starts with one worker, so every command's strips are
215/// generated on the calling thread during the walk and no helper thread is
216/// ever spawned. The kill switch for a device or a host where the per-frame
217/// helper costs more than it saves;
218/// [`crate::compile::SceneCompiler::set_parallel_workers`] is the
219/// per-compiler counterpart a caller comparing the two arms uses.
220pub fn parallel_walk_disabled() -> bool {
221    static DISABLED: OnceLock<bool> = OnceLock::new();
222    *DISABLED.get_or_init(|| {
223        env_flag_enabled(
224            option_env!("FRUST_ENGINE_NO_PARALLEL"),
225            std::env::var("FRUST_ENGINE_NO_PARALLEL").ok(),
226        )
227    })
228}
229
230/// The number of threads a frame's strips are generated on, the calling
231/// thread included, via `FRUST_ENGINE_PARALLEL_WORKERS=<n>`; `None` when unset
232/// or unparsable, which leaves the compiler's own default. Cached: read once
233/// per process.
234///
235/// A diagnostic knob for re-measuring the worker count on a device (a
236/// build-time `--define` reaches it through `option_env!`).
237/// `FRUST_ENGINE_NO_PARALLEL` wins over it.
238pub fn parallel_workers() -> Option<usize> {
239    static WORKERS: OnceLock<Option<usize>> = OnceLock::new();
240    *WORKERS.get_or_init(|| {
241        parse_parallel_workers(env_str(
242            option_env!("FRUST_ENGINE_PARALLEL_WORKERS"),
243            std::env::var("FRUST_ENGINE_PARALLEL_WORKERS").ok(),
244        ))
245    })
246}
247
248/// Parses a raw `FRUST_ENGINE_PARALLEL_WORKERS` value (already resolved by
249/// [`env_str`]); zero, like an unset or unparsable value, is no override.
250fn parse_parallel_workers(raw: Option<String>) -> Option<usize> {
251    raw.and_then(|s| s.trim().parse().ok())
252        .filter(|workers: &usize| *workers > 0)
253}
254
255/// The estimated cost a frame's planned strip generation must reach before a
256/// helper thread is spawned for it, via `FRUST_ENGINE_PARALLEL_MIN_COST=<n>`;
257/// `None` when unset or unparsable, which leaves
258/// [`crate::compile::DEFAULT_PARALLEL_MIN_COST`]. Cached: read once per
259/// process.
260///
261/// A diagnostic knob: `0` hands every frame with a planned shape command to
262/// the helpers, which is how a suite of small scenes — the golden images —
263/// is run through them.
264pub fn parallel_min_cost() -> Option<u64> {
265    static MIN_COST: OnceLock<Option<u64>> = OnceLock::new();
266    *MIN_COST.get_or_init(|| {
267        parse_parallel_min_cost(env_str(
268            option_env!("FRUST_ENGINE_PARALLEL_MIN_COST"),
269            std::env::var("FRUST_ENGINE_PARALLEL_MIN_COST").ok(),
270        ))
271    })
272}
273
274/// Parses a raw `FRUST_ENGINE_PARALLEL_MIN_COST` value (already resolved by
275/// [`env_str`]). Unlike the worker count, zero is a value here — every frame
276/// with a planned command spawns — so only an unset or unparsable value is no
277/// override.
278fn parse_parallel_min_cost(raw: Option<String>) -> Option<u64> {
279    raw.and_then(|s| s.trim().parse().ok())
280}
281
282/// Whether the resident glyph-run cache is disabled via
283/// `FRUST_ENGINE_NO_GLYPH_RUN_CACHE`. Cached: read once per process.
284///
285/// Consulted when a [`crate::compile::SceneCompiler`] is built: with it set,
286/// every glyph run the atlas serves is resolved glyph by glyph through `glifo`
287/// on every frame — key, probe, LRU touch, one sink call sequence per glyph —
288/// instead of being drawn from the slots the glyph-atlas policy remembered for
289/// it on an earlier frame. The kill switch for a device whose cached runs
290/// disagree with `glifo`'s own placement;
291/// [`crate::compile::SceneCompiler::set_glyph_run_cache`] is the per-compiler
292/// counterpart a caller comparing the two arms uses.
293pub fn glyph_run_cache_disabled() -> bool {
294    static DISABLED: OnceLock<bool> = OnceLock::new();
295    *DISABLED.get_or_init(|| {
296        env_flag_enabled(
297            option_env!("FRUST_ENGINE_NO_GLYPH_RUN_CACHE"),
298            std::env::var("FRUST_ENGINE_NO_GLYPH_RUN_CACHE").ok(),
299        )
300    })
301}
302
303/// Whether offscreen fragment-shader effects are disabled via
304/// `FRUST_ENGINE_NO_SHADER_EFFECTS`. Cached: read once per process.
305///
306/// Consulted by [`crate::effects::shader_quad`] and by the compiler's
307/// [`frust_scene::Command::ShaderQuad`] lowering: when set, no user fragment
308/// program is compiled and no shader quad is drawn — the frame renders
309/// everything else and reports the skip once at warning level. The escape
310/// hatch for a driver that miscompiles a user shader, where the alternative
311/// is losing the whole application rather than one effect.
312pub fn shader_effects_disabled() -> bool {
313    static DISABLED: OnceLock<bool> = OnceLock::new();
314    *DISABLED.get_or_init(|| {
315        env_flag_enabled(
316            option_env!("FRUST_ENGINE_NO_SHADER_EFFECTS"),
317            std::env::var("FRUST_ENGINE_NO_SHADER_EFFECTS").ok(),
318        )
319    })
320}
321
322/// Whether layer caching is disabled via `FRUST_ENGINE_NO_LAYERS`.
323/// Cached: read once per process.
324///
325/// The engine-side kill switch over retained pages
326/// ([`crate::cache::retained`]): when set, the renderer's compiler records no
327/// retained bracket and lowers every `PushRetained` inline, so the store stays
328/// empty, the resident-key ledger stays empty, and a widget tree reading it
329/// never emits a `DrawRetained`. The upstream switch, `FRUST_NO_RETAINED_LAYERS`,
330/// stops the widgets emitting the commands at all; this one is for a driver
331/// that mishandles the page round itself. `SceneCompiler::set_retained_layers`
332/// is its programmatic counterpart.
333pub fn layers_disabled() -> bool {
334    static DISABLED: OnceLock<bool> = OnceLock::new();
335    *DISABLED.get_or_init(|| {
336        env_flag_enabled(
337            option_env!("FRUST_ENGINE_NO_LAYERS"),
338            std::env::var("FRUST_ENGINE_NO_LAYERS").ok(),
339        )
340    })
341}
342
343/// The retained-page budget in MiB via `FRUST_ENGINE_RETAINED_BUDGET_MB=<n>`,
344/// overriding the adapter tier's own (see
345/// [`RetainedBudget::for_caps`](crate::cache::retained::RetainedBudget::for_caps)).
346/// `None` when unset, empty or not a whole number; `Some(0)` is a budget no
347/// page fits in, which refuses every bracket and retains nothing. Cached: read
348/// once per process.
349pub fn retained_budget_mb() -> Option<u64> {
350    static BUDGET: OnceLock<Option<u64>> = OnceLock::new();
351    *BUDGET.get_or_init(|| {
352        parse_retained_budget_mb(env_str(
353            option_env!("FRUST_ENGINE_RETAINED_BUDGET_MB"),
354            std::env::var("FRUST_ENGINE_RETAINED_BUDGET_MB").ok(),
355        ))
356    })
357}
358
359/// Parses a raw `FRUST_ENGINE_RETAINED_BUDGET_MB` value (already resolved by
360/// [`env_str`]): a whole number of MiB, or `None`.
361fn parse_retained_budget_mb(raw: Option<String>) -> Option<u64> {
362    raw.and_then(|value| value.trim().parse().ok())
363}
364
365/// Whether resource pooling is disabled via `FRUST_ENGINE_NO_POOL`.
366/// Cached: read once per process.
367///
368/// Reserved: parsed and cached, consulted by nothing yet; wired when its
369/// subsystem lands.
370pub fn pool_disabled() -> bool {
371    static DISABLED: OnceLock<bool> = OnceLock::new();
372    *DISABLED.get_or_init(|| {
373        env_flag_enabled(
374            option_env!("FRUST_ENGINE_NO_POOL"),
375            std::env::var("FRUST_ENGINE_NO_POOL").ok(),
376        )
377    })
378}
379
380/// Maximum texture resolution (each axis) via `FRUST_ENGINE_MAX_TEX=<n>`.
381/// Parsed as an integer; defaults to 0 (no limit). Cached: read once per process.
382///
383/// Reserved: parsed and cached, consulted by nothing yet; wired when its
384/// subsystem lands.
385pub fn max_texture_size() -> u32 {
386    static MAX_SIZE: OnceLock<u32> = OnceLock::new();
387    *MAX_SIZE.get_or_init(|| {
388        parse_max_texture_size(env_str(
389            option_env!("FRUST_ENGINE_MAX_TEX"),
390            std::env::var("FRUST_ENGINE_MAX_TEX").ok(),
391        ))
392    })
393}
394
395/// Atlas size via `FRUST_ENGINE_ATLAS_SIZE=<WxH>` (e.g., "2048x2048").
396/// Parsed as `(width, height)` or returns `None` if not set or invalid.
397/// Cached: read once per process.
398///
399/// Consulted by [`crate::cache::images::AtlasBudget`]: when set, it overrides
400/// the capability-chosen per-layer atlas extent (runtime wins).
401pub fn atlas_size() -> Option<(u32, u32)> {
402    static ATLAS_SIZE: OnceLock<Option<(u32, u32)>> = OnceLock::new();
403    *ATLAS_SIZE.get_or_init(|| {
404        parse_atlas_size(env_str(
405            option_env!("FRUST_ENGINE_ATLAS_SIZE"),
406            std::env::var("FRUST_ENGINE_ATLAS_SIZE").ok(),
407        ))
408    })
409}
410
411/// Downlevel mode via `FRUST_ENGINE_DOWNLEVEL=1`.
412/// Cached: read once per process.
413pub fn downlevel_mode() -> bool {
414    static DOWNLEVEL: OnceLock<bool> = OnceLock::new();
415    *DOWNLEVEL.get_or_init(|| {
416        env_flag_enabled(
417            option_env!("FRUST_ENGINE_DOWNLEVEL"),
418            std::env::var("FRUST_ENGINE_DOWNLEVEL").ok(),
419        )
420    })
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    #[test]
428    fn test_env_flag_enabled() {
429        assert!(env_flag_enabled(Some("1"), None));
430        assert!(env_flag_enabled(Some("true"), None));
431        assert!(env_flag_enabled(Some("TRUE"), None));
432        assert!(!env_flag_enabled(Some("0"), None));
433        assert!(!env_flag_enabled(Some("false"), None));
434        assert!(!env_flag_enabled(None, None));
435    }
436
437    #[test]
438    fn test_runtime_overrides_compile_time() {
439        assert!(env_flag_enabled(Some("0"), Some("1".to_string())));
440        assert!(!env_flag_enabled(Some("1"), Some("0".to_string())));
441    }
442
443    #[test]
444    fn test_env_str_unset_is_none() {
445        assert_eq!(env_str(None, None), None);
446    }
447
448    #[test]
449    fn test_env_str_runtime_wins_over_compile_time() {
450        assert_eq!(
451            env_str(Some("1024"), Some("2048".to_string())),
452            Some("2048".to_string())
453        );
454    }
455
456    #[test]
457    fn test_env_str_empty_runtime_falls_through_to_compile_time() {
458        assert_eq!(
459            env_str(Some("1024"), Some(String::new())),
460            Some("1024".to_string())
461        );
462    }
463
464    #[test]
465    fn test_env_str_compile_time_only() {
466        assert_eq!(env_str(Some("1024"), None), Some("1024".to_string()));
467    }
468
469    #[test]
470    fn test_env_str_runtime_only() {
471        assert_eq!(
472            env_str(None, Some("2048".to_string())),
473            Some("2048".to_string())
474        );
475    }
476
477    /// The production reader itself, on the value the process was started
478    /// with: unset in the gate, so the switch is off and shader effects run.
479    /// Reading it here also pins that the reader parses and caches without
480    /// panicking, which is what the frame path depends on.
481    #[test]
482    fn shader_effects_are_enabled_unless_the_switch_is_set() {
483        assert!(!shader_effects_disabled());
484    }
485
486    #[test]
487    fn parallel_workers_parses_a_positive_count_only() {
488        assert_eq!(parse_parallel_workers(Some("3".into())), Some(3));
489        assert_eq!(parse_parallel_workers(Some(" 2 ".into())), Some(2));
490        assert_eq!(parse_parallel_workers(Some("0".into())), None);
491        assert_eq!(parse_parallel_workers(Some("two".into())), None);
492        assert_eq!(parse_parallel_workers(None), None);
493    }
494
495    /// Zero is the one value that differs from the worker count: it is a
496    /// threshold every planned frame reaches, not an absent override.
497    #[test]
498    fn parallel_min_cost_parses_any_count_including_zero() {
499        assert_eq!(parse_parallel_min_cost(Some("8000".into())), Some(8000));
500        assert_eq!(parse_parallel_min_cost(Some(" 12 ".into())), Some(12));
501        assert_eq!(parse_parallel_min_cost(Some("0".into())), Some(0));
502        assert_eq!(parse_parallel_min_cost(Some("-1".into())), None);
503        assert_eq!(parse_parallel_min_cost(Some("many".into())), None);
504        assert_eq!(parse_parallel_min_cost(Some(String::new())), None);
505        assert_eq!(parse_parallel_min_cost(None), None);
506    }
507
508    /// The switch's own parsing rules, exercised through the combinator every
509    /// `FRUST_ENGINE_*` boolean shares — including the runtime-wins
510    /// precedence, which is what lets a compile-time-disabled build be
511    /// re-enabled at launch.
512    #[test]
513    fn shader_effects_switch_parses_like_every_other_engine_flag() {
514        assert!(env_flag_enabled(None, Some("1".to_string())));
515        assert!(env_flag_enabled(None, Some("TRUE".to_string())));
516        assert!(!env_flag_enabled(None, Some("0".to_string())));
517        assert!(!env_flag_enabled(None, Some(String::new())));
518        // Runtime wins in both directions.
519        assert!(env_flag_enabled(Some("0"), Some("1".to_string())));
520        assert!(!env_flag_enabled(Some("1"), Some("0".to_string())));
521    }
522
523    // The `OnceLock` getters (`uber_program_forced`, `pass_merge_disabled`,
524    // `image_sampler_enabled`, `run_split_min`, `glyph_quads_disabled`,
525    // `glyph_run_cache_disabled`) are
526    // deliberately not asserted here: they read the process environment, and
527    // a machine running the off-arm captures exports exactly these knobs. The
528    // pure parsers above and below are the contract.
529
530    /// A kill switch reads the other way round from every other boolean
531    /// here: unset — or set to anything but `0`/`false` — is on.
532    #[test]
533    fn a_default_on_flag_is_off_only_when_switched_off() {
534        assert!(env_flag_default_on(None, None));
535        assert!(env_flag_default_on(None, Some("1".to_string())));
536        assert!(env_flag_default_on(None, Some(String::new())));
537        assert!(!env_flag_default_on(None, Some("0".to_string())));
538        assert!(!env_flag_default_on(None, Some("FALSE".to_string())));
539        assert!(!env_flag_default_on(Some("0"), None));
540        // Runtime wins in both directions; an empty runtime value falls
541        // through to the compile-time half.
542        assert!(env_flag_default_on(Some("0"), Some("1".to_string())));
543        assert!(!env_flag_default_on(Some("1"), Some("0".to_string())));
544        assert!(!env_flag_default_on(Some("0"), Some(String::new())));
545    }
546
547    #[test]
548    fn the_run_split_threshold_parses_or_falls_back_to_the_measured_default() {
549        use crate::gpu::strips::RUN_SPLIT_MIN;
550        assert_eq!(parse_run_split_min(Some("64".to_string())), 64);
551        assert_eq!(parse_run_split_min(Some(" 256 ".to_string())), 256);
552        assert_eq!(parse_run_split_min(Some("0".to_string())), 0);
553        assert_eq!(parse_run_split_min(Some("many".to_string())), RUN_SPLIT_MIN);
554        assert_eq!(parse_run_split_min(None), RUN_SPLIT_MIN);
555    }
556
557    #[test]
558    fn the_retained_budget_parses_a_whole_number_of_mib_only() {
559        assert_eq!(parse_retained_budget_mb(None), None);
560        assert_eq!(parse_retained_budget_mb(Some("64".into())), Some(64));
561        assert_eq!(parse_retained_budget_mb(Some(" 0 ".into())), Some(0));
562        assert_eq!(parse_retained_budget_mb(Some("1.5".into())), None);
563        assert_eq!(parse_retained_budget_mb(Some("lots".into())), None);
564    }
565
566    #[test]
567    fn test_max_texture_size_parsing() {
568        assert_eq!(parse_max_texture_size(Some("1024".to_string())), 1024);
569        assert_eq!(parse_max_texture_size(Some("0".to_string())), 0);
570        assert_eq!(parse_max_texture_size(Some("invalid".to_string())), 0);
571        assert_eq!(parse_max_texture_size(None), 0);
572    }
573
574    #[test]
575    fn test_atlas_size_parsing() {
576        assert_eq!(
577            parse_atlas_size(Some("2048x2048".to_string())),
578            Some((2048, 2048))
579        );
580        assert_eq!(
581            parse_atlas_size(Some("1024x512".to_string())),
582            Some((1024, 512))
583        );
584        assert_eq!(parse_atlas_size(Some("2048".to_string())), None);
585        assert_eq!(parse_atlas_size(Some("2048x2048x2048".to_string())), None);
586        assert_eq!(parse_atlas_size(None), None);
587    }
588}