oxyplot-avalonia
oxyplot-avalonia copied to clipboard
[wip] implement SkiaSharp PlotViews
This adds two new PlotViews that can be used as a direct replacement of the existing OxyPlot.Avalonia.PlotView:
OxyPlot.SkiaSharp.Avalonia.PlotViewOxyPlot.SkiaSharp.Avalonia.DoubleBuffered.PlotView
Both of the new PlotViews use OxyPlot.SkiaSharp for rendering and only work when SkiaSharp is used as the rendering backend for Avalonia. As far as I understand, this is the case most of the time (but not always).
OxyPlot.SkiaSharp.Avalonia.PlotView overrides Avalonias Visual.Render method, leases a SkCanvas from Avalonia in a ICustomDrawOperation and uses this to render the plot. In terms of performance in comparison to CanvasRenderContext, this is a mixed bag; some examples are significantly faster, others quite a bit slower - at least that is the case on my machine.
Similar to OxyPlot.Avalonia.PlotView, updating and rendering always takes place on the UI thread. So probably in most cases there is not much incentive to switch from OxyPlot.Avalonia.PlotView to OxyPlot.SkiaSharp.Avalonia.PlotView, it's rather just an alternative solution that has some advantages and some drawbacks depending on the specific application.
Much more interesting is OxyPlot.SkiaSharp.Avalonia.DoubleBuffered.PlotView: This uses OxyPlot.SkiaSharp to render the plot into an in-memory Bitmap in a double-buffer configuration. This has the nice implication that rendering and updating can be done completely on a background thread; the UI thread only needs to draw the finished bitmap on the screen. This can help a lot with keeping the UI responsive even when rendering very complicated plots.
This PR is not quite finished, there are still some things I need to fix, improve and verify. But at least the ExampleBrowser is fully functional and allows switching between the three PlotViews.
I am not quite sure how much time I will have to work on this for the next couple of weeks, but I will definitely pick this up afterwards and finish it up. In the meantime, I thought I'd open this PR so people who are interested can play around with it. Feedback would be very welcome, espcially since I am fairly new to Avalonia (coming from WPF).
Latest commits make the render loop of the double buffered renderer async. I'm not a huge fan of the wait-loop to spin down the SemaphoreSlim, but I think this is the only way in the absence of an async AutoResetEvent. Overall I think this is an improvement.
Also reviewed the usage of the dirty flags, should make more sense now.
There is still some work to do, to be continued.
Quick question. Why won't you use SKPictureRecorder and then wrap the resulting SKPicture into ICustomDrawOperation instead of rasterizing into SKBitmap? Is the bottleneck the rasterization itself?
Summing up my understandig of this topic, please correct if I am wrong:
-
SKPictureRecorderwould allow us to offload the 'conversion' of aPlotModelto drawing commands (i.e.IPlotModel.Render) to a background thread. The rasterization of these drawing commands would be issued by Avalonia, and happen on the UI thread. -
The double-buffer approach allows to offload both the generation of drawing commands and rasterization on a background thread, the UI thread only needs to draw a bitmap.
I have not tried this, but I assume that rasterization might be fairly expensive as well, of course depending on the plot. But I have not actually evaluated how the two compare in terms of performance, I will look into it when I find time. Thanks for the suggestion, I had not thought of this before!
I'll try to take another look at this next weekend
Avalonia does not do any drawing on the UI thread. The DrawingContext you get in the Render method simply records drawing commands to be replayed on the render thread using a GPU-accelerated graphics context.
Ah I see, that sounds great! I will definitely give this a try. I think I might implement this approach as a third PlotView variant and than we can evaluate which ones are redundant and which are worth keeping.
Finally found some time to give the SKPictureRecorder appoach a shot.
The implementation is not quite finished, the main concern is that SKPictures are not properly disposed of, some thought would have to go into how to do that.
However it should work mostly fine and can be played around with in the ExampleBrowser.
Here is my summary of the three approaches, as far as I am concerned:
-
SkiaSharpRenders directly to aSKCanvasGood: Simple approach, no unnecessary memory overhead Bad: Blocks UI while rendering complexPlotModels -
SkiaSharp.DoubleBufferedRenders to an in-memory bitmap on a background thread Good: Most of the work (plot render + update) happens on a background thread, the render thread does minimal work. Does not block UI even when rendering very complexPlotModels Bad: Somewhat more complex, extra memory usage for double buffer bitmaps, proper locking ofPlotModelon user side is crucial -
SkiaSharp.PictureRecorderRecords the drawing operations necessary to render a PlotModel in a SKPicture and uses this to draw the plot Good: Plot update can happen on a background thread. Rendering may take advantage of GPU acceleration (though I didn't notice much difference during my testing) Bad: Blocks UI while rendering complex PlotModels (I guess while the render thread is busy rendering the plot, it just can't render anything else). Extra memory usage for recorded rendering operations, proper locking ofPlotModelon user side is crucial
For me the most compelling point of the double-buffer approach is that the UI remains responsive even when rendering very complex plots. Unfortunately, this advantage seems to be lost with the PictureRecorder approach. While this definitely also has its advantages, I am not sure if this is worth the extra complexity...
@Jonarw my 2 cents here: I've used SkPicture and its recorder to draw pdf document pages in an avalonia app: https://github.com/BobLd/Caly. The task is very similar to what you are trying to achieve here.
Regarding SkPicture dispose problems, you might want to look into Avalonia's Ref class here https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Base/Utilities/Ref.cs This is what I used and will help you count the references.
My app is still in early development stage so do not take my approach as 100% perfect.
@Jonarw is this ready for another review? (should have time later today if so)