[PATCH v5 0/1] MR9719: mfmediaengine/tests: Test we receive a frame before calling Play.
Media Engine on Windows appears to use scrubbing to receive a frame before `IMFMediaEngine::Play` is called. This adds a test to confirm we can receive said frame. -- v5: mfmediaengine/tests: Test we receive a frame before calling Play. https://gitlab.winehq.org/wine/wine/-/merge_requests/9719
From: Brendan McGrath <bmcgrath@codeweavers.com> --- dlls/mfmediaengine/tests/mfmediaengine.c | 103 +++++++++++++++++------ 1 file changed, 77 insertions(+), 26 deletions(-) diff --git a/dlls/mfmediaengine/tests/mfmediaengine.c b/dlls/mfmediaengine/tests/mfmediaengine.c index bf07752d695..dc26b80077b 100644 --- a/dlls/mfmediaengine/tests/mfmediaengine.c +++ b/dlls/mfmediaengine/tests/mfmediaengine.c @@ -1284,6 +1284,7 @@ struct test_transfer_notify IMFMediaEngineEx *media_engine; HANDLE ready_event, frame_ready_event; HRESULT error; + BOOL autoplay; }; static struct test_transfer_notify *impl_from_test_transfer_notify(IMFMediaEngineNotify *iface) @@ -1337,8 +1338,11 @@ static HRESULT WINAPI test_transfer_notify_EventNotify(IMFMediaEngineNotify *ifa switch (event) { case MF_MEDIA_ENGINE_EVENT_CANPLAY: - hr = IMFMediaEngineEx_Play(media_engine); - ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + if (notify->autoplay) + { + hr = IMFMediaEngineEx_Play(media_engine); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + } break; case MF_MEDIA_ENGINE_EVENT_FORMATCHANGE: @@ -1386,18 +1390,49 @@ static struct test_transfer_notify *create_transfer_notify(void) object->frame_ready_event = CreateEventW(NULL, FALSE, FALSE, NULL); ok(!!object->frame_ready_event, "Failed to create an event, error %lu.\n", GetLastError()); + object->autoplay = TRUE; + return object; } +#define compare_rgb32(texture, dst_rect, rb_texture, filename) compare_rgb32_(__LINE__, texture, dst_rect, rb_texture, filename) +static DWORD compare_rgb32_(int line, ID3D11Texture2D *texture, const RECT *dst_rect, ID3D11Texture2D *rb_texture, const WCHAR *filename) +{ + D3D11_MAPPED_SUBRESOURCE map_desc; + ID3D11DeviceContext *context; + D3D11_TEXTURE2D_DESC desc; + ID3D11Device *device; + HRESULT hr; + DWORD res; + + ID3D11Texture2D_GetDesc(texture, &desc); + ID3D11Texture2D_GetDevice(texture, &device); + ID3D11Device_GetImmediateContext(device, &context); + + ID3D11DeviceContext_CopySubresourceRegion(context, (ID3D11Resource *)rb_texture, + 0, 0, 0, 0, (ID3D11Resource *)texture, 0, NULL); + + memset(&map_desc, 0, sizeof(map_desc)); + hr = ID3D11DeviceContext_Map(context, (ID3D11Resource *)rb_texture, 0, D3D11_MAP_READ, 0, &map_desc); + ok_(__FILE__, line)(hr == S_OK, "Unexpected hr %#lx.\n", hr); + ok_(__FILE__, line)(!!map_desc.pData, "got pData %p\n", map_desc.pData); + ok_(__FILE__, line)(map_desc.DepthPitch == desc.Width * desc.Height * 4, "got DepthPitch %u\n", map_desc.DepthPitch); + ok_(__FILE__, line)(map_desc.RowPitch == desc.Width * 4, "got RowPitch %u\n", map_desc.RowPitch); + res = check_rgb32_data(L"rgb32frame.bmp", map_desc.pData, map_desc.RowPitch * desc.Height, dst_rect); + ID3D11DeviceContext_Unmap(context, (ID3D11Resource *)rb_texture, 0); + + ID3D11DeviceContext_Release(context); + ID3D11Device_Release(device); + return res; +} + static void test_TransferVideoFrame(void) { struct test_transfer_notify *notify; ID3D11Texture2D *texture = NULL, *rb_texture; - D3D11_MAPPED_SUBRESOURCE map_desc; IMFMediaEngineEx *media_engine = NULL; IWICImagingFactory *factory = NULL; IMFDXGIDeviceManager *manager; - ID3D11DeviceContext *context; D3D11_TEXTURE2D_DESC desc; IWICBitmap *bitmap = NULL; IMFByteStream *stream; @@ -1412,6 +1447,7 @@ static void test_TransferVideoFrame(void) stream = load_resource(L"i420-64x64.avi", L"video/avi"); notify = create_transfer_notify(); + notify->autoplay = FALSE; if (!(device = create_d3d11_device())) { @@ -1452,13 +1488,19 @@ static void test_TransferVideoFrame(void) WICBitmapCacheOnLoad, &bitmap); ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + /* show that we don't receive a frame prior to setting the byte stream */ + res = WaitForSingleObject(notify->frame_ready_event, 100); + ok(res == WAIT_TIMEOUT, "Unexpected res %#lx.\n", res); + url = SysAllocString(L"i420-64x64.avi"); hr = IMFMediaEngineEx_SetSourceFromByteStream(media_engine, stream, url); ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); SysFreeString(url); IMFByteStream_Release(stream); + /* now that byte stream is set, we will recieve a frame */ res = WaitForSingleObject(notify->frame_ready_event, 5000); + todo_wine ok(!res, "Unexpected res %#lx.\n", res); if (FAILED(notify->error)) @@ -1472,17 +1514,7 @@ static void test_TransferVideoFrame(void) ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); ok(res == 2, "Unexpected stream count %lu.\n", res); - /* FIXME: Wine first video frame is often full of garbage, wait for another update */ - res = WaitForSingleObject(notify->ready_event, 500); - /* It's also missing the MF_MEDIA_ENGINE_EVENT_TIMEUPDATE notifications */ - todo_wine - ok(!res, "Unexpected res %#lx.\n", res); - - SetRect(&dst_rect, 0, 0, desc.Width, desc.Height); - IMFMediaEngineEx_OnVideoStreamTick(notify->media_engine, &pts); - hr = IMFMediaEngineEx_TransferVideoFrame(notify->media_engine, (IUnknown *)texture, NULL, &dst_rect, NULL); - ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); - + /* create the readback texture */ ID3D11Texture2D_GetDesc(texture, &desc); desc.Usage = D3D11_USAGE_STAGING; desc.BindFlags = 0; @@ -1491,26 +1523,45 @@ static void test_TransferVideoFrame(void) hr = ID3D11Device_CreateTexture2D(device, &desc, NULL, &rb_texture); ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); - ID3D11Device_GetImmediateContext(device, &context); - ID3D11DeviceContext_CopySubresourceRegion(context, (ID3D11Resource *)rb_texture, - 0, 0, 0, 0, (ID3D11Resource *)texture, 0, NULL); + /* confirm we have a frame available before calling play */ + pts = 0; + hr = IMFMediaEngineEx_OnVideoStreamTick(media_engine, &pts); + todo_wine + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + todo_wine + ok(pts == 0, "Unexpected timestamp.\n"); - memset(&map_desc, 0, sizeof(map_desc)); - hr = ID3D11DeviceContext_Map(context, (ID3D11Resource *)rb_texture, 0, D3D11_MAP_READ, 0, &map_desc); + /* confirm we can transfer a frame before calling play */ + SetRect(&dst_rect, 0, 0, desc.Width, desc.Height); + hr = IMFMediaEngineEx_TransferVideoFrame(notify->media_engine, (IUnknown *)texture, NULL, &dst_rect, NULL); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + + res = compare_rgb32(texture, &dst_rect, rb_texture, L"rgb32frame.bmp"); + todo_wine + ok(res == 0, "Unexpected %lu%% diff\n", res); + + hr = IMFMediaEngineEx_Play(media_engine); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + + /* FIXME: Wine first video frame is often full of garbage, wait for another update */ + res = WaitForSingleObject(notify->ready_event, 500); + /* It's also missing the MF_MEDIA_ENGINE_EVENT_TIMEUPDATE notifications */ + todo_wine + ok(!res, "Unexpected res %#lx.\n", res); + + IMFMediaEngineEx_OnVideoStreamTick(notify->media_engine, &pts); + hr = IMFMediaEngineEx_TransferVideoFrame(notify->media_engine, (IUnknown *)texture, NULL, &dst_rect, NULL); ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); - ok(!!map_desc.pData, "got pData %p\n", map_desc.pData); - ok(map_desc.DepthPitch == 16384, "got DepthPitch %u\n", map_desc.DepthPitch); - ok(map_desc.RowPitch == desc.Width * 4, "got RowPitch %u\n", map_desc.RowPitch); - res = check_rgb32_data(L"rgb32frame.bmp", map_desc.pData, map_desc.RowPitch * desc.Height, &dst_rect); + + res = compare_rgb32(texture, &dst_rect, rb_texture, L"rgb32frame.bmp"); ok(res == 0, "Unexpected %lu%% diff\n", res); - ID3D11DeviceContext_Unmap(context, (ID3D11Resource *)rb_texture, 0); + /* Test audio session */ hr = IMFMediaEngineEx_SetVolume(media_engine, 0.5); ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); test_audio_session(FALSE); - ID3D11DeviceContext_Release(context); ID3D11Texture2D_Release(rb_texture); hr = IMFMediaEngineEx_TransferVideoFrame(notify->media_engine, (IUnknown *)bitmap, NULL, &dst_rect, NULL); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9719
v4: - create and use `compare_rgb32` helper function - add test for `frame_ready_event` prior to SetSource call - remove unnecessary comment - rebase master v5: - replace hardcoded value for expected `DepthPitch` with calculated value -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9719#note_127249
On Fri Jan 16 23:56:46 2026 +0000, Brendan McGrath wrote:
changed this line in [version 4 of the diff](/wine/wine/-/merge_requests/9719/diffs?diff_id=238410&start_sha=0718e0d578ede380cd8ddaf4e89ad6154fe631bc#f56f5661ad29be925a16bb2ec6a9a6bb11da77b4_1522_1542) We potentially could use this elsewhere. Whenever we want to compare the transferred frame with a bmp image. I've now created and made use of a helper function.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/9719#note_127250
On Thu Jan 15 19:46:05 2026 +0000, Nikolay Sivov wrote:
Does it mean that setting url/media item is enough to start the pipeline internally? Any indication if it's paused immediately, or something else is going on? Yep. I have a test app using the `MF_MEDIA_ENGINE_EXTENSION`. Here's the relevant output:
1178.781125|09504|WinMain: SetSource 0
1178.789417|04504|extension_BeginCreateObject
1178.910865|04504|extension_EndCreateObject: hr 0
1179.127780|04504|rate_control_SetRate: hr 0, thin 0, rate 0.000000
1179.129822|04504|media_source_Start
1179.151951|12024|audio|media_stream_RequestSample: hr 0
1179.152564|06964|video|media_stream_RequestSample: hr 0
1179.169550|04504|engine_notify_EventNotify: 1009 (MF_MEDIA_ENGINE_EVENT_FIRSTFRAMEREADY)
1179.202141|12028|media_source_Pause: hr 0
1179.204330|04504|rate_control_SetRate: hr 0, thin 0, rate 1.000000
So after calling `IMFMediaEngine::SetSource` (I never call `Play` in this test), it immediately: - creates the media source - sets rate to 0 (via `IMFRateControl::SetRate`) - calls `IMFMediaSource::Start` - requests samples from both streams (more than one from each, but I've only included one from each in the relevant output) - post the `FIRSTFRAMEREADY` event - pause - set rate back to 1 Do you think it is worth implementing the `MF_MEDIA_ENGINE_EXTENSION` just so I can add wine tests against it? Alternatively, I could use an MFT (as suggested by @besentv). For now I've also included a test to show that the `FIRSTFRAMEREADY` event is not sent prior to the `SetSource` call. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9719#note_127251
On Fri Jan 16 23:56:46 2026 +0000, Brendan McGrath wrote:
changed this line in [version 4 of the diff](/wine/wine/-/merge_requests/9719/diffs?diff_id=238410&start_sha=0718e0d578ede380cd8ddaf4e89ad6154fe631bc#f56f5661ad29be925a16bb2ec6a9a6bb11da77b4_1556_1565) Yeah, you are right. I was using it mainly to mark the end of the test and start of resource clean-up. But I've removed it (as there's now another very short test there that makes it even more obvious).
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/9719#note_127252
On Sat Jan 17 00:11:43 2026 +0000, Brendan McGrath wrote:
Yep. I have a test app using the `MF_MEDIA_ENGINE_EXTENSION`. Here's the relevant output: ``` 1178.781125|09504|WinMain: SetSource 0 1178.789417|04504|extension_BeginCreateObject 1178.910865|04504|extension_EndCreateObject: hr 0 1179.127780|04504|rate_control_SetRate: hr 0, thin 0, rate 0.000000 1179.129822|04504|media_source_Start 1179.151951|12024|audio|media_stream_RequestSample: hr 0 1179.152564|06964|video|media_stream_RequestSample: hr 0 1179.169550|04504|engine_notify_EventNotify: 1009 (MF_MEDIA_ENGINE_EVENT_FIRSTFRAMEREADY) 1179.202141|12028|media_source_Pause: hr 0 1179.204330|04504|rate_control_SetRate: hr 0, thin 0, rate 1.000000 ``` So after calling `IMFMediaEngine::SetSource` (I never call `Play` in this test), it immediately: - creates the media source - sets rate to 0 (via `IMFRateControl::SetRate`) - calls `IMFMediaSource::Start` - requests samples from both streams (more than one from each, but I've only included one from each in the relevant output) - post the `FIRSTFRAMEREADY` event - pause - set rate back to 1 Do you think it is worth implementing the `MF_MEDIA_ENGINE_EXTENSION` just so I can add wine tests against it? Alternatively, I could use an MFT (as suggested by @besentv). For now I've also included a test to show that the `FIRSTFRAMEREADY` event is not sent prior to the `SetSource` call. It depends on how much extra code this implies. I think having a test program outside of wine tests is enough for something so involved. Is the plan to implement correct startup sequence, to match these discoveries? Since this is obviously visible with user extension code.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/9719#note_127712
On Thu Jan 22 20:00:06 2026 +0000, Nikolay Sivov wrote:
It depends on how much extra code this implies. I think having a test program outside of wine tests is enough for something so involved. Is the plan to implement correct startup sequence, to match these discoveries? Since this is obviously visible with user extension code. Yes I will match. 'She Sees Red' only works correctly if it receives the `FIRSTFRAMEREADY` event prior to calling `Play`. Otherwise it calls `Pause` but never `Play` (until you access a menu and then resume the game, at which point it does call `Play`). So that's the motive for this fix.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/9719#note_127716
This merge request was approved by Nikolay Sivov. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9719
participants (3)
-
Brendan McGrath -
Brendan McGrath (@redmcg) -
Nikolay Sivov (@nsivov)