1
0
Эх сурвалжийг харах

[OSX] Implemented IOSurface/MTLSharedEvent interop APIs (#18791)

* [OSX] Implemented IOSurface/MTLSharedEvent interop APIs

* Bump Xcode?

* Xcode?

* APIDiff

* Use different XCode versions because of how awesome appium is

* A hack for crapium

* Replace SkiaMetalApi usages

* Update API suppressions

---------

Co-authored-by: Timothy Miller <[email protected]>
Co-authored-by: Julien Lebosquain <[email protected]>
Nikita Tsukanov 2 долоо хоног өмнө
parent
commit
22c4c630ce
41 өөрчлөгдсөн 1458 нэмэгдсэн , 129 устгасан
  1. 72 0
      api/Avalonia.nupkg.xml
  2. 2 1
      azure-pipelines-integrationtests.yml
  3. 2 2
      azure-pipelines.yml
  4. 4 1
      native/Avalonia.Native/inc/noarc.h
  5. 10 0
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  6. 55 0
      native/Avalonia.Native/src/OSX/cgl.mm
  7. 2 0
      native/Avalonia.Native/src/OSX/common.h
  8. 9 0
      native/Avalonia.Native/src/OSX/crapium.h
  9. 21 0
      native/Avalonia.Native/src/OSX/crapium.mm
  10. 16 0
      native/Avalonia.Native/src/OSX/main.mm
  11. 40 0
      native/Avalonia.Native/src/OSX/memhelp.mm
  12. 168 1
      native/Avalonia.Native/src/OSX/metal.mm
  13. 15 1
      native/Avalonia.Native/src/OSX/noarc.mm
  14. 42 0
      samples/GpuInterop/NativeMethods.cs
  15. 5 3
      samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs
  16. 88 30
      samples/GpuInterop/VulkanDemo/VulkanContext.cs
  17. 111 44
      samples/GpuInterop/VulkanDemo/VulkanImage.cs
  18. 59 10
      samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs
  19. 72 0
      samples/GpuInterop/VulkanDemo/VulkanTimelineSemaphore.cs
  20. 34 0
      src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs
  21. 10 0
      src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs
  22. 17 0
      src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs
  23. 5 1
      src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs
  24. 21 9
      src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs
  25. 13 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs
  26. 36 0
      src/Avalonia.Metal/IMetalExternalObjectsFeature.cs
  27. 154 5
      src/Avalonia.Native/AvaloniaNativeGlPlatformGraphics.cs
  28. 1 1
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  29. 55 0
      src/Avalonia.Native/GpuHandleWrapFeature.cs
  30. 99 4
      src/Avalonia.Native/Metal.cs
  31. 40 0
      src/Avalonia.Native/avn.idl
  32. 11 0
      src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs
  33. 3 3
      src/Avalonia.OpenGL/GlConsts.cs
  34. 12 0
      src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs
  35. 75 0
      src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalExternalObjectsFeature.cs
  36. 14 4
      src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs
  37. 50 7
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs
  38. 2 0
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs
  39. 4 0
      src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaExternalObjectsFeature.cs
  40. 7 2
      src/Skia/Avalonia.Skia/ImmutableBitmap.cs
  41. 2 0
      src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs

+ 72 - 0
api/Avalonia.nupkg.xml

@@ -25,6 +25,30 @@
     <Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
     <Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
   </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target>
+    <Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left>
+    <Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64)</Target>
+    <Left>baseline/Avalonia/lib/net6.0/Avalonia.OpenGL.dll</Left>
+    <Right>current/Avalonia/lib/net6.0/Avalonia.OpenGL.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>M:Avalonia.OpenGL.IGlExternalSemaphore.WaitTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64)</Target>
+    <Left>baseline/Avalonia/lib/net6.0/Avalonia.OpenGL.dll</Left>
+    <Right>current/Avalonia/lib/net6.0/Avalonia.OpenGL.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>P:Avalonia.OpenGL.IGlExternalImageTexture.TextureType</Target>
+    <Left>baseline/Avalonia/lib/net6.0/Avalonia.OpenGL.dll</Left>
+    <Right>current/Avalonia/lib/net6.0/Avalonia.OpenGL.dll</Right>
+  </Suppression>
   <Suppression>
     <DiagnosticId>CP0006</DiagnosticId>
     <Target>M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)</Target>
@@ -49,6 +73,30 @@
     <Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
     <Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
   </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target>
+    <Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
+    <Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64)</Target>
+    <Left>baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Left>
+    <Right>current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>M:Avalonia.OpenGL.IGlExternalSemaphore.WaitTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64)</Target>
+    <Left>baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Left>
+    <Right>current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>P:Avalonia.OpenGL.IGlExternalImageTexture.TextureType</Target>
+    <Left>baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Left>
+    <Right>current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Right>
+  </Suppression>
   <Suppression>
     <DiagnosticId>CP0006</DiagnosticId>
     <Target>M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)</Target>
@@ -73,4 +121,28 @@
     <Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
     <Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
   </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target>
+    <Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
+    <Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64)</Target>
+    <Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Left>
+    <Right>current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>M:Avalonia.OpenGL.IGlExternalSemaphore.WaitTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64)</Target>
+    <Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Left>
+    <Right>current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>P:Avalonia.OpenGL.IGlExternalImageTexture.TextureType</Target>
+    <Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Left>
+    <Right>current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll</Right>
+  </Suppression>
 </Suppressions>

+ 2 - 1
azure-pipelines-integrationtests.yml

@@ -25,12 +25,13 @@ jobs:
       arch="arm64"
       fi
       git clean -ffdx
-      sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
       pkill node
       pkill testmanagerd
       appium > appium.out &
       pkill IntegrationTestApp
+      sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer        
       ./build.sh CompileNative
+      sudo xcode-select -s /Applications/Xcode_14.3.app/Contents/Developer
       rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")")
       pkill IntegrationTestApp
       ./samples/IntegrationTestApp/bundle.sh

+ 2 - 2
azure-pipelines.yml

@@ -95,11 +95,11 @@ jobs:
     inputs:
       actions: 'build'
       scheme: ''
-      sdk: 'macosx13.0'
+      sdk: 'macosx14.2'
       configuration: 'Release'
       xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace'
       xcodeVersion: 'specifyPath' # Options: 8, 9, default, specifyPath
-      xcodeDeveloperDir: '/Applications/Xcode_14.1.app/Contents/Developer'
+      xcodeDeveloperDir: '/Applications/Xcode_15.2.app/Contents/Developer'
       args: '-derivedDataPath ./'
 
   - task: CmdLine@2

+ 4 - 1
native/Avalonia.Native/inc/noarc.h

@@ -8,4 +8,7 @@ public:
     ~CppAutoreleasePool();
 };
 
-#define START_ARP_CALL CppAutoreleasePool __autoreleasePool
+#define START_ARP_CALL CppAutoreleasePool __autoreleasePool
+extern void ReleaseNSObject(void* obj);
+extern void RetainNSObject(void* obj);
+extern uint64_t GetRetainCountForNSObject(void* obj);

+ 10 - 0
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@@ -31,6 +31,8 @@
 		1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; };
 		1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; };
 		1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; };
+		1AC7F1432DCA0C2E003A161B /* crapium.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AC7F1422DCA0C2E003A161B /* crapium.mm */; };
+		1AE55B8C2DC1060E00FD0BB3 /* memhelp.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AE55B8B2DC1060E00FD0BB3 /* memhelp.mm */; };
 		1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; };
 		37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; };
 		37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; };
@@ -91,6 +93,9 @@
 		1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = "<group>"; };
 		1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
 		1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = "<group>"; };
+		1AC7F1422DCA0C2E003A161B /* crapium.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objcpp; path = crapium.mm; sourceTree = "<group>"; };
+		1AC7F1442DCA0D6A003A161B /* crapium.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = crapium.h; sourceTree = "<group>"; };
+		1AE55B8B2DC1060E00FD0BB3 /* memhelp.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = memhelp.mm; sourceTree = "<group>"; };
 		1AFD334023E03C4F0042899B /* controlhost.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = controlhost.mm; sourceTree = "<group>"; };
 		37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = "<group>"; };
 		379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = "<group>"; };
@@ -171,6 +176,9 @@
 		AB7A61E62147C814003C5833 = {
 			isa = PBXGroup;
 			children = (
+				1AC7F1442DCA0D6A003A161B /* crapium.h */,
+				1AC7F1422DCA0C2E003A161B /* crapium.mm */,
+				1AE55B8B2DC1060E00FD0BB3 /* memhelp.mm */,
 				F10084852BFF1FB40024303E /* TopLevelImpl.mm */,
 				BC7C33832C066F1100945A48 /* AvnAccessibility.h */,
 				BC7C33812C066DBF00945A48 /* AvnAutomationNode.h */,
@@ -333,6 +341,7 @@
 				523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */,
 				AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */,
 				1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */,
+				1AE55B8C2DC1060E00FD0BB3 /* memhelp.mm in Sources */,
 				1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */,
 				BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */,
 				37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
@@ -353,6 +362,7 @@
 				ED754D262A97306B0078B4DF /* PlatformRenderTimer.mm in Sources */,
 				1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */,
 				18391AC16726CBC45856233B /* AvnWindow.mm in Sources */,
+				1AC7F1432DCA0C2E003A161B /* crapium.mm in Sources */,
 				18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */,
 				EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */,
 				64B1EA48E308E574685AFB07 /* metal.mm in Sources */,

+ 55 - 0
native/Avalonia.Native/src/OSX/cgl.mm

@@ -107,6 +107,61 @@ public:
         return Context;
     }
     
+    int texImageIOSurface2D(int target, int internal_format,
+                                int width, int height, int format, int type, void* ioSurface, int plane) override
+    {
+        return CGLTexImageIOSurface2D(Context, target, internal_format, width, height, format, type, (IOSurfaceRef)ioSurface, plane);
+    }
+    
+    bool GetIOKitRegistryId(uint64_t *value) override {
+        if (@available(macOS 10.13, *))
+        {
+            
+            GLint rendererId;
+            if(CGLGetParameter(Context, kCGLCPCurrentRendererID, &rendererId) != 0)
+                return false;
+            
+            GLint rendererCount = 0;
+            CGLRendererInfoObj rendererInfo;
+            
+            if(CGLQueryRendererInfo(0xFFFFFFFF, &rendererInfo, &rendererCount))
+                return false;
+            
+            @try
+            {
+                for(auto i = 0; i < rendererCount; i++)
+                {
+                    GLint thisRendererID;
+                    
+                    CGLDescribeRenderer(rendererInfo, i, kCGLRPRendererID, &thisRendererID);
+                    if(thisRendererID == rendererId)
+                    {
+                        GLint gpuIDLow  = 0;
+                        GLint gpuIDHigh = 0;
+                        
+                        if(CGLDescribeRenderer(rendererInfo, 0, kCGLRPRegistryIDLow, &gpuIDLow))
+                            return false;
+                        
+                        if(CGLDescribeRenderer(rendererInfo, 0, kCGLRPRegistryIDHigh, &gpuIDHigh))
+                            return false;
+                        
+                        *value = ((uint64_t)gpuIDHigh << 32) | gpuIDLow;
+                        return true;
+                    }
+                }
+                return false;
+                
+            }
+            @finally
+            {
+                CGLDestroyRendererInfo(rendererInfo);
+            }
+        }
+        else
+            return false;
+    }
+    
+    
     ~AvnGlContext()
     {
         CGLReleaseContext(Context);

+ 2 - 0
native/Avalonia.Native/src/OSX/common.h

@@ -33,6 +33,7 @@ extern IAvnPlatformBehaviorInhibition* CreatePlatformBehaviorInhibition();
 extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
 extern IAvnPlatformSettings* CreatePlatformSettings();
 extern IAvnPlatformRenderTimer* CreatePlatformRenderTimer();
+extern IAvnNativeObjectsMemoryManagement* CreateMemoryManagementHelper();
 extern void SetAppMenu(IAvnMenu *menu);
 extern void SetServicesMenu (IAvnMenu* menu);
 extern IAvnMenu* GetAppMenu ();
@@ -47,6 +48,7 @@ extern AvnPoint ToAvnPoint (NSPoint p);
 extern AvnPoint ConvertPointY (AvnPoint p);
 extern NSSize ToNSSize (AvnSize s);
 extern AvnSize FromNSSize (NSSize s);
+extern IAvnMTLSharedEvent* ImportMTLSharedEvent(void* object);
 #ifdef DEBUG
 #define NSDebugLog(...) NSLog(__VA_ARGS__)
 #else

+ 9 - 0
native/Avalonia.Native/src/OSX/crapium.h

@@ -0,0 +1,9 @@
+// The only reason this file exists is Appium which limits our highest Xcode version to 15.2. Please, purge Appium from our codebase
+#ifndef crapium_h
+#define crapium_h
+#import <Foundation/Foundation.h>
+@protocol MTLSharedEvent;
+
+API_AVAILABLE(macos(12))
+extern BOOL MtlSharedEventWaitUntilSignaledValueHack(id<MTLSharedEvent> ev, uint64_t value, uint64_t milliseconds);
+#endif /* crapium_h */

+ 21 - 0
native/Avalonia.Native/src/OSX/crapium.mm

@@ -0,0 +1,21 @@
+// The only reason this file exists is Appium which limits our highest Xcode version to 15.2. Please, purge Appium from our codebase
+#import <Foundation/Foundation.h>
+#import "crapium.h"
+@class MTLSharedEventHandle;
+@protocol MTLSharedEvent;
+@protocol MTLEvent;
+
+typedef void (^MTLSharedEventNotificationBlock)(id <MTLSharedEvent>, uint64_t value);
+
+API_AVAILABLE(macos(10.14), ios(12.0))
+@protocol MTLSharedEvent <MTLEvent>
+// Synchronously wait for the signaledValue to be greater than or equal to 'value', with a timeout
+// specified in milliseconds.   Returns YES if the value was signaled before the timeout, otherwise NO.
+- (BOOL)waitUntilSignaledValue:(uint64_t)value timeoutMS:(uint64_t)milliseconds API_AVAILABLE(macos(12.0), ios(15.0));
+@end
+
+API_AVAILABLE(macos(12))
+extern BOOL MtlSharedEventWaitUntilSignaledValueHack(id<MTLSharedEvent> ev, uint64_t value, uint64_t milliseconds)
+{
+    return [ev waitUntilSignaledValue:value timeoutMS:milliseconds];
+}

+ 16 - 0
native/Avalonia.Native/src/OSX/main.mm

@@ -467,6 +467,22 @@ public:
             return S_OK;
         }
     }
+    
+    virtual HRESULT ImportMTLSharedEvent(void* event, IAvnMTLSharedEvent** ppv) override
+    {
+        START_COM_CALL;
+        *ppv = ::ImportMTLSharedEvent(event);
+        return *ppv != nullptr ? S_OK : E_FAIL;
+    }
+    
+    HRESULT CreateMemoryManagementHelper(IAvnNativeObjectsMemoryManagement **ppv) override { 
+        START_COM_CALL;
+        *ppv = ::CreateMemoryManagementHelper();
+        return S_OK;
+    }
+    
+    
+    
 };
 
 extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()

+ 40 - 0
native/Avalonia.Native/src/OSX/memhelp.mm

@@ -0,0 +1,40 @@
+#include "common.h"
+class MemHelper : public ComSingleObject<IAvnNativeObjectsMemoryManagement, &IID_IAvnNativeObjectsMemoryManagement>
+{
+    FORWARD_IUNKNOWN()
+    void RetainNSObject(void *object) override
+    {
+        ::RetainNSObject(object);
+    }
+    
+    void ReleaseNSObject(void *object) override
+    {
+        ::ReleaseNSObject(object);
+    }
+    
+    void RetainCFObject(void *object) override
+    {
+        CFRetain(object);
+    }
+    
+    void ReleaseCFObject(void *object) override
+    {
+        CFRelease(object);
+    }
+    
+    uint64_t GetRetainCountForNSObject(void *obj) override {
+        return ::GetRetainCountForNSObject(obj);
+    }
+    
+    int64_t GetRetainCountForCFObject(void *obj) override { 
+        return CFGetRetainCount(obj);
+    }
+    
+    
+};
+
+
+extern IAvnNativeObjectsMemoryManagement* CreateMemoryManagementHelper()
+{
+    return new MemHelper();
+}

+ 168 - 1
native/Avalonia.Native/src/OSX/metal.mm

@@ -3,6 +3,74 @@
 #import <QuartzCore/QuartzCore.h>
 #include "common.h"
 #include "rendertarget.h"
