1use astrelis_core::alloc::HashMap;
7use std::sync::Arc;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct ShapeKey {
15 pub font_id: u32,
17 pub font_size_px: u16,
19 pub text_hash: u32,
21 pub wrap_width_bucket: u16,
23}
24
25impl ShapeKey {
26 pub fn new(font_id: u32, font_size: f32, text_content: &str, wrap_width: Option<f32>) -> Self {
28 Self {
29 font_id,
30 font_size_px: font_size.round() as u16,
31 text_hash: fxhash::hash32(text_content),
32 wrap_width_bucket: wrap_width.map(Self::bucket_width).unwrap_or(0),
33 }
34 }
35
36 fn bucket_width(width: f32) -> u16 {
41 (width / 4.0).round() as u16
42 }
43
44 pub fn bucketed_width(&self) -> Option<f32> {
46 if self.wrap_width_bucket == 0 {
47 None
48 } else {
49 Some(self.wrap_width_bucket as f32 * 4.0)
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
59pub struct ShapedTextData {
60 pub content: String,
62 pub bounds: (f32, f32),
64 pub shaped_at_version: u32,
68 pub render_count: u64,
70}
71
72impl ShapedTextData {
73 pub fn new(content: String, bounds: (f32, f32), version: u32) -> Self {
74 Self {
75 content,
76 bounds,
77 shaped_at_version: version,
78 render_count: 0,
79 }
80 }
81}
82
83pub struct TextShapeCache {
88 cache: HashMap<ShapeKey, Arc<ShapedTextData>>,
89 pub hits: u64,
91 pub misses: u64,
92}
93
94impl TextShapeCache {
95 pub fn new() -> Self {
97 Self {
98 cache: HashMap::with_capacity(256),
99 hits: 0,
100 misses: 0,
101 }
102 }
103
104 pub fn get_or_shape<F>(&mut self, key: ShapeKey, shape_fn: F) -> Arc<ShapedTextData>
109 where
110 F: FnOnce() -> ShapedTextData,
111 {
112 if let Some(cached) = self.cache.get_mut(&key) {
113 self.hits += 1;
114 if let Some(data) = Arc::get_mut(cached) {
116 data.render_count += 1;
117 }
118 return cached.clone();
119 }
120
121 self.misses += 1;
122 let shaped = Arc::new(shape_fn());
123 self.cache.insert(key, shaped.clone());
124 shaped
125 }
126
127 pub fn get(&mut self, key: &ShapeKey) -> Option<Arc<ShapedTextData>> {
129 let result = self.cache.get_mut(key).map(|cached| {
130 if let Some(data) = Arc::get_mut(cached) {
131 data.render_count += 1;
132 }
133 cached.clone()
134 });
135 if result.is_some() {
136 self.hits += 1;
137 } else {
138 self.misses += 1;
139 }
140 result
141 }
142
143 pub fn insert(&mut self, key: ShapeKey, data: ShapedTextData) -> Arc<ShapedTextData> {
145 let arc_data = Arc::new(data);
146 self.cache.insert(key, arc_data.clone());
147 arc_data
148 }
149
150 pub fn clear(&mut self) {
152 self.cache.clear();
153 self.hits = 0;
154 self.misses = 0;
155 }
156
157 pub fn prune_old_versions(&mut self, min_version: u32) {
159 self.cache
160 .retain(|_key, data| data.shaped_at_version >= min_version);
161 }
162
163 pub fn hit_rate(&self) -> f32 {
165 let total = self.hits + self.misses;
166 if total == 0 {
167 0.0
168 } else {
169 self.hits as f32 / total as f32
170 }
171 }
172
173 pub fn len(&self) -> usize {
175 self.cache.len()
176 }
177
178 pub fn is_empty(&self) -> bool {
180 self.cache.is_empty()
181 }
182
183 pub fn stats_string(&self) -> String {
185 let total_renders: u64 = self.cache.values().map(|arc| arc.render_count).sum();
186 format!(
187 "TextCache: {} entries, {:.1}% hit rate ({} hits, {} misses), {} total renders",
188 self.len(),
189 self.hit_rate() * 100.0,
190 self.hits,
191 self.misses,
192 total_renders
193 )
194 }
195
196 pub fn avg_renders_per_entry(&self) -> f32 {
198 if self.cache.is_empty() {
199 return 0.0;
200 }
201 let total_renders: u64 = self.cache.values().map(|arc| arc.render_count).sum();
202 total_renders as f32 / self.cache.len() as f32
203 }
204}
205
206impl Default for TextShapeCache {
207 fn default() -> Self {
208 Self::new()
209 }
210}