Browse Source

Refactored WASM rendering, added MT support for dispatcher (#15709)

* Refactored WASM rendering, added MT support for dispatcher

* Update src/Browser/Avalonia.Browser/WindowingPlatform.cs

* net8 fixes

---------

Co-authored-by: Max Katz <maxkatz6@outlook.com>
pull/15641/head
Nikita Tsukanov 2 years ago
committed by GitHub
parent
commit
864ecfd60a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
  2. 13
      src/Avalonia.Base/Platform/IPlatformGpu.cs
  3. 3
      src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs
  4. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  5. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  6. 20
      src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs
  7. 5
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  8. 15
      src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs
  9. 15
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  10. 6
      src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
  11. 35
      src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
  12. 35
      src/Browser/Avalonia.Browser/Interop/TimerHelper.cs
  13. 24
      src/Browser/Avalonia.Browser/Rendering/BrowserCompositor.cs
  14. 51
      src/Browser/Avalonia.Browser/Rendering/BrowserGlSurface.cs
  15. 98
      src/Browser/Avalonia.Browser/Rendering/BrowserRasterSurface.cs
  16. 23
      src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs
  17. 13
      src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs
  18. 69
      src/Browser/Avalonia.Browser/Rendering/BrowserSoftwareRenderTarget.cs
  19. 59
      src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs
  20. 180
      src/Browser/Avalonia.Browser/Rendering/BrowserWebGlRenderTarget.cs
  21. 101
      src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs
  22. 140
      src/Browser/Avalonia.Browser/Rendering/RenderWorker.cs
  23. 43
      src/Browser/Avalonia.Browser/Rendering/WebRenderTarget.cs
  24. 52
      src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs
  25. 40
      src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderSession.cs
  26. 39
      src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs
  27. 36
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  28. 1
      src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets
  29. 12
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
  30. 12
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts
  31. 66
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts
  32. 5
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/renderingMode.ts
  33. 0
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/resizeHandler.ts
  34. 44
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/softwareRenderTarget.ts
  35. 80
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webGlRenderTarget.ts
  36. 11
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webRenderTarget.ts
  37. 81
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webRenderTargetRegistry.ts
  38. 40
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts
  39. 41
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts
  40. 18
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceBase.ts
  41. 44
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceFactory.ts
  42. 58
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webGlSurface.ts
  43. 31
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts
  44. 40
      src/Browser/Avalonia.Browser/webapp/package-lock.json
  45. 1
      src/Browser/Avalonia.Browser/webapp/package.json
  46. 16
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs
  47. 8
      src/Skia/Avalonia.Skia/Gpu/OpenGl/IGlSkiaSpecificOptionsFeature.cs

9
samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj

@ -5,6 +5,8 @@
<WasmMainJSPath>wwwroot/main.js</WasmMainJSPath>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DebuggerSupport>true</DebuggerSupport>
<WasmDebugLevel>5</WasmDebugLevel>
</PropertyGroup>
<ItemGroup>
@ -12,6 +14,13 @@
<ProjectReference Include="../../src/Browser/Avalonia.Browser/Avalonia.Browser.csproj" />
<ProjectReference Include="../ControlCatalog/ControlCatalog.csproj" />
</ItemGroup>
<ItemGroup>
<!--
A workaround for VS and Rider being dumber than a rock when it comes to build dependency analysis
Also see https://youtrack.jetbrains.com/issue/RIDER-107175/Rider-doesnt-update-generate-transitive-Content-dependencies
-->
<AdditionalUpToDateCheckInput Include="../../src/Browser/Avalonia.Browser/**/*" Visible="false"/>
</ItemGroup>
<ItemGroup>
<WasmExtraFilesToDeploy Include="wwwroot/**" />

13
src/Avalonia.Base/Platform/IPlatformGpu.cs

@ -11,6 +11,19 @@ public interface IPlatformGraphics
IPlatformGraphicsContext GetSharedContext();
}
[Unstable]
public interface IPlatformGraphicsWithFeatures : IPlatformGraphics, IOptionalFeatureProvider
{
}
[Unstable]
public interface IPlatformGraphicsReadyStateFeature
{
bool IsReady { get; }
bool UsesContexts { get; }
}
[Unstable]
public interface IPlatformGraphicsContext : IDisposable, IOptionalFeatureProvider
{

3
src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs

@ -33,6 +33,7 @@ internal class UnmanagedBlob : IDisposable
GC.WaitForPendingFinalizers();
}
#endif
public static bool SuppressFinalizerWarning { get; set; }
public UnmanagedBlob(int size)
{
@ -78,7 +79,7 @@ internal class UnmanagedBlob : IDisposable
public void Dispose()
{
#if DEBUG
if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId)
if (!SuppressFinalizerWarning && Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId)
{
lock (_lock)
{

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -140,7 +140,6 @@ namespace Avalonia.Rendering.Composition.Server
if (!_redrawRequested)
return;
_redrawRequested = false;
var renderTargetWithProperties = _renderTarget as IRenderTargetWithProperties;
@ -199,6 +198,7 @@ namespace Avalonia.Rendering.Composition.Server
RenderedVisuals = 0;
_redrawRequested = false;
DirtyRects.Reset();
}
}

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@ -218,6 +218,8 @@ namespace Avalonia.Rendering.Composition.Server
try
{
if(!RenderInterface.IsReady)
return;
RenderInterface.EnsureValidBackendContext();
ExecuteServerJobs();
foreach (var t in _activeTargets)

20
src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs

@ -11,16 +11,24 @@ internal class PlatformRenderInterfaceContextManager
private readonly IPlatformGraphics? _graphics;
private IPlatformRenderInterfaceContext? _backend;
private OwnedDisposable<IPlatformGraphicsContext>? _gpuContext;
private readonly IPlatformGraphicsReadyStateFeature? _readyStateFeature;
public event Action? ContextDisposed;
public event Action<IPlatformRenderInterfaceContext>? ContextCreated;
public PlatformRenderInterfaceContextManager(IPlatformGraphics? graphics)
{
_graphics = graphics;
_readyStateFeature = (_graphics as IPlatformGraphicsWithFeatures)
?.TryGetFeature<IPlatformGraphicsReadyStateFeature>();
}
public bool IsReady => _readyStateFeature?.IsReady ?? true;
public void EnsureValidBackendContext()
{
if (!IsReady)
throw new InvalidOperationException("Platform graphics isn't ready yet");
if (_backend == null || _gpuContext?.Value.IsLost == true)
{
_backend?.Dispose();
@ -34,10 +42,14 @@ internal class PlatformRenderInterfaceContextManager
if (_graphics != null)
{
if (_graphics.UsesSharedContext)
_gpuContext = new OwnedDisposable<IPlatformGraphicsContext>(_graphics.GetSharedContext(), false);
else
_gpuContext = new OwnedDisposable<IPlatformGraphicsContext>(_graphics.CreateContext(), true);
if (_readyStateFeature?.UsesContexts != false)
{
if (_graphics.UsesSharedContext)
_gpuContext =
new OwnedDisposable<IPlatformGraphicsContext>(_graphics.GetSharedContext(), false);
else
_gpuContext = new OwnedDisposable<IPlatformGraphicsContext>(_graphics.CreateContext(), true);
}
}
_backend = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>()

5
src/Browser/Avalonia.Browser/BrowserAppBuilder.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Rendering;
using Avalonia.Metadata;
namespace Avalonia.Browser;
@ -107,6 +108,10 @@ public static class BrowserAppBuilder
AvaloniaLocator.CurrentMutable.Bind<BrowserPlatformOptions>().ToConstant(options);
await AvaloniaModule.ImportMain();
if (BrowserWindowingPlatform.IsThreadingEnabled)
{
await RenderWorker.InitializeAsync();
}
if (builder.WindowingSubsystemInitializer is null)
{

15
src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs

@ -14,16 +14,17 @@ internal class BrowserDispatcherImpl : IDispatcherImpl
private bool _signaled;
private int? _timerId;
private readonly Action _timerCallback;
private readonly Action _signalCallback;
public BrowserDispatcherImpl()
{
_thread = Thread.CurrentThread;
_clock = Stopwatch.StartNew();
_timerCallback = () => Timer?.Invoke();
_signalCallback = () =>
TimerHelper.Interval += () =>
{
Timer?.Invoke();
};
TimerHelper.Timeout = () =>
{
_signaled = false;
Signaled?.Invoke();
@ -44,7 +45,7 @@ internal class BrowserDispatcherImpl : IDispatcherImpl
// NOTE: by HTML5 spec minimal timeout is 4ms, but Chrome seems to work well with 1ms as well.
var interval = 1;
TimerHelper.SetTimeout(_signalCallback, interval);
TimerHelper.SetTimeout(interval);
}
public void UpdateTimer(long? dueTimeInMs)
@ -58,7 +59,7 @@ internal class BrowserDispatcherImpl : IDispatcherImpl
if (dueTimeInMs.HasValue)
{
var interval = Math.Max(1, dueTimeInMs.Value - _clock.ElapsedMilliseconds);
_timerId = TimerHelper.SetInterval(_timerCallback, (int)interval);
_timerId = TimerHelper.SetInterval((int)interval);
}
}
}

15
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@ -39,10 +39,9 @@ namespace Avalonia.Browser
{
InputHelper.InitializeBackgroundHandlers();
}
public BrowserTopLevelImpl(JSObject container, JSObject nativeControlHost, JSObject inputElement)
{
Surfaces = Enumerable.Empty<object>();
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
_inputHandler = new BrowserInputHandler(this, container);
@ -56,14 +55,12 @@ namespace Avalonia.Browser
_container = container;
_surface = BrowserSurface.Create(container, PixelFormats.Rgba8888);
var opts = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
_surface = RenderTargetBrowserSurface.Create(container, opts.RenderingMode);
_surface.SizeChanged += OnSizeChanged;
_surface.ScalingChanged += OnScalingChanged;
Surfaces = new[] { _surface };
Compositor = _surface.IsWebGl ?
BrowserCompositor.WebGlUiCompositor :
BrowserCompositor.SoftwareUiCompositor;
Compositor = _surface.Compositor;
}
private void OnScalingChanged()
@ -120,7 +117,7 @@ namespace Avalonia.Browser
public Size? FrameSize => null;
public double RenderScaling => _surface?.Scaling ?? 1;
public IEnumerable<object> Surfaces { get; set; }
public IEnumerable<object> Surfaces => _surface?.GetRenderSurfaces() ?? [];
public Action<RawInputEventArgs>? Input { get; set; }
public Action<Rect>? Paint { get; set; }

6
src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs

@ -6,11 +6,13 @@ namespace Avalonia.Browser.Interop;
internal static partial class AvaloniaModule
{
private static readonly Lazy<Task> s_importMain = new(() =>
private static readonly Lazy<Task> s_importMain = new(ImportMainToCurrentContext);
public static Task ImportMainToCurrentContext()
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js"));
});
}
private static readonly Lazy<Task> s_importStorage = new(() =>
{

35
src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs

@ -11,43 +11,16 @@ internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int
internal static partial class CanvasHelper
{
public static (JSObject CanvasView, GLInfo? GLInfo) CreateSurface(
JSObject container, BrowserRenderingMode renderingMode)
{
var isGlMode = renderingMode is BrowserRenderingMode.WebGL1 or BrowserRenderingMode.WebGL2;
var canvasView = Create(container, (int)renderingMode);
GLInfo? glInfo = null;
if (isGlMode)
{
glInfo = new GLInfo(
canvasView.GetPropertyAsInt32("contextHandle")!,
(uint)canvasView.GetPropertyAsInt32("fboId"),
canvasView.GetPropertyAsInt32("stencil"),
canvasView.GetPropertyAsInt32("sample"),
canvasView.GetPropertyAsInt32("depth"));
}
return (canvasView, glInfo);
}
[JSImport("CanvasFactory.onSizeChanged", AvaloniaModule.MainModuleName)]
[JSImport("CanvasSurface.onSizeChanged", AvaloniaModule.MainModuleName)]
public static partial void OnSizeChanged(
JSObject canvasSurface,
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>]
// TODO: this callback should be <int, int, double>. Revert after next .NET 9 preview.
Action<double, double, double> onSizeChanged);
[JSImport("CanvasFactory.create", AvaloniaModule.MainModuleName)]
private static partial JSObject Create(JSObject canvasSurface, int mode);
[JSImport("CanvasSurface.create", AvaloniaModule.MainModuleName)]
public static partial JSObject CreateRenderTargetSurface(JSObject canvasSurface, int[] modes, int threadId);
[JSImport("CanvasFactory.destroy", AvaloniaModule.MainModuleName)]
[JSImport("CanvasSurface.destroy", AvaloniaModule.MainModuleName)]
public static partial void Destroy(JSObject canvasSurface);
[JSImport("CanvasFactory.ensureSize", AvaloniaModule.MainModuleName)]
public static partial void EnsureSize(JSObject canvasSurface);
[JSImport("CanvasFactory.putPixelData", AvaloniaModule.MainModuleName)]
public static partial void PutPixelData(JSObject canvasSurface, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> data, int width, int height);
}

35
src/Browser/Avalonia.Browser/Interop/TimerHelper.cs

@ -1,22 +1,45 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Avalonia.Browser.Interop;
internal static partial class TimerHelper
{
[JSImport("TimerHelper.runAnimationFrames", AvaloniaModule.MainModuleName)]
public static partial void RunAnimationFrames(
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Boolean>>] Func<double, bool> renderFrameCallback);
public static partial void RunAnimationFrames();
[JSImport("globalThis.setTimeout")]
public static partial int SetTimeout([JSMarshalAs<JSType.Function>] Action callback, int intervalMs);
public static Action<double>? AnimationFrame;
[JSExport]
public static void JsExportOnAnimationFrame(double d)
{
AnimationFrame?.Invoke(d);
}
public static Action? Timeout;
[JSExport]
public static Task JsExportOnTimeout()
{
Timeout?.Invoke();
return Task.CompletedTask;
}
[JSImport("TimerHelper.setTimeout", AvaloniaModule.MainModuleName)]
public static partial int SetTimeout(int intervalMs);
[JSImport("globalThis.clearTimeout")]
public static partial int ClearTimeout(int id);
[JSImport("globalThis.setInterval")]
public static partial int SetInterval([JSMarshalAs<JSType.Function>] Action callback, int intervalMs);
public static Action? Interval;
[JSExport]
public static Task JsExportOnInterval()
{
Interval?.Invoke();
return Task.CompletedTask;
}
[JSImport("TimerHelper.setInterval", AvaloniaModule.MainModuleName)]
public static partial int SetInterval( int intervalMs);
[JSImport("globalThis.clearInterval")]
public static partial int ClearInterval(int id);

24
src/Browser/Avalonia.Browser/Rendering/BrowserCompositor.cs

@ -1,24 +0,0 @@
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
namespace Avalonia.Browser.Rendering;
/// <summary>
/// We want to reuse timer/compositor instances per each AvaloniaView.
/// But at the same time, we want to keep possiblity of having different rendering modes (both software and webgl) at the same time.
/// For example, WebGL contexts number might exceed maximum allowed, or we might want to keep popups in software renderer.
/// </summary>
internal static class BrowserCompositor
{
private static BrowserRenderTimer? s_browserUiRenderTimer;
private static BrowserRenderTimer BrowserUiRenderTimer => s_browserUiRenderTimer ??= new BrowserRenderTimer(false);
private static Compositor? s_webGlUiCompositor, s_softwareUiCompositor;
internal static Compositor WebGlUiCompositor => s_webGlUiCompositor ??= new Compositor(
new RenderLoop(BrowserUiRenderTimer), AvaloniaLocator.Current.GetRequiredService<IPlatformGraphics>());
internal static Compositor SoftwareUiCompositor => s_softwareUiCompositor ??= new Compositor(
new RenderLoop(BrowserUiRenderTimer), null);
}

51
src/Browser/Avalonia.Browser/Rendering/BrowserGlSurface.cs

@ -1,51 +0,0 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Skia;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Browser.Rendering;
internal sealed class BrowserGlSurface : BrowserSurface
{
private readonly GRGlInterface _glInterface;
public BrowserGlSurface(JSObject canvasSurface, GLInfo glInfo, PixelFormat pixelFormat,
BrowserRenderingMode renderingMode)
: base(canvasSurface, renderingMode)
{
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>();
_glInterface = GRGlInterface.Create() ?? throw new InvalidOperationException("Unable to create GRGlInterface.");
Context = GRContext.CreateGl(_glInterface) ??
throw new InvalidOperationException("Unable to create GRContext.");
if (skiaOptions?.MaxGpuResourceSizeBytes is { } resourceSizeBytes)
{
Context.SetResourceCacheLimit(resourceSizeBytes);
}
GlInfo = glInfo ?? throw new ArgumentNullException(nameof(glInfo));
PixelFormat = pixelFormat;
}
public PixelFormat PixelFormat { get; }
public GRContext Context { get; private set; }
public GLInfo GlInfo { get; }
public override void Dispose()
{
base.Dispose();
Context.Dispose();
Context = null!;
_glInterface.Dispose();
}
public void EnsureResize()
{
CanvasHelper.EnsureSize(JsSurface);
}
}

98
src/Browser/Avalonia.Browser/Rendering/BrowserRasterSurface.cs

@ -1,98 +0,0 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Browser.Interop;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Platform;
namespace Avalonia.Browser.Skia;
internal sealed class BrowserRasterSurface : BrowserSurface, IFramebufferPlatformSurface
{
public PixelFormat PixelFormat { get; set; }
private FramebufferData? _fbData;
private readonly Action _onDisposeAction;
private readonly int _bytesPerPixel;
public BrowserRasterSurface(JSObject canvasSurface, PixelFormat pixelFormat, BrowserRenderingMode renderingMode)
: base(canvasSurface, renderingMode)
{
PixelFormat = pixelFormat;
_onDisposeAction = Blit;
_bytesPerPixel = pixelFormat.BitsPerPixel / 8;
}
public override void Dispose()
{
_fbData?.Dispose();
_fbData = null;
base.Dispose();
}
public ILockedFramebuffer Lock()
{
var bytesPerPixel = _bytesPerPixel;
var dpi = Scaling * 96.0;
var size = RenderSize;
if (_fbData is null || _fbData?.Size != size)
{
_fbData?.Dispose();
_fbData = new FramebufferData(size.Width, size.Height, bytesPerPixel);
}
var data = _fbData;
return new LockedFramebuffer(
data.Address, data.Size, data.RowBytes,
new Vector(dpi, dpi), PixelFormat, _onDisposeAction);
}
private void Blit()
{
if (_fbData is { } data)
{
CanvasHelper.PutPixelData(JsSurface, data.AsSegment, data.Size.Width, data.Size.Height);
}
}
private class FramebufferData
{
private static ArrayPool<byte> s_pool = ArrayPool<byte>.Create();
private readonly byte[] _array;
private GCHandle _handle;
public FramebufferData(int width, int height, int bytesPerPixel)
{
Size = new PixelSize(width, height);
RowBytes = width * bytesPerPixel;
var length = width * height * bytesPerPixel;
_array = s_pool.Rent(length);
_handle = GCHandle.Alloc(_array, GCHandleType.Pinned);
Address = _handle.AddrOfPinnedObject();
AsSegment = new ArraySegment<byte>(_array, 0, length);
}
public PixelSize Size { get; }
public int RowBytes { get; }
public IntPtr Address { get; }
public ArraySegment<byte> AsSegment { get; }
public void Dispose()
{
_handle.Free();
s_pool.Return(_array);
}
}
public IFramebufferRenderTarget CreateFramebufferRenderTarget() => new FuncFramebufferRenderTarget(Lock);
}

23
src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs

@ -2,12 +2,14 @@ using System;
using System.Diagnostics;
using Avalonia.Browser.Interop;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace Avalonia.Browser.Rendering;
internal class BrowserRenderTimer : IRenderTimer
{
private Action<TimeSpan>? _tick;
private bool _started;
public BrowserRenderTimer(bool isBackground)
{
@ -20,10 +22,8 @@ internal class BrowserRenderTimer : IRenderTimer
{
add
{
if (_tick is null)
{
TimerHelper.RunAnimationFrames(RenderFrameCallback);
}
if (!BrowserWindowingPlatform.IsThreadingEnabled)
StartOnThisThread();
_tick += value;
}
@ -33,14 +33,21 @@ internal class BrowserRenderTimer : IRenderTimer
}
}
private bool RenderFrameCallback(double timestamp)
public void StartOnThisThread()
{
if (!_started)
{
_started = true;
TimerHelper.AnimationFrame += RenderFrameCallback;
TimerHelper.RunAnimationFrames();
}
}
private void RenderFrameCallback(double timestamp)
{
if (_tick is { } tick)
{
tick.Invoke(TimeSpan.FromMilliseconds(timestamp));
return true;
}
return false;
}
}

13
src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs

@ -0,0 +1,13 @@
using System;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
namespace Avalonia.Browser.Rendering;
internal static class BrowserSharedRenderLoop
{
private static BrowserRenderTimer? s_browserUiRenderTimer;
public static BrowserRenderTimer RenderTimer => s_browserUiRenderTimer ??= new BrowserRenderTimer(false);
public static Lazy<RenderLoop> RenderLoop = new(() => new RenderLoop(RenderTimer), true);
}

69
src/Browser/Avalonia.Browser/Rendering/BrowserSoftwareRenderTarget.cs

@ -0,0 +1,69 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Browser.Interop;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Platform;
using Avalonia.Reactive;
#pragma warning disable CS0169
#pragma warning disable CA1823
namespace Avalonia.Browser.Rendering;
partial class BrowserSoftwareRenderTarget : BrowserRenderTarget, IFramebufferPlatformSurface
{
private readonly Func<(PixelSize, double)> _sizeGetter;
public override IPlatformGraphicsContext? PlatformGraphicsContext => null;
private Action<RetainedFramebuffer> _blit;
public BrowserSoftwareRenderTarget(JSObject js, Func<(PixelSize, double)> sizeGetter) : base(js)
{
_sizeGetter = sizeGetter;
_blit = Blit;
}
class FramebufferRenderTarget : IFramebufferRenderTarget
{
private readonly BrowserSoftwareRenderTarget _parent;
private RetainedFramebuffer? _fb;
public FramebufferRenderTarget(BrowserSoftwareRenderTarget parent)
{
_parent = parent;
}
public void Dispose()
{
_fb?.Dispose();
_fb = null;
}
public ILockedFramebuffer Lock()
{
var (size, scaling) = _parent._sizeGetter();
_parent.UpdateSize(size);
if (_fb == null || _fb.Size != size)
{
_fb?.Dispose();
_fb = null;
_fb = new RetainedFramebuffer(size, PixelFormat.Rgba8888);
}
return _fb.Lock(new Vector(scaling * 96, scaling * 96), _parent._blit);
}
}
public IFramebufferRenderTarget CreateFramebufferRenderTarget()
{
return new FramebufferRenderTarget(this);
}
[JSImport("SoftwareRenderTarget.staticPutPixelData", AvaloniaModule.MainModuleName)]
public static partial void PutPixelData(JSObject js, int address, int size, int width, int height);
private void Blit(RetainedFramebuffer fb)
{
PutPixelData(Js, fb.Address.ToInt32(), fb.Size.Width * fb.Size.Height * 4, fb.Size.Width, fb.Size.Height);
}
}

59
src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs

@ -9,25 +9,24 @@ using Avalonia.Browser.Rendering;
using Avalonia.Logging;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia.Browser.Skia;
internal abstract class BrowserSurface : IDisposable
{
private readonly BrowserRenderingMode _renderingMode;
protected BrowserSurface(JSObject jsSurface, BrowserRenderingMode renderingMode)
protected BrowserSurface(JSObject jsSurface, Compositor compositor)
{
_renderingMode = renderingMode;
JsSurface = jsSurface;
Compositor = compositor;
Scaling = 1;
ClientSize = new Size(1, 1);
RenderSize = new PixelSize(1, 1);
}
public bool IsWebGl => _renderingMode is BrowserRenderingMode.WebGL1 or BrowserRenderingMode.WebGL2;
public Compositor Compositor { get; }
public JSObject JsSurface { get; private set; }
public double Scaling { get; private set; }
@ -39,45 +38,16 @@ internal abstract class BrowserSurface : IDisposable
public event Action? SizeChanged;
public event Action? ScalingChanged;
public static BrowserSurface Create(JSObject container, PixelFormat pixelFormat)
protected virtual void Initialize()
{
var opts = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
if (opts.RenderingMode is null || !opts.RenderingMode.Any())
{
throw new InvalidOperationException(
$"{nameof(BrowserPlatformOptions)}.{nameof(BrowserPlatformOptions.RenderingMode)} must not be empty or null");
}
BrowserSurface? surface = null;
foreach (var mode in opts.RenderingMode)
{
try
{
var (jsSurface, jsGlInfo) = CanvasHelper.CreateSurface(container, mode);
surface = jsGlInfo != null
? new BrowserGlSurface(jsSurface, jsGlInfo, pixelFormat, mode)
: new BrowserRasterSurface(jsSurface, pixelFormat, mode);
break;
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Error, LogArea.BrowserPlatform)?
.Log(null,
"Creation of BrowserSurface with mode {Mode} failed with an error:\r\n{Exception}",
mode, ex);
}
}
if (surface is null)
{
throw new InvalidOperationException(
$"{nameof(BrowserPlatformOptions)}.{nameof(BrowserPlatformOptions.RenderingMode)} has a value of \"{string.Join(", ", opts.RenderingMode)}\", but no options were applied.");
}
CanvasHelper.OnSizeChanged(surface.JsSurface, surface.OnSizeChanged);
return surface;
CanvasHelper.OnSizeChanged(JsSurface, OnSizeChanged);
var w = JsSurface.GetPropertyAsDouble("width");
var h = JsSurface.GetPropertyAsDouble("height");
var s = JsSurface.GetPropertyAsDouble("scaling");
Console.WriteLine($"Initial size: {w} {h} {s}");
OnSizeChanged((int)w, (int)h, s);
}
public virtual void Dispose()
{
CanvasHelper.Destroy(JsSurface);
@ -87,8 +57,9 @@ internal abstract class BrowserSurface : IDisposable
ClientSize = default;
}
private void OnSizeChanged(double pixelWidth, double pixelHeight, double dpr)
protected virtual void OnSizeChanged(double pixelWidth, double pixelHeight, double dpr)
{
Console.WriteLine($"OnSizeChanged: {Dispatcher.UIThread.CheckAccess()} {pixelWidth} {pixelHeight} {dpr} ");
var oldScaling = Scaling;
var oldClientSize = ClientSize;
RenderSize = new PixelSize((int)pixelWidth, (int)pixelHeight);
@ -99,4 +70,6 @@ internal abstract class BrowserSurface : IDisposable
if (Math.Abs(oldScaling - dpr) > 0.0001)
ScalingChanged?.Invoke();
}
public virtual object[] GetRenderSurfaces() => [this];
}

180
src/Browser/Avalonia.Browser/Rendering/BrowserWebGlRenderTarget.cs

@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using Avalonia.Browser.Interop;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform;
using Avalonia.Reactive;
namespace Avalonia.Browser.Rendering;
partial class BrowserWebGlRenderTarget : BrowserRenderTarget, IGlPlatformSurface
{
private readonly Func<(PixelSize Size, double Scaling)> _sizeGetter;
private readonly GLInfo _glInfo;
public IGlContext GlContext { get; }
public BrowserWebGlRenderTarget(JSObject js, Func<(PixelSize, double)> sizeGetter) : base(js)
{
_sizeGetter = sizeGetter;
_glInfo = new GLInfo(
js.GetPropertyAsInt32("contextHandle")!,
(uint)js.GetPropertyAsInt32("fboId"),
js.GetPropertyAsInt32("stencil"),
js.GetPropertyAsInt32("sample"),
js.GetPropertyAsInt32("depth"));
var contextId = js.GetPropertyAsInt32("contextHandle");
var version = js.GetPropertyAsJSObject("attrs")!.GetPropertyAsInt32("majorVersion");
GlContext = new WebGlContext(contextId, new GlVersion(GlProfileType.OpenGLES, version > 1 ? 3 : 2, 0),
_glInfo.Samples, _glInfo.Stencils);
}
class GlSession : IGlPlatformSurfaceRenderingSession
{
private IDisposable? _restoreContext;
public GlSession(IGlContext context, PixelSize size, double scaling, IDisposable restoreContext)
{
_restoreContext = restoreContext;
Context = context;
Size = size;
Scaling = scaling;
}
public void Dispose()
{
_restoreContext?.Dispose();
_restoreContext = null;
}
public IGlContext Context { get; }
public PixelSize Size { get; }
// This should technically be delivered via CompositionTarget.Scaling anyway, why do we still have this property
public double Scaling { get; }
public bool IsYFlipped => false;
}
class GlSurface : IGlPlatformSurfaceRenderTarget
{
private readonly BrowserWebGlRenderTarget _target;
public GlSurface(BrowserWebGlRenderTarget target)
{
_target = target;
}
public void Dispose()
{
// No-op
}
public IGlPlatformSurfaceRenderingSession BeginDraw()
{
var s = _target._sizeGetter();
_target.UpdateSize(s.Size);
var restoreContext = _target.GlContext.EnsureCurrent();
_target.GlContext.GlInterface.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, (int)_target._glInfo.FboId);
return new GlSession(_target.GlContext, s.Size, s.Scaling, restoreContext);
}
}
public override IPlatformGraphicsContext? PlatformGraphicsContext => GlContext;
public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context)
{
return new GlSurface(this);
}
}
partial class WebGlContext : IGlContext, Avalonia.Skia.IGlSkiaSpecificOptionsFeature
{
[JSImport("WebGlRenderTarget.getCurrentContext", AvaloniaModule.MainModuleName)]
private static partial int GetCurrentContext();
[JSImport("WebGlRenderTarget.makeContextCurrent", AvaloniaModule.MainModuleName)]
private static partial bool MakeContextCurrent(int context);
[DllImport("libSkiaSharp", EntryPoint = "eglGetProcAddress")]
private static extern IntPtr eglGetProcAddress(string name);
private int _contextId;
private readonly Thread _thread;
public WebGlContext(int contextId, GlVersion version, int sampleCount, int stencilSize)
{
Version = version;
SampleCount = sampleCount;
StencilSize = stencilSize;
_contextId = contextId;
_thread = Thread.CurrentThread;
using (MakeCurrent())
GlInterface = new GlInterface(version, eglGetProcAddress);
}
void VerifyAccess()
{
if (_thread != Thread.CurrentThread)
throw new InvalidOperationException("Call from invalid thread");
}
public IDisposable EnsureCurrent()
{
VerifyAccess();
if(GetCurrentContext() == _contextId)
return Disposable.Empty;
return MakeCurrent();
}
class RestoreContext : IDisposable
{
private int? _contextId;
public RestoreContext(int contextId)
{
_contextId = contextId;
}
public void Dispose()
{
if (_contextId != null)
MakeContextCurrent(_contextId.Value);
_contextId = null;
}
}
public IDisposable MakeCurrent()
{
VerifyAccess();
var old = GetCurrentContext();
if (!MakeContextCurrent(_contextId))
throw new OpenGlException("Unable to make the context current");
return new RestoreContext(old);
}
public void Dispose()
{
// No-op, destroyed with the render target
}
public object? TryGetFeature(Type featureType) => null;
// TODO: Implement
public bool IsLost => false;
public GlVersion Version { get; }
public GlInterface GlInterface { get; }
public int SampleCount { get; }
public int StencilSize { get; }
public bool IsSharedWith(IGlContext context) => false;
public bool CanCreateSharedContext => false;
public IGlContext? CreateSharedContext(IEnumerable<GlVersion>? preferredVersions = null) =>
throw new NotSupportedException();
public bool UseNativeSkiaGrGlInterface => true;
}

101
src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Skia;
using Avalonia.Platform;
using Avalonia.Rendering.Composition;
namespace Avalonia.Browser.Rendering;
internal class RenderTargetBrowserSurface : BrowserSurface
{
private readonly BrowserPlatformGraphics _graphics;
private record InitParams(Compositor Compositor, BrowserPlatformGraphics Graphics);
private static InitParams CreateCompositor(JSObject jsSurface)
{
var targetId = jsSurface.GetPropertyAsInt32("targetId");
var graphics = new BrowserPlatformGraphics(targetId);
var compositor = new Compositor(BrowserSharedRenderLoop.RenderLoop.Value, graphics);
return new(compositor, graphics);
}
public RenderTargetBrowserSurface(JSObject jsSurface) : this(jsSurface, CreateCompositor(jsSurface))
{
}
public override object[] GetRenderSurfaces()
{
if (_graphics.Target == null)
return [];
return [_graphics.Target];
}
protected override void OnSizeChanged(double pixelWidth, double pixelHeight, double dpr)
{
_graphics.CanvasSize = (Size: new PixelSize((int)pixelWidth, (int)pixelHeight), Scaling: dpr);
base.OnSizeChanged(pixelWidth, pixelHeight, dpr);
}
private RenderTargetBrowserSurface(JSObject jsSurface, InitParams init) : base(jsSurface, init.Compositor)
{
_graphics = init.Graphics;
base.Initialize();
}
class BrowserPlatformGraphics : IPlatformGraphicsWithFeatures, IPlatformGraphicsReadyStateFeature
{
private readonly int _targetId;
private BrowserRenderTarget? _target;
public BrowserPlatformGraphics(int targetId)
{
_targetId = targetId;
}
public BrowserRenderTarget? Target =>
_target ??= BrowserRenderTarget.GetRenderTarget(_targetId, () => CanvasSize);
public bool IsReady => Target != null && CanvasSize.Size != default;
public bool UsesContexts => Target!.PlatformGraphicsContext != null;
public bool UsesSharedContext => UsesContexts;
public (PixelSize Size, double Scaling) CanvasSize { get; set; }
public IPlatformGraphicsContext CreateContext() => throw new NotSupportedException();
public IPlatformGraphicsContext GetSharedContext() => Target!.PlatformGraphicsContext ??
throw new NotSupportedException(
"This platform graphics instance represents software rendering mode and cant create contexts, you are supposed to query IPlatformGraphicsReadyStateFeature to know this");
public object? TryGetFeature(Type featureType)
{
if (featureType == typeof(IPlatformGraphicsReadyStateFeature))
return this;
return null;
}
}
public override void Dispose()
{
// Technically this is a hack, but CompositionTarget should be gone at this point too
var c = Compositor;
Compositor.InvokeServerJobAsync(() =>
{
c.Loop.Remove(c.Server);
});
base.Dispose();
}
public static RenderTargetBrowserSurface Create(JSObject container, IReadOnlyList<BrowserRenderingMode> modes)
{
var js = CanvasHelper.CreateRenderTargetSurface(container, modes.Select(m => (int)m).ToArray(), RenderWorker.WorkerThreadId);
return new RenderTargetBrowserSurface(js);
}
}

140
src/Browser/Avalonia.Browser/Rendering/RenderWorker.cs

@ -0,0 +1,140 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
namespace Avalonia.Browser.Rendering;
public partial class RenderWorker
{
[DllImport("*")]
private static extern int pthread_self();
[JSImport("WebRenderTargetRegistry.initializeWorker", AvaloniaModule.MainModuleName)]
private static partial void InitializeRenderTargets();
internal static int WorkerThreadId;
public static Task InitializeAsync()
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var workerTask = JSWebWorkerClone.RunAsync(async () =>
{
try
{
await AvaloniaModule.ImportMainToCurrentContext();
InitializeRenderTargets();
WorkerThreadId = pthread_self();
BrowserSharedRenderLoop.RenderTimer.StartOnThisThread();
tcs.SetResult();
// Never surrender
await new TaskCompletionSource().Task;
}
catch (Exception e)
{
tcs.SetException(e);
}
});
workerTask.ContinueWith(_ =>
{
if (workerTask.IsFaulted)
tcs.TrySetException(workerTask.Exception);
});
return tcs.Task;
}
public static class JSWebWorkerClone
{
private static readonly MethodInfo _setExtLoop;
private static readonly MethodInfo _intallInterop;
[DynamicDependency(DynamicallyAccessedMemberTypes.All, "System.Runtime.InteropServices.JavaScript.JSSynchronizationContext",
"System.Runtime.InteropServices.JavaScript")]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, "System.Runtime.InteropServices.JavaScript.JSHostImplementation",
"System.Runtime.InteropServices.JavaScript")]
[UnconditionalSuppressMessage("Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "Private runtime API")]
static JSWebWorkerClone()
{
#pragma warning disable IL2075
var syncContext = typeof(System.Runtime.InteropServices.JavaScript.JSHost)
.Assembly!.GetType("System.Runtime.InteropServices.JavaScript.JSSynchronizationContext")!;
var hostImpl = typeof(System.Runtime.InteropServices.JavaScript.JSHost)
.Assembly!.GetType("System.Runtime.InteropServices.JavaScript.JSHostImplementation")!;
_setExtLoop = hostImpl.GetMethod("SetHasExternalEventLoop")!;
_intallInterop = syncContext.GetMethod("InstallWebWorkerInterop")!;
#pragma warning restore IL2075
}
public static Task RunAsync(Func<Task> run)
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var th = new Thread(_ =>
{
_intallInterop.Invoke(null, [false, CancellationToken.None]);
try
{
run().ContinueWith(t =>
{
if (t.IsFaulted)
tcs.TrySetException(t.Exception);
else if (t.IsCanceled)
tcs.TrySetCanceled();
else
tcs.TrySetResult();
});
}
catch(Exception e)
{
tcs.TrySetException(e);
}
})
{
Name = "Manual JS worker"
};
_setExtLoop.Invoke(null, [th]);
th.Start();
return tcs.Task;
}
}
// TODO: Use this class instead of JSWebWorkerClone once https://github.com/dotnet/runtime/issues/102010 is fixed
class JSWebWorkerWrapper
{
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker",
"System.Runtime.InteropServices.JavaScript")]
[UnconditionalSuppressMessage("Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "Private runtime API")]
static JSWebWorkerWrapper()
{
var type = typeof(System.Runtime.InteropServices.JavaScript.JSHost)
.Assembly!.GetType("System.Runtime.InteropServices.JavaScript.JSWebWorker");
#pragma warning disable IL2075
var m = type!
.GetMethods(BindingFlags.Static | BindingFlags.Public
).First(m => m.Name == "RunAsync"
&& m.ReturnType == typeof(Task)
&& m.GetParameters() is { } parameters
&& parameters.Length == 1
&& parameters[0].ParameterType == typeof(Func<Task>));
#pragma warning restore IL2075
RunAsync = (Func<Func<Task>, Task>) Delegate.CreateDelegate(typeof(Func<Func<Task>, Task>), m);
}
public static Func<Func<Task>, Task> RunAsync { get; set; }
}
}

