Matrix Rain help (poor performance)

RastHacker

Member
Joined
Oct 22, 2019
Messages
19
Programming Experience
1-3
Hi, i learn my self to making winforms in c# with vs 2015 since my 15 yo.

And today for fun i wanna make a matrix rain with rasta colors but in winform not console application.I have finished but i've got poor performance, and maybe because i'm not very good to C# coding ;D, to explain what i've done, i just have to show you ;D

(show below post 3 ...)

thanks for all the comunity for helping ;D (not registered but using this forum since a while ^^)
 
Last edited:
A quick question before I log off, though.

Why are you trying to drive your two loop timers at different rates?

Also, did you read the documentation? The WinForms timer has a resolution of 55 milliseconds. Trying to drive one of your timers at less than that rate isn't really helping you much other than piling up tick events right behind each other.
 
A quick question before I log off, though.

Why are you trying to drive your two loop timers at different rates?

Also, did you read the documentation? The WinForms timer has a resolution of 55 milliseconds. Trying to drive one of your timers at less than that rate isn't really helping you much other than piling up tick events right behind each other.

Okay thanks for editing (and sorry), i've changed backgroundworker to async task with await, more smooth ~3% less CPU usage, change loop interval to 66ms for all 2. more smoother but same CPU usage.

I've searched more ways to draw large bitmap faster but didn't found more faster than DrawImageUnscaled. Maybe there is one ?
 
Not for actually drawing your bitmap, for drawing stuff into your bitmap:
- You will likely get some more speed if you cache your brushes and pens instead of creating them on the fly.
- Some of the floating point calculations you do are repeated. Perhaps also only compute those once and keep the values around?

I'm not sure if the GraphicsPath is really buying you anything.
 
In the spoiler is a quick stab at trying to improve the performance.

The biggest bang for minimal buck was just a minor change to have the render loop invalidate the control at a fixed frame rate, and then have the Paint event handler draw the bitmap.

The next thing was to cut down on the number of rendering bitmaps being created and then have the bits copied around from output of the worker to the final renderer, and then from the final renderer to the screen. I implemented a simple double buffering were rendering always happens to the non-current bitmap by the worker, and then when the rendering is done, mark that bitmap as the current bitmap, and use the previous current bitmap as the next render bitmap.

The simple runs of the code had the VS showing that a lot of garbage collection was being done. I tried to some caching and sharing of GDI pens and brushes. The seemed to delay the GC that looked to run every 2 seconds out to something like every 7 seconds.

There's more tricks that can be done like using an pool of drawing elements so that those don't have to continually being destroyed and recreated, but I've since lost interest in this.

As a quick aside, your original code was already using a low amount of CPU at about 9%, but as you noticed it was running slowly and choppy. My code changes actually end up using more CPU at about 10-12%, but in my opinion it was running faster and smoother.

C#:
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using Timer = System.Windows.Forms.Timer;

namespace TestMatrixRain
{

    public class MatrixRainControl : Panel
    {
        const string CharactersToChoose = "ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘヲイクコソチトノフヤヨルレロン";
        const int FramesPerSecond = 12;
        const int MaxShadowCount = 19;
        const int MaxHaloCount = 9;

        static Random randomVar = new Random();

        BackgroundWorker worker = new BackgroundWorker();
        Timer workerLoop = new Timer();
        Timer renderLoop = new Timer();

        Bitmap [] renderBitmap = new Bitmap[2];
        int currentBitmapIndex = 0;

        List<DrawElement> ElementsList = new List<DrawElement>();
        private int minElements = 10;
        private int currMaxElements = 55;
        private int maxElement = 200;
        private int timeToUpMaxElement = 10;
        private int currTime = 0;

        Color elementForeColor;
        Color elementBackColor;
        Brush[,] shadowBrush = new Brush[MaxShadowCount, MaxShadowCount];
        Pen [] haloPen = new Pen[MaxHaloCount];
        Brush backColorBrush;

