Direct3D presentation, swap interval, clears, viewport and scissor

CNA snapshot 009d40f5  ·  Deep Dives › Renderers  ·  source links pinned to 009d40f5

✓

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. Read from the renderer sources and fixture registrations at 009d40f5; no test was executed. Half-rate pacing, tearing and exclusive full screen are not verified by any located test.

The three Direct3D renderers share a name but not a presentation model. DIRECTX9 recreates its whole device state through Reset(), while DIRECTX11 and DIRECTX12 present through a DXGI flip-model swap chain and compute a logical presentation rectangle inside it. This page compares, renderer by renderer, what happens to the five presentation modes, to PresentInterval, to the back-buffer and depth formats and the full-screen flag, to viewport and scissor, to clears and to depth bias. It is for porters who rely on any of these on Windows and for maintainers deciding whether a difference is a bug or a boundary. The cross-renderer contract these rows belong to is on presentation and back-buffer readback and targets, clears and viewports.

Presentation modes: DXGI renderers scale, DIRECTX9 does not

PresentationMode is a CNA extension (NativeBackBuffer, FixedHeightDynamicWidth, Stretch, Letterbox, Overscan); GraphicsDeviceManager forwards its choice to the device and the device to the renderer before every Reset. What the renderer does with it differs:

  • DIRECTX11 and DIRECTX12 keep drawing straight into the physical swap chain and map the game's logical canvas into it. Both call the shared ComputeD3DPresentationGeometry (D3DPresentation.hpp): GetViewportSize() returns the logical size and GetDefaultViewportRect() the physical destination rectangle, and GraphicsDevice maps every public viewport and scissor through that rectangle (see viewport and scissor). NativeBackBuffer (or no virtual size) keeps logical equal to physical; FixedHeightDynamicWidth keeps the virtual height and widens the logical width to the window's aspect; Stretch fills the drawable with the virtual size; Letterbox scales uniformly by the smaller axis ratio and centres the result; Overscan by the larger ratio, cropping. There is no separate logical render target and no final composite pass: the scaling is the viewport transform itself.
  • DIRECTX9 stores the mode and the virtual resolution and never reads either: its GetViewportSize() returns the back buffer's pixel size, which follows the window's drawable size, and it inherits the interface's default GetDefaultViewportRect() (origin, same size). On DIRECTX9 every mode therefore behaves like NativeBackBuffer.

The shared fixture presentation_mode_contract_test.cpp (DX-217) states the renderer-neutral contract for all five modes and is registered for both DXGI renderers through the DirectX parity inventory. A related gap is on the input side: CNA's input-model reading finds that DIRECTX11 and DIRECTX12 implement the window-to-logical transforms but never register them for their window, so mouse coordinates are not mapped through a letterbox (logical coordinates).

Back-buffer readback follows the same geometry

A GetBackBufferData() rectangle is in the game's logical space. When the presentation is not the identity, the DXGI renderers sample the physical back buffer at the centre of each logical pixel (presentX + floor((x + 0.5) * scaleX), likewise for y) and zero-fill samples outside the resource; before this, a 16×16 logical back buffer in a window that Windows will not make narrower than about 120 pixels read back the letterbox bars. When logical and physical agree the copy is direct. A device with no back buffer throws NotSupportedException by name rather than returning a zero-filled "successful" frame.

One PresentInterval::Two, three implementations

GraphicsDevice converts PresentInterval to 0 (Immediate), 2 (Two) or 1 (Default, One) and passes it to SetSwapInterval.

RendererRepresentationWhen it appliesFailure behaviour
DIRECTX9D3DPRESENT_PARAMETERS.PresentationInterval: INTERVAL_IMMEDIATE, INTERVAL_TWO or INTERVAL_ONEimmediately: a change marks presentation dirty and runs the same Reset() path as a resize, because Direct3D 9 has no per-present interval argumenta failed Reset() is logged, the previous size is restored, the dirty flag stays set and every later Present() retries; the renderer does not roll back the public PresentationParameters
DIRECTX11, DIRECTX12swapInterval_, plus vsyncEnabled_ = interval > 0at the next Present(), which passes max(0, interval) as DXGI's sync interval, so 2 reaches DXGI as 2a failed Present() is checked for device removal and logged, not thrown

Two cautions remain. Microsoft's Direct3D 9 contract allows INTERVAL_TWO only when D3DCAPS9::PresentationIntervals contains it, and a windowed device supports only default, immediate and one; DIRECTX9 checks neither, and an ordinary CNA game window is windowed, so requesting Two there can make Reset() fail into the retry loop above. On the DXGI renderers the tearing flag (DXGI_PRESENT_ALLOW_TEARING) is used only when tearing is supported, requested, the interval is 0 and the swap chain is not in exclusive full screen, which it never is. The shared swap_interval_forwarding_contract_test.cpp proves only that toggling SynchronizeWithVerticalRetrace reaches the renderer as interval 0 or 1 (it reads GetSwapIntervalEXT(); it never sets PresentInterval::Two and cannot see what DXGI receives, so 2 reaching DXGI rests on reading Present()); no located test measures half-rate pacing or the no-vsync tearing branch on any of the three, and DXVK's synthesized capability structure could not replace a real Windows check anyway. A stored PresentInterval::Two is not evidence of half-refresh pacing.

ℹ

Earlier descriptions of the DXGI renderers said they collapse every interval to 0 or 1. At this snapshot Present() passes the stored interval itself; only the tearing decision uses the Boolean.

Back-buffer format, depth format and full screen

  • DIRECTX9 maps the requested back-buffer SurfaceFormat to a D3DFORMAT, substituting A8R8G8B8 for Color's A8B8G8R8, which Direct3D 9 rejects for a swap chain. Depth maps None, Depth16, Depth24 and Depth24Stencil8 to no attachment, D3DFMT_D16, D3DFMT_D24X8 (a real depth-only format) and D3DFMT_D24S8, and Windowed = !IsFullScreen. A manager Reset reaches UpdatePresentationFormatEXT(), which resets the native device at once rather than at the next Present(), so a game that clears depth on its very first frame sees the requested surface. The mapping is not a guarantee that a format is a legal display mode, and a failed reset can leave the old attachments active while public state has changed.
  • DIRECTX11 and DIRECTX12 keep the colour format fixed at R8G8B8A8_UNORM (reported as Color) and ignore the full-screen argument, but the default depth attachment now follows PresentationParameters.DepthStencilFormat: None allocates nothing and Depth24 shares D24_UNORM_S8_UINT storage with its stencil kept out of reach, because DXGI has no 24-bit depth-only format. A depth-format change recreates the default surfaces immediately. d3d_presentation_format_contract_test.cpp checks that the reported and the native formats agree. Neither renderer calls SetFullscreenState or passes a full-screen descriptor to CreateSwapChainForHwnd, and their exclusiveFullscreen_ stays false: an SDL full-screen window is not DXGI exclusive full screen.
  • Windowless DIRECTX12 (HeadlessEXT) has no swap chain, but it is not target-less: it renders into an implicit off-screen R8G8B8A8 back buffer with the same depth attachment rules (see HeadlessEXT).

Flip-model presentation unbinds the back buffer

Both DXGI swap chains use DXGI_SWAP_EFFECT_FLIP_DISCARD with two buffers, and a flip-model Present() unbinds the back buffer from the output merger. Clear() names its render-target views explicitly and never noticed; every draw used whatever OMSetRenderTargets last bound, which after a present was nothing. From the second frame on a game showed the right clear colour and none of its geometry, and no readback test caught it because every one read before its first present. DirectX11Renderer::Present() now rebinds exactly the tracked set, leaving the viewport and any game-bound target as they were. DirectX12Renderer::Present() rebinds the next frame's back buffer and then restores the game's custom viewport, because GraphicsDevice re-pushes a viewport after a present only when the window size changed, and resetting it would put every later frame's draws outside a letterbox rectangle (DX12-0020).

Viewport and scissor: immediate on two, recorded per draw on one

RendererViewportScissor
DIRECTX9SetViewport immediately, through the checked wrapperSetScissorRect immediately; D3DRS_SCISSORTESTENABLE from RasterizerState
DIRECTX11RSSetViewports immediatelyRSSetScissorRects immediately; enable bit in the rasterizer state object
DIRECTX12stored; every draw, instanced draw, SpriteBatch flush and compiled-effect draw records GetEffectiveViewportEXT(): the custom rectangle unclamped (it is a transform, not a clip), depth clamped to [0, 1], or the full bound target when no positive rectangle is setstored; every draw records GetEffectiveScissorEXT(). Direct3D 12 has no scissor-enable bit, so a disabled test means "the whole bound target"; an enabled rectangle is clamped to the target (Direct3D 12 rejects one that leaves it) and a degenerate or fully outside one becomes empty and rasterises nothing

SpriteBatch derives its projection from the effective viewport on all three, so sprite coordinates stay local to a sub-viewport instead of being transformed twice. An enabled zero-area scissor clips everything on DIRECTX11 and DIRECTX12, while the Vulkan and GL families expand it to the full target; CNA's Deferred_Scissor fixture records that divergence per renderer rather than hiding it. Stencil reference and blend factor are command-list state on DIRECTX12 (OMSetStencilRef, OMSetBlendFactor), so changing them costs no pipeline rebuild; DIRECTX11 rebinds its current state object with the new value.

Evidence differs in strength. The Direct3D 9 plan calls its viewport and scissor "oracle-proven", but the oracle corpus has no viewport or scissor scene (see the DIRECTX9 oracle notes); the supportable claim is native implementation plus the shared DirectX9_Deferred_Viewport and DirectX9_Deferred_Scissor fixtures. The DXGI renderers register ViewportState, Viewport_Subregion, Scissor, Deferred_Viewport, Deferred_Scissor, SpriteBatch_CustomViewport and RenderTarget_ViewportScissorReset from the shared inventory.

Clears: native bits, one public contract

  • DIRECTX11 clears every bound render-target view and, only when a depth-stencil view is bound, selects the depth and stencil clear bits independently. Inside the renderer a depth or stencil clear with no view bound is a no-op (Direct3D 11 guards on its bound view, Direct3D 12 on boundDsv_), but the public GraphicsDevice::Clear never lets such a request through: it throws InvalidOperationException first, as XNA 4.0 does (see clear masking).
  • DIRECTX12 records the same selection into the frame's command list through one helper, ClearImpl, which transitions and clears every bound target of an MRT set.
  • DIRECTX9 builds IDirect3DDevice9::Clear flags from the depth format of the currently bound attachment (activeDepthStencilFormatOrdinal_, updated by every successful target transition), so a bound target without depth never receives a depth-clear bit and one without stencil never receives a stencil bit. Earlier descriptions said the flags consulted the back buffer's presentation format; that is no longer the code.

Above the renderer, GraphicsDevice derives depth and stencil availability from the bound 2D or cube attachment itself, and the ordered-clear fixture (GraphicsDevice_OrderedClear) is registered for all three, the clear-overload fixture (ClearOverloads) for the two DXGI renderers. The public masking rules are on clear masking.

Depth bias: floats on Direct3D 9, integers on DXGI

XNA's RasterizerState.DepthBias is a normalised float. DIRECTX9 passes it and SlopeScaleDepthBias straight through as the bit pattern of D3DRS_DEPTHBIAS and D3DRS_SLOPESCALEDEPTHBIAS, which are floats. Direct3D 11 and 12 take an integer count of the bound depth format's smallest resolvable step, so XnaDepthBiasToD3D (D3DStateMapping.cpp) scales by 65,535 for D16, 16,777,215 for D24S8 and 8,388,607 for 32-bit float depth, saturates at the int32 limits and maps NaN or an unknown format to 0. On DIRECTX12 the result is part of the pipeline-state key, computed per bound depth format.

Read in this order

  1. D3DPresentation.hpp: the whole presentation geometry.
  2. DirectX11Renderer.cpp: Present, GetViewportSize, ReadBackbuffer, UpdatePresentationFormatEXT, SetSwapInterval.
  3. DirectX12Renderer.cpp: the same methods plus GetEffectiveViewportEXT, GetEffectiveScissorEXT and ClearImpl.
  4. DirectX9Renderer.cpp: BuildPresentParameters, EnsureDeviceSize and the clear family.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Tests and validation
Test architecture: GPU tests