diff --git a/Cargo.toml b/Cargo.toml index 0b34269..e12f79e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ path = "src/main.rs" [features] default = ["wayland"] -wayland = ["dep:smithay", "dep:xkbcommon"] +wayland = ["dep:smithay"] profile_tokio = ["dep:console-subscriber", "tokio/tracing"] profile_app = ["dep:tracing-tracy"] @@ -59,7 +59,7 @@ serde_repr = "0.1.19" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } rand = "0.8.5" -xkbcommon = { version = "0.7.0", default-features = false, optional = true } +xkbcommon = { version = "0.7.0", default-features = false } libc = "0.2.155" input-event-codes = "6.2.0" nix = "0.29.0" diff --git a/src/core/eventloop.rs b/src/core/eventloop.rs deleted file mode 100644 index 4c6618b..0000000 --- a/src/core/eventloop.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::client::Client; -use super::task; -use color_eyre::eyre::Result; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::net::UnixListener; -use tokio::task::JoinHandle; -use tracing::error; - -pub struct EventLoop { - join_handle: JoinHandle<()>, -} - -impl EventLoop { - pub fn new(socket_path: PathBuf) -> Result> { - let socket = UnixListener::bind(socket_path)?; - - let join_handle = task::new(|| "event loop", async move { - loop { - let Ok((socket, _)) = socket.accept().await else { continue }; - if let Err(e) = Client::from_connection(socket) { - error!(?e, "Unable to create client from connection"); - } - } - })?; - let event_loop = Arc::new(EventLoop { join_handle }); - - Ok(event_loop) - } -} - -impl Drop for EventLoop { - fn drop(&mut self) { - self.join_handle.abort(); - } -} diff --git a/src/core/mod.rs b/src/core/mod.rs index a3d2763..93931c2 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -2,7 +2,6 @@ pub mod client; pub mod client_state; pub mod delta; pub mod destroy_queue; -pub mod eventloop; pub mod idl_utils; pub mod registry; pub mod resource; diff --git a/src/main.rs b/src/main.rs index a56ecb2..98c17f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,42 +2,34 @@ mod core; mod nodes; mod objects; +mod session; #[cfg(feature = "wayland")] mod wayland; -use crate::core::client::CLIENTS; use crate::core::destroy_queue; use crate::nodes::items::camera; use crate::nodes::{audio, drawable, hmd, input}; -use crate::objects::input::eye_pointer::EyePointer; -use crate::objects::input::mouse_pointer::MousePointer; -use crate::objects::input::sk_controller::SkController; -use crate::objects::input::sk_hand::SkHand; -use crate::objects::play_space::PlaySpace; -use self::core::eventloop::EventLoop; use clap::Parser; -use core::client_state::ClientStateParsed; +use core::client::Client; +use core::task; use directories::ProjectDirs; +use objects::ServerObjects; use once_cell::sync::OnceCell; +use session::{launch_start, save_session}; use stardust_xr::server; -use std::os::unix::fs::PermissionsExt; -use std::path::{Path, PathBuf}; -use std::process::{Child, Command, Stdio}; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use stereokit_rust::material::Material; use stereokit_rust::shader::Shader; -use stereokit_rust::sk::{ - sk_quit, AppMode, DepthMode, DisplayBlend, DisplayMode, QuitReason, SkSettings, -}; -use stereokit_rust::system::{Handed, LogLevel, Renderer, World}; +use stereokit_rust::sk::{sk_quit, AppMode, DepthMode, DisplayBlend, QuitReason, SkSettings}; +use stereokit_rust::system::{LogLevel, Renderer}; use stereokit_rust::tex::{SHCubemap, Tex, TexFormat, TexType}; use stereokit_rust::ui::Ui; -use stereokit_rust::util::{Color128, Device, Time}; +use stereokit_rust::util::{Color128, Time}; +use tokio::net::UnixListener; use tokio::sync::Notify; -use tokio::task::LocalSet; -use tokio::{runtime::Handle, sync::oneshot}; use tracing::metadata::LevelFilter; use tracing::{debug_span, error, info}; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; @@ -63,14 +55,12 @@ struct CliArgs { } static STARDUST_INSTANCE: OnceCell = OnceCell::new(); -static STOP_NOTIFIER: Notify = Notify::const_new(); -struct EventLoopInfo { - tokio_handle: Handle, - socket_path: PathBuf, -} +// #[tokio::main] +#[tokio::main(flavor = "current_thread")] +async fn main() { + color_eyre::install().unwrap(); -fn main() { let registry = tracing_subscriber::registry(); #[cfg(feature = "profile_app")] @@ -88,15 +78,80 @@ fn main() { .with_filter(EnvFilter::from_default_env()); registry.with(log_layer).init(); + let cli_args = Arc::new(CliArgs::parse()); + + let socket_path = + server::get_free_socket_path().expect("Unable to find a free stardust socket path"); + STARDUST_INSTANCE.set(socket_path.file_name().unwrap().to_string_lossy().into_owned()).expect("Someone hasn't done their job, yell at Nova because how is this set multiple times what the hell"); + info!( + socket_path = ?socket_path.display(), + "Stardust socket created" + ); + let socket = + UnixListener::bind(socket_path).expect("Couldn't spawn stardust server at {socket_path}"); + task::new(|| "client join loop", async move { + loop { + let Ok((stream, _)) = socket.accept().await else { + continue; + }; + if let Err(e) = Client::from_connection(stream) { + error!(?e, "Unable to create client from connection"); + } + } + }) + .unwrap(); + info!("Init client join loop"); + let project_dirs = ProjectDirs::from("", "", "stardust"); if project_dirs.is_none() { error!("Unable to get Stardust project directories, default skybox and startup script will not work."); } - let cli_args = Arc::new(CliArgs::parse()); + let sk_ready_notifier = Arc::new(Notify::new()); + let stereokit_loop = tokio::task::spawn_blocking({ + let sk_ready_notifier = sk_ready_notifier.clone(); + let project_dirs = project_dirs.clone(); + let flatscreen = cli_args.flatscreen; + let overlay_priority = cli_args.overlay_priority; + move || { + stereokit_loop( + sk_ready_notifier, + project_dirs, + flatscreen, + overlay_priority, + ) + } + }); + sk_ready_notifier.notified().await; + let mut startup_children = project_dirs + .as_ref() + .map(|project_dirs| launch_start(&cli_args, project_dirs)) + .unwrap_or_default(); + + tokio::select! { + _ = stereokit_loop => (), + _ = tokio::signal::ctrl_c() => unsafe {sk_quit(QuitReason::SystemClose)}, + } + println!("Stopping..."); + if let Some(project_dirs) = project_dirs { + save_session(&project_dirs).await; + } + for mut startup_child in startup_children.drain(..) { + let _ = startup_child.kill(); + } + + info!("Cleanly shut down Stardust"); +} + +fn stereokit_loop( + sk_ready_notifier: Arc, + project_dirs: Option, + intentional_flatscreen: bool, + overlay_priority: Option, +) { let sk = SkSettings::default() .app_name("Stardust XR") - .mode(if cli_args.flatscreen { + .mode(if intentional_flatscreen { AppMode::Simulator } else { AppMode::XR @@ -112,8 +167,8 @@ fn main() { Some(LevelFilter::OFF) => LogLevel::None, None => LogLevel::Warning, }) - .overlay_app(cli_args.overlay_priority.is_some()) - .overlay_priority(cli_args.overlay_priority.unwrap_or(u32::MAX)) + .overlay_app(overlay_priority.is_some()) + .overlay_priority(overlay_priority.unwrap_or(u32::MAX)) .disable_desktop_input_window(true) .render_scaling(2.0) .init() @@ -144,60 +199,12 @@ fn main() { } } - let mut mouse_pointer = cli_args - .flatscreen - .then(MousePointer::new) - .transpose() - .unwrap(); - let mut hands = (!cli_args.flatscreen) - .then(|| { - let left = SkHand::new(Handed::Left).ok(); - let right = SkHand::new(Handed::Right).ok(); - left.zip(right) - }) - .flatten(); - let mut controllers = (!cli_args.flatscreen) - .then(|| { - let left = SkController::new(Handed::Left).ok(); - let right = SkController::new(Handed::Right).ok(); - left.zip(right) - }) - .flatten(); - let eye_pointer = (sk.get_active_display_mode() == DisplayMode::MixedReality - && Device::has_eye_gaze()) - .then(EyePointer::new) - .transpose() - .unwrap(); - - let play_space = World::has_bounds().then(|| PlaySpace::new().ok()).flatten(); - - let (info_sender, info_receiver) = oneshot::channel::(); - let event_thread = std::thread::Builder::new() - .name("event_loop".to_owned()) - .spawn({ - let project_dirs = project_dirs.clone(); - move || event_loop(info_sender, project_dirs.clone()) - }) - .unwrap(); - let event_loop_info = info_receiver.blocking_recv().unwrap(); - let _tokio_handle = event_loop_info.tokio_handle.enter(); - #[cfg(feature = "wayland")] let mut wayland = wayland::Wayland::new().expect("Could not initialize wayland"); + sk_ready_notifier.notify_waiters(); info!("Stardust ready!"); - let mut startup_children = project_dirs - .as_ref() - .map(|project_dirs| { - launch_start( - &cli_args, - project_dirs, - &event_loop_info, - #[cfg(feature = "wayland")] - &wayland, - ) - }) - .unwrap_or_default(); + let mut objects = ServerObjects::new(intentional_flatscreen, &sk); let mut last_frame_delta = Duration::ZERO; let mut sleep_duration = Duration::ZERO; @@ -212,23 +219,7 @@ fn main() { wayland.frame_event(); destroy_queue::clear(); - if let Some(mouse_pointer) = &mut mouse_pointer { - mouse_pointer.update(); - } - if let Some((left_hand, right_hand)) = &mut hands { - left_hand.update(&sk, token); - right_hand.update(&sk, token); - } - if let Some((left_controller, right_controller)) = &mut controllers { - left_controller.update(token); - right_controller.update(token); - } - if let Some(eye_pointer) = &eye_pointer { - eye_pointer.update(); - } - if let Some(play_space) = &play_space { - play_space.update(); - } + objects.update(&sk, token); input::process_input(); nodes::root::Root::send_frame_events(Time::get_step_unscaled()); adaptive_sleep( @@ -249,17 +240,6 @@ fn main() { info!("Cleanly shut down StereoKit"); #[cfg(feature = "wayland")] drop(wayland); - - STOP_NOTIFIER.notify_waiters(); - event_thread - .join() - .expect("Failed to cleanly shut down event loop") - .unwrap(); - for mut startup_child in startup_children.drain(..) { - let _ = startup_child.kill(); - } - - info!("Cleanly shut down Stardust"); } fn adaptive_sleep( @@ -283,153 +263,3 @@ fn adaptive_sleep( std::thread::sleep(*sleep_duration); // to give clients a chance to even update anything before drawing }); } - -// #[tokio::main] -#[tokio::main(flavor = "current_thread")] -async fn event_loop( - info_sender: oneshot::Sender, - project_dirs: Option, -) -> color_eyre::eyre::Result<()> { - let socket_path = - server::get_free_socket_path().expect("Unable to find a free stardust socket path"); - STARDUST_INSTANCE.set(socket_path.file_name().unwrap().to_string_lossy().into_owned()).expect("Someone hasn't done their job, yell at Nova because how is this set multiple times what the hell"); - let _event_loop = EventLoop::new(socket_path.clone()).expect("Couldn't create server socket"); - info!("Init event loop"); - info!( - socket_path = ?socket_path.display(), - "Stardust socket created" - ); - let _ = info_sender.send(EventLoopInfo { - tokio_handle: Handle::current(), - socket_path, - }); - - STOP_NOTIFIER.notified().await; - println!("Stopping..."); - if let Some(project_dirs) = project_dirs { - save_session(&project_dirs).await; - } - - info!("Cleanly shut down event loop"); - - unsafe { - sk_quit(QuitReason::SystemClose); - } - - Ok(()) -} - -fn launch_start( - cli_args: &CliArgs, - project_dirs: &ProjectDirs, - event_loop_info: &EventLoopInfo, - #[cfg(feature = "wayland")] wayland: &wayland::Wayland, -) -> Vec { - if let Some(session_id) = &cli_args.restore { - let session_dir = project_dirs.state_dir().unwrap().join(session_id); - return restore_session(&session_dir, event_loop_info, wayland); - } - let startup_script_path = cli_args - .startup_script - .clone() - .and_then(|p| p.canonicalize().ok()) - .unwrap_or_else(|| project_dirs.config_dir().join("startup")); - run_script(&startup_script_path, event_loop_info, wayland) -} - -fn restore_session( - session_dir: &Path, - event_loop_info: &EventLoopInfo, - #[cfg(feature = "wayland")] wayland: &wayland::Wayland, -) -> Vec { - let Ok(clients) = session_dir.read_dir() else { - return Vec::new(); - }; - clients - .filter_map(Result::ok) - .filter_map(|c| ClientStateParsed::from_file(&c.path())) - .filter_map(ClientStateParsed::launch_command) - .filter_map(|startup_command| { - run_client( - startup_command, - event_loop_info, - #[cfg(feature = "wayland")] - wayland, - ) - }) - .collect() -} - -fn run_script( - script_path: &Path, - event_loop_info: &EventLoopInfo, - #[cfg(feature = "wayland")] wayland: &wayland::Wayland, -) -> Vec { - let _ = std::fs::set_permissions(script_path, std::fs::Permissions::from_mode(0o755)); - let startup_command = Command::new(script_path); - run_client( - startup_command, - event_loop_info, - #[cfg(feature = "wayland")] - wayland, - ) - .map(|c| vec![c]) - .unwrap_or_default() -} - -fn run_client( - mut command: Command, - event_loop_info: &EventLoopInfo, - #[cfg(feature = "wayland")] wayland: &wayland::Wayland, -) -> Option { - command.stdin(Stdio::null()); - command.stdout(Stdio::null()); - command.stderr(Stdio::null()); - command.env( - "FLAT_WAYLAND_DISPLAY", - std::env::var_os("WAYLAND_DISPLAY").unwrap_or_default(), - ); - command.env( - "STARDUST_INSTANCE", - event_loop_info - .socket_path - .file_name() - .expect("Stardust socket path not found"), - ); - #[cfg(feature = "wayland")] - { - if let Some(wayland_socket) = wayland.socket_name.as_ref() { - command.env("WAYLAND_DISPLAY", wayland_socket); - } - command.env("GDK_BACKEND", "wayland"); - command.env("QT_QPA_PLATFORM", "wayland"); - command.env("MOZ_ENABLE_WAYLAND", "1"); - command.env("CLUTTER_BACKEND", "wayland"); - command.env("SDL_VIDEODRIVER", "wayland"); - } - let child = command.spawn().ok()?; - Some(child) -} - -async fn save_session(project_dirs: &ProjectDirs) { - let session_id = nanoid::nanoid!(); - let state_dir = project_dirs.state_dir().unwrap(); - let session_dir = state_dir.join(&session_id); - std::fs::create_dir_all(&session_dir).unwrap(); - let _ = std::fs::remove_dir_all(state_dir.join("latest")); - std::os::unix::fs::symlink(&session_dir, state_dir.join("latest")).unwrap(); - - let local_set = LocalSet::new(); - for client in CLIENTS.get_vec() { - let session_dir = session_dir.clone(); - local_set.spawn_local(async move { - tokio::select! { - biased; - s = client.save_state() => {if let Some(s) = s { s.to_file(&session_dir) }}, - _ = tokio::time::sleep(Duration::from_millis(100)) => (), - } - }); - } - local_set.await; - println!("Session ID for restore is {session_id}"); -} diff --git a/src/nodes/drawable/mod.rs b/src/nodes/drawable/mod.rs index 4b6c6f7..43b2c64 100644 --- a/src/nodes/drawable/mod.rs +++ b/src/nodes/drawable/mod.rs @@ -1,5 +1,7 @@ pub mod lines; pub mod model; +#[cfg(feature = "wayland")] +pub mod shader_manipulation; pub mod shaders; pub mod text; diff --git a/src/nodes/drawable/shader_manipulation.rs b/src/nodes/drawable/shader_manipulation.rs new file mode 100644 index 0000000..bfcbad0 --- /dev/null +++ b/src/nodes/drawable/shader_manipulation.rs @@ -0,0 +1,137 @@ +#![allow(dead_code)] +use smithay::backend::renderer::gles::{ + ffi::{self, Gles2, FRAGMENT_SHADER, VERTEX_SHADER}, + GlesError, +}; +use stereokit_rust::shader::{Shader, _ShaderT}; +use tracing::error; + +struct FfiAssetHeader { + asset_type: i32, + asset_state: i32, + id: u64, + index: u64, + refs: i32, + debug: *mut u8, +} + +struct FfiSkgShader { + meta: *mut u8, + vertex: u32, + pixel: u32, + program: u32, + compute: u32, +} + +struct FfiShader { + header: FfiAssetHeader, + shader: FfiSkgShader, +} + +unsafe fn compile_shader( + gl: &ffi::Gles2, + variant: ffi::types::GLuint, + src: &str, +) -> Result { + let shader = gl.CreateShader(variant); + if shader == 0 { + return Err(GlesError::CreateShaderObject); + } + + gl.ShaderSource( + shader, + 1, + &src.as_ptr() as *const *const u8 as *const *const ffi::types::GLchar, + &(src.len() as i32) as *const _, + ); + gl.CompileShader(shader); + + let mut status = ffi::FALSE as i32; + gl.GetShaderiv(shader, ffi::COMPILE_STATUS, &mut status as *mut _); + if status == ffi::FALSE as i32 { + let mut max_len = 0; + gl.GetShaderiv(shader, ffi::INFO_LOG_LENGTH, &mut max_len as *mut _); + + let mut error = Vec::with_capacity(max_len as usize); + let mut len = 0; + gl.GetShaderInfoLog( + shader, + max_len as _, + &mut len as *mut _, + error.as_mut_ptr() as *mut _, + ); + error.set_len(len as usize); + + error!( + "[GL] {}", + std::str::from_utf8(&error).unwrap_or("") + ); + + gl.DeleteShader(shader); + return Err(GlesError::ShaderCompileError); + } + + Ok(shader) +} + +unsafe fn link_program( + gl: &ffi::Gles2, + vert: ffi::types::GLuint, + frag: ffi::types::GLuint, +) -> Result { + let program = gl.CreateProgram(); + gl.AttachShader(program, vert); + gl.AttachShader(program, frag); + gl.LinkProgram(program); + // gl.DetachShader(program, vert); + // gl.DetachShader(program, frag); + // gl.DeleteShader(vert); + // gl.DeleteShader(frag); + + let mut status = ffi::FALSE as i32; + gl.GetProgramiv(program, ffi::LINK_STATUS, &mut status as *mut _); + if status == ffi::FALSE as i32 { + let mut max_len = 0; + gl.GetProgramiv(program, ffi::INFO_LOG_LENGTH, &mut max_len as *mut _); + + let mut error = Vec::with_capacity(max_len as usize); + let mut len = 0; + gl.GetProgramInfoLog( + program, + max_len as _, + &mut len as *mut _, + error.as_mut_ptr() as *mut _, + ); + error.set_len(len as usize); + + error!( + "[GL] {}", + std::str::from_utf8(&error).unwrap_or("") + ); + + gl.DeleteProgram(program); + return Err(GlesError::ProgramLinkError); + } + + Ok(program) +} + +pub unsafe fn shader_inject( + c: &Gles2, + sk_shader: &mut Shader, + vert_str: &str, + frag_str: &str, +) -> Result<(), GlesError> { + let gl_vert = compile_shader(c, VERTEX_SHADER, vert_str)?; + let gl_frag = compile_shader(c, FRAGMENT_SHADER, frag_str)?; + let gl_prog = link_program(c, gl_vert, gl_frag)?; + + let shader = sk_shader.0.as_mut() as *mut _ShaderT as *mut FfiShader; + if let Some(shader) = shader.as_mut() { + shader.shader.vertex = gl_vert; + shader.shader.pixel = gl_frag; + shader.shader.program = gl_prog; + } + + Ok(()) +} diff --git a/src/nodes/drawable/shaders.rs b/src/nodes/drawable/shaders.rs index 0e2b272..2021453 100644 --- a/src/nodes/drawable/shaders.rs +++ b/src/nodes/drawable/shaders.rs @@ -1,143 +1,5 @@ -#![allow(dead_code)] -use smithay::backend::renderer::gles::{ - ffi::{self, Gles2, FRAGMENT_SHADER, VERTEX_SHADER}, - GlesError, -}; -use stereokit_rust::shader::{Shader, _ShaderT}; -use tracing::error; - // Simula shader with fancy lanzcos sampling pub const UNLIT_SHADER_BYTES: &[u8] = include_bytes!("assets/shaders/shader_unlit_gamma.hlsl.sks"); // Simula shader with fancy lanzcos sampling pub const PANEL_SHADER_BYTES: &[u8] = include_bytes!("assets/shaders/shader_unlit_simula.hlsl.sks"); - -struct FfiAssetHeader { - asset_type: i32, - asset_state: i32, - id: u64, - index: u64, - refs: i32, - debug: *mut u8, -} - -struct FfiSkgShader { - meta: *mut u8, - vertex: u32, - pixel: u32, - program: u32, - compute: u32, -} - -struct FfiShader { - header: FfiAssetHeader, - shader: FfiSkgShader, -} - -unsafe fn compile_shader( - gl: &ffi::Gles2, - variant: ffi::types::GLuint, - src: &str, -) -> Result { - let shader = gl.CreateShader(variant); - if shader == 0 { - return Err(GlesError::CreateShaderObject); - } - - gl.ShaderSource( - shader, - 1, - &src.as_ptr() as *const *const u8 as *const *const ffi::types::GLchar, - &(src.len() as i32) as *const _, - ); - gl.CompileShader(shader); - - let mut status = ffi::FALSE as i32; - gl.GetShaderiv(shader, ffi::COMPILE_STATUS, &mut status as *mut _); - if status == ffi::FALSE as i32 { - let mut max_len = 0; - gl.GetShaderiv(shader, ffi::INFO_LOG_LENGTH, &mut max_len as *mut _); - - let mut error = Vec::with_capacity(max_len as usize); - let mut len = 0; - gl.GetShaderInfoLog( - shader, - max_len as _, - &mut len as *mut _, - error.as_mut_ptr() as *mut _, - ); - error.set_len(len as usize); - - error!( - "[GL] {}", - std::str::from_utf8(&error).unwrap_or("") - ); - - gl.DeleteShader(shader); - return Err(GlesError::ShaderCompileError); - } - - Ok(shader) -} - -unsafe fn link_program( - gl: &ffi::Gles2, - vert: ffi::types::GLuint, - frag: ffi::types::GLuint, -) -> Result { - let program = gl.CreateProgram(); - gl.AttachShader(program, vert); - gl.AttachShader(program, frag); - gl.LinkProgram(program); - // gl.DetachShader(program, vert); - // gl.DetachShader(program, frag); - // gl.DeleteShader(vert); - // gl.DeleteShader(frag); - - let mut status = ffi::FALSE as i32; - gl.GetProgramiv(program, ffi::LINK_STATUS, &mut status as *mut _); - if status == ffi::FALSE as i32 { - let mut max_len = 0; - gl.GetProgramiv(program, ffi::INFO_LOG_LENGTH, &mut max_len as *mut _); - - let mut error = Vec::with_capacity(max_len as usize); - let mut len = 0; - gl.GetProgramInfoLog( - program, - max_len as _, - &mut len as *mut _, - error.as_mut_ptr() as *mut _, - ); - error.set_len(len as usize); - - error!( - "[GL] {}", - std::str::from_utf8(&error).unwrap_or("") - ); - - gl.DeleteProgram(program); - return Err(GlesError::ProgramLinkError); - } - - Ok(program) -} - -pub unsafe fn shader_inject( - c: &Gles2, - sk_shader: &mut Shader, - vert_str: &str, - frag_str: &str, -) -> Result<(), GlesError> { - let gl_vert = compile_shader(c, VERTEX_SHADER, vert_str)?; - let gl_frag = compile_shader(c, FRAGMENT_SHADER, frag_str)?; - let gl_prog = link_program(c, gl_vert, gl_frag)?; - - let shader = sk_shader.0.as_mut() as *mut _ShaderT as *mut FfiShader; - if let Some(shader) = shader.as_mut() { - shader.shader.vertex = gl_vert; - shader.shader.pixel = gl_frag; - shader.shader.program = gl_prog; - } - - Ok(()) -} diff --git a/src/nodes/root.rs b/src/nodes/root.rs index 9e313db..6ff8950 100644 --- a/src/nodes/root.rs +++ b/src/nodes/root.rs @@ -4,12 +4,9 @@ use crate::core::client::Client; use crate::core::client_state::ClientStateParsed; use crate::core::registry::Registry; use crate::nodes::spatial::SPATIAL_REF_ASPECT_ALIAS_INFO; -#[cfg(feature = "wayland")] -use crate::wayland::WAYLAND_DISPLAY; -use crate::STARDUST_INSTANCE; +use crate::session::connection_env; use color_eyre::eyre::{bail, Result}; use glam::Mat4; -use rustc_hash::FxHashMap; use std::path::PathBuf; use std::sync::Arc; use tracing::info; @@ -61,25 +58,7 @@ impl RootAspect for Root { _node: Arc, _calling_client: Arc, ) -> Result> { - macro_rules! var_env_insert { - ($env:ident, $name:ident) => { - $env.insert(stringify!($name).to_string(), $name.get().unwrap().clone()); - }; - } - - let mut env: FxHashMap = FxHashMap::default(); - var_env_insert!(env, STARDUST_INSTANCE); - #[cfg(feature = "wayland")] - { - var_env_insert!(env, WAYLAND_DISPLAY); - env.insert("GDK_BACKEND".to_string(), "wayland".to_string()); - env.insert("QT_QPA_PLATFORM".to_string(), "wayland".to_string()); - env.insert("MOZ_ENABLE_WAYLAND".to_string(), "1".to_string()); - env.insert("CLUTTER_BACKEND".to_string(), "wayland".to_string()); - env.insert("SDL_VIDEODRIVER".to_string(), "wayland".to_string()); - } - - Ok(env) + Ok(connection_env()) } #[doc = "Generate a client state token and return it back.\n\n When launching a new client, set the environment variable `STARDUST_STARTUP_TOKEN` to the returned string.\n Make sure the environment variable shows in `/proc/{pid}/environ` as that's the only reliable way to pass the value to the server (suggestions welcome).\n"] diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 77ba00e..e0926b7 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -1,2 +1,71 @@ +use input::{ + eye_pointer::EyePointer, mouse_pointer::MousePointer, sk_controller::SkController, + sk_hand::SkHand, +}; +use play_space::PlaySpace; +use stereokit_rust::{ + sk::{DisplayMode, MainThreadToken, Sk}, + system::{Handed, World}, + util::Device, +}; + pub mod input; pub mod play_space; + +pub struct ServerObjects { + mouse_pointer: Option, + hands: Option<(SkHand, SkHand)>, + controllers: Option<(SkController, SkController)>, + eye_pointer: Option, + play_space: Option, +} +impl ServerObjects { + pub fn new(intentional_flatscreen: bool, sk: &Sk) -> ServerObjects { + ServerObjects { + mouse_pointer: intentional_flatscreen + .then(MousePointer::new) + .transpose() + .unwrap(), + hands: (!intentional_flatscreen) + .then(|| { + let left = SkHand::new(Handed::Left).ok(); + let right = SkHand::new(Handed::Right).ok(); + left.zip(right) + }) + .flatten(), + controllers: (!intentional_flatscreen) + .then(|| { + let left = SkController::new(Handed::Left).ok(); + let right = SkController::new(Handed::Right).ok(); + left.zip(right) + }) + .flatten(), + eye_pointer: (sk.get_active_display_mode() == DisplayMode::MixedReality + && Device::has_eye_gaze()) + .then(EyePointer::new) + .transpose() + .unwrap(), + play_space: World::has_bounds().then(|| PlaySpace::new().ok()).flatten(), + } + } + + pub fn update(&mut self, sk: &Sk, token: &MainThreadToken) { + if let Some(mouse_pointer) = self.mouse_pointer.as_mut() { + mouse_pointer.update(); + } + if let Some((left_hand, right_hand)) = self.hands.as_mut() { + left_hand.update(&sk, token); + right_hand.update(&sk, token); + } + if let Some((left_controller, right_controller)) = self.controllers.as_mut() { + left_controller.update(token); + right_controller.update(token); + } + if let Some(eye_pointer) = self.eye_pointer.as_ref() { + eye_pointer.update(); + } + if let Some(play_space) = self.play_space.as_ref() { + play_space.update(); + } + } +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..77b2f64 --- /dev/null +++ b/src/session.rs @@ -0,0 +1,105 @@ +use crate::core::client::CLIENTS; +use crate::core::client_state::ClientStateParsed; +use crate::wayland::WAYLAND_DISPLAY; +use crate::{CliArgs, STARDUST_INSTANCE}; +use directories::ProjectDirs; +use rustc_hash::FxHashMap; +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +use std::process::{Child, Command, Stdio}; +use std::time::Duration; +use tokio::task::LocalSet; + +pub async fn save_session(project_dirs: &ProjectDirs) { + let session_id = nanoid::nanoid!(); + let state_dir = project_dirs.state_dir().unwrap(); + let session_dir = state_dir.join(&session_id); + std::fs::create_dir_all(&session_dir).unwrap(); + let _ = std::fs::remove_dir_all(state_dir.join("latest")); + std::os::unix::fs::symlink(&session_dir, state_dir.join("latest")).unwrap(); + + let local_set = LocalSet::new(); + for client in CLIENTS.get_vec() { + let session_dir = session_dir.clone(); + local_set.spawn_local(async move { + tokio::select! { + biased; + s = client.save_state() => {if let Some(s) = s { s.to_file(&session_dir) }}, + _ = tokio::time::sleep(Duration::from_millis(100)) => (), + } + }); + } + local_set.await; + println!("Session ID for restore is {session_id}"); +} + +pub fn launch_start(cli_args: &CliArgs, project_dirs: &ProjectDirs) -> Vec { + match (&cli_args.restore, &cli_args.startup_script) { + (Some(session_id), _) => { + restore_session(&project_dirs.state_dir().unwrap().join(session_id)) + } + (None, Some(startup_script)) => { + run_script(&startup_script.clone().canonicalize().unwrap_or_default()) + } + (None, None) => run_script(&project_dirs.config_dir().join("startup")), + } +} + +pub fn restore_session(session_dir: &Path) -> Vec { + let Ok(clients) = session_dir.read_dir() else { + return Vec::new(); + }; + clients + .filter_map(Result::ok) + .filter_map(|c| ClientStateParsed::from_file(&c.path())) + .filter_map(ClientStateParsed::launch_command) + .filter_map(|startup_command| run_client(startup_command)) + .collect() +} + +pub fn run_script(script_path: &Path) -> Vec { + let _ = std::fs::set_permissions(script_path, std::fs::Permissions::from_mode(0o755)); + let startup_command = Command::new(script_path); + run_client(startup_command) + .map(|c| vec![c]) + .unwrap_or_default() +} + +pub fn run_client(mut command: Command) -> Option { + command.stdin(Stdio::null()); + command.stdout(Stdio::null()); + command.stderr(Stdio::null()); + for (var, value) in connection_env() { + command.env(var, value); + } + let child = command.spawn().ok()?; + Some(child) +} + +pub fn connection_env() -> FxHashMap { + macro_rules! var_env_insert { + ($env:ident, $name:ident) => { + $env.insert(stringify!($name).to_string(), $name.get().unwrap().clone()); + }; + } + + let mut env: FxHashMap = FxHashMap::default(); + var_env_insert!(env, STARDUST_INSTANCE); + + if let Some(flat_wayland_display) = std::env::var_os("WAYLAND_DISPLAY") { + env.insert( + "FLAT_WAYLAND_DISPLAY".to_string(), + flat_wayland_display.to_string_lossy().into_owned(), + ); + } + #[cfg(feature = "wayland")] + { + var_env_insert!(env, WAYLAND_DISPLAY); + env.insert("GDK_BACKEND".to_string(), "wayland".to_string()); + env.insert("QT_QPA_PLATFORM".to_string(), "wayland".to_string()); + env.insert("MOZ_ENABLE_WAYLAND".to_string(), "1".to_string()); + env.insert("CLUTTER_BACKEND".to_string(), "wayland".to_string()); + env.insert("SDL_VIDEODRIVER".to_string(), "wayland".to_string()); + } + env +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 5aa9759..1531d24 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -24,7 +24,7 @@ use smithay::reexports::wayland_server::DisplayHandle; use smithay::reexports::wayland_server::{Display, ListeningSocket}; use smithay::wayland::dmabuf; use std::ffi::OsStr; -use std::os::fd::OwnedFd; +use std::os::fd::{IntoRawFd, OwnedFd}; use std::os::unix::prelude::AsRawFd; use std::{ ffi::c_void, @@ -32,6 +32,7 @@ use std::{ sync::Arc, }; use stereokit_rust::system::{Backend, BackendGraphics}; +use tokio::io::unix::AsyncFdReadyGuard; use tokio::sync::mpsc::UnboundedReceiver; use tokio::{ io::unix::AsyncFd, net::UnixListener as AsyncUnixListener, sync::mpsc, task::JoinHandle, @@ -78,6 +79,18 @@ impl DisplayWrapper { } } +struct UnownedFd(Option>); +impl UnownedFd { + async fn readable(&self) -> std::io::Result> { + self.0.as_ref().unwrap().readable().await + } +} +impl Drop for UnownedFd { + fn drop(&mut self) { + self.0.take().unwrap().into_inner().into_raw_fd(); + } +} + pub struct Wayland { display: Arc, pub socket_name: Option, @@ -137,7 +150,7 @@ impl Wayland { AsyncUnixListener::from_std(unsafe { UnixListener::from_raw_fd(socket.as_raw_fd()) })?; let dispatch_poll_fd = display.poll_fd()?; - let dispatch_poll_listener = AsyncFd::new(dispatch_poll_fd)?; + let dispatch_poll_listener = UnownedFd(Some(AsyncFd::new(dispatch_poll_fd)?)); let dh1 = display.handle(); let mut dh2 = dh1.clone(); diff --git a/src/wayland/xdg_shell.rs b/src/wayland/xdg_shell.rs index 00ccf71..e0ded36 100644 --- a/src/wayland/xdg_shell.rs +++ b/src/wayland/xdg_shell.rs @@ -195,7 +195,6 @@ impl XdgShellHandler for WaylandState { return; } utils::insert_data(popup.wl_surface(), SurfaceId::Child(uid)); - utils::insert_data(popup.wl_surface(), uid); utils::insert_data(popup.wl_surface(), Arc::downgrade(&panel_item)); CoreSurface::add_to( popup.wl_surface(),