From: Aleksandr Kosachev <kosachev_alex@outlook.com> Run the Steam client (CEF/Chromium) and games under Wayland with GPU acceleration, i.e. without disabling dxgi/d3d9/d3d11/d3d12 in the registry for steamwebhelper.exe, with correct multi-monitor scaling and WoW64 support. winevulkan: - Load Windows implicit Vulkan layers (Steam overlay) with the proper layer ABI (vk_layer.h chain links), per-handle dispatch routing and availability gating; honor the layer enable_environment/disable_environment manifest gates with the reference loader's semantics (including the object form) so the overlay loads only where Steam requests it, not into every Vulkan process. vkGetDeviceProcAddr keeps vkCreateDevice on our front for layered devices; resolve_library_path sizes the manifest path by its real length instead of overflowing MAX_PATH. - Client-side WSI presenting cross-process Vulkan frames: swapchain images are allocated as DMABUFs (VK_EXT_image_drm_format_modifier, LINEAR when available), exported via vkGetMemoryFdKHR, mapped to Win32 handles and duplicated into the owning (Steam/GDI) process with NtDuplicateObject to build wl_buffers through zwp_linux_dmabuf_v1. DRM format/modifier negotiation, correct rowPitch stride (queried only for modifier-tiled images; the OPTIMAL fallback leaves it implicit), present-fence signalling, a per-image back-pressure fence (armed at present, waited at acquire) and client-side DMABUF caching keyed by inode + size + format + modifier. vkQueuePresentKHR uses a per-present stack buffer instead of malloc, bounds-checks the image index and fills pResults. winewayland.drv -- presentation: - Cross-process and GL/VK-obscured child window rendering: shared-memory window surfaces for windows whose toplevel belongs to another process, presented through an overlay subsurface above GL/VK content (WantsCrossProcessSurfaces/FlushRemoteSurface/NeedsPaintOverlay driver entry points, remote surface tracking in win32u/dce.c). - Present cross-process Vulkan frames as DMABUF subsurfaces of the owning toplevel (alpha DRM formats mapped to opaque), stacked to match the Win32 Z-order; subsurfaces of (hierarchically) invisible windows are unmapped so an opaque full-window frame no longer covers the browser content and stale frames do not linger (fixes the black store page). A monotonic per-surface serial detects orphaned subsurfaces (libwayland recycles the wl_surface proxy address after destroy) so requests are skipped and the subsurface re-parented, fixing a fatal "wl_subsurface: no parent" protocol error and blank CEF popup-menu content; empty popup frames get a transparent buffer so their child render widget is shown. - A borderless / exclusive-fullscreen game that renders only through the DMABUF path (no GDI window surface to flush) acknowledges its xdg fullscreen configure promptly: reconfigure runs from the present path while a configure is pending, and a fullscreen/maximized configure is acked straight from the requested state rather than waiting on the win32 message loop, so the compositor finalizes the state instead of leaving the screen-sized window floating off-screen. The viewport source is set to the whole DMABUF buffer each frame, so a stale source left by an earlier windowed (GDI) phase cannot stretch the frame past the edge. winewayland.drv -- input, cursor, window management: - Deliver pointer input scoped to the toplevel (like the X11 driver) so win32u's window_from_point descends the whole tree to the toplevel non-client area: this enables borderless title-bar drag (xdg_toplevel_move), edge resize (xdg_toplevel_resize) and hover-opened menus for GPU-composited CEF content. A focused child subsurface with no cursor of its own inherits the toplevel's so the pointer never goes cursor-less; WAYLAND_SetCursor is accepted for any window of the focused toplevel. - Interactive edge resize and maximize for the borderless client: a WS_THICKFRAME monitor-sized window is no longer advertised to the compositor as fixed-size (win32u clears WINE_SWP_RESIZABLE for monitor-filling frames, so resizability is taken from the window style). The activated window is raised in the server Z-order on foreground change so cross-process hit-testing routes input to it (Wayland exposes no global window stacking, unlike the X11 WM). - Compositor text-input (zwp_text_input_v3) is enabled only while the application keeps an input context associated (ImmAssociateContext): a game that disables IME then gets raw keys instead of the compositor's dead-key/accent popup eating held keys. win32u's NtUserAssociateInputContext reports activation to the driver through a new pSetIMEEnabled entry point, aggregated per toplevel across its child windows (an app may associate the context on a child control), so IME tracks live -- fixing in-game ability keys staying dead after toggling between a game's chat box and gameplay. win32u -- displays: - Per-process emulated display modes: a source with a win32u-synthesized mode list no longer changes global state on ChangeDisplaySettings; the validated mode is recorded in a process-local override and all app-visible metrics, monitor rects, the virt<->raw mapping and the per-process monitor layout (re-placed with place_all_displays, no gaps/overlaps) reflect it for that process only, while the wineserver, the drivers and other processes keep the native view. WM_DISPLAYCHANGE goes only to the requesting process's toplevels, and EnumDisplaySettings/QueryDisplayConfig/GetDeviceCaps report the override consistently. Overrides are validated on each display-cache rebuild and dropped when their source or mode disappears; detach (zero-sized mode), an emulated depth change, DM_POSITION/DM_DISPLAYORIENTATION and ChangeDisplaySettings(NULL) keep their global semantics. The global mode of such a source always tracks the host mode (no stale ENUM_CURRENT_SETTINGS), and apply_display_settings clamps global-path modes to it. - EDID synthesis: struct edid_monitor_info lives in wine/gdi_driver.h and win32u builds the binary EDID (build_edid_from_info) from driver-provided parsed info when a driver exposes no hardware EDID, instead of each driver assembling bytes; winewayland fills that info (per-output id from the connector name, physical size, preferred mode) so games can identify monitors. Drivers with a real hardware EDID still pass it through and win32u parses it as before; since add_monitor reads gdi_monitor.edid_info only when non-NULL, winex11's xrandr monitor list zeroes the slots it grows with realloc so the pointer is never read uninitialized. - apply_window_pos maps the window and valid rects out of the process's scaled view into native coordinates for the server (a no-op without an override) and guards a NULL valid_rects (the creation/update paths pass one, and dereferencing it crashed every NtUserCreateWindowEx/SetWindowPos with c0000005); update_visible_region maps the server reply back into the virtual view; flush_remote_surface composites into the native driver surface resolved with the raw DPI the section was created with, so cross-process children are not blank under a mode override; remote surfaces use the standard idle flushing. winewayland.drv -- output scale and systray: - Window scale derived from the real compositor scale, fallback chain xdg_output logical size -> wl_output.scale -> configured DPI, with correct 90/270 output transforms and EDID geometry. Output physical positions are recomputed on every output-state change in every process (not only by the desktop process) so per-window scale selection picks the right output on multi-monitor setups. GDI blit fallback for color bitmaps NtGdiGetDIBitsInternal cannot convert directly (e.g. 16bpp cursor bitmaps). - System tray icons via the StatusNotifierItem D-Bus protocol, building gracefully without libdbus; the dispatch thread waits on all icon connections with a single poll and a self-pipe instead of serially blocking 200ms each. Locking discipline (the recurring hazard in this driver): - Never call win32u (NtUser*) from the window-surface flush/reconfigure path: it takes win32u's user_lock under the window_surface lock and deadlocks against apply_window_pos -> window_surface_set_shape. The WS_THICKFRAME resizability, the monitor rectangle used to clamp an oversized fullscreen/maximized window, and the subsurface position/stacking are computed at config time and cached; reapply_cursor_clipping is deferred until win_data_mutex is released; WAYLAND_SetIMEEnabled keeps its win_data_mutex (per-toplevel count) and text-input phases unnested; WAYLAND_SetCursor resolves window ancestors before taking the pointer lock. --- dlls/win32u/dce.c | 620 +++++++++- dlls/win32u/driver.c | 41 + dlls/win32u/imm.c | 13 + dlls/win32u/message.c | 2 + dlls/win32u/ntuser_private.h | 2 + dlls/win32u/spy.c | 3 +- dlls/win32u/sysparams.c | 622 +++++++++- dlls/win32u/vulkan.c | 19 + dlls/win32u/win32u_private.h | 1 + dlls/win32u/window.c | 55 +- dlls/winevulkan/Makefile.in | 2 + dlls/winevulkan/layer.c | 575 +++++++++ dlls/winevulkan/loader.c | 455 ++++++- dlls/winevulkan/make_vulkan | 25 + dlls/winevulkan/vk_layer.h | 119 ++ dlls/winevulkan/vulkan_loader.h | 20 + dlls/winevulkan/vulkan_thunks.c | 120 +- dlls/winevulkan/vulkan_thunks.h | 25 + dlls/winevulkan/wsi.c | 1092 +++++++++++++++++ dlls/winevulkan/wsi_private.h | 22 + dlls/winewayland.drv/Makefile.in | 4 +- dlls/winewayland.drv/display.c | 98 +- dlls/winewayland.drv/dllmain.c | 23 + .../linux-dmabuf-unstable-v1.xml | 589 +++++++++ dlls/winewayland.drv/systray.c | 1037 ++++++++++++++++ dlls/winewayland.drv/unixlib.h | 1 + dlls/winewayland.drv/wayland.c | 5 + dlls/winewayland.drv/wayland_output.c | 142 ++- dlls/winewayland.drv/wayland_pointer.c | 157 ++- dlls/winewayland.drv/wayland_surface.c | 437 ++++++- dlls/winewayland.drv/wayland_text_input.c | 144 ++- dlls/winewayland.drv/waylanddrv.h | 103 +- dlls/winewayland.drv/waylanddrv_main.c | 14 + dlls/winewayland.drv/window.c | 833 ++++++++++++- dlls/winewayland.drv/window_surface.c | 23 +- dlls/winex11.drv/xrandr.c | 4 + include/ntuser.h | 1 + include/wine/gdi_driver.h | 45 +- include/wine/vulkan_driver.h | 3 +- 39 files changed, 7139 insertions(+), 357 deletions(-) create mode 100644 dlls/winevulkan/layer.c create mode 100644 dlls/winevulkan/vk_layer.h create mode 100644 dlls/winevulkan/wsi.c create mode 100644 dlls/winevulkan/wsi_private.h create mode 100644 dlls/winewayland.drv/linux-dmabuf-unstable-v1.xml create mode 100644 dlls/winewayland.drv/systray.c diff --git a/dlls/win32u/dce.c b/dlls/win32u/dce.c index ad27cb143ae..14565504a1b 100644 --- a/dlls/win32u/dce.c +++ b/dlls/win32u/dce.c @@ -140,6 +140,544 @@ static void create_offscreen_window_surface( HWND hwnd, const RECT *surface_rect if (previous) window_surface_release( previous ); } +/******************************************************************* + * Cross-process (remote) window surface. + * + * Used when painting a window whose toplevel ancestor belongs to a + * different process, on display drivers without native support for + * cross-process rendering (e.g. Wayland). The painting process writes + * pixels into a named shared-memory section and notifies the process + * owning the toplevel with WM_WINE_FLUSH_REMOTE_SURFACE, which then + * composites the dirty region into its real window surface. + */ + +static void *window_surface_get_color( struct window_surface *surface, BITMAPINFO *info ); + +#define REMOTE_SURFACE_MAX_CACHE 16 +/* round the section/buffer size up to this granularity so that an interactive + * resize, which steps through many pixel sizes, reuses one section instead of + * recreating it (and racing the other process) every frame -> much less flicker. + * The section holds raw pixels only: both sides derive the dimensions and the + * stride from the section name (hwnd + rounded size), never from its content, + * since the content is writable by any process in the session. */ +#define REMOTE_SURFACE_ALIGN 256 +#define REMOTE_SURFACE_ROUND(x) (((x) + REMOTE_SURFACE_ALIGN - 1) & ~(REMOTE_SURFACE_ALIGN - 1)) + +struct remote_surface +{ + struct window_surface header; + struct list entry; /* entry in remote_surfaces list, protected by remote_surfaces_lock */ + HWND top_win; + HANDLE handle; + void *bits; + UINT win_width; /* true (unrounded) toplevel size; header.rect is rounded up */ + UINT win_height; + BOOL local; /* toplevel owned by this process (GL/VK content), no section needed */ +}; + +static struct list remote_surfaces = LIST_INIT( remote_surfaces ); +/* views of remote sections mapped on the toplevel side */ +struct remote_surface_view +{ + struct list entry; + HWND hwnd; + UINT width; + UINT height; + HANDLE handle; + void *bits; + UINT in_use; /* readers currently copying from the mapping */ +}; +static struct list remote_surface_views = LIST_INIT( remote_surface_views ); +static pthread_mutex_t remote_surfaces_lock = PTHREAD_MUTEX_INITIALIZER; + +static struct remote_surface *get_remote_surface_impl( struct window_surface *window_surface ) +{ + return CONTAINING_RECORD( window_surface, struct remote_surface, header ); +} + +static void remote_surface_init_name( WCHAR *buffer, UNICODE_STRING *str, HWND top_win, UINT width, UINT height ) +{ + char tmp[80]; + UINT len; + + len = snprintf( tmp, sizeof(tmp), "\\BaseNamedObjects\\__wine_remote_surface_%08x_%ux%u", + (UINT)(UINT_PTR)top_win, width, height ); + len = min( len, sizeof(tmp) - 1 ); + asciiz_to_unicode( buffer, tmp ); + str->Buffer = buffer; + str->Length = len * sizeof(WCHAR); + str->MaximumLength = (len + 1) * sizeof(WCHAR); +} + +static void remote_surface_set_clip( struct window_surface *window_surface, const RECT *rects, UINT count ) +{ +} + +/* clamp a dirty rect into [0,width)x[0,height); return whether it is still non-empty */ +static BOOL clamp_dirty_rect( RECT *r, UINT width, UINT height ) +{ + if (r->left < 0) r->left = 0; + if (r->top < 0) r->top = 0; + if (r->right > (LONG)width) r->right = width; + if (r->bottom > (LONG)height) r->bottom = height; + return r->right > r->left && r->bottom > r->top; +} + +/* copy the rows of r between two 32-bpp buffers with possibly different strides */ +static void blit_rows( void *dst, int dst_stride, const void *src, int src_stride, const RECT *r ) +{ + UINT width = (r->right - r->left) * 4; + LONG y; + + for (y = r->top; y < r->bottom; y++) + memcpy( (char *)dst + (SIZE_T)y * dst_stride + r->left * 4, + (const char *)src + (SIZE_T)y * src_stride + r->left * 4, width ); +} + +static BOOL remote_surface_flush( struct window_surface *window_surface, const RECT *rect, const RECT *dirty, + const BITMAPINFO *color_info, const void *color_bits, BOOL shape_changed, + const BITMAPINFO *shape_info, const void *shape_bits ) +{ + struct remote_surface *surface = get_remote_surface_impl( window_surface ); + char *dst; + const char *src = color_bits; + UINT src_stride = color_info->bmiHeader.biWidth * 4, dst_width, dst_height; + RECT r = *dirty; + + if (surface->local) + { + /* same-process toplevel with GL/VK content: hand the pixels to the + * driver directly, it will composite them as an overlay. Pass the true + * window size (not the rounded surface rect) so the overlay doesn't + * extend past the window edge; color_bits keeps the rounded DIB stride. */ + TRACE( "local overlay flush for %p dirty %s\n", surface->top_win, wine_dbgstr_rect( &r ) ); + user_driver->pFlushRemoteSurface( surface->top_win, &r, color_bits, src_stride, + surface->win_width, surface->win_height ); + return TRUE; + } + + /* use our own surface dimensions for all bounds: the section content is + * shared memory that other processes can write to */ + dst = surface->bits; + dst_width = window_surface->rect.right - window_surface->rect.left; + dst_height = window_surface->rect.bottom - window_surface->rect.top; + + if (!clamp_dirty_rect( &r, dst_width, dst_height )) return TRUE; + + blit_rows( dst, dst_width * 4, src, src_stride, &r ); + + if (!NtUserPostMessage( surface->top_win, WM_WINE_FLUSH_REMOTE_SURFACE, + MAKELONG( r.left, r.top ), MAKELONG( r.right, r.bottom ) )) + { + WARN( "failed to post remote flush to %p\n", surface->top_win ); + return FALSE; /* keep the dirty bounds so a later flush retries */ + } + TRACE( "posted remote flush to %p dirty %s\n", surface->top_win, wine_dbgstr_rect( &r ) ); + return TRUE; +} + +static void remote_surface_destroy( struct window_surface *window_surface ) +{ + struct remote_surface *surface = get_remote_surface_impl( window_surface ); + if (surface->bits) NtUnmapViewOfSection( GetCurrentProcess(), surface->bits ); + if (surface->handle) NtClose( surface->handle ); +} + +static const struct window_surface_funcs remote_surface_funcs = +{ + remote_surface_set_clip, + remote_surface_flush, + remote_surface_destroy +}; + +/* get a (cached) remote surface for painting a window of another process; returns an extra reference */ +static struct window_surface *get_remote_window_surface( HWND top_win, const RECT *top_rect, BOOL local ) +{ + char info_buffer[FIELD_OFFSET( BITMAPINFO, bmiColors[256] )]; + BITMAPINFO *info = (BITMAPINFO *)info_buffer; + UINT width = REMOTE_SURFACE_ROUND( top_rect->right - top_rect->left ); + UINT height = REMOTE_SURFACE_ROUND( top_rect->bottom - top_rect->top ); + RECT rect = {0, 0, width, height}; + struct remote_surface *remote, *next; + struct window_surface *window_surface; + OBJECT_ATTRIBUTES attr; + UNICODE_STRING name; + WCHAR nameW[80]; + LARGE_INTEGER section_size; + SIZE_T view_size = 0; + void *bits = NULL; + HANDLE handle; + NTSTATUS status; + UINT count = 0; + + if (!width || !height || width > 16384 || height > 16384) return NULL; + + pthread_mutex_lock( &remote_surfaces_lock ); + LIST_FOR_EACH_ENTRY_SAFE( remote, next, &remote_surfaces, struct remote_surface, entry ) + { + if (remote->top_win == top_win) + { + if (EqualRect( &remote->header.rect, &rect ) && remote->local == local) + { + window_surface_add_ref( &remote->header ); + pthread_mutex_unlock( &remote_surfaces_lock ); + return &remote->header; + } + } + else if (is_window( remote->top_win )) continue; + /* drop entries for resized or destroyed windows; they pin large mappings */ + list_remove( &remote->entry ); + register_window_surface( &remote->header, NULL ); + window_surface_release( &remote->header ); + } + pthread_mutex_unlock( &remote_surfaces_lock ); + + if (!local) + { + remote_surface_init_name( nameW, &name, top_win, width, height ); + InitializeObjectAttributes( &attr, &name, OBJ_CASE_INSENSITIVE | OBJ_OPENIF, 0, NULL ); + section_size.QuadPart = (ULONGLONG)width * 4 * height; + status = NtCreateSection( &handle, STANDARD_RIGHTS_REQUIRED | SECTION_QUERY | SECTION_MAP_READ | SECTION_MAP_WRITE, + &attr, §ion_size, PAGE_READWRITE, SEC_COMMIT, NULL ); + /* OBJ_OPENIF: an existing section (other painter / compositing side) + * returns the success code STATUS_OBJECT_NAME_EXISTS */ + if (!NT_SUCCESS( status )) + { + WARN( "failed to create section for %p, status %#x\n", top_win, (UINT)status ); + return NULL; + } + status = NtMapViewOfSection( handle, GetCurrentProcess(), &bits, 0, 0, NULL, + &view_size, ViewShare, 0, PAGE_READWRITE ); + if (!NT_SUCCESS( status )) + { + WARN( "failed to map section for %p, status %#x\n", top_win, (UINT)status ); + NtClose( handle ); + return NULL; + } + if (view_size < (SIZE_T)width * 4 * height) + { + WARN( "stale undersized section for %p\n", top_win ); + NtUnmapViewOfSection( GetCurrentProcess(), bits ); + NtClose( handle ); + return NULL; + } + } + else handle = NULL; + + memset( info, 0, sizeof(*info) ); + info->bmiHeader.biSize = sizeof(info->bmiHeader); + info->bmiHeader.biWidth = width; + info->bmiHeader.biHeight = -(LONG)height; /* top-down */ + info->bmiHeader.biPlanes = 1; + info->bmiHeader.biBitCount = 32; + info->bmiHeader.biSizeImage = get_dib_image_size( info ); + info->bmiHeader.biCompression = BI_RGB; + + if (!(window_surface = window_surface_create( sizeof(*remote), &remote_surface_funcs, top_win, &rect, info, 0 ))) + { + if (bits) NtUnmapViewOfSection( GetCurrentProcess(), bits ); + if (handle) NtClose( handle ); + return NULL; + } + + remote = get_remote_surface_impl( window_surface ); + remote->top_win = top_win; + remote->handle = handle; + remote->bits = bits; + remote->local = local; + /* header.rect is rounded up; keep the true window size for the overlay */ + remote->win_width = top_rect->right - top_rect->left; + remote->win_height = top_rect->bottom - top_rect->top; + + TRACE( "created remote surface %p for top win %p %s\n", window_surface, top_win, wine_dbgstr_rect( &rect ) ); + + /* so flush_window_surfaces() picks it up */ + register_window_surface( NULL, window_surface ); + + pthread_mutex_lock( &remote_surfaces_lock ); + window_surface_add_ref( window_surface ); /* reference owned by the cache */ + list_add_head( &remote_surfaces, &remote->entry ); + count = list_count( &remote_surfaces ); + while (count-- > REMOTE_SURFACE_MAX_CACHE) + { + remote = LIST_ENTRY( list_tail( &remote_surfaces ), struct remote_surface, entry ); + list_remove( &remote->entry ); + register_window_surface( &remote->header, NULL ); + window_surface_release( &remote->header ); + } + pthread_mutex_unlock( &remote_surfaces_lock ); + + return window_surface; +} + +/* unlink a view from the cache and release its mapping, handle and memory */ +static void destroy_remote_surface_view( struct remote_surface_view *view ) +{ + list_remove( &view->entry ); + NtUnmapViewOfSection( GetCurrentProcess(), view->bits ); + NtClose( view->handle ); + free( view ); +} + +/* get a (cached) view of the remote section for a toplevel window we own */ +static void *get_remote_surface_view( HWND hwnd, UINT width, UINT height ) +{ + struct remote_surface_view *view, *next; + OBJECT_ATTRIBUTES attr; + UNICODE_STRING name; + WCHAR nameW[80]; + SIZE_T view_size = 0; + void *bits = NULL; + HANDLE handle; + NTSTATUS status; + UINT count = 0; + + pthread_mutex_lock( &remote_surfaces_lock ); + LIST_FOR_EACH_ENTRY_SAFE( view, next, &remote_surface_views, struct remote_surface_view, entry ) + { + if (view->hwnd == hwnd) + { + if (view->width == width && view->height == height) + { + view->in_use++; + bits = view->bits; + pthread_mutex_unlock( &remote_surfaces_lock ); + return bits; + } + } + else if (is_window( view->hwnd )) continue; + if (view->in_use) continue; + /* drop views for resized or destroyed windows */ + destroy_remote_surface_view( view ); + } + pthread_mutex_unlock( &remote_surfaces_lock ); + + remote_surface_init_name( nameW, &name, hwnd, width, height ); + InitializeObjectAttributes( &attr, &name, OBJ_CASE_INSENSITIVE, 0, NULL ); + status = NtOpenSection( &handle, SECTION_QUERY | SECTION_MAP_READ | SECTION_MAP_WRITE, &attr ); + if (!NT_SUCCESS(status)) + { + TRACE( "no remote section for %p %ux%u, status %#x\n", hwnd, width, height, (UINT)status ); + return NULL; /* painting process not ready yet */ + } + status = NtMapViewOfSection( handle, GetCurrentProcess(), &bits, 0, 0, NULL, + &view_size, ViewShare, 0, PAGE_READWRITE ); + if (!NT_SUCCESS(status)) + { + NtClose( handle ); + return NULL; + } + + /* the name encodes the dimensions, so only a stale leftover section can be + * too small for them */ + if (view_size < (SIZE_T)width * 4 * height) + { + WARN( "invalid remote surface section for %p\n", hwnd ); + NtUnmapViewOfSection( GetCurrentProcess(), bits ); + NtClose( handle ); + return NULL; + } + + if (!(view = malloc( sizeof(*view) ))) + { + NtUnmapViewOfSection( GetCurrentProcess(), bits ); + NtClose( handle ); + return NULL; + } + view->hwnd = hwnd; + view->width = width; + view->height = height; + view->handle = handle; + view->bits = bits; + view->in_use = 1; + + pthread_mutex_lock( &remote_surfaces_lock ); + list_add_head( &remote_surface_views, &view->entry ); + /* First drop views whose window is gone and that nobody is copying from: pure + * dead weight pinning a shared-memory mapping. */ + LIST_FOR_EACH_ENTRY_SAFE( view, next, &remote_surface_views, struct remote_surface_view, entry ) + { + if (view->in_use || is_window( view->hwnd )) continue; + destroy_remote_surface_view( view ); + } + /* Then, if still over the cap, evict the oldest idle views (a cache miss just + * re-opens the section on the next paint). Count and decrement the same set so + * eviction stops at the right point. */ + count = list_count( &remote_surface_views ); + if (count >= REMOTE_SURFACE_MAX_CACHE) + { + LIST_FOR_EACH_ENTRY_SAFE_REV( view, next, &remote_surface_views, struct remote_surface_view, entry ) + { + if (count < REMOTE_SURFACE_MAX_CACHE) break; + if (view->in_use) continue; /* another thread is copying from it */ + destroy_remote_surface_view( view ); + count--; + } + } + pthread_mutex_unlock( &remote_surfaces_lock ); + + return bits; +} + +/* drop the in-use reference taken by get_remote_surface_view */ +static void release_remote_surface_view( void *bits ) +{ + struct remote_surface_view *view; + + pthread_mutex_lock( &remote_surfaces_lock ); + LIST_FOR_EACH_ENTRY( view, &remote_surface_views, struct remote_surface_view, entry ) + { + if (view->bits != bits) continue; + view->in_use--; + break; + } + pthread_mutex_unlock( &remote_surfaces_lock ); +} + +/* Drop cached remote surfaces and views for a destroyed window instead of + * waiting for the next paint to lazily evict them; they pin large shared-memory + * mappings. Covers this process's own toplevel (local overlay surface) and the + * owner-side views of sections it composites. */ +void evict_remote_surfaces( HWND hwnd ) +{ + struct remote_surface *remote, *remote_next; + struct remote_surface_view *view, *view_next; + + pthread_mutex_lock( &remote_surfaces_lock ); + LIST_FOR_EACH_ENTRY_SAFE( remote, remote_next, &remote_surfaces, struct remote_surface, entry ) + { + if (remote->top_win != hwnd) continue; + list_remove( &remote->entry ); + register_window_surface( &remote->header, NULL ); + window_surface_release( &remote->header ); + } + LIST_FOR_EACH_ENTRY_SAFE( view, view_next, &remote_surface_views, struct remote_surface_view, entry ) + { + if (view->hwnd != hwnd || view->in_use) continue; /* in-use views are evicted lazily */ + destroy_remote_surface_view( view ); + } + pthread_mutex_unlock( &remote_surfaces_lock ); +} + +/* WM_WINE_FLUSH_REMOTE_SURFACE handler: composite pixels painted by another + * process into the window surface of a toplevel window we own */ +LRESULT flush_remote_surface( HWND hwnd, WPARAM wparam, LPARAM lparam ) +{ + char info_buffer[FIELD_OFFSET( BITMAPINFO, bmiColors[256] )]; + BITMAPINFO *info = (BITMAPINFO *)info_buffer; + struct window_surface *surface = NULL; + const char *src; + char *dst; + UINT width, height, dst_stride, src_stride; + RECT dirty; + void *bits = NULL; + WND *win; + + dirty.left = (short)LOWORD( wparam ); + dirty.top = (short)HIWORD( wparam ); + dirty.right = (short)LOWORD( lparam ); + dirty.bottom = (short)HIWORD( lparam ); + + TRACE( "hwnd %p dirty %s\n", hwnd, wine_dbgstr_rect( &dirty ) ); + + if (!(win = get_win_ptr( hwnd )) || win == WND_OTHER_PROCESS || win == WND_DESKTOP) return FALSE; + if ((surface = win->surface)) window_surface_add_ref( surface ); + release_win_ptr( win ); + + if (!surface) + { + TRACE( "no surface for %p\n", hwnd ); + return FALSE; + } + if (surface == &dummy_surface) + { + /* GL/VK toplevel: no usable window surface, present through a + * driver overlay surface instead */ + UINT raw_dpi, monitor_dpi; + RECT rect; + window_surface_release( surface ); + surface = NULL; + /* The painter sized the section from this toplevel's monitor-DPI rect: + * a cross-process NtUserGetWindowRect resolves dpi 0 to the window's + * monitor DPI server-side. We are the window's own process, where dpi 0 + * means the (possibly different) per-window DPI, so query in monitor DPI + * explicitly -- otherwise the rounded section name would not match and we + * would never find the painter's section. The painter's rect is in + * native/server space, so also map our (possibly mode override scaled) + * view out to native space. */ + monitor_dpi = get_win_monitor_dpi( hwnd, &raw_dpi ); + if (!get_window_rect( hwnd, &rect, monitor_dpi )) return FALSE; + rect = map_rect_virt_to_raw( rect, monitor_dpi ); + width = rect.right - rect.left; + height = rect.bottom - rect.top; + if (!(bits = get_remote_surface_view( hwnd, REMOTE_SURFACE_ROUND( width ), + REMOTE_SURFACE_ROUND( height ) ))) return FALSE; + if (clamp_dirty_rect( &dirty, width, height )) + { + /* stride is the rounded section width (encoded in the section name) */ + user_driver->pFlushRemoteSurface( hwnd, &dirty, bits, + REMOTE_SURFACE_ROUND( width ) * 4, width, height ); + } + release_remote_surface_view( bits ); + return TRUE; + } + + /* Under a mode override the window surface may be a scaled wrapper living in + * this process's virtual space, while the painter sized the section (and its + * name) against the native driver surface rect the server reports. Composite + * into the underlying driver surface: its dimensions match the section and + * the painter's pixels are already in native space, so no rescaling is + * needed. Without an override this is a no-op (the surfaces are the same). */ + { + UINT raw_dpi; + struct window_surface *driver_surface; + + /* the scaled wrapper's target is the driver surface in raw DPI (see + * create_window_surface), so resolve it with the raw DPI */ + get_win_monitor_dpi( hwnd, &raw_dpi ); + driver_surface = get_driver_window_surface( surface, raw_dpi ); + + if (driver_surface != surface) + { + if (!driver_surface || driver_surface == &dummy_surface) + { + /* scaling surface in transition, retry on a later flush */ + window_surface_release( surface ); + return FALSE; + } + window_surface_add_ref( driver_surface ); + window_surface_release( surface ); + surface = driver_surface; + } + } + + width = surface->rect.right - surface->rect.left; + height = surface->rect.bottom - surface->rect.top; + /* the section is allocated at the rounded size (see get_remote_window_surface) */ + src_stride = REMOTE_SURFACE_ROUND( width ) * 4; + if (!(bits = get_remote_surface_view( hwnd, REMOTE_SURFACE_ROUND( width ), + REMOTE_SURFACE_ROUND( height ) ))) goto done; + src = bits; + + if (!clamp_dirty_rect( &dirty, width, height )) goto done; + + window_surface_lock( surface ); + if ((dst = window_surface_get_color( surface, info )) && + info->bmiHeader.biBitCount == 32 && info->bmiHeader.biWidth >= (LONG)width) + { + dst_stride = info->bmiHeader.biWidth * 4; + blit_rows( dst, dst_stride, src, src_stride, &dirty ); + add_bounds_rect( &surface->bounds, &dirty ); + } + window_surface_unlock( surface ); + window_surface_flush( surface ); + +done: + if (bits) release_remote_surface_view( bits ); + window_surface_release( surface ); + return TRUE; +} + struct scaled_surface { struct window_surface header; @@ -806,6 +1344,29 @@ static void dump_rdw_flags(UINT flags) #undef RDW_FLAGS } +/* map a region in place from native/raw space to this process's virtual view */ +static void map_region_raw_to_virt( HRGN hrgn, UINT dpi ) +{ + RGNDATA *data; + HRGN mapped; + RECT *rects; + DWORD i, size; + + if (!(size = NtGdiGetRegionData( hrgn, 0, NULL ))) return; + if (!(data = malloc( size ))) return; + if (NtGdiGetRegionData( hrgn, size, data )) + { + rects = (RECT *)data->Buffer; + for (i = 0; i < data->rdh.nCount; i++) rects[i] = map_rect_raw_to_virt( rects[i], dpi ); + if ((mapped = NtGdiExtCreateRegion( NULL, size, data ))) + { + NtGdiCombineRgn( hrgn, mapped, 0, RGN_COPY ); + NtGdiDeleteObjectApp( mapped ); + } + } + free( data ); +} + /*********************************************************************** * update_visible_region * @@ -815,6 +1376,7 @@ static void dump_rdw_flags(UINT flags) static void update_visible_region( struct dce *dce ) { struct window_surface *surface = NULL; + BOOL native_surface = FALSE; NTSTATUS status; HRGN vis_rgn = 0; HWND top_win = 0; @@ -861,9 +1423,6 @@ static void update_visible_region( struct dce *dce ) user_driver->pGetDC( dce->hdc, dce->hwnd, top_win, &win_rect, &top_rect, flags ); - if (dce->clip_rgn) NtGdiCombineRgn( vis_rgn, vis_rgn, dce->clip_rgn, - (flags & DCX_INTERSECTRGN) ? RGN_AND : RGN_DIFF ); - /* don't use a surface to paint the client area of OpenGL windows */ if (!(paint_flags & SET_WINPOS_PIXEL_FORMAT && user_driver->dc_funcs.pPutImage) || (flags & DCX_WINDOW)) { @@ -873,9 +1432,64 @@ static void update_visible_region( struct dce *dce ) surface = win->surface; if (surface) window_surface_add_ref( surface ); release_win_ptr( win ); + + if (surface && dce->hwnd != top_win && user_driver->pWantsCrossProcessSurfaces() && + (surface == &dummy_surface || user_driver->pNeedsPaintOverlay( top_win ))) + { + /* The toplevel's window surface is unusable (GL/VK content) or + * obscured by a client surface plane; paint children through a + * local overlay surface composited by the display driver. + * Match the owner-side handler in flush_remote_surface: in our + * own process dpi 0 resolves to the per-window DPI, so query in + * monitor DPI explicitly and map out to native space with the + * same DPI, otherwise the rounded section size (and name) would + * not match the owner's. */ + UINT raw_dpi, monitor_dpi = get_win_monitor_dpi( top_win, &raw_dpi ); + window_surface_release( surface ); + if (get_window_rect( top_win, &top_rect, monitor_dpi )) + { + top_rect = map_rect_virt_to_raw( top_rect, monitor_dpi ); + surface = get_remote_window_surface( top_win, &top_rect, TRUE ); + } + else surface = NULL; + native_surface = TRUE; + TRACE( "local overlay surface %p for hwnd %p top %p top_rect %s\n", + surface, dce->hwnd, top_win, wine_dbgstr_rect( &top_rect ) ); + } + } + else if (win == WND_OTHER_PROCESS && user_driver->pWantsCrossProcessSurfaces()) + { + /* if the toplevel has no real window surface (e.g. GL/VK content), + * its server-side surface rect is a 1x1 dummy; fall back to the + * toplevel window rect, the owner will composite via an overlay */ + if (top_rect.right - top_rect.left <= 1 && top_rect.bottom - top_rect.top <= 1) + NtUserGetWindowRect( top_win, &top_rect, 0 /* raw, same space as server rects */ ); + + /* the driver cannot render across processes; paint through a + * shared-memory surface composited by the toplevel's owner */ + surface = get_remote_window_surface( top_win, &top_rect, FALSE ); + native_surface = TRUE; + TRACE( "remote surface %p for hwnd %p top %p top_rect %s\n", + surface, dce->hwnd, top_win, wine_dbgstr_rect( &top_rect ) ); } } + if (surface && !native_surface && process_has_mode_overrides()) + { + /* Under a per-process mode override the server reply is in native + * space (apply_window_pos maps the rects it sends), while the DC and + * the window surface live in this process's virtual view: map the + * reply back. The overlay and shared-section surfaces above stay in + * native space on purpose, matching the server-side sizing. */ + UINT dpi = get_dpi_for_window( top_win ); + win_rect = map_rect_raw_to_virt( win_rect, dpi ); + top_rect = map_rect_raw_to_virt( top_rect, dpi ); + map_region_raw_to_virt( vis_rgn, dpi ); + } + + if (dce->clip_rgn) NtGdiCombineRgn( vis_rgn, vis_rgn, dce->clip_rgn, + (flags & DCX_INTERSECTRGN) ? RGN_AND : RGN_DIFF ); + if (!surface) SetRectEmpty( &top_rect ); set_visible_region( dce->hdc, vis_rgn, &win_rect, &top_rect, surface ); if (surface) window_surface_release( surface ); diff --git a/dlls/win32u/driver.c b/dlls/win32u/driver.c index 27259fd8506..6bc11e43193 100644 --- a/dlls/win32u/driver.c +++ b/dlls/win32u/driver.c @@ -684,6 +684,11 @@ static BOOL nulldrv_SetIMECompositionRect( HWND hwnd, RECT rect ) return FALSE; } +static BOOL nulldrv_SetIMEEnabled( HWND hwnd, BOOL enabled ) +{ + return FALSE; +} + static LRESULT nulldrv_DesktopWindowProc( HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam ) { return default_window_proc( hwnd, msg, wparam, lparam, FALSE ); @@ -947,6 +952,26 @@ static void nulldrv_ThreadDetach( void ) { } +static BOOL nulldrv_WantsCrossProcessSurfaces(void) +{ + return FALSE; +} + +static void nulldrv_FlushRemoteSurface( HWND hwnd, const RECT *dirty, const void *bits, + UINT stride, UINT width, UINT height ) +{ +} + +static void nulldrv_FlushRemoteSurfaceDMABUF( HWND hwnd, HANDLE handle, UINT64 modifier, + UINT width, UINT height, UINT stride ) +{ +} + +static BOOL nulldrv_NeedsPaintOverlay( HWND hwnd ) +{ + return FALSE; +} + static const WCHAR guid_key_prefixW[] = { '\\','R','e','g','i','s','t','r','y', @@ -1141,6 +1166,11 @@ static BOOL loaderdrv_SetIMECompositionRect( HWND hwnd, RECT rect ) return load_driver()->pSetIMECompositionRect( hwnd, rect ); } +static BOOL loaderdrv_SetIMEEnabled( HWND hwnd, BOOL enabled ) +{ + return load_driver()->pSetIMEEnabled( hwnd, enabled ); +} + static LONG loaderdrv_ChangeDisplaySettings( LPDEVMODEW displays, LPCWSTR primary_name, HWND hwnd, DWORD flags, LPVOID lparam ) { @@ -1286,6 +1316,7 @@ static const struct user_driver_funcs lazy_load_driver = loaderdrv_ImeToAsciiEx, loaderdrv_NotifyIMEStatus, loaderdrv_SetIMECompositionRect, + loaderdrv_SetIMEEnabled, /* cursor/icon functions */ nulldrv_DestroyCursorIcon, loaderdrv_SetCursor, @@ -1345,6 +1376,11 @@ static const struct user_driver_funcs lazy_load_driver = loaderdrv_OpenGLInit, /* thread management */ nulldrv_ThreadDetach, + /* cross-process rendering */ + nulldrv_WantsCrossProcessSurfaces, + nulldrv_FlushRemoteSurface, + nulldrv_FlushRemoteSurfaceDMABUF, + nulldrv_NeedsPaintOverlay, }; const struct user_driver_funcs *user_driver = &lazy_load_driver; @@ -1391,6 +1427,7 @@ void __wine_set_user_driver( const struct user_driver_funcs *funcs, UINT version SET_USER_FUNC(ImeToAsciiEx); SET_USER_FUNC(NotifyIMEStatus); SET_USER_FUNC(SetIMECompositionRect); + SET_USER_FUNC(SetIMEEnabled); SET_USER_FUNC(DestroyCursorIcon); SET_USER_FUNC(SetCursor); SET_USER_FUNC(GetCursorPos); @@ -1440,6 +1477,10 @@ void __wine_set_user_driver( const struct user_driver_funcs *funcs, UINT version SET_USER_FUNC(VulkanInit); SET_USER_FUNC(OpenGLInit); SET_USER_FUNC(ThreadDetach); + SET_USER_FUNC(WantsCrossProcessSurfaces); + SET_USER_FUNC(FlushRemoteSurface); + SET_USER_FUNC(FlushRemoteSurfaceDMABUF); + SET_USER_FUNC(NeedsPaintOverlay); #undef SET_USER_FUNC prev = InterlockedCompareExchangePointer( (void **)&user_driver, driver, (void *)&lazy_load_driver ); diff --git a/dlls/win32u/imm.c b/dlls/win32u/imm.c index 03c454d7377..43f7773da70 100644 --- a/dlls/win32u/imm.c +++ b/dlls/win32u/imm.c @@ -185,6 +185,7 @@ UINT WINAPI NtUserAssociateInputContext( HWND hwnd, HIMC ctx, ULONG flags ) { WND *win; UINT ret = AICR_OK; + BOOL ime_enabled; TRACE( "%p %p %x\n", hwnd, ctx, flags ); @@ -218,8 +219,20 @@ UINT WINAPI NtUserAssociateInputContext( HWND hwnd, HIMC ctx, ULONG flags ) if (win->imc != ctx && get_focus() == hwnd) ret = AICR_FOCUS_CHANGED; win->imc = ctx; } + ime_enabled = win->imc != NULL; release_win_ptr( win ); + + /* Tell the driver this window's IME got activated or deactivated, so a driver + * (e.g. winewayland) can enable/disable compositor text input live -- not only + * on a Wayland focus change. Unconditional so the driver can track every child + * window's IME state and aggregate per toplevel (an app may associate the + * context on a child control, not the toplevel). Reports the window's resulting + * context state, which an IACE_IGNORENOCONTEXT call may leave unchanged rather + * than the requested ctx. Done after release_win_ptr so the call does not run + * under the window lock. */ + user_driver->pSetIMEEnabled( hwnd, ime_enabled ); + return ret; } diff --git a/dlls/win32u/message.c b/dlls/win32u/message.c index b6cdd3efa0e..868e6e66f5f 100644 --- a/dlls/win32u/message.c +++ b/dlls/win32u/message.c @@ -2264,6 +2264,8 @@ static LRESULT handle_internal_message( HWND hwnd, UINT msg, WPARAM wparam, LPAR case WM_WINE_SETPIXELFORMAT: set_window_pixel_format( hwnd, wparam, lparam ); return 0; + case WM_WINE_FLUSH_REMOTE_SURFACE: + return flush_remote_surface( hwnd, wparam, lparam ); case WM_WINE_TRACKMOUSEEVENT: { TRACKMOUSEEVENT info; diff --git a/dlls/win32u/ntuser_private.h b/dlls/win32u/ntuser_private.h index dc14811ce86..209c69d3eed 100644 --- a/dlls/win32u/ntuser_private.h +++ b/dlls/win32u/ntuser_private.h @@ -200,6 +200,8 @@ HICON alloc_cursoricon_handle( BOOL is_icon ); /* dce.c */ extern void free_dce( struct dce *dce, HWND hwnd, struct list *drawables ); extern void invalidate_dce( WND *win, const RECT *old_rect ); +extern LRESULT flush_remote_surface( HWND hwnd, WPARAM wparam, LPARAM lparam ); +extern void evict_remote_surfaces( HWND hwnd ); extern BOOL is_cache_dc( HDC hdc ); /* message.c */ diff --git a/dlls/win32u/spy.c b/dlls/win32u/spy.c index c86d7944257..dd87f606b45 100644 --- a/dlls/win32u/spy.c +++ b/dlls/win32u/spy.c @@ -1127,7 +1127,7 @@ static const char * const CCMMessageTypeNames[SPY_MAX_CCMMSGNUM + 1] = "CCM_SETNOTIFYWINDOW" }; -#define SPY_MAX_WINEMSGNUM (WM_WINE_SETPIXELFORMAT - WM_WINE_DESTROYWINDOW) +#define SPY_MAX_WINEMSGNUM (WM_WINE_FLUSH_REMOTE_SURFACE - WM_WINE_DESTROYWINDOW) static const char * const WINEMessageTypeNames[SPY_MAX_WINEMSGNUM + 1] = { "WM_WINE_DESTROYWINDOW", @@ -1144,6 +1144,7 @@ static const char * const WINEMessageTypeNames[SPY_MAX_WINEMSGNUM + 1] = "WM_WINE_UPDATEWINDOWSTATE", "WM_WINE_TRACKMOUSEEVENT", "WM_WINE_SETPIXELFORMAT", + "WM_WINE_FLUSH_REMOTE_SURFACE", }; #define SPY_MAX_WINE_DRIVER_MSGNUM (WM_WINE_SETCURSOR - WM_WINE_CLIPCURSOR) diff --git a/dlls/win32u/sysparams.c b/dlls/win32u/sysparams.c index 239d13df6cc..131ba9a142a 100644 --- a/dlls/win32u/sysparams.c +++ b/dlls/win32u/sysparams.c @@ -123,25 +123,15 @@ struct source UINT state_flags; UINT monitor_count; UINT mode_count; + BOOL virtual_modes; /* the mode list is synthesized by win32u, the driver can't do real mode switches */ DEVMODEW current; DEVMODEW physical; DEVMODEW *modes; }; -#define MONITOR_INFO_HAS_MONITOR_ID 0x00000001 -#define MONITOR_INFO_HAS_MONITOR_NAME 0x00000002 -#define MONITOR_INFO_HAS_PREFERRED_MODE 0x00000004 -struct edid_monitor_info -{ - unsigned int flags; - /* MONITOR_INFO_HAS_MONITOR_ID */ - unsigned short manufacturer, product_code; - char monitor_id_string[8]; - /* MONITOR_INFO_HAS_MONITOR_NAME */ - WCHAR monitor_name[14]; - /* MONITOR_INFO_HAS_PREFERRED_MODE */ - unsigned int preferred_width, preferred_height; -}; +/* struct edid_monitor_info and the MONITOR_INFO_HAS_* flags now live in + * wine/gdi_driver.h so drivers can provide parsed info for win32u to convert to a + * binary EDID (see build_edid_from_info). */ struct monitor { @@ -166,6 +156,195 @@ static UINT64 monitor_update_serial; static pthread_mutex_t display_lock = PTHREAD_MUTEX_INITIALIZER; static BOOL emulate_modeset; + +/* Per-process emulated display mode overrides. + * + * Sources that cannot do real mode switches (the host exposes a single mode and + * win32u synthesizes the mode list, e.g. winewayland) don't change any global + * state when an app calls ChangeDisplaySettings. Instead the requested mode is + * recorded here, only for the requesting process, similar to Windows fullscreen + * optimizations: the process sees the emulated mode in all metrics and lives in + * a coherently scaled coordinate space, while the wineserver, the drivers and + * every other process keep working with the native desktop. */ +struct mode_override +{ + struct list entry; + unsigned int source_id; + DEVMODEW mode; +}; + +static struct list mode_overrides = LIST_INIT( mode_overrides ); +static pthread_mutex_t mode_override_lock = PTHREAD_MUTEX_INITIALIZER; +static LONG mode_override_count; /* fast lock-free emptiness check; written only under mode_override_lock */ + +static BOOL get_source_mode_override( unsigned int source_id, DEVMODEW *mode ) +{ + struct mode_override *override; + BOOL ret = FALSE; + + if (!ReadNoFence( &mode_override_count )) return FALSE; + + pthread_mutex_lock( &mode_override_lock ); + LIST_FOR_EACH_ENTRY( override, &mode_overrides, struct mode_override, entry ) + { + if (override->source_id != source_id) continue; + /* copy only the settings portion, preserving the caller's header fields */ + if (mode) memcpy( &mode->dmFields, &override->mode.dmFields, + offsetof(DEVMODEW, dmICMMethod) - offsetof(DEVMODEW, dmFields) ); + ret = TRUE; + break; + } + pthread_mutex_unlock( &mode_override_lock ); + return ret; +} + +/* set (or with mode == NULL remove) this process's mode override for a source */ +static void set_source_mode_override( unsigned int source_id, const DEVMODEW *mode ) +{ + struct mode_override *override, *found = NULL; + + pthread_mutex_lock( &mode_override_lock ); + LIST_FOR_EACH_ENTRY( override, &mode_overrides, struct mode_override, entry ) + if (override->source_id == source_id) { found = override; break; } + + if (!mode) + { + if (found) + { + list_remove( &found->entry ); + free( found ); + mode_override_count--; + } + } + else + { + if (!found && (found = calloc( 1, sizeof(*found) ))) + { + found->source_id = source_id; + list_add_tail( &mode_overrides, &found->entry ); + mode_override_count++; + } + if (found) found->mode = *mode; + } + pthread_mutex_unlock( &mode_override_lock ); +} + +static BOOL remove_all_mode_overrides(void) +{ + struct mode_override *override, *next; + BOOL removed = FALSE; + + if (!ReadNoFence( &mode_override_count )) return FALSE; + + pthread_mutex_lock( &mode_override_lock ); + LIST_FOR_EACH_ENTRY_SAFE( override, next, &mode_overrides, struct mode_override, entry ) + { + list_remove( &override->entry ); + free( override ); + mode_override_count--; + removed = TRUE; + } + pthread_mutex_unlock( &mode_override_lock ); + return removed; +} + +BOOL process_has_mode_overrides(void) +{ + return !!ReadNoFence( &mode_override_count ); +} + +/* Per-process view positions of all sources. A mode override shrinks or grows + * one monitor in the process's view; like a real mode change re-places the + * global layout, the view layout is re-placed around the emulated modes so the + * process never sees gaps or overlaps between monitors. Rebuilt by + * update_view_positions() whenever the overrides or the global modes change; + * non-empty only while mode_overrides is. */ +struct view_position +{ + struct list entry; /* entry in view_positions, protected by mode_override_lock */ + unsigned int source_id; + POINTL position; +}; + +static struct list view_positions = LIST_INIT( view_positions ); + +static BOOL get_source_view_position( unsigned int source_id, POINTL *position ) +{ + struct view_position *view; + BOOL ret = FALSE; + + if (!ReadNoFence( &mode_override_count )) return FALSE; + + pthread_mutex_lock( &mode_override_lock ); + LIST_FOR_EACH_ENTRY( view, &view_positions, struct view_position, entry ) + { + if (view->source_id != source_id) continue; + *position = view->position; + ret = TRUE; + break; + } + pthread_mutex_unlock( &mode_override_lock ); + return ret; +} + +static const DEVMODEW *find_display_mode( const DEVMODEW *modes, DEVMODEW *devmode ); +static void update_view_positions(void); + +/* Drop overrides that no longer match the display configuration: the source id + * is gone (sources were re-enumerated and the id now maps to nothing), the + * source is no longer mode-emulated, or the host mode changed so the recorded + * mode no longer exists in the rebuilt mode list. Without this an override + * could attach to a different source that inherited the id, or keep scaling + * from a mode the host cannot display anymore. display_lock must be held. */ +static void validate_mode_overrides(void) +{ + struct mode_override *override, *next; + struct source *source; + + if (!ReadNoFence( &mode_override_count )) return; + + pthread_mutex_lock( &mode_override_lock ); + LIST_FOR_EACH_ENTRY_SAFE( override, next, &mode_overrides, struct mode_override, entry ) + { + BOOL valid = FALSE; + + LIST_FOR_EACH_ENTRY( source, &sources, struct source, entry ) + { + if (source->id != override->source_id) continue; + valid = source->virtual_modes && find_display_mode( source->modes, &override->mode ); + break; + } + if (valid) continue; + + WARN( "dropping stale mode override for source %u\n", override->source_id ); + list_remove( &override->entry ); + free( override ); + mode_override_count--; + } + pthread_mutex_unlock( &mode_override_lock ); + + /* the global modes may have changed too; re-place the view layout */ + update_view_positions(); +} + +/* Get a source's current and physical mode in the view of the calling process + * (process_view TRUE), or the global/native view shared with the wineserver and + * other processes (process_view FALSE). */ +static void source_get_view_modes( struct source *source, BOOL process_view, DEVMODEW *current, DEVMODEW *physical ) +{ + if (current) + { + *current = source->current; + if (source->depth) current->dmBitsPerPel = source->depth; + } + if (physical) *physical = source->physical; + if (!process_view || !current) return; + + get_source_mode_override( source->id, current ); + /* while overrides exist the view layout is re-placed around the emulated + * modes, moving other monitors too, so the view has no gaps or overlaps */ + get_source_view_position( source->id, ¤t->dmPosition ); +} BOOL decorated_mode = TRUE; UINT64 thunk_lock_callback = 0; @@ -395,6 +574,82 @@ static void monitor_release( struct monitor *monitor ) C_ASSERT(sizeof(DEVMODEW) - offsetof(DEVMODEW, dmFields) == 0x94); +/* Build a minimal valid EDID 1.3 from driver-provided parsed info, for drivers + * (e.g. winewayland, headless/virtual outputs) that have no hardware EDID. What + * is written here is exactly what get_monitor_info_from_edid() below reads back, + * so the synthesized monitor id / name / preferred mode stay consistent. */ +static void build_edid_from_info( const struct edid_monitor_info *info, unsigned char edid[128] ) +{ + int w = info->preferred_width ? info->preferred_width : 1024; + int h = info->preferred_height ? info->preferred_height : 768; + int refresh_mhz = info->preferred_refresh ? info->preferred_refresh : 60000; + int phys_w_mm = info->width_mm ? info->width_mm : 520; + int phys_h_mm = info->height_mm ? info->height_mm : 320; + int hblank = 160, vblank = 35, pixclk; + unsigned int sum = 0, i; + + /* The detailed-timing descriptor encodes only 12 bits of horizontal/vertical + * active pixels; clamp so an 8K/ultrawide preferred mode doesn't wrap to a + * bogus size (this is only a hint, consumed by DISPLAYCONFIG_TARGET_PREFERRED_MODE). */ + if (w > 4095) w = 4095; + if (h > 4095) h = 4095; + + memset( edid, 0, 128 ); + /* header */ + for (i = 1; i <= 6; i++) edid[i] = 0xFF; + /* manufacturer (packed big-endian PNP id), product code (little-endian), serial */ + edid[8] = (info->manufacturer >> 8) & 0xFF; edid[9] = info->manufacturer & 0xFF; + edid[10] = info->product_code & 0xFF; edid[11] = (info->product_code >> 8) & 0xFF; + edid[12] = info->serial_number & 0xFF; edid[13] = (info->serial_number >> 8) & 0xFF; + edid[14] = (info->serial_number >> 16) & 0xFF; edid[15] = (info->serial_number >> 24) & 0xFF; + edid[16] = 1; edid[17] = 34; /* week / year (2024) */ + edid[18] = 1; edid[19] = 3; /* EDID 1.3 */ + /* digital, max image size in cm, gamma 2.2, RGB */ + edid[20] = 0x80; + edid[21] = min( 255, (phys_w_mm + 5) / 10 ); + edid[22] = min( 255, (phys_h_mm + 5) / 10 ); + edid[23] = 120; edid[24] = 0x0A; + /* chromaticity (generic sRGB-ish, but valid) */ + edid[25] = 0xEE; edid[26] = 0x91; edid[27] = 0xA3; edid[28] = 0x54; edid[29] = 0x4C; + edid[30] = 0x99; edid[31] = 0x26; edid[32] = 0x0F; edid[33] = 0x50; edid[34] = 0x54; + /* established + standard timings: unused */ + for (i = 38; i < 54; i++) edid[i] = 0x01; + /* descriptor 1: preferred detailed timing = preferred mode (pixel clock in + * 10 kHz units; refresh is mHz, so divide the product by 1e7) */ + pixclk = (int)(((long long)(w + hblank) * (h + vblank) * refresh_mhz) / 10000000); + if (pixclk < 1) pixclk = 1; + if (pixclk > 0xFFFF) pixclk = 0xFFFF; + edid[54] = pixclk & 0xFF; edid[55] = (pixclk >> 8) & 0xFF; + edid[56] = w & 0xFF; edid[57] = hblank & 0xFF; + edid[58] = (((w >> 8) & 0xF) << 4) | ((hblank >> 8) & 0xF); + edid[59] = h & 0xFF; edid[60] = vblank & 0xFF; + edid[61] = (((h >> 8) & 0xF) << 4) | ((vblank >> 8) & 0xF); + edid[62] = 48; edid[63] = 32; edid[64] = 0x55; + edid[66] = phys_w_mm & 0xFF; edid[67] = phys_h_mm & 0xFF; + edid[68] = (((phys_w_mm >> 8) & 0xF) << 4) | ((phys_h_mm >> 8) & 0xF); + edid[71] = 0x1E; + /* descriptor 2: monitor name (0xFC), up to 13 chars, 0x0A-terminated + padded */ + edid[75] = 0xFC; + { + int n = 0; + for (; n < 13 && info->monitor_name[n]; n++) + edid[77 + n] = (unsigned char)info->monitor_name[n]; + if (n < 13) + { + edid[77 + n] = 0x0A; + for (i = 77 + n + 1; i < 90; i++) edid[i] = 0x20; + } + } + /* descriptors 3, 4: dummy */ + edid[93] = 0x10; + for (i = 95; i < 108; i++) edid[i] = 0x20; + edid[111] = 0x10; + for (i = 113; i < 126; i++) edid[i] = 0x20; + /* checksum: bytes 0..127 sum to 0 mod 256 */ + for (i = 0; i < 127; i++) sum += edid[i]; + edid[127] = (256 - (sum & 0xFF)) & 0xFF; +} + static void get_monitor_info_from_edid( struct edid_monitor_info *info, const unsigned char *edid, unsigned int edid_len ) { unsigned int i, j; @@ -708,7 +963,10 @@ static BOOL read_source_from_registry( unsigned int index, struct source *source source->current.dmSize = sizeof(source->current); source->physical = source->current; if (read_source_mode( hkey, WINE_ENUM_PHYSICAL_SETTINGS, &source->physical )) + { source->physical.dmSize = sizeof(source->physical); + source->virtual_modes = TRUE; + } /* DeviceID */ size = query_reg_ascii_value( hkey, "GPUID", value, sizeof(buffer) ); @@ -1985,6 +2243,9 @@ static void add_monitor( const struct gdi_monitor *gdi_monitor, void *param ) struct source *source; char buffer[MAX_PATH]; char monitor_id_string[16]; + const unsigned char *edid = gdi_monitor->edid; + UINT edid_len = gdi_monitor->edid_len; + unsigned char built_edid[128]; assert( !list_empty( &sources ) ); source = LIST_ENTRY( list_tail( &sources ), struct source, entry ); @@ -1999,7 +2260,17 @@ static void add_monitor( const struct gdi_monitor *gdi_monitor, void *param ) TRACE( "%u %s %s\n", monitor->id, wine_dbgstr_rect(&gdi_monitor->rc_monitor), wine_dbgstr_rect(&gdi_monitor->rc_work) ); - get_monitor_info_from_edid( &monitor->edid_info, gdi_monitor->edid, gdi_monitor->edid_len ); + /* When the driver has no hardware EDID but provided parsed info, synthesize a + * binary EDID from it so the registry value and the parsed monitor info come + * from one canonical form (built here, parsed back just below). */ + if ((!edid || !edid_len) && gdi_monitor->edid_info) + { + build_edid_from_info( gdi_monitor->edid_info, built_edid ); + edid = built_edid; + edid_len = sizeof(built_edid); + } + + get_monitor_info_from_edid( &monitor->edid_info, edid, edid_len ); if (monitor->edid_info.flags & MONITOR_INFO_HAS_MONITOR_ID) strcpy( monitor_id_string, monitor->edid_info.monitor_id_string ); else @@ -2009,7 +2280,7 @@ static void add_monitor( const struct gdi_monitor *gdi_monitor, void *param ) snprintf( monitor->path, sizeof(monitor->path), "DISPLAY\\%s\\%04X&%04X", monitor_id_string, source->id, monitor->id ); set_reg_ascii_value( source->key, buffer, monitor->path ); - if (!write_monitor_to_registry( monitor, gdi_monitor->edid, gdi_monitor->edid_len )) + if (!write_monitor_to_registry( monitor, edid, edid_len )) { WARN( "Failed to write monitor %p to registry\n", monitor ); monitor_release( monitor ); @@ -2202,7 +2473,16 @@ static void add_modes( const DEVMODEW *current, UINT host_modes_count, const DEV } else { - if (!read_source_mode( source->key, ENUM_CURRENT_SETTINGS, &virtual ) || is_detached_mode( &virtual )) + /* Sources with a synthesized mode list get per-process emulated mode + * changes: the global view must always track the host mode. A mode + * recorded in the registry by a previous session (a different host + * resolution, another compositor, a different gamescope -w/-h) may not + * even exist in the new mode list, and adopting it would make every + * process see a desktop the host cannot display. Only EmulateModeset + * keeps its global, registry-backed semantics. */ + if (!emulate_modeset || + !read_source_mode( source->key, ENUM_CURRENT_SETTINGS, &virtual ) || + is_detached_mode( &virtual )) virtual = physical; if ((virtual_modes = get_virtual_modes( current, &physical, host_modes, host_modes_count, &virtual_count ))) @@ -2223,6 +2503,7 @@ static void add_modes( const DEVMODEW *current, UINT host_modes_count, const DEV set_reg_value( source->key, modesW, REG_BINARY, modes, modes_count * sizeof(*modes) ); set_reg_value( source->key, mode_countW, REG_DWORD, &modes_count, sizeof(modes_count) ); source->mode_count = modes_count; + source->virtual_modes = virtual_modes != NULL; source->current = *current; source->physical = physical; @@ -2282,29 +2563,34 @@ static BOOL is_monitor_primary( struct monitor *monitor ) return !!(source->state_flags & DISPLAY_DEVICE_PRIMARY_DEVICE); } -/* display_lock must be held */ +/* display_lock must be held; the calling process's view (with its mode overrides) */ static void monitor_virt_to_raw_ratio( struct monitor *monitor, UINT *num, UINT *den ) { + DEVMODEW current, physical; struct source *source = monitor->source; *num = *den = 1; if (!source) return; - if (source->physical.dmPelsWidth * source->current.dmPelsHeight <= - source->physical.dmPelsHeight * source->current.dmPelsWidth) + source_get_view_modes( source, TRUE, ¤t, &physical ); + + if (physical.dmPelsWidth * current.dmPelsHeight <= + physical.dmPelsHeight * current.dmPelsWidth) { - *num = source->physical.dmPelsWidth; - *den = source->current.dmPelsWidth; + *num = physical.dmPelsWidth; + *den = current.dmPelsWidth; } else { - *num = source->physical.dmPelsHeight; - *den = source->current.dmPelsHeight; + *num = physical.dmPelsHeight; + *den = current.dmPelsHeight; } } -/* display_lock must be held */ -static UINT monitor_get_dpi( struct monitor *monitor, MONITOR_DPI_TYPE type, UINT *dpi_x, UINT *dpi_y ) +/* display_lock must be held; process_view selects between the calling process's + * view (with its emulated mode overrides) and the global/native view shared + * with the wineserver. For MDT_EFFECTIVE_DPI the views are identical. */ +static UINT monitor_get_dpi_view( struct monitor *monitor, MONITOR_DPI_TYPE type, BOOL process_view, UINT *dpi_x, UINT *dpi_y ) { struct source *source = monitor->source; float scale_x = 1.0, scale_y = 1.0; @@ -2313,8 +2599,10 @@ static UINT monitor_get_dpi( struct monitor *monitor, MONITOR_DPI_TYPE type, UIN if (!source || !(dpi = source->dpi)) dpi = system_dpi; if (source && type != MDT_EFFECTIVE_DPI) { - scale_x = source->physical.dmPelsWidth / (float)source->current.dmPelsWidth; - scale_y = source->physical.dmPelsHeight / (float)source->current.dmPelsHeight; + DEVMODEW current, physical; + source_get_view_modes( source, process_view, ¤t, &physical ); + scale_x = physical.dmPelsWidth / (float)current.dmPelsWidth; + scale_y = physical.dmPelsHeight / (float)current.dmPelsHeight; } *dpi_x = round( dpi * scale_x ); @@ -2323,6 +2611,12 @@ static UINT monitor_get_dpi( struct monitor *monitor, MONITOR_DPI_TYPE type, UIN } /* display_lock must be held */ +static UINT monitor_get_dpi( struct monitor *monitor, MONITOR_DPI_TYPE type, UINT *dpi_x, UINT *dpi_y ) +{ + return monitor_get_dpi_view( monitor, type, TRUE, dpi_x, dpi_y ); +} + +/* display_lock must be held; the calling process's view (with its mode overrides) */ static RECT map_monitor_rect( struct monitor *monitor, RECT rect, UINT dpi_from, MONITOR_DPI_TYPE type_from, UINT dpi_to, MONITOR_DPI_TYPE type_to ) { @@ -2336,8 +2630,7 @@ static RECT map_monitor_rect( struct monitor *monitor, RECT rect, UINT dpi_from, DEVMODEW current_mode = {.dmSize = sizeof(DEVMODEW)}, physical_mode; UINT num, den, dpi; - source_get_current_settings( monitor->source, ¤t_mode ); - physical_mode = monitor->source->physical; + source_get_view_modes( monitor->source, TRUE, ¤t_mode, &physical_mode ); dpi = monitor_get_dpi( monitor, MDT_DEFAULT, &x, &y ); if (!dpi_from) dpi_from = dpi; @@ -2380,10 +2673,10 @@ static RECT map_monitor_rect( struct monitor *monitor, RECT rect, UINT dpi_from, return map_dpi_rect( rect, dpi_from, dpi_to ); } -/* display_lock must be held */ -static RECT monitor_get_rect( struct monitor *monitor, UINT dpi, MONITOR_DPI_TYPE type ) +/* display_lock must be held; process_view as in monitor_get_dpi_view */ +static RECT monitor_get_rect_view( struct monitor *monitor, UINT dpi, MONITOR_DPI_TYPE type, BOOL process_view ) { - DEVMODEW current_mode = {.dmSize = sizeof(DEVMODEW)}; + DEVMODEW current_mode = {.dmSize = sizeof(DEVMODEW)}, physical_mode; RECT rect = {0, 0, 1024, 768}; struct source *source; UINT dpi_from, x, y; @@ -2394,17 +2687,23 @@ static RECT monitor_get_rect( struct monitor *monitor, UINT dpi, MONITOR_DPI_TYP SetRectEmpty( &rect ); if (!(source->state_flags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP)) return rect; - source_get_current_settings( source, ¤t_mode ); + source_get_view_modes( source, process_view, ¤t_mode, &physical_mode ); - mode = type != MDT_EFFECTIVE_DPI ? &source->physical : ¤t_mode; + mode = type != MDT_EFFECTIVE_DPI ? &physical_mode : ¤t_mode; SetRect( &rect, mode->dmPosition.x, mode->dmPosition.y, mode->dmPosition.x + mode->dmPelsWidth, mode->dmPosition.y + mode->dmPelsHeight ); - dpi_from = monitor_get_dpi( monitor, type, &x, &y ); + dpi_from = monitor_get_dpi_view( monitor, type, process_view, &x, &y ); return map_dpi_rect( rect, dpi_from, dpi ); } +/* display_lock must be held */ +static RECT monitor_get_rect( struct monitor *monitor, UINT dpi, MONITOR_DPI_TYPE type ) +{ + return monitor_get_rect_view( monitor, dpi, type, TRUE ); +} + /* display_lock must be held */ static void monitor_get_info( struct monitor *monitor, MONITORINFO *info, UINT dpi ) { @@ -2437,9 +2736,13 @@ static void set_winstation_monitors( BOOL increment ) if (is_monitor_primary( monitor )) info->flags |= MONITOR_FLAG_PRIMARY; if (!is_monitor_active( monitor )) info->flags |= MONITOR_FLAG_INACTIVE; if (monitor->is_clone) info->flags |= MONITOR_FLAG_CLONE; - info->dpi = monitor_get_dpi( monitor, MDT_EFFECTIVE_DPI, &x, &y ); - info->virt = wine_server_rectangle( monitor_get_rect( monitor, 0, MDT_EFFECTIVE_DPI ) ); - info->raw = wine_server_rectangle( monitor_get_rect( monitor, 0, MDT_RAW_DPI ) ); + /* the wineserver works with the global/native view, not this process's + * (possibly mode override scaled) one; pass process_view=FALSE to match the + * virt/raw rects below (effective DPI is view-independent, so this is + * behavior-preserving — just consistent with the adjacent calls) */ + info->dpi = monitor_get_dpi_view( monitor, MDT_EFFECTIVE_DPI, FALSE, &x, &y ); + info->virt = wine_server_rectangle( monitor_get_rect_view( monitor, 0, MDT_EFFECTIVE_DPI, FALSE ) ); + info->raw = wine_server_rectangle( monitor_get_rect_view( monitor, 0, MDT_RAW_DPI, FALSE ) ); info++; } @@ -2624,6 +2927,9 @@ static BOOL update_display_cache_from_registry( UINT64 serial ) if ((ret = !list_empty( &sources ) && !list_empty( &monitors ))) last_query_display_time = key.LastWriteTime.QuadPart; + /* the sources (and their ids and mode lists) may have changed */ + validate_mode_overrides(); + set_winstation_monitors( FALSE ); release_display_device_init_mutex( mutex ); return ret; @@ -3678,6 +3984,10 @@ LONG WINAPI NtUserQueryDisplayConfig( UINT32 flags, UINT32 *paths_count, DISPLAY { goto done; } + /* report this process's emulated mode override, if any, so that + * QueryDisplayConfig agrees with EnumDisplaySettings and the metrics */ + get_source_mode_override( monitor->source->id, &devmode ); + get_source_view_position( monitor->source->id, &devmode.dmPosition ); if (path_index == *paths_count || mode_index == *modes_count) { @@ -4335,6 +4645,137 @@ static BOOL get_primary_source_mode( DEVMODEW *mode ) return ret; } +/* Rebuild this process's per-source view positions: take each source's current + * global mode, substitute the process's overrides, and re-place the layout the + * way a real mode change would (no gaps, no overlaps). display_lock must be + * held. */ +static void update_view_positions(void) +{ + WCHAR primary_name[CCHDEVICENAME] = {0}; + struct list new_positions = LIST_INIT( new_positions ); + struct view_position *view, *next; + struct source *source, *primary; + DEVMODEW *displays, *mode; + + /* No overrides: just drop any stale positions and bail. */ + if (!process_has_mode_overrides()) + { + pthread_mutex_lock( &mode_override_lock ); + LIST_FOR_EACH_ENTRY_SAFE( view, next, &view_positions, struct view_position, entry ) + { + list_remove( &view->entry ); + free( view ); + } + pthread_mutex_unlock( &mode_override_lock ); + return; + } + + if (!(displays = calloc( list_count( &sources ) + 1, sizeof(DEVMODEW) ))) return; + + mode = displays; + LIST_FOR_EACH_ENTRY( source, &sources, struct source, entry ) + { + char buffer[CCHDEVICENAME]; + + mode->dmSize = sizeof(DEVMODEW); + if (!source_get_current_settings( source, mode )) goto done; + get_source_mode_override( source->id, mode ); + snprintf( buffer, sizeof(buffer), "\\\\.\\DISPLAY%d", source->id + 1 ); + asciiz_to_unicode( mode->dmDeviceName, buffer ); + mode = NEXT_DEVMODEW(mode); + } + + if ((primary = find_primary_source())) + { + char device_name[CCHDEVICENAME]; + snprintf( device_name, sizeof(device_name), "\\\\.\\DISPLAY%d", primary->id + 1 ); + asciiz_to_unicode( primary_name, device_name ); + } + place_all_displays( displays, primary_name ); + + /* Build into a local list and only swap it in once the whole layout was + * computed, so a mid-build failure (goto done) leaves the previous, still + * valid positions intact instead of dropping every window to its native + * position. */ + mode = displays; + LIST_FOR_EACH_ENTRY( source, &sources, struct source, entry ) + { + if (!(view = calloc( 1, sizeof(*view) ))) goto done; + view->source_id = source->id; + view->position = mode->dmPosition; + list_add_tail( &new_positions, &view->entry ); + mode = NEXT_DEVMODEW(mode); + } + + pthread_mutex_lock( &mode_override_lock ); + LIST_FOR_EACH_ENTRY_SAFE( view, next, &view_positions, struct view_position, entry ) + { + list_remove( &view->entry ); + free( view ); + } + list_move_tail( &view_positions, &new_positions ); + pthread_mutex_unlock( &mode_override_lock ); + +done: + free( displays ); + /* Only non-empty on the goto-done path (the success path moved the list). */ + LIST_FOR_EACH_ENTRY_SAFE( view, next, &new_positions, struct view_position, entry ) + { + list_remove( &view->entry ); + free( view ); + } +} + +/* per-process counterpart of display_mode_changed(): the global display state + * didn't change, notify only the calling process's own top-level windows. + * notify FALSE when a global change is being broadcast anyway and only the + * window state refresh is needed. */ +static void process_display_mode_changed( BOOL notify ) +{ + DEVMODEW current_mode = {.dmSize = sizeof(DEVMODEW)}; + DWORD pid, self = GetCurrentProcessId(); + struct source *primary; + HWND *list = NULL; + ULONG i, count = 128; + NTSTATUS status; + + if (!(primary = find_source( NULL ))) return; + if (!source_get_current_settings( primary, ¤t_mode )) + { + source_release( primary ); + return; + } + get_source_mode_override( primary->id, ¤t_mode ); + source_release( primary ); + + for (;;) + { + if (!(list = malloc( count * sizeof(*list) ))) return; + status = NtUserBuildHwndList( 0, 0, FALSE, FALSE, 0, count, list, &count ); + if (!status) break; + free( list ); + if (status != STATUS_BUFFER_TOO_SMALL) return; + } + + /* Note: the global cursor clip is left alone: the screen didn't change for + * any other process, and another process (e.g. a fullscreen game) may hold + * a clip that a reset here would silently drop. */ + for (i = 0; i < count && list[i] != HWND_BOTTOM; i++) + { + if (!NtUserGetWindowThread( list[i], &pid ) || pid != self) continue; + if (notify) + send_notify_message( list[i], WM_DISPLAYCHANGE, current_mode.dmBitsPerPel, + MAKELPARAM( current_mode.dmPelsWidth, current_mode.dmPelsHeight ), FALSE ); + /* the server-side rects and the window surface both depend on the + * virt<->raw mapping that just changed with the override */ + update_window_state( list[i] ); + } + free( list ); + + /* post clip_fullscreen_window request to the foreground window */ + NtUserPostMessage( NtUserGetForegroundWindow(), WM_WINE_CLIPCURSOR, SET_CURSOR_FSCLIP, 0 ); +} + static void display_mode_changed( BOOL broadcast ) { DEVMODEW current_mode = {.dmSize = sizeof(DEVMODEW)}; @@ -4380,6 +4821,62 @@ static LONG apply_display_settings( struct source *target, const DEVMODEW *devmo LONG ret; if (!lock_display_devices( FALSE )) return DISP_CHANGE_FAILED; + + /* Sources with a win32u-synthesized mode list cannot really change modes. + * Apply the requested mode only to the calling process's view of the + * display, like Windows fullscreen optimizations: the wineserver, the + * drivers and all other processes keep working with the native mode. */ + if (!is_virtual_desktop() && !emulate_modeset) + { + /* A detach request (zero size) must keep its global semantics: recording + * it as an override would put zero-sized modes into the process's metrics + * (and zero denominators into the virt<->raw ratio). */ + if (target && devmode && target->virtual_modes && !is_detached_mode( devmode )) + { + DEVMODEW physical = target->physical, current = {.dmSize = sizeof(current)}; + unsigned int source_id = target->id; + BOOL layout_change; + + source_get_current_settings( target, ¤t ); + + /* an explicit position or orientation change rearranges the monitor + * layout, which keeps its global semantics: the emulated size is + * still recorded as an override, then the request falls through to + * the global path (where the size is clamped back to the host mode) */ + layout_change = ((devmode->dmFields & DM_POSITION) && + (devmode->dmPosition.x != current.dmPosition.x || + devmode->dmPosition.y != current.dmPosition.y)) || + ((devmode->dmFields & DM_DISPLAYORIENTATION) && + devmode->dmDisplayOrientation != current.dmDisplayOrientation); + + /* an emulated depth change (same size, different bpp) must also be + * recorded: removing the override would silently ignore it */ + if (devmode->dmPelsWidth == physical.dmPelsWidth && + devmode->dmPelsHeight == physical.dmPelsHeight && + (!(devmode->dmFields & DM_BITSPERPEL) || !devmode->dmBitsPerPel || + devmode->dmBitsPerPel == current.dmBitsPerPel) && + (!(devmode->dmFields & DM_DISPLAYFREQUENCY) || !devmode->dmDisplayFrequency || + !physical.dmDisplayFrequency || devmode->dmDisplayFrequency == physical.dmDisplayFrequency)) + set_source_mode_override( source_id, NULL ); /* back to the native mode */ + else + set_source_mode_override( source_id, devmode ); + update_view_positions(); + + if (!layout_change) + { + unlock_display_devices(); + process_display_mode_changed( TRUE ); + return DISP_CHANGE_SUCCESSFUL; + } + } + /* A reset (ChangeDisplaySettings(NULL)) drops this process's overrides + * and still runs the global path below: sources with real mode switches + * get their registry settings restored, emulated ones are clamped back + * to the host mode. */ + if (!target && !devmode && remove_all_mode_overrides()) + update_view_positions(); + } + if (!(displays = get_display_settings( target, devmode ))) { unlock_display_devices(); @@ -4394,6 +4891,28 @@ static LONG apply_display_settings( struct source *target, const DEVMODEW *devmo return DISP_CHANGE_SUCCESSFUL; } + /* The global view of a source with a synthesized mode list always tracks + * the host mode; mode changes for it are emulated per-process. Clamp any + * other requested mode (e.g. stale or CDS_UPDATEREGISTRY-written registry + * settings restored by a ChangeDisplaySettings(NULL) reset) back to the + * host mode instead of desynchronizing every process from the host. Done + * before placement so monitor positions are laid out from the final sizes. */ + if (!is_virtual_desktop() && !emulate_modeset) + { + mode = displays; + LIST_FOR_EACH_ENTRY( source, &sources, struct source, entry ) + { + if (source->virtual_modes && !is_detached_mode( mode )) + { + mode->dmPelsWidth = source->physical.dmPelsWidth; + mode->dmPelsHeight = source->physical.dmPelsHeight; + if ((source->physical.dmFields & DM_DISPLAYFREQUENCY) && source->physical.dmDisplayFrequency) + mode->dmDisplayFrequency = source->physical.dmDisplayFrequency; + } + mode = NEXT_DEVMODEW(mode); + } + } + if (!(primary = find_primary_source())) primary_name[0] = 0; else { @@ -4417,9 +4936,16 @@ static LONG apply_display_settings( struct source *target, const DEVMODEW *devmo WARN( "Failed to write source %u current mode.\n", source->id ); mode = NEXT_DEVMODEW(mode); } + /* the global layout changed under this process's overrides */ + update_view_positions(); } unlock_display_devices(); + /* refresh this process's window state against the new view; the caller's + * display_mode_changed() broadcast delivers the WM_DISPLAYCHANGE part */ + if (ret == DISP_CHANGE_SUCCESSFUL && process_has_mode_overrides()) + process_display_mode_changed( FALSE ); + free( displays ); if (ret) return ret; @@ -4517,7 +5043,16 @@ BOOL WINAPI NtUserEnumDisplaySettings( UNICODE_STRING *device, DWORD index, DEVM devmode->dmDriverExtra = 0; if (index == ENUM_REGISTRY_SETTINGS) ret = source_get_registry_settings( source, devmode ); - else if (index == ENUM_CURRENT_SETTINGS) ret = source_get_current_settings( source, devmode ); + else if (index == ENUM_CURRENT_SETTINGS) + { + ret = source_get_current_settings( source, devmode ); + if (ret) + { + /* report this process's emulated mode override, if any */ + get_source_mode_override( source->id, devmode ); + get_source_view_position( source->id, &devmode->dmPosition ); + } + } else if (index == WINE_ENUM_PHYSICAL_SETTINGS) ret = FALSE; else ret = source_enum_display_settings( source, index, devmode, flags ); source_release( source ); @@ -4568,7 +5103,12 @@ INT get_display_depth( UNICODE_STRING *name ) } if (!source_get_current_settings( source, ¤t_mode )) depth = 32; - else depth = current_mode.dmBitsPerPel; + else + { + /* report this process's emulated mode override, if any */ + get_source_mode_override( source->id, ¤t_mode ); + depth = current_mode.dmBitsPerPel; + } unlock_display_devices(); return depth; diff --git a/dlls/win32u/vulkan.c b/dlls/win32u/vulkan.c index 310ce8923c9..46d4bff8b4f 100644 --- a/dlls/win32u/vulkan.c +++ b/dlls/win32u/vulkan.c @@ -724,6 +724,18 @@ static VkResult convert_device_create_info( struct vulkan_physical_device *physi ALL_VK_DEVICE_EXTS #undef USE_VK_EXT + /* The ALL_VK_DEVICE_EXTS loop above already emits this when the app enabled + * it; only add it here if it isn't already in the list, otherwise vkCreateDevice + * rejects the duplicate extension string. physical_device->extensions is the + * host support set cached at init_physical_device time. */ + if (!device->extensions.has_VK_EXT_image_drm_format_modifier && + physical_device->extensions.has_VK_EXT_image_drm_format_modifier) + { + extensions[count++] = "VK_EXT_image_drm_format_modifier"; + /* Keep the in-memory extension state consistent with what we enabled. */ + device->extensions.has_VK_EXT_image_drm_format_modifier = 1; + } + TRACE( "Enabling %u host device extensions\n", count ); for (const char **extension = extensions, **end = extension + count; extension < end; extension++) TRACE( " - %s\n", debugstr_a(*extension) ); @@ -1944,6 +1956,12 @@ static VkResult win32u_vkAcquireNextImageKHR( VkDevice client_device, VkSwapchai return res; } +static void win32u_vkFlushRemoteSurfaceDMABUF( HWND hwnd, HANDLE handle, UINT64 modifier, UINT width, UINT height, UINT stride ) +{ + TRACE( "hwnd %p, handle %p, modifier %s, width %u, height %u, stride %u\n", hwnd, handle, wine_dbgstr_longlong(modifier), width, height, stride ); + user_driver->pFlushRemoteSurfaceDMABUF( hwnd, handle, modifier, width, height, stride ); +} + static VkResult win32u_vkQueuePresentKHR( VkQueue client_queue, const VkPresentInfoKHR *client_present_info ) { VkPresentInfoKHR *present_info = (VkPresentInfoKHR *)client_present_info; /* cast away const, it has been copied in the thunks */ @@ -2963,6 +2981,7 @@ static struct vulkan_funcs vulkan_funcs = .p_vkQueueSubmit2KHR = win32u_vkQueueSubmit2KHR, .p_vkUnmapMemory = win32u_vkUnmapMemory, .p_vkUnmapMemory2KHR = win32u_vkUnmapMemory2KHR, + .p_vkFlushRemoteSurfaceDMABUF = win32u_vkFlushRemoteSurfaceDMABUF, }; static VkResult nulldrv_vulkan_surface_create( struct client_surface *client, const struct vulkan_instance *instance, VkSurfaceKHR *surface ) diff --git a/dlls/win32u/win32u_private.h b/dlls/win32u/win32u_private.h index c217aa9ead6..48b2281a718 100644 --- a/dlls/win32u/win32u_private.h +++ b/dlls/win32u/win32u_private.h @@ -184,6 +184,7 @@ extern struct window_rects map_dpi_window_rects( struct window_rects rects, UINT extern RECT map_rect_raw_to_virt( RECT rect, UINT dpi_to ); extern RECT map_rect_virt_to_raw( RECT rect, UINT dpi_from ); extern struct window_rects map_window_rects_virt_to_raw( struct window_rects rects, UINT dpi_from ); +extern BOOL process_has_mode_overrides(void); extern POINT point_phys_to_win_dpi( HWND hwnd, POINT pt ); extern POINT point_thread_to_win_dpi( HWND hwnd, POINT pt ); extern RECT rect_thread_to_win_dpi( HWND hwnd, RECT rect ); diff --git a/dlls/win32u/window.c b/dlls/win32u/window.c index f4dc335c5cf..a126609eac4 100644 --- a/dlls/win32u/window.c +++ b/dlls/win32u/window.c @@ -2168,13 +2168,13 @@ static BOOL is_fullscreen( const MONITORINFO *info, const RECT *rect ) static BOOL apply_window_pos( HWND hwnd, HWND insert_after, UINT swp_flags, struct window_surface *new_surface, const struct window_rects *new_rects, const RECT *valid_rects ) { - struct window_rects monitor_rects; + struct window_rects monitor_rects, server_rects; WND *win; HWND owner_hint, surface_win = 0, toplevel; UINT raw_dpi, monitor_dpi, dpi = get_thread_dpi(); - BOOL ret, is_layered, is_child, need_icons = FALSE; + BOOL ret, is_layered, is_child, has_override, need_icons = FALSE; struct window_rects old_rects; - RECT extra_rects[3]; + RECT extra_rects[3], server_valid_rect; struct window_surface *old_surface; HICON icon, icon_small; ICONINFO ii, ii_small; @@ -2186,8 +2186,30 @@ static BOOL apply_window_pos( HWND hwnd, HWND insert_after, UINT swp_flags, stru if (is_child) monitor_dpi = get_win_monitor_dpi( toplevel, &raw_dpi ); else monitor_dpi = monitor_dpi_from_rect( new_rects->window, dpi, &raw_dpi ); + /* The wineserver, the drivers and other processes always work with native + * coordinates. When this process has a per-process emulated display mode, + * map the rects out of its scaled view before sending them to the server + * (a no-op otherwise, the spaces are then identical). */ + server_rects = *new_rects; + if (valid_rects) server_valid_rect = valid_rects[0]; + else SetRectEmpty( &server_valid_rect ); + if ((has_override = process_has_mode_overrides())) + { + if (is_child) + { + UINT from_dpi = dpi ? dpi : monitor_dpi; + server_rects = map_dpi_window_rects( *new_rects, from_dpi, raw_dpi ); + if (valid_rects) server_valid_rect = map_dpi_rect( valid_rects[0], from_dpi, raw_dpi ); + } + else + { + server_rects = map_window_rects_virt_to_raw( *new_rects, dpi ); + if (valid_rects) server_valid_rect = map_rect_virt_to_raw( valid_rects[0], dpi ); + } + } + get_window_rects( hwnd, COORDS_PARENT, &old_rects, dpi ); - if (IsRectEmpty( &valid_rects[0] ) || is_layered) valid_rects = NULL; + if (!valid_rects || IsRectEmpty( &valid_rects[0] ) || is_layered) valid_rects = NULL; if (!(win = get_win_ptr( hwnd )) || win == WND_DESKTOP || win == WND_OTHER_PROCESS) return FALSE; old_surface = win->surface; @@ -2206,17 +2228,26 @@ static BOOL apply_window_pos( HWND hwnd, HWND insert_after, UINT swp_flags, stru req->previous = wine_server_user_handle( insert_after ); req->swp_flags = swp_flags; req->monitor_dpi = monitor_dpi; - req->window = wine_server_rectangle( new_rects->window ); - req->client = wine_server_rectangle( new_rects->client ); - if (!EqualRect( &new_rects->window, &new_rects->visible ) || new_surface || valid_rects) + req->window = wine_server_rectangle( server_rects.window ); + req->client = wine_server_rectangle( server_rects.client ); + if (!EqualRect( &server_rects.window, &server_rects.visible ) || new_surface || valid_rects) { - extra_rects[0] = extra_rects[1] = new_rects->visible; + extra_rects[0] = extra_rects[1] = server_rects.visible; if (new_surface) { - extra_rects[1] = is_layered ? dummy_surface.rect : new_surface->rect; - OffsetRect( &extra_rects[1], new_rects->visible.left, new_rects->visible.top ); + struct window_surface *surface = new_surface; + /* under a mode override the driver surface describes the native-space pixels */ + if (has_override && !is_layered) + { + struct window_surface *driver_surface = get_driver_window_surface( new_surface, raw_dpi ); + /* a transient dpi mismatch yields the 1x1 dummy_surface; keep the real + * surface rect rather than recording a degenerate 1x1 visible rect. */ + if (driver_surface && driver_surface != &dummy_surface) surface = driver_surface; + } + extra_rects[1] = is_layered ? dummy_surface.rect : surface->rect; + OffsetRect( &extra_rects[1], server_rects.visible.left, server_rects.visible.top ); } - if (valid_rects) extra_rects[2] = valid_rects[0]; + if (valid_rects) extra_rects[2] = server_valid_rect; else SetRectEmpty( &extra_rects[2] ); wine_server_add_data( req, extra_rects, sizeof(extra_rects) ); } @@ -5294,6 +5325,7 @@ LRESULT destroy_window( HWND hwnd ) } detach_client_surfaces( hwnd ); + evict_remote_surfaces( hwnd ); if (win->current_drawable) opengl_drawable_release( win->current_drawable ); if (win->unused_drawable) opengl_drawable_release( win->unused_drawable ); user_driver->pDestroyWindow( hwnd ); @@ -5458,6 +5490,7 @@ void destroy_thread_windows(void) if (entry->handle == capture) user_driver->pSetCapture( NULL, 0, toplevel ); detach_client_surfaces( entry->handle ); + evict_remote_surfaces( entry->handle ); user_driver->pDestroyWindow( entry->handle ); if (entry->current_drawable) opengl_drawable_release( entry->current_drawable ); if (entry->unused_drawable) opengl_drawable_release( entry->unused_drawable ); diff --git a/dlls/winevulkan/Makefile.in b/dlls/winevulkan/Makefile.in index de433143801..b16c77242c4 100644 --- a/dlls/winevulkan/Makefile.in +++ b/dlls/winevulkan/Makefile.in @@ -7,8 +7,10 @@ UNIX_LIBS = -lwin32u $(PTHREAD_LIBS) VER_FILEDESCRIPTION_STR = "Wine Vulkan ICD" SOURCES = \ + layer.c \ loader.c \ loader_thunks.c \ vulkan.c \ vulkan_thunks.c \ + wsi.c \ winevulkan.rc diff --git a/dlls/winevulkan/layer.c b/dlls/winevulkan/layer.c new file mode 100644 index 00000000000..6346cbab8a2 --- /dev/null +++ b/dlls/winevulkan/layer.c @@ -0,0 +1,575 @@ +/* Wine Vulkan implicit layer loading (PE side) + * + * Discovers Windows implicit Vulkan layers registered under + * Software\Khronos\Vulkan\ImplicitLayers, loads the layer DLL, negotiates the + * Khronos layer interface, and exposes the layer's entry points plus the + * "next" trampolines so winevulkan's vkCreateInstance / vkCreateDevice can + * insert the layer into the call chain. + * + * Copyright 2025 Wine project + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ + +#include <stdlib.h> +#include "vulkan_loader.h" +#include "vk_layer.h" +#include "winreg.h" +#include "winnls.h" + +WINE_DEFAULT_DEBUG_CHANNEL(vulkan); + +/* Entry points the layer chains down to, defined in loader.c. They sit on the + * Khronos layer ABI (handed to the layer as PFN_vkCreateInstance etc.), so they + * must use VKAPI_CALL (= __stdcall on i386 PE) to match the PFN typedefs; a plain + * cdecl definition corrupts the stack when the layer calls down on i386. The + * create entry points are the *_layered shims: a create reached through the layer + * chain stamps the per-handle dispatch key. Destroy is shared with the bypass path. */ +extern VkResult VKAPI_CALL create_instance_layered(const VkInstanceCreateInfo *create_info, + const VkAllocationCallbacks *allocator, VkInstance *ret); +extern void VKAPI_CALL destroy_instance_real(VkInstance instance, const VkAllocationCallbacks *allocator); +extern void VKAPI_CALL destroy_device_real(VkDevice device, const VkAllocationCallbacks *allocator); +extern VkResult VKAPI_CALL create_device_layered(VkPhysicalDevice physical_device, const VkDeviceCreateInfo *create_info, + const VkAllocationCallbacks *allocator, VkDevice *ret); + +/* ICD physical-device proc-addr resolver, defined in loader.c. */ +extern void * WINAPI vk_icdGetPhysicalDeviceProcAddr(VkInstance instance, const char *name); + +/* Dispatch table resolvers (loader.c / loader_thunks.c). */ +extern void *wine_vk_get_global_proc_addr(const char *name); + +/* Availability-gated proc-addr resolvers shared with the public fronts + * (loader.c). Using these in the next-* trampolines makes the layer chain + * return NULL for unsupported / unenabled functions, like the unlayered path. */ +extern PFN_vkVoidFunction wine_vk_resolve_instance_proc_addr(VkInstance instance, const char *name); +extern PFN_vkVoidFunction wine_vk_resolve_device_proc_addr(VkDevice device, const char *name); + +struct loaded_layer +{ + char name[256]; + HMODULE module; + PFN_vkGetInstanceProcAddr gipa; /* layer's vkGetInstanceProcAddr */ + PFN_vkGetDeviceProcAddr gdpa; /* layer's vkGetDeviceProcAddr */ + PFN_GetPhysicalDeviceProcAddr gpdpa; /* layer's physical-device proc addr (may be NULL) */ +}; + +/* Implicit layers loaded for this process, in chain order (index 0 = closest to + * the application). Populated once under init_layers_once; immutable afterwards. */ +static struct loaded_layer g_layers[WINE_VK_MAX_LAYERS]; +static unsigned int g_layer_count; + +/* "next" trampolines: the layer calls these to reach winevulkan's real entry + * points. Create/destroy and the proc-addr functions themselves must resolve + * to the *real* bodies, or the layer would re-enter the public layer-aware + * fronts and recurse forever. */ + +PFN_vkVoidFunction WINAPI wine_layer_next_gdpa(VkDevice device, const char *name); + +PFN_vkVoidFunction WINAPI wine_layer_next_gipa(VkInstance instance, const char *name) +{ + void *func; + + if (!name) + return NULL; + + if (!strcmp(name, "vkCreateInstance")) + return (PFN_vkVoidFunction)create_instance_layered; + if (!strcmp(name, "vkCreateDevice")) + return (PFN_vkVoidFunction)create_device_layered; + if (!strcmp(name, "vkDestroyInstance")) + return (PFN_vkVoidFunction)destroy_instance_real; + if (!strcmp(name, "vkDestroyDevice")) + return (PFN_vkVoidFunction)destroy_device_real; + if (!strcmp(name, "vkGetInstanceProcAddr")) + return (PFN_vkVoidFunction)wine_layer_next_gipa; + if (!strcmp(name, "vkGetDeviceProcAddr")) + return (PFN_vkVoidFunction)wine_layer_next_gdpa; + + /* Global functions (vkGetInstanceProcAddr, vkEnumerateInstance*...) resolve + * regardless of instance, including the layer's NULL-instance global query. */ + if ((func = wine_vk_get_global_proc_addr(name))) + return func; + if (!instance) + return NULL; + + /* Honour host availability like the public front, so the layer never + * receives a live pointer for a function the instance does not support. */ + return wine_vk_resolve_instance_proc_addr(instance, name); +} + +PFN_vkVoidFunction WINAPI wine_layer_next_gdpa(VkDevice device, const char *name) +{ + if (!name) + return NULL; + + if (!strcmp(name, "vkCreateDevice")) + return (PFN_vkVoidFunction)create_device_layered; + if (!strcmp(name, "vkDestroyDevice")) + return (PFN_vkVoidFunction)destroy_device_real; + if (!strcmp(name, "vkGetDeviceProcAddr")) + return (PFN_vkVoidFunction)wine_layer_next_gdpa; + + /* Tolerate a NULL device like the public front does; the resolver below + * dereferences the handle. */ + if (!device) + return NULL; + + /* Honour host availability like the public front. */ + return wine_vk_resolve_device_proc_addr(device, name); +} + + +/* The application enters the layer chain at the top (index 0) layer and chains + * down through the per-layer links built in vkCreateInstance/vkCreateDevice. */ +PFN_vkGetInstanceProcAddr wine_vk_layer_instance_gipa(void) +{ + return g_layer_count ? g_layers[0].gipa : NULL; +} + +PFN_vkGetDeviceProcAddr wine_vk_layer_device_gdpa(void) +{ + /* The device-participating layer need not be the topmost one: an instance-only + * layer (no gdpa) may sit above one that hooks device functions. Return the + * first layer that implements a device proc-addr so device dispatch and + * teardown enter the chain at the right place, mirroring the device-chain build + * in vkCreateDevice (loader.c), which likewise skips instance-only layers. */ + unsigned int i; + for (i = 0; i < g_layer_count; i++) + if (g_layers[i].gdpa) return g_layers[i].gdpa; + return NULL; +} + +PFN_GetPhysicalDeviceProcAddr wine_vk_layer_next_gpdpa(void) +{ + return (PFN_GetPhysicalDeviceProcAddr)vk_icdGetPhysicalDeviceProcAddr; +} + +/* Per-index accessors so the loader can build the N-layer call chain. */ +unsigned int wine_vk_layer_count(void) +{ + return g_layer_count; +} + +PFN_vkGetInstanceProcAddr wine_vk_layer_gipa(unsigned int index) +{ + return index < g_layer_count ? g_layers[index].gipa : NULL; +} + +PFN_vkGetDeviceProcAddr wine_vk_layer_gdpa(unsigned int index) +{ + return index < g_layer_count ? g_layers[index].gdpa : NULL; +} + +PFN_GetPhysicalDeviceProcAddr wine_vk_layer_gpdpa(unsigned int index) +{ + return index < g_layer_count ? g_layers[index].gpdpa : NULL; +} + + +/* Find "key" : "value" in a JSON blob and copy the (un-escaped enough for our + * needs) string value into out. Returns TRUE on success. */ +static BOOL json_find_string(const char *json, const char *key, char *out, size_t out_len) +{ + char needle[64]; + const char *p, *v, *end; + size_t i; + + snprintf(needle, sizeof(needle), "\"%s\"", key); + if (!(p = strstr(json, needle))) + return FALSE; + p += strlen(needle); + + /* skip whitespace and colon */ + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' || *p == ':') + p++; + if (*p != '"') + return FALSE; + v = p + 1; + if (!(end = strchr(v, '"'))) + return FALSE; + + for (i = 0; v < end && i + 1 < out_len; v++) + { + /* Translate JSON "\\" escapes for path separators. */ + if (*v == '\\' && v + 1 < end && (v[1] == '\\' || v[1] == '/')) + { + out[i++] = '\\'; + v++; + continue; + } + out[i++] = (*v == '/') ? '\\' : *v; + } + out[i] = 0; + return TRUE; +} + +/* Read whole file into a malloc'd NUL-terminated buffer. */ +static char *read_text_file(const WCHAR *path) +{ + HANDLE file; + LARGE_INTEGER size; + DWORD read; + char *buf; + + file = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + if (file == INVALID_HANDLE_VALUE) + return NULL; + + if (!GetFileSizeEx(file, &size) || size.QuadPart <= 0 || size.QuadPart > (16 * 1024 * 1024)) + { + CloseHandle(file); + return NULL; + } + + if (!(buf = malloc((size_t)size.QuadPart + 1))) + { + CloseHandle(file); + return NULL; + } + + if (!ReadFile(file, buf, (DWORD)size.QuadPart, &read, NULL)) + { + free(buf); + CloseHandle(file); + return NULL; + } + buf[read] = 0; + CloseHandle(file); + return buf; +} + +/* Resolve a (possibly relative) library path against the JSON's directory and + * convert to a wide string. Returns a malloc'd WCHAR*, or NULL. */ +static WCHAR *resolve_library_path(const WCHAR *json_path, const char *library_path) +{ + WCHAR libW[MAX_PATH]; + WCHAR *full; + int len; + + len = MultiByteToWideChar(CP_UTF8, 0, library_path, -1, libW, ARRAY_SIZE(libW)); + if (!len) + return NULL; + + /* Absolute path (has a drive letter or leading separator)? Use as-is. */ + if (libW[0] && (libW[1] == ':' || libW[0] == '\\')) + { + if (!(full = malloc((lstrlenW(libW) + 1) * sizeof(WCHAR)))) + return NULL; + lstrcpyW(full, libW); + return full; + } + + /* Relative: prepend the directory containing the JSON manifest. */ + { + const WCHAR *slash = wcsrchr(json_path, '\\'); + size_t dir_len = slash ? slash + 1 - json_path : 0; + + if (!(full = malloc((dir_len + lstrlenW(libW) + 1) * sizeof(WCHAR)))) + return NULL; + memcpy(full, json_path, dir_len * sizeof(WCHAR)); + lstrcpyW(full + dir_len, libW); + } + return full; +} + +/* Find the enable_environment / disable_environment gate of a layer manifest + * and copy out its variable name and expected value. The manifest schema uses + * the object form { "VAR": "value" } (read the first pair, like the reference + * loader); a plain string value is accepted too, with an empty expected value. + * Returns FALSE if the key is absent. */ +static BOOL json_find_env_pair(const char *json, const char *key, char *var, size_t var_len, + char *value, size_t value_len) +{ + char needle[64]; + const char *p, *end; + size_t i; + BOOL is_object = FALSE; + + var[0] = 0; + value[0] = 0; + + snprintf(needle, sizeof(needle), "\"%s\"", key); + if (!(p = strstr(json, needle))) + return FALSE; + p += strlen(needle); + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' || *p == ':') + p++; + if (*p == '{') + { + is_object = TRUE; + p++; + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') + p++; + } + if (*p != '"' || !(end = strchr(p + 1, '"'))) + return FALSE; + for (i = 0, p++; p < end && i + 1 < var_len; p++) + var[i++] = *p; + var[i] = 0; + + /* In object form ({ "VAR": "value" }) the expected value follows the + * variable name. In bare-string form ("VAR") there is none; don't scan past + * it (a stray ':' in malformed JSON could otherwise grab a later key). */ + if (is_object) + { + p = end + 1; + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' || *p == ':') + p++; + if (*p == '"' && (end = strchr(p + 1, '"'))) + { + for (i = 0, p++; p < end && i + 1 < value_len; p++) + value[i++] = *p; + value[i] = 0; + } + } + return TRUE; +} + +/* Honor enable_environment / disable_environment with the reference Vulkan + * loader's semantics, so an implicit layer activates in exactly the processes + * it would on Windows: with an enable_environment gate the variable must be + * set to the given value (e.g. Steam sets ENABLE_VK_LAYER_VALVE_steam_overlay_1 + * only for games it launches); a set disable_environment variable always + * disables. Returns TRUE if the layer should be considered enabled. */ +static BOOL layer_env_enabled(const char *json) +{ + char var[256], value[256], env[256]; + DWORD len; + + if (json_find_env_pair(json, "enable_environment", var, sizeof(var), value, sizeof(value)) && var[0]) + { + len = GetEnvironmentVariableA(var, env, sizeof(env)); + if (!len || len >= sizeof(env) || (value[0] && strcmp(env, value))) + { + TRACE("Layer requires env var %s=%s, skipping\n", debugstr_a(var), debugstr_a(value)); + return FALSE; + } + } + + if (json_find_env_pair(json, "disable_environment", var, sizeof(var), value, sizeof(value)) && var[0]) + { + if (GetEnvironmentVariableA(var, env, sizeof(env)) != 0) + { + TRACE("Layer disabled via env var %s\n", debugstr_a(var)); + return FALSE; + } + } + + return TRUE; +} + +/* Try to load and negotiate one layer described by a JSON manifest path. */ +static BOOL try_load_layer(const WCHAR *json_path) +{ + char *json, library_path[MAX_PATH], name[256]; + struct loaded_layer *layer; + WCHAR *libW = NULL; + HMODULE module; + PFN_vkNegotiateLoaderLayerInterfaceVersion negotiate; + BOOL ret = FALSE; + unsigned int i; + + if (!(json = read_text_file(json_path))) + return FALSE; + + if (!json_find_string(json, "library_path", library_path, sizeof(library_path)) || !library_path[0]) + goto done; + + if (!layer_env_enabled(json)) + goto done; + + if (!json_find_string(json, "name", name, sizeof(name))) + name[0] = 0; + + if (g_layer_count >= WINE_VK_MAX_LAYERS) + { + WARN("Too many implicit layers (max %u), ignoring %s\n", WINE_VK_MAX_LAYERS, debugstr_a(name)); + goto done; + } + /* The same layer can be registered under both HKCU and HKLM; load it once. */ + if (name[0]) for (i = 0; i < g_layer_count; i++) + if (!strcmp(g_layers[i].name, name)) goto done; + + layer = &g_layers[g_layer_count]; + + if (!(libW = resolve_library_path(json_path, library_path))) + goto done; + + TRACE("Loading implicit layer %s from %s\n", debugstr_a(name), debugstr_w(libW)); + + if (!(module = LoadLibraryW(libW))) + { + WARN("Failed to load layer library %s (gle %lu)\n", debugstr_w(libW), GetLastError()); + goto done; + } + + negotiate = (void *)GetProcAddress(module, "vkNegotiateLoaderLayerInterfaceVersion"); + if (negotiate) + { + VkNegotiateLayerInterface iface = {0}; + iface.sType = LAYER_NEGOTIATE_INTERFACE_STRUCT; + iface.loaderLayerInterfaceVersion = 2; + if (negotiate(&iface) != VK_SUCCESS) + { + WARN("Layer %s negotiation failed\n", debugstr_a(name)); + FreeLibrary(module); + goto done; + } + layer->gipa = iface.pfnGetInstanceProcAddr; + layer->gdpa = iface.pfnGetDeviceProcAddr; + layer->gpdpa = iface.pfnGetPhysicalDeviceProcAddr; + } + else + { + /* Fallback: older layers export vkGetInstanceProcAddr / vkGetDeviceProcAddr. */ + layer->gipa = (void *)GetProcAddress(module, "vkGetInstanceProcAddr"); + layer->gdpa = (void *)GetProcAddress(module, "vkGetDeviceProcAddr"); + layer->gpdpa = (void *)GetProcAddress(module, "vk_layerGetPhysicalDeviceProcAddr"); + } + + if (!layer->gipa) + { + WARN("Layer %s has no usable vkGetInstanceProcAddr\n", debugstr_a(name)); + FreeLibrary(module); + goto done; + } + + layer->module = module; + lstrcpynA(layer->name, name, sizeof(layer->name)); + g_layer_count++; + ret = TRUE; + TRACE("Activated implicit layer %s (%u total)\n", debugstr_a(name), g_layer_count); + +done: + free(libW); + free(json); + return ret; +} + +/* Enumerate ImplicitLayers values under a given root key and load every enabled + * layer (deduped by name in try_load_layer), up to WINE_VK_MAX_LAYERS. */ +static void load_layers_from_root(HKEY root) +{ + static const WCHAR implicit_layersW[] = L"Software\\Khronos\\Vulkan\\ImplicitLayers"; + WCHAR value_name[MAX_PATH]; + DWORD index = 0, name_len, type, data, data_len; + HKEY key; + LSTATUS status; + + if (RegOpenKeyExW(root, implicit_layersW, 0, KEY_READ, &key) != ERROR_SUCCESS) + return; + + for (;;) + { + name_len = ARRAY_SIZE(value_name); + data_len = sizeof(data); + type = 0; + status = RegEnumValueW(key, index++, value_name, &name_len, NULL, + &type, (BYTE *)&data, &data_len); + if (status == ERROR_NO_MORE_ITEMS) + break; + if (status != ERROR_SUCCESS) + continue; + + /* The value name is the absolute path to the layer JSON manifest. + * REG_DWORD data: 0 = enabled, nonzero = disabled. */ + if (type == REG_DWORD && data != 0) + { + TRACE("Skipping disabled layer %s\n", debugstr_w(value_name)); + continue; + } + + try_load_layer(value_name); + if (g_layer_count >= WINE_VK_MAX_LAYERS) + break; + } + + RegCloseKey(key); +} + +/* Per-application override for implicit layer loading, read once from + * HKCU\Software\Wine\AppDefaults\<exe>\winevulkan : "ImplicitLayers" (REG_DWORD). + * 1 = force the layer on (even for a hooked D3D presenter), 0 = force it off, + * absent = use the automatic D3D-present detection. A per-game escape hatch beyond + * the blunt global WINE_VK_LAYERS=0; checked in vkCreateInstance, which runs the + * D3D-present detection before the lazy layer discovery. */ +int wine_vk_layer_app_override(void) +{ + static LONG cached = -2; /* -2 = unread, -1 = no override, 0/1 = forced off/on */ + WCHAR key_path[2 * MAX_PATH], exe[MAX_PATH], *name, *p; + DWORD value, size, type; + LONG val = -1; + HKEY key; + + if (ReadNoFence(&cached) != -2) + return cached; + + if (!GetModuleFileNameW(NULL, exe, ARRAY_SIZE(exe))) + { + InterlockedCompareExchange(&cached, val, -2); + return ReadNoFence(&cached); + } + for (name = p = exe; *p; p++) + if (*p == '\\' || *p == '/') name = p + 1; + + lstrcpyW(key_path, L"Software\\Wine\\AppDefaults\\"); + lstrcatW(key_path, name); + lstrcatW(key_path, L"\\winevulkan"); + + if (RegOpenKeyExW(HKEY_CURRENT_USER, key_path, 0, KEY_READ, &key) == ERROR_SUCCESS) + { + size = sizeof(value); + type = 0; + if (RegQueryValueExW(key, L"ImplicitLayers", NULL, &type, (BYTE *)&value, &size) == ERROR_SUCCESS && + type == REG_DWORD) + val = value ? 1 : 0; + RegCloseKey(key); + } + + InterlockedCompareExchange(&cached, val, -2); + TRACE("per-app ImplicitLayers override for %s: %d\n", debugstr_w(name), (int)ReadNoFence(&cached)); + return ReadNoFence(&cached); +} + + +/* Lazy init, called from vkCreateInstance. Returns whether any layer is active. */ +static BOOL CALLBACK init_layers_once(INIT_ONCE *once, void *param, void **context) +{ + char buf[8]; + + /* Opt-out: Windows implicit Vulkan layer loading is on by default (as on + * Windows); WINE_VK_LAYERS=0 disables it. Each layer's JSON + * enable_environment/disable_environment gate is honored with the + * reference loader's semantics (see layer_env_enabled), so e.g. the Steam + * overlay only activates in processes Steam launched. */ + if (GetEnvironmentVariableA("WINE_VK_LAYERS", buf, sizeof(buf)) && buf[0] == '0') + { + TRACE("Windows Vulkan implicit layers disabled via WINE_VK_LAYERS=0\n"); + return TRUE; + } + + /* Layers may be registered under either hive; load from both, deduped by + * name, so a per-user layer and a system layer can coexist. */ + load_layers_from_root(HKEY_CURRENT_USER); + load_layers_from_root(HKEY_LOCAL_MACHINE); + + return TRUE; +} + +BOOL wine_vk_init_layers(void) +{ + /* vkCreateInstance may legally be called from several threads at once */ + static INIT_ONCE init_once = INIT_ONCE_STATIC_INIT; + + InitOnceExecuteOnce(&init_once, init_layers_once, NULL, NULL); + return g_layer_count > 0; +} diff --git a/dlls/winevulkan/loader.c b/dlls/winevulkan/loader.c index 864328e3f4f..747174dd27b 100644 --- a/dlls/winevulkan/loader.c +++ b/dlls/winevulkan/loader.c @@ -19,10 +19,24 @@ #include <stdlib.h> #include "vulkan_loader.h" +#include "vk_layer.h" #include "winreg.h" WINE_DEFAULT_DEBUG_CHANNEL(vulkan); +/* Implicit layer support, implemented in layer.c. */ +extern BOOL wine_vk_init_layers(void); +extern PFN_vkGetInstanceProcAddr wine_vk_layer_instance_gipa(void); +extern PFN_vkGetDeviceProcAddr wine_vk_layer_device_gdpa(void); +extern PFN_GetPhysicalDeviceProcAddr wine_vk_layer_next_gpdpa(void); +extern PFN_vkVoidFunction WINAPI wine_layer_next_gipa(VkInstance instance, const char *name); +extern PFN_vkVoidFunction WINAPI wine_layer_next_gdpa(VkDevice device, const char *name); +extern int wine_vk_layer_app_override(void); +extern unsigned int wine_vk_layer_count(void); +extern PFN_vkGetInstanceProcAddr wine_vk_layer_gipa(unsigned int index); +extern PFN_vkGetDeviceProcAddr wine_vk_layer_gdpa(unsigned int index); +extern PFN_GetPhysicalDeviceProcAddr wine_vk_layer_gpdpa(unsigned int index); + /* For now default to 4 as it felt like a reasonable version feature wise to support. * Version 5 adds more extensive version checks. Something to tackle later. */ @@ -50,7 +64,7 @@ static const struct vulkan_func vk_global_dispatch_table[] = {"vkGetInstanceProcAddr", &vkGetInstanceProcAddr}, }; -static void *wine_vk_get_global_proc_addr(const char *name) +void *wine_vk_get_global_proc_addr(const char *name) { unsigned int i; @@ -84,29 +98,17 @@ static void *vulkan_client_object_create(size_t size) return object; } -PFN_vkVoidFunction WINAPI vkGetInstanceProcAddr(VkInstance instance, const char *name) +/* Resolve an instance entry point exactly as the public front does below the + * layer branch: the win32-surface specials, then the host-availability gate, + * then the instance and device dispatch tables. Shared with the layer's + * next-gipa trampoline (layer.c) so both honour availability identically -- a + * probe for an unsupported / unenabled function returns NULL on both paths, + * instead of the layer handing out a live thunk that dispatches to a NULL host + * function. */ +PFN_vkVoidFunction wine_vk_resolve_instance_proc_addr(VkInstance instance, const char *name) { void *func; - TRACE("%p, %s\n", instance, debugstr_a(name)); - - if (!name) - return NULL; - - /* vkGetInstanceProcAddr can load most Vulkan functions when an instance is passed in, however - * for a NULL instance it can only load global functions. - */ - func = wine_vk_get_global_proc_addr(name); - if (func) - { - return func; - } - if (!instance) - { - WARN("Global function %s not found.\n", debugstr_a(name)); - return NULL; - } - if (instance->extensions.has_VK_KHR_win32_surface) { if (!strcmp(name, "vkCreateWin32SurfaceKHR")) @@ -118,25 +120,21 @@ PFN_vkVoidFunction WINAPI vkGetInstanceProcAddr(VkInstance instance, const char if (!is_available_instance_function(instance, name)) return NULL; - func = wine_vk_get_instance_proc_addr(name); - if (func) return func; - - /* vkGetInstanceProcAddr also loads any children of instance, so device functions as well. */ - func = wine_vk_get_device_proc_addr(name); - if (func) return func; - - WARN("Unsupported device or instance function: %s.\n", debugstr_a(name)); + if ((func = wine_vk_get_instance_proc_addr(name))) + return func; + /* vkGetInstanceProcAddr also loads children of instance (device functions). */ + if ((func = wine_vk_get_device_proc_addr(name))) + return func; return NULL; } -PFN_vkVoidFunction WINAPI vkGetDeviceProcAddr(VkDevice device, const char *name) +/* Device counterpart of wine_vk_resolve_instance_proc_addr: external-memory / + * semaphore / fence win32 specials, then the host-availability gate, then the + * device dispatch table. The front's idTech6 instance-function fallback stays + * front-only (it is a per-game quirk, not part of the layer chain). */ +PFN_vkVoidFunction wine_vk_resolve_device_proc_addr(VkDevice device, const char *name) { void *func; - TRACE("%p, %s\n", device, debugstr_a(name)); - - /* The spec leaves return value undefined for a NULL device, let's just return NULL. */ - if (!device || !name) - return NULL; if (device->extensions.has_VK_KHR_external_memory_win32) { @@ -162,18 +160,84 @@ PFN_vkVoidFunction WINAPI vkGetDeviceProcAddr(VkDevice device, const char *name) return (PFN_vkVoidFunction)vkImportFenceWin32HandleKHR; } - /* Per the spec, we are only supposed to return device functions as in functions - * for which the first parameter is vkDevice or a child of vkDevice like a - * vkCommandBuffer or vkQueue. - * Loader takes care of filtering of extensions which are enabled or not. + if (is_available_device_function(device, name) && + (func = wine_vk_get_device_proc_addr(name))) + return func; + return NULL; +} + +PFN_vkVoidFunction WINAPI vkGetInstanceProcAddr(VkInstance instance, const char *name) +{ + void *func; + + TRACE("%p, %s\n", instance, debugstr_a(name)); + + if (!name) + return NULL; + + /* vkGetInstanceProcAddr can load most Vulkan functions when an instance is passed in, however + * for a NULL instance it can only load global functions. */ - if (is_available_device_function(device, name)) + func = wine_vk_get_global_proc_addr(name); + if (func) + { + return func; + } + if (!instance) + { + WARN("Global function %s not found.\n", debugstr_a(name)); + return NULL; + } + + /* Hand out the active layer's wrapped entry points, except + * vkCreateDevice / vkGetDeviceProcAddr: those must stay our fronts (they + * build the device layer chain the layer's wrappers rely on). Gate per-handle + * so an instance that bypassed the layer is not handed the layer's hooked + * entry points (which key on a dispatch table the layer never registered). */ + if (vulkan_client_object_is_layered(instance) && + strcmp(name, "vkCreateDevice") && strcmp(name, "vkGetDeviceProcAddr")) + { + PFN_vkGetInstanceProcAddr layer_gipa = wine_vk_layer_instance_gipa(); + if (layer_gipa && (func = layer_gipa(instance, name))) + return func; + } + + func = wine_vk_resolve_instance_proc_addr(instance, name); + if (!func) + WARN("Unsupported device or instance function: %s.\n", debugstr_a(name)); + return func; +} + +PFN_vkVoidFunction WINAPI vkGetDeviceProcAddr(VkDevice device, const char *name) +{ + void *func; + TRACE("%p, %s\n", device, debugstr_a(name)); + + /* The spec leaves return value undefined for a NULL device, let's just return NULL. */ + if (!device || !name) + return NULL; + + /* The layer's device-level wrappers (the overlay's vkQueuePresentKHR + * hook lives here). Gate per-handle: a device of a bypassed instance keeps + * the ICD magic and must not be routed through the layer. vkCreateDevice + * must stay our front (it builds the device layer chain): the layer would + * chain the query down to our next-gdpa trampoline and hand back the + * layered create shim, which stamps a layered dispatch key on a device the + * layer never actually wrapped. */ + if (vulkan_client_object_is_layered(device) && strcmp(name, "vkCreateDevice")) { - func = wine_vk_get_device_proc_addr(name); - if (func) + PFN_vkGetDeviceProcAddr layer_gdpa = wine_vk_layer_device_gdpa(); + if (layer_gdpa && (func = layer_gdpa(device, name))) return func; } + /* Per the spec, we are only supposed to return device functions, i.e. + * functions whose first parameter is a VkDevice or a child of it (VkQueue, + * VkCommandBuffer). The shared resolver applies the win32 extension specials + * and the host-availability gate. */ + if ((func = wine_vk_resolve_device_proc_addr(device, name))) + return func; + /* vkGetDeviceProcAddr was intended for loading device and subdevice functions. * idTech 6 titles such as Doom and Wolfenstein II, however use it also for * loading of instance functions. This is undefined behavior as the specification @@ -368,8 +432,8 @@ static BOOL is_device_extension_supported(VkPhysicalDevice physical_device, cons return FALSE; } -VkResult WINAPI vkCreateInstance(const VkInstanceCreateInfo *create_info, - const VkAllocationCallbacks *allocator, VkInstance *ret) +VkResult VKAPI_CALL create_instance_real(const VkInstanceCreateInfo *create_info, + const VkAllocationCallbacks *allocator, VkInstance *ret, BOOL layered) { struct vulkan_instance_extensions extensions = {0}; struct vkCreateInstance_params params; @@ -427,10 +491,176 @@ VkResult WINAPI vkCreateInstance(const VkInstanceCreateInfo *create_info, free(instance); *ret = NULL; } + else if (layered) + { + /* This instance was created through the layer chain: stamp a unique + * dispatch key. Layers map their per-instance state by the dispatch key + * *(void **)handle. The Windows loader gives each instance a unique + * dispatch table and stamps the same pointer on its physical devices; + * mimic that, or two instances (e.g. DXVK's DXGI factory + D3D11 + * device) collide on the shared ICD magic and the layer reads another + * instance's (possibly torn-down) state. A bypassed instance keeps the + * ICD magic, so vulkan_client_object_is_layered() can route it past the + * layer on destroy / proc-addr / device creation. */ + instance->obj.loader_magic = (UINT64)(uintptr_t)instance; + for (i = 0; i < instance->physical_device_count; i++) + instance->physical_device[i].obj.loader_magic = (UINT64)(uintptr_t)instance; + } return params.result; } -void WINAPI vkDestroyInstance(VkInstance instance, const VkAllocationCallbacks *pAllocator) +/* The layer re-enters our real create entry points through wine_layer_next_gipa/ + * gdpa with the fixed Khronos signature, so it cannot pass our internal `layered` + * flag. This shim is what the trampoline hands to the layer: a create reached + * through the layer chain is by definition layered. */ +VkResult VKAPI_CALL create_instance_layered(const VkInstanceCreateInfo *create_info, + const VkAllocationCallbacks *allocator, VkInstance *ret) +{ + return create_instance_real(create_info, allocator, ret, TRUE); +} + +/* Layers create their own dispatchable objects (e.g. command buffers used to + * draw the overlay) and call these to stamp the loader dispatch magic on them, + * so subsequent ICD calls recognise them. */ +static VkResult VKAPI_CALL wine_set_instance_loader_data(VkInstance instance, void *object) +{ + ((struct vulkan_client_object *)object)->loader_magic = (UINT64)(uintptr_t)instance; + return VK_SUCCESS; +} + +static VkResult VKAPI_CALL wine_set_device_loader_data(VkDevice device, void *object) +{ + ((struct vulkan_client_object *)object)->loader_magic = (UINT64)(uintptr_t)device; + return VK_SUCCESS; +} + +/* Identify whether the module that called vkCreateInstance presents its frames + * through a D3D path that Steam's GameOverlayRenderer already hooks: DXVK's + * classic D3D8/9 COM present (in d3d8.dll / d3d9.dll) and wined3d's present for + * every D3D version (in wined3d.dll, the old "Damavand" case). Such instances + * must skip the implicit Vulkan layer (the Steam overlay), or two overlays on one + * present path corrupt it and crash the game. DXVK's d3d11/d3d12/dxgi present and + * native Vulkan apps are not hooked by GameOverlayRenderer, so they keep the layer. + * + * Attributing the *caller* per instance is precise: DXVK and wined3d obtain + * vkCreateInstance via vkGetInstanceProcAddr and call it directly from their + * presenting DLL, so the return address lands in that DLL. This replaces the old + * global GetModuleHandleW snapshot, which misclassified a d3d9 game that merely + * had d3d11.dll resident (e.g. via the d2d1 -> d3d10_1 -> d3d10core -> d3d11 + * import chain) and relied on wined3d's private "Damavand" engine-name string. */ +static BOOL module_is_hooked_d3d(void *addr) +{ + static const WCHAR *const hooked_modules[] = { L"d3d8.dll", L"d3d9.dll", L"wined3d.dll" }; + HMODULE module = NULL; + WCHAR path[MAX_PATH], *name, *p; + unsigned int i; + + if (!addr) + return FALSE; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, addr, &module) || !module) + return FALSE; + if (!GetModuleFileNameW(module, path, ARRAY_SIZE(path))) + return FALSE; + for (name = p = path; *p; p++) + if (*p == '\\' || *p == '/') name = p + 1; + for (i = 0; i < ARRAY_SIZE(hooked_modules); i++) + if (!lstrcmpiW(name, hooked_modules[i])) + return TRUE; + return FALSE; +} + +static BOOL instance_creator_presents_through_hooked_d3d(void *ret_addr) +{ + void *frames[8]; + USHORT count, i; + + /* The immediate caller is the primary signal: DXVK's d3d8/d3d9 and wined3d + * obtain vkCreateInstance via vkGetInstanceProcAddr and call it from their + * presenting DLL, so its return address lands there. Also scan a few frames + * deeper, so an intermediate Vulkan-loader / trampoline frame between the d3d + * module and us does not hide it. This is a strict superset of the frame-0 + * check: only d3d8/d3d9/wined3d frames ever match, and DXVK's + * d3d11/d3d12/dxgi present (which keeps the overlay) goes through none of them. */ + if (module_is_hooked_d3d(ret_addr)) + return TRUE; + + count = RtlCaptureStackBackTrace(0, ARRAY_SIZE(frames), frames, NULL); + for (i = 0; i < count; i++) + if (module_is_hooked_d3d(frames[i])) + return TRUE; + return FALSE; +} + +VkResult WINAPI vkCreateInstance(const VkInstanceCreateInfo *create_info, + const VkAllocationCallbacks *allocator, VkInstance *ret) +{ + VkLayerInstanceCreateInfo chain_info, cb_info; + VkLayerInstanceLink links[WINE_VK_MAX_LAYERS]; + VkInstanceCreateInfo local_info; + PFN_vkGetInstanceProcAddr layer_gipa; + PFN_vkCreateInstance layer_create; + unsigned int layer_count, i; + int override; + + TRACE("create_info %p, allocator %p, instance %p\n", create_info, allocator, ret); + + /* Skip the implicit Vulkan layer (Steam overlay) for instances whose frames + * are presented through a D3D path GameOverlayRenderer already hooks; see + * instance_creator_presents_through_hooked_d3d(). A per-app registry override + * (HKCU\Software\Wine\AppDefaults\<exe>\winevulkan : ImplicitLayers) forces the + * layer on (1) or off (0) for a specific game, beyond the global WINE_VK_LAYERS=0. */ + override = wine_vk_layer_app_override(); + if (override == 0) + return create_instance_real(create_info, allocator, ret, FALSE); + if (override != 1 && + instance_creator_presents_through_hooked_d3d(__builtin_return_address(0))) + return create_instance_real(create_info, allocator, ret, FALSE); + + /* Lazy-discover implicit layers on first instance creation. When none is + * active we keep the original zero-overhead path. */ + if (!wine_vk_init_layers() || !(layer_count = wine_vk_layer_count())) + return create_instance_real(create_info, allocator, ret, FALSE); + + layer_gipa = wine_vk_layer_gipa(0); + layer_create = (PFN_vkCreateInstance)layer_gipa(NULL, "vkCreateInstance"); + if (!layer_create) + return create_instance_real(create_info, allocator, ret, FALSE); + + /* Build the loader instance link chain: each layer's "next" points at the + * layer below it, the bottom-most at winevulkan (wine_layer_next_gipa). The + * physical-device "next" skips layers that don't implement one, ending at + * the ICD resolver. Then prepend a LOADER_INSTANCE_CREATE_INFO node to a copy + * of the application's pNext chain; the unix side and thunk strip drop this + * node, so it never reaches the host. */ + for (i = 0; i < layer_count; i++) + { + unsigned int j; + links[i].pNext = i + 1 < layer_count ? &links[i + 1] : NULL; + links[i].pfnNextGetInstanceProcAddr = i + 1 < layer_count ? wine_vk_layer_gipa(i + 1) + : wine_layer_next_gipa; + links[i].pfnNextGetPhysicalDeviceProcAddr = wine_vk_layer_next_gpdpa(); + for (j = i + 1; j < layer_count; j++) + if (wine_vk_layer_gpdpa(j)) { links[i].pfnNextGetPhysicalDeviceProcAddr = wine_vk_layer_gpdpa(j); break; } + } + + cb_info.sType = VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO; + cb_info.pNext = create_info->pNext; + cb_info.function = VK_LOADER_DATA_CALLBACK; + cb_info.u.pfnSetInstanceLoaderData = wine_set_instance_loader_data; + + chain_info.sType = VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO; + chain_info.pNext = &cb_info; + chain_info.function = VK_LAYER_LINK_INFO; + chain_info.u.pLayerInfo = &links[0]; + + local_info = *create_info; + local_info.pNext = &chain_info; + + return layer_create(&local_info, allocator, ret); +} + +void VKAPI_CALL destroy_instance_real(VkInstance instance, const VkAllocationCallbacks *pAllocator) { struct vkDestroyInstance_params params; @@ -440,6 +670,27 @@ void WINAPI vkDestroyInstance(VkInstance instance, const VkAllocationCallbacks * free(instance); } +void WINAPI vkDestroyInstance(VkInstance instance, const VkAllocationCallbacks *pAllocator) +{ + /* Mirror creation: the layer must drop its per-instance state, or the + * stale entry would alias a future instance allocated at this address. + * Route per-handle: an instance that bypassed the layer (D3D presenter, or + * created before the layer activated) must not be sent into the layer's + * teardown with a key it never registered. */ + if (instance && vulkan_client_object_is_layered(instance)) + { + PFN_vkGetInstanceProcAddr layer_gipa = wine_vk_layer_instance_gipa(); + PFN_vkDestroyInstance layer_destroy = layer_gipa ? + (PFN_vkDestroyInstance)layer_gipa(instance, "vkDestroyInstance") : NULL; + if (layer_destroy) + { + layer_destroy(instance, pAllocator); + return; + } + } + destroy_instance_real(instance, pAllocator); +} + static VkResult enum_host_instance_extension_properties(const char *layer_name, uint32_t *count, VkExtensionProperties **properties) { @@ -614,8 +865,8 @@ VkResult WINAPI vkEnumerateInstanceVersion(uint32_t *version) return params.result; } -VkResult WINAPI vkCreateDevice(VkPhysicalDevice physical_device, const VkDeviceCreateInfo *create_info, - const VkAllocationCallbacks *allocator, VkDevice *ret) +VkResult VKAPI_CALL create_device_real(VkPhysicalDevice physical_device, const VkDeviceCreateInfo *create_info, + const VkAllocationCallbacks *allocator, VkDevice *ret, BOOL layered) { struct vulkan_device_extensions extensions = {0}; struct vkCreateDevice_params params; @@ -651,10 +902,94 @@ VkResult WINAPI vkCreateDevice(VkPhysicalDevice physical_device, const VkDeviceC free(device); *ret = NULL; } + else if (layered) + { + /* This device was created through the layer chain: stamp a unique + * dispatch key. Same trick as for instances: device and its queues + * share a unique key, distinct from the instance's. A bypassed device + * keeps the ICD magic so it routes past the layer. */ + device->obj.loader_magic = (UINT64)(uintptr_t)device; + for (VkQueue queue = device->queues, end = queue + queue_count; queue < end; queue++) + queue->obj.loader_magic = (UINT64)(uintptr_t)device; + } return params.result; } -void WINAPI vkDestroyDevice(VkDevice device, const VkAllocationCallbacks *allocator) +/* Layered counterpart handed to the layer by the device trampolines; see + * create_instance_layered. */ +VkResult VKAPI_CALL create_device_layered(VkPhysicalDevice physical_device, + const VkDeviceCreateInfo *create_info, const VkAllocationCallbacks *allocator, VkDevice *ret) +{ + return create_device_real(physical_device, create_info, allocator, ret, TRUE); +} + +VkResult WINAPI vkCreateDevice(VkPhysicalDevice physical_device, const VkDeviceCreateInfo *create_info, + const VkAllocationCallbacks *allocator, VkDevice *ret) +{ + VkLayerDeviceCreateInfo chain_info, cb_info; + VkLayerDeviceLink links[WINE_VK_MAX_LAYERS]; + VkDeviceCreateInfo local_info; + PFN_vkGetInstanceProcAddr layer_gipa; + PFN_vkCreateDevice layer_create; + unsigned int layer_count, i; + + TRACE("physical_device %p, create_info %p, allocator %p, device %p\n", + physical_device, create_info, allocator, ret); + + /* Route per-handle: only a physical device whose instance went through the + * layer (so it carries the instance's unique dispatch key) creates a layered + * device. A bypassed instance's physical device keeps the ICD magic and gets + * the original path, so the layer never sees a device it can't dispatch. */ + if (!vulkan_client_object_is_layered(physical_device) || + !(layer_count = wine_vk_layer_count()) || + !(layer_gipa = wine_vk_layer_instance_gipa())) + return create_device_real(physical_device, create_info, allocator, ret, FALSE); + + /* Need at least one layer that hooks device functions; if none does, + * device-layering is pointless (the chain's device-next would be all + * winevulkan), and the device-participating layer need not be the topmost + * one, so don't gate on the top layer's gdpa specifically. */ + for (i = 0; i < layer_count; i++) if (wine_vk_layer_gdpa(i)) break; + if (i == layer_count) + return create_device_real(physical_device, create_info, allocator, ret, FALSE); + + /* The layer's vkCreateDevice is fetched through its instance gipa. */ + layer_create = (PFN_vkCreateDevice)layer_gipa(NULL, "vkCreateDevice"); + if (!layer_create) + return create_device_real(physical_device, create_info, allocator, ret, FALSE); + + /* Device link chain: each layer's "next" points at the layer below it, the + * bottom-most at winevulkan (wine_layer_next_gipa/gdpa). */ + for (i = 0; i < layer_count; i++) + { + unsigned int j; + links[i].pNext = i + 1 < layer_count ? &links[i + 1] : NULL; + links[i].pfnNextGetInstanceProcAddr = i + 1 < layer_count ? wine_vk_layer_gipa(i + 1) + : wine_layer_next_gipa; + /* an instance-only layer in the stack has no device proc-addr; skip it so + * its next-link never hands a layer below a NULL gdpa, ending at winevulkan */ + links[i].pfnNextGetDeviceProcAddr = wine_layer_next_gdpa; + for (j = i + 1; j < layer_count; j++) + if (wine_vk_layer_gdpa(j)) { links[i].pfnNextGetDeviceProcAddr = wine_vk_layer_gdpa(j); break; } + } + + cb_info.sType = VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO; + cb_info.pNext = create_info->pNext; + cb_info.function = VK_LOADER_DATA_CALLBACK; + cb_info.u.pfnSetDeviceLoaderData = wine_set_device_loader_data; + + chain_info.sType = VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO; + chain_info.pNext = &cb_info; + chain_info.function = VK_LAYER_LINK_INFO; + chain_info.u.pLayerInfo = &links[0]; + + local_info = *create_info; + local_info.pNext = &chain_info; + + return layer_create(physical_device, &local_info, allocator, ret); +} + +void VKAPI_CALL destroy_device_real(VkDevice device, const VkAllocationCallbacks *allocator) { struct vkDestroyDevice_params params; @@ -664,6 +999,24 @@ void WINAPI vkDestroyDevice(VkDevice device, const VkAllocationCallbacks *alloca free(device); } +void WINAPI vkDestroyDevice(VkDevice device, const VkAllocationCallbacks *allocator) +{ + /* Same as vkDestroyInstance: keep the layer's lifecycle symmetric, routed + * per-handle so a bypassed device tears down on the real path. */ + if (device && vulkan_client_object_is_layered(device)) + { + PFN_vkGetDeviceProcAddr layer_gdpa = wine_vk_layer_device_gdpa(); + PFN_vkDestroyDevice layer_destroy = layer_gdpa ? + (PFN_vkDestroyDevice)layer_gdpa(device, "vkDestroyDevice") : NULL; + if (layer_destroy) + { + layer_destroy(device, allocator); + return; + } + } + destroy_device_real(device, allocator); +} + VkResult WINAPI vkCreateCommandPool(VkDevice device, const VkCommandPoolCreateInfo *create_info, const VkAllocationCallbacks *allocator, VkCommandPool *ret) { @@ -721,7 +1074,13 @@ VkResult WINAPI vkAllocateCommandBuffers(VkDevice device, const VkCommandBufferA uint32_t i; for (i = 0; i < allocate_info->commandBufferCount; i++) + { buffers[i] = vulkan_client_object_create(sizeof(*buffers[i])); + /* Children inherit the parent device's dispatch key (a unique key if the + * device was created through the layer, the ICD magic otherwise), as the + * Windows loader does; never key on the global flag. */ + buffers[i]->obj.loader_magic = device->obj.loader_magic; + } params.device = device; params.pAllocateInfo = allocate_info; diff --git a/dlls/winevulkan/make_vulkan b/dlls/winevulkan/make_vulkan index 71994e45788..956e696faef 100755 --- a/dlls/winevulkan/make_vulkan +++ b/dlls/winevulkan/make_vulkan @@ -271,6 +271,31 @@ MANUAL_UNIX_THUNKS = { "vkGetPhysicalDeviceExternalFenceProperties", "vkGetPhysicalDeviceExternalFencePropertiesKHR", "vkGetSwapchainTimeDomainPropertiesEXT", + "vkCreateWin32SurfaceKHR", + "vkDestroySurfaceKHR", + "vkGetPhysicalDeviceSurfaceSupportKHR", + "vkGetPhysicalDeviceSurfaceCapabilitiesKHR", + "vkGetPhysicalDeviceSurfaceCapabilities2KHR", + "vkGetPhysicalDeviceSurfaceFormatsKHR", + "vkGetPhysicalDeviceSurfaceFormats2KHR", + "vkGetPhysicalDeviceSurfacePresentModesKHR", + "vkCreateSwapchainKHR", + "vkDestroySwapchainKHR", + "vkGetSwapchainImagesKHR", + "vkAcquireNextImageKHR", + "vkAcquireNextImage2KHR", + "vkQueuePresentKHR", + "vkCreateDevice", + "vkGetDeviceGroupSurfacePresentModesKHR", + "vkGetPhysicalDevicePresentRectanglesKHR", + "vkGetLatencyTimingsNV", + "vkGetSwapchainTimingPropertiesEXT", + "vkLatencySleepNV", + "vkSetLatencyMarkerNV", + "vkSetLatencySleepModeNV", + "vkSetSwapchainPresentTimingQueueSizeEXT", + "vkWaitForPresent2KHR", + "vkWaitForPresentKHR", } # loader functions which are entirely manually implemented diff --git a/dlls/winevulkan/vk_layer.h b/dlls/winevulkan/vk_layer.h new file mode 100644 index 00000000000..d659e43b364 --- /dev/null +++ b/dlls/winevulkan/vk_layer.h @@ -0,0 +1,119 @@ +/* Wine Vulkan loader/layer interface structures + * + * These structures are vendored from the Khronos Vulkan-Loader project + * (vulkan/vk_layer.h, MIT licensed). They describe the ABI used between the + * Vulkan loader and an installed layer when chaining vkCreateInstance and + * vkCreateDevice. winevulkan needs them to insert Windows PE implicit layers + * (e.g. SteamOverlayVulkanLayer64.dll) into the instance/device call chain. + * + * Only the subset required by winevulkan is reproduced here. Field order and + * layout MUST match the upstream Khronos definition: layer DLLs read these + * structures directly by ABI. + * + * SPDX-License-Identifier: MIT + */ + +#ifndef __WINE_VK_LAYER_H +#define __WINE_VK_LAYER_H + +#include "wine/vulkan.h" + +/* PFN_GetPhysicalDeviceProcAddr is the ICD-private physical-device proc-addr + * resolver used to chain VK_EXT physical-device functions through layers. */ +typedef PFN_vkVoidFunction (VKAPI_PTR *PFN_GetPhysicalDeviceProcAddr)(VkInstance instance, const char *pName); + +/* Loader data callbacks - winevulkan does not implement loader data tagging, + * but the prototypes are needed for the create-info union below. */ +typedef VkResult (VKAPI_PTR *PFN_vkSetInstanceLoaderData)(VkInstance instance, void *object); +typedef VkResult (VKAPI_PTR *PFN_vkSetDeviceLoaderData)(VkDevice device, void *object); + +/* Layer device creation callbacks (loaderFeatures >= LAYER_CREATE_DEVICE). */ +typedef VkResult (VKAPI_PTR *PFN_vkLayerCreateDevice)(VkInstance instance, VkPhysicalDevice physicalDevice, + const VkDeviceCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkDevice *pDevice, + PFN_vkGetInstanceProcAddr layerGIPA, PFN_vkGetDeviceProcAddr *nextGDPA); +typedef void (VKAPI_PTR *PFN_vkLayerDestroyDevice)(VkDevice physicalDevice, const VkAllocationCallbacks *pAllocator, + PFN_vkDestroyDevice destroyFunction); + +typedef enum VkLayerFunction_ +{ + VK_LAYER_LINK_INFO = 0, + VK_LOADER_DATA_CALLBACK = 1, + VK_LOADER_LAYER_CREATE_DEVICE_CALLBACK = 2, + VK_LOADER_FEATURES = 3, +} VkLayerFunction; + +typedef enum VkLoaderFeastureFlagBits +{ + VK_LOADER_FEATURE_PHYSICAL_DEVICE_SORTING = 0x00000001, +} VkLoaderFeatureFlagBits; +typedef VkFlags VkLoaderFeatureFlags; + +/* ---- Instance chain ---- */ + +typedef struct VkLayerInstanceLink_ +{ + struct VkLayerInstanceLink_ *pNext; + PFN_vkGetInstanceProcAddr pfnNextGetInstanceProcAddr; + PFN_GetPhysicalDeviceProcAddr pfnNextGetPhysicalDeviceProcAddr; +} VkLayerInstanceLink; + +typedef struct +{ + VkStructureType sType; /* VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO */ + const void *pNext; + VkLayerFunction function; + union + { + VkLayerInstanceLink *pLayerInfo; + PFN_vkSetInstanceLoaderData pfnSetInstanceLoaderData; + struct + { + PFN_vkLayerCreateDevice pfnLayerCreateDevice; + PFN_vkLayerDestroyDevice pfnLayerDestroyDevice; + } layerDevice; + VkLoaderFeatureFlags loaderFeatures; + } u; +} VkLayerInstanceCreateInfo; + +/* ---- Device chain ---- */ + +typedef struct VkLayerDeviceLink_ +{ + struct VkLayerDeviceLink_ *pNext; + PFN_vkGetInstanceProcAddr pfnNextGetInstanceProcAddr; + PFN_vkGetDeviceProcAddr pfnNextGetDeviceProcAddr; +} VkLayerDeviceLink; + +typedef struct +{ + VkStructureType sType; /* VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO */ + const void *pNext; + VkLayerFunction function; + union + { + VkLayerDeviceLink *pLayerInfo; + PFN_vkSetDeviceLoaderData pfnSetDeviceLoaderData; + } u; +} VkLayerDeviceCreateInfo; + +/* ---- Negotiation interface (loader interface version 2) ---- */ + +typedef enum VkNegotiateLayerStructType +{ + LAYER_NEGOTIATE_UNINTIALIZED = 0, + LAYER_NEGOTIATE_INTERFACE_STRUCT = 1, +} VkNegotiateLayerStructType; + +typedef struct VkNegotiateLayerInterface_ +{ + VkNegotiateLayerStructType sType; + void *pNext; + uint32_t loaderLayerInterfaceVersion; + PFN_vkGetInstanceProcAddr pfnGetInstanceProcAddr; + PFN_vkGetDeviceProcAddr pfnGetDeviceProcAddr; + PFN_GetPhysicalDeviceProcAddr pfnGetPhysicalDeviceProcAddr; +} VkNegotiateLayerInterface; + +typedef VkResult (VKAPI_PTR *PFN_vkNegotiateLoaderLayerInterfaceVersion)(VkNegotiateLayerInterface *pVersionStruct); + +#endif /* __WINE_VK_LAYER_H */ diff --git a/dlls/winevulkan/vulkan_loader.h b/dlls/winevulkan/vulkan_loader.h index 9b0580673e7..175fdad0d4f 100644 --- a/dlls/winevulkan/vulkan_loader.h +++ b/dlls/winevulkan/vulkan_loader.h @@ -38,6 +38,26 @@ /* Magic value defined by Vulkan ICD / Loader spec */ #define VULKAN_ICD_MAGIC_VALUE 0x01CDC0DE +/* Maximum number of Windows implicit Vulkan layers chained at once. */ +#define WINE_VK_MAX_LAYERS 8 + +/* A dispatchable object is "layered" iff its loader_magic was overwritten from + * the ICD sentinel to a per-handle dispatch key when it was created through an + * active implicit layer. loader_magic is the first field of every dispatchable + * (vulkan_client_object) and is write-only inside Wine, so it doubles as a + * per-handle "routed through the layer" marker; children inherit the parent's + * key. Routing destroys / proc-addr / device creation on this (rather than the + * process-global layer set) keeps handles that bypassed the layer out of + * it, even when another handle in the same process did engage it. + * The key is (uintptr_t)instance/device; on i386 a handle could in theory equal + * VULKAN_ICD_MAGIC_VALUE (~29MB) and be misread as non-layered, but a heap + * dispatchable living at exactly that address is not realistic, so the marker is + * left as the raw pointer rather than reserving a high bit across all stamp sites. */ +static inline BOOL vulkan_client_object_is_layered(const void *dispatchable) +{ + return *(const UINT64 *)dispatchable != VULKAN_ICD_MAGIC_VALUE; +} + struct vk_command_pool { struct vulkan_client_object obj; diff --git a/dlls/winevulkan/vulkan_thunks.c b/dlls/winevulkan/vulkan_thunks.c index 7ed99c4ff3c..dc1290fbb1b 100644 --- a/dlls/winevulkan/vulkan_thunks.c +++ b/dlls/winevulkan/vulkan_thunks.c @@ -47519,25 +47519,13 @@ static void convert_VkInitializePerformanceApiInfoINTEL_win32_to_host(const VkIn FIXME("Unexpected pNext\n"); } -#ifdef _WIN64 -static void convert_VkLatencySleepInfoNV_win64_to_host(const VkLatencySleepInfoNV *in, VkLatencySleepInfoNV *out) -{ - if (!in) return; - - out->sType = in->sType; - out->pNext = in->pNext; - out->signalSemaphore = vulkan_semaphore_from_handle(in->signalSemaphore)->host.semaphore; - out->value = in->value; -} -#endif /* _WIN64 */ - -static void convert_VkLatencySleepInfoNV_win32_to_host(const VkLatencySleepInfoNV32 *in, VkLatencySleepInfoNV *out) +static void convert_VkLatencySleepInfoNV_win32_to_unwrapped_host(const VkLatencySleepInfoNV32 *in, VkLatencySleepInfoNV *out) { if (!in) return; out->sType = in->sType; out->pNext = NULL; - out->signalSemaphore = vulkan_semaphore_from_handle(in->signalSemaphore)->host.semaphore; + out->signalSemaphore = in->signalSemaphore; out->value = in->value; if (in->pNext) FIXME("Unexpected pNext\n"); @@ -50215,7 +50203,7 @@ static NTSTATUS thunk64_vkAcquireNextImage2KHR(void *args) TRACE("%p, %p, %p\n", params->device, params->pAcquireInfo, params->pImageIndex); - params->result = vk_funcs->p_vkAcquireNextImage2KHR(params->device, params->pAcquireInfo, params->pImageIndex); + params->result = wine_vkAcquireNextImage2KHR(params->device, params->pAcquireInfo, params->pImageIndex); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -50234,7 +50222,7 @@ static NTSTATUS thunk32_vkAcquireNextImage2KHR(void *args) TRACE("%#x, %#x, %#x\n", params->device, params->pAcquireInfo, params->pImageIndex); convert_VkAcquireNextImageInfoKHR_win32_to_unwrapped_host((const VkAcquireNextImageInfoKHR32 *)UlongToPtr(params->pAcquireInfo), &pAcquireInfo_host); - params->result = vk_funcs->p_vkAcquireNextImage2KHR((VkDevice)UlongToPtr(params->device), &pAcquireInfo_host, (uint32_t *)UlongToPtr(params->pImageIndex)); + params->result = wine_vkAcquireNextImage2KHR((VkDevice)UlongToPtr(params->device), &pAcquireInfo_host, (uint32_t *)UlongToPtr(params->pImageIndex)); return STATUS_SUCCESS; } @@ -50245,7 +50233,7 @@ static NTSTATUS thunk64_vkAcquireNextImageKHR(void *args) TRACE("%p, 0x%s, 0x%s, 0x%s, 0x%s, %p\n", params->device, wine_dbgstr_longlong(params->swapchain), wine_dbgstr_longlong(params->timeout), wine_dbgstr_longlong(params->semaphore), wine_dbgstr_longlong(params->fence), params->pImageIndex); - params->result = vk_funcs->p_vkAcquireNextImageKHR(params->device, params->swapchain, params->timeout, params->semaphore, params->fence, params->pImageIndex); + params->result = wine_vkAcquireNextImageKHR(params->device, params->swapchain, params->timeout, params->semaphore, params->fence, params->pImageIndex); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -50265,7 +50253,7 @@ static NTSTATUS thunk32_vkAcquireNextImageKHR(void *args) TRACE("%#x, 0x%s, 0x%s, 0x%s, 0x%s, %#x\n", params->device, wine_dbgstr_longlong(params->swapchain), wine_dbgstr_longlong(params->timeout), wine_dbgstr_longlong(params->semaphore), wine_dbgstr_longlong(params->fence), params->pImageIndex); - params->result = vk_funcs->p_vkAcquireNextImageKHR((VkDevice)UlongToPtr(params->device), params->swapchain, params->timeout, params->semaphore, params->fence, (uint32_t *)UlongToPtr(params->pImageIndex)); + params->result = wine_vkAcquireNextImageKHR((VkDevice)UlongToPtr(params->device), params->swapchain, params->timeout, params->semaphore, params->fence, (uint32_t *)UlongToPtr(params->pImageIndex)); return STATUS_SUCCESS; } @@ -59441,7 +59429,7 @@ static NTSTATUS thunk64_vkCreateDevice(void *args) init_conversion_context(ctx); convert_VkDeviceCreateInfo_win64_to_host(ctx, params->pCreateInfo, &pCreateInfo_host); - params->result = vk_funcs->p_vkCreateDevice(params->physicalDevice, &pCreateInfo_host, params->pAllocator, params->pDevice); + params->result = wine_vkCreateDevice(params->physicalDevice, &pCreateInfo_host, params->pAllocator, params->pDevice); free_conversion_context(ctx); return STATUS_SUCCESS; } @@ -59467,7 +59455,7 @@ static NTSTATUS thunk32_vkCreateDevice(void *args) init_conversion_context(ctx); convert_VkDeviceCreateInfo_win32_to_host(ctx, (const VkDeviceCreateInfo32 *)UlongToPtr(params->pCreateInfo), &pCreateInfo_host); pDevice_host = UlongToPtr(*(PTR32 *)UlongToPtr(params->pDevice)); - params->result = vk_funcs->p_vkCreateDevice((VkPhysicalDevice)UlongToPtr(params->physicalDevice), &pCreateInfo_host, (const VkAllocationCallbacks *)UlongToPtr(params->pAllocator), &pDevice_host); + params->result = wine_vkCreateDevice((VkPhysicalDevice)UlongToPtr(params->physicalDevice), &pCreateInfo_host, (const VkAllocationCallbacks *)UlongToPtr(params->pAllocator), &pDevice_host); *(PTR32 *)UlongToPtr(params->pDevice) = PtrToUlong(pDevice_host); free_conversion_context(ctx); return STATUS_SUCCESS; @@ -60577,7 +60565,7 @@ static NTSTATUS thunk64_vkCreateSwapchainKHR(void *args) TRACE("%p, %p, %p, %p\n", params->device, params->pCreateInfo, params->pAllocator, params->pSwapchain); - params->result = vk_funcs->p_vkCreateSwapchainKHR(params->device, params->pCreateInfo, params->pAllocator, params->pSwapchain); + params->result = wine_vkCreateSwapchainKHR(params->device, params->pCreateInfo, params->pAllocator, params->pSwapchain); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -60600,7 +60588,7 @@ static NTSTATUS thunk32_vkCreateSwapchainKHR(void *args) init_conversion_context(ctx); convert_VkSwapchainCreateInfoKHR_win32_to_unwrapped_host(ctx, (const VkSwapchainCreateInfoKHR32 *)UlongToPtr(params->pCreateInfo), &pCreateInfo_host); - params->result = vk_funcs->p_vkCreateSwapchainKHR((VkDevice)UlongToPtr(params->device), &pCreateInfo_host, (const VkAllocationCallbacks *)UlongToPtr(params->pAllocator), (VkSwapchainKHR *)UlongToPtr(params->pSwapchain)); + params->result = wine_vkCreateSwapchainKHR((VkDevice)UlongToPtr(params->device), &pCreateInfo_host, (const VkAllocationCallbacks *)UlongToPtr(params->pAllocator), (VkSwapchainKHR *)UlongToPtr(params->pSwapchain)); free_conversion_context(ctx); return STATUS_SUCCESS; } @@ -60783,7 +60771,7 @@ static NTSTATUS thunk64_vkCreateWin32SurfaceKHR(void *args) TRACE("%p, %p, %p, %p\n", params->instance, params->pCreateInfo, params->pAllocator, params->pSurface); - params->result = vk_funcs->p_vkCreateWin32SurfaceKHR(params->instance, params->pCreateInfo, params->pAllocator, params->pSurface); + params->result = wine_vkCreateWin32SurfaceKHR(params->instance, params->pCreateInfo, params->pAllocator, params->pSurface); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -60803,7 +60791,7 @@ static NTSTATUS thunk32_vkCreateWin32SurfaceKHR(void *args) TRACE("%#x, %#x, %#x, %#x\n", params->instance, params->pCreateInfo, params->pAllocator, params->pSurface); convert_VkWin32SurfaceCreateInfoKHR_win32_to_host((const VkWin32SurfaceCreateInfoKHR32 *)UlongToPtr(params->pCreateInfo), &pCreateInfo_host); - params->result = vk_funcs->p_vkCreateWin32SurfaceKHR((VkInstance)UlongToPtr(params->instance), &pCreateInfo_host, (const VkAllocationCallbacks *)UlongToPtr(params->pAllocator), (VkSurfaceKHR *)UlongToPtr(params->pSurface)); + params->result = wine_vkCreateWin32SurfaceKHR((VkInstance)UlongToPtr(params->instance), &pCreateInfo_host, (const VkAllocationCallbacks *)UlongToPtr(params->pAllocator), (VkSurfaceKHR *)UlongToPtr(params->pSurface)); return STATUS_SUCCESS; } @@ -62106,7 +62094,7 @@ static NTSTATUS thunk64_vkDestroySurfaceKHR(void *args) TRACE("%p, 0x%s, %p\n", params->instance, wine_dbgstr_longlong(params->surface), params->pAllocator); - vk_funcs->p_vkDestroySurfaceKHR(params->instance, params->surface, params->pAllocator); + wine_vkDestroySurfaceKHR(params->instance, params->surface, params->pAllocator); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -62122,7 +62110,7 @@ static NTSTATUS thunk32_vkDestroySurfaceKHR(void *args) TRACE("%#x, 0x%s, %#x\n", params->instance, wine_dbgstr_longlong(params->surface), params->pAllocator); - vk_funcs->p_vkDestroySurfaceKHR((VkInstance)UlongToPtr(params->instance), params->surface, (const VkAllocationCallbacks *)UlongToPtr(params->pAllocator)); + wine_vkDestroySurfaceKHR((VkInstance)UlongToPtr(params->instance), params->surface, (const VkAllocationCallbacks *)UlongToPtr(params->pAllocator)); return STATUS_SUCCESS; } @@ -62133,7 +62121,7 @@ static NTSTATUS thunk64_vkDestroySwapchainKHR(void *args) TRACE("%p, 0x%s, %p\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pAllocator); - vk_funcs->p_vkDestroySwapchainKHR(params->device, params->swapchain, params->pAllocator); + wine_vkDestroySwapchainKHR(params->device, params->swapchain, params->pAllocator); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -62149,7 +62137,7 @@ static NTSTATUS thunk32_vkDestroySwapchainKHR(void *args) TRACE("%#x, 0x%s, %#x\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pAllocator); - vk_funcs->p_vkDestroySwapchainKHR((VkDevice)UlongToPtr(params->device), params->swapchain, (const VkAllocationCallbacks *)UlongToPtr(params->pAllocator)); + wine_vkDestroySwapchainKHR((VkDevice)UlongToPtr(params->device), params->swapchain, (const VkAllocationCallbacks *)UlongToPtr(params->pAllocator)); return STATUS_SUCCESS; } @@ -64121,7 +64109,7 @@ static NTSTATUS thunk64_vkGetDeviceGroupSurfacePresentModesKHR(void *args) TRACE("%p, 0x%s, %p\n", params->device, wine_dbgstr_longlong(params->surface), params->pModes); - params->result = vulkan_device_from_handle(params->device)->p_vkGetDeviceGroupSurfacePresentModesKHR(vulkan_device_from_handle(params->device)->host.device, vulkan_surface_from_handle(params->surface)->host.surface, params->pModes); + params->result = wine_vkGetDeviceGroupSurfacePresentModesKHR(params->device, params->surface, params->pModes); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -64138,7 +64126,7 @@ static NTSTATUS thunk32_vkGetDeviceGroupSurfacePresentModesKHR(void *args) TRACE("%#x, 0x%s, %#x\n", params->device, wine_dbgstr_longlong(params->surface), params->pModes); - params->result = vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->p_vkGetDeviceGroupSurfacePresentModesKHR(vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->host.device, vulkan_surface_from_handle(params->surface)->host.surface, (VkDeviceGroupPresentModeFlagsKHR *)UlongToPtr(params->pModes)); + params->result = wine_vkGetDeviceGroupSurfacePresentModesKHR((VkDevice)UlongToPtr(params->device), params->surface, (VkDeviceGroupPresentModeFlagsKHR *)UlongToPtr(params->pModes)); return STATUS_SUCCESS; } @@ -65553,7 +65541,7 @@ static NTSTATUS thunk64_vkGetLatencyTimingsNV(void *args) TRACE("%p, 0x%s, %p\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pLatencyMarkerInfo); - vulkan_device_from_handle(params->device)->p_vkGetLatencyTimingsNV(vulkan_device_from_handle(params->device)->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, params->pLatencyMarkerInfo); + wine_vkGetLatencyTimingsNV(params->device, params->swapchain, params->pLatencyMarkerInfo); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -65574,7 +65562,7 @@ static NTSTATUS thunk32_vkGetLatencyTimingsNV(void *args) init_conversion_context(ctx); convert_VkGetLatencyMarkerInfoNV_win32_to_host(ctx, (VkGetLatencyMarkerInfoNV32 *)UlongToPtr(params->pLatencyMarkerInfo), &pLatencyMarkerInfo_host); - vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->p_vkGetLatencyTimingsNV(vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, &pLatencyMarkerInfo_host); + wine_vkGetLatencyTimingsNV((VkDevice)UlongToPtr(params->device), params->swapchain, &pLatencyMarkerInfo_host); convert_VkGetLatencyMarkerInfoNV_host_to_win32(&pLatencyMarkerInfo_host, (VkGetLatencyMarkerInfoNV32 *)UlongToPtr(params->pLatencyMarkerInfo)); free_conversion_context(ctx); return STATUS_SUCCESS; @@ -66810,7 +66798,7 @@ static NTSTATUS thunk64_vkGetPhysicalDevicePresentRectanglesKHR(void *args) TRACE("%p, 0x%s, %p, %p\n", params->physicalDevice, wine_dbgstr_longlong(params->surface), params->pRectCount, params->pRects); - params->result = vk_funcs->p_vkGetPhysicalDevicePresentRectanglesKHR(params->physicalDevice, params->surface, params->pRectCount, params->pRects); + params->result = wine_vkGetPhysicalDevicePresentRectanglesKHR(params->physicalDevice, params->surface, params->pRectCount, params->pRects); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -66828,7 +66816,7 @@ static NTSTATUS thunk32_vkGetPhysicalDevicePresentRectanglesKHR(void *args) TRACE("%#x, 0x%s, %#x, %#x\n", params->physicalDevice, wine_dbgstr_longlong(params->surface), params->pRectCount, params->pRects); - params->result = vk_funcs->p_vkGetPhysicalDevicePresentRectanglesKHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), params->surface, (uint32_t *)UlongToPtr(params->pRectCount), (VkRect2D *)UlongToPtr(params->pRects)); + params->result = wine_vkGetPhysicalDevicePresentRectanglesKHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), params->surface, (uint32_t *)UlongToPtr(params->pRectCount), (VkRect2D *)UlongToPtr(params->pRects)); return STATUS_SUCCESS; } @@ -67353,7 +67341,7 @@ static NTSTATUS thunk64_vkGetPhysicalDeviceSurfaceCapabilities2KHR(void *args) TRACE("%p, %p, %p\n", params->physicalDevice, params->pSurfaceInfo, params->pSurfaceCapabilities); - params->result = vk_funcs->p_vkGetPhysicalDeviceSurfaceCapabilities2KHR(params->physicalDevice, params->pSurfaceInfo, params->pSurfaceCapabilities); + params->result = wine_vkGetPhysicalDeviceSurfaceCapabilities2KHR(params->physicalDevice, params->pSurfaceInfo, params->pSurfaceCapabilities); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -67377,7 +67365,7 @@ static NTSTATUS thunk32_vkGetPhysicalDeviceSurfaceCapabilities2KHR(void *args) init_conversion_context(ctx); convert_VkPhysicalDeviceSurfaceInfo2KHR_win32_to_unwrapped_host(ctx, (const VkPhysicalDeviceSurfaceInfo2KHR32 *)UlongToPtr(params->pSurfaceInfo), &pSurfaceInfo_host); convert_VkSurfaceCapabilities2KHR_win32_to_host(ctx, (VkSurfaceCapabilities2KHR32 *)UlongToPtr(params->pSurfaceCapabilities), &pSurfaceCapabilities_host); - params->result = vk_funcs->p_vkGetPhysicalDeviceSurfaceCapabilities2KHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), &pSurfaceInfo_host, &pSurfaceCapabilities_host); + params->result = wine_vkGetPhysicalDeviceSurfaceCapabilities2KHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), &pSurfaceInfo_host, &pSurfaceCapabilities_host); convert_VkSurfaceCapabilities2KHR_host_to_win32(&pSurfaceCapabilities_host, (VkSurfaceCapabilities2KHR32 *)UlongToPtr(params->pSurfaceCapabilities)); free_conversion_context(ctx); return STATUS_SUCCESS; @@ -67390,7 +67378,7 @@ static NTSTATUS thunk64_vkGetPhysicalDeviceSurfaceCapabilitiesKHR(void *args) TRACE("%p, 0x%s, %p\n", params->physicalDevice, wine_dbgstr_longlong(params->surface), params->pSurfaceCapabilities); - params->result = vk_funcs->p_vkGetPhysicalDeviceSurfaceCapabilitiesKHR(params->physicalDevice, params->surface, params->pSurfaceCapabilities); + params->result = wine_vkGetPhysicalDeviceSurfaceCapabilitiesKHR(params->physicalDevice, params->surface, params->pSurfaceCapabilities); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -67407,7 +67395,7 @@ static NTSTATUS thunk32_vkGetPhysicalDeviceSurfaceCapabilitiesKHR(void *args) TRACE("%#x, 0x%s, %#x\n", params->physicalDevice, wine_dbgstr_longlong(params->surface), params->pSurfaceCapabilities); - params->result = vk_funcs->p_vkGetPhysicalDeviceSurfaceCapabilitiesKHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), params->surface, (VkSurfaceCapabilitiesKHR *)UlongToPtr(params->pSurfaceCapabilities)); + params->result = wine_vkGetPhysicalDeviceSurfaceCapabilitiesKHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), params->surface, (VkSurfaceCapabilitiesKHR *)UlongToPtr(params->pSurfaceCapabilities)); return STATUS_SUCCESS; } @@ -67418,7 +67406,7 @@ static NTSTATUS thunk64_vkGetPhysicalDeviceSurfaceFormats2KHR(void *args) TRACE("%p, %p, %p, %p\n", params->physicalDevice, params->pSurfaceInfo, params->pSurfaceFormatCount, params->pSurfaceFormats); - params->result = vk_funcs->p_vkGetPhysicalDeviceSurfaceFormats2KHR(params->physicalDevice, params->pSurfaceInfo, params->pSurfaceFormatCount, params->pSurfaceFormats); + params->result = wine_vkGetPhysicalDeviceSurfaceFormats2KHR(params->physicalDevice, params->pSurfaceInfo, params->pSurfaceFormatCount, params->pSurfaceFormats); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -67443,7 +67431,7 @@ static NTSTATUS thunk32_vkGetPhysicalDeviceSurfaceFormats2KHR(void *args) init_conversion_context(ctx); convert_VkPhysicalDeviceSurfaceInfo2KHR_win32_to_unwrapped_host(ctx, (const VkPhysicalDeviceSurfaceInfo2KHR32 *)UlongToPtr(params->pSurfaceInfo), &pSurfaceInfo_host); pSurfaceFormats_host = convert_VkSurfaceFormat2KHR_array_win32_to_host(ctx, (VkSurfaceFormat2KHR32 *)UlongToPtr(params->pSurfaceFormats), *(uint32_t *)UlongToPtr(params->pSurfaceFormatCount)); - params->result = vk_funcs->p_vkGetPhysicalDeviceSurfaceFormats2KHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), &pSurfaceInfo_host, (uint32_t *)UlongToPtr(params->pSurfaceFormatCount), pSurfaceFormats_host); + params->result = wine_vkGetPhysicalDeviceSurfaceFormats2KHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), &pSurfaceInfo_host, (uint32_t *)UlongToPtr(params->pSurfaceFormatCount), pSurfaceFormats_host); convert_VkSurfaceFormat2KHR_array_host_to_win32(pSurfaceFormats_host, (VkSurfaceFormat2KHR32 *)UlongToPtr(params->pSurfaceFormats), *(uint32_t *)UlongToPtr(params->pSurfaceFormatCount)); free_conversion_context(ctx); return STATUS_SUCCESS; @@ -67456,7 +67444,7 @@ static NTSTATUS thunk64_vkGetPhysicalDeviceSurfaceFormatsKHR(void *args) TRACE("%p, 0x%s, %p, %p\n", params->physicalDevice, wine_dbgstr_longlong(params->surface), params->pSurfaceFormatCount, params->pSurfaceFormats); - params->result = vk_funcs->p_vkGetPhysicalDeviceSurfaceFormatsKHR(params->physicalDevice, params->surface, params->pSurfaceFormatCount, params->pSurfaceFormats); + params->result = wine_vkGetPhysicalDeviceSurfaceFormatsKHR(params->physicalDevice, params->surface, params->pSurfaceFormatCount, params->pSurfaceFormats); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -67474,7 +67462,7 @@ static NTSTATUS thunk32_vkGetPhysicalDeviceSurfaceFormatsKHR(void *args) TRACE("%#x, 0x%s, %#x, %#x\n", params->physicalDevice, wine_dbgstr_longlong(params->surface), params->pSurfaceFormatCount, params->pSurfaceFormats); - params->result = vk_funcs->p_vkGetPhysicalDeviceSurfaceFormatsKHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), params->surface, (uint32_t *)UlongToPtr(params->pSurfaceFormatCount), (VkSurfaceFormatKHR *)UlongToPtr(params->pSurfaceFormats)); + params->result = wine_vkGetPhysicalDeviceSurfaceFormatsKHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), params->surface, (uint32_t *)UlongToPtr(params->pSurfaceFormatCount), (VkSurfaceFormatKHR *)UlongToPtr(params->pSurfaceFormats)); return STATUS_SUCCESS; } @@ -67485,7 +67473,7 @@ static NTSTATUS thunk64_vkGetPhysicalDeviceSurfacePresentModesKHR(void *args) TRACE("%p, 0x%s, %p, %p\n", params->physicalDevice, wine_dbgstr_longlong(params->surface), params->pPresentModeCount, params->pPresentModes); - params->result = vulkan_physical_device_from_handle(params->physicalDevice)->instance->p_vkGetPhysicalDeviceSurfacePresentModesKHR(vulkan_physical_device_from_handle(params->physicalDevice)->host.physical_device, params->surface ? vulkan_surface_from_handle(params->surface)->host.surface : 0, params->pPresentModeCount, params->pPresentModes); + params->result = wine_vkGetPhysicalDeviceSurfacePresentModesKHR(params->physicalDevice, params->surface, params->pPresentModeCount, params->pPresentModes); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -67503,7 +67491,7 @@ static NTSTATUS thunk32_vkGetPhysicalDeviceSurfacePresentModesKHR(void *args) TRACE("%#x, 0x%s, %#x, %#x\n", params->physicalDevice, wine_dbgstr_longlong(params->surface), params->pPresentModeCount, params->pPresentModes); - params->result = vulkan_physical_device_from_handle((VkPhysicalDevice)UlongToPtr(params->physicalDevice))->instance->p_vkGetPhysicalDeviceSurfacePresentModesKHR(vulkan_physical_device_from_handle((VkPhysicalDevice)UlongToPtr(params->physicalDevice))->host.physical_device, params->surface ? vulkan_surface_from_handle(params->surface)->host.surface : 0, (uint32_t *)UlongToPtr(params->pPresentModeCount), (VkPresentModeKHR *)UlongToPtr(params->pPresentModes)); + params->result = wine_vkGetPhysicalDeviceSurfacePresentModesKHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), params->surface, (uint32_t *)UlongToPtr(params->pPresentModeCount), (VkPresentModeKHR *)UlongToPtr(params->pPresentModes)); return STATUS_SUCCESS; } @@ -67514,7 +67502,7 @@ static NTSTATUS thunk64_vkGetPhysicalDeviceSurfaceSupportKHR(void *args) TRACE("%p, %u, 0x%s, %p\n", params->physicalDevice, params->queueFamilyIndex, wine_dbgstr_longlong(params->surface), params->pSupported); - params->result = vulkan_physical_device_from_handle(params->physicalDevice)->instance->p_vkGetPhysicalDeviceSurfaceSupportKHR(vulkan_physical_device_from_handle(params->physicalDevice)->host.physical_device, params->queueFamilyIndex, vulkan_surface_from_handle(params->surface)->host.surface, params->pSupported); + params->result = wine_vkGetPhysicalDeviceSurfaceSupportKHR(params->physicalDevice, params->queueFamilyIndex, params->surface, params->pSupported); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -67532,7 +67520,7 @@ static NTSTATUS thunk32_vkGetPhysicalDeviceSurfaceSupportKHR(void *args) TRACE("%#x, %u, 0x%s, %#x\n", params->physicalDevice, params->queueFamilyIndex, wine_dbgstr_longlong(params->surface), params->pSupported); - params->result = vulkan_physical_device_from_handle((VkPhysicalDevice)UlongToPtr(params->physicalDevice))->instance->p_vkGetPhysicalDeviceSurfaceSupportKHR(vulkan_physical_device_from_handle((VkPhysicalDevice)UlongToPtr(params->physicalDevice))->host.physical_device, params->queueFamilyIndex, vulkan_surface_from_handle(params->surface)->host.surface, (VkBool32 *)UlongToPtr(params->pSupported)); + params->result = wine_vkGetPhysicalDeviceSurfaceSupportKHR((VkPhysicalDevice)UlongToPtr(params->physicalDevice), params->queueFamilyIndex, params->surface, (VkBool32 *)UlongToPtr(params->pSupported)); return STATUS_SUCCESS; } @@ -68720,7 +68708,7 @@ static NTSTATUS thunk64_vkGetSwapchainImagesKHR(void *args) TRACE("%p, 0x%s, %p, %p\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pSwapchainImageCount, params->pSwapchainImages); - params->result = vulkan_device_from_handle(params->device)->p_vkGetSwapchainImagesKHR(vulkan_device_from_handle(params->device)->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, params->pSwapchainImageCount, params->pSwapchainImages); + params->result = wine_vkGetSwapchainImagesKHR(params->device, params->swapchain, params->pSwapchainImageCount, params->pSwapchainImages); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -68738,7 +68726,7 @@ static NTSTATUS thunk32_vkGetSwapchainImagesKHR(void *args) TRACE("%#x, 0x%s, %#x, %#x\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pSwapchainImageCount, params->pSwapchainImages); - params->result = vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->p_vkGetSwapchainImagesKHR(vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, (uint32_t *)UlongToPtr(params->pSwapchainImageCount), (VkImage *)UlongToPtr(params->pSwapchainImages)); + params->result = wine_vkGetSwapchainImagesKHR((VkDevice)UlongToPtr(params->device), params->swapchain, (uint32_t *)UlongToPtr(params->pSwapchainImageCount), (VkImage *)UlongToPtr(params->pSwapchainImages)); return STATUS_SUCCESS; } @@ -68781,7 +68769,7 @@ static NTSTATUS thunk64_vkGetSwapchainTimingPropertiesEXT(void *args) TRACE("%p, 0x%s, %p, %p\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pSwapchainTimingProperties, params->pSwapchainTimingPropertiesCounter); - params->result = vulkan_device_from_handle(params->device)->p_vkGetSwapchainTimingPropertiesEXT(vulkan_device_from_handle(params->device)->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, params->pSwapchainTimingProperties, params->pSwapchainTimingPropertiesCounter); + params->result = wine_vkGetSwapchainTimingPropertiesEXT(params->device, params->swapchain, params->pSwapchainTimingProperties, params->pSwapchainTimingPropertiesCounter); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -68801,7 +68789,7 @@ static NTSTATUS thunk32_vkGetSwapchainTimingPropertiesEXT(void *args) TRACE("%#x, 0x%s, %#x, %#x\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pSwapchainTimingProperties, params->pSwapchainTimingPropertiesCounter); convert_VkSwapchainTimingPropertiesEXT_win32_to_host((VkSwapchainTimingPropertiesEXT32 *)UlongToPtr(params->pSwapchainTimingProperties), &pSwapchainTimingProperties_host); - params->result = vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->p_vkGetSwapchainTimingPropertiesEXT(vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, &pSwapchainTimingProperties_host, (uint64_t *)UlongToPtr(params->pSwapchainTimingPropertiesCounter)); + params->result = wine_vkGetSwapchainTimingPropertiesEXT((VkDevice)UlongToPtr(params->device), params->swapchain, &pSwapchainTimingProperties_host, (uint64_t *)UlongToPtr(params->pSwapchainTimingPropertiesCounter)); convert_VkSwapchainTimingPropertiesEXT_host_to_win32(&pSwapchainTimingProperties_host, (VkSwapchainTimingPropertiesEXT32 *)UlongToPtr(params->pSwapchainTimingProperties)); return STATUS_SUCCESS; } @@ -69137,12 +69125,10 @@ static NTSTATUS thunk32_vkInvalidateMappedMemoryRanges(void *args) static NTSTATUS thunk64_vkLatencySleepNV(void *args) { struct vkLatencySleepNV_params *params = args; - VkLatencySleepInfoNV pSleepInfo_host; TRACE("%p, 0x%s, %p\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pSleepInfo); - convert_VkLatencySleepInfoNV_win64_to_host(params->pSleepInfo, &pSleepInfo_host); - params->result = vulkan_device_from_handle(params->device)->p_vkLatencySleepNV(vulkan_device_from_handle(params->device)->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, &pSleepInfo_host); + params->result = wine_vkLatencySleepNV(params->device, params->swapchain, params->pSleepInfo); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -69160,8 +69146,8 @@ static NTSTATUS thunk32_vkLatencySleepNV(void *args) TRACE("%#x, 0x%s, %#x\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pSleepInfo); - convert_VkLatencySleepInfoNV_win32_to_host((const VkLatencySleepInfoNV32 *)UlongToPtr(params->pSleepInfo), &pSleepInfo_host); - params->result = vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->p_vkLatencySleepNV(vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, &pSleepInfo_host); + convert_VkLatencySleepInfoNV_win32_to_unwrapped_host((const VkLatencySleepInfoNV32 *)UlongToPtr(params->pSleepInfo), &pSleepInfo_host); + params->result = wine_vkLatencySleepNV((VkDevice)UlongToPtr(params->device), params->swapchain, &pSleepInfo_host); return STATUS_SUCCESS; } @@ -69487,7 +69473,7 @@ static NTSTATUS thunk64_vkQueuePresentKHR(void *args) init_conversion_context(ctx); convert_VkPresentInfoKHR_win64_to_unwrapped_host(ctx, params->pPresentInfo, &pPresentInfo_host); - params->result = vk_funcs->p_vkQueuePresentKHR(params->queue, &pPresentInfo_host); + params->result = wine_vkQueuePresentKHR(params->queue, &pPresentInfo_host); free_conversion_context(ctx); return STATUS_SUCCESS; } @@ -69509,7 +69495,7 @@ static NTSTATUS thunk32_vkQueuePresentKHR(void *args) init_conversion_context(ctx); convert_VkPresentInfoKHR_win32_to_unwrapped_host(ctx, (const VkPresentInfoKHR32 *)UlongToPtr(params->pPresentInfo), &pPresentInfo_host); - params->result = vk_funcs->p_vkQueuePresentKHR((VkQueue)UlongToPtr(params->queue), &pPresentInfo_host); + params->result = wine_vkQueuePresentKHR((VkQueue)UlongToPtr(params->queue), &pPresentInfo_host); free_conversion_context(ctx); return STATUS_SUCCESS; } @@ -70322,7 +70308,7 @@ static NTSTATUS thunk64_vkSetLatencyMarkerNV(void *args) TRACE("%p, 0x%s, %p\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pLatencyMarkerInfo); - vulkan_device_from_handle(params->device)->p_vkSetLatencyMarkerNV(vulkan_device_from_handle(params->device)->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, params->pLatencyMarkerInfo); + wine_vkSetLatencyMarkerNV(params->device, params->swapchain, params->pLatencyMarkerInfo); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -70340,7 +70326,7 @@ static NTSTATUS thunk32_vkSetLatencyMarkerNV(void *args) TRACE("%#x, 0x%s, %#x\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pLatencyMarkerInfo); convert_VkSetLatencyMarkerInfoNV_win32_to_host((const VkSetLatencyMarkerInfoNV32 *)UlongToPtr(params->pLatencyMarkerInfo), &pLatencyMarkerInfo_host); - vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->p_vkSetLatencyMarkerNV(vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, &pLatencyMarkerInfo_host); + wine_vkSetLatencyMarkerNV((VkDevice)UlongToPtr(params->device), params->swapchain, &pLatencyMarkerInfo_host); return STATUS_SUCCESS; } @@ -70351,7 +70337,7 @@ static NTSTATUS thunk64_vkSetLatencySleepModeNV(void *args) TRACE("%p, 0x%s, %p\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pSleepModeInfo); - params->result = vulkan_device_from_handle(params->device)->p_vkSetLatencySleepModeNV(vulkan_device_from_handle(params->device)->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, params->pSleepModeInfo); + params->result = wine_vkSetLatencySleepModeNV(params->device, params->swapchain, params->pSleepModeInfo); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -70370,7 +70356,7 @@ static NTSTATUS thunk32_vkSetLatencySleepModeNV(void *args) TRACE("%#x, 0x%s, %#x\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pSleepModeInfo); convert_VkLatencySleepModeInfoNV_win32_to_host((const VkLatencySleepModeInfoNV32 *)UlongToPtr(params->pSleepModeInfo), &pSleepModeInfo_host); - params->result = vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->p_vkSetLatencySleepModeNV(vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, &pSleepModeInfo_host); + params->result = wine_vkSetLatencySleepModeNV((VkDevice)UlongToPtr(params->device), params->swapchain, &pSleepModeInfo_host); return STATUS_SUCCESS; } @@ -70441,7 +70427,7 @@ static NTSTATUS thunk64_vkSetSwapchainPresentTimingQueueSizeEXT(void *args) TRACE("%p, 0x%s, %u\n", params->device, wine_dbgstr_longlong(params->swapchain), params->size); - params->result = vulkan_device_from_handle(params->device)->p_vkSetSwapchainPresentTimingQueueSizeEXT(vulkan_device_from_handle(params->device)->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, params->size); + params->result = wine_vkSetSwapchainPresentTimingQueueSizeEXT(params->device, params->swapchain, params->size); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -70458,7 +70444,7 @@ static NTSTATUS thunk32_vkSetSwapchainPresentTimingQueueSizeEXT(void *args) TRACE("%#x, 0x%s, %u\n", params->device, wine_dbgstr_longlong(params->swapchain), params->size); - params->result = vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->p_vkSetSwapchainPresentTimingQueueSizeEXT(vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, params->size); + params->result = wine_vkSetSwapchainPresentTimingQueueSizeEXT((VkDevice)UlongToPtr(params->device), params->swapchain, params->size); return STATUS_SUCCESS; } @@ -71055,7 +71041,7 @@ static NTSTATUS thunk64_vkWaitForPresent2KHR(void *args) TRACE("%p, 0x%s, %p\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pPresentWait2Info); - params->result = vulkan_device_from_handle(params->device)->p_vkWaitForPresent2KHR(vulkan_device_from_handle(params->device)->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, params->pPresentWait2Info); + params->result = wine_vkWaitForPresent2KHR(params->device, params->swapchain, params->pPresentWait2Info); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -71074,7 +71060,7 @@ static NTSTATUS thunk32_vkWaitForPresent2KHR(void *args) TRACE("%#x, 0x%s, %#x\n", params->device, wine_dbgstr_longlong(params->swapchain), params->pPresentWait2Info); convert_VkPresentWait2InfoKHR_win32_to_host((const VkPresentWait2InfoKHR32 *)UlongToPtr(params->pPresentWait2Info), &pPresentWait2Info_host); - params->result = vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->p_vkWaitForPresent2KHR(vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, &pPresentWait2Info_host); + params->result = wine_vkWaitForPresent2KHR((VkDevice)UlongToPtr(params->device), params->swapchain, &pPresentWait2Info_host); return STATUS_SUCCESS; } @@ -71085,7 +71071,7 @@ static NTSTATUS thunk64_vkWaitForPresentKHR(void *args) TRACE("%p, 0x%s, 0x%s, 0x%s\n", params->device, wine_dbgstr_longlong(params->swapchain), wine_dbgstr_longlong(params->presentId), wine_dbgstr_longlong(params->timeout)); - params->result = vulkan_device_from_handle(params->device)->p_vkWaitForPresentKHR(vulkan_device_from_handle(params->device)->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, params->presentId, params->timeout); + params->result = wine_vkWaitForPresentKHR(params->device, params->swapchain, params->presentId, params->timeout); return STATUS_SUCCESS; } #endif /* _WIN64 */ @@ -71103,7 +71089,7 @@ static NTSTATUS thunk32_vkWaitForPresentKHR(void *args) TRACE("%#x, 0x%s, 0x%s, 0x%s\n", params->device, wine_dbgstr_longlong(params->swapchain), wine_dbgstr_longlong(params->presentId), wine_dbgstr_longlong(params->timeout)); - params->result = vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->p_vkWaitForPresentKHR(vulkan_device_from_handle((VkDevice)UlongToPtr(params->device))->host.device, vulkan_swapchain_from_handle(params->swapchain)->host.swapchain, params->presentId, params->timeout); + params->result = wine_vkWaitForPresentKHR((VkDevice)UlongToPtr(params->device), params->swapchain, params->presentId, params->timeout); return STATUS_SUCCESS; } diff --git a/dlls/winevulkan/vulkan_thunks.h b/dlls/winevulkan/vulkan_thunks.h index 6e81be28d0d..e17a28c799f 100644 --- a/dlls/winevulkan/vulkan_thunks.h +++ b/dlls/winevulkan/vulkan_thunks.h @@ -20,16 +20,23 @@ #define WINE_VK_VERSION VK_API_VERSION_1_4 /* Functions for which we have custom implementations outside of the thunks. */ +VkResult wine_vkAcquireNextImage2KHR(VkDevice device, const VkAcquireNextImageInfoKHR *pAcquireInfo, uint32_t *pImageIndex); +VkResult wine_vkAcquireNextImageKHR(VkDevice device, VkSwapchainKHR swapchain, uint64_t timeout, VkSemaphore semaphore, VkFence fence, uint32_t *pImageIndex); VkResult wine_vkAllocateCommandBuffers(VkDevice device, const VkCommandBufferAllocateInfo *pAllocateInfo, VkCommandBuffer *pCommandBuffers); VkResult wine_vkCreateCommandPool(VkDevice device, const VkCommandPoolCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkCommandPool *pCommandPool); VkResult wine_vkCreateDebugReportCallbackEXT(VkInstance instance, const VkDebugReportCallbackCreateInfoEXT *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkDebugReportCallbackEXT *pCallback); VkResult wine_vkCreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkDebugUtilsMessengerEXT *pMessenger); VkResult wine_vkCreateDeferredOperationKHR(VkDevice device, const VkAllocationCallbacks *pAllocator, VkDeferredOperationKHR *pDeferredOperation); +VkResult wine_vkCreateDevice(VkPhysicalDevice physicalDevice, const VkDeviceCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkDevice *pDevice); VkResult wine_vkCreateInstance(const VkInstanceCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkInstance *pInstance); +VkResult wine_vkCreateSwapchainKHR(VkDevice device, const VkSwapchainCreateInfoKHR *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkSwapchainKHR *pSwapchain); +VkResult wine_vkCreateWin32SurfaceKHR(VkInstance instance, const VkWin32SurfaceCreateInfoKHR *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkSurfaceKHR *pSurface); void wine_vkDestroyCommandPool(VkDevice device, VkCommandPool commandPool, const VkAllocationCallbacks *pAllocator); void wine_vkDestroyDebugReportCallbackEXT(VkInstance instance, VkDebugReportCallbackEXT callback, const VkAllocationCallbacks *pAllocator); void wine_vkDestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT messenger, const VkAllocationCallbacks *pAllocator); void wine_vkDestroyDeferredOperationKHR(VkDevice device, VkDeferredOperationKHR operation, const VkAllocationCallbacks *pAllocator); +void wine_vkDestroySurfaceKHR(VkInstance instance, VkSurfaceKHR surface, const VkAllocationCallbacks *pAllocator); +void wine_vkDestroySwapchainKHR(VkDevice device, VkSwapchainKHR swapchain, const VkAllocationCallbacks *pAllocator); VkResult wine_vkEnumerateDeviceLayerProperties(VkPhysicalDevice physicalDevice, uint32_t *pPropertyCount, VkLayerProperties *pProperties); VkResult wine_vkEnumerateInstanceExtensionProperties(const char *pLayerName, uint32_t *pPropertyCount, VkExtensionProperties *pProperties); VkResult wine_vkEnumerateInstanceVersion(uint32_t *pApiVersion); @@ -39,10 +46,28 @@ VkResult wine_vkEnumeratePhysicalDevices(VkInstance instance, uint32_t *pPhysica void wine_vkFreeCommandBuffers(VkDevice device, VkCommandPool commandPool, uint32_t commandBufferCount, const VkCommandBuffer *pCommandBuffers); VkResult wine_vkGetCalibratedTimestampsEXT(VkDevice device, uint32_t timestampCount, const VkCalibratedTimestampInfoKHR *pTimestampInfos, uint64_t *pTimestamps, uint64_t *pMaxDeviation); VkResult wine_vkGetCalibratedTimestampsKHR(VkDevice device, uint32_t timestampCount, const VkCalibratedTimestampInfoKHR *pTimestampInfos, uint64_t *pTimestamps, uint64_t *pMaxDeviation); +VkResult wine_vkGetDeviceGroupSurfacePresentModesKHR(VkDevice device, VkSurfaceKHR surface, VkDeviceGroupPresentModeFlagsKHR *pModes); +void wine_vkGetLatencyTimingsNV(VkDevice device, VkSwapchainKHR swapchain, VkGetLatencyMarkerInfoNV *pLatencyMarkerInfo); VkResult wine_vkGetPhysicalDeviceCalibrateableTimeDomainsEXT(VkPhysicalDevice physicalDevice, uint32_t *pTimeDomainCount, VkTimeDomainKHR *pTimeDomains); VkResult wine_vkGetPhysicalDeviceCalibrateableTimeDomainsKHR(VkPhysicalDevice physicalDevice, uint32_t *pTimeDomainCount, VkTimeDomainKHR *pTimeDomains); void wine_vkGetPhysicalDeviceExternalFenceProperties(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceExternalFenceInfo *pExternalFenceInfo, VkExternalFenceProperties *pExternalFenceProperties); void wine_vkGetPhysicalDeviceExternalFencePropertiesKHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceExternalFenceInfo *pExternalFenceInfo, VkExternalFenceProperties *pExternalFenceProperties); +VkResult wine_vkGetPhysicalDevicePresentRectanglesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t *pRectCount, VkRect2D *pRects); +VkResult wine_vkGetPhysicalDeviceSurfaceCapabilities2KHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceSurfaceInfo2KHR *pSurfaceInfo, VkSurfaceCapabilities2KHR *pSurfaceCapabilities); +VkResult wine_vkGetPhysicalDeviceSurfaceCapabilitiesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, VkSurfaceCapabilitiesKHR *pSurfaceCapabilities); +VkResult wine_vkGetPhysicalDeviceSurfaceFormats2KHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceSurfaceInfo2KHR *pSurfaceInfo, uint32_t *pSurfaceFormatCount, VkSurfaceFormat2KHR *pSurfaceFormats); +VkResult wine_vkGetPhysicalDeviceSurfaceFormatsKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t *pSurfaceFormatCount, VkSurfaceFormatKHR *pSurfaceFormats); +VkResult wine_vkGetPhysicalDeviceSurfacePresentModesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t *pPresentModeCount, VkPresentModeKHR *pPresentModes); +VkResult wine_vkGetPhysicalDeviceSurfaceSupportKHR(VkPhysicalDevice physicalDevice, uint32_t queueFamilyIndex, VkSurfaceKHR surface, VkBool32 *pSupported); +VkResult wine_vkGetSwapchainImagesKHR(VkDevice device, VkSwapchainKHR swapchain, uint32_t *pSwapchainImageCount, VkImage *pSwapchainImages); VkResult wine_vkGetSwapchainTimeDomainPropertiesEXT(VkDevice device, VkSwapchainKHR swapchain, VkSwapchainTimeDomainPropertiesEXT *pSwapchainTimeDomainProperties, uint64_t *pTimeDomainsCounter); +VkResult wine_vkGetSwapchainTimingPropertiesEXT(VkDevice device, VkSwapchainKHR swapchain, VkSwapchainTimingPropertiesEXT *pSwapchainTimingProperties, uint64_t *pSwapchainTimingPropertiesCounter); +VkResult wine_vkLatencySleepNV(VkDevice device, VkSwapchainKHR swapchain, const VkLatencySleepInfoNV *pSleepInfo); +VkResult wine_vkQueuePresentKHR(VkQueue queue, const VkPresentInfoKHR *pPresentInfo); +void wine_vkSetLatencyMarkerNV(VkDevice device, VkSwapchainKHR swapchain, const VkSetLatencyMarkerInfoNV *pLatencyMarkerInfo); +VkResult wine_vkSetLatencySleepModeNV(VkDevice device, VkSwapchainKHR swapchain, const VkLatencySleepModeInfoNV *pSleepModeInfo); +VkResult wine_vkSetSwapchainPresentTimingQueueSizeEXT(VkDevice device, VkSwapchainKHR swapchain, uint32_t size); +VkResult wine_vkWaitForPresent2KHR(VkDevice device, VkSwapchainKHR swapchain, const VkPresentWait2InfoKHR *pPresentWait2Info); +VkResult wine_vkWaitForPresentKHR(VkDevice device, VkSwapchainKHR swapchain, uint64_t presentId, uint64_t timeout); #endif /* __WINE_VULKAN_THUNKS_H */ diff --git a/dlls/winevulkan/wsi.c b/dlls/winevulkan/wsi.c new file mode 100644 index 00000000000..cb8e07ea5c1 --- /dev/null +++ b/dlls/winevulkan/wsi.c @@ -0,0 +1,1092 @@ +#if 0 +#pragma makedep unix +#endif + +#include "config.h" + +#include <unistd.h> +#include <stdlib.h> +#include <string.h> + +#include "vulkan_private.h" +#include "wsi_private.h" +#include "wine/debug.h" +#include "wine/server.h" +#include "ntuser.h" + +WINE_DEFAULT_DEBUG_CHANNEL(vulkan); + +#ifndef ARRAYSIZE +#define ARRAYSIZE(x) (sizeof(x) / sizeof((x)[0])) +#endif + +#ifndef min +#define min(a,b) ((a) < (b) ? (a) : (b)) +#endif + +#define WINE_WSI_MAX_IMAGES 8 + +struct wine_wsi_surface +{ + struct vulkan_surface obj; + HWND hwnd; +}; + +struct wine_wsi_swapchain +{ + struct vulkan_swapchain obj; + struct wine_wsi_surface *surface; + UINT width; + UINT height; + VkFormat format; + UINT image_count; + struct wine_wsi_image images[WINE_WSI_MAX_IMAGES]; + UINT current_image_idx; +}; + +static const WCHAR vulkan_drm_modifier_propW[] = {'V','u','l','k','a','n','D','r','m','M','o','d','i','f','i','e','r',0}; +static const WCHAR vulkan_drm_format_propW[] = {'V','u','l','k','a','n','D','r','m','F','o','r','m','a','t',0}; +/* Identity of the swapchain that currently owns the per-window props above, so + * DestroySwapchain only clears them if a newer swapchain hasn't taken over the + * window (the create-before-destroy resize pattern). */ +static const WCHAR vulkan_swapchain_propW[] = {'V','u','l','k','a','n','S','w','a','p','c','h','a','i','n',0}; + +static uint32_t find_memory_type(struct vulkan_device *device, uint32_t type_bits, VkMemoryPropertyFlags properties) +{ + VkPhysicalDeviceMemoryProperties *mem_props = &device->physical_device->memory_properties; + for (uint32_t i = 0; i < mem_props->memoryTypeCount; ++i) + { + if ((type_bits & (1u << i)) && (mem_props->memoryTypes[i].propertyFlags & properties) == properties) + return i; + } + return -1; +} + +static BOOL physical_device_supports_extension(VkPhysicalDevice physicalDevice, const char *extension_name) +{ + struct vulkan_physical_device *phys_dev = vulkan_physical_device_from_handle(physicalDevice); + VkExtensionProperties *props = NULL; + uint32_t count = 0; + VkResult res; + uint32_t i; + BOOL supported = FALSE; + + res = phys_dev->instance->p_vkEnumerateDeviceExtensionProperties(phys_dev->host.physical_device, NULL, &count, NULL); + TRACE("Checking support for extension %s, initial count = %u, res = %d\n", extension_name, count, res); + if (res != VK_SUCCESS || !count) return FALSE; + + props = malloc(count * sizeof(*props)); + if (!props) return FALSE; + + res = phys_dev->instance->p_vkEnumerateDeviceExtensionProperties(phys_dev->host.physical_device, NULL, &count, props); + if (res == VK_SUCCESS) + { + for (i = 0; i < count; i++) + { + if (!strcmp(props[i].extensionName, extension_name)) + { + supported = TRUE; + break; + } + } + } + + free(props); + return supported; +} + +/* Copy up to *dst_count elements of size elem from src (count elements) into + * dst, updating *dst_count to the number actually copied; returns VK_INCOMPLETE + * when the destination was too small for the full set, else VK_SUCCESS. */ +static VkResult copy_capped(void *dst, uint32_t *dst_count, const void *src, uint32_t count, size_t elem) +{ + uint32_t copy_count = min(*dst_count, count); + memcpy(dst, src, copy_count * elem); + *dst_count = copy_count; + + if (copy_count < count) return VK_INCOMPLETE; + return VK_SUCCESS; +} + +static void query_format_properties2(struct vulkan_physical_device *phys_dev, VkFormat format, VkFormatProperties2 *props) +{ + if (phys_dev->instance->p_vkGetPhysicalDeviceFormatProperties2) + { + phys_dev->instance->p_vkGetPhysicalDeviceFormatProperties2( + phys_dev->host.physical_device, format, props); + } + else if (phys_dev->instance->p_vkGetPhysicalDeviceFormatProperties2KHR) + { + phys_dev->instance->p_vkGetPhysicalDeviceFormatProperties2KHR( + phys_dev->host.physical_device, format, props); + } +} + +VkResult wine_vkCreateDevice(VkPhysicalDevice physicalDevice, const VkDeviceCreateInfo *pCreateInfo, + const VkAllocationCallbacks *pAllocator, VkDevice *pDevice) +{ + VkDeviceCreateInfo create_info = *pCreateInfo; + const char **extensions; + uint32_t count = pCreateInfo->enabledExtensionCount; + BOOL has_external = FALSE; + BOOL has_external_fd = FALSE; + BOOL has_modifier = FALSE; + BOOL support_modifier = FALSE; + VkResult res; + uint32_t i; + + for (i = 0; i < count; i++) + { + if (!strcmp(pCreateInfo->ppEnabledExtensionNames[i], VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME)) + has_external = TRUE; + if (!strcmp(pCreateInfo->ppEnabledExtensionNames[i], VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME)) + has_external_fd = TRUE; + if (!strcmp(pCreateInfo->ppEnabledExtensionNames[i], "VK_EXT_image_drm_format_modifier")) + has_modifier = TRUE; + } + + support_modifier = physical_device_supports_extension(physicalDevice, "VK_EXT_image_drm_format_modifier"); + TRACE("wine_vkCreateDevice: support_modifier = %d, has_modifier = %d, count = %u\n", support_modifier, has_modifier, count); + + if (!has_external || !has_external_fd || (support_modifier && !has_modifier)) + { + uint32_t new_count = count; + if (!has_external) new_count++; + if (!has_external_fd) new_count++; + if (support_modifier && !has_modifier) new_count++; + + extensions = malloc(new_count * sizeof(char *)); + if (!extensions) return VK_ERROR_OUT_OF_HOST_MEMORY; + + memcpy(extensions, pCreateInfo->ppEnabledExtensionNames, count * sizeof(char *)); + if (!has_external) + extensions[count++] = VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME; + if (!has_external_fd) + extensions[count++] = VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME; + if (support_modifier && !has_modifier) + extensions[count++] = "VK_EXT_image_drm_format_modifier"; + + for (i = 0; i < count; i++) + TRACE(" Enabling extension: %s\n", extensions[i]); + + create_info.enabledExtensionCount = count; + create_info.ppEnabledExtensionNames = extensions; + } + else + { + extensions = NULL; + } + + res = vk_funcs->p_vkCreateDevice(physicalDevice, &create_info, NULL, pDevice); + + free(extensions); + return res; +} + +VkResult wine_vkCreateWin32SurfaceKHR(VkInstance instance, const VkWin32SurfaceCreateInfoKHR *pCreateInfo, + const VkAllocationCallbacks *pAllocator, VkSurfaceKHR *pSurface) +{ + struct vulkan_instance *vk_instance = vulkan_instance_from_handle(instance); + struct wine_wsi_surface *surface; + + TRACE("instance %p, pCreateInfo %p, pAllocator %p, pSurface %p\n", instance, pCreateInfo, pAllocator, pSurface); + + surface = calloc(1, sizeof(*surface)); + if (!surface) return VK_ERROR_OUT_OF_HOST_MEMORY; + + vulkan_object_init(&surface->obj.obj, (UINT_PTR)surface); + surface->obj.instance = vk_instance; + surface->hwnd = pCreateInfo->hwnd; + + vk_instance->p_insert_object(vk_instance, &surface->obj.obj); + + *pSurface = (VkSurfaceKHR)&surface->obj; + return VK_SUCCESS; +} + +void wine_vkDestroySurfaceKHR(VkInstance instance, VkSurfaceKHR surface_handle, const VkAllocationCallbacks *pAllocator) +{ + struct vulkan_instance *vk_instance = vulkan_instance_from_handle(instance); + struct wine_wsi_surface *surface = (struct wine_wsi_surface *)vulkan_surface_from_handle(surface_handle); + + TRACE("instance %p, surface %s, pAllocator %p\n", instance, wine_dbgstr_longlong(surface_handle), pAllocator); + + if (!surface) return; + + vk_instance->p_remove_object(vk_instance, &surface->obj.obj); + free(surface); +} + +VkResult wine_vkGetPhysicalDeviceSurfaceSupportKHR(VkPhysicalDevice physicalDevice, uint32_t queueFamilyIndex, + VkSurfaceKHR surface, VkBool32 *pSupported) +{ + TRACE("physicalDevice %p, queueFamilyIndex %u, surface %s, pSupported %p\n", physicalDevice, queueFamilyIndex, wine_dbgstr_longlong(surface), pSupported); + *pSupported = VK_TRUE; + return VK_SUCCESS; +} + +VkResult wine_vkGetPhysicalDeviceSurfaceCapabilitiesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface_handle, + VkSurfaceCapabilitiesKHR *pSurfaceCapabilities) +{ + struct wine_wsi_surface *surface = (struct wine_wsi_surface *)vulkan_surface_from_handle(surface_handle); + RECT rect; + + TRACE("physicalDevice %p, surface %s, pSurfaceCapabilities %p\n", physicalDevice, wine_dbgstr_longlong(surface_handle), pSurfaceCapabilities); + + if (surface && surface->hwnd && NtUserGetClientRect(surface->hwnd, &rect, 96)) + { + pSurfaceCapabilities->currentExtent.width = rect.right - rect.left; + pSurfaceCapabilities->currentExtent.height = rect.bottom - rect.top; + } + else + { + pSurfaceCapabilities->currentExtent.width = 1024; + pSurfaceCapabilities->currentExtent.height = 768; + } + + pSurfaceCapabilities->minImageCount = 2; + pSurfaceCapabilities->maxImageCount = 8; + pSurfaceCapabilities->minImageExtent.width = 1; + pSurfaceCapabilities->minImageExtent.height = 1; + pSurfaceCapabilities->maxImageExtent.width = 16384; + pSurfaceCapabilities->maxImageExtent.height = 16384; + pSurfaceCapabilities->maxImageArrayLayers = 1; + pSurfaceCapabilities->supportedTransforms = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; + pSurfaceCapabilities->currentTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; + pSurfaceCapabilities->supportedCompositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + pSurfaceCapabilities->supportedUsageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT; + + return VK_SUCCESS; +} + +VkResult wine_vkGetPhysicalDeviceSurfaceCapabilities2KHR(VkPhysicalDevice physicalDevice, + const VkPhysicalDeviceSurfaceInfo2KHR *pSurfaceInfo, + VkSurfaceCapabilities2KHR *pSurfaceCapabilities) +{ + TRACE("physicalDevice %p, pSurfaceInfo %p, pSurfaceCapabilities %p\n", physicalDevice, pSurfaceInfo, pSurfaceCapabilities); + return wine_vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, pSurfaceInfo->surface, + &pSurfaceCapabilities->surfaceCapabilities); +} + +VkResult wine_vkGetPhysicalDeviceSurfaceFormatsKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, + uint32_t *pSurfaceFormatCount, VkSurfaceFormatKHR *pSurfaceFormats) +{ + VkSurfaceFormatKHR formats[] = { + { VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }, + { VK_FORMAT_R8G8B8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }, + { VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }, + { VK_FORMAT_R8G8B8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }, + }; + uint32_t count = ARRAYSIZE(formats); + + TRACE("physicalDevice %p, surface %s, pSurfaceFormatCount %p, pSurfaceFormats %p\n", + physicalDevice, wine_dbgstr_longlong(surface), pSurfaceFormatCount, pSurfaceFormats); + + if (!pSurfaceFormats) + { + *pSurfaceFormatCount = count; + return VK_SUCCESS; + } + + return copy_capped(pSurfaceFormats, pSurfaceFormatCount, formats, count, sizeof(VkSurfaceFormatKHR)); +} + +VkResult wine_vkGetPhysicalDeviceSurfaceFormats2KHR(VkPhysicalDevice physicalDevice, + const VkPhysicalDeviceSurfaceInfo2KHR *pSurfaceInfo, + uint32_t *pSurfaceFormatCount, VkSurfaceFormat2KHR *pSurfaceFormats) +{ + uint32_t count; + VkSurfaceFormatKHR *formats; + VkResult res; + uint32_t i; + + TRACE("physicalDevice %p, pSurfaceInfo %p, pSurfaceFormatCount %p, pSurfaceFormats %p\n", + physicalDevice, pSurfaceInfo, pSurfaceFormatCount, pSurfaceFormats); + + if (!pSurfaceFormats) + { + return wine_vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, pSurfaceInfo->surface, pSurfaceFormatCount, NULL); + } + + count = *pSurfaceFormatCount; + /* A zero count with a non-NULL array means "no room"; report VK_INCOMPLETE + * rather than risk malloc(0) returning NULL and looking like an OOM. */ + if (!count) return VK_INCOMPLETE; + formats = malloc(count * sizeof(VkSurfaceFormatKHR)); + if (!formats) return VK_ERROR_OUT_OF_HOST_MEMORY; + + res = wine_vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, pSurfaceInfo->surface, &count, formats); + for (i = 0; i < count; i++) + { + pSurfaceFormats[i].surfaceFormat = formats[i]; + } + *pSurfaceFormatCount = count; + free(formats); + return res; +} + +VkResult wine_vkGetPhysicalDeviceSurfacePresentModesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, + uint32_t *pPresentModeCount, VkPresentModeKHR *pPresentModes) +{ + VkPresentModeKHR modes[] = { + VK_PRESENT_MODE_FIFO_KHR, + VK_PRESENT_MODE_MAILBOX_KHR, + VK_PRESENT_MODE_IMMEDIATE_KHR, + }; + uint32_t count = ARRAYSIZE(modes); + + TRACE("physicalDevice %p, surface %s, pPresentModeCount %p, pPresentModes %p\n", + physicalDevice, wine_dbgstr_longlong(surface), pPresentModeCount, pPresentModes); + + if (!pPresentModes) + { + *pPresentModeCount = count; + return VK_SUCCESS; + } + + return copy_capped(pPresentModes, pPresentModeCount, modes, count, sizeof(VkPresentModeKHR)); +} + +VkResult wine_vkCreateSwapchainKHR(VkDevice device, const VkSwapchainCreateInfoKHR *pCreateInfo, + const VkAllocationCallbacks *pAllocator, VkSwapchainKHR *pSwapchain) +{ + struct vulkan_device *vk_device = vulkan_device_from_handle(device); + struct wine_wsi_surface *surface = (struct wine_wsi_surface *)vulkan_surface_from_handle(pCreateInfo->surface); + struct wine_wsi_swapchain *swapchain; + VkResult res; + uint32_t i; + PFN_vkGetImageDrmFormatModifierPropertiesEXT p_vkGetImageDrmFormatModifierPropertiesEXT; + PFN_vkGetMemoryFdKHR p_vkGetMemoryFdKHR; + VkImageDrmFormatModifierPropertiesEXT modifier_props; + VkImageSubresource subresource; + VkSubresourceLayout sub_layout; + VkDrmFormatModifierPropertiesListEXT modifier_list; + VkFormatProperties2 format_props; + VkDrmFormatModifierPropertiesEXT *modifier_props_arr = NULL; + uint64_t *modifiers = NULL; + uint32_t active_count = 0; + BOOL use_modifier_tiling = FALSE; + uint32_t j; + uint32_t drm_format; + HWND toplevel_hwnd; + + TRACE("device %p, pCreateInfo %p, pAllocator %p, pSwapchain %p\n", device, pCreateInfo, pAllocator, pSwapchain); + + swapchain = calloc(1, sizeof(*swapchain)); + if (!swapchain) return VK_ERROR_OUT_OF_HOST_MEMORY; + + for (i = 0; i < WINE_WSI_MAX_IMAGES; i++) + swapchain->images[i].dmabuf_fd = -1; + + vulkan_object_init(&swapchain->obj.obj, (UINT_PTR)swapchain); + swapchain->surface = surface; + swapchain->width = pCreateInfo->imageExtent.width; + swapchain->height = pCreateInfo->imageExtent.height; + swapchain->format = pCreateInfo->imageFormat; + /* Triple-buffer floor: under cross-process presentation the compositor can + * hold one buffer on-screen plus one queued, so fewer than three images + * starves the producer (it then blocks every frame on the per-image present + * fence / DMABUF implicit-sync read-completion). Creating more images than + * the app's minImageCount is spec-compliant: it learns the real count from + * vkGetSwapchainImagesKHR. Capped at the images[] array size. */ + swapchain->image_count = pCreateInfo->minImageCount; + if (swapchain->image_count < 3) swapchain->image_count = 3; + if (swapchain->image_count > WINE_WSI_MAX_IMAGES) swapchain->image_count = WINE_WSI_MAX_IMAGES; + + p_vkGetImageDrmFormatModifierPropertiesEXT = (PFN_vkGetImageDrmFormatModifierPropertiesEXT) + vk_funcs->p_vkGetDeviceProcAddr(vk_device->host.device, "vkGetImageDrmFormatModifierPropertiesEXT"); + + /* Same entry point for every image, so resolve it once outside the loop. */ + p_vkGetMemoryFdKHR = (PFN_vkGetMemoryFdKHR)vk_funcs->p_vkGetDeviceProcAddr(vk_device->host.device, "vkGetMemoryFdKHR"); + if (!p_vkGetMemoryFdKHR) + { + ERR("vkGetMemoryFdKHR not found!\n"); + res = VK_ERROR_INITIALIZATION_FAILED; + goto failed; + } + + if (p_vkGetImageDrmFormatModifierPropertiesEXT) + { + memset(&modifier_list, 0, sizeof(modifier_list)); + modifier_list.sType = VK_STRUCTURE_TYPE_DRM_FORMAT_MODIFIER_PROPERTIES_LIST_EXT; + modifier_list.pNext = NULL; + + memset(&format_props, 0, sizeof(format_props)); + format_props.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2; + format_props.pNext = &modifier_list; + + query_format_properties2(vk_device->physical_device, pCreateInfo->imageFormat, &format_props); + + if (modifier_list.drmFormatModifierCount > 0) + { + modifier_props_arr = malloc(modifier_list.drmFormatModifierCount * sizeof(*modifier_props_arr)); + if (modifier_props_arr) + { + modifier_list.pDrmFormatModifierProperties = modifier_props_arr; + + query_format_properties2(vk_device->physical_device, pCreateInfo->imageFormat, &format_props); + + modifiers = malloc(modifier_list.drmFormatModifierCount * sizeof(*modifiers)); + if (modifiers) + { + for (j = 0; j < modifier_list.drmFormatModifierCount; j++) + { + if (modifier_props_arr[j].drmFormatModifier == 0) /* DRM_FORMAT_MOD_LINEAR */ + { + modifiers[active_count++] = modifier_props_arr[j].drmFormatModifier; + break; + } + } + if (active_count > 0) + { + use_modifier_tiling = TRUE; + } + else + { + free(modifiers); + modifiers = NULL; + } + } + free(modifier_props_arr); + } + } + } + + for (i = 0; i < swapchain->image_count; i++) + { + VkExternalMemoryImageCreateInfo external_image_info; + VkImageDrmFormatModifierListCreateInfoEXT modifier_list_info; + VkImageCreateInfo image_info; + VkMemoryRequirements mem_reqs; + uint32_t memory_type_idx; + VkExportMemoryAllocateInfo export_alloc_info; + VkMemoryAllocateInfo alloc_info; + VkMemoryGetFdInfoKHR get_fd_info; + NTSTATUS status; + + external_image_info.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO; + external_image_info.pNext = NULL; + external_image_info.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + + memset(&image_info, 0, sizeof(image_info)); + image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + image_info.imageType = VK_IMAGE_TYPE_2D; + image_info.format = swapchain->format; + image_info.extent.width = swapchain->width; + image_info.extent.height = swapchain->height; + image_info.extent.depth = 1; + image_info.mipLevels = 1; + image_info.arrayLayers = 1; + image_info.samples = VK_SAMPLE_COUNT_1_BIT; + image_info.usage = pCreateInfo->imageUsage | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + image_info.sharingMode = pCreateInfo->imageSharingMode; + /* Forward the queue families for VK_SHARING_MODE_CONCURRENT; without them + * vkCreateImage is invalid (VUID-VkImageCreateInfo-sharingMode-00942) and + * the swapchain creation would fail for a multi-queue app. Ignored for the + * common EXCLUSIVE mode. */ + image_info.queueFamilyIndexCount = pCreateInfo->queueFamilyIndexCount; + image_info.pQueueFamilyIndices = pCreateInfo->pQueueFamilyIndices; + image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + if (use_modifier_tiling) + { + memset(&modifier_list_info, 0, sizeof(modifier_list_info)); + modifier_list_info.sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_LIST_CREATE_INFO_EXT; + modifier_list_info.pNext = &external_image_info; + modifier_list_info.drmFormatModifierCount = active_count; + modifier_list_info.pDrmFormatModifiers = modifiers; + + image_info.pNext = &modifier_list_info; + image_info.tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT; + } + else + { + image_info.pNext = &external_image_info; + image_info.tiling = VK_IMAGE_TILING_OPTIMAL; + } + + export_alloc_info.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO; + export_alloc_info.pNext = NULL; + export_alloc_info.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + + res = vk_device->p_vkCreateImage(vk_device->host.device, &image_info, NULL, &swapchain->images[i].image); + if (res != VK_SUCCESS) + { + ERR("Failed to create VkImage %u: %d\n", i, res); + goto failed; + } + + vk_device->p_vkGetImageMemoryRequirements(vk_device->host.device, swapchain->images[i].image, &mem_reqs); + + memory_type_idx = find_memory_type(vk_device, mem_reqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + /* Some (notably integrated) GPUs expose the DMABUF-exportable memory type + * without DEVICE_LOCAL; fall back to any compatible type before failing. */ + if (memory_type_idx == -1) + memory_type_idx = find_memory_type(vk_device, mem_reqs.memoryTypeBits, 0); + if (memory_type_idx == -1) + { + ERR("Failed to find suitable memory type for image %u\n", i); + res = VK_ERROR_INITIALIZATION_FAILED; + goto failed; + } + + alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc_info.pNext = &export_alloc_info; + alloc_info.allocationSize = mem_reqs.size; + alloc_info.memoryTypeIndex = memory_type_idx; + + res = vk_device->p_vkAllocateMemory(vk_device->host.device, &alloc_info, NULL, &swapchain->images[i].memory); + if (res != VK_SUCCESS) + { + ERR("Failed to allocate memory for image %u: %d\n", i, res); + goto failed; + } + + res = vk_device->p_vkBindImageMemory(vk_device->host.device, swapchain->images[i].image, swapchain->images[i].memory, 0); + if (res != VK_SUCCESS) + { + ERR("Failed to bind memory for image %u: %d\n", i, res); + goto failed; + } + + get_fd_info.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR; + get_fd_info.pNext = NULL; + get_fd_info.memory = swapchain->images[i].memory; + get_fd_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + + res = p_vkGetMemoryFdKHR(vk_device->host.device, &get_fd_info, &swapchain->images[i].dmabuf_fd); + if (res != VK_SUCCESS) + { + ERR("Failed to export memory fd for image %u: %d\n", i, res); + goto failed; + } + + status = wine_server_fd_to_handle(swapchain->images[i].dmabuf_fd, GENERIC_READ, 0, &swapchain->images[i].win32_handle); + close(swapchain->images[i].dmabuf_fd); + swapchain->images[i].dmabuf_fd = -1; + if (status != STATUS_SUCCESS) + { + ERR("Failed to convert fd to handle for image %u: %x\n", i, (unsigned int)status); + res = VK_ERROR_INITIALIZATION_FAILED; + goto failed; + } + + swapchain->images[i].drm_modifier = 0x00ffffffffffffffULL; /* DRM_FORMAT_MOD_INVALID */ + /* vkGetImageDrmFormatModifierPropertiesEXT is only valid for images created + * with VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT. When LINEAR was unavailable + * the image is OPTIMAL-tiled, so leave the modifier INVALID (the compositor + * imports it via the legacy implicit-modifier path) instead of issuing an + * invalid query. */ + if (use_modifier_tiling && p_vkGetImageDrmFormatModifierPropertiesEXT) + { + modifier_props.sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_PROPERTIES_EXT; + modifier_props.pNext = NULL; + if (p_vkGetImageDrmFormatModifierPropertiesEXT(vk_device->host.device, swapchain->images[i].image, &modifier_props) == VK_SUCCESS) + { + swapchain->images[i].drm_modifier = modifier_props.drmFormatModifier; + TRACE("Image %u has DRM modifier %s\n", i, wine_dbgstr_longlong(swapchain->images[i].drm_modifier)); + } + } + + /* vkGetImageSubresourceLayout is only valid for LINEAR or + * DRM_FORMAT_MODIFIER tiling (VUID-vkGetImageSubresourceLayout-image-07790). + * For the OPTIMAL fallback the layout is undefined, so leave stride 0 (the + * receiver derives width*bpp) and let the compositor resolve the real + * layout implicitly from the DRM_FORMAT_MOD_INVALID modifier. */ + if (use_modifier_tiling) + { + /* A DRM_FORMAT_MODIFIER-tiled image requires a memory-plane aspect + * (VUID-vkGetImageSubresourceLayout-tiling-08717), not COLOR; our + * formats are single-plane so plane 0 carries the row pitch. */ + subresource.aspectMask = VK_IMAGE_ASPECT_MEMORY_PLANE_0_BIT_EXT; + subresource.mipLevel = 0; + subresource.arrayLayer = 0; + vk_device->p_vkGetImageSubresourceLayout(vk_device->host.device, swapchain->images[i].image, &subresource, &sub_layout); + swapchain->images[i].stride = sub_layout.rowPitch; + TRACE("Image %u has stride %u, offset %s, size %s\n", i, (unsigned int)swapchain->images[i].stride, + wine_dbgstr_longlong(sub_layout.offset), wine_dbgstr_longlong(sub_layout.size)); + } + else + { + swapchain->images[i].stride = 0; + } + + { + VkFenceCreateInfo fence_info = { .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; + if (vk_device->p_vkCreateFence(vk_device->host.device, &fence_info, NULL, + &swapchain->images[i].present_fence) != VK_SUCCESS) + swapchain->images[i].present_fence = VK_NULL_HANDLE; /* degrade to no back-pressure */ + swapchain->images[i].present_pending = FALSE; + } + } + + if (swapchain->image_count > 0) + { + drm_format = 0x34325241; /* DRM_FORMAT_ARGB8888 */ + if (swapchain->format == VK_FORMAT_R8G8B8A8_UNORM || swapchain->format == VK_FORMAT_R8G8B8A8_SRGB) + drm_format = 0x34324241; /* DRM_FORMAT_ABGR8888 */ + else if (swapchain->format == VK_FORMAT_A2B10G10R10_UNORM_PACK32) + drm_format = 0x30333241; /* DRM_FORMAT_ABGR2101010 */ + else if (swapchain->format == VK_FORMAT_A2R10G10B10_UNORM_PACK32) + drm_format = 0x30333252; /* DRM_FORMAT_ARGB2101010 */ + + toplevel_hwnd = NtUserGetAncestor(swapchain->surface->hwnd, GA_ROOT); + NtUserSetProp(swapchain->surface->hwnd, vulkan_drm_modifier_propW, (HANDLE)(ULONG_PTR)(swapchain->images[0].drm_modifier + 1)); + NtUserSetProp(swapchain->surface->hwnd, vulkan_drm_format_propW, (HANDLE)(ULONG_PTR)drm_format); + NtUserSetProp(swapchain->surface->hwnd, vulkan_swapchain_propW, (HANDLE)swapchain); + if (toplevel_hwnd && toplevel_hwnd != swapchain->surface->hwnd) + { + NtUserSetProp(toplevel_hwnd, vulkan_drm_modifier_propW, (HANDLE)(ULONG_PTR)(swapchain->images[0].drm_modifier + 1)); + NtUserSetProp(toplevel_hwnd, vulkan_drm_format_propW, (HANDLE)(ULONG_PTR)drm_format); + } + } + + vk_device->physical_device->instance->p_insert_object(vk_device->physical_device->instance, &swapchain->obj.obj); + + *pSwapchain = (VkSwapchainKHR)(UINT_PTR)&swapchain->obj; + free(modifiers); + return VK_SUCCESS; + +failed: + for (i = 0; i < swapchain->image_count; i++) + { + if (swapchain->images[i].present_fence) + vk_device->p_vkDestroyFence(vk_device->host.device, swapchain->images[i].present_fence, NULL); + if (swapchain->images[i].image) + vk_device->p_vkDestroyImage(vk_device->host.device, swapchain->images[i].image, NULL); + if (swapchain->images[i].memory) + vk_device->p_vkFreeMemory(vk_device->host.device, swapchain->images[i].memory, NULL); + if (swapchain->images[i].dmabuf_fd >= 0) + close(swapchain->images[i].dmabuf_fd); + if (swapchain->images[i].win32_handle) + NtClose(swapchain->images[i].win32_handle); + } + free(swapchain); + free(modifiers); + return res; +} + +void wine_vkDestroySwapchainKHR(VkDevice device, VkSwapchainKHR swapchain_handle, const VkAllocationCallbacks *pAllocator) +{ + struct vulkan_device *vk_device = vulkan_device_from_handle(device); + struct wine_wsi_swapchain *swapchain = (struct wine_wsi_swapchain *)vulkan_swapchain_from_handle(swapchain_handle); + uint32_t i; + + TRACE("device %p, swapchain %s, pAllocator %p\n", device, wine_dbgstr_longlong(swapchain_handle), pAllocator); + + if (!swapchain) return; + + vk_device->physical_device->instance->p_remove_object(vk_device->physical_device->instance, &swapchain->obj.obj); + + for (i = 0; i < swapchain->image_count; i++) + { + if (swapchain->images[i].present_fence) + { + /* a fence with a pending submission must not be destroyed in flight */ + if (swapchain->images[i].present_pending) + vk_device->p_vkWaitForFences(vk_device->host.device, 1, + &swapchain->images[i].present_fence, VK_TRUE, UINT64_MAX); + vk_device->p_vkDestroyFence(vk_device->host.device, swapchain->images[i].present_fence, NULL); + } + if (swapchain->images[i].image) + vk_device->p_vkDestroyImage(vk_device->host.device, swapchain->images[i].image, NULL); + if (swapchain->images[i].memory) + vk_device->p_vkFreeMemory(vk_device->host.device, swapchain->images[i].memory, NULL); + if (swapchain->images[i].dmabuf_fd >= 0) + close(swapchain->images[i].dmabuf_fd); + if (swapchain->images[i].win32_handle) + NtClose(swapchain->images[i].win32_handle); + } + + /* Drop the per-window Vulkan props if this swapchain still owns them; a newer + * swapchain on the same window (the create-before-destroy resize pattern) has + * replaced the ownership marker, so we must not clear the props it just set. + * The shared toplevel props are left as a coarse "hosts Vulkan content" flag. + * No present can race this: presents only run while the swapchain is alive. */ + if (swapchain->surface && + NtUserGetProp(swapchain->surface->hwnd, vulkan_swapchain_propW) == (HANDLE)swapchain) + { + NtUserRemoveProp(swapchain->surface->hwnd, vulkan_swapchain_propW); + NtUserRemoveProp(swapchain->surface->hwnd, vulkan_drm_format_propW); + NtUserRemoveProp(swapchain->surface->hwnd, vulkan_drm_modifier_propW); + } + + free(swapchain); +} + +VkResult wine_vkGetSwapchainImagesKHR(VkDevice device, VkSwapchainKHR swapchain_handle, + uint32_t *pSwapchainImageCount, VkImage *pSwapchainImages) +{ + struct wine_wsi_swapchain *swapchain = (struct wine_wsi_swapchain *)vulkan_swapchain_from_handle(swapchain_handle); + uint32_t count; + uint32_t i; + + TRACE("device %p, swapchain %s, pSwapchainImageCount %p, pSwapchainImages %p\n", device, wine_dbgstr_longlong(swapchain_handle), pSwapchainImageCount, pSwapchainImages); + + if (!pSwapchainImages) + { + *pSwapchainImageCount = swapchain->image_count; + return VK_SUCCESS; + } + + count = min(*pSwapchainImageCount, swapchain->image_count); + for (i = 0; i < count; i++) + { + pSwapchainImages[i] = swapchain->images[i].image; + } + *pSwapchainImageCount = count; + + if (count < swapchain->image_count) return VK_INCOMPLETE; + return VK_SUCCESS; +} + +VkResult wine_vkAcquireNextImageKHR(VkDevice device, VkSwapchainKHR swapchain_handle, uint64_t timeout, + VkSemaphore semaphore, VkFence fence, uint32_t *pImageIndex) +{ + struct vulkan_device *vk_device = vulkan_device_from_handle(device); + struct wine_wsi_swapchain *swapchain = (struct wine_wsi_swapchain *)vulkan_swapchain_from_handle(swapchain_handle); + VkResult res = VK_SUCCESS; + VkSemaphore host_semaphore = VK_NULL_HANDLE; + VkFence host_fence = VK_NULL_HANDLE; + VkSubmitInfo submit; + + TRACE("device %p, swapchain %s, timeout %s, semaphore %s, fence %s, pImageIndex %p\n", + device, wine_dbgstr_longlong(swapchain_handle), wine_dbgstr_longlong(timeout), + wine_dbgstr_longlong(semaphore), wine_dbgstr_longlong(fence), pImageIndex); + + /* Back-pressure: don't hand the image back until the queue finished the work + * up to its previous present, so the app can't render over a frame the GPU + * is still processing. */ + { + uint32_t next_idx = (swapchain->current_image_idx + 1) % swapchain->image_count; + struct wine_wsi_image *image = &swapchain->images[next_idx]; + + if (image->present_pending && image->present_fence != VK_NULL_HANDLE) + { + VkResult wait = vk_device->p_vkWaitForFences(vk_device->host.device, 1, + &image->present_fence, VK_TRUE, timeout); + if (wait == VK_SUCCESS) + { + vk_device->p_vkResetFences(vk_device->host.device, 1, &image->present_fence); + image->present_pending = FALSE; + } + else if (timeout == 0) + { + /* Non-blocking poll and the previous present's GPU work isn't done: + * report that no image is ready rather than handing back an in-flight + * one, and don't advance so the round-robin stays in sync. */ + return VK_NOT_READY; + } + /* A timed wait that expired proceeds best-effort: the fence stays pending + * (the next present skips re-arming it). */ + } + + swapchain->current_image_idx = next_idx; + *pImageIndex = next_idx; + } + + if (semaphore != VK_NULL_HANDLE) + host_semaphore = vulkan_semaphore_from_handle(semaphore)->host.semaphore; + if (fence != VK_NULL_HANDLE) + host_fence = vulkan_fence_from_handle(fence)->host.fence; + + if (host_semaphore != VK_NULL_HANDLE || host_fence != VK_NULL_HANDLE) + { + submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit.pNext = NULL; + submit.waitSemaphoreCount = 0; + submit.pWaitSemaphores = NULL; + submit.pWaitDstStageMask = NULL; + submit.commandBufferCount = 0; + submit.pCommandBuffers = NULL; + submit.signalSemaphoreCount = host_semaphore != VK_NULL_HANDLE ? 1 : 0; + submit.pSignalSemaphores = &host_semaphore; + + if (vk_device->queue_count > 0) + { + res = vk_device->p_vkQueueSubmit(vk_device->queues[0].host.queue, 1, &submit, host_fence); + } + else + { + WARN("No queues available to signal acquire semaphore/fence!\n"); + } + } + + return res; +} + +VkResult wine_vkAcquireNextImage2KHR(VkDevice device, const VkAcquireNextImageInfoKHR *pAcquireInfo, uint32_t *pImageIndex) +{ + TRACE("device %p, pAcquireInfo %p, pImageIndex %p\n", device, pAcquireInfo, pImageIndex); + return wine_vkAcquireNextImageKHR(device, pAcquireInfo->swapchain, pAcquireInfo->timeout, + pAcquireInfo->semaphore, pAcquireInfo->fence, pImageIndex); +} + +/* Signal the per-swapchain present fences from index `start` with empty, + * ordering-independent submits. The fence at index 0 is normally signalled by + * the real wait-semaphore submit, so callers pass start=1 when that ran and + * start=0 otherwise. See the note at the call site about why fences past the + * first may signal marginally early. */ +static void wsi_signal_present_fences(struct vulkan_device *vk_device, struct vulkan_queue *vk_queue, + const VkSwapchainPresentFenceInfoKHR *present_fence_info, uint32_t start) +{ + uint32_t i; + + if (!present_fence_info || !present_fence_info->pFences) return; + + for (i = start; i < present_fence_info->swapchainCount; i++) + { + VkSubmitInfo submit = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO }; + VkResult submit_res; + + if (present_fence_info->pFences[i] == VK_NULL_HANDLE) continue; + + submit_res = vk_device->p_vkQueueSubmit(vk_queue->host.queue, 1, &submit, + present_fence_info->pFences[i]); + if (submit_res != VK_SUCCESS) + ERR("vkQueueSubmit failed for present fence %u: %d\n", i, submit_res); + } +} + +VkResult wine_vkQueuePresentKHR(VkQueue queue, const VkPresentInfoKHR *pPresentInfo) +{ + struct vulkan_queue *vk_queue = vulkan_queue_from_handle(queue); + struct vulkan_device *vk_device = vk_queue->device; + const VkSwapchainPresentFenceInfoKHR *present_fence_info = find_next_struct(pPresentInfo->pNext, VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_FENCE_INFO_KHR); + uint32_t i; + VkSemaphore stack_semaphores[16]; + VkPipelineStageFlags stack_stages[16]; + VkSemaphore *host_semaphores = NULL; + VkPipelineStageFlags *stages = NULL; + VkSubmitInfo submit; + VkResult submit_res; + VkFence submit_fence; + VkResult ret = VK_SUCCESS; + + TRACE("queue %p, pPresentInfo %p\n", queue, pPresentInfo); + + if (pPresentInfo->waitSemaphoreCount > 0) + { + VkFence fence0 = (present_fence_info && present_fence_info->swapchainCount > 0 && present_fence_info->pFences) + ? present_fence_info->pFences[0] : VK_NULL_HANDLE; + + /* Avoid a per-present heap allocation for the common small wait-count; + * fall back to malloc only for unusually large counts. */ + if (pPresentInfo->waitSemaphoreCount <= ARRAYSIZE(stack_semaphores)) + { + host_semaphores = stack_semaphores; + stages = stack_stages; + } + else + { + host_semaphores = malloc(pPresentInfo->waitSemaphoreCount * sizeof(*host_semaphores)); + stages = malloc(pPresentInfo->waitSemaphoreCount * sizeof(*stages)); + } + + if (host_semaphores && stages) + { + for (i = 0; i < pPresentInfo->waitSemaphoreCount; i++) + { + host_semaphores[i] = vulkan_semaphore_from_handle(pPresentInfo->pWaitSemaphores[i])->host.semaphore; + stages[i] = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; + } + + submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit.pNext = NULL; + submit.waitSemaphoreCount = pPresentInfo->waitSemaphoreCount; + submit.pWaitSemaphores = host_semaphores; + submit.pWaitDstStageMask = stages; + submit.commandBufferCount = 0; + submit.pCommandBuffers = NULL; + submit.signalSemaphoreCount = 0; + submit.pSignalSemaphores = NULL; + + submit_fence = VK_NULL_HANDLE; + if (fence0 != VK_NULL_HANDLE) + { + submit_fence = fence0; + } + + submit_res = vk_device->p_vkQueueSubmit(vk_queue->host.queue, 1, &submit, submit_fence); + if (submit_res != VK_SUCCESS) + { + ERR("vkQueueSubmit failed: %d\n", submit_res); + ret = submit_res; + } + } + else + { + ERR("Failed to allocate wait-semaphore arrays; present wait skipped\n"); + /* Still signal the present fence with an empty submit so an app + * waiting on it doesn't hang on this OOM path. */ + if (fence0 != VK_NULL_HANDLE) + { + VkSubmitInfo fence_submit = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO }; + vk_device->p_vkQueueSubmit(vk_queue->host.queue, 1, &fence_submit, + fence0); + } + ret = VK_ERROR_OUT_OF_HOST_MEMORY; + } + if (host_semaphores != stack_semaphores) free(host_semaphores); + if (stages != stack_stages) free(stages); + + /* Multi-swapchain present (swapchainCount > 1): the shared present + * wait-semaphores were consumed once, by pFences[0]'s batch above (a + * binary semaphore can be waited on exactly once). The remaining fences + * are signaled by ordering-independent empty submits, so each may signal + * marginally before that wait completes. A strictly-ordered fan-out would + * need a timeline semaphore, but win32u does not enable the + * timelineSemaphore device feature, so it is unavailable here. This best + * effort is benign in practice: image reuse is independently gated by the + * per-image back-pressure fence, and the target workloads present a single + * swapchain (so this loop never runs). */ + wsi_signal_present_fences(vk_device, vk_queue, present_fence_info, 1); + } + else + wsi_signal_present_fences(vk_device, vk_queue, present_fence_info, 0); + + for (i = 0; i < pPresentInfo->swapchainCount; i++) + { + struct wine_wsi_swapchain *swapchain = (struct wine_wsi_swapchain *)vulkan_swapchain_from_handle(pPresentInfo->pSwapchains[i]); + uint32_t image_idx = pPresentInfo->pImageIndices[i]; + struct wine_wsi_image *image; + + if (!swapchain || image_idx >= swapchain->image_count) + { + ERR("Invalid swapchain %p or image index %u in present\n", swapchain, image_idx); + if (pPresentInfo->pResults) pPresentInfo->pResults[i] = VK_ERROR_UNKNOWN; + ret = VK_ERROR_UNKNOWN; + continue; + } + image = &swapchain->images[image_idx]; + + /* Arm this image's back-pressure fence: it signals once the queue drains + * past this point (i.e. after the frame just submitted/waited on above). + * The matching acquire waits on it before reusing the image. Skip if a + * previous submission is still pending (acquire timed out), to avoid + * submitting a fence that is already in use. */ + if (image->present_fence != VK_NULL_HANDLE && !image->present_pending) + { + VkSubmitInfo fence_submit = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO }; + if (vk_device->p_vkQueueSubmit(vk_queue->host.queue, 1, &fence_submit, image->present_fence) == VK_SUCCESS) + image->present_pending = TRUE; + } + + if (vk_funcs->p_vkFlushRemoteSurfaceDMABUF) + { + vk_funcs->p_vkFlushRemoteSurfaceDMABUF(swapchain->surface->hwnd, + image->win32_handle, + image->drm_modifier, + swapchain->width, + swapchain->height, + image->stride); + } + else + { + ERR("p_vkFlushRemoteSurfaceDMABUF is NULL!\n"); + } + + if (pPresentInfo->pResults) pPresentInfo->pResults[i] = VK_SUCCESS; + } + + return ret; +} + +VkResult wine_vkGetDeviceGroupSurfacePresentModesKHR(VkDevice device, VkSurfaceKHR surface_handle, + VkDeviceGroupPresentModeFlagsKHR *pModes) +{ + TRACE("device %p, surface %s, pModes %p\n", device, wine_dbgstr_longlong(surface_handle), pModes); + *pModes = VK_DEVICE_GROUP_PRESENT_MODE_LOCAL_BIT_KHR; + return VK_SUCCESS; +} + +VkResult wine_vkGetPhysicalDevicePresentRectanglesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface_handle, + uint32_t *pRectCount, VkRect2D *pRects) +{ + struct wine_wsi_surface *surface = (struct wine_wsi_surface *)vulkan_surface_from_handle(surface_handle); + RECT rect; + + TRACE("physicalDevice %p, surface %s, pRectCount %p, pRects %p\n", + physicalDevice, wine_dbgstr_longlong(surface_handle), pRectCount, pRects); + + if (!pRects) + { + *pRectCount = 1; + return VK_SUCCESS; + } + + if (*pRectCount < 1) + { + *pRectCount = 1; + return VK_INCOMPLETE; + } + + if (surface && surface->hwnd && NtUserGetClientRect(surface->hwnd, &rect, 96)) + { + pRects[0].offset.x = 0; + pRects[0].offset.y = 0; + pRects[0].extent.width = rect.right - rect.left; + pRects[0].extent.height = rect.bottom - rect.top; + } + else + { + pRects[0].offset.x = 0; + pRects[0].offset.y = 0; + pRects[0].extent.width = 1024; + pRects[0].extent.height = 768; + } + + *pRectCount = 1; + return VK_SUCCESS; +} + +VkResult wine_vkGetSwapchainTimingPropertiesEXT(VkDevice device, VkSwapchainKHR swapchain, + VkSwapchainTimingPropertiesEXT *pProperties, uint64_t *pPropertiesCount) +{ + TRACE("device %p, swapchain %s, pPropertiesCount %p, pProperties %p\n", device, wine_dbgstr_longlong(swapchain), pPropertiesCount, pProperties); + if (pPropertiesCount) *pPropertiesCount = 0; + return VK_SUCCESS; +} + +VkResult wine_vkSetSwapchainPresentTimingQueueSizeEXT(VkDevice device, VkSwapchainKHR swapchain, uint32_t size) +{ + TRACE("device %p, swapchain %s, size %u\n", device, wine_dbgstr_longlong(swapchain), size); + return VK_SUCCESS; +} + +VkResult wine_vkWaitForPresentKHR(VkDevice device, VkSwapchainKHR swapchain, uint64_t presentId, uint64_t timeout) +{ + TRACE("device %p, swapchain %s, presentId %s, timeout %s\n", + device, wine_dbgstr_longlong(swapchain), wine_dbgstr_longlong(presentId), wine_dbgstr_longlong(timeout)); + return VK_SUCCESS; +} + +VkResult wine_vkWaitForPresent2KHR(VkDevice device, VkSwapchainKHR swapchain, const VkPresentWait2InfoKHR *pPresentWait2Info) +{ + TRACE("device %p, swapchain %s, pPresentWait2Info %p\n", device, wine_dbgstr_longlong(swapchain), pPresentWait2Info); + return VK_SUCCESS; +} + +VkResult wine_vkLatencySleepNV(VkDevice device, VkSwapchainKHR swapchain, const VkLatencySleepInfoNV *pSleepInfo) +{ + TRACE("device %p, swapchain %s, pSleepInfo %p\n", device, wine_dbgstr_longlong(swapchain), pSleepInfo); + return VK_SUCCESS; +} + +VkResult wine_vkSetLatencySleepModeNV(VkDevice device, VkSwapchainKHR swapchain, const VkLatencySleepModeInfoNV *pSleepModeInfo) +{ + TRACE("device %p, swapchain %s, pSleepModeInfo %p\n", device, wine_dbgstr_longlong(swapchain), pSleepModeInfo); + return VK_SUCCESS; +} + +void wine_vkSetLatencyMarkerNV(VkDevice device, VkSwapchainKHR swapchain, const VkSetLatencyMarkerInfoNV *pLatencyMarkerInfo) +{ + TRACE("device %p, swapchain %s, pLatencyMarkerInfo %p\n", device, wine_dbgstr_longlong(swapchain), pLatencyMarkerInfo); +} + +void wine_vkGetLatencyTimingsNV(VkDevice device, VkSwapchainKHR swapchain, VkGetLatencyMarkerInfoNV *pLatencyMarkerInfo) +{ + TRACE("device %p, swapchain %s, pLatencyMarkerInfo %p\n", device, wine_dbgstr_longlong(swapchain), pLatencyMarkerInfo); +} diff --git a/dlls/winevulkan/wsi_private.h b/dlls/winevulkan/wsi_private.h new file mode 100644 index 00000000000..d6e8871f54d --- /dev/null +++ b/dlls/winevulkan/wsi_private.h @@ -0,0 +1,22 @@ +#ifndef __WINE_VULKAN_WSI_PRIVATE_H +#define __WINE_VULKAN_WSI_PRIVATE_H + +#include "wine/vulkan.h" + +struct wine_wsi_image +{ + VkImage image; + VkDeviceMemory memory; + int dmabuf_fd; + uint64_t drm_modifier; + HANDLE win32_handle; + uint32_t stride; + /* Back-pressure fence: signaled when the queue has finished the work up to + * this image's last present. vkAcquireNextImageKHR waits on it before + * handing the image back, so the app can't start rendering into an image + * whose previous frame the GPU is still processing. */ + VkFence present_fence; + BOOL present_pending; /* present_fence submitted and not yet waited on */ +}; + +#endif /* __WINE_VULKAN_WSI_PRIVATE_H */ diff --git a/dlls/winewayland.drv/Makefile.in b/dlls/winewayland.drv/Makefile.in index 00dd0055971..a2ab287f8d0 100644 --- a/dlls/winewayland.drv/Makefile.in +++ b/dlls/winewayland.drv/Makefile.in @@ -1,6 +1,6 @@ MODULE = winewayland.drv UNIXLIB = winewayland.so -UNIX_CFLAGS = $(EGL_CFLAGS) $(WAYLAND_CLIENT_CFLAGS) $(WAYLAND_EGL_CFLAGS) $(XKBCOMMON_CFLAGS) $(XKBREGISTRY_CFLAGS) +UNIX_CFLAGS = $(DBUS_CFLAGS) $(EGL_CFLAGS) $(WAYLAND_CLIENT_CFLAGS) $(WAYLAND_EGL_CFLAGS) $(XKBCOMMON_CFLAGS) $(XKBREGISTRY_CFLAGS) UNIX_LIBS = -lwin32u $(WAYLAND_CLIENT_LIBS) $(WAYLAND_EGL_LIBS) $(XKBCOMMON_LIBS) $(XKBREGISTRY_LIBS) $(PTHREAD_LIBS) -lm IMPORTS = user32 win32u @@ -12,10 +12,12 @@ SOURCES = \ display.c \ dllmain.c \ fractional-scale-v1.xml \ + linux-dmabuf-unstable-v1.xml \ opengl.c \ pointer-constraints-unstable-v1.xml \ pointer-warp-v1.xml \ relative-pointer-unstable-v1.xml \ + systray.c \ text-input-unstable-v3.xml \ viewporter.xml \ vulkan.c \ diff --git a/dlls/winewayland.drv/display.c b/dlls/winewayland.drv/display.c index 1d46d64df6f..ea441ad80bc 100644 --- a/dlls/winewayland.drv/display.c +++ b/dlls/winewayland.drv/display.c @@ -185,6 +185,44 @@ static void output_info_array_arrange_physical_coords(struct wl_array *output_in output_info_cmp_primary_x_y); } +/* Build the list of outputs with their assigned physical-pixel positions and + * record the position on each output state, so the rest of the driver (e.g. + * per-window scale selection) can map a window in physical coordinates to the + * output it sits on. Requires the output lock. */ +static void output_info_array_build(struct wl_array *output_info_array) +{ + struct wayland_output *output; + struct output_info *output_info; + + wl_list_for_each(output, &process_wayland.output_list, link) + { + if (!output->current.current_mode) continue; + output_info = wl_array_add(output_info_array, sizeof(*output_info)); + if (output_info) output_info->output = &output->current; + else ERR("Failed to allocate space for output_info\n"); + } + + output_info_array_arrange_physical_coords(output_info_array); + + wl_array_for_each(output_info, output_info_array) + { + output_info->output->phys_x = output_info->x; + output_info->output->phys_y = output_info->y; + } +} + +/* Recompute the outputs' physical positions outside a full display-device + * update: every process needs them for per-window scale selection, but only + * the desktop process runs UpdateDisplayDevices. Requires the output lock. */ +void wayland_update_outputs_physical_coords(void) +{ + struct wl_array output_info_array; + + wl_array_init(&output_info_array); + output_info_array_build(&output_info_array); + wl_array_release(&output_info_array); +} + static void wayland_add_device_gpu(const struct gdi_device_manager *device_manager, void *param) { @@ -204,11 +242,57 @@ static void wayland_add_device_source(const struct gdi_device_manager *device_ma device_manager->add_source(output_info->output->name, state_flags, dpi, param); } +/* Wayland exposes no EDID; fill the parsed monitor info so win32u can synthesize + * a minimal valid EDID 1.3 and derive a monitor id / name / preferred mode. + * Without it the monitor is a "Generic Non-PnP Monitor" with no identifier, and + * some games (Unreal Engine titles like Gothic 1 Remake) then fail to identify + * any output and present an empty display/resolution list. The actual EDID byte + * assembly now lives once in win32u (build_edid_from_info). */ +static void wayland_fill_edid_info(struct output_info *output_info, struct edid_monitor_info *info) +{ + struct wayland_output_mode *mode = output_info->output->current_mode; + int phys_w_mm = output_info->output->physical_width; + int phys_h_mm = output_info->output->physical_height; + const char *name = output_info->output->name; + unsigned int hash = 2166136261u; /* FNV-1a */ + const char *src; + int i; + + memset(info, 0, sizeof(*info)); + + /* manufacturer id "WAY" (5-bit packed, big endian) */ + info->manufacturer = ((('W' - 'A' + 1) & 0x1F) << 10) | + ((('A' - 'A' + 1) & 0x1F) << 5) | + (('Y' - 'A' + 1) & 0x1F); + /* Derive a per-output product code and serial from the connector name + * (e.g. "eDP-1", "DP-2") so multiple monitors get distinct EDID ids + * (WAY0001, WAY0002, ...) instead of colliding on a fixed one. */ + if (name) for (i = 0; name[i]; i++) { hash ^= (unsigned char)name[i]; hash *= 16777619u; } + info->product_code = (hash & 0x7FFF) + 1; /* 1..0x8000, never 0 */ + info->serial_number = hash; + + /* fall back to a generic ~24" 16:10 panel when the compositor reports no + * physical size (virtual outputs, some headless setups) */ + if (phys_w_mm <= 0 || phys_h_mm <= 0) { phys_w_mm = 520; phys_h_mm = 320; } + info->width_mm = phys_w_mm; + info->height_mm = phys_h_mm; + + info->preferred_width = mode ? mode->width : 1024; + info->preferred_height = mode ? mode->height : 768; + info->preferred_refresh = mode ? mode->refresh : 60000; /* wl_output refresh is in mHz */ + + /* monitor name = connector name (ASCII), up to 13 chars */ + src = name ? name : "Wine Monitor"; + for (i = 0; i < 13 && src[i]; i++) info->monitor_name[i] = (unsigned char)src[i]; + info->monitor_name[i] = 0; +} + static void wayland_add_device_monitor(const struct gdi_device_manager *device_manager, void *param, struct output_info *output_info, struct output_info *primary) { struct gdi_monitor monitor = {0}; + struct edid_monitor_info edid_info; SetRect(&monitor.rc_monitor, output_info->x, output_info->y, output_info->x + output_info->output->current_mode->width, @@ -218,6 +302,9 @@ static void wayland_add_device_monitor(const struct gdi_device_manager *device_m /* We don't have a direct way to get the work area in Wayland. */ monitor.rc_work = monitor.rc_monitor; + wayland_fill_edid_info(output_info, &edid_info); + monitor.edid_info = &edid_info; + TRACE("name=%s rc_monitor=rc_work=%s\n", output_info->output->name, wine_dbgstr_rect(&monitor.rc_monitor)); @@ -270,7 +357,6 @@ static void wayland_add_device_modes(const struct gdi_device_manager *device_man */ UINT WAYLAND_UpdateDisplayDevices(const struct gdi_device_manager *device_manager, void *param) { - struct wayland_output *output; DWORD state_flags = DISPLAY_DEVICE_ATTACHED_TO_DESKTOP | DISPLAY_DEVICE_PRIMARY_DEVICE; struct output_info *primary = NULL, *output_info; struct wl_array output_info_array; @@ -281,15 +367,7 @@ UINT WAYLAND_UpdateDisplayDevices(const struct gdi_device_manager *device_manage pthread_mutex_lock(&process_wayland.output_mutex); - wl_list_for_each(output, &process_wayland.output_list, link) - { - if (!output->current.current_mode) continue; - output_info = wl_array_add(&output_info_array, sizeof(*output_info)); - if (output_info) output_info->output = &output->current; - else ERR("Failed to allocate space for output_info\n"); - } - - output_info_array_arrange_physical_coords(&output_info_array); + output_info_array_build(&output_info_array); /* Populate GDI devices. */ wayland_add_device_gpu(device_manager, param); diff --git a/dlls/winewayland.drv/dllmain.c b/dlls/winewayland.drv/dllmain.c index 8055d883ee0..a8163cd833b 100644 --- a/dlls/winewayland.drv/dllmain.c +++ b/dlls/winewayland.drv/dllmain.c @@ -38,6 +38,15 @@ static DWORD WINAPI wayland_read_events_thread(void *arg) return 0; } +static DWORD WINAPI wayland_tray_dispatch_thread(void *arg) +{ + /* Parks in the unix lib until tray icons need a D-Bus dispatch loop; + * runs as a proper win32 thread so the unix side can deliver + * notification messages to icon owner windows. */ + WAYLANDDRV_UNIX_CALL(tray_dispatch, NULL); + return 0; +} + static LRESULT CALLBACK clipboard_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) @@ -93,6 +102,17 @@ static DWORD WINAPI clipboard_thread(void *arg) return 0; } +/* Only explorer.exe drives the system tray (it owns Shell_TrayWnd), so the + * D-Bus dispatch thread is pointless overhead in every other GUI process. */ +static BOOL is_tray_host_process(void) +{ + WCHAR path[MAX_PATH], *name; + if (!GetModuleFileNameW(NULL, path, ARRAY_SIZE(path))) return FALSE; + for (name = path + lstrlenW(path); name > path; name--) + if (name[-1] == '\\' || name[-1] == '/') break; + return !lstrcmpiW(name, L"explorer.exe"); +} + BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, void *reserved) { DWORD tid; @@ -107,6 +127,9 @@ BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, void *reserved) /* Read wayland events from a dedicated thread. */ CloseHandle(CreateThread(NULL, 0, wayland_read_events_thread, NULL, 0, &tid)); + /* Dispatch D-Bus tray icon events from a dedicated thread (explorer only). */ + if (is_tray_host_process()) + CloseHandle(CreateThread(NULL, 0, wayland_tray_dispatch_thread, NULL, 0, &tid)); /* Handle clipboard events in a dedicated thread, if needed. */ if (!WAYLANDDRV_UNIX_CALL(init_clipboard, NULL)) CloseHandle(CreateThread(NULL, 0, clipboard_thread, NULL, 0, &tid)); diff --git a/dlls/winewayland.drv/linux-dmabuf-unstable-v1.xml b/dlls/winewayland.drv/linux-dmabuf-unstable-v1.xml new file mode 100644 index 00000000000..6f11e925b68 --- /dev/null +++ b/dlls/winewayland.drv/linux-dmabuf-unstable-v1.xml @@ -0,0 +1,589 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="linux_dmabuf_unstable_v1"> + + <copyright> + Copyright © 2014, 2015 Collabora, Ltd. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + </copyright> + + <interface name="zwp_linux_dmabuf_v1" version="5"> + <description summary="factory for creating dmabuf-based wl_buffers"> + Following the interfaces from: + https://www.khronos.org/registry/egl/extensions/EXT/EGL_EXT_image_dma_buf_im... + https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_image_dma_buf_im... + and the Linux DRM sub-system's AddFb2 ioctl. + + This interface offers ways to create generic dmabuf-based wl_buffers. + + Clients can use the get_surface_feedback request to get dmabuf feedback + for a particular surface. If the client wants to retrieve feedback not + tied to a surface, they can use the get_default_feedback request. + + The following are required from clients: + + - Clients must ensure that either all data in the dma-buf is + coherent for all subsequent read access or that coherency is + correctly handled by the underlying kernel-side dma-buf + implementation. + + - Don't make any more attachments after sending the buffer to the + compositor. Making more attachments later increases the risk of + the compositor not being able to use (re-import) an existing + dmabuf-based wl_buffer. + + The underlying graphics stack must ensure the following: + + - The dmabuf file descriptors relayed to the server will stay valid + for the whole lifetime of the wl_buffer. This means the server may + at any time use those fds to import the dmabuf into any kernel + sub-system that might accept it. + + However, when the underlying graphics stack fails to deliver the + promise, because of e.g. a device hot-unplug which raises internal + errors, after the wl_buffer has been successfully created the + compositor must not raise protocol errors to the client when dmabuf + import later fails. + + To create a wl_buffer from one or more dmabufs, a client creates a + zwp_linux_dmabuf_params_v1 object with a zwp_linux_dmabuf_v1.create_params + request. All planes required by the intended format are added with + the 'add' request. Finally, a 'create' or 'create_immed' request is + issued, which has the following outcome depending on the import success. + + The 'create' request, + - on success, triggers a 'created' event which provides the final + wl_buffer to the client. + - on failure, triggers a 'failed' event to convey that the server + cannot use the dmabufs received from the client. + + For the 'create_immed' request, + - on success, the server immediately imports the added dmabufs to + create a wl_buffer. No event is sent from the server in this case. + - on failure, the server can choose to either: + - terminate the client by raising a fatal error. + - mark the wl_buffer as failed, and send a 'failed' event to the + client. If the client uses a failed wl_buffer as an argument to any + request, the behaviour is compositor implementation-defined. + + For all DRM formats and unless specified in another protocol extension, + pre-multiplied alpha is used for pixel values. + + Unless specified otherwise in another protocol extension, implicit + synchronization is used. In other words, compositors and clients must + wait and signal fences implicitly passed via the DMA-BUF's reservation + mechanism. + + Disclaimer: This protocol extension has been marked stable. This copy is + no longer used and only retained for backwards compatibility. The + canonical version can be found in the stable/ directory. + </description> + + <request name="destroy" type="destructor"> + <description summary="unbind the factory"> + Objects created through this interface, especially wl_buffers, will + remain valid. + </description> + </request> + + <request name="create_params"> + <description summary="create a temporary object for buffer parameters"> + This temporary object is used to collect multiple dmabuf handles into + a single batch to create a wl_buffer. It can only be used once and + should be destroyed after a 'created' or 'failed' event has been + received. + </description> + <arg name="params_id" type="new_id" interface="zwp_linux_buffer_params_v1" + summary="the new temporary"/> + </request> + + <event name="format"> + <description summary="supported buffer format"> + This event advertises one buffer format that the server supports. + All the supported formats are advertised once when the client + binds to this interface. A roundtrip after binding guarantees + that the client has received all supported formats. + + For the definition of the format codes, see the + zwp_linux_buffer_params_v1::create request. + + Starting version 4, the format event is deprecated and must not be + sent by compositors. Instead, use get_default_feedback or + get_surface_feedback. + </description> + <arg name="format" type="uint" summary="DRM_FORMAT code"/> + </event> + + <event name="modifier" since="3"> + <description summary="supported buffer format modifier"> + This event advertises the formats that the server supports, along with + the modifiers supported for each format. All the supported modifiers + for all the supported formats are advertised once when the client + binds to this interface. A roundtrip after binding guarantees that + the client has received all supported format-modifier pairs. + + For legacy support, DRM_FORMAT_MOD_INVALID (that is, modifier_hi == + 0x00ffffff and modifier_lo == 0xffffffff) is allowed in this event. + It indicates that the server can support the format with an implicit + modifier. When a plane has DRM_FORMAT_MOD_INVALID as its modifier, it + is as if no explicit modifier is specified. The effective modifier + will be derived from the dmabuf. + + A compositor that sends valid modifiers and DRM_FORMAT_MOD_INVALID for + a given format supports both explicit modifiers and implicit modifiers. + + For the definition of the format and modifier codes, see the + zwp_linux_buffer_params_v1::create and zwp_linux_buffer_params_v1::add + requests. + + Starting version 4, the modifier event is deprecated and must not be + sent by compositors. Instead, use get_default_feedback or + get_surface_feedback. + </description> + <arg name="format" type="uint" summary="DRM_FORMAT code"/> + <arg name="modifier_hi" type="uint" + summary="high 32 bits of layout modifier"/> + <arg name="modifier_lo" type="uint" + summary="low 32 bits of layout modifier"/> + </event> + + <!-- Version 4 additions --> + + <request name="get_default_feedback" since="4"> + <description summary="get default feedback"> + This request creates a new wp_linux_dmabuf_feedback object not bound + to a particular surface. This object will deliver feedback about dmabuf + parameters to use if the client doesn't support per-surface feedback + (see get_surface_feedback). + </description> + <arg name="id" type="new_id" interface="zwp_linux_dmabuf_feedback_v1"/> + </request> + + <request name="get_surface_feedback" since="4"> + <description summary="get feedback for a surface"> + This request creates a new wp_linux_dmabuf_feedback object for the + specified wl_surface. This object will deliver feedback about dmabuf + parameters to use for buffers attached to this surface. + + If the surface is destroyed before the wp_linux_dmabuf_feedback object, + the feedback object becomes inert. + </description> + <arg name="id" type="new_id" interface="zwp_linux_dmabuf_feedback_v1"/> + <arg name="surface" type="object" interface="wl_surface"/> + </request> + </interface> + + <interface name="zwp_linux_buffer_params_v1" version="5"> + <description summary="parameters for creating a dmabuf-based wl_buffer"> + This temporary object is a collection of dmabufs and other + parameters that together form a single logical buffer. The temporary + object may eventually create one wl_buffer unless cancelled by + destroying it before requesting 'create'. + + Single-planar formats only require one dmabuf, however + multi-planar formats may require more than one dmabuf. For all + formats, an 'add' request must be called once per plane (even if the + underlying dmabuf fd is identical). + + You must use consecutive plane indices ('plane_idx' argument for 'add') + from zero to the number of planes used by the drm_fourcc format code. + All planes required by the format must be given exactly once, but can + be given in any order. Each plane index can be set only once. + </description> + + <enum name="error"> + <entry name="already_used" value="0" + summary="the dmabuf_batch object has already been used to create a wl_buffer"/> + <entry name="plane_idx" value="1" + summary="plane index out of bounds"/> + <entry name="plane_set" value="2" + summary="the plane index was already set"/> + <entry name="incomplete" value="3" + summary="missing or too many planes to create a buffer"/> + <entry name="invalid_format" value="4" + summary="format not supported"/> + <entry name="invalid_dimensions" value="5" + summary="invalid width or height"/> + <entry name="out_of_bounds" value="6" + summary="offset + stride * height goes out of dmabuf bounds"/> + <entry name="invalid_wl_buffer" value="7" + summary="invalid wl_buffer resulted from importing dmabufs via + the create_immed request on given buffer_params"/> + </enum> + + <request name="destroy" type="destructor"> + <description summary="delete this object, used or not"> + Cleans up the temporary data sent to the server for dmabuf-based + wl_buffer creation. + </description> + </request> + + <request name="add"> + <description summary="add a dmabuf to the temporary set"> + This request adds one dmabuf to the set in this + zwp_linux_buffer_params_v1. + + The 64-bit unsigned value combined from modifier_hi and modifier_lo + is the dmabuf layout modifier. DRM AddFB2 ioctl calls this the + fb modifier, which is defined in drm_mode.h of Linux UAPI. + This is an opaque token. Drivers use this token to express tiling, + compression, etc. driver-specific modifications to the base format + defined by the DRM fourcc code. + + Starting from version 4, the invalid_format protocol error is sent if + the format + modifier pair was not advertised as supported. + + Starting from version 5, the invalid_format protocol error is sent if + all planes don't use the same modifier. + + This request raises the PLANE_IDX error if plane_idx is too large. + The error PLANE_SET is raised if attempting to set a plane that + was already set. + </description> + <arg name="fd" type="fd" summary="dmabuf fd"/> + <arg name="plane_idx" type="uint" summary="plane index"/> + <arg name="offset" type="uint" summary="offset in bytes"/> + <arg name="stride" type="uint" summary="stride in bytes"/> + <arg name="modifier_hi" type="uint" + summary="high 32 bits of layout modifier"/> + <arg name="modifier_lo" type="uint" + summary="low 32 bits of layout modifier"/> + </request> + + <enum name="flags" bitfield="true"> + <entry name="y_invert" value="1" summary="contents are y-inverted"/> + <entry name="interlaced" value="2" summary="content is interlaced"/> + <entry name="bottom_first" value="4" summary="bottom field first"/> + </enum> + + <request name="create"> + <description summary="create a wl_buffer from the given dmabufs"> + This asks for creation of a wl_buffer from the added dmabuf + buffers. The wl_buffer is not created immediately but returned via + the 'created' event if the dmabuf sharing succeeds. The sharing + may fail at runtime for reasons a client cannot predict, in + which case the 'failed' event is triggered. + + The 'format' argument is a DRM_FORMAT code, as defined by the + libdrm's drm_fourcc.h. The Linux kernel's DRM sub-system is the + authoritative source on how the format codes should work. + + The 'flags' is a bitfield of the flags defined in enum "flags". + 'y_invert' means the that the image needs to be y-flipped. + + Flag 'interlaced' means that the frame in the buffer is not + progressive as usual, but interlaced. An interlaced buffer as + supported here must always contain both top and bottom fields. + The top field always begins on the first pixel row. The temporal + ordering between the two fields is top field first, unless + 'bottom_first' is specified. It is undefined whether 'bottom_first' + is ignored if 'interlaced' is not set. + + This protocol does not convey any information about field rate, + duration, or timing, other than the relative ordering between the + two fields in one buffer. A compositor may have to estimate the + intended field rate from the incoming buffer rate. It is undefined + whether the time of receiving wl_surface.commit with a new buffer + attached, applying the wl_surface state, wl_surface.frame callback + trigger, presentation, or any other point in the compositor cycle + is used to measure the frame or field times. There is no support + for detecting missed or late frames/fields/buffers either, and + there is no support whatsoever for cooperating with interlaced + compositor output. + + The composited image quality resulting from the use of interlaced + buffers is explicitly undefined. A compositor may use elaborate + hardware features or software to deinterlace and create progressive + output frames from a sequence of interlaced input buffers, or it + may produce substandard image quality. However, compositors that + cannot guarantee reasonable image quality in all cases are recommended + to just reject all interlaced buffers. + + Any argument errors, including non-positive width or height, + mismatch between the number of planes and the format, bad + format, bad offset or stride, may be indicated by fatal protocol + errors: INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS, + OUT_OF_BOUNDS. + + Dmabuf import errors in the server that are not obvious client + bugs are returned via the 'failed' event as non-fatal. This + allows attempting dmabuf sharing and falling back in the client + if it fails. + + This request can be sent only once in the object's lifetime, after + which the only legal request is destroy. This object should be + destroyed after issuing a 'create' request. Attempting to use this + object after issuing 'create' raises ALREADY_USED protocol error. + + It is not mandatory to issue 'create'. If a client wants to + cancel the buffer creation, it can just destroy this object. + </description> + <arg name="width" type="int" summary="base plane width in pixels"/> + <arg name="height" type="int" summary="base plane height in pixels"/> + <arg name="format" type="uint" summary="DRM_FORMAT code"/> + <arg name="flags" type="uint" enum="flags" summary="see enum flags"/> + </request> + + <event name="created"> + <description summary="buffer creation succeeded"> + This event indicates that the attempted buffer creation was + successful. It provides the new wl_buffer referencing the dmabuf(s). + + Upon receiving this event, the client should destroy the + zwp_linux_buffer_params_v1 object. + </description> + <arg name="buffer" type="new_id" interface="wl_buffer" + summary="the newly created wl_buffer"/> + </event> + + <event name="failed"> + <description summary="buffer creation failed"> + This event indicates that the attempted buffer creation has + failed. It usually means that one of the dmabuf constraints + has not been fulfilled. + + Upon receiving this event, the client should destroy the + zwp_linux_buffer_params_v1 object. + </description> + </event> + + <request name="create_immed" since="2"> + <description summary="immediately create a wl_buffer from the given + dmabufs"> + This asks for immediate creation of a wl_buffer by importing the + added dmabufs. + + In case of import success, no event is sent from the server, and the + wl_buffer is ready to be used by the client. + + Upon import failure, either of the following may happen, as seen fit + by the implementation: + - the client is terminated with one of the following fatal protocol + errors: + - INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS, OUT_OF_BOUNDS, + in case of argument errors such as mismatch between the number + of planes and the format, bad format, non-positive width or + height, or bad offset or stride. + - INVALID_WL_BUFFER, in case the cause for failure is unknown or + platform specific. + - the server creates an invalid wl_buffer, marks it as failed and + sends a 'failed' event to the client. The result of using this + invalid wl_buffer as an argument in any request by the client is + defined by the compositor implementation. + + This takes the same arguments as a 'create' request, and obeys the + same restrictions. + </description> + <arg name="buffer_id" type="new_id" interface="wl_buffer" + summary="id for the newly created wl_buffer"/> + <arg name="width" type="int" summary="base plane width in pixels"/> + <arg name="height" type="int" summary="base plane height in pixels"/> + <arg name="format" type="uint" summary="DRM_FORMAT code"/> + <arg name="flags" type="uint" enum="flags" summary="see enum flags"/> + </request> + </interface> + + <interface name="zwp_linux_dmabuf_feedback_v1" version="5"> + <description summary="dmabuf feedback"> + This object advertises dmabuf parameters feedback. This includes the + preferred devices and the supported formats/modifiers. + + The parameters are sent once when this object is created and whenever they + change. The done event is always sent once after all parameters have been + sent. When a single parameter changes, all parameters are re-sent by the + compositor. + + Compositors can re-send the parameters when the current client buffer + allocations are sub-optimal. Compositors should not re-send the + parameters if re-allocating the buffers would not result in a more optimal + configuration. In particular, compositors should avoid sending the exact + same parameters multiple times in a row. + + The tranche_target_device and tranche_formats events are grouped by + tranches of preference. For each tranche, a tranche_target_device, one + tranche_flags and one or more tranche_formats events are sent, followed + by a tranche_done event finishing the list. The tranches are sent in + descending order of preference. All formats and modifiers in the same + tranche have the same preference. + + To send parameters, the compositor sends one main_device event, tranches + (each consisting of one tranche_target_device event, one tranche_flags + event, tranche_formats events and then a tranche_done event), then one + done event. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the feedback object"> + Using this request a client can tell the server that it is not going to + use the wp_linux_dmabuf_feedback object anymore. + </description> + </request> + + <event name="done"> + <description summary="all feedback has been sent"> + This event is sent after all parameters of a wp_linux_dmabuf_feedback + object have been sent. + + This allows changes to the wp_linux_dmabuf_feedback parameters to be + seen as atomic, even if they happen via multiple events. + </description> + </event> + + <event name="format_table"> + <description summary="format and modifier table"> + This event provides a file descriptor which can be memory-mapped to + access the format and modifier table. + + The table contains a tightly packed array of consecutive format + + modifier pairs. Each pair is 16 bytes wide. It contains a format as a + 32-bit unsigned integer, followed by 4 bytes of unused padding, and a + modifier as a 64-bit unsigned integer. The native endianness is used. + + The client must map the file descriptor in read-only private mode. + + Compositors are not allowed to mutate the table file contents once this + event has been sent. Instead, compositors must create a new, separate + table file and re-send feedback parameters. Compositors are allowed to + store duplicate format + modifier pairs in the table. + </description> + <arg name="fd" type="fd" summary="table file descriptor"/> + <arg name="size" type="uint" summary="table size, in bytes"/> + </event> + + <event name="main_device"> + <description summary="preferred main device"> + This event advertises the main device that the server prefers to use + when direct scan-out to the target device isn't possible. The + advertised main device may be different for each + wp_linux_dmabuf_feedback object, and may change over time. + + There is exactly one main device. The compositor must send at least + one preference tranche with tranche_target_device equal to main_device. + + Clients need to create buffers that the main device can import and + read from, otherwise creating the dmabuf wl_buffer will fail (see the + wp_linux_buffer_params.create and create_immed requests for details). + The main device will also likely be kept active by the compositor, + so clients can use it instead of waking up another device for power + savings. + + In general the device is a DRM node. The DRM node type (primary vs. + render) is unspecified. Clients must not rely on the compositor sending + a particular node type. Clients cannot check two devices for equality + by comparing the dev_t value. + + If explicit modifiers are not supported and the client performs buffer + allocations on a different device than the main device, then the client + must force the buffer to have a linear layout. + </description> + <arg name="device" type="array" summary="device dev_t value"/> + </event> + + <event name="tranche_done"> + <description summary="a preference tranche has been sent"> + This event splits tranche_target_device and tranche_formats events in + preference tranches. It is sent after a set of tranche_target_device + and tranche_formats events; it represents the end of a tranche. The + next tranche will have a lower preference. + </description> + </event> + + <event name="tranche_target_device"> + <description summary="target device"> + This event advertises the target device that the server prefers to use + for a buffer created given this tranche. The advertised target device + may be different for each preference tranche, and may change over time. + + There is exactly one target device per tranche. + + The target device may be a scan-out device, for example if the + compositor prefers to directly scan-out a buffer created given this + tranche. The target device may be a rendering device, for example if + the compositor prefers to texture from said buffer. + + The client can use this hint to allocate the buffer in a way that makes + it accessible from the target device, ideally directly. The buffer must + still be accessible from the main device, either through direct import + or through a potentially more expensive fallback path. If the buffer + can't be directly imported from the main device then clients must be + prepared for the compositor changing the tranche priority or making + wl_buffer creation fail (see the wp_linux_buffer_params.create and + create_immed requests for details). + + If the device is a DRM node, the DRM node type (primary vs. render) is + unspecified. Clients must not rely on the compositor sending a + particular node type. Clients cannot check two devices for equality by + comparing the dev_t value. + + This event is tied to a preference tranche, see the tranche_done event. + </description> + <arg name="device" type="array" summary="device dev_t value"/> + </event> + + <event name="tranche_formats"> + <description summary="supported buffer format modifier"> + This event advertises the format + modifier combinations that the + compositor supports. + + It carries an array of indices, each referring to a format + modifier + pair in the last received format table (see the format_table event). + Each index is a 16-bit unsigned integer in native endianness. + + For legacy support, DRM_FORMAT_MOD_INVALID is an allowed modifier. + It indicates that the server can support the format with an implicit + modifier. When a buffer has DRM_FORMAT_MOD_INVALID as its modifier, it + is as if no explicit modifier is specified. The effective modifier + will be derived from the dmabuf. + + A compositor that sends valid modifiers and DRM_FORMAT_MOD_INVALID for + a given format supports both explicit modifiers and implicit modifiers. + + Compositors must not send duplicate format + modifier pairs within the + same tranche or across two different tranches with the same target + device and flags. + + This event is tied to a preference tranche, see the tranche_done event. + + For the definition of the format and modifier codes, see the + wp_linux_buffer_params.create request. + </description> + <arg name="indices" type="array" summary="array of 16-bit indexes"/> + </event> + + <enum name="tranche_flags" bitfield="true"> + <entry name="scanout" value="1" summary="direct scan-out tranche"/> + </enum> + + <event name="tranche_flags"> + <description summary="tranche flags"> + This event sets tranche-specific flags. + + The scanout flag is a hint that direct scan-out may be attempted by the + compositor on the target device if the client appropriately allocates a + buffer. How to allocate a buffer that can be scanned out on the target + device is implementation-defined. + + This event is tied to a preference tranche, see the tranche_done event. + </description> + <arg name="flags" type="uint" enum="tranche_flags" summary="tranche flags"/> + </event> + </interface> + +</protocol> diff --git a/dlls/winewayland.drv/systray.c b/dlls/winewayland.drv/systray.c new file mode 100644 index 00000000000..1c754acdca6 --- /dev/null +++ b/dlls/winewayland.drv/systray.c @@ -0,0 +1,1037 @@ +/* + * Wayland system tray support via the StatusNotifierItem D-Bus protocol + * + * Copyright 2026 gohryt + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#if 0 +#pragma makedep unix +#endif + +#include "config.h" + +#include "ntstatus.h" +#define WIN32_NO_STATUS +#include <dlfcn.h> +#include <fcntl.h> +#include <poll.h> +#include <pthread.h> +#include <unistd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "waylanddrv.h" +#include "wine/debug.h" +#include "wine/list.h" +#include "shellapi.h" + +WINE_DEFAULT_DEBUG_CHANNEL(systray); + +#ifdef SONAME_LIBDBUS_1 + +#include <dbus/dbus.h> + +#define DBUS_FUNCS \ + DO_FUNC(dbus_bus_add_match); \ + DO_FUNC(dbus_bus_get_private); \ + DO_FUNC(dbus_bus_get_unique_name); \ + DO_FUNC(dbus_bus_name_has_owner); \ + DO_FUNC(dbus_connection_close); \ + DO_FUNC(dbus_connection_flush); \ + DO_FUNC(dbus_connection_get_dispatch_status); \ + DO_FUNC(dbus_connection_get_unix_fd); \ + DO_FUNC(dbus_connection_ref); \ + DO_FUNC(dbus_connection_remove_filter); \ + DO_FUNC(dbus_connection_unref); \ + DO_FUNC(dbus_connection_add_filter); \ + DO_FUNC(dbus_connection_read_write_dispatch); \ + DO_FUNC(dbus_connection_send); \ + DO_FUNC(dbus_connection_set_exit_on_disconnect); \ + DO_FUNC(dbus_connection_try_register_object_path); \ + DO_FUNC(dbus_connection_unregister_object_path); \ + DO_FUNC(dbus_error_free); \ + DO_FUNC(dbus_error_init); \ + DO_FUNC(dbus_message_get_member); \ + DO_FUNC(dbus_message_is_method_call); \ + DO_FUNC(dbus_message_is_signal); \ + DO_FUNC(dbus_message_iter_append_basic); \ + DO_FUNC(dbus_message_iter_append_fixed_array); \ + DO_FUNC(dbus_message_iter_close_container); \ + DO_FUNC(dbus_message_iter_get_arg_type); \ + DO_FUNC(dbus_message_iter_get_basic); \ + DO_FUNC(dbus_message_iter_init); \ + DO_FUNC(dbus_message_iter_init_append); \ + DO_FUNC(dbus_message_iter_next); \ + DO_FUNC(dbus_message_iter_open_container); \ + DO_FUNC(dbus_message_new_method_call); \ + DO_FUNC(dbus_message_new_method_return); \ + DO_FUNC(dbus_message_new_signal); \ + DO_FUNC(dbus_message_unref); \ + DO_FUNC(dbus_threads_init_default) + +#define DO_FUNC(f) static typeof(f) * p_##f +DBUS_FUNCS; +#undef DO_FUNC + +#define SNI_INTERFACE "org.kde.StatusNotifierItem" +#define WATCHER_SERVICE "org.kde.StatusNotifierWatcher" +#define WATCHER_PATH "/StatusNotifierWatcher" +#define PROPERTIES_INTERFACE "org.freedesktop.DBus.Properties" +#define INTROSPECTABLE_INTERFACE "org.freedesktop.DBus.Introspectable" + +struct tray_icon +{ + struct list entry; + HWND owner; /* the window that created the icon */ + UINT id; /* the icon id, unique with owner */ + UINT callback_message; + UINT version; /* NOTIFYICON_VERSION */ + DWORD state; /* NIS_* */ + char tooltip[128 * 3]; /* UTF-8 tooltip text */ + DBusConnection *connection; /* private connection; one per icon, the + watcher tracks items by bus name */ + int icon_width; + int icon_height; + unsigned char *icon_argb; /* network byte order ARGB32 */ + BOOL registered; /* registered with the watcher */ +}; + +#define SNI_OBJECT_PATH "/StatusNotifierItem" + +static struct list tray_icons = LIST_INIT(tray_icons); +/* icons removed by the explorer thread but whose private D-Bus connection + * must be torn down on the dispatch thread (closing a connection while the + * dispatch thread is inside read_write_dispatch on it is a data race) */ +static struct list tray_dying_icons = LIST_INIT(tray_dying_icons); +static pthread_mutex_t tray_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t tray_start_cond = PTHREAD_COND_INITIALIZER; +/* self-pipe waking the dispatch thread's poll when the icon lists change */ +static int tray_wake_pipe[2] = { -1, -1 }; +static BOOL tray_dbus_loaded; +static BOOL tray_dbus_failed; + +static BOOL icon_is_alive(struct tray_icon *icon); +static void delete_icon(struct tray_icon *icon); +static void destroy_dying_icon(struct tray_icon *icon); + +/* wake the dispatch thread out of cond_wait or poll; tray_mutex held */ +static void tray_wake_dispatch(void) +{ + char byte = 0; + pthread_cond_broadcast(&tray_start_cond); + if (tray_wake_pipe[1] != -1 && write(tray_wake_pipe[1], &byte, 1) < 0) + { + /* a full pipe already wakes the dispatch thread */ + } +} + +static BOOL load_dbus_functions(void) +{ + void *handle; + + if (!(handle = dlopen(SONAME_LIBDBUS_1, RTLD_NOW))) goto failed; + +#define DO_FUNC(f) if (!(p_##f = dlsym(handle, #f))) goto failed + DBUS_FUNCS; +#undef DO_FUNC + return TRUE; + +failed: + WARN("failed to load D-Bus support: %s\n", dlerror()); + return FALSE; +} + +/********************************************************************** + * Icon pixel conversion + */ + +static BOOL set_icon_pixmap(struct tray_icon *icon, HICON handle) +{ + struct wayland_shm_buffer *shm_buffer = NULL; + ICONINFOEXW info = {0}; + unsigned char *dst; + uint32_t *src; + SIZE_T count, i; + HDC hdc; + + free(icon->icon_argb); + icon->icon_argb = NULL; + icon->icon_width = icon->icon_height = 0; + + if (!handle) return TRUE; + if (!wayland_get_icon_info(handle, &info)) return FALSE; + + if (info.hbmColor) + { + hdc = NtGdiCreateCompatibleDC(0); + shm_buffer = wayland_shm_buffer_from_color_bitmaps(hdc, info.hbmColor, info.hbmMask, FALSE); + NtGdiDeleteObjectApp(hdc); + } + else + { + shm_buffer = create_mono_cursor_buffer(info.hbmMask); + } + if (info.hbmColor) NtGdiDeleteObjectApp(info.hbmColor); + if (info.hbmMask) NtGdiDeleteObjectApp(info.hbmMask); + if (!shm_buffer) return FALSE; + + count = (SIZE_T)shm_buffer->width * shm_buffer->height; + if (!(dst = malloc(count * 4))) + { + wayland_shm_buffer_unref(shm_buffer); + return FALSE; + } + + /* the cursor helper premultiplies alpha for Wayland; SNI wants straight + * (non-premultiplied) ARGB32 in network byte order */ + src = shm_buffer->map_data; + for (i = 0; i < count; i++) + { + uint32_t v = src[i]; + unsigned char a = v >> 24; + + dst[i * 4 + 0] = a; + if (a && a != 255) + { + dst[i * 4 + 1] = min(255, ((v >> 16) & 0xff) * 255 / a); + dst[i * 4 + 2] = min(255, ((v >> 8) & 0xff) * 255 / a); + dst[i * 4 + 3] = min(255, (v & 0xff) * 255 / a); + } + else + { + dst[i * 4 + 1] = v >> 16; + dst[i * 4 + 2] = v >> 8; + dst[i * 4 + 3] = v; + } + } + + icon->icon_width = shm_buffer->width; + icon->icon_height = shm_buffer->height; + icon->icon_argb = dst; + wayland_shm_buffer_unref(shm_buffer); + return TRUE; +} + +static void set_icon_tooltip(struct tray_icon *icon, const WCHAR *tip) +{ + DWORD len, utf8_len = 0; + /* szTip is a fixed 128-WCHAR field the app need not NUL-terminate; bound the + * scan rather than trusting wcslen to find a terminator. */ + for (len = 0; len < 128 && tip[len]; len++) {} + RtlUnicodeToUTF8N(icon->tooltip, sizeof(icon->tooltip) - 1, &utf8_len, tip, len * sizeof(WCHAR)); + icon->tooltip[utf8_len] = 0; +} + +/********************************************************************** + * D-Bus marshalling helpers + */ + +/* append a(iiay) — an array of (width, height, pixel bytes) structs */ +static void append_pixmap_array(DBusMessageIter *iter, struct tray_icon *icon) +{ + DBusMessageIter array, rec, bytes; + + p_dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "(iiay)", &array); + if (icon->icon_argb) + { + const unsigned char *data = icon->icon_argb; + int len = icon->icon_width * icon->icon_height * 4; + + p_dbus_message_iter_open_container(&array, DBUS_TYPE_STRUCT, NULL, &rec); + p_dbus_message_iter_append_basic(&rec, DBUS_TYPE_INT32, &icon->icon_width); + p_dbus_message_iter_append_basic(&rec, DBUS_TYPE_INT32, &icon->icon_height); + p_dbus_message_iter_open_container(&rec, DBUS_TYPE_ARRAY, "y", &bytes); + p_dbus_message_iter_append_fixed_array(&bytes, DBUS_TYPE_BYTE, &data, len); + p_dbus_message_iter_close_container(&rec, &bytes); + p_dbus_message_iter_close_container(&array, &rec); + } + p_dbus_message_iter_close_container(iter, &array); +} + +/* append (s a(iiay) s s) — the SNI ToolTip structure */ +static void append_tooltip(DBusMessageIter *iter, struct tray_icon *icon) +{ + DBusMessageIter rec; + const char *empty = ""; + + p_dbus_message_iter_open_container(iter, DBUS_TYPE_STRUCT, NULL, &rec); + p_dbus_message_iter_append_basic(&rec, DBUS_TYPE_STRING, &empty); + { + DBusMessageIter array; + p_dbus_message_iter_open_container(&rec, DBUS_TYPE_ARRAY, "(iiay)", &array); + p_dbus_message_iter_close_container(&rec, &array); + } + { + const char *tip = icon->tooltip; + p_dbus_message_iter_append_basic(&rec, DBUS_TYPE_STRING, &tip); + } + p_dbus_message_iter_append_basic(&rec, DBUS_TYPE_STRING, &empty); + p_dbus_message_iter_close_container(iter, &rec); +} + +static const char *icon_status(struct tray_icon *icon) +{ + return (icon->state & NIS_HIDDEN) ? "Passive" : "Active"; +} + +/* append a single property value as a variant; returns FALSE for unknown names */ +static BOOL append_property(DBusMessageIter *iter, struct tray_icon *icon, const char *name) +{ + DBusMessageIter variant; + char id_buf[64]; + + if (!strcmp(name, "Category") || !strcmp(name, "Title") || + !strcmp(name, "Status") || !strcmp(name, "Id") || + !strcmp(name, "IconName") || !strcmp(name, "OverlayIconName") || + !strcmp(name, "AttentionIconName") || !strcmp(name, "AttentionMovieName") || + !strcmp(name, "IconThemePath")) + { + const char *value = ""; + if (!strcmp(name, "Category")) value = "ApplicationStatus"; + else if (!strcmp(name, "Status")) value = icon_status(icon); + /* Win32 tray icons carry no SNI "Title"; reuse the tooltip text (intentional). */ + else if (!strcmp(name, "Title")) value = icon->tooltip[0] ? icon->tooltip : "Wine application"; + else if (!strcmp(name, "Id")) + { + snprintf(id_buf, sizeof(id_buf), "wine_tray_%p_%u", icon->owner, icon->id); + value = id_buf; + } + p_dbus_message_iter_open_container(iter, DBUS_TYPE_VARIANT, "s", &variant); + p_dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &value); + p_dbus_message_iter_close_container(iter, &variant); + return TRUE; + } + if (!strcmp(name, "IconPixmap") || !strcmp(name, "OverlayIconPixmap") || + !strcmp(name, "AttentionIconPixmap")) + { + struct tray_icon empty = {0}; + p_dbus_message_iter_open_container(iter, DBUS_TYPE_VARIANT, "a(iiay)", &variant); + append_pixmap_array(&variant, strcmp(name, "IconPixmap") ? &empty : icon); + p_dbus_message_iter_close_container(iter, &variant); + return TRUE; + } + if (!strcmp(name, "ToolTip")) + { + p_dbus_message_iter_open_container(iter, DBUS_TYPE_VARIANT, "(sa(iiay)ss)", &variant); + append_tooltip(&variant, icon); + p_dbus_message_iter_close_container(iter, &variant); + return TRUE; + } + if (!strcmp(name, "ItemIsMenu")) + { + dbus_bool_t value = FALSE; + p_dbus_message_iter_open_container(iter, DBUS_TYPE_VARIANT, "b", &variant); + p_dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN, &value); + p_dbus_message_iter_close_container(iter, &variant); + return TRUE; + } + if (!strcmp(name, "Menu")) + { + /* no dbusmenu: hosts fall back to the ContextMenu method */ + const char *value = "/NO_DBUSMENU"; + p_dbus_message_iter_open_container(iter, DBUS_TYPE_VARIANT, "o", &variant); + p_dbus_message_iter_append_basic(&variant, DBUS_TYPE_OBJECT_PATH, &value); + p_dbus_message_iter_close_container(iter, &variant); + return TRUE; + } + if (!strcmp(name, "WindowId")) + { + dbus_int32_t value = 0; + p_dbus_message_iter_open_container(iter, DBUS_TYPE_VARIANT, "i", &variant); + p_dbus_message_iter_append_basic(&variant, DBUS_TYPE_INT32, &value); + p_dbus_message_iter_close_container(iter, &variant); + return TRUE; + } + return FALSE; +} + +static const char * const sni_property_names[] = +{ + "Category", "Id", "Title", "Status", "WindowId", "IconName", "IconPixmap", + "OverlayIconName", "OverlayIconPixmap", "AttentionIconName", + "AttentionIconPixmap", "AttentionMovieName", "ToolTip", "ItemIsMenu", + "Menu", "IconThemePath", +}; + +/********************************************************************** + * Forwarding clicks to the owner window + */ + +/* equivalent of the explorer/winemac notify_owner helpers; returns FALSE if + * the owner window is gone, so the caller can drop the stale icon */ +static BOOL notify_owner(struct tray_icon *icon, UINT msg, int x, int y) +{ + WPARAM wp = icon->id; + LPARAM lp = msg; + + if (icon->version >= NOTIFYICON_VERSION_4) + { + wp = MAKEWPARAM(x, y); + lp = MAKELPARAM(msg, icon->id); + } + + TRACE("posting msg %#x to hwnd %p id %u\n", msg, icon->owner, icon->id); + if (!NtUserMessageCall(icon->owner, icon->callback_message, wp, lp, + 0, NtUserSendNotifyMessage, FALSE) && + RtlGetLastWin32Error() == ERROR_INVALID_WINDOW_HANDLE) + return FALSE; + return TRUE; +} + +static void handle_activate(struct tray_icon *icon, int x, int y, UINT down, UINT up, UINT nin) +{ + struct tray_icon copy; + BOOL owner_alive; + + /* don't deliver messages while holding the mutex */ + pthread_mutex_lock(&tray_mutex); + if (!icon_is_alive(icon)) + { + pthread_mutex_unlock(&tray_mutex); + return; + } + copy = *icon; + pthread_mutex_unlock(&tray_mutex); + + owner_alive = notify_owner(©, down, x, y); + if (owner_alive) owner_alive = notify_owner(©, up, x, y); + if (owner_alive && copy.version && nin) owner_alive = notify_owner(©, nin, x, y); + + /* owner vanished without NIM_DELETE: drop the icon so it stops showing. + * delete_icon hands teardown to this very (dispatch) thread, matching the + * NIM_DELETE path; the liveness recheck guards a concurrent removal. */ + if (!owner_alive) + { + pthread_mutex_lock(&tray_mutex); + if (icon_is_alive(icon)) delete_icon(icon); + pthread_mutex_unlock(&tray_mutex); + } +} + +/********************************************************************** + * D-Bus object path handler (runs on the dispatch thread) + */ + +static DBusHandlerResult send_empty_reply(DBusConnection *conn, DBusMessage *msg) +{ + DBusMessage *reply = p_dbus_message_new_method_return(msg); + if (reply) + { + p_dbus_connection_send(conn, reply, NULL); + p_dbus_message_unref(reply); + } + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult tray_icon_message(DBusConnection *conn, DBusMessage *msg, void *user_data) +{ + struct tray_icon *icon = user_data; + DBusMessage *reply; + DBusMessageIter iter, args; + + if (p_dbus_message_is_method_call(msg, PROPERTIES_INTERFACE, "Get")) + { + const char *iface = NULL, *prop = NULL; + + if (p_dbus_message_iter_init(msg, &args) && + p_dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) + { + p_dbus_message_iter_get_basic(&args, &iface); + if (p_dbus_message_iter_next(&args) && + p_dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) + p_dbus_message_iter_get_basic(&args, &prop); + } + if (!prop) return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (!(reply = p_dbus_message_new_method_return(msg))) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + p_dbus_message_iter_init_append(reply, &iter); + pthread_mutex_lock(&tray_mutex); + if (!icon_is_alive(icon) || !append_property(&iter, icon, prop)) + { + pthread_mutex_unlock(&tray_mutex); + p_dbus_message_unref(reply); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + pthread_mutex_unlock(&tray_mutex); + p_dbus_connection_send(conn, reply, NULL); + p_dbus_message_unref(reply); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (p_dbus_message_is_method_call(msg, PROPERTIES_INTERFACE, "GetAll")) + { + DBusMessageIter dict, entry; + UINT i; + + if (!(reply = p_dbus_message_new_method_return(msg))) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + p_dbus_message_iter_init_append(reply, &iter); + p_dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &dict); + pthread_mutex_lock(&tray_mutex); + if (!icon_is_alive(icon)) + { + pthread_mutex_unlock(&tray_mutex); + p_dbus_message_unref(reply); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + for (i = 0; i < ARRAY_SIZE(sni_property_names); i++) + { + p_dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + p_dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &sni_property_names[i]); + if (!append_property(&entry, icon, sni_property_names[i])) + { + /* Every name in sni_property_names[] is handled by append_property, + * so this is unreachable; if the two ever drift, emit an empty + * string variant so the {sv} entry stays well-formed instead of + * letting libdbus abort on a dict entry with a missing value. */ + DBusMessageIter variant; + const char *empty = ""; + ERR("unhandled SNI property %s\n", sni_property_names[i]); + p_dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "s", &variant); + p_dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &empty); + p_dbus_message_iter_close_container(&entry, &variant); + } + p_dbus_message_iter_close_container(&dict, &entry); + } + pthread_mutex_unlock(&tray_mutex); + p_dbus_message_iter_close_container(&iter, &dict); + p_dbus_connection_send(conn, reply, NULL); + p_dbus_message_unref(reply); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (p_dbus_message_is_method_call(msg, SNI_INTERFACE, "Activate") || + p_dbus_message_is_method_call(msg, SNI_INTERFACE, "SecondaryActivate") || + p_dbus_message_is_method_call(msg, SNI_INTERFACE, "ContextMenu")) + { + const char *member = p_dbus_message_get_member(msg); + dbus_int32_t x = 0, y = 0; + + if (p_dbus_message_iter_init(msg, &args) && + p_dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_INT32) + { + p_dbus_message_iter_get_basic(&args, &x); + if (p_dbus_message_iter_next(&args) && + p_dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_INT32) + p_dbus_message_iter_get_basic(&args, &y); + } + + if (!strcmp(member, "Activate")) + handle_activate(icon, x, y, WM_LBUTTONDOWN, WM_LBUTTONUP, NIN_SELECT); + else if (!strcmp(member, "SecondaryActivate")) + handle_activate(icon, x, y, WM_MBUTTONDOWN, WM_MBUTTONUP, 0); + else /* ContextMenu */ + handle_activate(icon, x, y, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_CONTEXTMENU); + + return send_empty_reply(conn, msg); + } + + if (p_dbus_message_is_method_call(msg, SNI_INTERFACE, "Scroll") || + p_dbus_message_is_method_call(msg, SNI_INTERFACE, "ProvideXdgActivationToken")) + return send_empty_reply(conn, msg); + + if (p_dbus_message_is_method_call(msg, INTROSPECTABLE_INTERFACE, "Introspect")) + { + static const char xml[] = + "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n" + " \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n" + "<node>\n" + " <interface name=\"org.kde.StatusNotifierItem\">\n" + " <method name=\"Activate\"><arg name=\"x\" type=\"i\" direction=\"in\"/><arg name=\"y\" type=\"i\" direction=\"in\"/></method>\n" + " <method name=\"SecondaryActivate\"><arg name=\"x\" type=\"i\" direction=\"in\"/><arg name=\"y\" type=\"i\" direction=\"in\"/></method>\n" + " <method name=\"ContextMenu\"><arg name=\"x\" type=\"i\" direction=\"in\"/><arg name=\"y\" type=\"i\" direction=\"in\"/></method>\n" + " <method name=\"Scroll\"><arg name=\"delta\" type=\"i\" direction=\"in\"/><arg name=\"orientation\" type=\"s\" direction=\"in\"/></method>\n" + " <signal name=\"NewIcon\"/><signal name=\"NewToolTip\"/><signal name=\"NewStatus\"><arg type=\"s\"/></signal>\n" + " <property name=\"Category\" type=\"s\" access=\"read\"/>\n" + " <property name=\"Id\" type=\"s\" access=\"read\"/>\n" + " <property name=\"Title\" type=\"s\" access=\"read\"/>\n" + " <property name=\"Status\" type=\"s\" access=\"read\"/>\n" + " <property name=\"IconPixmap\" type=\"a(iiay)\" access=\"read\"/>\n" + " <property name=\"ToolTip\" type=\"(sa(iiay)ss)\" access=\"read\"/>\n" + " <property name=\"ItemIsMenu\" type=\"b\" access=\"read\"/>\n" + " <property name=\"Menu\" type=\"o\" access=\"read\"/>\n" + " </interface>\n" + "</node>\n"; + const char *xml_ptr = xml; + + if (!(reply = p_dbus_message_new_method_return(msg))) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + p_dbus_message_iter_init_append(reply, &iter); + p_dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &xml_ptr); + p_dbus_connection_send(conn, reply, NULL); + p_dbus_message_unref(reply); + return DBUS_HANDLER_RESULT_HANDLED; + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static const DBusObjectPathVTable tray_icon_vtable = +{ + .message_function = tray_icon_message, +}; + +/********************************************************************** + * Watcher registration and connection management + */ + +/* Fire-and-forget registration: blocking on a reply here could deadlock, + * since replies are delivered by the dispatch thread, which may itself be + * waiting on tray_mutex to serve a property request. */ +static BOOL register_icon_with_watcher(struct tray_icon *icon) +{ + DBusMessage *msg; + const char *name = p_dbus_bus_get_unique_name(icon->connection); + + if (!(msg = p_dbus_message_new_method_call(WATCHER_SERVICE, WATCHER_PATH, + WATCHER_SERVICE, "RegisterStatusNotifierItem"))) + return FALSE; + + { + DBusMessageIter iter; + p_dbus_message_iter_init_append(msg, &iter); + p_dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &name); + } + + p_dbus_connection_send(icon->connection, msg, NULL); + p_dbus_connection_flush(icon->connection); + p_dbus_message_unref(msg); + icon->registered = TRUE; + TRACE("registered %s for hwnd %p id %u\n", name, icon->owner, icon->id); + return TRUE; +} + +/* handle StatusNotifierWatcher restarts: re-register this icon */ +static DBusHandlerResult tray_filter(DBusConnection *conn, DBusMessage *msg, void *user_data) +{ + struct tray_icon *icon = user_data; + + if (p_dbus_message_is_signal(msg, "org.freedesktop.DBus", "NameOwnerChanged")) + { + const char *name = NULL, *new_owner = NULL; + DBusMessageIter args; + + /* signature is (sss): name, old owner, new owner. Check each arg is + * actually a string before reading it -- get_basic copies by the + * message's declared type, so a crafted non-string arg would write a + * 4-byte value into our 8-byte char* and we would deref garbage. */ + if (p_dbus_message_iter_init(msg, &args) && + p_dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) + { + p_dbus_message_iter_get_basic(&args, &name); + /* skip the old-owner arg, read the new-owner arg */ + if (p_dbus_message_iter_next(&args) && p_dbus_message_iter_next(&args) && + p_dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) + p_dbus_message_iter_get_basic(&args, &new_owner); + } + + if (name && !strcmp(name, WATCHER_SERVICE) && new_owner && new_owner[0]) + { + TRACE("StatusNotifierWatcher appeared, re-registering\n"); + pthread_mutex_lock(&tray_mutex); + if (icon_is_alive(icon)) register_icon_with_watcher(icon); + pthread_mutex_unlock(&tray_mutex); + } + } + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +/* Unix entry point for the PE-side dispatch thread: parks until tray icons + * exist, then waits on all their connections' fds at once and dispatches the + * ones with traffic. The thread is a proper win32 thread, so message delivery + * to icon owners and other NTUSER calls are safe here. A self-pipe wakes the + * poll when the icon lists change; other threads that send on a connection + * (registration, signal emission) flush it themselves. */ +NTSTATUS waylanddrv_unix_tray_dispatch(void *arg) +{ + DBusConnection *conns[64]; + struct pollfd fds[ARRAY_SIZE(conns) + 1]; + UINT i, count, nfds; + int timeout; + struct tray_icon *icon; + + pthread_mutex_lock(&tray_mutex); + if (tray_wake_pipe[0] == -1 && !pipe(tray_wake_pipe)) + { + for (i = 0; i < 2; i++) + { + fcntl(tray_wake_pipe[i], F_SETFL, O_NONBLOCK); + fcntl(tray_wake_pipe[i], F_SETFD, FD_CLOEXEC); + } + } + pthread_mutex_unlock(&tray_mutex); + + for (;;) + { + struct tray_icon *dying, *dying_next; + + pthread_mutex_lock(&tray_mutex); + /* the previous iteration's snapshot refs are already dropped, so no + * dispatch is in flight on these connections anymore: safe to close */ + LIST_FOR_EACH_ENTRY_SAFE(dying, dying_next, &tray_dying_icons, struct tray_icon, entry) + destroy_dying_icon(dying); + while (list_empty(&tray_icons) && list_empty(&tray_dying_icons)) + pthread_cond_wait(&tray_start_cond, &tray_mutex); + count = 0; + LIST_FOR_EACH_ENTRY(icon, &tray_icons, struct tray_icon, entry) + { + if (count == ARRAY_SIZE(conns)) + { + static BOOL warned; + if (!warned) { WARN("more than %zu tray icons, some will be unresponsive\n", ARRAY_SIZE(conns)); warned = TRUE; } + break; + } + p_dbus_connection_ref(icon->connection); + conns[count++] = icon->connection; + } + pthread_mutex_unlock(&tray_mutex); + + /* sleep until a connection has traffic or the icon lists change */ + nfds = 0; + timeout = -1; + if (tray_wake_pipe[0] != -1) + { + fds[nfds].fd = tray_wake_pipe[0]; + fds[nfds].events = POLLIN; + fds[nfds++].revents = 0; + } + else timeout = 200; /* no self-pipe: fall back to periodic list refresh */ + for (i = 0; i < count; i++) + { + int fd; + if (p_dbus_connection_get_unix_fd(conns[i], &fd)) + { + fds[nfds].fd = fd; + fds[nfds].events = POLLIN; + fds[nfds++].revents = 0; + } + else timeout = 0; /* no fd to wait on (disconnecting): service it now */ + } + poll(fds, nfds, timeout); + if (tray_wake_pipe[0] != -1 && fds[0].revents) + { + char buf[64]; + while (read(tray_wake_pipe[0], buf, sizeof(buf)) > 0) /* drain */; + } + + for (i = 0; i < count; i++) + { + BOOL alive; + + /* dispatch everything pending without blocking (each call handles + * at most one message), then flush replies queued by the handlers. + * read_write_dispatch returns FALSE once the connection has + * disconnected (e.g. the session bus restarted); the icon must + * then be dropped or its dead fd would wake the poll in a loop. */ + while ((alive = p_dbus_connection_read_write_dispatch(conns[i], 0)) && + p_dbus_connection_get_dispatch_status(conns[i]) == DBUS_DISPATCH_DATA_REMAINS) + ; + if (!alive) + { + struct tray_icon *dead; + pthread_mutex_lock(&tray_mutex); + LIST_FOR_EACH_ENTRY(dead, &tray_icons, struct tray_icon, entry) + { + if (dead->connection != conns[i]) continue; + delete_icon(dead); /* hands teardown to the top of this loop */ + break; + } + pthread_mutex_unlock(&tray_mutex); + } + else p_dbus_connection_flush(conns[i]); + p_dbus_connection_unref(conns[i]); + } + } + return STATUS_UNSUCCESSFUL; +} + +/* called with tray_mutex held */ +static BOOL ensure_dbus(void) +{ + if (tray_dbus_loaded) return TRUE; + if (tray_dbus_failed) return FALSE; + if (!load_dbus_functions()) + { + tray_dbus_failed = TRUE; + return FALSE; + } + p_dbus_threads_init_default(); + tray_dbus_loaded = TRUE; + return TRUE; +} + +/* create the icon's private bus connection; called with tray_mutex held, + * before the icon is visible to the dispatch thread, so the blocking + * watcher-presence check cannot deadlock */ +static BOOL icon_connect(struct tray_icon *icon) +{ + DBusConnection *connection; + DBusError error; + + p_dbus_error_init(&error); + if (!(connection = p_dbus_bus_get_private(DBUS_BUS_SESSION, &error))) + { + WARN("failed to connect to the session bus: %s\n", error.message ? error.message : "?"); + p_dbus_error_free(&error); + return FALSE; + } + p_dbus_connection_set_exit_on_disconnect(connection, FALSE); + + if (!p_dbus_bus_name_has_owner(connection, WATCHER_SERVICE, NULL)) + { + WARN("no StatusNotifierWatcher on the session bus, using the fallback tray\n"); + p_dbus_connection_close(connection); + p_dbus_connection_unref(connection); + return FALSE; + } + + p_dbus_bus_add_match(connection, + "type='signal',sender='org.freedesktop.DBus'," + "interface='org.freedesktop.DBus',member='NameOwnerChanged'," + "arg0='" WATCHER_SERVICE "'", NULL); + p_dbus_connection_add_filter(connection, tray_filter, icon, NULL); + + if (!p_dbus_connection_try_register_object_path(connection, SNI_OBJECT_PATH, + &tray_icon_vtable, icon, NULL)) + { + p_dbus_connection_close(connection); + p_dbus_connection_unref(connection); + return FALSE; + } + + icon->connection = connection; + return TRUE; +} + +/********************************************************************** + * Icon list management + */ + +/* check that the icon has not been freed; called with tray_mutex held. + * Handlers run on the dispatch thread with a raw user_data pointer, while + * delete_icon frees icons under the same mutex on the explorer thread. */ +static BOOL icon_is_alive(struct tray_icon *icon) +{ + struct tray_icon *cur; + + LIST_FOR_EACH_ENTRY(cur, &tray_icons, struct tray_icon, entry) + if (cur == icon) return TRUE; + return FALSE; +} + +static struct tray_icon *get_icon(HWND owner, UINT id) +{ + struct tray_icon *icon; + + LIST_FOR_EACH_ENTRY(icon, &tray_icons, struct tray_icon, entry) + if (icon->owner == owner && icon->id == id) return icon; + return NULL; +} + +static void emit_icon_signal(struct tray_icon *icon, const char *name) +{ + DBusMessage *msg; + + if (!(msg = p_dbus_message_new_signal(SNI_OBJECT_PATH, SNI_INTERFACE, name))) return; + if (!strcmp(name, "NewStatus")) + { + DBusMessageIter iter; + const char *status = icon_status(icon); + p_dbus_message_iter_init_append(msg, &iter); + p_dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &status); + } + p_dbus_connection_send(icon->connection, msg, NULL); + p_dbus_connection_flush(icon->connection); + p_dbus_message_unref(msg); +} + +static BOOL modify_icon(struct tray_icon *icon, NOTIFYICONDATAW *data) +{ + /* the icon conversion is the only fallible step: do it first so a + * failure does not leave partially applied changes */ + if (data->uFlags & NIF_ICON) + { + if (!set_icon_pixmap(icon, data->hIcon)) return FALSE; + if (icon->registered) emit_icon_signal(icon, "NewIcon"); + } + if (data->uFlags & NIF_MESSAGE) icon->callback_message = data->uCallbackMessage; + if (data->uFlags & NIF_STATE) + { + DWORD new_state = (icon->state & ~data->dwStateMask) | (data->dwState & data->dwStateMask); + if (new_state != icon->state) + { + icon->state = new_state; + if (icon->registered) emit_icon_signal(icon, "NewStatus"); + } + } + if (data->uFlags & NIF_TIP) + { + set_icon_tooltip(icon, data->szTip); + if (icon->registered) emit_icon_signal(icon, "NewToolTip"); + } + if (data->uFlags & NIF_INFO && data->szInfo[0]) + FIXME("balloon notifications are not supported\n"); + return TRUE; +} + +static LRESULT add_icon(HWND owner, NOTIFYICONDATAW *data) +{ + struct tray_icon *icon; + const char *env = getenv("WINEWAYLAND_NO_SNI"); + + if (env && atoi(env)) return -1; + if (!ensure_dbus()) return -1; + + /* On Windows a second NIM_ADD for the same (owner, id) fails; without this + * check explorer (which forwards every NIM_ADD to the driver) would leak a + * second StatusNotifierItem that NIM_DELETE can never remove. */ + if (get_icon(owner, data->uID)) + { + WARN("duplicate tray icon add for hwnd %p id %u\n", owner, data->uID); + return FALSE; + } + + if (!(icon = calloc(1, sizeof(*icon)))) return -1; /* let explorer fall back */ + icon->owner = owner; + icon->id = data->uID; + + if (!modify_icon(icon, data)) + { + free(icon->icon_argb); + free(icon); + return -1; /* transient failure: let explorer show its own tray icon */ + } + + if (!icon_connect(icon)) + { + free(icon->icon_argb); + free(icon); + return -1; + } + + register_icon_with_watcher(icon); + + list_add_tail(&tray_icons, &icon->entry); + /* wake up the parked or polling PE dispatch thread */ + tray_wake_dispatch(); + TRACE("added icon for hwnd %p id %u\n", owner, icon->id); + return TRUE; +} + +/* tear down a dying icon's connection; runs on the dispatch thread, with + * tray_mutex held, after read_write_dispatch on this connection has returned */ +static void destroy_dying_icon(struct tray_icon *icon) +{ + TRACE("destroying icon for hwnd %p id %u\n", icon->owner, icon->id); + p_dbus_connection_remove_filter(icon->connection, tray_filter, icon); + p_dbus_connection_unregister_object_path(icon->connection, SNI_OBJECT_PATH); + /* closing the connection drops our unique bus name; the watcher and + * hosts remove the item immediately */ + p_dbus_connection_close(icon->connection); + p_dbus_connection_unref(icon->connection); + list_remove(&icon->entry); + free(icon->icon_argb); + free(icon); +} + +static void delete_icon(struct tray_icon *icon) +{ + TRACE("removing icon for hwnd %p id %u\n", icon->owner, icon->id); + /* the list entry must go first: handlers on the dispatch thread validate + * liveness through the list under the same mutex our callers hold */ + list_remove(&icon->entry); + /* hand the connection teardown to the dispatch thread, which may be inside + * read_write_dispatch on this very connection right now */ + list_add_tail(&tray_dying_icons, &icon->entry); + tray_wake_dispatch(); +} + +/********************************************************************** + * Driver entry points + */ + +LRESULT WAYLAND_NotifyIcon(HWND hwnd, UINT msg, NOTIFYICONDATAW *data) +{ + struct tray_icon *icon; + LRESULT ret = FALSE; + + TRACE("hwnd %p msg %#x id %u\n", hwnd, msg, data->uID); + + pthread_mutex_lock(&tray_mutex); + switch (msg) + { + case NIM_ADD: + ret = add_icon(hwnd, data); + break; + case NIM_MODIFY: + if ((icon = get_icon(hwnd, data->uID))) ret = modify_icon(icon, data); + break; + case NIM_DELETE: + if ((icon = get_icon(hwnd, data->uID))) + { + delete_icon(icon); + ret = TRUE; + } + break; + case NIM_SETVERSION: + if ((icon = get_icon(hwnd, data->uID))) + { + icon->version = data->uVersion; + ret = TRUE; + } + break; + default: + FIXME("unhandled tray message %#x\n", msg); + ret = -1; + break; + } + pthread_mutex_unlock(&tray_mutex); + return ret; +} + +void WAYLAND_CleanupIcons(HWND hwnd) +{ + struct tray_icon *icon, *next; + + pthread_mutex_lock(&tray_mutex); + LIST_FOR_EACH_ENTRY_SAFE(icon, next, &tray_icons, struct tray_icon, entry) + if (icon->owner == hwnd) delete_icon(icon); + pthread_mutex_unlock(&tray_mutex); +} + +#else /* SONAME_LIBDBUS_1 */ + +LRESULT WAYLAND_NotifyIcon(HWND hwnd, UINT msg, NOTIFYICONDATAW *data) +{ + return -1; +} + +void WAYLAND_CleanupIcons(HWND hwnd) +{ +} + +/* The PE dispatch thread and both unix-call tables reference this symbol + * unconditionally, so it must exist even without libdbus; explorer falls back + * to its own tray window because WAYLAND_NotifyIcon returns -1 above. */ +NTSTATUS waylanddrv_unix_tray_dispatch(void *arg) +{ + return STATUS_NOT_SUPPORTED; +} + +#endif /* SONAME_LIBDBUS_1 */ diff --git a/dlls/winewayland.drv/unixlib.h b/dlls/winewayland.drv/unixlib.h index d9378fe8248..ab4b0924888 100644 --- a/dlls/winewayland.drv/unixlib.h +++ b/dlls/winewayland.drv/unixlib.h @@ -28,6 +28,7 @@ enum waylanddrv_unix_func waylanddrv_unix_func_init, waylanddrv_unix_func_read_events, waylanddrv_unix_func_init_clipboard, + waylanddrv_unix_func_tray_dispatch, waylanddrv_unix_func_count, }; diff --git a/dlls/winewayland.drv/wayland.c b/dlls/winewayland.drv/wayland.c index 5b2b0c5b25b..fb7ad1dac9c 100644 --- a/dlls/winewayland.drv/wayland.c +++ b/dlls/winewayland.drv/wayland.c @@ -215,6 +215,11 @@ static void registry_handle_global(void *data, struct wl_registry *registry, process_wayland.wp_fractional_scale_manager_v1 = wl_registry_bind(registry, id, &wp_fractional_scale_manager_v1_interface, 1); } + else if (strcmp(interface, "zwp_linux_dmabuf_v1") == 0) + { + process_wayland.zwp_linux_dmabuf_v1 = + wl_registry_bind(registry, id, &zwp_linux_dmabuf_v1_interface, 3); + } } static void registry_handle_global_remove(void *data, struct wl_registry *registry, diff --git a/dlls/winewayland.drv/wayland_output.c b/dlls/winewayland.drv/wayland_output.c index f76881a1770..7b251f3e6d4 100644 --- a/dlls/winewayland.drv/wayland_output.c +++ b/dlls/winewayland.drv/wayland_output.c @@ -39,6 +39,8 @@ static uint32_t next_output_id = 0; #define WAYLAND_OUTPUT_CHANGED_NAME 0x02 #define WAYLAND_OUTPUT_CHANGED_LOGICAL_XY 0x04 #define WAYLAND_OUTPUT_CHANGED_LOGICAL_WH 0x08 +#define WAYLAND_OUTPUT_CHANGED_GEOMETRY 0x10 +#define WAYLAND_OUTPUT_CHANGED_SCALE 0x20 /********************************************************************** * Output handling @@ -128,6 +130,21 @@ static void wayland_output_mode_free_rb(struct rb_entry *entry, void *ctx) free(RB_ENTRY_VALUE(entry, struct wayland_output_mode, entry)); } +/* A 90/270 (optionally flipped) rotation swaps an output's width and height. */ +static BOOL wayland_transform_swaps_dimensions(int32_t transform) +{ + switch (transform) + { + case WL_OUTPUT_TRANSFORM_90: + case WL_OUTPUT_TRANSFORM_270: + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + return TRUE; + default: + return FALSE; + } +} + static void wayland_output_done(struct wayland_output *output) { struct wayland_output_mode *mode; @@ -165,17 +182,66 @@ static void wayland_output_done(struct wayland_output *output) { output->current.logical_w = output->pending.logical_w; output->current.logical_h = output->pending.logical_h; + output->current.has_xdg_logical_size = TRUE; } + if (output->pending_flags & WAYLAND_OUTPUT_CHANGED_GEOMETRY) + { + output->current.transform = output->pending.transform; + output->current.physical_width = output->pending.physical_width; + output->current.physical_height = output->pending.physical_height; + } + + if (output->pending_flags & WAYLAND_OUTPUT_CHANGED_SCALE) + output->current.scale = output->pending.scale; + output->pending_flags = 0; - /* Ensure the logical dimensions have sane values. */ - if ((!output->current.logical_w || !output->current.logical_h) && - output->current.current_mode) + /* When xdg_output has never reported a logical size (e.g. a compositor + * without the protocol), default the logical size to the physical mode size + * for the display layout, but don't treat it as a reliable compositor scale: + * a stale default must not masquerade as a real logical size on a later + * wl_output.done that carries no new logical size. */ + if (!output->current.has_xdg_logical_size && output->current.current_mode) { output->current.logical_w = output->current.current_mode->width; output->current.logical_h = output->current.current_mode->height; } + output->current.has_logical_size = output->current.has_xdg_logical_size && + output->current.logical_w && output->current.logical_h; + + /* The compositor scale is the ratio of the output's physical mode size to + * its logical (post-scaling) size. Wine renders surfaces in physical pixels + * and maps them to logical Wayland coordinates by this factor, so it tracks + * the real output scale (including fractional scaling) rather than Wine's + * configured DPI. wl_output modes are pre-transform while the xdg_output + * logical size is post-transform, so for a 90/270 rotation the logical width + * maps to the mode's height. 0.0 means "unknown" -> callers fall back to the + * configured DPI. */ + if (output->current.has_logical_size && output->current.current_mode) + { + int mode_w = output->current.current_mode->width; + if (wayland_transform_swaps_dimensions(output->current.transform)) + mode_w = output->current.current_mode->height; + output->current.compositor_scale = (double)mode_w / output->current.logical_w; + } + else if (output->current.scale > 0) + { + /* No xdg_output logical size: fall back to the wl_output.scale event. + * Compositors without xdg_output (e.g. gamescope, which sends scale 1 + * and expects clients to render at the advertised mode size) still + * declare their intended scale this way. */ + output->current.compositor_scale = output->current.scale; + } + else + { + output->current.compositor_scale = 0.0; + } + + /* Keep the physical positions in sync in every process: only the desktop + * process runs a full display-device update, but per-window scale selection + * needs them everywhere. */ + wayland_update_outputs_physical_coords(); pthread_mutex_unlock(&process_wayland.output_mutex); @@ -200,6 +266,14 @@ static void output_handle_geometry(void *data, struct wl_output *wl_output, const char *make, const char *model, int32_t output_transform) { + struct wayland_output *output = data; + TRACE("transform=%d physical=%dx%d mm make=%s model=%s\n", + output_transform, physical_width, physical_height, + debugstr_a(make), debugstr_a(model)); + output->pending.transform = output_transform; + output->pending.physical_width = physical_width; + output->pending.physical_height = physical_height; + output->pending_flags |= WAYLAND_OUTPUT_CHANGED_GEOMETRY; } static void output_handle_mode(void *data, struct wl_output *wl_output, @@ -231,6 +305,10 @@ static void output_handle_done(void *data, struct wl_output *wl_output) static void output_handle_scale(void *data, struct wl_output *wl_output, int32_t scale) { + struct wayland_output *output = data; + TRACE("scale=%d\n", scale); + output->pending.scale = scale; + output->pending_flags |= WAYLAND_OUTPUT_CHANGED_SCALE; } static const struct wl_output_listener output_listener = { @@ -369,6 +447,8 @@ void wayland_output_destroy(struct wayland_output *output) { pthread_mutex_lock(&process_wayland.output_mutex); wl_list_remove(&output->link); + /* the layout of the remaining outputs may shift */ + wayland_update_outputs_physical_coords(); pthread_mutex_unlock(&process_wayland.output_mutex); wayland_output_state_deinit(&output->pending); @@ -394,3 +474,59 @@ void wayland_output_use_xdg_extension(struct wayland_output *output) zxdg_output_v1_add_listener(output->zxdg_output_v1, &zxdg_output_v1_listener, output); } + +/********************************************************************** + * wayland_output_scale_for_rect + * + * Returns the compositor scale (physical/logical ratio) of the output that + * contains the center of the given physical-pixel rectangle, so a window can + * be rendered and have its pointer mapped at the correct scale for the monitor + * it is on. Returns 0.0 when it can't be reliably determined (no xdg_output + * logical-size info, or the rect isn't on any known output), in which case the + * caller should fall back to the configured DPI. + */ +double wayland_output_scale_for_rect(const RECT *rect) +{ + struct wayland_output *output, *best = NULL; + double scale = 0.0; + int cx = (rect->left + rect->right) / 2; + int cy = (rect->top + rect->bottom) / 2; + + pthread_mutex_lock(&process_wayland.output_mutex); + + wl_list_for_each(output, &process_wayland.output_list, link) + { + struct wayland_output_state *os = &output->current; + int ext_w, ext_h; + + if (!os->current_mode || os->compositor_scale <= 0.0) continue; + + /* wl_output modes are pre-transform, so for a 90/270 rotation the on-screen + * extent swaps width and height. */ + ext_w = os->current_mode->width; + ext_h = os->current_mode->height; + if (wayland_transform_swaps_dimensions(os->transform)) + { + ext_w = os->current_mode->height; + ext_h = os->current_mode->width; + } + + /* Prefer the output whose physical area contains the rect center. */ + if (cx >= os->phys_x && cx < os->phys_x + ext_w && + cy >= os->phys_y && cy < os->phys_y + ext_h) + { + scale = os->compositor_scale; + break; + } + + /* Otherwise remember a usable output (preferring the primary one at the + * origin) as a fallback for windows that aren't on any output yet. */ + if (!best || (os->phys_x == 0 && os->phys_y == 0)) best = output; + } + + if (scale == 0.0 && best) scale = best->current.compositor_scale; + + pthread_mutex_unlock(&process_wayland.output_mutex); + + return scale; +} diff --git a/dlls/winewayland.drv/wayland_pointer.c b/dlls/winewayland.drv/wayland_pointer.c index af404994a44..4291515c8b8 100644 --- a/dlls/winewayland.drv/wayland_pointer.c +++ b/dlls/winewayland.drv/wayland_pointer.c @@ -121,6 +121,12 @@ static HWND wayland_pointer_get_focused_hwnd(void) return hwnd; } +static void pointer_send_to_toplevel(HWND hwnd, INPUT *input) +{ + HWND top = NtUserGetAncestor(hwnd, GA_ROOT); + NtUserSendHardwareInput(top ? top : hwnd, 0, input, 0); +} + static void pointer_handle_motion_internal(wl_fixed_t sx, wl_fixed_t sy) { INPUT input = {0}; @@ -141,14 +147,25 @@ static void pointer_handle_motion_internal(wl_fixed_t sx, wl_fixed_t sy) window_rect = &surface->window.rect; screen = map_point_from_surface(surface, screen); - screen.x += window_rect->left; - screen.y += window_rect->top; - /* Sometimes, due to rounding, we may end up with pointer coordinates - * slightly outside the target window, so bring them within bounds. */ - if (screen.x >= window_rect->right) screen.x = window_rect->right - 1; - else if (screen.x < window_rect->left) screen.x = window_rect->left; - if (screen.y >= window_rect->bottom) screen.y = window_rect->bottom - 1; - else if (screen.y < window_rect->top) screen.y = window_rect->top; + + if (surface->role == WAYLAND_SURFACE_ROLE_SUBSURFACE) + { + /* CEF/Chromium GPU content lives in subsurface child windows; map the + * surface-local point through the child to absolute screen coords so the + * hit test reaches the right window. */ + NtUserMapWindowPoints(hwnd, 0, &screen, 1, NtUserGetWinMonitorDpi(hwnd, MDT_RAW_DPI)); + } + else + { + screen.x += window_rect->left; + screen.y += window_rect->top; + /* Sometimes, due to rounding, we may end up with pointer coordinates + * slightly outside the target window, so bring them within bounds. */ + if (screen.x >= window_rect->right) screen.x = window_rect->right - 1; + else if (screen.x < window_rect->left) screen.x = window_rect->left; + if (screen.y >= window_rect->bottom) screen.y = window_rect->bottom - 1; + else if (screen.y < window_rect->top) screen.y = window_rect->top; + } wayland_win_data_release(data); @@ -161,7 +178,13 @@ static void pointer_handle_motion_internal(wl_fixed_t sx, wl_fixed_t sy) hwnd, wl_fixed_to_double(sx), wl_fixed_to_double(sy), screen.x, screen.y); - NtUserSendHardwareInput(hwnd, 0, &input, 0); + /* Deliver scoped to the toplevel (like the X11 driver and pointer_handle_button) + * so win32u's window_from_point descends the whole tree. With GPU CEF the + * content lives in subsurface child windows; scoping to the focused child + * confines the hit test to that subtree and a hover never reaches the menu + * bar items in the toplevel, so hover-to-open menus don't work (they do with + * GPU disabled, where content is on the main surface). */ + pointer_send_to_toplevel(hwnd, &input); } static void pointer_handle_motion(void *data, struct wl_pointer *wl_pointer, @@ -266,7 +289,7 @@ static void pointer_handle_button(void *data, struct wl_pointer *wl_pointer, TRACE("hwnd=%p button=%#x state=%u\n", hwnd, button, state); - NtUserSendHardwareInput(hwnd, 0, &input, 0); + pointer_send_to_toplevel(hwnd, &input); } static void pointer_handle_axis(void *data, struct wl_pointer *wl_pointer, @@ -313,7 +336,9 @@ static void pointer_handle_axis_value120(void *data, struct wl_pointer *wl_point TRACE("hwnd=%p axis=%u value120=%d\n", hwnd, axis, value120); - NtUserSendHardwareInput(hwnd, 0, &input, 0); + /* Deliver scoped to the toplevel (like pointer_handle_motion/button) so wheel + * events over a child subsurface (e.g. CEF content) hit-test correctly. */ + pointer_send_to_toplevel(hwnd, &input); } static void pointer_handle_axis_discrete(void *data, struct wl_pointer *wl_pointer, @@ -365,6 +390,11 @@ static void relative_pointer_v1_relative_motion(void *private, if (!(hwnd = wayland_pointer_get_focused_hwnd())) return; if (!(data = wayland_win_data_get(hwnd))) return; + if (!data->wayland_surface) + { + wayland_win_data_release(data); + return; + } wayland_motion_delta_to_window(data->wayland_surface, wl_fixed_to_double(dx), @@ -450,7 +480,7 @@ void wayland_pointer_deinit(void) * * Adapted from wineandroid.drv code. */ -static struct wayland_shm_buffer *create_mono_cursor_buffer(HBITMAP bmp) +struct wayland_shm_buffer *create_mono_cursor_buffer(HBITMAP bmp) { struct wayland_shm_buffer *shm_buffer = NULL; BITMAP bm; @@ -491,11 +521,11 @@ done: } /*********************************************************************** - * get_icon_info + * wayland_get_icon_info * * Local GetIconInfoExW helper implementation. */ -static BOOL get_icon_info(HICON handle, ICONINFOEXW *ret) +BOOL wayland_get_icon_info(HICON handle, ICONINFOEXW *ret) { UNICODE_STRING module, res_name; ICONINFO info; @@ -527,6 +557,15 @@ static BOOL cursor_buffer_is_transparent(struct wayland_shm_buffer *shm_buffer) return TRUE; } +static void cursor_drop_shm_buffer(struct wayland_cursor *cursor) +{ + if (cursor->shm_buffer) + { + wayland_shm_buffer_unref(cursor->shm_buffer); + cursor->shm_buffer = NULL; + } +} + static void wayland_pointer_update_cursor_buffer(HCURSOR hcursor, double scale) { struct wayland_cursor *cursor = &process_wayland.pointer.cursor; @@ -535,13 +574,9 @@ static void wayland_pointer_update_cursor_buffer(HCURSOR hcursor, double scale) if (!hcursor) goto clear_cursor; /* Create a new buffer for the specified cursor. */ - if (cursor->shm_buffer) - { - wayland_shm_buffer_unref(cursor->shm_buffer); - cursor->shm_buffer = NULL; - } + cursor_drop_shm_buffer(cursor); - if (!get_icon_info(hcursor, &info)) + if (!wayland_get_icon_info(hcursor, &info)) { ERR("Failed to get icon info for cursor=%p\n", hcursor); goto clear_cursor; @@ -588,11 +623,7 @@ static void wayland_pointer_update_cursor_buffer(HCURSOR hcursor, double scale) return; clear_cursor: - if (cursor->shm_buffer) - { - wayland_shm_buffer_unref(cursor->shm_buffer); - cursor->shm_buffer = NULL; - } + cursor_drop_shm_buffer(cursor); } static void wayland_pointer_clear_cursor_surface(void) @@ -609,11 +640,7 @@ static void wayland_pointer_clear_cursor_surface(void) wl_surface_destroy(cursor->wl_surface); cursor->wl_surface = NULL; } - if (cursor->shm_buffer) - { - wayland_shm_buffer_unref(cursor->shm_buffer); - cursor->shm_buffer = NULL; - } + cursor_drop_shm_buffer(cursor); } static void wayland_pointer_update_cursor_surface(double scale) @@ -633,7 +660,7 @@ static void wayland_pointer_update_cursor_surface(double scale) } } - if (!cursor->wp_viewport) + if (!cursor->wp_viewport && process_wayland.wp_viewporter) { cursor->wp_viewport = wp_viewporter_get_viewport(process_wayland.wp_viewporter, @@ -656,9 +683,10 @@ static void wayland_pointer_update_cursor_surface(double scale) * scale. Note that setting the viewport destination overrides * the buffer scale, so it's fine to set both. */ wl_surface_set_buffer_scale(cursor->wl_surface, round(scale)); - wp_viewport_set_destination(cursor->wp_viewport, - round(cursor->shm_buffer->width / scale), - round(cursor->shm_buffer->height / scale)); + if (cursor->wp_viewport) + wp_viewport_set_destination(cursor->wp_viewport, + round(cursor->shm_buffer->width / scale), + round(cursor->shm_buffer->height / scale)); wl_surface_commit(cursor->wl_surface); return; @@ -715,7 +743,7 @@ static BOOL wayland_pointer_set_cursor_shape(HCURSOR hcursor) if (!process_wayland.wp_cursor_shape_manager_v1) return FALSE; if (!hcursor) return FALSE; - if (!get_icon_info(hcursor, &info)) return FALSE; + if (!wayland_get_icon_info(hcursor, &info)) return FALSE; proto_version = wp_cursor_shape_manager_v1_get_version( process_wayland.wp_cursor_shape_manager_v1); shape = cursor_shape_from_info(&info, proto_version); @@ -757,6 +785,15 @@ static void wayland_set_cursor(HWND hwnd, HCURSOR hcursor, BOOL use_hcursor) struct wayland_win_data *data; double scale; BOOL reapply_clip = FALSE; + HWND hwnd_root, focused; + + /* Resolve the toplevel up front, BEFORE taking win_data_mutex (and + * pointer->mutex): NtUserGetAncestor takes win32u's user_lock, while the + * window-surface flush path holds user_lock and then acquires win_data_mutex + * (apply_window_pos -> wayland_window_surface_flush -> wayland_win_data_get). + * Calling NtUserGetAncestor while holding win_data_mutex would invert those + * two locks against the flush path and deadlock. */ + hwnd_root = NtUserGetAncestor(hwnd, GA_ROOT); if ((data = wayland_win_data_get(hwnd))) { @@ -767,7 +804,25 @@ static void wayland_set_cursor(HWND hwnd, HCURSOR hcursor, BOOL use_hcursor) } scale = surface->window.scale; if (use_hcursor) surface->hcursor = hcursor; - else hcursor = surface->hcursor; + else + { + hcursor = surface->hcursor; + /* A child surface (e.g. CEF/Chromium GPU content presented as a + * subsurface) may have no cursor of its own, so a plain enter would + * clear the pointer cursor. With input delivered scoped to the + * toplevel the cursor is set on the toplevel, so inherit it here: + * otherwise the empty cursor makes wayland_pointer_update_constraint + * treat a screen-sized window as a cursor-hiding fullscreen client + * and lock (hide) the pointer. */ + if (!hcursor && hwnd_root && hwnd_root != hwnd) + { + struct wayland_win_data *top_data; + /* hwnd_root resolved above, outside win_data_mutex */ + if ((top_data = wayland_win_data_get_nolock(hwnd_root)) && + top_data->wayland_surface) + hcursor = top_data->wayland_surface->hcursor; + } + } use_hcursor = TRUE; wayland_win_data_release(data); } @@ -776,9 +831,23 @@ static void wayland_set_cursor(HWND hwnd, HCURSOR hcursor, BOOL use_hcursor) scale = 1.0; } + /* Snapshot the pointer focus under pointer->mutex; hwnd_root was resolved + * above, outside all locks (NtUserGetAncestor takes user_lock and must not + * run under pointer->mutex or win_data_mutex). A focus change racing the + * snapshot is benign -- a stray cursor set for a just-unfocused window is + * corrected by the next enter/SetCursor. */ pthread_mutex_lock(&pointer->mutex); - if (pointer->focused_hwnd == hwnd) + focused = pointer->focused_hwnd; + pthread_mutex_unlock(&pointer->mutex); + + /* Accept a SetCursor for any window in the same toplevel as the pointer focus, + * not just the exact focused window: input is delivered scoped to the toplevel, + * so WM_SETCURSOR may target the toplevel or a sibling child of the focused + * subsurface. There is a single pointer cursor per focus, so this is safe and + * keeps the cursor visible. */ + if (focused && (focused == hwnd || NtUserGetAncestor(focused, GA_ROOT) == hwnd_root)) { + pthread_mutex_lock(&pointer->mutex); if ((!use_hcursor && pointer->wp_cursor_shape_device_v1) || (use_hcursor && hcursor && wayland_pointer_set_cursor_shape(hcursor))) { @@ -797,8 +866,8 @@ static void wayland_set_cursor(HWND hwnd, HCURSOR hcursor, BOOL use_hcursor) } wl_display_flush(process_wayland.wl_display); reapply_clip = TRUE; + pthread_mutex_unlock(&pointer->mutex); } - pthread_mutex_unlock(&pointer->mutex); /* Reapply cursor clip since cursor visibility affects pointer constraint * behavior. */ @@ -1085,9 +1154,15 @@ BOOL WAYLAND_ClipCursor(const RECT *clip, BOOL reset) wl_fixed_from_int(warp.y)); pthread_mutex_unlock(&pointer->mutex); - data = wayland_win_data_get(hwnd); - wl_surface_commit(wl_surface); - wayland_win_data_release(data); + /* The foreground window may have been torn down since we cached + * wl_surface; only commit while its win data (and thus the surface) is + * still alive, and never release a NULL data — that would unlock + * win_data_mutex without holding it. */ + if ((data = wayland_win_data_get(hwnd))) + { + wl_surface_commit(wl_surface); + wayland_win_data_release(data); + } TRACE("position hint hwnd=%p wayland_xy=%s screen_xy=%s\n", hwnd, wine_dbgstr_point(&warp), wine_dbgstr_point(&cursor_pos)); pthread_mutex_lock(&pointer->mutex); diff --git a/dlls/winewayland.drv/wayland_surface.c b/dlls/winewayland.drv/wayland_surface.c index 0dae0922179..85739c8ebf9 100644 --- a/dlls/winewayland.drv/wayland_surface.c +++ b/dlls/winewayland.drv/wayland_surface.c @@ -34,6 +34,17 @@ WINE_DEFAULT_DEBUG_CHANNEL(waylanddrv); +/* Bumped whenever a window's position/z-order/visibility may have changed; a + * subsurface restacks only when this differs from the value it last restacked + * at, so steady-state presents skip the window-list walk. File-static: only the + * bump helper below and the reconfigure path read it. */ +static LONG subsurface_restack_serial = 1; + +void wayland_invalidate_subsurface_zorder(void) +{ + InterlockedIncrement(&subsurface_restack_serial); +} + static void xdg_surface_handle_configure(void *private, struct xdg_surface *xdg_surface, uint32_t serial) { @@ -150,7 +161,9 @@ void wp_fractional_scale_handle_scale(void* user_data, TRACE("hwnd=%p scale=%lf\n", hwnd, scale); if (!(data = wayland_win_data_get(hwnd))) return; - if (!(surface = data->wayland_surface) || scale == surface->window.scale) + /* A protocol-illegal scale of 0 would make scale 0.0 and divide-by-zero in the + * window<->surface coordinate mapping; ignore it. */ + if (!(surface = data->wayland_surface) || scale <= 0.0 || scale == surface->window.scale) { wayland_win_data_release(data); return; @@ -197,6 +210,10 @@ struct wayland_surface *wayland_surface_create(HWND hwnd) TRACE("surface=%p\n", surface); + { + static LONG serial_counter; + surface->serial = (unsigned int)InterlockedIncrement(&serial_counter); + } surface->hwnd = hwnd; surface->wl_surface = wl_compositor_create_surface(process_wayland.wl_compositor); if (!surface->wl_surface) @@ -243,6 +260,8 @@ err: */ void wayland_surface_destroy(struct wayland_surface *surface) { + int i; + pthread_mutex_lock(&process_wayland.pointer.mutex); if (process_wayland.pointer.focused_hwnd == surface->hwnd) { @@ -295,6 +314,15 @@ void wayland_surface_destroy(struct wayland_surface *surface) surface->small_icon_buffer = NULL; } + for (i = 0; i < WAYLAND_DMABUF_CACHE_SIZE; i++) + { + if (surface->dmabuf_buffers[i].wl_buffer) + { + wl_buffer_destroy(surface->dmabuf_buffers[i].wl_buffer); + surface->dmabuf_buffers[i].wl_buffer = NULL; + } + } + wl_display_flush(process_wayland.wl_display); free(surface); @@ -369,6 +397,26 @@ err: ERR("Failed to assign toplevel role to wayland surface\n"); } +/* A shared, immutable 1x1 fully transparent buffer used to map a popup frame + * subsurface that only hosts a child render-widget and has no content of its + * own (CEF hover/click menu frames). Per the Wayland spec a subsurface with no + * buffer is unmapped, which also unmaps its child subsurfaces — so the menu + * content would never be shown. Any real content (e.g. the frame's GDI window + * surface) simply replaces this. */ +static struct wl_buffer *wayland_transparent_buffer(void) +{ + static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; + static struct wayland_shm_buffer *buf; + + /* shared immutable buffer; guard the one-time creation against concurrent + * callers so we don't create (and leak) two */ + pthread_mutex_lock(&mutex); + if (!buf && (buf = wayland_shm_buffer_create(1, 1, WL_SHM_FORMAT_ARGB8888))) + *(uint32_t *)buf->map_data = 0x00000000; + pthread_mutex_unlock(&mutex); + return buf ? buf->wl_buffer : NULL; +} + /********************************************************************** * wayland_surface_make_subsurface * @@ -378,7 +426,13 @@ void wayland_surface_make_subsurface(struct wayland_surface *surface, struct wayland_surface *owner) { assert(!surface->role || surface->role == WAYLAND_SURFACE_ROLE_SUBSURFACE); - if (surface->wl_subsurface && surface->owner_hwnd == owner->hwnd) return; + /* Already a subsurface of this owner's *current* surface: nothing to do. + * If the owner's serial differs the owner surface was recreated and this + * subsurface is orphaned, so fall through to recreate it against the live + * owner surface. */ + if (surface->wl_subsurface && surface->owner_hwnd == owner->hwnd && + surface->parent_serial == owner->serial) + return; wayland_surface_clear_role(surface); surface->role = WAYLAND_SURFACE_ROLE_SUBSURFACE; @@ -399,10 +453,27 @@ void wayland_surface_make_subsurface(struct wayland_surface *surface, surface->role = WAYLAND_SURFACE_ROLE_SUBSURFACE; surface->owner_hwnd = owner->hwnd; + surface->parent_serial = owner->serial; /* Present contents independently of the owner surface. */ wl_subsurface_set_desync(surface->wl_subsurface); + /* A visible popup frame that hosts a child render-widget (CEF menus) gets no + * buffer of its own and would stay unmapped, hiding its child content. Map + * it with a transparent buffer; real content replaces it. Restricted to + * popups so ordinary GL/VK/DMABUF content subsurfaces are untouched. */ + if (surface->hwnd && + (NtUserGetWindowLongW(surface->hwnd, GWL_STYLE) & WS_POPUP) && + NtUserGetWindowRelative(surface->hwnd, GW_CHILD)) + { + struct wl_buffer *tb = wayland_transparent_buffer(); + if (tb) + { + wl_surface_attach(surface->wl_surface, tb, 0, 0); + wl_surface_commit(surface->wl_surface); + } + } + wl_display_flush(process_wayland.wl_display); return; @@ -467,6 +538,9 @@ void wayland_surface_clear_role(struct wayland_surface *surface) } surface->owner_hwnd = NULL; + surface->parent_serial = 0; + surface->subsurface_placed = FALSE; + surface->last_restack_serial = 0; break; } @@ -590,21 +664,32 @@ BOOL wayland_surface_config_is_compatible(struct wayland_surface_config *conf, R static void wayland_surface_get_rect_in_monitor(struct wayland_surface *surface, RECT *rect) { - HMONITOR hmonitor; - MONITORINFO mi; - - mi.cbSize = sizeof(mi); - if (!(hmonitor = NtUserMonitorFromRect(&surface->window.rect, 0)) || - !NtUserGetMonitorInfo(hmonitor, (MONITORINFO *)&mi)) + /* Use the monitor rectangle cached at config time (wayland_win_data_get_config) + * instead of querying win32u here: this runs from the window-surface flush path + * while the surface lock is held, so NtUserMonitorFromRect/NtUserGetMonitorInfo + * must be kept off it (they would take user_lock in the opposite order from + * apply_window_pos -> window_surface_set_shape). */ + if (IsRectEmpty(&surface->window.monitor_rect)) { SetRectEmpty(rect); return; } - intersect_rect(rect, &mi.rcMonitor, &surface->window.rect); + intersect_rect(rect, &surface->window.monitor_rect, &surface->window.rect); OffsetRect(rect, -surface->window.rect.left, -surface->window.rect.top); } +/********************************************************************** + * wayland_surface_current_size + * + * Returns the size of the area the compositor gave us (current.rect). + */ +static void wayland_surface_current_size(struct wayland_surface *surface, int *width, int *height) +{ + *width = surface->current.rect.right - surface->current.rect.left; + *height = surface->current.rect.bottom - surface->current.rect.top; +} + /********************************************************************** * wayland_surface_reconfigure_geometry * @@ -612,12 +697,16 @@ static void wayland_surface_get_rect_in_monitor(struct wayland_surface *surface, */ static void wayland_surface_reconfigure_geometry(struct wayland_surface *surface, RECT rect) { + int curr_width, curr_height; + + wayland_surface_current_size(surface, &curr_width, &curr_height); + /* If the window size is bigger than the current state accepts, use the * largest visible (from Windows' perspective) subregion of the window. */ if ((surface->current.state & (WAYLAND_SURFACE_CONFIG_STATE_MAXIMIZED | WAYLAND_SURFACE_CONFIG_STATE_FULLSCREEN)) && - (rect.right - rect.left > surface->current.rect.right - surface->current.rect.left || - rect.bottom - rect.top > surface->current.rect.bottom - surface->current.rect.top)) + (rect.right - rect.left > curr_width || + rect.bottom - rect.top > curr_height)) { wayland_surface_get_rect_in_monitor(surface, &rect); @@ -627,16 +716,15 @@ static void wayland_surface_reconfigure_geometry(struct wayland_surface *surface * fall back to an appropriately sized rect at the top-left. */ if ((surface->current.state & WAYLAND_SURFACE_CONFIG_STATE_MAXIMIZED) && !(surface->current.state & WAYLAND_SURFACE_CONFIG_STATE_FULLSCREEN) && - (rect.right - rect.left < surface->current.rect.right - surface->current.rect.left || - rect.bottom - rect.top < surface->current.rect.bottom - surface->current.rect.top)) + (rect.right - rect.left < curr_width || + rect.bottom - rect.top < curr_height)) { - SetRect(&rect, 0, 0, surface->current.rect.right - surface->current.rect.left, - surface->current.rect.bottom - surface->current.rect.top); + SetRect(&rect, 0, 0, curr_width, curr_height); } else { - rect.right = min(rect.right, rect.left + surface->current.rect.right - surface->current.rect.left); - rect.bottom = min(rect.bottom, rect.top + surface->current.rect.bottom - surface->current.rect.top); + rect.right = min(rect.right, rect.left + curr_width); + rect.bottom = min(rect.bottom, rect.top + curr_height); } TRACE("Window is too large for Wayland state, using subregion\n"); } @@ -653,6 +741,14 @@ static void wayland_surface_reconfigure_geometry(struct wayland_surface *surface xdg_surface_set_window_geometry(surface->xdg_surface, rect.left, rect.top, width, height); + /* Advertise a fixed size to the compositor only for non-resizable + * windows. surface->window.resizeable is computed at config time + * (wayland_win_data_get_config) and already accounts for WS_THICKFRAME, + * so a resizable window filling the monitor is not wrongly locked. Do NOT + * query the window style here: this runs from the window-surface flush + * path while the surface lock is held, and a style query (NtUserGetWindowLongW) + * would take user_lock in the opposite order from apply_window_pos -> + * window_surface_set_shape, deadlocking. */ if (surface->window.resizeable) { xdg_toplevel_set_min_size(surface->xdg_toplevel, 0, 0); @@ -666,6 +762,21 @@ static void wayland_surface_reconfigure_geometry(struct wayland_surface *surface } } +/********************************************************************** + * wayland_surface_is_fullscreen_fill + * + * Whether a fullscreen surface should be forced to cover the whole area the + * compositor gave us (current.width/height). This stretches a game using a + * non-native exclusive-fullscreen resolution to fill the screen, instead of + * letting its smaller surface sit in a corner with the rest left black. + */ +static BOOL wayland_surface_is_fullscreen_fill(struct wayland_surface *surface) +{ + return (surface->window.state & WAYLAND_SURFACE_CONFIG_STATE_FULLSCREEN) && + surface->current.serial && + !IsRectEmpty(&surface->current.rect); +} + /********************************************************************** * wayland_surface_reconfigure_size * @@ -701,6 +812,18 @@ static void wayland_surface_reconfigure_client(struct wayland_surface *surface, TRACE("hwnd=%p rect=%s\n", surface->hwnd, wine_dbgstr_rect(&rect)); + /* When the toplevel is fullscreen, cover the whole area the compositor gave + * us, at the origin, regardless of the app's (possibly non-native) client + * size. The app buffer is scaled up by the viewport, so a game using a + * non-native exclusive-fullscreen resolution fills the screen instead of + * sitting in a corner with the rest left black. */ + if (wayland_surface_is_fullscreen_fill(surface)) + { + int curr_width, curr_height; + wayland_surface_current_size(surface, &curr_width, &curr_height); + SetRect(&rect, 0, 0, curr_width, curr_height); + } + if (client->wl_subsurface) { wl_subsurface_set_position(client->wl_subsurface, rect.left, rect.top); @@ -743,6 +866,25 @@ static BOOL wayland_surface_reconfigure_xdg(struct wayland_surface *surface, REC memset(&surface->requested, 0, sizeof(surface->requested)); xdg_surface_ack_configure(surface->xdg_surface, surface->current.serial); } + /* Acknowledge a fullscreen/maximized configure straight from the requested + * state, even when it isn't the initial one and the win32-side handling + * (WM_WAYLAND_CONFIGURE -> wayland_configure_window, which would move it to + * processing) hasn't run yet. A Vulkan/GL app commits frames from its own + * thread; while its message loop is busy (e.g. loading) the configure would + * otherwise sit unacked, the compositor would keep resending it and never + * finalize the state, leaving a screen-sized window floating off-screen. These + * states have a fixed size we conform to via the geometry set below, and + * config_is_compatible has confirmed the buffer fits, so acking now is safe. */ + else if (surface->requested.serial && + (surface->requested.state & (WAYLAND_SURFACE_CONFIG_STATE_FULLSCREEN | + WAYLAND_SURFACE_CONFIG_STATE_MAXIMIZED)) && + wayland_surface_config_is_compatible(&surface->requested, rect, + window->state)) + { + surface->current = surface->requested; + memset(&surface->requested, 0, sizeof(surface->requested)); + xdg_surface_ack_configure(surface->xdg_surface, surface->current.serial); + } else if (!surface->current.serial || !wayland_surface_config_is_compatible(&surface->current, rect, window->state)) @@ -755,34 +897,202 @@ static BOOL wayland_surface_reconfigure_xdg(struct wayland_surface *surface, REC return TRUE; } +/********************************************************************** + * wayland_unmap_invisible_subtree + * + * Drop the stale frame of every subsurface under hwnd that has become + * (hierarchically) invisible. Win32 visibility is inherited: hiding a window + * (e.g. switching the active CEF webview) makes its descendants invisible + * without delivering WindowPosChanged to each of them, so they would otherwise + * keep displaying a stale frame on top of the now-visible content. Called from + * the visibility-change driver entry points (WindowPosChanged with + * SWP_HIDEWINDOW, SetWindowStyle clearing WS_VISIBLE) instead of polling. + * Remapping happens on the next present, which clears content_hidden and + * attaches a fresh buffer. win_data_mutex must be held by the caller. + */ +void wayland_unmap_invisible_subtree(HWND hwnd) +{ + HWND *list = NULL; + ULONG count = 256; + int i; + + if ((list = malloc(count * sizeof(*list))) && + NtUserBuildHwndList(0, hwnd, TRUE, TRUE, 0, count, list, &count)) + { + free(list); + list = malloc(count * sizeof(*list)); + if (list && NtUserBuildHwndList(0, hwnd, TRUE, TRUE, 0, count, list, &count)) + { free(list); list = NULL; } + } + if (!list) return; + + for (i = 0; list[i] && list[i] != HWND_BOTTOM; i++) + { + struct wayland_win_data *d = wayland_win_data_get_nolock(list[i]); + struct wayland_surface *s = d ? d->wayland_surface : NULL; + + if (!s || s->role != WAYLAND_SURFACE_ROLE_SUBSURFACE || !s->wl_subsurface || + s->content_hidden || NtUserIsWindowVisible(list[i])) + continue; + + wl_surface_attach(s->wl_surface, NULL, 0, 0); + wl_surface_commit(s->wl_surface); + s->content_hidden = TRUE; + } + free(list); +} + +/********************************************************************** + * wayland_subsurface_restack + * + * Stack a subsurface among its siblings to match the Win32 Z-order of the + * corresponding windows. CEF/Chromium presents several GPU sub-views as + * separate subsurfaces of the same toplevel (e.g. a full-window frame plus + * the active webview); without honoring the Z-order an opaque sibling would + * cover the content rendered beneath it. + */ +static void wayland_subsurface_restack(struct wayland_surface *surface, + struct wayland_win_data *toplevel_data, + struct wayland_surface *toplevel_surface) +{ + struct wayland_surface *above = NULL; + HWND *list = NULL; + ULONG count = 128; + int i; + + /* Build the toplevel's descendants in Z-order (index 0 == topmost). On a + * too-small buffer NtUserBuildHwndList updates count to the needed size, so + * a second attempt with that size succeeds. */ + if ((list = malloc(count * sizeof(*list))) && + NtUserBuildHwndList(0, surface->owner_hwnd, TRUE, TRUE, 0, count, list, &count)) + { + free(list); + if ((list = malloc(count * sizeof(*list))) && + NtUserBuildHwndList(0, surface->owner_hwnd, TRUE, TRUE, 0, count, list, &count)) + { + free(list); + list = NULL; + } + } + + if (list) + { + /* The list is in Z-order (index 0 == topmost), so siblings seen before + * we reach self are above us; the last such valid sibling is the nearest + * one above us, and sets our stacking position. Only a true Wayland + * sibling (same live parent surface) is a legal place_below reference. */ + for (i = 0; list[i] && list[i] != HWND_BOTTOM; i++) + { + struct wayland_win_data *d; + struct wayland_surface *s; + + if (list[i] == surface->hwnd) break; /* reached self; `above` is now the nearest one */ + + d = wayland_win_data_get_nolock(list[i]); + s = d ? d->wayland_surface : NULL; + if (!s || s->role != WAYLAND_SURFACE_ROLE_SUBSURFACE || !s->wl_subsurface || + s->owner_hwnd != surface->owner_hwnd) + continue; + + if (s->parent_serial == surface->parent_serial && s->parent_serial != 0) + above = s; + } + + free(list); + } + + if (above) + { + /* place directly below the nearest higher sibling */ + wl_subsurface_place_below(surface->wl_subsurface, above->wl_surface); + } + else if (toplevel_data->client_surface && toplevel_data->client_surface->wl_subsurface) + { + /* topmost subsurface: above the toplevel's own client (GL/VK) surface */ + wl_subsurface_place_above(surface->wl_subsurface, toplevel_data->client_surface->wl_surface); + } + else + { + /* topmost subsurface: above the toplevel's main surface */ + wl_subsurface_place_above(surface->wl_subsurface, toplevel_surface->wl_surface); + } +} + /********************************************************************** * wayland_surface_reconfigure_subsurface * * Reconfigures the subsurface as needed to match the latest requested * state. */ -static void wayland_surface_reconfigure_subsurface(struct wayland_surface *surface) +static void wayland_surface_reconfigure_subsurface(struct wayland_surface *surface, BOOL from_flush) { + struct wayland_win_data *data; struct wayland_win_data *owner_data; struct wayland_surface *owner_surface; - if (surface->processing.serial && surface->processing.processed && + if ((data = wayland_win_data_get_nolock(surface->hwnd)) && (owner_data = wayland_win_data_get_nolock(surface->owner_hwnd)) && (owner_surface = owner_data->wayland_surface)) { - RECT rect = surface->window.rect; - - OffsetRect(&rect, -owner_surface->window.rect.left, -owner_surface->window.rect.top); - rect = map_rect_to_surface(surface, rect); - - TRACE("hwnd=%p rect=%s\n", surface->hwnd, wine_dbgstr_rect(&rect)); - - wl_subsurface_set_position(surface->wl_subsurface, rect.left, rect.top); - if (owner_data->client_surface && owner_data->client_surface->wl_subsurface) - wl_subsurface_place_above(surface->wl_subsurface, owner_data->client_surface->wl_surface); - else - wl_subsurface_place_above(surface->wl_subsurface, owner_surface->wl_surface); - wl_surface_commit(owner_surface->wl_surface); + /* If the owner surface was destroyed/recreated since this subsurface was + * created, the subsurface is orphaned: any wl_subsurface request + * (set_position/place_*) would raise a fatal "bad_surface" error. Skip; + * the window will be reconfigured with a fresh subsurface on the next + * create_wayland_surface. */ + if (surface->parent_serial != owner_surface->serial) return; + + /* Computing the subsurface position and z-order calls into win32u + * (NtUserGetWindowLongW/GetAncestor/MapWindowPoints/GetWinMonitorDpi, and + * NtUserBuildHwndList in the restack), which all take win32u's user_lock. + * The window-surface flush path holds the window_surface lock when it gets + * here, so taking user_lock would invert against apply_window_pos -> + * window_surface_set_shape and deadlock. The placement is kept current by + * the win_data_mutex-only paths (geometry changes via WindowPosChanged -> + * update_wayland_state, and every DMABUF present), so when flushing we + * keep the existing position/stacking and skip the win32u recompute. */ + if (!from_flush) + { + int local_x, local_y; + HWND parent = NULL; + POINT pt; + LONG serial = InterlockedCompareExchange(&subsurface_restack_serial, 0, 0); + BOOL committed = FALSE; + + if (NtUserGetWindowLongW(surface->hwnd, GWL_STYLE) & WS_CHILD) + parent = NtUserGetAncestor(surface->hwnd, GA_PARENT); + + pt.x = data->rects.window.left; + pt.y = data->rects.window.top; + NtUserMapWindowPoints(parent, surface->owner_hwnd, &pt, 1, + NtUserGetWinMonitorDpi(surface->hwnd, MDT_RAW_DPI)); + local_x = pt.x + (owner_data->rects.client.left - owner_data->rects.window.left); + local_y = pt.y + (owner_data->rects.client.top - owner_data->rects.window.top); + pt = map_point_to_surface(surface, (POINT){local_x, local_y}); + + /* Re-place/re-stack only when something actually changed: the window + * moved (position), or some window's z-order/visibility changed since + * the last present (restack serial, bumped from the window event entry + * points). A steady stream of frames at the same geometry then avoids + * the NtUserBuildHwndList walk in wayland_subsurface_restack and the + * extra owner-surface commit. Hiding of invisible windows is handled + * separately and event-driven by wayland_unmap_invisible_subtree. */ + if (!surface->subsurface_placed || + pt.x != surface->last_sub_x || pt.y != surface->last_sub_y) + { + wl_subsurface_set_position(surface->wl_subsurface, pt.x, pt.y); + surface->last_sub_x = pt.x; + surface->last_sub_y = pt.y; + committed = TRUE; + } + if (!surface->subsurface_placed || serial != surface->last_restack_serial) + { + wayland_subsurface_restack(surface, owner_data, owner_surface); + surface->last_restack_serial = serial; + committed = TRUE; + } + if (committed) wl_surface_commit(owner_surface->wl_surface); + surface->subsurface_placed = TRUE; + } memset(&surface->processing, 0, sizeof(surface->processing)); } @@ -794,11 +1104,22 @@ static void wayland_surface_reconfigure_subsurface(struct wayland_surface *surfa * Reconfigures the wayland surface as needed to match the latest requested * state. */ -BOOL wayland_surface_reconfigure(struct wayland_surface *surface) +BOOL wayland_surface_reconfigure(struct wayland_surface *surface, BOOL from_flush) { struct wayland_window_config *window = &surface->window; RECT rect = map_rect_to_surface(surface, surface->window.rect); + /* In fullscreen, force the surface to the exact size the compositor gave us + * so a non-native (smaller) app resolution is stretched to fill the screen + * (xdg geometry + viewport destination) instead of sitting in a corner with + * black around it. */ + if (wayland_surface_is_fullscreen_fill(surface)) + { + int curr_width, curr_height; + wayland_surface_current_size(surface, &curr_width, &curr_height); + SetRect(&rect, 0, 0, curr_width, curr_height); + } + TRACE("hwnd=%p window=%s,%#x processing=%s,%#x current=%s,%#x\n", surface->hwnd, wine_dbgstr_rect(&rect), window->state, wine_dbgstr_rect(&surface->processing.rect), surface->processing.state, @@ -814,7 +1135,7 @@ BOOL wayland_surface_reconfigure(struct wayland_surface *surface) break; case WAYLAND_SURFACE_ROLE_SUBSURFACE: if (!surface->wl_subsurface) break; /* surface role has been cleared */ - wayland_surface_reconfigure_subsurface(surface); + wayland_surface_reconfigure_subsurface(surface, from_flush); break; } @@ -983,6 +1304,44 @@ static void copy_rectangle_into_center_of_square(const unsigned int *src, memcpy(dest, src, src_w * 4); } +/* Read a color bitmap of any source format into a 32-bit ARGB buffer by + * blitting it through a 32bpp DIB section. Fallback for formats that + * NtGdiGetDIBitsInternal cannot convert directly (e.g. 16bpp cursors). */ +static BOOL blit_bitmap_to_argb(HBITMAP color, int width, int height, unsigned int *dst) +{ + char buffer[FIELD_OFFSET(BITMAPINFO, bmiColors[256])]; + BITMAPINFO *bi = (BITMAPINFO *)buffer; + HBITMAP dib; + HDC src_dc, dst_dc; + void *dib_bits = NULL; + BOOL ret = FALSE; + + memset(bi, 0, sizeof(*bi)); + bi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bi->bmiHeader.biWidth = width; + bi->bmiHeader.biHeight = -height; /* top-down */ + bi->bmiHeader.biPlanes = 1; + bi->bmiHeader.biBitCount = 32; + bi->bmiHeader.biCompression = BI_RGB; + + if (!(dib = NtGdiCreateDIBSection(NULL, NULL, 0, bi, DIB_RGB_COLORS, 0, 0, 0, &dib_bits))) + return FALSE; + + src_dc = NtGdiCreateCompatibleDC(0); + dst_dc = NtGdiCreateCompatibleDC(0); + if (src_dc && dst_dc && + NtGdiSelectBitmap(src_dc, color) && NtGdiSelectBitmap(dst_dc, dib) && + NtGdiBitBlt(dst_dc, 0, 0, width, height, src_dc, 0, 0, SRCCOPY, 0, 0)) + { + memcpy(dst, dib_bits, (SIZE_T)width * height * 4); + ret = TRUE; + } + if (src_dc) NtGdiDeleteObjectApp(src_dc); + if (dst_dc) NtGdiDeleteObjectApp(dst_dc); + NtGdiDeleteObjectApp(dib); + return ret; +} + /*********************************************************************** * wayland_shm_buffer_from_color_bitmaps * @@ -1037,7 +1396,13 @@ struct wayland_shm_buffer *wayland_shm_buffer_from_color_bitmaps(HDC hdc, HBITMA if (!NtGdiGetDIBitsInternal(hdc, color, 0, bm.bmHeight, bits, info, DIB_RGB_COLORS, 0, 0)) - goto failed; + { + /* NtGdiGetDIBitsInternal can fail to convert some device-dependent + * bitmap formats (e.g. 16bpp cursor bitmaps used by Source 2 games) + * directly to 32bpp. Fall back to a GDI blit through a 32bpp DIB. */ + if (!blit_bitmap_to_argb(color, bm.bmWidth, bm.bmHeight, bits)) + goto failed; + } for (i = 0; i < bm.bmWidth * bm.bmHeight; i++) if ((has_alpha = (bits[i] & 0xff000000) != 0)) break; @@ -1358,7 +1723,7 @@ void wayland_surface_ensure_contents(struct wayland_surface *surface) if (!(damage = NtGdiCreateRectRgn(0, 0, width, height))) WARN("Failed to create damage region for dummy buffer\n"); - if (wayland_surface_reconfigure(surface)) + if (wayland_surface_reconfigure(surface, FALSE)) { wayland_surface_attach_shm(surface, dummy_shm_buffer, damage); wl_surface_commit(surface->wl_surface); diff --git a/dlls/winewayland.drv/wayland_text_input.c b/dlls/winewayland.drv/wayland_text_input.c index 980f86728c8..d855c75c836 100644 --- a/dlls/winewayland.drv/wayland_text_input.c +++ b/dlls/winewayland.drv/wayland_text_input.c @@ -77,10 +77,42 @@ static void wayland_text_input_reset_all_state(struct wayland_text_input *text_i wayland_text_input_reset_pending_state(text_input); } +/* Enable / disable the compositor text-input (zwp_text_input_v3) for the focused + * window. text-input is the only keyboard path under Wayland, and enabling it + * lets the input method (KWin's built-in dead-key/accent composition, fcitx5, + * ibus, ...) grab and reinterpret keystrokes -- correct for editable fields, but + * wrong for 3D games and other windows that want raw keyboard (held keys would + * open the accent picker and the window would receive nothing). We therefore + * only enable it while the application keeps an input context associated with the + * window tree (driven by ImmAssociateContext -> WAYLAND_SetIMEEnabled, aggregated + * per toplevel via num_ime_children). Both helpers must run under text_input->mutex + * and are idempotent via text_input->enabled. */ +static void wayland_text_input_ime_enable(struct wayland_text_input *text_input) +{ + if (text_input->enabled) return; + zwp_text_input_v3_enable(text_input->zwp_text_input_v3); + zwp_text_input_v3_set_content_type(text_input->zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL); + zwp_text_input_v3_set_cursor_rectangle(text_input->zwp_text_input_v3, 0, 0, 0, 0); + zwp_text_input_v3_commit(text_input->zwp_text_input_v3); + text_input->enabled = TRUE; +} + +static void wayland_text_input_ime_disable(struct wayland_text_input *text_input) +{ + if (!text_input->enabled) return; + zwp_text_input_v3_disable(text_input->zwp_text_input_v3); + zwp_text_input_v3_commit(text_input->zwp_text_input_v3); + wayland_text_input_reset_all_state(text_input); + text_input->enabled = FALSE; +} + static void text_input_enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, struct wl_surface *surface) { struct wayland_text_input *text_input = data; + struct wayland_win_data *win; HWND hwnd; if (!surface) return; @@ -88,16 +120,19 @@ static void text_input_enter(void *data, struct zwp_text_input_v3 *zwp_text_inpu hwnd = wl_surface_get_user_data(surface); TRACE("data %p, text_input %p, hwnd %p.\n", data, zwp_text_input_v3, hwnd); + /* Take win_data_mutex (OUTER) then text_input->mutex (INNER), the driver's + * lock order. Enable IME only if some window in this toplevel's tree wants it + * (num_ime_children), kept current by WAYLAND_SetIMEEnabled. */ + if (!(win = wayland_win_data_get(hwnd))) return; + pthread_mutex_lock(&text_input->mutex); text_input->focused_hwnd = hwnd; - zwp_text_input_v3_enable(text_input->zwp_text_input_v3); - zwp_text_input_v3_set_content_type(text_input->zwp_text_input_v3, - ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL); - zwp_text_input_v3_set_cursor_rectangle(text_input->zwp_text_input_v3, 0, 0, 0, 0); - zwp_text_input_v3_commit(text_input->zwp_text_input_v3); + if (win->num_ime_children) + wayland_text_input_ime_enable(text_input); pthread_mutex_unlock(&text_input->mutex); + wayland_win_data_release(win); + activate_keyboard_hkl(hwnd, TRUE); } @@ -108,14 +143,12 @@ static void text_input_leave(void *data, struct zwp_text_input_v3 *zwp_text_inpu TRACE("data %p, text_input %p.\n", data, zwp_text_input_v3); pthread_mutex_lock(&text_input->mutex); - zwp_text_input_v3_disable(text_input->zwp_text_input_v3); - zwp_text_input_v3_commit(text_input->zwp_text_input_v3); + wayland_text_input_ime_disable(text_input); if (text_input->focused_hwnd) { post_ime_update(text_input->focused_hwnd, 0, NULL, NULL); text_input->focused_hwnd = NULL; } - wayland_text_input_reset_all_state(text_input); pthread_mutex_unlock(&text_input->mutex); } @@ -170,7 +203,7 @@ static void text_input_done(void *data, struct zwp_text_input_v3 *zwp_text_input * the focus state of the text input. This behavior is arguably out of spec, * but otherwise harmless, so just ignore the new state in such cases. * Additionally ignore done events that don't actually modify the state. */ - if (text_input->focused_hwnd && + if (text_input->focused_hwnd && text_input->enabled && (text_input->commit_string || text_input->preedit.cursor_pos != text_input->current_preedit.cursor_pos || !!text_input->preedit.string != !!text_input->current_preedit.string || @@ -205,6 +238,9 @@ void wayland_text_input_init(void) text_input->zwp_text_input_v3 = zwp_text_input_manager_v3_get_text_input( process_wayland.zwp_text_input_manager_v3, process_wayland.seat.wl_seat); zwp_text_input_v3_add_listener(text_input->zwp_text_input_v3, &text_input_listener, text_input); + /* A freshly created zwp_text_input_v3 is disabled per protocol; keep the flag + * in sync (also covers a seat replug where the struct persists). */ + text_input->enabled = FALSE; pthread_mutex_unlock(&text_input->mutex); }; @@ -216,6 +252,10 @@ void wayland_text_input_deinit(void) zwp_text_input_v3_destroy(text_input->zwp_text_input_v3); text_input->zwp_text_input_v3 = NULL; text_input->focused_hwnd = NULL; + /* Keep the invariant that enabled is only TRUE while a live object exists, so + * a re-created object (seat replug) isn't left thinking it is already enabled + * and skipping zwp_text_input_v3_enable on the next focus. */ + text_input->enabled = FALSE; wayland_text_input_reset_all_state(text_input); pthread_mutex_unlock(&text_input->mutex); }; @@ -232,27 +272,35 @@ BOOL WAYLAND_SetIMECompositionRect(HWND hwnd, RECT rect) TRACE("hwnd %p, rect %s.\n", hwnd, wine_dbgstr_rect(&rect)); - pthread_mutex_lock(&text_input->mutex); - - if (!text_input->zwp_text_input_v3 || hwnd != text_input->focused_hwnd) - goto err; - + /* Map the rect to surface coordinates under win_data_mutex, then release it + * before touching text_input state. The driver's lock order is win_data_mutex + * (outer) -> the per-subsystem mutexes (inner): wayland_surface_destroy() + * clears text_input.focused_hwnd while a caller holds win_data_mutex. Taking + * text_input->mutex first and then wayland_win_data_get() (as upstream still + * does) inverts that order and can deadlock; acquiring the two locks + * sequentially (never nested) sidesteps it entirely. */ if (!(data = wayland_win_data_get(hwnd))) - goto err; + return FALSE; - if (!(surface = data->wayland_surface)) + if (!(surface = data->wayland_surface) || !data->num_ime_children) { wayland_win_data_release(data); - goto err; + return FALSE; } - OffsetRect(&rect, -surface->window.rect.left, -surface->window.rect.top); surface_rect = map_rect_to_surface(surface, rect); wayland_win_data_release(data); + pthread_mutex_lock(&text_input->mutex); + + if (!text_input->zwp_text_input_v3 || !text_input->enabled || hwnd != text_input->focused_hwnd) + goto err; + + /* set_cursor_rectangle takes (x, y, width, height): y is surface_rect.top, + * not .right (the latter was a typo introduced by the RECT/POINT refactor). */ zwp_text_input_v3_set_cursor_rectangle(text_input->zwp_text_input_v3, - surface_rect.left, surface_rect.right, surface_rect.right - surface_rect.left, + surface_rect.left, surface_rect.top, surface_rect.right - surface_rect.left, surface_rect.bottom - surface_rect.top); zwp_text_input_v3_commit(text_input->zwp_text_input_v3); @@ -263,3 +311,59 @@ err: pthread_mutex_unlock(&text_input->mutex); return FALSE; } + +/*********************************************************************** + * SetIMEEnabled (WAYLANDDRV.@) + * + * Live IME enable/disable. win32u calls this from NtUserAssociateInputContext for + * every window whose input context is associated (enabled=TRUE) or cleared via + * ImmDisableIME/ImmAssociateContext(NULL) (enabled=FALSE) -- including child + * controls, so the count is aggregated on the GA_ROOT toplevel and IME stays on + * while any window in the tree wants it. Without this the compositor text-input + * state would only be (re)evaluated on a Wayland focus change, so an app toggling + * IME while staying focused (e.g. a game between its chat box and gameplay) would + * keep the stale state -- text input grabbing the keys meant for gameplay until a + * refocus. + */ +BOOL WAYLAND_SetIMEEnabled(HWND hwnd, BOOL enabled) +{ + struct wayland_text_input *text_input = &process_wayland.text_input; + struct wayland_win_data *hwnd_data, *toplevel_data; + HWND toplevel = NtUserGetAncestor(hwnd, GA_ROOT); + int num_ime_children = 0; + BOOL tracked = FALSE; + + TRACE("hwnd %p, toplevel %p, enabled %u.\n", hwnd, toplevel, enabled); + + /* Phase 1: maintain the per-toplevel IME-children count under win_data_mutex + * (the driver's OUTER lock). text_input->mutex (INNER) must NOT be held here: + * taking win_data_mutex under text_input->mutex would invert the order + * wayland_surface_destroy relies on and can deadlock (cf. SetIMECompositionRect). */ + if ((hwnd_data = wayland_win_data_get(hwnd))) + { + if ((toplevel_data = wayland_win_data_get_nolock(toplevel))) + { + if (!hwnd_data->ime_enabled && enabled) toplevel_data->num_ime_children++; + else if (hwnd_data->ime_enabled && !enabled && toplevel_data->num_ime_children > 0) + toplevel_data->num_ime_children--; + hwnd_data->ime_enabled = enabled; + num_ime_children = toplevel_data->num_ime_children; + tracked = TRUE; + } + wayland_win_data_release(hwnd_data); + } + if (!tracked) return FALSE; + + /* Phase 2: if that toplevel currently holds text-input focus, apply the + * aggregated state. Only text_input->mutex here; no win32u/win_data under it. */ + pthread_mutex_lock(&text_input->mutex); + if (text_input->zwp_text_input_v3 && text_input->focused_hwnd == toplevel) + { + if (num_ime_children) + wayland_text_input_ime_enable(text_input); + else + wayland_text_input_ime_disable(text_input); + } + pthread_mutex_unlock(&text_input->mutex); + return TRUE; +} diff --git a/dlls/winewayland.drv/waylanddrv.h b/dlls/winewayland.drv/waylanddrv.h index e0df8e56bb5..ba4db123e77 100644 --- a/dlls/winewayland.drv/waylanddrv.h +++ b/dlls/winewayland.drv/waylanddrv.h @@ -41,6 +41,7 @@ #include "pointer-warp-v1-client-protocol.h" #include "alpha-modifier-v1-client-protocol.h" #include "fractional-scale-v1-client-protocol.h" +#include "linux-dmabuf-unstable-v1-client-protocol.h" #include "windef.h" #include "winbase.h" @@ -70,6 +71,7 @@ enum wayland_window_message WM_WAYLAND_INIT_DISPLAY_DEVICES = WM_WINE_FIRST_DRIVER_MSG, WM_WAYLAND_CONFIGURE, WM_WAYLAND_SET_FOREGROUND, + WM_WAYLAND_FLUSH_REMOTE_SURFACE_DMABUF, }; enum wayland_surface_config_state @@ -132,6 +134,7 @@ struct wayland_text_input } preedit, current_preedit; WCHAR *commit_string; HWND focused_hwnd; + BOOL enabled; pthread_mutex_t mutex; }; @@ -184,6 +187,7 @@ struct wayland struct wp_cursor_shape_manager_v1 *wp_cursor_shape_manager_v1; struct wp_pointer_warp_v1 *wp_pointer_warp_v1; struct wp_alpha_modifier_v1 *wp_alpha_modifier_v1; + struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1; struct wayland_seat seat; struct wayland_keyboard keyboard; struct wayland_pointer pointer; @@ -211,6 +215,26 @@ struct wayland_output_state char *name; int logical_x, logical_y; int logical_w, logical_h; + /* Physical-pixel position assigned by the display layout (see display.c). + * Lets the rest of the driver map a window (in physical coords) to the + * output it sits on. */ + int phys_x, phys_y; + /* Ratio of the physical mode size to the logical (post-scaling) size, i.e. + * the real compositor output scale (1.0, 2.0, 1.5, ...). Valid only when + * has_logical_size is TRUE, otherwise 0.0. */ + double compositor_scale; + /* Whether logical_w/logical_h came from xdg_output (reliable). */ + BOOL has_logical_size; + /* Whether xdg_output has ever reported a logical size for this output; used + * to keep a stale defaulted logical size from masquerading as reliable. */ + BOOL has_xdg_logical_size; + /* wl_output.geometry: rotation and physical size in millimetres (0 if the + * compositor doesn't report it, e.g. virtual outputs). */ + int transform; + int physical_width, physical_height; + /* wl_output.scale event value (0 if never received). Fallback scale source + * for compositors without xdg_output (e.g. gamescope, which sends 1). */ + int32_t scale; }; struct wayland_output @@ -236,6 +260,9 @@ struct wayland_window_config { RECT rect; RECT client_rect; + /* Monitor rectangle containing the window, cached at config time so the + * flush-path reconfigure can clamp an oversized window without a win32u call. */ + RECT monitor_rect; enum wayland_surface_config_state state; /* The scale (i.e., normalized dpi) the window is rendering at. */ double scale; @@ -269,6 +296,19 @@ struct wayland_shm_buffer HRGN damage_region; }; +#define WAYLAND_DMABUF_CACHE_SIZE 8 + +struct wayland_dmabuf_buffer +{ + uint64_t ino; + uint64_t modifier; + struct wl_buffer *wl_buffer; + uint32_t width; + uint32_t height; + uint32_t format; + uint32_t last_used; +}; + struct wayland_surface { HWND hwnd; @@ -301,6 +341,28 @@ struct wayland_surface struct wayland_window_config window; int content_width, content_height; HCURSOR hcursor; + + struct wayland_dmabuf_buffer dmabuf_buffers[WAYLAND_DMABUF_CACHE_SIZE]; + uint32_t dmabuf_cache_clock; + /* TRUE when the subsurface has been unmapped because its window became + * (hierarchically) invisible; reset when real content is attached again, so + * the next present remaps it. Set by wayland_unmap_invisible_subtree(). */ + BOOL content_hidden; + /* Cached subsurface placement: skip the per-present window-list walk and + * parent commit while neither this window's position nor any window's + * z-order (wayland_subsurface_restack_serial) changed since the last present. */ + int last_sub_x, last_sub_y; + LONG last_restack_serial; + BOOL subsurface_placed; + /* Monotonic identity, never reused (unlike the wl_surface proxy address, + * which libwayland recycles after destroy). A subsurface records its + * parent's serial; if the parent window's current surface has a different + * serial the parent was destroyed/recreated and this subsurface is orphaned + * — issuing any wl_subsurface request on it would raise a fatal + * "bad_surface" protocol error. Also used to confirm true siblings before a + * place_above/below. */ + unsigned int serial; + unsigned int parent_serial; }; /********************************************************************** @@ -316,6 +378,8 @@ BOOL wayland_process_init(void); BOOL wayland_output_create(uint32_t id, uint32_t version); void wayland_output_destroy(struct wayland_output *output); void wayland_output_use_xdg_extension(struct wayland_output *output); +double wayland_output_scale_for_rect(const RECT *rect); +void wayland_update_outputs_physical_coords(void); /********************************************************************** * Wayland surface @@ -330,7 +394,13 @@ void wayland_surface_clear_role(struct wayland_surface *surface); void wayland_surface_attach_shm(struct wayland_surface *surface, struct wayland_shm_buffer *shm_buffer, HRGN surface_damage_region); -BOOL wayland_surface_reconfigure(struct wayland_surface *surface); +BOOL wayland_surface_reconfigure(struct wayland_surface *surface, BOOL from_flush); +void wayland_unmap_invisible_subtree(HWND hwnd); + +/* Mark that a window's position, z-order or visibility may have changed, so + * subsurfaces re-evaluate their stacking on the next present instead of walking + * the whole window list on every frame. */ +void wayland_invalidate_subsurface_zorder(void); BOOL wayland_surface_config_is_compatible(struct wayland_surface_config *conf, RECT rect, enum wayland_surface_config_state state); RECT map_rect_to_surface(struct wayland_surface *surface, RECT rect); @@ -360,6 +430,15 @@ struct wayland_shm_buffer *wayland_shm_buffer_from_color_bitmaps(HDC hdc, HBITMA void wayland_shm_buffer_ref(struct wayland_shm_buffer *shm_buffer); void wayland_shm_buffer_unref(struct wayland_shm_buffer *shm_buffer); +struct wayland_buffer_queue; +struct wayland_buffer_queue *wayland_buffer_queue_create(int width, int height, uint32_t format); +void wayland_buffer_queue_destroy(struct wayland_buffer_queue *queue); +struct wayland_shm_buffer *wayland_buffer_queue_get_free_buffer(struct wayland_buffer_queue *queue); +void wayland_buffer_queue_add_damage(struct wayland_buffer_queue *queue, HRGN damage); +void wayland_buffer_queue_get_size(struct wayland_buffer_queue *queue, int *width, int *height); +void copy_pixel_region(const char *src_pixels, RECT *src_rect, + char *dst_pixels, RECT *dst_rect, HRGN region, BOOL force_opaque); + /********************************************************************** * Wayland Window */ @@ -376,19 +455,26 @@ struct wayland_win_data struct wayland_surface *wayland_surface; /* wayland client surface (if any) for this window */ struct wayland_client_surface *client_surface; + /* overlay subsurface presenting pixels painted by other processes */ + struct wayland_surface *remote_overlay; + /* buffer pool for the overlay (busy tracking + per-buffer damage) */ + struct wayland_buffer_queue *remote_buffer_queue; /* window rects, relative to parent client area */ struct window_rects rects; BOOL is_fullscreen; - BOOL resizeable; BOOL managed; BOOL layered_attribs_set; + /* IME activation tracking: ime_enabled is this window's own state (set from + * the app's ImmAssociateContext), num_ime_children counts how many windows in + * this toplevel's tree currently want IME (kept on the GA_ROOT toplevel). */ + BOOL ime_enabled; + int num_ime_children; }; struct wayland_win_data *wayland_win_data_get(HWND hwnd); struct wayland_win_data *wayland_win_data_get_nolock(HWND hwnd); void wayland_win_data_release(struct wayland_win_data *data); -struct wayland_client_surface *get_client_surface(HWND hwnd); void set_client_surface(HWND hwnd, struct wayland_client_surface *client); BOOL set_window_surface_contents(HWND hwnd, struct wayland_shm_buffer *shm_buffer, HRGN damage_region); struct wayland_shm_buffer *get_window_surface_contents(HWND hwnd); @@ -411,6 +497,7 @@ void activate_keyboard_hkl(HWND hwnd, BOOL ime); void wayland_pointer_init(struct wl_pointer *wl_pointer); void wayland_pointer_deinit(void); void wayland_pointer_clear_constraint(void); +struct wayland_shm_buffer *create_mono_cursor_buffer(HBITMAP bmp); /********************************************************************** * Wayland text input @@ -454,6 +541,7 @@ BOOL WAYLAND_ClipCursor(const RECT *clip, BOOL reset); LRESULT WAYLAND_DesktopWindowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); void WAYLAND_DestroyWindow(HWND hwnd); BOOL WAYLAND_SetIMECompositionRect(HWND hwnd, RECT rect); +BOOL WAYLAND_SetIMEEnabled(HWND hwnd, BOOL enabled); void WAYLAND_SetCursor(HWND hwnd, HCURSOR hcursor); BOOL WAYLAND_SetCursorPos(INT x, INT y); void WAYLAND_SetLayeredWindowAttributes(HWND hwnd, COLORREF key, BYTE alpha, DWORD flags); @@ -468,6 +556,15 @@ void WAYLAND_WindowPosChanged(HWND hwnd, HWND insert_after, HWND owner_hint, UIN const struct window_rects *new_rects, struct window_surface *surface); BOOL WAYLAND_WindowPosChanging(HWND hwnd, UINT swp_flags, BOOL shaped, const struct window_rects *rects); struct client_surface *WAYLAND_CreateClientSurface(HWND hwnd, int pixel_format); +void WAYLAND_FlushRemoteSurface(HWND hwnd, const RECT *dirty, const void *bits, + UINT stride, UINT width, UINT height); +void WAYLAND_FlushRemoteSurfaceDMABUF(HWND hwnd, HANDLE handle, UINT64 modifier, + UINT width, UINT height, UINT stride); +BOOL WAYLAND_NeedsPaintOverlay(HWND hwnd); +LRESULT WAYLAND_NotifyIcon(HWND hwnd, UINT msg, NOTIFYICONDATAW *data); +void WAYLAND_CleanupIcons(HWND hwnd); +BOOL wayland_get_icon_info(HICON handle, ICONINFOEXW *ret); +NTSTATUS waylanddrv_unix_tray_dispatch(void *arg); BOOL WAYLAND_CreateWindowSurface(HWND hwnd, BOOL layered, const RECT *surface_rect, struct window_surface **surface); UINT WAYLAND_VulkanInit(UINT version, void *vulkan_handle, const struct vulkan_driver_funcs **driver_funcs); UINT WAYLAND_OpenGLInit(UINT version, const struct opengl_funcs *opengl_funcs, const struct opengl_driver_funcs **driver_funcs); diff --git a/dlls/winewayland.drv/waylanddrv_main.c b/dlls/winewayland.drv/waylanddrv_main.c index 275a3907f92..9863e032c68 100644 --- a/dlls/winewayland.drv/waylanddrv_main.c +++ b/dlls/winewayland.drv/waylanddrv_main.c @@ -32,13 +32,20 @@ char *process_name = NULL; +static BOOL WAYLAND_WantsCrossProcessSurfaces(void) +{ + return TRUE; +} + static const struct user_driver_funcs waylanddrv_funcs = { + .pCleanupIcons = WAYLAND_CleanupIcons, .pClipboardWindowProc = WAYLAND_ClipboardWindowProc, .pClipCursor = WAYLAND_ClipCursor, .pDesktopWindowProc = WAYLAND_DesktopWindowProc, .pDestroyWindow = WAYLAND_DestroyWindow, .pSetIMECompositionRect = WAYLAND_SetIMECompositionRect, + .pSetIMEEnabled = WAYLAND_SetIMEEnabled, .pKbdLayerDescriptor = WAYLAND_KbdLayerDescriptor, .pReleaseKbdTables = WAYLAND_ReleaseKbdTables, .pSetCursor = WAYLAND_SetCursor, @@ -57,6 +64,11 @@ static const struct user_driver_funcs waylanddrv_funcs = .pCreateWindowSurface = WAYLAND_CreateWindowSurface, .pVulkanInit = WAYLAND_VulkanInit, .pOpenGLInit = WAYLAND_OpenGLInit, + .pWantsCrossProcessSurfaces = WAYLAND_WantsCrossProcessSurfaces, + .pFlushRemoteSurface = WAYLAND_FlushRemoteSurface, + .pFlushRemoteSurfaceDMABUF = WAYLAND_FlushRemoteSurfaceDMABUF, + .pNeedsPaintOverlay = WAYLAND_NeedsPaintOverlay, + .pNotifyIcon = WAYLAND_NotifyIcon, }; static void wayland_init_process_name(void) @@ -128,6 +140,7 @@ const unixlib_entry_t __wine_unix_call_funcs[] = waylanddrv_unix_init, waylanddrv_unix_read_events, waylanddrv_unix_init_clipboard, + waylanddrv_unix_tray_dispatch, }; C_ASSERT(ARRAYSIZE(__wine_unix_call_funcs) == waylanddrv_unix_func_count); @@ -139,6 +152,7 @@ const unixlib_entry_t __wine_unix_call_wow64_funcs[] = waylanddrv_unix_init, waylanddrv_unix_read_events, waylanddrv_unix_init_clipboard, + waylanddrv_unix_tray_dispatch, }; C_ASSERT(ARRAYSIZE(__wine_unix_call_wow64_funcs) == waylanddrv_unix_func_count); diff --git a/dlls/winewayland.drv/window.c b/dlls/winewayland.drv/window.c index afef743755b..71408aec7ef 100644 --- a/dlls/winewayland.drv/window.c +++ b/dlls/winewayland.drv/window.c @@ -26,15 +26,21 @@ #include <assert.h> #include <stdlib.h> +#include <unistd.h> +#include <sys/stat.h> #include "ntstatus.h" +#include "winternl.h" #include "waylanddrv.h" #include "wine/debug.h" +#include "wine/server.h" WINE_DEFAULT_DEBUG_CHANNEL(waylanddrv); +static const WCHAR vulkan_drm_format_propW[] = {'V','u','l','k','a','n','D','r','m','F','o','r','m','a','t',0}; + static int wayland_win_data_cmp_rb(const void *key, const struct rb_entry *entry) @@ -51,6 +57,15 @@ static int wayland_win_data_cmp_rb(const void *key, static pthread_mutex_t win_data_mutex = PTHREAD_MUTEX_INITIALIZER; static struct rb_tree win_data_rb = { wayland_win_data_cmp_rb }; +static void reapply_cursor_clipping(void); +/* Set under win_data_mutex when a surface (re)creation needs the cursor clip + * re-applied; consumed in wayland_win_data_release once the lock is dropped. + * reapply_cursor_clipping() -> NtUserClipCursor() can synchronously re-enter the + * driver (WAYLAND_ClipCursor -> wayland_win_data_get) when GrabPointer is on, so + * it must not run while win_data_mutex (non-recursive) is held. Per-thread: it is + * always set and consumed on the same thread between a get/release pair. */ +static __thread BOOL reapply_clip_pending; + /*********************************************************************** * wayland_win_data_create * @@ -91,6 +106,33 @@ static struct wayland_win_data *wayland_win_data_create(HWND hwnd, const struct /*********************************************************************** * wayland_win_data_destroy */ +/* destroy the remote overlay subsurface and buffer; must be called before + * destroying or replacing the wayland surface the overlay is attached to */ +static void wayland_win_data_destroy_remote_overlay(struct wayland_win_data *data) +{ + if (data->remote_overlay) + { + wayland_surface_destroy(data->remote_overlay); + data->remote_overlay = NULL; + } + if (data->remote_buffer_queue) + { + wayland_buffer_queue_destroy(data->remote_buffer_queue); + data->remote_buffer_queue = NULL; + } +} + +/* tear down both the remote overlay and the wayland surface it is attached to */ +static void wayland_win_data_clear_surface(struct wayland_win_data *data) +{ + wayland_win_data_destroy_remote_overlay(data); + if (data->wayland_surface) + { + wayland_surface_destroy(data->wayland_surface); + data->wayland_surface = NULL; + } +} + static void wayland_win_data_destroy(struct wayland_win_data *data) { TRACE("hwnd=%p\n", data->hwnd); @@ -99,7 +141,7 @@ static void wayland_win_data_destroy(struct wayland_win_data *data) pthread_mutex_unlock(&win_data_mutex); - if (data->wayland_surface) wayland_surface_destroy(data->wayland_surface); + wayland_win_data_clear_surface(data); if (data->window_contents) wayland_shm_buffer_unref(data->window_contents); free(data); } @@ -145,6 +187,14 @@ void wayland_win_data_release(struct wayland_win_data *data) { assert(data); pthread_mutex_unlock(&win_data_mutex); + + /* Run any cursor-clip reapply requested while the lock was held, now that it + * is dropped (NtUserClipCursor may re-enter and re-take win_data_mutex). */ + if (reapply_clip_pending) + { + reapply_clip_pending = FALSE; + reapply_cursor_clipping(); + } } static void wayland_win_data_get_config(struct wayland_win_data *data, @@ -157,25 +207,75 @@ static void wayland_win_data_get_config(struct wayland_win_data *data, conf->client_rect = data->rects.client; style = NtUserGetWindowLongW(data->hwnd, GWL_STYLE); + /* Cache the rectangle of the monitor containing the window, in the same raw + * coordinate space as conf->rect, so reconfigure_geometry can clamp an oversized + * maximized/fullscreen window to the visible monitor area without querying win32u + * from the window-surface flush path (which would take user_lock under the surface + * lock and deadlock). Computed here on the app thread that also produces + * conf->rect, so the monitor rect and window rect share its DPI awareness context. */ + { + MONITORINFO mi = {.cbSize = sizeof(mi)}; + HMONITOR hmon = NtUserMonitorFromRect(&conf->rect, 0); + if (hmon && NtUserGetMonitorInfo(hmon, &mi)) + conf->monitor_rect = mi.rcMonitor; + else + SetRectEmpty(&conf->monitor_rect); + } + TRACE("window=%s style=%#x\n", wine_dbgstr_rect(&conf->rect), style); conf->minimized = !!(style & WS_MINIMIZE); - /* The fullscreen state is implied by the window position and style. */ - if (data->is_fullscreen) + /* The fullscreen/maximized state is implied by the window position and + * style. A window that merely fills the monitor is not necessarily a + * fullscreen app: borderless launchers (CEF/Steam, Chrome) resize + * themselves to screen size to emulate maximize. Treat a monitor-sized + * window as true fullscreen only when it is NOT user-resizable + * (no WS_THICKFRAME); resizable ones stay ordinary toplevels so the + * compositor keeps its panel visible and allows move/resize, unless the + * app explicitly requested WS_MAXIMIZE. */ + if (style & WS_MAXIMIZE) { - if ((style & WS_MAXIMIZE) && (style & WS_CAPTION) == WS_CAPTION) - window_state |= WAYLAND_SURFACE_CONFIG_STATE_MAXIMIZED; - else if (!(style & WS_MINIMIZE)) - window_state |= WAYLAND_SURFACE_CONFIG_STATE_FULLSCREEN; + window_state |= WAYLAND_SURFACE_CONFIG_STATE_MAXIMIZED; } - else if (style & WS_MAXIMIZE) + else if (data->is_fullscreen && !(style & WS_THICKFRAME) && !(style & WS_MINIMIZE)) { - window_state |= WAYLAND_SURFACE_CONFIG_STATE_MAXIMIZED; + window_state |= WAYLAND_SURFACE_CONFIG_STATE_FULLSCREEN; } - conf->resizeable = data->resizeable; + /* Whether the window is user-resizable, cached here (style is already read, + * and this is not the surface-flush path) for reconfigure_geometry, which + * sets the xdg_toplevel min/max size and must not call win32u from the flush + * path (that would take user_lock under the window-surface lock and deadlock + * against apply_window_pos -> window_surface_set_shape). + * + * Derive it from the WS_THICKFRAME sizing-border style, not win32u's + * WINE_SWP_RESIZABLE flag: that bit is the same but is additionally + * cleared for any window whose frame fills the monitor + * (is_fullscreen()), an X11-WM heuristic against auto-maximizing oversized + * frames. That doesn't apply to Wayland's client-driven sizing and would + * wrongly lock a screen-filling resizable window (e.g. the Steam client) to a + * fixed size, blocking interactive resize and maximize. */ + conf->resizeable = !!(style & WS_THICKFRAME); conf->state = window_state; + /* Derive the surface scale from the actual Wayland output the window is on + * (its physical-to-logical ratio) so windows are sized correctly and the + * pointer is mapped to the right location under compositor scaling — + * including fractional scales and mixed-DPI multi-monitor setups — + * regardless of Wine's configured DPI. Fall back to the process DPI when + * the compositor doesn't expose logical output sizes (no xdg_output) or the + * window isn't on any known output. conf->rect is in physical (raw) coords, + * which is the same space the output physical positions are recorded in. */ + { + double output_scale = wayland_output_scale_for_rect(&conf->rect); + /* Without wp_viewporter buffers are presented 1:1 (the compositor, + * e.g. gamescope, does any scaling to the output itself). */ + if (!process_wayland.wp_viewporter) + conf->scale = 1.0; + else + conf->scale = output_scale > 0.0 ? output_scale + : NtUserGetSystemDpiForProcess(0) / 96.0; + } conf->visible = (style & WS_VISIBLE) == WS_VISIBLE; conf->managed = data->managed; } @@ -210,8 +310,7 @@ static BOOL wayland_win_data_create_wayland_surface(struct wayland_win_data *dat if ((surface = data->wayland_surface) && role && surface->role && surface->role != role) { if (client) wayland_client_surface_attach(client, NULL); - wayland_surface_destroy(data->wayland_surface); - data->wayland_surface = NULL; + wayland_win_data_clear_surface(data); } if (!(surface = data->wayland_surface) && !(surface = wayland_surface_create(data->hwnd))) return FALSE; @@ -230,6 +329,9 @@ static BOOL wayland_win_data_create_wayland_surface(struct wayland_win_data *dat switch (role) { case WAYLAND_SURFACE_ROLE_NONE: + /* also drop the overlay: it would be stale when the window is shown + * again and the client surface stacking changes */ + wayland_win_data_destroy_remote_overlay(data); wayland_surface_clear_role(surface); break; case WAYLAND_SURFACE_ROLE_TOPLEVEL: @@ -244,8 +346,10 @@ static BOOL wayland_win_data_create_wayland_surface(struct wayland_win_data *dat wayland_win_data_get_config(data, &surface->window); /* Size/position changes affect the effective pointer constraint, so update - * it as needed. */ - if (data->hwnd == NtUserGetForegroundWindow()) reapply_cursor_clipping(); + * it as needed. Defer the actual reapply until win_data_mutex is released + * (see reapply_clip_pending) because NtUserClipCursor can re-enter the driver + * and re-take the non-recursive win_data_mutex. */ + if (data->hwnd == NtUserGetForegroundWindow()) reapply_clip_pending = TRUE; TRACE("hwnd=%p surface=%p=>%p\n", data->hwnd, data->wayland_surface, surface); data->wayland_surface = surface; @@ -320,12 +424,30 @@ static void wayland_win_data_update_wayland_state(struct wayland_win_data *data) * we use the config fields to mark them as updated. */ surface->processing.serial = 1; surface->processing.processed = TRUE; + wayland_surface_reconfigure(surface, FALSE); break; } wl_display_flush(process_wayland.wl_display); } +/* (re)create the wayland surface for the given owner and apply its state */ +static void wayland_win_data_reconfigure(struct wayland_win_data *data, struct wayland_surface *owner) +{ + if (wayland_win_data_create_wayland_surface(data, owner)) + wayland_win_data_update_wayland_state(data); +} + +/* Return the wayland surface of the window's owner/toplevel. Returns NULL when + * owner is NULL or is the window itself. The caller must hold win_data_mutex, + * as this uses the _nolock accessor without taking the lock. */ +static struct wayland_surface *win_data_surface_of(HWND owner, HWND self) +{ + struct wayland_win_data *owner_data = + owner && owner != self ? wayland_win_data_get_nolock(owner) : NULL; + return owner_data ? owner_data->wayland_surface : NULL; +} + static BOOL is_managed(HWND hwnd) { struct wayland_win_data *data = wayland_win_data_get(hwnd); @@ -411,16 +533,511 @@ static BOOL is_window_managed(HWND hwnd, UINT swp_flags, BOOL fullscreen) return FALSE; } + +/*********************************************************************** + * WAYLAND_NeedsPaintOverlay + */ +BOOL WAYLAND_NeedsPaintOverlay(HWND hwnd) +{ + struct wayland_win_data *data; + BOOL ret = FALSE; + + /* without a subcompositor there is no overlay subsurface to paint into */ + if (!process_wayland.wl_subcompositor) return FALSE; + + if ((data = wayland_win_data_get(hwnd))) + { + ret = data->client_surface != NULL; + wayland_win_data_release(data); + } + return ret; +} + +/*********************************************************************** + * WAYLAND_FlushRemoteSurface + * + * Present pixels painted by another process into a toplevel window we + * own, as an overlay subsurface above our own content (which may be + * GL/VK and thus unreachable through the window surface). + */ +void WAYLAND_FlushRemoteSurface(HWND hwnd, const RECT *dirty, const void *bits, + UINT stride, UINT width, UINT height) +{ + struct wayland_win_data *data; + struct wayland_surface *overlay; + struct wayland_shm_buffer *buffer; + RECT r = *dirty, src_rect, dst_rect; + BOOL queue_created = FALSE; + HRGN damage; + int qw = 0, qh = 0, dst_w, dst_h; + + if (!process_wayland.wl_subcompositor) return; /* no overlay subsurfaces */ + + if (!(data = wayland_win_data_get(hwnd))) return; + if (!data->wayland_surface) goto done; + + if (r.left < 0) r.left = 0; + if (r.top < 0) r.top = 0; + if (r.right > (LONG)width) r.right = width; + if (r.bottom > (LONG)height) r.bottom = height; + if (r.right <= r.left || r.bottom <= r.top) goto done; + + /* (re)create the buffer pool when the window size changes */ + if (data->remote_buffer_queue) + { + wayland_buffer_queue_get_size(data->remote_buffer_queue, &qw, &qh); + if (qw != (int)width || qh != (int)height) + { + wayland_buffer_queue_destroy(data->remote_buffer_queue); + data->remote_buffer_queue = NULL; + } + } + if (!data->remote_buffer_queue) + { + if (!(data->remote_buffer_queue = + wayland_buffer_queue_create(width, height, WL_SHM_FORMAT_ARGB8888))) + goto done; + queue_created = TRUE; + } + + if (!data->remote_overlay) + { + struct wl_region *input_region; + + /* hwnd 0: the overlay is a pass-through child of the toplevel, not a + * window of its own (the parent is recorded in toplevel_hwnd by + * make_subsurface). This keeps wayland_surface_destroy from clearing the + * parent's pointer/keyboard focus and constraint when the overlay goes. */ + if (!(overlay = wayland_surface_create(0))) goto done; + wayland_surface_make_subsurface(overlay, data->wayland_surface); + if (!overlay->wl_subsurface) + { + wayland_surface_destroy(overlay); + goto done; + } + wl_subsurface_set_position(overlay->wl_subsurface, 0, 0); + /* pass input through to the parent surface */ + if ((input_region = wl_compositor_create_region(process_wayland.wl_compositor))) + { + wl_surface_set_input_region(overlay->wl_surface, input_region); + wl_region_destroy(input_region); + } + data->remote_overlay = overlay; + /* subsurface state is applied on the next parent commit */ + wl_surface_commit(data->wayland_surface->wl_surface); + } + overlay = data->remote_overlay; + /* the buffer is in physical pixels like the parent's; present it at the + * parent's logical scale via the viewport below */ + SetRect(&overlay->window.rect, 0, 0, width, height); + overlay->window.scale = data->wayland_surface->window.scale; + + /* a fresh pool starts blank, so repaint the whole window into it */ + if (queue_created) SetRect(&r, 0, 0, width, height); + + if (!(damage = NtGdiCreateRectRgn(r.left, r.top, r.right, r.bottom))) goto done; + wayland_buffer_queue_add_damage(data->remote_buffer_queue, damage); + if (!(buffer = wayland_buffer_queue_get_free_buffer(data->remote_buffer_queue))) + { + NtGdiDeleteObjectApp(damage); + goto done; + } + + /* The painter's bits are a full-window snapshot at `stride`. Copy this + * buffer's accumulated damage (what changed in it since it was last + * presented) out of them, forcing painted pixels opaque -- GDI leaves the + * alpha channel zeroed. Areas the child never painted are never in any + * damage, so they stay transparent over our own GL/VK content instead of + * flooding it opaque. */ + SetRect(&src_rect, 0, 0, stride / 4, height); + SetRect(&dst_rect, 0, 0, width, height); + copy_pixel_region(bits, &src_rect, buffer->map_data, &dst_rect, buffer->damage_region, TRUE); + NtGdiSetRectRgn(buffer->damage_region, 0, 0, 0, 0); + + TRACE("hwnd %p dirty %s overlay %p\n", hwnd, wine_dbgstr_rect(&r), overlay); + + /* keep the overlay stacked above any GL/VK client surface; restack + * unconditionally (idempotent, applied on the next parent commit) so a + * recreated client surface can't leave the overlay buried */ + if (data->client_surface) + { + wl_subsurface_place_above(overlay->wl_subsurface, data->client_surface->wl_surface); + wl_surface_commit(data->wayland_surface->wl_surface); + } + + /* attach_shm takes its own buffer reference and marks it busy; the queue + * owns the buffer, so we must not unref it here */ + wayland_surface_attach_shm(overlay, buffer, damage); + { POINT p = map_point_to_surface(overlay, (POINT){width, height}); dst_w = p.x; dst_h = p.y; } + /* without wp_viewporter the committed buffer defines the surface size */ + if (overlay->wp_viewport) + wp_viewport_set_destination(overlay->wp_viewport, max(1, dst_w), max(1, dst_h)); + NtGdiDeleteObjectApp(damage); + wl_surface_commit(overlay->wl_surface); + wl_display_flush(process_wayland.wl_display); + +done: + wayland_win_data_release(data); +} + +/* Release listener for an uncached DMABUF wl_buffer (fstat gave no inode, so it + * can't go in the per-surface cache): destroy it once the compositor signals it + * is done reading, rather than right after commit (which could drop a frame the + * compositor hasn't composited yet). */ +static void dmabuf_oneshot_buffer_release(void *data, struct wl_buffer *buffer) +{ + wl_buffer_destroy(buffer); +} + +static const struct wl_buffer_listener dmabuf_oneshot_buffer_listener = +{ + dmabuf_oneshot_buffer_release +}; + +/*********************************************************************** + * post_dmabuf_to_owner + * + * Duplicate a presented DMABUF handle into the toplevel owner's process and + * post it, so the owner presents the frame on its toplevel surface. Opening the + * owner process on every frame (NtOpenProcess) is expensive and the owner + * rarely changes, so its PROCESS_DUP_HANDLE handle is cached in function-local + * state (refreshed when it goes stale, e.g. the owner exited and the pid was + * reused). Nothing outside this function needs that state, so it lives here. + */ +static void post_dmabuf_to_owner(HWND toplevel, DWORD pid, HWND hwnd, HANDLE handle, + UINT width, UINT height, UINT stride) +{ + static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; + static HANDLE cached_proc; + static DWORD cached_pid; + HANDLE parent_proc = NULL, target_handle = NULL; + BOOL remote = FALSE; + + pthread_mutex_lock(&mutex); + + if (cached_proc && cached_pid != pid) + { + NtClose(cached_proc); + cached_proc = NULL; + cached_pid = 0; + } + + if (pid != GetCurrentProcessId()) + { + int attempt; + /* one retry: a failed duplication usually means the cached handle is + * stale (owner exited, pid reused), so reopen and try once more */ + for (attempt = 0; attempt < 2 && !target_handle; attempt++) + { + if (!cached_proc) + { + CLIENT_ID cid = { .UniqueProcess = (HANDLE)(ULONG_PTR)pid }; + OBJECT_ATTRIBUTES attr = { .Length = sizeof(attr) }; + if (NtOpenProcess(&cached_proc, PROCESS_DUP_HANDLE, &attr, &cid) != STATUS_SUCCESS) + { + cached_proc = NULL; + break; + } + cached_pid = pid; + } + parent_proc = cached_proc; + remote = TRUE; + if (NtDuplicateObject((HANDLE)-1, handle, parent_proc, &target_handle, 0, 0, DUPLICATE_SAME_ACCESS) != STATUS_SUCCESS) + { + target_handle = NULL; + NtClose(cached_proc); + cached_proc = NULL; + cached_pid = 0; + } + } + } + else /* owner is us (no win data yet): duplicate within our own process */ + { + NtDuplicateObject((HANDLE)-1, handle, (HANDLE)-1, &target_handle, 0, 0, DUPLICATE_SAME_ACCESS); + } + + if (target_handle) + { + WPARAM wp = (WPARAM)(ULONG_PTR)hwnd | ((WPARAM)(ULONG_PTR)target_handle << 32); + /* width/height are packed into the low 16 bits each of the LPARAM (stride + * takes the high 32). Vulkan swapchain extents are clamped far below 65535 + * (maxImageDimension2D), so this is unreachable in practice; WARN rather + * than silently truncate if a future caller ever exceeds it (the local + * same-process path receives the full UINT dimensions directly). */ + if (width > 0xffff || height > 0xffff) + WARN("dimensions %ux%u exceed the 16-bit cross-process packing limit\n", width, height); + if (!NtUserPostMessage(toplevel, WM_WAYLAND_FLUSH_REMOTE_SURFACE_DMABUF, wp, + (LPARAM)(((UINT64)stride << 32) | MAKELONG(width, height)))) + { + /* the receiver closes the handle on success; reclaim it on failure + * so we don't leak it in the target process */ + if (remote) NtDuplicateObject(parent_proc, target_handle, NULL, NULL, 0, 0, DUPLICATE_CLOSE_SOURCE); + else NtClose(target_handle); + } + } + else ERR("Failed to duplicate handle %p to owner process %u\n", handle, (unsigned int)pid); + + pthread_mutex_unlock(&mutex); +} + +/*********************************************************************** + * WAYLAND_FlushRemoteSurfaceDMABUF + * + * Present a hardware-accelerated Vulkan frame using a client-side DMABUF + * exported from the child process. + */ +void WAYLAND_FlushRemoteSurfaceDMABUF(HWND hwnd, HANDLE handle, UINT64 modifier, + UINT width, UINT height, UINT stride) +{ + HWND toplevel = NtUserGetAncestor(hwnd, GA_ROOT); + struct wayland_win_data *data; + struct wayland_surface *surface; + struct wl_buffer *wl_buffer = NULL; + struct zwp_linux_buffer_params_v1 *params; + int fd = -1; + NTSTATUS status; + struct stat st; + uint64_t ino = 0; + int i, empty_slot = -1, oldest_slot = -1; + uint32_t oldest_time = 0xffffffff; + uint32_t drm_format; + DWORD toplevel_pid = 0; + + if (!NtUserIsWindowVisible(hwnd)) + { + return; + } + + if (toplevel) NtUserGetWindowThread(toplevel, &toplevel_pid); + + data = wayland_win_data_get(hwnd); + if (!data || (toplevel && toplevel_pid != GetCurrentProcessId())) + { + if (data) wayland_win_data_release(data); + /* Cross-process: hand the DMABUF frame to the toplevel owner to present. */ + if (toplevel) + post_dmabuf_to_owner(toplevel, toplevel_pid, hwnd, handle, width, height, stride); + return; + } + + if (!process_wayland.zwp_linux_dmabuf_v1) + { + ERR("zwp_linux_dmabuf_v1 is not bound!\n"); + goto done; + } + + if (!data->wayland_surface) + wayland_win_data_reconfigure(data, win_data_surface_of(toplevel, hwnd)); + + if (!(surface = data->wayland_surface)) + { + ERR("No wayland surface for window %p!\n", hwnd); + goto done; + } + + /* Heal an orphaned subsurface: CEF popup menu frames recreate their wayland + * surface (e.g. on a role/visibility change), which leaves the menu-content + * child subsurface parented to the destroyed frame surface, so it is never + * shown (the menu renders blank/white). Re-parent it to the frame's current + * surface. */ + if (surface->role == WAYLAND_SURFACE_ROLE_SUBSURFACE && surface->wl_subsurface) + { + struct wayland_win_data *pd = wayland_win_data_get_nolock(surface->owner_hwnd); + struct wayland_surface *ps = pd ? pd->wayland_surface : NULL; + if (ps && surface->parent_serial != ps->serial) + wayland_surface_make_subsurface(surface, ps); + } + + status = wine_server_handle_to_fd(handle, FILE_READ_DATA, &fd, NULL); + if (status != STATUS_SUCCESS || fd < 0) + { + ERR("Failed to get fd from handle: %x\n", status); + goto done; + } + + if (fstat(fd, &st) == 0) + { + ino = st.st_ino; + } + else + { + ERR("Failed to fstat fd\n"); + } + + drm_format = (uintptr_t)NtUserGetProp(hwnd, vulkan_drm_format_propW); + if (!drm_format) drm_format = 0x34325258; /* DRM_FORMAT_XRGB8888 */ + else if ((drm_format & 0xff) == 0x41) drm_format = (drm_format & 0xffffff00) | 0x58; /* Map alpha formats to opaque X formats */ + + if (ino != 0) + { + /* Find in cache. The key is (inode, size, format, modifier), not the inode + * alone: after a swapchain resize the kernel can recycle a freed dmabuf's + * inode for a new, differently-sized buffer, so a stale same-inode wl_buffer + * of the old geometry must not be reused. */ + for (i = 0; i < WAYLAND_DMABUF_CACHE_SIZE; i++) + { + if (surface->dmabuf_buffers[i].wl_buffer && + surface->dmabuf_buffers[i].ino == ino && + surface->dmabuf_buffers[i].width == width && + surface->dmabuf_buffers[i].height == height && + surface->dmabuf_buffers[i].format == drm_format && + surface->dmabuf_buffers[i].modifier == modifier) + { + wl_buffer = surface->dmabuf_buffers[i].wl_buffer; + surface->dmabuf_buffers[i].last_used = ++surface->dmabuf_cache_clock; + break; + } + } + } + + if (wl_buffer) + { + close(fd); + } + else + { + params = zwp_linux_dmabuf_v1_create_params(process_wayland.zwp_linux_dmabuf_v1); + if (!params) + { + ERR("Failed to create dmabuf params factory\n"); + close(fd); + goto done; + } + + zwp_linux_buffer_params_v1_add(params, fd, 0, 0, stride ? stride : width * 4, + (uint32_t)(modifier >> 32), + (uint32_t)(modifier & 0xffffffff)); + close(fd); + + wl_buffer = zwp_linux_buffer_params_v1_create_immed(params, width, height, + drm_format, 0); + zwp_linux_buffer_params_v1_destroy(params); + + if (!wl_buffer) + { + ERR("Failed to create wl_buffer from DMABUF\n"); + goto done; + } + + if (ino != 0) + { + for (i = 0; i < WAYLAND_DMABUF_CACHE_SIZE; i++) + { + if (!surface->dmabuf_buffers[i].wl_buffer) + { + empty_slot = i; + break; + } + if (surface->dmabuf_buffers[i].last_used < oldest_time) + { + oldest_time = surface->dmabuf_buffers[i].last_used; + oldest_slot = i; + } + } + + if (empty_slot != -1) + { + i = empty_slot; + } + else + { + i = oldest_slot; + TRACE("Evicting DMABUF buffer slot %d (ino %s)\n", i, wine_dbgstr_longlong(surface->dmabuf_buffers[i].ino)); + wl_buffer_destroy(surface->dmabuf_buffers[i].wl_buffer); + } + + surface->dmabuf_buffers[i].ino = ino; + surface->dmabuf_buffers[i].modifier = modifier; + surface->dmabuf_buffers[i].wl_buffer = wl_buffer; + surface->dmabuf_buffers[i].width = width; + surface->dmabuf_buffers[i].height = height; + surface->dmabuf_buffers[i].format = drm_format; + surface->dmabuf_buffers[i].last_used = ++surface->dmabuf_cache_clock; + } + } + + TRACE("Attaching DMABUF wl_buffer %p to surface %p, size %dx%d\n", wl_buffer, surface->wl_surface, width, height); + + surface->content_hidden = FALSE; + + /* Keep the subsurface position and stacking order in sync with the current + * Win32 window geometry and Z-order on every frame: CEF/Chromium moves and + * restacks its GPU sub-views (e.g. switching between the library and the + * store webview, or showing a popup menu's content above its frame) without + * recreating them, and an opaque sibling would otherwise cover the content + * beneath it. Reconfiguring a subsurface whose parent has been torn down is + * made safe by the parent_serial orphan check in reconfigure_subsurface. */ + if (surface->role == WAYLAND_SURFACE_ROLE_SUBSURFACE && surface->wl_subsurface) + wayland_surface_reconfigure(surface, FALSE); + /* A toplevel that renders only through this DMABUF path (a Vulkan/GL game with + * no GDI window_surface to flush) acks its xdg configures nowhere else. When + * one is pending, reconfigure here so it is acknowledged together with this + * frame; otherwise a fullscreen/maximized configure can sit unacked while the + * game's message loop is busy (loading), the compositor keeps resending it and + * never finalizes the state, and the screen-sized window is left floating + * off-screen. Gated on a pending configure to avoid per-frame work once the + * state has settled. */ + else if (surface->role == WAYLAND_SURFACE_ROLE_TOPLEVEL && surface->xdg_surface && + (surface->requested.serial || surface->processing.serial)) + wayland_surface_reconfigure(surface, FALSE); + + wl_surface_attach(surface->wl_surface, wl_buffer, 0, 0); + /* width/height are buffer pixels; with wp_viewport active the surface + * coordinate space differs, so damage in buffer space. */ + wl_surface_damage_buffer(surface->wl_surface, 0, 0, width, height); + + if (surface->wp_viewport) + { + POINT dst = map_point_to_surface(surface, (POINT){width, height}); + /* The DMABUF holds the whole frame, so sample all of it. Set the source + * explicitly: only the SHM path sets it otherwise, so a window that drew + * GDI content (windowed phase) before switching to this DMABUF path would + * keep that stale, smaller source rect and the buffer would be sampled + * partially and stretched (content overflows past the screen edge). */ + wp_viewport_set_source(surface->wp_viewport, 0, 0, + wl_fixed_from_int(width), wl_fixed_from_int(height)); + wp_viewport_set_destination(surface->wp_viewport, max(1, dst.x), max(1, dst.y)); + } + + wl_surface_commit(surface->wl_surface); + wl_display_flush(process_wayland.wl_display); + + if (ino == 0) + { + /* Not cached (no inode): destroy on wl_buffer.release rather than right + * after commit, so the compositor can finish reading the frame first. */ + wl_buffer_add_listener(wl_buffer, &dmabuf_oneshot_buffer_listener, NULL); + } + +done: + wayland_win_data_release(data); +} + /*********************************************************************** * WAYLAND_DestroyWindow */ void WAYLAND_DestroyWindow(HWND hwnd) { - struct wayland_win_data *data; + struct wayland_win_data *data, *toplevel_data; + /* Resolve the toplevel before win_data_mutex: NtUserGetAncestor takes + * user_lock, and taking it under win_data_mutex would invert the driver's + * lock order. */ + HWND toplevel = NtUserGetAncestor(hwnd, GA_ROOT); TRACE("%p\n", hwnd); + /* a sibling subsurface going away changes the stacking of the rest */ + wayland_invalidate_subsurface_zorder(); + if (!(data = wayland_win_data_get(hwnd))) return; + /* If this child still counted towards its (surviving) toplevel's IME-children + * tally, drop the contribution so a destroyed control without a matching + * ImmAssociateContext(NULL) does not leave IME stuck enabled. A toplevel being + * destroyed frees its own count, so only do this for a different toplevel. */ + if (data->ime_enabled && toplevel != hwnd && + (toplevel_data = wayland_win_data_get_nolock(toplevel)) && + toplevel_data->num_ime_children > 0) + toplevel_data->num_ime_children--; wayland_win_data_destroy(data); } @@ -449,23 +1066,25 @@ void WAYLAND_WindowPosChanged(HWND hwnd, HWND insert_after, HWND owner_hint, UIN HWND owner = NtUserGetAncestor(hwnd, GA_ROOT); struct wayland_surface *owner_surface; struct wayland_client_surface *client; - struct wayland_win_data *data, *owner_data; + struct wayland_win_data *data; BOOL managed, fullscreen = swp_flags & WINE_SWP_FULLSCREEN; TRACE("hwnd %p new_rects %s after %p flags %08x\n", hwnd, debugstr_window_rects(new_rects), insert_after, swp_flags); + /* a window's geometry or z-order may have changed; let subsurfaces + * re-evaluate their stacking on their next present */ + wayland_invalidate_subsurface_zorder(); + /* Get the managed state with win_data unlocked, as is_window_managed * may need to query win_data information about other HWNDs and thus * acquire the lock itself internally. */ if (!(managed = is_window_managed(hwnd, swp_flags, fullscreen)) && surface) owner = owner_hint; if (!(data = wayland_win_data_get(hwnd))) return; - owner_data = owner && owner != hwnd ? wayland_win_data_get_nolock(owner) : NULL; - owner_surface = owner_data ? owner_data->wayland_surface : NULL; + owner_surface = win_data_surface_of(owner, hwnd); data->rects = *new_rects; data->is_fullscreen = fullscreen; - data->resizeable = swp_flags & WINE_SWP_RESIZABLE; data->managed = managed; if (!surface) @@ -478,16 +1097,18 @@ void WAYLAND_WindowPosChanged(HWND hwnd, HWND insert_after, HWND owner_hint, UIN wayland_client_surface_attach(client, NULL); } - if (data->wayland_surface) - { - wayland_surface_destroy(data->wayland_surface); - data->wayland_surface = NULL; - } - } - else if (wayland_win_data_create_wayland_surface(data, owner_surface)) - { - wayland_win_data_update_wayland_state(data); + if (!data->client_surface && !NtUserGetProp(hwnd, vulkan_drm_format_propW)) + wayland_win_data_clear_surface(data); + else + wayland_win_data_reconfigure(data, owner_surface); } + else + wayland_win_data_reconfigure(data, owner_surface); + + /* On hide, descendants become (hierarchically) invisible without their own + * WindowPosChanged, so drop their stale subsurface frames now (still holding + * win_data_mutex, which wayland_unmap_invisible_subtree requires). */ + if (swp_flags & SWP_HIDEWINDOW) wayland_unmap_invisible_subtree(hwnd); wayland_win_data_release(data); } @@ -630,6 +1251,8 @@ static void wayland_configure_window(HWND hwnd) NtUserSetRawWindowPos(hwnd, rect, flags, FALSE); } +static const WCHAR vulkan_drm_modifier_propW[] = {'V','u','l','k','a','n','D','r','m','M','o','d','i','f','i','e','r',0}; + /********************************************************************** * WAYLAND_WindowMessage */ @@ -645,7 +1268,26 @@ LRESULT WAYLAND_WindowMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) return 0; case WM_WAYLAND_SET_FOREGROUND: NtUserSetForegroundWindowInternal(hwnd); + /* Wayland exposes no cross-process window stacking (unlike X11, where the + * window manager keeps win32u's window order in sync). A window the + * compositor just activated can therefore still sit below another + * process's window in the server's global Z-order -- e.g. a fullscreen + * game the user switched away from -- so pointer hit-testing routes input + * to that stale window and the activated window looks frozen. Raise it to + * the top to match the compositor's stacking. */ + NtUserSetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER); return 0; + case WM_WAYLAND_FLUSH_REMOTE_SURFACE_DMABUF: + { + HWND target_hwnd = (HWND)(ULONG_PTR)(wp & 0xffffffff); + HANDLE handle = (HANDLE)(ULONG_PTR)(wp >> 32); + UINT64 val = (UINT_PTR)NtUserGetProp(target_hwnd, vulkan_drm_modifier_propW); + UINT64 modifier = val ? (val - 1) : 0x00ffffffffffffffULL; /* DRM_FORMAT_MOD_INVALID */ + WAYLAND_FlushRemoteSurfaceDMABUF(target_hwnd, handle, modifier, LOWORD(lp), HIWORD(lp), (UINT)(lp >> 32)); + NtClose(handle); + return 0; + } default: FIXME("got window msg %x hwnd %p wp %lx lp %lx\n", msg, hwnd, (long)wp, lp); return 0; @@ -660,36 +1302,65 @@ LRESULT WAYLAND_DesktopWindowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) return NtUserMessageCall(hwnd, msg, wp, lp, 0, NtUserDefWindowProc, FALSE); } -/***************************************************************** - * WAYLAND_SetLayeredWindowAttributes - */ -void WAYLAND_SetLayeredWindowAttributes(HWND hwnd, COLORREF key, BYTE alpha, DWORD flags) +/* Apply layered-window opacity to the window's surface and (re)create it. */ +static void wayland_apply_layered(HWND hwnd, BYTE alpha, UINT flags) { struct wayland_win_data *data; - struct wayland_surface *surface; + struct wayland_surface *surface, *toplevel_surface; + HWND toplevel; if (!(data = wayland_win_data_get(hwnd))) return; + toplevel = NtUserGetAncestor(hwnd, GA_ROOT); + toplevel_surface = win_data_surface_of(toplevel, hwnd); + if ((surface = data->wayland_surface)) wayland_surface_set_opacity(surface, alpha, flags); data->layered_attribs_set = TRUE; + wayland_win_data_reconfigure(data, toplevel_surface); + wayland_win_data_release(data); } +/***************************************************************** + * WAYLAND_SetLayeredWindowAttributes + */ +void WAYLAND_SetLayeredWindowAttributes(HWND hwnd, COLORREF key, BYTE alpha, DWORD flags) +{ + wayland_apply_layered(hwnd, alpha, flags); +} + static enum xdg_toplevel_resize_edge hittest_to_resize_edge(WPARAM hittest) { switch (hittest) { - case WMSZ_LEFT: return XDG_TOPLEVEL_RESIZE_EDGE_LEFT; - case WMSZ_RIGHT: return XDG_TOPLEVEL_RESIZE_EDGE_RIGHT; - case WMSZ_TOP: return XDG_TOPLEVEL_RESIZE_EDGE_TOP; - case WMSZ_TOPLEFT: return XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT; - case WMSZ_TOPRIGHT: return XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT; - case WMSZ_BOTTOM: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM; - case WMSZ_BOTTOMLEFT: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT; - case WMSZ_BOTTOMRIGHT: return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT; - default: return XDG_TOPLEVEL_RESIZE_EDGE_NONE; + case WMSZ_LEFT: + case 10: /* HTLEFT */ + return XDG_TOPLEVEL_RESIZE_EDGE_LEFT; + case WMSZ_RIGHT: + case 11: /* HTRIGHT */ + return XDG_TOPLEVEL_RESIZE_EDGE_RIGHT; + case WMSZ_TOP: + case 12: /* HTTOP */ + return XDG_TOPLEVEL_RESIZE_EDGE_TOP; + case WMSZ_TOPLEFT: + case 13: /* HTTOPLEFT */ + return XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT; + case WMSZ_TOPRIGHT: + case 14: /* HTTOPRIGHT */ + return XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT; + case WMSZ_BOTTOM: + case 15: /* HTBOTTOM */ + return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM; + case WMSZ_BOTTOMLEFT: + case 16: /* HTBOTTOMLEFT */ + return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT; + case WMSZ_BOTTOMRIGHT: + case 17: /* HTBOTTOMRIGHT */ + return XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT; + default: + return XDG_TOPLEVEL_RESIZE_EDGE_NONE; } } @@ -739,6 +1410,12 @@ void WAYLAND_SetWindowStyle(HWND hwnd, INT offset, STYLESTRUCT *style) data->layered_attribs_set = FALSE; } + /* Visibility cleared via a direct style change (no WindowPosChanged): drop + * the now (hierarchically) invisible descendants' stale subsurface frames. + * win_data_mutex is held, as wayland_unmap_invisible_subtree requires. */ + if (offset == GWL_STYLE && (changed & WS_VISIBLE) && !(style->styleNew & WS_VISIBLE)) + wayland_unmap_invisible_subtree(hwnd); + wayland_win_data_release(data); } @@ -763,28 +1440,64 @@ void WAYLAND_SetWindowText(HWND hwnd, LPCWSTR text) /*********************************************************************** * WAYLAND_SysCommand */ +static HWND find_remote_child_window(HWND parent) +{ + HWND child; + HWND sub_child; + DWORD pid; + + child = NtUserGetWindowRelative(parent, GW_CHILD); + while (child) + { + pid = 0; + NtUserGetWindowThread(child, &pid); + if (pid && pid != GetCurrentProcessId()) + { + return child; + } + sub_child = find_remote_child_window(child); + if (sub_child) + { + return sub_child; + } + child = NtUserGetWindowRelative(child, GW_HWNDNEXT); + } + return NULL; +} + LRESULT WAYLAND_SysCommand(HWND hwnd, WPARAM wparam, LPARAM lparam, const POINT *pos) { LRESULT ret = -1; WPARAM command = wparam & 0xfff0; - uint32_t button_serial; + uint32_t button_serial = 0; struct wl_seat *wl_seat; struct wayland_surface *surface; struct wayland_win_data *data; + HWND toplevel; TRACE("cmd=%lx hwnd=%p, %lx, %lx\n", (long)command, hwnd, (long)wparam, lparam); pthread_mutex_lock(&process_wayland.pointer.mutex); - if (process_wayland.pointer.focused_hwnd == hwnd) - button_serial = process_wayland.pointer.button_serial; - else - button_serial = 0; + button_serial = process_wayland.pointer.button_serial; pthread_mutex_unlock(&process_wayland.pointer.mutex); + if (!button_serial && (command == SC_MOVE || command == SC_SIZE)) + { + HWND remote_child = find_remote_child_window(hwnd); + if (remote_child) + { + TRACE("Delegating syscommand to remote child window %p\n", remote_child); + NtUserPostMessage(remote_child, WM_SYSCOMMAND, wparam, lparam); + return 0; + } + } + if (command == SC_MOVE || command == SC_SIZE) { - if ((data = wayland_win_data_get(hwnd))) + toplevel = NtUserGetAncestor(hwnd, GA_ROOT); + if (!toplevel) toplevel = hwnd; + if ((data = wayland_win_data_get(toplevel))) { pthread_mutex_lock(&process_wayland.seat.mutex); wl_seat = process_wayland.seat.wl_seat; @@ -794,16 +1507,20 @@ LRESULT WAYLAND_SysCommand(HWND hwnd, WPARAM wparam, LPARAM lparam, const POINT if (command == SC_MOVE) { xdg_toplevel_move(surface->xdg_toplevel, wl_seat, button_serial); + ret = 0; } else if (command == SC_SIZE) { - xdg_toplevel_resize(surface->xdg_toplevel, wl_seat, button_serial, - hittest_to_resize_edge(wparam & 0x0f)); + enum xdg_toplevel_resize_edge edge = hittest_to_resize_edge(wparam & 0x0f); + if (edge != XDG_TOPLEVEL_RESIZE_EDGE_NONE && (NtUserGetWindowLongW(toplevel, GWL_STYLE) & WS_THICKFRAME)) + { + xdg_toplevel_resize(surface->xdg_toplevel, wl_seat, button_serial, edge); + ret = 0; + } } } pthread_mutex_unlock(&process_wayland.seat.mutex); wayland_win_data_release(data); - ret = 0; } } @@ -816,15 +1533,7 @@ LRESULT WAYLAND_SysCommand(HWND hwnd, WPARAM wparam, LPARAM lparam, const POINT */ void WAYLAND_UpdateLayeredWindow(HWND hwnd, BYTE alpha, UINT flags) { - struct wayland_win_data *data; - struct wayland_surface *surface; - - if (!(data = wayland_win_data_get(hwnd))) return; - - if ((surface = data->wayland_surface)) - wayland_surface_set_opacity(surface, alpha, flags); - - wayland_win_data_release(data); + wayland_apply_layered(hwnd, alpha, flags); } void set_client_surface(HWND hwnd, struct wayland_client_surface *new_client) @@ -865,7 +1574,7 @@ BOOL set_window_surface_contents(HWND hwnd, struct wayland_shm_buffer *shm_buffe if ((wayland_surface = data->wayland_surface)) { - if (wayland_surface_reconfigure(wayland_surface)) + if (wayland_surface_reconfigure(wayland_surface, TRUE)) { wayland_surface_attach_shm(wayland_surface, shm_buffer, damage_region); wl_surface_commit(wayland_surface->wl_surface); @@ -916,7 +1625,7 @@ void ensure_window_surface_contents(HWND hwnd) * surface state is applied by the compositor. */ if (wayland_surface->processing.serial && wayland_surface->processing.processed && - wayland_surface_reconfigure(wayland_surface)) + wayland_surface_reconfigure(wayland_surface, FALSE)) { wl_surface_commit(wayland_surface->wl_surface); } diff --git a/dlls/winewayland.drv/window_surface.c b/dlls/winewayland.drv/window_surface.c index e7595b7ccb0..63e6b8dc006 100644 --- a/dlls/winewayland.drv/window_surface.c +++ b/dlls/winewayland.drv/window_surface.c @@ -69,7 +69,7 @@ static const struct wl_buffer_listener buffer_listener = { buffer_release }; * * Destroys a buffer queue and any contained buffers. */ -static void wayland_buffer_queue_destroy(struct wayland_buffer_queue *queue) +void wayland_buffer_queue_destroy(struct wayland_buffer_queue *queue) { struct wayland_shm_buffer *shm_buffer, *next; @@ -103,8 +103,8 @@ static void wayland_buffer_queue_destroy(struct wayland_buffer_queue *queue) * * Creates a buffer queue containing buffers with the specified width and height. */ -static struct wayland_buffer_queue *wayland_buffer_queue_create(int width, int height, - uint32_t format) +struct wayland_buffer_queue *wayland_buffer_queue_create(int width, int height, + uint32_t format) { struct wayland_buffer_queue *queue; @@ -132,7 +132,7 @@ err: * Gets a free buffer from the buffer queue. If no free buffers * are available this function blocks until it can provide one. */ -static struct wayland_shm_buffer *wayland_buffer_queue_get_free_buffer(struct wayland_buffer_queue *queue) +struct wayland_shm_buffer *wayland_buffer_queue_get_free_buffer(struct wayland_buffer_queue *queue) { struct wayland_shm_buffer *shm_buffer; @@ -200,7 +200,7 @@ out: /********************************************************************** * wayland_buffer_queue_add_damage */ -static void wayland_buffer_queue_add_damage(struct wayland_buffer_queue *queue, HRGN damage) +void wayland_buffer_queue_add_damage(struct wayland_buffer_queue *queue, HRGN damage) { struct wayland_shm_buffer *shm_buffer; @@ -211,6 +211,12 @@ static void wayland_buffer_queue_add_damage(struct wayland_buffer_queue *queue, } } +void wayland_buffer_queue_get_size(struct wayland_buffer_queue *queue, int *width, int *height) +{ + *width = queue->width; + *height = queue->height; +} + /*********************************************************************** * wayland_window_surface_set_clip */ @@ -243,9 +249,9 @@ RGNDATA *get_region_data(HRGN region) /********************************************************************** * copy_pixel_region */ -static void copy_pixel_region(const char *src_pixels, RECT *src_rect, - char *dst_pixels, RECT *dst_rect, - HRGN region, BOOL force_opaque) +void copy_pixel_region(const char *src_pixels, RECT *src_rect, + char *dst_pixels, RECT *dst_rect, + HRGN region, BOOL force_opaque) { static const int bpp = WINEWAYLAND_BYTES_PER_PIXEL; RGNDATA *rgndata = get_region_data(region); @@ -418,6 +424,7 @@ static BOOL wayland_window_surface_flush(struct window_surface *window_surface, if (!copy_from_latest_region) { ERR("failed to create copy_from_latest region\n"); + wayland_shm_buffer_unref(latest_buffer); goto done; } NtGdiCombineRgn(copy_from_latest_region, shm_buffer->damage_region, diff --git a/dlls/winex11.drv/xrandr.c b/dlls/winex11.drv/xrandr.c index b147b952cab..c44d2845b01 100644 --- a/dlls/winex11.drv/xrandr.c +++ b/dlls/winex11.drv/xrandr.c @@ -1136,6 +1136,10 @@ static BOOL xrandr14_get_monitors( ULONG_PTR adapter_id, struct gdi_monitor **ne if (!realloc_monitors) goto done; monitors = realloc_monitors; + /* realloc does not zero the grown slots; clear them so fields the + * loop below leaves unset (edid_info, hdr_enabled) stay zeroed like + * the initial calloc'd slots, matching add_monitor's expectations. */ + memset( monitors + monitor_count, 0, (capacity - monitor_count) * sizeof(*monitors) ); } if (enum_output_info->crtc) diff --git a/include/ntuser.h b/include/ntuser.h index c3c4c8cdcf2..5e0b3512d7a 100644 --- a/include/ntuser.h +++ b/include/ntuser.h @@ -640,6 +640,7 @@ enum wine_internal_message WM_WINE_UPDATEWINDOWSTATE, WM_WINE_TRACKMOUSEEVENT, WM_WINE_SETPIXELFORMAT, + WM_WINE_FLUSH_REMOTE_SURFACE, WM_WINE_FIRST_DRIVER_MSG = 0x80001000, /* range of messages reserved for the USER driver */ WM_WINE_CLIPCURSOR = 0x80001ff0, /* internal driver notification messages */ WM_WINE_SETCURSOR, diff --git a/include/wine/gdi_driver.h b/include/wine/gdi_driver.h index 630e035a589..9615aab96bb 100644 --- a/include/wine/gdi_driver.h +++ b/include/wine/gdi_driver.h @@ -218,7 +218,7 @@ struct gdi_dc_funcs }; /* increment this when you change the DC function table */ -#define WINE_GDI_DRIVER_VERSION 110 +#define WINE_GDI_DRIVER_VERSION 111 #define GDI_PRIORITY_NULL_DRV 0 /* null driver */ #define GDI_PRIORITY_FONT_DRV 100 /* any font driver */ @@ -331,12 +331,41 @@ struct pci_id UINT16 revision; }; +#define MONITOR_INFO_HAS_MONITOR_ID 0x00000001 +#define MONITOR_INFO_HAS_MONITOR_NAME 0x00000002 +#define MONITOR_INFO_HAS_PREFERRED_MODE 0x00000004 + +/* Parsed monitor identification exchanged between win32u and the drivers. A + * driver may either provide a binary hardware EDID (which win32u parses into + * this), or fill this directly (which win32u converts to a binary EDID), so the + * EDID byte assembly lives in one place instead of being duplicated per driver. + * The first group of fields is what win32u extracts from any EDID; the second + * group is only consumed when synthesizing an EDID from driver-provided info. */ +struct edid_monitor_info +{ + unsigned int flags; + /* MONITOR_INFO_HAS_MONITOR_ID */ + unsigned short manufacturer, product_code; /* manufacturer: packed big-endian PNP id */ + char monitor_id_string[8]; + /* MONITOR_INFO_HAS_MONITOR_NAME */ + WCHAR monitor_name[14]; + /* MONITOR_INFO_HAS_PREFERRED_MODE */ + unsigned int preferred_width, preferred_height; + /* used only to synthesize a binary EDID (see edid_info below) */ + unsigned int serial_number; + unsigned int width_mm, height_mm; + unsigned int preferred_refresh; /* in mHz */ +}; + struct gdi_monitor { RECT rc_monitor; /* RcMonitor in MONITORINFO struct */ RECT rc_work; /* RcWork in MONITORINFO struct */ unsigned char *edid; /* Extended Device Identification Data */ UINT edid_len; + /* parsed monitor info, used to synthesize an EDID when edid above is NULL; + * ignored when a binary edid is provided */ + const struct edid_monitor_info *edid_info; BOOL hdr_enabled; }; @@ -375,6 +404,7 @@ struct user_driver_funcs UINT (*pImeToAsciiEx)(UINT,UINT,const BYTE*,HIMC); void (*pNotifyIMEStatus)(HWND,UINT); BOOL (*pSetIMECompositionRect)(HWND,RECT); + BOOL (*pSetIMEEnabled)(HWND,BOOL); /* cursor/icon functions */ void (*pDestroyCursorIcon)(HCURSOR); void (*pSetCursor)(HWND,HCURSOR); @@ -434,6 +464,19 @@ struct user_driver_funcs UINT (*pOpenGLInit)(UINT,const struct opengl_funcs *,const struct opengl_driver_funcs **); /* thread management */ void (*pThreadDetach)(void); + /* whether the driver wants shared-memory surfaces for painting windows + * whose toplevel ancestor belongs to another process (no native + * cross-process rendering support, e.g. Wayland) */ + BOOL (*pWantsCrossProcessSurfaces)(void); + /* present cross-process pixels for a toplevel window we own whose + * window surface cannot be used (e.g. OpenGL/Vulkan content) */ + void (*pFlushRemoteSurface)(HWND,const RECT *,const void *,UINT,UINT,UINT); + /* present cross-process DMABUF Vulkan frame */ + void (*pFlushRemoteSurfaceDMABUF)(HWND,HANDLE,UINT64,UINT,UINT,UINT); + /* whether GDI painting into this toplevel's window surface would be + * obscured (e.g. by a GL/VK client surface plane) and should instead + * go through pFlushRemoteSurface */ + BOOL (*pNeedsPaintOverlay)(HWND); }; W32KAPI void __wine_set_user_driver( const struct user_driver_funcs *funcs, UINT version ); diff --git a/include/wine/vulkan_driver.h b/include/wine/vulkan_driver.h index d2209a3ef54..e3a89d319e9 100644 --- a/include/wine/vulkan_driver.h +++ b/include/wine/vulkan_driver.h @@ -89,7 +89,7 @@ struct VkDevice_T #include "wine/list.h" /* Wine internal vulkan driver version, needs to be bumped upon vulkan_funcs changes. */ -#define WINE_VULKAN_DRIVER_VERSION 48 +#define WINE_VULKAN_DRIVER_VERSION 49 struct vulkan_object { @@ -352,6 +352,7 @@ struct vulkan_funcs PFN_vkQueueSubmit2KHR p_vkQueueSubmit2KHR; PFN_vkUnmapMemory p_vkUnmapMemory; PFN_vkUnmapMemory2KHR p_vkUnmapMemory2KHR; + void (*p_vkFlushRemoteSurfaceDMABUF)(HWND hwnd, HANDLE handle, UINT64 modifier, UINT width, UINT height, UINT stride); }; /* interface between win32u and the user drivers */ -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11256