Add a simple quadtree-based texture allocator

This commit is contained in:
Patrick Walton 2020-01-31 14:38:01 +01:00
parent 2db43797c3
commit b8f622203a
2 changed files with 230 additions and 0 deletions

229
renderer/src/allocator.rs Normal file
View File

@ -0,0 +1,229 @@
// pathfinder/renderer/src/allocator.rs
//
// Copyright © 2020 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! A simple quadtree-based texture allocator.
use pathfinder_geometry::rect::RectI;
use pathfinder_geometry::vector::Vector2I;
#[derive(Debug)]
pub struct TextureAllocator {
root: TreeNode,
size: u32,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct TextureLocation {
pub rect: RectI,
}
#[derive(Debug)]
enum TreeNode {
EmptyLeaf,
FullLeaf,
// Top left, top right, bottom left, and bottom right, in that order.
Parent([Box<TreeNode>; 4]),
}
impl TextureAllocator {
#[inline]
pub fn new(size: u32) -> TextureAllocator {
// Make sure that the size is a power of two.
debug_assert_eq!(size & (size - 1), 0);
TextureAllocator { root: TreeNode::EmptyLeaf, size }
}
#[inline]
pub fn allocate(&mut self, requested_size: Vector2I) -> Option<TextureLocation> {
let requested_length = requested_size.x().max(requested_size.y()) as u32;
self.root.allocate(Vector2I::default(), self.size, requested_length)
}
#[inline]
pub fn free(&mut self, location: TextureLocation) {
let requested_length = location.rect.width() as u32;
self.root.free(Vector2I::default(), self.size, location.rect.origin(), requested_length)
}
#[inline]
pub fn is_empty(&self) -> bool {
match self.root {
TreeNode::EmptyLeaf => true,
_ => false,
}
}
}
impl TreeNode {
// Invariant: `requested_size` must be a power of two.
fn allocate(&mut self, this_origin: Vector2I, this_size: u32, requested_size: u32)
-> Option<TextureLocation> {
if let TreeNode::FullLeaf = *self {
// No room here.
return None;
}
if this_size < requested_size {
// Doesn't fit.
return None;
}
// Allocate here or split, as necessary.
if let TreeNode::EmptyLeaf = *self {
// Do we have a perfect fit?
if this_size == requested_size {
*self = TreeNode::FullLeaf;
return Some(TextureLocation {
rect: RectI::new(this_origin, Vector2I::splat(this_size as i32)),
});
}
// Split.
*self = TreeNode::Parent([
Box::new(TreeNode::EmptyLeaf),
Box::new(TreeNode::EmptyLeaf),
Box::new(TreeNode::EmptyLeaf),
Box::new(TreeNode::EmptyLeaf),
]);
}
// Recurse into children.
match *self {
TreeNode::Parent(ref mut kids) => {
let kid_size = this_size / 2;
if let Some(origin) = kids[0].allocate(this_origin, kid_size, requested_size) {
return Some(origin);
}
if let Some(origin) =
kids[1].allocate(this_origin + Vector2I::new(kid_size as i32, 0),
kid_size,
requested_size) {
return Some(origin);
}
if let Some(origin) =
kids[2].allocate(this_origin + Vector2I::new(0, kid_size as i32),
kid_size,
requested_size) {
return Some(origin);
}
if let Some(origin) =
kids[3].allocate(this_origin + Vector2I::splat(kid_size as i32),
kid_size,
requested_size) {
return Some(origin);
}
self.merge_if_necessary();
return None;
}
TreeNode::EmptyLeaf | TreeNode::FullLeaf => unreachable!(),
}
}
fn free(&mut self,
this_origin: Vector2I,
this_size: u32,
requested_origin: Vector2I,
requested_size: u32) {
if this_size <= requested_size {
if this_size == requested_size && this_origin == requested_origin {
*self = TreeNode::EmptyLeaf;
}
return;
}
let child_size = this_size / 2;
let this_center = this_origin + Vector2I::splat(child_size as i32);
let child_index;
let mut child_origin = this_origin;
if requested_origin.y() < this_center.y() {
if requested_origin.x() < this_center.x() {
child_index = 0;
} else {
child_index = 1;
child_origin = child_origin + Vector2I::new(child_size as i32, 0);
}
} else {
if requested_origin.x() < this_center.x() {
child_index = 2;
child_origin = child_origin + Vector2I::new(0, child_size as i32);
} else {
child_index = 3;
child_origin = this_center;
}
}
match *self {
TreeNode::Parent(ref mut kids) => {
kids[child_index].free(child_origin, child_size, requested_origin, requested_size);
self.merge_if_necessary();
}
TreeNode::EmptyLeaf | TreeNode::FullLeaf => unreachable!(),
}
}
fn merge_if_necessary(&mut self) {
match *self {
TreeNode::Parent(ref mut kids) => {
if kids.iter().all(|kid| {
match **kid {
TreeNode::EmptyLeaf => true,
_ => false,
}
}) {
*self = TreeNode::EmptyLeaf;
}
}
_ => {}
}
}
}
#[cfg(test)]
mod test {
use pathfinder_geometry::vector::Vector2I;
use quickcheck;
use std::u32;
use super::TextureAllocator;
#[test]
fn test_allocation_and_freeing() {
quickcheck::quickcheck(prop_allocation_and_freeing_work as
fn(u32, Vec<(u32, u32)>) -> bool);
fn prop_allocation_and_freeing_work(mut length: u32, mut sizes: Vec<(u32, u32)>) -> bool {
length = u32::next_power_of_two(length).max(1);
for &mut (ref mut width, ref mut height) in &mut sizes {
*width = (*width).min(length).max(1);
*height = (*height).min(length).max(1);
}
let mut allocator = TextureAllocator::new(length);
let mut locations = vec![];
for &(width, height) in &sizes {
let size = Vector2I::new(width as i32, height as i32);
if let Some(location) = allocator.allocate(size) {
locations.push(location);
}
}
for location in locations {
allocator.free(location);
}
assert!(allocator.is_empty());
true
}
}
}

View File

@ -23,6 +23,7 @@ pub mod paint;
pub mod post;
pub mod scene;
mod allocator;
mod builder;
mod sorted_vector;
mod tile_map;