Search code examples
c#directx-9slimdx

how to lower cpu impact when dealing with slimdx


i am making an app that overlays other d3d games,
the app is working perfectly except it has a huge performance impact on the cpu
enter image description here
taking 21.4 % of the cpu when rendering only a single line ! i am using slimdx library on c# and here is my full code

OverLay.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using SlimDX.Direct3D11;
using SlimDX.Windows;
using System.Runtime.InteropServices;
using System.Security;
using SlimDX;
using SlimDX.DXGI;
using Device = SlimDX.Direct3D9.Device;
using Resource = SlimDX.Direct3D9.Resource;
using System.Threading;
using D3D = SlimDX.Direct3D9;

namespace OverlayForm
{
    public partial class OverLay : RenderForm
    {
        RenderForm form;

        Device device;
        // D3D.Sprite sprite;

        public OverLay()
        {
            InitializeComponent();

            Paint += OverLay_Paint;
            FormBorderStyle = FormBorderStyle.None;
            ShowIcon = false;
            ShowInTaskbar = false;
            TopMost = true;
            WindowState = FormWindowState.Maximized;



            //Make the window's border completely transparant
            //SetWindowLong(Handle , GWL_EXSTYLE , (IntPtr)(GetWindowLong(Handle , GWL_EXSTYLE) ^ WS_EX_LAYERED ^ WS_EX_TRANSPARENT));
            SetWindowLong(Handle , GWL_EXSTYLE , (IntPtr)(GetWindowLong(Handle , GWL_EXSTYLE) | WS_EX_LAYERED | WS_EX_TRANSPARENT));

            //Set the Alpha on the Whole Window to 255 (solid)
            SetLayeredWindowAttributes(Handle , 0 , 255 , LWA_ALPHA);



            form = this;
            form.FormClosing += Form_FormClosing;
            //Init DirectX
            //This initializes the DirectX device. It needs to be done once.
            //The alpha channel in the backbuffer is critical.
            D3D.PresentParameters presentParameters = new D3D.PresentParameters();
            presentParameters.Windowed = true;
            presentParameters.SwapEffect = D3D.SwapEffect.Discard;
            presentParameters.BackBufferFormat = D3D.Format.A8R8G8B8;


            device = new Device(new D3D.Direct3D() , 0 , D3D.DeviceType.Hardware , Handle ,
            D3D.CreateFlags.HardwareVertexProcessing , presentParameters);

            //sprite = new D3D.Sprite(device);

            font = new D3D.Font(device , new Font("Arial" , 9 , FontStyle.Regular));
            line = new D3D.Line(this.device);

            MessagePump.Run(form , new MainLoop(dxThread));
        }

        private void Form_FormClosing(object sender , FormClosingEventArgs e)
        {
            device.Dispose();
        }

        int centerx = Screen.PrimaryScreen.WorkingArea.Width / 2;
        int centery = Screen.PrimaryScreen.WorkingArea.Height / 2;
        private void OverLay_Paint(object sender , PaintEventArgs e)
        {
            //Create a margin (the whole form)
            marg.Left = 0;
            marg.Top = 0;
            marg.Right = Width;
            marg.Bottom = Height;

            //Expand the Aero Glass Effect Border to the WHOLE form.
            // since we have already had the border invisible we now
            // have a completely invisible window - apart from the DirectX
            // renders NOT in black.
            DwmExtendFrameIntoClientArea(Handle , ref marg);
        }
        private static D3D.Font font;
        private static D3D.Line line;
        private void dxThread()
        {
            form.TopMost = true;
            device.SetRenderState(D3D.RenderState.ZEnable , false);
            device.SetRenderState(D3D.RenderState.Lighting , false);
            device.SetRenderState(D3D.RenderState.CullMode , D3D.Cull.None);
            device.SetTransform(D3D.TransformState.Projection , Matrix.OrthoOffCenterLH(0 , Width , Height , 0 , 0 , 1));
            device.BeginScene();



            //DrawFilledBox(0 , 0 , 100 , 100 , Color.White);
            //font.DrawString( null, "Swag" , 10, 10 , new Color4(Color.White));            
            //DrawBox(0 , 0 , 10 , 10 , 1 , Color.Green);
            DrawLine(0 , 0 , Screen.PrimaryScreen.Bounds.Width , Screen.PrimaryScreen.Bounds.Height , 2 , Color.Pink);


            device.EndScene();
            device.Present();
        }

        public static void DrawFilledBox(float x , float y , float w , float h , Color Color)
        {
            Vector2[] vLine = new Vector2[2];

            line.GLLines = true;
            line.Antialias = false;
            line.Width = w;

            vLine[0].X = x + w / 2;
            vLine[0].Y = y;
            vLine[1].X = x + w / 2;
            vLine[1].Y = y + h;

            line.Begin();
            line.Draw(vLine , new Color4(Color));
            line.End();
        }
        public static void DrawLine(float x1 , float y1 , float x2 , float y2 , float w , Color Color)
        {
            Vector2[] vLine = new Vector2[2] { new Vector2(x1 , y1) , new Vector2(x2 , y2) };

            line.GLLines = true;
            line.Antialias = false;
            line.Width = w;

            line.Begin();
            line.Draw(vLine , new Color4(Color));
            line.End();

        }
        public static void DrawBox(float x , float y , float w , float h , float px , System.Drawing.Color Color)
        {
            DrawFilledBox(x , y + h , w , px , Color);
            DrawFilledBox(x - px , y , px , h , Color);
            DrawFilledBox(x , y - px , w , px , Color);
            DrawFilledBox(x + w , y , px , h , Color);
        }

