Skip to main content

tex_packer_core/
runtime.rs

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/// Owned logical and physical context returned by runtime placement operations.
14#[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/// KTD10 atlas metrics plus runtime allocator fragmentation data.
64#[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    /// Space currently reported by runtime placement strategies.
78    pub allocator_free_area: u128,
79    /// Number of allocator free rectangles or segments.
80    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    /// Returns the runtime allocator fragmentation ratio.
98    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    /// Returns the percentage of page area not occupied by physical allocations.
107    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    /// Finds the resolved runtime placement for a key.
305    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    /// Finds the physical allocation reserved for a key.
312    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    /// Evicts a texture by key without requiring its page identity.
318    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}