Tutorial 106: 2D Vector Renderers
What you’ll learn
- What
SKIA,BLEND2DandOPENVGgive you that the general-purpose 2D renderers do not. - Why
SKIAis the only renderer whereTexture2Daccepts a format other thanSurfaceFormat::Color. - The external Skia build these renderers' setup differs over, and the two CMake paths that gate it.
OPENVG's two hard constraints: an LGPL dependency, and one live renderer per process.
Before you start — Tutorial 21: Mastering SpriteBatch, since all three renderers implement the 2D drawing surface and nothing beyond it.
Three of CNA's renderers are built on real 2D vector-graphics engines: Google's Skia, Blend2D, and OpenVG through ShivaVG. All three are 2D-only — there is no 3D pipeline, and 3D calls throw deterministically rather than drawing nothing. They exist for cases where the quality, format range or authenticity of a real 2D engine matters more than a 3D pipeline you were never going to use.
| Renderer | Engine | Platforms | Dependency |
|---|---|---|---|
SKIA | Skia, CPU raster mode | Desktop | An externally built Skia. Never fetched by CMake. |
BLEND2D | Blend2D with its AsmJit JIT pipeline compiler | Desktop | Fetched at configure time, both Zlib-licensed, pinned to exact commits. |
OPENVG | ShivaVG, an OpenVG 1.1 implementation on fixed-function desktop OpenGL | Linux, Windows, macOS only | Fetched at configure time. LGPL-2.1. |
SKIA: the one renderer with a wider texture format range
CNA's shared Texture layer admits exactly one surface format — SurfaceFormat::Color — and every renderer inherits that boundary. Skia is the single exception, and it is a large one.
Under SKIA, Texture2D additionally accepts the packed 16-bit colour formats, BGRA and sRGB colour, single-channel and two-channel UNORM, the 32-bit and 16-bit float families, both normalised-byte formats, Alpha8, HdrBlendable, and the compressed Dxt1/Dxt3/Dxt5 and BC7 families. Each stores in its own native layout with an exact CPU transfer chain.
Two consequences to plan around. Compressed formats have no mip generation at all — there is no block encoder on this path, so every level of a compressed texture must be explicitly authored. And RenderTarget2D promotes a narrower set than Texture2D: only the thirteen non-Color formats real XNA and FNA report as renderable. Packed 16-bit colours, every compressed format, both SNORM formats, Alpha8 and BGRA colour stay permanently refused as render targets, matching real hardware.
Beyond formats, Skia's 2D surface is unusually complete: every SpriteBatch overload and sort mode, all nine TextureFilter ordinals with independently selected per-axis Clamp/Wrap/Mirror, a complete generated blend path covering every valid factor and function tuple with independent RGB and alpha equations, live blend constants, target-0 colour-write masks, and mipmapped render targets with deterministic dirty-descendant resolution.
Building against Skia
This is the one CNA renderer whose dependency you must build yourself. Skia uses GN and brings a large revision-locked dependency graph, and a CNA configure has to stay offline and deterministic, so nothing is downloaded for you. Two CMake cache paths are required, and each has its own FATAL_ERROR if it is missing or points at the wrong thing:
# 1. Build the pinned Skia revision in its minimal raster configuration
bin/gn gen /path/to/skia-out/raster --args='is_official_build=true is_debug=false \
cc="clang" cxx="clang++" skia_use_gl=false skia_enable_ganesh=false \
skia_use_vulkan=false skia_use_dawn=false skia_enable_graphite=false \
skia_enable_pdf=false skia_use_freetype=false skia_use_fontconfig=false \
skia_use_libpng_decode=false skia_use_libjpeg_turbo_decode=false \
skia_use_libwebp_decode=false skia_use_wuffs=false skia_use_icu=false \
skia_enable_tools=false'
ninja -C /path/to/skia-out/raster
# 2. Point CNA at the source checkout and the GN output directory
cmake -S . -B build-skia \
-DCNA_GRAPHICS_RENDERER=SKIA \
-DCNA_SKIA_ROOT=/path/to/skia \
-DCNA_SKIA_BUILD_DIR=/path/to/skia-out/raster
cmake --build build-skia -j3
| Configure error | Cause |
|---|---|
SKIA requires -DCNA_SKIA_ROOT=... | The header root is unset, or does not contain include/core/SkSurface.h. |
SKIA requires -DCNA_SKIA_BUILD_DIR=... | The GN output path is unset or has no libskia.a. Pass the exact raster output directory, not its parent. |
SKIA build directory is incomplete | The minimal raster build produces six mutually dependent archives; one is missing. CNA keeps them in a link group deliberately, because reordering them produces unresolved symbols even when every file is present. |
The selected artefact is CPU raster. This renderer deliberately does not create a Skia GPU context; SDL may internally choose an accelerated path purely to present the finished CPU image, and that is not a Skia GPU mode. Skia is distributed under a BSD-style licence — a packaged distribution needs to carry its upstream notice.
BLEND2D: the low-friction one
BLEND2D is the easiest of the three to build. Blend2D and AsmJit are both fetched at configure time, pinned to exact commits, and built as static archives:
cmake -S . -B build-blend2d -DCNA_GRAPHICS_RENDERER=BLEND2D -DCNA_BUILD_TESTS=ON
cmake --build build-blend2d -j3
A fresh configure needs network access unless CNA_BLEND2D_ROOT and CNA_ASMJIT_ROOT point at existing local checkouts. AsmJit is required because CNA selects Blend2D's JIT pipeline compiler — the upstream default and primary supported configuration — rather than the still-experimental non-JIT reference path.
The renderer owns a premultiplied Blend2D image and context, rasterises into it, reads the finished frame back, and presents it through an SDL streaming texture. SDL never executes a Blend2D command; it only displays the result. CNA's transfer contract is straight, non-premultiplied, top-row-first RGBA8 everywhere, so every transfer in or out converts explicitly — there is no raw byte copy anywhere in this renderer.
Two boundaries to know. Only the four stock blend presets have a real Blend2D operator; every other BlendState combination throws rather than quietly falling back to source-over. And ColorWriteChannels is honoured for render-target slot 0 only, via a bounded whole-surface merge when the mask is not the default; the other slots and a non-default multisample mask are rejected. Mip chains and MSAA are not implemented, and the public properties report that truthfully rather than echoing what you requested.
OPENVG: real OpenVG, with two hard constraints
OPENVG drives genuine vg* entry points — vgClear, vgCreateImage, vgDrawImage, vgDrawPath — through ShivaVG, on a desktop OpenGL compatibility-profile context CNA creates and owns itself. It is not a reimplementation of OpenVG semantics.
ShivaVG is LGPL-2.1 — the only copyleft dependency among CNA's renderers. CNA itself is Ms-PL, and every other renderer dependency on this page is permissive (Zlib for Blend2D and AsmJit, BSD-style for Skia). If your distribution terms matter, this is the renderer to check with a lawyer before shipping.
One live OPENVG renderer per process. The pinned ShivaVG revision keeps a single process-global context: a second vgCreateContextSH silently reuses the first, and destroying either frees it for both. CNA enforces the safe version of that with a process-wide ownership flag, so constructing a second OpenVgRenderer while one is alive throws immediately. Sequential construct/destroy/construct cycles are fully supported.
OPENVG is also the renderer that reports false for all thirteen capabilities, each for its own recorded reason. Two of those falses are unusually informative:
BlendState::Additiveis rejected outright.VG_BLEND_ADDITIVEis declared by the OpenVG 1.1 specification, but the pinned ShivaVG revision has no case for it in its own blending state machine — it silently falls through to normal alpha blending. CNA throws rather than making a false capability claim.- There is no
RenderTarget2Dat all. ShivaVG has no equivalent of binding an off-screen image as a draw target, soSetRenderTargetthrows transactionally, before any state changes, rather than leaving the real backbuffer bound whileGraphicsDevicebelieves a target is active.
Other real limits: an out-of-bounds source rectangle combined with Wrap or Mirror throws (in-bounds rectangles and Clamp, the default, work — Clamp overflow is implemented with a real CPU-side edge-extended image); wireframe fill and non-zero depth bias throw; and DepthStencilState::None is accepted, which is exactly what SpriteBatch::Begin() applies by default, so ordinary 2D usage is unaffected.
cmake -S . -B build-openvg -DCNA_GRAPHICS_RENDERER=OPENVG -DCNA_BUILD_TESTS=ON
cmake --build build-openvg -j3
Choosing between them
| You need | Pick |
|---|---|
A texture format other than SurfaceFormat::Color | SKIA — the only renderer that offers one |
Arbitrary BlendState factor and function combinations | SKIA — the other two accept only the stock presets |
| A 2D vector renderer with no external build step | BLEND2D |
| Genuine OpenVG semantics | OPENVG, having read the licence and single-instance notes above |
| Render targets | SKIA or BLEND2D — OPENVG has none |
| Maximum portability for a plain 2D game | None of these — SDL_RENDERER, see Tutorial 72 |
All three refuse 3D, MSAA, depth and stencil, occlusion queries and arbitrary custom effects. If you need any of those, you are on the wrong page — and if you need none of them, note that these renderers make that refusal explicit rather than letting a 3D call quietly do nothing.