别人都在用显卡搞 AI,而我还在用来搞“过时”的技术 · Avalonia 篇

别人都在用显卡搞 AI,而我还在用来搞“过时”的技术 · Avalonia 篇

这是本系列的第一篇。按理说应该先写 WPF 篇,再到 WinUI 篇,最后才轮到 Avalonia 篇——毕竟这才是它们诞生的先后顺序。但 Avalonia 篇是最近才折腾完的,趁着这股热乎劲儿还没散,索性先把它写了。WPF 和 WinUI 的坑,留待后续几篇再聊。

写在前面:这是个什么项目

当所有人都在拿显卡跑大模型、炼丹、生成图片的时候,本项目却把 GPU 摁在桌面 UI 这摊"过时"的活儿上——用它去实时渲染特效。

这个项目叫 MediaKit,是一套面向 .NET 桌面应用的视频特效与媒体播放控件库。它的核心思路只有一句话:把 GPU 像素着色器(Pixel Shader)接进桌面 UI 的渲染管线里,让每一帧视频/图像都能经过一段自定义的着色器代码处理后再上屏。

目前横跨了三个UI框架(2亲兄弟+1冒牌货↗有上位趋势):

平台 渲染底座 着色器语言
MediaKit.WPF WPF ShaderEffect HLSL(.fxfxc 编译成 .ps
MediaKit.WinUI3 Win2D PixelShaderEffect + Composition API HLSL(.hlsl.cso
MediaKit.Avalonia SkiaSharp SKRuntimeEffect SkSL(Skia 的运行时着色器语言)

搞了一点点效果:

  • 360° 全景投影(Pano):把等距柱状投影的全景视频/图片映射到球面,支持鼠标拖拽转视角、滚轮缩放,还带惯性滑动
  • 波纹(Ripple):点哪儿哪儿起水波,支持多点同时扩散;
  • Alpha 通道分离 / 视频平铺(WPF)
  • 一堆纯生成式的炫酷背景着色器(Clouds、Cosmos、Heartfelt、Octgrams、PhantomStar、Pyramid…直接从 Shadertoy 移植调教而来)。

一句话总结:GPU 不只能"思考",也能好好"画画"(这是GPU与生俱来的能力)。这篇就聊聊 Avalonia 篇是怎么搞出来的。


Avalonia 12:性能起飞了,但"口子"也焊死了

就在 2026 年 4 月,Avalonia 官方发布了 Avalonia 12。这一版被官方定位成一次"打地基"的版本——不堆花哨功能,专攻性能、稳定性和平台成熟度

翻了翻官方给的数字,确实唬人:

  • 在包含 35 万个独立可视元素的复杂场景里,Avalonia 12 的 FPS 相比旧版最高提升了 1867%——接近 20 倍的帧率;
  • 合成器(Compositor)被彻底重构,这是帧率暴涨的主要功臣;
  • PresentationSource 里缓存了 RenderScaling,避免重复查询;元素不可见时直接停掉动画处理
  • 编译绑定(Compiled Bindings)默认开启,几乎不用改代码就能吃到绑定性能红利。

对这类"每一帧都要往 GPU 上塞一段着色器"的场景来说,性能提升当然是天大的好事。可硬币的另一面是——渲染管线被封装得更深了。

Avalonia 一直走的是"自绘 UI"(drawn-UI)路线:一套 XAML、一条渲染管线、一份代码,跨平台像素级一致。为了做到这点,它必须把底层渲染细节层层包裹起来。到了 12,这层封装做得更厚实、更"优雅"了。

问题就出在这儿:需求其实很朴素——用像素着色器做一些好玩、好看的效果。可 Avalonia 的 Effect 体系只开放了框架内置的那寥寥几个效果(模糊、投影之类),既不支持继承,也没留自定义实现的入口。此路不通,只能往更底层绕——直接从渲染引擎下手,去拿那块原始的 Skia 画布(SKSurface)和 GPU 上下文(GRContext),在其上绘制 SkSL 着色器。

Avalonia 12 - Ready for What's Next


一段"和千问斗智斗勇"的学习史

说实话,这个后门找得并不轻松。

官方文档从头到尾翻了一遍,GitHub 仓库里的 issue、源码、sample 也逐一钻研过;上下班挤地铁那点碎片时间也没闲着——全用来跟千问(通义千问)"探讨技术"了。

结果……这里必须吐个槽:感觉千问是被喂了不少"shi"进去。经常一本正经地胡编乱造,要么就是给一堆早就过时的代码。最典型的是——提问里明明白纸黑字写了"Avalonia v12",它张口闭口给出的还是 v11 甚至更老的 API,CompositionCustomVisual 的签名对不上、ElementComposition 的用法早就变了,照着抄一行都跑不起来。

那段时间基本就是:问一句 → 得到一段看似合理实则报废的代码 → 自己去源码里对签名 → 发现被骗 → 再问一句。循环往复。

经过来来回回的折腾,最终还是找到了那把“钥匙”:

CompositionCustomVisual        // 挂在可视树上的自定义合成视觉
  └─ CompositionCustomVisualHandler   // 运行在合成/渲染线程上的绘制回调
       └─ ISkiaSharpApiLeaseFeature   // 关键!租借出底层的 SKSurface / GRContext

ISkiaSharpApiLeaseFeature 就是那个"框架故意留下的后门"——通过它 Lease() 出来的 SkSurfaceGrContext,便能在 Avalonia 12 的渲染线程上,直接操作原始的 Skia 画布了。(这里也有个暗坑,请看下文)

那阵子的日常基本就是个死循环:一边信誓旦旦地写死要 v12,千问那头却信誓旦旦地甩回一堆 v11;照抄、报错、翻源码、发现被坑,然后再问一句……周而复始。


在 Qoder 的辅助下,花了不少Credits,搞出了本项目

(Avalonia,其他2个是手撸的,有参考AI干活飞快)

真正把整个项目落地跑通,是在 Qoder 的辅助下完成的。下面就把 Avalonia 篇的核心设计拆开讲讲。

1. 核心:一个基于 SkSL 的 ShaderEffect 基类

整套体系的心脏是ShaderEffect这个类。它以 Avalonia 附加属性(Attached Property) 的形式挂到任意 Control 上:

<Rectangle mk:ShaderEffect.Effect="Pano" />

一旦挂上,它就会:

  1. 在目标控件上创建一个 CompositionCustomVisual
  2. 通过 ISkiaSharpApiLeaseFeature 拿到底层 SKSurfaceGRContext
  3. 把编译好的 SkSL(SKRuntimeEffect)连同 uniform、内容着色器一起,绘制到画布上。

它支持两种"内容来源":

  • Input 模式:从图片 URI 加载,上传成 GPU 纹理喂给着色器(iImage);
  • 快照模式:直接对目标控件当前画面 Snapshot() 出一张图交给着色器处理(相当于给任意 UI 加一层后期滤镜)。得益于 Avalonia 12 的 Skia 后端本就跑在 GPU 上,Snapshot() 拿到的是一张 GPU 纹理(GPU-backed SKImage),与原画布共享纹理、写时才复制,不会发生 GPU→CPU 的回读拷贝,几乎零开销。但暗坑就在这里:
    1. SKSurface 是整个窗口的,而非当前控件的 ——因此不能整张快照,好在surface.Snapshot()支持传入SKRectI,只截取控件所在的那块子区域。
    2. 控件在窗口里的绝对像素坐标没有现成 API 可取,计算有点弯弯绕绕:从画布 TotalMatrix(CTM)里取出平移分量 TransX/TransY 当作原点,再拼出 pixelBounds
    3. DPI 缩放需要自己处理——逻辑尺寸要乘上 ctm.ScaleX(即 RenderScaling)换算成设备像素,快照与 iResolution 均按设备像素计算;绘制时再以 canvas.Scale(1f/scale) 变换回去,否则在高 DPI 屏上会出现错位或模糊。

2. 动画与惯性:利用 Avalonia 12 的 vsync 帧回调(渲染线程)

对于动画,其实我一直在UI线程<->渲染线程来回摇摆,不过最终还是选择了渲染线程
对于需要动画的效果(比如全景惯性、水波扩散),没有自起 Timer,而是复用了合成器的帧调度机制:

  • RegisterForNextAnimationFrameUpdate() 注册下一帧回调(本质是跟着 vsync 走);
  • 每帧在 OnAnimationFrameUpdate 里推进时间、更新 uniform,再决定"要不要继续下一帧";
  • 用一个 OnFrameUpdate() 返回 bool 来控制动画的生死——惯性衰减到阈值以下就自然停帧。

流畅的惯性滑动:(其实,用UI线程来做也很流畅,不过也许是我电脑性能强悍所以感受不到差异?笑~)

3. 工程化:.sksl 文件 + 源生成器 = 省时省力

写着色器时,只需在 .sksl 文件顶部打几个"标记注释",剩下的强类型 C# 类、数据绑定属性、工厂注册,全由 Source Generator 自动生成:

// @effect: PanoEffect        ← 生成强类型子类 PanoEffect
uniform shader iImage;
uniform float2 iResolution;
// @property: 90.0            ← 生成可绑定的 DirectProperty,默认值 90
uniform float fov;
// @property: 0.5
uniform float rotationX;

half4 main(float2 fragCoord) { /* ... */ }

编译时,AvaloniaEmitter会:

  • 为每个 @property uniform 生成一个 Avalonia DirectProperty(可直接双向绑定到 Slider);
  • 生成 ShaderEffects 静态描述符 + [ModuleInitializer] 自动注册到工厂,ShaderEffect.Effect="Pano" 这种字符串写法就能直接用;
  • 检测重名 .sksl,重复了直接编译报错(SKSL001)。

标记语义速查:

标记 作用
// @effect: XxxEffect 生成强类型 ShaderEffect 子类(可扩展交互逻辑)
// @effect: default 只注册为工厂效果,不生成强类型类(适合纯展示)
// @animate 效果启动后自动播放动画(持续推送 iTime
// @property: 默认值 为紧跟其后的 uniform 生成可绑定属性

所以一个像 Pano 这样的交互式效果,代码被拆成两半:着色器逻辑 + 生成的属性.sksl / .g.cs 里,交互行为(鼠标拖拽、惯性、Reset)则写在手写的 partial class PanoEffect.cs 里,职责清清爽爽。(AI写源生成器轻松拿捏,手撸那可有点痛苦。

Pano.sksl 为例,左侧是手写的着色器源文件,右侧是编译期由源生成器产出的 PanoEffect.g.cs(示意):

// Pano.sksl
// @effect: PanoEffect
uniform shader iImage;
uniform float2 iResolution;
uniform float2 iSourceSize;
// @property: 0.5
uniform float rotationX;
// @property: 0.5
uniform float rotationY;
// @property: 0.5
uniform float zoom;
// @property: 90.0
uniform float fov;

half4 main(float2 fragCoord) {
    /* 全景投影计算… */
}
// PanoEffect.g.cs(自动生成)
[EffectName("Pano")]
public partial class PanoEffect : ShaderEffect
{
    private static readonly Uri _shaderUri =
        new("avares://MediaKit.Avalonia/Shaders/Pano.sksl");

    public static readonly DirectProperty<PanoEffect, double> FovProperty =
        AvaloniaProperty.RegisterDirect<PanoEffect, double>(
            nameof(Fov), o => o.Fov, (o, v) => o.Fov = v, 90.0);

    private double _fov = 90.0;
    public double Fov
    {
        get => _fov;
        set { if (SetAndRaise(FovProperty, ref _fov, value))
                  this["fov"] = (float)value; }
    }

    // rotationX / rotationY / zoom 同理…

    public PanoEffect() : base(_shaderUri)
    {
        this["rotationX"] = (float)_rotationX;
        this["rotationY"] = (float)_rotationY;
        this["zoom"]      = (float)_zoom;
        this["fov"]       = (float)_fov;
    }
}

小结

七拼八凑的扯了一通,回到那个略带自嘲的标题:

别人都在用显卡搞 AI,而我还在用来搞"过时"的技术

其实像素着色器肯定不是过时的(开玩笑,本职工作来的,怎么过时?),但是Avalonia、WinUI不支持罢了。
这一通折腾下来,有点心累,还好有AI。
怪不得套壳浏览器有一统天下的趋势。(若是网页,一句话就能让AI出成果了)

下一篇,聊聊最"根正苗红"的 WPF 篇——那才是这套东西最早生根发芽的地方。


本文项目地址:github.com/LazyCuteLion/MediaKit

posted @ 2026-07-05 12:14  疯狂的懒羊羊  阅读(39)  评论(0)    收藏  举报