1#![allow(clippy::unusual_byte_groupings)]
4
5use crate::geometry::Size;
6use crate::style::AvailableSpace;
7use crate::tree::{CollapsibleMarginSet, LayoutInput, LayoutOutput, RunMode, SizingMode};
8use crate::RequestedAxis;
9
10const CACHE_SIZE: usize = 9;
12
13const INFINITY_BITS: u32 = 0b_0_11111111_00000000000000000000000_u32;
18const NEG_INFINITY_BITS: u32 = 0b_1_11111111_00000000000000000000000_u32;
20
21const SIGN_BIT_1: u64 = 1u64 << 63;
27const SIGN_BIT_2: u64 = 1u64 << 31;
29const BOTH_SIGN_BITS_MASK: u64 = SIGN_BIT_1 | SIGN_BIT_2;
31const NON_SIGN_BITS_MASK: u64 = !BOTH_SIGN_BITS_MASK;
33
34const X_AXIS_VALUE_MASK: u64 = (u32::MAX as u64) << 32;
37
38#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[cfg_attr(feature = "serde", derive(Serialize))]
87struct CacheKey {
88 kd_available_space: u64,
90 parent_size: u64,
92 known_dimensions_are_definite: Size<bool>,
95}
96
97impl CacheKey {
98 #[inline(always)]
99 #[allow(dead_code)]
100 fn parent_size(&self) -> u64 {
102 self.parent_size & NON_SIGN_BITS_MASK
103 }
104
105 fn x_axis_parent_size(&self) -> u64 {
108 self.parent_size & (X_AXIS_VALUE_MASK & NON_SIGN_BITS_MASK)
109 }
110
111 fn requested_axis_bits(&self) -> u64 {
113 self.parent_size & BOTH_SIGN_BITS_MASK
114 }
115
116 fn size_is_valid_for(&self, other: &CacheKey) -> bool {
120 let entry_axis = self.requested_axis_bits();
121 entry_axis == BOTH_SIGN_BITS_MASK || entry_axis == other.requested_axis_bits()
122 }
123}
124
125impl From<&LayoutInput> for CacheKey {
126 fn from(input: &LayoutInput) -> Self {
127 let extra_bits = match input.axis {
129 RequestedAxis::Horizontal => SIGN_BIT_1,
130 RequestedAxis::Vertical => SIGN_BIT_2,
131 RequestedAxis::Both => SIGN_BIT_1 | SIGN_BIT_2,
132 };
133
134 Self {
135 kd_available_space: size_mixed_cache_key(input.known_dimensions, input.available_space),
136 parent_size: (size_option_cache_key(input.parent_size) & NON_SIGN_BITS_MASK) | extra_bits,
137 known_dimensions_are_definite: input
138 .known_dimensions_are_definite
139 .zip_map(input.known_dimensions, |is_definite, kd| is_definite || kd.is_none()),
140 }
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151#[cfg_attr(feature = "serde", derive(Serialize))]
152struct CacheMode(u8);
153
154impl CacheMode {
155 const INHERENT_SIZE_BIT: u8 = 0b001;
157 const COLLAPSIBLE_START_MARGIN_BIT: u8 = 0b010;
159 const COLLAPSIBLE_END_MARGIN_BIT: u8 = 0b100;
161
162 const INITIAL: Self = Self(0);
165}
166
167impl From<&LayoutInput> for CacheMode {
168 #[inline(always)]
169 fn from(input: &LayoutInput) -> Self {
170 let inherent_size = matches!(input.sizing_mode, SizingMode::InherentSize) as u8;
171 let collapsible = input.vertical_margins_are_collapsible;
172 Self(
173 (inherent_size * Self::INHERENT_SIZE_BIT)
174 | (collapsible.start as u8 * Self::COLLAPSIBLE_START_MARGIN_BIT)
175 | (collapsible.end as u8 * Self::COLLAPSIBLE_END_MARGIN_BIT),
176 )
177 }
178}
179
180#[cfg(all(debug_assertions, feature = "std"))]
181std::thread_local! {
182 static MODE_CHANGE_EVICTIONS: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
184}
185
186#[doc(hidden)]
196#[cfg(all(debug_assertions, feature = "std"))]
197pub fn cache_mode_change_evictions() -> usize {
198 MODE_CHANGE_EVICTIONS.with(|count| count.get())
199}
200
201#[derive(Debug, Clone, PartialEq)]
203#[cfg_attr(feature = "serde", derive(Serialize))]
204pub(crate) struct CacheEntry<T> {
205 key: CacheKey,
207 content: T,
209}
210
211#[derive(Debug, Clone, PartialEq)]
213#[cfg_attr(feature = "serde", derive(Serialize))]
214pub struct Cache {
215 final_layout_entry: Option<CacheEntry<LayoutOutput>>,
217 measure_entries: [Option<CacheEntry<Size<f32>>>; CACHE_SIZE],
219 recently_used_entries: u16,
221 next_measure_entry: u8,
223 is_empty: bool,
225 mode: CacheMode,
229}
230
231impl Default for Cache {
232 fn default() -> Self {
233 Self::new()
234 }
235}
236
237impl Cache {
238 pub const fn new() -> Self {
240 const NONE_MEASURE_ENTRY: Option<CacheEntry<Size<f32>>> = None;
242 Self {
243 final_layout_entry: None,
244 measure_entries: [NONE_MEASURE_ENTRY; CACHE_SIZE],
245 recently_used_entries: 0,
246 next_measure_entry: 0,
247 is_empty: true,
248 mode: CacheMode::INITIAL,
249 }
250 }
251
252 #[inline]
254 pub fn get(&mut self, input: &LayoutInput) -> Option<LayoutOutput> {
255 if self.mode != CacheMode::from(input) {
256 return None;
257 }
258 let key = CacheKey::from(input);
259 match input.run_mode {
260 RunMode::PerformLayout => {
261 self.final_layout_entry.as_ref().filter(|entry| entry.key == key).map(|e| e.content.clone())
262 }
263 RunMode::ComputeSize => {
264 for (index, entry) in self.measure_entries.iter().enumerate() {
265 let Some(entry) = entry else { continue };
266 if entry.key.kd_available_space == key.kd_available_space
267 && entry.key.known_dimensions_are_definite == key.known_dimensions_are_definite
268 && (entry.key.x_axis_parent_size() == key.x_axis_parent_size())
269 && entry.key.size_is_valid_for(&key)
270 {
271 self.recently_used_entries |= 1 << index;
272 return Some(LayoutOutput::from_outer_size(entry.content));
273 }
274 }
275
276 None
277 }
278 RunMode::PerformHiddenLayout => None,
279 }
280 }
281
282 pub fn store(&mut self, input: &LayoutInput, layout_output: LayoutOutput) {
284 if input.run_mode == RunMode::PerformHiddenLayout {
285 return;
286 }
287 let mode = CacheMode::from(input);
288 if self.mode != mode {
289 #[cfg(all(debug_assertions, feature = "std"))]
290 if !self.is_empty {
291 MODE_CHANGE_EVICTIONS.with(|count| count.set(count.get() + 1));
292 }
293 self.clear();
294 self.mode = mode;
295 }
296 let key = CacheKey::from(input);
297 match input.run_mode {
298 RunMode::PerformLayout => {
299 self.is_empty = false;
300 self.final_layout_entry = Some(CacheEntry { key, content: layout_output })
301 }
302 RunMode::ComputeSize => {
303 if layout_output.margins_can_collapse_through
308 || layout_output.top_margin != CollapsibleMarginSet::ZERO
309 || layout_output.bottom_margin != CollapsibleMarginSet::ZERO
310 {
311 return;
312 }
313 self.is_empty = false;
314 if let Some(index) =
315 self.measure_entries.iter().position(|entry| entry.as_ref().is_some_and(|entry| entry.key == key))
316 {
317 self.measure_entries[index].as_mut().unwrap().content = layout_output.size;
318 self.recently_used_entries |= 1 << index;
319 return;
320 }
321 while self.recently_used_entries & (1 << self.next_measure_entry) != 0 {
322 self.recently_used_entries &= !(1 << self.next_measure_entry);
323 self.next_measure_entry += 1;
324 if self.next_measure_entry == CACHE_SIZE as u8 {
325 self.next_measure_entry = 0;
326 }
327 }
328 let entry_index = self.next_measure_entry as usize;
329 self.measure_entries[entry_index] = Some(CacheEntry { key, content: layout_output.size });
330 self.recently_used_entries |= 1 << entry_index;
331 self.next_measure_entry += 1;
332 if self.next_measure_entry == CACHE_SIZE as u8 {
333 self.next_measure_entry = 0;
334 }
335 }
336 RunMode::PerformHiddenLayout => {}
337 }
338 }
339
340 pub fn clear(&mut self) -> ClearState {
342 if self.is_empty {
343 return ClearState::AlreadyEmpty;
344 }
345 self.is_empty = true;
346 self.final_layout_entry = None;
347 const NONE_MEASURE_ENTRY: Option<CacheEntry<Size<f32>>> = None;
349 self.measure_entries = [NONE_MEASURE_ENTRY; CACHE_SIZE];
350 self.recently_used_entries = 0;
351 self.next_measure_entry = 0;
352 ClearState::Cleared
353 }
354
355 pub fn is_empty(&self) -> bool {
357 self.final_layout_entry.is_none() && !self.measure_entries.iter().any(|entry| entry.is_some())
358 }
359}
360
361pub enum ClearState {
363 Cleared,
365 AlreadyEmpty,
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::geometry::Line;
373 use crate::tree::SizingMode;
374
375 fn input(width: f32) -> LayoutInput {
376 LayoutInput {
377 run_mode: RunMode::ComputeSize,
378 sizing_mode: SizingMode::InherentSize,
379 axis: RequestedAxis::Both,
380 known_dimensions: Size { width: Some(width), height: None },
381 known_dimensions_are_definite: Size { width: true, height: true },
382 parent_size: Size::NONE,
383 available_space: Size { width: AvailableSpace::MaxContent, height: AvailableSpace::MaxContent },
384 vertical_margins_are_collapsible: Line::FALSE,
385 }
386 }
387
388 fn output(width: f32) -> LayoutOutput {
389 LayoutOutput::from_outer_size(Size { width, height: width })
390 }
391
392 #[test]
393 fn recently_used_measure_entries_get_a_second_chance() {
394 let mut cache = Cache::new();
395 for width in 0..CACHE_SIZE {
396 cache.store(&input(width as f32), output(width as f32));
397 }
398 cache.store(&input(CACHE_SIZE as f32), output(CACHE_SIZE as f32));
399
400 assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
401 cache.store(&input((CACHE_SIZE + 1) as f32), output((CACHE_SIZE + 1) as f32));
402
403 assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
404 assert_eq!(cache.get(&input(2.0)), None);
405 }
406
407 #[test]
408 fn storing_an_existing_measurement_updates_it_in_place() {
409 let mut cache = Cache::new();
410 cache.store(&input(1.0), output(1.0));
411 cache.store(&input(2.0), output(2.0));
412 cache.store(&input(1.0), output(3.0));
413
414 assert_eq!(cache.measure_entries.iter().flatten().count(), 2);
415 assert_eq!(cache.get(&input(1.0)), Some(output(3.0)));
416 }
417
418 #[test]
419 fn measurements_with_margin_collapse_metadata_are_not_cached() {
420 let mut cache = Cache::new();
421
422 let mut collapse_through = output(1.0);
423 collapse_through.margins_can_collapse_through = true;
424 cache.store(&input(1.0), collapse_through);
425 assert_eq!(cache.get(&input(1.0)), None);
426
427 let mut carried_margin = output(2.0);
428 carried_margin.top_margin = CollapsibleMarginSet::from_margin(10.0);
429 cache.store(&input(2.0), carried_margin);
430 assert_eq!(cache.get(&input(2.0)), None);
431 }
432
433 #[test]
434 fn retrieving_a_measurement_only_marks_its_slot_as_used() {
435 let mut cache = Cache::new();
436 cache.store(&input(1.0), output(1.0));
437 cache.store(&input(2.0), output(2.0));
438 cache.recently_used_entries = 0;
439 let entries = cache.measure_entries.clone();
440
441 assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
442 assert_eq!(cache.measure_entries, entries);
443 assert_ne!(cache.recently_used_entries, 0);
444 }
445
446 fn perform_layout_input(width: f32, height: f32) -> LayoutInput {
447 LayoutInput {
448 run_mode: RunMode::PerformLayout,
449 known_dimensions: Size { width: Some(width), height: Some(height) },
450 parent_size: Size { width: Some(width), height: Some(height) },
451 ..input(width)
452 }
453 }
454
455 fn other_modes(input: LayoutInput) -> [LayoutInput; 4] {
457 [
458 LayoutInput { sizing_mode: SizingMode::ContentSize, ..input },
459 LayoutInput { vertical_margins_are_collapsible: Line::TRUE, ..input },
460 LayoutInput { vertical_margins_are_collapsible: Line { start: true, end: false }, ..input },
461 LayoutInput { vertical_margins_are_collapsible: Line { start: false, end: true }, ..input },
462 ]
463 }
464
465 #[test]
466 fn cache_mode_distinguishes_every_combination_of_inputs() {
467 let mut modes = [CacheMode::INITIAL; 8];
468 for (index, mode) in modes.iter_mut().enumerate() {
469 *mode = CacheMode::from(&LayoutInput {
470 sizing_mode: if index & 1 == 0 { SizingMode::ContentSize } else { SizingMode::InherentSize },
471 vertical_margins_are_collapsible: Line { start: index & 2 != 0, end: index & 4 != 0 },
472 ..input(1.0)
473 });
474 }
475 for (index, mode) in modes.iter().enumerate() {
476 assert!(!modes[..index].contains(mode));
477 }
478 }
479
480 #[test]
481 fn retrieving_with_a_different_mode_misses_and_keeps_the_entries() {
482 let mut cache = Cache::new();
483 cache.store(&perform_layout_input(100.0, 50.0), output(1.0));
484 cache.store(&input(1.0), output(2.0));
485
486 for other in other_modes(perform_layout_input(100.0, 50.0)) {
487 assert_eq!(cache.get(&other), None);
488 }
489 for other in other_modes(input(1.0)) {
490 assert_eq!(cache.get(&other), None);
491 }
492
493 assert_eq!(cache.get(&perform_layout_input(100.0, 50.0)), Some(output(1.0)));
494 assert_eq!(cache.get(&input(1.0)), Some(output(2.0)));
495 }
496
497 #[test]
498 fn storing_with_a_different_mode_drops_the_existing_entries() {
499 for other in other_modes(input(3.0)) {
500 let mut cache = Cache::new();
501 cache.store(&perform_layout_input(100.0, 50.0), output(1.0));
502 cache.store(&input(1.0), output(2.0));
503
504 cache.store(&other, output(3.0));
505 assert_eq!(cache.get(&other), Some(output(3.0)));
506 assert!(cache.final_layout_entry.is_none());
507 assert_eq!(cache.measure_entries.iter().flatten().count(), 1);
508
509 cache.store(&input(4.0), output(4.0));
511 assert_eq!(cache.get(&perform_layout_input(100.0, 50.0)), None);
512 assert_eq!(cache.get(&input(1.0)), None);
513 assert_eq!(cache.get(&other), None);
514 assert_eq!(cache.get(&input(4.0)), Some(output(4.0)));
515 }
516
517 for other in other_modes(perform_layout_input(100.0, 50.0)) {
518 let mut cache = Cache::new();
519 cache.store(&perform_layout_input(100.0, 50.0), output(1.0));
520 cache.store(&input(1.0), output(2.0));
521
522 cache.store(&other, output(3.0));
523 assert_eq!(cache.get(&other), Some(output(3.0)));
524 assert!(cache.measure_entries.iter().all(|entry| entry.is_none()));
525 assert_eq!(cache.get(&perform_layout_input(100.0, 50.0)), None);
526 }
527 }
528
529 #[test]
530 fn hidden_layouts_do_not_affect_the_cache() {
531 let mut cache = Cache::new();
532 cache.store(&input(1.0), output(1.0));
533 cache.store(&LayoutInput::HIDDEN, output(2.0));
534 assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
535 }
536
537 #[test]
538 #[cfg(all(debug_assertions, feature = "std"))]
539 fn mode_change_evictions_are_counted_for_non_empty_caches_only() {
540 let content_size = LayoutInput { sizing_mode: SizingMode::ContentSize, ..input(1.0) };
541 let evictions = cache_mode_change_evictions();
542
543 let mut cache = Cache::new();
544 cache.store(&content_size, output(1.0));
545 cache.store(&LayoutInput { sizing_mode: SizingMode::ContentSize, ..input(2.0) }, output(2.0));
546 assert_eq!(cache_mode_change_evictions(), evictions);
547
548 cache.store(&input(1.0), output(1.0));
549 assert_eq!(cache_mode_change_evictions(), evictions + 1);
550
551 cache.clear();
552 cache.store(&content_size, output(1.0));
553 assert_eq!(cache_mode_change_evictions(), evictions + 1);
554 }
555}