Browse Source
* 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
committed by
GitHub
47 changed files with 1107 additions and 630 deletions
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
|
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
export enum BrowserRenderingMode { |
|||
Software2D = 1, |
|||
WebGL1, |
|||
WebGL2 |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,8 @@ |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Skia; |
|||
[PrivateApi] |
|||
public interface IGlSkiaSpecificOptionsFeature |
|||
{ |
|||
public bool UseNativeSkiaGrGlInterface { get; } |
|||
} |
|||
Loading…
Reference in new issue