1

I would like to create a user control in WPF, which would be able draw an n x m matrix of characters using a mono-space font. The control will accept a string[] as fast as possible ( 60 fps is the target) and draw it on the screen.

I need performance similar to mplayer ascii playback.

All characters are drawn using the same mono-space font, but may have different colors and background according to certain rules (similar to syntax highlighting in VS).

I have implemented the solution in C# WinForms without any problem and got to 60 FPS, but when I wanted to learn how to do this in WPF I only found several articles and posts describing problems with WPF performance and conflicting information.

So what is the best way do achieve highest performance in this case?

A naive approach I tried is:

 /// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    Random rand = new Random();

    public MainWindow()
    {
        InitializeComponent();

        DispatcherTimer timer = new DispatcherTimer();
        timer.Interval = TimeSpan.FromMilliseconds(1);
        timer.Tick += timer_Tick;
        timer.Start();
    }

    string GenerateRandomString(int length)
    {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++)
        {
            sb.Append(rand.Next(10));
        }
        return sb.ToString();
    }
    void timer_Tick(object sender, EventArgs e)
    {
        myTextBlock.Inlines.Clear();

        for (int i = 0; i < 30; i++)
        {
            var run = new Run();
            run.Text = GenerateRandomString(800);
            run.Foreground = new SolidColorBrush(Color.FromArgb((byte)rand.Next(256),(byte)rand.Next(256),(byte)rand.Next(256),(byte)rand.Next(256)));
            run.Background = new SolidColorBrush(Color.FromArgb((byte)rand.Next(256),(byte)rand.Next(256),(byte)rand.Next(256),(byte)rand.Next(256)));
            myTextBlock.Inlines.Add(run);
        }

    }
}

The question is: Can you do better than that in WPF?

P.S. Yes, I could use DirectX directly, but this question is about WPF and not DX.

