SciChart® the market leader in Fast WPF Charts, WPF 3D Charts, and now iOS Charting & Android Chart Components

Fact – Immediate Mode Drawing in WPF, Silverlight & WinRT is Non-Existant, or Slow

It struck us recently that during the development of SciChart – High Performance WPF Silverlight Charts, we have pretty neatly architected an alternate rendering engine in WPF in order to draw fast 2D graphics. WPF contains no Immediate Mode rendering API like WinForms OnPaint and the override OnRender is sadly lacking. Even frameworks such as Direct2D are also extremely slow. There is no current solution for enabling high performance immediate mode rendering in WPF like there is in WinForms.

Stackoverflow has many questions from users on how to enable High-Performance graphics using WPF, or Bitmap Optimization patterns, so we believe that fast, Immediate-mode rendering is a needed pattern in WPF applications.

We have a Fast, Lightweight, Immediate Mode Drawing API for WPF

Internally to SciChart, we have a type called RenderSurface, which is a sort of a canvas. A RenderSurface can be included on your chart like a WPF Canvas, except it supports a Draw() event, which passes a RenderContext. For instance, this may be declared and used as follows:

[xml]
<!– XAML –>
<Grid>
<s:HighQualityRenderSurface x:Name="renderSurface" MaxFrameRate="25" Draw="OnDrawCallback"/>
</Grid>
[/xml]

[csharp]
// code behind
/// <summary>
/// Draw callback. Handles the immediate-mode draw event of the RenderSurface
/// </summary>
private void OnDrawCallback(object sender, DrawEventArgs e)
{
// Get a graphics context
using (var context = e.RenderSurface2D.GetRenderContext())

// Get a pen
using (var pen = context.CreatePen(Colors.Red, true, 5.0f))
using (var ellipsePen = context.CreatePen(Colors.Purple, true, 2.0f))
using (var ellipseFill = context.CreateBrush(Colors.PaleVioletRed))
{
// Draw some lines
context.DrawLine(pen, new Point(0, 0), new Point(this.ActualWidth, this.ActualHeight));
context.DrawLine(pen, new Point(20, ActualHeight-100), new Point(this.ActualWidth, this.ActualHeight));

// Draw some ellipses
context.DrawEllipse(10, 10, new Point(40, 80), ellipseFill, ellipsePen);
context.DrawEllipse(15, 15, new Point(80, 100), ellipseFill, ellipsePen);
context.DrawEllipse(15, 15, new Point(120, 120), ellipseFill, ellipsePen);

// Draw some text
context.DrawText(new Rect(0, 0, 400, 100), Colors.DimGray, 16f, "Hello Immediate-Mode World!");
}
}
[/csharp]

This results in the following output:

Screen Shot 2014-07-03 at 10.30.51

The Graphics Context – IRenderContext2D

The RenderSurface supports the most common drawing functions needed to draw charts and simple diagrams via the IRenderContext2D interface. For instance, API methods include:

