本文是渲染相關係列博客中的一篇,該系列博客已按照邏輯順序編排,方便大家依次閲讀。如您對渲染相關感興趣,可以通過以下鏈接訪問整個系列:渲染相關係列博客導航
在 DirectX 使用 Vortice 從零開始控制枱創建 Direct2D1 窗口修改顏色 博客中和大家介紹了最簡方式創建了窗口和對接了 DirectX 層。在此基礎上,大家也能看到此時創建的窗口是無法應用透明背景效果的
即使強行設置 SwapChainDescription1.AlphaMode 為 AlphaMode.Premultiplied 也會在 IDXGIFactory2.CreateSwapChainForHwnd 報錯
傳統 Win32 應用可以通過 UpdateLayeredWindow 方法設置窗口透明,然而 UpdateLayeredWindow 是有比較大的性能代價的,詳細請參閲 WPF 從最底層源代碼瞭解 AllowsTransparency 性能差的原因
性能較好的透明窗口實現可參閲 WPF 製作支持點擊穿透的高性能的透明背景異形窗口
以上是在 WPF 框架裏面幫忙封裝好的,現在咱只有純控制枱,需要自己手動幹一些活
為了方便大家閲讀,本文將重新從零控制枱開始,先創建好 WS_EX_LAYERED 的窗口,再將 DirectX 對接上去。總代碼控制在 500 行左右。額外,為了方便 Win32 方法調用,本文還請出了 CsWin32 庫,詳細使用方法請參閲
dotnet 使用 CsWin32 庫簡化 Win32 函數調用邏輯
準備工作
按照 .NET 慣例,先安裝一些庫。本文的 D2D 基本沒有戲份,僅用於繪製一點用於輔助測試的內容,本身此技術就和 D2D 無關
<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>
</ItemGroup>
安裝之後的 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>
</ItemGroup>
</Project>
如以上代碼所示,本文提供的代碼也是 AOT 友好的。在我的測試中,構建的 32 位的程序只需 2.12 MB 的體積。如果不知道本文的項目是如何組織的,可以在本文末尾找到本文全部代碼的下載方法,拉取代碼瞭解更多細節
添加 NativeMethods.txt 文件,添加以下內容,讓 CsWin32 輔助生成一些代碼
EnumDisplayMonitors
GetMonitorInfo
MONITORINFOEXW
EnumDisplaySettings
GetDisplayConfigBufferSizes
QueryDisplayConfig
DisplayConfigGetDeviceInfo
DISPLAYCONFIG_SOURCE_DEVICE_NAME
DISPLAYCONFIG_TARGET_DEVICE_NAME
RegisterClassEx
GetModuleHandle
LoadCursor
IDC_ARROW
WndProc
CreateWindowEx
CW_USEDEFAULT
ShowWindow
SW_SHOW
GetMessage
TranslateMessage
DispatchMessage
DefWindowProc
GetClientRect
WM
WM_PAINT
GetWindowLong
SetWindowLong
DwmIsCompositionEnabled
UpdateLayeredWindow
DwmExtendFrameIntoClientArea
DCompositionCreateDevice
以上提供的列表是超過本文所用範圍的,多了也沒有什麼關係,一來這是測試項目,二來發布的時候 AOT 帶裁剪
創建窗口
創建窗口的步驟和 上一篇博客 提供的方法十分接近,只是需要配置 WS_EX_LAYERED 樣式,核心代碼如下
WINDOW_EX_STYLE exStyle = WINDOW_EX_STYLE.WS_EX_OVERLAPPEDWINDOW
| WINDOW_EX_STYLE.WS_EX_LAYERED; // Layered 是透明窗口的最關鍵
var style = WNDCLASS_STYLES.CS_OWNDC | WNDCLASS_STYLES.CS_HREDRAW | WNDCLASS_STYLES.CS_VREDRAW;
var defaultCursor = LoadCursor(
new HINSTANCE(IntPtr.Zero), new PCWSTR(IDC_ARROW.Value));
var className = $"lindexi-{Guid.NewGuid().ToString()}";
var title = "The Title";
fixed (char* pClassName = className)
fixed (char* pTitle = title)
{
var wndClassEx = new WNDCLASSEXW
{
cbSize = (uint) Marshal.SizeOf<WNDCLASSEXW>(),
style = style,
lpfnWndProc = new WNDPROC(WndProc),
hInstance = new HINSTANCE(GetModuleHandle(null).DangerousGetHandle()),
hCursor = defaultCursor,
hbrBackground = new HBRUSH(IntPtr.Zero),
lpszClassName = new PCWSTR(pClassName)
};
ushort atom = RegisterClassEx(in wndClassEx);
var dwStyle = WINDOW_STYLE.WS_OVERLAPPEDWINDOW;
var windowHwnd = CreateWindowEx(
exStyle,
new PCWSTR((char*) atom),
new PCWSTR(pTitle),
dwStyle,
0, 0, 1900, 1000,
HWND.Null, HMENU.Null, HINSTANCE.Null, null);
return windowHwnd;
}
以上代碼放在 CreateWindow 方法中。在開始之前,也先調用 DwmIsCompositionEnabled 方法,判斷是否可用
DwmIsCompositionEnabled(out var compositionEnabled);
if (!compositionEnabled)
{
Console.WriteLine($"無法啓用透明窗口效果");
}
預期在 Win10 以上系統都是能使用的,除非系統被魔改
窗口的消息處理代碼 WndProc 先不着急寫,等待完成渲染部分的邏輯再一起寫
完成窗口創建之後,即可將窗口顯示出來,代碼如下
var window = CreateWindow();
ShowWindow(window, SHOW_WINDOW_CMD.SW_NORMAL);
隨後先開啓獨立的線程作為渲染線程,再跑起來消息循環
渲染線程相關邏輯,我封裝到 RenderManager 類型裏面,其代碼如下
var renderManager = new RenderManager(window);
renderManager.StartRenderThread();
跑起來渲染線程之後,使用標準的消息循環跑起來應用
while (true)
{
var msg = new MSG();
var getMessageResult = GetMessage(&msg, HWND, 0,
0);
if (!getMessageResult)
{
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
在 RenderManager 裏面也提供窗口尺寸變更的方法,可以在消息循環中調用。此時的消息循環的核心代碼如下
private LRESULT WndProc(HWND hwnd, uint message, WPARAM wParam, LPARAM lParam)
{
switch ((WindowsMessage)message)
{
case WindowsMessage.WM_NCCALCSIZE:
{
return new LRESULT(0);
}
case WindowsMessage.WM_SIZE:
{
RenderManager?.ReSize();
break;
}
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
以上的 WM_NCCALCSIZE 用於聲明客户區,通過直接返回 0 告訴系統整個區域都是客户區
透明窗口的實現在窗口創建過程中,最關鍵點只有 WS_EX_LAYERED 和 WM_NCCALCSIZE 的邏輯
渲染線程
獨立的渲染線程也是 WPF 等 UI 框架所採用的方式,只需要新建一個線程跑起來就可以了,如果有心的話,再設置線程為 STA 的就更好,代碼如下
unsafe class RenderManager(HWND hwnd)
{
public HWND HWND => hwnd;
private readonly Format _colorFormat = Format.B8G8R8A8_UNorm;
public void StartRenderThread()
{
var thread = new Thread(() => { RenderCore(); })
{
IsBackground = true,
Name = "Render"
};
thread.Priority = ThreadPriority.Highest;
thread.Start();
}
}
以上的 RenderCore 就是核心的渲染方法了
由於渲染線程是獨立的,不能在 ReSize 方法直接修改渲染線程相關的邏輯。本文這裏簡單使用一個字段表示窗口尺寸變更,需要渲染線程修改交換鏈尺寸
unsafe class RenderManager(HWND hwnd)
{
public void ReSize()
{
_isReSize = true;
}
private bool _isReSize;
}
在 RenderCore 的一開始就是執行初始化邏輯,初始化為本文的關鍵,核心就是對接 DirectComposition 實現透明窗口效果
初始化渲染
先獲取客户區,即窗口尺寸,此尺寸用於後續交換鏈的創建
RECT windowRect;
GetClientRect(HWND, &windowRect);
var clientSize = new SizeI(windowRect.right - windowRect.left, windowRect.bottom - windowRect.top);
按照 上一篇博客 提供的方法獲取顯卡信息,代碼如下
var dxgiFactory2 = DXGI.CreateDXGIFactory1<IDXGIFactory2>();
IDXGIAdapter1? hardwareAdapter = GetHardwareAdapter(dxgiFactory2)
// 這裏 ToList 只是想列出所有的 IDXGIAdapter1 在實際代碼裏,大部分都是獲取第一個
.ToList().FirstOrDefault();
if (hardwareAdapter == null)
{
throw new InvalidOperationException("Cannot detect D3D11 adapter");
}
private static IEnumerable<IDXGIAdapter1> GetHardwareAdapter(IDXGIFactory2 factory)
{
using IDXGIFactory6? factory6 = factory.QueryInterfaceOrNull<IDXGIFactory6>();
if (factory6 != null)
{
// 這個系統的 DX 支持 IDXGIFactory6 類型
// 先告訴系統,要高性能的顯卡
for (uint adapterIndex = 0;
factory6.EnumAdapterByGpuPreference(adapterIndex, GpuPreference.HighPerformance,
out IDXGIAdapter1? adapter).Success;
adapterIndex++)
{
if (adapter == null)
{
continue;
}
AdapterDescription1 desc = adapter.Description1;
if ((desc.Flags & AdapterFlags.Software) != AdapterFlags.None)
{
// Don't select the Basic Render Driver adapter.
adapter.Dispose();
continue;
}
Console.WriteLine($"枚舉到 {adapter.Description1.Description} 顯卡");
yield return adapter;
}
}
else
{
// 不支持就不支持咯,用舊版本的方式獲取顯示適配器接口
}
// 如果枚舉不到,那系統返回啥都可以
for (uint adapterIndex = 0;
factory.EnumAdapters1(adapterIndex, out IDXGIAdapter1? adapter).Success;
adapterIndex++)
{
AdapterDescription1 desc = adapter.Description1;
if ((desc.Flags & AdapterFlags.Software) != AdapterFlags.None)
{
// Don't select the Basic Render Driver adapter.
adapter.Dispose();
continue;
}
Console.WriteLine($"枚舉到 {adapter.Description1.Description} 顯卡");
yield return adapter;
}
}
嘗試創建 ID3D11Device 設備,代碼如下
FeatureLevel[] featureLevels = new[]
{
FeatureLevel.Level_12_2,
FeatureLevel.Level_12_1,
FeatureLevel.Level_12_0,
FeatureLevel.Level_11_1,
FeatureLevel.Level_11_0,
FeatureLevel.Level_10_1,
FeatureLevel.Level_10_0,
FeatureLevel.Level_9_3,
FeatureLevel.Level_9_2,
FeatureLevel.Level_9_1,
};
IDXGIAdapter1 adapter = hardwareAdapter;
DeviceCreationFlags creationFlags = DeviceCreationFlags.BgraSupport;
var result = D3D11.D3D11CreateDevice
(
adapter,
DriverType.Unknown,
creationFlags,
featureLevels,
out ID3D11Device d3D11Device, out FeatureLevel featureLevel,
out ID3D11DeviceContext d3D11DeviceContext
);
本文以上的 FeatureLevel[] 中添加了 Level_12_2 等的不合理需求,如果創建失敗了,則執行降級邏輯。按照技術原理,只需有 Level_11_1 即可
if (result.Failure)
{
// 降低等級試試
featureLevels = new[]
{
//FeatureLevel.Level_12_2,
//FeatureLevel.Level_12_1,
//FeatureLevel.Level_12_0,
FeatureLevel.Level_11_1,
FeatureLevel.Level_11_0,
FeatureLevel.Level_10_1,
FeatureLevel.Level_10_0,
FeatureLevel.Level_9_3,
FeatureLevel.Level_9_2,
FeatureLevel.Level_9_1,
};
result = D3D11.D3D11CreateDevice
(
adapter,
DriverType.Unknown,
creationFlags,
featureLevels,
out d3D11Device, out featureLevel,
out d3D11DeviceContext
);
}
將獲取到的 ID3D11Device 當成 ID3D11Device1 設備來用,這一步基本不會遇到出錯的,代碼如下
// 大部分情況下,用的是 ID3D11Device1 和 ID3D11DeviceContext1 類型
// 從 ID3D11Device 轉換為 ID3D11Device1 類型
ID3D11Device1 d3D11Device1 = d3D11Device.QueryInterface<ID3D11Device1>();
var d3D11DeviceContext1 = d3D11DeviceContext.QueryInterface<ID3D11DeviceContext1>();
// 獲取到了新的兩個接口,就可以減少 `d3D11Device` 和 `d3D11DeviceContext` 的引用計數。調用 Dispose 不會釋放掉剛才申請的 D3D 資源,只是減少引用計數
d3D11Device.Dispose();
d3D11DeviceContext.Dispose();
準備交換鏈參數,代碼如下
// 緩存的數量,包括前緩存。大部分應用來説,至少需要兩個緩存,這個玩過遊戲的夥伴都知道
const int FrameCount = 2;
SwapChainDescription1 swapChainDescription = new()
{
Width = (uint) clientSize.Width,
Height = (uint) clientSize.Height,
Format = _colorFormat,
BufferCount = FrameCount,
BufferUsage = Usage.RenderTargetOutput,
SampleDescription = SampleDescription.Default,
Scaling = Scaling.Stretch,
SwapEffect = SwapEffect.FlipSequential, // 使用 FlipSequential 配合 Composition
AlphaMode = AlphaMode.Premultiplied,
Flags = SwapChainFlags.None,
};
交換鏈中有以下參數必須固定為此搭配:
- Scaling: Scaling.Stretch
- SwapEffect: SwapEffect.FlipDiscard 或 SwapEffect.FlipSequential ,正常來説都會採用 FlipSequential 配合 Composition 使用
- AlphaMode: AlphaMode.Premultiplied 。使用
AlphaMode.Ignore和AlphaMode.Unspecified參數也是合法的,但是如此就丟失了窗口透明瞭,不是咱的需求。而AlphaMode.Straight參數則是不搭的
如果以上參數不搭配,則會在創建交換鏈時,返回 0x887A0001 錯誤
上文提到了 AlphaMode.Premultiplied 預乘,簡單來説就是最終輸出的值裏的 RGB 分量都乘以透明度。更多細節請參閲 支持的像素格式和 Alpha 模式 - Win32 apps - Microsoft Learn
判斷系統版本,決定能否使用 DirectComposition 功能,代碼如下
// 使用 DirectComposition 才能支持透明窗口
bool useDirectComposition = true;
// 使用 DirectComposition 時有系統版本要求
useDirectComposition = useDirectComposition & OperatingSystem.IsWindowsVersionAtLeast(8, 1);
如此可以讓代碼走兩個分支,使用 DirectComposition 的分支的代碼如下
IDXGISwapChain1 swapChain;
if (useDirectComposition)
{
// 使用 CreateSwapChainForComposition 創建支持預乘 Alpha 的 SwapChain
swapChain =
dxgiFactory2.CreateSwapChainForComposition(d3D11Device1, swapChainDescription);
// 創建 DirectComposition 設備和目標
IDXGIDevice dxgiDevice = d3D11Device1.QueryInterface<IDXGIDevice>();
IDCompositionDevice compositionDevice = DComp.DCompositionCreateDevice<IDCompositionDevice>(dxgiDevice);
compositionDevice.CreateTargetForHwnd(HWND, true, out IDCompositionTarget compositionTarget);
// 創建視覺對象並設置 SwapChain 作為內容
IDCompositionVisual compositionVisual = compositionDevice.CreateVisual();
compositionVisual.SetContent(swapChain);
compositionTarget.SetRoot(compositionVisual);
compositionDevice.Commit();
}
從上面代碼可見核心步驟是先讓 CreateSwapChainForComposition 創建出交換鏈對象。再將 ID3D11Device1 當成 IDXGIDevice 設備,用於調用 DComp.DCompositionCreateDevice 創建出 IDCompositionDevice 設備
調用 IDCompositionDevice 的 CreateTargetForHwnd 方法即可為當前的窗口掛上 IDCompositionTarget 對象。隨後再調用 IDCompositionDevice 設備的 CreateVisual 創建 IDCompositionVisual 視覺對象。將剛才創建出來的交換鏈作為視覺對象的內容,如此即可完成交換鏈與內容的綁定
// 創建視覺對象並設置 SwapChain 作為內容
IDCompositionVisual compositionVisual = compositionDevice.CreateVisual();
compositionVisual.SetContent(swapChain);
現在交換鏈所渲染的畫面已經能夠到 IDCompositionVisual 裏了,再將 IDCompositionVisual 作為 IDCompositionTarget 的根,即可讓 IDCompositionVisual 參與 DWM 合成
compositionTarget.SetRoot(compositionVisual);
compositionDevice.Commit();
如果沒有 DirectComposition 可用,則依然使用上一篇博客介紹的方法創建交換鏈,代碼如下
IDXGISwapChain1 swapChain;
if (useDirectComposition)
{
...
}
else
{
var fullscreenDescription = new SwapChainFullscreenDescription()
{
Windowed = true,
};
swapChainDescription.AlphaMode = AlphaMode.Ignore;
swapChain = dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, hwnd, swapChainDescription,
fullscreenDescription);
}
以上代碼的 DirectComposition 為本文的核心,只需要創建出輸出帶預乘的交換鏈,配合 WS_EX_LAYERED 窗口,即可渲染出透明窗口
接下來的邏輯就是和 D2D 對接,嘗試渲染透明的界面用於測試
對接渲染
由於 D2D 沒有什麼戲份,本文就只貼出核心代碼
using D2D.ID2D1Factory1 d2DFactory = D2D.D2D1.D2D1CreateFactory<D2D.ID2D1Factory1>();
var d3D11Texture2D = _renderContext.SwapChain.GetBuffer<ID3D11Texture2D>(0);
var dxgiSurface = d3D11Texture2D.QueryInterface<IDXGISurface>();
var renderTargetProperties = new D2D.RenderTargetProperties()
{
PixelFormat = new PixelFormat(D2DColorFormat, Vortice.DCommon.AlphaMode.Premultiplied),
Type = D2D.RenderTargetType.Hardware,
};
D2D.ID2D1RenderTarget d2D1RenderTarget =
d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);
while (!_isDisposed)
{
D2D.ID2D1RenderTarget renderTarget = d2D1RenderTarget;
renderTarget.BeginDraw();
var color = new Color4(Random.Shared.NextSingle(), Random.Shared.NextSingle(),
Random.Shared.NextSingle(), 0.1f);
renderTarget.Clear(color);
renderTarget.EndDraw();
_renderContext.SwapChain.Present(1, PresentFlags.None);
_renderContext.D3D11DeviceContext1.Flush();
}
如果準備處理窗口尺寸改變,則需要在循環裏面判斷 _isReSize 字段,調用交換鏈的 ResizeBuffers 方法,代碼如下
if (_isReSize)
{
// 處理窗口大小變化
_isReSize = false;
GetClientRect(HWND, out var pClientRect);
var clientSize = new SizeI(pClientRect.right - pClientRect.left, pClientRect.bottom - pClientRect.top);
var swapChain = _renderContext.SwapChain;
swapChain.ResizeBuffers(2,
(uint) (clientSize.Width),
(uint) (clientSize.Height),
_colorFormat,
SwapChainFlags.None
);
}
嘗試運行代碼,可見一個不斷閃爍的背景透明的窗口
代碼
本文代碼放在 github 和 gitee 上,可以使用如下命令行拉取代碼。我整個代碼倉庫比較龐大,使用以下命令行可以進行部分拉取,拉取速度比較快
先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行裏面輸入以下代碼,即可獲取到本文的代碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 369de6b65c4122cec6a6c9ffbcc0b352a419e83e
以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼。如果依然拉取不到代碼,可以發郵件向我要代碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 369de6b65c4122cec6a6c9ffbcc0b352a419e83e
獲取代碼之後,進入 DirectX/D2D/FarjairyakaBurnefuwache 文件夾,即可獲取到源代碼
更多技術博客,請參閲 博客導航
更多博客
渲染部分,關於 SharpDx 和 Vortice 的使用方法,包括入門級教程,請參閲:
- 渲染博客導航
- SharpDX 系列
更多關於我博客請參閲 博客導航