Notes on writing a VMR-9 custom allocator/presenter in C# PDF Print E-mail
The latest release of the Video Mixing Renderer (VMR-9) offers very interesting possibilities. It has a modular design that allows developpers to replace its main components by custom made components. We can replace the Allocator/Presenter and the Compositor component. In these notes, I'll describe how to write a custom allocator/presenter in C#. The code is not as well polished and robust as I would like it. But it should serve as a good starting point for others to build upon. Once again, the code can be found in a zip file.

The documentation for this task is contained in a single page of the DirectShow SDK documentation entitled "Supplying a Custom Allocator-Presenter for VMR-9". The VMR9Allocator sample (in the VMR-9 samples directory) was used as the base for our implementation.

First, we have to deal with accessing the required interfaces in C#. The DirectShowLib contains these definition in their "untested" sections. I looked at these, and I decided to create my own custom library because the "untested" version of IVMRSurfaceAllocatorNotify9.AllocateSurfaceHelper had an object for last argument, and I really prefer just a plain old IntPtr. So I wrote a small idl file with the needed interfaces which contain the following (plus a few that we won't need, all this taken from customap.idl)

 

  1. interface IGraphBuilder;
  2. interface IMediaControl;
  3. interface IMediaEventEx;
  4. interface IMediaSeeking;
  5. interface IVMRFilterConfig9;
  6. interface IVMRMixerControl9;
  7. interface IFileSinkFilter;
  8. interface IVMRSurfaceAllocator9;
  9. interface IVMRImagePresenter9;
  10. interface IVMRSurfaceAllocatorNotify9;
  11. interface IVMRMonitorConfig9;
 