[csharp]
/// <summary>
/// Defines the interface to a 2D RenderContext, allowing drawing, blitting and creation of pens and brushes on the <see cref="Abt.Controls.SciChart.Rendering.Common.RenderSurfaceBase"/>
/// </summary>
/// <remarks>The <see cref="IRenderContext2D"/> is a graphics context valid for the current render pass. Any class which implements <see cref="IDrawable"/> has an OnDraw method
/// in which an <see cref="IRenderContext2D"/> is passed in. Use this to draw penned lines, fills, rectangles, ellipses and blit graphics to the screen.</remarks>
public interface IRenderContext2D : IDisposable
{
/// <summary>
/// Gets the current size of the viewport.
/// </summary>
Size ViewportSize { get; }

/// <summary>
/// Creates a <see cref="IBrush2D"/> valid for the current render pass. Use this to draw rectangles, polygons and shaded areas 
/// </summary>
/// <param name="color">The color of the brush, supports transparency</param>
/// <param name="opacity">The opacity of the brush</param>
/// <param name="alphaBlend">If true, use alphablending when shading. If null, auto-detect</param>
/// <returns>The <see cref="IBrush2D"/> instance</returns>
IBrush2D CreateBrush(Color color, double opacity = 1, bool? alphaBlend = null);

/// <summary>
/// Creates a <see cref="IBrush2D"/> from WPF Brush valid for the current render pass. Use this to draw rectangles, polygons and shaded areas 
/// </summary>
/// <param name="brush">The WPF Brush to use as a source, e.g. this can be a <seealso cref="SolidColorBrush"/>, or it can be a <seealso cref="LinearGradientBrush"/>. Note that solid colors support transparency and are faster than gradient brushes</param>
/// <param name="opacity">The opacity of the brush</param>
/// <param name="textureMappingMode">Defines a <see cref="TextureMappingMode"/>, e.g. brushes share a texture per viewport or a new texture per primitive drawn</param>
/// <returns>The <see cref="IBrush2D"/> instance</returns>
IBrush2D CreateBrush(Brush brush, double opacity = 1, TextureMappingMode textureMappingMode = TextureMappingMode.PerScreen);

/// <summary>
/// Creates a <see cref="IPen2D"/> valid for the current render pass. Use this to draw outlines, quads and lines
/// </summary>
/// <param name="color">The color of the pen, supports transparency</param>
/// <param name="antiAliasing">If true, use antialiasing</param>
/// <param name="strokeThickness">The strokethickness, default=1.0</param>
/// <param name="opacity">The opecity of the pen</param>
/// <param name="strokeDashArray"> </param>
/// <param name="strokeEndLineCap"> </param>
/// <returns>The <see cref="IPen2D"/> instance</returns>
IPen2D CreatePen(Color color, bool antiAliasing, float strokeThickness, double opacity = 1.0, double[] strokeDashArray = null, PenLineCap strokeEndLineCap=PenLineCap.Round);

/// <summary>
/// Creates a Sprite from FrameworkElement by rendering to bitmap. This may be used in the <see cref="DrawSprite"/> method
/// to draw to the screen repeatedly
/// </summary>
/// <param name="fe"></param>
/// <returns></returns>
ISprite2D CreateSprite(FrameworkElement fe);

/// <summary>
/// Clears the <see cref="IRenderSurface2D"/>
/// </summary>
void Clear();

/// <summary>
/// Blits the source image onto the <see cref="IRenderSurface2D"/>
/// </summary>
/// <param name="srcSprite">The source sprite to render</param>
/// <param name="srcRect">The source rectangle</param>
/// <param name="destPoint">The destination point, which will be the top-left coordinate of the sprite after blitting</param>
void DrawSprite(ISprite2D srcSprite, Rect srcRect, Point destPoint);

/// <summary>
/// Batch draw of the source sprite onto the <see cref="IRenderSurface2D"/>
/// </summary>
/// <param name="sprite2D">The sprite to render</param>
/// <param name="srcRect">The source rectangle</param>
/// <param name="points">The points to draw sprites at</param>
void DrawSprites(ISprite2D sprite2D, Rect srcRect, IEnumerable<Point> points);

/// <summary>
/// Batch draw of the source sprite onto the <see cref="IRenderSurface2D"/>
/// </summary>
/// <param name="sprite2D">The sprite to render</param>
/// <param name="dstRects">The destination rectangles to draw sprites at</param>
void DrawSprites(ISprite2D sprite2D, IEnumerable<Rect> dstRects);

/// <summary>
/// Fills a rectangle on the <see cref="IRenderSurface2D"/> using the specified <see cref="IBrush2D"/>
/// </summary>
/// <param name="brush">The brush</param>
/// <param name="pt2">The top-left point of the rectangle</param>
/// <param name="pt1">The bottom-right point of the rectangle</param>
/// <param name="gradientRotationAngle">The angle which the brush is rotated by, default is zero</param>        
void FillRectangle(IBrush2D brush, Point pt1, Point pt2, double gradientRotationAngle = 0);

/// <summary>
/// Fills a polygon on the <see cref="IRenderSurface2D"/> using the specifie <see cref="IBrush2D"/>
/// </summary>
/// <param name="brush">The brush</param>
/// <param name="points">The list of points defining the closed polygon, where X,Y coordinates in clockwise direction</param>
void FillPolygon(IBrush2D brush, IEnumerable<Point> points);

/// <summary>
/// Fills an area sliced by the zero line, e.g. as in a mountain chart, using the specifie <see cref="IBrush2D"/>
/// </summary>
/// <param name="brush">The brush</param>
/// <param name="points">The list of points defining the open area outline</param>
/// <param name="zeroLine">The zero line (coordinate which corresponds to Y=0) which slices the area</param>
/// <param name="gradientRotationAngle">The angle which the brush is rotated by</param>
void FillArea(IBrush2D brush, IEnumerable<Point> points, double zeroLine, double gradientRotationAngle = 0);

/// <summary>
/// Draws a Quad on the <see cref="IRenderSurface2D"/> using the specified <see cref="IPen2D"/>
/// </summary>
/// <param name="pen">The Pen</param>
/// <param name="pt1">Left-top point in the quad</param>
/// <param name="pt2">Bottom-right point in the quad</param>
void DrawQuad(IPen2D pen, Point pt1, Point pt2);

/// <summary>
/// Draws an Ellipse on the <see cref="IRenderSurface2D"/> using the specified outline <see cref="IPen2D">Pen</see> and fill <see cref="IBrush2D">Brush</see>
/// </summary>
/// <param name="strokePen">The stroke pen</param>
/// <param name="fillBrush">The fill brush</param>
/// <param name="center">The center of the ellipse in pixels</param>
/// <param name="width">The width of the ellipse in pixels</param>
/// <param name="height">The height of the ellipse in pixels</param>
void DrawEllipse(IPen2D strokePen, IBrush2D fillBrush, Point center, double width, double height);

/// <summary>
/// Draws 0..N Ellipses at the points passed in with the same width, height, pen and brush
/// </summary>
/// <param name="strokePen"></param>
/// <param name="fillBrush"></param>
/// <param name="centres">The points to draw ellipses at</param>
/// <param name="width">The common width for all ellipses</param>
/// <param name="height">The common height for all ellipses</param>
void DrawEllipses(IPen2D strokePen, IBrush2D fillBrush, IEnumerable<Point> centres, double width, double height);

/// <summary>
/// Draws a single line on the <see cref="IRenderSurface2D"/> using the specified <see cref="IPen2D"/>. 
/// Note for a faster implementation in some rasterizers, use DrawLines passing in an IEnumerable
/// </summary>
/// <param name="pen">The pen</param>
/// <param name="pt1">The start of the line in pixels</param>
/// <param name="pt2">The end of the line in pixels</param>
void DrawLine(IPen2D pen, Point pt1, Point pt2);

/// <summary>
/// Draws a multi-point line on the <see cref="IRenderSurface2D"/> using the specified <see cref="IPen2D"/>
/// </summary>
/// <param name="pen">The pen</param>
/// <param name="points">The points </param>
/// <returns>The last point in the polyline drawn</returns>
Point DrawLines(IPen2D pen, IEnumerable<Point> points);

/// <summary>
/// Draws text if it does not go outside 
/// </summary>
void DrawText(Rect dstBoundingRect, Color foreColor, float fontSize, string text);    
}
[/csharp]

