Compare commits

...

8 Commits

Author SHA1 Message Date
Keaton
6effa13252 Merge f3e1d9cd50 into 3b5f6170d1 2025-01-24 23:27:47 -06:00
Evan Husted
3b5f6170d1 misc: chore: move Rainbow updating to a separate task started/stopped as needed
update gommon & use the Event class from it to allow easily clearing all handlers when the apphost exits to avoid leftover invalid event handlers in the rainbow event handler list.
More robust config application logic to ensure what needs to happen only happens once
2025-01-24 23:06:59 -06:00
Evan Husted
9b6afa0ea2 misc: chore: Add log line to the other parts of the Updater that represent "up to date" 2025-01-24 17:00:50 -06:00
Evan Husted
3541e282ea Fully disconnect gamepad handler for rainbow color if configuration is set with UseRainbowLed false
Also check if its even enabled before setting the rainbow color
Fixes strobing
2025-01-24 16:52:20 -06:00
Otozinclus
1ce37ec317 Add option to change controller LED color (#572)
This allows the user to change the controller LED while using Ryujinx.
Useful for PS4 and PS5 controllers as an example.

You can also use a spectrum-cycling Rainbow color option, or turn the LED off for DualSense controllers.

---------

Co-authored-by: Evan Husted <greem@greemdev.net>
2025-01-24 14:47:36 -06:00
Evan Husted
c06f16c5e6 infra: chore: Raise minimum required Windows 10 version
Inspired by the breakages covered in #409
2025-01-23 17:39:34 -06:00
Evan Husted
7829fd8ee7 misc: chore: OS + CPU arch helpers 2025-01-23 16:58:48 -06:00
KeatonTheBot
f3e1d9cd50 Revert "Vulkan: Feedback loop detection and barriers (#7226)"
This reverts commit ca59c3f499.
2025-01-19 22:46:56 -06:00
46 changed files with 545 additions and 578 deletions

View File

@@ -42,7 +42,7 @@
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.30.0-build32" />
<PackageVersion Include="Gommon" Version="2.7.0.2" />
<PackageVersion Include="Gommon" Version="2.7.1" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.6.0" />
<PackageVersion Include="shaderc.net" Version="0.1.0" />

View File

@@ -2,14 +2,24 @@
{
public class LedConfigController
{
/// <summary>
/// Packed RGB int of the color
/// </summary>
public uint LedColor { get; set; }
/// <summary>
/// Enable LED color changing by the emulator
/// </summary>
public bool EnableLed { get; set; }
/// <summary>
/// Ignores the color and disables the LED entirely.
/// </summary>
public bool TurnOffLed { get; set; }
/// <summary>
/// Ignores the color and uses the rainbow color functionality for the LED.
/// </summary>
public bool UseRainbow { get; set; }
/// <summary>
/// Packed RGB int of the color
/// </summary>
public uint LedColor { get; set; }
}
}

View File

@@ -1,33 +1,13 @@
using Ryujinx.Common.Utilities;
using System;
namespace Ryujinx.Common.GraphicsDriver
{
public static class DriverUtilities
{
private static void AddMesaFlags(string envVar, string newFlags)
{
string existingFlags = Environment.GetEnvironmentVariable(envVar);
string flags = existingFlags == null ? newFlags : $"{existingFlags},{newFlags}";
OsUtils.SetEnvironmentVariableNoCaching(envVar, flags);
}
public static void InitDriverConfig(bool oglThreading)
{
if (OperatingSystem.IsLinux())
{
AddMesaFlags("RADV_DEBUG", "nodcc");
}
ToggleOGLThreading(oglThreading);
}
public static void ToggleOGLThreading(bool enabled)
{
OsUtils.SetEnvironmentVariableNoCaching("mesa_glthread", enabled.ToString().ToLower());
OsUtils.SetEnvironmentVariableNoCaching("__GL_THREADED_OPTIMIZATIONS", enabled ? "1" : "0");
Environment.SetEnvironmentVariable("mesa_glthread", enabled.ToString().ToLower());
Environment.SetEnvironmentVariable("__GL_THREADED_OPTIMIZATIONS", enabled ? "1" : "0");
try
{

View File

@@ -0,0 +1,23 @@
using System;
using System.Runtime.InteropServices;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable InconsistentNaming
namespace Ryujinx.Common.Helper
{
public static class RunningPlatform
{
public static bool IsMacOS => OperatingSystem.IsMacOS();
public static bool IsWindows => OperatingSystem.IsWindows();
public static bool IsLinux => OperatingSystem.IsLinux();
public static bool IsIntelMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.X64;
public static bool IsArmMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.Arm64;
public static bool IsX64Windows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.X64);
public static bool IsArmWindows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
public static bool IsX64Linux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.X64);
public static bool IsArmLinux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
}
}

View File

@@ -1,5 +1,6 @@
using Gommon;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Helper;
using System;
using System.Linq;
using System.Runtime.InteropServices;
@@ -21,7 +22,7 @@ namespace Ryujinx.Common
return currentBackend;
}
if (!(OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture is Architecture.Arm64))
if (!RunningPlatform.IsArmMac)
return GraphicsBackend.Vulkan;
return GreatMetalTitles.ContainsIgnoreCase(titleId) ? GraphicsBackend.Metal : GraphicsBackend.Vulkan;

View File

@@ -1,24 +0,0 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Ryujinx.Common.Utilities
{
public partial class OsUtils
{
[LibraryImport("libc", SetLastError = true)]
private static partial int setenv([MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string value, int overwrite);
public static void SetEnvironmentVariableNoCaching(string key, string value)
{
// Set the value in the cached environment variables, too.
Environment.SetEnvironmentVariable(key, value);
if (!OperatingSystem.IsWindows())
{
int res = setenv(key, value, 1);
Debug.Assert(res != -1);
}
}
}
}

View File

@@ -0,0 +1,110 @@
using Gommon;
using System;
using System.Drawing;
using System.Threading.Tasks;
namespace Ryujinx.Common.Utilities
{
public static class Rainbow
{
public static bool CyclingEnabled { get; set; }
public static void Enable()
{
if (!CyclingEnabled)
{
CyclingEnabled = true;
Executor.ExecuteBackgroundAsync(async () =>
{
while (CyclingEnabled)
{
await Task.Delay(15);
Tick();
}
});
}
}
public static void Disable()
{
CyclingEnabled = false;
}
public static float Speed { get; set; } = 1;
public static Color Color { get; private set; } = Color.Blue;
public static void Tick()
{
Color = HsbToRgb((Color.GetHue() + Speed) / 360);
UpdatedHandler.Call(Color.ToArgb());
}
public static void Reset()
{
Color = Color.Blue;
UpdatedHandler.Clear();
}
public static event Action<int> Updated
{
add => UpdatedHandler.Add(value);
remove => UpdatedHandler.Remove(value);
}
internal static Event<int> UpdatedHandler = new();
private static Color HsbToRgb(float hue, float saturation = 1, float brightness = 1)
{
int r = 0, g = 0, b = 0;
if (saturation == 0)
{
r = g = b = (int)(brightness * 255.0f + 0.5f);
}
else
{
float h = (hue - (float)Math.Floor(hue)) * 6.0f;
float f = h - (float)Math.Floor(h);
float p = brightness * (1.0f - saturation);
float q = brightness * (1.0f - saturation * f);
float t = brightness * (1.0f - (saturation * (1.0f - f)));
switch ((int)h)
{
case 0:
r = (int)(brightness * 255.0f + 0.5f);
g = (int)(t * 255.0f + 0.5f);
b = (int)(p * 255.0f + 0.5f);
break;
case 1:
r = (int)(q * 255.0f + 0.5f);
g = (int)(brightness * 255.0f + 0.5f);
b = (int)(p * 255.0f + 0.5f);
break;
case 2:
r = (int)(p * 255.0f + 0.5f);
g = (int)(brightness * 255.0f + 0.5f);
b = (int)(t * 255.0f + 0.5f);
break;
case 3:
r = (int)(p * 255.0f + 0.5f);
g = (int)(q * 255.0f + 0.5f);
b = (int)(brightness * 255.0f + 0.5f);
break;
case 4:
r = (int)(t * 255.0f + 0.5f);
g = (int)(p * 255.0f + 0.5f);
b = (int)(brightness * 255.0f + 0.5f);
break;
case 5:
r = (int)(brightness * 255.0f + 0.5f);
g = (int)(p * 255.0f + 0.5f);
b = (int)(q * 255.0f + 0.5f);
break;
}
}
return Color.FromArgb(Convert.ToByte(255), Convert.ToByte(r), Convert.ToByte(g), Convert.ToByte(b));
}
}
}

View File

@@ -32,12 +32,10 @@ namespace Ryujinx.Graphics.Vulkan
CommandBuffer
}
private bool _feedbackLoopActive;
private PipelineStageFlags _incoherentBufferWriteStages;
private PipelineStageFlags _incoherentTextureWriteStages;
private PipelineStageFlags _extraStages;
private IncoherentBarrierType _queuedIncoherentBarrier;
private bool _queuedFeedbackLoopBarrier;
public BarrierBatch(VulkanRenderer gd)
{
@@ -55,6 +53,17 @@ namespace Ryujinx.Graphics.Vulkan
stages |= PipelineStageFlags.TransformFeedbackBitExt;
}
if (!gd.IsTBDR)
{
// Desktop GPUs can transform image barriers into memory barriers.
access |= AccessFlags.DepthStencilAttachmentWriteBit | AccessFlags.ColorAttachmentWriteBit;
access |= AccessFlags.DepthStencilAttachmentReadBit | AccessFlags.ColorAttachmentReadBit;
stages |= PipelineStageFlags.EarlyFragmentTestsBit | PipelineStageFlags.LateFragmentTestsBit;
stages |= PipelineStageFlags.ColorAttachmentOutputBit;
}
return (access, stages);
}
@@ -169,34 +178,16 @@ namespace Ryujinx.Graphics.Vulkan
}
_queuedIncoherentBarrier = IncoherentBarrierType.None;
_queuedFeedbackLoopBarrier = false;
}
else if (_feedbackLoopActive && _queuedFeedbackLoopBarrier)
{
// Feedback loop barrier.
MemoryBarrier barrier = new MemoryBarrier()
{
SType = StructureType.MemoryBarrier,
SrcAccessMask = AccessFlags.ShaderWriteBit,
DstAccessMask = AccessFlags.ShaderReadBit
};
QueueBarrier(barrier, PipelineStageFlags.FragmentShaderBit, PipelineStageFlags.AllGraphicsBit);
_queuedFeedbackLoopBarrier = false;
}
_feedbackLoopActive = false;
}
}
public unsafe void Flush(CommandBufferScoped cbs, bool inRenderPass, RenderPassHolder rpHolder, Action endRenderPass)
{
Flush(cbs, null, false, inRenderPass, rpHolder, endRenderPass);
Flush(cbs, null, inRenderPass, rpHolder, endRenderPass);
}
public unsafe void Flush(CommandBufferScoped cbs, ShaderCollection program, bool feedbackLoopActive, bool inRenderPass, RenderPassHolder rpHolder, Action endRenderPass)
public unsafe void Flush(CommandBufferScoped cbs, ShaderCollection program, bool inRenderPass, RenderPassHolder rpHolder, Action endRenderPass)
{
if (program != null)
{
@@ -204,8 +195,6 @@ namespace Ryujinx.Graphics.Vulkan
_incoherentTextureWriteStages |= program.IncoherentTextureWriteStages;
}
_feedbackLoopActive |= feedbackLoopActive;
FlushMemoryBarrier(program, inRenderPass);
if (!inRenderPass && rpHolder != null)
@@ -417,8 +406,6 @@ namespace Ryujinx.Graphics.Vulkan
{
_queuedIncoherentBarrier = type;
}
_queuedFeedbackLoopBarrier = true;
}
public void QueueTextureBarrier()

View File

@@ -4,7 +4,6 @@ using Ryujinx.Graphics.Shader;
using Silk.NET.Vulkan;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using CompareOp = Ryujinx.Graphics.GAL.CompareOp;
@@ -43,15 +42,15 @@ namespace Ryujinx.Graphics.Vulkan
private record struct TextureRef
{
public ShaderStage Stage;
public TextureView View;
public Auto<DisposableImageView> ImageView;
public TextureStorage Storage;
public Auto<DisposableImageView> View;
public Auto<DisposableSampler> Sampler;
public TextureRef(ShaderStage stage, TextureView view, Auto<DisposableImageView> imageView, Auto<DisposableSampler> sampler)
public TextureRef(ShaderStage stage, TextureStorage storage, Auto<DisposableImageView> view, Auto<DisposableSampler> sampler)
{
Stage = stage;
Storage = storage;
View = view;
ImageView = imageView;
Sampler = sampler;
}
}
@@ -59,14 +58,14 @@ namespace Ryujinx.Graphics.Vulkan
private record struct ImageRef
{
public ShaderStage Stage;
public TextureView View;
public Auto<DisposableImageView> ImageView;
public TextureStorage Storage;
public Auto<DisposableImageView> View;
public ImageRef(ShaderStage stage, TextureView view, Auto<DisposableImageView> imageView)
public ImageRef(ShaderStage stage, TextureStorage storage, Auto<DisposableImageView> view)
{
Stage = stage;
Storage = storage;
View = view;
ImageView = imageView;
}
}
@@ -124,8 +123,6 @@ namespace Ryujinx.Graphics.Vulkan
private readonly TextureView _dummyTexture;
private readonly SamplerHolder _dummySampler;
public List<TextureView> FeedbackLoopHazards { get; private set; }
public DescriptorSetUpdater(VulkanRenderer gd, Device device)
{
_gd = gd;
@@ -210,15 +207,10 @@ namespace Ryujinx.Graphics.Vulkan
_templateUpdater = new();
}
public void Initialize(bool isMainPipeline)
public void Initialize()
{
MemoryOwner<byte> dummyTextureData = MemoryOwner<byte>.RentCleared(4);
_dummyTexture.SetData(dummyTextureData);
if (isMainPipeline)
{
FeedbackLoopHazards = new();
}
}
private static bool BindingOverlaps(ref DescriptorBufferInfo info, int bindingOffset, int offset, int size)
@@ -281,18 +273,6 @@ namespace Ryujinx.Graphics.Vulkan
public void InsertBindingBarriers(CommandBufferScoped cbs)
{
if ((FeedbackLoopHazards?.Count ?? 0) > 0)
{
// Clear existing hazards - they will be rebuilt.
foreach (TextureView hazard in FeedbackLoopHazards)
{
hazard.DecrementHazardUses();
}
FeedbackLoopHazards.Clear();
}
foreach (ResourceBindingSegment segment in _program.BindingSegments[PipelineBase.TextureSetIndex])
{
if (segment.Type == ResourceType.TextureAndSampler)
@@ -302,7 +282,7 @@ namespace Ryujinx.Graphics.Vulkan
for (int i = 0; i < segment.Count; i++)
{
ref var texture = ref _textureRefs[segment.Binding + i];
texture.View?.PrepareForUsage(cbs, texture.Stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards);
texture.Storage?.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, texture.Stage.ConvertToPipelineStageFlags());
}
}
else
@@ -323,7 +303,7 @@ namespace Ryujinx.Graphics.Vulkan
for (int i = 0; i < segment.Count; i++)
{
ref var image = ref _imageRefs[segment.Binding + i];
image.View?.PrepareForUsage(cbs, image.Stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards);
image.Storage?.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, image.Stage.ConvertToPipelineStageFlags());
}
}
else
@@ -397,12 +377,8 @@ namespace Ryujinx.Graphics.Vulkan
}
else if (image is TextureView view)
{
ref ImageRef iRef = ref _imageRefs[binding];
iRef.View?.ClearUsage(FeedbackLoopHazards);
view?.PrepareForUsage(cbs, stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards);
iRef = new(stage, view, view.GetIdentityImageView());
view.Storage.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, stage.ConvertToPipelineStageFlags());
_imageRefs[binding] = new(stage, view.Storage, view.GetIdentityImageView());
}
else
{
@@ -500,12 +476,9 @@ namespace Ryujinx.Graphics.Vulkan
}
else if (texture is TextureView view)
{
ref TextureRef iRef = ref _textureRefs[binding];
view.Storage.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, stage.ConvertToPipelineStageFlags());
iRef.View?.ClearUsage(FeedbackLoopHazards);
view?.PrepareForUsage(cbs, stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards);
iRef = new(stage, view, view.GetImageView(), ((SamplerHolder)sampler)?.GetSampler());
_textureRefs[binding] = new(stage, view.Storage, view.GetImageView(), ((SamplerHolder)sampler)?.GetSampler());
}
else
{
@@ -527,7 +500,7 @@ namespace Ryujinx.Graphics.Vulkan
{
view.Storage.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, stage.ConvertToPipelineStageFlags());
_textureRefs[binding] = new(stage, view, view.GetIdentityImageView(), ((SamplerHolder)sampler)?.GetSampler());
_textureRefs[binding] = new(stage, view.Storage, view.GetIdentityImageView(), ((SamplerHolder)sampler)?.GetSampler());
SignalDirty(DirtyFlags.Texture);
}
@@ -853,7 +826,7 @@ namespace Ryujinx.Graphics.Vulkan
ref var texture = ref textures[i];
ref var refs = ref _textureRefs[binding + i];
texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default;
texture.ImageView = refs.View?.Get(cbs).Value ?? default;
texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default;
if (texture.ImageView.Handle == 0)
@@ -903,7 +876,7 @@ namespace Ryujinx.Graphics.Vulkan
for (int i = 0; i < count; i++)
{
images[i].ImageView = _imageRefs[binding + i].ImageView?.Get(cbs).Value ?? default;
images[i].ImageView = _imageRefs[binding + i].View?.Get(cbs).Value ?? default;
}
tu.Push<DescriptorImageInfo>(images[..count]);
@@ -974,7 +947,7 @@ namespace Ryujinx.Graphics.Vulkan
ref var texture = ref textures[i];
ref var refs = ref _textureRefs[binding + i];
texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default;
texture.ImageView = refs.View?.Get(cbs).Value ?? default;
texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default;
if (texture.ImageView.Handle == 0)

