1

As with most UI lag problems, the root cause is normally too much is happening on the dispatcher thread. However, to the best of my understanding (source from a seperate question), processes run on seperate threads to the main window so this should not be the cause of the issue.

Please correct me if I am wrong on that point.


I originally started looking into how to embed a Unity application in a WPF application from this SO question

This allowed me to develop an application that had an embedded Unity window in.

The problem is that there is considerable UI lag. When I try to move a slider on my window, it takes a few seconds to catch up with the mouse.

What is the cause of this UI lag and how could it be resolved?

My MainWindow looks like the following.

enter image description here

Codebehind

using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.ComponentModel;
using System.Threading;

namespace CerbyEmbeddedUnity
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        [DllImport("User32.dll")]
        static extern bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw);

        internal delegate int WindowEnumProc(IntPtr hwnd, IntPtr lparam);
        [DllImport("user32.dll")]
        internal static extern bool EnumChildWindows(IntPtr hwnd, WindowEnumProc func, IntPtr lParam);

        [DllImport("user32.dll")]
        static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

        private Process unityProcess;
        private IntPtr unityHWND = IntPtr.Zero;

        private const int WM_ACTIVATE = 0x0006;
        private readonly IntPtr WA_ACTIVE = new IntPtr(1);
        private readonly IntPtr WA_INACTIVE = new IntPtr(0);

        private bool isFirstTimeActivating = true;
        private bool isFirstTimeDeactivating = true;
        private bool processActivatedSuccessfully = false;
        private const string PATH = @"..\..\..\..\Unity Side\Embedded Unity Demo\Build\Embedded Unity Demo.exe";

        public MainWindow()
        {
            InitializeComponent();
            SetUpUnityWindow();
        }

        private void SetUpUnityWindow()
        {
            bool exeExists = File.Exists(PATH);
            Console.WriteLine($"{Path.GetFullPath(PATH)} {(exeExists ? "does exist" : "doesn't exist")}"); //As PATH is relative, this helps make sure it is correct

            if(!exeExists)
            {
                //Unity .exe missing
                return;
            }

            IntPtr handle = UnityPanel.Handle;

            unityProcess = new Process();
            unityProcess.StartInfo.FileName = PATH;
            unityProcess.StartInfo.Arguments = "-parentHWND " + handle.ToInt32() + " " + Environment.CommandLine;
            unityProcess.StartInfo.UseShellExecute = true;
            unityProcess.StartInfo.CreateNoWindow = true;

            try
            {
                unityProcess.Start();
            }
            catch (InvalidOperationException)
            {
                //Something went wrong setting up the unity process
                return;
            }

            unityProcess.WaitForInputIdle();

            processActivatedSuccessfully = true;

            EnumChildWindows(handle, WindowEnum, IntPtr.Zero);

            unityHWNDLabel.Content = "Unity HWND: 0x" + unityHWND.ToString("X8");
        }

        private void ActivateUnityWindow()
        {
            SendMessage(unityHWND, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
        }

        private void DeactivateUnityWindow()
        {
            SendMessage(unityHWND, WM_ACTIVATE, WA_INACTIVE, IntPtr.Zero);
        }

        private int WindowEnum(IntPtr hwnd, IntPtr lparam)
        {
            unityHWND = hwnd;
            ActivateUnityWindow();
            return 0;
        }

        private void OnUnityPanelResize(object sender, EventArgs e)
        {
            MoveWindow(unityHWND, 0, 0, UnityPanel.Width, UnityPanel.Height, true);
            ActivateUnityWindow();
        }

        public void OnWindowClosing(object sender, CancelEventArgs e)
        {
            if(!processActivatedSuccessfully)
            {
                //Unity process didn't start. Nothing to close
                return;
            }

            try
            {
                unityProcess.CloseMainWindow();
            }
            catch (InvalidOperationException)
            {
                //Unity process already dead
                return;
            }

            Thread.Sleep(1000);
            while (!unityProcess.HasExited)
            {
                unityProcess.Kill();
            }
        }

        private void Activate(object sender, EventArgs e)
        {
            if(isFirstTimeActivating)
            {
                isFirstTimeActivating = false;
                ActivateUnityWindow();
            }
        }

        private void Deactivate(object sender, EventArgs e)
        {
            if(isFirstTimeDeactivating)
            {
                isFirstTimeDeactivating = false;
                DeactivateUnityWindow();
            }
        }
    }
}