43
src/Browser/Avalonia.Browser/Rendering/WebRenderTarget.cs

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using Avalonia.Browser.Interop;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Reactive;
using SkiaSharp;
namespace Avalonia.Browser.Rendering;
abstract partial class BrowserRenderTarget(JSObject js)
{
protected readonly JSObject Js = js;
[JSImport("WebRenderTargetRegistry.getRenderTarget", AvaloniaModule.MainModuleName)]
private static partial JSObject? GetJsRenderTarget(int id);
[JSImport("WebRenderTarget.setSize", AvaloniaModule.MainModuleName)]
private static partial void SetJsSize(JSObject target, int w, int h);
public static BrowserRenderTarget? GetRenderTarget(int id, Func<(PixelSize, double)> sizeGetter)
{
var js = GetJsRenderTarget(id);
if (js == null)
return null;
var type = js.GetPropertyAsString("renderTargetType");
if (type == "webgl")
return new BrowserWebGlRenderTarget(js, sizeGetter);
if (type == "software")
return new BrowserSoftwareRenderTarget(js, sizeGetter);
throw new NotSupportedException(type);
}
public abstract IPlatformGraphicsContext? PlatformGraphicsContext { get; }
protected void UpdateSize(PixelSize size)
{
SetJsSize(Js, size.Width, size.Height);
}
}

