From: Rémi Bernon <rbernon@codeweavers.com> --- dlls/mfsrcsnk/media_source.c | 88 +++---- dlls/winegstreamer/Makefile.in | 1 + dlls/winegstreamer/gst_private.h | 1 + dlls/winegstreamer/main.c | 2 +- dlls/winegstreamer/winedmo.c | 324 ++++++++++++++++++++++++++ dlls/winegstreamer/winegstreamer.spec | 9 + 6 files changed, 374 insertions(+), 51 deletions(-) create mode 100644 dlls/winegstreamer/winedmo.c diff --git a/dlls/mfsrcsnk/media_source.c b/dlls/mfsrcsnk/media_source.c index b39f4b3b125..53bb00fb64a 100644 --- a/dlls/mfsrcsnk/media_source.c +++ b/dlls/mfsrcsnk/media_source.c @@ -40,7 +40,7 @@ struct winedmo_demuxer_funcs GUID *major, union winedmo_format **format); }; -static const struct winedmo_demuxer_funcs winedmo_funcs = +static struct winedmo_demuxer_funcs winedmo_funcs = { .p_check = winedmo_demuxer_check, .p_create = winedmo_demuxer_create, @@ -52,28 +52,51 @@ static const struct winedmo_demuxer_funcs winedmo_funcs = .p_stream_type = winedmo_demuxer_stream_type, }; -static BOOL use_gst_byte_stream_handler(void) +static NTSTATUS winedmo_unsupported_check(const char *mime_type) +{ + return STATUS_UNSUCCESSFUL; +} + +static BOOL load_winedmo_demuxer_funcs(const WCHAR *name, struct winedmo_demuxer_funcs *funcs) +{ + HMODULE module; + + if (!(module = LoadLibraryW(name))) + { + funcs->p_check = winedmo_unsupported_check; + return FALSE; + } + funcs->p_check = (void *)GetProcAddress(module, "winedmo_demuxer_check"); + funcs->p_create = (void *)GetProcAddress(module, "winedmo_demuxer_create"); + funcs->p_destroy = (void *)GetProcAddress(module, "winedmo_demuxer_destroy"); + funcs->p_read = (void *)GetProcAddress(module, "winedmo_demuxer_read"); + funcs->p_seek = (void *)GetProcAddress(module, "winedmo_demuxer_seek"); + funcs->p_stream_lang = (void *)GetProcAddress(module, "winedmo_demuxer_stream_lang"); + funcs->p_stream_name = (void *)GetProcAddress(module, "winedmo_demuxer_stream_name"); + funcs->p_stream_type = (void *)GetProcAddress(module, "winedmo_demuxer_stream_type"); + return TRUE; +} + +static BOOL CALLBACK init_dynamic_funcs(INIT_ONCE *once, void *param, void **ctx) { BOOL result; DWORD size = sizeof(result); /* @@ Wine registry key: HKCU\Software\Wine\MediaFoundation */ - if (!RegGetValueW(HKEY_CURRENT_USER, L"Software\\Wine\\MediaFoundation", L"DisableGstByteStreamHandler", - RRF_RT_REG_DWORD, NULL, &result, &size)) - return !result; + if (RegGetValueW(HKEY_CURRENT_USER, L"Software\\Wine\\MediaFoundation", L"DisableGstByteStreamHandler", + RRF_RT_REG_DWORD, NULL, &result, &size ) || !result) + load_winedmo_demuxer_funcs(L"winegstreamer", &winedmo_funcs); return TRUE; } static const struct winedmo_demuxer_funcs *get_winedmo_demuxer_funcs(const char *mime_type) { - if (use_gst_byte_stream_handler()) - return NULL; + static INIT_ONCE once = INIT_ONCE_STATIC_INIT; - if (!winedmo_funcs.p_check(mime_type)) - return &winedmo_funcs; + InitOnceExecuteOnce(&once, init_dynamic_funcs, NULL, NULL); - return NULL; + return &winedmo_funcs; } #define DEFINE_MF_ASYNC_PARAMS(type) \ @@ -2081,14 +2104,7 @@ static HRESULT byte_stream_plugin_create(const struct winedmo_demuxer_funcs *fun static HRESULT WINAPI asf_byte_stream_plugin_factory_CreateInstance(IClassFactory *iface, IUnknown *outer, REFIID riid, void **out) { - const struct winedmo_demuxer_funcs *funcs; - - if (!(funcs = get_winedmo_demuxer_funcs("video/x-ms-asf")) || use_gst_byte_stream_handler()) - { - static const GUID CLSID_GStreamerByteStreamHandler = {0x317df618,0x5e5a,0x468a,{0x9f,0x15,0xd8,0x27,0xa9,0xa0,0x81,0x62}}; - return CoCreateInstance(&CLSID_GStreamerByteStreamHandler, outer, CLSCTX_INPROC_SERVER, riid, out); - } - + const struct winedmo_demuxer_funcs *funcs = get_winedmo_demuxer_funcs("video/x-ms-asf"); return byte_stream_plugin_create(funcs, outer, riid, out); } @@ -2106,14 +2122,7 @@ IClassFactory asf_byte_stream_plugin_factory = {&asf_byte_stream_plugin_factory_ static HRESULT WINAPI avi_byte_stream_plugin_factory_CreateInstance(IClassFactory *iface, IUnknown *outer, REFIID riid, void **out) { - const struct winedmo_demuxer_funcs *funcs; - - if (!(funcs = get_winedmo_demuxer_funcs("video/avi")) || use_gst_byte_stream_handler()) - { - static const GUID CLSID_GStreamerByteStreamHandler = {0x317df618,0x5e5a,0x468a,{0x9f,0x15,0xd8,0x27,0xa9,0xa0,0x81,0x62}}; - return CoCreateInstance(&CLSID_GStreamerByteStreamHandler, outer, CLSCTX_INPROC_SERVER, riid, out); - } - + const struct winedmo_demuxer_funcs *funcs = get_winedmo_demuxer_funcs("video/avi"); return byte_stream_plugin_create(funcs, outer, riid, out); } @@ -2131,14 +2140,7 @@ IClassFactory avi_byte_stream_plugin_factory = {&avi_byte_stream_plugin_factory_ static HRESULT WINAPI mpeg4_byte_stream_plugin_factory_CreateInstance(IClassFactory *iface, IUnknown *outer, REFIID riid, void **out) { - const struct winedmo_demuxer_funcs *funcs; - - if (!(funcs = get_winedmo_demuxer_funcs("video/mp4")) || use_gst_byte_stream_handler()) - { - static const GUID CLSID_GStreamerByteStreamHandler = {0x317df618,0x5e5a,0x468a,{0x9f,0x15,0xd8,0x27,0xa9,0xa0,0x81,0x62}}; - return CoCreateInstance(&CLSID_GStreamerByteStreamHandler, outer, CLSCTX_INPROC_SERVER, riid, out); - } - + const struct winedmo_demuxer_funcs *funcs = get_winedmo_demuxer_funcs("video/mp4"); return byte_stream_plugin_create(funcs, outer, riid, out); } @@ -2156,14 +2158,7 @@ IClassFactory mpeg4_byte_stream_plugin_factory = {&mpeg4_byte_stream_plugin_fact static HRESULT WINAPI wav_byte_stream_plugin_factory_CreateInstance(IClassFactory *iface, IUnknown *outer, REFIID riid, void **out) { - const struct winedmo_demuxer_funcs *funcs; - - if (!(funcs = get_winedmo_demuxer_funcs("audio/wav")) || use_gst_byte_stream_handler()) - { - static const GUID CLSID_GStreamerByteStreamHandler = {0x317df618,0x5e5a,0x468a,{0x9f,0x15,0xd8,0x27,0xa9,0xa0,0x81,0x62}}; - return CoCreateInstance(&CLSID_GStreamerByteStreamHandler, outer, CLSCTX_INPROC_SERVER, riid, out); - } - + const struct winedmo_demuxer_funcs *funcs = get_winedmo_demuxer_funcs("audio/wav"); return byte_stream_plugin_create(funcs, outer, riid, out); } @@ -2181,14 +2176,7 @@ IClassFactory wav_byte_stream_plugin_factory = {&wav_byte_stream_plugin_factory_ static HRESULT WINAPI mp3_byte_stream_plugin_factory_CreateInstance(IClassFactory *iface, IUnknown *outer, REFIID riid, void **out) { - const struct winedmo_demuxer_funcs *funcs; - - if (!(funcs = get_winedmo_demuxer_funcs("audio/mp3")) || use_gst_byte_stream_handler()) - { - static const GUID CLSID_GStreamerByteStreamHandler = {0x317df618,0x5e5a,0x468a,{0x9f,0x15,0xd8,0x27,0xa9,0xa0,0x81,0x62}}; - return CoCreateInstance(&CLSID_GStreamerByteStreamHandler, outer, CLSCTX_INPROC_SERVER, riid, out); - } - + const struct winedmo_demuxer_funcs *funcs = get_winedmo_demuxer_funcs("audio/mp3"); return byte_stream_plugin_create(funcs, outer, riid, out); } diff --git a/dlls/winegstreamer/Makefile.in b/dlls/winegstreamer/Makefile.in index 63ca3f61fdf..26f83901e4e 100644 --- a/dlls/winegstreamer/Makefile.in +++ b/dlls/winegstreamer/Makefile.in @@ -28,6 +28,7 @@ SOURCES = \ wg_parser.c \ wg_sample.c \ wg_transform.c \ + winedmo.c \ winegstreamer_classes.idl \ wm_reader.c \ wma_decoder.c diff --git a/dlls/winegstreamer/gst_private.h b/dlls/winegstreamer/gst_private.h index 84b7db4a14c..62f2aa71279 100644 --- a/dlls/winegstreamer/gst_private.h +++ b/dlls/winegstreamer/gst_private.h @@ -152,6 +152,7 @@ extern HRESULT mfplat_get_class_object(REFCLSID rclsid, REFIID riid, void **obj) IMFMediaType *mf_media_type_from_wg_format(const struct wg_format *format); void mf_media_type_to_wg_format(IMFMediaType *type, struct wg_format *format); +HRESULT wg_media_type_from_mf(IMFMediaType *media_type, struct wg_media_type *wg_media_type); HRESULT wg_sample_create_mf(IMFSample *sample, struct wg_sample **out); HRESULT wg_sample_create_quartz(IMediaSample *sample, struct wg_sample **out); diff --git a/dlls/winegstreamer/main.c b/dlls/winegstreamer/main.c index 7ff79bda4ab..4a13436b8d4 100644 --- a/dlls/winegstreamer/main.c +++ b/dlls/winegstreamer/main.c @@ -108,7 +108,7 @@ static HRESULT video_format_from_media_type(IMFMediaType *media_type, MFVIDEOFOR return hr; } -static HRESULT wg_media_type_from_mf(IMFMediaType *media_type, struct wg_media_type *wg_media_type) +HRESULT wg_media_type_from_mf(IMFMediaType *media_type, struct wg_media_type *wg_media_type) { HRESULT hr; diff --git a/dlls/winegstreamer/winedmo.c b/dlls/winegstreamer/winedmo.c new file mode 100644 index 00000000000..92bcb5327d5 --- /dev/null +++ b/dlls/winegstreamer/winedmo.c @@ -0,0 +1,324 @@ +/* + * Copyright 2025 Rémi Bernon for CodeWeavers + * + * 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 + */ + +#include <stddef.h> +#include <stdarg.h> + +#include "ntstatus.h" +#define WIN32_NO_STATUS +#include "windef.h" +#include "winbase.h" + +#include "gst_private.h" + +#include "wine/winedmo.h" +#include "wine/debug.h" +#include "wine/list.h" + +WINE_DEFAULT_DEBUG_CHANNEL(dmo); + +struct demuxer +{ + struct winedmo_stream *stream; + UINT64 stream_size; + wg_parser_t wg_parser; + HANDLE thread; + LONG shutdown; +}; + +static struct demuxer *demuxer_from_handle(struct winedmo_demuxer handle) +{ + return (struct demuxer *)(UINT_PTR)handle.handle; +} + +static DWORD CALLBACK read_thread(void *arg) +{ + struct demuxer *demuxer = arg; + QWORD file_size = demuxer->stream_size; + size_t buffer_size = 4096; + void *data; + + if (!(data = malloc(buffer_size))) + return 0; + + TRACE("Starting read thread for demuxer %p.\n", demuxer); + + while (!ReadAcquire(&demuxer->shutdown)) + { + uint64_t offset; + ULONG ret_size; + uint32_t size; + HRESULT hr; + + if (!wg_parser_get_next_read_offset(demuxer->wg_parser, &offset, &size)) + continue; + + if (offset >= file_size) + size = 0; + else if (offset + size >= file_size) + size = file_size - offset; + + /* Some IMFByteStreams (including the standard file-based stream) return + * an error when reading past the file size. */ + if (!size) + { + wg_parser_push_data(demuxer->wg_parser, data, 0); + continue; + } + + if (!array_reserve(&data, &buffer_size, size, 1)) + { + free(data); + return 0; + } + + ret_size = size; + if (FAILED(hr = HRESULT_FROM_NT(demuxer->stream->p_seek(demuxer->stream, &offset))) + || FAILED(hr = HRESULT_FROM_NT(demuxer->stream->p_read(demuxer->stream, data, &ret_size)))) + ERR("Failed to read %u bytes at offset %I64u, hr %#lx.\n", size, offset, hr); + if (ret_size != size) + ERR("Unexpected short read: requested %u bytes, got %lu.\n", size, ret_size); + wg_parser_push_data(demuxer->wg_parser, SUCCEEDED(hr) ? data : NULL, ret_size); + } + + free(data); + TRACE("Media source is shutting down; exiting.\n"); + return 0; +} + +NTSTATUS CDECL winedmo_demuxer_check(const char *mime_type) +{ + return STATUS_SUCCESS; +} + +NTSTATUS CDECL winedmo_demuxer_create(const WCHAR *url, struct winedmo_stream *stream, UINT64 stream_size, + INT64 *duration, UINT *stream_count, WCHAR *mime_type, struct winedmo_demuxer *demuxer) +{ + struct demuxer *object; + + TRACE("url %s, stream %p, stream_size %#I64x, mime_type %p, demuxer %p\n", debugstr_w(url), + stream, stream_size, mime_type, demuxer); + + if (!init_gstreamer()) + return STATUS_UNSUCCESSFUL; + + if (!(object = calloc(1, sizeof(*object)))) + return STATUS_NO_MEMORY; + object->stream = stream; + object->stream_size = stream_size; + + if (!(object->wg_parser = wg_parser_create(TRUE))) + goto failed; + if (!(object->thread = CreateThread(NULL, 0, read_thread, object, 0, NULL))) + goto failed; + if (FAILED(wg_parser_connect(object->wg_parser, stream_size, url))) + goto failed; + + *duration = 0; + *stream_count = wg_parser_get_stream_count(object->wg_parser); + wcscpy(mime_type, L"video/x-application"); + + for (UINT i = 0; i < *stream_count; i++) + { + wg_parser_stream_t wg_stream = wg_parser_get_stream(object->wg_parser, i); + *duration = max(*duration, wg_parser_stream_get_duration(wg_stream)); + } + + demuxer->handle = (UINT_PTR)object; + TRACE("created demuxer %#I64x, stream %p, duration %I64d, stream_count %u, mime_type %s\n", + demuxer->handle, stream, *duration, *stream_count, debugstr_w(mime_type)); + return STATUS_SUCCESS; + +failed: + if (object->thread) + { + WriteRelease(&object->shutdown, 1); + WaitForSingleObject(object->thread, INFINITE); + CloseHandle(object->thread); + } + + wg_parser_destroy(object->wg_parser); + free(object); + return STATUS_UNSUCCESSFUL; +} + +NTSTATUS CDECL winedmo_demuxer_destroy(struct winedmo_demuxer *demuxer) +{ + struct demuxer *object = demuxer_from_handle(*demuxer); + + if (!object) + return STATUS_SUCCESS; + + TRACE("demuxer %#I64x\n", demuxer->handle); + + wg_parser_disconnect(object->wg_parser); + + WriteRelease(&object->shutdown, 1); + WaitForSingleObject(object->thread, INFINITE); + CloseHandle(object->thread); + + wg_parser_destroy(object->wg_parser); + free(object); + + demuxer->handle = 0; + return STATUS_SUCCESS; +} + +static void buffer_lock( DMO_OUTPUT_DATA_BUFFER *buffer, BYTE **data, DWORD *size ) +{ + HRESULT hr; + + if (FAILED(hr = IMediaBuffer_GetBufferAndLength( buffer->pBuffer, data, size ))) + ERR( "Failed to get media buffer data %p, hr %#lx\n", buffer, hr ); + if (FAILED(hr = IMediaBuffer_GetMaxLength( buffer->pBuffer, size ))) + ERR( "Failed to get media buffer max length %p, hr %#lx\n", buffer, hr ); +} + +static void buffer_unlock( DMO_OUTPUT_DATA_BUFFER *buffer, struct wg_parser_buffer *sample ) +{ + IMFSample *object; + HRESULT hr; + + if (FAILED(hr = IMediaBuffer_SetLength( buffer->pBuffer, sample->size ))) + ERR( "Failed to update buffer length, hr %#lx\n", hr ); + + buffer->dwStatus = 0; + if (SUCCEEDED(hr = IMediaBuffer_QueryInterface( buffer->pBuffer, &IID_IMFSample, (void **)&object ))) + { + if (sample->has_pts) IMFSample_SetSampleTime( object, sample->pts ); + if (sample->has_duration) IMFSample_SetSampleDuration( object, sample->duration ); + if (!sample->delta) IMFSample_SetUINT32( object, &MFSampleExtension_CleanPoint, 1 ); + IMFSample_Release( object ); + } + + if ((buffer->rtTimestamp = sample->pts) != INT64_MIN) buffer->dwStatus |= DMO_OUTPUT_DATA_BUFFERF_TIME; + if ((buffer->rtTimelength = sample->duration) != INT64_MIN) buffer->dwStatus |= DMO_OUTPUT_DATA_BUFFERF_TIMELENGTH; + if (!sample->delta) buffer->dwStatus |= DMO_OUTPUT_DATA_BUFFERF_SYNCPOINT; +} + +NTSTATUS CDECL winedmo_demuxer_read(struct winedmo_demuxer demuxer, UINT *stream, DMO_OUTPUT_DATA_BUFFER *buffer, UINT *buffer_size) +{ + struct demuxer *object = demuxer_from_handle(demuxer); + struct wg_parser_buffer wg_buffer; + NTSTATUS status = STATUS_SUCCESS; + wg_parser_stream_t wg_stream; + BYTE *data; + DWORD size; + + TRACE("demuxer %#I64x, stream %p, buffer %p, buffer_size %p\n", demuxer.handle, stream, buffer, buffer_size); + + if (!wg_parser_stream_get_buffer(object->wg_parser, 0, &wg_buffer)) + return STATUS_END_OF_FILE; + *buffer_size = wg_buffer.size; + *stream = wg_buffer.stream; + + if (SUCCEEDED(IMediaBuffer_GetMaxLength( buffer->pBuffer, &size )) && size < wg_buffer.size) + return STATUS_BUFFER_TOO_SMALL; + + buffer_lock(buffer, &data, &size); + wg_stream = wg_parser_get_stream(object->wg_parser, wg_buffer.stream); + if (!wg_parser_stream_copy_buffer(wg_stream, data, 0, wg_buffer.size)) + { + status = STATUS_UNSUCCESSFUL; + size = 0; + } + wg_parser_stream_release_buffer(wg_stream); + buffer_unlock(buffer, &wg_buffer); + + TRACE("Got buffer %p, buffer_size %#x on stream %u\n", buffer->pBuffer, *buffer_size, *stream); + return status; +} + +NTSTATUS CDECL winedmo_demuxer_seek(struct winedmo_demuxer demuxer, INT64 timestamp) +{ + struct demuxer *object = demuxer_from_handle(demuxer); + wg_parser_stream_t wg_stream; + + TRACE("demuxer %#I64x, timestamp %I64d\n", demuxer.handle, timestamp); + + wg_stream = wg_parser_get_stream(object->wg_parser, 0); + wg_parser_stream_seek(wg_stream, 1.0, timestamp, 0, AM_SEEKING_AbsolutePositioning, AM_SEEKING_NoPositioning); + return STATUS_SUCCESS; +} + +NTSTATUS CDECL winedmo_demuxer_stream_lang(struct winedmo_demuxer demuxer, UINT stream, WCHAR *buffer, UINT len) +{ + struct demuxer *object = demuxer_from_handle(demuxer); + wg_parser_stream_t wg_stream; + char *tag; + + TRACE("demuxer %#I64x, stream %u\n", demuxer.handle, stream); + + wg_stream = wg_parser_get_stream(object->wg_parser, stream); + if (!(tag = wg_parser_stream_get_tag(wg_stream, WG_PARSER_TAG_LANGUAGE))) return STATUS_UNSUCCESSFUL; + len = MultiByteToWideChar(CP_UTF8, 0, tag, -1, buffer, len); + buffer[len - 1] = 0; + free(tag); + + return STATUS_SUCCESS; +} + +NTSTATUS CDECL winedmo_demuxer_stream_name(struct winedmo_demuxer demuxer, UINT stream, WCHAR *buffer, UINT len) +{ + struct demuxer *object = demuxer_from_handle(demuxer); + wg_parser_stream_t wg_stream; + char *tag; + + TRACE("demuxer %#I64x, stream %u\n", demuxer.handle, stream); + + wg_stream = wg_parser_get_stream(object->wg_parser, stream); + if (!(tag = wg_parser_stream_get_tag(wg_stream, WG_PARSER_TAG_NAME))) return STATUS_UNSUCCESSFUL; + len = MultiByteToWideChar(CP_UTF8, 0, tag, -1, buffer, len); + buffer[len - 1] = 0; + free(tag); + + return STATUS_SUCCESS; +} + +NTSTATUS CDECL winedmo_demuxer_stream_type(struct winedmo_demuxer demuxer, UINT stream, GUID *major, union winedmo_format **format) +{ + struct demuxer *object = demuxer_from_handle(demuxer); + struct wg_media_type wg_media_type; + wg_parser_stream_t wg_stream; + struct wg_format wg_format; + IMFMediaType *media_type; + HRESULT hr; + + TRACE("demuxer %#I64x, stream %u, major %p, format %p\n", demuxer.handle, stream, major, format); + + wg_stream = wg_parser_get_stream(object->wg_parser, stream); + wg_parser_stream_get_current_format(wg_stream, &wg_format); + + if (!(media_type = mf_media_type_from_wg_format(&wg_format))) + return STATUS_NO_MEMORY; + hr = wg_media_type_from_mf(media_type, &wg_media_type); + IMFMediaType_Release(media_type); + if (FAILED(hr)) + return STATUS_UNSUCCESSFUL; + + if (!(*format = calloc(1, wg_media_type.format_size))) + { + CoTaskMemFree(wg_media_type.u.format); + return STATUS_NO_MEMORY; + } + + *major = wg_media_type.major; + memcpy(*format, wg_media_type.u.format, wg_media_type.format_size); + CoTaskMemFree(wg_media_type.u.format); + return STATUS_SUCCESS; +} diff --git a/dlls/winegstreamer/winegstreamer.spec b/dlls/winegstreamer/winegstreamer.spec index 095f75a0865..f9e206eb588 100644 --- a/dlls/winegstreamer/winegstreamer.spec +++ b/dlls/winegstreamer/winegstreamer.spec @@ -4,3 +4,12 @@ @ stdcall -private DllUnregisterServer() @ stdcall winegstreamer_create_wm_sync_reader(ptr ptr) @ stdcall winegstreamer_create_video_decoder(ptr) + +@ cdecl winedmo_demuxer_check(ptr) +@ cdecl winedmo_demuxer_create(wstr ptr int64 ptr ptr ptr ptr) +@ cdecl winedmo_demuxer_destroy(ptr) +@ cdecl winedmo_demuxer_read(int64 ptr ptr ptr) +@ cdecl winedmo_demuxer_seek(int64 int64) +@ cdecl winedmo_demuxer_stream_lang(int64 long ptr long) +@ cdecl winedmo_demuxer_stream_name(int64 long ptr long) +@ cdecl winedmo_demuxer_stream_type(int64 long ptr ptr) -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9913