XAML

<Window x:Class="CerbyEmbeddedUnity.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
        mc:Ignorable="d"
        Activated="Activate"
        Deactivated="Deactivate"
        Closing="OnWindowClosing"
        WindowState="Maximized"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="2*"/>
        </Grid.ColumnDefinitions>
        <WindowsFormsHost Grid.Column="1">
            <wf:Panel x:Name="UnityPanel" Resize="OnUnityPanelResize"/>
        </WindowsFormsHost>
        <StackPanel>
            <Label Name="unityHWNDLabel" HorizontalAlignment="Center"/>

            <!-- #region X -->
            <StackPanel Margin="4">
                <Label Content="X" FontSize="25" FontWeight="Bold" HorizontalAlignment="Center"/>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="auto"/>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="auto"/>
                    </Grid.ColumnDefinitions>

                    <Label Grid.Row="1"
                           Content="X Rotation Speed"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           VerticalContentAlignment="Center"/>
                    <Slider Grid.Row="1" Grid.Column="1"
                            Minimum="0" Maximum="100"
                            VerticalAlignment="Center"/>
                    <Label Grid.Row="1" Grid.Column="2"
                           HorizontalAlignment="Center"
                           Content="000" ContentStringFormat="D3"
                           VerticalAlignment="Center"
                           VerticalContentAlignment="Center"/>
                </Grid>
            </StackPanel>
            <!-- #endregion -->
        </StackPanel>
    </Grid>
</Window>

The Unity project I used here was very simple. It consisted of a camera and a cube in front of the camera with the script below on it.

Simple Unity Rotation Script

using UnityEngine;

public class CubeRotation : MonoBehaviour
{
    [SerializeField]
    [Range(0, 100)]
    private float rotationSpeedX = 1;

    [SerializeField]
    [Range(0, 360)]
    private float xAngle = 0;

    [SerializeField]
    [Range(0, 360)]
    private float yAngle = 0;

    [SerializeField]
    [Range(0, 360)]
    private float zAngle = 0;

    [SerializeField]
    private bool canXRotate = true;