+#import "crapium.h"
+
+
+class API_AVAILABLE(macos(12.0)) AvnMTLSharedEvent : public ComSingleObject<IAvnMTLSharedEvent, &IID_IAvnMTLSharedEvent>
+{
+    id<MTLSharedEvent> _event;
+public:
+    
+    AvnMTLSharedEvent(id<MTLSharedEvent> ev) : _event(ev)
+    {
+        
+    }
+    
+    FORWARD_IUNKNOWN()
+    
+    id<MTLSharedEvent> GetEvent()
+    {
+        return _event;
+    }
+    
+    void *GetNativeHandle() override {
+        return (__bridge void*)_event;
+    }
+    
+    bool Wait(uint64_t value, uint64_t timeoutMS) override {
+        return MtlSharedEventWaitUntilSignaledValueHack(_event, value, timeoutMS);
+    }
+    
+    void SetSignaledValue(uint64_t value) override {
+        _event.signaledValue = value;
+    }
+    
+    uint64_t GetSignaledValue() override {
+        return _event.signaledValue;
+    }
+};
+
+
+class AvnMetalTexture : public ComSingleObject<IAvnMetalTexture, &IID_IAvnMetalTexture>
+{
+    id<MTLTexture> _texture;
+public:
+    FORWARD_IUNKNOWN()
+    AvnMetalTexture(id<MTLTexture> texture) : _texture(texture)
+    {
+        
+    }
+    void *GetNativeHandle() override
+    {
+        return (__bridge void*)_texture;
+    }
+    
+    int GetWidth() override
+    {
+        return (int)_texture.width;
+    }
+    
+    int GetHeight() override
+    {
+        return (int)_texture.height;
+    }
+    
+    int GetSampleCount() override
+    {
+        return (int)_texture.sampleCount;
+    }
+    
+};
 
 class AvnMetalDevice : public ComSingleObject<IAvnMetalDevice, &IID_IAvnMetalDevice>
 {
@@ -18,7 +86,86 @@ public:
     void *GetQueue() override {
         return (__bridge void*) queue;
     }
-
+    
+    HRESULT ImportIOSurface(void *handle, AvnPixelFormat pixelFormat, IAvnMetalTexture **ppv) override { 
+        auto surf = (IOSurfaceRef)handle;
+        auto width = IOSurfaceGetWidth(surf);
+        auto height = IOSurfaceGetHeight(surf);
+        
+        auto desc = [MTLTextureDescriptor new];
+        if(pixelFormat == kAvnRgba8888)
+            desc.pixelFormat = MTLPixelFormatRGBA8Unorm;
+        else if(pixelFormat == kAvnBgra8888)
+            desc.pixelFormat = MTLPixelFormatBGRA8Unorm;
+        else
+            return E_INVALIDARG;
+        desc.textureType = MTLTextureType2D;
+        desc.width = width;
+        desc.height = height;
+        desc.depth = 1;
+        desc.mipmapLevelCount = 1;
+        desc.sampleCount = 1;
+        desc.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget;
+        
+        auto texture = [device newTextureWithDescriptor:desc iosurface:surf plane:0];
+        if(texture == nullptr)
+            return E_FAIL;
+        *ppv = new AvnMetalTexture(texture);
+        return S_OK;
+        
+    }
+    
+    HRESULT ImportSharedEvent(void *mtlSharedEventInstance, IAvnMTLSharedEvent**ppv) override {
+        if (@available(macOS 12.0, *)) {
+            auto external = (__bridge id<MTLSharedEvent>)mtlSharedEventInstance;
+            auto handle = external.newSharedEventHandle;
+            auto imported = [device newSharedEventWithHandle: handle];
+            *ppv = new AvnMTLSharedEvent(imported);
+            return S_OK;
+        } 
+        else
+        {
+            return E_NOTIMPL;
+        }
+    }
+    
+    
+    HRESULT SignalOrWait(IAvnMTLSharedEvent *ev, uint64_t value, bool wait)
+    {
+        if (@available(macOS 12.0, *))
+        {
+            auto e = dynamic_cast<AvnMTLSharedEvent*>(ev);
+            if(e == nullptr)
+                return E_FAIL;;
+            auto buf = [queue commandBuffer];
+            if(wait)
+                [buf encodeWaitForEvent:e->GetEvent() value:value];
+            else
+                [buf encodeSignalEvent:e->GetEvent() value:value];
+            [buf commit];
+            return S_OK;
+        }
+        else
+            return E_FAIL;
+    }
+    
+    HRESULT SubmitWait(IAvnMTLSharedEvent *ev, uint64_t value) override {
+        return SignalOrWait(ev, value, true);
+    }
+    
+    HRESULT SubmitSignal(IAvnMTLSharedEvent *ev, uint64_t value) override { 
+        return SignalOrWait(ev, value, false);
+    }
+    
+    bool GetIOKitRegistryId(uint64_t *value) override { 
+        if (@available(macOS 10.13, *)) {
+            *value = [device registryID];
+            return true;
+        } else {
+            return false;
+        }
+    }
+    
     AvnMetalDevice(id <MTLDevice> device, id <MTLCommandQueue> queue) : device(device), queue(queue) {
     }
 
@@ -160,3 +307,23 @@ extern IAvnMetalDisplay* GetMetalDisplay()
 {
     return _display;
 }
+
+
+extern IAvnMTLSharedEvent* ImportMTLSharedEvent(void* object)
+{
+    if (@available(macOS 12.0, *)) {
+    if(object == nullptr)
+        return nil;
+    auto evId = (__bridge id<MTLSharedEvent>)object;
+    
+    if(evId == nil)
+        return nil;
+    
+    
+    return new AvnMTLSharedEvent(evId);
+    } 
+    else
+    {
+        return nil;
+    }
+}

+ 15 - 1
native/Avalonia.Native/src/OSX/noarc.mm

@@ -1,5 +1,5 @@
 #include "noarc.h"
-
+#include "avalonia-native.h"
 CppAutoreleasePool::CppAutoreleasePool()
 {
     _pool = [[NSAutoreleasePool alloc] init];
@@ -9,3 +9,17 @@ CppAutoreleasePool::~CppAutoreleasePool() {
     auto ptr = (NSAutoreleasePool*)_pool;
     [ptr release];
 }
+
+extern void ReleaseNSObject(void* obj)
+{
+    [(NSObject*)obj release];
+}
+extern void RetainNSObject(void* obj)
+{
+    [(NSObject*)obj retain];
+}
+
+extern uint64_t GetRetainCountForNSObject(void* obj)
+{
+    return [(NSObject*)obj retainCount];
+}

+ 42 - 0
samples/GpuInterop/NativeMethods.cs

@@ -0,0 +1,42 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace GpuInterop;
+
+static class NativeMethods
+{
+    [Flags]
+    public enum IOSurfaceLockOptions : uint
+    {
+        None = 0,
+        ReadOnly = 1 << 0,
+        AvoidSync = 1 << 1,
+    }
+
+    [DllImport("/System/Library/Frameworks/IOSurface.framework/IOSurface")]
+    public static extern int IOSurfaceLock(IntPtr surface, IOSurfaceLockOptions options, IntPtr seed);
+
+    [DllImport("/System/Library/Frameworks/IOSurface.framework/IOSurface")]
+    public static extern nint IOSurfaceGetWidth(IntPtr surface);
+
+    [DllImport("/System/Library/Frameworks/IOSurface.framework/IOSurface")]
+    public static extern nint IOSurfaceGetHeight(IntPtr surface);
+
+    [DllImport("/System/Library/Frameworks/IOSurface.framework/IOSurface")]
+    public static extern nint IOSurfaceGetBytesPerRow(IntPtr surface);
+
+    [DllImport("/System/Library/Frameworks/IOSurface.framework/IOSurface")]
+    public static extern IntPtr IOSurfaceGetBaseAddress(IntPtr surface);
+
+    [DllImport("/System/Library/Frameworks/IOSurface.framework/IOSurface")]
+    public static extern void IOSurfaceUnlock(IntPtr surface, IOSurfaceLockOptions options, IntPtr seed);
+    
+    [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
+    public static extern IntPtr CFRetain(IntPtr cf);
+
+    [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
+    public static extern void CFRelease(IntPtr cf);
+
+    [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
+    public static extern nint CFGetRetainCount(IntPtr cf);
+}

+ 5 - 3
samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs

@@ -167,7 +167,8 @@ namespace Avalonia.Vulkan
                 ReadOnlySpan<PipelineStageFlags> waitDstStageMask = default,
                 ReadOnlySpan<Semaphore> signalSemaphores = default,
                 Fence? fence = null,
-                KeyedMutexSubmitInfo? keyedMutex = null)
+                KeyedMutexSubmitInfo? keyedMutex = null,
+                IntPtr pNext = default)
             {
                 EndRecording();
 
@@ -189,7 +190,8 @@ namespace Avalonia.Vulkan
                         PReleaseKeys = &releaseKey,
                         PAcquireSyncs = &devMem,
                         PReleaseSyncs = &devMem,
-                        PAcquireTimeouts = &timeout
+                        PAcquireTimeouts = &timeout,
+                        PNext = (void*)pNext
                     };
                 
                 fixed (Semaphore* pWaitSemaphores = waitSemaphores, pSignalSemaphores = signalSemaphores)
@@ -199,7 +201,7 @@ namespace Avalonia.Vulkan
                         var commandBuffer = InternalHandle;
                         var submitInfo = new SubmitInfo
                         {
-                            PNext = keyedMutex != null ? &mutex : null,
+                            PNext = keyedMutex != null ? &mutex : (void*)pNext,
                             SType = StructureType.SubmitInfo,
                             WaitSemaphoreCount = waitSemaphores != null ? (uint)waitSemaphores.Length : 0,
                             PWaitSemaphores = pWaitSemaphores,

+ 88 - 30
samples/GpuInterop/VulkanDemo/VulkanContext.cs

@@ -1,17 +1,21 @@
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Runtime.InteropServices;
 using Avalonia.Platform;
 using Avalonia.Rendering.Composition;
 using Avalonia.Vulkan;
 using Silk.NET.Core;
+using Silk.NET.Core.Contexts;
+using Silk.NET.Core.Loader;
 using Silk.NET.Vulkan;
 using Silk.NET.Vulkan.Extensions.EXT;
 using Silk.NET.Vulkan.Extensions.KHR;
 using SilkNetDemo;
 using SkiaSharp;
 using D3DDevice = SharpDX.Direct3D11.Device;
+#pragma warning disable CS0162 // Unreachable code detected
 
 namespace GpuInterop.VulkanDemo;
 
@@ -24,7 +28,7 @@ public unsafe class VulkanContext : IDisposable
     public required Queue Queue { get; init; }
     public required uint QueueFamilyIndex { get; init; }
     public required VulkanCommandBufferPool Pool { get; init; }
-    public required GRContext GrContext { get; init; }
+    public required GRContext? GrContext { get; init; }
     public required DescriptorPool DescriptorPool { get; init; }
     public required D3DDevice? D3DDevice { get; init; }
 
@@ -44,14 +48,22 @@ public unsafe class VulkanContext : IDisposable
 
         var enabledExtensions = new List<string>()
         {
-            "VK_KHR_get_physical_device_properties2",
-            "VK_KHR_external_memory_capabilities",
-            "VK_KHR_external_semaphore_capabilities"
+            "VK_KHR_get_physical_device_properties2"
         };
+        if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            enabledExtensions.Add("VK_KHR_portability_enumeration");
+
+        if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+        {
+            enabledExtensions.AddRange([
+                "VK_KHR_external_memory_capabilities",
+                "VK_KHR_external_semaphore_capabilities"
+            ]);
+        }
 
         var enabledLayers = new List<string>();
         
-        Vk api = Vk.GetApi();
+        Vk api = GetApi();
         enabledExtensions.Add("VK_EXT_debug_utils");
         if (IsLayerAvailable(api, "VK_LAYER_KHRONOS_validation"))
             enabledLayers.Add("VK_LAYER_KHRONOS_validation");
@@ -61,8 +73,10 @@ public unsafe class VulkanContext : IDisposable
         DescriptorPool descriptorPool = default;
         VulkanCommandBufferPool? pool = null;
         GRContext? grContext = null;
+        bool success = false;
         try
         {
+            enabledLayers.Clear();
             using var pRequiredExtensions = new ByteStringList(enabledExtensions);
             using var pEnabledLayers = new ByteStringList(enabledLayers);
             api.CreateInstance(new InstanceCreateInfo
@@ -72,7 +86,8 @@ public unsafe class VulkanContext : IDisposable
                 PpEnabledExtensionNames = pRequiredExtensions,
                 EnabledExtensionCount = pRequiredExtensions.UCount,
                 PpEnabledLayerNames = pEnabledLayers,
-                EnabledLayerCount = pEnabledLayers.UCount
+                EnabledLayerCount = pEnabledLayers.UCount,
+                Flags = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? InstanceCreateFlags.EnumeratePortabilityBitKhr : default
             }, null, out var vkInstance).ThrowOnError();
 
 
@@ -93,10 +108,13 @@ public unsafe class VulkanContext : IDisposable
                 debugUtils.CreateDebugUtilsMessenger(vkInstance, debugCreateInfo, null, out _);
             }
 
-            var requireDeviceExtensions = new List<string>
+            var requireDeviceExtensions = new List<string>();
+            if(!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
             {
-                "VK_KHR_external_memory",
-                "VK_KHR_external_semaphore"
+                requireDeviceExtensions.AddRange([
+                    "VK_KHR_external_memory",
+                    "VK_KHR_external_semaphore"
+                ]);
             };
 
             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -112,6 +130,14 @@ public unsafe class VulkanContext : IDisposable
                 requireDeviceExtensions.Add("VK_KHR_dedicated_allocation");
                 requireDeviceExtensions.Add("VK_KHR_get_memory_requirements2");
             }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                if (!gpuInterop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes
+                        .IOSurfaceRef)
+                   )
+                    return (null, "Image sharing is not supported by the current backend");
+                requireDeviceExtensions.AddRange(["VK_EXT_metal_objects", "VK_KHR_timeline_semaphore"]);
+            }
             else
             {
                 if (!gpuInterop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes
@@ -223,26 +249,29 @@ public unsafe class VulkanContext : IDisposable
                         .ThrowOnError();
 
                     pool = new VulkanCommandBufferPool(api, device, queue, queueFamilyIndex);
-                    grContext = GRContext.CreateVulkan(new GRVkBackendContext
+                    if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
                     {
-                        VkInstance = vkInstance.Handle,
-                        VkDevice = device.Handle,
-                        VkQueue = queue.Handle,
-                        GraphicsQueueIndex = queueFamilyIndex,
-                        VkPhysicalDevice = physicalDevice.Handle,
-                        GetProcedureAddress = (proc, _, _) =>
+                        grContext = GRContext.CreateVulkan(new GRVkBackendContext
                         {
-                            var rv = api.GetDeviceProcAddr(device, proc);
-                            if (rv != IntPtr.Zero)
-                                return rv;
-                            rv = api.GetInstanceProcAddr(vkInstance, proc);
-                            if (rv != IntPtr.Zero)
-                                return rv;
-                            return api.GetInstanceProcAddr(default, proc);
-                        }
-                    });
-                    
-                    
+                            VkInstance = vkInstance.Handle,
+                            VkDevice = device.Handle,
+                            VkQueue = queue.Handle,
+                            GraphicsQueueIndex = queueFamilyIndex,
+                            VkPhysicalDevice = physicalDevice.Handle,
+                            GetProcedureAddress = (proc, _, _) =>
+                            {
+                                var rv = api.GetDeviceProcAddr(device, proc);
+                                if (rv != IntPtr.Zero)
+                                    return rv;
+                                rv = api.GetInstanceProcAddr(vkInstance, proc);
+                                if (rv != IntPtr.Zero)
+                                    return rv;
+                                return api.GetInstanceProcAddr(default, proc);
+                            }
+                        });
+                        if (grContext == null)
+                            return (null, "Can't create Skia GrContext, device is likely broken");
+                    }
 
                     D3DDevice? d3dDevice = null;
                     if (physicalDeviceIDProperties.DeviceLuidvalid &&
@@ -251,7 +280,7 @@ public unsafe class VulkanContext : IDisposable
                         )
                         d3dDevice = D3DMemoryHelper.CreateDeviceByLuid(
                             new Span<byte>(physicalDeviceIDProperties.DeviceLuid, 8));
-
+                    success = true;
                     return (new VulkanContext
                     {
                         Api = api,
@@ -278,7 +307,7 @@ public unsafe class VulkanContext : IDisposable
         }
         finally
         {
-            if (grContext == null && api != null)
+            if (!success)
             {
                 pool?.Dispose();
                 if (descriptorPool.Handle != default)
@@ -322,11 +351,40 @@ public unsafe class VulkanContext : IDisposable
 
         return Vk.False;
     }
+
+
+    private const string MacVulkanSdkGlobalPath = "/usr/local/lib/libvulkan.dylib";
+    class MacLibraryNameContainer : SearchPathContainer
+    {
+        public override string Windows64 { get; }
+        public override string Windows86 { get; }
+        public override string Linux { get; }
+        public override string MacOS { get; } = MacVulkanSdkGlobalPath;
+    }
+    private static Vk GetApi()
+    {
+        if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || !File.Exists(MacVulkanSdkGlobalPath))
+            return Vk.GetApi();
+        var ctx = new MultiNativeContext(new INativeContext[2]
+        {
+            Vk.CreateDefaultContext(new MacLibraryNameContainer().GetLibraryName()),
+            null!
+        });
+        var ret = new Vk(ctx);
+        ctx.Contexts[1] = new LamdaNativeContext((Func<string, IntPtr>) ((x) =>
+        {
+            if (x.EndsWith("ProcAddr"))
+                return IntPtr.Zero;
+            IntPtr deviceProcAddr = (IntPtr) ret.GetDeviceProcAddr(ret.CurrentDevice.GetValueOrDefault(), x);
+            return deviceProcAddr != IntPtr.Zero ? deviceProcAddr : (IntPtr) ret.GetInstanceProcAddr(ret.CurrentInstance.GetValueOrDefault(), x);
+        }));
+        return ret;
+    }
     
     public void Dispose()
     {
         D3DDevice?.Dispose();
-        GrContext.Dispose();
+        GrContext?.Dispose();
         Pool.Dispose();
         Api.DestroyDescriptorPool(Device, DescriptorPool, null);
         Api.DestroyDevice(Device, null);

+ 111 - 44
samples/GpuInterop/VulkanDemo/VulkanImage.cs

@@ -8,6 +8,7 @@ using Avalonia.Platform;
 using Avalonia.Vulkan;
 using SharpDX.DXGI;
 using Silk.NET.Vulkan;
+using Silk.NET.Vulkan.Extensions.EXT;
 using Silk.NET.Vulkan.Extensions.KHR;
 using SilkNetDemo;
 using SkiaSharp;
@@ -45,6 +46,8 @@ public unsafe class VulkanImage : IDisposable
         public ulong MemorySize { get; }
         public uint CurrentLayout => (uint) _currentLayout;
 
+        private bool _hasIOSurface;
+
         public VulkanImage(VulkanContext vk, uint format, PixelSize size,
             bool exportable, IReadOnlyList<string> supportedHandleTypes)
         {
@@ -75,10 +78,22 @@ public unsafe class VulkanImage : IDisposable
                 SType = StructureType.ExternalMemoryImageCreateInfo,
                 HandleTypes = handleType
             };
+
+            
+            var ioSurfaceCreateInfo = new ExportMetalObjectCreateInfoEXT
+            {
+                SType = StructureType.ExportMetalObjectCreateInfoExt,
+                ExportObjectType = ExportMetalObjectTypeFlagsEXT.IosurfaceBitExt
+            };
+
+            _hasIOSurface = exportable && RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
             
             var imageCreateInfo = new ImageCreateInfo
             {
-                PNext = exportable ? &externalMemoryCreateInfo : null,
+                PNext = exportable ?
+                    RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
+                        &ioSurfaceCreateInfo : 
+                        &externalMemoryCreateInfo : null,
                 SType = StructureType.ImageCreateInfo,
                 ImageType = ImageType.Type2D,
                 Format = Format,
@@ -98,59 +113,64 @@ public unsafe class VulkanImage : IDisposable
             Api
                 .CreateImage(_device, imageCreateInfo, null, out var image).ThrowOnError();
             InternalHandle = image;
-            
-            Api.GetImageMemoryRequirements(_device, InternalHandle,
-                out var memoryRequirements);
 
-            var dedicatedAllocation = new MemoryDedicatedAllocateInfoKHR
-            {
-                SType = StructureType.MemoryDedicatedAllocateInfoKhr,
-                Image = image
-            };
-            
-            var fdExport = new ExportMemoryAllocateInfo
+            if (!exportable || !RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
             {
-                HandleTypes = handleType, SType = StructureType.ExportMemoryAllocateInfo,
-                PNext = &dedicatedAllocation
-            };
 
-            ImportMemoryWin32HandleInfoKHR handleImport = default;
-            if (handleType == ExternalMemoryHandleTypeFlags.D3D11TextureBit && exportable)
-            {
-                var d3dDevice = vk.D3DDevice ?? throw new NotSupportedException("Vulkan D3DDevice wasn't created");
-                _d3dTexture2D = D3DMemoryHelper.CreateMemoryHandle(d3dDevice, size, Format);
-                using var dxgi = _d3dTexture2D.QueryInterface<SharpDX.DXGI.Resource1>();
+                Api.GetImageMemoryRequirements(_device, InternalHandle,
+                    out var memoryRequirements);
 
-                handleImport = new ImportMemoryWin32HandleInfoKHR
+                var dedicatedAllocation = new MemoryDedicatedAllocateInfoKHR
                 {
-                    PNext = &dedicatedAllocation,
-                    SType = StructureType.ImportMemoryWin32HandleInfoKhr,
-                    HandleType = ExternalMemoryHandleTypeFlags.D3D11TextureBit,
-                    Handle = dxgi.CreateSharedHandle(null, SharedResourceFlags.Read | SharedResourceFlags.Write),
+                    SType = StructureType.MemoryDedicatedAllocateInfoKhr, Image = image
                 };
-            }
 
-            var memoryAllocateInfo = new MemoryAllocateInfo
-            {
-                PNext =
-                    exportable ? handleImport.Handle != IntPtr.Zero  ? &handleImport : &fdExport : null,
-                SType = StructureType.MemoryAllocateInfo,
-                AllocationSize = memoryRequirements.Size,
-                MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex(
-                    Api,
-                    _physicalDevice,
-                    memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.DeviceLocalBit)
-            };
+                var fdExport = new ExportMemoryAllocateInfo
+                {
+                    HandleTypes = handleType,
+                    SType = StructureType.ExportMemoryAllocateInfo,
+                    PNext = &dedicatedAllocation
+                };
 
-            Api.AllocateMemory(_device, memoryAllocateInfo, null,
-                out var imageMemory).ThrowOnError();
+                ImportMemoryWin32HandleInfoKHR handleImport = default;
+                if (handleType == ExternalMemoryHandleTypeFlags.D3D11TextureBit && exportable)
+                {
+                    var d3dDevice = vk.D3DDevice ?? throw new NotSupportedException("Vulkan D3DDevice wasn't created");
+                    _d3dTexture2D = D3DMemoryHelper.CreateMemoryHandle(d3dDevice, size, Format);
+                    using var dxgi = _d3dTexture2D.QueryInterface<SharpDX.DXGI.Resource1>();
 
-            _imageMemory = imageMemory;
-            
-            
-            MemorySize = memoryRequirements.Size;
+                    handleImport = new ImportMemoryWin32HandleInfoKHR
+                    {
+                        PNext = &dedicatedAllocation,
+                        SType = StructureType.ImportMemoryWin32HandleInfoKhr,
+                        HandleType = ExternalMemoryHandleTypeFlags.D3D11TextureBit,
+                        Handle = dxgi.CreateSharedHandle(null, SharedResourceFlags.Read | SharedResourceFlags.Write),
+                    };
+                }
+
+                var memoryAllocateInfo = new MemoryAllocateInfo
+                {
+                    PNext =
+                        exportable ? handleImport.Handle != IntPtr.Zero ? &handleImport : &fdExport : null,
+                    SType = StructureType.MemoryAllocateInfo,
+                    AllocationSize = memoryRequirements.Size,
+                    MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex(
+                        Api,
+                        _physicalDevice,
+                        memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.DeviceLocalBit)
+                };
+
+                Api.AllocateMemory(_device, memoryAllocateInfo, null,
+                    out var imageMemory).ThrowOnError();
+
+                _imageMemory = imageMemory;
+
+
+                MemorySize = memoryRequirements.Size;
+
+                Api.BindImageMemory(_device, InternalHandle, _imageMemory, 0).ThrowOnError();
+            }
 
-            Api.BindImageMemory(_device, InternalHandle, _imageMemory, 0).ThrowOnError();
             var componentMapping = new ComponentMapping(
                 ComponentSwizzle.Identity,
                 ComponentSwizzle.Identity,
@@ -209,6 +229,26 @@ public unsafe class VulkanImage : IDisposable
             ext.GetMemoryWin32Handle(_device, info, out var fd).ThrowOnError();
             return fd;
         }
+
+        public IntPtr ExportIOSurface()
+        {
+            if (!Api.TryGetDeviceExtension<ExtMetalObjects>(_instance, _device, out var ext))
+                throw new InvalidOperationException();
+            var surfaceExport = new ExportMetalIOSurfaceInfoEXT
+            {
+                SType = StructureType.ExportMetalIOSurfaceInfoExt,
+                Image = InternalHandle
+            };
+            var export = new ExportMetalObjectsInfoEXT()
+            {
+                SType = StructureType.ExportMetalObjectsInfoExt,
+                PNext = &surfaceExport
+            };
+            ext.ExportMetalObjects(_device, ref export);
+            if (surfaceExport.IoSurface == IntPtr.Zero)
+                throw new Exception("Unable to export IOSurfaceRef");
+            return surfaceExport.IoSurface;
+        }
         
         public IPlatformHandle Export()
         {
@@ -225,6 +265,9 @@ public unsafe class VulkanImage : IDisposable
                 return new PlatformHandle(ExportOpaqueNtHandle(),
                     KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaqueNtHandle);
             }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+                return new PlatformHandle(ExportIOSurface(),
+                    KnownPlatformGraphicsExternalImageHandleTypes.IOSurfaceRef);
             else
                 return new PlatformHandle(new IntPtr(ExportFd()),
                     KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor);
@@ -281,6 +324,30 @@ public unsafe class VulkanImage : IDisposable
 
         public void SaveTexture(string path)
         {
+            if (_vk.GrContext == null)
+            {
+                if (_hasIOSurface)
+                {
+                    var surf = ExportIOSurface();
+                    if (NativeMethods.IOSurfaceLock(surf, 0, IntPtr.Zero) != 0)
+                        throw new Exception("IOSurfaceLock failed");
+                    var w = (int)NativeMethods.IOSurfaceGetWidth(surf);
+                    var h = (int)NativeMethods.IOSurfaceGetHeight(surf);
+                    var sstride = NativeMethods.IOSurfaceGetBytesPerRow(surf);
+
+                    var pSurface = NativeMethods.IOSurfaceGetBaseAddress(surf);
+                    using var b = new Avalonia.Media.Imaging.Bitmap(PixelFormat.Bgra8888,
+                        AlphaFormat.Premul, pSurface, new PixelSize(w, h),
+                        new Vector(96, 96), (int)sstride);
+                    b.Save(path);
+
+                    NativeMethods.IOSurfaceUnlock(surf, 0, IntPtr.Zero);
+                    return;
+                }
+                else
+                    throw new NotSupportedException("Need skia to dump textures, sorry");
+            }
+
             _vk.GrContext.ResetContext();
             var _image = this;
             var imageInfo = new GRVkImageInfo()

+ 59 - 10
samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs

@@ -39,8 +39,10 @@ class VulkanSwapchainImage : ISwapchainImage
     private readonly ICompositionGpuInterop _interop;
     private readonly CompositionDrawingSurface _target;
     private readonly VulkanImage _image;
-    private readonly VulkanSemaphorePair _semaphorePair;
-    private ICompositionImportedGpuSemaphore? _availableSemaphore, _renderCompletedSemaphore;
+    private readonly VulkanSemaphorePair? _semaphorePair;
+    private readonly VulkanTimelineSemaphore? _timelineSemaphore;
+    private ulong _timelineCounter;
+    private ICompositionImportedGpuSemaphore? _availableSemaphore, _renderCompletedSemaphore, _importedTimelineSemaphore;
     private ICompositionImportedGpuImage? _importedImage;
     private Task? _lastPresent;
     public VulkanImage Image => _image;
@@ -52,8 +54,12 @@ class VulkanSwapchainImage : ISwapchainImage
         _interop = interop;
         _target = target;
         Size = size;
-        _image = new VulkanImage(vk, (uint)Format.R8G8B8A8Unorm, size, true, interop.SupportedImageHandleTypes);
-        _semaphorePair = new VulkanSemaphorePair(vk, true);
+        var format = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? Format.B8G8R8A8Unorm : Format.R8G8B8A8Unorm;
+        _image = new VulkanImage(vk, (uint)format, size, true, interop.SupportedImageHandleTypes);
+        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            _timelineSemaphore = new(vk);
+        else
+            _semaphorePair = new VulkanSemaphorePair(vk, true);
     }
 
     public async ValueTask DisposeAsync()
@@ -62,11 +68,13 @@ class VulkanSwapchainImage : ISwapchainImage
             await LastPresent;
         if (_importedImage != null)
             await _importedImage.DisposeAsync();
+        
         if (_availableSemaphore != null)
             await _availableSemaphore.DisposeAsync();
         if (_renderCompletedSemaphore != null)
             await _renderCompletedSemaphore.DisposeAsync();
-        _semaphorePair.Dispose();
+        _semaphorePair?.Dispose();
+        _timelineSemaphore?.Dispose();
         _image.Dispose();
     }
 
@@ -89,6 +97,22 @@ class VulkanSwapchainImage : ISwapchainImage
                 AcquireKey = 0,
                 DeviceMemory = _image.DeviceMemory
             });
+        else if (_timelineSemaphore != null)
+        {
+            unsafe
+            {
+                var wait = _timelineCounter;
+                var submitInfo = new TimelineSemaphoreSubmitInfo
+                {
+                    PWaitSemaphoreValues = &wait,
+                    WaitSemaphoreValueCount = 1,
+                    SType = StructureType.TimelineSemaphoreSubmitInfo
+                };
+                var waitSemaphores = new[] { _timelineSemaphore.Handle };
+
+                buffer.Submit(waitSemaphores, pNext: (IntPtr)(&submitInfo));
+            }
+        }
         else if (_initial)
         {
             _initial = false;
@@ -110,7 +134,7 @@ class VulkanSwapchainImage : ISwapchainImage
         buffer.BeginRecording();
         _image.TransitionLayout(buffer.InternalHandle, ImageLayout.TransferSrcOptimal, AccessFlags.TransferWriteBit);
 
-        
+
         if (_image.IsDirectXBacked)
         {
             buffer.Submit(null, null, null, null,
@@ -119,10 +143,30 @@ class VulkanSwapchainImage : ISwapchainImage
                     DeviceMemory = _image.DeviceMemory, ReleaseKey = 1
                 });
         }
+        else if (_timelineSemaphore != null)
+        {
+            unsafe
+            {
+                var signal = _timelineCounter + 1;
+                var submitInfo = new TimelineSemaphoreSubmitInfo
+                {
+                    PSignalSemaphoreValues = &signal,
+                    SignalSemaphoreValueCount = 1,
+                    SType = StructureType.TimelineSemaphoreSubmitInfo
+                };
+                var signalSemaphores = new[] { _timelineSemaphore.Handle };
+
+                buffer.Submit(default, signalSemaphores: signalSemaphores, pNext: (IntPtr)(&submitInfo));
+            }
+        }
         else
             buffer.Submit(null, null, new[] { _semaphorePair.RenderFinishedSemaphore });
 
-        if (!_image.IsDirectXBacked)
+        if (_timelineSemaphore != null)
+        {
+            _importedTimelineSemaphore ??= _interop.ImportSemaphore(_timelineSemaphore.Export());
+        }
+        else if (!_image.IsDirectXBacked)
         {
             _availableSemaphore ??= _interop.ImportSemaphore(_semaphorePair.Export(false));
             
@@ -132,13 +176,18 @@ class VulkanSwapchainImage : ISwapchainImage
         _importedImage ??= _interop.ImportImage(_image.Export(),
             new PlatformGraphicsExternalImageProperties
             {
-                Format = PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm,
+                Format = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm : PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm,
                 Width = Size.Width,
                 Height = Size.Height,
                 MemorySize = _image.MemorySize
             });
-
-        if (_image.IsDirectXBacked)
+        if (_importedTimelineSemaphore != null)
+        {
+            _lastPresent = _target.UpdateWithTimelineSemaphoresAsync(_importedImage,
+                _importedTimelineSemaphore, _timelineCounter + 1, _importedTimelineSemaphore, _timelineCounter + 2);
+            _timelineCounter += 2;
+        }
+        else if (_image.IsDirectXBacked)
             _lastPresent = _target.UpdateWithKeyedMutexAsync(_importedImage, 1, 0);
         else
             _lastPresent = _target.UpdateWithSemaphoresAsync(_importedImage, _renderCompletedSemaphore!, _availableSemaphore!);

+ 72 - 0
samples/GpuInterop/VulkanDemo/VulkanTimelineSemaphore.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Platform;
+using Silk.NET.Vulkan;
+using Silk.NET.Vulkan.Extensions.EXT;
+using SilkNetDemo;
+
+namespace GpuInterop.VulkanDemo;
+
+class VulkanTimelineSemaphore : IDisposable
+{
+    private VulkanContext _resources;
+
+    public unsafe VulkanTimelineSemaphore(VulkanContext resources)
+    {
+        _resources = resources;
+        var mtlEvent = new ExportMetalObjectCreateInfoEXT
+        {
+            SType = StructureType.ExportMetalObjectCreateInfoExt,
+            ExportObjectType = ExportMetalObjectTypeFlagsEXT.SharedEventBitExt
+        };
+        
+        var semaphoreTypeInfo = new SemaphoreTypeCreateInfoKHR()
+        {
+            SType = StructureType.SemaphoreTypeCreateInfo,
+            SemaphoreType = SemaphoreType.Timeline,
+            PNext = &mtlEvent
+        };
+
+        var semaphoreCreateInfo = new SemaphoreCreateInfo
+        {
+            SType = StructureType.SemaphoreCreateInfo,
+            PNext = &semaphoreTypeInfo,
+        };
+        resources.Api.CreateSemaphore(resources.Device, semaphoreCreateInfo, null, out var semaphore).ThrowOnError();
+        Handle = semaphore;
+    }
+
+    public Semaphore Handle { get; }
+    public unsafe void Dispose()
+    {
+        _resources.Api.DestroySemaphore(_resources.Device, Handle, null);
+    }
+
+    
+    public unsafe IntPtr ExportSharedEvent()
+    {
+        if (!_resources.Api.TryGetDeviceExtension<ExtMetalObjects>(_resources.Instance, _resources.Device, out var ext))
+            throw new InvalidOperationException();
+        var eventExport = new ExportMetalSharedEventInfoEXT()
+        {
+            SType = StructureType.ExportMetalSharedEventInfoExt,
+            Semaphore = Handle,
+        };
+        var export = new ExportMetalObjectsInfoEXT()
+        {
+            SType = StructureType.ExportMetalObjectsInfoExt,
+            PNext = &eventExport
+        };
+        ext.ExportMetalObjects(_resources.Device, ref export);
+        if (eventExport.MtlSharedEvent == IntPtr.Zero)
+            throw new Exception("Unable to export IOSurfaceRef");
+        return eventExport.MtlSharedEvent;
+    }
+    public IPlatformHandle Export()
+    {
+        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            return new PlatformHandle(ExportSharedEvent(),
+                KnownPlatformGraphicsExternalSemaphoreHandleTypes.MetalSharedEvent);
+        throw new PlatformNotSupportedException();
+    }
+}

+ 34 - 0
src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs

@@ -24,11 +24,41 @@ public interface IExternalObjectsRenderInterfaceContextFeature
     IPlatformRenderInterfaceImportedImage ImportImage(ICompositionImportableSharedGpuContextImage image);
 
     IPlatformRenderInterfaceImportedSemaphore ImportSemaphore(IPlatformHandle handle);
+    
     CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType);
     public byte[]? DeviceUuid { get; }
     public byte[]? DeviceLuid { get; }
 }
 
