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