gled 2.32.1

gled is an application for creating animations and effects on artnet or dmx installations
#![recursion_limit = "256"]
#![windows_subsystem = "windows"]
#![forbid(unsafe_code)]
#![deny(clippy::unwrap_used)]
#![warn(clippy::todo)]
#![warn(missing_debug_implementations)]
#![allow(clippy::too_many_arguments, clippy::type_complexity)]

mod engine_link;
mod platform;
mod remote;
mod remote_link;

use eframe::egui_wgpu::{WgpuConfiguration, WgpuSetup, WgpuSetupCreateNew};
use gled_ui::{
    app::App, extension::AppExtension, link::EngineLink,
    ui::window_common::default_viewport_builder,
};
use std::sync::Arc;
use wgpu::PresentMode;

/// gled: LED animations rendered on the GPU, sent over Art-Net and DMX.
#[derive(Debug, clap::Parser)]
#[command(version, about)]
struct Args {
    /// Show OpenFont License
    #[arg(short = 's', long)]
    show_open_font_license: bool,
    /// Control the engine of gled-server or of another gled with remote
    /// access (e.g. gled.local, gled.local:8443) instead of running one
    #[arg(long, value_name = "SERVER")]
    connect: Option<String>,
    /// The fingerprint of the server's certificate, as the server shows it;
    /// remembered for the next start
    #[arg(long, value_name = "SHA256", requires = "connect")]
    fingerprint: Option<String>,
}

fn init_keyring_store() {
    #[cfg(target_os = "macos")]
    {
        let store = apple_native_keyring_store::keychain::Store::new()
            .expect("Could not create Apple keyring store");
        keyring_core::set_default_store(store);
    }
    #[cfg(target_os = "windows")]
    {
        let store = windows_native_keyring_store::Store::new()
            .expect("Could not create Windows keyring store");
        keyring_core::set_default_store(store);
    }
    #[cfg(target_os = "linux")]
    {
        let store = dbus_secret_service_keyring_store::Store::new()
            .expect("Could not create DBus Secret Service keyring store");
        keyring_core::set_default_store(store);
    }
}

