Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom Render sample #73

Open
timunie opened this issue Jan 8, 2024 · 1 comment
Open

Add custom Render sample #73

timunie opened this issue Jan 8, 2024 · 1 comment

Comments

@timunie
Copy link
Collaborator

timunie commented Jan 8, 2024

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>
    
</Window>
  1. 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
}
  1. Set a breakpoint at await Task.Delay(5000)
  2. Run the program in debug mode
  3. When debugging stops at await Task.Delay(5000), do Step Over, and then Resume Program.
  4. 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

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 AvaloniaUI/Avalonia#14144

@maxkatz6
Copy link
Member

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.

  • On how to force next rendered frame (Dispatcher + InvalidateVisual in 0.10, and Window.RequestAnimationFrame in 11.0)
  • On how to render text efficiently (can be yet another series of samples, as there are many approaches again)

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

No branches or pull requests

2 participants