Skip to content

Atlas Manager

Shmellyorc edited this page Aug 31, 2026 · 6 revisions

The Atlas Manager is a texture atlasing system that automatically packs textures into larger texture pages to reduce draw calls and improve rendering performance. It handles page allocation, texture packing, LRU eviction, and fragmentation management.


Overview

Texture atlasing is a technique where multiple smaller textures are combined into a single larger texture. This reduces the number of draw calls because the renderer can draw many sprites from the same texture in a single batch.

Without Atlasing With Atlasing
One draw call per texture One draw call per atlas page
100 textures = 100 draw calls 100 textures = 1 draw call
Texture switches are expensive Texture switches are minimized
Poor batching efficiency Excellent batching efficiency

The Atlas Manager handles all of this automatically. You don't need to think about it. Just pack your textures and the system handles the rest.


Integration with SpriteBatcher

The SpriteBatcher uses the Atlas Manager by default. When you call any of the standard Draw or DrawText methods, the batcher automatically attempts to pack your texture into the atlas.

// This automatically uses the atlas
batcher.Draw(texture, position, Color.White);
batcher.Draw(texture, dstRect, srcRect, Color.White);
batcher.Draw(texture, position, srcRect, Color.White, rotation, scale, origin, effects, depth);

What happens behind the scenes:

  1. The batcher calls AtlasManager.TryPack() with your texture and source rectangle
  2. If the texture is successfully packed, it uses the atlas texture and packed rectangle
  3. If the texture cannot be packed, it falls back to using the original texture directly
  4. This all happens transparently. You don't need to change your code.

This means you can write your rendering code without thinking about atlasing. The engine handles it for you.


When to Bypass the Atlas

There are cases where you should bypass the atlas and use DrawBypassAtlas instead of the standard Draw methods.

Created Textures

When you create textures programmatically, they are not managed by the asset system and can fill up the atlas very quickly.

// Create a texture at runtime
var dynamicTexture = new Texture(new Vect2(256, 256), Color.Red);

// These textures will rapidly fill the atlas if using standard Draw
// Always use DrawBypassAtlas for created textures
batcher.DrawBypassAtlas(dynamicTexture, position, Color.White);

Render Targets

Render targets are textures that are updated every frame. Packing them into the atlas would cause constant churn and eviction.

// Get a render target from the pool
var renderTarget = RenderTarget.Get(1920, 1080);

// Use DrawBypassAtlas for render targets
batcher.DrawBypassAtlas(renderTarget.GetTexture(), position, Color.White);

// Return to pool
RenderTarget.Return(renderTarget);

Why You Should Bypass

Texture Type Use Standard Draw Use DrawBypassAtlas
Loaded assets (PNG, etc.) ✅ Yes ❌ No
Created textures ❌ No ✅ Yes
Render targets ❌ No ✅ Yes
Procedural textures ❌ No ✅ Yes
Atlas pages themselves ❌ No ✅ Yes
Large textures (> page size) ❌ No ✅ Yes

The Problem with Packing Created Textures

Created textures are not cached by the AssetManager. Every frame, they could be treated as new textures and packed into the atlas. This causes:

  1. Rapid atlas filling - Created textures take up space in the atlas
  2. High eviction rates - Other textures get evicted to make room
  3. Performance degradation - Constant packing and eviction is expensive
  4. Memory waste - The atlas fills up with textures that shouldn't be there

By using DrawBypassAtlas, you skip the atlas entirely and draw directly from the original texture.


How It Works

Pages

The atlas is divided into pages. Each page is a fixed-size render texture (default: 2048x2048 pixels). Textures are packed into these pages until they are full, then the next page is used.

Page 0                    Page 1                    Page 2
┌────────────────────┐   ┌────────────────────┐   ┌────────────────────┐
│ [Tex A] [Tex B]    │   │ [Tex G] [Tex H]    │   │ [Tex M] [Tex N]    │
│ [Tex C]            │   │ [Tex I]            │   │ [Tex O] [Tex P]    │
│ [Tex D] [Tex E]    │   │ [Tex J] [Tex K]    │   │                    │
│ [Tex F]            │   │ [Tex L]            │   │                    │
└────────────────────┘   └────────────────────┘   └────────────────────┘

Packing Algorithms

The Atlas Manager supports multiple packing algorithms through the IAtlasPacker interface. This interface allows you to implement your own custom packing algorithms if the built-in ones don't meet your needs.

IAtlasPacker Interface

