AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.23k stars 2.18k forks source link

The app crashes when we resize it while it renders an image #8515

Closed aboccag closed 1 month ago

aboccag commented 2 years ago

Describe the bug I am acquiering images with a camera very fast and I'm displaying them on the UI using an Observable. During the aquisition i'm resizing the application and it crashes. I guess its because I am resizing the application while it renders the image.

Exception

System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Media.Imaging.Bitmap.get_Size() in /_/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs:line 134
   at Avalonia.Controls.Image.MeasureOverride(Size availableSize) in /_/src/Avalonia.Controls/Image.cs:line 105
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 559
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 364
   at Avalonia.Layout.LayoutHelper.MeasureChild(ILayoutable control, Size availableSize, Thickness padding) in /_/src/Avalonia.Layout/LayoutHelper.cs:line 46
   at Avalonia.Layout.LayoutHelper.MeasureChild(ILayoutable control, Size availableSize, Thickness padding, Thickness borderThickness) in /_/src/Avalonia.Layout/LayoutHelper.cs:line 39
   at Avalonia.Controls.Border.MeasureOverride(Size availableSize) in /_/src/Avalonia.Controls/Border.cs:line 187
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 559
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 364
   at Avalonia.Controls.Grid.MeasureOverride(Size constraint) in /_/src/Avalonia.Controls/Grid.cs:line 230
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 559
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 364
   at Avalonia.Controls.Grid.MeasureCell(Int32 cell, Boolean forceInfinityV) in /_/src/Avalonia.Controls/Grid.cs:line 1150
   at Avalonia.Controls.Grid.MeasureCellsGroup(Int32 cellsHead, Size referenceSize, Boolean ignoreDesiredSizeU, Boolean forceInfinityV, Boolean& hasDesiredSizeUChanged) in /_/src/Avalonia.Controls/Grid.cs:line 1005
   at Avalonia.Controls.Grid.MeasureCellsGroup(Int32 cellsHead, Size referenceSize, Boolean ignoreDesiredSizeU, Boolean forceInfinityV) in /_/src/Avalonia.Controls/Grid.cs:line 968
   at Avalonia.Controls.Grid.MeasureOverride(Size constraint) in /_/src/Avalonia.Controls/Grid.cs:line 489
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 559
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 364
   at Avalonia.Controls.Grid.MeasureOverride(Size constraint) in /_/src/Avalonia.Controls/Grid.cs:line 230
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 559
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 364
   at Avalonia.Controls.Grid.MeasureCell(Int32 cell, Boolean forceInfinityV) in /_/src/Avalonia.Controls/Grid.cs:line 1150
   at Avalonia.Controls.Grid.MeasureCellsGroup(Int32 cellsHead, Size referenceSize, Boolean ignoreDesiredSizeU, Boolean forceInfinityV, Boolean& hasDesiredSizeUChanged) in /_/src/Avalonia.Controls/Grid.cs:line 1005
   at Avalonia.Controls.Grid.MeasureCellsGroup(Int32 cellsHead, Size referenceSize, Boolean ignoreDesiredSizeU, Boolean forceInfinityV) in /_/src/Avalonia.Controls/Grid.cs:line 968
   at Avalonia.Controls.Grid.MeasureOverride(Size constraint) in /_/src/Avalonia.Controls/Grid.cs:line 489
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 559
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 364
   at Avalonia.Layout.LayoutHelper.MeasureChild(ILayoutable control, Size availableSize, Thickness padding) in /_/src/Avalonia.Layout/LayoutHelper.cs:line 46
   at Avalonia.Layout.LayoutHelper.MeasureChild(ILayoutable control, Size availableSize, Thickness padding, Thickness borderThickness) in /_/src/Avalonia.Layout/LayoutHelper.cs:line 39
   at Avalonia.Controls.Presenters.ContentPresenter.MeasureOverride(Size availableSize) in /_/src/Avalonia.Controls/Presenters/ContentPresenter.cs:line 366
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 559
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 364
   at Avalonia.Layout.LayoutHelper.MeasureChild(ILayoutable control, Size availableSize, Thickness padding) in /_/src/Avalonia.Layout/LayoutHelper.cs:line 46
   at Avalonia.Controls.Decorator.MeasureOverride(Size availableSize) in /_/src/Avalonia.Controls/Decorator.cs:line 54
   at Avalonia.Controls.Primitives.VisualLayerManager.MeasureOverride(Size availableSize) in /_/src/Avalonia.Controls/Primitives/VisualLayerManager.cs:line 133
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 559
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 364
   at Avalonia.Layout.Layoutable.MeasureOverride(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 625
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 559
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 364
   at Avalonia.Layout.Layoutable.MeasureOverride(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 625
   at Avalonia.Controls.Window.MeasureOverride(Size availableSize) in /_/src/Avalonia.Controls/Window.cs:line 937
   at Avalonia.Controls.WindowBase.MeasureCore(Size availableSize) in /_/src/Avalonia.Controls/WindowBase.cs:line 247
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in /_/src/Avalonia.Layout/Layoutable.cs:line 364
   at Avalonia.Layout.LayoutManager.Measure(ILayoutable control) in /_/src/Avalonia.Layout/LayoutManager.cs:line 297
   at Avalonia.Layout.LayoutManager.ExecuteMeasurePass() in /_/src/Avalonia.Layout/LayoutManager.cs:line 261
   at Avalonia.Layout.LayoutManager.InnerLayoutPass() in /_/src/Avalonia.Layout/LayoutManager.cs:line 243
   at Avalonia.Layout.LayoutManager.ExecuteLayoutPass() in /_/src/Avalonia.Layout/LayoutManager.cs:line 145
   at Avalonia.Controls.WindowBase.HandleResized(Size clientSize, PlatformResizeReason reason) in /_/src/Avalonia.Controls/WindowBase.cs:line 225
   at Avalonia.Controls.Window.HandleResized(Size clientSize, PlatformResizeReason reason) in /_/src/Avalonia.Controls/Window.cs:line 1018
   at Avalonia.Win32.WindowImpl.AppWndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam) in /_/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs:line 399
   at Avalonia.Win32.WindowImpl.WndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam) in /_/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs:line 30
   at Avalonia.Win32.Interop.UnmanagedMethods.DefWindowProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam)
   at Avalonia.Win32.WindowImpl.AppWndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam) in /_/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs:line 539
   at Avalonia.Win32.WindowImpl.WndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam) in /_/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs:line 30

