ifc_lite_core/decoder/caches.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! `EntityDecoder`'s cache management.
6//!
7//! Split from `decoder.rs` under the house rule (AGENTS.md). These are one
8//! cohesive concern — what is retained, what is handed across rayon workers,
9//! and what a caller may drop to bound memory — and they read better together
10//! than scattered among the decode paths that populate them.
11
12use rustc_hash::FxHashMap;
13use std::sync::Arc;
14
15use super::EntityDecoder;
16use crate::DecodedEntity;
17
18impl EntityDecoder<'_> {
19 /// Read an indexed entity, honoring existing memoized values but retaining
20 /// no new value. Linear discovery passes must not fill the geometry cache
21 /// with every unrelated property set they inspect once.
22 pub(crate) fn decode_by_id_transient(&mut self, id: u32) -> crate::Result<DecodedEntity> {
23 if let Some(entity) = self.cache.get(&id) {
24 return Ok(entity.as_ref().clone());
25 }
26 self.build_index();
27 let (start, end) = self.entity_index.as_ref().and_then(|index| index.lookup(id))
28 .ok_or_else(|| crate::Error::parse(0, format!("Entity #{} not found", id)))?;
29 let entity = self.decode_at_uncached(start, end)?;
30 // Match decode_at's parsed-id cache lookup, including caller-supplied
31 // indexes whose key differs from the id declared at that span.
32 Ok(match self.cache.get(&entity.id) {
33 Some(cached) => cached.as_ref().clone(),
34 None => entity,
35 })
36 }
37
38 /// Drain the populated cache out of this decoder for sharing across
39 /// rayon tasks. After calling this, the decoder is empty (cache
40 /// moved out); callers typically then drop the decoder.
41 pub fn drain_cache(&mut self) -> FxHashMap<u32, Arc<DecodedEntity>> {
42 std::mem::take(&mut self.cache)
43 }
44
45 /// Clear all caches to free memory
46 pub fn clear_cache(&mut self) {
47 self.cache.clear();
48 self.point_cache.clear();
49 self.placement_transform_cache.clear();
50 }
51
52 /// Clear only the decoded-entity cache, keeping the placement memo.
53 ///
54 /// For a caller that drains on a size trigger while resolving placements.
55 /// The two caches have very different value per byte: an entry in the
56 /// entity cache saves one re-decode of one entity, while an entry in the
57 /// placement memo can save re-walking a chain that thousands of elements
58 /// share — a site or building transform is resolved once and read by every
59 /// product under it. Dropping the memo to bound the entity cache trades the
60 /// expensive one away to bound the cheap one, and does it invisibly: output
61 /// stays correct and large models simply get slower.
62 pub fn clear_entity_cache(&mut self) {
63 self.cache.clear();
64 }
65
66 /// Clear only the point coordinate cache (used after BREP preprocessing).
67 /// The entity cache is preserved for subsequent geometry processing.
68 pub fn clear_point_cache(&mut self) {
69 self.point_cache.clear();
70 }
71
72 /// Move the CartesianPoint coordinate cache OUT of this decoder, leaving it
73 /// empty. Paired with [`Self::set_point_cache`] to hoist the cache across the
74 /// per-element decoders a single worker builds within one batch: the cache is
75 /// pure memoization of `content` + point id -> coords, so reusing it across
76 /// elements is byte-identical (speed only). Cheap: moves the map header, not
77 /// its contents.
78 pub fn take_point_cache(&mut self) -> FxHashMap<u32, (f64, f64, f64)> {
79 std::mem::take(&mut self.point_cache)
80 }
81
82 /// Install a previously-accumulated point cache (see [`Self::take_point_cache`]).
83 /// Does not reset the hit/miss counters, which stay per-decoder so each job's
84 /// [`Self::point_cache_stats`] reflect only that job's activity.
85 pub fn set_point_cache(&mut self, cache: FxHashMap<u32, (f64, f64, f64)>) {
86 self.point_cache = cache;
87 }
88
89 /// `(hits, misses)` served by [`Self::get_polyloop_coords_cached`] over this
90 /// decoder's lifetime. A hit is a CartesianPoint served from the cache; a miss
91 /// is one parsed for the first time. Non-zero hits after processing more than
92 /// one faceted part with a shared point list prove cross-element memoization.
93 pub fn point_cache_stats(&self) -> (u64, u64) {
94 (self.point_cache_hits, self.point_cache_misses)
95 }
96
97 /// Move the placement-transform memo OUT of this decoder, leaving it empty.
98 /// Paired with [`Self::set_placement_transform_cache`] to hoist the cache
99 /// across the per-element decoders a single worker builds within one batch,
100 /// exactly like [`Self::take_point_cache`]. The memo is a pure function of
101 /// `content` + placement id (deterministic `parent * local` composition), so
102 /// reusing it across elements is byte-identical (speed only). Cheap: moves
103 /// the map header, not its contents.
104 pub fn take_placement_transform_cache(&mut self) -> FxHashMap<u32, [f64; 16]> {
105 std::mem::take(&mut self.placement_transform_cache)
106 }
107
108 /// Install a previously-accumulated placement-transform memo (see
109 /// [`Self::take_placement_transform_cache`]).
110 pub fn set_placement_transform_cache(&mut self, cache: FxHashMap<u32, [f64; 16]>) {
111 self.placement_transform_cache = cache;
112 }
113
114 /// Read a memoized placement world transform by placement id. Returns a copy
115 /// (`[f64; 16]` is `Copy`) so the caller can drop the borrow before
116 /// reconstructing its `Matrix4`. The array is the opaque column-major layout
117 /// written by [`Self::cache_placement_transform`].
118 pub fn get_placement_transform_cached(&self, id: u32) -> Option<[f64; 16]> {
119 self.placement_transform_cache.get(&id).copied()
120 }
121
122 /// Memoize a placement world transform under its placement id.
123 ///
124 /// This is an unconditional insert, and last write wins. It does not
125 /// validate `transform`, does not compare it against any entry already held
126 /// for `id`, and has no way to tell a complete world transform from a
127 /// partial one — nothing in this crate computes placement transforms, so
128 /// nothing here can.
129 ///
130 /// Whether the memo is a pure function of the placement id is therefore a
131 /// property of the CALLERS, not of this method. The geometry router is the
132 /// one that owes it: it stores only fully composed local/linear/grid
133 /// transforms, and in particular never stores one composed from a walk its
134 /// depth guard cut short, because what such a walk composed depends on the
135 /// depth it was entered at rather than on the placement id (#3012). A caller
136 /// that breaks that discipline does not fail here — it makes whichever
137 /// reader happens to query the id first decide the answer for every reader
138 /// after it, including across workers via
139 /// [`Self::take_placement_transform_cache`].
140 pub fn cache_placement_transform(&mut self, id: u32, transform: [f64; 16]) {
141 self.placement_transform_cache.insert(id, transform);
142 }
143
144 /// Get cache size
145 pub fn cache_size(&self) -> usize {
146 self.cache.len()
147 }
148}