View File

@@ -1,12 +0,0 @@
using System;
namespace Ryujinx.Graphics.Vulkan
{
[Flags]
internal enum FeedbackLoopAspects
{
None = 0,
Color = 1 << 0,
Depth = 1 << 1,
}
}

View File

@@ -302,27 +302,6 @@ namespace Ryujinx.Graphics.Vulkan
_depthStencil?.Storage?.AddStoreOpUsage(true);
}
public void ClearBindings()
{
_depthStencil?.Storage.ClearBindings();
for (int i = 0; i < _colorsCanonical.Length; i++)
{
_colorsCanonical[i]?.Storage.ClearBindings();
}
}
public void AddBindings()
{
_depthStencil?.Storage.AddBinding(_depthStencil);
for (int i = 0; i < _colorsCanonical.Length; i++)
{
TextureView color = _colorsCanonical[i];
color?.Storage.AddBinding(color);
}
}
public (RenderPassHolder rpHolder, Auto<DisposableFramebuffer> framebuffer) GetPassAndFramebuffer(
VulkanRenderer gd,
Device device,

View File

@@ -46,8 +46,6 @@ namespace Ryujinx.Graphics.Vulkan
public readonly bool SupportsViewportArray2;
public readonly bool SupportsHostImportedMemory;
public readonly bool SupportsDepthClipControl;
public readonly bool SupportsAttachmentFeedbackLoop;
public readonly bool SupportsDynamicAttachmentFeedbackLoop;
public readonly uint SubgroupSize;
public readonly SampleCountFlags SupportedSampleCounts;
public readonly PortabilitySubsetFlags PortabilitySubset;
@@ -86,8 +84,6 @@ namespace Ryujinx.Graphics.Vulkan
bool supportsViewportArray2,
bool supportsHostImportedMemory,
bool supportsDepthClipControl,
bool supportsAttachmentFeedbackLoop,
bool supportsDynamicAttachmentFeedbackLoop,
uint subgroupSize,
SampleCountFlags supportedSampleCounts,
PortabilitySubsetFlags portabilitySubset,
@@ -125,8 +121,6 @@ namespace Ryujinx.Graphics.Vulkan
SupportsViewportArray2 = supportsViewportArray2;
SupportsHostImportedMemory = supportsHostImportedMemory;
SupportsDepthClipControl = supportsDepthClipControl;
SupportsAttachmentFeedbackLoop = supportsAttachmentFeedbackLoop;
SupportsDynamicAttachmentFeedbackLoop = supportsDynamicAttachmentFeedbackLoop;
SubgroupSize = subgroupSize;
SupportedSampleCounts = supportedSampleCounts;
PortabilitySubset = portabilitySubset;

View File

@@ -2,7 +2,6 @@ using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -34,7 +33,6 @@ namespace Ryujinx.Graphics.Vulkan
public readonly Action EndRenderPassDelegate;
protected PipelineDynamicState DynamicState;
protected bool IsMainPipeline;
private PipelineState _newState;
private bool _graphicsStateDirty;
private bool _computeStateDirty;
@@ -87,9 +85,6 @@ namespace Ryujinx.Graphics.Vulkan
private bool _tfEnabled;
private bool _tfActive;
private FeedbackLoopAspects _feedbackLoop;
private bool _passWritesDepthStencil;
private readonly PipelineColorBlendAttachmentState[] _storedBlend;
public ulong DrawCount { get; private set; }
public bool RenderPassActive { get; private set; }
@@ -131,7 +126,7 @@ namespace Ryujinx.Graphics.Vulkan
public void Initialize()
{
_descriptorSetUpdater.Initialize(IsMainPipeline);
_descriptorSetUpdater.Initialize();
QuadsToTrisPattern = new IndexBufferPattern(Gd, 4, 6, 0, new[] { 0, 1, 2, 0, 2, 3 }, 4, false);
TriFanToTrisPattern = new IndexBufferPattern(Gd, 3, 3, 2, new[] { int.MinValue, -1, 0 }, 1, true);
@@ -819,8 +814,6 @@ namespace Ryujinx.Graphics.Vulkan
_newState.DepthTestEnable = depthTest.TestEnable;
_newState.DepthWriteEnable = depthTest.WriteEnable;
_newState.DepthCompareOp = depthTest.Func.Convert();
UpdatePassDepthStencil();
SignalStateChange();
}
@@ -1086,8 +1079,6 @@ namespace Ryujinx.Graphics.Vulkan
_newState.StencilFrontPassOp = stencilTest.FrontDpPass.Convert();
_newState.StencilFrontDepthFailOp = stencilTest.FrontDpFail.Convert();
_newState.StencilFrontCompareOp = stencilTest.FrontFunc.Convert();
UpdatePassDepthStencil();
SignalStateChange();
}
@@ -1435,23 +1426,7 @@ namespace Ryujinx.Graphics.Vulkan
}
}
if (IsMainPipeline)
{
FramebufferParams?.ClearBindings();
}
FramebufferParams = new FramebufferParams(Device, colors, depthStencil);
if (IsMainPipeline)
{
FramebufferParams.AddBindings();
_newState.FeedbackLoopAspects = FeedbackLoopAspects.None;
_bindingBarriersDirty = true;
}
_passWritesDepthStencil = false;
UpdatePassDepthStencil();
UpdatePipelineAttachmentFormats();
}
@@ -1518,82 +1493,11 @@ namespace Ryujinx.Graphics.Vulkan
}
}
Gd.Barriers.Flush(Cbs, _program, _feedbackLoop != 0, RenderPassActive, _rpHolder, EndRenderPassDelegate);
Gd.Barriers.Flush(Cbs, _program, RenderPassActive, _rpHolder, EndRenderPassDelegate);
_descriptorSetUpdater.UpdateAndBindDescriptorSets(Cbs, PipelineBindPoint.Compute);
}
private bool ChangeFeedbackLoop(FeedbackLoopAspects aspects)
{
if (_feedbackLoop != aspects)
{
if (Gd.Capabilities.SupportsDynamicAttachmentFeedbackLoop)
{
DynamicState.SetFeedbackLoop(aspects);
}
else
{
_newState.FeedbackLoopAspects = aspects;
}
_feedbackLoop = aspects;
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool UpdateFeedbackLoop()
{
List<TextureView> hazards = _descriptorSetUpdater.FeedbackLoopHazards;
if ((hazards?.Count ?? 0) > 0)
{
FeedbackLoopAspects aspects = 0;
foreach (TextureView view in hazards)
{
// May need to enforce feedback loop layout here in the future.
// Though technically, it should always work with the general layout.
if (view.Info.Format.IsDepthOrStencil())
{
if (_passWritesDepthStencil)
{
// If depth/stencil isn't written in the pass, it doesn't count as a feedback loop.
aspects |= FeedbackLoopAspects.Depth;
}
}
else
{
aspects |= FeedbackLoopAspects.Color;
}
}
return ChangeFeedbackLoop(aspects);
}
else if (_feedbackLoop != 0)
{
return ChangeFeedbackLoop(FeedbackLoopAspects.None);
}
return false;
}
private void UpdatePassDepthStencil()
{
if (!RenderPassActive)
{
_passWritesDepthStencil = false;
}
// Stencil test being enabled doesn't necessarily mean a write, but it's not critical to check.
_passWritesDepthStencil |= (_newState.DepthTestEnable && _newState.DepthWriteEnable) || _newState.StencilTestEnable;
}
private bool RecreateGraphicsPipelineIfNeeded()
{
if (AutoFlush.ShouldFlushDraw(DrawCount))
@@ -1601,7 +1505,7 @@ namespace Ryujinx.Graphics.Vulkan
Gd.FlushAllCommands();
}
DynamicState.ReplayIfDirty(Gd, CommandBuffer);
DynamicState.ReplayIfDirty(Gd.Api, CommandBuffer);
if (_needsIndexBufferRebind && _indexBufferPattern == null)
{
@@ -1635,15 +1539,7 @@ namespace Ryujinx.Graphics.Vulkan
_vertexBufferUpdater.Commit(Cbs);
}
if (_bindingBarriersDirty)
{
// Stale barriers may have been activated by switching program. Emit any that are relevant.
_descriptorSetUpdater.InsertBindingBarriers(Cbs);
_bindingBarriersDirty = false;
}
if (UpdateFeedbackLoop() || _graphicsStateDirty || Pbp != PipelineBindPoint.Graphics)
if (_graphicsStateDirty || Pbp != PipelineBindPoint.Graphics)
{
if (!CreatePipeline(PipelineBindPoint.Graphics))
{
@@ -1652,9 +1548,17 @@ namespace Ryujinx.Graphics.Vulkan
_graphicsStateDirty = false;
Pbp = PipelineBindPoint.Graphics;
if (_bindingBarriersDirty)
{
// Stale barriers may have been activated by switching program. Emit any that are relevant.
_descriptorSetUpdater.InsertBindingBarriers(Cbs);
_bindingBarriersDirty = false;
}
}
Gd.Barriers.Flush(Cbs, _program, _feedbackLoop != 0, RenderPassActive, _rpHolder, EndRenderPassDelegate);
Gd.Barriers.Flush(Cbs, _program, RenderPassActive, _rpHolder, EndRenderPassDelegate);
_descriptorSetUpdater.UpdateAndBindDescriptorSets(Cbs, PipelineBindPoint.Graphics);

View File

@@ -1,6 +1,5 @@
using Ryujinx.Common.Memory;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.EXT;
namespace Ryujinx.Graphics.Vulkan
{
@@ -22,8 +21,6 @@ namespace Ryujinx.Graphics.Vulkan
private Array4<float> _blendConstants;
private FeedbackLoopAspects _feedbackLoopAspects;
public uint ViewportsCount;
public Array16<Viewport> Viewports;
@@ -35,8 +32,7 @@ namespace Ryujinx.Graphics.Vulkan
Scissor = 1 << 2,
Stencil = 1 << 3,
Viewport = 1 << 4,
FeedbackLoop = 1 << 5,
All = Blend | DepthBias | Scissor | Stencil | Viewport | FeedbackLoop,
All = Blend | DepthBias | Scissor | Stencil | Viewport,
}
private DirtyFlags _dirty;
@@ -103,22 +99,13 @@ namespace Ryujinx.Graphics.Vulkan
}
}
public void SetFeedbackLoop(FeedbackLoopAspects aspects)
{
_feedbackLoopAspects = aspects;
_dirty |= DirtyFlags.FeedbackLoop;
}
public void ForceAllDirty()
{
_dirty = DirtyFlags.All;
}
public void ReplayIfDirty(VulkanRenderer gd, CommandBuffer commandBuffer)
public void ReplayIfDirty(Vk api, CommandBuffer commandBuffer)
{
Vk api = gd.Api;
if (_dirty.HasFlag(DirtyFlags.Blend))
{
RecordBlend(api, commandBuffer);
@@ -144,11 +131,6 @@ namespace Ryujinx.Graphics.Vulkan
RecordViewport(api, commandBuffer);
}
if (_dirty.HasFlag(DirtyFlags.FeedbackLoop) && gd.Capabilities.SupportsDynamicAttachmentFeedbackLoop)
{
RecordFeedbackLoop(gd.DynamicFeedbackLoopApi, commandBuffer);
}
_dirty = DirtyFlags.None;
}
@@ -187,17 +169,5 @@ namespace Ryujinx.Graphics.Vulkan
api.CmdSetViewport(commandBuffer, 0, ViewportsCount, Viewports.AsSpan());
}
}
private readonly void RecordFeedbackLoop(ExtAttachmentFeedbackLoopDynamicState api, CommandBuffer commandBuffer)
{
ImageAspectFlags aspects = (_feedbackLoopAspects & FeedbackLoopAspects.Color) != 0 ? ImageAspectFlags.ColorBit : 0;
if ((_feedbackLoopAspects & FeedbackLoopAspects.Depth) != 0)
{
aspects |= ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit;
}
api.CmdSetAttachmentFeedbackLoopEnable(commandBuffer, aspects);
}
}
}