+/// <summary>
+/// This interface allows proper management of ref-counted platform handles.
+/// If we immediately wrap the handle, the caller can destroy its copy immediately after the call
+/// This is needed for MoltenVK-based users that can e.g. get an MTLSharedEvent from a VkSemaphore.
+/// This does NOT actually increase the ref-counter of MTLSharedEvent, since it's declared as
+/// __unsafe_unretained in vulkan headers.
+/// Same happens with exporting an IOSurfaceRef from a VkImage.
+/// So in a case when the VkSemaphore or VkImage is destroyed, the "handle" which is actually a pointer
+/// will be pointing to a dead object.
+/// To prevent this we need to increase the reference counter in a handle-specific means
+/// synchronously before returning control back to the user.
+///
+/// This is not needed for fds or DXGI handles, since those are _created_ on demand as proper NT handles
+/// </summary>
+[Unstable, NotClientImplementable]
+public interface IExternalObjectsHandleWrapRenderInterfaceContextFeature
+{
+    IExternalObjectsWrappedGpuHandle? WrapImageHandleOnAnyThread(IPlatformHandle handle,
+        PlatformGraphicsExternalImageProperties properties);
+    IExternalObjectsWrappedGpuHandle? WrapSemaphoreHandleOnAnyThread(IPlatformHandle handle);
+
+}
+
+[Unstable, NotClientImplementable]
+public interface IExternalObjectsWrappedGpuHandle : IPlatformHandle, IDisposable
+{
+    
+}
+
 [Unstable]
 public interface IPlatformRenderInterfaceImportedObject : IDisposable
 {
@@ -43,6 +73,10 @@ public interface IPlatformRenderInterfaceImportedImage : IPlatformRenderInterfac
     IBitmapImpl SnapshotWithSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore,
         IPlatformRenderInterfaceImportedSemaphore signalSemaphore);
 
+    IBitmapImpl SnapshotWithTimelineSemaphores(
+        IPlatformRenderInterfaceImportedSemaphore waitForSemaphore, ulong waitForValue,
+        IPlatformRenderInterfaceImportedSemaphore signalSemaphore, ulong signalValue);
+
     IBitmapImpl SnapshotWithAutomaticSync();
 }
 

+ 10 - 0
src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs

@@ -43,6 +43,11 @@ public static class KnownPlatformGraphicsExternalImageHandleTypes
     
     // A global shared handle that's been exported by Vulkan using VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT or in a compatible way
     public const string VulkanOpaqueKmtHandle = nameof(VulkanOpaqueKmtHandle);
+
+    /// <summary>
+    /// A reference to IOSurface
+    /// </summary>
+    public const string IOSurfaceRef = nameof(IOSurfaceRef);
 }
 
 /// <summary>
@@ -65,4 +70,9 @@ public static class KnownPlatformGraphicsExternalSemaphoreHandleTypes
     
     /// A DXGI NT handle returned by ID3D12Device::CreateSharedHandle or ID3D11Fence::CreateSharedHandle
     public const string Direct3D12FenceNtHandle = nameof(Direct3D12FenceNtHandle);
+    
+    /// <summary>
+    /// A pointer to MTLSharedEvent object
+    /// </summary>
+    public const string MetalSharedEvent = nameof(MetalSharedEvent);
 }

+ 17 - 0
src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs

@@ -41,6 +41,23 @@ public sealed class CompositionDrawingSurface : CompositionSurface, IDisposable
         var signal = (CompositionImportedGpuSemaphore)signalSemaphore;
         return Compositor.InvokeServerJobAsync(() => Server.UpdateWithSemaphores(img, wait, signal));
     }
+    
+    /// <summary>
+    /// Updates the surface contents using an imported memory image using a semaphore pair as the means of synchronization
+    /// </summary>
+    /// <param name="image">GPU image with new surface contents</param>
+    /// <param name="waitForSemaphore">The semaphore to wait for before accessing the image</param>
+    /// <param name="signalSemaphore">The semaphore to signal after accessing the image</param>
+    /// <returns>A task that completes when update operation is completed and user code is free to destroy or dispose the image</returns>
+    public Task UpdateWithTimelineSemaphoresAsync(ICompositionImportedGpuImage image,
+        ICompositionImportedGpuSemaphore waitForSemaphore, ulong waitForValue,
+        ICompositionImportedGpuSemaphore signalSemaphore, ulong signalValue)
+    {
+        var img = (CompositionImportedGpuImage)image;
+        var wait = (CompositionImportedGpuSemaphore)waitForSemaphore;
+        var signal = (CompositionImportedGpuSemaphore)signalSemaphore;
+        return Compositor.InvokeServerJobAsync(() => Server.UpdateWithTimelineSemaphores(img, wait, waitForValue, signal, signalValue));
+    }
 
     /// <summary>
     /// Updates the surface contents using an unspecified automatic means of synchronization

+ 5 - 1
src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs

@@ -81,7 +81,11 @@ public enum CompositionGpuImportedImageSynchronizationCapabilities
     /// <summary>
     /// Synchronization and ordering is somehow handled by the underlying platform
     /// </summary>
-    Automatic = 4
+    Automatic = 4,
+    /// <summary>
+    /// Pre-render and after-render timeline semaphores must be provided alongside with the image
+    /// </summary>
+    TimelineSemaphores = 8
 }
 
 /// <summary>

+ 21 - 9
src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs

@@ -10,7 +10,8 @@ internal class CompositionInterop : ICompositionGpuInterop
     private readonly Compositor _compositor;
     private readonly IPlatformRenderInterfaceContext _context;
     private readonly IExternalObjectsRenderInterfaceContextFeature _externalObjects;
-    
+    private readonly IExternalObjectsHandleWrapRenderInterfaceContextFeature? _externalObjectsWithHandleWrap;
+
 
     public CompositionInterop(
         Compositor compositor,
@@ -21,6 +22,7 @@ internal class CompositionInterop : ICompositionGpuInterop
         DeviceLuid = externalObjects.DeviceLuid;
         DeviceUuid = externalObjects.DeviceUuid;
         _externalObjects = externalObjects;
+        _externalObjectsWithHandleWrap = _context.TryGetFeature<IExternalObjectsHandleWrapRenderInterfaceContextFeature>();
     }
 
     public IReadOnlyList<string> SupportedImageHandleTypes => _externalObjects.SupportedImageHandleTypes;
@@ -31,17 +33,23 @@ internal class CompositionInterop : ICompositionGpuInterop
 
     public ICompositionImportedGpuImage ImportImage(IPlatformHandle handle,
         PlatformGraphicsExternalImageProperties properties)
-        => new CompositionImportedGpuImage(_compositor, _context, _externalObjects, 
-            () => _externalObjects.ImportImage(handle, properties));
+    {
+        handle = _externalObjectsWithHandleWrap?.WrapImageHandleOnAnyThread(handle, properties) ?? handle;
+        return new CompositionImportedGpuImage(_compositor, _context, _externalObjects,
+            () => _externalObjects.ImportImage(handle, properties), handle);
+    }
 
     public ICompositionImportedGpuImage ImportImage(ICompositionImportableSharedGpuContextImage image)
     {
         return new CompositionImportedGpuImage(_compositor, _context, _externalObjects,
-            () => _externalObjects.ImportImage(image));
+            () => _externalObjects.ImportImage(image), null);
     }
 
     public ICompositionImportedGpuSemaphore ImportSemaphore(IPlatformHandle handle)
-        => new CompositionImportedGpuSemaphore(handle, _compositor, _context, _externalObjects);
+    {
+        handle = _externalObjectsWithHandleWrap?.WrapSemaphoreHandleOnAnyThread(handle) ?? handle;
+        return new CompositionImportedGpuSemaphore(handle, _compositor, _context, _externalObjects);
+    }
 
     public ICompositionImportedGpuImage ImportSemaphore(ICompositionImportableSharedGpuContextSemaphore image)
     {
@@ -61,13 +69,17 @@ abstract class CompositionGpuImportedObjectBase : ICompositionGpuImportedObject
 
     public CompositionGpuImportedObjectBase(Compositor compositor,
         IPlatformRenderInterfaceContext context,
-        IExternalObjectsRenderInterfaceContextFeature feature)
+        IExternalObjectsRenderInterfaceContextFeature feature, IPlatformHandle? handle)
     {
         Compositor = compositor;
         Context = context;
         Feature = feature;
         
-        ImportCompleted = Compositor.InvokeServerJobAsync(Import);
+        ImportCompleted = Compositor.InvokeServerJobAsync(() =>
+        {
+            using var _ = handle as IExternalObjectsWrappedGpuHandle;
+            Import();
+        });
     }
     
     protected abstract void Import();
@@ -93,7 +105,7 @@ class CompositionImportedGpuImage : CompositionGpuImportedObjectBase, ICompositi
     public CompositionImportedGpuImage(Compositor compositor,
         IPlatformRenderInterfaceContext context,
         IExternalObjectsRenderInterfaceContextFeature feature,
-        Func<IPlatformRenderInterfaceImportedImage> importer): base(compositor, context, feature)
+        Func<IPlatformRenderInterfaceImportedImage> importer, IPlatformHandle? handle): base(compositor, context, feature, handle)
     {
         _importer = importer;
     }
@@ -128,7 +140,7 @@ class CompositionImportedGpuSemaphore : CompositionGpuImportedObjectBase, ICompo
 
     public CompositionImportedGpuSemaphore(IPlatformHandle handle,
         Compositor compositor, IPlatformRenderInterfaceContext context,
-        IExternalObjectsRenderInterfaceContextFeature feature) : base(compositor, context, feature)
+        IExternalObjectsRenderInterfaceContextFeature feature) : base(compositor, context, feature, handle)
     {
         _handle = handle;
     }

+ 13 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs

@@ -75,6 +75,19 @@ internal class ServerCompositionDrawingSurface : ServerCompositionSurface, IDisp
             Update(image.Image.SnapshotWithSemaphores(wait.Semaphore, signal.Semaphore), image.Context);
         }
     }
+    
+    public void UpdateWithTimelineSemaphores(CompositionImportedGpuImage image, 
+        CompositionImportedGpuSemaphore wait, ulong waitForValue,
+        CompositionImportedGpuSemaphore signal, ulong signalValue)
+    {
+        using (Compositor.RenderInterface.EnsureCurrent())
+        {
+            PerformSanityChecks(image);
+            if (!wait.IsUsable || !signal.IsUsable)
+                throw new PlatformGraphicsContextLostException();
+            Update(image.Image.SnapshotWithTimelineSemaphores(wait.Semaphore, waitForValue, signal.Semaphore, signalValue), image.Context);
+        }
+    }
 
     public void Dispose()
     {

+ 36 - 0
src/Avalonia.Metal/IMetalExternalObjectsFeature.cs

@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Metadata;
+using Avalonia.Platform;
+using Avalonia.Rendering.Composition;
+
+namespace Avalonia.Metal;
+
+[PrivateApi]
+public interface IMetalExternalObjectsFeature
+{
+    IReadOnlyList<string> SupportedImageHandleTypes { get; }
+    IReadOnlyList<string> SupportedSemaphoreTypes { get; }
+    byte[]? DeviceLuid { get; }
+    CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType);
+    IMetalExternalTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties);
+    IMetalSharedEvent ImportSharedEvent(IPlatformHandle handle);
+
+    void SubmitWait(IMetalSharedEvent @event, ulong waitForValue);
+    void SubmitSignal(IMetalSharedEvent @event, ulong signalValue);
+}
+
+[PrivateApi]
+public interface IMetalExternalTexture : IDisposable
+{
+    int Width { get; }
+    int Height { get; }
+    int Samples { get; }
+    IntPtr Handle { get; }
+}
+
+[PrivateApi]
+public interface IMetalSharedEvent : IDisposable
+{
+    IntPtr Handle { get; }
+}

