Render Targets
Implementation status: RenderTarget2D is implemented on EasyGL and Vulkan, which also support multiple render targets (MRT); RenderTargetCube is implemented on EasyGL. Render targets are also real on SDL_GPU, DIRECTX9 and DIRECTX11. Two limitations are worth knowing before you design around them: the WebGPU renderer has no render-target support at all, and DIRECTX12 clears an MRT but never draws to it. Format admission is renderer-qualified: Skia has a broad promoted subset, IGL promotes Rg32/Single, and other families use the framework's Color-only rule. See Renderers for the full per-renderer picture.
Overview
RenderTarget2D is a subclass of Texture2D in the Microsoft::Xna::Framework::Graphics namespace. It can be bound as the active render target so that subsequent draw calls write their colour (and optionally depth) output into the texture rather than the back buffer. Because it inherits from Texture2D, the same object can be passed directly to SpriteBatch.Draw() or bound to an effect sampler — no explicit copy or conversion is required.
The XNA render-target model maps cleanly onto both the EasyGL and Vulkan renderers: EasyGL implements render targets as OpenGL Framebuffer Objects (FBOs) with an attached depth renderbuffer, and Vulkan implements them as off-screen Vulkan images with a depth attachment and an explicit VkRenderPass.
Constructor
The minimal constructor takes a GraphicsDevice, a width, and a height. An optional extended form accepts surface format, depth format, multisample count, and usage hint:
// Minimal — RGBA8 colour, Depth24Stencil8, no MSAA
RenderTarget2D rt(graphicsDevice, 1024, 768);
// Extended form
RenderTarget2D rt(
graphicsDevice,
1024, 768,
SurfaceFormat::Color, // preferredFormat
DepthFormat::Depth24Stencil8, // preferredDepthFormat
0, // preferredMultiSampleCount (0 = no MSAA)
RenderTargetUsage::DiscardContents
);
| Parameter | Type | Default | Description |
|---|---|---|---|
graphicsDevice |
GraphicsDevice& |
— | The device that owns this render target |
width, height |
int |
— | Dimensions in texels |
preferredFormat |
SurfaceFormat |
Color |
Colour attachment surface format |
preferredDepthFormat |
DepthFormat |
Depth24Stencil8 |
Depth (and optional stencil) format; use None to omit depth |
preferredMultiSampleCount |
int |
0 |
MSAA sample count; 0 disables multisampling |
usage |
RenderTargetUsage |
DiscardContents |
Whether prior contents are preserved between frames |
Renderer implementation
| Renderer | Colour attachment | Depth attachment | Status |
|---|---|---|---|
| EasyGL (OpenGL ES 3.0 / WebGL 2) | FBO + GL_TEXTURE_2D |
Depth renderbuffer (GL_RENDERBUFFER) |
Real, incl. MRT |
| Vulkan | Off-screen VkImage + VkImageView |
Depth VkImage attachment in VkRenderPass |
Real; MRT proxy, MSAA resolve |
| DIRECTX9 (Windows-only) | Off-screen surface in D3DPOOL_DEFAULT |
Depth-stencil surface | Real; caps-validated MRT |
| DIRECTX11 (Windows-only) | Render-target view over a texture | Depth-stencil view | Real; MRT with deferred resolve |
| SDL_GPU | SDL_GPU texture used as a colour target | Depth-stencil target | Real; stencil + MSAA resolve |
| DIRECTX12 (Windows-only) | Render-target view | Depth-stencil view | MRT is cleared but never drawn to |
| WebGPU | — | — | No render-target support at all |
| Headless | — | — | Renders nothing by design |
The remaining renderers — SDL_Renderer, Direct2D, Canvas, HTML_DOM, Skia, Blend2D, FreeDirect, GDI, SVG_DOM and OpenVG (all 2D-only), plus Software and bgfx — are not characterised for render-target behaviour by the current audit, so do not assume off-screen rendering works on them without testing. WebGPU is a backbuffer-only forward renderer: it has no render targets, no MSAA and no cube/3D textures, so every technique on this page is unavailable there. The Headless renderer deliberately renders nothing at all; its ReadBackbuffer returns the last clear colour. See Renderers for the full picture across all 50 renderers.
Color is the portable choice, not the only alpha.1 choice. Skia promotes a broad verified render-target subset and IGL promotes Rg32 and Single. Other families defer to the framework's Color-only rule. Every example on this page uses SurfaceFormat::Color because it is the common denominator.
Setting the render target
Call graphicsDevice.SetRenderTarget() before any draw calls you want redirected to the texture. All subsequent draws — including SpriteBatch draws, 3D primitive draws, and model draws — write to the render target until a different target (or nullptr) is set.
// Redirect output to the render target
graphicsDevice.SetRenderTarget(renderTarget);
// Clear the off-screen surface
graphicsDevice.Clear(Color::CornflowerBlue);
// Draw whatever you need into the render target
spriteBatch.Begin();
spriteBatch.Draw(myTexture, Vector2::Zero, Color::White);
spriteBatch.End();
Restoring the back buffer
Pass nullptr (or an equivalent null render target) to restore rendering to the default back buffer. After this call the render target texture is complete and ready to be sampled.
// Restore default back buffer
graphicsDevice.SetRenderTarget(nullptr);
// The render target is now a fully populated texture — draw it to the screen
spriteBatch.Begin();
spriteBatch.Draw(renderTarget, Vector2::Zero, Color::White);
spriteBatch.End();
Always restore the back buffer before calling GraphicsDevice.Present(). Drawing to the screen while a non-null render target is bound is undefined behaviour and will silently produce no output on most renderers.
Using the result as a texture
Because RenderTarget2D inherits from Texture2D, you can pass it anywhere a Texture2D is accepted — no copy, readback, or conversion is required. This makes it easy to chain render passes:
// renderTarget is-a Texture2D — use it directly with SpriteBatch
spriteBatch.Draw(renderTarget, screenRect, Color::White);
// Or bind it to a 3D effect sampler
basicEffect.SetTexture(renderTarget);
basicEffect.SetTextureEnabled(true);
Multiple Render Targets (MRT)
MRT is real on EasyGL, Vulkan, DIRECTX9 (caps-validated) and DIRECTX11 (with a deferred resolve). Pass a collection of render targets to SetRenderTargets(). Each target must have the same dimensions. In the fragment shader, output values are written to each attachment in order.
Two caveats: DIRECTX12 clears an MRT but never draws to it, so a G-buffer pass silently produces nothing there; and most renderer families only admit SurfaceFormat::Color, so portable G-buffers must encode into RGBA8. Skia and IGL have explicit promoted exceptions, not a framework-wide guarantee.
// Create G-buffer targets (albedo, normals, emissive)
// NOTE: all attachments must be SurfaceFormat::Color — CNA cannot
// construct Rgba1010102 / HalfVector4 / any other format today.
RenderTarget2D rtAlbedo (graphicsDevice, 1920, 1080, SurfaceFormat::Color,
DepthFormat::Depth24Stencil8, 0,
RenderTargetUsage::DiscardContents);
RenderTarget2D rtNormals(graphicsDevice, 1920, 1080, SurfaceFormat::Color,
DepthFormat::None, 0,
RenderTargetUsage::DiscardContents);
RenderTarget2D rtEmissive(graphicsDevice, 1920, 1080, SurfaceFormat::Color,
DepthFormat::None, 0,
RenderTargetUsage::DiscardContents);
// Bind all three at once — geometry pass writes to all simultaneously
graphicsDevice.SetRenderTargets({rtAlbedo, rtNormals, rtEmissive});
graphicsDevice.Clear(Color::Black);
// ... draw geometry here ...
// Restore back buffer for the lighting pass
graphicsDevice.SetRenderTarget(nullptr);
MRT support: real on EasyGL, Vulkan, DIRECTX9 and DIRECTX11. DIRECTX12 clears an MRT but never draws to it. WebGPU has no render targets at all. The 2D-only renderers and bgfx are not characterised for MRT — test before relying on them.
RenderTargetCube
RenderTargetCube is a cube-map render target used primarily for dynamic environment mapping and reflections. It exposes six faces via the CubeMapFace enum: PositiveX, NegativeX, PositiveY, NegativeY, PositiveZ, NegativeZ. Each face must be rendered in a separate pass with the camera oriented toward that face.
// Create a 512x512 cube-map render target
RenderTargetCube rtCube(graphicsDevice, 512,
SurfaceFormat::Color,
DepthFormat::Depth24Stencil8,
0,
RenderTargetUsage::DiscardContents);
// Render each face
for (int face = 0; face < 6; ++face) {
graphicsDevice.SetRenderTarget(rtCube,
static_cast<CubeMapFace>(face));
graphicsDevice.Clear(Color::Black);
// Draw the scene from the cube face's view direction
DrawSceneForFace(static_cast<CubeMapFace>(face));
}
// Restore back buffer and bind the cube map to an effect
graphicsDevice.SetRenderTarget(nullptr);
environmentMapEffect.SetEnvironmentMap(rtCube);
DepthFormat enum
| Value | Description |
|---|---|
DepthFormat::None |
No depth or stencil attachment. Use for colour-only targets such as off-screen UI layers or G-buffer colour channels. |
DepthFormat::Depth16 |
16-bit depth buffer. Lower memory footprint; reduced precision at large distances. |
DepthFormat::Depth24 |
24-bit depth buffer. Standard precision for most 3D rendering. |
DepthFormat::Depth24Stencil8 |
24-bit depth + 8-bit stencil buffer. Required for stencil operations such as shadow volumes and portal rendering. |
RenderTargetUsage enum
| Value | Description |
|---|---|
RenderTargetUsage::DiscardContents |
Default. Contents are considered undefined after SetRenderTarget(nullptr) until the next explicit Clear(). Allows tile-based GPUs to avoid flushing attachment data, improving mobile performance. |
RenderTargetUsage::PreserveContents |
Contents are guaranteed to be preserved across render target switches. Use when you need to read back previous frame data without an explicit copy. |
RenderTargetUsage::PlatformContents |
Defers to the platform's default behaviour. Equivalent to DiscardContents on most modern renderers. |
Common use cases
- Shadow maps — render the scene from the light's point of view into a
RenderTarget2D(DepthFormat::Depth24), then sample it in the main pass to compute shadow factors. Note that the colour attachment must still beSurfaceFormat::Color, so a single-channel float depth target is not available; pack depth into RGBA8 instead. - Post-processing effects — render the full scene into a render target, then apply screen-space effects (blur, bloom, colour grading, FXAA) in one or more additional passes before presenting.
- Minimap rendering — render a top-down view of the game world into a small render target each frame and display it as a HUD element via
SpriteBatch. - Blur and bloom passes — chain multiple render targets: render → horizontal blur → vertical blur → composite back onto the main scene.
- Deferred shading G-buffer — use MRT to write albedo, normals, and material properties simultaneously, then run a screen-space lighting pass reading from all three.
- Dynamic environment mapping — use
RenderTargetCubeto capture the surroundings at runtime and feed it intoEnvironmentMapEffectfor real-time reflections.
Code example 1 — basic render-to-texture
Render a scene into a texture, then display that texture scaled to the screen.
// --- Setup (LoadContent) ---
auto renderTarget = std::make_shared<RenderTarget2D>(
*graphicsDevice, 512, 512);
// --- Per-frame (Draw) ---
// 1. Render scene into the render target
graphicsDevice->SetRenderTarget(*renderTarget);
graphicsDevice->Clear(Color::CornflowerBlue);
spriteBatch->Begin();
spriteBatch->Draw(sceneTexture, Vector2::Zero, Color::White);
spriteBatch->End();
// 2. Restore back buffer
graphicsDevice->SetRenderTarget(nullptr);
graphicsDevice->Clear(Color::Black);
// 3. Display the captured texture on screen
Rectangle destRect(0, 0,
graphicsDevice->Viewport().Width,
graphicsDevice->Viewport().Height);
spriteBatch->Begin();
spriteBatch->Draw(*renderTarget, destRect, Color::White);
spriteBatch->End();
Code example 2 — post-processing pipeline
A two-pass blur: render the scene, apply a horizontal blur, apply a vertical blur, then composite to screen.
// --- Setup ---
auto rtScene = std::make_shared<RenderTarget2D>(*graphicsDevice, 1920, 1080);
auto rtBlurH = std::make_shared<RenderTarget2D>(*graphicsDevice, 1920, 1080);
auto rtBlurV = std::make_shared<RenderTarget2D>(*graphicsDevice, 1920, 1080);
auto blurHEffect = content->Load<ShaderEffect>("shaders/blur_horizontal");
auto blurVEffect = content->Load<ShaderEffect>("shaders/blur_vertical");
// --- Per-frame ---
// Pass 1: render the scene
graphicsDevice->SetRenderTarget(*rtScene);
graphicsDevice->Clear(Color::Black);
DrawScene();
// Pass 2: horizontal blur
graphicsDevice->SetRenderTarget(*rtBlurH);
blurHEffect->Parameters["SourceTexture"]->SetValue(*rtScene);
blurHEffect->CurrentTechnique->Passes[0]->Apply();
DrawFullscreenQuad();
// Pass 3: vertical blur
graphicsDevice->SetRenderTarget(*rtBlurV);
blurVEffect->Parameters["SourceTexture"]->SetValue(*rtBlurH);
blurVEffect->CurrentTechnique->Passes[0]->Apply();
DrawFullscreenQuad();
// Pass 4: composite to back buffer
graphicsDevice->SetRenderTarget(nullptr);
graphicsDevice->Clear(Color::Black);
spriteBatch->Begin();
spriteBatch->Draw(*rtBlurV, fullscreenRect, Color::White);
spriteBatch->End();
Code example 3 — MRT g-buffer setup
A minimal deferred rendering geometry pass writing albedo and normals simultaneously.
// --- Setup ---
auto rtAlbedo = std::make_shared<RenderTarget2D>(
*graphicsDevice, 1920, 1080,
SurfaceFormat::Color, DepthFormat::Depth24Stencil8, 0,
RenderTargetUsage::DiscardContents);
auto rtNormals = std::make_shared<RenderTarget2D>(
*graphicsDevice, 1920, 1080,
SurfaceFormat::Color, DepthFormat::None, 0, // portable across renderer families
RenderTargetUsage::DiscardContents);
auto gBufferEffect = content->Load<ShaderEffect>("shaders/gbuffer_write");
// --- Geometry pass ---
graphicsDevice->SetRenderTargets({*rtAlbedo, *rtNormals});
graphicsDevice->Clear(Color::Black);
// The fragment shader writes:
// layout(location = 0) out vec4 outAlbedo;
// layout(location = 1) out vec4 outNormal;
gBufferEffect->CurrentTechnique->Passes[0]->Apply();
DrawGeometry();
// --- Lighting pass ---
graphicsDevice->SetRenderTarget(nullptr);
graphicsDevice->Clear(Color::Black);
auto lightingEffect = content->Load<ShaderEffect>("shaders/deferred_lighting");
lightingEffect->Parameters["AlbedoMap"] ->SetValue(*rtAlbedo);
lightingEffect->Parameters["NormalMap"] ->SetValue(*rtNormals);
lightingEffect->CurrentTechnique->Passes[0]->Apply();
DrawFullscreenQuad();