public interface IAtlasPacker
{
    bool TryPack(int width, int height, out Rect2 packedRect);
    void Clear();
    List<(Rect2 OldRect, Rect2 NewRect)> Defrag();
    void Free(Rect2 rect);
    float Fragmentation { get; }
    int UsedSpace { get; }
    int TotalSpace { get; }
}

Creating a Custom Packer

You can implement your own packing algorithm by creating a class that implements IAtlasPacker.

public class MyCustomPacker : IAtlasPacker
{
    private readonly int _width;
    private readonly int _height;
    private List<Rect2> _freeRects;
    private List<Rect2> _packedRects;
    private int _usedSpace;

    public MyCustomPacker(int width, int height)
    {
        _width = width;
        _height = height;
        _freeRects = new List<Rect2> { new Rect2(0, 0, width, height) };
        _packedRects = new List<Rect2>();
        _usedSpace = 0;
    }

    public bool TryPack(int width, int height, out Rect2 packedRect)
    {
        // Your custom packing logic here
        // Find the best free rectangle, place the texture, update free space
    }

    public void Clear()
    {
        _freeRects.Clear();
        _freeRects.Add(new Rect2(0, 0, _width, _height));
        _packedRects.Clear();
        _usedSpace = 0;
    }

    public List<(Rect2 OldRect, Rect2 NewRect)> Defrag()
    {
        // Your custom defragmentation logic here
        // Repack all textures, return move list for updating texture data
    }

    public void Free(Rect2 rect)
    {
        // Your custom free logic here
        // Remove the rectangle, merge free space
    }

    public float Fragmentation => 1f - ((float)_usedSpace / TotalSpace);
    public int UsedSpace => _usedSpace;
    public int TotalSpace => _width * _height;
}

Registering a Custom Packer

Once you have implemented your custom packer, you can register it with the engine through GameSettings.

var settings = GameSettings.Instance
    .SetAtlasPacker(typeof(MyCustomPacker))
    .Build();

Built-in Packing Algorithms

The Atlas Manager includes two built-in packing algorithms.

GuillotinePacker

The Guillotine algorithm maintains a list of free rectangles and selects the best fit for each texture.

Feature Description
Strategy Best area fit (smallest free rectangle that fits)
Search Time O(n) where n is the number of free rectangles
Fragmentation Tends to create more fragmentation over time
Best For Textures of varying sizes

How it works:

  1. Maintains a list of free rectangles in the atlas
  2. When packing, finds the free rectangle with the smallest area that fits the texture
  3. Splits the chosen rectangle into remaining free space
  4. When freeing, merges adjacent free rectangles to reduce fragmentation
  5. Defragmentation repacks all textures to optimize space

SkylinePacker

The Skyline algorithm maintains a skyline of the topmost occupied pixels and places textures in the lowest available position.

Feature Description
Strategy Lowest Y position that fits the texture width
Search Time O(n²) where n is the number of skyline nodes
Fragmentation Less fragmentation than Guillotine
Best For Textures of similar sizes

How it works:

  1. Maintains a skyline (topmost occupied Y position for each X coordinate)
  2. When packing, finds the lowest Y position that can accommodate the texture width
  3. Places the texture at that position and updates the skyline
  4. When freeing, inserts free space back into the skyline
  5. Defragmentation repacks all textures in order of position

AtlasManager

The AtlasManager is the main class that manages the entire atlasing system.

Singleton Access

var atlas = AtlasManager.Instance;

Packing Textures

// Pack a texture into the atlas
if (atlas.TryPack(sfTexture, srcRect, out var packedRect, out var pageId))
{
    // Texture was packed at packedRect on page pageId
    var pageTexture = atlas.GetPageTexture(pageId);
    
    // Draw using the atlas texture
    batcher.DrawBypassAtlas(pageTexture, dstRect, packedRect, Color.White);
}

Getting Page Textures

// Get the texture for a specific page
SFTexture pageTexture = atlas.GetPageTexture(pageId);

Metrics

// Get atlas metrics
var metrics = atlas.GetMetrics();

Console.WriteLine($"Pages: {metrics.UsedPages}/{metrics.TotalPages}");
Console.WriteLine($"Usage: {metrics.PercentageFull:F1}%");
Console.WriteLine($"Textures: {metrics.TextureCount}");
Console.WriteLine($"Evictions: {metrics.EvictionCount}");

Processing Defragmentation

// Process pending defragmentation moves (call once per frame)
bool stillDefragging = atlas.ProcessPendingDefragMoves(maxMovesPerFrame: 10);

Clearing

// Clear all atlas data
atlas.Clear();

LRU Eviction

When the atlas is full, the Atlas Manager evicts the least recently used textures to make room for new ones.

