diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index 06ec9a9cd6..ce10f951c3 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -1,39 +1,41 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; - -using Android.OS; -using Android.Views; - using Avalonia.Reactive; using Avalonia.Rendering; -using ThreadPriority = System.Threading.ThreadPriority; +using static Avalonia.Android.Platform.SkiaPlatform.AndroidFramebuffer; + +using Looper = Android.OS.Looper; namespace Avalonia.Android { - internal sealed class ChoreographerTimer : Java.Lang.Object, IRenderTimer, Choreographer.IFrameCallback + internal sealed class ChoreographerTimer : IRenderTimer { + private static readonly bool s_supports64Callback = OperatingSystem.IsAndroidVersionAtLeast(10); private readonly object _lock = new(); - - private AutoResetEvent _event = new(false); - private long _lastTime; - private readonly TaskCompletionSource _choreographer = new(); - - private readonly ISet _views = new HashSet(); + private readonly TaskCompletionSource _choreographer = new(); + private readonly AutoResetEvent _event = new(false); + private readonly GCHandle _timerHandle; + private readonly HashSet _views = new(); private Action? _tick; + private long _lastTime; private int _count; public ChoreographerTimer() { + _timerHandle = GCHandle.Alloc(this); new Thread(Loop) { - Priority = ThreadPriority.AboveNormal + Priority = ThreadPriority.AboveNormal, + Name = "Choreographer Thread" }.Start(); new Thread(RenderLoop) { - Priority = ThreadPriority.AboveNormal + Priority = ThreadPriority.AboveNormal, + Name = "Render Thread" }.Start(); } @@ -50,7 +52,7 @@ namespace Avalonia.Android if (_count == 1) { - _choreographer.Task.Result.PostFrameCallback(this); + PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle)); } } } @@ -72,7 +74,7 @@ namespace Avalonia.Android if (_views.Count == 1) { - _choreographer.Task.Result.PostFrameCallback(this); + PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle)); } } @@ -90,10 +92,10 @@ namespace Avalonia.Android private void Loop() { Looper.Prepare(); - _choreographer.SetResult(Choreographer.Instance!); + _choreographer.SetResult(AChoreographer_getInstance()); Looper.Loop(); } - + private void RenderLoop() { while (true) @@ -108,18 +110,46 @@ namespace Avalonia.Android } } - - public void DoFrame(long frameTimeNanos) + private void DoFrameCallback(long frameTimeNanos, IntPtr data) { lock (_lock) { if (_count > 0 && _views.Count > 0) { - Choreographer.Instance!.PostFrameCallback(this); + PostFrameCallback(_choreographer.Task.Result, data); } _lastTime = frameTimeNanos; _event.Set(); } } + + private static unsafe void PostFrameCallback(IntPtr choreographer, IntPtr data) + { + // AChoreographer_postFrameCallback is deprecated on 10.0+. + if (s_supports64Callback) + { + AChoreographer_postFrameCallback64(choreographer, &FrameCallback64, data); + } + else + { + AChoreographer_postFrameCallback(choreographer, &FrameCallback, data); + } + + return; + + [UnmanagedCallersOnly] + static void FrameCallback(int frameTimeNanos, IntPtr data) + { + var timer = (ChoreographerTimer)GCHandle.FromIntPtr(data).Target!; + timer.DoFrameCallback(frameTimeNanos, data); + } + + [UnmanagedCallersOnly] + static void FrameCallback64(long frameTimeNanos, IntPtr data) + { + var timer = (ChoreographerTimer)GCHandle.FromIntPtr(data).Target!; + timer.DoFrameCallback(frameTimeNanos, data); + } + } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs index 7db13704ab..ef224d6c37 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using Android.Runtime; using Android.Views; using Avalonia.Platform; @@ -60,6 +61,19 @@ namespace Avalonia.Android.Platform.SkiaPlatform [DllImport("android")] internal static extern void ANativeWindow_unlockAndPost(IntPtr window); + [DllImport("android")] + internal static extern IntPtr AChoreographer_getInstance(); + + [DllImport("android")] + [UnsupportedOSPlatform("android10.0")] + internal static extern void AChoreographer_postFrameCallback( + IntPtr choreographer, delegate* unmanaged callback, IntPtr data); + + [DllImport("android")] + [SupportedOSPlatform("android10.0")] + internal static extern void AChoreographer_postFrameCallback64( + IntPtr choreographer, delegate* unmanaged callback, IntPtr data); + [DllImport("android")] internal static extern int ANativeWindow_lock(IntPtr window, ANativeWindow_Buffer* outBuffer, ARect* inOutDirtyBounds); public enum AndroidPixelFormat @@ -69,6 +83,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform WINDOW_FORMAT_RGB_565 = 4, } + [StructLayout(LayoutKind.Sequential)] internal struct ARect { public int left; @@ -76,7 +91,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform public int right; public int bottom; } - + + [StructLayout(LayoutKind.Sequential)] internal struct ANativeWindow_Buffer { // The number of pixels that are shown horizontally. diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 91b03a2aa5..f15322a6a7 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -1,30 +1,30 @@ using System; +using System.Threading; using Android.Content; using Android.Graphics; -using Android.OS; using Android.Runtime; -using Android.Util; using Android.Views; using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Logging; using Avalonia.Platform; +using Java.Lang; namespace Avalonia.Android { - internal abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, INativePlatformHandleSurface + internal abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback2, INativePlatformHandleSurface { - bool _invalidateQueued; - private bool _isDisposed; - private bool _isSurfaceValid; - readonly object _lock = new object(); - private readonly Handler _handler; + private IntPtr _nativeWindowHandle = IntPtr.Zero; + private PixelSize _size = new(1, 1); + private double _scaling = 1; - internal event EventHandler? SurfaceWindowCreated; + public event EventHandler? SurfaceWindowCreated; + public PixelSize Size => _size; + public double Scaling => _scaling; - IntPtr IPlatformHandle.Handle => _isSurfaceValid && Holder?.Surface?.Handle is { } handle ? - AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, handle) : - default; + IntPtr IPlatformHandle.Handle => _nativeWindowHandle; + string IPlatformHandle.HandleDescriptor => "SurfaceView"; - public InvalidationAwareSurfaceView(Context context) : base(context) + protected InvalidationAwareSurfaceView(Context context) : base(context) { if (Holder is null) throw new InvalidOperationException( @@ -32,71 +32,77 @@ namespace Avalonia.Android Holder.AddCallback(this); Holder.SetFormat(global::Android.Graphics.Format.Transparent); - _handler = new Handler(context.MainLooper!); } - public override void Invalidate() + protected override void Dispose(bool disposing) { - lock (_lock) - { - if (_invalidateQueued) - return; - _handler.Post(() => - { - if (_isDisposed || Holder?.Surface?.IsValid != true) - return; - try - { - DoDraw(); - } - catch (Exception e) - { - Log.WriteLine(LogPriority.Error, "Avalonia", e.ToString()); - } - }); - } - } - - internal new void Dispose() - { - _isDisposed = true; + ReleaseNativeWindowHandle(); + base.Dispose(disposing); } - public void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) + public virtual void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) { - _isSurfaceValid = true; - Log.Info("AVALONIA", "Surface Changed"); - DoDraw(); + CacheSurfaceProperties(holder); + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "InvalidationAwareSurfaceView Changed"); } public void SurfaceCreated(ISurfaceHolder holder) { - _isSurfaceValid = true; - Log.Info("AVALONIA", "Surface Created"); + CacheSurfaceProperties(holder); + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "InvalidationAwareSurfaceView Created"); SurfaceWindowCreated?.Invoke(this, EventArgs.Empty); - DoDraw(); } public void SurfaceDestroyed(ISurfaceHolder holder) { - _isSurfaceValid = false; - Log.Info("AVALONIA", "Surface Destroyed"); + ReleaseNativeWindowHandle(); + _size = new PixelSize(1, 1); + _scaling = 1; + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "InvalidationAwareSurfaceView Destroyed"); + } + public virtual void SurfaceRedrawNeeded(ISurfaceHolder holder) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "InvalidationAwareSurfaceView RedrawNeeded"); + } + + public virtual void SurfaceRedrawNeededAsync(ISurfaceHolder holder, IRunnable drawingFinished) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "InvalidationAwareSurfaceView RedrawNeededAsync"); } - protected void DoDraw() + private void CacheSurfaceProperties(ISurfaceHolder holder) { - lock (_lock) + var surface = holder?.Surface; + var newHandle = IntPtr.Zero; + if (surface?.Handle is { } handle) { - _invalidateQueued = false; + newHandle = AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, handle); } - Draw(); - } - protected abstract void Draw(); - public string HandleDescriptor => "SurfaceView"; - public PixelSize Size => new(Holder?.SurfaceFrame?.Width() ?? 1, Holder?.SurfaceFrame?.Height() ?? 1); + if (Interlocked.Exchange(ref _nativeWindowHandle, newHandle) is var oldHandle + && oldHandle != IntPtr.Zero) + { + AndroidFramebuffer.ANativeWindow_release(oldHandle); + } + + var frame = holder?.SurfaceFrame; + _size = frame != null ? new PixelSize(frame.Width(), frame.Height()) : new PixelSize(1, 1); + _scaling = Resources?.DisplayMetrics?.Density ?? 1; + } - public double Scaling => Resources?.DisplayMetrics?.Density ?? 1; + private void ReleaseNativeWindowHandle() + { + if (Interlocked.Exchange(ref _nativeWindowHandle, IntPtr.Zero) is var oldHandle + && oldHandle != IntPtr.Zero) + { + AndroidFramebuffer.ANativeWindow_release(oldHandle); + } + } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 26e1fe9ea0..00ae95abaf 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -32,9 +32,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform { class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy { - private readonly IGlPlatformSurface _gl; - private readonly IFramebufferPlatformSurface _framebuffer; - private readonly AndroidKeyboardEventsHelper _keyboardHelper; private readonly AndroidMotionEventsHelper _pointerHelper; private readonly AndroidInputMethod _textInputMethod; @@ -59,13 +56,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform _textInputMethod = new AndroidInputMethod(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); _pointerHelper = new AndroidMotionEventsHelper(this); - _gl = new EglGlPlatformSurface(this); - _framebuffer = new FramebufferManager(this); _clipboard = new ClipboardImpl(avaloniaView.Context.GetSystemService(Context.ClipboardService).JavaCast()); _screens = new AndroidScreens(avaloniaView.Context); - RenderScaling = _view.Scaling; - if (avaloniaView.Context is Activity mainActivity) { _insetsManager = new AndroidInsetsManager(mainActivity, this); @@ -78,15 +71,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform _systemNavigationManager = new AndroidSystemNavigationManagerImpl(avaloniaView.Context as IActivityNavigationService); - Surfaces = new object[] { _gl, _framebuffer, _view }; + var gl = new EglGlPlatformSurface(this); + var framebuffer = new FramebufferManager(this); + Surfaces = [gl, framebuffer, _view]; Handle = new AndroidViewControlHandle(_view); } public IInputRoot? InputRoot { get; private set; } - public virtual Size ClientSize => _view.Size.ToSize(RenderScaling); - - public Size? FrameSize => null; + public Size ClientSize => _view.Size.ToSize(RenderScaling); + public double RenderScaling => _view.Scaling; public Action? Closed { get; set; } @@ -110,16 +104,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform public Compositor Compositor => AndroidPlatform.Compositor ?? throw new InvalidOperationException("Android backend wasn't initialized. Make sure .UseAndroid() was executed."); - public virtual void Hide() - { - _view.Visibility = ViewStates.Invisible; - } - - public void Invalidate(Rect rect) - { - if (_view.Holder?.Surface?.IsValid == true) _view.Invalidate(); - } - public Point PointToClient(PixelPoint point) { return point.ToPoint(RenderScaling); @@ -140,18 +124,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform InputRoot = inputRoot; } - public virtual void Show() - { - _view.Visibility = ViewStates.Visible; - } - - public double RenderScaling { get; } - - void Draw() - { - Paint?.Invoke(new Rect(new Point(0, 0), ClientSize)); - } - public virtual void Dispose() { _systemNavigationManager.Dispose(); @@ -159,7 +131,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view = null!; } - protected virtual void OnResized(Size size) + protected void OnResized(Size size) { Resized?.Invoke(size, WindowResizeReason.Unspecified); } @@ -169,10 +141,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform Resized?.Invoke(size, WindowResizeReason.Layout); } - class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo + sealed class ViewImpl : InvalidationAwareSurfaceView, IInitEditorInfo { private readonly TopLevelImpl _tl; private Size _oldSize; + private double _oldScaling; public ViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context) { @@ -181,13 +154,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform SetZOrderOnTop(true); } - public TopLevelImpl TopLevelImpl => _tl; - - protected override void Draw() - { - _tl.Draw(); - } - protected override void DispatchDraw(global::Android.Graphics.Canvas canvas) { // Workaround issue #9230 on where screen remains gray after splash screen. @@ -234,20 +200,40 @@ namespace Avalonia.Android.Platform.SkiaPlatform return res ?? baseResult; } - void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) + public override void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) { - var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling); + base.SurfaceChanged(holder, format, width, height); + + var newSize = Size.ToSize(Scaling); + var newScaling = Scaling; if (newSize != _oldSize) { _oldSize = newSize; _tl.OnResized(newSize); } + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (newScaling != _oldScaling) + { + _oldScaling = newScaling; + _tl.ScalingChanged?.Invoke(newScaling); + } + } - base.SurfaceChanged(holder, format, width, height); + public override void SurfaceRedrawNeeded(ISurfaceHolder holder) + { + // Compositor Renderer handles Paint event in-sync, which is perfect for sync SurfaceRedrawNeeded + _tl.Paint?.Invoke(new Rect(new Point(), Size.ToSize(Scaling))); + base.SurfaceRedrawNeeded(holder); + } + + public override void SurfaceRedrawNeededAsync(ISurfaceHolder holder, IRunnable drawingFinished) + { + _tl.Compositor.RequestCompositionUpdate(drawingFinished.Run); + base.SurfaceRedrawNeededAsync(holder, drawingFinished); } - public sealed override bool OnCheckIsTextEditor() + public override bool OnCheckIsTextEditor() { return true; } @@ -259,11 +245,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform _initEditorInfo = init; } - public sealed override IInputConnection OnCreateInputConnection(EditorInfo? outAttrs) + public override IInputConnection OnCreateInputConnection(EditorInfo? outAttrs) { return _initEditorInfo?.Invoke(_tl, outAttrs!)!; } - } public IPopupImpl? CreatePopup() => null; @@ -303,10 +288,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; bool EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy.SkipWaits => true; - - public PixelSize Size => _view.Size; - - public double Scaling => RenderScaling; + PixelSize EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Size => _view.Size; + double EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Scaling => _view.Scaling; public void SetTransparencyLevelHint(IReadOnlyList transparencyLevels) { diff --git a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs index c72de8e197..056912835a 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs @@ -1,9 +1,11 @@ +using System; using Android.Views; namespace Avalonia.Android.Platform.Specific { public interface IAndroidView { + [Obsolete("Use TopLevel.TryGetPlatformHandle instead, which can be casted to AndroidViewControlHandle.")] View View { get; } } }