Tutorial 21: SpriteBatch Deep Dive
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
| Value | When sprites flush | Sort order |
|---|---|---|
Deferred | At End() | Draw call order |
Immediate | Each Draw() call | Draw call order |
Texture | At End() | By texture object (minimise texture switches) |
BackToFront | At End() | Descending by layerDepth (back first) |
FrontToBack | At 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:
- Sorts the accumulated sprite list if the sort mode requires it.
- Groups consecutive sprites by texture to minimise GPU texture-switch overhead.
- Uploads one vertex buffer with all positions/UVs/colours.
- 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
Textureonly 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 callGraphicsDevicestate-change methods between individual sprite draws. - Draw HUD in a separate Begin()/End() without a camera transform, so world and HUD transforms never mix.