From 7a01250352dae7a8d2128508007e1c4576c5acf5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 3 Oct 2017 18:51:17 +0300 Subject: [PATCH 1/6] [GTK#] Burn in fire, fire, fire, you're no longer required --- appveyor.yml | 3 --- docs/guidelines/build.md | 6 ------ 2 files changed, 9 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index cef8c4a2cc..aa8c19ace4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,9 +16,7 @@ environment: init: - ps: if (Test-Path env:nuget_address) {[System.IO.File]::AppendAllText("C:\Windows\System32\drivers\etc\hosts", "`n$($env:nuget_address)`tapi.nuget.org")} install: - - if not exist gtk-sharp-2.12.26.msi appveyor DownloadFile http://download.xamarin.com/GTKforWindows/Windows/gtk-sharp-2.12.26.msi - if not exist dotnet-2.0.0.exe appveyor DownloadFile https://download.microsoft.com/download/0/F/D/0FD852A4-7EA1-4E2A-983A-0484AC19B92C/dotnet-sdk-2.0.0-win-x64.exe -FileName "dotnet-2.0.0.exe" - - ps: Start-Process -FilePath "msiexec" -ArgumentList "/i gtk-sharp-2.12.26.msi /quiet /qn /norestart" -Wait - ps: Start-Process -FilePath "dotnet-2.0.0.exe" -ArgumentList "/quiet" -Wait - cmd: set PATH=%programfiles(x86)%\GtkSharp\2.12\bin\;%PATH% before_build: @@ -36,5 +34,4 @@ artifacts: - path: artifacts\zip\*.zip - path: artifacts\inspectcode.xml cache: - - gtk-sharp-2.12.26.msi - dotnet-2.0.0.exe diff --git a/docs/guidelines/build.md b/docs/guidelines/build.md index 828a4ddab3..559790b197 100644 --- a/docs/guidelines/build.md +++ b/docs/guidelines/build.md @@ -4,12 +4,6 @@ Avalonia requires at least Visual Studio 2017 and .NET Core SDK 2.0 to build on Windows. -### Install GTK Sharp - -For the moment under windows, you must have [gtk-sharp](http://www.mono-project.com/download/#download-win) -installed. Note that after installing the package your machine may require a restart before GTK# is -added to your path. We hope to remove or make this dependency optional at some point in the future. - ### Clone the Avalonia repository ``` From 097716141399b0a27a34887e109981a7ff1e9b6c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 4 Oct 2017 11:23:48 +0300 Subject: [PATCH 2/6] Use AppDomain.GetLoadedAssemblies() for all platforms --- build/NetCore.props | 2 - .../NetCoreRuntimePlatform.cs | 42 ------------------- .../StandardRuntimePlatform.cs | 7 +--- 3 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 src/Avalonia.DotNetCoreRuntime/NetCoreRuntimePlatform.cs diff --git a/build/NetCore.props b/build/NetCore.props index cebb02c7f5..b9cde28015 100644 --- a/build/NetCore.props +++ b/build/NetCore.props @@ -1,6 +1,4 @@  - - diff --git a/src/Avalonia.DotNetCoreRuntime/NetCoreRuntimePlatform.cs b/src/Avalonia.DotNetCoreRuntime/NetCoreRuntimePlatform.cs deleted file mode 100644 index 6aab8e0243..0000000000 --- a/src/Avalonia.DotNetCoreRuntime/NetCoreRuntimePlatform.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Microsoft.DotNet.PlatformAbstractions; -using Microsoft.Extensions.DependencyModel; - -namespace Avalonia.Shared.PlatformSupport -{ - internal partial class StandardRuntimePlatform - { - private static readonly Lazy Assemblies = new Lazy(LoadAssemblies); - public Assembly[] GetLoadedAssemblies() => Assemblies.Value; - - static Assembly[] LoadAssemblies() - { - var assemblies = new List(); - // Mostly copy-pasted from (MIT): - // https://github.com/StefH/System.AppDomain.Core/blob/0b35e676c2721aa367b96e62eb52c97ee0b43a70/src/System.AppDomain.NetCoreApp/AppDomain.cs - - foreach (var assemblyName in - DependencyContext.Default.GetRuntimeAssemblyNames(RuntimeEnvironment.GetRuntimeIdentifier())) - { - try - { - var assembly = Assembly.Load(assemblyName); - // just load all types and skip this assembly if one or more types cannot be resolved - assembly.DefinedTypes.ToArray(); - assemblies.Add(assembly); - } - catch (Exception ex) - { - Debug.Write(ex.Message); - } - } - return assemblies.ToArray(); - } - } -} diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index 092910a08f..757cf52853 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -11,13 +11,8 @@ namespace Avalonia.Shared.PlatformSupport { internal partial class StandardRuntimePlatform : IRuntimePlatform { - -#if NETCOREAPP2_0 - public void PostThreadPoolItem(Action cb) => ThreadPool.QueueUserWorkItem(_ => cb(), null); -#else - public Assembly[] GetLoadedAssemblies() => AppDomain.CurrentDomain.GetAssemblies(); public void PostThreadPoolItem(Action cb) => ThreadPool.UnsafeQueueUserWorkItem(_ => cb(), null); -#endif + public Assembly[] GetLoadedAssemblies() => AppDomain.CurrentDomain.GetAssemblies(); public IDisposable StartSystemTimer(TimeSpan interval, Action tick) { return new Timer(_ => tick(), null, interval, interval); From b05f913e2501c47f6a42f8b9ddead69cb076723e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 4 Oct 2017 12:15:22 +0300 Subject: [PATCH 3/6] [GTK3] Normal priority for timers --- src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs b/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs index be886ea1c7..5253be5afb 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs @@ -53,7 +53,7 @@ namespace Avalonia.Gtk3.Interop if (interval == 0) throw new ArgumentException("Don't know how to create a timer with zero or negative interval"); var timer = new Timer (); - GlibTimeout.Add(GlibPriority.FromDispatcherPriority(DispatcherPriority.Background), interval, + GlibTimeout.Add(GlibPriority.FromDispatcherPriority(DispatcherPriority.Normal), interval, () => { if (timer.Stopped) From f6a60e27592d89e76129a5f47157d62a2981b34d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 4 Oct 2017 12:16:29 +0300 Subject: [PATCH 4/6] [GTK3] Block in SetNextRenderOperation until previous frame is dequeued --- src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index bfec3db756..8771106895 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using Avalonia.Controls; using Avalonia.Gtk3.Interop; using Avalonia.Input; @@ -29,6 +30,7 @@ namespace Avalonia.Gtk3 private GCHandle _gcHandle; private object _lock = new object(); private IDeferredRenderOperation _nextRenderOperation; + private readonly AutoResetEvent _canSetNextOperation = new AutoResetEvent(true); public WindowBaseImpl(GtkWindow gtkWidget) { @@ -255,11 +257,19 @@ namespace Avalonia.Gtk3 public void SetNextRenderOperation(IDeferredRenderOperation op) { - lock (_lock) + while (true) { - _nextRenderOperation?.Dispose(); - _nextRenderOperation = op; + lock (_lock) + { + if (_nextRenderOperation == null) + { + _nextRenderOperation = op; + return; + } + } + _canSetNextOperation.WaitOne(); } + } private void OnRenderTick() @@ -272,6 +282,7 @@ namespace Avalonia.Gtk3 op = _nextRenderOperation; _nextRenderOperation = null; } + _canSetNextOperation.Set(); } if (op != null) { From 29791fa241976caa9de681eddc6791669c48aa39 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 4 Oct 2017 15:27:18 +0300 Subject: [PATCH 5/6] Manual native memory management for leak detection --- .../Platform/IRuntimePlatform.cs | 9 ++ .../Avalonia.DotNetCoreRuntime.csproj | 3 +- .../Avalonia.Gtk3/ImageSurfaceFramebuffer.cs | 20 +-- .../Interop/ManagedCairoSurface.cs | 38 +++++ src/Gtk/Avalonia.Gtk3/Interop/Native.cs | 4 + .../StandardRuntimePlatform.cs | 132 +++++++++++++++++- src/Skia/Avalonia.Skia/BitmapImpl.cs | 13 ++ 7 files changed, 207 insertions(+), 12 deletions(-) create mode 100644 src/Gtk/Avalonia.Gtk3/Interop/ManagedCairoSurface.cs diff --git a/src/Avalonia.Base/Platform/IRuntimePlatform.cs b/src/Avalonia.Base/Platform/IRuntimePlatform.cs index e1a09f094d..66253dc5b2 100644 --- a/src/Avalonia.Base/Platform/IRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/IRuntimePlatform.cs @@ -14,6 +14,15 @@ namespace Avalonia.Platform IDisposable StartSystemTimer(TimeSpan interval, Action tick); string GetStackTrace(); RuntimePlatformInfo GetRuntimeInfo(); + IUnmanagedBlob AllocBlob(int size); + } + + public interface IUnmanagedBlob : IDisposable + { + IntPtr Address { get; } + int Size { get; } + bool IsDisposed { get; } + } public struct RuntimePlatformInfo diff --git a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj index eade213c4c..53b2c997d0 100644 --- a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj +++ b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj @@ -2,6 +2,7 @@ netcoreapp2.0 false + $(DefineConstants);DOTNETCORE bin\$(Configuration)\Avalonia.DotNetCoreRuntime.XML @@ -21,5 +22,5 @@ - + \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs index 182da7df58..4ebb080dd0 100644 --- a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs +++ b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs @@ -16,7 +16,7 @@ namespace Avalonia.Gtk3 { private readonly WindowBaseImpl _impl; private readonly GtkWidget _widget; - private CairoSurface _surface; + private ManagedCairoSurface _surface; private int _factor; private object _lock = new object(); public ImageSurfaceFramebuffer(WindowBaseImpl impl, int width, int height, int factor) @@ -26,13 +26,13 @@ namespace Avalonia.Gtk3 _factor = factor; width *= _factor; height *= _factor; - _surface = Native.CairoImageSurfaceCreate(1, width, height); + _surface = new ManagedCairoSurface(width, height); Width = width; Height = height; - Address = Native.CairoImageSurfaceGetData(_surface); - RowBytes = Native.CairoImageSurfaceGetStride(_surface); - Native.CairoSurfaceFlush(_surface); + Address = _surface.Buffer; + RowBytes = _surface.Stride; + Native.CairoSurfaceFlush(_surface.Surface); } static void Draw(IntPtr context, CairoSurface surface, double factor) @@ -83,12 +83,12 @@ namespace Avalonia.Gtk3 class RenderOp : IDeferredRenderOperation { private readonly GtkWidget _widget; - private CairoSurface _surface; + private ManagedCairoSurface _surface; private readonly double _factor; private readonly int _width; private readonly int _height; - public RenderOp(GtkWidget widget, CairoSurface _surface, double factor, int width, int height) + public RenderOp(GtkWidget widget, ManagedCairoSurface _surface, double factor, int width, int height) { _widget = widget; this._surface = _surface; @@ -105,7 +105,7 @@ namespace Avalonia.Gtk3 public void RenderNow() { - DrawToWidget(_widget, _surface, _width, _height, _factor); + DrawToWidget(_widget, _surface.Surface, _width, _height, _factor); } } @@ -116,9 +116,9 @@ namespace Avalonia.Gtk3 if (Dispatcher.UIThread.CheckAccess()) { if (_impl.CurrentCairoContext != IntPtr.Zero) - Draw(_impl.CurrentCairoContext, _surface, _factor); + Draw(_impl.CurrentCairoContext, _surface.Surface, _factor); else - DrawToWidget(_widget, _surface, Width, Height, _factor); + DrawToWidget(_widget, _surface.Surface, Width, Height, _factor); _surface.Dispose(); } else diff --git a/src/Gtk/Avalonia.Gtk3/Interop/ManagedCairoSurface.cs b/src/Gtk/Avalonia.Gtk3/Interop/ManagedCairoSurface.cs new file mode 100644 index 0000000000..2cde99afa5 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Interop/ManagedCairoSurface.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Platform; + +namespace Avalonia.Gtk3.Interop +{ + class ManagedCairoSurface : IDisposable + { + public IntPtr Buffer { get; private set; } + public CairoSurface Surface { get; private set; } + public int Stride { get; private set; } + private int _size; + private IRuntimePlatform _plat; + private IUnmanagedBlob _blob; + + public ManagedCairoSurface(int width, int height) + { + _plat = AvaloniaLocator.Current.GetService(); + Stride = width * 4; + _size = height * Stride; + _blob = _plat.AllocBlob(_size * 2); + Buffer = _blob.Address; + Surface = Native.CairoImageSurfaceCreateForData(Buffer, 1, width, height, Stride); + } + + public void Dispose() + { + + if (Buffer != IntPtr.Zero) + { + Surface.Dispose(); + _blob.Dispose(); + Buffer = IntPtr.Zero; + } + } + + } +} \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index c96fb23366..433094e0ae 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -160,6 +160,9 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Cairo)] public delegate CairoSurface cairo_image_surface_create(int format, int width, int height); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Cairo)] + public delegate CairoSurface cairo_image_surface_create_for_data(IntPtr data, int format, int width, int height, int stride); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Cairo)] public delegate IntPtr cairo_image_surface_get_data(CairoSurface surface); @@ -459,6 +462,7 @@ namespace Avalonia.Gtk3.Interop public static D.gdk_cairo_create GdkCairoCreate; public static D.cairo_image_surface_create CairoImageSurfaceCreate; + public static D.cairo_image_surface_create_for_data CairoImageSurfaceCreateForData; public static D.cairo_image_surface_get_data CairoImageSurfaceGetData; public static D.cairo_image_surface_get_stride CairoImageSurfaceGetStride; public static D.cairo_surface_mark_dirty CairoSurfaceMarkDirty; diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index 757cf52853..d648b630bd 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -2,8 +2,11 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Reflection; using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using Avalonia.Platform; @@ -18,8 +21,135 @@ namespace Avalonia.Shared.PlatformSupport return new Timer(_ => tick(), null, interval, interval); } + public string GetStackTrace() => Environment.StackTrace; + + public IUnmanagedBlob AllocBlob(int size) => new UnmanagedBlob(this, size); + + class UnmanagedBlob : IUnmanagedBlob + { + private readonly StandardRuntimePlatform _plat; +#if DEBUG + private static readonly List Backtraces = new List(); + private static Thread GCThread; + private readonly string _backtrace; - public string GetStackTrace() => Environment.StackTrace; + class GCThreadDetector + { + ~GCThreadDetector() + { + GCThread = Thread.CurrentThread; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void Spawn() => new GCThreadDetector(); + + static UnmanagedBlob() + { + Spawn(); + GC.WaitForPendingFinalizers(); + } + +#endif + + public UnmanagedBlob(StandardRuntimePlatform plat, int size) + { + _plat = plat; + Address = plat.Alloc(size); + GC.AddMemoryPressure(size); + Size = size; +#if DEBUG + _backtrace = Environment.StackTrace; + Backtraces.Add(_backtrace); +#endif + } + + void DoDispose() + { + if (!IsDisposed) + { + Backtraces.Remove(_backtrace); + _plat.Free(Address, Size); + GC.RemoveMemoryPressure(Size); + IsDisposed = true; + Address = IntPtr.Zero; + } + } + + public void Dispose() + { +#if DEBUG + if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId) + { + Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: " + + Environment.StackTrace + + "\n\nBlob created by " + _backtrace); + } +#endif + DoDispose(); + GC.SuppressFinalize(this); + } + + ~UnmanagedBlob() + { +#if DEBUG + Console.Error.WriteLine("Undisposed native blob created by " + _backtrace); +#endif + DoDispose(); + } + + public IntPtr Address { get; private set; } + public int Size { get; private set; } + public bool IsDisposed { get; private set; } + } + + + +#if FULLDOTNET || DOTNETCORE + [DllImport("libc", SetLastError = true)] + private static extern IntPtr mmap(IntPtr addr, IntPtr length, int prot, int flags, int fd, IntPtr offset); + [DllImport("libc", SetLastError = true)] + private static extern int munmap(IntPtr addr, IntPtr length); + [DllImport("libc", SetLastError = true)] + private static extern long sysconf(int name); + + private bool? _useMmap; + private bool UseMmap + => _useMmap ?? ((_useMmap = GetRuntimeInfo().OperatingSystem == OperatingSystemType.Linux)).Value; + + IntPtr Alloc(int size) + { + if (UseMmap) + { + var rv = mmap(IntPtr.Zero, new IntPtr(size), 3, 0x22, -1, IntPtr.Zero); + if (rv.ToInt64() == -1 || (ulong) rv.ToInt64() == 0xffffffff) + { + var errno = Marshal.GetLastWin32Error(); + throw new Exception("Unable to allocate memory: " + errno); + } + return rv; + } + else + return Marshal.AllocHGlobal(size); + } + + void Free(IntPtr ptr, int len) + { + if (UseMmap) + { + if (munmap(ptr, new IntPtr(len)) == -1) + { + var errno = Marshal.GetLastWin32Error(); + throw new Exception("Unable to free memory: " + errno); + } + } + else + Marshal.FreeHGlobal(ptr); + } +#else + IntPtr Alloc(int size) => Marshal.AllocHGlobal(size); + void Free(IntPtr ptr, int len) => Marshal.FreeHGlobal(ptr); +#endif } } \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs index ce3efded11..d13190deff 100644 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/BitmapImpl.cs @@ -20,6 +20,13 @@ namespace Avalonia.Skia _dpi = new Vector(96, 96); } + static void ReleaseProc(IntPtr address, object ctx) + { + ((IUnmanagedBlob) ctx).Dispose(); + } + + private static readonly SKBitmapReleaseDelegate ReleaseDelegate = ReleaseProc; + public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null) { PixelHeight = height; @@ -29,6 +36,12 @@ namespace Avalonia.Skia var runtime = AvaloniaLocator.Current?.GetService()?.GetRuntimeInfo(); if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux) colorType = SKColorType.Bgra8888; + + Bitmap = new SKBitmap(); + var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + var plat = AvaloniaLocator.Current.GetService(); + var blob = plat.AllocBlob(nfo.BytesSize); + Bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, ReleaseDelegate, blob); Bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); Bitmap.Erase(SKColor.Empty); } From 835bf9a7e5054a2078d01655c1546215d37cdb34 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 4 Oct 2017 15:45:37 +0300 Subject: [PATCH 6/6] Fixed release build --- src/Shared/PlatformSupport/StandardRuntimePlatform.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index d648b630bd..fafa17a810 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -69,7 +69,9 @@ namespace Avalonia.Shared.PlatformSupport { if (!IsDisposed) { +#if DEBUG Backtraces.Remove(_backtrace); +#endif _plat.Free(Address, Size); GC.RemoveMemoryPressure(Size); IsDisposed = true;