Skip to main content

taffy/tree/
cache.rs

1//! A cache for storing the results of layout computation
2
3#![allow(clippy::unusual_byte_groupings)]
4
5use crate::geometry::Size;
6use crate::style::AvailableSpace;
7use crate::tree::{LayoutInput, LayoutOutput, RunMode};
8use crate::RequestedAxis;
9
10/// The number of cache entries for each node in the tree
11const CACHE_SIZE: usize = 9;
12
13// Manually written-out results of float to u32 bit casts because
14// `f32::to_bits` is not yet const at our MSRV.
15
16/// `f32::INFINITY` as a u32
17const INFINITY_BITS: u32 = 0b_0_11111111_00000000000000000000000_u32;
18/// `f32::NEG_INFINITY` as a u32
19const NEG_INFINITY_BITS: u32 = 0b_1_11111111_00000000000000000000000_u32;
20
21// The `CacheKey` encodes two f32s as a u64. We know that the f32s will always be
22// non-negative, so we pack two extra bits encoding the `RequestedAxis` into the
23// sign bits of the f32s. These constants help to encode and decode those bits.
24
25/// The sign bit of the first f32
26const SIGN_BIT_1: u64 = 1u64 << 63;
27/// The sign bit of the second f32
28const SIGN_BIT_2: u64 = 1u64 << 31;
29/// Mask of both sign bits (used to compute NON_SIGN_BITS_MASK)
30const BOTH_SIGN_BITS_MASK: u64 = SIGN_BIT_1 | SIGN_BIT_2;
31/// Mask of excluding the sign bits (used when setting/getting the size excluding the packed bits)
32const NON_SIGN_BITS_MASK: u64 = !BOTH_SIGN_BITS_MASK;
33
34/// Mask which includes only the bits which encode the x-axis value that we can use to ignore the
35/// y-axis value when comparing a cache key.
36const X_AXIS_VALUE_MASK: u64 = (u32::MAX as u64) << 32;
37
38/// Pack `Option<f32>` into `u32`
39#[inline(always)]
40fn option_cache_key(input: Option<f32>) -> u32 {
41    match input {
42        Some(value) => value.to_bits(),
43        None => INFINITY_BITS,
44    }
45}
46
47/// Pack `Size<Option<f32>>` into `u64`
48#[inline(always)]
49fn size_option_cache_key(input: Size<Option<f32>>) -> u64 {
50    (option_cache_key(input.width) as u64) << 32 | option_cache_key(input.height) as u64
51}
52
53/// Pack `AvailableSpace` into `u32`
54#[inline(always)]
55fn available_space_cache_key(input: AvailableSpace) -> u32 {
56    match input {
57        AvailableSpace::Definite(value) => (-value).to_bits(),
58        AvailableSpace::MinContent => NEG_INFINITY_BITS,
59        AvailableSpace::MaxContent => INFINITY_BITS,
60    }
61}
62
63/// Pack `Size<AvailableSpace>` into `u64`
64#[inline(always)]
65#[allow(dead_code)]
66fn size_available_space_cache_key(input: Size<AvailableSpace>) -> u64 {
67    (available_space_cache_key(input.width) as u64) << 32 | available_space_cache_key(input.height) as u64
68}
69
70/// Encodes combination of a `known_dimension` (Option<f32>) and `AvailableSpace` in
71/// a single dimension into a cache key in a single dimension.
72#[inline(always)]
73fn mixed_cache_key(kd: Option<f32>, avs: AvailableSpace) -> u32 {
74    kd.map(|kd| kd.to_bits()).unwrap_or_else(|| available_space_cache_key(avs))
75}
76
77/// Encodes combination of a `known_dimension` (Option<f32>) and `AvailableSpace` in
78/// two dimensions into a cache key in a single dimension.
79#[inline(always)]
80fn size_mixed_cache_key(kd: Size<Option<f32>>, avs: Size<AvailableSpace>) -> u64 {
81    (mixed_cache_key(kd.width, avs.width) as u64) << 32 | mixed_cache_key(kd.height, avs.height) as u64
82}
83
84/// Space-optimised cache key that packs bits into as small a size as possible
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[cfg_attr(feature = "serde", derive(Serialize))]
87struct CacheKey {
88    /// The initial cached size of the node itself
89    kd_available_space: u64,
90    /// The initial cached size of the parent's node
91    parent_size: u64,
92}
93
94impl CacheKey {
95    #[inline(always)]
96    #[allow(dead_code)]
97    /// Return the parent size with the extra bits that encode the requested axis masked out
98    fn parent_size(&self) -> u64 {
99        self.parent_size & NON_SIGN_BITS_MASK
100    }
101
102    /// Return the parent size with the extra bits that encode the requested axis masked out
103    /// And the y-axis value masked out
104    fn x_axis_parent_size(&self) -> u64 {
105        self.parent_size & (X_AXIS_VALUE_MASK & NON_SIGN_BITS_MASK)
106    }
107}
108
109impl From<&LayoutInput> for CacheKey {
110    fn from(input: &LayoutInput) -> Self {
111        // Pack axis enum into spare bits in the known_dimensions and available_space values
112        let extra_bits = match input.axis {
113            RequestedAxis::Horizontal => SIGN_BIT_1,
114            RequestedAxis::Vertical => SIGN_BIT_2,
115            RequestedAxis::Both => SIGN_BIT_1 | SIGN_BIT_2,
116        };
117
118        Self {
119            kd_available_space: size_mixed_cache_key(input.known_dimensions, input.available_space),
120            parent_size: (size_option_cache_key(input.parent_size) & NON_SIGN_BITS_MASK) | extra_bits,
121        }
122    }
123}
124
125/// Cached intermediate layout results
126#[derive(Debug, Clone, Copy, PartialEq)]
127#[cfg_attr(feature = "serde", derive(Serialize))]
128pub(crate) struct CacheEntry<T> {
129    /// The key for the cache entry
130    key: CacheKey,
131    /// The cached size and baselines of the item
132    content: T,
133}
134
135/// A cache for caching the results of a sizing a Grid Item or Flexbox Item
136#[derive(Debug, Clone, PartialEq)]
137#[cfg_attr(feature = "serde", derive(Serialize))]
138pub struct Cache {
139    /// The cache entry for the node's final layout
140    final_layout_entry: Option<CacheEntry<LayoutOutput>>,
141    /// The cache entries for the node's preliminary size measurements
142    measure_entries: [Option<CacheEntry<Size<f32>>>; CACHE_SIZE],
143    /// Tracks if all cache entries are empty
144    is_empty: bool,
145}
146
147impl Default for Cache {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153impl Cache {
154    /// Create a new empty cache
155    pub const fn new() -> Self {
156        Self { final_layout_entry: None, measure_entries: [None; CACHE_SIZE], is_empty: true }
157    }
158
159    /// Return the cache slot to cache the current computed result in
160    ///
161    /// ## Caching Strategy
162    ///
163    /// We need multiple cache slots, because a node's size is often queried by it's parent multiple times in the course of the layout
164    /// process, and we don't want later results to clobber earlier ones.
165    ///
166    /// The two variables that we care about when determining cache slot are:
167    ///
168    ///   - How many "known_dimensions" are set. In the worst case, a node may be called first with neither dimension known, then with one
169    ///     dimension known (either width of height - which doesn't matter for our purposes here), and then with both dimensions known.
170    ///   - Whether unknown dimensions are being sized under a min-content or a max-content available space constraint (definite available space
171    ///     shares a cache slot with max-content because a node will generally be sized under one or the other but not both).
172    ///
173    /// ## Cache slots:
174    ///
175    /// - Slot 0: Both known_dimensions were set
176    /// - Slots 1-4: 1 of 2 known_dimensions were set and:
177    ///   - Slot 1: width but not height known_dimension was set and the other dimension was either a MaxContent or Definite available space constraintraint
178    ///   - Slot 2: width but not height known_dimension was set and the other dimension was a MinContent constraint
179    ///   - Slot 3: height but not width known_dimension was set and the other dimension was either a MaxContent or Definite available space constraintable space constraint
180    ///   - Slot 4: height but not width known_dimension was set and the other dimension was a MinContent constraint
181    /// - Slots 5-8: Neither known_dimensions were set and:
182    ///   - Slot 5: x-axis available space is MaxContent or Definite and y-axis available space is MaxContent or Definite
183    ///   - Slot 6: x-axis available space is MaxContent or Definite and y-axis available space is MinContent
184    ///   - Slot 7: x-axis available space is MinContent and y-axis available space is MaxContent or Definite
185    ///   - Slot 8: x-axis available space is MinContent and y-axis available space is MinContent
186    #[inline]
187    fn compute_cache_slot(known_dimensions: Size<Option<f32>>, available_space: Size<AvailableSpace>) -> usize {
188        use AvailableSpace::{Definite, MaxContent, MinContent};
189
190        let has_known_width = known_dimensions.width.is_some();
191        let has_known_height = known_dimensions.height.is_some();
192
193        // Slot 0: Both known_dimensions were set
194        if has_known_width && has_known_height {
195            return 0;
196        }
197
198        // Slot 1: width but not height known_dimension was set and the other dimension was either a MaxContent or Definite available space constraint
199        // Slot 2: width but not height known_dimension was set and the other dimension was a MinContent constraint
200        if has_known_width && !has_known_height {
201            return 1 + (available_space.height == MinContent) as usize;
202        }
203
204        // Slot 3: height but not width known_dimension was set and the other dimension was either a MaxContent or Definite available space constraint
205        // Slot 4: height but not width known_dimension was set and the other dimension was a MinContent constraint
206        if has_known_height && !has_known_width {
207            return 3 + (available_space.width == MinContent) as usize;
208        }
209
210        // Slots 5-8: Neither known_dimensions were set and:
211        match (available_space.width, available_space.height) {
212            // Slot 5: x-axis available space is MaxContent or Definite and y-axis available space is MaxContent or Definite
213            (MaxContent | Definite(_), MaxContent | Definite(_)) => 5,
214            // Slot 6: x-axis available space is MaxContent or Definite and y-axis available space is MinContent
215            (MaxContent | Definite(_), MinContent) => 6,
216            // Slot 7: x-axis available space is MinContent and y-axis available space is MaxContent or Definite
217            (MinContent, MaxContent | Definite(_)) => 7,
218            // Slot 8: x-axis available space is MinContent and y-axis available space is MinContent
219            (MinContent, MinContent) => 8,
220        }
221    }
222
223    /// Try to retrieve a cached result from the cache
224    #[inline]
225    pub fn get(&self, input: &LayoutInput) -> Option<LayoutOutput> {
226        let key = CacheKey::from(input);
227        match input.run_mode {
228            RunMode::PerformLayout => self.final_layout_entry.filter(|entry| entry.key == key).map(|e| e.content),
229            RunMode::ComputeSize => {
230                for entry in self.measure_entries.iter().flatten() {
231                    if entry.key.kd_available_space == key.kd_available_space
232                        && (entry.key.x_axis_parent_size() == key.x_axis_parent_size())
233                    {
234                        return Some(LayoutOutput::from_outer_size(entry.content));
235                    }
236                }
237
238                None
239            }
240            RunMode::PerformHiddenLayout => None,
241        }
242    }
243
244    /// Store a computed size in the cache
245    pub fn store(&mut self, input: &LayoutInput, layout_output: LayoutOutput) {
246        let key = CacheKey::from(input);
247        match input.run_mode {
248            RunMode::PerformLayout => {
249                self.is_empty = false;
250                self.final_layout_entry = Some(CacheEntry { key, content: layout_output })
251            }
252            RunMode::ComputeSize => {
253                self.is_empty = false;
254                let cache_slot = Self::compute_cache_slot(input.known_dimensions, input.available_space);
255                self.measure_entries[cache_slot] = Some(CacheEntry { key, content: layout_output.size });
256            }
257            RunMode::PerformHiddenLayout => {}
258        }
259    }
260
261    /// Clear all cache entries and reports clear operation outcome ([`ClearState`])
262    pub fn clear(&mut self) -> ClearState {
263        if self.is_empty {
264            return ClearState::AlreadyEmpty;
265        }
266        self.is_empty = true;
267        self.final_layout_entry = None;
268        self.measure_entries = [None; CACHE_SIZE];
269        ClearState::Cleared
270    }
271
272    /// Returns true if all cache entries are None, else false
273    pub fn is_empty(&self) -> bool {
274        self.final_layout_entry.is_none() && !self.measure_entries.iter().any(|entry| entry.is_some())
275    }
276}
277
278/// Clear operation outcome. See [`Cache::clear`]
279pub enum ClearState {
280    /// Cleared some values
281    Cleared,
282    /// Everything was already cleared
283    AlreadyEmpty,
284}