gled-core 2.34.0

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
//! Starting gled without a user interface: the background services first,
//! then the engine once a GPU adapter is known. The desktop app gets its
//! adapter from the window; the server opens one headless.

use gled_midi::{HostEvent, native::NativeHost};
use kanal::Receiver;
use thiserror::Error;
use tracing::info;

use crate::{
    action::UiAction,
    audio::AudioPool,
    engine::{self, EngineConfig, EngineConnector},
    host_temperature,
    input::{Input, artnet},
    network_stats, output_state,
    pipeline::{
        extract_output::{ExtractOutput, PreviewOutput},
        output_sender,
    },
    storage,
};

#[derive(Debug, Error)]
pub enum StartError {
    #[error("the action queue was already set up")]
    ActionQueue,
    #[error("could not start the output: {0:#}")]
    Output(anyhow::Error),
    #[error("no GPU works")]
    NoAdapter,
    #[error("could not start the engine: {0:#}")]
    Engine(anyhow::Error),
}

/// What the engine needs from the services, until it is spawned.
#[derive(Debug)]
pub struct EngineStart {
    actions: Receiver<UiAction>,
    extract_output: ExtractOutput,
    artnet_control: Receiver<Vec<u8>>,
    preview_output: Receiver<PreviewOutput>,
    network_stats: Receiver<(f64, f64)>,
    temperature: Receiver<Option<f32>>,
    midi: NativeHost,
    midi_events: Receiver<HostEvent>,
}

/// Starts storage, MIDI, Art-Net, the output and the input polling, and
/// returns what the engine needs of them. Call once per process.
pub fn services() -> Result<EngineStart, StartError> {
    let actions = UiAction::init_queue().ok_or(StartError::ActionQueue)?;
    storage::start_thread();
    let (midi_events, midi_event_receiver) = kanal::unbounded();
    let midi = NativeHost::start(midi_events);
    let network_stats = network_stats::start_thread();
    let temperature = host_temperature::start_thread();
    let (extract_output, output_receiver, preview_output) = ExtractOutput::new();
    let artnet_inputs = output_sender::start(output_receiver).map_err(StartError::Output)?;
    let (artnet_bridge, artnet_control) = artnet::start_thread(artnet_inputs);
    Input::init(artnet_bridge);
    output_state::init();
    Ok(EngineStart {
        actions,
        extract_output,
        artnet_control,
        preview_output,
        network_stats,
        temperature,
        midi,
        midi_events: midi_event_receiver,
    })
}

impl EngineStart {
    /// Starts the engine rendering on `adapter`.
    pub fn spawn(self, adapter: wgpu::Adapter) -> Result<EngineConnector, StartError> {
        engine::spawn(
            EngineConfig {
                adapter,
                extract_output: self.extract_output,
                artnet_control_receiver: self.artnet_control,
                preview_output: self.preview_output,
                network_stats: self.network_stats,
                temperature: self.temperature,
                midi: self.midi,
                midi_events: self.midi_events,
                audio_pool: AudioPool::default(),
            },
            self.actions,
        )
        .map_err(StartError::Engine)
    }
}

/// Opens a GPU without a window, for the server: the fastest that works,
/// see `gpu_choice`. The backends can be narrowed with `WGPU_BACKEND`, e.g.
/// `vulkan` on a Raspberry Pi.
pub async fn headless_adapter() -> Result<wgpu::Adapter, StartError> {
    let instance =
        wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
    let adapters = instance.enumerate_adapters(wgpu::Backends::all()).await;
    let adapter = crate::gpu_choice::choose(&adapters, |_| true).ok_or(StartError::NoAdapter)?;
    let adapter_info = adapter.get_info();
    info!(
        name = %adapter_info.name,
        backend = ?adapter_info.backend,
        "rendering on GPU adapter"
    );
    Ok(adapter)
}