Skip to content

Add Experimental Avalonia Renderer Package and Catalog Sample. - #415

Open
JimBobSquarePants wants to merge 9 commits into
mainfrom
js/avalonia-sample
Open

Add Experimental Avalonia Renderer Package and Catalog Sample.#415
JimBobSquarePants wants to merge 9 commits into
mainfrom
js/avalonia-sample

Conversation

@JimBobSquarePants

@JimBobSquarePants JimBobSquarePants commented Aug 22, 2026

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Adds a new SixLabors.ImageSharp.Drawing.Renderers.Avalonia package that implements Avalonia rendering and text shaping using ImageSharp.Drawing and SixLabors.Fonts.

The renderer supports CPU framebuffer and WebGPU native-surface rendering, with automatic or explicit backend selection. It covers Avalonia geometry, regions, images, glyph runs, render targets, layers, and effects.

This also adds an adapted upstream Avalonia Control Catalog as a comprehensive visual sample. The catalog can switch between the ImageSharp and Skia renderers and select the CPU or WebGPU backend for comparison.

Supporting changes include path-backed Region construction and region containment/intersection operations required by Avalonia geometry handling, plus the associated package, dependency, and solution updates.

ControlCatalog_C8zMvZ1Lm8.mp4

Move the Avalonia rendering backend out of the sample app into a new `SixLabors.ImageSharp.Drawing.Renderers.Avalonia` project, add shared versioning support for independently versioned packages, and update the control catalog to consume the packaged renderer and Avalonia CI packages.

This also adds `Region` path scan conversion plus region containment/intersection APIs so the renderer can implement geometry fill intersection checks, and includes related dependency and solution updates.
Updates `GlyphRunImpl` to default to `HintingMode.Full` instead of `Standard`, including the stored run hinting mode and initial `GlyphOptions`. It also maps Avalonia `Strong` hinting (and the fallback case) to `HintingMode.Full`, keeping glyph geometry and decoration intersection calculations aligned with fully hinted rendering.
@JimBobSquarePants JimBobSquarePants changed the title Add Avalonia Catalog sample and experimental renderer package Add Experimenta Avalonia Renderer Package and Catalog Sample. Aug 22, 2026
@JimBobSquarePants JimBobSquarePants changed the title Add Experimenta Avalonia Renderer Package and Catalog Sample. Add Experimental Avalonia Renderer Package and Catalog Sample. Aug 22, 2026
@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79%. Comparing base (61bd770) to head (9a15171).

Additional details and impacted files
@@          Coverage Diff           @@
##            main    #415    +/-   ##
======================================
- Coverage     79%     79%    -1%     
======================================
  Files        232     232            
  Lines      27356   27610   +254     
  Branches    3113    3172    +59     
