Render Targets

Microsoft::Xna::Framework::Graphics — RenderTarget2D, RenderTargetCube, off-screen rendering

ⓘ

Implementation status: at snapshot c1c316b9, RenderTarget2D is implemented on every 3D renderer — the three EasyGL identities, VULKAN, WEBGPU, SDL_GPU, DIRECTX9/11, FNA3D, SOFTWARE and METAL — and in a Color-only, depth-less form on the 2D-only SDL_RENDERER. RenderTargetCube has real storage on the same 3D renderers. Multiple render targets (MRT) are real on 11 identities, but MRT, float and HDR formats, larger cubes and 3D textures require GraphicsProfile::HiDef: the default profile is Reach, and it is now enforced on every renderer. See the profile limits, and 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 every 3D renderer: EasyGL implements render targets as OpenGL Framebuffer Objects (FBOs) with an attached depth renderbuffer, Vulkan as off-screen Vulkan images with a depth attachment and an explicit VkRenderPass, and the Direct3D renderers as render-target views over textures. A RenderTarget2D is not copyable (it can be moved), so hold it in a std::unique_ptr or as a member.

Reach and HiDef profile limits

⚠

The default GraphicsProfile is Reach, and this snapshot enforces it on every renderer (alpha.1 enforced Reach ceilings only on DIRECTX9). Several render-target features throw, or quietly degrade to Color, unless you ask for HiDef before Initialize() applies your preferences to the device:

RuleReach (default)HiDef
RenderTarget2D edge length20484096
RenderTargetCube edge length512, power of two only4096
Simultaneous render targets (SetRenderTargets)14 (also the global cap)
Render-target SurfaceFormatColour formats only (Color, Bgr565, Bgra5551, Bgra4444, NormalizedByte2/4); a float, HDR, Rgba1010102, Rg32, Rgba64 or Alpha8 request silently becomes ColorAll XNA formats the renderer reports renderable (never DXT); a request the renderer cannot honour still becomes Color
Aspect ratio of a RenderTarget2DAt most 2048 : 1 in both profiles
Texture3D, OcclusionQuery, 32-bit index buffers, GetBackBufferDatarefusedallowed (where the renderer supports them)

Ask for HiDef in your game's constructor, before Initialize() applies your preferences to the device (or call CNA::SetProjectGraphicsProfileEXT(GraphicsProfile::HiDef) before the Game is created):

MyGame() : graphics_(this)
{
    graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef);
}

The full list of what the profile gates, with the exact exception messages, is Tutorial 152: Reach vs HiDef. Because the format fallback is silent, do not assume you got the format you asked for: query it. GraphicsDevice::SupportsSurfaceFormatAsRenderTargetEXT(format) answers with the profile rule and the renderer's own verdict, and GraphicsDevice::SupportsCapability(GraphicsCapability::MultipleRenderTargets) is false under Reach on every renderer.

Constructor

The minimal constructor takes a GraphicsDevice, a width, and a height, and creates a Color target without a depth buffer. The extended form adds a mip-map flag, surface format, depth format, multisample count, and usage hint:

// Minimal — RGBA8 colour, DepthFormat::None, no mip chain, no MSAA
RenderTarget2D rt(graphicsDevice, 1024, 768);

