Summary
ScreenGrabberWindows detects DXGI_ERROR_WAIT_TIMEOUT by matching the English
exception message. On a localized Windows install the message is translated, the
when filter never matches, and the exception falls through to the next handler —
which calls SetCurrentOutputFaulted() and falls back to GDI BitBlt.
The result: on any non-English Windows, DirectX capture is effectively never used,
and the "DirectX" toggle in the viewer has no observable effect.
Environment
- ControlR 0.27.6.0
- Windows 10, Czech (cs-CZ) system locale
- Agent running as a Windows service
Root cause
ControlR.DesktopClient.Windows/Services/ScreenGrabberWindows.cs:
catch (COMException ex) when (ex.Message.StartsWith("The timeout value has elapsed"))
{
return CaptureResult.NoChanges(captureMode: DirectXCaptureMode);
}
catch (COMException ex)
{
_dxOutputGenerator.SetCurrentOutputFaulted();
_logger.LogWarningDeduped(
"Failed to capture with DirectX, falling back to BitBlt. Display: {DisplayId}", ...);
return CaptureResult.Fail(ex);
}
AcquireNextFrame is called with a 0 ms timeout, so DXGI_ERROR_WAIT_TIMEOUT
(0x887A0027) is returned whenever a new frame isn't already queued. That is the
common case and not an error condition.
On Czech Windows the message reads:
Hodnota časového limitu vypršela a prostředek není ještě k dispozici. (0x887A0027)
which does not start with "The timeout value has elapsed", so the filter fails.
Agent log
[WRN] Failed to capture with DirectX, falling back to BitBlt. Display: \\.\DISPLAY1
System.Runtime.InteropServices.COMException (0x887A0027): Hodnota časového limitu
vypršela a prostředek není ještě k dispozici. (0x887A0027)
at Windows.Win32.Graphics.Dxgi.IDXGIOutputDuplication.AcquireNextFrame(...)
at ControlR.DesktopClient.Windows.Services.ScreenGrabberWindows.GetDirectXCapture(...)
SetCurrentOutputFaulted() marks the output faulted for 10 seconds and disposes it,
so the duplication object is torn down and recreated in a loop for the whole session.
Impact
Measured on the same machine, same activity, before and after the fix:
|
FPS |
Agent CPU |
| Before |
10–13 |
~35 % |
| After |
20–40 |
lower |
Because BitBlt provides no dirty rectangles, GetDirtyRegions also falls back to a
full software pixel comparison of every frame, and the cursor-only fast path in
HandleCursorOnlyUpdate is never taken.
Suggested fix
Match on the HRESULT, which is locale-independent:
// DXGI_ERROR_WAIT_TIMEOUT = 0x887A0027
catch (COMException ex) when (ex.HResult == unchecked((int)0x887A0027))
{
return CaptureResult.NoChanges(captureMode: DirectXCaptureMode);
}
We've been running this patch on Czech Windows and it resolves the issue.
Note
Three other message-text comparisons exist in the codebase, but none of them read an
OS-generated message, so they should not be affected:
ControlR.DesktopClient/Program.cs — "RenderTimer" (Avalonia)
ControlR.Agent.Common/Services/AgentHubClient.cs — "canceled by client" (SignalR)
ControlR.Web.Server/Hubs/AgentHub.cs — "does not exist" (server runs in a Linux container)
Summary
ScreenGrabberWindowsdetectsDXGI_ERROR_WAIT_TIMEOUTby matching the Englishexception message. On a localized Windows install the message is translated, the
whenfilter never matches, and the exception falls through to the next handler —which calls
SetCurrentOutputFaulted()and falls back to GDI BitBlt.The result: on any non-English Windows, DirectX capture is effectively never used,
and the "DirectX" toggle in the viewer has no observable effect.
Environment
Root cause
ControlR.DesktopClient.Windows/Services/ScreenGrabberWindows.cs:AcquireNextFrameis called with a 0 ms timeout, soDXGI_ERROR_WAIT_TIMEOUT(
0x887A0027) is returned whenever a new frame isn't already queued. That is thecommon case and not an error condition.
On Czech Windows the message reads:
which does not start with
"The timeout value has elapsed", so the filter fails.Agent log
SetCurrentOutputFaulted()marks the output faulted for 10 seconds and disposes it,so the duplication object is torn down and recreated in a loop for the whole session.
Impact
Measured on the same machine, same activity, before and after the fix:
Because BitBlt provides no dirty rectangles,
GetDirtyRegionsalso falls back to afull software pixel comparison of every frame, and the cursor-only fast path in
HandleCursorOnlyUpdateis never taken.Suggested fix
Match on the HRESULT, which is locale-independent:
We've been running this patch on Czech Windows and it resolves the issue.
Note
Three other message-text comparisons exist in the codebase, but none of them read an
OS-generated message, so they should not be affected:
ControlR.DesktopClient/Program.cs—"RenderTimer"(Avalonia)ControlR.Agent.Common/Services/AgentHubClient.cs—"canceled by client"(SignalR)ControlR.Web.Server/Hubs/AgentHub.cs—"does not exist"(server runs in a Linux container)