22. Rendering pipeline in plain words

Goal

Why this matters

Prerequisites

1. Mental model

  1. UI thread builds and updates the visual tree (Visuals/Controls). When properties change, visuals mark themselves dirty (e.g., via InvalidateVisual).
  2. Scene graph represents visuals and draw operations in a batched form (SceneGraph.cs).
  3. Compositor commits scene graph updates to the render thread and keeps track of dirty rectangles.
  4. Render loop (driven by an IRenderTimer) asks the renderer to draw frames while work is pending.
  5. Renderer walks the scene graph, issues drawing commands, and marshals them to Skia or another backend.
  6. Skia/render interface rasterizes shapes/text/images into GPU textures (or CPU bitmaps) before the platform swapchain presents the frame.

Avalonia uses two main threads: UI thread and render thread. Keep the UI thread free of long-running work so animations, input dispatch, and composition stay responsive.

2. UI thread: creating and invalidating visuals

3. Render thread and renderer pipeline

Avalonia ships both CompositingRenderer (default) and DeferredRenderer. The renderer uses dirty rectangles to redraw minimal regions and produces scene graph nodes consumed by Skia.

CompositionTarget

CompositionTarget abstracts the surface being rendered. It holds references to swapchains, frame buffers, and frame timing metrics. You usually observe it through IRenderer.Diagnostics (frame times, dirty rect counts) or via DevTools/remote diagnostics rather than accessing the object directly.

Immediate renderer

ImmediateRenderer renders a visual subtree synchronously into a DrawingContext. Used for RenderTargetBitmap, VisualBrush, etc. Not used for normal window presentation.

4. Compositor and render loop

The compositor orchestrates UI → render thread updates (see Compositor.cs).

Render timers

Scene graph commits

Each RenderLoop tick calls Compositor.CommitScenes. The compositor transforms dirty visuals into render passes, prunes unchanged branches, and tracks retained GPU resources for reuse across frames.

5. Backend selection and GPU interfaces

Avalonia targets multiple render interfaces via IRenderInterface. Skia is the default implementation and chooses GPU versus CPU paths per platform.

Backend selection logic

Force a backend with UseSkia(new SkiaOptions { RenderMode = RenderMode.Software }) or by setting AVALONIA_RENDERER environment variable (e.g., software, open_gl). Always pair overrides with tests on target hardware.

GPU resource management

Skia configuration

Avalonia uses Skia for cross-platform drawing:

SkiaOptions

AppBuilder.Configure<App>()
    .UsePlatformDetect()
    .UseSkia(new SkiaOptions
    {
        MaxGpuResourceSizeBytes = 64L * 1024 * 1024,
        UseOpacitySaveLayer = false
    })
    .LogToTrace();

6. RenderOptions (per Visual)

RenderOptions attached properties influence interpolation and text rendering:

Example:

RenderOptions.SetBitmapInterpolationMode(image, BitmapInterpolationMode.HighQuality);
RenderOptions.SetTextRenderingMode(smallText, TextRenderingMode.Aliased);

RenderOptions apply to a visual and flow down to children unless overridden.

7. When does a frame render?

Prevent unnecessary redraws:

8. Frame timing instrumentation

Renderer diagnostics

DevTools

Logging

AppBuilder.Configure<App>()
    .UsePlatformDetect()
    .LogToTrace(LogEventLevel.Debug, new[] { LogArea.Rendering, LogArea.Layout })
    .StartWithClassicDesktopLifetime(args);

Render overlays

RendererDebugOverlays (see RendererDebugOverlays.cs) enable overlays showing dirty rectangles, FPS, layout costs.

if (TopLevel is { Renderer: { } renderer })
    renderer.DebugOverlays = RendererDebugOverlays.Fps | RendererDebugOverlays.LayoutTimeGraph;

Tools

9. Immediate rendering utilities

RenderTargetBitmap

var bitmap = new RenderTargetBitmap(new PixelSize(300, 200), new Vector(96, 96));
await bitmap.RenderAsync(myControl);
bitmap.Save("snapshot.png");

Uses ImmediateRenderer to render a control off-screen.

Drawing manually

DrawingContext allows custom drawing via immediate renderer.

10. Platform-specific notes

11. Practice exercises

  1. Replace the render timer with a custom IRenderTimer implementation and graph frame cadence using timestamps collected from SceneInvalidated.
  2. Override SkiaOptions.RenderMode to force software rendering, then switch back to GPU; profile render time using overlays in both modes.
  3. Capture frame diagnostics (RendererDebugOverlays.LayoutTimeGraph | RenderTimeGraph) during an animation and export metrics for analysis.
  4. Instrument RenderRoot.Renderer.Diagnostics to log dirty rectangle counts when toggling InvalidateVisual; correlate with DevTools overlays.
  5. Use DevTools remote transport to attach from another process (Chapter 24) and verify frame timing matches local instrumentation.

Look under the hood (source bookmarks)

Check yourself

What's next