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