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}