├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── rive-integration.yml └── workflows │ └── build.yml ├── .gitattributes ├── src ├── ImpellerSharp.Interop │ ├── Types │ │ ├── ImpellerPixelFormat.cs │ │ ├── ImpellerFillType.cs │ │ ├── ImpellerFontStyle.cs │ │ ├── ImpellerTextDirection.cs │ │ ├── ImpellerClipOperation.cs │ │ ├── ImpellerStrokeCap.cs │ │ ├── ImpellerTextureSampling.cs │ │ ├── ImpellerStrokeJoin.cs │ │ ├── ImpellerDrawStyle.cs │ │ ├── ImpellerColorSpace.cs │ │ ├── ImpellerTileMode.cs │ │ ├── ImpellerBlurStyle.cs │ │ ├── ImpellerTextDecorationStyle.cs │ │ ├── ImpellerTextAlignment.cs │ │ ├── ImpellerRange.cs │ │ ├── ImpellerTextDecorationType.cs │ │ ├── ImpellerFontWeight.cs │ │ ├── ImpellerMapping.cs │ │ ├── ImpellerPoint.cs │ │ ├── ImpellerISize.cs │ │ ├── ImpellerContextVulkanInfo.cs │ │ ├── ImpellerContextVulkanSettings.cs │ │ ├── ImpellerRect.cs │ │ ├── ImpellerTextureDescriptor.cs │ │ ├── ImpellerColor.cs │ │ ├── ImpellerTextDecoration.cs │ │ ├── ImpellerRoundingRadii.cs │ │ ├── ImpellerBlendMode.cs │ │ ├── ImpellerMatrix.cs │ │ └── ImpellerColorMatrix.cs │ ├── ImpellerLibrary.cs │ ├── ImpellerInteropException.cs │ ├── Utf8String.cs │ ├── ImpellerCallbacks.cs │ ├── ImpellerNative.Path.cs │ ├── Handles │ │ ├── ImpellerSafeHandle.cs │ │ ├── ImpellerDisplayListHandle.cs │ │ ├── ImpellerMaskFilterHandle.cs │ │ ├── ImpellerPathHandle.cs │ │ ├── ImpellerColorFilterHandle.cs │ │ ├── ImpellerFragmentProgramHandle.cs │ │ ├── ImpellerTypographyContextHandle.cs │ │ ├── ImpellerGlyphInfoHandle.cs │ │ ├── ImpellerVulkanSwapchainHandle.cs │ │ ├── ImpellerParagraphBuilderHandle.cs │ │ ├── ImpellerLineMetricsHandle.cs │ │ ├── ImpellerTextureHandle.cs │ │ ├── ImpellerParagraphHandle.cs │ │ ├── ImpellerPaintHandle.cs │ │ ├── ImpellerPathBuilderHandle.cs │ │ ├── ImpellerParagraphStyleHandle.cs │ │ ├── ImpellerContextHandle.cs │ │ └── ImpellerImageFilterHandle.cs │ ├── ImpellerNative.MaskFilter.cs │ ├── ImpellerDiagnostics.cs │ ├── ImpellerNative.FragmentProgram.cs │ ├── ImpellerNative.Vulkan.cs │ ├── ImpellerEventSource.cs │ ├── ImpellerNative.ColorFilter.cs │ ├── Hosting │ │ ├── MetalGlfwHostOptions.cs │ │ └── GlfwNative.cs │ ├── ImpellerSharp.Interop.csproj │ ├── ImpellerMappingUtilities.cs │ ├── ImpellerNative.Texture.cs │ ├── ImpellerInteropOptions.cs │ ├── ImpellerNative.GlyphInfo.cs │ ├── ImpellerNative.Surface.cs │ ├── ImpellerNative.Core.cs │ ├── ImpellerNative.ImageFilter.cs │ ├── ImpellerNative.Paint.cs │ ├── ImpellerNative.ColorSource.cs │ ├── ImpellerCommandPointers.cs │ ├── ImpellerNative.LineMetrics.cs │ ├── ImpellerNative.PathBuilder.cs │ └── TextureFactory.cs ├── ImpellerSharp.Avalonia.Linux │ ├── Controls │ │ └── LinuxImpellerView.cs │ └── ImpellerSharp.Avalonia.Linux.csproj ├── ImpellerSharp.Avalonia.Windows │ ├── Controls │ │ └── Win32ImpellerView.cs │ └── ImpellerSharp.Avalonia.Windows.csproj ├── ImpellerSharp.Native │ └── ImpellerSharp.Native.csproj ├── ImpellerSharp.Avalonia │ ├── ImpellerSharp.Avalonia.csproj │ └── Controls │ │ └── ImpellerSkiaView.cs └── ImpellerSharp.Avalonia.Mac │ ├── ImpellerSharp.Avalonia.Mac.csproj │ └── Controls │ └── MetalRenderEventArgs.cs ├── samples ├── AvaloniaImpellerApp │ ├── SceneKind.cs │ ├── App.axaml │ ├── Rendering │ │ └── IImpellerScene.cs │ ├── App.axaml.cs │ ├── Program.cs │ ├── AvaloniaImpellerApp.csproj │ └── MainWindow.axaml ├── BasicShapes │ ├── Scenes │ │ ├── SceneExecutionContext.cs │ │ ├── IScene.cs │ │ ├── SceneFactory.cs │ │ ├── TextureStreamingScene.cs │ │ └── TypographyScene.cs │ ├── BasicShapes.csproj │ ├── Program.cs │ ├── MacCapture.cs │ ├── VulkanSwapchainProbe.cs │ ├── BackendContextFactory.cs │ ├── VulkanLoader.cs │ ├── Program.mac.cs │ ├── ObjectiveCRuntime.cs │ └── HeadlessRunner.cs ├── MotionMarkOriginal │ ├── Program.cs │ └── MotionMarkOriginal.csproj └── MotionMark │ ├── Program.cs │ └── MotionMark.csproj ├── .gitmodules ├── benchmarks └── ImpellerSharp.Benchmarks │ ├── ImpellerSharp.Benchmarks.csproj │ ├── Program.cs │ ├── RectSequenceCache.cs │ ├── DisplayListBuilderBenchmark.cs │ ├── SurfacePresentBenchmark.cs │ ├── NativeDisplayListBenchmark.cs │ └── TextureUploadBenchmark.cs ├── docs ├── api-summary.md ├── README.md ├── compatibility-matrix.md ├── developer-docs-outline.md ├── sample-scenes.md ├── interop-parity-plan.md ├── benchmarking-plan.md ├── ci-plan.md └── developer-guide.md ├── Directory.Build.props └── LICENSE /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [wieslawsoltes] 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerPixelFormat.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | public enum ImpellerPixelFormat : int 3 | { 4 | Rgba8888 = 0, 5 | } 6 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerFillType.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerFillType : int 4 | { 5 | NonZero = 0, 6 | Odd = 1, 7 | } 8 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerFontStyle.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerFontStyle 4 | { 5 | Normal = 0, 6 | Italic = 1, 7 | } 8 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerLibrary.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | internal static class ImpellerLibrary 4 | { 5 | public const string Name = "impeller"; 6 | } 7 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerTextDirection.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerTextDirection 4 | { 5 | RightToLeft = 0, 6 | LeftToRight = 1, 7 | } 8 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerClipOperation.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerClipOperation : int 4 | { 5 | Difference = 0, 6 | Intersect = 1, 7 | } 8 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerStrokeCap.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerStrokeCap : int 4 | { 5 | Butt = 0, 6 | Round = 1, 7 | Square = 2, 8 | } 9 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerTextureSampling.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerTextureSampling 4 | { 5 | NearestNeighbor = 0, 6 | Linear = 1, 7 | } 8 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerStrokeJoin.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerStrokeJoin : int 4 | { 5 | Miter = 0, 6 | Round = 1, 7 | Bevel = 2, 8 | } 9 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerDrawStyle.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerDrawStyle : int 4 | { 5 | Fill = 0, 6 | Stroke = 1, 7 | StrokeAndFill = 2, 8 | } 9 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerColorSpace.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerColorSpace : int 4 | { 5 | SRgb = 0, 6 | ExtendedSRgb = 1, 7 | DisplayP3 = 2, 8 | } 9 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerTileMode.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerTileMode 4 | { 5 | Clamp = 0, 6 | Repeat = 1, 7 | Mirror = 2, 8 | Decal = 3, 9 | } 10 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerBlurStyle.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerBlurStyle : int 4 | { 5 | Normal = 0, 6 | Solid = 1, 7 | Outer = 2, 8 | Inner = 3, 9 | } 10 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerTextDecorationStyle.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerTextDecorationStyle 4 | { 5 | Solid = 0, 6 | Double = 1, 7 | Dotted = 2, 8 | Dashed = 3, 9 | Wavy = 4, 10 | } 11 | -------------------------------------------------------------------------------- /samples/AvaloniaImpellerApp/SceneKind.cs: -------------------------------------------------------------------------------- 1 | namespace AvaloniaImpellerApp; 2 | 3 | public enum SceneKind 4 | { 5 | BasicDemo, 6 | GradientGallery, 7 | Typography, 8 | VectorPaths, 9 | LayerEffects, 10 | BlendModes, 11 | MotionMark 12 | } 13 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerTextAlignment.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerTextAlignment 4 | { 5 | Left = 0, 6 | Right = 1, 7 | Center = 2, 8 | Justify = 3, 9 | Start = 4, 10 | End = 5, 11 | } 12 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerRange.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | public struct ImpellerRange 7 | { 8 | public ulong Start; 9 | public ulong End; 10 | } 11 | -------------------------------------------------------------------------------- /samples/BasicShapes/Scenes/SceneExecutionContext.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Samples.BasicShapes.Scenes; 2 | 3 | internal static class SceneExecutionContext 4 | { 5 | internal static bool Headless { get; set; } 6 | 7 | internal static string? LastVulkanSwapchainProbe { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerInteropException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed class ImpellerInteropException : Exception 6 | { 7 | public ImpellerInteropException(string message) 8 | : base(message) 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerTextDecorationType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | [Flags] 6 | public enum ImpellerTextDecorationType 7 | { 8 | None = 0, 9 | Underline = 1 << 0, 10 | Overline = 1 << 1, 11 | LineThrough = 1 << 2, 12 | } 13 | -------------------------------------------------------------------------------- /samples/AvaloniaImpellerApp/App.axaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerFontWeight.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerFontWeight 4 | { 5 | W100 = 0, 6 | W200 = 1, 7 | W300 = 2, 8 | W400 = 3, 9 | W500 = 4, 10 | W600 = 5, 11 | W700 = 6, 12 | W800 = 7, 13 | W900 = 8, 14 | } 15 | -------------------------------------------------------------------------------- /samples/AvaloniaImpellerApp/Rendering/IImpellerScene.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImpellerSharp.Interop; 3 | 4 | namespace AvaloniaImpellerApp.Rendering; 5 | 6 | internal interface IImpellerScene : IDisposable 7 | { 8 | bool Render(ImpellerDisplayListBuilderHandle builder, float width, float height); 9 | } 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "extern/flutter"] 2 | path = extern/flutter 3 | url = https://github.com/flutter/flutter.git 4 | [submodule "extern/depot_tools"] 5 | path = extern/depot_tools 6 | url = https://chromium.googlesource.com/chromium/tools/depot_tools.git 7 | [submodule "extern/rive"] 8 | path = extern/rive 9 | url = https://github.com/rive-app/rive-cpp.git 10 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerMapping.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal unsafe struct ImpellerMapping 8 | { 9 | public byte* Data; 10 | public ulong Length; 11 | public delegate* unmanaged[Cdecl] OnRelease; 12 | } 13 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerPoint.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | public struct ImpellerPoint 7 | { 8 | public float X; 9 | public float Y; 10 | 11 | public ImpellerPoint(float x, float y) 12 | { 13 | X = x; 14 | Y = y; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Avalonia.Linux/Controls/LinuxImpellerView.cs: -------------------------------------------------------------------------------- 1 | using ImpellerSharp.Avalonia.Controls; 2 | 3 | namespace ImpellerSharp.Avalonia.Linux.Controls; 4 | 5 | /// 6 | /// Linux-oriented Impeller view leveraging the Skia lease integration. 7 | /// 8 | public sealed class LinuxImpellerView : ImpellerSkiaView 9 | { 10 | public LinuxImpellerView() 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Avalonia.Windows/Controls/Win32ImpellerView.cs: -------------------------------------------------------------------------------- 1 | using ImpellerSharp.Avalonia.Controls; 2 | 3 | namespace ImpellerSharp.Avalonia.Windows.Controls; 4 | 5 | /// 6 | /// Windows-friendly Impeller view that inherits the Skia lease plumbing. 7 | /// 8 | public sealed class Win32ImpellerView : ImpellerSkiaView 9 | { 10 | public Win32ImpellerView() 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerISize.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | public struct ImpellerISize 7 | { 8 | public long Width; 9 | public long Height; 10 | 11 | public ImpellerISize(long width, long height) 12 | { 13 | Width = width; 14 | Height = height; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/BasicShapes/Scenes/IScene.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImpellerSharp.Interop; 3 | 4 | namespace ImpellerSharp.Samples.BasicShapes.Scenes; 5 | 6 | internal interface IScene : IDisposable 7 | { 8 | string Name { get; } 9 | 10 | void Initialize(ImpellerContextHandle context); 11 | 12 | ImpellerDisplayListHandle CreateDisplayList(ImpellerContextHandle context, int frameIndex); 13 | 14 | string DescribeFrame(int frameIndex); 15 | } 16 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerContextVulkanInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | public struct ImpellerContextVulkanInfo 8 | { 9 | public IntPtr Instance; 10 | public IntPtr PhysicalDevice; 11 | public IntPtr LogicalDevice; 12 | public uint GraphicsQueueFamilyIndex; 13 | public uint GraphicsQueueIndex; 14 | } 15 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerContextVulkanSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | public struct ImpellerContextVulkanSettings 8 | { 9 | public IntPtr UserData; 10 | public ImpellerVulkanProcAddressCallback ProcAddressCallback; 11 | 12 | [MarshalAs(UnmanagedType.I1)] 13 | public bool EnableValidation; 14 | } 15 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerRect.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | public struct ImpellerRect 7 | { 8 | public float X; 9 | public float Y; 10 | public float Width; 11 | public float Height; 12 | 13 | public ImpellerRect(float x, float y, float width, float height) 14 | { 15 | X = x; 16 | Y = y; 17 | Width = width; 18 | Height = height; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Utf8String.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | internal readonly struct Utf8String : IDisposable 7 | { 8 | public IntPtr Pointer { get; } 9 | 10 | public Utf8String(string? value) 11 | { 12 | Pointer = value is null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8(value); 13 | } 14 | 15 | public void Dispose() 16 | { 17 | if (Pointer != IntPtr.Zero) 18 | { 19 | Marshal.FreeCoTaskMem(Pointer); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerCallbacks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 7 | public delegate IntPtr ImpellerProcAddressCallback( 8 | [MarshalAs(UnmanagedType.LPUTF8Str)] string procName, 9 | IntPtr userData); 10 | 11 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 12 | public delegate IntPtr ImpellerVulkanProcAddressCallback( 13 | IntPtr vulkanInstance, 14 | [MarshalAs(UnmanagedType.LPUTF8Str)] string procName, 15 | IntPtr userData); 16 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerTextureDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | public struct ImpellerTextureDescriptor 7 | { 8 | public ImpellerPixelFormat PixelFormat; 9 | public ImpellerISize Size; 10 | public uint MipCount; 11 | 12 | public ImpellerTextureDescriptor(ImpellerPixelFormat pixelFormat, ImpellerISize size, uint mipCount = 1) 13 | { 14 | PixelFormat = pixelFormat; 15 | Size = size; 16 | MipCount = mipCount; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/AvaloniaImpellerApp/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace AvaloniaImpellerApp; 6 | 7 | public partial class App : Application 8 | { 9 | public override void Initialize() 10 | { 11 | AvaloniaXamlLoader.Load(this); 12 | } 13 | 14 | public override void OnFrameworkInitializationCompleted() 15 | { 16 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 17 | { 18 | desktop.MainWindow = new MainWindow(); 19 | } 20 | 21 | base.OnFrameworkInitializationCompleted(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/AvaloniaImpellerApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | 3 | namespace AvaloniaImpellerApp; 4 | 5 | internal static class Program 6 | { 7 | [STAThread] 8 | public static void Main(string[] args) => BuildAvaloniaApp() 9 | .StartWithClassicDesktopLifetime(args); 10 | 11 | public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() 12 | .UsePlatformDetect() 13 | .UseSkia() 14 | .With(new AvaloniaNativePlatformOptions() 15 | { 16 | RenderingMode = 17 | [ 18 | AvaloniaNativeRenderingMode.Metal 19 | ] 20 | }) 21 | .LogToTrace(); 22 | } 23 | -------------------------------------------------------------------------------- /benchmarks/ImpellerSharp.Benchmarks/ImpellerSharp.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net8.0 5 | enable 6 | enable 7 | true 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerColor.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | public struct ImpellerColor 7 | { 8 | public float Red; 9 | public float Green; 10 | public float Blue; 11 | public float Alpha; 12 | public ImpellerColorSpace ColorSpace; 13 | 14 | public ImpellerColor(float red, float green, float blue, float alpha, ImpellerColorSpace colorSpace = ImpellerColorSpace.SRgb) 15 | { 16 | Red = red; 17 | Green = green; 18 | Blue = blue; 19 | Alpha = alpha; 20 | ColorSpace = colorSpace; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerTextDecoration.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | public struct ImpellerTextDecoration 7 | { 8 | public ImpellerTextDecorationType Types; 9 | public ImpellerColor Color; 10 | public ImpellerTextDecorationStyle Style; 11 | public float ThicknessMultiplier; 12 | 13 | public static ImpellerTextDecoration None => new() 14 | { 15 | Types = ImpellerTextDecorationType.None, 16 | Color = new ImpellerColor(0f, 0f, 0f, 0f), 17 | Style = ImpellerTextDecorationStyle.Solid, 18 | ThicknessMultiplier = 1f, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /docs/api-summary.md: -------------------------------------------------------------------------------- 1 | # Impeller Interop API Coverage 2 | 3 | _Generated on 2025-11-06T08:09:55Z by `build/scripts/generate_api_summary.py`._ 4 | 5 | - Header inspected: `extern/flutter/engine/src/flutter/impeller/toolkit/interop/impeller.h` 6 | - Managed bindings: `src/ImpellerSharp.Interop` 7 | 8 | | Metric | Count | 9 | | --- | --- | 10 | | Native exports in header | 176 | 11 | | Managed P/Invoke entry points | 177 | 12 | | Shared exports (bound) | 176 | 13 | | Missing managed bindings | 0 | 14 | | Extra managed bindings | 1 | 15 | | Coverage | 100.00% | 16 | 17 | ## Missing Managed Bindings 18 | 19 | _None_ 20 | 21 | ## Extra Managed Bindings (no native export) 22 | 23 | - `ImpellerSurfaceCreateWrappedMetalTextureNew` 24 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerRoundingRadii.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | public struct ImpellerRoundingRadii 7 | { 8 | public ImpellerPoint TopLeft; 9 | public ImpellerPoint BottomLeft; 10 | public ImpellerPoint TopRight; 11 | public ImpellerPoint BottomRight; 12 | 13 | public static ImpellerRoundingRadii Uniform(float radius) 14 | { 15 | var point = new ImpellerPoint(radius, radius); 16 | return new ImpellerRoundingRadii 17 | { 18 | TopLeft = point, 19 | BottomLeft = point, 20 | TopRight = point, 21 | BottomRight = point, 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.Path.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathRetain")] 8 | [SuppressGCTransition] 9 | internal static partial void ImpellerPathRetain(nint path); 10 | 11 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathRelease")] 12 | [SuppressGCTransition] 13 | internal static partial void ImpellerPathRelease(nint path); 14 | 15 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathGetBounds")] 16 | [SuppressGCTransition] 17 | internal static unsafe partial void ImpellerPathGetBounds(nint path, ImpellerRect* outBounds); 18 | } 19 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerSafeHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | public abstract class ImpellerSafeHandle : SafeHandle 7 | { 8 | protected ImpellerSafeHandle() 9 | : base(IntPtr.Zero, ownsHandle: true) 10 | { 11 | } 12 | 13 | protected ImpellerSafeHandle(bool ownsHandle) 14 | : base(IntPtr.Zero, ownsHandle) 15 | { 16 | } 17 | 18 | public override bool IsInvalid => handle == IntPtr.Zero; 19 | 20 | protected static nint EnsureSuccess(nint native, string message) 21 | { 22 | if (native == nint.Zero) 23 | { 24 | throw new ImpellerInteropException(message); 25 | } 26 | 27 | return native; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0.1.0 4 | https://github.com/yourname/ImpellerSharp 5 | https://github.com/yourname/ImpellerSharp 6 | ImpellerSharp Contributors 7 | MIT 8 | false 9 | $(MSBuildThisFileDirectory)artifacts/native 10 | 11 | 12 | 13 | $(ImpellerSharpVersion) 14 | $(ImpellerSharpVersion) 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | This directory contains planning and reference material for ImpellerSharp. 4 | 5 | ## Roadmaps 6 | - `impeller-interop-plan.md` – Overall milestones (interop, benchmarks, CI, docs). 7 | - `compatibility-matrix.md` – Target platforms/backends. 8 | 9 | ## Performance 10 | - `benchmarking-plan.md` – Benchmark scenarios, profiling workflow, stress tests. 11 | 12 | ## Tooling & CI 13 | - `ci-plan.md` – Proposed GitHub Actions pipeline for native + managed builds. 14 | 15 | ## Samples & Guides 16 | - `sample-scenes.md` – Planned demo applications. 17 | - `developer-docs-outline.md` – Structure for the developer guide. 18 | - `developer-guide.md` – current consolidated guide. 19 | - `samples/AvaloniaImpellerApp` – Avalonia sample using NuGet packages (see README for run instructions). 20 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerBlendMode.cs: -------------------------------------------------------------------------------- 1 | namespace ImpellerSharp.Interop; 2 | 3 | public enum ImpellerBlendMode : int 4 | { 5 | Clear = 0, 6 | Source = 1, 7 | Destination = 2, 8 | SourceOver = 3, 9 | DestinationOver = 4, 10 | SourceIn = 5, 11 | DestinationIn = 6, 12 | SourceOut = 7, 13 | DestinationOut = 8, 14 | SourceATop = 9, 15 | DestinationATop = 10, 16 | Xor = 11, 17 | Plus = 12, 18 | Modulate = 13, 19 | Screen = 14, 20 | Overlay = 15, 21 | Darken = 16, 22 | Lighten = 17, 23 | ColorDodge = 18, 24 | ColorBurn = 19, 25 | HardLight = 20, 26 | SoftLight = 21, 27 | Difference = 22, 28 | Exclusion = 23, 29 | Multiply = 24, 30 | Hue = 25, 31 | Saturation = 26, 32 | Color = 27, 33 | Luminosity = 28, 34 | } 35 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.MaskFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerMaskFilterRetain")] 8 | [SuppressGCTransition] 9 | internal static partial void ImpellerMaskFilterRetain(nint maskFilter); 10 | 11 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerMaskFilterRelease")] 12 | [SuppressGCTransition] 13 | internal static partial void ImpellerMaskFilterRelease(nint maskFilter); 14 | 15 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerMaskFilterCreateBlurNew")] 16 | [SuppressGCTransition] 17 | internal static partial nint ImpellerMaskFilterCreateBlurNew(ImpellerBlurStyle style, float sigma); 18 | } 19 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static class ImpellerDiagnostics 6 | { 7 | internal static readonly ActivitySource ActivitySource = new("ImpellerSharp.Interop"); 8 | 9 | internal static void ContextCreated(string backend, uint version) 10 | { 11 | ImpellerEventSource.Log.ContextCreated(backend, version); 12 | } 13 | 14 | internal static void TextureCreated(in ImpellerTextureDescriptor descriptor) 15 | { 16 | ImpellerEventSource.Log.TextureCreated(descriptor.Size.Width, descriptor.Size.Height, (int)descriptor.PixelFormat); 17 | } 18 | 19 | internal static void SurfaceDrawDisplayList(bool success) 20 | { 21 | ImpellerEventSource.Log.SurfaceDrawDisplayList(success); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.FragmentProgram.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerFragmentProgramNew")] 8 | [SuppressGCTransition] 9 | internal static unsafe partial nint ImpellerFragmentProgramNew( 10 | in ImpellerMapping mapping, 11 | nint userData); 12 | 13 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerFragmentProgramRetain")] 14 | [SuppressGCTransition] 15 | internal static partial void ImpellerFragmentProgramRetain(nint fragmentProgram); 16 | 17 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerFragmentProgramRelease")] 18 | [SuppressGCTransition] 19 | internal static partial void ImpellerFragmentProgramRelease(nint fragmentProgram); 20 | } 21 | -------------------------------------------------------------------------------- /benchmarks/ImpellerSharp.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using BenchmarkDotNet.Configs; 4 | using BenchmarkDotNet.Exporters; 5 | using BenchmarkDotNet.Exporters.Json; 6 | using BenchmarkDotNet.Running; 7 | 8 | namespace ImpellerSharp.Benchmarks; 9 | 10 | internal static class Program 11 | { 12 | private static void Main(string[] args) 13 | { 14 | var artifactsPath = Path.Combine(AppContext.BaseDirectory, "BenchmarkArtifacts"); 15 | Directory.CreateDirectory(artifactsPath); 16 | 17 | var config = ManualConfig 18 | .Create(DefaultConfig.Instance) 19 | .WithArtifactsPath(artifactsPath) 20 | .AddExporter(MarkdownExporter.GitHub, JsonExporter.Default) 21 | .WithOption(ConfigOptions.DisableOptimizationsValidator, true); 22 | 23 | BenchmarkSwitcher 24 | .FromAssembly(typeof(Program).Assembly) 25 | .Run(args, config); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/BasicShapes/Scenes/SceneFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ImpellerSharp.Samples.BasicShapes.Scenes; 5 | 6 | internal static class SceneFactory 7 | { 8 | private static readonly Dictionary> SceneResolvers = new(StringComparer.OrdinalIgnoreCase) 9 | { 10 | ["rects"] = () => new RectGridScene(), 11 | ["stream"] = () => new TextureStreamingScene(), 12 | ["typography"] = () => new TypographyScene(), 13 | }; 14 | 15 | internal static IScene Create(string name) 16 | { 17 | if (SceneResolvers.TryGetValue(name, out var factory)) 18 | { 19 | return factory(); 20 | } 21 | 22 | throw new ArgumentException( 23 | $"Unknown scene '{name}'. Available scenes: {string.Join(", ", SceneResolvers.Keys)}", 24 | nameof(name)); 25 | } 26 | 27 | internal static IReadOnlyCollection Scenes => SceneResolvers.Keys; 28 | } 29 | -------------------------------------------------------------------------------- /samples/MotionMarkOriginal/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImpellerSharp.Interop.Hosting; 3 | 4 | namespace ImpellerSharp.Samples.MotionMarkOriginal; 5 | 6 | internal static class Program 7 | { 8 | private static int Main() 9 | { 10 | if (!OperatingSystem.IsMacOS()) 11 | { 12 | Console.Error.WriteLine("MotionMarkOriginal sample currently supports macOS Metal backends only."); 13 | return 1; 14 | } 15 | 16 | using var simulation = new OriginalMotionMarkSimulation(); 17 | simulation.SetComplexity(12); 18 | 19 | var options = new MetalGlfwHostOptions 20 | { 21 | Title = "Impeller MotionMark Classic (Metal)", 22 | Width = 1280, 23 | Height = 720, 24 | ErrorLogger = message => Console.Error.WriteLine(message), 25 | }; 26 | 27 | using var host = new MetalGlfwAppHost(options); 28 | return host.Run((builder, width, height) => simulation.Render(builder, width, height)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Native/ImpellerSharp.Native.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | false 5 | disable 6 | true 7 | false 8 | false 9 | ImpellerSharp.Native 10 | Native Impeller binaries packaged for ImpellerSharp. 11 | impeller;rendering;gpu;native 12 | README.md 13 | NU5128 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /samples/MotionMark/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using AvaloniaImpellerApp.Rendering; 3 | using ImpellerSharp.Interop.Hosting; 4 | 5 | namespace ImpellerSharp.Samples.MotionMark; 6 | 7 | internal static class Program 8 | { 9 | private static int Main() 10 | { 11 | if (!OperatingSystem.IsMacOS()) 12 | { 13 | Console.Error.WriteLine("MotionMark sample currently supports macOS Metal backends only."); 14 | return 1; 15 | } 16 | 17 | using var simulation = new MotionMarkSimulation(); 18 | simulation.SetComplexity(9); 19 | 20 | var options = new MetalGlfwHostOptions 21 | { 22 | Title = "Impeller MotionMark (Metal)", 23 | Width = 1280, 24 | Height = 720, 25 | ErrorLogger = message => Console.Error.WriteLine(message), 26 | }; 27 | 28 | using var host = new MetalGlfwAppHost(options); 29 | return host.Run((builder, width, height) => simulation.Render(builder, width, height)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/compatibility-matrix.md: -------------------------------------------------------------------------------- 1 | # Compatibility Matrix (M6 Task 6.4) 2 | 3 | | Platform | Backend | Native Binary | Managed Runtime | Status | Notes | 4 | | --- | --- | --- | --- | --- | --- | 5 | | macOS 14 (arm64) | Metal | libimpeller.dylib | .NET 8 (CoreCLR) | Planned | Primary dev environment | 6 | | macOS 14 (arm64) | Vulkan (MoltenVK) | libimpeller_vulkan.dylib | .NET 8 | Planned | Requires MoltenVK install | 7 | | Windows 11 (x64) | Vulkan | impeller.dll | .NET 8 | Planned | Needs GN Windows build | 8 | | Linux (Ubuntu 22.04) | Vulkan | libimpeller.so | .NET 8 | Planned | CI target | 9 | | Windows 11 (x64) | OpenGL ES (ANGLE) | impeller.dll | .NET 8 | Stretch | Evaluate demand | 10 | | macOS 14 (arm64) | Metal | libimpeller.dylib | .NET NativeAOT | Planned | Packaging considerations | 11 | | Windows/Linux | Vulkan | impeller.* | .NET NativeAOT | Planned | Validate P/Invoke compatibility | 12 | | macOS/Windows | Metal/Vulkan | -- | Unity (Mono) | Stretch | Requires IL2CPP bindings | 13 | 14 | Legend: Planned = roadmap target; Stretch = optional future support. 15 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Avalonia/ImpellerSharp.Avalonia.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | enable 5 | enable 6 | true 7 | ImpellerSharp.Avalonia 8 | ImpellerSharp.Avalonia 9 | ImpellerSharp.Avalonia 10 | Avalonia controls and helpers for hosting Impeller. 11 | impeller;avalonia;rendering;gpu 12 | README.md 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.Vulkan.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerVulkanSwapchainCreateNew")] 8 | [SuppressGCTransition] 9 | internal static partial nint ImpellerVulkanSwapchainCreateNew(nint context, nint vulkanSurface); 10 | 11 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerVulkanSwapchainRetain")] 12 | [SuppressGCTransition] 13 | internal static partial void ImpellerVulkanSwapchainRetain(nint swapchain); 14 | 15 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerVulkanSwapchainRelease")] 16 | [SuppressGCTransition] 17 | internal static partial void ImpellerVulkanSwapchainRelease(nint swapchain); 18 | 19 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerVulkanSwapchainAcquireNextSurfaceNew")] 20 | [SuppressGCTransition] 21 | internal static partial nint ImpellerVulkanSwapchainAcquireNextSurfaceNew(nint swapchain); 22 | } 23 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerEventSource.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Tracing; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | [EventSource(Name = "ImpellerSharp.Interop")] 6 | internal sealed class ImpellerEventSource : EventSource 7 | { 8 | internal static readonly ImpellerEventSource Log = new(); 9 | 10 | private ImpellerEventSource() 11 | { 12 | } 13 | 14 | [Event(1, Level = EventLevel.Informational, Message = "Context created: {0}, version {1}")] 15 | public void ContextCreated(string backend, uint version) 16 | { 17 | WriteEvent(1, backend, version); 18 | } 19 | 20 | [Event(2, Level = EventLevel.Informational, Message = "Texture created {0}x{1}, format {2}")] 21 | public void TextureCreated(long width, long height, int pixelFormat) 22 | { 23 | WriteEvent(2, width, height, pixelFormat); 24 | } 25 | 26 | [Event(3, Level = EventLevel.Informational, Message = "Surface draw display list success={0}")] 27 | public void SurfaceDrawDisplayList(bool success) 28 | { 29 | WriteEvent(3, success); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/rive-integration.yml: -------------------------------------------------------------------------------- 1 | name: Rive Integration Checklist 2 | description: Track progress for Rive native integration tasks. 3 | title: "Rive integration tracking - " 4 | labels: ["tracking", "rive"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Use this issue to monitor the Rive integration plan. Check off items as they land. 10 | - type: checkboxes 11 | id: workstreams 12 | attributes: 13 | label: Workstream status 14 | options: 15 | - label: Workstream A – Native build tooling complete 16 | - label: Workstream B – Packaging scripts and helpers complete 17 | - label: Workstream C – CI coverage (native + managed + packaging) 18 | - label: Workstream D – Release automation 19 | - label: Workstream E – README modernization 20 | - label: Workstream F – Tracking & governance 21 | - type: textarea 22 | id: notes 23 | attributes: 24 | label: Notes / next actions 25 | placeholder: Optional notes, blockers, or follow-up tasks. 26 | render: markdown 27 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerMatrix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | public unsafe struct ImpellerMatrix 8 | { 9 | private fixed float _m[16]; 10 | 11 | public Span AsSpan() 12 | { 13 | fixed (float* ptr = _m) 14 | { 15 | return new Span(ptr, 16); 16 | } 17 | } 18 | 19 | public void Set(ReadOnlySpan values) 20 | { 21 | if (values.Length != 16) 22 | { 23 | throw new ArgumentException("Matrix requires exactly 16 values.", nameof(values)); 24 | } 25 | 26 | values.CopyTo(AsSpan()); 27 | } 28 | 29 | public static ImpellerMatrix Identity 30 | { 31 | get 32 | { 33 | var matrix = new ImpellerMatrix(); 34 | var span = matrix.AsSpan(); 35 | span.Clear(); 36 | span[0] = 1f; 37 | span[5] = 1f; 38 | span[10] = 1f; 39 | span[15] = 1f; 40 | return matrix; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Wiesław Šoltés 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.ColorFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorFilterRetain")] 8 | [SuppressGCTransition] 9 | internal static partial void ImpellerColorFilterRetain(nint colorFilter); 10 | 11 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorFilterRelease")] 12 | [SuppressGCTransition] 13 | internal static partial void ImpellerColorFilterRelease(nint colorFilter); 14 | 15 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorFilterCreateBlendNew")] 16 | [SuppressGCTransition] 17 | internal static unsafe partial nint ImpellerColorFilterCreateBlendNew( 18 | ImpellerColor* color, 19 | ImpellerBlendMode blendMode); 20 | 21 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorFilterCreateColorMatrixNew")] 22 | [SuppressGCTransition] 23 | internal static unsafe partial nint ImpellerColorFilterCreateColorMatrixNew( 24 | ImpellerColorMatrix* colorMatrix); 25 | } 26 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Avalonia.Linux/ImpellerSharp.Avalonia.Linux.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | enable 5 | enable 6 | true 7 | ImpellerSharp.Avalonia.Linux 8 | ImpellerSharp.Avalonia.Linux 9 | ImpellerSharp.Avalonia.Linux 10 | Linux helpers for Impeller Avalonia hosts (Wayland/X11). 11 | impeller;avalonia;linux 12 | README.md 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Avalonia.Mac/ImpellerSharp.Avalonia.Mac.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | enable 5 | enable 6 | true 7 | ImpellerSharp.Avalonia.Mac 8 | ImpellerSharp.Avalonia.Mac 9 | ImpellerSharp.Avalonia.Mac 10 | macOS native host controls for ImpellerSharp Avalonia integrations. 11 | impeller;avalonia;macos;metal 12 | README.md 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Avalonia.Windows/ImpellerSharp.Avalonia.Windows.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | enable 5 | enable 6 | true 7 | ImpellerSharp.Avalonia.Windows 8 | ImpellerSharp.Avalonia.Windows 9 | ImpellerSharp.Avalonia.Windows 10 | Windows-specific helpers for hosting Impeller in Avalonia. 11 | impeller;avalonia;windows 12 | README.md 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerDisplayListHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed class ImpellerDisplayListHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerDisplayListHandle(nint native) 8 | { 9 | SetHandle(native); 10 | } 11 | 12 | internal static ImpellerDisplayListHandle FromOwned(nint native) 13 | { 14 | return new ImpellerDisplayListHandle( 15 | EnsureSuccess(native, "Failed to create Impeller display list.")); 16 | } 17 | 18 | internal void Retain() 19 | { 20 | if (IsInvalid) 21 | { 22 | throw new ObjectDisposedException(nameof(ImpellerDisplayListHandle)); 23 | } 24 | 25 | ImpellerNative.ImpellerDisplayListRetain(handle); 26 | } 27 | 28 | protected override bool ReleaseHandle() 29 | { 30 | ImpellerNative.ImpellerDisplayListRelease(handle); 31 | return true; 32 | } 33 | 34 | internal new nint DangerousGetHandle() 35 | { 36 | if (IsInvalid) 37 | { 38 | throw new ObjectDisposedException(nameof(ImpellerDisplayListHandle)); 39 | } 40 | 41 | return handle; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Hosting/MetalGlfwHostOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop.Hosting; 4 | 5 | /// 6 | /// Options used to configure . 7 | /// 8 | public sealed class MetalGlfwHostOptions 9 | { 10 | /// 11 | /// Gets or sets the window title. 12 | /// 13 | public string Title { get; set; } = "Impeller Application"; 14 | 15 | /// 16 | /// Gets or sets the initial window width in logical pixels. 17 | /// 18 | public int Width { get; set; } = 1280; 19 | 20 | /// 21 | /// Gets or sets the initial window height in logical pixels. 22 | /// 23 | public int Height { get; set; } = 720; 24 | 25 | /// 26 | /// Gets or sets a value indicating whether the window should be visible. 27 | /// Defaults to true. 28 | /// 29 | public bool Visible { get; set; } = true; 30 | 31 | /// 32 | /// Gets or sets a callback invoked when GLFW reports an error. 33 | /// Defaults to writing to . 34 | /// 35 | public Action? ErrorLogger { get; set; } 36 | } 37 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Types/ImpellerColorMatrix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | public unsafe struct ImpellerColorMatrix 8 | { 9 | public const int ElementCount = 20; 10 | 11 | private fixed float _m[ElementCount]; 12 | 13 | public Span AsSpan() 14 | { 15 | fixed (float* ptr = _m) 16 | { 17 | return new Span(ptr, ElementCount); 18 | } 19 | } 20 | 21 | public void Set(ReadOnlySpan values) 22 | { 23 | if (values.Length != ElementCount) 24 | { 25 | throw new ArgumentException("Color matrix requires exactly 20 values.", nameof(values)); 26 | } 27 | 28 | values.CopyTo(AsSpan()); 29 | } 30 | 31 | public static ImpellerColorMatrix Identity 32 | { 33 | get 34 | { 35 | var matrix = new ImpellerColorMatrix(); 36 | var span = matrix.AsSpan(); 37 | span.Clear(); 38 | span[0] = 1f; 39 | span[6] = 1f; 40 | span[12] = 1f; 41 | span[18] = 1f; 42 | return matrix; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Avalonia.Mac/Controls/MetalRenderEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | 4 | namespace ImpellerSharp.Avalonia.Mac.Controls; 5 | 6 | public sealed class MetalRenderEventArgs 7 | { 8 | internal MetalRenderEventArgs( 9 | nint view, 10 | nint layer, 11 | nint drawable, 12 | nint texture, 13 | nint device, 14 | PixelSize pixelSize, 15 | double scaling) 16 | { 17 | View = view; 18 | Layer = layer; 19 | Drawable = drawable; 20 | Texture = texture; 21 | Device = device; 22 | PixelSize = pixelSize; 23 | Scaling = scaling; 24 | } 25 | 26 | public nint View { get; } 27 | 28 | public nint Layer { get; } 29 | 30 | public nint Drawable { get; } 31 | 32 | public nint Texture { get; } 33 | 34 | public nint Device { get; } 35 | 36 | public PixelSize PixelSize { get; } 37 | 38 | public double Scaling { get; } 39 | 40 | public Size LogicalSize => Scaling > 0 41 | ? new Size(PixelSize.Width / Scaling, PixelSize.Height / Scaling) 42 | : new Size(0, 0); 43 | 44 | public void MarkRendered() 45 | { 46 | Rendered = true; 47 | } 48 | 49 | public bool Rendered { get; private set; } 50 | } 51 | -------------------------------------------------------------------------------- /docs/developer-docs-outline.md: -------------------------------------------------------------------------------- 1 | # Developer Documentation Outline (M6 Task 6.3) 2 | 3 | ## 1. Getting Started 4 | - Prerequisites: native Impeller SDK, .NET 8 or later, platform SDKs (Metal/Vulkan). 5 | - Project setup: referencing `ImpellerSharp.Interop`, platform-specific native binaries. 6 | - First draw: minimal code sample creating context, texture, display list, surface draw. 7 | 8 | ## 2. Threading Model 9 | - Context creation and thread safety. 10 | - Command buffer encoding (single-thread restriction, multi-buffer patterns). 11 | - Background uploads and synchronization guidelines. 12 | 13 | ## 3. Memory Management 14 | - SafeHandle semantics, Retain/Release conventions. 15 | - Texture uploads (pinned spans vs. eager copy). 16 | - Resource disposal patterns and strict mode. 17 | 18 | ## 4. Shader & Pipeline Updates 19 | - Using `ImpellerFragmentProgramNew`. 20 | - Managing shader warm-up and caching. 21 | - Handling runtime shader blobs with `RuntimeStage`. 22 | 23 | ## 5. Diagnostics & Profiling 24 | - EventSource/ActivitySource integration. 25 | - Benchmarks & profiling workflows (link to Benchmarking Plan). 26 | - Logging/telemetry best practices. 27 | 28 | ## 6. Troubleshooting 29 | - Version negotiation failures. 30 | - Threading violations. 31 | - Common error codes/null returns. 32 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerMaskFilterHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed class ImpellerMaskFilterHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerMaskFilterHandle(nint native) 8 | { 9 | SetHandle(native); 10 | } 11 | 12 | public static ImpellerMaskFilterHandle CreateBlur(ImpellerBlurStyle style, float sigma) 13 | { 14 | var native = ImpellerNative.ImpellerMaskFilterCreateBlurNew(style, sigma); 15 | return new ImpellerMaskFilterHandle( 16 | EnsureSuccess(native, "Failed to create Impeller mask filter (blur).")); 17 | } 18 | 19 | internal void Retain() 20 | { 21 | ThrowIfInvalid(); 22 | ImpellerNative.ImpellerMaskFilterRetain(handle); 23 | } 24 | 25 | protected override bool ReleaseHandle() 26 | { 27 | ImpellerNative.ImpellerMaskFilterRelease(handle); 28 | return true; 29 | } 30 | 31 | internal new nint DangerousGetHandle() 32 | { 33 | ThrowIfInvalid(); 34 | return handle; 35 | } 36 | 37 | private void ThrowIfInvalid() 38 | { 39 | if (IsInvalid) 40 | { 41 | throw new ObjectDisposedException(nameof(ImpellerMaskFilterHandle)); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/sample-scenes.md: -------------------------------------------------------------------------------- 1 | # Sample Scenes & Demos (M6 Task 6.2) 2 | 3 | | Scene | Path | Description | Key APIs | Output | 4 | | --- | --- | --- | --- | --- | 5 | | BasicShapes | `samples/BasicShapes` | CLI-selectable scenes: rect grid, streaming textures, or typography. Supports Metal window capture via `--capture` | `ImpellerDisplayListBuilder`, `TextureFactory`, `ImpellerTypographyContext` | PNG capture (macOS) + console timings | 6 | | Avalonia Impeller Surface | `samples/AvaloniaImpellerApp` | Avalonia app that leases SkiaSharp context via `ISkiaSharpApiLeaseFeature` and exposes Metal/Vulkan handles | `ImpellerSkiaView`, `ISkiaSharpApiLease`, Avalonia `Window` | Desktop window | 7 | | TextureGallery | `samples/TextureGallery` (TBD) | Streams textures from disk, animates quads | `TextureFactory`, `Surface.DrawDisplayList` | GIF/Video capture | 8 | | TextLayout | `samples/TextLayout` (TBD) | Renders paragraphs with different fonts/shaders | `Typography` (future), `ImpellerPaintSetColor` | PDF snapshot | 9 | 10 | All samples should provide: 11 | * Managed harness (`dotnet run`), optional CLI options for backend selection. 12 | * Batching or streaming modes to stress resource upload paths. 13 | * Scripts or flags to run during CI (headless) with golden comparisons (see `build/scripts/export_basicshapes_golden.sh`). 14 | -------------------------------------------------------------------------------- /docs/interop-parity-plan.md: -------------------------------------------------------------------------------- 1 | # ImpellerSharp Interop Parity Plan 2 | 3 | 1. [x] Build an automated coverage report that parses `impeller.h`, compares native exports to managed P/Invoke signatures, and refreshes `docs/api-summary.md` whenever drift is detected. 4 | 2. [x] Add missing interop signatures for Vulkan swapchain, display-list advanced draws, path utilities, paint miter setter, and paragraph metrics in the `ImpellerNative.*.cs` files. 5 | 3. [x] Introduce managed wrappers for the new interop points (for example `ImpellerVulkanSwapchainHandle` plus extended builder/paint/paragraph helpers) so higher-level code can access the added functionality. 6 | 4. [x] Update samples and tests to exercise the new APIs (rounded-rect drawing, paragraph metric queries, Vulkan swapchain acquisition) and ensure they run cleanly across supported platforms. 7 | 5. [ ] Verify native packaging includes the `ImpellerSurfaceCreateWrappedMetalTextureNew` shim or replace it with an upstream-supported alternative to avoid missing exports at runtime. 8 | 6. [ ] Re-run the coverage script, commit the refreshed `docs/api-summary.md`, and wire the script into CI to block regressions when the upstream Impeller header changes. 9 | 10 | > Run `python3 build/scripts/generate_api_summary.py` to regenerate the coverage report after modifying bindings. 11 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerSharp.Interop.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | enable 5 | enable 6 | true 7 | true 8 | ImpellerSharp.Interop 9 | ImpellerSharp.Interop 10 | false 11 | ImpellerSharp.Interop 12 | .NET interop bindings for the Impeller rendering engine. 13 | impeller;rendering;gpu;flutter;interop 14 | README.md 15 | true 16 | false 17 | $(NoWarn);CS1591 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerMappingUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace ImpellerSharp.Interop; 6 | 7 | internal static unsafe class ImpellerMappingUtilities 8 | { 9 | [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] 10 | private static void ReleaseAllocatedMapping(nint userData) 11 | { 12 | if (userData != 0) 13 | { 14 | NativeMemory.Free((void*)userData); 15 | } 16 | } 17 | 18 | public static ImpellerMapping Create(ReadOnlySpan data, out nint userData) 19 | { 20 | if (data.IsEmpty) 21 | { 22 | userData = nint.Zero; 23 | return new ImpellerMapping 24 | { 25 | Data = null, 26 | Length = 0, 27 | OnRelease = null, 28 | }; 29 | } 30 | 31 | var buffer = (byte*)NativeMemory.Alloc((nuint)data.Length); 32 | data.CopyTo(new Span(buffer, data.Length)); 33 | 34 | userData = (nint)buffer; 35 | 36 | return new ImpellerMapping 37 | { 38 | Data = buffer, 39 | Length = (ulong)data.Length, 40 | OnRelease = &ReleaseAllocatedMapping, 41 | }; 42 | } 43 | 44 | public static void Free(nint userData) 45 | { 46 | if (userData != 0) 47 | { 48 | NativeMemory.Free((void*)userData); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.Texture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | internal static partial class ImpellerNative 7 | { 8 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerTextureCreateWithContentsNew")] 9 | [SuppressGCTransition] 10 | internal static unsafe partial nint ImpellerTextureCreateWithContentsNew( 11 | nint context, 12 | in ImpellerTextureDescriptor descriptor, 13 | in ImpellerMapping mapping, 14 | nint contentsOnReleaseUserData); 15 | 16 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerTextureCreateWithOpenGLTextureHandleNew")] 17 | [SuppressGCTransition] 18 | internal static unsafe partial nint ImpellerTextureCreateWithOpenGLTextureHandleNew( 19 | nint context, 20 | in ImpellerTextureDescriptor descriptor, 21 | ulong handle); 22 | 23 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerTextureRetain")] 24 | [SuppressGCTransition] 25 | internal static partial void ImpellerTextureRetain(nint texture); 26 | 27 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerTextureRelease")] 28 | [SuppressGCTransition] 29 | internal static partial void ImpellerTextureRelease(nint texture); 30 | 31 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerTextureGetOpenGLHandle")] 32 | [SuppressGCTransition] 33 | internal static partial ulong ImpellerTextureGetOpenGLHandle(nint texture); 34 | } 35 | -------------------------------------------------------------------------------- /samples/BasicShapes/BasicShapes.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerPathHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed class ImpellerPathHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerPathHandle(nint native) 8 | { 9 | SetHandle(native); 10 | } 11 | 12 | internal static ImpellerPathHandle FromOwned(nint native) 13 | { 14 | return new ImpellerPathHandle( 15 | EnsureSuccess(native, "Failed to create Impeller path.")); 16 | } 17 | 18 | internal void Retain() 19 | { 20 | if (IsInvalid) 21 | { 22 | throw new ObjectDisposedException(nameof(ImpellerPathHandle)); 23 | } 24 | 25 | ImpellerNative.ImpellerPathRetain(handle); 26 | } 27 | 28 | public ImpellerRect GetBounds() 29 | { 30 | if (IsInvalid) 31 | { 32 | throw new ObjectDisposedException(nameof(ImpellerPathHandle)); 33 | } 34 | 35 | unsafe 36 | { 37 | ImpellerRect bounds = default; 38 | ImpellerNative.ImpellerPathGetBounds(handle, &bounds); 39 | return bounds; 40 | } 41 | } 42 | 43 | internal new nint DangerousGetHandle() 44 | { 45 | if (IsInvalid) 46 | { 47 | throw new ObjectDisposedException(nameof(ImpellerPathHandle)); 48 | } 49 | 50 | return handle; 51 | } 52 | 53 | protected override bool ReleaseHandle() 54 | { 55 | ImpellerNative.ImpellerPathRelease(handle); 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /samples/AvaloniaImpellerApp/AvaloniaImpellerApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | libimpeller.dylib 25 | PreserveNewest 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/MotionMarkOriginal/MotionMarkOriginal.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | true 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /benchmarks/ImpellerSharp.Benchmarks/RectSequenceCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ImpellerSharp.Interop; 4 | 5 | namespace ImpellerSharp.Benchmarks; 6 | 7 | internal static class RectSequenceCache 8 | { 9 | private static readonly object Gate = new(); 10 | private static readonly Dictionary Cache = new(); 11 | private const float BaseX = 10f; 12 | private const float BaseY = 10f; 13 | private const float Width = 32f; 14 | private const float Height = 32f; 15 | private const float Spacing = 4f; 16 | private const int Columns = 32; 17 | 18 | internal static ImpellerRect[] GetRects(int count) 19 | { 20 | lock (Gate) 21 | { 22 | if (!Cache.TryGetValue(count, out var rects)) 23 | { 24 | rects = BuildRectSequence(count); 25 | Cache[count] = rects; 26 | } 27 | 28 | return rects; 29 | } 30 | } 31 | 32 | private static ImpellerRect[] BuildRectSequence(int count) 33 | { 34 | if (count <= 0) 35 | { 36 | throw new ArgumentOutOfRangeException(nameof(count), count, "Rectangle count must be positive."); 37 | } 38 | 39 | var rects = new ImpellerRect[count]; 40 | 41 | for (var i = 0; i < count; i++) 42 | { 43 | var column = i % Columns; 44 | var row = i / Columns; 45 | 46 | rects[i] = new ImpellerRect( 47 | BaseX + column * (Width + Spacing), 48 | BaseY + row * (Height + Spacing), 49 | Width, 50 | Height); 51 | } 52 | 53 | return rects; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /samples/BasicShapes/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImpellerSharp.Interop; 3 | 4 | namespace ImpellerSharp.Samples.BasicShapes; 5 | 6 | internal static partial class Program 7 | { 8 | private static void Main(string[] args) 9 | { 10 | Console.WriteLine("Impeller BasicShapes sample bootstrap"); 11 | 12 | try 13 | { 14 | SampleOptions options; 15 | try 16 | { 17 | options = SampleOptions.Parse(args); 18 | } 19 | catch (Exception parseEx) 20 | { 21 | Console.Error.WriteLine(parseEx.Message); 22 | SampleOptions.PrintUsage(); 23 | return; 24 | } 25 | 26 | if (options.Headless) 27 | { 28 | if (!HeadlessRunner.Run(options)) 29 | { 30 | Environment.ExitCode = 1; 31 | } 32 | return; 33 | } 34 | 35 | if (!OperatingSystem.IsMacOS()) 36 | { 37 | Console.Error.WriteLine("Interactive mode is currently only implemented for macOS. Use --headless for golden exports."); 38 | Environment.ExitCode = 1; 39 | return; 40 | } 41 | 42 | RunPlatformSample(options); 43 | } 44 | catch (ImpellerInteropException ex) 45 | { 46 | Console.Error.WriteLine($"Impeller initialization failed: {ex.Message}"); 47 | } 48 | catch (Exception ex) 49 | { 50 | Console.Error.WriteLine($"Unhandled exception: {ex}"); 51 | } 52 | } 53 | 54 | static partial void RunPlatformSample(SampleOptions options); 55 | } 56 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerColorFilterHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed unsafe class ImpellerColorFilterHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerColorFilterHandle(nint native) 8 | { 9 | SetHandle(native); 10 | } 11 | 12 | public static ImpellerColorFilterHandle CreateBlend(in ImpellerColor color, ImpellerBlendMode blendMode) 13 | { 14 | var value = color; 15 | var native = ImpellerNative.ImpellerColorFilterCreateBlendNew(&value, blendMode); 16 | return new ImpellerColorFilterHandle( 17 | EnsureSuccess(native, "Failed to create Impeller color filter (blend).")); 18 | } 19 | 20 | public static ImpellerColorFilterHandle CreateColorMatrix(in ImpellerColorMatrix matrix) 21 | { 22 | var value = matrix; 23 | var native = ImpellerNative.ImpellerColorFilterCreateColorMatrixNew(&value); 24 | return new ImpellerColorFilterHandle( 25 | EnsureSuccess(native, "Failed to create Impeller color filter (matrix).")); 26 | } 27 | 28 | internal void Retain() 29 | { 30 | ThrowIfInvalid(); 31 | ImpellerNative.ImpellerColorFilterRetain(handle); 32 | } 33 | 34 | protected override bool ReleaseHandle() 35 | { 36 | ImpellerNative.ImpellerColorFilterRelease(handle); 37 | return true; 38 | } 39 | 40 | internal new nint DangerousGetHandle() 41 | { 42 | ThrowIfInvalid(); 43 | return handle; 44 | } 45 | 46 | private void ThrowIfInvalid() 47 | { 48 | if (IsInvalid) 49 | { 50 | throw new ObjectDisposedException(nameof(ImpellerColorFilterHandle)); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerInteropOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public static class ImpellerInteropOptions 6 | { 7 | private const string StrictEnvironmentVariable = "IMPELLER_INTEROP_STRICT"; 8 | private const string StrictAppContextSwitch = "ImpellerSharp.Interop.StrictMode"; 9 | private static ImpellerInteropConfiguration _configuration = ImpellerInteropConfiguration.Default; 10 | 11 | public static ImpellerInteropConfiguration Configuration => _configuration; 12 | 13 | static ImpellerInteropOptions() 14 | { 15 | if (AppContext.TryGetSwitch(StrictAppContextSwitch, out var strict) && strict) 16 | { 17 | EnableStrictMode(); 18 | return; 19 | } 20 | 21 | var env = Environment.GetEnvironmentVariable(StrictEnvironmentVariable); 22 | if (!string.IsNullOrEmpty(env) && IsTruthy(env)) 23 | { 24 | EnableStrictMode(); 25 | } 26 | } 27 | 28 | public static void Configure(ImpellerInteropConfiguration configuration) 29 | { 30 | _configuration = configuration; 31 | } 32 | 33 | public static void EnableStrictMode() 34 | { 35 | _configuration = _configuration with { StrictMode = true }; 36 | } 37 | 38 | private static bool IsTruthy(string value) 39 | { 40 | return value.Equals("1", StringComparison.OrdinalIgnoreCase) 41 | || value.Equals("true", StringComparison.OrdinalIgnoreCase) 42 | || value.Equals("yes", StringComparison.OrdinalIgnoreCase); 43 | } 44 | } 45 | 46 | public readonly record struct ImpellerInteropConfiguration(bool StrictMode, bool ValidateThreadAffinity) 47 | { 48 | public static ImpellerInteropConfiguration Default => new(false, false); 49 | } 50 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerFragmentProgramHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed class ImpellerFragmentProgramHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerFragmentProgramHandle(nint native, bool ownsHandle) 8 | : base(ownsHandle) 9 | { 10 | SetHandle(native); 11 | } 12 | 13 | public static ImpellerFragmentProgramHandle Create(ReadOnlySpan compiledShader) 14 | { 15 | if (compiledShader.IsEmpty) 16 | { 17 | throw new ArgumentException("Fragment program data must not be empty.", nameof(compiledShader)); 18 | } 19 | 20 | var mapping = ImpellerMappingUtilities.Create(compiledShader, out var userData); 21 | var native = ImpellerNative.ImpellerFragmentProgramNew(in mapping, userData); 22 | if (native == nint.Zero) 23 | { 24 | ImpellerMappingUtilities.Free(userData); 25 | throw new ImpellerInteropException("Failed to create Impeller fragment program."); 26 | } 27 | 28 | return new ImpellerFragmentProgramHandle(native, ownsHandle: true); 29 | } 30 | 31 | internal void Retain() 32 | { 33 | ThrowIfInvalid(); 34 | ImpellerNative.ImpellerFragmentProgramRetain(handle); 35 | } 36 | 37 | protected override bool ReleaseHandle() 38 | { 39 | ImpellerNative.ImpellerFragmentProgramRelease(handle); 40 | return true; 41 | } 42 | 43 | internal new nint DangerousGetHandle() 44 | { 45 | ThrowIfInvalid(); 46 | return handle; 47 | } 48 | 49 | private void ThrowIfInvalid() 50 | { 51 | if (IsInvalid) 52 | { 53 | throw new ObjectDisposedException(nameof(ImpellerFragmentProgramHandle)); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.GlyphInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerGlyphInfoRetain")] 8 | [SuppressGCTransition] 9 | internal static partial void ImpellerGlyphInfoRetain(nint glyphInfo); 10 | 11 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerGlyphInfoRelease")] 12 | [SuppressGCTransition] 13 | internal static partial void ImpellerGlyphInfoRelease(nint glyphInfo); 14 | 15 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerGlyphInfoGetGraphemeClusterCodeUnitRangeBegin")] 16 | [SuppressGCTransition] 17 | internal static partial nuint ImpellerGlyphInfoGetGraphemeClusterCodeUnitRangeBegin(nint glyphInfo); 18 | 19 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerGlyphInfoGetGraphemeClusterCodeUnitRangeEnd")] 20 | [SuppressGCTransition] 21 | internal static partial nuint ImpellerGlyphInfoGetGraphemeClusterCodeUnitRangeEnd(nint glyphInfo); 22 | 23 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerGlyphInfoGetGraphemeClusterBounds")] 24 | [SuppressGCTransition] 25 | internal static unsafe partial void ImpellerGlyphInfoGetGraphemeClusterBounds(nint glyphInfo, ImpellerRect* bounds); 26 | 27 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerGlyphInfoIsEllipsis")] 28 | [SuppressGCTransition] 29 | [return: MarshalAs(UnmanagedType.I1)] 30 | internal static partial bool ImpellerGlyphInfoIsEllipsis(nint glyphInfo); 31 | 32 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerGlyphInfoGetTextDirection")] 33 | [SuppressGCTransition] 34 | internal static partial ImpellerTextDirection ImpellerGlyphInfoGetTextDirection(nint glyphInfo); 35 | } 36 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerTypographyContextHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | public sealed unsafe class ImpellerTypographyContextHandle : ImpellerSafeHandle 7 | { 8 | private ImpellerTypographyContextHandle(nint native) 9 | { 10 | SetHandle(native); 11 | } 12 | 13 | public static ImpellerTypographyContextHandle Create() 14 | { 15 | var native = ImpellerNative.ImpellerTypographyContextNew(); 16 | return new ImpellerTypographyContextHandle( 17 | EnsureSuccess(native, "Failed to create Impeller typography context.")); 18 | } 19 | 20 | public bool RegisterFont(ReadOnlySpan fontData, string? familyAlias = null) 21 | { 22 | ThrowIfInvalid(); 23 | 24 | var mapping = ImpellerMappingUtilities.Create(fontData, out var userData); 25 | 26 | using var alias = new Utf8String(familyAlias); 27 | var result = ImpellerNative.ImpellerTypographyContextRegisterFont( 28 | handle, 29 | in mapping, 30 | userData, 31 | (byte*)alias.Pointer); 32 | 33 | if (!result) 34 | { 35 | ImpellerMappingUtilities.Free(userData); 36 | } 37 | 38 | return result; 39 | } 40 | 41 | internal void Retain() 42 | { 43 | ThrowIfInvalid(); 44 | ImpellerNative.ImpellerTypographyContextRetain(handle); 45 | } 46 | 47 | protected override bool ReleaseHandle() 48 | { 49 | ImpellerNative.ImpellerTypographyContextRelease(handle); 50 | return true; 51 | } 52 | 53 | internal new nint DangerousGetHandle() 54 | { 55 | ThrowIfInvalid(); 56 | return handle; 57 | } 58 | 59 | private void ThrowIfInvalid() 60 | { 61 | if (IsInvalid) 62 | { 63 | throw new ObjectDisposedException(nameof(ImpellerTypographyContextHandle)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.Surface.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerSurfaceRetain")] 8 | [SuppressGCTransition] 9 | internal static partial void ImpellerSurfaceRetain(nint surface); 10 | 11 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerSurfaceRelease")] 12 | [SuppressGCTransition] 13 | internal static partial void ImpellerSurfaceRelease(nint surface); 14 | 15 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerSurfaceDrawDisplayList")] 16 | [SuppressGCTransition] 17 | [return: MarshalAs(UnmanagedType.I1)] 18 | internal static partial bool ImpellerSurfaceDrawDisplayList(nint surface, nint displayList); 19 | 20 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerSurfacePresent")] 21 | [SuppressGCTransition] 22 | [return: MarshalAs(UnmanagedType.I1)] 23 | internal static partial bool ImpellerSurfacePresent(nint surface); 24 | 25 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerSurfaceCreateWrappedFBONew")] 26 | [SuppressGCTransition] 27 | internal static partial nint ImpellerSurfaceCreateWrappedFBONew( 28 | nint context, 29 | ulong framebuffer, 30 | ImpellerPixelFormat format, 31 | in ImpellerISize size); 32 | 33 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerSurfaceCreateWrappedMetalDrawableNew")] 34 | [SuppressGCTransition] 35 | internal static partial nint ImpellerSurfaceCreateWrappedMetalDrawableNew( 36 | nint context, 37 | nint metalDrawable); 38 | 39 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerSurfaceCreateWrappedMetalTextureNew")] 40 | [SuppressGCTransition] 41 | internal static partial nint ImpellerSurfaceCreateWrappedMetalTextureNew( 42 | nint context, 43 | nint metalTexture); 44 | } 45 | -------------------------------------------------------------------------------- /samples/MotionMark/MotionMark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | true 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.Core.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal struct ImpellerContextVulkanSettingsNative 8 | { 9 | public IntPtr UserData; 10 | public IntPtr ProcAddressCallback; 11 | 12 | [MarshalAs(UnmanagedType.I1)] 13 | public bool EnableValidation; 14 | } 15 | 16 | internal static partial class ImpellerNative 17 | { 18 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerGetVersion")] 19 | internal static partial uint ImpellerGetVersion(); 20 | 21 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerContextCreateMetalNew")] 22 | [SuppressGCTransition] 23 | internal static partial nint ImpellerContextCreateMetalNew(uint version); 24 | 25 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerContextCreateOpenGLESNew")] 26 | [SuppressGCTransition] 27 | internal static partial nint ImpellerContextCreateOpenGLESNew( 28 | uint version, 29 | IntPtr callback, 30 | IntPtr userData); 31 | 32 | [DllImport(ImpellerLibrary.Name, EntryPoint = "ImpellerContextCreateVulkanNew")] 33 | internal static extern nint ImpellerContextCreateVulkanNew( 34 | uint version, 35 | ref ImpellerContextVulkanSettingsNative settings); 36 | 37 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerContextGetVulkanInfo")] 38 | [SuppressGCTransition] 39 | [return: MarshalAs(UnmanagedType.I1)] 40 | internal static partial bool ImpellerContextGetVulkanInfo( 41 | nint context, 42 | out ImpellerContextVulkanInfo info); 43 | 44 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerContextRetain")] 45 | [SuppressGCTransition] 46 | internal static partial void ImpellerContextRetain(nint context); 47 | 48 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerContextRelease")] 49 | [SuppressGCTransition] 50 | internal static partial void ImpellerContextRelease(nint context); 51 | } 52 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerGlyphInfoHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed unsafe class ImpellerGlyphInfoHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerGlyphInfoHandle(nint native) 8 | { 9 | SetHandle(native); 10 | } 11 | 12 | internal static ImpellerGlyphInfoHandle? FromOwned(nint native) 13 | { 14 | return native == nint.Zero 15 | ? null 16 | : new ImpellerGlyphInfoHandle(native); 17 | } 18 | 19 | internal void Retain() 20 | { 21 | ThrowIfInvalid(); 22 | ImpellerNative.ImpellerGlyphInfoRetain(handle); 23 | } 24 | 25 | public (ulong Start, ulong End) GetGraphemeClusterCodeUnitRange() 26 | { 27 | ThrowIfInvalid(); 28 | var begin = ImpellerNative.ImpellerGlyphInfoGetGraphemeClusterCodeUnitRangeBegin(handle); 29 | var end = ImpellerNative.ImpellerGlyphInfoGetGraphemeClusterCodeUnitRangeEnd(handle); 30 | return (begin, end); 31 | } 32 | 33 | public ImpellerRect GetGraphemeClusterBounds() 34 | { 35 | ThrowIfInvalid(); 36 | ImpellerRect rect; 37 | ImpellerNative.ImpellerGlyphInfoGetGraphemeClusterBounds(handle, &rect); 38 | return rect; 39 | } 40 | 41 | public bool IsEllipsis() 42 | { 43 | ThrowIfInvalid(); 44 | return ImpellerNative.ImpellerGlyphInfoIsEllipsis(handle); 45 | } 46 | 47 | public ImpellerTextDirection GetTextDirection() 48 | { 49 | ThrowIfInvalid(); 50 | return ImpellerNative.ImpellerGlyphInfoGetTextDirection(handle); 51 | } 52 | 53 | protected override bool ReleaseHandle() 54 | { 55 | ImpellerNative.ImpellerGlyphInfoRelease(handle); 56 | return true; 57 | } 58 | 59 | internal new nint DangerousGetHandle() 60 | { 61 | ThrowIfInvalid(); 62 | return handle; 63 | } 64 | 65 | private void ThrowIfInvalid() 66 | { 67 | if (IsInvalid) 68 | { 69 | throw new ObjectDisposedException(nameof(ImpellerGlyphInfoHandle)); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /samples/BasicShapes/MacCapture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | 5 | namespace ImpellerSharp.Samples.BasicShapes; 6 | 7 | internal static class MacCapture 8 | { 9 | private const string CaptureTool = "/usr/sbin/screencapture"; 10 | 11 | internal static void CaptureWindow(nuint windowNumber, string destinationPath) 12 | { 13 | if (!OperatingSystem.IsMacOS()) 14 | { 15 | throw new PlatformNotSupportedException("Window capture is only supported on macOS."); 16 | } 17 | 18 | if (windowNumber == 0) 19 | { 20 | throw new ArgumentException("Window number must be non-zero for capture.", nameof(windowNumber)); 21 | } 22 | 23 | if (!File.Exists(CaptureTool)) 24 | { 25 | throw new FileNotFoundException("macOS screencapture utility not found.", CaptureTool); 26 | } 27 | 28 | var fullPath = Path.GetFullPath(destinationPath); 29 | var directory = Path.GetDirectoryName(fullPath); 30 | if (!string.IsNullOrEmpty(directory)) 31 | { 32 | Directory.CreateDirectory(directory); 33 | } 34 | 35 | using var process = new Process 36 | { 37 | StartInfo = new ProcessStartInfo 38 | { 39 | FileName = CaptureTool, 40 | RedirectStandardError = true, 41 | RedirectStandardOutput = true, 42 | UseShellExecute = false, 43 | } 44 | }; 45 | 46 | process.StartInfo.ArgumentList.Add("-x"); 47 | process.StartInfo.ArgumentList.Add("-o"); 48 | process.StartInfo.ArgumentList.Add("-l"); 49 | process.StartInfo.ArgumentList.Add(windowNumber.ToString()); 50 | process.StartInfo.ArgumentList.Add(fullPath); 51 | 52 | process.Start(); 53 | process.WaitForExit(); 54 | 55 | if (process.ExitCode != 0) 56 | { 57 | var error = process.StandardError.ReadToEnd(); 58 | throw new InvalidOperationException($"screencapture failed with exit code {process.ExitCode}: {error}"); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /samples/BasicShapes/VulkanSwapchainProbe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using ImpellerSharp.Interop; 4 | using ImpellerSharp.Samples.BasicShapes.Scenes; 5 | 6 | namespace ImpellerSharp.Samples.BasicShapes; 7 | 8 | internal static class VulkanSwapchainProbe 9 | { 10 | internal static void Run(ImpellerContextHandle context, nint? configuredSurface) 11 | { 12 | if (context is null || context.IsInvalid) 13 | { 14 | return; 15 | } 16 | 17 | var surfaceHandle = configuredSurface ?? ParseSurfaceFromEnvironment(); 18 | if (surfaceHandle == nint.Zero) 19 | { 20 | SceneExecutionContext.LastVulkanSwapchainProbe = "skipped"; 21 | return; 22 | } 23 | 24 | try 25 | { 26 | using var swapchain = context.CreateVulkanSwapchain(surfaceHandle); 27 | using var surface = swapchain.AcquireNextSurface(); 28 | SceneExecutionContext.LastVulkanSwapchainProbe = surface is null ? "acquired-null" : "acquired-surface"; 29 | } 30 | catch (Exception ex) 31 | { 32 | SceneExecutionContext.LastVulkanSwapchainProbe = $"failed:{ex.GetType().Name}"; 33 | } 34 | } 35 | 36 | private static nint ParseSurfaceFromEnvironment() 37 | { 38 | var value = Environment.GetEnvironmentVariable("IMPELLER_VK_SURFACE"); 39 | if (string.IsNullOrWhiteSpace(value)) 40 | { 41 | return nint.Zero; 42 | } 43 | 44 | value = value.Trim(); 45 | 46 | try 47 | { 48 | if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) 49 | { 50 | if (ulong.TryParse(value.AsSpan(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hex)) 51 | { 52 | return new nint(unchecked((long)hex)); 53 | } 54 | } 55 | else if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var dec)) 56 | { 57 | return new nint(dec); 58 | } 59 | } 60 | catch 61 | { 62 | // Ignore invalid environment values and treat as unset. 63 | } 64 | 65 | return nint.Zero; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.ImageFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerImageFilterRetain")] 8 | [SuppressGCTransition] 9 | internal static partial void ImpellerImageFilterRetain(nint imageFilter); 10 | 11 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerImageFilterRelease")] 12 | [SuppressGCTransition] 13 | internal static partial void ImpellerImageFilterRelease(nint imageFilter); 14 | 15 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerImageFilterCreateBlurNew")] 16 | [SuppressGCTransition] 17 | internal static partial nint ImpellerImageFilterCreateBlurNew( 18 | float sigmaX, 19 | float sigmaY, 20 | ImpellerTileMode tileMode); 21 | 22 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerImageFilterCreateDilateNew")] 23 | [SuppressGCTransition] 24 | internal static partial nint ImpellerImageFilterCreateDilateNew(float radiusX, float radiusY); 25 | 26 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerImageFilterCreateErodeNew")] 27 | [SuppressGCTransition] 28 | internal static partial nint ImpellerImageFilterCreateErodeNew(float radiusX, float radiusY); 29 | 30 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerImageFilterCreateMatrixNew")] 31 | [SuppressGCTransition] 32 | internal static unsafe partial nint ImpellerImageFilterCreateMatrixNew( 33 | ImpellerMatrix* matrix, 34 | ImpellerTextureSampling sampling); 35 | 36 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerImageFilterCreateComposeNew")] 37 | [SuppressGCTransition] 38 | internal static partial nint ImpellerImageFilterCreateComposeNew(nint outerFilter, nint innerFilter); 39 | 40 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerImageFilterCreateFragmentProgramNew")] 41 | [SuppressGCTransition] 42 | internal static unsafe partial nint ImpellerImageFilterCreateFragmentProgramNew( 43 | nint context, 44 | nint fragmentProgram, 45 | nint* samplers, 46 | nuint samplersCount, 47 | byte* data, 48 | nuint dataLength); 49 | } 50 | -------------------------------------------------------------------------------- /benchmarks/ImpellerSharp.Benchmarks/DisplayListBuilderBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Attributes; 3 | using ImpellerSharp.Interop; 4 | 5 | namespace ImpellerSharp.Benchmarks; 6 | 7 | [MemoryDiagnoser] 8 | [ShortRunJob] 9 | public class DisplayListBuilderBenchmark : IDisposable 10 | { 11 | private ImpellerPaintHandle? _paint; 12 | private ImpellerRect[] _rects = Array.Empty(); 13 | private bool _initialized; 14 | private string? _skipReason; 15 | 16 | [Params(256, 1024)] 17 | public int RectCount { get; set; } 18 | 19 | [GlobalSetup] 20 | public void GlobalSetup() 21 | { 22 | try 23 | { 24 | _paint = ImpellerPaintHandle.Create(); 25 | _paint.SetColor(new ImpellerColor(1f, 0f, 0f, 1f)); 26 | _rects = RectSequenceCache.GetRects(RectCount); 27 | _initialized = true; 28 | } 29 | catch (Exception ex) 30 | { 31 | _skipReason = ex.Message; 32 | _initialized = false; 33 | } 34 | } 35 | 36 | [GlobalCleanup] 37 | public void GlobalCleanup() 38 | { 39 | Dispose(); 40 | } 41 | 42 | [Benchmark(Description = "Managed display list build (rects)")] 43 | public int BuildDisplayList() 44 | { 45 | EnsureInitialized(); 46 | 47 | using var builder = ImpellerDisplayListBuilderHandle.Create(); 48 | 49 | var paint = _paint!; 50 | var rects = _rects; 51 | if (rects.Length != RectCount) 52 | { 53 | rects = _rects = RectSequenceCache.GetRects(RectCount); 54 | } 55 | 56 | for (var i = 0; i < rects.Length; i++) 57 | { 58 | builder.DrawRect(rects[i], paint); 59 | } 60 | 61 | using var displayList = builder.Build(); 62 | return RectCount; 63 | } 64 | 65 | private void EnsureInitialized() 66 | { 67 | if (_initialized) 68 | { 69 | return; 70 | } 71 | 72 | throw new InvalidOperationException( 73 | $"Benchmark prerequisites are missing: {_skipReason ?? "unknown error creating Impeller handles."} " + 74 | "Ensure the Impeller native library is discoverable before running benchmarks."); 75 | } 76 | 77 | public void Dispose() 78 | { 79 | _paint?.Dispose(); 80 | _paint = null; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /benchmarks/ImpellerSharp.Benchmarks/SurfacePresentBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Attributes; 3 | using ImpellerSharp.Interop; 4 | using ImpellerSharp.Interop.Hosting; 5 | 6 | namespace ImpellerSharp.Benchmarks; 7 | 8 | [MemoryDiagnoser] 9 | [ShortRunJob] 10 | public class SurfacePresentBenchmark : IDisposable 11 | { 12 | private MetalGlfwAppHost? _host; 13 | private bool _initialized; 14 | private string? _skipReason; 15 | 16 | [GlobalSetup] 17 | public void GlobalSetup() 18 | { 19 | if (!OperatingSystem.IsMacOS()) 20 | { 21 | _skipReason = "Surface present benchmark currently requires macOS Metal backend."; 22 | _initialized = false; 23 | return; 24 | } 25 | 26 | _host = new MetalGlfwAppHost(new MetalGlfwHostOptions 27 | { 28 | Title = "ImpellerSharp.Benchmarks.SurfacePresent", 29 | Width = 640, 30 | Height = 480, 31 | Visible = false, 32 | }); 33 | 34 | _initialized = true; 35 | } 36 | 37 | [GlobalCleanup] 38 | public void Dispose() 39 | { 40 | _host?.Dispose(); 41 | _host = null; 42 | } 43 | 44 | [Benchmark(Description = "Managed surface draw + present loop")] 45 | public int ManagedSurfacePresent() 46 | { 47 | EnsureInitialized(); 48 | 49 | if (_host is null) 50 | { 51 | throw new InvalidOperationException("Host not initialised."); 52 | } 53 | 54 | var frameCount = 0; 55 | 56 | var result = _host.Run((builder, width, height) => 57 | { 58 | if (frameCount >= 60) 59 | { 60 | return false; 61 | } 62 | 63 | using var paint = CreateClearPaint(); 64 | builder.DrawRect(new ImpellerRect(0, 0, width, height), paint); 65 | frameCount++; 66 | return true; 67 | }); 68 | 69 | if (result != 0) 70 | { 71 | throw new InvalidOperationException($"MetalGlfwAppHost returned exit code {result}."); 72 | } 73 | 74 | return frameCount; 75 | } 76 | 77 | private static ImpellerPaintHandle CreateClearPaint() 78 | { 79 | var paint = ImpellerPaintHandle.Create(); 80 | paint.SetColor(new ImpellerColor(0f, 0f, 0f, 1f)); 81 | return paint; 82 | } 83 | 84 | private void EnsureInitialized() 85 | { 86 | if (_initialized) 87 | { 88 | return; 89 | } 90 | 91 | throw new InvalidOperationException( 92 | $"Surface present benchmark prerequisites missing: {_skipReason ?? "unknown error"}"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Hosting/GlfwNative.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Silk.NET.GLFW; 4 | 5 | namespace ImpellerSharp.Interop.Hosting; 6 | 7 | internal static unsafe class GlfwNative 8 | { 9 | private static bool s_libraryLoaded; 10 | private static IntPtr s_libraryHandle; 11 | private static delegate* unmanaged s_getCocoaWindow; 12 | 13 | public static void Initialise(Glfw glfw) 14 | { 15 | if (!OperatingSystem.IsMacOS()) 16 | { 17 | return; 18 | } 19 | 20 | if (s_libraryLoaded && s_getCocoaWindow != null) 21 | { 22 | return; 23 | } 24 | 25 | if (!TryLoadLibrary()) 26 | { 27 | throw new InvalidOperationException("Unable to load GLFW native library; ensure Ultz.Native.GLFW is present."); 28 | } 29 | 30 | var export = NativeLibrary.GetExport(s_libraryHandle, "glfwGetCocoaWindow"); 31 | if (export == IntPtr.Zero) 32 | { 33 | throw new InvalidOperationException("glfwGetCocoaWindow export not found in GLFW library."); 34 | } 35 | 36 | s_getCocoaWindow = (delegate* unmanaged)export; 37 | s_libraryLoaded = true; 38 | } 39 | 40 | public static IntPtr GetCocoaWindow(WindowHandle* window) 41 | { 42 | if (!OperatingSystem.IsMacOS()) 43 | { 44 | return IntPtr.Zero; 45 | } 46 | 47 | if (s_getCocoaWindow == null) 48 | { 49 | throw new InvalidOperationException("Call GlfwNative.Initialise before requesting the native window handle."); 50 | } 51 | 52 | return s_getCocoaWindow(window); 53 | } 54 | 55 | private static bool TryLoadLibrary() 56 | { 57 | if (s_libraryHandle != IntPtr.Zero) 58 | { 59 | return true; 60 | } 61 | 62 | bool TryLoad(string name) 63 | { 64 | if (NativeLibrary.TryLoad(name, out var handle)) 65 | { 66 | s_libraryHandle = handle; 67 | return true; 68 | } 69 | 70 | return false; 71 | } 72 | 73 | return 74 | TryLoad("libglfw.3.dylib") || 75 | TryLoad("libglfw.dylib") || 76 | TryLoad("glfw") || 77 | TryLoad(System.IO.Path.Combine(AppContext.BaseDirectory, "libglfw.3.dylib")) || 78 | TryLoad(System.IO.Path.Combine(AppContext.BaseDirectory, "runtimes", "osx-arm64", "native", "libglfw.3.dylib")) || 79 | TryLoad(System.IO.Path.Combine(AppContext.BaseDirectory, "runtimes", "osx-x64", "native", "libglfw.3.dylib")); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /samples/AvaloniaImpellerApp/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 8 | 10 | 15 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 30 | 31 | 35 | 38 | 41 | 45 | 47 | 54 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /docs/benchmarking-plan.md: -------------------------------------------------------------------------------- 1 | # Managed Impeller Benchmark & Profiling Plan 2 | 3 | ## Benchmark Scenarios (Task 5.1) 4 | 5 | | Scenario | Description | Metrics | Notes | 6 | | --- | --- | --- | --- | 7 | | BL-DrawRect-HotLoop | Build a display list with 1k rects and issue repeated `SurfaceDrawDisplayList` | avg frame submission (ms), stddev, CPU % | Compare managed pipeline vs native `impeller/toolkit` example | 8 | | TextureUpload-ManagedVsNative | Upload 256x256 RGBA textures via managed `TextureFactory` vs native ABI | upload latency (ms), allocations, GC count | Backed by `TextureUploadBenchmark` (BenchmarkDotNet) | 9 | | TextureUpload-256 | Upload 256x256 RGBA texture via `TextureFactory.CreateTexture` | upload latency (ms), GC allocations | Use deterministic data, measure map/unmap | 10 | | SurfacePresent-Metal | Present a Metal surface after display list draw | present latency, success rate | Requires macOS w/Metal backend | 11 | 12 | Benchmark harness should use `BenchmarkDotNet` with custom job pinning to avoid interference. Each scenario should have control (native C++ path using `impeller` CLI example) for comparison. 13 | 14 | ## Allocation Profiling (Task 5.2) 15 | 16 | * Run `dotnet-trace collect --providers Microsoft-Windows-DotNETRuntime:0x4c14fccbd` targeting managed sample app to capture allocation metrics while running Scenario 1. 17 | * Shortcut: `build/scripts/collect_allocations.sh` wraps the BenchmarkDotNet harness for `TextureUploadBenchmark` to emit `.nettrace` captures. 18 | * Use `dotnet-counters monitor System.Runtime` to watch `GC Heap Size`, `Allocation Rate` in real-time, ensuring < 1KB/frame in hot loop. 19 | * For finer detail, integrate `EventPipeEventSource` to emit per-frame summaries into logs (scripted). 20 | 21 | ## Stress Tests (Task 5.3) 22 | 23 | 1. **Texture streaming**: Upload 100 textures (1024x1024) sequentially, then randomly re-upload different mip levels; watch for failures and time per upload. 24 | 2. **Uniform buffer pressure**: Generate display lists with increasingly large uniform data (gradients, color matrices) to ensure bindings stay valid. 25 | 3. **Shader warm-up**: Preload multiple fragment programs via `ImpellerFragmentProgramNew` and time first draw vs subsequent draws. 26 | 4. **Concurrency**: Issue texture uploads on background tasks while main thread renders to verify `ImpellerContext` thread-safe expectations. 27 | 28 | ## Guardrails (Task 5.4) 29 | 30 | * SafeHandle wrappers guard against disposed usage (already throws `ObjectDisposedException`). 31 | * Add debug assertions (conditional `DEBUG`) to ensure library version negotiation succeeded. 32 | * Provide optional `ImpellerInteropOptions` enabling strict mode: checks for null returns, logs warnings, and ensures callbacks invoked on thread pool. 33 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerVulkanSwapchainHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed class ImpellerVulkanSwapchainHandle : ImpellerSafeHandle 6 | { 7 | private readonly ImpellerContextHandle _context; 8 | private readonly bool _contextRefHeld; 9 | 10 | private ImpellerVulkanSwapchainHandle(nint native, ImpellerContextHandle context) 11 | { 12 | if (context is null) 13 | { 14 | throw new ArgumentNullException(nameof(context)); 15 | } 16 | 17 | SetHandle(native); 18 | _context = context; 19 | 20 | var addedRef = false; 21 | context.DangerousAddRef(ref addedRef); 22 | _contextRefHeld = addedRef; 23 | } 24 | 25 | public static ImpellerVulkanSwapchainHandle Create(ImpellerContextHandle context, nint vulkanSurface) 26 | { 27 | if (context is null) 28 | { 29 | throw new ArgumentNullException(nameof(context)); 30 | } 31 | 32 | if (vulkanSurface == nint.Zero) 33 | { 34 | throw new ArgumentException("VkSurfaceKHR pointer must not be null.", nameof(vulkanSurface)); 35 | } 36 | 37 | if (context.IsInvalid) 38 | { 39 | throw new ObjectDisposedException(nameof(ImpellerContextHandle)); 40 | } 41 | 42 | var native = ImpellerNative.ImpellerVulkanSwapchainCreateNew(context.DangerousGetHandle(), vulkanSurface); 43 | return FromOwned(native, context); 44 | } 45 | 46 | internal static ImpellerVulkanSwapchainHandle FromOwned(nint native, ImpellerContextHandle context) 47 | { 48 | native = EnsureSuccess(native, "Failed to create Impeller Vulkan swapchain."); 49 | return new ImpellerVulkanSwapchainHandle(native, context); 50 | } 51 | 52 | internal void Retain() 53 | { 54 | ThrowIfInvalid(); 55 | ImpellerNative.ImpellerVulkanSwapchainRetain(handle); 56 | } 57 | 58 | public ImpellerSurfaceHandle? AcquireNextSurface() 59 | { 60 | ThrowIfInvalid(); 61 | var nativeSurface = ImpellerNative.ImpellerVulkanSwapchainAcquireNextSurfaceNew(handle); 62 | if (nativeSurface == nint.Zero) 63 | { 64 | return null; 65 | } 66 | 67 | return ImpellerSurfaceHandle.FromOwned(nativeSurface); 68 | } 69 | 70 | protected override bool ReleaseHandle() 71 | { 72 | ImpellerNative.ImpellerVulkanSwapchainRelease(handle); 73 | if (_contextRefHeld) 74 | { 75 | _context.DangerousRelease(); 76 | } 77 | 78 | return true; 79 | } 80 | 81 | internal new nint DangerousGetHandle() 82 | { 83 | ThrowIfInvalid(); 84 | return handle; 85 | } 86 | 87 | private void ThrowIfInvalid() 88 | { 89 | if (IsInvalid) 90 | { 91 | throw new ObjectDisposedException(nameof(ImpellerVulkanSwapchainHandle)); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerParagraphBuilderHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | public sealed unsafe class ImpellerParagraphBuilderHandle : ImpellerSafeHandle 7 | { 8 | private ImpellerParagraphBuilderHandle(nint native) 9 | { 10 | SetHandle(native); 11 | } 12 | 13 | public static ImpellerParagraphBuilderHandle Create(ImpellerTypographyContextHandle context) 14 | { 15 | if (context is null || context.IsInvalid) 16 | { 17 | throw new ArgumentNullException(nameof(context)); 18 | } 19 | 20 | var addedRef = false; 21 | 22 | try 23 | { 24 | context.DangerousAddRef(ref addedRef); 25 | var native = ImpellerNative.ImpellerParagraphBuilderNew(context.DangerousGetHandle()); 26 | return new ImpellerParagraphBuilderHandle( 27 | EnsureSuccess(native, "Failed to create Impeller paragraph builder.")); 28 | } 29 | finally 30 | { 31 | if (addedRef) 32 | { 33 | context.DangerousRelease(); 34 | } 35 | } 36 | } 37 | 38 | internal void Retain() 39 | { 40 | ThrowIfInvalid(); 41 | ImpellerNative.ImpellerParagraphBuilderRetain(handle); 42 | } 43 | 44 | public void PushStyle(ImpellerParagraphStyleHandle style) 45 | { 46 | ThrowIfInvalid(); 47 | if (style is null || style.IsInvalid) 48 | { 49 | throw new ArgumentNullException(nameof(style)); 50 | } 51 | 52 | ImpellerNative.ImpellerParagraphBuilderPushStyle(handle, style.DangerousGetHandle()); 53 | } 54 | 55 | public void PopStyle() 56 | { 57 | ThrowIfInvalid(); 58 | ImpellerNative.ImpellerParagraphBuilderPopStyle(handle); 59 | } 60 | 61 | public void AddText(string text) 62 | { 63 | ThrowIfInvalid(); 64 | if (string.IsNullOrEmpty(text)) 65 | { 66 | return; 67 | } 68 | 69 | var length = Encoding.UTF8.GetByteCount(text); 70 | using var utf8 = new Utf8String(text); 71 | ImpellerNative.ImpellerParagraphBuilderAddText(handle, (byte*)utf8.Pointer, (uint)length); 72 | } 73 | 74 | public ImpellerParagraphHandle Build(float width) 75 | { 76 | ThrowIfInvalid(); 77 | var native = ImpellerNative.ImpellerParagraphBuilderBuildParagraphNew(handle, width); 78 | return ImpellerParagraphHandle.FromOwned(native); 79 | } 80 | 81 | protected override bool ReleaseHandle() 82 | { 83 | ImpellerNative.ImpellerParagraphBuilderRelease(handle); 84 | return true; 85 | } 86 | 87 | internal new nint DangerousGetHandle() 88 | { 89 | ThrowIfInvalid(); 90 | return handle; 91 | } 92 | 93 | private void ThrowIfInvalid() 94 | { 95 | if (IsInvalid) 96 | { 97 | throw new ObjectDisposedException(nameof(ImpellerParagraphBuilderHandle)); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.Paint.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintNew")] 8 | [SuppressGCTransition] 9 | internal static partial nint ImpellerPaintNew(); 10 | 11 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintRetain")] 12 | [SuppressGCTransition] 13 | internal static partial void ImpellerPaintRetain(nint paint); 14 | 15 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintRelease")] 16 | [SuppressGCTransition] 17 | internal static partial void ImpellerPaintRelease(nint paint); 18 | 19 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintSetColor")] 20 | [SuppressGCTransition] 21 | internal static unsafe partial void ImpellerPaintSetColor(nint paint, ImpellerColor* color); 22 | 23 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintSetBlendMode")] 24 | [SuppressGCTransition] 25 | internal static partial void ImpellerPaintSetBlendMode(nint paint, ImpellerBlendMode blendMode); 26 | 27 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintSetDrawStyle")] 28 | [SuppressGCTransition] 29 | internal static partial void ImpellerPaintSetDrawStyle(nint paint, ImpellerDrawStyle drawStyle); 30 | 31 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintSetStrokeCap")] 32 | [SuppressGCTransition] 33 | internal static partial void ImpellerPaintSetStrokeCap(nint paint, ImpellerStrokeCap cap); 34 | 35 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintSetStrokeJoin")] 36 | [SuppressGCTransition] 37 | internal static partial void ImpellerPaintSetStrokeJoin(nint paint, ImpellerStrokeJoin join); 38 | 39 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintSetStrokeWidth")] 40 | [SuppressGCTransition] 41 | internal static partial void ImpellerPaintSetStrokeWidth(nint paint, float width); 42 | 43 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintSetStrokeMiter")] 44 | [SuppressGCTransition] 45 | internal static partial void ImpellerPaintSetStrokeMiter(nint paint, float miter); 46 | 47 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintSetColorSource")] 48 | [SuppressGCTransition] 49 | internal static partial void ImpellerPaintSetColorSource(nint paint, nint colorSource); 50 | 51 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintSetColorFilter")] 52 | [SuppressGCTransition] 53 | internal static partial void ImpellerPaintSetColorFilter(nint paint, nint colorFilter); 54 | 55 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintSetImageFilter")] 56 | [SuppressGCTransition] 57 | internal static partial void ImpellerPaintSetImageFilter(nint paint, nint imageFilter); 58 | 59 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPaintSetMaskFilter")] 60 | [SuppressGCTransition] 61 | internal static partial void ImpellerPaintSetMaskFilter(nint paint, nint maskFilter); 62 | } 63 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerLineMetricsHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed unsafe class ImpellerLineMetricsHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerLineMetricsHandle(nint native) 8 | { 9 | SetHandle(native); 10 | } 11 | 12 | internal static ImpellerLineMetricsHandle? FromOwned(nint native) 13 | { 14 | return native == nint.Zero 15 | ? null 16 | : new ImpellerLineMetricsHandle(native); 17 | } 18 | 19 | internal void Retain() 20 | { 21 | ThrowIfInvalid(); 22 | ImpellerNative.ImpellerLineMetricsRetain(handle); 23 | } 24 | 25 | public double GetUnscaledAscent(uint line) => Query(line, ImpellerNative.ImpellerLineMetricsGetUnscaledAscent); 26 | 27 | public double GetAscent(uint line) => Query(line, ImpellerNative.ImpellerLineMetricsGetAscent); 28 | 29 | public double GetDescent(uint line) => Query(line, ImpellerNative.ImpellerLineMetricsGetDescent); 30 | 31 | public double GetBaseline(uint line) => Query(line, ImpellerNative.ImpellerLineMetricsGetBaseline); 32 | 33 | public bool IsHardBreak(uint line) 34 | { 35 | ThrowIfInvalid(); 36 | return ImpellerNative.ImpellerLineMetricsIsHardbreak(handle, line); 37 | } 38 | 39 | public double GetWidth(uint line) => Query(line, ImpellerNative.ImpellerLineMetricsGetWidth); 40 | 41 | public double GetHeight(uint line) => Query(line, ImpellerNative.ImpellerLineMetricsGetHeight); 42 | 43 | public double GetLeft(uint line) => Query(line, ImpellerNative.ImpellerLineMetricsGetLeft); 44 | 45 | public ulong GetCodeUnitStartIndex(uint line) 46 | { 47 | ThrowIfInvalid(); 48 | return ImpellerNative.ImpellerLineMetricsGetCodeUnitStartIndex(handle, line); 49 | } 50 | 51 | public ulong GetCodeUnitEndIndex(uint line) 52 | { 53 | ThrowIfInvalid(); 54 | return ImpellerNative.ImpellerLineMetricsGetCodeUnitEndIndex(handle, line); 55 | } 56 | 57 | public ulong GetCodeUnitEndIndexExcludingWhitespace(uint line) 58 | { 59 | ThrowIfInvalid(); 60 | return ImpellerNative.ImpellerLineMetricsGetCodeUnitEndIndexExcludingWhitespace(handle, line); 61 | } 62 | 63 | public ulong GetCodeUnitEndIndexIncludingNewline(uint line) 64 | { 65 | ThrowIfInvalid(); 66 | return ImpellerNative.ImpellerLineMetricsGetCodeUnitEndIndexIncludingNewline(handle, line); 67 | } 68 | 69 | protected override bool ReleaseHandle() 70 | { 71 | ImpellerNative.ImpellerLineMetricsRelease(handle); 72 | return true; 73 | } 74 | 75 | internal new nint DangerousGetHandle() 76 | { 77 | ThrowIfInvalid(); 78 | return handle; 79 | } 80 | 81 | private double Query(uint line, Func accessor) 82 | { 83 | ThrowIfInvalid(); 84 | return accessor(handle, line); 85 | } 86 | 87 | private void ThrowIfInvalid() 88 | { 89 | if (IsInvalid) 90 | { 91 | throw new ObjectDisposedException(nameof(ImpellerLineMetricsHandle)); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /docs/ci-plan.md: -------------------------------------------------------------------------------- 1 | # CI Automation & Packaging Plan 2 | 3 | ## 6.1 Native + Managed Build Automation 4 | 5 | ### Pipeline Layout 6 | 7 | 1. **Native Build Stage (macOS & Linux)** 8 | - Checkout repo, run `gclient sync`. 9 | - Execute GN/Ninja build for Impeller targets (`engine/src/out/impeller_host_debug`). 10 | - Archive generated Impeller SDK artifacts (`impeller.h`, metallibs, dylibs). 11 | - Upload build outputs as pipeline artifacts. 12 | 13 | 2. **Managed Build/Test Stage (Windows/macOS/Linux)** 14 | - Restore .NET SDK (>= 8.0), run `dotnet build` for `ImpellerSharp.Interop`. 15 | - Execute unit tests/benchmarks gating using `dotnet test`/`BenchmarkDotNet`. 16 | - Consume native artifacts from Stage 1 (download and place on `PATH`). 17 | 18 | 3. **Packaging Stage** 19 | - Produce NuGet package with managed binaries + native assets (RID-specific). 20 | - Publish to internal feed (GitHub Packages/Azure Artifacts) using token. 21 | 22 | ### Recommended GitHub Actions Outline 23 | ```yaml 24 | name: Impeller CI 25 | 26 | on: 27 | push: 28 | branches: [ main ] 29 | pull_request: 30 | 31 | jobs: 32 | native-build: 33 | runs-on: macos-14 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: Install dependencies 37 | run: brew install ninja 38 | - name: Sync dependencies 39 | run: gclient sync 40 | - name: Build Impeller 41 | run: ./engine/src/flutter/tools/gn --unoptimized --runtime-mode debug --mac --mac-cpu arm64 --target-dir impeller_host_debug && ninja -C engine/src/out/impeller_host_debug flutter/impeller:impeller 42 | - name: Upload native artifacts 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: impeller-native-macos-arm64 46 | path: | 47 | engine/src/out/impeller_host_debug/libimpeller.dylib 48 | engine/src/out/impeller_host_debug/impeller.h 49 | 50 | managed-build: 51 | runs-on: ubuntu-22.04 52 | needs: native-build 53 | steps: 54 | - uses: actions/checkout@v4 55 | - uses: actions/download-artifact@v4 56 | with: 57 | name: impeller-native-macos-arm64 58 | path: native 59 | - name: Install .NET 60 | uses: actions/setup-dotnet@v4 61 | with: 62 | dotnet-version: '8.0.x' 63 | - name: Build managed library 64 | run: dotnet build src/ImpellerSharp.Interop -c Release 65 | - name: Run tests 66 | run: dotnet test tests/ImpellerSharp.Tests -c Release 67 | 68 | package: 69 | runs-on: ubuntu-22.04 70 | needs: managed-build 71 | steps: 72 | - uses: actions/checkout@v4 73 | - name: Pack NuGet 74 | run: dotnet pack src/ImpellerSharp.Interop -c Release -o artifacts 75 | - name: Publish package 76 | if: github.event_name == 'push' 77 | run: dotnet nuget push artifacts/*.nupkg --source ${{ secrets.NUGET_FEED }} --api-key ${{ secrets.NUGET_API_KEY }} 78 | ``` 79 | 80 | ### Considerations 81 | 82 | - Add matrix builds for Windows/Linux RIDs once native builds exist for those platforms. 83 | - Integrate BenchmarkDotNet job to gather perf numbers post-build. 84 | - Cache `gclient` downloads using Actions cache to reduce sync times. 85 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.ColorSource.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorSourceRetain")] 8 | [SuppressGCTransition] 9 | internal static partial void ImpellerColorSourceRetain(nint colorSource); 10 | 11 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorSourceRelease")] 12 | [SuppressGCTransition] 13 | internal static partial void ImpellerColorSourceRelease(nint colorSource); 14 | 15 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorSourceCreateLinearGradientNew")] 16 | [SuppressGCTransition] 17 | internal static unsafe partial nint ImpellerColorSourceCreateLinearGradientNew( 18 | ImpellerPoint* startPoint, 19 | ImpellerPoint* endPoint, 20 | uint stopCount, 21 | ImpellerColor* colors, 22 | float* stops, 23 | ImpellerTileMode tileMode, 24 | ImpellerMatrix* transform); 25 | 26 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorSourceCreateRadialGradientNew")] 27 | [SuppressGCTransition] 28 | internal static unsafe partial nint ImpellerColorSourceCreateRadialGradientNew( 29 | ImpellerPoint* center, 30 | float radius, 31 | uint stopCount, 32 | ImpellerColor* colors, 33 | float* stops, 34 | ImpellerTileMode tileMode, 35 | ImpellerMatrix* transform); 36 | 37 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorSourceCreateConicalGradientNew")] 38 | [SuppressGCTransition] 39 | internal static unsafe partial nint ImpellerColorSourceCreateConicalGradientNew( 40 | ImpellerPoint* startCenter, 41 | float startRadius, 42 | ImpellerPoint* endCenter, 43 | float endRadius, 44 | uint stopCount, 45 | ImpellerColor* colors, 46 | float* stops, 47 | ImpellerTileMode tileMode, 48 | ImpellerMatrix* transform); 49 | 50 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorSourceCreateSweepGradientNew")] 51 | [SuppressGCTransition] 52 | internal static unsafe partial nint ImpellerColorSourceCreateSweepGradientNew( 53 | ImpellerPoint* center, 54 | float start, 55 | float end, 56 | uint stopCount, 57 | ImpellerColor* colors, 58 | float* stops, 59 | ImpellerTileMode tileMode, 60 | ImpellerMatrix* transform); 61 | 62 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorSourceCreateImageNew")] 63 | [SuppressGCTransition] 64 | internal static unsafe partial nint ImpellerColorSourceCreateImageNew( 65 | nint texture, 66 | ImpellerTileMode horizontalTileMode, 67 | ImpellerTileMode verticalTileMode, 68 | ImpellerTextureSampling sampling, 69 | ImpellerMatrix* transform); 70 | 71 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerColorSourceCreateFragmentProgramNew")] 72 | [SuppressGCTransition] 73 | internal static unsafe partial nint ImpellerColorSourceCreateFragmentProgramNew( 74 | nint context, 75 | nint fragmentProgram, 76 | nint* samplers, 77 | nuint samplersCount, 78 | byte* data, 79 | nuint dataLength); 80 | } 81 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerCommandPointers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace ImpellerSharp.Interop; 6 | 7 | internal static unsafe class ImpellerCommandPointers 8 | { 9 | private static readonly nint LibraryHandle = LoadLibrary(); 10 | 11 | internal static readonly delegate* unmanaged[Cdecl] 12 | SurfaceDrawDisplayList = (delegate* unmanaged[Cdecl]) 13 | GetExport(nameof(ImpellerNative.ImpellerSurfaceDrawDisplayList)); 14 | 15 | private static nint LoadLibrary() 16 | { 17 | if (NativeLibrary.TryLoad(ImpellerLibrary.Name, out var handle)) 18 | { 19 | return handle; 20 | } 21 | 22 | var baseDirectory = AppContext.BaseDirectory; 23 | foreach (var candidate in GetPlatformCandidates(baseDirectory)) 24 | { 25 | if (File.Exists(candidate) && NativeLibrary.TryLoad(candidate, out handle)) 26 | { 27 | return handle; 28 | } 29 | } 30 | 31 | throw new ImpellerInteropException($"Unable to load native library '{ImpellerLibrary.Name}'."); 32 | } 33 | 34 | private static string[] GetPlatformCandidates(string baseDirectory) 35 | { 36 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 37 | { 38 | return new[] 39 | { 40 | Path.Combine(baseDirectory, "libimpeller.dylib"), 41 | Path.Combine(baseDirectory, "impeller.dylib"), 42 | Path.Combine(baseDirectory, "runtimes", "osx-arm64", "native", "libimpeller.dylib"), 43 | Path.Combine(baseDirectory, "runtimes", "osx-x64", "native", "libimpeller.dylib"), 44 | }; 45 | } 46 | 47 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 48 | { 49 | return new[] 50 | { 51 | Path.Combine(baseDirectory, "impeller.dll"), 52 | Path.Combine(baseDirectory, "runtimes", "win-x64", "native", "impeller.dll"), 53 | Path.Combine(baseDirectory, "runtimes", "win-x86", "native", "impeller.dll"), 54 | Path.Combine(baseDirectory, "runtimes", "win-arm64", "native", "impeller.dll"), 55 | }; 56 | } 57 | 58 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 59 | { 60 | return new[] 61 | { 62 | Path.Combine(baseDirectory, "libimpeller.so"), 63 | Path.Combine(baseDirectory, "impeller.so"), 64 | Path.Combine(baseDirectory, "runtimes", "linux-x64", "native", "libimpeller.so"), 65 | Path.Combine(baseDirectory, "runtimes", "linux-arm", "native", "libimpeller.so"), 66 | Path.Combine(baseDirectory, "runtimes", "linux-arm64", "native", "libimpeller.so"), 67 | }; 68 | } 69 | 70 | return Array.Empty(); 71 | } 72 | 73 | private static nint GetExport(string managedName) 74 | { 75 | // Managed name matches the entry point for the exports we cache. 76 | if (!NativeLibrary.TryGetExport(LibraryHandle, managedName, out var address)) 77 | { 78 | throw new ImpellerInteropException($"Unable to locate native export '{managedName}'."); 79 | } 80 | 81 | return address; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.LineMetrics.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsRetain")] 8 | [SuppressGCTransition] 9 | internal static partial void ImpellerLineMetricsRetain(nint metrics); 10 | 11 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsRelease")] 12 | [SuppressGCTransition] 13 | internal static partial void ImpellerLineMetricsRelease(nint metrics); 14 | 15 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsGetUnscaledAscent")] 16 | [SuppressGCTransition] 17 | internal static partial double ImpellerLineMetricsGetUnscaledAscent(nint metrics, nuint line); 18 | 19 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsGetAscent")] 20 | [SuppressGCTransition] 21 | internal static partial double ImpellerLineMetricsGetAscent(nint metrics, nuint line); 22 | 23 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsGetDescent")] 24 | [SuppressGCTransition] 25 | internal static partial double ImpellerLineMetricsGetDescent(nint metrics, nuint line); 26 | 27 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsGetBaseline")] 28 | [SuppressGCTransition] 29 | internal static partial double ImpellerLineMetricsGetBaseline(nint metrics, nuint line); 30 | 31 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsIsHardbreak")] 32 | [SuppressGCTransition] 33 | [return: MarshalAs(UnmanagedType.I1)] 34 | internal static partial bool ImpellerLineMetricsIsHardbreak(nint metrics, nuint line); 35 | 36 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsGetWidth")] 37 | [SuppressGCTransition] 38 | internal static partial double ImpellerLineMetricsGetWidth(nint metrics, nuint line); 39 | 40 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsGetHeight")] 41 | [SuppressGCTransition] 42 | internal static partial double ImpellerLineMetricsGetHeight(nint metrics, nuint line); 43 | 44 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsGetLeft")] 45 | [SuppressGCTransition] 46 | internal static partial double ImpellerLineMetricsGetLeft(nint metrics, nuint line); 47 | 48 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsGetCodeUnitStartIndex")] 49 | [SuppressGCTransition] 50 | internal static partial nuint ImpellerLineMetricsGetCodeUnitStartIndex(nint metrics, nuint line); 51 | 52 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsGetCodeUnitEndIndex")] 53 | [SuppressGCTransition] 54 | internal static partial nuint ImpellerLineMetricsGetCodeUnitEndIndex(nint metrics, nuint line); 55 | 56 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsGetCodeUnitEndIndexExcludingWhitespace")] 57 | [SuppressGCTransition] 58 | internal static partial nuint ImpellerLineMetricsGetCodeUnitEndIndexExcludingWhitespace(nint metrics, nuint line); 59 | 60 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerLineMetricsGetCodeUnitEndIndexIncludingNewline")] 61 | [SuppressGCTransition] 62 | internal static partial nuint ImpellerLineMetricsGetCodeUnitEndIndexIncludingNewline(nint metrics, nuint line); 63 | } 64 | -------------------------------------------------------------------------------- /benchmarks/ImpellerSharp.Benchmarks/NativeDisplayListBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Attributes; 3 | using ImpellerSharp.Interop; 4 | 5 | namespace ImpellerSharp.Benchmarks; 6 | 7 | [MemoryDiagnoser] 8 | [ShortRunJob] 9 | public unsafe class NativeDisplayListBenchmark : IDisposable 10 | { 11 | private nint _paint; 12 | private ImpellerRect[] _rects = Array.Empty(); 13 | private bool _initialized; 14 | private string? _skipReason; 15 | 16 | [Params(256, 1024)] 17 | public int RectCount { get; set; } 18 | 19 | [GlobalSetup] 20 | public void GlobalSetup() 21 | { 22 | try 23 | { 24 | _paint = NativeDisplayListInterop.PaintNew(); 25 | if (_paint == nint.Zero) 26 | { 27 | throw new InvalidOperationException("ImpellerPaintNew returned null."); 28 | } 29 | 30 | var color = new ImpellerColor(1f, 0.2f, 0.1f, 1f); 31 | NativeDisplayListInterop.PaintSetColor(_paint, &color); 32 | _rects = RectSequenceCache.GetRects(RectCount); 33 | _initialized = true; 34 | } 35 | catch (Exception ex) 36 | { 37 | _skipReason = ex.Message; 38 | _initialized = false; 39 | } 40 | } 41 | 42 | [GlobalCleanup] 43 | public void GlobalCleanup() 44 | { 45 | Dispose(); 46 | } 47 | 48 | [Benchmark(Description = "Native ABI display list build (rects)")] 49 | public int BuildDisplayListNative() 50 | { 51 | EnsureInitialized(); 52 | 53 | var builder = NativeDisplayListInterop.DisplayListBuilderNew(null); 54 | if (builder == nint.Zero) 55 | { 56 | throw new InvalidOperationException("ImpellerDisplayListBuilderNew returned null."); 57 | } 58 | 59 | var paint = _paint; 60 | var rects = _rects; 61 | if (rects.Length != RectCount) 62 | { 63 | rects = _rects = RectSequenceCache.GetRects(RectCount); 64 | } 65 | 66 | try 67 | { 68 | for (var i = 0; i < rects.Length; i++) 69 | { 70 | var rect = rects[i]; 71 | NativeDisplayListInterop.DisplayListBuilderDrawRect(builder, &rect, paint); 72 | } 73 | 74 | var displayList = NativeDisplayListInterop.DisplayListBuilderCreateDisplayListNew(builder); 75 | if (displayList == nint.Zero) 76 | { 77 | throw new InvalidOperationException("ImpellerDisplayListBuilderCreateDisplayListNew returned null."); 78 | } 79 | 80 | NativeDisplayListInterop.DisplayListRelease(displayList); 81 | } 82 | finally 83 | { 84 | NativeDisplayListInterop.DisplayListBuilderRelease(builder); 85 | } 86 | 87 | return rects.Length; 88 | } 89 | 90 | private void EnsureInitialized() 91 | { 92 | if (_initialized) 93 | { 94 | return; 95 | } 96 | 97 | throw new InvalidOperationException( 98 | $"Native benchmark prerequisites are missing: {_skipReason ?? "unknown error resolving native exports."} " + 99 | "Ensure the Impeller native library is available and built before running benchmarks."); 100 | } 101 | 102 | public void Dispose() 103 | { 104 | if (_paint != nint.Zero) 105 | { 106 | NativeDisplayListInterop.PaintRelease(_paint); 107 | _paint = nint.Zero; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /samples/BasicShapes/BackendContextFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ImpellerSharp.Interop; 4 | 5 | namespace ImpellerSharp.Samples.BasicShapes; 6 | 7 | internal static class BackendContextFactory 8 | { 9 | internal static bool TryCreateContext(SampleOptions options, out ImpellerContextHandle? context, out string resolvedBackend, out string? error) 10 | { 11 | string? lastError = null; 12 | foreach (var candidate in ResolveBackends(options.Backend)) 13 | { 14 | try 15 | { 16 | ImpellerContextHandle handle; 17 | string actualBackend; 18 | switch (candidate) 19 | { 20 | case "metal": 21 | handle = CreateMetalContext(); 22 | actualBackend = "metal"; 23 | break; 24 | case "vulkan": 25 | handle = CreateVulkanContext(options, out actualBackend); 26 | break; 27 | default: 28 | throw new NotSupportedException($"Backend '{candidate}' is not supported in headless mode."); 29 | } 30 | 31 | context = handle; 32 | resolvedBackend = actualBackend; 33 | error = null; 34 | return true; 35 | } 36 | catch (Exception ex) 37 | { 38 | lastError = ex.Message; 39 | } 40 | } 41 | 42 | context = null; 43 | resolvedBackend = options.Backend; 44 | error = lastError ?? "No suitable backend available."; 45 | return false; 46 | } 47 | 48 | private static IEnumerable ResolveBackends(string backendPreference) 49 | { 50 | switch (backendPreference) 51 | { 52 | case "auto": 53 | if (OperatingSystem.IsMacOS()) 54 | { 55 | yield return "metal"; 56 | yield return "vulkan"; 57 | } 58 | else 59 | { 60 | yield return "vulkan"; 61 | } 62 | break; 63 | case "metal": 64 | yield return "metal"; 65 | break; 66 | case "vulkan": 67 | yield return "vulkan"; 68 | break; 69 | case "opengles": 70 | throw new NotSupportedException("OpenGLES backend is not yet supported in this sample."); 71 | default: 72 | throw new ArgumentException($"Unsupported backend '{backendPreference}'.", nameof(backendPreference)); 73 | } 74 | } 75 | 76 | private static ImpellerContextHandle CreateMetalContext() 77 | { 78 | if (!OperatingSystem.IsMacOS()) 79 | { 80 | throw new PlatformNotSupportedException("Metal backend is only available on macOS."); 81 | } 82 | 83 | return ImpellerContextHandle.CreateMetal(); 84 | } 85 | 86 | private static ImpellerContextHandle CreateVulkanContext(SampleOptions options, out string resolvedBackend) 87 | { 88 | if (!VulkanLoader.TryCreateSettings(out var settings, out var failure)) 89 | { 90 | throw new InvalidOperationException(failure ?? "Unable to configure Vulkan backend."); 91 | } 92 | 93 | resolvedBackend = "vulkan"; 94 | var context = ImpellerContextHandle.CreateVulkan(settings); 95 | VulkanSwapchainProbe.Run(context, options.VulkanSurface); 96 | return context; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/ImpellerNative.PathBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | internal static partial class ImpellerNative 6 | { 7 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderNew")] 8 | [SuppressGCTransition] 9 | internal static partial nint ImpellerPathBuilderNew(); 10 | 11 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderRetain")] 12 | [SuppressGCTransition] 13 | internal static partial void ImpellerPathBuilderRetain(nint builder); 14 | 15 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderRelease")] 16 | [SuppressGCTransition] 17 | internal static partial void ImpellerPathBuilderRelease(nint builder); 18 | 19 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderMoveTo")] 20 | [SuppressGCTransition] 21 | internal static unsafe partial void ImpellerPathBuilderMoveTo(nint builder, ImpellerPoint* location); 22 | 23 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderLineTo")] 24 | [SuppressGCTransition] 25 | internal static unsafe partial void ImpellerPathBuilderLineTo(nint builder, ImpellerPoint* location); 26 | 27 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderQuadraticCurveTo")] 28 | [SuppressGCTransition] 29 | internal static unsafe partial void ImpellerPathBuilderQuadraticCurveTo( 30 | nint builder, 31 | ImpellerPoint* controlPoint, 32 | ImpellerPoint* endPoint); 33 | 34 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderCubicCurveTo")] 35 | [SuppressGCTransition] 36 | internal static unsafe partial void ImpellerPathBuilderCubicCurveTo( 37 | nint builder, 38 | ImpellerPoint* controlPoint1, 39 | ImpellerPoint* controlPoint2, 40 | ImpellerPoint* endPoint); 41 | 42 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderTakePathNew")] 43 | [SuppressGCTransition] 44 | internal static partial nint ImpellerPathBuilderTakePathNew(nint builder, ImpellerFillType fillType); 45 | 46 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderAddRect")] 47 | [SuppressGCTransition] 48 | internal static unsafe partial void ImpellerPathBuilderAddRect(nint builder, ImpellerRect* rect); 49 | 50 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderAddArc")] 51 | [SuppressGCTransition] 52 | internal static unsafe partial void ImpellerPathBuilderAddArc( 53 | nint builder, 54 | ImpellerRect* ovalBounds, 55 | float startAngleDegrees, 56 | float endAngleDegrees); 57 | 58 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderAddOval")] 59 | [SuppressGCTransition] 60 | internal static unsafe partial void ImpellerPathBuilderAddOval(nint builder, ImpellerRect* ovalBounds); 61 | 62 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderAddRoundedRect")] 63 | [SuppressGCTransition] 64 | internal static unsafe partial void ImpellerPathBuilderAddRoundedRect( 65 | nint builder, 66 | ImpellerRect* rect, 67 | ImpellerRoundingRadii* roundingRadii); 68 | 69 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderClose")] 70 | [SuppressGCTransition] 71 | internal static partial void ImpellerPathBuilderClose(nint builder); 72 | 73 | [LibraryImport(ImpellerLibrary.Name, EntryPoint = "ImpellerPathBuilderCopyPathNew")] 74 | [SuppressGCTransition] 75 | internal static partial nint ImpellerPathBuilderCopyPathNew(nint builder, ImpellerFillType fillType); 76 | } 77 | -------------------------------------------------------------------------------- /benchmarks/ImpellerSharp.Benchmarks/TextureUploadBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Attributes; 3 | using ImpellerSharp.Interop; 4 | 5 | namespace ImpellerSharp.Benchmarks; 6 | 7 | [MemoryDiagnoser] 8 | [ShortRunJob] 9 | public unsafe class TextureUploadBenchmark : IDisposable 10 | { 11 | private BenchmarkContextProvider? _contextProvider; 12 | private ImpellerTextureDescriptor _descriptor; 13 | private byte[] _pixels = Array.Empty(); 14 | private bool _initialized; 15 | private string? _skipReason; 16 | 17 | [Params(128, 256)] 18 | public int TextureSize { get; set; } 19 | 20 | [GlobalSetup] 21 | public void GlobalSetup() 22 | { 23 | _contextProvider = new BenchmarkContextProvider(); 24 | 25 | if (!_contextProvider.TryEnsureContext(out var failure)) 26 | { 27 | _skipReason = failure ?? "Unknown context creation failure."; 28 | _initialized = false; 29 | return; 30 | } 31 | 32 | InitialiseTexture(TextureSize); 33 | _initialized = true; 34 | } 35 | 36 | [GlobalCleanup] 37 | public void GlobalCleanup() 38 | { 39 | Dispose(); 40 | } 41 | 42 | [Benchmark(Description = "Managed texture upload (TextureFactory)")] 43 | public int ManagedTextureUpload() 44 | { 45 | EnsureInitialized(); 46 | 47 | var context = _contextProvider!.Context!; 48 | var descriptor = _descriptor; 49 | 50 | using var texture = context.CreateTexture(descriptor, _pixels); 51 | return (int)descriptor.Size.Width; 52 | } 53 | 54 | [Benchmark(Description = "Native texture upload (C ABI)")] 55 | public int NativeTextureUpload() 56 | { 57 | EnsureInitialized(); 58 | 59 | var context = _contextProvider!.Context!; 60 | var descriptor = _descriptor; 61 | 62 | fixed (byte* data = _pixels) 63 | { 64 | var mapping = new NativeDisplayListInterop.NativeImpellerMapping 65 | { 66 | Data = data, 67 | Length = (ulong)_pixels.Length, 68 | OnRelease = null, 69 | }; 70 | 71 | var texture = NativeDisplayListInterop.TextureCreateWithContentsNew( 72 | context.DangerousGetHandle(), 73 | descriptor, 74 | mapping, 75 | nint.Zero); 76 | 77 | if (texture == nint.Zero) 78 | { 79 | throw new InvalidOperationException("ImpellerTextureCreateWithContentsNew returned null."); 80 | } 81 | 82 | NativeDisplayListInterop.TextureRelease(texture); 83 | } 84 | 85 | return (int)descriptor.Size.Width; 86 | } 87 | 88 | public void Dispose() 89 | { 90 | _contextProvider?.Dispose(); 91 | _contextProvider = null; 92 | } 93 | 94 | private void InitialiseTexture(int size) 95 | { 96 | _descriptor = new ImpellerTextureDescriptor( 97 | ImpellerPixelFormat.Rgba8888, 98 | new ImpellerISize(size, size)); 99 | 100 | var totalBytes = checked(size * size * 4); 101 | _pixels = new byte[totalBytes]; 102 | 103 | var seed = new Random(1234); 104 | seed.NextBytes(_pixels); 105 | } 106 | 107 | private void EnsureInitialized() 108 | { 109 | if (_initialized) 110 | { 111 | return; 112 | } 113 | 114 | throw new InvalidOperationException( 115 | $"Texture upload benchmark prerequisites are missing: {_skipReason ?? "unknown error."} "); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerTextureHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | public sealed unsafe class ImpellerTextureHandle : ImpellerSafeHandle 7 | { 8 | private ImpellerTextureHandle(nint handle) 9 | { 10 | SetHandle(handle); 11 | } 12 | 13 | internal static ImpellerTextureHandle FromOwned(nint native) 14 | { 15 | return new ImpellerTextureHandle( 16 | ImpellerSafeHandle.EnsureSuccess(native, "Failed to create Impeller texture.")); 17 | } 18 | 19 | public static ImpellerTextureHandle CreateWithContents( 20 | ImpellerContextHandle context, 21 | in ImpellerTextureDescriptor descriptor, 22 | ReadOnlySpan contents) 23 | { 24 | if (context is null) 25 | { 26 | throw new ArgumentNullException(nameof(context)); 27 | } 28 | 29 | if (contents.IsEmpty) 30 | { 31 | throw new ArgumentException("Texture data must not be empty.", nameof(contents)); 32 | } 33 | 34 | var mapping = ImpellerMappingUtilities.Create(contents, out var userData); 35 | var addedRef = false; 36 | nint native = nint.Zero; 37 | 38 | try 39 | { 40 | context.DangerousAddRef(ref addedRef); 41 | native = ImpellerNative.ImpellerTextureCreateWithContentsNew( 42 | context.DangerousGetHandle(), 43 | in descriptor, 44 | in mapping, 45 | userData); 46 | } 47 | finally 48 | { 49 | if (native == nint.Zero) 50 | { 51 | ImpellerMappingUtilities.Free(userData); 52 | } 53 | 54 | if (addedRef) 55 | { 56 | context.DangerousRelease(); 57 | } 58 | } 59 | 60 | return FromOwned(native); 61 | } 62 | 63 | public static ImpellerTextureHandle CreateWithOpenGLHandle( 64 | ImpellerContextHandle context, 65 | in ImpellerTextureDescriptor descriptor, 66 | ulong textureHandle) 67 | { 68 | if (context is null) 69 | { 70 | throw new ArgumentNullException(nameof(context)); 71 | } 72 | 73 | var addedRef = false; 74 | nint native = nint.Zero; 75 | 76 | try 77 | { 78 | context.DangerousAddRef(ref addedRef); 79 | native = ImpellerNative.ImpellerTextureCreateWithOpenGLTextureHandleNew( 80 | context.DangerousGetHandle(), 81 | in descriptor, 82 | textureHandle); 83 | } 84 | finally 85 | { 86 | if (addedRef) 87 | { 88 | context.DangerousRelease(); 89 | } 90 | } 91 | 92 | return FromOwned(native); 93 | } 94 | 95 | public ulong GetOpenGLHandle() 96 | { 97 | ThrowIfInvalid(); 98 | return ImpellerNative.ImpellerTextureGetOpenGLHandle(handle); 99 | } 100 | 101 | internal void Retain() 102 | { 103 | ThrowIfInvalid(); 104 | ImpellerNative.ImpellerTextureRetain(handle); 105 | } 106 | 107 | protected override bool ReleaseHandle() 108 | { 109 | ImpellerNative.ImpellerTextureRelease(handle); 110 | return true; 111 | } 112 | 113 | internal new nint DangerousGetHandle() 114 | { 115 | ThrowIfInvalid(); 116 | return handle; 117 | } 118 | 119 | private void ThrowIfInvalid() 120 | { 121 | if (IsInvalid) 122 | { 123 | throw new ObjectDisposedException(nameof(ImpellerTextureHandle)); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /samples/BasicShapes/VulkanLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | using ImpellerSharp.Interop; 6 | 7 | namespace ImpellerSharp.Samples.BasicShapes; 8 | 9 | internal static unsafe class VulkanLoader 10 | { 11 | private static readonly object Gate = new(); 12 | private static bool _attemptedLoad; 13 | private static IntPtr _vulkanLibrary; 14 | private static unsafe delegate* unmanaged[Cdecl] _vkGetInstanceProcAddr; 15 | private static readonly ImpellerVulkanProcAddressCallback Callback = ResolveProcAddress; 16 | 17 | internal static bool TryCreateSettings(out ImpellerContextVulkanSettings settings, out string? error) 18 | { 19 | lock (Gate) 20 | { 21 | if (!EnsureLoader(out error)) 22 | { 23 | settings = default; 24 | return false; 25 | } 26 | } 27 | 28 | settings = new ImpellerContextVulkanSettings 29 | { 30 | ProcAddressCallback = Callback, 31 | UserData = IntPtr.Zero, 32 | EnableValidation = false, 33 | }; 34 | error = null; 35 | return true; 36 | } 37 | 38 | private static bool EnsureLoader(out string? error) 39 | { 40 | if (_vkGetInstanceProcAddr != null) 41 | { 42 | error = null; 43 | return true; 44 | } 45 | 46 | if (_attemptedLoad && _vkGetInstanceProcAddr == null) 47 | { 48 | error = "Vulkan loader not found or vkGetInstanceProcAddr missing."; 49 | return false; 50 | } 51 | 52 | _attemptedLoad = true; 53 | 54 | foreach (var name in EnumerateCandidateLibraries()) 55 | { 56 | if (NativeLibrary.TryLoad(name, out var handle)) 57 | { 58 | _vulkanLibrary = handle; 59 | break; 60 | } 61 | } 62 | 63 | if (_vulkanLibrary == IntPtr.Zero) 64 | { 65 | error = "Unable to locate Vulkan loader (libvulkan)."; 66 | return false; 67 | } 68 | 69 | if (!NativeLibrary.TryGetExport(_vulkanLibrary, "vkGetInstanceProcAddr", out var export)) 70 | { 71 | error = "vkGetInstanceProcAddr symbol missing from Vulkan loader."; 72 | return false; 73 | } 74 | 75 | unsafe 76 | { 77 | _vkGetInstanceProcAddr = (delegate* unmanaged[Cdecl])export; 78 | } 79 | 80 | error = null; 81 | return true; 82 | } 83 | 84 | private static IEnumerable EnumerateCandidateLibraries() 85 | { 86 | if (OperatingSystem.IsWindows()) 87 | { 88 | yield return "vulkan-1.dll"; 89 | } 90 | else if (OperatingSystem.IsMacOS()) 91 | { 92 | yield return "libvulkan.dylib"; 93 | yield return "/usr/local/lib/libvulkan.dylib"; 94 | } 95 | else 96 | { 97 | yield return "libvulkan.so.1"; 98 | yield return "libvulkan.so"; 99 | } 100 | } 101 | 102 | private static unsafe IntPtr ResolveProcAddress(IntPtr instance, string procName, IntPtr userData) 103 | { 104 | if (_vkGetInstanceProcAddr == null) 105 | { 106 | return IntPtr.Zero; 107 | } 108 | 109 | if (procName == "vkGetInstanceProcAddr") 110 | { 111 | return (IntPtr)_vkGetInstanceProcAddr; 112 | } 113 | 114 | var utf8 = Encoding.UTF8.GetBytes(procName + "\0"); 115 | fixed (byte* namePtr = utf8) 116 | { 117 | return _vkGetInstanceProcAddr(instance, namePtr); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerParagraphHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed class ImpellerParagraphHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerParagraphHandle(nint native) 8 | { 9 | SetHandle(native); 10 | } 11 | 12 | internal static ImpellerParagraphHandle FromOwned(nint native) 13 | { 14 | return new ImpellerParagraphHandle( 15 | EnsureSuccess(native, "Failed to create Impeller paragraph.")); 16 | } 17 | 18 | internal void Retain() 19 | { 20 | ThrowIfInvalid(); 21 | ImpellerNative.ImpellerParagraphRetain(handle); 22 | } 23 | 24 | public float GetMaxWidth() 25 | { 26 | ThrowIfInvalid(); 27 | return ImpellerNative.ImpellerParagraphGetMaxWidth(handle); 28 | } 29 | 30 | public float GetHeight() 31 | { 32 | ThrowIfInvalid(); 33 | return ImpellerNative.ImpellerParagraphGetHeight(handle); 34 | } 35 | 36 | public float GetLongestLineWidth() 37 | { 38 | ThrowIfInvalid(); 39 | return ImpellerNative.ImpellerParagraphGetLongestLineWidth(handle); 40 | } 41 | 42 | public float GetMinIntrinsicWidth() 43 | { 44 | ThrowIfInvalid(); 45 | return ImpellerNative.ImpellerParagraphGetMinIntrinsicWidth(handle); 46 | } 47 | 48 | public float GetMaxIntrinsicWidth() 49 | { 50 | ThrowIfInvalid(); 51 | return ImpellerNative.ImpellerParagraphGetMaxIntrinsicWidth(handle); 52 | } 53 | 54 | public float GetIdeographicBaseline() 55 | { 56 | ThrowIfInvalid(); 57 | return ImpellerNative.ImpellerParagraphGetIdeographicBaseline(handle); 58 | } 59 | 60 | public float GetAlphabeticBaseline() 61 | { 62 | ThrowIfInvalid(); 63 | return ImpellerNative.ImpellerParagraphGetAlphabeticBaseline(handle); 64 | } 65 | 66 | public uint GetLineCount() 67 | { 68 | ThrowIfInvalid(); 69 | return ImpellerNative.ImpellerParagraphGetLineCount(handle); 70 | } 71 | 72 | public ImpellerRange GetWordBoundary(ulong codeUnitIndex) 73 | { 74 | ThrowIfInvalid(); 75 | ImpellerRange range = default; 76 | unsafe 77 | { 78 | ImpellerNative.ImpellerParagraphGetWordBoundary(handle, (nuint)codeUnitIndex, &range); 79 | } 80 | 81 | return range; 82 | } 83 | 84 | public ImpellerLineMetricsHandle? GetLineMetrics() 85 | { 86 | ThrowIfInvalid(); 87 | var native = ImpellerNative.ImpellerParagraphGetLineMetrics(handle); 88 | return ImpellerLineMetricsHandle.FromOwned(native); 89 | } 90 | 91 | public ImpellerGlyphInfoHandle? CreateGlyphInfoAtCodeUnitIndex(ulong codeUnitIndex) 92 | { 93 | ThrowIfInvalid(); 94 | var native = ImpellerNative.ImpellerParagraphCreateGlyphInfoAtCodeUnitIndexNew(handle, (nuint)codeUnitIndex); 95 | return ImpellerGlyphInfoHandle.FromOwned(native); 96 | } 97 | 98 | public ImpellerGlyphInfoHandle? CreateGlyphInfoAtParagraphCoordinates(double x, double y) 99 | { 100 | ThrowIfInvalid(); 101 | var native = ImpellerNative.ImpellerParagraphCreateGlyphInfoAtParagraphCoordinatesNew(handle, x, y); 102 | return ImpellerGlyphInfoHandle.FromOwned(native); 103 | } 104 | 105 | protected override bool ReleaseHandle() 106 | { 107 | ImpellerNative.ImpellerParagraphRelease(handle); 108 | return true; 109 | } 110 | 111 | internal new nint DangerousGetHandle() 112 | { 113 | ThrowIfInvalid(); 114 | return handle; 115 | } 116 | 117 | private void ThrowIfInvalid() 118 | { 119 | if (IsInvalid) 120 | { 121 | throw new ObjectDisposedException(nameof(ImpellerParagraphHandle)); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /samples/BasicShapes/Program.mac.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using ImpellerSharp.Interop; 4 | using ImpellerSharp.Samples.BasicShapes.Scenes; 5 | 6 | namespace ImpellerSharp.Samples.BasicShapes; 7 | 8 | internal static partial class Program 9 | { 10 | static partial void RunPlatformSample(SampleOptions options) 11 | { 12 | if (!OperatingSystem.IsMacOS()) 13 | { 14 | Console.WriteLine("Mac-specific sample invoked on non-macOS platform."); 15 | return; 16 | } 17 | 18 | var backendPreference = options.Backend; 19 | var chosenBackend = backendPreference == "auto" ? "metal" : backendPreference; 20 | if (!string.Equals(chosenBackend, "metal", StringComparison.OrdinalIgnoreCase)) 21 | { 22 | Console.Error.WriteLine($"Backend '{backendPreference}' is not supported for the interactive macOS sample. Falling back to Metal."); 23 | chosenBackend = "metal"; 24 | } 25 | 26 | Console.WriteLine($"Scene: {options.Scene}"); 27 | Console.WriteLine($"Frame limit: {options.FrameLimit?.ToString() ?? "unbounded"}"); 28 | if (!string.IsNullOrEmpty(options.CapturePath)) 29 | { 30 | Console.WriteLine($"Capture: {Path.GetFullPath(options.CapturePath)} (first frame)"); 31 | } 32 | 33 | using var host = MacMetalHost.Create(1280, 720, "Impeller BasicShapes"); 34 | using var context = ImpellerContextHandle.CreateMetal(); 35 | 36 | IScene scene; 37 | try 38 | { 39 | scene = SceneFactory.Create(options.Scene); 40 | } 41 | catch (Exception ex) 42 | { 43 | Console.Error.WriteLine(ex.Message); 44 | return; 45 | } 46 | 47 | using (scene) 48 | { 49 | try 50 | { 51 | scene.Initialize(context); 52 | } 53 | catch (Exception initEx) 54 | { 55 | Console.Error.WriteLine($"Scene initialization failed: {initEx.Message}"); 56 | return; 57 | } 58 | 59 | Console.WriteLine("Entering render loop. Close the window to exit."); 60 | 61 | var frameIndex = 0; 62 | var captureWritten = false; 63 | 64 | while (!host.ShouldClose && (!options.FrameLimit.HasValue || frameIndex < options.FrameLimit.Value)) 65 | { 66 | host.PumpEvents(); 67 | 68 | if (!host.TryAcquireDrawable(out var drawable)) 69 | { 70 | continue; 71 | } 72 | 73 | using (var displayList = scene.CreateDisplayList(context, frameIndex)) 74 | { 75 | using var surface = ImpellerSurfaceHandle.WrapMetalDrawable(context, drawable); 76 | if (surface.DrawDisplayList(displayList)) 77 | { 78 | surface.Present(); 79 | } 80 | } 81 | 82 | if (!captureWritten && !string.IsNullOrEmpty(options.CapturePath)) 83 | { 84 | try 85 | { 86 | MacCapture.CaptureWindow(host.WindowNumber, options.CapturePath!); 87 | captureWritten = true; 88 | Console.WriteLine($"Captured frame to {options.CapturePath}"); 89 | } 90 | catch (Exception captureEx) 91 | { 92 | Console.Error.WriteLine($"Capture failed: {captureEx.Message}"); 93 | captureWritten = true; 94 | } 95 | } 96 | 97 | host.ReleaseDrawable(drawable); 98 | frameIndex++; 99 | } 100 | } 101 | 102 | Console.WriteLine("Render loop exited."); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerPaintHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed unsafe class ImpellerPaintHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerPaintHandle(nint native) 8 | { 9 | SetHandle(native); 10 | } 11 | 12 | public static ImpellerPaintHandle Create() 13 | { 14 | var native = ImpellerNative.ImpellerPaintNew(); 15 | return new ImpellerPaintHandle( 16 | EnsureSuccess(native, "Failed to create Impeller paint.")); 17 | } 18 | 19 | internal void Retain() 20 | { 21 | if (IsInvalid) 22 | { 23 | throw new ObjectDisposedException(nameof(ImpellerPaintHandle)); 24 | } 25 | 26 | ImpellerNative.ImpellerPaintRetain(handle); 27 | } 28 | 29 | public void SetColor(in ImpellerColor color) 30 | { 31 | ThrowIfInvalid(); 32 | var value = color; 33 | ImpellerNative.ImpellerPaintSetColor(handle, &value); 34 | } 35 | 36 | public void SetBlendMode(ImpellerBlendMode blendMode) 37 | { 38 | ThrowIfInvalid(); 39 | ImpellerNative.ImpellerPaintSetBlendMode(handle, blendMode); 40 | } 41 | 42 | public void SetDrawStyle(ImpellerDrawStyle drawStyle) 43 | { 44 | ThrowIfInvalid(); 45 | ImpellerNative.ImpellerPaintSetDrawStyle(handle, drawStyle); 46 | } 47 | 48 | public void SetStrokeCap(ImpellerStrokeCap strokeCap) 49 | { 50 | ThrowIfInvalid(); 51 | ImpellerNative.ImpellerPaintSetStrokeCap(handle, strokeCap); 52 | } 53 | 54 | public void SetStrokeJoin(ImpellerStrokeJoin strokeJoin) 55 | { 56 | ThrowIfInvalid(); 57 | ImpellerNative.ImpellerPaintSetStrokeJoin(handle, strokeJoin); 58 | } 59 | 60 | public void SetStrokeWidth(float width) 61 | { 62 | ThrowIfInvalid(); 63 | ImpellerNative.ImpellerPaintSetStrokeWidth(handle, width); 64 | } 65 | 66 | public void SetStrokeMiter(float miterLimit) 67 | { 68 | ThrowIfInvalid(); 69 | ImpellerNative.ImpellerPaintSetStrokeMiter(handle, miterLimit); 70 | } 71 | 72 | public void SetColorSource(ImpellerColorSourceHandle? colorSource) 73 | { 74 | ThrowIfInvalid(); 75 | var native = colorSource?.DangerousGetHandle() ?? nint.Zero; 76 | ImpellerNative.ImpellerPaintSetColorSource(handle, native); 77 | } 78 | 79 | public void SetColorFilter(ImpellerColorFilterHandle colorFilter) 80 | { 81 | if (colorFilter is null) 82 | { 83 | throw new ArgumentNullException(nameof(colorFilter)); 84 | } 85 | 86 | ThrowIfInvalid(); 87 | ImpellerNative.ImpellerPaintSetColorFilter(handle, colorFilter.DangerousGetHandle()); 88 | } 89 | 90 | public void SetImageFilter(ImpellerImageFilterHandle imageFilter) 91 | { 92 | if (imageFilter is null) 93 | { 94 | throw new ArgumentNullException(nameof(imageFilter)); 95 | } 96 | 97 | ThrowIfInvalid(); 98 | ImpellerNative.ImpellerPaintSetImageFilter(handle, imageFilter.DangerousGetHandle()); 99 | } 100 | 101 | public void SetMaskFilter(ImpellerMaskFilterHandle maskFilter) 102 | { 103 | if (maskFilter is null) 104 | { 105 | throw new ArgumentNullException(nameof(maskFilter)); 106 | } 107 | 108 | ThrowIfInvalid(); 109 | ImpellerNative.ImpellerPaintSetMaskFilter(handle, maskFilter.DangerousGetHandle()); 110 | } 111 | 112 | protected override bool ReleaseHandle() 113 | { 114 | ImpellerNative.ImpellerPaintRelease(handle); 115 | return true; 116 | } 117 | 118 | internal new nint DangerousGetHandle() 119 | { 120 | ThrowIfInvalid(); 121 | return handle; 122 | } 123 | 124 | private void ThrowIfInvalid() 125 | { 126 | if (IsInvalid) 127 | { 128 | throw new ObjectDisposedException(nameof(ImpellerPaintHandle)); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerPathBuilderHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed unsafe class ImpellerPathBuilderHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerPathBuilderHandle(nint native) 8 | { 9 | SetHandle(native); 10 | } 11 | 12 | public static ImpellerPathBuilderHandle Create() 13 | { 14 | var native = ImpellerNative.ImpellerPathBuilderNew(); 15 | return new ImpellerPathBuilderHandle( 16 | EnsureSuccess(native, "Failed to create Impeller path builder.")); 17 | } 18 | 19 | internal void Retain() 20 | { 21 | if (IsInvalid) 22 | { 23 | throw new ObjectDisposedException(nameof(ImpellerPathBuilderHandle)); 24 | } 25 | 26 | ImpellerNative.ImpellerPathBuilderRetain(handle); 27 | } 28 | 29 | public void MoveTo(in ImpellerPoint point) 30 | { 31 | ThrowIfInvalid(); 32 | var location = point; 33 | ImpellerNative.ImpellerPathBuilderMoveTo(handle, &location); 34 | } 35 | 36 | public void LineTo(in ImpellerPoint point) 37 | { 38 | ThrowIfInvalid(); 39 | var location = point; 40 | ImpellerNative.ImpellerPathBuilderLineTo(handle, &location); 41 | } 42 | 43 | public void QuadraticCurveTo(in ImpellerPoint controlPoint, in ImpellerPoint endPoint) 44 | { 45 | ThrowIfInvalid(); 46 | var control = controlPoint; 47 | var end = endPoint; 48 | ImpellerNative.ImpellerPathBuilderQuadraticCurveTo(handle, &control, &end); 49 | } 50 | 51 | public void CubicCurveTo(in ImpellerPoint controlPoint1, in ImpellerPoint controlPoint2, in ImpellerPoint endPoint) 52 | { 53 | ThrowIfInvalid(); 54 | var c1 = controlPoint1; 55 | var c2 = controlPoint2; 56 | var end = endPoint; 57 | ImpellerNative.ImpellerPathBuilderCubicCurveTo(handle, &c1, &c2, &end); 58 | } 59 | 60 | public void AddRect(in ImpellerRect rect) 61 | { 62 | ThrowIfInvalid(); 63 | var value = rect; 64 | ImpellerNative.ImpellerPathBuilderAddRect(handle, &value); 65 | } 66 | 67 | public void AddArc(in ImpellerRect ovalBounds, float startAngleDegrees, float endAngleDegrees) 68 | { 69 | ThrowIfInvalid(); 70 | var bounds = ovalBounds; 71 | ImpellerNative.ImpellerPathBuilderAddArc(handle, &bounds, startAngleDegrees, endAngleDegrees); 72 | } 73 | 74 | public void AddOval(in ImpellerRect ovalBounds) 75 | { 76 | ThrowIfInvalid(); 77 | var bounds = ovalBounds; 78 | ImpellerNative.ImpellerPathBuilderAddOval(handle, &bounds); 79 | } 80 | 81 | public void AddRoundedRect(in ImpellerRect rect, in ImpellerRoundingRadii radii) 82 | { 83 | ThrowIfInvalid(); 84 | var value = rect; 85 | var rounding = radii; 86 | ImpellerNative.ImpellerPathBuilderAddRoundedRect(handle, &value, &rounding); 87 | } 88 | 89 | public void ClosePath() 90 | { 91 | ThrowIfInvalid(); 92 | ImpellerNative.ImpellerPathBuilderClose(handle); 93 | } 94 | 95 | public ImpellerPathHandle TakePath(ImpellerFillType fillType = ImpellerFillType.NonZero) 96 | { 97 | ThrowIfInvalid(); 98 | var native = ImpellerNative.ImpellerPathBuilderTakePathNew(handle, fillType); 99 | return ImpellerPathHandle.FromOwned(native); 100 | } 101 | 102 | public ImpellerPathHandle CopyPath(ImpellerFillType fillType = ImpellerFillType.NonZero) 103 | { 104 | ThrowIfInvalid(); 105 | var native = ImpellerNative.ImpellerPathBuilderCopyPathNew(handle, fillType); 106 | return ImpellerPathHandle.FromOwned(native); 107 | } 108 | 109 | protected override bool ReleaseHandle() 110 | { 111 | ImpellerNative.ImpellerPathBuilderRelease(handle); 112 | return true; 113 | } 114 | 115 | private void ThrowIfInvalid() 116 | { 117 | if (IsInvalid) 118 | { 119 | throw new ObjectDisposedException(nameof(ImpellerPathBuilderHandle)); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /samples/BasicShapes/ObjectiveCRuntime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Samples.BasicShapes; 5 | 6 | internal static class ObjectiveCRuntime 7 | { 8 | private const string ObjCLibrary = "/usr/lib/libobjc.A.dylib"; 9 | private const string FoundationLibrary = "/System/Library/Frameworks/Foundation.framework/Foundation"; 10 | private const string MetalLibrary = "/System/Library/Frameworks/Metal.framework/Metal"; 11 | 12 | [DllImport(ObjCLibrary, EntryPoint = "sel_registerName")] 13 | internal static extern nint Selector(string name); 14 | 15 | [DllImport(ObjCLibrary, EntryPoint = "objc_getClass")] 16 | internal static extern nint GetClass(string name); 17 | 18 | [DllImport(ObjCLibrary, EntryPoint = "objc_msgSend")] 19 | internal static extern nint objc_msgSend_IntPtr(nint receiver, nint selector); 20 | 21 | [DllImport(ObjCLibrary, EntryPoint = "objc_msgSend")] 22 | internal static extern void objc_msgSend_Void_IntPtr(nint receiver, nint selector, nint arg1); 23 | 24 | [DllImport(ObjCLibrary, EntryPoint = "objc_msgSend")] 25 | internal static extern void objc_msgSend_Void_Bool(nint receiver, nint selector, bool value); 26 | 27 | [DllImport(ObjCLibrary, EntryPoint = "objc_msgSend")] 28 | internal static extern void objc_msgSend_Void_Double(nint receiver, nint selector, double value); 29 | 30 | [DllImport(ObjCLibrary, EntryPoint = "objc_msgSend")] 31 | internal static extern void objc_msgSend_Void_CGSize(nint receiver, nint selector, CGSize size); 32 | 33 | [DllImport(ObjCLibrary, EntryPoint = "objc_msgSend")] 34 | internal static extern void objc_msgSend_Void_CGRect(nint receiver, nint selector, CGRect rect); 35 | 36 | [DllImport(ObjCLibrary, EntryPoint = "objc_msgSend")] 37 | internal static extern double objc_msgSend_Double(nint receiver, nint selector); 38 | 39 | [DllImport(ObjCLibrary, EntryPoint = "objc_msgSend")] 40 | internal static extern nuint objc_msgSend_NUInt(nint receiver, nint selector); 41 | 42 | [DllImport(ObjCLibrary, EntryPoint = "objc_release")] 43 | internal static extern void Release(nint obj); 44 | 45 | [DllImport(ObjCLibrary, EntryPoint = "objc_retain")] 46 | internal static extern nint Retain(nint obj); 47 | 48 | [DllImport(ObjCLibrary, EntryPoint = "objc_autoreleasePoolPush")] 49 | private static extern nint AutoreleasePoolPush(); 50 | 51 | [DllImport(ObjCLibrary, EntryPoint = "objc_autoreleasePoolPop")] 52 | private static extern void AutoreleasePoolPop(nint state); 53 | 54 | [DllImport(MetalLibrary, EntryPoint = "MTLCreateSystemDefaultDevice")] 55 | internal static extern nint MTLCreateSystemDefaultDevice(); 56 | 57 | internal static AutoreleasePoolScope PushAutoreleasePool() 58 | { 59 | return new AutoreleasePoolScope(AutoreleasePoolPush()); 60 | } 61 | 62 | internal readonly ref struct AutoreleasePoolScope 63 | { 64 | private readonly nint _state; 65 | 66 | internal AutoreleasePoolScope(nint state) 67 | { 68 | _state = state; 69 | } 70 | 71 | public void Dispose() 72 | { 73 | if (_state != nint.Zero) 74 | { 75 | AutoreleasePoolPop(_state); 76 | } 77 | } 78 | } 79 | } 80 | 81 | [StructLayout(LayoutKind.Sequential)] 82 | internal struct CGSize 83 | { 84 | public double Width; 85 | public double Height; 86 | 87 | public CGSize(double width, double height) 88 | { 89 | Width = width; 90 | Height = height; 91 | } 92 | } 93 | 94 | [StructLayout(LayoutKind.Sequential)] 95 | internal struct CGPoint 96 | { 97 | public double X; 98 | public double Y; 99 | 100 | public CGPoint(double x, double y) 101 | { 102 | X = x; 103 | Y = y; 104 | } 105 | } 106 | 107 | [StructLayout(LayoutKind.Sequential)] 108 | internal struct CGRect 109 | { 110 | public CGPoint Origin; 111 | public CGSize Size; 112 | 113 | public CGRect(CGPoint origin, CGSize size) 114 | { 115 | Origin = origin; 116 | Size = size; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Avalonia/Controls/ImpellerSkiaView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | using Avalonia.Controls; 4 | using Avalonia.Media; 5 | using Avalonia.Rendering; 6 | using Avalonia.Rendering.Composition; 7 | using Avalonia.Rendering.SceneGraph; 8 | using Avalonia.Platform; 9 | using Avalonia.Skia; 10 | using Avalonia.VisualTree; 11 | 12 | namespace ImpellerSharp.Avalonia.Controls; 13 | 14 | /// 15 | /// Avalonia control that exposes Skia lease rendering callbacks compatible with Impeller. 16 | /// 17 | public class ImpellerSkiaView : Control 18 | { 19 | private CompositionCustomVisual? _customVisual; 20 | private ImpellerVisualHandler? _handler; 21 | 22 | /// 23 | /// Raised when a new Skia lease is available for rendering. 24 | /// 25 | public event EventHandler? RenderSkia; 26 | 27 | protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) 28 | { 29 | base.OnAttachedToVisualTree(e); 30 | AttachCompositionVisual(); 31 | } 32 | 33 | protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) 34 | { 35 | base.OnDetachedFromVisualTree(e); 36 | DetachCompositionVisual(); 37 | } 38 | 39 | protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) 40 | { 41 | base.OnPropertyChanged(change); 42 | if (change.Property == BoundsProperty) 43 | { 44 | UpdateCompositionSize(Bounds.Size); 45 | } 46 | } 47 | 48 | private void AttachCompositionVisual() 49 | { 50 | var rootVisual = ElementComposition.GetElementVisual(this); 51 | if (rootVisual is null) 52 | { 53 | return; 54 | } 55 | 56 | if (_customVisual != null) 57 | { 58 | DetachCompositionVisual(); 59 | } 60 | 61 | _handler = new ImpellerVisualHandler(this); 62 | _customVisual = rootVisual.Compositor.CreateCustomVisual(_handler); 63 | UpdateCompositionSize(Bounds.Size); 64 | ElementComposition.SetElementChildVisual(this, _customVisual); 65 | } 66 | 67 | private void DetachCompositionVisual() 68 | { 69 | _customVisual?.SendHandlerMessage(ImpellerVisualHandler.DisposeMessage.Instance); 70 | ElementComposition.SetElementChildVisual(this, null); 71 | _customVisual = null; 72 | _handler = null; 73 | } 74 | 75 | private void UpdateCompositionSize(Size size) 76 | { 77 | if (_customVisual is not null) 78 | { 79 | _customVisual.Size = new Vector(size.Width, size.Height); 80 | } 81 | } 82 | 83 | private sealed class ImpellerVisualHandler : CompositionCustomVisualHandler 84 | { 85 | private readonly ImpellerSkiaView _owner; 86 | 87 | public ImpellerVisualHandler(ImpellerSkiaView owner) 88 | { 89 | _owner = owner; 90 | } 91 | 92 | public override void OnRender(ImmediateDrawingContext drawingContext) 93 | { 94 | var handler = _owner.RenderSkia; 95 | if (handler is null) 96 | { 97 | return; 98 | } 99 | 100 | if (!drawingContext.TryGetFeature(out var leaseFeature)) 101 | { 102 | return; 103 | } 104 | 105 | using var lease = leaseFeature.Lease(); 106 | var args = new ImpellerSkiaRenderEventArgs(lease); 107 | try 108 | { 109 | handler(_owner, args); 110 | } 111 | finally 112 | { 113 | args.Dispose(); 114 | } 115 | } 116 | 117 | public override void OnMessage(object message) 118 | { 119 | if (ReferenceEquals(message, DisposeMessage.Instance)) 120 | { 121 | // nothing to dispose currently 122 | } 123 | } 124 | 125 | public sealed class DisposeMessage 126 | { 127 | public static readonly DisposeMessage Instance = new(); 128 | 129 | private DisposeMessage() 130 | { 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerParagraphStyleHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed unsafe class ImpellerParagraphStyleHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerParagraphStyleHandle(nint native) 8 | { 9 | SetHandle(native); 10 | } 11 | 12 | public static ImpellerParagraphStyleHandle Create() 13 | { 14 | var native = ImpellerNative.ImpellerParagraphStyleNew(); 15 | return new ImpellerParagraphStyleHandle( 16 | EnsureSuccess(native, "Failed to create Impeller paragraph style.")); 17 | } 18 | 19 | internal void Retain() 20 | { 21 | ThrowIfInvalid(); 22 | ImpellerNative.ImpellerParagraphStyleRetain(handle); 23 | } 24 | 25 | public void SetForeground(ImpellerPaintHandle paint) 26 | { 27 | ThrowIfInvalid(); 28 | if (paint is null || paint.IsInvalid) 29 | { 30 | throw new ArgumentNullException(nameof(paint)); 31 | } 32 | 33 | ImpellerNative.ImpellerParagraphStyleSetForeground(handle, paint.DangerousGetHandle()); 34 | } 35 | 36 | public void SetBackground(ImpellerPaintHandle paint) 37 | { 38 | ThrowIfInvalid(); 39 | if (paint is null || paint.IsInvalid) 40 | { 41 | throw new ArgumentNullException(nameof(paint)); 42 | } 43 | 44 | ImpellerNative.ImpellerParagraphStyleSetBackground(handle, paint.DangerousGetHandle()); 45 | } 46 | 47 | public void SetFontWeight(ImpellerFontWeight weight) 48 | { 49 | ThrowIfInvalid(); 50 | ImpellerNative.ImpellerParagraphStyleSetFontWeight(handle, weight); 51 | } 52 | 53 | public void SetFontStyle(ImpellerFontStyle style) 54 | { 55 | ThrowIfInvalid(); 56 | ImpellerNative.ImpellerParagraphStyleSetFontStyle(handle, style); 57 | } 58 | 59 | public void SetFontFamily(string? family) 60 | { 61 | ThrowIfInvalid(); 62 | using var utf8 = new Utf8String(family); 63 | ImpellerNative.ImpellerParagraphStyleSetFontFamily(handle, (byte*)utf8.Pointer); 64 | } 65 | 66 | public void SetFontSize(float size) 67 | { 68 | ThrowIfInvalid(); 69 | ImpellerNative.ImpellerParagraphStyleSetFontSize(handle, size); 70 | } 71 | 72 | public void SetHeight(float height) 73 | { 74 | ThrowIfInvalid(); 75 | ImpellerNative.ImpellerParagraphStyleSetHeight(handle, height); 76 | } 77 | 78 | public void SetTextAlignment(ImpellerTextAlignment alignment) 79 | { 80 | ThrowIfInvalid(); 81 | ImpellerNative.ImpellerParagraphStyleSetTextAlignment(handle, alignment); 82 | } 83 | 84 | public void SetTextDirection(ImpellerTextDirection direction) 85 | { 86 | ThrowIfInvalid(); 87 | ImpellerNative.ImpellerParagraphStyleSetTextDirection(handle, direction); 88 | } 89 | 90 | public void SetTextDecoration(ImpellerTextDecoration decoration) 91 | { 92 | ThrowIfInvalid(); 93 | ImpellerNative.ImpellerParagraphStyleSetTextDecoration(handle, &decoration); 94 | } 95 | 96 | public void SetMaxLines(uint maxLines) 97 | { 98 | ThrowIfInvalid(); 99 | ImpellerNative.ImpellerParagraphStyleSetMaxLines(handle, maxLines); 100 | } 101 | 102 | public void SetLocale(string? locale) 103 | { 104 | ThrowIfInvalid(); 105 | using var utf8 = new Utf8String(locale); 106 | ImpellerNative.ImpellerParagraphStyleSetLocale(handle, (byte*)utf8.Pointer); 107 | } 108 | 109 | public void SetEllipsis(string? ellipsis) 110 | { 111 | ThrowIfInvalid(); 112 | using var utf8 = new Utf8String(ellipsis); 113 | ImpellerNative.ImpellerParagraphStyleSetEllipsis(handle, (byte*)utf8.Pointer); 114 | } 115 | 116 | protected override bool ReleaseHandle() 117 | { 118 | ImpellerNative.ImpellerParagraphStyleRelease(handle); 119 | return true; 120 | } 121 | 122 | internal new nint DangerousGetHandle() 123 | { 124 | ThrowIfInvalid(); 125 | return handle; 126 | } 127 | 128 | private void ThrowIfInvalid() 129 | { 130 | if (IsInvalid) 131 | { 132 | throw new ObjectDisposedException(nameof(ImpellerParagraphStyleHandle)); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /samples/BasicShapes/Scenes/TextureStreamingScene.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using ImpellerSharp.Interop; 4 | 5 | namespace ImpellerSharp.Samples.BasicShapes.Scenes; 6 | 7 | internal sealed class TextureStreamingScene : IScene 8 | { 9 | private readonly Random _random = new(1337); 10 | private byte[] _pixelBuffer = Array.Empty(); 11 | private ImpellerTextureDescriptor _descriptor; 12 | private ImpellerTextureHandle? _texture; 13 | private ImpellerPaintHandle? _borderPaint; 14 | private ImpellerRect _destinationRect; 15 | private ImpellerRect _sourceRect; 16 | private string _lastDigest = string.Empty; 17 | 18 | public string Name => "stream"; 19 | 20 | public void Initialize(ImpellerContextHandle context) 21 | { 22 | _descriptor = new ImpellerTextureDescriptor( 23 | ImpellerPixelFormat.Rgba8888, 24 | new ImpellerISize(256, 256)); 25 | 26 | var pixelCount = (int)(_descriptor.Size.Width * _descriptor.Size.Height * 4); 27 | _pixelBuffer = new byte[pixelCount]; 28 | 29 | _sourceRect = new ImpellerRect(0, 0, _descriptor.Size.Width, _descriptor.Size.Height); 30 | _destinationRect = new ImpellerRect(256, 160, _descriptor.Size.Width, _descriptor.Size.Height); 31 | 32 | _borderPaint = ImpellerPaintHandle.Create(); 33 | _borderPaint.SetColor(new ImpellerColor(0.95f, 0.95f, 0.95f, 1f)); 34 | _borderPaint.SetStrokeWidth(4f); 35 | _borderPaint.SetDrawStyle(ImpellerDrawStyle.Stroke); 36 | } 37 | 38 | public ImpellerDisplayListHandle CreateDisplayList(ImpellerContextHandle context, int frameIndex) 39 | { 40 | RefreshTexture(context, frameIndex); 41 | 42 | using var builder = ImpellerDisplayListBuilderHandle.Create(); 43 | 44 | // Draw gradient background. 45 | using var backgroundPaint = ImpellerPaintHandle.Create(); 46 | backgroundPaint.SetColor(new ImpellerColor(0.05f, 0.07f, 0.1f, 1f)); 47 | builder.DrawRect(new ImpellerRect(0, 0, 1280, 720), backgroundPaint); 48 | 49 | if (_texture is not null) 50 | { 51 | builder.DrawTextureRect( 52 | _texture, 53 | _sourceRect, 54 | _destinationRect, 55 | ImpellerTextureSampling.Linear); 56 | } 57 | 58 | builder.DrawRect(_destinationRect, _borderPaint!); 59 | 60 | return builder.Build(); 61 | } 62 | 63 | public string DescribeFrame(int frameIndex) 64 | { 65 | if (string.IsNullOrEmpty(_lastDigest)) 66 | { 67 | return $"stream:digest=unavailable;frame={frameIndex}"; 68 | } 69 | 70 | return $"stream:digest={_lastDigest};frame={frameIndex}"; 71 | } 72 | 73 | public void Dispose() 74 | { 75 | _texture?.Dispose(); 76 | _texture = null; 77 | 78 | _borderPaint?.Dispose(); 79 | _borderPaint = null; 80 | } 81 | 82 | private void RefreshTexture(ImpellerContextHandle context, int frameIndex) 83 | { 84 | Span pixels = _pixelBuffer; 85 | var width = (int)_descriptor.Size.Width; 86 | var height = (int)_descriptor.Size.Height; 87 | var stride = width * 4; 88 | var phase = frameIndex % 360; 89 | 90 | for (var y = 0; y < height; y++) 91 | { 92 | var row = pixels.Slice(y * stride, stride); 93 | for (var x = 0; x < width; x++) 94 | { 95 | var offset = x * 4; 96 | var u = (byte)((x + phase) % 256); 97 | var v = (byte)((y * 2 + phase) % 256); 98 | row[offset + 0] = u; 99 | row[offset + 1] = v; 100 | row[offset + 2] = (byte)_random.Next(32, 220); 101 | row[offset + 3] = 255; 102 | } 103 | } 104 | 105 | _lastDigest = ComputeDigest(pixels); 106 | 107 | if (SceneExecutionContext.Headless) 108 | { 109 | return; 110 | } 111 | 112 | var nextTexture = context.CreateTexture(_descriptor, pixels); 113 | 114 | _texture?.Dispose(); 115 | _texture = nextTexture; 116 | } 117 | 118 | private static string ComputeDigest(Span pixels) 119 | { 120 | var hash = SHA256.HashData(pixels); 121 | return Convert.ToHexString(hash); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /docs/developer-guide.md: -------------------------------------------------------------------------------- 1 | # ImpellerSharp Developer Guide 2 | 3 | This guide explains how to integrate the ImpellerSharp interop layer into your .NET applications, manage threading and memory, work with shaders, and troubleshoot common issues. It assumes you have already built the native Impeller SDK and the managed `ImpellerSharp.Interop` library. 4 | 5 | ## 1. Getting Started 6 | 7 | ### Prerequisites 8 | - Native Impeller SDK build (see `docs/ci-plan.md` for GN/Ninja steps). 9 | - .NET SDK 8.0 or later. 10 | - Platform SDKs: 11 | - macOS: Xcode command-line tools & Metal toolchain. 12 | - Linux: Vulkan SDK (LunarG) or SwiftShader for software fallback. 13 | - Windows: Visual Studio build tools + Vulkan SDK (future support). 14 | 15 | ### Project Setup 16 | 1. Add a project reference to `src/ImpellerSharp.Interop/ImpellerSharp.Interop.csproj`. 17 | 2. Ensure native binaries (`libimpeller.*`, `impeller.h`) are available at runtime (copy to output or configure `LD_LIBRARY_PATH`/`DYLD_LIBRARY_PATH`). 18 | 3. (Optional) Reference `samples/BasicShapes` for basic wiring. 19 | 20 | ### Minimal Code Sample 21 | ```csharp 22 | using ImpellerSharp.Interop; 23 | 24 | using var context = ImpellerContextHandle.CreateMetal(); // macOS example 25 | var descriptor = new ImpellerTextureDescriptor( 26 | ImpellerPixelFormat.Rgba8888, 27 | new ImpellerISize(256, 256)); 28 | 29 | var pixels = new byte[descriptor.Size.Width * descriptor.Size.Height * 4]; 30 | using var texture = context.CreateTexture(descriptor, pixels); 31 | 32 | // TODO: wrap swapchain/surface and draw display list 33 | ``` 34 | 35 | ## 2. Threading Model 36 | - `ImpellerContextHandle` is thread-safe, but `CommandBuffer` encoding must happen on a single thread. Use multiple command buffers to parallelize work. 37 | - Background texture uploads: `TextureFactory.CreateTexture` may pin buffers and invoke release callbacks on worker threads. Use thread-safe data structures for baton state. 38 | - For UI frameworks, execute main rendering on the UI thread while offloading uploads to background tasks. 39 | 40 | ## 3. Memory Management 41 | - Handles derive from `SafeHandle`. Always dispose them or use `using`. 42 | - Native `Retain`/`Release` semantics are encapsulated in methods like `Retain()`, `DangerousGetHandle()`. 43 | - When uploading texture data, prefer span-based APIs that copy into pooled pinned buffers. Provide release callbacks for long transfers. 44 | - Consider adding a strict mode (see `docs/benchmarking-plan.md`) to detect unexpected allocations or double releases during development. 45 | 46 | ## 4. Shader & Pipeline Updates 47 | - Use `ImpellerFragmentProgramNew` to load precompiled shader blobs generated via `impellerc`. Cache `ImpellerFragmentProgram` handles. 48 | - For runtime shader effects, integrate with `RuntimeStage::DecodeRuntimeStages` by exposing managed struct mirrors (future work). 49 | - Warm up pipelines by rendering off-screen once; record timings via `ImpellerDiagnostics.ActivitySource`. 50 | 51 | ## 5. Diagnostics & Profiling 52 | - `ImpellerDiagnostics` forwards context creation, texture uploads, and draw submissions to `ImpellerEventSource` and an `ActivitySource`. Subscribe via `EventListener` or `DiagnosticSource`. 53 | - Enable strict guardrails via `AppContext.SetSwitch("ImpellerSharp.Interop.StrictMode", true)` or `IMPELLER_INTEROP_STRICT=1`. The strict configuration (see `ImpellerInteropOptions`) validates texture payload sizes and throws immediately on surface draw failures—ideal for catching misuse during development. 54 | - For managed allocation profiling, follow the steps in `docs/benchmarking-plan.md` (dotnet-trace, dotnet-counters). 55 | - Integrate BenchmarkDotNet scenarios (`BL-DrawRect`, `TextureUpload`) to detect regressions. 56 | 57 | ## 6. Troubleshooting 58 | - **Version mismatch**: Ensure `ImpellerContextHandle.Create*` passes `ImpellerNative.ImpellerGetVersion()`. 59 | - **Null returns**: Check that native binaries are discoverable and that required backends (Metal/Vulkan) are present. 60 | - **Threading violations**: Avoid using surfaces/command buffers after disposal; check logs for ActivitySource events with failure status. 61 | - **Strict mode exceptions**: `ImpellerInteropOptions` may throw if resources are misused (e.g., undersized texture data). Disable strict mode in production or update callsites to satisfy the stricter contracts. 62 | - **Dynamic library load failures**: On macOS, configure `DYLD_LIBRARY_PATH` or embed `libimpeller.dylib` via `rpath`. On Linux/Windows (future), use RID assets in NuGet packaging. 63 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerContextHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ImpellerSharp.Interop; 5 | 6 | public sealed class ImpellerContextHandle : ImpellerSafeHandle 7 | { 8 | private readonly ImpellerProcAddressCallback? _openGlProcCallback; 9 | private readonly ImpellerVulkanProcAddressCallback? _vulkanProcCallback; 10 | 11 | private ImpellerContextHandle( 12 | nint handle, 13 | ImpellerProcAddressCallback? openGlCallback = null, 14 | ImpellerVulkanProcAddressCallback? vulkanCallback = null) 15 | { 16 | SetHandle(handle); 17 | _openGlProcCallback = openGlCallback; 18 | _vulkanProcCallback = vulkanCallback; 19 | } 20 | 21 | public static ImpellerContextHandle CreateMetal(uint? version = null) 22 | { 23 | var resolvedVersion = version ?? ImpellerNative.ImpellerGetVersion(); 24 | var native = EnsureSuccess( 25 | ImpellerNative.ImpellerContextCreateMetalNew(resolvedVersion), 26 | "Failed to create Impeller Metal context."); 27 | 28 | var handle = new ImpellerContextHandle(native); 29 | ImpellerDiagnostics.ContextCreated("Metal", resolvedVersion); 30 | return handle; 31 | } 32 | 33 | public static ImpellerContextHandle CreateOpenGLES( 34 | ImpellerProcAddressCallback callback, 35 | IntPtr userData, 36 | uint? version = null) 37 | { 38 | if (callback is null) 39 | { 40 | throw new ArgumentNullException(nameof(callback)); 41 | } 42 | 43 | var resolvedVersion = version ?? ImpellerNative.ImpellerGetVersion(); 44 | var callbackPtr = Marshal.GetFunctionPointerForDelegate(callback); 45 | 46 | var native = EnsureSuccess( 47 | ImpellerNative.ImpellerContextCreateOpenGLESNew(resolvedVersion, callbackPtr, userData), 48 | "Failed to create Impeller OpenGL ES context."); 49 | 50 | var handle = new ImpellerContextHandle(native, openGlCallback: callback); 51 | ImpellerDiagnostics.ContextCreated("OpenGLES", resolvedVersion); 52 | return handle; 53 | } 54 | 55 | public static ImpellerContextHandle CreateVulkan( 56 | ImpellerContextVulkanSettings settings, 57 | uint? version = null) 58 | { 59 | if (settings.ProcAddressCallback is null) 60 | { 61 | throw new ArgumentException("Vulkan proc address callback must be supplied.", nameof(settings)); 62 | } 63 | 64 | var resolvedVersion = version ?? ImpellerNative.ImpellerGetVersion(); 65 | 66 | var nativeSettings = new ImpellerContextVulkanSettingsNative 67 | { 68 | UserData = settings.UserData, 69 | ProcAddressCallback = Marshal.GetFunctionPointerForDelegate(settings.ProcAddressCallback), 70 | EnableValidation = settings.EnableValidation, 71 | }; 72 | 73 | var native = EnsureSuccess( 74 | ImpellerNative.ImpellerContextCreateVulkanNew(resolvedVersion, ref nativeSettings), 75 | "Failed to create Impeller Vulkan context."); 76 | 77 | var handle = new ImpellerContextHandle(native, vulkanCallback: settings.ProcAddressCallback); 78 | ImpellerDiagnostics.ContextCreated("Vulkan", resolvedVersion); 79 | return handle; 80 | } 81 | 82 | public bool TryGetVulkanInfo(out ImpellerContextVulkanInfo info) 83 | { 84 | if (IsInvalid) 85 | { 86 | throw new ObjectDisposedException(nameof(ImpellerContextHandle)); 87 | } 88 | 89 | return ImpellerNative.ImpellerContextGetVulkanInfo(handle, out info); 90 | } 91 | 92 | public ImpellerVulkanSwapchainHandle CreateVulkanSwapchain(nint vulkanSurface) 93 | { 94 | return ImpellerVulkanSwapchainHandle.Create(this, vulkanSurface); 95 | } 96 | 97 | internal void Retain() 98 | { 99 | if (IsInvalid) 100 | { 101 | throw new ObjectDisposedException(nameof(ImpellerContextHandle)); 102 | } 103 | 104 | ImpellerNative.ImpellerContextRetain(handle); 105 | } 106 | 107 | protected override bool ReleaseHandle() 108 | { 109 | ImpellerNative.ImpellerContextRelease(handle); 110 | return true; 111 | } 112 | 113 | internal new nint DangerousGetHandle() 114 | { 115 | if (IsInvalid) 116 | { 117 | throw new ObjectDisposedException(nameof(ImpellerContextHandle)); 118 | } 119 | 120 | return handle; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /samples/BasicShapes/HeadlessRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text.Json; 5 | using ImpellerSharp.Interop; 6 | using ImpellerSharp.Samples.BasicShapes.Scenes; 7 | 8 | namespace ImpellerSharp.Samples.BasicShapes; 9 | 10 | internal static class HeadlessRunner 11 | { 12 | internal static bool Run(SampleOptions options) 13 | { 14 | if (!BackendContextFactory.TryCreateContext(options, out var context, out var backend, out var error)) 15 | { 16 | Console.Error.WriteLine(error ?? "Failed to create Impeller context."); 17 | return false; 18 | } 19 | 20 | using (context) 21 | { 22 | IScene scene; 23 | try 24 | { 25 | scene = SceneFactory.Create(options.Scene); 26 | } 27 | catch (Exception ex) 28 | { 29 | Console.Error.WriteLine(ex.Message); 30 | return false; 31 | } 32 | 33 | SceneExecutionContext.Headless = true; 34 | SceneExecutionContext.LastVulkanSwapchainProbe = null; 35 | try 36 | { 37 | using (scene) 38 | { 39 | try 40 | { 41 | scene.Initialize(context!); 42 | } 43 | catch (Exception initEx) 44 | { 45 | Console.Error.WriteLine($"Scene initialization failed: {initEx.Message}"); 46 | return false; 47 | } 48 | 49 | var frameCount = options.FrameLimit ?? 1; 50 | var digests = new List(frameCount); 51 | 52 | for (var frame = 0; frame < frameCount; frame++) 53 | { 54 | using var displayList = scene.CreateDisplayList(context!, frame); 55 | digests.Add(new FrameDigest(frame, scene.DescribeFrame(frame))); 56 | } 57 | 58 | Console.WriteLine($"Headless run complete. Scene={options.Scene}, Backend={backend}, Frames={frameCount}"); 59 | if (!string.IsNullOrEmpty(SceneExecutionContext.LastVulkanSwapchainProbe)) 60 | { 61 | Console.WriteLine($"Vulkan swapchain probe: {SceneExecutionContext.LastVulkanSwapchainProbe}"); 62 | } 63 | 64 | if (!string.IsNullOrEmpty(options.OutputPath)) 65 | { 66 | WriteGoldenArtifact(options.OutputPath!, backend, options.Scene, digests); 67 | Console.WriteLine($"Golden artifact written to {Path.GetFullPath(options.OutputPath!)}"); 68 | } 69 | } 70 | } 71 | finally 72 | { 73 | SceneExecutionContext.Headless = false; 74 | SceneExecutionContext.LastVulkanSwapchainProbe = null; 75 | } 76 | } 77 | 78 | return true; 79 | } 80 | 81 | private static void WriteGoldenArtifact(string path, string backend, string scene, IReadOnlyList digests) 82 | { 83 | var payload = new GoldenArtifact 84 | { 85 | Scene = scene, 86 | Backend = backend, 87 | GeneratedAt = DateTimeOffset.UtcNow, 88 | Frames = digests, 89 | SwapchainProbe = SceneExecutionContext.LastVulkanSwapchainProbe, 90 | }; 91 | 92 | var directory = Path.GetDirectoryName(path); 93 | if (!string.IsNullOrEmpty(directory)) 94 | { 95 | Directory.CreateDirectory(directory); 96 | } 97 | 98 | var options = new JsonSerializerOptions 99 | { 100 | WriteIndented = true, 101 | }; 102 | 103 | using var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None); 104 | JsonSerializer.Serialize(stream, payload, options); 105 | } 106 | 107 | private sealed record GoldenArtifact 108 | { 109 | public string Scene { get; init; } = string.Empty; 110 | 111 | public string Backend { get; init; } = string.Empty; 112 | 113 | public DateTimeOffset GeneratedAt { get; init; } 114 | 115 | public IReadOnlyList Frames { get; init; } = Array.Empty(); 116 | 117 | public string? SwapchainProbe { get; init; } 118 | } 119 | 120 | private sealed record FrameDigest(int Frame, string Summary); 121 | } 122 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/TextureFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Diagnostics; 4 | using System.Runtime.CompilerServices; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace ImpellerSharp.Interop; 8 | 9 | public static unsafe class TextureFactory 10 | { 11 | private static readonly delegate* unmanaged[Cdecl] ReleaseCallbackPointer = &ReleasePinnedBuffer; 12 | 13 | public static ImpellerTextureHandle CreateTexture( 14 | this ImpellerContextHandle context, 15 | in ImpellerTextureDescriptor descriptor, 16 | ReadOnlySpan pixelData) 17 | { 18 | if (context is null) 19 | { 20 | throw new ArgumentNullException(nameof(context)); 21 | } 22 | 23 | if (pixelData.IsEmpty) 24 | { 25 | throw new ArgumentException("Texture data must not be empty.", nameof(pixelData)); 26 | } 27 | 28 | if (ImpellerInteropOptions.Configuration.StrictMode) 29 | { 30 | var expectedBytes = checked((ulong)descriptor.Size.Width * (ulong)descriptor.Size.Height * 4UL); 31 | if ((ulong)pixelData.Length < expectedBytes) 32 | { 33 | throw new ImpellerInteropException($"Texture data length ({pixelData.Length}) is smaller than expected for descriptor ({expectedBytes})."); 34 | } 35 | } 36 | 37 | var upload = new PinnedTextureUpload(pixelData); 38 | var userDataHandle = GCHandle.Alloc(upload, GCHandleType.Normal); 39 | var handoffToNative = false; 40 | var addedRef = false; 41 | 42 | using var activity = ImpellerDiagnostics.ActivitySource.StartActivity("ImpellerTexture.Upload"); 43 | 44 | try 45 | { 46 | context.DangerousAddRef(ref addedRef); 47 | 48 | var mapping = new ImpellerMapping 49 | { 50 | Data = upload.Pointer, 51 | Length = (ulong)upload.Length, 52 | OnRelease = ReleaseCallbackPointer, 53 | }; 54 | 55 | var texturePtr = ImpellerNative.ImpellerTextureCreateWithContentsNew( 56 | context.DangerousGetHandle(), 57 | descriptor, 58 | mapping, 59 | GCHandle.ToIntPtr(userDataHandle)); 60 | 61 | if (texturePtr == nint.Zero) 62 | { 63 | throw new ImpellerInteropException("ImpellerTextureCreateWithContentsNew returned null."); 64 | } 65 | 66 | handoffToNative = true; 67 | ImpellerDiagnostics.TextureCreated(descriptor); 68 | return ImpellerTextureHandle.FromOwned(texturePtr); 69 | } 70 | finally 71 | { 72 | if (addedRef) 73 | { 74 | context.DangerousRelease(); 75 | } 76 | 77 | if (!handoffToNative) 78 | { 79 | upload.Dispose(); 80 | if (userDataHandle.IsAllocated) 81 | { 82 | userDataHandle.Free(); 83 | } 84 | } 85 | } 86 | } 87 | 88 | [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] 89 | private static void ReleasePinnedBuffer(nint userData) 90 | { 91 | if (userData == nint.Zero) 92 | { 93 | return; 94 | } 95 | 96 | var handle = GCHandle.FromIntPtr(userData); 97 | if (handle.Target is PinnedTextureUpload upload) 98 | { 99 | upload.Dispose(); 100 | } 101 | 102 | if (handle.IsAllocated) 103 | { 104 | handle.Free(); 105 | } 106 | } 107 | 108 | private sealed class PinnedTextureUpload : IDisposable 109 | { 110 | private readonly byte[] _buffer; 111 | private readonly GCHandle _pinned; 112 | private bool _disposed; 113 | 114 | internal PinnedTextureUpload(ReadOnlySpan source) 115 | { 116 | Length = source.Length; 117 | _buffer = ArrayPool.Shared.Rent(Length); 118 | source.CopyTo(_buffer.AsSpan(0, Length)); 119 | _pinned = GCHandle.Alloc(_buffer, GCHandleType.Pinned); 120 | } 121 | 122 | internal int Length { get; } 123 | 124 | internal byte* Pointer => (byte*)_pinned.AddrOfPinnedObject(); 125 | 126 | public void Dispose() 127 | { 128 | if (_disposed) 129 | { 130 | return; 131 | } 132 | 133 | _disposed = true; 134 | if (_pinned.IsAllocated) 135 | { 136 | _pinned.Free(); 137 | } 138 | 139 | ArrayPool.Shared.Return(_buffer, clearArray: false); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /samples/BasicShapes/Scenes/TypographyScene.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using ImpellerSharp.Interop; 4 | 5 | namespace ImpellerSharp.Samples.BasicShapes.Scenes; 6 | 7 | internal sealed class TypographyScene : IScene 8 | { 9 | private static readonly string[] FontCandidates = 10 | { 11 | "/System/Library/Fonts/SFNS.ttf", 12 | "/System/Library/Fonts/SFNSDisplay.ttf", 13 | "/System/Library/Fonts/SFNSMono.ttf", 14 | "/System/Library/Fonts/Supplemental/Arial.ttf", 15 | "/System/Library/Fonts/Supplemental/Helvetica.ttf", 16 | "/System/Library/Fonts/Supplemental/Times New Roman.ttf", 17 | }; 18 | 19 | private ImpellerTypographyContextHandle? _typography; 20 | private ImpellerParagraphHandle? _paragraph; 21 | private ImpellerPaintHandle? _textPaint; 22 | private ImpellerPaintHandle? _backgroundPaint; 23 | private readonly ImpellerRoundingRadii _textPanelRadii = ImpellerRoundingRadii.Uniform(24f); 24 | private string? _fontFamilyAlias; 25 | private string _sampleText = string.Empty; 26 | private float _longestLine; 27 | private float _minIntrinsicWidth; 28 | private float _maxIntrinsicWidth; 29 | private uint _lineCount; 30 | 31 | public string Name => "typography"; 32 | 33 | public void Initialize(ImpellerContextHandle context) 34 | { 35 | _typography = ImpellerTypographyContextHandle.Create(); 36 | 37 | var fontAlias = "SampleFont"; 38 | var fontData = LoadFontBytes(out var resolvedAlias); 39 | if (!_typography.RegisterFont(fontData, resolvedAlias)) 40 | { 41 | throw new InvalidOperationException("Failed to register typography font."); 42 | } 43 | 44 | _fontFamilyAlias = resolvedAlias ?? fontAlias; 45 | 46 | _textPaint = ImpellerPaintHandle.Create(); 47 | _textPaint.SetColor(new ImpellerColor(0.92f, 0.95f, 0.98f, 1f)); 48 | 49 | _backgroundPaint = ImpellerPaintHandle.Create(); 50 | _backgroundPaint.SetColor(new ImpellerColor(0.1f, 0.12f, 0.16f, 1f)); 51 | 52 | using var paragraphStyle = ImpellerParagraphStyleHandle.Create(); 53 | paragraphStyle.SetFontFamily(_fontFamilyAlias); 54 | paragraphStyle.SetFontSize(48f); 55 | paragraphStyle.SetForeground(_textPaint); 56 | paragraphStyle.SetTextAlignment(ImpellerTextAlignment.Center); 57 | paragraphStyle.SetTextDirection(ImpellerTextDirection.LeftToRight); 58 | 59 | using var builder = ImpellerParagraphBuilderHandle.Create(_typography); 60 | builder.PushStyle(paragraphStyle); 61 | _sampleText = GetSampleText(); 62 | builder.AddText(_sampleText); 63 | builder.PopStyle(); 64 | 65 | _paragraph = builder.Build(960f); 66 | _longestLine = _paragraph.GetLongestLineWidth(); 67 | _minIntrinsicWidth = _paragraph.GetMinIntrinsicWidth(); 68 | _maxIntrinsicWidth = _paragraph.GetMaxIntrinsicWidth(); 69 | _lineCount = _paragraph.GetLineCount(); 70 | } 71 | 72 | public ImpellerDisplayListHandle CreateDisplayList(ImpellerContextHandle context, int frameIndex) 73 | { 74 | using var builder = ImpellerDisplayListBuilderHandle.Create(); 75 | builder.DrawRect(new ImpellerRect(0, 0, 1280, 720), _backgroundPaint!); 76 | 77 | if (_paragraph is not null) 78 | { 79 | var panelBounds = new ImpellerRect(140f, 140f, 1000f, 440f); 80 | builder.DrawRoundedRect(panelBounds, _textPanelRadii, _backgroundPaint!); 81 | 82 | builder.DrawParagraph(_paragraph, new ImpellerPoint(160f, 200f)); 83 | } 84 | 85 | return builder.Build(); 86 | } 87 | 88 | public string DescribeFrame(int frameIndex) 89 | { 90 | var fontAlias = _fontFamilyAlias ?? "unknown"; 91 | return FormattableString.Invariant( 92 | $"typography:font={fontAlias};length={_sampleText.Length};lines={_lineCount};longest={_longestLine:F2};minW={_minIntrinsicWidth:F2};maxW={_maxIntrinsicWidth:F2};frame={frameIndex}"); 93 | } 94 | 95 | public void Dispose() 96 | { 97 | _paragraph?.Dispose(); 98 | _paragraph = null; 99 | 100 | _typography?.Dispose(); 101 | _typography = null; 102 | 103 | _textPaint?.Dispose(); 104 | _textPaint = null; 105 | 106 | _backgroundPaint?.Dispose(); 107 | _backgroundPaint = null; 108 | } 109 | 110 | private static byte[] LoadFontBytes(out string? alias) 111 | { 112 | foreach (var candidate in FontCandidates) 113 | { 114 | if (File.Exists(candidate)) 115 | { 116 | alias = Path.GetFileNameWithoutExtension(candidate); 117 | return File.ReadAllBytes(candidate); 118 | } 119 | } 120 | 121 | throw new FileNotFoundException("Unable to locate a system font for typography scene.", string.Join(", ", FontCandidates)); 122 | } 123 | 124 | private static string GetSampleText() => 125 | "Impeller Typography\n" + 126 | "Safe handles, spans, and command encoders\n" + 127 | "rendered directly from .NET."; 128 | } 129 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Package ImpellerSharp 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | env: 11 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 12 | DOTNET_NOLOGO: 1 13 | 14 | jobs: 15 | native: 16 | name: Build native (${{ matrix.os }}) 17 | runs-on: ${{ matrix.runner }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: macos 23 | runner: macos-latest 24 | platform: macos 25 | arch: arm64 26 | - os: linux 27 | runner: ubuntu-22.04 28 | platform: linux 29 | arch: x64 30 | - os: windows 31 | runner: windows-latest 32 | platform: windows 33 | arch: x64 34 | 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | with: 39 | submodules: recursive 40 | 41 | - name: Add depot_tools to PATH (Unix) 42 | if: matrix.os != 'windows' 43 | run: echo "${{ github.workspace }}/extern/depot_tools" >> $GITHUB_PATH 44 | shell: bash 45 | 46 | - name: Add depot_tools to PATH (Windows) 47 | if: matrix.os == 'windows' 48 | run: Add-Content $env:GITHUB_PATH "$env:GITHUB_WORKSPACE\extern\depot_tools" 49 | 50 | - name: Install dependencies (macOS) 51 | if: matrix.os == 'macos' 52 | run: | 53 | brew update 54 | brew install ninja python@3.11 55 | shell: bash 56 | 57 | - name: Install dependencies (Linux) 58 | if: matrix.os == 'linux' 59 | run: | 60 | sudo apt-get update 61 | sudo apt-get install -y python3 python3-pip ninja-build clang pkg-config 62 | 63 | - name: Install dependencies (Windows) 64 | if: matrix.os == 'windows' 65 | run: | 66 | choco install -y ninja python --no-progress 67 | 68 | - name: Build native Impeller (Unix) 69 | if: matrix.os != 'windows' 70 | run: > 71 | python3 build/native/build_impeller.py 72 | --platform ${{ matrix.platform }} 73 | --arch ${{ matrix.arch }} 74 | --configuration Release 75 | 76 | - name: Build native Impeller (Windows) 77 | if: matrix.os == 'windows' 78 | run: > 79 | python build/native/build_impeller.py 80 | --platform ${{ matrix.platform }} 81 | --arch ${{ matrix.arch }} 82 | --configuration Release 83 | 84 | - name: Upload native artifact 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: native-${{ matrix.os }} 88 | path: artifacts/native/** 89 | 90 | managed_tests: 91 | name: Managed Build & Tests 92 | runs-on: ubuntu-22.04 93 | needs: 94 | - native 95 | 96 | steps: 97 | - name: Checkout 98 | uses: actions/checkout@v4 99 | with: 100 | submodules: recursive 101 | 102 | - name: Download Impeller artifacts 103 | uses: actions/download-artifact@v4 104 | with: 105 | pattern: native-* 106 | path: artifacts/native 107 | merge-multiple: true 108 | 109 | - name: Stage native assets 110 | run: python3 build/managed/stage_native_assets.py --configuration Release 111 | 112 | - name: Setup .NET 113 | uses: actions/setup-dotnet@v4 114 | with: 115 | dotnet-version: 8.0.x 116 | 117 | - name: Build solution 118 | run: dotnet build ImpellerSharp.sln -c Release 119 | 120 | - name: Run tests 121 | run: dotnet test ImpellerSharp.sln -c Release --no-build --logger trx --results-directory artifacts/test-results 122 | 123 | - name: Upload test results 124 | uses: actions/upload-artifact@v4 125 | with: 126 | name: test-results 127 | path: artifacts/test-results 128 | 129 | pack: 130 | name: Pack NuGet 131 | runs-on: ubuntu-22.04 132 | needs: 133 | - native 134 | - managed_tests 135 | if: github.event_name != 'pull_request' 136 | 137 | steps: 138 | - name: Checkout 139 | uses: actions/checkout@v4 140 | with: 141 | submodules: recursive 142 | 143 | - name: Download native artifacts 144 | uses: actions/download-artifact@v4 145 | with: 146 | pattern: native-* 147 | path: artifacts/native 148 | merge-multiple: true 149 | 150 | - name: Stage native assets 151 | run: python3 build/managed/stage_native_assets.py --configuration Release 152 | 153 | - name: Setup .NET 154 | uses: actions/setup-dotnet@v4 155 | with: 156 | dotnet-version: 8.0.x 157 | 158 | - name: Pack NuGet packages 159 | run: python3 build/managed/package_all.py --configuration Release 160 | 161 | - name: Upload NuGet packages 162 | uses: actions/upload-artifact@v4 163 | with: 164 | name: nuget-packages 165 | path: artifacts/nuget 166 | 167 | - name: Upload packaging manifest 168 | uses: actions/upload-artifact@v4 169 | with: 170 | name: nuget-manifest 171 | path: artifacts/nuget/manifest.json 172 | -------------------------------------------------------------------------------- /src/ImpellerSharp.Interop/Handles/ImpellerImageFilterHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImpellerSharp.Interop; 4 | 5 | public sealed unsafe class ImpellerImageFilterHandle : ImpellerSafeHandle 6 | { 7 | private ImpellerImageFilterHandle(nint native) 8 | { 9 | SetHandle(native); 10 | } 11 | 12 | public static ImpellerImageFilterHandle CreateBlur(float sigmaX, float sigmaY, ImpellerTileMode tileMode) 13 | { 14 | var native = ImpellerNative.ImpellerImageFilterCreateBlurNew(sigmaX, sigmaY, tileMode); 15 | return new ImpellerImageFilterHandle( 16 | EnsureSuccess(native, "Failed to create Impeller image filter (blur).")); 17 | } 18 | 19 | public static ImpellerImageFilterHandle CreateDilate(float radiusX, float radiusY) 20 | { 21 | var native = ImpellerNative.ImpellerImageFilterCreateDilateNew(radiusX, radiusY); 22 | return new ImpellerImageFilterHandle( 23 | EnsureSuccess(native, "Failed to create Impeller image filter (dilate).")); 24 | } 25 | 26 | public static ImpellerImageFilterHandle CreateErode(float radiusX, float radiusY) 27 | { 28 | var native = ImpellerNative.ImpellerImageFilterCreateErodeNew(radiusX, radiusY); 29 | return new ImpellerImageFilterHandle( 30 | EnsureSuccess(native, "Failed to create Impeller image filter (erode).")); 31 | } 32 | 33 | public static ImpellerImageFilterHandle CreateMatrix(in ImpellerMatrix matrix, ImpellerTextureSampling sampling) 34 | { 35 | var value = matrix; 36 | var native = ImpellerNative.ImpellerImageFilterCreateMatrixNew(&value, sampling); 37 | return new ImpellerImageFilterHandle( 38 | EnsureSuccess(native, "Failed to create Impeller image filter (matrix).")); 39 | } 40 | 41 | public static ImpellerImageFilterHandle CreateCompose( 42 | ImpellerImageFilterHandle outer, 43 | ImpellerImageFilterHandle inner) 44 | { 45 | if (outer is null) 46 | { 47 | throw new ArgumentNullException(nameof(outer)); 48 | } 49 | 50 | if (inner is null) 51 | { 52 | throw new ArgumentNullException(nameof(inner)); 53 | } 54 | 55 | var native = ImpellerNative.ImpellerImageFilterCreateComposeNew( 56 | outer.DangerousGetHandle(), 57 | inner.DangerousGetHandle()); 58 | 59 | return new ImpellerImageFilterHandle( 60 | EnsureSuccess(native, "Failed to create Impeller image filter (compose).")); 61 | } 62 | 63 | public static ImpellerImageFilterHandle CreateFragmentProgram( 64 | ImpellerContextHandle context, 65 | ImpellerFragmentProgramHandle fragmentProgram, 66 | ReadOnlySpan samplers, 67 | ReadOnlySpan uniformData) 68 | { 69 | if (context is null) 70 | { 71 | throw new ArgumentNullException(nameof(context)); 72 | } 73 | 74 | if (fragmentProgram is null) 75 | { 76 | throw new ArgumentNullException(nameof(fragmentProgram)); 77 | } 78 | 79 | Span samplerHandles = samplers.Length <= 16 80 | ? stackalloc nint[samplers.Length] 81 | : new nint[samplers.Length]; 82 | 83 | for (var i = 0; i < samplers.Length; ++i) 84 | { 85 | if (samplers[i] is null) 86 | { 87 | throw new ArgumentNullException(nameof(samplers), "Sampler textures must not be null."); 88 | } 89 | 90 | samplerHandles[i] = samplers[i]!.DangerousGetHandle(); 91 | } 92 | 93 | var addedRef = false; 94 | nint native = nint.Zero; 95 | 96 | try 97 | { 98 | context.DangerousAddRef(ref addedRef); 99 | 100 | fixed (nint* samplerPtr = samplerHandles) 101 | fixed (byte* dataPtr = uniformData) 102 | { 103 | native = ImpellerNative.ImpellerImageFilterCreateFragmentProgramNew( 104 | context.DangerousGetHandle(), 105 | fragmentProgram.DangerousGetHandle(), 106 | samplerPtr, 107 | (nuint)samplers.Length, 108 | dataPtr, 109 | (nuint)uniformData.Length); 110 | } 111 | } 112 | finally 113 | { 114 | if (addedRef) 115 | { 116 | context.DangerousRelease(); 117 | } 118 | } 119 | 120 | return new ImpellerImageFilterHandle( 121 | EnsureSuccess(native, "Failed to create Impeller image filter (fragment program).")); 122 | } 123 | 124 | internal void Retain() 125 | { 126 | ThrowIfInvalid(); 127 | ImpellerNative.ImpellerImageFilterRetain(handle); 128 | } 129 | 130 | protected override bool ReleaseHandle() 131 | { 132 | ImpellerNative.ImpellerImageFilterRelease(handle); 133 | return true; 134 | } 135 | 136 | internal new nint DangerousGetHandle() 137 | { 138 | ThrowIfInvalid(); 139 | return handle; 140 | } 141 | 142 | private void ThrowIfInvalid() 143 | { 144 | if (IsInvalid) 145 | { 146 | throw new ObjectDisposedException(nameof(ImpellerImageFilterHandle)); 147 | } 148 | } 149 | } 150 | --------------------------------------------------------------------------------