    private void LateUpdate()
    {
        if(canXRotate)
        {
            xAngle += rotationSpeedX;

            if (xAngle > 360)
            {
                xAngle -= 360;
            }
        }

        transform.localEulerAngles = new Vector3(xAngle, yAngle, zAngle);
    }
}
Dan
  • 7,286
  • 6
  • 49
  • 114
  • The answer you linked clearly said that you should use TCP or Named Pipes if you need to communicate between Unity and WPF. I didn't see you using of them. – Programmer Sep 11 '18 at 11:51
  • @Programmer That is because I am not sending any information to or from Unity? – Dan Sep 11 '18 at 11:52
  • @Programmer The slider is not meant to do anything at the moment but slide, the issue is it is a very laggy slide – Dan Sep 11 '18 at 11:53
  • @Programmer Unless I misunderstood something about your answer on the other question and I am meant to use TCP somehow over directly embedding the window? – Dan Sep 11 '18 at 11:55
  • I don't understand your comment but you can embed Unity into WPF. When you need to communicate between the two, for example, use the slider from WPF to control a cube rotation in Unity, you should use *TCP* or *Named Pipes*. – Programmer Sep 11 '18 at 12:00
  • @Programmer Ah yes, that is the end goal, but at the moment the slider is not set up to anything. The problem is that when I try drag the slider, there is a lot of lag in it moving. I will post a gif in a moment to try and show what I mean better – Dan Sep 11 '18 at 12:02
  • Oh I see. If it is slow even when you haven't started to send commands to Unity then that's a different issue. What happens when you can call the `SetUpUnityWindow()` function in another `Thread` instead of calling it directly? This might actually fix the issue. Give it a try. – Programmer Sep 11 '18 at 12:16
  • @Programmer Apologies for the delayed response, I appear to be unable to do that. Once I put the process in a separate thread, I get a lot of cross thread issues. The main issue is it tries to access the UI in SetUpUnityWindow but as soon as I put those parts on the Dispatcher thread, I get another exception as the code in the dispatcher thread cannot be accessed from a thread other than the one it was created from – Dan Sep 11 '18 at 12:46
  • 1
    Can you send the Visual studio project to me? I don't need the Unity project. I will take a look when I have time – Programmer Sep 11 '18 at 15:04
  • @Programmer Of course. You can find it in a Bitbucket repository [here](https://bitbucket.org/DanJBower/temp-unity-wpf/src). I really appreciate you taking the time to help – Dan Sep 11 '18 at 15:25
  • This makes it harder to fix because it runs fine and smoothly on my side. Not anywhere slow as shown in your animated gif. What's your Visual Studio and OS version and architecture? Can you change the Visual Studio build to "Release" and also specially build to for 64 bit instead of "Any CPU" and see what happens? – Programmer Sep 11 '18 at 16:09
  • @Programmer Well... That is strange. I changed it to release and x64 but it made no difference. I am running Windows 10 64bit. Visual Studio Enterprise 2017 15.8.3 and Unity 2018.2.7f1 – Dan Sep 11 '18 at 16:31
  • @Programmer May I ask what you are building everything with? – Dan Sep 11 '18 at 16:36
  • Visual Studio Enterprise 2015 14.0.25431 Unity 2018.2.61f. This really shouldn't be a problem. I thought you were using old VS version. Have you tried upgrading your .NET version? – Programmer Sep 11 '18 at 16:43
  • @Programmer I just changed the .Net the Unity was running on to 4.x if that is what you meant. Unfortunately it made no difference. But in general everything is pretty up to date – Dan Sep 11 '18 at 16:56
  • Not really what I meant. I meant updating your .NET version. I don't know which version you have. I doubt that's the issue. As for the thread comment I made earlier, you should try it. You said you got some error. Replace your `SetUpUnityWindow` function with the function from [here](https://pastebin.com/ajeCpVGW). See if the Thread fixes the issue. Really nothing else I can do since I can't duplicate the issue. (Updated link so check again) – Programmer Sep 11 '18 at 17:29
  • @Programmer Ah I see. It is .NET Framework 4.7.2 so it is not out of date. Unfortunately your version of `SetUpUnityWindow` did not work. Thank you for all your help though. It is ashame the issue only appears to be on my system at the moment. – Dan Sep 11 '18 at 17:44
  • *"your version of SetUpUnityWindow did not work."* What do you mean by that? Error? The-same behavior? Anyways, it runs fine without error on my side – Programmer Sep 11 '18 at 17:47
  • @Programmer Sorry, I wrote that last comment in a bit of a rush. It did work but the same performance issue was still there – Dan Sep 11 '18 at 18:10
  • Ok. Is it slow if you don't load Unity in it? If it is then it has nothing to do with Unity. If it is slow then I suggest you file for a bug report with the project – Programmer Sep 11 '18 at 18:13
  • @Programmer Unfortunately it works perfectly fine when Unity is not loaded and it even works fine when unity is not embedded but loaded separately. I ran a performance profiler, with the Unity program embedded and running, and it doesn't show the CPU / GPU going over 10% within the application. So I am pretty stumped – Dan Sep 11 '18 at 18:28
  • I suggest filing for a bug report. Hopefully, they can replicate this issue on their side – Programmer Sep 11 '18 at 18:30
  • 1
    @Programmer I will do that. Thanks again for all your help – Dan Sep 11 '18 at 18:35

0 Answers0