Skip to main content

flight_debug/
debug.rs

1//! The debug umbrella (TS `debug.ts`): a registry of diagnostic subsystems and one debug session
2//! over a caller-owned log state. Enabling installs a dev sink, raises the global and per-channel log
3//! levels, and runs each selected subsystem's guard enabler; disabling undoes exactly that. The
4//! registry and session live in a caller-owned `DebugState` (D5), and no lock is held while a
5//! subsystem's guard binding runs.
6
7use flight_entity::allocate_entity;
8use flight_log::{
9    LogLevel, LogSink, LogStateRef, add_log_sink, clear_log_channel_level, create_console_log_sink,
10    get_log_channel_level, get_log_level, remove_log_sink, set_log_channel_level, set_log_level,
11};
12use flight_render::{RenderState, enable_color_adjustment_guards, enable_render_registries_guards};
13use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
14
15use crate::types::*;
16
17/// Creates an empty debug state: no registered subsystems and no active session (D5: the source
18/// module's globals, owned by the caller).
19// flight-lint: allow(conform-extra) D5: the caller-owned state replaces the source module globals
20pub fn create_debug_state() -> DebugState {
21    let mut out: DebugState = allocate_entity();
22    initialize_debug_state(&mut out);
23    return out;
24}
25
26/// Resets a debug state to the source module's initial values (the construction initializer of
27/// `create_debug_state`).
28// flight-lint: allow(conform-extra) D5: the construction initializer of the caller-owned state
29pub fn initialize_debug_state(out: &mut DebugState) {
30    out.enabled = false;
31    out.enabled_subsystems = Vec::new();
32    out.frame_number = 0;
33    out.installed_sink = None;
34    out.saved_channel_levels = Vec::new();
35    out.saved_global_level = LogLevel::Verbose;
36    out.subsystems = Vec::new();
37}
38
39// Raises the global level and every selected channel's level to `level` for the session.
40fn apply_debug_levels(log_state: &LogStateRef, level: LogLevel, channels: &[String]) {
41    set_log_level(log_state, level);
42    for channel in channels {
43        set_log_channel_level(log_state, channel, level);
44    }
45}
46
47// Gathers the channels to raise: every selected subsystem's channels plus any explicit extras.
48fn collect_debug_channels(
49    subsystems: &[DebugSubsystemHooks],
50    extra: Option<&[String]>,
51) -> Vec<String> {
52    let mut channels = Vec::new();
53    for hooks in subsystems {
54        if let Some(own) = &hooks.channels {
55            channels.extend(own.iter().cloned());
56        }
57    }
58    if let Some(extra) = extra {
59        channels.extend(extra.iter().cloned());
60    }
61    return channels;
62}
63
64/// Returns Flight to the zero-debug baseline (TS `disableDebug`): runs the disable-guards binding of
65/// every subsystem the session switched on, removes the sink it installed, and restores the log
66/// levels it raised. A no-op when debug is not enabled.
67// flight-lint: allow(conform-params) D5: caller-owned debug and log state replace the source module globals
68pub fn disable_debug(state: &DebugStateRef, log_state: &LogStateRef) {
69    if !state.read().unwrap().enabled {
70        return;
71    }
72    let enabled = state.read().unwrap().enabled_subsystems.clone();
73    for hooks in &enabled {
74        if let Some(disable_guards) = &hooks.disable_guards {
75            disable_guards();
76        }
77    }
78    state.write().unwrap().enabled_subsystems.clear();
79    remove_debug_sink(state, log_state);
80    restore_debug_levels(state, log_state);
81    state.write().unwrap().enabled = false;
82}
83
84/// Enables a debug session (TS `enableDebug`). It installs `options.sink`, raises the global level
85/// (default `LogLevel::Debug`) and the channels of the selected subsystems plus `options.channels`,
86/// then runs each selected subsystem's guard enabler in order. `options.subsystems` selects
87/// registered subsystems by name (unregistered names are skipped); omitted, every registered
88/// subsystem is switched on. A second call while enabled is a no-op.
89///
90/// A guard enabler that panics (the source's throw) unwinds the whole transaction: every subsystem
91/// already switched on, the failing one included, runs its disabler newest first, the sink is
92/// removed and every level is restored, each step continuing past its own failure, and then the
93/// original panic resumes. An omitted `options.sink` installs a console sink with the level-prefixed
94/// text formatter (`create_console_log_sink`), as the source does.
95// flight-lint: allow(conform-params) D5: caller-owned debug and log state replace the source module globals
96pub fn enable_debug(
97    state: &DebugStateRef,
98    log_state: &LogStateRef,
99    options: Option<&DebugOptions>,
100) {
101    if state.read().unwrap().enabled {
102        return;
103    }
104    let level = options
105        .and_then(|options| options.level)
106        .unwrap_or(LogLevel::Debug);
107    let subsystems = resolve_debug_subsystems(
108        state,
109        options.and_then(|options| options.subsystems.as_deref()),
110    );
111    let channels = collect_debug_channels(
112        &subsystems,
113        options.and_then(|options| options.channels.as_deref()),
114    );
115    let saved_global_level = get_log_level(log_state);
116    state.write().unwrap().saved_global_level = saved_global_level;
117    let mut saved_channel_levels: Vec<(String, Option<LogLevel>)> = Vec::new();
118    for channel in &channels {
119        if !saved_channel_levels
120            .iter()
121            .any(|(saved, _)| saved == channel)
122        {
123            saved_channel_levels.push((channel.clone(), get_log_channel_level(log_state, channel)));
124        }
125    }
126    let sink = options
127        .and_then(|options| options.sink.clone())
128        .unwrap_or_else(|| create_console_log_sink(log_state, None));
129    let enabled = catch_unwind(AssertUnwindSafe(|| {
130        apply_debug_levels(log_state, level, &channels);
131        install_debug_sink(state, log_state, &sink);
132        for hooks in &subsystems {
133            state
134                .write()
135                .unwrap()
136                .enabled_subsystems
137                .push(hooks.clone());
138            if let Some(enable_guards) = &hooks.enable_guards {
139                enable_guards();
140            }
141        }
142    }));
143    if let Err(error) = enabled {
144        let switched_on = std::mem::take(&mut state.write().unwrap().enabled_subsystems);
145        for hooks in switched_on.iter().rev() {
146            if let Some(disable_guards) = &hooks.disable_guards {
147                // Keep unwinding; the original enable failure owns this transaction.
148                let _ = catch_unwind(AssertUnwindSafe(|| disable_guards()));
149            }
150        }
151        let _ = catch_unwind(AssertUnwindSafe(|| remove_debug_sink(state, log_state)));
152        for (channel, saved_level) in &saved_channel_levels {
153            let _ = catch_unwind(AssertUnwindSafe(|| match saved_level {
154                None => clear_log_channel_level(log_state, channel),
155                Some(saved_level) => set_log_channel_level(log_state, channel, *saved_level),
156            }));
157        }
158        let _ = catch_unwind(AssertUnwindSafe(|| {
159            set_log_level(log_state, saved_global_level)
160        }));
161        resume_unwind(error);
162    }
163    let mut current = state.write().unwrap();
164    current.saved_channel_levels = saved_channel_levels;
165    current.enabled = true;
166}
167
168/// Enables a debug session with the default options, then the generic render guards for one render
169/// state (TS `enableFlightDiagnostics`), reporting through the same caller-owned log state.
170// flight-lint: allow(conform-params, contract-vocabulary) D5: caller-owned debug and log state replace the source module globals; the RenderState bound is flight-render's own guard-enabler signature (TS `state: RenderState`)
171pub fn enable_flight_diagnostics<S: RenderState>(
172    debug_state: &DebugStateRef,
173    log_state: &LogStateRef,
174    state: &mut S,
175) {
176    enable_debug(debug_state, log_state, None);
177    enable_color_adjustment_guards(state, Some(log_state.clone()));
178    enable_render_registries_guards(state, Some(log_state.clone()));
179}
180
181// Installs the debug sink and records it so disable_debug removes exactly this one.
182fn install_debug_sink(state: &DebugStateRef, log_state: &LogStateRef, sink: &LogSink) {
183    state.write().unwrap().installed_sink = Some(sink.clone());
184    add_log_sink(log_state, sink.clone());
185}
186
187/// Whether a debug session is in effect (TS `isDebugEnabled`).
188// flight-lint: allow(conform-params) D5: the caller-owned debug state replaces the source module global
189pub fn is_debug_enabled(state: &DebugStateRef) -> bool {
190    return state.read().unwrap().enabled;
191}
192
193/// Registers a subsystem's channels and guard bindings under `name` (TS `registerDebugSubsystem`).
194/// Last write wins, and a replaced name keeps its registration position, as the source `Map` does.
195// flight-lint: allow(conform-params) D5: the caller-owned debug state replaces the source module global
196pub fn register_debug_subsystem(state: &DebugStateRef, name: &str, hooks: &DebugSubsystemHooks) {
197    let mut current = state.write().unwrap();
198    if let Some(entry) = current
199        .subsystems
200        .iter_mut()
201        .find(|(registered, _)| registered == name)
202    {
203        entry.1 = hooks.clone();
204        return;
205    }
206    current.subsystems.push((name.to_string(), hooks.clone()));
207}
208
209// Removes the installed debug sink, if any.
210fn remove_debug_sink(state: &DebugStateRef, log_state: &LogStateRef) {
211    let installed = state.write().unwrap().installed_sink.take();
212    if let Some(sink) = installed {
213        remove_log_sink(log_state, &sink);
214    }
215}
216
217// The hooks to drive: the named ones in the given order when `names` is given (unregistered names
218// are skipped), otherwise every registered subsystem in registration order.
219fn resolve_debug_subsystems(
220    state: &DebugStateRef,
221    names: Option<&[DebugSubsystemName]>,
222) -> Vec<DebugSubsystemHooks> {
223    let current = state.read().unwrap();
224    let Some(names) = names else {
225        return current
226            .subsystems
227            .iter()
228            .map(|(_, hooks)| hooks.clone())
229            .collect();
230    };
231    let mut resolved = Vec::new();
232    for name in names {
233        if let Some((_, hooks)) = current
234            .subsystems
235            .iter()
236            .find(|(registered, _)| registered == name)
237        {
238            resolved.push(hooks.clone());
239        }
240    }
241    return resolved;
242}
243
244// Restores the global level and the per-channel levels the session saved. Only the channels debug
245// raised are touched; overrides the caller set before the session survive.
246fn restore_debug_levels(state: &DebugStateRef, log_state: &LogStateRef) {
247    let (saved_global_level, saved_channel_levels) = {
248        let mut current = state.write().unwrap();
249        (
250            current.saved_global_level,
251            std::mem::take(&mut current.saved_channel_levels),
252        )
253    };
254    set_log_level(log_state, saved_global_level);
255    for (channel, saved_level) in &saved_channel_levels {
256        match saved_level {
257            None => clear_log_channel_level(log_state, channel),
258            Some(saved_level) => set_log_channel_level(log_state, channel, *saved_level),
259        }
260    }
261}
262
263/// Removes a subsystem registration (TS `unregisterDebugSubsystem`): false when nothing was
264/// registered under `name`. An already active session is not affected.
265// flight-lint: allow(conform-params) D5: the caller-owned debug state replaces the source module global
266pub fn unregister_debug_subsystem(state: &DebugStateRef, name: &str) -> bool {
267    let mut current = state.write().unwrap();
268    let Some(index) = current
269        .subsystems
270        .iter()
271        .position(|(registered, _)| registered == name)
272    else {
273        return false;
274    };
275    current.subsystems.remove(index);
276    return true;
277}