During the development, I had to modify the standard marshaler behaviour for IVMRSurfaceAllocatorNotify9.SetD3DDevice because it creates this method with ref uint arguments and the only way I found to provide an interface pointer to the device needed for the first argument of this method was through the property Device.UnmanagedComPointer (which require the /unsafe switch to the C# compiler). The modification is minor and was created as follows: I ran the build_customaptypelib command file to produce an interop assembly for the interfaces listed above. I disassembled the assembly just produced (with the command "ildasm /out="tmp.il" customaptypelib.dll" and replace, in the file "tmp.il", the two arguments to SetD3DDevice with native int. Then I reassembled the assembly with the command: ilasm /dll /output=customaptypelib.dll" tmp.il. These steps are well explained in the section entitled "Editing An Interop Assembly" of the .Net SDK documentation but it makes you wonder why Microsoft doesn't provide a type library (.tlb) file for DirectShow. Now, we are ready to actually implement our custom allocator/presenter.

 

  1.  
  2. using Microsoft.DirectX;
  3. using Microsoft.DirectX.Direct3D;
  4. using CustomAPTypeLib;
 

The VMR-9 renderer will hand over video samples as Direct3D surfaces to our custom allocator/presenter, so we need access to Direct3D and we reference these assemblies (plus our own CustomAPTypeLib). Then, we declare variables for the interfaces that we haven't seen before:

  1. IVMRFilterConfig9 fc = null;
  2. IVMRSurfaceAllocatorNotify9 san = null;
  3. CustomAP customAP = null;
 

The interface IVMRFilterConfig9 is needed to set the VMR-9 filter in "renderless" mode i.e. the filter will not render samples but will let our "presenter" component do the job. The IVMRSurfaceAllocatorNotify9 interface is implemented on the VMR-9 filter and allows the renderer and our component to communicate. The customAP variable is our custom allocator/presenter component that implement two interfaces; it is declared as follows: (taken from CustomAP.cs)

  1. public class CustomAP : IVMRSurfaceAllocator9, IVMRImagePresenter9
 

We'll come back to these two interfaces a little later. In the function VMR9Alloc which build the filter graph, we find:

 

  1. // create the vmr object
  2. t = Type.GetTypeFromCLSID( Vmr9Guid );
  3. vmr = Activator.CreateInstance( t );
  4. // configure the vmr in "renderless" mode
  5. fc = (IVMRFilterConfig9)vmr;
  6. fc.SetRenderingMode( 0x00000004 ); //VMRMode_Renderless = 0x00000004
  7. // create our custom a/p
  8. customAP = new CustomAP( this );
  9. // tell the renderer about our allocator
  10. san = (IVMRSurfaceAllocatorNotify9)vmr;
  11. san.AdviseSurfaceAllocator( 0, customAP );
  12. // tell our AP about the render
  13. IVMRSurfaceAllocator9 sa = (IVMRSurfaceAllocator9)customAP;
  14. sa.AdviseNotify( san );
  15. gb.AddFilter( (IBaseFilter)vmr, "Video Mixing Renderer9" );
 

We create the VMR-9 renderer and access its IVMRFilterConfig9 interface to call the method SetRenderingMode with a "renderless" argument. Then we create our custom A/P and tell the renderer about it using the interface IVMRSurfaceAllocatorNotify9 and the method AdviseSurfaceAllocator. While it's not clear from the documentation, accessing this interface on the renderer should be done after setting the renderer in "renderless" mode because some cards will not allow it otherwise (I learned it the hard way :-| We tell our component about the renderer with a call to AdviseNotify and let "Intelligent Connect" do its magic.

We are now ready to look at the implementation of our allocator/presenter component. Its member variables are as follows:

  1. // our direct3d device
  2. Device device = null;
  3. // we need to remember our renderer
  4. IVMRSurfaceAllocatorNotify9 m_san = null;
  5. // a ref to our main window (needed in when we create a device)
  6. Form m_window = null;
  7. // our render target surface
  8. Surface m_renderTarget = null;
  9. // an array of pointer to surfaces we allocate
  10. IntPtr [] surfaces = null;
  11. // our object to draw a scene
  12. Scene m_scene = null;
  13. // a guid for the Direct3DTexture interface
  14. Guid IID_IDirect3DTexture9;
 

The comments make these declarations fairly straightforward. Then we have the method CreateDevice

 

  1. void CreateDevice()
  2. {
  3. AdapterInformation ai = Manager.Adapters[0];
  4. DisplayMode dm = ai.CurrentDisplayMode;
  5. PresentParameters pp = new PresentParameters();
  6. pp.Windowed = true;
  7. pp.DeviceWindow = m_window;
  8. pp.SwapEffect = SwapEffect.Copy;
  9. pp.BackBufferFormat = dm.Format;
  10. device = new Device( 0, DeviceType.Hardware, m_window,
  11. CreateFlags.SoftwareVertexProcessing, pp );
  12. device.DeviceResizing += new CancelEventHandler( cancelResizing );
  13. m_renderTarget = device.GetRenderTarget( 0 );
  14. }
 

This is a direct translation of the C++ code in the VMR9Allocator sample. The only difference is the new handler for cancelling the DeviceResizing event. The vertex buffer for our rectangle, in the Scene class, will be declared with the Pool.Managed as argument and we'll be happy with the default DirectX behaviour (instead of the smarter processing done by Managed DirectX).

The DirectShow documentation says on point 6 (for supplying a custom A/P): "Allocate surfaces in the IVMRSurfaceAllocator9::InitializeDevice callback method". So this what we are going to do next. We start with:

 

  1. uint width = 1, height = 1;
  2. while( width < lpAllocInfo.dwWidth ) width = width << 1;
  3. while( height < lpAllocInfo.dwHeight ) height = height << 1;
  4. float fTU = 1.0f, fTV = 1.0f;
  5. fTU = (float)(lpAllocInfo.dwWidth) / (float)(width);
  6. fTV = (float)(lpAllocInfo.dwHeight) / (float)(height);
  7. m_scene.SetSrcRect( fTU, fTV );
  8. lpAllocInfo.dwWidth = width;
  9. lpAllocInfo.dwHeight = height;
 

Because textures whose dimensions are not power of 2 suffer significant performance penalties on many cards, we fix the dimension to satisfy this requirement. We also arrange our texture coordinates to represent our new dimension. Following this, we make sure that we are requesting "texture" surfaces and call AllocateSurfaceHelper on the IVMRSurfaceAllocatorNotify9 of the renderer.

  1. // VMR9AllocFlag_TextureSurface = 0x0004
  2. lpAllocInfo.dwFlags |= 0x0004;
  3. DeleteSurfaces();
  4. // allocate memory for our surface pointers
  5. IntPtr buffer = Marshal.AllocCoTaskMem( Marshal.SizeOf( typeof( IntPtr )) *
  6. (int)lpNumBuffers );
  7. // ask the renderer to create these surfaces
  8. m_san.AllocateSurfaceHelper( ref lpAllocInfo, ref lpNumBuffers, buffer );
  9. // retrieve the pointers and stuff them in our array
  10. surfaces = new IntPtr[lpNumBuffers];
  11. for( int i = 0; i < lpNumBuffers; i++ )
  12. {
  13. // following only work on 32 bit machines
  14. surfaces[i] = Marshal.ReadIntPtr(buffer , i*4);
  15. }
  16. Marshal.FreeCoTaskMem( buffer );
 

We are keeping pointers to surfaces in our surfaces array which is sufficient for our purpose; so we just read the values returned by AllocateSurfaceHelper and stuff them in our array. After that, we call Init on our Scene

The implementation of GetSurface just returned one of the pointers returned previously:

  1. // IVMRSurfaceAllocator9.GetSurface
  2. public void GetSurface ( UInt32 dwUserID ,
  3. UInt32 SurfaceIndex ,
  4. UInt32 SurfaceFlags ,
  5. IntPtr lplpSurface )
  6. {
  7. if( SurfaceIndex > surfaces.Length )
  8. throw new Exception( "can't get surface" );
  9. lock( this )
  10. {
  11. Marshal.WriteIntPtr( lplpSurface, surfaces[SurfaceIndex] );
  12. }
  13. }
 

Since lplpSurface points in native memory, we call WriteIntPtr to copy our surface pointer into this location. AdviseNotify stores away the pointer to our renderer and asks it to SetD3DDevice:

  1. // IVMRSurfaceAllocator9.AdviseNotify
  2. public unsafe void AdviseNotify ( IVMRSurfaceAllocatorNotify9 lpIVMRSurfAllocNotify )
  3. {
  4. m_san = lpIVMRSurfAllocNotify;
  5. AdapterInformation ai = Manager.Adapters.Default;
  6. IntPtr hMonitor = Manager.GetAdapterMonitor(ai.Adapter);
  7. lock( this )
  8. {
  9. m_san.SetD3DDevice( (IntPtr)device.UnmanagedComPointer, hMonitor );
  10. }
  11. }
 

I haven't found a better way to do this in C#. PresentImage is called by the renderer when it wants our component to display a video frame (or sample). The code simply sets the rendering target, gets the texture from the surface field of the PresentationInfo structure, calls our Scene.DrawScene and finally dump it to the screen with Device.Present.

  1. Surface s = null;
  2. Texture texture = null;
  3. // IVMRImagePresenter9.PresentImage
  4. public void PresentImage ( System.UInt32 dwUserID , ref _VMR9PresentationInfo lpPresInfo )
  5. {
  6. // should check args and lock
  7. if( lpPresInfo.lpSurf == IntPtr.Zero )
  8. throw new Exception( "Presentation Info corrupted" );
  9. lock( this )
  10. {
  11. device.SetRenderTarget( 0, m_renderTarget );
  12. s = new Surface( lpPresInfo.lpSurf );
  13. texture = (Texture)s.GetContainer( IID_IDirect3DTexture9 );
  14. m_scene.DrawScene( device, texture );
  15. device.Present();
  16. texture.Dispose();
  17. s.Dispose();
  18. }
  19. }
 

The Scene class is standard Managed DirectX and we'll skip the details.

[Note (25/9/06), I have received the following message. When I'll update this document, I'll look at. Till then, it might be of some help.

Hello,

I'm sending you some informations related to playing .MPG and .WMV files using your useful C# implementation of custom VMR-9 allocator/presenter (tutorialDS7.zip). Playing .AVI files worked from start but when I tried to play .MPG or .WMV files, there were some problems.

After some poking around I found out a way to play these type of files as well. For example, addition to VidTexture.cs:

 

... fc = (IVMRFilterConfig9)vmr; fc.SetNumberOfStreams(1); fc.SetRenderingMode( 0x00000004 ); //VMRMode_Renderless = 0x00000004 ...

Consequence of this addition is that lpAllocInfo.Format as argument to IVMRSurfaceAllocator9.InitializeDevice() is always 0 and call to m_san.AllocateSurfaceHelper doesn't fail as in situation when fc.SetNumberOfStreams is not called.

If you think these informations are useful to other users of tutorialDS7.zip, you can post it on your notes page (http://www.a2ii..com/tech/directx/tutorialDS7.htm).

Thanks, Predrag ]

 




Bookmark it...
Digg!Reddit!Del.icio.us!Google!Facebook!Slashdot!Technorati!StumbleUpon!Newsvine!Furl!Yahoo!Ma.gnolia!
 
< Prev   Next >
Joomla Template by Joomlashack
Joomla Templates by JoomlaShack Joomla Templates by Compass Design