Render-target binding, clears, viewport and scissor across renderers
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Shared rules read in GraphicsDevice.cpp at 009d40f5; family rows from the named hooks or a source search. No test was executed and the Windows, macOS and browser families were not observed.
Binding a render target, clearing it and restricting drawing to a viewport or scissor rectangle look like simple setters, but in CNA each one is a shared transaction in GraphicsDevice followed by a family-specific native operation. This page states the shared rules at snapshot 009d40f5 — the order of validation, renderer call and public state, when a rebind is a no-op, what RenderTargetUsage really selects, which clear requests throw — and then what each renderer family does with the result. It is for anyone debugging a black target, a clear that wiped too much or too little, or a split-screen that works on one renderer only, and for test authors who need a check that a wrong implementation would fail.
Binding is one transaction
Every public binding call — SetRenderTarget(RenderTarget2D*), the cube-face overload and SetRenderTargets — funnels into GraphicsDevice::SetRenderTargets in GraphicsDevice.cpp. Each binding is normalised into a RenderTargetBindingDescriptor that names a 2D target or one cube face together with its dimensions and applied sample count; the first descriptor then decides the viewport, the scissor and the discard clear for the whole set. Before any renderer work the shared layer rejects more than four bindings, more than the profile allows (one under Reach), a null, disposed or foreign-device target, a non-zero 2D array slice, a cube face out of range, a target whose renderer object is null (NotSupportedException), the same resource in two slots — including two faces of one cube — and mismatched dimensions, applied sample counts or pixel sizes. Only then does it call the renderer, and only after the renderer returns does it publish the bindings, reset viewport and scissor and perform the discard clear, so a failed native bind never leaves the device claiming success. The step-by-step trace, including destruction of a bound target, is on Textures and render targets: SetRenderTargets.
Two shapes are refused by some families only, at the renderer: a cube face inside a multi-target set (DirectX 9, 11 and 12, SDL_GPU, WebGPU and Headless refuse it; see RenderTargetCube) and any multi-target set at all on Metal.
Rebinding the same set is a no-op
An unchanged binding set returns immediately — before validation, renderer work, resolve, discard or the viewport reset. The comparison is by target resource and selected cube face, so an empty-to-empty call is also a no-op; the one exception is a set whose bound target was destroyed, where the next call is a real transition even though nothing remains to compare. This matches Microsoft XNA 4.0, and it is recent: descriptions that say CNA has no redundant-binding early return, so that rebinding a DiscardContents target clears it again and repeats resolve work, describe an older revision. Code ported from such advice, which deliberately re-binds to force a clear, now needs an explicit Clear.
RenderTargetUsage: two answers, not three
RenderTargetUsagePreservesContentsEXT(usage) is the single mapping every renderer receives: PreserveContents and PlatformContents both mean "preserve", only exact DiscardContents means "discard". Discard is implemented once, in the shared layer: after a successful bind whose first target is DiscardContents, the device clears colour to opaque black, depth to 1 and stencil to 0, including each depth or stencil aspect only when the first descriptor's renderer object reports that plane as really present (HasRealDepthBuffer, HasRealStencilBuffer). A renderer therefore needs no load action to carry a usage decision; EasyGL deliberately ignores the preserveContents factory argument because an FBO bind never touches its attachment. The back buffer's own usage is outside this policy: only the first bound target's usage is consulted. The public reference is RenderTargetUsage.
Multiple targets and the first-target rule
Because the first descriptor owns viewport, scissor, discard and the depth buffer, an MRT set behaves like its first target in every shared respect. The stock effects write attachment 0 only; only a custom ShaderEffect with outputs at locations 0 to 3 fans out. Which families bind real MRT storage — Software up to four CPU targets, WebGPU two to four, the ES 3 generation of EasyGL, OpenGL4, Vulkan, SDL_GPU, the Direct3D families and FNA3D — is tabulated on Render targets: MRT. A true MultipleRenderTargets answer (false under Reach on every renderer, because the device ANDs it with the profile limit) must therefore be paired with a successful multi-target bind and an oracle that checks every intended output, not only attachment 0.
Target transitions are family work. A family that defers or multisamples must finalise the outgoing target — resolve, regenerate mips — before another face, 2D target, MRT set or the back buffer becomes active. EasyGL finalises the MRT set and the outgoing target on every switch, including a switch between faces of the same cube (older descriptions that it could skip the outgoing face are stale; see EasyGL internals); Vulkan retires MRT proxies only after the frames that reference them have completed (why Vulkan cannot free on unbind).
Clears
What the device sends
GraphicsDevice::Clear(ClearOptions, Color, depth, stencil) converts the colour once (the Vector4 overload quantises through Color first, as XNA does), normalises depth, and dispatches exactly one of seven renderer entry points — ClearColorDepthAndStencil, ClearColorAndDepth, ClearColorAndStencil, ClearDepthAndStencil, Clear (colour only), ClearDepth, ClearStencil — all pure virtual. Aspects are not silently masked: if the request names depth or stencil and the active target or back buffer has no real plane for it, the call throws InvalidOperationException, as Microsoft XNA does. Clear(Color) instead uses the device's default options, which include depth and stencil only where those planes really exist, with depth 1. The raw Clear(float r, g, b, a) overload calls the renderer's colour-only entry point directly.
What each family does
| Families | Clear behaviour at this snapshot |
|---|---|
| EasyGL | Immediate glClear. The colour-only entry point clears colour only (it formerly added the depth bit, so a ClearOptions::Target request also wiped depth); every clear temporarily disables the scissor test and forces all colour write channels, because XNA clears the complete target regardless of scissor and ColorWriteChannels, then restores both. Details on EasyGL clears |
VULKAN, WEBGPU, SDL_GPU | Recorded and replayed in submission order across deferred pass segments; see Vulkan ordered clears |
DIRECTX11, DIRECTX12 | Immediate native clears of the bound render-target and depth-stencil views |
DIRECTX9 | Native clear whose depth and stencil flags follow the active target's depth format, tracked on every target bind (it formerly consulted the back-buffer format even while a differently formatted target was bound) |
SDL_RENDERER, CANVAS, FREEDIRECT and the other 2D-only families | No depth or stencil plane, so Clear(Color) sends colour only and an explicit depth or stencil request throws in the device before any 2D hook is reached. GDI is the exception for stencil: it keeps a separate 8-bit CPU stencil plane, reports StencilBuffer, and clears it through ClearStencil and ClearColorAndStencil; a depth request still throws |
SOFTWARE | Colour, depth and stencil cleared in its CPU arrays |
HEADLESS | Validates and records the request; there are no attachments to clear |
No capability member reports whether a clear is aspect-selective or how it orders against surrounding draws. A test must prime each aspect, clear only one, and then observe the others after intervening work: an unchanged colour pixel alone does not prove that a depth-only clear happened, and a colour check alone does not prove that a colour-only clear left depth alone. easygl_clear_overloads_test.cpp is an example of the overload matrix on one family.
Viewport and scissor
The shared rules
setViewportProperty and setScissorRectangleProperty validate against the active surface — a viewport outside the target, with a non-positive size or with an unordered or out-of-range depth range throws ArgumentException, as does a scissor rectangle outside the surface — then map the logical rectangle onto the presentation rectangle (MapLogicalRectToPresentation; the identity while a render target is bound, because a target's rectangles are in its own pixels), call the renderer hook, and only then commit the public value. The public property keeps the logical rectangle the game assigned. Scissor enable never travels with the rectangle: it arrives separately in ApplyRasterizerState from the current RasterizerState. A getter round trip therefore proves accepted bookkeeping, not rasterisation.
Binding a target resets both rectangles to the first target's size, and returning to the back buffer resets them to the back-buffer size; construction establishes both full rectangles. Present() and a reset that leaves the logical size and the physical presentation rectangle unchanged preserve a game-set viewport, because UpdateViewportFromWindow resets only when one of them changed (GraphicsDevice internals). No capability member reports viewport or scissor support.
What each family does
| Families | Viewport and scissor at this snapshot |
|---|---|
EasyGL, OPENGL4 | Applied immediately to GL state, Y-flipped against the active target's height, for the back buffer, 2D targets and cube faces; EasyGL's SpriteBatch keeps a custom sub-viewport and builds its projection from the viewport's logical size |
VULKAN, WEBGPU, SDL_GPU | Captured per deferred draw and replayed with it; see the family pages |
DIRECTX12 | Sets the effective viewport and the effective scissor on every draw, sprite batches included; it formerly installed a full-target scissor and made scissored sprite drawing a no-op |
DIRECTX9, DIRECTX11, DIRECT2D, GDI, METAL, FNA3D, PORTABLEGL, HTML_DOM, SVG_DOM | Override both hooks and apply them through their own native or browser state (PortableGL passes viewport, depth range and scissor to real PortableGL state) |
SOFTWARE | Snapshots the viewport transform and a clip rectangle (framebuffer ∩ viewport ∩ scissor when enabled) per draw, for 2D and 3D alike |
SDL_RENDERER | No viewport hook. SetScissorRect sets SDL's clip rectangle for any non-empty rectangle and the family has no rasterizer-state override, so ScissorTestEnable is ignored: a game that assigns a scissor rectangle with the test disabled is still clipped |
CANVAS, FREEDIRECT | Neither hook is overridden; viewport and scissor have no effect |
HEADLESS | Validates and records; nothing is rasterised |
The SDL_RENDERER scissor behaviour is a divergence from XNA recorded for Known Issues review; the user-level description of both properties is on Graphics state: Viewport and ScissorRectangle. easygl_scissor_test.cpp (disabled, enabled, disabled again) is the shape of a test that a wrong implementation fails.
Evidence and limits
The shared rules were read in GraphicsDevice.cpp at 009d40f5; the family rows come from reading the named hooks or, where a row says only that a family overrides a hook, from a source search. No test was executed; the Direct3D, Direct2D, GDI, Metal and browser rows in particular were not observed on their hosts.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Architecture
- Graphics architecture: state and render targets
- Maintainer workflow
- Fix a renderer bug
- Tests and validation
- Test architecture: GPU tests