Browse Source

Cleanup InvalidationAwareSurfaceView related code (#19045)

* Use NDK instead of C#-JNI for choreographer

* Mark IAndroidView as obsolete, it was replaced long time ago

* Minor cleanup in TopLevelImpl, remove unused methods

* Remove ancient renderer support with Invalidate/Paint calls, drastically simplify InvalidationAwareSurfaceView

* Some simple mitigation against of double release

* Implement ISurfaceHolderCallback2 and handle SurfaceRedrawNeeded

* Handle SurfaceRedrawNeededAsync as well
pull/19100/head
Max Katz 8 months ago
committed by GitHub
parent
commit
1a9463effd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 72
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  2. 18
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  3. 118
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  4. 87
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  5. 2
      src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs

72
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> _choreographer = new();
private readonly ISet<AvaloniaView> _views = new HashSet<AvaloniaView>();
private readonly TaskCompletionSource<IntPtr> _choreographer = new();
private readonly AutoResetEvent _event = new(false);
private readonly GCHandle _timerHandle;
private readonly HashSet<AvaloniaView> _views = new();
private Action<TimeSpan>? _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);
}
}
}
}

18
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<int, IntPtr, void> callback, IntPtr data);
[DllImport("android")]
[SupportedOSPlatform("android10.0")]
internal static extern void AChoreographer_postFrameCallback64(
IntPtr choreographer, delegate* unmanaged<long, IntPtr, void> 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.

118
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);
}
}
}
}

87
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<TopLevelImpl> _keyboardHelper;
private readonly AndroidMotionEventsHelper _pointerHelper;
private readonly AndroidInputMethod<ViewImpl> _textInputMethod;
@ -59,13 +56,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
_pointerHelper = new AndroidMotionEventsHelper(this);
_gl = new EglGlPlatformSurface(this);
_framebuffer = new FramebufferManager(this);
_clipboard = new ClipboardImpl(avaloniaView.Context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>());
_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<WindowTransparencyLevel> transparencyLevels)
{

2
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; }
}
}

Loading…
Cancel
Save