Mortal Kombat 11

Has the face morph read its three-float deltas the way D3DMetal and DXMT can, which removes the dark patches, and gives the movies their colours and their frame order back. DirectX 12 and 11 alike.

What you see

Bare faces -- forehead, brows, around the eyes -- turn into smeared dark patches with hard edges that follow the light and change from frame to frame. Clean right after a screen loads, broken once the face animates; barely visible in a fight, glaring in close views, the character select and the fatalities. Masked fighters never show it. It has been there since at least an M1 Pro

Why it happens

The face morph reads its per-vertex deltas from DXGI_FORMAT_R32G32B32_FLOAT buffer views, and D3DMetal serves those as two floats an element with z = 0

What the fix changes

Every such view is created as an R32_FLOAT view three times as long, and the two morph shaders read a delta's three floats one by one

The code

The faces. Every three-float buffer view is created as a one-float view three times as long, which D3DMetal and DXMT both read correctly; the rewritten morph shaders then read a delta's three floats one by one.

fixes/mk11-face-fix.c · hk_CreateShaderResourceView, lines 155–172
c
static void STDMETHODCALLTYPE hk_CreateShaderResourceView(ID3D12Device *dev, ID3D12Resource *res,
        const D3D12_SHADER_RESOURCE_VIEW_DESC *desc, D3D12_CPU_DESCRIPTOR_HANDLE h)
{
    void (STDMETHODCALLTYPE *orig)(ID3D12Device *, ID3D12Resource *, const D3D12_SHADER_RESOURCE_VIEW_DESC *,
            D3D12_CPU_DESCRIPTOR_HANDLE) = orig_fn(dev, DEV_IDX(CreateShaderResourceView));
    if (desc && desc->ViewDimension == D3D12_SRV_DIMENSION_BUFFER && desc->Format == DXGI_FORMAT_R32G32B32_FLOAT
            && !(desc->Buffer.Flags & D3D12_BUFFER_SRV_FLAG_RAW))
    {
        D3D12_SHADER_RESOURCE_VIEW_DESC one = *desc;
        one.Format = DXGI_FORMAT_R32_FLOAT;
        one.Buffer.FirstElement = desc->Buffer.FirstElement * 3;
        one.Buffer.NumElements = desc->Buffer.NumElements * 3;
        InterlockedIncrement(&views_made);
        orig(dev, res, &one, h);
        return;
    }
    orig(dev, res, desc, h);
}

The colours. CrossOver's H.264 decoder leaves NV12 out of the types it offers, so the game took YV12 and drew it as NV12. NV12 is offered again, first.

fixes/mk11-movie.c · movie_GetOutputAvailableType, lines 324–345
c
static HRESULT STDMETHODCALLTYPE movie_GetOutputAvailableType(IMFTransform *t, DWORD stream, DWORD index,
        IMFMediaType **type)
{
    HRESULT (STDMETHODCALLTYPE *real)(IMFTransform *, DWORD, DWORD, IMFMediaType **)
            = orig_fn(t, MFT_IDX(GetOutputAvailableType));
    IMFMediaType *first = NULL, *nv12 = NULL;
    GUID sub = { 0 };
    if (!type || FAILED(real(t, stream, 0, &first)) || !first) return real(t, stream, index, type);
    if (SUCCEEDED(IMFMediaType_GetGUID(first, &movie_subtype, &sub)) && IsEqualGUID(&sub, &movie_nv12))
    {
        IMFMediaType_Release(first);
        return real(t, stream, index, type);
    }
    if (index) { IMFMediaType_Release(first); return real(t, stream, index - 1, type); }
    if (FAILED(create_media_type(&nv12)) || !nv12) { *type = first; return S_OK; }
    IMFMediaType_CopyAllItems(first, (IMFAttributes *)nv12);
    IMFMediaType_SetGUID(nv12, &movie_subtype, &movie_nv12);
    IMFMediaType_Release(first);
    if (!InterlockedExchange(&nv12_offered, 1)) log_msg("movie: the H.264 decoder offers no NV12; it is offered first");
    *type = nv12;
    return S_OK;
}

The frame order. The decoder hands frames out in decode order; the fix holds them and hands out the one that comes first in display order.

fixes/mk11-movie.c · hand_out, lines 304–321
c
static HRESULT hand_out(movie_decoder *d, MFT_OUTPUT_DATA_BUFFER *out, DWORD *status)
{
    int i, best = 0;
    BOOL ok;
    BYTE *data;
    DWORD cap;
    for (i = 1; i < d->nheld; i++) if (d->held[i].key < d->held[best].key) best = i;
    ok = move_frame(out->pSample, &d->held[best].data, &d->held[best].len, &d->held[best].cap, TRUE);
    if (best) d->moved++;
    /* The rest stay oldest first; the emptied buffer goes to the end for reuse. */
    data = d->held[best].data; cap = d->held[best].cap;
    memmove(&d->held[best], &d->held[best + 1], (size_t)(d->nheld - best - 1) * sizeof(d->held[0]));
    d->nheld--;
    d->held[d->nheld].data = data; d->held[d->nheld].cap = cap; d->held[d->nheld].len = 0;
    out->dwStatus = 0;
    if (status) *status = 0;
    return ok ? S_OK : E_FAIL;
}

Thanks to

  • Anton PavliukFound and published first that the movie frames come out in decode order, and the reordering (MIT, DirectX 11 only); mk11-movie.c is a separate implementation.

Details

Needs
D3DMetal with D3D12 (MK11_DX12.exe) and DXMT with D3D11 (MK11.exe): one install covers both, whichever Steam starts
Installer
install-mk11-fix.sh
Executable
MK11_DX12.exe
Built from
mk11-face-fix.c, sm2fix_core.c, mk11-morph-shaders.h, mk11-movie.c
Verified on
Preview 27 through NotProton, Game Porting Toolkit 4.0b3 (2026-10-16 and 17)

The full story, with the measurements and the wrong turns