Hi there,
I have to draw some graphs in my WPF application. With MonoGame I finally found a solution that is performant enough to achieve this. To use MonoGame with WPF I use MonoGame.WpfCore.
But when I started testing, I found out, that it is flickering on some computers/screens. I have not yet been able to find out exactly when this is the case (external GPU, monitor, connection to the monitor).
It should look like this:
But when flickering it looks something like this:
It looks like the drawing just stops at some point. I checked the Draw() method in my view model, but it is always executed to the end. It sounds like it is a similar problem to the one here: https://github.com/craftworkgames/MonoGame.WpfCore/issues/2.
My question now is if I am making a mistake, or maybe there is a bug here. I tested two ways of drawing. But both show the same flickering.
GraphicsDevice.DrawUserPrimitives()
One way to draw my graphs, i found, was to use GraphicsDevice.DrawUserPrimitives():
public class MainWindowViewModel : MonoGameViewModel
{
public override void LoadContent()
{
Random rand = new Random();
for (int g = 0; g < _graphsCount; g++)
_colors[g] = new Color((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble());
for (int i = 0; i < _values.Length; i++)
_values[i] = rand.Next(0, _valueRange);
_graphVertices = new VertexPositionColor[_graphsCount][];
CreateGraphVertices();
_cursorVertices = new VertexPositionColor[2];
_basicEffect = new BasicEffect(GraphicsDevice) { VertexColorEnabled = true };
_basicEffect.World = Matrix.CreateOrthographicOffCenter(
0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 0, 1);
}
private const int _graphsCount = 50;
private Color[] _colors = new Color[_graphsCount];
private const int _valueRange = 100;
private int[] _values = new int[2000];
private VertexPositionColor[] _cursorVertices;
private VertexPositionColor[][] _graphVertices;
private BasicEffect _basicEffect;
public override void SizeChanged(object sender, SizeChangedEventArgs args)
{
_width = (int)args.NewSize.Width;
_height = (int)args.NewSize.Height;
if (_basicEffect != null)
_basicEffect.World = Matrix.CreateOrthographicOffCenter(
0, (int)_width, (int)_height, 0, 0, 1);
if (_graphVertices != null)
CreateGraphVertices();
}
private double _width = 100;
private double _height = 100;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CreateGraphVertices()
{
for (int g = 0; g < _graphsCount; g++)
{
_graphVertices[g] = new VertexPositionColor[_values.Length * 2 - 1];
for (int i = 0; i < _graphVertices[g].Length; i += 2)
{
int valueIndex = i / 2;
int x1 = GetX(valueIndex);
int y1 = GetY(valueIndex, g);
_graphVertices[g][i] = new VertexPositionColor(new Vector3(x1, y1, 0), _colors[g]);
if (i + 1 < _graphVertices[g].Length)
{
int x2 = GetX(valueIndex + 1);
_graphVertices[g][i + 1] = new VertexPositionColor(new Vector3(x2, y1, 0), _colors[g]);
}
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Update(GameTime gameTime)
{
// Update cursor.
_cursorPos++;
if (_cursorPos > _width)
_cursorPos = 1;
_cursorVertices = new[]
{
new VertexPositionColor(new Vector3(_cursorPos, 0, 0), Color.White),
new VertexPositionColor(new Vector3(_cursorPos, GraphicsDevice.Viewport.Height, 0), Color.White)
};
}
private int _cursorPos = 1;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
EffectTechnique effectTechnique = _basicEffect.Techniques[0];
EffectPassCollection effectPassCollection = effectTechnique.Passes;
foreach (EffectPass pass in effectPassCollection)
{
pass.Apply();
for (int g = 0; g < _graphsCount; ++g)
GraphicsDevice.DrawUserPrimitives(PrimitiveType.LineStrip, _graphVertices[g], 0, _graphVertices[g].Length - 1);
GraphicsDevice.DrawUserPrimitives(PrimitiveType.LineStrip, _cursorVertices, 0, 1);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetX(int valueIndex)
=> (int)(_width / _values.Length * valueIndex);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetY(int valueIndex, int graphIndex)
{
return (int)(_height / _graphsCount * graphIndex
+ _height / _graphsCount / _valueRange * _values[valueIndex]);
}
}
SpriteBatch.Draw()
I also tried to draw my graphs with a sprite batch:
public class MainWindowViewModel : MonoGameViewModel
{
private SpriteBatch _spriteBatch;
public override void LoadContent()
{
Random rand = new Random();
for (int g = 0; g < _graphsCount; ++g)
_colors[g] = new Color((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble());
for (int i = 0; i < _values.Length; i++)
_values[i] = rand.Next(0, _valueRange);
_spriteBatch = new SpriteBatch(GraphicsDevice);
}
private const int _graphsCount = 50;
private Color[] _colors = new Color[_graphsCount];
private const int _valueRange = 100;
private int[] _values = new int[1000];
public override void SizeChanged(object sender, SizeChangedEventArgs args)
{
_width = (int)args.NewSize.Width;
_height = (int)args.NewSize.Height;
}
private double _width = 100;
private double _height = 100;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Update(GameTime gameTime)
{
// Update cursor.
_cursorPos++;
if (_cursorPos > _width)
_cursorPos = 1;
}
private int _cursorPos = 1;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
_spriteBatch.Begin();
// Draw graphs.
for (int g = 0; g < _graphsCount; ++g)
{
for (int i = 0; i < _values.Length - 1; i++)
{
int x1 = GetX(i);
int y1 = GetY(i, g);
// Draw lines to next value.
if (i + 1 < _values.Length)
{
int x2 = GetX(i + 1);
int y2 = GetY(i + 1, g);
_spriteBatch.DrawHorizontalLine(x1, y1, x2 - x1 + 1, _colors[g], 1);
_spriteBatch.DrawVerticalLine(x2, y2, y1 - y2, _colors[g], 1);
}
}
}
// Draw cursor.
_spriteBatch.DrawVerticalLine(_cursorPos, 0, (int)_height, Color.White, 1);
_spriteBatch.End();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetX(int valueIndex)
=> (int)(_width / _values.Length * valueIndex);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetY(int valueIndex, int graphIndex)
{
return (int)(_height / _graphsCount * graphIndex
+ _height / _graphsCount / _valueRange * _values[valueIndex]);
}
}
public static class Lines
{
private static Texture2D _pixelTexture;
private static void CreateTexture(SpriteBatch spriteBatch)
{
_pixelTexture = new Texture2D(spriteBatch.GraphicsDevice, 1, 1, false, SurfaceFormat.Color);
_pixelTexture.SetData(new[] { Color.White });
}
public static void DrawHorizontalLine(this SpriteBatch spriteBatch, int x, int y, int length, Color color, int thickness)
{
if (_pixelTexture == null)
CreateTexture(spriteBatch);
spriteBatch.Draw(_pixelTexture, new Vector2(x, y), null, color, 0, Vector2.Zero, new Vector2(length, thickness), SpriteEffects.None, 0);
}
public static void DrawVerticalLine(this SpriteBatch spriteBatch, int x, int y, int height, Color color, float thickness)
{
if (_pixelTexture == null)
CreateTexture(spriteBatch);
if (height > 0)
{
spriteBatch.Draw(_pixelTexture, new Vector2(x, y), null, color, 0, Vector2.Zero, new Vector2(thickness, height), SpriteEffects.None, 0);
}
else
{
spriteBatch.Draw(_pixelTexture, new Vector2(x, y + height), null, color, 0, Vector2.Zero, new Vector2(thickness, height * (-1)), SpriteEffects.None, 0);
}
}
}
Now I hope someone can help me. Am I doing something wrong? Or is there a better way to draw graphs? Or is this a bug after all? I am grateful for any advice.
Perhaps some hints to reproduce:
- Most of the cases when the flickering appeared, I used laptops with intel hd graphics. One laptop with intel hd graphics plus a dedicated GPU showed no flickering, as long as the laptop screen was used. But as soon as it was connected to a screen via USB type C it started flickering.
- All tested devices had Windows 10 installed.
- Tested with MonoGame.Framework.WindowsDX 3.7.1.189 and 3.8.1.303 as well.
Thank you very much for your help.