Skip to content

DirectWrite Text Backend (Windows)

WhatsCanvas ships a native Windows text backend built on DirectWrite (shaping, layout, metrics, font enumeration) and Direct2D + WIC (headless glyph rasterization). It is an alternative to the portable FreeType/stb glyph-atlas backend and produces high-quality, OS-consistent text on Windows.

Selecting the backend

Select it from the public Canvas API (portable is the default):

// Native DirectWrite with ClearType, else portable fallback off Windows.
canvas->setTextBackend(wsc::Canvas::TextBackend::DirectWrite,
                       wsc::Canvas::TextRenderMode::ClearType);
if (canvas->textBackend() == wsc::Canvas::TextBackend::DirectWrite) {
    // native backend active
}

setTextBackend resets text state (registered fonts / fallback chains), so call it before registering fonts. A DirectWrite request falls back to the portable glyph-atlas backend when DirectWrite is unavailable.

The backend can also be created directly through the internal factory:

#include "text/DirectWriteTextBackend.h"

if (wsc::text::isDirectWriteAvailable()) {                 // true on _WIN32
    wsc::text::DirectWriteBackendOptions options;
    options.rasterMode = wsc::text::DirectWriteRasterMode::Grayscale; // or ClearType
    std::unique_ptr<wsc::text::ITextBackend> backend =
        wsc::text::createDirectWriteTextBackend(options);   // nullptr off Windows
}

What it does

  • Shaping & layout via IDWriteTextLayout (complex scripts, bidi, kerning).
  • Metrics — width/height, and ascent/descent/lineGap from the font's design metrics.
  • Rasterization into an RGBA bitmap through a Direct2D WIC render target.
  • System font fallback — IDWriteTextLayout resolves missing glyphs automatically.

Raster modes

Mode Use when Notes
Grayscale (default) Any surface, transforms, animation Coverage in the alpha channel; white RGB. Safe and portable.
ClearType Axis-aligned text over a known opaque background Preserves horizontal RGB subpixel coverage; best-effort composite. Avoid under alpha blending / transforms (colored fringes).

Text styling surface

All of these Paint knobs flow through to DirectWrite:

  • setFontFamily / setFontWeight (100–900) / setFontSlant (normal/italic/oblique)
  • setTextSize
  • setLetterSpacing — baked into the layout via IDWriteTextLayout1::SetCharacterSpacing, so measurement and rendering stay consistent
  • setTextAlign / setTextBaseline
  • setTextLocale (BCP-47, e.g. "en-US", "ja-JP") — locale-aware shaping and Han-unification fallback
  • setFontFeature — whole-run, case-sensitive four-byte OpenType tags are applied through IDWriteTypography and included in bitmap cache keys. There is no whitelist; unsupported features have no effect. Value 0 disables, value 1 normally enables, and other values are passed through for feature-specific alternate selection. See the Text Feature Matrix.
  • setUnderline / setStrikethrough — text decorations drawn by DirectWrite over the run
  • setTextRenderMode — per-Paint raster mode override (Grayscale / ClearType / Default = inherit backend setting). Lets a single canvas mix ClearType body text with grayscale labels/animated text; distinct modes get distinct cache entries.

Custom fonts

registerFontFace accepts both on-disk and in-memory fonts:

const std::string fontPath = resolveApplicationFont("Inter-Regular.ttf");
backend->registerFontFace(wsc::FontFace::fromFile(wsc::FontDescriptor("Inter"),
                                                  fontPath));

std::vector<std::uint8_t> bytes = /* load a .ttf/.otf */;
backend->registerFontFace(wsc::FontFace::fromMemory(wsc::FontDescriptor("Inter"), bytes));
  • On-disk fonts use a custom IDWriteFontCollectionLoader + file enumerator.
  • In-memory fonts use IDWriteInMemoryFontFileLoader (IDWriteFactory5, Windows 10+); unavailable factories degrade gracefully (returns false).
  • A registered family is preferred over the system collection; the system collection still drives automatic fallback.

Refreshing installed fonts

Applications that install or remove fonts while a canvas is alive can refresh the process snapshot and the native DirectWrite collection without recreating the canvas:

const std::uint64_t before = wsc::FontSystem::installedFontGeneration();
if (canvas->refreshSystemFonts()) {
    const std::uint64_t after = wsc::FontSystem::installedFontGeneration();
    // after > before; application-registered fonts and fallback chains remain.
}

Refreshing invalidates text/atlas bitmap caches whose results may depend on the installed collection. Explicit file-backed and memory-backed font registrations and custom fallback chains are preserved.

Custom font fallback chains

wsc::FontFallbackChain chain("Inter");
chain.addFallbackFamily("Yu Gothic");      // Japanese
chain.addFallbackFamily("Microsoft YaHei"); // Simplified Chinese
backend->setFontFallbackChain(chain);

Builds an IDWriteFontFallback (via IDWriteFontFallbackBuilder) mapping the Unicode range to the requested families in order, then appends the system fallback so any uncovered scripts still resolve. An empty chain clears it.

HiDPI

DirectWrite text participates in the same device-resolution path as the portable backend: under a scaled transform or setDevicePixelRatio, glyphs rasterize at the effective device pixel size and stay crisp. See Text Sharpness & HiDPI.

Known limitations

  • Bitmap per draw: text is rasterized to a CPU bitmap and uploaded as a GPU texture on the first drawText for a given styled string. Three caches keep the amortized cost low for repeated UI text: the COM apartment plus the WIC/D2D factories live for the process; identical (text, paint, dpr, mode) reuses a cached bitmap + intrinsic metrics on the backend (LRU, 4 MB byte budget by default); and Canvas keeps a bounded LRU (256 entries) of GPU textures keyed by the backend-provided content id, so repeat renders skip the CPU→GPU upload entirely. Only a shared GPU glyph atlas that batches distinct same-frame text into one texture remains a future optimization.
  • ClearType is best-effort: it renders white-on-opaque-black and derives an alpha from the brightest subpixel, so true subpixel sharpness only holds for axis-aligned text over an opaque destination.