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 b9f1ff3c77
commit 6f0395538b
58 changed files with 868 additions and 3531 deletions

View File

@@ -1,3 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
@@ -59,29 +60,63 @@ namespace Ryujinx.Ava.Ui.Controls
string input = string.Empty;
var overlay = new ContentDialogOverlayWindow()
{
Height = window.Bounds.Height,
Width = window.Bounds.Width,
Position = window.PointToScreen(new Point())
};
window.PositionChanged += OverlayOnPositionChanged;
void OverlayOnPositionChanged(object sender, PixelPointEventArgs e)
{
overlay.Position = window.PointToScreen(new Point());
}
contentDialog = overlay.ContentDialog;
bool opened = false;
content.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax);
if (contentDialog != null)
content._host = contentDialog;
contentDialog.Title = title;
contentDialog.PrimaryButtonText = args.SubmitText;
contentDialog.IsPrimaryButtonEnabled = content._checkLength(content.Message.Length);
contentDialog.SecondaryButtonText = "";
contentDialog.CloseButtonText = LocaleManager.Instance["InputDialogCancel"];
contentDialog.Content = content;
TypedEventHandler<ContentDialog, ContentDialogClosedEventArgs> handler = (sender, eventArgs) =>
{
content._host = contentDialog;
contentDialog.Title = title;
contentDialog.PrimaryButtonText = args.SubmitText;
contentDialog.IsPrimaryButtonEnabled = content._checkLength(content.Message.Length);
contentDialog.SecondaryButtonText = "";
contentDialog.CloseButtonText = LocaleManager.Instance["InputDialogCancel"];
contentDialog.Content = content;
TypedEventHandler<ContentDialog, ContentDialogClosedEventArgs> handler = (sender, eventArgs) =>
if (eventArgs.Result == ContentDialogResult.Primary)
{
if (eventArgs.Result == ContentDialogResult.Primary)
{
result = UserResult.Ok;
input = content.Input.Text;
}
};
contentDialog.Closed += handler;
result = UserResult.Ok;
input = content.Input.Text;
}
};
contentDialog.Closed += handler;
overlay.Opened += OverlayOnActivated;
async void OverlayOnActivated(object sender, EventArgs e)
{
if (opened)
{
return;
}
opened = true;
overlay.Position = window.PointToScreen(new Point());
await contentDialog.ShowAsync();
contentDialog.Closed -= handler;
}
overlay.Close();
};
await overlay.ShowDialog(window);
return (result, input);
}

View File

@@ -1,76 +0,0 @@
using Avalonia;
using System;
using System.Runtime.InteropServices;
using static Ryujinx.Ava.Ui.Backend.Interop;
namespace Ryujinx.Ava.Ui.Backend
{
public abstract class BackendSurface : IDisposable
{
protected IntPtr Display => _display;
private IntPtr _display = IntPtr.Zero;
[DllImport("libX11.so.6")]
public static extern IntPtr XOpenDisplay(IntPtr display);
[DllImport("libX11.so.6")]
public static extern int XCloseDisplay(IntPtr display);
private PixelSize _currentSize;
public IntPtr Handle { get; protected set; }
public bool IsDisposed { get; private set; }
public BackendSurface(IntPtr handle)
{
Handle = handle;
if (OperatingSystem.IsLinux())
{
_display = XOpenDisplay(IntPtr.Zero);
}
}
public PixelSize Size
{
get
{
PixelSize size = new PixelSize();
if (OperatingSystem.IsWindows())
{
GetClientRect(Handle, out var rect);
size = new PixelSize(rect.right, rect.bottom);
}
else if (OperatingSystem.IsLinux())
{
XWindowAttributes attributes = new XWindowAttributes();
XGetWindowAttributes(Display, Handle, ref attributes);
size = new PixelSize(attributes.width, attributes.height);
}
_currentSize = size;
return size;
}
}
public PixelSize CurrentSize => _currentSize;
public virtual void Dispose()
{
if (IsDisposed)
{
throw new ObjectDisposedException(nameof(BackendSurface));
}
IsDisposed = true;
if (_display != IntPtr.Zero)
{
XCloseDisplay(_display);
}
}
}
}

View File

@@ -1,49 +0,0 @@
using FluentAvalonia.Interop;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Ava.Ui.Backend
{
public static class Interop
{
[StructLayout(LayoutKind.Sequential)]
public struct XWindowAttributes
{
public int x;
public int y;
public int width;
public int height;
public int border_width;
public int depth;
public IntPtr visual;
public IntPtr root;
public int c_class;
public int bit_gravity;
public int win_gravity;
public int backing_store;
public IntPtr backing_planes;
public IntPtr backing_pixel;
public int save_under;
public IntPtr colormap;
public int map_installed;
public int map_state;
public IntPtr all_event_masks;
public IntPtr your_event_mask;
public IntPtr do_not_propagate_mask;
public int override_direct;
public IntPtr screen;
}
[DllImport("user32.dll")]
public static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect);
[DllImport("libX11.so.6")]
public static extern int XCloseDisplay(IntPtr display);
[DllImport("libX11.so.6")]
public static extern int XGetWindowAttributes(IntPtr display, IntPtr window, ref XWindowAttributes attributes);
[DllImport("libX11.so.6")]
public static extern IntPtr XOpenDisplay(IntPtr display);
}
}

View File

@@ -1,26 +0,0 @@
using Avalonia;
using Avalonia.Skia;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Ava.Ui.Backend.Vulkan;
namespace Ryujinx.Ava.Ui.Backend
{
public static class SkiaGpuFactory
{
public static ISkiaGpu CreateVulkanGpu()
{
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>() ?? new SkiaOptions();
var platformInterface = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
if (platformInterface == null)
{
VulkanPlatformInterface.TryInitialize();
}
var gpu = new VulkanSkiaGpu(skiaOptions.MaxGpuResourceSizeBytes);
AvaloniaLocator.CurrentMutable.Bind<VulkanSkiaGpu>().ToConstant(gpu);
return gpu;
}
}
}

View File

@@ -1,17 +0,0 @@
using System;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
public static class ResultExtensions
{
public static void ThrowOnError(this Result result)
{
// Only negative result codes are errors.
if ((int)result < (int)Result.Success)
{
throw new Exception($"Unexpected API error \"{result}\".");
}
}
}
}

View File

