kekyo / SkiaImageView

A control for easy way showing SkiaSharp-based image objects onto WPF/XF/.NET MAUI/Avalonia applications.
Apache License 2.0
32 stars 3 forks source link

Garbage collector performance issue #5

Open AWAS666 opened 2 months ago

AWAS666 commented 2 months ago

Hey, using this in avalonia 11 and works pretty good out of the box, only minor issue I have that I do swap my bound image at like 30-60 fps and this makes it call the gc to dispose the old image alot.

Is there any way to circumvent this?

kekyo commented 2 months ago

There is unfortunately no direct way to do this in the current version. By the way, what format of object are you using to bind?

For example, if you are binding an SKBitmap, and instead of setting another instance of the SKBitmap, do you expect to directly rewrite the bitmap bits inside and reflect that, or is that the method you are using or planning?

AWAS666 commented 2 months ago

Well I'm currently generating SKimages in a background thread which calls an eventhandler which then replaces the bound source in the view model.

Not sure if there is a cleaner solution for it tho, I had been struggling with a variety of issues, currently it does sorta depend on the view model to dispose old frames...

Been tinkering with my own control but that still has a ton of other issues as I render directly to the canvas which makes stuff like double click events not work (and it flickers sometimes...)

kekyo commented 2 months ago

For example, what if there was a method like SKImageView.ForceUpdate() and you called it, and it re-evaluated the image instance regardless of the binding trigger?

(I just thought of this, so I haven't thought deeply about whether this method is appropriate or not.)

AWAS666 commented 2 months ago

I mean it doesn't sound like the cleanest solution there is, but it should work for my use case.

kekyo commented 2 months ago

Don't expect anything. If I get some time, I'll try to implement it :)

AWAS666 commented 3 weeks ago

This is my own implementation now which is based on yours. I only need avalonia, so I stripped it to a minimum.

Basically the issue is that a new "WriteableBitmap" gets created with each new Source, that causes the garbage collector to go mad. This now keeps a single one.

Maybe you can also implement it on your end, but that might be quite some work...

Treat this as a minimal solution/example.

Ofc you still gotta manage disposal of old instances in the code that creates the bitmaps.

My code: ` public class SKImageViewer : UserControl {

 public static readonly StyledProperty<SKBitmap> SourceProperty =
     AvaloniaProperty.Register<SKImageViewer, SKBitmap>(nameof(Source));

 static SKImageViewer()
 {
     AffectsRender<SKImageViewer>(SourceProperty);
 }

 public SKImageViewer()
 {
     ClipToBounds = true;

     SourceProperty.Changed.Subscribe(new AnonymousObserver<AvaloniaPropertyChangedEventArgs<SKBitmap>>(
        e =>
        {
            base.InvalidateMeasure();
            base.InvalidateVisual();
        })
     );
 }

 private Size RenderSize =>
   this.Bounds.Size;
 private WriteableBitmap writableBitmap;

 protected override Size MeasureOverride(Size constraint) =>
    this.InternalMeasureArrangeOverride(constraint);

 protected override Size ArrangeOverride(Size arrangeSize) =>
     this.InternalMeasureArrangeOverride(arrangeSize);

 private Size InternalMeasureArrangeOverride(Size targetSize)
 {
     if (Source != null)
     {
         var self = new Size(Source.Width, Source.Height);
         var scaleFactor = ComputeScaleFactor(
             targetSize,
             self)
           ;
         return new(
            self.Width * scaleFactor.Width,
            self.Height * scaleFactor.Height);
     }
     else
     {
         return default;
     }
 }

 public SKBitmap Source
 {
     get => GetValue(SourceProperty);
     set => SetValue(SourceProperty, value);
 }

 public override void Render(DrawingContext drawingContext)
 {
     base.Render(drawingContext);
     if (Source == null) return;

     int width = Source.Width;
     int height = Source.Height;

     var info = new SKImageInfo(
         width, height, SKImageInfo.PlatformColorType, SKAlphaType.Premul);

     writableBitmap = writableBitmap ?? new WriteableBitmap(
          new(info.Width, info.Height), new(96.0, 96.0), PixelFormat.Bgra8888, AlphaFormat.Premul);
     using var locker = writableBitmap.Lock();
     using var surface = SKSurface.Create(info, locker.Address, locker.RowBytes);
     surface.Canvas.Clear();
     surface.Canvas.DrawBitmap(Source, default(SKPoint));
     drawingContext.DrawImage(writableBitmap, new(new(), this.RenderSize));
 }

 private Size ComputeScaleFactor(Size availableSize, Size contentSize)
 {
     // Compute scaling factors to use for axes
     double scaleX = 1.0;
     double scaleY = 1.0;

     // Compute scaling factors for both axes
     scaleX = availableSize.Width / contentSize.Width;
     scaleY = availableSize.Height / contentSize.Height;

     //Find maximum scale that we use for both axes
     double minscale = scaleX < scaleY ? scaleX : scaleY;
     scaleX = scaleY = minscale;

     //Return this as a size now
     return new Size(scaleX, scaleY);
 }

} `

edit: added clear

kekyo commented 3 weeks ago

Thanks, I will refer your code!