Skip to main content

gled_core/engine/
mod.rs

1//! The engine: the one place that owns the running show. It holds the
2//! authoritative project, applies actions from the user interfaces, OSC, MIDI
3//! and Art-Net, evaluates the inputs, keeps time with Ableton Link, drives the
4//! render thread and the audio analysis, and publishes its state to every
5//! connected user interface.
6//!
7//! It runs on its own thread at a fixed tick rate, independent of any window.
8//! The desktop app talks to it through an `EngineHandle`; a remote client will
9//! talk to it the same way over the network.
10//!
11//! User interfaces change the show only through actions (see
12//! `ui::project_edits` for edits made directly in widgets). Their actions are
13//! numbered, and the engine reports the newest one it applied in
14//! `EngineState::applied_sequence`, so an interface knows when the engine's
15//! project includes its own edits.
16
17mod actions;
18mod applied_output;
19mod clients;
20mod controllers;
21mod devices;
22mod event_log;
23#[cfg(any(test, feature = "test-util"))]
24mod fake;
25mod midi_tools;
26mod outbox;
27mod patch_layout;
28mod pictures;
29mod project;
30mod publish;
31mod selection;
32mod session;
33mod setup;
34mod status;
35#[cfg(test)]
36mod tick_bench;
37
38pub(crate) use event_log::{EventLog, InputLog, LearnLog, MidiLog};
39#[cfg(any(test, feature = "test-util"))]
40pub use fake::{FakeEngine, fake_engine};
41pub use gled_protocol::state::{
42    ClientInput, Devices, EngineEvent, EngineState, RenderStats, ServerSettings, ServerStatus,
43    SoundSnapshot, StorageCommand,
44};
45pub use outbox::Outbox;
46pub use session::{ClientConnection, ClientId, ClientState, EngineConnector};
47pub(crate) use setup::{EngineConfig, spawn};
48
49use std::{
50    collections::{BTreeSet, HashMap},
51    sync::Arc,
52    time::{Duration, Instant},
53};
54
55use kanal::Receiver;
56use tracing::debug;
57
58use crate::{
59    action::UiAction,
60    audio::{AudioPool, sound_data::SoundData},
61    input::{
62        Input, external_control::ExternalControlState, osc::OSCHandler, osc::OscConfig,
63        project_inputs::update_input_state,
64    },
65    output_state::ProjectState,
66    pipeline::extract_output::{OutputConfig, PreviewOutput},
67    runtime::{
68        persistent_state::PersistentState,
69        render_thread::{RenderSnapshot, RenderThread},
70        timing::Timing,
71    },
72    storage::{
73        asset::{Asset, project::Project},
74        asset_id::AssetId,
75        collections::Collections,
76        is_loading,
77    },
78};
79use applied_output::AppliedOutput;
80use gled_model::{
81    asset::{output_device::routing::OutputRouting, scene::grid::GridLocation},
82    collection::Collections as AssetCollections,
83};
84use gled_protocol::state::GridLine;
85use patch_layout::PatchLayout;
86use uuid::Uuid;
87
88/// The engine's tick rate. Inputs are evaluated and state is published once
89/// per tick; the LED output runs at its own rate on the render thread.
90///
91/// 8333 us per tick is about 120 updates per second.
92const TICK: Duration = Duration::from_micros(8_333);
93
94pub(crate) struct Engine {
95    /// Load the last project once storage has finished loading.
96    startup: bool,
97    timing: Timing,
98    /// Shared with the render thread, the MIDI and OSC feedback and the
99    /// interfaces: changing it (`Arc::make_mut`) copies it only while one of
100    /// them still holds the version published last, so a tick that changes
101    /// nothing copies nothing.
102    project: Option<Arc<Project>>,
103    project_id: Option<AssetId<Project>>,
104    selected_scene_instance: GridLocation,
105    effect_selection: selection::EffectSelection,
106    /// The line a MIDI controller last moved the selection along.
107    controller_line: Option<GridLine>,
108    controllers: controllers::Controllers,
109    blackout: bool,
110    blackout_hold: bool,
111    midi_output_active: bool,
112    actions: Receiver<UiAction>,
113    inputs: Receiver<session::EngineInput>,
114    sessions: HashMap<ClientId, session::Session>,
115    collections: Collections,
116    persistent_state: PersistentState,
117    output_config: OutputConfig,
118    render_thread: RenderThread,
119    external_control_state: ExternalControlState,
120    audio_pool: AudioPool,
121    sound_data: SoundData,
122    osc_handler: Option<Arc<OSCHandler>>,
123    /// The OSC settings the handler runs with.
124    osc_config: Option<OscConfig>,
125    /// The project's output settings as last applied to the running output.
126    applied_output: AppliedOutput,
127    device_scan: devices::DeviceScan,
128    pictures: pictures::Pictures,
129    /// The sound triggers each interface edits.
130    watched_triggers: HashMap<ClientId, BTreeSet<Uuid>>,
131    midi: midi_tools::MidiTools,
132    /// The input events of the last ticks, for assigning inputs.
133    input_log: Arc<InputLog>,
134    /// The output routing an interface points at, and which one.
135    hovered_routing: Option<(ClientId, OutputRouting)>,
136    patch_layout: PatchLayout,
137    render_stats: RenderStats,
138    preview_output: Receiver<PreviewOutput>,
139    network_stats_receiver: Receiver<(f64, f64)>,
140    network_stats: (f64, f64),
141    temperature_receiver: Receiver<Option<f32>>,
142    temperature: Option<f32>,
143    /// This machine's name, which interfaces on others show it under.
144    host_name: String,
145    sound: Arc<SoundSnapshot>,
146    ticks_since_sound: u32,
147    /// The newest preview frame; `None` while no project renders.
148    preview: Option<PreviewOutput>,
149    /// The assets as last published, and the replica's generation then.
150    published_assets: (u64, Arc<AssetCollections>),
151}
152
153impl Engine {
154    fn run(mut self) {
155        let mut next_tick = Instant::now();
156        loop {
157            if !self.tick() {
158                debug!("All user interfaces are gone, stopping the engine");
159                return;
160            }
161            next_tick += TICK;
162            let now = Instant::now();
163            match next_tick.checked_duration_since(now) {
164                Some(wait) => std::thread::sleep(wait),
165                // Behind schedule, e.g. after loading a project: start over
166                // instead of running a burst of ticks.
167                None => next_tick = now,
168            }
169        }
170    }
171
172    /// One engine step. Returns `false` once no user interface is connected.
173    #[cfg_attr(feature = "profiling", profiling::function)]
174    fn tick(&mut self) -> bool {
175        self.receive_render_feedback();
176        self.persistent_state.update();
177        self.sound_data.update();
178        self.timing.tick(&self.persistent_state);
179        self.collections.update();
180        self.sync_sound_triggers();
181
182        // Loading finishing does not mean the collections have arrived: they
183        // come over a channel that `Collections::update` drains, possibly a
184        // tick later. Waiting for the asset itself avoids starting with no
185        // project.
186        if !is_loading() && self.startup {
187            match self.persistent_state.last_project_id() {
188                Some(id) if Asset::get(id, &self.collections).is_some() => {
189                    self.startup = false;
190                    UiAction::SetProject(id).enqueue();
191                }
192                // keep waiting while the project collection is still in flight
193                Some(_) if self.collections.get::<Project>().is_none() => {}
194                _ => self.startup = false,
195            }
196        }
197
198        if !self.receive_client_inputs() {
199            return false;
200        }
201        Input::tick();
202        self.midi.tick();
203        let new_events = Input::new_events();
204        if !new_events.is_empty() {
205            let log = Arc::make_mut(&mut self.input_log);
206            for event in new_events {
207                log.push(event);
208            }
209        }
210        self.handle_actions();
211
212        self.external_control_state
213            .process_events(&mut self.project, &self.collections);
214        self.evaluate_triggers();
215        if let Some(project) = &mut self.project {
216            update_input_state(project, self.timing.beat_progression());
217        }
218        self.apply_project_settings();
219
220        self.render_thread.send_snapshot(RenderSnapshot {
221            project: self.project.clone(),
222            blackout: self.blackout || self.blackout_hold,
223            output_config: self.output_config.clone(),
224            fps_limit: self.persistent_state.fps_limit(),
225            layout_generation: self.patch_layout.generation(),
226        });
227
228        ProjectState {
229            project: self.project.clone(),
230            selected_scene_instance: self.selected_scene_instance,
231            blackout: self.blackout || self.blackout_hold,
232            beats_per_minute: self.timing.beats_per_minute(),
233            beat_progression: self.timing.beat_progression(),
234        }
235        .enqueue();
236        self.publish_midi();
237
238        self.publish_state();
239        true
240    }
241}