Skip to content

Commit e52db18

Browse files
committed
feat: add dpiAwareness docs and app manifest
1 parent 63dd733 commit e52db18

File tree

8 files changed

+173
-1
lines changed

8 files changed

+173
-1
lines changed

docs/dpi-awareness.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# DPI Awareness for DXGI Desktop Duplication
2+
3+
When using DXGI Desktop Duplication (`MonitorCaptureMethod.DesktopDuplication`) for monitor capture on Windows, your application must be configured as **per-monitor DPI aware**.
4+
5+
## The Problem
6+
7+
The DXGI `IDXGIOutput5::DuplicateOutput1` API requires the calling thread to be per-monitor DPI aware. Without this, you will encounter the error:
8+
9+
```
10+
IDXGIOutput5::DuplicateOutput1: The calling thread must be per-monitor DPI aware to use the output duplication APIs.
11+
```
12+
13+
## Solution
14+
15+
Add an application manifest to your project that declares per-monitor DPI awareness.
16+
17+
### Step 1: Create app.manifest
18+
19+
Create a file named `app.manifest` in your project root:
20+
21+
```xml
22+
<?xml version="1.0" encoding="utf-8"?>
23+
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
24+
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
25+
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
26+
<security>
27+
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
28+
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
29+
</requestedPrivileges>
30+
</security>
31+
</trustInfo>
32+
33+
<application xmlns="urn:schemas-microsoft-com:asm.v3">
34+
<windowsSettings>
35+
<!-- Per-monitor DPI awareness V2 - required for DXGI output duplication -->
36+
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
37+
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
38+
</windowsSettings>
39+
</application>
40+
41+
</assembly>
42+
```
43+
44+
### Step 2: Reference the manifest in your .csproj
45+
46+
Add the `ApplicationManifest` property to your project file:
47+
48+
```xml
49+
<PropertyGroup>
50+
<ApplicationManifest>app.manifest</ApplicationManifest>
51+
</PropertyGroup>
52+
```
53+
54+
## Alternative: Use Windows Graphics Capture
55+
56+
If you cannot configure DPI awareness (e.g., library constraints), use Windows Graphics Capture instead:
57+
58+
```csharp
59+
var source = MonitorCapture.FromPrimary()
60+
.SetCaptureMethod(MonitorCaptureMethod.WindowsGraphicsCapture);
61+
```
62+
63+
Windows Graphics Capture (WGC) is the recommended capture method for Windows 10 1903+ and does not have the DPI awareness requirement.
64+
65+
## More Information
66+
67+
- [Microsoft Docs: High DPI Desktop Application Development](https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows)
68+
- [Microsoft Docs: DPI Awareness Context](https://docs.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context)

samples/ObsKit.NET.Sample.Recording/ObsKit.NET.Sample.Recording.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<RootNamespace>ObsKit.NET.Sample.Recording</RootNamespace>
6+
<ApplicationManifest>app.manifest</ApplicationManifest>
67
</PropertyGroup>
78

89
<ItemGroup>

samples/ObsKit.NET.Sample.Recording/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
// Get monitor info for dimensions
4646
var primaryMonitor = MonitorCapture.AvailableMonitors.FirstOrDefault(m => m.IsPrimary)
4747
?? MonitorCapture.AvailableMonitors.First();
48-
using var monitorSource = MonitorCapture.FromMonitor(primaryMonitor);
48+
using var monitorSource = MonitorCapture.FromMonitor(primaryMonitor)
49+
.SetCaptureMethod(MonitorCaptureMethod.DesktopDuplication);
4950

5051
// You can also change video/audio settings after initialization using Obs.SetVideo/SetAudio.
5152
// These use the same configuration options as WithVideo/WithAudio above.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
3+
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
4+
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
5+
<security>
6+
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
7+
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
8+
</requestedPrivileges>
9+
</security>
10+
</trustInfo>
11+
12+
<application xmlns="urn:schemas-microsoft-com:asm.v3">
13+
<windowsSettings>
14+
<!-- Per-monitor DPI awareness V2 - required for DXGI output duplication -->
15+
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
16+
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
17+
</windowsSettings>
18+
</application>
19+
20+
</assembly>

samples/ObsKit.NET.Sample.ReplayBuffer/ObsKit.NET.Sample.ReplayBuffer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<RootNamespace>ObsKit.NET.Sample.ReplayBuffer</RootNamespace>
6+
<ApplicationManifest>app.manifest</ApplicationManifest>
67
</PropertyGroup>
78

89
<ItemGroup>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
3+
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
4+
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
5+
<security>
6+
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
7+
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
8+
</requestedPrivileges>
9+
</security>
10+
</trustInfo>
11+
12+
<application xmlns="urn:schemas-microsoft-com:asm.v3">
13+
<windowsSettings>
14+
<!-- Per-monitor DPI awareness V2 - required for DXGI output duplication -->
15+
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
16+
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
17+
</windowsSettings>
18+
</application>
19+
20+
</assembly>

src/ObsKit.NET/Platform/Windows/Interop/User32.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,57 @@ internal static partial byte EnumDisplayDevices(
9898

9999
#endregion
100100

101+
#region DPI Awareness
102+
103+
/// <summary>
104+
/// Gets the DPI awareness context for the current thread.
105+
/// </summary>
106+
[LibraryImport(Lib, SetLastError = true)]
107+
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
108+
internal static partial nint GetThreadDpiAwarenessContext();
109+
110+
/// <summary>
111+
/// Gets the DPI_AWARENESS value from a DPI_AWARENESS_CONTEXT.
112+
/// </summary>
113+
[LibraryImport(Lib, SetLastError = true)]
114+
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
115+
internal static partial int GetAwarenessFromDpiAwarenessContext(nint value);
116+
117+
/// <summary>
118+
/// DPI unaware. The thread does not scale for DPI changes.
119+
/// </summary>
120+
internal const int DPI_AWARENESS_UNAWARE = 0;
121+
122+
/// <summary>
123+
/// System DPI aware. The thread queries for DPI once at startup.
124+
/// </summary>
125+
internal const int DPI_AWARENESS_SYSTEM_AWARE = 1;
126+
127+
/// <summary>
128+
/// Per-monitor DPI aware. The thread receives DPI change notifications.
129+
/// </summary>
130+
internal const int DPI_AWARENESS_PER_MONITOR_AWARE = 2;
131+
132+
/// <summary>
133+
/// Checks if the current thread is per-monitor DPI aware.
134+
/// </summary>
135+
internal static bool IsPerMonitorDpiAware()
136+
{
137+
try
138+
{
139+
var context = GetThreadDpiAwarenessContext();
140+
var awareness = GetAwarenessFromDpiAwarenessContext(context);
141+
return awareness >= DPI_AWARENESS_PER_MONITOR_AWARE;
142+
}
143+
catch
144+
{
145+
// API not available (pre-Windows 10 1607)
146+
return false;
147+
}
148+
}
149+
150+
#endregion
151+
101152
#region Structures
102153

103154
[StructLayout(LayoutKind.Sequential)]

src/ObsKit.NET/Sources/MonitorCapture.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using ObsKit.NET.Core;
22
using ObsKit.NET.Platform;
3+
using ObsKit.NET.Platform.Windows.Interop;
34

45
namespace ObsKit.NET.Sources;
56

@@ -215,6 +216,15 @@ public MonitorCapture SetCaptureMethod(MonitorCaptureMethod method)
215216
{
216217
if (OperatingSystem.IsWindows())
217218
{
219+
// Check DPI awareness when using DXGI Desktop Duplication
220+
if ((method == MonitorCaptureMethod.DesktopDuplication || method == MonitorCaptureMethod.Auto) && !User32.IsPerMonitorDpiAware())
221+
{
222+
Console.Error.WriteLine("[ObsKit.NET] Warning: Desktop Duplication (DXGI) requires per-monitor DPI awareness.");
223+
Console.Error.WriteLine("[ObsKit.NET] Your application must include an app.manifest with DPI awareness settings.");
224+
Console.Error.WriteLine("[ObsKit.NET] Or use MonitorCaptureMethod.WindowsGraphicsCapture instead.");
225+
Console.Error.WriteLine("[ObsKit.NET] See: https://github.com/Segergren/ObsKit.NET/blob/main/docs/dpi-awareness.md");
226+
}
227+
218228
Update(s =>
219229
{
220230
// OBS uses integer values: 0=auto, 1=DXGI, 2=WGC

0 commit comments

Comments
 (0)