+ 154 - 5
src/Avalonia.Native/AvaloniaNativeGlPlatformGraphics.cs

@@ -1,10 +1,11 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using Avalonia.OpenGL;
 using Avalonia.Native.Interop;
-using System.Drawing;
 using Avalonia.OpenGL.Surfaces;
 using Avalonia.Platform;
+using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
 
 namespace Avalonia.Native
@@ -13,7 +14,7 @@ namespace Avalonia.Native
     {
         private readonly IAvnGlDisplay _display;
 
-        public AvaloniaNativeGlPlatformGraphics(IAvnGlDisplay display)
+        public AvaloniaNativeGlPlatformGraphics(IAvnGlDisplay display, IAvaloniaNativeFactory factory)
         {
             _display = display;
             var context = display.CreateContext(null);
@@ -36,7 +37,7 @@ namespace Avalonia.Native
                 });
             }
 
-            GlDisplay = new GlDisplay(display, glInterface, context.SampleCount, context.StencilSize);
+            GlDisplay = new GlDisplay(display, glInterface, context.SampleCount, context.StencilSize, factory);
             SharedContext =(GlContext)CreateContext();
         }
 
@@ -59,12 +60,15 @@ namespace Avalonia.Native
     {
         private readonly IAvnGlDisplay _display;
 
-        public GlDisplay(IAvnGlDisplay display, GlInterface glInterface, int sampleCount, int stencilSize)
+        public GlDisplay(IAvnGlDisplay display, GlInterface glInterface, int sampleCount, int stencilSize,
+            IAvaloniaNativeFactory factory)
         {
             _display = display;
             SampleCount = sampleCount;
             StencilSize = stencilSize;
+            Factory = factory;
             GlInterface = glInterface;
+            MemoryHelper = factory.CreateMemoryManagementHelper();
         }
 
         public GlInterface GlInterface { get; }
@@ -72,6 +76,9 @@ namespace Avalonia.Native
         public int SampleCount { get; }
 
         public int StencilSize { get; }
+        public IAvaloniaNativeFactory Factory { get; }
+
+        public IAvnNativeObjectsMemoryManagement MemoryHelper { get; }
 
         public void ClearContext() => _display.LegacyClearCurrentContext();
 
@@ -84,6 +91,8 @@ namespace Avalonia.Native
     {
         private readonly GlDisplay _display;
         private readonly GlContext _sharedWith;
+        private readonly GpuHandleWrapFeature _handleWrapFeature;
+        private readonly GlExternalObjectsFeature _externalObjects;
         public IAvnGlContext Context { get; private set; }
 
         public GlContext(GlDisplay display, GlContext sharedWith, IAvnGlContext context, GlVersion version)
@@ -92,6 +101,8 @@ namespace Avalonia.Native
             _sharedWith = sharedWith;
             Context = context;
             Version = version;
+            _handleWrapFeature = new GpuHandleWrapFeature(display.Factory);
+            _externalObjects = new GlExternalObjectsFeature(this, display);
         }
 
         public GlVersion Version { get; }
@@ -128,7 +139,14 @@ namespace Avalonia.Native
             Context = null;
         }
 
-        public object TryGetFeature(Type featureType) => null;
+        public object TryGetFeature(Type featureType)
+        {
+            if (featureType == typeof(IExternalObjectsHandleWrapRenderInterfaceContextFeature))
+                return _handleWrapFeature;
+            if (featureType == typeof(IGlContextExternalObjectsFeature))
+                return _externalObjects;
+            return null;
+        }
     }
 
 
@@ -203,6 +221,137 @@ namespace Avalonia.Native
             var avnContext = (GlContext)context;
             return new GlPlatformSurfaceRenderTarget(_topLevel.CreateGlRenderTarget(avnContext.Context), avnContext);
         }
+    }
+
+    class GlExternalObjectsFeature : IGlContextExternalObjectsFeature
+    {
+        private readonly GlContext _context;
+        private readonly GlDisplay _display;
+
+        public unsafe GlExternalObjectsFeature(GlContext context, GlDisplay display)
+        {
+            _context = context;
+            _display = display;
+            ulong registryId = 0;
+            if (context.Context.GetIOKitRegistryId(&registryId) != 0)
+            {
+                // We are reversing bytes to match MoltenVK (LUID is a Vulkan term after all)
+                DeviceLuid = BitConverter.GetBytes(registryId).Reverse().ToArray();
+            }
+        }
+
+        public IReadOnlyList<string> SupportedImportableExternalImageTypes { get; } =
+            [KnownPlatformGraphicsExternalImageHandleTypes.IOSurfaceRef];
+        public IReadOnlyList<string> SupportedExportableExternalImageTypes { get; } = [];
+
+        public IReadOnlyList<string> SupportedImportableExternalSemaphoreTypes { get; } =
+            [KnownPlatformGraphicsExternalSemaphoreHandleTypes.MetalSharedEvent];
+
+        public IReadOnlyList<string> SupportedExportableExternalSemaphoreTypes { get; } = [];
+        public IReadOnlyList<PlatformGraphicsExternalImageFormat> GetSupportedFormatsForExternalMemoryType(string type)
+        {
+            return [PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm];
+        }
+
+        public IGlExportableExternalImageTexture CreateImage(string type, PixelSize size, PlatformGraphicsExternalImageFormat format) => 
+            throw new NotSupportedException();
+
+        public IGlExportableExternalImageTexture CreateSemaphore(string type) => throw new NotSupportedException();
+
+        public IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties)
+        {
+            if (handle.HandleDescriptor == KnownPlatformGraphicsExternalImageHandleTypes.IOSurfaceRef)
+            {
+                if (properties.Format != PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm)
+                    throw new OpenGlException("Only B8G8R8A8UNorm format is supported for IOSurfaceRef");
+                using (_context.EnsureCurrent())
+                {
+                    _context.GlInterface.GetIntegerv(GlConsts.GL_TEXTURE_BINDING_RECTANGLE, out var oldTexture);
+                    var textureId = _context.GlInterface.GenTexture();
+                    _context.GlInterface.BindTexture(GlConsts.GL_TEXTURE_RECTANGLE, textureId);
+                    var error = _context.Context.texImageIOSurface2D(GlConsts.GL_TEXTURE_RECTANGLE, GlConsts.GL_RGBA8,
+                        properties.Width, properties.Height, GlConsts.GL_BGRA, GlConsts.GL_UNSIGNED_INT_8_8_8_8_REV,
+                        handle.Handle, 0);
+                    //var error = 0;
+                    _context.GlInterface.BindTexture(GlConsts.GL_TEXTURE_RECTANGLE, oldTexture);
+                    
+                    if(error != 0)
+                    {
+                        _context.GlInterface.DeleteTexture(textureId);
+                        throw new OpenGlException("CGLTexImageIOSurface2D returned " + error);
+                    }
+                    return new ImportedTexture(_context, GlConsts.GL_TEXTURE_RECTANGLE, textureId,
+                        GlConsts.GL_RGBA8, properties);
+                }
+            }
+            throw new NotSupportedException("This handle type is not supported");
+        }
+
+        public IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle)
+        {
+            if (handle.HandleDescriptor == KnownPlatformGraphicsExternalSemaphoreHandleTypes.MetalSharedEvent)
+            {
+                var imported = _display.Factory.ImportMTLSharedEvent(handle.Handle);
+                return new MtlEventSemaphore(imported);
+            }
+
+            throw new NotSupportedException("This handle type is not supported");
+        }
+
+        public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType)
+        {
+            return CompositionGpuImportedImageSynchronizationCapabilities.Automatic |
+                   CompositionGpuImportedImageSynchronizationCapabilities.TimelineSemaphores;
+        }
+
+        public byte[] DeviceLuid { get; }
+        public byte[] DeviceUuid { get; }
+    }
+
+    class MtlEventSemaphore(IAvnMTLSharedEvent inner) : IGlExternalSemaphore
+    {
+        public void WaitSemaphore(IGlExternalImageTexture texture) =>
+            throw new NotSupportedException("This is a timeline semaphore");
+
+        public void SignalSemaphore(IGlExternalImageTexture texture) => 
+            throw new NotSupportedException("This is a timeline semaphore");
+
+        public void WaitTimelineSemaphore(IGlExternalImageTexture texture, ulong value) =>
+            inner.Wait(value, 1000);
+
+        public void SignalTimelineSemaphore(IGlExternalImageTexture texture, ulong value) =>
+            inner.SetSignaledValue(value);
+
+        public void Dispose() => inner.Dispose();
+    }
+    
+    class ImportedTexture(GlContext context, int type, int id, int internalFormat,
+        PlatformGraphicsExternalImageProperties properties) : IGlExternalImageTexture
+    {
+        private bool _disposed;
+        public void Dispose()
+        {
+            if(_disposed)
+                return;
+            try
+            {
+                _disposed = true;
+                using (context.EnsureCurrent()) 
+                    context.GlInterface.DeleteTexture(id);
+            }
+            catch
+            {
+                // Ignore, context is likely broken
+            }
+        }
+
+        public void AcquireKeyedMutex(uint key) => throw new NotSupportedException();
+
+        public void ReleaseKeyedMutex(uint key) => throw new NotSupportedException();
 
+        public int TextureId => id;
+        public int InternalFormat => internalFormat;
+        public int TextureType => type;
+        public PlatformGraphicsExternalImageProperties Properties => properties;
     }
 }

+ 1 - 1
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -149,7 +149,7 @@ namespace Avalonia.Native
                 {
                     try
                     {
-                        _platformGraphics = new AvaloniaNativeGlPlatformGraphics(_factory.ObtainGlDisplay());
+                        _platformGraphics = new AvaloniaNativeGlPlatformGraphics(_factory.ObtainGlDisplay(), _factory);
                         break;
                     }
                     catch (Exception)

+ 55 - 0
src/Avalonia.Native/GpuHandleWrapFeature.cs

@@ -0,0 +1,55 @@
+using System;
+using Avalonia.Native.Interop;
+using Avalonia.Platform;
+
+namespace Avalonia.Native;
+
+class GpuHandleWrapFeature : IExternalObjectsHandleWrapRenderInterfaceContextFeature
+{
+    private readonly IAvnNativeObjectsMemoryManagement _helper;
+
+    public GpuHandleWrapFeature(IAvaloniaNativeFactory factory)
+    {
+        _helper = factory.CreateMemoryManagementHelper();
+    }
+    public IExternalObjectsWrappedGpuHandle? WrapImageHandleOnAnyThread(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties)
+    {
+        if (handle.HandleDescriptor == KnownPlatformGraphicsExternalImageHandleTypes.IOSurfaceRef)
+        {
+            _helper.RetainCFObject(handle.Handle);
+            return new CFObjectWrapper(_helper, handle.Handle, handle.HandleDescriptor);
+        }
+
+        return null;
+    }
+
+    public IExternalObjectsWrappedGpuHandle? WrapSemaphoreHandleOnAnyThread(IPlatformHandle handle)
+    {
+        if (handle.HandleDescriptor == KnownPlatformGraphicsExternalSemaphoreHandleTypes.MetalSharedEvent)
+        {
+            _helper.RetainNSObject(handle.Handle);
+            return new NSObjectWrapper(_helper, handle.Handle, handle.HandleDescriptor);
+        }
+
+        return null;
+    }
+
+    class NSObjectWrapper(IAvnNativeObjectsMemoryManagement helper, IntPtr handle, string descriptor) : IExternalObjectsWrappedGpuHandle
+    {
+        public void Dispose() => helper.ReleaseNSObject(handle);
+
+        public IntPtr Handle => handle;
+        public string HandleDescriptor => descriptor;
+    }
+    
+    class CFObjectWrapper(IAvnNativeObjectsMemoryManagement helper, IntPtr handle, string descriptor) : IExternalObjectsWrappedGpuHandle
+    {
+        public void Dispose()
+        {
+            helper.ReleaseCFObject(handle);
+        }
+
+        public IntPtr Handle => handle;
+        public string HandleDescriptor => descriptor;
+    }
+}

+ 99 - 4
src/Avalonia.Native/Metal.cs

@@ -1,7 +1,10 @@
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using Avalonia.Metal;
 using Avalonia.Native.Interop;
 using Avalonia.Platform;
+using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
 using Avalonia.Utilities;
 
@@ -9,14 +12,16 @@ namespace Avalonia.Native;
 
 class MetalPlatformGraphics : IPlatformGraphics
 {
+    private readonly IAvaloniaNativeFactory _factory;
     private readonly IAvnMetalDisplay _display;
 
     public MetalPlatformGraphics(IAvaloniaNativeFactory factory)
     {
+        _factory = factory;
         _display = factory.ObtainMetalDisplay();
     }
     public bool UsesSharedContext => false;
-    public IPlatformGraphicsContext CreateContext() => new MetalDevice(_display.CreateDevice());
+    public IPlatformGraphicsContext CreateContext() => new MetalDevice(_factory, _display.CreateDevice());
 
     public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException();
 }
@@ -25,11 +30,15 @@ class MetalDevice : IMetalDevice
 {
     public IAvnMetalDevice Native { get; private set; }
     private DisposableLock _syncRoot = new();
-    
+    private readonly GpuHandleWrapFeature _handleWrapFeature;
+    private readonly MetalExternalObjectsFeature _externalObjectsFeature;
+
 
-    public MetalDevice(IAvnMetalDevice native)
+    public MetalDevice(IAvaloniaNativeFactory factory, IAvnMetalDevice native)
     {
         Native = native;
+        _handleWrapFeature = new GpuHandleWrapFeature(factory);
+        _externalObjectsFeature = new MetalExternalObjectsFeature(native);
     }
 
     public void Dispose()
@@ -38,7 +47,14 @@ class MetalDevice : IMetalDevice
         Native = null;
     }
 
-    public object TryGetFeature(Type featureType) => null;
+    public object TryGetFeature(Type featureType)
+    {
+        if (featureType == typeof(IExternalObjectsHandleWrapRenderInterfaceContextFeature))
+            return _handleWrapFeature;
+        if (featureType == typeof(IMetalExternalObjectsFeature))
+            return _externalObjectsFeature;
+        return null;
+    }
 
     public bool IsLost => false;
 
@@ -67,6 +83,85 @@ class MetalPlatformSurface : IMetalPlatformSurface
     }
 }
 