Eiver
  • 2,594
  • 2
  • 22
  • 36
  • 1
    considering WPF is _hardware-accelerated_ and WinForms is not, you stand a good chance to implement something in WPF that can meet your 60 FPS requirement. WPF uses DirectX under the hood (e.g. its possible to use HLSL in WPF). Other than that your question is arguably **too broad**. –  Oct 20 '15 at 07:50
  • 1
    Surely the best way is to actually first build it. *Then* see if it meets your criteria. And if not, *then* ask questions. Like this, it's too broad indeed. – Willem van Rumpt Oct 20 '15 at 08:01
  • There are several other questions about WPF performance, like [this](http://stackoverflow.com/questions/8713864/high-performance-graphics-using-the-wpf-visual-layer) and [this](http://stackoverflow.com/questions/5401549/ways-to-improve-wpf-ui-rendering-speed). – Anders Carstensen Oct 20 '15 at 08:31
  • 1
    How come such a simple and well defined problem (drawing lots of arbitrary chars on the screen) can be considered too broad? – Eiver Oct 20 '15 at 08:41
  • @AndersKellerCarstensen I think the problem with those examples are that people are creating far too many screen visuals (rather than compositing them) and trying to databind them all. The performance issue is in the _databinding_ **not** the _rendering_. –  Oct 20 '15 at 08:47
  • 3
    How do you know you *have* a problem? – Willem van Rumpt Oct 20 '15 at 08:47
  • 1
    @Anders The first link suggests using WriteableBitmap. I will look into that if I fail to find a more high level solution. The link also suggests to use SlimDX, which I want to avoid. I want to use WPF for its high level features and get best performance there. If I wanted to draw on bitmap I would not ask this question and I would not add a P.S. – Eiver Oct 20 '15 at 08:48
  • 1
    @Eiver Did you see this though - _"[There is one caveat though. While WPF uses the CPU for tesselation / GPU for rendering, **WriteableBitmap will use CPU for everything**. Therefore the fill-rate (number of pixels rendered per frame) becomes the **bottleneck** depending on your **CPU** power](http://stackoverflow.com/questions/8713864/high-performance-graphics-using-the-wpf-visual-layer)"_. It may not be any faster than your WinForms code –  Oct 20 '15 at 08:51
  • 1
    That is why asked this question, because all answers I found have caveats like this – Eiver Oct 20 '15 at 08:55
  • 1
    @Anders link 2 is not the answer to my problem, but a bunch of generic hits to improve applications with lots of controls. I will have a single control in my application or do you suggest I should have a separate control for every char?? They mention DrawingContext.DrawGlyphs. I found no such thing, but I found DrawingContext.DrawGlyphRun(). Are you saying this is the best method? – Eiver Oct 20 '15 at 08:57
  • 1
    Please refer to http://stackoverflow.com/questions/16707193/is-there-a-fast-way-of-drawing-characters-to-the-screen to see what kind of answer I am looking for. Look at the answer by Aseem Gautam not the one marked as "Answer". If my question is so trival that it deserves a down-vote, then why don't you consider providing a code example how to solve this trivial task. – Eiver Oct 20 '15 at 08:59
  • 1
    @Eiver I don't think we can come up with the best performing solution by discussing it - you have to try out different things. Right now I tried making one TextBlock with 64*32 runs (one char in each), and then I used CompositionTarget.Rendering to update the background color of each of these runs. That ran at 20 fps. I also tried using a DispatcherTimer with an Interval of 1ms instead of using CompositionTarget, but the result was the same. I would expect WriteableBitmapEx to perform better. – Anders Carstensen Oct 20 '15 at 09:03
  • 1
    I have added an example to my original question. Things that I tried before asking it. – Eiver Oct 20 '15 at 09:08
  • 2
    Your code example doesn't help illustrate the question. Even ignoring for the moment the question of whether a person can read text changing at 60 fps, the code example has at least two significant problems in terms of measuring performance: `DispatcherTimer` cannot reliably even trigger `Tick` events at 60 Hz; and without controlling memory allocations more carefully (e.g. reusing a single fixed-capacity `StringBuilder`, or even better just pre-computing all strings to be displayed) your test is confounded by GC activity. – Peter Duniho Oct 21 '15 at 04:05
  • 1
    Tick does trigger on my machine at 60 fps if the amount of chars is not too large. GenerateRandomString() in the for loop executes at 600+ fps, so I am really far from getting a bottleneck from that. – Eiver Oct 21 '15 at 12:06
  • WPF already provides animations and timings to change property values and generate eg spinning or fading text, just by using Markup. Trying to change brush values in the code behind is wrong. In fact, your WinForms code isn't good either as you create a *lot* of expensive GDI brushes – Panagiotis Kanavos Oct 27 '15 at 10:57
  • @Eiver the problem with your question is that 80% of SO layabouts cant understand let alone answer and it angers them immensely because they are hooked on dopamine reward drug.. you are too clever for them. cant be fixed – Boppity Bop May 17 '22 at 19:25

2 Answers2

11

Probably the fastest way to draw text in WPF is to use GlyphRun.

Here is a sample code:

MainWindow.xaml

<Window x:Class="WpfApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="300" Width="300">
    <Image>
        <Image.Source>
            <DrawingImage x:Name="drawingImage"/>
        </Image.Source>
    </Image>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;

namespace WpfApplication
{
    public partial class MainWindow : Window
    {
        Random rand = new Random();

        Stopwatch stopwatch;
        long frameCounter = 0;

        GlyphTypeface glyphTypeface;
        double renderingEmSize, advanceWidth, advanceHeight;
        Point baselineOrigin;

        public MainWindow()
        {
            InitializeComponent();

            new Typeface("Consolas").TryGetGlyphTypeface(out this.glyphTypeface);
            this.renderingEmSize = 10;
            this.advanceWidth = this.glyphTypeface.AdvanceWidths[0] * this.renderingEmSize;
            this.advanceHeight = this.glyphTypeface.Height * this.renderingEmSize;
            this.baselineOrigin = new Point(0, this.glyphTypeface.Baseline * this.renderingEmSize);

            CompositionTarget.Rendering += CompositionTarget_Rendering;

            DispatcherTimer timer = new DispatcherTimer();
            timer.Interval = TimeSpan.FromMilliseconds(1000);
            timer.Tick += timer_Tick;
            timer.Start();
        }

        void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            if (this.stopwatch == null)
                this.stopwatch = Stopwatch.StartNew();

            ++this.frameCounter;

            this.drawingImage.Drawing = this.Render();
        }

        string GenerateRandomString(int length)
        {
            var chars = new char[length];
            for (int i = 0; i < chars.Length; ++i)
                chars[i] = (char)rand.Next('A', 'Z' + 1);

            return new string(chars);
        }

        void timer_Tick(object sender, EventArgs e)
        {
            var seconds = this.stopwatch.Elapsed.TotalSeconds;
            Trace.WriteLine((long)(this.frameCounter / seconds));

            if (seconds > 10)
            {
                this.stopwatch.Restart();
                this.frameCounter = 0;
            }
        }

        private Drawing Render()
        {
            var lines = new string[30];
            for (int i = 0; i < lines.Length; ++i)
                lines[i] = GenerateRandomString(100);

            var drawing = new DrawingGroup();
            using (var drawingContext = drawing.Open())
            {
                // TODO: draw rectangles which represent background.

                // TODO: group of glyphs which has the same color should be drawn together.
                // Following code draws all glyphs in Red color.
                var glyphRun = ConvertTextLinesToGlyphRun(this.glyphTypeface, this.renderingEmSize, this.advanceWidth, this.advanceHeight, this.baselineOrigin, lines);
                drawingContext.DrawGlyphRun(Brushes.Red, glyphRun);
            }

            return drawing;
        }

        static GlyphRun ConvertTextLinesToGlyphRun(GlyphTypeface glyphTypeface, double renderingEmSize, double advanceWidth, double advanceHeight, Point baselineOrigin, string[] lines)
        {
            var glyphIndices = new List<ushort>();
            var advanceWidths = new List<double>();
            var glyphOffsets = new List<Point>();

            var y = baselineOrigin.Y;
            for (int i = 0; i < lines.Length; ++i)
            {
                var line = lines[i];

                var x = baselineOrigin.X;
                for (int j = 0; j < line.Length; ++j)
                {
                    var glyphIndex = glyphTypeface.CharacterToGlyphMap[line[j]];
                    glyphIndices.Add(glyphIndex);
                    advanceWidths.Add(0);
                    glyphOffsets.Add(new Point(x, y));

                    x += advanceWidth;

                }

                y += advanceHeight;
            }

            return new GlyphRun(
                glyphTypeface,
                0,
                false,
                renderingEmSize,
                glyphIndices,
                baselineOrigin,
                advanceWidths,
                glyphOffsets,
                null,
                null,
                null,
                null,
                null);
        }
    }
}
Stipo
  • 4,566
  • 1
  • 21
  • 37
  • See https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/introduction-to-the-glyphrun-object-and-glyphs-element?view=netframeworkdesktop-4.8 for more information about how to properly use advance widths and baselines. – cdiggins Apr 01 '22 at 15:43
2

This answer seems a bit flawed. Possible cases:

  1. The advance widths are different for each character
  2. There is no effort to align the baselines, since a line may consist of several glyph runs
  3. Since the absolute y position is buried in the run it can't be easily moved when a line is inserted
Malakai
  • 3,011
  • 9
  • 35
  • 49
Roswell
  • 21
  • 2