diff --git a/crates/re_data_store/src/entity_properties.rs b/crates/re_data_store/src/entity_properties.rs index ca7915c9bd70..0c95b3b593ca 100644 --- a/crates/re_data_store/src/entity_properties.rs +++ b/crates/re_data_store/src/entity_properties.rs @@ -143,23 +143,24 @@ impl ExtraQueryHistory { pub enum Colormap { /// Perceptually even Grayscale, + + Inferno, + Magma, + Plasma, #[default] Turbo, Viridis, - Plasma, - Magma, - Inferno, } impl std::fmt::Display for Colormap { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Colormap::Grayscale => "Grayscale", + Colormap::Inferno => "Inferno", + Colormap::Magma => "Magma", + Colormap::Plasma => "Plasma", Colormap::Turbo => "Turbo", Colormap::Viridis => "Viridis", - Colormap::Plasma => "Plasma", - Colormap::Magma => "Magma", - Colormap::Inferno => "Inferno", }) } } diff --git a/crates/re_log_types/src/data.rs b/crates/re_log_types/src/data.rs index 248c67f3acb7..7317be465c3e 100644 --- a/crates/re_log_types/src/data.rs +++ b/crates/re_log_types/src/data.rs @@ -114,6 +114,25 @@ impl TensorDataType { } } + #[inline] + pub fn min_value(&self) -> f64 { + match self { + Self::U8 => u8::MIN as _, + Self::U16 => u16::MIN as _, + Self::U32 => u32::MIN as _, + Self::U64 => u64::MIN as _, + + Self::I8 => i8::MIN as _, + Self::I16 => i16::MIN as _, + Self::I32 => i32::MIN as _, + Self::I64 => i64::MIN as _, + + Self::F16 => f16::MIN.into(), + Self::F32 => f32::MIN as _, + Self::F64 => f64::MIN, + } + } + #[inline] pub fn max_value(&self) -> f64 { match self { diff --git a/crates/re_renderer/examples/2d.rs b/crates/re_renderer/examples/2d.rs index 4cebee363cc4..34f7250b1b55 100644 --- a/crates/re_renderer/examples/2d.rs +++ b/crates/re_renderer/examples/2d.rs @@ -225,25 +225,22 @@ impl framework::Example for Render2D { vec![ // 2d view to the left { - let mut view_builder = ViewBuilder::default(); - view_builder - .setup_view( - re_ctx, - TargetConfiguration { - name: "2D".into(), - resolution_in_pixel: splits[0].resolution_in_pixel, - view_from_world: macaw::IsoTransform::IDENTITY, - projection_from_view: Projection::Orthographic { - camera_mode: - view_builder::OrthographicCameraMode::TopLeftCornerAndExtendZ, - vertical_world_size: splits[0].resolution_in_pixel[1] as f32, - far_plane_distance: 1000.0, - }, - pixels_from_point, - ..Default::default() + let mut view_builder = ViewBuilder::new( + re_ctx, + TargetConfiguration { + name: "2D".into(), + resolution_in_pixel: splits[0].resolution_in_pixel, + view_from_world: macaw::IsoTransform::IDENTITY, + projection_from_view: Projection::Orthographic { + camera_mode: + view_builder::OrthographicCameraMode::TopLeftCornerAndExtendZ, + vertical_world_size: splits[0].resolution_in_pixel[1] as f32, + far_plane_distance: 1000.0, }, - ) - .unwrap(); + pixels_from_point, + ..Default::default() + }, + ); view_builder.queue_draw(&line_strip_draw_data); view_builder.queue_draw(&point_draw_data); view_builder.queue_draw(&rectangle_draw_data); @@ -258,7 +255,6 @@ impl framework::Example for Render2D { }, // and 3d view of the same scene to the right { - let mut view_builder = ViewBuilder::default(); let seconds_since_startup = time.seconds_since_startup(); let camera_rotation_center = screen_size.extend(0.0) * 0.5; let camera_position = glam::vec3( @@ -267,27 +263,25 @@ impl framework::Example for Render2D { seconds_since_startup.cos(), ) * screen_size.x.max(screen_size.y) + camera_rotation_center; - view_builder - .setup_view( - re_ctx, - view_builder::TargetConfiguration { - name: "3D".into(), - resolution_in_pixel: splits[1].resolution_in_pixel, - view_from_world: macaw::IsoTransform::look_at_rh( - camera_position, - camera_rotation_center, - glam::Vec3::Y, - ) - .unwrap(), - projection_from_view: Projection::Perspective { - vertical_fov: 70.0 * std::f32::consts::TAU / 360.0, - near_plane_distance: 0.01, - }, - pixels_from_point, - ..Default::default() + let mut view_builder = ViewBuilder::new( + re_ctx, + view_builder::TargetConfiguration { + name: "3D".into(), + resolution_in_pixel: splits[1].resolution_in_pixel, + view_from_world: macaw::IsoTransform::look_at_rh( + camera_position, + camera_rotation_center, + glam::Vec3::Y, + ) + .unwrap(), + projection_from_view: Projection::Perspective { + vertical_fov: 70.0 * std::f32::consts::TAU / 360.0, + near_plane_distance: 0.01, }, - ) - .unwrap(); + pixels_from_point, + ..Default::default() + }, + ); let command_buffer = view_builder .queue_draw(&line_strip_draw_data) .queue_draw(&point_draw_data) diff --git a/crates/re_renderer/examples/depth_cloud.rs b/crates/re_renderer/examples/depth_cloud.rs index cbd79faa8a25..2003292731e6 100644 --- a/crates/re_renderer/examples/depth_cloud.rs +++ b/crates/re_renderer/examples/depth_cloud.rs @@ -110,28 +110,25 @@ impl RenderDepthClouds { builder.to_draw_data(re_ctx).unwrap() }; - let mut view_builder = ViewBuilder::default(); - view_builder - .setup_view( - re_ctx, - view_builder::TargetConfiguration { - name: "Point Cloud".into(), - resolution_in_pixel, - view_from_world: IsoTransform::look_at_rh( - self.camera_position, - Vec3::ZERO, - Vec3::Y, - ) - .unwrap(), - projection_from_view: Projection::Perspective { - vertical_fov: 70.0 * std::f32::consts::TAU / 360.0, - near_plane_distance: 0.01, - }, - pixels_from_point, - ..Default::default() + let mut view_builder = ViewBuilder::new( + re_ctx, + view_builder::TargetConfiguration { + name: "Point Cloud".into(), + resolution_in_pixel, + view_from_world: IsoTransform::look_at_rh( + self.camera_position, + Vec3::ZERO, + Vec3::Y, + ) + .unwrap(), + projection_from_view: Projection::Perspective { + vertical_fov: 70.0 * std::f32::consts::TAU / 360.0, + near_plane_distance: 0.01, }, - ) - .unwrap(); + pixels_from_point, + ..Default::default() + }, + ); let command_buffer = view_builder .queue_draw(&GenericSkyboxDrawData::new(re_ctx)) @@ -191,28 +188,25 @@ impl RenderDepthClouds { ) .unwrap(); - let mut view_builder = ViewBuilder::default(); - view_builder - .setup_view( - re_ctx, - view_builder::TargetConfiguration { - name: "Depth Cloud".into(), - resolution_in_pixel, - view_from_world: IsoTransform::look_at_rh( - self.camera_position, - Vec3::ZERO, - Vec3::Y, - ) - .unwrap(), - projection_from_view: Projection::Perspective { - vertical_fov: 70.0 * std::f32::consts::TAU / 360.0, - near_plane_distance: 0.01, - }, - pixels_from_point, - ..Default::default() + let mut view_builder = ViewBuilder::new( + re_ctx, + view_builder::TargetConfiguration { + name: "Depth Cloud".into(), + resolution_in_pixel, + view_from_world: IsoTransform::look_at_rh( + self.camera_position, + Vec3::ZERO, + Vec3::Y, + ) + .unwrap(), + projection_from_view: Projection::Perspective { + vertical_fov: 70.0 * std::f32::consts::TAU / 360.0, + near_plane_distance: 0.01, }, - ) - .unwrap(); + pixels_from_point, + ..Default::default() + }, + ); let command_buffer = view_builder .queue_draw(&GenericSkyboxDrawData::new(re_ctx)) diff --git a/crates/re_renderer/examples/framework.rs b/crates/re_renderer/examples/framework.rs index 47e0b42a3212..fef6e7544b5b 100644 --- a/crates/re_renderer/examples/framework.rs +++ b/crates/re_renderer/examples/framework.rs @@ -288,14 +288,11 @@ impl Application { }); for draw_result in &draw_results { - draw_result - .view_builder - .composite( - &self.re_ctx, - &mut composite_pass, - draw_result.target_location, - ) - .expect("Failed to composite view main surface"); + draw_result.view_builder.composite( + &self.re_ctx, + &mut composite_pass, + draw_result.target_location, + ); } }; diff --git a/crates/re_renderer/examples/multiview.rs b/crates/re_renderer/examples/multiview.rs index 35d234152b3b..48b77e24388f 100644 --- a/crates/re_renderer/examples/multiview.rs +++ b/crates/re_renderer/examples/multiview.rs @@ -210,8 +210,7 @@ impl Multiview { draw_data: &D, index: u32, ) -> (ViewBuilder, wgpu::CommandBuffer) { - let mut view_builder = ViewBuilder::default(); - view_builder.setup_view(re_ctx, target_cfg).unwrap(); + let mut view_builder = ViewBuilder::new(re_ctx, target_cfg); if self .take_screenshot_next_frame_for_view diff --git a/crates/re_renderer/examples/outlines.rs b/crates/re_renderer/examples/outlines.rs index b7b106a60426..2cdc2ed6e87c 100644 --- a/crates/re_renderer/examples/outlines.rs +++ b/crates/re_renderer/examples/outlines.rs @@ -40,8 +40,6 @@ impl framework::Example for Outlines { time: &framework::Time, pixels_from_point: f32, ) -> Vec { - let mut view_builder = ViewBuilder::default(); - if !self.is_paused { self.seconds_since_startup += time.last_frame_duration.as_secs_f32(); } @@ -49,35 +47,30 @@ impl framework::Example for Outlines { // TODO(#1426): unify camera logic between examples. let camera_position = glam::vec3(1.0, 3.5, 7.0); - view_builder - .setup_view( - re_ctx, - TargetConfiguration { - name: "OutlinesDemo".into(), - resolution_in_pixel: resolution, - view_from_world: macaw::IsoTransform::look_at_rh( - camera_position, - glam::Vec3::ZERO, - glam::Vec3::Y, - ) - .unwrap(), - projection_from_view: Projection::Perspective { - vertical_fov: 70.0 * std::f32::consts::TAU / 360.0, - near_plane_distance: 0.01, - }, - pixels_from_point, - outline_config: Some(OutlineConfig { - outline_radius_pixel: (seconds_since_startup * 2.0).sin().abs() * 10.0 - + 2.0, - color_layer_a: re_renderer::Rgba::from_rgb(1.0, 0.6, 0.0), - color_layer_b: re_renderer::Rgba::from_rgba_unmultiplied( - 0.25, 0.3, 1.0, 0.5, - ), - }), - ..Default::default() + let mut view_builder = ViewBuilder::new( + re_ctx, + TargetConfiguration { + name: "OutlinesDemo".into(), + resolution_in_pixel: resolution, + view_from_world: macaw::IsoTransform::look_at_rh( + camera_position, + glam::Vec3::ZERO, + glam::Vec3::Y, + ) + .unwrap(), + projection_from_view: Projection::Perspective { + vertical_fov: 70.0 * std::f32::consts::TAU / 360.0, + near_plane_distance: 0.01, }, - ) - .unwrap(); + pixels_from_point, + outline_config: Some(OutlineConfig { + outline_radius_pixel: (seconds_since_startup * 2.0).sin().abs() * 10.0 + 2.0, + color_layer_a: re_renderer::Rgba::from_rgb(1.0, 0.6, 0.0), + color_layer_b: re_renderer::Rgba::from_rgba_unmultiplied(0.25, 0.3, 1.0, 0.5), + }), + ..Default::default() + }, + ); let outline_mask_large_mesh = match ((seconds_since_startup * 0.5) as u64) % 5 { 0 => OutlineMaskPreference::NONE, diff --git a/crates/re_renderer/examples/picking.rs b/crates/re_renderer/examples/picking.rs index ee3413dae42d..afcb442ffbba 100644 --- a/crates/re_renderer/examples/picking.rs +++ b/crates/re_renderer/examples/picking.rs @@ -118,33 +118,29 @@ impl framework::Example for Picking { } } - let mut view_builder = ViewBuilder::default(); - // TODO(#1426): unify camera logic between examples. let camera_position = glam::vec3(1.0, 3.5, 7.0); - view_builder - .setup_view( - re_ctx, - TargetConfiguration { - name: "OutlinesDemo".into(), - resolution_in_pixel: resolution, - view_from_world: macaw::IsoTransform::look_at_rh( - camera_position, - glam::Vec3::ZERO, - glam::Vec3::Y, - ) - .unwrap(), - projection_from_view: Projection::Perspective { - vertical_fov: 70.0 * std::f32::consts::TAU / 360.0, - near_plane_distance: 0.01, - }, - pixels_from_point, - outline_config: None, - ..Default::default() + let mut view_builder = ViewBuilder::new( + re_ctx, + TargetConfiguration { + name: "OutlinesDemo".into(), + resolution_in_pixel: resolution, + view_from_world: macaw::IsoTransform::look_at_rh( + camera_position, + glam::Vec3::ZERO, + glam::Vec3::Y, + ) + .unwrap(), + projection_from_view: Projection::Perspective { + vertical_fov: 70.0 * std::f32::consts::TAU / 360.0, + near_plane_distance: 0.01, }, - ) - .unwrap(); + pixels_from_point, + outline_config: None, + ..Default::default() + }, + ); // Use an uneven number of pixels for the picking rect so that there is a clearly defined middle-pixel. // (for this sample a size of 1 would be sufficient, but for a real application you'd want to use a larger size to allow snapping) diff --git a/crates/re_renderer/shader/colormap.wgsl b/crates/re_renderer/shader/colormap.wgsl index d12b43246dca..59be61afdfe4 100644 --- a/crates/re_renderer/shader/colormap.wgsl +++ b/crates/re_renderer/shader/colormap.wgsl @@ -3,11 +3,11 @@ // NOTE: Keep in sync with `colormap.rs`! const COLORMAP_GRAYSCALE: u32 = 1u; -const COLORMAP_TURBO: u32 = 2u; -const COLORMAP_VIRIDIS: u32 = 3u; +const COLORMAP_INFERNO: u32 = 2u; +const COLORMAP_MAGMA: u32 = 3u; const COLORMAP_PLASMA: u32 = 4u; -const COLORMAP_MAGMA: u32 = 5u; -const COLORMAP_INFERNO: u32 = 6u; +const COLORMAP_TURBO: u32 = 5u; +const COLORMAP_VIRIDIS: u32 = 6u; /// Returns a gamma-space sRGB in 0-1 range. /// @@ -15,16 +15,16 @@ const COLORMAP_INFERNO: u32 = 6u; fn colormap_srgb(which: u32, t: f32) -> Vec3 { if which == COLORMAP_GRAYSCALE { return linear_from_srgb(Vec3(t)); + } else if which == COLORMAP_INFERNO { + return colormap_inferno_srgb(t); + } else if which == COLORMAP_MAGMA { + return colormap_magma_srgb(t); + } else if which == COLORMAP_PLASMA { + return colormap_plasma_srgb(t); } else if which == COLORMAP_TURBO { return colormap_turbo_srgb(t); } else if which == COLORMAP_VIRIDIS { return colormap_viridis_srgb(t); - } else if which == COLORMAP_PLASMA { - return colormap_plasma_srgb(t); - } else if which == COLORMAP_MAGMA { - return colormap_magma_srgb(t); - } else if which == COLORMAP_INFERNO { - return colormap_inferno_srgb(t); } else { return ERROR_RGBA.rgb; } diff --git a/crates/re_renderer/src/colormap.rs b/crates/re_renderer/src/colormap.rs index c64d85dbb24b..15cd98d5dc14 100644 --- a/crates/re_renderer/src/colormap.rs +++ b/crates/re_renderer/src/colormap.rs @@ -6,17 +6,42 @@ use glam::{Vec2, Vec3A, Vec4, Vec4Swizzles}; // NOTE: Keep in sync with `colormap.wgsl`! #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[repr(u32)] pub enum Colormap { // Reserve 0 for "disabled" /// Perceptually even #[default] Grayscale = 1, - Turbo = 2, - Viridis = 3, + Inferno = 2, + Magma = 3, Plasma = 4, - Magma = 5, - Inferno = 6, + Turbo = 5, + Viridis = 6, +} + +impl Colormap { + pub const ALL: [Self; 6] = [ + Self::Grayscale, + Self::Inferno, + Self::Magma, + Self::Plasma, + Self::Turbo, + Self::Viridis, + ]; +} + +impl std::fmt::Display for Colormap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Colormap::Grayscale => write!(f, "Grayscale"), + Colormap::Inferno => write!(f, "Inferno"), + Colormap::Magma => write!(f, "Magma"), + Colormap::Plasma => write!(f, "Plasma"), + Colormap::Turbo => write!(f, "Turbo"), + Colormap::Viridis => write!(f, "Viridis"), + } + } } pub fn colormap_srgb(which: Colormap, t: f32) -> [u8; 4] { diff --git a/crates/re_renderer/src/lib.rs b/crates/re_renderer/src/lib.rs index 3b9d660be33e..770c0589f7fe 100644 --- a/crates/re_renderer/src/lib.rs +++ b/crates/re_renderer/src/lib.rs @@ -41,7 +41,7 @@ pub use depth_offset::DepthOffset; pub use line_strip_builder::{LineStripBuilder, LineStripSeriesBuilder}; pub use point_cloud_builder::{PointCloudBatchBuilder, PointCloudBuilder}; pub use size::Size; -pub use view_builder::AutoSizeConfig; +pub use view_builder::{AutoSizeConfig, ViewBuilder}; pub use wgpu_resources::WgpuResourcePoolStatistics; mod draw_phases; diff --git a/crates/re_renderer/src/view_builder.rs b/crates/re_renderer/src/view_builder.rs index 704f0a22f7be..af4edc5287f7 100644 --- a/crates/re_renderer/src/view_builder.rs +++ b/crates/re_renderer/src/view_builder.rs @@ -32,9 +32,6 @@ struct QueuedDraw { #[derive(thiserror::Error, Debug)] pub enum ViewBuilderError { - #[error("ViewBuilder::setup_view needs to be called first.")] - ViewNotSetup, - #[error("Screenshot was already scheduled.")] ScreenshotAlreadyScheduled, @@ -43,11 +40,9 @@ pub enum ViewBuilderError { } /// The highest level rendering block in `re_renderer`. -/// Used to build up/collect various resources and then send them off for rendering of a single view. -#[derive(Default)] +/// Used to build up/collect various resources and then send them off for rendering of a single view. pub struct ViewBuilder { - /// Result of [`ViewBuilder::setup_view`] - needs to be `Option` sine some of the fields don't have a default. - setup: Option, + setup: ViewTargetSetup, queued_draws: Vec, // TODO(andreas): Consider making "render processors" a "thing" by establishing a form of hardcoded/limited-flexibility render-graph @@ -248,11 +243,7 @@ impl ViewBuilder { }, }); - pub fn setup_view( - &mut self, - ctx: &mut RenderContext, - config: TargetConfiguration, - ) -> Result<&mut Self, ViewBuilderError> { + pub fn new(ctx: &mut RenderContext, config: TargetConfiguration) -> Self { crate::profile_function!(); // Can't handle 0 size resolution since this would imply creating zero sized textures. @@ -297,24 +288,6 @@ impl ViewBuilder { }, ); - self.outline_mask_processor = config.outline_config.as_ref().map(|outline_config| { - OutlineMaskProcessor::new( - ctx, - outline_config, - &config.name, - config.resolution_in_pixel, - ) - }); - - self.queue_draw(&CompositorDrawData::new( - ctx, - &main_target_resolved, - self.outline_mask_processor - .as_ref() - .map(|p| p.final_voronoi_texture()), - &config.outline_config, - )); - let aspect_ratio = config.resolution_in_pixel[0] as f32 / config.resolution_in_pixel[1] as f32; @@ -453,7 +426,25 @@ impl ViewBuilder { frame_uniform_buffer, ); - self.setup = Some(ViewTargetSetup { + let outline_mask_processor = config.outline_config.as_ref().map(|outline_config| { + OutlineMaskProcessor::new( + ctx, + outline_config, + &config.name, + config.resolution_in_pixel, + ) + }); + + let composition_draw = queued_draw(&CompositorDrawData::new( + ctx, + &main_target_resolved, + outline_mask_processor + .as_ref() + .map(|p| p.final_voronoi_texture()), + &config.outline_config, + )); + + let setup = ViewTargetSetup { name: config.name, bind_group_0, main_target_msaa: hdr_render_target_msaa, @@ -461,9 +452,15 @@ impl ViewBuilder { depth_buffer, resolution_in_pixel: config.resolution_in_pixel, frame_uniform_buffer_content, - }); + }; - Ok(self) + Self { + setup, + queued_draws: vec![composition_draw], + outline_mask_processor, + screenshot_processor: Default::default(), + picking_processor: Default::default(), + } } fn draw_phase<'a>( @@ -491,21 +488,7 @@ impl ViewBuilder { draw_data: &D, ) -> &mut Self { crate::profile_function!(); - self.queued_draws.push(QueuedDraw { - draw_func: Box::new(move |ctx, phase, pass, draw_data| { - let renderers = ctx.renderers.read(); - let renderer = renderers - .get::() - .context("failed to retrieve renderer")?; - let draw_data = draw_data - .downcast_ref::() - .expect("passed wrong type of draw data"); - renderer.draw(&ctx.gpu_resources, phase, pass, draw_data) - }), - draw_data: Box::new(draw_data.clone()), - renderer_name: std::any::type_name::(), - participated_phases: D::Renderer::participated_phases(), - }); + self.queued_draws.push(queued_draw(draw_data)); self } @@ -518,10 +501,7 @@ impl ViewBuilder { ) -> anyhow::Result { crate::profile_function!(); - let setup = self - .setup - .as_ref() - .context("ViewBuilder::setup_view wasn't called yet")?; + let setup = &self.setup; let mut encoder = ctx .device @@ -610,7 +590,7 @@ impl ViewBuilder { /// Schedules the taking of a screenshot. /// - /// Needs to be called after [`ViewBuilder::setup_view`] and before [`ViewBuilder::draw`]. + /// Needs to be called before [`ViewBuilder::draw`]. /// Can only be called once per frame per [`ViewBuilder`]. /// /// Data from the screenshot needs to be retrieved via [`crate::ScreenshotProcessor::next_readback_result`]. @@ -640,12 +620,10 @@ impl ViewBuilder { return Err(ViewBuilderError::ScreenshotAlreadyScheduled); }; - let setup = self.setup.as_ref().ok_or(ViewBuilderError::ViewNotSetup)?; - self.screenshot_processor = Some(ScreenshotProcessor::new( ctx, - &setup.name, - setup.resolution_in_pixel.into(), + &self.setup.name, + self.setup.resolution_in_pixel.into(), identifier, user_data, )); @@ -655,7 +633,7 @@ impl ViewBuilder { /// Schedules the readback of a rectangle from the picking layer. /// - /// Needs to be called after [`ViewBuilder::setup_view`] and before [`ViewBuilder::draw`]. + /// Needs to be called before [`ViewBuilder::draw`]. /// Can only be called once per frame per [`ViewBuilder`]. /// /// The result will still be valid if the rectangle is partially or fully outside of bounds. @@ -697,14 +675,12 @@ impl ViewBuilder { return Err(ViewBuilderError::PickingRectAlreadyScheduled); }; - let setup = self.setup.as_ref().ok_or(ViewBuilderError::ViewNotSetup)?; - let picking_processor = PickingLayerProcessor::new( ctx, - &setup.name, - setup.resolution_in_pixel.into(), + &self.setup.name, + self.setup.resolution_in_pixel.into(), picking_rect, - &setup.frame_uniform_buffer_content, + &self.setup.frame_uniform_buffer_content, show_debug_view, readback_identifier, readback_user_data, @@ -714,7 +690,7 @@ impl ViewBuilder { self.queue_draw(&DebugOverlayDrawData::new( ctx, &picking_processor.picking_target, - setup.resolution_in_pixel.into(), + self.setup.resolution_in_pixel.into(), picking_rect, )); } @@ -733,22 +709,37 @@ impl ViewBuilder { ctx: &'a RenderContext, pass: &mut wgpu::RenderPass<'a>, screen_position: glam::Vec2, - ) -> Result<(), ViewBuilderError> { + ) { crate::profile_function!(); - let setup = self.setup.as_ref().ok_or(ViewBuilderError::ViewNotSetup)?; pass.set_viewport( screen_position.x, screen_position.y, - setup.resolution_in_pixel[0] as f32, - setup.resolution_in_pixel[1] as f32, + self.setup.resolution_in_pixel[0] as f32, + self.setup.resolution_in_pixel[1] as f32, 0.0, 1.0, ); - pass.set_bind_group(0, &setup.bind_group_0, &[]); + pass.set_bind_group(0, &self.setup.bind_group_0, &[]); self.draw_phase(ctx, DrawPhase::Compositing, pass); + } +} - Ok(()) +fn queued_draw(draw_data: &D) -> QueuedDraw { + QueuedDraw { + draw_func: Box::new(move |ctx, phase, pass, draw_data| { + let renderers = ctx.renderers.read(); + let renderer = renderers + .get::() + .context("failed to retrieve renderer")?; + let draw_data = draw_data + .downcast_ref::() + .expect("passed wrong type of draw data"); + renderer.draw(&ctx.gpu_resources, phase, pass, draw_data) + }), + draw_data: Box::new(draw_data.clone()), + renderer_name: std::any::type_name::(), + participated_phases: D::Renderer::participated_phases(), } } diff --git a/crates/re_tensor_ops/src/dimension_mapping.rs b/crates/re_tensor_ops/src/dimension_mapping.rs index d2f9dbab1595..37b7517c8281 100644 --- a/crates/re_tensor_ops/src/dimension_mapping.rs +++ b/crates/re_tensor_ops/src/dimension_mapping.rs @@ -1,6 +1,6 @@ use re_log_types::component_types; -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] pub struct DimensionSelector { pub visible: bool, pub dim_idx: usize, @@ -15,7 +15,7 @@ impl DimensionSelector { } } -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] pub struct DimensionMapping { /// Which dimensions have selectors, and are they visible? pub selectors: Vec, diff --git a/crates/re_viewer/src/gpu_bridge/mod.rs b/crates/re_viewer/src/gpu_bridge/mod.rs new file mode 100644 index 000000000000..0a70befc9057 --- /dev/null +++ b/crates/re_viewer/src/gpu_bridge/mod.rs @@ -0,0 +1,204 @@ +//! Bridge to `re_renderer` + +mod tensor_to_gpu; +pub use tensor_to_gpu::tensor_to_gpu; + +// ---------------------------------------------------------------------------- + +use egui::mutex::Mutex; + +use re_renderer::{ + renderer::ColormappedTexture, + resource_managers::{GpuTexture2DHandle, Texture2DCreationDesc}, + RenderContext, ViewBuilder, +}; + +// ---------------------------------------------------------------------------- + +/// Errors that can happen when supplying a tensor range to the GPU. +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum RangeError { + /// This is weird. Should only happen with JPEGs, and those should have been decoded already + #[error("Missing a range.")] + MissingRange, + + #[error("Non-finite range of values")] + NonfiniteRange, +} + +/// Get a valid, finite range for the gpu to use. +pub fn range(tensor_stats: &crate::misc::caches::TensorStats) -> Result<[f32; 2], RangeError> { + let (min, max) = tensor_stats.range.ok_or(RangeError::MissingRange)?; + + let min = min as f32; + let max = max as f32; + + if !min.is_finite() || !max.is_finite() { + Err(RangeError::NonfiniteRange) + } else if min == max { + // uniform range. This can explode the colormapping, so let's map all colors to the middle: + Ok([min - 1.0, max + 1.0]) + } else { + Ok([min, max]) + } +} + +// ---------------------------------------------------------------------------- + +pub fn viewport_resolution_in_pixels(clip_rect: egui::Rect, pixels_from_point: f32) -> [u32; 2] { + let min = (clip_rect.min.to_vec2() * pixels_from_point).round(); + let max = (clip_rect.max.to_vec2() * pixels_from_point).round(); + let resolution = max - min; + [resolution.x as u32, resolution.y as u32] +} + +pub fn get_or_create_texture<'a, Err>( + render_ctx: &mut RenderContext, + texture_key: u64, + try_create_texture_desc: impl FnOnce() -> Result, Err>, +) -> Result { + render_ctx.texture_manager_2d.get_or_create_with( + texture_key, + &mut render_ctx.gpu_resources.textures, + try_create_texture_desc, + ) +} + +/// Render a `re_render` view using the given clip rectangle. +pub fn renderer_paint_callback( + render_ctx: &mut re_renderer::RenderContext, + command_buffer: wgpu::CommandBuffer, + view_builder: re_renderer::ViewBuilder, + clip_rect: egui::Rect, + pixels_from_point: f32, +) -> egui::PaintCallback { + crate::profile_function!(); + + slotmap::new_key_type! { pub struct ViewBuilderHandle; } + + type ViewBuilderMap = slotmap::SlotMap; + + // egui paint callback are copyable / not a FnOnce (this in turn is because egui primitives can be callbacks and are copyable) + let command_buffer = std::sync::Arc::new(Mutex::new(Some(command_buffer))); + + let composition_view_builder_map = render_ctx + .active_frame + .per_frame_data_helper + .entry::() + .or_insert_with(Default::default); + let view_builder_handle = composition_view_builder_map.insert(view_builder); + + let screen_position = (clip_rect.min.to_vec2() * pixels_from_point).round(); + let screen_position = glam::vec2(screen_position.x, screen_position.y); + + egui::PaintCallback { + rect: clip_rect, + callback: std::sync::Arc::new( + egui_wgpu::CallbackFn::new() + .prepare( + move |_device, _queue, _encoder, _paint_callback_resources| { + let mut command_buffer = command_buffer.lock(); + vec![std::mem::replace(&mut *command_buffer, None) + .expect("egui_wgpu prepare callback called more than once")] + }, + ) + .paint(move |_info, render_pass, paint_callback_resources| { + crate::profile_scope!("paint"); + // TODO(andreas): This should work as well but doesn't work in the 3d view. + // Looks like a bug in egui, but unclear what's going on. + //let clip_rect = info.clip_rect_in_pixels(); + + let ctx = paint_callback_resources.get::().unwrap(); + ctx.active_frame + .per_frame_data_helper + .get::() + .unwrap()[view_builder_handle] + .composite(ctx, render_pass, screen_position); + }), + ), + } +} + +/// Render the given image, respecting the clip rectangle of the given painter. +pub fn render_image( + render_ctx: &mut re_renderer::RenderContext, + painter: &egui::Painter, + image_rect_on_screen: egui::Rect, + colormapped_texture: ColormappedTexture, + texture_options: egui::TextureOptions, + debug_name: &str, +) -> anyhow::Result<()> { + crate::profile_function!(); + + use re_renderer::renderer::{TextureFilterMag, TextureFilterMin}; + + // Where in "world space" to paint the image. + let space_rect = egui::Rect::from_min_size(egui::Pos2::ZERO, image_rect_on_screen.size()); + + let textured_rectangle = re_renderer::renderer::TexturedRect { + top_left_corner_position: glam::vec3(space_rect.min.x, space_rect.min.y, 0.0), + extent_u: glam::Vec3::X * space_rect.width(), + extent_v: glam::Vec3::Y * space_rect.height(), + colormapped_texture, + texture_filter_magnification: match texture_options.magnification { + egui::TextureFilter::Nearest => TextureFilterMag::Nearest, + egui::TextureFilter::Linear => TextureFilterMag::Linear, + }, + texture_filter_minification: match texture_options.minification { + egui::TextureFilter::Nearest => TextureFilterMin::Nearest, + egui::TextureFilter::Linear => TextureFilterMin::Linear, + }, + multiplicative_tint: egui::Rgba::WHITE, + depth_offset: 0, + outline_mask: Default::default(), + }; + + // ------------------------------------------------------------------------ + + let pixels_from_points = painter.ctx().pixels_per_point(); + let ui_from_space = egui::emath::RectTransform::from_to(space_rect, image_rect_on_screen); + let space_from_ui = ui_from_space.inverse(); + let space_from_points = space_from_ui.scale().y; + let points_from_pixels = 1.0 / painter.ctx().pixels_per_point(); + let space_from_pixel = space_from_points * points_from_pixels; + + let resolution_in_pixel = + crate::gpu_bridge::viewport_resolution_in_pixels(painter.clip_rect(), pixels_from_points); + anyhow::ensure!(resolution_in_pixel[0] > 0 && resolution_in_pixel[1] > 0); + + let camera_position_space = space_from_ui.transform_pos(painter.clip_rect().min); + + let top_left_position = glam::vec2(camera_position_space.x, camera_position_space.y); + let target_config = re_renderer::view_builder::TargetConfiguration { + name: debug_name.into(), + resolution_in_pixel, + view_from_world: macaw::IsoTransform::from_translation(-top_left_position.extend(0.0)), + projection_from_view: re_renderer::view_builder::Projection::Orthographic { + camera_mode: re_renderer::view_builder::OrthographicCameraMode::TopLeftCornerAndExtendZ, + vertical_world_size: space_from_pixel * resolution_in_pixel[1] as f32, + far_plane_distance: 1000.0, + }, + pixels_from_point: pixels_from_points, + auto_size_config: Default::default(), + outline_config: None, + }; + + let mut view_builder = ViewBuilder::new(render_ctx, target_config); + + view_builder.queue_draw(&re_renderer::renderer::RectangleDrawData::new( + render_ctx, + &[textured_rectangle], + )?); + + let command_buffer = view_builder.draw(render_ctx, re_renderer::Rgba::TRANSPARENT)?; + + painter.add(crate::gpu_bridge::renderer_paint_callback( + render_ctx, + command_buffer, + view_builder, + painter.clip_rect(), + painter.ctx().pixels_per_point(), + )); + + Ok(()) +} diff --git a/crates/re_viewer/src/misc/tensor_to_gpu.rs b/crates/re_viewer/src/gpu_bridge/tensor_to_gpu.rs similarity index 95% rename from crates/re_viewer/src/misc/tensor_to_gpu.rs rename to crates/re_viewer/src/gpu_bridge/tensor_to_gpu.rs index f6882cd2c203..f907b5b62fe9 100644 --- a/crates/re_viewer/src/misc/tensor_to_gpu.rs +++ b/crates/re_viewer/src/gpu_bridge/tensor_to_gpu.rs @@ -1,3 +1,5 @@ +//! Upload [`Tensor`] to [`re_renderer`]. + use std::borrow::Cow; use bytemuck::{allocation::pod_collect_to_vec, cast_slice, Pod}; @@ -7,11 +9,13 @@ use wgpu::TextureFormat; use re_log_types::component_types::{Tensor, TensorData}; use re_renderer::{ renderer::{ColorMapper, ColormappedTexture}, - resource_managers::{GpuTexture2DHandle, Texture2DCreationDesc}, + resource_managers::Texture2DCreationDesc, RenderContext, }; -use super::caches::TensorStats; +use crate::misc::caches::TensorStats; + +use super::get_or_create_texture; // ---------------------------------------------------------------------------- @@ -104,12 +108,7 @@ fn color_tensor_to_gpu( } else if texture_format == TextureFormat::R8Snorm { [-1.0, 1.0] } else { - // For instance: 16-bit images. - // TODO(emilk): consider assuming [0-1] range for all float tensors. - let (min, max) = tensor_stats - .range - .ok_or_else(|| anyhow::anyhow!("missing tensor range. compressed?"))?; - [min as f32, max as f32] + crate::gpu_bridge::range(tensor_stats)? }; let color_mapper = if texture_format.describe().components == 1 { @@ -382,18 +381,6 @@ fn general_texture_creation_desc_from_tensor<'a>( }) } -pub fn get_or_create_texture<'a, Err>( - render_ctx: &mut RenderContext, - texture_key: u64, - try_create_texture_desc: impl FnOnce() -> Result, Err>, -) -> Result { - render_ctx.texture_manager_2d.get_or_create_with( - texture_key, - &mut render_ctx.gpu_resources.textures, - try_create_texture_desc, - ) -} - fn cast_slice_to_cow(slice: &[From]) -> Cow<'_, [u8]> { cast_slice(slice).into() } diff --git a/crates/re_viewer/src/lib.rs b/crates/re_viewer/src/lib.rs index 10ceb56ce0c6..85a2939b3160 100644 --- a/crates/re_viewer/src/lib.rs +++ b/crates/re_viewer/src/lib.rs @@ -5,6 +5,7 @@ mod app; pub mod env_vars; +pub(crate) mod gpu_bridge; pub mod math; mod misc; mod remote_viewer_app; diff --git a/crates/re_viewer/src/misc/mod.rs b/crates/re_viewer/src/misc/mod.rs index 023aafc3d6e9..7ba4a8e82f05 100644 --- a/crates/re_viewer/src/misc/mod.rs +++ b/crates/re_viewer/src/misc/mod.rs @@ -6,7 +6,6 @@ pub(crate) mod mesh_loader; pub mod queries; mod selection_state; pub(crate) mod space_info; -pub mod tensor_to_gpu; pub(crate) mod time_control; pub(crate) mod time_control_ui; mod transform_cache; diff --git a/crates/re_viewer/src/ui/view_spatial/mod.rs b/crates/re_viewer/src/ui/view_spatial/mod.rs index 141f6c3f9dd6..240c83ae2023 100644 --- a/crates/re_viewer/src/ui/view_spatial/mod.rs +++ b/crates/re_viewer/src/ui/view_spatial/mod.rs @@ -5,7 +5,7 @@ mod space_camera_3d; mod ui; mod ui_2d; mod ui_3d; -mod ui_renderer_bridge; +pub mod ui_renderer_bridge; pub use self::scene::{Image, MeshSource, MeshSourceData, SceneSpatial, UiLabel, UiLabelTarget}; pub use self::space_camera_3d::SpaceCamera3D; diff --git a/crates/re_viewer/src/ui/view_spatial/scene/scene_part/images.rs b/crates/re_viewer/src/ui/view_spatial/scene/scene_part/images.rs index 16dc90f6ad64..0c2c29156e50 100644 --- a/crates/re_viewer/src/ui/view_spatial/scene/scene_part/images.rs +++ b/crates/re_viewer/src/ui/view_spatial/scene/scene_part/images.rs @@ -45,7 +45,7 @@ fn push_tensor_texture( let debug_name = entity_path.to_string(); let tensor_stats = ctx.cache.tensor_stats(tensor); - match crate::misc::tensor_to_gpu::tensor_to_gpu( + match crate::gpu_bridge::tensor_to_gpu( ctx.render_ctx, &debug_name, tensor, diff --git a/crates/re_viewer/src/ui/view_spatial/ui_2d.rs b/crates/re_viewer/src/ui/view_spatial/ui_2d.rs index 0cedd7495fff..ca790381a47a 100644 --- a/crates/re_viewer/src/ui/view_spatial/ui_2d.rs +++ b/crates/re_viewer/src/ui/view_spatial/ui_2d.rs @@ -10,13 +10,12 @@ use super::{ SpatialNavigationMode, ViewSpatialState, }; use crate::{ + gpu_bridge, misc::{HoveredSpace, SpaceViewHighlights}, ui::{ view_spatial::{ ui::outline_config, - ui_renderer_bridge::{ - fill_view_builder, get_viewport, renderer_paint_callback, ScreenBackground, - }, + ui_renderer_bridge::{fill_view_builder, ScreenBackground}, SceneSpatial, }, SpaceViewId, @@ -322,12 +321,7 @@ fn view_2d_scrollable( return response; }; - // TODO(andreas): separate setup for viewbuilder doesn't make sense. - let mut view_builder = ViewBuilder::default(); - if let Err(err) = view_builder.setup_view(ctx.render_ctx, target_config) { - re_log::error!("Failed to setup view: {}", err); - return response; - } + 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 = create_labels( @@ -381,7 +375,7 @@ fn view_2d_scrollable( return response; } }; - painter.add(renderer_paint_callback( + painter.add(gpu_bridge::renderer_paint_callback( ctx.render_ctx, command_buffer, view_builder, @@ -412,7 +406,8 @@ fn setup_target_config( any_outlines: bool, ) -> anyhow::Result { let pixels_from_points = painter.ctx().pixels_per_point(); - let resolution_in_pixel = get_viewport(painter.clip_rect(), pixels_from_points); + let resolution_in_pixel = + gpu_bridge::viewport_resolution_in_pixels(painter.clip_rect(), pixels_from_points); anyhow::ensure!(resolution_in_pixel[0] > 0 && resolution_in_pixel[1] > 0); let camera_position_space = space_from_ui.transform_pos(painter.clip_rect().min); diff --git a/crates/re_viewer/src/ui/view_spatial/ui_3d.rs b/crates/re_viewer/src/ui/view_spatial/ui_3d.rs index c464af8731e9..86cb34e808f4 100644 --- a/crates/re_viewer/src/ui/view_spatial/ui_3d.rs +++ b/crates/re_viewer/src/ui/view_spatial/ui_3d.rs @@ -11,13 +11,12 @@ use re_renderer::{ }; use crate::{ + gpu_bridge, misc::{HoveredSpace, Item, SpaceViewHighlights}, ui::{ view_spatial::{ ui::{create_labels, outline_config, picking, screenshot_context_menu}, - ui_renderer_bridge::{ - fill_view_builder, get_viewport, renderer_paint_callback, ScreenBackground, - }, + ui_renderer_bridge::{fill_view_builder, ScreenBackground}, SceneSpatial, SpaceCamera3D, SpatialNavigationMode, }, SpaceViewId, @@ -315,7 +314,8 @@ pub fn view_3d( } // Determine view port resolution and position. - let resolution_in_pixel = get_viewport(rect, ui.ctx().pixels_per_point()); + let resolution_in_pixel = + gpu_bridge::viewport_resolution_in_pixels(rect, ui.ctx().pixels_per_point()); if resolution_in_pixel[0] == 0 || resolution_in_pixel[1] == 0 { return; } @@ -340,12 +340,7 @@ pub fn view_3d( .then(|| outline_config(ui.ctx())), }; - let mut view_builder = ViewBuilder::default(); - // TODO(andreas): separate setup_view doesn't make sense, add a `new` method instead. - if let Err(err) = view_builder.setup_view(ctx.render_ctx, target_config) { - re_log::error!("Failed to setup view: {}", err); - return; - } + 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 = create_labels( @@ -508,7 +503,7 @@ pub fn view_3d( return; } }; - ui.painter().add(renderer_paint_callback( + ui.painter().add(gpu_bridge::renderer_paint_callback( ctx.render_ctx, command_buffer, view_builder, diff --git a/crates/re_viewer/src/ui/view_spatial/ui_renderer_bridge.rs b/crates/re_viewer/src/ui/view_spatial/ui_renderer_bridge.rs index 5e84036d766b..513abdf82430 100644 --- a/crates/re_viewer/src/ui/view_spatial/ui_renderer_bridge.rs +++ b/crates/re_viewer/src/ui/view_spatial/ui_renderer_bridge.rs @@ -1,4 +1,3 @@ -use egui::mutex::Mutex; use re_renderer::{ renderer::{DepthCloudDrawData, GenericSkyboxDrawData, MeshDrawData, RectangleDrawData}, view_builder::ViewBuilder, @@ -7,13 +6,6 @@ use re_renderer::{ use super::scene::SceneSpatialPrimitives; -pub fn get_viewport(clip_rect: egui::Rect, pixels_from_point: f32) -> [u32; 2] { - let min = (clip_rect.min.to_vec2() * pixels_from_point).round(); - let max = (clip_rect.max.to_vec2() * pixels_from_point).round(); - let resolution = max - min; - [resolution.x as u32, resolution.y as u32] -} - pub enum ScreenBackground { GenericSkybox, ClearColor(re_renderer::Rgba), @@ -57,58 +49,3 @@ pub fn fill_view_builder( Ok(command_buffer) } - -slotmap::new_key_type! { pub struct ViewBuilderHandle; } - -type ViewBuilderMap = slotmap::SlotMap; - -pub fn renderer_paint_callback( - render_ctx: &mut RenderContext, - command_buffer: wgpu::CommandBuffer, - view_builder: ViewBuilder, - clip_rect: egui::Rect, - pixels_from_point: f32, -) -> egui::PaintCallback { - crate::profile_function!(); - - // egui paint callback are copyable / not a FnOnce (this in turn is because egui primitives can be callbacks and are copyable) - let command_buffer = std::sync::Arc::new(Mutex::new(Some(command_buffer))); - - let composition_view_builder_map = render_ctx - .active_frame - .per_frame_data_helper - .entry::() - .or_insert_with(Default::default); - let view_builder_handle = composition_view_builder_map.insert(view_builder); - - let screen_position = (clip_rect.min.to_vec2() * pixels_from_point).round(); - let screen_position = glam::vec2(screen_position.x, screen_position.y); - - egui::PaintCallback { - rect: clip_rect, - callback: std::sync::Arc::new( - egui_wgpu::CallbackFn::new() - .prepare( - move |_device, _queue, _encoder, _paint_callback_resources| { - let mut command_buffer = command_buffer.lock(); - vec![std::mem::replace(&mut *command_buffer, None) - .expect("egui_wgpu prepare callback called more than once")] - }, - ) - .paint(move |_info, render_pass, paint_callback_resources| { - crate::profile_scope!("paint"); - // TODO(andreas): This should work as well but doesn't work in the 3d view. - // Looks like a bug in egui, but unclear what's going on. - //let clip_rect = info.clip_rect_in_pixels(); - - let ctx = paint_callback_resources.get::().unwrap(); - ctx.active_frame - .per_frame_data_helper - .get::() - .unwrap()[view_builder_handle] - .composite(ctx, render_pass, screen_position) - .expect("Failed compositing view builder with main target."); - }), - ), - } -} diff --git a/crates/re_viewer/src/ui/view_tensor/mod.rs b/crates/re_viewer/src/ui/view_tensor/mod.rs index 349f77cc6aa4..9c54bf671b05 100644 --- a/crates/re_viewer/src/ui/view_tensor/mod.rs +++ b/crates/re_viewer/src/ui/view_tensor/mod.rs @@ -1,3 +1,5 @@ +mod tensor_slice_to_gpu; + mod scene; pub(crate) use self::scene::SceneTensor; diff --git a/crates/re_viewer/src/ui/view_tensor/tensor_slice_to_gpu.rs b/crates/re_viewer/src/ui/view_tensor/tensor_slice_to_gpu.rs new file mode 100644 index 000000000000..0bb4d9f9d81c --- /dev/null +++ b/crates/re_viewer/src/ui/view_tensor/tensor_slice_to_gpu.rs @@ -0,0 +1,174 @@ +use re_log_types::{ + component_types::{Tensor, TensorCastError}, + TensorDataType, +}; +use re_renderer::{renderer::ColormappedTexture, resource_managers::Texture2DCreationDesc}; + +use crate::{ + gpu_bridge::{range, RangeError}, + misc::caches::TensorStats, +}; + +use super::{ + ui::{selected_tensor_slice, SliceSelection}, + ViewTensorState, +}; + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum TensorUploadError { + #[error(transparent)] + TensorCastError(#[from] TensorCastError), + + #[error("Expected a 2D slice")] + Not2D, + + #[error(transparent)] + RangeError(#[from] RangeError), +} + +pub fn colormapped_texture( + render_ctx: &mut re_renderer::RenderContext, + tensor: &Tensor, + tensor_stats: &TensorStats, + state: &ViewTensorState, +) -> Result { + crate::profile_function!(); + + let range = range(tensor_stats)?; + let texture = upload_texture_slice_to_gpu(render_ctx, tensor, state.slice())?; + + let color_mapping = state.color_mapping(); + + Ok(ColormappedTexture { + texture, + range, + gamma: color_mapping.gamma, + color_mapper: Some(re_renderer::renderer::ColorMapper::Function( + color_mapping.map, + )), + }) +} + +fn upload_texture_slice_to_gpu( + render_ctx: &mut re_renderer::RenderContext, + tensor: &Tensor, + slice_selection: &SliceSelection, +) -> Result { + let id = egui::util::hash((tensor.id(), slice_selection)); + + crate::gpu_bridge::get_or_create_texture(render_ctx, id, || { + texture_desc_from_tensor(tensor, slice_selection) + }) +} + +fn texture_desc_from_tensor( + tensor: &Tensor, + slice_selection: &SliceSelection, +) -> Result, TensorUploadError> { + use wgpu::TextureFormat; + crate::profile_function!(); + + match tensor.dtype() { + TensorDataType::U8 => { + let tensor = ndarray::ArrayViewD::::try_from(tensor)?; + to_texture_desc(&tensor, slice_selection, TextureFormat::R8Uint, |x| x) + } + TensorDataType::U16 => { + let tensor = ndarray::ArrayViewD::::try_from(tensor)?; + to_texture_desc(&tensor, slice_selection, TextureFormat::R16Uint, |x| x) + } + TensorDataType::U32 => { + let tensor = ndarray::ArrayViewD::::try_from(tensor)?; + to_texture_desc(&tensor, slice_selection, TextureFormat::R32Uint, |x| x) + } + TensorDataType::U64 => { + // narrow to f32: + let tensor = ndarray::ArrayViewD::::try_from(tensor)?; + to_texture_desc( + &tensor, + slice_selection, + TextureFormat::R32Float, + |x: u64| x as f32, + ) + } + TensorDataType::I8 => { + let tensor = ndarray::ArrayViewD::::try_from(tensor)?; + to_texture_desc(&tensor, slice_selection, TextureFormat::R8Sint, |x| x) + } + TensorDataType::I16 => { + let tensor = ndarray::ArrayViewD::::try_from(tensor)?; + to_texture_desc(&tensor, slice_selection, TextureFormat::R16Sint, |x| x) + } + TensorDataType::I32 => { + let tensor = ndarray::ArrayViewD::::try_from(tensor)?; + to_texture_desc(&tensor, slice_selection, TextureFormat::R32Sint, |x| x) + } + TensorDataType::I64 => { + // narrow to f32: + let tensor = ndarray::ArrayViewD::::try_from(tensor)?; + to_texture_desc( + &tensor, + slice_selection, + TextureFormat::R32Float, + |x: i64| x as f32, + ) + } + TensorDataType::F16 => { + let tensor = ndarray::ArrayViewD::::try_from(tensor)?; + to_texture_desc(&tensor, slice_selection, TextureFormat::R16Float, |x| x) + } + TensorDataType::F32 => { + let tensor = ndarray::ArrayViewD::::try_from(tensor)?; + to_texture_desc(&tensor, slice_selection, TextureFormat::R32Float, |x| x) + } + TensorDataType::F64 => { + // narrow to f32: + let tensor = ndarray::ArrayViewD::::try_from(tensor)?; + to_texture_desc( + &tensor, + slice_selection, + TextureFormat::R32Float, + |x: f64| x as f32, + ) + } + } +} + +fn to_texture_desc( + tensor: &ndarray::ArrayViewD<'_, From>, + slice_selection: &SliceSelection, + format: wgpu::TextureFormat, + caster: impl Fn(From) -> To, +) -> Result, TensorUploadError> { + crate::profile_function!(); + + use ndarray::Dimension as _; + + let slice = selected_tensor_slice(slice_selection, tensor); + let slice = slice + .into_dimensionality::() + .map_err(|_err| TensorUploadError::Not2D)?; + + let (height, width) = slice.raw_dim().into_pattern(); + let mut pixels: Vec = vec![To::zeroed(); height * width]; + let pixels_view = ndarray::ArrayViewMut2::from_shape(slice.raw_dim(), pixels.as_mut_slice()) + .expect("Mismatched length."); + + { + crate::profile_scope!("copy_from_slice"); + ndarray::Zip::from(pixels_view) + .and(slice) + .for_each(|pixel: &mut To, value: &From| { + *pixel = caster(*value); + }); + } + + crate::profile_scope!("pod_collect_to_vec"); + Ok(Texture2DCreationDesc { + label: "tensor_slice".into(), + data: bytemuck::pod_collect_to_vec(&pixels).into(), + format, + width: width as u32, + height: height as u32, + }) +} diff --git a/crates/re_viewer/src/ui/view_tensor/ui.rs b/crates/re_viewer/src/ui/view_tensor/ui.rs index 12d57d6b4374..f9adcaad42c2 100644 --- a/crates/re_viewer/src/ui/view_tensor/ui.rs +++ b/crates/re_viewer/src/ui/view_tensor/ui.rs @@ -1,13 +1,11 @@ use std::{collections::BTreeMap, fmt::Display}; use eframe::emath::Align2; -use egui::{epaint::TextShape, Color32, ColorImage, NumExt as _, Vec2}; -use ndarray::{Axis, Ix2}; +use egui::{epaint::TextShape, NumExt as _, Vec2}; +use ndarray::Axis; -use re_log_types::{ - component_types::{self, Tensor}, - TensorDataType, -}; +use re_log_types::component_types::{self, Tensor}; +use re_renderer::Colormap; use re_tensor_ops::dimension_mapping::{DimensionMapping, DimensionSelector}; use crate::ui::data_ui::image::tensor_summary_ui_grid_contents; @@ -16,13 +14,20 @@ use super::dimension_mapping_ui; // --- -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -pub struct ViewTensorState { +/// How we slice a given tensor +#[derive(Clone, Debug, Hash, serde::Deserialize, serde::Serialize)] +pub struct SliceSelection { /// How we select which dimensions to project the tensor onto. - dimension_mapping: DimensionMapping, + pub dim_mapping: DimensionMapping, /// Selected value of every dimension (iff they are in [`DimensionMapping::selectors`]). - selector_values: BTreeMap, + pub selector_values: BTreeMap, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct ViewTensorState { + /// What slice are we vieiwing? + slice: SliceSelection, /// How we map values to colors. color_mapping: ColorMapping, @@ -39,14 +44,24 @@ pub struct ViewTensorState { impl ViewTensorState { pub fn create(tensor: &Tensor) -> ViewTensorState { Self { - selector_values: Default::default(), - dimension_mapping: DimensionMapping::create(tensor.shape()), + slice: SliceSelection { + dim_mapping: DimensionMapping::create(tensor.shape()), + selector_values: Default::default(), + }, color_mapping: ColorMapping::default(), texture_settings: TextureSettings::default(), tensor: Some(tensor.clone()), } } + pub fn slice(&self) -> &SliceSelection { + &self.slice + } + + pub fn color_mapping(&self) -> &ColorMapping { + &self.color_mapping + } + pub(crate) fn ui(&mut self, ctx: &mut crate::misc::ViewerContext<'_>, ui: &mut egui::Ui) { let Some(tensor) = &self.tensor else { ui.label("No Tensor shown in this Space View."); @@ -68,18 +83,18 @@ impl ViewTensorState { ui.separator(); ui.strong("Dimension Mapping"); - dimension_mapping_ui(ctx.re_ui, ui, &mut self.dimension_mapping, tensor.shape()); + dimension_mapping_ui(ctx.re_ui, ui, &mut self.slice.dim_mapping, tensor.shape()); let default_mapping = DimensionMapping::create(tensor.shape()); if ui .add_enabled( - self.dimension_mapping != default_mapping, + self.slice.dim_mapping != default_mapping, egui::Button::new("Reset mapping"), ) .on_disabled_hover_text("The default is already set up.") .on_hover_text("Reset dimension mapping to the default.") .clicked() { - self.dimension_mapping = DimensionMapping::create(tensor.shape()); + self.slice.dim_mapping = DimensionMapping::create(tensor.shape()); } } } @@ -94,15 +109,16 @@ pub(crate) fn view_tensor( state.tensor = Some(tensor.clone()); - if !state.dimension_mapping.is_valid(tensor.num_dim()) { - state.dimension_mapping = DimensionMapping::create(tensor.shape()); + if !state.slice.dim_mapping.is_valid(tensor.num_dim()) { + state.slice.dim_mapping = DimensionMapping::create(tensor.shape()); } let default_item_spacing = ui.spacing_mut().item_spacing; ui.spacing_mut().item_spacing.y = 0.0; // No extra spacing between sliders and tensor if state - .dimension_mapping + .slice + .dim_mapping .selectors .iter() .any(|selector| selector.visible) @@ -117,289 +133,118 @@ pub(crate) fn view_tensor( }); } - tensor_ui(ctx, ui, state, tensor); + let dimension_labels = { + let dm = &state.slice.dim_mapping; + [ + ( + dimension_name(&tensor.shape, dm.width.unwrap()), + dm.invert_width, + ), + ( + dimension_name(&tensor.shape, dm.height.unwrap()), + dm.invert_height, + ), + ] + }; + + egui::ScrollArea::both().show(ui, |ui| { + if let Err(err) = tensor_slice_ui(ctx, ui, state, tensor, dimension_labels) { + ui.label(ctx.re_ui.error_text(err.to_string())); + } + }); } -fn tensor_ui( +fn tensor_slice_ui( ctx: &mut crate::misc::ViewerContext<'_>, ui: &mut egui::Ui, state: &mut ViewTensorState, tensor: &Tensor, -) { - let tensor_shape = tensor.shape(); + dimension_labels: [(String, bool); 2], +) -> anyhow::Result<()> { + let (response, painter, image_rect) = paint_tensor_slice(ctx, ui, state, tensor)?; - let tensor_stats = ctx.cache.tensor_stats(tensor); - let range = tensor_stats.range; - let color_mapping = &state.color_mapping; - - match tensor.dtype() { - TensorDataType::U8 => match ndarray::ArrayViewD::::try_from(tensor) { - Ok(tensor) => { - let color_from_value = |value: u8| { - // We always use the full range for u8 - color_mapping.color_from_normalized(value as f32 / 255.0) - }; - - let slice = selected_tensor_slice(state, &tensor); - slice_ui(ctx, ui, state, tensor_shape, slice, color_from_value); - } - Err(err) => { - ui.label(ctx.re_ui.error_text(err.to_string())); - } - }, - - TensorDataType::U16 => match ndarray::ArrayViewD::::try_from(tensor) { - Ok(tensor) => { - let color_from_value = |value: u16| { - let (tensor_min, tensor_max) = range.unwrap_or((0.0, u16::MAX as f64)); // the cache should provide the range - color_mapping.color_from_normalized(egui::remap( - value as f32, - tensor_min as f32..=tensor_max as f32, - 0.0..=1.0, - )) - }; - - let slice = selected_tensor_slice(state, &tensor); - slice_ui(ctx, ui, state, tensor_shape, slice, color_from_value); - } - Err(err) => { - ui.label(ctx.re_ui.error_text(err.to_string())); - } - }, - - TensorDataType::U32 => match ndarray::ArrayViewD::::try_from(tensor) { - Ok(tensor) => { - let (tensor_min, tensor_max) = range.unwrap_or((0.0, u32::MAX as f64)); // the cache should provide the range - - let color_from_value = |value: u32| { - color_mapping.color_from_normalized(egui::remap( - value as f64, - tensor_min..=tensor_max, - 0.0..=1.0, - ) as f32) - }; - - let slice = selected_tensor_slice(state, &tensor); - slice_ui(ctx, ui, state, tensor_shape, slice, color_from_value); - } - Err(err) => { - ui.label(ctx.re_ui.error_text(err.to_string())); - } - }, - - TensorDataType::U64 => match ndarray::ArrayViewD::::try_from(tensor) { - Ok(tensor) => { - let color_from_value = |value: u64| { - let (tensor_min, tensor_max) = range.unwrap_or((0.0, u64::MAX as f64)); // the cache should provide the range - color_mapping.color_from_normalized(egui::remap( - value as f64, - tensor_min..=tensor_max, - 0.0..=1.0, - ) as f32) - }; - - let slice = selected_tensor_slice(state, &tensor); - slice_ui(ctx, ui, state, tensor_shape, slice, color_from_value); - } - Err(err) => { - ui.label(ctx.re_ui.error_text(err.to_string())); - } - }, - - TensorDataType::I8 => match ndarray::ArrayViewD::::try_from(tensor) { - Ok(tensor) => { - let color_from_value = |value: i8| { - // We always use the full range for i8: - let (tensor_min, tensor_max) = (i8::MIN as f32, i8::MAX as f32); - color_mapping.color_from_normalized(egui::remap( - value as f32, - tensor_min..=tensor_max, - 0.0..=1.0, - )) - }; - - let slice = selected_tensor_slice(state, &tensor); - slice_ui(ctx, ui, state, tensor_shape, slice, color_from_value); - } - Err(err) => { - ui.label(ctx.re_ui.error_text(err.to_string())); - } - }, - - TensorDataType::I16 => match ndarray::ArrayViewD::::try_from(tensor) { - Ok(tensor) => { - let color_from_value = |value: i16| { - let (tensor_min, tensor_max) = - range.unwrap_or((i16::MIN as f64, i16::MAX as f64)); // the cache should provide the range - color_mapping.color_from_normalized(egui::remap( - value as f32, - tensor_min as f32..=tensor_max as f32, - 0.0..=1.0, - )) - }; - - let slice = selected_tensor_slice(state, &tensor); - slice_ui(ctx, ui, state, tensor_shape, slice, color_from_value); - } - Err(err) => { - ui.label(ctx.re_ui.error_text(err.to_string())); - } - }, - - TensorDataType::I32 => match ndarray::ArrayViewD::::try_from(tensor) { - Ok(tensor) => { - let color_from_value = |value: i32| { - let (tensor_min, tensor_max) = - range.unwrap_or((i32::MIN as f64, i32::MAX as f64)); // the cache should provide the range - color_mapping.color_from_normalized(egui::remap( - value as f64, - tensor_min..=tensor_max, - 0.0..=1.0, - ) as f32) - }; - - let slice = selected_tensor_slice(state, &tensor); - slice_ui(ctx, ui, state, tensor_shape, slice, color_from_value); - } - Err(err) => { - ui.label(ctx.re_ui.error_text(err.to_string())); - } - }, - - TensorDataType::I64 => match ndarray::ArrayViewD::::try_from(tensor) { - Ok(tensor) => { - let color_from_value = |value: i64| { - let (tensor_min, tensor_max) = - range.unwrap_or((i64::MIN as f64, i64::MAX as f64)); // the cache should provide the range - color_mapping.color_from_normalized(egui::remap( - value as f64, - tensor_min..=tensor_max, - 0.0..=1.0, - ) as f32) - }; - - let slice = selected_tensor_slice(state, &tensor); - slice_ui(ctx, ui, state, tensor_shape, slice, color_from_value); - } - Err(err) => { - ui.label(ctx.re_ui.error_text(err.to_string())); - } - }, - - TensorDataType::F16 => match ndarray::ArrayViewD::::try_from(tensor) { - Ok(tensor) => { - let color_from_value = |value: half::f16| { - let (tensor_min, tensor_max) = range.unwrap_or((0.0, 1.0)); // the cache should provide the range - color_mapping.color_from_normalized(egui::remap( - value.to_f32(), - tensor_min as f32..=tensor_max as f32, - 0.0..=1.0, - )) - }; - - let slice = selected_tensor_slice(state, &tensor); - slice_ui(ctx, ui, state, tensor_shape, slice, color_from_value); - } - Err(err) => { - ui.label(ctx.re_ui.error_text(err.to_string())); - } - }, - - TensorDataType::F32 => match ndarray::ArrayViewD::::try_from(tensor) { - Ok(tensor) => { - let color_from_value = |value: f32| { - let (tensor_min, tensor_max) = range.unwrap_or((0.0, 1.0)); // the cache should provide the range - color_mapping.color_from_normalized(egui::remap( - value, - tensor_min as f32..=tensor_max as f32, - 0.0..=1.0, - )) - }; - - let slice = selected_tensor_slice(state, &tensor); - slice_ui(ctx, ui, state, tensor_shape, slice, color_from_value); - } - Err(err) => { - ui.label(ctx.re_ui.error_text(err.to_string())); - } - }, - - TensorDataType::F64 => match ndarray::ArrayViewD::::try_from(tensor) { - Ok(tensor) => { - let color_from_value = |value: f64| { - let (tensor_min, tensor_max) = range.unwrap_or((0.0, 1.0)); // the cache should provide the range - color_mapping.color_from_normalized(egui::remap( - value, - tensor_min..=tensor_max, - 0.0..=1.0, - ) as f32) - }; - - let slice = selected_tensor_slice(state, &tensor); - slice_ui(ctx, ui, state, tensor_shape, slice, color_from_value); - } - Err(err) => { - ui.label(ctx.re_ui.error_text(err.to_string())); - } - }, + if !response.hovered() { + let font_id = egui::TextStyle::Body.resolve(ui.style()); + paint_axis_names(ui, &painter, image_rect, font_id, dimension_labels); } + + Ok(()) } -// ---------------------------------------------------------------------------- +fn paint_tensor_slice( + ctx: &mut crate::misc::ViewerContext<'_>, + ui: &mut egui::Ui, + state: &mut ViewTensorState, + tensor: &Tensor, +) -> anyhow::Result<(egui::Response, egui::Painter, egui::Rect)> { + crate::profile_function!(); -#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] -enum Colormap { - Greyscale, - Turbo, - Virdis, -} + let tensor_stats = ctx.cache.tensor_stats(tensor); + let colormapped_texture = super::tensor_slice_to_gpu::colormapped_texture( + ctx.render_ctx, + tensor, + tensor_stats, + state, + )?; + let texture = ctx + .render_ctx + .texture_manager_2d + .get(&colormapped_texture.texture)?; + let size = texture.creation_desc.size; + let width = size.width; + let height = size.height; + + let img_size = egui::vec2(width as _, height as _); + let img_size = Vec2::max(Vec2::splat(1.0), img_size); // better safe than sorry + let desired_size = match state.texture_settings.scaling { + TextureScaling::Original => img_size, + TextureScaling::Fill => { + let desired_size = ui.available_size(); + if state.texture_settings.keep_aspect_ratio { + let scale = (desired_size / img_size).min_elem(); + img_size * scale + } else { + desired_size + } + } + }; -impl std::fmt::Display for Colormap { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Colormap::Greyscale => "Greyscale", - Colormap::Turbo => "Turbo", - Colormap::Virdis => "Viridis", - }) - } + let (response, painter) = ui.allocate_painter(desired_size, egui::Sense::hover()); + let rect = response.rect; + let image_rect = egui::Rect::from_min_max(rect.min, rect.max); + + let debug_name = "tensor_slice"; + crate::gpu_bridge::render_image( + ctx.render_ctx, + &painter, + image_rect, + colormapped_texture, + state.texture_settings.options, + debug_name, + )?; + + Ok((response, painter, image_rect)) } +// ---------------------------------------------------------------------------- + /// How we map values to colors. #[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)] -struct ColorMapping { - map: Colormap, - gamma: f32, +pub struct ColorMapping { + pub map: Colormap, + pub gamma: f32, } impl Default for ColorMapping { fn default() -> Self { Self { - map: Colormap::Virdis, + map: Colormap::Viridis, gamma: 1.0, } } } impl ColorMapping { - pub fn color_from_normalized(&self, f: f32) -> Color32 { - let f = f.powf(self.gamma); - - match self.map { - Colormap::Greyscale => { - let lum = (f * 255.0 + 0.5) as u8; - Color32::from_gray(lum) - } - Colormap::Turbo => { - let [r, g, b, _] = re_renderer::colormap_turbo_srgb(f); - Color32::from_rgb(r, g, b) - } - Colormap::Virdis => { - let [r, g, b, _] = re_renderer::colormap_viridis_srgb(f); - Color32::from_rgb(r, g, b) - } - } - } - fn ui(&mut self, re_ui: &re_ui::ReUi, ui: &mut egui::Ui) { let ColorMapping { map, gamma } = self; @@ -408,9 +253,9 @@ impl ColorMapping { .selected_text(map.to_string()) .show_ui(ui, |ui| { ui.style_mut().wrap = Some(false); - ui.selectable_value(map, Colormap::Greyscale, Colormap::Greyscale.to_string()); - ui.selectable_value(map, Colormap::Virdis, Colormap::Virdis.to_string()); - ui.selectable_value(map, Colormap::Turbo, Colormap::Turbo.to_string()); + for option in Colormap::ALL { + ui.selectable_value(map, option, option.to_string()); + } }); ui.end_row(); @@ -476,46 +321,6 @@ impl Default for TextureSettings { } } -// helpers -impl TextureSettings { - fn paint_image( - &self, - ui: &mut egui::Ui, - margin: Vec2, - image: ColorImage, - ) -> (egui::Response, egui::Painter, egui::Rect) { - let img_size = egui::vec2(image.size[0] as _, image.size[1] as _); - let img_size = Vec2::max(Vec2::splat(1.0), img_size); // better safe than sorry - let desired_size = match self.scaling { - TextureScaling::Original => img_size + margin, - TextureScaling::Fill => { - let desired_size = ui.available_size() - margin; - if self.keep_aspect_ratio { - let scale = (desired_size / img_size).min_elem(); - img_size * scale - } else { - desired_size - } - } - }; - - // TODO(cmc): don't recreate texture unless necessary - let texture = ui.ctx().load_texture("tensor_slice", image, self.options); - - let (response, painter) = ui.allocate_painter(desired_size, egui::Sense::hover()); - let rect = response.rect; - let image_rect = egui::Rect::from_min_max(rect.min + margin, rect.max); - - let mut mesh = egui::Mesh::with_texture(texture.id()); - let uv = egui::Rect::from_min_max(egui::Pos2::ZERO, egui::pos2(1.0, 1.0)); - mesh.add_rect_with_uv(image_rect, uv, Color32::WHITE); - - painter.add(mesh); - - (response, painter, image_rect) - } -} - // ui impl TextureSettings { fn ui(&mut self, re_ui: &re_ui::ReUi, ui: &mut egui::Ui) { @@ -544,62 +349,66 @@ impl TextureSettings { }); ui.end_row(); - re_ui.grid_left_hand_label(ui, "Filtering"); + // TODO(#1612): support texture filtering again + if false { + re_ui + .grid_left_hand_label(ui, "Filtering") + .on_hover_text("Filtering to use when magnifying"); - fn tf_to_string(tf: egui::TextureFilter) -> &'static str { - match tf { - egui::TextureFilter::Nearest => "Nearest", - egui::TextureFilter::Linear => "Linear", + fn tf_to_string(tf: egui::TextureFilter) -> &'static str { + match tf { + egui::TextureFilter::Nearest => "Nearest", + egui::TextureFilter::Linear => "Linear", + } } - } - egui::ComboBox::from_id_source("texture_filter") - .selected_text(tf_to_string(options.magnification)) - .show_ui(ui, |ui| { - ui.style_mut().wrap = Some(false); - ui.set_min_width(64.0); + egui::ComboBox::from_id_source("texture_filter") + .selected_text(tf_to_string(options.magnification)) + .show_ui(ui, |ui| { + ui.style_mut().wrap = Some(false); + ui.set_min_width(64.0); - let mut selectable_value = |ui: &mut egui::Ui, e| { - ui.selectable_value(&mut options.magnification, e, tf_to_string(e)) - }; - selectable_value(ui, egui::TextureFilter::Linear); - selectable_value(ui, egui::TextureFilter::Nearest); - }); - ui.end_row(); + let mut selectable_value = |ui: &mut egui::Ui, e| { + ui.selectable_value(&mut options.magnification, e, tf_to_string(e)) + }; + selectable_value(ui, egui::TextureFilter::Linear); + selectable_value(ui, egui::TextureFilter::Nearest); + }); + ui.end_row(); + } } } // ---------------------------------------------------------------------------- -fn selected_tensor_slice<'a, T: Copy>( - state: &ViewTensorState, +pub fn selected_tensor_slice<'a, T: Copy>( + slice_selection: &SliceSelection, tensor: &'a ndarray::ArrayViewD<'_, T>, ) -> ndarray::ArrayViewD<'a, T> { - let dim_mapping = &state.dimension_mapping; + let SliceSelection { + dim_mapping: dimension_mapping, + selector_values, + } = slice_selection; - assert!(dim_mapping.is_valid(tensor.ndim())); + assert!(dimension_mapping.is_valid(tensor.ndim())); // TODO(andreas) - shouldn't just give up here - if dim_mapping.width.is_none() || dim_mapping.height.is_none() { + if dimension_mapping.width.is_none() || dimension_mapping.height.is_none() { return tensor.view(); } - let axis = dim_mapping + let axis = dimension_mapping .height .into_iter() - .chain(dim_mapping.width.into_iter()) - .chain(dim_mapping.selectors.iter().map(|s| s.dim_idx)) + .chain(dimension_mapping.width.into_iter()) + .chain(dimension_mapping.selectors.iter().map(|s| s.dim_idx)) .collect::>(); let mut slice = tensor.view().permuted_axes(axis); - for DimensionSelector { dim_idx, .. } in &dim_mapping.selectors { - let selector_value = state - .selector_values - .get(dim_idx) - .copied() - .unwrap_or_default() as usize; + for DimensionSelector { dim_idx, .. } in &dimension_mapping.selectors { + let selector_value = selector_values.get(dim_idx).copied().unwrap_or_default() as usize; assert!( selector_value < slice.shape()[2], - "Bad tensor slicing. Trying to select slice index {selector_value} of dim=2. tensor shape: {:?}, dim_mapping: {dim_mapping:#?}", + "Bad tensor slicing. Trying to select slice index {selector_value} of dim=2. tensor shape: {:?}, dim_mapping: {dimension_mapping:#?}", tensor.shape() ); @@ -607,53 +416,16 @@ fn selected_tensor_slice<'a, T: Copy>( // This call removes Axis(2), so the next iteration of the loop does the right thing again. slice.index_axis_inplace(Axis(2), selector_value); } - if dim_mapping.invert_height { + if dimension_mapping.invert_height { slice.invert_axis(Axis(0)); } - if dim_mapping.invert_width { + if dimension_mapping.invert_width { slice.invert_axis(Axis(1)); } slice } -fn slice_ui( - ctx: &mut crate::misc::ViewerContext<'_>, - ui: &mut egui::Ui, - view_state: &ViewTensorState, - tensor_shape: &[component_types::TensorDimension], - slice: ndarray::ArrayViewD<'_, T>, - color_from_value: impl Fn(T) -> Color32, -) { - crate::profile_function!(); - - let ndims = slice.ndim(); - if let Ok(slice) = slice.into_dimensionality::() { - let dimension_labels = { - let dm = &view_state.dimension_mapping; - [ - ( - dimension_name(tensor_shape, dm.width.unwrap()), - dm.invert_width, - ), - ( - dimension_name(tensor_shape, dm.height.unwrap()), - dm.invert_height, - ), - ] - }; - - let image = into_image(&slice, color_from_value); - egui::ScrollArea::both().show(ui, |ui| { - image_ui(ui, view_state, image, dimension_labels); - }); - } else { - ui.label(ctx.re_ui.error_text(format!( - "Only 2D slices supported at the moment, but slice ndim {ndims}" - ))); - } -} - fn dimension_name(shape: &[component_types::TensorDimension], dim_idx: usize) -> String { let dim = &shape[dim_idx]; dim.name.as_ref().map_or_else( @@ -662,50 +434,6 @@ fn dimension_name(shape: &[component_types::TensorDimension], dim_idx: usize) -> ) } -fn into_image( - slice: &ndarray::ArrayView2<'_, T>, - color_from_value: impl Fn(T) -> Color32, -) -> ColorImage { - crate::profile_function!(); - - use ndarray::Dimension as _; - let (height, width) = slice.raw_dim().into_pattern(); - let mut image = egui::ColorImage::new([width, height], Color32::DEBUG_COLOR); - - let image_view = - ndarray::ArrayViewMut2::from_shape(slice.raw_dim(), image.pixels.as_mut_slice()) - .expect("Mismatched length."); - - crate::profile_scope!("color_mapper"); - ndarray::Zip::from(image_view) - .and(slice) - .for_each(|pixel, value| { - *pixel = color_from_value(*value); - }); - - image -} - -fn image_ui( - ui: &mut egui::Ui, - view_state: &ViewTensorState, - image: ColorImage, - dimension_labels: [(String, bool); 2], -) { - crate::profile_function!(); - - let font_id = egui::TextStyle::Body.resolve(ui.style()); - - let margin = egui::vec2(0.0, 0.0); - - let (response, painter, image_rect) = - view_state.texture_settings.paint_image(ui, margin, image); - - if !response.hovered() { - paint_axis_names(ui, &painter, image_rect, font_id, dimension_labels); - } -} - fn paint_axis_names( ui: &mut egui::Ui, painter: &egui::Painter, @@ -834,7 +562,7 @@ fn paint_axis_names( } fn selectors_ui(ui: &mut egui::Ui, state: &mut ViewTensorState, tensor: &Tensor) { - for selector in &state.dimension_mapping.selectors { + for selector in &state.slice.dim_mapping.selectors { if !selector.visible { continue; } @@ -843,6 +571,7 @@ fn selectors_ui(ui: &mut egui::Ui, state: &mut ViewTensorState, tensor: &Tensor) let size = dim.size; let selector_value = state + .slice .selector_values .entry(selector.dim_idx) .or_insert_with(|| size / 2); // start in the middle diff --git a/scripts/lint.py b/scripts/lint.py index 594748c94056..375e156ed263 100755 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -270,7 +270,7 @@ def lint_file(filepath: str, args: Any) -> int: errors, lines_out = lint_vertical_spacing(lines_in) for error in errors: - print(f"{filepath}{error}") + print(f"{filepath}:{error}") if args.fix and lines_in != lines_out: with open(filepath, "w") as f: