“阻断疗法” - 拯救 WPF 启动过程中发生设备热插拔导致触摸失效问题


如果你在WPF程序启动过程中进行设备热插拔(例如,插入一个U盘,一个USB摄像头),那么你的WPF程序很有可能失去所有触摸消息响应,通过 Tablet.TabletDevices.Count 检查当前程序的挂靠触摸设备,发现为0。有趣的是,如果你将触摸线重新插拔后,程序恢复正常。所以,这是WPF的Bug,微软的锅。那么这个锅的根本原因是啥?有兴趣的可以调试 .net framework 源码,这里没有深究。
如上面讲到,触摸线重新插拔就可以解决这个问题,但是,导致这个问题的热插拔设备也不是触摸设备啊,只是一个普通的U盘,反过来想,如果导致问题的不是触摸设备热插拔,反而触摸设备的热插拔能够修复这个问题,那我能不能“模拟”一下触摸设备的热插拔事件呢?在这篇文章里描述怎样模拟触摸设备移除事件来达到禁用WPF触摸的效果,反过来试试,通过 OnTabletAdded 事件看看能不能发生奇迹。然而,奇迹并没有发生,所以这个方法不行。
既然模拟设备添加事件的方法不行,那我从源头阻挡这个问题的发生:启动过程中不要处理设备变动事件。那么问题来了,我想要阻断 win32 WM 事件通知,必须要拿到一个窗口句柄呀,但是在 mainwindow show 出来的时候,这个问题已经发生了,这个时候的阻断已经没有效果了,一定要程序启动一开始做阻断。进一步搜索,这里https://stackoverflow.com/questions/38642479/how-to-disable-wpf-tablet-support-in-surface-4-pro 是一个突破口:

WPF does not register to these messages on the applications MainWindow, but through a hidden windows named “SystemResources…” which is created for each application instance. So handling those messages on the MainWindow (which would be easy) does not help here.

相信看到这里,聪明的你已经知道怎么做了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class WPFTouchUtil
{
//添加钩子,阻断设备改动消息
public static void HandleDeviceChangedWM()
{
// hook into internal class SystemResources to keep it from updating the TabletDevices on system events
object hwndWrapper = GetSystemResourcesHwnd();
if (hwndWrapper != null)
{
// invoke hwndWrapper.AddHook( .. our method ..)
var internalHwndWrapperType = hwndWrapper.GetType();
// if the delegate is already set, we have already added the hook.
if (_handleAndHideMessageDelegate == null)
{
// create the internal delegate that will hook into the window messages
// need to hold a reference to that one, because internally the delegate is stored through a WeakReference object
var internalHwndWrapperHookDelegate = internalHwndWrapperType.Assembly.GetType("MS.Win32.HwndWrapperHook");
var handleAndHideMessagesHandle = typeof(WPFTouchUtil).GetMethod(nameof(HandleAndHideMessages), BindingFlags.Static | BindingFlags.NonPublic);
_handleAndHideMessageDelegate = Delegate.CreateDelegate(internalHwndWrapperHookDelegate, handleAndHideMessagesHandle);
// add a delegate that handles WM_TABLET_ADD
internalHwndWrapperType.InvokeMember("AddHook",
BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public,
null, hwndWrapper, new object[] { _handleAndHideMessageDelegate });
}
}
}
//移除钩子,恢复状态
public static void RestoreDeviceChangedWM()
{
object hwndWrapper = GetSystemResourcesHwnd();
if (hwndWrapper != null)
{
var internalHwndWrapperType = hwndWrapper.GetType();
internalHwndWrapperType.InvokeMember("RemoveHook",
BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public,
null, hwndWrapper, new object[] {_handleAndHideMessageDelegate});
}
}
private static Delegate _handleAndHideMessageDelegate = null;
private static object GetSystemResourcesHwnd()
{
var internalSystemResourcesType = typeof(Application).Assembly.GetType("System.Windows.SystemResources");
// get HwndWrapper from internal property SystemRessources.Hwnd;
var hwndWrapper = internalSystemResourcesType.InvokeMember("Hwnd",
BindingFlags.GetProperty | BindingFlags.Static | BindingFlags.NonPublic,
null, null, null);
return hwndWrapper;
}
private static IntPtr HandleAndHideMessages(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == (int)WindowMessage.WM_DEVICECHANGE)
{
handled = true;
}
return IntPtr.Zero;
}
enum WindowMessage : int
{
WM_DEVICECHANGE = 0x0219
}
}

