Tutorial 25: SpriteSortMode and Layering

Graphics  ·  SpriteSortMode  ·  layerDepth  ·  Batching

ℹ

What you’ll learn

  • All five SpriteSortMode values and what each does to draw order.
  • How the layer-depth parameter interacts with the sort mode.
  • The performance cost of the sorting modes, and splitting work across several Begin/End pairs.

Before you start — Tutorial 21: SpriteBatch Deep Dive — sort mode is a Begin() parameter.

SpriteSortMode is the first parameter to SpriteBatch::Begin(). It controls two things: when accumulated sprites are submitted to the GPU and in what order they appear on screen. Choosing the right mode has visual and performance consequences. This tutorial covers all five values and shows practical multi-batch layering patterns. (There is no Begin(SpriteSortMode) overload on its own: whenever you name a sort mode you also pass a blend state, as every example below does. Begin() with no arguments means Deferred with AlphaBlend.)

SpriteSortMode enum — all five values

Deferred (default)

Sprites are queued in CPU memory until End() is called. At End() they are submitted to the GPU in draw-call order with consecutive sprites using the same texture grouped into a single draw call. This is the most efficient mode for most games.

spriteBatch_->Begin(SpriteSortMode::Deferred, BlendState::AlphaBlend);
// Draw 500 sprites from 5 textures, grouped by texture → 5 draw calls at End()
// (alternate the textures on every call and each change of texture starts a new draw call)
spriteBatch_->End();

Use when: You are drawing a fixed set of sprites and want the fewest draw calls. The layerDepth parameter is ignored.

Immediate

Each Draw() call is handed to the renderer immediately, and the render state (blend, sampler, depth-stencil and rasterizer) is applied once, at Begin(). On EasyGL, FNA3D, SOFTWARE and the per-sprite 2D renderers the sprite is then submitted before Draw() returns, so this is the mode where you can interleave your own GraphicsDevice work between individual Draw() calls; the Direct3D families, WEBGPU, VULKAN and SDL_GPU do not act on the mode for stock sprites and still batch one run per texture until a texture change or End() (see Immediate mode below the seam).

spriteBatch_->Begin(SpriteSortMode::Immediate, BlendState::AlphaBlend,
                    &SamplerState::PointClamp, nullptr, nullptr);
spriteBatch_->Draw(*texA, posA, Color::White);   // drawn right now
// ... your own GraphicsDevice calls can go here ...
spriteBatch_->Draw(*texB, posB, Color::White);   // drawn right now
spriteBatch_->End();

Two rules to know. First, choose the sampler through Begin(), not through the device: SpriteBatch writes its own sampler (LinearClamp unless you pass another) into sampler slot 0 whenever it applies its state, so end the batch and begin a new one to switch sampling mid-frame. Second, an Immediate batch cannot be active at the same time as any other SpriteBatch on the same device (and vice versa): Begin() throws InvalidOperationException.

Use when: You need per-sprite GPU state changes. Very slow for large sprite counts — use only when necessary.

Texture

Same as Deferred but at End() sprites are additionally sorted by texture identity before submission. This groups all sprites from the same texture together regardless of draw-call order, further reducing draw calls when sprites from different textures are interleaved in game logic.

spriteBatch_->Begin(SpriteSortMode::Texture, BlendState::AlphaBlend);
// Draw player (atlas A), enemy (atlas B), tree (atlas A), coin (atlas B)
// Sorted at End() → atlas A batch, atlas B batch = 2 draw calls
spriteBatch_->End();

Use when: Your game logic draws sprites from multiple textures in interleaved order and profiling shows texture-switch overhead. The layerDepth parameter is ignored; visual order equals draw-call order.

BackToFront

At End(), sprites are sorted by descending layerDepth (1.0 = back, 0.0 = front) then submitted. Sprites at depth 1.0 are drawn first; sprites at 0.0 are drawn last and appear on top. This produces correct alpha-blended layering without manual draw-call ordering:

spriteBatch_->Begin(SpriteSortMode::BackToFront, BlendState::AlphaBlend);

// Call Draw() in any order — layer depth controls final paint order
spriteBatch_->Draw(*playerTex_,  playerPos_,  std::nullopt, Color::White, 0,
                   Vector2::Zero, 1.0f, SpriteEffects::None, 0.1f);  // near front
spriteBatch_->Draw(*enemyTex_,   enemyPos_,   std::nullopt, Color::White, 0,
                   Vector2::Zero, 1.0f, SpriteEffects::None, 0.2f);
spriteBatch_->Draw(*backgroundTex_, Vector2::Zero, std::nullopt, Color::White, 0,
                   Vector2::Zero, 1.0f, SpriteEffects::None, 0.9f);  // far back

spriteBatch_->End();
// Render order: background (0.9), enemy (0.2), player (0.1)

Use when: You need pixel-correct depth ordering with alpha blending (e.g. a top-down RPG with trees that overlap characters).

⚠

Equal depths have no guaranteed order. Like XNA, the sorting modes use an unstable sort (CNA reproduces XNA’s own quicksort on purpose), so sprites that share the same layerDepth may come out in a different relative order than you submitted them, and that order can change when other sprites are added. Give overlapping sprites distinct depths, or draw them in separate Begin()/End() pairs. The same goes for Texture mode: it discards draw-call order.

