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}