using DirectShowLib; using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using WPFMediaKit.DirectX; namespace WPFMediaKit.DirectShow.MediaPlayers; /// /// The Vmr9Allocator is a custom allocator for the VideoMixingRenderer9 /// [ComVisible(true)] public class Vmr9Allocator : IVMRSurfaceAllocator9, IVMRImagePresenter9, ICustomAllocator { internal const string VMR9_ERROR = "Do you have the graphics driver and DirectX properly installed?"; /// /// Base constant for FAIL error codes /// private const int E_FAIL = unchecked((int)0x80004005); /// /// The SDK version of D3D we are using /// private const ushort D3D_SDK_VERSION = 32; /// /// Lock for shared resources /// private static object m_staticLock = new object(); /// /// Direct3D functions /// private IDirect3D9 m_d3d; /// /// Direct3D functions of Vista /// private IDirect3D9Ex m_d3dEx; /// /// The window handle, needed for D3D intialization /// private static readonly IntPtr m_hWnd; /// /// The Direct3D device /// private IDirect3DDevice9 m_device; /// /// Lock for instance's resources /// private readonly object m_instanceLock = new object(); /// /// Part of the "Dispose" pattern /// private bool m_disposed; public bool IsDisposed { get { return m_disposed; } } /// /// Applications use this interface to set a custom allocator-presenter /// and the allocator-presenter uses this interface to inform the VMR of /// changes to the system environment that affect the Direct3D surfaces. /// private IVMRSurfaceAllocatorNotify9 m_allocatorNotify; /// /// Fires each time a frame needs to be presented /// public event Action NewAllocatorFrame; /// /// Fires when new D3D surfaces are allocated /// public event NewAllocatorSurfaceDelegate NewAllocatorSurface; /// /// Private surface for YUV stuffs /// private IDirect3DSurface9 m_privateSurface; /// /// Private texture for YUV stuffs /// private IDirect3DTexture9 m_privateTexture; [DllImport("user32.dll", SetLastError = false)] private static extern IntPtr GetDesktopWindow(); static Vmr9Allocator() { m_hWnd = GetDesktopWindow(); } /// /// Creates a new VMR9 custom allocator to use with Direct3D /// public Vmr9Allocator() { /* Use the 9Ex for Vista */ int hr = 0; if (IsVistaOrBetter) hr = Direct3D.Direct3DCreate9Ex(D3D_SDK_VERSION, out m_d3dEx); else /* For XP */ m_d3d = Direct3D.Direct3DCreate9(D3D_SDK_VERSION); if (m_d3dEx == null && m_d3d == null) { string hrStr = hr == 0 ? "" : $"({hr:X})"; throw new WPFMediaKitException($"Could not create IDirect3D9 {hrStr}. " + VMR9_ERROR); } CreateDevice(); } /// /// Fires the OnNewAllocatorSurface event, notifying the /// subscriber that new surfaces are available /// private void InvokeNewSurfaceEvent(IntPtr pSurface) { if (NewAllocatorSurface != null) NewAllocatorSurface(this, pSurface); } /// /// Fires the NewAllocatorFrame event notifying the /// subscriber that a new frame is ready to be presented /// private void InvokeNewAllocatorFrame() { Action newAllocatorFrameHandler = NewAllocatorFrame; if (newAllocatorFrameHandler != null) newAllocatorFrameHandler(); } /// /// Frees any remaining unmanaged memory /// public void Dispose() { Dispose(true); //GC.SuppressFinalize(this); } /// /// Part of the dispose pattern /// protected virtual void Dispose(bool disposing) { //if (m_disposed) return; if (disposing) { InvokeNewSurfaceEvent(IntPtr.Zero); /* Pass a dummy cookie to TerminateDevice */ TerminateDevice(IntPtr.Zero); if (m_allocatorNotify != null) { Marshal.FinalReleaseComObject(m_allocatorNotify); m_allocatorNotify = null; } if (m_d3d != null) { Marshal.FinalReleaseComObject(m_d3d); m_d3d = null; } if (m_d3dEx != null) { Marshal.FinalReleaseComObject(m_d3dEx); m_d3dEx = null; } if (m_device != null) { Marshal.FinalReleaseComObject(m_device); m_device = null; } } m_disposed = true; } /// /// Current Direct3D surfaces our allocator has ready and allocated /// private IntPtr[] DxSurfaces { get; set; } /// /// The StartPresenting method is called just before the video starts playing. /// The allocator-presenter should perform any necessary configuration in this method. /// /// /// An application-defined DWORD_PTR cookie that uniquely identifies this instance of the /// VMR for use in scenarios when one instance of the allocator-presenter is used with multiple VMR instances. /// /// Returns an HRESULT public int StartPresenting(IntPtr userId) { return m_device == null ? E_FAIL : 0; } /// /// The StopPresenting method is called just after the video stops playing. /// The allocator-presenter should perform any necessary cleanup in this method. /// /// /// An application-defined DWORD_PTR cookie that uniquely identifies this instance of the /// VMR for use in scenarios when one instance of the allocator-presenter is used with multiple VMR instances. /// /// public int StopPresenting(IntPtr dwUserID) { return 0; } /// /// The PresentImage method is called at precisely the moment this video frame should be presented. /// /// /// An application-defined DWORD_PTR that uniquely identifies this instance of the VMR in scenarios when /// multiple instances of the VMR are being used with a single instance of an allocator-presenter. /// /// /// Specifies a VMR9PresentationInfo structure that contains information about the video frame. /// /// Returns an HRESULT public int PresentImage(IntPtr dwUserID, ref VMR9PresentationInfo lpPresInfo) { VMR9PresentationInfo presInfo = lpPresInfo; int hr = 0; try { lock (m_staticLock) { /* Test to see if our device was lost, is so fix it */ TestRestoreLostDevice(); if (m_privateSurface != null) hr = m_device.StretchRect(presInfo.lpSurf, presInfo.rcSrc, m_privateSurface, presInfo.rcDst, 0); if (hr < 0) return hr; } /* Notify to our listeners we just got a new frame */ InvokeNewAllocatorFrame(); hr = 0; } catch (Exception) { hr = E_FAIL; } return hr; } /// /// Tests if the D3D device has been lost and if it has /// it is retored. This happens on XP with things like /// resolution changes or pressing ctrl + alt + del. With /// Vista, this will most likely never be called unless the /// video driver hangs or is changed. /// private void TestRestoreLostDevice() { if (m_device == null) return; /* This will throw an exception * if the device is lost */ int hr = m_device.TestCooperativeLevel(); /* Do nothing if S_OK */ if (hr == 0) return; FreeSurfaces(); CreateDevice(); /* TODO: This is bad. FIX IT! * Figure out how to tell when the new * device is ready to use */ Thread.Sleep(1500); IntPtr pDev = GetComPointer(m_device); /* Update with our new device */ m_allocatorNotify.ChangeD3DDevice(pDev, GetAdapterMonitor(0)); } /// /// Gets the pointer to the adapter monitor /// /// The ordinal of the adapter /// A pointer to the adaptor monitor private IntPtr GetAdapterMonitor(uint adapterOrdinal) { IntPtr pMonitor = m_d3dEx != null ? m_d3dEx.GetAdapterMonitor(adapterOrdinal) : m_d3d.GetAdapterMonitor(adapterOrdinal); return pMonitor; } /// /// The InitializeDevice method is called by the Video Mixing Renderer 9 (VMR-9) /// when it needs the allocator-presenter to allocate surfaces. /// /// /// Application-defined identifier. This value is the same value that the application /// passed to the IVMRSurfaceAllocatorNotify9.AdviseSurfaceAllocator method in the /// dwUserID parameter. /// /// /// Pointer to a VMR9AllocationInfo structure that contains a description of the surfaces to create. /// /// /// On input, specifies the number of surfaces to create. When the method returns, /// this parameter contains the number of buffers that were actually allocated. /// /// Returns an HRESULT code public int InitializeDevice(IntPtr userId, ref VMR9AllocationInfo lpAllocInfo, ref int lpNumBuffers) { if (m_allocatorNotify == null) { return E_FAIL; } try { int hr; lock (m_staticLock) { /* These two pointers are passed to the the helper * to create our D3D surfaces */ var pDevice = GetComPointer(m_device); var pMonitor = GetAdapterMonitor(0); /* Setup our D3D Device with our renderer */ hr = m_allocatorNotify.SetD3DDevice(pDevice, pMonitor); DsError.ThrowExceptionForHR(hr); /* This is only used if the AllocateSurfaceHelper is used */ lpAllocInfo.dwFlags |= VMR9SurfaceAllocationFlags.TextureSurface; /* Make sure our old surfaces are free'd */ FreeSurfaces(); /* This is an IntPtr array of pointers to D3D surfaces */ DxSurfaces = new IntPtr[lpNumBuffers]; /* This is where the magic happens, surfaces are allocated */ hr = m_allocatorNotify.AllocateSurfaceHelper(ref lpAllocInfo, ref lpNumBuffers, DxSurfaces); if (hr < 0) { FreeSurfaces(); if (lpAllocInfo.Format > 0) { hr = m_device.CreateTexture(lpAllocInfo.dwWidth, lpAllocInfo.dwHeight, 1, 1, D3DFORMAT.D3DFMT_X8R8G8B8, 0, out m_privateTexture, IntPtr.Zero); DsError.ThrowExceptionForHR(hr); hr = m_privateTexture.GetSurfaceLevel(0, out m_privateSurface); DsError.ThrowExceptionForHR(hr); } lpAllocInfo.dwFlags &= ~VMR9SurfaceAllocationFlags.TextureSurface; lpAllocInfo.dwFlags |= VMR9SurfaceAllocationFlags.OffscreenSurface; DxSurfaces = new IntPtr[lpNumBuffers]; hr = m_allocatorNotify.AllocateSurfaceHelper(ref lpAllocInfo, ref lpNumBuffers, DxSurfaces); if (hr < 0) { FreeSurfaces(); return hr; } } } /* Nofity to our listeners we have new surfaces */ InvokeNewSurfaceEvent(m_privateSurface != null ? GetComPointer(m_privateSurface) : DxSurfaces[0]); return hr; } catch { return E_FAIL; } } /// /// The TerminateDevice method releases the Direct3D device. /// /// /// Application-defined identifier. This value is the same value that the application /// passed to the IVMRSurfaceAllocatorNotify9.AdviseSurfaceAllocator method /// in the dwUserID parameter. /// /// public int TerminateDevice(IntPtr id) { FreeSurfaces(); return 0; } /// /// The GetSurface method retrieves a Direct3D surface /// /// /// Application-defined identifier. This value is /// the same value that the application passed to the /// IVMRSurfaceAllocatorNotify9.AdviseSurfaceAllocator /// method in the dwUserID parameter. /// /// /// Specifies the index of the surface to retrieve. /// /// /// /// Address of a variable that receives an IDirect3DSurface9 /// interface pointer. The caller must release the interface. /// public int GetSurface(IntPtr userId, int surfaceIndex, int surfaceFlags, out IntPtr lplpSurface) { lplpSurface = IntPtr.Zero; if (DxSurfaces == null) return E_FAIL; if (surfaceIndex > DxSurfaces.Length) return E_FAIL; lock (m_instanceLock) { if (DxSurfaces == null) return E_FAIL; lplpSurface = DxSurfaces[surfaceIndex]; /* Increment the reference count to our surface, * which is a pointer to a COM object */ Marshal.AddRef(lplpSurface); return 0; } } /// /// The AdviseNotify method provides the allocator-presenter with the VMR-9 filter's /// interface for notification callbacks. If you are using a custom allocator-presenter, /// the application must call this method on the allocator-presenter, with a pointer to /// the VMR's IVMRSurfaceAllocatorNotify9 interface. The allocator-presenter uses this /// interface to communicate with the VMR. /// /// /// Specifies the IVMRSurfaceAllocatorNotify9 interface that the allocator-presenter will /// use to pass notifications back to the VMR. /// Returns an HRESULT value public int AdviseNotify(IVMRSurfaceAllocatorNotify9 lpIVMRSurfAllocNotify) { lock (m_staticLock) { m_allocatorNotify = lpIVMRSurfAllocNotify; var pMonitor = GetAdapterMonitor(0); var pDevice = GetComPointer(m_device); return m_allocatorNotify.SetD3DDevice(pDevice, pMonitor); } } /// /// Gets a native pointer to a COM object. This method does not /// add a reference count. /// /// The RCW to the COM object /// Pointer to the COM object private static IntPtr GetComPointer(object comObj) { if (!Marshal.IsComObject(comObj)) throw new ArgumentException("comObj"); IntPtr pComObj = Marshal.GetIUnknownForObject(comObj); /* Get IUnknownForObject adds a reference count * to the COM object so we remove a reference count * before we return the pointer to avoid any possible * memory leaks with COM */ Marshal.Release(pComObj); return pComObj; } ~Vmr9Allocator() { Dispose(); } /// /// Gets if the current operating system is /// Windows Vista or higher. /// private static bool IsVistaOrBetter { get { return Environment.OSVersion.Version.Major >= 6; } } /// /// Creates a Direct3D device /// [MethodImpl(MethodImplOptions.Synchronized)] private void CreateDevice() { if (m_device != null) return; var param = new D3DPRESENT_PARAMETERS { Windowed = 1, Flags = ((short)D3DPRESENTFLAG.D3DPRESENTFLAG_VIDEO), BackBufferFormat = D3DFORMAT.D3DFMT_X8R8G8B8, SwapEffect = D3DSWAPEFFECT.D3DSWAPEFFECT_COPY }; /* The COM pointer to our D3D Device */ IntPtr dev; /* Windows Vista runs much more performant with the IDirect3DDevice9Ex */ int hr = 0; if (m_d3dEx != null) { hr = m_d3dEx.CreateDeviceEx(0, D3DDEVTYPE.D3DDEVTYPE_HAL, m_hWnd, CreateFlags.D3DCREATE_SOFTWARE_VERTEXPROCESSING | CreateFlags.D3DCREATE_MULTITHREADED, ref param, IntPtr.Zero, out dev); } else/* Windows XP */ { hr = m_d3d.CreateDevice(0, D3DDEVTYPE.D3DDEVTYPE_HAL, m_hWnd, CreateFlags.D3DCREATE_SOFTWARE_VERTEXPROCESSING | CreateFlags.D3DCREATE_MULTITHREADED, ref param, out dev); } if (dev == IntPtr.Zero) throw new WPFMediaKitException($"Cannot create D3D device ({hr:X}). Do you have D3D acceleration enabled for your graphics card?"); m_device = (IDirect3DDevice9)Marshal.GetObjectForIUnknown(dev); Marshal.Release(dev); } /// /// Releases reference to all allocated D3D surfaces /// private void FreeSurfaces() { lock (m_instanceLock) { if (m_privateSurface != null) { Marshal.ReleaseComObject(m_privateSurface); m_privateSurface = null; } if (m_privateTexture != null) { Marshal.ReleaseComObject(m_privateTexture); m_privateTexture = null; } if (DxSurfaces != null) { foreach (var ptr in DxSurfaces) { if (ptr != IntPtr.Zero) { /* Release COM reference */ Marshal.Release(ptr); } } } /* Make sure we uninitialize the pointer array * so the rest of our code knows it is invalid */ DxSurfaces = null; } } }