High-performance rectangle packing for .NET 10 and 11

Pack rectangles.
Compose the atlas.

Tedd.RectanglePacker places rectangles with the MaxRects or Skyline algorithm and searches for a small atlas size in parallel. The atlas builder then writes sprite pixels, with optional extrude, mirror or wrap padding, into a buffer that the caller allocates. Inputs are raw pixel views in seven byte layouts or Tedd.FIC containers.

Fastest measured atlas-size search among the compared .NET libraries on five published workloads.

dotnet add package Tedd.RectanglePacker
  • .NET 10 and 11
  • No rotation
  • Deterministic layouts
  • MIT
Tedd.RectanglePacker · Atlas layoutmeasured output
ATLAS LAYOUT 1,000 sprites · 8–128 px · default options
AtlasBuilder.TryPack

ATLAS1,833 × 2,567pixels

OCCUPANCY98.8 %sprite area ÷ atlas area

SEARCH TIME3.65 msMaxRects Balanced · 32 threads

PLACEMENTSEvery rectangle, drawn to scale
1833 × 2567
INPUT 0 layout.Rects.Span[0] (1397, 367) · 86 × 118
Measured, not illustrated. AMD Ryzen 9 5950X 16-Core Processor · .NET 11.0.0-rc.1.26425.128

MaxRects and SkylineFive MaxRects and two Skyline placement rules

Parallel size searchThe same layout for any thread count

Edge paddingTransparent, extrude, mirror and wrap

Caller-owned memoryLayouts and pixels written into lent buffers

Layout contract

Rectangles come back in input order, without padding.

The layout is a width, a height and one rectangle per input. It carries no references to the images, so it can be stored, reused, or computed before any pixels exist.

Ordering

rects[i] belongs to sizes[i]

Rectangles are returned in input order, whatever order the search packed them in. Empty inputs are reported at (0, 0). Rectangles are never rotated.

Content rectangle

PackedRect excludes padding

Each rectangle is the sprite's content area. Padding surrounds it, Spacing separates padded cells, and BorderPadding keeps the atlas edges free. Texture coordinates can be taken from the rectangle directly.

Cell size

w + 2 × padding + spacing

Sprites are packed as cells of this width (and the corresponding height). The trailing spacing of the last row and column falls outside the atlas, so no spacing is wasted at the edges.

Pixel layout

Rgba32: red at byte 0

Format names list byte order in memory, independent of processor endianness: Gray8, Rgb24, Bgr24, Rgba32, Bgra32, Argb32, Abgr32. Every image view has its own stride.

Pipeline

Layout and pixels are separate steps.

Packing needs only sizes. Composition needs the layout, the source pixels and a destination. The one-shot Build methods run both steps and ask a callback for the destination once its size is known.

  1. 01

    Measure

    RectSize / RawImage / FicImage

    Supply sizes, raw pixel views, or FIC containers. A FicImage reads its dimensions from the header without decoding pixels.

  2. 02

    Pack

    AtlasBuilder.TryPack

    Search candidate widths, sort orders and heuristics, and write the winning rectangles into a caller-provided Span<PackedRect>.

  3. 03

    Compose

    AtlasBuilder.Compose

    Convert each source to the atlas format, write padding, and clear the background, into a RawImageBuffer over any memory.

  4. 04

    Encode (optional)

    AtlasBuilder.BuildFic

    Stage the composed atlas in pooled memory and encode it as a Tedd.FIC container, RGB or RGBA.

Capabilities

Placement, size search, padding and memory control.

The low-level packers place rectangles in a bin of fixed size. The atlas builder adds the size search, padding and pixel composition on top of them.

⌕

Atlas size search

Three effort levels

Fast runs one pass at an estimated width. Balanced and Thorough try more widths, sort orders and heuristics in parallel. The winner is chosen by a fixed rule, so the result does not depend on thread scheduling.

⊡

Size constraints

Bounds, powers of two, squares

Limit the atlas with MaxWidth and MaxHeight (16,384 by default, 65,536 at most), round to powers of two, force a square, or pack into an exact FixedSize.

⧉

Padding modes

Transparent, extrude, mirror, wrap

Extrude repeats edge pixels outward, which prevents colour bleeding under bilinear filtering. Mirror reflects across the edge. Wrap continues from the opposite edge for tiling textures.

⇆

Pixel formats

Per-image conversion

Sources may use different formats. Missing alpha becomes 255, Gray8 expands to RGB, and colour converts to Gray8 with integer Rec. 601 luma.

≡

Composition order

Scanline or per sprite