Step What Happens
1 Atlas is full
2 Least recently used texture is identified
3 Texture is removed from the atlas page
4 Space is freed for the new texture
5 New texture is packed into the freed space

Evictions are tracked in the AtlasMetrics structure.


Defragmentation

Over time, packing and freeing textures can leave fragmented free space that cannot be used for larger textures. Defragmentation rearranges packed textures to consolidate free space into larger contiguous blocks.

When Defragmentation Triggers

Condition Action
No free space found in any page Find the most fragmented page
Fragmentation > threshold (default: 30%) Defragment that page
Space is freed Continue

Defragmentation Process

  1. Identify the most fragmented page
  2. Get all textures on that page
  3. Repack them into a clean atlas page
  4. Queue move operations for processing over multiple frames
  5. Update packed positions in the map

Processing Moves

Defragmentation is spread across multiple frames to avoid frame rate hitches.

// Process up to 10 moves per frame
atlas.ProcessPendingDefragMoves(10);

Configuration

Atlas settings are configured through GameSettings.

var settings = GameSettings.Instance
    .SetAtlasPageSize(2048)                // Page size in pixels
    .SetAtlasPageCount(4)                   // Number of pages
    .SetAtlasDefragThreshold(0.3f)          // 30% fragmentation triggers defrag
    .SetAtlasDefragMovesPerFrame(10)       // Moves per frame
    .SetAtlasPacker(typeof(SkylinePacker))  // Packing algorithm (or custom)
    .Build();

Settings Reference

Setting Description Default
AtlasPageSize Size of each atlas page in pixels 2048
AtlasPageCount Number of atlas pages 4
AtlasDefragThreshold Fragmentation percentage that triggers defrag 30%
AtlasDefragMovesPerFrame Number of texture moves per frame during defrag 10
AtlasPacker Packing algorithm to use (built-in or custom) SkylinePacker

Performance

Draw Call Reduction

Scenario Without Atlas With Atlas
50 textures 50 draw calls 1 draw call
500 textures 500 draw calls 1-2 draw calls
5000 textures 5000 draw calls 3-5 draw calls

Memory Usage

Component Memory Usage
Atlas page (2048x2048) 16 MB
Atlas page (4096x4096) 64 MB
Texture data (in packer) Minimal

Defragmentation Performance

Metric Value
Moves per frame Configurable (default: 10)
Time per move ~0.1-0.5 ms
Total defrag time Spread across frames

Examples

Basic Usage with SpriteBatcher

public class MyGame : Game
{
    private SpriteBatcher _batcher;
    private Texture _playerTexture;
    private Texture _renderTargetTexture;

    protected override void OnEnter()
    {
        _batcher = new SpriteBatcher();
        _playerTexture = AssetManager.Instance.Load<Texture>("player.png");
        
        // Create a render target
        var renderTarget = RenderTarget.Get(256, 256);
        _renderTargetTexture = renderTarget.GetTexture();
    }

    protected override void OnDraw(FrameTime frameTime)
    {
        _batcher.Begin(SortMode.BackToFront);
        
        // This automatically uses the atlas
        _batcher.Draw(_playerTexture, new Vect2(100, 100), Color.White);
        
        // This bypasses the atlas (render target)
        _batcher.DrawBypassAtlas(_renderTargetTexture, new Vect2(200, 100), Color.White);
        
        _batcher.End();
    }
}

Custom Packer Implementation

// Implement a custom packer for a specific use case
public class MyCustomPacker : IAtlasPacker
{
    // Your custom implementation here
}

// Register it in settings
var settings = GameSettings.Instance
    .SetAtlasPacker(typeof(MyCustomPacker))
    .Build();

Monitoring Atlas Usage

public void CheckAtlasHealth()
{
    var metrics = AtlasManager.Instance.GetMetrics();
    
    if (metrics.PercentageFull > 90f)
    {
        Console.WriteLine("Atlas is nearly full! Consider increasing page count.");
    }
    
    if (metrics.EvictionCount > 100)
    {
        Console.WriteLine("High eviction count. Atlas may be too small.");
    }
}

Summary

Feature Description
Texture Atlasing Combines textures into larger pages
Automatic Packing Textures are packed automatically
SpriteBatcher Integration Uses atlas by default
Bypass Support Use DrawBypassAtlas for render targets and created textures
Custom Packer Support Implement IAtlasPacker for your own algorithms
Multiple Built-in Algorithms Guillotine and Skyline packing
LRU Eviction Least recently used textures are evicted
Defragmentation Fragmented space is consolidated
Page Management Multiple pages are managed automatically
Metrics Usage statistics are tracked

Back to Home

Clone this wiki locally