1mod actions;
18mod applied_output;
19mod devices;
20mod event_log;
21#[cfg(any(test, feature = "test-util"))]
22mod fake;
23mod midi_tools;
24mod outbox;
25mod session;
26mod status;
27mod svg_layout;
28mod svg_templates;
29
30pub(crate) use event_log::{EventLog, InputLog, LearnLog, MidiLog};
31#[cfg(any(test, feature = "test-util"))]
32pub use fake::{FakeEngine, fake_engine};
33use gled_model::sound::trigger_ref::SoundLevels;
34use gled_protocol::bytes::SharedBytes;
35pub use gled_protocol::state::{
36 ClientInput, Devices, EngineEvent, EngineState, RenderStats, ServerSettings, ServerStatus,
37 SoundSnapshot, StorageCommand,
38};
39pub use outbox::Outbox;
40pub use session::{ClientConnection, ClientId, ClientState, EngineConnector};
41
42use std::{
43 collections::{BTreeSet, HashMap},
44 sync::Arc,
45 time::{Duration, Instant},
46};
47
48use kanal::{Receiver, unbounded};
49use tracing::debug;
50
51use crate::{
52 action::UiAction,
53 audio::{
54 AudioPool,
55 sound_data::SoundData,
56 sound_trigger_ref::{CollectSoundTriggers, SoundTriggerSet},
57 },
58 input::{
59 Input, external_control::ExternalControlState, osc::OSCHandler, osc::OscConfig,
60 project_inputs::ProjectInputs,
61 },
62 output_state::ProjectState,
63 pipeline::extract_output::{ExtractOutput, OutputConfig, PreviewOutput},
64 pipeline::output_sender::HOVERED_OUTPUT_ROUTING,
65 runtime::{
66 persistent_state::PersistentState,
67 render_thread::{RenderFeedback, RenderSnapshot, RenderThread},
68 timing::Timing,
69 },
70 storage::{
71 action::StorageAction,
72 asset::{Asset, project::Project, scene::Scene},
73 asset_id::AssetId,
74 collections::Collections,
75 is_loading,
76 store::{AssetStore, delete_asset},
77 },
78};
79use applied_output::AppliedOutput;
80use gled_model::{
81 asset::{AssetTrait, output_device::routing::OutputRouting, scene::grid::GridLocation},
82 collection::Collections as AssetCollections,
83};
84use gled_protocol::{
85 assets::{AssetIdVisitor, AssetVisitor},
86 midi::{MidiMonitorEvent, TestCommand},
87};
88use svg_layout::SvgLayout;
89use uuid::Uuid;
90
91const TICK: Duration = Duration::from_micros(8_333);
96const SOUND_EVERY_TICKS: u32 = 4;
98
99#[derive(Debug)]
101pub(crate) struct EngineConfig {
102 pub(crate) adapter: wgpu::Adapter,
104 pub(crate) extract_output: ExtractOutput,
105 pub(crate) artnet_control_receiver: Receiver<Vec<u8>>,
106 pub(crate) preview_output: Receiver<PreviewOutput>,
108 pub(crate) network_stats: Receiver<(f64, f64)>,
110 pub(crate) temperature: Receiver<Option<f32>>,
112 pub(crate) midi_monitor: Receiver<MidiMonitorEvent>,
113 pub(crate) midi_learn: Receiver<[u8; 3]>,
114 pub(crate) midi_test: kanal::Sender<TestCommand>,
115 pub(crate) audio_pool: AudioPool,
116}
117
118pub(crate) fn spawn(
121 config: EngineConfig,
122 actions: Receiver<UiAction>,
123) -> anyhow::Result<EngineConnector> {
124 let timing = Timing::default();
125 let persistent_state = PersistentState::default();
126 let render_thread = RenderThread::spawn(
127 &config.adapter,
128 timing.link(),
129 config.extract_output,
130 persistent_state.fps_limit(),
131 )?;
132
133 let (input_sender, input_receiver) = unbounded();
134
135 let engine = Engine {
136 startup: true,
137 timing,
138 project: None,
139 project_id: None,
140 selected_scene_instance: GridLocation::default(),
141 blackout: true,
142 blackout_hold: false,
143 midi_output_active: false,
144 actions,
145 inputs: input_receiver,
146 sessions: HashMap::new(),
147 collections: Collections::default(),
148 persistent_state,
149 output_config: OutputConfig::default(),
150 render_thread,
151 external_control_state: ExternalControlState::new(config.artnet_control_receiver),
152 audio_pool: config.audio_pool,
153 sound_data: SoundData::default(),
154 osc_handler: None,
155 osc_config: None,
156 applied_output: AppliedOutput::default(),
157 hovered_routing: None,
158 input_log: Arc::default(),
159 device_scan: devices::DeviceScan::default(),
160 svg_templates: svg_templates::SvgTemplates::default(),
161 watched_triggers: HashMap::new(),
162 midi: midi_tools::MidiTools::new(config.midi_monitor, config.midi_learn, config.midi_test),
163 svg_layout: SvgLayout::default(),
164 render_stats: RenderStats::default(),
165 published_assets: (u64::MAX, Arc::default()),
166 preview_output: config.preview_output,
167 network_stats_receiver: config.network_stats,
168 network_stats: (0.0, 0.0),
169 temperature_receiver: config.temperature,
170 temperature: None,
171 host_name: gethostname::gethostname().to_string_lossy().into_owned(),
172 sound: Arc::default(),
173 ticks_since_sound: 0,
174 preview: None,
175 };
176
177 std::thread::Builder::new()
178 .name("gled:engine".to_string())
179 .spawn(move || engine.run())?;
180
181 Ok(EngineConnector {
182 inputs: input_sender,
183 next_id: Arc::default(),
184 })
185}
186
187pub(crate) struct Engine {
188 startup: bool,
190 timing: Timing,
191 project: Option<Project>,
192 project_id: Option<AssetId<Project>>,
193 selected_scene_instance: GridLocation,
194 blackout: bool,
195 blackout_hold: bool,
196 midi_output_active: bool,
197 actions: Receiver<UiAction>,
198 inputs: Receiver<session::EngineInput>,
199 sessions: HashMap<ClientId, session::Session>,
200 collections: Collections,
201 persistent_state: PersistentState,
202 output_config: OutputConfig,
203 render_thread: RenderThread,
204 external_control_state: ExternalControlState,
205 audio_pool: AudioPool,
206 sound_data: SoundData,
207 osc_handler: Option<Arc<OSCHandler>>,
208 osc_config: Option<OscConfig>,
210 applied_output: AppliedOutput,
212 device_scan: devices::DeviceScan,
213 svg_templates: svg_templates::SvgTemplates,
214 watched_triggers: HashMap<ClientId, BTreeSet<Uuid>>,
216 midi: midi_tools::MidiTools,
217 input_log: Arc<InputLog>,
219 hovered_routing: Option<(ClientId, OutputRouting)>,
221 svg_layout: SvgLayout,
222 render_stats: RenderStats,
223 preview_output: Receiver<PreviewOutput>,
224 network_stats_receiver: Receiver<(f64, f64)>,
225 network_stats: (f64, f64),
226 temperature_receiver: Receiver<Option<f32>>,
227 temperature: Option<f32>,
228 host_name: String,
230 sound: Arc<SoundSnapshot>,
231 ticks_since_sound: u32,
232 preview: Option<PreviewOutput>,
234 published_assets: (u64, Arc<AssetCollections>),
236}
237
238impl Engine {
239 fn run(mut self) {
240 let mut next_tick = Instant::now();
241 loop {
242 if !self.tick() {
243 debug!("All user interfaces are gone, stopping the engine");
244 return;
245 }
246 next_tick += TICK;
247 let now = Instant::now();
248 match next_tick.checked_duration_since(now) {
249 Some(wait) => std::thread::sleep(wait),
250 None => next_tick = now,
253 }
254 }
255 }
256
257 #[cfg_attr(feature = "profiling", profiling::function)]
259 fn tick(&mut self) -> bool {
260 self.receive_render_feedback();
261 self.persistent_state.update();
262 self.sound_data.update();
263 self.timing.tick(&self.persistent_state);
264 self.collections.update();
265 self.sync_sound_triggers();
266
267 if !is_loading() && self.startup {
268 self.startup = false;
269 if let Some(project) = self.persistent_state.last_project_id() {
270 UiAction::SetProject(project).enqueue();
271 }
272 }
273
274 if !self.receive_client_inputs() {
275 return false;
276 }
277 Input::tick();
278 self.midi.tick();
279 let new_events = Input::new_events();
280 if !new_events.is_empty() {
281 let log = Arc::make_mut(&mut self.input_log);
282 for event in new_events {
283 log.push(event);
284 }
285 }
286 self.handle_actions();
287
288 self.external_control_state
289 .process_events(&mut self.project, &self.collections);
290 self.evaluate_triggers();
291 if let Some(project) = &mut self.project {
292 project.update_input_state(self.timing.beat_progression());
293 }
294 self.apply_project_settings();
295
296 self.render_thread.send_snapshot(RenderSnapshot {
297 project: self.project.clone(),
298 blackout: self.blackout || self.blackout_hold,
299 output_config: self.output_config.clone(),
300 fps_limit: self.persistent_state.fps_limit(),
301 });
302
303 ProjectState {
304 project: self.project.clone(),
305 selected_scene_instance: self.selected_scene_instance,
306 blackout: self.blackout || self.blackout_hold,
307 beats_per_minute: self.timing.beats_per_minute(),
308 beat_progression: self.timing.beat_progression(),
309 }
310 .enqueue();
311
312 self.publish_state();
313 true
314 }
315
316 fn receive_client_inputs(&mut self) -> bool {
319 loop {
320 match self.inputs.try_recv() {
321 Ok(Some(session::EngineInput::Connect { id, state, events })) => {
322 debug!(id, "client connected");
323 self.sessions.insert(
324 id,
325 session::Session {
326 state,
327 events,
328 applied_sequence: 0,
329 keys: BTreeSet::new(),
330 },
331 );
332 }
333 Ok(Some(session::EngineInput::Client { id, input })) => {
334 self.apply_client_input(id, input)
335 }
336 Ok(Some(session::EngineInput::Disconnect(id))) => self.disconnect(id),
337 Ok(None) => return true,
338 Err(_) => return false,
339 }
340 }
341 }
342
343 fn apply_client_input(&mut self, id: ClientId, input: ClientInput) {
344 match input {
345 ClientInput::Action { sequence, action } => {
346 if let Some(session) = self.sessions.get_mut(&id) {
347 session.applied_sequence = session.applied_sequence.max(sequence);
348 }
349 self.apply_action(action);
350 }
351 ClientInput::Keys(keys) => {
352 debug!(id, ?keys, "keys held in the interface");
353 if let Some(session) = self.sessions.get_mut(&id) {
354 session.keys = keys;
355 }
356 self.update_keys();
357 }
358 ClientInput::SetBeatsPerMinute(bpm) => self.timing.set_beats_per_minute(bpm),
359 ClientInput::SaveAsset(asset) => asset.accept(&mut SaveInto(&mut self.collections)),
360 ClientInput::DeleteAsset(id) => id.accept(&mut DeleteFrom(&mut self.collections)),
361 ClientInput::Storage(command) => match command {
362 StorageCommand::Commit { message } => StorageAction::Commit { message }.enqueue(),
363 StorageCommand::Pull => StorageAction::Pull.enqueue(),
364 StorageCommand::Push => StorageAction::Push.enqueue(),
365 StorageCommand::SwitchBranch(branch) => {
366 StorageAction::SwitchBranch(branch).enqueue()
367 }
368 StorageCommand::Reclone => StorageAction::Nuke.enqueue(),
369 StorageCommand::SetGitPassphrase(passphrase) => self
370 .persistent_state
371 .git_credentials_mut()
372 .set_passphrase(passphrase),
373 },
374 ClientInput::SetServerSettings(settings) => self.apply_server_settings(settings),
375 ClientInput::WatchDevices(watch) => self.device_scan.watch(id, watch),
376 ClientInput::WatchMidi(watch) => self.midi.watch(id, watch),
377 ClientInput::MidiLearning(learning) => self.midi.learning(id, learning),
378 ClientInput::MidiTest(command) => self.midi.test(command),
379 ClientInput::UseSvgTemplate(name) => match svg_templates::read(&name) {
380 Ok(contents) => self.apply_action(UiAction::SetSvg(Some(
381 gled_model::asset::svg::Svg::new(contents),
382 ))),
383 Err(err) => self.send_event(EngineEvent::Error(err)),
384 },
385 ClientInput::WatchSoundTriggers(triggers) => {
386 self.watched_triggers.insert(id, triggers);
387 }
388 ClientInput::HoverOutputRouting(routing) => {
389 let owned = self
390 .hovered_routing
391 .as_ref()
392 .is_some_and(|(owner, _)| *owner == id);
393 match routing {
394 Some(routing) => self.set_hovered_routing(Some((id, routing))),
395 None if owned => self.set_hovered_routing(None),
396 None => {}
397 }
398 }
399 }
400 }
401
402 fn disconnect(&mut self, id: ClientId) {
403 if self.sessions.remove(&id).is_some() {
404 debug!(id, "client disconnected");
405 self.update_keys();
407 self.device_scan.disconnect(id);
408 self.midi.disconnect(id);
409 self.watched_triggers.remove(&id);
410 if self
411 .hovered_routing
412 .as_ref()
413 .is_some_and(|(owner, _)| *owner == id)
414 {
415 self.set_hovered_routing(None);
416 }
417 }
418 }
419
420 fn set_hovered_routing(&mut self, hovered: Option<(ClientId, OutputRouting)>) {
421 *HOVERED_OUTPUT_ROUTING.lock() = hovered.as_ref().map(|(_, routing)| routing.clone());
422 self.hovered_routing = hovered;
423 }
424
425 fn update_keys(&self) {
427 let keys = self
428 .sessions
429 .values()
430 .flat_map(|session| session.keys.iter().copied())
431 .collect();
432 Input::set_keys(keys);
433 }
434
435 fn evaluate_triggers(&mut self) {
437 let Some(project) = &self.project else {
438 self.blackout_hold = false;
439 return;
440 };
441 if project.blackout_input_is_new() {
442 self.blackout = !self.blackout;
443 }
444 self.blackout_hold = project.blackout_hold_input_is_live();
445 if project.tap_input_is_new() {
446 self.timing.tap(&self.persistent_state);
447 }
448 if project.double_input_is_new() {
449 self.timing.multiply_speed(2.0, &self.persistent_state);
450 }
451 if project.half_input_is_new() {
452 self.timing.multiply_speed(0.5, &self.persistent_state);
453 }
454 }
455
456 fn apply_project_settings(&mut self) {
459 let svg = self
460 .project
461 .as_ref()
462 .and_then(|project| project.svg.as_ref());
463 if self.svg_layout.update(svg, &mut self.output_config) {
464 self.render_thread
465 .send_command(crate::runtime::render_thread::RenderCommand::SendPositions);
466 }
467
468 if let Some(project) = &self.project {
469 self.applied_output.apply(project);
470 }
471
472 let osc_config = self.project.as_ref().map(|project| project.osc_config);
473 if osc_config != self.osc_config {
474 self.osc_config = osc_config;
475 self.apply_osc_config(osc_config.unwrap_or_default());
476 }
477 }
478
479 fn apply_osc_config(&mut self, config: OscConfig) {
480 if let Some(handler) = self.osc_handler.take() {
481 handler.stop();
482 }
483 if config.active {
484 self.osc_handler = OSCHandler::start(config.port)
485 .map_err(|err| {
486 UiAction::Error(format!(
487 "Could not start OSC on port {}: {err}",
488 config.port
489 ))
490 .enqueue();
491 })
492 .ok();
493 }
494 }
495
496 fn sync_sound_triggers(&mut self) {
499 let mut wanted = SoundTriggerSet::new();
500 self.project.collect_sound_triggers(&mut wanted);
501 for scene in Asset::<Scene>::all(&self.collections) {
502 scene.data.collect_sound_triggers(&mut wanted);
503 }
504 if self.sound_data.sync_triggers(&wanted) {
505 self.sound_data.save();
506 }
507 }
508
509 fn receive_render_feedback(&mut self) {
510 while let Ok(Some(stats)) = self.network_stats_receiver.try_recv() {
511 self.network_stats = stats;
512 }
513 while let Ok(Some(temperature)) = self.temperature_receiver.try_recv() {
514 self.temperature = temperature;
515 }
516 while let Ok(Some(output)) = self.preview_output.try_recv() {
517 self.preview = Some(output);
518 }
519 if self.project.is_none() {
520 self.preview = None;
521 }
522 while let Some(feedback) = self.render_thread.try_recv_feedback() {
523 match feedback {
524 RenderFeedback::FrameStats(stats) => {
525 self.timing.set_framerate(stats.fps);
526 self.render_stats = stats;
527 }
528 }
529 }
530 }
531
532 fn publish_state(&mut self) {
533 let shared = Arc::new(EngineState {
534 project: self.project.clone(),
535 project_id: self.project_id,
536 selected_scene_instance: self.selected_scene_instance,
537 blackout: self.blackout,
538 blackout_hold: self.blackout_hold,
539 midi_output_active: self.midi_output_active,
540 output_config: self.output_config.clone(),
541 beat: self.timing.beat_state(),
542 tap_count: self.timing.tap_count(),
543 layout_generation: self.svg_layout.generation(),
544 render: self.render_stats,
545 status: self.server_status(),
546 });
547 self.ticks_since_sound += 1;
548 if self.ticks_since_sound >= SOUND_EVERY_TICKS {
549 self.ticks_since_sound = 0;
550 let watched: BTreeSet<Uuid> =
551 self.watched_triggers.values().flatten().copied().collect();
552 self.sound = Arc::new(SoundSnapshot {
553 levels: self.sound_data.trigger_levels().collect(),
554 impulses: self
555 .sound_data
556 .triggers
557 .iter()
558 .map(|(id, trigger)| (*id, trigger.impulse))
559 .collect(),
560 spectrum: SharedBytes(Arc::new(self.sound_data.spectrum_bytes().to_vec())),
561 trigger_spectra: watched
562 .iter()
563 .filter_map(|id| {
564 let trigger = self.sound_data.triggers.get(id)?;
565 let bytes = trigger
566 .spectrum
567 .iter()
568 .flat_map(|value| value.to_le_bytes())
569 .collect();
570 Some((*id, SharedBytes(Arc::new(bytes))))
571 })
572 .collect(),
573 });
574 }
575 if self.published_assets.0 != self.collections.generation() {
576 self.published_assets = (
577 self.collections.generation(),
578 Arc::new(AssetCollections::clone(&self.collections)),
579 );
580 }
581 let mut gone = Vec::new();
582 for (id, session) in &self.sessions {
583 let state = ClientState {
586 shared: shared.clone(),
587 applied_sequence: session.applied_sequence,
588 assets: self.published_assets.1.clone(),
589 preview: self.preview.clone(),
590 sound: self.sound.clone(),
591 input_log: self.input_log.clone(),
592 midi_log: self.midi.monitor_log.clone(),
593 learn_log: self.midi.learn_log.clone(),
594 };
595 if session.state.try_send(state).is_err() {
596 gone.push(*id);
597 }
598 }
599 for id in gone {
600 self.disconnect(id);
601 }
602 }
603
604 fn send_event(&self, event: EngineEvent) {
605 for session in self.sessions.values() {
606 let _ = session.events.send(event.clone());
608 }
609 }
610}
611
612struct SaveInto<'a>(&'a mut Collections);
614
615impl AssetVisitor for SaveInto<'_> {
616 fn asset<T: AssetTrait>(&mut self, asset: Arc<Asset<T>>) {
617 Arc::unwrap_or_clone(asset).save(self.0);
618 }
619}
620
621struct DeleteFrom<'a>(&'a mut Collections);
623
624impl AssetIdVisitor for DeleteFrom<'_> {
625 fn id<T: AssetTrait>(&mut self, id: AssetId<T>) {
626 delete_asset(id, self.0);
627 }
628}