        public MatrixRainControl()
        {
            worker.DoWork += Worker_DoWork;
            workerLoop.Interval = 66;
            workerLoop.Tick += WorkerLoop_Tick;

            renderLoop.Interval = 1000 / FramesPerSecond;
            renderLoop.Tick += (o, e) => Invalidate();

            Color baseColor = Color.MediumAquamarine;
            elementBackColor = ControlPaint.LightLight(baseColor);
            elementForeColor = ControlPaint.Dark(baseColor);
            for(int i = 1; i < MaxShadowCount; i++)
            {
                for(int j = 1; j <  MaxShadowCount; j++)
                {
                    int alpha = 255 - Math.Min(230, ((230 / i) * j));
                    shadowBrush[i, j] = new SolidBrush(Color.FromArgb(alpha, elementForeColor));
                }
            }
            for (int i = 0; i < MaxHaloCount; i++)
            {
                haloPen[i] = new Pen(Color.FromArgb(32, elementBackColor), i);
                haloPen[i].LineJoin = LineJoin.Round;
            }
            backColorBrush = new SolidBrush(elementBackColor);

            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.UserPaint, true);
            SetStyle(ControlStyles.DoubleBuffer, true);
            UpdateStyles();
        }

        public void StartAnimation()
        {
            renderBitmap[0] = new Bitmap(this.Width, this.Height);
            renderBitmap[1] = new Bitmap(this.Width, this.Height);

            workerLoop.Start();
            renderLoop.Start();

            AddUnlimitedMatrixChars(250);
        }

        void AddUnlimitedMatrixChars(int interval)
        {
            var t = new Timer();
            t.Interval = interval;
            t.Tick += delegate (object sender, EventArgs args)
            {
                if (ElementsList.Count < currMaxElements)
                {
                    AddMatrixChar();
                }
            };

            t.Start();
        }

        void AddMatrixChar()
        {
            Color baseColor = Color.MediumAquamarine;
            var el = new DrawElement()
            {
                Location = new PointF(randomVar.Next(0, this.Width), -350),
                Size = new SizeF(20, 20),
                BackColor = elementBackColor,
                ForeColor = elementForeColor
            };

            int step = randomVar.Next(2, 6);
            float fStep = (float)(step / 10);
            int kanaChangeTime = randomVar.Next(2, 10);
            int currKanaChange = 0;
            float textWidth = randomVar.Next(5, 34);
            int shadowCount = randomVar.Next(6, MaxShadowCount);

            Action<Graphics> draw = delegate (Graphics graphics)
            {
                string character = (string) el.Tag;
                using (var gp = new GraphicsPath())
                {
                    gp.AddString(character, this.Font.FontFamily, 0, textWidth, el.Location, StringFormat.GenericDefault);

                    for (int i = 1; i < shadowCount; i++)
                    {
                        string shadowChar = "";
                        int shadowTagIndex = (el.TagHistory.Count - 1) - i;
                        if (shadowTagIndex >= 0)
                        {
                            shadowChar = (string) el.TagHistory[shadowTagIndex];
                        }

                        using (var gpShadow = new GraphicsPath())
                        {
                            float yLocation = el.Location.Y - (((float)(textWidth * 1.2)) * i);

                            gpShadow.AddString(shadowChar, this.Font.FontFamily, 0, textWidth, new PointF(el.Location.X, yLocation), StringFormat.GenericDefault);
                            graphics.FillPath(shadowBrush[shadowCount, i], gpShadow);
                        }
                    }

                    int maxHalo = Math.Min((int)(textWidth / 2), MaxHaloCount);
                    for (int i = 1; i < maxHalo; i++)
                    {
                        graphics.DrawPath(haloPen[i], gp);
                    }

                    graphics.FillPath(backColorBrush, gp);
                }

                if (el.Location.Y < (this.Height) + ((float)(textWidth * 1.2) * shadowCount))
                {
                    el.Location.Y += step;

                    if (currKanaChange < kanaChangeTime)
                    {
                        currKanaChange += 1;
                    }
                    else
                    {
                        currKanaChange = 0;
                        el.AddTag(GetRandomKana().ToString());
                    }
                }
                else
                {
                    el.DeleteMySelf = true;
                }
            };

            el.AddTag(GetRandomKana().ToString());

            el.DrawAction = draw;
            ElementsList.Add(el);
        }

