Avalonia UIとGPUシェーダーの統合
桌面UIフレームワークにおいて、GPUのピクセルシェーダーをレンダリングパイプラインに直接統合することは、リッチなビジュアルエフェクトや高性能なメディア処理を実現する上で不可欠です。本稿では、Avalonia UIフレームワークにおいて、SkiaSharpのランタイムシェーダー言語(SkSL)を活用し、カスタムピクセルシェーダーをUIコントロールに適用する技術的アプローチについて解説します。
Avalonia 12のアーキテクチャとレンダリングへのアクセス
Avalonia 12では、コンポジター(Compositor)の大幅なリファクタリングにより、複雑なビジュアルツリーにおけるレンダリングパフォーマンスが飛躍的に向上しました。しかし、この抽象化の強化は、カスタムシェーダーエフェクトのような低レベルなGPU操作を必要とする機能の実装を困難にしています。標準の Effect プロパティは組み込みのエフェクト(ぼかしやドロップシャドウなど)に限定されており、任意のピクセルシェーダーを注入する公式なAPIは提供されていません。
この制限を回避するため、Avaloniaの視覚ツリー(Visual Tree)の底層にあるSkiaバックエンドに直接アクセスする手法を採用します。具体的には、CompositionCustomVisual と ISkiaSharpApiLeaseFeature を組み合わせることで、レンダースレッド上で原始的な SKSurface と GRContext を取得します。
// 視覚ツリーとSkiaバックエンドへのアクセス階層
CompositionCustomVisual
└── CompositionCustomVisualHandler (レンダースレッドコールバック)
└── ISkiaSharpApiLeaseFeature (SKSurface / GRContext のリース)
ShaderEffect アタッチプロパティの設計
カスタムシェーダーを任意のUI要素に適用するため、CustomShaderBase をアタッチプロパティとして設計します。これにより、XAML上で宣言的にエフェクトを適用できるようになります。
<Border local:CustomShader.Apply="RippleEffect" />
このアタッチプロパティは、ターゲットコントロール上に CompositionCustomVisual を生成し、SkSLで記述された SKRuntimeEffect をUniform変数やコンテンツテクスチャと共に描画します。
コンテンツソースとしては、画像URIからGPUテクスチャを生成する「Inputモード」と、ターゲットコントロールの現在の描画内容をスナップショットとして取得する「Snapshotモード」の2つをサポートします。Snapshotモードでは、AvaloniaのSkiaバックエンドがGPU上で動作しているため、surface.Snapshot() を使用することでGPU-to-CPUの読み戻し(Readback)を発生させずに、ゼロコピーでGPUバックドの SKImage を取得できます。
ただし、Snapshotモードにはいくつかの技術的注意点があります。SKSurface はウィンドウ全体のものであるため、SKRectI を指定して対象コントロールの領域のみを切り出す必要があります。また、コントロールの絶対ピクセル座標は、キャンバスの TotalMatrix (CTM) から平行移動成分を抽出して計算します。さらに、高DPI環境でのぼやけや位置ずれを防ぐため、論理サイズに RenderScaling を乗算してデバイスピクセルに変換し、描画時に canvas.Scale(1f / scale) で逆変換を行う必要があります。
VSync同期によるアニメーションと慣性処理
パノラマビューの慣性スクロールや波紋の拡散など、時間経過に伴うエフェクトには、UIスレッドではなくレンダースレッドでのアニメーション処理が適しています。
独自のタイマーを実装する代わりに、コンポジターのフレームスケジューリング機構を活用します。RegisterForNextAnimationFrameUpdate() を呼び出してVSyncに同期した次フレームのコールバックを登録し、OnAnimationFrameUpdate 内で時間経過とUniform変数を更新します。アニメーションの継続可否はブール値で制御し、慣性減衰が閾値を下回った時点でフレーム更新を停止することで、リソース消費を最適化します。
ソースジェネレーターによるSkSLとC#のバインディング
シェーダーの開発効率を高め、型安全性を確保するために、.sksl ファイル内の特定のコメントアノテーションを解析し、C#のバインディングコードを自動生成するソースジェネレーターを導入します。
// RippleShader.sksl
// @effect: RippleShaderEffect
uniform shader sourceTexture;
uniform float2 viewportSize;
// @property: 1.5
uniform float waveAmplitude;
// @property: 0.8
uniform float waveFrequency;
half4 main(float2 fragCoord) {
// 波紋の計算ロジック...
return half4(1.0);
}
コンパイル時に、ソースジェネレーターは以下の処理を行います:
@propertyアノテーションに基づき、双方向バインディングが可能な AvaloniaDirectPropertyを生成します。- エフェクトをファクトリに自動登録するための静的ディスクリプターと
[ModuleInitializer]を生成します。
生成されるC#コードの例:
// RippleShaderEffect.g.cs (自動生成コード)
[EffectName("Ripple")]
public partial class RippleShaderEffect : CustomShaderBase
{
private static readonly Uri ShaderResourceUri =
new("avares://VisualEffects/Shaders/RippleShader.sksl");
public static readonly DirectProperty<RippleShaderEffect, double> WaveAmplitudeProperty =
AvaloniaProperty.RegisterDirect<RippleShaderEffect, double>(
nameof(WaveAmplitude), o => o.WaveAmplitude, (o, v) => o.WaveAmplitude = v, 1.5);
private double _waveAmplitude = 1.5;
public double WaveAmplitude
{
get => _waveAmplitude;
set
{
if (SetAndRaise(WaveAmplitudeProperty, ref _waveAmplitude, value))
SetUniform("waveAmplitude", (float)value);
}
}
public RippleShaderEffect() : base(ShaderResourceUri)
{
SetUniform("waveAmplitude", (float)_waveAmplitude);
SetUniform("waveFrequency", (float)_waveFrequency);
}
}
このアーキテクチャにより、シェーダーのアルゴリズム(.sksl)とUIのインタラクションロジック(Partial Class)を明確に分離し、保守性の高いエフェクトライブラリを構築できます。