use crate::compat::atomic::{AtomicU64, Ordering};
use crate::compat::{HashMap, Rc, RefCell, Vec, VecDeque};
use crate::core::{Color, Font, Rect, Size};
use crate::render::default_software_render_config;
use crate::render::pipeline::pixel_ops::{fill_pixels, pixel_visible, write_pixel_with_mode};
use crate::render::{
BackBuffer, BlendMode, ShapedText, SoftwareRenderConfig, SoftwareSurface, TextMetrics,
};
use crate::style::Gradient;
use crate::style::GradientType;
static TEXT_METRICS_EPOCH: AtomicU64 = AtomicU64::new(1);
#[cfg(feature = "runtime-fonts")]
pub(crate) fn invalidate_text_measurements() {
TEXT_METRICS_EPOCH.fetch_add(1, Ordering::Relaxed);
}
const TEXT_METRICS_CACHE_LIMIT: usize = 2048;
const TEXT_SHAPES_CACHE_LIMIT: usize = 512;
type TextMetricsCache = HashMap<(u64, u64), TextMetrics>;
type ShapeCache = RefCell<(u64, HashMap<(u64, u64), Rc<ShapedText>>)>;
thread_local! {
static TEXT_METRICS: RefCell<(u64, TextMetricsCache)> =
RefCell::new((0, HashMap::new()));
static TEXT_METRICS_ORDER: RefCell<VecDeque<(u64, u64)>> =
const { RefCell::new(VecDeque::new()) };
static TEXT_SHAPES: ShapeCache =
RefCell::new((0, HashMap::new()));
static TEXT_SHAPES_ORDER: RefCell<VecDeque<(u64, u64)>> =
const { RefCell::new(VecDeque::new()) };
}
fn text_measurement_fingerprint(text: &str, font: &Font, scale: f32) -> (u64, u64) {
use core::hash::{Hash, Hasher};
let size = font.size().to_bits();
let letter = font.letter_spacing().to_bits();
let word = font.word_spacing().to_bits();
let line = font.line_height().to_bits();
let scale = scale.to_bits();
let mut first = std::collections::hash_map::DefaultHasher::new();
text.hash(&mut first);
font.family().hash(&mut first);
size.hash(&mut first);
font.weight().hash(&mut first);
font.is_bold().hash(&mut first);
font.is_italic().hash(&mut first);
letter.hash(&mut first);
word.hash(&mut first);
line.hash(&mut first);
scale.hash(&mut first);
let bytes = text.as_bytes();
let head = &bytes[..bytes.len().min(8)];
let tail = &bytes[bytes.len().saturating_sub(8)..];
let mut second = std::collections::hash_map::DefaultHasher::new();
0xA5u8.hash(&mut second);
bytes.len().hash(&mut second);
head.hash(&mut second);
tail.hash(&mut second);
scale.hash(&mut second);
line.hash(&mut second);
word.hash(&mut second);
letter.hash(&mut second);
font.is_italic().hash(&mut second);
font.is_bold().hash(&mut second);
font.weight().hash(&mut second);
size.hash(&mut second);
font.family().hash(&mut second);
(first.finish(), second.finish())
}
impl SoftwareSurface {
pub fn new(size: Size, dpi_scale: f32) -> Self {
let config = default_software_render_config();
Self {
buffer: BackBuffer::new(size, dpi_scale),
aa_samples_per_axis: config.aa_samples_per_axis,
clip_stack: Vec::new(),
nonzero_intersections: Vec::new(),
text_coverage_scratch: Vec::new(),
blend_mode: BlendMode::Normal,
}
}
pub fn set_blend_mode(&mut self, mode: BlendMode) {
self.blend_mode = mode;
}
pub fn blend_mode(&self) -> BlendMode {
self.blend_mode
}
pub fn render_config(&self) -> SoftwareRenderConfig {
SoftwareRenderConfig { aa_samples_per_axis: self.aa_samples_per_axis }
}
pub fn apply_render_config(&mut self, config: SoftwareRenderConfig) {
let normalized = config.normalized();
self.aa_samples_per_axis = normalized.aa_samples_per_axis;
}
pub fn set_aa_samples_per_axis(&mut self, samples: u8) {
self.apply_render_config(SoftwareRenderConfig { aa_samples_per_axis: samples });
}
pub fn aa_samples_per_axis(&self) -> u8 {
self.aa_samples_per_axis
}
pub fn begin_frame(&mut self, clear: Color) {
fill_pixels(self.buffer.back_mut(), clear);
}
pub fn end_frame(&mut self) {
self.buffer.present();
}
pub fn size(&self) -> Size {
self.buffer.size()
}
pub fn resize(&mut self, size: Size) {
self.buffer.resize(size);
}
pub fn set_dpi_scale(&mut self, dpi_scale: f32) {
self.buffer.set_dpi_scale(dpi_scale);
}
pub fn dpi_scale(&self) -> f32 {
self.buffer.dpi_scale()
}
pub fn frame_rgba(&self) -> &[u8] {
self.buffer.front()
}
pub fn into_frame_rgba(mut self) -> Vec<u8> {
self.buffer.take_front()
}
pub fn measure_text(&self, text: &str, font: &Font) -> TextMetrics {
let scale = self.buffer.dpi_scale();
let epoch = TEXT_METRICS_EPOCH.load(Ordering::Relaxed);
let key = text_measurement_fingerprint(text, font, scale);
if let Some(hit) = TEXT_METRICS.with(|cache| {
let cache = cache.borrow();
(cache.0 == epoch).then(|| cache.1.get(&key).cloned()).flatten()
}) {
return hit;
}
let shaped = self.shape_text(text, font);
let width = shaped.advance().round() as u32;
let metrics = TextMetrics { width, ..TextMetrics::for_font(font, scale) };
note_measurement_built();
TEXT_METRICS.with(|cache| {
let mut cache = cache.borrow_mut();
if cache.0 != epoch {
cache.0 = epoch;
cache.1.clear();
let _ = TEXT_METRICS_ORDER.try_with(|order| order.borrow_mut().clear());
}
TEXT_METRICS_ORDER.with(|order| {
let mut order = order.borrow_mut();
if order.len() >= TEXT_METRICS_CACHE_LIMIT {
if let Some(oldest) = order.pop_front() {
cache.1.remove(&oldest);
}
}
order.push_back(key);
});
cache.1.insert(key, metrics);
});
metrics
}
pub fn shape_text(&self, text: &str, font: &Font) -> Rc<ShapedText> {
let scale = self.buffer.dpi_scale();
let epoch = TEXT_METRICS_EPOCH.load(Ordering::Relaxed);
let key = text_measurement_fingerprint(text, font, scale);
if let Some(hit) = TEXT_SHAPES.with(|cache| {
let cache = cache.borrow();
(cache.0 == epoch).then(|| cache.1.get(&key).cloned()).flatten()
}) {
return hit;
}
let shaped = Rc::new(crate::render::text::shape_line(text, font, scale));
note_shape_built();
TEXT_SHAPES.with(|cache| {
let mut cache = cache.borrow_mut();
if cache.0 != epoch {
cache.0 = epoch;
cache.1.clear();
let _ = TEXT_SHAPES_ORDER.try_with(|order| order.borrow_mut().clear());
}
TEXT_SHAPES_ORDER.with(|order| {
let mut order = order.borrow_mut();
if order.len() >= TEXT_SHAPES_CACHE_LIMIT {
if let Some(oldest) = order.pop_front() {
cache.1.remove(&oldest);
}
}
order.push_back(key);
});
cache.1.insert(key, Rc::clone(&shaped));
});
shaped
}
pub fn fill_rect_gradient(&mut self, rect: Rect, gradient: &Gradient) {
let size = self.buffer.size();
let clip = self.current_clip();
let x0 = rect.x.max(0) as u32;
let y0 = rect.y.max(0) as u32;
let x1 = rect.x.saturating_add(rect.width as i32).max(0) as u32;
let y1 = rect.y.saturating_add(rect.height as i32).max(0) as u32;
let x1 = x1.min(size.width);
let y1 = y1.min(size.height);
let frame = self.buffer.back_mut();
const LUT_SIZE: usize = 256;
let mut lut = [Color::BLACK; LUT_SIZE];
for (index, slot) in lut.iter_mut().enumerate() {
*slot = gradient.interpolate(index as f32 / (LUT_SIZE - 1) as f32);
}
let lut_index = |pos: f32| -> usize {
((pos.clamp(0.0, 1.0) * (LUT_SIZE - 1) as f32) as usize).min(LUT_SIZE - 1)
};
let linear_dx = gradient.end_point.x as f32 - gradient.start_point.x as f32;
let linear_dy = gradient.end_point.y as f32 - gradient.start_point.y as f32;
let linear_dot = linear_dx * linear_dx + linear_dy * linear_dy;
let linear_step = if linear_dot == 0.0 { 0.0 } else { linear_dx / linear_dot };
for y in y0..y1 {
let row_start = if linear_dot == 0.0 {
0.0
} else {
((x0 as f32 - gradient.start_point.x as f32) * linear_dx
+ (y as f32 - gradient.start_point.y as f32) * linear_dy)
/ linear_dot
};
for x in x0..x1 {
let pos = match gradient.gradient_type {
GradientType::Linear => {
if linear_dot == 0.0 {
0.0
} else {
(row_start + (x - x0) as f32 * linear_step).clamp(0.0, 1.0)
}
}
GradientType::Radial => {
let dx = x as f32 - gradient.center.x as f32;
let dy = y as f32 - gradient.center.y as f32;
let dist = (dx * dx + dy * dy).sqrt();
if gradient.radius <= 0.0 {
0.0
} else {
(dist / gradient.radius).clamp(0.0, 1.0)
}
}
GradientType::Conic => {
let dx = x as f32 - gradient.center.x as f32;
let dy = y as f32 - gradient.center.y as f32;
let angle = dy.atan2(dx) + core::f32::consts::PI;
let start = gradient.angle.to_radians();
let angle = (angle + start) % core::f32::consts::TAU;
let angle =
if angle < 0.0 { angle + core::f32::consts::TAU } else { angle };
angle / core::f32::consts::TAU
}
};
let color = lut[lut_index(pos)];
if pixel_visible(clip, x as i32, y as i32) {
write_pixel_with_mode(self.blend_mode, frame, size.width, x, y, color, 1.0);
}
}
}
}
pub fn push_clip(&mut self, x: i32, y: i32, width: u32, height: u32) {
if let Some(&(cx, cy, cw, ch)) = self.clip_stack.last() {
let nx = x.max(cx);
let ny = y.max(cy);
let nx2 = (x.saturating_add(width as i32)).min(cx.saturating_add(cw as i32));
let ny2 = (y.saturating_add(height as i32)).min(cy.saturating_add(ch as i32));
let nw = (nx2 - nx).max(0) as u32;
let nh = (ny2 - ny).max(0) as u32;
self.clip_stack.push((nx, ny, nw, nh));
} else {
self.clip_stack.push((x, y, width, height));
}
}
pub fn pop_clip(&mut self) {
self.clip_stack.pop();
}
pub fn current_clip(&self) -> Option<(i32, i32, u32, u32)> {
self.clip_stack.last().copied()
}
}
#[cfg(test)]
thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static TEXT_SHAPES_BUILT: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
#[allow(clippy::missing_const_for_thread_local)]
static TEXT_METRICS_BUILT: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
#[cfg(test)]
fn note_shape_built() {
TEXT_SHAPES_BUILT.with(|count| count.set(count.get() + 1));
}
#[cfg(not(test))]
#[inline(always)]
fn note_shape_built() {}
#[cfg(test)]
fn note_measurement_built() {
TEXT_METRICS_BUILT.with(|count| count.set(count.get() + 1));
}
#[cfg(not(test))]
#[inline(always)]
fn note_measurement_built() {}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Font, Size};
use crate::render::SoftwareSurface;
fn take_builds() -> (usize, usize) {
let shapes = TEXT_SHAPES_BUILT.with(|count| count.replace(0));
let metrics = TEXT_METRICS_BUILT.with(|count| count.replace(0));
(shapes, metrics)
}
#[test]
fn repeated_text_measurements_are_cached() {
#[cfg(all(feature = "runtime-fonts", feature = "text-shaping"))]
let _guard = crate::render::text::runtime_fonts::test_lock();
let surface = SoftwareSurface::new(Size::new(20, 20), 1.0);
let font = Font::default();
let warm = surface.measure_text("hello", &font);
let (shapes, metrics) = take_builds();
assert_eq!((shapes, metrics), (1, 1), "the first measurement has to build its answer");
assert_eq!(surface.measure_text("hello", &font), warm);
assert_eq!(take_builds(), (0, 0), "a repeated measurement must be a cache hit");
let _ = surface.shape_text("hello", &font);
assert_eq!(take_builds(), (0, 0), "a repeated shape must be a cache hit too");
let _ = surface.measure_text("world", &font);
let (shapes, metrics) = take_builds();
assert_eq!((shapes, metrics), (1, 1), "a different string is a different key");
}
#[test]
fn a_shaped_run_hit_is_the_same_allocation_not_a_copy() {
#[cfg(all(feature = "runtime-fonts", feature = "text-shaping"))]
let _guard = crate::render::text::runtime_fonts::test_lock();
let surface = SoftwareSurface::new(Size::new(20, 20), 1.0);
let font = Font::default();
let first = surface.shape_text("hello", &font);
let _ = take_builds();
let second = surface.shape_text("hello", &font);
assert!(Rc::ptr_eq(&first, &second), "a cache hit must return the cached run itself");
assert_eq!(take_builds(), (0, 0), "and it must not have shaped anything");
}
#[cfg(feature = "runtime-fonts")]
#[test]
fn invalidating_text_measurements_forces_a_rebuild() {
#[cfg(all(feature = "runtime-fonts", feature = "text-shaping"))]
let _guard = crate::render::text::runtime_fonts::test_lock();
let surface = SoftwareSurface::new(Size::new(20, 20), 1.0);
let font = Font::default();
let _ = surface.measure_text("hello", &font);
let _ = take_builds();
invalidate_text_measurements();
let _ = surface.measure_text("hello", &font);
assert_eq!(take_builds(), (1, 1), "a new epoch must miss the old entry");
}
#[test]
fn fingerprints_separate_texts_that_share_a_summary() {
let font = Font::default();
let head = "head-head-";
let tail = "-tail-tail";
let a = format!("{head}alpha{tail}");
let b = format!("{head}bravo{tail}");
assert_eq!(a.len(), b.len(), "the fixture must agree on the summary's length");
assert_eq!(
fingerprint_summary(&a),
fingerprint_summary(&b),
"and on the head/tail bytes, or this test proves nothing"
);
let key_a = text_measurement_fingerprint(&a, &font, 1.0);
let key_b = text_measurement_fingerprint(&b, &font, 1.0);
assert_ne!(key_a, key_b, "different middles must still be different keys");
assert_eq!(text_measurement_fingerprint(&a, &font, 1.0), key_a);
let mut bigger = Font::default();
bigger.set_size(28.0);
assert_ne!(text_measurement_fingerprint(&a, &bigger, 1.0), key_a);
}
fn fingerprint_summary(text: &str) -> (usize, &[u8], &[u8]) {
let bytes = text.as_bytes();
(bytes.len(), &bytes[..bytes.len().min(8)], &bytes[bytes.len().saturating_sub(8)..])
}
}