======================================
- Hits       21875   21874     -1     
- Misses      4589    4843   +254     
- Partials     892     893     +1     
Flag Coverage Δ
unittests 79% <ø> (-1%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JimBobSquarePants

JimBobSquarePants commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

Avalonia has no real supported path for a third-party renderer today. This package exists only through workarounds, and this first sample is Windows-only because of them. I want to lay out exactly what I had to do, why, and what Avalonia needs to add so that anyone can bring their own renderer.

What the renderer is

SixLabors.ImageSharp.Drawing.Renderers.Avalonia is a full IPlatformRenderInterface. It draws with ImageSharp.Drawing and shapes text with SixLabors.Fonts. It has two backends:

  • CPU: draws into Avalonia's framebuffer surface.
  • WebGPU (wgpu-native): draws into a native window surface.

Where WebGPU stands per platform

Platform What WebGPU needs What Avalonia gives a third-party renderer Status
Windows HWND + HINSTANCE INativePlatformHandleSurface "HWND"; no HINSTANCE Runs only through the workarounds below.
Linux X11 Display* + Window "XID" surface with the window; Display* is internal Runs only through the workarounds below.
Android ANativeWindow* "SurfaceView" surface; the handle is a real ANativeWindow* Wired. Not yet tested on a device.
macOS CAMetalLayer or the NSView Nothing in Surfaces. NSView exists only on ITopLevelImpl.Handle, which the render interface never receives. The Metal layer is private to Avalonia.Native. Blocked.
iOS CAMetalLayer or the UIView Nothing. MetalPlatformSurface is internal and holds the layer privately. Blocked.
Wayland wl_display* + wl_surface* Nothing. Handle is null. Blocked.
Browser a webgpu canvas context Only 2d and webgl contexts. Blocked.

I also checked the other route: render offscreen and import through ICompositionGpuInterop. Avalonia can import D3D11 shared handles, Vulkan opaque handles, and IOSurfaces. wgpu-native cannot export any of them. So native surfaces are the only route.

Why the whole approach is a workaround

Every step below is a hack around a missing extension point. None of it should exist.

  1. Registration. AppBuilder has one rendering-subsystem slot and the last writer wins. UsePlatformDetect(), UseAndroid(), UseiOS(), and UseBrowser() all register Skia inside them. So UseImageSharpDrawing() only takes effect if it is called after them. Call it before, and Skia silently replaces it. There is no way to register a third-party renderer as a first-class peer of Skia.

  2. Windows CPU drawing needs a window-style change I have to make for the user. Avalonia creates a composited window (WS_EX_NOREDIRECTIONBITMAP) whenever a GPU platform exists (WindowImpl.cs, UseRedirectionBitmap). CPU pixels drawn into that window never show. Skia never hits this because Skia's GPU is Avalonia's GPU: when the GPU exists, Skia uses it. My GPU is not Avalonia's GPU, so my CPU mode can land on a composited window. The only fix is Win32RenderingMode.Software. I now set it from inside my renderer initializer when the app has not set Win32PlatformOptions itself. A renderer should not be reaching into windowing options at all.

  3. Auto mode has to probe WebGPU before any window exists. Same root cause. Avalonia fixes the window style before my renderer runs. If WebGPU then fails, the CPU fallback is invisible. So I probe wgpu at start-up to guess the style. Avalonia should be making that decision, as it does for Skia.

  4. The existing custom-GPU hook cannot be used. Win32PlatformOptions.CustomPlatformGraphics exists, but Win32Platform.cs only accepts it with CompositionMode = RedirectionSurface. That turns composition off for the whole app: no transparency levels, no Mica, no acrylic. No other backend has a hook at all; X11, Avalonia.Native, Android, iOS, and Browser each pick their IPlatformGraphics in private code and bind it last, so anything I bind first is overwritten.

  5. Handle gaps I fill myself. HINSTANCE on Windows (I call GetModuleHandle(null)) and Display* on X11 (I open a second connection with XOpenDisplay). Both are workarounds for data Avalonia already has and does not expose.

What Avalonia needs to provide for third-party renderers

Two changes would remove all of the above.

A. A native surface in TopLevel.Surfaces on every backend. Windows, X11, and Android already do this through INativePlatformHandleSurface. The missing ones:

  • macOS: the NSView (descriptor "NSView").
  • iOS: the UIView (descriptor "UIView").
  • Wayland: both wl_display* and wl_surface*, which needs a two-handle surface type.
  • X11: the Display*. Windows: the HINSTANCE.
  • Browser: a webgpu canvas context option.

B. A composition-safe custom IPlatformGraphics hook on every backend, registered as a first-class renderer. Like Win32PlatformOptions.CustomPlatformGraphics, but on X11PlatformOptions, AvaloniaNativePlatformOptions, AndroidPlatformOptions, and iOSPlatformOptions too, bound into the locator the same way the built-in path is. On Win32 it must not force composition off: make IWindowsSurfaceFactory public and not tied to EGL, and decide UseRedirectionBitmap from the supplied graphics instead of rejecting it. With this, Avalonia would create my device, decide the window style from it, and hand it to my renderer, exactly how it treats Skia. Items 1 to 4 above then disappear.

@MikeCodesDotNET, these are the two changes Avalonia needs before anyone can bring their own renderer. Can the team take them on? Until they exist, this renderer stays experimental and Windows-only.

Open our own Xlib Display and pair it with the XID Avalonia exposes,
since window ids are valid across connections to the same server.
Select Win32 software rendering from the renderer initializer when the
application has not configured Win32PlatformOptions: always in Cpu mode,
and in Auto mode when the WebGPU probe fails. Composited windows do not
display framebuffer output. Removes the sample-only override.

Use GetModuleHandle(null) for the HWND surface, the module Avalonia
registers its window class against.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant