I am using WPF as UI framework to develop a game. The game engine doesn't mutate it self, it only got changed when a call from the UI application telling it to update to next frame.
This is the key part of my codes:
public class GameWorldViewModel : ViewModel {
public GameWorldViewModel(GameWorld gameWorld) {
this.gameWorld = gameWorld;
GameBodyCollectionViewModel = new GameBodyCollectionViewModel(gameWorld);
CopyData();
Proceed();
}
public GameBodyCollectionViewModel GameBodyCollectionViewModel { get; init; }
private readonly GameWorld gameWorld;
private readonly Stopwatch stopwatch = new Stopwatch();
private bool isProceed = false;
public void Pause() {
if (!isProceed) throw new InvalidOperationException("The GameWorld is already paused.");
isProceed = false;
stopwatch.Reset();
}
public void Proceed() {
if (isProceed) throw new InvalidOperationException("The GameWorld is already proceeding.");
isProceed = true;
Action action = () => DispatherLoopCallback(Task.CompletedTask);
stopwatch.Start();
Application.Current.Dispatcher.BeginInvoke(action, DispatcherPriority.Background);
}
private void DispatherLoopCallback(Task task) {
if (!isProceed) return;
if (task.IsCompleted) {//Check if the backgroud update has completed
CopyData();//Copy the data but not update UI
double deltaTime = stopwatch.Elapsed.TotalSeconds;
stopwatch.Restart();
task = gameWorld.BeginUpdate(deltaTime);//Let backgroud game engine calculate next frame of the game.
NotifyChange();//Update UI, runing concurrently with backgroud game engine thread. After this line is removed, the memory leak doesn't occur any more.
}
Task task_forLambda = task;
Action action = () => DispatherLoopCallback(task_forLambda);
Application.Current.Dispatcher.BeginInvoke(action, DispatcherPriority.Background);//Send next call of this method to the dispatcher, leave space for other WPF process.
}
private void CopyData() {
GameBodyCollectionViewModel.CopyData();
}
private void NotifyChange() {
GameBodyCollectionViewModel.NotifyChange();
}
}
However, when the game is running, even there are nothing, the memory usage keep increasing. And this increasing stop when the game pause. So I am sure there is a memory leak. After investigation, I found the problem come from NotifyChange(). But I cannot figure out how are these ViewModel classes causing problem.
public class GameBodyCollectionViewModel : CollectionViewModel<GameBodyViewModel> {
public GameBodyCollectionViewModel(GameWorld gameWorld) {
this.gameWorld = gameWorld;
}
private readonly GameWorld gameWorld;
public override IEnumerator<GameBodyViewModel> GetEnumerator() => copiedData.GetEnumerator();
internal void CopyData() {
copiedData.Clear();
copiedData.AddRange(from gb in gameWorld.GetGameBodies() select new GameBodyViewModel(gb));
}
private readonly List<GameBodyViewModel> copiedData = new List<GameBodyViewModel>();
internal void NotifyChange() {
NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));//After this line is removed, the memory leak doesn't happen any more.
}
}
public class GameBodyViewModel : ViewModel {
public GameBodyViewModel(GameBody gameBody) {
AABBLowerX = gameBody.AABB.LowerBound.X;
AABBLowerY = gameBody.AABB.LowerBound.Y;
AABBWidth = gameBody.AABB.Width;
AABBHeight = gameBody.AABB.Height;
}
public double AABBLowerX { get; }
public double AABBLowerY { get; }
public double AABBWidth { get; }
public double AABBHeight { get; }
}
public abstract class ViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([CallerMemberName] String propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public abstract class CollectionViewModel<T> : ViewModel, INotifyCollectionChanged, IEnumerable<T> {
public abstract IEnumerator<T> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public event NotifyCollectionChangedEventHandler CollectionChanged;
protected void NotifyCollectionChanged(NotifyCollectionChangedEventArgs e) {
CollectionChanged?.Invoke(this, e);
}
}
The xaml codes of views are unlikely to be relevant to the context so I didn't write them here. If any more detail is required please tell me.
Update0
It was a fault to neglect the xaml codes. I found its actually relevant.
<v:View x:TypeArguments="local:GameBodyCollectionViewModel" x:Name="view"
x:Class="Enigma.GameWPF.Visual.Game.GameBodyCollectionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:v ="clr-namespace:Enigma.GameWPF.Visual"
xmlns:local="clr-namespace:Enigma.GameWPF.Visual.Game"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<ItemsControl ItemsSource="{Binding ViewModel,ElementName=view}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas></Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding AABBLowerX}"/>
<Setter Property="Canvas.Bottom" Value="{Binding AABBLowerY}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:GameBodyView ViewModel="{Binding}" Width="{Binding AABBWidth}" Height="{Binding AABBHeight}"></local:GameBodyView>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</v:View>
After removing the whole ItemsControl, the memory leak was not happenning.
Update1
Based on what I observed, I made a demo project inorder for audiences to have better information and able to reproduce the bug. As StackOverFlow dosen't support file attach and the same thing is posted on github as well, I am posting the link of that post here. You may goto that link to download the demo. https://github.com/dotnet/wpf/issues/5739
Update2
Based on whats done already, I tried to used .NET provided ObservableCollection instead of my own implementation of INotifyCollectionChanged. DemoCollectionViewModel was changed to :
public class DemoCollectionViewModel : ViewModel {
public DemoCollectionViewModel() {
DemoItemViewModels = new ObservableCollection<DemoItemViewModel>();
}
public ObservableCollection<DemoItemViewModel> DemoItemViewModels { get; }
private readonly List<DemoItemViewModel> copiedData = new List<DemoItemViewModel>();
internal void CopyData() {
copiedData.Clear();
copiedData.AddRange(from SimulatedModelItem smi in SimulatedModel.GetItems select new DemoItemViewModel(smi));
}
internal void NotifyChange() {
DemoItemViewModels.Clear();
foreach (DemoItemViewModel vm in copiedData) {
DemoItemViewModels.Add(vm);
}
}
}
And in the view, the ItemsControl's ItemsSource is binded to the ObservableCollection instead. However the problem persists