Tedd.RectanglePacker
0.1.7
dotnet add package Tedd.RectanglePacker --version 0.1.7
NuGet\Install-Package Tedd.RectanglePacker -Version 0.1.7
<PackageReference Include="Tedd.RectanglePacker" Version="0.1.7" />
<PackageVersion Include="Tedd.RectanglePacker" Version="0.1.7" />
<PackageReference Include="Tedd.RectanglePacker" />
paket add Tedd.RectanglePacker --version 0.1.7
#r "nuget: Tedd.RectanglePacker, 0.1.7"
#:package Tedd.RectanglePacker@0.1.7
#addin nuget:?package=Tedd.RectanglePacker&version=0.1.7
#tool nuget:?package=Tedd.RectanglePacker&version=0.1.7
Tedd.RectanglePacker
High-performance rectangle packer and texture-atlas builder for .NET 10 and .NET 11. In the published comparison benchmarks, it is the fastest rectangle packer on the measured size-search workloads; results depend on workload and hardware.
- Packing: MaxRects (best short side, long side, area, bottom-left, contact point) and Skyline (bottom-left, min-waste), with a parallel atlas-size search that finds a small atlas and returns the same result for any thread count.
- Inputs: raw pixel buffers (
RawImage: width, height, format, stride, bytes) or Tedd.FIC images (FicImage). - Outputs: a raw pixel buffer (
RawImageBuffer) or a FIC container. - Padding: optional per-sprite padding filled as
Transparent,Extrude(clamp-to-edge),MirrororWrap, plusSpacingbetween sprites andBorderPaddingaround the atlas. - Zero allocation: every buffer can be lent by the caller. You can compute the layout first and then compose straight into your own memory (a pooled array, GPU upload buffer, memory-mapped file, ...). Repeated calls on a reused
AtlasBuilderallocate nothing, apart from what Tedd.FIC's encoder allocates internally. - Fast writes: the atlas is written in destination scanline order, so each atlas byte is written once, left to right. Bands of rows are split across threads. A per-sprite block-copy strategy is also available.
- Pixel formats:
Gray8,Rgb24,Bgr24,Rgba32,Bgra32,Argb32,Abgr32. Every input may use a different format; pixels are converted while they are written.
Install
dotnet add package Tedd.RectanglePacker
Quick start
using Tedd.RectanglePacker;
var builder = new AtlasBuilder(new AtlasOptions
{
Padding = 2, // pixels around every sprite...
PaddingMode = PaddingMode.Extrude, // ...filled by repeating the edge (no bilinear bleeding)
Spacing = 1, // transparent gap between padded sprites
});
RawImage[] sprites =
[
new RawImage(64, 64, PixelFormat.Rgba32, rgbaBytes),
new RawImage(32, 48, PixelFormat.Bgra32, bgraBytes, stride: 32 * 4 + 16),
];
RawAtlas atlas = builder.Build(sprites, PixelFormat.Rgba32);
// atlas.Image -> RawImageBuffer (Width, Height, Format, Stride, Pixels)
// atlas.Layout -> Width, Height, Rects (content rectangle of sprite i at index i)
Write directly into your own buffer
The size of the atlas is known as soon as the layout is packed. Pass a provider to receive that size and return the memory to write into:
RawAtlas atlas = builder.Build(sprites, PixelFormat.Rgba32,
output: (width, height, format) => new RawImageBuffer(width, height, format, myStagingMemory, stride: myRowPitch));
Or split the two phases yourself:
AtlasLayout layout = builder.Pack(sprites); // sizes only; no pixels touched
var target = new RawImageBuffer(layout.Width, layout.Height, PixelFormat.Rgba32, rentedMemory);
builder.Compose(sprites, layout.Rects.Span, target); // writes every atlas byte once
Zero-allocation steady state
var rects = new PackedRect[sprites.Length]; // lent layout storage
RawImageBuffer target = default;
AtlasPixelBufferProvider provider = (w, h, f) =>
target.Width == w && target.Height == h ? target : (target = RawImageBuffer.Allocate(w, h, f));
for (int frame = 0; ; frame++)
builder.Build(sprites, PixelFormat.Rgba32, provider, rects); // no allocations after warm-up
TryPack(ReadOnlySpan<RectSize>, Span<PackedRect>, out w, out h) packs sizes into caller memory without throwing.
Tedd.FIC input and output
FicImage[] inputs = files.Select(bytes => new FicImage(bytes)).ToArray();
RawAtlas raw = builder.Build(inputs, PixelFormat.Rgba32); // decoded in parallel into pooled (or lent) scratch
FicAtlas fic = builder.BuildFic(sprites, new FicEncodeOptions { Effort = FicEffort.Fast });
File.WriteAllBytes("atlas.fic", fic.Data.ToArray());
FicImage(data) reads the dimensions from the header. For containers with outer compression, reading the header costs a decompression, so pass the dimensions with FicImage(data, width, height, channels) when you already know them. Decode scratch can be lent with the decodeScratch argument (AtlasBuilder.GetDecodeScratchLength). Encoded output can be written into your memory through a FicBufferProvider.
Low-level online packers
For dynamic atlases such as glyph caches, use the fixed-bin packers directly:
var packer = new MaxRectsPacker(1024, 1024);
if (packer.TryInsert(18, 24, MaxRectsHeuristic.BestShortSideFit, out PackedRect slot)) { /* upload glyph at slot */ }
var skyline = new SkylinePacker(1024, 1024); // cheaper per insert
Options
| Option | Default | Meaning |
|---|---|---|
MaxWidth, MaxHeight |
16384 | Largest allowed atlas. |
PowerOfTwo, Square |
false | Round the atlas to powers of two and/or force a square atlas. |
FixedSize |
false | Use exactly MaxWidth x MaxHeight. |
Padding |
0 | Pixels added on each side of every sprite. |
PaddingMode |
Extrude |
Transparent (background), Extrude (clamp), Mirror (GL_MIRRORED_REPEAT), Wrap (GL_REPEAT). |
Spacing |
0 | Background pixels between neighbouring padded sprites. |
BorderPadding |
0 | Background pixels along the atlas edges. |
Algorithm |
MaxRects |
MaxRects (densest) or Skyline (fastest). |
Effort |
Balanced |
Fast: one pass. Balanced: several widths and sort orders, refined. Thorough: many widths, orders and heuristics. |
Composition |
Auto |
Scanline (destination order) or PerSprite (source order). |
ClearBackground |
true | Zero uncovered pixels. Turn off only when the destination is already zeroed. |
MaxDegreeOfParallelism |
all cores | Threads for packing search, FIC decoding and composition. |
VerifyFicChecksum |
true | Verify FIC pixel checksums while decoding. |
ScratchPool |
ArrayPool<byte>.Shared |
Pool for internal scratch buffers. |
Content rectangles (PackedRect) exclude padding. Sprite i occupies rects[i], and its padding ring surrounds that rectangle. The layout is deterministic: the same inputs and options produce the same atlas regardless of thread count.
Performance
These are medians of interleaved runs of AtlasBuilder.Build with default options (MaxRects, Balanced effort, all 32 threads) and lent output buffers, on an AMD Ryzen 9 5950X with .NET 11 RC1. Every layout and atlas byte is identical to the first implementation. Only the speed changed:
| Workload | First implementation | Optimized | Change |
|---|---|---|---|
| 2000 RGBA sprites, 16-64 px | 126.7 ms | 13.1 ms | -90% |
| 4000 Gray8 glyphs | 189.6 ms | 16.3 ms | -91% |
| 600 mixed sprites, 8-400 px | 11.1 ms | 2.7 ms | -76% |
| 600 RGB24 sprites into RGBA | 11.2 ms | 2.5 ms | -78% |
| 600 FIC sprites | 12.6 ms | 4.1 ms | -68% |
| 24 large textures (44 MB atlas) | 3.2 ms | 2.4 ms | -23% |
| 4096 grid tiles of 16/32/64 px | 4.0 ms | 3.8 ms | -5% |
On very regular inputs such as tile grids, where every free list holds only a handful of rectangles, the single-threaded packer is about 12% slower than the first implementation. Multi-threaded builds of the same input are still faster.
The main gains come from:
- MaxRects free list: stored as edge arrays and scanned with AVX2. A proven edge-coincidence filter replaces the full containment prune.
- FIC decoding: overlapped with packing.
- Pixel conversion: pshufb kernels.
- Streaming stores: non-temporal stores for atlases larger than the last-level cache.
The research report documents the method, all 30 hypotheses (rejected ones included), the raw data and the reproduction commands.
Documentation and benchmarks
- Project site: https://tedd.github.io/Tedd.RectanglePacker/
- Comparison with other .NET packers and image libraries: https://tedd.github.io/Tedd.RectanglePacker/benchmarks.html. The method, command line and full result tables are in BENCHMARKS.md.
- Research holds the measured optimization investigation behind the current implementation: the benchmark rig, hypothesis ledger, raw results and report.
Build
dotnet build src/Tedd.RectanglePacker.slnx -c Release
dotnet test src/Tedd.RectanglePacker.slnx -c Release
The benchmark harness lives in src/Tedd.RectanglePacker.Benchmarks. Run it with measure, hash or phases, or pass BenchmarkDotNet arguments.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.