It doesn’t look like a lot, but internally, we have used the lowly IRenderContext2D interface to draw all of this, and yes, its freakin’ fast! …

 

Switchable Rendering Plugins – High Speed / High Quality / DirectX10

What’s more, the RenderSurface implementation we have built supports three rendering plugins:

  • HighSpeed – A Fast, Integer Fixed-point software implementation with approximate alpha-blend that draws onto a WriteableBitmap
  • HighQuality – A Floating-point software implementation with true alpha-blend that draws onto a WriteableBitmap
  • DirectX10 – A Floating-point hardware implementation with true alpha-blend that draws onto a Custom DX10 surface (Supports Windows Vista, 7, 8 and Up)

 

Believe it or not, DirectX or OpenGL itself do not have a hardware implementation for drawing lines with thickness > 1 or with AntiAliasing!

 

The DirectX10 implementation is recent. We have built completely proprietary code and Pixel/Geometry shaders to efficiently draw Anti-Aliased lines of variable thickness on the GPU gaining a significant performance boost over software-only or vanilla Direct2D / Direct3D implementations.

All three implementations (HS, HQ, DX10) support:

  • Stroked, AntiAliased or Dashed lines of variable thickness
  • Fast Polyline implementation
  • LinearGradientBrush, SolidColorBrush arbitrary Polygon, Quad (rectangle) fills.
  • Ellipse, Triangle and Sprite (renders a sprite from WPF FrameworkElement)

So far we’re only used this internally to SciChart, but we see its potential …

 

We have built completely proprietary code and Pixel/Geometry shaders to efficiently draw Anti-Aliased lines of variable thickness on the GPU gaining a significant performance boost over software-only or vanilla Direct2D / Direct3D implementations.

 

Why are we telling you this?

Well, aside from a marketing exercise and showcase of SciChart’s awesomeness (!) we are considering spinning off the RenderSurface into a sister-product of its own. This won’t affect existing SciChart customers as they already have access to this functionality. However, we want to know from the wider-community if you could see a need for this in your work. For instance, are you trying to build:

  • WPF Diagramming Applications?
  • WPF CAD, Drawing or Paint Applications?
  • Custom Scientific 2D Visualisation which requires Immediate-Mode drawing?
  • Accelerate something in your work

If so, we have some questions for you:

Q: Would you be interested in us packaging the RenderSurface as its own DLL with documentation? 

Q: Would you be interested in this on the WPF, or Silverlight, or WinRT platforms? 

Q: Would you be interested in Source-Code or DLL only?

 

If the answer is YES, please Contact Sales with the subject line RenderSurface to let us know what you think.

 

Packaging the DLL up and refactoring the code to be production-ready will take some effort. We only want to do it if there is a demand for it. We’re not thinking of charging a lot, in fact, we’re thinking of packaging it with SciChart licenses, and releasing stand-alone for a fraction of the cost. Something to cover the effort of support / maintenance.

Let us know what you think 🙂

Best regards,
[SciChart HQ]

 

Comments are closed.