gled-core 2.34.0

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
//! The engine: the one place that owns the running show. It holds the
//! authoritative project, applies actions from the user interfaces, OSC, MIDI
//! and Art-Net, evaluates the inputs, keeps time with Ableton Link, drives the
//! render thread and the audio analysis, and publishes its state to every
//! connected user interface.
//!
//! It runs on its own thread at a fixed tick rate, independent of any window.
//! The desktop app talks to it through an `EngineHandle`; a remote client will
//! talk to it the same way over the network.
//!
//! User interfaces change the show only through actions (see
//! `ui::project_edits` for edits made directly in widgets). Their actions are
//! numbered, and the engine reports the newest one it applied in
//! `EngineState::applied_sequence`, so an interface knows when the engine's
//! project includes its own edits.

mod actions;
mod applied_output;
mod clients;
mod controllers;
mod devices;
mod event_log;
#[cfg(any(test, feature = "test-util"))]
mod fake;
mod midi_tools;
mod outbox;
mod patch_layout;
mod pictures;
mod project;
mod publish;
mod selection;
mod session;
mod setup;
mod status;
#[cfg(test)]
mod tick_bench;

pub(crate) use event_log::{EventLog, InputLog, LearnLog, MidiLog};
#[cfg(any(test, feature = "test-util"))]
pub use fake::{FakeEngine, fake_engine};
pub use gled_protocol::state::{
    ClientInput, Devices, EngineEvent, EngineState, RenderStats, ServerSettings, ServerStatus,
    SoundSnapshot, StorageCommand,
};
pub use outbox::Outbox;
pub use session::{ClientConnection, ClientId, ClientState, EngineConnector};
pub(crate) use setup::{EngineConfig, spawn};

use std::{
    collections::{BTreeSet, HashMap},
    sync::Arc,
    time::{Duration, Instant},
};

use kanal::Receiver;
use tracing::debug;

use crate::{
    action::UiAction,
    audio::{AudioPool, sound_data::SoundData},
    input::{
        Input, external_control::ExternalControlState, osc::OSCHandler, osc::OscConfig,
        project_inputs::update_input_state,
    },
    output_state::ProjectState,
    pipeline::extract_output::{OutputConfig, PreviewOutput},
    runtime::{
        persistent_state::PersistentState,
        render_thread::{RenderSnapshot, RenderThread},
        timing::Timing,
    },
    storage::{
        asset::{Asset, project::Project},
        asset_id::AssetId,
        collections::Collections,
        is_loading,
    },
};
use applied_output::AppliedOutput;
use gled_model::{
    asset::{output_device::routing::OutputRouting, scene::grid::GridLocation},
    collection::Collections as AssetCollections,
};
use gled_protocol::state::GridLine;
use patch_layout::PatchLayout;
use uuid::Uuid;

/// The engine's tick rate. Inputs are evaluated and state is published once
/// per tick; the LED output runs at its own rate on the render thread.
///
/// 8333 us per tick is about 120 updates per second.
const TICK: Duration = Duration::from_micros(8_333);

pub(crate) struct Engine {
    /// Load the last project once storage has finished loading.
    startup: bool,
    timing: Timing,
    /// Shared with the render thread, the MIDI and OSC feedback and the
    /// interfaces: changing it (`Arc::make_mut`) copies it only while one of
    /// them still holds the version published last, so a tick that changes
    /// nothing copies nothing.
    project: Option<Arc<Project>>,
    project_id: Option<AssetId<Project>>,
    selected_scene_instance: GridLocation,
    effect_selection: selection::EffectSelection,
    /// The line a MIDI controller last moved the selection along.
    controller_line: Option<GridLine>,
    controllers: controllers::Controllers,
    blackout: bool,
    blackout_hold: bool,
    midi_output_active: bool,
    actions: Receiver<UiAction>,
    inputs: Receiver<session::EngineInput>,
    sessions: HashMap<ClientId, session::Session>,
    collections: Collections,
    persistent_state: PersistentState,
    output_config: OutputConfig,
    render_thread: RenderThread,
    external_control_state: ExternalControlState,
    audio_pool: AudioPool,
    sound_data: SoundData,
    osc_handler: Option<Arc<OSCHandler>>,
    /// The OSC settings the handler runs with.
    osc_config: Option<OscConfig>,
    /// The project's output settings as last applied to the running output.
    applied_output: AppliedOutput,
    device_scan: devices::DeviceScan,
    pictures: pictures::Pictures,
    /// The sound triggers each interface edits.
    watched_triggers: HashMap<ClientId, BTreeSet<Uuid>>,
    midi: midi_tools::MidiTools,
    /// The input events of the last ticks, for assigning inputs.
    input_log: Arc<InputLog>,
    /// The output routing an interface points at, and which one.
    hovered_routing: Option<(ClientId, OutputRouting)>,
    patch_layout: PatchLayout,
    render_stats: RenderStats,
    preview_output: Receiver<PreviewOutput>,
    network_stats_receiver: Receiver<(f64, f64)>,
    network_stats: (f64, f64),
    temperature_receiver: Receiver<Option<f32>>,
    temperature: Option<f32>,
    /// This machine's name, which interfaces on others show it under.
    host_name: String,
    sound: Arc<SoundSnapshot>,
    ticks_since_sound: u32,
    /// The newest preview frame; `None` while no project renders.
    preview: Option<PreviewOutput>,
    /// The assets as last published, and the replica's generation then.
    published_assets: (u64, Arc<AssetCollections>),
}

impl Engine {
    fn run(mut self) {
        let mut next_tick = Instant::now();
        loop {
            if !self.tick() {
                debug!("All user interfaces are gone, stopping the engine");
                return;
            }
            next_tick += TICK;
            let now = Instant::now();
            match next_tick.checked_duration_since(now) {
                Some(wait) => std::thread::sleep(wait),
                // Behind schedule, e.g. after loading a project: start over
                // instead of running a burst of ticks.
                None => next_tick = now,
            }
        }
    }

    /// One engine step. Returns `false` once no user interface is connected.
    #[cfg_attr(feature = "profiling", profiling::function)]
    fn tick(&mut self) -> bool {
        self.receive_render_feedback();
        self.persistent_state.update();
        self.sound_data.update();
        self.timing.tick(&self.persistent_state);
        self.collections.update();
        self.sync_sound_triggers();

        // Loading finishing does not mean the collections have arrived: they
        // come over a channel that `Collections::update` drains, possibly a
        // tick later. Waiting for the asset itself avoids starting with no
        // project.
        if !is_loading() && self.startup {
            match self.persistent_state.last_project_id() {
                Some(id) if Asset::get(id, &self.collections).is_some() => {
                    self.startup = false;
                    UiAction::SetProject(id).enqueue();
                }
                // keep waiting while the project collection is still in flight
                Some(_) if self.collections.get::<Project>().is_none() => {}
                _ => self.startup = false,
            }
        }

        if !self.receive_client_inputs() {
            return false;
        }
        Input::tick();
        self.midi.tick();
        let new_events = Input::new_events();
        if !new_events.is_empty() {
            let log = Arc::make_mut(&mut self.input_log);
            for event in new_events {
                log.push(event);
            }
        }
        self.handle_actions();

        self.external_control_state
            .process_events(&mut self.project, &self.collections);
        self.evaluate_triggers();
        if let Some(project) = &mut self.project {
            update_input_state(project, self.timing.beat_progression());
        }
        self.apply_project_settings();

        self.render_thread.send_snapshot(RenderSnapshot {
            project: self.project.clone(),
            blackout: self.blackout || self.blackout_hold,
            output_config: self.output_config.clone(),
            fps_limit: self.persistent_state.fps_limit(),
            layout_generation: self.patch_layout.generation(),
        });

        ProjectState {
            project: self.project.clone(),
            selected_scene_instance: self.selected_scene_instance,
            blackout: self.blackout || self.blackout_hold,
            beats_per_minute: self.timing.beats_per_minute(),
            beat_progression: self.timing.beat_progression(),
        }
        .enqueue();
        self.publish_midi();

        self.publish_state();
        true
    }
}