From 6f5a488937469b850507799d52ace4c4986b0a7c Mon Sep 17 00:00:00 2001 From: mosure Date: Tue, 30 Jul 2024 18:27:09 -0500 Subject: [PATCH] feat: scaffold for ffi io --- .github/workflows/clippy.yml | 2 +- Cargo.toml | 18 +- ffi/bevy_zeroverse/Cargo.toml | 2 + ffi/bevy_zeroverse/python/dataloader.py | 21 +- ffi/bevy_zeroverse/src/lib.rs | 127 ++++++++- src/app.rs | 74 +++-- src/io.rs | 342 ++++++++++++++++++++++++ src/lib.rs | 1 + src/render/mod.rs | 2 + src/scene/mod.rs | 2 + 10 files changed, 545 insertions(+), 46 deletions(-) create mode 100644 src/io.rs diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 0c07027..7c18160 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -34,4 +34,4 @@ jobs: enable-sccache: "true" - name: lint - run: cargo clippy -- -Dwarnings + run: cargo clippy --all -- -Dwarnings diff --git a/Cargo.toml b/Cargo.toml index 111bac0..7f1e8f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,13 @@ exclude = [ default-run = "viewer" +[workspace] +members = [ + ".", + "ffi/bevy_zeroverse", +] + + [features] default = [ "asset_pipeline", @@ -71,14 +78,18 @@ bevy_args = "1.6" bevy-inspector-egui = { version = "0.25", optional = true } bevy_panorbit_camera = { version = "0.19", optional = true, features = ["bevy_egui"] } clap = { version = "4.4", features = ["derive"] } +futures-intrusive = "0.5" glob = "0.3" itertools = "0.13" noise = { version = "0.9" } +pollster = "0.3" +pyo3 = { version = "0.22", features = ["extension-module"] } rand = "0.8" rayon = { version = "1.10", optional = true } serde = "1.0" strum = "0.26" strum_macros = "0.26" +wgpu = "0.20.0" [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -145,10 +156,3 @@ path = "src/lib.rs" name = "viewer" path = "src/viewer.rs" required-features = ["viewer"] - - -[workspace] -members = [ - "ffi/bevy_zeroverse", - ".", -] diff --git a/ffi/bevy_zeroverse/Cargo.toml b/ffi/bevy_zeroverse/Cargo.toml index 0c6dda5..afb7422 100644 --- a/ffi/bevy_zeroverse/Cargo.toml +++ b/ffi/bevy_zeroverse/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [dependencies] bevy_zeroverse = { path = "../../" } +ndarray = { version = "0.15", features = ["blas"] } +once_cell = "1.19" pyo3 = { version = "0.22", features = ["extension-module"] } pyo3-log = "0.11" diff --git a/ffi/bevy_zeroverse/python/dataloader.py b/ffi/bevy_zeroverse/python/dataloader.py index dd3b0ef..9389d80 100644 --- a/ffi/bevy_zeroverse/python/dataloader.py +++ b/ffi/bevy_zeroverse/python/dataloader.py @@ -1,20 +1,13 @@ import bevy_zeroverse -print('initializing bevy zeroverse...') -bevy_zeroverse.initialize() -print('bevy zeroverse initialized!') +config = bevy_zeroverse.BevyZeroverseConfig() +config.headless = True +config.num_cameras = 4 -# dataloader = bevy_zeroverse.ZeroverseDataloader( -# width=256, -# height=144, -# num_cameras=4, -# render_modes=['color', 'depth', 'normal'], -# seed=0, -# scene_type='room', -# ) +bevy_zeroverse.initialize(config) -# for batch in dataloader: -# print(batch) -# break + +sample = bevy_zeroverse.next() +print(sample) diff --git a/ffi/bevy_zeroverse/src/lib.rs b/ffi/bevy_zeroverse/src/lib.rs index 8d79c2f..9f704c5 100644 --- a/ffi/bevy_zeroverse/src/lib.rs +++ b/ffi/bevy_zeroverse/src/lib.rs @@ -1,15 +1,93 @@ use std::{ sync::{ - atomic::{AtomicBool, Ordering}, Arc, + Mutex, + atomic::{ + AtomicBool, + Ordering, + }, + mpsc::{ + Sender, + Receiver, + RecvTimeoutError, + }, }, thread, + time::Duration, }; -// use bevy::prelude::*; -use pyo3::prelude::*; +use bevy::prelude::*; +use ndarray::{Array2, Array3}; +use once_cell::sync::OnceCell; +use pyo3::{ + prelude::*, + exceptions::PyTimeoutError, + types::PyList, +}; + +use ::bevy_zeroverse::app::{ + viewer_app, + BevyZeroverseConfig, +}; + + +type ColorImage = Array3; +type DepthImage = Array2; +type NormalImage = Array2<[f32; 3]>; + + +#[derive(Clone)] +#[pyclass] +struct View { + color: ColorImage, + depth: DepthImage, + normal: NormalImage, + + #[pyo3(get, set)] + view_from_world: [[f32; 4]; 4], + + #[pyo3(get, set)] + fovy: f32, +} + +#[pymethods] +impl View { + // TODO: upgrade rust-numpy to latest once pyo3 0.22 support is available + #[getter] + fn color<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> { + let color_list: Vec<_> = self.color.iter().map(|&v| v.into_py(py)).collect(); + PyList::new_bound(py, color_list) + } + + #[getter] + fn depth<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> { + let depth_list: Vec<_> = self.depth.iter().map(|&v| v.into_py(py)).collect(); + PyList::new_bound(py, depth_list) + } + + #[getter] + fn normal<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> { + let normal_list: Vec<_> = self.normal.iter().map(|&v| v.into_py(py)).collect(); + PyList::new_bound(py, normal_list) + } +} -use ::bevy_zeroverse::app::viewer_app; +#[pyclass] +struct Sample { + views: Vec, +} + +#[pymethods] +impl Sample { + #[getter] + fn views<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> { + let views_list: Vec<_> = self.views.iter().map(|v| v.clone().into_py(py)).collect(); + PyList::new_bound(py, views_list) + } +} + +static SAMPLE_RECEIVER: OnceCell>>> = OnceCell::new(); +// static SAMPLE_SENDER: OnceCell> = OnceCell::new(); // TODO: create Dataloader torch class (or a render `n` frames and return capture fn, used within a python wrapper dataloader, wrapper requires setup.py to include the python module) @@ -17,6 +95,7 @@ use ::bevy_zeroverse::app::viewer_app; pub fn setup_and_run_app( new_thread: bool, + override_args: Option, ) { let ready = Arc::new(AtomicBool::new(false)); @@ -24,7 +103,7 @@ pub fn setup_and_run_app( let ready = Arc::clone(&ready); move || { - let mut app = viewer_app(None); + let mut app = viewer_app(override_args); ready.store(true, Ordering::Release); @@ -33,6 +112,8 @@ pub fn setup_and_run_app( }; if new_thread { + info!("starting bevy_zeroverse in a new thread"); + thread::spawn(startup); while !ready.load(Ordering::Acquire) { @@ -44,21 +125,53 @@ pub fn setup_and_run_app( } -// TODO: add BevyZeroverseViewer struct parameter #[pyfunction] +#[pyo3(signature = (override_args=None))] fn initialize( py: Python<'_>, + override_args: Option, ) { py.allow_threads(|| { - setup_and_run_app(true); + setup_and_run_app(true, override_args); }); } +// TODO: support batch dimension (e.g. single array allocation for multiple samples) +// TODO: add systems for pushing and receiving camera outputs + metadata to python +// TODO: add options to bevy_zeroverse.next (e.g. render_modes, scene parameters, etc.) +#[pyfunction] +fn next() -> PyResult { + // TODO: advance 'n' frames - requires app reference + // TODO: capture frame - requires app system registration to write to textures and readback, triggered by app event after 'n' frames + // TODO: send frame + + let receiver = SAMPLE_RECEIVER.get().unwrap(); + let receiver = receiver.lock().unwrap(); + + let timeout = Duration::from_secs(5); + + match receiver.recv_timeout(timeout) { + Ok(sample) => Ok(sample), + Err(RecvTimeoutError::Timeout) => { + Err(PyTimeoutError::new_err("receive operation timed out")) + } + Err(RecvTimeoutError::Disconnected) => { + Err(PyTimeoutError::new_err("channel disconnected")) + } + } +} + + #[pymodule] fn bevy_zeroverse(m: &Bound<'_, PyModule>) -> PyResult<()> { pyo3_log::init(); + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(initialize, m)?)?; + m.add_function(wrap_pyfunction!(next, m)?)?; Ok(()) } diff --git a/src/app.rs b/src/app.rs index 6b86237..fa8e286 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,6 +3,7 @@ use bevy::{ app::AppExit, time::Stopwatch, render::camera::RenderTarget, + winit::WinitPlugin, }; use bevy_args::{ parse_args, @@ -15,6 +16,7 @@ use bevy_panorbit_camera::{ PanOrbitCamera, PanOrbitCameraPlugin, }; +use pyo3::prelude::*; use crate::{ BevyZeroversePlugin, @@ -54,62 +56,93 @@ use crate::{ )] #[command(about = "bevy_zeroverse viewer", version, long_about = None)] #[reflect(Resource)] -pub struct BevyZeroverseViewer { +#[pyclass] +pub struct BevyZeroverseConfig { /// enable the bevy inspector + #[pyo3(get, set)] #[arg(long, default_value = "true")] pub editor: bool, /// no window will be shown - #[arg(long, default_value = "true")] + #[pyo3(get, set)] + #[arg(long, default_value = "false")] pub headless: bool, /// view available material basecolor textures in a grid + #[pyo3(get, set)] #[arg(long, default_value = "false")] pub material_grid: bool, /// view plücker embeddings + #[pyo3(get, set)] #[arg(long, default_value = "false")] pub plucker_visualization: bool, /// enable closing the window with the escape key (doesn't work in web) + #[pyo3(get, set)] #[arg(long, default_value = "true")] pub press_esc_close: bool, + #[pyo3(get, set)] #[arg(long, default_value = "1920.0")] pub width: f32, + #[pyo3(get, set)] #[arg(long, default_value = "1080.0")] pub height: f32, + #[pyo3(get, set)] #[arg(long, default_value = "0")] pub num_cameras: usize, /// display a grid of Zeroverse cameras + #[pyo3(get, set)] #[arg(long, default_value = "false")] pub camera_grid: bool, /// window title + #[pyo3(get, set)] #[arg(long, default_value = "bevy_zeroverse")] pub name: String, /// move to the next scene after `regenerate_ms` milliseconds + #[pyo3(get, set)] #[arg(long, default_value = "0")] pub regenerate_ms: u32, /// automatically rotate the root scene object in the y axis + #[pyo3(get, set)] #[arg(long, default_value = "0.0")] pub yaw_speed: f32, + #[pyo3(get, set)] #[arg(long, value_enum, default_value_t = RenderMode::Color)] pub render_mode: RenderMode, + #[pyo3(get, set)] #[arg(long, value_enum, default_value_t = ZeroverseSceneType::Object)] pub scene_type: ZeroverseSceneType, } -impl Default for BevyZeroverseViewer { - fn default() -> BevyZeroverseViewer { - BevyZeroverseViewer { +#[pymethods] +impl BevyZeroverseConfig { + #[new] + pub fn new() -> Self { + Default::default() + } + + fn __str__(&self) -> PyResult { + Ok(format!("{:?}", self)) + } + + fn __repr__(&self) -> PyResult { + Ok(format!("{:?}", self)) + } +} + +impl Default for BevyZeroverseConfig { + fn default() -> BevyZeroverseConfig { + BevyZeroverseConfig { editor: true, headless: false, material_grid: false, @@ -130,11 +163,11 @@ impl Default for BevyZeroverseViewer { pub fn viewer_app( - override_args: Option, + override_args: Option, ) -> App { let args = match override_args { Some(args) => args, - None => parse_args::(), + None => parse_args::(), }; let mut app = App::new(); @@ -175,12 +208,19 @@ pub fn viewer_app( }); app.insert_resource(ClearColor(Color::srgba(0.0, 0.0, 0.0, 0.0))); + let default_plugins = DefaultPlugins - .set(ImagePlugin::default_nearest()) - .set(WindowPlugin { + .set(ImagePlugin::default_nearest()); + + let default_plugins = if args.headless { + default_plugins + .disable::() + } else { + default_plugins.set(WindowPlugin { primary_window, ..default() - }); + }) + }; app.add_plugins(default_plugins); @@ -189,7 +229,7 @@ pub fn viewer_app( app.insert_resource(Msaa::Sample8); if args.editor { - app.register_type::(); + app.register_type::(); app.add_plugins(WorldInspectorPlugin::new()); } @@ -232,7 +272,7 @@ pub fn viewer_app( struct MaterialGridCameraMarker; fn setup_camera( - args: Res, + args: Res, mut commands: Commands, material_grid_cameras: Query>, editor_cameras: Query>, @@ -290,7 +330,7 @@ struct CameraGrid; fn setup_camera_grid( mut commands: Commands, - args: Res, + args: Res, camera_grids: Query>, zeroverse_cameras: Query< (Entity, &Camera), @@ -360,7 +400,7 @@ struct MaterialGrid; fn setup_material_grid( mut commands: Commands, - args: Res, + args: Res, standard_materials: Res>, zeroverse_materials: Res, material_grids: Query>, @@ -429,7 +469,7 @@ fn setup_scene( #[allow(clippy::too_many_arguments)] fn regenerate_scene_system( - args: Res, + args: Res, keys: Res>, time: Res