From 5a6b32f12062702054b2bc83fd011d7b862cadb7 Mon Sep 17 00:00:00 2001 From: Sir Walrus Date: Tue, 12 Sep 2023 19:27:56 +0200 Subject: [PATCH] Implement a stub IMediaSeeking on the mpeg splitter, some VNs need that --- README.md | 2 +- krkrwine.cpp | 83 ++++++++++++++++++++++++++++++++++++------------ making.md | 15 ++++++++- reformat.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 23 deletions(-) create mode 100755 reformat.py diff --git a/README.md b/README.md index fd873ce..3d9f8c4 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Note that on Debian, gstreamer1.0-libav is not part of, or a dependency of, gstr install.py will obey the WINEPREFIX environment variable, if set. -krkrwine includes a GStreamer component; it's installed in Proton, but is not necessary or recommended under Wine, it does nothing useful if you have the above plugins and don't have protonmediaconverter. If you want them anyways for development purposes, you can copy or link them to ~/.local/share/gstreamer-1.0/plugins/. +krkrwine's GStreamer component will not be installed under Wine; it does nothing useful if you have the above plugins and don't have protonmediaconverter. If you want them anyways for development purposes, you can copy or link them to ~/.local/share/gstreamer-1.0/plugins/. Installation - macOS -------------------- diff --git a/krkrwine.cpp b/krkrwine.cpp index 28b38a3..7eff406 100644 --- a/krkrwine.cpp +++ b/krkrwine.cpp @@ -50,8 +50,6 @@ DEFINE_GUID(IID_IUnknown, 0x00000000, 0x0000, 0x0000, 0xc0,0x00, 0x00,0x00,0x00, DEFINE_GUID(IID_IAMOpenProgress,0x8E1C39A1, 0xDE53, 0x11cf, 0xAA, 0x63, 0x00, 0x80, 0xC7, 0x44, 0x52, 0x8D); DEFINE_GUID(IID_IAMDeviceRemoval,0xf90a6130,0xb658,0x11d2,0xae,0x49,0x00,0x00,0xf8,0x75,0x4b,0x99); __CRT_UUID_DECL(IAMStreamSelect, 0xc1960960, 0x17f5, 0x11d1, 0xab,0xe1, 0x00,0xa0,0xc9,0x05,0xf3,0x75) -// used only to detect DXVK -DEFINE_GUID(IID_ID3D9VkInteropDevice, 0x2eaa4b89, 0x0107, 0x4bdb, 0x87,0xf7, 0x0f,0x54,0x1c,0x49,0x3c,0xe0); static char* guid_to_str(const GUID& guid) { @@ -65,6 +63,8 @@ static char* guid_to_str(const GUID& guid) template T min(T a, T b) { return a < b ? a : b; } template T max(T a, T b) { return a > b ? a : b; } +static bool is_debian_build; + template class CComPtr { void assign(T* ptr) @@ -714,7 +714,7 @@ static inline int mp2_packet_parse(const uint8_t * ptr, size_t len, int* sampler return 1; } -class CMPEG1Splitter : public base_filter { +class CMPEG1Splitter : public base_filter { public: SRWLOCK srw; // threading: safe CONDITION_VARIABLE wake_parent; // threading: safe @@ -1192,6 +1192,50 @@ class CMPEG1Splitter : public base_filter { // ignore the others, Kirikiri just leaves them as null (and it never uses the group, it just stores it somewhere) return S_OK; } + + // IMediaSeeking + HRESULT STDMETHODCALLTYPE CheckCapabilities(DWORD* pCapabilities) override + { puts("CheckCapabilities"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE ConvertTimeFormat(LONGLONG* pTarget, const GUID* pTargetFormat, LONGLONG Source, const GUID* pSourceFormat) override + { puts("ConvertTimeFormat"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetAvailable(LONGLONG* pEarliest, LONGLONG* pLatest) override + { puts("GetAvailable"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetCapabilities(DWORD* pCapabilities) override + { puts("GetCapabilities"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetCurrentPosition(LONGLONG* pCurrent) override + { puts("GetCurrentPosition"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetDuration(LONGLONG* pDuration) override + { puts("GetDuration"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetPositions(LONGLONG* pCurrent, LONGLONG* pStop) override + { puts("GetPositions"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetPreroll(LONGLONG* pllPreroll) override + { puts("GetPreroll"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetRate(double* pdRate) override + { puts("GetRate"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetStopPosition(LONGLONG* pStop) override + { puts("GetStopPosition"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE GetTimeFormat(GUID* pFormat) override + { puts("GetTimeFormat"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE IsFormatSupported(const GUID* pFormat) override + { + puts("IsFormatSupported"); + if (*pFormat == TIME_FORMAT_MEDIA_TIME) // Wine ignores my IMediaSeeking unless this is supported + return S_OK; + return E_OUTOFMEMORY; + } + HRESULT STDMETHODCALLTYPE IsUsingTimeFormat(const GUID* pFormat) override + { puts("IsUsingTimeFormat"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE QueryPreferredFormat(GUID* pFormat) override + { puts("QueryPreferredFormat"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE SetPositions(LONGLONG* pCurrent, DWORD dwCurrentFlags, LONGLONG* pStop, DWORD dwStopFlags) override + { + puts("SetPositions"); // just do nothing + return S_OK; + } + HRESULT STDMETHODCALLTYPE SetRate(double dRate) override + { puts("SetRate"); return E_OUTOFMEMORY; } + HRESULT STDMETHODCALLTYPE SetTimeFormat(const GUID* pFormat) override + { puts("SetTimeFormat"); return E_OUTOFMEMORY; } }; class CMpegVideoCodec : public base_filter { @@ -1484,13 +1528,7 @@ class CVideoMixingRenderer9 : public com_base { HRESULT STDMETHODCALLTYPE InitializeDevice(DWORD_PTR dwUserID, VMR9AllocationInfo* lpAllocInfo, DWORD* lpNumBuffers) { need_reinit = false; - need_move_window = true; - // DXVK doesn't have this glitch - // I'd rather detect wined3d, not DXVK, but those are the only two options, so good enough - // (native windows shouldn't run this code at all) - CComPtr detect_dxvk; - if (SUCCEEDED(the_d3ddevice->QueryInterface(IID_ID3D9VkInteropDevice, (void**)&detect_dxvk))) - need_move_window = false; + need_move_window = is_debian_build; // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1043052 (likely same as 1051492) the_userid = dwUserID; the_allocinfo = *lpAllocInfo; return parent_alloc->InitializeDevice(dwUserID, lpAllocInfo, lpNumBuffers); @@ -1557,6 +1595,7 @@ class CVideoMixingRenderer9 : public com_base { if (!need_reinit) return; +puts("DOING EVIL REINIT"); need_reinit = false; the_surface[0]->Release(); the_surface[0] = nullptr; @@ -2173,11 +2212,12 @@ class fake_DMOObject final : public IMediaObject { } }; -template +template class ClassFactory : public com_base { public: HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) { +//printf("krkrwine: create %s (iface %s)\n", guid_to_str(clsid), guid_to_str(riid)); if (pUnkOuter != nullptr) return CLASS_E_NOAGGREGATION; return qi_release(new T(), riid, ppvObject); @@ -2185,11 +2225,12 @@ class ClassFactory : public com_base { HRESULT STDMETHODCALLTYPE LockServer(BOOL lock) { return S_OK; } // don't care }; -template +template class AggregatingClassFactory : public com_base { public: HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) { +//printf("krkrwine: create %s (iface %s)\n", guid_to_str(clsid), guid_to_str(riid)); if (pUnkOuter == nullptr) return VFW_E_NEED_OWNER; // wrong if this isn't a VFW object, but who cares, it's an error, good enough if (riid != IID_IUnknown) @@ -2228,20 +2269,24 @@ static void very_unsafe_init() if (initialized) return; initialized = true; +//setvbuf(stdout, nullptr, _IONBF, 0); +//puts("krkrwine - hello world"); // refuse to operate on Windows - if (!GetProcAddress(GetModuleHandle("ntdll.dll"), "wine_get_version")) + auto wine_get_build_id = (const char*(*)())GetProcAddress(GetModuleHandle("ntdll.dll"), "wine_get_version"); + if (!wine_get_build_id) return; + is_debian_build = (strstr(wine_get_build_id(), "Debian")); DWORD ignore; - IClassFactory* fac_mpegsplit = new ClassFactory(); + IClassFactory* fac_mpegsplit = new ClassFactory(); CoRegisterClassObject(CLSID_MPEG1Splitter, fac_mpegsplit, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore); - IClassFactory* fac_mpegvideo = new ClassFactory(); + IClassFactory* fac_mpegvideo = new ClassFactory(); CoRegisterClassObject(CLSID_CMpegVideoCodec, fac_mpegvideo, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore); - IClassFactory* fac_vmr9 = new ClassFactory(); + IClassFactory* fac_vmr9 = new ClassFactory(); CoRegisterClassObject(CLSID_VideoMixingRenderer9, fac_vmr9, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore); // if all I needed was to swap/implement the above objects, I'd put that in install.py @@ -2282,7 +2327,7 @@ static void very_unsafe_init() } // only register these if the fake WMCreateSyncReader can be injected - IClassFactory* fac_dmo = new AggregatingClassFactory(); + IClassFactory* fac_dmo = new AggregatingClassFactory(); CoRegisterClassObject(CLSID_CWMVDecMediaObject, fac_dmo, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore); CoRegisterClassObject(CLSID_CWMADecMediaObject, fac_dmo, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore); } @@ -2301,10 +2346,6 @@ static void very_unsafe_init() EXPORT(HRESULT, DllGetClassObject, (REFCLSID rclsid, REFIID riid, void** ppvObj)) { - //freopen("Z:\\dev\\stdout", "wt", stdout); - //setvbuf(stdout, nullptr, _IONBF, 0); - //puts("krkrwine - hello world"); - very_unsafe_init(); // this DllGetClassObject doesn't actually implement anything, it just calls very_unsafe_init then defers to the original diff --git a/making.md b/making.md index 64ca106..6791e6c 100644 --- a/making.md +++ b/making.md @@ -712,7 +712,7 @@ In conclusion, I've encountered, and had to fill in or work around most of, the - WMCreateSyncReader SetReadStreamSamples is incomplete (and not even tagged semi-stub) - CLSID_CWMVDecMediaObject and CLSID_CWMADecMediaObject are unimplemented - IVMRSurfaceAllocatorNotify9 AllocateSurfaceHelper can return D3DERR_INVALIDCALL and zero out *lpNumBuffers if originally 1 (I don't know if real Windows lets the corresponding call succeed, or if it fails and leaves *lpNumBuffers unchanged, but Kirikiri cannot handle any zeroing) -- Direct3D 9 under wined3d doesn't work in child windows +- Direct3D 9 under wined3d doesn't work in child windows, on Debian's Wine (does not reproduce upstream) - Direct3D 9 under DXVK doesn't work in former child windows promoted to borderless standalone (I didn't investigate this one very closely) - SetWindowLongPtr(GWLP_HWNDPARENT) does not properly create an ownership relation unless the window is hidden and re-shown (unconfirmed what that does on Windows) - At least one drawing API (probably the HDC family, or some subset thereof) only works if the window is in the top left part of the screen @@ -720,3 +720,16 @@ In conclusion, I've encountered, and had to fill in or work around most of, the - WMCreateSyncReader()->IWMSyncReader::SetOutputProps() doesn't reconfigure its allocator; this causes various trouble if the requested pixel format (for example RGB32) has more bits per pixel than the default (RGB24) - Something is causing IWMSyncReader::GetNextSample() to occasionally return VFW_E_NOT_COMMITTED. Unfortunately, I haven't been able to narrow this one down more than 'turn off the VFW_E_NOT_COMMITTED check, and watch the Wagamama High Spec OP video in Proton'. - sink_NewSegment() calls TRACE("pin %p %s:%s, start %s, stop %s, rate %.16e.\n", ...). It renders as [...] start 0.0, stop 95.6, rate e. + +The Sequel +---------- + +Fun fact: Not all Kirikiri games are equal. Some use more engine functionality than others. + +I, of course, implemented only the pieces I needed for microkiri and Wagamama High Spec. This is sufficient for some of them, but far from all - Island Diary calls IMediaPosition::put_CurrentPosition(0) on the filter graph. This is implemented in Wine, but it's implemented by looping through all filters and trying to call IMediaSeeking::SetPositions(), and none of my filters support that. + +Stubbing out everything except that function with an out of memory error yields... the exact same error. And a note in the log about IMediaSeeking::IsFormatSupported(). + +Turns out Wine calls IsFormatSupported(TIME_FORMAT_MEDIA_TIME) on every filter, and if that fails, it ignores the rest of the IMediaSeeking interface. A reasonable implementation, I guess... sure, I can implement that too. + +An annoying side track, but not particularly difficult. Now that I have some of the VNs working, it's obvious that there's only small pieces missing if any specific one fails. diff --git a/reformat.py b/reformat.py new file mode 100755 index 0000000..abf06ba --- /dev/null +++ b/reformat.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +# This script takes a bunch of function prototypes copypasted from MSDN (or whatever it's called these days), and reformats them to valid C++. +# The functions will either call the same function on a parent object, or just return an error. + +parent_name = "" + +text = """ + + +HRESULT CheckCapabilities( + [in, out] DWORD *pCapabilities +);HRESULT ConvertTimeFormat( + [out] LONGLONG *pTarget, + [in] const GUID *pTargetFormat, + [in] LONGLONG Source, + [in] const GUID *pSourceFormat +); + +HRESULT GetAvailable( + [out] LONGLONG *pEarliest, + [out] LONGLONG *pLatest +); + +HRESULT GetCapabilities( + [out] DWORD *pCapabilities +);HRESULT GetCurrentPosition( + [out] LONGLONG *pCurrent +); +HRESULT GetDuration( + [out] LONGLONG *pDuration +);HRESULT GetPositions( + [out] LONGLONG *pCurrent, + [out] LONGLONG *pStop +);HRESULT GetPreroll( + [out] LONGLONG *pllPreroll +);HRESULT GetRate( + [out] double *pdRate +);HRESULT GetStopPosition( + [out] LONGLONG *pStop +); + +HRESULT GetTimeFormat( + [out] GUID *pFormat +);HRESULT IsFormatSupported( + [in] const GUID *pFormat +);HRESULT IsUsingTimeFormat( + [in] const GUID *pFormat +);HRESULT QueryPreferredFormat( + [out] GUID *pFormat +); + +HRESULT SetPositions( + [in, out] LONGLONG *pCurrent, + [in] DWORD dwCurrentFlags, + [in, out] LONGLONG *pStop, + [in] DWORD dwStopFlags +);HRESULT SetRate( + [in] double dRate +);HRESULT SetTimeFormat( + [in] const GUID *pFormat +); + + +""" + +import re +for fn in text.replace("\n", "").split(";"): + if not fn: + continue + ret,name,args = re.match(r" *(.*) ([^ ]+)\((.*)\)", fn).groups() + args = re.sub(r"\[[^\]]+\]", "", args) + args = args.split(',') + for n,arg in enumerate(args): + arg = arg.strip() + args[n] = arg + # print("------") + # print(ret,name,args) + if args: + args_untyped = ','.join(re.search("[A-Za-z0-9_a+]+$", a)[0] for a in args) + else: + args_untyped = '' + # print(args_untyped) + if parent_name: + process = 'return '+parent_name+'->'+name+'('+args_untyped+');' + else: + process = 'return E_OUTOFMEMORY;' + fn = ret+' STDMETHODCALLTYPE '+name+'('+', '.join(args)+') override\n\t{ puts("'+name+'"); '+process+' }' + fn = re.sub(" +", " ", fn).replace("( ", "(").replace(" )", ")").replace(" *", "* ").replace(" *", "* ") + print(fn)