etagere
A fast, dependency-light Rust library for dynamic 2D texture atlas allocation using the shelf packing algorithm.
Repository Health
Technical Analysis
Etagere is a Rust crate that implements dynamic 2D texture atlas allocation using the shelf-packing algorithm, the same general technique used by projects like Mozilla’s WebRender to batch GPU draw calls efficiently. It ships two allocator implementations: AtlasAllocator, which tracks each item individually and works to minimize fragmentation, and BucketedAtlasAllocator, which groups items into buckets for faster allocation and deallocation at some cost to packing efficiency — so callers can pick the trade-off that fits their workload, whether that’s dynamic glyph atlases or GPU texture batching.
The crate is no_std-compatible by default (with an opt-in std feature enabling SVG debugging output), supports optional serde serialization, alignment constraints, vertical shelf orientation, and multi-column layouts, and ships a small companion CLI tool for interactively experimenting with the packing algorithms.
What You Get
- AtlasAllocator - a per-item shelf-packing allocator that tracks each allocation individually and works to minimize fragmentation as items are added and removed.
- BucketedAtlasAllocator - a bucketed variant that groups items and only reclaims space once an entire bucket empties, trading some packing efficiency for faster allocate/deallocate performance on large item counts.
- Configurable AllocatorOptions - alignment constraints, vertical vs. horizontal shelf orientation, and multi-column splitting to tune packing behavior for a given workload.
- no_std support - the core crate builds without the standard library, with an opt-in
stdfeature for SVG dump/debug output. - Optional serde serialization - atlas state and IDs can be serialized via the
serializationfeature for persistence or cross-process use. - A companion CLI (
etagere-cmd) - an interactive command-line tool in thecli/workspace member for experimenting with and visualizing the packing algorithms.
Common Use Cases
- Dynamic glyph atlases - packing rasterized font glyphs into a shared GPU texture as text is rendered, reclaiming space as glyphs fall out of use.
- GPU draw-call batching - combining many small textures into one atlas so a renderer can issue fewer draw calls, as used by rendering engines like WebRender.
- Game engine sprite/texture packing - allocating space for sprites or UI textures at runtime rather than pre-baking a fixed atlas layout.
- FFI-driven graphics pipelines - the
ffifeature exposes a C-compatible API so non-Rust renderers can drive the same allocator.
Under The Hood
Architecture
Etagere is organized around two parallel allocator implementations that share a common shape but differ in bookkeeping strategy. AtlasAllocator (src/allocator.rs) maintains a list of shelves, each holding a row of allocated rectangles, and walks/merges free space on deallocation to control fragmentation; BucketedAtlasAllocator (src/bucketed.rs) instead groups items into fixed-size buckets within shelves and defers reclaiming a shelf’s space until every item in its bucket has been freed, with limited support for coalescing adjacent empty shelves. Both allocators expose the same AllocId/Allocation/Rectangle vocabulary defined in lib.rs, so callers can swap one for the other without changing call sites. An optional ffi module (src/ffi.rs) wraps the core API behind a #[repr(C)]-friendly surface for non-Rust consumers, and a small cli/ workspace crate built on clap and ron provides an interactive front end for exercising both allocators. Because there’s no external state beyond the allocator struct itself, changing the shelf/bucket data structures is the main lever for altering packing behavior — there’s no plugin or trait-based extension point.
Tech Stack
The crate targets Rust 2018 edition and is no_std by default via #![cfg_attr(not(feature = "std"), no_std)], pulling in alloc directly rather than assuming a full standard library. Its only required dependency is euclid (geometry primitives for points/sizes/boxes), with svg_fmt gated behind the std feature for SVG debug dumps and serde gated behind a serialization feature. The workspace also contains a cli member depending on clap 2.x and ron for a standalone command-line tool, and a fuzz/ directory for fuzz-testing the allocators. There’s no async runtime, database, or web framework involved — this is a pure computational library meant to be embedded in graphics/rendering codebases.
Code Quality
Both core modules carry substantial inline #[test] suites (a dozen-plus tests each in allocator.rs and bucketed.rs) covering basic allocation/deallocation, option handling, shelf coalescing, and edge cases, and CI (.github/workflows) runs cargo build and cargo test --all on both stable and nightly Rust. A fuzz/ crate is present for additional confidence beyond unit tests. The public API relies on Rust’s type system (newtype IDs, Option-returning fallible allocation) rather than panics for the common failure path, though some internal bookkeeping still uses unwrap/expect where invariants are assumed to hold. There’s no separate linter/formatter config beyond standard cargo fmt/clippy conventions implied by the ecosystem.
What Makes It Unique Rather than a general-purpose bin-packing library, Etagere is narrowly focused on the dynamic case — supporting deallocation, not just one-shot packing — which is the harder and less commonly solved variant of the shelf-packing problem. Offering two allocator strategies with the same API lets consumers benchmark fragmentation-minimizing behavior against bucketed throughput for their specific workload without switching libraries, and its no_std, FFI-ready design makes it usable from constrained or non-Rust graphics pipelines, which is a deliberate design choice most atlas-packing crates don’t make.