PR #23206 opened by mxschmitt
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23206
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23206.patch

On headless Windows machines (cloud VMs, remote desktop sessions,
virtual display adapters), GetCursorInfo() returns flags=2
(CURSOR_SUPPRESSED) with hCursor=NULL, causing paint_mouse_pointer()
to skip cursor drawing entirely.

CURSOR_SUPPRESSED (0x00000002) was introduced in Windows 8 and is
documented as "the system is not drawing the cursor because the user
is providing input through touch or pen instead of the mouse" [1].
In practice, Windows also reports this state on any headless or
virtual display where no physical monitor is consuming the display
signal. This affects all cloud VM environments (AWS EC2, Azure VMs,
QEMU/KVM guests) and disconnected Remote Desktop sessions.

The cursor position in ptScreenPos remains valid when suppressed.
Additionally, GetCursor() returns a valid cursor handle for the
current thread even in the suppressed state.

The paint_mouse_pointer() function and its CURSOR_SHOWING check have
been unchanged since the initial gdigrab commit in 2014 (08909fb5).
At that time, headless cloud VMs were not a common recording target.
The existing Wine fallback (LoadCursor IDC_ARROW) already acknowledges
that CopyCursor(hCursor) can fail in some environments.

Fix by:
1. Only skipping cursor draw when flags=0 (truly hidden, i.e. the
   cursor display counter is <= 0), not when flags=2 (suppressed).
2. Using GetCursor() as fallback when CopyCursor(hCursor) returns NULL,
   before falling back to IDC_ARROW.

On a normal desktop with a physical display, GetCursorInfo always
returns flags=1 (CURSOR_SHOWING) with a valid hCursor, so the new
code path is never reached. The fix only activates in suppressed or
headless environments.

Tested on Windows Server 2025 headless EC2 instances (AWS) at 30 and
60 fps with correct cursor shape rendering across different UI
elements (arrow, I-beam, hand pointer, resize cursors).

[1] 
https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-cursorinfo

Signed-off-by: Max Schmitt <[email protected]>


>From db07a3f64d648d175bb482875ce8b0e1ff2e6750 Mon Sep 17 00:00:00 2001
From: Max Schmitt <[email protected]>
Date: Fri, 22 May 2026 15:03:34 -0700
Subject: [PATCH] avdevice/gdigrab: fix cursor capture when CURSOR_SUPPRESSED

On headless Windows machines (cloud VMs, remote desktop sessions,
virtual display adapters), GetCursorInfo() returns flags=2
(CURSOR_SUPPRESSED) with hCursor=NULL, causing paint_mouse_pointer()
to skip cursor drawing entirely.

CURSOR_SUPPRESSED (0x00000002) was introduced in Windows 8 and is
documented as "the system is not drawing the cursor because the user
is providing input through touch or pen instead of the mouse" [1].
In practice, Windows also reports this state on any headless or
virtual display where no physical monitor is consuming the display
signal. This affects all cloud VM environments (AWS EC2, Azure VMs,
QEMU/KVM guests) and disconnected Remote Desktop sessions.

The cursor position in ptScreenPos remains valid when suppressed.
Additionally, GetCursor() returns a valid cursor handle for the
current thread even in the suppressed state.

The paint_mouse_pointer() function and its CURSOR_SHOWING check have
been unchanged since the initial gdigrab commit in 2014 (08909fb5).
At that time, headless cloud VMs were not a common recording target.
The existing Wine fallback (LoadCursor IDC_ARROW) already acknowledges
that CopyCursor(hCursor) can fail in some environments.

Fix by:
1. Only skipping cursor draw when flags=0 (truly hidden, i.e. the
   cursor display counter is <= 0), not when flags=2 (suppressed).
2. Using GetCursor() as fallback when CopyCursor(hCursor) returns NULL,
   before falling back to IDC_ARROW.

On a normal desktop with a physical display, GetCursorInfo always
returns flags=1 (CURSOR_SHOWING) with a valid hCursor, so the new
code path is never reached. The fix only activates in suppressed or
headless environments.

Tested on Windows Server 2025 headless EC2 instances (AWS) at 30 and
60 fps with correct cursor shape rendering across different UI
elements (arrow, I-beam, hand pointer, resize cursors).

[1] 
https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-cursorinfo

Signed-off-by: Max Schmitt <[email protected]>
---
 libavdevice/gdigrab.c | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/libavdevice/gdigrab.c b/libavdevice/gdigrab.c
index fa9ba27db0..c542501186 100644
--- a/libavdevice/gdigrab.c
+++ b/libavdevice/gdigrab.c
@@ -492,14 +492,15 @@ static void paint_mouse_pointer(AVFormatContext *s1, 
struct gdigrab *gdigrab)
         info.hbmMask = NULL;
         info.hbmColor = NULL;
 
-        if (ci.flags != CURSOR_SHOWING)
+        if (!ci.flags)
             return;
 
         if (!icon) {
-            /* Use the standard arrow cursor as a fallback.
-             * You'll probably only hit this in Wine, which can't fetch
-             * the current system cursor. */
-            icon = CopyCursor(LoadCursor(NULL, IDC_ARROW));
+            /* CURSOR_SUPPRESSED: hCursor is NULL. Use GetCursor() to
+             * obtain the current cursor for this thread. */
+            icon = CopyCursor(GetCursor());
+            if (!icon)
+                icon = CopyCursor(LoadCursor(NULL, IDC_ARROW));
         }
 
         if (!GetIconInfo(icon, &info)) {
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to