CNA_GRAPHICS_RENDERER=DIRECT2D is CNA's Windows-only, SpriteBatch-oriented 2D renderer. It is
graphics-renderer identity 39 and never falls back to another renderer. CMake rejects the selection
deterministically on non-Windows targets.
Application drawing is issued through ID2D1DeviceContext. A BGRA-capable D3D11 device and a DXGI
1.2 flip-sequential swap chain exist only to create the Direct2D device/surfaces and present an
HWND. They are not an application-visible D3D11 renderer. The renderer does not use DirectWrite,
WIC, SDL_Renderer, or a hidden 3D/compositing pass. Use CNA_GRAPHICS_RENDERER=D3D11 for 3D.
- The only texture, render-target, and backbuffer format is XNA
SurfaceFormat::Color. Public RGBA bytes are converted byte-exactly to Direct2D's nativeDXGI_FORMAT_B8G8R8A8_UNORMstorage and back on readback. Odd widths, padded row pitches, asymmetric RGB channels, nontrivial alpha, repeated updates, and short-pitch rejection have independent tests. Texture2Dsupports level zero and explicitly authored mip levels.SetData,GetData, device recovery, and SpriteBatch sampling use the selected authored level. Mip-linear filter families interpolate between two initialized authored levels; an incomplete chain falls back toward the nearest initialized level instead of sampling undefined data.RenderTarget2Dsupports one color target at level zero, rendering, full/partial readback, level-zero upload, sampling after unbind, all threeRenderTargetUsagevalues, and transparent reallocation after recovery. Mipmapped render targets are deliberately unsupported and fail at construction. The former generated-mip path was removed because its NPOT downsample omitted source quadrants (D2D-78).- SpriteBatch supports crop, origin, positive or negative source rectangles, horizontal/vertical
flips, rotation, nonuniform scale, batch transforms, viewport transforms, scissor clipping,
Clamp/Wrap/Mirror, and all nineTextureFiltervalues within the authored-Texture2D contract. Unknown filter, address, presentation, orSpriteEffectsvalues and non-finite transforms fail before native state changes. Opaque,AlphaBlend, andNonPremultipliedare supported. Exact symmetric Porter-Duff tuples map to Direct2D image-composite modes. Blend tuples without an exact Direct2D representation, channel masks, coverage masks, and non-white blend factors are rejected transactionally, with the previously accepted blend remaining active. In particular,BlendState::Additiveis XNASourceAlpha/One; it is not Direct2DOne/One. Because the renderer cannot implement that contract for every source type,AdditiveBlendingis reported false and Additive is rejected consistently rather than approximated.- A render target used as a SpriteBatch source stays GPU-resident. Native Direct2D built-in
ColorMatrix/Premultiplyeffects provide supported tint/straight-alpha decoration when the runtime exposes them. A runtime that reports the effects unregistered receives a namedNotSupportedExceptionfor that exact decorated path; unexpected or device-loss HRESULTs are never converted into a compatibility skip. - Sampling the currently bound render target is rejected as a read/write alias.
Presentwhile a render target remains bound is also rejected; callers must unbind explicitly.SetDataduring active drawing commits outstanding commands first, so an Immediate-mode draw keeps the old bitmap snapshot and the next draw observes the successful update. - The logical framebuffer implements Letterbox, Overscan, Stretch, NativeBackBuffer, and
FixedHeightDynamicWidth. Backbuffer readback remains in logical coordinates. Empty-frame
Presentstill observes an SDL client resize. Final presentation uses linear Direct2D interpolation. - CNA sprite, viewport, and scissor coordinates are logical-framebuffer pixel units, not Windows
DIPs. The Direct2D context and every bitmap are forced to 96 DPI so one Direct2D unit equals one
logical target pixel.
NativeBackBufferis 1:1 with client pixels; the other presentation modes map the logical framebuffer into the client-pixel swap chain. SDL3's Win32 HWND property supplies the native target. Deterministic conversion/resize tests run under Wine; physical multi-monitor DPI and desktop-capture validation remain an external Windows gate. SpriteSortMode::Immediateissues each sprite duringDraw. Present interval zero calls the flip-model swap chain withPresent(0, 0); CNA does not advertise a tearing capability or addDXGI_PRESENT_ALLOW_TEARING. Intervals one and two use the corresponding synchronized interval.- Registered ordinary textures recover from their RGBA shadows. Render targets are recreated transparent. Device loss is classified consistently across Direct2D, D3D11, and DXGI HRESULTs; public lost/resetting/reset events fire once in order. Unregistered stale resources fail instead of being used on a new device generation.
A DPI change -- the user changing a monitor's scaling, or the window moving to a differently scaled monitor -- reaches this renderer as a change of the client rectangle in physical pixels, which the per-frame size check already acts on: the swap chain is resized and the backbuffer target and logical framebuffer are rebuilt at the new size, without a restart.
Nothing else has to react, and that is a consequence of D2D-54 rather than an omission. Every Direct2D surface is pinned at 96 DPI and every CNA coordinate is a physical client pixel, so there is no DIP-derived quantity to recompute when the scale changes. The logical/window round trip is derived from the backbuffer's own pixel size, so it follows the resize automatically.
What the renderer does do is observe the scale: with CNA_DIRECT2D_DIAGNOSTICS=1 a change of
SDL's window-units-to-pixels ratio is logged with both sizes. A run that claims presentation
evidence therefore records the DPI it actually ran at, instead of leaving it to be assumed.
Physical multi-monitor and non-96-DPI validation remains an external Windows gate.
Direct2D composes premultiplied colors. CNA's public surface does not, so the exact place where a straight-alpha value becomes premultiplied matters. Every boundary is listed here; there is no other conversion site in the renderer.
| Boundary | Representation | Conversion |
|---|---|---|
Texture2D::SetData / ImageData bytes |
Exactly the bytes the caller passed, RGBA order | None. Only the channel order changes (RGBA to native BGRA) |
Texture2D, RenderTarget2D, logical framebuffer storage |
DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED |
None; the stored bytes are taken as already premultiplied |
BlendState::AlphaBlend and BlendState::Opaque sources |
Premultiplied, as stored | None |
BlendState::NonPremultiplied sources |
Straight, as stored | Premultiplied exactly once, by the Direct2D Premultiply effect on the GPU path or by MakeSpritePixels on the CPU fallback |
SpriteBatch Color modulation |
Straight per channel | Color.RGB scales the (already premultiplied) RGB, Color.A scales only alpha. It is never applied twice |
ColorMatrix tint stage |
Premultiplied by default; D2D1_COLORMATRIX_ALPHA_MODE_STRAIGHT for NonPremultiplied and for Opaque |
The straight mode keeps Color.A from attenuating the copied RGB, whose source factor is One |
GraphicsDevice::Clear |
Straight D2D1::ColorF |
Direct2D premultiplies internally |
| Physical swap-chain surface | D2D1_ALPHA_MODE_IGNORE, DXGI_ALPHA_MODE_IGNORE |
None. It is the presentation destination only; application readback never reads it |
GetData / GetBackBufferData |
Premultiplied bytes of the read surface, RGBA order | None. Only the channel order changes (native BGRA to RGBA) |
| Readback staging bitmap | Inherits the source bitmap's pixel format | None |
Two consequences are worth stating explicitly. Readback returns premultiplied bytes even for
content uploaded as straight alpha under NonPremultiplied, because the conversion happened on the
way in and is not undone. And no application-visible bitmap uses an alpha-ignoring format: the only
alpha-ignoring surface in the renderer is the physical backbuffer, which nothing reads.
The capability query is an exhaustive switch over all 13 current GraphicsCapability values:
| Capability | Direct2D |
|---|---|
ThreeD |
false |
DepthStencilBuffer |
false |
MultiSampleAntiAliasing |
false |
MultipleRenderTargets |
false |
AnisotropicFiltering |
true |
WireFrame |
false |
OcclusionQuery |
false |
CustomEffects |
false |
Texture3D |
false |
MultiStreamVertexInput |
false |
Instancing |
false |
StencilBuffer |
false |
AdditiveBlending |
false |
The fixed depth format is DepthFormat::None; no real depth or stencil buffer exists. A
multisample request other than the non-multisampled 0/1 convention is rejected. Multiple render
targets, vertex/index buffers, ordinary/indexed/instanced 3D draws, cube/volume textures and
targets, queries, wireframe, depth bias, custom effects, and 3D clear/state operations are outside
this renderer. The policy-aware 3D entry points throw by default and return only inert safe handles
when Unsupported3DGraphicsCallBehavior::WarnAndStub was explicitly selected.
The Direct2D CTest label contains five sequential tests:
Direct2D_Smoke: HWND, D3D11 staging readback, and point SpriteBatch draw.Direct2D_2DParity: public pixel, transform, update, render-target, presentation, recovery, capability, and deterministic-rejection oracles.Direct2D_Lifetime: repeated target switching, readback, recovery, resize, and resource churn.Direct2D_Unit: theDirect2D*GoogleTest subset fromCnaTests, run through the dedicated Direct2D Wine/Proton runner and prefix.Direct2D_Soak: per-branch SpriteBatch throughput and a long resize/recovery/target-switch soak with a process working-set oracle. The throughput numbers are reported, not gated, unlessCNA_DIRECT2D_BENCH_BASELINE_MSsupplies a baseline recorded on comparable hardware -- a timing threshold on shared CI, on WARP, or under Wine is either too loose to prove anything or fails at random.CNA_DIRECT2D_SOAK_CYCLESraises the default 2000 cycles for an acceptance run.
A bounded cross-build and run uses:
cmake --build cmake-build-direct2d-integration --parallel 2 \
--target CnaTests cna_test_direct2d_smoke cna_test_direct2d_2d_parity \
cna_test_direct2d_lifetime cna_test_direct2d_soak
scripts/run-direct2d-virtual-display.sh \
ctest --test-dir cmake-build-direct2d-integration -L Direct2D -VDirect2D always presents to a real HWND, so every run needs a display.
scripts/run-direct2d-virtual-display.sh is the single canonical decision: an isolated Xvfb at an
explicit geometry (CNA_DIRECT2D_XVFB_SCREEN, default 1280x800x24), a private X authority file,
and cleanup on every exit path. It is reentrant, so a ctest already launched under it is reused
instead of nesting a second server, and both run-wine-direct2d.sh and run-proton-direct2d.sh
enter it automatically. CNA_DIRECT2D_USE_HOST_DISPLAY=1 opts out for interactive debugging.
scripts/run-direct2d-fresh-wine-suite.sh <build-dir> runs the whole label against a prefix created
from nothing (wineboot --init in a temporary directory, removed afterwards unless
CNA_DIRECT2D_KEEP_PREFIX=1), so a pass cannot depend on a developer's accumulated ~/.wine state.
The prefix deliberately receives no winetricks package, no DLL override, and no DXVK install: the
supported contract is Wine's own built-in d2d1/d3d11/dxgi.
The Proton lane is pinned by scripts/direct2d-proton-pin.txt, a priority-ordered list of Steam
library directory names. Proton - Experimental is refused unless
CNA_DIRECT2D_PROTON_ALLOW_EXPERIMENTAL=1 is set for a one-off investigation, because Steam
upgrades it underneath a passing run and such a result cannot be reproduced. Every run publishes the
resolved runtime identity — Proton directory, build id, bundled Wine build, and DXVK version — to
stderr and, with CNA_DIRECT2D_PROTON_IDENTITY_FILE, to an artifact file.
CNA_DIRECT2D_PROTON_DRY_RUN=1 resolves and publishes that identity without running anything.
scripts/verify-direct2d-parallel-jobs.sh rejects missing, nonnumeric, or greater-than-two build
parallelism in the Direct2D workflow/helpers. CNA_ENABLE_NET=OFF excludes only tests belonging to
the omitted Net and GamerServices modules; it retains the Direct2D unit subset.
scripts/verify-direct2d-debug-log.py turns the native debug-layer output into a gate rather than
an artifact. With CNA_DIRECT2D_DEBUG_LAYER=1 the renderer emits every stored D3D11 message as
[Direct2D D3D11 debug] severity=<S> category=<C> id=<I> <description> and closes the report with
[Direct2D diagnostics] live-object report end.. The parser fails on a message of severity ERROR
or CORRUPTION, on a live object outside the whitelist of objects CNA itself still holds when it
reports (ID3D11Device, ID3D11DeviceContext, ID3D11Debug, ID3D11InfoQueue), on a run whose
diagnostics or debug layer never engaged, and on a truncated report. Its own classification is
regression-tested against the committed positive/negative fixtures in tests/fixtures/direct2d/:
python3 scripts/verify-direct2d-debug-log.py --self-test
python3 scripts/verify-direct2d-debug-log.py build/direct2d-native-diagnostics/*.logscripts/direct2d_mutation_check.py proves the suite can actually go red. It introduces one
deliberate defect at a time -- a blend-factor mapping, the Porter-Duff coefficients, the letterbox
scale, the mip rounding, a dropped validation -- and requires the named test to fail. --dry-run
only checks that each anchor still matches exactly once (the part that goes stale after a refactor)
and needs no build; --run <build-dir> performs the full apply, rebuild, and expect-failure pass.
scripts/validate_direct2d_plan.py keeps plan_direct2d.md honest about
its own state. It fails on a status outside the documented set, a duplicated task, a ✅ row citing
no evidence at all, a cited path/CTest name/CNA_DIRECT2D_* variable that does not exist, a
reference to a task without a row, an incomplete row missing from the plan's closing classification
table, and a stale direct2d-plan-status summary marker. Run it after any status change:
python3 scripts/validate_direct2d_plan.pyTwo further documents complete the picture:
direct2d-runtime-support-matrix.md gives every
runtime-dependent branch one of four dispositions (supported, fallback, named reject, open task)
across Windows, WARP, Wine, and Proton, and
direct2d-release-gate.md defines the release criteria and the command
that evaluates them.
Wine 10.0 is useful evidence for the supported portable/native-API surface but is not physical
Windows. WineD3D does not register Direct2D's built-in ColorMatrix effect and does not implement
the bounded-copy image composite used by the Opaque pixel oracle. The compatibility run therefore
sets only the narrow CNA_DIRECT2D_SKIP_RENDER_TARGET_DECORATION and
CNA_DIRECT2D_SKIP_ADVANCED_BLEND branches. Those skips do not claim native coverage.
The manual Windows graphics workflow is prepared to run MSVC with the Direct2D/D3D11 debug layers,
hardware and WARP lifetime passes, diagnostics, and live-object output. Native built-in-effect and
composite pixels, physical display/DPI/presentation capture, adapter-specific behavior, and
debug-layer/live-object acceptance remain external evidence limits until a real x64 Windows run is
recorded. See plan_direct2d.md for that evidence backlog and nonblocking
performance/process work.