Avalonia - Couple fixes and improvements to vulkan (#3483)

* drop split devices, rebase

* add fallback to opengl if vulkan is not available

* addressed review

* ensure present image references are incremented and decremented when necessary

* allow changing vsync for vulkan

* fix screenshot on avalonia vulkan

* save favorite when toggled

* improve sync between popups

* use separate devices for each new window

* fix crash when closing window

* addressed review

* don't create the main window with immediate mode

* change skia vk delegate to method

* update vulkan throwonerror

* addressed review
This commit is contained in:
Emmanuel Hansen
2022-08-16 16:32:37 +00:00
committed by GitHub
parent 0ec933a615
commit c8f9292bab
28 changed files with 585 additions and 312 deletions

View File

@@ -12,6 +12,7 @@ using Ryujinx.Audio.Integration;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.Ui.Backend.Vulkan;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Ava.Ui.Vulkan;
@@ -334,6 +335,8 @@ namespace Ryujinx.Ava
return;
}
AvaloniaLocator.Current.GetService<VulkanPlatformInterface>()?.MainSurface.Display.ChangeVSyncMode(true);
_isStopped = true;
_isActive = false;
}
@@ -596,12 +599,13 @@ namespace Ryujinx.Ava
if (Program.UseVulkan)
{
var vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
renderer = new VulkanRenderer(vulkan.Instance.InternalHandle,
vulkan.Device.InternalHandle,
vulkan.MainSurface.Device.InternalHandle,
vulkan.PhysicalDevice.InternalHandle,
vulkan.Device.Queue.InternalHandle,
vulkan.MainSurface.Device.Queue.InternalHandle,
vulkan.PhysicalDevice.QueueFamilyIndex,
vulkan.Device.Lock);
vulkan.MainSurface.Device.Lock);
}
else
{
@@ -775,7 +779,10 @@ namespace Ryujinx.Ava
Width = (int)e.Width;
Height = (int)e.Height;
SetRendererWindowSize(e);
if (!Program.UseVulkan)
{
SetRendererWindowSize(e);
}
}
private void MainLoop()
@@ -815,12 +822,11 @@ namespace Ryujinx.Ava
_renderer.ScreenCaptured += Renderer_ScreenCaptured;
if (!Program.UseVulkan)
{
(_renderer as OpenGLRenderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext((Renderer as OpenGLRendererControl).GameContext));
(_renderer as OpenGLRenderer)?.InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext((Renderer as OpenGLRendererControl).GameContext));
Renderer.MakeCurrent();
}
Renderer.MakeCurrent();
AvaloniaLocator.Current.GetService<VulkanPlatformInterface>()?.MainSurface?.Display?.ChangeVSyncMode(Device.EnableDeviceVsync);
Device.Gpu.Renderer.Initialize(_glLogLevel);
@@ -837,8 +843,6 @@ namespace Ryujinx.Ava
Renderer.Start();
Renderer.QueueRender();
while (_isActive)
{
if (Device.WaitFifo())
@@ -889,6 +893,16 @@ namespace Ryujinx.Ava
$"FIFO: {Device.Statistics.GetFifoPercent():00.00} %",
$"GPU: {_renderer.GetHardwareInfo().GpuVendor}"));
if (Program.UseVulkan)
{
var platformInterface = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
if (platformInterface.MainSurface.Display.IsSurfaceChanged())
{
SetRendererWindowSize(new Size(Width, Height));
return;
}
}
Renderer.Present(image);
}
@@ -970,6 +984,9 @@ namespace Ryujinx.Ava
{
case KeyboardHotkeyState.ToggleVSync:
Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
AvaloniaLocator.Current.GetService<VulkanPlatformInterface>()?.MainSurface?.Display?.ChangeVSyncMode(Device.EnableDeviceVsync);
break;
case KeyboardHotkeyState.Screenshot:
ScreenshotRequested = true;

View File

@@ -94,7 +94,6 @@ namespace Ryujinx.Ava
.With(new Ui.Vulkan.VulkanOptions()
{
ApplicationName = "Ryujinx.Graphics.Vulkan",
VulkanVersion = new Version(1, 2),
MaxQueueCount = 2,
PreferDiscreteGpu = true,
PreferredDevice = !PreviewerDetached ? "" : ConfigurationState.Instance.Graphics.PreferredGpu.Value,
@@ -181,6 +180,18 @@ namespace Ryujinx.Ava
UseVulkan = PreviewerDetached ? ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan : false;
if (UseVulkan)
{
if (VulkanRenderer.GetPhysicalDevices().Length == 0)
{
UseVulkan = false;
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl;
Logger.Warning?.PrintMsg(LogClass.Application, "A suitable Vulkan physical device is not available. Falling back to OpenGL");
}
}
if (UseVulkan)
{
// With a custom gpu backend, avalonia doesn't enable dpi awareness, so the backend must handle it. This isn't so for the opengl backed,

View File

@@ -7,7 +7,8 @@ namespace Ryujinx.Ava.Ui.Vulkan
{
public static void ThrowOnError(this Result result)
{
if (result != Result.Success)
// Only negative result codes are errors.
if ((int)result < (int)Result.Success)
{
throw new Exception($"Unexpected API error \"{result}\".");
}

View File

@@ -1,26 +1,90 @@
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; set; }
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();
}
@@ -45,20 +109,22 @@ namespace Ryujinx.Ava.Ui.Backend.Vulkan
{
GrContext.ResetContext();
var image = _surface.GetImage();
var imageInfo = new GRVkImageInfo()
{
CurrentQueueFamily = disp.QueueFamilyIndex,
Format = _surface.ImageFormat,
Image = _surface.Image.Handle,
ImageLayout = (uint)_surface.Image.CurrentLayout,
ImageTiling = (uint)_surface.Image.Tiling,
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 = _surface.Image.MemoryHandle,
Memory = image.MemoryHandle,
Flags = 0,
Offset = 0,
Size = _surface.MemorySize

View File

@@ -13,71 +13,12 @@ namespace Ryujinx.Ava.Ui.Backend.Vulkan
public class VulkanSkiaGpu : ISkiaGpu
{
private readonly VulkanPlatformInterface _vulkan;
private readonly long? _maxResourceBytes;
private GRVkBackendContext _grVkBackend;
private bool _initialized;
public GRContext GrContext { get; private set; }
public long? MaxResourceBytes { get; }
public VulkanSkiaGpu(long? maxResourceBytes)
{
_vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
_maxResourceBytes = maxResourceBytes;
}
private void Initialize()
{
if (_initialized)
{
return;
}
_initialized = true;
GRVkGetProcedureAddressDelegate getProc = (string name, IntPtr instanceHandle, IntPtr deviceHandle) =>
{
IntPtr addr = IntPtr.Zero;
if (deviceHandle != IntPtr.Zero)
{
addr = _vulkan.Device.Api.GetDeviceProcAddr(new Device(deviceHandle), name);
if (addr != IntPtr.Zero)
{
return addr;
}
addr = _vulkan.Device.Api.GetDeviceProcAddr(new Device(_vulkan.Device.Handle), name);
if (addr != IntPtr.Zero)
{
return addr;
}
}
addr = _vulkan.Device.Api.GetInstanceProcAddr(new Instance(_vulkan.Instance.Handle), name);
if (addr == IntPtr.Zero)
{
addr = _vulkan.Device.Api.GetInstanceProcAddr(new Instance(instanceHandle), name);
}
return addr;
};
_grVkBackend = new GRVkBackendContext()
{
VkInstance = _vulkan.Device.Handle,
VkPhysicalDevice = _vulkan.PhysicalDevice.Handle,
VkDevice = _vulkan.Device.Handle,
VkQueue = _vulkan.Device.Queue.Handle,
GraphicsQueueIndex = _vulkan.PhysicalDevice.QueueFamilyIndex,
GetProcedureAddress = getProc
};
GrContext = GRContext.CreateVulkan(_grVkBackend);
if (_maxResourceBytes.HasValue)
{
GrContext.SetResourceCacheLimit(_maxResourceBytes.Value);
}
MaxResourceBytes = maxResourceBytes;
}
public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces)
@@ -106,10 +47,6 @@ namespace Ryujinx.Ava.Ui.Backend.Vulkan
VulkanRenderTarget vulkanRenderTarget = new VulkanRenderTarget(_vulkan, window);
Initialize();
vulkanRenderTarget.GrContext = GrContext;
return vulkanRenderTarget;
}

View File

@@ -1,5 +1,6 @@
using System;
using Avalonia;
using Ryujinx.Graphics.Vulkan;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
@@ -7,24 +8,35 @@ namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
internal class VulkanSurfaceRenderTarget : IDisposable
{
private readonly VulkanPlatformInterface _platformInterface;
private readonly Format _format;
public VulkanImage Image { get; private set; }
public bool IsCorrupted { get; private set; } = true;
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;
Display = VulkanDisplay.CreateDisplay(platformInterface.Instance, platformInterface.Device,
platformInterface.PhysicalDevice, surface);
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;
@@ -33,13 +45,13 @@ namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
public bool IsRgba { get; }
public uint ImageFormat => (uint) _format;
public uint ImageFormat => (uint)_format;
public ulong MemorySize => Image.MemorySize;
public VulkanDisplay Display { get; }
public VulkanDisplay Display { get; private set; }
public VulkanSurface Surface { get; }
public VulkanSurface Surface { get; private set; }
public uint UsageFlags => Image.UsageFlags;
@@ -47,46 +59,76 @@ namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
public void Dispose()
{
_platformInterface.Device.WaitIdle();
DestroyImage();
Display?.Dispose();
Surface?.Dispose();
lock (_lock)
{
DestroyImage();
Display?.Dispose();
Surface?.Dispose();
Device?.Dispose();
Display = null;
Surface = null;
}
}
public VulkanSurfaceRenderingSession BeginDraw(float scaling)
{
var session = new VulkanSurfaceRenderingSession(Display, _platformInterface.Device, this, scaling);
if (Image == null)
{
RecreateImage();
}
if (IsCorrupted)
{
IsCorrupted = false;
DestroyImage();
CreateImage();
}
else
{
Image.TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
}
_commandBuffer?.WaitForFence();
_commandBuffer = null;
var session = new VulkanSurfaceRenderingSession(Display, Device, this, scaling);
Image.TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
return session;
}
public void Invalidate()
public void RecreateImage()
{
IsCorrupted = true;
DestroyImage();
CreateImage();
}
private void CreateImage()
{
Size = Display.Size;
Image = new VulkanImage(_platformInterface.Device, _platformInterface.PhysicalDevice, _platformInterface.Device.CommandBufferPool, ImageFormat, Size);
Image = new VulkanImage(Device, _platformInterface.PhysicalDevice, Display.CommandBufferPool, ImageFormat, Size);
}
private void DestroyImage()
{
_platformInterface.Device.WaitIdle();
_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

@@ -10,6 +10,7 @@ namespace Ryujinx.Ava.Ui.Vulkan
private readonly CommandPool _commandPool;
private readonly List<VulkanCommandBuffer> _usedCommandBuffers = new();
private readonly object _lock = new object();
public unsafe VulkanCommandBufferPool(VulkanDevice device, VulkanPhysicalDevice physicalDevice)
{
@@ -36,9 +37,12 @@ namespace Ryujinx.Ava.Ui.Vulkan
Level = CommandBufferLevel.Primary
};
_device.Api.AllocateCommandBuffers(_device.InternalHandle, commandBufferAllocateInfo, out var commandBuffer);
lock (_lock)
{
_device.Api.AllocateCommandBuffers(_device.InternalHandle, commandBufferAllocateInfo, out var commandBuffer);
return commandBuffer;
return commandBuffer;
}
}
public VulkanCommandBuffer CreateCommandBuffer()
@@ -48,7 +52,7 @@ namespace Ryujinx.Ava.Ui.Vulkan
public void FreeUsedCommandBuffers()
{
lock (_usedCommandBuffers)
lock (_lock)
{
foreach (var usedCommandBuffer in _usedCommandBuffers)
{
@@ -61,7 +65,7 @@ namespace Ryujinx.Ava.Ui.Vulkan
private void DisposeCommandBuffer(VulkanCommandBuffer commandBuffer)
{
lock (_usedCommandBuffers)
lock (_lock)
{
_usedCommandBuffers.Add(commandBuffer);
}
@@ -69,8 +73,11 @@ namespace Ryujinx.Ava.Ui.Vulkan
public void Dispose()
{
FreeUsedCommandBuffers();
_device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, Span<AllocationCallbacks>.Empty);
lock (_lock)
{
FreeUsedCommandBuffers();
_device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, Span<AllocationCallbacks>.Empty);
}
}
public class VulkanCommandBuffer : IDisposable
@@ -80,6 +87,8 @@ namespace Ryujinx.Ava.Ui.Vulkan
private readonly Fence _fence;
private bool _hasEnded;
private bool _hasStarted;
private bool _isDisposed;
private object _lock = new object();
public IntPtr Handle => InternalHandle.Handle;
@@ -101,6 +110,22 @@ namespace Ryujinx.Ava.Ui.Vulkan
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)
@@ -173,9 +198,17 @@ namespace Ryujinx.Ava.Ui.Vulkan
public void Dispose()
{
_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);
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

@@ -14,12 +14,9 @@ namespace Ryujinx.Ava.Ui.Vulkan
api.GetDeviceQueue(apiHandle, physicalDevice.QueueFamilyIndex, 0, out var queue);
var vulkanQueue = new VulkanQueue(this, queue);
Queue = vulkanQueue;
Queue = new VulkanQueue(this, queue);
PresentQueue = vulkanQueue;
CommandBufferPool = new VulkanCommandBufferPool(this, physicalDevice);
PresentQueue = Queue;
}
public IntPtr Handle => InternalHandle.Handle;
@@ -29,13 +26,12 @@ namespace Ryujinx.Ava.Ui.Vulkan
public VulkanQueue Queue { get; private set; }
public VulkanQueue PresentQueue { get; }
public VulkanCommandBufferPool CommandBufferPool { get; }
public void Dispose()
{
WaitIdle();
CommandBufferPool?.Dispose();
Queue = null;
Api.DestroyDevice(InternalHandle, Span<AllocationCallbacks>.Empty);
}
internal void Submit(SubmitInfo submitInfo, Fence fence = default)

View File

@@ -3,7 +3,6 @@ using System.Linq;
using System.Threading;
using Avalonia;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Ryujinx.Ui.Common.Configuration;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
@@ -15,16 +14,19 @@ namespace Ryujinx.Ava.Ui.Vulkan
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 VulkanDevice _device { get; }
private ImageView[] _swapchainImageViews = new ImageView[0];
private ImageView[] _swapchainImageViews = Array.Empty<ImageView>();
private bool _vsyncStateChanged;
private bool _vsyncEnabled;
private bool _surfaceChanged;
public event EventHandler Presented;
public VulkanCommandBufferPool CommandBufferPool { get; set; }
@@ -73,6 +75,14 @@ namespace Ryujinx.Ava.Ui.Vulkan
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)
@@ -193,22 +203,23 @@ namespace Ryujinx.Ava.Ui.Vulkan
}
var modes = presentModes.ToList();
var presentMode = PresentModeKHR.PresentModeFifoKhr;
if (!vsyncEnabled && modes.Contains(PresentModeKHR.PresentModeImmediateKhr))
{
presentMode = PresentModeKHR.PresentModeImmediateKhr;
return PresentModeKHR.PresentModeImmediateKhr;
}
else if (modes.Contains(PresentModeKHR.PresentModeMailboxKhr))
{
presentMode = PresentModeKHR.PresentModeMailboxKhr;
return PresentModeKHR.PresentModeMailboxKhr;
}
else if (modes.Contains(PresentModeKHR.PresentModeImmediateKhr))
else if (modes.Contains(PresentModeKHR.PresentModeFifoKhr))
{
presentMode = PresentModeKHR.PresentModeImmediateKhr;
return PresentModeKHR.PresentModeFifoKhr;
}
else
{
return PresentModeKHR.PresentModeImmediateKhr;
}
return presentMode;
}
internal static VulkanDisplay CreateDisplay(VulkanInstance instance, VulkanDevice device,
@@ -266,6 +277,8 @@ namespace Ryujinx.Ava.Ui.Vulkan
_swapchain = CreateSwapchain(_instance, _device, _physicalDevice, _surface, out _swapchainExtent, _swapchain, _vsyncEnabled);
CreateSwapchainImages();
_surfaceChanged = true;
}
private unsafe ImageView CreateSwapchainImageView(Image swapchainImage, Format format)
@@ -306,7 +319,7 @@ namespace Ryujinx.Ava.Ui.Vulkan
return true;
}
internal VulkanCommandBufferPool.VulkanCommandBuffer StartPresentation(VulkanSurfaceRenderTarget renderTarget)
internal VulkanCommandBufferPool.VulkanCommandBuffer StartPresentation()
{
_nextImage = 0;
while (true)
@@ -346,8 +359,10 @@ namespace Ryujinx.Ava.Ui.Vulkan
internal void BlitImageToCurrentImage(VulkanSurfaceRenderTarget renderTarget, CommandBuffer commandBuffer)
{
var image = renderTarget.GetImage();
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer,
renderTarget.Image.InternalHandle.Value, (ImageLayout)renderTarget.Image.CurrentLayout,
image.InternalHandle.Value, (ImageLayout)image.CurrentLayout,
AccessFlags.AccessNoneKhr,
ImageLayout.TransferSrcOptimal,
AccessFlags.AccessTransferReadBit,
@@ -381,7 +396,7 @@ namespace Ryujinx.Ava.Ui.Vulkan
}
};
_device.Api.CmdBlitImage(commandBuffer, renderTarget.Image.InternalHandle.Value,
_device.Api.CmdBlitImage(commandBuffer, image.InternalHandle.Value,
ImageLayout.TransferSrcOptimal,
_swapchainImages[_nextImage],
ImageLayout.TransferDstOptimal,
@@ -390,9 +405,9 @@ namespace Ryujinx.Ava.Ui.Vulkan
Filter.Linear);
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer,
renderTarget.Image.InternalHandle.Value, ImageLayout.TransferSrcOptimal,
image.InternalHandle.Value, ImageLayout.TransferSrcOptimal,
AccessFlags.AccessTransferReadBit,
(ImageLayout)renderTarget.Image.CurrentLayout,
(ImageLayout)image.CurrentLayout,
AccessFlags.AccessNoneKhr,
renderTarget.MipLevels);
}
@@ -434,6 +449,8 @@ namespace Ryujinx.Ava.Ui.Vulkan
}
CommandBufferPool.FreeUsedCommandBuffers();
Presented?.Invoke(this, null);
}
}
}

View File

@@ -148,20 +148,18 @@ namespace Ryujinx.Ava.Ui.Vulkan
_currentAccessFlags = destinationAccessFlags;
}
public void TransitionLayout(uint destinationLayout, uint destinationAccessFlags)
public void Dispose()
{
TransitionLayout((ImageLayout)destinationLayout, (AccessFlags)destinationAccessFlags);
}
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);
public unsafe void Dispose()
{
_device.Api.DestroyImageView(_device.InternalHandle, _imageView.Value, null);
_device.Api.DestroyImage(_device.InternalHandle, InternalHandle.Value, null);
_device.Api.FreeMemory(_device.InternalHandle, _imageMemory, null);
_imageView = default;
InternalHandle = default;
_imageMemory = default;
_imageView = default;
InternalHandle = null;
_imageMemory = default;
}
}
}
}

View File

@@ -57,8 +57,7 @@ namespace Ryujinx.Ava.Ui.Vulkan
var applicationInfo = new ApplicationInfo
{
PApplicationName = (byte*)applicationName,
ApiVersion = new Version32((uint)options.VulkanVersion.Major, (uint)options.VulkanVersion.Minor,
(uint)options.VulkanVersion.Build),
ApiVersion = Vk.Version12.Value,
PEngineName = (byte*)engineName,
EngineVersion = new Version32(1, 0, 0),
ApplicationVersion = new Version32(1, 0, 0)

View File

@@ -11,11 +11,6 @@ namespace Ryujinx.Ava.Ui.Vulkan
/// </summary>
public string ApplicationName { get; set; }
/// <summary>
/// Specifies the Vulkan API version to use
/// </summary>
public Version VulkanVersion { get; set; } = new Version(1, 1, 0);
/// <summary>
/// Specifies additional extensions to enable if available on the instance
/// </summary>

View File

@@ -18,13 +18,11 @@ namespace Ryujinx.Ava.Ui.Vulkan
public VulkanPhysicalDevice PhysicalDevice { get; private set; }
public VulkanInstance Instance { get; }
public VulkanDevice Device { get; set; }
public Vk Api { get; private set; }
public VulkanSurfaceRenderTarget MainSurface { get; set; }
public void Dispose()
{
Device?.Dispose();
Instance?.Dispose();
Api?.Dispose();
}
@@ -54,16 +52,9 @@ namespace Ryujinx.Ava.Ui.Vulkan
{
var surface = VulkanSurface.CreateSurface(Instance, platformSurface);
if (Device == null)
if (PhysicalDevice == null)
{
PhysicalDevice = VulkanPhysicalDevice.FindSuitablePhysicalDevice(Instance, surface, _options.PreferDiscreteGpu, _options.PreferredDevice);
var device = VulkanInitialization.CreateDevice(Instance.Api,
PhysicalDevice.InternalHandle,
PhysicalDevice.QueueFamilyIndex,
VulkanInitialization.GetSupportedExtensions(Instance.Api, PhysicalDevice.InternalHandle),
PhysicalDevice.QueueCount);
Device = new VulkanDevice(device, PhysicalDevice, Instance.Api);
}
var renderTarget = new VulkanSurfaceRenderTarget(this, surface);
@@ -71,7 +62,6 @@ namespace Ryujinx.Ava.Ui.Vulkan
if (MainSurface == null && surface != null)
{
MainSurface = renderTarget;
MainSurface.Display.ChangeVSyncMode(false);
}
return renderTarget;

View File

@@ -9,7 +9,6 @@ namespace Ryujinx.Ava.Ui.Vulkan
{
private readonly VulkanDevice _device;
private readonly VulkanSurfaceRenderTarget _renderTarget;
private VulkanCommandBufferPool.VulkanCommandBuffer _commandBuffer;
public VulkanSurfaceRenderingSession(VulkanDisplay display, VulkanDevice device,
VulkanSurfaceRenderTarget renderTarget, float scaling)
@@ -32,17 +31,13 @@ namespace Ryujinx.Ava.Ui.Vulkan
{
if (!Display.EnsureSwapchainAvailable())
{
_renderTarget.Invalidate();
_renderTarget.RecreateImage();
}
}
public void Dispose()
{
_commandBuffer = Display.StartPresentation(_renderTarget);
Display.BlitImageToCurrentImage(_renderTarget, _commandBuffer.InternalHandle);
Display.EndPresentation(_commandBuffer);
_renderTarget.EndDraw();
}
}
}

View File

@@ -37,7 +37,7 @@ namespace Ryujinx.Ava.Ui.Controls
public override void DestroyBackgroundContext()
{
_image = null;
Image = null;
if (_fence != IntPtr.Zero)
{
@@ -57,6 +57,8 @@ namespace Ryujinx.Ava.Ui.Controls
Dispatcher.UIThread.InvokeAsync(() =>
{
Image = (int)image;
InvalidateVisual();
}).Wait();
if (_fence != IntPtr.Zero)
@@ -66,7 +68,7 @@ namespace Ryujinx.Ava.Ui.Controls
_fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
QueueRender();
InvalidateVisual();
_gameBackgroundWindow.SwapBuffers();
}

View File

@@ -11,25 +11,7 @@ namespace Ryujinx.Ava.Ui.Controls
{
internal abstract class RendererControl : Control
{
protected object _image;
static RendererControl()
{
AffectsRender<RendererControl>(ImageProperty);
}
public readonly static StyledProperty<object> ImageProperty =
AvaloniaProperty.Register<RendererControl, object>(
nameof(Image),
0,
inherits: true,
defaultBindingMode: BindingMode.TwoWay);
protected object Image
{
get => _image;
set => SetAndRaise(ImageProperty, ref _image, value);
}
protected object Image { get; set; }
public event EventHandler<EventArgs> RendererInitialized;
public event EventHandler<Size> SizeChanged;
@@ -60,8 +42,6 @@ namespace Ryujinx.Ava.Ui.Controls
if (!rect.IsEmpty)
{
RenderSize = rect.Size * VisualRoot.RenderScaling;
DrawOperation?.Dispose();
DrawOperation = CreateDrawOperation();
}
}
@@ -97,17 +77,11 @@ namespace Ryujinx.Ava.Ui.Controls
RendererInitialized?.Invoke(this, EventArgs.Empty);
}
public void QueueRender()
{
Program.RenderTimer.TickNow();
}
internal abstract void Present(object image);
internal void Start()
{
IsStarted = true;
QueueRender();
}
internal void Stop()

View File

@@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
@@ -11,20 +12,23 @@ using Silk.NET.Vulkan;
using SkiaSharp;
using SPB.Windowing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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()
@@ -37,6 +41,40 @@ namespace Ryujinx.Ava.Ui.Controls
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()
{
}
@@ -51,12 +89,29 @@ namespace Ryujinx.Ava.Ui.Controls
internal override void Present(object image)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
Image = image;
}).Wait();
Image = image;
QueueRender();
_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
@@ -64,6 +119,7 @@ namespace Ryujinx.Ava.Ui.Controls
public Rect Bounds { get; }
private readonly VulkanRendererControl _control;
private bool _isDestroyed;
public VulkanDrawOperation(VulkanRendererControl control)
{
@@ -73,7 +129,12 @@ namespace Ryujinx.Ava.Ui.Controls
public void Dispose()
{
if (_isDestroyed)
{
return;
}
_isDestroyed = true;
}
public bool Equals(ICustomDrawOperation other)
@@ -86,30 +147,33 @@ namespace Ryujinx.Ava.Ui.Controls
return Bounds.Contains(p);
}
public void Render(IDrawingContextImpl context)
public unsafe void Render(IDrawingContextImpl context)
{
if (_control.Image == null || _control.RenderSize.Width == 0 || _control.RenderSize.Height == 0)
if (_isDestroyed || _control.Image == null || _control.RenderSize.Width == 0 || _control.RenderSize.Height == 0 ||
context is not ISkiaDrawingContextImpl skiaDrawingContextImpl)
{
return;
}
var image = (PresentImageInfo)_control.Image;
var image = _control.GetImage();
if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl)
if (!image.State.IsValid)
{
_control._currentImage = null;
return;
}
_control._platformInterface.Device.QueueWaitIdle();
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.ColorAttachmentOptimal,
ImageLayout = (uint)ImageLayout.TransferSrcOptimal,
ImageTiling = (uint)ImageTiling.Optimal,
ImageUsageFlags = (uint)(ImageUsageFlags.ImageUsageColorAttachmentBit
| ImageUsageFlags.ImageUsageTransferSrcBit
@@ -127,13 +191,15 @@ namespace Ryujinx.Ava.Ui.Controls
};
using var backendTexture = new GRBackendRenderTarget(
(int)_control.RenderSize.Width,
(int)_control.RenderSize.Height,
(int)image.Extent.Width,
(int)image.Extent.Height,
1,
imageInfo);
var vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
using var surface = SKSurface.Create(
gpu.GrContext,
skiaDrawingContextImpl.GrContext,
backendTexture,
GRSurfaceOrigin.TopLeft,
SKColorType.Rgba8888);
@@ -143,10 +209,11 @@ namespace Ryujinx.Ava.Ui.Controls
return;
}
var rect = new Rect(new Point(), _control.RenderSize);
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());
skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(),
new SKPaint());
}
}
}

