Skip to content

Use WriteableBitmap to improve performance of WPF rendering#1388

Merged
swharden merged 6 commits intoScottPlot:masterfrom
jbuckmccready:master
Oct 22, 2021
Merged

Use WriteableBitmap to improve performance of WPF rendering#1388
swharden merged 6 commits intoScottPlot:masterfrom
jbuckmccready:master

Conversation

@jbuckmccready
Copy link
Contributor

@jbuckmccready jbuckmccready commented Oct 20, 2021

Added use of a WriteableBitmap to avoid unneeded allocations and rebinding the image element source when the backend bitmap is only updated (not changed)

Purpose:
Improves performance of WPF rendering and fixes #1387.

- Added use of a WriteableBitmap to avoid unneeded allocations and
  rebinding the image element source when the backend bitmap is only
  updated (not changed)
Copy link
Member

@bclehmann bclehmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this is the more preferred version of doing something like this.

It mostly looks good, just a little spooked by some of the error handling code.

- Locking is not required to call WritePixels (locking is done by the call)
- Fixed typo in comment
@jbuckmccready
Copy link
Contributor Author

OK I cleaned it up - feel free to squash the commits before merge or let me know if you want me to.

@bclehmann
Copy link
Member

OK I cleaned it up - feel free to squash the commits before merge or let me know if you want me to.

We don't normally squash PRs into one commit, so I think that's ok.

@bclehmann
Copy link
Member

bclehmann commented Oct 20, 2021

Also, it's worth noting that this enhancement could likely be made to WinForms and Avalonia. Non-Windows platforms don't have System.Windows.Media.Imaging.WritableBitmap, but Avalonia seems to offer an alternative.

If you're interested that could become part of this or a later PR, or we can open an issue for someone to do that later.

@swharden
Copy link
Member

swharden commented Oct 20, 2021

Non-Windows platforms don't have System.Windows.Media.Imaging.WritableBitmap, but Avalonia seems to offer an alternative.

I don't want to reinvent the wheel here... but is this something we could implement ourselves? The BMP file format isn't that bad, and an obligate writer could be more straightforward to implement than a reader that would have to support multiple/nuanced file structures and header types.

Resources:

If you're interested that could become part of this or a later PR, or we can open an issue for someone to do that later.

I'm definitely interested in exploring this idea for the other two controls! A separate PR/issue sounds like a good idea because I see the warning signs of a rabbit hole 😉

@jbuckmccready
Copy link
Contributor Author

jbuckmccready commented Oct 21, 2021

I didn't want to affect the backend API since my issue was only on WPF and the changes I envision to the backend are quite a bit more work. The architecture could be changed to optimize things quite a bit even with the inefficiencies that come with blitting bitmaps rather than a pipeline utilizing the GPU to rasterize directly to a frame (the nice thing about blitting bitmaps is the API is simple and highly portable, so I'm not arguing against the way it's done now since it has advantages).

Right now the backend does many large memory allocations (the bitmap buffers are very large when scaled up on a high resolution screen) during real time interaction which puts a lot of pressure on the garbage collector. It should be possible to reuse all the bitmap buffers involved until a resize event occurs and the bitmap buffer must be increased or decreased (although even in this case it's possible to only resize if the bitmap is made larger and just use part of the buffer at the cost of retaining more memory when things are made smaller).

There are a few common architectures for this type of things but I like the idea of a double swap type of architecture so the consumer is never blocked waiting for the renderer to create a frame and no allocations or copies are made.

The backend renderer has two bitmap buffers (one for actively writing to called A, one for swapping with called B), the consumer has one bitmap buffer it uses (called C). The backend renderer writes to A to render then locks and swaps with B then notifies the consumer of a new frame. When the consumer receives the notification it then locks and swaps C with B. The idea here is that the only time the consumer or renderer is blocked by a lock is for a simple pointer swap. No copies or memory allocations occur through this whole process unless a resize occurs (or is required by the consuming platform UI API).

Resize events are handled by having the renderer always look at A's size before writing to it and allocating a new buffer for A if required, everything else proceeds the same (as the renderer writes to each buffer they will each be resized as needed).

@swharden swharden mentioned this pull request Oct 21, 2021
82 tasks
@swharden
Copy link
Member

I'm going to test this now and hopefully merge it in tonight... Thanks @jbuckmccready for this PR, and @bclehmann for your input! 🚀

@swharden: but is this something we could implement ourselves?

@jbuckmccready: I didn't want to affect the backend API

I should have clarified #1388 (comment) was directed toward @bclehmann, and implementing an in-memory bitmap writer could be a goal for another issue/PR. This PR is looking great, and it certainly makes sense to use System.Windows.Media.Imaging.WritableBitmap for the WPF control 👍

utilizing the GPU to rasterize directly to a frame

I'm inclined away from this because I want to minimize the dependencies (software and hardware) this project takes on. If we went into GPU land there would be some interesting mathematical operations we could optimize too. I don't intend to go there any time soon (beyond Skia/OpenGL support for hardware-accelerated 2D drawing, #1036).

It should be possible to reuse all the bitmap buffers involved until a resize event occurs and the bitmap buffer must be increased or decreased

ScottPlot actually does this under the hood (re-using a System.Drawing.Bitmap and drawing on it, creating a new one only when resized).

double swap type of architecture so the consumer is never blocked

Double-buffering is an excellent concept to consider! I added it to the triage list #1028 to revisit in the future. I did some early experiments with this long ago, but I think it's definitely worth reconsidering in the future.

Note that the primary System.Drawing.Bitmap that is re-used by ScottPlot is displayed as an Image in a PictureBox in the Windows Forms control. The PictureBox Image is double-buffered out of the box, allowing the WinForms control to update that image in some situations without blocking the UI thread.

@jbuckmccready
Copy link
Contributor Author

jbuckmccready commented Oct 22, 2021

ScottPlot actually does this under the hood (re-using a System.Drawing.Bitmap and drawing on it, creating a new one only when resized).

Ah looking at it again I misread the code involving the OldBitmaps queue in the backend, the bitmap buffer allocations while panning were only happening on the WPF side of things and are always the same size until a resize so probably not a big deal. The weird hang while panning seems tied to the way the WPF Image element updates when the Source property changes.

I'm inclined away from this because I want to minimize the dependencies (software and hardware) this project takes on. If we went into GPU land there would be some interesting mathematical operations we could optimize too. I don't intend to go there any time soon (beyond Skia/OpenGL support for hardware-accelerated 2D drawing, #1036).

Thanks for the context. The Microsoft.Maui.Graphics progression looks awesome.

@swharden
Copy link
Member

This PR made me take a second look at this function:

private static BitmapImage BmpImageFromBmp(System.Drawing.Bitmap bmp)
{
using var memory = new System.IO.MemoryStream();
bmp.Save(memory, System.Drawing.Imaging.ImageFormat.Png);
memory.Position = 0;
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
return bitmapImage;
}

I wonder what advantage/disadvantage there is encoding as Png vs Bmp? I'm guessing Png would require less space in memory, but perhaps Bmp would be faster requiring less processing to encode/decode? It may be worth benchmarking both ways to see if one is meaningfully favorable.

@swharden swharden merged commit 27ce5dd into ScottPlot:master Oct 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WPF Plot Stops Refreshing Until Mouse Panning Stops

3 participants