gled-core 2.32.1

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
//! What the engine's tick costs for the project with a large show: ~500
//! fixtures and a full scene grid. Run with
//! `cargo test -p gled-core tick_bench -- --ignored --nocapture`.

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

use gled_model::{
    asset::scene::{effect::Effect, grid::GridLocation, instance::SceneInstance},
    collection::Collections,
    patching::{
        fixture::{Fixture, LayoutPosition},
        layout::LayoutRef,
    },
};

use super::{ClientState, EngineState, Outbox};
use crate::{
    input::project_inputs::update_input_state, output_state::ProjectState,
    pipeline::extract_output::OutputConfig, runtime::render_thread::RenderSnapshot,
    storage::asset::project::Project,
};

const TICKS: u32 = 300;
/// The output state's subscribers without OSC: MIDI state and MIDI runtime.
const PROJECT_STATE_SUBSCRIBERS: usize = 2;

fn large_project() -> Project {
    let mut project = Project::default();
    project.set_grid_size(8, 6);
    for index in 0..500 {
        project.patch.add_fixture(
            Fixture::new(format!("bar {index}")),
            LayoutPosition::default(),
        );
    }
    for _ in 0..3 {
        let layout = project.patch.add_layout();
        let ids: Vec<_> = project.patch.topology.fixtures.keys().copied().collect();
        for id in ids {
            project.patch.add_member(LayoutRef::Layout(layout), id);
        }
    }
    for row in 0..6 {
        for col in 0..8 {
            let mut instance =
                SceneInstance::from_scene_id(Default::default(), &Collections::default());
            for _ in 0..3 {
                instance.scene.effects.push(Effect::default());
            }
            project.add_scene_instance(GridLocation::new(col, row), instance);
        }
    }
    project
}

/// One tick's work on the project after the actions: the inputs, the
/// snapshot for the render thread, the state for MIDI and OSC with its
/// fan-out, the state for the interfaces and one interface's messages.
/// The render thread keeps the newest snapshot, `rendered`, until the next.
/// `assets` are the same value every tick, as the engine publishes them
/// while none changes.
fn tick(
    project: &mut Option<Arc<Project>>,
    assets: &Arc<Collections>,
    outbox: &mut Outbox,
    rendered: &mut Option<RenderSnapshot>,
    fader: Option<f32>,
) {
    if let Some(value) = fader
        && let Some(project) = project.as_mut().map(Arc::make_mut)
        && let Some(scene) = project
            .scenes_instances_grid
            .get_mut(&GridLocation::new(0, 0))
    {
        scene.opacity.multiplier = value;
    }
    if let Some(project) = project.as_mut() {
        update_input_state(project, 0.5);
    }
    *rendered = Some(RenderSnapshot {
        project: project.clone(),
        blackout: false,
        output_config: OutputConfig::default(),
        fps_limit: 60.0,
        layout_generation: 1,
    });
    let state = ProjectState {
        project: project.clone(),
        selected_scene_instance: GridLocation::default(),
        blackout: false,
        beats_per_minute: 120.0,
        beat_progression: 0.5,
    };
    let fanned: Vec<_> = (0..PROJECT_STATE_SUBSCRIBERS)
        .map(|_| state.clone())
        .collect();
    let shared = Arc::new(EngineState {
        project: project.clone(),
        ..EngineState::default()
    });
    let messages = outbox.messages(ClientState {
        shared,
        applied_sequence: 0,
        assets: assets.clone(),
        preview: None,
        sound: Arc::default(),
        input_log: Arc::default(),
        midi_log: Arc::default(),
        learn_log: Arc::default(),
    });
    drop((state, fanned, messages));
}

/// The median tick, which a busy machine disturbs less than the mean.
fn measure(fader: bool) -> Duration {
    let mut project = Some(Arc::new(large_project()));
    let mut outbox = Outbox::default();
    let assets = Arc::default();
    let mut rendered = None;
    tick(&mut project, &assets, &mut outbox, &mut rendered, None);
    let mut ticks = Vec::with_capacity(TICKS as usize);
    for index in 0..TICKS {
        let start = Instant::now();
        tick(
            &mut project,
            &assets,
            &mut outbox,
            &mut rendered,
            fader.then_some(index as f32 / TICKS as f32),
        );
        ticks.push(start.elapsed());
    }
    ticks.sort();
    ticks[ticks.len() / 2]
}

#[test]
#[ignore = "a measurement, not a test"]
fn tick_bench() {
    let idle = measure(false);
    let fader = measure(true);
    println!("engine tick, project work (median): idle {idle:?}, one fader moving {fader:?}");
}