#![allow(clippy::unusual_byte_groupings)]
use core::sync::atomic::{AtomicU64, Ordering};
use crate::geometry::Size;
use crate::style::AvailableSpace;
use crate::tree::{LayoutInput, LayoutOutput, RunMode};
use crate::RequestedAxis;
static EVICTIONS: AtomicU64 = AtomicU64::new(0);
static STORES: AtomicU64 = AtomicU64::new(0);
pub fn eviction_counts() -> (u64, u64) {
(EVICTIONS.load(Ordering::Relaxed), STORES.load(Ordering::Relaxed))
}
const CACHE_SIZE: usize = 24;
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,
}
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)
}
}
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,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub(crate) struct CacheEntry<T> {
key: CacheKey,
content: T,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub(crate) struct MeasureInputs {
known_dimensions: Size<Option<f32>>,
available_space: Size<AvailableSpace>,
}
#[inline]
fn axis_still_valid(
stored_known: Option<f32>,
new_known: Option<f32>,
stored_space: AvailableSpace,
new_space: AvailableSpace,
measured: f32,
) -> bool {
use AvailableSpace::{Definite, MaxContent, MinContent};
match (stored_known, new_known) {
(Some(stored), Some(new)) => return stored == new,
(None, None) => {}
(None, Some(new)) => {
return matches!(stored_space, Definite(stored) if stored == new) && measured <= new;
}
(Some(_), None) => return false,
}
match (stored_space, new_space) {
(MinContent, MinContent) | (MaxContent, MaxContent) => true,
(Definite(stored), Definite(new)) => {
stored == new || (measured <= stored && measured <= new)
}
(MaxContent, Definite(new)) => measured <= new,
(Definite(_), MaxContent) => false,
(MinContent, _) | (_, MinContent) => false,
}
}
#[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],
measure_inputs: [Option<MeasureInputs>; CACHE_SIZE],
is_empty: bool,
next_eviction: u8,
}
impl Default for Cache {
fn default() -> Self {
Self::new()
}
}
impl Cache {
pub const fn new() -> Self {
Self {
final_layout_entry: None,
measure_entries: [None; CACHE_SIZE],
measure_inputs: [None; CACHE_SIZE],
is_empty: true,
next_eviction: 0,
}
}
#[inline]
fn slot_for(&mut self, key: &CacheKey) -> usize {
for (index, entry) in self.measure_entries.iter().enumerate() {
match entry {
Some(entry) if entry.key == *key => return index,
_ => {}
}
}
if let Some(index) = self.measure_entries.iter().position(Option::is_none) {
return index;
}
EVICTIONS.fetch_add(1, Ordering::Relaxed);
let index = self.next_eviction as usize;
self.next_eviction = (self.next_eviction + 1) % CACHE_SIZE as u8;
index
}
#[inline]
pub fn get(&self, input: &LayoutInput) -> Option<LayoutOutput> {
let key = CacheKey::from(input);
match input.run_mode {
RunMode::PerformLayout => self.final_layout_entry.filter(|entry| entry.key == key).map(|e| e.content),
RunMode::ComputeSize => {
for (entry, stored) in self.measure_entries.iter().zip(self.measure_inputs.iter()) {
let Some(entry) = entry else { continue };
if entry.key.x_axis_parent_size() != key.x_axis_parent_size() {
continue;
}
if entry.key.kd_available_space == key.kd_available_space {
return Some(LayoutOutput::from_outer_size(entry.content));
}
let Some(stored) = stored else { continue };
let width_ok = axis_still_valid(
stored.known_dimensions.width,
input.known_dimensions.width,
stored.available_space.width,
input.available_space.width,
entry.content.width,
);
let height_ok = axis_still_valid(
stored.known_dimensions.height,
input.known_dimensions.height,
stored.available_space.height,
input.available_space.height,
entry.content.height,
);
if width_ok && height_ok {
return Some(LayoutOutput::from_outer_size(Size {
width: input.known_dimensions.width.unwrap_or(entry.content.width),
height: input.known_dimensions.height.unwrap_or(entry.content.height),
}));
}
}
None
}
RunMode::PerformHiddenLayout => None,
}
}
pub fn store(&mut self, input: &LayoutInput, layout_output: LayoutOutput) {
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 => {
self.is_empty = false;
STORES.fetch_add(1, Ordering::Relaxed);
let slot = self.slot_for(&key);
self.measure_entries[slot] = Some(CacheEntry { key, content: layout_output.size });
self.measure_inputs[slot] = Some(MeasureInputs {
known_dimensions: input.known_dimensions,
available_space: input.available_space,
});
}
RunMode::PerformHiddenLayout => {}
}
}
pub fn clear(&mut self) -> ClearState {
if self.is_empty {
return ClearState::AlreadyEmpty;
}
self.is_empty = true;
self.final_layout_entry = None;
self.measure_entries = [None; CACHE_SIZE];
self.measure_inputs = [None; CACHE_SIZE];
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 relaxation_tests {
use super::*;
use crate::geometry::{Line, Size};
use crate::tree::{LayoutInput, LayoutOutput, RunMode, SizingMode};
fn measure_input(known: Size<Option<f32>>, space: Size<AvailableSpace>) -> LayoutInput {
LayoutInput {
run_mode: RunMode::ComputeSize,
sizing_mode: SizingMode::ContentSize,
axis: RequestedAxis::Both,
known_dimensions: known,
parent_size: Size { width: Some(800.0), height: Some(600.0) },
available_space: space,
vertical_margins_are_collapsible: Line::FALSE,
}
}
#[test]
fn an_imposed_size_reuses_the_measurement_taken_under_the_same_offer() {
let mut cache = Cache::new();
let stored = measure_input(
Size { width: None, height: None },
Size { width: AvailableSpace::Definite(300.0), height: AvailableSpace::MaxContent },
);
cache.store(&stored, LayoutOutput::from_outer_size(Size { width: 250.0, height: 40.0 }));
let asked = measure_input(
Size { width: Some(300.0), height: None },
Size { width: AvailableSpace::Definite(300.0), height: AvailableSpace::MaxContent },
);
let hit = cache.get(&asked).expect("the entry still answers the question");
assert_eq!(hit.size.width, 300.0, "the imposed width is the answer, not the measured one");
assert_eq!(hit.size.height, 40.0, "the measured height carries over");
}
#[test]
fn a_free_query_does_not_reuse_an_imposed_measurement() {
let mut cache = Cache::new();
let stored = measure_input(
Size { width: Some(300.0), height: None },
Size { width: AvailableSpace::Definite(300.0), height: AvailableSpace::MaxContent },
);
cache.store(&stored, LayoutOutput::from_outer_size(Size { width: 300.0, height: 40.0 }));
let asked = measure_input(
Size { width: None, height: None },
Size { width: AvailableSpace::Definite(300.0), height: AvailableSpace::MaxContent },
);
assert!(cache.get(&asked).is_none(), "a free measure must not inherit an imposed width");
}
#[test]
fn an_overflowing_measurement_is_not_reused_when_the_size_is_imposed() {
let mut cache = Cache::new();
let stored = measure_input(
Size { width: None, height: None },
Size { width: AvailableSpace::Definite(300.0), height: AvailableSpace::MaxContent },
);
cache.store(&stored, LayoutOutput::from_outer_size(Size { width: 420.0, height: 20.0 }));
let asked = measure_input(
Size { width: Some(300.0), height: None },
Size { width: AvailableSpace::Definite(300.0), height: AvailableSpace::MaxContent },
);
assert!(cache.get(&asked).is_none(), "content wider than the offer must be re-measured");
}
}