diff --git a/Cargo.lock b/Cargo.lock index 51d350994063..e946a50a1935 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6966,8 +6966,10 @@ dependencies = [ "directories", "egui", "egui-wgpu", + "egui_kittest", "egui_tiles", "emath", + "env_logger", "glam", "half", "home", @@ -6979,6 +6981,7 @@ dependencies = [ "nohash-hasher", "once_cell", "parking_lot", + "pollster 0.4.0", "re_capabilities", "re_chunk", "re_chunk_store", diff --git a/crates/viewer/re_component_ui/Cargo.toml b/crates/viewer/re_component_ui/Cargo.toml index bef6b1d32796..2c849f389994 100644 --- a/crates/viewer/re_component_ui/Cargo.toml +++ b/crates/viewer/re_component_ui/Cargo.toml @@ -39,6 +39,8 @@ egui.workspace = true [dev-dependencies] +re_viewer_context = { workspace = true, features = ["testing"] } + egui_kittest.workspace = true itertools.workspace = true nohash-hasher.workspace = true diff --git a/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_narrow/Colormap_placeholder.png b/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_narrow/Colormap_placeholder.png index d11afc2fbfdb..a0a2955ab84a 100644 --- a/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_narrow/Colormap_placeholder.png +++ b/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_narrow/Colormap_placeholder.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18b8ada626cbb59ec85db0ba8d167deeef516d18279d5b85ad3dd43bef7b8113 -size 3326 +oid sha256:addb336fa14268b87e22974902909b3a3e5629fbe483f7d98fe9ea04f2836e47 +size 2986 diff --git a/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_wide/Colormap_placeholder.png b/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_wide/Colormap_placeholder.png index d2de7193371d..692dc938a304 100644 --- a/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_wide/Colormap_placeholder.png +++ b/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_wide/Colormap_placeholder.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b15bc121e4102dbc979e92f5564c36ad7f085a3c9465518f3d90427d0fbe3f38 -size 3637 +oid sha256:64a67110962fafff3f8bef5f849b5401a6d7f2adebf3a3ac82d286ef70eea7ed +size 4208 diff --git a/crates/viewer/re_component_ui/tests/test_all_components_ui.rs b/crates/viewer/re_component_ui/tests/test_all_components_ui.rs index 639372b92e53..1c966332a2a7 100644 --- a/crates/viewer/re_component_ui/tests/test_all_components_ui.rs +++ b/crates/viewer/re_component_ui/tests/test_all_components_ui.rs @@ -213,7 +213,8 @@ fn test_single_component_ui_as_list_item( ); }; - let mut harness = egui_kittest::Harness::builder() + let mut harness = test_context + .setup_kittest_for_rendering() .with_size(Vec2::new(ui_width, 40.0)) .build_ui(|ui| { test_context.run(&ui.ctx().clone(), |ctx| { diff --git a/crates/viewer/re_time_panel/tests/time_panel_tests.rs b/crates/viewer/re_time_panel/tests/time_panel_tests.rs index 8feeeeb2ad1a..ae93eac6531e 100644 --- a/crates/viewer/re_time_panel/tests/time_panel_tests.rs +++ b/crates/viewer/re_time_panel/tests/time_panel_tests.rs @@ -76,7 +76,8 @@ fn run_time_panel_and_save_snapshot(mut test_context: TestContext, _snapshot_nam let mut panel = TimePanel::default(); //TODO(ab): this contains a lot of boilerplate which should be provided by helpers - let mut harness = egui_kittest::Harness::builder() + let mut harness = test_context + .setup_kittest_for_rendering() .with_size(Vec2::new(700.0, 300.0)) .build_ui(|ui| { test_context.run(&ui.ctx().clone(), |viewer_ctx| { diff --git a/crates/viewer/re_view_graph/tests/basic.rs b/crates/viewer/re_view_graph/tests/basic.rs index 409ea5864036..a394fe5b6c27 100644 --- a/crates/viewer/re_view_graph/tests/basic.rs +++ b/crates/viewer/re_view_graph/tests/basic.rs @@ -215,7 +215,8 @@ fn run_graph_view_and_save_snapshot( .collect(); //TODO(ab): this contains a lot of boilerplate which should be provided by helpers - let mut harness = egui_kittest::Harness::builder() + let mut harness = test_context + .setup_kittest_for_rendering() .with_size(size) .with_max_steps(256) // Give it some time to settle the graph. .build_ui(|ui| { diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs index c1cdd1033e6b..934705af994b 100644 --- a/crates/viewer/re_viewer/src/app.rs +++ b/crates/viewer/re_viewer/src/app.rs @@ -1822,7 +1822,6 @@ impl eframe::App for App { } // NOTE: GPU resource stats are cheap to compute so we always do. - // TODO(andreas): store the re_renderer somewhere else. let gpu_resource_stats = { re_tracing::profile_scope!("gpu_resource_stats"); @@ -1852,7 +1851,6 @@ impl eframe::App for App { self.purge_memory_if_needed(&mut store_hub); { - // TODO(andreas): store the re_renderer somewhere else. let egui_renderer = { let render_state = frame.wgpu_render_state().unwrap(); &mut render_state.renderer.read() diff --git a/crates/viewer/re_viewer/src/lib.rs b/crates/viewer/re_viewer/src/lib.rs index acee4e585132..85d5e1baa9ff 100644 --- a/crates/viewer/re_viewer/src/lib.rs +++ b/crates/viewer/re_viewer/src/lib.rs @@ -201,15 +201,15 @@ pub fn customize_eframe_and_setup_renderer( if let Some(render_state) = &cc.wgpu_render_state { use re_renderer::RenderContext; + // Put the renderer into paint callback resources, so we have access to the renderer + // when we need to process egui draw callbacks. let paint_callback_resources = &mut render_state.renderer.write().callback_resources; - let render_ctx = RenderContext::new( &render_state.adapter, render_state.device.clone(), render_state.queue.clone(), render_state.target_format, )?; - paint_callback_resources.insert(render_ctx); } diff --git a/crates/viewer/re_viewer_context/Cargo.toml b/crates/viewer/re_viewer_context/Cargo.toml index ff18d3d3bcf7..abbe4073254d 100644 --- a/crates/viewer/re_viewer_context/Cargo.toml +++ b/crates/viewer/re_viewer_context/Cargo.toml @@ -18,6 +18,10 @@ workspace = true [package.metadata.docs.rs] all-features = true +[features] +## Enable for testing utilities. +testing = ["dep:pollster", "dep:egui_kittest", "dep:env_logger"] + [dependencies] re_capabilities.workspace = true re_chunk_store.workspace = true @@ -69,6 +73,11 @@ thiserror.workspace = true uuid = { workspace = true, features = ["serde", "v4", "js"] } wgpu.workspace = true +# Optional dependencies: +egui_kittest = { workspace = true, features = ["wgpu"], optional = true } +env_logger = { workspace = true, optional = true } +pollster = { workspace = true, optional = true } + # Native dependencies: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] home.workspace = true diff --git a/crates/viewer/re_viewer_context/src/lib.rs b/crates/viewer/re_viewer_context/src/lib.rs index 82bb73c67971..fc955d94985d 100644 --- a/crates/viewer/re_viewer_context/src/lib.rs +++ b/crates/viewer/re_viewer_context/src/lib.rs @@ -24,7 +24,6 @@ mod selection_state; mod store_context; pub mod store_hub; mod tensor; -pub mod test_context; //TODO(ab): this should be behind #[cfg(test)], but then ` cargo clippy --all-targets` fails mod time_control; mod time_drag_value; mod typed_entity_collections; @@ -33,6 +32,9 @@ mod utils; mod view; mod viewer_context; +#[cfg(feature = "testing")] +pub mod test_context; + // TODO(andreas): Move to its own crate? pub mod gpu_bridge; diff --git a/crates/viewer/re_viewer_context/src/test_context.rs b/crates/viewer/re_viewer_context/src/test_context.rs index 3e014354bc54..cd9354ab1adb 100644 --- a/crates/viewer/re_viewer_context/src/test_context.rs +++ b/crates/viewer/re_viewer_context/src/test_context.rs @@ -1,8 +1,12 @@ use std::sync::Arc; use ahash::HashMap; +use once_cell::sync::Lazy; +use parking_lot::Mutex; + use re_chunk_store::LatestAtQuery; use re_entity_db::EntityDb; +use re_log::ResultExt as _; use re_log_types::{StoreId, StoreKind}; use re_types_core::reflection::Reflection; @@ -40,10 +44,15 @@ pub struct TestContext { command_sender: CommandSender, command_receiver: CommandReceiver, + egui_render_state: Mutex>, } impl Default for TestContext { fn default() -> Self { + // We rely a lot on logging in the viewer to identify issues. + // Make sure logging is set up if it hasn't been done yet. + let _ = env_logger::builder().is_test(true).try_init(); + let recording_store = EntityDb::new(StoreId::random(StoreKind::Recording)); let blueprint_store = EntityDb::new(StoreId::random(StoreKind::Blueprint)); @@ -72,11 +81,131 @@ impl Default for TestContext { reflection, command_sender, command_receiver, + + // Created lazily since each egui_kittest harness needs a new one. + egui_render_state: Mutex::new(None), } } } +/// Create an `egui_wgpu::RenderState` for tests. +/// +/// May be `None` if we failed to initialize the wgpu renderer setup. +fn create_egui_renderstate() -> Option { + re_tracing::profile_function!(); + + let shared_wgpu_setup = (*SHARED_WGPU_RENDERER_SETUP).as_ref()?; + + let config = egui_wgpu::WgpuConfiguration { + wgpu_setup: egui_wgpu::WgpuSetupExisting { + instance: shared_wgpu_setup.instance.clone(), + adapter: shared_wgpu_setup.adapter.clone(), + device: shared_wgpu_setup.device.clone(), + queue: shared_wgpu_setup.queue.clone(), + } + .into(), + + // None of these matter for tests as we're not going to draw to a surfaces. + present_mode: wgpu::PresentMode::Immediate, + desired_maximum_frame_latency: None, + on_surface_error: Arc::new(|_| { + unreachable!("tests aren't expected to draw to surfaces"); + }), + }; + + let compatible_surface = None; + // `re_renderer`'s individual views (managed each by a `ViewBuilder`) have MSAA, + // but egui's final target doesn't - re_renderer resolves and copies into egui in `ViewBuilder::composite`. + let msaa_samples = 1; + // Similarly, depth is handled by re_renderer. + let depth_format = None; + // Disable dithering in order to not unnecessarily add a source of noise & variance between renderers. + let dithering = false; + + let render_state = pollster::block_on(egui_wgpu::RenderState::create( + &config, + &shared_wgpu_setup.instance, + compatible_surface, + depth_format, + msaa_samples, + dithering, + )) + .expect("Failed to set up egui_wgpu::RenderState"); + + // Put re_renderer::RenderContext into the callback resources so that render callbacks can access it. + render_state.renderer.write().callback_resources.insert( + re_renderer::RenderContext::new( + &shared_wgpu_setup.adapter, + shared_wgpu_setup.device.clone(), + shared_wgpu_setup.queue.clone(), + wgpu::TextureFormat::Rgba8Unorm, + ) + .expect("Failed to initialize re_renderer"), + ); + Some(render_state) +} + +/// Instance & adapter +struct SharedWgpuResources { + instance: Arc, + adapter: Arc, + device: Arc, + + // Sharing the queue across parallel running tests should work fine in theory - it's obviously threadsafe. + // Note though that this becomes an odd sync point that is shared with all tests that put in work here. + queue: Arc, +} + +static SHARED_WGPU_RENDERER_SETUP: Lazy> = + Lazy::new(try_init_shared_renderer_setup); + +fn try_init_shared_renderer_setup() -> Option { + // TODO(andreas, emilk/egui#5506): Use centralized wgpu setup logic that… + // * lives mostly in re_renderer and is shared with viewer & renderer examples + // * can be told to prefer software rendering + // * can be told to match a specific device tier + // For the moment we just use wgpu defaults. + + // TODO(#8245): Should we require this to succeed? + + let instance = wgpu::Instance::default(); + let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + force_fallback_adapter: false, + compatible_surface: None, + }))?; + + let device_caps = re_renderer::config::DeviceCaps::from_adapter(&adapter) + .warn_on_err_once("Failed to determine device capabilities")?; + let (device, queue) = + pollster::block_on(adapter.request_device(&device_caps.device_descriptor(), None)) + .warn_on_err_once("Failed to request device.")?; + + Some(SharedWgpuResources { + instance: Arc::new(instance), + adapter: Arc::new(adapter), + device: Arc::new(device), + queue: Arc::new(queue), + }) +} + impl TestContext { + pub fn setup_kittest_for_rendering(&self) -> egui_kittest::HarnessBuilder<()> { + if let Some(new_render_state) = create_egui_renderstate() { + let builder = egui_kittest::Harness::builder().renderer( + // Note that render state clone is mostly cloning of inner `Arc`. + // This does _not_ duplicate re_renderer's context. + egui_kittest::wgpu::WgpuTestRenderer::from_render_state(new_render_state.clone()), + ); + + // Egui kittests insists on having a fresh render state for each test. + self.egui_render_state.lock().replace(new_render_state); + builder + } else { + egui_kittest::Harness::builder() + } + } + /// Timeline the recording config is using by default. pub fn active_timeline(&self) -> re_chunk::Timeline { *self.recording_config.time_ctrl.read().timeline() @@ -108,6 +237,20 @@ impl TestContext { let drag_and_drop_manager = crate::DragAndDropManager::new(ItemCollection::default()); + let context_render_state = self.egui_render_state.lock(); + let mut renderer; + let render_ctx = if let Some(render_state) = context_render_state.as_ref() { + renderer = render_state.renderer.write(); + let render_ctx = renderer + .callback_resources + .get_mut::() + .expect("No re_renderer::RenderContext in egui_render_state"); + render_ctx.begin_frame(); + Some(render_ctx) + } else { + None + }; + let ctx = ViewerContext { app_options: &Default::default(), cache: &Default::default(), @@ -123,13 +266,17 @@ impl TestContext { selection_state: &self.selection_state, blueprint_query: &self.blueprint_query, egui_ctx, - render_ctx: None, + render_ctx: render_ctx.as_deref(), command_sender: &self.command_sender, focused_item: &None, drag_and_drop_manager: &drag_and_drop_manager, }; func(&ctx); + + if let Some(render_ctx) = render_ctx { + render_ctx.before_submit(); + } } /// Run the given function with a [`ViewerContext`] produced by the [`Self`], in the context of diff --git a/crates/viewer/re_viewport_blueprint/Cargo.toml b/crates/viewer/re_viewport_blueprint/Cargo.toml index de84226d4558..8b324a16620f 100644 --- a/crates/viewer/re_viewport_blueprint/Cargo.toml +++ b/crates/viewer/re_viewport_blueprint/Cargo.toml @@ -41,3 +41,6 @@ parking_lot.workspace = true slotmap.workspace = true smallvec.workspace = true thiserror.workspace = true + +[dev-dependencies] +re_viewer_context = { workspace = true, features = ["testing"] } diff --git a/deny.toml b/deny.toml index 91c5b9afdd06..30765b149088 100644 --- a/deny.toml +++ b/deny.toml @@ -45,16 +45,17 @@ deny = [ { name = "openssl" }, # We prefer rustls ] skip = [ - { name = "ahash" }, # Popular crate + fast release schedule = lots of crates still using old versions - { name = "base64" }, # Too popular - { name = "cargo_metadata" }, # Older version used by ply-rs. It's small, and it's build-time only! - { name = "cfg_aliases" }, # Tiny macro-only crate. wgpu/naga is using an old version - { name = "hashbrown" }, # Old version used by polar-rs - { name = "memoffset" }, # Small crate - { name = "prettyplease" }, # Old version being used by prost - { name = "pulldown-cmark" }, # Build-dependency via `ply-rs` (!). TODO(emilk): use a better crate for .ply parsing - { name = "redox_syscall" }, # Plenty of versions in the wild - { name = "pollster" }, # rfd is still on 0.3 + { name = "accesskit_consumer" }, # Duplicate as when used as dev dependency - eframe & egui_kittest are on different versions. + { name = "ahash" }, # Popular crate + fast release schedule = lots of crates still using old versions + { name = "base64" }, # Too popular + { name = "cargo_metadata" }, # Older version used by ply-rs. It's small, and it's build-time only! + { name = "cfg_aliases" }, # Tiny macro-only crate. wgpu/naga is using an old version + { name = "hashbrown" }, # Old version used by polar-rs + { name = "memoffset" }, # Small crate + { name = "pollster" }, # rfd is still on 0.3 + { name = "prettyplease" }, # Old version being used by prost + { name = "pulldown-cmark" }, # Build-dependency via `ply-rs` (!). TODO(emilk): use a better crate for .ply parsing + { name = "redox_syscall" }, # Plenty of versions in the wild ] skip-tree = [ { name = "cargo-run-wasm" }, # Dev-tool