View File

@@ -38,6 +38,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
{
internal class MainWindowViewModel : BaseModel
{
private const int HotKeyPressDelayMs = 500;
private readonly MainWindow _owner;
private ObservableCollection<ApplicationData> _applications;
private string _aspectStatusText;
@@ -54,6 +56,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
private bool _isLoading;
private int _progressMaximum;
private int _progressValue;
private long _lastFullscreenToggle = Environment.TickCount64;
private bool _showLoadProgress;
private bool _showMenuAndStatusBar = true;
private bool _showStatusSeparator;
@@ -929,6 +932,13 @@ namespace Ryujinx.Ava.Ui.ViewModels
public void ToggleFullscreen()
{
if (Environment.TickCount64 - _lastFullscreenToggle < HotKeyPressDelayMs)
{
return;
}
_lastFullscreenToggle = Environment.TickCount64;
WindowState state = _owner.WindowState;
if (state == WindowState.FullScreen)
@@ -1085,6 +1095,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
{
selection.Favorite = !selection.Favorite;
_owner.ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata =>
{
appMetadata.Favorite = selection.Favorite;
});
RefreshView();
}
}

View File

@@ -48,6 +48,10 @@ namespace Ryujinx.Ava.Ui.ViewModels
private int _graphicsBackendMultithreadingIndex;
private float _previousVolumeLevel;
private float _volume;
private bool _isVulkanAvailable = true;
private List<string> _gpuIds = new List<string>();
private KeyboardHotkeys _keyboardHotkeys;
private int _graphicsBackendIndex;
public int ResolutionScale
{
@@ -97,6 +101,17 @@ namespace Ryujinx.Ava.Ui.ViewModels
}
}
public bool IsVulkanAvailable
{
get => _isVulkanAvailable;
set
{
_isVulkanAvailable = value;
OnPropertyChanged();
}
}
public bool EnableDiscordIntegration { get; set; }
public bool CheckUpdatesOnStart { get; set; }
public bool ShowConfirmExit { get; set; }
@@ -143,10 +158,10 @@ namespace Ryujinx.Ava.Ui.ViewModels
public int BaseStyleIndex { get; set; }
public int GraphicsBackendIndex
{
get => graphicsBackendIndex;
get => _graphicsBackendIndex;
set
{
graphicsBackendIndex = value;
_graphicsBackendIndex = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsVulkanSelected));
}
@@ -170,14 +185,9 @@ namespace Ryujinx.Ava.Ui.ViewModels
public DateTimeOffset DateOffset { get; set; }
public TimeSpan TimeOffset { get; set; }
public AvaloniaList<TimeZone> TimeZones { get; set; }
public AvaloniaList<string> GameDirectories { get; set; }
public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; }
private KeyboardHotkeys _keyboardHotkeys;
private int graphicsBackendIndex;
private List<string> _gpuIds = new List<string>();
public KeyboardHotkeys KeyboardHotkeys
{
get => _keyboardHotkeys;
@@ -233,20 +243,31 @@ namespace Ryujinx.Ava.Ui.ViewModels
if (!Program.UseVulkan)
{
var devices = VulkanRenderer.GetPhysicalDevices();
foreach (var device in devices)
if (devices.Length == 0)
{
_gpuIds.Add(device.Id);
names.Add($"{device.Name} {(device.IsDiscrete ? "(dGpu)" : "")}");
IsVulkanAvailable = false;
GraphicsBackendIndex = 1;
}
else
{
foreach (var device in devices)
{
_gpuIds.Add(device.Id);
names.Add($"{device.Name} {(device.IsDiscrete ? "(dGPU)" : "")}");
}
}
}
else
{
foreach (var device in VulkanPhysicalDevice.SuitableDevices)
{
_gpuIds.Add(VulkanInitialization.StringFromIdPair(device.Value.VendorID, device.Value.DeviceID));
_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)" : "")}");
names.Add(
$"{Marshal.PtrToStringAnsi((IntPtr)name)} {(device.Value.DeviceType == PhysicalDeviceType.DiscreteGpu ? "(dGPU)" : "")}");
}
}

View File

@@ -656,7 +656,12 @@ namespace Ryujinx.Ava.Ui.Windows
{
AppHost = null;
Dispatcher.UIThread.Post(Close);
Dispatcher.UIThread.Post(() =>
{
MainContent = null;
Close();
});
};
AppHost?.Stop();

View File

@@ -519,7 +519,7 @@
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
SelectedIndex="{Binding GraphicsBackendIndex}">
<ComboBoxItem>
<ComboBoxItem IsVisible="{Binding IsVulkanAvailable}">
<TextBlock Text="Vulkan" />
</ComboBoxItem>
<ComboBoxItem>