-
Notifications
You must be signed in to change notification settings - Fork 1
Atlas Manager
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.
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.
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:
- The batcher calls
AtlasManager.TryPack()with your texture and source rectangle - If the texture is successfully packed, it uses the atlas texture and packed rectangle
- If the texture cannot be packed, it falls back to using the original texture directly
- 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.
There are cases where you should bypass the atlas and use DrawBypassAtlas instead of the standard Draw methods.
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 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);| 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 |
Created textures are not cached by the AssetManager. Every frame, they could be treated as new textures and packed into the atlas. This causes:
- Rapid atlas filling - Created textures take up space in the atlas
- High eviction rates - Other textures get evicted to make room
- Performance degradation - Constant packing and eviction is expensive
- 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.
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] │ │ │
└────────────────────┘ └────────────────────┘ └────────────────────┘
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.
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; }
}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;
}Once you have implemented your custom packer, you can register it with the engine through GameSettings.
var settings = GameSettings.Instance
.SetAtlasPacker(typeof(MyCustomPacker))
.Build();The Atlas Manager includes two built-in packing algorithms.
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:
- Maintains a list of free rectangles in the atlas
- When packing, finds the free rectangle with the smallest area that fits the texture
- Splits the chosen rectangle into remaining free space
- When freeing, merges adjacent free rectangles to reduce fragmentation
- Defragmentation repacks all textures to optimize space
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:
- Maintains a skyline (topmost occupied Y position for each X coordinate)
- When packing, finds the lowest Y position that can accommodate the texture width
- Places the texture at that position and updates the skyline
- When freeing, inserts free space back into the skyline
- Defragmentation repacks all textures in order of position
The AtlasManager is the main class that manages the entire atlasing system.
var atlas = AtlasManager.Instance;// 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);
}// Get the texture for a specific page
SFTexture pageTexture = atlas.GetPageTexture(pageId);// 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}");// Process pending defragmentation moves (call once per frame)
bool stillDefragging = atlas.ProcessPendingDefragMoves(maxMovesPerFrame: 10);// Clear all atlas data
atlas.Clear();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.
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.
| 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 |
- Identify the most fragmented page
- Get all textures on that page
- Repack them into a clean atlas page
- Queue move operations for processing over multiple frames
- Update packed positions in the map
Defragmentation is spread across multiple frames to avoid frame rate hitches.
// Process up to 10 moves per frame
atlas.ProcessPendingDefragMoves(10);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();| 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 |
| 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 |
| Component | Memory Usage |
|---|---|
| Atlas page (2048x2048) | 16 MB |
| Atlas page (4096x4096) | 64 MB |
| Texture data (in packer) | Minimal |
| Metric | Value |
|---|---|
| Moves per frame | Configurable (default: 10) |
| Time per move | ~0.1-0.5 ms |
| Total defrag time | Spread across frames |
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();
}
}// 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();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.");
}
}| 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 |
- MathHelper
- FileHelper
- HashHelper
- JsonHelper
- MapHelper
- TextHelper
- SoundHelper
- AlignHelpers
- Instance Helper
- Enum Extensions
- String Extensions
- Int Extensions
- Float Extensions
- IEnumerable Extensions
- Random Extensions
- Sound Extensions
- Font Extensions