AvaloniaUI / Avalonia.Samples

Avalonia.Samples aims to provide some minimal samples focusing on a particular issue at a time. This should help getting new users started.
https://www.avaloniaui.net
606 stars 103 forks source link

Add custom Render sample #73

Open timunie opened 8 months ago

timunie commented 8 months ago

This is a simple but yet stunning sample, so we want to show it.

Description

The problem is very strange for me, I have never encountered this before, and, to be honest, I am not entirely sure what exactly the problem is, in my code, in Avalonia, in .NET Core, or maybe in the debugger. Or maybe not a problem at all, but an inevitable behavior when using a debugger and breakpoints.

When I add my custom control to Window (this control uses Render), and then execute an asynchronous command in the WindowViewModel that awaits something longer than ~2 seconds (and has a breakpoint on it), and after that it also executes some code, then when the code of this command is completed, the application freezes completely and then crashes.

Steps to reproduce the behavior

  1. Create a new Avalonia MVVM app project on .NET Core 8 (Avalonia version 11.0.6)
  2. Add a custom SnowfallControl to the project with the following code:

    public class SnowfallControl : Control
    {
    private readonly List<Snowflake> snowflakes = [];
    private readonly DispatcherTimer timer = new();
    private readonly Stopwatch stopwatch = new();
    
    public SnowfallControl()
    {
        Loaded += (_, _) =>
        {
            // Initialize snowflakes
            for (var i = 0; i < 200; i++)
            {
                snowflakes.Add(new Snowflake
                {
                    Position = new Point(Random.Shared.NextDouble() * Bounds.Width, Random.Shared.NextDouble() * Bounds.Height),
                    Speed = Random.Shared.NextDouble() * 40 + 20,
                    Size = Random.Shared.NextDouble() * 2 + 1
                });
            }
            timer.Interval = TimeSpan.FromMilliseconds(1000.0 / 60.0); // 60 FPS
            timer.Tick += (_, _) => InvalidateVisual();
            timer.Start();
    
            stopwatch.Start();
        };
    }
    
    public override void Render(DrawingContext context)
    {
        base.Render(context);
    
        // Calculate elapsed time since last frame
        var elapsedTime = stopwatch.Elapsed.TotalSeconds;
        stopwatch.Restart();
    
        // Update and draw snowflakes
        foreach (var snowflake in snowflakes)
        {
            snowflake.Position = new Point(snowflake.Position.X, snowflake.Position.Y + snowflake.Speed * elapsedTime);
            if (snowflake.Position.Y > Bounds.Height)
            {
                snowflake.Position = new Point(Random.Shared.NextDouble() * Bounds.Width, 0);
            }
    
            context.DrawRectangle(Brushes.White, null, new Rect(snowflake.Position.X, snowflake.Position.Y, snowflake.Size, snowflake.Size));
        }
    
    }
    }
public class Snowflake
{
    public Point Position { get; set; }
    public double Speed { get; set; }
    public double Size { get; set; }
}
  1. Add this control to MainWindow.axaml:

    
    <Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:AvaloniaCustomControlRenderBugReproduction.ViewModels"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:controls="clr-namespace:AvaloniaCustomControlRenderBugReproduction.Controls"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="AvaloniaCustomControlRenderBugReproduction.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Icon="/Assets/avalonia-logo.ico"
        Title="AvaloniaCustomControlRenderBugReproduction">
    
    <Design.DataContext>
        <!-- This only sets the DataContext for the previewer in an IDE,
             to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
        <vm:MainWindowViewModel/>
    </Design.DataContext>
    
    <Grid>
        <StackPanel>
            <TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
            <Button Command="{Binding DoSomethingCommand}" Content="Test"/>
        </StackPanel>
        <controls:SnowfallControl IsHitTestVisible="False"/>
    </Grid>


4. Change the MainWindowViewModel code to the following:

public class MainWindowViewModel : ViewModelBase {

pragma warning disable CA1822 // Mark members as static

private string greeting = "Welcome to Avalonia!";
public string Greeting 
{
    get => greeting;
    set => this.RaiseAndSetIfChanged(ref greeting, value);
}

public ReactiveCommand<Unit, Unit> DoSomethingCommand { get; init; }

public MainWindowViewModel()
{
    DoSomethingCommand = ReactiveCommand.CreateFromTask(DoSomething);
}

private async Task DoSomething()
{
    await Task.Delay(5000);
    //No matter what code will be executed after awaiting, the issue happens
    var test1 = "Test1";
    var test2 = "Test2";
    var test3 = test1 += test2;
    //Greeting = "Delay awaited";
}

pragma warning restore CA1822 // Mark members as static

}


5. Set a breakpoint at `await Task.Delay(5000)`
6. Run the program in debug mode
7. When debugging stops at await Task.Delay(5000), do Step Over, and then Resume Program.
8. After this, the application will freeze for a while and crash with the following error:

Fatal error. Internal CLR error. (0x80131506) at Avalonia.Rendering.Composition.Drawing.RenderDataDrawingContext.DrawRectangleCore(Avalonia.Media.IBrush, Avalonia.Media.IPen, Avalonia.RoundedRect, Avalonia.Media.BoxShadows) at Avalonia.Media.DrawingContext.DrawRectangle(Avalonia.Media.IBrush, Avalonia.Media.IPen, Avalonia.Rect, Double, Double, Avalonia.Media.BoxShadows) at AvaloniaCustomControlRenderBugReproduction.Controls.SnowfallControl.Render(Avalonia.Media.DrawingContext) at Avalonia.Rendering.Composition.CompositingRenderer.UpdateCore() at Avalonia.Rendering.Composition.CompositingRenderer.Update() at Avalonia.Rendering.Composition.Compositor.CommitCore() at Avalonia.Rendering.Composition.Compositor.Commit() at Avalonia.Media.MediaContext.CommitCompositor(Avalonia.Rendering.Composition.Compositor) at Avalonia.Media.MediaContext.CommitCompositorsWithThrottling() at Avalonia.Media.MediaContext.RenderCore() at Avalonia.Media.MediaContext.Render() at Avalonia.Threading.DispatcherOperation.InvokeCore() at Avalonia.Threading.DispatcherOperation.Execute() at Avalonia.Threading.Dispatcher.ExecuteJob(Avalonia.Threading.DispatcherOperation) at Avalonia.Threading.Dispatcher.ExecuteJobsCore(Boolean) at Avalonia.Threading.Dispatcher.Signaled() at Avalonia.Win32.Win32DispatcherImpl.DispatchWorkItem() at Avalonia.Win32.Win32Platform.WndProc(IntPtr, UInt32, IntPtr, IntPtr) at Avalonia.Win32.Interop.UnmanagedMethods.DispatchMessage(MSG ByRef) at Avalonia.Win32.Interop.UnmanagedMethods.DispatchMessage(MSG ByRef) at Avalonia.Win32.Win32DispatcherImpl.RunLoop(System.Threading.CancellationToken) at Avalonia.Threading.DispatcherFrame.Run(Avalonia.Threading.IControlledDispatcherImpl) at Avalonia.Threading.Dispatcher.PushFrame(Avalonia.Threading.DispatcherFrame) at Avalonia.Threading.Dispatcher.MainLoop(System.Threading.CancellationToken) at Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.Start(System.String[]) at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(Avalonia.AppBuilder, System.String[], Avalonia.Controls.ShutdownMode) at AvaloniaCustomControlRenderBugReproduction.Program.Main(System.String[])



When the program crashes, the IDE also displays the following message:
`Target process has exited during evaluation of instance method System.Exception::get_Message() with actual parameters System.ExecutionEngineException. This may possibly happen due to StackOverflowException.`

[Issue reproduction video](https://youtu.be/EqAuUQwJsgI)

## Environment
- OS: Windows 10 22H2 (19045.3803)
- Avalonia-Version: 11.0.6
- JetBrains Rider: 2023.3.2
- .NET Core: 8

## Additional context
The problem does not arise in production if the program is executed without debugging. The problem also does not arise even if the program is launched in debugging mode but there is no breakpoint on await.

_Originally posted by @BnnQ in https://github.com/AvaloniaUI/Avalonia/discussions/14144_
maxkatz6 commented 8 months ago

In general, we need samples for these different approaches:

  1. WriteableBitmap rendering (poor performance and quality, but easy caching and flexibility). WriteableBitmapPage
  2. Render method override with Draw commands (DrawRectanle...). CustomDrawingExampleControl
  3. ICustomDrawOperation - render thread rendering, direct drawing to the canvas. Can access SkiaSharp canvas. CustomSkiaPage
  4. Composition API custom visual CompositionCustomVisualHandler
  5. GPU interop - hardcore approach, the best for low level rendering with direct access to OpenGL/D3D/Vulkan. Should be ideal for media players or game integrations. GpuInterop OpenGl

We have samples for all of these approaches randomly scattered across our repositories. At the very least this comment should help with googling them. Ideally, yes, these should be duplicated here.