To Reproduce Steps to reproduce the behavior: 0 - Load an image and make sure you copy it at each call 1 - Create an observable of Avalonia.Media.Imaging.Bitmap, to simulate the camera, i'm using a Interval Observble ticking very fast (10 ms) and project the time to a copy of _mySkBitmap. The Observavle converts the SKBitmap to the WriteableBitmap

    private SKBitmap _mySkBitmap = SKBitmap.Decode(System.IO.File.ReadAllBytes("image_00028.bmp"));
    private SKBitmap MySkBitmap => _mySkBitmap.Copy();

    public IObservable<Bitmap?> DisplayInputImageObservable => Observable.Interval(TimeSpan.FromMilliseconds(10)).Select(_ => MySkBitmap)       
        .ObserveOn(Scheduler.Default)
        .Select(img =>
        {
            // bitmap is a SkiaSharp.SKBitmap
            using var skImage = SKImage.FromBitmap(img);
            using var data = skImage.Encode(SKEncodedImageFormat.Jpeg, 100);
            using var stream = data.AsStream();
            var writeableBitmap = WriteableBitmap.Decode(stream);
            img.Dispose();

            if (_prevRef != writeableBitmap && _prevRef is not null)
            {
                _prevRef.Dispose();
            }

            _prevRef = writeableBitmap;

            return writeableBitmap;
        })
        .Catch<Bitmap?, Exception>(e => { return Observable.Empty<Bitmap>(); });

2 - Connect it to the View

<Image Source="{Binding DisplayInputImageObservable^}"
       Name="MyImage"
       Stretch="Fill" />

Expected behavior The app should resize correcly without crashing when it renders an image

Screenshots

image

image

Desktop (please complete the following information):

maxkatz6 commented 2 years ago

.ObserveOn(Scheduler.Default)

That's dangerous. UI changes should be done only on UI thread. .ObserveOn(RxApp.MainThreadScheduler) should be used at least before Catch call.

Probably that's a reason of your problem as well. As Image.Source is updated from another thread.

aboccag commented 2 years ago

Thank you @maxkatz6

Unfortunately, I have the same issue with .ObserveOn(RxApp.MainThreadScheduler) as well as without any ObserveOn

Gillibald commented 2 years ago

Why are you recreating the Bitmap every frame? Try to just allocate one Bitmap and override its content. Your camera feed should have fixed dimensions.

aboccag commented 2 years ago

Hello @Gillibald and thank you for your help. I have not found any way to allocate the pixels to an existing Avalonia.Media.Imaging.Bitmap or Avalonia.Media.Imaging.WriteableBitmap. The only way I found is to creating a new object from a stream. Am I missing something ?

best regards

Gillibald commented 2 years ago
var bitmap = new WriteableBitmap(new PixelSize(100, 100), new Vector(96, 96), Avalonia.Platform.PixelFormat.Rgba8888, Avalonia.Platform.AlphaFormat.Premul);

using(var buffer = bitmap.Lock())
{
    buffer.Address //Write here
}
aboccag commented 2 years ago

Thank you @Gillibald, the Address pointer property is get only if i'm not wrong.

Gillibald commented 2 years ago

A memory address is just the start of a memory region. You have to write to that region. You know how many pixels you have and how much space a pixel takes.

aboccag commented 2 years ago

A memory address is just the start of a memory region. You have to write to that region. You know how many pixels you have and how much space a pixel takes.

Yes that's right! How would you then trigger the framework to redraw as it is the same referenced object? With this approach, you use an observable or a property with RaiseAndSetIfChanged ?

Gillibald commented 2 years ago

Just raising a change event for Source should work

aboccag commented 2 years ago

Hello, following up on this topic, just raising the change event does not work. Nothing is happening, the image does not show on the app.

