diff --git a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj index f223eb0725..309d54722c 100644 --- a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj +++ b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj @@ -5,6 +5,8 @@ wwwroot/main.js Exe true + true + 5 @@ -12,6 +14,13 @@ + + + + diff --git a/src/Avalonia.Base/Platform/IPlatformGpu.cs b/src/Avalonia.Base/Platform/IPlatformGpu.cs index f6953619d2..b9f11c3ebb 100644 --- a/src/Avalonia.Base/Platform/IPlatformGpu.cs +++ b/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 { diff --git a/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs b/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs index 92ec820b2c..9a7582ca5f 100644 --- a/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs +++ b/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) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index f2de59dda7..73d9b89504 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/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(); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 196bd88409..a25c958e4f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/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) diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index d6576511b9..db61ad84f1 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -11,16 +11,24 @@ internal class PlatformRenderInterfaceContextManager private readonly IPlatformGraphics? _graphics; private IPlatformRenderInterfaceContext? _backend; private OwnedDisposable? _gpuContext; + private readonly IPlatformGraphicsReadyStateFeature? _readyStateFeature; public event Action? ContextDisposed; public event Action? ContextCreated; public PlatformRenderInterfaceContextManager(IPlatformGraphics? graphics) { _graphics = graphics; + _readyStateFeature = (_graphics as IPlatformGraphicsWithFeatures) + ?.TryGetFeature(); } + 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(_graphics.GetSharedContext(), false); - else - _gpuContext = new OwnedDisposable(_graphics.CreateContext(), true); + if (_readyStateFeature?.UsesContexts != false) + { + if (_graphics.UsesSharedContext) + _gpuContext = + new OwnedDisposable(_graphics.GetSharedContext(), false); + else + _gpuContext = new OwnedDisposable(_graphics.CreateContext(), true); + } } _backend = AvaloniaLocator.Current.GetRequiredService() diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs index 950d07f207..02fa530fae 100644 --- a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs +++ b/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().ToConstant(options); await AvaloniaModule.ImportMain(); + if (BrowserWindowingPlatform.IsThreadingEnabled) + { + await RenderWorker.InitializeAsync(); + } if (builder.WindowingSubsystemInitializer is null) { diff --git a/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs b/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs index d7eaa81abf..2d3d031c9b 100644 --- a/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs +++ b/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); } } } diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 9e5742954b..dc997b840a 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/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(); 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() ?? 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 Surfaces { get; set; } + public IEnumerable Surfaces => _surface?.GetRenderSurfaces() ?? []; public Action? Input { get; set; } public Action? Paint { get; set; } diff --git a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs index b48a00919c..8e2d66b73e 100644 --- a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs +++ b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs @@ -6,11 +6,13 @@ namespace Avalonia.Browser.Interop; internal static partial class AvaloniaModule { - private static readonly Lazy s_importMain = new(() => + private static readonly Lazy s_importMain = new(ImportMainToCurrentContext); + + public static Task ImportMainToCurrentContext() { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js")); - }); + } private static readonly Lazy s_importStorage = new(() => { diff --git a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs index 331777191a..7274fa582f 100644 --- a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs +++ b/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>] // TODO: this callback should be . Revert after next .NET 9 preview. Action 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] ArraySegment data, int width, int height); } diff --git a/src/Browser/Avalonia.Browser/Interop/TimerHelper.cs b/src/Browser/Avalonia.Browser/Interop/TimerHelper.cs index 32c2f66565..b47b31076e 100644 --- a/src/Browser/Avalonia.Browser/Interop/TimerHelper.cs +++ b/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>] Func renderFrameCallback); + public static partial void RunAnimationFrames(); - [JSImport("globalThis.setTimeout")] - public static partial int SetTimeout([JSMarshalAs] Action callback, int intervalMs); + public static Action? 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] 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); diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserCompositor.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserCompositor.cs deleted file mode 100644 index a16b659a6e..0000000000 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserCompositor.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Rendering.Composition; - -namespace Avalonia.Browser.Rendering; - -/// -/// 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. -/// -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()); - - internal static Compositor SoftwareUiCompositor => s_softwareUiCompositor ??= new Compositor( - new RenderLoop(BrowserUiRenderTimer), null); -} diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserGlSurface.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserGlSurface.cs deleted file mode 100644 index 5cb65bafd8..0000000000 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserGlSurface.cs +++ /dev/null @@ -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(); - _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); - } -} diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserRasterSurface.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserRasterSurface.cs deleted file mode 100644 index 725d6241ac..0000000000 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserRasterSurface.cs +++ /dev/null @@ -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 s_pool = ArrayPool.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(_array, 0, length); - } - - public PixelSize Size { get; } - - public int RowBytes { get; } - - public IntPtr Address { get; } - - public ArraySegment AsSegment { get; } - - public void Dispose() - { - _handle.Free(); - s_pool.Return(_array); - } - } - - public IFramebufferRenderTarget CreateFramebufferRenderTarget() => new FuncFramebufferRenderTarget(Lock); -} diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs index 15f74e6585..fea94d0248 100644 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs +++ b/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? _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; } } diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs new file mode 100644 index 0000000000..8d454ff582 --- /dev/null +++ b/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 = new(() => new RenderLoop(RenderTimer), true); +} diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserSoftwareRenderTarget.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserSoftwareRenderTarget.cs new file mode 100644 index 0000000000..b3395e87d9 --- /dev/null +++ b/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 _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); + } +} \ No newline at end of file diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs index 8f73a67c77..9a4726481a 100644 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs +++ b/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() ?? 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]; } diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserWebGlRenderTarget.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserWebGlRenderTarget.cs new file mode 100644 index 0000000000..bbb713255b --- /dev/null +++ b/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? preferredVersions = null) => + throw new NotSupportedException(); + + public bool UseNativeSkiaGrGlInterface => true; +} \ No newline at end of file diff --git a/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs b/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs new file mode 100644 index 0000000000..c9fc086675 --- /dev/null +++ b/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 modes) + { + var js = CanvasHelper.CreateRenderTargetSurface(container, modes.Select(m => (int)m).ToArray(), RenderWorker.WorkerThreadId); + return new RenderTargetBrowserSurface(js); + } +} \ No newline at end of file diff --git a/src/Browser/Avalonia.Browser/Rendering/RenderWorker.cs b/src/Browser/Avalonia.Browser/Rendering/RenderWorker.cs new file mode 100644 index 0000000000..0a46b37ec1 --- /dev/null +++ b/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 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)); + +#pragma warning restore IL2075 + RunAsync = (Func, Task>) Delegate.CreateDelegate(typeof(Func, Task>), m); + + } + + public static Func, Task> RunAsync { get; set; } + } + +} \ No newline at end of file diff --git a/src/Browser/Avalonia.Browser/Rendering/WebRenderTarget.cs b/src/Browser/Avalonia.Browser/Rendering/WebRenderTarget.cs new file mode 100644 index 0000000000..b11f084bd2 --- /dev/null +++ b/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); + } +} diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs deleted file mode 100644 index cb8a912b8c..0000000000 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs +++ /dev/null @@ -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 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; - } -} diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderSession.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderSession.cs deleted file mode 100644 index b8fa21342c..0000000000 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderSession.cs +++ /dev/null @@ -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; } - } -} diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs deleted file mode 100644 index db4c5f2f8b..0000000000 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs +++ /dev/null @@ -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; - } -} diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index 27e5537431..e344db3d73 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/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().ToSingleton() .Bind().ToConstant(s_keyboard) .Bind().ToSingleton() - .Bind().ToSingleton() - .Bind().ToConstant(ManualTriggerRenderTimer.Instance) .Bind().ToConstant(instance) - .Bind().ToConstant(new BrowserSkiaGraphics()) .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToSingleton(); + AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + + // GC thread is the same as the main one when MT is disabled + if (IsThreadingEnabled) + UnmanagedBlob.SuppressFinalizerWarning = true; if (AvaloniaLocator.Current.GetService() is { } options && options.RegisterAvaloniaServiceWorker) diff --git a/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets b/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets index 53ad40168a..fe66c02dd8 100644 --- a/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets +++ b/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets @@ -6,6 +6,7 @@ + diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts index 7d7b64656c..c663b4c1b9 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts +++ b/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 }; diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts new file mode 100644 index 0000000000..18fde7449a --- /dev/null +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts @@ -0,0 +1,12 @@ +export class JsExports { + public static resolvedExports?: any; + public static exportsPromise: Promise; +} +async function resolveExports (): Promise { + const runtimeApi = await globalThis.getDotnetRuntime(0); + if (runtimeApi == null) { return; } + JsExports.resolvedExports = await runtimeApi.getAssemblyExports("Avalonia.Browser.dll"); + return JsExports.resolvedExports; +} + +JsExports.exportsPromise = resolveExports(); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts new file mode 100644 index 0000000000..35c4455b4b --- /dev/null +++ b/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); + } +} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/renderingMode.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/renderingMode.ts new file mode 100644 index 0000000000..6904d650cc --- /dev/null +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/renderingMode.ts @@ -0,0 +1,5 @@ +export enum BrowserRenderingMode { + Software2D = 1, + WebGL1, + WebGL2 +} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/resizeHandler.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/resizeHandler.ts similarity index 100% rename from src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/resizeHandler.ts rename to src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/resizeHandler.ts diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/softwareRenderTarget.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/softwareRenderTarget.ts new file mode 100644 index 0000000000..3b2d3c218c --- /dev/null +++ b/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); + } +} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webGlRenderTarget.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webGlRenderTarget.ts new file mode 100644 index 0000000000..5825f54c4b --- /dev/null +++ b/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; + } +} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webRenderTarget.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webRenderTarget.ts new file mode 100644 index 0000000000..c0cb8df9ca --- /dev/null +++ b/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; + } +} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webRenderTargetRegistry.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/webRenderTargetRegistry.ts new file mode 100644 index 0000000000..592ccfa2f2 --- /dev/null +++ b/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); + } +} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts deleted file mode 100644 index 30a6bbac03..0000000000 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts +++ /dev/null @@ -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; - } - } -} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts deleted file mode 100644 index 79c27493d3..0000000000 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts +++ /dev/null @@ -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); - } -} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceBase.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceBase.ts deleted file mode 100644 index 88cde86f37..0000000000 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceBase.ts +++ /dev/null @@ -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; -} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceFactory.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceFactory.ts deleted file mode 100644 index cb5810f5a4..0000000000 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceFactory.ts +++ /dev/null @@ -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); - } -} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webGlSurface.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webGlSurface.ts deleted file mode 100644 index 9da7f5bf8a..0000000000 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webGlSurface.ts +++ /dev/null @@ -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); - } -} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts index 7e52092056..3ccd669293 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts +++ b/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); } } diff --git a/src/Browser/Avalonia.Browser/webapp/package-lock.json b/src/Browser/Avalonia.Browser/webapp/package-lock.json index fd4c6a9539..cc1262fc47 100644 --- a/src/Browser/Avalonia.Browser/webapp/package-lock.json +++ b/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", diff --git a/src/Browser/Avalonia.Browser/webapp/package.json b/src/Browser/Avalonia.Browser/webapp/package.json index 20dd8d4e6a..065399492e 100644 --- a/src/Browser/Avalonia.Browser/webapp/package.json +++ b/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", diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 00752f8b2b..a44ebc1e39 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/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() 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) diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/IGlSkiaSpecificOptionsFeature.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/IGlSkiaSpecificOptionsFeature.cs new file mode 100644 index 0000000000..8737c8f34e --- /dev/null +++ b/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; } +} \ No newline at end of file