17
\$\begingroup\$

I decided to learn about building games, so I picked up C# to use it along with Windows Form - I already have C# experience, so that was the main reason I did so. That said, I tasked myself to build a simple Windows Form app that renders a noise image targeting hopefully at 60 FPS. I'm using the GDI+ APIs to render bitmaps on screen; however, I noticed the app isn't even close to render 60 FPS - it renders around 36-39 FPS on a release build.

I should mention I'm a totally newbie in both Windows Form, and image rendering, so I believe my code isn't optimized at all. I'm sharing below the code snippet the app runs in order to loop infinitely to render the noise images.

    private static Bitmap GetStaticNoise(int width, int height)
    {
        var bitmap = new Bitmap(width, height);
        for (int y = 0; y < bitmap.Height; y++)
        {
            for (int x = 0; x < bitmap.Width; x++)
            {
                Color color = Color.Black;

                var rnd = Random.Shared.Next(0, 2);
                if (rnd % 2 == 0)
                {
                    color = Color.White;
                }

                bitmap.SetPixel(x, y, color);
            }
        }

        return bitmap;
    }

    private void Form1_Load(object? sender, EventArgs e)
    {
        Task.Factory.StartNew(() =>
        {
            var frameStopwatch = new Stopwatch();

            var screenGraphics = _screen.CreateGraphics();

            var font = new Font(FontFamily.GenericMonospace, 30);

            var ct = cts.Token;
            while (!ct.IsCancellationRequested)
            {
                for (int frame = 0; frame < 60; frame++)
                {
                    frameStopwatch.Restart();

                    // generates "noise"
                    Bitmap bitmap = GetStaticNoise(_screen.ClientSize.Width, _screen.ClientSize.Height);
                    screenGraphics.DrawImage(bitmap, new Point(0, 0));
                    
                    bitmap.Dispose();

                    frameStopwatch.Stop();

                    screenGraphics.DrawString(frameStopwatch.ElapsedMilliseconds.ToString(), font, Brushes.Red, new Point(0, 0));

                    //Thread.Sleep(Math.Max(0, _frameCooldown.Milliseconds - (int)frameStopwatch.ElapsedMilliseconds));
                }
            }

        }, TaskCreationOptions.LongRunning);
    }

Any advice about what could be changed here it's appreciated. Also, any resource that could be useful to learn about using GDI+ for game development with Windows Form is also appreciated (or any resource that could help me understanding for instance this situation I'm running into).

Ah! the Windows Form is on .NET 6.0. I'm also attaching a picture of the bitmap rendered on screen (upper left corner shows the time in MS to render the frame)

app frame rate

EDIT: thank you so much for the comments, etc. I did follow the recommendations, and I realized I wasn't even hitting the FPS I mentioned above :(. I added a fps counter, and I'm seeing 29-30 FPS - I will update my code to use something different than GDI+ - and eventually use something else for developing games. I'm attaching below the updated code.