@@ -1,201 +0,0 @@
using System;
using Avalonia;
using Avalonia.Skia;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Silk.NET.Vulkan;
using SkiaSharp;
namespace Ryujinx.Ava.Ui.Backend.Vulkan
{
internal class VulkanRenderTarget : ISkiaGpuRenderTarget
{
public GRContext GrContext { get; private set; }
private readonly VulkanSurfaceRenderTarget _surface;
private readonly VulkanPlatformInterface _vulkanPlatformInterface;
private readonly IVulkanPlatformSurface _vulkanPlatformSurface;
private GRVkBackendContext _grVkBackend;
public VulkanRenderTarget(VulkanPlatformInterface vulkanPlatformInterface, IVulkanPlatformSurface vulkanPlatformSurface)
{
_surface = vulkanPlatformInterface.CreateRenderTarget(vulkanPlatformSurface);
_vulkanPlatformInterface = vulkanPlatformInterface;
_vulkanPlatformSurface = vulkanPlatformSurface;
Initialize();
}
private void Initialize()
{
GRVkGetProcedureAddressDelegate getProc = GetVulkanProcAddress;
_grVkBackend = new GRVkBackendContext()
{
VkInstance = _surface.Device.Handle,
VkPhysicalDevice = _vulkanPlatformInterface.PhysicalDevice.Handle,
VkDevice = _surface.Device.Handle,
VkQueue = _surface.Device.Queue.Handle,
GraphicsQueueIndex = _vulkanPlatformInterface.PhysicalDevice.QueueFamilyIndex,
GetProcedureAddress = getProc
};
GrContext = GRContext.CreateVulkan(_grVkBackend);
var gpu = AvaloniaLocator.Current.GetService<VulkanSkiaGpu>();
if (gpu.MaxResourceBytes.HasValue)
{
GrContext.SetResourceCacheLimit(gpu.MaxResourceBytes.Value);
}
}
private IntPtr GetVulkanProcAddress(string name, IntPtr instanceHandle, IntPtr deviceHandle)
{
IntPtr addr;
if (deviceHandle != IntPtr.Zero)
{
addr = _vulkanPlatformInterface.Api.GetDeviceProcAddr(new Device(deviceHandle), name);
if (addr != IntPtr.Zero)
{
return addr;
}
addr = _vulkanPlatformInterface.Api.GetDeviceProcAddr(new Device(_surface.Device.Handle), name);
if (addr != IntPtr.Zero)
{
return addr;
}
}
addr = _vulkanPlatformInterface.Api.GetInstanceProcAddr(new Instance(_vulkanPlatformInterface.Instance.Handle), name);
if (addr == IntPtr.Zero)
{
addr = _vulkanPlatformInterface.Api.GetInstanceProcAddr(new Instance(instanceHandle), name);
}
return addr;
}
public void Dispose()
{
_grVkBackend.Dispose();
GrContext.Dispose();
_surface.Dispose();
}
public ISkiaGpuRenderSession BeginRenderingSession()
{
var session = _surface.BeginDraw(_vulkanPlatformSurface.Scaling);
bool success = false;
try
{
var disp = session.Display;
var api = session.Api;
var size = session.Size;
var scaling = session.Scaling;
if (size.Width <= 0 || size.Height <= 0 || scaling < 0)
{
size = new Avalonia.PixelSize(1, 1);
scaling = 1;
}
lock (GrContext)
{
GrContext.ResetContext();
var image = _surface.GetImage();
var imageInfo = new GRVkImageInfo()
{
CurrentQueueFamily = disp.QueueFamilyIndex,
Format = (uint)image.Format,
Image = image.Handle,
ImageLayout = (uint)image.CurrentLayout,
ImageTiling = (uint)image.Tiling,
ImageUsageFlags = _surface.UsageFlags,
LevelCount = _surface.MipLevels,
SampleCount = 1,
Protected = false,
Alloc = new GRVkAlloc()
{
Memory = image.MemoryHandle,
Flags = 0,
Offset = 0,
Size = _surface.MemorySize
}
};
var renderTarget =
new GRBackendRenderTarget((int)size.Width, (int)size.Height, 1,
imageInfo);
var surface = SKSurface.Create(GrContext, renderTarget,
GRSurfaceOrigin.TopLeft,
_surface.IsRgba ? SKColorType.Rgba8888 : SKColorType.Bgra8888, SKColorSpace.CreateSrgb());
if (surface == null)
{
throw new InvalidOperationException(
"Surface can't be created with the provided render target");
}
success = true;
return new VulkanGpuSession(GrContext, renderTarget, surface, session);
}
}
finally
{
if (!success)
{
session.Dispose();
}
}
}
public bool IsCorrupted { get; }
internal class VulkanGpuSession : ISkiaGpuRenderSession
{
private readonly GRBackendRenderTarget _backendRenderTarget;
private readonly VulkanSurfaceRenderingSession _vulkanSession;
public VulkanGpuSession(GRContext grContext,
GRBackendRenderTarget backendRenderTarget,
SKSurface surface,
VulkanSurfaceRenderingSession vulkanSession)
{
GrContext = grContext;
_backendRenderTarget = backendRenderTarget;
SkSurface = surface;
_vulkanSession = vulkanSession;
SurfaceOrigin = GRSurfaceOrigin.TopLeft;
}
public void Dispose()
{
lock (_vulkanSession.Display.Lock)
{
SkSurface.Canvas.Flush();
SkSurface.Dispose();
_backendRenderTarget.Dispose();
GrContext.Flush();
_vulkanSession.Dispose();
}
}
public GRContext GrContext { get; }
public SKSurface SkSurface { get; }
public double ScaleFactor => _vulkanSession.Scaling;
public GRSurfaceOrigin SurfaceOrigin { get; }
}
}
}

View File

@@ -1,61 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Skia;
using Avalonia.X11;
using Ryujinx.Ava.Ui.Vulkan;
using Silk.NET.Vulkan;
using SkiaSharp;
namespace Ryujinx.Ava.Ui.Backend.Vulkan
{
public class VulkanSkiaGpu : ISkiaGpu
{
private readonly VulkanPlatformInterface _vulkan;
public long? MaxResourceBytes { get; }
public VulkanSkiaGpu(long? maxResourceBytes)
{
_vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
MaxResourceBytes = maxResourceBytes;
}
public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces)
{
foreach (var surface in surfaces)
{
VulkanWindowSurface window;
if (surface is IPlatformHandle handle)
{
window = new VulkanWindowSurface(handle.Handle);
}
else if (surface is X11FramebufferSurface x11FramebufferSurface)
{
// As of Avalonia 0.10.13, an IPlatformHandle isn't passed for linux, so use reflection to otherwise get the window id
var xId = (IntPtr)x11FramebufferSurface.GetType().GetField(
"_xid",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(x11FramebufferSurface);
window = new VulkanWindowSurface(xId);
}
else
{
continue;
}
VulkanRenderTarget vulkanRenderTarget = new VulkanRenderTarget(_vulkan, window);
return vulkanRenderTarget;
}
return null;
}
public ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session)
{
return null;
}
}
}

View File

@@ -1,53 +0,0 @@
using Avalonia;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
using System;
namespace Ryujinx.Ava.Ui.Backend.Vulkan
{
internal class VulkanWindowSurface : BackendSurface, IVulkanPlatformSurface
{
public float Scaling => (float)Program.ActualScaleFactor;
public PixelSize SurfaceSize => Size;
public VulkanWindowSurface(IntPtr handle) : base(handle)
{
}
public unsafe SurfaceKHR CreateSurface(VulkanInstance instance)
{
if (OperatingSystem.IsWindows())
{
if (instance.Api.TryGetInstanceExtension(new Instance(instance.Handle), out KhrWin32Surface surfaceExtension))
{
var createInfo = new Win32SurfaceCreateInfoKHR() { Hinstance = 0, Hwnd = Handle, SType = StructureType.Win32SurfaceCreateInfoKhr };
surfaceExtension.CreateWin32Surface(new Instance(instance.Handle), createInfo, null, out var surface).ThrowOnError();
return surface;
}
}
else if (OperatingSystem.IsLinux())
{
if (instance.Api.TryGetInstanceExtension(new Instance(instance.Handle), out KhrXlibSurface surfaceExtension))
{
var createInfo = new XlibSurfaceCreateInfoKHR()
{
SType = StructureType.XlibSurfaceCreateInfoKhr,
Dpy = (nint*)Display,
Window = Handle
};
surfaceExtension.CreateXlibSurface(new Instance(instance.Handle), createInfo, null, out var surface).ThrowOnError();
return surface;
}
}
throw new PlatformNotSupportedException("The current platform does not support surface creation.");
}
}
}

View File

@@ -1,13 +0,0 @@
using System;
using Avalonia;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
{
public interface IVulkanPlatformSurface : IDisposable
{
float Scaling { get; }
PixelSize SurfaceSize { get; }
SurfaceKHR CreateSurface(VulkanInstance instance);
}
}

View File

@@ -1,134 +0,0 @@
using System;
using Avalonia;
using Ryujinx.Graphics.Vulkan;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
{
internal class VulkanSurfaceRenderTarget : IDisposable
{
private readonly VulkanPlatformInterface _platformInterface;
private readonly Format _format;
private VulkanCommandBufferPool.VulkanCommandBuffer _commandBuffer;
private VulkanImage Image { get; set; }
private object _lock = new object();
public uint MipLevels => Image.MipLevels;
public VulkanDevice Device { get; }
public VulkanSurfaceRenderTarget(VulkanPlatformInterface platformInterface, VulkanSurface surface)
{
_platformInterface = platformInterface;
var device = VulkanInitialization.CreateDevice(platformInterface.Api,
platformInterface.PhysicalDevice.InternalHandle,
platformInterface.PhysicalDevice.QueueFamilyIndex,
VulkanInitialization.GetSupportedExtensions(platformInterface.Api, platformInterface.PhysicalDevice.InternalHandle),
platformInterface.PhysicalDevice.QueueCount);
Device = new VulkanDevice(device, platformInterface.PhysicalDevice, platformInterface.Api);
Display = VulkanDisplay.CreateDisplay(
platformInterface.Instance,
Device,
platformInterface.PhysicalDevice,
surface);
Surface = surface;
// Skia seems to only create surfaces from images with unorm format
IsRgba = Display.SurfaceFormat.Format >= Format.R8G8B8A8Unorm &&
Display.SurfaceFormat.Format <= Format.R8G8B8A8Srgb;
_format = IsRgba ? Format.R8G8B8A8Unorm : Format.B8G8R8A8Unorm;
}
public bool IsRgba { get; }
public uint ImageFormat => (uint)_format;
public ulong MemorySize => Image.MemorySize;
public VulkanDisplay Display { get; private set; }
public VulkanSurface Surface { get; private set; }
public uint UsageFlags => Image.UsageFlags;
public PixelSize Size { get; private set; }
public void Dispose()
{
lock (_lock)
{
DestroyImage();
Display?.Dispose();
Surface?.Dispose();
Device?.Dispose();
Display = null;
Surface = null;
}
}
public VulkanSurfaceRenderingSession BeginDraw(float scaling)
{
if (Image == null)
{
RecreateImage();
}
_commandBuffer?.WaitForFence();
_commandBuffer = null;
var session = new VulkanSurfaceRenderingSession(Display, Device, this, scaling);
Image.TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
return session;
}
public void RecreateImage()
{
DestroyImage();
CreateImage();
}
private void CreateImage()
{
Size = Display.Size;
Image = new VulkanImage(Device, _platformInterface.PhysicalDevice, Display.CommandBufferPool, ImageFormat, Size);
}
private void DestroyImage()
{
_commandBuffer?.WaitForFence();
_commandBuffer = null;
Image?.Dispose();
Image = null;
}
public VulkanImage GetImage()
{
return Image;
}
public void EndDraw()
{
lock (_lock)
{
if (Display == null)
{
return;
}
_commandBuffer = Display.StartPresentation();
Display.BlitImageToCurrentImage(this, _commandBuffer.InternalHandle);
Display.EndPresentation(_commandBuffer);
}
}
}
}

