Files
server/src/main.rs
Schmarni b1ab37c8dc Revert "fix: don't wait for frame"
This reverts commit 2b07345286.
2025-10-22 17:25:09 +02:00

524 lines
17 KiB
Rust

#![recursion_limit = "256"]
#![allow(clippy::empty_docs)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::type_complexity)]
mod core;
mod nodes;
mod objects;
mod session;
mod spectator_cam;
pub mod tracking_offset;
#[cfg(feature = "wayland")]
mod wayland;
use crate::nodes::drawable::sky::SkyPlugin;
use crate::nodes::input;
use crate::spectator_cam::SpectatorCameraPlugin;
use bevy::MinimalPlugins;
use bevy::a11y::AccessibilityPlugin;
use bevy::app::{App, ScheduleRunnerPlugin, TerminalCtrlCHandlerPlugin};
use bevy::asset::{AssetMetaCheck, UnapprovedPathMode};
use bevy::audio::AudioPlugin;
use bevy::core_pipeline::CorePipelinePlugin;
use bevy::core_pipeline::oit::OrderIndependentTransparencySettings;
use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::diagnostic::DiagnosticsPlugin;
use bevy::ecs::schedule::{ExecutorKind, ScheduleLabel};
use bevy::gizmos::GizmoPlugin;
use bevy::gltf::GltfPlugin;
use bevy::input::InputPlugin;
use bevy::pbr::PbrPlugin;
use bevy::render::pipelined_rendering::{
PipelinedRenderThreadOnCreateCallback, PipelinedRenderingPlugin,
};
use bevy::render::settings::{Backends, RenderCreation, WgpuSettings};
use bevy::render::{RenderDebugFlags, RenderPlugin};
use bevy::scene::ScenePlugin;
use bevy::window::{CompositeAlphaMode, PresentMode};
use bevy::winit::{WakeUp, WinitPlugin};
use bevy_dmabuf::import::DmabufImportPlugin;
use bevy_mod_openxr::action_set_attaching::OxrActionAttachingPlugin;
use bevy_mod_openxr::action_set_syncing::OxrActionSyncingPlugin;
use bevy_mod_openxr::add_xr_plugins;
use bevy_mod_openxr::exts::OxrExtensions;
use bevy_mod_openxr::features::overlay::OxrOverlaySettings;
use bevy_mod_openxr::graphics::{GraphicsBackend, OxrManualGraphicsConfig};
use bevy_mod_openxr::init::{OxrInitPlugin, should_run_frame_loop};
use bevy_mod_openxr::reference_space::OxrReferenceSpacePlugin;
use bevy_mod_openxr::render::{OxrRenderPlugin, OxrWaitFrameSystem};
use bevy_mod_openxr::resources::{OxrFrameState, OxrFrameWaiter, OxrSessionConfig};
use bevy_mod_openxr::types::AppInfo;
use bevy_mod_xr::camera::XrProjection;
use bevy_mod_xr::session::{XrFirst, XrHandleEvents, XrSessionPlugin};
use clap::Parser;
use core::client::{Client, tick_internal_client};
use core::entity_handle::EntityHandlePlugin;
use core::task;
use directories::ProjectDirs;
use nodes::audio::AudioNodePlugin;
use nodes::drawable::lines::LinesNodePlugin;
use nodes::drawable::model::ModelNodePlugin;
use nodes::drawable::text::TextNodePlugin;
use nodes::fields::FieldDebugGizmoPlugin;
use nodes::spatial::SpatialNodePlugin;
use objects::hmd::HmdPlugin;
use objects::input::mouse_pointer::FlatscreenInputPlugin;
use objects::input::oxr_controller::ControllerPlugin;
use objects::input::oxr_hand::HandPlugin;
use objects::play_space::PlaySpacePlugin;
use openxr::{EnvironmentBlendMode, ReferenceSpaceType};
use session::{launch_start, save_session};
use stardust_xr::schemas::dbus::object_registry::ObjectRegistry;
use stardust_xr::server::LockedSocket;
use std::ops::DerefMut as _;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, OnceLock};
use tokio::net::UnixListener;
use tokio::sync::Notify;
use tokio::task::JoinError;
use tracing::metadata::LevelFilter;
use tracing::{error, info};
use tracing_subscriber::filter::Directive;
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
use tracking_offset::TrackingOffsetPlugin;
use wayland::{Wayland, WaylandPlugin};
use zbus::Connection;
use zbus::fdo::ObjectManager;
use bevy::prelude::*;
#[derive(Debug, Clone, Parser)]
#[clap(author, version, about, long_about = None)]
struct CliArgs {
/// Force flatscreen mode and use the mouse pointer as a 3D pointer
#[clap(short, long, action)]
force_flatscreen: bool,
/// Replaces the flatscreen mode with a first person spectator camera
#[clap(short, long, action)]
spectator: bool,
/// Creates a transparent window fot the flatscreen mode
#[clap(short, long, action)]
transparent_flatscreen: bool,
/// If monado insists on emulating them, set this flag...we want the raw input
#[clap(long)]
disable_controllers: bool,
/// If monado insists on emulating , set this flag...we want the raw input
#[clap(long)]
disable_hands: bool,
/// Run Stardust XR as an overlay with given priority
#[clap(id = "PRIORITY", short = 'o', long = "overlay", action)]
overlay_priority: Option<u32>,
/// Debug the clients started by the server
#[clap(short = 'd', long = "debug", action)]
debug_launched_clients: bool,
/// Run a script when ready for clients to connect. If this is not set the script at $HOME/.config/stardust/startup will be ran if it exists.
#[clap(id = "PATH", short = 'e', long = "execute-startup-script", action)]
startup_script: Option<PathBuf>,
/// Restore the session with the given ID (or `latest`), ignoring the startup script. Sessions are stored in directories at `~/.local/state/stardust/`.
#[clap(id = "SESSION_ID", long = "restore", action)]
restore: Option<String>,
}
pub type BevyMaterial = StandardMaterial;
static STARDUST_INSTANCE: OnceLock<String> = OnceLock::new();
// #[tokio::main(flavor = "current_thread")]
#[tokio::main]
async fn main() -> Result<AppExit, JoinError> {
color_eyre::install().unwrap();
let registry = tracing_subscriber::registry();
#[cfg(feature = "profile_app")]
let registry = registry.with(
tracing_tracy::TracyLayer::new(tracing_tracy::DefaultConfig::default())
.with_filter(LevelFilter::DEBUG),
);
#[cfg(feature = "profile_tokio")]
let registry = registry.with(console_subscriber::spawn());
let log_layer = fmt::Layer::new()
.with_thread_names(true)
.with_ansi(true)
.with_line_number(true)
.with_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::WARN.into())
.from_env_lossy()
.add_directive(Directive::from_str("bevy_mesh_text_3d::text_glyphs=off").unwrap()),
);
registry.with(log_layer).init();
let cli_args = CliArgs::parse();
let locked_socket =
LockedSocket::get_free().expect("Unable to find a free stardust socket path");
STARDUST_INSTANCE.set(locked_socket.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 = ?locked_socket.socket_path.display(),
"Stardust socket created"
);
let socket = UnixListener::bind(locked_socket.socket_path)
.expect("Couldn't spawn stardust server at {socket_path}");
task::new(|| "Stardust socket accept 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 dbus_connection = Connection::session()
.await
.expect("Could not open dbus session");
// why is this requested here? should there be a specific server bus name that we check
// instead?
dbus_connection
.request_name("org.stardustxr.HMD")
.await
.expect(
"Another instance of the server is running. This is not supported currently (but is planned).",
);
dbus_connection
.object_server()
.at("/", ObjectManager)
.await
.expect("Couldn't add the object manager");
let object_registry = ObjectRegistry::new(&dbus_connection).await;
let _wayland = Wayland::new().expect("Couldn't create Wayland instance");
let ready_notifier = Arc::new(Notify::new());
let io_loop = tokio::task::spawn_blocking({
let ready_notifier = ready_notifier.clone();
let project_dirs = project_dirs.clone();
let cli_args = cli_args.clone();
let dbus_connection = dbus_connection.clone();
move || {
bevy_loop(
ready_notifier,
project_dirs,
cli_args,
dbus_connection,
object_registry,
)
}
});
ready_notifier.notified().await;
let mut startup_children = project_dirs
.as_ref()
.map(|project_dirs| launch_start(&cli_args, project_dirs))
.unwrap_or_default();
let return_value = io_loop.await;
info!("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");
return_value
}
// static DEFAULT_SKYTEX: OnceLock<Tex> = OnceLock::new();
// static DEFAULT_SKYLIGHT: OnceLock<SphericalHarmonics> = OnceLock::new();
#[derive(ScheduleLabel, Hash, Debug, PartialEq, Eq, Clone, Copy)]
pub struct PreFrameWait;
#[derive(Resource, Deref)]
pub struct ObjectRegistryRes(Arc<ObjectRegistry>);
#[derive(Resource, Deref)]
pub struct DbusConnection(Connection);
fn bevy_loop(
ready_notifier: Arc<Notify>,
_project_dirs: Option<ProjectDirs>,
args: CliArgs,
dbus_connection: Connection,
object_registry: Arc<ObjectRegistry>,
) -> AppExit {
let mut app = App::new();
app.insert_resource(DbusConnection(dbus_connection));
app.insert_resource(OxrManualGraphicsConfig {
fallback_backend: GraphicsBackend::Vulkan(()),
vk_instance_exts: Vec::new(),
vk_device_exts: bevy_dmabuf::required_device_extensions(),
});
app.add_plugins(AssetPlugin {
meta_check: AssetMetaCheck::Never,
unapproved_path_mode: UnapprovedPathMode::Allow,
..default()
});
let mut plugins = MinimalPlugins
.build()
.add(DiagnosticsPlugin)
.add(TransformPlugin)
.add(InputPlugin)
.add(AccessibilityPlugin);
plugins = plugins
.add(TerminalCtrlCHandlerPlugin)
// bevy_mod_openxr will replace this, TODO: figure out how to mix this with
// bevy-dmabuf
.add(RenderPlugin {
render_creation: RenderCreation::Automatic(WgpuSettings {
backends: Some(Backends::VULKAN),
..Default::default()
}),
..Default::default()
})
.add(ImagePlugin::default())
.add(CorePipelinePlugin)
// theoretically we shouldn't need this because of bevy_sk, but everything is tangled in
// there and idk what we actually need to run
.add(PbrPlugin {
// this seems to only apply to StandardMaterial, we don't use that
prepass_enabled: true,
add_default_deferred_lighting_plugin: false,
use_gpu_instance_buffer_builder: true,
debug_flags: RenderDebugFlags::default(),
})
// required for gltf
.add(ScenePlugin)
.add(GltfPlugin::default())
// .add(AnimationPlugin)
.add(AudioPlugin::default())
.add(GizmoPlugin)
.add(WindowPlugin::default())
.add(DmabufImportPlugin);
let mut task_pool_plugin = TaskPoolPlugin::default();
// make tokio work
let handle = tokio::runtime::Handle::current();
let enter_runtime_context = Arc::new(move || {
// TODO: this might be a memory leak
std::mem::forget(handle.enter());
});
task_pool_plugin.task_pool_options.io.on_thread_spawn = Some(enter_runtime_context.clone());
task_pool_plugin.task_pool_options.compute.on_thread_spawn =
Some(enter_runtime_context.clone());
task_pool_plugin
.task_pool_options
.async_compute
.on_thread_spawn = Some(enter_runtime_context.clone());
plugins = plugins.set(task_pool_plugin);
if std::env::var("DISPLAY").is_ok() || std::env::var("WAYLAND_DISPLAY").is_ok() {
let mut plugin = WinitPlugin::<WakeUp>::default();
plugin.run_on_any_thread = true;
plugins = plugins.add(plugin).disable::<ScheduleRunnerPlugin>();
plugins = match args.spectator {
true => plugins.add(SpectatorCameraPlugin),
false => plugins.add(FlatscreenInputPlugin),
};
}
app.insert_resource(PipelinedRenderThreadOnCreateCallback(
enter_runtime_context.clone(),
));
app.add_plugins(
if !args.force_flatscreen {
add_xr_plugins(plugins)
.set(OxrInitPlugin {
app_info: AppInfo {
name: "Stardust XR".into(),
version: bevy_mod_openxr::types::Version(0, 44, 1),
},
exts: {
// all OpenXR extensions can be requested here
let mut exts = OxrExtensions::default();
exts.enable_hand_tracking();
if args.overlay_priority.is_some() {
exts.enable_extx_overlay();
}
exts.khr_convert_timespec_time = true;
exts
},
..default()
})
.set(OxrRenderPlugin {
default_wait_frame: false,
..default()
})
.set(OxrReferenceSpacePlugin {
default_primary_ref_space: ReferenceSpaceType::LOCAL,
})
// Disable a bunch of unneeded plugins
// we don't do any action stuff that needs to integrate with the ecosystem
.disable::<OxrActionAttachingPlugin>()
.disable::<OxrActionSyncingPlugin>()
} else {
// enable a event
plugins = plugins.add(XrSessionPlugin { auto_handle: false });
bevy_dmabuf::wgpu_init::add_dmabuf_init_plugin(plugins)
}
.set(WindowPlugin {
primary_window: Some(Window {
transparent: args.transparent_flatscreen,
present_mode: PresentMode::AutoNoVsync,
composite_alpha_mode: if args.transparent_flatscreen {
CompositeAlphaMode::PreMultiplied
} else {
CompositeAlphaMode::Auto
},
title: "StardustXR server flatscreen mode".to_string(),
..default()
}),
..default()
}),
);
app.add_plugins(PipelinedRenderingPlugin);
app.add_plugins(bevy_sk::hand::HandPlugin);
app.add_plugins(bevy_equirect::EquirectangularPlugin);
// app.add_plugins(HandGizmosPlugin);
app.world_mut().resource_mut::<AmbientLight>().brightness = 1000.0;
if let Some(priority) = args.overlay_priority {
app.insert_resource(OxrOverlaySettings {
session_layer_placement: priority,
..default()
});
}
app.insert_resource(OxrSessionConfig {
blend_mode_preference: vec![
EnvironmentBlendMode::ALPHA_BLEND,
EnvironmentBlendMode::ADDITIVE,
EnvironmentBlendMode::OPAQUE,
],
..default()
});
let mut pre_frame_wait = Schedule::new(PreFrameWait);
pre_frame_wait.set_executor_kind(ExecutorKind::MultiThreaded);
app.add_schedule(pre_frame_wait);
app.insert_resource(ClearColor(Color::BLACK.with_alpha(0.0)));
app.insert_resource(ObjectRegistryRes(object_registry));
#[cfg(feature = "bevy_debugging")]
{
use bevy::remote::{RemotePlugin, http::RemoteHttpPlugin};
app.add_plugins((RemotePlugin::default(), RemoteHttpPlugin::default()));
}
// the Stardust server plugins
// infra plugins
app.add_plugins(EntityHandlePlugin);
// node plugins
app.add_plugins((
SpatialNodePlugin,
ModelNodePlugin,
TextNodePlugin,
LinesNodePlugin,
AudioNodePlugin,
// not really a node ig? at least for now
SkyPlugin,
));
// object plugins
app.add_plugins((PlaySpacePlugin, HandPlugin, ControllerPlugin, HmdPlugin));
// feature plugins
app.add_plugins((WaylandPlugin, TrackingOffsetPlugin, FieldDebugGizmoPlugin));
app.add_systems(PostStartup, move || {
ready_notifier.notify_waiters();
});
app.add_observer(cam_settings);
app.add_systems(
XrFirst,
xr_step
.in_set(OxrWaitFrameSystem)
.in_set(XrHandleEvents::FrameLoop),
);
app.run()
}
fn cam_settings(
trigger: Trigger<OnAdd, Camera3d>,
mut query: Query<(Entity, &mut Projection, &mut Msaa, &mut Tonemapping), With<Camera3d>>,
mut cmds: Commands,
) {
let Ok((entity, mut projection, mut msaa, mut tonemapping)) = query.get_mut(trigger.target())
else {
return;
};
info!("modifying cam");
match projection.deref_mut() {
Projection::Perspective(perspective_projection) => perspective_projection.near = 0.003,
Projection::Orthographic(orthographic_projection) => orthographic_projection.near = 0.003,
Projection::Custom(custom_projection) => {
if let Some(xr) = custom_projection.get_mut::<XrProjection>() {
xr.near = 0.003
} else {
error_once!("unknown custom camera projection");
}
}
}
*msaa = Msaa::Off;
*tonemapping = Tonemapping::None;
cmds.entity(entity)
.insert(OrderIndependentTransparencySettings::default());
}
fn xr_step(world: &mut World) {
// we are targeting the frame after the wait
if let Some(mut state) = world.get_resource_mut::<OxrFrameState>() {
state.predicted_display_time = openxr::Time::from_nanos(
state.predicted_display_time.as_nanos() + state.predicted_display_period.as_nanos(),
);
}
// update things like the Xr input methods
world.run_schedule(PreFrameWait);
input::process_input();
let time = world.resource::<bevy::prelude::Time>().delta_secs_f64();
nodes::root::Root::send_frame_events(time);
let should_wait = world
.run_system_cached(should_run_frame_loop)
.unwrap_or(false);
// we might want to do an adaptive sleep when not OpenXR waiting
if should_wait {
world.resource_scope::<OxrFrameWaiter, _>(|world, mut waiter| {
let state = waiter
.wait()
.inspect_err(|err| error!("failed to wait OpenXR frame: {err}"))
.ok();
if let Some(state) = state {
world.insert_resource(OxrFrameState(state));
}
});
}
tick_internal_client();
}
pub fn get_time(pipelined: bool, state: &OxrFrameState) -> openxr::Time {
if pipelined {
openxr::Time::from_nanos(
state.predicted_display_time.as_nanos() + state.predicted_display_period.as_nanos(),
)
} else {
state.predicted_display_time
}
}