View File

@@ -28,8 +28,6 @@ namespace Ryujinx.Graphics.Vulkan
_activeBufferMirrors = new();
CommandBuffer = (Cbs = gd.CommandBufferPool.Rent()).CommandBuffer;
IsMainPipeline = true;
}
private void CopyPendingQuery()
@@ -237,7 +235,7 @@ namespace Ryujinx.Graphics.Vulkan
if (Pipeline != null && Pbp == PipelineBindPoint.Graphics)
{
DynamicState.ReplayIfDirty(Gd, CommandBuffer);
DynamicState.ReplayIfDirty(Gd.Api, CommandBuffer);
}
}

View File

@@ -8,7 +8,6 @@ namespace Ryujinx.Graphics.Vulkan
struct PipelineState : IDisposable
{
private const int RequiredSubgroupSize = 32;
private const int MaxDynamicStatesCount = 9;
public PipelineUid Internal;
@@ -300,12 +299,6 @@ namespace Ryujinx.Graphics.Vulkan
set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFBF) | ((value ? 1UL : 0UL) << 6);
}
public FeedbackLoopAspects FeedbackLoopAspects
{
readonly get => (FeedbackLoopAspects)((Internal.Id8 >> 7) & 0x3);
set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFE7F) | (((ulong)value) << 7);
}
public bool HasTessellationControlShader;
public NativeArray<PipelineShaderStageCreateInfo> Stages;
public PipelineLayout PipelineLayout;
@@ -571,11 +564,9 @@ namespace Ryujinx.Graphics.Vulkan
}
bool supportsExtDynamicState = gd.Capabilities.SupportsExtendedDynamicState;
bool supportsFeedbackLoopDynamicState = gd.Capabilities.SupportsDynamicAttachmentFeedbackLoop;
int dynamicStatesCount = supportsExtDynamicState ? 8 : 7;
DynamicState* dynamicStates = stackalloc DynamicState[MaxDynamicStatesCount];
int dynamicStatesCount = 7;
DynamicState* dynamicStates = stackalloc DynamicState[dynamicStatesCount];
dynamicStates[0] = DynamicState.Viewport;
dynamicStates[1] = DynamicState.Scissor;
@@ -587,12 +578,7 @@ namespace Ryujinx.Graphics.Vulkan
if (supportsExtDynamicState)
{
dynamicStates[dynamicStatesCount++] = DynamicState.VertexInputBindingStrideExt;
}
if (supportsFeedbackLoopDynamicState)
{
dynamicStates[dynamicStatesCount++] = DynamicState.AttachmentFeedbackLoopEnableExt;
dynamicStates[7] = DynamicState.VertexInputBindingStrideExt;
}
var pipelineDynamicStateCreateInfo = new PipelineDynamicStateCreateInfo
@@ -602,27 +588,9 @@ namespace Ryujinx.Graphics.Vulkan
PDynamicStates = dynamicStates,
};
PipelineCreateFlags flags = 0;
if (gd.Capabilities.SupportsAttachmentFeedbackLoop)
{
FeedbackLoopAspects aspects = FeedbackLoopAspects;
if ((aspects & FeedbackLoopAspects.Color) != 0)
{
flags |= PipelineCreateFlags.CreateColorAttachmentFeedbackLoopBitExt;
}
if ((aspects & FeedbackLoopAspects.Depth) != 0)
{
flags |= PipelineCreateFlags.CreateDepthStencilAttachmentFeedbackLoopBitExt;
}
}
var pipelineCreateInfo = new GraphicsPipelineCreateInfo
{
SType = StructureType.GraphicsPipelineCreateInfo,
Flags = flags,
StageCount = StagesCount,
PStages = Stages.Pointer,
PVertexInputState = &vertexInputState,

View File

@@ -4,7 +4,6 @@ using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using Format = Ryujinx.Graphics.GAL.Format;
using VkBuffer = Silk.NET.Vulkan.Buffer;
using VkFormat = Silk.NET.Vulkan.Format;
@@ -13,11 +12,6 @@ namespace Ryujinx.Graphics.Vulkan
{
class TextureStorage : IDisposable
{
private struct TextureSliceInfo
{
public int BindCount;
}
private const MemoryPropertyFlags DefaultImageMemoryFlags =
MemoryPropertyFlags.DeviceLocalBit;
@@ -49,7 +43,6 @@ namespace Ryujinx.Graphics.Vulkan
private readonly Image _image;
private readonly Auto<DisposableImage> _imageAuto;
private readonly Auto<MemoryAllocation> _allocationAuto;
private readonly int _depthOrLayers;
private Auto<MemoryAllocation> _foreignAllocationAuto;
private Dictionary<Format, TextureStorage> _aliasedStorages;
@@ -62,9 +55,6 @@ namespace Ryujinx.Graphics.Vulkan
private int _viewsCount;
private readonly ulong _size;
private int _bindCount;
private readonly TextureSliceInfo[] _slices;
public VkFormat VkFormat { get; }
public unsafe TextureStorage(
@@ -85,7 +75,6 @@ namespace Ryujinx.Graphics.Vulkan
var depth = (uint)(info.Target == Target.Texture3D ? info.Depth : 1);
VkFormat = format;
_depthOrLayers = info.GetDepthOrLayers();
var type = info.Target.Convert();
@@ -161,8 +150,6 @@ namespace Ryujinx.Graphics.Vulkan
InitialTransition(ImageLayout.Preinitialized, ImageLayout.General);
}
_slices = new TextureSliceInfo[levels * _depthOrLayers];
}
public TextureStorage CreateAliasedColorForDepthStorageUnsafe(Format format)
@@ -325,12 +312,6 @@ namespace Ryujinx.Graphics.Vulkan
usage |= ImageUsageFlags.StorageBit;
}
if (capabilities.SupportsAttachmentFeedbackLoop &&
(usage & (ImageUsageFlags.DepthStencilAttachmentBit | ImageUsageFlags.ColorAttachmentBit)) != 0)
{
usage |= ImageUsageFlags.AttachmentFeedbackLoopBitExt;
}
return usage;
}
@@ -531,55 +512,6 @@ namespace Ryujinx.Graphics.Vulkan
}
}
public void AddBinding(TextureView view)
{
// Assumes a view only has a first level.
int index = view.FirstLevel * _depthOrLayers + view.FirstLayer;
int layers = view.Layers;
for (int i = 0; i < layers; i++)
{
ref TextureSliceInfo info = ref _slices[index++];
info.BindCount++;
}
_bindCount++;
}
public void ClearBindings()
{
if (_bindCount != 0)
{
Array.Clear(_slices, 0, _slices.Length);
_bindCount = 0;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsBound(TextureView view)
{
if (_bindCount != 0)
{
int index = view.FirstLevel * _depthOrLayers + view.FirstLayer;
int layers = view.Layers;
for (int i = 0; i < layers; i++)
{
ref TextureSliceInfo info = ref _slices[index++];
if (info.BindCount != 0)
{
return true;
}
}
}
return false;
}
public void IncrementViewsCount()
{
_viewsCount++;

View File

@@ -23,8 +23,6 @@ namespace Ryujinx.Graphics.Vulkan
private readonly Auto<DisposableImageView> _imageView2dArray;
private Dictionary<Format, TextureView> _selfManagedViews;
private int _hazardUses;
private readonly TextureCreateInfo _info;
private HashTableSlim<RenderPassCacheKey, RenderPassHolder> _renderPasses;
@@ -1039,34 +1037,6 @@ namespace Ryujinx.Graphics.Vulkan
throw new NotImplementedException();
}
public void PrepareForUsage(CommandBufferScoped cbs, PipelineStageFlags flags, List<TextureView> feedbackLoopHazards)
{
Storage.QueueWriteToReadBarrier(cbs, AccessFlags.ShaderReadBit, flags);
if (feedbackLoopHazards != null && Storage.IsBound(this))
{
feedbackLoopHazards.Add(this);
_hazardUses++;
}
}
public void ClearUsage(List<TextureView> feedbackLoopHazards)
{
if (_hazardUses != 0 && feedbackLoopHazards != null)
{
feedbackLoopHazards.Remove(this);
_hazardUses--;
}
}
public void DecrementHazardUses()
{
if (_hazardUses != 0)
{
_hazardUses--;
}
}
public (RenderPassHolder rpHolder, Auto<DisposableFramebuffer> framebuffer) GetPassAndFramebuffer(
VulkanRenderer gd,
Device device,

View File

@@ -44,8 +44,6 @@ namespace Ryujinx.Graphics.Vulkan
"VK_EXT_4444_formats",
"VK_KHR_8bit_storage",
"VK_KHR_maintenance2",
"VK_EXT_attachment_feedback_loop_layout",
"VK_EXT_attachment_feedback_loop_dynamic_state",
};
private static readonly string[] _requiredExtensions = {
@@ -359,28 +357,6 @@ namespace Ryujinx.Graphics.Vulkan
features2.PNext = &supportedFeaturesDepthClipControl;
}
PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT supportedFeaturesAttachmentFeedbackLoopLayout = new()
{
SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesExt,
PNext = features2.PNext,
};
if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_layout"))
{
features2.PNext = &supportedFeaturesAttachmentFeedbackLoopLayout;
}
PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesEXT supportedFeaturesDynamicAttachmentFeedbackLoopLayout = new()
{
SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesExt,
PNext = features2.PNext,
};
if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_dynamic_state"))
{
features2.PNext = &supportedFeaturesDynamicAttachmentFeedbackLoopLayout;
}
PhysicalDeviceVulkan12Features supportedPhysicalDeviceVulkan12Features = new()
{
SType = StructureType.PhysicalDeviceVulkan12Features,
@@ -555,36 +531,6 @@ namespace Ryujinx.Graphics.Vulkan
pExtendedFeatures = &featuresDepthClipControl;
}
PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT featuresAttachmentFeedbackLoopLayout;
if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_layout") &&
supportedFeaturesAttachmentFeedbackLoopLayout.AttachmentFeedbackLoopLayout)
{
featuresAttachmentFeedbackLoopLayout = new()
{
SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesExt,
PNext = pExtendedFeatures,
AttachmentFeedbackLoopLayout = true,
};
pExtendedFeatures = &featuresAttachmentFeedbackLoopLayout;
}
PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesEXT featuresDynamicAttachmentFeedbackLoopLayout;
if (physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_dynamic_state") &&
supportedFeaturesDynamicAttachmentFeedbackLoopLayout.AttachmentFeedbackLoopDynamicState)
{
featuresDynamicAttachmentFeedbackLoopLayout = new()
{
SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesExt,
PNext = pExtendedFeatures,
AttachmentFeedbackLoopDynamicState = true,
};
pExtendedFeatures = &featuresDynamicAttachmentFeedbackLoopLayout;
}
var enabledExtensions = _requiredExtensions.Union(_desirableExtensions.Intersect(physicalDevice.DeviceExtensions)).ToArray();
nint* ppEnabledExtensions = stackalloc nint[enabledExtensions.Length];

View File

@@ -42,7 +42,6 @@ namespace Ryujinx.Graphics.Vulkan
internal KhrPushDescriptor PushDescriptorApi { get; private set; }
internal ExtTransformFeedback TransformFeedbackApi { get; private set; }
internal KhrDrawIndirectCount DrawIndirectCountApi { get; private set; }
internal ExtAttachmentFeedbackLoopDynamicState DynamicFeedbackLoopApi { get; private set; }
internal uint QueueFamilyIndex { get; private set; }
internal Queue Queue { get; private set; }
@@ -156,11 +155,6 @@ namespace Ryujinx.Graphics.Vulkan
DrawIndirectCountApi = drawIndirectCountApi;
}
if (Api.TryGetDeviceExtension(_instance.Instance, _device, out ExtAttachmentFeedbackLoopDynamicState dynamicFeedbackLoopApi))
{
DynamicFeedbackLoopApi = dynamicFeedbackLoopApi;
}
if (maxQueueCount >= 2)
{
Api.GetDeviceQueue(_device, queueFamilyIndex, 1, out var backgroundQueue);
@@ -255,16 +249,6 @@ namespace Ryujinx.Graphics.Vulkan
SType = StructureType.PhysicalDeviceDepthClipControlFeaturesExt,
};
PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT featuresAttachmentFeedbackLoop = new()
{
SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesExt,
};
PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesEXT featuresDynamicAttachmentFeedbackLoop = new()
{
SType = StructureType.PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesExt,
};
PhysicalDevicePortabilitySubsetFeaturesKHR featuresPortabilitySubset = new()
{
SType = StructureType.PhysicalDevicePortabilitySubsetFeaturesKhr,
@@ -301,22 +285,6 @@ namespace Ryujinx.Graphics.Vulkan
features2.PNext = &featuresDepthClipControl;
}
bool supportsAttachmentFeedbackLoop = _physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_layout");
if (supportsAttachmentFeedbackLoop)
{
featuresAttachmentFeedbackLoop.PNext = features2.PNext;
features2.PNext = &featuresAttachmentFeedbackLoop;
}
bool supportsDynamicAttachmentFeedbackLoop = _physicalDevice.IsDeviceExtensionPresent("VK_EXT_attachment_feedback_loop_dynamic_state");
if (supportsDynamicAttachmentFeedbackLoop)
{
featuresDynamicAttachmentFeedbackLoop.PNext = features2.PNext;
features2.PNext = &featuresDynamicAttachmentFeedbackLoop;
}
bool usePortability = _physicalDevice.IsDeviceExtensionPresent("VK_KHR_portability_subset");
if (usePortability)
@@ -439,8 +407,6 @@ namespace Ryujinx.Graphics.Vulkan
_physicalDevice.IsDeviceExtensionPresent("VK_NV_viewport_array2"),
_physicalDevice.IsDeviceExtensionPresent(ExtExternalMemoryHost.ExtensionName),
supportsDepthClipControl && featuresDepthClipControl.DepthClipControl,
supportsAttachmentFeedbackLoop && featuresAttachmentFeedbackLoop.AttachmentFeedbackLoopLayout,
supportsDynamicAttachmentFeedbackLoop && featuresDynamicAttachmentFeedbackLoop.AttachmentFeedbackLoopDynamicState,
propertiesSubgroup.SubgroupSize,
supportedSampleCounts,
portabilityFlags,

