Skip to content

[BUG] - DirectX capture always falls back to BitBlt on non-English Windows #171

Description

@Opicka879

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)

Activity

  1. Opicka879 commented on Sep 10, 2026

    @Opicka879
    Author

    Since GitHub doesn't accept .json attachments, here is the file inline.

    ControlR.DesktopClient.Common/Resources/Strings/cs-CZ.json

    Complete Czech translation — 93 keys, matching en-US.json exactly (no missing
    or extra keys). It contains no branding of ours; it's a plain translation.

    Embedding it needs one more line in ControlR.DesktopClient.Common.csproj,
    next to the existing one:

    <EmbeddedResource Include="Resources\Strings\cs-CZ.json" />

    Nothing else is required. Localization.GetLocalizationStrings() picks the
    embedded resource whose name ends with {CurrentCulture}.json and falls back to
    en-US, so there is no culture list to maintain.

    Please feel free to include this in the project under its MIT license — no
    attribution needed. It's been in production use on Czech Windows installs.

    cs-CZ.json
    {
      "YesText": "Ano",
      "NoText": "Ne",
      "CancelText": "Zrušit",
      "OkText": "OK",
      "SubmitText": "Odeslat",
      "ManagedDeviceMessage": "Toto zařízení spravuje vaše organizace.",
      "ManagedDeviceTitle": "Spravované zařízení",
      "MacPermissionStaleHint": "Pokud přepnutí oprávnění nezabere, odeberte aplikaci tlačítkem „−“ v Nastavení systému a přidejte ji znovu.",
      "Accessibility": "Zpřístupnění",
      "Status": "Stav:",
      "GrantPermission": "Udělit oprávnění",
      "Granted": "Uděleno",
      "NotGranted": "Neuděleno",
      "AccessibilityPermissionDescription": "Potřebné pro vzdálené ovládání zařízení.",
      "ScreenRecording": "Nahrávání obrazovky",
      "ScreenCapturePermissionDescription": "Potřebné pro zobrazení obrazovky zařízení.",
      "ScreenCastPermission": "Snímání obrazovky (ScreenCast)",
      "ScreenCastPermissionDescription": "Potřebné pro snímání a sdílení obrazovky přes XDG Desktop Portal.",
      "RemoteControlPermission": "Vzdálené ovládání",
      "RemoteControlPermissionDescription": "Potřebné pro snímání obrazovky a simulaci vstupu přes XDG Desktop Portal.",
      "Notifications": "Oznámení",
      "NotificationPermissionDescription": "Potřebné pro zobrazování oznámení a upozornění.",
      "OpenSettings": "Otevřít nastavení",
      "LearnMore": "Zjistit více",
      "RemoteControlSessionEndToastMessage": "{0} se odpojil. Sdílení obrazovky skončilo.",
      "RemoteControlSessionToastTitle": "Relace vzdáleného ovládání",
      "RemoteControlSessionStartToastMessage": "{0} se připojil a nyní sdílí vaši obrazovku.",
      "NewChatMessageToastTitle": "Nová zpráva v chatu",
      "NewChatMessageToastMessage": "Máte novou zprávu od {0}.",
      "ChatWindowTitle": "Chat s {0}",
      "ChatWindowDefaultTitle": "Chat ControlR",
      "ChatWindowClosedSystemMessage": "Systémová zpráva: Uživatel zavřel okno chatu.",
      "SendMessageText": "Odeslat",
      "EnterMessagePlaceholder": "Napište zprávu…",
      "NoMessagesText": "Zatím žádné zprávy. Konverzaci zahájíte odesláním zprávy.",
      "CloseText": "Zavřít",
      "SendText": "Odeslat",
      "ChatSessionClosedToastTitle": "Chat ukončen",
      "ChatSessionClosedToastMessage": "Vzdálený uživatel ukončil chat.",
      "ChatMessageSendFailureTitle": "Odeslání selhalo",
      "ChatMessageSendFailureMessage": "Zprávu se nepodařilo odeslat",
      "You": "Vy",
      "Version": "Verze",
      "InstanceId": "ID instance",
      "Theme": "Motiv",
      "Auto": "Automaticky",
      "Light": "Světlý",
      "Dark": "Tmavý",
      "ADeviceAdministrator": "správce zařízení",
      "RemoteControlRequestTitle": "Žádost o vzdálené ovládání",
      "RemoteControlRequestMessage": "Povolit relaci vzdáleného ovládání od {0}?",
      "PermissionsMissingToastTitle": "Vyžadována oprávnění",
      "PermissionsMissingToastMessage": "Chybí potřebná oprávnění. Kliknutím je udělíte.",
      "MacAccessibilityPermissionRestartRequired": "Než půjde požádat o oprávnění Zpřístupnění, je nutné restartovat aplikaci.",
      "MacScreenCapturePermissionRestartRequired": "Než půjde požádat o oprávnění Nahrávání obrazovky, je nutné restartovat aplikaci.",
      "NoPermissionsRequired": "V tomto operačním systému nejsou potřeba žádná oprávnění.",
      "ActiveConnections": "Aktivní připojení",
      "AgentStatus": "Stav agenta",
      "AgentConnectedTooltip": "Připojeno ke službě na pozadí",
      "AgentDisconnectedTooltip": "Odpojeno od služby na pozadí",
      "Connected": "Připojeno",
      "Disconnected": "Odpojeno",
      "Connections": "Připojení",
      "Settings": "Nastavení",
      "Permissions": "Oprávnění",
      "Disconnect": "Odpojit",
      "ConfirmDisconnect": "Opravdu chcete tuto relaci vzdáleného ovládání odpojit?",
      "ConfirmDisconnectTitle": "Potvrzení odpojení",
      "NoActiveConnections": "Žádná aktivní připojení",
      "About": "O aplikaci",
      "AboutLicenseLabel": "Licence",
      "AboutProjectLabel": "Projekt",
      "AboutWebsiteLabel": "Web",
      "AboutSponsorLabel": "Podpora projektu",
      "AboutSponsorButton": "❤️ Podpořit projekt",
      "AboutSponsorDescription": "ControlR je zdarma, s otevřeným kódem, provozovatelný na vlastním serveru a žije z darů.",
      "AboutThirdPartyLicensesLabel": "Licence třetích stran",
      "AboutThirdPartyLicensesDescription": "ControlR používá následující otevřený software a knihovny.",
      "AboutLibraryAvaloniaUi": "Avalonia UI",
      "AboutLibrarySerilog": "Serilog",
      "AboutLibraryTmdsDbus": "Tmds.DBus",
      "AboutLibraryMicrosoftLibraries": "Knihovny Microsoftu",
      "AboutSourceText": "zdroj",
      "LinkLicenseText": "licence",
      "ConnectedAtLabel": "Připojeno v",
      "TrayShow": "Zobrazit",
      "TrayExit": "Ukončit",
      "InvalidViewModelTypeMessage": "Neplatný typ modelu zobrazení",
      "ViewNotFoundMessage": "Zobrazení nenalezeno: {0}",
      "CouldNotCreateViewMessage": "Nepodařilo se vytvořit zobrazení: {0}",
      "UnhandledExceptionMessage": "Došlo k neošetřené výjimce. Podrobnosti najdete v protokolu.",
      "UnhandledExceptionTitle": "Neošetřená výjimka",
      "None": "Žádné"
    }
  2. changed the title [-]DirectX capture always falls back to BitBlt on non-English Windows[/-] [+][BUG] - DirectX capture always falls back to BitBlt on non-English Windows[/+] on Sep 10, 2026
  3. bitbound commented on Sep 10, 2026

    @bitbound
    Owner

    @Opicka879 Nice catch! :)

    Thank you for the detailed report and for the translations. I'll include these in the next release.

  4. bitbound commented on Oct 8, 2026

    @bitbound
    Owner

    The DirectX capture fix shipped in v0.28.15.0.

    I'll add the translations in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions