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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input; using Ryujinx.Ava.Input;
using Ryujinx.Ava.Ui.Backend.Vulkan;
using Ryujinx.Ava.Ui.Controls; using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Models; using Ryujinx.Ava.Ui.Models;
using Ryujinx.Ava.Ui.Vulkan; using Ryujinx.Ava.Ui.Vulkan;
@ -334,6 +335,8 @@ namespace Ryujinx.Ava
return; return;
} }
AvaloniaLocator.Current.GetService<VulkanPlatformInterface>()?.MainSurface.Display.ChangeVSyncMode(true);
_isStopped = true; _isStopped = true;
_isActive = false; _isActive = false;
} }
@ -596,12 +599,13 @@ namespace Ryujinx.Ava
if (Program.UseVulkan) if (Program.UseVulkan)
{ {
var vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>(); var vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
renderer = new VulkanRenderer(vulkan.Instance.InternalHandle, renderer = new VulkanRenderer(vulkan.Instance.InternalHandle,
vulkan.Device.InternalHandle, vulkan.MainSurface.Device.InternalHandle,
vulkan.PhysicalDevice.InternalHandle, vulkan.PhysicalDevice.InternalHandle,
vulkan.Device.Queue.InternalHandle, vulkan.MainSurface.Device.Queue.InternalHandle,
vulkan.PhysicalDevice.QueueFamilyIndex, vulkan.PhysicalDevice.QueueFamilyIndex,
vulkan.Device.Lock); vulkan.MainSurface.Device.Lock);
} }
else else
{ {
@ -775,7 +779,10 @@ namespace Ryujinx.Ava
Width = (int)e.Width; Width = (int)e.Width;
Height = (int)e.Height; Height = (int)e.Height;
SetRendererWindowSize(e); if (!Program.UseVulkan)
{
SetRendererWindowSize(e);
}
} }
private void MainLoop() private void MainLoop()
@ -815,12 +822,11 @@ namespace Ryujinx.Ava
_renderer.ScreenCaptured += Renderer_ScreenCaptured; _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); Device.Gpu.Renderer.Initialize(_glLogLevel);
@ -837,8 +843,6 @@ namespace Ryujinx.Ava
Renderer.Start(); Renderer.Start();
Renderer.QueueRender();
while (_isActive) while (_isActive)
{ {
if (Device.WaitFifo()) if (Device.WaitFifo())
@ -889,6 +893,16 @@ namespace Ryujinx.Ava
$"FIFO: {Device.Statistics.GetFifoPercent():00.00} %", $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %",
$"GPU: {_renderer.GetHardwareInfo().GpuVendor}")); $"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); Renderer.Present(image);
} }
@ -970,6 +984,9 @@ namespace Ryujinx.Ava
{ {
case KeyboardHotkeyState.ToggleVSync: case KeyboardHotkeyState.ToggleVSync:
Device.EnableDeviceVsync = !Device.EnableDeviceVsync; Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
AvaloniaLocator.Current.GetService<VulkanPlatformInterface>()?.MainSurface?.Display?.ChangeVSyncMode(Device.EnableDeviceVsync);
break; break;
case KeyboardHotkeyState.Screenshot: case KeyboardHotkeyState.Screenshot:
ScreenshotRequested = true; ScreenshotRequested = true;

View file

@ -94,7 +94,6 @@ namespace Ryujinx.Ava
.With(new Ui.Vulkan.VulkanOptions() .With(new Ui.Vulkan.VulkanOptions()
{ {
ApplicationName = "Ryujinx.Graphics.Vulkan", ApplicationName = "Ryujinx.Graphics.Vulkan",
VulkanVersion = new Version(1, 2),
MaxQueueCount = 2, MaxQueueCount = 2,
PreferDiscreteGpu = true, PreferDiscreteGpu = true,
PreferredDevice = !PreviewerDetached ? "" : ConfigurationState.Instance.Graphics.PreferredGpu.Value, PreferredDevice = !PreviewerDetached ? "" : ConfigurationState.Instance.Graphics.PreferredGpu.Value,
@ -181,6 +180,18 @@ namespace Ryujinx.Ava
UseVulkan = PreviewerDetached ? ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan : false; 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) 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, // 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) 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}\"."); throw new Exception($"Unexpected API error \"{result}\".");
} }

View file

@ -1,26 +1,90 @@
using System; using System;
using Avalonia;
using Avalonia.Skia; using Avalonia.Skia;
using Ryujinx.Ava.Ui.Vulkan; using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Ava.Ui.Vulkan.Surfaces; using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Silk.NET.Vulkan;
using SkiaSharp; using SkiaSharp;
namespace Ryujinx.Ava.Ui.Backend.Vulkan namespace Ryujinx.Ava.Ui.Backend.Vulkan
{ {
internal class VulkanRenderTarget : ISkiaGpuRenderTarget internal class VulkanRenderTarget : ISkiaGpuRenderTarget
{ {
public GRContext GrContext { get; set; } public GRContext GrContext { get; private set; }
private readonly VulkanSurfaceRenderTarget _surface; private readonly VulkanSurfaceRenderTarget _surface;
private readonly VulkanPlatformInterface _vulkanPlatformInterface;
private readonly IVulkanPlatformSurface _vulkanPlatformSurface; private readonly IVulkanPlatformSurface _vulkanPlatformSurface;
private GRVkBackendContext _grVkBackend;
public VulkanRenderTarget(VulkanPlatformInterface vulkanPlatformInterface, IVulkanPlatformSurface vulkanPlatformSurface) public VulkanRenderTarget(VulkanPlatformInterface vulkanPlatformInterface, IVulkanPlatformSurface vulkanPlatformSurface)
{ {
_surface = vulkanPlatformInterface.CreateRenderTarget(vulkanPlatformSurface); _surface = vulkanPlatformInterface.CreateRenderTarget(vulkanPlatformSurface);
_vulkanPlatformInterface = vulkanPlatformInterface;
_vulkanPlatformSurface = vulkanPlatformSurface; _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() public void Dispose()
{ {
_grVkBackend.Dispose();
GrContext.Dispose();
_surface.Dispose(); _surface.Dispose();
} }
@ -45,20 +109,22 @@ namespace Ryujinx.Ava.Ui.Backend.Vulkan
{ {
GrContext.ResetContext(); GrContext.ResetContext();
var image = _surface.GetImage();
var imageInfo = new GRVkImageInfo() var imageInfo = new GRVkImageInfo()
{ {
CurrentQueueFamily = disp.QueueFamilyIndex, CurrentQueueFamily = disp.QueueFamilyIndex,
Format = _surface.ImageFormat, Format = (uint)image.Format,
Image = _surface.Image.Handle, Image = image.Handle,
ImageLayout = (uint)_surface.Image.CurrentLayout, ImageLayout = (uint)image.CurrentLayout,
ImageTiling = (uint)_surface.Image.Tiling, ImageTiling = (uint)image.Tiling,
ImageUsageFlags = _surface.UsageFlags, ImageUsageFlags = _surface.UsageFlags,
LevelCount = _surface.MipLevels, LevelCount = _surface.MipLevels,
SampleCount = 1, SampleCount = 1,
Protected = false, Protected = false,
Alloc = new GRVkAlloc() Alloc = new GRVkAlloc()
{ {
Memory = _surface.Image.MemoryHandle, Memory = image.MemoryHandle,
Flags = 0, Flags = 0,
Offset = 0, Offset = 0,
Size = _surface.MemorySize Size = _surface.MemorySize

View file

@ -13,71 +13,12 @@ namespace Ryujinx.Ava.Ui.Backend.Vulkan
public class VulkanSkiaGpu : ISkiaGpu public class VulkanSkiaGpu : ISkiaGpu
{ {
private readonly VulkanPlatformInterface _vulkan; private readonly VulkanPlatformInterface _vulkan;
private readonly long? _maxResourceBytes; public long? MaxResourceBytes { get; }
private GRVkBackendContext _grVkBackend;
private bool _initialized;
public GRContext GrContext { get; private set; }
public VulkanSkiaGpu(long? maxResourceBytes) public VulkanSkiaGpu(long? maxResourceBytes)
{ {
_vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>(); _vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
_maxResourceBytes = maxResourceBytes; 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);
}
} }
public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces) public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces)
@ -106,10 +47,6 @@ namespace Ryujinx.Ava.Ui.Backend.Vulkan
VulkanRenderTarget vulkanRenderTarget = new VulkanRenderTarget(_vulkan, window); VulkanRenderTarget vulkanRenderTarget = new VulkanRenderTarget(_vulkan, window);
Initialize();
vulkanRenderTarget.GrContext = GrContext;
return vulkanRenderTarget; return vulkanRenderTarget;
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using Avalonia; using Avalonia;
using Ryujinx.Graphics.Vulkan;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan.Surfaces namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
@ -7,24 +8,35 @@ namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
internal class VulkanSurfaceRenderTarget : IDisposable internal class VulkanSurfaceRenderTarget : IDisposable
{ {
private readonly VulkanPlatformInterface _platformInterface; private readonly VulkanPlatformInterface _platformInterface;
private readonly Format _format; private readonly Format _format;
public VulkanImage Image { get; private set; } private VulkanCommandBufferPool.VulkanCommandBuffer _commandBuffer;
public bool IsCorrupted { get; private set; } = true; private VulkanImage Image { get; set; }
private object _lock = new object();
public uint MipLevels => Image.MipLevels; public uint MipLevels => Image.MipLevels;
public VulkanDevice Device { get; }
public VulkanSurfaceRenderTarget(VulkanPlatformInterface platformInterface, VulkanSurface surface) public VulkanSurfaceRenderTarget(VulkanPlatformInterface platformInterface, VulkanSurface surface)
{ {
_platformInterface = platformInterface; _platformInterface = platformInterface;
Display = VulkanDisplay.CreateDisplay(platformInterface.Instance, platformInterface.Device, var device = VulkanInitialization.CreateDevice(platformInterface.Api,
platformInterface.PhysicalDevice, surface); 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; Surface = surface;
// Skia seems to only create surfaces from images with unorm format // Skia seems to only create surfaces from images with unorm format
IsRgba = Display.SurfaceFormat.Format >= Format.R8G8B8A8Unorm && IsRgba = Display.SurfaceFormat.Format >= Format.R8G8B8A8Unorm &&
Display.SurfaceFormat.Format <= Format.R8G8B8A8Srgb; Display.SurfaceFormat.Format <= Format.R8G8B8A8Srgb;
@ -33,13 +45,13 @@ namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
public bool IsRgba { get; } public bool IsRgba { get; }
public uint ImageFormat => (uint) _format; public uint ImageFormat => (uint)_format;
public ulong MemorySize => Image.MemorySize; 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; public uint UsageFlags => Image.UsageFlags;
@ -47,46 +59,76 @@ namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
public void Dispose() public void Dispose()
{ {
_platformInterface.Device.WaitIdle(); lock (_lock)
DestroyImage(); {
Display?.Dispose(); DestroyImage();
Surface?.Dispose(); Display?.Dispose();
Surface?.Dispose();
Device?.Dispose();
Display = null;
Surface = null;
}
} }
public VulkanSurfaceRenderingSession BeginDraw(float scaling) public VulkanSurfaceRenderingSession BeginDraw(float scaling)
{ {
var session = new VulkanSurfaceRenderingSession(Display, _platformInterface.Device, this, scaling); if (Image == null)
{
RecreateImage();
}
if (IsCorrupted) _commandBuffer?.WaitForFence();
{ _commandBuffer = null;
IsCorrupted = false;
DestroyImage(); var session = new VulkanSurfaceRenderingSession(Display, Device, this, scaling);
CreateImage();
} Image.TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
else
{
Image.TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
}
return session; return session;
} }
public void Invalidate() public void RecreateImage()
{ {
IsCorrupted = true; DestroyImage();
CreateImage();
} }
private void CreateImage() private void CreateImage()
{ {
Size = Display.Size; 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() private void DestroyImage()
{ {
_platformInterface.Device.WaitIdle(); _commandBuffer?.WaitForFence();
_commandBuffer = null;
Image?.Dispose(); 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 CommandPool _commandPool;
private readonly List<VulkanCommandBuffer> _usedCommandBuffers = new(); private readonly List<VulkanCommandBuffer> _usedCommandBuffers = new();
private readonly object _lock = new object();
public unsafe VulkanCommandBufferPool(VulkanDevice device, VulkanPhysicalDevice physicalDevice) public unsafe VulkanCommandBufferPool(VulkanDevice device, VulkanPhysicalDevice physicalDevice)
{ {
@ -36,9 +37,12 @@ namespace Ryujinx.Ava.Ui.Vulkan
Level = CommandBufferLevel.Primary 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() public VulkanCommandBuffer CreateCommandBuffer()
@ -48,7 +52,7 @@ namespace Ryujinx.Ava.Ui.Vulkan
public void FreeUsedCommandBuffers() public void FreeUsedCommandBuffers()
{ {
lock (_usedCommandBuffers) lock (_lock)
{ {
foreach (var usedCommandBuffer in _usedCommandBuffers) foreach (var usedCommandBuffer in _usedCommandBuffers)
{ {
@ -61,7 +65,7 @@ namespace Ryujinx.Ava.Ui.Vulkan
private void DisposeCommandBuffer(VulkanCommandBuffer commandBuffer) private void DisposeCommandBuffer(VulkanCommandBuffer commandBuffer)
{ {
lock (_usedCommandBuffers) lock (_lock)
{ {
_usedCommandBuffers.Add(commandBuffer); _usedCommandBuffers.Add(commandBuffer);
} }
@ -69,8 +73,11 @@ namespace Ryujinx.Ava.Ui.Vulkan
public void Dispose() public void Dispose()
{ {
FreeUsedCommandBuffers(); lock (_lock)
_device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, Span<AllocationCallbacks>.Empty); {
FreeUsedCommandBuffers();
_device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, Span<AllocationCallbacks>.Empty);
}
} }
public class VulkanCommandBuffer : IDisposable public class VulkanCommandBuffer : IDisposable
@ -80,6 +87,8 @@ namespace Ryujinx.Ava.Ui.Vulkan
private readonly Fence _fence; private readonly Fence _fence;
private bool _hasEnded; private bool _hasEnded;
private bool _hasStarted; private bool _hasStarted;
private bool _isDisposed;
private object _lock = new object();
public IntPtr Handle => InternalHandle.Handle; public IntPtr Handle => InternalHandle.Handle;
@ -101,6 +110,22 @@ namespace Ryujinx.Ava.Ui.Vulkan
device.Api.CreateFence(device.InternalHandle, fenceCreateInfo, null, out _fence); 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() public void BeginRecording()
{ {
if (!_hasStarted) if (!_hasStarted)
@ -173,9 +198,17 @@ namespace Ryujinx.Ava.Ui.Vulkan
public void Dispose() public void Dispose()
{ {
_device.Api.WaitForFences(_device.InternalHandle, 1, _fence, true, ulong.MaxValue); lock (_lock)
_device.Api.FreeCommandBuffers(_device.InternalHandle, _commandBufferPool._commandPool, 1, InternalHandle); {
_device.Api.DestroyFence(_device.InternalHandle, _fence, Span<AllocationCallbacks>.Empty); 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); api.GetDeviceQueue(apiHandle, physicalDevice.QueueFamilyIndex, 0, out var queue);
var vulkanQueue = new VulkanQueue(this, queue); Queue = new VulkanQueue(this, queue);
Queue = vulkanQueue;
PresentQueue = vulkanQueue; PresentQueue = Queue;
CommandBufferPool = new VulkanCommandBufferPool(this, physicalDevice);
} }
public IntPtr Handle => InternalHandle.Handle; public IntPtr Handle => InternalHandle.Handle;
@ -29,13 +26,12 @@ namespace Ryujinx.Ava.Ui.Vulkan
public VulkanQueue Queue { get; private set; } public VulkanQueue Queue { get; private set; }
public VulkanQueue PresentQueue { get; } public VulkanQueue PresentQueue { get; }
public VulkanCommandBufferPool CommandBufferPool { get; }
public void Dispose() public void Dispose()
{ {
WaitIdle(); WaitIdle();
CommandBufferPool?.Dispose();
Queue = null; Queue = null;
Api.DestroyDevice(InternalHandle, Span<AllocationCallbacks>.Empty);
} }
internal void Submit(SubmitInfo submitInfo, Fence fence = default) internal void Submit(SubmitInfo submitInfo, Fence fence = default)

View file

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

View file

@ -148,20 +148,18 @@ namespace Ryujinx.Ava.Ui.Vulkan
_currentAccessFlags = destinationAccessFlags; _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() _imageView = default;
{ InternalHandle = null;
_device.Api.DestroyImageView(_device.InternalHandle, _imageView.Value, null); _imageMemory = default;
_device.Api.DestroyImage(_device.InternalHandle, InternalHandle.Value, null); }
_device.Api.FreeMemory(_device.InternalHandle, _imageMemory, null);
_imageView = default;
InternalHandle = default;
_imageMemory = default;
} }
} }
} }

View file

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

View file

@ -11,11 +11,6 @@ namespace Ryujinx.Ava.Ui.Vulkan
/// </summary> /// </summary>
public string ApplicationName { get; set; } 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> /// <summary>
/// Specifies additional extensions to enable if available on the instance /// Specifies additional extensions to enable if available on the instance
/// </summary> /// </summary>

View file

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

View file

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

View file

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

View file

@ -11,25 +11,7 @@ namespace Ryujinx.Ava.Ui.Controls
{ {
internal abstract class RendererControl : Control internal abstract class RendererControl : Control
{ {
protected object _image; protected object Image { get; set; }
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);
}
public event EventHandler<EventArgs> RendererInitialized; public event EventHandler<EventArgs> RendererInitialized;
public event EventHandler<Size> SizeChanged; public event EventHandler<Size> SizeChanged;
@ -60,8 +42,6 @@ namespace Ryujinx.Ava.Ui.Controls
if (!rect.IsEmpty) if (!rect.IsEmpty)
{ {
RenderSize = rect.Size * VisualRoot.RenderScaling; RenderSize = rect.Size * VisualRoot.RenderScaling;
DrawOperation?.Dispose();
DrawOperation = CreateDrawOperation(); DrawOperation = CreateDrawOperation();
} }
} }
@ -97,17 +77,11 @@ namespace Ryujinx.Ava.Ui.Controls
RendererInitialized?.Invoke(this, EventArgs.Empty); RendererInitialized?.Invoke(this, EventArgs.Empty);
} }
public void QueueRender()
{
Program.RenderTimer.TickNow();
}
internal abstract void Present(object image); internal abstract void Present(object image);
internal void Start() internal void Start()
{ {
IsStarted = true; IsStarted = true;
QueueRender();
} }
internal void Stop() internal void Stop()

View file

@ -1,4 +1,5 @@
using Avalonia; using Avalonia;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia; using Avalonia.Skia;
@ -11,20 +12,23 @@ using Silk.NET.Vulkan;
using SkiaSharp; using SkiaSharp;
using SPB.Windowing; using SPB.Windowing;
using System; using System;
using System.Collections.Generic; using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Ui.Controls namespace Ryujinx.Ava.Ui.Controls
{ {
internal class VulkanRendererControl : RendererControl internal class VulkanRendererControl : RendererControl
{ {
private const int MaxImagesInFlight = 3;
private VulkanPlatformInterface _platformInterface; private VulkanPlatformInterface _platformInterface;
private ConcurrentQueue<PresentImageInfo> _imagesInFlight;
private PresentImageInfo _currentImage;
public VulkanRendererControl(GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel) public VulkanRendererControl(GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel)
{ {
_platformInterface = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>(); _platformInterface = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
_imagesInFlight = new ConcurrentQueue<PresentImageInfo>();
} }
public override void DestroyBackgroundContext() public override void DestroyBackgroundContext()
@ -37,6 +41,40 @@ namespace Ryujinx.Ava.Ui.Controls
return new VulkanDrawOperation(this); 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() protected override void CreateWindow()
{ {
} }
@ -51,12 +89,29 @@ namespace Ryujinx.Ava.Ui.Controls
internal override void Present(object image) internal override void Present(object image)
{ {
Dispatcher.UIThread.InvokeAsync(() => Image = image;
{
Image = image;
}).Wait();
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 private class VulkanDrawOperation : ICustomDrawOperation
@ -64,6 +119,7 @@ namespace Ryujinx.Ava.Ui.Controls
public Rect Bounds { get; } public Rect Bounds { get; }
private readonly VulkanRendererControl _control; private readonly VulkanRendererControl _control;
private bool _isDestroyed;
public VulkanDrawOperation(VulkanRendererControl control) public VulkanDrawOperation(VulkanRendererControl control)
{ {
@ -73,7 +129,12 @@ namespace Ryujinx.Ava.Ui.Controls
public void Dispose() public void Dispose()
{ {
if (_isDestroyed)
{
return;
}
_isDestroyed = true;
} }
public bool Equals(ICustomDrawOperation other) public bool Equals(ICustomDrawOperation other)
@ -86,30 +147,33 @@ namespace Ryujinx.Ava.Ui.Controls
return Bounds.Contains(p); 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; return;
} }
var image = (PresentImageInfo)_control.Image; var image = _control.GetImage();
if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl) if (!image.State.IsValid)
{ {
_control._currentImage = null;
return; return;
} }
_control._platformInterface.Device.QueueWaitIdle();
var gpu = AvaloniaLocator.Current.GetService<VulkanSkiaGpu>(); var gpu = AvaloniaLocator.Current.GetService<VulkanSkiaGpu>();
image.Get();
var imageInfo = new GRVkImageInfo() var imageInfo = new GRVkImageInfo()
{ {
CurrentQueueFamily = _control._platformInterface.PhysicalDevice.QueueFamilyIndex, CurrentQueueFamily = _control._platformInterface.PhysicalDevice.QueueFamilyIndex,
Format = (uint)Format.R8G8B8A8Unorm, Format = (uint)Format.R8G8B8A8Unorm,
Image = image.Image.Handle, Image = image.Image.Handle,
ImageLayout = (uint)ImageLayout.ColorAttachmentOptimal, ImageLayout = (uint)ImageLayout.TransferSrcOptimal,
ImageTiling = (uint)ImageTiling.Optimal, ImageTiling = (uint)ImageTiling.Optimal,
ImageUsageFlags = (uint)(ImageUsageFlags.ImageUsageColorAttachmentBit ImageUsageFlags = (uint)(ImageUsageFlags.ImageUsageColorAttachmentBit
| ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageTransferSrcBit
@ -127,13 +191,15 @@ namespace Ryujinx.Ava.Ui.Controls
}; };
using var backendTexture = new GRBackendRenderTarget( using var backendTexture = new GRBackendRenderTarget(
(int)_control.RenderSize.Width, (int)image.Extent.Width,
(int)_control.RenderSize.Height, (int)image.Extent.Height,
1, 1,
imageInfo); imageInfo);
var vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
using var surface = SKSurface.Create( using var surface = SKSurface.Create(
gpu.GrContext, skiaDrawingContextImpl.GrContext,
backendTexture, backendTexture,
GRSurfaceOrigin.TopLeft, GRSurfaceOrigin.TopLeft,
SKColorType.Rgba8888); SKColorType.Rgba8888);
@ -143,10 +209,11 @@ namespace Ryujinx.Ava.Ui.Controls
return; 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(); 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 internal class MainWindowViewModel : BaseModel
{ {
private const int HotKeyPressDelayMs = 500;
private readonly MainWindow _owner; private readonly MainWindow _owner;
private ObservableCollection<ApplicationData> _applications; private ObservableCollection<ApplicationData> _applications;
private string _aspectStatusText; private string _aspectStatusText;
@ -54,6 +56,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
private bool _isLoading; private bool _isLoading;
private int _progressMaximum; private int _progressMaximum;
private int _progressValue; private int _progressValue;
private long _lastFullscreenToggle = Environment.TickCount64;
private bool _showLoadProgress; private bool _showLoadProgress;
private bool _showMenuAndStatusBar = true; private bool _showMenuAndStatusBar = true;
private bool _showStatusSeparator; private bool _showStatusSeparator;
@ -929,6 +932,13 @@ namespace Ryujinx.Ava.Ui.ViewModels
public void ToggleFullscreen() public void ToggleFullscreen()
{ {
if (Environment.TickCount64 - _lastFullscreenToggle < HotKeyPressDelayMs)
{
return;
}
_lastFullscreenToggle = Environment.TickCount64;
WindowState state = _owner.WindowState; WindowState state = _owner.WindowState;
if (state == WindowState.FullScreen) if (state == WindowState.FullScreen)
@ -1085,6 +1095,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
{ {
selection.Favorite = !selection.Favorite; selection.Favorite = !selection.Favorite;
_owner.ApplicationLibrary.LoadAndSaveMetaData(selection.TitleId, appMetadata =>
{
appMetadata.Favorite = selection.Favorite;
});
RefreshView(); RefreshView();
} }
} }

View file

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

View file

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

View file

@ -7,5 +7,7 @@ namespace Ryujinx.Graphics.GAL
void Present(ITexture texture, ImageCrop crop, Action<object> swapBuffersCallback); void Present(ITexture texture, ImageCrop crop, Action<object> swapBuffersCallback);
void SetSize(int width, int height); void SetSize(int width, int height);
void ChangeVSyncMode(bool vsyncEnabled);
} }
} }

View file

@ -30,5 +30,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
{ {
_impl.Window.SetSize(width, height); _impl.Window.SetSize(width, height);
} }
public void ChangeVSyncMode(bool vsyncEnabled) { }
} }
} }

View file

@ -58,6 +58,8 @@ namespace Ryujinx.Graphics.OpenGL
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4); GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4);
} }
public void ChangeVSyncMode(bool vsyncEnabled) { }
private void CreateStagingFramebuffer() private void CreateStagingFramebuffer()
{ {
_stagingFrameBuffer = GL.GenFramebuffer(); _stagingFrameBuffer = GL.GenFramebuffer();

View file

@ -7,7 +7,9 @@ namespace Ryujinx.Graphics.Vulkan
{ {
class ImageWindow : WindowBase, IWindow, IDisposable class ImageWindow : WindowBase, IWindow, IDisposable
{ {
private const int ImageCount = 5; internal const VkFormat Format = VkFormat.R8G8B8A8Unorm;
private const int ImageCount = 3;
private const int SurfaceWidth = 1280; private const int SurfaceWidth = 1280;
private const int SurfaceHeight = 720; private const int SurfaceHeight = 720;
@ -18,52 +20,49 @@ namespace Ryujinx.Graphics.Vulkan
private Auto<DisposableImage>[] _images; private Auto<DisposableImage>[] _images;
private Auto<DisposableImageView>[] _imageViews; private Auto<DisposableImageView>[] _imageViews;
private Auto<MemoryAllocation>[] _imageAllocationAuto; private Auto<MemoryAllocation>[] _imageAllocationAuto;
private ImageState[] _states;
private PresentImageInfo[] _presentedImages;
private FenceHolder[] _fences;
private ulong[] _imageSizes; private ulong[] _imageSizes;
private ulong[] _imageOffsets; private ulong[] _imageOffsets;
private Semaphore _imageAvailableSemaphore;
private Semaphore _renderFinishedSemaphore;
private int _width = SurfaceWidth; private int _width = SurfaceWidth;
private int _height = SurfaceHeight; private int _height = SurfaceHeight;
private VkFormat _format;
private bool _recreateImages; private bool _recreateImages;
private int _nextImage; private int _nextImage;
internal new bool ScreenCaptureRequested { get; set; }
public unsafe ImageWindow(VulkanRenderer gd, PhysicalDevice physicalDevice, Device device) public unsafe ImageWindow(VulkanRenderer gd, PhysicalDevice physicalDevice, Device device)
{ {
_gd = gd; _gd = gd;
_physicalDevice = physicalDevice; _physicalDevice = physicalDevice;
_device = device; _device = device;
_format = VkFormat.R8G8B8A8Unorm;
_images = new Auto<DisposableImage>[ImageCount]; _images = new Auto<DisposableImage>[ImageCount];
_imageAllocationAuto = new Auto<MemoryAllocation>[ImageCount]; _imageAllocationAuto = new Auto<MemoryAllocation>[ImageCount];
_imageSizes = new ulong[ImageCount]; _imageSizes = new ulong[ImageCount];
_imageOffsets = new ulong[ImageCount]; _imageOffsets = new ulong[ImageCount];
_states = new ImageState[ImageCount];
_presentedImages = new PresentImageInfo[ImageCount];
CreateImages(); CreateImages();
var semaphoreCreateInfo = new SemaphoreCreateInfo()
{
SType = StructureType.SemaphoreCreateInfo
};
gd.Api.CreateSemaphore(device, semaphoreCreateInfo, null, out _imageAvailableSemaphore).ThrowOnError();
gd.Api.CreateSemaphore(device, semaphoreCreateInfo, null, out _renderFinishedSemaphore).ThrowOnError();
} }
private void RecreateImages() private void RecreateImages()
{ {
for (int i = 0; i < ImageCount; i++) for (int i = 0; i < ImageCount; i++)
{ {
_imageViews[i]?.Dispose(); lock (_states[i])
_imageAllocationAuto[i]?.Dispose(); {
_images[i]?.Dispose(); _states[i].IsValid = false;
_fences[i]?.Wait();
_fences[i]?.Put();
_imageViews[i]?.Dispose();
_imageAllocationAuto[i]?.Dispose();
_images[i]?.Dispose();
}
} }
_presentedImages = null;
CreateImages(); CreateImages();
} }
@ -71,34 +70,35 @@ namespace Ryujinx.Graphics.Vulkan
private unsafe void CreateImages() private unsafe void CreateImages()
{ {
_imageViews = new Auto<DisposableImageView>[ImageCount]; _imageViews = new Auto<DisposableImageView>[ImageCount];
_fences = new FenceHolder[ImageCount];
_presentedImages = new PresentImageInfo[ImageCount];
_nextImage = 0;
var cbs = _gd.CommandBufferPool.Rent(); var cbs = _gd.CommandBufferPool.Rent();
var imageCreateInfo = new ImageCreateInfo
{
SType = StructureType.ImageCreateInfo,
ImageType = ImageType.ImageType2D,
Format = Format,
Extent = new Extent3D((uint?)_width, (uint?)_height, 1),
MipLevels = 1,
ArrayLayers = 1,
Samples = SampleCountFlags.SampleCount1Bit,
Tiling = ImageTiling.Optimal,
Usage = ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageTransferDstBit,
SharingMode = SharingMode.Exclusive,
InitialLayout = ImageLayout.Undefined,
Flags = ImageCreateFlags.ImageCreateMutableFormatBit
};
for (int i = 0; i < _images.Length; i++) for (int i = 0; i < _images.Length; i++)
{ {
var imageCreateInfo = new ImageCreateInfo
{
SType = StructureType.ImageCreateInfo,
ImageType = ImageType.ImageType2D,
Format = _format,
Extent =
new Extent3D((uint?)_width,
(uint?)_height, 1),
MipLevels = 1,
ArrayLayers = 1,
Samples = SampleCountFlags.SampleCount1Bit,
Tiling = ImageTiling.Optimal,
Usage = ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageTransferDstBit,
SharingMode = SharingMode.Exclusive,
InitialLayout = ImageLayout.Undefined,
Flags = ImageCreateFlags.ImageCreateMutableFormatBit
};
_gd.Api.CreateImage(_device, imageCreateInfo, null, out var image).ThrowOnError(); _gd.Api.CreateImage(_device, imageCreateInfo, null, out var image).ThrowOnError();
_images[i] = new Auto<DisposableImage>(new DisposableImage(_gd.Api, _device, image)); _images[i] = new Auto<DisposableImage>(new DisposableImage(_gd.Api, _device, image));
_gd.Api.GetImageMemoryRequirements(_device, image, _gd.Api.GetImageMemoryRequirements(_device, image,
out var memoryRequirements); out var memoryRequirements);
var allocation = _gd.MemoryAllocator.AllocateDeviceMemory(_physicalDevice, memoryRequirements, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit); var allocation = _gd.MemoryAllocator.AllocateDeviceMemory(_physicalDevice, memoryRequirements, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit);
_imageSizes[i] = allocation.Size; _imageSizes[i] = allocation.Size;
@ -108,7 +108,7 @@ namespace Ryujinx.Graphics.Vulkan
_gd.Api.BindImageMemory(_device, image, allocation.Memory, allocation.Offset); _gd.Api.BindImageMemory(_device, image, allocation.Memory, allocation.Offset);
_imageViews[i] = CreateImageView(image, _format); _imageViews[i] = CreateImageView(image, Format);
Transition( Transition(
cbs.CommandBuffer, cbs.CommandBuffer,
@ -116,7 +116,9 @@ namespace Ryujinx.Graphics.Vulkan
0, 0,
0, 0,
ImageLayout.Undefined, ImageLayout.Undefined,
ImageLayout.ColorAttachmentOptimal); ImageLayout.TransferSrcOptimal);
_states[i] = new ImageState();
} }
_gd.CommandBufferPool.Return(cbs); _gd.CommandBufferPool.Return(cbs);
@ -165,7 +167,7 @@ namespace Ryujinx.Graphics.Vulkan
image.GetUnsafe().Value, image.GetUnsafe().Value,
0, 0,
AccessFlags.AccessTransferWriteBit, AccessFlags.AccessTransferWriteBit,
ImageLayout.ColorAttachmentOptimal, ImageLayout.TransferSrcOptimal,
ImageLayout.General); ImageLayout.General);
var view = (TextureView)texture; var view = (TextureView)texture;
@ -232,7 +234,7 @@ namespace Ryujinx.Graphics.Vulkan
_imageViews[_nextImage], _imageViews[_nextImage],
_width, _width,
_height, _height,
_format, Format,
new Extents2D(srcX0, srcY0, srcX1, srcY1), new Extents2D(srcX0, srcY0, srcX1, srcY1),
new Extents2D(dstX0, dstY1, dstX1, dstY0), new Extents2D(dstX0, dstY1, dstX1, dstY0),
true, true,
@ -244,7 +246,7 @@ namespace Ryujinx.Graphics.Vulkan
0, 0,
0, 0,
ImageLayout.General, ImageLayout.General,
ImageLayout.ColorAttachmentOptimal); ImageLayout.TransferSrcOptimal);
_gd.CommandBufferPool.Return( _gd.CommandBufferPool.Return(
cbs, cbs,
@ -252,12 +254,30 @@ namespace Ryujinx.Graphics.Vulkan
stackalloc[] { PipelineStageFlags.PipelineStageColorAttachmentOutputBit }, stackalloc[] { PipelineStageFlags.PipelineStageColorAttachmentOutputBit },
null); null);
var memory = _imageAllocationAuto[_nextImage].GetUnsafe().Memory; _fences[_nextImage]?.Put();
var presentInfo = new PresentImageInfo(image.GetUnsafe().Value, memory, _imageSizes[_nextImage], _imageOffsets[_nextImage], _renderFinishedSemaphore, _imageAvailableSemaphore); _fences[_nextImage] = cbs.GetFence();
cbs.GetFence().Get();
swapBuffersCallback(presentInfo); PresentImageInfo info = _presentedImages[_nextImage];
_nextImage %= ImageCount; if (info == null)
{
info = new PresentImageInfo(
image,
_imageAllocationAuto[_nextImage],
_device,
_physicalDevice,
_imageSizes[_nextImage],
_imageOffsets[_nextImage],
new Extent2D((uint)_width, (uint)_height),
_states[_nextImage]);
_presentedImages[_nextImage] = info;
}
swapBuffersCallback(info);
_nextImage = (_nextImage + 1) % ImageCount;
} }
private unsafe void Transition( private unsafe void Transition(
@ -320,11 +340,11 @@ namespace Ryujinx.Graphics.Vulkan
{ {
unsafe unsafe
{ {
_gd.Api.DestroySemaphore(_device, _renderFinishedSemaphore, null);
_gd.Api.DestroySemaphore(_device, _imageAvailableSemaphore, null);
for (int i = 0; i < ImageCount; i++) for (int i = 0; i < ImageCount; i++)
{ {
_states[i].IsValid = false;
_fences[i]?.Wait();
_fences[i]?.Put();
_imageViews[i]?.Dispose(); _imageViews[i]?.Dispose();
_imageAllocationAuto[i]?.Dispose(); _imageAllocationAuto[i]?.Dispose();
_images[i]?.Dispose(); _images[i]?.Dispose();
@ -337,25 +357,73 @@ namespace Ryujinx.Graphics.Vulkan
{ {
Dispose(true); Dispose(true);
} }
public override void ChangeVSyncMode(bool vsyncEnabled) { }
}
public class ImageState
{
private bool _isValid = true;
public bool IsValid
{
get => _isValid;
internal set
{
_isValid = value;
StateChanged?.Invoke(this, _isValid);
}
}
public event EventHandler<bool> StateChanged;
} }
public class PresentImageInfo public class PresentImageInfo
{ {
public Image Image { get; } private readonly Auto<DisposableImage> _image;
public DeviceMemory Memory { get; } private readonly Auto<MemoryAllocation> _memory;
public ulong MemorySize { get; set; }
public ulong MemoryOffset { get; set; }
public Semaphore ReadySemaphore { get; }
public Semaphore AvailableSemaphore { get; }
public PresentImageInfo(Image image, DeviceMemory memory, ulong memorySize, ulong memoryOffset, Semaphore readySemaphore, Semaphore availableSemaphore) public Image Image => _image.GetUnsafe().Value;
public DeviceMemory Memory => _memory.GetUnsafe().Memory;
public Device Device { get; }
public PhysicalDevice PhysicalDevice { get; }
public ulong MemorySize { get; }
public ulong MemoryOffset { get; }
public Extent2D Extent { get; }
public ImageState State { get; internal set; }
internal PresentImageInfo(
Auto<DisposableImage> image,
Auto<MemoryAllocation> memory,
Device device,
PhysicalDevice physicalDevice,
ulong memorySize,
ulong memoryOffset,
Extent2D extent2D,
ImageState state)
{ {
this.Image = image; _image = image;
this.Memory = memory; _memory = memory;
this.MemorySize = memorySize; Device = device;
this.MemoryOffset = memoryOffset; PhysicalDevice = physicalDevice;
this.ReadySemaphore = readySemaphore; MemorySize = memorySize;
this.AvailableSemaphore = availableSemaphore; MemoryOffset = memoryOffset;
Extent = extent2D;
State = state;
}
public void Get()
{
_memory.IncrementReferenceCount();
_image.IncrementReferenceCount();
}
public void Put()
{
_memory.DecrementReferenceCount();
_image.DecrementReferenceCount();
} }
} }
} }

View file

@ -25,6 +25,8 @@ namespace Ryujinx.Graphics.Vulkan
private int _width; private int _width;
private int _height; private int _height;
private bool _vsyncEnabled;
private bool _vsyncModeChanged;
private VkFormat _format; private VkFormat _format;
public unsafe Window(VulkanRenderer gd, SurfaceKHR surface, PhysicalDevice physicalDevice, Device device) public unsafe Window(VulkanRenderer gd, SurfaceKHR surface, PhysicalDevice physicalDevice, Device device)
@ -47,6 +49,8 @@ namespace Ryujinx.Graphics.Vulkan
private void RecreateSwapchain() private void RecreateSwapchain()
{ {
_vsyncModeChanged = false;
for (int i = 0; i < _swapchainImageViews.Length; i++) for (int i = 0; i < _swapchainImageViews.Length; i++)
{ {
_swapchainImageViews[i].Dispose(); _swapchainImageViews[i].Dispose();
@ -110,7 +114,7 @@ namespace Ryujinx.Graphics.Vulkan
ImageArrayLayers = 1, ImageArrayLayers = 1,
PreTransform = capabilities.CurrentTransform, PreTransform = capabilities.CurrentTransform,
CompositeAlpha = CompositeAlphaFlagsKHR.CompositeAlphaOpaqueBitKhr, CompositeAlpha = CompositeAlphaFlagsKHR.CompositeAlphaOpaqueBitKhr,
PresentMode = ChooseSwapPresentMode(presentModes), PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled),
Clipped = true, Clipped = true,
OldSwapchain = oldSwapchain OldSwapchain = oldSwapchain
}; };
@ -178,9 +182,9 @@ namespace Ryujinx.Graphics.Vulkan
return availableFormats[0]; return availableFormats[0];
} }
private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes) private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled)
{ {
if (availablePresentModes.Contains(PresentModeKHR.PresentModeImmediateKhr)) if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.PresentModeImmediateKhr))
{ {
return PresentModeKHR.PresentModeImmediateKhr; return PresentModeKHR.PresentModeImmediateKhr;
} }
@ -188,6 +192,10 @@ namespace Ryujinx.Graphics.Vulkan
{ {
return PresentModeKHR.PresentModeMailboxKhr; return PresentModeKHR.PresentModeMailboxKhr;
} }
else if (availablePresentModes.Contains(PresentModeKHR.PresentModeFifoKhr))
{
return PresentModeKHR.PresentModeFifoKhr;
}
else else
{ {
return PresentModeKHR.PresentModeFifoKhr; return PresentModeKHR.PresentModeFifoKhr;
@ -224,7 +232,8 @@ namespace Ryujinx.Graphics.Vulkan
ref nextImage); ref nextImage);
if (acquireResult == Result.ErrorOutOfDateKhr || if (acquireResult == Result.ErrorOutOfDateKhr ||
acquireResult == Result.SuboptimalKhr) acquireResult == Result.SuboptimalKhr ||
_vsyncModeChanged)
{ {
RecreateSwapchain(); RecreateSwapchain();
} }
@ -404,6 +413,12 @@ namespace Ryujinx.Graphics.Vulkan
// Not needed as we can get the size from the surface. // Not needed as we can get the size from the surface.
} }
public override void ChangeVSyncMode(bool vsyncEnabled)
{
_vsyncEnabled = vsyncEnabled;
_vsyncModeChanged = true;
}
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (disposing) if (disposing)

View file

@ -10,5 +10,6 @@ namespace Ryujinx.Graphics.Vulkan
public abstract void Dispose(); public abstract void Dispose();
public abstract void Present(ITexture texture, ImageCrop crop, Action<object> swapBuffersCallback); public abstract void Present(ITexture texture, ImageCrop crop, Action<object> swapBuffersCallback);
public abstract void SetSize(int width, int height); public abstract void SetSize(int width, int height);
public abstract void ChangeVSyncMode(bool vsyncEnabled);
} }
} }

View file

@ -402,6 +402,8 @@ namespace Ryujinx.Ui
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
Translator.IsReadyForTranslation.Set(); Translator.IsReadyForTranslation.Set();
Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
(Toplevel as MainWindow)?.ActivatePauseMenu(); (Toplevel as MainWindow)?.ActivatePauseMenu();
while (_isActive) while (_isActive)