View File

@@ -1,6 +1,8 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Hid;
using SDL2;
using System;
using System.Collections.Generic;
@@ -86,7 +88,7 @@ namespace Ryujinx.Input.SDL2
Id = driverId;
Features = GetFeaturesFlag();
_triggerThreshold = 0.0f;
// Enable motion tracking
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
{
@@ -102,6 +104,18 @@ namespace Ryujinx.Input.SDL2
}
}
public void SetLed(uint packedRgb)
{
if (!Features.HasFlag(GamepadFeaturesFlag.Led)) return;
byte red = packedRgb > 0 ? (byte)(packedRgb >> 16) : (byte)0;
byte green = packedRgb > 0 ? (byte)(packedRgb >> 8) : (byte)0;
byte blue = packedRgb > 0 ? (byte)(packedRgb % 256) : (byte)0;
if (SDL_GameControllerSetLED(_gamepadHandle, red, green, blue) != 0)
Logger.Error?.Print(LogClass.Hid, "LED is not supported on this game controller.");
}
private GamepadFeaturesFlag GetFeaturesFlag()
{
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
@@ -112,9 +126,7 @@ namespace Ryujinx.Input.SDL2
result |= GamepadFeaturesFlag.Motion;
}
int error = SDL_GameControllerRumble(_gamepadHandle, 0, 0, 100);
if (error == 0)
if (SDL_GameControllerHasRumble(_gamepadHandle) == SDL_bool.SDL_TRUE)
{
result |= GamepadFeaturesFlag.Rumble;
}
@@ -136,6 +148,8 @@ namespace Ryujinx.Input.SDL2
{
if (disposing && _gamepadHandle != nint.Zero)
{
Rainbow.Updated -= RainbowColorChanged;
SDL_GameControllerClose(_gamepadHandle);
_gamepadHandle = nint.Zero;
@@ -214,12 +228,41 @@ namespace Ryujinx.Input.SDL2
private static Vector3 GsToMs2(Vector3 gs) => gs / SDL_STANDARD_GRAVITY;
private void RainbowColorChanged(int packedRgb)
{
if (!_configuration.Led.UseRainbow) return;
SetLed((uint)packedRgb);
}
private bool _rainbowColorEnabled;
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
{
_configuration = (StandardControllerInputConfig)configuration;
if (Features.HasFlag(GamepadFeaturesFlag.Led) && _configuration.Led.EnableLed)
{
if (_configuration.Led.TurnOffLed)
(this as IGamepad).ClearLed();
else switch (_configuration.Led.UseRainbow)
{
case true when !_rainbowColorEnabled:
Rainbow.Updated += RainbowColorChanged;
_rainbowColorEnabled = true;
break;
case false when _rainbowColorEnabled:
Rainbow.Updated -= RainbowColorChanged;
_rainbowColorEnabled = false;
break;
}
if (!_configuration.Led.TurnOffLed && !_rainbowColorEnabled)
SetLed(_configuration.Led.LedColor);
}
_buttonsUserMapping.Clear();
// First update sticks

View File

@@ -173,5 +173,16 @@ namespace Ryujinx.Input.SDL2
return new SDL2Gamepad(gamepadHandle, id);
}
public IEnumerable<IGamepad> GetGamepads()
{
lock (_gamepadsIds)
{
foreach (string gamepadId in _gamepadsIds)
{
yield return GetGamepad(gamepadId);
}
}
}
}
}

