none
Slow performance transforming OnMouseMove

    Question

  • Greetings,

    I've written and enclosed a demo app that creates several geometries on a DrawingVisual and transforms the DrawingVisual based on a MouseMove position. Visually, the performance is good, but a little choppy. The final application may have a geometry count five to ten times whats demonstrated. As more geometries are added the expectation is that the frame rate will be approxiamtely 30 during MouseMove to enable panning that is smooth to the eye. The demo app, which is extracted from a more complex control using three DrawingVisuals, crawls at 6-12 fps during MouseMove translate transforms. The demo is an accurate sample of what occurs in the other app.

    While running the enclosed demo app, Perforator shows a frame rate of 15 when I click on the DrawingVisual and drag it from left to right. I am hoping to see a frame rate of at least 30. Does anyone have suggestions to increase the frame rate to 30 during MouseMove transforms? Or, to make the transforms appear completely smooth to the eye?

    Thanks in advance,

    Rana Ian

    --- Ready for Copy-Paste into Window1.xaml.cs ---

    using System;

    using System.Windows;

    using System.Windows.Controls;

    using System.Windows.Data;

    using System.Windows.Documents;

    using System.Windows.Media;

    using System.Windows.Media.Imaging;

    using System.Windows.Shapes;

    using System.Collections.Generic;

    using System.Windows.Input;

    namespace XAxisDemo

    {

    public partial class Window1 : Window

    {

    public Window1()

    {

    InitializeComponent();

    this.Loaded += Window_Loaded;

    }

    void Window_Loaded(object sender, RoutedEventArgs e)

    {

    this.Width = 1300;

    this.Height = 700;

    this.Content = new ChartHost();

    }

    }

    public class ChartHost : FrameworkElement

    {

    #region Fields

    VisualCollection m_vcChildren;

    SolidColorBrush m_TextBrush;

    Pen m_TickPen;

    SolidColorBrush m_MarginBrush;

    Glyphs m_Glyphs;

    GeometryGroup m_MarginGeometry;

    GeometryGroup m_TickGeometry;

    GeometryGroup m_BottomTextGeometry;

    GeometryGroup m_TopTextGeometry;

    DrawingVisual m_DrawingVisual;

    TranslateTransform m_DrawingVisualTransform;

    TranslateTransform m_InitialTransform;

    Point? m_InitialPoint;

     

    #endregion

    #region Constructors

    public ChartHost()

    {

    m_vcChildren = new VisualCollection(this);

    // Reflect the the Y-axis so that

    // 0,0 begins at the lower left corner of FrameworkElement

    this.RenderTransformOrigin = new Point(0.5, 0.5);

    this.RenderTransform = new ScaleTransform(1, -1);

    m_TextBrush = Brushes.Gray;

    m_TickPen = new Pen(Brushes.DimGray, 1);

    m_TickPen.Freeze();

    m_MarginBrush = Brushes.AntiqueWhite;

    m_Glyphs = new Glyphs();

    m_Glyphs.FontUri = new Uri("c:\\windows\\fonts\\arial.ttf");

    m_Glyphs.FontRenderingEmSize = 12;

    }

    #endregion

    #region Properties

    public VisualCollection Children

    {

    set

    {

    m_vcChildren = value;

    }

    get

    {

    return m_vcChildren;

    }

    }

    protected override int VisualChildrenCount

    {

    get

    {

    return m_vcChildren.Count;

    }

    }

    #endregion

    #region Methods

    protected override Visual GetVisualChild(int index)

    {

    if (index < 0 || index > m_vcChildren.Count)

    {

    throw new ArgumentOutOfRangeException();

    }

    return m_vcChildren[index];

    }

    protected override Size ArrangeOverride(Size finalSize)

    {

    this.InitializeVisual(finalSize);

    return base.ArrangeOverride(finalSize);

    }

    void InitializeVisual(Size finalSize)

    {

    m_vcChildren.Clear();

    m_MarginGeometry = new GeometryGroup();

    m_TickGeometry = new GeometryGroup();

    m_BottomTextGeometry = new GeometryGroup();

    m_TopTextGeometry = new GeometryGroup();

    #region Create Time & Pixel Points

    DateTime start = new DateTime(2010, 1, 1, 0, 0, 0, 0);

    DateTime stop = new DateTime(2010, 1, 2, 0, 0, 0, 0);

    TimeSpan tsIncrement = new TimeSpan(0, 1, 0);

    List<DateTime> lTimes = new List<DateTime>(1440);

    DoubleCollection dcPixels = new DoubleCollection(1440);

    double dPixel = 0;

    DateTime dtCurrent = start;

    while (dtCurrent < stop)

    {

    lTimes.Add(dtCurrent);

    dcPixels.Add(dPixel);

    dtCurrent = dtCurrent.Add(tsIncrement);

    dPixel += 4;

    }

    #endregion

    double dMarginHeight = 22;

    double dMarginLength = dcPixels[dcPixels.Count - 1];

    double dTopMarginBase = finalSize.Height - dMarginHeight;

    double dTickHeight = 8;

    double dBottomTickBase = dMarginHeight - dTickHeight;

    double dTopTickTop = dTopMarginBase + dTickHeight;

    double dTextPadding = 3;

    #region Create Margin Geometry

    // lower left corner is origin

    Rect bottomRect = new Rect(

    0,

    0,

    dMarginLength,

    dMarginHeight);

    m_MarginGeometry.Children.Add(new RectangleGeometry(bottomRect));

    Rect topRect = new Rect(

    0,

    dTopMarginBase,

    dMarginLength,

    dMarginHeight);

    m_MarginGeometry.Children.Add(new RectangleGeometry(topRect));

    #endregion

    #region Create Tick & Text

    for (int n = 0; n < lTimes.Count; n++)

    {

    double currentPixelX = dcPixels[n];

    DateTime currentTime = lTimes[n];

    bool bIsHour = currentTime.Minute == 0;

    bool bIs20Minute = currentTime.Minute % 20 == 0;

    bool bIs40Minute = currentTime.Minute % 40 == 0;

    if (bIsHour || bIs20Minute || bIs40Minute)

    {

    // Bottom Tick

    Point startPoint = new Point(currentPixelX, dBottomTickBase);

    Point endPoint = new Point(currentPixelX, dMarginHeight);

    LineGeometry topTick = new LineGeometry(startPoint, endPoint);

    m_TickGeometry.Children.Add(topTick);

    // Top Tick

    startPoint = new Point(currentPixelX, dTopMarginBase);

    endPoint = new Point(currentPixelX, dTopTickTop);

    LineGeometry bottomTick = new LineGeometry(startPoint, endPoint);

    m_TickGeometry.Children.Add(bottomTick);

     

    // Text

    if (bIsHour)

    {

    m_Glyphs.UnicodeString = currentTime.ToString("htt");

    }

    else

    {

    m_Glyphs.UnicodeString = currentTime.ToString("m.").TrimEnd('.');

    }

    Geometry glyphGeometry = m_Glyphs.ToGlyphRun().BuildGeometry();

    m_Glyphs.OriginX = currentPixelX - (glyphGeometry.Bounds.Width / 2);

    m_Glyphs.OriginY = 0;

    Geometry bottomGlyphGeometry = m_Glyphs.ToGlyphRun().BuildGeometry();

    m_BottomTextGeometry.Children.Add(bottomGlyphGeometry);

    Geometry topGlyphGeometry = m_Glyphs.ToGlyphRun().BuildGeometry();

    m_TopTextGeometry.Children.Add(topGlyphGeometry);

    }

    }

    // Compensate for y-axis inversion

    ScaleTransform scaleTransform = new ScaleTransform(1, -1);

    #region Bottom Text Compensation

    TransformGroup tgBottom = new TransformGroup();

    tgBottom.Children.Add(scaleTransform);

    TranslateTransform ttBottom = new TranslateTransform(0, dTextPadding);

    tgBottom.Children.Add(ttBottom);

    m_BottomTextGeometry.Transform = tgBottom;

    #endregion

    #region Top Text Compensation

    TransformGroup tgTop = new TransformGroup();

    tgTop.Children.Add(scaleTransform);

    double dOffsetY = dTopMarginBase + dTickHeight + dTextPadding;

    TranslateTransform ttTop = new TranslateTransform(0, dOffsetY);

    tgTop.Children.Add(ttTop);

    m_TopTextGeometry.Transform = tgTop;

    #endregion

    #endregion

    m_MarginGeometry.Freeze();

    m_TickGeometry.Freeze();

    m_BottomTextGeometry.Freeze();

    m_TopTextGeometry.Freeze();

    m_DrawingVisual = new DrawingVisual();

    m_DrawingVisualTransform = new TranslateTransform(0, 0);

    m_DrawingVisual.Transform = m_DrawingVisualTransform;

    DrawingContext drawingContext = m_DrawingVisual.RenderOpen();

    using (drawingContext)

    {

    drawingContext.DrawGeometry(m_MarginBrush, null, m_MarginGeometry);

    drawingContext.DrawGeometry(null, m_TickPen, m_TickGeometry);

    drawingContext.DrawGeometry(m_TextBrush, null, m_BottomTextGeometry);

    drawingContext.DrawGeometry(m_TextBrush, null, m_TopTextGeometry);

    }

    m_vcChildren.Add(m_DrawingVisual);

    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)

    {

    base.OnMouseLeftButtonDown(e);

    m_InitialPoint = e.GetPosition(this);

    m_DrawingVisualTransform = m_DrawingVisual.Transform as TranslateTransform;

    m_InitialTransform = m_DrawingVisualTransform.Clone();

    m_InitialTransform.Freeze();

    Mouse.Capture(this);

    }

    protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)

    {

    base.OnMouseLeftButtonUp(e);

    m_InitialPoint = null;

    Mouse.Capture(null);

    }

    protected override void OnMouseMove(MouseEventArgs e)

    {

    base.OnMouseMove(e);

    if (m_InitialPoint != null)

    {

    Point currentPoint = e.GetPosition(this);

    Vector? vDelta =

    currentPoint -

    m_InitialPoint;

     

    double dNewOffsetX =

    m_InitialTransform.Value.OffsetX +

    vDelta.Value.X;

    m_DrawingVisualTransform.X = dNewOffsetX;

    }

    }

    #endregion

    }

    }

    Sunday, May 21, 2006 10:44 PM

Answers

  • The demo app now clocks 110 fps. The original control with 3 DrawingVisuals and many geometries clocks 30 fps. The following observations and improvements were made. Many thanks to John and David for their support.

     

    Observation: Pen.DashStyle severely slows transforms.

    Solution: Avoid using Pen.DashStyle.

     

    Observation: Positioning geometries with Geometry.Transform adds overhead to the final transform. Positioning many geometries adds a lot of overhead.

    Solution: Position all geometries with the cpu. Avoid using Geometry.Transform for layout. 

     

    Observation: Drawing text as geometry decreases transform performance.

    Solution: Use DrawingContext.DrawGlyphRun() instead of DrawingContext.DrawGeometry(). If text needs to be positioned based on its geometry, call Glyphs.ToGlyphRun().BuildGeometry() to obtain geometry Bounds and toss the geometry.

     

    Observation: Using StreamGeometry increases transform performance.

    Solution: Pile in as many geometries into a single StreamGeometry that share fill and stroke.

     

    HTH,

    Rana

    Wednesday, May 24, 2006 8:34 PM

All replies

  • Eh, correction: the control which the demo app was extracted, which has more geometries and three DrawingVisuals blisters in at a 2-3 fps during TranslateTransform OnMouseMove. The performance is not good. I'm hoping someone will point out that this implmentation is improper, and point me to a proper one. Or, someone will say its fine and WPF is a sluggard. Thanks.
    Tuesday, May 23, 2006 3:08 AM
  • I haven't tried running your code yet as I'm just reading these forums for something to do while I eat my lunch so I'm sort of backseat driving here, but have you tested how often the ArrangeOverride function gets called? It might be getting called everytime you change a child object.

    I notice you have an initialization function being called there in the ArrangeOverride function. Is that because there are some drawing area size dependencies? If so then maybe you should hook into the "SizeChanged" event instead.

    Tuesday, May 23, 2006 5:21 AM
  • Hi John,

    Thank you for the response. The InitializeVisual(Size) method from the demo app is called within ArrangeOverride to receive the final control Size which is used to measure geometry dimensions. ArrangeOverride(Size) and OnRenderSizeChanged() are called when the window is presented or resized. So calls to OnMouseMove are not raising ArrangeOverride(Size) or OnRenderSizeChanged() and are not part of the performance decrease.

    After more investigation I determined that applying many individual transforms to individual geometries is detrimental to transforming a composite DrawingVisual. Another version which asks the cpu to calculate geometry layout has higher fps for transformations. So in this demo app, y-axis inversion using RenderTransform and text positioning using TranslateTransform is decreasing the overall performance of a composite DrawingVisual transform.

    Cheers,

    Rana Ian

    Tuesday, May 23, 2006 6:11 AM
  • I guess when you change a scene the renderer has to walk all of the objects in the visual tree to rerender the whole scene again. Having a transform attached to every object would certainly slow this process down, and this has to be done every render frame, whereas if you calculate the position using the CPU then that only has to be done for one frame.

    So I can see how if you have a scene with a transform attached to every object how  a change in that scene could be slow to render. It's a bit of a trap with this new WPF stuff in that you don't really have to think anymore about what goes into OnPaint().

    Tuesday, May 23, 2006 6:26 AM
  • Also, I'd strongly suggest you render the text via drawingContext.DrawGlyphRun() or
    drawingContext.DrawText() instead of converting all of the text into geometry and rendering it with drawingContext.DrawGeometry().  Rendering text as geometry can be substantially slower than allowing us to decide how to render it.  Additionally, you lose clear type when you render your text as geometry.

    David  

    Wednesday, May 24, 2006 12:21 AM
  • David,

    Margin dimensions in the given chart are based on text dimensions. Text placement is center-justified to specific tick geometries. Text width and height needs to be determined to determine text midpoint. Since Glyphs.ActuaWidth, Glyphs.ActualHeight are evaluated to 0.0 after Glyphs.UnicodeString is assigned, and GlyphRun doesn't offer rendered dimensions would you recommend I place GlyphRun s on the DrawingVisual while using GlyphRun.BuildGeometry().Bounds to determine how to place the GlyphRun origin? If theres no other way to determine GlyphRun render size than by calling BuildGeometry() I might as well use the geometry? At least that was the assumption I am operating on. Or, is the performance of transforming GlyphRuns clearly faster than operating on GlyphRun.BuildGeometries?

     Rana

    Wednesday, May 24, 2006 1:45 AM
  • If the only way to determine the dimensions of a GlyphRun is to get the dimensions after .BuildGeometries() (I'm not familiar enough with the text code to verify this), then I'd suggest you get the dimensions and then just discard the geometries.  You should only need to do this once in your constructor, so it's a one-time cost.  Otherwise, the cost of rendering all of your glyphs as geometries will hit you every frame.

    David

    Wednesday, May 24, 2006 2:14 AM
  • Excellent. GlyphRuns it is.
    Wednesday, May 24, 2006 2:24 AM
  • The demo app now clocks 110 fps. The original control with 3 DrawingVisuals and many geometries clocks 30 fps. The following observations and improvements were made. Many thanks to John and David for their support.

     

    Observation: Pen.DashStyle severely slows transforms.

    Solution: Avoid using Pen.DashStyle.

     

    Observation: Positioning geometries with Geometry.Transform adds overhead to the final transform. Positioning many geometries adds a lot of overhead.

    Solution: Position all geometries with the cpu. Avoid using Geometry.Transform for layout. 

     

    Observation: Drawing text as geometry decreases transform performance.

    Solution: Use DrawingContext.DrawGlyphRun() instead of DrawingContext.DrawGeometry(). If text needs to be positioned based on its geometry, call Glyphs.ToGlyphRun().BuildGeometry() to obtain geometry Bounds and toss the geometry.

     

    Observation: Using StreamGeometry increases transform performance.

    Solution: Pile in as many geometries into a single StreamGeometry that share fill and stroke.

     

    HTH,

    Rana

    Wednesday, May 24, 2006 8:34 PM