From 68a1061ff617ad45b304eb6c4866a3c1d4923f7f Mon Sep 17 00:00:00 2001 From: Antoine Beyeler <49431240+abey79@users.noreply.github.com> Date: Tue, 28 May 2024 17:59:34 +0200 Subject: [PATCH] `ViewerContext` test harness 0.1 (#6432) ### What - Make the `render_context` field of `ViewerContext` an `Option` to make it easier to crate a `ViewerContext` in a test environment. - Introduce a (very basic) test helper that creates a `ViewerContext` for use in unit test. - Demo unit test that attempts to run `SelectionPanel::show_panel()` Chained to #6431 There are many improvements that could be added: - similar support for `ViewportBlueprint` - make `re_log::warn/err` assert: https://github.com/rerun-io/rerun/issues/6450 - add support for easily populating the stores with data - benchmarking support? - etc. etc. ### Checklist * [x] I have read and agree to [Contributor Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and the [Code of Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md) * [x] I've included a screenshot or gif (if applicable) * [x] I have tested the web demo (if applicable): * Using examples from latest `main` build: [rerun.io/viewer](https://rerun.io/viewer/pr/6432?manifest_url=https://app.rerun.io/version/main/examples_manifest.json) * Using full set of examples from `nightly` build: [rerun.io/viewer](https://rerun.io/viewer/pr/6432?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json) * [x] The PR title and labels are set such as to maximize their usefulness for the next release's CHANGELOG * [x] If applicable, add a new check to the [release checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)! - [PR Build Summary](https://build.rerun.io/pr/6432) - [Recent benchmark results](https://build.rerun.io/graphs/crates.html) - [Wasm size tracking](https://build.rerun.io/graphs/sizes.html) To run all checks from `main`, comment on the PR with `@rerun-bot full-check`. --- crates/re_data_ui/src/image.rs | 14 ++- crates/re_selection_panel/src/lib.rs | 33 +++++ crates/re_space_view_spatial/src/lib.rs | 7 +- crates/re_space_view_spatial/src/ui.rs | 34 ++--- crates/re_space_view_spatial/src/ui_2d.rs | 12 +- crates/re_space_view_spatial/src/ui_3d.rs | 58 +++++---- .../src/visualizers/arrows2d.rs | 6 +- .../src/visualizers/arrows3d.rs | 6 +- .../src/visualizers/assets3d.rs | 19 ++- .../src/visualizers/boxes2d.rs | 6 +- .../src/visualizers/boxes3d.rs | 6 +- .../src/visualizers/cameras.rs | 6 +- .../src/visualizers/images.rs | 26 +++- .../src/visualizers/lines2d.rs | 6 +- .../src/visualizers/lines3d.rs | 6 +- .../src/visualizers/meshes.rs | 19 ++- .../src/visualizers/points2d.rs | 8 +- .../src/visualizers/points3d.rs | 8 +- .../src/visualizers/transform3d_arrows.rs | 6 +- .../src/space_view_class.rs | 14 ++- crates/re_viewer/src/app_state.rs | 8 +- .../src/gpu_bridge/colormap.rs | 14 ++- crates/re_viewer_context/src/lib.rs | 2 + .../re_viewer_context/src/selection_state.rs | 6 +- .../re_viewer_context/src/space_view/mod.rs | 3 + crates/re_viewer_context/src/store_hub.rs | 2 + crates/re_viewer_context/src/test_context.rs | 117 ++++++++++++++++++ .../re_viewer_context/src/viewer_context.rs | 2 +- crates/re_viewport/src/viewport.rs | 46 +++---- 29 files changed, 387 insertions(+), 113 deletions(-) create mode 100644 crates/re_viewer_context/src/test_context.rs diff --git a/crates/re_data_ui/src/image.rs b/crates/re_data_ui/src/image.rs index f5e810ad5391..643f9a3ce33c 100644 --- a/crates/re_data_ui/src/image.rs +++ b/crates/re_data_ui/src/image.rs @@ -122,8 +122,12 @@ pub fn tensor_ui( None }; + let Some(render_ctx) = ctx.render_ctx else { + return; + }; + let texture_result = gpu_bridge::tensor_to_gpu( - ctx.render_ctx, + render_ctx, &debug_name, tensor_data_row_id, tensor, @@ -147,7 +151,7 @@ pub fn tensor_ui( ui.set_min_size(preview_size); show_image_preview( - ctx.render_ctx, + render_ctx, ctx.re_ui, ui, texture.clone(), @@ -158,7 +162,7 @@ pub fn tensor_ui( // Show larger image on hover. let preview_size = Vec2::splat(400.0); show_image_preview( - ctx.render_ctx, + render_ctx, ctx.re_ui, ui, texture.clone(), @@ -216,7 +220,7 @@ pub fn tensor_ui( .min(texture_size(texture)) .min(egui::vec2(150.0, 300.0)); let response = show_image_preview( - ctx.render_ctx, + render_ctx, ctx.re_ui, ui, texture.clone(), @@ -227,7 +231,7 @@ pub fn tensor_ui( if let Some(pointer_pos) = ui.ctx().pointer_latest_pos() { let image_rect = response.rect; show_zoomed_image_region_tooltip( - ctx.render_ctx, + render_ctx, ui, response, tensor_data_row_id, diff --git a/crates/re_selection_panel/src/lib.rs b/crates/re_selection_panel/src/lib.rs index 9b5ff318510a..3375d15ed792 100644 --- a/crates/re_selection_panel/src/lib.rs +++ b/crates/re_selection_panel/src/lib.rs @@ -8,3 +8,36 @@ mod space_view_entity_picker; mod space_view_space_origin_ui; pub use selection_panel::SelectionPanel; + +#[cfg(test)] +mod test { + use super::*; + use re_data_store::LatestAtQuery; + use re_viewer_context::{blueprint_timeline, Item, SpaceViewId}; + use re_viewport_blueprint::ViewportBlueprint; + + /// This test mainly serve to demonstrate that non-trivial UI code can be executed with a "fake" + /// [`ViewerContext`]. + // TODO(#6450): check that no warning/error is logged + #[test] + fn test_selection_panel() { + re_log::setup_logging(); + + let mut test_ctx = re_viewer_context::test_context::TestContext::default(); + test_ctx.edit_selection(|selection_state| { + selection_state.set_selection(Item::SpaceView(SpaceViewId::random())); + }); + + test_ctx.run(|ctx, ui| { + let (sender, _) = std::sync::mpsc::channel(); + let blueprint = ViewportBlueprint::try_from_db( + ctx.store_context.blueprint, + &LatestAtQuery::latest(blueprint_timeline()), + sender, + ); + + let mut selection_panel = SelectionPanel::default(); + selection_panel.show_panel(ctx, &blueprint, &mut Default::default(), ui, true); + }); + } +} diff --git a/crates/re_space_view_spatial/src/lib.rs b/crates/re_space_view_spatial/src/lib.rs index 0fdb9c99b163..5d98460a3e43 100644 --- a/crates/re_space_view_spatial/src/lib.rs +++ b/crates/re_space_view_spatial/src/lib.rs @@ -23,6 +23,7 @@ mod ui_2d; mod ui_3d; mod visualizers; +use re_renderer::RenderContext; use re_types::blueprint::components::BackgroundKind; use re_types::components::{Resolution, TensorData}; @@ -78,7 +79,7 @@ fn query_pinhole( } pub(crate) fn configure_background( - ctx: &re_viewer_context::ViewerContext<'_>, + render_ctx: &RenderContext, kind: BackgroundKind, color: re_types::components::Color, ) -> (Option, re_renderer::Rgba) { @@ -88,7 +89,7 @@ pub(crate) fn configure_background( BackgroundKind::GradientDark => ( Some( renderer::GenericSkyboxDrawData::new( - ctx.render_ctx, + render_ctx, renderer::GenericSkyboxType::GradientDark, ) .into(), @@ -99,7 +100,7 @@ pub(crate) fn configure_background( BackgroundKind::GradientBright => ( Some( renderer::GenericSkyboxDrawData::new( - ctx.render_ctx, + render_ctx, renderer::GenericSkyboxType::GradientBright, ) .into(), diff --git a/crates/re_space_view_spatial/src/ui.rs b/crates/re_space_view_spatial/src/ui.rs index fb51cbeb89d4..b3240fd51eba 100644 --- a/crates/re_space_view_spatial/src/ui.rs +++ b/crates/re_space_view_spatial/src/ui.rs @@ -435,6 +435,10 @@ pub fn picking( return Ok(response); }; + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + let picking_context = PickingContext::new( pointer_pos_ui, space_from_ui, @@ -453,7 +457,7 @@ pub fn picking( .at_most(128.0) as u32; let _ = view_builder.schedule_picking_rect( - ctx.render_ctx, + render_ctx, re_renderer::RectInt::from_middle_and_extent( picking_context.pointer_in_pixel.as_ivec2(), glam::uvec2(picking_rect_size, picking_rect_size), @@ -467,7 +471,7 @@ pub fn picking( let images = visualizers.get::()?; let picking_result = picking_context.pick( - ctx.render_ctx, + render_ctx, query.space_view_id.gpu_readback_id(), &state.previous_picking_result, &images.images, @@ -725,18 +729,20 @@ fn image_hover_ui( let tensor_stats = ctx.cache.entry(|c: &mut TensorStatsCache| { c.entry(tensor_data_row_id, &decoded_tensor) }); - show_zoomed_image_region( - ctx.render_ctx, - ui, - tensor_data_row_id, - &decoded_tensor, - &tensor_stats, - &annotations, - meaning, - meter, - &tensor_name, - [coords[0] as _, coords[1] as _], - ); + if let Some(render_ctx) = ctx.render_ctx { + show_zoomed_image_region( + render_ctx, + ui, + tensor_data_row_id, + &decoded_tensor, + &tensor_stats, + &annotations, + meaning, + meter, + &tensor_name, + [coords[0] as _, coords[1] as _], + ); + } } Err(err) => re_log::warn_once!( "Encountered problem decoding tensor at path {tensor_name}: {err}" diff --git a/crates/re_space_view_spatial/src/ui_2d.rs b/crates/re_space_view_spatial/src/ui_2d.rs index 3a24f7444bf8..8549d5471cf5 100644 --- a/crates/re_space_view_spatial/src/ui_2d.rs +++ b/crates/re_space_view_spatial/src/ui_2d.rs @@ -246,8 +246,6 @@ pub fn view_2d( return Ok(()); }; - let mut view_builder = ViewBuilder::new(ctx.render_ctx, target_config); - // Create labels now since their shapes participate are added to scene.ui for picking. let (label_shapes, ui_rects) = create_labels( collect_ui_labels(&parts), @@ -258,6 +256,12 @@ pub fn view_2d( SpatialSpaceViewKind::TwoD, ); + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + + let mut view_builder = ViewBuilder::new(render_ctx, target_config); + if ui.ctx().dragged_id().is_none() { response = picking( ctx, @@ -283,7 +287,7 @@ pub fn view_2d( let background = re_viewport_blueprint::view_property::(ctx, query.space_view_id) .unwrap_or(Background::DEFAULT_2D); let (background_drawable, clear_color) = crate::configure_background( - ctx, + render_ctx, background.kind, background.color.unwrap_or(Background::DEFAULT_COLOR_2D), ); @@ -295,7 +299,7 @@ pub fn view_2d( if let Some(mode) = screenshot_context_menu(ctx, &response) { view_builder - .schedule_screenshot(ctx.render_ctx, query.space_view_id.gpu_readback_id(), mode) + .schedule_screenshot(render_ctx, query.space_view_id.gpu_readback_id(), mode) .ok(); } diff --git a/crates/re_space_view_spatial/src/ui_3d.rs b/crates/re_space_view_spatial/src/ui_3d.rs index 80d8ec77cafc..093c183b59f1 100644 --- a/crates/re_space_view_spatial/src/ui_3d.rs +++ b/crates/re_space_view_spatial/src/ui_3d.rs @@ -466,30 +466,6 @@ pub fn view_3d( ); let eye = view_eye.to_eye(); - // Various ui interactions draw additional lines. - let mut line_builder = LineDrawableBuilder::new(ctx.render_ctx); - line_builder.radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES); - // We don't know ahead of time how many lines we need, but it's not gonna be a huge amount! - line_builder.reserve_strips(32)?; - line_builder.reserve_vertices(64)?; - - // Origin gizmo if requested. - // TODO(andreas): Move this to the transform3d_arrow scene part. - // As of #2522 state is now longer accessible there, move the property to a context? - if state.state_3d.show_axes { - let axis_length = 1.0; // The axes are also a measuring stick - crate::visualizers::add_axis_arrows( - &mut line_builder, - macaw::Affine3A::IDENTITY, - None, - axis_length, - re_renderer::OutlineMaskPreference::NONE, - ); - - // If we are showing the axes for the space, then add the space origin to the bounding box. - state.bounding_boxes.current.extend(glam::Vec3::ZERO); - } - // Determine view port resolution and position. let resolution_in_pixel = gpu_bridge::viewport_resolution_in_pixels(rect, ui.ctx().pixels_per_point()); @@ -519,7 +495,35 @@ pub fn view_3d( .then(|| outline_config(ui.ctx())), }; - let mut view_builder = ViewBuilder::new(ctx.render_ctx, target_config); + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + + // Various ui interactions draw additional lines. + let mut line_builder = LineDrawableBuilder::new(render_ctx); + line_builder.radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES); + // We don't know ahead of time how many lines we need, but it's not gonna be a huge amount! + line_builder.reserve_strips(32)?; + line_builder.reserve_vertices(64)?; + + // Origin gizmo if requested. + // TODO(andreas): Move this to the transform3d_arrow scene part. + // As of #2522 state is now longer accessible there, move the property to a context? + if state.state_3d.show_axes { + let axis_length = 1.0; // The axes are also a measuring stick + crate::visualizers::add_axis_arrows( + &mut line_builder, + macaw::Affine3A::IDENTITY, + None, + axis_length, + re_renderer::OutlineMaskPreference::NONE, + ); + + // If we are showing the axes for the space, then add the space origin to the bounding box. + state.bounding_boxes.current.extend(glam::Vec3::ZERO); + } + + let mut view_builder = ViewBuilder::new(render_ctx, target_config); // Create labels now since their shapes participate are added to scene.ui for picking. let (label_shapes, ui_rects) = create_labels( @@ -610,7 +614,7 @@ pub fn view_3d( // Screenshot context menu. if let Some(mode) = screenshot_context_menu(ctx, &response) { view_builder - .schedule_screenshot(ctx.render_ctx, query.space_view_id.gpu_readback_id(), mode) + .schedule_screenshot(render_ctx, query.space_view_id.gpu_readback_id(), mode) .ok(); } @@ -668,7 +672,7 @@ pub fn view_3d( let background = re_viewport_blueprint::view_property::(ctx, query.space_view_id) .unwrap_or(Background::DEFAULT_3D); let (background_drawable, clear_color) = crate::configure_background( - ctx, + render_ctx, background.kind, background.color.unwrap_or(Background::DEFAULT_COLOR_3D), ); diff --git a/crates/re_space_view_spatial/src/visualizers/arrows2d.rs b/crates/re_space_view_spatial/src/visualizers/arrows2d.rs index 4949d1d81f98..e614dafb02b6 100644 --- a/crates/re_space_view_spatial/src/visualizers/arrows2d.rs +++ b/crates/re_space_view_spatial/src/visualizers/arrows2d.rs @@ -212,7 +212,11 @@ impl VisualizerSystem for Arrows2DVisualizer { view_query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { - let mut line_builder = LineDrawableBuilder::new(ctx.render_ctx); + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + + let mut line_builder = LineDrawableBuilder::new(render_ctx); line_builder.radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES); super::entity_iterator::process_archetype::( diff --git a/crates/re_space_view_spatial/src/visualizers/arrows3d.rs b/crates/re_space_view_spatial/src/visualizers/arrows3d.rs index 0ece72c01637..92e24b29859f 100644 --- a/crates/re_space_view_spatial/src/visualizers/arrows3d.rs +++ b/crates/re_space_view_spatial/src/visualizers/arrows3d.rs @@ -215,7 +215,11 @@ impl VisualizerSystem for Arrows3DVisualizer { view_query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { - let mut line_builder = LineDrawableBuilder::new(ctx.render_ctx); + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + + let mut line_builder = LineDrawableBuilder::new(render_ctx); line_builder.radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES); super::entity_iterator::process_archetype::( diff --git a/crates/re_space_view_spatial/src/visualizers/assets3d.rs b/crates/re_space_view_spatial/src/visualizers/assets3d.rs index 9ffbfd2db991..2024ff475f03 100644 --- a/crates/re_space_view_spatial/src/visualizers/assets3d.rs +++ b/crates/re_space_view_spatial/src/visualizers/assets3d.rs @@ -2,6 +2,7 @@ use re_entity_db::EntityPath; use re_log_types::{Instance, RowId, TimeInt}; use re_query::range_zip_1x2; use re_renderer::renderer::MeshInstance; +use re_renderer::RenderContext; use re_types::{ archetypes::Asset3D, components::{Blob, MediaType, OutOfTreeTransform3D}, @@ -44,6 +45,7 @@ impl Asset3DVisualizer { fn process_data<'a>( &mut self, ctx: &ViewerContext<'_>, + render_ctx: &RenderContext, instances: &mut Vec, entity_path: &EntityPath, ent_context: &SpatialSceneEntityContext<'_>, @@ -73,7 +75,7 @@ impl Asset3DVisualizer { media_type: data.media_type.cloned(), }, AnyMesh::Asset(&mesh), - ctx.render_ctx, + render_ctx, ) }); @@ -133,6 +135,10 @@ impl VisualizerSystem for Asset3DVisualizer { view_query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + let mut instances = Vec::new(); super::entity_iterator::process_archetype::( @@ -169,12 +175,19 @@ impl VisualizerSystem for Asset3DVisualizer { }) }); - self.process_data(ctx, &mut instances, entity_path, spatial_ctx, data); + self.process_data( + ctx, + render_ctx, + &mut instances, + entity_path, + spatial_ctx, + data, + ); Ok(()) }, )?; - match re_renderer::renderer::MeshDrawData::new(ctx.render_ctx, &instances) { + match re_renderer::renderer::MeshDrawData::new(render_ctx, &instances) { Ok(draw_data) => Ok(vec![draw_data.into()]), Err(err) => { re_log::error_once!("Failed to create mesh draw data from mesh instances: {err}"); diff --git a/crates/re_space_view_spatial/src/visualizers/boxes2d.rs b/crates/re_space_view_spatial/src/visualizers/boxes2d.rs index 964431730861..c4f75a77999b 100644 --- a/crates/re_space_view_spatial/src/visualizers/boxes2d.rs +++ b/crates/re_space_view_spatial/src/visualizers/boxes2d.rs @@ -208,7 +208,11 @@ impl VisualizerSystem for Boxes2DVisualizer { view_query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { - let mut line_builder = LineDrawableBuilder::new(ctx.render_ctx); + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + + let mut line_builder = LineDrawableBuilder::new(render_ctx); line_builder.radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES); super::entity_iterator::process_archetype::( diff --git a/crates/re_space_view_spatial/src/visualizers/boxes3d.rs b/crates/re_space_view_spatial/src/visualizers/boxes3d.rs index 27a94e572d89..d09fb6d41ee1 100644 --- a/crates/re_space_view_spatial/src/visualizers/boxes3d.rs +++ b/crates/re_space_view_spatial/src/visualizers/boxes3d.rs @@ -199,7 +199,11 @@ impl VisualizerSystem for Boxes3DVisualizer { view_query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { - let mut line_builder = LineDrawableBuilder::new(ctx.render_ctx); + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + + let mut line_builder = LineDrawableBuilder::new(render_ctx); line_builder.radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES); super::entity_iterator::process_archetype::( diff --git a/crates/re_space_view_spatial/src/visualizers/cameras.rs b/crates/re_space_view_spatial/src/visualizers/cameras.rs index 054993d8c5c1..ea6e24d74312 100644 --- a/crates/re_space_view_spatial/src/visualizers/cameras.rs +++ b/crates/re_space_view_spatial/src/visualizers/cameras.rs @@ -206,11 +206,15 @@ impl VisualizerSystem for CamerasVisualizer { query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + let transforms = view_ctx.get::()?; // Counting all cameras ahead of time is a bit wasteful, but we also don't expect a huge amount, // so let re_renderer's allocator internally decide what buffer sizes to pick & grow them as we go. - let mut line_builder = re_renderer::LineDrawableBuilder::new(ctx.render_ctx); + let mut line_builder = re_renderer::LineDrawableBuilder::new(render_ctx); line_builder.radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES); for data_result in query.iter_visible_data_results(ctx, Self::identifier()) { diff --git a/crates/re_space_view_spatial/src/visualizers/images.rs b/crates/re_space_view_spatial/src/visualizers/images.rs index 41f0f9c2c658..877f04ca1372 100644 --- a/crates/re_space_view_spatial/src/visualizers/images.rs +++ b/crates/re_space_view_spatial/src/visualizers/images.rs @@ -9,7 +9,7 @@ use re_log_types::{EntityPathHash, RowId, TimeInt}; use re_query::range_zip_1x2; use re_renderer::{ renderer::{DepthCloud, DepthClouds, RectangleOptions, TexturedRect}, - Colormap, + Colormap, RenderContext, }; use re_space_view::diff_component_filter; use re_types::{ @@ -57,6 +57,7 @@ pub struct ViewerImage { #[allow(clippy::too_many_arguments)] fn to_textured_rect( ctx: &ViewerContext<'_>, + render_ctx: &RenderContext, ent_path: &EntityPath, ent_context: &SpatialSceneEntityContext<'_>, tensor_data_row_id: RowId, @@ -72,7 +73,7 @@ fn to_textured_rect( .entry(|c: &mut TensorStatsCache| c.entry(tensor_data_row_id, tensor)); match gpu_bridge::tensor_to_gpu( - ctx.render_ctx, + render_ctx, &debug_name, tensor_data_row_id, tensor, @@ -210,6 +211,7 @@ impl ImageVisualizer { fn process_image_data<'a>( &mut self, ctx: &ViewerContext<'_>, + render_ctx: &RenderContext, transforms: &TransformContext, ent_props: &EntityProperties, entity_path: &EntityPath, @@ -264,6 +266,7 @@ impl ImageVisualizer { if let Some(textured_rect) = to_textured_rect( ctx, + render_ctx, entity_path, ent_context, tensor_data_row_id, @@ -302,6 +305,7 @@ impl ImageVisualizer { fn process_segmentation_image_data<'a>( &mut self, ctx: &ViewerContext<'_>, + render_ctx: &RenderContext, transforms: &TransformContext, ent_props: &EntityProperties, entity_path: &EntityPath, @@ -354,6 +358,7 @@ impl ImageVisualizer { if let Some(textured_rect) = to_textured_rect( ctx, + render_ctx, entity_path, ent_context, tensor_data_row_id, @@ -392,6 +397,7 @@ impl ImageVisualizer { fn process_depth_image_data<'a>( &mut self, ctx: &ViewerContext<'_>, + render_ctx: &RenderContext, depth_clouds: &mut Vec, transforms: &TransformContext, ent_props: &EntityProperties, @@ -448,6 +454,7 @@ impl ImageVisualizer { // What we want are the extrinsics of the depth camera! match Self::process_entity_view_as_depth_cloud( ctx, + render_ctx, transforms, ent_context, ent_props, @@ -481,6 +488,7 @@ impl ImageVisualizer { if let Some(textured_rect) = to_textured_rect( ctx, + render_ctx, entity_path, ent_context, tensor_data_row_id, @@ -518,6 +526,7 @@ impl ImageVisualizer { #[allow(clippy::too_many_arguments)] fn process_entity_view_as_depth_cloud( ctx: &ViewerContext<'_>, + render_ctx: &RenderContext, transforms: &TransformContext, ent_context: &SpatialSceneEntityContext<'_>, properties: &EntityProperties, @@ -561,7 +570,7 @@ impl ImageVisualizer { .cache .entry(|c: &mut TensorStatsCache| c.entry(tensor_data_row_id, tensor)); let depth_texture = re_viewer_context::gpu_bridge::depth_tensor_to_gpu( - ctx.render_ctx, + render_ctx, &debug_name, tensor_data_row_id, tensor, @@ -702,6 +711,10 @@ impl VisualizerSystem for ImageVisualizer { view_query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + let mut depth_clouds = Vec::new(); self.process_image_archetype::( @@ -719,6 +732,7 @@ impl VisualizerSystem for ImageVisualizer { data| { visualizer.process_image_data( ctx, + render_ctx, transforms, entity_props, entity_path, @@ -743,6 +757,7 @@ impl VisualizerSystem for ImageVisualizer { data| { visualizer.process_segmentation_image_data( ctx, + render_ctx, transforms, entity_props, entity_path, @@ -767,6 +782,7 @@ impl VisualizerSystem for ImageVisualizer { data| { visualizer.process_depth_image_data( ctx, + render_ctx, depth_clouds, transforms, entity_props, @@ -782,7 +798,7 @@ impl VisualizerSystem for ImageVisualizer { let mut draw_data_list = Vec::new(); match re_renderer::renderer::DepthCloudDrawData::new( - ctx.render_ctx, + render_ctx, &DepthClouds { clouds: depth_clouds, radius_boost_in_ui_points_for_outlines: SIZE_BOOST_IN_POINTS_FOR_POINT_OUTLINES, @@ -803,7 +819,7 @@ impl VisualizerSystem for ImageVisualizer { .iter() .map(|image| image.textured_rect.clone()) .collect_vec(); - match re_renderer::renderer::RectangleDrawData::new(ctx.render_ctx, &rectangles) { + match re_renderer::renderer::RectangleDrawData::new(render_ctx, &rectangles) { Ok(draw_data) => { draw_data_list.push(draw_data.into()); } diff --git a/crates/re_space_view_spatial/src/visualizers/lines2d.rs b/crates/re_space_view_spatial/src/visualizers/lines2d.rs index b64f15a268c5..82be395413c1 100644 --- a/crates/re_space_view_spatial/src/visualizers/lines2d.rs +++ b/crates/re_space_view_spatial/src/visualizers/lines2d.rs @@ -197,7 +197,11 @@ impl VisualizerSystem for Lines2DVisualizer { view_query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { - let mut line_builder = re_renderer::LineDrawableBuilder::new(ctx.render_ctx); + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + + let mut line_builder = re_renderer::LineDrawableBuilder::new(render_ctx); line_builder.radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES); super::entity_iterator::process_archetype::( diff --git a/crates/re_space_view_spatial/src/visualizers/lines3d.rs b/crates/re_space_view_spatial/src/visualizers/lines3d.rs index 165d4d5958c4..e079d707b1cb 100644 --- a/crates/re_space_view_spatial/src/visualizers/lines3d.rs +++ b/crates/re_space_view_spatial/src/visualizers/lines3d.rs @@ -206,7 +206,11 @@ impl VisualizerSystem for Lines3DVisualizer { view_query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { - let mut line_builder = re_renderer::LineDrawableBuilder::new(ctx.render_ctx); + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + + let mut line_builder = re_renderer::LineDrawableBuilder::new(render_ctx); line_builder.radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES); super::entity_iterator::process_archetype::( diff --git a/crates/re_space_view_spatial/src/visualizers/meshes.rs b/crates/re_space_view_spatial/src/visualizers/meshes.rs index 5c420eaac504..0f9db1ce0189 100644 --- a/crates/re_space_view_spatial/src/visualizers/meshes.rs +++ b/crates/re_space_view_spatial/src/visualizers/meshes.rs @@ -3,6 +3,7 @@ use re_entity_db::EntityPath; use re_log_types::{Instance, RowId, TimeInt}; use re_query::range_zip_1x7; use re_renderer::renderer::MeshInstance; +use re_renderer::RenderContext; use re_types::{ archetypes::Mesh3D, components::{ @@ -57,6 +58,7 @@ impl Mesh3DVisualizer { fn process_data<'a>( &mut self, ctx: &ViewerContext<'_>, + render_ctx: &RenderContext, instances: &mut Vec, entity_path: &EntityPath, ent_context: &SpatialSceneEntityContext<'_>, @@ -108,7 +110,7 @@ impl Mesh3DVisualizer { }, texture_key: re_log_types::hash::Hash64::hash(&key).hash64(), }, - ctx.render_ctx, + render_ctx, ) }); @@ -164,6 +166,10 @@ impl VisualizerSystem for Mesh3DVisualizer { view_query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + let mut instances = Vec::new(); super::entity_iterator::process_archetype::( @@ -227,12 +233,19 @@ impl VisualizerSystem for Mesh3DVisualizer { }, ); - self.process_data(ctx, &mut instances, entity_path, spatial_ctx, data); + self.process_data( + ctx, + render_ctx, + &mut instances, + entity_path, + spatial_ctx, + data, + ); Ok(()) }, )?; - match re_renderer::renderer::MeshDrawData::new(ctx.render_ctx, &instances) { + match re_renderer::renderer::MeshDrawData::new(render_ctx, &instances) { Ok(draw_data) => Ok(vec![draw_data.into()]), Err(err) => { re_log::error_once!("Failed to create mesh draw data from mesh instances: {err}"); diff --git a/crates/re_space_view_spatial/src/visualizers/points2d.rs b/crates/re_space_view_spatial/src/visualizers/points2d.rs index d38bebeabb60..b2338f542931 100644 --- a/crates/re_space_view_spatial/src/visualizers/points2d.rs +++ b/crates/re_space_view_spatial/src/visualizers/points2d.rs @@ -208,13 +208,17 @@ impl VisualizerSystem for Points2DVisualizer { view_query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { - let mut point_builder = PointCloudBuilder::new(ctx.render_ctx); + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + + let mut point_builder = PointCloudBuilder::new(render_ctx); point_builder .radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_POINT_OUTLINES); // We need lines from keypoints. The number of lines we'll have is harder to predict, so we'll // go with the dynamic allocation approach. - let mut line_builder = LineDrawableBuilder::new(ctx.render_ctx); + let mut line_builder = LineDrawableBuilder::new(render_ctx); line_builder .radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_POINT_OUTLINES); diff --git a/crates/re_space_view_spatial/src/visualizers/points3d.rs b/crates/re_space_view_spatial/src/visualizers/points3d.rs index 002269a5c19b..d04d4bda951e 100644 --- a/crates/re_space_view_spatial/src/visualizers/points3d.rs +++ b/crates/re_space_view_spatial/src/visualizers/points3d.rs @@ -199,13 +199,17 @@ impl VisualizerSystem for Points3DVisualizer { view_query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { - let mut point_builder = PointCloudBuilder::new(ctx.render_ctx); + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + + let mut point_builder = PointCloudBuilder::new(render_ctx); point_builder .radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_POINT_OUTLINES); // We need lines from keypoints. The number of lines we'll have is harder to predict, so we'll go // with the dynamic allocation approach. - let mut line_builder = LineDrawableBuilder::new(ctx.render_ctx); + let mut line_builder = LineDrawableBuilder::new(render_ctx); line_builder .radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_POINT_OUTLINES); diff --git a/crates/re_space_view_spatial/src/visualizers/transform3d_arrows.rs b/crates/re_space_view_spatial/src/visualizers/transform3d_arrows.rs index c9efd4216dc5..dad69fe7082a 100644 --- a/crates/re_space_view_spatial/src/visualizers/transform3d_arrows.rs +++ b/crates/re_space_view_spatial/src/visualizers/transform3d_arrows.rs @@ -49,13 +49,17 @@ impl VisualizerSystem for Transform3DArrowsVisualizer { query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, ) -> Result, SpaceViewSystemExecutionError> { + let Some(render_ctx) = ctx.render_ctx else { + return Err(SpaceViewSystemExecutionError::NoRenderContextError); + }; + let transforms = view_ctx.get::()?; let latest_at_query = re_data_store::LatestAtQuery::new(query.timeline, query.latest_at); // Counting all transforms ahead of time is a bit wasteful, but we also don't expect a huge amount, // so let re_renderer's allocator internally decide what buffer sizes to pick & grow them as we go. - let mut line_builder = re_renderer::LineDrawableBuilder::new(ctx.render_ctx); + let mut line_builder = re_renderer::LineDrawableBuilder::new(render_ctx); line_builder.radius_boost_in_ui_points_for_outlines(SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES); for data_result in query.iter_visible_data_results(ctx, Self::identifier()) { diff --git a/crates/re_space_view_tensor/src/space_view_class.rs b/crates/re_space_view_tensor/src/space_view_class.rs index 50af4a4557b5..b13fff2baba0 100644 --- a/crates/re_space_view_tensor/src/space_view_class.rs +++ b/crates/re_space_view_tensor/src/space_view_class.rs @@ -159,7 +159,9 @@ impl SpaceViewClass for TensorSpaceView { } state.texture_settings.ui(ctx.re_ui, ui); - state.color_mapping.ui(ctx.render_ctx, ctx.re_ui, ui); + if let Some(render_ctx) = ctx.render_ctx { + state.color_mapping.ui(render_ctx, ctx.re_ui, ui); + } }); if let Some((_, tensor)) = &state.tensor { @@ -316,11 +318,15 @@ fn paint_tensor_slice( ) -> anyhow::Result<(egui::Response, egui::Painter, egui::Rect)> { re_tracing::profile_function!(); + let Some(render_ctx) = ctx.render_ctx else { + return Err(anyhow::Error::msg("No render context available.")); + }; + let tensor_stats = ctx .cache .entry(|c: &mut TensorStatsCache| c.entry(tensor_data_row_id, tensor)); let colormapped_texture = super::tensor_slice_to_gpu::colormapped_texture( - ctx.render_ctx, + render_ctx, tensor_data_row_id, tensor, &tensor_stats, @@ -349,7 +355,7 @@ fn paint_tensor_slice( let debug_name = "tensor_slice"; gpu_bridge::render_image( - ctx.render_ctx, + render_ctx, &painter, image_rect, colormapped_texture, @@ -388,7 +394,7 @@ impl ColorMapping { let Self { map, gamma } = self; re_ui.grid_left_hand_label(ui, "Color map"); - colormap_dropdown_button_ui(render_ctx, re_ui, ui, map); + colormap_dropdown_button_ui(Some(render_ctx), re_ui, ui, map); ui.end_row(); re_ui.grid_left_hand_label(ui, "Brightness"); diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index 94ad9fd3c313..623aca0bc9af 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -191,7 +191,9 @@ impl AppState { viewport.is_item_valid(store_context, item) }, - re_viewer_context::Item::StoreId(store_context.recording.store_id().clone()), + Some(re_viewer_context::Item::StoreId( + store_context.recording.store_id().clone(), + )), ); if ui.input(|i| i.key_pressed(egui::Key::Escape)) { @@ -251,7 +253,7 @@ impl AppState { selection_state, blueprint_query: &blueprint_query, re_ui, - render_ctx, + render_ctx: Some(render_ctx), command_sender, focused_item, }; @@ -314,7 +316,7 @@ impl AppState { selection_state, blueprint_query: &blueprint_query, re_ui, - render_ctx, + render_ctx: Some(render_ctx), command_sender, focused_item, }; diff --git a/crates/re_viewer_context/src/gpu_bridge/colormap.rs b/crates/re_viewer_context/src/gpu_bridge/colormap.rs index 9e70a4208bb4..dc75de5ceae5 100644 --- a/crates/re_viewer_context/src/gpu_bridge/colormap.rs +++ b/crates/re_viewer_context/src/gpu_bridge/colormap.rs @@ -61,7 +61,7 @@ fn colormap_preview_ui( } pub fn colormap_dropdown_button_ui( - render_ctx: &re_renderer::RenderContext, + render_ctx: Option<&re_renderer::RenderContext>, re_ui: &re_ui::ReUi, ui: &mut egui::Ui, map: &mut re_renderer::Colormap, @@ -69,16 +69,20 @@ pub fn colormap_dropdown_button_ui( let selected_text = map.to_string(); let content_ui = |ui: &mut egui::Ui| { for option in re_renderer::Colormap::ALL { - let response = list_item::ListItem::new(re_ui) - .selected(&option == map) - .show_flat( + let list_item = list_item::ListItem::new(re_ui).selected(&option == map); + + let response = if let Some(render_ctx) = render_ctx { + list_item.show_flat( ui, list_item::PropertyContent::new(option.to_string()).value_fn(|_, ui, _| { if let Err(err) = colormap_preview_ui(render_ctx, ui, option) { re_log::error_once!("Failed to paint colormap preview: {err}"); } }), - ); + ) + } else { + list_item.show_flat(ui, list_item::LabelContent::new(option.to_string())) + }; if response.clicked() { *map = option; diff --git a/crates/re_viewer_context/src/lib.rs b/crates/re_viewer_context/src/lib.rs index 4dad50df7018..04edd44405db 100644 --- a/crates/re_viewer_context/src/lib.rs +++ b/crates/re_viewer_context/src/lib.rs @@ -20,6 +20,8 @@ mod space_view; mod store_context; pub mod store_hub; mod tensor; +//TODO(ab): this should be behind #[cfg(test)], but then ` cargo clippy --all-targets` fails +pub mod test_context; mod time_control; mod typed_entity_collections; mod utils; diff --git a/crates/re_viewer_context/src/selection_state.rs b/crates/re_viewer_context/src/selection_state.rs index 9c289345fabd..902d8a189d2c 100644 --- a/crates/re_viewer_context/src/selection_state.rs +++ b/crates/re_viewer_context/src/selection_state.rs @@ -230,7 +230,7 @@ impl ApplicationSelectionState { pub fn on_frame_start( &mut self, item_retain_condition: impl Fn(&Item) -> bool, - fallback_selection: Item, + fallback_selection: Option, ) { // Use a different name so we don't get a collision in puffin. re_tracing::profile_scope!("SelectionState::on_frame_start"); @@ -243,7 +243,9 @@ impl ApplicationSelectionState { let selection_this_frame = self.selection_this_frame.get_mut(); selection_this_frame.retain(|item, _| item_retain_condition(item)); if selection_this_frame.is_empty() { - *selection_this_frame = ItemCollection::from(fallback_selection); + if let Some(fallback_selection) = fallback_selection { + *selection_this_frame = ItemCollection::from(fallback_selection); + } } // Hovering needs to be refreshed every frame: If it wasn't hovered last frame, it's no longer hovered! diff --git a/crates/re_viewer_context/src/space_view/mod.rs b/crates/re_viewer_context/src/space_view/mod.rs index 31003ddf662e..64249d3ce3cf 100644 --- a/crates/re_viewer_context/src/space_view/mod.rs +++ b/crates/re_viewer_context/src/space_view/mod.rs @@ -61,6 +61,9 @@ pub enum SpaceViewSystemExecutionError { #[error("Failed to downcast space view's to the {0}.")] StateCastError(&'static str), + + #[error("No render context error.")] + NoRenderContextError, } // Convenience conversions for some re_renderer error types since these are so frequent. diff --git a/crates/re_viewer_context/src/store_hub.rs b/crates/re_viewer_context/src/store_hub.rs index 3227e95e6ff2..d28a2c51c00b 100644 --- a/crates/re_viewer_context/src/store_hub.rs +++ b/crates/re_viewer_context/src/store_hub.rs @@ -32,6 +32,7 @@ use crate::StoreContext; /// /// The default blueprint is usually the blueprint set by the SDK. /// This lets users reset the active blueprint to the one sent by the SDK. +#[derive(Default)] pub struct StoreHub { /// How we load and save blueprints. persistence: BlueprintPersistence, @@ -61,6 +62,7 @@ pub type BlueprintSaver = dyn Fn(&ApplicationId, &EntityDb) -> anyhow::Result<() pub type BlueprintValidator = dyn Fn(&EntityDb) -> bool + Send + Sync; /// How to save and load blueprints +#[derive(Default)] pub struct BlueprintPersistence { pub loader: Option>, pub saver: Option>, diff --git a/crates/re_viewer_context/src/test_context.rs b/crates/re_viewer_context/src/test_context.rs new file mode 100644 index 000000000000..25b1a487fa5c --- /dev/null +++ b/crates/re_viewer_context/src/test_context.rs @@ -0,0 +1,117 @@ +use crate::{ + command_channel, ApplicationSelectionState, ComponentUiRegistry, StoreContext, ViewerContext, +}; + +use re_data_store::LatestAtQuery; +use re_entity_db::EntityDb; +use re_log_types::{StoreId, StoreKind}; + +/// Harness to execute code that rely on [`crate::ViewerContext`]. +/// +/// Example: +/// ```rust +/// use re_viewer_context::test_context::TestContext; +/// use re_viewer_context::ViewerContext; +/// +/// let mut test_context = TestContext::default(); +/// test_context.run(|ctx: &ViewerContext, _| { +/// /* do something with ctx */ +/// }); +/// ``` +pub struct TestContext { + recording_store: EntityDb, + blueprint_store: EntityDb, + selection_state: ApplicationSelectionState, +} + +impl Default for TestContext { + fn default() -> Self { + let recording_store = EntityDb::new(StoreId::random(StoreKind::Recording)); + let blueprint_store = EntityDb::new(StoreId::random(StoreKind::Blueprint)); + Self { + recording_store, + blueprint_store, + selection_state: Default::default(), + } + } +} + +impl TestContext { + pub fn edit_selection(&mut self, edit_fn: impl FnOnce(&mut ApplicationSelectionState)) { + edit_fn(&mut self.selection_state); + + // the selection state is double-buffered, so let's ensure it's updated + self.selection_state.on_frame_start(|_| true, None); + } + + pub fn run(&self, mut func: impl FnMut(&ViewerContext<'_>, &mut egui::Ui)) { + egui::__run_test_ui(|ui| { + let re_ui = re_ui::ReUi::load_and_apply(ui.ctx()); + let blueprint_query = LatestAtQuery::latest(re_log_types::Timeline::new( + "timeline", + re_log_types::TimeType::Time, + )); + let (command_sender, _) = command_channel(); + let component_ui_registry = ComponentUiRegistry::new(Box::new( + |_ctx, _ui, _ui_layout, _query, _db, _entity_path, _component, _instance| {}, + )); + + let store_context = StoreContext { + app_id: "rerun_test".into(), + blueprint: &self.blueprint_store, + default_blueprint: None, + recording: &self.recording_store, + bundle: &Default::default(), + hub: &Default::default(), + }; + + let ctx = ViewerContext { + app_options: &Default::default(), + cache: &Default::default(), + component_ui_registry: &component_ui_registry, + space_view_class_registry: &Default::default(), + store_context: &store_context, + applicable_entities_per_visualizer: &Default::default(), + indicated_entities_per_visualizer: &Default::default(), + query_results: &Default::default(), + rec_cfg: &Default::default(), + blueprint_cfg: &Default::default(), + selection_state: &self.selection_state, + blueprint_query: &blueprint_query, + re_ui: &re_ui, + render_ctx: None, + command_sender: &command_sender, + focused_item: &None, + }; + + func(&ctx, ui); + }); + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Item; + use re_entity_db::InstancePath; + + /// Test that `TestContext:edit_selection` works as expected, aka. its side effects are visible + /// from `TestContext::run`. + #[test] + fn test_edit_selection() { + let mut test_context = TestContext::default(); + + let item = Item::InstancePath(InstancePath::entity_all("/entity/path".into())); + + test_context.edit_selection(|selection_state| { + selection_state.set_selection(item.clone()); + }); + + test_context.run(|ctx, _| { + assert_eq!( + ctx.selection_state.selected_items().single_item(), + Some(&item) + ); + }); + } +} diff --git a/crates/re_viewer_context/src/viewer_context.rs b/crates/re_viewer_context/src/viewer_context.rs index d2bf51790df7..2a7bcc595ffb 100644 --- a/crates/re_viewer_context/src/viewer_context.rs +++ b/crates/re_viewer_context/src/viewer_context.rs @@ -59,7 +59,7 @@ pub struct ViewerContext<'a> { pub re_ui: &'a re_ui::ReUi, /// The global `re_renderer` context, holds on to all GPU resources. - pub render_ctx: &'a re_renderer::RenderContext, + pub render_ctx: Option<&'a re_renderer::RenderContext>, /// Interface for sending commands back to the app pub command_sender: &'a CommandSender, diff --git a/crates/re_viewport/src/viewport.rs b/crates/re_viewport/src/viewport.rs index ae760e465caa..d47f6ee189f6 100644 --- a/crates/re_viewport/src/viewport.rs +++ b/crates/re_viewport/src/viewport.rs @@ -177,28 +177,30 @@ impl<'a> Viewport<'a> { pub fn on_frame_start(&mut self, ctx: &ViewerContext<'_>, view_states: &mut ViewStates) { re_tracing::profile_function!(); - for space_view in self.blueprint.space_views.values() { - let PerViewState { - auto_properties, - view_state: space_view_state, - } = view_states.view_state_mut( - ctx.space_view_class_registry, - space_view.id, - space_view.class_identifier(), - ); - - #[allow(clippy::blocks_in_conditions)] - while ScreenshotProcessor::next_readback_result( - ctx.render_ctx, - space_view.id.gpu_readback_id(), - |data, extent, mode| { - handle_pending_space_view_screenshots(space_view, data, extent, mode); - }, - ) - .is_some() - {} - - space_view.on_frame_start(ctx, space_view_state.as_mut(), auto_properties); + if let Some(render_ctx) = ctx.render_ctx { + for space_view in self.blueprint.space_views.values() { + let PerViewState { + auto_properties, + view_state: space_view_state, + } = view_states.view_state_mut( + ctx.space_view_class_registry, + space_view.id, + space_view.class_identifier(), + ); + + #[allow(clippy::blocks_in_conditions)] + while ScreenshotProcessor::next_readback_result( + render_ctx, + space_view.id.gpu_readback_id(), + |data, extent, mode| { + handle_pending_space_view_screenshots(space_view, data, extent, mode); + }, + ) + .is_some() + {} + + space_view.on_frame_start(ctx, space_view_state.as_mut(), auto_properties); + } } self.blueprint.on_frame_start(ctx);