concinnity-engine 0.18.69

Runtime engine for Concinnity: ECS schedule, graphics, spawn, streaming
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
//! Process-wide flags shared between the engine loop (library) and the
//! binary-only `cn debug` subsystem. Only the flags the library itself names
//! live here; the world.jsonl / shader-stage "changed" flags and the decal /
//! emitter spawn queue moved fully into the binary-only debug tree
//! (`crate::debug`), since nothing in the library references them.
//!
//!   ENABLED              "are we running under a dev-loop entry point?" Set
//!                        once by main.rs's `Commands::Debug` / `Commands::Editor`
//!                        arms before world build; read by `GraphicsSystem::init`
//!                        / `AnimationSystem` / the draw list builder to enable
//!                        disk-first shader loading + the hot-reload source
//!                        capture. `cn run` leaves it false so production keeps
//!                        the static `include_str!`-baked path with no
//!                        filesystem dependency.
//!   PENDING_ANIMATIONS   "an Animation source changed." Set by the cn debug
//!                        watcher / WS `reload-assets` handler; consumed by the
//!                        editor crate's `anim_reload::reload_clips_if_pending`,
//!                        which the debug drive calls each frame to re-import
//!                        file-backed clips. The flag lives here (in the runtime
//!                        crate) because it bridges the runtime AnimationSystem,
//!                        which reads ENABLED, and the editor-driven hot-reload.
//!   VALIDATION           "did the launch request graphics validation?" Set by
//!                        the CLI `--validation` flag (`cn run` / `cn debug`).
//!                        Tri-state: unset defers to the build profile (on for
//!                        debug, off for release). `resolve_validation` settles
//!                        the two, and `GraphicsSystem::init` enables the
//!                        DirectX / Vulkan debug layers from the result. Metal's
//!                        validation layer cannot be toggled from a running
//!                        process, so the CLI re-execs with the env var instead;
//!                        this flag does not drive Metal.
//!   QUALITY_PRESET       "did the launch force a master quality preset?" Set by
//!                        the CLI `--quality-preset` flag. Outranks the persisted
//!                        settings-menu choice at `GraphicsSystem::init` and is
//!                        never written back, so a probe / CI run can force a
//!                        preset (e.g. `ultra`, the only tier whose ceiling
//!                        permits ray-traced reflections) without touching
//!                        settings.bin. Unset leaves the persisted choice.
//!   RT_DYNAMIC           "how should the ray-tracing acceleration structure
//!                        track moving props?" Set by the CLI `--rt-dynamic`
//!                        flag; travels to the backends through
//!                        `PostSettings::rt_dynamic`. Unset resolves to `Auto`,
//!                        the shipping dirty-gated rebuild.
//!   RT_SKINNED_GEOMETRY  "may skinned meshes join the ray-tracing acceleration
//!                        structure?" Set by the CLI `--rt-skinned-geometry`
//!                        flag; travels to the backends through
//!                        `PostSettings::rt_skinned_geometry`. Unset leaves them
//!                        in, so clearing it isolates the skinned trace path.
//!   WORLD_JSONL_PATH     the world.jsonl the dev host is running. Set by the
//!                        editor's `cn debug` / `cn editor` entry once the world
//!                        path is resolved; read by `GraphicsSystem::init` (only
//!                        under ENABLED) so the Prop-transform hot-reload watcher
//!                        knows which file to subscribe to. world.jsonl discovery
//!                        is authoring I/O that lives in `concinnity-cook`, which
//!                        the runtime does not link, so the dev host resolves the
//!                        path and hands it in rather than the engine looking it
//!                        up. Left None for `cn run` and embedded preview.
//!
//! A static is the pragmatic shape here: the flags are process-wide because the
//! rendering backend is too (a single context per process owns the GPU), and
//! plumbing them through the public `App` / `run_interpreted` signatures would
//! touch far more code for the same observable behaviour.

use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};

pub use crate::gfx::quality_preset::QualityPreset;
pub use concinnity_core::render::rt_geom::RtDynamicMode;

use crate::gfx::quality_preset::{preset_at, preset_index};