52
src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs

@ -1,52 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Browser.Rendering;
using Avalonia.Platform;
using Avalonia.Skia;
using Avalonia.Reactive;
namespace Avalonia.Browser.Skia
{
internal class BrowserSkiaGpu : ISkiaGpu
{
public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable<object> surfaces)
{
foreach (var surface in surfaces)
{
if (surface is BrowserGlSurface browserSkiaSurface)
{
return new BrowserSkiaGpuRenderTarget(browserSkiaSurface);
}
}
return null;
}
public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession? session)
{
return null;
}
public void Dispose()
{
}
public object? TryGetFeature(Type t) => null;
public bool IsLost => false;
public IDisposable EnsureCurrent()
{
return Disposable.Empty;
}
}
internal class BrowserSkiaGraphics : IPlatformGraphics
{
private BrowserSkiaGpu _skia = new();
public bool UsesSharedContext => true;
public IPlatformGraphicsContext CreateContext() => throw new NotSupportedException();
public IPlatformGraphicsContext GetSharedContext() => _skia;
}
}

40
src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderSession.cs

@ -1,40 +0,0 @@
using System;
using Avalonia.Browser.Rendering;
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Browser.Skia
{
internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession
{
private readonly SKSurface _surface;
public BrowserSkiaGpuRenderSession(BrowserGlSurface browserGlSurface, GRBackendRenderTarget renderTarget)
{
_surface = SKSurface.Create(browserGlSurface.Context, renderTarget, GRSurfaceOrigin.BottomLeft,
browserGlSurface.PixelFormat.ToSkColorType(), new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal))
?? throw new InvalidOperationException("Unable to create SKSurface.");
GrContext = browserGlSurface.Context;
ScaleFactor = browserGlSurface.Scaling;
SurfaceOrigin = GRSurfaceOrigin.BottomLeft;
browserGlSurface.EnsureResize();
}
public void Dispose()
{
_surface.Flush();
_surface.Dispose();
}
public GRContext GrContext { get; }
public SKSurface SkSurface => _surface;
public double ScaleFactor { get; }
public GRSurfaceOrigin SurfaceOrigin { get; }
}
}

