mono / SkiaSharp

SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library. It provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.
MIT License
4.47k stars 538 forks source link

[BUG] Blazor Wasm runs out of memory trying to resize HTML canvas #1838

Closed artemiusgreat closed 2 years ago

artemiusgreat commented 3 years ago

Trying to create a new Blazor Wasm app using Skia similar to this sample. https://github.com/mattleibow/SkiaSharpBlazorWebAssembly

The result is here. https://github.com/artemiusgreat/SkiaBlazorWasmIssue

The sample app is running fine, but the new app keeps reloading page (notice the scrollbars) until it runs out of memory. There might be an issue with Blazor template, because new Blazor app created in VS specified port 5060 and 5061 as target but was still running on 5000 and 5001, so I copied files like launch.json, nuget packages in proj file and index.razor component from the sample app into new app. I tried both, new app and the sample in the same environment, the same nuget version, the same .NET version, but only new app is failing.

Error

I think I should add some config or additional dependency in the new app to make it work, but can't understand which exactly. Could somebody possibly take a look at the second link above and tell what is missing?

artemiusgreat commented 3 years ago

Update.

Original

Even if I just copy original sample project as is into my VS solution and just change namespace from SkiaSharpSample to another one, e.g. Client, I get the same error. It looks like original sample has some reference cached and makes it work fine, but any new project or another namespace causes this error.

Also, I replaced reference to local Views project to Nuget pacckage, so besides changing the namespace I also had to change project file too.

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>disable</Nullable>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
    <WasmNativeStrip>false</WasmNativeStrip>
    <EmccCompileOptimizationFlag>-O1</EmccCompileOptimizationFlag>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0-rc.2.21470.37" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0-rc.2.21470.37" PrivateAssets="all" />
    <PackageReference Include="SkiaSharp" Version="2.88.0-preview.152" />
    <PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.0-preview.152" />
    <PackageReference Include="SkiaSharp.Views.Blazor" Version="2.88.0-preview.152" />
  </ItemGroup>

</Project>
artemiusgreat commented 2 years ago

Reproduced the issue again and sort of found a root cause. Looks like SkiaSharp.Views package is trying to adjust the size of canvas element to the size of the screen, which is good. The problem is that this resize event seems to trigger some reinitialization which in turn triggers resize event again creating an infinite loop until stack or memory is full.

This little style prevents infinite resize. https://github.com/mattleibow/SkiaSharpBlazorWebAssembly/blob/main/blazor-native/wwwroot/css/app.css#L58

canvas {
  height: 300px;
}

Meanwhile, canvas tag is just an HTML, so it should be able to resize to the full width and height of the screen without hardcoded values and going into an infinite loop.

artemiusgreat commented 2 years ago

Hi @mattleibow I would be glad to provide some PR for this issue and would appreciate if you could point me in the direction where to look for it. I also recorded quick video to explain it better if it helps. https://www.youtube.com/watch?v=gRZVCrzuDDk The issue must be coming somewhere from the rendering method below but I couldn't debug it because including SkiaSharp and SkiaSharp.Views.Blazor directly into my app is not copying Skia PDB files to my project. https://github.com/mono/SkiaSharp/blob/main/source/SkiaSharp.Views.Blazor/SkiaSharp.Views.Blazor/SKCanvasView.razor.cs#L85

The only temporary workaround that I was able to come up with is to make sure that HTML canvas is not stretched outside of the screen. So, the width and height of the canvas must not exceed 100% of the size of its container. If canvas is too big and its container creates scroll, this is when infinite scaling starts resulting in OutOfMemoryException.

canvas {
  max-width: 100% !important;
  max-height: 100% !important;
}
JensKrumsieck commented 2 years ago

I can confirm this bug (when running without debugger from VS) with 2.88.0-preview.171, even if canvas has width and height attributes. Only aforementioned css prevents from scaling the canvas into infinity.

If a debugger is attached setting width- and height-Attributes without the css works.

JensKrumsieck commented 2 years ago

I monitored the Value of canvasSize and dpi in my OnPaintSurface-Event via Reflection:

if(_canvasView != null)
        {
            var canvasSizeField = typeof(SKCanvasView).GetField("canvasSize", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            var size = (SKSize)canvasSizeField.GetValue(_canvasView);
            Console.WriteLine(size.Width + " : " + size.Height);
            var dpiField = typeof(SKCanvasView).GetField("dpi", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            var dpi = (double)dpiField.GetValue(_canvasView);
            Console.WriteLine("DPI: " + dpi + " -> predicting: " + size.Width * dpi + ": " + size.Height * dpi);
        }

Which generates the following output:

800 : 600
DPI: 1,25 -> predicting: 1000: 750
1000 : 750
DPI: 1,25 -> predicting: 1250: 937,5
1250 : 937
DPI: 1,25 -> predicting: 1562,5: 1171,25
1562 : 1171
DPI: 1,25 -> predicting: 1952,5: 1463,75
1952 : 1463
DPI: 1,25 -> predicting: 2440: 1828,75
2440 : 1828
DPI: 1,25 -> predicting: 3050: 2285
3050 : 2285
DPI: 1,25 -> predicting: 3812,5: 2856,25
3812 : 2856
DPI: 1,25 -> predicting: 4765: 3570
4765 : 3570
DPI: 1,25 -> predicting: 5956,25: 4462,5
5956 : 4462
DPI: 1,25 -> predicting: 7445: 5577,5
7445 : 5577
DPI: 1,25 -> predicting: 9306,25: 6971,25
9306 : 6971
DPI: 1,25 -> predicting: 11632,5: 8713,75
11632 : 8713
DPI: 1,25 -> predicting: 14540: 10891,25
14540 : 10891
DPI: 1,25 -> predicting: 18175: 13613,75

As you can see the values of width and height are raised by the factor of dpi each time the event fires.

But why did I think I had solved the error with my commit (above)? Because I had performed the debug operation on a different monitor. The 1.25 DPI is the 125% scaling of the monitor from the Windows settings. The other monitor is set to 100%. On the monitor everything seems to work.... @artemiusgreat So i think you also have set a scaling in your monitor settings? If i do not provide width and height the canvas size remains 0! @mattleibow

JensKrumsieck commented 2 years ago

I think the problem could be:

interop.RequestAnimationFrame(EnableRenderLoop, (int)(canvasSize.Width * dpi), (int)(canvasSize.Height * dpi)); (Line 80) sends the size multiplied by DPI as argument which is resizing the canvas and forcing the sizeInteropWatcher's event to fire. This results in rerendering the canvas as the new size is set and Invalidate is called again here https://github.com/mono/SkiaSharp/blob/main/source/SkiaSharp.Views.Blazor/SkiaSharp.Views.Blazor/SKCanvasView.razor.cs#L158-L163

artemiusgreat commented 2 years ago

I monitored the Value of canvasSize and dpi in my OnPaintSurface-Event via Reflection:

if(_canvasView != null)
        {
            var canvasSizeField = typeof(SKCanvasView).GetField("canvasSize", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            var size = (SKSize)canvasSizeField.GetValue(_canvasView);
            Console.WriteLine(size.Width + " : " + size.Height);
            var dpiField = typeof(SKCanvasView).GetField("dpi", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            var dpi = (double)dpiField.GetValue(_canvasView);
            Console.WriteLine("DPI: " + dpi + " -> predicting: " + size.Width * dpi + ": " + size.Height * dpi);
        }

Which generates the following output:

800 : 600
DPI: 1,25 -> predicting: 1000: 750
1000 : 750
DPI: 1,25 -> predicting: 1250: 937,5
1250 : 937
DPI: 1,25 -> predicting: 1562,5: 1171,25
1562 : 1171
DPI: 1,25 -> predicting: 1952,5: 1463,75
1952 : 1463
DPI: 1,25 -> predicting: 2440: 1828,75
2440 : 1828
DPI: 1,25 -> predicting: 3050: 2285
3050 : 2285
DPI: 1,25 -> predicting: 3812,5: 2856,25
3812 : 2856
DPI: 1,25 -> predicting: 4765: 3570
4765 : 3570
DPI: 1,25 -> predicting: 5956,25: 4462,5
5956 : 4462
DPI: 1,25 -> predicting: 7445: 5577,5
7445 : 5577
DPI: 1,25 -> predicting: 9306,25: 6971,25
9306 : 6971
DPI: 1,25 -> predicting: 11632,5: 8713,75
11632 : 8713
DPI: 1,25 -> predicting: 14540: 10891,25
14540 : 10891
DPI: 1,25 -> predicting: 18175: 13613,75

As you can see the values of width and height are raised by the factor of dpi each time the event fires.

But why did I think I had solved the error with my commit (above)? Because I had performed the debug operation on a different monitor. The 1.25 DPI is the 125% scaling of the monitor from the Windows settings. The other monitor is set to 100%. On the monitor everything seems to work.... @artemiusgreat So i think you also have set a scaling in your monitor settings? If i do not provide width and height the canvas size remains 0! @mattleibow

Thank you for investigation. This seems to be the case, my Windows font scaling is set to 125%

JensKrumsieck commented 2 years ago

PR #1889 is merged which is in Version 2.88.0-preview.178 on NuGet (Latest Ver. 2.88.0-preview.179 is on NuGet, too) https://www.nuget.org/packages/SkiaSharp.Views.Blazor/2.88.0-preview.179 I assume this issue can be closed now ?!