static ENABLED: AtomicBool = AtomicBool::new(false);
static PENDING_ANIMATIONS: AtomicBool = AtomicBool::new(false);
// "keep the presented frame blit-readable for an exit screenshot." Set by
// `cn run --screenshot` before world build; read by `GraphicsSystem::init`.
// The dev loop's ENABLED implies capture without this flag.
static CAPTURE: AtomicBool = AtomicBool::new(false);

// Tri-state validation request: 0 = unset (use the build-profile default),
// 1 = explicitly off, 2 = explicitly on.
static VALIDATION: AtomicU8 = AtomicU8::new(0);

// Launch-forced master quality preset: 0 = unset, otherwise the preset's cycle
// index plus one.
static QUALITY_PRESET: AtomicU8 = AtomicU8::new(0);

// Launch-forced ray-tracing update mode: 0 = unset, otherwise `RT_DYNAMIC_ORDER`
// index plus one.
static RT_DYNAMIC: AtomicU8 = AtomicU8::new(0);

// The modes `RT_DYNAMIC` encodes, in encoding order.
const RT_DYNAMIC_ORDER: [RtDynamicMode; 4] = [
    RtDynamicMode::Off,
    RtDynamicMode::Auto,
    RtDynamicMode::Rebuild,
    RtDynamicMode::Tlas,
];

// Tri-state skinned-RT-geometry request: 0 = unset, 1 = excluded, 2 = included.
static RT_SKINNED_GEOMETRY: AtomicU8 = AtomicU8::new(0);

// Path to the world.jsonl the dev host is running, or None outside a dev host.
static WORLD_JSONL_PATH: Mutex<Option<String>> = Mutex::new(None);

// The harness runs a binary's tests on parallel threads, so a test that writes
// a flag races every test whose code reads one -- graphics init reads three.
// Writers take this exclusively, readers share it, so only the writers
// serialise.
#[cfg(test)]
static FLAG_ACCESS: std::sync::RwLock<()> = std::sync::RwLock::new(());

/// Mark this process as running under a dev-loop entry point. Call once
/// before world build; the library only reads the flag.
pub fn set_enabled(v: bool) {
    ENABLED.store(v, Ordering::SeqCst);
}

// True when the process is running under a dev-loop entry point that wants
// shader hot-reload. False for `cn run` and any embedded preview.
pub(crate) fn enabled() -> bool {
    ENABLED.load(Ordering::SeqCst)
}

// Arm frame capture for a production run that wants an exit screenshot.
// Called by `start_runtime` before world build when a screenshot path was
// requested.
pub(crate) fn set_capture(v: bool) {
    CAPTURE.store(v, Ordering::SeqCst);
}

// True when a production run armed frame capture (`cn run --screenshot`).
pub(crate) fn capture() -> bool {
    CAPTURE.load(Ordering::SeqCst)
}

/// Raise the "Animation source changed" flag. Called by the asset hot-reload
/// watcher and the WS `reload-assets` handler; the library only reads it.
pub fn set_pending_animations() {
    PENDING_ANIMATIONS.store(true, Ordering::SeqCst);
}

/// Swap the "Animation source changed" flag to `false`, returning whether it
/// was set. The editor crate's `anim_reload::reload_clips_if_pending` calls
/// this; a `true` result kicks the per-clip re-import pass.
pub fn take_pending_animations() -> bool {
    PENDING_ANIMATIONS.swap(false, Ordering::SeqCst)
}

/// Record the CLI `--validation` request. `None` leaves the build-profile
/// default in effect; `Some` forces validation on or off. The library only
/// reads it.
pub fn set_validation(v: Option<bool>) {
    let encoded = match v {
        None => 0,
        Some(false) => 1,
        Some(true) => 2,
    };
    VALIDATION.store(encoded, Ordering::SeqCst);
}

// The CLI validation request, or `None` when the launch did not specify one.
pub(crate) fn validation() -> Option<bool> {
    match VALIDATION.load(Ordering::SeqCst) {
        1 => Some(false),
        2 => Some(true),
        _ => None,
    }
}

// Settle the graphics-validation request: the CLI `--validation` flag if the
// launch passed one, otherwise the build profile. Running a debug layer is a
// launch concern, so no world can ask for it.
pub(crate) fn resolve_validation() -> bool {
    validation().unwrap_or(cfg!(debug_assertions))
}