Scanline writes every atlas row once, left to right, in parallel bands. PerSprite clears the background and then copies each sprite as a block, in parallel by sprite. Auto chooses a strategy.

⌁

Allocation control

Lent buffers for rectangles and pixels

TryPack writes into a caller span, and Compose into a RawImageBuffer over pooled, pinned, native or GPU-mapped memory. With lent buffers, repeated calls allocate nothing apart from allocations inside Tedd.FIC's decoder and encoder.

◫

FIC input and output

Decode during composition

FicImage inputs are decoded into pooled or lent scratch memory while the atlas is written, with optional checksum verification. BuildFic encodes the finished atlas.

⋮⋮

Threading

Bounded worker count

MaxDegreeOfParallelism bounds the threads used for searching, decoding and composing. Builder and packer instances keep scratch storage between calls and are not thread-safe; use one per thread.

Two-phase use

Allocate the atlas after the size is known.

Pack returns the atlas size and the rectangles. Compose then writes into any buffer with that size, such as a pooled array or a mapped GPU upload buffer.

  • Mixed formats. Sources are converted to the destination format while they are copied.
  • Background. Uncovered pixels are cleared to zero unless ClearBackground is disabled for a buffer that is already zeroed.
  • One-shot alternative. Build packs and composes in one call and requests the buffer through an AtlasPixelBufferProvider.
Atlas.csC#
var builder = new AtlasBuilder(new AtlasOptions
{
    Padding = 2,
    PaddingMode = PaddingMode.Extrude,
    Spacing = 1,
    PowerOfTwo = true,
});

// Views over decoded pixels; formats may differ per sprite.
RawImage[] sprites = LoadSprites();

// 1. Layout: one rectangle per sprite, in input order.
AtlasLayout layout = builder.Pack(sprites);

// 2. Pixels: compose into memory the caller owns.
byte[] pixels = ArrayPool<byte>.Shared.Rent(
    layout.Width * layout.Height * 4);
var atlas = new RawImageBuffer(
    layout.Width, layout.Height, PixelFormat.Rgba32, pixels);
builder.Compose(sprites, layout.Rects.Span, atlas);

// Content rectangle of sprites[i], excluding padding.
PackedRect r = layout.Rects.Span[0];

Benchmarks

Measured against other .NET packers.

4 other .NET libraries were compared on atlas size search, 7 on packing into the same fixed bin, and 2 image libraries on pixel composition. Packing results were checked for overlaps; composition outputs matched the reference pixels.

4 of 5datasets where the default settings produced the densest atlas of all size-searching libraries tested
57.6×speed of Fast effort versus the quickest other size search, geometric mean over 5 datasets (range 19.8× to 347×); its atlas was at least as dense on 5 of 5
12.6×speed of MaxRects BestShortSideFit versus the quickest other implementation of that heuristic, one thread in the same fixed bin; geometric mean over 5 datasets
2.9×composition throughput with 32 threads versus SkiaSharp drawing the same layout; on one thread the ratio is 1.6×

AMD Ryzen 9 5950X 16-Core Processor, .NET 11.0.0-rc.1.26425.128. Results depend on the input set and hardware.

All results, method and raw data →

Entry points

Use the smallest API that covers the task.

MaxRectsPackerPlaces rectangles one at a time into a bin of fixed size. Suits online allocation, for example glyphs added to a cache texture as they are first used.
SkylinePackerThe same interface with lower cost per insertion. Space below an overhang is not reused.
AtlasBuilder.TryPackChooses the atlas size for a complete set of rectangles and writes the layout into a caller span without allocating.
AtlasBuilder.ComposeWrites pixels for an existing layout into a caller buffer, including padding, format conversion and background.
AtlasBuilder.BuildPacks and composes in one call. The destination comes from a provider callback or a new array.
AtlasBuilder.BuildFicPacks, composes and encodes the atlas as a Tedd.FIC container.

Operational constraints

What the library does not do.

Rectangles are never rotated. Output is a single atlas: when the inputs do not fit within the configured maximum size, Pack throws InvalidOperationException and TryPack returns false. There is no multi-page output.

The library does not decode image files such as PNG or JPEG. Callers pass decoded pixels as RawImage views or Tedd.FIC containers as FicImage. It does not trim transparent borders, premultiply alpha, or merge identical sprites.

The package targets .NET 10 and 11. AtlasBuilder, MaxRectsPacker and SkylinePacker instances are not thread-safe.

Read the full documentation ↗

Tedd.RectanglePacker

Compute the layout.
Write into your buffer.

Install the package, compute a layout, and compose the atlas into the buffer your renderer already uses.