questions/patch proposal for x11 hwcursor rendering artifacts | winex11.drv/mouse.c
Hi, I don't want to disturb you much but I would really appreciate some pointers with regards to winex11. I came across a pretty frustrating bug on buffered xorg based window managers where drawing of custom cursors that exceed XQueryBestSize end up in draws with artifacts with nvidia proprietary (visually looks like a bounded overflow draw), with mesa/intel you get flickering in some situations on xfce but not as crazy, (likely rapid switching between hw cursor <-> sw cursor or what have you). Anyways, i'm no X expert but I really tried looking into the issue and see which level of stack should the issue fall on/be addressed at. Well.. It's not obvious to me. Xlib / X protocol technically doesn't impose any policy on cursor limits but it does expose things like XQueryBestSize. For example on xfce/xlibre/nvidia machine: ``` ❯ xdpyinfo | grep -i cursor largest cursor: 256x256 ``` And so there is one game on steam that creates a cursor of 384x384 in 1440p and that ends up blowing up and turning into a visual mess. Of course, that cursor is not 384x384, it just creates a buffer of 384x384 because the game for some reason does like 4x internal scaling or something (96dpi x 4 I assume). Basically it's not that easy to really nail where the discrepancy is. x11/xorg server does 'technically' have path that switches hw cursor to sw cursor but it doesn't seem to work or is unstable on window manager/graphics driver basis? Like I said earlier, I experienced this artifact on both xlibre/xfce and freedesktop xorg/gnome on nvidia and mesa. The reason I'm mentioning this is that it seems that it doesn't seem to be a problem of just one window manager and also I'm still uncertain whether patching this on xorg server is either valid approach or easy. So from a protocol perspective I don't know whether or not technically the rendering artifacts are a fault of X protocol or server entirely or not. I think in part but the issue is that from a protocol perspective there is no real strictness about this so... I do figure that userspace apps that interact with create cursor can basically take advantage of protocol facilities that result in stable drawing. The only problem being is that I found two ways to address this issue, in wine in particular. either: 1. pass smaller BITMAP dimensions to NtGdiCreateDIBSection (cursor initializer?) which win seems to auto downscale the cursor to those dimensions ? (looks correct in my use-case but the cursor is visibly smaller) 2. clamp/trim/crop the larger cursor buffer into smaller one that fits XQueryBestSize. (looks 1:1 to windows in my use-case) The problem is that visually for my use-case 2 gives correct visual behavior but I think trimming is likely wrong approach if it were default. As you can see from the diff I'm gating both methods atm but what I would love to know if: - it's acceptable to have something like this be handled by winex11.drv at all or do you think this is not wine's problem at all? I would say that it would be good if wine respected XQueryBestSize but I'm not certain yet on what the right shape of the hot path should be. - does this diff look sensible or is this wrong approach in general? In terms of naming I'm pretty sure it's wrong, I just don't know what the right names should be. scale cursor is technically also an implicit lie (it doesn't resample, it just rewrites dims right - it just happend to give that resulting effect when I was testing)... If you think it's better for me to create a merge/draft request lmk, I just figured to just ask around first before jumping into something more formal: ``` diff --git a/dlls/winex11.drv/mouse.c b/dlls/winex11.drv/mouse.c index e1a5e70a6fe..899990af266 100644 --- a/dlls/winex11.drv/mouse.c +++ b/dlls/winex11.drv/mouse.c @@ -673,7 +673,8 @@ static void send_mouse_input( HWND hwnd, Window window, unsigned int state, INPU static XcursorImage *create_xcursor_frame( HDC hdc, const ICONINFOEXW *iinfo, HANDLE icon, HBITMAP hbmColor, unsigned char *color_bits, int color_size, HBITMAP hbmMask, unsigned char *mask_bits, int mask_size, - int width, int height, int istep ) + int width, int height, int istep, + int x_width, int x_height) { XcursorImage *image, *ret = NULL; DWORD delay_jiffies, num_steps; @@ -681,15 +682,29 @@ static XcursorImage *create_xcursor_frame( HDC hdc, const ICONINFOEXW *iinfo, HA BOOL has_alpha = FALSE; XcursorPixel *ptr; - image = pXcursorImageCreate( width, height ); + image = pXcursorImageCreate( x_width, x_height ); if (!image) { ERR("X11 failed to produce a cursor frame!\n"); return NULL; } - image->xhot = iinfo->xHotspot; - image->yhot = iinfo->yHotspot; + if (!x_width || !x_height || !width || !height) + { + return NULL; + } + + if(iinfo->xHotspot > x_width) { + image->xhot = (XcursorDim)x_width - 1; + } else { + image->xhot = iinfo->xHotspot; + } + + if(iinfo->yHotspot > y_width) { + image->yhot = (XcursorDim)y_width - 1; + } else { + image->yhot = iinfo->yHotspot; + } image->delay = 100; /* fallback delay, 100 ms */ if (NtUserGetCursorFrameInfo(icon, istep, &delay_jiffies, &num_steps) != 0) @@ -705,10 +720,18 @@ static XcursorImage *create_xcursor_frame( HDC hdc, const ICONINFOEXW *iinfo, HA TRACE("Could not draw frame %d (walk past end of frames).\n", istep); goto cleanup; } - memcpy( image->pixels, color_bits, color_size ); + + if (x_width >= width && x_height >= height) { + memcpy( image->pixels, color_bits, color_size ); + } else { + for (y = 0; y < x_height; y++) + memcpy( image->pixels + y * x_width, + (XcursorPixel *)color_bits + y * width, + x_width * sizeof(XcursorPixel) ); + } /* check if the cursor frame was drawn with an alpha channel */ - for (i = 0, ptr = image->pixels; i < width * height; i++, ptr++) + for (i = 0, ptr = image->pixels; i < x_width * x_height; i++, ptr++) if ((has_alpha = (*ptr & 0xff000000) != 0)) break; /* if no alpha channel was drawn then generate it from the mask */ @@ -725,8 +748,8 @@ static XcursorImage *create_xcursor_frame( HDC hdc, const ICONINFOEXW *iinfo, HA goto cleanup; } /* use the buffer to directly modify the XcursorImage alpha channel */ - for (y = 0, ptr = image->pixels; y < height; y++) - for (x = 0; x < width; x++, ptr++) + for (y = 0, ptr = image->pixels; y < x_height; y++) + for (x = 0; x < x_width; x++, ptr++) if (!((mask_bits[y * width_bytes + x / 8] << (x % 8)) & 0x80)) *ptr |= 0xff000000; } @@ -752,6 +775,27 @@ static Cursor create_xcursor_cursor( HDC hdc, const ICONINFOEXW *iinfo, HANDLE i XcursorImages *images; XcursorImage **imgs; Cursor cursor = 0; + unsigned int x_width = 0; + unsigned int x_height = 0; + int w_width = width; + int w_height = height; + + /* Clamp requested cursor frame dimensions to max best dimensions + * available by x to avoid hwcursor overflows (or forced sw cursor switch) + * with oversized cursors on draw */ + if (scale_cursor_to_best_size ^ clamp_cursor_to_best_size) { + XQueryBestCursor( gdi_display, DefaultRootWindow( gdi_display ), + w_width, w_height, &x_width, &x_height ); + + /* Aggressively reassign cursor dimensions to max available */ + if(scale_cursor_to_best_size) { + if (width > (int)x_width) w_width = (int)x_width; + if (height > (int)x_height) w_height = (int)x_height; + } + } else { + x_width = w_width; + x_height = w_height; + } /* Retrieve the number of frames to render */ if (!NtUserGetCursorFrameInfo(icon, 0, &delay_jiffies, &nFrames)) return 0; @@ -760,8 +804,8 @@ static Cursor create_xcursor_cursor( HDC hdc, const ICONINFOEXW *iinfo, HANDLE i /* Allocate all of the resources necessary to obtain a cursor frame */ if (!(info = malloc( FIELD_OFFSET( BITMAPINFO, bmiColors[256] )))) goto cleanup; info->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - info->bmiHeader.biWidth = width; - info->bmiHeader.biHeight = -height; + info->bmiHeader.biWidth = w_width; + info->bmiHeader.biHeight = -w_height; info->bmiHeader.biPlanes = 1; info->bmiHeader.biCompression = BI_RGB; info->bmiHeader.biXPelsPerMeter = 0; @@ -769,7 +813,7 @@ static Cursor create_xcursor_cursor( HDC hdc, const ICONINFOEXW *iinfo, HANDLE i info->bmiHeader.biClrUsed = 0; info->bmiHeader.biClrImportant = 0; info->bmiHeader.biBitCount = 32; - color_size = width * height * 4; + color_size = w_width * w_height * 4; info->bmiHeader.biSizeImage = color_size; hbmColor = NtGdiCreateDIBSection( hdc, NULL, 0, info, DIB_RGB_COLORS, 0, 0, 0, (void **)&color_bits ); if (!hbmColor) @@ -787,7 +831,7 @@ static Cursor create_xcursor_cursor( HDC hdc, const ICONINFOEXW *iinfo, HANDLE i info->bmiColors[1].rgbBlue = 0xff; info->bmiColors[1].rgbReserved = 0; - mask_size = ((width + 31) / 32 * 4) * height; /* width_bytes * height */ + mask_size = ((width + 31) / 32 * 4) * w_height; /* width_bytes * height */ info->bmiHeader.biSizeImage = mask_size; hbmMask = NtGdiCreateDIBSection( hdc, NULL, 0, info, DIB_RGB_COLORS, 0, 0, 0, (void **)&mask_bits ); if (!hbmMask) @@ -802,7 +846,7 @@ static Cursor create_xcursor_cursor( HDC hdc, const ICONINFOEXW *iinfo, HANDLE i imgs[i] = create_xcursor_frame( hdc, iinfo, icon, hbmColor, color_bits, color_size, hbmMask, mask_bits, mask_size, - width, height, i ); + w_width, w_height, i, (int)x_width, (int)x_height ); if (!imgs[i]) goto cleanup; } diff --git a/dlls/winex11.drv/x11drv.h b/dlls/winex11.drv/x11drv.h index 9436f35e1ca..cdcdf95f5c0 100644 --- a/dlls/winex11.drv/x11drv.h +++ b/dlls/winex11.drv/x11drv.h @@ -478,6 +478,8 @@ extern BOOL usexcomposite; extern BOOL use_xfixes; extern BOOL managed_mode; extern BOOL private_color_map; +extern BOOL clamp_cursor_to_best_size; +extern BOOL scale_cursor_to_best_size; extern int primary_monitor; extern int copy_default_colors; extern int alloc_system_colors; diff --git a/dlls/winex11.drv/x11drv_main.c b/dlls/winex11.drv/x11drv_main.c index 81008f45c33..0b1c87fef91 100644 --- a/dlls/winex11.drv/x11drv_main.c +++ b/dlls/winex11.drv/x11drv_main.c @@ -81,6 +81,8 @@ int primary_monitor = 0; BOOL client_side_graphics = TRUE; BOOL client_side_with_render = TRUE; BOOL shape_layered_windows = TRUE; +BOOL clamp_cursor_to_best_size = FALSE; +BOOL scale_cursor_to_best_size = FALSE; int copy_default_colors = 128; int alloc_system_colors = 256; int xrender_error_base = 0; @@ -514,6 +516,12 @@ static void setup_options(void) if (!get_config_key( hkey, appkey, "AllocSystemColors", buffer, sizeof(buffer) )) alloc_system_colors = wcstol( buffer, NULL, 0 ); + if (!get_config_key( hkey, appkey, "ClampCursorToBestSize", buffer, sizeof(buffer) )) + clamp_cursor_to_best_size = IS_OPTION_TRUE( buffer[0] ); + + if (!get_config_key( hkey, appkey, "ScaleCursorToBestSize", buffer, sizeof(buffer) )) + scale_cursor_to_best_size = IS_OPTION_TRUE( buffer[0] ); + get_config_key( hkey, appkey, "InputStyle", input_style, sizeof(input_style) ); NtClose( appkey ); ```
Syntax error in diff. appologies: ``` diff --git a/dlls/winex11.drv/mouse.c b/dlls/winex11.drv/mouse.c index e1a5e70a6fe..9eaa2c3555e 100644 --- a/dlls/winex11.drv/mouse.c +++ b/dlls/winex11.drv/mouse.c @@ -673,7 +673,8 @@ static void send_mouse_input( HWND hwnd, Window window, unsigned int state, INPU static XcursorImage *create_xcursor_frame( HDC hdc, const ICONINFOEXW *iinfo, HANDLE icon, HBITMAP hbmColor, unsigned char *color_bits, int color_size, HBITMAP hbmMask, unsigned char *mask_bits, int mask_size, - int width, int height, int istep ) + int width, int height, int istep, + int x_width, int x_height) { XcursorImage *image, *ret = NULL; DWORD delay_jiffies, num_steps; @@ -681,15 +682,29 @@ static XcursorImage *create_xcursor_frame( HDC hdc, const ICONINFOEXW *iinfo, HA BOOL has_alpha = FALSE; XcursorPixel *ptr; - image = pXcursorImageCreate( width, height ); + image = pXcursorImageCreate( x_width, x_height ); if (!image) { ERR("X11 failed to produce a cursor frame!\n"); return NULL; } - image->xhot = iinfo->xHotspot; - image->yhot = iinfo->yHotspot; + if (!x_width || !x_height || !width || !height) + { + return NULL; + } + + if(iinfo->xHotspot > x_width) { + image->xhot = (XcursorDim)x_width - 1; + } else { + image->xhot = iinfo->xHotspot; + } + + if(iinfo->yHotspot > x_height) { + image->yhot = (XcursorDim)x_height - 1; + } else { + image->yhot = iinfo->yHotspot; + } image->delay = 100; /* fallback delay, 100 ms */ if (NtUserGetCursorFrameInfo(icon, istep, &delay_jiffies, &num_steps) != 0) @@ -705,10 +720,18 @@ static XcursorImage *create_xcursor_frame( HDC hdc, const ICONINFOEXW *iinfo, HA TRACE("Could not draw frame %d (walk past end of frames).\n", istep); goto cleanup; } - memcpy( image->pixels, color_bits, color_size ); + + if (x_width >= width && x_height >= height) { + memcpy( image->pixels, color_bits, color_size ); + } else { + for (y = 0; y < x_height; y++) + memcpy( image->pixels + y * x_width, + (XcursorPixel *)color_bits + y * width, + x_width * sizeof(XcursorPixel) ); + } /* check if the cursor frame was drawn with an alpha channel */ - for (i = 0, ptr = image->pixels; i < width * height; i++, ptr++) + for (i = 0, ptr = image->pixels; i < x_width * x_height; i++, ptr++) if ((has_alpha = (*ptr & 0xff000000) != 0)) break; /* if no alpha channel was drawn then generate it from the mask */ @@ -725,8 +748,8 @@ static XcursorImage *create_xcursor_frame( HDC hdc, const ICONINFOEXW *iinfo, HA goto cleanup; } /* use the buffer to directly modify the XcursorImage alpha channel */ - for (y = 0, ptr = image->pixels; y < height; y++) - for (x = 0; x < width; x++, ptr++) + for (y = 0, ptr = image->pixels; y < x_height; y++) + for (x = 0; x < x_width; x++, ptr++) if (!((mask_bits[y * width_bytes + x / 8] << (x % 8)) & 0x80)) *ptr |= 0xff000000; } @@ -752,6 +775,27 @@ static Cursor create_xcursor_cursor( HDC hdc, const ICONINFOEXW *iinfo, HANDLE i XcursorImages *images; XcursorImage **imgs; Cursor cursor = 0; + unsigned int x_width = 0; + unsigned int x_height = 0; + int w_width = width; + int w_height = height; + + /* Clamp requested cursor frame dimensions to max best dimensions + * available by x to avoid hwcursor overflows (or forced sw cursor switch) + * with oversized cursors on draw */ + if (scale_cursor_to_best_size ^ clamp_cursor_to_best_size) { + XQueryBestCursor( gdi_display, DefaultRootWindow( gdi_display ), + w_width, w_height, &x_width, &x_height ); + + /* Aggressively reassign cursor dimensions to max available */ + if(scale_cursor_to_best_size) { + if (width > (int)x_width) w_width = (int)x_width; + if (height > (int)x_height) w_height = (int)x_height; + } + } else { + x_width = w_width; + x_height = w_height; + } /* Retrieve the number of frames to render */ if (!NtUserGetCursorFrameInfo(icon, 0, &delay_jiffies, &nFrames)) return 0; @@ -760,8 +804,8 @@ static Cursor create_xcursor_cursor( HDC hdc, const ICONINFOEXW *iinfo, HANDLE i /* Allocate all of the resources necessary to obtain a cursor frame */ if (!(info = malloc( FIELD_OFFSET( BITMAPINFO, bmiColors[256] )))) goto cleanup; info->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - info->bmiHeader.biWidth = width; - info->bmiHeader.biHeight = -height; + info->bmiHeader.biWidth = w_width; + info->bmiHeader.biHeight = -w_height; info->bmiHeader.biPlanes = 1; info->bmiHeader.biCompression = BI_RGB; info->bmiHeader.biXPelsPerMeter = 0; @@ -769,7 +813,7 @@ static Cursor create_xcursor_cursor( HDC hdc, const ICONINFOEXW *iinfo, HANDLE i info->bmiHeader.biClrUsed = 0; info->bmiHeader.biClrImportant = 0; info->bmiHeader.biBitCount = 32; - color_size = width * height * 4; + color_size = w_width * w_height * 4; info->bmiHeader.biSizeImage = color_size; hbmColor = NtGdiCreateDIBSection( hdc, NULL, 0, info, DIB_RGB_COLORS, 0, 0, 0, (void **)&color_bits ); if (!hbmColor) @@ -787,7 +831,7 @@ static Cursor create_xcursor_cursor( HDC hdc, const ICONINFOEXW *iinfo, HANDLE i info->bmiColors[1].rgbBlue = 0xff; info->bmiColors[1].rgbReserved = 0; - mask_size = ((width + 31) / 32 * 4) * height; /* width_bytes * height */ + mask_size = ((width + 31) / 32 * 4) * w_height; /* width_bytes * height */ info->bmiHeader.biSizeImage = mask_size; hbmMask = NtGdiCreateDIBSection( hdc, NULL, 0, info, DIB_RGB_COLORS, 0, 0, 0, (void **)&mask_bits ); if (!hbmMask) @@ -802,7 +846,7 @@ static Cursor create_xcursor_cursor( HDC hdc, const ICONINFOEXW *iinfo, HANDLE i imgs[i] = create_xcursor_frame( hdc, iinfo, icon, hbmColor, color_bits, color_size, hbmMask, mask_bits, mask_size, - width, height, i ); + w_width, w_height, i, (int)x_width, (int)x_height ); if (!imgs[i]) goto cleanup; } diff --git a/dlls/winex11.drv/x11drv.h b/dlls/winex11.drv/x11drv.h index 9436f35e1ca..cdcdf95f5c0 100644 --- a/dlls/winex11.drv/x11drv.h +++ b/dlls/winex11.drv/x11drv.h @@ -478,6 +478,8 @@ extern BOOL usexcomposite; extern BOOL use_xfixes; extern BOOL managed_mode; extern BOOL private_color_map; +extern BOOL clamp_cursor_to_best_size; +extern BOOL scale_cursor_to_best_size; extern int primary_monitor; extern int copy_default_colors; extern int alloc_system_colors; diff --git a/dlls/winex11.drv/x11drv_main.c b/dlls/winex11.drv/x11drv_main.c index 81008f45c33..0b1c87fef91 100644 --- a/dlls/winex11.drv/x11drv_main.c +++ b/dlls/winex11.drv/x11drv_main.c @@ -81,6 +81,8 @@ int primary_monitor = 0; BOOL client_side_graphics = TRUE; BOOL client_side_with_render = TRUE; BOOL shape_layered_windows = TRUE; +BOOL clamp_cursor_to_best_size = FALSE; +BOOL scale_cursor_to_best_size = FALSE; int copy_default_colors = 128; int alloc_system_colors = 256; int xrender_error_base = 0; @@ -514,6 +516,12 @@ static void setup_options(void) if (!get_config_key( hkey, appkey, "AllocSystemColors", buffer, sizeof(buffer) )) alloc_system_colors = wcstol( buffer, NULL, 0 ); + if (!get_config_key( hkey, appkey, "ClampCursorToBestSize", buffer, sizeof(buffer) )) + clamp_cursor_to_best_size = IS_OPTION_TRUE( buffer[0] ); + + if (!get_config_key( hkey, appkey, "ScaleCursorToBestSize", buffer, sizeof(buffer) )) + scale_cursor_to_best_size = IS_OPTION_TRUE( buffer[0] ); + get_config_key( hkey, appkey, "InputStyle", input_style, sizeof(input_style) ); NtClose( appkey ); ```
Am 21.04.2026 um 07:34 schrieb meow via Wine-Devel <wine-devel@list.winehq.org>:
And so there is one game on steam that creates a cursor of 384x384 in 1440p and that ends up blowing up and turning into a visual mess. Of course, that cursor is not 384x384, it just creates a buffer of 384x384 because the game for some reason does like 4x internal scaling or something (96dpi x 4 I assume).
Did you check what Windows does in this case? As you say, cropping the cursor gives identical results to what you see on Windows, so maybe it is actually the right thing to do. It is also possible that Windows returns an error and the application responds to that by creating a smaller image. The third option is that some information returned to the application is incorrect, causing the application to behave in an unintended way. If it is an old program, it is likely that it queries the DPI, but doesn't treat the result correctly and breaks if dpi != 96. In this case you might be able to reproduce a similar issue on Windows when you disable OS-Side scaling (i.e. set compatibility option scaling performed by application)
Yeah unfortunately it's proprietary game w/ wine dpi set to 96, so it's just some internal shenanigans that the devs are doing likely. Granted it would be helpful to have some version of a toggle that would allow to work around this cursor weirdness in x11/xorg/xlibre. Drawing a 384x384 floating buffer shouldn't be of an issue from a conceptual standpoint, alas. I created a thread on xlibre as well, but it might take a while for the devs to reply.
participants (2)
-
Stefan Dösinger -
winehq@jaceee.com