/// Record the CLI `--quality-preset` request. `None` leaves the persisted
/// settings-menu choice in effect. The library only reads it.
pub fn set_quality_preset(preset: Option<QualityPreset>) {
    let encoded = preset.map_or(0, |p| preset_index(p) as u8 + 1);
    QUALITY_PRESET.store(encoded, Ordering::SeqCst);
}

// The CLI quality-preset request, or `None` when the launch did not force one.
pub(crate) fn quality_preset() -> Option<QualityPreset> {
    match QUALITY_PRESET.load(Ordering::SeqCst) {
        0 => None,
        n => Some(preset_at(n as usize - 1)),
    }
}

// Settle the master quality preset: the CLI `--quality-preset` flag if the
// launch passed one, otherwise the persisted settings-menu choice. `None` means
// neither exists, which is a first launch the caller seeds.
pub(crate) fn resolve_quality_preset(persisted: Option<QualityPreset>) -> Option<QualityPreset> {
    quality_preset().or(persisted)
}

/// Record the CLI `--rt-dynamic` request. `None` leaves the default `Auto`
/// update mode in effect. The library only reads it.
pub fn set_rt_dynamic(mode: Option<RtDynamicMode>) {
    let encoded = mode.map_or(0, |m| {
        RT_DYNAMIC_ORDER
            .iter()
            .position(|&candidate| candidate == m)
            .expect("RT_DYNAMIC_ORDER covers every mode") as u8
            + 1
    });
    RT_DYNAMIC.store(encoded, Ordering::SeqCst);
}

// The CLI ray-tracing update-mode request, or `None` when the launch passed one.
pub(crate) fn rt_dynamic() -> Option<RtDynamicMode> {
    RT_DYNAMIC_ORDER
        .get(RT_DYNAMIC.load(Ordering::SeqCst).wrapping_sub(1) as usize)
        .copied()
}

// Settle how the acceleration structure tracks moving props: the CLI
// `--rt-dynamic` flag if the launch passed one, otherwise `Auto`.
pub(crate) fn resolve_rt_dynamic() -> RtDynamicMode {
    rt_dynamic().unwrap_or_default()
}

/// Record the CLI `--rt-skinned-geometry` request. `None` leaves skinned meshes
/// in the acceleration structure. The library only reads it.
pub fn set_rt_skinned_geometry(v: Option<bool>) {
    let encoded = match v {
        None => 0,
        Some(false) => 1,
        Some(true) => 2,
    };
    RT_SKINNED_GEOMETRY.store(encoded, Ordering::SeqCst);
}

// The CLI skinned-RT-geometry request, or `None` when the launch did not pass one.
pub(crate) fn rt_skinned_geometry() -> Option<bool> {
    match RT_SKINNED_GEOMETRY.load(Ordering::SeqCst) {
        1 => Some(false),
        2 => Some(true),
        _ => None,
    }
}

// Settle whether skinned meshes join the acceleration structure: the CLI
// `--rt-skinned-geometry` flag if the launch passed one, otherwise in.
pub(crate) fn resolve_rt_skinned_geometry() -> bool {
    rt_skinned_geometry().unwrap_or(true)
}

/// Record the world.jsonl path the dev host resolved, so the hot-reload watcher
/// can subscribe to it. Called by the editor's `cn debug` / `cn editor` entry
/// before world build; the library only reads it.
pub fn set_world_jsonl_path(path: Option<String>) {
    *WORLD_JSONL_PATH.lock().unwrap() = path;
}

// The world.jsonl path the dev host handed in, or None outside a dev host. Read
// by `GraphicsSystem::init` to seed the Prop-transform reload watcher.
pub(crate) fn world_jsonl_path() -> Option<String> {
    WORLD_JSONL_PATH.lock().unwrap().clone()
}

// Shared flag access for a test whose code path reads a flag. Held for as long
// as the read matters: for graphics init, across the whole `run_init`.
#[cfg(test)]
pub(crate) fn read_access() -> std::sync::RwLockReadGuard<'static, ()> {
    FLAG_ACCESS.read().unwrap_or_else(|e| e.into_inner())
}

