Tutorial 21: SpriteBatch Deep Dive

Graphics  ·  SpriteBatch  ·  SpriteSortMode  ·  BlendState

SpriteBatch is the primary 2D drawing API in CNA. Internally it accumulates draw calls into a vertex buffer, then flushes to the GPU in the fewest possible draw calls. This tutorial explores every overload of Begin() and Draw(), explains the SpriteSortMode enum, and covers batching performance in depth.

SpriteBatch::Begin() — all parameters

void SpriteBatch::Begin(
    SpriteSortMode          sortMode        = SpriteSortMode::Deferred,
    BlendState*             blendState      = nullptr,       // nullptr = AlphaBlend
    SamplerState*           samplerState    = nullptr,       // nullptr = LinearClamp
    DepthStencilState*      depthStencil    = nullptr,       // nullptr = None
    RasterizerState*        rasterizerState = nullptr,       // nullptr = CullCounterClockwise
    Effect*                 effect          = nullptr,       // nullptr = built-in sprite shader
    std::optional<Matrix>  transformMatrix = std::nullopt   // identity if omitted
);

sortMode

Controls when sprites are submitted to the GPU and in what order. See Tutorial 25 for details. For most games Deferred (default) gives the best performance.

blendState

Controls how sprite pixels are composited over the existing render target. Common values:

// AlphaBlend (default) — pre-multiplied alpha compositing
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::AlphaBlend);

// Additive — for particles, glows
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::Additive);

// Opaque — no alpha, overwrites background
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::Opaque);

// NonPremultiplied — straight (non-premultiplied) alpha
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::NonPremultiplied);

samplerState

Controls how textures are sampled:

// LinearClamp (default) — bilinear filter, clamp to edge
// PointClamp            — nearest-neighbour (pixel art)
// LinearWrap            — bilinear + repeat for tiling textures
spriteBatch_->Begin(SpriteSortMode::Deferred, nullptr,
                    &SamplerState::PointClamp);

effect

Substitute a custom Effect for the built-in sprite shader. The effect must accept a float4x4 MatrixTransform parameter and output a float4 colour. See Tutorial 24 for post-process usage.

spriteBatch_->Begin(SpriteSortMode::Deferred,
                    nullptr, nullptr, nullptr, nullptr,
                    grayscaleEffect_.get());

SpriteSortMode enum

ValueWhen sprites flushSort order
DeferredAt End()Draw call order
ImmediateEach Draw() callDraw call order
TextureAt End()By texture object (minimise texture switches)
BackToFrontAt End()Descending by layerDepth (back first)
FrontToBackAt End()Ascending by layerDepth (front first)

Draw() — all nine overloads

// 1. Position only
sb.Draw(tex, Vector2(x, y), Color::White);

// 2. Position + source rectangle (sprite sheet region)
sb.Draw(tex, Vector2(x, y), sourceRect, Color::White);

// 3. Destination rectangle (scale to fit)
sb.Draw(tex, destRect, Color::White);

// 4. Destination + source rectangle
sb.Draw(tex, destRect, sourceRect, Color::White);

// 5. Full control: position, source, color, rotation, origin, scale, effects, depth
sb.Draw(tex,
        position,        // Vector2
        sourceRect,      // std::optional<Rectangle>
        color,           // Color
        rotation,        // Single — radians
        origin,          // Vector2 — pivot point relative to source rect
        scale,           // Single — uniform scale
        SpriteEffects::None,
        layerDepth);     // Single — 0.0 (front) to 1.0 (back) in BackToFront mode

// 6. As above but scale is a Vector2 (non-uniform)
sb.Draw(tex, position, sourceRect, color, rotation, origin,
        Vector2(scaleX, scaleY), SpriteEffects::None, layerDepth);

// 7. Destination rectangle + full control
sb.Draw(tex, destRect, sourceRect, color, rotation, origin,
        SpriteEffects::None, layerDepth);

// 8. Texture* overload (raw pointer)
sb.Draw(*tex, Vector2(x, y), Color::White);

// 9. SharedPointer overload
sb.Draw(texShared, Vector2(x, y), Color::White);

SpriteEffects

// Flip horizontally (mirror left-right)
sb.Draw(tex, pos, std::nullopt, Color::White, 0.0f, Vector2::Zero,
        1.0f, SpriteEffects::FlipHorizontally, 0.0f);

// Flip vertically
sb.Draw(tex, pos, std::nullopt, Color::White, 0.0f, Vector2::Zero,
        1.0f, SpriteEffects::FlipVertically, 0.0f);

// Both
sb.Draw(tex, pos, std::nullopt, Color::White, 0.0f, Vector2::Zero,
        1.0f,
        SpriteEffects::FlipHorizontally | SpriteEffects::FlipVertically,
        0.0f);

DrawString overloads

// 1. String at position
sb.DrawString(*font, "Hello World", Vector2(100, 50), Color::White);

// 2. With rotation, origin, scale, effects, depth
sb.DrawString(*font, "Score: " + std::to_string(score),
              position, color,
              0.0f,          // rotation
              Vector2::Zero, // origin
              1.0f,          // scale
              SpriteEffects::None,
              0.0f);         // layer depth

// 3. std::wstring overload (for Unicode)
sb.DrawString(*font, L"ゲーム", position, Color::Yellow);

Flush and batching behaviour

In Deferred mode (the default) SpriteBatch accumulates all Draw() calls into a CPU-side vertex array. When End() is called it:

  1. Sorts the accumulated sprite list if the sort mode requires it.
  2. Groups consecutive sprites by texture to minimise GPU texture-switch overhead.
  3. Uploads one vertex buffer with all positions/UVs/colours.
  4. Issues one draw call per texture group.

If you draw 1000 sprites from 3 different textures, you get 3 GPU draw calls regardless of the order you called Draw(). This is why SpriteSortMode::Texture can be faster than Deferred when you have many different textures — it clusters them before upload.

Layered drawing with BackToFront

spriteBatch_->Begin(SpriteSortMode::BackToFront);

// layerDepth 1.0 = back, 0.0 = front
spriteBatch_->Draw(*skyTex,    skyPos,    std::nullopt, Color::White,
                   0, Vector2::Zero, 1.0f, SpriteEffects::None, 1.0f);
spriteBatch_->Draw(*mountainTex, mtPos,  std::nullopt, Color::White,
                   0, Vector2::Zero, 1.0f, SpriteEffects::None, 0.8f);
spriteBatch_->Draw(*treeTex,   treePos,  std::nullopt, Color::White,
                   0, Vector2::Zero, 1.0f, SpriteEffects::None, 0.5f);
spriteBatch_->Draw(*playerTex, playerPos,std::nullopt, Color::White,
                   0, Vector2::Zero, 1.0f, SpriteEffects::None, 0.2f);
spriteBatch_->Draw(*hudTex,    hudPos,   std::nullopt, Color::White,
                   0, Vector2::Zero, 1.0f, SpriteEffects::None, 0.0f);

spriteBatch_->End();
// Sprites are submitted back → front: sky, mountain, tree, player, HUD

Custom effect in Begin()

// Load a custom shader effect (see Tutorial 24 for shader authoring)
auto customFx = Content.Load<Effect>("Shaders/wavy");

spriteBatch_->Begin(SpriteSortMode::Deferred,
                    nullptr,    // BlendState
                    nullptr,    // SamplerState
                    nullptr,    // DepthStencilState
                    nullptr,    // RasterizerState
                    customFx.get());

// All draws in this batch use the custom shader
spriteBatch_->Draw(*backgroundTex, Vector2::Zero, Color::White);

spriteBatch_->End();

Performance rules of thumb

  • Minimise Begin()/End() pairs per frame — each pair is at minimum one GPU state change.
  • Atlas your textures — sprites from the same atlas sheet are batched in a single draw call.
  • Use Deferred for most scenes; switch to Texture only when profiling shows texture-switch overhead.
  • Avoid Immediate — it flushes on every Draw() call and negates all batching benefits. Only use it when you need to call GraphicsDevice state-change methods between individual sprite draws.
  • Draw HUD in a separate Begin()/End() without a camera transform, so world and HUD transforms never mix.