diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e5d54b35306d..9eccacdfc7ae 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -145,3 +145,70 @@ jobs: - run: rustup target add wasm32-unknown-unknown - run: ./crates/re_viewer/setup_web.sh - run: ./crates/re_viewer/wasm_bindgen_check.sh + + maturin-linux: + name: Maturin Linux + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: messense/maturin-action@v1 + with: + manylinux: auto + maturin-version: "0.12.20" + command: build + args: -m crates/re_sdk_python/Cargo.toml --release -o dist + - name: Upload wheels + uses: actions/upload-artifact@v2 + with: + name: wheels + path: dist + + maturin-windows: + name: Maturin Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: messense/maturin-action@v1 + with: + maturin-version: "0.12.20" + command: build + args: -m crates/re_sdk_python/Cargo.toml --release --no-sdist -o dist + - name: Upload wheels + uses: actions/upload-artifact@v2 + with: + name: wheels + path: dist + + maturin-macos: + name: Maturin MacOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: messense/maturin-action@v1 + with: + maturin-version: "0.12.20" + command: build + args: -m crates/re_sdk_python/Cargo.toml --release --no-sdist -o dist --universal2 + - name: Upload wheels + uses: actions/upload-artifact@v2 + with: + name: wheels + path: dist + + maturin-release: + name: Maturin Release + runs-on: ubuntu-latest + if: "startsWith(github.ref, 'refs/tags/')" + needs: [maturin-macos, maturin-windows, maturin-linux] + steps: + - uses: actions/download-artifact@v2 + with: + name: wheels + - name: Publish to PyPI + uses: messense/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + maturin-version: "0.12.20" + command: upload + args: -m crates/re_sdk_python/Cargo.toml --skip-existing * diff --git a/.gitignore b/.gitignore index 691b141f07af..f1ad402a14f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ .DS_Store + +# Rust compile target directory: **/target + +# python virtual environment: +/env + /media /objectron/dataset diff --git a/.vscode/launch.json b/.vscode/launch.json index f800acfe500b..eb8009cf7e5d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,16 +5,16 @@ "version": "0.2.0", "configurations": [ { - "name": "Debug 'viewer' recording.rrd", + "name": "Debug 're_viewer' recording.rrd", "type": "lldb", "request": "launch", "cargo": { "args": [ "build", - "--package=viewer" + "--package=re_viewer" ], "filter": { - "name": "viewer", + "name": "re_viewer", "kind": "bin" } }, @@ -61,23 +61,5 @@ ], "cwd": "${workspaceFolder}" }, - { - "name": "Debug 'prototype' tests", - "type": "lldb", - "request": "launch", - "cargo": { - "args": [ - "test", - "--no-run", - "--package=prototype" - ], - "filter": { - "name": "prototype", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, ] } diff --git a/Cargo.lock b/Cargo.lock index 99988fd3e700..d10cce051aef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1750,6 +1750,12 @@ dependencies = [ "hashbrown 0.11.2", ] +[[package]] +name = "indoc" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05a0bd019339e5d968b37855180087b7b9d512c5046fbd244cf8c95687927d6e" + [[package]] name = "inflections" version = "1.1.1" @@ -1953,6 +1959,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "matrixmultiply" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add85d4dd35074e6fedc608f8c8f513a3548619a9024b751949ef0e8e45a4d84" +dependencies = [ + "rawpointer", +] + [[package]] name = "memchr" version = "2.5.0" @@ -2038,6 +2053,19 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ndarray" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec23e6762830658d2b3d385a75aa212af2f67a4586d4442907144f3bb6a1ca8" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", +] + [[package]] name = "ndk" version = "0.5.0" @@ -2170,6 +2198,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-complex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2243,6 +2280,19 @@ dependencies = [ "syn", ] +[[package]] +name = "numpy" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ae168529a39fc97cbc1d9d4fa865377731a519bc27553ed96f50594de7c45" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-traits", + "pyo3", +] + [[package]] name = "nyud" version = "0.1.0" @@ -2620,6 +2670,65 @@ dependencies = [ "puffin", ] +[[package]] +name = "pyo3" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6302e85060011447471887705bb7838f14aba43fcb06957d823739a496b3dc" +dependencies = [ + "cfg-if 1.0.0", + "indoc", + "libc", + "parking_lot 0.12.0", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b65b546c35d8a3b1b2f0ddbac7c6a569d759f357f2b9df884f5d6b719152c8" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c275a07127c1aca33031a563e384ffdd485aee34ef131116fcd58e3430d1742b" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284fc4485bfbcc9850a6d661d627783f18d19c2ab55880b021671c4ba83e90f7" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bda0f58f73f5c5429693c96ed57f7abdb38fdfc28ae06da4101a257adb7faf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.18" @@ -2668,6 +2777,12 @@ dependencies = [ "cty", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.5.3" @@ -2722,6 +2837,31 @@ dependencies = [ "zstd", ] +[[package]] +name = "re_sdk_comms" +version = "0.1.0" +dependencies = [ + "anyhow", + "bincode", + "re_log_types", + "tracing", +] + +[[package]] +name = "re_sdk_python" +version = "0.1.0" +dependencies = [ + "nohash-hasher", + "numpy", + "once_cell", + "pyo3", + "pyo3-build-config", + "re_log_types", + "re_sdk_comms", + "tracing", + "tracing-subscriber", +] + [[package]] name = "re_string_interner" version = "0.1.0" @@ -2764,6 +2904,7 @@ dependencies = [ "rand", "re_data_store", "re_log_types", + "re_sdk_comms", "re_string_interner", "re_ws_comms", "rfd", @@ -3237,6 +3378,12 @@ dependencies = [ "version-compare", ] +[[package]] +name = "target-lexicon" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1" + [[package]] name = "tempfile" version = "3.3.0" @@ -3611,6 +3758,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +[[package]] +name = "unindent" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52fee519a3e570f7df377a06a1a7775cdbfb7aa460be7e08de2b1f0e69973a44" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index a9ce270a3030..beca79aa5df0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,8 @@ resolver = "2" members = [ "crates/re_data_store", "crates/re_log_types", + "crates/re_sdk_comms", + "crates/re_sdk_python", "crates/re_string_interner", "crates/re_viewer", "crates/re_web_server", diff --git a/Cranky.toml b/Cranky.toml index 8b6def7318cf..1bf9bdcaf1f9 100644 --- a/Cranky.toml +++ b/Cranky.toml @@ -82,6 +82,7 @@ warn = [ "clippy::todo", "clippy::trailing_empty_array", "clippy::trait_duplication_in_bounds", + "clippy::undocumented_unsafe_blocks", "clippy::unimplemented", "clippy::unnecessary_wraps", "clippy::unnested_or_patterns", @@ -95,6 +96,7 @@ warn = [ "rust_2018_idioms", "rust_2021_prelude_collisions", "rustdoc::missing_crate_level_docs", + "rustdoc::unsafe_op_in_unsafe_fn", "semicolon_in_expressions_from_macros", "trivial_numeric_casts", "unused_extern_crates", diff --git a/crates/re_log_types/src/encoding.rs b/crates/re_log_types/src/encoding.rs index a1784ce8773e..6fb24605db99 100644 --- a/crates/re_log_types/src/encoding.rs +++ b/crates/re_log_types/src/encoding.rs @@ -164,6 +164,12 @@ fn test_encode_decode() { "snake", 1337.0, )), + LogMsg::from(data_msg( + &time_point, + ObjPathBuilder::from("badger") / "mushroom", + "pos", + Data::Vec3([1.0, 2.5, 3.0]), + )), ]; let mut file = vec![]; diff --git a/crates/re_log_types/src/time.rs b/crates/re_log_types/src/time.rs index 717ecbb97723..745987943ceb 100644 --- a/crates/re_log_types/src/time.rs +++ b/crates/re_log_types/src/time.rs @@ -6,10 +6,15 @@ use std::ops::RangeInclusive; pub struct Time(i64); impl Time { - // #[inline] - // pub fn now() -> Self { - // Self(nanos_since_epoch()) - // } + #[cfg(not(target_arch = "wasm32"))] + #[inline] + pub fn now() -> Self { + let nanos_since_epoch = std::time::SystemTime::UNIX_EPOCH + .elapsed() + .expect("Expected system clock to be set to after 1970") + .as_nanos() as _; + Self(nanos_since_epoch) + } #[inline] pub fn nanos_since_epoch(&self) -> i64 { diff --git a/crates/re_sdk_comms/Cargo.toml b/crates/re_sdk_comms/Cargo.toml new file mode 100644 index 000000000000..0140a686f88e --- /dev/null +++ b/crates/re_sdk_comms/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "re_sdk_comms" +version = "0.1.0" +edition = "2021" +rust-version = "1.62" +license = "MIT OR Apache-2.0" +publish = false + + +[features] +client = [] +server = [] + + +[dependencies] +re_log_types = { path = "../re_log_types", features = ["serde"] } + +anyhow = "1.0.56" +bincode = "1.3" +tracing = "0.1" diff --git a/crates/re_sdk_comms/README.md b/crates/re_sdk_comms/README.md new file mode 100644 index 000000000000..de5e211cd584 --- /dev/null +++ b/crates/re_sdk_comms/README.md @@ -0,0 +1 @@ +TCP communication between Rerun SDK and Rerun Server. diff --git a/crates/re_sdk_comms/src/client.rs b/crates/re_sdk_comms/src/client.rs new file mode 100644 index 000000000000..9505f58036d7 --- /dev/null +++ b/crates/re_sdk_comms/src/client.rs @@ -0,0 +1,75 @@ +use std::net::{SocketAddr, TcpStream}; + +use re_log_types::LogMsg; + +/// Connect to a rerun server and send log messages. +pub struct Client { + addrs: Vec, + stream: Option, +} + +impl Default for Client { + fn default() -> Self { + Self { + addrs: vec![crate::default_server_addr()], + stream: None, + } + } +} + +impl Client { + pub fn set_addr(&mut self, addr: SocketAddr) { + self.addrs = vec![addr]; + self.stream = None; + } + + pub fn send(&mut self, log_msg: &LogMsg) { + use std::io::Write as _; + + if self.stream.is_none() { + match TcpStream::connect(&self.addrs[..]) { + Ok(mut stream) => { + if let Err(err) = stream.write(&crate::PROTOCOL_VERSION.to_le_bytes()) { + tracing::warn!( + "Failed to send to Rerun server at {:?}: {err:?}", + self.addrs + ); + } else { + stream + .set_nonblocking(true) + .expect("set_nonblocking call failed"); + self.stream = Some(stream); + } + } + Err(err) => { + tracing::warn!( + "Failed to connect to Rerun server at {:?}: {err:?}", + self.addrs + ); + } + } + } + + if let Some(stream) = &mut self.stream { + let msg = crate::encode_log_msg(log_msg); + + tracing::trace!("Sending a LogMsg of size {}…", msg.len()); + if let Err(err) = stream.write(&(msg.len() as u32).to_le_bytes()) { + tracing::warn!( + "Failed to send to Rerun server at {:?}: {err:?}", + self.addrs + ); + self.stream = None; + return; + } + + if let Err(err) = stream.write(&msg) { + tracing::warn!( + "Failed to send to Rerun server at {:?}: {err:?}", + self.addrs + ); + self.stream = None; + } + } + } +} diff --git a/crates/re_sdk_comms/src/lib.rs b/crates/re_sdk_comms/src/lib.rs new file mode 100644 index 000000000000..68109a936cb0 --- /dev/null +++ b/crates/re_sdk_comms/src/lib.rs @@ -0,0 +1,48 @@ +//! Communications between server and viewer + +#[cfg(feature = "client")] +mod client; + +#[cfg(feature = "client")] +pub use client::Client; + +#[cfg(feature = "server")] +mod server; + +#[cfg(feature = "server")] +pub use server::serve; + +use re_log_types::LogMsg; + +pub type Result = anyhow::Result; + +pub const PROTOCOL_VERSION: u16 = 0; + +pub const DEFAULT_SERVER_PORT: u16 = 9876; + +pub fn default_server_addr() -> std::net::SocketAddr { + std::net::SocketAddr::from(([127, 0, 0, 1], DEFAULT_SERVER_PORT)) +} + +const PREFIX: [u8; 4] = *b"RR00"; + +pub fn encode_log_msg(log_msg: &LogMsg) -> Vec { + use bincode::Options as _; + let mut bytes = PREFIX.to_vec(); + bincode::DefaultOptions::new() + .serialize_into(&mut bytes, log_msg) + .unwrap(); + bytes +} + +pub fn decode_log_msg(data: &[u8]) -> Result { + let payload = data + .strip_prefix(&PREFIX) + .ok_or_else(|| anyhow::format_err!("Message didn't start with the correct prefix"))?; + + use anyhow::Context as _; + use bincode::Options as _; + bincode::DefaultOptions::new() + .deserialize(payload) + .context("bincode") +} diff --git a/crates/re_sdk_comms/src/server.rs b/crates/re_sdk_comms/src/server.rs new file mode 100644 index 000000000000..3685c3ec6262 --- /dev/null +++ b/crates/re_sdk_comms/src/server.rs @@ -0,0 +1,90 @@ +//! TODO(emilk): use tokio instead + +use std::sync::mpsc::{Receiver, Sender}; + +use re_log_types::LogMsg; + +/// ``` +/// let log_msg_rx = serve("127.0.0.1:80")?; +/// ``` +pub fn serve(addr: impl std::net::ToSocketAddrs) -> anyhow::Result> { + let listener = std::net::TcpListener::bind(addr)?; + + let (tx, rx) = std::sync::mpsc::channel(); + + std::thread::Builder::new() + .name("sdk-server".into()) + .spawn(move || { + // accept connections and process them serially + for stream in listener.incoming() { + match stream { + Ok(stream) => { + let tx = tx.clone(); + handle_client(stream, tx); + } + Err(err) => { + tracing::warn!("Failed to accept incoming SDK client: {err:?}"); + } + } + } + }) + .expect("Failed to spawn thread"); + + Ok(rx) +} + +fn handle_client(stream: std::net::TcpStream, tx: Sender) { + std::thread::Builder::new() + .name("sdk-server-client-handler".into()) + .spawn(move || { + tracing::info!("New SDK client connected: {:?}", stream.peer_addr()); + + if let Err(err) = run_client(stream, &tx) { + tracing::warn!("Closing connection to client: {err:?}"); + } + }) + .expect("Failed to spawn thread"); +} + +fn run_client(mut stream: std::net::TcpStream, tx: &Sender) -> anyhow::Result<()> { + use std::io::Read as _; + + let mut client_version = [0_u8; 2]; + stream.read_exact(&mut client_version)?; + let client_version = u16::from_le_bytes(client_version); + + 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 ({}).", + client_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 ({}).", + client_version, + crate::PROTOCOL_VERSION + ); + } + } + + let mut packet = Vec::new(); + + loop { + let mut packet_size = [0_u8; 4]; + stream.read_exact(&mut packet_size)?; + let packet_size = u32::from_le_bytes(packet_size); + + packet.resize(packet_size as usize, 0_u8); + stream.read_exact(&mut packet)?; + + tracing::trace!("Received log message of size {packet_size}."); + + let msg = crate::decode_log_msg(&packet)?; + + tx.send(msg)?; + } +} diff --git a/crates/re_sdk_python/Cargo.toml b/crates/re_sdk_python/Cargo.toml new file mode 100644 index 000000000000..15fd88ab3288 --- /dev/null +++ b/crates/re_sdk_python/Cargo.toml @@ -0,0 +1,35 @@ +[package] +edition = "2021" +license = "MIT OR Apache-2.0" +name = "re_sdk_python" # name of the rust crate +publish = false +rust-version = "1.62" +version = "0.1.0" + +[package.metadata.maturin] +python-source = "python" + + +[lib] +crate-type = ["cdylib"] +name = "rerun_sdk" # name of the Python library + + +[features] +extension-module = ["pyo3/extension-module"] +default = ["extension-module"] + + +[dependencies] +re_log_types = { path = "../re_log_types" } +re_sdk_comms = { path = "../re_sdk_comms", features = ["client"] } + +nohash-hasher = "0.2" +numpy = "0.16" +once_cell = "1.12" +pyo3 = "0.16.5" +tracing = "0.1" +tracing-subscriber = "0.3" + +[build-dependencies] +pyo3-build-config = "0.16.5" diff --git a/crates/re_sdk_python/README.md b/crates/re_sdk_python/README.md new file mode 100644 index 000000000000..96b83bb9515e --- /dev/null +++ b/crates/re_sdk_python/README.md @@ -0,0 +1,37 @@ +The Rerun Python Log SDK. + +Goal: an ergonomic Python library for logging rich data, over TCP, to a rerun server. + +Note: The rust crate is called `re_sdk_python`, while the Python library is called `rerun_sdk`. + +## Setup + +Run this is in the workspace root: + +``` +python3 -m venv env +source env/bin/activate +python3 -m pip install -r crates/re_sdk_python/requirements.txt +``` + +The Python bindings is using https://github.com/PyO3/pyo3 + + +## Testing +First start up a viewer with a server that the logger can connect to: + +```sh +(cd crates/re_viewer && RUST_LOG=debug cargo r --features server -- --host) +``` + +Then run the test logging: + +Debug build: +``` sh +maturin develop -m crates/re_sdk_python/Cargo.toml && RUST_LOG=debug python3 crates/re_sdk_python/test.py +``` + +Release build: +``` sh +maturin develop -m crates/re_sdk_python/Cargo.toml --release && RUST_LOG=debug python3 crates/re_sdk_python/test.py +``` diff --git a/crates/re_sdk_python/build.rs b/crates/re_sdk_python/build.rs new file mode 100644 index 000000000000..647f3d4e8e51 --- /dev/null +++ b/crates/re_sdk_python/build.rs @@ -0,0 +1,4 @@ +fn main() { + // Required for `cargo build` to work on mac: https://pyo3.rs/v0.14.2/building_and_distribution.html#macos + pyo3_build_config::add_extension_module_link_args(); +} diff --git a/crates/re_sdk_python/pyproject.toml b/crates/re_sdk_python/pyproject.toml new file mode 100644 index 000000000000..4f0f5e05992c --- /dev/null +++ b/crates/re_sdk_python/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +build-backend = "maturin" +requires = ["maturin>=0.12,<0.13"] + +[project] +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = ["opencv-python"] +name = "rerun_sdk" +requires-python = ">=3.7" diff --git a/crates/re_sdk_python/python/rerun_sdk/.gitignore b/crates/re_sdk_python/python/rerun_sdk/.gitignore new file mode 100644 index 000000000000..4cd992125909 --- /dev/null +++ b/crates/re_sdk_python/python/rerun_sdk/.gitignore @@ -0,0 +1,2 @@ +*.so +*.pyc diff --git a/crates/re_sdk_python/python/rerun_sdk/__init__.py b/crates/re_sdk_python/python/rerun_sdk/__init__.py new file mode 100644 index 000000000000..d99a2230795c --- /dev/null +++ b/crates/re_sdk_python/python/rerun_sdk/__init__.py @@ -0,0 +1,5 @@ +# The Rerun Python SDK, which wraped around the Rust crate rerun_sdk + +from .rerun_sdk import * + +print("rerun sdk initialized") diff --git a/crates/re_sdk_python/requirements.txt b/crates/re_sdk_python/requirements.txt new file mode 100644 index 000000000000..d14f0ce891a4 --- /dev/null +++ b/crates/re_sdk_python/requirements.txt @@ -0,0 +1,2 @@ +maturin +opencv-python diff --git a/crates/re_sdk_python/src/lib.rs b/crates/re_sdk_python/src/lib.rs new file mode 100644 index 000000000000..c0df071eb5da --- /dev/null +++ b/crates/re_sdk_python/src/lib.rs @@ -0,0 +1,2 @@ +mod python_bridge; +pub(crate) mod sdk; diff --git a/crates/re_sdk_python/src/python_bridge.rs b/crates/re_sdk_python/src/python_bridge.rs new file mode 100644 index 000000000000..20ccb1ed31f4 --- /dev/null +++ b/crates/re_sdk_python/src/python_bridge.rs @@ -0,0 +1,114 @@ +use pyo3::prelude::*; +use re_log_types::{ + Data, DataMsg, DataPath, LogId, LogMsg, ObjPath, ObjectType, TimePoint, TimeValue, +}; + +use crate::sdk::Sdk; + +/// The python module is called "rerun_sdk". +#[pymodule] +fn rerun_sdk(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + // Log to stdout (if you run with `RUST_LOG=debug`). + tracing_subscriber::fmt::init(); + + m.add_function(wrap_pyfunction!(info, m)?)?; + m.add_function(wrap_pyfunction!(log_point, m)?)?; + m.add_function(wrap_pyfunction!(log_image, m)?)?; + Ok(()) +} + +#[pyfunction] +fn info() -> String { + "Rerun Python SDK".to_owned() +} + +#[pyfunction] +fn log_point(name: &str, x: f32, y: f32) { + let mut sdk = Sdk::global(); + + let obj_path = ObjPath::from(name); // TODO(emilk): pass in proper obj path somehow + sdk.register_type(obj_path.obj_type_path(), ObjectType::Point2D); + let data_path = DataPath::new(obj_path, "pos".into()); + + let data = Data::Vec2([x, y]); + let data_msg = DataMsg { + id: LogId::random(), + time_point: time_point(), + data_path, + data: re_log_types::LoggedData::Single(data), + }; + let log_msg = LogMsg::DataMsg(data_msg); + sdk.send(&log_msg); +} + +#[allow(clippy::needless_pass_by_value)] +#[pyfunction] +fn log_image(name: &str, img: numpy::PyReadonlyArrayDyn<'_, u8>) -> PyResult<()> { + let mut sdk = Sdk::global(); + + let obj_path = ObjPath::from(name); // TODO(emilk): pass in proper obj path somehow + sdk.register_type(obj_path.obj_type_path(), ObjectType::Image); + let data_path = DataPath::new(obj_path, "image".into()); + + let image = to_rerun_image(&img)?; + + let data = Data::Image(image); + let data_msg = DataMsg { + id: LogId::random(), + time_point: time_point(), + data_path, + data: re_log_types::LoggedData::Single(data), + }; + let log_msg = LogMsg::DataMsg(data_msg); + sdk.send(&log_msg); + + Ok(()) +} + +fn time_point() -> TimePoint { + let mut time_point = TimePoint::default(); + time_point.0.insert( + "log_time".into(), + TimeValue::Time(re_log_types::Time::now()), + ); + time_point +} + +fn to_rerun_image(img: &numpy::PyReadonlyArrayDyn<'_, u8>) -> PyResult { + let shape = img.shape(); + + let [w, h, depth] = match shape.len() { + // NOTE: opencv/numpy uses "height x width" convention + 2 => [shape[1], shape[0], 1], + 3 => [shape[1], shape[0], shape[2]], + _ => { + return Err(pyo3::exceptions::PyTypeError::new_err(format!( + "Expected image of dim 2 or 3. Got image of shape {shape:?}" + ))); + } + }; + + let size = [w as u32, h as u32]; + let data = img.to_owned_array().into_raw_vec(); + + if data.len() != w * h * depth { + return Err(pyo3::exceptions::PyTypeError::new_err(format!( + "Got image of shape {shape:?} (product = {}), but data length is {}", + w * h * depth, + data.len() + ))); + } + + let format = match depth { + 1 => re_log_types::ImageFormat::Luminance8, + 3 => re_log_types::ImageFormat::Rgb8, + 4 => re_log_types::ImageFormat::Rgba8, + _ => { + return Err(pyo3::exceptions::PyTypeError::new_err(format!( + "Expected depth to be one of 1,3,4. Got image of shape {shape:?}", + ))); + } + }; + + Ok(re_log_types::Image { size, format, data }) +} diff --git a/crates/re_sdk_python/src/sdk.rs b/crates/re_sdk_python/src/sdk.rs new file mode 100644 index 000000000000..8420fea9839c --- /dev/null +++ b/crates/re_sdk_python/src/sdk.rs @@ -0,0 +1,44 @@ +use re_log_types::{LogId, LogMsg, ObjTypePath, ObjectType, TypeMsg}; + +#[derive(Default)] +pub struct Sdk { + // TODO(emilk): also support sending over `mpsc::Sender`. + sender: re_sdk_comms::Client, + + // TODO(emilk): just store `ObjTypePathHash` + registered_types: nohash_hasher::IntMap, +} + +impl Sdk { + /// Access the global [`Sdk`]. This is a singleton. + pub fn global() -> std::sync::MutexGuard<'static, Self> { + use once_cell::sync::OnceCell; + use std::sync::Mutex; + static INSTANCE: OnceCell> = OnceCell::new(); + let mutex = INSTANCE.get_or_init(Default::default); + mutex.lock().unwrap() + } + + pub fn register_type(&mut self, obj_type_path: &ObjTypePath, typ: ObjectType) { + if let Some(prev_type) = self.registered_types.get(obj_type_path) { + if *prev_type != typ { + tracing::warn!("Registering different types to the same object type path: {}. First you uses {:?}, then {:?}", + obj_type_path, prev_type, typ); + } + } else { + self.registered_types.insert(obj_type_path.clone(), typ); + + self.send(&LogMsg::TypeMsg(TypeMsg { + id: LogId::random(), + type_path: obj_type_path.clone(), + object_type: typ, + })); + } + } +} + +impl Sdk { + pub fn send(&mut self, log_msg: &LogMsg) { + self.sender.send(log_msg); + } +} diff --git a/crates/re_sdk_python/test.py b/crates/re_sdk_python/test.py new file mode 100644 index 000000000000..55728c020822 --- /dev/null +++ b/crates/re_sdk_python/test.py @@ -0,0 +1,22 @@ +import cv2 +import math +import time + +import rerun_sdk + + +print(rerun_sdk.info()) + +img = cv2.imread('crates/re_viewer/data/logo_dark_mode.png', + cv2.IMREAD_UNCHANGED) +rerun_sdk.log_image("logo", img) + +for i in range(64): + angle = 6.28 * i / 64 + r = 20.0 + x = r * math.cos(angle) + 18.0 + y = r * math.sin(angle) + 16.0 + rerun_sdk.log_point(f"point_{i}", x, y) + + +time.sleep(1.0) # HACK: give rerun time to send it all diff --git a/crates/re_viewer/Cargo.toml b/crates/re_viewer/Cargo.toml index 6fedaa318bd3..053d40d86d65 100644 --- a/crates/re_viewer/Cargo.toml +++ b/crates/re_viewer/Cargo.toml @@ -13,14 +13,20 @@ crate-type = ["cdylib", "rlib"] [features] default = ["puffin"] + +## Add support for the [`puffin`](https://github.com/EmbarkStudios/puffin) profiler. puffin = ["dep:puffin", "dep:puffin_http", "eframe/puffin"] +## Run a HTTP server that listens to incoming log messages from a Rerun SDK. +server = ["dep:re_sdk_comms"] + [dependencies] -re_ws_comms = { path = "../re_ws_comms", features = ["client"] } re_data_store = { path = "../re_data_store", features = ["puffin"] } re_log_types = { path = "../re_log_types", features = ["save", "load"] } +re_sdk_comms = { path = "../re_sdk_comms", optional=true, features = ["server"] } re_string_interner = { path = "../re_string_interner" } +re_ws_comms = { path = "../re_ws_comms", features = ["client"] } # We use bleeding edge egui, from 2022-06-09 eframe = { git = "https://github.com/emilk/egui", rev = "317436c0570dde617fb408d6c9a973c5dd0a2fc4", features = ["persistence"] } diff --git a/crates/re_viewer/README.md b/crates/re_viewer/README.md index 3cd0918a7c18..245a5aa59cbf 100644 --- a/crates/re_viewer/README.md +++ b/crates/re_viewer/README.md @@ -7,3 +7,10 @@ This is both a library and a binary. Can be compiled both natively for desktop, Talks to the server over WebSockets (using `re_ws_comms`). `cargo run --release -p re_viewer -- --help` + +## Hosting an SDK server +This will host an SDK server inline in the viewer, that SDK:s can connect to: + +```sh +(cd crates/re_viewer && RUST_LOG=debug cargo r --features server -- --host) +``` diff --git a/crates/re_viewer/src/main.rs b/crates/re_viewer/src/main.rs index ed537370994e..dfe7771015b5 100644 --- a/crates/re_viewer/src/main.rs +++ b/crates/re_viewer/src/main.rs @@ -4,7 +4,13 @@ compile_error!("Feature 'puffin' must be enabled when compiling the viewer binar #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; -/// Run viewer connected to a rerun server over websocket. +/// The Rerun Viewer +/// +/// Features: +/// +/// * Read `.rrd` (rerun recording files). +/// * Connect to a Rerun Server over web-sockets. +/// * Host a Rerun Server that Rerun SDK:s can connect to. #[derive(Debug, clap::Parser)] #[clap(author, version, about, long_about = None)] struct Args { @@ -12,8 +18,18 @@ struct Args { #[clap(long)] profile: bool, + /// Host a Rerun Server that the SDK can connect to. + #[cfg(feature = "server")] + #[clap(long)] + host: bool, + + /// When using `--host`, what port do we listen on? + #[cfg(feature = "server")] + #[clap(long, default_value_t = re_sdk_comms::DEFAULT_SERVER_PORT)] + port: u16, + /// Either a path to a `.rrd` file, or an url to a websocket server. - url_or_path: String, + url_or_path: Option, } #[tokio::main] @@ -36,33 +52,56 @@ async fn main() { ..Default::default() }; - let path = std::path::Path::new(&args.url_or_path).to_path_buf(); - if path.exists() || args.url_or_path.ends_with(".rrd") { - eframe::run_native( - "rerun viewer", - native_options, - Box::new(move |cc| { - re_viewer::customize_egui(&cc.egui_ctx); - let mut app = re_viewer::App::from_rrd_path(cc.storage, &path); - app.set_profiler(profiler); - Box::new(app) - }), - ); - } else { - let mut url = args.url_or_path; - // let url = re_ws_comms::default_server_url(); - if !url.contains("://") { - url = format!("{}://{url}", re_ws_comms::PROTOCOL); + #[cfg(feature = "server")] + if args.host { + let bind_addr = format!("127.0.0.1:{}", args.port); + match re_sdk_comms::serve(&bind_addr) { + Ok(rx) => { + tracing::info!("Hosting SDK server on {bind_addr}"); + re_viewer::run_native_viewer(rx); + } + Err(err) => { + panic!("Failed to host: {err}"); + } + } + } + + if let Some(url_or_path) = &args.url_or_path { + let path = std::path::Path::new(url_or_path).to_path_buf(); + if path.exists() || url_or_path.ends_with(".rrd") { + eframe::run_native( + "rerun viewer", + native_options, + Box::new(move |cc| { + re_viewer::customize_egui(&cc.egui_ctx); + let mut app = re_viewer::App::from_rrd_path(cc.storage, &path); + app.set_profiler(profiler); + Box::new(app) + }), + ); + } else { + let mut url = url_or_path.clone(); + // let url = re_ws_comms::default_server_url(); + if !url.contains("://") { + url = format!("{}://{url}", re_ws_comms::PROTOCOL); + } + eframe::run_native( + "rerun viewer", + native_options, + Box::new(move |cc| { + re_viewer::customize_egui(&cc.egui_ctx); + let mut app = + re_viewer::RemoteViewerApp::new(cc.egui_ctx.clone(), cc.storage, url); + app.set_profiler(profiler); + Box::new(app) + }), + ); } - eframe::run_native( - "rerun viewer", - native_options, - Box::new(move |cc| { - re_viewer::customize_egui(&cc.egui_ctx); - let mut app = re_viewer::RemoteViewerApp::new(cc.egui_ctx.clone(), cc.storage, url); - app.set_profiler(profiler); - Box::new(app) - }), - ); + } + + if cfg!(feature = "server") { + panic!("No --host, nor url or .rrd path given"); + } else { + panic!("No url or .rrd path given"); } } diff --git a/crates/re_viewer/src/native.rs b/crates/re_viewer/src/native.rs index 718ddc944f67..871e1aac7843 100644 --- a/crates/re_viewer/src/native.rs +++ b/crates/re_viewer/src/native.rs @@ -2,7 +2,7 @@ use std::sync::mpsc::Receiver; use re_log_types::LogMsg; -pub fn run_native_viewer(rx: Receiver) { +pub fn run_native_viewer(rx: Receiver) -> ! { let native_options = eframe::NativeOptions { depth_buffer: 24, multisampling: 8, diff --git a/crates/re_viewer/src/ui/view2d.rs b/crates/re_viewer/src/ui/view2d.rs index 6b27db98d178..6fd6eae031ef 100644 --- a/crates/re_viewer/src/ui/view2d.rs +++ b/crates/re_viewer/src/ui/view2d.rs @@ -13,14 +13,14 @@ pub(crate) struct State2D { /// Estimate of the the bounding box of all data. Accumulated. #[serde(skip)] - scene_bbox: epaint::Rect, + scene_bbox_accum: epaint::Rect, } impl Default for State2D { fn default() -> Self { Self { hovered_obj: Default::default(), - scene_bbox: epaint::Rect::NOTHING, + scene_bbox_accum: epaint::Rect::NOTHING, } } } @@ -28,8 +28,8 @@ impl Default for State2D { impl State2D { /// Size of the 2D bounding box, if any. pub fn size(&self) -> Option { - if self.scene_bbox.is_positive() { - Some(self.scene_bbox.size()) + if self.scene_bbox_accum.is_positive() { + Some(self.scene_bbox_accum.size()) } else { None } @@ -46,14 +46,26 @@ pub(crate) fn combined_view_2d( ) { crate::profile_function!(); - state.scene_bbox = state.scene_bbox.union(crate::misc::calc_bbox_2d(objects)); + state.scene_bbox_accum = state + .scene_bbox_accum + .union(crate::misc::calc_bbox_2d(objects)); + let scene_bbox = if state.scene_bbox_accum.is_positive() { + state.scene_bbox_accum + } else { + Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)) + }; let desired_size = { let max_size = ui.available_size(); - let mut desired_size = state.scene_bbox.size(); + let mut desired_size = scene_bbox.size(); desired_size *= max_size.x / desired_size.x; // fill full width desired_size *= (max_size.y / desired_size.y).at_most(1.0); // shrink so we don't fill more than full height - desired_size + + if desired_size.is_finite() { + desired_size + } else { + max_size + } }; let (response, painter) = ui.allocate_painter(desired_size, egui::Sense::click()); @@ -72,11 +84,11 @@ pub(crate) fn combined_view_2d( // ------------------------------------------------------------------------ - let to_screen = egui::emath::RectTransform::from_to(state.scene_bbox, response.rect); + let to_screen = egui::emath::RectTransform::from_to(scene_bbox, response.rect); // Paint background in case there is no image covering it all: let mut shapes = vec![Shape::rect_filled( - to_screen.transform_rect(state.scene_bbox), + to_screen.transform_rect(scene_bbox), 3.0, ui.visuals().extreme_bg_color, )]; diff --git a/crates/re_viewer/src/ui/view3d.rs b/crates/re_viewer/src/ui/view3d.rs index 3148b58e6fde..7f330379d1dc 100644 --- a/crates/re_viewer/src/ui/view3d.rs +++ b/crates/re_viewer/src/ui/view3d.rs @@ -487,6 +487,7 @@ fn with_three_d_context( } #[allow(unsafe_code)] + // SAFETY: we should have a valid glow context here, and we _should_ be in the correct thread. unsafe { use glow::HasContext as _; gl.enable(glow::DEPTH_TEST); diff --git a/crates/re_ws_comms/src/client.rs b/crates/re_ws_comms/src/client.rs index aa5f3af63354..b746eafa99d7 100644 --- a/crates/re_ws_comms/src/client.rs +++ b/crates/re_ws_comms/src/client.rs @@ -15,6 +15,7 @@ impl Connection { url: String, on_log_msg: impl Fn(LogMsg) -> ControlFlow<()> + Send + 'static, ) -> Result { + tracing::info!("Connecting to {url:?}…"); let sender = ewebsock::ws_connect( url, Box::new(move |event: WsEvent| match event { diff --git a/deny.toml b/deny.toml index 04fb112ef498..18f760188ca9 100644 --- a/deny.toml +++ b/deny.toml @@ -44,17 +44,17 @@ allow-osi-fsf-free = "neither" confidence-threshold = 0.92 # We want really high confidence when inferring licenses from text copyleft = "deny" allow = [ - # "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html - "Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0) - "BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd) - "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised) - "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained - "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/ - "ISC", # https://tldrlegal.com/license/-isc-license - "MIT", # https://tldrlegal.com/license/mit-license - "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux. - "OpenSSL", # https://www.openssl.org/source/license.html - used on Linux - "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) + "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html + "Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0) + "BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd) + "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised) + "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained + "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/ + "ISC", # https://tldrlegal.com/license/-isc-license + "MIT", # https://tldrlegal.com/license/mit-license + "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux. + "OpenSSL", # https://www.openssl.org/source/license.html - used on Linux + "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) ] [[licenses.clarify]]