View File

@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Numerics;
@@ -385,6 +386,11 @@ namespace Ryujinx.Input.SDL2
}
}
public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL2Keyboard");
}
public void SetTriggerThreshold(float triggerThreshold)
{
// No operations

View File

@@ -1,4 +1,5 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using System;
using System.Drawing;
using System.Numerics;
@@ -76,6 +77,11 @@ namespace Ryujinx.Input.SDL2
throw new NotImplementedException();
}
public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL2Mouse");
}
public void SetTriggerThreshold(float triggerThreshold)
{
throw new NotImplementedException();

View File

@@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Numerics;
@@ -164,6 +165,8 @@ namespace Ryujinx.Input.SDL2
return new SDL2Mouse(this);
}
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
public void Dispose()
{
if (_isDisposed)

View File

@@ -1,5 +1,6 @@
using Ryujinx.SDL2.Common;
using System;
using System.Collections.Generic;
namespace Ryujinx.Input.SDL2
{
@@ -51,5 +52,13 @@ namespace Ryujinx.Input.SDL2
return new SDL2Keyboard(this, _keyboardIdentifers[0], "All keyboards");
}
public IEnumerable<IGamepad> GetGamepads()
{
foreach (var keyboardId in _keyboardIdentifers)
{
yield return GetGamepad(keyboardId);
}
}
}
}

