Skip to content

Commit

Permalink
Implement a stub IMediaSeeking on the mpeg splitter, some VNs need that
Browse files Browse the repository at this point in the history
  • Loading branch information
Alcaro committed Sep 12, 2023
1 parent e1e6746 commit 5a6b32f
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 23 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------------
Expand Down
83 changes: 62 additions & 21 deletions krkrwine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -65,6 +63,8 @@ static char* guid_to_str(const GUID& guid)
template<typename T> T min(T a, T b) { return a < b ? a : b; }
template<typename T> T max(T a, T b) { return a > b ? a : b; }

static bool is_debian_build;


template<typename T> class CComPtr {
void assign(T* ptr)
Expand Down Expand Up @@ -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<CMPEG1Splitter, IAMStreamSelect> {
class CMPEG1Splitter : public base_filter<CMPEG1Splitter, IAMStreamSelect, IMediaSeeking> {
public:
SRWLOCK srw; // threading: safe
CONDITION_VARIABLE wake_parent; // threading: safe
Expand Down Expand Up @@ -1192,6 +1192,50 @@ class CMPEG1Splitter : public base_filter<CMPEG1Splitter, IAMStreamSelect> {
// 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<CMpegVideoCodec> {
Expand Down Expand Up @@ -1484,13 +1528,7 @@ class CVideoMixingRenderer9 : public com_base<IBaseFilter> {
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<IUnknown> 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);
Expand Down Expand Up @@ -1557,6 +1595,7 @@ class CVideoMixingRenderer9 : public com_base<IBaseFilter> {
if (!need_reinit)
return;

puts("DOING EVIL REINIT");
need_reinit = false;
the_surface[0]->Release();
the_surface[0] = nullptr;
Expand Down Expand Up @@ -2173,23 +2212,25 @@ class fake_DMOObject final : public IMediaObject {
}
};

template<typename T>
template<typename T, const GUID& clsid>
class ClassFactory : public com_base<IClassFactory> {
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);
}
HRESULT STDMETHODCALLTYPE LockServer(BOOL lock) { return S_OK; } // don't care
};

template<typename T>
template<typename T, const GUID& clsid>
class AggregatingClassFactory : public com_base<IClassFactory> {
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)
Expand Down Expand Up @@ -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<CMPEG1Splitter>();
IClassFactory* fac_mpegsplit = new ClassFactory<CMPEG1Splitter, CLSID_MPEG1Splitter>();
CoRegisterClassObject(CLSID_MPEG1Splitter, fac_mpegsplit, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore);

IClassFactory* fac_mpegvideo = new ClassFactory<CMpegVideoCodec>();
IClassFactory* fac_mpegvideo = new ClassFactory<CMpegVideoCodec, CLSID_CMpegVideoCodec>();
CoRegisterClassObject(CLSID_CMpegVideoCodec, fac_mpegvideo, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore);

IClassFactory* fac_vmr9 = new ClassFactory<CVideoMixingRenderer9>();
IClassFactory* fac_vmr9 = new ClassFactory<CVideoMixingRenderer9, CLSID_VideoMixingRenderer9>();
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
Expand Down Expand Up @@ -2282,7 +2327,7 @@ static void very_unsafe_init()
}

// only register these if the fake WMCreateSyncReader can be injected
IClassFactory* fac_dmo = new AggregatingClassFactory<fake_DMOObject>();
IClassFactory* fac_dmo = new AggregatingClassFactory<fake_DMOObject, CLSID_CWMVDecMediaObject>();
CoRegisterClassObject(CLSID_CWMVDecMediaObject, fac_dmo, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore);
CoRegisterClassObject(CLSID_CWMADecMediaObject, fac_dmo, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &ignore);
}
Expand All @@ -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
Expand Down
15 changes: 14 additions & 1 deletion making.md
Original file line number Diff line number Diff line change
Expand Up @@ -712,11 +712,24 @@ 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
- Clearing WS_CLIPCHILDREN, drawing on the parent with Direct3D 9, then restoring WS_CLIPCHILDREN, seems to break drawing on the child window using the above drawing API (I didn't investigate this one very closely)
- 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.
90 changes: 90 additions & 0 deletions reformat.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 5a6b32f

Please sign in to comment.