From: Rémi Bernon rbernon@codeweavers.com
--- dlls/mfmediaengine/tests/Makefile.in | 2 + dlls/mfmediaengine/tests/mfmediaengine.c | 344 ++++++++++++++++++++++- dlls/mfmediaengine/tests/resource.rc | 32 +++ dlls/mfmediaengine/tests/rgb32frame.bmp | Bin 0 -> 16438 bytes dlls/mfmediaengine/tests/test.mp4 | Bin 0 -> 3983 bytes 5 files changed, 376 insertions(+), 2 deletions(-) create mode 100644 dlls/mfmediaengine/tests/resource.rc create mode 100644 dlls/mfmediaengine/tests/rgb32frame.bmp create mode 100644 dlls/mfmediaengine/tests/test.mp4
diff --git a/dlls/mfmediaengine/tests/Makefile.in b/dlls/mfmediaengine/tests/Makefile.in index 421b75587a0..b23d530d46f 100644 --- a/dlls/mfmediaengine/tests/Makefile.in +++ b/dlls/mfmediaengine/tests/Makefile.in @@ -3,3 +3,5 @@ IMPORTS = ole32 mfplat oleaut32 mfuuid uuid
C_SRCS = \ mfmediaengine.c + +RC_SRCS = resource.rc diff --git a/dlls/mfmediaengine/tests/mfmediaengine.c b/dlls/mfmediaengine/tests/mfmediaengine.c index 3ff81b1f9b1..60a914a6020 100644 --- a/dlls/mfmediaengine/tests/mfmediaengine.c +++ b/dlls/mfmediaengine/tests/mfmediaengine.c @@ -28,6 +28,7 @@ #include "mfmediaengine.h" #include "mferror.h" #include "dxgi.h" +#include "d3d11.h" #include "initguid.h" #include "mmdeviceapi.h" #include "audiosessiontypes.h" @@ -35,6 +36,9 @@ #include "wine/test.h"
static HRESULT (WINAPI *pMFCreateDXGIDeviceManager)(UINT *token, IMFDXGIDeviceManager **manager); +static HRESULT (WINAPI *pD3D11CreateDevice)(IDXGIAdapter *adapter, D3D_DRIVER_TYPE driver_type, HMODULE swrast, UINT flags, + const D3D_FEATURE_LEVEL *feature_levels, UINT levels, UINT sdk_version, ID3D11Device **device_out, + D3D_FEATURE_LEVEL *obtained_feature_level, ID3D11DeviceContext **immediate_context);
static IMFMediaEngineClassFactory *factory;
@@ -62,12 +66,104 @@ static void check_interface_(unsigned int line, void *iface_ptr, REFIID iid, BOO IUnknown_Release(unk); }
+static DWORD compare_rgb32(const BYTE *data, DWORD *length, const RECT *rect, const BYTE *expect) +{ + DWORD x, y, size, diff = 0, width = (rect->right + 0xf) & ~0xf, height = (rect->bottom + 0xf) & ~0xf; + + /* skip BMP header from the dump */ + size = *(DWORD *)(expect + 2 + 2 * sizeof(DWORD)); + *length = *length + size; + expect = expect + size; + + for (y = 0; y < height; y++, data += width * 4, expect += width * 4) + { + if (y < rect->top || y >= rect->bottom) continue; + for (x = 0; x < width; x++) + { + if (x < rect->left || x >= rect->right) continue; + diff += abs((int)expect[4 * x + 0] - (int)data[4 * x + 0]); + diff += abs((int)expect[4 * x + 1] - (int)data[4 * x + 1]); + diff += abs((int)expect[4 * x + 2] - (int)data[4 * x + 2]); + } + } + + size = (rect->right - rect->left) * (rect->bottom - rect->top) * 3; + return diff * 100 / 256 / size; +} + +static void dump_rgb32(const BYTE *data, DWORD length, const RECT *rect, HANDLE output) +{ + DWORD width = (rect->right + 0xf) & ~0xf, height = (rect->bottom + 0xf) & ~0xf; + static const char magic[2] = "BM"; + struct + { + DWORD length; + DWORD reserved; + DWORD offset; + BITMAPINFOHEADER biHeader; + } header = + { + .length = length + sizeof(header) + 2, .offset = sizeof(header) + 2, + .biHeader = + { + .biSize = sizeof(BITMAPINFOHEADER), .biWidth = width, .biHeight = height, .biPlanes = 1, + .biBitCount = 32, .biCompression = BI_RGB, .biSizeImage = width * height * 4, + }, + }; + DWORD written; + BOOL ret; + + ret = WriteFile(output, magic, sizeof(magic), &written, NULL); + ok(ret, "WriteFile failed, error %lu\n", GetLastError()); + ok(written == sizeof(magic), "written %lu bytes\n", written); + ret = WriteFile(output, &header, sizeof(header), &written, NULL); + ok(ret, "WriteFile failed, error %lu\n", GetLastError()); + ok(written == sizeof(header), "written %lu bytes\n", written); + ret = WriteFile(output, data, length, &written, NULL); + ok(ret, "WriteFile failed, error %lu\n", GetLastError()); + ok(written == length, "written %lu bytes\n", written); +} + +#define check_rgb32_data(a, b, c, d, e) check_rgb32_data_(__LINE__, a, b, c, d, e) +static void check_rgb32_data_(int line, const WCHAR *filename, const BYTE *data, DWORD length, const RECT *rect, BOOL todo) +{ + WCHAR output_path[MAX_PATH]; + const BYTE *expect_data; + HRSRC resource; + HANDLE output; + DWORD diff; + + GetTempPathW(ARRAY_SIZE(output_path), output_path); + lstrcatW(output_path, filename); + output = CreateFileW(output_path, GENERIC_READ|GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, 0); + ok(output != INVALID_HANDLE_VALUE, "CreateFileW failed, error %lu\n", GetLastError()); + dump_rgb32(data, length, rect, output); + trace("created %s\n", debugstr_w(output_path)); + CloseHandle(output); + + resource = FindResourceW(NULL, filename, (const WCHAR *)RT_RCDATA); + ok(resource != 0, "FindResourceW failed, error %lu\n", GetLastError()); + expect_data = LockResource(LoadResource(GetModuleHandleW(NULL), resource)); + + diff = compare_rgb32(data, &length, rect, expect_data); + todo_wine_if(todo) + ok_(__FILE__, line)(diff == 0, "Unexpected %lu%% diff\n", diff); +} + static void init_functions(void) { - HMODULE mod = GetModuleHandleA("mfplat.dll"); + HMODULE mod;
#define X(f) p##f = (void*)GetProcAddress(mod, #f) - X(MFCreateDXGIDeviceManager); + if ((mod = GetModuleHandleA("mfplat.dll"))) + { + X(MFCreateDXGIDeviceManager); + } + + if ((mod = LoadLibraryA("d3d11.dll"))) + { + X(D3D11CreateDevice); + } #undef X }
@@ -138,6 +234,34 @@ static struct media_engine_notify *create_callback(void) return object; }
+static ID3D11Device *create_d3d11_device(void) +{ + static const D3D_FEATURE_LEVEL default_feature_level[] = + { + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + }; + const D3D_FEATURE_LEVEL *feature_level; + unsigned int feature_level_count; + ID3D11Device *device; + + feature_level = default_feature_level; + feature_level_count = ARRAY_SIZE(default_feature_level); + + if (SUCCEEDED(pD3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, + feature_level, feature_level_count, D3D11_SDK_VERSION, &device, NULL, NULL))) + return device; + if (SUCCEEDED(pD3D11CreateDevice(NULL, D3D_DRIVER_TYPE_WARP, NULL, 0, + feature_level, feature_level_count, D3D11_SDK_VERSION, &device, NULL, NULL))) + return device; + if (SUCCEEDED(pD3D11CreateDevice(NULL, D3D_DRIVER_TYPE_REFERENCE, NULL, 0, + feature_level, feature_level_count, D3D11_SDK_VERSION, &device, NULL, NULL))) + return device; + + return NULL; +} + static IMFMediaEngine *create_media_engine(IMFMediaEngineNotify *callback, IUnknown *device, UINT32 output_format) { IMFDXGIDeviceManager *manager; @@ -152,6 +276,14 @@ static IMFMediaEngine *create_media_engine(IMFMediaEngineNotify *callback, IUnkn hr = MFCreateAttributes(&attributes, 3); ok(hr == S_OK, "Unexpected hr %#lx.\n", hr);
+ if (device) + { + hr = IMFDXGIDeviceManager_ResetDevice(manager, device, token); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = IMFAttributes_SetUnknown(attributes, &MF_MEDIA_ENGINE_DXGI_MANAGER, (IUnknown *)manager); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + } + hr = IMFAttributes_SetUnknown(attributes, &MF_MEDIA_ENGINE_CALLBACK, (IUnknown *)callback); ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); hr = IMFAttributes_SetUINT32(attributes, &MF_MEDIA_ENGINE_VIDEO_OUTPUT_FORMAT, output_format); @@ -972,6 +1104,213 @@ static void test_audio_configuration(void) IMFMediaEngineNotify_Release(¬ify->IMFMediaEngineNotify_iface); }
+static IMFByteStream *load_resource(const WCHAR *name, const WCHAR *mime) +{ + IMFAttributes *attributes; + const BYTE *resource_data; + IMFByteStream *stream; + ULONG resource_len; + HRSRC resource; + HRESULT hr; + + resource = FindResourceW(NULL, name, (const WCHAR *)RT_RCDATA); + ok(resource != 0, "FindResourceW %s failed, error %lu\n", debugstr_w(name), GetLastError()); + resource_data = LockResource(LoadResource(GetModuleHandleW(NULL), resource)); + resource_len = SizeofResource(GetModuleHandleW(NULL), resource); + + hr = MFCreateTempFile(MF_ACCESSMODE_READWRITE, MF_OPENMODE_DELETE_IF_EXIST, MF_FILEFLAGS_NONE, &stream); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = IMFByteStream_Write(stream, resource_data, resource_len, &resource_len); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = IMFByteStream_SetCurrentPosition(stream, 0); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + + hr = IMFByteStream_QueryInterface(stream, &IID_IMFAttributes, (void **)&attributes); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + hr = IMFAttributes_SetString(attributes, &MF_BYTESTREAM_CONTENT_TYPE, mime); + ok(hr == S_OK, "Failed to set string value, hr %#lx.\n", hr); + IMFAttributes_Release(attributes); + + return stream; +} + +struct test_transfer_notify +{ + IMFMediaEngineNotify IMFMediaEngineNotify_iface; + + IMFMediaEngineEx *media_engine; + HANDLE ready_event; + HRESULT error; +}; + +static HRESULT WINAPI test_transfer_notify_QueryInterface(IMFMediaEngineNotify *iface, REFIID riid, void **obj) +{ + if (IsEqualIID(riid, &IID_IUnknown) + || IsEqualIID(riid, &IID_IMFMediaEngineNotify)) + { + *obj = iface; + IMFMediaEngineNotify_AddRef(iface); + return S_OK; + } + + *obj = NULL; + return E_NOINTERFACE; +} + +static ULONG WINAPI test_transfer_notify_AddRef(IMFMediaEngineNotify *iface) +{ + return 2; +} + +static ULONG WINAPI test_transfer_notify_Release(IMFMediaEngineNotify *iface) +{ + return 1; +} + +static HRESULT WINAPI test_transfer_notify_EventNotify(IMFMediaEngineNotify *iface, DWORD event, DWORD_PTR param1, DWORD param2) +{ + struct test_transfer_notify *notify = CONTAINING_RECORD(iface, struct test_transfer_notify, IMFMediaEngineNotify_iface); + IMFMediaEngineEx *media_engine = notify->media_engine; + DWORD width, height; + HRESULT hr; + BOOL ret; + + switch (event) + { + case MF_MEDIA_ENGINE_EVENT_CANPLAY: + hr = IMFMediaEngineEx_Play(media_engine); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + break; + + case MF_MEDIA_ENGINE_EVENT_FORMATCHANGE: + ret = IMFMediaEngineEx_HasVideo(media_engine); + ok(ret, "Unexpected HasVideo %u.\n", ret); + hr = IMFMediaEngineEx_GetNativeVideoSize(media_engine, &width, &height); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + ok(width == 64, "Unexpected width %lu.\n", width); + ok(height == 64, "Unexpected height %lu.\n", height); + break; + + case MF_MEDIA_ENGINE_EVENT_ERROR: + ok(broken(param2 == MF_E_UNSUPPORTED_BYTESTREAM_TYPE), "Unexpected error %#lx\n", param2); + notify->error = param2; + /* fallthrough */ + case MF_MEDIA_ENGINE_EVENT_FIRSTFRAMEREADY: + case MF_MEDIA_ENGINE_EVENT_TIMEUPDATE: + SetEvent(notify->ready_event); + break; + } + + return S_OK; +} + +static IMFMediaEngineNotifyVtbl test_transfer_notify_vtbl = +{ + test_transfer_notify_QueryInterface, + test_transfer_notify_AddRef, + test_transfer_notify_Release, + test_transfer_notify_EventNotify, +}; + +static void test_TransferVideoFrames(void) +{ + struct test_transfer_notify notify = {{&test_transfer_notify_vtbl}}; + ID3D11Texture2D *texture, *rb_texture; + D3D11_MAPPED_SUBRESOURCE map_desc; + IMFMediaEngineEx *media_engine; + ID3D11DeviceContext *context; + WCHAR url[] = {L"test.mp4"}; + D3D11_TEXTURE2D_DESC desc; + IMFByteStream *stream; + ID3D11Device *device; + RECT dst_rect; + HRESULT hr; + DWORD res; + + stream = load_resource(L"test.mp4", L"video/mp4"); + + notify.ready_event = CreateEventW(NULL, FALSE, FALSE, NULL); + ok(!!notify.ready_event, "CreateEventW failed, error %lu\n", GetLastError()); + + if (!(device = create_d3d11_device())) + { + skip("Failed to create a D3D11 device, skipping tests.\n"); + return; + } + + if (!(media_engine = create_media_engine_ex(¬ify.IMFMediaEngineNotify_iface, + (IUnknown *)device, DXGI_FORMAT_B8G8R8X8_UNORM))) + return; + notify.media_engine = media_engine; + + memset(&desc, 0, sizeof(desc)); + desc.Width = 64; + desc.Height = 64; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8X8_UNORM; + desc.BindFlags = D3D11_BIND_RENDER_TARGET; + desc.SampleDesc.Count = 1; + hr = ID3D11Device_CreateTexture2D(device, &desc, NULL, &texture); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + + hr = IMFMediaEngineEx_SetSourceFromByteStream(media_engine, stream, url); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + IMFByteStream_Release(stream); + + res = WaitForSingleObject(notify.ready_event, 5000); + ok(!res, "Unexpected res %#lx.\n", res); + + if (FAILED(notify.error)) + { + win_skip("Media engine reported error %#lx, skipping tests.\n", notify.error); + goto done; + } + + /* 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); + + ID3D11Texture2D_GetDesc(texture, &desc); + desc.Usage = D3D11_USAGE_STAGING; + desc.BindFlags = 0; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + desc.MiscFlags = 0; + hr = ID3D11Device_CreateTexture2D(device, &desc, NULL, &rb_texture); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + + ID3D11Device_GetImmediateContext(device, &context); + + SetRect(&dst_rect, 0, 0, desc.Width, desc.Height); + hr = IMFMediaEngineEx_TransferVideoFrame(notify.media_engine, (IUnknown *)texture, NULL, NULL, NULL); + ok(hr == S_OK, "Unexpected hr %#lx.\n", hr); + + 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(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); + check_rgb32_data(L"rgb32frame.bmp", map_desc.pData, map_desc.RowPitch * desc.Height, &dst_rect, TRUE); + ID3D11DeviceContext_Unmap(context, (ID3D11Resource *)rb_texture, 0); + + ID3D11DeviceContext_Release(context); + ID3D11Texture2D_Release(rb_texture); + +done: + IMFMediaEngineEx_Shutdown(media_engine); + IMFMediaEngineEx_Release(media_engine); + + ID3D11Texture2D_Release(texture); + ID3D11Device_Release(device); + + CloseHandle(notify.ready_event); +} + START_TEST(mfmediaengine) { HRESULT hr; @@ -1002,6 +1341,7 @@ START_TEST(mfmediaengine) test_time_range(); test_SetSourceFromByteStream(); test_audio_configuration(); + test_TransferVideoFrames();
IMFMediaEngineClassFactory_Release(factory);
diff --git a/dlls/mfmediaengine/tests/resource.rc b/dlls/mfmediaengine/tests/resource.rc new file mode 100644 index 00000000000..0f9f1678495 --- /dev/null +++ b/dlls/mfmediaengine/tests/resource.rc @@ -0,0 +1,32 @@ +/* + * Copyright 2022 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 "windef.h" + +/* Generated with: + * gst-launch-1.0 videotestsrc num-buffers=60 pattern=smpte100 ! \ + * video/x-raw,format=I420,width=64,height=64,framerate=30000/1001 ! \ + * videoflip method=clockwise ! videoconvert ! \ + * x264enc ! mp4mux ! filesink location=dlls/mfmediaengine/tests/test.mp4 + */ +/* @makedep: test.mp4 */ +test.mp4 RCDATA test.mp4 + +/* Generated from running the tests on Windows */ +/* @makedep: rgb32frame.bmp */ +rgb32frame.bmp RCDATA rgb32frame.bmp diff --git a/dlls/mfmediaengine/tests/rgb32frame.bmp b/dlls/mfmediaengine/tests/rgb32frame.bmp new file mode 100644 index 0000000000000000000000000000000000000000..b8ca716abb5b4dccd0a2ba61a8392b106721b0e2 GIT binary patch literal 16438 zcmeI$!4ZNm5CzcCfVSY#3OF;~?M3ZFX%!n_T~J4D!F`$efopyqgyC@PhqjJB=Y3{) zrE9yq%Q39;&lH9RG@t<uXg~wy8o1?(E9X2-Q8_L5Uv$6#0}L?0KrI8cR!g`2W`F?( z7+_#AFvhlYF83Cd)1?8?0Rs#$zyJfa49xi%EIMF-0R|Xgpq7E_CZcjqZHmfiZH}7B xzn}T-TXXHpo?E~D=il@1{a@3-^ZoyI5WEir*U#ao3tce400Rs#z`)-bcmN3t5E=jg
literal 0 HcmV?d00001
diff --git a/dlls/mfmediaengine/tests/test.mp4 b/dlls/mfmediaengine/tests/test.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..752d860008d472a1bfb26a1a97ef0c9b36ab2ba5 GIT binary patch literal 3983 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 JJHR;<_)p|_beRAE
literal 0 HcmV?d00001