View File

@@ -65,6 +65,15 @@ namespace Ryujinx.Input
/// <param name="configuration">The configuration of the gamepad</param>
void SetConfiguration(InputConfig configuration);
/// <summary>
/// Set the LED on the gamepad to a given color.
/// </summary>
/// <remarks>Does nothing on a controller without LED functionality.</remarks>
/// <param name="packedRgb">The packed RGB integer.</param>
void SetLed(uint packedRgb);
public void ClearLed() => SetLed(0);
/// <summary>
/// Starts a rumble effect on the gamepad.
/// </summary>

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace Ryujinx.Input
{
@@ -33,6 +34,11 @@ namespace Ryujinx.Input
/// <param name="id">The unique id of the gamepad</param>
/// <returns>An instance of <see cref="IGamepad"/> associated to the gamepad id given or null if not found</returns>
IGamepad GetGamepad(string id);
/// <summary>
/// Returns an <see cref="IEnumerable{T}"/> of the connected gamepads.
/// </summary>
IEnumerable<IGamepad> GetGamepads();
/// <summary>
/// Clear the internal state of the driver.

View File

@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

View File

@@ -501,6 +501,8 @@ namespace Ryujinx.Ava
_renderingThread.Start();
_viewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value;
Rainbow.Enable();
MainLoop();
@@ -587,6 +589,15 @@ namespace Ryujinx.Ava
return;
}
foreach (IGamepad gamepad in RyujinxApp.MainWindow.InputManager.GamepadDriver.GetGamepads())
{
gamepad?.ClearLed();
gamepad?.Dispose();
}
Rainbow.Disable();
Rainbow.Reset();
_isStopped = true;
Stop();
}

View File

@@ -7628,7 +7628,57 @@
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Custom LED",
"en_US": "LED",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "ControllerSettingsLedColorDisable",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Disable",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "ControllerSettingsLedColorRainbow",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Rainbow",
"es_ES": "",
"fr_FR": "",
"he_IL": "",

View File

@@ -5,7 +5,6 @@ using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Logging.Targets;
using Ryujinx.Common.SystemInterop;
@@ -285,8 +284,6 @@ namespace Ryujinx.Headless
GraphicsConfig.ShadersDumpPath = option.GraphicsShadersDumpPath;
GraphicsConfig.EnableMacroHLE = !option.DisableMacroHLE;
DriverUtilities.InitDriverConfig(option.BackendThreading == BackendThreading.Off);
while (true)
{
LoadApplication(option);

View File

@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging;
using Ryujinx.Input;
using System;
using System.Collections.Generic;
@@ -143,6 +144,11 @@ namespace Ryujinx.Ava.Input
}
}
public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
}
public void SetTriggerThreshold(float triggerThreshold) { }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { }

View File

@@ -59,6 +59,8 @@ namespace Ryujinx.Ava.Input
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.AllKeyboards]);
}
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
protected virtual void Dispose(bool disposing)
{
if (disposing)

View File

@@ -1,4 +1,5 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.Input;
using System;
using System.Drawing;
@@ -74,6 +75,11 @@ namespace Ryujinx.Ava.Input
throw new NotImplementedException();
}
public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaMouse");
}
public void SetTriggerThreshold(float triggerThreshold)
{
throw new NotImplementedException();

View File

@@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Input;
using Ryujinx.Input;
using System;
using System.Collections.Generic;
using System.Numerics;
using MouseButton = Ryujinx.Input.MouseButton;
using Size = System.Drawing.Size;
@@ -134,6 +135,8 @@ namespace Ryujinx.Ava.Input
return new AvaloniaMouse(this);
}
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
public void Dispose()
{
if (_isDisposed)

View File

@@ -47,9 +47,9 @@ namespace Ryujinx.Ava
{
Version = ReleaseInformation.Version;
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
{
_ = MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 1803 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
_ = MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
}
PreviewerDetached = true;
@@ -136,8 +136,8 @@ namespace Ryujinx.Ava
// Logging system information.
PrintSystemInfo();
// Enable OGL multithreading on the driver, and some other flags.
DriverUtilities.InitDriverConfig(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
// Enable OGL multithreading on the driver, when available.
DriverUtilities.ToggleOGLThreading(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
// Check if keys exists.
if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))

View File

@@ -388,30 +388,6 @@ namespace Ryujinx.Ava.UI.Models.Input
}
}
private bool _enableLedChanging;
public bool EnableLedChanging
{
get => _enableLedChanging;
set
{
_enableLedChanging = value;
OnPropertyChanged();
}
}
private Color _ledColor;
public Color LedColor
{
get => _ledColor;
set
{
_ledColor = value;
OnPropertyChanged();
}
}
private bool _enableMotion;
public bool EnableMotion
{
@@ -433,6 +409,58 @@ namespace Ryujinx.Ava.UI.Models.Input
OnPropertyChanged();
}
}
private bool _enableLedChanging;
public bool EnableLedChanging
{
get => _enableLedChanging;
set
{
_enableLedChanging = value;
OnPropertyChanged();
}
}
public bool ShowLedColorPicker => !TurnOffLed && !UseRainbowLed;
private bool _turnOffLed;
public bool TurnOffLed
{
get => _turnOffLed;
set
{
_turnOffLed = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowLedColorPicker));
}
}
private bool _useRainbowLed;
public bool UseRainbowLed
{
get => _useRainbowLed;
set
{
_useRainbowLed = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowLedColorPicker));
}
}
private Color _ledColor;
public Color LedColor
{
get => _ledColor;
set
{
_ledColor = value;
OnPropertyChanged();
}
}
public GamepadInputConfig(InputConfig config)
{
@@ -512,6 +540,8 @@ namespace Ryujinx.Ava.UI.Models.Input
if (controllerInput.Led != null)
{
EnableLedChanging = controllerInput.Led.EnableLed;
TurnOffLed = controllerInput.Led.TurnOffLed;
UseRainbowLed = controllerInput.Led.UseRainbow;
uint rawColor = controllerInput.Led.LedColor;
byte alpha = (byte)(rawColor >> 24);
byte red = (byte)(rawColor >> 16);
@@ -579,6 +609,8 @@ namespace Ryujinx.Ava.UI.Models.Input
Led = new LedConfigController
{
EnableLed = EnableLedChanging,
TurnOffLed = this.TurnOffLed,
UseRainbow = UseRainbowLed,
LedColor = LedColor.ToUInt32()
},
Version = InputConfig.CurrentVersion,

View File

@@ -1,5 +1,8 @@
using Avalonia.Svg.Skia;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Views.Input;
@@ -57,6 +60,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
await RumbleInputView.Show(this);
}
public RelayCommand LedDisabledChanged => Commands.Create(() =>
{
if (!Config.EnableLedChanging) return;
if (Config.TurnOffLed)
ParentModel.SelectedGamepad.ClearLed();
else
ParentModel.SelectedGamepad.SetLed(Config.LedColor.ToUInt32());
});
public void OnParentModelChanged()
{
IsLeft = ParentModel.IsLeft;

View File

@@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Svg.Skia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers;
@@ -54,7 +55,18 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public IGamepadDriver AvaloniaKeyboardDriver { get; }
public IGamepad SelectedGamepad { get; private set; }
private IGamepad _selectedGamepad;
public IGamepad SelectedGamepad
{
get => _selectedGamepad;
private set
{
_selectedGamepad = value;
OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed));
}
}
public ObservableCollection<PlayerModel> PlayerIndexes { get; set; }
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
@@ -69,8 +81,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool IsRight { get; set; }
public bool IsLeft { get; set; }
public bool HasLed => false; //temporary
//SelectedGamepad.Features.HasFlag(GamepadFeaturesFlag.Led);
public bool HasLed => SelectedGamepad.Features.HasFlag(GamepadFeaturesFlag.Led);
public bool CanClearLed => SelectedGamepad.Name.ContainsIgnoreCase("DualSense");
public bool IsModified { get; set; }
public event Action NotifyChangesEvent;

