1use std::collections::HashMap;
2
3use crate::config::RuntimeConfig;
4use crate::error::{Result, TexPackerError};
5use crate::geometry::PlacementGeometry;
6use crate::model::{
7 Atlas, Frame, FrameId, Meta, PageId, Rect, Region, RegionId, area_percentage, area_ratio,
8};
9use crate::runtime_placement::{PreparedPageAppend, RuntimePage};
10
11pub use crate::runtime_atlas::{RuntimeAtlas, RuntimeImageUpdate, UpdateRegion};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct RuntimePlacement {
16 page_id: PageId,
17 frame: Frame,
18 region: Region,
19}
20
21impl RuntimePlacement {
22 pub(crate) const fn new(page_id: PageId, frame: Frame, region: Region) -> Self {
23 Self {
24 page_id,
25 frame,
26 region,
27 }
28 }
29
30 pub const fn page_id(&self) -> PageId {
31 self.page_id
32 }
33
34 pub const fn frame_id(&self) -> FrameId {
35 self.frame.id()
36 }
37
38 pub const fn region_id(&self) -> RegionId {
39 self.region.id()
40 }
41
42 pub const fn frame(&self) -> &Frame {
43 &self.frame
44 }
45
46 pub const fn region(&self) -> &Region {
47 &self.region
48 }
49
50 pub const fn content(&self) -> Rect {
51 self.region.content()
52 }
53
54 pub const fn allocation(&self) -> Rect {
55 self.region.allocation()
56 }
57
58 pub const fn rotated(&self) -> bool {
59 self.region.rotated()
60 }
61}
62
63#[derive(Debug, Clone, PartialEq)]
65pub struct RuntimeStats {
66 pub num_pages: usize,
67 pub num_frames: usize,
68 pub num_regions: usize,
69 pub num_aliases: usize,
70 pub num_rotated_regions: usize,
71 pub num_trimmed_frames: usize,
72 pub page_area: u128,
73 pub content_area: u128,
74 pub allocation_area: u128,
75 pub content_occupancy: f64,
76 pub allocation_occupancy: f64,
77 pub allocator_free_area: u128,
79 pub num_free_rects: usize,
81}
82
83impl RuntimeStats {
84 pub fn summary(&self) -> String {
85 format!(
86 "Pages: {}, Frames: {}, Regions: {}, Content occupancy: {:.2}%, Allocation occupancy: {:.2}%, Allocator free: {} px² ({} rects)",
87 self.num_pages,
88 self.num_frames,
89 self.num_regions,
90 self.content_occupancy * 100.0,
91 self.allocation_occupancy * 100.0,
92 self.allocator_free_area,
93 self.num_free_rects,
94 )
95 }
96
97 pub fn fragmentation(&self) -> f64 {
99 if self.allocator_free_area == 0 {
100 0.0
101 } else {
102 self.num_free_rects as f64 / (self.allocator_free_area as f64 / 1000.0).max(1.0)
103 }
104 }
105
106 pub fn waste_percentage(&self) -> f64 {
108 area_percentage(
109 self.page_area.saturating_sub(self.allocation_area),
110 self.page_area,
111 )
112 }
113}
114
115pub struct AtlasSession {
116 pub(crate) cfg: RuntimeConfig,
117 pages: Vec<RuntimePage>,
118 page_slots: HashMap<PageId, usize>,
119 key_index: HashMap<String, RuntimeHandle>,
120 next_page_id: u32,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124struct RuntimeHandle {
125 page_id: PageId,
126 frame_id: FrameId,
127}
128
129enum PreparedAppendTarget {
130 Existing {
131 page_index: usize,
132 page_append: PreparedPageAppend,
133 },
134 New {
135 page: RuntimePage,
136 next_page_id: u32,
137 placement: RuntimePlacement,
138 },
139}
140
141pub(crate) struct PreparedAppend {
142 target: PreparedAppendTarget,
143 page_size: (u32, u32),
144 key: String,
145}
146
147impl PreparedAppend {
148 pub(crate) const fn placement(&self) -> &RuntimePlacement {
149 match &self.target {
150 PreparedAppendTarget::Existing { page_append, .. } => page_append.placement(),
151 PreparedAppendTarget::New { placement, .. } => placement,
152 }
153 }
154
155 pub(crate) const fn page_size(&self) -> (u32, u32) {
156 self.page_size
157 }
158}
159
160impl AtlasSession {
161 pub fn new(cfg: RuntimeConfig) -> Self {
162 Self {
163 cfg,
164 pages: Vec::new(),
165 page_slots: HashMap::new(),
166 key_index: HashMap::new(),
167 next_page_id: 0,
168 }
169 }
170
171 pub fn append(&mut self, key: String, w: u32, h: u32) -> Result<RuntimePlacement> {
172 let prepared = self.prepare_append(key, w, h)?;
173 Ok(self.commit_append(prepared))
174 }
175
176 pub(crate) fn prepare_append(&self, key: String, w: u32, h: u32) -> Result<PreparedAppend> {
177 if self.key_index.contains_key(&key) {
178 return Err(TexPackerError::DuplicateKey { key });
179 }
180 if w == 0 || h == 0 {
181 return Err(TexPackerError::InvalidDimensions {
182 width: w,
183 height: h,
184 });
185 }
186
187 let page_config = self.cfg.page_config();
188 let Some(geometry) = PlacementGeometry::from_size(w, h, page_config) else {
189 return Err(TexPackerError::TextureTooLarge {
190 key,
191 width: w,
192 height: h,
193 max_width: page_config.max_width(),
194 max_height: page_config.max_height(),
195 });
196 };
197 let source = Rect::new(0, 0, w, h);
198
199 for (page_index, page) in self.pages.iter().enumerate() {
200 if let Some(page_append) = page.prepare_append(&key, geometry, source)? {
201 return Ok(PreparedAppend {
202 target: PreparedAppendTarget::Existing {
203 page_index,
204 page_append,
205 },
206 page_size: page.size(),
207 key,
208 });
209 }
210 }
211
212 let page_id = PageId::new(self.next_page_id);
213 let next_page_id =
214 self.next_page_id
215 .checked_add(1)
216 .ok_or_else(|| TexPackerError::InvariantViolation {
217 context: "runtime atlas".into(),
218 reason: format!("page identity space exhausted at {}", self.next_page_id),
219 })?;
220 let page_config = self.cfg.page_config();
221 let mut page = RuntimePage::new(
222 page_id,
223 page_config.max_width(),
224 page_config.max_height(),
225 page_config,
226 self.cfg.strategy(),
227 );
228 let page_size = page.size();
229 if let Some(page_append) = page.prepare_append(&key, geometry, source)? {
230 let placement = page.commit_append(page_append);
231 return Ok(PreparedAppend {
232 target: PreparedAppendTarget::New {
233 page,
234 next_page_id,
235 placement,
236 },
237 page_size,
238 key,
239 });
240 }
241
242 Err(TexPackerError::OutOfSpace {
243 key,
244 width: w,
245 height: h,
246 pages_attempted: self.pages.len() + 1,
247 })
248 }
249
250 pub(crate) fn commit_append(&mut self, prepared: PreparedAppend) -> RuntimePlacement {
251 let placement = match prepared.target {
252 PreparedAppendTarget::Existing {
253 page_index,
254 page_append,
255 } => self.pages[page_index].commit_append(page_append),
256 PreparedAppendTarget::New {
257 page,
258 next_page_id,
259 placement,
260 } => {
261 let page_id = page.id();
262 let page_slot = self.pages.len();
263 self.pages.push(page);
264 let replaced_slot = self.page_slots.insert(page_id, page_slot);
265 debug_assert!(replaced_slot.is_none(), "new page identity must be unique");
266 self.next_page_id = next_page_id;
267 placement
268 }
269 };
270 let handle = RuntimeHandle {
271 page_id: placement.page_id(),
272 frame_id: placement.frame_id(),
273 };
274 let replaced_handle = self.key_index.insert(prepared.key, handle);
275 debug_assert!(
276 replaced_handle.is_none(),
277 "prepared runtime key must remain unique"
278 );
279 placement
280 }
281
282 pub fn evict(&mut self, page_id: PageId, key: &str) -> bool {
283 let Some(handle) = self.key_index.get(key).copied() else {
284 return false;
285 };
286 if handle.page_id != page_id {
287 return false;
288 }
289 self.evict_handle(key, handle)
290 }
291
292 pub fn snapshot_atlas(&self) -> Result<Atlas> {
293 let mut page_refs: Vec<_> = self.pages.iter().collect();
294 page_refs.sort_unstable_by_key(|page| page.id());
295 let pages = page_refs
296 .into_iter()
297 .map(RuntimePage::snapshot)
298 .collect::<Result<Vec<_>>>()?;
299 let page_config = self.cfg.page_config();
300 let meta = Meta::for_run(page_config, false, false, "none");
301 Atlas::try_new(pages, meta)
302 }
303
304 pub fn get_frame(&self, key: &str) -> Option<RuntimePlacement> {
306 let handle = self.key_index.get(key)?;
307 let page_slot = *self.page_slots.get(&handle.page_id)?;
308 self.pages.get(page_slot)?.placement(handle.frame_id)
309 }
310
311 pub fn get_reserved_slot(&self, key: &str) -> Option<(PageId, Rect)> {
313 self.get_frame(key)
314 .map(|placement| (placement.page_id(), placement.allocation()))
315 }
316
317 pub fn evict_by_key(&mut self, key: &str) -> bool {
319 let Some(handle) = self.key_index.get(key).copied() else {
320 return false;
321 };
322 self.evict_handle(key, handle)
323 }
324
325 pub fn contains(&self, key: &str) -> bool {
326 self.key_index.contains_key(key)
327 }
328
329 pub fn keys(&self) -> Vec<&str> {
330 let mut entries: Vec<_> = self.key_index.iter().collect();
331 entries.sort_unstable_by_key(|(_, handle)| (handle.page_id, handle.frame_id));
332 entries.into_iter().map(|(key, _)| key.as_str()).collect()
333 }
334
335 pub fn texture_count(&self) -> usize {
336 self.pages.iter().map(RuntimePage::len).sum()
337 }
338
339 pub fn stats(&self) -> RuntimeStats {
340 let num_pages = self.pages.len();
341 let mut num_frames = 0usize;
342 let mut num_regions = 0usize;
343 let mut num_rotated_regions = 0usize;
344 let mut page_area = 0u128;
345 let mut content_area = 0u128;
346 let mut allocation_area = 0u128;
347 let mut allocator_free_area = 0u128;
348 let mut num_free_rects = 0usize;
349 for page in &self.pages {
350 let metrics = page.metrics();
351 num_frames += metrics.num_frames;
352 num_regions += metrics.num_regions;
353 num_rotated_regions += metrics.num_rotated_regions;
354 page_area += metrics.page_area;
355 content_area += metrics.content_area;
356 allocation_area += metrics.allocation_area;
357 allocator_free_area += metrics.allocator_free_area;
358 num_free_rects += metrics.num_free_rects;
359 }
360
361 RuntimeStats {
362 num_pages,
363 num_frames,
364 num_regions,
365 num_aliases: 0,
366 num_rotated_regions,
367 num_trimmed_frames: 0,
368 page_area,
369 content_area,
370 allocation_area,
371 content_occupancy: area_ratio(content_area, page_area),
372 allocation_occupancy: area_ratio(allocation_area, page_area),
373 allocator_free_area,
374 num_free_rects,
375 }
376 }
377
378 fn evict_handle(&mut self, key: &str, handle: RuntimeHandle) -> bool {
379 let Some(&page_slot) = self.page_slots.get(&handle.page_id) else {
380 return false;
381 };
382 let Some(page) = self.pages.get_mut(page_slot) else {
383 return false;
384 };
385 if page.evict(handle.frame_id).is_none() {
386 return false;
387 }
388 let removed = self.key_index.remove(key);
389 debug_assert_eq!(removed, Some(handle));
390 true
391 }
392}