Tuesday, September 20, 2011

A word on video overlays

So I was looking around for alternatives to forcing my window to the very top. I discovered this project: http://www.codeproject.com/KB/directx/Overlay_Tools.aspx which was written in 2007 for C# using DirectDraw. (Kudos to ]Metty[ )

Sweet, the overlay stays on top, and that should work just fine, but wait, what's this?

It doesn't quite compile out of the box.

1) The project needs to be compiled for x86, not any processor or x86-64.

2) It uses unsafe code which is icky.

3) Doesn't play nice with Aero and Windows 7.

#1 is an easy fix. You can change that by adjusting the project settings.

#2 is as simple as replacing the unsafe code with a call to Marshal.Copy in the methods BlitYUY2 and BlitRGB32.

#3 is simple as well. Make a call to DwmEnableComposition in dwmapi to disable Aero.

Here are my changes in full to Overlay.CS


using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using Microsoft.DirectX;
using Microsoft.DirectX.DirectDraw;
using PixelFormat = Microsoft.DirectX.DirectDraw.PixelFormat;

namespace OverlayLib
{

    static class NativeMethods
    {
        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string dllToLoad);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);


        [DllImport("kernel32.dll")]
        public static extern bool FreeLibrary(IntPtr hModule);

        
    }

    /// <summary>
    /// A DirectDraw Overlay
    /// (c) 2007 Metty
    /// </summary>
    public class Overlay : IDisposable
    {
        #region ePixelFormat enum

        /// <summary>
        /// The PixelFormat of the render surface
        /// </summary>
        public enum ePixelFormat
        {
            RGB32, //Defines a RGB32 (R,G,B, -) pixel format
            YUY2, //Defines a YUY2 (16 bit) pixel format
        }

        #endregion

        private delegate uint Win32DwmEnableComposition(uint uCompositionAction);

        Win32DwmEnableComposition _myDwmEnable;

        private bool m_AlphaEnabled = false;
        private Surface m_BackBuffer;
        private Surface m_Buffer; //Render Source
        private Device m_Device;
        private OverlayEffects m_Effects;
        private OverlayFlags m_Flags;
        private ePixelFormat m_PixelFormat = ePixelFormat.RGB32;
        private Point m_Position = Point.Empty;
        private Surface m_Primary; //Display
        private RenderDelegate m_Renderer;
        private Bitmap m_RenderTarget; //Rendertarget

        private Size m_Size = new Size(100, 100);
        private IntPtr wdmFuncPtr;

        /// <summary>
        /// The position of the overlay on the screen
        /// </summary>
        public Point Position
        {
            get { return m_Position; }
            set
            {
                if (m_Device != null)
                {
                    Hide();
                    m_Position = value;
                    Update();
                }
                else
                {
                    m_Position = value;
                }
            }
        }

        /// <summary>
        /// The Size of the overlay
        /// </summary>
        public Size Size
        {
            get { return m_Size; }
            set
            {
                m_Size = value;

                if (m_Device != null)
                {
                    Dispose();
                    Initialise();
                }
            }
        }

        /// <summary>
        /// Boundings of the overlay
        /// Combins Position and Size
        /// </summary>
        public Rectangle Boundings
        {
            get { return new Rectangle(Position, Size); }
        }

        /// <summary>
        /// Called when the overlay shall be redrawn
        /// </summary>
        public RenderDelegate Renderer
        {
            get { return m_Renderer; }
            set { m_Renderer = value; }
        }

        /// <summary>
        /// The pixelformat currently used
        /// Either let this be determined default, or set it to try using the specified value as a start value
        /// </summary>
        public ePixelFormat PixelFormat
        {
            get { return m_PixelFormat; }
            set { m_PixelFormat = value; }
        }

        /// <summary>
        /// Determines whether alpha is enabled or not
        /// Beware: Is not supported on many (all?) nvidia graphic cards
        /// </summary>
        public bool AlphaEnabled
        {
            get { return m_AlphaEnabled; }
            set
            {
                m_AlphaEnabled = value;

                if (m_Device != null)
                {
                    CreateFlags();
                }
            }
        }

        #region IDisposable Members

        /// <summary>
        /// Disposes the overlay and frees resources
        /// </summary>
        public void Dispose()
        {
            if (m_Primary != null)
            {
                m_Primary.Dispose();
                m_Primary = null;
            }

            if (m_Buffer != null)
            {
                m_Buffer.Dispose();
                m_Buffer = null;
            }

            if (m_BackBuffer != null)
            {
                m_BackBuffer.Dispose();
                m_BackBuffer = null;
            }

            if (m_RenderTarget != null)
            {
                m_RenderTarget.Dispose();
                m_RenderTarget = null;
            }

            if (m_Device != null)
            {
                m_Device.Dispose();
                m_Device = null;
            }

            if (System.Environment.OSVersion.Version.Major >= 6)
            {
              
                _myDwmEnable(1);

                NativeMethods.FreeLibrary(wdmFuncPtr);
            }
        }

        #endregion

        /// <summary>
        /// Gets the directX pixelformat for the specified pixelformat
        /// </summary>
        /// <param name="e">ePixelFormat</param>
        /// <returns>DirectX PixelFormat</returns>
        public static PixelFormat GetPixelFormat(ePixelFormat e)
        {
            PixelFormat pixelFormat = new PixelFormat();

            switch (e)
            {
                case ePixelFormat.RGB32:
                    pixelFormat.Rgb = true;
                    pixelFormat.RgbBitCount = 32;
                    pixelFormat.RBitMask = 0xFF0000;
                    pixelFormat.GBitMask = 0x00FF00;
                    pixelFormat.BBitMask = 0x0000FF;
                    break;
                case ePixelFormat.YUY2:
                    pixelFormat.FourCC = 0x32595559;
                    pixelFormat.FourCcIsValid = true;
                    break;
            }

            return pixelFormat;
        }

        /// <summary>
        /// Initialises the overlay with the specified Boundings
        /// </summary>
        public void Initialise()
        {
            m_Device = new Device();
            m_Device.SetCooperativeLevel(null, CooperativeLevelFlags.Normal); //Only a overlay..

            //Create Primary
            {
                SurfaceDescription desc = new SurfaceDescription();
                desc.SurfaceCaps.PrimarySurface = true;
                m_Primary = new Surface(desc, m_Device);
            }

            //Create buffer
            {
                SurfaceDescription desc = new SurfaceDescription();
                desc.Width = Boundings.Width;
                desc.Height = Boundings.Height;
                desc.BackBufferCount = 1;
                desc.SurfaceCaps.Flip = true;
                desc.SurfaceCaps.Overlay = true;
                desc.SurfaceCaps.Complex = true;
                desc.SurfaceCaps.VideoMemory = true;

                //Try which pixelformat works
                do
                {
                    try
                    {
                        desc.PixelFormatStructure = GetPixelFormat(m_PixelFormat);
                        m_Buffer = new Surface(desc, m_Device);
                    }
                    catch (DirectXException)
                    {
                        m_PixelFormat++;
                    }
                } while (m_Buffer == null && Enum.IsDefined(typeof(ePixelFormat), m_PixelFormat)); //Stop if a valid format is found or no one is left

                if (m_Buffer == null)
                {
                    //Bad!
                    throw new GraphicsException("Could not create overlay - Either your graphic card does not support any of the available pixelformats or overlays in general.");
                }
            }

            //Create backbuffer
            {
                SurfaceCaps caps = new SurfaceCaps();
                caps.BackBuffer = true;
                m_BackBuffer = m_Buffer.GetAttachedSurface(caps);
            }

            //Create rendertarget
            {
                //We use a Bitmap, as it works on every graphicscard, a RGB-Surface wont
                m_RenderTarget = new Bitmap(Boundings.Width, Boundings.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
            }

            if (System.Environment.OSVersion.Version.Major >= 6)
            {
                wdmFuncPtr = NativeMethods.LoadLibrary(@"dwmapi.DLL");



                IntPtr pAddressOfFunctionToCall = NativeMethods.GetProcAddress(wdmFuncPtr, "DwmEnableComposition");

                _myDwmEnable =(Win32DwmEnableComposition)Marshal.GetDelegateForFunctionPointer(pAddressOfFunctionToCall, typeof(Win32DwmEnableComposition));

                _myDwmEnable(0);
            }

            CreateFlags();
        }

        /// <summary>
        /// Creates the render flags
        /// </summary>
        public void CreateFlags()
        {
            //Create Flags
            {
                m_Flags = OverlayFlags.Show;

                if (AlphaEnabled)
                {
                    m_Flags |= OverlayFlags.Effects | OverlayFlags.KeySourceOverride;
                }
            }

            //Create Effects
            {
                m_Effects = new OverlayEffects();

                //Transparency
                if (AlphaEnabled)
                {
                    ColorKey key = new ColorKey();
                    key.ColorSpaceHighValue = key.ColorSpaceLowValue = 0; //Make Black (0,0,0) transparent
                    m_Effects.DestinationColorKey = key;
                }
            }
        }

        /// <summary>
        /// Renders (Updates) the overlay
        /// * Calls the RenderDelegate
        /// </summary>
        public void Update()
        {
            if (m_Device == null)
            {
                return;
            }

            try
            {
                if (m_Renderer != null)
                {
                    //Draw on the backbuffer
                    Graphics g = Graphics.FromImage(m_RenderTarget);
                    g.Clear(Color.Black);
                    m_Renderer(g);
                    Blit(m_RenderTarget, m_BackBuffer);
                }

                //Flip maps
                m_Buffer.Flip(null, FlipFlags.Wait);

                //Render



                m_Buffer.UpdateOverlay(m_Primary, Boundings, m_Flags, m_Effects);
            }
            catch (SurfaceLostException)
            {
                //May occur, but not bad - TestCooperativeLevel wont work in some D3D apps, but overlay will still render
                if (m_Device.TestCooperativeLevel())
                {
                    m_Device.RestoreAllSurfaces();
                }
            }
        }

        /// <summary>
        /// Hides the overlay from screen
        /// Can be undone by calling Update()
        /// </summary>
        public void Hide()
        {
            if (m_Device == null)
            {
                return;
            }

            m_Buffer.UpdateOverlay(m_Primary, Boundings, OverlayFlags.Hide);
        }

        /// <summary>
        /// Blits the RGB Bitmap to the specified surface
        /// </summary>
        /// <param name="src">RGB Bitmap</param>
        /// <param name="dest">Surface</param>
        public void Blit(Bitmap src, Surface dest)
        {
            switch (m_PixelFormat)
            {
                case ePixelFormat.RGB32:
                    BlitRGB32(src, dest);
                    break;
                case ePixelFormat.YUY2:
                    BlitYUY2(src, dest);
                    break;
            }
        }

        /// <summary>
        /// Blits the RGB Bitmap to a YUY2 surface
        /// </summary>
        /// <param name="src">RGB Bitmap</param>
        /// <param name="dest">YUY2 Surface</param>
        public void BlitYUY2(Bitmap src, Surface dest)
        {
            BitmapData ds =
                src.LockBits(new Rectangle(0, 0, src.Width, src.Height), ImageLockMode.ReadOnly,
                             System.Drawing.Imaging.PixelFormat.Format24bppRgb);
            try
            {
                LockedData dd = dest.Lock(LockFlags.WriteOnly);

                try
                {
                    int ps = ds.Stride - (ds.Width * 3);
                    byte[] pd = new byte[dd.Pitch - (dd.Width * 2)];



                    byte[] ptr = new byte[Math.Abs(ds.Stride) * ds.Height];
                    int ptrIndex = 0;
                    Marshal.Copy(ds.Scan0, ptr, 0, Math.Abs(ds.Stride) * ds.Height);

                    for (int h = 0; h < ds.Height; h++)
                    {
                        for (int w = 0; w < ds.Width; w += 2)
                        {
                            byte[] dbuf = new byte[4]; //2 pixel (2x16bit)

                            byte r1 = ptr[ptrIndex + 0];
                            byte g1 = ptr[ptrIndex + 1];
                            byte b1 = ptr[ptrIndex + 2];

                            ptrIndex += 3;

                            byte r2 = ptr[ptrIndex + 0];
                            byte g2 = ptr[ptrIndex + 1];
                            byte b2 = ptr[ptrIndex + 2];

                            ptrIndex += 3;

                            //Dont ask me for the conversion formulas - They are from FourCC and a bit of own modifications to match colors better
                            dbuf[0] = (byte)Math.Min(255, (0.230 * r1) + (0.600 * g1) + (0.170 * b1)); //Yo - luminescent 1
                            dbuf[2] = (byte)Math.Min(255, (0.230 * r2) + (0.600 * g2) + (0.170 * b2)); //Y1 - luminescent 2
                            dbuf[1] = (byte)Math.Min(255, +(0.439 * r1) - (0.368 * g1) - (0.071 * b1) + 128); //Ux - same for both
                            dbuf[3] = (byte)Math.Min(255, -(0.148 * r1) - (0.291 * g1) + (0.439 * b1) + 128); //Vx - same for both

                            dd.Data.Write(dbuf, 0, dbuf.Length);
                        }
                        ptrIndex += ps;

                        if (pd.Length > 0)
                        {
                            dd.Data.Write(pd, 0, pd.Length);
                        }
                    }
                }
                catch (Exception e)
                {
                    MessageBox.Show(e.ToString());
                }
                finally
                {
                    dest.Unlock();
                }
            }
            catch (Exception e)
            {
                MessageBox.Show(e.ToString());
            }
            finally
            {
                src.UnlockBits(ds);
            }
        }

        /// <summary>
        /// Blits the RGB-Bitmap to a RGB32 Surface
        /// </summary>
        /// <param name="src">RGB Bitmap</param>
        /// <param name="dest">RGB32 Surface</param>
        public void BlitRGB32(Bitmap src, Surface dest)
        {
            BitmapData ds =
                src.LockBits(new Rectangle(0, 0, src.Width, src.Height), ImageLockMode.ReadOnly,
                             System.Drawing.Imaging.PixelFormat.Format24bppRgb);
            try
            {
                LockedData dd = dest.Lock(LockFlags.WriteOnly);

                try
                {
                    int ps = ds.Stride - (ds.Width * 3);
                    byte[] pd = new byte[dd.Pitch - (dd.Width * 4)];

                    byte[] ptr = new byte[Math.Abs(ds.Stride) * ds.Height];
                    int ptrIndex = 0;
                    Marshal.Copy(ds.Scan0, ptr, 0, Math.Abs(ds.Stride) * ds.Height);

                    for (int h = 0; h < ds.Height; h++)
                    {
                        for (int w = 0; w < ds.Width; w += 1)
                        {
                            byte[] dbuf = new byte[4]; //2 pixel (2x16)

                            byte r1 = ptr[ptrIndex + 0];
                            byte g1 = ptr[ptrIndex + 1];
                            byte b1 = ptr[ptrIndex + 2];

                            ptrIndex += 3;



                            dbuf[0] = r1;
                            dbuf[1] = g1;
                            dbuf[2] = b1;

                            dd.Data.Write(dbuf, 0, dbuf.Length);
                        }

                        ptrIndex += ps;

                        if (pd.Length > 0)
                        {
                            dd.Data.Write(pd, 0, pd.Length);
                        }
                    }
                }
                finally
                {
                    dest.Unlock();
                }
            }
            finally
            {
                src.UnlockBits(ds);
            }
        }
    }

    /// <summary>
    /// Delegate for rendering
    /// </summary>
    /// <param name="g">Graphics Object</param>
    public delegate void RenderDelegate(Graphics g);
}
I hope this was useful.

Other changes may be to upgrade to directx 9 overlays (directdraw seems to work OK though..), or dwmapi http://msdn.microsoft.com/en-us/library/aa969540(v=VS.85).aspx

Thanks!

1 comment:

  1. I really am trying to set up my system so I can view my Twitch.tv chat (which is IRC-based) in-game... Just a few lines, since I don't have room for a second monitor or anything on my desk. This has been a terrible trial, one I thought would be easy to accomplish in the age of programs like Xfire, Steam, Xsplit and so on.

    Everything on this page is greek to me, but really what I described above is all I'm looking for. Can you possibly help me? :(

    ReplyDelete

Note: Only a member of this blog may post a comment.