View File

@@ -1,215 +0,0 @@
using System;
using System.Collections.Generic;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanCommandBufferPool : IDisposable
{
private readonly VulkanDevice _device;
private readonly CommandPool _commandPool;
private readonly List<VulkanCommandBuffer> _usedCommandBuffers = new();
private readonly object _lock = new object();
public unsafe VulkanCommandBufferPool(VulkanDevice device, VulkanPhysicalDevice physicalDevice)
{
_device = device;
var commandPoolCreateInfo = new CommandPoolCreateInfo
{
SType = StructureType.CommandPoolCreateInfo,
Flags = CommandPoolCreateFlags.CommandPoolCreateResetCommandBufferBit,
QueueFamilyIndex = physicalDevice.QueueFamilyIndex
};
device.Api.CreateCommandPool(_device.InternalHandle, commandPoolCreateInfo, null, out _commandPool)
.ThrowOnError();
}
private CommandBuffer AllocateCommandBuffer()
{
var commandBufferAllocateInfo = new CommandBufferAllocateInfo
{
SType = StructureType.CommandBufferAllocateInfo,
CommandPool = _commandPool,
CommandBufferCount = 1,
Level = CommandBufferLevel.Primary
};
lock (_lock)
{
_device.Api.AllocateCommandBuffers(_device.InternalHandle, commandBufferAllocateInfo, out var commandBuffer);
return commandBuffer;
}
}
public VulkanCommandBuffer CreateCommandBuffer()
{
return new(_device, 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 void Dispose()
{
lock (_lock)
{
FreeUsedCommandBuffers();
_device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, Span<AllocationCallbacks>.Empty);
}
}
public class VulkanCommandBuffer : IDisposable
{
private readonly VulkanCommandBufferPool _commandBufferPool;
private readonly VulkanDevice _device;
private readonly Fence _fence;
private bool _hasEnded;
private bool _hasStarted;
private bool _isDisposed;
private object _lock = new object();
public IntPtr Handle => InternalHandle.Handle;
internal CommandBuffer InternalHandle { get; }
internal unsafe VulkanCommandBuffer(VulkanDevice device, VulkanCommandBufferPool commandBufferPool)
{
_device = device;
_commandBufferPool = commandBufferPool;
InternalHandle = _commandBufferPool.AllocateCommandBuffer();
var fenceCreateInfo = new FenceCreateInfo()
{
SType = StructureType.FenceCreateInfo,
Flags = FenceCreateFlags.FenceCreateSignaledBit
};
device.Api.CreateFence(device.InternalHandle, fenceCreateInfo, null, out _fence);
}
public void WaitForFence()
{
if (_isDisposed)
{
return;
}
lock (_lock)
{
if (!_isDisposed)
{
_device.Api.WaitForFences(_device.InternalHandle, 1, _fence, true, ulong.MaxValue);
}
}
}
public void BeginRecording()
{
if (!_hasStarted)
{
_hasStarted = true;
var beginInfo = new CommandBufferBeginInfo
{
SType = StructureType.CommandBufferBeginInfo,
Flags = CommandBufferUsageFlags.CommandBufferUsageOneTimeSubmitBit
};
_device.Api.BeginCommandBuffer(InternalHandle, beginInfo);
}
}
public void EndRecording()
{
if (_hasStarted && !_hasEnded)
{
_hasEnded = true;
_device.Api.EndCommandBuffer(InternalHandle);
}
}
public void Submit()
{
Submit(null, null, null, _fence);
}
public unsafe void Submit(
ReadOnlySpan<Semaphore> waitSemaphores,
ReadOnlySpan<PipelineStageFlags> waitDstStageMask,
ReadOnlySpan<Semaphore> signalSemaphores,
Fence? fence = null)
{
EndRecording();
if (!fence.HasValue)
{
fence = _fence;
}
fixed (Semaphore* pWaitSemaphores = waitSemaphores, pSignalSemaphores = signalSemaphores)
{
fixed (PipelineStageFlags* pWaitDstStageMask = waitDstStageMask)
{
var commandBuffer = InternalHandle;
var submitInfo = new SubmitInfo
{
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,
};
_device.Api.ResetFences(_device.InternalHandle, 1, fence.Value);
_device.Submit(submitInfo, fence.Value);
}
}
_commandBufferPool.DisposeCommandBuffer(this);
}
public void Dispose()
{
lock (_lock)
{
if (!_isDisposed)
{
_isDisposed = true;
_device.Api.WaitForFences(_device.InternalHandle, 1, _fence, true, ulong.MaxValue);
_device.Api.FreeCommandBuffers(_device.InternalHandle, _commandBufferPool._commandPool, 1, InternalHandle);
_device.Api.DestroyFence(_device.InternalHandle, _fence, Span<AllocationCallbacks>.Empty);
}
}
}
}
}
}

View File

@@ -1,63 +0,0 @@
using System;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanDevice : IDisposable
{
private static object _lock = new object();
public VulkanDevice(Device apiHandle, VulkanPhysicalDevice physicalDevice, Vk api)
{
InternalHandle = apiHandle;
Api = api;
api.GetDeviceQueue(apiHandle, physicalDevice.QueueFamilyIndex, 0, out var queue);
Queue = new VulkanQueue(this, queue);
PresentQueue = Queue;
}
public IntPtr Handle => InternalHandle.Handle;
internal Device InternalHandle { get; }
public Vk Api { get; }
public VulkanQueue Queue { get; private set; }
public VulkanQueue PresentQueue { get; }
public void Dispose()
{
WaitIdle();
Queue = null;
Api.DestroyDevice(InternalHandle, Span<AllocationCallbacks>.Empty);
}
internal void Submit(SubmitInfo submitInfo, Fence fence = default)
{
lock (_lock)
{
Api.QueueSubmit(Queue.InternalHandle, 1, submitInfo, fence).ThrowOnError();
}
}
public void WaitIdle()
{
lock (_lock)
{
Api.DeviceWaitIdle(InternalHandle);
}
}
public void QueueWaitIdle()
{
lock (_lock)
{
Api.QueueWaitIdle(Queue.InternalHandle);
}
}
public object Lock => _lock;
}
}

View File

@@ -1,456 +0,0 @@
using System;
using System.Linq;
using System.Threading;
using Avalonia;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanDisplay : IDisposable
{
private static KhrSwapchain _swapchainExtension;
private readonly VulkanInstance _instance;
private readonly VulkanPhysicalDevice _physicalDevice;
private readonly VulkanSemaphorePair _semaphorePair;
private readonly VulkanDevice _device;
private uint _nextImage;
private readonly VulkanSurface _surface;
private SurfaceFormatKHR _surfaceFormat;
private SwapchainKHR _swapchain;
private Extent2D _swapchainExtent;
private Image[] _swapchainImages;
private ImageView[] _swapchainImageViews = Array.Empty<ImageView>();
private bool _vsyncStateChanged;
private bool _vsyncEnabled;
private bool _surfaceChanged;
public event EventHandler Presented;
public VulkanCommandBufferPool CommandBufferPool { get; set; }
public object Lock => _device.Lock;
private VulkanDisplay(VulkanInstance instance, VulkanDevice device,
VulkanPhysicalDevice physicalDevice, VulkanSurface surface, SwapchainKHR swapchain,
Extent2D swapchainExtent)
{
_instance = instance;
_device = device;
_physicalDevice = physicalDevice;
_swapchain = swapchain;
_swapchainExtent = swapchainExtent;
_surface = surface;
CreateSwapchainImages();
_semaphorePair = new VulkanSemaphorePair(_device);
CommandBufferPool = new VulkanCommandBufferPool(device, physicalDevice);
}
public PixelSize Size { get; private set; }
public uint QueueFamilyIndex => _physicalDevice.QueueFamilyIndex;
internal SurfaceFormatKHR SurfaceFormat
{
get
{
if (_surfaceFormat.Format == Format.Undefined)
{
_surfaceFormat = _surface.GetSurfaceFormat(_physicalDevice);
}
return _surfaceFormat;
}
}
public void Dispose()
{
_device.WaitIdle();
_semaphorePair?.Dispose();
DestroyCurrentImageViews();
_swapchainExtension.DestroySwapchain(_device.InternalHandle, _swapchain, Span<AllocationCallbacks>.Empty);
CommandBufferPool.Dispose();
}
public bool IsSurfaceChanged()
{
var changed = _surfaceChanged;
_surfaceChanged = false;
return changed;
}
private static unsafe SwapchainKHR CreateSwapchain(VulkanInstance instance, VulkanDevice device,
VulkanPhysicalDevice physicalDevice, VulkanSurface surface, out Extent2D swapchainExtent,
SwapchainKHR? oldswapchain = null, bool vsyncEnabled = true)
{
if (_swapchainExtension == null)
{
instance.Api.TryGetDeviceExtension(instance.InternalHandle, device.InternalHandle, out _swapchainExtension);
}
while (!surface.CanSurfacePresent(physicalDevice))
{
Thread.Sleep(16);
}
VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfaceCapabilities(physicalDevice.InternalHandle,
surface.ApiHandle, out var capabilities);
var imageCount = capabilities.MinImageCount + 1;
if (capabilities.MaxImageCount > 0 && imageCount > capabilities.MaxImageCount)
{
imageCount = capabilities.MaxImageCount;
}
var surfaceFormat = surface.GetSurfaceFormat(physicalDevice);
bool supportsIdentityTransform = capabilities.SupportedTransforms.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformIdentityBitKhr);
bool isRotated = capabilities.CurrentTransform.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformRotate90BitKhr) ||
capabilities.CurrentTransform.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformRotate270BitKhr);
swapchainExtent = GetSwapchainExtent(surface, capabilities);
CompositeAlphaFlagsKHR compositeAlphaFlags = GetSuitableCompositeAlphaFlags(capabilities);
PresentModeKHR presentMode = GetSuitablePresentMode(physicalDevice, surface, vsyncEnabled);
var swapchainCreateInfo = new SwapchainCreateInfoKHR
{
SType = StructureType.SwapchainCreateInfoKhr,
Surface = surface.ApiHandle,
MinImageCount = imageCount,
ImageFormat = surfaceFormat.Format,
ImageColorSpace = surfaceFormat.ColorSpace,
ImageExtent = swapchainExtent,
ImageUsage =
ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit,
ImageSharingMode = SharingMode.Exclusive,
ImageArrayLayers = 1,
PreTransform = supportsIdentityTransform && isRotated ?
SurfaceTransformFlagsKHR.SurfaceTransformIdentityBitKhr :
capabilities.CurrentTransform,
CompositeAlpha = compositeAlphaFlags,
PresentMode = presentMode,
Clipped = true,
OldSwapchain = oldswapchain ?? new SwapchainKHR()
};
_swapchainExtension.CreateSwapchain(device.InternalHandle, swapchainCreateInfo, null, out var swapchain)
.ThrowOnError();
if (oldswapchain != null)
{
_swapchainExtension.DestroySwapchain(device.InternalHandle, oldswapchain.Value, null);
}
return swapchain;
}
private static unsafe Extent2D GetSwapchainExtent(VulkanSurface surface, SurfaceCapabilitiesKHR capabilities)
{
Extent2D swapchainExtent;
if (capabilities.CurrentExtent.Width != uint.MaxValue)
{
swapchainExtent = capabilities.CurrentExtent;
}
else
{
var surfaceSize = surface.SurfaceSize;
var width = Math.Clamp((uint)surfaceSize.Width, capabilities.MinImageExtent.Width, capabilities.MaxImageExtent.Width);
var height = Math.Clamp((uint)surfaceSize.Height, capabilities.MinImageExtent.Height, capabilities.MaxImageExtent.Height);
swapchainExtent = new Extent2D(width, height);
}
return swapchainExtent;
}
private static unsafe CompositeAlphaFlagsKHR GetSuitableCompositeAlphaFlags(SurfaceCapabilitiesKHR capabilities)
{
var compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaOpaqueBitKhr;
if (capabilities.SupportedCompositeAlpha.HasFlag(CompositeAlphaFlagsKHR.CompositeAlphaPostMultipliedBitKhr))
{
compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaPostMultipliedBitKhr;
}
else if (capabilities.SupportedCompositeAlpha.HasFlag(CompositeAlphaFlagsKHR.CompositeAlphaPreMultipliedBitKhr))
{
compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaPreMultipliedBitKhr;
}
return compositeAlphaFlags;
}
private static unsafe PresentModeKHR GetSuitablePresentMode(VulkanPhysicalDevice physicalDevice, VulkanSurface surface, bool vsyncEnabled)
{
uint presentModesCount;
VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfacePresentModes(physicalDevice.InternalHandle,
surface.ApiHandle,
&presentModesCount, null);
var presentModes = new PresentModeKHR[presentModesCount];
fixed (PresentModeKHR* pPresentModes = presentModes)
{
VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfacePresentModes(physicalDevice.InternalHandle,
surface.ApiHandle, &presentModesCount, pPresentModes);
}
var modes = presentModes.ToList();
if (!vsyncEnabled && modes.Contains(PresentModeKHR.PresentModeImmediateKhr))
{
return PresentModeKHR.PresentModeImmediateKhr;
}
else if (modes.Contains(PresentModeKHR.PresentModeMailboxKhr))
{
return PresentModeKHR.PresentModeMailboxKhr;
}
else if (modes.Contains(PresentModeKHR.PresentModeFifoKhr))
{
return PresentModeKHR.PresentModeFifoKhr;
}
else
{
return PresentModeKHR.PresentModeImmediateKhr;
}
}
internal static VulkanDisplay CreateDisplay(VulkanInstance instance, VulkanDevice device,
VulkanPhysicalDevice physicalDevice, VulkanSurface surface)
{
var swapchain = CreateSwapchain(instance, device, physicalDevice, surface, out var extent, null, true);
return new VulkanDisplay(instance, device, physicalDevice, surface, swapchain, extent);
}
private unsafe void CreateSwapchainImages()
{
DestroyCurrentImageViews();
Size = new PixelSize((int)_swapchainExtent.Width, (int)_swapchainExtent.Height);
uint imageCount = 0;
_swapchainExtension.GetSwapchainImages(_device.InternalHandle, _swapchain, &imageCount, null);
_swapchainImages = new Image[imageCount];
fixed (Image* pSwapchainImages = _swapchainImages)
{
_swapchainExtension.GetSwapchainImages(_device.InternalHandle, _swapchain, &imageCount, pSwapchainImages);
}
_swapchainImageViews = new ImageView[imageCount];
var surfaceFormat = SurfaceFormat;
for (var i = 0; i < imageCount; i++)
{
_swapchainImageViews[i] = CreateSwapchainImageView(_swapchainImages[i], surfaceFormat.Format);
}
}
private void DestroyCurrentImageViews()
{
for (var i = 0; i < _swapchainImageViews.Length; i++)
{
_instance.Api.DestroyImageView(_device.InternalHandle, _swapchainImageViews[i], Span<AllocationCallbacks>.Empty);
}
}
internal void ChangeVSyncMode(bool vsyncEnabled)
{
_vsyncStateChanged = true;
_vsyncEnabled = vsyncEnabled;
}
private void Recreate()
{
_device.WaitIdle();
_swapchain = CreateSwapchain(_instance, _device, _physicalDevice, _surface, out _swapchainExtent, _swapchain, _vsyncEnabled);
CreateSwapchainImages();
_surfaceChanged = true;
}
private unsafe ImageView CreateSwapchainImageView(Image swapchainImage, Format format)
{
var componentMapping = new ComponentMapping(
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity);
var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, 1, 0, 1);
var imageCreateInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = swapchainImage,
ViewType = ImageViewType.ImageViewType2D,
Format = format,
Components = componentMapping,
SubresourceRange = subresourceRange
};
_instance.Api.CreateImageView(_device.InternalHandle, imageCreateInfo, null, out var imageView).ThrowOnError();
return imageView;
}
public bool EnsureSwapchainAvailable()
{
if (Size != _surface.SurfaceSize || _vsyncStateChanged)
{
_vsyncStateChanged = false;
Recreate();
return false;
}
return true;
}
internal VulkanCommandBufferPool.VulkanCommandBuffer StartPresentation()
{
_nextImage = 0;
while (true)
{
var acquireResult = _swapchainExtension.AcquireNextImage(
_device.InternalHandle,
_swapchain,
ulong.MaxValue,
_semaphorePair.ImageAvailableSemaphore,
new Fence(),
ref _nextImage);
if (acquireResult == Result.ErrorOutOfDateKhr ||
acquireResult == Result.SuboptimalKhr)
{
Recreate();
}
else
{
acquireResult.ThrowOnError();
break;
}
}
var commandBuffer = CommandBufferPool.CreateCommandBuffer();
commandBuffer.BeginRecording();
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle,
_swapchainImages[_nextImage], ImageLayout.Undefined,
AccessFlags.AccessNoneKhr,
ImageLayout.TransferDstOptimal,
AccessFlags.AccessTransferWriteBit,
1);
return commandBuffer;
}
internal void BlitImageToCurrentImage(VulkanSurfaceRenderTarget renderTarget, CommandBuffer commandBuffer)
{
var image = renderTarget.GetImage();
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer,
image.InternalHandle.Value, (ImageLayout)image.CurrentLayout,
AccessFlags.AccessNoneKhr,
ImageLayout.TransferSrcOptimal,
AccessFlags.AccessTransferReadBit,
renderTarget.MipLevels);
var srcBlitRegion = new ImageBlit
{
SrcOffsets = new ImageBlit.SrcOffsetsBuffer
{
Element0 = new Offset3D(0, 0, 0),
Element1 = new Offset3D(renderTarget.Size.Width, renderTarget.Size.Height, 1),
},
DstOffsets = new ImageBlit.DstOffsetsBuffer
{
Element0 = new Offset3D(0, 0, 0),
Element1 = new Offset3D(Size.Width, 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
}
};
_device.Api.CmdBlitImage(commandBuffer, image.InternalHandle.Value,
ImageLayout.TransferSrcOptimal,
_swapchainImages[_nextImage],
ImageLayout.TransferDstOptimal,
1,
srcBlitRegion,
Filter.Linear);
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer,
image.InternalHandle.Value, ImageLayout.TransferSrcOptimal,
AccessFlags.AccessTransferReadBit,
(ImageLayout)image.CurrentLayout,
AccessFlags.AccessNoneKhr,
renderTarget.MipLevels);
}
internal unsafe void EndPresentation(VulkanCommandBufferPool.VulkanCommandBuffer commandBuffer)
{
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle,
_swapchainImages[_nextImage], ImageLayout.TransferDstOptimal,
AccessFlags.AccessNoneKhr,
ImageLayout.PresentSrcKhr,
AccessFlags.AccessNoneKhr,
1);
commandBuffer.Submit(
stackalloc[] { _semaphorePair.ImageAvailableSemaphore },
stackalloc[] { PipelineStageFlags.PipelineStageColorAttachmentOutputBit },
stackalloc[] { _semaphorePair.RenderFinishedSemaphore });
var semaphore = _semaphorePair.RenderFinishedSemaphore;
var swapchain = _swapchain;
var nextImage = _nextImage;
Result result;
var presentInfo = new PresentInfoKHR
{
SType = StructureType.PresentInfoKhr,
WaitSemaphoreCount = 1,
PWaitSemaphores = &semaphore,
SwapchainCount = 1,
PSwapchains = &swapchain,
PImageIndices = &nextImage,
PResults = &result
};
lock (_device.Lock)
{
_swapchainExtension.QueuePresent(_device.PresentQueue.InternalHandle, presentInfo);
}
CommandBufferPool.FreeUsedCommandBuffers();
Presented?.Invoke(this, null);
}
}
}

View File

@@ -1,165 +0,0 @@
using System;
using Avalonia;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanImage : IDisposable
{
private readonly VulkanDevice _device;
private readonly VulkanPhysicalDevice _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; }
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 uint MipLevels { get; private set; }
public PixelSize Size { get; }
public ulong MemorySize { get; private set; }
public uint CurrentLayout => (uint)_currentLayout;
public VulkanImage(
VulkanDevice device,
VulkanPhysicalDevice physicalDevice,
VulkanCommandBufferPool commandBufferPool,
uint format,
PixelSize size,
uint mipLevels = 0)
{
_device = device;
_physicalDevice = physicalDevice;
_commandBufferPool = commandBufferPool;
Format = (Format)format;
Size = size;
MipLevels = mipLevels;
_imageUsageFlags =
ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit |
ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageSampledBit;
Initialize();
}
public unsafe void Initialize()
{
if (!InternalHandle.HasValue)
{
MipLevels = MipLevels != 0 ? MipLevels : (uint)Math.Floor(Math.Log(Math.Max(Size.Width, Size.Height), 2));
var imageCreateInfo = new ImageCreateInfo
{
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
};
_device.Api.CreateImage(_device.InternalHandle, imageCreateInfo, null, out var image).ThrowOnError();
InternalHandle = image;
_device.Api.GetImageMemoryRequirements(_device.InternalHandle, InternalHandle.Value,
out var memoryRequirements);
var memoryAllocateInfo = new MemoryAllocateInfo
{
SType = StructureType.MemoryAllocateInfo,
AllocationSize = memoryRequirements.Size,
MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex(
_physicalDevice,
memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit)
};
_device.Api.AllocateMemory(_device.InternalHandle, memoryAllocateInfo, null,
out var imageMemory);
_imageMemory = imageMemory;
_device.Api.BindImageMemory(_device.InternalHandle, InternalHandle.Value, _imageMemory, 0);
MemorySize = memoryRequirements.Size;
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
};
_device.Api
.CreateImageView(_device.InternalHandle, imageViewCreateInfo, null, out var imageView)
.ThrowOnError();
_imageView = imageView;
_currentLayout = ImageLayout.Undefined;
TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
}
}
public ImageTiling Tiling => ImageTiling.Optimal;
internal void TransitionLayout(ImageLayout destinationLayout, AccessFlags destinationAccessFlags)
{
var commandBuffer = _commandBufferPool.CreateCommandBuffer();
commandBuffer.BeginRecording();
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle, InternalHandle.Value,
_currentLayout,
_currentAccessFlags,
destinationLayout, destinationAccessFlags,
MipLevels);
commandBuffer.EndRecording();
commandBuffer.Submit();
_currentLayout = destinationLayout;
_currentAccessFlags = destinationAccessFlags;
}
public void Dispose()
{
if (InternalHandle != null)
{
_device.Api.DestroyImageView(_device.InternalHandle, _imageView.Value, Span<AllocationCallbacks>.Empty);
_device.Api.DestroyImage(_device.InternalHandle, InternalHandle.Value, Span<AllocationCallbacks>.Empty);
_device.Api.FreeMemory(_device.InternalHandle, _imageMemory, Span<AllocationCallbacks>.Empty);
_imageView = default;
InternalHandle = null;
_imageMemory = default;
}
}
}
}

View File

@@ -1,135 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Silk.NET.Core;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.EXT;
namespace Ryujinx.Ava.Ui.Vulkan
{
public class VulkanInstance : IDisposable
{
private const string EngineName = "Avalonia Vulkan";
private VulkanInstance(Instance apiHandle, Vk api)
{
InternalHandle = apiHandle;
Api = api;
}
public IntPtr Handle => InternalHandle.Handle;
internal Instance InternalHandle { get; }
public Vk Api { get; }
internal static IEnumerable<string> RequiredInstanceExtensions
{
get
{
yield return "VK_KHR_surface";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
yield return "VK_KHR_xlib_surface";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
yield return "VK_KHR_win32_surface";
}
}
}
public void Dispose()
{
Api?.DestroyInstance(InternalHandle, Span<AllocationCallbacks>.Empty);
Api?.Dispose();
}
internal static unsafe VulkanInstance Create(VulkanOptions options)
{
var api = Vk.GetApi();
var applicationName = Marshal.StringToHGlobalAnsi(options.ApplicationName);
var engineName = Marshal.StringToHGlobalAnsi(EngineName);
var enabledExtensions = new List<string>(options.InstanceExtensions);
enabledExtensions.AddRange(RequiredInstanceExtensions);
var applicationInfo = new ApplicationInfo
{
PApplicationName = (byte*)applicationName,
ApiVersion = Vk.Version12.Value,
PEngineName = (byte*)engineName,
EngineVersion = new Version32(1, 0, 0),
ApplicationVersion = new Version32(1, 0, 0)
};
var enabledLayers = new HashSet<string>();
if (options.UseDebug)
{
enabledExtensions.Add(ExtDebugUtils.ExtensionName);
enabledExtensions.Add(ExtDebugReport.ExtensionName);
if (IsLayerAvailable(api, "VK_LAYER_KHRONOS_validation"))
enabledLayers.Add("VK_LAYER_KHRONOS_validation");
}
foreach (var layer in options.EnabledLayers)
enabledLayers.Add(layer);
var ppEnabledExtensions = stackalloc IntPtr[enabledExtensions.Count];
var ppEnabledLayers = stackalloc IntPtr[enabledLayers.Count];
for (var i = 0; i < enabledExtensions.Count; i++)
ppEnabledExtensions[i] = Marshal.StringToHGlobalAnsi(enabledExtensions[i]);
var layers = enabledLayers.ToList();
for (var i = 0; i < enabledLayers.Count; i++)
ppEnabledLayers[i] = Marshal.StringToHGlobalAnsi(layers[i]);
var instanceCreateInfo = new InstanceCreateInfo
{
SType = StructureType.InstanceCreateInfo,
PApplicationInfo = &applicationInfo,
PpEnabledExtensionNames = (byte**)ppEnabledExtensions,
PpEnabledLayerNames = (byte**)ppEnabledLayers,
EnabledExtensionCount = (uint)enabledExtensions.Count,
EnabledLayerCount = (uint)enabledLayers.Count
};
api.CreateInstance(in instanceCreateInfo, null, out var instance).ThrowOnError();
Marshal.FreeHGlobal(applicationName);
Marshal.FreeHGlobal(engineName);
for (var i = 0; i < enabledExtensions.Count; i++) Marshal.FreeHGlobal(ppEnabledExtensions[i]);
for (var i = 0; i < enabledLayers.Count; i++) Marshal.FreeHGlobal(ppEnabledLayers[i]);
return new VulkanInstance(instance, api);
}
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;
}
}
}

View File

@@ -1,59 +0,0 @@
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal static class VulkanMemoryHelper
{
internal static int FindSuitableMemoryTypeIndex(VulkanPhysicalDevice physicalDevice, uint memoryTypeBits,
MemoryPropertyFlags flags)
{
physicalDevice.Api.GetPhysicalDeviceMemoryProperties(physicalDevice.InternalHandle, 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(VulkanDevice device,
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
};
device.Api.CmdPipelineBarrier(
commandBuffer,
PipelineStageFlags.PipelineStageAllCommandsBit,
PipelineStageFlags.PipelineStageAllCommandsBit,
0,
0,
null,
0,
null,
1,
barrier);
}
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Ui.Vulkan
{
public class VulkanOptions
{
/// <summary>
/// Sets the application name of the Vulkan instance
/// </summary>
public string ApplicationName { get; set; }
/// <summary>
/// Specifies additional extensions to enable if available on the instance
/// </summary>
public IEnumerable<string> InstanceExtensions { get; set; } = Enumerable.Empty<string>();
/// <summary>
/// Specifies layers to enable if available on the instance
/// </summary>
public IEnumerable<string> EnabledLayers { get; set; } = Enumerable.Empty<string>();
/// <summary>
/// Enables the debug layer
/// </summary>
public bool UseDebug { get; set; }
/// <summary>
/// Selects the first suitable discrete GPU available
/// </summary>
public bool PreferDiscreteGpu { get; set; }
/// <summary>
/// Sets the device to use if available and suitable.
/// </summary>
public string PreferredDevice { get; set; }
/// <summary>
/// Max number of device queues to request
/// </summary>
public uint MaxQueueCount { get; set; }
}
}

View File

@@ -1,219 +0,0 @@
using Ryujinx.Graphics.Vulkan;
using Silk.NET.Core;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace Ryujinx.Ava.Ui.Vulkan
{
public unsafe class VulkanPhysicalDevice
{
private VulkanPhysicalDevice(PhysicalDevice apiHandle, Vk api, uint queueCount, uint queueFamilyIndex)
{
InternalHandle = apiHandle;
Api = api;
QueueCount = queueCount;
QueueFamilyIndex = queueFamilyIndex;
api.GetPhysicalDeviceProperties(apiHandle, out var properties);
DeviceName = Marshal.PtrToStringAnsi((IntPtr)properties.DeviceName);
DeviceId = VulkanInitialization.StringFromIdPair(properties.VendorID, properties.DeviceID);
var version = (Version32)properties.ApiVersion;
ApiVersion = new Version((int)version.Major, (int)version.Minor, 0, (int)version.Patch);
}
internal PhysicalDevice InternalHandle { get; }
internal Vk Api { get; }
public uint QueueCount { get; }
public uint QueueFamilyIndex { get; }
public IntPtr Handle => InternalHandle.Handle;
public string DeviceName { get; }
public string DeviceId { get; }
public Version ApiVersion { get; }
public static Dictionary<PhysicalDevice, PhysicalDeviceProperties> PhysicalDevices { get; private set; }
public static IEnumerable<KeyValuePair<PhysicalDevice, PhysicalDeviceProperties>> SuitableDevices { get; private set; }
internal static void SelectAvailableDevices(VulkanInstance instance,
VulkanSurface surface, bool preferDiscreteGpu, string preferredDevice)
{
uint physicalDeviceCount;
instance.Api.EnumeratePhysicalDevices(instance.InternalHandle, &physicalDeviceCount, null).ThrowOnError();
var physicalDevices = new PhysicalDevice[physicalDeviceCount];
fixed (PhysicalDevice* pPhysicalDevices = physicalDevices)
{
instance.Api.EnumeratePhysicalDevices(instance.InternalHandle, &physicalDeviceCount, pPhysicalDevices)
.ThrowOnError();
}
PhysicalDevices = new Dictionary<PhysicalDevice, PhysicalDeviceProperties>();
foreach (var physicalDevice in physicalDevices)
{
instance.Api.GetPhysicalDeviceProperties(physicalDevice, out var properties);
PhysicalDevices.Add(physicalDevice, properties);
}
SuitableDevices = PhysicalDevices.Where(x => IsSuitableDevice(
instance.Api,
x.Key,
x.Value,
surface.ApiHandle,
out _,
out _));
}
internal static VulkanPhysicalDevice FindSuitablePhysicalDevice(VulkanInstance instance,
VulkanSurface surface, bool preferDiscreteGpu, string preferredDevice)
{
SelectAvailableDevices(instance, surface, preferDiscreteGpu, preferredDevice);
uint queueFamilyIndex = 0;
uint queueCount = 0;
if (!string.IsNullOrWhiteSpace(preferredDevice))
{
var physicalDevice = SuitableDevices.FirstOrDefault(x => VulkanInitialization.StringFromIdPair(x.Value.VendorID, x.Value.DeviceID) == preferredDevice);
queueFamilyIndex = FindSuitableQueueFamily(instance.Api, physicalDevice.Key,
surface.ApiHandle, out queueCount);
if (queueFamilyIndex != int.MaxValue)
{
return new VulkanPhysicalDevice(physicalDevice.Key, instance.Api, queueCount, queueFamilyIndex);
}
}
if (preferDiscreteGpu)
{
var discreteGpus = SuitableDevices.Where(p => p.Value.DeviceType == PhysicalDeviceType.DiscreteGpu);
foreach (var gpu in discreteGpus)
{
queueFamilyIndex = FindSuitableQueueFamily(instance.Api, gpu.Key,
surface.ApiHandle, out queueCount);
if (queueFamilyIndex != int.MaxValue)
{
return new VulkanPhysicalDevice(gpu.Key, instance.Api, queueCount, queueFamilyIndex);
}
}
}
foreach (var physicalDevice in SuitableDevices)
{
queueFamilyIndex = FindSuitableQueueFamily(instance.Api, physicalDevice.Key,
surface.ApiHandle, out queueCount);
if (queueFamilyIndex != int.MaxValue)
{
return new VulkanPhysicalDevice(physicalDevice.Key, instance.Api, queueCount, queueFamilyIndex);
}
}
throw new Exception("No suitable physical device found");
}
private static unsafe bool IsSuitableDevice(Vk api, PhysicalDevice physicalDevice, PhysicalDeviceProperties properties, SurfaceKHR surface,
out uint queueCount, out uint familyIndex)
{
queueCount = 0;
familyIndex = 0;
if (properties.DeviceType == PhysicalDeviceType.Cpu) return false;
var extensionMatches = 0;
uint propertiesCount;
api.EnumerateDeviceExtensionProperties(physicalDevice, (byte*)null, &propertiesCount, null).ThrowOnError();
var extensionProperties = new ExtensionProperties[propertiesCount];
fixed (ExtensionProperties* pExtensionProperties = extensionProperties)
{
api.EnumerateDeviceExtensionProperties(
physicalDevice,
(byte*)null,
&propertiesCount,
pExtensionProperties).ThrowOnError();
for (var i = 0; i < propertiesCount; i++)
{
var extensionName = Marshal.PtrToStringAnsi((IntPtr)pExtensionProperties[i].ExtensionName);
if (VulkanInitialization.RequiredExtensions.Contains(extensionName))
{
extensionMatches++;
}
}
}
if (extensionMatches == VulkanInitialization.RequiredExtensions.Length)
{
familyIndex = FindSuitableQueueFamily(api, physicalDevice, surface, out queueCount);
return familyIndex != uint.MaxValue;
}
return false;
}
internal unsafe string[] GetSupportedExtensions()
{
uint propertiesCount;
Api.EnumerateDeviceExtensionProperties(InternalHandle, (byte*)null, &propertiesCount, null).ThrowOnError();
var extensionProperties = new ExtensionProperties[propertiesCount];
fixed (ExtensionProperties* pExtensionProperties = extensionProperties)
{
Api.EnumerateDeviceExtensionProperties(InternalHandle, (byte*)null, &propertiesCount, pExtensionProperties)
.ThrowOnError();
}
return extensionProperties.Select(x => Marshal.PtrToStringAnsi((IntPtr)x.ExtensionName)).ToArray();
}
private static uint FindSuitableQueueFamily(Vk api, PhysicalDevice physicalDevice, SurfaceKHR surface,
out uint queueCount)
{
const QueueFlags RequiredFlags = QueueFlags.QueueGraphicsBit | QueueFlags.QueueComputeBit;
var khrSurface = new KhrSurface(api.Context);
uint propertiesCount;
api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, &propertiesCount, null);
var properties = new QueueFamilyProperties[propertiesCount];
fixed (QueueFamilyProperties* pProperties = properties)
{
api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, &propertiesCount, pProperties);
}
for (uint index = 0; index < propertiesCount; index++)
{
var queueFlags = properties[index].QueueFlags;
khrSurface.GetPhysicalDeviceSurfaceSupport(physicalDevice, index, surface, out var surfaceSupported)
.ThrowOnError();
if (queueFlags.HasFlag(RequiredFlags) && surfaceSupported)
{
queueCount = properties[index].QueueCount;
return index;
}
}
queueCount = 0;
return uint.MaxValue;
}
}
}

View File

@@ -1,70 +0,0 @@
using Avalonia;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Ryujinx.Graphics.Vulkan;
using Silk.NET.Vulkan;
using System;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanPlatformInterface : IDisposable
{
private static VulkanOptions _options;
private VulkanPlatformInterface(VulkanInstance instance)
{
Instance = instance;
Api = instance.Api;
}
public VulkanPhysicalDevice PhysicalDevice { get; private set; }
public VulkanInstance Instance { get; }
public Vk Api { get; private set; }
public VulkanSurfaceRenderTarget MainSurface { get; set; }
public void Dispose()
{
Instance?.Dispose();
Api?.Dispose();
}
private static VulkanPlatformInterface TryCreate()
{
_options = AvaloniaLocator.Current.GetService<VulkanOptions>() ?? new VulkanOptions();
var instance = VulkanInstance.Create(_options);
return new VulkanPlatformInterface(instance);
}
public static bool TryInitialize()
{
var feature = TryCreate();
if (feature != null)
{
AvaloniaLocator.CurrentMutable.Bind<VulkanPlatformInterface>().ToConstant(feature);
return true;
}
return false;
}
public VulkanSurfaceRenderTarget CreateRenderTarget(IVulkanPlatformSurface platformSurface)
{
var surface = VulkanSurface.CreateSurface(Instance, platformSurface);
if (PhysicalDevice == null)
{
PhysicalDevice = VulkanPhysicalDevice.FindSuitablePhysicalDevice(Instance, surface, _options.PreferDiscreteGpu, _options.PreferredDevice);
}
var renderTarget = new VulkanSurfaceRenderTarget(this, surface);
if (MainSurface == null && surface != null)
{
MainSurface = renderTarget;
}
return renderTarget;
}
}
}

View File

@@ -1,18 +0,0 @@
using System;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanQueue
{
public VulkanQueue(VulkanDevice device, Queue apiHandle)
{
Device = device;
InternalHandle = apiHandle;
}
public VulkanDevice Device { get; }
public IntPtr Handle => InternalHandle.Handle;
internal Queue InternalHandle { get; }
}
}

View File

@@ -1,32 +0,0 @@
using System;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanSemaphorePair : IDisposable
{
private readonly VulkanDevice _device;
public unsafe VulkanSemaphorePair(VulkanDevice device)
{
_device = device;
var semaphoreCreateInfo = new SemaphoreCreateInfo { SType = StructureType.SemaphoreCreateInfo };
_device.Api.CreateSemaphore(_device.InternalHandle, semaphoreCreateInfo, null, out var semaphore).ThrowOnError();
ImageAvailableSemaphore = semaphore;
_device.Api.CreateSemaphore(_device.InternalHandle, semaphoreCreateInfo, null, out semaphore).ThrowOnError();
RenderFinishedSemaphore = semaphore;
}
internal Semaphore ImageAvailableSemaphore { get; }
internal Semaphore RenderFinishedSemaphore { get; }
public unsafe void Dispose()
{
_device.Api.DestroySemaphore(_device.InternalHandle, ImageAvailableSemaphore, null);
_device.Api.DestroySemaphore(_device.InternalHandle, RenderFinishedSemaphore, null);
}
}
}

View File

@@ -1,75 +0,0 @@
using System;
using Avalonia;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
namespace Ryujinx.Ava.Ui.Vulkan
{
public class VulkanSurface : IDisposable
{
private readonly VulkanInstance _instance;
private readonly IVulkanPlatformSurface _vulkanPlatformSurface;
private VulkanSurface(IVulkanPlatformSurface vulkanPlatformSurface, VulkanInstance instance)
{
_vulkanPlatformSurface = vulkanPlatformSurface;
_instance = instance;
ApiHandle = vulkanPlatformSurface.CreateSurface(instance);
}
internal SurfaceKHR ApiHandle { get; }
internal static KhrSurface SurfaceExtension { get; private set; }
internal PixelSize SurfaceSize => _vulkanPlatformSurface.SurfaceSize;
public void Dispose()
{
SurfaceExtension.DestroySurface(_instance.InternalHandle, ApiHandle, Span<AllocationCallbacks>.Empty);
_vulkanPlatformSurface.Dispose();
}
internal static VulkanSurface CreateSurface(VulkanInstance instance, IVulkanPlatformSurface vulkanPlatformSurface)
{
if (SurfaceExtension == null)
{
instance.Api.TryGetInstanceExtension(instance.InternalHandle, out KhrSurface extension);
SurfaceExtension = extension;
}
return new VulkanSurface(vulkanPlatformSurface, instance);
}
internal bool CanSurfacePresent(VulkanPhysicalDevice physicalDevice)
{
SurfaceExtension.GetPhysicalDeviceSurfaceSupport(physicalDevice.InternalHandle, physicalDevice.QueueFamilyIndex, ApiHandle, out var isSupported);
return isSupported;
}
internal SurfaceFormatKHR GetSurfaceFormat(VulkanPhysicalDevice physicalDevice)
{
Span<uint> surfaceFormatsCount = stackalloc uint[1];
SurfaceExtension.GetPhysicalDeviceSurfaceFormats(physicalDevice.InternalHandle, ApiHandle, surfaceFormatsCount, Span<SurfaceFormatKHR>.Empty);
Span<SurfaceFormatKHR> surfaceFormats = stackalloc SurfaceFormatKHR[(int)surfaceFormatsCount[0]];
SurfaceExtension.GetPhysicalDeviceSurfaceFormats(physicalDevice.InternalHandle, ApiHandle, surfaceFormatsCount, surfaceFormats);
if (surfaceFormats.Length == 1 && surfaceFormats[0].Format == Format.Undefined)
{
return new SurfaceFormatKHR(Format.B8G8R8A8Unorm, ColorSpaceKHR.ColorspaceSrgbNonlinearKhr);
}
foreach (var format in surfaceFormats)
{
if (format.Format == Format.B8G8R8A8Unorm && format.ColorSpace == ColorSpaceKHR.ColorspaceSrgbNonlinearKhr)
{
return format;
}
}
return surfaceFormats[0];
}
}
}

View File

@@ -1,43 +0,0 @@
using System;
using Avalonia;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanSurfaceRenderingSession : IDisposable
{
private readonly VulkanDevice _device;
private readonly VulkanSurfaceRenderTarget _renderTarget;
public VulkanSurfaceRenderingSession(VulkanDisplay display, VulkanDevice device,
VulkanSurfaceRenderTarget renderTarget, float scaling)
{
Display = display;
_device = device;
_renderTarget = renderTarget;
Scaling = scaling;
Begin();
}
public VulkanDisplay Display { get; }
public PixelSize Size => _renderTarget.Size;
public Vk Api => _device.Api;
public float Scaling { get; }
private void Begin()
{
if (!Display.EnsureSwapchainAvailable())
{
_renderTarget.RecreateImage();
}
}
public void Dispose()
{
_renderTarget.EndDraw();
}
}
}

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);
}
}

View File

@@ -10,7 +10,6 @@ using Ryujinx.Audio.Backends.SoundIo;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
@@ -252,34 +251,19 @@ namespace Ryujinx.Ava.Ui.ViewModels
{
_gpuIds = new List<string>();
List<string> names = new List<string>();
if (!Program.UseVulkan)
{
var devices = VulkanRenderer.GetPhysicalDevices();
var devices = VulkanRenderer.GetPhysicalDevices();
if (devices.Length == 0)
{
IsVulkanAvailable = false;
GraphicsBackendIndex = 1;
}
else
{
foreach (var device in devices)
{
_gpuIds.Add(device.Id);
names.Add($"{device.Name} {(device.IsDiscrete ? "(dGPU)" : "")}");
}
}
if (devices.Length == 0)
{
IsVulkanAvailable = false;
GraphicsBackendIndex = 1;
}
else
{
foreach (var device in VulkanPhysicalDevice.SuitableDevices)
foreach (var device in devices)
{
_gpuIds.Add(
VulkanInitialization.StringFromIdPair(device.Value.VendorID, device.Value.DeviceID));
var value = device.Value;
var name = value.DeviceName;
names.Add(
$"{Marshal.PtrToStringAnsi((IntPtr)name)} {(device.Value.DeviceType == PhysicalDeviceType.DiscreteGpu ? "(dGPU)" : "")}");
_gpuIds.Add(device.Id);
names.Add($"{device.Name} {(device.IsDiscrete ? "(dGPU)" : "")}");
}
}
@@ -407,7 +391,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
_previousVolumeLevel = Volume;
}
public async Task SaveSettings()
public void SaveSettings()
{
ConfigurationState config = ConfigurationState.Instance;
@@ -422,8 +406,6 @@ namespace Ryujinx.Ava.Ui.ViewModels
config.System.TimeZone.Value = TimeZone;
}
bool requiresRestart = config.Graphics.GraphicsBackend.Value != (GraphicsBackend)GraphicsBackendIndex;
config.Logger.EnableError.Value = EnableError;
config.Logger.EnableTrace.Value = EnableTrace;
config.Logger.EnableWarn.Value = EnableWarn;
@@ -456,19 +438,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
config.System.Language.Value = (Language)Language;
config.System.Region.Value = (Region)Region;
var selectedGpu = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
if (!requiresRestart)
{
var platform = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
if (platform != null)
{
var physicalDevice = platform.PhysicalDevice;
requiresRestart = physicalDevice.DeviceId != selectedGpu;
}
}
config.Graphics.PreferredGpu.Value = selectedGpu;
config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex)
{
@@ -507,20 +477,6 @@ namespace Ryujinx.Ava.Ui.ViewModels
MainWindow.UpdateGraphicsConfig();
_previousVolumeLevel = Volume;
if (requiresRestart)
{
var choice = await ContentDialogHelper.CreateChoiceDialog(
LocaleManager.Instance["SettingsAppRequiredRestartMessage"],
LocaleManager.Instance["SettingsGpuBackendRestartMessage"],
LocaleManager.Instance["SettingsGpuBackendRestartSubMessage"]);
if (choice)
{
Process.Start(Environment.ProcessPath);
Environment.Exit(0);
}
}
}
public void RevertIfNotSaved()

View File

@@ -0,0 +1,27 @@
<window:StyleableWindow 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"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
x:Class="Ryujinx.Ava.Ui.Windows.ContentDialogOverlayWindow"
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
Title="ContentDialogOverlayWindow">
<window:StyleableWindow.Styles>
<Style Selector="ui|ContentDialog /template/ Panel#LayoutRoot">
<Setter Property="Background"
Value="Transparent" />
</Style>
</window:StyleableWindow.Styles>
<ContentControl
Focusable="False"
IsVisible="False"
KeyboardNavigation.IsTabStop="False">
<ui:ContentDialog Name="ContentDialog"
IsPrimaryButtonEnabled="True"
IsSecondaryButtonEnabled="True"
IsVisible="False" />
</ContentControl>
</window:StyleableWindow>

View File

@@ -0,0 +1,25 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
namespace Ryujinx.Ava.Ui.Windows
{
public partial class ContentDialogOverlayWindow : StyleableWindow
{
public ContentDialogOverlayWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
ExtendClientAreaToDecorationsHint = true;
TransparencyLevelHint = WindowTransparencyLevel.Transparent;
WindowStartupLocation = WindowStartupLocation.Manual;
SystemDecorations = SystemDecorations.None;
ExtendClientAreaTitleBarHeightHint = 0;
Background = Brushes.Transparent;
CanResize = false;
}
}
}

View File

@@ -462,6 +462,7 @@
VerticalAlignment="Stretch"
Background="{DynamicResource ThemeContentBackgroundColor}"
IsVisible="{Binding ShowLoadProgress}"
Name="LoadingView"
ZIndex="1000">
<Grid
Margin="40"

View File

@@ -58,7 +58,7 @@ namespace Ryujinx.Ava.Ui.Windows
internal AppHost AppHost { get; private set; }
public InputManager InputManager { get; private set; }
internal RendererControl RendererControl { get; private set; }
internal RendererHost RendererControl { get; private set; }
internal MainWindowViewModel ViewModel { get; private set; }
public SettingsWindow SettingsWindow { get; set; }
@@ -237,7 +237,16 @@ namespace Ryujinx.Ava.Ui.Windows
_mainViewContent = MainContent.Content as Control;
RendererControl = Program.UseVulkan ? new VulkanRendererControl(ConfigurationState.Instance.Logger.GraphicsDebugLevel) : new OpenGLRendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
RendererControl = new RendererHost(ConfigurationState.Instance.Logger.GraphicsDebugLevel);
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl)
{
RendererControl.CreateOpenGL();
}
else
{
RendererControl.CreateVulkan();
}
AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
if (!AppHost.LoadGuestApplication().Result)
@@ -296,8 +305,8 @@ namespace Ryujinx.Ava.Ui.Windows
public void SwitchToGameControl(bool startFullscreen = false)
{
ViewModel.ShowContent = true;
ViewModel.ShowLoadProgress = false;
ViewModel.ShowContent = true;
ViewModel.IsLoadingIndeterminate = false;
Dispatcher.UIThread.InvokeAsync(() =>
@@ -346,17 +355,17 @@ namespace Ryujinx.Ava.Ui.Windows
Dispatcher.UIThread.InvokeAsync(() =>
{
if (MainContent.Content != _mainViewContent)
{
MainContent.Content = _mainViewContent;
}
ViewModel.ShowMenuAndStatusBar = true;
ViewModel.ShowContent = true;
ViewModel.ShowLoadProgress = false;
ViewModel.IsLoadingIndeterminate = false;
Cursor = Cursor.Default;
if (MainContent.Content != _mainViewContent)
{
MainContent.Content = _mainViewContent;
}
AppHost = null;
HandleRelaunch();

View File

@@ -211,9 +211,9 @@ namespace Ryujinx.Ava.Ui.Windows
}
}
private async void SaveButton_Clicked(object sender, RoutedEventArgs e)
private void SaveButton_Clicked(object sender, RoutedEventArgs e)
{
await SaveSettings();
SaveSettings();
Close();
}
@@ -224,14 +224,14 @@ namespace Ryujinx.Ava.Ui.Windows
Close();
}
private async void ApplyButton_Clicked(object sender, RoutedEventArgs e)
private void ApplyButton_Clicked(object sender, RoutedEventArgs e)
{
await SaveSettings();
SaveSettings();
}
private async Task SaveSettings()
private void SaveSettings()
{
await ViewModel.SaveSettings();
ViewModel.SaveSettings();
ControllerSettings?.SaveCurrentProfile();