+internal class MetalExternalObjectsFeature : IMetalExternalObjectsFeature
+{
+    private readonly IAvnMetalDevice _device;
+
+    public unsafe MetalExternalObjectsFeature(IAvnMetalDevice device)
+    {
+        _device = device;
+        ulong registryId;
+        if (_device.GetIOKitRegistryId(&registryId) != 0)
+        {
+            DeviceLuid = BitConverter.GetBytes(registryId).Reverse().ToArray();
+        }
+    }
+
+    public IReadOnlyList<string> SupportedImageHandleTypes { get; } =
+        [KnownPlatformGraphicsExternalImageHandleTypes.IOSurfaceRef];
+
+    public IReadOnlyList<string> SupportedSemaphoreTypes { get; } =
+        [KnownPlatformGraphicsExternalSemaphoreHandleTypes.MetalSharedEvent];
+    
+    public byte[] DeviceLuid { get; }
+
+    public CompositionGpuImportedImageSynchronizationCapabilities
+        GetSynchronizationCapabilities(string imageHandleType) =>
+        CompositionGpuImportedImageSynchronizationCapabilities.TimelineSemaphores;
+
+    public IMetalExternalTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties)
+    {
+        var format = properties.Format switch
+        {
+            PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm => AvnPixelFormat.kAvnRgba8888,
+            PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm => AvnPixelFormat.kAvnBgra8888,
+            _ => throw new NotSupportedException("Pixel format is not supported")
+        };
+        
+        if (handle.HandleDescriptor != KnownPlatformGraphicsExternalImageHandleTypes.IOSurfaceRef)
+            throw new NotSupportedException();
+
+        return new ImportedTexture(_device.ImportIOSurface(handle.Handle, format));
+    }
+
+    public IMetalSharedEvent ImportSharedEvent(IPlatformHandle handle)
+    {
+        if (handle.HandleDescriptor != KnownPlatformGraphicsExternalSemaphoreHandleTypes.MetalSharedEvent)
+            throw new NotSupportedException();
+        return new SharedEvent(_device.ImportSharedEvent(handle.Handle));
+    }
+
+    class ImportedTexture(IAvnMetalTexture texture) : IMetalExternalTexture
+    {
+        public void Dispose() => texture.Dispose();
+
+        public int Width => texture.Width;
+
+        public int Height => texture.Height;
+
+        public int Samples => texture.SampleCount;
+
+        public IntPtr Handle => texture.NativeHandle;
+    }
+    
+    class SharedEvent(IAvnMTLSharedEvent inner) : IMetalSharedEvent
+    {
+        public IAvnMTLSharedEvent Native => inner;
+        public void Dispose()
+        {
+            inner.Dispose();
+        }
+
+        public IntPtr Handle => inner.NativeHandle;
+    }
+
+    public void SubmitWait(IMetalSharedEvent @event, ulong waitForValue) =>
+        _device.SubmitWait(((SharedEvent)@event).Native, waitForValue);
+
+    public void SubmitSignal(IMetalSharedEvent @event, ulong signalValue) =>
+        _device.SubmitSignal(((SharedEvent)@event).Native, signalValue);
+}
+
 internal class MetalRenderTarget : IMetalPlatformSurfaceRenderTarget
 {
     private IAvnMetalRenderTarget _native;

+ 40 - 0
src/Avalonia.Native/avn.idl

@@ -2,6 +2,7 @@
 @clr-access internal
 @clr-map bool int
 @clr-map u_int64_t ulong
+@clr-map uint64_t ulong
 @clr-map int64_t long
 @clr-map long IntPtr
 @cpp-preamble @@
@@ -700,6 +701,8 @@ interface IAvaloniaNativeFactory : IUnknown
      HRESULT CreatePlatformSettings(IAvnPlatformSettings** ppv);
      HRESULT CreatePlatformBehaviorInhibition(IAvnPlatformBehaviorInhibition** ppv);
      HRESULT CreatePlatformRenderTimer(IAvnPlatformRenderTimer** ppv);
+     HRESULT ImportMTLSharedEvent([intptr]void* idMtlSharedEvent, IAvnMTLSharedEvent** ppv);
+     HRESULT CreateMemoryManagementHelper(IAvnNativeObjectsMemoryManagement** ppv);
 }
 
 [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@@ -1043,6 +1046,9 @@ interface IAvnGlContext : IUnknown
      int GetSampleCount();
      int GetStencilSize();
      [intptr]void* GetNativeHandle();
+     int texImageIOSurface2D(int target, int internal_format,
+     						int width, int height, int format, int type, [intptr]void* ioSurface, int plane);
+     bool GetIOKitRegistryId(uint64_t* value);
 }
 
 [uuid(931062d2-5bc8-4062-8588-83dd8deb99c2)]
@@ -1069,6 +1075,11 @@ interface IAvnMetalDevice : IUnknown
 {
     [intptr]void* GetDevice();
     [intptr]void* GetQueue();
+    bool GetIOKitRegistryId(uint64_t* value);
+    HRESULT ImportIOSurface([intptr]void* handle, AvnPixelFormat pixelFormat, IAvnMetalTexture** ppv);
+    HRESULT ImportSharedEvent([intptr]void*mtlSharedEventInstance, IAvnMTLSharedEvent**ppv);
+    HRESULT SubmitWait(IAvnMTLSharedEvent* ev, uint64_t value);
+    HRESULT SubmitSignal(IAvnMTLSharedEvent* ev, uint64_t value);
 }
 
 [uuid(f1306b71-eca0-426e-8700-105192693b1a)]
@@ -1076,6 +1087,35 @@ interface IAvnMetalRenderTarget : IUnknown
 {
     HRESULT BeginDrawing(IAvnMetalRenderingSession** ret);
 }
+
+[uuid(a1f4fcde-9152-48bd-bf8a-b1b651134a69)]
+interface IAvnMTLSharedEvent : IUnknown
+{
+    [intptr]void* GetNativeHandle();
+    bool Wait(uint64_t value, uint64_t timeoutMS);
+    void SetSignaledValue(uint64_t value);
+    uint64_t GetSignaledValue();
+}
+
+[uuid(722aad20-a87b-4ce5-b50f-f05cfa4cda39)]
+interface IAvnMetalTexture
+{
+    [intptr]void* GetNativeHandle();
+    int GetWidth();
+    int GetHeight();
+    int GetSampleCount();
+}
+
+[uuid(74027aa2-5262-45a5-a74a-5a53373dcc17)]
+interface IAvnNativeObjectsMemoryManagement
+{
+    void RetainNSObject([intptr] void* obj);
+    void ReleaseNSObject([intptr] void* obj);
+    uint64_t GetRetainCountForNSObject([intptr] void* obj);
+    void RetainCFObject([intptr] void* obj);
+    void ReleaseCFObject([intptr] void* obj);
+    int64_t GetRetainCountForCFObject([intptr] void* obj);
+}
     
 [uuid(e625b406-f04c-484e-946a-4abd2c6015ad)]
 interface IAvnMetalRenderingSession : IUnknown

+ 11 - 0
src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs

@@ -249,6 +249,16 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe
             var dstLayout = 0;
             _ext.SignalSemaphoreEXT(_semaphore, 0, null, 1, &texId, &dstLayout);
         }
+
+        public void WaitTimelineSemaphore(IGlExternalImageTexture texture, ulong value)
+        {
+            throw new NotSupportedException("This semaphore type doesn't support value-based wait");
+        }
+
+        public void SignalTimelineSemaphore(IGlExternalImageTexture texture, ulong value)
+        {
+            throw new NotSupportedException("This semaphore type doesn't support value-based signaling");
+        }
     }
 
     private class ExternalImageTexture : IGlExternalImageTexture
@@ -286,6 +296,7 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe
 
         public int TextureId { get; }
         public int InternalFormat => GL_RGBA8;
+        public int TextureType => GL_TEXTURE_2D;
         public PlatformGraphicsExternalImageProperties Properties { get; }
     }
 }

+ 3 - 3
src/Avalonia.OpenGL/GlConsts.cs

@@ -544,7 +544,7 @@ namespace Avalonia.OpenGL
 //        public const int GL_UNSIGNED_SHORT_5_5_5_1 = 0x8034;
 //        public const int GL_UNSIGNED_SHORT_1_5_5_5_REV = 0x8366;
 //        public const int GL_UNSIGNED_INT_8_8_8_8 = 0x8035;
-//        public const int GL_UNSIGNED_INT_8_8_8_8_REV = 0x8367;
+        public const int GL_UNSIGNED_INT_8_8_8_8_REV = 0x8367;
 //        public const int GL_UNSIGNED_INT_10_10_10_2 = 0x8036;
 //        public const int GL_UNSIGNED_INT_2_10_10_10_REV = 0x8368;
 //        public const int GL_LIGHT_MODEL_COLOR_CONTROL = 0x81F8;
@@ -1231,8 +1231,8 @@ namespace Avalonia.OpenGL
 //        public const int GL_MAX_TEXTURE_BUFFER_SIZE = 0x8C2B;
 //        public const int GL_TEXTURE_BINDING_BUFFER = 0x8C2C;
 //        public const int GL_TEXTURE_BUFFER_DATA_STORE_BINDING = 0x8C2D;
-//        public const int GL_TEXTURE_RECTANGLE = 0x84F5;
-//        public const int GL_TEXTURE_BINDING_RECTANGLE = 0x84F6;
+        public const int GL_TEXTURE_RECTANGLE = 0x84F5;
+        public const int GL_TEXTURE_BINDING_RECTANGLE = 0x84F6;
 //        public const int GL_PROXY_TEXTURE_RECTANGLE = 0x84F7;
 //        public const int GL_MAX_RECTANGLE_TEXTURE_SIZE = 0x84F8;
 //        public const int GL_R8_SNORM = 0x8F94;

+ 12 - 0
src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs

@@ -1,10 +1,13 @@
 using System;
 using System.Collections.Generic;
+using Avalonia.Metadata;
 using Avalonia.Platform;
 using Avalonia.Rendering.Composition;
 
 namespace Avalonia.OpenGL;
 
+//TODO12: Make private and expose IBitmapImpl-based import API for composition visuals
+[NotClientImplementable]
 public interface IGlContextExternalObjectsFeature
 {
     IReadOnlyList<string> SupportedImportableExternalImageTypes { get; }
@@ -23,10 +26,14 @@ public interface IGlContextExternalObjectsFeature
     public byte[]? DeviceUuid { get; }
 }
 
+//TODO12: Make private and expose IBitmapImpl-based import API for composition visuals
+[NotClientImplementable]
 public interface IGlExternalSemaphore : IDisposable
 {
     void WaitSemaphore(IGlExternalImageTexture texture);
     void SignalSemaphore(IGlExternalImageTexture texture);
+    void WaitTimelineSemaphore(IGlExternalImageTexture texture, ulong value);
+    void SignalTimelineSemaphore(IGlExternalImageTexture texture, ulong value);
 }
 
 public interface IGlExportableExternalSemaphore : IGlExternalSemaphore
@@ -34,12 +41,17 @@ public interface IGlExportableExternalSemaphore : IGlExternalSemaphore
     IPlatformHandle GetHandle();
 }
 
