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
FieldsVisualCollection
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 inversionScaleTransform
scaleTransform = new ScaleTransform(1, -1);#region
Bottom Text CompensationTransformGroup
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 CompensationTransformGroup
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
}
}

Correction
Kristian Fischer
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.
Coolsingh
WernerKossack
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
Matt_K
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
Avada Kedavra
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
rhonda945
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
NetworkPro
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
A Barber
Mark Boulter
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().