Bug 1899118 - Update etagere to 0.2.11. r=gfx-reviewers,supply-chain-reviewers,jrmuizel

It contains a big perf improvement for atlases with many items.

Differential Revision: https://phabricator.services.mozilla.com/D212583
This commit is contained in:
Nicolas Silva 2024-06-11 10:33:44 +00:00
Родитель ea33877efc
Коммит 0fd61a2116
13 изменённых файлов: 310 добавлений и 48 удалений

4
Cargo.lock сгенерированный
Просмотреть файл

@ -1720,9 +1720,9 @@ dependencies = [
[[package]]
name = "etagere"
version = "0.2.7"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6301151a318f367f392c31395beb1cfba5ccd9abc44d1db0db3a4b27b9601c89"
checksum = "4460bc8c5caa3360e4221a5a2930c654a141dd3fe067a00c5355c488e711ae79"
dependencies = [
"euclid",
"serde",

4
gfx/wr/Cargo.lock сгенерированный
Просмотреть файл

@ -782,9 +782,9 @@ dependencies = [
[[package]]
name = "etagere"
version = "0.2.6"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eb66dc3d6bb6b2ab4a12454db6988079311d6443e627bc7e6065f907f556272"
checksum = "dff377452246a4a2e0ef3a7e85ce78ed77c7f93c3a4771e1c93d0cc0c69eb411"
dependencies = [
"euclid",
"serde",

Просмотреть файл

@ -51,7 +51,7 @@ glyph_rasterizer = { version = "0.1.0", path = "../wr_glyph_rasterizer", package
svg_fmt = "0.4"
tracy-rs = "0.1.2"
derive_more = { version = "0.99", default-features = false, features = ["add_assign"] }
etagere = "0.2.6"
etagere = "0.2.11"
glean = { version = "60.1.1", optional = true }
firefox-on-glean = { version = "0.1.0", optional = true }
swgl = { path = "../swgl", optional = true }

Просмотреть файл

@ -174,7 +174,7 @@ who = "Nicolas Silva <nical@fastmail.com>"
criteria = "safe-to-deploy"
user-id = 1281 # Nicolas Silva (nical)
start = "2020-11-12"
end = "2024-04-25"
end = "2025-06-01"
notes = "I am the author of this crate."
[[wildcard-audits.euclid]]

Просмотреть файл

@ -197,6 +197,13 @@ user-id = 1281
user-login = "nical"
user-name = "Nicolas Silva"
[[publisher.etagere]]
version = "0.2.11"
when = "2024-06-04"
user-id = 1281
user-login = "nical"
user-name = "Nicolas Silva"
[[publisher.euclid]]
version = "0.22.10"
when = "2024-05-21"

Просмотреть файл

@ -1 +1 @@
{"files":{"Cargo.toml":"131c1c4b73450c0109caa889c666ffd912d19e8cbb9249f3c71ef8f19ae5f909","LICENSE":"739e55c73735c9733d8be0e3ab1c3bdb2240df594602c18eefbf8c851a83a734","README.md":"e983d46af6c7fc18592ad9644d340bf3c5be9117490606b04aff9ac91c8c0495","src/allocator.rs":"7eea45f5cacb8b1822697638e7cf63d10c4a2047b6405123e3e401beae391510","src/bucketed.rs":"caad4039803df2eaf0b326c900b608f5707dda9066a72c87f95c98de87f96e0c","src/lib.rs":"ca8c534dcbccfc8c694b40622dbcecb45062bf79b36e1f5e000b680851059cb1"},"package":"6301151a318f367f392c31395beb1cfba5ccd9abc44d1db0db3a4b27b9601c89"}
{"files":{"Cargo.toml":"62163c32b26a2035c22f8d99a9ac5eb6077bacb751b8582171df6cacf480d5f8","LICENSE-APACHE":"2d56a37f3d1e461f6b1539ab94e0e6bcc55443e376098aee85ac3b2890860290","LICENSE-MIT":"9cfc9c9a4608501b5da3746948d2be2dd709ad51f6bcb0c1c685760ee3dde31f","README.md":"e983d46af6c7fc18592ad9644d340bf3c5be9117490606b04aff9ac91c8c0495","src/allocator.rs":"d84d62a3eb8dda52064f31d6b135ca08bc8c6b04937b34a61accf875740314ff","src/bucketed.rs":"ebd8e560f0e4b839b4ea887789be5e03767839cc245bd52567ad66bec224053f","src/lib.rs":"6dbb3c623cc4a1bc641dd314e0a8482788945eb6fb7c11c9fd9d6ddad6a630e1"},"package":"4460bc8c5caa3360e4221a5a2930c654a141dd3fe067a00c5355c488e711ae79"}

12
third_party/rust/etagere/Cargo.toml поставляемый
Просмотреть файл

@ -12,16 +12,19 @@
[package]
edition = "2018"
name = "etagere"
version = "0.2.7"
version = "0.2.11"
authors = ["Nicolas Silva <nical@fastmail.com>"]
exclude = [".backup*"]
description = "Dynamic 2D texture atlas allocation using the shelf packing algorithm."
documentation = "https://docs.rs/etagere/"
readme = "README.md"
keywords = ["2d"]
license = "MIT/Apache-2.0"
repository = "https://github.com/nical/etagere"
[profile.release]
debug = true
debug = 2
[dependencies.euclid]
version = "0.22"
@ -34,4 +37,7 @@ version = "0.4"
[features]
checks = []
serialization = ["serde", "euclid/serde"]
serialization = [
"serde",
"euclid/serde",
]

21
third_party/rust/etagere/LICENSE поставляемый
Просмотреть файл

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Nicolas Silva
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

13
third_party/rust/etagere/LICENSE-APACHE поставляемый Normal file
Просмотреть файл

@ -0,0 +1,13 @@
Copyright 2020 Nicolas Silva
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

20
third_party/rust/etagere/LICENSE-MIT поставляемый Normal file
Просмотреть файл

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2020 Nicolas Silva
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

86
third_party/rust/etagere/src/allocator.rs поставляемый
Просмотреть файл

@ -42,6 +42,7 @@ struct Shelf {
prev: ShelfIndex,
next: ShelfIndex,
first_item: ItemIndex,
first_unallocated: ItemIndex,
is_empty: bool,
}
@ -52,6 +53,8 @@ struct Item {
width: u16,
prev: ItemIndex,
next: ItemIndex,
prev_unallocated: ItemIndex,
next_unallocated: ItemIndex,
shelf: ShelfIndex,
allocated: bool,
generation: u16,
@ -148,6 +151,7 @@ impl AtlasAllocator {
next,
is_empty: true,
first_item,
first_unallocated: first_item,
});
self.items.push(Item {
@ -155,6 +159,8 @@ impl AtlasAllocator {
width: self.shelf_width,
prev: ItemIndex::NONE,
next: ItemIndex::NONE,
prev_unallocated: ItemIndex::NONE,
next_unallocated: ItemIndex::NONE,
shelf: current,
allocated: false,
generation: 1,
@ -213,14 +219,14 @@ impl AtlasAllocator {
continue;
}
let mut item_idx = shelf.first_item;
let mut item_idx = shelf.first_unallocated;
while item_idx.is_some() {
let item = &self.items[item_idx.index()];
if !item.allocated && item.width >= width {
break;
}
item_idx = item.next;
item_idx = item.next_unallocated;
}
if item_idx.is_some() {
@ -257,6 +263,7 @@ impl AtlasAllocator {
prev: selected_shelf,
next: shelf.next,
first_item: ItemIndex::NONE,
first_unallocated: ItemIndex::NONE,
is_empty: true,
});
@ -265,12 +272,15 @@ impl AtlasAllocator {
width: self.shelf_width,
prev: ItemIndex::NONE,
next: ItemIndex::NONE,
prev_unallocated: ItemIndex::NONE,
next_unallocated: ItemIndex::NONE,
shelf: new_shelf_idx,
allocated: false,
generation: 1,
});
self.shelves[new_shelf_idx.index()].first_item = new_item_idx;
self.shelves[new_shelf_idx.index()].first_unallocated = new_item_idx;
let next = self.shelves[selected_shelf.index()].next;
self.shelves[selected_shelf.index()].height = height;
@ -292,6 +302,8 @@ impl AtlasAllocator {
width: item.width - width,
prev: selected_item,
next: item.next,
prev_unallocated: item.prev_unallocated,
next_unallocated: item.next_unallocated,
shelf: item.shelf,
allocated: false,
generation: 1,
@ -303,7 +315,31 @@ impl AtlasAllocator {
if item.next.is_some() {
self.items[item.next.index()].prev = new_item_idx;
}
// Replace the item in the "unallocated" list.
let shelf = &mut self.shelves[selected_shelf.index()];
if shelf.first_unallocated == selected_item {
shelf.first_unallocated = new_item_idx;
}
if item.prev_unallocated.is_some() {
self.items[item.prev_unallocated.index()].next_unallocated = new_item_idx;
}
if item.next_unallocated.is_some() {
self.items[item.next_unallocated.index()].prev_unallocated = new_item_idx;
}
} else {
// Remove the item from the "unallocated" list.
let shelf = &mut self.shelves[selected_shelf.index()];
if shelf.first_unallocated == selected_item {
shelf.first_unallocated = item.next_unallocated;
}
if item.prev_unallocated.is_some() {
self.items[item.prev_unallocated.index()].next_unallocated = item.next_unallocated;
}
if item.next_unallocated.is_some() {
self.items[item.next_unallocated.index()].prev_unallocated = item.prev_unallocated;
}
width = item.width;
}
@ -337,7 +373,6 @@ impl AtlasAllocator {
pub fn deallocate(&mut self, id: AllocId) {
let item_idx = ItemIndex(id.index());
//let item = self.items[item_idx.index()].clone();
let Item { mut prev, mut next, mut width, allocated, shelf, generation, .. } = self.items[item_idx.index()];
assert!(allocated);
assert_eq!(generation, id.generation(), "Invalid AllocId");
@ -350,6 +385,18 @@ impl AtlasAllocator {
let next_next = self.items[next.index()].next;
let next_width = self.items[next.index()].width;
// Remove next from the "unallocated" list.
let next_unallocated = self.items[next.index()].next_unallocated;
let prev_unallocated = self.items[next.index()].prev_unallocated;
if self.shelves[shelf.index()].first_unallocated == next {
self.shelves[shelf.index()].first_unallocated = next_unallocated;
}
if prev_unallocated.is_some() {
self.items[prev_unallocated.index()].next_unallocated = next_unallocated;
}
if next_unallocated.is_some() {
self.items[next_unallocated.index()].prev_unallocated = prev_unallocated;
}
self.items[item_idx.index()].next = next_next;
self.items[item_idx.index()].width += next_width;
@ -367,6 +414,8 @@ impl AtlasAllocator {
if prev.is_some() && !self.items[prev.index()].allocated {
// Merge the item into the previous one.
// No need to add the item_idx to the "unallocated" list since it
// is getting merged into an already unallocated item.
self.items[prev.index()].next = next;
self.items[prev.index()].width += width;
@ -379,6 +428,15 @@ impl AtlasAllocator {
self.remove_item(item_idx);
prev = self.items[prev.index()].prev;
} else {
// Insert item_idx in the "unallocated" list.
let first = self.shelves[shelf.index()].first_unallocated;
if first.is_some() {
self.items[first.index()].prev_unallocated = item_idx;
}
self.items[item_idx.index()].next_unallocated = first;
self.items[item_idx.index()].prev_unallocated = ItemIndex::NONE;
self.shelves[shelf.index()].first_unallocated = item_idx;
}
if prev.is_none() && next.is_none() {
@ -536,12 +594,16 @@ impl AtlasAllocator {
prev_empty = shelf.is_empty;
let mut accum_w = 0;
let mut accum_unallocated_w = 0;
let mut prev_allocated = true;
let mut item_idx = shelf.first_item;
let mut prev_item_idx = ItemIndex::NONE;
while item_idx.is_some() {
let item = &self.items[item_idx.index()];
accum_w += item.width;
if !item.allocated {
accum_unallocated_w += item.width;
}
assert_eq!(item.prev, prev_item_idx);
@ -556,6 +618,24 @@ impl AtlasAllocator {
assert_eq!(accum_w, self.shelf_width);
// Traverse the shelf's unallocated list, validate it and check that it matches
// the amount of unallocated space we found from traversing the whole shelf.
accum_w = 0;
let mut item_idx = shelf.first_unallocated;
let mut prev_unallocated_idx = ItemIndex::NONE;
while item_idx.is_some() {
let item = &self.items[item_idx.index()];
assert!(!item.allocated);
assert_eq!(item.prev_unallocated, prev_unallocated_idx);
accum_w += item.width;
prev_unallocated_idx = item_idx;
item_idx = item.next_unallocated;
}
assert_eq!(accum_w, accum_unallocated_w, "items missing from the unallocated list?");
shelf_idx = shelf.next;
}
}

183
third_party/rust/etagere/src/bucketed.rs поставляемый
Просмотреть файл

@ -135,6 +135,69 @@ impl BucketedAtlasAllocator {
size2(w as i32, h as i32)
}
pub fn grow(&mut self, new_size: Size) {
assert!(new_size.width < u16::MAX as i32);
assert!(new_size.height < u16::MAX as i32);
let (new_width, new_height) = if self.flip_xy {
(new_size.height as u16, new_size.width as u16)
} else {
(new_size.width as u16, new_size.height as u16)
};
assert!(new_width >= self.width);
assert!(new_height >= self.height);
self.available_height += new_height - self.height;
self.width = new_width;
self.height = new_height;
if self.num_columns == 1 {
// Add as many new buckets as possible to the existing shelves.
let additional_width = self.width - self.column_width;
let len = self.shelves.len();
for shelf_index in 0..len {
let shelf = &self.shelves[shelf_index];
let mut x = self.column_width;
let bucket_width = shelf.bucket_width;
let max_new_buckets = (MAX_BIN_COUNT - self.buckets.len()) as u16;
let mut num_buckets_to_add = additional_width / bucket_width;
num_buckets_to_add = num_buckets_to_add.min(max_new_buckets);
let mut bucket_next = shelf.first_bucket;
for _ in 0..num_buckets_to_add {
let bucket = Bucket {
next: bucket_next,
x,
free_space: bucket_width,
refcount: 0,
shelf: shelf_index as u16,
generation: Wrapping(0),
item_count: 0,
};
x += bucket_width;
let bucket_index = self.add_bucket(bucket);
bucket_next = bucket_index;
}
self.shelves[shelf_index].first_bucket = bucket_next;
}
// Resize the existing column.
self.column_width = self.width;
} else {
// Add as many new columns as possible.
self.num_columns = self.width / self.column_width;
}
}
pub fn is_empty(&self) -> bool {
self.shelves.is_empty()
}
@ -235,7 +298,7 @@ impl BucketedAtlasAllocator {
/// How much space is available for future allocations.
pub fn free_space(&self) -> i32 {
(self.width * self.height) as i32 - self.allocated_space
(self.width as i32 * self.height as i32) - self.allocated_space
}
fn alloc_from_bucket(&mut self, shelf_index: usize, bucket_index: BucketIndex, width: u16) -> Option<Allocation> {
@ -274,6 +337,22 @@ impl BucketedAtlasAllocator {
Some(Allocation { id, rectangle })
}
fn add_bucket(&mut self, mut bucket: Bucket) -> BucketIndex {
let mut bucket_index = self.first_unallocated_bucket;
if bucket_index == BucketIndex::INVALID {
bucket_index = BucketIndex(self.buckets.len() as u16);
self.buckets.push(bucket);
} else {
let idx = bucket_index.to_usize();
bucket.generation = self.buckets[idx].generation + Wrapping(1);
self.first_unallocated_bucket = self.buckets[idx].next;
self.buckets[idx] = bucket;
}
bucket_index
}
fn add_shelf(&mut self, width: u16, height: u16) -> usize {
let can_add_column = self.current_column + 1 < self.num_columns;
@ -304,7 +383,7 @@ impl BucketedAtlasAllocator {
let mut x = self.current_column * self.column_width;
let mut bucket_next = BucketIndex::INVALID;
for _ in 0..num_buckets {
let mut bucket = Bucket {
let bucket = Bucket {
next: bucket_next,
x,
free_space: bucket_width,
@ -314,19 +393,9 @@ impl BucketedAtlasAllocator {
item_count: 0,
};
let mut bucket_index = self.first_unallocated_bucket;
x += bucket_width;
if bucket_index == BucketIndex::INVALID {
bucket_index = BucketIndex(self.buckets.len() as u16);
self.buckets.push(bucket);
} else {
let idx = bucket_index.to_usize();
bucket.generation = self.buckets[idx].generation + Wrapping(1);
self.first_unallocated_bucket = self.buckets[idx].next;
self.buckets[idx] = bucket;
}
let bucket_index = self.add_bucket(bucket);
bucket_next = bucket_index;
}
@ -724,6 +793,94 @@ fn test_coalesce_shelves() {
assert_eq!(atlas.allocated_space(), 0);
}
#[test]
fn grow_vertically() {
let mut atlas = BucketedAtlasAllocator::new(size2(256, 256));
// Allocate 7 shelves (leaving 32px of remaining space on top).
let mut ids = Vec::new();
for _ in 0..7 {
for _ in 0..8 {
ids.push(atlas.allocate(size2(32, 32)).unwrap().id)
}
}
// Free the first shelf.
for i in 0..8 {
atlas.deallocate(ids[i]);
}
// Free the 3rd and 4th shelf.
for i in 16..32 {
atlas.deallocate(ids[i]);
}
// Not enough space left in existing shelves and above.
// even coalescing is not sufficient.
assert!(atlas.allocate(size2(70, 70)).is_none());
// Grow just enough vertically to fit the previous region
atlas.grow(size2(256, 256 + 70 - 32));
// Allocation should succeed now
assert!(atlas.allocate(size2(70, 70)).is_some());
}
#[test]
fn grow_horizontally() {
let mut atlas = BucketedAtlasAllocator::new(size2(256, 256));
// Allocate 7 shelves (leaving 32px of remaining space on top).
let mut ids = Vec::new();
for _ in 0..7 {
for _ in 0..8 {
ids.push(atlas.allocate(size2(32, 32)).unwrap().id)
}
}
// Free the first shelf.
for i in 0..8 {
atlas.deallocate(ids[i]);
}
// Free the 3rd and 4th shelf.
for i in 16..32 {
atlas.deallocate(ids[i]);
}
// Not enough space left in existing shelves and above.
// even coalescing is not sufficient.
assert!(atlas.allocate(size2(512, 32)).is_none());
// Grow just enough horizontally to add more buckets
atlas.grow(size2(256 * 2, 256));
// Allocation should succeed now
assert!(atlas.allocate(size2(512, 32)).is_some());
}
#[test]
fn grow_to_fit_allocation() {
let mut atlas = BucketedAtlasAllocator::new(size2(32, 32));
// Allocate a shelve to make sure we have a non-empty atlas to test the update.
atlas.allocate(size2(32, 32)).unwrap();
// Try to make a big allocation that doesn't fit.
let big_allocation = size2(256, 256);
assert!(atlas.allocate(big_allocation).is_none());
// Grow to make enough space for the wanted allocation plus the original shelf.
atlas.grow(size2(256, 32 + 256));
// Adding to the original shelf should succeed.
assert!(atlas.allocate(size2(32, 32)).is_some());
// Big allocation should also succeed now.
assert!(atlas.allocate(big_allocation).is_some());
}
#[test]
fn columns() {
let mut atlas = BucketedAtlasAllocator::with_options(size2(64, 64), &AllocatorOptions {

2
third_party/rust/etagere/src/lib.rs поставляемый
Просмотреть файл

@ -8,7 +8,7 @@
//! This crate provides two implementations of the shelf packing algorithm for *dynamic*
//! texture atlas allocation (dynamic here means supporting both allocation and deallocation).
//!
//! [A thousand ways to pack the bin](http://pds25.egloos.com/pds/201504/21/98/RectangleBinPack.pdf)
//! [A thousand ways to pack the bin](https://github.com/juj/RectangleBinPack/blob/master/RectangleBinPack.pdf)
//! is a good resource to learn about rectangle packing algorithms, although it does not not cover
//! deallocation which complicates the problem space a fair bit.
//!