public partial class Form1 : Form
{
    private readonly Bitmap _bitmap;
    private readonly Graphics _bitmapGraphics;

private readonly Random _rnd = new Random();

private readonly int _whiteArg = Color.White.ToArgb();
private readonly int _blackArg = Color.Black.ToArgb();

private readonly int[] _noise;

private readonly PictureBox _screen;
private readonly Graphics _screenGraphics;

private readonly CancellationTokenSource cts = new CancellationTokenSource();

public Form1()
{
    InitializeComponent();

    _screen = new PictureBox
    {
        ClientSize = this.ClientSize
    };
    _screenGraphics = _screen.CreateGraphics();

    _bitmap = new Bitmap(_screen.ClientSize.Width, _screen.ClientSize.Height);
    _bitmapGraphics = Graphics.FromImage(_bitmap);

    _noise = new int[_screen.ClientSize.Width * _screen.ClientSize.Height];

    Controls.Add(_screen);

    this.Load += Form1_Load;
    this.FormClosing += Form1_FormClosing;
}

private void Form1_FormClosing(object? sender, FormClosingEventArgs e)
{
    cts.Cancel();
}

private void SetNoise()
{
    for (int i = 0; i < _noise.Length; i++)
    {
        int val = _rnd.Next(0, 2);

        _noise[i] = val == 1 ? _whiteArg : _blackArg;
    }

    var bitmapData = _bitmap.LockBits(new Rectangle(0, 0, _bitmap.Width, _bitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

    var ptr = bitmapData.Scan0;

    System.Runtime.InteropServices.Marshal.Copy(_noise, 0, ptr, _noise.Length);

    _bitmap.UnlockBits(bitmapData);
}

private void Form1_Load(object? sender, EventArgs e)
{
    Task.Factory.StartNew(() =>
    {
        var ct = cts.Token;

        var font = new Font("Times New Roman", 32, FontStyle.Regular, GraphicsUnit.Pixel);

        var sw = new Stopwatch();

        int frames = 0;
        int lastFps = 0;
        long timeSpentInMs = 0;

        while (!ct.IsCancellationRequested)
        {
            sw.Start();

            SetNoise();

            sw.Stop();

            frames++;
            timeSpentInMs += sw.ElapsedMilliseconds;

            _bitmapGraphics.DrawString(lastFps.ToString(), font, Brushes.Red, 0, 0);
            _screenGraphics.DrawImage(_bitmap, new Point(0, 0));

            // a second has elapsed
            if (timeSpentInMs - 1_000 >= 0)
            {
                lastFps = frames;

                frames = 0;
                timeSpentInMs -= 1_000;
                sw.Reset();
            }
        }

    }, TaskCreationOptions.LongRunning);
}

}

\$\endgroup\$
4
  • 5
    \$\begingroup\$ I think the easiest improvement for now is to generate a noise texture once, in a larger resolution than your window (say double), and draw it at a random point calculated so that it covers the whole window. It's a technique I've used that doesn't require you to get into shaders or other low level graphics stuff, and it's pretty convincing. \$\endgroup\$
    – Devsman
    Commented Nov 20, 2023 at 17:14
  • 5
    \$\begingroup\$ One thing I'd read elsewhere is: if your CPU is doing anything per-pixel, then you need to redesign it to not do that. \$\endgroup\$ Commented Nov 20, 2023 at 20:21
  • 3
    \$\begingroup\$ Not an answer to this specific question, but if you want to get into games development with C#, considering looking into Godot and Unity, both of which are full-fledged game engines and allow you to write scripts in C#. \$\endgroup\$
    – Dan Bryant
    Commented Nov 20, 2023 at 21:08
  • 3
    \$\begingroup\$ .Net is a good choice, but GDI really isn't. It's a pity that Microsoft killed XNA. You might find SDL useful : jsayers.dev/category/c-sdl-tutorial-series \$\endgroup\$
    – pjc50
    Commented Nov 21, 2023 at 9:33

3 Answers 3

41
\$\begingroup\$

TLDR: Get DirectBitmap from https://stackoverflow.com/a/34801225 and add

        public void SetPixel(int x, int y, int argb)
        {
            int index = x + (y * Width);

            Bits[index] = argb;
        }

then change GetStaticNoise to

        readonly static int white = Color.White.ToArgb();
        readonly static int black = Color.Black.ToArgb();
        readonly static Random NoiseRnd = new Random();
        private static DirectBitmap GetStaticNoise(int width, int height)
        {
            var bitmap = new DirectBitmap(width, height);
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    var rnd = NoiseRnd.Next(0, 2);

                    bitmap.SetPixel(x, y, rnd == 0 ? white : black);
                }
            }

            return bitmap;
        }

Long story: I have done stupid things in the past and know how to optimize your code

To start with, there is no need to guess when you can just ask code profiler to tell why your code is slow :)

Roughly 96% of CPU time is spent in GetStaticNoise in which major culprit is, unsurprisingly, SetPixel. code profile of GetStaticNoise

Before dealing with big fat pixel setting, lets take a look at other lines with surprisingly high CPU time.

  • bitmap.Width and bitmap.Height does bunch of stuff under the hood.
  • preset colors isn't a simple integer, but a fat object.
  • Random.Shared creates Random object for each call. It is also thread-safe, which means there will be some performance cost hidden inside
  • Ternary conditional operator generates different bytecode than full IF, and can be faster for conditional assignments

Few seconds of refactor later you get about +10% performance for "free". itsy bitsy refactor

You could optimize further by reusing Bitmap, but for that size would have to be either constant or hook into OnSizeChanged event.

Knowing that SetPixel is a performance hog, you can just search for "c# fast SetPixel" and just copy&paste ready to use solution from another SE post. Make sure to read through the comments on why it is not a perfect solution.

Lo and behold, performance increased tenfold just by not using SetPixel unnecessary safety logic.

But that ain't the end, let's run profiler again to get reminded that Color object sucks for game development.

ugh this damn Color object...