// Exclusive flag access for a test that writes one. Restores every flag graphics
// init reads when it drops, so a panicking test cannot leak one into the rest of
// the binary. Poison is ignored: the test holding it has already failed, and
// erroring every later lock buries that failure under a cascade. Not reentrant,
// so a test holding this must not also take `read_access`.
#[cfg(test)]
pub(crate) struct WriteAccess {
    _guard: std::sync::RwLockWriteGuard<'static, ()>,
    enabled: bool,
    validation: Option<bool>,
    quality_preset: Option<QualityPreset>,
    rt_dynamic: Option<RtDynamicMode>,
    rt_skinned_geometry: Option<bool>,
    world_jsonl_path: Option<String>,
}

#[cfg(test)]
pub(crate) fn write_access() -> WriteAccess {
    WriteAccess {
        _guard: FLAG_ACCESS.write().unwrap_or_else(|e| e.into_inner()),
        enabled: enabled(),
        validation: validation(),
        quality_preset: quality_preset(),
        rt_dynamic: rt_dynamic(),
        rt_skinned_geometry: rt_skinned_geometry(),
        world_jsonl_path: world_jsonl_path(),
    }
}

#[cfg(test)]
impl Drop for WriteAccess {
    fn drop(&mut self) {
        set_enabled(self.enabled);
        set_validation(self.validation);
        set_quality_preset(self.quality_preset);
        set_rt_dynamic(self.rt_dynamic);
        set_rt_skinned_geometry(self.rt_skinned_geometry);
        set_world_jsonl_path(self.world_jsonl_path.take());
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn defaults_off_and_round_trips() {
        let _flags = write_access();
        set_enabled(false);
        assert!(!enabled());
        set_enabled(true);
        assert!(enabled());
    }

    #[test]
    fn validation_tristate_round_trips() {
        let _flags = write_access();
        set_validation(None);
        assert_eq!(validation(), None);
        set_validation(Some(true));
        assert_eq!(validation(), Some(true));
        set_validation(Some(false));
        assert_eq!(validation(), Some(false));
    }

    #[test]
    fn every_quality_preset_round_trips_through_the_flag() {
        let _flags = write_access();
        set_quality_preset(None);
        assert_eq!(quality_preset(), None);
        for preset in QualityPreset::ALL {
            set_quality_preset(Some(preset));
            assert_eq!(quality_preset(), Some(preset));
        }
    }

    #[test]
    fn the_quality_flag_outranks_the_persisted_choice() {
        let _flags = write_access();

        // No flag: the persisted settings-menu choice decides, unchanged.
        set_quality_preset(None);
        assert_eq!(resolve_quality_preset(None), None);
        assert_eq!(
            resolve_quality_preset(Some(QualityPreset::Auto)),
            Some(QualityPreset::Auto)
        );

        // The flag wins over any persisted value, and over none.
        set_quality_preset(Some(QualityPreset::Ultra));
        assert_eq!(
            resolve_quality_preset(Some(QualityPreset::Auto)),
            Some(QualityPreset::Ultra)
        );
        assert_eq!(resolve_quality_preset(None), Some(QualityPreset::Ultra));
    }

    #[test]
    fn every_rt_dynamic_mode_round_trips_and_unset_is_auto() {
        let _flags = write_access();
        set_rt_dynamic(None);
        assert_eq!(rt_dynamic(), None);
        assert_eq!(resolve_rt_dynamic(), RtDynamicMode::Auto);
        for mode in RT_DYNAMIC_ORDER {
            set_rt_dynamic(Some(mode));
            assert_eq!(rt_dynamic(), Some(mode));
            assert_eq!(resolve_rt_dynamic(), mode);
        }
    }

    #[test]
    fn skinned_rt_geometry_is_in_unless_the_flag_clears_it() {
        let _flags = write_access();
        set_rt_skinned_geometry(None);
        assert_eq!(rt_skinned_geometry(), None);
        assert!(resolve_rt_skinned_geometry());
        set_rt_skinned_geometry(Some(true));
        assert!(resolve_rt_skinned_geometry());
        set_rt_skinned_geometry(Some(false));
        assert!(!resolve_rt_skinned_geometry());
    }

    #[test]
    fn the_launch_flag_outranks_the_build_profile() {
        let _flags = write_access();

        // No flag: the build profile decides.
        set_validation(None);
        assert_eq!(resolve_validation(), cfg!(debug_assertions));

        // An explicit flag decides instead, either way.
        set_validation(Some(false));
        assert!(!resolve_validation());
        set_validation(Some(true));
        assert!(resolve_validation());
    }
}