diff --git a/.github/workflows/ci-rs.yml b/.github/workflows/ci-rs.yml index 168ddc0..61a0b03 100644 --- a/.github/workflows/ci-rs.yml +++ b/.github/workflows/ci-rs.yml @@ -24,9 +24,7 @@ concurrency: jobs: ci-rs: - # revert later - # runs-on: ubuntu-latest - runs-on: macos-latest + runs-on: [ macos-latest, ubuntu-latest] strategy: fail-fast: false matrix: @@ -44,22 +42,15 @@ jobs: components: rustfmt, clippy enable-sccache: "true" - # - name: Install binaries - # run: sudo apt-get update && sudo apt-get install -y clang pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 gcc-multilib + - name: Install binaries + run: sudo apt-get update && sudo apt-get install -y clang pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 gcc-multilib - name: Build - run: cargo build --release # --verbose + run: cargo build --release --features docker - name: Test - run: cargo test --release # --verbose + run: cargo test --release - name: Lint run: cargo fmt --all -- --check - # && cargo clippy --verbose -- -D warnings - - # - name: Bloat Check - # uses: cs50victor/cargo-bloat-action@master - # with: - # token: ${{ secrets.GITHUB_TOKEN }} - # kv_token: ${{ secrets.KV_TOKEN }} - # included_packages: "lkgpt" + # && cargo clippy --verbose -- -D warnings \ No newline at end of file diff --git a/crates/bevy_headless/Cargo.toml b/crates/bevy_headless/Cargo.toml index e90b288..ebb4ca8 100644 --- a/crates/bevy_headless/Cargo.toml +++ b/crates/bevy_headless/Cargo.toml @@ -5,13 +5,13 @@ edition = "2021" rust-version.workspace = true [dependencies] -anyhow = { workspace = true } -base64 = { workspace = true } -bevy = { workspace = true } +anyhow.workspace = true +base64.workspace = true +bevy.workspace = true bytemuck = "1.14.1" futures = "0.3.30" futures-lite = "2.2.0" image = { version = "0.24.8", features = ["exr", "png", "jpeg", "webp"], default-features = false } -wgpu = { workspace = true } -log = { workspace = true } +wgpu.workspace = true +log.workspace = true parking_lot = "0.12.1" diff --git a/crates/bevy_remote_asset/Cargo.toml b/crates/bevy_remote_asset/Cargo.toml new file mode 100644 index 0000000..b98cb49 --- /dev/null +++ b/crates/bevy_remote_asset/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "bevy_remote_asset" +version = "0.1.0" +edition = "2021" +rust-version.workspace = true + +[dependencies] +bevy.workspace = true +pin-project = "1.1.3" +surf = {version = "2.3", default-features = false, features = ["h1-client-rustls"]} \ No newline at end of file diff --git a/crates/bevy_remote_asset/src/lib.rs b/crates/bevy_remote_asset/src/lib.rs new file mode 100644 index 0000000..79106de --- /dev/null +++ b/crates/bevy_remote_asset/src/lib.rs @@ -0,0 +1,7 @@ +// derived from https://github.com/johanhelsing/bevy_web_asset + +mod web_asset_plugin; +mod web_asset_source; + +pub use web_asset_plugin::WebAssetPlugin; +pub use web_asset_source::WebAssetReader; diff --git a/crates/bevy_remote_asset/src/web_asset_plugin.rs b/crates/bevy_remote_asset/src/web_asset_plugin.rs new file mode 100644 index 0000000..2fc9334 --- /dev/null +++ b/crates/bevy_remote_asset/src/web_asset_plugin.rs @@ -0,0 +1,33 @@ +use bevy::prelude::*; + +use crate::web_asset_source::*; +use bevy::asset::io::AssetSource; + +/// Add this plugin to bevy to support loading http and https urls. +/// +/// Needs to be added before Bevy's `DefaultPlugins`. +/// +/// # Example +/// +/// ```no_run +/// # use bevy::prelude::*; +/// # use bevy_web_asset::WebAssetPlugin; +/// +/// let mut app = App::new(); +/// +/// app.add_plugins(( +/// WebAssetPlugin::default(), +/// DefaultPlugins +/// )); +/// ``` +#[derive(Default)] +pub struct WebAssetPlugin; + +impl Plugin for WebAssetPlugin { + fn build(&self, app: &mut App) { + app.register_asset_source( + "https", + AssetSource::build().with_reader(|| Box::new(WebAssetReader::Https)), + ); + } +} diff --git a/crates/bevy_remote_asset/src/web_asset_source.rs b/crates/bevy_remote_asset/src/web_asset_source.rs new file mode 100644 index 0000000..8c9fe3c --- /dev/null +++ b/crates/bevy_remote_asset/src/web_asset_source.rs @@ -0,0 +1,143 @@ +use bevy::{asset::io::PathStream, utils::BoxedFuture}; +use std::path::{Path, PathBuf}; + +use bevy::asset::io::{AssetReader, AssetReaderError, Reader}; + +/// Treats paths as urls to load assets from. +pub enum WebAssetReader { + /// Use TLS for setting up connections. + Https, +} + +impl WebAssetReader { + fn make_uri(&self, path: &Path) -> PathBuf { + PathBuf::from(match self { + Self::Https => "https://", + }) + .join(path) + } + + /// See [bevy::asset::io::get_meta_path] + fn make_meta_uri(&self, path: &Path) -> PathBuf { + let mut uri = self.make_uri(path); + let mut extension = + path.extension().expect("asset paths must have extensions").to_os_string(); + extension.push(".meta"); + uri.set_extension(extension); + uri + } +} + +async fn get<'a>(path: PathBuf) -> Result>, AssetReaderError> { + use std::{ + future::Future, + io, + pin::Pin, + task::{Context, Poll}, + }; + + use bevy::asset::io::VecReader; + use surf::StatusCode; + + #[pin_project::pin_project] + struct ContinuousPoll(#[pin] T); + + impl Future for ContinuousPoll { + type Output = T::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + // Always wake - blocks on single threaded executor. + cx.waker().wake_by_ref(); + + self.project().0.poll(cx) + } + } + + let str_path = path.to_str().ok_or_else(|| { + AssetReaderError::Io(io::Error::new( + io::ErrorKind::Other, + format!("non-utf8 path: {}", path.display()), + )) + })?; + let mut response = ContinuousPoll(surf::get(str_path)).await.map_err(|err| { + AssetReaderError::Io(io::Error::new( + io::ErrorKind::Other, + format!( + "unexpected status code {} while loading {}: {}", + err.status(), + path.display(), + err.into_inner(), + ), + )) + })?; + + match response.status() { + StatusCode::Ok => Ok(Box::new(VecReader::new( + ContinuousPoll(response.body_bytes()) + .await + .map_err(|_| AssetReaderError::NotFound(path.to_path_buf()))?, + )) as _), + StatusCode::NotFound => Err(AssetReaderError::NotFound(path)), + code => Err(AssetReaderError::Io(io::Error::new( + io::ErrorKind::Other, + format!("unexpected status code {} while loading {}", code, path.display()), + ))), + } +} + +impl AssetReader for WebAssetReader { + fn read<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + Box::pin(get(self.make_uri(path))) + } + + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + Box::pin(get(self.make_meta_uri(path))) + } + + fn is_directory<'a>( + &'a self, + _path: &'a Path, + ) -> BoxedFuture<'a, Result> { + Box::pin(async { Ok(false) }) + } + + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result, AssetReaderError>> { + Box::pin(async { Err(AssetReaderError::NotFound(self.make_uri(path))) }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn make_https_uri() { + assert_eq!( + WebAssetReader::Https + .make_uri(Path::new("s3.johanhelsing.studio/dump/favicon.png")) + .to_str() + .unwrap(), + "https://s3.johanhelsing.studio/dump/favicon.png" + ); + } + + #[test] + fn make_https_meta_uri() { + assert_eq!( + WebAssetReader::Https + .make_meta_uri(Path::new("s3.johanhelsing.studio/dump/favicon.png")) + .to_str() + .unwrap(), + "https://s3.johanhelsing.studio/dump/favicon.png.meta" + ); + } +} diff --git a/crates/bevy_ws_server/Cargo.toml b/crates/bevy_ws_server/Cargo.toml index 250cb2f..722fc6f 100644 --- a/crates/bevy_ws_server/Cargo.toml +++ b/crates/bevy_ws_server/Cargo.toml @@ -5,16 +5,16 @@ edition = "2021" rust-version.workspace = true [dependencies] -anyhow = {workspace = true} -base64 = {workspace = true} +anyhow.workspace = true +base64.workspace = true bevy = { version = "0.12.1", default-features = false, features = ["multi-threaded"] } -async-net = {workspace = true} +async-net.workspace = true futures = "0.3.29" async-tungstenite = "0.24.0" -log = {workspace = true} -crossbeam-channel = {workspace = true} +log.workspace = true +crossbeam-channel.workspace = true async-channel = "2.1.0" async-native-tls = "0.5.0" smol = "2.0.0" -tungstenite = {workspace = true} +tungstenite.workspace = true pollster = "0.3.0" diff --git a/crates/minimal_example/Cargo.toml b/crates/minimal_example/Cargo.toml index 03c29ba..ab6f60e 100644 --- a/crates/minimal_example/Cargo.toml +++ b/crates/minimal_example/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" rust-version.workspace = true [dependencies] -bevy = { workspace = true } +bevy.workspace = true bevy_headless = { path = "../../crates/bevy_headless"} -log = { workspace = true } -pretty_env_logger = { workspace = true } +log.workspace = true +pretty_env_logger.workspace = true diff --git a/new_media/Cargo.toml b/new_media/Cargo.toml index 5bc410f..2657e21 100644 --- a/new_media/Cargo.toml +++ b/new_media/Cargo.toml @@ -2,26 +2,27 @@ name = "new_media" version = "0.0.1" edition = "2021" -rust-version.workspace = true +rust.workspace = true default-run = "new_media" [dependencies] -anyhow = { workspace = true } -async-net = { workspace = true } -base64 = { workspace = true } -bevy = { workspace = true } +anyhow.workspace = true +async-net.workspace = true +base64.workspace = true +bevy.workspace = true bevy_ws_server = { path = "../crates/bevy_ws_server" } bevy_headless = { path = "../crates/bevy_headless" } +bevy_remote_asset = { path = "../crates/bevy_remote_asset" } bevy_gaussian_splatting = { version = "2.0.2", default-features = true } -bevy_panorbit_camera = { workspace = true } -crossbeam-channel = { workspace = true } +bevy_panorbit_camera.workspace = true +crossbeam-channel.workspace = true dotenvy = "0.15.7" -log = { workspace = true } -pretty_env_logger = { workspace = true } +log.workspace = true +pretty_env_logger.workspace = true serde = { version = "1.0.192", features = ["derive"] } serde_json = "1.0.108" -tungstenite = { workspace = true } +tungstenite.workspace = true openssl = { version = "0.10.63", features = ["vendored"], optional = true} [features] -docker = ["dep:openssl","bevy/x11"] +docker = ["dep:openssl","bevy/x11"] \ No newline at end of file diff --git a/new_media/src/asset.rs b/new_media/src/asset.rs new file mode 100644 index 0000000..a388fd5 --- /dev/null +++ b/new_media/src/asset.rs @@ -0,0 +1,57 @@ +use bevy::{ + asset::{AssetServer, Assets}, + core::Name, + core_pipeline::{core_3d::Camera3dBundle, tonemapping::Tonemapping}, + ecs::system::{Commands, Res, ResMut}, + math::Vec3, + render::{camera::Camera, texture::Image}, + transform::components::Transform, + utils::default, +}; +use bevy_headless::ImageExportSource; +use bevy_panorbit_camera::PanOrbitCamera; + +use bevy_gaussian_splatting::{GaussianCloud, GaussianSplattingBundle}; + +pub fn setup_gaussian_cloud( + mut commands: Commands, + asset_server: Res, + mut gaussian_assets: ResMut>, + mut scene_controller: ResMut, + mut images: ResMut>, + export_sources: ResMut>, +) { + // let remote_file = Some("https://huggingface.co/datasets/cs50victor/splats/resolve/main/train/point_cloud/iteration_7000/point_cloud.gcloud"); + // TODO: figure out how to load remote files later + let splat_file = "splats/garden/point_cloud/iteration_7000/point_cloud.gcloud"; + log::info!("loading {}", splat_file); + // let cloud = asset_server.load(splat_file.to_string()); + + let cloud = gaussian_assets.add(GaussianCloud::test_model()); + + let render_target = bevy_headless::setup_render_target( + &mut commands, + &mut images, + &mut scene_controller, + export_sources, + ); + + let gs = GaussianSplattingBundle { cloud, ..default() }; + commands.spawn((gs, Name::new("gaussian_cloud"))); + + commands.spawn(( + Camera3dBundle { + transform: Transform::from_translation(Vec3::new(0.0, 1.5, 5.0)), + tonemapping: Tonemapping::None, + camera: Camera { target: render_target, ..default() }, + ..default() + }, + PanOrbitCamera { + allow_upside_down: true, + orbit_smoothness: 0.0, + pan_smoothness: 0.0, + zoom_smoothness: 0.0, + ..default() + }, + )); +} diff --git a/new_media/src/main.rs b/new_media/src/main.rs index 7198dbf..e73392f 100644 --- a/new_media/src/main.rs +++ b/new_media/src/main.rs @@ -1,68 +1,22 @@ +mod asset; mod controls; mod server; +use asset::setup_gaussian_cloud; use bevy::{ app::{App as Engine, ScheduleRunnerPlugin, Startup, Update}, - asset::{AssetServer, Assets}, - core::Name, - core_pipeline::{clear_color::ClearColor, core_3d::Camera3dBundle, tonemapping::Tonemapping}, - ecs::system::{Commands, Res, ResMut}, - math::Vec3, - render::{camera::Camera, color::Color, texture::Image}, - transform::components::Transform, - utils::default, + core_pipeline::clear_color::ClearColor, + render::color::Color, }; -use bevy_headless::{HeadlessPlugin, ImageExportSource}; -use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; +use bevy_headless::HeadlessPlugin; +use bevy_panorbit_camera::PanOrbitCameraPlugin; -use bevy_gaussian_splatting::{GaussianCloud, GaussianSplattingBundle, GaussianSplattingPlugin}; +use bevy_gaussian_splatting::GaussianSplattingPlugin; +use bevy_remote_asset::WebAssetPlugin; use bevy_ws_server::WsPlugin; use controls::{update_world_from_input, WorldControlChannel}; use server::{receive_message, start_ws}; -fn setup_gaussian_cloud( - mut commands: Commands, - asset_server: Res, - mut gaussian_assets: ResMut>, - mut scene_controller: ResMut, - mut images: ResMut>, - export_sources: ResMut>, -) { - // let remote_file = Some("https://huggingface.co/datasets/cs50victor/splats/resolve/main/train/point_cloud/iteration_7000/point_cloud.gcloud"); - // TODO: figure out how to load remote files later - let splat_file = "splats/garden/point_cloud/iteration_7000/point_cloud.gcloud"; - log::info!("loading {}", splat_file); - // let cloud = asset_server.load(splat_file.to_string()); - - let cloud = gaussian_assets.add(GaussianCloud::test_model()); - - let render_target = bevy_headless::setup_render_target( - &mut commands, - &mut images, - &mut scene_controller, - export_sources, - ); - - let gs = GaussianSplattingBundle { cloud, ..default() }; - commands.spawn((gs, Name::new("gaussian_cloud"))); - - commands.spawn(( - Camera3dBundle { - transform: Transform::from_translation(Vec3::new(0.0, 1.5, 5.0)), - tonemapping: Tonemapping::None, - camera: Camera { target: render_target, ..default() }, - ..default() - }, - PanOrbitCamera { - allow_upside_down: true, - orbit_smoothness: 0.0, - pan_smoothness: 0.0, - zoom_smoothness: 0.0, - ..default() - }, - )); -} - pub struct AppConfig { pub width: u32, pub height: u32, @@ -88,6 +42,7 @@ fn main() { // .insert_resource(ClearColor(Color::rgb_u8(255, 255, 255))) .insert_resource(ClearColor(Color::rgb_u8(0, 0, 0))) .add_plugins(( + WebAssetPlugin, HeadlessPlugin, WsPlugin, ScheduleRunnerPlugin::run_loop(std::time::Duration::from_secs_f64(1.0 / 60.0)),