+[NotClientImplementable]
 public interface IGlExternalImageTexture : IDisposable
 {
     void AcquireKeyedMutex(uint key);
     void ReleaseKeyedMutex(uint key);
     int TextureId { get; }
     int InternalFormat { get; }
+    /// <summary>
+    /// GL_TEXTURE_2D or GL_TEXTURE_RECTANGLE
+    /// </summary>
+    public int TextureType { get; }
     
     PlatformGraphicsExternalImageProperties Properties { get; }
 }

+ 75 - 0
src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalExternalObjectsFeature.cs

@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Metal;
+using Avalonia.Platform;
+using Avalonia.Rendering.Composition;
+using SkiaSharp;
+
+namespace Avalonia.Skia.Metal;
+
+class SkiaMetalExternalObjectsFeature(SkiaMetalGpu gpu, IMetalExternalObjectsFeature inner) : IExternalObjectsRenderInterfaceContextFeature
+{
+    public IReadOnlyList<string> SupportedImageHandleTypes => inner.SupportedImageHandleTypes;
+    
+    public IReadOnlyList<string> SupportedSemaphoreTypes => inner.SupportedSemaphoreTypes;
+    
+    class ImportedSemaphore(IMetalSharedEvent ev) : IPlatformRenderInterfaceImportedSemaphore
+    {
+        public IMetalSharedEvent Event => ev;
+        public void Dispose() => ev.Dispose();
+    }
+    
+    class ImportedImage(SkiaMetalGpu gpu, IMetalExternalObjectsFeature feature, IMetalExternalTexture texture,
+        SKColorType colorType, bool topLeftOrigin) : IPlatformRenderInterfaceImportedImage
+    {
+        public void Dispose() => texture.Dispose();
+
+        public IBitmapImpl SnapshotWithKeyedMutex(uint acquireIndex, uint releaseIndex) => throw new System.NotSupportedException();
+
+        public IBitmapImpl SnapshotWithSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore,
+            IPlatformRenderInterfaceImportedSemaphore signalSemaphore) =>
+            throw new System.NotSupportedException();
+
+        public IBitmapImpl SnapshotWithTimelineSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore,
+            ulong waitForValue, IPlatformRenderInterfaceImportedSemaphore signalSemaphore, ulong signalValue)
+        {
+            gpu.GrContext.Flush(true, false);
+            feature.SubmitWait(((ImportedSemaphore)waitForSemaphore).Event, waitForValue);
+            using var backendTarget = new GRBackendRenderTarget(texture.Width, texture.Height, new GRMtlTextureInfo(texture.Handle));
+            
+            using var surface = SKSurface.Create(gpu.GrContext, backendTarget,
+                topLeftOrigin ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft,
+                colorType);
+            var rv = new ImmutableBitmap(surface.Snapshot());
+            gpu.GrContext.Flush();
+            feature.SubmitSignal(((ImportedSemaphore)signalSemaphore).Event, signalValue);
+            return rv;
+        }
+
+        public IBitmapImpl SnapshotWithAutomaticSync() => throw new System.NotSupportedException();
+    }
+    
+    public IPlatformRenderInterfaceImportedImage ImportImage(IPlatformHandle handle,
+        PlatformGraphicsExternalImageProperties properties)
+    {
+        var format = properties.Format switch
+        {
+            PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm => SKColorType.Rgba8888,
+            PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm => SKColorType.Bgra8888,
+            _ => throw new NotSupportedException("Pixel format is not supported")
+        };
+
+        return new ImportedImage(gpu, inner, inner.ImportImage(handle, properties), format,
+            properties.TopLeftOrigin);
+    }
+
+    public IPlatformRenderInterfaceImportedImage ImportImage(ICompositionImportableSharedGpuContextImage image) => throw new System.NotSupportedException();
+
+    public IPlatformRenderInterfaceImportedSemaphore ImportSemaphore(IPlatformHandle handle) => new ImportedSemaphore(inner.ImportSharedEvent(handle));
+
+    public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType)
+        => inner.GetSynchronizationCapabilities(imageHandleType);
+
+    public byte[]? DeviceUuid { get; } = null;
+    public byte[]? DeviceLuid => inner.DeviceLuid;
+}

+ 14 - 4
src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Runtime.InteropServices;
 using Avalonia.Metal;
 using Avalonia.Platform;
 using SkiaSharp;
@@ -11,6 +10,9 @@ internal class SkiaMetalGpu : ISkiaGpu, ISkiaGpuWithPlatformGraphicsContext
 {
     private GRContext? _context;
     private readonly IMetalDevice _device;
+    private readonly SkiaMetalExternalObjectsFeature? _externalObjects;
+
+    internal GRContext GrContext => _context ?? throw new ObjectDisposedException(nameof(SkiaMetalGpu));
 
     public SkiaMetalGpu(IMetalDevice device, long? maxResourceBytes)
     {
@@ -19,6 +21,8 @@ internal class SkiaMetalGpu : ISkiaGpu, ISkiaGpuWithPlatformGraphicsContext
                        new GRContextOptions { AvoidStencilBuffers = true })
                    ?? throw new InvalidOperationException("Unable to create GRContext from Metal device.");
         _device = device;
+        if (device.TryGetFeature<IMetalExternalObjectsFeature>() is { } externalObjects)
+            _externalObjects = new SkiaMetalExternalObjectsFeature(this, externalObjects);
         if (maxResourceBytes.HasValue)
             _context.SetResourceCacheLimit(maxResourceBytes.Value);
     }
@@ -29,15 +33,21 @@ internal class SkiaMetalGpu : ISkiaGpu, ISkiaGpuWithPlatformGraphicsContext
         _context = null;
     }
 
-    public object? TryGetFeature(Type featureType) => null;
+    public object? TryGetFeature(Type featureType)
+    {
+        if (featureType == typeof(IExternalObjectsHandleWrapRenderInterfaceContextFeature))
+            return _device.TryGetFeature(featureType);
+        if (featureType == typeof(IExternalObjectsRenderInterfaceContextFeature))
+            return _externalObjects;
+        return null;
+    }
 
     public bool IsLost => false;
     public IDisposable EnsureCurrent() => _device.EnsureCurrent();
     public IPlatformGraphicsContext? PlatformGraphicsContext => _device;
 
     public IScopedResource<GRContext> TryGetGrContext() =>
-        ScopedResource<GRContext>.Create(_context ?? throw new ObjectDisposedException(nameof(SkiaMetalGpu)),
-            EnsureCurrent().Dispose);
+        ScopedResource<GRContext>.Create(GrContext, EnsureCurrent().Dispose);
 
     public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable<object> surfaces)
     {

+ 50 - 7
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using Avalonia.Media.Imaging;
 using Avalonia.OpenGL;
 using Avalonia.Platform;
 using Avalonia.Rendering.Composition;
@@ -108,17 +109,17 @@ internal class GlSkiaImportedImage : IPlatformRenderInterfaceImportedImage
             _ => SKColorType.Rgba8888
         };
 
-    SKSurface? TryCreateSurface(int textureId, int format, int width, int height, bool topLeft)
+    SKSurface? TryCreateSurface(int target, int textureId, int format, int width, int height, bool topLeft)
     {
         var origin = topLeft ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft; 
         using var texture = new GRBackendTexture(width, height, false,
-            new GRGlTextureInfo(GlConsts.GL_TEXTURE_2D, (uint)textureId, (uint)format));
+            new GRGlTextureInfo((uint)target, (uint)textureId, (uint)format));
         var surf = SKSurface.Create(_gpu.GrContext, texture, origin, SKColorType.Rgba8888);
         if (surf != null)
             return surf;
         
         using var unformatted = new GRBackendTexture(width, height, false,
-            new GRGlTextureInfo(GlConsts.GL_TEXTURE_2D, (uint)textureId));
+            new GRGlTextureInfo((uint)GlConsts.GL_TEXTURE_2D, (uint)textureId));
         
         return SKSurface.Create(_gpu.GrContext, unformatted, origin, SKColorType.Rgba8888);
     }
@@ -130,16 +131,34 @@ internal class GlSkiaImportedImage : IPlatformRenderInterfaceImportedImage
         var internalFormat = _image?.InternalFormat ?? _sharedTexture!.InternalFormat;
         var textureId = _image?.TextureId ?? _sharedTexture!.TextureId;
         var topLeft = _image?.Properties.TopLeftOrigin ?? false;
+        var textureType = _image?.TextureType ?? GlConsts.GL_TEXTURE_2D;
         
-        using var texture = new GRBackendTexture(width, height, false,
-            new GRGlTextureInfo(GlConsts.GL_TEXTURE_2D, (uint)textureId, (uint)internalFormat));
         
         IBitmapImpl rv;
-        using (var surf = TryCreateSurface(textureId, internalFormat, width, height, topLeft))
+        using (var surf = TryCreateSurface(textureType, textureId, internalFormat, width, height, topLeft))
         {
             if (surf == null)
                 throw new OpenGlException("Unable to consume provided texture");
-            rv = new ImmutableBitmap(surf.Snapshot());
+            var snapshot = surf.Snapshot();
+            var context = _gpu.GlContext;
+            
+            rv = new ImmutableBitmap(snapshot, () =>
+            {
+                IDisposable? restoreContext = null;
+                try
+                {
+                    restoreContext = context.EnsureCurrent();
+                }
+                catch
+                {
+                    // Ignore, context is likely dead
+                }
+
+                using (restoreContext)
+                {
+                    snapshot.Dispose();
+                }
+            });
         }
 
         _gpu.GrContext.Flush();
@@ -192,6 +211,30 @@ internal class GlSkiaImportedImage : IPlatformRenderInterfaceImportedImage
         }
     }
 
+    public IBitmapImpl SnapshotWithTimelineSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore,
+        ulong waitForValue, IPlatformRenderInterfaceImportedSemaphore signalSemaphore, ulong signalValue)
+    {
+        if (_image is null)
+        {
+            throw new NotSupportedException("Only supported with an external image");
+        }
+
+        var wait = (GlSkiaImportedSemaphore)waitForSemaphore;
+        var signal = (GlSkiaImportedSemaphore)signalSemaphore;
+        using (_gpu.EnsureCurrent())
+        {
+            wait.Semaphore.WaitTimelineSemaphore(_image, waitForValue);
+            try
+            {
+                return TakeSnapshot();
+            }
+            finally
+            {
+                signal.Semaphore.SignalTimelineSemaphore(_image, signalValue);
+            }
+        }
+    }
+
     public IBitmapImpl SnapshotWithAutomaticSync()
     {
         using (_gpu.EnsureCurrent())

+ 2 - 0
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs

@@ -173,6 +173,8 @@ namespace Avalonia.Skia
                 return this;
             if (featureType == typeof(IExternalObjectsRenderInterfaceContextFeature))
                 return _externalObjectsFeature;
+            if (featureType == typeof(IExternalObjectsHandleWrapRenderInterfaceContextFeature))
+                return _glContext.TryGetFeature(featureType);
             return null;
         }
         

+ 4 - 0
src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaExternalObjectsFeature.cs

@@ -108,6 +108,10 @@ internal class VulkanSkiaExternalObjectsFeature : IExternalObjectsRenderInterfac
             return new ImmutableBitmap(image);
         }
 
+        public IBitmapImpl SnapshotWithTimelineSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore,
+            ulong waitForValue, IPlatformRenderInterfaceImportedSemaphore signalSemaphore, ulong signalValue) =>
+            throw new NotSupportedException();
+
         public IBitmapImpl SnapshotWithAutomaticSync() => throw new NotSupportedException();
     }
 

+ 7 - 2
src/Skia/Avalonia.Skia/ImmutableBitmap.cs

@@ -14,6 +14,7 @@ namespace Avalonia.Skia
     {
         private readonly SKImage _image;
         private readonly SKBitmap? _bitmap;
+        private readonly Action? _customImageDispose = null;
 
         /// <summary>
         /// Create immutable bitmap from given stream.
@@ -39,9 +40,10 @@ namespace Avalonia.Skia
             }
         }
 
-        public ImmutableBitmap(SKImage image)
+        public ImmutableBitmap(SKImage image, Action? customImageDispose = null)
         {
             _image = image;
+            _customImageDispose = customImageDispose;
             PixelSize = new PixelSize(image.Width, image.Height);
             Dpi = new Vector(96, 96);
         }
@@ -156,7 +158,10 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void Dispose()
         {
-            _image.Dispose();
+            if (_customImageDispose != null)
+                _customImageDispose();
+            else
+                _image.Dispose();
             _bitmap?.Dispose();
         }
 

+ 2 - 0
src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs

@@ -68,6 +68,8 @@ internal class AngleExternalMemoryD3D11Texture2D : IGlExternalImageTexture
 
     public int TextureId { get; private set; }
     public int InternalFormat { get; }
+
+    public int TextureType => GL_TEXTURE_2D;
     public PlatformGraphicsExternalImageProperties Properties { get; }
 }