1mod 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 project;
28mod publish;
29mod selection;
30mod session;
31mod setup;
32mod status;
33#[cfg(test)]
34mod tick_bench;
35
36pub(crate) use event_log::{EventLog, InputLog, LearnLog, MidiLog};
37#[cfg(any(test, feature = "test-util"))]
38pub use fake::{FakeEngine, fake_engine};
39pub use gled_protocol::state::{
40 ClientInput, Devices, EngineEvent, EngineState, RenderStats, ServerSettings, ServerStatus,
41 SoundSnapshot, StorageCommand,
42};
43pub use outbox::Outbox;
44pub use session::{ClientConnection, ClientId, ClientState, EngineConnector};
45pub(crate) use setup::{EngineConfig, spawn};
46
47use std::{
48 collections::{BTreeSet, HashMap},
49 sync::Arc,
50 time::{Duration, Instant},
51};
52
53use kanal::Receiver;
54use tracing::debug;
55
56use crate::{
57 action::UiAction,
58 audio::{AudioPool, sound_data::SoundData},
59 input::{
60 Input, external_control::ExternalControlState, osc::OSCHandler, osc::OscConfig,
61 project_inputs::update_input_state,
62 },
63 output_state::ProjectState,
64 pipeline::extract_output::{OutputConfig, PreviewOutput},
65 runtime::{
66 persistent_state::PersistentState,
67 render_thread::{RenderSnapshot, RenderThread},
68 timing::Timing,
69 },
70 storage::{
71 asset::{Asset, project::Project},
72 asset_id::AssetId,
73 collections::Collections,
74 is_loading,
75 },
76};
77use applied_output::AppliedOutput;
78use gled_model::{
79 asset::{output_device::routing::OutputRouting, scene::grid::GridLocation},
80 collection::Collections as AssetCollections,
81};
82use patch_layout::PatchLayout;
83use uuid::Uuid;
84
85const TICK: Duration = Duration::from_micros(8_333);
90
91pub(crate) struct Engine {
92 startup: bool,
94 timing: Timing,
95 project: Option<Arc<Project>>,
100 project_id: Option<AssetId<Project>>,
101 selected_scene_instance: GridLocation,
102 effect_selection: selection::EffectSelection,
103 blackout: bool,
104 blackout_hold: bool,
105 midi_output_active: bool,
106 actions: Receiver<UiAction>,
107 inputs: Receiver<session::EngineInput>,
108 sessions: HashMap<ClientId, session::Session>,
109 collections: Collections,
110 persistent_state: PersistentState,
111 output_config: OutputConfig,
112 render_thread: RenderThread,
113 external_control_state: ExternalControlState,
114 audio_pool: AudioPool,
115 sound_data: SoundData,
116 osc_handler: Option<Arc<OSCHandler>>,
117 osc_config: Option<OscConfig>,
119 applied_output: AppliedOutput,
121 device_scan: devices::DeviceScan,
122 watched_triggers: HashMap<ClientId, BTreeSet<Uuid>>,
124 midi: midi_tools::MidiTools,
125 input_log: Arc<InputLog>,
127 hovered_routing: Option<(ClientId, OutputRouting)>,
129 patch_layout: PatchLayout,
130 render_stats: RenderStats,
131 preview_output: Receiver<PreviewOutput>,
132 network_stats_receiver: Receiver<(f64, f64)>,
133 network_stats: (f64, f64),
134 temperature_receiver: Receiver<Option<f32>>,
135 temperature: Option<f32>,
136 host_name: String,
138 sound: Arc<SoundSnapshot>,
139 ticks_since_sound: u32,
140 preview: Option<PreviewOutput>,
142 published_assets: (u64, Arc<AssetCollections>),
144}
145
146impl Engine {
147 fn run(mut self) {
148 let mut next_tick = Instant::now();
149 loop {
150 if !self.tick() {
151 debug!("All user interfaces are gone, stopping the engine");
152 return;
153 }
154 next_tick += TICK;
155 let now = Instant::now();
156 match next_tick.checked_duration_since(now) {
157 Some(wait) => std::thread::sleep(wait),
158 None => next_tick = now,
161 }
162 }
163 }
164
165 #[cfg_attr(feature = "profiling", profiling::function)]
167 fn tick(&mut self) -> bool {
168 self.receive_render_feedback();
169 self.persistent_state.update();
170 self.sound_data.update();
171 self.timing.tick(&self.persistent_state);
172 self.collections.update();
173 self.sync_sound_triggers();
174
175 if !is_loading() && self.startup {
180 match self.persistent_state.last_project_id() {
181 Some(id) if Asset::get(id, &self.collections).is_some() => {
182 self.startup = false;
183 UiAction::SetProject(id).enqueue();
184 }
185 Some(_) if self.collections.get::<Project>().is_none() => {}
187 _ => self.startup = false,
188 }
189 }
190
191 if !self.receive_client_inputs() {
192 return false;
193 }
194 Input::tick();
195 self.midi.tick();
196 let new_events = Input::new_events();
197 if !new_events.is_empty() {
198 let log = Arc::make_mut(&mut self.input_log);
199 for event in new_events {
200 log.push(event);
201 }
202 }
203 self.handle_actions();
204
205 self.external_control_state
206 .process_events(&mut self.project, &self.collections);
207 self.evaluate_triggers();
208 if let Some(project) = &mut self.project {
209 update_input_state(project, self.timing.beat_progression());
210 }
211 self.apply_project_settings();
212
213 self.render_thread.send_snapshot(RenderSnapshot {
214 project: self.project.clone(),
215 blackout: self.blackout || self.blackout_hold,
216 output_config: self.output_config.clone(),
217 fps_limit: self.persistent_state.fps_limit(),
218 layout_generation: self.patch_layout.generation(),
219 });
220
221 ProjectState {
222 project: self.project.clone(),
223 selected_scene_instance: self.selected_scene_instance,
224 blackout: self.blackout || self.blackout_hold,
225 beats_per_minute: self.timing.beats_per_minute(),
226 beat_progression: self.timing.beat_progression(),
227 }
228 .enqueue();
229 self.publish_midi();
230
231 self.publish_state();
232 true
233 }
234}