[PATCH 0/1] MR10519: dlls/wineandroid.drv: Switch to AHardwareBuffer and raise minSdkVersion to 26
Switching the Android backend to `AHardwareBuffer` drops the legacy `gralloc` path in favor of the supported and stable NDK API. According to https://apilevels.com/, Android 8.0+ devices already account for about 94.8% of the ecosystem, and the real share is likely higher today, so removing the old `gralloc` code path is a reasonable trade-off. `AHardwareBuffer` does not provide a way to construct a buffer from an existing `native_handle_t`, so buffer transfer is implemented by fully duplicating it through the existing socket-based code path, even for same-process cases. This keeps the implementation consistent with the cross-process path. Function loading is done via `dlopen("libandroid.so")` in `device.c` (instead of expected location in `init.c`). This works around an NDK issue where symbols marked with `__INTRODUCED_IN` cannot be safely referenced when targeting lower API levels, even in `typeof` expressions (I reported it to upstream). To make this work, `__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__` is used, although this is not ideal. Moving these definitions into shared headers would require enabling this macro for the entire module, which could lead to unexpected runtime linker behavior during development in the future. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10519
From: Twaik Yont <9674930+twaik@users.noreply.github.com> Replace legacy gralloc usage with the supported AHardwareBuffer API and update minSdkVersion to 26 to match platform requirements and avoid crashes on unsupported devices. Signed-off-by: Twaik Yont <9674930+twaik@users.noreply.github.com> --- dlls/wineandroid.drv/android.h | 2 +- dlls/wineandroid.drv/android_native.h | 193 +-------- dlls/wineandroid.drv/build.gradle.in | 2 +- dlls/wineandroid.drv/device.c | 563 +++++++++++--------------- dlls/wineandroid.drv/init.c | 52 +-- 5 files changed, 233 insertions(+), 579 deletions(-) diff --git a/dlls/wineandroid.drv/android.h b/dlls/wineandroid.drv/android.h index 16d60911174..3107136cb7e 100644 --- a/dlls/wineandroid.drv/android.h +++ b/dlls/wineandroid.drv/android.h @@ -119,7 +119,7 @@ enum android_window_messages WM_ANDROID_REFRESH = WM_WINE_FIRST_DRIVER_MSG, }; -extern void init_gralloc( const struct hw_module_t *module ); +extern void init_ahardwarebuffers(void); extern HWND get_capture_window(void); extern void init_monitors( int width, int height ); extern void set_screen_dpi( DWORD dpi ); diff --git a/dlls/wineandroid.drv/android_native.h b/dlls/wineandroid.drv/android_native.h index fc26beb6534..7c3c49630ad 100644 --- a/dlls/wineandroid.drv/android_native.h +++ b/dlls/wineandroid.drv/android_native.h @@ -26,16 +26,6 @@ /* Native window definitions */ -typedef struct native_handle -{ - int version; - int numFds; - int numInts; - int data[0]; -} native_handle_t; - -typedef const native_handle_t *buffer_handle_t; - struct android_native_base_t { int magic; @@ -53,18 +43,7 @@ typedef struct android_native_rect_t int32_t bottom; } android_native_rect_t; -struct ANativeWindowBuffer -{ - struct android_native_base_t common; - int width; - int height; - int stride; - int format; - int usage; - void *reserved[2]; - buffer_handle_t handle; - void *reserved_proc[8]; -}; +struct ANativeWindowBuffer; struct ANativeWindow { @@ -125,9 +104,7 @@ enum native_window_perform enum native_window_api { NATIVE_WINDOW_API_EGL = 1, - NATIVE_WINDOW_API_CPU = 2, - NATIVE_WINDOW_API_MEDIA = 3, - NATIVE_WINDOW_API_CAMERA = 4 + NATIVE_WINDOW_API_CPU = 2 }; enum android_pixel_format @@ -141,176 +118,10 @@ enum android_pixel_format PF_RGBA_4444 = 7 }; - -/* Hardware module definitions */ - -struct hw_module_methods_t; -struct hw_device_t; -struct android_ycbcr; - -struct hw_module_t -{ - uint32_t tag; - uint16_t module_api_version; - uint16_t hal_api_version; - const char *id; - const char *name; - const char *author; - struct hw_module_methods_t *methods; - void *dso; - void *reserved[32-7]; -}; - -struct hw_module_methods_t -{ - int (*open)(const struct hw_module_t *module, const char *id, struct hw_device_t **device); -}; - -struct hw_device_t -{ - uint32_t tag; - uint32_t version; - struct hw_module_t *module; - void *reserved[12]; - int (*close)(struct hw_device_t *device); -}; - -struct gralloc_module_t -{ - struct hw_module_t common; - int (*registerBuffer)(struct gralloc_module_t const *module, buffer_handle_t handle); - int (*unregisterBuffer)(struct gralloc_module_t const *module, buffer_handle_t handle); - int (*lock)(struct gralloc_module_t const *module, buffer_handle_t handle, int usage, int l, int t, int w, int h, void **vaddr); - int (*unlock)(struct gralloc_module_t const *module, buffer_handle_t handle); - int (*perform)(struct gralloc_module_t const *module, int operation, ... ); - int (*lock_ycbcr)(struct gralloc_module_t const *module, buffer_handle_t handle, int usage, int l, int t, int w, int h, struct android_ycbcr *ycbcr); - void *reserved_proc[6]; -}; - #define ANDROID_NATIVE_MAKE_CONSTANT(a,b,c,d) \ (((unsigned)(a)<<24)|((unsigned)(b)<<16)|((unsigned)(c)<<8)|(unsigned)(d)) #define ANDROID_NATIVE_WINDOW_MAGIC \ ANDROID_NATIVE_MAKE_CONSTANT('_','w','n','d') -#define ANDROID_NATIVE_BUFFER_MAGIC \ - ANDROID_NATIVE_MAKE_CONSTANT('_','b','f','r') - -enum gralloc_usage -{ - GRALLOC_USAGE_SW_READ_NEVER = 0x00000000, - GRALLOC_USAGE_SW_READ_RARELY = 0x00000002, - GRALLOC_USAGE_SW_READ_OFTEN = 0x00000003, - GRALLOC_USAGE_SW_READ_MASK = 0x0000000F, - GRALLOC_USAGE_SW_WRITE_NEVER = 0x00000000, - GRALLOC_USAGE_SW_WRITE_RARELY = 0x00000020, - GRALLOC_USAGE_SW_WRITE_OFTEN = 0x00000030, - GRALLOC_USAGE_SW_WRITE_MASK = 0x000000F0, - GRALLOC_USAGE_HW_TEXTURE = 0x00000100, - GRALLOC_USAGE_HW_RENDER = 0x00000200, - GRALLOC_USAGE_HW_2D = 0x00000400, - GRALLOC_USAGE_HW_COMPOSER = 0x00000800, - GRALLOC_USAGE_HW_FB = 0x00001000, - GRALLOC_USAGE_HW_VIDEO_ENCODER = 0x00010000, - GRALLOC_USAGE_HW_CAMERA_WRITE = 0x00020000, - GRALLOC_USAGE_HW_CAMERA_READ = 0x00040000, - GRALLOC_USAGE_HW_CAMERA_ZSL = 0x00060000, - GRALLOC_USAGE_HW_CAMERA_MASK = 0x00060000, - GRALLOC_USAGE_HW_MASK = 0x00071F00, - GRALLOC_USAGE_EXTERNAL_DISP = 0x00002000, - GRALLOC_USAGE_PROTECTED = 0x00004000, - GRALLOC_USAGE_PRIVATE_0 = 0x10000000, - GRALLOC_USAGE_PRIVATE_1 = 0x20000000, - GRALLOC_USAGE_PRIVATE_2 = 0x40000000, - GRALLOC_USAGE_PRIVATE_3 = 0x80000000, - GRALLOC_USAGE_PRIVATE_MASK = 0xF0000000, -}; - -#define GRALLOC_HARDWARE_MODULE_ID "gralloc" - -extern int hw_get_module(const char *id, const struct hw_module_t **module); - -typedef enum -{ - GRALLOC1_CAPABILITY_INVALID = 0, - GRALLOC1_CAPABILITY_TEST_ALLOCATE = 1, - GRALLOC1_CAPABILITY_LAYERED_BUFFERS = 2, - GRALLOC1_CAPABILITY_RELEASE_IMPLY_DELETE = 3, - GRALLOC1_LAST_CAPABILITY = 3, -} gralloc1_capability_t; - -typedef enum -{ - GRALLOC1_FUNCTION_INVALID = 0, - GRALLOC1_FUNCTION_DUMP = 1, - GRALLOC1_FUNCTION_CREATE_DESCRIPTOR = 2, - GRALLOC1_FUNCTION_DESTROY_DESCRIPTOR = 3, - GRALLOC1_FUNCTION_SET_CONSUMER_USAGE = 4, - GRALLOC1_FUNCTION_SET_DIMENSIONS = 5, - GRALLOC1_FUNCTION_SET_FORMAT = 6, - GRALLOC1_FUNCTION_SET_PRODUCER_USAGE = 7, - GRALLOC1_FUNCTION_GET_BACKING_STORE = 8, - GRALLOC1_FUNCTION_GET_CONSUMER_USAGE = 9, - GRALLOC1_FUNCTION_GET_DIMENSIONS = 10, - GRALLOC1_FUNCTION_GET_FORMAT = 11, - GRALLOC1_FUNCTION_GET_PRODUCER_USAGE = 12, - GRALLOC1_FUNCTION_GET_STRIDE = 13, - GRALLOC1_FUNCTION_ALLOCATE = 14, - GRALLOC1_FUNCTION_RETAIN = 15, - GRALLOC1_FUNCTION_RELEASE = 16, - GRALLOC1_FUNCTION_GET_NUM_FLEX_PLANES = 17, - GRALLOC1_FUNCTION_LOCK = 18, - GRALLOC1_FUNCTION_LOCK_FLEX = 19, - GRALLOC1_FUNCTION_UNLOCK = 20, - GRALLOC1_FUNCTION_SET_LAYER_COUNT = 21, - GRALLOC1_FUNCTION_GET_LAYER_COUNT = 22, - GRALLOC1_FUNCTION_VALIDATE_BUFFER_SIZE = 23, - GRALLOC1_FUNCTION_GET_TRANSPORT_SIZE = 24, - GRALLOC1_FUNCTION_IMPORT_BUFFER = 25, - GRALLOC1_LAST_FUNCTION = 25, -} gralloc1_function_descriptor_t; - -typedef enum -{ - GRALLOC1_ERROR_NONE = 0, - GRALLOC1_ERROR_BAD_DESCRIPTOR = 1, - GRALLOC1_ERROR_BAD_HANDLE = 2, - GRALLOC1_ERROR_BAD_VALUE = 3, - GRALLOC1_ERROR_NOT_SHARED = 4, - GRALLOC1_ERROR_NO_RESOURCES = 5, - GRALLOC1_ERROR_UNDEFINED = 6, - GRALLOC1_ERROR_UNSUPPORTED = 7, -} gralloc1_error_t; - -typedef enum -{ - GRALLOC1_PRODUCER_USAGE_NONE = 0, - GRALLOC1_PRODUCER_USAGE_CPU_READ = 1u << 1, - GRALLOC1_PRODUCER_USAGE_CPU_READ_OFTEN = 1u << 2 | GRALLOC1_PRODUCER_USAGE_CPU_READ, - GRALLOC1_PRODUCER_USAGE_CPU_WRITE = 1u << 5, - GRALLOC1_PRODUCER_USAGE_CPU_WRITE_OFTEN = 1u << 6 | GRALLOC1_PRODUCER_USAGE_CPU_WRITE, -} gralloc1_producer_usage_t; - -typedef enum -{ - GRALLOC1_CONSUMER_USAGE_NONE = 0, - GRALLOC1_CONSUMER_USAGE_CPU_READ = 1u << 1, - GRALLOC1_CONSUMER_USAGE_CPU_READ_OFTEN = 1u << 2 | GRALLOC1_CONSUMER_USAGE_CPU_READ, -} gralloc1_consumer_usage_t; - -typedef struct gralloc1_device -{ - struct hw_device_t common; - void (*getCapabilities)(struct gralloc1_device *device, uint32_t *outCount, int32_t *outCapabilities); - void* (*getFunction)(struct gralloc1_device *device, int32_t descriptor); -} gralloc1_device_t; - -typedef struct gralloc1_rect -{ - int32_t left; - int32_t top; - int32_t width; - int32_t height; -} gralloc1_rect_t; - #endif /* __WINE_ANDROID_NATIVE_H */ diff --git a/dlls/wineandroid.drv/build.gradle.in b/dlls/wineandroid.drv/build.gradle.in index 8d0e138c9a2..56ffd06ca82 100644 --- a/dlls/wineandroid.drv/build.gradle.in +++ b/dlls/wineandroid.drv/build.gradle.in @@ -112,7 +112,7 @@ android defaultConfig { applicationId "org.winehq.wine" - minSdkVersion 17 + minSdkVersion 26 versionCode 1 versionName "@PACKAGE_VERSION@" } diff --git a/dlls/wineandroid.drv/device.c b/dlls/wineandroid.drv/device.c index 8ae0172a57f..3d77033a980 100644 --- a/dlls/wineandroid.drv/device.c +++ b/dlls/wineandroid.drv/device.c @@ -24,11 +24,14 @@ #include "config.h" +#define __ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ + #include <assert.h> #include <errno.h> #include <stdio.h> #include <stdarg.h> #include <sys/ioctl.h> +#include <sys/socket.h> #include <unistd.h> #include "ntstatus.h" @@ -41,8 +44,23 @@ #include "wine/server.h" #include "wine/debug.h" +#include <dlfcn.h> + WINE_DEFAULT_DEBUG_CHANNEL(android); +#define DECL_FUNCPTR(f) static typeof(f) * p##f = NULL + +struct AHardwareBuffer* ANativeWindowBuffer_getHardwareBuffer(struct ANativeWindowBuffer* anwb) __INTRODUCED_IN(26); + +DECL_FUNCPTR( AHardwareBuffer_describe ); +DECL_FUNCPTR( AHardwareBuffer_acquire ); +DECL_FUNCPTR( AHardwareBuffer_release ); +DECL_FUNCPTR( AHardwareBuffer_lock ); +DECL_FUNCPTR( AHardwareBuffer_unlock ); +DECL_FUNCPTR( AHardwareBuffer_recvHandleFromUnixSocket ); +DECL_FUNCPTR( AHardwareBuffer_sendHandleToUnixSocket ); +DECL_FUNCPTR( ANativeWindowBuffer_getHardwareBuffer ); + #ifndef SYNC_IOC_WAIT #define SYNC_IOC_WAIT _IOW('>', 0, __s32) #endif @@ -74,21 +92,11 @@ enum android_ioctl #define NB_CACHED_BUFFERS 4 -struct native_buffer_wrapper; - -/* buffer for storing a variable-size native handle inside an ioctl structure */ -union native_handle_buffer -{ - native_handle_t handle; - int space[256]; -}; - /* data about the native window in the context of the Java process */ struct native_win_data { struct ANativeWindow *parent; - struct ANativeWindowBuffer *buffers[NB_CACHED_BUFFERS]; - void *mappings[NB_CACHED_BUFFERS]; + struct AHardwareBuffer *buffers[NB_CACHED_BUFFERS]; HWND hwnd; BOOL opengl; int generation; @@ -102,25 +110,18 @@ struct native_win_data struct native_win_wrapper { struct ANativeWindow win; - struct native_buffer_wrapper *buffers[NB_CACHED_BUFFERS]; - struct ANativeWindowBuffer *locked_buffer; + struct + { + struct AHardwareBuffer *self; + int buffer_id; + int generation; + } buffers[NB_CACHED_BUFFERS]; + struct AHardwareBuffer *locked_buffer; HWND hwnd; BOOL opengl; LONG ref; }; -/* wrapper for a native buffer in the context of the client (non-Java) process */ -struct native_buffer_wrapper -{ - struct ANativeWindowBuffer buffer; - LONG ref; - HWND hwnd; - void *bits; - int buffer_id; - int generation; - union native_handle_buffer native_handle; -}; - struct ioctl_header { int hwnd; @@ -155,15 +156,9 @@ struct ioctl_android_window_pos_changed struct ioctl_android_dequeueBuffer { struct ioctl_header hdr; - int win32; - int width; - int height; - int stride; - int format; - int usage; - int buffer_id; - int generation; - union native_handle_buffer native_handle; + HANDLE handle; + int buffer_id; + int generation; }; struct ioctl_android_queueBuffer @@ -223,18 +218,6 @@ struct ioctl_android_set_cursor int bits[1]; }; -static struct gralloc_module_t *gralloc_module; -static struct gralloc1_device *gralloc1_device; -static BOOL gralloc1_caps[GRALLOC1_LAST_CAPABILITY + 1]; - -static gralloc1_error_t (*gralloc1_retain)( gralloc1_device_t *device, buffer_handle_t buffer ); -static gralloc1_error_t (*gralloc1_release)( gralloc1_device_t *device, buffer_handle_t buffer ); -static gralloc1_error_t (*gralloc1_lock)( gralloc1_device_t *device, buffer_handle_t buffer, - uint64_t producerUsage, uint64_t consumerUsage, - const gralloc1_rect_t *accessRegion, void **outData, - int32_t acquireFence ); -static gralloc1_error_t (*gralloc1_unlock)( gralloc1_device_t *device, buffer_handle_t buffer, - int32_t *outReleaseFence ); static inline BOOL is_in_desktop_process(void) { @@ -247,11 +230,6 @@ static inline DWORD current_client_id(void) return client_id ? client_id : GetCurrentProcessId(); } -static inline BOOL is_client_in_process(void) -{ - return current_client_id() == GetCurrentProcessId(); -} - #ifdef __i386__ /* the Java VM uses %fs/%gs for its own purposes, so we need to wrap the calls */ static WORD orig_fs, java_fs; @@ -327,75 +305,46 @@ static void wait_fence_and_close( int fence ) close( fence ); } -static int duplicate_fd( HANDLE client, int fd ) +static inline struct ANativeWindowBuffer *anwb_from_ahb(AHardwareBuffer *ahb) { - HANDLE handle, ret = 0; + static ptrdiff_t off = (ptrdiff_t)-1; - if (!wine_server_fd_to_handle( dup(fd), GENERIC_READ | SYNCHRONIZE, 0, &handle )) - NtDuplicateObject( GetCurrentProcess(), handle, client, &ret, - 0, 0, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE ); - - if (!ret) return -1; - return HandleToLong( ret ); -} + if (!ahb) return NULL; -static int map_native_handle( union native_handle_buffer *dest, const native_handle_t *src, - HANDLE mapping, HANDLE client ) -{ - const size_t size = offsetof( native_handle_t, data[src->numFds + src->numInts] ); - int i; - - if (mapping) /* only duplicate the mapping handle */ - { - HANDLE ret = 0; - if (!NtDuplicateObject( GetCurrentProcess(), mapping, client, &ret, - 0, 0, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE )) - return -ENOSPC; - dest->handle.numFds = 0; - dest->handle.numInts = 1; - dest->handle.data[0] = HandleToLong( ret ); - return 0; - } - if (is_client_in_process()) /* transfer the actual handle pointer */ + if (off == (ptrdiff_t)-1) { - dest->handle.numFds = 0; - dest->handle.numInts = sizeof(src) / sizeof(int); - memcpy( dest->handle.data, &src, sizeof(src) ); - return 0; + struct ANativeWindowBuffer *fake = + (struct ANativeWindowBuffer *)(uintptr_t)0x10000u; + + AHardwareBuffer *h = pANativeWindowBuffer_getHardwareBuffer(fake); + off = (const char *)h - (const char *)fake; } - if (size > sizeof(*dest)) return -ENOSPC; - memcpy( dest, src, size ); - /* transfer file descriptors to the client process */ - for (i = 0; i < dest->handle.numFds; i++) - dest->handle.data[i] = duplicate_fd( client, src->data[i] ); - return 0; + + return (struct ANativeWindowBuffer *)((char *)ahb - off); } -static native_handle_t *unmap_native_handle( const native_handle_t *src ) +static AHardwareBuffer *ahb_from_anwb( struct native_win_wrapper *win, struct ANativeWindowBuffer *buffer, int *buffer_id, int *generation ) { - const size_t size = offsetof( native_handle_t, data[src->numFds + src->numInts] ); - native_handle_t *dest; - int i; + AHardwareBuffer *ahb; + unsigned int i; - if (!is_in_desktop_process()) + if (!buffer) return NULL; + + ahb = pANativeWindowBuffer_getHardwareBuffer(buffer); + + if (win) { - dest = malloc( size ); - memcpy( dest, src, size ); - /* fetch file descriptors passed from the server process */ - for (i = 0; i < dest->numFds; i++) - wine_server_handle_to_fd( LongToHandle(src->data[i]), GENERIC_READ | SYNCHRONIZE, - &dest->data[i], NULL ); - } - else memcpy( &dest, src->data, sizeof(dest) ); - return dest; -} + for (i = 0; i < NB_CACHED_BUFFERS; ++i) + { + if (win->buffers[i].self != ahb) continue; -static void close_native_handle( native_handle_t *handle ) -{ - int i; + if (buffer_id) *buffer_id = win->buffers[i].buffer_id; + if (generation) *generation = win->buffers[i].generation; + break; + } + } - for (i = 0; i < handle->numFds; i++) close( handle->data[i] ); - free( handle ); + return ahb; } /* insert a buffer index at the head of the LRU list */ @@ -414,8 +363,7 @@ static void insert_buffer_lru( struct native_win_data *win, int index ) win->buffer_lru[0] = index; } -static int register_buffer( struct native_win_data *win, struct ANativeWindowBuffer *buffer, - HANDLE *mapping, int *is_new ) +static int register_buffer( struct native_win_data *win, struct AHardwareBuffer *buffer, int *is_new ) { unsigned int i; @@ -434,27 +382,12 @@ static int register_buffer( struct native_win_data *win, struct ANativeWindowBuf TRACE( "%p %p evicting buffer %p id %d from cache\n", win->hwnd, win->parent, win->buffers[i], i ); - win->buffers[i]->common.decRef( &win->buffers[i]->common ); - if (win->mappings[i]) NtUnmapViewOfSection( GetCurrentProcess(), win->mappings[i] ); + pAHardwareBuffer_release(win->buffers[i]); } win->buffers[i] = buffer; - win->mappings[i] = NULL; - if (mapping) - { - OBJECT_ATTRIBUTES attr; - LARGE_INTEGER size; - SIZE_T count = 0; - size.QuadPart = buffer->stride * buffer->height * 4; - InitializeObjectAttributes( &attr, NULL, OBJ_OPENIF, NULL, NULL ); - NtCreateSection( mapping, - STANDARD_RIGHTS_REQUIRED | SECTION_QUERY | SECTION_MAP_READ | SECTION_MAP_WRITE, - &attr, &size, PAGE_READWRITE, 0, INVALID_HANDLE_VALUE ); - NtMapViewOfSection( *mapping, GetCurrentProcess(), &win->mappings[i], 0, 0, - NULL, &count, ViewShare, 0, PAGE_READONLY ); - } - buffer->common.incRef( &buffer->common ); + pAHardwareBuffer_acquire(buffer); *is_new = 1; TRACE( "%p %p %p -> %d\n", win->hwnd, win->parent, buffer, i ); @@ -470,7 +403,7 @@ static struct ANativeWindowBuffer *get_registered_buffer( struct native_win_data ERR( "unknown buffer %d for %p %p\n", id, win->hwnd, win->parent ); return NULL; } - return win->buffers[id]; + return anwb_from_ahb(win->buffers[id]); } static void release_native_window( struct native_win_data *data ) @@ -480,12 +413,10 @@ static void release_native_window( struct native_win_data *data ) if (data->parent) pANativeWindow_release( data->parent ); for (i = 0; i < NB_CACHED_BUFFERS; i++) { - if (data->buffers[i]) data->buffers[i]->common.decRef( &data->buffers[i]->common ); - if (data->mappings[i]) NtUnmapViewOfSection( GetCurrentProcess(), data->mappings[i] ); + if (data->buffers[i]) pAHardwareBuffer_release(data->buffers[i]); data->buffer_lru[i] = -1; } memset( data->buffers, 0, sizeof(data->buffers) ); - memset( data->mappings, 0, sizeof(data->mappings) ); } static void free_native_win_data( struct native_win_data *data ) @@ -555,93 +486,36 @@ void register_native_window( HWND hwnd, struct ANativeWindow *win, BOOL opengl ) NtQueueApcThread( thread, register_window_callback, (ULONG_PTR)hwnd, (ULONG_PTR)win, opengl ); } -void init_gralloc( const struct hw_module_t *module ) +#define LOAD_FUNCPTR(lib, func) do { \ + const char *err; \ + dlerror(); \ + p##func = dlsym( lib, #func ); \ + if (!p##func) \ + { \ + err = dlerror(); \ + ERR( "can't find symbol %s: %s\n", #func, err ? err : "unknown error" ); \ + abort(); \ + } \ +} while (0) +void init_ahardwarebuffers(void) { - struct hw_device_t *device; - int ret; - - TRACE( "got module %p ver %u.%u id %s name %s author %s\n", - module, module->module_api_version >> 8, module->module_api_version & 0xff, - debugstr_a(module->id), debugstr_a(module->name), debugstr_a(module->author) ); + void *libandroid; - switch (module->module_api_version >> 8) + if (!(libandroid = dlopen( "libandroid.so", RTLD_NOW ))) { - case 0: - gralloc_module = (struct gralloc_module_t *)module; - break; - case 1: - if (!(ret = module->methods->open( module, GRALLOC_HARDWARE_MODULE_ID, &device ))) - { - int32_t caps[64]; - uint32_t i, count = ARRAY_SIZE(caps); - - gralloc1_device = (struct gralloc1_device *)device; - gralloc1_retain = gralloc1_device->getFunction( gralloc1_device, GRALLOC1_FUNCTION_RETAIN ); - gralloc1_release = gralloc1_device->getFunction( gralloc1_device, GRALLOC1_FUNCTION_RELEASE ); - gralloc1_lock = gralloc1_device->getFunction( gralloc1_device, GRALLOC1_FUNCTION_LOCK ); - gralloc1_unlock = gralloc1_device->getFunction( gralloc1_device, GRALLOC1_FUNCTION_UNLOCK ); - TRACE( "got device version %u funcs %p %p %p %p\n", device->version, - gralloc1_retain, gralloc1_release, gralloc1_lock, gralloc1_unlock ); - - gralloc1_device->getCapabilities( gralloc1_device, &count, caps ); - if (count == ARRAY_SIZE(caps)) ERR( "too many gralloc capabilities\n" ); - for (i = 0; i < count; i++) - if (caps[i] < ARRAY_SIZE(gralloc1_caps)) gralloc1_caps[caps[i]] = TRUE; - } - else ERR( "failed to open gralloc err %d\n", ret ); - break; - default: - ERR( "unknown gralloc module version %u\n", module->module_api_version >> 8 ); - break; + const char *err = dlerror(); + ERR( "failed to dlopen libandroid.so: %s\n", err ? err : "unknown error" ); + abort(); } -} -static int gralloc_grab_buffer( struct ANativeWindowBuffer *buffer ) -{ - if (gralloc1_device) - return gralloc1_retain( gralloc1_device, buffer->handle ); - if (gralloc_module) - return gralloc_module->registerBuffer( gralloc_module, buffer->handle ); - return -ENODEV; -} - -static void gralloc_release_buffer( struct ANativeWindowBuffer *buffer ) -{ - if (gralloc1_device) gralloc1_release( gralloc1_device, buffer->handle ); - else if (gralloc_module) gralloc_module->unregisterBuffer( gralloc_module, buffer->handle ); - - if (!gralloc1_caps[GRALLOC1_CAPABILITY_RELEASE_IMPLY_DELETE]) - close_native_handle( (native_handle_t *)buffer->handle ); -} - -static int gralloc_lock( struct ANativeWindowBuffer *buffer, void **bits ) -{ - if (gralloc1_device) - { - gralloc1_rect_t rect = { 0, 0, buffer->width, buffer->height }; - return gralloc1_lock( gralloc1_device, buffer->handle, - GRALLOC1_PRODUCER_USAGE_CPU_READ_OFTEN | - GRALLOC1_PRODUCER_USAGE_CPU_WRITE_OFTEN, - GRALLOC1_CONSUMER_USAGE_NONE, &rect, bits, -1 ); - } - if (gralloc_module) - return gralloc_module->lock( gralloc_module, buffer->handle, - GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN, - 0, 0, buffer->width, buffer->height, bits ); - - *bits = ((struct native_buffer_wrapper *)buffer)->bits; - return 0; -} - -static void gralloc_unlock( struct ANativeWindowBuffer *buffer ) -{ - if (gralloc1_device) - { - int fence; - gralloc1_unlock( gralloc1_device, buffer->handle, &fence ); - wait_fence_and_close( fence ); - } - else if (gralloc_module) gralloc_module->unlock( gralloc_module, buffer->handle ); + LOAD_FUNCPTR( libandroid, AHardwareBuffer_describe ); + LOAD_FUNCPTR( libandroid, AHardwareBuffer_acquire ); + LOAD_FUNCPTR( libandroid, AHardwareBuffer_release ); + LOAD_FUNCPTR( libandroid, AHardwareBuffer_lock ); + LOAD_FUNCPTR( libandroid, AHardwareBuffer_unlock ); + LOAD_FUNCPTR( libandroid, AHardwareBuffer_recvHandleFromUnixSocket ); + LOAD_FUNCPTR( libandroid, AHardwareBuffer_sendHandleToUnixSocket ); + LOAD_FUNCPTR( libandroid, ANativeWindowBuffer_getHardwareBuffer ); } /* get the capture window stored in the desktop process */ @@ -805,53 +679,97 @@ static NTSTATUS dequeueBuffer_ioctl( void *data, DWORD in_size, DWORD out_size, struct ioctl_android_dequeueBuffer *res = data; struct native_win_data *win_data; struct ANativeWindowBuffer *buffer = NULL; + AHardwareBuffer *ahb = NULL; int fence, ret, is_new; if (out_size < sizeof( *res )) return STATUS_BUFFER_OVERFLOW; - - if (in_size < offsetof( struct ioctl_android_dequeueBuffer, native_handle )) - return STATUS_INVALID_PARAMETER; + if (in_size < sizeof(res->hdr)) return STATUS_INVALID_PARAMETER; if (!(win_data = get_ioctl_native_win_data( &res->hdr ))) return STATUS_INVALID_HANDLE; if (!(parent = win_data->parent)) return STATUS_DEVICE_NOT_READY; - *ret_size = offsetof( struct ioctl_android_dequeueBuffer, native_handle ); + res->handle = 0; + res->buffer_id = -1; + res->generation = 0; + *ret_size = sizeof(*res); + wrap_java_call(); ret = parent->dequeueBuffer( parent, &buffer, &fence ); unwrap_java_call(); - if (!ret) + if (ret) + { + ERR( "%08x failed %d\n", res->hdr.hwnd, ret ); + return android_error_to_status( ret ); + } + + if (!buffer) + { + TRACE( "got invalid buffer\n" ); + return STATUS_UNSUCCESSFUL; + } + + TRACE( "%08x got buffer %p fence %d\n", res->hdr.hwnd, buffer, fence ); + ahb = pANativeWindowBuffer_getHardwareBuffer( buffer ); + + res->buffer_id = register_buffer( win_data, ahb, &is_new ); + res->generation = win_data->generation; + res->handle = 0; + + if (is_new) { - HANDLE mapping = 0; + int sv[2] = { -1, -1 }; + HANDLE local = 0; + OBJECT_ATTRIBUTES attr = { .Length = sizeof(attr) }; + CLIENT_ID cid = { .UniqueProcess = UlongToHandle( current_client_id() ) }; + HANDLE process; - if (!buffer) + if (!ahb) { - TRACE( "got invalid buffer\n" ); + wait_fence_and_close( fence ); return STATUS_UNSUCCESSFUL; } - TRACE( "%08x got buffer %p fence %d\n", res->hdr.hwnd, buffer, fence ); - res->width = buffer->width; - res->height = buffer->height; - res->stride = buffer->stride; - res->format = buffer->format; - res->usage = buffer->usage; - res->buffer_id = register_buffer( win_data, buffer, res->win32 ? &mapping : NULL, &is_new ); - res->generation = win_data->generation; - if (is_new) + if (socketpair( AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv ) < 0) { - OBJECT_ATTRIBUTES attr = { .Length = sizeof(attr) }; - CLIENT_ID cid = { .UniqueProcess = UlongToHandle( current_client_id() ) }; - HANDLE process; - NtOpenProcess( &process, PROCESS_DUP_HANDLE, &attr, &cid ); - map_native_handle( &res->native_handle, buffer->handle, mapping, process ); - NtClose( process ); - *ret_size = sizeof( *res ); + wait_fence_and_close( fence ); + return STATUS_UNSUCCESSFUL; + } + + if (NtOpenProcess( &process, PROCESS_DUP_HANDLE, &attr, &cid )) + { + close( sv[0] ); + close( sv[1] ); + wait_fence_and_close( fence ); + return STATUS_UNSUCCESSFUL; + } + + if (!wine_server_fd_to_handle( dup(sv[1]), GENERIC_READ | SYNCHRONIZE, 0, &local )) + { + NTSTATUS status = NtDuplicateObject( GetCurrentProcess(), local, process, &res->handle, 0, 0, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE ); + if (status && local) NtClose( local ); + } + + NtClose( process ); + close( sv[1] ); + + if (!res->handle) + { + close( sv[0] ); + wait_fence_and_close( fence ); + return STATUS_UNSUCCESSFUL; + } + + ret = pAHardwareBuffer_sendHandleToUnixSocket( ahb, sv[0] ); + close( sv[0] ); + if (ret) + { + wait_fence_and_close( fence ); + return android_error_to_status( ret ); } - wait_fence_and_close( fence ); - return STATUS_SUCCESS; } - ERR( "%08x failed %d\n", res->hdr.hwnd, ret ); - return android_error_to_status( ret ); + + wait_fence_and_close( fence ); + return STATUS_SUCCESS; } static NTSTATUS cancelBuffer_ioctl( void *data, DWORD in_size, DWORD out_size, ULONG_PTR *ret_size ) @@ -893,15 +811,7 @@ static NTSTATUS queueBuffer_ioctl( void *data, DWORD in_size, DWORD out_size, UL if (!(buffer = get_registered_buffer( win_data, res->buffer_id ))) return STATUS_INVALID_HANDLE; - TRACE( "%08x buffer %p mapping %p\n", res->hdr.hwnd, buffer, win_data->mappings[res->buffer_id] ); - if (win_data->mappings[res->buffer_id]) - { - void *bits; - ret = gralloc_lock( buffer, &bits ); - if (ret) return android_error_to_status( ret ); - memcpy( bits, win_data->mappings[res->buffer_id], buffer->stride * buffer->height * 4 ); - gralloc_unlock( buffer ); - } + TRACE( "%08x buffer %p\n", res->hdr.hwnd, buffer ); wrap_java_call(); ret = parent->queueBuffer( parent, buffer, -1 ); unwrap_java_call(); @@ -1239,98 +1149,72 @@ static void win_decRef( struct android_native_base_t *base ) InterlockedDecrement( &win->ref ); } -static void buffer_incRef( struct android_native_base_t *base ) -{ - struct native_buffer_wrapper *buffer = (struct native_buffer_wrapper *)base; - InterlockedIncrement( &buffer->ref ); -} - -static void buffer_decRef( struct android_native_base_t *base ) -{ - struct native_buffer_wrapper *buffer = (struct native_buffer_wrapper *)base; - - if (!InterlockedDecrement( &buffer->ref )) - { - if (!is_in_desktop_process()) gralloc_release_buffer( &buffer->buffer ); - if (buffer->bits) NtUnmapViewOfSection( GetCurrentProcess(), buffer->bits ); - free( buffer ); - } -} - static int dequeueBuffer( struct ANativeWindow *window, struct ANativeWindowBuffer **buffer, int *fence ) { struct native_win_wrapper *win = (struct native_win_wrapper *)window; struct ioctl_android_dequeueBuffer res = {0}; DWORD size = sizeof(res); - int ret, use_win32 = !gralloc_module && !gralloc1_device; + int ret; res.hdr.hwnd = HandleToLong( win->hwnd ); res.hdr.opengl = win->opengl; - res.win32 = use_win32; - ret = android_ioctl( IOCTL_DEQUEUE_BUFFER, - &res, offsetof( struct ioctl_android_dequeueBuffer, native_handle ), - &res, &size ); + res.handle = 0; + res.buffer_id = -1; + res.generation = 0; + + ret = android_ioctl( IOCTL_DEQUEUE_BUFFER, &res, sizeof(res.hdr), &res, &size ); if (ret) return ret; + if (size < sizeof(res)) return -EINVAL; + + if (res.buffer_id < 0 || res.buffer_id >= NB_CACHED_BUFFERS) return -EINVAL; - /* if we received the native handle, this is a new buffer */ - if (size > offsetof( struct ioctl_android_dequeueBuffer, native_handle )) + if (res.handle) { - struct native_buffer_wrapper *buf = calloc( 1, sizeof(*buf) ); - - buf->buffer.common.magic = ANDROID_NATIVE_BUFFER_MAGIC; - buf->buffer.common.version = sizeof( buf->buffer ); - buf->buffer.common.incRef = buffer_incRef; - buf->buffer.common.decRef = buffer_decRef; - buf->buffer.width = res.width; - buf->buffer.height = res.height; - buf->buffer.stride = res.stride; - buf->buffer.format = res.format; - buf->buffer.usage = res.usage; - buf->buffer.handle = unmap_native_handle( &res.native_handle.handle ); - buf->ref = 1; - buf->hwnd = win->hwnd; - buf->buffer_id = res.buffer_id; - buf->generation = res.generation; - if (win->buffers[res.buffer_id]) - win->buffers[res.buffer_id]->buffer.common.decRef(&win->buffers[res.buffer_id]->buffer.common); - win->buffers[res.buffer_id] = buf; - - if (use_win32) - { - LARGE_INTEGER zero = { .QuadPart = 0 }; - SIZE_T count = 0; - HANDLE mapping = LongToHandle( res.native_handle.handle.data[0] ); - NtMapViewOfSection( mapping, GetCurrentProcess(), &buf->bits, 0, 0, &zero, &count, - ViewShare, 0, PAGE_READWRITE ); - NtClose( mapping ); - } - else if (!is_in_desktop_process()) + AHardwareBuffer *ahb = NULL; + int fd; + + if (wine_server_handle_to_fd( res.handle, GENERIC_READ | SYNCHRONIZE, &fd, NULL )) { - if ((ret = gralloc_grab_buffer( &buf->buffer )) < 0) - WARN( "hwnd %p, buffer %p failed to register %d %s\n", - win->hwnd, &buf->buffer, ret, strerror(-ret) ); + NtClose( res.handle ); + return -EINVAL; } + NtClose( res.handle ); + + ret = pAHardwareBuffer_recvHandleFromUnixSocket( fd, &ahb ); + close( fd ); + if (ret) return ret; + + if (win->buffers[res.buffer_id].self) + pAHardwareBuffer_release( win->buffers[res.buffer_id].self ); + + win->buffers[res.buffer_id].self = ahb; + win->buffers[res.buffer_id].buffer_id = res.buffer_id; + win->buffers[res.buffer_id].generation = res.generation; } - *buffer = &win->buffers[res.buffer_id]->buffer; + if (!win->buffers[res.buffer_id].self) return -EINVAL; + + *buffer = anwb_from_ahb(win->buffers[res.buffer_id].self); *fence = -1; - TRACE( "hwnd %p, buffer %p %dx%d stride %d fmt %d usage %d fence %d\n", - win->hwnd, *buffer, res.width, res.height, res.stride, res.format, res.usage, *fence ); + TRACE( "hwnd %p, buffer %p id %d gen %d fence %d\n", + win->hwnd, *buffer, res.buffer_id, res.generation, *fence ); return 0; } static int cancelBuffer( struct ANativeWindow *window, struct ANativeWindowBuffer *buffer, int fence ) { struct native_win_wrapper *win = (struct native_win_wrapper *)window; - struct native_buffer_wrapper *buf = (struct native_buffer_wrapper *)buffer; struct ioctl_android_cancelBuffer cancel; - TRACE( "hwnd %p buffer %p %dx%d stride %d fmt %d usage %d fence %d\n", - win->hwnd, buffer, buffer->width, buffer->height, - buffer->stride, buffer->format, buffer->usage, fence ); - cancel.buffer_id = buf->buffer_id; - cancel.generation = buf->generation; + TRACE( "hwnd %p buffer %p fence %d\n", win->hwnd, buffer, fence ); + + if (!ahb_from_anwb( win, buffer, &cancel.buffer_id, &cancel.generation )) + { + wait_fence_and_close( fence ); + return -EINVAL; + } + cancel.hdr.hwnd = HandleToLong( win->hwnd ); cancel.hdr.opengl = win->opengl; wait_fence_and_close( fence ); @@ -1340,14 +1224,16 @@ static int cancelBuffer( struct ANativeWindow *window, struct ANativeWindowBuffe static int queueBuffer( struct ANativeWindow *window, struct ANativeWindowBuffer *buffer, int fence ) { struct native_win_wrapper *win = (struct native_win_wrapper *)window; - struct native_buffer_wrapper *buf = (struct native_buffer_wrapper *)buffer; struct ioctl_android_queueBuffer queue; - TRACE( "hwnd %p buffer %p %dx%d stride %d fmt %d usage %d fence %d\n", - win->hwnd, buffer, buffer->width, buffer->height, - buffer->stride, buffer->format, buffer->usage, fence ); - queue.buffer_id = buf->buffer_id; - queue.generation = buf->generation; + TRACE( "hwnd %p buffer %p fence %d\n", win->hwnd, buffer, fence ); + + if (!ahb_from_anwb( win, buffer, &queue.buffer_id, &queue.generation )) + { + wait_fence_and_close( fence ); + return -EINVAL; + } + queue.hdr.hwnd = HandleToLong( win->hwnd ); queue.hdr.opengl = win->opengl; wait_fence_and_close( fence ); @@ -1476,6 +1362,7 @@ static int perform( ANativeWindow *window, int operation, ... ) { struct ANativeWindowBuffer *buffer = NULL; struct ANativeWindow_Buffer *buffer_ret = va_arg( args, ANativeWindow_Buffer * ); + struct AHardwareBuffer* b = NULL; ARect *bounds = va_arg( args, ARect * ); int ret = window->dequeueBuffer_DEPRECATED( window, &buffer ); if (!ret && !buffer) @@ -1485,25 +1372,31 @@ static int perform( ANativeWindow *window, int operation, ... ) } if (!ret) { - if ((ret = gralloc_lock( buffer, &buffer_ret->bits ))) + if (!(b = ahb_from_anwb((struct native_win_wrapper*) window, buffer, NULL, NULL))) { + ret = -EINVAL; + } + + if (b && (ret = pAHardwareBuffer_lock( b, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, -1, NULL, &buffer_ret->bits ))) { - WARN( "gralloc->lock %p failed %d %s\n", win->hwnd, ret, strerror(-ret) ); + WARN( "AHardwareBuffer_lock %p failed %d %s\n", win->hwnd, ret, strerror(-ret) ); window->cancelBuffer( window, buffer, -1 ); } } if (!ret) { - buffer_ret->width = buffer->width; - buffer_ret->height = buffer->height; - buffer_ret->stride = buffer->stride; - buffer_ret->format = buffer->format; - win->locked_buffer = buffer; + AHardwareBuffer_Desc d = {0}; + pAHardwareBuffer_describe(b, &d); + buffer_ret->width = d.width; + buffer_ret->height = d.height; + buffer_ret->stride = d.stride; + buffer_ret->format = d.format; + win->locked_buffer = b; if (bounds) { bounds->left = 0; bounds->top = 0; - bounds->right = buffer->width; - bounds->bottom = buffer->height; + bounds->right = d.width; + bounds->bottom = d.height; } } va_end( args ); @@ -1515,8 +1408,8 @@ static int perform( ANativeWindow *window, int operation, ... ) int ret = -EINVAL; if (win->locked_buffer) { - gralloc_unlock( win->locked_buffer ); - ret = window->queueBuffer( window, win->locked_buffer, -1 ); + pAHardwareBuffer_unlock(win->locked_buffer, NULL); + ret = window->queueBuffer( window, anwb_from_ahb(win->locked_buffer), -1 ); win->locked_buffer = NULL; } va_end( args ); @@ -1589,7 +1482,7 @@ void release_ioctl_window( struct ANativeWindow *window ) TRACE( "%p %p\n", win, win->hwnd ); for (i = 0; i < ARRAY_SIZE( win->buffers ); i++) - if (win->buffers[i]) win->buffers[i]->buffer.common.decRef( &win->buffers[i]->buffer.common ); + if (win->buffers[i].self) pAHardwareBuffer_release(win->buffers[i].self); destroy_ioctl_window( win->hwnd, win->opengl ); free( win ); diff --git a/dlls/wineandroid.drv/init.c b/dlls/wineandroid.drv/init.c index 01c9671b68f..0adda2ad0ad 100644 --- a/dlls/wineandroid.drv/init.c +++ b/dlls/wineandroid.drv/init.c @@ -341,7 +341,6 @@ static const JNINativeMethod methods[] = DECL_FUNCPTR( __android_log_print ); DECL_FUNCPTR( ANativeWindow_fromSurface ); DECL_FUNCPTR( ANativeWindow_release ); -DECL_FUNCPTR( hw_get_module ); #ifndef DT_GNU_HASH #define DT_GNU_HASH 0x6ffffef5 @@ -440,55 +439,6 @@ static void *find_symbol( const struct dl_phdr_info* info, const char *var, int return NULL; } -static int enum_libs( struct dl_phdr_info* info, size_t size, void* data ) -{ - const char *p; - - if (!info->dlpi_name) return 0; - if (!(p = strrchr( info->dlpi_name, '/' ))) return 0; - if (strcmp( p, "/libhardware.so" )) return 0; - TRACE( "found libhardware at %p\n", info->dlpi_phdr ); - phw_get_module = find_symbol( info, "hw_get_module", STT_FUNC ); - return 1; -} - -static void load_hardware_libs(void) -{ - const struct hw_module_t *module; - int ret; - void *libhardware; - - if ((libhardware = dlopen( "libhardware.so", RTLD_GLOBAL ))) - { - LOAD_FUNCPTR( libhardware, hw_get_module ); - } - else - { - /* Android >= N disallows loading libhardware, so we load libandroid (which imports - * libhardware), and then we can find libhardware in the list of loaded libraries. - */ - if (!dlopen( "libandroid.so", RTLD_GLOBAL )) - { - ERR( "failed to load libandroid.so: %s\n", dlerror() ); - return; - } - dl_iterate_phdr( enum_libs, 0 ); - if (!phw_get_module) - { - ERR( "failed to find hw_get_module\n" ); - return; - } - } - - if ((ret = phw_get_module( GRALLOC_HARDWARE_MODULE_ID, &module ))) - { - ERR( "failed to load gralloc module err %d\n", ret ); - return; - } - - init_gralloc( module ); -} - static void load_android_libs(void) { void *libandroid, *liblog; @@ -533,7 +483,7 @@ static HRESULT android_init( void *arg ) object = *p_java_object; - load_hardware_libs(); + init_ahardwarebuffers(); pthread_mutexattr_init( &attr ); pthread_mutexattr_settype( &attr, PTHREAD_MUTEX_RECURSIVE ); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10519
With this change, Wine installs and runs on Android 8.0 and 9.0. Stability is still somewhat limited, but that does not appear to be directly related to this change. On Android 10.0 and newer, there are indications that the current `ANativeWindow` proxy is missing some required functionality. As a follow-up, I plan to remove the proxy and instead use `AImageReader` as the ANativeWindow implementation. This should avoid reliance on private Android APIs, which may vary across vendors. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10519#note_134462
Seems like I found the reasons for Android 10.0 crashes. 1. I used `compileSdkVersion 36`. For new apps it enforces W^X restriction so you can not simply execute anything from app's private storage. 2. `signal 31 (SIGSYS), code 1 (SYS_SECCOMP), fault addr ... Cause: seccomp prevented call to disallowed x86 system call 123` (triggered by `modify_ldt`). Explorer runs in zygote-spawned process so it inherits seccomp restrictions. It does not happen in termux because there it runs separately, in non JVM process. Seems like we should find some other workaround for this, probably it will mean separating Activity process and `explorer.exe` process. And also seems like it will result in implementing my own microprotocol and transport for that. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10519#note_134541
participants (2)
-
Twaik Yont -
Twaik Yont (@twaik)