Tutorial 106: 2D Vector Renderers

CNA Tutorials  ·  Renderers

What you’ll learn

  • What SKIA, BLEND2D and OPENVG give you that the general-purpose 2D renderers do not.
  • Why SKIA is the only renderer where Texture2D accepts a format other than SurfaceFormat::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 startTutorial 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.

RendererEnginePlatformsDependency
SKIASkia, CPU raster modeDesktopAn externally built Skia. Never fetched by CMake.
BLEND2DBlend2D with its AsmJit JIT pipeline compilerDesktopFetched at configure time, both Zlib-licensed, pinned to exact commits.
OPENVGShivaVG, an OpenVG 1.1 implementation on fixed-function desktop OpenGLLinux, Windows, macOS onlyFetched 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 errorCause
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 incompleteThe 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::Additive is rejected outright. VG_BLEND_ADDITIVE is 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 RenderTarget2D at all. ShivaVG has no equivalent of binding an off-screen image as a draw target, so SetRenderTarget throws transactionally, before any state changes, rather than leaving the real backbuffer bound while GraphicsDevice believes 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 needPick
A texture format other than SurfaceFormat::ColorSKIA — the only renderer that offers one
Arbitrary BlendState factor and function combinationsSKIA — the other two accept only the stock presets
A 2D vector renderer with no external build stepBLEND2D
Genuine OpenVG semanticsOPENVG, having read the licence and single-instance notes above
Render targetsSKIA or BLEND2DOPENVG has none
Maximum portability for a plain 2D gameNone 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.

Where to go next