From: Conor McCarthy <cmccarthy@codeweavers.com> --- dlls/mf/tests/mf.c | 396 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 381 insertions(+), 15 deletions(-) diff --git a/dlls/mf/tests/mf.c b/dlls/mf/tests/mf.c index 7aca38c7fd2..1c0b8424576 100644 --- a/dlls/mf/tests/mf.c +++ b/dlls/mf/tests/mf.c @@ -10190,6 +10190,8 @@ static void test_media_session_thinning(void) struct test_transform { IMFTransform IMFTransform_iface; + IMFMediaEventGenerator IMFMediaEventGenerator_iface; + IMFShutdown IMFShutdown_iface; LONG refcount; const MFT_OUTPUT_STREAM_INFO *output_stream_info; @@ -10205,13 +10207,50 @@ struct test_transform IMFSample *output; HANDLE flush_event; + + IMFAttributes *attributes; + IMFMediaEventQueue *event_queue; + BOOL async; + BOOL streaming; + BOOL pending_input; + BOOL drain; + BOOL is_shut_down; }; +#define test_transform_check_unlocked(a) test_transform_check_unlocked_(__LINE__, a) +static void test_transform_check_unlocked_(int line, struct test_transform *transform) +{ + UINT32 unlock; + HRESULT hr; + + if (!transform->async) + return; + hr = IMFAttributes_GetUINT32(transform->attributes, &MF_TRANSFORM_ASYNC_UNLOCK, &unlock); + ok_(__FILE__, line)(hr == S_OK, "Unexpected hr %#lx.\n", hr); + todo_wine + ok_(__FILE__, line)(unlock, "Transform is locked.\n"); +} + static struct test_transform *test_transform_from_IMFTransform(IMFTransform *iface) { return CONTAINING_RECORD(iface, struct test_transform, IMFTransform_iface); } +static struct test_transform *test_transform_from_IMFMediaEventGenerator(IMFMediaEventGenerator *iface) +{ + return CONTAINING_RECORD(iface, struct test_transform, IMFMediaEventGenerator_iface); +} + +static struct test_transform *test_transform_from_IMFShutdown(IMFShutdown *iface) +{ + return CONTAINING_RECORD(iface, struct test_transform, IMFShutdown_iface); +} + +static HRESULT test_transform_queue_event(struct test_transform *transform, MediaEventType type) +{ + return IMFMediaEventQueue_QueueEventParamVar(transform->event_queue, type, &GUID_NULL, S_OK, NULL); +} + static HRESULT WINAPI test_transform_QueryInterface(IMFTransform *iface, REFIID iid, void **out) { struct test_transform *transform = test_transform_from_IMFTransform(iface); @@ -10219,13 +10258,24 @@ static HRESULT WINAPI test_transform_QueryInterface(IMFTransform *iface, REFIID if (IsEqualGUID(iid, &IID_IUnknown) || IsEqualGUID(iid, &IID_IMFTransform)) { - IMFTransform_AddRef(&transform->IMFTransform_iface); *out = &transform->IMFTransform_iface; - return S_OK; + } + else if (transform->async && IsEqualIID(iid, &IID_IMFMediaEventGenerator)) + { + *out = &transform->IMFMediaEventGenerator_iface; + } + else if (transform->async && IsEqualIID(iid, &IID_IMFShutdown)) + { + *out = &transform->IMFShutdown_iface; + } + else + { + *out = NULL; + return E_NOINTERFACE; } - *out = NULL; - return E_NOINTERFACE; + IUnknown_AddRef((IUnknown *)*out); + return S_OK; } static ULONG WINAPI test_transform_AddRef(IMFTransform *iface) @@ -10247,6 +10297,13 @@ static ULONG WINAPI test_transform_Release(IMFTransform *iface) if (transform->output_type) IMFMediaType_Release(transform->output_type); CloseHandle(transform->flush_event); + if (transform->async) + { + if (!transform->is_shut_down) + IMFShutdown_Shutdown(&transform->IMFShutdown_iface); + IMFAttributes_Release(transform->attributes); + IMFMediaEventQueue_Release(transform->event_queue); + } free(transform); } @@ -10288,6 +10345,8 @@ static HRESULT WINAPI test_transform_GetOutputStreamInfo(IMFTransform *iface, DW { struct test_transform *transform = test_transform_from_IMFTransform(iface); + test_transform_check_unlocked(transform); + ok(!!transform->output_stream_info, "Unexpected %s iface %p call.\n", __func__, iface); if (!transform->output_stream_info) return E_NOTIMPL; @@ -10298,7 +10357,14 @@ static HRESULT WINAPI test_transform_GetOutputStreamInfo(IMFTransform *iface, DW static HRESULT WINAPI test_transform_GetAttributes(IMFTransform *iface, IMFAttributes **attributes) { - return E_NOTIMPL; + struct test_transform *transform = test_transform_from_IMFTransform(iface); + + if (!transform->async) + return E_NOTIMPL; + + *attributes = transform->attributes; + IMFAttributes_AddRef(*attributes); + return S_OK; } static HRESULT WINAPI test_transform_GetInputStreamAttributes(IMFTransform *iface, DWORD id, IMFAttributes **attributes) @@ -10344,6 +10410,8 @@ static HRESULT WINAPI test_transform_GetOutputAvailableType(IMFTransform *iface, { struct test_transform *transform = test_transform_from_IMFTransform(iface); + test_transform_check_unlocked(transform); + if (index >= transform->output_count) { *type = NULL; @@ -10358,6 +10426,9 @@ static HRESULT WINAPI test_transform_GetOutputAvailableType(IMFTransform *iface, static HRESULT WINAPI test_transform_SetInputType(IMFTransform *iface, DWORD id, IMFMediaType *type, DWORD flags) { struct test_transform *transform = test_transform_from_IMFTransform(iface); + + test_transform_check_unlocked(transform); + if (flags & MFT_SET_TYPE_TEST_ONLY) return S_OK; if (transform->input_type) @@ -10370,6 +10441,9 @@ static HRESULT WINAPI test_transform_SetInputType(IMFTransform *iface, DWORD id, static HRESULT WINAPI test_transform_SetOutputType(IMFTransform *iface, DWORD id, IMFMediaType *type, DWORD flags) { struct test_transform *transform = test_transform_from_IMFTransform(iface); + + test_transform_check_unlocked(transform); + if (flags & MFT_SET_TYPE_TEST_ONLY) return S_OK; if (transform->output_type) @@ -10382,6 +10456,9 @@ static HRESULT WINAPI test_transform_SetOutputType(IMFTransform *iface, DWORD id static HRESULT WINAPI test_transform_GetInputCurrentType(IMFTransform *iface, DWORD id, IMFMediaType **type) { struct test_transform *transform = test_transform_from_IMFTransform(iface); + + test_transform_check_unlocked(transform); + if (!(*type = transform->input_type)) return MF_E_TRANSFORM_TYPE_NOT_SET; IMFMediaType_AddRef(*type); @@ -10391,6 +10468,9 @@ static HRESULT WINAPI test_transform_GetInputCurrentType(IMFTransform *iface, DW static HRESULT WINAPI test_transform_GetOutputCurrentType(IMFTransform *iface, DWORD id, IMFMediaType **type) { struct test_transform *transform = test_transform_from_IMFTransform(iface); + + test_transform_check_unlocked(transform); + if (!(*type = transform->output_type)) return MF_E_TRANSFORM_TYPE_NOT_SET; IMFMediaType_AddRef(*type); @@ -10424,11 +10504,15 @@ static HRESULT WINAPI test_transform_ProcessEvent(IMFTransform *iface, DWORD id, DEFINE_EXPECT(test_transform_ProcessMessage_BEGIN_STREAMING); DEFINE_EXPECT(test_transform_ProcessMessage_START_OF_STREAM); DEFINE_EXPECT(test_transform_ProcessMessage_FLUSH); +DEFINE_EXPECT(test_transform_ProcessInput); +DEFINE_EXPECT(test_transform_ProcessOutput); static HRESULT WINAPI test_transform_ProcessMessage(IMFTransform *iface, MFT_MESSAGE_TYPE message, ULONG_PTR param) { struct test_transform *transform = test_transform_from_IMFTransform(iface); + test_transform_check_unlocked(transform); + switch (message) { case MFT_MESSAGE_NOTIFY_BEGIN_STREAMING: @@ -10439,31 +10523,59 @@ static HRESULT WINAPI test_transform_ProcessMessage(IMFTransform *iface, MFT_MES case MFT_MESSAGE_NOTIFY_START_OF_STREAM: CHECK_EXPECT(test_transform_ProcessMessage_START_OF_STREAM); add_object_state(&actual_object_state_record, MFT_START); + if (transform->async) + { + transform->streaming = TRUE; + transform->pending_input = TRUE; + SET_EXPECT(test_transform_ProcessInput); + return test_transform_queue_event(transform, METransformNeedInput); + } return S_OK; case MFT_MESSAGE_COMMAND_FLUSH: + if (transform->output) + { + IMFSample_Release(transform->output); + transform->output = NULL; + } SetEvent(transform->flush_event); CHECK_EXPECT2(test_transform_ProcessMessage_FLUSH); add_object_state(&actual_object_state_record, MFT_FLUSH); return S_OK; + case MFT_MESSAGE_COMMAND_DRAIN: + if (transform->async) + { + if (!transform->pending_input && !transform->output) + return test_transform_queue_event(transform, METransformDrainComplete); + transform->drain = TRUE; + return S_OK; + } + break; + + case MFT_MESSAGE_NOTIFY_END_STREAMING: + if (transform->async) + return S_OK; + break; + default: - ok(0, "Unexpected %s call %#x.\n", __func__, message); - return E_NOTIMPL; + break; } -} -DEFINE_EXPECT(test_transform_ProcessInput); -DEFINE_EXPECT(test_transform_ProcessOutput); + ok(0, "Unexpected %s call %#x.\n", __func__, message); + return E_NOTIMPL; +} static HRESULT WINAPI test_transform_ProcessInput(IMFTransform *iface, DWORD id, IMFSample *sample, DWORD flags) { struct test_transform *transform = test_transform_from_IMFTransform(iface); HRESULT hr; + test_transform_check_unlocked(transform); + if (expect_test_transform_ProcessInput) { - if (transform->output) + if (transform->output || (transform->async && !transform->pending_input)) { hr = MF_E_NOTACCEPTING; } @@ -10471,6 +10583,12 @@ static HRESULT WINAPI test_transform_ProcessInput(IMFTransform *iface, DWORD id, { IMFSample_AddRef(transform->output = sample); hr = S_OK; + + if (transform->async) + { + transform->pending_input = FALSE; + hr = test_transform_queue_event(transform, METransformHaveOutput); + } } } else @@ -10490,6 +10608,8 @@ static HRESULT WINAPI test_transform_ProcessOutput(IMFTransform *iface, DWORD fl struct test_transform *transform = test_transform_from_IMFTransform(iface); HRESULT hr; + test_transform_check_unlocked(transform); + if (expect_test_transform_ProcessOutput) { if (transform->output) @@ -10498,10 +10618,25 @@ static HRESULT WINAPI test_transform_ProcessOutput(IMFTransform *iface, DWORD fl data->pSample = transform->output; transform->output = NULL; hr = S_OK; + + if (transform->async) + { + if (transform->drain) + { + hr = test_transform_queue_event(transform, METransformDrainComplete); + transform->drain = FALSE; + } + else if (transform->streaming) + { + transform->pending_input = TRUE; + SET_EXPECT(test_transform_ProcessInput); + hr = test_transform_queue_event(transform, METransformNeedInput); + } + } } else { - hr = MF_E_TRANSFORM_NEED_MORE_INPUT; + hr = transform->async ? E_UNEXPECTED : MF_E_TRANSFORM_NEED_MORE_INPUT; } } else @@ -10545,14 +10680,125 @@ static const IMFTransformVtbl test_transform_vtbl = test_transform_ProcessOutput, }; +static HRESULT WINAPI test_transform_events_QueryInterface(IMFMediaEventGenerator *iface, REFIID iid, void **out) +{ + struct test_transform *transform = test_transform_from_IMFMediaEventGenerator(iface); + return IMFTransform_QueryInterface(&transform->IMFTransform_iface, iid, out); +} + +static ULONG WINAPI test_transform_events_AddRef(IMFMediaEventGenerator *iface) +{ + struct test_transform *transform = test_transform_from_IMFMediaEventGenerator(iface); + return IMFTransform_AddRef(&transform->IMFTransform_iface); +} + +static ULONG WINAPI test_transform_events_Release(IMFMediaEventGenerator *iface) +{ + struct test_transform *transform = test_transform_from_IMFMediaEventGenerator(iface); + return IMFTransform_Release(&transform->IMFTransform_iface); +} + +static HRESULT WINAPI test_transform_events_GetEvent(IMFMediaEventGenerator *iface, DWORD flags, IMFMediaEvent **event) +{ + struct test_transform *transform = test_transform_from_IMFMediaEventGenerator(iface); + return IMFMediaEventQueue_GetEvent(transform->event_queue, flags, event); +} + +static HRESULT WINAPI test_transform_events_BeginGetEvent(IMFMediaEventGenerator *iface, IMFAsyncCallback *callback, + IUnknown *state) +{ + struct test_transform *transform = test_transform_from_IMFMediaEventGenerator(iface); + return IMFMediaEventQueue_BeginGetEvent(transform->event_queue, callback, state); +} + +static HRESULT WINAPI test_transform_events_EndGetEvent(IMFMediaEventGenerator *iface, IMFAsyncResult *result, + IMFMediaEvent **event) +{ + struct test_transform *transform = test_transform_from_IMFMediaEventGenerator(iface); + return IMFMediaEventQueue_EndGetEvent(transform->event_queue, result, event); +} + +static HRESULT WINAPI test_transform_events_QueueEvent(IMFMediaEventGenerator *iface, MediaEventType event_type, + REFGUID ext_type, HRESULT hr, const PROPVARIANT *value) +{ + struct test_transform *transform = test_transform_from_IMFMediaEventGenerator(iface); + return IMFMediaEventQueue_QueueEventParamVar(transform->event_queue, event_type, ext_type, hr, value); +} + +static const IMFMediaEventGeneratorVtbl test_transform_events_vtbl = +{ + test_transform_events_QueryInterface, + test_transform_events_AddRef, + test_transform_events_Release, + test_transform_events_GetEvent, + test_transform_events_BeginGetEvent, + test_transform_events_EndGetEvent, + test_transform_events_QueueEvent, +}; + +static HRESULT WINAPI test_transform_shutdown_QueryInterface(IMFShutdown *iface, REFIID iid, void **out) +{ + struct test_transform *transform = test_transform_from_IMFShutdown(iface); + return IMFTransform_QueryInterface(&transform->IMFTransform_iface, iid, out); +} + +static ULONG WINAPI test_transform_shutdown_AddRef(IMFShutdown *iface) +{ + struct test_transform *transform = test_transform_from_IMFShutdown(iface); + return IMFTransform_AddRef(&transform->IMFTransform_iface); +} + +static ULONG WINAPI test_transform_shutdown_Release(IMFShutdown *iface) +{ + struct test_transform *transform = test_transform_from_IMFShutdown(iface); + return IMFTransform_Release(&transform->IMFTransform_iface); +} + +static HRESULT WINAPI test_transform_shutdown_Shutdown(IMFShutdown *iface) +{ + struct test_transform *transform = test_transform_from_IMFShutdown(iface); + + IMFMediaEventQueue_Shutdown(transform->event_queue); + transform->is_shut_down = TRUE; + + return S_OK; +} + +static HRESULT WINAPI test_transform_shutdown_GetShutdownStatus(IMFShutdown *iface, MFSHUTDOWN_STATUS *status) +{ + struct test_transform *transform = test_transform_from_IMFShutdown(iface); + HRESULT hr = S_OK; + + if (!status) + return E_INVALIDARG; + + if (transform->is_shut_down) + *status = MFSHUTDOWN_COMPLETED; + else + hr = MF_E_INVALIDREQUEST; + + return hr; +} + +static const IMFShutdownVtbl test_transform_shutdown_vtbl = +{ + test_transform_shutdown_QueryInterface, + test_transform_shutdown_AddRef, + test_transform_shutdown_Release, + test_transform_shutdown_Shutdown, + test_transform_shutdown_GetShutdownStatus, +}; + static HRESULT WINAPI test_transform_create(UINT input_count, IMFMediaType **input_types, - UINT output_count, IMFMediaType **output_types, BOOL d3d_aware, IMFTransform **out) + UINT output_count, IMFMediaType **output_types, BOOL d3d_aware, BOOL async, IMFTransform **out) { struct test_transform *transform; if (!(transform = calloc(1, sizeof(*transform)))) return E_OUTOFMEMORY; transform->IMFTransform_iface.lpVtbl = &test_transform_vtbl; + transform->IMFMediaEventGenerator_iface.lpVtbl = async ? &test_transform_events_vtbl : NULL; + transform->IMFShutdown_iface.lpVtbl = async ? &test_transform_shutdown_vtbl : NULL; transform->refcount = 1; transform->input_count = input_count; @@ -10566,6 +10812,20 @@ static HRESULT WINAPI test_transform_create(UINT input_count, IMFMediaType **inp transform->flush_event = CreateEventW(NULL, FALSE, FALSE, NULL); ok(!!transform->flush_event, "CreateEventW failed, error %lu\n", GetLastError()); + if ((transform->async = async)) + { + HRESULT hr = MFCreateAttributes(&transform->attributes, 3); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = IMFAttributes_SetUINT32(transform->attributes, &MF_TRANSFORM_ASYNC, TRUE); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = IMFAttributes_SetUINT32(transform->attributes, &MF_TRANSFORM_ASYNC_UNLOCK, FALSE); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = IMFAttributes_SetUINT32(transform->attributes, &MFT_SUPPORT_DYNAMIC_FORMAT_CHANGE, TRUE); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = MFCreateEventQueue(&transform->event_queue); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + } + *out = &transform->IMFTransform_iface; return S_OK; } @@ -10627,7 +10887,7 @@ static void test_media_session_seek(void) hr = IMFMediaType_SetUINT32(type, &MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE); ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); mft = NULL; - hr = test_transform_create(1, &type, 1, &type, FALSE, &mft); + hr = test_transform_create(1, &type, 1, &type, FALSE, FALSE, &mft); ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); test_transform_set_output_stream_info(mft, &output_stream_info); @@ -10934,7 +11194,7 @@ static void test_media_session_scrubbing(void) hr = IMFMediaType_SetUINT64(type, &MF_MT_FRAME_SIZE, (UINT64)640 << 32 | 480); ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); mft = NULL; - hr = test_transform_create(1, &type, 1, &type, FALSE, &mft); + hr = test_transform_create(1, &type, 1, &type, FALSE, FALSE, &mft); ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); transform = test_transform_from_IMFTransform(mft); test_transform_set_output_stream_info(mft, &output_stream_info); @@ -11293,6 +11553,111 @@ static void test_media_session_invalid_topology(void) ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); } +static void test_async_transform(void) +{ + media_type_desc video_nv12_desc = + { + ATTR_GUID(MF_MT_MAJOR_TYPE, MFMediaType_Video), + ATTR_GUID(MF_MT_SUBTYPE, MFVideoFormat_NV12), + }; + MFT_OUTPUT_STREAM_INFO output_stream_info = {0}; + struct test_grabber_callback *grabber_callback; + struct test_transform *transform; + IMFActivate *sink_activate; + IMFAsyncCallback *callback; + IMFMediaType *output_type; + PROPVARIANT propvar = {0}; + IMFMediaSession *session; + IMFTransform *mft = NULL; + IMFMediaSource *source; + IMFTopology *topology; + HRESULT hr; + DWORD ret; + UINT i; + + hr = MFStartup(MF_VERSION, MFSTARTUP_FULL); + ok(hr == S_OK, "Failed to start up, hr %#lx.\n", hr); + + if (!(source = create_media_source(L"test.mp4", L"video/mp4"))) + { + todo_wine /* Gitlab CI Debian runner */ + win_skip("MP4 media source is not supported, skipping tests.\n"); + MFShutdown(); + return; + } + + grabber_callback = impl_from_IMFSampleGrabberSinkCallback(create_test_grabber_callback()); + grabber_callback->ready_event = CreateEventW(NULL, FALSE, FALSE, NULL); + ok(!!grabber_callback->ready_event, "CreateEventW failed, error %lu\n", GetLastError()); + + hr = MFCreateMediaType(&output_type); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + init_media_type(output_type, video_nv12_desc, -1); + hr = MFCreateSampleGrabberSinkActivate(output_type, &grabber_callback->IMFSampleGrabberSinkCallback_iface, &sink_activate); + ok(hr == S_OK, "Failed to create grabber sink, hr %#lx.\n", hr); + + memset(&actual_object_state_record, 0, sizeof(actual_object_state_record)); + SET_EXPECT(test_transform_ProcessMessage_START_OF_STREAM); + SET_EXPECT(test_transform_ProcessMessage_BEGIN_STREAMING); + SET_EXPECT(test_transform_ProcessOutput); + SET_EXPECT(test_transform_ProcessMessage_FLUSH); + + hr = test_transform_create(1, &output_type, 1, &output_type, FALSE, TRUE, &mft); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + test_transform_set_output_stream_info(mft, &output_stream_info); + + hr = MFCreateMediaSession(NULL, &session); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + + topology = create_test_topology_unk(source, (IUnknown *)sink_activate, (IUnknown *)mft, NULL); + hr = IMFMediaSession_SetTopology(session, 0, topology); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + IMFTopology_Release(topology); + IMFActivate_Release(sink_activate); + + callback = create_test_callback(TRUE); + propvar.vt = VT_EMPTY; + hr = IMFMediaSession_Start(session, &GUID_NULL, &propvar); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = wait_media_event(session, callback, MESessionStarted, 1000, &propvar); + + /* Check for repeated sample delivery */ + for (i = 0, ret = WAIT_OBJECT_0; i < 10 && ret == WAIT_OBJECT_0; ++i) + { + ret = WaitForSingleObject(grabber_callback->ready_event, 1000); + ok(ret == WAIT_OBJECT_0, "WaitForSingleObject returned %lu\n", ret); + } + + transform = test_transform_from_IMFTransform(mft); + ok(transform->streaming, "Transform is not streaming.\n"); + + hr = IMFMediaSession_Stop(session); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = IMFMediaSession_Close(session); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = wait_media_event(session, callback, MESessionClosed, 1000, &propvar); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + + ok(!transform->is_shut_down, "Transform was shut down.\n"); + + hr = IMFMediaSource_Shutdown(source); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = IMFMediaSession_Shutdown(session); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + + ok(transform->is_shut_down, "Transform was not shut down.\n"); + + IMFTransform_Release(mft); + IMFMediaType_Release(output_type); + IMFAsyncCallback_Release(callback); + IMFMediaSession_Release(session); + IMFSampleGrabberSinkCallback_Release(&grabber_callback->IMFSampleGrabberSinkCallback_iface); + IMFMediaSource_Release(source); + + hr = MFShutdown(); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); +} + START_TEST(mf) { init_functions(); @@ -11337,4 +11702,5 @@ START_TEST(mf) test_media_session_scrubbing(); test_media_session_sample_request(); test_media_session_invalid_topology(); + test_async_transform(); } -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10606