        char GetRandomKana()
        {
            return CharactersToChoose[randomVar.Next(0, CharactersToChoose.Length)];
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            e.Graphics.DrawImageUnscaled(renderBitmap[currentBitmapIndex], new Rectangle(0, 0, this.Width, this.Height));
        }

        private void WorkerLoop_Tick(object sender, EventArgs e)
        {
            if (!worker.IsBusy)
            {
                worker.RunWorkerAsync();

                if (currTime < timeToUpMaxElement)
                {
                    currTime += 1;
                }
                else
                {
                    currTime = 0;
                    if (currMaxElements < maxElement)
                    {
                        currMaxElements += 1;
                    }
                }
            }
            else
            {
                if (currMaxElements > minElements)
                {
                    currMaxElements -= 1;
                    currTime = 0;
                }
            }
        }

        private void Worker_DoWork(object sender, DoWorkEventArgs e)
        {
            int nextBitmapIndex = (currentBitmapIndex + 1) % renderBitmap.Length;
            Bitmap tempPic = renderBitmap[nextBitmapIndex];

            List<DrawElement> tempList = new List<DrawElement>();
            Graphics tempPicGraphs = Graphics.FromImage(tempPic);

            tempList.AddRange(ElementsList);

            tempPicGraphs.Clear(this.BackColor);

            foreach (DrawElement drawElement in tempList)
            {
                drawElement.Draw(tempPicGraphs);

                if (drawElement.DeleteMySelf)
                {
                    ElementsList.Remove(drawElement);
                }
            }

            e.Result = tempPic;

            Interlocked.Exchange(ref currentBitmapIndex, nextBitmapIndex);
        }
    }


    public class DrawElement
    {
        public PointF Location;
        public SizeF Size;
        public Color BackColor;
        public Color ForeColor;
        public Object Tag;

        public Action<Graphics> DrawAction;

        public List<Object> TagHistory = new List<object>();

        public bool DeleteMySelf = false;

        public DrawElement()
        {
        }

        public void AddTag(string tagString)
        {
            if (Tag != null)
            {
                TagHistory.Add(Tag);
                Tag = tagString;
            }
            else
            {
                Tag = tagString;
                TagHistory.Add(Tag);
            }
        }

        public void AddTag(Object tagObj)
        {
            if (Tag != null)
            {
                TagHistory.Add(tagObj);
                Tag = tagObj;
            }
            else
            {
                TagHistory.Add(tagObj);
                Tag = tagObj;
            }
        }

        async public void Draw(Graphics g)
        {
            DrawAction(g);
        }
    }
}
 
Thank you a lot, i'll remake your code by myself to understant what you've done, and for me less CPU usage ^^ now it's arround 3-7%

Backgroundworker vs async Task ? same ?
 
Last edited by a moderator:
After many tests, you are the goat ^^

I can add more elements at same times without lag, i can anime in 1920x1080 without lag, and even if i change FPS from 12 to 30. Insane

Thank you very much
 
Recall the time resolution of the WinForms Timer. 1000 / 30 = 33. 33 is less than 55. You'll need to try switching to a different Timer. You have multiple choices.
 
Recall the time resolution of the WinForms Timer. 1000 / 30 = 33. 33 is less than 55. You'll need to try switching to a different Timer. You have multiple choices.

Yes the animation freeze some times, i've tested with System.Threading.Timer.
I don't know if it is the best but it's smoother ^^ thank you

Now i am at 25FPS, with 22ms worker loop, and it's very smooth but higher CPU usage (arround 15%)
 
That might be good enough considering that movies traditionally are shown at 24 FPS.

Of course, if you are a gamer, then your standards might be a lot higher. :)
 
Back
Top Bottom