        #region Extras
        private Margins marg;
        //this is used to specify the boundaries of the transparent area
        internal struct Margins
        {
            public int Left, Right, Top, Bottom;
        }

        [DllImport("user32.dll" , SetLastError = true)]

        private static extern UInt32 GetWindowLong(IntPtr hWnd , int nIndex);

        [DllImport("user32.dll")]

        static extern int SetWindowLong(IntPtr hWnd , int nIndex , IntPtr dwNewLong);

        [DllImport("user32.dll")]

        static extern bool SetLayeredWindowAttributes(IntPtr hwnd , uint crKey , byte bAlpha , uint dwFlags);

        public const int GWL_EXSTYLE = -20;

        public const int WS_EX_LAYERED = 0x80000;

        public const int WS_EX_TRANSPARENT = 0x20;

        public const int LWA_ALPHA = 0x2;

        public const int LWA_COLORKEY = 0x1;

        [DllImport("dwmapi.dll")]
        static extern void DwmExtendFrameIntoClientArea(IntPtr hWnd , ref Margins pMargins);

        #endregion



    }
}

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace OverlayForm
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>        
        static void Main()
        {
            using (OverLay x = new OverLay())
            {

            }
        }
    }
}

Please note :
i already saw this : Very high CPU usage directx 9

but i am using MessagePump.Run and don't know how to apply the answer.


Solution

  • The reason for the high CPU is that SlimDX is using PeekMessage rather than the more usual GetMessage (that the majority of Windows apps use). The former does not wait for a message to appear in the message pump unlike the latter. In other words, GetMessage() will block the current thread possibly reducing the CPU load which is what you want a well-behaved Windows desktop application to do.

    MSDN:

    Retrieves a message from the calling thread's message queue. The function dispatches incoming sent messages until a posted message is available for retrieval. Unlike GetMessage, the PeekMessage function does not wait for a message to be posted before returning More...

    Now a typical graceful Windows message pump looks like this:

    while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
    { 
        if (bRet == -1)
        {
            // handle the error and possibly exit
        }
        else
        {
            TranslateMessage(&msg); 
            DispatchMessage(&msg); 
        }
    } 
    

    ...however SlimDX uses what some people refer to as an action game loop:

    static bool AppStillIdle
    {
        get
        {
            Message msg;
            return !PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
        }
    }
    
    public void MainLoop()
    {
        // hook the application's idle event
        Application.Idle += new EventHandler(OnApplicationIdle);
        Application.Run(form);
    }
    
    void OnApplicationIdle(object sender, EventArgs e)
    {
        while (AppStillIdle)
        {
            // Render a frame during idle time (no messages are waiting)
            RenderFrame();
        }
    }
    

    With nothing to draw you will experience a very tight loop of PeekMessage with no waiting in between!

    My suggestion is that you either use one of the MessagePump.Run overloads for idle, or add a sleep as per below:

    Change this:

    void OnApplicationIdle(object sender, EventArgs e)
    {
        while (AppStillIdle)
        {
            // Render a frame during idle time (no messages are waiting)
            RenderFrame();
        }
    }
    

    ...to this:

    void OnApplicationIdle(object sender, EventArgs e)
    {
        while (AppStillIdle)
        {
            // Render a frame during idle time (no messages are waiting)
            RenderFrame();
    
            Thread.Sleep(0); // <------------- be graceful
        }
    }
    

    Note the use of Thread.Sleep(0). This pauses for the minimal amount of time whilst still allowing the thread to be relinquished to the OS.

    MSDN:

    The number of milliseconds for which the thread is suspended. If the value of the millisecondsTimeout argument is zero, the thread relinquishes the remainder of its time slice to any thread of equal priority that is ready to run. If there are no other threads of equal priority that are ready to run, execution of the current thread is not suspended.

    I see in your answer you already had a Thread.Sleep(50) but now it is good to know why SlimDX requires a Sleep in the first place and that 50 is perhaps too high a value.

    GetMessage

    OP:

    i won't need a very active rendering mechanism, because i will only use this to show overlay from my music player about current song playing , next song , etc

    Considering this is the case, the most CPU-efficient means is to replace your action loop with a turn-based game loop using GetMessage() instead of PeekMessage(). Then put your rendering in your application's OnIdle() callback.

    As Nick Dandoulakis says in Windows Game Loop 50% CPU on Dual Core:

    Nick:

    That's a standard game loop for action games, where you must update objects positions / game world. If you are making a board game GetMessage would be a better choice. It really depends on what game you are making. More...

    No Sleep() required.