在程序刚启动的时候添加“阻断”,启动流程过后,不要忘了恢复状态。
缺陷,如果程序启动过程中,真的发生了触摸设备变动,也会被阻断。

评论

Timelapse#2

Lens: Nikkor 24-70mm F2.8
f2.8 70mm ISO400

评论

WPF 渲染小结

在上一篇文章D3DImage - 它能做啥、解决了什么问题、有哪些瓶颈、怎么最佳实践最后,提到DropShadowEffect严重影响到D3DImage的渲染性能问题,导致程序在渲染8分屏(8个远端视频)的时候,出现严重的性能下降,渲染卡顿。要知道,在使用原生窗口渲染方案渲染8分屏,CPU占用和内存占用也不过25%和~200Mb,稍差一点,使用D3DImage优化后方案渲染,CPU占用并没有出现多大的跳跃,大约在30%左右。即使是添加了DropShadowEffect的情况下,CPU占用和内存占用好像都没有多大变化;既然在CPU和内存占用都没有多大变化的情况下,WPF渲染卡顿,那肯定(可能吧)是“帧生成”时间过长的锅。

帧生成时间

玩游戏的人都知道,影响游戏帧数的一个关键因素是帧生成时间,帧生成时间过长必定导致游戏FPS下降,游戏不流畅。帧生成时间并不等同帧更新时间,这个需要搞清楚。例如一个游戏锁帧60FPS,那么帧更新时间为1000/60=16.6ms,通常来说,如果你硬件性能足够强劲,那么帧生成时间要小于16.6ms才能保证游戏运行在60FPS的帧率下,否则会掉帧。类似的,WPF的渲染帧率下降可能(无责任猜想)也是同样的因素导致。But why?

DropShadowEffect 的锅

不要误会,DropShadowEffect并没有什么过错。只是在特定情境下,DropShadowEffect(及其他所有Effect类),就是WPF渲染瓶颈的关键:

  • 将Effect应用到时刻变化的元素
  • 在应用了Effect的元素上,叠加了其他时刻变化的兄弟元素

远程会议的问题就是碰到了第二种情景,我们以为只要不直接应用DropShadowEffect到D3DImage这种时刻更新帧的元素上,应该就能避免渲染瓶颈,然而被打脸。
说了这么久,好像还是没有说为什么;年轻人,不要这么着急,继续往下看。

WPF 的渲染知识两则

  • 当WPF在渲染一个窗口的时候,它只更新需要更新的区域,称为脏区(DirtyRect)。
  • 显存的占用与渲染面积成正相关

使用下面这个例子来模拟导致问题的场景:

左边是应用了DropShadowEffect的Grid,中间是应用ColorAnimation的Grid,右边是3个视频渲染。暂时来说,情况看起来还是可以的,没有出现明显的渲染卡顿,整个界面的渲染都维持在一个比较高的帧数。

那么,将中间的元素叠加在左边的元素上看看:

问题出现了,帧率下降严重,视频出现卡顿,ColorAnimation变得不平滑:

在这两种情况下,脏区数量都是一样的,分别是始终变化的ColorAnimation Grid和视频区,唯一不同的是,ColorAnimation Grid的位置变了,与应用了DropShadowEffect的Grid部分重叠了,这导致每帧渲染多了一个HW IRT(hardware intermediate render target),对于WPF来说,HW IRT是一个代价高昂的渲染过程,比它更惨的是SW IRT,如果你的WPF程序在渲染过程中出现多个这种渲染过程,那么可以肯定你的程序需要完成大量的工作来渲染你的程序。

那么,什么是IRT?

Intermediate Render Target。在现代的图形处理单元(GPU)中,我们可以将我们要进行渲染的内容先在Render Target中渲染,然后像素着色器可以通过处理这个Render Target来添加特定的效果,这个过程完成后才将处理完的数据储存到后台缓存(Back Buffer),这个时候渲染线程(Render Thread)可以将back buffer拷贝到前台缓存(Front Buffer)进行显示。对应到上面的例子,动态元素在拥有DropShadowEffect的元素上刷新,引起脏区更新,这个脏区有关DropShadwoEffect,DropShadowEffect需要像素着色器渲染指令(因为它本身就是由HLSL创建的),嘣!!!,IRT就来了。但是,IRT在一次渲染中是很正常的啊,有些WPF程序在一次渲染中可能存在几个IRT都不会引起这么明显的性能下降。4K是性能的试金石,要知道,我们的程序是运行在4K下的,变化的脏区面积足够大,才引起了显著的性能下降,而且,不要忘了,WPF在使用像素着色器时有天生的缺陷,这篇文章有详细说明,其中提到的关键一点:

WPF has an extensible pixel shader API, along with some build in effects. This allows developers to really add some very unique effects to their UI. In Direct3D when you apply a shader to an existing texture, it’s very typical to use an intermediate rendertarget…after all you can’t sample from a texture you are writing to! WPF does this also, but unfortunately it will create a totally new texture EACH FRAME and destroy it when it’s done. Creating and destroying GPU resources is one of the slowest things you can do on a per frame basis. I wouldn’t even typically do this with system memory allocations of that size. There would be a considerable performance increase on the use of shaders if somehow these intermediate surfaces can be reused. If you’ve ever wondered why you get noticeable CPU usage with these hardware accelerated shaders, this is why.

至此,WPF的渲染相关文章结束。

评论

D3DImage - 它能做啥、解决了什么问题、有哪些瓶颈、怎么最佳实践

D3dImage,.Net Framework 3.5 之后,微软提供的一个全新的ImageSource对象,可以在WPF中很好的呈现DirectX内容;在此之前,你只能将DirectX内容直接渲染在Windows窗口之上,这必然引起令人头疼的AirSpace问题,为了在这些内容上面添加我们习以为常的WPF UI 元素,你只能使用Popup来承载这些内容,完全丧失WPF UI开发的灵活性,且有经验的WPF程序员都知道一个事实:WPF Popup就是一个深坑 - 你需要手动处理各种显示隐藏问题、因为其导致的焦点问题,显示层级问题以及最令人头疼的性能问题,特别是在4K屏幕下,因为我们都知道,Popup就是一个Window,为了解决Airspace问题而使用Popup来承载UI必定需要使其AllowTransparency=True,这就引起了另外一个问题,透明窗口占用内存与其面积成正相关,在4K屏幕下,你可能将整个程序大部分的内存占用贡献给了这些Popup UI。说了这么多,好像在诉控Popup有多么的垃圾(它的确如此,如果在做大量的UI容器时)。

年轻人,如果你觉得Airspace问题真的没有办法解决了,只能用Popup这种技术手段来规避了,那么听老人一句话,不要浪费时间在Popup上了,因为你在前期投入的时间来规避种种Popup UI导致的问题以及各种你意想不到的Bug,到最后总会碰到解决不了,完全不能规避的情况,从而导致整个Popup UI替换方案完全失败的情形。

查看更多

评论

FFmpeg 入门

废话少说,要解码一个视频帧,你需要这样做:

获得帧数据后,用SDL还是直接用D3d渲染,那就看你自己了。

评论

D3DImage in WPF

在.Net Framework 3.5 SP1中,微软在WPF中提供了D3DImage对象,D3DImage是一个ImageSource,这可以让我们在WPF原生的D3D Surface上渲染Direct3D Surface,大大提高了WPF和DirectX内容的交互性。
在此之前,要想在WPF上渲染DirectX的内容,只能让DirectX直接渲染到窗口上,这样会造成不可避免的Airspace问题,因为DirectX内容要时刻刷新重绘,导致WPF窗口上的其他内容被覆盖,表现就是DirectX内容始终在窗口最顶层。
正如前面所说,D3DImage是一个ImageSource,在WPF中,这意味着,我们可以将一个3D场景变成一个Image对象的Source,或者构建一个ImageBrush,这意味这D3D Surface可以渲染到WPF中任意一个以Brush进行渲染的元素上,例如图片,文本前景色,元素背景色等等。
如下,就是一个典型的应用D3DImage的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (d3dimg.IsFrontBufferAvailable)
{
IntPtr pSurface = IntPtr.Zero;
pSurface = _view.GetBackBuffer();
if (pSurface != IntPtr.Zero)
{
d3dimg.Lock();
d3dimg.SetBackBuffer(D3DResourceType.IDirect3DSurface9,pSurface);
_view.Draw();
d3dimg.AddDirtyRect(new Int32Rect(0, 0, d3dimg.PixelWidth, d3dimg.PixelHeight));
d3dimg.Unlock();
}
}

只需要Direct3D内容渲染的Surface接口指针即可,如上面的pSurface。

评论

There Is No Thread

这里并没有线程

原文地址: http://blog.stephencleary.com/2013/11/there-is-no-thread.html


最纯粹的async形式中存在一个重要的真相:这里并没有线程(或者不存在新建的线程)
举不胜数的反对者哭喊道:“不!如果我正在等待一个操作,那一定存在一个线程在等待这个操作!它可能是一个线程池中的线程。或者是系统线程!或者是其他类似设备驱动的东西…”。

不要听从那些哭喊的人。如果那些async操作是纯粹的,那么这里将不会存在线程。

那些持怀疑态度的人并没有被说服。让我们来娱乐一下他们。

我们可以一路跟踪一条异步操作指令到硬件层面,特别留意其中的.Net部分和设备驱动部分。为了简化这部分描述,我们排除掉部分中间层的细节,但是这应该不会让我们偏离真相。

思考一个通用的“写”操作(写一个文件、网络流、USB等等)。我们的代码很简单:

1
2
3
4
5
private async void Button_Clicked(object sender, RoutedEventArgs e)
{
byte[] data = ...
await myDevice.WriteAsync(data, 0, data.Length);
}

我们早就知道,UI线程并不会被await操作阻塞。那么问题来了:这里是不是存在另外一个线程,它牺牲自己所以UI线程才能存活?

抓住我的手,我们要潜的更深一点。

第一站:类库(例如,查看BCL代码)。我们假设 WriteAsync 是用 .Net 标准的 P/Invoke 异步 I/O 操作实现的。所以,这个操作在设备的句柄上开始了一个Win32的重叠I/O操作。

系统紧接着转到设备驱动并让设备开始写操作。它首先构造一个表示写请求的对象,这被称为I/O Request Packet(IRP)。设备驱动获得这个IRP并向设备发起命令来写对应的数据。如果设备支持Direct Memory Access(DMA),这个操作就像向设备寄存器中写入缓存地址一样简单。这是设备驱动能做的所有事情;它使得IRP进入“等待”并转回到系统。

事实的核心:当处理IRP时,设备驱动不允许堵塞。这意味着,如果IRP不能立即完成,那么它一定要异步执行。这对于同步API也一样成立。在设备驱动这一层,所有(重要的)请求都是异步的。
随着IRP进入“等待”状态,系统通过返回一个未完成的Task给刚才堵塞的async按钮点击事件,然后UI线程继续执行。
我们深入追踪到系统底层的写操作,直至物理设备。
现在,写操作正在执行,那有多少线程在处理它呢?

一个都没有

这里并没有设备驱动线程,系统线程,BLC线程或者线程池线程在处理那个写操作。这里根本就没有线程
现在,我们看一下对应的回复(Response)。
写请求开始片刻,设备完成了写操作,他通过中断(Interrupt)通知CPU。
设备驱动的Interrupt Service Routine(ISR)对这个中断做出反应。中断是CPU层的时间,它会临时从当前CPU所运行的线程中获得CPU的控制权。你可以认为ISR是在“借用”当前正在运行的线程,但是我更倾向于认为ISR在更底层中执行,底层到根本不存在线程这个概念的水平。或者说它在所有线程之下。
无论如何,ISR已经被妥当的进行了写操作,它做的所有事情就是告诉设备“谢谢你的中断请求”并且将一个Deferred Procedure Call(DPC)入队(queue)。
当CPU被中断“骚扰”完之后,它会转向它的DPC。DPC也是在一个不能直接用线程描述的底层水平。和ISR一样,DPC直接在CPU上执行,在线程系统之下。
DPC获取代表写操作的ISR并将其标志成“完成”。然而,那么“完成”状态只存在系统层;必须要通知到进程自己拥有的内存空间。所以系统会入队一个special-kernel-mode Asynchronous Procedure Call(APC)给拥有HANDLE的线程。
因为上述提到的类库/BLC使用的是标准P/Inovke Overlapped I/O 系统,它早就注册了I/O Completion Port(IOCP)的句柄,这个句柄是线程池的一部分。所以,一个I/O线程池线程被短暂的“借用”来执行APC,通过它来通知task已经完成。
现在task捕获了UI线程的上下文,它不直接从线程池线程中返回async方法,相反,它将async方法后续的执行加入到UI线程的上下文,当UI线程执行到这里的时候就会继续执行这部分代码。
所以,当请求发生时,我们可以看到这里并没有线程。当请求完成时,大量的线程被“借用”或者短暂暂存到它们那里。这些工作大约在一毫秒(运行在线程池的APC)或者低到一微秒(例如ISR)。但是这里并没有线程因为等待请求完毕而被堵塞。

评论

Mandelbrot Set - 在复平面上绘制曼德博集合

曼德博集合(Mandelbrot set,或译为曼德布洛特复数集合)是一种在复平面上组成分形的点的集合,以数学家本华·曼德博的名字命名。曼德博集合与朱利亚集合有些相似的地方,例如使用相同的复二次多项式来进行迭代。(维基百科)

其中的迭代公式:

1
f(Zn+1)=Zn^2+c

其中,c是任意复数。我们知道c可以表示为:c=x+y*i。根据复数的定义,i^2=-1。因此,我们通过将二维平面当作复平面,x是其中复数的实部R,y是复数的虚部Im。根据上面的额迭代公式,使用OpenCL对每个点进行同步的迭代,快速得到曼德博集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//openCL代码
kernel void Mandelbrot(
global write_only int* result,
int width,//图片宽度
int height,//图片高度
float minReal,//最小实部
float maxReal,//最大实部
float minIma,//最小虚部
float maxIma,//最大虚部
int max_iter)//最大迭代次数,当迭代这么多次后,该复数还没逃逸,则认为其在曼德博集合中
{
//获取GPU当前线程的编号,可以将这个物化为图片当前位置的一个像素点的计算方位
int tX = global_get_id(0);
int tY = global_get_id(1);
//计算坐标的刻度值
float real_inter = (maxReal - minReal) / width;
float ima_inter = (maxIma - minIma) / height;
//当前线程(像素点)在复平面的位置
int cX = minReal + tx * real_inter;
int cY = minIma + (height - tY) * ima_inter;
//迭代
float zX=0;
float zY=0;
int iter=0;
float length_sqr=0;
do
{
iter++;
float temp = zX * zX - zY * zY + cX;
zY = 2 * zX * zY + cY;
zX = temp;
length_sqr=zX * zX + zY * zY;//在GPU上,根号运算要慢得多
}//根据曼德博集合的性质,我们知道集合的中的任意一个复数|z|<2,因此对于复数z=tX+tY*i,|z|^2=tX*tX+tY*tY<4
while(length_sqr < 4 && iter < max_iter);
int loc = tY * width + tX;
result[loc] = iter;//通过迭代值来标识当前位置的像素点是否属于曼德博集合
}

这种每个点都可以独立迭代运算,互不干扰的特性,最适合用GPU来进行计算的了,因此,通过OpenCL,我们可以快速得到想要的数据。使用 Surface Book 的内置显卡nVidia 520计算3000*3000规模,迭代512次的数据,可以在30ms内完成。如下是通过得到的数据生成的一些配色图:

X光效果

复数与曼德罗集合的相关信息,可参考:https://msdn.microsoft.com/zh-cn/library/jj635753(v=vs.85).aspx

评论

OpenCL with CLOO

CLOO 是一个对 OpenCL 的 .Net 封装,可以让 .Net/Mono 程序充分使用 OpenCL 的优势,易用、开源。

今天用 OpenCL 中的 “Hello World” 程序 - 矩阵乘法,来简单介绍一下 OpenCL。

OpenCL 是一个开放的工业标准,既然是开放的,那么,所有厂商就可以提供自己的实现,例如英特尔,英伟达等等。也正因为如此,也导致在同一台机器上可能存在多个支持不同版本的硬件。例如,英伟达的GPU到现在也才支持 OpenCL 1.2 版本,但是OpenCL都已经出到2.x版本了。

我们可以查看当前设备中,有哪些厂商提供了 OpenCL 的支持,以及运算平台是啥。例如,在 Surface Book (with Nvidia GPU) 上,我们调用以下代码看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//获取所有平台
var platforms = ComputePlatform.Platforms;
foreach(var platform in platforms)
{
Console.WriteLine($"{platform.Name},{platform.Version}");
//获取该平台下的计算设备
var devices = platform.QueryDevices();
foreach(var device in devices)
{
Console.WriteLine($" Device:{device.Name}");
}
}

查看更多

评论

Timelapse

4K:http://omg3ewm0l.bkt.clouddn.com/timelapse_home.mp4

评论