Here is what I have :

fields:

private WriteableBitmap _outputImage = new WriteableBitmap(new PixelSize(1920, 1200), new Vector(96, 96), Avalonia.Platform.PixelFormat.Rgba8888, Avalonia.Platform.AlphaFormat.Premul);
private WriteableBitmap _inputImage = new WriteableBitmap(new PixelSize(1920, 1200), new Vector(96, 96), Avalonia.Platform.PixelFormat.Rgba8888, Avalonia.Platform.AlphaFormat.Premul);

When I recive a new image I'm doing this.

using SKBitmap outputToCopy = img.Output;
using var bufferDestination = _outputImage.Lock();
Marshal.Copy(outputToCopy.Bytes, 0, bufferDestination.Address, outputToCopy.Width * outputToCopy.Height);
this.RaisePropertyChanged();

The property linked from the view to the view model:

public WriteableBitmap OutputImage => _outputImage;
public WriteableBitmap InputImage => _inputImage;
timunie commented 2 years ago

Your best bet is to provide a minimal sample. Then we can have a deeper look

aboccag commented 2 years ago

Thank you,

There it is. Basically there is an interval observable of 10 ms that simulate a camera stream. The image is displayed on the main view. Try to resize the application and it crashes

best regards

https://github.com/broadside74/avalonia.issue.rezize_while_render_image

timunie commented 2 years ago

I wonder why you dispose your image over and over again and create a new one. I thought WriteableBitmap was there to just replace the Pixels?

You can also do new Bitmap(stream) if you don't need WriteableBitmap.

@avaloniaui-team : I found that in https://github.com/AvaloniaUI/Avalonia/blob/8f788883315c66ad1b9af27a9103e6b2049cfa3c/src/Avalonia.Base/Media/Imaging/Bitmap.cs#L110 Item can be null and that is where it crashes on resize. I don't know if we can / should mark Item as nullable and make a null check where needed?

aboccag commented 2 years ago

I wonder why you dispose your image over and over again and create a new one. I thought WriteableBitmap was there to just replace the Pixels? It is related to this discussion https://github.com/AvaloniaUI/Avalonia/discussions/7669#discussioncomment-2221375 You can also do new Bitmap(stream) if you don't need WriteableBitmap.

@avaloniaui-team : I found that in

https://github.com/AvaloniaUI/Avalonia/blob/8f788883315c66ad1b9af27a9103e6b2049cfa3c/src/Avalonia.Base/Media/Imaging/Bitmap.cs#L110

Item can be null and that is where it crashes on resize. I don't know if we can / should mark Item as nullable and make a null check where needed?

timunie commented 2 years ago

I'd suggest to not recreate images but to use WritableBitmap instead.

That means: don't create the image over and over again. Just replace the pixels. You need to lock your Bitmap to do so I think. ATM I don't have a sample for you, but maybe you can find one online.

timunie commented 2 years ago

I think the issue is, that some times the Binding has not the new Bitmap picked up while it actually gets disposed. To work around this I've sent you a PR.

https://github.com/broadside74/avalonia.issue.rezize_while_render_image/pull/1

It may only be a workaround, but at least for me I had no more crash

aboccag commented 2 years ago

Hello @timunie

First of all, big thank you for your help and your PR! it works in this case but I have some questions and remarks.

With this workaround, what if the camera update the MySkBitmap object quicker than the tick of the timer ?

https://github.com/AvaloniaUI/Avalonia/blob/8f788883315c66ad1b9af27a9103e6b2049cfa3c/src/Avalonia.Base/Media/Imaging/Bitmap.cs#L110

@avaloniaui-team By design, the property PlatformImpl.Item is not marked as nullable but it can actually be.

image

I tested the following code and it works fine. I think we definitely need to check the image is not null before actually reaching the Size property. This must be the same with the Dpi property.


public Size Size
{
    get
    {
        if(PlatformImpl.Item == null)
            return Size.Empty;
        return PlatformImpl.Item.PixelSize.ToSizeWithDpi(Dpi);
    }
}

Best regards

Alex

timunie commented 2 years ago

I agree that my PR is just a workaround. Things to add:

If you need to render every frame, your Observable may fail as well. Instead you need to listen to your camera change event if it exists.

Moreover you may want to pre cache some frames.

Also try update to 11.0 preview 1 as this is faster in rendering.

Gillibald commented 2 years ago

Item can only be null if the PlatformImpl is disposed before it is being used by the renderer. This should not happen. So we need to investigate when it is being disposed too early.

aboccag commented 2 years ago

Item can only be null if the PlatformImpl is disposed before it is being used by the renderer. This should not happen. So we need to investigate when it is being disposed too early.

Exactly ! It was because I had previously memory issues (see this thread https://github.com/AvaloniaUI/Avalonia/discussions/7669#discussioncomment-2221375). I decided to manualy dispose previous avalonia.imaging.Bitmap object which did the trick but when we resize the app, we need apparently need the reference.

if (_prevRef != writeableBitmap && _prevRef is not null)
{
    _prevRef.Dispose();
}

I suggest closing this issue and find a better way to correctly dispose these Bitmap

Thank you all