Direct3D presentation, swap interval, clears, viewport and scissor

CNA snapshot 3b43bd86  ·  Deep Dives › Renderers  ·  source links pinned to 3b43bd86

✓

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 3b43bd86; unless a sentence says otherwise, nothing here was built or executed. Read from the renderer sources and fixture registrations at c6dec91f and re-checked at 3b43bd86; no test was executed. Half-rate pacing, tearing and exclusive full screen are not verified by any located test.

DIRECTX9 is CNA’s Direct3D renderer, and its presentation model is Direct3D 9’s: there is no per-present interval and no resizable swap chain, so every change to the presentation recreates the device state through IDirect3DDevice9::Reset(). This page states what happens to the five presentation modes, to PresentInterval, to the back-buffer and depth formats and the full-screen flag, to back-buffer readback, 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. On Windows the default renderer is SDL_GPU, whose presentation runs through SDL’s Direct3D 12 GPU driver and is covered with the other renderers on presentation and back-buffer readback and targets, clears and viewports.

Presentation modes: five rules, one geometry

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. DIRECTX9 applies the five rules with its own geometry code (DirectX9Renderer.cpp): GetViewportSize() returns the logical size (the window's client size for NativeBackBuffer or no virtual size, the widened width for FixedHeightDynamicWidth, the virtual size otherwise), and GetDefaultViewportRect() returns the centred, scaled destination for Letterbox (smaller axis ratio) and Overscan (larger ratio, cropping) and the whole back buffer for the others. There is no separate logical render target and no final composite pass: the scaling is the viewport transform itself, and GraphicsDevice maps every public viewport and scissor through that rectangle (see viewport and scissor). Its window-to-logical transforms use the same rectangle, and Direct3D 9's half-pixel correction stays in physical pixels (FULLSCREEN-002, with a resize and motion regression in directx9_resize_state_test.cpp).

The shared fixture presentation_mode_contract_test.cpp states the renderer-neutral contract for all five modes; at this snapshot it is registered for EasyGL and SDL_GPU, not for DIRECTX9. A related gap is on the input side: DIRECTX9 implements the window-to-logical transforms but never registers itself for its window, so the mouse and touch paths, which look the renderer up by window, do not map coordinates through a letterbox (logical coordinates).

Back-buffer readback

ReadBackbuffer copies the requested rectangle from the physical back buffer through an off-screen system-memory surface, zero-filling rows and columns that fall outside it. Only the two channel orders the format table maps can be read, Color (A8B8G8R8) and ColorBgraEXT (A8R8G8B8, swizzled back to RGBA); any other back-buffer format throws. The rectangle is not remapped through the presentation geometry above, so under Letterbox or Overscan a logical rectangle reads physical pixels.

PresentInterval through Reset()

GraphicsDevice converts PresentInterval to 0 (Immediate), 2 (Two) or 1 (Default, One) and passes it to SetSwapInterval. DIRECTX9 stores it in D3DPRESENT_PARAMETERS.PresentationInterval as D3DPRESENT_INTERVAL_IMMEDIATE, D3DPRESENT_INTERVAL_TWO or D3DPRESENT_INTERVAL_ONE. Direct3D 9 has no per-present interval argument, so a change marks the presentation dirty and runs the same Reset() path as a resize, immediately rather than at the next Present().

A failed Reset() is not retried as if nothing happened: Direct3D 9 permits only Reset, TestCooperativeLevel and Release on a device whose reset failed, so the renderer keeps the previous size, leaves the presentation dirty, marks the device lost and hands over to its device-loss recovery path; a Present() on a lost device only polls for recovery, and a SetViewport throws until recovery succeeds (DIRECTX9 fidelity describes the recovery). The renderer does not roll back the public PresentationParameters.

One caution remains. 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 recovery path above. The shared swap_interval_forwarding_contract_test.cpp proves only that toggling SynchronizeWithVerticalRetrace reaches a renderer as interval 0 or 1 (it reads GetSwapIntervalEXT() and never sets PresentInterval::Two), and it is registered for EasyGL and SDL_GPU; no located test measures half-rate pacing on DIRECTX9, 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.

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 leaves the device lost while public state has changed.

Viewport and scissor: applied immediately

DIRECTX9 calls SetViewport immediately through its checked wrapper and SetScissorRect immediately; D3DRS_SCISSORTESTENABLE comes from RasterizerState. SpriteBatch derives its projection from the effective viewport, so sprite coordinates stay local to a sub-viewport instead of being transformed twice.

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 (DirectX9 examples CMake).

Clears: native bits, one public contract

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. A clear covers the whole target, as XNA’s does: Direct3D 9’s own Clear is bounded by the viewport and an enabled scissor rectangle, so the renderer lifts both for the clear and restores them afterwards.

Above the renderer, GraphicsDevice derives depth and stencil availability from the bound 2D or cube attachment itself and throws InvalidOperationException for a depth or stencil clear with nothing to clear, as XNA 4.0 does; the ordered-clear fixture DirectX9_GraphicsDevice_OrderedClear is registered for DIRECTX9. The public masking rules are on clear masking.

Depth bias: floats on Direct3D 9

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, so no conversion to a depth-format-dependent integer step takes place.

Read in this order

  1. DirectX9Renderer.cpp: BuildPresentParameters, EnsureDeviceSize, GetPresentedRectEXT, ReadBackbuffer, ClearWholeTargetEXT and the clear family.
  2. directx9_resize_state_test.cpp: the resize, state and motion regression.

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

Tests and validation
Test architecture: GPU tests