NINJA GAIDEN 4
The game asks Media Foundation whether anything decodes VP9 and quits if the answer is zero; on D3DMetal 4 its fences are repaired so it presents at all. Needs the staged Matroska demuxer too, except on CrossOver Preview 27, which ships it.
What you see
*"Windows is missing required components… install Windows Media Foundation and the VP9 Codec. The game will now exit"* -- and it exits
Why it happens
CrossOver ships no Matroska demuxer, so a WebM is recognised and then has nothing to hand off to
What the fix changes
Stage libgstmatroska beside the decoder; answer the VP9 MFT gate and decode in software
The code
When the engine finds no VP9 decoder, the gate the game asks Media Foundation is answered with the H.264 decoder's entry, so the game goes on and decodes in software.
tools/ng4-observe.c · my_MFTEnumEx, lines 1195–1291static HRESULT WINAPI my_MFTEnumEx(GUID category, UINT32 flags,
const REG_TYPE_INFO *in,
const REG_TYPE_INFO *out,
void ***mfts, UINT32 *count)
{
HRESULT hr = real_MFTEnumEx(category, flags, in, out, mfts, count);
/* The engine's own answer, captured before the substitution below can
* overwrite it. */
BOOL engine_answered_vp9 = SUCCEEDED(hr) && in && count && *count > 0
&& IsEqualGUID(&in->guidSubtype, &guid_VP90);
if (answer_mft_gate && in && count && *count == 0
&& IsEqualGUID(&in->guidSubtype, &guid_VP90))
{
REG_TYPE_INFO substitute = *in;
static LONG said;
const char *how = "H264";
substitute.guidSubtype = guid_H264;
hr = real_MFTEnumEx(category, flags, &substitute, out, mfts, count);
/* H.264 is the first choice and not the only one.
*
* Borrowing one named format's list assumed that format is registered,
* and on a CrossOver that offers no H.264 decoder at all the fallback
* had nothing to hand back: the gate still saw zero and the game still
* quit, on an engine where everything else about the fix was working.
* Measured on a patched Preview 27, where MFTEnumEx offers zero H.264
* MFTs while 26.3 offers some -- so the answer was engine-dependent
* through no property of this title.
*
* Asking with no input type at all means "every video decoder you
* have", whatever this engine happens to register. It keeps the reason
* the substitution is done this way -- these are real MFTs with real
* lifetimes, not an array we fabricated for the game to free -- while
* depending on nothing being present by name. */
if (SUCCEEDED(hr) && *count == 0)
{
hr = real_MFTEnumEx(category, flags, NULL, out, mfts, count);
how = "any video decoder";
}
if (InterlockedIncrement(&said) == 1)
logf_("VP90 had no decoder; asked again for %s and got %u. On 26.3 "
"the game only counts these and never activates them -- if "
"an IMFActivate::ActivateObject line follows on this engine, "
"that assumption does not hold here and this answer is "
"hiding a legible error behind a later one",
how, count ? *count : 0);
if (count && *count == 0)
logf_(" and this engine registers NO video decoder of any kind, so "
"the gate cannot be answered from what is here");
}
logf_("MFTEnumEx flags=0x%lx -> 0x%08lx, %u decoder(s) offered",
flags, hr, count ? *count : 0);
/* Whether this engine registers a VP9 decoder -- measured, not obeyed.
*
* This fix pushes decoding to software by refusing the game a D3D device
* manager. 5f781eb stood that down when the engine offers a decoder of its
* own, reasoning that an engine which can decode VP9 needs no push. Both
* halves of that were wrong, and each was wrong in its own way:
*
* - It read *count after the substitution block below had run. That
* block is the workaround -- when the VP9 ask comes back empty we hand
* the game an H264 list so its gate passes -- so the count was ours.
* Fixed by capturing the game's typed ask first.
* - The idea itself does not hold. Measured 27 Aug on an engine that
* does register VP9 (winevideo's vpx plugin under our winegstreamer):
* the enumeration answers 1 on the first ask, the stand-down fires
* exactly as designed, and the title dies on its first video anyway --
* six access violations, the same ones it dies with everywhere else.
* On stock 26.3, where the ask returns 0 and the refusal stands, the
* videos play through to the menu.
*
* So a decoder existing does not make the hardware path work here. What
* the title needs is the software path, on every engine, which is what
* shipped on 25 Aug. The flag stays because the log is more legible for
* having it -- it says which kind of engine this is -- and nothing reads
* it.
*/
if (engine_answered_vp9)
engine_offers_decoder = TRUE;
(void)engine_offers_decoder;
if (in) describe_subtype("wants to decode", &in->guidSubtype);
/* Watch the promised decoder actually be created. */
if (SUCCEEDED(hr) && mfts && *mfts && count && *count > 0)
{
static void *ao;
if (patch_slot("ActivateObject", (*mfts)[0], SLOT_ACTIVATE_OBJECT, (void *)my_ActivateObject, &ao))
real_ActivateObject = (HRESULT (WINAPI *)(void *, REFIID, void **))ao;
}
if (out) describe_subtype("wants out as ", &out->guidSubtype);
if (count && *count == 0)
logf_(" NOTHING can decode that here -- this is the failure, if the "
"picture is missing and the sound is not");
return hr;
}On D3DMetal 4: a fence wait for a value no one has signalled yet is bridged through the CPU instead of being handed to the GPU.
tools/ng4-observe.c · my_QWaitF, lines 4053–4087static HRESULT WINAPI my_QWaitF(void *q, void *f, UINT64 v)
{
int i, j;
HRESULT hr;
UINT64 done = real_FGetCompletedF ? real_FGetCompletedF(f) : 0, a = v;
LONG k = InterlockedIncrement(&fence_ops);
AcquireSRWLockExclusive(&fence_lock);
i = q_index(q); j = f_index(f, FALSE);
if (i >= 0) { fq[i].wf = f; fq[i].wv = v; fq[i].wt = GetTickCount(); }
if (j >= 0) ff[j].waits++;
if (mono_fences && j >= 0 && ff[j].nbase) { a = mono_wait_(j, v); done = mono_back_(j, done); }
if (i >= 0) fq[i].wv = a;
{
/* Early: no signal of at least this value issued yet, by anyone. */
BOOL early = bridge_waits && mono_fences && i >= 0 && j >= 0 && ff[j].nbase && a > ff[j].top;
ReleaseSRWLockExclusive(&fence_lock);
if (!early && bridge_all && mono_fences && i >= 0 && j >= 0 && real_FGetCompletedF(f) < a)
early = TRUE;
if (early && bridge_wait_(i, q, f, a, &hr))
{
if (k <= 200000) flog_("Q%d(%s) Wait F%d >= %llu [actual %llu] BRIDGED -> 0x%08lx", i,
qtype(fq[i].type), j, (unsigned long long)v, (unsigned long long)a, hr);
return hr;
}
}
hr = real_QWaitF(q, f, a);
if (k <= 200000 || done < v)
flog_("Q%d(%s) Wait F%d >= %llu [actual %llu] (completed %llu, asked %llu by %s%d)%s -> 0x%08lx",
i, i >= 0 ? qtype(fq[i].type) : "?", j, (unsigned long long)v, (unsigned long long)a,
(unsigned long long)done,
j >= 0 ? (unsigned long long)ff[j].asked : 0ULL,
j >= 0 && ff[j].who == -1 ? "CPU" : "Q", j >= 0 ? ff[j].who : -9,
done < v ? " NOT YET MET" : "", hr);
return hr;
}Details
- Needs
- D3DMetal, D3D12
- Installer
- install-ng4-fix.sh
- Executable
- NINJAGAIDEN4-Steam.exe
- Built from
- ../tools/ng4-observe.c
- Verified on
- Stable 26.3.0.39832, measured on a stock install as well as on this project's patched engine; CrossOver Preview 27 through NotProton, with nothing staged (2026-10-14)