With color already being prefetched, we can preconvert it as well and suddenly code is so fast that property getter becomes performance hog, taking whole 5%! surprising performance hog

Chopping properties away you can reach spot where Random numbers takes longer to generate than SetPixel. diminishing returns

But at this point we have pretty much reached limits of software rendering on windows desktop. For faster performance you'll need to use GPU.

To clarify on using Ternary Operator as performance optimization: Two-element array is twice as fast as full If statement, and Ternary Operator is ~30% faster than array. Original code used modulo 2 to check parity, but with Random already returning only 0 or 1 it can be used without modulo for small performance gain. conditional assignments comparison

But if you reorder IF statements slightly, you can squeeze out a bit more performance: reordered IF's Have in mind that (ab)using array indexes as logic (pointers) should work better in native code, without all the fancy memory management of .NET. Or it might not, depending on performance tricks done by CPU's themselves. Micro optimizations are tricky.

However, if your loop is tight enough and dataset fits into cpu cache, results will become Damn you CPU loop unrolling!

In the end, the real winner is white*0, but... that won't work in opaque ARGB colors, where A is 255.

TLDR: microoptimizations are hard and CPU doing its own tricks makes it even harder :)

\$\endgroup\$
10
  • 19
    \$\begingroup\$ Upvoted for mentioning profiling. This is the reliable way to go. \$\endgroup\$
    – eff
    Commented Nov 20, 2023 at 12:44
  • 1
    \$\begingroup\$ A very easy way to improve the performance of the random is to get the full int and then use each bit of the random instead of reducing it to just a single bit of signal. Probably gets better randomness too. \$\endgroup\$ Commented Nov 20, 2023 at 13:09
  • \$\begingroup\$ If you're concerned about the if and propose a ternary operator instead, the even more efficient solution is to put white and black in an array of length 2, and index that array with rnd %2. No conditional code at all, and every modern CPU can efficiently index arrays. \$\endgroup\$
    – MSalters
    Commented Nov 20, 2023 at 14:28
  • 2
    \$\begingroup\$ @MSalters yes but no but yes :) It should be faster, but .NET does lots of silly things on array indexers, but if loop is tight enough CPU does silly things with loop... In general, it is safe to suggest ternary operator but array is... complicated. At least in .NET. \$\endgroup\$
    – PTwr
    Commented Nov 20, 2023 at 15:18
  • 1
    \$\begingroup\$ This is a really excellent post because it doesn't just answer the question, it actually explains the process of performance analysis and remediation of code in clear detail. Wonderful! If I had enough reputation on this forum I would gladly award it a bonus. \$\endgroup\$ Commented Nov 22, 2023 at 15:11
17
\$\begingroup\$

When things bog down, check your inner loop. That's the code being run the most often, so even a small inefficiency there stacks up. Here we see:

Color color = Color.Black;

var rnd = Random.Shared.Next(0, 2);
if (rnd % 2 == 0)
{
    color = Color.White;
}

bitmap.SetPixel(x, y, color);

Two top suspects:

  • Random.Shared.Next(0, 2) - that "Shared" means this object is shared between threads, so to protect it from bugs when two threads try to generate a random number at the same time, there's probably a mutex (mutual exclusion) lock guarding it. That means for every single pixel, this code is acquiring and releasing a lock. That's relatively inexpensive when there are no other threads contesting it, but we have to check every time.

    Make your own local instance of Random outside the loop and use that to avoid this overhead.

    This is also using a heavier-duty random algorithm than you need (probably a Mersenne Twister). For a basic static effect like this, you could probably get away with a simple Xorshift or similar PRNG that you can run entirely out of L1 cache.

  • bitmap.SetPixel(x, y, color) - setting pixels one by one is often very slow, as we need to make sure the copy of the bitmap we're writing to isn't currently being used, take exclusive control of it, set one tiny bit of memory, then potentially re-upload the changed version to the GPU for rendering. (I don't know exactly what steps GDI uses though)

    Game APIs will often expose a way to set the whole bitmap at once, which is much more efficient. We'll set our colour bits in our own buffer or array, then only at the end of the loop do we ask the bitmap to replace its entire contents with that array.

    I'm not deeply familiar with GDI, but it looks like you can call Bitmap.LockBits to get access to the bitmap's BitmapData buffer before the loop, write all your data into that without extra overhead, then unlock it once at the end. (Thanks to Basic for suggesting a better route than I'd initially found!)

In a typical game context, we'd usually get this work off the CPU entirely and do noise generation on the GPU in a shader. That way we can process many pixels in parallel, rather than one at a time in sequence like CPU code. So you may want to consider switching from Windows Forms to a tech stack that makes it easier to offload graphics work this way. Engines like Godot and Unity both support C#, while giving you a richer toolbox of features that are helpful for game development.

\$\endgroup\$
6
  • 2
    \$\begingroup\$ Related: davidthomasbernal.com/blog/2008/03/13/… \$\endgroup\$
    – Basic
    Commented Nov 19, 2023 at 13:25
  • 5
    \$\begingroup\$ Having used GDI a bit, I can confirm that bitmap.SetPixel(..) is painfully slow. I think I essentially used a double buffer approach instead (maybe LockBits wasn't available at the time?) but at any rate SetPixel(..) was generally too slow for most any real time use & based on current documentation it doesn't seem like that's changed. \$\endgroup\$
    – Pikalek
    Commented Nov 19, 2023 at 17:06
  • 1
    \$\begingroup\$ also, no need to allocate a new bitmap for every frame (suppose the bitmap is used like a framebuffer here, then clearing it would be sufficient, too) I'm surprised that ~40 fps are even possible this way. Windows Forms has a DoubleBuffered property. Setting that may impact the blitting speed as well. \$\endgroup\$ Commented Nov 19, 2023 at 19:37
  • 3
    \$\begingroup\$ Oops, I didn't even spot the recreation of the bitmap each frame. I think your comment is well worth posting as its own answer — I'd upvote it. \$\endgroup\$
    – DMGregory
    Commented Nov 19, 2023 at 20:54
  • \$\begingroup\$ If you know you generate a random number that is either 0 or 1, it is wasteful to check for it being 0 by using the remainder operator. Hopefully compiler would optimize it to at least rnd & 1 == 0 check, but really, just do if (rnd == 0). \$\endgroup\$
    – n0rd
    Commented Nov 20, 2023 at 18:36
3
\$\begingroup\$

I wonder, do you really need to render noise that is uniquely random down to the pixel?

Or would it be okay if it just appears random?

If you're alright with an approach that relies on staggered texture tiling instead of C# noise generation, please read on.

You may already be familiar with the cicada principle. The idea is that layering tiled images that repeat in prime number intervals makes the tiling less noticeable to the eye. This works for non-noise tiled image textures too, but is even less noticeable in the case of noise. Using transparency is one way to accomplish this. Another is blend modes.

A demo of the concept can even be constructed with a few still images and plain CSS.

359px

257px

193px

/* general appearance for demo */
    body {
      padding: 20px;
      background-color: #222;
    }
    .demo-cont {
      display: flex;
      justify-content: center;
    }
    .div-size {
      width: 50%;
      height: 360px;
      border-radius: 6px;
    }

    /* noise effect from here */
    .color-noise {
      background-repeat: repeat;
      background-position: center;
      background-blend-mode: hue;
      background-image:
        /* each image has a different prime number resolution */
        url(https://i.sstatic.net/ZWudq.png), /* 359px tile */
        url(https://i.sstatic.net/HiXuV.png), /* 257px tile */
        url(https://i.sstatic.net/3kPSN.png); /* 193px tile */
      background-size: 
        /* CSS screen pixels are double-size, so halve that res to get the true resolution. 
        Optionally, scale to fine-tune, or just create a more varied anti-aliased look. */
        calc(359px * 0.5),
        calc(257px * 0.55),
        calc(193px * 0.67);
    }

    @keyframes filter-anim {
      /* Rotating hue and over-saturating color "moves" the perceived brightness around.
      This remains visible in grayscale. */
      0%   {filter: hue-rotate(0deg)   saturate(4) grayscale(1);}
      100% {filter: hue-rotate(360deg) saturate(4) grayscale(1);}
    }

    .color-noise.animate {
      animation: filter-anim 0.5s infinite linear;
    }
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Cicada Noise</title>
</head>
<body>
  <div class="demo-cont">
    <div class="color-noise animate div-size"></div>
  </div>
</body>
</html>

I don't know if this approach will be suitable for your project, but it's a "think outside the box" sort of technique that may be more performant than traditional generated noise. If anyone tests this out, I would be curious to hear how it performs in your game. Cheers.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Nice trick (tho that example looks more like fluid than noise), but in C#/WinForms it will also suck because there is no hardware accelerated Hue rotation function available, so you'd end up with same performance issues as with SetPixel Winforms are just not meant for games :) \$\endgroup\$
    – PTwr
    Commented Nov 22, 2023 at 16:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .