为什么同一支手写笔,在 WPF 和 WinUI 3 里的跟手感差这么多?
为什么同一支笔,在 WPF 和 WinUI 3 里的跟手感差这么多?
之前产品经理总是觉得我特别菜,很多很多年了,总是觉得我跟她讲不清楚为什么我切换了winrt 的winui3延迟就降低了。原理是什么,和WPF相比到底怎么回事,怎么提升的,延迟怎么就从60ms 下降到20ms了。我总是给她讲不清楚,就只能说是winrt针对笔做了优化。不走WPF这套渲染了,有独立的优化。但是讲不清楚细节,就导致了干任何事情,她都觉得我很菜。因为很多winui3的源码,我也没有看过,太耗时间了,有些东西还需要反复看,然后还要梳理、消化。现在有AI了。上下文又很长,这个让我产生了写这篇博客的想法。当前这套里也没有讲清楚,因为有一些windows 不开放的API。 当然还有更多的问题没有解释清楚,为什么WPF的字体美化不能继续迁移到winui3了。为什么让你改个笔头你都改不了了。当前这些在其他的文章里写了。但是由于设计到很多windows 不开放的API。我觉得在他开源之前,这些事情没有价值。所以我不想深入去写了。就这这篇延迟的。价值比较高。
这篇文章只讲两个核心差异:
- 墨迹走的是哪一棵视觉树
- 用户最终感受到的延迟是怎样产生和被补偿的
一、先说结论
同样一支笔、同一块触控屏、同一块 60 Hz 屏幕,WPF 和 WinUI 3 的硬件输入速度不会因为应用框架不同而改变。
真正发生变化的是:
驱动拿到笔坐标以后,
墨迹通过什么路径进入屏幕。
可以先把两种方案理解成两条路:
WPF:
笔点 → WPF自己生成墨迹画面 → WPF渲染系统 → 屏幕
WinUI 3 + Windows InkPresenter:
笔点 → Windows系统墨迹引擎 → 系统合成树 → 屏幕
两条路都能画出湿墨迹,也都有后台处理能力。
主要差异不是“有没有湿墨迹”,而是:
- WPF 的湿墨迹先成为 WPF Visual,再经过 WPF 的渲染体系。
- WinUI 3 可以直接托管 Windows InkPresenter,让系统湿墨迹进入 Composition 合成树。
- Windows InkPresenter 还可以预测未来最多 20 ms 的笔尖位置,让墨迹在视觉上更靠近笔尖。
一句话总结:
WPF 是应用框架自己把墨迹画进窗口;UWP&WinUI 3 是把 Windows 系统墨迹通道直接接进窗口。
二、笔写延迟到底是什么
用户写字时,现实中的笔尖已经移动,但屏幕上的墨迹通常还停留在之前的位置:
屏幕墨迹末端 当前笔尖
●----------------------●
距离
这段距离越长,用户越觉得“不跟手”。
距离产生的原因是整条链路都需要时间:
笔尖移动
→ 触控屏扫描
→ 芯片计算坐标
→ 降噪和平滑
→ 驱动收到坐标
→ 软件生成墨迹
→ 画面进入显示系统
→ 屏幕刷新
两种需要区分的延迟
1. 真实延迟
从笔尖真实移动,到对应真实坐标变成屏幕像素的时间。
2. 视觉延迟
当前笔尖与屏幕墨迹末端之间的距离,换算成时间。
轨迹预测可以明显降低视觉延迟,但不会让硬件真的提前完成工作。
例如:
真实系统延迟 35 ms
向前预测补偿 -20 ms
------------------------
用户看到的跟手差 15 ms
因此,WinUI 3 开启 Prediction 后测得的 10~20 ms,更适合理解为预测后的视觉等效延迟。
三、两种方案前面的硬件完全一样
如果使用同一支笔和同一台设备,那么下面这段基本相同:
笔尖真实移动
↓
触控面板扫描笔的位置
↓
Touch IC 计算 X/Y、压力和倾角
↓
底层滤波,减少抖动和噪声
↓
Windows 驱动收到坐标
换成 WinUI 3 不会自动消除:
- 触控屏扫描时间
- 硬件滤波时间
- 坐标传输时间
- 显示器刷新时间
所以,WPF 和 WinUI 3 的主要差异从这里开始:
驱动已经有了笔坐标,接下来谁负责把坐标变成屏幕上的墨迹?
四、先理解“视觉树”是什么
“视觉树”可以把它理解成应用交给显示系统的一张分层画面清单。
例如一个笔记应用的画面可能是:
窗口
├─ 工具栏
├─ 页面背景
├─ 图片
├─ 已完成的笔迹
└─ 当前正在书写的湿墨迹
每个图形元素都在视觉树中占一个位置。显示系统根据这棵树决定:
- 什么内容在上面
- 什么内容在下面
- 内容位于哪里
- 内容多大
- 内容是否透明
- 哪些区域需要重新显示
因此,笔数据到达以后,并不是“有坐标就能立刻上屏”。软件还必须把墨迹变成视觉树中的可显示内容。
这正是 WPF 和 WinUI 3 方案最重要的差异。
五、WPF:先变成 WPF Visual,再进入 WPF 渲染系统
第一步:Pen Thread 接收笔点
WPF 有专门的 Pen Thread 接收高频笔数据。
这些数据包括:
坐标
压力
倾角
时间戳
按钮状态
所以 WPF 并不是完全依赖应用主线程接收每一个笔点。
第二步:DynamicRenderer 生成湿墨迹
WPF 中的 DynamicRenderer 是一个专门处理实时墨迹的组件。
DynamicRenderer 会在独立的 Dynamic Rendering Thread 上根据笔点生成临时湿墨迹。
可以把它想象成一个“临时画笔助手”:
Pen Thread 报告新位置
↓
DynamicRenderer 立即补画一小段
↓
用户先看到临时墨迹
这说明 WPF 本身也有:
- 独立输入处理
- 独立动态渲染线程
- 湿墨迹
所以不能简单说 WinUI 3 快,是因为 WinUI 3 有湿墨迹而 WPF 没有。
第三步:湿墨迹成为 WPF Visual
DynamicRenderer 生成的湿墨迹不是直接交给屏幕,而是生成一棵 WPF 动态视觉树。
DynamicRenderer
↓
DynamicRenderer.RootVisual
↓
WPF InkPresenter
这里的 RootVisual 可以理解为:
当前正在书写的临时墨迹,在 WPF 视觉树中的入口。
第四步:进入 WPF 的渲染管线
湿墨迹成为 WPF Visual 后,还要继续经过:
WPF Visual
↓
PresentationCore
↓
MIL / milcore
↓
窗口画面
↓
DWM 桌面合成
↓
屏幕
这条路径的意义是,WPF 可以把墨迹和应用里的按钮、图片、页面、缩放等内容统一管理。
代价是,实时墨迹仍然属于 WPF 的整套视觉和渲染体系。
第五步:抬笔后换成正式笔迹
WPF 在书写过程中显示的是临时动态墨迹,但最终还需要创建正式 Stroke。
书写时:动态湿墨迹
↓
抬笔后:UI Thread 创建正式 Stroke
↓
正式 Stroke 完成渲染
↓
删除临时湿墨迹
所以 WPF 同时维护两份内容:
临时动态 Visual
最终静态 Stroke
两者需要完成一次交接。
WPF 的核心特点
笔点
→ WPF DynamicRenderer
→ WPF 动态 Visual
→ WPF 渲染系统
→ 窗口
→ DWM
→ 屏幕
通俗地说:
WPF 的实时墨迹虽然由专门线程生成,但墨迹仍然要先加入 WPF 自己的画面体系,再通过 WPF 的渲染管线上屏。
六、WinUI 3:直接把 Windows 系统墨迹接入合成树
WinUI 3 实验版 InkCanvas 的核心变化,是直接托管 Windows 系统提供的 InkPresenter。
第一步:Windows InkPresenter 接管实时墨迹
InkPresenter 不只是一个显示控件。InkPresenter 统一负责:
接收墨迹输入
处理笔点
建立笔迹模型
生成湿墨迹
管理最终 Stroke
因此,应用不需要为每个笔点执行:
收到 PointerMoving
→ 应用计算曲线
→ 创建图形
→ 修改 XAML 元素
→ 等待页面重新渲染
第二步:建立独立的墨迹宿主
在桌面程序中,Windows Ink 使用专门的桌面托管机制。
可以把 IInkDesktopHost 理解成一个“系统墨迹工作区”:
应用 UI Thread
负责页面、按钮、业务逻辑
Ink Host Thread
负责系统墨迹相关工作
这不是简单地多创建一个线程。
真正重要的是:
墨迹宿主线程、InkPresenter 和系统合成树属于同一套 Windows 墨迹管线。
实时墨迹不需要先回到应用 UI Thread,再由应用把笔点变成普通 XAML 图形。
第三步:系统湿墨迹已经是一棵可合成的 Visual
InkPresenter 会生成系统管理的 Wet Ink Visual。
通过桌面承载和 Visual Link,这棵墨迹 Visual 可以接入 WinUI 3 的 Composition Tree。
Windows InkPresenter
↓
系统 Wet Ink Visual
↓
WinUI Composition Tree
通俗地说:
Windows 系统已经把实时墨迹做好了,WinUI 3 InkCanvas 只需要把这层系统墨迹“挂”到自己的窗口中。
而不是每一个笔点都重新走:
应用事件
→ 应用计算
→ XAML元素更新
→ 页面渲染
第四步:直接提交给合成系统
Wet Ink Visual 更新以后,可以直接提交 DirectComposition 命令:
新笔点到达
↓
InkPresenter 更新 Wet Ink Visual
↓
Composition Commit
↓
DWM 获得最新墨迹
↓
屏幕显示
这意味着墨迹更新不一定要等待普通 WinUI 页面完成一整轮 UI 更新。
WinUI 3 的核心特点
笔点
→ Windows InkPresenter
→ 系统 Wet Ink Visual
→ Composition Tree
→ DWM
→ 屏幕
通俗地说:
WinUI 3 不是自己重新画一遍墨迹,而是把 Windows 已经准备好的系统墨迹层直接接入窗口。
七、两棵视觉树的差异
这是整篇文章最重要的一张概念图。
WPF
笔点
↓
DynamicRenderer
↓
WPF 动态墨迹 Visual
↓
WPF InkPresenter
↓
WPF PresentationCore / MIL
↓
窗口内容
↓
DWM
↓
屏幕
WinUI 3 + Windows InkPresenter
笔点
↓
Windows InkPresenter
↓
系统 Wet Ink Visual
↓
DirectComposition / WinUI Composition Tree
↓
DWM
↓
屏幕
可以这样理解
WPF 像“把急件交给公司内部完整流程”
急件
→ 部门处理
→ 进入公司格式
→ 进入公司审批和发布流程
→ 对外发布
WinUI 3 像“给急件开了一条系统专线”
急件
→ 专线处理
→ 直接进入发布系统
这里不是说 WPF 的路径一定慢,也不是说 WinUI 3 完全没有中间步骤。
真正的区别是:
WPF 湿墨迹属于 WPF Visual;Windows Ink 湿墨迹属于系统 Ink Visual,并直接接入 Composition。
八、Prediction 为什么还能继续降低延迟
即使使用更直接的系统墨迹路径,硬件扫描、数据传输和屏幕刷新仍然需要时间。
因此,Windows InkPresenter 还提供一个视觉补偿机制:轨迹预测。
预测做了什么
当前已知的真实位置 预测的未来位置
●------------------●
最多20 ms
系统根据:
- 最近的坐标
- 速度
- 方向
- 加速度
预测笔尖在未来几毫秒的位置,并把湿墨迹先延伸过去。
默认预测时间是 15 ms,最大支持 20 ms。
预测没有做什么
预测没有让下面这些物理过程消失:
- 触控屏扫描
- 硬件降噪
- 数据传输
- 屏幕刷新
- 像素响应
预测只是提前猜测:
“按照现在的速度和方向,笔尖接下来大概率会到这里。”
为什么用户觉得明显更跟手
假设真实可见延迟为 35 ms,向前预测 20 ms:
真实延迟 35 ms
预测补偿 -20 ms
------------------------
视觉跟手差 15 ms
因此,WinUI 3 的低延迟体验通常来自两部分:
更直接的系统墨迹视觉链路
+
最多20 ms的轨迹预测补偿
九、为什么 60 Hz 屏幕也可能测到 10~20 ms
60 Hz 表示屏幕约每 16.67 ms 刷新一轮,但并不表示所有延迟只能是 16.67 ms 的整数倍。
原因包括:
- 笔数据可能在刷新周期中的任意时刻到达
- 屏幕采用逐行扫描,不是所有像素同时变化
- 墨迹位置可能通过预测向前补偿
- 测量常根据笔尖和墨迹之间的距离换算时间
因此,在 60 Hz 屏幕上测到 10~20 ms 的视觉等效延迟是可能的,但不能据此认为真实硬件因果延迟只有 10 ms。
十、对产品价值意味着什么
1. 用户最先感知的是笔尖附近
用户不会在书写时持续分析整条笔迹。用户最敏感的是:
笔尖旁边有没有墨迹
墨迹是否紧跟笔尖
急转弯时是否甩尾
停笔时是否继续滑动
所以,低延迟系统应优先优化实时湿墨迹和笔尖末端。
2. 更低延迟不等于更好的所有体验
预测越激进,笔迹越靠近笔尖,但也可能出现:
- 急转弯时的修正
- 停笔时的回拉
- 预测轨迹与真实轨迹不一致
因此产品设计要在两件事之间平衡:
更跟手
与
更稳定、更准确
3. 技术指标应该拆开
建议产品规格同时记录:
真实端到端延迟
预测后的视觉等效延迟
直线跟手距离
急转弯修正幅度
停笔回拉距离
只写一个“笔延迟 15 ms”,很容易掩盖测量口径差异。
十一、最终总结
给产品经理的一段话
同样的笔和屏幕,WPF 与 WinUI 3 的前半段硬件延迟基本相同。差异主要发生在坐标进入软件以后。WPF 的实时墨迹由 DynamicRenderer 生成,再作为 WPF Visual 进入 WPF 自己的渲染体系;WinUI 3 实验版 InkCanvas 则直接托管 Windows InkPresenter,把系统已经生成的 Wet Ink Visual 接入 Composition 合成树。这样,实时墨迹不需要由应用逐点转成普通 XAML 图形。同时,Windows InkPresenter 还可以预测未来最多 20 ms 的笔尖位置,让墨迹末端在视觉上更靠近笔尖。因此,WinUI 3 的优势不是“有湿墨迹,而 WPF 没有”,而是“系统墨迹视觉链路更直接,并且自带短时预测补偿”。
一句话版本
WPF 是把笔迹画进框架自己的视觉树,再交给显示系统;WinUI 3 是把 Windows 系统墨迹层直接挂到合成树,并用预测让墨迹提前追上笔尖。
三句话版本
- 硬件相同,所以扫描、滤波和传输延迟基本相同。
- WPF 和 WinUI 3 的主要差异,是湿墨迹进入屏幕时走的视觉树不同。
- WinUI 3 再通过最多 20 ms 的预测,进一步降低用户看到的笔尖与墨迹距离。
技术边界
本文可以确认的事实:
- WPF 有 Pen Thread、UI Thread、Dynamic Rendering Thread 和湿墨迹。
- WPF DynamicRenderer 会生成动态 WPF Visual,并通过 WPF 渲染体系显示。
- Windows InkPresenter 负责墨迹输入、处理和呈现。
- Windows Ink 桌面承载可以把墨迹 Visual 接入 DirectComposition Visual Tree。
- PredictionTime 默认 15 ms,有效范围 0~20 ms。
仍需通过测试确认的内容:
- Composition 路径本身具体节省多少毫秒。
- WinUI 3 关闭 Prediction 后,相对 WPF 还快多少。
- 当前项目 10~20 ms 的结果中,多少来自管线优化,多少来自预测。
建议保留三组对比:
WPF InkCanvas
WinUI 3 InkCanvas,PredictionTime = 0 ms
WinUI 3 InkCanvas,PredictionTime = 20 ms
这样产品和研发才能分别看到:
系统墨迹管线带来的收益
预测补偿带来的收益
参考资料
- Microsoft Learn,WPF Ink Threading Model
https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/the-ink-threading-model - Microsoft Learn,WPF DynamicRenderer
https://learn.microsoft.com/en-us/dotnet/api/system.windows.input.stylusplugins.dynamicrenderer - Microsoft Learn,WPF Architecture
https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/wpf-architecture - Microsoft Learn,IInkDesktopHost
https://learn.microsoft.com/en-us/windows/win32/api/inkpresenterdesktop/nn-inkpresenterdesktop-iinkdesktophost - Microsoft Learn,IInkPresenterDesktop::SetRootVisual
https://learn.microsoft.com/en-us/windows/win32/api/inkpresenterdesktop/nf-inkpresenterdesktop-iinkpresenterdesktop-setrootvisual - Microsoft Learn,InkModelerAttributes.PredictionTime
https://learn.microsoft.com/en-us/uwp/api/windows.ui.input.inking.inkmodelerattributes.predictiontime - Microsoft,WinUI InkCanvas 开源实现
https://github.com/microsoft/microsoft-ui-xaml/tree/main/controls/dev/InkCanvas
浙公网安备 33010602011771号