1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 use api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize};
6 
7 //TODO: gather real-world statistics on the bin usage in order to assist the decision
8 // on where to place the size thresholds.
9 
10 /// This is an optimization tweak to enable looking through all the free rectangles in a bin
11 /// and choosing the smallest, as opposed to picking the first match.
12 const FIND_SMALLEST_AREA: bool = false;
13 
14 const NUM_BINS: usize = 3;
15 /// The minimum number of pixels on each side that we require for rects to be classified as
16 /// particular bin of freelists.
17 const MIN_RECT_AXIS_SIZES: [i32; NUM_BINS] = [1, 16, 32];
18 
19 #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
20 struct FreeListBin(u8);
21 
22 #[derive(Debug, Clone, Copy)]
23 struct FreeListIndex(usize);
24 
25 impl FreeListBin {
for_size(size: &DeviceIntSize) -> Self26     fn for_size(size: &DeviceIntSize) -> Self {
27         MIN_RECT_AXIS_SIZES
28             .iter()
29             .enumerate()
30             .rev()
31             .find(|(_, &min_size)| min_size <= size.width && min_size <= size.height)
32             .map(|(id, _)| FreeListBin(id as u8))
33             .expect("Unable to find a bin!")
34     }
35 }
36 
37 #[derive(Debug, Clone, Copy, PartialEq)]
38 #[cfg_attr(feature = "capture", derive(Serialize))]
39 #[cfg_attr(feature = "replay", derive(Deserialize))]
40 pub struct FreeRectSlice(pub u32);
41 
42 #[cfg_attr(feature = "capture", derive(Serialize))]
43 #[cfg_attr(feature = "replay", derive(Deserialize))]
44 pub struct FreeRect {
45     slice: FreeRectSlice,
46     rect: DeviceIntRect,
47 }
48 
49 /// A texture allocator using the guillotine algorithm with the rectangle merge improvement. See
50 /// sections 2.2 and 2.2.5 in "A Thousand Ways to Pack the Bin - A Practical Approach to Two-
51 /// Dimensional Rectangle Bin Packing":
52 ///
53 ///    http://clb.demon.fi/files/RectangleBinPack.pdf
54 ///
55 /// This approach was chosen because of its simplicity, good performance, and easy support for
56 /// dynamic texture deallocation.
57 ///
58 /// Note: the allocations are spread across multiple textures, and also are binned
59 /// orthogonally in order to speed up the search.
60 #[cfg_attr(feature = "capture", derive(Serialize))]
61 #[cfg_attr(feature = "replay", derive(Deserialize))]
62 pub struct ArrayAllocationTracker {
63     bins: [Vec<FreeRect>; NUM_BINS],
64 }
65 
66 impl ArrayAllocationTracker {
new() -> Self67     pub fn new() -> Self {
68         ArrayAllocationTracker {
69             bins: [
70                 Vec::new(),
71                 Vec::new(),
72                 Vec::new(),
73             ],
74         }
75     }
76 
push(&mut self, slice: FreeRectSlice, rect: DeviceIntRect)77     fn push(&mut self, slice: FreeRectSlice, rect: DeviceIntRect) {
78         let id = FreeListBin::for_size(&rect.size).0 as usize;
79         self.bins[id].push(FreeRect {
80             slice,
81             rect,
82         })
83     }
84 
85     /// Find a suitable rect in the free list. We choose the smallest such rect
86     /// in terms of area (Best-Area-Fit, BAF).
find_index_of_best_rect( &self, requested_dimensions: &DeviceIntSize, ) -> Option<(FreeListBin, FreeListIndex)>87     fn find_index_of_best_rect(
88         &self,
89         requested_dimensions: &DeviceIntSize,
90     ) -> Option<(FreeListBin, FreeListIndex)> {
91         let start_bin = FreeListBin::for_size(requested_dimensions);
92         (start_bin.0 .. NUM_BINS as u8)
93             .find_map(|id| if FIND_SMALLEST_AREA {
94                 let mut smallest_index_and_area = None;
95                 for (candidate_index, candidate) in self.bins[id as usize].iter().enumerate() {
96                     if requested_dimensions.width > candidate.rect.size.width ||
97                         requested_dimensions.height > candidate.rect.size.height
98                     {
99                         continue;
100                     }
101 
102                     let candidate_area = candidate.rect.size.area();
103                     match smallest_index_and_area {
104                         Some((_, area)) if candidate_area >= area => continue,
105                         _ => smallest_index_and_area = Some((candidate_index, candidate_area)),
106                     }
107                 }
108 
109                 smallest_index_and_area
110                     .map(|(index, _)| (FreeListBin(id), FreeListIndex(index)))
111             } else {
112                 self.bins[id as usize]
113                     .iter()
114                     .position(|candidate| {
115                         requested_dimensions.width <= candidate.rect.size.width &&
116                         requested_dimensions.height <= candidate.rect.size.height
117                     })
118                     .map(|index| (FreeListBin(id), FreeListIndex(index)))
119             })
120     }
121 
122     // Split that results in the single largest area (Min Area Split Rule, MINAS).
split_guillotine(&mut self, chosen: &FreeRect, requested_dimensions: &DeviceIntSize)123     fn split_guillotine(&mut self, chosen: &FreeRect, requested_dimensions: &DeviceIntSize) {
124         let candidate_free_rect_to_right = DeviceIntRect::new(
125             DeviceIntPoint::new(
126                 chosen.rect.origin.x + requested_dimensions.width,
127                 chosen.rect.origin.y,
128             ),
129             DeviceIntSize::new(
130                 chosen.rect.size.width - requested_dimensions.width,
131                 requested_dimensions.height,
132             ),
133         );
134         let candidate_free_rect_to_bottom = DeviceIntRect::new(
135             DeviceIntPoint::new(
136                 chosen.rect.origin.x,
137                 chosen.rect.origin.y + requested_dimensions.height,
138             ),
139             DeviceIntSize::new(
140                 requested_dimensions.width,
141                 chosen.rect.size.height - requested_dimensions.height,
142             ),
143         );
144 
145         // Guillotine the rectangle.
146         let new_free_rect_to_right;
147         let new_free_rect_to_bottom;
148         if candidate_free_rect_to_right.size.area() > candidate_free_rect_to_bottom.size.area() {
149             new_free_rect_to_right = DeviceIntRect::new(
150                 candidate_free_rect_to_right.origin,
151                 DeviceIntSize::new(
152                     candidate_free_rect_to_right.size.width,
153                     chosen.rect.size.height,
154                 ),
155             );
156             new_free_rect_to_bottom = candidate_free_rect_to_bottom
157         } else {
158             new_free_rect_to_right = candidate_free_rect_to_right;
159             new_free_rect_to_bottom = DeviceIntRect::new(
160                 candidate_free_rect_to_bottom.origin,
161                 DeviceIntSize::new(
162                     chosen.rect.size.width,
163                     candidate_free_rect_to_bottom.size.height,
164                 ),
165             )
166         }
167 
168         // Add the guillotined rects back to the free list.
169         if !new_free_rect_to_right.is_empty() {
170             self.push(chosen.slice, new_free_rect_to_right);
171         }
172         if !new_free_rect_to_bottom.is_empty() {
173             self.push(chosen.slice, new_free_rect_to_bottom);
174         }
175     }
176 
allocate( &mut self, requested_dimensions: &DeviceIntSize ) -> Option<(FreeRectSlice, DeviceIntPoint)>177     pub fn allocate(
178         &mut self, requested_dimensions: &DeviceIntSize
179     ) -> Option<(FreeRectSlice, DeviceIntPoint)> {
180         if requested_dimensions.width == 0 || requested_dimensions.height == 0 {
181             return Some((FreeRectSlice(0), DeviceIntPoint::new(0, 0)));
182         }
183         let (bin, index) = self.find_index_of_best_rect(requested_dimensions)?;
184 
185         // Remove the rect from the free list and decide how to guillotine it.
186         let chosen = self.bins[bin.0 as usize].swap_remove(index.0);
187         self.split_guillotine(&chosen, requested_dimensions);
188 
189         // Return the result.
190         Some((chosen.slice, chosen.rect.origin))
191     }
192 
193     /// Add a new slice to the allocator, and immediately allocate a rect from it.
extend( &mut self, slice: FreeRectSlice, total_size: DeviceIntSize, requested_dimensions: DeviceIntSize, )194     pub fn extend(
195         &mut self,
196         slice: FreeRectSlice,
197         total_size: DeviceIntSize,
198         requested_dimensions: DeviceIntSize,
199     ) {
200         self.split_guillotine(
201             &FreeRect { slice, rect: total_size.into() },
202             &requested_dimensions
203         );
204     }
205 }
206 
207 #[cfg(test)]
random_fill(count: usize, texture_size: i32) -> f32208 fn random_fill(count: usize, texture_size: i32) -> f32 {
209     use rand::{thread_rng, Rng};
210 
211     let total_rect = DeviceIntRect::new(
212         DeviceIntPoint::zero(),
213         DeviceIntSize::new(texture_size, texture_size),
214     );
215     let mut rng = thread_rng();
216     let mut allocator = ArrayAllocationTracker::new();
217 
218     // check for empty allocation
219     assert_eq!(
220         allocator.allocate(&DeviceIntSize::new(0, 12)),
221         Some((FreeRectSlice(0), DeviceIntPoint::zero())),
222     );
223 
224     let mut slices: Vec<Vec<DeviceIntRect>> = Vec::new();
225     let mut requested_area = 0f32;
226     // fill up the allocator
227     for _ in 0 .. count {
228         let size = DeviceIntSize::new(
229             rng.gen_range(1, texture_size),
230             rng.gen_range(1, texture_size),
231         );
232         requested_area += size.area() as f32;
233 
234         match allocator.allocate(&size) {
235             Some((slice, origin)) => {
236                 let rect = DeviceIntRect::new(origin, size);
237                 assert_eq!(None, slices[slice.0 as usize].iter().find(|r| r.intersects(&rect)));
238                 assert!(total_rect.contains_rect(&rect));
239                 slices[slice.0 as usize].push(rect);
240             }
241             None => {
242                 allocator.extend(FreeRectSlice(slices.len() as u32), total_rect.size, size);
243                 let rect = DeviceIntRect::new(DeviceIntPoint::zero(), size);
244                 slices.push(vec![rect]);
245             }
246         }
247     }
248     // validate the free rects
249     for (i, free_vecs) in allocator.bins.iter().enumerate() {
250         for fr in free_vecs {
251             assert_eq!(FreeListBin(i as u8), FreeListBin::for_size(&fr.rect.size));
252             assert_eq!(None, slices[fr.slice.0 as usize].iter().find(|r| r.intersects(&fr.rect)));
253             assert!(total_rect.contains_rect(&fr.rect));
254             slices[fr.slice.0 as usize].push(fr.rect);
255         }
256     }
257 
258     let allocated_area = slices.len() as f32 * (texture_size * texture_size) as f32;
259     requested_area / allocated_area
260 }
261 
262 #[test]
test_small()263 fn test_small() {
264     random_fill(100, 100);
265 }
266 
267 #[test]
test_large()268 fn test_large() {
269     random_fill(1000, 10000);
270 }
271