【翻译】关于 WPF 透明窗口的内存占用

翻译自己的文章才是最骚的。。。 Origin Post


要实现一个透明的 WPF 窗口?
多么简单的一个任务啊!只要设置 AllowTransparency 和 WindowStyle,你可以在毫秒间完成这个任务。

AllowTransparency="True" WindowStyle="None" Background="Transparent"

正确吧?当然。
但是(你懂得),打开你的任务管理器看看,简单的任务通常会带来大量的内存占用,特别是4K分辨率的透明窗口。100+ MB的内存被浪费了,就只为了显示一个空白的透明窗口!这是不可接受的。一年前,如果你说:”谁关心内存呀,现在的内存条太便宜了。“,你可能是对的。但是查查这一年内存条的价格走向,它们现在贵上天了。

WPF 透明窗口的有趣小真相

  • 内存占用随着窗口尺寸增大而增加
  • Win32 窗口没有这样的问题

等等,什么?窗口越大,内存消耗的更多?嗯。。。这看起来很熟悉嘛,就像一个Bitmap。知道现在,我们并不知道 WPF 是如何处理透明窗口的,但是这种症状显示它就好像直接将整个桌面作为一个位图,然后窗口用这张位图的重叠部分作为其背景来更新自己,让它看起来是“透明”的。多么聪明的做法呀。。。
在 WPF 刚刚发布的那些日子里,低分辨率的计算机屏幕占据主流位置,即使在今天,大多数的笔记本电脑依然带着一块1366*768分辨率的屏幕被推向市场(离谱吧)。让我们唾弃那些OEM厂商讲的毫无根据的废话并且思考一下运行在高分辨率下的程序的情况。

内存并不是免费的,不要浪费之

很显然,浪费100+MB的内存来显示一个4K的透明窗口是不可接受的,特别是和 Win32 窗口只占用10+MB的内存进行比较时。这差距让 WPF 看起来蠢透了。
抱怨已经够多了,想想对此我们能做什么呢?我可不想用C++和GDI将我的UI代码重写一遍,这太没效率并且也跟不上时代,况且,没人会为此“区区小事”去放弃他们漂亮、易于维护的Xaml UI代码。

使用 Win32 承载 WPF 内容

好吧,确实,没人愿意为了区区90MB内存去重写它们的UI。与使用C++重写UI所耗费的精力相比,这个内存的占用差距看起来是可以接受的(#笑脸)。但是请记住,我们一如既往的可以在 Win32 窗口中承载 WPF 的内容。
例如,我们想创建一个全屏、半透明背景带着非透明内容的对话框。为了规避 WPF 透明窗口的内存问题,我们可以使用 Win32 创建一个半透明的窗口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DWORD Flags1 = WS_EX_LAYERED;
DWORD Flags2 = WS_POPUP;

HWND hWnd = CreateWindowEx(Flags1,szWindowClass, szTitle, Flags2,
CW_USEDEFAULT, 0, 3840,2160, nullptr, nullptr, hInstance, nullptr);

SetLayeredWindowAttributes(hWnd, RRR, (BYTE)125, LWA_ALPHA);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);

case WM_ERASEBKGND:
RECT rect;
GetClientRect(hWnd, &rect);
FillRect((HDC)wParam, &rect, CreateSolidBrush(RGB(0, 0, 0)));
break;

通过启用 C++/CLI,我们可以直接访问 WPF 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace ManagedCode
{
using namespace System;
using namespace System::Windows;
using namespace System::Windows::Interop;
using namespace System::Windows::Media;

HWND GetHwnd(HWND parent, int x, int y, int width, int height) {
HwndSource^ source = gcnew HwndSource(
0, // class style
WS_VISIBLE | WS_CHILD, // style
0, // exstyle
x, y, width, height,
"hi", // NAME
IntPtr(parent) // parent window
);

UIElement^ page = gcnew ManagedContent::WPFContent();
source->RootVisual = page;
return (HWND)source->Handle.ToPointer();
}
}

最后

1
2
//managed content
ManagedCode::GetHwnd(hWnd, 0, 0, 200, 200);

由于 WPF 和 GDI 背后的技术不尽相同,还有更多的工作需要做来解决不可避免的透明通道问题,但是,为了方便,你始终可以使用 Popup 来实现你的目标。


Macbook Pro 2016 的键盘真垃圾。。。。。。

评论

Light Weighted DropshadowEffect

Let’s create a light weighted wpf drop shadow effect, considering that the origin one performs badly in some special occasions.
As I mentioned before (check it out), the original WPF DropShadow Effect can cause severe preformance problem. Due to the “flaw” M$FT brought to the HLSL support for WPF, the Effect class that implements the visual effect creates and destroy GPU resource each frame, which is the worst thing you could do with GPU resources. So, what about implementing a custom shadow effect to avoid it? This sounds interesting.

– 春节补充

评论

关于 WPF 透明窗口的内存占用

要实现一个透明的 WPF 窗口?
What an easy task! By setting AllowTransparency and WindowStyle, you could finish it in seconds.

AllowTransparency="True" WindowStyle="None" Background="Transparent"

Correct? Of course.

However (you know this is coming), look at your task manager, easy task comes with large memory consumption, especially for 4K transparent window. 100+ MB ram are wasted for just showing an empty, transparent window! That’s unacceptable. A year ago, you might be right saying “Who cares about RAM, they are cheap as hell”, but check out the price they’ve grown over this year, they are expensive as hell now.

Fun fact of WPF transparent window

  • RAM usage increase as window size enlarge
  • Win32 window has no such problem

Wait, what? The larger the window is, the more RAM it consumes? Hmmmmm… this looks familiar, just like a Bitmap. For now, we don’t know how WPF handles transparent window, but the symptom shows that it’s like using the whole screen as a bitmap and the window updating itself with portion of that bitmap, making it “transparent”. What a smart move…
Back in the days when WPF was first released, low screen resolution was the main stream. Even today, most laptops still are shipping with a monitor of 1366*768 (ridiculous, right?). Let’s despise the nonsense the OEM told us and think about program running in computers with higher screen resolution.

RAM is not free, do not waste it

Obviously, costing 100+ mb of ram for showing a transparent window in 4K is unacceptable, especially compared with Win32 transparent window, which costs only 10+ mb. The gap between them makes WPF look dump.
Enough complaining, what can we do about it? I don’t want to write UI code with GDI using C++, that’s inefficient and not modern, plus, no one would abandon their beautiful, easy to maintain xaml UI code for this.

Hosting WPF content in Win32 Window

Well, indeed, no one would rewrite their UI code for just about 90mb of RAM. Compared with the work needed to rewrite C++ UI code, the RAM consumption gap seems acceptable (#smile face). But please remember, we can always host WPF content in win32 window.
Let say, we want to create a full screen notification dialog with semi-transparent background and apaque notication content in the center. To avoid the WPF ram problem, we create a semi-transparent window using win32:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DWORD Flags1 = WS_EX_LAYERED;
DWORD Flags2 = WS_POPUP;

HWND hWnd = CreateWindowEx(Flags1,szWindowClass, szTitle, Flags2,
CW_USEDEFAULT, 0, 3840,2160, nullptr, nullptr, hInstance, nullptr);

SetLayeredWindowAttributes(hWnd, RRR, (BYTE)125, LWA_ALPHA);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);

case WM_ERASEBKGND:
RECT rect;
GetClientRect(hWnd, &rect);
FillRect((HDC)wParam, &rect, CreateSolidBrush(RGB(0, 0, 0)));
break;

By enabling C++/CLI, we can access WPF content directly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace ManagedCode
{
using namespace System;
using namespace System::Windows;
using namespace System::Windows::Interop;
using namespace System::Windows::Media;

HWND GetHwnd(HWND parent, int x, int y, int width, int height) {
HwndSource^ source = gcnew HwndSource(
0, // class style
WS_VISIBLE | WS_CHILD, // style
0, // exstyle
x, y, width, height,
"hi", // NAME
IntPtr(parent) // parent window
);

UIElement^ page = gcnew ManagedContent::WPFContent();
source->RootVisual = page;
return (HWND)source->Handle.ToPointer();
}
}

and finally

1
2
//managed content
ManagedCode::GetHwnd(hWnd, 0, 0, 200, 200);

Due to the different technologies behind WPF and GDI, more work needed to be done for the unavoidable alpha blending problem, but, you can use wpf popup to achieve your goal for short.

评论

使用 OpenCL 实现图片高斯模糊

高斯模糊( https://zh.wikipedia.org/wiki/%E9%AB%98%E6%96%AF%E6%A8%A1%E7%B3%8A )是一种常见的图像处理算法,使用高斯分布与图像做卷积,得到模糊的效果。其二维定义:

σ是正态分布的标准偏差。在应用的时候,假设σ为2.5。对于模糊半径为1,则高斯矩阵为3*3的一个矩阵,以[1,1]为中心,带入公式计算高斯矩阵的值,得到:

0.0216996633 0.0235069655 0.0216996633
0.0235069655 0.0254647918 0.0235069655
0.0216996633 0.0235069655 0.0216996633

他们的和为 0.206291318,我们需要他们的和为1,因此与总和相除得到:

0.105189413 0.113950342 0.105189413
0.113950342 0.123440929 0.113950342
0.105189413 0.113950342 0.105189413

根据这个矩阵,对图像的每个像素点进行计算,计算的九个点的各rgb分量之和就是最终像素的rgb分量。

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
69
70
71
72
73
74
75
76
//计算高斯矩阵
private void ComputeWeightMatrix()
{
var center = Radius;
var conBase = 2 * Math.Pow(Variance, 2);
var conRoot = 1 / (Math.PI * conBase);

float sum = 0f;
for (int x = -Radius; x <= Radius; x++)
{
for (int y = Radius; y >= -Radius; y--)
{
var weight = conRoot * Math.Pow(Math.E, -(x * x + y * y) / conBase);
_matrix[GridPosToArrayIndex(x, y, center, Radius)] = (float)weight;
sum += (float)weight;
}
}
for (int i = 0; i < _matrix.Length; i++)
{
_matrix[i] /= sum;
}
}

//Compute
public void Compute(string imageFile)
{
using (var bitmap = new Bitmap(imageFile))
{
var datas = bitmap.LockBits(new Rectangle(new Point(), new Size(bitmap.Width, bitmap.Height)),ImageLockMode.ReadOnly,bitmap.PixelFormat);
var dataSize = datas.Stride * datas.Height;
var argbs = new byte[dataSize];
var dsts = new byte[dataSize];
int matrixWidth = Radius * 2 + 1;
Marshal.Copy(datas.Scan0, argbs, 0, dataSize);

Stopwatch sw=Stopwatch.StartNew();
for (int y = 0; y < bitmap.Height; y++)
{
for (int x = 0; x < bitmap.Width; x++)
{
float sumA = 0;
float sumR = 0;
float sumG = 0;
float sumB=0;
for (int i = 0; i < _matrix.Length; i++)
{
var pos = transform_pos(x, y, matrixWidth, bitmap.Width, bitmap.Height, Radius, i);
var position = pos.Y * datas.Stride + pos.X*4;
sumR += argbs[position] * _matrix[i];
sumG += argbs[position + 1] * _matrix[i];
sumB += argbs[position + 2] * _matrix[i];
sumA += argbs[position + 3] * _matrix[i];
}
var dstPos = y * datas.Stride + x * 4;
dsts[dstPos] = (byte)sumR;
dsts[dstPos+1] = (byte)sumG;
dsts[dstPos+2] = (byte)sumB;
dsts[dstPos+3] = (byte)sumA;
}
}
bitmap.UnlockBits(datas);

var elapse = sw.Elapsed;
Console.WriteLine($"Costing: {elapse}");
Debug.WriteLine($"Costing: {elapse}");

var handle = GCHandle.Alloc(dsts, GCHandleType.Pinned);
using (var dstBmp = new Bitmap(datas.Width, datas.Height, datas.Stride, bitmap.PixelFormat,
handle.AddrOfPinnedObject()))
{
dstBmp.Save("processed_normal.bmp");
}

handle.Free();
}
}

当然,这样能完成工作,但是耗时太长,对于3000*1920尺寸的图片处理需要2分51秒(Intel Core i7-4770),这显然是不可接受的。
对于这种分别计算每个像素,且各像素间互不干扰的问题,使用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
44
45
/*
OpenCL 高斯模糊代码
Copyright Gandalfliang
*/
inline int2 transform_pos(int centerX,int centerY,int matrixWidth,int radius,int index)
{
int x=index%matrixWidth;
int offsetX=x-(radius+1);
int y=index/matrixWidth;
int offsetY=radius-y;
return (int2)(centerX+offsetX,centerY-offsetY);
};

const sampler_t sampler_img=CLK_NORMALIZED_COORDS_FALSE|CLK_ADDRESS_CLAMP_TO_EDGE;

//opencl kernel 代码
kernel void gaussian_blur(
read_only image2d_t src,
global write_only char* dst,
global read_only float* matrix,
read_only int radius,
read_only int width)
{
int x=get_global_id(0);
int y=get_global_id(1);

float sumR,sumG,sumB,sumA;
int matrixWidth=radius*2+1;
int matrix_size=pow(matrixWidth,2);
for(int i=0;i<matrix_size;i++)
{
int2 pix=transform_pos(x,y,matrixWidth,radius,i);
uint4 rgba = read_imageui(src,sampler_img,pix);
sumR+=rgba.x*matrix[i];
sumG+=rgba.y*matrix[i];
sumB+=rgba.z*matrix[i];
sumA+=rgba.w*matrix[i];
}

int loc=y*width*4+x*4;
dst[loc]=sumR;
dst[loc+1]=sumG;
dst[loc+2]=sumB;
dst[loc+3]=sumA;
}

Host代码:

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
public void Compute_cl(string imageFile)
{
//选取设备
var platform = ComputePlatform.Platforms.FirstOrDefault();
var device = platform.Devices.FirstOrDefault();
//设置相关上下文
var properties = new ComputeContextPropertyList(platform);
var context = new ComputeContext(new[] {device}, properties, null, IntPtr.Zero);
//命令队列,用于控制执行的代码
ComputeCommandQueue commands = new ComputeCommandQueue(context, context.Devices[0],
ComputeCommandQueueFlags.None);
//读取opencl代码
var code = File.ReadAllText(@"gaussianblur.cl");
//编译
var program = new ComputeProgram(context, code);
try
{
program.Build(new[] {device}, null, null, IntPtr.Zero);
}
catch (Exception ex)
{
throw;
}

var images = CreateImageFromBitmap(imageFile, context,
ComputeMemoryFlags.ReadWrite | ComputeMemoryFlags.CopyHostPointer);

//创建核心代码,就是cl代码中以kernel标识的函数
var kernel = program.CreateKernel("gaussian_blur");
//矩阵规模
//储存计算结果的数组

//创建的核心代码函数以这种方式来传参
var resultBuffer=new ComputeBuffer<char>(context,ComputeMemoryFlags.WriteOnly, dstBytes.Length);
kernel.SetMemoryArgument(0, images);
kernel.SetMemoryArgument(1, resultBuffer);
kernel.SetMemoryArgument(2, new ComputeBuffer<float>(context,ComputeMemoryFlags.ReadOnly|ComputeMemoryFlags.CopyHostPointer,_matrix));
kernel.SetValueArgument(3, Radius);
kernel.SetValueArgument(4, (int)images.Width);
Console.WriteLine($"运行平台: {platform.Name}\n运行设备: {device.Name}\n");
Stopwatch sw = Stopwatch.StartNew();
var climg = images;

//执行代码
commands.Execute(kernel, null, new long[] {climg.Width, climg.Height}, null, null);

//read data
char[] resultArray = new char[dstBytes.Length];
var arrHandle = GCHandle.Alloc(resultArray, GCHandleType.Pinned);
commands.Read(resultBuffer, true, 0, dstBytes.Length, arrHandle.AddrOfPinnedObject(), null);
//commands.ReadFromImage(images.Item2, processeddata.Scan0, true, null);

var resultHandle = GCHandle.Alloc(resultArray, GCHandleType.Pinned);
var bmp=new Bitmap(climg.Width,climg.Height, climg.Width*4, PixelFormat.Format32bppArgb, resultHandle.AddrOfPinnedObject());
var elapsed = sw.Elapsed;
Console.WriteLine($"耗时: {elapsed.TotalMilliseconds} ms\n");
kernel.Dispose();

bmp.Save("processed_cl.bmp");
}

相同尺寸的图片处理,使用 Intel Core i7-4770 自带的核显 HD4600 处理,耗时只需要164毫秒。

以下是相关测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
运行平台: Intel(R) OpenCL
运行设备: Intel(R) HD Graphics 4600
处理图片尺寸:790*501
OpenCL处理耗时: 13.6597 ms

处理图片尺寸:790*501
常规方法耗时: 11482.9402 ms

运行平台: Intel(R) OpenCL
运行设备: Intel(R) HD Graphics 4600
处理图片尺寸:1339*693
OpenCL处理耗时: 33.0095 ms

处理图片尺寸:1339*693
常规方法耗时: 26908.9926 ms

运行平台: Intel(R) OpenCL
运行设备: Intel(R) HD Graphics 4600
处理图片尺寸:1920*1080
OpenCL处理耗时: 51.3885 ms

处理图片尺寸:1920*1080
常规方法耗时: 60147.3815 ms

当然,常规方法都只使用了单线程,还未发挥多核CPU的威力,然而,可以预见的是,即使是使用多线程,提升也是有限的。

原图:

高斯模糊:

代码: https://github.com/gandalfliang/cloo_netstandard/tree/temp

Update: 在nVidia的环境下会导致处理后的图片出现花屏现象,估计是cl代码的问题,又或者是nVidia的驱动有问题?下次再更新
评论

.NET Standard CLOO

鉴于 .NET Standard 2.0 已经支持大量的.NET api,移植CLOO已经是毫无难度的一件事情 Github

CLOO使用p/invoke方式调用opencl api,但是对于不同平台下,opencl 的名称并不一致,例如在linux下为libOpenCL.so,Windows下为OpenCL.dll,且 .NET Standard 没有提供 Mono 类似的 dllmap 模式,因此,现在来说还不能达到用一个package,在所有平台引用的目的。

评论

Gitsoler

Gitsoler - a Visual Studio extension, now goes public: https://marketplace.visualstudio.com/items?itemName=gandalfliang.gitsoler

评论

“阻断疗法” - 拯救 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替换方案完全失败的情形。

查看更多

评论