fn main() {
    let args = <Args as clap::Parser>::parse();
    if args.show_open_font_license {
        println!("{}", gled_ui::style::OPEN_FONT_LICENSE);
        return;
    }

    #[cfg(feature = "profiling")]
    let _puffin_servers = start_profile_servers();
    tracing_subscriber::fmt::init();
    // An engine on another machine, or one in this process.
    let remote_url = args
        .connect
        .as_deref()
        .map(gled_server::client::websocket_url);
    let engine_start = match remote_url {
        Some(_) => {
            // The engine reports its machine's temperature; this one's too.
            gled_ui::ui::temperature::start_local_thread();
            None
        }
        None => {
            init_keyring_store();
            Some(gled_core::start::services().expect("Could not start gled's services"))
        }
    };

    #[cfg(not(debug_assertions))]
    gled_ui::ui::update_check::Update::start_thread();

    let mut wgpu_options = WgpuConfiguration::default();
    // The window is vsynced; the render and output rate is paced independently
    // on the render thread (see `app::render_thread`).
    wgpu_options.surface.present_mode = PresentMode::AutoVsync;
    wgpu_options.wgpu_setup = match wgpu_options.wgpu_setup {
        WgpuSetup::CreateNew(create_new) => WgpuSetup::CreateNew(WgpuSetupCreateNew {
            // The fastest GPU that can draw into the window, measured once
            // per set of GPUs.
            native_adapter_selector: Some(Arc::new(|adapters, surface| {
                gled_core::gpu_choice::choose(adapters, |adapter| {
                    surface.is_none_or(|surface| adapter.is_surface_supported(surface))
                })
                .ok_or_else(|| "No GPU can draw gled's window".to_owned())
            })),
            // What the GPU offers rather than egui's minimum: on OpenGL egui
            // asks for WebGL2 limits, which allow no storage buffers, and
            // the preview pipelines need them.
            device_descriptor: Arc::new(|adapter| wgpu::DeviceDescriptor {
                label: Some("gled window device"),
                #[cfg(feature = "profiling")]
                required_features: adapter.features()
                    & wgpu_profiler::GpuProfiler::ALL_WGPU_TIMER_FEATURES,
                required_limits: adapter.limits(),
                ..Default::default()
            }),
            ..create_new
        }),
        _ => unreachable!(),
    };

    let options = eframe::NativeOptions {
        viewport: default_viewport_builder()
            .with_inner_size([1300.0, 1024.0])
            .with_drag_and_drop(true)
            .with_min_inner_size([300.0, 200.0]),
        wgpu_options,
        dithering: false,
        ..Default::default()
    };
    eframe::run_native(
        "gled",
        options,
        Box::new(|cc| {
            gled_ui::style::apply(&cc.egui_ctx);

            cc.wgpu_render_state
                .as_ref()
                .expect("wgpu render state is not available")
                .device
                .on_uncaptured_error(Arc::new(|error| {
                    tracing::error!("WGPU error: {:?}", error);
                }));

            if !gled_ui::render_state::set(
                cc.wgpu_render_state
                    .clone()
                    .ok_or("wgpu render state is not available")?,
            ) {
                return Err("the app was started twice".into());
            }
            let settings = cc
                .storage
                .and_then(|storage| eframe::get_value(storage, gled_ui::settings::STORAGE_KEY))
                .unwrap_or_else(platform::previous_client_settings);
            let subscriptions = gled_protocol::message::Subscriptions {
                preview: true,
                sound: true,
                input_events: true,
                midi: true,
            };
            let (link, extension): (Box<dyn EngineLink>, Option<Box<dyn AppExtension>>) =
                match (engine_start, remote_url) {
                    (Some(engine_start), _) => {
                        let connector = engine_start
                            .spawn(gled_ui::wgpu_render_state().adapter)
                            .map_err(|err| err.to_string())?;
                        (
                            Box::new(engine_link::InProcessLink::new(connector.connect())),
                            Some(Box::new(remote::RemoteAccessExtension::new(connector))),
                        )
                    }
                    (None, Some(url)) => (
                        Box::new(remote_link::RemoteLink::new(
                            url,
                            args.fingerprint,
                            cc.egui_ctx.clone(),
                        )?),
                        None,
                    ),
                    (None, None) => return Err("neither an engine nor a server".into()),
                };
            let connection = gled_ui::link::Connection::new(link, subscriptions);
            Ok(Box::new(App::new(connection, settings, extension)))
        }),
    )
    .expect("Could not run native");
}

#[cfg(feature = "profiling")]
struct PuffinViewerChildGuard(std::process::Child);
#[cfg(feature = "profiling")]
impl Drop for PuffinViewerChildGuard {
    fn drop(&mut self) {
        match self.0.kill() {
            Err(e) => tracing::warn!("Could not kill puffin viewer process: {}", e),
            Ok(_) => tracing::info!("Successfully killed puffin viewer process"),
        }
    }
}

#[cfg(feature = "profiling")]
fn start_profile_servers() -> (
    puffin_http::Server,
    puffin_http::Server,
    PuffinViewerChildGuard,
    PuffinViewerChildGuard,
) {
    puffin::set_scopes_on(true);
    let cpu_server = puffin_http::Server::new(&format!("0.0.0.0:{}", puffin_http::DEFAULT_PORT))
        .expect("Could not start puffin server for cpu");
    let gpu_server = puffin_http::Server::new_custom(
        &format!("0.0.0.0:{}", puffin_http::DEFAULT_PORT + 1),
        |sink| {
            gled_core::runtime::PUFFIN_GPU_PROFILER
                .lock()
                .add_sink(sink)
        },
        |id| {
            _ = gled_core::runtime::PUFFIN_GPU_PROFILER
                .lock()
                .remove_sink(id)
        },
    )
    .expect("Could not start puffin server for gpu");
    let cpu_profiler =
        PuffinViewerChildGuard(std::process::Command::new("puffin_viewer").spawn().expect(
            "Could not run puffin_viewer, maybe install it with: \"cargo install puffin_viewer\"",
        ));
    let gpu_profiler = PuffinViewerChildGuard(std::process::Command::new("puffin_viewer").arg("--url").arg(format!("127.0.0.1:{}", puffin_http::DEFAULT_PORT + 1)).spawn().expect(
            "Could not run puffin_viewer, maybe install it with: \"cargo install puffin_viewer\"",
        ));
    (cpu_server, gpu_server, cpu_profiler, gpu_profiler)
}