diff --git a/Cargo.lock b/Cargo.lock index 5233077f37db..f1eae8439cf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1667,6 +1667,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "extend_viewer_ui" +version = "0.6.0" +dependencies = [ + "mimalloc", + "re_crash_handler", + "re_sdk_comms", + "re_viewer", + "tokio", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -3890,6 +3901,18 @@ dependencies = [ "cargo_metadata", ] +[[package]] +name = "re_crash_handler" +version = "0.6.0" +dependencies = [ + "backtrace", + "itertools", + "libc", + "parking_lot 0.12.1", + "re_analytics", + "re_build_info", +] + [[package]] name = "re_data_store" version = "0.6.0" @@ -4022,7 +4045,6 @@ dependencies = [ "image", "itertools", "lazy_static", - "macaw", "ndarray", "nohash-hasher", "num-derive", @@ -4164,7 +4186,6 @@ name = "re_sdk_comms" version = "0.6.0" dependencies = [ "ahash 0.8.3", - "anyhow", "crossbeam", "document-features", "rand", @@ -4172,6 +4193,7 @@ dependencies = [ "re_log_encoding", "re_log_types", "re_smart_channel", + "thiserror", "tokio", ] @@ -4428,17 +4450,15 @@ name = "rerun" version = "0.6.0" dependencies = [ "anyhow", - "backtrace", "clap", "document-features", "itertools", - "libc", - "parking_lot 0.12.1", "puffin", "rayon", "re_analytics", "re_build_build_info", "re_build_info", + "re_crash_handler", "re_data_store", "re_format", "re_log", diff --git a/Cargo.toml b/Cargo.toml index 8fab520d17fa..2f4a5fcab659 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ re_arrow_store = { path = "crates/re_arrow_store", version = "0.6.0", default-fe re_build_build_info = { path = "crates/re_build_build_info", version = "0.6.0", default-features = false } re_build_info = { path = "crates/re_build_info", version = "0.6.0", default-features = false } re_build_web_viewer = { path = "crates/re_build_web_viewer", version = "0.6.0", default-features = false } +re_crash_handler = { path = "crates/re_crash_handler", version = "0.6.0", default-features = false } re_data_store = { path = "crates/re_data_store", version = "0.6.0", default-features = false } re_data_ui = { path = "crates/re_data_ui", version = "0.6.0", default-features = false } re_error = { path = "crates/re_error", version = "0.6.0", default-features = false } @@ -44,10 +45,12 @@ re_sdk_comms = { path = "crates/re_sdk_comms", version = "0.6.0", default-featur re_smart_channel = { path = "crates/re_smart_channel", version = "0.6.0", default-features = false } re_string_interner = { path = "crates/re_string_interner", version = "0.6.0", default-features = false } re_tensor_ops = { path = "crates/re_tensor_ops", version = "0.6.0", default-features = false } +re_time_panel = { path = "crates/re_time_panel", version = "=0.6.0", default-features = false } re_tuid = { path = "crates/re_tuid", version = "0.6.0", default-features = false } re_ui = { path = "crates/re_ui", version = "0.6.0", default-features = false } re_viewer = { path = "crates/re_viewer", version = "0.6.0", default-features = false } re_viewer_context = { path = "crates/re_viewer_context", version = "0.6.0", default-features = false } +re_viewport = { path = "crates/re_viewport", version = "0.6.0", default-features = false } re_web_viewer_server = { path = "crates/re_web_viewer_server", version = "0.6.0", default-features = false } re_ws_comms = { path = "crates/re_ws_comms", version = "0.6.0", default-features = false } rerun = { path = "crates/rerun", version = "0.6.0", default-features = false } diff --git a/crates/re_arrow_store/src/lib.rs b/crates/re_arrow_store/src/lib.rs index b61f18c2768a..5d7b90596cf5 100644 --- a/crates/re_arrow_store/src/lib.rs +++ b/crates/re_arrow_store/src/lib.rs @@ -53,6 +53,10 @@ pub use arrow2::io::ipc::read::{StreamReader, StreamState}; #[doc(no_inline)] pub use re_log_types::{TimeInt, TimeRange, TimeType, Timeline}; // for politeness sake +pub mod external { + pub use arrow2; +} + // --- /// Native-only profiling macro for puffin. diff --git a/crates/re_arrow_store/src/store_helpers.rs b/crates/re_arrow_store/src/store_helpers.rs index 1cf79b6ac05a..5a4233771e1f 100644 --- a/crates/re_arrow_store/src/store_helpers.rs +++ b/crates/re_arrow_store/src/store_helpers.rs @@ -1,6 +1,6 @@ use re_log_types::{ ComponentName, DataCell, DataRow, DeserializableComponent, EntityPath, RowId, - SerializableComponent, TimeInt, TimePoint, Timeline, + SerializableComponent, TimePoint, Timeline, }; use crate::{DataStore, LatestAtQuery}; @@ -65,7 +65,7 @@ impl DataStore { { crate::profile_function!(); - let query = LatestAtQuery::new(Timeline::default(), TimeInt::MAX); + let query = LatestAtQuery::latest(Timeline::default()); self.query_latest_component(entity_path, &query) } } diff --git a/crates/re_arrow_store/src/store_read.rs b/crates/re_arrow_store/src/store_read.rs index 02c753ce4be3..c0f24b976e30 100644 --- a/crates/re_arrow_store/src/store_read.rs +++ b/crates/re_arrow_store/src/store_read.rs @@ -35,6 +35,13 @@ impl LatestAtQuery { pub const fn new(timeline: Timeline, at: TimeInt) -> Self { Self { timeline, at } } + + pub const fn latest(timeline: Timeline) -> Self { + Self { + timeline, + at: TimeInt::MAX, + } + } } /// A query over a time range, for a given timeline. diff --git a/crates/re_crash_handler/Cargo.toml b/crates/re_crash_handler/Cargo.toml new file mode 100644 index 000000000000..f3726438e081 --- /dev/null +++ b/crates/re_crash_handler/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "re_crash_handler" +authors.workspace = true +description = "Detect panics and signals, logging them and optionally sending them to analytics." +edition.workspace = true +homepage.workspace = true +include.workspace = true +license.workspace = true +publish = true +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[package.metadata.docs.rs] +all-features = true + + +[features] +default = ["analytics"] + +## Send analytics to Rerun on crashes +analytics = ["dep:re_analytics"] + +[dependencies] +re_build_info.workspace = true + +itertools.workspace = true +parking_lot.workspace = true + +# Optional dependencies: +re_analytics = { workspace = true, optional = true } + +# Native dependencies: +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +backtrace = "0.3" + +# Native unix dependencies: +[target.'cfg(not(any(target_arch = "wasm32", target_os = "windows")))'.dependencies] +libc = "0.2" diff --git a/crates/re_crash_handler/README.md b/crates/re_crash_handler/README.md new file mode 100644 index 000000000000..363c7ddfc4bd --- /dev/null +++ b/crates/re_crash_handler/README.md @@ -0,0 +1,10 @@ +# re_crash_handler + +Part of the [`rerun`](https://github.com/rerun-io/rerun) family of crates. + +[![Latest version](https://img.shields.io/crates/v/re_crash_handler.svg)](https://crates.io/crates/re_crash_handler) +[![Documentation](https://docs.rs/re_crash_handler/badge.svg)](https://docs.rs/re_crash_handler) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![Apache](https://img.shields.io/badge/license-Apache-blue.svg) + +Detect and handle signals, panics, and other crashes, making sure to log them and optionally send them off to analytics. diff --git a/crates/rerun/src/crash_handler.rs b/crates/re_crash_handler/src/lib.rs similarity index 98% rename from crates/rerun/src/crash_handler.rs rename to crates/re_crash_handler/src/lib.rs index 03f40e761453..293861b9d50f 100644 --- a/crates/rerun/src/crash_handler.rs +++ b/crates/re_crash_handler/src/lib.rs @@ -1,3 +1,5 @@ +//! Detect and handle signals, panics, and other crashes, making sure to log them and optionally send them off to analytics. + use re_build_info::BuildInfo; use parking_lot::Mutex; @@ -180,10 +182,7 @@ fn install_signal_handler(build_info: BuildInfo) { // Send analytics - this also sleeps a while to give the analytics time to send the event. #[cfg(feature = "analytics")] - { - let build_info = BUILD_INFO - .lock() - .unwrap_or_else(|| re_build_info::build_info!()); + if let Some(build_info) = *BUILD_INFO.lock() { send_signal_analytics(build_info, signal_name, callstack); } diff --git a/crates/re_data_store/src/log_db.rs b/crates/re_data_store/src/log_db.rs index 0a1b3f040507..2bc37f82cc68 100644 --- a/crates/re_data_store/src/log_db.rs +++ b/crates/re_data_store/src/log_db.rs @@ -43,6 +43,12 @@ impl Default for EntityDb { } impl EntityDb { + /// A sorted list of all the entity paths in this database. + pub fn entity_paths(&self) -> Vec<&EntityPath> { + use itertools::Itertools as _; + self.entity_path_from_hash.values().sorted().collect() + } + #[inline] pub fn entity_path_from_hash(&self, entity_path_hash: &EntityPathHash) -> Option<&EntityPath> { self.entity_path_from_hash.get(entity_path_hash) diff --git a/crates/re_data_store/src/util.rs b/crates/re_data_store/src/util.rs new file mode 100644 index 000000000000..3d54365f4bd6 --- /dev/null +++ b/crates/re_data_store/src/util.rs @@ -0,0 +1,87 @@ +use re_arrow_store::LatestAtQuery; +use re_log_types::{ + DataRow, DeserializableComponent, EntityPath, RowId, SerializableComponent, TimePoint, Timeline, +}; + +use crate::LogDb; + +// ---------------------------------------------------------------------------- + +/// Get the latest value for a given [`re_log_types::Component`]. +/// +/// This assumes that the row we get from the store only contains a single instance for this +/// component; it will log a warning otherwise. +/// +/// This should only be used for "mono-components" such as `Transform` and `Tensor`. +pub fn query_latest_single( + data_store: &re_arrow_store::DataStore, + entity_path: &EntityPath, + query: &LatestAtQuery, +) -> Option +where + for<'b> &'b C::ArrayType: IntoIterator, +{ + crate::profile_function!(); + + // Although it would be nice to use the `re_query` helpers for this, we would need to move + // this out of re_data_store to avoid a circular dep. Since we don't need to do a join for + // single components this is easy enough. + + let (_, cells) = data_store.latest_at(query, entity_path, C::name(), &[C::name()])?; + let cell = cells.get(0)?.as_ref()?; + + let mut iter = cell.try_to_native::().ok()?; + + let component = iter.next(); + + if iter.next().is_some() { + re_log::warn_once!("Unexpected batch for {} at: {}", C::name(), entity_path); + } + + component +} + +/// Get the latest value for a given [`re_log_types::Component`] assuming it is timeless. +/// +/// This assumes that the row we get from the store only contains a single instance for this +/// component; it will log a warning otherwise. +pub fn query_timeless_single( + data_store: &re_arrow_store::DataStore, + entity_path: &EntityPath, +) -> Option +where + for<'b> &'b C::ArrayType: IntoIterator, +{ + let query = re_arrow_store::LatestAtQuery::latest(Timeline::default()); + query_latest_single(data_store, entity_path, &query) +} + +// ---------------------------------------------------------------------------- + +/// Store a single value for a given [`re_log_types::Component`]. +pub fn store_one_component( + log_db: &mut LogDb, + entity_path: &EntityPath, + timepoint: &TimePoint, + component: C, +) { + let mut row = DataRow::from_cells1( + RowId::random(), + entity_path.clone(), + timepoint.clone(), + 1, + [component].as_slice(), + ); + row.compute_all_size_bytes(); + + match log_db.entity_db.try_add_data_row(&row) { + Ok(()) => {} + Err(err) => { + re_log::warn_once!( + "Failed to store component {}.{}: {err}", + entity_path, + C::name(), + ); + } + } +} diff --git a/crates/re_data_ui/src/component.rs b/crates/re_data_ui/src/component.rs index 8931ea3c0616..23bf74687ca5 100644 --- a/crates/re_data_ui/src/component.rs +++ b/crates/re_data_ui/src/component.rs @@ -34,13 +34,7 @@ impl DataUi for EntityComponentWithInstances { ) { crate::profile_function!(self.component_name().full_name()); - let mut instance_keys = match self.component_data.iter_instance_keys() { - Ok(instance_keys) => instance_keys, - Err(err) => { - ui.label(ctx.re_ui.error_text(format!("Error: {err}"))); - return; - } - }; + let instance_keys: Vec<_> = self.component_data.iter_instance_keys().collect(); let num_instances = self.num_instances(); @@ -52,7 +46,7 @@ impl DataUi for EntityComponentWithInstances { if num_instances == 0 { ui.weak("(empty)"); } else if num_instances == 1 { - if let Some(instance_key) = instance_keys.next() { + if let Some(instance_key) = instance_keys.first() { ctx.component_ui_registry.ui( ctx, ui, @@ -60,7 +54,7 @@ impl DataUi for EntityComponentWithInstances { query, &self.entity_path, &self.component_data, - &instance_key, + instance_key, ); } else { ui.label(ctx.re_ui.error_text("Error: missing instance key")); @@ -88,15 +82,10 @@ impl DataUi for EntityComponentWithInstances { re_ui::ReUi::setup_table_body(&mut body); let row_height = re_ui::ReUi::table_line_height(); body.rows(row_height, num_instances, |index, mut row| { - if let Some(instance_key) = self - .component_data - .iter_instance_keys() - .ok() - .and_then(|mut keys| keys.nth(index)) - { + if let Some(instance_key) = instance_keys.get(index) { row.col(|ui| { let instance_path = - InstancePath::instance(self.entity_path.clone(), instance_key); + InstancePath::instance(self.entity_path.clone(), *instance_key); item_ui::instance_path_button_to( ctx, ui, @@ -113,7 +102,7 @@ impl DataUi for EntityComponentWithInstances { query, &self.entity_path, &self.component_data, - &instance_key, + instance_key, ); }); } diff --git a/crates/re_data_ui/src/component_path.rs b/crates/re_data_ui/src/component_path.rs index 8e7b5d805fc2..2bb0eefefdcb 100644 --- a/crates/re_data_ui/src/component_path.rs +++ b/crates/re_data_ui/src/component_path.rs @@ -13,26 +13,19 @@ impl DataUi for ComponentPath { ) { let store = &ctx.log_db.entity_db.data_store; - match re_query::get_component_with_instances( + if let Some((_, component_data)) = re_query::get_component_with_instances( store, query, self.entity_path(), self.component_name, ) { - Err(re_query::QueryError::PrimaryNotFound) => { - ui.label(""); - } - Err(err) => { - // Any other failure to get a component is unexpected - ui.label(ctx.re_ui.error_text(format!("Error: {err}"))); - } - Ok((_, component_data)) => { - super::component::EntityComponentWithInstances { - entity_path: self.entity_path.clone(), - component_data, - } - .data_ui(ctx, ui, verbosity, query); + super::component::EntityComponentWithInstances { + entity_path: self.entity_path.clone(), + component_data, } + .data_ui(ctx, ui, verbosity, query); + } else { + ui.label(""); } } } diff --git a/crates/re_data_ui/src/component_ui_registry.rs b/crates/re_data_ui/src/component_ui_registry.rs index e140cc781120..8ec71c456e78 100644 --- a/crates/re_data_ui/src/component_ui_registry.rs +++ b/crates/re_data_ui/src/component_ui_registry.rs @@ -1,6 +1,6 @@ use re_arrow_store::LatestAtQuery; use re_log_types::{ - component_types::InstanceKey, external::arrow2, DeserializableComponent, EntityPath, SizeBytes, + component_types::InstanceKey, external::arrow2, DeserializableComponent, EntityPath, }; use re_query::ComponentWithInstances; use re_viewer_context::{ComponentUiRegistry, UiVerbosity, ViewerContext}; @@ -79,21 +79,29 @@ fn fallback_component_ui( ) { // No special ui implementation - use a generic one: if let Some(value) = component.lookup_arrow(instance_key) { - let bytes = value.total_size_bytes(); - if bytes < 256 { - // For small items, print them - let mut repr = String::new(); - let display = arrow2::array::get_display(value.as_ref(), "null"); - display(&mut repr, 0).unwrap(); - ui.label(repr); - } else { - ui.label(format!("{bytes} bytes")); - } + ui.label(format_arrow(&*value)); } else { ui.weak("(null)"); } } +fn format_arrow(value: &dyn arrow2::array::Array) -> String { + use re_log_types::SizeBytes as _; + + let bytes = value.total_size_bytes(); + if bytes < 256 { + // Print small items: + let mut string = String::new(); + let display = arrow2::array::get_display(value, "null"); + if display(&mut string, 0).is_ok() { + return string; + } + } + + // Fallback: + format!("{bytes} bytes") +} + // ---------------------------------------------------------------------------- impl DataUi for re_log_types::component_types::TextEntry { diff --git a/crates/re_data_ui/src/instance_path.rs b/crates/re_data_ui/src/instance_path.rs index 4f3256f3fc64..ae28214dc770 100644 --- a/crates/re_data_ui/src/instance_path.rs +++ b/crates/re_data_ui/src/instance_path.rs @@ -1,6 +1,6 @@ use re_data_store::InstancePath; use re_log_types::ComponentPath; -use re_query::{get_component_with_instances, QueryError}; +use re_query::get_component_with_instances; use re_viewer_context::{UiVerbosity, ViewerContext}; use super::DataUi; @@ -29,16 +29,14 @@ impl DataUi for InstancePath { .num_columns(2) .show(ui, |ui| { for component_name in components { - let component_data = get_component_with_instances( + let Some((_, component_data)) = get_component_with_instances( store, query, &self.entity_path, component_name, - ); - - if matches!(component_data, Err(QueryError::PrimaryNotFound)) { + ) else { continue; // no need to show components that are unset at this point in time - } + }; // Certain fields are hidden. if HIDDEN_COMPONENTS_FOR_ALL_VERBOSITY.contains(&component_name.as_str()) { @@ -62,34 +60,22 @@ impl DataUi for InstancePath { &ComponentPath::new(self.entity_path.clone(), component_name), ); - match component_data { - Err(err) => { - ui.label(ctx.re_ui.error_text(format!("Error: {err}"))); - } - Ok((_, component_data)) => { - if self.instance_key.is_splat() { - super::component::EntityComponentWithInstances { - entity_path: self.entity_path.clone(), - component_data, - } - .data_ui( - ctx, - ui, - UiVerbosity::Small, - query, - ); - } else { - ctx.component_ui_registry.ui( - ctx, - ui, - UiVerbosity::Small, - query, - &self.entity_path, - &component_data, - &self.instance_key, - ); - } + if self.instance_key.is_splat() { + super::component::EntityComponentWithInstances { + entity_path: self.entity_path.clone(), + component_data, } + .data_ui(ctx, ui, UiVerbosity::Small, query); + } else { + ctx.component_ui_registry.ui( + ctx, + ui, + UiVerbosity::Small, + query, + &self.entity_path, + &component_data, + &self.instance_key, + ); } ui.end_row(); diff --git a/crates/re_log/src/setup.rs b/crates/re_log/src/setup.rs index d24d4e5d1597..ccec8c9324d6 100644 --- a/crates/re_log/src/setup.rs +++ b/crates/re_log/src/setup.rs @@ -21,6 +21,7 @@ fn log_filter() -> String { rust_log } +/// Directs [`log`] calls to stderr. #[cfg(not(target_arch = "wasm32"))] pub fn setup_native_logging() { if std::env::var("RUST_BACKTRACE").is_err() { diff --git a/crates/re_log_types/Cargo.toml b/crates/re_log_types/Cargo.toml index 4da5fef9ded8..f237428b8a4c 100644 --- a/crates/re_log_types/Cargo.toml +++ b/crates/re_log_types/Cargo.toml @@ -27,7 +27,7 @@ arrow_datagen = ["dep:rand"] ecolor = ["dep:ecolor"] ## Add support for some math operations using [`glam`](https://crates.io/crates/glam/). -glam = ["dep:glam", "dep:macaw"] +glam = ["dep:glam"] ## Integration with the [`image`](https://crates.io/crates/image/) crate. image = ["dep:ecolor", "dep:image"] @@ -84,7 +84,6 @@ glam = { workspace = true, optional = true } image = { workspace = true, optional = true, default-features = false, features = [ "jpeg", ] } -macaw = { workspace = true, optional = true } rand = { version = "0.8", optional = true } rmp-serde = { version = "1.1", optional = true } serde = { version = "1", optional = true, features = ["derive", "rc"] } diff --git a/crates/re_query/src/dataframe_util.rs b/crates/re_query/src/dataframe_util.rs index 2dfc18ba224c..e08a0015c184 100644 --- a/crates/re_query/src/dataframe_util.rs +++ b/crates/re_query/src/dataframe_util.rs @@ -143,7 +143,7 @@ impl ComponentWithInstances { } let instance_keys: Vec> = - self.iter_instance_keys()?.map(Some).collect_vec(); + self.iter_instance_keys().map(Some).collect_vec(); let values = self.values.try_to_native_opt()?.collect_vec(); @@ -157,7 +157,7 @@ where for<'a> &'a Primary::ArrayType: IntoIterator, { pub fn as_df1(&self) -> crate::Result { - let instance_keys = self.primary.iter_instance_keys()?.map(Some).collect_vec(); + let instance_keys = self.primary.iter_instance_keys().map(Some).collect_vec(); let primary_values = self.primary.values.try_to_native_opt()?.collect_vec(); @@ -169,7 +169,7 @@ where C1: SerializableComponent + DeserializableComponent + Clone, for<'a> &'a C1::ArrayType: IntoIterator, { - let instance_keys = self.primary.iter_instance_keys()?.map(Some).collect_vec(); + let instance_keys = self.primary.iter_instance_keys().map(Some).collect_vec(); let primary_values = self.primary.values.try_to_native_opt()?.collect_vec(); diff --git a/crates/re_query/src/entity_view.rs b/crates/re_query/src/entity_view.rs index b1ba90505858..e87a99934e27 100644 --- a/crates/re_query/src/entity_view.rs +++ b/crates/re_query/src/entity_view.rs @@ -1,6 +1,7 @@ use std::{collections::BTreeMap, marker::PhantomData}; use arrow2::array::{Array, PrimitiveArray}; +use itertools::Either; use re_format::arrow; use re_log_types::{ component_types::InstanceKey, @@ -45,10 +46,14 @@ impl ComponentWithInstances { /// /// If the instance keys don't exist, generate them based on array-index position of the values #[inline] - pub fn iter_instance_keys(&self) -> crate::Result + '_> { - self.instance_keys - .try_to_native::() - .map_err(Into::into) + pub fn iter_instance_keys(&self) -> impl Iterator + '_ { + match self.instance_keys.try_to_native::() { + Ok(instance_keys) => Either::Left(instance_keys.into_iter()), + Err(err) => { + re_log::warn_once!("Instance keys of wrong type: {err}"); + Either::Right(std::iter::empty()) + } + } } /// Iterate over the values and convert them to a native `Component` @@ -278,7 +283,7 @@ where { /// Iterate over the instance keys #[inline] - pub fn iter_instance_keys(&self) -> crate::Result + '_> { + pub fn iter_instance_keys(&self) -> impl Iterator + '_ { self.primary.iter_instance_keys() } @@ -319,9 +324,9 @@ where let component = self.components.get(&C::name()); if let Some(component) = component { - let primary_instance_key_iter = self.primary.iter_instance_keys()?; + let primary_instance_key_iter = self.primary.iter_instance_keys(); - let mut component_instance_key_iter = component.iter_instance_keys()?; + let mut component_instance_key_iter = component.iter_instance_keys(); let component_value_iter = arrow_array_deserialize_iterator::>(component.values.as_arrow_ref())?; diff --git a/crates/re_query/src/query.rs b/crates/re_query/src/query.rs index d7826679eaef..b053fe4c538d 100644 --- a/crates/re_query/src/query.rs +++ b/crates/re_query/src/query.rs @@ -8,6 +8,9 @@ use re_log_types::{ use crate::{ComponentWithInstances, EntityView, QueryError}; /// Retrieves a [`ComponentWithInstances`] from the [`DataStore`]. +/// +/// Returns `None` if the component is not found. +/// /// ``` /// # use re_arrow_store::LatestAtQuery; /// # use re_log_types::{Timeline, component_types::Point2D, Component}; @@ -48,22 +51,20 @@ pub fn get_component_with_instances( query: &LatestAtQuery, ent_path: &EntityPath, component: ComponentName, -) -> crate::Result<(RowId, ComponentWithInstances)> { +) -> Option<(RowId, ComponentWithInstances)> { debug_assert_eq!(store.cluster_key(), InstanceKey::name()); let components = [InstanceKey::name(), component]; - let (row_id, mut cells) = store - .latest_at(query, ent_path, component, &components) - .ok_or(QueryError::PrimaryNotFound)?; + let (row_id, mut cells) = store.latest_at(query, ent_path, component, &components)?; - Ok(( + Some(( row_id, ComponentWithInstances { // NOTE: The unwrap cannot fail, the cluster key's presence is guaranteed // by the store. instance_keys: cells[0].take().unwrap(), - values: cells[1].take().ok_or(QueryError::PrimaryNotFound)?, + values: cells[1].take()?, }, )) } @@ -124,33 +125,29 @@ pub fn query_entity_with_primary( ) -> crate::Result> { crate::profile_function!(); - let (row_id, primary) = get_component_with_instances(store, query, ent_path, Primary::name())?; + let (row_id, primary) = get_component_with_instances(store, query, ent_path, Primary::name()) + .ok_or(QueryError::PrimaryNotFound)?; // TODO(jleibs): lots of room for optimization here. Once "instance" is // guaranteed to be sorted we should be able to leverage this during the // join. Series have a SetSorted option to specify this. join_asof might be // the right place to start digging. - let components: crate::Result> = components + let components: BTreeMap = components .iter() // Filter out `Primary` and `InstanceKey` from the component list since they are // always queried above when creating the primary. .filter(|component| *component != &Primary::name() && *component != &InstanceKey::name()) .filter_map(|component| { - match get_component_with_instances(store, query, ent_path, *component) - .map(|(_, cwi)| cwi) - { - Ok(component_result) => Some(Ok((*component, component_result))), - Err(QueryError::PrimaryNotFound) => None, - Err(err) => Some(Err(err)), - } + get_component_with_instances(store, query, ent_path, *component) + .map(|(_, component_result)| (*component, component_result)) }) .collect(); Ok(EntityView { row_id, primary, - components: components?, + components, phantom: std::marker::PhantomData, }) } diff --git a/crates/re_query/src/range.rs b/crates/re_query/src/range.rs index 2198d9dedcb2..f92b939bfda1 100644 --- a/crates/re_query/src/range.rs +++ b/crates/re_query/src/range.rs @@ -72,13 +72,12 @@ pub fn range_entity_with_primary<'a, Primary: Component + 'a, const N: usize>( // this will allow us to build up the initial state and send an initial latest-at // entity-view if needed. for (i, primary) in components.iter().enumerate() { - let cwi = get_component_with_instances( + cwis_latest_raw[i] = get_component_with_instances( store, &LatestAtQuery::new(query.timeline, latest_time), ent_path, *primary, ); - cwis_latest_raw[i] = cwi.ok(); } if cwis_latest_raw[primary_col].is_some() { diff --git a/crates/re_query/src/visit.rs b/crates/re_query/src/visit.rs index 86bdb178a179..c618ab3d2fa4 100644 --- a/crates/re_query/src/visit.rs +++ b/crates/re_query/src/visit.rs @@ -74,7 +74,7 @@ macro_rules! create_visitor { crate::profile_function!(); ::itertools::izip!( - self.primary.iter_instance_keys()?, + self.primary.iter_instance_keys(), self.primary.iter_values::()?, $( self.iter_component::<$CC>()?, diff --git a/crates/re_sdk_comms/Cargo.toml b/crates/re_sdk_comms/Cargo.toml index ba28da47b836..fcba1a980001 100644 --- a/crates/re_sdk_comms/Cargo.toml +++ b/crates/re_sdk_comms/Cargo.toml @@ -31,8 +31,8 @@ re_log_types = { workspace = true, features = ["serde"] } re_smart_channel.workspace = true ahash.workspace = true -anyhow.workspace = true crossbeam.workspace = true document-features = "0.2" rand = { version = "0.8.5", features = ["small_rng"] } +thiserror.workspace = true tokio.workspace = true diff --git a/crates/re_sdk_comms/src/lib.rs b/crates/re_sdk_comms/src/lib.rs index 44c61432af9b..6c9e2044292d 100644 --- a/crates/re_sdk_comms/src/lib.rs +++ b/crates/re_sdk_comms/src/lib.rs @@ -11,15 +11,13 @@ pub(crate) mod tcp_client; mod buffered_client; #[cfg(feature = "client")] -pub use buffered_client::Client; +pub use {buffered_client::Client, tcp_client::ClientError}; #[cfg(feature = "server")] mod server; #[cfg(feature = "server")] -pub use server::{serve, ServerOptions}; - -pub type Result = anyhow::Result; +pub use server::{serve, ServerError, ServerOptions}; pub const PROTOCOL_VERSION: u16 = 0; diff --git a/crates/re_sdk_comms/src/server.rs b/crates/re_sdk_comms/src/server.rs index 9a52a79bc3ad..4a496a0869fe 100644 --- a/crates/re_sdk_comms/src/server.rs +++ b/crates/re_sdk_comms/src/server.rs @@ -1,14 +1,50 @@ -//! TODO(emilk): use tokio instead - use std::time::Instant; -use anyhow::Context; use rand::{Rng as _, SeedableRng}; use re_log_types::{LogMsg, TimePoint, TimeType, TimelineName}; use re_smart_channel::{Receiver, Sender}; use tokio::net::{TcpListener, TcpStream}; +#[derive(thiserror::Error, Debug)] +pub enum ServerError { + #[error("Failed to bind TCP address {bind_addr:?}. Another Rerun instance is probably running. {err}")] + TcpBindError { + bind_addr: String, + err: std::io::Error, + }, +} + +#[derive(thiserror::Error, Debug)] +enum VersionError { + #[error("SDK client is using an older protocol version ({client_version}) than the SDK server ({server_version})")] + ClientIsOlder { + client_version: u16, + server_version: u16, + }, + + #[error("SDK client is using a newer protocol version ({client_version}) than the SDK server ({server_version})")] + ClientIsNewer { + client_version: u16, + server_version: u16, + }, +} + +#[derive(thiserror::Error, Debug)] +enum ConnectionError { + #[error(transparent)] + VersionError(#[from] VersionError), + + #[error(transparent)] + SendError(#[from] std::io::Error), + + #[error(transparent)] + DecodeError(#[from] re_log_encoding::decoder::DecodeError), + + #[error("The receiving end of the channel was closed")] + ChannelDisconnected(#[from] re_smart_channel::SendError), +} + #[derive(Clone, Copy, Debug, PartialEq)] pub struct ServerOptions { /// If the latency in the [`LogMsg`] channel is greater than this, @@ -34,22 +70,24 @@ impl Default for ServerOptions { /// # use re_sdk_comms::{serve, ServerOptions}; /// #[tokio::main] /// async fn main() { -/// let log_msg_rx = serve("0.0.0.0", 80, ServerOptions::default()).await.unwrap(); +/// let log_msg_rx = serve("0.0.0.0", re_sdk_comms::DEFAULT_SERVER_PORT, ServerOptions::default()).await.unwrap(); /// } /// ``` pub async fn serve( bind_ip: &str, port: u16, options: ServerOptions, -) -> anyhow::Result> { +) -> Result, ServerError> { let (tx, rx) = re_smart_channel::smart_channel(re_smart_channel::Source::TcpServer { port }); let bind_addr = format!("{bind_ip}:{port}"); - let listener = TcpListener::bind(&bind_addr).await.with_context(|| { - format!( - "Failed to bind TCP address {bind_addr:?}. Another Rerun instance is probably running." - ) - })?; + let listener = + TcpListener::bind(&bind_addr) + .await + .map_err(|err| ServerError::TcpBindError { + bind_addr: bind_addr.clone(), + err, + })?; if options.quiet { re_log::debug!( @@ -100,7 +138,7 @@ async fn run_client( mut stream: TcpStream, tx: &Sender, options: ServerOptions, -) -> anyhow::Result<()> { +) -> Result<(), ConnectionError> { #![allow(clippy::read_zero_byte_vec)] // false positive: https://github.com/rust-lang/rust-clippy/issues/9274 use tokio::io::AsyncReadExt as _; @@ -111,19 +149,17 @@ async fn run_client( match client_version.cmp(&crate::PROTOCOL_VERSION) { std::cmp::Ordering::Less => { - anyhow::bail!( - "sdk client is using an older protocol version ({}) than the sdk server ({}).", + return Err(ConnectionError::VersionError(VersionError::ClientIsOlder { client_version, - crate::PROTOCOL_VERSION - ); + server_version: crate::PROTOCOL_VERSION, + })); } std::cmp::Ordering::Equal => {} std::cmp::Ordering::Greater => { - anyhow::bail!( - "sdk client is using a newer protocol version ({}) than the sdk server ({}).", + return Err(ConnectionError::VersionError(VersionError::ClientIsNewer { client_version, - crate::PROTOCOL_VERSION - ); + server_version: crate::PROTOCOL_VERSION, + })); } } diff --git a/crates/re_sdk_comms/src/tcp_client.rs b/crates/re_sdk_comms/src/tcp_client.rs index 4007de42a8a0..f5df588b1769 100644 --- a/crates/re_sdk_comms/src/tcp_client.rs +++ b/crates/re_sdk_comms/src/tcp_client.rs @@ -3,6 +3,21 @@ use std::{ net::{SocketAddr, TcpStream}, }; +#[derive(thiserror::Error, Debug)] +pub enum ClientError { + #[error("Failed to connect to Rerun server at {addrs:?}: {err}")] + Connect { + addrs: Vec, + err: std::io::Error, + }, + + #[error("Failed to send to Rerun server at {addrs:?}: {err}")] + Send { + addrs: Vec, + err: std::io::Error, + }, +} + /// State of the [`TcpStream`] /// /// Because the [`TcpClient`] lazily connects on [`TcpClient::send`], it needs a @@ -63,7 +78,7 @@ impl TcpClient { /// Returns `false` on failure. Does nothing if already connected. /// /// [`Self::send`] will call this. - pub fn connect(&mut self) -> anyhow::Result<()> { + pub fn connect(&mut self) -> Result<(), ClientError> { if let TcpStreamState::Connected(_) = self.stream_state { Ok(()) } else { @@ -72,7 +87,10 @@ impl TcpClient { Ok(mut stream) => { if let Err(err) = stream.write(&crate::PROTOCOL_VERSION.to_le_bytes()) { self.stream_state = TcpStreamState::Disconnected; - anyhow::bail!("Failed to send to Rerun server at {:?}: {err}", self.addrs); + Err(ClientError::Send { + addrs: self.addrs.clone(), + err, + }) } else { self.stream_state = TcpStreamState::Connected(stream); Ok(()) @@ -80,17 +98,17 @@ impl TcpClient { } Err(err) => { self.stream_state = TcpStreamState::Disconnected; - anyhow::bail!( - "Failed to connect to Rerun server at {:?}: {err}", - self.addrs - ); + Err(ClientError::Connect { + addrs: self.addrs.clone(), + err, + }) } } } } - /// blocks until it is sent - pub fn send(&mut self, packet: &[u8]) -> anyhow::Result<()> { + /// Blocks until it is sent. + pub fn send(&mut self, packet: &[u8]) -> Result<(), ClientError> { use std::io::Write as _; self.connect()?; @@ -99,12 +117,18 @@ impl TcpClient { re_log::trace!("Sending a packet of size {}…", packet.len()); if let Err(err) = stream.write(&(packet.len() as u32).to_le_bytes()) { self.stream_state = TcpStreamState::Disconnected; - anyhow::bail!("Failed to send to Rerun server at {:?}: {err}", self.addrs); + return Err(ClientError::Send { + addrs: self.addrs.clone(), + err, + }); } if let Err(err) = stream.write(packet) { self.stream_state = TcpStreamState::Disconnected; - anyhow::bail!("Failed to send to Rerun server at {:?}: {err}", self.addrs); + return Err(ClientError::Send { + addrs: self.addrs.clone(), + err, + }); } Ok(()) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 70b4b23f0b64..cb38ac8c866c 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -607,7 +607,7 @@ fn paint_background_fill(ui: &mut egui::Ui) { // Of course this does some over-draw, but we have to live with that. ui.painter().rect_filled( - ui.ctx().screen_rect().shrink(0.5), + ui.max_rect().shrink(0.5), re_ui::ReUi::native_window_rounding(), ui.visuals().panel_fill, ); @@ -870,7 +870,8 @@ impl App { self.log_db().map_or(false, |log_db| !log_db.is_empty()) } - fn log_db(&self) -> Option<&LogDb> { + /// Get access to the currently shown [`LogDb`], if any. + pub fn log_db(&self) -> Option<&LogDb> { self.state .selected_rec_id .as_ref() diff --git a/crates/re_viewer/src/lib.rs b/crates/re_viewer/src/lib.rs index 59ab977008fb..5924d2c5aa0c 100644 --- a/crates/re_viewer/src/lib.rs +++ b/crates/re_viewer/src/lib.rs @@ -19,16 +19,18 @@ pub use app::{App, StartupOptions}; pub use remote_viewer_app::RemoteViewerApp; pub mod external { - pub use eframe; - pub use egui; - pub use re_renderer; + pub use {eframe, egui}; + pub use { + re_arrow_store, re_arrow_store::external::arrow2, re_data_store, re_log, re_log_types, + re_memory, re_renderer, re_ui, re_viewer_context, re_viewer_context::external::re_query, + }; } // ---------------------------------------------------------------------------- // When compiling for native: #[cfg(not(target_arch = "wasm32"))] -mod native; +pub mod native; #[cfg(not(target_arch = "wasm32"))] pub use native::{run_native_app, run_native_viewer_with_messages}; @@ -65,7 +67,15 @@ macro_rules! profile_scope { // --------------------------------------------------------------------------- +/// Information about this version of the crate. +pub fn build_info() -> re_build_info::BuildInfo { + re_build_info::build_info!() +} + +// --------------------------------------------------------------------------- + /// Where is this App running in? +/// Used for analytics. #[derive(Clone, Debug, PartialEq, Eq)] pub enum AppEnvironment { /// Created from the Rerun Python SDK. @@ -85,6 +95,9 @@ pub enum AppEnvironment { /// We are a web-viewer running in a browser as Wasm. Web, + + /// Some custom application wrapping re_viewer + Custom(String), } impl AppEnvironment { @@ -135,8 +148,9 @@ pub(crate) fn wgpu_options() -> egui_wgpu::WgpuConfiguration { } } +/// Customize eframe and egui to suit the rerun viewer. #[must_use] -pub(crate) fn customize_eframe(cc: &eframe::CreationContext<'_>) -> re_ui::ReUi { +pub fn customize_eframe(cc: &eframe::CreationContext<'_>) -> re_ui::ReUi { if let Some(render_state) = &cc.wgpu_render_state { use re_renderer::{config::RenderContextConfig, RenderContext}; diff --git a/crates/re_viewer/src/native.rs b/crates/re_viewer/src/native.rs index 732c94d92696..9472e27b82d3 100644 --- a/crates/re_viewer/src/native.rs +++ b/crates/re_viewer/src/native.rs @@ -5,7 +5,21 @@ type AppCreator = // NOTE: the name of this function is hard-coded in `crates/rerun/src/crash_handler.rs`! pub fn run_native_app(app_creator: AppCreator) -> eframe::Result<()> { - let native_options = eframe::NativeOptions { + let native_options = eframe_options(); + + let window_title = "Rerun Viewer"; + eframe::run_native( + window_title, + native_options, + Box::new(move |cc| { + let re_ui = crate::customize_eframe(cc); + app_creator(cc, re_ui) + }), + ) +} + +pub fn eframe_options() -> eframe::NativeOptions { + eframe::NativeOptions { // Controls where on disk the app state is persisted. app_id: Some("rerun".to_owned()), @@ -31,17 +45,7 @@ pub fn run_native_app(app_creator: AppCreator) -> eframe::Result<()> { multisampling: 0, // the 3D views do their own MSAA ..Default::default() - }; - - let window_title = "Rerun Viewer"; - eframe::run_native( - window_title, - native_options, - Box::new(move |cc| { - let re_ui = crate::customize_eframe(cc); - app_creator(cc, re_ui) - }), - ) + } } #[allow(clippy::unnecessary_wraps)] diff --git a/crates/re_viewer/src/ui/space_view_heuristics.rs b/crates/re_viewer/src/ui/space_view_heuristics.rs index a0880e2caf38..dda55286c3cf 100644 --- a/crates/re_viewer/src/ui/space_view_heuristics.rs +++ b/crates/re_viewer/src/ui/space_view_heuristics.rs @@ -129,7 +129,7 @@ fn default_created_space_views_from_candidates( crate::profile_function!(); // All queries are "right most" on the log timeline. - let query = LatestAtQuery::new(Timeline::log_time(), re_arrow_store::TimeInt::MAX); + let query = LatestAtQuery::latest(Timeline::log_time()); // First pass to look for interesting roots, as their existence influences the heuristic for non-roots! let categories_with_interesting_roots = candidates diff --git a/crates/re_viewer/src/ui/view_spatial/scene/scene_part/points2d.rs b/crates/re_viewer/src/ui/view_spatial/scene/scene_part/points2d.rs index a9a46c5208f5..b9e77b5ee0d5 100644 --- a/crates/re_viewer/src/ui/view_spatial/scene/scene_part/points2d.rs +++ b/crates/re_viewer/src/ui/view_spatial/scene/scene_part/points2d.rs @@ -83,7 +83,7 @@ impl Points2DPart { let instance_path_hashes_for_picking = { crate::profile_scope!("instance_hashes"); entity_view - .iter_instance_keys()? + .iter_instance_keys() .map(|instance_key| { instance_path_hash_for_picking( ent_path, @@ -124,7 +124,7 @@ impl Points2DPart { .filter_map(|pt| pt.map(glam::Vec2::from)) }; - let picking_instance_ids = entity_view.iter_instance_keys()?.map(|instance_key| { + let picking_instance_ids = entity_view.iter_instance_keys().map(|instance_key| { instance_key_to_picking_id( instance_key, entity_view.num_instances(), @@ -146,7 +146,7 @@ impl Points2DPart { for (highlighted_key, instance_mask_ids) in &entity_highlight.instances { // TODO(andreas/jeremy): We can do this much more efficiently let highlighted_point_index = entity_view - .iter_instance_keys()? + .iter_instance_keys() .position(|key| key == *highlighted_key); if let Some(highlighted_point_index) = highlighted_point_index { point_range_builder = point_range_builder diff --git a/crates/re_viewer/src/ui/view_spatial/scene/scene_part/points3d.rs b/crates/re_viewer/src/ui/view_spatial/scene/scene_part/points3d.rs index 076210d1d9ef..428e8ddcf72e 100644 --- a/crates/re_viewer/src/ui/view_spatial/scene/scene_part/points3d.rs +++ b/crates/re_viewer/src/ui/view_spatial/scene/scene_part/points3d.rs @@ -91,7 +91,7 @@ impl Points3DPart { let instance_path_hashes_for_picking = { crate::profile_scope!("instance_hashes"); entity_view - .iter_instance_keys()? + .iter_instance_keys() .map(|instance_key| { instance_path_hash_for_picking( ent_path, @@ -128,7 +128,7 @@ impl Points3DPart { .filter_map(|pt| pt.map(glam::Vec3::from)) }; - let picking_instance_ids = entity_view.iter_instance_keys()?.map(|instance_key| { + let picking_instance_ids = entity_view.iter_instance_keys().map(|instance_key| { instance_key_to_picking_id( instance_key, entity_view.num_instances(), @@ -149,7 +149,7 @@ impl Points3DPart { for (highlighted_key, instance_mask_ids) in &entity_highlight.instances { // TODO(andreas/jeremy): We can do this much more efficiently let highlighted_point_index = entity_view - .iter_instance_keys()? + .iter_instance_keys() .position(|key| key == *highlighted_key); if let Some(highlighted_point_index) = highlighted_point_index { point_range_builder = point_range_builder diff --git a/crates/re_viewer/src/viewer_analytics.rs b/crates/re_viewer/src/viewer_analytics.rs index b764be5e2356..ddd7de41e85e 100644 --- a/crates/re_viewer/src/viewer_analytics.rs +++ b/crates/re_viewer/src/viewer_analytics.rs @@ -82,6 +82,7 @@ impl ViewerAnalytics { AppEnvironment::RustSdk { .. } => "rust_sdk", AppEnvironment::RerunCli { .. } => "rerun_cli", AppEnvironment::Web => "web_viewer", + AppEnvironment::Custom(_) => "custom", }; self.register("app_env", app_env_str); diff --git a/crates/re_viewer_context/src/lib.rs b/crates/re_viewer_context/src/lib.rs index b3d7023d3f45..6ac02af7310a 100644 --- a/crates/re_viewer_context/src/lib.rs +++ b/crates/re_viewer_context/src/lib.rs @@ -38,6 +38,10 @@ mod clipboard; #[cfg(not(target_arch = "wasm32"))] pub use clipboard::Clipboard; +pub mod external { + pub use {re_arrow_store, re_data_store, re_log_types, re_query, re_ui}; +} + // --------------------------------------------------------------------------- /// A unique id for each space view. diff --git a/crates/rerun/Cargo.toml b/crates/rerun/Cargo.toml index b21e1ba4d680..a913845de9e4 100644 --- a/crates/rerun/Cargo.toml +++ b/crates/rerun/Cargo.toml @@ -25,6 +25,7 @@ default = ["analytics", "demo", "glam", "image", "sdk", "server"] ## Enable telemetry using our analytics SDK. analytics = [ "dep:re_analytics", + "re_crash_handler/analytics", "re_viewer?/analytics", "re_web_viewer_server?/analytics", ] @@ -65,6 +66,7 @@ web_viewer = [ [dependencies] re_build_info.workspace = true +re_crash_handler.workspace = true re_data_store.workspace = true re_format.workspace = true re_log_encoding = { workspace = true, features = ["decoder", "encoder"] } @@ -76,8 +78,7 @@ re_ws_comms = { workspace = true, features = ["client"] } anyhow.workspace = true document-features = "0.2" -itertools = { workspace = true } -parking_lot.workspace = true +itertools.workspace = true # Optional dependencies: re_analytics = { workspace = true, optional = true } @@ -90,16 +91,11 @@ webbrowser = { version = "0.8", optional = true } # Native dependencies: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -backtrace = "0.3" clap = { workspace = true, features = ["derive"] } puffin.workspace = true rayon.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -# Native unix dependencies: -[target.'cfg(not(any(target_arch = "wasm32", target_os = "windows")))'.dependencies] -libc = "0.2" - [build-dependencies] re_build_build_info.workspace = true diff --git a/crates/rerun/src/lib.rs b/crates/rerun/src/lib.rs index be41451e9761..6094f924c4ce 100644 --- a/crates/rerun/src/lib.rs +++ b/crates/rerun/src/lib.rs @@ -90,7 +90,6 @@ #![warn(missing_docs)] // Let's keep the this crate well-documented! -mod crash_handler; mod run; /// Module for integrating with the [`clap`](https://crates.io/crates/clap) command line argument parser. diff --git a/crates/rerun/src/run.rs b/crates/rerun/src/run.rs index 6fe740af11ae..ac038a4cc3df 100644 --- a/crates/rerun/src/run.rs +++ b/crates/rerun/src/run.rs @@ -224,7 +224,7 @@ where re_viewer::env_vars::RERUN_TRACK_ALLOCATIONS, ); - crate::crash_handler::install_crash_handlers(build_info); + re_crash_handler::install_crash_handlers(build_info); use clap::Parser as _; let args = Args::parse_from(args); @@ -852,7 +852,7 @@ impl log::Log for StrictLogger { eprintln!("{level} logged in --strict mode: {}", record.args()); eprintln!( "{}", - crate::crash_handler::callstack_from(&["log::__private_api_log"]) + re_crash_handler::callstack_from(&["log::__private_api_log"]) ); std::process::exit(1); } diff --git a/examples/rust/extend_viewer_ui/Cargo.toml b/examples/rust/extend_viewer_ui/Cargo.toml new file mode 100644 index 000000000000..9947c3e8c4bb --- /dev/null +++ b/examples/rust/extend_viewer_ui/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "extend_viewer_ui" +version = "0.6.0" +edition = "2021" +rust-version = "1.69" +license = "MIT OR Apache-2.0" +publish = false + +[features] +default = [] + +# Turn on if you want to share analytics with Rerun (e.g. callstacks on crashes). +analytics = ["re_crash_handler/analytics", "re_viewer/analytics"] + +[dependencies] +re_crash_handler = { path = "../../../crates/re_crash_handler" } +re_viewer = { path = "../../../crates/re_viewer", default-features = false, features = [ + "webgl", +] } + +# We need re_sdk_comms to receive log events from and SDK: +re_sdk_comms = { path = "../../../crates/re_sdk_comms", features = ["server"] } + +# mimalloc is a much faster allocator: +mimalloc = "0.1" + +# We need tokio for re_sdk_comms: +tokio = { version = "1.24", features = ["macros", "rt-multi-thread"] } diff --git a/examples/rust/extend_viewer_ui/README.md b/examples/rust/extend_viewer_ui/README.md new file mode 100644 index 000000000000..9a0e0a70921d --- /dev/null +++ b/examples/rust/extend_viewer_ui/README.md @@ -0,0 +1,15 @@ +# External Viewer UI +Example showing how to wrap the Rerun Viewer in your own GUI. + +You create your own [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) app and write your own GUI using [`egui`](https://github.com/emilk/egui). + +The example is really basic, but should be something you can build upon. + +The example starts an SDK server which the Python or Rust logging SDK can connect to. + +![image](https://github.com/rerun-io/rerun/assets/1148717/cbbad63e-9b18-4e54-bafe-b6ffd723f63e) + +## Testing it +Start it with `cargo run -p extend_viewer_ui`. + +Then put some data into it with: `cargo run -p minimal_options -- --connect` diff --git a/examples/rust/extend_viewer_ui/src/main.rs b/examples/rust/extend_viewer_ui/src/main.rs new file mode 100644 index 000000000000..317062d7a7fd --- /dev/null +++ b/examples/rust/extend_viewer_ui/src/main.rs @@ -0,0 +1,203 @@ +//! This example shows how to wrap the Rerun Viewer in your own GUI. + +use re_viewer::external::{ + arrow2, eframe, egui, re_arrow_store, re_data_store, re_log, re_log_types, re_memory, re_query, +}; + +// By using `re_memory::AccountingAllocator` Rerun can keep track of exactly how much memory it is using, +// and prune the data store when it goes above a certain limit. +// By using `mimalloc` we get faster allocations. +#[global_allocator] +static GLOBAL: re_memory::AccountingAllocator = + re_memory::AccountingAllocator::new(mimalloc::MiMalloc); + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Direct calls using the `log` crate to stderr. Control with `RUST_LOG=debug` etc. + re_log::setup_native_logging(); + + // Install handlers for panics and crashes that prints to stderr and send + // them to Rerun analytics (if the `analytics` feature is on in `Cargo.toml`). + re_crash_handler::install_crash_handlers(re_viewer::build_info()); + + // Listen for TCP connections from Rerun's logging SDKs. + // There are other ways of "feeding" the viewer though - all you need is a `re_smart_channel::Receiver`. + let rx = re_sdk_comms::serve( + "0.0.0.0", + re_sdk_comms::DEFAULT_SERVER_PORT, + Default::default(), + ) + .await?; + + let native_options = eframe::NativeOptions { + app_id: Some("my_app_id".to_owned()), + ..re_viewer::native::eframe_options() + }; + + let startup_options = re_viewer::StartupOptions { + memory_limit: re_memory::MemoryLimit { + // Start pruning the data once we reach this much memory allocated + limit: Some(12_000_000_000), + }, + persist_state: true, + }; + + // This is used for analytics, if the `analytics` feature is on in `Cargo.toml` + let app_env = re_viewer::AppEnvironment::Custom("My Wrapper".to_owned()); + + let window_title = "My Customized Viewer"; + eframe::run_native( + window_title, + native_options, + Box::new(move |cc| { + let rx = re_viewer::wake_up_ui_thread_on_each_msg(rx, cc.egui_ctx.clone()); + + let re_ui = re_viewer::customize_eframe(cc); + + let rerun_app = re_viewer::App::from_receiver( + re_viewer::build_info(), + &app_env, + startup_options, + re_ui, + cc.storage, + rx, + ); + + Box::new(MyApp { rerun_app }) + }), + )?; + + Ok(()) +} + +struct MyApp { + rerun_app: re_viewer::App, +} + +impl eframe::App for MyApp { + fn save(&mut self, storage: &mut dyn eframe::Storage) { + // Store viewer state on disk + self.rerun_app.save(storage); + } + + /// Called whenever we need repainting, which could be 60 Hz. + fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + // First add our panel(s): + egui::SidePanel::right("my_side_panel") + .default_width(200.0) + .show(ctx, |ui| { + self.ui(ui); + }); + + // Now show the Rerun Viewer in the remaining space: + self.rerun_app.update(ctx, frame); + } +} + +impl MyApp { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.add_space(4.0); + ui.vertical_centered(|ui| { + ui.strong("My custom panel"); + }); + ui.separator(); + + if let Some(log_db) = self.rerun_app.log_db() { + log_db_ui(ui, log_db); + } else { + ui.label("No log database loaded yet."); + } + } +} + +/// Show the content of the log database. +fn log_db_ui(ui: &mut egui::Ui, log_db: &re_data_store::LogDb) { + if let Some(recording_info) = log_db.recording_info() { + ui.label(format!("Application ID: {}", recording_info.application_id)); + } + + // There can be many timelines, but the `log_time` timeline is always there: + let timeline = re_log_types::Timeline::log_time(); + + ui.separator(); + + ui.strong("Entities:"); + + egui::ScrollArea::vertical() + .auto_shrink([false, true]) + .show(ui, |ui| { + for entity_path in log_db.entity_db.entity_paths() { + ui.collapsing(entity_path.to_string(), |ui| { + entity_ui(ui, log_db, timeline, entity_path); + }); + } + }); +} + +fn entity_ui( + ui: &mut egui::Ui, + log_db: &re_data_store::LogDb, + timeline: re_log_types::Timeline, + entity_path: &re_log_types::EntityPath, +) { + // Each entity can have many components (e.g. position, color, radius, …): + if let Some(mut components) = log_db + .entity_db + .data_store + .all_components(&timeline, entity_path) + { + components.sort(); // Make the order predicatable + for component in components { + ui.collapsing(component.to_string(), |ui| { + component_ui(ui, log_db, timeline, entity_path, component); + }); + } + } +} + +fn component_ui( + ui: &mut egui::Ui, + log_db: &re_data_store::LogDb, + timeline: re_log_types::Timeline, + entity_path: &re_log_types::EntityPath, + component_name: re_log_types::ComponentName, +) { + // You can query the data for any time point, but for now + // just show the last value logged for each component: + let query = re_arrow_store::LatestAtQuery::latest(timeline); + + if let Some((_, component)) = re_query::get_component_with_instances( + &log_db.entity_db.data_store, + &query, + entity_path, + component_name, + ) { + egui::ScrollArea::vertical() + .auto_shrink([false, true]) + .show(ui, |ui| { + // Iterate over all the instances (e.g. all the points in the point cloud): + for instance_key in component.iter_instance_keys() { + if let Some(value) = component.lookup_arrow(&instance_key) { + ui.label(format_arrow(&*value)); + } + } + }); + }; +} + +fn format_arrow(value: &dyn arrow2::array::Array) -> String { + use re_log_types::SizeBytes as _; + + let bytes = value.total_size_bytes(); + if bytes < 256 { + // Print small items: + let mut string = String::new(); + let display = arrow2::array::get_display(value, "null"); + if display(&mut string, 0).is_ok() { + return string; + } + } + + // Fallback: + format!("{bytes} bytes") +} diff --git a/scripts/publish_crates.sh b/scripts/publish_crates.sh index 1dd17b329857..63bab31cd95f 100755 --- a/scripts/publish_crates.sh +++ b/scripts/publish_crates.sh @@ -99,6 +99,7 @@ cargo publish $FLAGS -p re_tuid cargo publish $FLAGS -p re_format cargo publish $FLAGS -p re_string_interner cargo publish $FLAGS -p re_analytics +cargo publish $FLAGS -p re_crash_handler cargo publish $FLAGS -p re_memory cargo publish $FLAGS -p re_log_types cargo publish $FLAGS -p re_smart_channel