View File

@@ -429,7 +429,7 @@
</StackPanel>
</StackPanel>
</Border>
<!-- Motion + Rumble -->
<!-- Motion, Rumble, LED -->
<StackPanel
Margin="0,10,0,0"
Spacing="5"
@@ -495,25 +495,47 @@
Margin="0,-1,0,0">
<Grid IsVisible="{Binding ParentModel.HasLed}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<CheckBox
Margin="10"
Margin="10, 10, 5, 10"
MinWidth="0"
Grid.Column="0"
IsChecked="{Binding Config.EnableLedChanging, Mode=TwoWay}">
<TextBlock Text="{ext:Locale ControllerSettingsLedColor}" />
</CheckBox>
<ui:ColorPickerButton
<CheckBox
Margin="5, 10, 5, 10"
MinWidth="0"
Grid.Column="1"
Margin="10"
IsVisible="{Binding ParentModel.CanClearLed}"
IsChecked="{Binding Config.TurnOffLed, Mode=TwoWay}"
Command="{Binding LedDisabledChanged}">
<TextBlock Text="{ext:Locale ControllerSettingsLedColorDisable}" />
</CheckBox>
<CheckBox
Margin="5, 10 5,10"
MinWidth="0"
Grid.Column="2"
IsEnabled="{Binding !Config.TurnOffLed}"
IsChecked="{Binding Config.UseRainbowLed, Mode=TwoWay}">
<TextBlock Text="{ext:Locale ControllerSettingsLedColorRainbow}" />
</CheckBox>
<ui:ColorPickerButton
Grid.Column="3"
IsEnabled="{Binding Config.ShowLedColorPicker}"
Margin="5, 10, 10, 10"
IsMoreButtonVisible="False"
UseColorPalette="False"
UseColorTriangle="False"
UseColorWheel="False"
ShowAcceptDismissButtons="False"
IsAlphaEnabled="False"
AttachedToVisualTree="ColorPickerButton_OnAttachedToVisualTree"
ColorChanged="ColorPickerButton_OnColorChanged"
Color="{Binding Config.LedColor, Mode=TwoWay}">
</ui:ColorPickerButton>
</Grid>

View File

@@ -4,11 +4,14 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System.Linq;
using StickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
namespace Ryujinx.Ava.UI.Views.Input
@@ -82,7 +85,7 @@ namespace Ryujinx.Ava.UI.Views.Input
private void Button_IsCheckedChanged(object sender, RoutedEventArgs e)
{
if (sender is ToggleButton button)
if (sender is ToggleButton button)
{
if (button.IsChecked is true)
{
@@ -103,7 +106,9 @@ namespace Ryujinx.Ava.UI.Views.Input
var viewModel = (DataContext as ControllerInputViewModel);
IKeyboard keyboard = (IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations.
IKeyboard keyboard =
(IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver
.GetGamepad("0"); // Open Avalonia keyboard for cancel operations.
IButtonAssigner assigner = CreateButtonAssigner(isStick);
_currentAssigner.ButtonAssigned += (sender, e) =>
@@ -231,8 +236,31 @@ namespace Ryujinx.Ava.UI.Views.Input
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
foreach (IGamepad gamepad in RyujinxApp.MainWindow.InputManager.GamepadDriver.GetGamepads())
{
gamepad?.ClearLed();
}
_currentAssigner?.Cancel();
_currentAssigner = null;
}
private void ColorPickerButton_OnColorChanged(ColorPickerButton sender, ColorButtonColorChangedEventArgs args)
{
if (!args.NewColor.HasValue) return;
if (DataContext is not ControllerInputViewModel cVm) return;
if (!cVm.Config.EnableLedChanging) return;
cVm.ParentModel.SelectedGamepad.SetLed(args.NewColor.Value.ToUInt32());
}
private void ColorPickerButton_OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (DataContext is not ControllerInputViewModel cVm) return;
if (!cVm.Config.EnableLedChanging) return;
cVm.ParentModel.SelectedGamepad.SetLed(cVm.Config.LedColor.ToUInt32());
}
}
}

View File

@@ -736,9 +736,7 @@ namespace Ryujinx.Ava.UI.Windows
});
}
private static bool _intelMacWarningShown = !(OperatingSystem.IsMacOS() &&
(RuntimeInformation.OSArchitecture == Architecture.X64 ||
RuntimeInformation.OSArchitecture == Architecture.X86));
private static bool _intelMacWarningShown = !RunningPlatform.IsIntelMac;
public static async Task ShowIntelMacWarningAsync()
{

View File

@@ -4,9 +4,14 @@ using Avalonia.Input;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.HLE.FileSystem;
using Ryujinx.Input;
using System;
using System.Linq;
using Key = Avalonia.Input.Key;
namespace Ryujinx.Ava.UI.Windows
{
@@ -106,6 +111,12 @@ namespace Ryujinx.Ava.UI.Windows
protected override void OnClosing(WindowClosingEventArgs e)
{
HotkeysPage.Dispose();
foreach (IGamepad gamepad in RyujinxApp.MainWindow.InputManager.GamepadDriver.GetGamepads())
{
gamepad?.ClearLed();
}
InputPage.Dispose();
base.OnClosing(e);
}

View File

@@ -118,6 +118,8 @@ namespace Ryujinx.Ava
OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion));
}
}
Logger.Info?.Print(LogClass.Application, "Up to date.");
_running = false;
@@ -188,6 +190,8 @@ namespace Ryujinx.Ava
OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion));
}
}
Logger.Info?.Print(LogClass.Application, "Up to date.");
_running = false;

View File

@@ -1,3 +1,4 @@
using Avalonia.Media;
using Gommon;
using Ryujinx.Ava.Utilities.Configuration.System;
using Ryujinx.Ava.Utilities.Configuration.UI;
@@ -421,7 +422,9 @@ namespace Ryujinx.Ava.Utilities.Configuration
config.Led = new LedConfigController
{
EnableLed = false,
LedColor = 328189
TurnOffLed = false,
UseRainbow = false,
LedColor = new Color(255, 5, 1, 253).ToUInt32()
};
}
})