Effector: The Avalonia Extension That Patches the Framework for Custom GPU Effects

A build-time IL weaving system that turns Skia-backed effects, interactive shaders, and thread-safe snapshots into something Avalonia can render as if it were native.

9 min read • View on GitHub • More from wieslawsoltes

A wide editorial scene of a desktop UI being repaired from behind the scenes. In front, a clean application window shows a subtle custom visual effect, while behind the wall small assembly blocks are being fitted into place and a conduit carries rendering work toward a Skia output layer. The image explains that the project alters the framework at build time instead of adding effects only through public APIs.
Effector’s core move is not a new widget. It is a build-time rewrite that lets Avalonia accept custom effects as if they belonged there from the start.
Key Takeaways

What Effector Actually Does

Effector solves a narrow problem with an unusually aggressive toolset. Avalonia gives you a public effect story, but not a wide-open path for building custom GPU effects that behave like first-class framework features. This project closes that gap by combining IL weaving, app-local assembly patching, and Skia-backed rendering in one build-time pipeline.

That makes it feel closer to infrastructure than a library. You write effect classes, annotate them, and let the build process generate the glue that Avalonia itself does not expose. The result is a system that aims to be invisible at runtime and invasive only where the build can afford it.

The Trick: Build-Time Surgery, Not Runtime Monkey-Patching

The interesting move is not the shader itself. It is the chain that turns user-authored effect classes into patched framework calls and finally into Skia output.

At a high level, Effector does three things. First, it scans for effect classes marked in a way the weaver understands. Second, it rewrites the consumer build and local Avalonia assemblies so those effects become part of the rendering path. Third, it routes render-time work through a runtime registry instead of asking UI objects to cross thread boundaries directly.

// Conceptual flow, simplified
[SkiaEffect]
public sealed class BlurGlowEffect : SkiaEffectBase
{
    public double Radius { get; set; }
}

// Build time
EffectorWeaver -> generates immutable value type
AvaloniaAssemblyPatcher -> injects render hook

// Runtime
EffectorRuntime.Lookup(visual)
    -> descriptor
    -> snapshot
    -> SKImageFilter factory

How the Render-Thread Bridge Works

The hardest part of the design is the thread boundary. Avalonia UI objects live in one world. Skia rendering happens in another. Effector bridges them by using a central runtime registry, descriptors, and immutable snapshots so the render thread never has to touch mutable UI state directly.

That is where ConditionalWeakTable matters. It lets the runtime associate effect metadata with live visuals without leaking memory when those visuals disappear. The patched Avalonia code asks the runtime for the effect attached to a visual, and the runtime returns the safe, frozen data needed to build the Skia image filter.

A close-up machine diagram showing a mutable effect object on the left, a freezing chamber in the middle, and a render-side Skia factory on the right. The left side has animated controls and a hand editing properties, the middle converts them into an immutable capsule, and the right side turns that capsule into a filter for final rendering. The image explains how Effector crosses the UI-thread to render-thread boundary safely.
Effector’s real invention is not the effect class. It is the frozen handoff that makes the effect safe to consume on the render thread.

This is why the project feels disciplined despite being invasive. The mutable side stays on the UI side. The render side gets a compact, immutable representation. The patching makes the handoff possible, and the snapshot makes it safe.

Why Immutable Snapshots Matter

Effector leans on generated immutable value types because thread safety is not a nice-to-have here. If the render thread consumes live UI objects, the model breaks the moment properties animate or input arrives mid-frame. The weaver removes that burden from the developer by creating the frozen representation automatically.

That also keeps the API pleasant. Effect authors can work with familiar Avalonia patterns, including animation-friendly properties, while the build tools handle the ugly part. The project’s value is not only performance. It is that the unsafe part becomes mechanical.

Shaders in XAML, Without the Usual Pain

Shader support sits on top of the same architecture. The SkiaRuntimeShaderBuilder compiles SKSL and caches compiled results so repeated shader creation does not become a tax on every frame. In practice, that means shader-driven effects can live inside the same effect model instead of being bolted on as a separate rendering path.

That matters because shader systems often drift into either too much ceremony or too much runtime cost. Effector tries to keep both under control. The build step prepares the wiring, the runtime step does lookup and construction, and the shader compiler stays out of the hot path when it can.

// Conceptual shader flow
var shader = SkiaRuntimeShaderBuilder.CompileShaderSource(sourceCode);
var filter = shader.CreateImageFilter(parameters);

// Cached by source so the expensive compile work is reused.

Where Effector Fits Compared to Conventional Effect Systems

The comparison is the whole point. Effector is not the safest option, and it is not trying to be. It sits in a middle ground where build-time complexity buys you native-feeling integration at runtime.

ApproachPowerMaintenance costRuntime feel
Public API onlyLow to mediumLowClean, but limited
Fork the frameworkHighVery highNative, but heavy to ship
Effector app-local patchingHighMedium to high at build timeNative-feeling without a permanent fork

Public APIs are easy to live with, but they stop where the framework stops. A fork gives you full control, but you own the merge burden forever. Effector makes a different bargain: pay at build time, keep runtime behavior close to the framework, and avoid carrying a private Avalonia branch.

Constraints, Risks, and Why This Is Still Interesting

This design is not casual. It is tied to specific framework internals, it adds build-system complexity, and it depends on patching local copies of Avalonia assemblies with care. That means maintenance discipline matters as much as the idea itself.

Still, the architecture is compelling because it solves a real mismatch. Sometimes the public extension model is too small for the effect you want. Effector does not pretend that limitation does not exist. It steps around it, then makes the result look ordinary to the app that uses it.