在传统的写法里面,都是面向于 DXGI 交换链实现界面渲染。在 DirectComposition 里面可以通过 IDCompositionDevice 的 Commit 和 WaitForCommitCompletion 方法配置将窗口内容提交到 DWM(DWM Desktop Window Manager)进行渲染,整个过程无需交换链参与
在 DirectComposition 里面提供了 Commit 机制,一次 Commit 的所有内容都能在相同的一帧在屏幕显示出来,如此可以非常方便地完成渲染对齐任务
通过 WaitForCommitCompletion 方法可以等待 Commit 内容完成渲染,此方法作用相当于等待交换链写法的等待垂直同步实现
在 上一篇博客 中,采用了传统的 DXGI 交换链与 DirectComposition 对接
在本文这里将去掉交换链,可以很大简化对接渲染的逻辑。采用 DXGI 交换链对接的方式,可以比较方便对接原有的程序,且可以实现更高帧率的控制。而采用 DirectComposition 的 Commit 写法,可以更好利用 DirectComposition 机制,实现多表面合成以及更加实时的合成器动画
本文将给出最简实现对接的代码逻辑,其步骤如下
- 创建 Win32 窗口
- 创建 DirectComposition 设备和关联窗口,获取渲染表面
- 执行渲染逻辑
为了保持本文简洁,我将不在正文部分贴出非关键部分的代码,在本文末尾给出全部核心代码。本文的全部核心代码部分不到 200 行,适合一口气完成。本文也使用了到了一些库,为了防止大家不知道项目如何配置的,在本文末尾也给出整个项目全部代码和配置的下载方法
基础库
按照 .NET 的惯例,开始之前先安装基础库,安装之后的 csproj 项目文件代码大概如下
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<IsAotCompatible>true</IsAotCompatible>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Vortice.Direct2D1" Version="3.8.2" />
<PackageReference Include="Vortice.Direct3D11" Version="3.8.2" />
<PackageReference Include="Vortice.DirectComposition" Version="3.8.2" />
<PackageReference Include="Vortice.DXGI" Version="3.8.2" />
<PackageReference Include="Vortice.Win32" Version="2.3.0" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.257">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="MicroCom.Runtime" Version="0.11.0" />
</ItemGroup>
</Project>
创建 Win32 窗口
创建 Win32 窗口仅仅只是想拿到窗口句柄,不是本文重点,这里就忽略 CreateWindow 方法的实现
// 创建窗口
HWND window = CreateWindow();
// 显示窗口
ShowWindow(window, SHOW_WINDOW_CMD.SW_NORMAL);
以上代码的 ShowWindow 是标准的 Win32 方法,由 CsWin32 库生成。定义如下
[DllImport("USER32.dll", ExactSpelling = true),DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[SupportedOSPlatform("windows5.0")]
internal static extern winmdroot.Foundation.BOOL ShowWindow(winmdroot.Foundation.HWND hWnd, winmdroot.UI.WindowsAndMessaging.SHOW_WINDOW_CMD nCmdShow);
为了直接使用方法,在本文这里直接在命名空间引用静态类,代码如下
using static Windows.Win32.PInvoke;
创建 DirectComposition 设备
先使用以下快速地代码创建 ID3D11Device 设备。正常来说,还是会尝试遍历获取显示适配器用来手动创建设备。本文这里使用的是比较不稳妥的简化写法。正确且常用的写法代码稍多,在本文这里 ID3D11Device 不是主角。如对此感兴趣,请参阅 渲染博客导航
var result = D3D11.D3D11CreateDevice(null, DriverType.Hardware, DeviceCreationFlags.BgraSupport,
featureLevels: [], out ID3D11Device iD3D11Device, out var feature,
out ID3D11DeviceContext iD3D11DeviceContext);
result.CheckError();
_ = feature;
iD3D11DeviceContext.Dispose(); // 用不着就先释放。释放不代表立刻回收资源,只是表示业务层不需要用到,减少引用计数
在本文这里只用到了 ID3D11Device 设备,于是就选择立刻释放 ID3D11DeviceContext 的引用
拿到 ID3D11Device 设备,就可以调用 DComp.DCompositionCreateDevice3 创建 IDCompositionDevice 设备,代码如下
IDCompositionDevice compositionDevice = DComp.DCompositionCreateDevice3<IDCompositionDevice>(iD3D11Device);
这里的 IDCompositionDevice 应该就是 IDirectCompositionDevice 的缩写
对接窗口
通过 CreateTargetForHwnd 方法可以将 DirectComposition 关联到窗口上,代码如下
IDCompositionDevice compositionDevice = DComp.DCompositionCreateDevice3<IDCompositionDevice>(iD3D11Device);
compositionDevice.CreateTargetForHwnd(window, topmost: true, out IDCompositionTarget compositionTarget);
这里的 CreateTargetForHwnd 的第二个参数比较迷惑,以上的 CreateTargetForHwnd 参数 topmost 不是值窗口置顶,而是:如果视觉树应显示在由 hwnd 参数指定的窗口子元素之上,则为 TRUE;否则,视觉树将显示在子元素之后。原文:
TRUE if the visual tree should be displayed on top of the children of the window specified by the hwnd parameter; otherwise, the visual tree is displayed behind the children.
创建视觉对象
创建视觉对象之前,需要获取当前窗口的尺寸,代码如下
RECT windowRect;
PInvoke.GetClientRect(window, &windowRect);
var clientSize = new SizeI(windowRect.right - windowRect.left, windowRect.bottom - windowRect.top);
创建视觉对象只需简单地调用 IDCompositionDevice.CreateVisual 方法,然而此时视觉对象还没有内容,可通过 IDCompositionDevice.CreateVirtualSurface 创建表面来作为内容,简单写法如下
IDCompositionVisual compositionVisual = compositionDevice.CreateVisual();
IDCompositionVirtualSurface surface = compositionDevice.CreateVirtualSurface((uint) clientSize.Width,
(uint) clientSize.Height, Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied);
compositionVisual.SetContent(surface);
创建表面时,可用 CreateVirtualSurface 或 CreateSurface 方法。两者不同的是 CreateVirtualSurface 创建的是稀疏表面,而 CreateSurface 是大数组(矩阵),在调用 BeginDraw 之前 IDCompositionVirtualSurface 不会分别空间,且 IDCompositionVirtualSurface 还能 Resize 重新设置大小。而 CreateSurface 则就不能。使用 CreateSurface 的例子如下
var createSurfaceResult = compositionDevice.CreateSurface((uint) clientSize.Width,
(uint) clientSize.Height, Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied, out IDCompositionSurface? dCompositionSurface);
createSurfaceResult.CheckError();
创建表面需要传染颜色格式和对 AlphaMode 的处理,本文这里传入的是 Premultiplied 采用预乘方法。对 Premultiplied 简单来说就是最终输出的值里的 RGB 分量都乘以透明度。更多细节请参阅 支持的像素格式和 Alpha 模式 - Win32 apps - Microsoft Learn
传入 Premultiplied 预乘时,会更多占用 DWM 的资源,在 4K 的全屏窗口上,对比 Premultiplied 预乘与忽略 AlphaMode 的性能,可以看到预乘会比忽略占用多非常多的 DWM 资源。如果自己的应用是无需窗口背景透明的,还请设置为忽略 AlphaMode 模式
配置视觉对象
完成视觉对象创建之后,可将此视觉对象设置为窗口的根内容。在 DirectComposition 里可以设置视觉树,一个视觉对象上可以添加很多个视觉对象,但只有其中一个可以成为 IDCompositionTarget 的 Root 视觉对象
compositionTarget.SetRoot(compositionVisual);
compositionDevice.Commit(); // 非必须
对接 D2D 渲染
以上代码就完成了视觉对象的创建和在窗口上显示的基础逻辑。为了能够绘制漂亮的界面,在本文这里将和 D2D 进行对接
为了能够和 D2D 进行对接,需要给 D2D 一个绘制表面。从 IDCompositionVirtualSurface 或 IDCompositionSurface 表面调用 BeginDraw 方法,即可获取到 IDXGISurface 表面,从而让 D2D 在此表面上绘制,基础逻辑如下
IDXGISurface dxgiSurface = surface.BeginDraw<IDXGISurface>(null, out var updateOffset);
再按照 D2D 的初始化方法,将 D2D 的 ID2D1RenderTarget 创建出来,代码如下
// 工厂创建只需一次
Vortice.Direct2D1.ID2D1Factory1 d2DFactory = Vortice.Direct2D1.D2D1.D2D1CreateFactory<Vortice.Direct2D1.ID2D1Factory1>();
// 以下循环为每一帧执行
while (!_isMainWindowClosed)
{
using IDXGISurface dxgiSurface = surface.BeginDraw<IDXGISurface>(null, out var updateOffset);
var renderTargetProperties = new Vortice.Direct2D1.RenderTargetProperties()
{
PixelFormat = new PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied),
Type = Vortice.Direct2D1.RenderTargetType.Hardware,
};
using Vortice.Direct2D1.ID2D1RenderTarget d2D1RenderTarget =
d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);
Vortice.Direct2D1.ID2D1RenderTarget renderTarget = d2D1RenderTarget;
... // 忽略其他代码
}
拿到 ID2D1RenderTarget 即可进行 D2D 的绘制,在本文这里使用了名为 D2DRenderDemo 的辅助类进行绘制,具体代码可以在后文找到
// 以下循环为每一帧执行
while (!_isMainWindowClosed)
{
using IDXGISurface dxgiSurface = surface.BeginDraw<IDXGISurface>(null, out var updateOffset);
var renderTargetProperties = new Vortice.Direct2D1.RenderTargetProperties()
{
PixelFormat = new PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied),
Type = Vortice.Direct2D1.RenderTargetType.Hardware,
};
using Vortice.Direct2D1.ID2D1RenderTarget d2D1RenderTarget =
d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);
Vortice.Direct2D1.ID2D1RenderTarget renderTarget = d2D1RenderTarget;
renderTarget.BeginDraw();
// 在这里编写绘制逻辑
renderTarget.Clear(new Color4(0f));
d2DRenderDemo.Draw(renderTarget, clientSize);
renderTarget.EndDraw();
... // 忽略其他代码
}
当 D2D 绘制完成之后,需要调用 IDCompositionVirtualSurface 或 IDCompositionSurface 的 EndDraw 方法。再调用 IDCompositionDevice 的 Commit 方法将内容提交出去。当所有的窗口都完成绘制和 Commit 之后,调用 IDCompositionDevice.WaitForCommitCompletion 等待 DWM 消费。调用 IDCompositionDevice.WaitForCommitCompletion 约等于等待垂直同步,等待界面刷新
renderTarget.BeginDraw();
// 在这里编写绘制逻辑
renderTarget.Clear(new Color4(0f));
d2DRenderDemo.Draw(renderTarget, clientSize);
renderTarget.EndDraw();
surface.EndDraw();
compositionDevice.Commit();
compositionDevice.WaitForCommitCompletion();
在本文这里,由于只有一个窗口,于是在 Commit 之后即可立刻调用 WaitForCommitCompletion 方法了
整个渲染代码,即每一帧跑的代码如下
Vortice.Direct2D1.ID2D1Factory1 d2DFactory = Vortice.Direct2D1.D2D1.D2D1CreateFactory<Vortice.Direct2D1.ID2D1Factory1>();
var d2DRenderDemo = new D2DRenderDemo();
while (!_isMainWindowClosed)
{
using IDXGISurface dxgiSurface = surface.BeginDraw<IDXGISurface>(null, out var updateOffset);
_ = updateOffset;
var renderTargetProperties = new Vortice.Direct2D1.RenderTargetProperties()
{
PixelFormat = new PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied),
Type = Vortice.Direct2D1.RenderTargetType.Hardware,
};
using Vortice.Direct2D1.ID2D1RenderTarget d2D1RenderTarget =
d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);
Vortice.Direct2D1.ID2D1RenderTarget renderTarget = d2D1RenderTarget;
renderTarget.BeginDraw();
// 在这里编写绘制逻辑
renderTarget.Clear(new Color4(0f));
d2DRenderDemo.Draw(renderTarget, clientSize);
renderTarget.EndDraw();
surface.EndDraw();
compositionDevice.Commit();
compositionDevice.WaitForCommitCompletion();
while (true)
{
var success = PInvoke.PeekMessage(out var message, window, 0, 0, PEEK_MESSAGE_REMOVE_TYPE.PM_REMOVE);
if (!success)
{
break;
}
PInvoke.TranslateMessage(&message);
PInvoke.DispatchMessage(&message);
}
}
以上代码也跑了 PeekMessage 方法防止窗口未响应
在每次进入绘制的时候调用 renderTarget.Clear(new Color4(0f)); 可以解决双缓存带来的闪烁问题,即界面内容被分别不同步地绘制到两个纹理表面上。通过 Clear 确保每次都是重新绘制,解决此问题
什么时候可以不做清理而进行绘制?进行某些性能优化的时候,且此时应该确保绘制同步。或再开一个表面纹理,通过同步的方式再将最终界面绘制
核心代码
核心的代码被我放在一个文件里面,代码如下
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Vortice.DCommon;
using Vortice.Direct3D;
using Vortice.Direct3D11;
using Vortice.DirectComposition;
using Vortice.DXGI;
using Vortice.Mathematics;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.WindowsAndMessaging;
using AlphaMode = Vortice.DXGI.AlphaMode;
using D2D = Vortice.Direct2D1;
namespace DijallnemrecerkuCheberewhibair;
[SupportedOSPlatform("windows6.1")]
class DirectCompositionDemo
{
public unsafe void Run()
{
var window = CreateWindow();
PInvoke.ShowWindow(window, SHOW_WINDOW_CMD.SW_MAXIMIZE);
var result = D3D11.D3D11CreateDevice(null, DriverType.Hardware, DeviceCreationFlags.BgraSupport,
featureLevels: [], out ID3D11Device iD3D11Device, out var feature,
out ID3D11DeviceContext iD3D11DeviceContext);
result.CheckError();
_ = feature;
iD3D11DeviceContext.Dispose(); // 用不着就先释放。释放不代表立刻回收资源,只是表示业务层不需要用到,减少引用计数
IDCompositionDevice compositionDevice = DComp.DCompositionCreateDevice3<IDCompositionDevice>(iD3D11Device);
compositionDevice.CreateTargetForHwnd(window, topmost: true, out IDCompositionTarget compositionTarget);
// 以上的 CreateTargetForHwnd 参数 topmost 不是值窗口置顶,而是:如果视觉树应显示在由 hwnd 参数指定的窗口子元素之上,则为 TRUE;否则,视觉树将显示在子元素之后
// > TRUE if the visual tree should be displayed on top of the children of the window specified by the hwnd parameter; otherwise, the visual tree is displayed behind the children.
// https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createtargetforhwnd
// 创建视觉对象
RECT windowRect;
PInvoke.GetClientRect(window, &windowRect);
var clientSize = new SizeI(windowRect.right - windowRect.left, windowRect.bottom - windowRect.top);
IDCompositionVisual compositionVisual = compositionDevice.CreateVisual();
IDCompositionVirtualSurface surface = compositionDevice.CreateVirtualSurface((uint) clientSize.Width,
(uint) clientSize.Height, Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied);
// 创建 IDCompositionSurface 有两个方法,分别是 CreateVirtualSurface 和 CreateSurface 方法。两者不同的是 CreateVirtualSurface 创建的是稀疏表面,而 CreateSurface 是大数组(矩阵),在调用 BeginDraw 之前 IDCompositionVirtualSurface 不会分别空间,且 IDCompositionVirtualSurface 还能 Resize 重新设置大小。而 CreateSurface 则就不能
//var createSurfaceResult = compositionDevice.CreateSurface((uint) clientSize.Width,
// (uint) clientSize.Height, Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied, out IDCompositionSurface? dCompositionSurface);
//createSurfaceResult.CheckError();
//surface = dCompositionSurface;
compositionVisual.SetContent(surface);
compositionTarget.SetRoot(compositionVisual);
compositionDevice.Commit();
Vortice.Direct2D1.ID2D1Factory1 d2DFactory = Vortice.Direct2D1.D2D1.D2D1CreateFactory<Vortice.Direct2D1.ID2D1Factory1>();
var d2DRenderDemo = new D2DRenderDemo();
while (!_isMainWindowClosed)
{
using IDXGISurface dxgiSurface = surface.BeginDraw<IDXGISurface>(null, out var updateOffset);
_ = updateOffset;
var renderTargetProperties = new Vortice.Direct2D1.RenderTargetProperties()
{
PixelFormat = new PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied),
Type = Vortice.Direct2D1.RenderTargetType.Hardware,
};
using Vortice.Direct2D1.ID2D1RenderTarget d2D1RenderTarget =
d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);
Vortice.Direct2D1.ID2D1RenderTarget renderTarget = d2D1RenderTarget;
renderTarget.BeginDraw();
// 在这里编写绘制逻辑
renderTarget.Clear(new Color4(0f));
d2DRenderDemo.Draw(renderTarget, clientSize);
renderTarget.EndDraw();
surface.EndDraw();
compositionDevice.Commit();
compositionDevice.WaitForCommitCompletion();
while (true)
{
var success = PInvoke.PeekMessage(out var message, window, 0, 0, PEEK_MESSAGE_REMOVE_TYPE.PM_REMOVE);
if (!success)
{
break;
}
PInvoke.TranslateMessage(&message);
PInvoke.DispatchMessage(&message);
}
}
}
private bool _isMainWindowClosed;
private unsafe HWND CreateWindow()
{
PInvoke.DwmIsCompositionEnabled(out var compositionEnabled);
if (!compositionEnabled)
{
Console.WriteLine($"无法启用透明窗口效果");
}
// [Windows 窗口样式 什么是 WS_EX_NOREDIRECTIONBITMAP 样式](https://blog.lindexi.com/post/Windows-%E7%AA%97%E5%8F%A3%E6%A0%B7%E5%BC%8F-%E4%BB%80%E4%B9%88%E6%98%AF-WS_EX_NOREDIRECTIONBITMAP-%E6%A0%B7%E5%BC%8F.html )
WINDOW_EX_STYLE exStyle = WINDOW_EX_STYLE.WS_EX_NOREDIRECTIONBITMAP;
var style = WNDCLASS_STYLES.CS_OWNDC | WNDCLASS_STYLES.CS_HREDRAW | WNDCLASS_STYLES.CS_VREDRAW;
var defaultCursor = PInvoke.LoadCursor(
new HINSTANCE(IntPtr.Zero), new PCWSTR(PInvoke.IDC_ARROW.Value));
var className = $"lindexi-{Guid.NewGuid().ToString()}";
var title = "The Title";
_wndProcDelegate = new WNDPROC(WndProc); // 仅用于防止 GC 回收。详细请看 https://github.com/lindexi/lindexi_gd/pull/85
fixed (char* pClassName = className)
fixed (char* pTitle = title)
{
var wndClassEx = new WNDCLASSEXW
{
cbSize = (uint) Marshal.SizeOf<WNDCLASSEXW>(),
style = style,
lpfnWndProc = _wndProcDelegate,
hInstance = new HINSTANCE(PInvoke.GetModuleHandle(null).DangerousGetHandle()),
hCursor = defaultCursor,
hbrBackground = new HBRUSH(IntPtr.Zero),
lpszClassName = new PCWSTR(pClassName)
};
ushort atom = PInvoke.RegisterClassEx(in wndClassEx);
var dwStyle = WINDOW_STYLE.WS_OVERLAPPEDWINDOW | WINDOW_STYLE.WS_VISIBLE;
var windowHwnd = PInvoke.CreateWindowEx(
exStyle,
new PCWSTR((char*) atom),
new PCWSTR(pTitle),
dwStyle,
0, 0, 1900, 1000,
HWND.Null, HMENU.Null, HINSTANCE.Null, null);
return windowHwnd;
}
}
private WNDPROC? _wndProcDelegate;
private LRESULT WndProc(HWND hwnd, uint message, WPARAM wParam, LPARAM lParam)
{
if (message == PInvoke.WM_CLOSE)
{
_isMainWindowClosed = true;
}
return PInvoke.DefWindowProc(hwnd, message, wParam, lParam);
}
}
class D2DRenderDemo
{
// 此为调试代码,绘制一些矩形条
private List<D2DRenderInfo>? _renderList;
public void OnReSize()
{
_renderList = null;
}
public void Draw(D2D.ID2D1RenderTarget renderTarget, SizeI clientSize)
{
var rectWeight = 10;
var rectHeight = 20;
var margin = 5;
if (_renderList is null)
{
_renderList = new List<D2DRenderInfo>();
for (int top = margin; top < clientSize.Height - rectHeight - margin; top += rectHeight + margin)
{
Rect rect = new Rect(margin, top, rectWeight, rectHeight);
var color = new Color4(Random.Shared.NextSingle(), Random.Shared.NextSingle(),
Random.Shared.NextSingle());
var step = Random.Shared.Next(1, 20);
var renderInfo = new D2DRenderInfo(rect, step, color);
_renderList.Add(renderInfo);
}
}
for (var i = 0; i < _renderList.Count; i++)
{
var renderInfo = _renderList[i];
using var brush = renderTarget.CreateSolidColorBrush(renderInfo.Color);
renderTarget.FillRectangle(renderInfo.Rect, brush);
var nextRect = renderInfo.Rect with
{
Width = renderInfo.Rect.Width + renderInfo.Step
};
if (nextRect.Width > clientSize.Width - margin * 2)
{
nextRect = nextRect with
{
Width = rectWeight
};
}
_renderList[i] = renderInfo with
{
Rect = nextRect
};
}
}
private readonly record struct D2DRenderInfo(Rect Rect, int Step, Color4 Color);
}
全部代码
本文代码放在 github 和 gitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快
先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin daef15201848fb5c338f519d49f4879017590124
以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin daef15201848fb5c338f519d49f4879017590124
获取代码之后,进入 DirectX/DirectComposition/DijallnemrecerkuCheberewhibair 文件夹,即可获取到源代码
更多博客
渲染部分,关于 SharpDx 和 Vortice 的使用方法,包括入门级教程,请参阅:
更多关于我博客请参阅 博客导航
本文会经常更新,请阅读原文: https://blog.lindexi.com/post/dotnet-Vortice-%E6%97%A0%E9%9C%80%E4%BA%A4%E6%8D%A2%E9%93%BE%E4%B8%8E-DirectComposition-%E5%AF%B9%E6%8E%A5%E6%B8%B2%E6%9F%93%E5%B1%82.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航
本作品采用
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接:
https://blog.lindexi.com
),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请
与我联系
。
无盈利,不卖课,做纯粹的技术博客
以下是广告时间
推荐关注 Edi.Wang 的公众号
欢迎进入 Eleven 老师组建的 .NET 社区
以上广告全是友情推广,无盈利