1

I'm building a video recorder type of app, and the method GetData() from Texture2D is just too slow. It takes about 50% of CPU usage time, which means that the game is going to have fps drops, between 30-40, instead of 60. Here´s the current code:

byte[] stream = new byte[textW * textH * 4];
try
{
    texture.GetData(stream); // Very slow !!!
} catch (Exception e)
{
    Console.WriteLine(e.Message);
}
var bmp1 = new Bitmap(textW, textH);
BitmapData data = bmp1.LockBits(new Rectangle(0, 0, textW, textH), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
IntPtr ptr = data.Scan0;

Marshal.Copy(stream, 0, ptr, textW * textH * 4);
unsafe
{
     byte* ptrFirstPixel = (byte*)data.Scan0;
     int bytesPerPixel = Bitmap.GetPixelFormatSize(data.PixelFormat) / 8;
     int heightInPixxel = data.Height;
     int widthInBytes = data.Width * bytesPerPixel;

     Parallel.For(0, heightInPixxel, y =>
     {
          byte* currLine = ptrFirstPixel + (y * data.Stride);
          for (int x = 0; x < widthInBytes; x += bytesPerPixel)
          {
               int b = currLine[x];
               int r = currLine[x + 2];
               currLine[x + 2] = (byte)b;
               currLine[x] = (byte)r;
          }
     });
}
bmp1.UnlockBits(data);

bmp1.Save(outputPath, ImageFormat.Png);
bmp1.Dispose();
texture.Dispose();
Jason Aller
  • 3,541
  • 28
  • 38
  • 38
rtiago97
  • 29
  • 4
  • 3
    Transferring data from GPU memory to main memory (which is what happens in your `GetData` call) is an expensive operation in general. – JosephDaSilva Jul 26 '21 at 17:23
  • Is "SaveAsPng" an option? It may be faster than using `GetData()` (I haven't tested this) and you can give it a stream to write to. In your code, you save as png aswell, so it may help. – Max Play Jul 26 '21 at 18:16
  • An app that saves frames of video memory is not a "video recorder". – Ian Kemp Jul 26 '21 at 18:37
  • @MaxPlay I looked into the source, it appears `SaveAsPng` just another abstraction layer over `GetData<>` unfortunately. – DekuDesu Jul 26 '21 at 19:36

3 Answers3

1

Is there a more efficient way of creating a bitmap from a Texture2D in Monogame C#?

For reasons stated below, I do not believe there is any faster way to retrieve or save a png any faster using MonoGame's implementation of Texture2D. However, that being said, I believe you still have options available to you.

You may want to consider accessing the framebuffer directly using OpenGL and saving it manually to avoid the high level of abstraction that MonoGame uses. This has it's own drawbacks especially regarding implementation, but it may be the fastest solution for you.

But keep in mind regardless of implementation you should consider avoiding dumping the frame buffer into ram every frame, that has inherent performance issues associated with it.

You may want to consider other methods such as using Direct3d11 instead or the previously deprecated DirectX9, but I didn't do look to far into it since its outside the scope of the question.

the method GetData() from Texture2D is just too slow

If you're interested in the nitty gritty you can find their implementation of Texture2D over on github.

In summary their implementation is slow because forced to run on the mainthread to to dump the frame buffer, in addition to the multiple layers of abstraction over their low level† OpenGL calls.††

The path for your use case would be
public void GetData<T> (T[] data) where T : struct Texture 2D.cs 264

† This is used relatively. All of these layers are very remote from traditional low level calls.
†† This is speculation, based on reviewing the source and my own experience, I am neither a creator, or contributor to Monogame or their repositories

DekuDesu
  • 2,224
  • 1
  • 5
  • 19
0

Processing each pixel and then encoding the result as a png is a slow process.

The Parallel.for speeds up the initial conversion, but the entire thread has to wait until the png conversion and I/O write is complete.

It would be faster to allocate many fixed buffers, and spawn new threads to write those buffers to disk.

Since a harddrive or SSD is magnitudes slower than ram, these buffers will fill up. You have two options to overcome this limitation:

  1. Reduce the frame rate encode every other, or third, frame. OR...
  2. Open a file handle to a large output file (with the appropriate AVI header), Write the raw bitmap data to the file. Writing to a large file is magnitudes faster than writing many small files.
-1

I have a couple of issues with your statements:

It takes about 50% of cpu usage time

What specifically takes 50% of "cpu usage time"? Is it the texture transfer? The Png encoding? The extremely inefficient Parallel.For for a straight memory copy? The even more inefficient GDI+ Bitmap allocation and disposing every single frame?

My guess is all of it, and that reading the frame data is the smallest part of it. Of course it's all a guess since you don't show any profiling data, just some vague assumptions. But a better understanding of C#, memory (when have you ever seen a multi-threaded memcpy?) and GDI+ (at least enough to realize when not to use it, and especially not like that) would help you far more than making the frame transfer more efficient.

That said, the abstraction layer you're using over OpenGL is hiding the most efficient solution from you: pixel buffer objects used for temporary transfers in round-robin format, as many as needed to not force a CPU sync.

Blindy
  • 65,249
  • 10
  • 91
  • 131