Avalonia - Use embedded window for avalonia (#3674)

* wip

* use embedded window

* fix race condition on opengl Windows

* fix glx issues on prime nvidia

* fix mouse support win32

* clean up

* addressed review

* addressed review

* fix warnings

* fix sotware keyboard dialog

* Update Ryujinx.Ava/Ui/Applet/SwkbdAppletDialog.axaml.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* remove double semi

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
This commit is contained in:
Emmanuel Hansen 2022-09-19 18:05:26 +00:00 committed by GitHub
parent 7922856ca4
commit 40e84f3f40
58 changed files with 868 additions and 3531 deletions

View file

@ -1,4 +1,8 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
@ -27,9 +31,69 @@ namespace Ryujinx.Ava.Ui.Controls
{
UserResult result = UserResult.None;
ContentDialog contentDialog = new ContentDialog();
bool useOverlay = false;
Window mainWindow = null;
await ShowDialog();
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al)
{
foreach (var item in al.Windows)
{
if (item.IsActive && item is MainWindow window && window.ViewModel.IsGameRunning)
{
mainWindow = window;
useOverlay = true;
break;
}
}
}
ContentDialog contentDialog = null;
ContentDialogOverlayWindow overlay = null;
if (useOverlay)
{
overlay = new ContentDialogOverlayWindow()
{
Height = mainWindow.Bounds.Height,
Width = mainWindow.Bounds.Width,
Position = mainWindow.PointToScreen(new Point())
};
mainWindow.PositionChanged += OverlayOnPositionChanged;
void OverlayOnPositionChanged(object sender, PixelPointEventArgs e)
{
overlay.Position = mainWindow.PointToScreen(new Point());
}
contentDialog = overlay.ContentDialog;
bool opened = false;
overlay.Opened += OverlayOnActivated;
async void OverlayOnActivated(object sender, EventArgs e)
{
if (opened)
{
return;
}
opened = true;
overlay.Position = mainWindow.PointToScreen(new Point());
await ShowDialog();
}
await overlay.ShowDialog(mainWindow);
}
else
{
contentDialog = new ContentDialog();
await ShowDialog();
}
async Task ShowDialog()
{
@ -53,6 +117,14 @@ namespace Ryujinx.Ava.Ui.Controls
});
await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
overlay?.Close();
}
if (useOverlay)
{
overlay.Content = null;
overlay.Close();
}
return result;
@ -323,4 +395,4 @@ namespace Ryujinx.Ava.Ui.Controls
return string.Empty;
}
}
}
}

View file

@ -0,0 +1,204 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform;
using SPB.Graphics;
using SPB.Platform;
using SPB.Platform.GLX;
using SPB.Platform.X11;
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using static Ryujinx.Ava.Ui.Controls.Win32NativeInterop;
namespace Ryujinx.Ava.Ui.Controls
{
public unsafe class EmbeddedWindow : NativeControlHost
{
private WindowProc _wndProcDelegate;
private string _className;
protected GLXWindow X11Window { get; private set; }
protected IntPtr WindowHandle { get; set; }
protected IntPtr X11Display { get; set; }
public event EventHandler<IntPtr> WindowCreated;
public event EventHandler<Size> SizeChanged;
protected virtual void OnWindowDestroyed() { }
protected virtual void OnWindowDestroying()
{
WindowHandle = IntPtr.Zero;
X11Display = IntPtr.Zero;
}
public EmbeddedWindow()
{
var stateObserverable = this.GetObservable(Control.BoundsProperty);
stateObserverable.Subscribe(StateChanged);
this.Initialized += NativeEmbeddedWindow_Initialized;
}
public virtual void OnWindowCreated() { }
private void NativeEmbeddedWindow_Initialized(object sender, EventArgs e)
{
OnWindowCreated();
Task.Run(() =>
{
WindowCreated?.Invoke(this, WindowHandle);
});
}
private void StateChanged(Rect rect)
{
SizeChanged?.Invoke(this, rect.Size);
}
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
{
if (OperatingSystem.IsLinux())
{
return CreateLinux(parent);
}
else if (OperatingSystem.IsWindows())
{
return CreateWin32(parent);
}
return base.CreateNativeControlCore(parent);
}
protected override void DestroyNativeControlCore(IPlatformHandle control)
{
OnWindowDestroying();
if (OperatingSystem.IsLinux())
{
DestroyLinux();
}
else if (OperatingSystem.IsWindows())
{
DestroyWin32(control);
}
else
{
base.DestroyNativeControlCore(control);
}
OnWindowDestroyed();
}
[SupportedOSPlatform("linux")]
IPlatformHandle CreateLinux(IPlatformHandle parent)
{
X11Window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100) as GLXWindow;
WindowHandle = X11Window.WindowHandle.RawHandle;
X11Display = X11Window.DisplayHandle.RawHandle;
return new PlatformHandle(WindowHandle, "X11");
}
[SupportedOSPlatform("windows")]
unsafe IPlatformHandle CreateWin32(IPlatformHandle parent)
{
_className = "NativeWindow-" + Guid.NewGuid();
_wndProcDelegate = WndProc;
var wndClassEx = new WNDCLASSEX
{
cbSize = Marshal.SizeOf<WNDCLASSEX>(),
hInstance = GetModuleHandle(null),
lpfnWndProc = _wndProcDelegate,
style = ClassStyles.CS_OWNDC,
lpszClassName = _className,
hCursor = LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW)
};
var atom = RegisterClassEx(ref wndClassEx);
var handle = CreateWindowEx(
0,
_className,
"NativeWindow",
WindowStyles.WS_CHILD,
0,
0,
640,
480,
parent.Handle,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero);
WindowHandle = handle;
return new PlatformHandle(WindowHandle, "HWND");
}
[SupportedOSPlatform("windows")]
internal IntPtr WndProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam)
{
var point = new Point((long)lParam & 0xFFFF, ((long)lParam >> 16) & 0xFFFF);
var root = VisualRoot as Window;
bool isLeft = false;
switch (msg)
{
case WindowsMessages.LBUTTONDOWN:
case WindowsMessages.RBUTTONDOWN:
isLeft = msg == WindowsMessages.LBUTTONDOWN;
this.RaiseEvent(new PointerPressedEventArgs(
this,
new Avalonia.Input.Pointer(0, PointerType.Mouse, true),
root,
this.TranslatePoint(point, root).Value,
(ulong)Environment.TickCount64,
new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonPressed : PointerUpdateKind.RightButtonPressed),
KeyModifiers.None));
break;
case WindowsMessages.LBUTTONUP:
case WindowsMessages.RBUTTONUP:
isLeft = msg == WindowsMessages.LBUTTONUP;
this.RaiseEvent(new PointerReleasedEventArgs(
this,
new Avalonia.Input.Pointer(0, PointerType.Mouse, true),
root,
this.TranslatePoint(point, root).Value,
(ulong)Environment.TickCount64,
new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonReleased : PointerUpdateKind.RightButtonReleased),
KeyModifiers.None,
isLeft ? MouseButton.Left : MouseButton.Right));
break;
case WindowsMessages.MOUSEMOVE:
this.RaiseEvent(new PointerEventArgs(
PointerMovedEvent,
this,
new Avalonia.Input.Pointer(0, PointerType.Mouse, true),
root,
this.TranslatePoint(point, root).Value,
(ulong)Environment.TickCount64,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.Other),
KeyModifiers.None));
break;
}
return DefWindowProc(hWnd, msg, (IntPtr)wParam, (IntPtr)lParam);
}
void DestroyLinux()
{
X11Window?.Dispose();
}
[SupportedOSPlatform("windows")]
void DestroyWin32(IPlatformHandle handle)
{
DestroyWindow(handle.Handle);
UnregisterClass(_className, GetModuleHandle(null));
}
}
}

View file

@ -0,0 +1,85 @@
using Avalonia;
using Avalonia.OpenGL;
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Configuration;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using SPB.Platform;
using SPB.Platform.GLX;
using SPB.Platform.WGL;
using SPB.Windowing;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
public class OpenGLEmbeddedWindow : EmbeddedWindow
{
private readonly int _major;
private readonly int _minor;
private readonly GraphicsDebugLevel _graphicsDebugLevel;
private SwappableNativeWindowBase _window;
public OpenGLContextBase Context { get; set; }
public OpenGLEmbeddedWindow(int major, int minor, GraphicsDebugLevel graphicsDebugLevel)
{
_major = major;
_minor = minor;
_graphicsDebugLevel = graphicsDebugLevel;
}
protected override void OnWindowDestroying()
{
Context.Dispose();
base.OnWindowDestroying();
}
public override void OnWindowCreated()
{
base.OnWindowCreated();
if (OperatingSystem.IsWindows())
{
_window = new WGLWindow(new NativeHandle(WindowHandle));
}
else if (OperatingSystem.IsLinux())
{
_window = X11Window;
}
else
{
throw new PlatformNotSupportedException();
}
var flags = OpenGLContextFlags.Compat;
if (_graphicsDebugLevel != GraphicsDebugLevel.None)
{
flags |= OpenGLContextFlags.Debug;
}
Context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, _major, _minor, flags);
Context.Initialize(_window);
Context.MakeCurrent(_window);
var bindingsContext = new OpenToolkitBindingsContext(Context.GetProcAddress);
GL.LoadBindings(bindingsContext);
Context.MakeCurrent(null);
}
public void MakeCurrent()
{
Context.MakeCurrent(_window);
}
public void MakeCurrent(NativeWindowBase window)
{
Context.MakeCurrent(window);
}
public void SwapBuffers()
{
_window.SwapBuffers();
}
}
}

View file

@ -1,192 +0,0 @@
using Avalonia;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using Avalonia.Threading;
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Configuration;
using SkiaSharp;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using SPB.Platform;
using SPB.Windowing;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
internal class OpenGLRendererControl : RendererControl
{
public int Major { get; }
public int Minor { get; }
public OpenGLContextBase GameContext { get; set; }
public static OpenGLContextBase PrimaryContext =>
AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>()
.PrimaryContext.AsOpenGLContextBase();
private SwappableNativeWindowBase _gameBackgroundWindow;
private IntPtr _fence;
public OpenGLRendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel)
{
Major = major;
Minor = minor;
}
public override void DestroyBackgroundContext()
{
Image = null;
if (_fence != IntPtr.Zero)
{
DrawOperation.Dispose();
GL.DeleteSync(_fence);
}
GlDrawOperation.DeleteFramebuffer();
GameContext?.Dispose();
_gameBackgroundWindow?.Dispose();
}
internal override void Present(object image)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
Image = (int)image;
InvalidateVisual();
}).Wait();
if (_fence != IntPtr.Zero)
{
GL.DeleteSync(_fence);
}
_fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
InvalidateVisual();
_gameBackgroundWindow.SwapBuffers();
}
internal override void MakeCurrent()
{
GameContext.MakeCurrent(_gameBackgroundWindow);
}
internal override void MakeCurrent(SwappableNativeWindowBase window)
{
GameContext.MakeCurrent(window);
}
protected override void CreateWindow()
{
var flags = OpenGLContextFlags.Compat;
if (DebugLevel != GraphicsDebugLevel.None)
{
flags |= OpenGLContextFlags.Debug;
}
_gameBackgroundWindow = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
_gameBackgroundWindow.Hide();
GameContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, Major, Minor, flags, shareContext: PrimaryContext);
GameContext.Initialize(_gameBackgroundWindow);
}
protected override ICustomDrawOperation CreateDrawOperation()
{
return new GlDrawOperation(this);
}
private class GlDrawOperation : ICustomDrawOperation
{
private static int _framebuffer;
public Rect Bounds { get; }
private readonly OpenGLRendererControl _control;
public GlDrawOperation(OpenGLRendererControl control)
{
_control = control;
Bounds = _control.Bounds;
}
public void Dispose() { }
public static void DeleteFramebuffer()
{
if (_framebuffer == 0)
{
GL.DeleteFramebuffer(_framebuffer);
}
_framebuffer = 0;
}
public bool Equals(ICustomDrawOperation other)
{
return other is GlDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds;
}
public bool HitTest(Point p)
{
return Bounds.Contains(p);
}
private void CreateRenderTarget()
{
_framebuffer = GL.GenFramebuffer();
}
public void Render(IDrawingContextImpl context)
{
if (_control.Image == null)
{
return;
}
if (_framebuffer == 0)
{
CreateRenderTarget();
}
int currentFramebuffer = GL.GetInteger(GetPName.FramebufferBinding);
var image = _control.Image;
var fence = _control._fence;
GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer);
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, (int)image, 0);
GL.BindFramebuffer(FramebufferTarget.Framebuffer, currentFramebuffer);
if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl)
{
return;
}
var imageInfo = new SKImageInfo((int)_control.RenderSize.Width, (int)_control.RenderSize.Height, SKColorType.Rgba8888);
var glInfo = new GRGlFramebufferInfo((uint)_framebuffer, SKColorType.Rgba8888.ToGlSizedFormat());
GL.WaitSync(fence, WaitSyncFlags.None, ulong.MaxValue);
using var backendTexture = new GRBackendRenderTarget(imageInfo.Width, imageInfo.Height, 1, 0, glInfo);
using var surface = SKSurface.Create(skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888);
if (surface == null)
{
return;
}
var rect = new Rect(new Point(), _control.RenderSize);
using var snapshot = surface.Snapshot();
skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint());
}
}
}
}

View file

@ -1,96 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Rendering.SceneGraph;
using Ryujinx.Common.Configuration;
using SPB.Windowing;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
internal abstract class RendererControl : Control
{
protected object Image { get; set; }
public event EventHandler<EventArgs> RendererInitialized;
public event EventHandler<Size> SizeChanged;
protected Size RenderSize { get; private set; }
public bool IsStarted { get; private set; }
public GraphicsDebugLevel DebugLevel { get; }
private bool _isInitialized;
protected ICustomDrawOperation DrawOperation { get; private set; }
public RendererControl(GraphicsDebugLevel graphicsDebugLevel)
{
DebugLevel = graphicsDebugLevel;
IObservable<Rect> resizeObservable = this.GetObservable(BoundsProperty);
resizeObservable.Subscribe(Resized);
Focusable = true;
}
protected void Resized(Rect rect)
{
SizeChanged?.Invoke(this, rect.Size);
if (!rect.IsEmpty)
{
RenderSize = rect.Size * VisualRoot.RenderScaling;
DrawOperation = CreateDrawOperation();
}
}
protected abstract ICustomDrawOperation CreateDrawOperation();
protected abstract void CreateWindow();
public override void Render(DrawingContext context)
{
if (!_isInitialized)
{
CreateWindow();
OnRendererInitialized();
_isInitialized = true;
}
if (!IsStarted || Image == null)
{
return;
}
if (DrawOperation != null)
{
context.Custom(DrawOperation);
}
base.Render(context);
}
protected void OnRendererInitialized()
{
RendererInitialized?.Invoke(this, EventArgs.Empty);
}
internal abstract void Present(object image);
internal void Start()
{
IsStarted = true;
}
internal void Stop()
{
IsStarted = false;
}
public abstract void DestroyBackgroundContext();
internal abstract void MakeCurrent();
internal abstract void MakeCurrent(SwappableNativeWindowBase window);
}
}

View file

@ -0,0 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.Ui.Controls.RendererHost">
<ContentControl
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch"
Name="View"
/>
</UserControl>

View file

@ -0,0 +1,126 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ryujinx.Common.Configuration;
using Silk.NET.Vulkan;
using SPB.Graphics.OpenGL;
using SPB.Windowing;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
public partial class RendererHost : UserControl, IDisposable
{
private readonly GraphicsDebugLevel _graphicsDebugLevel;
private EmbeddedWindow _currentWindow;
public bool IsVulkan { get; private set; }
public RendererHost(GraphicsDebugLevel graphicsDebugLevel)
{
_graphicsDebugLevel = graphicsDebugLevel;
InitializeComponent();
}
public RendererHost()
{
InitializeComponent();
}
public void CreateOpenGL()
{
Dispose();
_currentWindow = new OpenGLEmbeddedWindow(3, 3, _graphicsDebugLevel);
Initialize();
IsVulkan = false;
}
private void Initialize()
{
_currentWindow.WindowCreated += CurrentWindow_WindowCreated;
_currentWindow.SizeChanged += CurrentWindow_SizeChanged;
View.Content = _currentWindow;
}
public void CreateVulkan()
{
Dispose();
_currentWindow = new VulkanEmbeddedWindow();
Initialize();
IsVulkan = true;
}
public OpenGLContextBase GetContext()
{
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
{
return openGlEmbeddedWindow.Context;
}
return null;
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
Dispose();
}
private void CurrentWindow_SizeChanged(object sender, Size e)
{
SizeChanged?.Invoke(sender, e);
}
private void CurrentWindow_WindowCreated(object sender, IntPtr e)
{
RendererInitialized?.Invoke(this, EventArgs.Empty);
}
public void MakeCurrent()
{
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
{
openGlEmbeddedWindow.MakeCurrent();
}
}
public void MakeCurrent(SwappableNativeWindowBase window)
{
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
{
openGlEmbeddedWindow.MakeCurrent(window);
}
}
public void SwapBuffers()
{
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
{
openGlEmbeddedWindow.SwapBuffers();
}
}
public event EventHandler<EventArgs> RendererInitialized;
public event Action<object, Size> SizeChanged;
public void Dispose()
{
if (_currentWindow != null)
{
_currentWindow.WindowCreated -= CurrentWindow_WindowCreated;
_currentWindow.SizeChanged -= CurrentWindow_SizeChanged;
}
}
public SurfaceKHR CreateVulkanSurface(Instance instance, Vk api)
{
return (_currentWindow is VulkanEmbeddedWindow vulkanEmbeddedWindow)
? vulkanEmbeddedWindow.CreateSurface(instance)
: default;
}
}
}

View file

@ -0,0 +1,33 @@
using Ryujinx.Ava.Ui.Controls;
using Silk.NET.Vulkan;
using SPB.Graphics.Vulkan;
using SPB.Platform.Win32;
using SPB.Platform.X11;
using SPB.Windowing;
using System;
namespace Ryujinx.Ava.Ui
{
public class VulkanEmbeddedWindow : EmbeddedWindow
{
private NativeWindowBase _window;
public SurfaceKHR CreateSurface(Instance instance)
{
if (OperatingSystem.IsWindows())
{
_window = new SimpleWin32Window(new NativeHandle(WindowHandle));
}
else if (OperatingSystem.IsLinux())
{
_window = X11Window;
}
else
{
throw new PlatformNotSupportedException();
}
return new SurfaceKHR((ulong?)VulkanHelper.CreateWindowSurface(instance.Handle, _window));
}
}
}

View file

@ -1,220 +0,0 @@
using Avalonia;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using Avalonia.Threading;
using Ryujinx.Ava.Ui.Backend.Vulkan;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Common.Configuration;
using Ryujinx.Graphics.Vulkan;
using Silk.NET.Vulkan;
using SkiaSharp;
using SPB.Windowing;
using System;
using System.Collections.Concurrent;
namespace Ryujinx.Ava.Ui.Controls
{
internal class VulkanRendererControl : RendererControl
{
private const int MaxImagesInFlight = 3;
private VulkanPlatformInterface _platformInterface;
private ConcurrentQueue<PresentImageInfo> _imagesInFlight;
private PresentImageInfo _currentImage;
public VulkanRendererControl(GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel)
{
_platformInterface = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
_imagesInFlight = new ConcurrentQueue<PresentImageInfo>();
}
public override void DestroyBackgroundContext()
{
}
protected override ICustomDrawOperation CreateDrawOperation()
{
return new VulkanDrawOperation(this);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_imagesInFlight.Clear();
if (_platformInterface.MainSurface.Display != null)
{
_platformInterface.MainSurface.Display.Presented -= Window_Presented;
}
_currentImage?.Put();
_currentImage = null;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_platformInterface.MainSurface.Display.Presented += Window_Presented;
}
private void Window_Presented(object sender, EventArgs e)
{
_platformInterface.MainSurface.Device.QueueWaitIdle();
_currentImage?.Put();
_currentImage = null;
}
public override void Render(DrawingContext context)
{
base.Render(context);
}
protected override void CreateWindow()
{
}
internal override void MakeCurrent()
{
}
internal override void MakeCurrent(SwappableNativeWindowBase window)
{
}
internal override void Present(object image)
{
Image = image;
_imagesInFlight.Enqueue((PresentImageInfo)image);
if (_imagesInFlight.Count > MaxImagesInFlight)
{
_imagesInFlight.TryDequeue(out _);
}
Dispatcher.UIThread.Post(InvalidateVisual);
}
private PresentImageInfo GetImage()
{
lock (_imagesInFlight)
{
if (!_imagesInFlight.TryDequeue(out _currentImage))
{
_currentImage = (PresentImageInfo)Image;
}
return _currentImage;
}
}
private class VulkanDrawOperation : ICustomDrawOperation
{
public Rect Bounds { get; }
private readonly VulkanRendererControl _control;
private bool _isDestroyed;
public VulkanDrawOperation(VulkanRendererControl control)
{
_control = control;
Bounds = _control.Bounds;
}
public void Dispose()
{
if (_isDestroyed)
{
return;
}
_isDestroyed = true;
}
public bool Equals(ICustomDrawOperation other)
{
return other is VulkanDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds;
}
public bool HitTest(Point p)
{
return Bounds.Contains(p);
}
public unsafe void Render(IDrawingContextImpl context)
{
if (_isDestroyed || _control.Image == null || _control.RenderSize.Width == 0 || _control.RenderSize.Height == 0 ||
context is not ISkiaDrawingContextImpl skiaDrawingContextImpl)
{
return;
}
var image = _control.GetImage();
if (!image.State.IsValid)
{
_control._currentImage = null;
return;
}
var gpu = AvaloniaLocator.Current.GetService<VulkanSkiaGpu>();
image.Get();
var imageInfo = new GRVkImageInfo()
{
CurrentQueueFamily = _control._platformInterface.PhysicalDevice.QueueFamilyIndex,
Format = (uint)Format.R8G8B8A8Unorm,
Image = image.Image.Handle,
ImageLayout = (uint)ImageLayout.TransferSrcOptimal,
ImageTiling = (uint)ImageTiling.Optimal,
ImageUsageFlags = (uint)(ImageUsageFlags.ImageUsageColorAttachmentBit
| ImageUsageFlags.ImageUsageTransferSrcBit
| ImageUsageFlags.ImageUsageTransferDstBit),
LevelCount = 1,
SampleCount = 1,
Protected = false,
Alloc = new GRVkAlloc()
{
Memory = image.Memory.Handle,
Flags = 0,
Offset = image.MemoryOffset,
Size = image.MemorySize
}
};
using var backendTexture = new GRBackendRenderTarget(
(int)image.Extent.Width,
(int)image.Extent.Height,
1,
imageInfo);
var vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
using var surface = SKSurface.Create(
skiaDrawingContextImpl.GrContext,
backendTexture,
GRSurfaceOrigin.TopLeft,
SKColorType.Rgba8888);
if (surface == null)
{
return;
}
var rect = new Rect(new Point(), new Size(image.Extent.Width, image.Extent.Height));
using var snapshot = surface.Snapshot();
skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(),
new SKPaint());
}
}
}
}

View file

@ -0,0 +1,113 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.Ava.Ui.Controls
{
[SupportedOSPlatform("windows")]
internal class Win32NativeInterop
{
[Flags]
public enum ClassStyles : uint
{
CS_CLASSDC = 0x40,
CS_OWNDC = 0x20,
}
[Flags]
public enum WindowStyles : uint
{
WS_CHILD = 0x40000000
}
public enum Cursors : uint
{
IDC_ARROW = 32512
}
public enum WindowsMessages : uint
{
MOUSEMOVE = 0x0200,
LBUTTONDOWN = 0x0201,
LBUTTONUP = 0x0202,
LBUTTONDBLCLK = 0x0203,
RBUTTONDOWN = 0x0204,
RBUTTONUP = 0x0205,
RBUTTONDBLCLK = 0x0206,
MBUTTONDOWN = 0x0207,
MBUTTONUP = 0x0208,
MBUTTONDBLCLK = 0x0209,
MOUSEWHEEL = 0x020A,
XBUTTONDOWN = 0x020B,
XBUTTONUP = 0x020C,
XBUTTONDBLCLK = 0x020D,
MOUSEHWHEEL = 0x020E,
MOUSELAST = 0x020E
}
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
internal delegate IntPtr WindowProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WNDCLASSEX
{
public int cbSize;
public ClassStyles style;
[MarshalAs(UnmanagedType.FunctionPtr)]
public WindowProc lpfnWndProc; // not WndProc
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpszMenuName;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpszClassName;
public IntPtr hIconSm;
public static WNDCLASSEX Create()
{
return new WNDCLASSEX
{
cbSize = Marshal.SizeOf<WNDCLASSEX>()
};
}
}
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern ushort RegisterClassEx(ref WNDCLASSEX param);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern short UnregisterClass([MarshalAs(UnmanagedType.LPWStr)] string lpClassName, IntPtr instance);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr DefWindowProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DestroyWindow(IntPtr hwnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr LoadCursor(IntPtr hInstance, IntPtr lpCursorName);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern IntPtr CreateWindowEx(
uint dwExStyle,
string lpClassName,
string lpWindowName,
WindowStyles dwStyle,
int x,
int y,
int nWidth,
int nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
}
}