#![allow(clippy::unusual_byte_groupings)]
use crate::geometry::Size;
use crate::style::AvailableSpace;
use crate::tree::{CollapsibleMarginSet, LayoutInput, LayoutOutput, RunMode, SizingMode};
use crate::RequestedAxis;
const CACHE_SIZE: usize = 9;
const INFINITY_BITS: u32 = 0b_0_11111111_00000000000000000000000_u32;
const NEG_INFINITY_BITS: u32 = 0b_1_11111111_00000000000000000000000_u32;
const SIGN_BIT_1: u64 = 1u64 << 63;
const SIGN_BIT_2: u64 = 1u64 << 31;
const BOTH_SIGN_BITS_MASK: u64 = SIGN_BIT_1 | SIGN_BIT_2;
const NON_SIGN_BITS_MASK: u64 = !BOTH_SIGN_BITS_MASK;
const X_AXIS_VALUE_MASK: u64 = (u32::MAX as u64) << 32;
#[inline(always)]
fn option_cache_key(input: Option<f32>) -> u32 {
match input {
Some(value) => value.to_bits(),
None => INFINITY_BITS,
}
}
#[inline(always)]
fn size_option_cache_key(input: Size<Option<f32>>) -> u64 {
(option_cache_key(input.width) as u64) << 32 | option_cache_key(input.height) as u64
}
#[inline(always)]
fn available_space_cache_key(input: AvailableSpace) -> u32 {
match input {
AvailableSpace::Definite(value) => (-value).to_bits(),
AvailableSpace::MinContent => NEG_INFINITY_BITS,
AvailableSpace::MaxContent => INFINITY_BITS,
}
}
#[inline(always)]
#[allow(dead_code)]
fn size_available_space_cache_key(input: Size<AvailableSpace>) -> u64 {
(available_space_cache_key(input.width) as u64) << 32 | available_space_cache_key(input.height) as u64
}
#[inline(always)]
fn mixed_cache_key(kd: Option<f32>, avs: AvailableSpace) -> u32 {
kd.map(|kd| kd.to_bits()).unwrap_or_else(|| available_space_cache_key(avs))
}
#[inline(always)]
fn size_mixed_cache_key(kd: Size<Option<f32>>, avs: Size<AvailableSpace>) -> u64 {
(mixed_cache_key(kd.width, avs.width) as u64) << 32 | mixed_cache_key(kd.height, avs.height) as u64
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
struct CacheKey {
kd_available_space: u64,
parent_size: u64,
known_dimensions_are_definite: Size<bool>,
}
impl CacheKey {
#[inline(always)]
#[allow(dead_code)]
fn parent_size(&self) -> u64 {
self.parent_size & NON_SIGN_BITS_MASK
}
fn x_axis_parent_size(&self) -> u64 {
self.parent_size & (X_AXIS_VALUE_MASK & NON_SIGN_BITS_MASK)
}
fn requested_axis_bits(&self) -> u64 {
self.parent_size & BOTH_SIGN_BITS_MASK
}
fn size_is_valid_for(&self, other: &CacheKey) -> bool {
let entry_axis = self.requested_axis_bits();
entry_axis == BOTH_SIGN_BITS_MASK || entry_axis == other.requested_axis_bits()
}
}
impl From<&LayoutInput> for CacheKey {
fn from(input: &LayoutInput) -> Self {
let extra_bits = match input.axis {
RequestedAxis::Horizontal => SIGN_BIT_1,
RequestedAxis::Vertical => SIGN_BIT_2,
RequestedAxis::Both => SIGN_BIT_1 | SIGN_BIT_2,
};
Self {
kd_available_space: size_mixed_cache_key(input.known_dimensions, input.available_space),
parent_size: (size_option_cache_key(input.parent_size) & NON_SIGN_BITS_MASK) | extra_bits,
known_dimensions_are_definite: input
.known_dimensions_are_definite
.zip_map(input.known_dimensions, |is_definite, kd| is_definite || kd.is_none()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
struct CacheMode(u8);
impl CacheMode {
const INHERENT_SIZE_BIT: u8 = 0b001;
const COLLAPSIBLE_START_MARGIN_BIT: u8 = 0b010;
const COLLAPSIBLE_END_MARGIN_BIT: u8 = 0b100;
const INITIAL: Self = Self(0);
}
impl From<&LayoutInput> for CacheMode {
#[inline(always)]
fn from(input: &LayoutInput) -> Self {
let inherent_size = matches!(input.sizing_mode, SizingMode::InherentSize) as u8;
let collapsible = input.vertical_margins_are_collapsible;
Self(
(inherent_size * Self::INHERENT_SIZE_BIT)
| (collapsible.start as u8 * Self::COLLAPSIBLE_START_MARGIN_BIT)
| (collapsible.end as u8 * Self::COLLAPSIBLE_END_MARGIN_BIT),
)
}
}
#[cfg(all(debug_assertions, feature = "std"))]
std::thread_local! {
static MODE_CHANGE_EVICTIONS: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
#[doc(hidden)]
#[cfg(all(debug_assertions, feature = "std"))]
pub fn cache_mode_change_evictions() -> usize {
MODE_CHANGE_EVICTIONS.with(|count| count.get())
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub(crate) struct CacheEntry<T> {
key: CacheKey,
content: T,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Cache {
final_layout_entry: Option<CacheEntry<LayoutOutput>>,
measure_entries: [Option<CacheEntry<Size<f32>>>; CACHE_SIZE],
recently_used_entries: u16,
next_measure_entry: u8,
is_empty: bool,
mode: CacheMode,
}
impl Default for Cache {
fn default() -> Self {
Self::new()
}
}
impl Cache {
pub const fn new() -> Self {
const NONE_MEASURE_ENTRY: Option<CacheEntry<Size<f32>>> = None;
Self {
final_layout_entry: None,
measure_entries: [NONE_MEASURE_ENTRY; CACHE_SIZE],
recently_used_entries: 0,
next_measure_entry: 0,
is_empty: true,
mode: CacheMode::INITIAL,
}
}
#[inline]
pub fn get(&mut self, input: &LayoutInput) -> Option<LayoutOutput> {
if self.mode != CacheMode::from(input) {
return None;
}
let key = CacheKey::from(input);
match input.run_mode {
RunMode::PerformLayout => {
self.final_layout_entry.as_ref().filter(|entry| entry.key == key).map(|e| e.content.clone())
}
RunMode::ComputeSize => {
for (index, entry) in self.measure_entries.iter().enumerate() {
let Some(entry) = entry else { continue };
if entry.key.kd_available_space == key.kd_available_space
&& entry.key.known_dimensions_are_definite == key.known_dimensions_are_definite
&& (entry.key.x_axis_parent_size() == key.x_axis_parent_size())
&& entry.key.size_is_valid_for(&key)
{
self.recently_used_entries |= 1 << index;
return Some(LayoutOutput::from_outer_size(entry.content));
}
}
None
}
RunMode::PerformHiddenLayout => None,
}
}
pub fn store(&mut self, input: &LayoutInput, layout_output: LayoutOutput) {
if input.run_mode == RunMode::PerformHiddenLayout {
return;
}
let mode = CacheMode::from(input);
if self.mode != mode {
#[cfg(all(debug_assertions, feature = "std"))]
if !self.is_empty {
MODE_CHANGE_EVICTIONS.with(|count| count.set(count.get() + 1));
}
self.clear();
self.mode = mode;
}
let key = CacheKey::from(input);
match input.run_mode {
RunMode::PerformLayout => {
self.is_empty = false;
self.final_layout_entry = Some(CacheEntry { key, content: layout_output })
}
RunMode::ComputeSize => {
if layout_output.margins_can_collapse_through
|| layout_output.top_margin != CollapsibleMarginSet::ZERO
|| layout_output.bottom_margin != CollapsibleMarginSet::ZERO
{
return;
}
self.is_empty = false;
if let Some(index) =
self.measure_entries.iter().position(|entry| entry.as_ref().is_some_and(|entry| entry.key == key))
{
self.measure_entries[index].as_mut().unwrap().content = layout_output.size;
self.recently_used_entries |= 1 << index;
return;
}
while self.recently_used_entries & (1 << self.next_measure_entry) != 0 {
self.recently_used_entries &= !(1 << self.next_measure_entry);
self.next_measure_entry += 1;
if self.next_measure_entry == CACHE_SIZE as u8 {
self.next_measure_entry = 0;
}
}
let entry_index = self.next_measure_entry as usize;
self.measure_entries[entry_index] = Some(CacheEntry { key, content: layout_output.size });
self.recently_used_entries |= 1 << entry_index;
self.next_measure_entry += 1;
if self.next_measure_entry == CACHE_SIZE as u8 {
self.next_measure_entry = 0;
}
}
RunMode::PerformHiddenLayout => {}
}
}
pub fn clear(&mut self) -> ClearState {
if self.is_empty {
return ClearState::AlreadyEmpty;
}
self.is_empty = true;
self.final_layout_entry = None;
const NONE_MEASURE_ENTRY: Option<CacheEntry<Size<f32>>> = None;
self.measure_entries = [NONE_MEASURE_ENTRY; CACHE_SIZE];
self.recently_used_entries = 0;
self.next_measure_entry = 0;
ClearState::Cleared
}
pub fn is_empty(&self) -> bool {
self.final_layout_entry.is_none() && !self.measure_entries.iter().any(|entry| entry.is_some())
}
}
pub enum ClearState {
Cleared,
AlreadyEmpty,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Line;
use crate::tree::SizingMode;
fn input(width: f32) -> LayoutInput {
LayoutInput {
run_mode: RunMode::ComputeSize,
sizing_mode: SizingMode::InherentSize,
axis: RequestedAxis::Both,
known_dimensions: Size { width: Some(width), height: None },
known_dimensions_are_definite: Size { width: true, height: true },
parent_size: Size::NONE,
available_space: Size { width: AvailableSpace::MaxContent, height: AvailableSpace::MaxContent },
vertical_margins_are_collapsible: Line::FALSE,
}
}
fn output(width: f32) -> LayoutOutput {
LayoutOutput::from_outer_size(Size { width, height: width })
}
#[test]
fn recently_used_measure_entries_get_a_second_chance() {
let mut cache = Cache::new();
for width in 0..CACHE_SIZE {
cache.store(&input(width as f32), output(width as f32));
}
cache.store(&input(CACHE_SIZE as f32), output(CACHE_SIZE as f32));
assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
cache.store(&input((CACHE_SIZE + 1) as f32), output((CACHE_SIZE + 1) as f32));
assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
assert_eq!(cache.get(&input(2.0)), None);
}
#[test]
fn storing_an_existing_measurement_updates_it_in_place() {
let mut cache = Cache::new();
cache.store(&input(1.0), output(1.0));
cache.store(&input(2.0), output(2.0));
cache.store(&input(1.0), output(3.0));
assert_eq!(cache.measure_entries.iter().flatten().count(), 2);
assert_eq!(cache.get(&input(1.0)), Some(output(3.0)));
}
#[test]
fn measurements_with_margin_collapse_metadata_are_not_cached() {
let mut cache = Cache::new();
let mut collapse_through = output(1.0);
collapse_through.margins_can_collapse_through = true;
cache.store(&input(1.0), collapse_through);
assert_eq!(cache.get(&input(1.0)), None);
let mut carried_margin = output(2.0);
carried_margin.top_margin = CollapsibleMarginSet::from_margin(10.0);
cache.store(&input(2.0), carried_margin);
assert_eq!(cache.get(&input(2.0)), None);
}
#[test]
fn retrieving_a_measurement_only_marks_its_slot_as_used() {
let mut cache = Cache::new();
cache.store(&input(1.0), output(1.0));
cache.store(&input(2.0), output(2.0));
cache.recently_used_entries = 0;
let entries = cache.measure_entries.clone();
assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
assert_eq!(cache.measure_entries, entries);
assert_ne!(cache.recently_used_entries, 0);
}
fn perform_layout_input(width: f32, height: f32) -> LayoutInput {
LayoutInput {
run_mode: RunMode::PerformLayout,
known_dimensions: Size { width: Some(width), height: Some(height) },
parent_size: Size { width: Some(width), height: Some(height) },
..input(width)
}
}
fn other_modes(input: LayoutInput) -> [LayoutInput; 4] {
[
LayoutInput { sizing_mode: SizingMode::ContentSize, ..input },
LayoutInput { vertical_margins_are_collapsible: Line::TRUE, ..input },
LayoutInput { vertical_margins_are_collapsible: Line { start: true, end: false }, ..input },
LayoutInput { vertical_margins_are_collapsible: Line { start: false, end: true }, ..input },
]
}
#[test]
fn cache_mode_distinguishes_every_combination_of_inputs() {
let mut modes = [CacheMode::INITIAL; 8];
for (index, mode) in modes.iter_mut().enumerate() {
*mode = CacheMode::from(&LayoutInput {
sizing_mode: if index & 1 == 0 { SizingMode::ContentSize } else { SizingMode::InherentSize },
vertical_margins_are_collapsible: Line { start: index & 2 != 0, end: index & 4 != 0 },
..input(1.0)
});
}
for (index, mode) in modes.iter().enumerate() {
assert!(!modes[..index].contains(mode));
}
}
#[test]
fn retrieving_with_a_different_mode_misses_and_keeps_the_entries() {
let mut cache = Cache::new();
cache.store(&perform_layout_input(100.0, 50.0), output(1.0));
cache.store(&input(1.0), output(2.0));
for other in other_modes(perform_layout_input(100.0, 50.0)) {
assert_eq!(cache.get(&other), None);
}
for other in other_modes(input(1.0)) {
assert_eq!(cache.get(&other), None);
}
assert_eq!(cache.get(&perform_layout_input(100.0, 50.0)), Some(output(1.0)));
assert_eq!(cache.get(&input(1.0)), Some(output(2.0)));
}
#[test]
fn storing_with_a_different_mode_drops_the_existing_entries() {
for other in other_modes(input(3.0)) {
let mut cache = Cache::new();
cache.store(&perform_layout_input(100.0, 50.0), output(1.0));
cache.store(&input(1.0), output(2.0));
cache.store(&other, output(3.0));
assert_eq!(cache.get(&other), Some(output(3.0)));
assert!(cache.final_layout_entry.is_none());
assert_eq!(cache.measure_entries.iter().flatten().count(), 1);
cache.store(&input(4.0), output(4.0));
assert_eq!(cache.get(&perform_layout_input(100.0, 50.0)), None);
assert_eq!(cache.get(&input(1.0)), None);
assert_eq!(cache.get(&other), None);
assert_eq!(cache.get(&input(4.0)), Some(output(4.0)));
}
for other in other_modes(perform_layout_input(100.0, 50.0)) {
let mut cache = Cache::new();
cache.store(&perform_layout_input(100.0, 50.0), output(1.0));
cache.store(&input(1.0), output(2.0));
cache.store(&other, output(3.0));
assert_eq!(cache.get(&other), Some(output(3.0)));
assert!(cache.measure_entries.iter().all(|entry| entry.is_none()));
assert_eq!(cache.get(&perform_layout_input(100.0, 50.0)), None);
}
}
#[test]
fn hidden_layouts_do_not_affect_the_cache() {
let mut cache = Cache::new();
cache.store(&input(1.0), output(1.0));
cache.store(&LayoutInput::HIDDEN, output(2.0));
assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
}
#[test]
#[cfg(all(debug_assertions, feature = "std"))]
fn mode_change_evictions_are_counted_for_non_empty_caches_only() {
let content_size = LayoutInput { sizing_mode: SizingMode::ContentSize, ..input(1.0) };
let evictions = cache_mode_change_evictions();
let mut cache = Cache::new();
cache.store(&content_size, output(1.0));
cache.store(&LayoutInput { sizing_mode: SizingMode::ContentSize, ..input(2.0) }, output(2.0));
assert_eq!(cache_mode_change_evictions(), evictions);
cache.store(&input(1.0), output(1.0));
assert_eq!(cache_mode_change_evictions(), evictions + 1);
cache.clear();
cache.store(&content_size, output(1.0));
assert_eq!(cache_mode_change_evictions(), evictions + 1);
}
}