Tutorial 21: SpriteBatch Deep Dive
What you’ll learn
- Every parameter of
SpriteBatch::Begin()and what it actually changes. - All nine
Drawoverloads and theDrawStringoverloads. - When the batch flushes, and how that drives draw-call count.
- Passing a custom effect into
Begin().
Before you start — Tutorial 06: Drawing Your First 2D Shape, Tutorial 08: Loading and Drawing Textures and Tutorial 09: Drawing Text with SpriteFont — this is the reference pass over the API those three introduced.
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, BlendState::AlphaBlend,
&SamplerState::PointClamp);
effect
Substitute a custom Effect for the built-in sprite shader. A ShaderEffect is CNA's renderer-native CNAEXT path; its vertex shader must declare a mat4 projection uniform (SpriteBatch sets it on whichever program is bound) and its fragment shader must write a vec4. Alpha.1 can also execute XNA/FNA D3D9 Effect Framework binaries through SpriteBatch on FNA3D or an explicitly enabled EasyGL, SDL_GPU or Vulkan build. That separate path accepts compiled .fxb/XNB payloads, not HLSL .fx source, DXBC or MGFX; see Tutorial 128.
spriteBatch_->Begin(SpriteSortMode::Deferred,
BlendState::AlphaBlend, 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);
Position, rotation and flip, as the real XNA runtime drew them
These four frames come from CNA's XNA oracle corpus (tools/xna-oracle/): output of the genuine Microsoft XNA 4.0 runtime, which CNA's DIRECTX9 renderer is diffed against at --tolerance 0. They exercise the Draw() arguments described above.
The simplest overload — texture plus position, no rotation, no effects. Real XNA 4.0 output; CNA's DIRECTX9 renderer matches it pixel-for-pixel.
A non-zero rotation about the given origin. Real XNA 4.0 output; CNA's DIRECTX9 renderer matches it pixel-for-pixel.
SpriteEffects::FlipHorizontally — the source is sampled mirrored, so the colour layout reverses. Real XNA 4.0 output; CNA's DIRECTX9 renderer matches it pixel-for-pixel.
Sprites from more than one texture inside a single Begin()/End() pair — the batcher splits the draw at each texture change. Real XNA 4.0 output; CNA's DIRECTX9 renderer matches it pixel-for-pixel.
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()
This example deliberately uses ShaderEffect with renderer-native source. getContentProperty().Load<std::shared_ptr<Effect>>("Shaders/wavy") is also possible for an XNB containing XNA/FNA D3D9 Effect Framework bytecode when the active alpha.1 renderer supports compiled effects, but that is a renderer-qualified compatibility path rather than an HLSL .fx source compiler. See Tutorial 52.
#include "Microsoft/Xna/Framework/Graphics/ShaderEffect.hpp"
// Three arguments: device, vertex source, fragment source. Not file paths.
auto customFx = std::make_unique<ShaderEffect>(
getGraphicsDeviceProperty(), kWavyVertSrc, kWavyFragSrc);
if (!customFx->IsEffectValid()) {
// The shader did not compile. The constructor does not throw; this is the only signal.
}
spriteBatch_->Begin(SpriteSortMode::Deferred,
BlendState::AlphaBlend, // 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.