Skip to main content

re_tf/transform_resolution_cache/
cache.rs

1use std::sync::Arc;
2
3use ahash::HashMap;
4use parking_lot::{ArcRwLockReadGuard, RawRwLock, RwLock};
5use re_byte_size::SizeBytes;
6use re_chunk_store::ChunkStore;
7use re_entity_db::EntityDb;
8use re_log::{debug_assert, debug_assert_eq};
9use re_log_types::{TimeInt, TimelineName};
10use re_sdk_types::archetypes;
11
12use crate::frame_id_registry::FrameIdRegistry;
13use crate::transform_aspect::TransformAspect;
14
15use super::cached_transforms_for_timeline::CachedTransformsForTimeline;
16use super::iter_child_frames_in_chunk;
17
18type ArcRwLock<T> = Arc<RwLock<T>>;
19
20/// Resolves all transform relationship defining components to affine transforms for fast lookup.
21///
22/// It only handles resulting transforms individually to each frame connection, not how these transforms propagate in the tree.
23/// For transform tree propagation see [`crate::TransformForest`].
24///
25/// There are different kinds of transforms handled here:
26/// * [`archetypes::Transform3D`]
27///   Tree transforms that should propagate in the tree (via [`crate::TransformForest`]).
28/// * [`re_sdk_types::components::PinholeProjection`] and [`re_sdk_types::components::ViewCoordinates`]
29///   Pinhole projections & associated view coordinates used for visualizing cameras in 3D and embedding 2D in 3D
30/// * [`archetypes::InstancePoses3D`]
31///   Instance poses that should be applied to the tree transforms (via [`crate::TransformForest`]) but not propagate.
32///   Also unlike tree transforms, these are not associated with transform frames but rather with entity paths.
33pub struct TransformResolutionCache {
34    /// The frame id registry is co-located in the resolution cache for convenience:
35    /// the resolution cache is often the lowest level of transform access and
36    /// thus allowing us to access debug information across the stack.
37    frame_id_registry: ArcRwLock<FrameIdRegistry>,
38
39    /// The timelines for which we have cached transforms for.
40    ///
41    /// Some timelines may be missing from this map.
42    /// They will be lazily initialized from scratch on-demand.
43    per_timeline: HashMap<TimelineName, ArcRwLock<CachedTransformsForTimeline>>,
44
45    static_timeline: ArcRwLock<CachedTransformsForTimeline>,
46}
47
48impl Default for TransformResolutionCache {
49    #[inline]
50    fn default() -> Self {
51        Self {
52            frame_id_registry: Default::default(),
53            per_timeline: Default::default(),
54            static_timeline: Arc::new(RwLock::new(CachedTransformsForTimeline::new_static())),
55        }
56    }
57}
58
59impl TransformResolutionCache {
60    /// Creates a new cache, initialized with the static timeline and frame registry from the given entity database.
61    ///
62    /// Per-timeline data will be lazily initialized on demand.
63    pub fn new(entity_db: &EntityDb) -> Self {
64        re_tracing::profile_function!();
65
66        let mut cache = Self::default();
67
68        for chunk in entity_db.storage_engine().store().iter_physical_chunks() {
69            // Register all frames even if this chunk doesn't have transform data.
70            cache
71                .frame_id_registry
72                .write()
73                .register_all_frames_in_chunk(chunk);
74
75            let aspects = TransformAspect::transform_aspects_of(chunk);
76            if aspects.is_empty() {
77                continue;
78            }
79
80            // Only process static chunks - temporal data will be lazily initialized per-timeline.
81            if chunk.is_static() {
82                cache.add_static_chunk(chunk, aspects);
83            }
84        }
85
86        cache
87    }
88}
89
90impl SizeBytes for TransformResolutionCache {
91    fn heap_size_bytes(&self) -> u64 {
92        re_tracing::profile_function!();
93
94        let Self {
95            frame_id_registry,
96            per_timeline,
97            static_timeline,
98        } = self;
99
100        frame_id_registry.heap_size_bytes()
101            + per_timeline.heap_size_bytes()
102            + static_timeline.heap_size_bytes()
103    }
104}
105
106impl re_byte_size::MemUsageTreeCapture for TransformResolutionCache {
107    fn capture_mem_usage_tree(&self) -> re_byte_size::MemUsageTree {
108        re_tracing::profile_function!();
109
110        let Self {
111            frame_id_registry,
112            per_timeline,
113            static_timeline,
114        } = self;
115
116        let mut per_timeline_node = re_byte_size::MemUsageNode::new();
117        for (timeline, cached_transforms) in per_timeline {
118            per_timeline_node = per_timeline_node.with_child(
119                timeline.to_string(),
120                cached_transforms.read().capture_mem_usage_tree(),
121            );
122        }
123
124        re_byte_size::MemUsageNode::new()
125            .with_child("frame_id_registry", frame_id_registry.total_size_bytes())
126            .with_child("per_timeline", per_timeline_node.into_tree())
127            .with_child("static_timeline", static_timeline.total_size_bytes())
128            .into_tree()
129    }
130}
131
132impl TransformResolutionCache {
133    /// Returns the registry of all known frame ids.
134    #[inline]
135    pub fn frame_id_registry(&self) -> ArcRwLockReadGuard<RawRwLock, FrameIdRegistry> {
136        self.frame_id_registry.read_arc()
137    }
138
139    /// Accesses the transform component tracking data for a given timeline.
140    #[inline]
141    pub fn transforms_for_timeline(
142        &self,
143        timeline: TimelineName,
144    ) -> ArcRwLockReadGuard<RawRwLock, CachedTransformsForTimeline> {
145        if let Some(per_timeline) = self.per_timeline.get(&timeline) {
146            per_timeline.read_arc()
147        } else {
148            self.static_timeline.read_arc()
149        }
150    }
151
152    /// Returns an iterator over all initialized timelines.
153    #[inline]
154    pub fn cached_timelines(&self) -> impl Iterator<Item = TimelineName> + '_ {
155        self.per_timeline.keys().copied()
156    }
157
158    /// Ensure we have a cache for this timeline.
159    pub fn ensure_timeline_is_initialized(
160        &mut self,
161        chunk_store: &ChunkStore,
162        timeline: TimelineName,
163    ) {
164        re_tracing::profile_function!(timeline);
165
166        let static_timeline = self.static_timeline.read();
167        let frame_id_registry = self.frame_id_registry.read();
168
169        self.per_timeline.entry(timeline).or_insert_with(|| {
170            Arc::new(RwLock::new(CachedTransformsForTimeline::new_temporal(
171                timeline,
172                &static_timeline,
173                &frame_id_registry,
174                chunk_store,
175            )))
176        });
177    }
178
179    /// Evicts a timeline from the cache.
180    pub fn evict_timeline_cache(&mut self, timeline: TimelineName) {
181        re_tracing::profile_function!(); // There can be A LOT of tiny allocations to drop.
182        self.per_timeline.remove(&timeline);
183    }
184
185    /// Makes sure the internal transform index is up to date and outdated cache entries are discarded.
186    ///
187    /// This needs to be called once per frame prior to any transform propagation.
188    /// (which is done by [`crate::TransformForest`])
189    ///
190    /// This will internally…
191    /// * keep track of which child frames are influenced by which entity
192    /// * create empty entries for where transforms may change over time (may happen conservatively - creating more entries than needed)
193    ///     * this may invalidate previous entries at the same position
194    /// * remove cached entries if chunks were GC'ed
195    pub fn process_store_events<'a>(
196        &mut self,
197        events: impl Iterator<Item = &'a re_chunk_store::ChunkStoreEvent>,
198    ) {
199        re_tracing::profile_function!();
200
201        for event in events {
202            // This doesn't maintain a collection of chunks that needs to be kept in sync 1:1 with
203            // the store, rather it just keeps track of what entities have what properties, and for
204            // that a delta chunk is all we need.
205            let Some(delta_chunk) = event.delta_chunk() else {
206                continue; // virtual event, we don't care
207            };
208
209            // Since entity paths lead to implicit frames, we have to prime our lookup table
210            // with them even if this chunk doesn't have transform data.
211            self.frame_id_registry
212                .write()
213                .register_all_frames_in_chunk(delta_chunk);
214
215            let aspects = TransformAspect::transform_aspects_of(delta_chunk);
216            if aspects.is_empty() {
217                continue;
218            }
219
220            if event.is_deletion() {
221                self.remove_chunk(delta_chunk, aspects);
222            } else if delta_chunk.is_static() {
223                self.add_static_chunk(delta_chunk, aspects);
224            } else {
225                self.add_temporal_chunk(delta_chunk, aspects);
226            }
227        }
228    }
229
230    fn add_temporal_chunk(&self, chunk: &re_chunk_store::Chunk, aspects: TransformAspect) {
231        re_tracing::profile_function!(format!(
232            "{} rows, {}",
233            chunk.num_rows(),
234            chunk.entity_path()
235        ));
236
237        debug_assert!(!chunk.is_static());
238
239        let static_timeline = self.static_timeline.read();
240        let frame_id_registry = self.frame_id_registry.read();
241
242        for timeline in chunk.timelines().keys() {
243            // Skip timelines that haven't been requested yet (lazy initialization).
244            let Some(per_timeline) = self.per_timeline.get(timeline) else {
245                continue;
246            };
247            let mut per_timeline = per_timeline.write();
248
249            per_timeline.add_temporal_chunk(
250                chunk,
251                aspects,
252                *timeline,
253                &static_timeline,
254                &frame_id_registry,
255            );
256        }
257    }
258
259    fn add_static_chunk(&mut self, chunk: &re_chunk_store::Chunk, aspects: TransformAspect) {
260        re_tracing::profile_function!();
261
262        debug_assert!(chunk.is_static());
263
264        let entity_path = chunk.entity_path();
265        let place_holder_timeline = TimelineName::new("ignored for static chunk");
266
267        let transform_child_frame_component =
268            archetypes::Transform3D::descriptor_child_frame().component;
269        let pinhole_child_frame_component = archetypes::Pinhole::descriptor_child_frame().component;
270
271        let mut static_timeline = self.static_timeline.write();
272        let frame_id_registry = self.frame_id_registry.read();
273
274        // Add a static transform invalidation to affected child frames on ALL timelines.
275
276        if aspects.contains(TransformAspect::Frame) {
277            for (time, frame) in iter_child_frames_in_chunk(
278                chunk,
279                place_holder_timeline,
280                transform_child_frame_component,
281            ) {
282                debug_assert_eq!(time, TimeInt::STATIC);
283
284                let frame_transforms = static_timeline.get_or_create_tree_transforms_static(
285                    entity_path,
286                    frame,
287                    &frame_id_registry,
288                );
289                frame_transforms.invalidate_transform_at(TimeInt::STATIC);
290
291                #[cfg_attr(not(debug_assertions), expect(clippy::for_kv_map))]
292                for (_timeline, per_timeline) in &mut self.per_timeline {
293                    // Don't call `get_or_create_tree_transforms_temporal` here since we may not yet know a temporal entity that this is associated with.
294                    // Also, this may be the first time we associate with a static entity instead which `get_or_create_tree_transforms_static` takes care of.
295                    let mut per_timeline_guard = per_timeline.write();
296                    let transforms = per_timeline_guard.get_or_create_tree_transforms_static(
297                        entity_path,
298                        frame,
299                        &frame_id_registry,
300                    );
301                    transforms.invalidate_transform_at(TimeInt::STATIC);
302
303                    // Entry might have been newly created. Have to ensure that its associated with the right timeline.
304                    #[cfg(debug_assertions)]
305                    {
306                        transforms.timeline = Some(*_timeline);
307                    }
308                }
309            }
310        }
311        if aspects.contains(TransformAspect::Pose) {
312            let frame_transforms =
313                static_timeline.get_or_create_pose_transforms_static(entity_path);
314            frame_transforms.invalidate_at(TimeInt::STATIC);
315
316            for per_timeline in self.per_timeline.values_mut() {
317                per_timeline
318                    .write()
319                    .get_or_create_pose_transforms_temporal(entity_path, &static_timeline)
320                    .invalidate_at(TimeInt::STATIC);
321            }
322        }
323        if aspects.contains(TransformAspect::PinholeOrViewCoordinates) {
324            for (time, frame) in iter_child_frames_in_chunk(
325                chunk,
326                place_holder_timeline,
327                pinhole_child_frame_component,
328            ) {
329                debug_assert_eq!(time, TimeInt::STATIC);
330
331                let frame_transforms = static_timeline.get_or_create_tree_transforms_static(
332                    entity_path,
333                    frame,
334                    &frame_id_registry,
335                );
336                frame_transforms.invalidate_pinhole_projection_at(TimeInt::STATIC);
337
338                #[cfg_attr(not(debug_assertions), expect(clippy::for_kv_map))]
339                for (_timeline, per_timeline) in &mut self.per_timeline {
340                    // Don't call `get_or_create_tree_transforms_temporal` here since we may not yet know a temporal entity that this is associated with.
341                    // Also, this may be the first time we associate with a static entity instead which `get_or_create_tree_transforms_static` takes care of.
342                    let mut per_timeline_guard = per_timeline.write();
343                    let transforms = per_timeline_guard.get_or_create_tree_transforms_static(
344                        entity_path,
345                        frame,
346                        &frame_id_registry,
347                    );
348                    transforms.invalidate_pinhole_projection_at(TimeInt::STATIC);
349
350                    // Entry might have been newly created. Have to ensure that its associated with the right timeline.
351                    #[cfg(debug_assertions)]
352                    {
353                        transforms.timeline = Some(*_timeline);
354                    }
355                }
356            }
357        }
358
359        // Don't care about clears here, they don't have any effect for keeping track of changes when logged static.
360    }
361
362    fn remove_chunk(&mut self, chunk: &re_chunk_store::Chunk, aspects: TransformAspect) {
363        re_tracing::profile_function!();
364
365        // TODO(andreas): handle removal of static chunks?
366        for timeline in chunk.timelines().keys() {
367            let Some(per_timeline_rw) = self.per_timeline.get_mut(timeline) else {
368                continue;
369            };
370
371            let mut per_timeline = per_timeline_rw.write();
372            per_timeline.remove_chunk(chunk, aspects, *timeline);
373
374            // Remove the entire timeline if it's empty.
375            let is_empty = per_timeline.per_child_frame_transforms.is_empty();
376            drop(per_timeline);
377            if is_empty {
378                self.per_timeline.remove(timeline);
379            }
380        }
381    }
382}