concinnity_engine/app/dev_flags.rs
1//! Process-wide flags shared between the engine loop (library) and the
2//! binary-only `cn debug` subsystem. Only the flags the library itself names
3//! live here; the world.jsonl / shader-stage "changed" flags and the decal /
4//! emitter spawn queue moved fully into the binary-only debug tree
5//! (`crate::debug`), since nothing in the library references them.
6//!
7//! ENABLED "are we running under a dev-loop entry point?" Set
8//! once by main.rs's `Commands::Debug` / `Commands::Editor`
9//! arms before world build; read by `GraphicsSystem::init`
10//! / `AnimationSystem` / the draw list builder to enable
11//! disk-first shader loading + the hot-reload source
12//! capture. `cn run` leaves it false so production keeps
13//! the static `include_str!`-baked path with no
14//! filesystem dependency.
15//! PENDING_ANIMATIONS "an Animation source changed." Set by the cn debug
16//! watcher / WS `reload-assets` handler; consumed by the
17//! editor crate's `anim_reload::reload_clips_if_pending`,
18//! which the debug drive calls each frame to re-import
19//! file-backed clips. The flag lives here (in the runtime
20//! crate) because it bridges the runtime AnimationSystem,
21//! which reads ENABLED, and the editor-driven hot-reload.
22//! VALIDATION "did the launch request graphics validation?" Set by
23//! the CLI `--validation` flag (`cn run` / `cn debug`).
24//! Tri-state: unset defers to the build profile (on for
25//! debug, off for release). `resolve_validation` settles
26//! the two, and `GraphicsSystem::init` enables the
27//! DirectX / Vulkan debug layers from the result. Metal's
28//! validation layer cannot be toggled from a running
29//! process, so the CLI re-execs with the env var instead;
30//! this flag does not drive Metal.
31//! QUALITY_PRESET "did the launch force a master quality preset?" Set by
32//! the CLI `--quality-preset` flag. Outranks the persisted
33//! settings-menu choice at `GraphicsSystem::init` and is
34//! never written back, so a probe / CI run can force a
35//! preset (e.g. `ultra`, the only tier whose ceiling
36//! permits ray-traced reflections) without touching
37//! settings.bin. Unset leaves the persisted choice.
38//! RT_DYNAMIC "how should the ray-tracing acceleration structure
39//! track moving props?" Set by the CLI `--rt-dynamic`
40//! flag; travels to the backends through
41//! `PostSettings::rt_dynamic`. Unset resolves to `Auto`,
42//! the shipping dirty-gated rebuild.
43//! RT_SKINNED_GEOMETRY "may skinned meshes join the ray-tracing acceleration
44//! structure?" Set by the CLI `--rt-skinned-geometry`
45//! flag; travels to the backends through
46//! `PostSettings::rt_skinned_geometry`. Unset leaves them
47//! in, so clearing it isolates the skinned trace path.
48//! WORLD_JSONL_PATH the world.jsonl the dev host is running. Set by the
49//! editor's `cn debug` / `cn editor` entry once the world
50//! path is resolved; read by `GraphicsSystem::init` (only
51//! under ENABLED) so the Prop-transform hot-reload watcher
52//! knows which file to subscribe to. world.jsonl discovery
53//! is authoring I/O that lives in `concinnity-cook`, which
54//! the runtime does not link, so the dev host resolves the
55//! path and hands it in rather than the engine looking it
56//! up. Left None for `cn run` and embedded preview.
57//!
58//! A static is the pragmatic shape here: the flags are process-wide because the
59//! rendering backend is too (a single context per process owns the GPU), and
60//! plumbing them through the public `App` / `run_interpreted` signatures would
61//! touch far more code for the same observable behaviour.
62
63use std::sync::Mutex;
64use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
65
66pub use crate::gfx::quality_preset::QualityPreset;
67pub use concinnity_core::render::rt_geom::RtDynamicMode;
68
69use crate::gfx::quality_preset::{preset_at, preset_index};
70
71static ENABLED: AtomicBool = AtomicBool::new(false);
72static PENDING_ANIMATIONS: AtomicBool = AtomicBool::new(false);
73// "keep the presented frame blit-readable for an exit screenshot." Set by
74// `cn run --screenshot` before world build; read by `GraphicsSystem::init`.
75// The dev loop's ENABLED implies capture without this flag.
76static CAPTURE: AtomicBool = AtomicBool::new(false);
77
78// Tri-state validation request: 0 = unset (use the build-profile default),
79// 1 = explicitly off, 2 = explicitly on.
80static VALIDATION: AtomicU8 = AtomicU8::new(0);
81
82// Launch-forced master quality preset: 0 = unset, otherwise the preset's cycle
83// index plus one.
84static QUALITY_PRESET: AtomicU8 = AtomicU8::new(0);
85
86// Launch-forced ray-tracing update mode: 0 = unset, otherwise `RT_DYNAMIC_ORDER`
87// index plus one.
88static RT_DYNAMIC: AtomicU8 = AtomicU8::new(0);
89
90// The modes `RT_DYNAMIC` encodes, in encoding order.
91const RT_DYNAMIC_ORDER: [RtDynamicMode; 4] = [
92 RtDynamicMode::Off,
93 RtDynamicMode::Auto,
94 RtDynamicMode::Rebuild,
95 RtDynamicMode::Tlas,
96];
97
98// Tri-state skinned-RT-geometry request: 0 = unset, 1 = excluded, 2 = included.
99static RT_SKINNED_GEOMETRY: AtomicU8 = AtomicU8::new(0);
100
101// Path to the world.jsonl the dev host is running, or None outside a dev host.
102static WORLD_JSONL_PATH: Mutex<Option<String>> = Mutex::new(None);
103
104// The harness runs a binary's tests on parallel threads, so a test that writes
105// a flag races every test whose code reads one -- graphics init reads three.
106// Writers take this exclusively, readers share it, so only the writers
107// serialise.
108#[cfg(test)]
109static FLAG_ACCESS: std::sync::RwLock<()> = std::sync::RwLock::new(());
110
111/// Mark this process as running under a dev-loop entry point. Call once
112/// before world build; the library only reads the flag.
113pub fn set_enabled(v: bool) {
114 ENABLED.store(v, Ordering::SeqCst);
115}
116
117// True when the process is running under a dev-loop entry point that wants
118// shader hot-reload. False for `cn run` and any embedded preview.
119pub(crate) fn enabled() -> bool {
120 ENABLED.load(Ordering::SeqCst)
121}
122
123// Arm frame capture for a production run that wants an exit screenshot.
124// Called by `start_runtime` before world build when a screenshot path was
125// requested.
126pub(crate) fn set_capture(v: bool) {
127 CAPTURE.store(v, Ordering::SeqCst);
128}
129
130// True when a production run armed frame capture (`cn run --screenshot`).
131pub(crate) fn capture() -> bool {
132 CAPTURE.load(Ordering::SeqCst)
133}
134
135/// Raise the "Animation source changed" flag. Called by the asset hot-reload
136/// watcher and the WS `reload-assets` handler; the library only reads it.
137pub fn set_pending_animations() {
138 PENDING_ANIMATIONS.store(true, Ordering::SeqCst);
139}
140
141/// Swap the "Animation source changed" flag to `false`, returning whether it
142/// was set. The editor crate's `anim_reload::reload_clips_if_pending` calls
143/// this; a `true` result kicks the per-clip re-import pass.
144pub fn take_pending_animations() -> bool {
145 PENDING_ANIMATIONS.swap(false, Ordering::SeqCst)
146}
147
148/// Record the CLI `--validation` request. `None` leaves the build-profile
149/// default in effect; `Some` forces validation on or off. The library only
150/// reads it.
151pub fn set_validation(v: Option<bool>) {
152 let encoded = match v {
153 None => 0,
154 Some(false) => 1,
155 Some(true) => 2,
156 };
157 VALIDATION.store(encoded, Ordering::SeqCst);
158}
159
160// The CLI validation request, or `None` when the launch did not specify one.
161pub(crate) fn validation() -> Option<bool> {
162 match VALIDATION.load(Ordering::SeqCst) {
163 1 => Some(false),
164 2 => Some(true),
165 _ => None,
166 }
167}
168
169// Settle the graphics-validation request: the CLI `--validation` flag if the
170// launch passed one, otherwise the build profile. Running a debug layer is a
171// launch concern, so no world can ask for it.
172pub(crate) fn resolve_validation() -> bool {
173 validation().unwrap_or(cfg!(debug_assertions))
174}
175
176/// Record the CLI `--quality-preset` request. `None` leaves the persisted
177/// settings-menu choice in effect. The library only reads it.
178pub fn set_quality_preset(preset: Option<QualityPreset>) {
179 let encoded = preset.map_or(0, |p| preset_index(p) as u8 + 1);
180 QUALITY_PRESET.store(encoded, Ordering::SeqCst);
181}
182
183// The CLI quality-preset request, or `None` when the launch did not force one.
184pub(crate) fn quality_preset() -> Option<QualityPreset> {
185 match QUALITY_PRESET.load(Ordering::SeqCst) {
186 0 => None,
187 n => Some(preset_at(n as usize - 1)),
188 }
189}
190
191// Settle the master quality preset: the CLI `--quality-preset` flag if the
192// launch passed one, otherwise the persisted settings-menu choice. `None` means
193// neither exists, which is a first launch the caller seeds.
194pub(crate) fn resolve_quality_preset(persisted: Option<QualityPreset>) -> Option<QualityPreset> {
195 quality_preset().or(persisted)
196}
197
198/// Record the CLI `--rt-dynamic` request. `None` leaves the default `Auto`
199/// update mode in effect. The library only reads it.
200pub fn set_rt_dynamic(mode: Option<RtDynamicMode>) {
201 let encoded = mode.map_or(0, |m| {
202 RT_DYNAMIC_ORDER
203 .iter()
204 .position(|&candidate| candidate == m)
205 .expect("RT_DYNAMIC_ORDER covers every mode") as u8
206 + 1
207 });
208 RT_DYNAMIC.store(encoded, Ordering::SeqCst);
209}
210
211// The CLI ray-tracing update-mode request, or `None` when the launch passed one.
212pub(crate) fn rt_dynamic() -> Option<RtDynamicMode> {
213 RT_DYNAMIC_ORDER
214 .get(RT_DYNAMIC.load(Ordering::SeqCst).wrapping_sub(1) as usize)
215 .copied()
216}
217
218// Settle how the acceleration structure tracks moving props: the CLI
219// `--rt-dynamic` flag if the launch passed one, otherwise `Auto`.
220pub(crate) fn resolve_rt_dynamic() -> RtDynamicMode {
221 rt_dynamic().unwrap_or_default()
222}
223
224/// Record the CLI `--rt-skinned-geometry` request. `None` leaves skinned meshes
225/// in the acceleration structure. The library only reads it.
226pub fn set_rt_skinned_geometry(v: Option<bool>) {
227 let encoded = match v {
228 None => 0,
229 Some(false) => 1,
230 Some(true) => 2,
231 };
232 RT_SKINNED_GEOMETRY.store(encoded, Ordering::SeqCst);
233}
234
235// The CLI skinned-RT-geometry request, or `None` when the launch did not pass one.
236pub(crate) fn rt_skinned_geometry() -> Option<bool> {
237 match RT_SKINNED_GEOMETRY.load(Ordering::SeqCst) {
238 1 => Some(false),
239 2 => Some(true),
240 _ => None,
241 }
242}
243
244// Settle whether skinned meshes join the acceleration structure: the CLI
245// `--rt-skinned-geometry` flag if the launch passed one, otherwise in.
246pub(crate) fn resolve_rt_skinned_geometry() -> bool {
247 rt_skinned_geometry().unwrap_or(true)
248}
249
250/// Record the world.jsonl path the dev host resolved, so the hot-reload watcher
251/// can subscribe to it. Called by the editor's `cn debug` / `cn editor` entry
252/// before world build; the library only reads it.
253pub fn set_world_jsonl_path(path: Option<String>) {
254 *WORLD_JSONL_PATH.lock().unwrap() = path;
255}
256
257// The world.jsonl path the dev host handed in, or None outside a dev host. Read
258// by `GraphicsSystem::init` to seed the Prop-transform reload watcher.
259pub(crate) fn world_jsonl_path() -> Option<String> {
260 WORLD_JSONL_PATH.lock().unwrap().clone()
261}
262
263// Shared flag access for a test whose code path reads a flag. Held for as long
264// as the read matters: for graphics init, across the whole `run_init`.
265#[cfg(test)]
266pub(crate) fn read_access() -> std::sync::RwLockReadGuard<'static, ()> {
267 FLAG_ACCESS.read().unwrap_or_else(|e| e.into_inner())
268}
269
270// Exclusive flag access for a test that writes one. Restores every flag graphics
271// init reads when it drops, so a panicking test cannot leak one into the rest of
272// the binary. Poison is ignored: the test holding it has already failed, and
273// erroring every later lock buries that failure under a cascade. Not reentrant,
274// so a test holding this must not also take `read_access`.
275#[cfg(test)]
276pub(crate) struct WriteAccess {
277 _guard: std::sync::RwLockWriteGuard<'static, ()>,
278 enabled: bool,
279 validation: Option<bool>,
280 quality_preset: Option<QualityPreset>,
281 rt_dynamic: Option<RtDynamicMode>,
282 rt_skinned_geometry: Option<bool>,
283 world_jsonl_path: Option<String>,
284}
285
286#[cfg(test)]
287pub(crate) fn write_access() -> WriteAccess {
288 WriteAccess {
289 _guard: FLAG_ACCESS.write().unwrap_or_else(|e| e.into_inner()),
290 enabled: enabled(),
291 validation: validation(),
292 quality_preset: quality_preset(),
293 rt_dynamic: rt_dynamic(),
294 rt_skinned_geometry: rt_skinned_geometry(),
295 world_jsonl_path: world_jsonl_path(),
296 }
297}
298
299#[cfg(test)]
300impl Drop for WriteAccess {
301 fn drop(&mut self) {
302 set_enabled(self.enabled);
303 set_validation(self.validation);
304 set_quality_preset(self.quality_preset);
305 set_rt_dynamic(self.rt_dynamic);
306 set_rt_skinned_geometry(self.rt_skinned_geometry);
307 set_world_jsonl_path(self.world_jsonl_path.take());
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn defaults_off_and_round_trips() {
317 let _flags = write_access();
318 set_enabled(false);
319 assert!(!enabled());
320 set_enabled(true);
321 assert!(enabled());
322 }
323
324 #[test]
325 fn validation_tristate_round_trips() {
326 let _flags = write_access();
327 set_validation(None);
328 assert_eq!(validation(), None);
329 set_validation(Some(true));
330 assert_eq!(validation(), Some(true));
331 set_validation(Some(false));
332 assert_eq!(validation(), Some(false));
333 }
334
335 #[test]
336 fn every_quality_preset_round_trips_through_the_flag() {
337 let _flags = write_access();
338 set_quality_preset(None);
339 assert_eq!(quality_preset(), None);
340 for preset in QualityPreset::ALL {
341 set_quality_preset(Some(preset));
342 assert_eq!(quality_preset(), Some(preset));
343 }
344 }
345
346 #[test]
347 fn the_quality_flag_outranks_the_persisted_choice() {
348 let _flags = write_access();
349
350 // No flag: the persisted settings-menu choice decides, unchanged.
351 set_quality_preset(None);
352 assert_eq!(resolve_quality_preset(None), None);
353 assert_eq!(
354 resolve_quality_preset(Some(QualityPreset::Auto)),
355 Some(QualityPreset::Auto)
356 );
357
358 // The flag wins over any persisted value, and over none.
359 set_quality_preset(Some(QualityPreset::Ultra));
360 assert_eq!(
361 resolve_quality_preset(Some(QualityPreset::Auto)),
362 Some(QualityPreset::Ultra)
363 );
364 assert_eq!(resolve_quality_preset(None), Some(QualityPreset::Ultra));
365 }
366
367 #[test]
368 fn every_rt_dynamic_mode_round_trips_and_unset_is_auto() {
369 let _flags = write_access();
370 set_rt_dynamic(None);
371 assert_eq!(rt_dynamic(), None);
372 assert_eq!(resolve_rt_dynamic(), RtDynamicMode::Auto);
373 for mode in RT_DYNAMIC_ORDER {
374 set_rt_dynamic(Some(mode));
375 assert_eq!(rt_dynamic(), Some(mode));
376 assert_eq!(resolve_rt_dynamic(), mode);
377 }
378 }
379
380 #[test]
381 fn skinned_rt_geometry_is_in_unless_the_flag_clears_it() {
382 let _flags = write_access();
383 set_rt_skinned_geometry(None);
384 assert_eq!(rt_skinned_geometry(), None);
385 assert!(resolve_rt_skinned_geometry());
386 set_rt_skinned_geometry(Some(true));
387 assert!(resolve_rt_skinned_geometry());
388 set_rt_skinned_geometry(Some(false));
389 assert!(!resolve_rt_skinned_geometry());
390 }
391
392 #[test]
393 fn the_launch_flag_outranks_the_build_profile() {
394 let _flags = write_access();
395
396 // No flag: the build profile decides.
397 set_validation(None);
398 assert_eq!(resolve_validation(), cfg!(debug_assertions));
399
400 // An explicit flag decides instead, either way.
401 set_validation(Some(false));
402 assert!(!resolve_validation());
403 set_validation(Some(true));
404 assert!(resolve_validation());
405 }
406}