FrontToBack

Same as BackToFront but sorted in ascending order (0.0 = front drawn first). This is useful with depth testing: front sprites write their depth values early, causing later back sprites to fail the depth test and be discarded — saving pixel shader time on fillrate-bound scenes. Less useful for 2D alpha blending.

spriteBatch_->Begin(SpriteSortMode::FrontToBack, BlendState::Opaque);
// Layer 0.0 = front, 1.0 = back
// Primarily useful with opaque sprites and depth testing enabled
// (the default depthStencil argument of Begin() is DepthStencilState::None: depth testing is off)
spriteBatch_->End();

Use when: You have many opaque sprites and want early-depth-rejection. Rarely needed in 2D games; more relevant in 2.5D or isometric scenes with a depth buffer.

What the sort actually changes

These three frames come from CNA's XNA oracle corpus (tools/xna-oracle/, 39 scenes): they are the real Microsoft XNA 4.0 runtime's own output, captured under Wine + DXVK on Linux, and CNA's DIRECTX9 renderer is recorded as matching them at --tolerance 0 (a check run by hand on the project’s reference machine, not in CI). The same overlapping sprites are submitted in the same order under three different sort modes. Note that Deferred and FrontToBack are byte-identical here — only BackToFront reverses which sprite ends up on top.

An olive-green square on a black background: with Deferred sorting the last sprite submitted is painted last and covers the others.

Deferred — submission order wins; layerDepth is ignored. Real XNA 4.0 output, recorded as pixel-identical to CNA's DIRECTX9 renderer.

A brown square on a black background: with BackToFront sorting a different sprite is drawn last, so the visible colour changes.

BackToFront — the deepest sprite is drawn first, so a different sprite finishes on top. Real XNA 4.0 output, recorded as pixel-identical to CNA's DIRECTX9 renderer.

An olive-green square on a black background, identical to the Deferred frame: FrontToBack sorting leaves the same sprite on top in this scene.

FrontToBack — here the sort is a no-op and the frame is byte-identical to Deferred. Real XNA 4.0 output, recorded as pixel-identical to CNA's DIRECTX9 renderer.

Layer depth parameter in Draw()

The layerDepth parameter is the last argument to the full Draw() overload. It is a float in the range [0.0, 1.0]. It is only meaningful when the sort mode is BackToFront or FrontToBack; otherwise it is ignored:

// Full Draw() signature showing layerDepth
spriteBatch_->Draw(
    texture,          // const Texture2D&
    position,         // Vector2
    sourceRect,       // std::optional<Rectangle>
    color,            // Color
    rotation,         // float (radians)
    origin,           // Vector2
    scale,            // float or Vector2
    effects,          // SpriteEffects
    layerDepth        // float — 0.0f to 1.0f
);

Performance implications

ModeCPU sort costGPU draw callsNotes
DeferredNone1 per texture groupBest general performance
ImmediateNone1 per Draw() callWorst GPU performance
TextureO(n log n) by texture1 per textureBetter than Deferred when textures interleave
BackToFrontO(n log n) by depth1 per texture group (post-sort)Required for correct alpha layering
FrontToBackO(n log n) by depth1 per texture group (post-sort)Good for opaque + depth rejection

Combining sort modes with multiple SpriteBatch calls

A professional game typically uses multiple Begin()/End() pairs per frame, each with a different sort mode, to achieve correct layering cheaply:

void Draw(const GameTime&) override {
    auto& gd = getGraphicsDeviceProperty();
    gd.Clear(Color::Black);

    // Layer 1 — Background tiles (opaque, Deferred = fast)
    spriteBatch_->Begin(SpriteSortMode::Deferred, BlendState::Opaque,
                         nullptr, nullptr, nullptr, nullptr,
                         camera_->getTransform());
    DrawBackgroundTiles();
    spriteBatch_->End();

    // Layer 2 — World entities with depth-sorted alpha
    spriteBatch_->Begin(SpriteSortMode::BackToFront, BlendState::AlphaBlend,
                         nullptr, nullptr, nullptr, nullptr,
                         camera_->getTransform());
    DrawEntities();    // each entity passes its Y as layerDepth for isometric sort
    spriteBatch_->End();

    // Layer 3 — Particle effects (additive, no sort needed)
    spriteBatch_->Begin(SpriteSortMode::Deferred, BlendState::Additive,
                         nullptr, nullptr, nullptr, nullptr,
                         camera_->getTransform());
    DrawParticles();
    spriteBatch_->End();

    // Layer 4 — HUD (screen space, no camera transform)
    spriteBatch_->Begin(SpriteSortMode::Deferred, BlendState::AlphaBlend);
    DrawHUD();
    spriteBatch_->End();

    // No gd.Present(): Game::EndDraw() presents the frame after Draw() returns.
}

Isometric Y-sorting

A common pattern for isometric games: entities further up the screen (smaller Y, since Y grows downward) appear behind those lower on screen (larger Y). Use BackToFront and derive layerDepth from the entity's Y position:

// Normalise Y to [0,1] range
float layerDepth = 1.0f - (entity.position.Y / static_cast<float>(worldHeight));
spriteBatch_->Draw(*entity.tex, entity.position, std::nullopt, Color::White,
                   0.0f, entity.origin, 1.0f, SpriteEffects::None, layerDepth);