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.
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);
}
}