diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 3fa8e969c8..2f034bd083 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -5,6 +5,7 @@ "packages\\Avalonia\\Avalonia.csproj", "samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj", "samples\\ControlCatalog\\ControlCatalog.csproj", + "samples\\GpuInterop\\GpuInterop.csproj", "samples\\IntegrationTestApp\\IntegrationTestApp.csproj", "samples\\MiniMvvm\\MiniMvvm.csproj", "samples\\SampleControls\\ControlSamples.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index fc42a5d63b..ce9a37a3ce 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,6 +231,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -542,6 +544,10 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -606,6 +612,7 @@ Global {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098} {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/Base.props b/build/Base.props index 9ec1c3c2d3..433ce8e950 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,6 +1,7 @@  + diff --git a/build/SharpDX.props b/build/SharpDX.props index 69aa817a01..ff521977fd 100644 --- a/build/SharpDX.props +++ b/build/SharpDX.props @@ -1,9 +1,14 @@  + + 4.0.1 + - - - - - + + + + + + + diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs index ded02330d5..c77d65ddf1 100644 --- a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs @@ -78,12 +78,7 @@ namespace ControlCatalog.Pages get => _info; private set => SetAndRaise(InfoProperty, ref _info, value); } - - static OpenGlPageControl() - { - AffectsRender(YawProperty, PitchProperty, RollProperty, DiscoProperty); - } - + private int _vertexShader; private int _fragmentShader; private int _shaderProgram; @@ -254,7 +249,7 @@ namespace ControlCatalog.Pages Console.WriteLine(err); } - protected unsafe override void OnOpenGlInit(GlInterface GL, int fb) + protected override unsafe void OnOpenGlInit(GlInterface GL) { CheckError(GL); @@ -309,7 +304,7 @@ namespace ControlCatalog.Pages } - protected override void OnOpenGlDeinit(GlInterface GL, int fb) + protected override void OnOpenGlDeinit(GlInterface GL) { // Unbind everything GL.BindBuffer(GL_ARRAY_BUFFER, 0); @@ -366,7 +361,15 @@ namespace ControlCatalog.Pages CheckError(GL); if (_disco > 0.01) - Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); + RequestNextFrameRendering(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == YawProperty || change.Property == RollProperty || change.Property == PitchProperty || + change.Property == DiscoProperty) + RequestNextFrameRendering(); + base.OnPropertyChanged(change); } } } diff --git a/samples/GpuInterop/App.axaml b/samples/GpuInterop/App.axaml new file mode 100644 index 0000000000..6b18dbe520 --- /dev/null +++ b/samples/GpuInterop/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/samples/GpuInterop/App.axaml.cs b/samples/GpuInterop/App.axaml.cs new file mode 100644 index 0000000000..85973d59bb --- /dev/null +++ b/samples/GpuInterop/App.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace GpuInterop +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { + desktopLifetime.MainWindow = new MainWindow(); + } + } + } +} diff --git a/samples/GpuInterop/D3DDemo/D3D11DemoControl.cs b/samples/GpuInterop/D3DDemo/D3D11DemoControl.cs new file mode 100644 index 0000000000..c7ee2bb9a4 --- /dev/null +++ b/samples/GpuInterop/D3DDemo/D3D11DemoControl.cs @@ -0,0 +1,147 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using SharpDX; +using SharpDX.Direct2D1; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SharpDX.Mathematics.Interop; +using Buffer = SharpDX.Direct3D11.Buffer; +using DeviceContext = SharpDX.Direct2D1.DeviceContext; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +using Matrix = SharpDX.Matrix; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiResource = SharpDX.DXGI.Resource; +using FeatureLevel = SharpDX.Direct3D.FeatureLevel; +using Vector3 = SharpDX.Vector3; + +namespace GpuInterop.D3DDemo; + +public class D3D11DemoControl : DrawingSurfaceDemoBase +{ + private D3DDevice _device; + private D3D11Swapchain _swapchain; + private SharpDX.Direct3D11.DeviceContext _context; + private Matrix _view; + private PixelSize _lastSize; + private Texture2D _depthBuffer; + private DepthStencilView _depthView; + private Matrix _proj; + private Buffer _constantBuffer; + private Stopwatch _st = Stopwatch.StartNew(); + + protected override (bool success, string info) InitializeGraphicsResources(Compositor compositor, + CompositionDrawingSurface surface, ICompositionGpuInterop interop) + { + if (interop?.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .D3D11TextureGlobalSharedHandle) != true) + return (false, "DXGI shared handle import is not supported by the current graphics backend"); + + var factory = new DxgiFactory1(); + using var adapter = factory.GetAdapter1(0); + _device = new D3DDevice(adapter, DeviceCreationFlags.None, new[] + { + FeatureLevel.Level_12_1, + FeatureLevel.Level_12_0, + FeatureLevel.Level_11_1, + FeatureLevel.Level_11_0, + FeatureLevel.Level_10_0, + FeatureLevel.Level_9_3, + FeatureLevel.Level_9_2, + FeatureLevel.Level_9_1, + }); + _swapchain = new D3D11Swapchain(_device, interop, surface); + _context = _device.ImmediateContext; + _constantBuffer = D3DContent.CreateMesh(_device); + _view = Matrix.LookAtLH(new Vector3(0, 0, -5), new Vector3(0, 0, 0), Vector3.UnitY); + return (true, $"D3D11 ({_device.FeatureLevel}) {adapter.Description1.Description}"); + } + + protected override void FreeGraphicsResources() + { + _swapchain.DisposeAsync(); + _swapchain = null!; + Utilities.Dispose(ref _depthView); + Utilities.Dispose(ref _depthBuffer); + Utilities.Dispose(ref _constantBuffer); + Utilities.Dispose(ref _context); + Utilities.Dispose(ref _device); + } + + protected override bool SupportsDisco => true; + + protected override void RenderFrame(PixelSize pixelSize) + { + if (pixelSize == default) + return; + if (pixelSize != _lastSize) + Resize(pixelSize); + using (_swapchain.BeginDraw(pixelSize, out var renderView)) + { + + _device.ImmediateContext.OutputMerger.SetTargets(_depthView, renderView); + var viewProj = Matrix.Multiply(_view, _proj); + var context = _device.ImmediateContext; + + var now = _st.Elapsed.TotalSeconds * 5; + var scaleX = (float)(1f + Disco * (Math.Sin(now) + 1) / 6); + var scaleY = (float)(1f + Disco * (Math.Cos(now) + 1) / 8); + var colorOff =(float) (Math.Sin(now) + 1) / 2 * Disco; + + + // Clear views + context.ClearDepthStencilView(_depthView, DepthStencilClearFlags.Depth, 1.0f, 0); + context.ClearRenderTargetView(renderView, + new RawColor4(1 - colorOff, colorOff, (float)0.5 + colorOff / 2, 1)); + + + var ypr = Matrix4x4.CreateFromYawPitchRoll(Yaw, Pitch, Roll); + // Update WorldViewProj Matrix + var worldViewProj = Matrix.RotationX((float)Yaw) * Matrix.RotationY((float)Pitch) + * Matrix.RotationZ((float)Roll) + * Matrix.Scaling(new Vector3(scaleX, scaleY, 1)) + * viewProj; + worldViewProj.Transpose(); + context.UpdateSubresource(ref worldViewProj, _constantBuffer); + + // Draw the cube + context.Draw(36, 0); + + + _context.Flush(); + } + } + + private void Resize(PixelSize size) + { + Utilities.Dispose(ref _depthBuffer); + _depthBuffer = new Texture2D(_device, + new Texture2DDescription() + { + Format = Format.D32_Float_S8X24_UInt, + ArraySize = 1, + MipLevels = 1, + Width = (int)size.Width, + Height = (int)size.Height, + SampleDescription = new SampleDescription(1, 0), + Usage = ResourceUsage.Default, + BindFlags = BindFlags.DepthStencil, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None + }); + + Utilities.Dispose(ref _depthView); + _depthView = new DepthStencilView(_device, _depthBuffer); + + // Setup targets and viewport for rendering + _device.ImmediateContext.Rasterizer.SetViewport(new Viewport(0, 0, (int)size.Width, (int)size.Height, 0.0f, 1.0f)); + + // Setup new projection matrix with correct aspect ratio + _proj = Matrix.PerspectiveFovLH((float)Math.PI / 4.0f, (float)(size.Width / size.Height), 0.1f, 100.0f); + } +} diff --git a/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs b/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs new file mode 100644 index 0000000000..30a4c19d35 --- /dev/null +++ b/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using SharpDX.Direct2D1; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SharpDX.Mathematics.Interop; +using Buffer = SharpDX.Direct3D11.Buffer; +using DeviceContext = SharpDX.Direct2D1.DeviceContext; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +using Matrix = SharpDX.Matrix; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiResource = SharpDX.DXGI.Resource; +using FeatureLevel = SharpDX.Direct3D.FeatureLevel; + +namespace GpuInterop.D3DDemo; + +class D3D11Swapchain : SwapchainBase +{ + private readonly D3DDevice _device; + + public D3D11Swapchain(D3DDevice device, ICompositionGpuInterop interop, CompositionDrawingSurface target) + : base(interop, target) + { + _device = device; + } + + protected override D3D11SwapchainImage CreateImage(PixelSize size) => new(_device, size, Interop, Target); + + public IDisposable BeginDraw(PixelSize size, out RenderTargetView view) + { + var rv = BeginDrawCore(size, out var image); + view = image.RenderTargetView; + return rv; + } +} + +public class D3D11SwapchainImage : ISwapchainImage +{ + public PixelSize Size { get; } + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _target; + private readonly Texture2D _texture; + private readonly KeyedMutex _mutex; + private readonly IntPtr _handle; + private PlatformGraphicsExternalImageProperties _properties; + private ICompositionImportedGpuImage? _imported; + public Task? LastPresent { get; private set; } + public RenderTargetView RenderTargetView { get; } + + public D3D11SwapchainImage(D3DDevice device, PixelSize size, + ICompositionGpuInterop interop, + CompositionDrawingSurface target) + { + Size = size; + _interop = interop; + _target = target; + _texture = new Texture2D(device, + new Texture2DDescription + { + Format = Format.R8G8B8A8_UNorm, + Width = size.Width, + Height = size.Height, + ArraySize = 1, + MipLevels = 1, + SampleDescription = new SampleDescription { Count = 1, Quality = 0 }, + CpuAccessFlags = default, + OptionFlags = ResourceOptionFlags.SharedKeyedmutex, + BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource + }); + _mutex = _texture.QueryInterface(); + using (var res = _texture.QueryInterface()) + _handle = res.SharedHandle; + _properties = new PlatformGraphicsExternalImageProperties + { + Width = size.Width, Height = size.Height, Format = PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm + }; + + RenderTargetView = new RenderTargetView(device, _texture); + } + + public void BeginDraw() + { + _mutex.Acquire(0, int.MaxValue); + } + + public void Present() + { + _mutex.Release(1); + _imported ??= _interop.ImportImage( + new PlatformHandle(_handle, KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle), + _properties); + LastPresent = _target.UpdateWithKeyedMutexAsync(_imported, 1, 0); + } + + + public async ValueTask DisposeAsync() + { + if (LastPresent != null) + try + { + await LastPresent; + } + catch + { + // Ignore + } + + RenderTargetView.Dispose(); + _mutex.Dispose(); + _texture.Dispose(); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/D3DDemo/D3DContent.cs b/samples/GpuInterop/D3DDemo/D3DContent.cs new file mode 100644 index 0000000000..f670a9a8c9 --- /dev/null +++ b/samples/GpuInterop/D3DDemo/D3DContent.cs @@ -0,0 +1,110 @@ +using SharpDX; +using SharpDX.D3DCompiler; +using SharpDX.Direct3D; + +using System; +using System.Linq; +using System.Threading.Tasks; +using SharpDX.Direct2D1; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SharpDX.Mathematics.Interop; +using Buffer = SharpDX.Direct3D11.Buffer; +using DeviceContext = SharpDX.Direct2D1.DeviceContext; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +using Matrix = SharpDX.Matrix; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiResource = SharpDX.DXGI.Resource; +using FeatureLevel = SharpDX.Direct3D.FeatureLevel; +using InputElement = SharpDX.Direct3D11.InputElement; + + +namespace GpuInterop.D3DDemo; + +public class D3DContent +{ + + public static Buffer CreateMesh(D3DDevice device) + { + // Compile Vertex and Pixel shaders + var vertexShaderByteCode = ShaderBytecode.CompileFromFile("D3DDemo\\MiniCube.fx", "VS", "vs_4_0"); + var vertexShader = new VertexShader(device, vertexShaderByteCode); + + var pixelShaderByteCode = ShaderBytecode.CompileFromFile("D3DDemo\\MiniCube.fx", "PS", "ps_4_0"); + var pixelShader = new PixelShader(device, pixelShaderByteCode); + + var signature = ShaderSignature.GetInputSignature(vertexShaderByteCode); + + var inputElements = new[] + { + new InputElement("POSITION", 0, Format.R32G32B32A32_Float, 0, 0), + new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 16, 0) + }; + + // Layout from VertexShader input signature + var layout = new InputLayout( + device, + signature, + inputElements); + + // Instantiate Vertex buffer from vertex data + using var vertices = Buffer.Create( + device, + BindFlags.VertexBuffer, + new[] + { + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), // Front + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), // BACK + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), // Top + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), // Bottom + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), // Left + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), // Right + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + }); + + // Create Constant Buffer + var constantBuffer = new Buffer(device, Utilities.SizeOf(), ResourceUsage.Default, + BindFlags.ConstantBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0); + + var context = device.ImmediateContext; + + // Prepare All the stages + context.InputAssembler.InputLayout = layout; + context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; + context.InputAssembler.SetVertexBuffers(0, + new VertexBufferBinding(vertices, Utilities.SizeOf() * 2, 0)); + context.VertexShader.SetConstantBuffer(0, constantBuffer); + context.VertexShader.Set(vertexShader); + context.PixelShader.Set(pixelShader); + return constantBuffer; + } +} diff --git a/samples/GpuInterop/D3DDemo/MiniCube.fx b/samples/GpuInterop/D3DDemo/MiniCube.fx new file mode 100644 index 0000000000..c6064ea56c --- /dev/null +++ b/samples/GpuInterop/D3DDemo/MiniCube.fx @@ -0,0 +1,47 @@ +// Copyright (c) 2010-2013 SharpDX - Alexandre Mutel +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +struct VS_IN +{ + float4 pos : POSITION; + float4 col : COLOR; +}; + +struct PS_IN +{ + float4 pos : SV_POSITION; + float4 col : COLOR; +}; + +float4x4 worldViewProj; + +PS_IN VS( VS_IN input ) +{ + PS_IN output = (PS_IN)0; + + output.pos = mul(input.pos, worldViewProj); + output.col = input.col; + + return output; +} + +float4 PS( PS_IN input ) : SV_Target +{ + return input.col; +} diff --git a/samples/GpuInterop/DrawingSurfaceDemoBase.cs b/samples/GpuInterop/DrawingSurfaceDemoBase.cs new file mode 100644 index 0000000000..aad813ea82 --- /dev/null +++ b/samples/GpuInterop/DrawingSurfaceDemoBase.cs @@ -0,0 +1,141 @@ +using System; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.LogicalTree; +using Avalonia.Rendering.Composition; +using Avalonia.VisualTree; + +namespace GpuInterop; + +public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo +{ + private CompositionSurfaceVisual? _visual; + private Compositor? _compositor; + private Action _update; + private string _info; + private bool _updateQueued; + private bool _initialized; + + protected CompositionDrawingSurface Surface { get; private set; } + + public DrawingSurfaceDemoBase() + { + _update = UpdateFrame; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + Initialize(); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_initialized) + FreeGraphicsResources(); + _initialized = false; + base.OnDetachedFromLogicalTree(e); + } + + async void Initialize() + { + try + { + var selfVisual = ElementComposition.GetElementVisual(this)!; + _compositor = selfVisual.Compositor; + + Surface = _compositor.CreateDrawingSurface(); + _visual = _compositor.CreateSurfaceVisual(); + _visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + _visual.Surface = Surface; + ElementComposition.SetElementChildVisual(this, _visual); + var (res, info) = await DoInitialize(_compositor, Surface); + _info = info; + if (ParentControl != null) + ParentControl.Info = info; + _initialized = res; + QueueNextFrame(); + } + catch (Exception e) + { + if (ParentControl != null) + ParentControl.Info = e.ToString(); + } + } + + void UpdateFrame() + { + _updateQueued = false; + var root = this.GetVisualRoot(); + if (root == null) + return; + + _visual!.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + var size = PixelSize.FromSize(Bounds.Size, root.RenderScaling); + RenderFrame(size); + if (SupportsDisco && Disco > 0) + QueueNextFrame(); + } + + void QueueNextFrame() + { + if (_initialized && !_updateQueued && _compositor != null) + { + _updateQueued = true; + _compositor?.RequestCompositionUpdate(_update); + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if(change.Property == BoundsProperty) + QueueNextFrame(); + base.OnPropertyChanged(change); + } + + async Task<(bool success, string info)> DoInitialize(Compositor compositor, + CompositionDrawingSurface compositionDrawingSurface) + { + var interop = await compositor.TryGetCompositionGpuInterop(); + if (interop == null) + return (false, "Compositor doesn't support interop for the current backend"); + return InitializeGraphicsResources(compositor, compositionDrawingSurface, interop); + } + + protected abstract (bool success, string info) InitializeGraphicsResources(Compositor compositor, + CompositionDrawingSurface compositionDrawingSurface, ICompositionGpuInterop gpuInterop); + + protected abstract void FreeGraphicsResources(); + + + protected abstract void RenderFrame(PixelSize pixelSize); + protected virtual bool SupportsDisco => false; + + public void Update(GpuDemo parent, float yaw, float pitch, float roll, float disco) + { + ParentControl = parent; + if (ParentControl != null) + { + ParentControl.Info = _info; + ParentControl.DiscoVisible = true; + } + + Yaw = yaw; + Pitch = pitch; + Roll = roll; + Disco = disco; + QueueNextFrame(); + } + + public GpuDemo? ParentControl { get; private set; } + + public float Disco { get; private set; } + + public float Roll { get; private set; } + + public float Pitch { get; private set; } + + public float Yaw { get; private set; } +} diff --git a/samples/GpuInterop/GpuDemo.axaml b/samples/GpuInterop/GpuDemo.axaml new file mode 100644 index 0000000000..6ca51abadb --- /dev/null +++ b/samples/GpuInterop/GpuDemo.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + Yaw + + Pitch + + Roll + + + + D + I + S + C + O + + + + + + + diff --git a/samples/GpuInterop/GpuDemo.axaml.cs b/samples/GpuInterop/GpuDemo.axaml.cs new file mode 100644 index 0000000000..f7acac09e1 --- /dev/null +++ b/samples/GpuInterop/GpuDemo.axaml.cs @@ -0,0 +1,116 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace GpuInterop; + +public class GpuDemo : UserControl +{ + public GpuDemo() + { + AvaloniaXamlLoader.Load(this); + } + + private float _yaw = 5; + + public static readonly DirectProperty YawProperty = + AvaloniaProperty.RegisterDirect("Yaw", o => o.Yaw, (o, v) => o.Yaw = v); + + public float Yaw + { + get => _yaw; + set => SetAndRaise(YawProperty, ref _yaw, value); + } + + private float _pitch = 5; + + public static readonly DirectProperty PitchProperty = + AvaloniaProperty.RegisterDirect("Pitch", o => o.Pitch, (o, v) => o.Pitch = v); + + public float Pitch + { + get => _pitch; + set => SetAndRaise(PitchProperty, ref _pitch, value); + } + + + private float _roll = 5; + + public static readonly DirectProperty RollProperty = + AvaloniaProperty.RegisterDirect("Roll", o => o.Roll, (o, v) => o.Roll = v); + + public float Roll + { + get => _roll; + set => SetAndRaise(RollProperty, ref _roll, value); + } + + + private float _disco; + + public static readonly DirectProperty DiscoProperty = + AvaloniaProperty.RegisterDirect("Disco", o => o.Disco, (o, v) => o.Disco = v); + + public float Disco + { + get => _disco; + set => SetAndRaise(DiscoProperty, ref _disco, value); + } + + private string _info = string.Empty; + + public static readonly DirectProperty InfoProperty = + AvaloniaProperty.RegisterDirect("Info", o => o.Info, (o, v) => o.Info = v); + + public string Info + { + get => _info; + set => SetAndRaise(InfoProperty, ref _info, value); + } + + private bool _discoVisible; + + public static readonly DirectProperty DiscoVisibleProperty = + AvaloniaProperty.RegisterDirect("DiscoVisible", o => o.DiscoVisible, + (o, v) => o._discoVisible = v); + + public bool DiscoVisible + { + get => _discoVisible; + set => SetAndRaise(DiscoVisibleProperty, ref _discoVisible, value); + } + + private IGpuDemo _demo; + + public static readonly DirectProperty DemoProperty = + AvaloniaProperty.RegisterDirect("Demo", o => o.Demo, + (o, v) => o._demo = v); + + public IGpuDemo Demo + { + get => _demo; + set => SetAndRaise(DemoProperty, ref _demo, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == YawProperty + || change.Property == PitchProperty + || change.Property == RollProperty + || change.Property == DiscoProperty + || change.Property == DemoProperty + ) + { + if (change.Property == DemoProperty) + ((IGpuDemo)change.OldValue)?.Update(null, 0, 0, 0, 0); + _demo?.Update(this, Yaw, Pitch, Roll, Disco); + } + + base.OnPropertyChanged(change); + } +} + +public interface IGpuDemo +{ + void Update(GpuDemo parent, float yaw, float pitch, float roll, float disco); +} diff --git a/samples/GpuInterop/GpuInterop.csproj b/samples/GpuInterop/GpuInterop.csproj new file mode 100644 index 0000000000..c201d9bf85 --- /dev/null +++ b/samples/GpuInterop/GpuInterop.csproj @@ -0,0 +1,50 @@ + + + + Exe + net7.0 + true + enable + false + true + true + true + + + + + + + + + + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/GpuInterop/MainWindow.axaml b/samples/GpuInterop/MainWindow.axaml new file mode 100644 index 0000000000..afb025ec27 --- /dev/null +++ b/samples/GpuInterop/MainWindow.axaml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/samples/GpuInterop/MainWindow.axaml.cs b/samples/GpuInterop/MainWindow.axaml.cs new file mode 100644 index 0000000000..8fc8926783 --- /dev/null +++ b/samples/GpuInterop/MainWindow.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace GpuInterop +{ + public class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + this.AttachDevTools(); + this.Renderer.DrawFps = true; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/GpuInterop/Program.cs b/samples/GpuInterop/Program.cs new file mode 100644 index 0000000000..86fd239b4c --- /dev/null +++ b/samples/GpuInterop/Program.cs @@ -0,0 +1,15 @@ +using Avalonia; + +namespace GpuInterop +{ + public class Program + { + static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); + } +} diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile b/samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile new file mode 100644 index 0000000000..900e35a93e --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile @@ -0,0 +1,12 @@ +#!/usr/bin/make -f + +all: vert.spirv frag.spirv +.PHONY: all + +vert.spirv: vert.glsl + glslc -fshader-stage=vert vert.glsl -o vert.spirv + +frag.spirv: frag.glsl + glslc -fshader-stage=frag frag.glsl -o frag.spirv + + diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl b/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl new file mode 100644 index 0000000000..31e9f00390 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl @@ -0,0 +1,42 @@ +#version 450 + layout(location = 0) in vec3 FragPos; + layout(location = 1) in vec3 VecPos; + layout(location = 2) in vec3 Normal; + layout(push_constant) uniform constants{ + layout(offset = 0) float maxY; + layout(offset = 4) float minY; + layout(offset = 8) float time; + layout(offset = 12) float disco; + }; + layout(location = 0) out vec4 outFragColor; + + void main() + { + float y = (VecPos.y - minY) / (maxY - minY); + float c = cos(atan(VecPos.x, VecPos.z) * 20.0 + time * 40.0 + y * 50.0); + float s = sin(-atan(VecPos.z, VecPos.x) * 20.0 - time * 20.0 - y * 30.0); + + vec3 discoColor = vec3( + 0.5 + abs(0.5 - y) * cos(time * 10.0), + 0.25 + (smoothstep(0.3, 0.8, y) * (0.5 - c / 4.0)), + 0.25 + abs((smoothstep(0.1, 0.4, y) * (0.5 - s / 4.0)))); + + vec3 objectColor = vec3((1.0 - y), 0.40 + y / 4.0, y * 0.75 + 0.25); + objectColor = objectColor * (1.0 - disco) + discoColor * disco; + + float ambientStrength = 0.3; + vec3 lightColor = vec3(1.0, 1.0, 1.0); + vec3 lightPos = vec3(maxY * 2.0, maxY * 2.0, maxY * 2.0); + vec3 ambient = ambientStrength * lightColor; + + + vec3 norm = normalize(Normal); + vec3 lightDir = normalize(lightPos - FragPos); + + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = diff * lightColor; + + vec3 result = (ambient + diffuse) * objectColor; + outFragColor = vec4(result, 1.0); + + } \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.spirv b/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.spirv new file mode 100644 index 0000000000..dbac67c368 Binary files /dev/null and b/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.spirv differ diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl b/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl new file mode 100644 index 0000000000..c700d78d25 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl @@ -0,0 +1,36 @@ +#version 450 + layout(location = 0) in vec3 aPos; + layout(location = 1) in vec3 aNormal; + + layout(location = 0) out vec3 FragPos; + layout(location = 1) out vec3 VecPos; + layout(location = 2) out vec3 Normal; + + layout(push_constant) uniform constants{ + float maxY; + float minY; + float time; + float disco; + mat4 model; + }; + + layout(binding = 0) uniform UniformBufferObject { + mat4 projection; + } ubo; + + void main() + { + float discoScale = sin(time * 10.0) / 10.0; + float distortionX = 1.0 + disco * cos(time * 20.0) / 10.0; + + float scale = 1.0 + disco * discoScale; + + vec3 scaledPos = aPos; + scaledPos.x = scaledPos.x * distortionX; + + scaledPos *= scale; + gl_Position = ubo.projection * model * vec4(scaledPos, 1.0); + FragPos = vec3(model * vec4(aPos, 1.0)); + VecPos = aPos; + Normal = normalize(vec3(model * vec4(aNormal, 1.0))); + } \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.spirv b/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.spirv new file mode 100644 index 0000000000..3bb2ba9383 Binary files /dev/null and b/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.spirv differ diff --git a/samples/GpuInterop/VulkanDemo/ByteString.cs b/samples/GpuInterop/VulkanDemo/ByteString.cs new file mode 100644 index 0000000000..bce66e017d --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/ByteString.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; + +namespace GpuInterop.VulkanDemo; + +unsafe class ByteString : IDisposable +{ + public IntPtr Pointer { get; } + + public ByteString(string s) + { + Pointer = Marshal.StringToHGlobalAnsi(s); + } + + public void Dispose() + { + Marshal.FreeHGlobal(Pointer); + } + + public static implicit operator byte*(ByteString h) => (byte*)h.Pointer; +} + +unsafe class ByteStringList : IDisposable +{ + private List _inner; + private byte** _ptr; + + public ByteStringList(IEnumerable items) + { + _inner = items.Select(x => new ByteString(x)).ToList(); + _ptr = (byte**)Marshal.AllocHGlobal(IntPtr.Size * _inner.Count + 1); + for (var c = 0; c < _inner.Count; c++) + _ptr[c] = (byte*)_inner[c].Pointer; + } + + public int Count => _inner.Count; + public uint UCount => (uint)_inner.Count; + + public void Dispose() + { + Marshal.FreeHGlobal(new IntPtr(_ptr)); + } + + public static implicit operator byte**(ByteStringList h) => h._ptr; +} diff --git a/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs b/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs new file mode 100644 index 0000000000..a0b7d32d3b --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +namespace GpuInterop.VulkanDemo; + +public class D3DMemoryHelper +{ + public static D3DDevice CreateDeviceByLuid(Span luid) + { + var factory = new DxgiFactory1(); + var longLuid = MemoryMarshal.Cast(luid)[0]; + for (var c = 0; c < factory.GetAdapterCount1(); c++) + { + using var adapter = factory.GetAdapter1(0); + if (adapter.Description1.Luid != longLuid) + continue; + + return new D3DDevice(adapter, DeviceCreationFlags.None, + new[] + { + FeatureLevel.Level_12_1, FeatureLevel.Level_12_0, FeatureLevel.Level_11_1, + FeatureLevel.Level_11_0, FeatureLevel.Level_10_0, FeatureLevel.Level_9_3, + FeatureLevel.Level_9_2, FeatureLevel.Level_9_1, + }); + } + + throw new ArgumentException("Device with the corresponding LUID not found"); + } + + public static Texture2D CreateMemoryHandle(D3DDevice device, PixelSize size, Silk.NET.Vulkan.Format format) + { + if (format != Silk.NET.Vulkan.Format.R8G8B8A8Unorm) + throw new ArgumentException("Not supported format"); + return new Texture2D(device, + new Texture2DDescription + { + Format = Format.R8G8B8A8_UNorm, + Width = size.Width, + Height = size.Height, + ArraySize = 1, + MipLevels = 1, + SampleDescription = new SampleDescription { Count = 1, Quality = 0 }, + CpuAccessFlags = default, + OptionFlags = ResourceOptionFlags.SharedKeyedmutex, + BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource + }); + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs b/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs new file mode 100644 index 0000000000..949a951a36 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs @@ -0,0 +1,80 @@ +using System; +using System.Runtime.CompilerServices; +using Silk.NET.Vulkan; +using SilkNetDemo; +using Buffer = Silk.NET.Vulkan.Buffer; +using SystemBuffer = System.Buffer; + +namespace GpuInterop.VulkanDemo; + +static class VulkanBufferHelper +{ + public unsafe static void AllocateBuffer(VulkanContext vk, + BufferUsageFlags bufferUsageFlags, + out Buffer buffer, out DeviceMemory memory, + Span initialData) where T:unmanaged + { + var api = vk.Api; + var device = vk.Device; + + var size = Unsafe.SizeOf() * initialData.Length; + var bufferInfo = new BufferCreateInfo() + { + SType = StructureType.BufferCreateInfo, + Size = (ulong)size, + Usage = bufferUsageFlags, + SharingMode = SharingMode.Exclusive + }; + api.CreateBuffer(device, bufferInfo, null, out buffer).ThrowOnError(); + + api.GetBufferMemoryRequirements(device, buffer, out var memoryRequirements); + + var physicalDevice = vk.PhysicalDevice; + + var memoryAllocateInfo = new MemoryAllocateInfo + { + SType = StructureType.MemoryAllocateInfo, + AllocationSize = memoryRequirements.Size, + MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api, + physicalDevice, + memoryRequirements.MemoryTypeBits, + MemoryPropertyFlags.MemoryPropertyHostCoherentBit | + MemoryPropertyFlags.MemoryPropertyHostVisibleBit) + }; + + api.AllocateMemory(device, memoryAllocateInfo, null, out memory).ThrowOnError(); + api.BindBufferMemory(device, buffer, memory, 0); + UpdateBufferMemory(vk, memory, initialData); + } + + public static unsafe void UpdateBufferMemory(VulkanContext vk, DeviceMemory memory, + Span data) where T : unmanaged + { + var api = vk.Api; + var device = vk.Device; + + var size = data.Length * Unsafe.SizeOf(); + void* pointer = null; + api.MapMemory(device, memory, 0, (ulong)size, 0, ref pointer); + + data.CopyTo(new Span(pointer, size)); + + api.UnmapMemory(device, memory); + + } + + private static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits, + MemoryPropertyFlags flags) + { + api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties); + + for (var i = 0; i < properties.MemoryTypeCount; i++) + { + var type = properties.MemoryTypes[i]; + + if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i; + } + + return -1; + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs b/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs new file mode 100644 index 0000000000..65a05c5226 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using Avalonia.Input; +using Silk.NET.Vulkan; +using SilkNetDemo; + +namespace Avalonia.Vulkan +{ + public class VulkanCommandBufferPool : IDisposable + { + private readonly Vk _api; + private readonly Device _device; + private readonly Queue _queue; + private readonly CommandPool _commandPool; + + private readonly List _usedCommandBuffers = new(); + private object _lock = new object(); + + public unsafe VulkanCommandBufferPool(Vk api, Device device, Queue queue, uint queueFamilyIndex) + { + _api = api; + _device = device; + _queue = queue; + + var commandPoolCreateInfo = new CommandPoolCreateInfo + { + SType = StructureType.CommandPoolCreateInfo, + Flags = CommandPoolCreateFlags.CommandPoolCreateResetCommandBufferBit, + QueueFamilyIndex = queueFamilyIndex + }; + + _api.CreateCommandPool(_device, commandPoolCreateInfo, null, out _commandPool) + .ThrowOnError(); + } + + public unsafe void Dispose() + { + lock (_lock) + { + FreeUsedCommandBuffers(); + _api.DestroyCommandPool(_device, _commandPool, null); + } + } + + private CommandBuffer AllocateCommandBuffer() + { + var commandBufferAllocateInfo = new CommandBufferAllocateInfo + { + SType = StructureType.CommandBufferAllocateInfo, + CommandPool = _commandPool, + CommandBufferCount = 1, + Level = CommandBufferLevel.Primary + }; + + lock (_lock) + { + _api.AllocateCommandBuffers(_device, commandBufferAllocateInfo, out var commandBuffer); + + return commandBuffer; + } + } + + public VulkanCommandBuffer CreateCommandBuffer() + { + return new(_api, _device, _queue, this); + } + + public void FreeUsedCommandBuffers() + { + lock (_lock) + { + foreach (var usedCommandBuffer in _usedCommandBuffers) usedCommandBuffer.Dispose(); + + _usedCommandBuffers.Clear(); + } + } + + private void DisposeCommandBuffer(VulkanCommandBuffer commandBuffer) + { + lock (_lock) + { + _usedCommandBuffers.Add(commandBuffer); + } + } + + public class VulkanCommandBuffer : IDisposable + { + private readonly VulkanCommandBufferPool _commandBufferPool; + private readonly Vk _api; + private readonly Device _device; + private readonly Queue _queue; + private readonly Fence _fence; + private bool _hasEnded; + private bool _hasStarted; + + public IntPtr Handle => InternalHandle.Handle; + + internal CommandBuffer InternalHandle { get; } + + internal unsafe VulkanCommandBuffer(Vk api, Device device, Queue queue, VulkanCommandBufferPool commandBufferPool) + { + _api = api; + _device = device; + _queue = queue; + _commandBufferPool = commandBufferPool; + + InternalHandle = _commandBufferPool.AllocateCommandBuffer(); + + var fenceCreateInfo = new FenceCreateInfo() + { + SType = StructureType.FenceCreateInfo, + Flags = FenceCreateFlags.FenceCreateSignaledBit + }; + + api.CreateFence(device, fenceCreateInfo, null, out _fence); + } + + public unsafe void Dispose() + { + _api.WaitForFences(_device, 1, _fence, true, ulong.MaxValue); + lock (_commandBufferPool._lock) + { + _api.FreeCommandBuffers(_device, _commandBufferPool._commandPool, 1, InternalHandle); + } + _api.DestroyFence(_device, _fence, null); + } + + public void BeginRecording() + { + if (!_hasStarted) + { + _hasStarted = true; + + var beginInfo = new CommandBufferBeginInfo + { + SType = StructureType.CommandBufferBeginInfo, + Flags = CommandBufferUsageFlags.CommandBufferUsageOneTimeSubmitBit + }; + + _api.BeginCommandBuffer(InternalHandle, beginInfo); + } + } + + public void EndRecording() + { + if (_hasStarted && !_hasEnded) + { + _hasEnded = true; + + _api.EndCommandBuffer(InternalHandle); + } + } + + public void Submit() + { + Submit(null, null, null, _fence); + } + + public class KeyedMutexSubmitInfo + { + public ulong? AcquireKey { get; set; } + public ulong? ReleaseKey { get; set; } + public DeviceMemory DeviceMemory { get; set; } + } + + public unsafe void Submit( + ReadOnlySpan waitSemaphores, + ReadOnlySpan waitDstStageMask = default, + ReadOnlySpan signalSemaphores = default, + Fence? fence = null, + KeyedMutexSubmitInfo keyedMutex = null) + { + EndRecording(); + + if (!fence.HasValue) + fence = _fence; + + + ulong acquireKey = keyedMutex?.AcquireKey ?? 0, releaseKey = keyedMutex?.ReleaseKey ?? 0; + DeviceMemory devMem = keyedMutex?.DeviceMemory ?? default; + uint timeout = uint.MaxValue; + Win32KeyedMutexAcquireReleaseInfoKHR mutex = default; + if (keyedMutex != null) + mutex = new Win32KeyedMutexAcquireReleaseInfoKHR + { + SType = StructureType.Win32KeyedMutexAcquireReleaseInfoKhr, + AcquireCount = keyedMutex.AcquireKey.HasValue ? 1u : 0u, + ReleaseCount = keyedMutex.ReleaseKey.HasValue ? 1u : 0u, + PAcquireKeys = &acquireKey, + PReleaseKeys = &releaseKey, + PAcquireSyncs = &devMem, + PReleaseSyncs = &devMem, + PAcquireTimeouts = &timeout + }; + + fixed (Semaphore* pWaitSemaphores = waitSemaphores, pSignalSemaphores = signalSemaphores) + { + fixed (PipelineStageFlags* pWaitDstStageMask = waitDstStageMask) + { + var commandBuffer = InternalHandle; + var submitInfo = new SubmitInfo + { + PNext = keyedMutex != null ? &mutex : null, + SType = StructureType.SubmitInfo, + WaitSemaphoreCount = waitSemaphores != null ? (uint)waitSemaphores.Length : 0, + PWaitSemaphores = pWaitSemaphores, + PWaitDstStageMask = pWaitDstStageMask, + CommandBufferCount = 1, + PCommandBuffers = &commandBuffer, + SignalSemaphoreCount = signalSemaphores != null ? (uint)signalSemaphores.Length : 0, + PSignalSemaphores = pSignalSemaphores, + }; + + _api.ResetFences(_device, 1, fence.Value); + + _api.QueueSubmit(_queue, 1, submitInfo, fence.Value); + } + } + + _commandBufferPool.DisposeCommandBuffer(this); + } + } + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanContent.cs b/samples/GpuInterop/VulkanDemo/VulkanContent.cs new file mode 100644 index 0000000000..b16343190a --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanContent.cs @@ -0,0 +1,829 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Threading; +using Silk.NET.Vulkan; +using SilkNetDemo; +using Buffer = System.Buffer; +using Image = Silk.NET.Vulkan.Image; + +namespace GpuInterop.VulkanDemo; + +unsafe class VulkanContent : IDisposable +{ + private readonly VulkanContext _context; + private ShaderModule _vertShader; + private ShaderModule _fragShader; + private PipelineLayout _pipelineLayout; + private RenderPass _renderPass; + private Pipeline _pipeline; + private DescriptorSetLayout _descriptorSetLayout; + private Silk.NET.Vulkan.Buffer _vertexBuffer; + private DeviceMemory _vertexBufferMemory; + private Silk.NET.Vulkan.Buffer _indexBuffer; + private DeviceMemory _indexBufferMemory; + private Silk.NET.Vulkan.Buffer _uniformBuffer; + private DeviceMemory _uniformBufferMemory; + private Framebuffer _framebuffer; + + private Image _depthImage; + private DeviceMemory _depthImageMemory; + private ImageView _depthImageView; + + public VulkanContent(VulkanContext context) + { + _context = context; + var name = typeof(VulkanContent).Assembly.GetManifestResourceNames().First(x => x.Contains("teapot.bin")); + using (var sr = new BinaryReader(typeof(VulkanContent).Assembly.GetManifestResourceStream(name))) + { + var buf = new byte[sr.ReadInt32()]; + sr.Read(buf, 0, buf.Length); + var points = new float[buf.Length / 4]; + Buffer.BlockCopy(buf, 0, points, 0, buf.Length); + buf = new byte[sr.ReadInt32()]; + sr.Read(buf, 0, buf.Length); + _indices = new ushort[buf.Length / 2]; + Buffer.BlockCopy(buf, 0, _indices, 0, buf.Length); + _points = new Vertex[points.Length / 3]; + for (var primitive = 0; primitive < points.Length / 3; primitive++) + { + var srci = primitive * 3; + _points[primitive] = new Vertex + { + Position = new Vector3(points[srci], points[srci + 1], points[srci + 2]) + }; + } + + for (int i = 0; i < _indices.Length; i += 3) + { + Vector3 a = _points[_indices[i]].Position; + Vector3 b = _points[_indices[i + 1]].Position; + Vector3 c = _points[_indices[i + 2]].Position; + var normal = Vector3.Normalize(Vector3.Cross(c - b, a - b)); + + _points[_indices[i]].Normal += normal; + _points[_indices[i + 1]].Normal += normal; + _points[_indices[i + 2]].Normal += normal; + } + + for (int i = 0; i < _points.Length; i++) + { + _points[i].Normal = Vector3.Normalize(_points[i].Normal); + _maxY = Math.Max(_maxY, _points[i].Position.Y); + _minY = Math.Min(_minY, _points[i].Position.Y); + } + } + + var api = _context.Api; + var device = _context.Device; + var vertShaderData = GetShader(false); + var fragShaderData = GetShader(true); + + fixed (byte* ptr = vertShaderData) + { + var shaderCreateInfo = new ShaderModuleCreateInfo() + { + SType = StructureType.ShaderModuleCreateInfo, + CodeSize = (nuint)vertShaderData.Length, + PCode = (uint*)ptr, + }; + + api.CreateShaderModule(device, shaderCreateInfo, null, out _vertShader); + } + + fixed (byte* ptr = fragShaderData) + { + var shaderCreateInfo = new ShaderModuleCreateInfo() + { + SType = StructureType.ShaderModuleCreateInfo, + CodeSize = (nuint)fragShaderData.Length, + PCode = (uint*)ptr, + }; + + api.CreateShaderModule(device, shaderCreateInfo, null, out _fragShader); + } + + CreateBuffers(); + } + + private byte[] GetShader(bool fragment) + { + var name = typeof(VulkanContent).Assembly.GetManifestResourceNames() + .First(x => x.Contains((fragment ? "frag" : "vert") + ".spirv")); + using (var sr = typeof(VulkanContent).Assembly.GetManifestResourceStream(name)) + { + using (var mem = new MemoryStream()) + { + sr.CopyTo(mem); + return mem.ToArray(); + } + } + } + + private PixelSize? _previousImageSize = PixelSize.Empty; + + + public void Render(VulkanImage image, + double yaw, double pitch, double roll, double disco) + { + + var api = _context.Api; + + if (image.Size != _previousImageSize) + CreateTemporalObjects(image.Size); + + _previousImageSize = image.Size; + + + var model = Matrix4x4.CreateFromYawPitchRoll((float)yaw, (float)pitch, (float)roll); + var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0)); + var projection = + Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)((float)image.Size.Width / image.Size.Height), + 0.01f, 1000); + + var vertexConstant = new VertextPushConstant() + { + Disco = (float)disco, + MinY = _minY, + MaxY = _maxY, + Model = model, + Time = (float)St.Elapsed.TotalSeconds + }; + + var commandBuffer = _context.Pool.CreateCommandBuffer(); + commandBuffer.BeginRecording(); + + _colorAttachment.TransitionLayout(commandBuffer.InternalHandle, + ImageLayout.Undefined, AccessFlags.None, + ImageLayout.ColorAttachmentOptimal, AccessFlags.ColorAttachmentWriteBit); + + var commandBufferHandle = new CommandBuffer(commandBuffer.Handle); + + api.CmdSetViewport(commandBufferHandle, 0, 1, + new Viewport() + { + Width = (float)image.Size.Width, + Height = (float)image.Size.Height, + MaxDepth = 1, + MinDepth = 0, + X = 0, + Y = 0 + }); + + var scissor = new Rect2D + { + Extent = new Extent2D((uint?)image.Size.Width, (uint?)image.Size.Height) + }; + + api.CmdSetScissor(commandBufferHandle, 0, 1, &scissor); + + var clearColor = new ClearValue(new ClearColorValue(1, 0, 0, 0.1f), new ClearDepthStencilValue(1, 0)); + + var clearValues = new[] { clearColor, clearColor }; + + + fixed (ClearValue* clearValue = clearValues) + { + var beginInfo = new RenderPassBeginInfo() + { + SType = StructureType.RenderPassBeginInfo, + RenderPass = _renderPass, + Framebuffer = _framebuffer, + RenderArea = new Rect2D(new Offset2D(0, 0), new Extent2D((uint?)image.Size.Width, (uint?)image.Size.Height)), + ClearValueCount = 2, + PClearValues = clearValue + }; + + api.CmdBeginRenderPass(commandBufferHandle, beginInfo, SubpassContents.Inline); + } + + api.CmdBindPipeline(commandBufferHandle, PipelineBindPoint.Graphics, _pipeline); + + var dset = _descriptorSet; + api.CmdBindDescriptorSets(commandBufferHandle, PipelineBindPoint.Graphics, + _pipelineLayout,0,1, &dset, null); + + api.CmdPushConstants(commandBufferHandle, _pipelineLayout, ShaderStageFlags.ShaderStageVertexBit | ShaderStageFlags.FragmentBit, 0, + (uint)Marshal.SizeOf(), &vertexConstant); + api.CmdBindVertexBuffers(commandBufferHandle, 0, 1, _vertexBuffer, 0); + api.CmdBindIndexBuffer(commandBufferHandle, _indexBuffer, 0, IndexType.Uint16); + + api.CmdDrawIndexed(commandBufferHandle, (uint)_indices.Length, 1, 0, 0, 0); + + + api.CmdEndRenderPass(commandBufferHandle); + + _colorAttachment.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit); + image.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit); + + + var srcBlitRegion = new ImageBlit + { + SrcOffsets = new ImageBlit.SrcOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D(image.Size.Width, image.Size.Height, 1), + }, + DstOffsets = new ImageBlit.DstOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D(image.Size.Width, image.Size.Height, 1), + }, + SrcSubresource = + new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ImageAspectColorBit, + BaseArrayLayer = 0, + LayerCount = 1, + MipLevel = 0 + }, + DstSubresource = new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ImageAspectColorBit, + BaseArrayLayer = 0, + LayerCount = 1, + MipLevel = 0 + } + }; + + api.CmdBlitImage(commandBuffer.InternalHandle, _colorAttachment.InternalHandle.Value, + ImageLayout.TransferSrcOptimal, + image.InternalHandle.Value, ImageLayout.TransferDstOptimal, 1, srcBlitRegion, Filter.Linear); + + commandBuffer.Submit(); + } + + public unsafe void Dispose() + { + if (_isInit) + { + var api = _context.Api; + var device = _context.Device; + + DestroyTemporalObjects(); + + api.DestroyShaderModule(device, _vertShader, null); + api.DestroyShaderModule(device, _fragShader, null); + + api.DestroyBuffer(device, _vertexBuffer, null); + api.FreeMemory(device, _vertexBufferMemory, null); + + api.DestroyBuffer(device, _indexBuffer, null); + api.FreeMemory(device, _indexBufferMemory, null); + + + } + + _isInit = false; + } + + public unsafe void DestroyTemporalObjects() + { + if (_isInit) + { + if (_renderPass.Handle != 0) + { + var api = _context.Api; + var device = _context.Device; + api.FreeDescriptorSets(_context.Device, _context.DescriptorPool, new[] { _descriptorSet }); + + api.DestroyImageView(device, _depthImageView, null); + api.DestroyImage(device, _depthImage, null); + api.FreeMemory(device, _depthImageMemory, null); + + api.DestroyFramebuffer(device, _framebuffer, null); + api.DestroyPipeline(device, _pipeline, null); + api.DestroyPipelineLayout(device, _pipelineLayout, null); + api.DestroyRenderPass(device, _renderPass, null); + api.DestroyDescriptorSetLayout(device, _descriptorSetLayout, null); + + api.DestroyBuffer(device, _uniformBuffer, null); + api.FreeMemory(device, _uniformBufferMemory, null); + _colorAttachment?.Dispose(); + + _colorAttachment = null; + _depthImage = default; + _depthImageView = default; + _depthImageView = default; + _framebuffer = default; + _pipeline = default; + _renderPass = default; + _pipelineLayout = default; + _descriptorSetLayout = default; + _uniformBuffer = default; + _uniformBufferMemory = default; + } + } + } + + private unsafe void CreateDepthAttachment(PixelSize size) + { + var imageCreateInfo = new ImageCreateInfo + { + SType = StructureType.ImageCreateInfo, + ImageType = ImageType.ImageType2D, + Format = Format.D32Sfloat, + Extent = + new Extent3D((uint?)size.Width, + (uint?)size.Height, 1), + MipLevels = 1, + ArrayLayers = 1, + Samples = SampleCountFlags.SampleCount1Bit, + Tiling = ImageTiling.Optimal, + Usage = ImageUsageFlags.ImageUsageDepthStencilAttachmentBit, + SharingMode = SharingMode.Exclusive, + InitialLayout = ImageLayout.Undefined, + Flags = ImageCreateFlags.ImageCreateMutableFormatBit + }; + + var api = _context.Api; + var device = _context.Device; + api + .CreateImage(device, imageCreateInfo, null, out _depthImage).ThrowOnError(); + + api.GetImageMemoryRequirements(device, _depthImage, + out var memoryRequirements); + + var memoryAllocateInfo = new MemoryAllocateInfo + { + SType = StructureType.MemoryAllocateInfo, + AllocationSize = memoryRequirements.Size, + MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api, + _context.PhysicalDevice, + memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit) + }; + + api.AllocateMemory(device, memoryAllocateInfo, null, + out _depthImageMemory).ThrowOnError(); + + api.BindImageMemory(device, _depthImage, _depthImageMemory, 0); + + var componentMapping = new ComponentMapping( + ComponentSwizzle.R, + ComponentSwizzle.G, + ComponentSwizzle.B, + ComponentSwizzle.A); + + var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectDepthBit, + 0, 1, 0, 1); + + var imageViewCreateInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = _depthImage, + ViewType = ImageViewType.ImageViewType2D, + Format = Format.D32Sfloat, + Components = componentMapping, + SubresourceRange = subresourceRange + }; + + api + .CreateImageView(device, imageViewCreateInfo, null, out _depthImageView) + .ThrowOnError(); + } + + private unsafe void CreateTemporalObjects(PixelSize size) + { + DestroyTemporalObjects(); + + var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0)); + var projection = + Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)((float)size.Width / size.Height), + 0.01f, 1000); + + _colorAttachment = new VulkanImage(_context, (uint)Format.R8G8B8A8Unorm, size, false); + CreateDepthAttachment(size); + + var api = _context.Api; + var device = _context.Device; + + // create renderpasses + var colorAttachment = new AttachmentDescription() + { + Format = Format.R8G8B8A8Unorm, + Samples = SampleCountFlags.SampleCount1Bit, + LoadOp = AttachmentLoadOp.Clear, + StoreOp = AttachmentStoreOp.Store, + InitialLayout = ImageLayout.Undefined, + FinalLayout = ImageLayout.ColorAttachmentOptimal, + StencilLoadOp = AttachmentLoadOp.DontCare, + StencilStoreOp = AttachmentStoreOp.DontCare + }; + + var depthAttachment = new AttachmentDescription() + { + Format = Format.D32Sfloat, + Samples = SampleCountFlags.SampleCount1Bit, + LoadOp = AttachmentLoadOp.Clear, + StoreOp = AttachmentStoreOp.DontCare, + InitialLayout = ImageLayout.Undefined, + FinalLayout = ImageLayout.DepthStencilAttachmentOptimal, + StencilLoadOp = AttachmentLoadOp.DontCare, + StencilStoreOp = AttachmentStoreOp.DontCare + }; + + var subpassDependency = new SubpassDependency() + { + SrcSubpass = Vk.SubpassExternal, + DstSubpass = 0, + SrcStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit, + SrcAccessMask = 0, + DstStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit, + DstAccessMask = AccessFlags.AccessColorAttachmentWriteBit + }; + + var colorAttachmentReference = new AttachmentReference() + { + Attachment = 0, Layout = ImageLayout.ColorAttachmentOptimal + }; + + var depthAttachmentReference = new AttachmentReference() + { + Attachment = 1, Layout = ImageLayout.DepthStencilAttachmentOptimal + }; + + var subpassDescription = new SubpassDescription() + { + PipelineBindPoint = PipelineBindPoint.Graphics, + ColorAttachmentCount = 1, + PColorAttachments = &colorAttachmentReference, + PDepthStencilAttachment = &depthAttachmentReference + }; + + var attachments = new[] { colorAttachment, depthAttachment }; + + fixed (AttachmentDescription* atPtr = attachments) + { + var renderPassCreateInfo = new RenderPassCreateInfo() + { + SType = StructureType.RenderPassCreateInfo, + AttachmentCount = (uint)attachments.Length, + PAttachments = atPtr, + SubpassCount = 1, + PSubpasses = &subpassDescription, + DependencyCount = 1, + PDependencies = &subpassDependency + }; + + api.CreateRenderPass(device, renderPassCreateInfo, null, out _renderPass).ThrowOnError(); + + + // create framebuffer + var frameBufferAttachments = new[] { new ImageView(_colorAttachment.ViewHandle), _depthImageView }; + + fixed (ImageView* frAtPtr = frameBufferAttachments) + { + var framebufferCreateInfo = new FramebufferCreateInfo() + { + SType = StructureType.FramebufferCreateInfo, + RenderPass = _renderPass, + AttachmentCount = (uint)frameBufferAttachments.Length, + PAttachments = frAtPtr, + Width = (uint)size.Width, + Height = (uint)size.Height, + Layers = 1 + }; + + api.CreateFramebuffer(device, framebufferCreateInfo, null, out _framebuffer).ThrowOnError(); + } + } + + // Create pipeline + var pname = Marshal.StringToHGlobalAnsi("main"); + var vertShaderStageInfo = new PipelineShaderStageCreateInfo() + { + SType = StructureType.PipelineShaderStageCreateInfo, + Stage = ShaderStageFlags.ShaderStageVertexBit, + Module = _vertShader, + PName = (byte*)pname, + }; + var fragShaderStageInfo = new PipelineShaderStageCreateInfo() + { + SType = StructureType.PipelineShaderStageCreateInfo, + Stage = ShaderStageFlags.ShaderStageFragmentBit, + Module = _fragShader, + PName = (byte*)pname, + }; + + var stages = new[] { vertShaderStageInfo, fragShaderStageInfo }; + + var bindingDescription = Vertex.VertexInputBindingDescription; + var attributeDescription = Vertex.VertexInputAttributeDescription; + + fixed (VertexInputAttributeDescription* attrPtr = attributeDescription) + { + var vertextInputInfo = new PipelineVertexInputStateCreateInfo() + { + SType = StructureType.PipelineVertexInputStateCreateInfo, + VertexAttributeDescriptionCount = (uint)attributeDescription.Length, + VertexBindingDescriptionCount = 1, + PVertexAttributeDescriptions = attrPtr, + PVertexBindingDescriptions = &bindingDescription + }; + + var inputAssembly = new PipelineInputAssemblyStateCreateInfo() + { + SType = StructureType.PipelineInputAssemblyStateCreateInfo, + Topology = PrimitiveTopology.TriangleList, + PrimitiveRestartEnable = false + }; + + var viewport = new Viewport() + { + X = 0, + Y = 0, + Width = (float)size.Width, + Height = (float)size.Height, + MinDepth = 0, + MaxDepth = 1 + }; + + var scissor = new Rect2D() + { + Offset = new Offset2D(0, 0), Extent = new Extent2D((uint)viewport.Width, (uint)viewport.Height) + }; + + var pipelineViewPortCreateInfo = new PipelineViewportStateCreateInfo() + { + SType = StructureType.PipelineViewportStateCreateInfo, + ViewportCount = 1, + PViewports = &viewport, + ScissorCount = 1, + PScissors = &scissor + }; + + var rasterizerStateCreateInfo = new PipelineRasterizationStateCreateInfo() + { + SType = StructureType.PipelineRasterizationStateCreateInfo, + DepthClampEnable = false, + RasterizerDiscardEnable = false, + PolygonMode = PolygonMode.Fill, + LineWidth = 1, + CullMode = CullModeFlags.CullModeNone, + DepthBiasEnable = false + }; + + var multisampleStateCreateInfo = new PipelineMultisampleStateCreateInfo() + { + SType = StructureType.PipelineMultisampleStateCreateInfo, + SampleShadingEnable = false, + RasterizationSamples = SampleCountFlags.SampleCount1Bit + }; + + var depthStencilCreateInfo = new PipelineDepthStencilStateCreateInfo() + { + SType = StructureType.PipelineDepthStencilStateCreateInfo, + StencilTestEnable = false, + DepthCompareOp = CompareOp.Less, + DepthTestEnable = true, + DepthWriteEnable = true, + DepthBoundsTestEnable = false, + }; + + var colorBlendAttachmentState = new PipelineColorBlendAttachmentState() + { + ColorWriteMask = ColorComponentFlags.ColorComponentABit | + ColorComponentFlags.ColorComponentRBit | + ColorComponentFlags.ColorComponentGBit | + ColorComponentFlags.ColorComponentBBit, + BlendEnable = false + }; + + var colorBlendState = new PipelineColorBlendStateCreateInfo() + { + SType = StructureType.PipelineColorBlendStateCreateInfo, + LogicOpEnable = false, + AttachmentCount = 1, + PAttachments = &colorBlendAttachmentState + }; + + var dynamicStates = new DynamicState[] { DynamicState.Viewport, DynamicState.Scissor }; + + fixed (DynamicState* states = dynamicStates) + { + var dynamicStateCreateInfo = new PipelineDynamicStateCreateInfo() + { + SType = StructureType.PipelineDynamicStateCreateInfo, + DynamicStateCount = (uint)dynamicStates.Length, + PDynamicStates = states + }; + + var vertexPushConstantRange = new PushConstantRange() + { + Offset = 0, + Size = (uint)Marshal.SizeOf(), + StageFlags = ShaderStageFlags.ShaderStageVertexBit + }; + + var fragPushConstantRange = new PushConstantRange() + { + //Offset = vertexPushConstantRange.Size, + Size = (uint)Marshal.SizeOf(), + StageFlags = ShaderStageFlags.ShaderStageFragmentBit + }; + + var layoutBindingInfo = new DescriptorSetLayoutBinding + { + Binding = 0, + StageFlags = ShaderStageFlags.VertexBit, + DescriptorCount = 1, + DescriptorType = DescriptorType.UniformBuffer, + }; + + var layoutInfo = new DescriptorSetLayoutCreateInfo + { + SType = StructureType.DescriptorSetLayoutCreateInfo, + BindingCount = 1, + PBindings = &layoutBindingInfo + }; + + api.CreateDescriptorSetLayout(device, &layoutInfo, null, out _descriptorSetLayout).ThrowOnError(); + + var projView = view * projection; + VulkanBufferHelper.AllocateBuffer(_context, BufferUsageFlags.UniformBufferBit, + out _uniformBuffer, + out _uniformBufferMemory, new[] + { + new UniformBuffer + { + Projection = projView + } + }); + + var descriptorSetLayout = _descriptorSetLayout; + var descriptorCreateInfo = new DescriptorSetAllocateInfo + { + SType = StructureType.DescriptorSetAllocateInfo, + DescriptorPool = _context.DescriptorPool, + DescriptorSetCount = 1, + PSetLayouts = &descriptorSetLayout + }; + api.AllocateDescriptorSets(device, &descriptorCreateInfo, out _descriptorSet).ThrowOnError(); + + var descriptorBufferInfo = new DescriptorBufferInfo + { + Buffer = _uniformBuffer, + Range = (ulong)Unsafe.SizeOf(), + }; + var descriptorWrite = new WriteDescriptorSet + { + SType = StructureType.WriteDescriptorSet, + DstSet = _descriptorSet, + DescriptorType = DescriptorType.UniformBuffer, + DescriptorCount = 1, + PBufferInfo = &descriptorBufferInfo, + }; + api.UpdateDescriptorSets(device, 1, &descriptorWrite, 0, null); + + var constants = new[] { vertexPushConstantRange, fragPushConstantRange }; + + fixed (PushConstantRange* constant = constants) + { + var setLayout = _descriptorSetLayout; + var pipelineLayoutCreateInfo = new PipelineLayoutCreateInfo() + { + SType = StructureType.PipelineLayoutCreateInfo, + PushConstantRangeCount = (uint)constants.Length, + PPushConstantRanges = constant, + SetLayoutCount = 1, + PSetLayouts = &setLayout + }; + + api.CreatePipelineLayout(device, pipelineLayoutCreateInfo, null, out _pipelineLayout) + .ThrowOnError(); + } + + + fixed (PipelineShaderStageCreateInfo* stPtr = stages) + { + var pipelineCreateInfo = new GraphicsPipelineCreateInfo() + { + SType = StructureType.GraphicsPipelineCreateInfo, + StageCount = 2, + PStages = stPtr, + PVertexInputState = &vertextInputInfo, + PInputAssemblyState = &inputAssembly, + PViewportState = &pipelineViewPortCreateInfo, + PRasterizationState = &rasterizerStateCreateInfo, + PMultisampleState = &multisampleStateCreateInfo, + PDepthStencilState = &depthStencilCreateInfo, + PColorBlendState = &colorBlendState, + PDynamicState = &dynamicStateCreateInfo, + Layout = _pipelineLayout, + RenderPass = _renderPass, + Subpass = 0, + BasePipelineHandle = _pipeline.Handle != 0 ? _pipeline : new Pipeline(), + BasePipelineIndex = _pipeline.Handle != 0 ? 0 : -1 + }; + + api.CreateGraphicsPipelines(device, new PipelineCache(), 1, &pipelineCreateInfo, null, + out _pipeline).ThrowOnError(); + } + } + } + + Marshal.FreeHGlobal(pname); + _isInit = true; + } + + private unsafe void CreateBuffers() + { + VulkanBufferHelper.AllocateBuffer(_context, BufferUsageFlags.VertexBufferBit, out _vertexBuffer, + out _vertexBufferMemory, _points); + VulkanBufferHelper.AllocateBuffer(_context, BufferUsageFlags.IndexBufferBit, out _indexBuffer, + out _indexBufferMemory, _indices); + } + + private static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits, + MemoryPropertyFlags flags) + { + api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties); + + for (var i = 0; i < properties.MemoryTypeCount; i++) + { + var type = properties.MemoryTypes[i]; + + if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i; + } + + return -1; + } + + + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct Vertex + { + public Vector3 Position; + public Vector3 Normal; + + public static unsafe VertexInputBindingDescription VertexInputBindingDescription + { + get + { + return new VertexInputBindingDescription() + { + Binding = 0, + Stride = (uint)Marshal.SizeOf(), + InputRate = VertexInputRate.Vertex + }; + } + } + + public static unsafe VertexInputAttributeDescription[] VertexInputAttributeDescription + { + get + { + return new VertexInputAttributeDescription[] + { + new VertexInputAttributeDescription + { + Binding = 0, + Location = 0, + Format = Format.R32G32B32Sfloat, + Offset = (uint)Marshal.OffsetOf("Position") + }, + new VertexInputAttributeDescription + { + Binding = 0, + Location = 1, + Format = Format.R32G32B32Sfloat, + Offset = (uint)Marshal.OffsetOf("Normal") + } + }; + } + } + } + + private readonly Vertex[] _points; + private readonly ushort[] _indices; + private readonly float _minY; + private readonly float _maxY; + + + static Stopwatch St = Stopwatch.StartNew(); + private bool _isInit; + private VulkanImage _colorAttachment; + private DescriptorSet _descriptorSet; + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct VertextPushConstant + { + public float MaxY; + public float MinY; + public float Time; + public float Disco; + public Matrix4x4 Model; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct UniformBuffer + { + public Matrix4x4 Projection; + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanContext.cs b/samples/GpuInterop/VulkanDemo/VulkanContext.cs new file mode 100644 index 0000000000..56041d6965 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanContext.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Avalonia.Vulkan; +using Silk.NET.Core; +using Silk.NET.Core.Native; +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; +using DxgiDevice = SharpDX.DXGI.Device; + +namespace GpuInterop.VulkanDemo; + +public unsafe class VulkanContext : IDisposable +{ + public Vk Api { get; init; } + public Instance Instance { get; init; } + public PhysicalDevice PhysicalDevice { get; init; } + public Device Device { get; init; } + public Queue Queue { get; init; } + public uint QueueFamilyIndex { get; init; } + public VulkanCommandBufferPool Pool { get; init; } + public GRContext GrContext { get; init; } + public DescriptorPool DescriptorPool { get; init; } + public D3DDevice? D3DDevice { get; init; } + + public static (VulkanContext? result, string info) TryCreate(ICompositionGpuInterop gpuInterop) + { + using var appName = new ByteString("GpuInterop"); + using var engineName = new ByteString("Test"); + var applicationInfo = new ApplicationInfo + { + SType = StructureType.ApplicationInfo, + PApplicationName = appName, + ApiVersion = new Version32(1, 1, 0), + PEngineName = appName, + EngineVersion = new Version32(1, 0, 0), + ApplicationVersion = new Version32(1, 0, 0) + }; + + var enabledExtensions = new List() + { + "VK_KHR_get_physical_device_properties2", + "VK_KHR_external_memory_capabilities", + "VK_KHR_external_semaphore_capabilities" + }; + + var enabledLayers = new List(); + + Vk api = Vk.GetApi(); + enabledExtensions.Add("VK_EXT_debug_utils"); + if (IsLayerAvailable(api, "VK_LAYER_KHRONOS_validation")) + enabledLayers.Add("VK_LAYER_KHRONOS_validation"); + + + Instance vkInstance = default; + Silk.NET.Vulkan.PhysicalDevice physicalDevice = default; + Device device = default; + DescriptorPool descriptorPool = default; + VulkanCommandBufferPool? pool = null; + GRContext? grContext = null; + try + { + using var pRequiredExtensions = new ByteStringList(enabledExtensions); + using var pEnabledLayers = new ByteStringList(enabledLayers); + api.CreateInstance(new InstanceCreateInfo + { + SType = StructureType.InstanceCreateInfo, + PApplicationInfo = &applicationInfo, + PpEnabledExtensionNames = pRequiredExtensions, + EnabledExtensionCount = pRequiredExtensions.UCount, + PpEnabledLayerNames = pEnabledLayers, + EnabledLayerCount = pEnabledLayers.UCount + }, null, out vkInstance).ThrowOnError(); + + + if (api.TryGetInstanceExtension(vkInstance, out ExtDebugUtils debugUtils)) + { + var debugCreateInfo = new DebugUtilsMessengerCreateInfoEXT + { + SType = StructureType.DebugUtilsMessengerCreateInfoExt, + MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityVerboseBitExt | + DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityWarningBitExt | + DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityErrorBitExt, + MessageType = DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeGeneralBitExt | + DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeValidationBitExt | + DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypePerformanceBitExt, + PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(LogCallback), + }; + + debugUtils.CreateDebugUtilsMessenger(vkInstance, debugCreateInfo, null, out var messenger); + } + + var requireDeviceExtensions = new List + { + "VK_KHR_external_memory", + "VK_KHR_external_semaphore" + }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (!gpuInterop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .D3D11TextureGlobalSharedHandle) + ) + return (null, "Image sharing is not supported by the current backend"); + requireDeviceExtensions.Add(KhrExternalMemoryWin32.ExtensionName); + requireDeviceExtensions.Add(KhrExternalSemaphoreWin32.ExtensionName); + requireDeviceExtensions.Add("VK_KHR_dedicated_allocation"); + requireDeviceExtensions.Add("VK_KHR_get_memory_requirements2"); + } + else + { + if (!gpuInterop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .VulkanOpaquePosixFileDescriptor) + || !gpuInterop.SupportedSemaphoreTypes.Contains(KnownPlatformGraphicsExternalSemaphoreHandleTypes + .VulkanOpaquePosixFileDescriptor) + ) + return (null, "Image sharing is not supported by the current backend"); + requireDeviceExtensions.Add(KhrExternalMemoryFd.ExtensionName); + requireDeviceExtensions.Add(KhrExternalSemaphoreFd.ExtensionName); + } + + uint count = 0; + api.EnumeratePhysicalDevices(vkInstance, ref count, null).ThrowOnError(); + var physicalDevices = stackalloc PhysicalDevice[(int)count]; + api.EnumeratePhysicalDevices(vkInstance, ref count, physicalDevices) + .ThrowOnError(); + + for (uint c = 0; c < count; c++) + { + if (requireDeviceExtensions.Any(ext => !api.IsDeviceExtensionPresent(physicalDevices[c], ext))) + continue; + + var physicalDeviceIDProperties = new PhysicalDeviceIDProperties() + { + SType = StructureType.PhysicalDeviceIDProperties + }; + var physicalDeviceProperties2 = new PhysicalDeviceProperties2() + { + SType = StructureType.PhysicalDeviceProperties2, + PNext = &physicalDeviceIDProperties + }; + api.GetPhysicalDeviceProperties2(physicalDevices[c], &physicalDeviceProperties2); + + if (gpuInterop.DeviceLuid != null && physicalDeviceIDProperties.DeviceLuidvalid) + { + if (!new Span(physicalDeviceIDProperties.DeviceLuid, 8) + .SequenceEqual(gpuInterop.DeviceLuid)) + continue; + } + else if (gpuInterop.DeviceUuid != null) + { + if (!new Span(physicalDeviceIDProperties.DeviceUuid, 16) + .SequenceEqual(gpuInterop?.DeviceUuid)) + continue; + } + + physicalDevice = physicalDevices[c]; + + var name = Marshal.PtrToStringAnsi(new IntPtr(physicalDeviceProperties2.Properties.DeviceName))!; + + + uint queueFamilyCount = 0; + api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, ref queueFamilyCount, null); + var familyProperties = stackalloc QueueFamilyProperties[(int)queueFamilyCount]; + api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, ref queueFamilyCount, familyProperties); + for (uint queueFamilyIndex = 0; queueFamilyIndex < queueFamilyCount; queueFamilyIndex++) + { + var family = familyProperties[c]; + if (!family.QueueFlags.HasAllFlags(QueueFlags.GraphicsBit)) + continue; + + + var queuePriorities = stackalloc float[(int)family.QueueCount]; + + for (var i = 0; i < family.QueueCount; i++) + queuePriorities[i] = 1f; + + var features = new PhysicalDeviceFeatures(); + + var queueCreateInfo = new DeviceQueueCreateInfo + { + SType = StructureType.DeviceQueueCreateInfo, + QueueFamilyIndex = queueFamilyIndex, + QueueCount = family.QueueCount, + PQueuePriorities = queuePriorities + }; + + using var pEnabledDeviceExtensions = new ByteStringList(requireDeviceExtensions); + var deviceCreateInfo = new DeviceCreateInfo + { + SType = StructureType.DeviceCreateInfo, + QueueCreateInfoCount = 1, + PQueueCreateInfos = &queueCreateInfo, + PpEnabledExtensionNames = pEnabledDeviceExtensions, + EnabledExtensionCount = pEnabledDeviceExtensions.UCount, + PEnabledFeatures = &features + }; + + api.CreateDevice(physicalDevice, in deviceCreateInfo, null, out device) + .ThrowOnError(); + + api.GetDeviceQueue(device, queueFamilyIndex, 0, out var queue); + + var descriptorPoolSize = new DescriptorPoolSize + { + Type = DescriptorType.UniformBuffer, DescriptorCount = 16 + }; + var descriptorPoolInfo = new DescriptorPoolCreateInfo + { + SType = StructureType.DescriptorPoolCreateInfo, + PoolSizeCount = 1, + PPoolSizes = &descriptorPoolSize, + MaxSets = 16, + Flags = DescriptorPoolCreateFlags.FreeDescriptorSetBit + }; + + api.CreateDescriptorPool(device, &descriptorPoolInfo, null, out descriptorPool) + .ThrowOnError(); + + pool = new VulkanCommandBufferPool(api, device, queue, queueFamilyIndex); + grContext = GRContext.CreateVulkan(new GRVkBackendContext + { + 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); + } + }); + + + D3DDevice? d3dDevice = null; + if (physicalDeviceIDProperties.DeviceLuidvalid && + RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + d3dDevice = D3DMemoryHelper.CreateDeviceByLuid( + new Span(physicalDeviceIDProperties.DeviceLuid, 8)); + + var dxgiDevice = d3dDevice?.QueryInterface(); + return (new VulkanContext + { + Api = api, + Device = device, + Instance = vkInstance, + PhysicalDevice = physicalDevice, + Queue = queue, + QueueFamilyIndex = queueFamilyIndex, + Pool = pool, + DescriptorPool = descriptorPool, + GrContext = grContext, + D3DDevice = d3dDevice + }, name); + } + return (null, "No suitable device queue found"); + } + + return (null, "Suitable device not found"); + + } + catch (Exception e) + { + return (null, e.ToString()); + } + finally + { + if (grContext == null && api != null) + { + pool?.Dispose(); + if (descriptorPool.Handle != default) + api.DestroyDescriptorPool(device, descriptorPool, null); + if (device.Handle != default) + api.DestroyDevice(device, null); + } + } + } + + private static unsafe bool IsLayerAvailable(Vk api, string layerName) + { + uint layerPropertiesCount; + + api.EnumerateInstanceLayerProperties(&layerPropertiesCount, null).ThrowOnError(); + + var layerProperties = new LayerProperties[layerPropertiesCount]; + + fixed (LayerProperties* pLayerProperties = layerProperties) + { + api.EnumerateInstanceLayerProperties(&layerPropertiesCount, layerProperties).ThrowOnError(); + + for (var i = 0; i < layerPropertiesCount; i++) + { + var currentLayerName = Marshal.PtrToStringAnsi((IntPtr)pLayerProperties[i].LayerName); + + if (currentLayerName == layerName) return true; + } + } + + return false; + } + + private static unsafe uint LogCallback(DebugUtilsMessageSeverityFlagsEXT messageSeverity, DebugUtilsMessageTypeFlagsEXT messageTypes, DebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) + { + if (messageSeverity != DebugUtilsMessageSeverityFlagsEXT.VerboseBitExt) + { + var message = Marshal.PtrToStringAnsi((nint)pCallbackData->PMessage); + Console.WriteLine(message); + } + + return Vk.False; + } + + public void Dispose() + { + D3DDevice?.Dispose(); + GrContext.Dispose(); + Pool.Dispose(); + Api.DestroyDescriptorPool(Device, DescriptorPool, null); + Api.DestroyDevice(Device, null); + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs b/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs new file mode 100644 index 0000000000..6a1cd641b3 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Silk.NET.Core; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; +using SilkNetDemo; + +namespace GpuInterop.VulkanDemo; + +public class VulkanDemoControl : DrawingSurfaceDemoBase +{ + private Instance _vkInstance; + private Vk _api; + + class VulkanResources : IAsyncDisposable + { + public VulkanContext Context { get; } + public VulkanSwapchain Swapchain { get; } + public VulkanContent Content { get; } + + public VulkanResources(VulkanContext context, VulkanSwapchain swapchain, VulkanContent content) + { + Context = context; + Swapchain = swapchain; + Content = content; + } + public async ValueTask DisposeAsync() + { + Context.Pool.FreeUsedCommandBuffers(); + Content.Dispose(); + await Swapchain.DisposeAsync(); + Context.Dispose(); + } + } + + protected override bool SupportsDisco => true; + + private VulkanResources? _resources; + + protected override (bool success, string info) InitializeGraphicsResources(Compositor compositor, + CompositionDrawingSurface compositionDrawingSurface, ICompositionGpuInterop gpuInterop) + { + var (context, info) = VulkanContext.TryCreate(gpuInterop); + if (context == null) + return (false, info); + try + { + var content = new VulkanContent(context); + _resources = new VulkanResources(context, + new VulkanSwapchain(context, gpuInterop, compositionDrawingSurface), content); + return (true, info); + } + catch(Exception e) + { + return (false, e.ToString()); + } + } + + protected override void FreeGraphicsResources() + { + _resources?.DisposeAsync(); + _resources = null; + } + + protected override unsafe void RenderFrame(PixelSize pixelSize) + { + if (_resources == null) + return; + using (_resources.Swapchain.BeginDraw(pixelSize, out var image)) + { + /* + var commandBuffer = _resources.Context.Pool.CreateCommandBuffer(); + commandBuffer.BeginRecording(); + image.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferDstOptimal, AccessFlags.None); + + var range = new ImageSubresourceRange + { + AspectMask = ImageAspectFlags.ColorBit, + LayerCount = 1, + LevelCount = 1, + BaseArrayLayer = 0, + BaseMipLevel = 0 + }; + var color = new ClearColorValue + { + Float32_0 = 1, Float32_1 = 0, Float32_2 = 0, Float32_3 = 1 + }; + _resources.Context.Api.CmdClearColorImage(commandBuffer.InternalHandle, image.InternalHandle.Value, ImageLayout.TransferDstOptimal, + &color, 1, &range); + commandBuffer.Submit();*/ + _resources.Content.Render(image, Yaw, Pitch, Roll, Disco); + } + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanExtensions.cs b/samples/GpuInterop/VulkanDemo/VulkanExtensions.cs new file mode 100644 index 0000000000..1a13ebd4c5 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanExtensions.cs @@ -0,0 +1,12 @@ +using System; +using Silk.NET.Vulkan; + +namespace SilkNetDemo; + +public static class VulkanExtensions +{ + public static void ThrowOnError(this Result result) + { + if (result != Result.Success) throw new Exception($"Unexpected API error \"{result}\"."); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanImage.cs b/samples/GpuInterop/VulkanDemo/VulkanImage.cs new file mode 100644 index 0000000000..e8854bfeb2 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanImage.cs @@ -0,0 +1,276 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Vulkan; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; +using SilkNetDemo; +using SkiaSharp; + +namespace GpuInterop.VulkanDemo; + +public unsafe class VulkanImage : IDisposable + { + private readonly VulkanContext _vk; + private readonly Instance _instance; + private readonly Device _device; + private readonly PhysicalDevice _physicalDevice; + private readonly VulkanCommandBufferPool _commandBufferPool; + private ImageLayout _currentLayout; + private AccessFlags _currentAccessFlags; + private ImageUsageFlags _imageUsageFlags { get; } + private ImageView? _imageView { get; set; } + private DeviceMemory _imageMemory { get; set; } + private SharpDX.Direct3D11.Texture2D? _d3dTexture2D; + private IntPtr _win32ShareHandle; + + internal Image? InternalHandle { get; private set; } + internal Format Format { get; } + internal ImageAspectFlags AspectFlags { get; private set; } + + public ulong Handle => InternalHandle?.Handle ?? 0; + public ulong ViewHandle => _imageView?.Handle ?? 0; + public uint UsageFlags => (uint) _imageUsageFlags; + public ulong MemoryHandle => _imageMemory.Handle; + public DeviceMemory DeviceMemory => _imageMemory; + public uint MipLevels { get; private set; } + public Vk Api { get; } + public PixelSize Size { get; } + public ulong MemorySize { get; private set; } + public uint CurrentLayout => (uint) _currentLayout; + + public VulkanImage(VulkanContext vk, uint format, PixelSize size, + bool exportable, uint mipLevels = 0) + { + _vk = vk; + _instance = vk.Instance; + _device = vk.Device; + _physicalDevice = vk.PhysicalDevice; + _commandBufferPool = vk.Pool; + Format = (Format)format; + Api = vk.Api; + Size = size; + MipLevels = 1;//mipLevels; + _imageUsageFlags = + ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit | + ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageSampledBit; + + //MipLevels = MipLevels != 0 ? MipLevels : (uint)Math.Floor(Math.Log(Math.Max(Size.Width, Size.Height), 2)); + + var handleType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + ExternalMemoryHandleTypeFlags.D3D11TextureKmtBit : + ExternalMemoryHandleTypeFlags.OpaqueFDBit; + var externalMemoryCreateInfo = new ExternalMemoryImageCreateInfo + { + SType = StructureType.ExternalMemoryImageCreateInfo, + HandleTypes = handleType + }; + + var imageCreateInfo = new ImageCreateInfo + { + PNext = exportable ? &externalMemoryCreateInfo : null, + SType = StructureType.ImageCreateInfo, + ImageType = ImageType.ImageType2D, + Format = Format, + Extent = + new Extent3D((uint?)Size.Width, + (uint?)Size.Height, 1), + MipLevels = MipLevels, + ArrayLayers = 1, + Samples = SampleCountFlags.SampleCount1Bit, + Tiling = Tiling, + Usage = _imageUsageFlags, + SharingMode = SharingMode.Exclusive, + InitialLayout = ImageLayout.Undefined, + Flags = ImageCreateFlags.ImageCreateMutableFormatBit + }; + + Api + .CreateImage(_device, imageCreateInfo, null, out var image).ThrowOnError(); + InternalHandle = image; + + Api.GetImageMemoryRequirements(_device, InternalHandle.Value, + out var memoryRequirements); + + + var fdExport = new ExportMemoryAllocateInfo + { + HandleTypes = handleType, SType = StructureType.ExportMemoryAllocateInfo + }; + var dedicatedAllocation = new MemoryDedicatedAllocateInfoKHR + { + SType = StructureType.MemoryDedicatedAllocateInfoKhr, + Image = image + }; + ImportMemoryWin32HandleInfoKHR handleImport = default; + if (exportable && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _d3dTexture2D = D3DMemoryHelper.CreateMemoryHandle(vk.D3DDevice, size, Format); + using var dxgi = _d3dTexture2D.QueryInterface(); + _win32ShareHandle = dxgi.SharedHandle; + handleImport = new ImportMemoryWin32HandleInfoKHR + { + PNext = &dedicatedAllocation, + SType = StructureType.ImportMemoryWin32HandleInfoKhr, + HandleType = ExternalMemoryHandleTypeFlags.D3D11TextureKmtBit, + Handle = _win32ShareHandle, + }; + } + + var memoryAllocateInfo = new MemoryAllocateInfo + { + PNext = + exportable ? RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? &handleImport : &fdExport : null, + SType = StructureType.MemoryAllocateInfo, + AllocationSize = memoryRequirements.Size, + MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex( + Api, + _physicalDevice, + memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit) + }; + + Api.AllocateMemory(_device, memoryAllocateInfo, null, + out var imageMemory).ThrowOnError(); + + _imageMemory = imageMemory; + + + MemorySize = memoryRequirements.Size; + + Api.BindImageMemory(_device, InternalHandle.Value, _imageMemory, 0).ThrowOnError(); + var componentMapping = new ComponentMapping( + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity); + + AspectFlags = ImageAspectFlags.ImageAspectColorBit; + + var subresourceRange = new ImageSubresourceRange(AspectFlags, 0, MipLevels, 0, 1); + + var imageViewCreateInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = InternalHandle.Value, + ViewType = ImageViewType.ImageViewType2D, + Format = Format, + Components = componentMapping, + SubresourceRange = subresourceRange + }; + + Api + .CreateImageView(_device, imageViewCreateInfo, null, out var imageView) + .ThrowOnError(); + + _imageView = imageView; + + _currentLayout = ImageLayout.Undefined; + + TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr); + } + + public int ExportFd() + { + if (!Api.TryGetDeviceExtension(_instance, _device, out var ext)) + throw new InvalidOperationException(); + var info = new MemoryGetFdInfoKHR + { + Memory = _imageMemory, + SType = StructureType.MemoryGetFDInfoKhr, + HandleType = ExternalMemoryHandleTypeFlags.OpaqueFDBit + }; + ext.GetMemoryF(_device, info, out var fd).ThrowOnError(); + return fd; + } + + public IPlatformHandle Export() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + new PlatformHandle(_win32ShareHandle, + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle) : + new PlatformHandle(new IntPtr(ExportFd()), + KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor); + + public ImageTiling Tiling => ImageTiling.Optimal; + + + + internal void TransitionLayout(CommandBuffer commandBuffer, + ImageLayout fromLayout, AccessFlags fromAccessFlags, + ImageLayout destinationLayout, AccessFlags destinationAccessFlags) + { + VulkanMemoryHelper.TransitionLayout(Api, commandBuffer, InternalHandle.Value, + fromLayout, + fromAccessFlags, + destinationLayout, destinationAccessFlags, + MipLevels); + + _currentLayout = destinationLayout; + _currentAccessFlags = destinationAccessFlags; + } + + internal void TransitionLayout(CommandBuffer commandBuffer, + ImageLayout destinationLayout, AccessFlags destinationAccessFlags) + => TransitionLayout(commandBuffer, _currentLayout, _currentAccessFlags, destinationLayout, + destinationAccessFlags); + + + internal void TransitionLayout(ImageLayout destinationLayout, AccessFlags destinationAccessFlags) + { + var commandBuffer = _commandBufferPool.CreateCommandBuffer(); + commandBuffer.BeginRecording(); + TransitionLayout(commandBuffer.InternalHandle, destinationLayout, destinationAccessFlags); + commandBuffer.EndRecording(); + commandBuffer.Submit(); + } + + public void TransitionLayout(uint destinationLayout, uint destinationAccessFlags) + { + TransitionLayout((ImageLayout)destinationLayout, (AccessFlags)destinationAccessFlags); + } + + public unsafe void Dispose() + { + Api.DestroyImageView(_device, _imageView.Value, null); + Api.DestroyImage(_device, InternalHandle.Value, null); + Api.FreeMemory(_device, _imageMemory, null); + + _imageView = default; + InternalHandle = default; + _imageMemory = default; + } + + public void SaveTexture(string path) + { + _vk.GrContext.ResetContext(); + var _image = this; + var imageInfo = new GRVkImageInfo() + { + CurrentQueueFamily = _vk.QueueFamilyIndex, + Format = (uint)_image.Format, + Image = _image.Handle, + ImageLayout = (uint)_image.CurrentLayout, + ImageTiling = (uint)_image.Tiling, + ImageUsageFlags = (uint)_image.UsageFlags, + LevelCount = _image.MipLevels, + SampleCount = 1, + Protected = false, + Alloc = new GRVkAlloc() + { + Memory = _image.MemoryHandle, Flags = 0, Offset = 0, Size = _image.MemorySize + } + }; + + using (var backendTexture = new GRBackendRenderTarget(_image.Size.Width, _image.Size.Height, 1, + imageInfo)) + using (var surface = SKSurface.Create(_vk.GrContext, backendTexture, + GRSurfaceOrigin.TopLeft, + SKColorType.Rgba8888, SKColorSpace.CreateSrgb())) + { + using var snap = surface.Snapshot(); + using var encoded = snap.Encode(); + using (var s = File.Create(path)) + encoded.SaveTo(s); + } + } + } diff --git a/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs b/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs new file mode 100644 index 0000000000..f6778610dc --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs @@ -0,0 +1,59 @@ +using Silk.NET.Vulkan; + +namespace GpuInterop.VulkanDemo; + +internal static class VulkanMemoryHelper +{ + internal static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits, + MemoryPropertyFlags flags) + { + api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties); + + for (var i = 0; i < properties.MemoryTypeCount; i++) + { + var type = properties.MemoryTypes[i]; + + if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i; + } + + return -1; + } + + internal static unsafe void TransitionLayout( + Vk api, + CommandBuffer commandBuffer, + Image image, + ImageLayout sourceLayout, + AccessFlags sourceAccessMask, + ImageLayout destinationLayout, + AccessFlags destinationAccessMask, + uint mipLevels) + { + var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, mipLevels, 0, 1); + + var barrier = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = sourceAccessMask, + DstAccessMask = destinationAccessMask, + OldLayout = sourceLayout, + NewLayout = destinationLayout, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = image, + SubresourceRange = subresourceRange + }; + + api.CmdPipelineBarrier( + commandBuffer, + PipelineStageFlags.PipelineStageAllCommandsBit, + PipelineStageFlags.PipelineStageAllCommandsBit, + 0, + 0, + null, + 0, + null, + 1, + barrier); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs b/samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs new file mode 100644 index 0000000000..279c313a27 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs @@ -0,0 +1,58 @@ +using System; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; +using SilkNetDemo; + +namespace GpuInterop.VulkanDemo; + +class VulkanSemaphorePair : IDisposable +{ + private readonly VulkanContext _resources; + + public unsafe VulkanSemaphorePair(VulkanContext resources, bool exportable) + { + _resources = resources; + + var semaphoreExportInfo = new ExportSemaphoreCreateInfo + { + SType = StructureType.ExportSemaphoreCreateInfo, + HandleTypes = ExternalSemaphoreHandleTypeFlags.OpaqueFDBit + }; + + var semaphoreCreateInfo = new SemaphoreCreateInfo + { + SType = StructureType.SemaphoreCreateInfo, + PNext = exportable ? &semaphoreExportInfo : null + }; + + resources.Api.CreateSemaphore(resources.Device, semaphoreCreateInfo, null, out var semaphore).ThrowOnError(); + ImageAvailableSemaphore = semaphore; + + resources.Api.CreateSemaphore(resources.Device, semaphoreCreateInfo, null, out semaphore).ThrowOnError(); + RenderFinishedSemaphore = semaphore; + } + + public int ExportFd(bool renderFinished) + { + if (!_resources.Api.TryGetDeviceExtension(_resources.Instance, _resources.Device, + out var ext)) + throw new InvalidOperationException(); + var info = new SemaphoreGetFdInfoKHR() + { + SType = StructureType.SemaphoreGetFDInfoKhr, + Semaphore = renderFinished ? RenderFinishedSemaphore : ImageAvailableSemaphore, + HandleType = ExternalSemaphoreHandleTypeFlags.OpaqueFDBit + }; + ext.GetSemaphoreF(_resources.Device, info, out var fd).ThrowOnError(); + return fd; + } + + internal Semaphore ImageAvailableSemaphore { get; } + internal Semaphore RenderFinishedSemaphore { get; } + + public unsafe void Dispose() + { + _resources.Api.DestroySemaphore(_resources.Device, ImageAvailableSemaphore, null); + _resources.Api.DestroySemaphore(_resources.Device, RenderFinishedSemaphore, null); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs b/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs new file mode 100644 index 0000000000..325c815ccb --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs @@ -0,0 +1,154 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Vulkan; +using Metsys.Bson; +using Silk.NET.Vulkan; +using SkiaSharp; + +namespace GpuInterop.VulkanDemo; + +class VulkanSwapchain : SwapchainBase +{ + private readonly VulkanContext _vk; + + public VulkanSwapchain(VulkanContext vk, ICompositionGpuInterop interop, CompositionDrawingSurface target) : base(interop, target) + { + _vk = vk; + } + + protected override VulkanSwapchainImage CreateImage(PixelSize size) + { + return new VulkanSwapchainImage(_vk, size, Interop, Target); + } + + public IDisposable BeginDraw(PixelSize size, out VulkanImage image) + { + _vk.Pool.FreeUsedCommandBuffers(); + var rv = BeginDrawCore(size, out var swapchainImage); + image = swapchainImage.Image; + return rv; + } +} + +class VulkanSwapchainImage : ISwapchainImage +{ + private readonly VulkanContext _vk; + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _target; + private readonly VulkanImage _image; + private readonly VulkanSemaphorePair _semaphorePair; + private ICompositionImportedGpuSemaphore? _availableSemaphore, _renderCompletedSemaphore; + private ICompositionImportedGpuImage? _importedImage; + private Task? _lastPresent; + public VulkanImage Image => _image; + private bool _initial = true; + + public VulkanSwapchainImage(VulkanContext vk, PixelSize size, ICompositionGpuInterop interop, CompositionDrawingSurface target) + { + _vk = vk; + _interop = interop; + _target = target; + Size = size; + _image = new VulkanImage(vk, (uint)Format.R8G8B8A8Unorm, size, true); + _semaphorePair = new VulkanSemaphorePair(vk, true); + } + + public async ValueTask DisposeAsync() + { + if (LastPresent != null) + await LastPresent; + if (_importedImage != null) + await _importedImage.DisposeAsync(); + if (_availableSemaphore != null) + await _availableSemaphore.DisposeAsync(); + if (_renderCompletedSemaphore != null) + await _renderCompletedSemaphore.DisposeAsync(); + _semaphorePair.Dispose(); + _image.Dispose(); + } + + public PixelSize Size { get; } + + public Task? LastPresent => _lastPresent; + + public void BeginDraw() + { + var buffer = _vk.Pool.CreateCommandBuffer(); + buffer.BeginRecording(); + + _image.TransitionLayout(buffer.InternalHandle, + ImageLayout.Undefined, AccessFlags.None, + ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessColorAttachmentReadBit); + + if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + buffer.Submit(null,null,null, null, new VulkanCommandBufferPool.VulkanCommandBuffer.KeyedMutexSubmitInfo + { + AcquireKey = 0, + DeviceMemory = _image.DeviceMemory + }); + else if (_initial) + { + _initial = false; + buffer.Submit(); + } + else + buffer.Submit(new[] { _semaphorePair.ImageAvailableSemaphore }, + new[] + { + PipelineStageFlags.AllGraphicsBit + }); + } + + + + public void Present() + { + var buffer = _vk.Pool.CreateCommandBuffer(); + buffer.BeginRecording(); + _image.TransitionLayout(buffer.InternalHandle, ImageLayout.TransferSrcOptimal, AccessFlags.TransferWriteBit); + + + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + buffer.Submit(null, null, null, null, + new VulkanCommandBufferPool.VulkanCommandBuffer.KeyedMutexSubmitInfo + { + DeviceMemory = _image.DeviceMemory, ReleaseKey = 1 + }); + } + else + buffer.Submit(null, null, new[] { _semaphorePair.RenderFinishedSemaphore }); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _availableSemaphore ??= _interop.ImportSemaphore(new PlatformHandle( + new IntPtr(_semaphorePair.ExportFd(false)), + KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor)); + + _renderCompletedSemaphore ??= _interop.ImportSemaphore(new PlatformHandle( + new IntPtr(_semaphorePair.ExportFd(true)), + KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor)); + } + + _importedImage ??= _interop.ImportImage(_image.Export(), + new PlatformGraphicsExternalImageProperties + { + Format = PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm, + Width = Size.Width, + Height = Size.Height, + MemorySize = _image.MemorySize + }); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + _lastPresent = _target.UpdateWithKeyedMutexAsync(_importedImage, 1, 0); + else + _lastPresent = _target.UpdateWithSemaphoresAsync(_importedImage, _renderCompletedSemaphore, _availableSemaphore); + } +} diff --git a/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs b/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs new file mode 100644 index 0000000000..5b06651b5f --- /dev/null +++ b/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Avalonia.Metadata; +using Avalonia.Rendering.Composition; + +namespace Avalonia.Platform; + +[Unstable] +public interface IExternalObjectsRenderInterfaceContextFeature +{ + /// + /// Returns the list of image handle types supported by the current GPU backend, see + /// + IReadOnlyList SupportedImageHandleTypes { get; } + + /// + /// Returns the list of semaphore types supported by the current GPU backend, see + /// + IReadOnlyList SupportedSemaphoreTypes { get; } + + IPlatformRenderInterfaceImportedImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties); + + IPlatformRenderInterfaceImportedImage ImportImage(ICompositionImportableSharedGpuContextImage image); + + IPlatformRenderInterfaceImportedSemaphore ImportSemaphore(IPlatformHandle handle); + CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType); + public byte[]? DeviceUuid { get; } + public byte[]? DeviceLuid { get; } +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedObject : IDisposable +{ + +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedImage : IPlatformRenderInterfaceImportedObject +{ + IBitmapImpl SnapshotWithKeyedMutex(uint acquireIndex, uint releaseIndex); + + IBitmapImpl SnapshotWithSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore, + IPlatformRenderInterfaceImportedSemaphore signalSemaphore); + + IBitmapImpl SnapshotWithAutomaticSync(); +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedSemaphore : IPlatformRenderInterfaceImportedObject +{ + +} diff --git a/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs index b6464dea58..66d4f083c2 100644 --- a/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs +++ b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace Avalonia.Platform; @@ -15,4 +16,12 @@ public static class OptionalFeatureProviderExtensions { public static T? TryGetFeature(this IOptionalFeatureProvider provider) where T : class => (T?)provider.TryGetFeature(typeof(T)); -} \ No newline at end of file + + public static bool TryGetFeature(this IOptionalFeatureProvider provider, [MaybeNullWhen(false)] out T rv) + where T : class + { + rv = provider.TryGetFeature(); + return rv != null; + } + +} diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 1828f24aff..89bf047401 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -210,5 +210,10 @@ namespace Avalonia.Platform /// /// An . IRenderTarget CreateRenderTarget(IEnumerable surfaces); + + /// + /// Indicates that the context is no longer usable. This method should be thread-safe + /// + bool IsLost { get; } } } diff --git a/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs new file mode 100644 index 0000000000..cad4ab2051 --- /dev/null +++ b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Avalonia.Platform; + +public struct PlatformGraphicsExternalImageProperties +{ + public int Width { get; set; } + public int Height { get; set; } + public PlatformGraphicsExternalImageFormat Format { get; set; } + public ulong MemorySize { get; set; } + public ulong MemoryOffset { get; set; } + public bool TopLeftOrigin { get; set; } +} + +public enum PlatformGraphicsExternalImageFormat +{ + R8G8B8A8UNorm, + B8G8R8A8UNorm +} + +/// +/// Describes various GPU memory handle types that are currently supported by Avalonia graphics backends +/// +public static class KnownPlatformGraphicsExternalImageHandleTypes +{ + /// + /// An DXGI global shared handle returned by IDXGIResource::GetSharedHandle D3D11_RESOURCE_MISC_SHARED or D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX flag. + /// The handle does not own the reference to the underlying video memory, so the provider should make sure that the resource is valid until + /// the handle has been successfully imported + /// + public const string D3D11TextureGlobalSharedHandle = nameof(D3D11TextureGlobalSharedHandle); + /// + /// A DXGI NT handle returned by IDXGIResource1::CreateSharedHandle for a texture created with D3D11_RESOURCE_MISC_SHARED_NTHANDLE or flag + /// + public const string D3D11TextureNtHandle = nameof(D3D11TextureNtHandle); + /// + /// A POSIX file descriptor that's exported by Vulkan using VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT or in a compatible way + /// + public const string VulkanOpaquePosixFileDescriptor = nameof(VulkanOpaquePosixFileDescriptor); +} + +/// +/// Describes various GPU semaphore handle types that are currently supported by Avalonia graphics backends +/// +public static class KnownPlatformGraphicsExternalSemaphoreHandleTypes +{ + /// + /// A POSIX file descriptor that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT or in a compatible way + /// + public const string VulkanOpaquePosixFileDescriptor = nameof(VulkanOpaquePosixFileDescriptor); + + /// + /// A NT handle that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT or in a compatible way + /// + public const string VulkanOpaqueNtHandle = nameof(VulkanOpaqueNtHandle); + + // A global shared handle that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT or in a compatible way + public const string VulkanOpaqueKmtHandle = nameof(VulkanOpaqueKmtHandle); + + /// A DXGI NT handle returned by ID3D12Device::CreateSharedHandle or ID3D11Fence::CreateSharedHandle + public const string Direct3D12FenceNtHandle = nameof(Direct3D12FenceNtHandle); +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index b2080aeb87..a2209db09f 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -28,7 +28,7 @@ public class CompositingRenderer : IRendererWithCompositor private HashSet _dirty = new(); private HashSet _recalculateChildren = new(); private bool _queuedUpdate; - private Action _update; + private Action _update; private bool _updating; internal CompositionTarget CompositionTarget; @@ -70,7 +70,7 @@ public class CompositingRenderer : IRendererWithCompositor if(_queuedUpdate) return; _queuedUpdate = true; - _compositor.InvokeBeforeNextCommit(_update); + _compositor.RequestCompositionUpdate(_update); } /// @@ -265,7 +265,7 @@ public class CompositingRenderer : IRendererWithCompositor SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); } - private void Update(Task batchCompletion) + private void Update() { if(_updating) return; diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs new file mode 100644 index 0000000000..2f7e261250 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition; + +public class CompositionDrawingSurface : CompositionSurface +{ + internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server; + internal CompositionDrawingSurface(Compositor compositor) : base(compositor, new ServerCompositionDrawingSurface(compositor.Server)) + { + } + + /// + /// Updates the surface contents using an imported memory image using a keyed mutex as the means of synchronization + /// + /// GPU image with new surface contents + /// The mutex key to wait for before accessing the image + /// The mutex key to release for after accessing the image + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateWithKeyedMutexAsync(ICompositionImportedGpuImage image, uint acquireIndex, uint releaseIndex) + { + var img = (CompositionImportedGpuImage)image; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithKeyedMutex(img, acquireIndex, releaseIndex)); + } + + /// + /// Updates the surface contents using an imported memory image using a semaphore pair as the means of synchronization + /// + /// GPU image with new surface contents + /// The semaphore to wait for before accessing the image + /// The semaphore to signal after accessing the image + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateWithSemaphoresAsync(ICompositionImportedGpuImage image, + ICompositionImportedGpuSemaphore waitForSemaphore, + ICompositionImportedGpuSemaphore signalSemaphore) + { + var img = (CompositionImportedGpuImage)image; + var wait = (CompositionImportedGpuSemaphore)waitForSemaphore; + var signal = (CompositionImportedGpuSemaphore)signalSemaphore; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithSemaphores(img, wait, signal)); + } + + /// + /// Updates the surface contents using an unspecified automatic means of synchronization + /// provided by the underlying platform + /// + /// GPU image with new surface contents + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateAsync(ICompositionImportedGpuImage image) + { + var img = (CompositionImportedGpuImage)image; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithAutomaticSync(img)); + } + + ~CompositionDrawingSurface() + { + Dispose(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs b/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs new file mode 100644 index 0000000000..ce728f86a2 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Metadata; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition; +public interface ICompositionGpuInterop +{ + /// + /// Returns the list of image handle types supported by the current GPU backend, see + /// + IReadOnlyList SupportedImageHandleTypes { get; } + + /// + /// Returns the list of semaphore types supported by the current GPU backend, see + /// + IReadOnlyList SupportedSemaphoreTypes { get; } + + /// + /// Returns the supported ways to synchronize access to the imported GPU image + /// + /// + CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType); + + /// + /// Asynchronously imports a texture. The returned object is immediately usable. + /// + ICompositionImportedGpuImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties); + + /// + /// Asynchronously imports a texture. The returned object is immediately usable. + /// If import operation fails, the caller is responsible for destroying the handle + /// + /// An image that belongs to the same GPU context or the same GPU context sharing group as one used by compositor + ICompositionImportedGpuImage ImportImage(ICompositionImportableSharedGpuContextImage image); + + /// + /// Asynchronously imports a semaphore object. The returned object is immediately usable. + /// If import operation fails, the caller is responsible for destroying the handle + /// + ICompositionImportedGpuSemaphore ImportSemaphore(IPlatformHandle handle); + + /// + /// Asynchronously imports a semaphore object. The returned object is immediately usable. + /// + /// A semaphore that belongs to the same GPU context or the same GPU context sharing group as one used by compositor + ICompositionImportedGpuImage ImportSemaphore(ICompositionImportableSharedGpuContextSemaphore image); + + /// + /// Indicates if the device context this instance is associated with is no longer available + /// + public bool IsLost { get; } + + /// + /// The LUID of the graphics adapter used by the compositor + /// + public byte[]? DeviceLuid { get; set; } + + /// + /// The UUID of the graphics adapter used by the compositor + /// + public byte[]? DeviceUuid { get; set; } +} + +[Flags] +public enum CompositionGpuImportedImageSynchronizationCapabilities +{ + /// + /// Pre-render and after-render semaphores must be provided alongside with the image + /// + Semaphores = 1, + /// + /// Image must be created with D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX or in other compatible way + /// + KeyedMutex = 2, + /// + /// Synchronization and ordering is somehow handled by the underlying platform + /// + Automatic = 4 +} + +/// +/// An imported GPU object that's usable by composition APIs +/// +public interface ICompositionGpuImportedObject : IAsyncDisposable +{ + /// + /// Tracks the import status of the object. Once the task is completed, + /// the user code is allowed to free the resource owner in case when a non-owning + /// sharing handle was used + /// + Task ImportCompeted { get; } + /// + /// Indicates if the device context this instance is associated with is no longer available + /// + bool IsLost { get; } +} + +/// +/// An imported GPU image object that's usable by composition APIs +/// +[NotClientImplementable] +public interface ICompositionImportedGpuImage : ICompositionGpuImportedObject +{ + +} + +/// +/// An imported GPU semaphore object that's usable by composition APIs +/// +[NotClientImplementable] +public interface ICompositionImportedGpuSemaphore : ICompositionGpuImportedObject +{ + +} + +/// +/// An GPU object descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextObject : IDisposable +{ +} + +/// +/// An GPU image descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextImage : IDisposable +{ +} + +/// +/// An GPU semaphore descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextSemaphore : IDisposable +{ +} + diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs b/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs new file mode 100644 index 0000000000..1643ec6e8d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition; + +internal class CompositionInterop : ICompositionGpuInterop +{ + private readonly Compositor _compositor; + private readonly IPlatformRenderInterfaceContext _context; + private readonly IExternalObjectsRenderInterfaceContextFeature _externalObjects; + + + public CompositionInterop( + Compositor compositor, + IExternalObjectsRenderInterfaceContextFeature externalObjects) + { + _compositor = compositor; + _context = compositor.Server.RenderInterface.Value; + DeviceLuid = externalObjects.DeviceLuid; + DeviceUuid = externalObjects.DeviceUuid; + _externalObjects = externalObjects; + } + + public IReadOnlyList SupportedImageHandleTypes => _externalObjects.SupportedImageHandleTypes; + public IReadOnlyList SupportedSemaphoreTypes => _externalObjects.SupportedSemaphoreTypes; + + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + => _externalObjects.GetSynchronizationCapabilities(imageHandleType); + + public ICompositionImportedGpuImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties) + => new CompositionImportedGpuImage(_compositor, _context, _externalObjects, + () => _externalObjects.ImportImage(handle, properties)); + + public ICompositionImportedGpuImage ImportImage(ICompositionImportableSharedGpuContextImage image) + { + return new CompositionImportedGpuImage(_compositor, _context, _externalObjects, + () => _externalObjects.ImportImage(image)); + } + + public ICompositionImportedGpuSemaphore ImportSemaphore(IPlatformHandle handle) + => new CompositionImportedGpuSemaphore(handle, _compositor, _context, _externalObjects); + + public ICompositionImportedGpuImage ImportSemaphore(ICompositionImportableSharedGpuContextSemaphore image) + { + throw new System.NotSupportedException(); + } + + public bool IsLost { get; } + public byte[]? DeviceLuid { get; set; } + public byte[]? DeviceUuid { get; set; } +} + +abstract class CompositionGpuImportedObjectBase : ICompositionGpuImportedObject +{ + protected Compositor Compositor { get; } + public IPlatformRenderInterfaceContext Context { get; } + public IExternalObjectsRenderInterfaceContextFeature Feature { get; } + + public CompositionGpuImportedObjectBase(Compositor compositor, + IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature) + { + Compositor = compositor; + Context = context; + Feature = feature; + + ImportCompeted = Compositor.InvokeServerJobAsync(Import); + } + + protected abstract void Import(); + public abstract void Dispose(); + + public Task ImportCompeted { get; } + public bool IsLost => Context.IsLost; + + public ValueTask DisposeAsync() => new(Compositor.InvokeServerJobAsync(() => + { + if (ImportCompeted.Status == TaskStatus.RanToCompletion) + Dispose(); + })); +} + +class CompositionImportedGpuImage : CompositionGpuImportedObjectBase, ICompositionImportedGpuImage +{ + private readonly Func _importer; + private IPlatformRenderInterfaceImportedImage? _image; + + public CompositionImportedGpuImage(Compositor compositor, + IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature, + Func importer): base(compositor, context, feature) + { + _importer = importer; + } + + protected override void Import() + { + using (Compositor.Server.RenderInterface.EnsureCurrent()) + { + // The original context was lost and the new one might have different capabilities + if (Context != Compositor.Server.RenderInterface.Value) + throw new PlatformGraphicsContextLostException(); + _image = _importer(); + } + } + + public IPlatformRenderInterfaceImportedImage Image => + _image ?? throw new ObjectDisposedException(nameof(CompositionImportedGpuImage)); + + public bool IsUsable => _image != null && Compositor.Server.RenderInterface.Value == Context; + + public override void Dispose() + { + _image?.Dispose(); + _image = null!; + } +} + +class CompositionImportedGpuSemaphore : CompositionGpuImportedObjectBase, ICompositionImportedGpuSemaphore +{ + private readonly IPlatformHandle _handle; + private IPlatformRenderInterfaceImportedSemaphore? _semaphore; + + public CompositionImportedGpuSemaphore(IPlatformHandle handle, + Compositor compositor, IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature) : base(compositor, context, feature) + { + _handle = handle; + } + + public IPlatformRenderInterfaceImportedSemaphore Semaphore => + _semaphore ?? throw new ObjectDisposedException(nameof(CompositionImportedGpuSemaphore)); + + + public bool IsUsable => _semaphore != null && Compositor.Server.RenderInterface.Value == Context; + + protected override void Import() + { + _semaphore = Feature.ImportSemaphore(_handle); + } + + public override void Dispose() + { + _semaphore?.Dispose(); + _semaphore = null; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs new file mode 100644 index 0000000000..0edd8ac732 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs @@ -0,0 +1,10 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition; + +public class CompositionSurface : CompositionObject +{ + internal CompositionSurface(Compositor compositor, ServerObject server) : base(compositor, server) + { + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 2b1b3f461f..6dba18704f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -35,4 +35,8 @@ public partial class Compositor new(this, new ServerCompositionSolidColorVisual(Server)); public CompositionCustomVisual CreateCustomVisual(CompositionCustomVisualHandler handler) => new(this, handler); -} \ No newline at end of file + + public CompositionSurfaceVisual CreateSurfaceVisual() => new(this, new ServerCompositionSurfaceVisual(_server)); + + public CompositionDrawingSurface CreateDrawingSurface() => new(this); +} diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 7fc5487171..bdcbe65403 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.Composition private BatchStreamObjectPool _batchObjectPool = new(); private BatchStreamMemoryPool _batchMemoryPool = new(); private List _objectsForSerialization = new(); - private Queue> _invokeBeforeCommit = new(); + private Queue _invokeBeforeCommitWrite = new(), _invokeBeforeCommitRead = new(); internal ServerCompositor Server => _server; private Task? _pendingBatch; private readonly object _pendingBatchLock = new(); @@ -77,16 +77,30 @@ namespace Avalonia.Rendering.Composition return _nextCommit.Task; } - + internal Task Commit() + { + try + { + return CommitCore(); + } + finally + { + if (_invokeBeforeCommitWrite.Count > 0) + RequestCommitAsync(); + } + } + + Task CommitCore() { Dispatcher.UIThread.VerifyAccess(); using var noPump = NonPumpingLockHelper.Use(); _nextCommit ??= new TaskCompletionSource(); - while (_invokeBeforeCommit.Count > 0) - _invokeBeforeCommit.Dequeue()(_nextCommit.Task); + (_invokeBeforeCommitRead, _invokeBeforeCommitWrite) = (_invokeBeforeCommitWrite, _invokeBeforeCommitRead); + while (_invokeBeforeCommitRead.Count > 0) + _invokeBeforeCommitRead.Dequeue()(); var batch = new Batch(_nextCommit); @@ -109,6 +123,7 @@ namespace Avalonia.Rendering.Composition writer.WriteObject(job); writer.WriteObject(ServerCompositor.RenderThreadJobsEndMarker); } + _pendingServerCompositorJobs.Clear(); } batch.CommittedAt = Server.Clock.Elapsed; @@ -138,34 +153,73 @@ namespace Avalonia.Rendering.Composition RequestCommitAsync(); } - internal void InvokeBeforeNextCommit(Action action) + /// + /// Enqueues a callback to be called before the next scheduled commit. + /// If there is no scheduled commit it automatically schedules one + /// This is useful for updating your composition tree objects after binding + /// and layout passes have completed + /// + public void RequestCompositionUpdate(Action action) { Dispatcher.UIThread.VerifyAccess(); - _invokeBeforeCommit.Enqueue(action); + _invokeBeforeCommitWrite.Enqueue(action); RequestCommitAsync(); } - /// - /// Attempts to query for a feature from the platform render interface - /// - public ValueTask TryGetRenderInterfaceFeature(Type featureType) + internal void PostServerJob(Action job) { - var tcs = new TaskCompletionSource(); - _pendingServerCompositorJobs.Add(() => + Dispatcher.UIThread.VerifyAccess(); + _pendingServerCompositorJobs.Add(job); + RequestCommitAsync(); + } + + internal Task InvokeServerJobAsync(Action job) => + InvokeServerJobAsync(() => + { + job(); + return null; + }); + + internal Task InvokeServerJobAsync(Func job) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + PostServerJob(() => { try { - using (Server.RenderInterface.EnsureCurrent()) - { - tcs.TrySetResult(Server.RenderInterface.Value.TryGetFeature(featureType)); - } + tcs.SetResult(job()); } catch (Exception e) { tcs.TrySetException(e); } }); - return new ValueTask(tcs.Task); + return tcs.Task; } + + /// + /// Attempts to query for a feature from the platform render interface + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => + new(InvokeServerJobAsync(() => + { + using (Server.RenderInterface.EnsureCurrent()) + { + return Server.RenderInterface.Value.TryGetFeature(featureType); + } + })); + + public ValueTask TryGetCompositionGpuInterop() => + new(InvokeServerJobAsync(() => + { + using (Server.RenderInterface.EnsureCurrent()) + { + var feature = Server.RenderInterface.Value + .TryGetFeature(); + if (feature == null) + return null; + return new CompositionInterop(this, feature); + } + })); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs new file mode 100644 index 0000000000..9c6c78c1ad --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs @@ -0,0 +1,74 @@ +using System; +using System.Runtime.ExceptionServices; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionDrawingSurface : ServerCompositionSurface, IDisposable +{ + private IRef? _bitmap; + private IPlatformRenderInterfaceContext? _createdWithContext; + public override IRef? Bitmap + { + get + { + // Failsafe to avoid consuming an image imported with a different context + if (Compositor.RenderInterface.Value != _createdWithContext) + return null; + return _bitmap; + } + } + + public ServerCompositionDrawingSurface(ServerCompositor compositor) : base(compositor) + { + } + + void PerformSanityChecks(CompositionImportedGpuImage image) + { + // Failsafe to avoid consuming an image imported with a different context + if (!image.IsUsable) + throw new PlatformGraphicsContextLostException(); + + // This should never happen, but check for it anyway to avoid a deadlock + if (!image.ImportCompeted.IsCompleted) + throw new InvalidOperationException("The import operation is not completed yet"); + + // Rethrow the import here exception + if (image.ImportCompeted.IsFaulted) + image.ImportCompeted.GetAwaiter().GetResult(); + } + + void Update(IBitmapImpl newImage, IPlatformRenderInterfaceContext context) + { + _bitmap?.Dispose(); + _bitmap = RefCountable.Create(newImage); + _createdWithContext = context; + Changed?.Invoke(); + } + + public void UpdateWithAutomaticSync(CompositionImportedGpuImage image) + { + PerformSanityChecks(image); + Update(image.Image.SnapshotWithAutomaticSync(), image.Context); + } + + public void UpdateWithKeyedMutex(CompositionImportedGpuImage image, uint acquireIndex, uint releaseIndex) + { + PerformSanityChecks(image); + Update(image.Image.SnapshotWithKeyedMutex(acquireIndex, releaseIndex), image.Context); + } + + public void UpdateWithSemaphores(CompositionImportedGpuImage image, CompositionImportedGpuSemaphore wait, CompositionImportedGpuSemaphore signal) + { + PerformSanityChecks(image); + if (!wait.IsUsable || !signal.IsUsable) + throw new PlatformGraphicsContextLostException(); + Update(image.Image.SnapshotWithSemaphores(wait.Semaphore, signal.Semaphore), image.Context); + } + + public void Dispose() + { + _bitmap?.Dispose(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs index 32a99fa187..88f10ba642 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs @@ -1,11 +1,18 @@ // Special license applies License.md +using System; +using Avalonia.Platform; +using Avalonia.Utilities; + namespace Avalonia.Rendering.Composition.Server { - internal abstract class ServerCompositionSurface : ServerObject + internal abstract partial class ServerCompositionSurface : ServerObject { protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor) { } + + public abstract IRef? Bitmap { get; } + public Action? Changed { get; set; } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs new file mode 100644 index 0000000000..c75ae8e631 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs @@ -0,0 +1,48 @@ +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositionSurfaceVisual +{ + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + { + if (Surface == null) + return; + if (Surface.Bitmap == null) + return; + var bmp = Surface.Bitmap.Item; + + //TODO: add a way to always render the whole bitmap instead of just assuming 96 DPI + canvas.DrawBitmap(Surface.Bitmap, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect( + new Size(Size.X, Size.Y))); + } + + + private void OnSurfaceInvalidated() => ValuesInvalidated(); + + protected override void OnAttachedToRoot(ServerCompositionTarget target) + { + if (Surface != null) + Surface.Changed += OnSurfaceInvalidated; + base.OnAttachedToRoot(target); + } + + protected override void OnDetachedFromRoot(ServerCompositionTarget target) + { + if (Surface != null) + Surface.Changed -= OnSurfaceInvalidated; + base.OnDetachedFromRoot(target); + } + + partial void OnSurfaceChanged() + { + if (Surface != null) + Surface.Changed += OnSurfaceInvalidated; + } + + partial void OnSurfaceChanging() + { + if (Surface != null) + Surface.Changed -= OnSurfaceInvalidated; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index e7405995f5..0492997200 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -22,7 +22,8 @@ namespace Avalonia.Rendering.Composition.Server { private readonly IRenderLoop _renderLoop; - private readonly Queue _batches = new Queue(); + private readonly Queue _batches = new Queue(); + private readonly Queue _receivedJobQueue = new(); public long LastBatchId { get; private set; } public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } @@ -75,7 +76,7 @@ namespace Avalonia.Rendering.Composition.Server var readObject = stream.ReadObject(); if (readObject == RenderThreadJobsStartMarker) { - ReadAndExecuteJobs(stream); + ReadServerJobs(stream); continue; } @@ -97,21 +98,24 @@ namespace Avalonia.Rendering.Composition.Server } } - void ReadAndExecuteJobs(BatchStreamReader reader) + void ReadServerJobs(BatchStreamReader reader) { object? readObject; while ((readObject = reader.ReadObject()) != RenderThreadJobsEndMarker) - { - var job = (Action)readObject!; + _receivedJobQueue.Enqueue((Action)readObject!); + } + + void ExecuteServerJobs() + { + while(_receivedJobQueue.Count > 0) try { - job(); + _receivedJobQueue.Dequeue()(); } catch { // Ignore } - } } void CompletePendingBatches() @@ -160,6 +164,7 @@ namespace Avalonia.Rendering.Composition.Server try { RenderInterface.EnsureValidBackendContext(); + ExecuteServerJobs(); foreach (var t in _activeTargets) t.Render(); } diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index 198b36564a..51a4ca1bf3 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -50,6 +50,8 @@ public class PlatformRenderInterfaceContextManager } } + internal IPlatformGraphicsContext? GpuContext => _gpuContext?.Value; + public IDisposable EnsureCurrent() { EnsureValidBackendContext(); diff --git a/src/Avalonia.Base/Rendering/SwapchainBase.cs b/src/Avalonia.Base/Rendering/SwapchainBase.cs new file mode 100644 index 0000000000..ccfb704f00 --- /dev/null +++ b/src/Avalonia.Base/Rendering/SwapchainBase.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Rendering.Composition; + +namespace Avalonia.Rendering; + +/// +/// A helper class for composition-backed swapchains, should not be a public API yet +/// +abstract class SwapchainBase : IAsyncDisposable where TImage : class, ISwapchainImage +{ + protected ICompositionGpuInterop Interop { get; } + protected CompositionDrawingSurface Target { get; } + private List _pendingImages = new(); + + public SwapchainBase(ICompositionGpuInterop interop, CompositionDrawingSurface target) + { + Interop = interop; + Target = target; + } + + static bool IsBroken(TImage image) => image.LastPresent?.IsFaulted == true; + static bool IsReady(TImage image) => image.LastPresent == null || image.LastPresent.Status == TaskStatus.RanToCompletion; + + TImage? CleanupAndFindNextImage(PixelSize size) + { + TImage? firstFound = null; + var foundMultiple = false; + + for (var c = _pendingImages.Count - 1; c > -1; c--) + { + var image = _pendingImages[c]; + var ready = IsReady(image); + var matches = image.Size == size; + if (IsBroken(image) || (!matches && ready)) + { + image.DisposeAsync(); + _pendingImages.RemoveAt(c); + } + + if (matches && ready) + { + if (firstFound == null) + firstFound = image; + else + foundMultiple = true; + } + + } + + // We are making sure that there was at least one image of the same size in flight + // Otherwise we might encounter UI thread lockups + return foundMultiple ? firstFound : null; + } + + protected abstract TImage CreateImage(PixelSize size); + + protected IDisposable BeginDrawCore(PixelSize size, out TImage image) + { + var img = CleanupAndFindNextImage(size) ?? CreateImage(size); + + img.BeginDraw(); + _pendingImages.Remove(img); + image = img; + return Disposable.Create(() => + { + img.Present(); + _pendingImages.Add(img); + }); + } + + public async ValueTask DisposeAsync() + { + foreach (var img in _pendingImages) + await img.DisposeAsync(); + } +} + + +interface ISwapchainImage : IAsyncDisposable +{ + PixelSize Size { get; } + Task? LastPresent { get; } + void BeginDraw(); + void Present(); +} diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 6dfcb2e74d..a0dbf238dc 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -7,6 +7,8 @@ + + @@ -30,6 +32,9 @@ + + + @@ -46,4 +51,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 9bb4b34976..a12d876e64 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -49,6 +49,7 @@ namespace Avalonia.Headless public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); + public bool IsLost => false; public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index 9b38c150fa..73493c69c0 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -15,7 +15,7 @@ - - + + diff --git a/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs b/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs new file mode 100644 index 0000000000..3db0aafd7b --- /dev/null +++ b/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; + +namespace Avalonia.OpenGL.Controls; + +internal class CompositionOpenGlSwapchain : SwapchainBase +{ + private readonly IGlContext _context; + private readonly IGlContextExternalObjectsFeature? _externalObjectsFeature; + private readonly IOpenGlTextureSharingRenderInterfaceContextFeature? _sharingFeature; + + public CompositionOpenGlSwapchain(IGlContext context, ICompositionGpuInterop interop, CompositionDrawingSurface target, + IOpenGlTextureSharingRenderInterfaceContextFeature sharingFeature + ) : base(interop, target) + { + _context = context; + _sharingFeature = sharingFeature; + } + + public CompositionOpenGlSwapchain(IGlContext context, ICompositionGpuInterop interop, CompositionDrawingSurface target, + IGlContextExternalObjectsFeature externalObjectsFeature) : base(interop, target) + { + _context = context; + _externalObjectsFeature = externalObjectsFeature; + } + + + + protected override IGlSwapchainImage CreateImage(PixelSize size) + { + if (_sharingFeature != null) + return new CompositionOpenGlSwapChainImage(_context, _sharingFeature, size, Interop, Target); + return new DxgiMutexOpenGlSwapChainImage(Interop, Target, _externalObjectsFeature!, size); + } + + public IDisposable BeginDraw(PixelSize size, out IGlTexture texture) + { + var rv = BeginDrawCore(size, out var tex); + texture = tex; + return rv; + } +} + +internal interface IGlTexture +{ + int TextureId { get; } + int InternalFormat { get; } + PixelSize Size { get; } +} + + +interface IGlSwapchainImage : ISwapchainImage, IGlTexture +{ + +} +internal class DxgiMutexOpenGlSwapChainImage : IGlSwapchainImage +{ + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _surface; + private readonly IGlExportableExternalImageTexture _texture; + private Task? _lastPresent; + private ICompositionImportedGpuImage? _imported; + + public DxgiMutexOpenGlSwapChainImage(ICompositionGpuInterop interop, CompositionDrawingSurface surface, + IGlContextExternalObjectsFeature externalObjects, PixelSize size) + { + _interop = interop; + _surface = surface; + _texture = externalObjects.CreateImage(KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle, + size, PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm); + } + public async ValueTask DisposeAsync() + { + // The texture is already sent to the compositor, so we need to wait for its attempts to use the texture + // before destroying it + if (_imported != null) + { + // No need to wait for import / LastPresent since calls are serialized on the compositor side anyway + try + { + await _imported.DisposeAsync(); + } + catch + { + // Ignore + } + } + _texture.Dispose(); + } + + public int TextureId => _texture.TextureId; + public int InternalFormat => _texture.InternalFormat; + public PixelSize Size => new(_texture.Properties.Width, _texture.Properties.Height); + public Task LastPresent => _lastPresent; + public void BeginDraw() => _texture.AcquireKeyedMutex(0); + + public void Present() + { + _texture.ReleaseKeyedMutex(1); + _imported ??= _interop.ImportImage(_texture.GetHandle(), _texture.Properties); + _lastPresent = _surface.UpdateWithKeyedMutexAsync(_imported, 1, 0); + } +} + +internal class CompositionOpenGlSwapChainImage : IGlSwapchainImage +{ + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _target; + private readonly ICompositionImportableOpenGlSharedTexture _texture; + private ICompositionImportedGpuImage? _imported; + + public CompositionOpenGlSwapChainImage( + IGlContext context, + IOpenGlTextureSharingRenderInterfaceContextFeature sharingFeature, + PixelSize size, + ICompositionGpuInterop interop, + CompositionDrawingSurface target) + { + _interop = interop; + _target = target; + _texture = sharingFeature.CreateSharedTextureForComposition(context, size); + } + + + public async ValueTask DisposeAsync() + { + // The texture is already sent to the compositor, so we need to wait for its attempts to use the texture + // before destroying it + if (_imported != null) + { + // No need to wait for import / LastPresent since calls are serialized on the compositor side anyway + try + { + await _imported.DisposeAsync(); + } + catch + { + // Ignore + } + } + + _texture.Dispose(); + } + + public int TextureId => _texture.TextureId; + public int InternalFormat => _texture.InternalFormat; + public PixelSize Size => _texture.Size; + public Task? LastPresent { get; private set; } + public void BeginDraw() + { + // No-op for texture sharing + } + + public void Present() + { + _imported ??= _interop.ImportImage(_texture); + LastPresent = _target.UpdateAsync(_imported); + } +} diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index c62a3d8d2f..4d0663a12b 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -1,120 +1,53 @@ using System; +using System.Numerics; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Logging; -using Avalonia.Media; -using Avalonia.OpenGL.Imaging; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.VisualTree; -using static Avalonia.OpenGL.GlConsts; - +using Avalonia.Platform; namespace Avalonia.OpenGL.Controls { public abstract class OpenGlControlBase : Control { - private IGlContext _context; - private int _fb, _depthBuffer; - private OpenGlBitmap _bitmap; - private IOpenGlBitmapAttachment _attachment; - private PixelSize _depthBufferSize; - + private CompositionSurfaceVisual _visual; + private Action _update; + private bool _updateQueued; private Task _initialization; - private IOpenGlTextureSharingRenderInterfaceContextFeature _feature; - - protected GlVersion GlVersion { get; private set; } - public sealed override void Render(DrawingContext context) - { - if(!EnsureInitialized()) - return; - - using (_context.MakeCurrent()) - { - _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); - EnsureTextureAttachment(); - EnsureDepthBufferAttachment(_context.GlInterface); - if(!CheckFramebufferStatus(_context.GlInterface)) - return; - - OnOpenGlRender(_context.GlInterface, _fb); - _attachment.Present(); - } - - context.DrawImage(_bitmap, new Rect(_bitmap.Size), new Rect(Bounds.Size)); - base.Render(context); - } + private OpenGlControlBaseResources? _resources; + private Compositor? _compositor; + protected GlVersion GlVersion => _resources?.Context.Version ?? default; - void EnsureTextureAttachment() + public OpenGlControlBase() { - _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); - if (_bitmap == null || _attachment == null || _bitmap.PixelSize != GetPixelSize()) - { - _attachment?.Dispose(); - _attachment = null; - _bitmap?.Dispose(); - _bitmap = null; - _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); - _attachment = _bitmap.CreateFramebufferAttachment(_context); - } - } - - void EnsureDepthBufferAttachment(GlInterface gl) - { - var size = GetPixelSize(); - if (size == _depthBufferSize && _depthBuffer != 0) - return; - - gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); - if (_depthBuffer != 0) gl.DeleteRenderbuffer(_depthBuffer); - - _depthBuffer = gl.GenRenderbuffer(); - gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer); - gl.RenderbufferStorage(GL_RENDERBUFFER, - GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, - size.Width, size.Height); - gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthBuffer); - gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); + _update = Update; } void DoCleanup() { - if (_context != null) + if (_initialization is { Status: TaskStatus.RanToCompletion } && _resources != null) { - using (_context.MakeCurrent()) + try { - var gl = _context.GlInterface; - gl.ActiveTexture(GL_TEXTURE0); - gl.BindTexture(GL_TEXTURE_2D, 0); - gl.BindFramebuffer(GL_FRAMEBUFFER, 0); - if (_fb != 0) - gl.DeleteFramebuffer(_fb); - _fb = 0; - if (_depthBuffer != 0) - gl.DeleteRenderbuffer(_depthBuffer); - _depthBuffer = 0; - _attachment?.Dispose(); - _attachment = null; - _bitmap?.Dispose(); - _bitmap = null; - - try - { - if (_initialization is { Status: TaskStatus.RanToCompletion, Result: true }) - { - OnOpenGlDeinit(_context.GlInterface, _fb); - _initialization = null; - } - } - finally + using (_resources.Context.EnsureCurrent()) { - _context.Dispose(); - _context = null; + OnOpenGlDeinit(_resources.Context.GlInterface); } } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to free user OpenGL resources: {exception}", e); + } } - _fb = _depthBuffer = 0; - _attachment = null; - _bitmap = null; - _feature = null; + ElementComposition.SetElementChildVisual(this, null); + _visual = null; + + _resources?.DisposeAsync(); + _resources = null; + _initialization = null; } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) @@ -123,95 +56,75 @@ namespace Avalonia.OpenGL.Controls base.OnDetachedFromVisualTree(e); } - private bool EnsureInitializedCore(IOpenGlTextureSharingRenderInterfaceContextFeature feature) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - try - { - _context = feature.CreateSharedContext(); - _feature = feature; - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); - return false; - } + base.OnAttachedToVisualTree(e); + _compositor = (this.GetVisualRoot()?.Renderer as IRendererWithCompositor)?.Compositor; + RequestNextFrameRendering(); + } - if (_context == null) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create additional OpenGL context."); - return false; - } + private bool EnsureInitializedCore( + ICompositionGpuInterop interop, + IOpenGlTextureSharingRenderInterfaceContextFeature contextSharingFeature) + { + var surface = _compositor.CreateDrawingSurface(); - GlVersion = _context.Version; + IGlContext ctx = null; + var contextFactory = AvaloniaLocator.Current.GetService(); try { - _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); - if (!_bitmap.SupportsContext(_context)) + if (contextSharingFeature?.CanCreateSharedContext == true) + _resources = OpenGlControlBaseResources.TryCreate(surface, interop, contextSharingFeature); + + if(_resources == null) + { + ctx = contextFactory.CreateContext(null); + if (ctx.TryGetFeature(out var externalObjects)) + _resources = OpenGlControlBaseResources.TryCreate(ctx, surface, interop, externalObjects); + else + ctx.Dispose(); + } + + if(_resources == null) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create OpenGlBitmap: OpenGL context is not compatible"); + "Unable to initialize OpenGL: current platform does not support multithreaded context sharing and shared memory"); return false; } } catch (Exception e) { - _context.Dispose(); - _context = null; Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create OpenGlBitmap: {exception}", e); + "Unable to initialize OpenGL: {exception}", e); + ctx?.Dispose(); return false; } + + _visual = _compositor.CreateSurfaceVisual(); + _visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + _visual.Surface = _resources.Surface; + ElementComposition.SetElementChildVisual(this, _visual); + using (_resources.Context.MakeCurrent()) + OnOpenGlInit(_resources.Context.GlInterface); + return true; - using (_context.MakeCurrent()) - { - try - { - _depthBufferSize = GetPixelSize(); - var gl = _context.GlInterface; - _fb = gl.GenFramebuffer(); - gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); - - EnsureDepthBufferAttachment(gl); - EnsureTextureAttachment(); - - return CheckFramebufferStatus(gl); - } - catch(Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL FBO: {exception}", e); - return false; - } - } } - - private static bool CheckFramebufferStatus(GlInterface gl) + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); - if (status != GL_FRAMEBUFFER_COMPLETE) + if (_visual != null && change.Property == BoundsProperty) { - int code; - while ((code = gl.GetError()) != 0) - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL FBO: {code}", code); - return false; + _visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + RequestNextFrameRendering(); } - return true; + base.OnPropertyChanged(change); } void ContextLost() { - _context = null; - _feature = null; _initialization = null; - _attachment = null; - _bitmap = null; - _fb = 0; - _depthBuffer = 0; - _depthBufferSize = default; + _resources?.DisposeAsync(); OnOpenGlLost(); } @@ -228,59 +141,106 @@ namespace Avalonia.OpenGL.Controls if (_initialization is { IsCompleted: false }) return false; - if (_context.IsLost) + if (_resources!.Context.IsLost) ContextLost(); else return true; } _initialization = InitializeAsync(); + + async void ContinueOnInitialization() + { + try + { + await _initialization; + RequestNextFrameRendering(); + } + catch + { + // + } + } + ContinueOnInitialization(); return false; } + + private void Update() + { + _updateQueued = false; + if (VisualRoot == null) + return; + if(!EnsureInitialized()) + return; + using (_resources.BeginDraw(GetPixelSize())) + OnOpenGlRender(_resources.Context.GlInterface, _resources.Fbo); + } + private async Task InitializeAsync() { + if (_compositor == null) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to obtain Compositor instance"); + return false; + } + + var gpuInteropTask = _compositor.TryGetCompositionGpuInterop(); + var contextSharingFeature = (IOpenGlTextureSharingRenderInterfaceContextFeature) - await this.GetVisualRoot()!.Renderer.TryGetRenderInterfaceFeature( + await _compositor.TryGetRenderInterfaceFeature( typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)); + var interop = await gpuInteropTask; - if (contextSharingFeature == null || !contextSharingFeature.CanCreateSharedContext) + if (interop == null) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); + "Compositor backend doesn't support GPU interop"); return false; } - if (!EnsureInitializedCore(contextSharingFeature)) + if (!EnsureInitializedCore(interop, contextSharingFeature)) { DoCleanup(); return false; } - using (_context.MakeCurrent()) - OnOpenGlInit(_context.GlInterface, _fb); - - InvalidateVisual(); + using (_resources!.Context.MakeCurrent()) + OnOpenGlInit(_resources.Context.GlInterface); return true; } + + [Obsolete("Use RequestNextFrameRendering()")] + // ReSharper disable once MemberCanBeProtected.Global + public new void InvalidateVisual() => RequestNextFrameRendering(); + public void RequestNextFrameRendering() + { + if ((_initialization == null || _initialization is { Status: TaskStatus.RanToCompletion }) && + !_updateQueued) + { + _updateQueued = true; + _compositor?.RequestCompositionUpdate(_update); + } + } + private PixelSize GetPixelSize() { - var scaling = VisualRoot.RenderScaling; + var scaling = VisualRoot!.RenderScaling; return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)), Math.Max(1, (int)(Bounds.Height * scaling))); } - - - protected virtual void OnOpenGlInit(GlInterface gl, int fb) + + protected virtual void OnOpenGlInit(GlInterface gl) { } - protected virtual void OnOpenGlDeinit(GlInterface gl, int fb) + protected virtual void OnOpenGlDeinit(GlInterface gl) { } diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs new file mode 100644 index 0000000000..81270bb642 --- /dev/null +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs @@ -0,0 +1,170 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia.Logging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using static Avalonia.OpenGL.GlConsts; +namespace Avalonia.OpenGL.Controls; + +internal class OpenGlControlBaseResources : IAsyncDisposable +{ + private int _depthBuffer; + public int Fbo { get; private set; } + private PixelSize _depthBufferSize; + public CompositionDrawingSurface Surface { get; } + public CompositionOpenGlSwapchain _swapchain; + public IGlContext Context { get; private set; } + + public static OpenGlControlBaseResources? TryCreate(CompositionDrawingSurface surface, + ICompositionGpuInterop interop, + IOpenGlTextureSharingRenderInterfaceContextFeature feature) + { + IGlContext context; + try + { + context = feature.CreateSharedContext(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); + return null; + } + + if (context == null) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create additional OpenGL context."); + return null; + } + + return new OpenGlControlBaseResources(context, surface, interop, feature, null); + } + + public static OpenGlControlBaseResources? TryCreate(IGlContext context, CompositionDrawingSurface surface, + ICompositionGpuInterop interop, IGlContextExternalObjectsFeature externalObjects) + { + if (!interop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .D3D11TextureGlobalSharedHandle) + || !externalObjects.SupportedExportableExternalImageTypes.Contains( + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle)) + return null; + return new OpenGlControlBaseResources(context, surface, interop, null, externalObjects); + } + + public OpenGlControlBaseResources(IGlContext context, + CompositionDrawingSurface surface, + ICompositionGpuInterop interop, + IOpenGlTextureSharingRenderInterfaceContextFeature? feature, + IGlContextExternalObjectsFeature? externalObjects + ) + { + Context = context; + Surface = surface; + using (context.MakeCurrent()) + Fbo = context.GlInterface.GenFramebuffer(); + _swapchain = + feature != null ? + new CompositionOpenGlSwapchain(context, interop, Surface, feature) : + new CompositionOpenGlSwapchain(context, interop, Surface, externalObjects); + } + + void UpdateDepthRenderbuffer(PixelSize size) + { + if (size == _depthBufferSize && _depthBuffer != 0) + return; + + var gl = Context.GlInterface; + gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); + if (_depthBuffer != 0) gl.DeleteRenderbuffer(_depthBuffer); + + _depthBuffer = gl.GenRenderbuffer(); + gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer); + gl.RenderbufferStorage(GL_RENDERBUFFER, + Context.Version.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, + size.Width, size.Height); + gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthBuffer); + gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); + _depthBufferSize = size; + } + + public IDisposable BeginDraw(PixelSize size) + { + var restoreContext = Context.EnsureCurrent(); + IDisposable? imagePresent = null; + var success = false; + try + { + var gl = Context.GlInterface; + Context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, Fbo); + UpdateDepthRenderbuffer(size); + + imagePresent = _swapchain.BeginDraw(size, out var texture); + gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.TextureId, 0); + + var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) + { + int code = gl.GetError(); + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to configure OpenGL FBO: {code}", code); + throw OpenGlException.GetFormattedException("Unable to configure OpenGL FBO", code); + } + + success = true; + return Disposable.Create(() => + { + try + { + Context.GlInterface.Flush(); + imagePresent.Dispose(); + } + finally + { + restoreContext.Dispose(); + } + }); + } + finally + { + if (!success) + { + imagePresent?.Dispose(); + restoreContext.Dispose(); + } + } + } + + public async ValueTask DisposeAsync() + { + if (Context is { IsLost: false }) + { + try + { + using (Context.MakeCurrent()) + { + var gl = Context.GlInterface; + if (Fbo != 0) + gl.DeleteFramebuffer(Fbo); + Fbo = 0; + if (_depthBuffer != 0) + gl.DeleteRenderbuffer(_depthBuffer); + _depthBuffer = 0; + } + + } + catch + { + // + } + + Surface.Dispose(); + await _swapchain.DisposeAsync(); + + Context = null!; + } + } +} diff --git a/src/Avalonia.OpenGL/Egl/EglConsts.cs b/src/Avalonia.OpenGL/Egl/EglConsts.cs index 428d11857f..aff56ecfe0 100644 --- a/src/Avalonia.OpenGL/Egl/EglConsts.cs +++ b/src/Avalonia.OpenGL/Egl/EglConsts.cs @@ -64,7 +64,7 @@ namespace Avalonia.OpenGL.Egl public const int EGL_WIDTH = 0x3057; public const int EGL_WINDOW_BIT = 0x0004; -// public const int EGL_BACK_BUFFER = 0x3084; + public const int EGL_BACK_BUFFER = 0x3084; // public const int EGL_BIND_TO_TEXTURE_RGB = 0x3039; // public const int EGL_BIND_TO_TEXTURE_RGBA = 0x303A; public const int EGL_CONTEXT_LOST = 0x300E; @@ -73,11 +73,11 @@ namespace Avalonia.OpenGL.Egl // public const int EGL_MIPMAP_TEXTURE = 0x3082; // public const int EGL_MIPMAP_LEVEL = 0x3083; // public const int EGL_NO_TEXTURE = 0x305C; -// public const int EGL_TEXTURE_2D = 0x305F; -// public const int EGL_TEXTURE_FORMAT = 0x3080; + public const int EGL_TEXTURE_2D = 0x305F; + public const int EGL_TEXTURE_FORMAT = 0x3080; // public const int EGL_TEXTURE_RGB = 0x305D; -// public const int EGL_TEXTURE_RGBA = 0x305E; -// public const int EGL_TEXTURE_TARGET = 0x3081; + public const int EGL_TEXTURE_RGBA = 0x305E; + public const int EGL_TEXTURE_TARGET = 0x3081; // public const int EGL_ALPHA_FORMAT = 0x3088; // public const int EGL_ALPHA_FORMAT_NONPRE = 0x308B; @@ -216,5 +216,7 @@ namespace Avalonia.OpenGL.Egl public const int EGL_TEXTURE_OFFSET_Y_ANGLE = 0x3491; public const int EGL_FLEXIBLE_SURFACE_COMPATIBILITY_SUPPORTED_ANGLE = 0x33A6; + + public const int EGL_TEXTURE_INTERNAL_FORMAT_ANGLE = 0x345D; } } diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index 4d75a776c3..425206e3c4 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Disposables; using System.Threading; using Avalonia.Platform; @@ -19,21 +20,24 @@ namespace Avalonia.OpenGL.Egl private readonly object _lock; internal EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, EglSurface offscreenSurface, - GlVersion version, int sampleCount, int stencilSize, Action disposeCallback, Dictionary features) + GlVersion version, int sampleCount, int stencilSize, Action disposeCallback, + Dictionary> features) { _disp = display; _egl = egl; _sharedWith = sharedWith; _context = ctx; _disposeCallback = disposeCallback; - _features = features; OffscreenSurface = offscreenSurface; Version = version; SampleCount = sampleCount; StencilSize = stencilSize; _lock = display.ContextSharedSyncRoot ?? new object(); using (MakeCurrent()) + { GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, _egl.GetProcAddress); + _features = features.ToDictionary(x => x.Key, x => x.Value(this)); + } } public IntPtr Context => @@ -155,6 +159,14 @@ namespace Avalonia.OpenGL.Egl { if(_context == IntPtr.Zero) return; + + foreach(var f in _features.ToList()) + if (f.Value is IDisposable d) + { + d.Dispose(); + _features.Remove(f.Key); + } + _egl.DestroyContext(_disp.Handle, Context); OffscreenSurface?.Dispose(); _context = IntPtr.Zero; diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index eea2587587..e4f0b2826d 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -93,7 +93,7 @@ namespace Avalonia.OpenGL.Egl var rv = new EglContext(this, _egl, share, ctx, offscreenSurface, _config.Version, _config.SampleCount, _config.StencilSize, - options.DisposeCallback, options.ExtraFeatures); + options.DisposeCallback, options.ExtraFeatures ?? new()); _contexts.Add(rv); return rv; } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs index 5648645c54..906a533e2d 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs @@ -19,7 +19,7 @@ public class EglContextOptions public EglContext ShareWith { get; set; } public EglSurface OffscreenSurface { get; set; } public Action DisposeCallback { get; set; } - public Dictionary ExtraFeatures { get; set; } + public Dictionary> ExtraFeatures { get; set; } } public class EglDisplayCreationOptions : EglDisplayOptions diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index a913c05996..67fd172e8e 100644 --- a/src/Avalonia.OpenGL/Egl/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -98,6 +98,9 @@ namespace Avalonia.OpenGL.Egl [GetProcAddress("eglCreateWindowSurface")] public partial IntPtr CreateWindowSurface(IntPtr display, IntPtr config, IntPtr window, int[] attrs); + [GetProcAddress("eglBindTexImage")] + public partial int BindTexImage(IntPtr display, IntPtr surface, int buffer); + [GetProcAddress("eglGetConfigAttrib")] public partial bool GetConfigAttrib(IntPtr display, IntPtr config, int attr, out int rv); diff --git a/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs b/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs new file mode 100644 index 0000000000..02d152ff61 --- /dev/null +++ b/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using Avalonia.Logging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Avalonia.SourceGenerator; +using static Avalonia.OpenGL.GlConsts; + +namespace Avalonia.OpenGL.Features; + +unsafe partial class ExternalObjectsInterface +{ + public ExternalObjectsInterface(Func getProcAddress) + { + Initialize(getProcAddress); + } + + [GetProcAddress("glImportMemoryFdEXT", true)] + public partial void ImportMemoryFdEXT(uint memory, ulong size, int handleType, int fd); + + [GetProcAddress("glImportSemaphoreFdEXT", true)] + public partial void ImportSemaphoreFdEXT(uint semaphore, + int handleType, + int fd); + + [GetProcAddress("glCreateMemoryObjectsEXT")] + public partial void CreateMemoryObjectsEXT(int n, out uint memoryObjects); + + [GetProcAddress("glDeleteMemoryObjectsEXT")] + public partial void DeleteMemoryObjectsEXT(int n, ref uint objects); + + [GetProcAddress("glTexStorageMem2DEXT")] + public partial void TexStorageMem2DEXT(int target, int levels, int internalFormat, int width, int height, + uint memory, ulong offset); + + [GetProcAddress("glGenSemaphoresEXT")] + public partial void GenSemaphoresEXT(int n, out uint semaphores); + + [GetProcAddress("glDeleteSemaphoresEXT")] + public partial void DeleteSemaphoresEXT(int n, ref uint semaphores); + + [GetProcAddress("glWaitSemaphoreEXT")] + public partial void WaitSemaphoreEXT(uint semaphore, + uint numBufferBarriers, uint* buffers, + uint numTextureBarriers, int* textures, + int* srcLayouts); + + [GetProcAddress("glSignalSemaphoreEXT")] + public partial void SignalSemaphoreEXT(uint semaphore, + uint numBufferBarriers, uint* buffers, + uint numTextureBarriers, int* textures, + int* dstLayouts); + + + [GetProcAddress("glGetUnsignedBytei_vEXT", true)] + public partial void GetUnsignedBytei_vEXT(int target, uint index, byte* data); + + [GetProcAddress("glGetUnsignedBytevEXT", true)] + public partial void GetUnsignedBytevEXT(int target, byte* data); +} + +public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFeature +{ + private readonly IGlContext _context; + private readonly ExternalObjectsInterface _ext; + private List _imageTypes = new(); + private List _semaphoreTypes = new(); + + public static ExternalObjectsOpenGlExtensionFeature TryCreate(IGlContext context) + { + var extensions = context.GlInterface.GetExtensions(); + if (extensions.Contains("GL_EXT_memory_object") && extensions.Contains("GL_EXT_semaphore")) + { + try + { + return new ExternalObjectsOpenGlExtensionFeature(context, extensions); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(nameof(ExternalObjectsOpenGlExtensionFeature), + "Unable to initialize EXT_external_objects extension: " + e); + } + } + + return null; + } + + private unsafe ExternalObjectsOpenGlExtensionFeature(IGlContext context, List extensions) + { + _context = context; + _ext = new ExternalObjectsInterface(_context.GlInterface.GetProcAddress); + + if (_ext.IsGetUnsignedBytei_vEXTAvailable) + { + _context.GlInterface.GetIntegerv(GL_NUM_DEVICE_UUIDS_EXT, out var numUiids); + if (numUiids > 0) + { + DeviceUuid = new byte[16]; + fixed (byte* pUuid = DeviceUuid) + _ext.GetUnsignedBytei_vEXT(GL_DEVICE_UUID_EXT, 0, pUuid); + } + } + + if (_ext.IsGetUnsignedBytevEXTAvailable) + { + if (extensions.Contains("GL_EXT_memory_object_win32") || extensions.Contains("GL_EXT_semaphore_win32")) + { + DeviceLuid = new byte[8]; + fixed (byte* pLuid = DeviceLuid) + _ext.GetUnsignedBytevEXT(GL_DEVICE_LUID_EXT, pLuid); + } + } + + if (extensions.Contains("GL_EXT_memory_object_fd") + && extensions.Contains("GL_EXT_semaphore_fd")) + { + _imageTypes.Add(KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor); + _semaphoreTypes.Add(KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor); + } + + + + } + + public IReadOnlyList SupportedImportableExternalImageTypes => _imageTypes; + public IReadOnlyList SupportedExportableExternalImageTypes { get; } = Array.Empty(); + public IReadOnlyList SupportedImportableExternalSemaphoreTypes => _semaphoreTypes; + public IReadOnlyList SupportedExportableExternalSemaphoreTypes { get; } = Array.Empty(); + public IReadOnlyList GetSupportedFormatsForExternalMemoryType(string type) + { + return new[] + { + PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm + }; + } + + 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(!_imageTypes.Contains(handle.HandleDescriptor)) + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + + if (handle.HandleDescriptor == KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor) + { + while (_context.GlInterface.GetError() != 0) + { + //Skip existing errors + } + _ext.CreateMemoryObjectsEXT(1, out var memoryObject); + _ext.ImportMemoryFdEXT(memoryObject, properties.MemorySize, GL_HANDLE_TYPE_OPAQUE_FD_EXT, + handle.Handle.ToInt32()); + + var err = _context.GlInterface.GetError(); + if (err != 0) + throw OpenGlException.GetFormattedException("glImportMemoryFdEXT", err); + + _context.GlInterface.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); + + var texture = _context.GlInterface.GenTexture(); + _context.GlInterface.BindTexture(GL_TEXTURE_2D, texture); + _ext.TexStorageMem2DEXT(GL_TEXTURE_2D, 1, GL_RGBA8, properties.Width, properties.Height, + memoryObject, properties.MemoryOffset); + err = _context.GlInterface.GetError(); + + _context.GlInterface.BindTexture(GL_TEXTURE_2D, oldTexture); + if (err != 0) + throw OpenGlException.GetFormattedException("glTexStorageMem2DEXT", err); + + return new ExternalImageTexture(_context, properties, _ext, memoryObject, texture); + } + + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + } + + public IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle) + { + if(!_semaphoreTypes.Contains(handle.HandleDescriptor)) + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + + if (handle.HandleDescriptor == + KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor) + { + _ext.GenSemaphoresEXT(1, out var semaphore); + _ext.ImportSemaphoreFdEXT(semaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, handle.Handle.ToInt32()); + return new ExternalSemaphore(_context, _ext, semaphore); + } + + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + } + + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + { + if (imageHandleType == KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor) + return CompositionGpuImportedImageSynchronizationCapabilities.Semaphores; + return default; + } + + public byte[] DeviceLuid { get; } + public byte[] DeviceUuid { get; } + + unsafe class ExternalSemaphore : IGlExternalSemaphore + { + private readonly IGlContext _context; + private readonly ExternalObjectsInterface _ext; + private uint _semaphore; + + public ExternalSemaphore(IGlContext context, ExternalObjectsInterface ext, uint semaphore) + { + _context = context; + _ext = ext; + _semaphore = semaphore; + } + + public void Dispose() + { + if(_context.IsLost) + return; + using (_context.EnsureCurrent()) + _ext.DeleteSemaphoresEXT(1, ref _semaphore); + _semaphore = 0; + } + + public void WaitSemaphore(IGlExternalImageTexture texture) + { + var tex = (ExternalImageTexture)texture; + var texId = tex.TextureId; + var srcLayout = GL_LAYOUT_TRANSFER_SRC_EXT; + _ext.WaitSemaphoreEXT(_semaphore, 0, null, 1, &texId, &srcLayout); + } + + public void SignalSemaphore(IGlExternalImageTexture texture) + { + var tex = (ExternalImageTexture)texture; + var texId = tex.TextureId; + var dstLayout = 0; + _ext.SignalSemaphoreEXT(_semaphore, 0, null, 1, &texId, &dstLayout); + } + } + + class ExternalImageTexture : IGlExternalImageTexture + { + private readonly IGlContext _context; + private readonly ExternalObjectsInterface _ext; + private uint _objectId; + + public ExternalImageTexture(IGlContext context, + PlatformGraphicsExternalImageProperties properties, + ExternalObjectsInterface ext, uint objectId, int textureId) + { + Properties = properties; + TextureId = textureId; + _context = context; + _ext = ext; + _objectId = objectId; + } + + public void Dispose() + { + if(_context.IsLost) + return; + using (_context.EnsureCurrent()) + { + _context.GlInterface.DeleteTexture(TextureId); + _ext.DeleteMemoryObjectsEXT(1, ref _objectId); + _objectId = 0; + } + } + + public void AcquireKeyedMutex(uint key) => throw new NotSupportedException(); + + public void ReleaseKeyedMutex(uint key) => throw new NotSupportedException(); + + public int TextureId { get; } + public int InternalFormat => GL_RGBA8; + public PlatformGraphicsExternalImageProperties Properties { get; } + } +} diff --git a/src/Avalonia.OpenGL/GlConsts.cs b/src/Avalonia.OpenGL/GlConsts.cs index 3fdc147701..4fbe738ca9 100644 --- a/src/Avalonia.OpenGL/GlConsts.cs +++ b/src/Avalonia.OpenGL/GlConsts.cs @@ -534,7 +534,7 @@ namespace Avalonia.OpenGL // public const int GL_MAX_ELEMENTS_VERTICES = 0x80E8; // public const int GL_MAX_ELEMENTS_INDICES = 0x80E9; // public const int GL_BGR = 0x80E0; -// public const int GL_BGRA = 0x80E1; + public const int GL_BGRA = 0x80E1; // public const int GL_UNSIGNED_BYTE_3_3_2 = 0x8032; // public const int GL_UNSIGNED_BYTE_2_3_3_REV = 0x8362; // public const int GL_UNSIGNED_SHORT_5_6_5 = 0x8363; @@ -3372,16 +3372,16 @@ namespace Avalonia.OpenGL // public const int GL_TILING_TYPES_EXT = 0x9583; // public const int GL_OPTIMAL_TILING_EXT = 0x9584; // public const int GL_LINEAR_TILING_EXT = 0x9585; -// public const int GL_NUM_DEVICE_UUIDS_EXT = 0x9596; -// public const int GL_DEVICE_UUID_EXT = 0x9597; + internal const int GL_NUM_DEVICE_UUIDS_EXT = 0x9596; + internal const int GL_DEVICE_UUID_EXT = 0x9597; // public const int GL_DRIVER_UUID_EXT = 0x9598; // public const int GL_UUID_SIZE_EXT = 16; // public const int GL_EXT_memory_object_fd = 1; -// public const int GL_HANDLE_TYPE_OPAQUE_FD_EXT = 0x9586; + internal const int GL_HANDLE_TYPE_OPAQUE_FD_EXT = 0x9586; // public const int GL_EXT_memory_object_win32 = 1; // public const int GL_HANDLE_TYPE_OPAQUE_WIN32_EXT = 0x9587; // public const int GL_HANDLE_TYPE_OPAQUE_WIN32_KMT_EXT = 0x9588; -// public const int GL_DEVICE_LUID_EXT = 0x9599; + public const int GL_DEVICE_LUID_EXT = 0x9599; // public const int GL_DEVICE_NODE_MASK_EXT = 0x959A; // public const int GL_LUID_SIZE_EXT = 8; // public const int GL_HANDLE_TYPE_D3D12_TILEPOOL_EXT = 0x9589; @@ -3483,11 +3483,11 @@ namespace Avalonia.OpenGL // public const int GL_SECONDARY_COLOR_ARRAY_EXT = 0x845E; // public const int GL_EXT_semaphore = 1; // public const int GL_LAYOUT_GENERAL_EXT = 0x958D; -// public const int GL_LAYOUT_COLOR_ATTACHMENT_EXT = 0x958E; + internal const int GL_LAYOUT_COLOR_ATTACHMENT_EXT = 0x958E; // public const int GL_LAYOUT_DEPTH_STENCIL_ATTACHMENT_EXT = 0x958F; // public const int GL_LAYOUT_DEPTH_STENCIL_READ_ONLY_EXT = 0x9590; // public const int GL_LAYOUT_SHADER_READ_ONLY_EXT = 0x9591; -// public const int GL_LAYOUT_TRANSFER_SRC_EXT = 0x9592; + internal const int GL_LAYOUT_TRANSFER_SRC_EXT = 0x9592; // public const int GL_LAYOUT_TRANSFER_DST_EXT = 0x9593; // public const int GL_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_EXT = 0x9530; // public const int GL_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_EXT = 0x9531; diff --git a/src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs b/src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs new file mode 100644 index 0000000000..676f8eaf86 --- /dev/null +++ b/src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; + +namespace Avalonia.OpenGL; + +public interface IGlContextExternalObjectsFeature +{ + IReadOnlyList SupportedImportableExternalImageTypes { get; } + IReadOnlyList SupportedExportableExternalImageTypes { get; } + IReadOnlyList SupportedImportableExternalSemaphoreTypes { get; } + IReadOnlyList SupportedExportableExternalSemaphoreTypes { get; } + IReadOnlyList GetSupportedFormatsForExternalMemoryType(string type); + + IGlExportableExternalImageTexture CreateImage(string type,PixelSize size, PlatformGraphicsExternalImageFormat format); + + IGlExportableExternalImageTexture CreateSemaphore(string type); + IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties); + IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle); + CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType); + public byte[]? DeviceLuid { get; } + public byte[]? DeviceUuid { get; } +} + +public interface IGlExternalSemaphore : IDisposable +{ + void WaitSemaphore(IGlExternalImageTexture texture); + void SignalSemaphore(IGlExternalImageTexture texture); +} + +public interface IGlExportableExternalSemaphore : IGlExternalSemaphore +{ + IPlatformHandle GetHandle(); +} + +public interface IGlExternalImageTexture : IDisposable +{ + void AcquireKeyedMutex(uint key); + void ReleaseKeyedMutex(uint key); + int TextureId { get; } + int InternalFormat { get; } + + PlatformGraphicsExternalImageProperties Properties { get; } +} + +public interface IGlExportableExternalImageTexture : IGlExternalImageTexture +{ + IPlatformHandle GetHandle(); +} diff --git a/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs index 9c22d446ef..2043e944d1 100644 --- a/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs +++ b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs @@ -1,6 +1,6 @@ +using System; using System.Collections.Generic; -using Avalonia.OpenGL.Imaging; -using Avalonia.Platform; +using Avalonia.Rendering.Composition; namespace Avalonia.OpenGL { @@ -8,6 +8,13 @@ namespace Avalonia.OpenGL { bool CanCreateSharedContext { get; } IGlContext CreateSharedContext(IEnumerable preferredVersions = null); - IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); + ICompositionImportableOpenGlSharedTexture CreateSharedTextureForComposition(IGlContext context, PixelSize size); + } + + public interface ICompositionImportableOpenGlSharedTexture : ICompositionImportableSharedGpuContextImage + { + int TextureId { get; } + int InternalFormat { get; } + PixelSize Size { get; } } } diff --git a/src/Avalonia.OpenGL/IPlatformGraphicsOpenGlContextFactory.cs b/src/Avalonia.OpenGL/IPlatformGraphicsOpenGlContextFactory.cs new file mode 100644 index 0000000000..e6c83415e6 --- /dev/null +++ b/src/Avalonia.OpenGL/IPlatformGraphicsOpenGlContextFactory.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Avalonia.OpenGL; + +public interface IPlatformGraphicsOpenGlContextFactory +{ + IGlContext CreateContext(IEnumerable? versions); +} diff --git a/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs b/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs deleted file mode 100644 index 22f0cebf57..0000000000 --- a/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using Avalonia.Metadata; -using Avalonia.Platform; - -namespace Avalonia.OpenGL.Imaging -{ - [Unstable] - public interface IOpenGlBitmapImpl : IBitmapImpl - { - IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback); - bool SupportsContext(IGlContext context); - } - - [Unstable] - public interface IOpenGlBitmapAttachment : IDisposable - { - void Present(); - } -} diff --git a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs deleted file mode 100644 index 53013ae5a3..0000000000 --- a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Avalonia.Media; -using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Threading; - -namespace Avalonia.OpenGL.Imaging -{ - public class OpenGlBitmap : Bitmap, IAffectsRender - { - private IOpenGlBitmapImpl _impl; - - public OpenGlBitmap(IOpenGlTextureSharingRenderInterfaceContextFeature feature, - PixelSize size, Vector dpi) - : base(CreateOrThrow(feature, size, dpi)) - { - _impl = (IOpenGlBitmapImpl)PlatformImpl.Item; - } - - static IOpenGlBitmapImpl CreateOrThrow(IOpenGlTextureSharingRenderInterfaceContextFeature feature, - PixelSize size, Vector dpi) => feature.CreateOpenGlBitmap(size, dpi); - - public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context) => - _impl.CreateFramebufferAttachment(context, SetIsDirty); - - public bool SupportsContext(IGlContext context) => _impl.SupportsContext(context); - - void SetIsDirty() - { - if (Dispatcher.UIThread.CheckAccess()) - CallInvalidated(); - else - Dispatcher.UIThread.Post(CallInvalidated); - } - - private void CallInvalidated() => Invalidated?.Invoke(this, EventArgs.Empty); - - public event EventHandler Invalidated; - } -} diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index efe305dba5..c498ed7833 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -27,6 +27,9 @@ namespace Avalonia.OpenGL return GetFormattedException(funcName, (GlErrors)err, err); } + public static OpenGlException GetFormattedException(string funcName, int errorCode) => + GetFormattedException(funcName, (GlErrors)errorCode, errorCode); + public static OpenGlException GetFormattedEglException(string funcName, int errorCode) => GetFormattedException(funcName, (EglErrors)errorCode,errorCode); diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index def5228e94..dd10beb32e 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Reactive.Disposables; using System.Threading; using Avalonia.OpenGL; +using Avalonia.OpenGL.Features; + namespace Avalonia.X11.Glx { class GlxContext : IGlContext @@ -14,6 +16,7 @@ namespace Avalonia.X11.Glx private readonly IntPtr _defaultXid; private readonly bool _ownsPBuffer; private readonly object _lock = new object(); + private ExternalObjectsOpenGlExtensionFeature? _externalObjects; public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, GlxContext sharedWith, @@ -32,7 +35,10 @@ namespace Avalonia.X11.Glx SampleCount = sampleCount; StencilSize = stencilSize; using (MakeCurrent()) + { GlInterface = new GlInterface(version, GlxInterface.SafeGetProcAddress); + _externalObjects = ExternalObjectsOpenGlExtensionFeature.TryCreate(this); + } } public GlxDisplay Display { get; } @@ -123,6 +129,11 @@ namespace Avalonia.X11.Glx Glx.DestroyPbuffer(_x11.Display, _defaultXid); } - public object TryGetFeature(Type featureType) => null; + public object TryGetFeature(Type featureType) + { + if (featureType == typeof(IGlContextExternalObjectsFeature)) + return _externalObjects; + return null; + } } } diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index b064445a0b..a5782037f3 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.Platform; using SkiaSharp; @@ -33,9 +31,4 @@ namespace Avalonia.Skia bool CanBlit { get; } void Blit(SKCanvas canvas); } - - public interface IOpenGlAwareSkiaGpu : ISkiaGpu - { - IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); - } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs new file mode 100644 index 0000000000..4bf43634ef --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs @@ -0,0 +1,191 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Avalonia.OpenGL; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using SkiaSharp; + +namespace Avalonia.Skia; + +internal class GlSkiaExternalObjectsFeature : IExternalObjectsRenderInterfaceContextFeature +{ + private readonly GlSkiaGpu _gpu; + private readonly IGlContextExternalObjectsFeature? _feature; + + public GlSkiaExternalObjectsFeature(GlSkiaGpu gpu, IGlContextExternalObjectsFeature? feature) + { + _gpu = gpu; + _feature = feature; + } + + public IReadOnlyList SupportedImageHandleTypes => _feature?.SupportedImportableExternalImageTypes + ?? Array.Empty(); + public IReadOnlyList SupportedSemaphoreTypes => _feature?.SupportedImportableExternalSemaphoreTypes + ?? Array.Empty(); + + public IPlatformRenderInterfaceImportedImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties) + { + if (_feature == null) + throw new NotSupportedException("Importing this platform handle is not supported"); + using (_gpu.EnsureCurrent()) + { + var image = _feature.ImportImage(handle, properties); + return new GlSkiaImportedImage(_gpu, image); + } + } + + public IPlatformRenderInterfaceImportedImage ImportImage(ICompositionImportableSharedGpuContextImage image) + { + var img = (GlSkiaSharedTextureForComposition)image; + if (!img.Context.IsSharedWith(_gpu.GlContext)) + throw new InvalidOperationException("Contexts do not belong to the same share group"); + + return new GlSkiaImportedImage(_gpu, img); + } + + public IPlatformRenderInterfaceImportedSemaphore ImportSemaphore(IPlatformHandle handle) + { + if (_feature == null) + throw new NotSupportedException("Importing this platform handle is not supported"); + using (_gpu.EnsureCurrent()) + { + var semaphore = _feature.ImportSemaphore(handle); + return new GlSkiaImportedSemaphore(_gpu, semaphore); + } + } + + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + => _feature?.GetSynchronizationCapabilities(imageHandleType) ?? default; + + public byte[]? DeviceUuid => _feature?.DeviceUuid; + public byte[]? DeviceLuid => _feature?.DeviceLuid; +} + +internal class GlSkiaImportedSemaphore : IPlatformRenderInterfaceImportedSemaphore +{ + private readonly GlSkiaGpu _gpu; + public IGlExternalSemaphore Semaphore { get; } + + public GlSkiaImportedSemaphore(GlSkiaGpu gpu, IGlExternalSemaphore semaphore) + { + _gpu = gpu; + Semaphore = semaphore; + } + + public void Dispose() => Semaphore.Dispose(); +} + +internal class GlSkiaImportedImage : IPlatformRenderInterfaceImportedImage +{ + private readonly GlSkiaSharedTextureForComposition? _sharedTexture; + private readonly GlSkiaGpu _gpu; + private readonly IGlExternalImageTexture? _image; + + public GlSkiaImportedImage(GlSkiaGpu gpu, IGlExternalImageTexture image) + { + _gpu = gpu; + _image = image; + } + + public GlSkiaImportedImage(GlSkiaGpu gpu, GlSkiaSharedTextureForComposition sharedTexture) + { + _gpu = gpu; + _sharedTexture = sharedTexture; + } + + public void Dispose() + { + _image?.Dispose(); + _sharedTexture?.Dispose(_gpu.GlContext); + } + + SKColorType ConvertColorType(PlatformGraphicsExternalImageFormat format) => + format switch + { + PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm => SKColorType.Bgra8888, + PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm => SKColorType.Rgba8888, + _ => SKColorType.Rgba8888 + }; + + SKSurface? TryCreateSurface(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)); + 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)); + + return SKSurface.Create(_gpu.GrContext, unformatted, origin, SKColorType.Rgba8888); + } + + IBitmapImpl TakeSnapshot() + { + var width = _image?.Properties.Width ?? _sharedTexture!.Size.Width; + var height = _image?.Properties.Height ?? _sharedTexture!.Size.Height; + var internalFormat = _image?.InternalFormat ?? _sharedTexture!.InternalFormat; + var textureId = _image?.TextureId ?? _sharedTexture!.TextureId; + var topLeft = _image?.Properties.TopLeftOrigin ?? false; + + 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)) + { + if (surf == null) + throw new OpenGlException("Unable to consume provided texture"); + rv = new ImmutableBitmap(surf.Snapshot()); + } + + _gpu.GrContext.Flush(); + _gpu.GlContext.GlInterface.Flush(); + return rv; + } + + public IBitmapImpl SnapshotWithKeyedMutex(uint acquireIndex, uint releaseIndex) + { + using (_gpu.EnsureCurrent()) + { + _image.AcquireKeyedMutex(acquireIndex); + try + { + return TakeSnapshot(); + } + finally + { + _image.ReleaseKeyedMutex(releaseIndex); + } + } + } + + public IBitmapImpl SnapshotWithSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore, + IPlatformRenderInterfaceImportedSemaphore signalSemaphore) + { + var wait = (GlSkiaImportedSemaphore)waitForSemaphore; + var signal = (GlSkiaImportedSemaphore)signalSemaphore; + using (_gpu.EnsureCurrent()) + { + wait.Semaphore.WaitSemaphore(_image); + try + { + return TakeSnapshot(); + } + finally + { + signal.Semaphore.SignalSemaphore(_image); + } + } + } + + public IBitmapImpl SnapshotWithAutomaticSync() + { + using (_gpu.EnsureCurrent()) + return TakeSnapshot(); + } +} diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index cdba3b9ea2..bf3e950e81 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -3,19 +3,22 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Logging; using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using SkiaSharp; +using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - class GlSkiaGpu : IOpenGlAwareSkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature + class GlSkiaGpu : ISkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature { private GRContext _grContext; private IGlContext _glContext; + public GRContext GrContext => _grContext; + public IGlContext GlContext => _glContext; private List _postDisposeCallbacks = new(); private bool? _canCreateSurfaces; + private IExternalObjectsRenderInterfaceContextFeature? _externalObjectsFeature; public GlSkiaGpu(IGlContext context, long? maxResourceBytes) { @@ -32,6 +35,9 @@ namespace Avalonia.Skia _grContext.SetResourceCacheLimit(maxResourceBytes.Value); } } + + context.TryGetFeature(out var externalObjects); + _externalObjectsFeature = new GlSkiaExternalObjectsFeature(this, externalObjects); } } @@ -103,8 +109,34 @@ namespace Avalonia.Skia public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => _glContext.CreateSharedContext(preferredVersions); - public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) => new GlOpenGlBitmapImpl(_glContext, size, dpi); - + public ICompositionImportableOpenGlSharedTexture CreateSharedTextureForComposition(IGlContext context, PixelSize size) + { + if (!context.IsSharedWith(_glContext)) + throw new InvalidOperationException("Contexts do not belong to the same share group"); + + using (context.EnsureCurrent()) + { + var gl = context.GlInterface; + gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out int oldTexture); + var tex = gl.GenTexture(); + + var format = context.Version.Type == GlProfileType.OpenGLES && context.Version.Major == 2 + ? GL_RGBA + : GL_RGBA8; + + gl.BindTexture(GL_TEXTURE_2D, tex); + gl.TexImage2D(GL_TEXTURE_2D, 0, + format, size.Width, size.Height, + 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); + + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl.BindTexture(GL_TEXTURE_2D, oldTexture); + + return new GlSkiaSharedTextureForComposition(context, tex, format, size); + } + } + public void Dispose() { if (_glContext.IsLost) @@ -125,6 +157,8 @@ namespace Avalonia.Skia { if (featureType == typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)) return this; + if (featureType == typeof(IExternalObjectsRenderInterfaceContextFeature)) + return _externalObjectsFeature; return null; } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaSharedTextureForComposition.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaSharedTextureForComposition.cs new file mode 100644 index 0000000000..2d53254500 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaSharedTextureForComposition.cs @@ -0,0 +1,44 @@ +using Avalonia.OpenGL; + +namespace Avalonia.Skia; + +internal class GlSkiaSharedTextureForComposition : ICompositionImportableOpenGlSharedTexture +{ + public IGlContext Context { get; } + private readonly object _lock = new(); + + public GlSkiaSharedTextureForComposition(IGlContext context, int textureId, int internalFormat, PixelSize size) + { + Context = context; + TextureId = textureId; + InternalFormat = internalFormat; + Size = size; + } + public void Dispose(IGlContext context) + { + lock (_lock) + { + if(TextureId == 0) + return; + try + { + using (context.EnsureCurrent()) + context.GlInterface.DeleteTexture(TextureId); + } + catch + { + // Ignore + } + + TextureId = 0; + } + } + + public int TextureId { get; private set; } + public int InternalFormat { get; } + public PixelSize Size { get; } + public void Dispose() + { + Dispose(Context); + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs deleted file mode 100644 index d8bff7cfc8..0000000000 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; -using Avalonia.Utilities; -using SkiaSharp; -using static Avalonia.OpenGL.GlConsts; - -namespace Avalonia.Skia -{ - class GlOpenGlBitmapImpl : IOpenGlBitmapImpl, IDrawableBitmapImpl - { - private readonly IGlContext _context; - private readonly object _lock = new object(); - private IGlPresentableOpenGlSurface _surface; - - public GlOpenGlBitmapImpl(IGlContext context, PixelSize pixelSize, Vector dpi) - { - _context = context; - PixelSize = pixelSize; - Dpi = dpi; - } - - public Vector Dpi { get; } - public PixelSize PixelSize { get; } - public int Version { get; private set; } - public void Save(string fileName, int? quality = null) => throw new NotSupportedException(); - - public void Save(Stream stream, int? quality = null) => throw new NotSupportedException(); - - public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) - { - lock (_lock) - { - if (_surface == null) - return; - using (_surface.Lock()) - { - using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false, - new GRGlTextureInfo( - GlConsts.GL_TEXTURE_2D, (uint)_surface.GetTextureId(), - (uint)_surface.InternalFormat))) - using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, - SKColorType.Rgba8888, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal))) - { - // Again, silently ignore, if something went wrong it's not our fault - if (surface == null) - return; - - using (var snapshot = surface.Snapshot()) - context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint); - } - - } - } - } - - public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback) - { - if (!SupportsContext(context)) - throw new OpenGlException("Context is not supported for texture sharing"); - return new SharedOpenGlBitmapAttachment(this, context, presentCallback); - } - - public bool SupportsContext(IGlContext context) - { - // TODO: negotiated platform surface sharing - return _context.IsSharedWith(context); - } - - public void Dispose() - { - - } - - internal void Present(IGlPresentableOpenGlSurface surface) - { - lock (_lock) - { - _surface = surface; - } - } - } - - interface IGlPresentableOpenGlSurface : IDisposable - { - int GetTextureId(); - int InternalFormat { get; } - IDisposable Lock(); - } - - class SharedOpenGlBitmapAttachment : IOpenGlBitmapAttachment, IGlPresentableOpenGlSurface - { - private readonly GlOpenGlBitmapImpl _bitmap; - private readonly IGlContext _context; - private readonly Action _presentCallback; - private readonly int _fbo; - private readonly int _texture; - private readonly int _frontBuffer; - private bool _disposed; - private readonly DisposableLock _lock = new DisposableLock(); - - public unsafe SharedOpenGlBitmapAttachment(GlOpenGlBitmapImpl bitmap, IGlContext context, Action presentCallback) - { - _bitmap = bitmap; - _context = context; - _presentCallback = presentCallback; - using (_context.EnsureCurrent()) - { - var glVersion = _context.Version; - InternalFormat = glVersion.Type == GlProfileType.OpenGLES && glVersion.Major == 2 - ? GL_RGBA - : GL_RGBA8; - - _context.GlInterface.GetIntegerv(GL_FRAMEBUFFER_BINDING, out _fbo); - if (_fbo == 0) - throw new OpenGlException("Current FBO is 0"); - - { - var gl = _context.GlInterface; - - Span textures = stackalloc int[2]; - fixed (int* ptex = textures) - gl.GenTextures(2, ptex); - _texture = textures[0]; - _frontBuffer = textures[1]; - - gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); - foreach (var t in textures) - { - gl.BindTexture(GL_TEXTURE_2D, t); - gl.TexImage2D(GL_TEXTURE_2D, 0, - InternalFormat, - _bitmap.PixelSize.Width, _bitmap.PixelSize.Height, - 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); - - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - } - - gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); - gl.BindTexture(GL_TEXTURE_2D, oldTexture); - } - } - } - - public void Present() - { - using (_context.EnsureCurrent()) - { - if (_disposed) - throw new ObjectDisposedException(nameof(SharedOpenGlBitmapAttachment)); - - var gl = _context.GlInterface; - - gl.Finish(); - using (Lock()) - { - gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var oldFbo); - gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); - gl.GetIntegerv(GL_ACTIVE_TEXTURE, out var oldActive); - - gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo); - gl.ActiveTexture(GL_TEXTURE0); - gl.BindTexture(GL_TEXTURE_2D, _frontBuffer); - - gl.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, _bitmap.PixelSize.Width, - _bitmap.PixelSize.Height); - - gl.BindFramebuffer(GL_FRAMEBUFFER, oldFbo); - gl.ActiveTexture(oldActive); - gl.BindTexture(GL_TEXTURE_2D, oldTexture); - - gl.Finish(); - } - } - - _bitmap.Present(this); - _presentCallback(); - } - - public unsafe void Dispose() - { - var gl = _context.GlInterface; - _bitmap.Present(null); - - if(_disposed) - return; - using (_context.MakeCurrent()) - using (Lock()) - { - if(_disposed) - return; - _disposed = true; - var ptex = stackalloc[] { _texture, _frontBuffer }; - gl.DeleteTextures(2, ptex); - } - } - - int IGlPresentableOpenGlSurface.GetTextureId() - { - return _frontBuffer; - } - - public int InternalFormat { get; } - - public IDisposable Lock() => _lock.Lock(); - } -} diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index e24d805050..802736119f 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -37,6 +37,13 @@ namespace Avalonia.Skia } } + public ImmutableBitmap(SKImage image) + { + _image = image; + PixelSize = new PixelSize(image.Width, image.Height); + Dpi = new Vector(96, 96); + } + public ImmutableBitmap(ImmutableBitmap src, PixelSize destinationSize, BitmapInterpolationMode interpolationMode) { SKImageInfo info = new SKImageInfo(destinationSize.Width, destinationSize.Height, SKColorType.Bgra8888); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index b202b60cdf..23b7edcc8f 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -2,13 +2,8 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Threading; - -using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.Platform; using Avalonia.Media.Imaging; using SkiaSharp; diff --git a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs index 4949f4a50d..0cf66767cb 100644 --- a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -43,5 +43,7 @@ internal class SkiaContext : IPlatformRenderInterfaceContext "Don't know how to create a Skia render target from any of provided surfaces"); } + public bool IsLost => _gpu.IsLost; + public object TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); -} \ No newline at end of file +} diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 5887ba2172..25c351c908 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -238,6 +238,7 @@ namespace Avalonia.Direct2D1 } public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => _platform.CreateRenderTarget(surfaces); + public bool IsLost => false; } public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs index a749ed1b45..1357e8aa63 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs @@ -52,6 +52,44 @@ namespace Avalonia.Win32.DirectX D3D11_USAGE_STAGING = 3, } + + [Flags] + internal enum D3D11_RESOURCE_MISC_FLAG + { + D3D11_RESOURCE_MISC_GENERATE_MIPS = 0x00000001, + D3D11_RESOURCE_MISC_SHARED = 0x00000002, + D3D11_RESOURCE_MISC_TEXTURECUBE = 0x00000004, + D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS = 0x00000010, + D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS = 0x00000020, + D3D11_RESOURCE_MISC_BUFFER_STRUCTURED = 0x00000040, + D3D11_RESOURCE_MISC_RESOURCE_CLAMP = 0x00000080, + D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX = 0x00000100, + D3D11_RESOURCE_MISC_GDI_COMPATIBLE = 0x00000200, + D3D11_RESOURCE_MISC_SHARED_NTHANDLE = 0x00000800, + D3D11_RESOURCE_MISC_RESTRICTED_CONTENT = 0x00001000, + D3D11_RESOURCE_MISC_RESTRICT_SHARED_RESOURCE = 0x00002000, + D3D11_RESOURCE_MISC_RESTRICT_SHARED_RESOURCE_DRIVER = 0x00004000, + D3D11_RESOURCE_MISC_GUARDED = 0x00008000, + D3D11_RESOURCE_MISC_TILE_POOL = 0x00020000, + D3D11_RESOURCE_MISC_TILED = 0x00040000, + D3D11_RESOURCE_MISC_HW_PROTECTED = 0x00080000, + } + + [Flags] + internal enum D3D11_BIND_FLAG + { + D3D11_BIND_VERTEX_BUFFER = 0x00000001, + D3D11_BIND_INDEX_BUFFER = 0x00000002, + D3D11_BIND_CONSTANT_BUFFER = 0x00000004, + D3D11_BIND_SHADER_RESOURCE = 0x00000008, + D3D11_BIND_STREAM_OUTPUT = 0x00000010, + D3D11_BIND_RENDER_TARGET = 0x00000020, + D3D11_BIND_DEPTH_STENCIL = 0x00000040, + D3D11_BIND_UNORDERED_ACCESS = 0x00000080, + D3D11_BIND_DECODER = 0x00000200, + D3D11_BIND_VIDEO_ENCODER = 0x00000400, + } + internal enum DXGI_SWAP_EFFECT { DXGI_SWAP_EFFECT_DISCARD = 0, diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs index 47451831a6..c11a6026e7 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs @@ -282,11 +282,11 @@ namespace Avalonia.Win32.DirectX public D3D11_USAGE Usage; - public uint BindFlags; + public D3D11_BIND_FLAG BindFlags; public uint CPUAccessFlags; - public uint MiscFlags; + public D3D11_RESOURCE_MISC_FLAG MiscFlags; } #nullable restore } diff --git a/src/Windows/Avalonia.Win32/DirectX/directx.idl b/src/Windows/Avalonia.Win32/DirectX/directx.idl index a4552eedea..1d66c898db 100644 --- a/src/Windows/Avalonia.Win32/DirectX/directx.idl +++ b/src/Windows/Avalonia.Win32/DirectX/directx.idl @@ -10,6 +10,7 @@ @clr-map SIZE Avalonia.Win32.Interop.UnmanagedMethods.SIZE @clr-map POINT Avalonia.Win32.Interop.UnmanagedMethods.POINT @clr-map HWND IntPtr +@clr-map HANDLE IntPtr @clr-map BOOL int @clr-map DWORD int @clr-map SIZE_T IntPtr @@ -259,10 +260,28 @@ interface IDXGISurface : IDXGIDeviceSubObject HRESULT Unmap(); } +[uuid( 035f3ab4-482e-4e50-b41f-8a7f8bd8960b)] +interface IDXGIResource : IDXGIDeviceSubObject +{ + HRESULT GetSharedHandle( [out, annotation("_Out_")] HANDLE * pSharedHandle ); + HRESULT GetUsage( [out] DXGI_USAGE * pUsage ); + HRESULT SetEvictionPriority( [in] UINT EvictionPriority ); + HRESULT GetEvictionPriority( [out, retval, annotation("_Out_")] UINT* pEvictionPriority ); +}; + + +[ uuid( 9d8e1289-d7b3-465f-8126-250e349af85d)] +interface IDXGIKeyedMutex : + IDXGIDeviceSubObject +{ + HRESULT AcquireSync( [in] UINT64 Key, [in] uint dwMilliseconds); + HRESULT ReleaseSync( [in] UINT64 Key); +}; + [uuid(770aae78-f26f-4dba-a829-253c83d1b387)] interface IDXGIFactory1 : IDXGIFactory { - HRESULT EnumAdapters1([in] UINT Adapter, [out, annotation("_COM_Outptr_")] IDXGIAdapter1** ppAdapter); + int EnumAdapters1([in] UINT Adapter, [out] void** ppAdapter); BOOL IsCurrent(); } @@ -339,9 +358,9 @@ interface ID3D11Device : IUnknown IntPtr pInitialData, [out, retval] IUnknown** ppTexture1D ); HRESULT CreateTexture2D( - IntPtr pDesc, + D3D11_TEXTURE2D_DESC* pDesc, IntPtr pInitialData, - [out, retval] IUnknown** ppTexture2D ); + [out, retval] ID3D11Texture2D** ppTexture2D ); HRESULT CreateTexture3D( IntPtr pDesc, IntPtr pInitialData, @@ -481,3 +500,11 @@ interface ID3D11Device : IUnknown HRESULT SetExceptionMode( UINT RaiseFlags ); UINT GetExceptionMode(); } + + +[uuid( 6f15aaf2-d208-4e89-9ab4-489535d34f9c)] +interface ID3D11Texture2D : IUnknown +{ + // Just a marker interface for now +}; + diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs new file mode 100644 index 0000000000..b0fa76fda2 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs @@ -0,0 +1,107 @@ +#nullable enable +using System; +using System.Threading; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; +using Avalonia.Platform; +using Avalonia.Win32.DirectX; +using MicroCom.Runtime; +using static Avalonia.OpenGL.Egl.EglConsts; +using static Avalonia.OpenGL.GlConsts; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleExternalMemoryD3D11Texture2D : IGlExternalImageTexture +{ + private readonly EglContext _context; + private ID3D11Texture2D _texture2D; + private EglSurface _eglSurface; + private IDXGIKeyedMutex _mutex; + + public unsafe AngleExternalMemoryD3D11Texture2D(EglContext context, ID3D11Texture2D texture2D, PlatformGraphicsExternalImageProperties props) + { + _context = context; + _texture2D = texture2D.CloneReference(); + _mutex = _texture2D.QueryInterface(); + Properties = props; + + InternalFormat = GL_RGBA8; + + _eglSurface = _context.Display.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, texture2D.GetNativeIntPtr(), + new[] + { + EGL_WIDTH, props.Width, EGL_HEIGHT, props.Height, EGL_TEXTURE_FORMAT, EGL_TEXTURE_RGBA, + EGL_TEXTURE_TARGET, EGL_TEXTURE_2D, EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_RGBA, EGL_NONE, EGL_NONE, + EGL_NONE + }); + + var gl = _context.GlInterface; + int temp = 0; + gl.GenTextures(1, &temp); + TextureId = temp; + gl.BindTexture(GlConsts.GL_TEXTURE_2D, TextureId); + + if (_context.Display.EglInterface.BindTexImage(_context.Display.Handle, _eglSurface.DangerousGetHandle(), + EGL_BACK_BUFFER) == 0) + + throw OpenGlException.GetFormattedException("eglBindTexImage", _context.Display.EglInterface); + } + + public void Dispose() + { + + if (!_context.IsLost && TextureId != 0) + using (_context.EnsureCurrent()) + _context.GlInterface.DeleteTexture(TextureId); + TextureId = 0; + _eglSurface?.Dispose(); + _eglSurface = null!; + _texture2D?.Dispose(); + _texture2D = null!; + _mutex?.Dispose(); + _mutex = null!; + } + + + public void AcquireKeyedMutex(uint key) => _mutex.AcquireSync(key, int.MaxValue); + + public void ReleaseKeyedMutex(uint key) => _mutex.ReleaseSync(key); + + public int TextureId { get; private set; } + public int InternalFormat { get; } + public PlatformGraphicsExternalImageProperties Properties { get; } +} + +internal class AngleExternalMemoryD3D11ExportedTexture2D : AngleExternalMemoryD3D11Texture2D, IGlExportableExternalImageTexture +{ + static IPlatformHandle GetHandle(ID3D11Texture2D texture2D) + { + using var resource = texture2D.QueryInterface(); + return new PlatformHandle(resource.SharedHandle, + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle); + } + + public AngleExternalMemoryD3D11ExportedTexture2D(EglContext context, ID3D11Texture2D texture2D, + D3D11_TEXTURE2D_DESC desc, + PlatformGraphicsExternalImageFormat format) + : this(context, texture2D, GetHandle(texture2D), + new PlatformGraphicsExternalImageProperties + { + Width = (int)desc.Width, Height = (int)desc.Height, Format = format + }) + { + + } + + private AngleExternalMemoryD3D11ExportedTexture2D(EglContext context, ID3D11Texture2D texture2D, + IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties) + : base(context, texture2D, properties) + { + Handle = handle; + } + + public IPlatformHandle Handle { get; } + public IPlatformHandle GetHandle() => Handle; + +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs new file mode 100644 index 0000000000..a7b52e953e --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Controls.Documents; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Avalonia.Win32.DirectX; +using MicroCom.Runtime; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleExternalObjectsFeature : IGlContextExternalObjectsFeature, IDisposable +{ + private readonly EglContext _context; + private readonly ID3D11Device _device; + + public AngleExternalObjectsFeature(EglContext context) + { + _context = context; + var angle = (AngleWin32EglDisplay)context.Display; + _device = MicroComRuntime.CreateProxyFor(angle.GetDirect3DDevice(), false).CloneReference(); + using var dxgiDevice = _device.QueryInterface(); + using var adapter = dxgiDevice.Adapter; + DeviceLuid = BitConverter.GetBytes(adapter.Desc.AdapterLuid); + } + + public IReadOnlyList SupportedImportableExternalImageTypes { get; } = new[] + { + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle + }; + + public IReadOnlyList SupportedExportableExternalImageTypes => SupportedImportableExternalImageTypes; + public IReadOnlyList SupportedImportableExternalSemaphoreTypes => Array.Empty(); + public IReadOnlyList SupportedExportableExternalSemaphoreTypes => Array.Empty(); + + public IReadOnlyList GetSupportedFormatsForExternalMemoryType(string type) => + new[] + { + PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm + }; + + public unsafe IGlExportableExternalImageTexture CreateImage(string type, PixelSize size, + PlatformGraphicsExternalImageFormat format) + { + if (format != PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm) + throw new NotSupportedException("Unsupported external memory format"); + using (_context.EnsureCurrent()) + { + var fmt = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM; + + var desc = new D3D11_TEXTURE2D_DESC + { + Format = fmt, + Width = (uint)size.Width, + Height = (uint)size.Height, + ArraySize = 1, + MipLevels = 1, + SampleDesc = new DXGI_SAMPLE_DESC { Count = 1, Quality = 0 }, + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + CPUAccessFlags = 0, + MiscFlags = D3D11_RESOURCE_MISC_FLAG.D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX, + BindFlags = D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET | D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE, + }; + using var texture = _device.CreateTexture2D(&desc, IntPtr.Zero); + return new AngleExternalMemoryD3D11ExportedTexture2D(_context, texture, desc, format); + } + } + + public IGlExportableExternalImageTexture CreateSemaphore(string type) => throw new NotSupportedException(); + + public unsafe IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties) + { + if (handle.HandleDescriptor != KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle) + throw new NotSupportedException("Unsupported external memory type"); + + using (_context.EnsureCurrent()) + { + var guid = MicroComRuntime.GetGuidFor(typeof(ID3D11Texture2D)); + using var opened = _device.OpenSharedResource(handle.Handle, &guid); + using var texture = opened.QueryInterface(); + return new AngleExternalMemoryD3D11Texture2D(_context, texture, properties); + } + } + + public IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle) => throw new NotSupportedException(); + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + { + if (imageHandleType == KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle + || imageHandleType == KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureNtHandle) + return CompositionGpuImportedImageSynchronizationCapabilities.KeyedMutex; + return default; + } + + public byte[] DeviceLuid { get; } + public byte[] DeviceUuid { get; } + + public void Dispose() + { + _device.Dispose(); + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs index b2d7b2014b..53bf2fb8b1 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs @@ -1,5 +1,8 @@ +#nullable enable annotations using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Runtime.InteropServices; using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; @@ -7,6 +10,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.Win32.DirectX; using MicroCom.Runtime; using static Avalonia.OpenGL.Egl.EglConsts; +// ReSharper disable SimplifyLinqExpressionUseMinByAndMaxBy namespace Avalonia.Win32.OpenGl.Angle { @@ -40,51 +44,90 @@ namespace Avalonia.Win32.OpenGl.Angle }, AngleOptions.PlatformApi.DirectX11); } - public static AngleWin32EglDisplay CreateD3D11Display(Win32AngleEglInterface egl) + public static unsafe AngleWin32EglDisplay CreateD3D11Display(Win32AngleEglInterface egl, + bool preferDiscreteAdapter = false) { - unsafe + var featureLevels = new[] { - var featureLevels = new[] - { - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_0, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_3, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_2, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_1 - }; - - DirectXUnmanagedMethods.D3D11CreateDevice(IntPtr.Zero, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, - IntPtr.Zero, 0, featureLevels, (uint)featureLevels.Length, - 7, out var pD3dDevice, out var featureLevel, null); - if (pD3dDevice == IntPtr.Zero) - throw new Win32Exception("Unable to create D3D11 Device"); + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_1 + }; - var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); - var angleDevice = IntPtr.Zero; - var display = IntPtr.Zero; + var dxgiFactoryGuid = MicroComRuntime.GetGuidFor(typeof(IDXGIFactory1)); + DirectXUnmanagedMethods.CreateDXGIFactory1(ref dxgiFactoryGuid, out var pDxgiFactory); + IDXGIAdapter1? chosenAdapter = null; + if (pDxgiFactory != null) + { + using var factory = MicroComRuntime.CreateProxyFor(pDxgiFactory, true); - void Cleanup() + void* pAdapter = null; + if (preferDiscreteAdapter) { - if (angleDevice != IntPtr.Zero) - egl.ReleaseDeviceANGLE(angleDevice); - d3dDevice.Dispose(); + ushort adapterIndex = 0; + var adapters = new List<(IDXGIAdapter1 adapter, string name)>(); + while (factory.EnumAdapters1(adapterIndex, &pAdapter) == 0) + { + var adapter = MicroComRuntime.CreateProxyFor(pAdapter, true); + var desc = adapter.Desc1; + var name = Marshal.PtrToStringUni(new IntPtr(desc.Description))!.ToLowerInvariant(); + adapters.Add((adapter, name)); + adapterIndex++; + } + + if (adapters.Count == 0) + throw new OpenGlException("No adapters found"); + chosenAdapter = adapters + .OrderByDescending(x => x.name.Contains("nvidia") ? 2 : x.name.Contains("amd") ? 1 : 0) + .First().adapter.CloneReference(); + foreach (var a in adapters) + a.adapter.Dispose(); } - - bool success = false; - try + else { - angleDevice = egl.CreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE, pD3dDevice, null); - if (angleDevice == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateDeviceANGLE", egl); + if (factory.EnumAdapters1(0, &pAdapter) != 0) + throw new OpenGlException("No adapters found"); + chosenAdapter = MicroComRuntime.CreateProxyFor(pAdapter, true); + } + } + + IntPtr pD3dDevice; + using (chosenAdapter) + DirectXUnmanagedMethods.D3D11CreateDevice(chosenAdapter?.GetNativeIntPtr() ?? IntPtr.Zero, + D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_UNKNOWN, + IntPtr.Zero, 0, featureLevels, (uint)featureLevels.Length, + 7, out pD3dDevice, out var featureLevel, null); + - display = egl.GetPlatformDisplayExt(EGL_PLATFORM_DEVICE_EXT, angleDevice, null); - if (display == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglGetPlatformDisplayEXT", egl); + if (pD3dDevice == IntPtr.Zero) + throw new Win32Exception("Unable to create D3D11 Device"); + var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); + var angleDevice = IntPtr.Zero; + var display = IntPtr.Zero; - var rv = new AngleWin32EglDisplay(display, new EglDisplayOptions + void Cleanup() + { + if (angleDevice != IntPtr.Zero) + egl.ReleaseDeviceANGLE(angleDevice); + d3dDevice.Dispose(); + } + + bool success = false; + try + { + angleDevice = egl.CreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE, pD3dDevice, null); + if (angleDevice == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateDeviceANGLE", egl); + + display = egl.GetPlatformDisplayExt(EGL_PLATFORM_DEVICE_EXT, angleDevice, null); + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetPlatformDisplayEXT", egl); + + + var rv = new AngleWin32EglDisplay(display, + new EglDisplayOptions { DisposeCallback = Cleanup, Egl = egl, @@ -92,17 +135,16 @@ namespace Avalonia.Win32.OpenGl.Angle DeviceLostCheckCallback = () => d3dDevice.DeviceRemovedReason != 0, GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles }, AngleOptions.PlatformApi.DirectX11); - success = true; - return rv; - } - finally + success = true; + return rv; + } + finally + { + if (!success) { - if (!success) - { - if (display != IntPtr.Zero) - egl.Terminate(display); - Cleanup(); - } + if (display != IntPtr.Zero) + egl.Terminate(display); + Cleanup(); } } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs index 9a829aff92..a4d1ea457a 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs @@ -9,7 +9,7 @@ using Avalonia.Platform; namespace Avalonia.Win32.OpenGl.Angle; -internal class AngleWin32PlatformGraphics : IPlatformGraphics +internal class AngleWin32PlatformGraphics : IPlatformGraphics, IPlatformGraphicsOpenGlContextFactory { private readonly Win32AngleEglInterface _egl; private AngleWin32EglDisplay _sharedDisplay; @@ -29,9 +29,10 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics var rv = display.CreateContext(new EglContextOptions { DisposeCallback = display.Dispose, - ExtraFeatures = new Dictionary + ExtraFeatures = new Dictionary> { - [typeof(IGlPlatformSurfaceRenderTargetFactory)] = new AngleD3DTextureFeature() + [typeof(IGlPlatformSurfaceRenderTargetFactory)] = _ => new AngleD3DTextureFeature(), + [typeof(IGlContextExternalObjectsFeature)] = context => new AngleExternalObjectsFeature(context) } }); success = true; @@ -73,8 +74,6 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics public static AngleWin32PlatformGraphics TryCreate(AngleOptions options) { - - Win32AngleEglInterface egl; try { @@ -128,4 +127,13 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics } return null; } + + public IGlContext CreateContext(IEnumerable? versions) + { + if (UsesSharedContext) + throw new InvalidOperationException(); + if (versions != null && versions.All(v => v.Type != GlProfileType.OpenGLES || v.Major != 3)) + throw new OpenGlException("Unable to create context with requested version"); + return (IGlContext)CreateContext(); + } } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 25ea060576..ab6de1a027 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -37,6 +37,9 @@ namespace Avalonia.Win32 if (egl != null && egl.PlatformApi == AngleOptions.PlatformApi.DirectX11) { + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(egl); + if (opts.UseWindowsUIComposition) { WinUiCompositorConnection.TryCreateAndRegister(); diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 42e33729ac..6e0d7f657a 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -15,6 +15,8 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } + public bool IsLost => false; + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index a272d89b8a..5b28453c17 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -46,6 +46,8 @@ namespace Avalonia.Benchmarks throw new NotImplementedException(); } + public bool IsLost => false; + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index f9e1e45098..143937307e 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -57,6 +57,8 @@ namespace Avalonia.UnitTests return new MockRenderTarget(); } + public bool IsLost => false; + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)