aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/Ui/Controls
diff options
context:
space:
mode:
authorEmmanuel Hansen <emmausssss@gmail.com>2022-09-19 18:05:26 +0000
committerGitHub <noreply@github.com>2022-09-19 15:05:26 -0300
commit6f0395538b8e8af3bba7536b44780d57e51e8697 (patch)
tree4d3f4f620dd287fc1ca38ea9ea722b6e022301dd /Ryujinx.Ava/Ui/Controls
parentb9f1ff3c7748c6a2665e76d17e86c3b7228f44fe (diff)
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>
Diffstat (limited to 'Ryujinx.Ava/Ui/Controls')
-rw-r--r--Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs78
-rw-r--r--Ryujinx.Ava/Ui/Controls/EmbeddedWindow.cs204
-rw-r--r--Ryujinx.Ava/Ui/Controls/OpenGLEmbeddedWindow.cs85
-rw-r--r--Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs192
-rw-r--r--Ryujinx.Ava/Ui/Controls/RendererControl.cs96
-rw-r--r--Ryujinx.Ava/Ui/Controls/RendererHost.axaml14
-rw-r--r--Ryujinx.Ava/Ui/Controls/RendererHost.axaml.cs126
-rw-r--r--Ryujinx.Ava/Ui/Controls/VulkanEmbeddedWindow.cs33
-rw-r--r--Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs220
-rw-r--r--Ryujinx.Ava/Ui/Controls/Win32NativeInterop.cs113
10 files changed, 650 insertions, 511 deletions
diff --git a/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs b/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs
index 15ecaa77..e774a09a 100644
--- a/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs
+++ b/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs
@@ -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;
}
}
-} \ No newline at end of file
+}
diff --git a/Ryujinx.Ava/Ui/Controls/EmbeddedWindow.cs b/Ryujinx.Ava/Ui/Controls/EmbeddedWindow.cs
new file mode 100644
index 00000000..d9fae93a
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/EmbeddedWindow.cs
@@ -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));
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/OpenGLEmbeddedWindow.cs b/Ryujinx.Ava/Ui/Controls/OpenGLEmbeddedWindow.cs
new file mode 100644
index 00000000..ce579fdf
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/OpenGLEmbeddedWindow.cs
@@ -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();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs b/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs
deleted file mode 100644
index e58bdaa0..00000000
--- a/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs
+++ /dev/null
@@ -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());
- }
- }
- }
-}
diff --git a/Ryujinx.Ava/Ui/Controls/RendererControl.cs b/Ryujinx.Ava/Ui/Controls/RendererControl.cs
deleted file mode 100644
index 392f67e3..00000000
--- a/Ryujinx.Ava/Ui/Controls/RendererControl.cs
+++ /dev/null
@@ -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);
- }
-} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/RendererHost.axaml b/Ryujinx.Ava/Ui/Controls/RendererHost.axaml
new file mode 100644
index 00000000..be72fd61
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/RendererHost.axaml
@@ -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>
diff --git a/Ryujinx.Ava/Ui/Controls/RendererHost.axaml.cs b/Ryujinx.Ava/Ui/Controls/RendererHost.axaml.cs
new file mode 100644
index 00000000..0d1984fd
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/RendererHost.axaml.cs
@@ -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;
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/VulkanEmbeddedWindow.cs b/Ryujinx.Ava/Ui/Controls/VulkanEmbeddedWindow.cs
new file mode 100644
index 00000000..d2c980dd
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/VulkanEmbeddedWindow.cs
@@ -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));
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs b/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs
deleted file mode 100644
index 7b7dfaa1..00000000
--- a/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs
+++ /dev/null
@@ -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());
- }
- }
- }
-}
diff --git a/Ryujinx.Ava/Ui/Controls/Win32NativeInterop.cs b/Ryujinx.Ava/Ui/Controls/Win32NativeInterop.cs
new file mode 100644
index 00000000..124536d9
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/Win32NativeInterop.cs
@@ -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);
+ }
+}