别人都在用显卡搞 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(.fx → fxc 编译成 .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 着色器。
一段"和千问斗智斗勇"的学习史
说实话,这个后门找得并不轻松。
官方文档从头到尾翻了一遍,GitHub 仓库里的 issue、源码、sample 也逐一钻研过;上下班挤地铁那点碎片时间也没闲着——全用来跟千问(通义千问)"探讨技术"了。
结果……这里必须吐个槽:感觉千问是被喂了不少"shi"进去。经常一本正经地胡编乱造,要么就是给一堆早就过时的代码。最典型的是——提问里明明白纸黑字写了"Avalonia v12",它张口闭口给出的还是 v11 甚至更老的 API,CompositionCustomVisual 的签名对不上、ElementComposition 的用法早就变了,照着抄一行都跑不起来。
那段时间基本就是:问一句 → 得到一段看似合理实则报废的代码 → 自己去源码里对签名 → 发现被骗 → 再问一句。循环往复。
经过来来回回的折腾,最终还是找到了那把“钥匙”:
CompositionCustomVisual // 挂在可视树上的自定义合成视觉
└─ CompositionCustomVisualHandler // 运行在合成/渲染线程上的绘制回调
└─ ISkiaSharpApiLeaseFeature // 关键!租借出底层的 SKSurface / GRContext
ISkiaSharpApiLeaseFeature 就是那个"框架故意留下的后门"——通过它 Lease() 出来的 SkSurface 和 GrContext,便能在 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" />
一旦挂上,它就会:
- 在目标控件上创建一个
CompositionCustomVisual; - 通过
ISkiaSharpApiLeaseFeature拿到底层SKSurface和GRContext; - 把编译好的 SkSL(
SKRuntimeEffect)连同 uniform、内容着色器一起,绘制到画布上。
它支持两种"内容来源":
- Input 模式:从图片 URI 加载,上传成 GPU 纹理喂给着色器(
iImage); - 快照模式:直接对目标控件当前画面
Snapshot()出一张图交给着色器处理(相当于给任意 UI 加一层后期滤镜)。得益于 Avalonia 12 的 Skia 后端本就跑在 GPU 上,Snapshot()拿到的是一张 GPU 纹理(GPU-backedSKImage),与原画布共享纹理、写时才复制,不会发生 GPU→CPU 的回读拷贝,几乎零开销。但暗坑就在这里:SKSurface是整个窗口的,而非当前控件的 ——因此不能整张快照,好在surface.Snapshot()支持传入SKRectI,只截取控件所在的那块子区域。- 控件在窗口里的绝对像素坐标没有现成 API 可取,计算有点弯弯绕绕:从画布
TotalMatrix(CTM)里取出平移分量TransX/TransY当作原点,再拼出pixelBounds。 - 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会:
- 为每个
@propertyuniform 生成一个 AvaloniaDirectProperty(可直接双向绑定到 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(示意):
|
|
小结
七拼八凑的扯了一通,回到那个略带自嘲的标题:
别人都在用显卡搞 AI,而我还在用来搞"过时"的技术
其实像素着色器肯定不是过时的(开玩笑,本职工作来的,怎么过时?),但是Avalonia、WinUI不支持罢了。
这一通折腾下来,有点心累,还好有AI。
怪不得套壳浏览器有一统天下的趋势。(若是网页,一句话就能让AI出成果了)
下一篇,聊聊最"根正苗红"的 WPF 篇——那才是这套东西最早生根发芽的地方。

浙公网安备 33010602011771号