39
src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs

@ -1,39 +0,0 @@
using System;
using Avalonia.Browser.Rendering;
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Browser.Skia
{
internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget
{
private readonly GRBackendRenderTarget _renderTarget;
private readonly BrowserGlSurface _browserGlSurface;
private readonly PixelSize _size;
public BrowserSkiaGpuRenderTarget(BrowserGlSurface browserGlSurface)
{
_size = browserGlSurface.RenderSize;
var glFbInfo = new GRGlFramebufferInfo(browserGlSurface.GlInfo.FboId, browserGlSurface.PixelFormat.ToSkColorType().ToGlSizedFormat());
_browserGlSurface = browserGlSurface;
_renderTarget = new GRBackendRenderTarget(
_size.Width,
_size.Height,
browserGlSurface.GlInfo.Samples,
browserGlSurface.GlInfo.Stencils, glFbInfo);
}
public void Dispose()
{
_renderTarget.Dispose();
}
public ISkiaGpuRenderSession BeginRenderingSession()
{
return new BrowserSkiaGpuRenderSession(_browserGlSurface, _renderTarget);
}
public bool IsCorrupted => _browserGlSurface.RenderSize != _size;
}
}

36
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@ -1,10 +1,14 @@
using System;
using System.Reflection;
using System.Threading;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Skia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
using Avalonia.Platform.Internal;
using Avalonia.Rendering;
using Avalonia.Threading;
@ -12,6 +16,30 @@ namespace Avalonia.Browser;
internal class BrowserWindowingPlatform : IWindowingPlatform
{
internal static readonly bool IsThreadingEnabled = DetectThreadSupport();
static bool DetectThreadSupport()
{
// TODO Replace with public API https://github.com/dotnet/runtime/issues/77541.
var prop = typeof(System.Threading.Thread).GetProperty("IsThreadStartSupported",
BindingFlags.Static | BindingFlags.NonPublic);
if (prop != null && prop.GetValue(null) is bool value)
return value;
// No property is found, try to start a thread and get an exception in the face if threads aren't available
try
{
#pragma warning disable CA1416
new Thread(() => { }).Start();
#pragma warning restore CA1416
return true;
}
catch
{
return false;
}
}
private static KeyboardDevice? s_keyboard;
public IWindowImpl CreateWindow() => throw new NotSupportedException("Browser doesn't support windowing platform. In order to display a single-view content, set ISingleViewApplicationLifetime.MainView.");
@ -39,13 +67,15 @@ internal class BrowserWindowingPlatform : IWindowingPlatform
.Bind<ICursorFactory>().ToSingleton<CssCursorFactory>()
.Bind<IKeyboardDevice>().ToConstant(s_keyboard)
.Bind<IPlatformSettings>().ToSingleton<BrowserPlatformSettings>()
.Bind<IDispatcherImpl>().ToSingleton<BrowserDispatcherImpl>()
.Bind<IRenderTimer>().ToConstant(ManualTriggerRenderTimer.Instance)
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformGraphics>().ToConstant(new BrowserSkiaGraphics())
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IActivatableLifetime>().ToSingleton<BrowserActivatableLifetime>();
AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToSingleton<BrowserDispatcherImpl>();
// GC thread is the same as the main one when MT is disabled
if (IsThreadingEnabled)
UnmanagedBlob.SuppressFinalizerWarning = true;
if (AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() is { } options
&& options.RegisterAvaloniaServiceWorker)

1
src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets

@ -6,6 +6,7 @@
<ItemGroup>
<!-- So we can access Emscripten APIs -->
<EmccExportedRuntimeMethod Include="GL" />
<EmccExportedRuntimeMethod Include="PThread" />
</ItemGroup>
<!-- Fallback for applications without StaticWebAssetsEnabled (legacy WASM SDK) -->

12
src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts

@ -6,7 +6,11 @@ import { NativeControlHost } from "./avalonia/nativeControlHost";
import { NavigationHelper } from "./avalonia/navigationHelper";
import { GeneralHelpers } from "./avalonia/generalHelpers";
import { TimerHelper } from "./avalonia/timer";
import { CanvasFactory } from "./avalonia/surfaces/surfaceFactory";
import { CanvasSurface } from "./avalonia/rendering/canvasSurface";
import { WebRenderTargetRegistry } from "./avalonia/rendering/webRenderTargetRegistry";
import { WebRenderTarget } from "./avalonia/rendering/webRenderTarget";
import { SoftwareRenderTarget } from "./avalonia/rendering/softwareRenderTarget";
import { WebGlRenderTarget } from "./avalonia/rendering/webGlRenderTarget";
async function registerServiceWorker(path: string, scope: string | undefined) {
if ("serviceWorker" in navigator) {
@ -16,7 +20,6 @@ async function registerServiceWorker(path: string, scope: string | undefined) {
export {
Caniuse,
CanvasFactory,
InputHelper,
AvaloniaDOM,
StreamHelper,
@ -24,5 +27,10 @@ export {
NavigationHelper,
GeneralHelpers,
TimerHelper,
WebRenderTarget,
CanvasSurface,
WebRenderTargetRegistry,
SoftwareRenderTarget,
WebGlRenderTarget,
registerServiceWorker
};

12
src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts

@ -0,0 +1,12 @@
export class JsExports {
public static resolvedExports?: any;
public static exportsPromise: Promise<any>;
}
async function resolveExports (): Promise<any> {
const runtimeApi = await globalThis.getDotnetRuntime(0);
if (runtimeApi == null) { return; }
JsExports.resolvedExports = await runtimeApi.getAssemblyExports("Avalonia.Browser.dll");
return JsExports.resolvedExports;
}
JsExports.exportsPromise = resolveExports();

66
src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts

@ -0,0 +1,66 @@
import { ResizeHandler } from "./resizeHandler";
import { WebRenderTargetRegistry } from "./webRenderTargetRegistry";
import { AvaloniaDOM } from "../dom";
import { BrowserRenderingMode } from "./renderingMode";
export class CanvasSurface {
public targetId: number;
private sizeParams?: [number, number, number];
private sizeChangedCallback?: (width: number, height: number, dpr: number) => void;
constructor(public canvas: HTMLCanvasElement, modes: BrowserRenderingMode[], threadId: number) {
this.targetId = WebRenderTargetRegistry.create(threadId, canvas, modes);
// No need to ubsubscribe, canvas never leaves JS world, it should be GC'ed with all callbacks.
ResizeHandler.observeSize(canvas, (width, height, dpr) => {
this.sizeParams = [width, height, dpr];
if (this.sizeChangedCallback) {
this.sizeChangedCallback(width, height, dpr);
}
});
}
public get width() {
if (this.sizeParams) { return this.sizeParams[0]; }
return 1;
}
public get height() {
if (this.sizeParams) { return this.sizeParams[1]; }
return 1;
}
public get scaling() {
if (this.sizeParams) { return this.sizeParams[2]; }
return 1;
}
public destroy(): void {
delete this.sizeChangedCallback;
}
public onSizeChanged(sizeChangedCallback: (width: number, height: number, dpr: number) => void) {
if (this.sizeChangedCallback) { throw new Error("For simplicity, we don't support multiple size changed callbacks per surface, not needed yet."); }
this.sizeChangedCallback = sizeChangedCallback;
// if (this.sizeParams) { this.sizeChangedCallback(this.sizeParams[0], this.sizeParams[1], this.sizeParams[2]); }
}
public static create(container: HTMLElement, modes: BrowserRenderingMode[], threadId: number): CanvasSurface {
const canvas = AvaloniaDOM.createAvaloniaCanvas(container);
AvaloniaDOM.attachCanvas(container, canvas);
try {
return new CanvasSurface(canvas, modes, threadId);
} catch (ex) {
AvaloniaDOM.detachCanvas(container, canvas);
throw ex;
}
}
public static destroy(surface: CanvasSurface) {
surface.destroy();
}
public static onSizeChanged(surface: CanvasSurface, sizeChangedCallback: (width: number, height: number, dpr: number) => void) {
surface.onSizeChanged(sizeChangedCallback);
}
}

5
src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/renderingMode.ts

@ -0,0 +1,5 @@
export enum BrowserRenderingMode {
Software2D = 1,
WebGL1,
WebGL2
}

0
src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/resizeHandler.ts → src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/resizeHandler.ts

44
src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/softwareRenderTarget.ts

@ -0,0 +1,44 @@
import { RuntimeAPI } from "../../../types/dotnet";
import { WebRenderTarget } from "./webRenderTarget";
export class SoftwareRenderTarget extends WebRenderTarget {
private readonly runtime: RuntimeAPI | undefined;
private readonly context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement | OffscreenCanvas) {
const context = canvas.getContext("2d", {
alpha: true
});
if (!context) {
throw new Error("HTMLCanvasElement.getContext(2d) returned null.");
}
super(canvas, "software");
this.context = context;
this.runtime = globalThis.getDotnetRuntime(0);
}
public putPixelData(pointer: number, length: number, width: number, height: number): void {
const heap8 = this.runtime?.localHeapViewU8();
let clampedBuffer: Uint8ClampedArray;
if (heap8?.buffer) {
clampedBuffer = new Uint8ClampedArray(heap8.buffer, pointer, length);
// Need to make a copy if using MT, ImageData can't consume shared arrays
if (this.canvas instanceof OffscreenCanvas) {
const dstArrayBuffer = new ArrayBuffer(clampedBuffer.byteLength);
const copy = new Uint8ClampedArray(dstArrayBuffer);
copy.set(clampedBuffer);
clampedBuffer = copy;
}
} else throw new Error("Unable to access .NET memory");
const imageData = new ImageData(clampedBuffer, width, height);
(this.context).putImageData(imageData, 0, 0);
}
public static staticPutPixelData(target: SoftwareRenderTarget, pointer: number, length: number, width: number, height: number): void {
target.putPixelData(pointer, length, width, height);
}
}

80
src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webGlRenderTarget.ts

@ -0,0 +1,80 @@
import { BrowserRenderingMode } from "./renderingMode";
import { WebRenderTarget } from "./webRenderTarget";
interface EmscriptenGlContext {
handle: number;
}
interface EmscriptenGL {
registerContext: (ctx: WebGLRenderingContext, attrs: WebGLContextAttributes) => number;
currentContext?: EmscriptenGlContext;
makeContextCurrent: (handle: number) => boolean;
}
function getGL(): EmscriptenGL {
const self = globalThis as any;
const module = self.Module ?? self.getDotnetRuntime(0)?.Module;
return (module?.GL ?? self.AvaloniaGL ?? self.SkiaSharpGL) as EmscriptenGL;
}
export class WebGlRenderTarget extends WebRenderTarget {
public contextHandle?: number;
public attrs: WebGLContextAttributes;
public fboId?: number;
public stencil?: number;
public sample?: number;
public depth?: number;
private static _gl: EmscriptenGL | null = null;
constructor(public canvas: HTMLCanvasElement | OffscreenCanvas, mode: BrowserRenderingMode) {
// Skia only understands WebGL context wrapped in Emscripten.
if (WebGlRenderTarget._gl == null) { WebGlRenderTarget._gl = getGL(); }
if (!WebGlRenderTarget._gl) {
throw new Error("Module.GL object wasn't initialized, WebGL can't be used.");
}
const attrs: WebGLContextAttributes | any =
{
alpha: true,
depth: true,
stencil: true,
antialias: false,
premultipliedAlpha: true,
preserveDrawingBuffer: false,
// only supported on older browsers, which is perfect as we want to fallback to 2d there.
failIfMajorPerformanceCaveat: true,
// attrs used by Emscripten:
majorVersion: mode === BrowserRenderingMode.WebGL1 ? 1 : 2,
minorVersion: 0,
enableExtensionsByDefault: 1,
explicitSwapControl: 0
};
const context = (mode === BrowserRenderingMode.WebGL1
? canvas.getContext("webgl", attrs)
: canvas.getContext("webgl2", attrs)) as WebGLRenderingContext;
if (!context) {
throw new Error("HTMLCanvasElement.getContext returned null.");
}
const handle = WebGlRenderTarget._gl.registerContext(context, attrs);
(context as any).gl_handle = handle;
super(canvas, "webgl");
this.contextHandle = handle;
this.fboId = context.getParameter(context.FRAMEBUFFER_BINDING)?.id ?? 0;
this.stencil = context.getParameter(context.STENCIL_BITS);
this.sample = context.getParameter(context.SAMPLES);
this.depth = context.getParameter(context.DEPTH_BITS);
this.attrs = attrs;
}
public static getCurrentContext(): number {
return WebGlRenderTarget._gl?.currentContext?.handle ?? 0;
}
public static makeContextCurrent(handle: number): boolean {
if (WebGlRenderTarget._gl == null) { return false; }
const ret = WebGlRenderTarget._gl.makeContextCurrent(handle);
return handle === 0 || ret;
}
}

11
src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webRenderTarget.ts

@ -0,0 +1,11 @@
export class WebRenderTarget {
renderTargetType: string;
constructor(protected canvas: HTMLCanvasElement | OffscreenCanvas, type: string) {
this.renderTargetType = type;
}
static setSize(target: WebRenderTarget, w: number, h: number) {
target.canvas.width = w;
target.canvas.height = h;
}
}

81
src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webRenderTargetRegistry.ts

@ -0,0 +1,81 @@
import { BrowserRenderingMode } from "./renderingMode";
import { WebGlRenderTarget } from "./webGlRenderTarget";
import { WebRenderTarget } from "./webRenderTarget";
import { SoftwareRenderTarget } from "./softwareRenderTarget";
export class WebRenderTargetRegistry {
private static targets: { [id: number]: (WebRenderTarget) } = {};
private static registry: { [id: number]: ({
canvas: HTMLCanvasElement;
worker?: Worker;
}); } = {};
private static nextId = 1;
static create(pthreadId: number, canvas: HTMLCanvasElement, preferredModes: BrowserRenderingMode[]): number {
const id = WebRenderTargetRegistry.nextId++;
if (pthreadId === 0) {
WebRenderTargetRegistry.registry[id] = {
canvas
};
WebRenderTargetRegistry.targets[id] = WebRenderTargetRegistry.createRenderTarget(canvas, preferredModes);
} else {
const self = globalThis as any;
const module = self.Module ?? self.getDotnetRuntime(0)?.Module;
const pthreads = module?.PThread;
if (pthreads == null) { throw new Error("Unable to access emscripten PThread api"); }
const pthread = pthreads.pthreads[pthreadId];
if (pthread == null) { throw new Error(`Unable get pthread with id ${pthreadId}`); }
let worker: Worker | undefined;
if (pthread.postMessage != null) { worker = pthread as Worker; } else { worker = pthread.worker; }
if (worker == null) { throw new Error(`Unable get Worker for pthread ${pthreadId}`); }
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({
avaloniaCmd: "registerCanvas",
canvas: offscreen,
modes: preferredModes,
id
}, [offscreen]);
WebRenderTargetRegistry.registry[id] = {
canvas,
worker
};
}
return id;
}
static initializeWorker() {
const oldHandler = self.onmessage;
self.onmessage = ev => {
const msg = ev;
if (msg.data.avaloniaCmd === "registerCanvas") {
WebRenderTargetRegistry.targets[msg.data.id] = WebRenderTargetRegistry.createRenderTarget(msg.data.canvas, msg.data.modes);
} else if (msg.data.avaloniaCmd === "unregisterCanvas") {
/* eslint-disable */
// Our keys are _always_ numbers and are safe to delete
delete WebRenderTargetRegistry.targets[msg.data.id];
/* eslint-enable */
} else if (oldHandler != null) { oldHandler.call(self, ev); }
};
}
static getRenderTarget(id: number): WebRenderTarget | undefined {
return WebRenderTargetRegistry.targets[id];
}
private static createRenderTarget(canvas: HTMLCanvasElement | OffscreenCanvas, modes: BrowserRenderingMode[]): WebRenderTarget {
for (const mode of modes) {
try {
if (mode === BrowserRenderingMode.Software2D) { return new SoftwareRenderTarget(canvas); }
return new WebGlRenderTarget(canvas, mode);
} catch (e) {
let message = "";
if (e instanceof Error) { message = ": " + e.message; }
console.error(`Failed to create render target for mode ${mode} ${message}`);
}
}
// Still try software as a fallback
return new SoftwareRenderTarget(canvas);
}
}

40
src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts

@ -1,40 +0,0 @@
import { ResizeHandler } from "./resizeHandler";
import { CanvasSurface, AvaloniaRenderingContext, BrowserRenderingMode } from "./surfaceBase";
export abstract class HtmlCanvasSurfaceBase extends CanvasSurface {
private sizeParams?: [number, number, number];
private sizeChangedCallback?: (width: number, height: number, dpr: number) => void;
constructor(
public canvas: HTMLCanvasElement,
public context: AvaloniaRenderingContext,
public mode: BrowserRenderingMode) {
super(context, mode);
// No need to ubsubsribe, canvas never leaves JS world, it should be GC'ed with all callbacks.
ResizeHandler.observeSize(canvas, (width, height, dpr) => {
this.sizeParams = [width, height, dpr];
if (this.sizeChangedCallback) {
this.sizeChangedCallback(width, height, dpr);
}
});
}
public destroy(): void {
delete this.sizeChangedCallback;
}
public onSizeChanged(sizeChangedCallback: (width: number, height: number, dpr: number) => void) {
if (this.sizeChangedCallback) { throw new Error("For simplicity, we don't support multiple size changed callbacks per surface, not needed yet."); }
this.sizeChangedCallback = sizeChangedCallback;
}
public ensureSize() {
if (this.sizeParams) {
this.canvas.width = this.sizeParams[0];
this.canvas.height = this.sizeParams[1];
delete this.sizeParams;
}
}
}

41
src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts

@ -1,41 +0,0 @@
import { BrowserRenderingMode } from "./surfaceBase";
import { HtmlCanvasSurfaceBase } from "./htmlSurfaceBase";
import { RuntimeAPI } from "../../../types/dotnet";
import { isSharedArrayBuffer } from "../stream";
export class SoftwareSurface extends HtmlCanvasSurfaceBase {
private readonly runtime: RuntimeAPI | undefined;
constructor(public canvas: HTMLCanvasElement) {
const context = canvas.getContext("2d", {
alpha: true
});
if (!context) {
throw new Error("HTMLCanvasElement.getContext(2d) returned null.");
}
super(canvas, context, BrowserRenderingMode.Software2D);
this.runtime = globalThis.getDotnetRuntime(0);
}
public putPixelData(span: any /* IMemoryView */, width: number, height: number): void {
this.ensureSize();
const heap8 = this.runtime?.localHeapViewU8();
let clampedBuffer: Uint8ClampedArray;
if (span._pointer > 0 && span._length > 0 && heap8 && !isSharedArrayBuffer(heap8.buffer)) {
// Attempt to use undocumented access to the HEAP8 directly
// Note, SharedArrayBuffer cannot be used with ImageData (when WasmEnableThreads = true).
clampedBuffer = new Uint8ClampedArray(heap8.buffer, span._pointer, span._length);
} else {
// Or fallback to the normal API that does multiple array copies.
const copy = new Uint8Array(span.byteLength);
span.copyTo(copy);
clampedBuffer = new Uint8ClampedArray(copy.buffer);
}
const imageData = new ImageData(clampedBuffer, width, height);
(this.context as CanvasRenderingContext2D).putImageData(imageData, 0, 0);
}
}

18
src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceBase.ts

@ -1,18 +0,0 @@
export type AvaloniaRenderingContext = RenderingContext;
export enum BrowserRenderingMode {
Software2D = 1,
WebGL1,
WebGL2
}
export abstract class CanvasSurface {
constructor(
public context: AvaloniaRenderingContext,
public mode: BrowserRenderingMode) {
}
abstract destroy(): void;
abstract ensureSize(): void;
abstract onSizeChanged(sizeChangedCallback: (width: number, height: number, dpr: number) => void): void;
}

44
src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceFactory.ts

@ -1,44 +0,0 @@
import { AvaloniaDOM } from "../dom";
import { SoftwareSurface } from "./softwareSurface";
import { BrowserRenderingMode, CanvasSurface } from "./surfaceBase";
import { WebGlSurface } from "./webGlSurface";
export class CanvasFactory {
public static create(container: HTMLElement, mode: BrowserRenderingMode): CanvasSurface {
if (!container) {
throw new Error("No html container was provided.");
}
const canvas = AvaloniaDOM.createAvaloniaCanvas(container);
AvaloniaDOM.attachCanvas(container, canvas);
try {
if (mode === BrowserRenderingMode.Software2D) {
return new SoftwareSurface(canvas);
} else if (mode === BrowserRenderingMode.WebGL1 || mode === BrowserRenderingMode.WebGL2) {
return new WebGlSurface(canvas, mode);
} else {
throw new Error(`Unsupported rendering mode: ${BrowserRenderingMode[mode]}`);
}
} catch (ex) {
AvaloniaDOM.detachCanvas(container, canvas);
throw ex;
}
}
public static destroy(surface: CanvasSurface) {
surface.destroy();
}
public static onSizeChanged(surface: CanvasSurface, sizeChangedCallback: (width: number, height: number, dpr: number) => void) {
surface.onSizeChanged(sizeChangedCallback);
}
public static ensureSize(surface: CanvasSurface): void {
surface.ensureSize();
}
public static putPixelData(surface: SoftwareSurface, span: any /* IMemoryView */, width: number, height: number): void {
surface.putPixelData(span, width, height);
}
}

58
src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webGlSurface.ts

@ -1,58 +0,0 @@
import { BrowserRenderingMode } from "./surfaceBase";
import { HtmlCanvasSurfaceBase } from "./htmlSurfaceBase";
function getGL(): any {
const self = globalThis as any;
const module = self.Module ?? self.getDotnetRuntime(0)?.Module;
return module?.GL ?? self.AvaloniaGL ?? self.SkiaSharpGL;
}
export class WebGlSurface extends HtmlCanvasSurfaceBase {
public contextHandle?: number;
public fboId?: number;
public stencil?: number;
public sample?: number;
public depth?: number;
constructor(public canvas: HTMLCanvasElement, mode: BrowserRenderingMode.WebGL1 | BrowserRenderingMode.WebGL2) {
// Skia only understands WebGL context wrapped in Emscripten.
const gl = getGL();
if (!gl) {
throw new Error("Module.GL object wasn't initialized, WebGL can't be used.");
}
const modeStr = mode === BrowserRenderingMode.WebGL1 ? "webgl" : "webgl2";
const attrs: WebGLContextAttributes | any =
{
alpha: true,
depth: true,
stencil: true,
antialias: false,
premultipliedAlpha: true,
preserveDrawingBuffer: false,
// only supported on older browsers, which is perfect as we want to fallback to 2d there.
failIfMajorPerformanceCaveat: true,
// attrs used by Emscripten:
majorVersion: mode === BrowserRenderingMode.WebGL1 ? 1 : 2,
minorVersion: 0,
enableExtensionsByDefault: 1,
explicitSwapControl: 0
};
const context = canvas.getContext(modeStr, attrs) as WebGLRenderingContext;
if (!context) {
throw new Error(`HTMLCanvasElement.getContext(${modeStr}) returned null.`);
}
const handle = gl.registerContext(context, attrs);
gl.makeContextCurrent(handle);
(context as any).gl_handle = handle;
super(canvas, context, BrowserRenderingMode.Software2D);
this.contextHandle = handle;
this.fboId = context.getParameter(context.FRAMEBUFFER_BINDING)?.id ?? 0;
this.stencil = context.getParameter(context.STENCIL_BITS);
this.sample = context.getParameter(context.SAMPLES);
this.depth = context.getParameter(context.DEPTH_BITS);
}
}

31
src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts

@ -1,12 +1,33 @@
import { JsExports } from "./jsExports";
export class TimerHelper {
public static runAnimationFrames(renderFrameCallback: (timestamp: number) => boolean): void {
public static runAnimationFrames(): void {
function render(time: number) {
const next = renderFrameCallback(time);
if (next) {
window.requestAnimationFrame(render);
if (JsExports.resolvedExports != null) {
JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnAnimationFrame(time);
}
self.requestAnimationFrame(render);
}
self.requestAnimationFrame(render);
}
static onTimeout() {
if (JsExports.resolvedExports != null) {
JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnTimeout();
} else { console.error("TimerHelper.onTimeout call while uninitialized"); }
}
static onInterval() {
if (JsExports.resolvedExports != null) {
JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnInterval();
} else { console.error("TimerHelper.onInterval call while uninitialized"); }
}
public static setTimeout(interval: number): number {
return setTimeout(TimerHelper.onTimeout, interval);
}
window.requestAnimationFrame(render);
public static setInterval(interval: number): number {
return setInterval(TimerHelper.onInterval, interval);
}
}

40
src/Browser/Avalonia.Browser/webapp/package-lock.json

@ -10,6 +10,7 @@
},
"devDependencies": {
"@types/emscripten": "^1.39.6",
"@types/offscreencanvas": "2019.7.1",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"esbuild": "^0.15.7",
"eslint": "^8.24.0",
@ -172,6 +173,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"node_modules/@types/offscreencanvas": {
"version": "2019.7.1",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.1.tgz",
"integrity": "sha512-+HSrJgjBW77ALieQdMJvXhRZUIRN1597L+BKvsyeiIlHHERnqjcuOLyodK3auJ3Y3zRezNKtKAhuQWYJfEgFHQ==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.38.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz",
@ -724,6 +731,30 @@
"integrity": "sha512-OnYr1rkMVxtmMHIAKZLMcEUlJmqcbxBz9QoBU8G9v455na0fuzlT/GLu6l+SRghrk0Mm2fSSciMmzV43Q8e0Gg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@esbuild/android-arm": "0.15.9",
"@esbuild/linux-loong64": "0.15.9",
"esbuild-android-64": "0.15.9",
"esbuild-android-arm64": "0.15.9",
"esbuild-darwin-64": "0.15.9",
"esbuild-darwin-arm64": "0.15.9",
"esbuild-freebsd-64": "0.15.9",
"esbuild-freebsd-arm64": "0.15.9",
"esbuild-linux-32": "0.15.9",
"esbuild-linux-64": "0.15.9",
"esbuild-linux-arm": "0.15.9",
"esbuild-linux-arm64": "0.15.9",
"esbuild-linux-mips64le": "0.15.9",
"esbuild-linux-ppc64le": "0.15.9",
"esbuild-linux-riscv64": "0.15.9",
"esbuild-linux-s390x": "0.15.9",
"esbuild-netbsd-64": "0.15.9",
"esbuild-openbsd-64": "0.15.9",
"esbuild-sunos-64": "0.15.9",
"esbuild-windows-32": "0.15.9",
"esbuild-windows-64": "0.15.9",
"esbuild-windows-arm64": "0.15.9"
},
"bin": {
"esbuild": "bin/esbuild"
},
@ -2322,6 +2353,9 @@
}
],
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.2.0"
},
"engines": {
"node": ">=14.8.0"
},
@ -3434,6 +3468,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"@types/offscreencanvas": {
"version": "2019.7.1",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.1.tgz",
"integrity": "sha512-+HSrJgjBW77ALieQdMJvXhRZUIRN1597L+BKvsyeiIlHHERnqjcuOLyodK3auJ3Y3zRezNKtKAhuQWYJfEgFHQ==",
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "5.38.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz",

1
src/Browser/Avalonia.Browser/webapp/package.json

@ -8,6 +8,7 @@
},
"devDependencies": {
"@types/emscripten": "^1.39.6",
"@types/offscreencanvas": "2019.7.1",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"esbuild": "^0.15.7",
"eslint": "^8.24.0",

16
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs

@ -26,9 +26,19 @@ namespace Avalonia.Skia
_glContext = context;
using (_glContext.EnsureCurrent())
{
using (var iface = context.Version.Type == GlProfileType.OpenGL ?
GRGlInterface.CreateOpenGl(proc => context.GlInterface.GetProcAddress(proc)) :
GRGlInterface.CreateGles(proc => context.GlInterface.GetProcAddress(proc)))
GRGlInterface iface;
if (context.TryGetFeature<IGlSkiaSpecificOptionsFeature>() is { } skiaOptions &&
skiaOptions.UseNativeSkiaGrGlInterface)
{
iface = GRGlInterface.Create();
}
else
iface = context.Version.Type == GlProfileType.OpenGL
? GRGlInterface.CreateOpenGl(proc => context.GlInterface.GetProcAddress(proc))
: GRGlInterface.CreateGles(proc => context.GlInterface.GetProcAddress(proc));
using(iface)
{
_grContext = GRContext.CreateGl(iface, new GRContextOptions { AvoidStencilBuffers = true });
if (maxResourceBytes.HasValue)

8
src/Skia/Avalonia.Skia/Gpu/OpenGl/IGlSkiaSpecificOptionsFeature.cs

@ -0,0 +1,8 @@
using Avalonia.Metadata;
namespace Avalonia.Skia;
[PrivateApi]
public interface IGlSkiaSpecificOptionsFeature
{
public bool UseNativeSkiaGrGlInterface { get; }
}
Loading…
Cancel
Save