// Extended form
RenderTarget2D rt2(
    graphicsDevice,
    1024, 768,
    false,                         // mipMap
    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; limited by the profile (2048 Reach, 4096 HiDef) and by a 2048 : 1 aspect ratio
mipMap bool false Allocates a full mip chain (extended form only; not supported on the 2D-only SDL_RENDERER)
preferredFormat SurfaceFormat Color Colour attachment surface format; falls back to Color when the profile or renderer cannot honour it
preferredDepthFormat DepthFormat None (minimal constructor) Depth (and optional stencil) format; None omits depth. Use Depth24Stencil8 when you need stencil
preferredMultiSampleCount int 0 MSAA sample count, rounded down to a power of two (1 becomes 0); 0 disables multisampling; the applied count is device-clamped
usage RenderTargetUsage DiscardContents Whether prior contents are preserved between frames

Renderer implementation

Renderer Colour attachment Depth attachment Status
EasyGL: OPENGLES3, OPENGL33, WEBGL2 FBO + GL_TEXTURE_2D Depth renderbuffer (GL_RENDERBUFFER) Real, incl. MRT (up to 4) and cube faces in an MRT set
VULKAN Off-screen VkImage + VkImageView Depth VkImage attachment in VkRenderPass Real; MRT when maxColorAttachments > 1, MSAA resolve, per-format probing
WEBGPU WebGPU texture, RENDER_ATTACHMENT Depth attachment Real; MRT of 2–4 RenderTarget2Ds, 4x MSAA when the probe passes; a cube face inside an MRT set is refused
DIRECTX9 (Windows-only) Off-screen surface in D3DPOOL_DEFAULT Depth-stencil surface Real; MRT validated against device caps; a cube face inside an MRT set is refused
DIRECTX11 (Windows-only) Render-target view over a texture Depth-stencil view Real; MRT; a cube face inside an MRT set is refused
SDL_GPU SDL_GPU texture used as a colour target Depth-stencil target Real; four independently writable, mixed-format colour targets; MSAA resolve; cube faces inside an MRT set refused
FNA3D FNA3D render-target texture FNA3D depth-stencil buffer Real, incl. MRT and cube faces in an MRT set; no float render-target formats through the public API (Color only)
SOFTWARE CPU colour buffer CPU depth/stencil buffer Real, incl. 4 targets, cubes, float formats and 4x MSAA (CPU)
METAL (macOS, iOS) Metal texture Depth-stencil texture (only the planes the depth format names; Depth16 native) Real 2D and cube targets in Color and the float, half and wide formats; MRT up to 8 targets, cube faces included; MSAA (2x/4x on an M4), resolved at every encoder boundary; a multisampled cube refuses SetData
HEADLESS — — Records and validates draws; renders nothing by design
STUB — — No render targets (construction succeeds, binding throws NotSupportedException)
SDL_RENDERER Backend off-screen surface None 2D targets for SpriteBatch work: Color only, request DepthFormat::None, no mip chain, no MSAA

SDL_RENDERER is 2D-only by design. Its off-screen targets serve SpriteBatch work such as a minimap or a UI cache; the 3D techniques on this page (depth, MRT, cube targets, float formats) are unavailable there. It throws when you construct a target with a depth format, a mip chain or multisampling, and it refuses a multi-target set or a cube face, so use the minimal constructor there.

Formats

⚠

Color is the portable choice, not the only choice. Format admission is a two-step rule: first the GraphicsProfile gate (Reach refuses the float and HDR formats), then the renderer's verdict. Seven families classify formats themselves — DIRECTX11 (all XNA formats the device reports renderable), VULKAN (per-format device properties), the EasyGL identities (probed against the live context), SDL_GPU (float and half render targets exist), WEBGPU (probed; Rgba64 needs an optional feature), SOFTWARE (all seven float/half formats) and METAL (Rgba1010102, Rg32, Rgba64 and every float and half format). A renderer that does not classify falls back to the framework rule, which admits Color only; FNA3D, DIRECTX9, HEADLESS and the 2D-only SDL_RENDERER are in that group for render targets. Most examples on this page use SurfaceFormat::Color because it is the common denominator.

// Ask, do not assume: profile rule AND renderer verdict.
SurfaceFormat hdr = SurfaceFormat::HdrBlendable;
if (!graphicsDevice.SupportsSurfaceFormatAsRenderTargetEXT(hdr))
    hdr = SurfaceFormat::Color;   // what the RenderTarget2D constructor would have picked anyway
RenderTarget2D scene(graphicsDevice, 1280, 720, false, hdr, DepthFormat::Depth24);

Setting the render target

Call graphicsDevice.SetRenderTarget() before any draw calls you want redirected to the texture. It takes a pointer. 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(sceneTexture, Vector2::Zero, Color::White);
spriteBatch.End();

Restoring the back buffer

Pass nullptr 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 (SetRenderTarget(nullptr)) before the frame ends: draws issued while a non-null render target is bound go to that target, not the screen, so the frame shows nothing. Do not call GraphicsDevice::Present() yourself to finish a frame inside Game::Draw() — Game presents in EndDraw(), and a manual Present() presents twice. Also avoid sampling a render target while it is still bound (EasyGL refuses it).

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 (setTextureProperty takes a Texture2D*)
basicEffect.setTextureProperty(&renderTarget);
basicEffect.setTextureEnabledProperty(true);

One Reach detail to remember when you sample a target with a 3D effect rather than SpriteBatch: the device's default sampler state for slot 0 is LinearWrap, and under the default profile a non-power-of-two texture — which a window-sized render target usually is — may only be sampled with Clamp addressing, otherwise the draw call throws. Set graphicsDevice.getSamplerStatesProperty()[0] = SamplerState::LinearClamp first (SpriteBatch::Begin already uses LinearClamp by default).

Multiple Render Targets (MRT)

MRT is real on 11 identities: the EasyGL identities (OPENGLES3, OPENGL33, WEBGL2), VULKAN (when the device allows more than one colour attachment), WEBGPU, SDL_GPU, DIRECTX9, DIRECTX11, FNA3D, SOFTWARE and METAL (up to 8). It is unavailable on STUB and the 2D-only SDL_RENDERER. It also requires GraphicsProfile::HiDef: under the default Reach profile a set of more than one target throws, and SupportsCapability(GraphicsCapability::MultipleRenderTargets) reports false on every renderer. Pass a collection of RenderTargetBindings (a RenderTarget2D* converts implicitly) to SetRenderTargets(). Each target must have the same dimensions and applied sample count, and the first target owns the depth buffer. Stock effects write attachment 0 only; a custom ShaderEffect fans out to every attachment through layout(location = N) out.

Mixed formats: attachments no longer have to be Color. On renderers that report the format renderable under HiDef (for example SDL_GPU documents “four independently writable, mixed-format colour targets”), a G-buffer can use Rgba1010102 or HalfVector4; check each format with SupportsSurfaceFormatAsRenderTargetEXT(), because an unsupported request quietly becomes Color. Portable G-buffers should still be prepared to encode into RGBA8.

// Ask for HiDef in the Game constructor first (see "Reach and HiDef profile limits").
// Create G-buffer targets (albedo, normals, emissive)
rtAlbedo_   = std::make_unique<RenderTarget2D>(graphicsDevice, 1920, 1080, false,
                  SurfaceFormat::Color, DepthFormat::Depth24Stencil8, 0,
                  RenderTargetUsage::DiscardContents);
rtNormals_  = std::make_unique<RenderTarget2D>(graphicsDevice, 1920, 1080, false,
                  SurfaceFormat::Color, DepthFormat::None, 0,
                  RenderTargetUsage::DiscardContents);
rtEmissive_ = std::make_unique<RenderTarget2D>(graphicsDevice, 1920, 1080, false,
                  SurfaceFormat::Color, DepthFormat::None, 0,
                  RenderTargetUsage::DiscardContents);

// Bind all three at once — geometry pass writes to all simultaneously
graphicsDevice.SetRenderTargets({rtAlbedo_.get(), rtNormals_.get(), rtEmissive_.get()});
graphicsDevice.Clear(Color::Black);

// ... draw geometry here ...

// Restore back buffer for the lighting pass
graphicsDevice.SetRenderTarget(nullptr);
ⓘ

MRT support: HiDef profile required; real on the EasyGL identities, VULKAN, WEBGPU, SDL_GPU, DIRECTX9/11, FNA3D, SOFTWARE and METAL; unavailable on STUB and SDL_RENDERER. Guard the technique with SupportsCapability(GraphicsCapability::MultipleRenderTargets).

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. Cube targets have real storage on every 3D renderer with render targets, and are limited to 512 (power of two) under Reach and 4096 under HiDef. A cube face can be one member of a multi-target set on the EasyGL identities, VULKAN, SOFTWARE, FNA3D and METAL; DIRECTX9/11, SDL_GPU, WEBGPU and HEADLESS refuse it.

// Create a 512x512 cube-map render target (512 is the Reach ceiling)
rtCube_ = std::make_unique<RenderTargetCube>(graphicsDevice, 512,
              false,                         // mipMap
              SurfaceFormat::Color,
              DepthFormat::Depth24Stencil8,
              0,
              RenderTargetUsage::DiscardContents);

// Render each face
for (int face = 0; face < 6; ++face) {
    graphicsDevice.SetRenderTarget(rtCube_.get(),
        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.setEnvironmentMapProperty(rtCube_.get());

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.

The depth format a target reports (getDepthStencilFormatProperty()) is what the renderer applied, which can differ from what you asked for: Vulkan always creates a depth buffer and reports the format it chose, and SDL_GPU normalises to a supported native format.

RenderTargetUsage enum

Value Description
RenderTargetUsage::DiscardContents Default. Contents are considered undefined after SetRenderTarget(nullptr) until the next explicit Clear(); CNA makes them deterministic by clearing on every bind to colour (0, 0, 0, 255), depth 1 and stencil 0. 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. In CNA it preserves contents exactly like PreserveContents, on every renderer (RenderTargetUsagePreservesContentsEXT).

What happens to the contents on each bind

At this snapshot the shared GraphicsDevice implements RenderTargetUsage the same way on every renderer, through one mapping (RenderTargetUsagePreservesContentsEXT):

  • DiscardContents: every bind clears the first bound target to a deterministic value, colour (0, 0, 0, 255), depth 1.0 and stencil 0 (each only where that attachment exists). The contents are not left undefined.
  • PreserveContents and PlatformContents: both preserve. Colour, depth and stencil survive a full unbind and rebind, as in FNA.
  • An explicit Clear() after the bind always wins, aspect by aspect.
  • With several targets bound, the first target's usage decides whether the bind clears.
  • The back buffer's PresentationParameters.RenderTargetUsage is stored but not consulted when you return to the back buffer.
  • Binding the exact set that is already bound is a no-op: nothing is cleared or resolved, and the viewport is not reset.

Cube targets follow the same rules, with one depth-stencil buffer shared by all six faces. The details and the per-renderer differences in resolve and mip generation are on Render targets: usage, cube faces, resolve and readback.

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. Under HiDef on a renderer that reports SurfaceFormat::Single renderable you can store light-space distance in a single-channel float target; otherwise (and under the default Reach profile) pack depth into RGBA8.
  • 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 (HiDef) to write albedo, normals, and material properties simultaneously, then run a screen-space lighting pass reading from all three.
  • Dynamic environment mapping — use RenderTargetCube to capture the surroundings at runtime and feed it into EnvironmentMapEffect for 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) ---
renderTarget_ = std::make_unique<RenderTarget2D>(graphicsDevice, 512, 512);

// --- Per-frame (Draw) ---

// 1. Render scene into the render target
graphicsDevice.SetRenderTarget(renderTarget_.get());
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.getViewportProperty().getWidthProperty(),
    graphicsDevice.getViewportProperty().getHeightProperty());

spriteBatch.Begin();
spriteBatch.Draw(*renderTarget_, destRect, Color::White);
spriteBatch.End();
// No Present() here: Game presents in EndDraw().

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. Each blur pass draws the previous target through SpriteBatch with a custom ShaderEffect (the renderer must accept custom effects; see Shader Effects), so the fragment shader samples texture1 (unit 0, where SpriteBatch binds the sprite) and the projection uniform is set by SpriteBatch itself. The shader source is dialect-specific (GLSL ES 3.00 shown in Tutorial 24).

// --- Setup ---
rtScene_ = std::make_unique<RenderTarget2D>(graphicsDevice, 1920, 1080);
rtBlurH_ = std::make_unique<RenderTarget2D>(graphicsDevice, 1920, 1080);
rtBlurV_ = std::make_unique<RenderTarget2D>(graphicsDevice, 1920, 1080);

// Build each effect once and keep it alive; the strings are shader SOURCE, not paths.
blurH_ = std::make_unique<ShaderEffect>(graphicsDevice,
    System::IO::File::ReadAllText("Content/shaders/sprite.vert"),
    System::IO::File::ReadAllText("Content/shaders/blur_horizontal.frag"));
blurV_ = std::make_unique<ShaderEffect>(graphicsDevice,
    System::IO::File::ReadAllText("Content/shaders/sprite.vert"),
    System::IO::File::ReadAllText("Content/shaders/blur_vertical.frag"));

// --- Per-frame ---
Rectangle full(0, 0, 1920, 1080);

// Pass 1: render the scene
graphicsDevice.SetRenderTarget(rtScene_.get());
graphicsDevice.Clear(Color::Black);
DrawScene();

// Pass 2: horizontal blur (sample the scene target, write rtBlurH_)
graphicsDevice.SetRenderTarget(rtBlurH_.get());
blurH_->Apply();
blurH_->SetUniformVec2("TexelSize", 1.0f / 1920.0f, 0.0f);
spriteBatch.Begin(SpriteSortMode::Deferred, BlendState::Opaque, nullptr, nullptr, nullptr, blurH_.get());
spriteBatch.Draw(*rtScene_, full, Color::White);
spriteBatch.End();

// Pass 3: vertical blur
graphicsDevice.SetRenderTarget(rtBlurV_.get());
blurV_->Apply();
blurV_->SetUniformVec2("TexelSize", 0.0f, 1.0f / 1080.0f);
spriteBatch.Begin(SpriteSortMode::Deferred, BlendState::Opaque, nullptr, nullptr, nullptr, blurV_.get());
spriteBatch.Draw(*rtBlurH_, full, Color::White);
spriteBatch.End();

// Pass 4: composite to back buffer
graphicsDevice.SetRenderTarget(nullptr);
graphicsDevice.Clear(Color::Black);

spriteBatch.Begin();
spriteBatch.Draw(*rtBlurV_, full, Color::White);
spriteBatch.End();

Code example 3 — MRT g-buffer setup

A minimal deferred rendering geometry pass writing albedo and normals simultaneously. This needs GraphicsProfile::HiDef (see above) and a renderer whose ShaderEffect path fans out to several attachments (EasyGL ES3 generation, WEBGPU; a stock effect writes attachment 0 only).

// --- Setup ---
rtAlbedo_ = std::make_unique<RenderTarget2D>(
    graphicsDevice, 1920, 1080, false,
    SurfaceFormat::Color, DepthFormat::Depth24Stencil8, 0,
    RenderTargetUsage::DiscardContents);

rtNormals_ = std::make_unique<RenderTarget2D>(
    graphicsDevice, 1920, 1080, false,
    SurfaceFormat::Color, DepthFormat::None, 0,  // portable across renderer families
    RenderTargetUsage::DiscardContents);

gBufferEffect_ = std::make_unique<ShaderEffect>(graphicsDevice, gbufferVertSrc, gbufferFragSrc);

// --- Geometry pass ---
graphicsDevice.SetRenderTargets({rtAlbedo_.get(), rtNormals_.get()});
graphicsDevice.Clear(Color::Black);

// The fragment shader writes:
//   layout(location = 0) out vec4 outAlbedo;
//   layout(location = 1) out vec4 outNormal;
gBufferEffect_->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
DrawGeometry();

// --- Lighting pass ---
graphicsDevice.SetRenderTarget(nullptr);
graphicsDevice.Clear(Color::Black);

lightingEffect_->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
lightingEffect_->SetTexture(0, *rtAlbedo_);    // RenderTarget2D is-a Texture2D
lightingEffect_->SetTexture(1, *rtNormals_);
lightingEffect_->SetUniformInt("AlbedoMap", 0);
lightingEffect_->SetUniformInt("NormalMap", 1);
DrawFullscreenQuad();